/* ClientBase · design tokens */

/* ============================================================
   TYPOGRAPHIE · Coolvetica (titres) + Geist (corps)
   ============================================================
   C'EST LA TYPOGRAPHIE D'ORIGINE DU SITE. Ne pas la remplacer sans
   demande explicite de Noah, elle a déjà fait l'aller-retour deux fois.

   HISTORIQUE, POUR QUE PERSONNE NE REFASSE L'ERREUR
   Le 26 juillet 2026, les titres sont passés en Manrope, puis en Gabarito
   le 27, dans le cadre de l'alignement sur la DA de PrépaEntretien. Le
   même jour j'ai cru revenir « à l'origine » en mettant tout en Geist :
   c'était faux. Geist n'a jamais été que la police de CORPS. Les titres
   du site ont toujours été en Coolvetica. Le 27 juillet au soir, Noah a
   demandé l'état d'avant le 26 : c'est celui-ci.

   RÉPARTITION
   · Titres, chiffres premium, topbar de l'espace pro → Coolvetica
     (auto-hébergée, app/assets/fonts/, un seul poids : Regular 400)
   · Corps, interface, boutons, formulaires → Geist (Google Fonts, variable)
   · Logo → Geist (demande explicite : ne jamais changer la police du logo)
   · Code / valeurs monospace → Geist Mono

   POURQUOI LES TITRES SONT EN POIDS 400
   Coolvetica ne possède qu'un Regular. Un `font-weight: 700` sur un titre
   ferait fabriquer au navigateur un faux-gras baveux. D'où
   `font-synthesis-weight: none` ci-dessous : les titres restent fins même
   si un font-weight élevé traîne dans un style inline. C'est voulu, c'est
   la DA du site, ce n'est pas un bug à « corriger ».

   DEUX FICHIERS CHARGÉS SUR CINQ
   L'original chargeait les 5 .otf (~330 Ko). Les variantes Condensed,
   Crammed et Heavy ont ZÉRO appel dans tout le code (vérifié) : seuls le
   Regular et son Italique sont déclarés ici. Les 3 autres restent sur le
   disque au cas où. Économie : ~198 Ko par visite, à rendu identique.
   ============================================================ */
@font-face {
  font-family: "Coolvetica";
  src: url("/app/assets/fonts/Coolvetica_Rg.otf") format("opentype");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Coolvetica";
  src: url("/app/assets/fonts/Coolvetica_Rg_It.otf") format("opentype");
  font-weight: 400; font-style: italic; font-display: swap;
}

/* --- POLICES HÉBERGÉES CHEZ NOUS · 25 août 2026 --- */
/* Servies depuis /app/assets/fonts/ au lieu de fonts.gstatic.com.
   Ce que ça supprime : deux connexions à des serveurs tiers (DNS +
   TLS) sur le chemin critique de l'affichage, sur CHAQUE page.

   `unicode-range` n'est pas décoratif : c'est lui qui fait que le
   navigateur ignore le fichier « latin-ext » tant qu'aucun caractère
   de cette plage n'apparaît. Sur un site français, il ne le charge
   presque jamais.

   Geist est une police VARIABLE : un seul fichier couvre les poids
   300 à 700. C'est pour ça qu'il n'y a que deux fichiers Geist pour
   cinq graisses utilisées.

   Licence : SIL Open Font License, texte dans app/assets/fonts/. */
@font-face {
  font-family: "Geist";
  src: url("/app/assets/fonts/geist-latin-ext-300-700.woff2") format("woff2");
  font-weight: 300 700; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Geist";
  src: url("/app/assets/fonts/geist-latin-300-700.woff2") format("woff2");
  font-weight: 300 700; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Sora";
  src: url("/app/assets/fonts/sora-latin-ext-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Sora";
  src: url("/app/assets/fonts/sora-latin-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Sora";
  src: url("/app/assets/fonts/sora-latin-ext-600.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Sora";
  src: url("/app/assets/fonts/sora-latin-600.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Geist Mono";
  src: url("/app/assets/fonts/geist-mono-latin-ext-400-600.woff2") format("woff2");
  font-weight: 400 600; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Geist Mono";
  src: url("/app/assets/fonts/geist-mono-latin-400-600.woff2") format("woff2");
  font-weight: 400 600; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* --- FIN DES POLICES HÉBERGÉES --- */





/* Coupe le faux-gras de synthèse (cf. l'explication ci-dessus). Geist étant
   une police variable à vrais poids, elle n'est pas concernée : son gras
   réel continue de s'afficher normalement. */
html { font-synthesis-weight: none; }

:root {
  /* Color · neutrals (warm-tinted, very low chroma) */
  --bg: #FFFFFF;
  --bg-alt: oklch(97.5% 0.003 260);
  /* Gris de démarcation · le fond des sections paires de l'accueil et du
     pied de page. Neutre exprès : Noah, 1er août 2026, « un gris très clair
     plutôt qu'un bleu ». Deux points de luminosité sous le blanc, c'est ce
     qui se voit sans jamais faire tache. */
  --bg-gris: oklch(96.2% 0.002 260);
  --warn-soft:   oklch(96% 0.025 55);
  --warn-soft-2: oklch(90% 0.05 55);
  --warn-ink:    oklch(45% 0.14 55);
  --bg-section: oklch(98% 0.004 260);
  --surface: #ffffff;
  --ink: oklch(18% 0.012 260);
  --ink-2: oklch(32% 0.015 260);
  --ink-3: oklch(48% 0.012 260);
  --ink-4: oklch(65% 0.01 260);
  --line: oklch(91% 0.005 80);
  --line-strong: oklch(84% 0.007 80);

  /* Accent · ClientBase indigo (matches logo #5A50E6) */
  --accent: oklch(55% 0.22 278);
  --accent-ink: oklch(42% 0.22 278);
  --accent-soft: oklch(96% 0.035 278);
  --accent-soft-2: oklch(92% 0.06 278);
  --accent-strong: #3C2FBD;               /* indigo profond (theme-color, dégradés) */

  /* Status · succès / étoiles / destructif (alignés sur la DA) */
  --success:     #10B981;
  --gold:        #F59E0B;                  /* ★ notes */
  --danger:      oklch(55% 0.18 25);
  --danger-line: oklch(86% 0.06 25);

  /* =========================================================
     PALETTE VITRINE CANONIQUE (2026-05-16)
     -----------------------------------------------------------
     3 couleurs uniquement sur le site marketing : INDIGO (--accent),
     ROSE POUDRÉ (--rose), SAUGE (--sage). Toujours préférer la version
     -soft pour les fonds / hovers. Les neutres (--ink, --line) restent
     pour le texte et la structure. NE PAS ré-introduire teal/orange/
     ambre/lavande dans la vitrine · ces tokens sont conservés pour
     l'espace pro (cartes, badges modules) mais sont marqués legacy.
     =========================================================  */

  /* Sauge · accent calme, naturel. Légèrement éclairci par rapport à l'ancienne valeur
     pour bien lire en contraste avec l'indigo et passer en pastel. */
  --sage: oklch(78% 0.08 160);
  --sage-ink: oklch(40% 0.10 160);
  --sage-soft: oklch(96% 0.025 160);
  --sage-soft-2: oklch(92% 0.05 160);

  --warn: oklch(70% 0.13 55);
  --warn-soft: oklch(96% 0.03 55);

  /* Rose poudré · chaud, doux, accueillant. Désaturé volontairement pour
     éviter le bonbon. Utilisé en accent secondaire et comme couleur
     dominante sur la vitrine client. */
  --rose:        oklch(76% 0.10 12);
  --rose-ink:    oklch(45% 0.12 12);
  --rose-soft:   oklch(97% 0.025 12);
  --rose-soft-2: oklch(93% 0.05 12);

  /* === Legacy · gardés pour compatibilité avec l'espace pro, à NE PAS
     utiliser dans les pages vitrine (home-hero, home-rest, pages-a, pages-b). === */
  --teal:        oklch(55% 0.15 200);
  --teal-ink:    oklch(40% 0.12 200);
  --teal-soft:   oklch(95% 0.04 200);
  --teal-soft-2: oklch(91% 0.07 200);
  --orange:      oklch(64% 0.17 35);
  --orange-ink:  oklch(45% 0.16 35);
  --orange-soft: oklch(96% 0.04 35);
  --orange-soft-2: oklch(92% 0.07 35);
  --ambre:       oklch(72% 0.14 75);
  --ambre-ink:   oklch(50% 0.14 75);
  --ambre-soft:  oklch(96% 0.04 75);
  --ambre-soft-2: oklch(93% 0.08 75);
  --lavande:     oklch(58% 0.16 290);
  --lavande-ink: oklch(45% 0.14 290);
  --lavande-soft: oklch(96% 0.04 290);
  --lavande-soft-2: oklch(92% 0.07 290);

  /* Vitrine · audience switch : 2 teintes voisines pour rester dans la même
     famille (cohérence forte) tout en différenciant l'audience.
     Pro = indigo de marque (oklch hue 278), Client = violet clair (hue 295)
     juste à côté dans la roue chromatique · distinct mais cousin.
     Les neutres et le rose/sauge restent disponibles pour les accents. */
  --cli-accent:   oklch(55% 0.22 295);   /* violet client · aligné DA (consumer) */
  --cli-ink:      oklch(45% 0.18 295);
  --cli-soft:     oklch(96% 0.05 295);
  --cli-soft-2:   oklch(92% 0.06 295);
  --v-pro-accent: var(--accent);
  --v-pro-soft:   var(--accent-soft);
  --v-pro-ink:    var(--accent-ink);
  --v-cli-accent: var(--cli-accent);
  --v-cli-soft:   var(--cli-soft);
  --v-cli-ink:    var(--cli-ink);

  /* Type · Titres = Coolvetica, corps = Geist. Cf. l'en-tête de ce fichier
     pour la répartition complète et l'historique. */
  --ff-display: "Coolvetica", "Geist", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", system-ui, sans-serif;
  /* Déclinaisons Condensed / Crammed / Heavy : zéro appel dans le code, et
     les .otf correspondants ne sont plus chargés (cf. en-tête). Ces trois
     tokens retombent donc sur le Regular. Gardés pour qu'un appel oublié
     quelque part ne casse rien. */
  --ff-display-cond: var(--ff-display);
  --ff-display-cram: var(--ff-display);
  --ff-display-comp: var(--ff-display);
  --ff-text: "Geist", -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;
  --ff-mono: "Geist Mono", "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* --ff-adresse · L'ADRESSE DE RÉSERVATION D'UN PRO, ses liens, ses codes
     et les heures affichées à ses clients. C'était --ff-mono (Geist Mono).
     Noah, 1er août 2026 : « je n'aime pas cette police d'écriture et je veux
     plus l'avoir », sur la vitrine, dans l'espace pro, et sur les heures de
     la carte « Votre journée ». C'est donc Geist, la police du site, avec des
     chiffres à chasse fixe là où des heures doivent s'aligner
     (font-variant-numeric: tabular-nums, à poser au cas par cas).
     --ff-mono reste, mais UNIQUEMENT pour ce qui est vraiment technique :
     messages d'erreur bruts, blocs de journal, badges de raccourci clavier. */
  --ff-adresse: var(--ff-text);
  /* Chiffres · SORA, et enfin réellement chargée.
     Ce token a menti deux fois. Il annonçait Sora depuis le début alors que
     Sora n'était PAS dans index.html : il retombait donc sur Geist gras. Je
     l'ai ensuite basculé sur Coolvetica, qui a l'inconvénient d'être
     condensée : un « 217 € » y devient étroit et se confond avec un titre.
     Sora est maintenant chargée pour de bon, avec deux graisses.

     POURQUOI UNE POLICE DÉDIÉE AUX CHIFFRES, ET PAS CELLE DES TITRES
     Un compteur (« 14 pros actifs »), un montant (« 217 € »), une
     statistique : ce ne sont ni des titres ni du texte courant. Ils doivent se
     lire d'un coup d'œil, s'aligner en colonne, et ne jamais laisser hésiter
     entre un 1 et un 7 ou entre un 0 et un O. Sora est géométrique, ses
     chiffres sont larges et ouverts, et elle a de vraies figures tabulaires.
     Elle ne sert QUE là : ne pas l'utiliser pour du texte. */
  --ff-num: "Sora", "Geist", -apple-system, "Segoe UI", system-ui, sans-serif;
  /* Alias historique, 14 appels dans l'espace pro. Redevient bien
     Coolvetica, comme son nom l'annonce. */
  --ff-coolvetica: var(--ff-display);
  /* Logo wordmark · conservé en Geist (demande explicite : ne pas changer la police du logo). */
  --ff-logo: "Geist", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", system-ui, sans-serif;

  /* --ff-hand · ECRITURE A LA MAIN (26 aout 2026).
     Un seul usage aujourd'hui : les deux mots manuscrits du titre d'accueil
     (« cahier, » et « numérique. »). C'est Caveat BRUSH, la version au feutre
     épais : la Caveat ordinaire, même en 700, rendait un trait si fin qu'à
     côté de Coolvetica le mot manuscrit s'effaçait. Ne PAS l'utiliser pour du
     texte courant : une manuscrite se lit mal au-delà de trois mots. */
  --ff-hand: "Caveat Brush", "Segoe Script", "Bradley Hand", "Brush Script MT", cursive;



  /* Space */
  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 24px; --s-6: 32px; --s-7: 48px; --s-8: 64px;
  --s-9: 96px; --s-10: 128px;

  /* Radius · coins très arrondis (DA) : cartes 16-24, images 22-28, pilules 999 */
  --r-1: 6px; --r-2: 10px; --r-3: 14px; --r-4: 20px;
  --r-card: 18px; --r-card-lg: 24px; --r-pill: 999px;

  /* Shadow · barely-there */
  --sh-1: 0 1px 2px rgba(15, 18, 30, 0.04), 0 1px 1px rgba(15, 18, 30, 0.02);
  --sh-2: 0 6px 18px -6px rgba(15, 18, 30, 0.10), 0 2px 4px rgba(15, 18, 30, 0.04);
  --sh-3: 0 24px 48px -18px rgba(15, 18, 30, 0.18), 0 6px 14px rgba(15, 18, 30, 0.05);

  --container: 1200px;
}

/* Warm theme variant */
:root[data-theme="warm"] {
  --bg: oklch(98.2% 0.012 70);
  --bg-alt: oklch(95.8% 0.02 70);
  --bg-gris: oklch(95.4% 0.012 70);
  --bg-section: oklch(96.8% 0.015 70);
  --accent: oklch(58% 0.13 40);
  --accent-ink: oklch(42% 0.13 40);
  --accent-soft: oklch(96% 0.035 40);
  --accent-soft-2: oklch(92% 0.06 40);
}

/* Dark theme variant */
:root[data-theme="dark"] {
  --bg: oklch(16% 0.012 260);
  --bg-alt: oklch(20% 0.014 260);
  --bg-gris: oklch(19% 0.012 260);
  --bg-section: oklch(18% 0.013 260);
  --surface: oklch(22% 0.015 260);
  --ink: oklch(98% 0.005 80);
  --ink-2: oklch(88% 0.006 80);
  --ink-3: oklch(70% 0.008 80);
  --ink-4: oklch(55% 0.01 80);
  --line: oklch(28% 0.015 260);
  --line-strong: oklch(38% 0.02 260);
  --accent: oklch(72% 0.16 268);
  --accent-ink: oklch(82% 0.14 268);
  --accent-soft: oklch(28% 0.08 268);
  --accent-soft-2: oklch(34% 0.12 268);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { margin: 0; padding: 0; }

/* ============================================================
   LE HAUT DE PAGE SOUS L'ENCOCHE
   ============================================================
   Deux corrections successives, le 2 puis le 4 septembre 2026, et la
   seconde annule la première. Les deux sont racontées ici : la
   première était raisonnable, et c'est pour ça qu'il faut dire
   pourquoi elle ne pouvait pas marcher.

   ------------------------------------------------------------
   LE PROBLÈME
   ------------------------------------------------------------
   Noah, cinq fois en quatre jours : « il y a toujours une barre
   blanche en haut de ClientBase, sur mobile », « elle est
   absolument partout, espace pro, espace client, site vitrine ».

   Le site déclare `apple-mobile-web-app-capable` et
   `viewport-fit=cover` : installé depuis l'écran d'accueil, il se
   dessine SOUS l'encoche, jusqu'au bord physique. Les soixante
   premiers pixels de la fenêtre appartiennent donc à la page, et
   c'est elle qui doit les peindre.

   ------------------------------------------------------------
   LA PREMIÈRE RÉPONSE, ET POURQUOI ELLE A EMPIRÉ LES CHOSES
   ------------------------------------------------------------
   Une bande `body::before` couvrait cette zone, peinte de la
   « couleur qui ouvre la page » : #FDFDFF, mesurée sur la première
   ligne de pixels.

   Le raisonnement supposait que la page ouvre sur UNE couleur. Elle
   n'ouvre pas sur une couleur : elle ouvre sur un HALO. Mesuré au
   pixel sur la page d'accueil, avec l'encoche simulée, la première
   ligne va de (255,255,255) à gauche à (234,233,252) à droite -
   vingt points d'écart d'un bord à l'autre. Aucun aplat ne peut
   égaler un dégradé.

   La bande APLATISSAIT donc les soixante premiers pixels en un blanc
   uniforme, effaçant le halo violet qui continue juste en dessous.
   C'est exactement ce que Noah voyait, et c'est pour ça que la
   correction censée faire disparaître la barre l'a rendue plus nette
   qu'avant : avant, le dégradé continuait, il n'y avait pas de barre
   du tout à cet endroit.

   ------------------------------------------------------------
   LA RÉPONSE : PAS DE BANDE DU TOUT
   ------------------------------------------------------------
   La zone d'encoche se peint toute seule, pour une raison simple :
   les surfaces qui ouvrent les pages y montent déjà.
   · la vitrine, `.cbm-hero-sec` remonte de `-127px` et commence à
     l'ordonnée zéro : son cahier, son halo et sa papeterie occupent
     la bande ;
   · les espaces, `.e-bg` est `fixed; inset: 0` ;
   · l'écran d'entrée, `.cb-auth-fond`, même chose ;
   · les autres pages de la vitrine ouvrent sur le fond du corps de
     page, qui couvre la fenêtre entière.

   C'est le modèle de bynoah, que Noah donne en exemple : là-bas le
   bandeau coloré EST le haut de la page, avec son
   `env(safe-area-inset-top)` en rembourrage. Il n'y a jamais eu de
   bande séparée à recaler.

   ⚠️ NE PAS REMETTRE DE BANDE ICI. Toute couleur unique posée sur
   cette zone se verra, parce qu'aucune des quatre surfaces ci-dessus
   n'est unie. Si une page future ouvrait sur du vide, c'est À ELLE
   de monter dans la zone sûre, pas à une bande globale de la couvrir.

   CE QUI RESTE DE LA PREMIÈRE CORRECTION, et qui était juste :
   · `apple-mobile-web-app-status-bar-style: black-translucent` dans
     l'en-tête, sans lui, iOS dessine lui-même une barre d'état
     opaque qu'aucune règle de la page ne peut atteindre ;
   · `color-scheme: light`, sans lui, iOS peut écrire l'heure en
     blanc sur une page claire, et la barre d'état devient illisible ;
   · `--cb-haut-de-page`, que `cbAccorderLaBarreDuHaut` (shared.jsx)
     lit encore pour annoncer `theme-color` sur la vitrine.
   ============================================================ */
:root {
  color-scheme: light;
  /* ⚠️ DEUX VARIABLES, ET IL EN FAUT DEUX.
     · `--cb-fond-site` est l'ENTRÉE : la couleur d'ouverture de la
       vitrine, mesurée sur sa première ligne de pixels. Les pages
       comparatives et les pages SEO la redéfinissent pour la leur.
     · `--cb-haut-de-page` est la SORTIE : ce que `html` et `body`
       peignent, et ce que `cbAccorderLaBarreDuHaut` écrit sur la racine
       à chaque écran.

     Les confondre crée une boucle : la sonde relirait la valeur qu'elle
     vient d'écrire, et la vitrine garderait le lavande d'un espace
     visité juste avant. */
  --cb-fond-site: #FDFDFF;
  --cb-haut-de-page: var(--cb-fond-site);
}

/* ============================================================
   LE CANEVAS · la surface derrière le document
   ------------------------------------------------------------
   Le navigateur étend une couleur AU-DELÀ de la page : derrière la
   barre d'outils quand elle se replie, et dans le rebond élastique
   quand on tire la page vers le bas. Elle n'est pas prise sur
   `theme-color`, elle est prise sur `<html>`, et à défaut sur
   `<body>`, qui vaut #FFFFFF.

   Dans les espaces, tout le lavande est peint par `.e-bg`, un enfant
   en `position: fixed`. Superbe dans la page, invisible pour le
   canevas : le navigateur avait donc toujours du blanc à étaler
   au-dessus. C'est la barre blanche que Noah signale depuis une
   semaine, et c'est aussi pourquoi elle revenait au moindre geste de
   défilement alors qu'elle semblait réglée à l'arrêt.

   `cbAccorderLaBarreDuHaut` (shared.jsx) pose la couleur EXACTE sur
   `<html>` dès que l'écran est monté. Les deux règles ci-dessous ne
   servent qu'à la PREMIÈRE IMAGE, avant que le script ait tourné :
   sans elles, on verrait un éclair blanc au chargement. Leur valeur
   est approchée exprès, l'exacte dépend de la couleur d'accent, que
   seul le document connaît.
   ============================================================ */
/* ============================================================
   ⚠️ `html` ET `body` PORTENT LA MÊME COULEUR · 5 septembre 2026
   ------------------------------------------------------------
   La leçon vient de PrépaEntretien, où Noah a vécu exactement le même
   feuilleton, et où elle est écrite mot pour mot :

     « Peindre `html` sans peindre `body`, c'est repeindre un mur
       derrière une armoire. »

   Le `body` s'étend sur toute la hauteur de la fenêtre. Tant qu'il vaut
   `--bg`, c'est-à-dire #FFFFFF, il RECOUVRE la couleur posée sur `html`
   - y compris dans les deux seules zones qui nous intéressent : la barre
   d'état du système, et l'espace qu'on découvre en tirant la page vers le
   bas. J'ai peint `html` le 5 septembre au matin, mesuré `body` à
   rgb(255,255,255) dans l'audit du même jour, et écarté l'observation en
   me disant que le `body` était de toute façon couvert par les surfaces
   de la page. Il l'est DANS la page. Il ne l'est pas au-dessus d'elle.

   ⚠️ ET AUCUNE CAPTURE NE POUVAIT LE MONTRER, c'est l'autre leçon de
   PrépaEntretien. Une capture lit des pixels DANS la page ; ce blanc-là
   n'apparaît qu'en dehors. Six passes de mesures « correctes ».

   UNE SEULE SOURCE, `--cb-haut-de-page`, lue par `html`, par `body` et
   par `theme-color`. Trois copies d'une couleur finissent toujours par
   ne plus dire la même chose.

   Les deux règles `:has()` ci-dessous ne servent qu'à la PREMIÈRE IMAGE,
   avant que le script de tête n'ait tourné : elles posent la VARIABLE, pas
   le fond, pour que `html` et `body` bougent ensemble.
   ============================================================ */
html { background: var(--cb-haut-de-page); }
html:has(.cb-espace) { --cb-haut-de-page: #F1EEFF; }
html:has(.cb-auth-section) { --cb-haut-de-page: #ECEFFF; }

/* ⚠️ UN SEUL ÉLÉMENT ÉCARTE L'ENCOCHE, ET C'EST LE PREMIER DE LA PAGE.
   `.cb-nav-flottant` ajoute `env(safe-area-inset-top)` à sa marge et à son
   `top`, parce qu'elle est presque toujours la première chose de la page.
   Quand la bande d'offre est affichée, elle passe devant : la bande se
   retrouve alors SOUS la Dynamic Island (qui recouvre le cadeau et le
   début du texte), et la barre ajoute quand même son écart, soit
   soixante pixels de vide entre les deux.

   L'écart passe donc à la bande, et la barre le rend. `:has()` interroge
   dans le bon sens : le `body` ne peut pas savoir autrement qu'une bande
   existe plus bas. Le `!important` n'est pas de la paresse, les deux
   valeurs de la barre sont écrites en style EN LIGNE (shared.jsx), et
   rien d'autre ne peut les battre depuis une feuille de style. */
body:has(.cb-offre-bande) .cb-nav-flottant {
  top: 14px !important;
  margin-top: 14px !important;
}
/* Le même écart que la règle de `app/index.html` à cette largeur : la barre
   se resserre sur téléphone, et elle doit garder cette valeur ici aussi. */
@media (max-width: 720px) {
  body:has(.cb-offre-bande) .cb-nav-flottant {
    top: 10px !important;
    margin-top: 10px !important;
  }
}

/* Touch feedback without the grey rectangle flash */
button, a, [role="button"], .btn {
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
body {
  /* ⚠️ `--cb-haut-de-page` ET NON `--bg`. Voir la note « html ET body
     portent la même couleur » plus haut : un `body` blanc recouvre ce
     qu'on peint sur `html`, et rend la correction invisible là où elle
     devait agir. Les deux valeurs ne diffèrent que de deux points sur la
     vitrine (#FDFDFF contre #FFFFFF) ; dans les espaces, elles diffèrent
     de tout. */
  background: var(--cb-haut-de-page);
  color: var(--ink);
  font-family: var(--ff-text);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  letter-spacing: -0.005em;
}

/* Titres = Coolvetica au poids naturel (400) + tracking serré.

   LE POIDS 400 EST VOLONTAIRE, PAS UN OUBLI. Coolvetica ne possède que ce
   poids ; demander 700 ne donnerait qu'un faux-gras, qu'on désactive de
   toute façon (font-synthesis-weight, en haut du fichier). La DA du site
   repose sur des titres GRANDS et FINS, pas sur du gras.

   LES TAILLES SONT PLUS GÉNÉREUSES QU'AVEC UNE POLICE NORMALE (72 px sur
   h1) parce que Coolvetica est condensée : à taille égale elle occupe
   moins de largeur. Si un jour la police change, il faut redescendre ces
   tailles, sinon les titres débordent. C'est exactement ce qui s'est passé
   les 26 et 27 juillet. */
h1, h2, h3, h4 {
  font-family: var(--ff-display);
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin: 0;
  text-wrap: balance;
}
h1 { font-size: clamp(44px, 5.2vw, 72px); line-height: 0.98; letter-spacing: -0.015em; font-weight: 400; }
h2 { font-size: clamp(32px, 3.4vw, 46px); line-height: 1.0;  letter-spacing: -0.012em; font-weight: 400; }
h3 { font-size: 22px; line-height: 1.2; letter-spacing: -0.006em; font-weight: 400; }
h4 { font-size: 17px; line-height: 1.3; letter-spacing: 0; font-weight: 400; }
p  { margin: 0; color: var(--ink-2); text-wrap: pretty; }
a  { color: inherit; text-decoration: none; }

/* Prevent iOS Safari from zooming into focused form fields.
   iOS triggers the zoom when the input's computed font-size is < 16px. */
@media (max-width: 768px) {
  input, select, textarea {
    font-size: 16px !important;
  }
}

.eyebrow {
  font-size: 13px;
  color: var(--accent-ink);
  font-weight: 580;
  letter-spacing: -0.005em;
}
.muted { color: var(--ink-3); }
.container {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 24px;
}

/* Animation du dot pulsant utilisée par les boutons « Voir la démo »
   (vitrine pro et vitrine client). Définie globalement pour rester
   accessible quelle que soit la page rendue. */
@keyframes cbDemoDot {
  0%, 100% { box-shadow: 0 0 0 3px color-mix(in oklab, var(--sage) 25%, transparent); }
  50%      { box-shadow: 0 0 0 6px color-mix(in oklab, var(--sage) 10%, transparent); }
}

/* Bouton « Voir la démo » · style partagé vitrine pro + vitrine client.
   Défini globalement pour que le hover marche sur toutes les pages. */
.cb-demo-btn {
  transition: transform .14s ease, box-shadow .2s ease, background .15s ease;
}
.cb-demo-btn:hover {
  transform: translateY(-2px);
  background: color-mix(in oklab, var(--sage) 22%, var(--surface)) !important;
  box-shadow: 0 14px 30px -12px color-mix(in oklab, var(--sage) 70%, transparent) !important;
}

/* Global smooth scroll + focus ring */
html {
  scroll-behavior: smooth;
  /* LA PLACE DE LA BARRE DE DÉFILEMENT EST TOUJOURS RÉSERVÉE · 27 juillet 2026.

     Noah : « quand on passe de Accueil à Rendez-vous dans le compte client,
     j'ai l'impression que la page s'élargit légèrement ».

     Ce n'était pas une impression, et ce n'était pas la mise en page : j'ai
     mesuré les deux écrans, tous les conteneurs font exactement la même
     largeur (1440 et 1376 px, identiques). La différence est la HAUTEUR -
     Accueil tient dans la fenêtre (900 px), Rendez-vous non (1478 px). La
     barre de défilement apparaît donc en arrivant sur Rendez-vous, prend une
     quinzaine de pixels, et tout le contenu centré se décale. C'est le
     mouvement qu'il voyait.

     `scrollbar-gutter: stable` réserve la gouttière en permanence : la
     largeur utile ne change plus jamais d'un écran à l'autre, sur tout le
     site et pas seulement ici. Le coût est une bande de 15 px sur les pages
     qui ne défilent pas ; le bénéfice est qu'aucune navigation ne fait plus
     sauter la mise en page.

     ⚠️ RENFORCÉ LE 8 SEPTEMBRE 2026 : le même symptôme, au mot près
     (« léger décalage » en passant d'Accueil à Agenda, côté pro cette
     fois), est réapparu malgré cette règle. `scrollbar-gutter` n'est
     soutenu que depuis 2021-2023 selon les navigateurs (Safari en
     particulier l'a supporté tardivement) : sur un navigateur qui ne le
     connaît pas, cette ligne ne fait simplement rien, en silence, sans
     avertissement dans la console. `overflow-y: scroll` est la version
     universelle du même geste, reconnue depuis toujours par tous les
     navigateurs : elle force la barre de défilement à toujours exister
     (même sur une page qui tient dans l'écran), donc la largeur utile ne
     bouge plus jamais, peu importe le navigateur. Les deux lignes
     coexistent sans se gêner, la seconde est un filet, pas un remplacement. */
  scrollbar-gutter: stable;
  overflow-y: scroll;
}
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
  transition: outline-offset .15s ease;
}

/* Liens cliquables dans le texte */
a { transition: color .15s ease, opacity .15s ease; }
a:hover { opacity: 0.82; }

/* Inputs · focus animé */
input, select, textarea {
  transition: border-color .18s ease, box-shadow .18s ease, background .18s ease;
}
input:focus, select:focus, textarea:focus {
  border-color: var(--accent) !important;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 18%, transparent);
}

/* Buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  height: 44px; padding: 0 20px;
  border-radius: var(--r-pill, 999px);
  /* LIBELLÉS DE BOUTONS · POLICE DE TEXTE, PAS LA DISPLAY.

     J'avais mis Coolvetica sur tous les boutons le matin du 27 juillet, en
     me disant qu'un libellé de bouton est un petit titre. Noah a tranché le
     soir même : « je ne veux plus que dans les boutons ce soit celle-là ».

     Il a raison et l'argument est simple : un bouton se CLIQUE, il ne se
     contemple pas. Une display condensée à graisse unique y gagne en
     caractère ce qu'elle perd en lisibilité immédiate, et sur un libellé
     long (« Créer mon compte gratuit », « Voir la démo client ») la
     condensation devient franchement gênante.

     La police d'affichage reste où elle a un sens : les titres et les
     grands chiffres. NE PAS LA REMETTRE SUR LES BOUTONS. */
  font-family: var(--ff-text);
  font-size: 15px; font-weight: 600;
  letter-spacing: -0.005em;
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform .22s cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow .22s cubic-bezier(0.22, 1, 0.36, 1),
              background-color .18s ease,
              border-color .18s ease,
              color .18s ease,
              opacity .18s ease;
  white-space: nowrap;
  will-change: transform;
}
.btn:active { transform: translateY(1px) scale(0.98); transition-duration: .08s; }
.btn:disabled { cursor: not-allowed; opacity: 0.6; }
.btn-primary {
  background: var(--ink);
  color: var(--bg);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: var(--sh-2); }
.btn-accent {
  background: var(--accent);
  color: white;
}
.btn-accent:hover { transform: translateY(-2px); box-shadow: 0 14px 32px -10px var(--accent); }
.btn-ghost {
  background: transparent; color: var(--ink);
  border-color: var(--line-strong);
}
.btn-ghost:hover { background: var(--bg-alt); border-color: var(--ink-4); transform: translateY(-1px); }
.cb-btn-destructive { color: var(--danger); border-color: var(--danger-line); margin-right: auto; }
.cb-btn-destructive:hover { background: oklch(97% 0.025 25); border-color: oklch(70% 0.16 25); color: oklch(48% 0.20 25); transform: translateY(-1px); }
/* Bouton plein destructif · 31 juillet 2026.
   Les écrans « réinitialiser » et « supprimer mon compte » se peignaient
   chacun leur rouge à la main, avec quatre valeurs oklch écrites dans le
   JSX et introuvables ailleurs. Résultat : deux rouges légèrement
   différents, aucun des deux aligné sur --danger, et un écran qui ne
   ressemblait plus au reste du site. Une classe, les tokens de la DA, et
   les mêmes gestes de survol que les autres boutons. */
.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 14px 32px -12px var(--danger); }
.btn-danger:disabled { background: var(--bg-alt); color: var(--ink-4); }
.btn-lg { height: 52px; padding: 0 26px; font-size: 15.5px; border-radius: var(--r-pill, 999px); }
/* Petits boutons de la vitrine : Geist, pas la display condensée.
   Même raison que .e-btn-sm dans espace.css, un petit bouton est une
   étiquette, pas un titre, et une condensée à graisse unique y perd en
   lisibilité. Les gros boutons gardent la display. */
.btn-sm {
  height: 36px; padding: 0 16px; border-radius: var(--r-pill, 999px);
  font-family: var(--ff-text);
  font-size: 13.5px; font-weight: 600; letter-spacing: -0.005em;
}

/* Utility */
.hstack { display: flex; align-items: center; gap: var(--s-3); }
.vstack { display: flex; flex-direction: column; gap: var(--s-3); }
.grid   { display: grid; gap: var(--s-5); }

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card, 18px);
  transition: transform .3s cubic-bezier(0.22, 1, 0.36, 1),
              box-shadow .3s cubic-bezier(0.22, 1, 0.36, 1),
              border-color .2s ease;
}

/* Liens de navigation · l'état actif est géré en JSX (pill background) */

/* Transition de page · fade-in léger au changement de route */
@keyframes cbPageIn {
  from { opacity: 0; transform: translateY(6px) scale(0.995); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
main[data-screen-label] {
  animation: cbPageIn .32s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Mobile · press feedback sur tous les éléments tactiles */
@media (hover: none) and (pointer: coarse) {
  .btn:active,
  button:active,
  [role="button"]:active {
    transform: scale(0.97);
    transition-duration: 0.08s;
  }
}

/* Respecter la préférence système "reduce motion" */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

.section { padding: var(--s-10) 0; }
.section-sm { padding: var(--s-8) 0; }

.badge {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 10px 5px 8px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: 12.5px; font-weight: 500;
  border: 1px solid var(--accent-soft-2);
}
.badge .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }

.kbd {
  font-family: var(--ff-mono);
  font-size: 11.5px;
  padding: 2px 6px;
  border: 1px solid var(--line-strong);
  border-bottom-width: 2px;
  border-radius: 5px;
  background: var(--surface);
  color: var(--ink-2);
}

/* Divider line */
.hr { height: 1px; background: var(--line); border: 0; margin: 0; }

/* Focus */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* Scrollbar light touch */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 10px; }
::-webkit-scrollbar-track { background: transparent; }

/* ============================================================
   DA Fresha/iOS/Airbnb · espace pro (juin 2026)
   Implémentation du design kit Claude Design.
   Pattern : floating white card on soft canvas, sidebar transparente,
   sélection cellule élevée, CTA noir pill, Coolvetica sur les titres.
   Helpers cb-fresha-* utilisables dans tout l'espace pro.
   ============================================================ */

/* Layout shell plein écran : le contenu occupe toute la zone disponible,
   sans marges visibles ni effet de carte flottante. */
.cb-fresha-frame {
  display: flex; height: 100vh;
  padding: 0; gap: 0;
  background: var(--bg-section);
}
.cb-fresha-maincard {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column;
  background: var(--surface);
  border: none;
  border-left: 1px solid var(--line);
  border-radius: 0;
  overflow: hidden;
  box-shadow: none;
}

/* Boutons Fresha-style (pill 999, CTA principal noir signature) */
.cb-fresha-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 44px; padding: 0 18px;
  border-radius: 999px;
  font-family: var(--ff-text);
  font-size: 14px; font-weight: 560; letter-spacing: -0.005em;
  border: 1px solid transparent;
  cursor: pointer; white-space: nowrap;
  transition: transform .18s cubic-bezier(.22,1,.36,1), box-shadow .18s, background .15s, border-color .15s;
}
.cb-fresha-btn:active { transform: scale(0.97); }
.cb-fresha-btn-sm { height: 38px; padding: 0 15px; font-size: 13.5px; }
.cb-fresha-btn-accent {
  background: var(--ink);  /* CTA NOIR signature Fresha */
  color: #fff;
}
.cb-fresha-btn-accent:hover {
  transform: translateY(-1px);
  box-shadow: 0 12px 26px -12px rgba(15, 18, 30, 0.5);
}
.cb-fresha-btn-ghost {
  background: var(--surface); color: var(--ink);
  border-color: var(--line-strong);
}
.cb-fresha-btn-ghost:hover {
  background: var(--bg-alt);
  border-color: var(--ink-4);
}

/* Bouton icône rond ghost (topbar notif, etc.) */
.cb-fresha-icon-btn {
  position: relative;
  width: 40px; height: 40px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink-3);
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background .15s, color .15s;
}
.cb-fresha-icon-btn:hover {
  background: var(--bg-alt);
  color: var(--ink);
}

/* Card sobre Fresha-style */
.cb-fresha-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 18px;
}

/* Quick action row (boutons rapides Accueil) */
.cb-fresha-quick {
  display: flex; align-items: center; gap: 11px;
  width: 100%;
  padding: 13px 15px;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--surface);
  cursor: pointer;
  font-family: inherit;
  font-size: 13.5px; font-weight: 540;
  color: var(--ink-2);
  transition: border-color .15s, background .15s, transform .12s;
}
.cb-fresha-quick:hover {
  border-color: var(--ink);
  background: var(--bg-alt);
  color: var(--ink);
  transform: translateY(-1px);
}
.cb-fresha-quick svg:first-child { color: var(--accent); }

/* NavCell · item sidebar : actif = white elevated cell (signature iOS) */
.cb-fresha-navcell {
  display: flex; align-items: center; gap: 12px;
  width: 100%;
  padding: 10px 12px;
  border-radius: 13px;
  border: none;
  cursor: pointer;
  font-family: inherit;
  font-size: 14.5px;
  text-align: left;
  background: transparent;
  color: var(--ink-2);
  transition: background .16s, color .16s, box-shadow .16s;
}
.cb-fresha-navcell:hover:not([data-on="true"]) {
  background: color-mix(in oklab, var(--surface) 60%, transparent);
}
.cb-fresha-navcell[data-on="true"] {
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: 600;
}
.cb-fresha-navcell[data-on="true"] .cb-fresha-navcell-icon {
  color: var(--accent);
}
.cb-fresha-navcell-icon {
  color: var(--ink-3);
  transition: color .16s;
}

/* Label de groupe sidebar */
.cb-fresha-navgroup-label {
  font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--ink-4);
  padding: 2px 12px 8px;
}

/* Search pill (topbar) */
.cb-fresha-search {
  display: flex; align-items: center; gap: 8px;
  height: 40px; padding: 0 14px;
  background: var(--bg-alt);
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--ink-4);
  font-size: 13.5px;
  width: 240px;
  cursor: pointer;
  transition: background .15s, border-color .15s;
}
.cb-fresha-search:hover {
  background: var(--surface);
  border-color: var(--line-strong);
}
.cb-fresha-search-kbd {
  margin-left: auto;
  font-family: var(--ff-mono);
  font-size: 11px;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  padding: 1px 5px;
}

/* Title de page Coolvetica · line-height généreux + padding bas pour ne PAS
   couper les jambages (j, g, p) de Coolvetica avec un overflow:hidden. */
.cb-fresha-title {
  font-family: var(--ff-coolvetica);
  font-size: 26px;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--ink);
  line-height: 1.3;
  padding-bottom: 2px;
}

/* Account row au bas de la sidebar */
.cb-fresha-account {
  display: flex; align-items: center; gap: 11px;
  width: 100%;
  padding: 9px 11px;
  margin-top: 4px;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--surface);
  cursor: pointer;
  font-family: inherit;
  box-shadow: var(--sh-1);
  transition: background .14s;
}
.cb-fresha-account:hover { background: var(--bg-alt); }
.cb-fresha-account-avatar {
  width: 34px; height: 34px; border-radius: 11px;
  flex-shrink: 0;
  background: linear-gradient(135deg, oklch(91% 0.05 278), oklch(74% 0.10 282));
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-family: var(--ff-display);
  font-size: 16px; font-weight: 600;
}

/* ============================================================
   Print · nimprimer que la facture
   Active : un parent ajoute la classe .cb-print-invoice à <body>,
   ou on cible directement #cb-invoice-print quand lutilisateur lance window.print().
   ============================================================ */
@media print {
  /* Cache toute lapp */
  body * { visibility: hidden !important; }
  /* Affiche uniquement la facture imprimable et ses descendants */
  #cb-invoice-print, #cb-invoice-print * { visibility: visible !important; }
  /* Sort la facture du flux modal/overlay et laffiche en pleine page */
  #cb-invoice-print {
    position: absolute !important;
    top: 0; left: 0;
    width: 100%;
    margin: 0 !important;
    padding: 24mm 18mm !important;
    border: none !important;
    border-radius: 0 !important;
    background: white !important;
    color: #111 !important;
    box-shadow: none !important;
  }
  /* Marges papier A4 raisonnables */
  @page { size: A4; margin: 12mm; }
  /* Retire les overlays/modale autour */
  .cb-modal-overlay, .cb-modal-shell { background: transparent !important; box-shadow: none !important; padding: 0 !important; }
}

/* ============================================================
   Bannière démo (pro + client) · masque le message sur mobile
   pour laisser de la place aux CTAs "Créer mon compte" + "Quitter"
   ============================================================ */
@media (max-width: 600px) {
  .cb-demo-msg { display: none !important; }
}

/* ============================================================
   TITRE D'ACCUEIL · 26 août 2026
   ------------------------------------------------------------
   « La simplicité d'un cahier, la puissance du numérique. »

   Deux polices portent les deux moitiés de la phrase : Coolvetica,
   massive, pour ce que la machine apporte ; Caveat Brush, tracée au
   feutre, pour les deux mots qui parlent du carnet et de l'outil.
   Un lecteur qui ne lit que les deux mots indigo a déjà compris.

   PREMIÈRE VERSION, ABANDONNÉE : un bandeau orange plein, copié de
   l'affiche envoyée par Noah. L'orange n'existe pas sur la vitrine
   (palette indigo / rose / sauge) et le bloc ressortait comme une
   pièce rapportée. On garde l'IDÉE, deux écritures qui s'opposent -
   et on la peint avec les couleurs du site.

   Utilisé par app/src/home-rest.jsx (le héros réellement servi) et
   app/src/home-hero.jsx. Les règles vivent ici pour n'exister qu'en
   un seul exemplaire.
   ============================================================ */
.cb-titre {
  margin: 0;
  font-family: var(--ff-display);
  font-size: clamp(31px, 5.4vw, 70px);
  line-height: 1.04;
  letter-spacing: -0.035em;
  font-weight: 580;
  color: var(--ink);
}

/* Les deux mots tracés à la main. Taille en em : ils suivent le titre
   quelle que soit la largeur d'écran. L'oeil de Caveat Brush est plus
   petit que celui de Coolvetica, d'où le 1.22 qui les remet à la même
   hauteur apparente. Caveat Brush n'a qu'une graisse (400) et penche
   déjà : le font-style: italic ne fait qu'accentuer l'inclinaison. */
.cb-titre-manuscrit {
  font-family: var(--ff-hand);
  font-style: italic;
  font-weight: 400;
  font-size: 1.22em;
  line-height: 0.8;
  letter-spacing: 0;
  color: var(--ton-da, var(--accent));
  padding-inline: 0.02em;
}

/* Retour à la ligne réservé au téléphone · cf. le media query plus bas. */
.cb-titre-br-m { display: none; }

/* Le fond de page réglé, comme un cahier. C'est le seul rappel du
   « cahier » qui ne passe pas par un mot : des lignes très pâles,
   effacées vers le bas par un masque, pour que le texte reste net.
   Décoratif, donc aria-hidden côté JSX. */
.cb-cahier {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0 37px,
    color-mix(in oklab, var(--accent) 30%, transparent) 37px 38px
  );
  -webkit-mask-image: radial-gradient(115% 78% at 50% 4%, #000 0%, transparent 70%);
          mask-image: radial-gradient(115% 78% at 50% 4%, #000 0%, transparent 70%);
  opacity: 0.72;
}

/* Halo indigo derrière le titre · donne du relief sans ajouter de bloc. */
.cb-halo {
  position: absolute;
  left: 50%;
  top: -12%;
  width: min(940px, 108%);
  aspect-ratio: 2 / 1;
  transform: translateX(-50%);
  pointer-events: none;
  background: radial-gradient(50% 50% at 50% 50%, color-mix(in oklab, var(--accent) 13%, transparent) 0%, transparent 72%);
}

@media (max-width: 620px) {
  /* Sur téléphone, chaque mot manuscrit passe seul à la ligne : quatre
     lignes courtes plutôt que deux lignes coupées n'importe où. Le
     <br/> existe toujours dans le HTML, il est simplement éteint sur
     grand écran. */
  .cb-titre-br-m { display: inline; }
  .cb-titre { font-size: clamp(29px, 8.2vw, 40px); line-height: 1.08; }
  .cb-titre-manuscrit { font-size: 1.24em; }
  .cb-cahier {
    background-image: repeating-linear-gradient(
      to bottom,
      transparent 0 29px,
      color-mix(in oklab, var(--accent) 30%, transparent) 29px 30px
    );
  }
}

/* ============================================================
   LES PETITES ANIMATIONS DE LA VITRINE · 26 août 2026
   ------------------------------------------------------------
   Trois gestes, pas plus, et réutilisés partout. Une page où
   chaque bloc invente son animation devient une page qui bouge
   dans tous les sens : on préfère trois gestes reconnaissables.

   · cbSurgit , l'élément arrive quand le regard l'atteint
   · cbFlotte , il respire sur place, en boucle, sans fin
   · le survol, il se soulève et s'incline d'un cheveu

   TOUT EST ÉTEINT pour qui a coché « réduire les animations »
   dans les réglages de son téléphone ou de son ordinateur. Ce
   n'est pas un goût, c'est un réglage d'accessibilité : chez
   certaines personnes le mouvement déclenche un vrai malaise.
   ============================================================ */
@keyframes cbSurgit {
  from { opacity: 0; transform: translateY(16px) scale(0.9); }
  to   { opacity: 1; transform: translateY(0)    scale(1); }
}
@keyframes cbFlotte {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-7px); }
}
/* TROIS FAÇONS DE FLOTTER, PAS UNE · 26 août 2026.
   Noah : « les icônes, j’aimerais bien qu’elles bougent un peu plus, et pas
   toutes dans le même sens, pas toutes en même temps ». Avec une seule
   trajectoire et un simple décalage de départ, huit tuiles font la même
   chose à huit instants différents : l’œil reconnaît le motif et ça devient
   mécanique. Trois trajectoires (verticale, verticale + bascule, dérive en
   diagonale) et huit durées premières entre elles : les tuiles ne repassent
   jamais ensemble par la même position. */
@keyframes cbFlotteB {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-11px) rotate(3.5deg); }
}
@keyframes cbFlotteC {
  0%, 100% { transform: translate(0, 0) rotate(0deg); }
  35%      { transform: translate(6px, -6px) rotate(-2.5deg); }
  70%      { transform: translate(-5px, -10px) rotate(2deg); }
}

/* ------------------------------------------------------------
   LA CONSTELLATION D'ICÔNES · 26 août 2026
   ------------------------------------------------------------
   Des tuiles carrées à coins ronds, comme les icônes du bureau
   d'un téléphone, semées autour du titre plutôt que rangées en
   file. Elles JAILLISSENT DU CENTRE à l'arrivée, puis se posent
   et respirent.

   Première version, refusée : les six mêmes tuiles alignées en
   une rangée sous les boutons. Noah : « j'imaginais juste un
   placement comme la capture, et une transition quand t'arrives,
   ils partent tous du milieu pour se disperser aux quatre coins ».
   Une rangée, c'est un sommaire ; une constellation, c'est un
   décor, et le décor laisse le titre seul au centre.

   QUATRE ÉPAISSEURS IMBRIQUÉES, une seule tâche chacune, sinon
   les transformations se marchent dessus :
     .cb-astre         → la position finale (left / top en %)
     .cb-astre-jaillit → le voyage depuis le centre
     .cb-astre-flot    → la respiration, une fois posée
     .cb-appli-tuile   → l'inclinaison et le survol

   Le vecteur du voyage (--dx / --dy) ne peut pas s'écrire en CSS :
   translate() en % se calcule sur la taille de l'ÉLÉMENT, pas du
   parent. Il est donc mesuré une fois en JavaScript, au moment où
   la rangée entre dans l'écran. Cf. shared.jsx.
   ------------------------------------------------------------ */
.cb-constellation {
  position: absolute;
  inset: 0;
  pointer-events: none;   /* le décor ne vole jamais un clic au texte */
  z-index: 0;
  overflow: hidden;
}

.cb-astre {
  position: absolute;
  left: var(--x);
  top: var(--y);
  transform: translate(-50%, -50%);
  pointer-events: auto;   /* sauf la tuile elle-même, qu'on peut survoler */
}

.cb-astre-jaillit {
  display: block;
  opacity: 0;
  /* Au départ : ramenée au centre du bloc et minuscule. */
  transform: translate(var(--dx, 0px), var(--dy, 0px)) scale(0.18);
  transition:
    transform 1.05s cubic-bezier(.18,.86,.25,1),
    opacity   0.45s ease;
  transition-delay: var(--d, 0s);
}
.cb-astre.est-vu .cb-astre-jaillit {
  opacity: 1;
  transform: translate(0, 0) scale(1);
}

.cb-astre-flot { display: block; }
/* La respiration ne démarre qu'une fois la tuile posée : sinon elle
   tire dessus pendant le voyage et la trajectoire tremble. */
.cb-astre.est-vu .cb-astre-flot {
  animation: var(--vol, cbFlotte) var(--duree, 5.4s) ease-in-out infinite;
  animation-delay: calc(var(--d, 0s) + 1.1s);
}
/* Huit tuiles, huit durées, trois trajectoires. Les durées sont choisies
   pour ne pas se retomber dessus : 4,3 / 6,7 / 5,1 / 7,3 / 4,9 / 6,1 /
   5,7 / 7,9 secondes. */
.cb-astre:nth-child(1) { --vol: cbFlotteB; --duree: 4.3s; }
.cb-astre:nth-child(2) { --vol: cbFlotteC; --duree: 6.7s; }
.cb-astre:nth-child(3) { --vol: cbFlotte;  --duree: 5.1s; }
.cb-astre:nth-child(4) { --vol: cbFlotteC; --duree: 7.3s; }
.cb-astre:nth-child(5) { --vol: cbFlotte;  --duree: 4.9s; }
.cb-astre:nth-child(6) { --vol: cbFlotteB; --duree: 6.1s; }
.cb-astre:nth-child(7) { --vol: cbFlotteC; --duree: 5.7s; }
.cb-astre:nth-child(8) { --vol: cbFlotteB; --duree: 7.9s; }
.cb-astre:hover .cb-astre-flot { animation-play-state: paused; }

.cb-appli-tuile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--taille, clamp(52px, 5.4vw, 74px));
  aspect-ratio: 1 / 1;
  border-radius: 24%;               /* le rayon des icônes d'iOS */
  color: #fff;
  background: linear-gradient(150deg, var(--ton-haut, var(--accent)) 0%, var(--ton-bas, var(--accent-strong)) 100%);
  box-shadow:
    0 18px 30px -14px color-mix(in oklab, var(--ton, var(--accent)) 60%, transparent),
    inset 0 1px 0 rgba(255,255,255,.4);
  transform: rotate(var(--rot, 0deg));
  transition: transform .3s cubic-bezier(.22,1,.36,1), box-shadow .3s ease;
}
/* Au survol la tuile se redresse et grandit : le geste dit « celle-ci ». */
.cb-astre:hover .cb-appli-tuile {
  transform: rotate(0deg) scale(1.12);
  box-shadow:
    0 30px 44px -16px color-mix(in oklab, var(--ton, var(--accent)) 66%, transparent),
    inset 0 1px 0 rgba(255,255,255,.45);
}
/* Le haut du dégradé est clair : une icône blanche posée dessus s'y
   perdrait. L'ombre portée lui redonne un bord net sans introduire une
   deuxième couleur d'icône selon la tuile. */
.cb-appli-tuile svg { filter: drop-shadow(0 1px 2.5px rgba(24,18,48,.34)); }

/* LES QUATRE TONS DES TUILES · une seule famille de couleurs.
   Ce sont les deux couleurs de ClientBase, l'indigo du pro et le violet
   du client, chacune en version profonde et en version claire. Huit
   tuiles se lisent alors comme UNE suite d'applications ; huit couleurs
   différentes se seraient lues comme huit produits.

   Essayé avant, et écarté : le rose et la sauge de la palette vitrine.
   Leurs versions -ink sont des couleurs de TEXTE, faites pour être
   sombres : en aplat de 70 px elles sortaient brique et sapin. Elles
   restent parfaites pour un liseré ou un mot, pas pour une tuile. */
.cb-astre--indigo        { --ton: var(--accent);     --ton-haut: color-mix(in oklab, var(--accent) 62%, white);     --ton-bas: var(--accent-strong); }
.cb-astre--indigo-clair  { --ton: var(--accent);     --ton-haut: color-mix(in oklab, var(--accent) 48%, white);     --ton-bas: var(--accent); }
.cb-astre--violet        { --ton: var(--cli-accent); --ton-haut: color-mix(in oklab, var(--cli-accent) 62%, white); --ton-bas: color-mix(in oklab, var(--cli-accent) 62%, var(--accent-strong)); }
.cb-astre--violet-clair  { --ton: var(--cli-accent); --ton-haut: color-mix(in oklab, var(--cli-accent) 48%, white); --ton-bas: var(--cli-accent); }

/* Sur téléphone il n'y a plus de marge sur les côtés du texte : les
   tuiles passent sur une autre carte de positions (--xm / --ym), plus
   petites, et se replient sur deux bandes horizontales. Les deux
   dernières disparaissent : huit tuiles sur deux bandes feraient une
   frise, six font un semis. */
@media (max-width: 900px) {
  .cb-astre { left: var(--xm, var(--x)); top: var(--ym, var(--y)); }
  .cb-astre:nth-child(n+7) { display: none; }
  .cb-appli-tuile { --taille: clamp(42px, 12.5vw, 56px); }
}

/* LE SEMIS DISPARAÎT SUR TÉLÉPHONE · 26 août 2026.
   Noah : « le tout premier bloc, je le trouve trop chargé, il y a trop de
   trucs qui volent un peu partout ». Sur un écran de 390 px, six tuiles
   autour d'un titre de quatre lignes, d'un sous-titre, de deux boutons, de
   trois preuves et d'une capture produit, ça fait sept objets pour un seul
   écran. Le semis est un DÉCOR : il a besoin de marge pour se lire comme
   tel, et sur téléphone il n'y en a pas, il devenait du bruit.
   Il reste entier sur ordinateur, où il a la place. */
@media (max-width: 700px) {
  .cb-constellation { display: none; }
}

/* Le réglage système gagne toujours : les tuiles sont là, à leur place,
   et plus rien ne bouge. */
@media (prefers-reduced-motion: reduce) {
  .cb-astre-jaillit { opacity: 1 !important; transform: none !important; transition: none !important; }
  .cb-astre.est-vu .cb-astre-flot { animation: none !important; }
  .cb-appli-tuile { transition: none !important; }
  .cb-astre:hover .cb-appli-tuile { transform: rotate(var(--rot, 0deg)) !important; }
}

/* ============================================================
   LE BLOC-NOTES ET LE CRAYON · 27 août 2026
   ------------------------------------------------------------
   Le décor du premier écran, à la place des huit tuiles
   d'applications. Le dessin est dans app/src/shared.jsx, bloc du
   même nom ; ici, la place qu'ils prennent et la façon dont ils
   arrivent.

   TROIS ÉPAISSEURS, une tâche chacune, c'est la leçon de la
   constellation, deux transformations sur le même élément se
   marchent dessus :
     .cb-pap-objet   → la place, et la dérive au défilement
     .cb-pap-arrivee → le voyage d'entrée
     .cb-pap-flot    → la respiration une fois posé
   Le SVG lui-même porte son inclinaison de repos.

   ILS NE PRENNENT AUCUNE PLACE DANS LA PAGE. Tout est en
   `position: absolute` dans une couche qui remplit le héros :
   ajouter ou agrandir un objet ne pousse jamais le titre d'un
   pixel. C'est la réponse exacte à ce que Noah voulait éviter -
   « je vais pas me retrouver avec le gros bloc-notes qui
   apparaît en haut et qui fait une énorme place ».
   ============================================================ */
.cb-papeterie {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;   /* le décor ne vole jamais un clic au texte */
}

/* La largeur passe par --w et non par width : la formule de centrage
   en a besoin, et ça ne fait qu’une valeur à changer par palier. */
.cb-pap-objet { position: absolute; display: block; width: var(--w); }
.cb-pap-objet > span { display: block; }
/* L'OMBRE EST POSÉE SUR LE SVG, PAS SUR UNE FORME DEDANS. Une ombre
   dessinée dans le SVG suivrait chaque contour du bloc-notes, le dos,
   la feuille, chaque anneau. Un `drop-shadow` de filtre prend la
   silhouette entière et n'en fait qu'une : c'est ce qui pose l'objet
   sur la page au lieu de le coller dessus. */
.cb-pap-bloc .cb-pap-flot   { filter: drop-shadow(0 24px 32px rgba(20, 18, 45, .17)); }
.cb-pap-crayon .cb-pap-flot { filter: drop-shadow(0 20px 28px rgba(20, 18, 45, .19)); }
.cb-pap-objet svg { display: block; width: 100%; height: 100%; }

/* ---------- LA PLACE, SUR ORDINATEUR ----------
   Les deux objets vivent dans les MARGES de la colonne de texte,
   jamais dessus. Première version : collés au bord de l'écran.
   Noah : « ils sont peut-être un peu trop éloignés du centre,
   essaye de bien les centrer sur les côtés pour qu'ils soient bien
   visibles ».

   ILS SONT DONC CENTRÉS DANS LEUR BANDE, et c'est un calcul, pas
   un réglage à l'œil. Le texte du héros est plafonné à 960 px et
   centré : son bord gauche est donc à 50% - 480px, et la bande
   libre va de 0 jusque-là. Le milieu de cette bande est à la
   moitié, soit 25% - 240px, et l'objet s'y centre en reculant de
   sa demi-largeur. Une seule formule, juste à toutes les largeurs,
   et qui suit LE TEXTE au lieu de suivre le bord de l'écran.

   Le max() est le garde-fou : sous 1 250 px la bande devient plus
   étroite que l'objet, et il vaut mieux le laisser se faire rogner
   par le bord que le voir passer sous le titre. */
.cb-pap-bloc {
  --w: clamp(162px, 14vw, 228px);
  left: max(-86px, calc(25% - 240px - var(--w) / 2));
  top: 11%;
}
.cb-pap-crayon {
  --w: clamp(68px, 6.7vw, 108px);
  right: max(-54px, calc(25% - 240px - var(--w) / 2));
  top: 8%;
}
/* L'inclinaison de repos. Le bloc-notes est posé de travers sur la
   table ; le crayon pointe vers le titre, pas vers le vide. */
.cb-pap-bloc svg   { transform: rotate(-8deg); }
.cb-pap-crayon svg { transform: rotate(19deg); }

/* ---------- LA PLACE DU DUO CLIENT ----------
   Même formule que le duo pro, avec UNE seule différence qui
   compte : le titre de la vitrine client est plafonné à 860 px et
   non à 960. Sa bande libre commence donc à 50% - 430px, et son
   milieu est à 25% - 215px. Le reste est identique, c'est la même
   mécanique d'arrivée, de respiration et de dérive pour les deux
   vitrines, il n'y a qu'un décor à maintenir.

   Les deux silhouettes sont opposées à dessein : une carte large
   et pleine de couleur à gauche, un petit bloc haut et blanc à
   droite. Deux rectangles de même proportion se seraient lus
   comme un objet dupliqué. */
.cb-pap-carte {
  --w: clamp(178px, 15vw, 248px);
  left: max(-92px, calc(25% - 215px - var(--w) / 2));
  top: 13%;
}
.cb-pap-jour {
  --w: clamp(104px, 8.6vw, 140px);
  right: max(-58px, calc(25% - 215px - var(--w) / 2));
  top: 15%;
}
.cb-pap-carte svg { transform: rotate(-7deg); }
.cb-pap-jour svg  { transform: rotate(6deg); }
.cb-pap-carte .cb-pap-flot { filter: drop-shadow(0 22px 30px rgba(20, 18, 45, .19)); }
.cb-pap-jour .cb-pap-flot  { filter: drop-shadow(0 20px 28px rgba(20, 18, 45, .16)); }

/* ---------- L'ARRIVÉE ----------
   Les deux entrent PAR LEUR BORD, en se redressant : c'est le
   geste de deux objets qu'on pose sur une table, pas de deux
   images qui apparaissent. */
.cb-pap-arrivee {
  opacity: 0;
  transition: opacity .5s ease var(--d, 0s),
              transform 1.05s cubic-bezier(.2, .85, .28, 1) var(--d, 0s);
}
.cb-pap-bloc   .cb-pap-arrivee,
.cb-pap-carte  .cb-pap-arrivee { transform: translate(-42%, 6%) rotate(-13deg); }
.cb-pap-crayon .cb-pap-arrivee,
.cb-pap-jour   .cb-pap-arrivee { transform: translate(46%, -8%) rotate(14deg); --d: .12s; }
.cb-papeterie.est-vu .cb-pap-arrivee { opacity: 1; transform: none; }

/* ---------- LA RESPIRATION ----------
   Elle ne démarre qu'une fois l'objet posé : sinon elle tire
   dessus pendant le voyage et la trajectoire tremble. Les deux
   durées sont premières entre elles (7,4 s et 5,9 s) pour que les
   deux objets ne retombent jamais en phase, deux décors qui
   montent ensemble font un bloc qui tremble. */
.cb-papeterie.est-vu .cb-pap-bloc .cb-pap-flot,
.cb-papeterie.est-vu .cb-pap-carte .cb-pap-flot {
  animation: cbPapFlot 7.4s ease-in-out 1.2s infinite;
}
.cb-papeterie.est-vu .cb-pap-crayon .cb-pap-flot,
.cb-papeterie.est-vu .cb-pap-jour .cb-pap-flot {
  animation: cbPapTient 5.9s ease-in-out 1.35s infinite;
}
/* L'AMPLITUDE A DOUBLÉ · 27 août au soir. Noah : « qu'ils
   continuent à être un peu animés et qu'ils s'animent pas qu'une
   seule fois ». La respiration TOURNAIT déjà en boucle, elle
   était simplement trop discrète pour qu'on la voie : 11 px sur
   7,4 secondes, c'est un pixel et demi par seconde. On ne l'a pas
   accélérée (ça deviendrait nerveux), on l'a rendue plus AMPLE. */
@keyframes cbPapFlot {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-22px) rotate(2deg); }
}
/* Le crayon ne flotte pas tout à fait comme le bloc : il OSCILLE,
   comme une main qui le tient. */
@keyframes cbPapTient {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  35%      { transform: translateY(14px) rotate(-2.6deg); }
  70%      { transform: translateY(-10px) rotate(1.7deg); }
}

/* ---------- ET LA PAGE S'ÉCRIT ----------
   La partie qu'aucune des deux références ne faisait, et qui
   change tout : les trois lignes et la coche du bloc-notes SE
   TRACENT, l'une après l'autre, une fois le bloc posé. Le décor ne
   fait plus qu'apparaître, il montre ce que fait le produit.

   Même mécanique que le mot cerné du titre : le contour est
   découpé en un tiret aussi long que le tracé (pathLength="100",
   posé côté JSX) et décalé de toute sa longueur, donc invisible ;
   on ramène le décalage à zéro et la main écrit.

   `100 220` et pas `100` : avec une seule valeur, le motif est
   « trait 100 / vide 100 », et au décalage 100 on retombe pile sur
   le début du trait suivant, un trait de longueur nulle que le
   bout rond transforme en point resté sur la page. */
.cb-pap-ecrit path {
  stroke-dasharray: 100 220;
  /* PAR DÉFAUT, LA PAGE EST ÉCRITE. C'est la bonne façon de rater :
     si le script qui pose est-vu ne tourne jamais, on voit un
     bloc-notes rempli, pas une page blanche. Et personne ne voit
     jamais cet état-là au chargement, l'objet entier est invisible
     tant que est-vu n'est pas posé, c'est l'arrivée qui l'allume. */
  stroke-dashoffset: 0;
}
/* ELLE S'ÉCRIT, S'EFFACE, ET RECOMMENCE · 27 août au soir. Noah :
   « qu'ils s'animent pas qu'une seule fois ». Le trait se trace,
   tient cinq secondes, puis SORT PAR LE MÊME BOUT, le décalage
   continue vers le négatif, donc la main balaie dans le sens où
   elle a écrit, et la page se réécrit. Onze secondes le tour, les
   cinq traits décalés : on ne voit jamais deux lignes faire le même
   geste au même instant.

   backwards fait tenir l'état de départ pendant le retard : sans
   lui, la ligne apparaîtrait écrite, puis sauterait au début de son
   animation. */
.cb-papeterie.est-vu .cb-pap-ecrit path {
  animation: cbPapEcrit 11s cubic-bezier(.4, 0, .3, 1) var(--d, 0s) infinite backwards;
}
@keyframes cbPapEcrit {
  0%        { stroke-dashoffset: 100; }
  15%, 60%  { stroke-dashoffset: 0; }
  76%, 100% { stroke-dashoffset: -100; }
}

/* ---------- TABLETTE ----------
   La colonne de texte occupe toute la largeur : les objets se
   rangent derrière elle, plus petits et bien plus rognés. */
@media (max-width: 1100px) {
  /* En pixels et non en pourcentage, pour la même raison que le duo
     client juste en dessous : la marge du haut doit tenir la respiration
     (22 px) et l'inclinaison, quelle que soit la hauteur du héros. */
  .cb-pap-bloc   { --w: 178px; left: -34px; top: 58px; }
  /* LE HAUT EST EN PIXELS, PAS EN POURCENTAGE, et c'est une correction
     mesurée. Le héros client est bien plus court que le héros pro dans
     cette bande (392 px contre 772 à 900 px de large) : 9 % n'y faisaient
     que 35 px de marge. Or l'objet respire de 22 px et son inclinaison
     lui en fait gagner 11 de plus, il touchait donc le bord haut de la
     section une fois sur deux, et s'y faisait trancher net. Une valeur en
     pixels tient la marge quelle que soit la hauteur du héros. */
  .cb-pap-carte  { --w: 194px; left: -46px; top: 52px; }
  .cb-pap-jour   { --w: 116px; right: -18px; top: 46px; }
  /* Le crayon est coincé entre deux bords : trop bas, sa pointe tombe
     sur « Lire les avis » (mesuré à 820 px, une pointe noire sur un
     lien, c'est le lien qui perd) ; trop haut, sa gomme dépasse du
     héros quand il respire. Il est donc RACCOURCI plutôt que déplacé :
     78 px de large au lieu de 84, ce qui lui rend 21 px de course. */
  .cb-pap-crayon { --w: 78px; right: -12px; top: 24px; }
}

/* ---------- LE BAS DE LA BANDE TABLETTE ----------
   Entre 761 et 900 px, la colonne de texte prend presque tout : mesuré au
   contrôle automatique (outils de capture, `pap-controle.js`), le
   bloc-notes mordait sur le titre puis sur la ligne de preuves, de 6 à
   44 px selon le moment de la respiration. Mesuré au navigateur, sur les
   boîtes d'ENCRE réelles (celles des lignes de texte) et non sur les
   boîtes de bloc : un titre centré occupe toute la colonne mais ne
   l'encre que sur la largeur de ses mots. Les objets rapetissent donc et
   sortent davantage. C'est la dernière marche avant
   le téléphone, où ils changent complètement de place. */
@media (max-width: 900px) {
  .cb-pap-bloc   { --w: 150px; left: -56px; top: 40px; }
  .cb-pap-crayon { --w: 64px; right: -20px; top: 18px; }
  .cb-pap-carte  { --w: 166px; left: -66px; top: 44px; }
  .cb-pap-jour   { --w: 98px; right: -26px; top: 38px; }
}

/* ---------- TÉLÉPHONE ----------
   Noah : « je vais pas me retrouver avec le gros bloc-notes qui
   apparaît en haut et qui fait une énorme place entre le header et
   ça, et le crayon qui apparaît dessous ».

   Ils ne s'empilent donc pas : ils vont dans les deux SEULS coins
   que le premier écran laisse libres, en haut à gauche, à côté de
   la pastille du compteur, et en bas à droite, sous le lien vers
   les avis. Les deux sont rognés par le bord de l'écran à plus
   d'un tiers : c'est ce qui les garde en décor. Et comme tout est
   en position absolue, ils ne coûtent pas un pixel de hauteur.

   ⚠️ C'est aussi la raison pour laquelle le semis d'icônes avait
   été SUPPRIMÉ sous 700 px le 26 août : « le tout premier bloc, je
   le trouve trop chargé, il y a trop de trucs qui volent un peu
   partout ». Huit objets, c'était du bruit ; deux objets posés
   dans les coins, c'est un cadre. */
@media (max-width: 760px) {
  /* ILS NE TOUCHENT PLUS LE BORD HAUT DE LA SECTION, et c’était le
     « bug de barre blanche » que Noah a vu. Le menu du téléphone
     n’est pas une pilule flottante comme sur ordinateur : c’est une
     barre blanche pleine largeur, POSÉE DANS LE FLUX au-dessus du
     héros. Les deux objets, calés à top: -8px, se faisaient donc
     trancher net juste sous elle, et une silhouette coupée au
     rasoir par une barre blanche, ça ne se lit pas comme un décor,
     ça se lit comme un défaut d’affichage.
     Ils commencent maintenant DANS le héros : plus aucune arête, et
     la barre du menu redevient ce qu’elle est, un menu. */
  .cb-pap-bloc {
    --w: 134px; left: -36px; top: 92px;
  }
  /* LES DEUX COINS DU HAUT, ET PAS AILLEURS, c'est mesuré, pas choisi
     au hasard. Le premier écran d'un téléphone est plein : titre sur
     quatre lignes, sous-titre, deux boutons, la ligne de preuves, le
     lien vers les avis. La SEULE bande qui reste libre est celle de la
     pastille du compteur : elle est centrée sur 200 px, donc les deux
     coins du haut sont vides sur une centaine de pixels de haut.

     Essayé avant : le crayon en bas à droite, pour faire une diagonale.
     Il passait derrière « Données en France », un décor indigo sous un
     texte gris, c'est le texte qui perd. Les deux coins du haut
     encadrent le titre sans jamais toucher un mot.

     Et ils ne coûtent pas un pixel de hauteur : tout est en position
     absolue, donc rien ne pousse le titre vers le bas. C'est la réponse
     exacte au « je vais pas me retrouver avec le gros bloc-notes qui
     fait une énorme place entre le header et ça ». */
  .cb-pap-crayon {
    --w: 56px; right: -14px; top: 94px; bottom: auto;
  }
  /* LE DUO CLIENT VA EN BAS, ET C'EST SA PAGE QUI L'IMPOSE. Sur la
     vitrine pro, le titre tient sur quatre lignes courtes : il reste
     deux coins libres en haut. Ici il tient sur DEUX lignes larges, et
     le sous-titre puis la barre de recherche prennent toute la
     largeur, mesuré à 390 px : il ne reste rien sur les côtés entre
     le menu et la barre de recherche.

     Les deux objets se posent donc dans les coins du BAS, de part et
     d'autre de la ligne des compteurs, et débordent sous la section
     (qui les rogne). On les voit par le haut, ce qui est le bon bout
     pour les deux : la carte montre ses tampons, le bloc montre sa
     date. Et ils sont aussi loin du menu qu'on peut l'être. */
  /* ============================================================
     LE DUO CLIENT ENCADRE LE TITRE, comme le duo pro
     ------------------------------------------------------------
     Noah, sur le premier essai : « le placement n'est pas du tout
     bon ». Il l'était : les deux objets, calés dans les 34 px qui
     restaient sous la ligne des compteurs, se retrouvaient à cheval sur
     la couture entre le héros et la section suivante, et tranchés par
     elle aux trois quarts.

     LE VRAI PROBLÈME N'ÉTAIT PAS LEUR PLACE, C'ÉTAIT LE TITRE. Il
     tenait sur deux lignes LARGES, les lettres allaient de x=50 à
     x=340 sur 390, et ne laissait rien sur les côtés, là où le titre
     pro, coupé en quatre lignes courtes, en laisse une centaine de
     pixels. On a donc appliqué au titre client le mécanisme du titre
     pro (`.cb-titre-br-m`, un retour à la ligne qui n'existe que sous
     620 px) : trois lignes courtes, 85 px rendus de chaque bord.

     Les deux objets peuvent alors l'encadrer exactement comme le
     bloc-notes et le crayon encadrent celui du pro : rognés par le bord
     de l'ÉCRAN, un cadrage -, jamais par une limite invisible, un
     bug -, et calés pour s'arrêter avant le sous-titre.
     ============================================================ */
  .cb-pap-carte { --w: 116px; left: -52px; top: 92px; bottom: auto; }
  .cb-pap-jour  { --w: 70px; right: -16px; top: 92px; bottom: auto; }
  .cb-pap-carte svg { transform: rotate(-9deg); }
  .cb-pap-jour svg  { transform: rotate(8deg); }
  .cb-pap-bloc svg   { transform: rotate(-11deg); }
  .cb-pap-crayon svg { transform: rotate(17deg); }
  /* Le décor est plus près du texte qu’ailleurs : on le retient d’un
     souffle pour qu’il ne dispute jamais la lecture, mais à peine,
     Noah les veut plus visibles, pas plus effacés. */
  .cb-papeterie { opacity: .92; }
}
/* ---------- LES PETITS ÉCRANS ----------
   Sous 440 px, le titre du héros pro passe à cinq lignes et la ligne de
   preuves à deux : le premier écran se remplit, et les deux objets, à
   leur taille de 390 px, mordaient sur le titre puis sur les preuves.
   Contrôlé au navigateur sur les boîtes d'encre réelles, en ajoutant la
   course de la respiration pour tester le PIRE moment et pas celui de la
   capture : ils rapetissent, sortent un peu plus, et plus rien ne se
   touche entre 360 et 1 920 px. */
@media (max-width: 440px) {
  .cb-pap-bloc   { --w: 112px; left: -46px; top: 88px; }
  .cb-pap-crayon { --w: 48px; right: -14px; top: 92px; }
}

/* SOUS 345 PX, PLUS DE DÉCOR DU TOUT. C'est le seul palier où on
   renonce, et c'est délibéré : à cette largeur le titre du héros passe à
   six lignes, il ne reste aucune bande libre, et les deux objets se
   retrouveraient sur les mots. Un décor qui gêne la lecture ne décore
   plus rien. (Même décision que le semis d'icônes, retiré du téléphone
   le 26 août pour la même raison.) */
@media (max-width: 345px) { .cb-papeterie { display: none; } }

/* Le réglage système gagne toujours : les objets sont là, à leur
   place, la page est écrite, et plus rien ne bouge. */
@media (prefers-reduced-motion: reduce) {
  .cb-pap-arrivee { opacity: 1 !important; transform: none !important; transition: none !important; }
  .cb-papeterie.est-vu .cb-pap-flot { animation: none !important; }
  /* La page reste ECRITE, et elle ne s efface plus : l animation est
     coupee, et le decalage force a zero gagne de toute facon sur elle. */
  .cb-papeterie.est-vu .cb-pap-ecrit path,
  .cb-pap-ecrit path { animation: none !important; transition: none !important; stroke-dashoffset: 0 !important; }
}

/* ------------------------------------------------------------
   Le titre d'accueil côté CLIENT · même dessin, autre couleur.
   La vitrine client est violette (--cli-accent), pas indigo :
   les deux mots manuscrits suivent l'audience.
   ------------------------------------------------------------ */
.cb-titre--client { --ton-da: var(--cli-accent); }
.cb-cahier--client {
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0 37px,
    color-mix(in oklab, var(--cli-accent) 30%, transparent) 37px 38px
  );
}
.cb-halo--client {
  background: radial-gradient(50% 50% at 50% 50%, color-mix(in oklab, var(--cli-accent) 13%, transparent) 0%, transparent 72%);
}
@media (max-width: 620px) {
  .cb-cahier--client {
    background-image: repeating-linear-gradient(
      to bottom,
      transparent 0 29px,
      color-mix(in oklab, var(--cli-accent) 30%, transparent) 29px 30px
    );
  }
}

/* ============================================================
   DA « CAHIER & NUMÉRIQUE » · 26 août 2026
   ------------------------------------------------------------
   Le titre d'accueil dit la promesse du produit : « la simplicité
   d'un cahier, la puissance du numérique ». Cette direction
   artistique ne fait qu'une chose : tenir cette phrase sur toute
   la longueur des deux vitrines.

   DEUX MONDES, DEUX ÉCRITURES.
   · Le CAHIER · ce qu'une main écrit : les intertitres, les notes
     dans la marge, les mots entourés, les traits qui barrent. En
     Caveat Brush, indigo côté pro, violet côté client, toujours
     un peu de travers.
   · Le NUMÉRIQUE · ce que la machine affiche : la Coolvetica
     massive des titres, les tuiles d'applications en dégradé, les
     cartes nettes, les chiffres qui comptent.

   Chaque section fait dialoguer les deux. C'est ce qui distingue
   la page d'un site de logiciel ordinaire, et c'est aussi ce qui
   la rend impossible à copier sans copier la phrase.

   RÈGLE DE DOSAGE : au plus UNE trace manuscrite par bloc, en
   plus de l'intertitre. Deux, et la page devient un cahier de
   brouillon ; zéro, et c'est un site de logiciel comme un autre.
   ============================================================ */

/* --- 1 · L'INTERTITRE MANUSCRIT -----------------------------
   Il remplace les petites capitales espacées (« NOS DIFFÉRENCES »)
   qui ouvraient chaque section. C'était la chose la plus banale
   de la page, et elle revenait dix fois. */
.cb-chapitre {
  display: inline-block;
  font-family: var(--ff-hand);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(21px, 2.2vw, 30px);
  line-height: 1;
  letter-spacing: 0;
  color: var(--ton-da, var(--accent));
  /* L angle vient du texte lui-meme · cf. cbAngleDe dans shared.jsx. */
  transform: rotate(var(--incline, -2deg));
  margin-bottom: 10px;
}

/* --- 2 · LE CERNE ------------------------------------------
   L'ellipse tracée autour d'un mot ou d'un chiffre, comme au
   stylo. Elle SE DESSINE quand on l'atteint : le trait part d'un
   bout et fait le tour. Un trait déjà fermé à l'arrivée n'aurait
   été qu'un cadre de plus.

   stroke-dasharray/offset · le contour est découpé en un seul
   tiret aussi long que le tracé, décalé de toute sa longueur : il
   est donc invisible, puis on ramène le décalage à zéro. */
.cb-cerne { position: relative; display: inline-block; white-space: nowrap; }
.cb-cerne > svg {
  position: absolute;
  left: -9%; top: -26%;
  width: 118%; height: 154%;
  overflow: visible;
  color: var(--ton-da, var(--accent));
  pointer-events: none;
}
.cb-cerne > svg path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.6;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  /* pathLength="100" est posé sur le tracé côté JSX : la longueur du
     trait devient 100 quelle que soit la taille à l'écran, donc ces deux
     valeurs restent justes même quand le SVG est étiré. */
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  /* Une TRANSITION, pas une animation · même raison que pour la cascade :
     une animation écrase les déclarations normales tant qu'elle tient
     l'élément, et si elle ne démarre jamais le trait reste invisible. Avec
     une transition, l'arrivée (stroke-dashoffset: 0) est une déclaration
     ordinaire, que le filet de useEntreeEcran finit toujours par déclencher. */
  transition: stroke-dashoffset 1s cubic-bezier(.55,0,.35,1) .2s;
}
.est-vu .cb-cerne > svg path,
.cb-cerne.est-vu > svg path { stroke-dashoffset: 0; }

/* --- 3 · LA NOTE DANS LA MARGE -----------------------------
   Trois mots écrits à côté d'un argument, comme quelqu'un qui
   annote sa propre page. C'est là qu'on met ce qu'un service
   commercial ne dirait pas : « oui, vraiment zéro ». */
.cb-note {
  display: inline-flex;
  align-items: flex-start;
  gap: 6px;
  font-family: var(--ff-hand);
  font-style: italic;
  font-size: clamp(16px, 1.5vw, 21px);
  line-height: 1.15;
  color: var(--ton-da, var(--accent));
  transform: rotate(var(--incline, -3deg));
}
/* La flèche courbe qui relie la note à ce qu'elle commente. */
.cb-note svg { flex-shrink: 0; margin-top: 2px; }
/* La flèche « haut » part du bas et remonte : elle doit toucher la ligne
   qu'elle commente, pas flotter à côté. */
.cb-note--haut { align-items: flex-end; gap: 8px; }
.cb-note--haut svg { margin: 0 0 -3px; }

/* --- 4 · LA CASCADE ----------------------------------------
   Les cartes d'une grille n'arrivent pas ensemble : l'oeil les
   prend une par une, et le décalage lui donne le temps. Huit
   rangs suffisent, au-delà l'attente devient une attente. */
/* UNE TRANSITION, PAS UNE ANIMATION · et c'est important.
   Première version : une @keyframes avec fill-mode. Une animation ÉCRASE les
   déclarations normales tant qu'elle tient l'élément : si elle ne démarre
   jamais, onglet en arrière-plan, navigateur qui a d'autres priorités, le
   bloc reste bloqué sur son image de départ, donc invisible. Avec une
   transition, l'état final (opacity:1) est écrit dans la feuille de style :
   au pire il n'y a pas d'animation, mais le contenu est là. Sur une grille
   qui porte l'annuaire des pros, ce n'est pas un détail de confort.
   Filet supplémentaire côté JS : cf. useEntreeEcran dans shared.jsx. */
/* .cb-cascade N ANIME PLUS RIEN ELLE-MEME · 27 août 2026.
   Elle portait son propre « fondu + glissement + réduction », en
   escalier sur ses huit premiers enfants. C'est maintenant le rôle
   des ARRIVÉES (bloc plus bas) : la classe reste, mais comme simple
   REPÈRE, elle dit « ici, des enfants qui se posent un par un », et
   le chef d'orchestre s'occupe du geste et du tempo.
   Deux systèmes sur les mêmes éléments, c'était deux opacités qui se
   disputent, et sur téléphone une animation que Noah ne veut plus. */

/* --- 5 · LA PAGE -------------------------------------------
   Une section réglée comme une page de cahier. Les lignes sont
   très pâles et s'effacent sur les bords : elles donnent une
   matière, elles ne doivent jamais se lire. */
.cb-page { position: relative; overflow: hidden; }
.cb-page > .cb-reglure {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    to bottom,
    transparent 0 37px,
    color-mix(in oklab, var(--ton-da, var(--accent)) 26%, transparent) 37px 38px
  );
  -webkit-mask-image: radial-gradient(90% 70% at 50% 50%, #000 0%, transparent 78%);
          mask-image: radial-gradient(90% 70% at 50% 50%, #000 0%, transparent 78%);
  opacity: 0.55;
}

/* --- 5 bis · LA RATURE --------------------------------------
   Un mot barré d'un trait de stylo. Même mécanique que le cerne :
   le trait se tire quand on l'atteint, et l'état d'arrivée est une
   déclaration ordinaire, jamais une image-clé, sinon un texte
   peut rester bloqué invisible (leçon du 26 août).
   Elle sert à dire ce que les autres NE font pas : rien ne le dit
   mieux qu'une ligne rayée à la main. */
.cb-raye { position: relative; display: inline-block; }
.cb-raye > svg {
  position: absolute;
  left: -2%; top: 38%;
  width: 104%; height: 32%;
  overflow: visible;
  color: var(--ton-raye, var(--danger));
  pointer-events: none;
}
.cb-raye > svg path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  transition: stroke-dashoffset .55s cubic-bezier(.5,0,.4,1);
}
.est-vu .cb-raye > svg path,
.cb-raye.est-vu > svg path { stroke-dashoffset: 0; }
@media (prefers-reduced-motion: reduce) {
  .cb-raye > svg path { stroke-dashoffset: 0 !important; transition: none !important; }
}

/* --- 6 · LE TRAIT TIRÉ À LA MAIN ----------------------------
   Le séparateur de la DA, à la place des filets d'un pixel. */
.cb-trait {
  display: block;
  width: 100%;
  height: 8px;
  overflow: visible;
  color: color-mix(in oklab, var(--ton-da, var(--accent)) 32%, var(--line-strong));
}
.cb-trait path {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

/* La vitrine client parle violet : un seul token à basculer. */
.cb-da--client { --ton-da: var(--cli-accent); }

/* L'ÉCRITURE À LA MAIN SUR TÉLÉPHONE · 26 août 2026.
   Noah : « tous les textes écrits à la main se rapprochent trop des
   bordures ». Deux causes, corrigées ensemble :
     · l'inclinaison. Une rotation ne change pas la place que prend
       l'élément dans la page, mais elle déplace ses PIXELS. À -3°, une
       ligne de 300 px sort de 8 px de chaque côté, ce qui suffit à toucher
       la gouttière d'un téléphone. L'inclinaison tombe donc à -1°.
     · la largeur. Les notes prennent maintenant une marge intérieure et ne
       dépassent jamais la colonne de texte. */
@media (max-width: 620px) {
  .cb-note {
    transform: rotate(calc(var(--incline, -2deg) * 0.5));
    padding-inline: 10px;
    max-width: 100%;
    text-wrap: balance;
  }
  /* LA FLÈCHE DES NOTES DISPARAÎT SUR TÉLÉPHONE · 26 août 2026.
     Noah : « la flèche est complètement loin du texte, elle est complètement
     à gauche ». Sur un écran étroit la note passe sur deux lignes : la
     flèche, qui est le premier élément de la rangée, se retrouve alors
     collée au bord gauche, à des centimètres du texte qu’elle désigne. Une
     note posée juste sous la ligne qu’elle commente n’a pas besoin de
     flèche pour qu’on comprenne à quoi elle se rapporte. */
  .cb-note svg { display: none; }
  .cb-note { justify-content: center; text-align: center; }
  /* Sur telephone l inclinaison est divisee par deux : la colonne est
     etroite, un angle marque pousse le texte vers la gouttiere. */
  .cb-chapitre { transform: rotate(calc(var(--incline, -2deg) * 0.5)); margin-bottom: 8px; padding-inline: 2px; }
  .cb-page > .cb-reglure {
    background-image: repeating-linear-gradient(
      to bottom,
      transparent 0 29px,
      color-mix(in oklab, var(--ton-da, var(--accent)) 26%, transparent) 29px 30px
    );
  }
}

@media (prefers-reduced-motion: reduce) {
  .cb-cerne > svg path { stroke-dashoffset: 0 !important; transition: none !important; }
}

/* ============================================================
   LES COUPS DE CRAYON · 26 août 2026
   ------------------------------------------------------------
   Des gestes de stylo dans les marges, qui s’écrivent quand la
   section arrive. Le contenu des tracés et l’historique des trois
   versions abandonnées sont dans app/src/shared.jsx, bloc du même
   nom.

   TROIS GARDE-FOUS :
     · deux gestes par section, jamais plus ;
     · rien en dessous de 1440 px de large, sous cette taille il
       n’y a plus de marge, et un trait sans place devient un trait
       qui gêne ;
     · pointer-events: none, toujours.
   ============================================================ */
.cb-marges {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

/* LA POSITION · troisième réglage, et le bon. Ni collée au texte, ni
   collée au bord de l’écran : toujours à la même distance DEHORS de la
   colonne de 1180 px, quelle que soit la largeur de la fenêtre. */
.cb-marge {
  position: absolute;
  top: var(--y, 30%);
  transform: rotate(var(--rot, 0deg));
}
/* clamp() plutot qu une valeur fixe · il prend la valeur du milieu, bornee
   par les deux autres. Ici : 70 px dehors de la colonne quand la fenetre est
   large, et jamais moins de 24 px du bord de l ecran quand elle se resserre.
   Une seule ligne remplace trois media queries. */
.cb-marge--g { left: clamp(24px, calc(50% - 738px), 320px); }
.cb-marge--d { right: clamp(24px, calc(50% - 738px), 320px); left: auto; }

/* LE TRAIT S’ÉCRIT · le contour est découpé en un seul tiret aussi long
   que le tracé et décalé de toute sa longueur : il est donc invisible,
   puis on ramène le décalage à zéro et la main écrit.
   Une TRANSITION, jamais une image-clé : l’état d’arrivée reste une
   déclaration ordinaire, donc le trait finit toujours par exister même
   si l’animation ne part pas (leçon du 26 août, deux fois). */
.cb-crayon { display: block; color: color-mix(in oklab, var(--ton-da, var(--accent)) 34%, transparent); }
.cb-crayon path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  transition: stroke-dashoffset 1.15s cubic-bezier(.45,0,.35,1) var(--d, 0s);
}
.cb-marges.est-vu .cb-crayon path { stroke-dashoffset: 0; }

/* Une fois écrit, le trait respire à peine, assez pour qu’il ne soit
   pas mort, pas assez pour qu’on le regarde. */
.cb-marges.est-vu .cb-crayon { animation: cbCrayonVit 11s ease-in-out infinite; animation-delay: calc(var(--d, 0s) + 1.4s); }
@keyframes cbCrayonVit {
  0%, 100% { opacity: .9; transform: translateY(0); }
  50%      { opacity: 1;  transform: translateY(-5px); }
}

@media (max-width: 1440px) { .cb-marges { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .cb-crayon path { stroke-dashoffset: 0 !important; transition: none !important; }
  .cb-marges.est-vu .cb-crayon { animation: none !important; }
}

/* ============================================================
   LES ARRIVÉES · 27 août 2026
   ------------------------------------------------------------
   Comment le contenu entre à l'écran quand on descend la page.

   CE QU'IL Y AVAIT AVANT, et pourquoi ça ne suffisait plus : un
   seul geste, fondu + glissement vers le haut, appliqué à des
   BLOCS ENTIERS. C'est l'animation de tous les sites, et Noah
   l'avait écrit dans ava/APPRENTISSAGE.md avant même qu'on la pose :
   il n'aime pas « les animations répétitives et prévisibles
   (toujours fade+slide) ». Un bloc qui monte en fondu ne raconte
   rien : il dit juste « il y a quelque chose ici ».

   LE PRINCIPE DE LA NOUVELLE VERSION : ce n'est plus le bloc qui
   arrive, ce sont SES PARTIES, chacune avec le geste qui lui
   correspond, dans l'ordre où on les lit. Une section n'apparaît
   plus, elle SE MET EN PLACE :
     · le sur-titre manuscrit S'ÉCRIT, de gauche à droite, comme
       une main qui le trace ;
     · le titre REMONTE de derrière sa propre ligne, sans fondu -
       un fondu sur un gros titre, ça le rend sale une demi-seconde ;
     · le paragraphe suit, plus discrètement ;
     · les cartes SE POSENT une par une, très légèrement de
       travers, comme des cartes qu'on pose sur une table ;
     · les traits SE TIRENT depuis la gauche ;
     · les grandes images arrivent DE PLUS LOIN, sans glisser.

   QUATRE RÈGLES, écrites ici parce qu'elles ne se devinent pas :

     1. LE VERROU DE LARGEUR EST DANS LE SCRIPT, PAS ICI · 31 août
        2026. Cette règle disait « ordinateur seulement (≥ 1024 px) »
        et ces gestes vivaient dans un @media. Noah demande l'inverse
        aujourd'hui : « sur desktop t'as fait une version où quand on
        descend on a des petites animations, mais la version mobile
        reste très basique comme site, alors que j'aime bien avoir un
        site un peu complet ». Le téléphone y a droit, avec des
        retards comprimés, cf. « LES ARRIVÉES SUR TÉLÉPHONE » plus
        bas.
        Le @media est retiré plutôt que dédoublé : la règle 2 dit
        déjà que RIEN ne s'applique sans `data-arr`, et c'est le
        script qui pose l'attribut. Une seule décision, à un seul
        endroit, au lieu de deux qui doivent rester d'accord.

     2. RIEN N'EST CACHÉ PAR DÉFAUT. Aucune règle ne s'applique
        sans l'attribut `data-arr`, et c'est le script qui le pose.
        S'il ne tourne pas, vieux navigateur, script bloqué, erreur
        ailleurs, la page reste ENTIÈRE. Une animation ratée doit
        laisser une page complète, jamais un écran blanc.

     3. UNE FOIS, ET C'EST TOUT. On ne rejoue pas en remontant :
        une page qui rejoue ses animations est fatigante au
        deuxième passage.

     5. LA CLASSE EST SUR L'ÉLÉMENT, PAS SUR SON PARENT.
        Écrite « .est-la [data-arr] », elle ruissellerait : une
        grande section qui s'ouvre déclencherait aussi les cartes
        de sa grille, 1200 px plus bas, hors de l'écran. Chaque
        groupe est donc réveillé pour lui-même.

     4. L'ATTRIBUT S'EFFACE APRÈS. L'arrivée finie, le script
        retire `data-arr`. Plus aucune règle de `transform` ne
        traîne sur l'élément, et les effets de survol des cartes
        retrouvent leur terrain, c'est le piège classique de ce
        genre de système : une carte qui ne se soulève plus au
        survol parce qu'un `transform: none` d'animation gagne.

   Le script est dans app/src/shared.jsx, bloc « LES ARRIVÉES ».
   ============================================================ */

/* `screen` et non un seuil de largeur : à l'impression, un élément
   figé sur une arrivée qui n'a jamais eu lieu sortirait blanc. */
@media screen {
  /* ============================================================
     LE TEXTE ARRIVE FLOU, ET SE FAIT NET · 4 septembre 2026
     ------------------------------------------------------------
     Noah : « j'aimerais que toutes les animations de texte sur le site
     vitrine, ce soit un truc flou qui arrive vers le net, pour que ça
     fasse un peu plus sympa ».

     POURQUOI C'EST LE BON GESTE ICI, et pas un effet de plus.
     Les entrées précédentes déplaçaient le texte : balayage latéral
     d'abord (retiré le 1er septembre, « vraiment moche »), puis
     dévoilement par le bas au `clip-path`. Les deux ont le même défaut
     de fond : ILS BOUGENT LA LIGNE QU'ON ESSAIE DE LIRE. L'œil se cale
     sur le début du mot, le voit se déplacer, et attend au lieu de lire.

     Le flou ne déplace rien. Le texte est DÉJÀ à sa place, il se
     précise, c'est tout. C'est le seul geste d'arrivée qui n'entre pas
     en concurrence avec la lecture, et c'est pour ça qu'il fonctionne
     sur un site qu'on vient lire.

     TROIS INTENSITÉS, selon la taille du texte : 9 px sur les grands
     titres (à 46 px de corps, 4 px ne se verraient pas), 4 px sur les
     sur-titres et le corps, 3 px sur les lignes de liste et de tableau,
     qui se répètent et doivent rester discrètes.

     ⚠️ `filter: none` à L'ARRIVÉE, ET PAS `blur(0)`. Un filtre, même
     nul, laisse l'élément sur sa propre couche de composition et crée
     un bloc conteneur pour ses descendants positionnés. Sur une page
     qui en compte des dizaines, c'est de la mémoire vidéo gardée pour
     rien, et un `position: fixed` à l'intérieur cesserait de suivre la
     fenêtre. On le retire complètement une fois l'animation finie.
     ============================================================ */

  /* ⚠️ LE SUR-TITRE NE S'ÉCRIT PLUS DE GAUCHE À DROITE · 1er septembre
     2026. Noah, deux fois : « les animations où le texte apparaît de
     droite à gauche, je trouve ça vraiment moche, pas très
     professionnel ».

     C'était un balayage horizontal, le texte se dévoilait lettre par
     lettre comme une écriture. L'intention était juste (la DA a une main
     dessinée), l'effet ne l'est pas : sur trois mots ça amuse, sur chaque
     section d'une page ça retarde la lecture, et l'œil finit par attendre
     le balayage au lieu de lire.

     Il prend donc la même entrée que le texte courant : un fondu et six
     pixels. Le sur-titre garde son écriture manuscrite, c'est la POLICE
     qui porte la main, pas l'animation. */
  [data-arr="main"] {
    opacity: 0; transform: translateY(6px); filter: blur(4px);
    transition: opacity .44s ease var(--d, 0s),
                filter .5s ease var(--d, 0s),
                transform .52s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
  }
  [data-arr="main"].est-la { opacity: 1; transform: none; filter: none; }

  /* LE TITRE · il remonte de derrière sa propre ligne. Pas de
     fondu : un texte à moitié transparent est un texte sale, et sur
     un titre de 46 px ça se voit. Le débord de 45 % en bas laisse
     passer les jambages ; celui du haut, les accents. */
  [data-arr="ligne"] {
    opacity: 0;
    transform: translateY(.16em);
    filter: blur(9px);
    transition: opacity .5s ease var(--d, 0s),
                filter .78s cubic-bezier(.22, 1, .36, 1) var(--d, 0s),
                transform .74s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
  }
  [data-arr="ligne"].est-la { opacity: 1; transform: none; filter: none; }

  /* LE TEXTE COURANT · lui a le droit au fondu, il n'a pas de
     dessin à abîmer. Le glissement est minuscule : 8 px, juste de
     quoi qu'il arrive APRÈS le titre et pas avec lui. */
  [data-arr="suite"] {
    opacity: 0; transform: translateY(8px); filter: blur(4px);
    transition: opacity .5s ease var(--d, 0s),
                filter .58s ease var(--d, 0s),
                transform .6s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
  }
  [data-arr="suite"].est-la { opacity: 1; transform: none; filter: none; }

  /* ⚠️ LA CARTE NE SE POSE PLUS DE TRAVERS · 1er septembre 2026.
     `--gite` faisait arriver chaque carte avec une inclinaison
     différente, pour imiter une main qui pose. Vu une fois, c'est
     charmant ; vu à chaque section, ça donne l'impression que la page
     n'est pas droite, et c'est exactement ce que Noah appelle « pas
     très professionnel ». On garde la descente et le changement
     d'échelle, qui disent « ça se pose » sans faire pencher la page. */
  [data-arr="pose"] {
    opacity: 0;
    transform: translateY(-6px) scale(.968);
    transition: opacity .44s ease var(--d, 0s),
                transform .62s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
  }
  [data-arr="pose"].est-la { opacity: 1; transform: none; }

  /* LE TRAIT · il se tire, depuis la gauche. */
  [data-arr="trait"] {
    transform: scaleX(0); transform-origin: left center;
    transition: transform .72s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
  }
  [data-arr="trait"].est-la { transform: none; }

  /* LA GRANDE IMAGE · elle vient de plus loin. Pas de glissement du
     tout : sur un visuel large, un déplacement vertical fait sauter
     la page. Un changement d'échelle, lui, ne bouge pas le centre. */
  [data-arr="souffle"] {
    opacity: 0; transform: scale(1.035);
    transition: opacity .66s ease var(--d, 0s),
                transform .95s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
  }
  [data-arr="souffle"].est-la { opacity: 1; transform: none; }

  /* LA LIGNE DE TABLEAU · elle s'ouvre depuis le haut. Un tableau
     dont les lignes se posent l'une après l'autre se lit comme une
     liste qu'on remplit, ce qui est exactement ce qu'il est. */
  /* ⚠️ PLUS D'ENTRÉE PAR LE CÔTÉ · 1er septembre 2026.
     Noah : « les animations où le texte apparaît de droite à gauche, je
     trouve ça vraiment moche, pas très professionnel ».

     Il a raison, et la raison est mécanique : un texte qui arrive
     latéralement traverse la colonne de lecture. L'œil, qui se cale sur
     le bord gauche pour lire, voit ce bord bouger, donc il attend au
     lieu de lire. Une montée de quelques pixels ne déplace jamais le
     début de la ligne : elle se remarque à peine, et c'est exactement ce
     qu'on veut d'une animation d'entrée.

     Toutes les entrées passent donc en Y, avec la même courbe. */
  [data-arr="rang"] {
    opacity: 0; transform: translateY(8px); filter: blur(3px);
    transition: opacity .38s ease var(--d, 0s),
                filter .46s ease var(--d, 0s),
                transform .5s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
  }
  [data-arr="rang"].est-la { opacity: 1; transform: none; filter: none; }

  /* LE TITRE, MOT À MOT · deuxième version, 27 août au soir. Noah :
     « vraiment plus d'animations, plus de trucs sympas pour humaniser
     le site ». Un titre qui monte d'un bloc, c'est un panneau qu'on
     lève ; un titre dont les mots arrivent l'un après l'autre, c'est
     quelqu'un qui l'écrit. Le découpage en mots est fait par le
     script, le CSS ne sait pas couper une phrase. */
  [data-arr="mots"] > .cb-mot {
    display: inline-block;
    opacity: 0;
    transform: translateY(.18em);
    filter: blur(8px);
    transition: opacity .42s ease var(--dm, 0s),
                filter .62s cubic-bezier(.22, 1, .36, 1) var(--dm, 0s),
                transform .62s cubic-bezier(.22, 1, .36, 1) var(--dm, 0s);
  }
  [data-arr="mots"].est-la > .cb-mot { opacity: 1; transform: none; filter: none; }

  /* LA LIGNE D'UNE LISTE · elle arrive par la gauche, et sa coche
     éclot juste après. Une liste de bénéfices qui se coche toute
     seule, c'est le geste le plus proche de quelqu'un qui vous
     montre du doigt en énumérant. */
  [data-arr="ligneliste"] {
    opacity: 0; transform: translateY(9px); filter: blur(3px);
    transition: opacity .4s ease var(--d, 0s),
                filter .48s ease var(--d, 0s),
                transform .52s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
  }
  [data-arr="ligneliste"].est-la { opacity: 1; transform: none; filter: none; }
  [data-arr="ligneliste"] svg, [data-arr="ligneliste"] [class*="icon"] {
    transform: scale(0); transform-origin: center;
    transition: transform .42s cubic-bezier(.2, .9, .3, 1.5) calc(var(--d, 0s) + .16s);
  }
  [data-arr="ligneliste"].est-la svg,
  [data-arr="ligneliste"].est-la [class*="icon"] { transform: none; }

  /* CE QUI ÉCLOT · badges, pastilles, boutons. Un rebond très court
     et sans excès : Noah n'aime « pas de slamming, pas de bounce
     agressif » (ava/APPRENTISSAGE.md). 1.5 sur la courbe, pas 3. */
  [data-arr="eclot"] {
    opacity: 0; transform: scale(.82);
    transition: opacity .3s ease var(--d, 0s),
                transform .46s cubic-bezier(.2, .9, .3, 1.5) var(--d, 0s);
  }
  [data-arr="eclot"].est-la { opacity: 1; transform: none; }

  /* LA PAGE QU'ON POSE À PLAT · un basculement de quelques degrés qui
     se redresse. Réservé aux grandes maquettes : c'est le geste de
     poser une feuille sur une table, et il ne vaut que sur un objet
     assez grand pour qu'on voie la perspective. */
  [data-arr="bascule"] {
    opacity: 0;
    transform: perspective(1400px) rotateX(7deg) translateY(14px) scale(.985);
    transform-origin: center top;
    transition: opacity .6s ease var(--d, 0s),
                transform .95s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
  }
  [data-arr="bascule"].est-la { opacity: 1; transform: none; }

  /* TOUT CE QUI EST DESSINÉ À LA MAIN SE DESSINE · les flèches des
     notes, les mots rayés, les traits. Le contour est découpé en un
     seul tiret aussi long que le tracé et décalé de toute sa
     longueur : invisible, puis on ramène le décalage à zéro et la
     main dessine. C'est le geste signature de la DA, il n'y avait
     aucune raison qu'il reste réservé au mot cerné. */
  [data-arr="dessin"] path {
    stroke-dasharray: 100; stroke-dashoffset: 100;
    transition: stroke-dashoffset .8s cubic-bezier(.4, 0, .3, 1) var(--d, 0s);
  }
  [data-arr="dessin"].est-la path { stroke-dashoffset: 0; }

}

/* ---------- LES DEUX EFFETS CONTINUS, ORDINATEUR SEULEMENT ----------
   Ceux-là gardent leur seuil de largeur, et pour une raison technique
   précise : ils s'appuient sur une variable réécrite à CHAQUE image de
   défilement. Sur iOS, les événements de défilement sont gelés pendant
   l'inertie, le décor resterait en arrière puis rattraperait tout son
   retard d'un coup à l'arrêt du doigt. Un décor qui saute est pire
   qu'un décor immobile. */
@media (min-width: 1024px) {
  /* ---------- LES DEUX EFFETS CONTINUS ----------
     Ceux-là ne sont pas des arrivées : ils vivent tant qu'on défile.
     C'est ce qui fait la différence entre une page qui s'anime une
     fois et une page qui bouge. Le script écrit la variable, le
     navigateur compose, aucune de ces deux règles ne déclenche de
     recalcul de mise en page. */

  /* LE PAPIER DÉRIVE · la réglure du cahier avance un peu moins vite
     que le texte posé dessus. Deux plans au lieu d'un, et c'est le
     décor même de la DA qui prend de la profondeur, pas un objet
     ajouté par-dessus. */
  .cb-page > .cb-reglure { transform: translateY(var(--derive, 0px)); will-change: transform; }

  /* LA MAQUETTE FLOTTE · quelques pixels de retard sur le défilement,
     pas plus. Au-delà, ce n'est plus de la profondeur, c'est un
     élément qui se décroche de sa légende. */
  [data-flotte] { transform: translateY(var(--flot, 0px)); will-change: transform; }
}

/* ============================================================
   LES ARRIVÉES SUR TÉLÉPHONE · 31 août 2026
   ------------------------------------------------------------
   Les mêmes gestes, pas le même tempo. Au pouce on descend deux à
   trois fois plus vite qu'à la molette : un enchaînement calibré
   pour un grand écran se termine SOUS le pouce, c'est-à-dire dans
   le dos de la personne qui lit. Pire, une liste qui se coche en
   1,3 s laisse ses trois dernières lignes invisibles au moment où
   on arrive dessus.

   Deux réglages, et deux seulement :
     · les retards sont comprimés, coefficient 0,55, posé par le
       script (shared.jsx, `TEMPO_TEL`) ;
     · les deux gestes les plus lents, la grande image et la page
       qu'on pose à plat, repassent sous 0,6 s.

   Les DISTANCES ne bougent pas. 8 px restent 8 px : sur un écran
   étroit elles se lisent même mieux, et les réduire n'aurait fait
   qu'effacer l'animation qu'on vient d'allumer.
   ============================================================ */
html[data-arr-tel] [data-arr="souffle"] {
  transition: opacity .42s ease var(--d, 0s),
              transform .58s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
}
html[data-arr-tel] [data-arr="bascule"] {
  transition: opacity .4s ease var(--d, 0s),
              transform .6s cubic-bezier(.22, 1, .36, 1) var(--d, 0s);
}

/* Le réglage système gagne, toujours : plus une seule des règles
   ci-dessus ne s'applique, et tout est là dès la première image. */
@media (prefers-reduced-motion: reduce) {
  [data-arr], [data-arr] > .cb-mot {
    opacity: 1 !important; transform: none !important;
    clip-path: none !important; filter: none !important;
    transition: none !important;
  }
}

/* ============================================================
   LE PRODUIT QUI SE TRANSFORME · 27 août 2026
   ------------------------------------------------------------
   Le composant est dans app/src/home-rest.jsx, bloc du même nom.
   Deux dessins, et ils n'ont rien à voir l'un avec l'autre :

   · TÉLÉPHONE (sous 1024 px), trois cartes COUCHÉES, dessin à
     gauche, texte à droite. C'est exactement ce qu'il y avait
     avant le 27 août, et Noah a demandé qu'on le lui rende :
     « tu m'as aussi modifié sur les mobiles alors que le mobile
     m'allait complètement ». Le nouveau balisage n'est pas celui
     de l'ancienne carte, donc la mise en page passe par une
     GRILLE À DEUX COLONNES (dessin | titre + texte) plutôt que
     par la rangée souple d'avant. À l'écran, c'est le même objet.

   · ORDINATEUR (1024 px et plus), les étapes descendent à
     gauche le long d'un rail qui se remplit, et une fenêtre de
     produit reste épinglée à droite en changeant d'état.
   ============================================================ */

/* ---------- CE QUI VAUT AUX DEUX TAILLES ---------- */
.cbm-viv-etape h3 {
  margin: 0; font-size: 20px; line-height: 1.2; letter-spacing: -0.015em;
  font-family: var(--ff-text); font-weight: 640;
  display: flex; align-items: center; gap: 10px;
}
.cbm-viv-etape p {
  margin: 10px 0 0; font-size: 15.5px; line-height: 1.6; color: var(--ink-3);
}
.cbm-viv-ecran { display: none; }
.cbm-viv-fin { text-align: center; padding-top: clamp(22px, 4vw, 34px); }
.cbm-viv-fin .cbm-outils-cta { margin-top: clamp(18px, 2.4vw, 28px); }

/* ============================================================
   1 · TÉLÉPHONE ET TABLETTE · les trois cartes couchées
   ============================================================ */
@media (max-width: 1023px) {
  /* ============================================================
     LES ONGLETS DU TÉLÉPHONE · 31 août 2026
     ------------------------------------------------------------
     Ils remplacent la pile de trois cartes. Le raisonnement complet
     est dans home-rest.jsx, bloc « LES TROIS OUTILS AU POUCE » ;
     ici, seulement ce que le dessin doit tenir.
     ============================================================ */
  .cbm-viv-liste { display: none; }
  .cbm-viv-num,
  .cbm-viv-detail { display: none; }

  .cbm-tel-outils { max-width: 520px; margin: 0 auto; }

  /* LA RANGÉE D'ONGLETS · un rail de fond, trois parts égales. Les
     parts sont en 1fr et non en contenu : trois libellés de longueurs
     différentes donneraient trois cibles de tailles différentes, et
     c'est la plus petite qui déciderait du confort de tout le bloc. */
  .cbm-tel-onglets {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 4px; padding: 4px;
    background: color-mix(in oklab, var(--ink) 5%, transparent);
    border-radius: 14px;
  }
  .cbm-tel-onglet {
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    /* 40 px de haut : en dessous, la cible passe sous le seuil confortable
       du pouce, et on touche l'onglet d'à côté une fois sur cinq. */
    min-height: 40px; padding: 0 6px;
    border: none; border-radius: 11px; background: transparent;
    font-family: inherit; font-size: 13.5px; font-weight: 600;
    color: var(--ink-3); cursor: pointer;
    transition: background .24s ease, color .24s ease, box-shadow .24s ease;
  }
  .cbm-tel-onglet svg { color: currentColor; flex-shrink: 0; }
  .cbm-tel-onglet.est-actif {
    background: var(--surface);
    color: var(--ton, var(--accent));
    box-shadow: 0 6px 16px -8px rgba(20, 18, 45, .4);
  }

  /* LES TROIS VOLETS DANS UNE SEULE CASE · c'est la ligne qui empêche
     la page de sauter. Ils sont tous les trois en 1/1 : le bloc prend
     la hauteur du plus long, définitivement, et changer d'onglet ne
     déplace plus rien de ce qui suit. */
  .cbm-tel-volets {
    display: grid; margin-top: 14px;
    /* Le navigateur garde le défilement vertical quoi qu'il arrive :
       le balayage horizontal est à nous, la descente reste à lui. */
    touch-action: pan-y;
  }
  .cbm-tel-volet {
    grid-area: 1 / 1;
    padding: 16px 16px 18px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 18px;
    box-shadow: 0 20px 44px -34px rgba(20, 18, 45, .35);
    opacity: 0; visibility: hidden;
    transform: translateY(6px);
    transition: opacity .28s ease, transform .34s cubic-bezier(.22,1,.36,1),
                visibility 0s linear .34s;
  }
  .cbm-tel-volet.est-actif {
    opacity: 1; visibility: visible; transform: none;
    transition: opacity .28s ease, transform .34s cubic-bezier(.22,1,.36,1),
                visibility 0s;
  }
  /* L'APERÇU GARDE SA HAUTEUR D'ORIGINE. Premier essai à 176 px, pour
     qu'il soit le sujet du volet plutôt qu'une vignette : raté. Ces
     dessins sont calibrés au pixel pour un cadre de 138 px (cf. le bloc
     LES APERÇUS DE VIGNETTE dans home-rest.jsx) ; agrandi, le cadre
     s'étire mais la carte de fidélité, elle, garde sa taille et se
     retrouve à flotter au milieu de 40 px de vide en haut comme en bas.
     Le volet est déjà deux fois plus large que l'ancienne vignette :
     l'aperçu y gagne assez de présence sans qu'on touche à sa hauteur. */
  .cbm-tel-vue { margin-bottom: 13px; }
  .cbm-tel-volet h3 {
    margin: 0; font-size: 18px; letter-spacing: -0.02em;
    color: var(--ton, var(--ink));
  }
  .cbm-tel-volet p { margin: 6px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-2); }
  .cbm-tel-detail {
    list-style: none; padding: 0; margin: 12px 0 0;
    display: flex; flex-direction: column; gap: 6px;
  }
  .cbm-tel-detail li {
    display: flex; align-items: flex-start; gap: 8px;
    font-size: 12.5px; color: var(--ink-3);
  }
  .cbm-tel-detail svg { color: var(--ton, var(--accent)); flex-shrink: 0; margin-top: 2px; }

  /* Le réglage système : on change d'onglet, on ne glisse pas. */
  @media (prefers-reduced-motion: reduce) {
    .cbm-tel-volet, .cbm-tel-onglet { transition: none; }
    .cbm-tel-volet { transform: none; }
  }

  /* La carte d'avant, refaite en grille : le dessin occupe une
     colonne entière sur les deux lignes, le titre et le texte se
     rangent dans l'autre. */
  .cbm-viv-etape {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas: "vign titre" "vign texte";
    align-items: center;
    column-gap: 14px; row-gap: 4px;
    padding: 14px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: 18px;
    box-shadow: 0 20px 44px -34px rgba(20, 18, 45, .35);
  }
  .cbm-viv-etape h3 { grid-area: titre; font-size: 17px; }
  .cbm-viv-etape p { grid-area: texte; margin: 0; font-size: 13px; }
  .cbm-viv-vignette { grid-area: vign; }
  .cbm-pilier-ico {
    width: 30px; height: 30px; border-radius: 10px; flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: color-mix(in srgb, var(--pilier-ton) 14%, transparent);
    color: var(--pilier-ton);
  }
}
@media (max-width: 560px) {
  .cbm-viv-etape { padding: 11px; column-gap: 12px; }
  .cbm-viv-etape h3 { font-size: 15.5px; gap: 8px; }
  .cbm-viv-etape p { font-size: 12.5px; line-height: 1.45; }
  .cbm-pilier-ico { width: 25px; height: 25px; border-radius: 8px; }
  .cbm-pilier-ico svg { width: 13px; height: 13px; }
}

/* ============================================================
   2 · ORDINATEUR · le rail, les étapes, la fenêtre épinglée
   ============================================================ */
@media (min-width: 1024px) {
  /* Les onglets du téléphone n'ont rien à faire ici : le rail dit la
     même chose, en mieux, dès qu'il y a la place. */
  .cbm-tel-outils { display: none; }

  /* La rustine « #cb-outils .cbm-wrap.cbm-section { padding-inline: 28px } »
   a été retirée le 31 août 2026 : `.cbm-section` ne déclare plus que sa
   verticale (`padding-block`), elle ne reprend donc plus à `.cbm-wrap` la
   gouttière que celle-ci vient de poser. Le défaut est corrigé à sa
   source, pour toutes les sections d'un coup. */

  .cbm-viv {
    display: grid;
    grid-template-columns: minmax(0, 0.94fr) minmax(0, 1.06fr);
    gap: clamp(38px, 4.6vw, 82px);
    align-items: start;
  }
  /* L'aperçu vit dans la fenêtre épinglée, plus sous le texte. */
  .cbm-viv-vignette { display: none; }
  .cbm-viv-ecran { display: flex; }

  /* ---------- LE RAIL ----------
     Un trait pâle sur toute la hauteur des trois étapes, et
     par-dessus le même trait en couleur dont la HAUTEUR suit
     l'étape lue. Les numéros sont posés dessus comme des jalons.

     POURQUOI IL EXISTE : trois blocs de texte séparés par une
     demi-page de gris ne font pas un rythme, ils font des trous.
     Un trait qui les relie transforme le vide en marche, c'est
     la réponse au « je le trouve un peu vide » de Noah, et elle
     n'ajoute pas un mot à lire. */
  .cbm-viv-liste { position: relative; }
  .cbm-viv-liste::before,
  .cbm-viv-liste::after {
    content: ""; position: absolute;
    left: 11px; top: 0; bottom: 0; width: 14px;
    z-index: 0;                     /* DERRIÈRE les jalons · cf. plus bas */
    pointer-events: none;

    /* ---------- DES COUPS DE CRAYON, PAS DES POINTS ----------
       Deuxième version, 27 août au soir. Noah : « ça fait pas assez
       crayon, coup de crayon en pointillé. Je parlais plus genre de
       traits un peu comme des pointillés, coups de crayon. »

       Une rangée de points ronds, même décalée, reste un motif de
       machine : un point n'a ni sens ni longueur, donc pas de geste.
       Un TRAIT en a un, il part, il tourne un peu, il s'arrête.

       COMMENT C'EST FAIT, ET POURQUOI PAS AUTREMENT. Le motif est un
       petit SVG de deux traits courbes, répété tous les 26 px, mais
       il sert de MASQUE, pas d'image de fond. C'est la seule façon de
       garder un dessin vectoriel ET une couleur qui change : la teinte
       du rail suit l'outil qu'on lit (var(--ton-rail)), et une image
       SVG en fond aurait sa couleur écrite en dur dedans.
       Deuxième raison, moins évidente : un SVG étiré en hauteur sur
       800 px aurait aplati ses courbes. Répété à taille fixe, chaque
       coup de crayon garde exactement le dessin qu'on lui a donné. */
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 26'%3E%3Cg fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M8.3 3c-1.1 2.4-1.6 4.9-1.5 7.4'/%3E%3Cpath d='M6 15.4c.4 2.5 1.1 4.8 2.1 7'/%3E%3C/g%3E%3C/svg%3E");
            mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 26'%3E%3Cg fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M8.3 3c-1.1 2.4-1.6 4.9-1.5 7.4'/%3E%3Cpath d='M6 15.4c.4 2.5 1.1 4.8 2.1 7'/%3E%3C/g%3E%3C/svg%3E");
    -webkit-mask-size: 14px 26px;    mask-size: 14px 26px;
    -webkit-mask-repeat: repeat-y;   mask-repeat: repeat-y;
    -webkit-mask-position: center top; mask-position: center top;
  }

  /* LES DEUX BOUTS S'ÉTEIGNENT, et c'est le FOND qui s'en charge, pas un
     deuxième masque. Le premier jalon est au milieu de son étape, donc à
     un tiers d'écran du haut de la liste : des traits qui commenceraient
     net là-haut auraient l'air coupés. Superposer deux masques aurait
     demandé mask-composite, mal supporté ; un dégradé de fond qui part du
     transparent fait exactement le même travail, partout. */
  .cbm-viv-liste::before {
    background-image: linear-gradient(180deg,
      transparent 0,
      color-mix(in oklab, var(--ink) 30%, transparent) 12%,
      color-mix(in oklab, var(--ink) 30%, transparent) 88%,
      transparent 100%);
  }
  /* LA PROGRESSION SE FAIT AU DÉCOUPAGE, PAS À LA HAUTEUR. Les deux
     traits font la même hauteur et portent le même dégradé : le coloré
     est simplement rogné par le bas. Sans ça, son dégradé se serait
     recalculé sur sa propre hauteur à chaque étape, et le fondu du haut
     aurait bougé sous les yeux. */
  .cbm-viv-liste::after {
    background-image: linear-gradient(180deg,
      transparent 0,
      var(--ton-rail, var(--accent)) 12%,
      var(--ton-rail, var(--accent)) 88%,
      transparent 100%);
    clip-path: inset(0 0 calc(100% - var(--avance, 0%)) 0);
    transition: clip-path .6s cubic-bezier(.22, 1, .36, 1);
  }

  /* CHAQUE ÉTAPE PREND UN TIERS DE PAGE, PAS PLUS. C'est le réglage qui
     décide de la longueur de tout le bloc. La grille à deux colonnes
     met le numéro sur le rail, en face du titre. */
  .cbm-viv-etape {
    position: relative;
    /* AU-DESSUS DU POINTILLÉ. Sans ce z-index, l'étape et le pseudo-élément
       `::after` du rail sont tous les deux positionnés sans z-index : c'est
       l'ordre du document qui tranche, et le pseudo, dernier enfant, passe
       PAR-DESSUS le numéro. Noah : « fais gaffe que le trait ne traverse pas
       le 1 2 3 ». C'était exactement ça. */
    z-index: 1;
    display: grid;
    grid-template-columns: 36px minmax(0, 1fr);
    grid-template-areas: "num titre" "vide texte" "vide detail";
    column-gap: 20px;
    align-content: center;
    min-height: 34vh;
    /* Les étapes qu'on ne lit pas s'effacent : c'est ce qui dit à l'œil
       laquelle commande la fenêtre de droite. */
    opacity: .34;
    transition: opacity .45s ease;
  }
  .cbm-viv-etape.est-actif { opacity: 1; }

  /* TOUT CE QUI S'ÉCRIT À GAUCHE EST PLUS GROS · 27 août au soir.
     Noah : « toutes les écritures à gauche, essaye de les agrandir un
     peu, il y a encore de la place à l'écran ». Il a raison, et c'est
     mesurable : à 1 680 px la colonne fait 460 px de large et les lignes
     s'arrêtaient à 62 caractères, bien en dessous des 75 qu'on peut se
     permettre. Le titre passe de 20 à 24 px, le texte de 15,5 à 17, les
     étiquettes de 12,5 à 13,5. La colonne ne s'élargit pas d'un pixel :
     c'est le TEXTE qui remplit enfin la place qu'il avait déjà. */
  .cbm-viv-etape h3 { grid-area: titre; font-size: 24px; gap: 12px; }
  .cbm-viv-etape p { grid-area: texte; margin-top: 12px; font-size: 17px; line-height: 1.62; }

  /* La pastille de l'outil · elle n'existait que sur téléphone. Devant un
     titre de 24 px, un glyphe posé nu paraissait orphelin. */
  .cbm-pilier-ico {
    width: 34px; height: 34px; border-radius: 11px; flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: color-mix(in srgb, var(--ton, var(--accent)) 12%, transparent);
    color: var(--ton, var(--accent));
  }
  .cbm-pilier-ico svg { width: 17px; height: 17px; }

  /* ---------- LE JALON · 27 août 2026 (retour de Noah) ----------
     Il avait un cercle parfait, bordure d'1,5 px et halo : « on dirait des
     tampons de fidélité ». Et le trait lui passait au travers.

     Maintenant : le chiffre seul, posé sur un DISQUE OPAQUE de la couleur
     du fond, c'est lui qui coupe le pointillé, sans qu'on voie jamais où
     il commence, et, sur l'étape qu'on lit, un CERCLE TRACÉ À LA MAIN qui
     se dessine d'un trait. Le tracé n'est pas fermé : il dépasse son point
     de départ, comme un stylo qui ne s'arrête pas au bon endroit. C'est le
     même geste que le mot cerné du titre et que les coups de crayon des
     marges, la DA avait déjà cette main-là, elle n'était pas descendue
     jusqu'ici.

     Le disque est étendu de 6 px par une ombre portée pleine : ça élargit
     la coupure du pointillé jusque SOUS le cercle dessiné, sans changer
     d'un pixel la place que le jalon prend dans la grille. */
  .cbm-viv-num {
    grid-area: num;
    position: relative;
    width: 36px; height: 36px; border-radius: 50%;
    display: grid; place-items: center;
    background: var(--bg-gris);
    box-shadow: 0 0 0 9px var(--bg-gris);
  }
  /* LE CHIFFRE EST ÉCRIT À LA MAIN, ET IL EST GROS. Noah : « la police
     d'écriture des numéros, j'aimerais bien que ça fasse aussi coup de
     crayon, et surtout un peu plus gros parce que là c'est trop petit ».
     C'est la même manuscrite que les sur-titres et que les deux mots du
     héros, deux caractères, donc bien en deçà de la limite des trois
     mots que s'impose cette police (cf. --ff-hand). */
  .cbm-viv-num > b {
    font-family: var(--ff-hand); font-style: italic; font-weight: 400;
    font-size: 25px; line-height: 1; letter-spacing: 0;
    color: color-mix(in oklab, var(--ink) 40%, transparent);
    transition: color .4s ease;
  }
  .cbm-viv-etape.est-actif .cbm-viv-num > b { color: var(--ton, var(--accent)); }

  /* LE CERCLE SE DESSINE · même mécanique que le mot cerné : le contour
     est découpé en un seul tiret aussi long que le tracé et décalé de
     toute sa longueur, donc invisible, puis on ramène le décalage à
     zéro et la main tourne. `pathLength="100"` est posé côté JSX, ce qui
     rend ces deux valeurs justes quelle que soit la taille à l'écran. */
  .cbm-viv-num > svg {
    position: absolute; left: -7px; top: -7px;
    width: 50px; height: 50px; overflow: visible;
    color: var(--ton, var(--accent));
    pointer-events: none;
  }
  .cbm-viv-num > svg path {
    fill: none; stroke: currentColor; stroke-width: 1.9;
    stroke-linecap: round; vector-effect: non-scaling-stroke;
    /* « 100 220 » et pas « 100 » tout court, et ça se voit à l'écran :
       avec une seule valeur, le motif est « trait 100 / vide 100 », et
       au décalage 100 on retombe pile sur le DÉBUT du trait suivant -
       un trait de longueur nulle, que le bout rond transforme en petit
       point resté sur le côté du chiffre. Un vide plus long que le
       tracé, et il n'y a plus rien du tout. */
    stroke-dasharray: 100 220; stroke-dashoffset: 100;
    transition: stroke-dashoffset .8s cubic-bezier(.4, 0, .3, 1);
  }
  .cbm-viv-etape.est-actif .cbm-viv-num > svg path { stroke-dashoffset: 0; }

  /* ---------- LES TROIS ÉTIQUETTES ----------
     Trois mots chacune, et rien de plus. Elles occupent la place
     laissée sous le paragraphe et disent une chose neuve, c'est
     l'inverse d'un remplissage, qui répéterait le paragraphe en
     plus court. */
  .cbm-viv-detail {
    grid-area: detail;
    list-style: none; margin: 18px 0 0; padding: 0;
    display: flex; flex-wrap: wrap; gap: 8px;
  }
  .cbm-viv-detail li {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 6px 14px 6px 11px;
    border-radius: 999px;
    font-size: 13.5px; font-weight: 520; color: var(--ink-2);
    background: color-mix(in srgb, var(--ton, var(--accent)) 6%, var(--surface));
    border: 1px solid color-mix(in srgb, var(--ton, var(--accent)) 20%, var(--line));
  }
  .cbm-viv-detail svg { color: var(--ton, var(--accent)); flex-shrink: 0; }

  /* LA COURSE APRÈS LA DERNIÈRE ÉTAPE. Un écran épinglé se décroche quand
     le bas de sa colonne le rattrape : la durée d'épinglage vaut
     (hauteur de la colonne − hauteur de l'écran). Sans rien derrière la
     troisième étape, l'écran partait donc AVANT qu'on la lise, mesuré :
     il se décrochait à 703 px alors que l'étape 3 commençait à 836.
     C'est mathématique, aucun réglage de hauteurs ne le répare : il FAUT
     de la place derrière. Autant qu'elle serve, c'est la note et le
     bouton, qui se lisent pendant que la dernière fenêtre est encore là. */
  .cbm-viv-fin { padding: clamp(26px, 4vh, 46px) 0 clamp(130px, 20vh, 240px); }

  /* LE PIÈGE, MESURÉ : L'ÉCRAN NE COLLAIT PAS.
     `.cb-page`, la section réglée comme une page de cahier, porte
     `overflow: hidden`, et un ancêtre qui découpe empêche TOUT descendant
     de coller. Le `overflow-x: clip` de `.cbm-home`, lui, ne gêne pas :
     découper UN SEUL axe laisse l'autre en `visible` (mesuré le 27 août).
     C'est `hidden`, sur les deux axes, qui tue le collant.
     Ce `hidden` ne sert à rien ici : la réglure est en `inset: 0`, elle ne
     peut pas déborder. On le rouvre donc pour cette section, et pour elle
     seule, les autres `.cb-page` gardent le leur.
     À RETENIR : avant de chercher pourquoi un collant ne colle pas,
     remonter la chaîne des parents et lire leur `overflow`. */
  #cb-outils.cb-page { overflow: visible; }

  /* ---------- LA FENÊTRE ÉPINGLÉE ----------
     `top` la cale sous la barre flottante, qui fait 74 px une fois
     collée. Pas de mesure en JavaScript : cette colonne n'a pas
     besoin d'être au pixel près. */
  .cbm-viv-ecran {
    position: sticky; top: clamp(92px, 13vh, 132px);
    height: min(64vh, 560px);
    display: flex; align-items: center;
  }
  /* Un halo à la teinte de l'outil, DERRIÈRE la fenêtre. Il tient la
     fenêtre dans la page au lieu de la laisser flotter sur un aplat
     gris, et c'est le seul élément de couleur du fond. */
  .cbm-viv-ecran::before {
    content: ""; position: absolute; inset: -10% -8%;
    border-radius: 50%;
    background: radial-gradient(56% 50% at 50% 48%,
      color-mix(in srgb, var(--ton-actif, var(--accent)) 26%, transparent) 0%,
      transparent 72%);
    opacity: .8; pointer-events: none;
    transition: background .55s ease;
  }
  .cbm-viv-fenetre {
    position: relative; width: 100%; height: 100%;
    display: flex; flex-direction: column;
    border-radius: 20px; overflow: hidden;
    background: var(--surface);
    border: 1px solid color-mix(in oklab, var(--ton-actif, var(--accent)) 28%, var(--line));
    box-shadow: 0 44px 78px -46px rgba(20, 18, 45, .5),
                0 3px 12px -8px rgba(20, 18, 45, .16);
    transition: border-color .55s ease;
  }

  /* LA BARRE · elle donne à l'objet son statut de fenêtre, et elle
     porte le mot « Aperçu », la mention qui rend honnête un écran
     rempli d'exemples. */
  .cbm-viv-barre {
    flex-shrink: 0; height: 46px;
    display: flex; align-items: center; gap: 10px;
    padding: 0 14px;
    border-bottom: 1px solid var(--line);
    background: color-mix(in oklab, var(--ton-actif, var(--accent)) 5%, var(--surface));
    transition: background-color .55s ease;
  }
  .cbm-viv-marque {
    width: 22px; height: 22px; border-radius: 7px; flex-shrink: 0;
    display: grid; place-items: center;
    background: var(--ink); color: #fff;
  }
  .cbm-viv-onglet { font-size: 13px; font-weight: 620; color: var(--ink-2); }
  .cbm-viv-apercu {
    margin-left: auto;
    font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
    color: var(--ink-4);
    border: 1px solid var(--line); border-radius: 999px;
    padding: 3px 9px;
  }

  .cbm-viv-corps { flex: 1; min-height: 0; display: flex; }

  /* LE RAIL D'OUTILS · trois icônes dans UNE fenêtre. C'est lui qui
     démontre « trois outils, un seul endroit » : le titre de la
     section n'a plus besoin d'être répété en dessous. */
  .cbm-viv-rangee {
    flex-shrink: 0; width: 54px;
    display: flex; flex-direction: column; align-items: center; gap: 8px;
    padding: 12px 0;
    border-right: 1px solid var(--line);
    background: color-mix(in oklab, var(--ink) 2.5%, var(--surface));
  }
  .cbm-viv-ico {
    width: 32px; height: 32px; border-radius: 10px;
    display: grid; place-items: center;
    color: var(--ink-4);
    transition: background-color .4s ease, color .4s ease;
  }
  .cbm-viv-ico.est-actif {
    background: color-mix(in srgb, var(--ton, var(--accent)) 14%, transparent);
    color: var(--ton, var(--accent));
  }

  /* LES TROIS ÉTATS SONT SUPERPOSÉS · un seul est visible. La fenêtre ne
     bouge donc jamais : c'est le MÊME produit qui change, pas trois
     images qui défilent. Toute la démonstration tient là. */
  .cbm-viv-toile { position: relative; flex: 1; min-width: 0; }
  .cbm-viv-etat {
    position: absolute; inset: 0;
    padding: 16px;
    opacity: 0; transform: scale(.975) translateY(8px);
    transition: opacity .42s ease, transform .55s cubic-bezier(.22, 1, .36, 1);
    pointer-events: none;
  }
  .cbm-viv-etat.est-actif { opacity: 1; transform: none; }

  /* ============================================================
     LES TROIS ÉCRANS · dessinés pour cette taille-là
     ------------------------------------------------------------
     Ils remplacent les vignettes agrandies, qui à 460 px ne
     montraient plus un produit mais un écran en train de charger.
     Ici : des heures, des prénoms, des prestations, des tampons.
     ============================================================ */
  .ov { height: 100%; display: flex; flex-direction: column; }

  /* ---- L'AGENDA ---- */
  .ov-agenda { gap: 12px; }
  .ov-barre { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
  .ov-seg {
    font-size: 11.5px; font-weight: 600; color: var(--ink-4);
    padding: 4px 10px; border-radius: 8px;
  }
  .ov-seg.est-actif {
    background: color-mix(in srgb, var(--ton) 13%, transparent);
    color: color-mix(in oklab, var(--ton) 78%, var(--ink));
  }
  .ov-barre-fin { margin-left: auto; font-size: 11px; color: var(--ink-4); }
  .ov-semaine {
    flex: 1; min-height: 0;
    display: grid; grid-template-columns: 30px repeat(5, minmax(0, 1fr)); gap: 6px;
  }
  /* Les heures s'alignent sur la colonne, pas sur l'en-tête du jour :
     le retrait du haut vaut la hauteur de cet en-tête. */
  .ov-heures {
    display: flex; flex-direction: column; justify-content: space-between;
    padding: 30px 0 0; text-align: right;
  }
  .ov-heures span { font-size: 10px; color: var(--ink-4); font-variant-numeric: tabular-nums; }
  .ov-jour { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
  .ov-jour-tete {
    text-align: center; font-size: 10px; line-height: 1.1;
    color: var(--ink-4); text-transform: lowercase;
  }
  .ov-jour-tete b {
    display: block; margin-top: 2px;
    font-size: 13px; font-weight: 650; color: var(--ink-2);
  }
  .ov-jour.est-auj .ov-jour-tete,
  .ov-jour.est-auj .ov-jour-tete b { color: var(--ton); }
  .ov-col {
    position: relative; flex: 1; border-radius: 10px;
    background: color-mix(in srgb, var(--ton) 6%, transparent);
    border: 1px solid color-mix(in srgb, var(--ton) 10%, transparent);
  }
  .ov-jour.est-auj .ov-col {
    background: color-mix(in srgb, var(--ton) 13%, transparent);
    border-color: color-mix(in srgb, var(--ton) 22%, transparent);
  }
  .ov-rdv {
    position: absolute; left: 4px; right: 4px;
    border-radius: 9px; padding: 6px 8px; overflow: hidden;
    background: color-mix(in srgb, var(--ton) 92%, #fff);
    color: #fff;
    box-shadow: 0 6px 14px -8px color-mix(in srgb, var(--ton) 85%, #000);
  }
  .ov-rdv b {
    display: block; font-size: 9.5px; font-weight: 700; opacity: .82;
    font-variant-numeric: tabular-nums;
  }
  .ov-rdv > span {
    display: block; font-size: 11.5px; font-weight: 650; line-height: 1.25;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .ov-rdv i {
    display: block; font-style: normal; font-size: 9.5px; opacity: .8;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* Le deuxième rendez-vous d'une même journée est plus clair : deux
     aplats pleins côte à côte dans une colonne de 70 px, ça fait un
     bloc, pas deux rendez-vous. */
  .ov-rdv.est-pale {
    background: color-mix(in srgb, var(--ton) 22%, #fff);
    color: color-mix(in oklab, var(--ton) 72%, var(--ink));
    box-shadow: none;
  }

  /* ---- LES FICHES CLIENTS ---- */
  .ov-clients { gap: 10px; }
  .ov-cherche {
    flex-shrink: 0;
    display: flex; align-items: center; gap: 9px;
    padding: 10px 13px; border-radius: 12px;
    border: 1px solid var(--line);
    background: color-mix(in oklab, var(--ink) 2.5%, var(--surface));
    font-size: 12.5px; color: var(--ink-4);
  }
  .ov-fiches {
    list-style: none; margin: 0; padding: 0;
    flex: 1; min-height: 0;
    display: flex; flex-direction: column; gap: 8px;
  }
  /* Les lignes se PARTAGENT la hauteur au lieu d'être espacées : une
     liste dont les lignes sont serrées et les blancs larges se lit comme
     une liste inachevée. */
  .ov-fiche {
    flex: 1; min-height: 0;
    display: flex; align-items: center; gap: 12px;
    padding: 10px 13px; border-radius: 13px;
    border: 1px solid var(--line); background: var(--surface);
  }
  .ov-ini {
    width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
    display: grid; place-items: center;
    font-size: 13px; font-weight: 700; color: #fff;
    background: linear-gradient(140deg,
      color-mix(in srgb, var(--ton) 82%, #fff),
      color-mix(in srgb, var(--ton) 50%, #fff));
  }
  .ov-ini.t1 { filter: hue-rotate(-34deg); }
  .ov-ini.t2 { filter: hue-rotate(30deg); }
  .ov-ini.t3 { filter: hue-rotate(-62deg); }
  .ov-fiche > div { flex: 1; min-width: 0; }
  .ov-fiche b {
    display: block; font-size: 13.5px; font-weight: 640; color: var(--ink);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .ov-fiche i {
    display: block; font-style: normal; font-size: 11.5px; color: var(--ink-4);
    margin-top: 2px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .ov-tag {
    flex-shrink: 0;
    font-size: 11px; font-weight: 650;
    padding: 4px 10px; border-radius: 999px;
    background: color-mix(in srgb, var(--ton) 12%, transparent);
    color: color-mix(in oklab, var(--ton) 70%, var(--ink));
  }

  /* ---- LA CARTE DE FIDÉLITÉ ---- */
  .ov-fidelite { align-items: center; justify-content: center; gap: 20px; }
  .ov-carte {
    width: 100%; max-width: 370px;
    padding: 22px; border-radius: 20px; color: #fff;
    background: linear-gradient(140deg, var(--ton), color-mix(in srgb, var(--ton) 56%, #fff));
    box-shadow: 0 20px 38px -22px color-mix(in srgb, var(--ton) 85%, #000);
  }
  .ov-carte-tete {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 10px; margin-bottom: 16px;
  }
  .ov-carte-tete b { font-size: 15.5px; font-weight: 680; }
  .ov-carte-tete span {
    font-size: 9.5px; font-weight: 700; letter-spacing: .1em;
    text-transform: uppercase; opacity: .82;
  }
  .ov-tampons { display: grid; grid-template-columns: repeat(5, 1fr); gap: 9px; }
  .ov-tampon {
    aspect-ratio: 1; border-radius: 50%;
    display: grid; place-items: center;
    border: 1.6px dashed rgba(255, 255, 255, .5);
  }
  .ov-tampon.est-pris { background: #fff; border-color: transparent; color: var(--ton); }
  .ov-tampon svg { width: 56%; height: 56%; }
  .ov-carte-pied {
    display: flex; align-items: baseline; justify-content: space-between;
    gap: 10px; margin-top: 16px;
  }
  .ov-carte-pied b { font-size: 19px; font-weight: 700; letter-spacing: -.01em; }
  .ov-carte-pied span { font-size: 12px; opacity: .85; }

  /* QUI APPROCHE DE SA RÉCOMPENSE · la vraie première question de
     l'écran Fidélité du produit. Sans elle, la carte flottait seule
     au milieu d'une grande fenêtre blanche. */
  .ov-proches { width: 100%; max-width: 370px; }
  .ov-proches-t {
    display: block; margin-bottom: 10px;
    font-size: 10px; font-weight: 700; letter-spacing: .1em;
    text-transform: uppercase; color: var(--ink-4);
  }
  .ov-proche {
    display: flex; align-items: center; gap: 12px;
    padding: 8px 0;
  }
  .ov-proche b { font-size: 12.5px; font-weight: 620; color: var(--ink-2); width: 82px; flex-shrink: 0; }
  .ov-jauge {
    flex: 1; height: 7px; border-radius: 999px; overflow: hidden;
    background: color-mix(in srgb, var(--ton) 13%, transparent);
  }
  .ov-jauge i { display: block; height: 100%; border-radius: 999px; background: var(--ton); }
  .ov-proche em {
    font-style: normal; font-size: 11.5px; font-weight: 640;
    color: color-mix(in oklab, var(--ton) 72%, var(--ink));
    font-variant-numeric: tabular-nums; flex-shrink: 0;
  }

  /* ---------- CE QUI SE PASSE QUAND UN ÉTAT ARRIVE ----------
     Les micro-animations sont accrochées à l'arrivée de `est-actif`,
     donc elles se rejouent quand on remonte. `backwards` fait tenir
     l'état de départ pendant le retard : sans lui, l'élément
     apparaîtrait d'abord en place, puis sauterait au début de son
     animation. Ce sont les trois gestes que Noah a validés, on les
     rebranche à l'identique sur le nouveau dessin. */

  /* L'agenda se remplit · les rendez-vous se posent colonne par colonne. */
  .cbm-viv-etat.est-actif .ov-rdv { animation: cbVivPose .5s cubic-bezier(.22, 1, .36, 1) backwards; }
  .cbm-viv-etat.est-actif .ov-jour:nth-child(2) .ov-rdv { animation-delay: .18s; }
  .cbm-viv-etat.est-actif .ov-jour:nth-child(3) .ov-rdv { animation-delay: .28s; }
  .cbm-viv-etat.est-actif .ov-jour:nth-child(4) .ov-rdv { animation-delay: .38s; }
  .cbm-viv-etat.est-actif .ov-jour:nth-child(5) .ov-rdv { animation-delay: .48s; }
  .cbm-viv-etat.est-actif .ov-jour:nth-child(6) .ov-rdv { animation-delay: .58s; }

  /* Les fiches s'ouvrent · elles arrivent par la gauche, l'une après l'autre. */
  .cbm-viv-etat.est-actif .ov-fiche { animation: cbVivGlisse .5s cubic-bezier(.22, 1, .36, 1) backwards; }
  .cbm-viv-etat.est-actif .ov-fiches li:nth-child(1) { animation-delay: .16s; }
  .cbm-viv-etat.est-actif .ov-fiches li:nth-child(2) { animation-delay: .26s; }
  .cbm-viv-etat.est-actif .ov-fiches li:nth-child(3) { animation-delay: .36s; }
  .cbm-viv-etat.est-actif .ov-fiches li:nth-child(4) { animation-delay: .46s; }
  .cbm-viv-etat.est-actif .ov-fiches li:nth-child(5) { animation-delay: .56s; }

  /* Les jauges de fidélité se remplissent · même geste que la barre
     « 82 % rempli » du héros. */
  .cbm-viv-etat.est-actif .ov-jauge i {
    animation: cbVivJauge .8s cubic-bezier(.22, 1, .36, 1) .3s backwards;
    transform-origin: left center;
  }
  @keyframes cbVivJauge { from { transform: scaleX(0); } }

  /* La carte se tamponne · les tampons pris éclosent un par un. */
  .cbm-viv-etat.est-actif .ov-tampon.est-pris { animation: cbVivTampon .42s cubic-bezier(.2, .9, .3, 1.5) backwards; }
  .cbm-viv-etat.est-actif .ov-tampon:nth-child(1) { animation-delay: .18s; }
  .cbm-viv-etat.est-actif .ov-tampon:nth-child(2) { animation-delay: .27s; }
  .cbm-viv-etat.est-actif .ov-tampon:nth-child(3) { animation-delay: .36s; }
  .cbm-viv-etat.est-actif .ov-tampon:nth-child(4) { animation-delay: .45s; }
  .cbm-viv-etat.est-actif .ov-tampon:nth-child(5) { animation-delay: .54s; }
  .cbm-viv-etat.est-actif .ov-tampon:nth-child(6) { animation-delay: .63s; }
  .cbm-viv-etat.est-actif .ov-tampon:nth-child(7) { animation-delay: .72s; }

  @keyframes cbVivPose   { from { opacity: 0; transform: translateY(-8px) scaleY(.5); } }
  @keyframes cbVivGlisse { from { opacity: 0; transform: translateY(10px); } }
  @keyframes cbVivTampon { from { opacity: 0; transform: scale(.35); } }
}

@media (prefers-reduced-motion: reduce) {
  .cbm-viv-etape { opacity: 1 !important; transition: none !important; }
  .cbm-viv-etat { transition: none !important; }
  .cbm-viv-etat.est-actif * { animation: none !important; }
  .cbm-viv-liste::after { transition: none !important; }
  /* Le cercle du jalon ne se dessine plus, il est là. Comme partout
     ailleurs dans la DA, l'état d'arrivée est une déclaration ordinaire :
     si l'animation ne joue pas, le trait reste visible. */
  .cbm-viv-num > svg path { transition: none !important; }
  .cbm-viv-etape.est-actif .cbm-viv-num > svg path { stroke-dashoffset: 0 !important; }
}

/* L'étiquette « coup de cœur » ne s'affiche pas sur téléphone.
   Sur un grand écran, la couverture est large et l'étiquette se pose dans
   un coin vide. Sur 390 px, la photo prend toute la largeur : l'étiquette
   lui tombe dessus, souvent sur le visage. Et l'intertitre « Le coup de
   cœur » dit déjà la même chose, juste au-dessus, on répétait en gênant.
   Demande de Noah, 1er septembre 2026. */
.cli2-feat-tag { display: inline-flex; }
@media (max-width: 760px) {
  .cli2-feat-tag { display: none; }
}

/* ============================================================
   L'ÉCRAN D'ENTRÉE · REPRIS DE JOINLY, LE 5 SEPTEMBRE 2026
   ============================================================
   Noah, après une première reprise jugée insuffisante : « je veux
   vraiment le même formulaire d'inscription et de connexion, juste
   pas les mêmes champs, mais exactement le même design sur mobile et
   sur desktop que joinly. Là, ça va pas du tout : le design n'est pas
   le même au niveau des formats des boutons, du texte. »

   Il a raison, et la ressemblance ne tenait qu'à l'animation d'arrivée.
   Tout le reste divergeait : une carte blanche sur ordinateur contre
   une colonne nue, un texte aligné à gauche contre un texte centré, un
   titre de 30 px contre un de 42, des champs de 14 de rayon contre 15,
   un bouton à quatre couches d'ombre contre un dégradé simple.

   ------------------------------------------------------------
   CE QUI FAIT LE DESSIN DE JOINLY, EN CINQ POINTS
   ------------------------------------------------------------
   · UNE COLONNE CENTRÉE, la même sur téléphone et sur ordinateur.
     Aucune carte, aucun cadre : la page EST le formulaire. C'est ce
     qui donne le calme, et c'est ce qui manquait le plus ;
   · UN TITRE EN DEUX LIGNES, dont la seconde est peinte au dégradé de
     la marque. Il est gros, 32 px sur téléphone, 42 sur ordinateur -
     parce qu'il n'a qu'une chose à dire ;
   · DES CHAMPS SANS ÉTIQUETTE. Le nom du champ est écrit DANS le
     champ. Deux champs à l'écran ne demandent pas de légende, et une
     étiquette au-dessus de chacun double la hauteur du formulaire ;
   · UN BOUTON PLEINE LARGEUR, 460 px au plus, au dégradé de la marque,
     avec une ombre portée large et douce plutôt qu'un contour ;
   · UN FOND QUI RESPIRE : un dégradé vertical très pâle, et deux
     halos flous qui bougent lentement.

   ⚠️ LES CHAMPS N'ONT PLUS DE STYLE EN LIGNE, et c'est délibéré.
   Un style en ligne bat toujours une feuille de style : tant que
   `background` était écrit dans le JSX, le fond blanc du focus ne
   pouvait pas s'appliquer, et il avait fallu le contrefaire avec des
   ombres. Tout est ici, donc `:focus` et `::placeholder` marchent.

   ⚠️ LA GRAISSE MAXIMALE EST 700, PAS 800. Geist est une police
   variable qui couvre 300 à 700 (cf. les @font-face en tête de
   fichier) ; joinly, lui, écrit ses titres en Manrope 800. Demander
   800 ici ne donnerait rien de plus que 700, ou pire, une graisse
   synthétisée par le navigateur. L'interlettrage resserré
   (-0.035em) fait le reste du travail.
   ============================================================ */

.cb-auth-section {
  position: relative;
  isolation: isolate;
  /* UNE SEULE VARIABLE POUR LA COULEUR D'AUDIENCE, posée par le composant
     (pro ou cliente). Elle était recopiée en ligne à sept endroits, ce qui
     garantissait qu'un jour l'un des sept diverge. */
  --cb-auth-degrade: linear-gradient(110deg, var(--cb-auth-c), var(--cb-auth-c2));
  /* La couleur du PREMIER PIXEL de la page, relue à chaque écran par
     `cbAccorderLaBarreDuHaut` (shared.jsx) pour peindre la bande du haut.
     C'est exactement la première couleur du dégradé de fond ci-dessous :
     deux valeurs écrites à deux endroits finissent toujours par diverger. */
  --cb-bande: color-mix(in oklab, var(--cb-auth-c) 10%, #fff);
}

/* ------------------------------------------------------------
   L'ÉCRAN ARRIVE, IL NE REMPLACE PAS LA VITRINE D'UN COUP
   ------------------------------------------------------------
   On quittait la vitrine et la page de connexion prenait sa place au même
   seizième de seconde. On ne sait alors pas si l'on a AVANCÉ ou si le site a
   rechargé, et c'est cette hésitation-là qui donne l'impression d'un produit
   rêche.

   ⚠️ `transform: none` À L'ARRIVÉE, ET SURTOUT PAS `translateY(0)`. Avec
   `both`, la dernière image reste appliquée : un `translateY(0)` figé ferait
   de la section un repère de position pour tout ce qu'elle contient, et le
   fond en `position: fixed` juste en dessous se recalerait sur elle au lieu
   de la fenêtre. */
@keyframes cbAuthEcran {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
/* Le fond, lui, ne fait que se révéler : il ne monte pas. Un fond qui
   glisse de dix pixels découvre le bas de l'écran pendant qu'il monte. */
@keyframes cbAuthFondEntre { from { opacity: 0; } to { opacity: 1; } }

/* ------------------------------------------------------------
   LE FOND
   ------------------------------------------------------------
   Un dégradé vertical très pâle, et deux halos qui dérivent. Ils sont
   la seule chose qui bouge en permanence sur cet écran : assez pour
   que la page ait l'air vivante, assez lents (quatorze secondes) pour
   qu'on ne les regarde jamais.

   `fixed` : c'est la seule façon de couvrir aussi la hauteur qu'occupe
   la barre de navigation du site, et de ne pas défiler avec un
   formulaire plus haut que la fenêtre.

   ⚠️⚠️ L'ANIMATION D'ARRIVÉE EST SUR LA COLONNE, PAS SUR LA SECTION,
   ET C'EST CE FOND-LÀ QUI L'IMPOSE. Mesuré le 5 septembre 2026 : avec
   l'animation posée sur `.cb-auth-section`, ce fond faisait 830 px de haut
   dans une fenêtre de 900, et commençait à 34 px du haut. Autrement dit il
   ne couvrait NI la bande de l'en-tête, NI le bas de l'écran.

   La cause est la règle qu'on a déjà payée trois fois sur ce projet : un
   `position: fixed` ne se cale sur la FENÊTRE que si aucun ancêtre ne crée
   de repère de position. Et un élément PORTEUR D'UNE ANIMATION QUI TOUCHE
   `transform` en crée un, même quand l'animation est finie et que sa
   dernière image vaut `transform: none`, parce que `both` la laisse
   appliquée et que le navigateur garde la couche composée.

   ⚠️ LE PIÈGE EST QUE ÇA NE SE VOYAIT PAS. Tant que le fond était un
   presque-blanc sur une page blanche, un trou de 34 px en haut ne
   ressemblait à rien. C'est un dégradé coloré qui l'a révélé, et une
   mesure qui l'a prouvé. Le contrôle `outils/ecran-entree.test.js`
   compare désormais la boîte du fond à celle de la fenêtre.

   La section n'anime donc plus rien. L'arrivée est jouée par deux
   animations sœurs : la colonne monte, le fond se révèle. */
.cb-auth-fond {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  animation: cbAuthFondEntre .42s ease both;
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--cb-auth-c) 10%, #fff) 0%,
    color-mix(in oklab, var(--cb-auth-c2) 8%, #fff) 46%,
    color-mix(in oklab, var(--cb-auth-c) 2%, #fff) 100%);
}
.cb-auth-fond::before,
.cb-auth-fond::after {
  content: "";
  position: absolute;
  border-radius: 50%;
  animation: cbAuthHalo 14s ease-in-out infinite;
}
.cb-auth-fond::before {
  left: 4%; top: -170px;
  width: 440px; height: 440px;
  background: radial-gradient(circle,
    color-mix(in oklab, var(--cb-auth-c) 19%, transparent) 0%, transparent 70%);
}
.cb-auth-fond::after {
  right: 4%; bottom: -200px;
  width: 420px; height: 420px;
  background: radial-gradient(circle,
    color-mix(in oklab, var(--cb-auth-c2) 19%, transparent) 0%, transparent 70%);
  animation-direction: reverse;
}
@keyframes cbAuthHalo {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate(30px, -20px) scale(1.08); }
}

/* ------------------------------------------------------------
   LA COLONNE
   ------------------------------------------------------------
   Elle se centre dans la hauteur. Un formulaire de deux champs calé en
   haut d'un écran de 900 px laisse les deux tiers vides sous lui :
   c'est ça, la sensation de vide.

   `min-height` et non `height` : un formulaire plus haut que la fenêtre
   fait simplement grandir la colonne au lieu d'être coupé par le
   milieu. Et `dvh` avant `vh`, parce que sur téléphone `vh` compte la
   barre d'adresse repliée et pousse le bouton sous le pli.

   ⚠️ `overflow-x: clip` PARCE QUE LES ÉTAPES GLISSENT. Une étape entre
   en venant de 34 px sur le côté ; sans découpe, ces 34 px créent une
   barre de défilement horizontale sur toute la page pendant un tiers de
   seconde. `clip` et non `hidden` : `hidden` ferait de la colonne un
   conteneur de défilement, et `clip` sur un seul axe laisse l'autre
   libre, ce que `hidden` ne permet pas.

   ⚠️ ET LA DÉCOUPE EST SUR LA COLONNE, PAS SUR LA SECTION. Un ancêtre
   qui découpe rogne aussi ses descendants en `position: fixed` : posée
   sur la section, elle mangerait le fond. */
.cb-auth-colonne {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  max-width: 660px;
  margin: 0 auto;
  min-height: min(calc(100dvh - 150px), calc(100vh - 150px));
  padding: 26px 20px calc(48px + env(safe-area-inset-bottom, 0px));
  overflow-x: clip;
  text-align: center;
  animation: cbAuthEcran .42s cubic-bezier(.22, 1, .36, 1) both;
}
@media (min-width: 640px) {
  .cb-auth-colonne { max-width: 720px; padding: 40px 24px 64px; }
}

/* ------------------------------------------------------------
   LA TÊTE D'ÉTAPE · des traits et un compteur
   ------------------------------------------------------------
   Des traits, jamais un pourcentage : on voit où l'on en est sans
   qu'on nous compte des points. Ils disparaissent complètement quand
   il n'y a qu'un temps, annoncer un parcours là où il n'y en a pas
   serait pire que de ne rien annoncer. */
.cb-auth-tete {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-bottom: 26px;
  flex-wrap: wrap;
}
.cb-auth-rail {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}
.cb-auth-rail i {
  display: block;
  width: 34px;
  height: 4px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--cb-auth-c) 15%, #dedcf0);
  transition: background .34s ease;
}
.cb-auth-rail i.est-fait { background: var(--cb-auth-degrade); }
.cb-auth-compte {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* ------------------------------------------------------------
   LE TITRE
   ------------------------------------------------------------
   ⚠️ `-webkit-text-fill-color` PLUTÔT QUE `color: transparent` sur la
   seconde ligne. Un navigateur qui ne connaît pas la découpe du texte
   ignore la propriété et garde la couleur posée juste au-dessus : le
   titre reste lisible au lieu de disparaître. Avec `color: transparent`,
   il s'effacerait. */
.cb-auth-titre {
  margin: 0;
  font-family: var(--ff-text);
  font-size: 32px;
  font-weight: 700;
  letter-spacing: -0.035em;
  line-height: 1.08;
  color: var(--ink);
  text-wrap: balance;
  animation: cbAuthFadeUp .6s ease backwards;
}
@media (min-width: 640px) { .cb-auth-titre { font-size: 42px; } }
.cb-auth-eclat {
  background: var(--cb-auth-degrade);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: var(--cb-auth-c);
}
.cb-auth-sous {
  margin: 16px auto 0;
  max-width: 38ch;
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink-3);
  animation: cbAuthFadeUp .6s ease .06s backwards;
}

/* La cascade d'arrivée : le titre, puis le sous-titre, puis les champs.
   Des retards de soixante millisecondes, assez pour que l'œil parte du
   haut, trop peu pour qu'on attende. */
@keyframes cbAuthFadeUp {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------
   LE CORPS · les champs
   ------------------------------------------------------------
   460 px et pas 720 : la colonne a besoin de largeur pour que le titre
   tienne en deux lignes, un champ de mot de passe large comme une page
   se lit mal. Et le texte repasse à gauche, un champ centré ne se
   remplit pas droit. */
.cb-auth-haut,
.cb-auth-corps {
  max-width: 460px;
  margin: 0 auto;
  text-align: left;
}
.cb-auth-haut { margin-top: 22px; }
.cb-auth-corps {
  margin-top: 28px;
  animation: cbAuthFadeUp .6s ease .12s backwards;
}
.cb-auth-field + .cb-auth-field { margin-top: 12px; }
label.cb-auth-field { display: block; margin-top: 18px; }

/* 54 px de haut : plus généreux qu'ailleurs dans le site, et c'est voulu -
   il n'y en a que deux ou trois à l'écran, ils peuvent prendre la place.
   16 px de texte, en dessous desquels iOS zoome au focus et fait perdre
   la page ; 15 sur ordinateur, où le clavier ne zoome jamais. */
.cb-auth-section input:not([type="checkbox"]):not([type="radio"]),
.cb-auth-section select,
.cb-auth-section textarea {
  width: 100%;
  min-height: 54px;
  padding: 15px 16px;
  font-family: inherit;
  font-size: 16px;
  color: var(--ink);
  background: color-mix(in oklab, var(--cb-auth-c) 5%, #f7f7fc);
  border: 1px solid transparent;
  border-radius: 15px;
  outline: none;
  transition: background-color .18s ease, box-shadow .2s ease;
}
@media (min-width: 640px) {
  .cb-auth-section input:not([type="checkbox"]):not([type="radio"]),
  .cb-auth-section select,
  .cb-auth-section textarea { font-size: 15px; }
}
.cb-auth-section ::placeholder {
  color: color-mix(in oklab, var(--ink) 32%, transparent);
  opacity: 1;
}
.cb-auth-section input:not([type="checkbox"]):not([type="radio"]):focus,
.cb-auth-section select:focus,
.cb-auth-section textarea:focus {
  background: #fff;
  box-shadow: 0 0 0 2px var(--cb-auth-c);
}

/* La ligne d'explication sous un champ, quand le nom écrit dedans ne
   suffit pas à dire à quoi il servira. Rare par construction : si un
   champ a besoin d'un paragraphe, c'est le champ qu'il faut revoir. */
.cb-auth-aide {
  margin: 8px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-4);
}
.cb-auth-question {
  margin: 22px 0 10px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink-2);
}

/* Le mot de passe se montre. Un mot de passe tapé en aveugle sur un
   téléphone, avec l'auto-majuscule et les gros doigts, se trompe une fois
   sur trois, et à la création, la faute est ENREGISTRÉE. Caché par défaut :
   c'est la personne qui décide de le montrer, jamais nous. */
.cb-auth-oeil {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  padding: 8px 12px;
  border: none;
  border-radius: 999px;
  background: none;
  font-family: inherit;
  font-size: 12px;
  font-weight: 700;
  color: var(--ink-3);
  cursor: pointer;
  transition: background .16s ease, color .16s ease;
}
.cb-auth-oeil:hover { background: #fff; color: var(--cb-auth-ink); }

/* ------------------------------------------------------------
   L'ERREUR, SOUS LE TITRE ET PAS DANS UNE FENÊTRE
   ------------------------------------------------------------
   On la lit sans perdre ce qu'on vient de taper. */
.cb-auth-alerte,
.cb-auth-info {
  max-width: 460px;
  margin: 18px auto 0;
  padding: 12px 16px;
  border-radius: 13px;
  font-size: 13.5px;
  line-height: 1.5;
  text-align: left;
}
.cb-auth-alerte { background: #fdeeee; color: #a62a1c; }
.cb-auth-info {
  background: color-mix(in oklab, var(--cb-auth-c) 8%, #fff);
  color: var(--ink-2);
  display: flex;
  gap: 10px;
  align-items: flex-start;
}

/* ------------------------------------------------------------
   L'ARRIVÉE D'UNE ÉTAPE
   ------------------------------------------------------------
   Le contenu changeait sur place : mêmes titres, mêmes tailles, à la
   même hauteur, on doutait d'avoir cliqué. Il entre maintenant par la
   droite quand on avance, par la gauche quand on revient, ce qui dit où
   l'on va sans un mot. Une transition qui part du mauvais côté donne
   l'impression d'être revenu en arrière.

   ⚠️ LA CLÉ N'EST PAS DÉCORATIVE, côté JSX : sans elle, React garde les
   mêmes nœuds et le navigateur ne rejoue aucune animation. */
@keyframes cbAuthVersLaGauche {
  from { opacity: 0; transform: translateX(34px); }
  to   { opacity: 1; transform: none; }
}
@keyframes cbAuthVersLaDroite {
  from { opacity: 0; transform: translateX(-34px); }
  to   { opacity: 1; transform: none; }
}
.cb-auth-etape { animation: cbAuthVersLaGauche .34s cubic-bezier(.22, 1, .36, 1) both; }
.cb-auth-etape[data-sens="arriere"] { animation-name: cbAuthVersLaDroite; }

/* ------------------------------------------------------------
   LES ACTIONS
   ------------------------------------------------------------
   Un bouton pleine largeur, et « Revenir » réduit à un mot en dessous.
   Deux boutons de même poids feraient hésiter à chaque étape, alors
   qu'il n'y a qu'un chemin normal.

   L'ombre est large, basse et très diffuse (38 px de flou, 16 px de
   remontée) : elle pose le bouton au-dessus de la page au lieu de le
   cerner. C'est ce qui remplace les quatre couches de contour de la
   version d'avant. */
.cb-auth-suite { margin-top: 22px; }
.cb-auth-submit {
  display: block;
  width: 100%;
  max-width: 460px;
  margin: 0 auto;
  padding: 17px 20px;
  border: none;
  border-radius: 15px;
  font-family: inherit;
  font-size: 15.5px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: #fff;
  cursor: pointer;
  background: var(--cb-auth-degrade);
  box-shadow: 0 18px 38px -16px color-mix(in oklab, var(--cb-auth-c) 62%, transparent);
  transition: transform .16s ease, box-shadow .26s ease, opacity .15s ease;
}
.cb-auth-submit:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 24px 46px -16px color-mix(in oklab, var(--cb-auth-c) 68%, transparent);
}
.cb-auth-submit:active:not(:disabled) { transform: translateY(0) scale(0.99); }
.cb-auth-submit:disabled { opacity: .7; cursor: default; }
.cb-auth-submit:focus-visible { outline: 2px solid var(--cb-auth-c); outline-offset: 4px; }

.cb-auth-retour {
  display: block;
  width: 100%;
  margin: 14px auto 0;
  padding: 4px 8px;
  border: none;
  background: none;
  font-family: inherit;
  font-size: 13.5px;
  color: var(--ink-3);
  text-align: center;
  cursor: pointer;
}
.cb-auth-retour:hover { color: var(--ink-2); }

/* ------------------------------------------------------------
   LE PIED · une ligne, pas un séparateur
   ------------------------------------------------------------
   Sur téléphone, les deux boutons de l'en-tête du site sont repliés
   dans le menu : sans cette ligne, quelqu'un sans compte devrait ouvrir
   un menu pour découvrir qu'il peut en créer un. Elle n'a ni trait de
   séparation ni soulignement, c'est une phrase, pas un pied de page. */
.cb-auth-bascule {
  margin-top: 16px;
  text-align: center;
  font-size: 13.5px;
  color: var(--ink-3);
}
.cb-auth-bascule button {
  padding: 0;
  border: none;
  background: none;
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--cb-auth-ink);
  cursor: pointer;
}
.cb-auth-bascule button:hover { text-decoration: underline; text-underline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .cb-auth-colonne,
  .cb-auth-fond,
  .cb-auth-etape,
  .cb-auth-titre,
  .cb-auth-sous,
  .cb-auth-corps { animation: none !important; }
  .cb-auth-fond::before,
  .cb-auth-fond::after { animation: none !important; }
  .cb-auth-submit { transition: none !important; }
}

/* ------------------------------------------------------------
   LA CARTE · réservée à « Vérifiez votre boîte mail »
   ------------------------------------------------------------
   Cet écran-là n'a rien à remplir : il annonce, il attend. Une carte
   posée sur le lavis convient à un message ; elle serait de trop
   autour d'un formulaire, où elle ajouterait un cadre à chaque
   étape. */
.cb-auth-carte {
  width: 100%;
  padding: 30px 26px 26px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 22px;
  box-shadow: 0 24px 50px -32px rgba(20, 18, 60, 0.35);
}

/* La suggestion d'adresse e-mail · cf. EmailSuggestPill dans pages-b.jsx.
   « Tab » ne s'affiche que là où la touche existe, c'est-à-dire là où il
   y a un curseur. Un média `max-width` aurait été faux : une tablette avec
   clavier est large, un téléphone en paysage est étroit. */
.cb-suggest-mail-tab { display: none; }
@media (pointer: fine) { .cb-suggest-mail-tab { display: inline-block; } }
.cb-suggest-mail:hover { filter: brightness(0.985); }

/* 14 octobre 2026 · PWA : « la lumière blanche en haut de l'écran ». C'est l'effet de bord de iOS 26 (Liquid Glass) :
   il floute et blanchit le haut de la fenêtre, SAUF si une boîte fixe, pleine largeur, d'une seule couleur unie
   occupe le bord (WebKit étend alors sa couleur sous l'heure), et sans `backdrop-filter` dans le haut.
   Donc, uniquement dans l'application installée : un fond fixe uni derrière tout, le dégradé du fond remplacé par
   son aplat, et plus aucun flou sur les barres du haut. */
@media (display-mode: standalone) {
  body::before { content: ""; position: fixed; inset: 0; z-index: -2; pointer-events: none; background-color: var(--cb-haut-de-page, #F2F3FF); }
  .e-bg { background: var(--e-fond-haut, #F2F3FF) !important; background-image: none !important; }
  .e-bg > * { display: none !important; }
  /* 15 octobre 2026 : le flou des barres est REMIS. L'effet iOS 26 venait de `viewport-fit=cover` (retiré de app/index.html), plus de page sous la barre d'état. */
}
