/*
 * style.css — L'habillage du site et du tableau de bord.
 *
 * Les couleurs sont celles des pages légales (deploy/build-legal.js) et des
 * cartes du bot : même rose #FF9EBD, même fond, même dégradé. Un visiteur qui
 * passe de /terms à l'accueil ne doit pas avoir l'impression de changer de
 * service.
 *
 * ⚠️ La CSP du site interdit le style EN LIGNE (pas de 'unsafe-inline') : aucun
 * attribut style= dans le HTML, aucune balise <style>. Ce qui doit varier à
 * l'exécution passe par une variable CSS posée en JavaScript
 * (el.style.setProperty), que la CSP n'interdit pas.
 */

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

:root {
  /*
   * Dit au navigateur que la page est SOMBRE. Sans cette ligne, il dessine ses
   * propres éléments sur un fond clair : le rail de la barre de défilement
   * ressortait BLANC le long d'un site noir, et les champs remplis
   * automatiquement viraient au jaune pâle. C'est une ligne, et elle règle
   * aussi le sélecteur de couleur, les menus natifs et le curseur de saisie.
   */
  color-scheme: dark;

  --bg:      #0E0B12;
  --card:    rgba(255,255,255,.045);
  --card2:   rgba(255,255,255,.028);
  --line:    rgba(255,255,255,.09);
  --line2:   rgba(255,255,255,.16);
  --tx:      #F6F2F5;
  --dim:     #B7A9B4;
  /*
   * #6E6270 ne donnait que 3,39:1 sur le fond, alors que le texte courant en
   * exige 4,5:1 (WCAG AA). Or c'est la couleur des mentions, des unités, des
   * légendes de chiffres et des explications sous chaque champ — c'est-à-dire
   * de tout ce qui est déjà petit.
   *
   * Le seuil ne se juge PAS sur le fond de page mais sur la surface la plus
   * CLAIRE où la couleur apparaît — ici le fond des menus déroulants (#1A141F).
   * #847686 passait sur la page (4,57) et échouait dans un menu (4,22).
   * #8D7D8F passe partout : 5,08 sur la page, 4,69 dans un menu.
   */
  --faint:   #8D7D8F;
  --rose:    #FF9EBD;
  --rose-dim:rgba(255,158,189,.16);
  --warn:    #FFC940;
  --ok:      #7BE3A3;
  --bad:     #FF7A7A;
  --radius:  16px;
  --wrap:    1120px;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* Le décalage d'ancre doit valoir la hauteur de l'en-tête COLLANT. Elle vaut
   62 px sur ordinateur, mais l'en-tête se replie sur deux lignes en dessous de
   640 px : à 80 px, le titre visé se garait derrière lui. */
html { scroll-behavior: smooth; scroll-padding-top: 80px }

/* Premier arrêt de tabulation : sauter la barre de navigation. Caché tant qu'on
   ne l'atteint pas au clavier — c'est le seul public qui en a besoin. */
.skip {
  position: absolute; left: 12px; top: -60px; z-index: 100;
  background: var(--rose); color: #2A0D16; font-weight: 600;
  padding: 10px 16px; border-radius: 10px; transition: top .15s;
}
.skip:focus { top: 12px; text-decoration: none }

/* Un contour visible au clavier, jamais à la souris. */
:focus-visible { outline: 2px solid var(--rose); outline-offset: 2px; border-radius: 6px }
:focus:not(:focus-visible) { outline: none }

body {
  /* iOS agrandit le texte de lui-même au passage en paysage (« text
     inflation ») : la mise en page saute sans qu'on ait rien demandé. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  background: radial-gradient(1100px 560px at 50% -10%, #2a1b2b 0%, var(--bg) 60%) no-repeat;
  background-color: var(--bg);
  color: var(--tx);
  font-family: var(--font);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
}

a { color: var(--rose); text-decoration: none }
a:hover { text-decoration: underline }
strong { color: var(--tx) }
code, .mono { font-family: var(--mono) }

/*
 * PIÈGE DE SPÉCIFICITÉ, et il a coûté toute la respiration de la page.
 *
 * `padding: 0 22px` est un RACCOURCI : il remet aussi le haut et le bas à zéro.
 * Et `.wrap` (une classe) l'emporte sur `section` (un élément), donc la règle
 * `section { padding: 68px 0 }` juste en dessous n'a JAMAIS rien fait sur les
 * quatre `<section class="wrap">` de la page d'accueil. Écart réel entre deux
 * sections : 0 px — la grille des chiffres et le bandeau du tableau de bord se
 * touchaient bord à bord.
 *
 * En écriture longue, le padding horizontal ne touche plus au vertical.
 */
.wrap { max-width: var(--wrap); margin: 0 auto; padding-left: 22px; padding-right: 22px }
.narrow { max-width: 860px }


/* ── Barres de défilement ─────────────────────────────────────────────────── */
/*
 * Celles du système jurent avec tout le reste : grises, carrées, larges, et
 * blanches sur un fond sombre chez la moitié des visiteurs. Les deux syntaxes
 * cohabitent volontairement — `scrollbar-*` est la norme (Firefox, et Chrome
 * récent), `::-webkit-scrollbar` couvre Safari et les Chrome plus anciens.
 * Aucune des deux ne suffit seule.
 */
* { scrollbar-width: thin; scrollbar-color: rgba(255,158,189,.38) transparent }

::-webkit-scrollbar { width: 11px; height: 11px }
::-webkit-scrollbar-track { background: transparent }
::-webkit-scrollbar-thumb {
  background: rgba(255,158,189,.30);
  border-radius: 999px;
  /* La bordure transparente amincit le curseur SANS réduire la zone
     cliquable : viser 5 px à la souris est pénible, 11 px ne l'est pas. */
  border: 3px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover  { background: rgba(255,158,189,.55); background-clip: content-box }
::-webkit-scrollbar-thumb:active { background: var(--rose); background-clip: content-box }
::-webkit-scrollbar-corner { background: transparent }

/* ── Sélection de texte ───────────────────────────────────────────────────── */
/* Le bleu du système sur un fond violine : autant garder la couleur du bot. */
::selection { background: rgba(255,158,189,.30); color: #FFF }

/* ── Champs ───────────────────────────────────────────────────────────────── */
/* La petite croix des champs de recherche est dessinée par le navigateur, en
   gris sombre : invisible ici. On la remplace par la nôtre. */
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none; appearance: none;
  width: 15px; height: 15px; cursor: pointer; opacity: .5;
  background: currentColor;
  -webkit-mask: var(--x) center/contain no-repeat;
          mask: var(--x) center/contain no-repeat;
}
input[type="search"]::-webkit-search-cancel-button:hover { opacity: 1; color: var(--rose) }
:root { --x: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 4l8 8M12 4l-8 8' stroke='%23fff' stroke-width='2' stroke-linecap='round' fill='none'/%3E%3C/svg%3E") }

/* Les flèches d'un champ numérique : minuscules, grises, et elles apparaissent
   au survol seulement. Autant les retirer — les valeurs se tapent. */
input[type="number"] { -moz-appearance: textfield; appearance: textfield }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0 }

/* Le sélecteur de couleur natif : une pastille grise cerclée de blanc. */
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0 }
input[type="color"]::-webkit-color-swatch { border: none; border-radius: 7px }
input[type="color"]::-moz-color-swatch { border: none; border-radius: 7px }

/* ── Barre du haut ───────────────────────────────────────────────────────── */

header.top {
  position: sticky; top: 0; z-index: 50;
  background: rgba(14,11,18,.78);
  /* Safari a besoin du préfixe jusqu'à iOS 18 : sans lui, la barre du haut
     n'est pas dépolie et le texte défile en clair derrière elle. */
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--line);
}
.top .wrap { display: flex; align-items: center; gap: 18px; height: 62px }
/* L'entrée Premium de la barre : reconnaissable sans crier. Elle est la seule
   porte vers /premium depuis le reste du site — avant, il n'y en avait aucune. */
.nav-prem { color: var(--rose) !important; font-weight: 600 }
.nav-prem[aria-current="page"] { color: var(--tx) !important }

.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; letter-spacing: -.01em; color: var(--tx) }
.brand:hover { text-decoration: none }
/* Le logo du site EST l'avatar du bot (scripts/build-site-assets.js le
   retélécharge). Son fond est EXACTEMENT #FF9EBD, l'accent du site : un liseré
   rose y serait invisible, d'où le filet clair. */
.brand .mark {
  width: 30px; height: 30px; border-radius: 50%; flex: none;
  box-shadow: 0 0 0 1px rgba(255,255,255,.22), 0 6px 18px -10px var(--rose);
}
.top nav { display: flex; gap: 4px; margin-left: auto; align-items: center; flex-wrap: wrap }
.top nav a { color: var(--dim); font-size: 14px; padding: 7px 11px; border-radius: 9px }
.top nav a:hover { color: var(--tx); background: var(--card); text-decoration: none }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px 18px; border-radius: 11px; border: 1px solid var(--line2);
  color: var(--tx); background: var(--card); font: inherit; font-size: 14.5px; font-weight: 600;
  cursor: pointer; white-space: nowrap; transition: background .15s, border-color .15s, transform .1s;
}
.btn:hover { background: rgba(255,255,255,.09); border-color: rgba(255,255,255,.26); text-decoration: none }
.btn:active { transform: translateY(1px) }
.btn.primary { background: var(--rose); border-color: var(--rose); color: #2A0D16 }
.btn.primary:hover { background: #FFB3CB; border-color: #FFB3CB }
.btn.small { padding: 6px 12px; font-size: 13px; border-radius: 9px }
.btn.ghost { background: transparent }
.btn:disabled { opacity: .5; cursor: not-allowed }
/* Une action qui efface des réglages ne doit pas ressembler à « Enregistrer ». */
.btn.danger { border-color: rgba(255,122,122,.4); color: var(--bad) }
.btn.danger:hover { background: rgba(255,122,122,.1); border-color: rgba(255,122,122,.6) }

.lang { display: flex; gap: 6px; align-items: center }
.lang a { font-size: 12.5px; color: var(--faint); border: 1px solid var(--line); padding: 4px 10px; border-radius: 999px }
.lang a:hover { color: var(--rose); border-color: rgba(255,158,189,.5); text-decoration: none }

/* ── Titres et sections ──────────────────────────────────────────────────── */

section { padding: 68px 0 }
.eyebrow { color: var(--rose); font-size: 12.5px; letter-spacing: .18em; text-transform: uppercase; font-weight: 700 }
h1 { font-size: clamp(31px, 4.6vw, 50px); line-height: 1.08; letter-spacing: -.028em; margin: 16px 0;
     text-wrap: balance }
h2 { font-size: clamp(23px, 3vw, 31px); line-height: 1.2; letter-spacing: -.02em; margin-bottom: 10px }
h3 { font-size: 16.5px; margin-bottom: 6px }
.lead { color: var(--dim); font-size: 16.5px; max-width: 62ch }
.section-head { margin-bottom: 32px }

/* ── Accueil ─────────────────────────────────────────────────────────────── */

/*
 * `overflow-x: clip` et non `hidden` : `hidden` créerait un conteneur de
 * défilement, ce qui casserait le `position: sticky` de la barre du haut.
 * Sans lui, le halo du logo (330 px de large dans une colonne de 280) poussait
 * la page de 15 px et l'iPad Pro défilait latéralement.
 */
.hero { padding: 74px 0 44px; overflow-x: clip }
.hero .lead { font-size: 18px; max-width: 60ch }
.hero-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) 280px; gap: 48px; align-items: center }
.hero-art { position: relative; display: grid; place-items: center }
/* Le fond de l'avatar EST l'accent du site : posé sur une lueur rose, il s'y
   dissolvait en tache. Un anneau SOMBRE l'en détache, doublé d'un filet rose
   pour le rattacher quand même à la charte. */
.hero-art img {
  width: 228px; height: 228px; border-radius: 50%; position: relative;
  box-shadow: 0 0 0 8px rgba(14,11,18,.92), 0 0 0 9px rgba(255,158,189,.38),
              0 26px 60px -34px rgba(255,158,189,.7);
}
.hero-art .halo {
  position: absolute; width: 330px; height: 330px; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,158,189,.10) 0%, rgba(255,158,189,0) 68%);
}
.cta { display: flex; gap: 12px; flex-wrap: wrap; margin: 30px 0 14px }
.hint { color: var(--faint); font-size: 13.5px; max-width: 58ch }

.strip {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px;
  background: var(--line); border: 1px solid var(--line); border-radius: var(--radius);
  overflow: hidden; margin-top: 40px;
}
.strip div { background: #130F17; padding: 20px 18px }
.strip b { display: block; font-size: clamp(22px, 3.2vw, 30px); font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums }
.strip span { color: var(--faint); font-size: 12.5px; text-transform: uppercase; letter-spacing: .1em }

.grid { display: grid; gap: 16px }
.grid.g3 { grid-template-columns: repeat(3, minmax(0, 1fr)) }
.grid.g2 { grid-template-columns: repeat(2, minmax(0, 1fr)) }
.grid.g4 { grid-template-columns: repeat(4, minmax(0, 1fr)) }

.card {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 22px;
}
.card.hover:hover { border-color: rgba(255,158,189,.35); background: rgba(255,158,189,.05) }
/* Une carte entière peut être un lien : son texte reste du texte. */
a.card, a.card:hover { text-decoration: none; color: inherit }

.feature .icon {
  width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center;
  background: var(--rose-dim); color: var(--rose); font-size: 17px; margin-bottom: 14px;
  transition: background .2s ease, transform .2s ease;
}
.feature .icon svg { width: 21px; height: 21px; display: block }
.card.hover:hover .icon { background: rgba(255,158,189,.26); transform: scale(1.06) }

/* ── Icônes animées ───────────────────────────────────────────────────────── */
/*
 * Les animations ne tournent QUE quand la carte est survolée : neuf boucles
 * permanentes dans la fenêtre, c'est du calcul pour rien et un fond qui
 * grouille. `transform-box: fill-box` fait porter l'origine sur la forme
 * elle-même, sinon tout pivote autour du coin du SVG.
 */
.feature svg [class] { transform-box: fill-box; transform-origin: center }

/*
 * PIÈGE DU RACCOURCI. `animation: …` posé sur `.card.hover:hover .eq` (0,3,1)
 * REMET `animation-delay` à zéro, et il l'emporte sur `.eq:nth-child(2)`
 * (0,2,0) écrit juste en dessous : les quatre barres pulsaient donc ENSEMBLE au
 * lieu de faire une vague — l'animation existait, son décalage non. Les
 * décalages sont désormais posés à la même spécificité que le raccourci.
 */
.card.hover:hover .eq { animation: eq 1.05s ease-in-out infinite }
.card.hover:hover .eq:nth-child(2) { animation-delay: .14s }
.card.hover:hover .eq:nth-child(3) { animation-delay: .28s }
.card.hover:hover .eq:nth-child(4) { animation-delay: .42s }
@keyframes eq { 0%, 100% { transform: scaleY(.35) } 50% { transform: scaleY(1) } }

.card.hover:hover .tip  { animation: tip 2.4s ease-in-out infinite }
@keyframes tip { 0%, 100% { transform: rotate(-7deg) } 50% { transform: rotate(7deg) } }

.card.hover:hover .ping { animation: ping 2s ease-out infinite }
@keyframes ping { 0% { transform: scale(.45); opacity: .55 } 100% { transform: scale(1); opacity: 0 } }
.card.hover:hover .tick { animation: tick .5s ease both }
@keyframes tick { from { stroke-dasharray: 12; stroke-dashoffset: 12 } to { stroke-dashoffset: 0 } }

.card.hover:hover .spark { animation: spark 1.6s ease-in-out infinite }
.card.hover:hover .spark.s2 { animation-delay: .5s }
@keyframes spark { 0%, 100% { opacity: 0; transform: scale(.4) } 45% { opacity: 1; transform: scale(1.25) } }

.card.hover:hover .flip { animation: flip 2.6s ease-in-out infinite }
@keyframes flip { 0%, 55% { transform: rotateY(0) } 78%, 100% { transform: rotateY(180deg) } }

.card.hover:hover .drop { animation: drop 1.9s ease-in-out infinite }
@keyframes drop { 0%, 100% { transform: translateY(-4px); opacity: 0 } 35%, 70% { transform: translateY(0); opacity: 1 } }

.card.hover:hover .beat { animation: beat 1.25s ease-in-out infinite }
@keyframes beat { 0%, 100% { transform: scale(1) } 18% { transform: scale(1.14) } 36% { transform: scale(1) } 54% { transform: scale(1.09) } }

.card.hover:hover .slide { animation: slide 1.7s ease-in-out infinite }
@keyframes slide { 0%, 100% { transform: translateX(0); opacity: .55 } 50% { transform: translateX(2.5px); opacity: 1 } }

.card.hover:hover .spin { animation: spin 7s linear infinite }
@keyframes spin { to { transform: rotate(360deg) } }
.feature { position: relative }
.feature .n {
  position: absolute; top: 20px; right: 20px; font-size: 12px; color: var(--faint);
  border: 1px solid var(--line); border-radius: 999px; padding: 2px 10px; font-variant-numeric: tabular-nums;
}
.feature p.sub { color: var(--dim); font-size: 14.5px; margin-bottom: 12px }
.feature ul { list-style: none; display: grid; gap: 6px }
.feature li { color: var(--dim); font-size: 13.5px; padding-left: 16px; position: relative }
.feature li::before { content: '·'; position: absolute; left: 4px; color: var(--rose); font-weight: 700 }

/* ── Explorateur de commandes ────────────────────────────────────────────── */

.toolbar { position: sticky; top: 62px; z-index: 20; background: rgba(14,11,18,.92);
           -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
           padding: 14px 0 8px; margin-bottom: 8px }
.filters { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 12px }
/* Sur mobile, les catégories défilent au doigt au lieu d'empiler six lignes. */
.filters.scrollx { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 4px; scrollbar-width: thin }
.search {
  flex: 1 1 260px; min-width: 200px;
  background: var(--card); border: 1px solid var(--line); border-radius: 11px;
  padding: 11px 14px; color: var(--tx); font: inherit; font-size: 14.5px;
}
.search:focus { outline: none; border-color: rgba(255,158,189,.5) }
.search::placeholder { color: var(--faint) }

.pill {
  border: 1px solid var(--line); background: transparent; color: var(--dim);
  padding: 7px 13px; border-radius: 999px; font: inherit; font-size: 13px; cursor: pointer;
}
.pill:hover { color: var(--tx); border-color: var(--line2) }
.pill[aria-pressed="true"] { background: var(--rose-dim); border-color: rgba(255,158,189,.5); color: var(--rose); font-weight: 600 }

.cmds { display: grid; grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); gap: 12px }
.cmd { background: var(--card2); border: 1px solid var(--line); border-radius: 13px; padding: 15px 17px }
.cmd h3 { font-family: var(--mono); font-size: 14.5px; color: var(--rose); font-weight: 600; margin-bottom: 5px }
.cmd p { color: var(--dim); font-size: 13.5px; line-height: 1.5 }
.cmd .subs { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 9px }
.cmd .subs span {
  font-family: var(--mono); font-size: 11px; color: var(--faint);
  border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px;
}
.count { color: var(--faint); font-size: 13px; margin-left: auto }

/* ── Chiffres ────────────────────────────────────────────────────────────── */

.nums { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px }
.num { background: var(--card); border: 1px solid var(--line); border-radius: 13px; padding: 18px 20px;
       /* `position: relative` est AUSSI déclaré plus bas, dans le bloc des
          timelines de défilement. Il doit l'être ici : la puce « +N » et
          l'anneau de mise à jour se posent sur cette tuile même quand le
          navigateur ne connaît pas `view()` ou que le mouvement est refusé. */
       position: relative }
.num b { display: block; font-size: 26px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums }
.num span { color: var(--faint); font-size: 13px }

/* ── Bandeau tableau de bord ─────────────────────────────────────────────── */

.promo {
  border: 1px solid rgba(255,158,189,.28); border-radius: 22px; padding: 38px;
  background: linear-gradient(160deg, rgba(255,158,189,.10), rgba(255,158,189,.02));
  display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); gap: 38px; align-items: center;
}
/* L'enregistrement du panneau. Le cadre l'inscrit dans la page comme une
   fenêtre posée, plutôt que comme une image collée. */
.promo-demo { margin: 0; position: relative }
.promo-demo img {
  display: block; width: 100%; height: auto; border-radius: 13px;
  border: 1px solid var(--line2); box-shadow: 0 28px 60px -28px #000;
  transition: opacity .3s ease;
}

/*
 * L'enregistrement s'arrête au bas de la fenêtre filmée, donc AU MILIEU d'une
 * ligne de tableau : ça se lisait comme une image cassée, pas comme un écran
 * qui continue plus bas. Ce dégradé fait de la coupure une intention.
 * `pointer-events: none` pour ne pas voler le clic à l'image.
 */
.promo-demo::after {
  content: ''; position: absolute; left: 1px; right: 1px; bottom: 1px; height: 74px;
  border-radius: 0 0 12px 12px; pointer-events: none;
  background: linear-gradient(to bottom, rgba(20,15,24,0) 0%, rgba(20,15,24,.72) 58%, rgba(20,15,24,.95) 100%);
}
.promo-demo img.live { animation: demo-in .4s ease }
@keyframes demo-in { from { opacity: .55 } }
.promo ul { list-style: none; display: grid; gap: 8px; margin: 20px 0 26px }
.promo li { color: var(--dim); font-size: 14.5px; padding-left: 22px; position: relative }
.promo li::before { content: '✓'; position: absolute; left: 0; color: var(--rose) }

/* ── Pied de page ────────────────────────────────────────────────────────── */

footer { border-top: 1px solid var(--line); padding: 44px 0 60px; margin-top: 30px }
footer .cols { display: grid; grid-template-columns: 2fr repeat(3, minmax(0, 1fr)); gap: 28px }
/* Un <h2> habillé en petite étiquette : le pied de page sautait de h2 à h4,
   et un lecteur d'écran y perdait le plan du document. La taille est une
   affaire de style, pas de niveau. */
footer .ftitle { color: var(--tx); font-size: 13px; font-weight: 700; text-transform: uppercase;
                 letter-spacing: .12em; margin-bottom: 12px }
footer a, footer p { color: var(--dim); font-size: 14px; display: block; margin-bottom: 7px }
footer .note { color: var(--faint); font-size: 12.5px; margin-top: 26px }
.state { display: inline-flex; align-items: center; gap: 7px; color: var(--dim); font-size: 13.5px }
.state .led { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px rgba(123,227,163,.15) }

/* ── Tableau de bord ─────────────────────────────────────────────────────── */

.who { display: flex; align-items: center; gap: 9px; color: var(--dim); font-size: 14px }
.who img { width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--line2) }

.servers { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 14px }
.server { display: flex; align-items: center; gap: 14px; text-align: left }
.server .ico { width: 46px; height: 46px; border-radius: 14px; flex: none; object-fit: cover; border: 1px solid var(--line) }
.server .ico.letter { display: grid; place-items: center; background: var(--rose-dim); color: var(--rose); font-weight: 700; font-size: 18px }
.server b { display: block; font-size: 15px; line-height: 1.3; color: var(--tx) }
.server small { color: var(--faint); font-size: 12.5px }
.server.absent { opacity: .72 }

/*
 * `minmax(0, 1fr)` et non `1fr`. PIÈGE : `1fr` vaut `minmax(auto, 1fr)`, et ce
 * `auto` donne à la PISTE la largeur minimale de son contenu. Poser
 * `min-width: 0` sur l'élément ne suffit donc pas — la piste elle-même faisait
 * 625 px dans un panneau de 368, et toute la page défilait latéralement.
 */
.panel { display: grid; grid-template-columns: 216px minmax(0, 1fr); gap: 30px; align-items: start; padding: 34px 0 70px }
.side { position: sticky; top: 82px; display: grid; gap: 3px }
.side button {
  text-align: left; background: none; border: none; color: var(--dim);
  font: inherit; font-size: 14px; padding: 9px 13px; border-radius: 10px; cursor: pointer;
}
.side button:hover { color: var(--tx); background: var(--card) }
.side button[aria-current="true"] { background: var(--rose-dim); color: var(--rose); font-weight: 600 }

.ghead { display: flex; align-items: center; gap: 14px; margin-bottom: 26px; padding-top: 26px }
.ghead img, .ghead .letter { width: 52px; height: 52px; border-radius: 16px; border: 1px solid var(--line) }
.ghead .letter { display: grid; place-items: center; background: var(--rose-dim); color: var(--rose); font-weight: 700; font-size: 21px }
.ghead h1 { font-size: 27px; margin: 0 }
.ghead small { color: var(--faint) }

fieldset { border: none }
.block { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 24px; margin-bottom: 16px }
.block > h3 { font-size: 17px; margin-bottom: 4px }
.block > p.sub { color: var(--faint); font-size: 13.5px; margin-bottom: 20px }

.field { display: grid; gap: 6px; margin-bottom: 18px }
.field > label { font-size: 13.5px; font-weight: 600; color: var(--tx) }
.field .hint { color: var(--faint); font-size: 12.5px; margin: 0 }
.field.row { grid-template-columns: 1fr auto; align-items: center; gap: 14px }

input[type="text"], input[type="number"], select, textarea {
  background: #16121B; border: 1px solid var(--line2); border-radius: 10px;
  padding: 10px 12px; color: var(--tx); font: inherit; font-size: 14px; width: 100%;
}

/*
 * PIÈGE iOS : Safari ZOOME toute la page dès qu'on met le curseur dans un champ
 * dont le texte fait moins de 16 px — et ne dézoome pas ensuite. C'est le défaut
 * le plus courant d'un formulaire mobile, et il ne se voit jamais sur un
 * ordinateur. On repasse donc tous les champs à 16 px sur écran tactile.
 * `pointer: coarse` vise le doigt, pas la largeur : une tablette avec clavier
 * garde la taille compacte.
 */
@media (pointer: coarse), (max-width: 700px) {
  /*
   * PIÈGE DE SPÉCIFICITÉ : la règle de base vise `input[type="number"]`, un
   * sélecteur d'attribut, qui l'emporte sur un simple `input`. La règle à 16 px
   * était donc ignorée pour exactement les champs qui en avaient besoin — et
   * iOS zoomait toujours. On répète les mêmes sélecteurs.
   */
  input[type="text"], input[type="number"], input[type="search"], input[type="color"],
  select, textarea, .search, .dd-search, .mp-search, .dd-btn, .dd-opt, .mp-row {
    font-size: 16px;
  }
}
textarea { min-height: 84px; resize: vertical; line-height: 1.55 }
input:focus, select:focus, textarea:focus { outline: none; border-color: rgba(255,158,189,.55) }
input:disabled, select:disabled { opacity: .5 }
select option { background: #16121B }
input[type="color"] { width: 46px; height: 34px; padding: 2px; border-radius: 9px; background: #16121B; border: 1px solid var(--line2); cursor: pointer }

.inline { display: flex; gap: 10px; align-items: center; flex-wrap: wrap }
.inline input[type="number"] { width: 108px }
.unit { color: var(--faint); font-size: 13px }

/* Interrupteur — une case à cocher habillée, donc accessible au clavier. */
.toggle { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--line) }
.toggle:last-child { border-bottom: none }
.toggle .txt b { display: block; font-size: 14.5px; font-weight: 600 }
.toggle .txt span { color: var(--faint); font-size: 12.5px }
.switch { position: relative; flex: none; width: 44px; height: 25px }
.switch input { position: absolute; inset: 0; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer }
.switch i {
  position: absolute; inset: 0; border-radius: 999px; background: rgba(255,255,255,.12);
  border: 1px solid var(--line2); transition: background .18s, border-color .18s; pointer-events: none;
}
.switch i::after {
  content: ''; position: absolute; top: 3px; left: 3px; width: 17px; height: 17px;
  border-radius: 50%; background: var(--dim); transition: transform .18s, background .18s;
}
.switch input:checked + i { background: var(--rose-dim); border-color: rgba(255,158,189,.55) }
.switch input:checked + i::after { transform: translateX(19px); background: var(--rose) }
.switch input:focus-visible + i { box-shadow: 0 0 0 3px rgba(255,158,189,.28) }

.actions { display: flex; align-items: center; gap: 12px; margin-top: 22px }
.status { font-size: 13.5px; color: var(--faint) }
.status.ok { color: var(--ok) }
.status.err { color: var(--bad) }

.stats4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px }
.stat { background: var(--card2); border: 1px solid var(--line); border-radius: 13px; padding: 16px 18px }
.stat b { display: block; font-size: 22px; font-variant-numeric: tabular-nums; letter-spacing: -.02em }
.stat span { color: var(--faint); font-size: 12.5px; line-height: 1.45; display: block }

table { width: 100%; border-collapse: collapse; font-size: 14px }
th, td { padding: 10px 12px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: middle }
th { color: var(--rose); font-weight: 600; font-size: 12.5px; text-transform: uppercase; letter-spacing: .08em; white-space: nowrap }
td { color: var(--dim) }
td b { color: var(--tx); font-weight: 600 }
tr:last-child td { border-bottom: none }
.scroll { overflow-x: auto }

.tag { display: inline-block; font-size: 11.5px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line2); color: var(--dim) }
.tag.ok { color: var(--ok); border-color: rgba(123,227,163,.4) }
.tag.warn { color: var(--warn); border-color: rgba(255,201,64,.4) }
.tag.off { color: var(--faint) }

.permline { display: flex; align-items: center; gap: 9px; padding: 5px 0; font-size: 14px; color: var(--dim) }
.notice { border: 1px solid rgba(255,201,64,.4); background: rgba(255,201,64,.07); border-radius: 13px;
          padding: 16px 18px; color: var(--dim); font-size: 14px;
          display: flex; align-items: center; gap: 12px; flex-wrap: wrap }
/* Un message posé dans une grille en occupe TOUTE la largeur : coincé dans une
   colonne sur trois, il se lit mal et laisse deux trous à côté. */
.grid > .notice, .cmds > .notice { grid-column: 1 / -1 }
.notice .btn { flex: none }
.notice.bad { border-color: rgba(255,122,122,.4); background: rgba(255,122,122,.07) }
.empty { color: var(--faint); font-size: 14px; padding: 22px 0 }
/* Écran d'échec : centré et borné, pas une bannière d'un bord à l'autre. */
.failure { max-width: 620px; margin: 60px auto }

.notfound { padding: 90px 0; text-align: center }
.notfound .lost { width: 96px; height: 96px; border-radius: 50%; opacity: .55; margin-bottom: 26px;
                  box-shadow: 0 0 0 1px rgba(255,158,189,.3), 0 24px 60px -30px var(--rose) }
.notfound .lead { margin: 0 auto }
.notfound .cta { justify-content: center }

.userdata { margin-top: 34px }
.datalist { list-style: none; display: grid; gap: 1px; background: var(--line);
            border: 1px solid var(--line); border-radius: 12px; overflow: hidden; margin-bottom: 4px }
.datalist li { display: flex; justify-content: space-between; align-items: center; gap: 16px;
               background: #16121B; padding: 11px 15px; font-size: 14px; color: var(--dim) }
.datalist b { color: var(--tx); font-variant-numeric: tabular-nums; white-space: nowrap }

.skeleton { color: var(--faint); font-size: 14px; padding: 40px 0; text-align: center }

/* La classe est POSÉE PAR JAVASCRIPT : sans lui, rien n'est masqué et la page
   reste entièrement lisible. Masquer d'abord en CSS aurait rendu le site blanc
   pour qui bloque les scripts. */
.reveal { opacity: 0; transform: translateY(14px) }
.reveal.in { opacity: 1; transform: none; transition: opacity .55s ease, transform .55s ease }

.top nav a[aria-current="true"] { color: var(--rose); background: var(--rose-dim) }



/* ── Menu déroulant ───────────────────────────────────────────────────────── */
/* Remplace le `<select>` natif, dessiné par le système et impossible à styler.
   Voir `dropdown()` dans dash.js pour le clavier et les rôles ARIA. */
.dd { position: relative }

.dd-btn {
  display: flex; align-items: center; gap: 10px; width: 100%;
  background: #16121B; border: 1px solid var(--line2); border-radius: 10px;
  padding: 10px 12px; color: var(--tx); font: inherit; font-size: 14px;
  cursor: pointer; text-align: left; transition: border-color .15s, background .15s;
}
.dd-btn:hover:not(:disabled) { border-color: rgba(255,158,189,.35) }
.dd-btn:disabled { opacity: .5; cursor: not-allowed }
.dd.open .dd-btn { border-color: rgba(255,158,189,.55) }

.dd-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.dd-label.muted { color: var(--faint) }

/* Le chevron pivote à l'ouverture : le seul repère qui dit dans quel état on
   est quand la liste s'ouvre vers le haut. */
.dd-caret {
  flex: none; width: 9px; height: 9px; border-right: 2px solid var(--faint);
  border-bottom: 2px solid var(--faint); transform: translateY(-2px) rotate(45deg);
  transition: transform .18s ease, border-color .18s;
}
.dd.open .dd-caret { transform: translateY(1px) rotate(-135deg); border-color: var(--rose) }

.dd-list {
  position: absolute; z-index: 60; left: 0; right: 0; top: calc(100% + 6px);
  list-style: none; margin: 0; padding: 5px;
  max-height: 260px; overflow-y: auto;
  background: #1A141F; border: 1px solid var(--line2); border-radius: 12px;
  box-shadow: 0 22px 48px -20px #000, 0 0 0 1px rgba(255,158,189,.10);
  animation: dd-in .16s ease;
}
.dd.up .dd-list { top: auto; bottom: calc(100% + 6px); animation: dd-up .16s ease }
@keyframes dd-in { from { opacity: 0; transform: translateY(-6px) } }
@keyframes dd-up { from { opacity: 0; transform: translateY(6px) } }

.dd-opt {
  padding: 9px 11px; border-radius: 8px; font-size: 14px; color: var(--dim);
  cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background .12s, color .12s;
}
.dd-opt:hover, .dd-opt.on { background: rgba(255,255,255,.06); color: var(--tx) }
.dd-opt.muted { color: var(--faint); font-style: italic }
/* Une valeur enregistrée qui ne correspond plus à rien (salon supprimé) : on la
   montre plutôt que de la remplacer en silence par « aucun ». */
.dd-opt.missing { color: var(--bad) }

/* L'option retenue porte une coche, pas seulement une teinte : la couleur
   seule ne se voit pas de tout le monde. */
.dd-opt[aria-selected="true"] {
  background: var(--rose-dim); color: var(--rose); font-weight: 600;
  position: relative; padding-right: 30px;
}
.dd-opt[aria-selected="true"]::after {
  content: ''; position: absolute; right: 12px; top: 50%;
  width: 5px; height: 9px; margin-top: -6px;
  border-right: 2px solid var(--rose); border-bottom: 2px solid var(--rose);
  transform: rotate(45deg);
}

/* Pastille de couleur d'un rôle, glyphe d'un salon, mise en garde. La couleur
   vient de Discord : elle est posée en variable CSS par le script, parce que la
   CSP interdit l'attribut `style` mais pas `setProperty`. */
.dd-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--c, var(--faint));
          box-shadow: 0 0 0 1px rgba(0,0,0,.45) }
.dd-ico { flex: none; width: 14px; text-align: center; color: var(--faint); font-family: var(--mono) }
.dd-txt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.dd-note { flex: none; color: var(--warn); font-size: 12px }
.dd-opt { display: flex; align-items: center; gap: 9px }
.dd-btn .dd-dot, .dd-btn .dd-ico { margin-right: -2px }

/* Recherche interne, au-delà d'une douzaine d'entrées. */
.dd-search {
  position: absolute; z-index: 61; left: 0; right: 0; top: calc(100% + 6px);
  margin: 5px; width: calc(100% - 10px);
  background: #221A28; border: 1px solid var(--line2); border-radius: 8px;
  padding: 8px 10px; color: var(--tx); font: inherit; font-size: 13.5px;
}
.dd:not(.open) .dd-search { display: none }
/* La place du champ n'est réservée QUE s'il existe : sinon toute liste courte
   s'ouvrait avec un vide de 46 px en haut. */
.dd.searchable.open .dd-list { padding-top: 46px }
.dd.searchable.up .dd-search { top: auto; bottom: calc(100% + 6px); margin-bottom: 5px }
.dd.searchable.up.open .dd-list { padding-top: 5px; padding-bottom: 46px }

/* ── Choix multiple ───────────────────────────────────────────────────────── */
/* Remplace `<select multiple>` et son ctrl+clic. Voir `multiPicker()`. */
.mp { display: grid; gap: 8px }
.mp-search {
  background: #16121B; border: 1px solid var(--line2); border-radius: 9px;
  padding: 8px 11px; color: var(--tx); font: inherit; font-size: 13.5px;
}
.mp-list {
  max-height: 210px; overflow-y: auto;
  background: #16121B; border: 1px solid var(--line2); border-radius: 10px; padding: 5px;
}
.mp-row {
  display: flex; align-items: center; gap: 10px; padding: 8px 10px;
  border-radius: 8px; cursor: pointer; font-size: 14px; color: var(--dim);
  transition: background .12s, color .12s;
}
.mp-row:hover { background: rgba(255,255,255,.05); color: var(--tx) }
.mp-row input { position: absolute; opacity: 0; width: 0; height: 0 }

/* La case elle-même : celle du système ne se colore pas et reste bleue. */
.mp-box {
  flex: none; width: 17px; height: 17px; border-radius: 5px;
  border: 1.5px solid var(--line2); position: relative;
  transition: background .15s, border-color .15s;
}
.mp-box::after {
  content: ''; position: absolute; left: 5px; top: 1px;
  width: 4px; height: 9px; border-right: 2px solid #2A0D16; border-bottom: 2px solid #2A0D16;
  transform: rotate(45deg) scale(0); transition: transform .16s cubic-bezier(.3,1.5,.6,1);
}
.mp-row input:checked + .mp-box { background: var(--rose); border-color: var(--rose) }
.mp-row input:checked + .mp-box::after { transform: rotate(45deg) scale(1) }
.mp-row input:focus-visible + .mp-box { box-shadow: 0 0 0 3px rgba(255,158,189,.3) }
.mp-row:has(input:checked) { color: var(--tx) }

.mp-txt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.mp-count { font-size: 12.5px; color: var(--rose); font-variant-numeric: tabular-nums }
.mp-count.muted { color: var(--faint) }

/* ── Notifications ────────────────────────────────────────────────────────── */
/* `alert()` bloque la page, sort du style du site et ne dit pas d'où il vient.
   Ces bulles-là s'empilent en bas à droite et disparaissent seules. */
.toasts {
  position: fixed; right: 18px; bottom: 18px; z-index: 200;
  display: grid; gap: 10px; max-width: min(380px, calc(100vw - 36px));
}
.toast {
  background: #1A141F; border: 1px solid var(--line2); border-left: 3px solid var(--rose);
  border-radius: 12px; padding: 13px 16px; color: var(--tx); font-size: 14px;
  box-shadow: 0 18px 40px -18px #000; animation: toast-in .25s ease;
}
.toast.ok  { border-left-color: var(--ok) }
.toast.err { border-left-color: var(--bad) }
.toast.out { animation: toast-out .25s ease both }
@keyframes toast-in  { from { opacity: 0; transform: translateY(10px) scale(.98) } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(6px) scale(.98) } }

/* ── Boîte de confirmation ────────────────────────────────────────────────── */
dialog.ask {
  /*
   * `margin: auto` EN PREMIER, et ce n'est pas cosmétique. Le navigateur centre
   * une <dialog> modale avec `position: fixed; inset: 0; margin: auto`. Mais le
   * `* { margin: 0 }` du haut de ce fichier est une règle d'AUTEUR, et l'auteur
   * l'emporte sur la feuille du navigateur : la boîte se retrouvait
   * sur-contrainte (inset 0 des deux côtés + taille fixe) et posée dans le COIN
   * HAUT-GAUCHE — sous l'encoche, sur un iPhone.
   */
  margin: auto;
  border: 1px solid var(--line2); border-radius: 16px; background: #16121B; color: var(--tx);
  padding: 24px;
  /*
   * `100vw` INCLUT la barre de défilement, pas le centrage : la boîte sortait
   * 10 px trop large et son centre tombait 5 px à gauche. `100%` se rapporte au
   * bloc conteneur — pour une <dialog> modale, la fenêtre sans la barre.
   */
  width: min(420px, calc(100% - 40px));
}
dialog.ask::backdrop { background: rgba(6,4,9,.68);
                       -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px) }
dialog.ask p { color: var(--dim); font-size: 14.5px; margin-bottom: 20px }
dialog.ask .actions { margin: 0; justify-content: flex-end }

/* ── Attente ──────────────────────────────────────────────────────────────── */
/* Des blocs gris qui respirent : on voit que ça arrive, et la page ne saute
   pas quand le contenu prend leur place. */
.sk { display: grid; gap: 10px; padding: 6px 0 }
.sk i {
  display: block; height: 14px; border-radius: 7px;
  background: linear-gradient(90deg, rgba(255,255,255,.05), rgba(255,255,255,.10), rgba(255,255,255,.05));
  background-size: 200% 100%; animation: sk 1.3s ease-in-out infinite;
}
.sk i:nth-child(2) { width: 78% } .sk i:nth-child(3) { width: 55% }
@keyframes sk { to { background-position: -200% 0 } }

/* ── Micro-interactions ───────────────────────────────────────────────────── */
.card.hover { transition: transform .18s ease, border-color .18s ease, background .18s ease }
.card.hover:hover { transform: translateY(-2px) }
.cmd { transition: border-color .16s ease, background .16s ease }
.cmd:hover { border-color: rgba(255,158,189,.3); background: rgba(255,158,189,.04) }
.num, .stat { transition: border-color .18s ease }
.num:hover, .stat:hover { border-color: var(--line2) }
/* Le contenu d'un onglet apparaît, il ne clignote pas. */
.tab-in { animation: tab .22s ease }
@keyframes tab { from { opacity: 0; transform: translateY(6px) } }

/* Étiquette « suit ton compte » sur une commande installable sur un compte. */
.cmd .where { float: right; font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase;
              color: var(--faint); border: 1px solid var(--line); border-radius: 5px; padding: 1px 6px }

/* Raccourci clavier affiché dans la recherche. */
.kbd { border: 1px solid var(--line2); border-radius: 6px; padding: 1px 7px; font-size: 11.5px;
       color: var(--faint); font-family: var(--mono) }
.searchwrap { position: relative; flex: 1 1 260px; min-width: 200px; display: flex; align-items: center }
.searchwrap .search { flex: 1 }
.searchwrap .kbd { position: absolute; right: 12px; pointer-events: none }

/* Ligne d'ajustement des bonus d'invitation. */
.bonus { display: flex; gap: 6px; align-items: center }
.bonus button { width: 30px; height: 30px; padding: 0; font-size: 16px; line-height: 1 }

/* ── Ambiance ─────────────────────────────────────────────────────────────── */
/*
 * Tout ce qui bouge ici est LENT et de faible amplitude : une page qui
 * s'agite fatigue et détourne du texte. Chaque animation est coupée sous
 * `prefers-reduced-motion` — voir la fin du fichier.
 */

/* Le logo respire. Douze secondes par cycle, quatre pixels d'amplitude : on ne
   le remarque pas, on le sent. */
.hero-art img { animation: float 12s ease-in-out infinite }
@keyframes float { 0%, 100% { transform: translateY(0) } 50% { transform: translateY(-9px) } }

/* Le halo tourne derrière lui, très lentement, pour que la lumière ne soit pas
   parfaitement figée. */
.hero-art .halo { animation: breathe 9s ease-in-out infinite }
@keyframes breathe { 0%, 100% { transform: scale(1); opacity: 1 } 50% { transform: scale(1.09); opacity: .72 } }

/* Le point vert du pied de page bat comme un signal de vie. */
.state .led { animation: pulse 2.4s ease-in-out infinite }
@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(123,227,163,.15) }
  50%      { box-shadow: 0 0 0 6px rgba(123,227,163,.04) }
}

/* Un lien de navigation se souligne en glissant, plutôt que de s'allumer d'un
   bloc — c'est ce qui distingue une barre soignée d'une barre par défaut. */
.top nav a { position: relative }
.top nav a[href^="#"]::after {
  content: ''; position: absolute; left: 11px; right: 11px; bottom: 4px; height: 1.5px;
  background: var(--rose); border-radius: 2px;
  transform: scaleX(0); transform-origin: left; transition: transform .22s ease;
}
.top nav a[href^="#"]:hover::after,
.top nav a[aria-current="true"]::after { transform: scaleX(1) }
.top nav a[href^="#"]:hover { background: none; text-decoration: none }

/* Les boutons pleins reçoivent un reflet qui traverse au survol. */
.btn.primary { position: relative; overflow: hidden }
.btn.primary::before {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(105deg, transparent 35%, rgba(255,255,255,.42) 50%, transparent 65%);
  transform: translateX(-120%); transition: transform .55s ease;
}
.btn.primary:hover::before { transform: translateX(120%) }

/* Les chiffres de l'en-tête se posent l'un après l'autre au chargement. */
.strip div { animation: rise .5s ease both }
.strip div:nth-child(2) { animation-delay: .07s }
.strip div:nth-child(3) { animation-delay: .14s }
.strip div:nth-child(4) { animation-delay: .21s }
@keyframes rise { from { opacity: 0; transform: translateY(10px) } }

/* Le titre et l'accroche arrivent juste avant. */
.hero-text > * { animation: rise .55s ease both }
.hero-text > *:nth-child(2) { animation-delay: .06s }
.hero-text > *:nth-child(3) { animation-delay: .12s }
.hero-text > *:nth-child(4) { animation-delay: .18s }
.hero-text > *:nth-child(5) { animation-delay: .24s }

/* Une carte de commande s'éclaire par la gauche au survol. */
.cmd { position: relative; overflow: hidden }
.cmd::before {
  content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--rose); transform: scaleY(0); transform-origin: top;
  transition: transform .22s ease;
}
.cmd:hover::before { transform: scaleY(1) }

/* Les lignes d'un tableau se teintent au survol : sur trente lignes, savoir où
   l'œil se trouve compte plus que la décoration. */
tbody tr { transition: background .12s }
tbody tr:hover { background: rgba(255,255,255,.028) }

/* Les onglets du panneau glissent leur repère actif. */
.side button { position: relative; transition: color .16s, background .16s }

/* ── L'arrivée : la lumière, la matière, la profondeur ────────────────────── */
/*
 * Le fond de la page était un dégradé posé sur `body` : une image plate,
 * immobile, identique du premier au dernier pixel. Ce qui suit le remplace par
 * une PIÈCE ÉCLAIRÉE — trois masses de lumière, du grain, un creux d'ombre —
 * dans laquelle les cartes existantes (déjà translucides : `--card` est un
 * blanc à 4,5 %, `--card2` à 2,8 %) se mettent enfin à baigner. Rien n'est
 * ajouté par-dessus l'interface : tout se passe derrière elle.
 *
 * POURQUOI DES ÉLÉMENTS ET NON UN `background` ANIMÉ SUR `body` : un dégradé de
 * fond qu'on déplace se REDESSINE à chaque image, plein écran, sur le
 * processeur. Trois éléments qu'on déplace en `transform` ne redessinent rien —
 * le compositeur repose des textures déjà peintes, sur la carte graphique.
 * C'est toute la différence entre un fond vivant et un ventilateur qui souffle.
 */

.atmo {
  /*
   * PIÈGE `z-index: -1`. Le fond de `body` est « propagé au canevas » par le
   * navigateur (la racine n'ayant aucun fond, c'est celui de `body` qui devient
   * celui de la page) : il est donc peint AVANT les enfants à z-index négatif,
   * qui passent par-dessus. Le décor s'affiche.
   *   Passer ce calque à `z-index: 0` le ferait au contraire passer par-dessus
   * TOUT LE TEXTE : `main` n'est pas positionné, et un élément positionné à
   * z-index 0 est peint après les blocs en flux. Le -1 n'est pas un détail.
   *   Corollaire : le jour où quelqu'un pose un fond sur `html`, ou un
   * `transform`/`isolation` sur `body`, l'atmosphère disparaît d'un coup.
   */
  /*
   * ANCRÉ EN HAUT DU DOCUMENT, ET NON FIXE À L'ÉCRAN. C'est une décision de
   * lisibilité, pas de style, et elle a été prise sur une MESURE.
   *
   * En `position: fixed`, la lumière reste à l'écran pendant que le contenu
   * défile dessous : n'importe quel petit texte finit donc par passer sous sa
   * partie la plus claire. Mesuré sur les 534 textes de la page, fond composé
   * relevé sous chaque glyphe : 68 tombaient sous le seuil WCAG de 4,5:1, les
   * pires à 2,19 (les puces de sous-commandes, 11 px, sur un fond remonté à
   * rgb(102,67,84) par la masse rose).
   *
   * Le budget est mince et c'est arithmétique : `--faint` (#8D7D8F) tient
   * 5,08:1 sur le fond de base, ce qui n'autorise le fond qu'à monter jusqu'à
   * une luminance de 0,0115 — moins de 30/255. Une lueur visible dépasse ça.
   *
   * Ancrée en haut, la lumière éclaire le HÉROS — où le texte est grand, blanc
   * et dispose d'une marge énorme — puis sort de l'écran. Le reste de la page
   * garde son fond sombre maîtrisé. C'est aussi plus juste physiquement : la
   * source, c'est l'avatar du bot, et il est en haut.
   */
  position: absolute; top: 0; left: 0; right: 0; height: min(1250px, 145vh);
  z-index: -1; pointer-events: none;
  overflow: hidden;
  /* La lumière ne doit pas s'arrêter net : elle s'éteint sur son dernier tiers. */
  -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
  /*
   * `contain` borne la zone de dessin au calque : les masses font jusqu'à
   * 1220 px de côté, mais seule la partie visible est rasterisée — la taille
   * des masses ne coûte donc rien en mémoire graphique — et rien de ce qui
   * bouge ici ne peut déclencher un recalcul de la mise en page du site.
   */
  contain: layout paint;
}

/* Le calque de parallaxe. `--sy` est posé par site.js ; le FACTEUR vit ici,
   parce que c'est une décision de mise en scène, pas de programmation. */
.atmo-field {
  position: absolute; inset: 0;
  transform: translate3d(0, calc(var(--sy, 0px) * -.055), 0);
}

.glow { position: absolute; border-radius: 50%; will-change: transform }

/*
 * PIÈGE DES DEUX ANIMATIONS. La dérive et l'arrivée jouent en même temps sur le
 * même élément. Si les deux touchaient `transform`, la dernière de la liste
 * l'emporterait et, avec `both`, GÈLERAIT la dérive pour toujours à la fin du
 * fondu. L'arrivée n'anime donc que `opacity` et `scale` — `scale` est une
 * propriété INDÉPENDANTE de `transform`, les deux se composent au lieu de se
 * disputer. Un navigateur trop vieux pour `scale` ignore l'échelle et garde le
 * fondu : rien ne casse.
 *
 * 53 s, 67 s, 41 s : des durées premières entre elles. Trois masses qui
 * reviendraient ensemble à leur point de départ, c'est une boucle — et une
 * boucle, ça se voit.
 */
.glow-a {                                  /* la source : rose, derrière l'avatar */
  top: -300px; right: -190px; width: 1020px; height: 1020px;
  background: radial-gradient(closest-side,
    rgba(255,158,189,.34), rgba(255,158,189,.12) 42%, rgba(255,158,189,0) 74%);
  animation: drift-a 53s ease-in-out infinite,
             glow-in 1.1s cubic-bezier(.16,.84,.36,1) both;
}
.glow-b {                    /* la réverbération : prune, elle inonde le bas-gauche */
  bottom: -420px; left: -280px; width: 1220px; height: 1220px;
  background: radial-gradient(closest-side,
    rgba(150,92,178,.30), rgba(110,66,134,.11) 46%, rgba(110,66,134,0) 76%);
  animation: drift-b 67s ease-in-out infinite,
             glow-in 1.1s cubic-bezier(.16,.84,.36,1) .12s both;
}
/* Le creux. Une masse SOMBRE, plus noire que le fond, posée en haut à gauche :
   c'est elle qui donne l'épaisseur — sans creux, trois lueurs sur un fond uni
   restent trois taches. Le titre se détache dessus, la lumière est en face :
   l'œil traverse la page en diagonale, du texte vers le bot. */
.glow-c {
  top: -430px; left: -330px; width: 1000px; height: 1000px;
  background: radial-gradient(closest-side,
    rgba(5,3,8,.80), rgba(5,3,8,.32) 54%, rgba(5,3,8,0) 80%);
  animation: drift-c 41s ease-in-out infinite,
             glow-in 1.1s cubic-bezier(.16,.84,.36,1) .06s both;
}
/* `from` SEUL : l'image d'arrivée est l'état NATUREL de l'élément. Une animation
   qui ne démarrerait jamais laisse donc la lumière allumée, jamais éteinte —
   c'est le piège classique de `fill-mode: both`, évité ici par construction. */
@keyframes glow-in { from { opacity: 0; scale: .70 } }
@keyframes drift-a { 0%,100% { transform: translate3d(0,0,0) } 50% { transform: translate3d(-4%, 3%, 0) } }
@keyframes drift-b { 0%,100% { transform: translate3d(0,0,0) } 50% { transform: translate3d(5%, -3%, 0) } }
@keyframes drift-c { 0%,100% { transform: translate3d(0,0,0) } 50% { transform: translate3d(3%, -4%, 0) } }

/* Le balayage d'arrivée : une lame de lumière très large traverse UNE fois, en
   1,7 s, et ne revient jamais. C'est ce passage qui fait comprendre que le fond
   a une matière — sans lui, les masses ont seulement « fondu ».
   Aucun `fill-mode` : l'état de repos est `opacity: 0`, donc invisible. */
.atmo-beam {
  position: absolute; top: -60%; left: 0; width: 40%; height: 220%;
  background: linear-gradient(90deg,
    rgba(255,158,189,0), rgba(255,222,235,.10) 48%, rgba(255,158,189,0));
  opacity: 0;
  animation: beam 1.7s cubic-bezier(.32,.5,.24,1) .1s;
}
@keyframes beam {
  0%   { opacity: 0; transform: rotate(16deg) translate3d(-80vw, 0, 0) }
  18%  { opacity: 1 }
  100% { opacity: 0; transform: rotate(16deg) translate3d(190vw, 0, 0) }
}

/*
 * La torche du curseur.
 *
 * POURQUOI UN ÉLÉMENT DÉPLACÉ et non un `radial-gradient(600px at X Y)` recalé
 * sur la position de la souris : le second REPEINT le dégradé plein écran à
 * chaque image. Celui-ci est peint UNE fois et translaté — le coût par image
 * est celui d'un `translate3d`, c'est-à-dire rien.
 */
.atmo-torch {
  position: absolute; top: 0; left: 0; width: 640px; height: 640px;
  margin: -320px 0 0 -320px;           /* le point chaud tombe sur le curseur */
  background: radial-gradient(closest-side,
    rgba(255,158,189,.15), rgba(255,158,189,.045) 40%, rgba(255,158,189,0) 72%);
  transform: translate3d(var(--tx, 50vw), var(--ty, 38vh), 0);
  opacity: 0; transition: opacity .8s ease-out;
  will-change: transform;
}
:root.atmo-lit .atmo-torch { opacity: 1 }

/*
 * LE GRAIN — c'est lui, la « matière ». Sans lui, trois dégradés sur du noir
 * donnent une bouillie lisse et, sur un écran 8 bits, des anneaux de bandes
 * bien visibles dans les zones sombres. Le bruit les casse.
 *
 * POURQUOI UN SVG EN LIGNE ET PAS UN PNG : du bruit ne se compresse pas. Une
 * tuile PNG de bruit mesurée ici pèse 12,9 Ko en 64 px et 46,5 Ko en 128 px ;
 * cette turbulence fait 330 octets et passe encore par gzip.
 * `stitchTiles='stitch'` raccorde les bords, sinon la répétition dessine une
 * grille. Le `data:` est autorisé par la CSP (`img-src 'self' data:`).
 *
 * POURQUOI AUCUN `mix-blend-mode` : mesuré sur cette page. `overlay` vaut
 * 2 × fond × grain quand le fond est sombre — sur du #0E0B12 il ne fait
 * littéralement rien — et il force une passe de composition supplémentaire,
 * coûteuse sur iOS. En dépôt simple à 7 %, le fond monte de 2,6 niveaux sur 255
 * (invisible) et l'écart-type local passe de 4,8 à 6,5 : c'est ça, du grain.
 */
.atmo-grain {
  position: absolute; inset: -90px;    /* marge pour la dérive, jamais de bord nu */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='150' height='150' filter='url(%23g)'/%3E%3C/svg%3E");
  opacity: .07;
  /* Par PALIERS, pas en continu : un grain qui glisse est un défaut d'écran, un
     grain qui saute trois fois par cycle est de la pellicule. */
  animation: grain 9s steps(3) infinite, atmo-fade 1.6s ease-out .35s both;
}
@keyframes grain {
  0%,100% { transform: translate3d(0,0,0) }
  33%     { transform: translate3d(-22px, 14px, 0) }
  66%     { transform: translate3d(16px, -18px, 0) }
}

/* La vignette ferme les bords et pose le centre de gravité de l'image sur le
   haut de page. Son centre est décalé à 62 %/24 % : l'endroit exact où se
   trouve l'avatar, donc la source de lumière. */
.atmo-vignette {
  position: absolute; inset: 0;
  background: radial-gradient(132% 104% at 62% 24%,
    rgba(14,11,18,0) 50%, rgba(8,5,11,.26) 82%, rgba(6,4,9,.46) 100%);
  animation: atmo-fade 1.6s ease-out .42s both;
}
@keyframes atmo-fade { from { opacity: 0 } }

/* ── Épaisseur des surfaces ──────────────────────────────────────────────── */
/* Une carte translucide posée sur une lumière ne suffit pas : il lui faut une
   ARÊTE. Un filet clair d'un pixel en haut (la lumière qui accroche le bord) et
   une ombre courte en dessous, et la carte cesse d'être un rectangle peint pour
   devenir une plaque posée. Aucune couleur nouvelle : du blanc et du noir. */
.card, .cmd, .num, .stat, .strip div {
  box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 10px -6px rgba(0,0,0,.7);
}
.promo { box-shadow: inset 0 1px 0 rgba(255,222,235,.13), 0 40px 90px -50px rgba(255,158,189,.35) }

/* ── Atmosphère : petits écrans ──────────────────────────────────────────── */
/* Des masses de 1200 px sur 390 px de large, c'est un aplat : plus aucun
   dégradé, juste une teinte. On les resserre pour que la lumière garde un
   centre et un bord. */
@media (max-width: 700px) {
  .glow-a { top: -180px; right: -220px; width: 700px; height: 700px }
  .glow-b { bottom: -260px; left: -220px; width: 780px; height: 780px }
  .glow-c { top: -300px; left: -260px; width: 700px; height: 700px }
  .atmo-vignette {
    background: radial-gradient(140% 60% at 60% 16%,
      rgba(14,11,18,0) 46%, rgba(8,5,11,.22) 78%, rgba(6,4,9,.42) 100%);
  }
}

/* ── Écrans tactiles ──────────────────────────────────────────────────────── */
/*
 * `pointer: coarse` désigne le DOIGT, pas une petite fenêtre : une tablette
 * branchée à un clavier garde l'affichage compact, un téléphone en paysage
 * reçoit quand même les grandes cibles. C'est la bonne question à poser.
 *
 * Les tailles viennent des deux références : Apple recommande 44×44 points,
 * Google 48×48 dp. On vise 44, avec la zone étendue par un pseudo-élément
 * quand agrandir l'élément lui-même casserait la mise en page.
 */
@media (pointer: coarse), (max-width: 700px) {
  .top nav a, .lang a, .btn.small { min-height: 44px; display: inline-flex; align-items: center }
  .brand { min-height: 44px }

  /* Une pastille de filtre se touche : 30 px de haut, c'est rater une fois sur trois. */
  .pill { min-height: 44px; padding: 11px 16px }

  /* Les liens du pied de page faisaient 23 px de haut. La marge devient de la
     hauteur cliquable : le pied garde son allure, la cible triple. */
  footer a { min-height: 44px; display: flex; align-items: center; margin-bottom: 0 }
  footer .cols > div { display: grid }

  /* L'interrupteur reste visuellement fin, mais sa zone sensible déborde. */
  .switch { width: 46px; height: 27px }
  .switch input { min-width: 44px; min-height: 44px; left: 50%; top: 50%; transform: translate(-50%, -50%) }

  /* Les boutons ± des bonus d'invitation : minuscules à la souris, impossibles au doigt. */
  .bonus button { width: 40px; height: 40px }

  /* Les lignes d'un menu déroulant et d'une liste de cases. */
  .dd-opt, .mp-row { padding-top: 12px; padding-bottom: 12px }
  .dd-btn { min-height: 46px }

  /* Le halo bleu que met Chrome sur Android à chaque appui. */
  a, button, label, .switch, .dd-opt, .mp-row { -webkit-tap-highlight-color: rgba(255,158,189,.18) }

  /* Rien ne « survole » au doigt : ces effets restaient collés après un appui. */
  .card.hover:hover { transform: none }
  .cmd:hover::before { transform: scaleY(0) }

  /* Le relief (voir plus bas). Une carte n'a pas à s'incliner sous un pouce qui
     fait défiler, et le liseré resterait allumé après l'appui. La perspective
     est retirée AU REPOS aussi : à zéro degré elle ne se voit pas, mais elle
     crée quand même un contexte d'empilement et un calque à composer, pour
     rien, sur l'appareil qui en a le moins les moyens. */
  .feature { transform: none; transform-style: flat }
  .feature::before, .promo::before,
  .feature:hover::before, .promo:hover::before { opacity: 0 }
  .feature:hover .icon, .feature:hover .n, .feature:hover h3 { translate: none }
}

/* ── Encoche et coins arrondis (iPhone) ───────────────────────────────────── */
/*
 * En paysage, la barre du haut passait SOUS l'encoche et le premier lien
 * devenait inatteignable. `env(safe-area-inset-*)` vaut 0 partout ailleurs, la
 * règle est donc sans effet sur les autres appareils.
 * Elle exige `viewport-fit=cover` dans la balise viewport — voir les gabarits.
 */
@supports (padding: max(0px)) {
  .wrap {
    padding-left:  max(22px, env(safe-area-inset-left));
    padding-right: max(22px, env(safe-area-inset-right));
  }
  footer { padding-bottom: max(60px, env(safe-area-inset-bottom)) }
  .toasts {
    right:  max(18px, env(safe-area-inset-right));
    bottom: max(18px, env(safe-area-inset-bottom));
  }
}

/* ── Adaptatif ───────────────────────────────────────────────────────────── */

@media (max-width: 980px) {
  .hero-grid { grid-template-columns: 1fr }
  .hero-art { display: none }
  .promo { grid-template-columns: 1fr; gap: 28px }
}

@media (max-width: 900px) {
  .grid.g3, .grid.g4 { grid-template-columns: repeat(2, minmax(0, 1fr)) }
  .nums { grid-template-columns: repeat(2, minmax(0, 1fr)) }
  .stats4 { grid-template-columns: repeat(2, minmax(0, 1fr)) }
  footer .cols { grid-template-columns: 1fr 1fr }
  .panel { grid-template-columns: minmax(0, 1fr); gap: 18px }
  .side {
    position: static; grid-auto-flow: column; grid-auto-columns: max-content;
    overflow-x: auto; gap: 6px; padding-bottom: 6px;
    /* PIÈGE : un élément de grille vaut `min-width: auto`, c'est-à-dire « au
       moins la largeur de mon contenu ». Les dix onglets mis bout à bout
       élargissaient donc la COLONNE, et la page entière défilait latéralement
       (236 px de trop sur un Pixel 8) au lieu que la barre défile seule. */
    min-width: 0;
    /* Élan de défilement sur iOS, et arrêt net sur un onglet. */
    -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity;
  }
  .side button { scroll-snap-align: start }
  .side button { white-space: nowrap; border: 1px solid var(--line); border-radius: 999px }
}

@media (max-width: 640px) {
  /*
   * `top: 62px` est la hauteur de l'en-tête sur ORDINATEUR. Sous 640 px il se
   * replie sur deux lignes et mesure ~174 px : la barre d'outils se garait donc
   * DERRIÈRE lui (z-index 20 contre 50), et la recherche disparaissait dès
   * qu'on faisait défiler la section des commandes. Sur téléphone elle défile
   * avec la page — la seule position qui ne dépende pas d'une hauteur d'en-tête
   * qu'on ne connaît pas d'ici.
   */
  /* L'en-tête replié mesure ~175 px sur un 390 px : une ancre visée depuis la
     barre du haut se garait derrière lui. */
  html { scroll-padding-top: 190px }

  .toolbar {
    position: static; padding-top: 0; background: none;
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }

  /* `bottom: 12px` écrasait la marge d'encoche posée plus haut : sur un iPhone,
     la dernière bulle passait sous la barre de gestes. On la conserve ici. */
  .toasts {
    right: 12px; left: 12px; max-width: none;
    bottom: max(12px, env(safe-area-inset-bottom));
  }
  section { padding: 46px 0 }
  .hero { padding: 44px 0 30px }
  .grid.g3, .grid.g2, .grid.g4 { grid-template-columns: 1fr }
  .strip { grid-template-columns: repeat(2, 1fr) }
  .nums, .stats4 { grid-template-columns: repeat(2, 1fr) }
  footer .cols { grid-template-columns: 1fr }
  .top .wrap { height: auto; padding-top: 10px; padding-bottom: 10px; flex-wrap: wrap }
  .top nav { width: 100%; margin-left: 0; order: 3 }
  .promo { padding: 26px 22px }
  .cta .btn { flex: 1 1 100% }
}

/* ── Arrivée : « Le réveil » ──────────────────────────────────────────────── */
/*
 * Une séquence, pas un fondu : le bot s'allume seul dans le noir, sa lumière
 * traverse la page, les mots du titre montent dedans, les chiffres partent sur
 * le temps de la scène, et le dernier reflet tombe sur le bouton — une séquence
 * doit finir sur ce qu'on attend du visiteur.
 *
 * TOUT est écrit sous `html.lit`, une classe posée par site.js. C'est la règle
 * de `.reveal` plus haut, poussée d'un cran : hors de `.lit`, pas une seule
 * déclaration de cette section ne s'applique. Sans JavaScript, avec « moins de
 * mouvement », en revenant sur la page dans le même onglet ou en arrivant sur
 * une ancre, la page est exactement celle d'avant — pas « presque », la même.
 *
 * Et chaque image-clé n'a qu'un `from` : l'état d'arrivée EST l'état de repos
 * du CSS normal, jamais une valeur recopiée. C'est ce qui rend `backwards`
 * suffisant (il ne tient l'élément que pendant son délai) et ce qui interdit
 * structurellement le `fill-mode: both` qui, s'il ne démarre pas, laisse un
 * élément transparent pour toujours.
 */

/*
 * Le tempo de toute la scène tient dans une variable. Les délais s'écrivent en
 * nombres nus (`calc(240 * var(--tempo))`) et site.js descend `--tempo` à
 * .76ms au doigt : les vingt temps de la séquence se resserrent ENSEMBLE au
 * lieu de se décaler les uns par rapport aux autres. Un téléphone se lit à bout
 * de bras, la mise en scène doit y être plus vive.
 * La valeur par défaut est ici pour que la feuille tienne debout seule.
 */
.lit { --tempo: 1ms }

/* ── Plan 1 : le noir, puis le bot ───────────────────────────────────────── */

/* La barre du haut est le cadre, pas le spectacle : elle se contente de venir
   avec la première lumière. Sans ça, la page s'ouvrait sur une navigation
   parfaitement nette au-dessus d'un grand rectangle vide — ça ne se lisait pas
   comme un noir de départ mais comme un chargement en panne. On anime son
   CONTENU et pas elle : son `backdrop-filter` reste une couche tranquille. */
.lit .top .wrap {
  animation: ig-fade calc(620 * var(--tempo)) ease calc(80 * var(--tempo)) backwards;
}
@keyframes ig-fade { from { opacity: 0 } }

/* Le bot est la SOURCE de lumière : il doit exister avant l'onde. `float` est
   redéclaré derrière, décalé d'autant — deux animations qui se disputent la
   même propriété `transform`, c'est la seconde qui gagne et la première qui
   saccade. */
.lit .hero-art img {
  animation: ig-logo calc(1000 * var(--tempo)) cubic-bezier(.16, .9, .3, 1)
             calc(40 * var(--tempo)) backwards,
             float 12s ease-in-out calc(1040 * var(--tempo)) infinite;
}
@keyframes ig-logo { from { opacity: 0; transform: scale(.74) } 55% { opacity: 1 } }

.lit .hero-art .halo {
  animation: ig-halo calc(1400 * var(--tempo)) cubic-bezier(.16, .9, .3, 1) backwards,
             breathe 9s ease-in-out calc(1400 * var(--tempo)) infinite;
}
@keyframes ig-halo { from { opacity: 0; transform: scale(.28) } }

/* ── Plan 2 : l'onde ─────────────────────────────────────────────────────── */
/*
 * `.ignite` est CRÉÉ par site.js et RETIRÉ à la fin : une couche plein écran
 * qui resterait, même invisible, serait une surface de plus à composer à chaque
 * défilement de la page.
 *
 * Son état de repos est `opacity: 0` et ses animations n'ont AUCUN fill-mode.
 * Si elle ne se joue pas, si le script meurt avant de retirer le nœud, si la
 * carte graphique refuse le mélange — dans tous les cas ce qui reste est un div
 * transparent, sans événement de pointeur, contenu. Une couche décorative ne
 * doit jamais pouvoir prendre la page en otage.
 *
 * `mix-blend-mode: screen`, parce que c'est de la LUMIÈRE. En mélange normal le
 * rose passe DEVANT le titre et le teinte comme un filtre de gélatine ; en
 * « screen » il ne peut qu'ajouter : il éclate sur le fond #0E0B12 et effleure
 * à peine le texte blanc. C'est l'opération de la lumière réelle.
 *
 * `z-index: 40` : sous la barre du haut (50), qui est en `backdrop-filter` —
 * l'onde passe donc DERRIÈRE le verre dépoli et s'y diffuse toute seule.
 * `overflow: hidden` avant `contain` : `contain` n'existe pas avant Safari 15.4
 * et l'anneau déborde de trois fois l'écran.
 */
.ignite {
  position: fixed; inset: 0; z-index: 40;
  pointer-events: none; overflow: hidden; contain: layout paint style;
  mix-blend-mode: screen;
}
.ignite::before, .ignite::after { content: ''; position: absolute; opacity: 0 }

/*
 * L'anneau : un DÉGRADÉ, jamais une bordure. La carte graphique rastérise la
 * couche une fois à sa taille naturelle puis l'agrandit 3,4 fois — un trait
 * d'un pixel y devient une bouillie de trois pixels gris, alors qu'un dégradé
 * n'a pas d'arête et n'a donc rien à crénerler.
 *
 * `min(70vmax, 900px)` borne la texture à 900×900 (3,2 Mo de mémoire vidéo)
 * au lieu des 12 Mo qu'un 70vmax nu coûterait sur un écran de bureau.
 *
 * --x / --y : le centre, mesuré sur l'avatar par site.js — une variable CSS
 * posée en JS est la seule chose que la CSP du site laisse varier.
 */
.ignite::before {
  left: var(--x, 50%); top: var(--y, 15%);
  width: min(70vmax, 900px); height: min(70vmax, 900px);
  border-radius: 50%;
  background: radial-gradient(circle,
      rgba(255,158,189,0)   36%,
      rgba(255,158,189,.26) 46%,
      rgba(255,158,189,.60) 50%,
      rgba(255,158,189,.14) 57%,
      rgba(255,158,189,0)   67%);
  animation: ig-wave calc(1100 * var(--tempo)) cubic-bezier(.15, .85, .3, 1)
             calc(140 * var(--tempo));
}
@keyframes ig-wave {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.05) }
  9%   { opacity: 1 }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(3.4) }
}

/* La lueur d'ambiance, derrière l'onde. Elle enfle et RETOMBE À ZÉRO : la fin
   de la séquence est exactement le fond de la page sans JavaScript, et non un
   fond « amélioré » qu'il faudrait ensuite maintenir en double. */
.ignite::after {
  inset: 0;
  background: radial-gradient(58% 52% at var(--x, 50%) var(--y, 15%),
      rgba(255,158,189,.22) 0%, rgba(255,158,189,.06) 44%, transparent 72%);
  animation: ig-bloom calc(2000 * var(--tempo)) ease-out calc(100 * var(--tempo));
}
@keyframes ig-bloom { 0% { opacity: 0 } 24% { opacity: 1 } 100% { opacity: 0 } }

/* ── Plan 3 : le texte prend la lumière ──────────────────────────────────── */

/* On reprend la main sur `rise` (section « Ambiance ») pour les mêmes éléments :
   `.lit .hero-text > *` l'emporte sur `.hero-text > *`, la première
   chorégraphie ne joue plus. */
.lit .hero-text > * {
  animation: ig-lift calc(700 * var(--tempo)) cubic-bezier(.2, .85, .25, 1)
             calc(var(--d, 0) * var(--tempo)) backwards;
}
.lit .hero-text > .lead { --d: 580 }
.lit .hero-text > .cta  { --d: 720 }
.lit .hero-text > .hint { --d: 830 }
@keyframes ig-lift { from { opacity: 0; transform: translateY(16px) } }

/* Le sur-titre s'écarte puis se resserre : un carton de générique.
   `nowrap` parce qu'à .5em d'écartement « Bot Discord » passait à la ligne au
   premier tiers de l'animation et remontait au dernier — deux sauts de mise en
   page pour onze lettres. */
.lit .hero-text > .eyebrow {
  white-space: nowrap;
  animation: ig-eyebrow calc(820 * var(--tempo)) cubic-bezier(.2, .8, .2, 1)
             calc(200 * var(--tempo)) backwards;
}
@keyframes ig-eyebrow { from { opacity: 0; letter-spacing: .5em } }

/* Le titre ne monte pas d'un bloc : ses mots montent l'un après l'autre. */
.lit .hero-text > h1 { animation: none }
/* …sauf quand site.js renonce à le découper (titre balisé, ou trop long) : il
   le dit avec cette classe, et le titre reprend l'entrée simple. Sans cette
   ligne, un titre non découpé n'aurait plus AUCUNE animation et apparaîtrait
   d'un bloc au milieu d'une scène qui, elle, prend son temps. */
.lit .hero-text > h1.whole {
  animation: ig-lift calc(760 * var(--tempo)) cubic-bezier(.2, .85, .25, 1)
             calc(240 * var(--tempo)) backwards;
}

.lit h1 .word {
  display: inline-block;
  /*
   * PIÈGE, et il a coûté dix pixels d'interligne. Le masque évident, c'est
   * `overflow: hidden` — mais sur un élément en ligne, `overflow` autre que
   * `visible` DÉPLACE la ligne de base : la spec la fait passer au bord bas de
   * la marge. Le titre découpé était donc plus haut que le même titre non
   * découpé, et le bloc entier sautait à la seconde où le script prenait la
   * main. Vérifié aux deux langues et sur huit largeurs : avec `clip-path`,
   * hauteur du titre identique au pixel ; avec `overflow`, +10 px partout.
   *
   * Les débords négatifs rendent aux jambages (le « q » de « qu’un », le « y »
   * de « Everything ») la place qu'un masque à ras trancherait net, et laissent
   * au léger pivot d'entrée de quoi passer sur les côtés.
   */
  clip-path: inset(-.45em -.16em -.24em -.16em);
}
.lit h1 .word > span {
  display: inline-block;
  animation: ig-word calc(760 * var(--tempo)) cubic-bezier(.16, .9, .28, 1)
             calc(var(--d, 0) * var(--tempo)) backwards;
}
@keyframes ig-word { from { opacity: 0; transform: translateY(112%) rotate(1.6deg) } }

/* ── Plan 4 : le filet de la barre du haut ───────────────────────────────── */
/* L'onde atteint le haut de l'écran vers 350 ms : le filet s'allume à cet
   instant-là et retombe. Ce n'est pas un ornement, c'est la lumière qui passe —
   d'où un délai qui n'est pas rond. */
.lit header.top::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
  background: linear-gradient(90deg, transparent, var(--rose), transparent);
  opacity: 0; pointer-events: none;
  animation: ig-rail calc(1300 * var(--tempo)) ease-out calc(320 * var(--tempo));
}
@keyframes ig-rail {
  0%   { opacity: 0; transform: scaleX(0) }
  22%  { opacity: 1 }
  100% { opacity: 0; transform: scaleX(1) }
}

/* ── Plan 5 : le bandeau de chiffres ─────────────────────────────────────── */
/* Le cadre arrive entier, puis les quatre cases s'allument de gauche à droite.
   C'est ce balayage qui annonce les compteurs — et site.js les fait partir sur
   le même temps, au lieu de les laisser démarrer au retour du réseau (voir
   `curtain` dans site.js). */
.lit .strip {
  animation: ig-lift calc(760 * var(--tempo)) cubic-bezier(.2, .85, .25, 1)
             calc(900 * var(--tempo)) backwards;
}
.lit .strip div {
  animation: ig-cell calc(640 * var(--tempo)) ease-out
             calc(var(--d, 960) * var(--tempo)) backwards;
}
.lit .strip div:nth-child(2) { --d: 1024 }
.lit .strip div:nth-child(3) { --d: 1088 }
.lit .strip div:nth-child(4) { --d: 1152 }
/* Une couleur OPAQUE : entre deux cases, le fond de la grille est un filet
   d'un pixel qui sert de bordure. Un rose translucide s'y serait mélangé et les
   séparateurs auraient clignoté. #34232E, c'est déjà #130F17 + rose à 14 %. */
@keyframes ig-cell { from { background: #34232E } }

/* Le chiffre s'allume le temps de rouler, puis retrouve son blanc. `--d` est
   hérité de sa case : le halo suit la vague, sans une ligne de plus. */
.lit .strip b {
  animation: ig-num calc(1500 * var(--tempo)) ease-out
             calc(var(--d, 960) * var(--tempo)) backwards;
}
@keyframes ig-num { from { text-shadow: 0 0 22px rgba(255,158,189,.85) } }

/* ── Plan 6 : la chute ───────────────────────────────────────────────────── */
/*
 * Le reflet du bouton principal — celui qui n'existe qu'au survol le reste du
 * temps — traverse UNE fois, tout seul, à la fin. C'est le dernier mouvement de
 * la page : l'œil suit encore, et ce qu'il trouve est ce qu'on lui demande de
 * faire. Sur téléphone, où « survoler » ne veut rien dire, c'est la SEULE fois
 * où ce reflet se voit.
 */
.lit .hero .btn.primary::before {
  animation: ig-shine calc(1000 * var(--tempo)) cubic-bezier(.35, 0, .2, 1)
             calc(1150 * var(--tempo));
}
@keyframes ig-shine {
  from { transform: translateX(-120%) }
  to   { transform: translateX(120%) }
}

/* ── Kirby aspire ─────────────────────────────────────────────────────────── */
/*
 * Le bot s'appelle Kirby, son avatar EST un Kirby, et le geste de Kirby est
 * d'aspirer. C'est la seule chose que ce site possède et qu'aucun autre bot
 * Discord n'aura jamais ; elle ne servait à rien jusqu'ici.
 *
 * Au clic, au doigt, et une fois tout seul à la fin de l'arrivée : la lumière
 * du décor se referme sur l'avatar, des grains roses y tombent en spirale, il
 * les avale, se gonfle, rebondit, et relâche une onde.
 *
 * Tout ce bloc est INERTE tant que site.js n'a pas posé `.gulp` sur le sujet et
 * inséré le calque `.inhale`. C'est la règle de `.reveal` et de `html.lit`,
 * appliquée une fois de plus : sans JavaScript, la page est exactement celle
 * d'aujourd'hui — pas « presque », la même.
 */

/* Le geste se découvre : au survol, le curseur le dit. `touch-action` n'est pas
   du confort — sans lui, deux aspirations rapides au doigt sont lues comme un
   double tap et Safari ZOOME la page. `user-select` évite qu'au troisième clic
   l'image parte en surbrillance bleue. */
.hero-art { cursor: pointer; -webkit-user-select: none; user-select: none; touch-action: manipulation }

/*
 * LE SUJET. Il se creuse (il inspire), gonfle (il avale), rebondit, se pose.
 *
 * PIÈGE, et c'est exactement celui de `.glow-a` plus haut : `.hero-art img`
 * porte DÉJÀ `float`, et `ig-logo` par-dessus pendant l'arrivée — les deux sur
 * `transform`. Une troisième animation sur la même propriété gèlerait la
 * respiration du logo à la fin du gonflement. On n'anime donc pas l'image mais
 * son CONTENEUR, et par la propriété INDÉPENDANTE `scale` : les deux se
 * composent au lieu de se disputer, `float` ne redémarre jamais, et un
 * navigateur trop vieux pour `scale` ignore la bouffée sans que rien d'autre ne
 * bouge d'un pixel.
 *
 * Aucun `fill-mode` : l'état de repos EST l'état d'arrivée (l'échelle
 * naturelle). Si l'animation ne démarre pas, ou si « moins de mouvement »
 * bascule en plein milieu, l'avatar reste à sa taille — jamais gonflé pour
 * toujours.
 */
.hero-art.gulp, .top .brand .mark.gulp {
  animation: kb-gulp calc(1240 * var(--kb, 1ms)) cubic-bezier(.3, .7, .3, 1);
}
@keyframes kb-gulp {
  0%   { scale: 1 }
  22%  { scale: .975 1.02 }     /* il inspire : il se creuse */
  44%  { scale: .94  1.05 }
  56%  { scale: 1.17 1.09 }     /* il avale */
  67%  { scale: 1.09 1.14 }     /* la bouffée remonte */
  79%  { scale: .975 .985 }     /* le rebond */
  90%  { scale: 1.012 1.004 }
  100% { scale: 1 }
}

/*
 * LE CALQUE. Créé et RETIRÉ par site.js, comme `.ignite` et pour la même
 * raison : une couche plein écran qui resterait, même transparente, serait une
 * surface de plus à composer à chaque défilement de la page — pour un effet qui
 * dure une seconde et quart.
 *
 * `mix-blend-mode: screen`, parce que c'est de la LUMIÈRE. En mélange normal
 * les grains passeraient DEVANT le titre et le teinteraient comme une gélatine ;
 * en « screen » ils ne peuvent qu'AJOUTER — ils éclatent sur le #0E0B12 et
 * effleurent à peine le texte blanc.
 *
 * `z-index: 40` : sous la barre du haut (50), qui est en `backdrop-filter`.
 * Sur téléphone, où l'aspiration est centrée sur le petit Kirby de l'en-tête,
 * l'onde passe donc DERRIÈRE le verre dépoli : il la floute et lui écrête son
 * pic avant qu'elle n'atteigne les liens de navigation.
 *
 * `overflow: hidden` AVANT `contain` : `contain` n'existe pas avant Safari 15.4
 * et l'onde déborde de deux fois et demie son cadre.
 */
.inhale {
  position: fixed; inset: 0; z-index: 40;
  pointer-events: none; overflow: hidden; contain: layout paint style;
  mix-blend-mode: screen;
  --kb: 1ms;                       /* le tempo, resserré au doigt par site.js */
  --core: min(38vmin, 340px);      /* le halo fait 330 px : l'onde naît à SA taille */
}

/*
 * LE SEUL PARI DE TOUT LE BLOC, et il se ferme ici. Sans `mix-blend-mode`, les
 * grains et l'onde ne s'ajoutent plus : ils se POSENT sur le texte, en rose, et
 * l'effet devient pire qu'absent. Mieux vaut alors ne rien jouer du tout.
 * Les navigateurs sans `@supports` n'exécutent de toute façon pas site.js
 * (`const`, fonctions fléchées, `?.`) : le calque n'y est jamais créé.
 */
@supports not (mix-blend-mode: screen) { .inhale { display: none } }

/* État de repos : transparent. Aucune animation d'ici n'a de `fill-mode` — ce
   qui reste quand rien ne se joue est donc rigoureusement invisible. */
.inhale::before, .inhale::after { content: ''; position: absolute; opacity: 0 }

/*
 * L'IRIS — la lumière du décor qui se referme. C'est un anneau à la taille
 * EXACTE du halo de l'avatar : l'œil ne voit donc pas apparaître un objet
 * nouveau, il voit le halo lui-même être avalé.
 *
 * Il ne dépasse jamais 170 px de rayon et il RÉTRÉCIT. Sur ordinateur, la
 * colonne de texte s'arrête à 188 px du centre de l'avatar (mesuré : 1076 px de
 * contenu, colonne de 280, gouttière de 48). Cette lueur-là ne touche donc
 * aucun texte — par géométrie, pas par prudence.
 */
.inhale::before {
  left: var(--x, 50%); top: var(--y, 15%);
  width: var(--core); height: var(--core); border-radius: 50%;
  background: radial-gradient(closest-side,
      rgba(255,158,189,0)    52%,
      rgba(255,158,189,.07)  70%,
      rgba(255,222,235,.13)  84%,
      rgba(255,158,189,0)   100%);
  animation: kb-draw calc(620 * var(--kb)) cubic-bezier(.5, .03, .72, .2);
}
@keyframes kb-draw {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(1.04) }
  22%  { opacity: 1 }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(.10) }
}

/*
 * L'ONDE RELÂCHÉE. Un DÉGRADÉ, jamais une bordure : la couche est rastérisée à
 * sa taille naturelle puis agrandie 2,5 fois, et un trait d'un pixel y devient
 * une bouillie de trois pixels gris.
 *
 * La bande est MINCE — 76 % à 100 % du rayon, soit 40 px à la naissance — et
 * l'onde part vite : elle balaie un point donné en 25 à 40 ms. C'est ce qui la
 * rend inoffensive pour le texte, et c'est aussi ce qui la fait lire comme un
 * choc plutôt que comme un lavis. Passé le tiers de sa course elle est déjà
 * retombée sous 10 % d'opacité, donc au niveau du budget de contraste.
 */
.inhale::after {
  left: var(--x, 50%); top: var(--y, 15%);
  width: var(--core); height: var(--core); border-radius: 50%;
  background: radial-gradient(closest-side,
      rgba(255,158,189,0)    76%,
      rgba(255,158,189,.16)  84%,
      rgba(255,222,235,.34)  90%,
      rgba(255,158,189,.10)  96%,
      rgba(255,158,189,0)   100%);
  animation: kb-wave calc(780 * var(--kb)) cubic-bezier(.12, .86, .28, 1) calc(600 * var(--kb));
}
@keyframes kb-wave {
  0%   { opacity: 0;   transform: translate(-50%, -50%) scale(.16) }
  10%  { opacity: 1 }
  32%  { opacity: .30 }
  62%  { opacity: .06 }
  100% { opacity: 0;   transform: translate(-50%, -50%) scale(2.5) }
}

/*
 * LE SOUFFLE : la seule partie de l'effet qui passe VRAIMENT sur du texte
 * courant, donc la seule qui soit plafonnée par un calcul et pas par un goût.
 * 5,5 % en « screen » sur #0E0B12 portent le fond composé à rgb(27,19,27),
 * luminance 0,00795 ; `--faint` (#8D7D8F) y tient encore 4,71:1, et le budget
 * s'arrête à 0,0107. Ne pas monter ce nombre.
 */
.inhale .breath {
  position: absolute; left: var(--x, 50%); top: var(--y, 15%);
  width: min(120vmin, 900px); height: min(120vmin, 900px); border-radius: 50%;
  background: radial-gradient(closest-side,
      rgba(255,158,189,.055) 0%, rgba(255,158,189,.02) 48%, rgba(255,158,189,0) 78%);
  opacity: 0;
  animation: kb-breath calc(900 * var(--kb)) ease-out calc(580 * var(--kb));
}
@keyframes kb-breath {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(.2) }
  16%  { opacity: 1 }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1) }
}

/*
 * LES GRAINS. Deux nœuds par grain, et c'est ce qui fait la SPIRALE : le bras
 * (`.mote`) tourne, le grain (`i`) tombe vers le centre le long du bras. Une
 * rotation composée à une translation, c'est une spirale — sans une ligne de
 * trigonométrie et sans une seule image calculée en JavaScript.
 *
 * L'angle de départ est dans `transform` (partout), le balayage dans la
 * propriété INDÉPENDANTE `rotate` (Safari 14.1+). Un navigateur qui ignore
 * `rotate` garde donc des grains répartis tout autour qui tombent DROIT : c'est
 * moins beau, ce n'est pas cassé. Mettre l'angle dans `rotate` aurait au
 * contraire aligné les seize grains du même côté.
 *
 * `--a` angle, `--r` rayon, `--z` taille, `--d` retard, `--t` durée : posés par
 * site.js avec `setProperty`, la seule chose que la CSP laisse varier ici.
 */
.mote {
  position: absolute; left: var(--x, 50%); top: var(--y, 15%);
  width: 0; height: 0;
  transform: rotate(var(--a, 0deg));
  animation: kb-swirl calc(var(--t, 430) * var(--kb)) cubic-bezier(.45, .02, .72, .24)
             calc(var(--d, 0) * var(--kb));
}
@keyframes kb-swirl { from { rotate: 0deg } to { rotate: var(--w, -130deg) } }

/* Le grain est INVISIBLE au repos, et c'est ce qui rend tout `fill-mode`
   inutile : pendant son retard il n'existe pas, après sa chute il n'existe
   plus, et une animation qui ne démarrerait jamais ne laisse rien à l'écran. */
.mote i {
  position: absolute; left: 0; top: 0;
  width: var(--z, 5px); height: var(--z, 5px);
  margin: calc(var(--z, 5px) / -2) 0 0 calc(var(--z, 5px) / -2);
  border-radius: 50%; opacity: 0;
  background: radial-gradient(circle, #FFF1F6 0%, rgba(255,158,189,.9) 40%, rgba(255,158,189,0) 100%);
  animation: kb-mote calc(var(--t, 430) * var(--kb)) cubic-bezier(.45, .02, .72, .24)
             calc(var(--d, 0) * var(--kb));
}
@keyframes kb-mote {
  0%   { opacity: 0;   transform: translateX(var(--r, 260px)) scale(.45) }
  30%  { opacity: .28 }        /* loin de l'avatar, donc au-dessus du texte : à peine là */
  72%  { opacity: 1 }          /* près du centre : plus aucun texte en dessous */
  100% { opacity: 0;   transform: translateX(0) scale(.95, .22) }
}

/* Sous 980 px l'aspiration est centrée sur le Kirby de la barre du haut, qui
   fait 30 px : une onde née à 340 px l'engloutirait au lieu d'en sortir. */
@media (max-width: 980px) {
  .inhale { --core: min(40vmin, 170px) }
}

/* ── Le fil de lumière : le DÉFILEMENT devient la timeline ────────────────── */
/*
 * Jusqu'ici, « apparaître » était un travail de JavaScript : `reveals()` guette
 * l'entrée d'un élément dans l'écran, pose une classe, et une transition de
 * 550 ms se joue toute seule — une fois, à sa propre vitesse, sur le fil
 * principal. Le doigt donnait le TOP DE DÉPART, rien de plus.
 *
 * Ici, le doigt devient l'HORLOGE. `animation-timeline: view()` remplace le
 * temps par la position de l'élément dans l'écran, `scroll()` par la position
 * de la page : l'image affichée est une fonction du défilement, calculée sur le
 * compositeur, image par image, dans les deux sens. On remonte, ça se rejoue à
 * l'envers. On s'arrête au milieu, ça s'arrête au milieu. Aucune ligne de
 * JavaScript n'est exécutée pendant tout ça — pas même un écouteur de scroll.
 *
 * DEUX PORTES, ET AUCUNE LISTE D'EXTINCTION.
 *   `@supports` : Safari ne connaît pas encore ces timelines. Il ne voit donc
 *   RIEN de ce bloc, et garde la page d'aujourd'hui au pixel — `reveals()`
 *   compris, puisque site.js pose exactement la même question au même analyseur
 *   (`CSS.supports('animation-timeline', 'view()')`). Deux moteurs, une seule
 *   question : ils ne peuvent ni jouer tous les deux, ni s'abstenir tous les deux.
 *   `prefers-reduced-motion: no-preference` : la contrainte habituelle veut une
 *   liste d'extinction qui BATTE EN SPÉCIFICITÉ chaque règle éteinte — un
 *   exercice qu'on rate un jour ou l'autre (`.dd.up .dd-list`, déjà vu). Ici il
 *   n'y a rien à éteindre : sous « moins de mouvement », pas une déclaration de
 *   ce bloc n'existe, pas même les `@keyframes`, pas même les pseudo-éléments
 *   qu'elles animent. Les éléments retrouvent leur état NATUREL, c'est-à-dire
 *   visibles. Et comme c'est le CSS qui décide, basculer le réglage du système
 *   en cours de lecture éteint tout à la seconde — ce qu'un test JavaScript
 *   fait au chargement ne sait pas faire.
 *
 * LA RÈGLE QUI GOUVERNE TOUT LE RESTE : une animation de défilement peut être
 * ARRÊTÉE SUR N'IMPORTE QUELLE IMAGE. Chaque image intermédiaire doit donc être
 * acceptable. D'où deux décisions non négociables :
 *   — l'opacité atteint 1 à 55-60 % de la course, jamais à la fin : un lecteur
 *     qui s'arrête en plein milieu ne peut pas se retrouver devant un texte à
 *     45 % d'opacité, c'est-à-dire sous le seuil de contraste, pour toujours ;
 *   — la course se termine AVANT que l'élément soit entièrement à l'écran
 *     (`entry … %`), jamais après : ce qui est lisible est arrivé.
 *   C'est aussi pourquoi le héros ne s'efface PAS en sortant d'écran, alors que
 *   c'est le premier réflexe de ce genre de page : un titre figé à mi-fondu
 *   est un titre illisible que rien ne vient réparer.
 *
 * TOUT est en `transform`/`opacity`, plus `translate`/`rotate`/`scale` —
 * les propriétés indépendantes, qui SE COMPOSENT avec un `transform` venu
 * d'ailleurs au lieu de se le disputer (c'est déjà la leçon de `.glow` :
 * « scale est une propriété INDÉPENDANTE de transform »). C'est ce qui laisse
 * `.card.hover:hover { transform: translateY(-2px) }` intact sous une animation
 * en `fill-mode: both`. Aucune mesure, aucune couleur, aucune ombre animée.
 *
 * AUCUN `will-change` : Chrome promeut lui-même la couche pendant la phase
 * active d'une animation de défilement et la rend ensuite. Le poser à la main
 * clouerait vingt calques en mémoire vidéo du premier au dernier pixel de la
 * page, pour deux secondes d'usage.
 */

@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {

  /* ── 1. Le décor n'a plus besoin de personne ───────────────────────────── */
  /*
   * La parallaxe du fond était la SEULE chose qui obligeait site.js à écouter
   * le défilement : un écouteur, une boucle `requestAnimationFrame`, et une
   * variable CSS réécrite soixante fois par seconde. La voici en deux
   * déclarations, sur le compositeur, avec la même pente : `-.055 × scrollY`
   * hier, `-69px au bout de 1250px` ici — c'est la même droite.
   *
   * `scroll(root block)` EXPLICITEMENT, jamais `scroll()` nu : `scroll()` va
   * chercher le conteneur de défilement le plus PROCHE et, s'il n'en trouve
   * aucun, l'animation ne se joue pas du tout, en silence. `.atmo` porte
   * `contain: layout paint` juste au-dessus — le genre de propriété dont on
   * n'a pas envie de vérifier l'effet à chaque refonte.
   *
   * `animation-range` s'arrête à 1250 px, la hauteur du décor : au-delà, il est
   * entièrement sorti par le haut. `fill-mode: both` fige alors la lumière au
   * lieu de continuer à la déplacer pour personne.
   *
   * PIÈGE : l'animation porte sur `transform`, exactement la propriété que la
   * règle statique de `.atmo-field` calcule à partir de `--sy`. Une animation
   * l'emporte sur la cascade normale : `--sy` devient inerte, et non
   * contradictoire. Le jour où quelqu'un remet l'écouteur JavaScript, le décor
   * ne bougera pas deux fois plus vite — il ne se passera simplement rien.
   */
  .atmo-field {
    animation-name: sc-parallax;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: scroll(root block);
    animation-range: 0px 1250px;
  }
  @keyframes sc-parallax {
    from { transform: translate3d(0, 0, 0) }
    to   { transform: translate3d(0, -69px, 0) }
  }

  /* ── 2. Le fil de lecture ──────────────────────────────────────────────── */
  /*
   * Une barre de progression, mais posée sur l'arête de la barre du haut, à la
   * place exacte de son filet d'un pixel : elle ne prend pas un pixel de page
   * et elle ne peut passer devant aucun texte.
   *
   * POURQUOI `::before` ET SURTOUT PAS `::after` : `.lit header.top::after` est
   * déjà pris par l'onde d'arrivée (« Plan 4 : le filet de la barre du haut »),
   * et `.lit` n'est JAMAIS retirée de <html>. Une règle `header.top::after`
   * (0,0,2) n'aurait donc jamais rien fait contre `.lit header.top::after`
   * (0,1,2) : le fil n'aurait pas bougé d'un pixel, sans la moindre erreur.
   *
   * Le dégradé remplace la tête de comète : en s'étirant, la barre garde son
   * point le plus clair à sa pointe. Une seule couche, un seul `scaleX`.
   *
   * Pas de `box-shadow` de halo, malgré l'envie : la barre est COLLANTE, son
   * halo passerait donc en permanence sur le texte qui défile dessous, et un
   * rose à 45 % sur douze pixels suffit à faire tomber `--faint` sous 4,5:1.
   * L'arithmétique du contraste laisse au fond 0,0115 de luminance : il n'y a
   * pas la place pour une lueur.
   *
   * À scroll 0 la barre vaut `scaleX(0)` : la séquence d'arrivée n'est pas
   * touchée, et le filet d'onde du Plan 4 la traverse sans rien rencontrer.
   * Elle apparaît aussi sur le tableau de bord, qui partage cette feuille et
   * cet en-tête — c'est voulu, une page de réglages est longue. Pour la
   * réserver à l'accueil : `body:has(.hero) header.top::before`.
   */
  header.top::before {
    content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px;
    background: linear-gradient(90deg,
      rgba(255,158,189,0), rgba(255,158,189,.45) 42%, var(--rose));
    transform: scaleX(0); transform-origin: 0 50%;
    pointer-events: none;
    animation-name: sc-rail;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: scroll(root block);
  }
  @keyframes sc-rail {
    from { transform: scaleX(0) }
    to   { transform: scaleX(1) }
  }

  /* ── 3. Le même contrat que `reveals()`, un autre moteur ───────────────── */
  /*
   * `[data-reveal]` reste LE point d'accroche : c'est l'attribut que pose la
   * page et que lisait l'IntersectionObserver. Rien à renommer, rien à
   * dupliquer, et un `data-reveal` ajouté demain dans index.html sera pris en
   * charge par les deux moteurs sans qu'on rouvre ce fichier.
   *
   * PIÈGE DE PORTÉE, celui qui décide de tout le reste de ce bloc : une plage
   * `entry` n'est sûre que pour un élément PLUS COURT que la fenêtre. Au-delà,
   * `entry 100%` n'arrive qu'une fois le haut de l'élément DÉJÀ sorti par le
   * haut — l'animation tournerait encore pendant qu'on lit. C'est mesuré sur
   * `.promo` : 430 px sur ordinateur, mais près de 900 px sur un téléphone de
   * 700 px de haut. D'où la suite : le bandeau n'est jamais animé en bloc, ce
   * sont ses deux moitiés qui le sont.
   */
  [data-reveal] {
    animation-name: sc-lift;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 4% entry 86%;
  }

  /* ── 4. Les neuf cartes se distribuent ─────────────────────────────────── */
  /*
   * PIÈGE N°1 DES TIMELINES DE DÉFILEMENT, et il n'y en a pas de plus coûteux :
   * `animation-delay` NE FAIT RIEN. Il n'y a plus de temps à retarder. Le
   * décalage d'une carte à l'autre s'écrit dans `animation-range` — chaque
   * élément a SA propre timeline, calée sur SA position. Une colonne qui part
   * 8 % plus tard, c'est 8 % de sa propre entrée, pas 80 ms.
   *   Conséquence heureuse : le décalage VERTICAL est gratuit. Une carte de la
   * deuxième ligne entre plus tard parce qu'elle est plus bas, pas parce qu'on
   * le lui a demandé. Il ne reste à écrire que le décalage horizontal.
   *
   * PIÈGE N°2 : ne JAMAIS écrire le raccourci `animation:` ici. Il remet
   * `animation-timeline` et `animation-range` à leur valeur initiale — même
   * placé avant, une seule ligne de raccourci ajoutée plus tard par distraction
   * débranche la timeline. L'animation redevient alors une animation de temps
   * de durée nulle : elle se joue instantanément au chargement et l'élément
   * finit visible. La panne est invisible, c'est ce qui la rend méchante. Les
   * quatre longhands, toujours.
   *
   * Les colonnes de gauche et de droite arrivent aussi DE leur côté et
   * légèrement inclinées : trois cartes qu'on distribue, pas trois rectangles
   * qui montent. 1,5° suffit — au-delà, le texte de la carte fourmille pendant
   * la rotation sur un écran non-Retina.
   */
  .feature {
    /* La timeline de la carte est NOMMÉE : c'est elle que l'icône ira chercher
       trois lignes plus bas. Un `animation-timeline: --carte` remonte les
       ANCÊTRES de l'élément jusqu'à trouver ce nom — aucun `timeline-scope`
       n'est nécessaire tant qu'on descend. */
    view-timeline-name: --carte;
    animation-name: sc-deal;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: --carte;
    animation-range: entry 0% entry 74%;
  }
  .feature:nth-child(3n+1) { --dx: -20px; --tilt: -1.5deg }
  .feature:nth-child(3n+2) { animation-range: entry 8% entry 87% }
  .feature:nth-child(3n+3) { --dx: 20px; --tilt: 1.5deg; animation-range: entry 16% entry 100% }

  /*
   * La pastille d'icône arrive APRÈS sa carte, avec un dépassement : c'est le
   * dernier geste, celui qu'on remarque. Comme le rythme est linéaire (une
   * timeline de défilement n'a pas d'élan), le rebond est écrit à la main dans
   * les images-clés.
   *   Elle anime `scale`, pas `transform` : `.card.hover:hover .icon` pose
   * `transform: scale(1.06)`, les deux se multiplient au lieu de s'annuler. Le
   * survol continue de marcher pendant et après l'entrée.
   */
  .feature .icon {
    animation-name: sc-icon;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: --carte;
    animation-range: entry 30% entry 100%;
  }

  /* ── 5. Les chiffres s'allument par l'arête ────────────────────────────── */
  /*
   * L'idée évidente — remplir la tuile d'un fond rose au passage — a été
   * MESURÉE et abandonnée : la légende d'une tuile est en `--faint` (#8D7D8F)
   * sur un fond composé à 0,0083 de luminance, soit 4,68:1. Le budget avant de
   * tomber sous 4,5:1 est de 0,0024 de luminance. Du rose à 6 % le fait passer
   * à 0,0146 — contraste 4,23:1, échec. Même à 2 % on est à 4,54:1, c'est-à-dire
   * sur la corde. On n'éclaire donc RIEN derrière du petit texte.
   *
   * Ce qui reste est mieux : c'est l'ARÊTE de la tuile qui s'allume, un filet
   * d'un pixel qui se déploie à sa largeur, à 18 px au-dessus de la première
   * lettre. Huit tuiles qui s'allument l'une après l'autre au fil du
   * défilement, et rien à recalculer côté contraste.
   *
   * `overflow: clip`, SURTOUT PAS `hidden`. `hidden` fait de la tuile un
   * conteneur de défilement — invisible, non défilable, mais un conteneur : la
   * timeline `view()` du filet se recalerait alors sur la tuile elle-même, où
   * il est toujours « entièrement visible », et le filet resterait figé à son
   * état final pour toujours. C'est la même raison qui a fait choisir
   * `overflow-x: clip` sur `.hero` plus haut dans ce fichier.
   */
  .num {
    position: relative; overflow: clip;
    animation-name: sc-rise;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 0% entry 80%;
  }
  .num:nth-child(4n+2) { animation-range: entry 6% entry 86% }
  .num:nth-child(4n+3) { animation-range: entry 12% entry 92% }
  .num:nth-child(4n+4) { animation-range: entry 18% entry 98% }

  .num::before {
    content: ''; position: absolute; inset: 0 0 auto; height: 1px;
    background: linear-gradient(90deg,
      rgba(255,158,189,0), var(--rose) 50%, rgba(255,158,189,0));
    transform: scaleX(.15); opacity: 0; pointer-events: none;
    animation-name: sc-edge;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 25% entry 100%;
  }

  /* ── 6. Le bandeau du tableau de bord s'ouvre en deux ──────────────────── */
  /*
   * Le texte vient de la gauche, l'écran vient de la droite en pivotant sur son
   * arête : deux battants qui s'ouvrent sur le panneau. C'est le seul endroit
   * de la page où l'on se permet de la perspective, parce que c'est le seul
   * objet qui EST un écran.
   *
   * `animation-name: none` sur `.promo` : la règle générique `[data-reveal]`
   * (0,1,0) et celle-ci (0,1,0) ont la MÊME spécificité — c'est l'ordre du
   * fichier qui tranche, et il faut donc que cette ligne reste après. Sans
   * elle, le bandeau monterait en bloc pendant que ses moitiés s'écartent.
   *
   * `overflow: clip` (et non `hidden`, même raison qu'aux tuiles) : sous 640 px
   * la marge du bandeau tombe à 22 px alors que les battants parcourent 26 px.
   * Quatre pixels de texte sortiraient de la boîte, et sur un téléphone quatre
   * pixels de trop, c'est une barre de défilement horizontale.
   */
  .promo { animation-name: none; overflow: clip }

  .promo-text {
    animation-name: sc-door-l;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 5% entry 78%;
  }
  .promo-demo {
    transform-origin: right center;
    animation-name: sc-door-r;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 5% entry 88%;
  }

  /* Les quatre arguments se cochent l'un après l'autre, derrière leur moitié. */
  .promo li {
    animation-name: sc-tick;
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: view();
    animation-range: entry 30% entry 92%;
  }
  .promo li:nth-child(2) { animation-range: entry 36% entry 95% }
  .promo li:nth-child(3) { animation-range: entry 42% entry 98% }
  .promo li:nth-child(4) { animation-range: entry 48% entry 100% }

  /* ── Les images-clés ───────────────────────────────────────────────────── */
  /*
   * Aucune n'a de `to` explicite, sauf le filet. Ce n'est pas une économie :
   * l'image finale IMPLICITE d'une animation CSS est la valeur SOUS-JACENTE,
   * celle que la cascade calcule en direct. C'est exactement ce qui permet à
   * `fill-mode: both` de tenir un élément à la fin de sa course sans figer quoi
   * que ce soit : une carte survolée reprend son `translateY(-2px)` d'elle-même.
   * Un `to` recopié à la main, lui, l'aurait gelée.
   *   Corollaire à retenir : si `reveals()` posait encore `.reveal` (opacity 0)
   * sur ces éléments, la valeur sous-jacente serait 0 et l'animation
   * « finirait » transparente. D'où le garde-fou en tête de `reveals()`.
   */
  @keyframes sc-lift {
    from { opacity: 0; translate: 0 18px }
    55%  { opacity: 1 }
  }
  @keyframes sc-deal {
    from { opacity: 0; translate: var(--dx, 0px) var(--dy, 30px);
           scale: .955; rotate: var(--tilt, 0deg) }
    55%  { opacity: 1 }
  }
  @keyframes sc-icon {
    from { opacity: 0; scale: .45 }
    55%  { opacity: 1; scale: 1.14 }
    78%  { scale: .98 }
  }
  @keyframes sc-rise {
    from { opacity: 0; translate: 0 22px }
    55%  { opacity: 1 }
  }
  /* Le seul `to` explicite du lot : l'état de repos du filet est éteint, il
     faut donc dire où il s'arrête. Il ne peut pas rester coincé invisible : la
     règle qui le crée est dans ce bloc, exactement comme celle qui l'anime. */
  @keyframes sc-edge {
    from { opacity: 0; transform: scaleX(.15) }
    45%  { opacity: 1 }
    to   { opacity: .7; transform: scaleX(1) }
  }
  @keyframes sc-door-l {
    from { opacity: 0; translate: -26px 0 }
    60%  { opacity: 1 }
  }
  @keyframes sc-door-r {
    from { opacity: 0; translate: 26px 0; transform: perspective(900px) rotateY(-10deg) }
    60%  { opacity: 1 }
  }
  @keyframes sc-tick {
    from { opacity: 0; translate: -12px 0 }
    60%  { opacity: 1 }
  }

  /* ── Petits écrans ─────────────────────────────────────────────────────── */
  /*
   * Sous 980 px le bandeau passe sur une colonne : deux battants qui s'écartent
   * horizontalement n'ont plus rien à ouvrir, et la perspective sur une image
   * pleine largeur se voit comme un défaut. Les deux moitiés reprennent la
   * montée commune.
   */
  @media (max-width: 980px) {
    .promo-text, .promo-demo { animation-name: sc-lift }
  }

  /*
   * Sous 900 px la grille passe à deux colonnes : le décalage écrit en 3n
   * tombe alors sur des colonnes qui n'existent plus.
   * PIÈGE DE SPÉCIFICITÉ : `.feature` (0,1,0) ne peut rien contre
   * `.feature:nth-child(3n+1)` (0,2,0). Les sélecteurs sont donc REPRIS un par
   * un, à leur propre poids — c'est la même règle que la liste d'extinction du
   * bas de ce fichier, et elle se rate de la même façon.
   *   Le geste devient aussi plus court : à bout de bras, 30 px de course sur
   * une carte pleine largeur, c'est du ballottement.
   */
  @media (max-width: 900px) {
    .feature { --dy: 18px }
    .feature:nth-child(3n+1),
    .feature:nth-child(3n+3) { --dx: 0px; --tilt: 0deg }
    .feature:nth-child(3n+1),
    .feature:nth-child(3n+2),
    .feature:nth-child(3n+3) { animation-range: entry 0% entry 74% }
    .feature:nth-child(2n)   { animation-range: entry 10% entry 88% }

    .num:nth-child(4n+2),
    .num:nth-child(4n+3),
    .num:nth-child(4n+4) { animation-range: entry 0% entry 80% }
    .num:nth-child(2n)   { animation-range: entry 8% entry 88% }
  }
}
}


/* ── Le relief : la carte prend la lumière du pointeur ────────────────────── */
/*
 * Une carte survolée changeait de couleur. C'est une réaction, ce n'est pas une
 * matière. Ici elle s'INCLINE vers le pointeur, son arête s'allume à l'endroit
 * exact où il se trouve, et son icône se décolle de la surface. La même carte,
 * vue sous un autre angle.
 *
 * TROIS CONTRAINTES ont écrit cette solution, et aucune n'était négociable.
 *
 * 1. LA POLITIQUE DE SÉCURITÉ INTERDIT LE STYLE EN LIGNE (`style-src 'self'`).
 *    Un attribut `style="…"` posé par le script ne serait pas appliqué — pas
 *    d'erreur, juste rien. La seule variation permise à l'exécution est
 *    `element.style.setProperty()`, qui passe par le CSSOM et non par le HTML.
 *    Les quatre variables ci-dessous sont donc TOUT ce que site.js a le droit
 *    d'écrire ; la géométrie, elle, est entièrement ici.
 *
 * 2. UNE ANIMATION BAT TOUJOURS UNE DÉCLARATION. Ces cartes ont déjà une
 *    animation de défilement (« Le fil de lumière »), en `animation-fill-mode:
 *    both` : elle impose une valeur À TOUT INSTANT, y compris au repos. Une
 *    inclinaison écrite dans les mêmes propriétés serait donc ignorée en
 *    silence. Elle anime `translate`, `rotate` et `scale` — les propriétés
 *    INDIVIDUELLES ; on écrit ici dans `transform`, qui est une propriété
 *    distincte, appliquée APRÈS les trois autres. Les deux cohabitent sans se
 *    voir. C'est la seule raison pour laquelle ce bloc utilise `transform`
 *    alors que tout le reste du fichier utilise les propriétés individuelles.
 *
 * 3. RIEN DE TOUT ÇA N'A DE SENS AU DOIGT. Un écran tactile n'a pas de pointeur
 *    à suivre, et une carte qui bascule sous le pouce pendant qu'on fait
 *    défiler est un défaut, pas un effet. La garde est donc à l'ENVERS — l'effet
 *    est déclaré pour tout le monde puis éteint sous `(pointer: coarse)`, dans
 *    le bloc « Écrans tactiles », avec les autres survols du site.
 *      Ce n'est pas un détail de style : `(hover: hover) and (pointer: fine)`
 *    répond FAUX dans un navigateur sans écran ni souris, et tout ce qui est
 *    gardé derrière est donc invérifiable — c'est-à-dire jamais vérifié. La
 *    garde négative, elle, se teste : on émule le tactile, et on regarde.
 *
 * CE QUE L'ON NE FAIT PAS, ET POURQUOI. Le reflet évident — une tache de
 * lumière posée SUR la carte, à l'endroit du pointeur — a été calculé puis
 * abandonné. La légende d'une carte est en `--dim` sur un fond composé à 0,0092
 * de luminance, ce qui donne 8,2:1 ; du blanc à 13 % ferait monter le fond à
 * 0,138 et tomber le contraste à 2,6:1. Le budget réel avant de passer sous
 * 4,5:1 est de 5 % d'alpha, et de 0,15 % seulement pour le texte en `--faint`.
 * Autant dire zéro. La lumière va donc sur l'ARÊTE — un liseré d'un pixel, où
 * il n'y a jamais de texte, et où le budget est infini.
 */
/*
 * L'inclinaison est pour les CARTES, pas pour le grand panneau. Essayée sur
 * `.promo` (1100 px de large, une capture d'écran dedans), elle ne se lit pas
 * comme un objet qu'on penche mais comme une image qui gondole : plus la
 * surface est grande, plus le même angle produit de déplacement au bord. Le
 * panneau garde donc l'arête qui s'allume, et rien d'autre.
 *
 * `perspective()` DANS la transformation, et non la propriété `perspective`
 * sur la grille : neuf cartes doivent chacune être vues de face, pas vues
 * depuis un point de fuite commun — sans quoi celles des bords seraient déjà
 * de travers au repos.
 */
.feature {
  transform: perspective(900px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transform-style: preserve-3d;
}

/*
 * L'arête qui s'allume. Le dégradé remplit toute la carte, puis un masque
 * n'en garde QUE le cadre d'un pixel : c'est la seule façon d'avoir une
 * bordure en dégradé, `border-image` ne sachant pas suivre un point mobile.
 *   `mask-composite: exclude` retire le contenu du tout — reste le liseré.
 * La variante `-webkit-` (`xor`) est pour Safari, qui n'a pas la standard.
 */
.feature::before, .promo::before {
  content: ''; position: absolute; inset: 0; padding: 1px;
  border-radius: inherit; pointer-events: none;
  opacity: 0; transition: opacity .3s ease;
  background: radial-gradient(160px circle at var(--mx, 50%) var(--my, 50%),
              rgba(255,214,229,.9), rgba(255,158,189,.35) 45%, transparent 72%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
}
.feature:hover::before, .promo:hover::before { opacity: 1 }

/* Un repère de position, sans quoi le liseré se poserait sur le premier
   ancêtre positionné, c'est-à-dire n'importe où dans la page. */
.feature, .promo { position: relative }

/*
 * La profondeur. Sans elle, l'inclinaison n'est qu'une image qu'on penche ;
 * avec elle, c'est un objet. Trois plans suffisent, et l'ordre des distances
 * est celui de l'importance : la pastille devant, le titre derrière, le corps
 * du texte au fond — qui ne bouge pas du tout, parce qu'un paragraphe qui
 * flotte devient illisible.
 *   `translate`, pas `transform` : `.card.hover:hover .icon` pose déjà
 * `transform: scale(1.06)` et l'animation d'entrée de l'icône écrit dans
 * `scale`. Les trois s'empilent au lieu de s'effacer.
 */
.feature:hover .icon { translate: 0 0 26px }
.feature:hover .n    { translate: 0 0 30px }
.feature:hover h3    { translate: 0 0 12px }

/* ── Le bandeau de chiffres est VIVANT ────────────────────────────────────── */
/*
 * Ces huit nombres sont relus toutes les soixante secondes. Jusqu'ici la
 * nouvelle valeur remplaçait l'ancienne dans le texte, sans un mot : le
 * bandeau était donc exact et parfaitement mort, et personne ne pouvait savoir
 * qu'il vivait. Maintenant le nombre ROULE de l'ancienne valeur à la nouvelle,
 * la tuile s'entoure d'un filet rose le temps d'une respiration, et l'écart
 * s'affiche une seconde et demie.
 *
 * C'est le seul effet du site qui ne soit pas décoratif : il n'a lieu QUE
 * lorsqu'une valeur a réellement changé. Une page laissée ouverte montre donc
 * le service qui tourne. Une page où rien ne bouge dit la vérité elle aussi.
 *
 * `overflow: clip` sur la tuile (bloc des timelines) découpe tout ce qui sort :
 * la puce est donc posée DANS le rembourrage, jamais au-dessus du bord.
 */
.num::after {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; opacity: 0;
  box-shadow: inset 0 0 0 1px var(--rose), inset 0 0 18px rgba(255,158,189,.16);
}
.num.bump::after { animation: bump 1.6s ease-out both }
@keyframes bump { 0% { opacity: 0 } 14% { opacity: 1 } 100% { opacity: 0 } }

/* Le nombre lui-même passe au rose puis revient : c'est ce qu'on regarde. */
.num.bump b { animation: bump-n 1.6s ease-out both }
@keyframes bump-n { 0%, 14% { color: var(--rose) } 100% { color: var(--tx) } }

/* L'écart. `<i>` sans italique : il fallait une balise courte et neutre, et
   toutes les autres portaient déjà un sens dans cette page. */
.num .delta {
  position: absolute; top: 9px; right: 12px;
  font-size: 11.5px; font-weight: 700; font-style: normal;
  letter-spacing: .01em; color: var(--rose); pointer-events: none;
  animation: delta 1.6s cubic-bezier(.2,.8,.3,1) both;
}
@keyframes delta {
  from { opacity: 0; translate: 0 7px }
  18%  { opacity: 1; translate: 0 0 }
  70%  { opacity: 1 }
  to   { opacity: 0; translate: 0 -9px }
}

/* ── Les transitions entre pages ──────────────────────────────────────────── */
/*
 * Aller de l'accueil au tableau de bord, c'était un blanc puis une autre page.
 * Sur un site aussi tenu, c'était la dernière couture visible.
 *
 * `@view-transition { navigation: auto }` demande au navigateur de photographier
 * l'ancienne page, de charger la nouvelle, et de FONDRE les deux — sans une
 * ligne de JavaScript, sans que le site devienne une application monopage, et
 * sans rien changer au rendu côté serveur.
 *
 * POURQUOI SI PEU DE RÈGLES. La transition par défaut est déjà la bonne : un
 * fondu croisé du document entier. On ne se contente pas de l'accepter, on la
 * CORRIGE sur deux points, et c'est tout ce qui manque.
 */
@view-transition { navigation: auto }

/*
 * PIÈGE n°1 : un nom de transition doit être UNIQUE dans le document. La marque
 * apparaît DEUX fois sur l'accueil — dans l'en-tête et dans le pied de page —
 * et nommer « .brand .mark » désignerait les deux : le navigateur abandonne
 * alors la transition entière, en silence. D'où le `.top` : seule celle de
 * l'en-tête est nommée, et c'est la seule qui existe sur toutes les pages.
 *
 * L'effet : le logo ne disparaît pas pour réapparaître, il RESTE. C'est le même
 * objet qui traverse, et c'est ce qui fait que le site cesse de ressembler à une
 * succession de documents.
 */
.top .brand .mark { view-transition-name: marque }

/*
 * PIÈGE n°2 : par défaut c'est la RACINE entière qui est capturée, en-tête
 * compris. La barre du haut clignotait donc à chaque navigation alors qu'elle
 * est identique partout. En nommant le contenu principal, il devient son propre
 * calque : seul lui change, l'en-tête reste posé.
 */
main { view-transition-name: contenu }

/* L'ancienne page s'efface plus vite qu'elle n'est remplacée : sans ce
   décalage, les deux se superposent au milieu et le texte devient illisible
   pendant 150 ms. Elle recule AUSSI d'un cheveu (scale .985) : filmée image par
   image, une double exposition qui ne joue que sur l'opacité se lit comme une
   bouillie, la même avec un écart de profondeur se lit comme un passage. */
::view-transition-old(contenu) { animation: vt-sortie .22s ease-in both }
::view-transition-new(contenu) { animation: vt-entree .34s cubic-bezier(.2,.8,.3,1) .06s both }
@keyframes vt-sortie { to   { opacity: 0; transform: translateY(-8px) scale(.985) } }
@keyframes vt-entree { from { opacity: 0; transform: translateY(14px) } }

/* La marque, elle, ne fond pas : elle se DÉPLACE. Le navigateur interpole tout
   seul entre les deux positions, on ne fait qu'allonger un peu le temps pour
   que l'œil ait la chance de le suivre. */
::view-transition-group(marque) { animation-duration: .34s }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto }
  * { transition: none !important }
  /* Sans l'animation, l'élément doit être visible : la classe .reveal seule
     le laisserait transparent pour toujours. */
  .reveal { opacity: 1; transform: none }
  .card.hover:hover { transform: none }
  /* Le décor RESTE — c'est le décor, pas une animation. Il cesse seulement de
     bouger : les masses gardent leur état naturel, donc la page reste aussi
     éclairée, simplement immobile. Le balayage n'a jamais lieu et la torche
     n'existe pas : ce sont les deux seules choses qui RÉAGISSENT, et réagir est
     justement ce qu'on refuse ici. */
  .glow, .atmo-grain, .atmo-vignette { animation: none }
  .atmo-beam, .atmo-torch { display: none }
  .atmo-field { transform: none }

  /*
   * PIÈGE : cette liste doit BATTRE EN SPÉCIFICITÉ chaque règle qu'elle éteint.
   * Elle ne visait que les sélecteurs de REPOS — or les animations sont
   * déclarées sur des sélecteurs d'ÉTAT, plus spécifiques : `.dd-list` (0,1,0)
   * ne peut rien contre `.dd.up .dd-list` (0,3,0), ni `.toast` contre
   * `.toast.out`, ni `.promo-demo img` contre `.promo-demo img.live`. Mesuré au
   * navigateur : un menu qui s'ouvre vers le haut glissait encore, et chaque
   * bulle repartait en glissant, pour quelqu'un qui demandait moins de mouvement.
   */
  .sk i, .tab-in, .toast, .toast.out, .dd-list, .dd.up .dd-list,
  .hero-art img, .hero-art .halo, .state .led, .strip div, .hero-text > *, .cmd,
  .promo-demo img, .promo-demo img.live { animation: none }
  .btn.primary::before, .top nav a::after, .cmd::before { transition: none }
  /* Les icônes gardent leur dessin, elles cessent simplement de bouger. */
  .feature svg [class] { animation: none !important }
  .card.hover:hover .icon { transform: none }
  .dd-caret, .mp-box, .mp-box::after { transition: none }

  /*
   * Le relief et le bandeau vivant. Les deux premières lignes visent des
   * sélecteurs d'ÉTAT (`.num.bump`, `.feature:hover`) : citées au repos, elles
   * ne pourraient rien. La troisième remet la carte à plat — `transform` n'est
   * pas une transition, `* { transition: none }` plus haut ne l'atteint pas, et
   * les variables continueraient d'être écrites par le script.
   */
  .num.bump::after, .num.bump b, .num .delta { animation: none !important; opacity: 0 }
  .feature::before, .promo::before, .feature:hover::before, .promo:hover::before { opacity: 0 }
  .feature { transform: none }
  .feature:hover .icon, .feature:hover .n, .feature:hover h3 { translate: none }



  /*
   * Ceinture ET bretelles pour l'arrivée. site.js ne pose déjà pas `.lit` quand
   * le mouvement est refusé — mais le réglage du système peut changer APRÈS le
   * chargement : le CSS suit en direct, le script non. Sans ces lignes,
   * basculer « moins de mouvement » à 260 ms laissait l'accroche, les boutons
   * et le bandeau de chiffres à `opacity: 0`, pour toujours. Mesuré.
   */
  .ignite { display: none }
  /* Même raison : les règles d'état de la séquence sont citées une par une, à
     leur propre spécificité — `.lit .hero-text > *` (0,2,0) ne peut rien contre
     `.lit .hero-text > .eyebrow` (0,3,0). */
  .lit .top .wrap, .lit .hero-text > *, .lit h1 .word > span, .lit .strip,
  .lit .strip div, .lit .strip b, .lit .hero-art img, .lit .hero-art .halo,
  .lit header.top::after, .lit .hero .btn.primary::before,
  .lit .hero-text > .eyebrow, .lit .hero-text > h1.whole { animation: none }
  /* Sans animation, plus rien ne doit être masqué. */
  .lit h1 .word { clip-path: none }

  /*
   * Kirby aspire : le calque n'est même pas peint, et le sujet garde sa taille.
   * Les sélecteurs sont cités À LEUR PROPRE SPÉCIFICITÉ, comme le reste de ce
   * bloc : `.hero-art img` (juste au-dessus) ne peut RIEN contre
   * `.hero-art.gulp` (0,2,0), qui vise un autre élément — l'oublier laisserait
   * l'avatar en train de gonfler pour quelqu'un qui vient de demander l'inverse.
   */
  .inhale { display: none }
  .hero-art.gulp, .top .brand .mark.gulp { animation: none }
  .mote, .mote i { animation: none }
  /* Un curseur en forme de main qui ne promet plus rien serait un mensonge. */
  .hero-art { cursor: default }

  /*
   * Une page qui glisse donne la nausée à certaines personnes. `animation: none`
   * sur les calques de transition les fait tenir zéro seconde : la navigation
   * redevient instantanée, exactement comme avant. Le sélecteur universel `(*)`
   * couvre aussi les noms qu'on ajouterait plus tard sans y penser.
   */
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
    animation: none !important;
  }
}

/* ── Kirby Premium : mur des soutiens, page publique, éditeur de tags (11/09) ──
 *
 * Trois pages servies par premium.js. Même vocabulaire que le tableau de bord
 * (.block, .field, .toggle, .btn, dialog.ask) : seules les pièces propres à ces
 * pages vivent ici. La couleur choisie par un abonné n'entre JAMAIS par un
 * attribut style= (la CSP le refuse) : premium.js pose --pp-accent par le CSSOM.
 */

/* `hidden` doit l'emporter sur les display: flex/grid de ces composants. */
.pw-app [hidden], .pp-app [hidden], .tg-app [hidden] { display: none !important }

.phead { padding: 44px 0 26px }
.phead h1 { font-size: clamp(30px, 5vw, 44px); line-height: 1.1; margin: 8px 0 12px }

.pw-count { color: var(--faint); font-size: 13.5px; margin-bottom: 14px }
.pw-empty { color: var(--dim); padding: 24px 0 32px }
.pw-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 14px; margin-bottom: 34px }
.pw-card {
  display: flex; align-items: center; gap: 14px; min-width: 0; padding: 14px 16px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  color: var(--tx); text-decoration: none;
}
a.pw-card:hover { border-color: rgba(255,158,189,.35); background: rgba(255,158,189,.05); text-decoration: none }
.pw-card[data-tier="sapphire"] { border-color: rgba(110,168,255,.34) }
.pw-card[data-tier="amethyst"] { border-color: rgba(196,140,255,.38) }
.pw-card[data-tier="crown"]    { border-color: rgba(255,201,64,.45); background: linear-gradient(135deg, rgba(255,201,64,.08), var(--card) 62%) }
.pw-av { width: 52px; height: 52px; flex: none; border-radius: 50%; object-fit: cover; border: 1px solid var(--line2) }
.pw-txt { display: grid; gap: 3px; min-width: 0 }
.pw-name { display: flex; align-items: center; gap: 7px; min-width: 0; margin: 0; font-size: 15px; font-weight: 700 }
.pw-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.pw-badge { width: 20px; height: 20px; flex: none }
.pw-meta { color: var(--faint); font-size: 12.5px }
.pw-chip {
  justify-self: start; margin-top: 2px; padding: 3px 9px; border-radius: 99px;
  border: 1px solid rgba(255,201,64,.4); color: var(--warn);
  font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
}
.pw-join { margin: 6px 0 64px }

/* ── /u/<id> : la page composée ──────────────────────────────────────────────
 *
 * Les couleurs viennent du document de la personne (services/premiumProfile.js),
 * posées en variables CSS par premium.js. Chaque valeur a un REPLI : une page
 * non réglée reste la page Kirby habituelle.
 *
 * ⚠️ EMPILEMENT. Le fond et son voile sont des calques FIXES. Les mettre en
 * `z-index: -1` les enverrait DERRIÈRE le fond opaque de <body> — le piège déjà
 * rencontré sur le panneau musique. La couleur de base est donc portée par
 * <html>, <body> devient transparent, et les trois plans sont numérotés
 * explicitement : fond 0, voile 0, contenu 1.
 */
body[data-page="profile"] { background: var(--pp-bg, var(--bg)); color: var(--pp-text, var(--tx));
  overflow-x: clip }
body[data-page="profile"] > *:not(.pp-sky):not(.pp-veil) { position: relative; z-index: 1 }

.pp-sky, .pp-veil { position: fixed; inset: 0; z-index: 0; pointer-events: none }
.pp-sky {
  background-image: var(--pp-image); background-size: cover; background-position: center;
  filter: blur(var(--pp-blur, 0px));
  /* Le flou mange les bords : le calque déborde volontairement de 6 %, sinon il
     laisse une frange transparente de la largeur du rayon tout autour.
     ⚠️ PAS de `transform: scale()` : l'agrandissement partait du centre et
     sortait du cadre à DROITE à toutes les largeurs (mesuré de 320 à 1920 px).
     `inset` négatif déborde des quatre côtés sans jamais élargir la page. */
  inset: -6%;
}
.pp-veil { background: rgba(var(--pp-veil-color, 0,0,0), var(--pp-veil-alpha, 0)) }

/* Surface d'un bloc : la teinte du voile, donc toujours lisible sous le texte. */
.pp-app { --pp-panel: rgba(var(--pp-veil-color, 0,0,0), .22); --pp-hair: rgba(var(--pp-veil-color, 0,0,0), .3) }
.pp-blocks { display: grid; gap: 18px; align-content: start; min-width: 0 }
.pp-block {
  background: var(--pp-panel); border: 1px solid var(--pp-border, var(--line));
  border-radius: var(--radius); padding: 20px;
  backdrop-filter: blur(6px);
}
.pp-block h3 { color: var(--pp-text, var(--tx)); margin-bottom: 12px; font-size: 15px }
.pp-block .sub, .pp-block small { color: var(--pp-text, var(--dim)); opacity: .72 }

/* ── Identité ─────────────────────────────────────────────────────────────── */
/* ── L'IDENTITÉ EST UNE SEULE CARTE ────────────────────────────────────────
 *
 * ⚠️ LA CAUSE DU « BUG DE SIDEBAR », TROUVÉE LE 18/09 (V5.1).
 *
 * L'identité était composée de DEUX boîtes stylées séparément : la bannière
 * (sa propre bordure, son propre rayon) et `.pp-idt` (son propre voile, son
 * propre rayon). Empilées, elles formaient deux rectangles aux largeurs et aux
 * coins qui ne se répondaient pas, séparés par une couture horizontale nette —
 * ce que l'œil lit comme un panneau cassé ou dupliqué.
 *
 * La V5 avait retiré le voile, mais UNIQUEMENT dans la règle `fiche` du bloc
 * `@media (min-width: 900px)`. En dessous de 900 px — c'est-à-dire sur tous les
 * téléphones et toutes les tablettes — et dans le gabarit `vitrine` à TOUTES
 * les largeurs, les deux rectangles étaient toujours là. D'où un bug qui
 * « revenait » : il n'avait jamais été corrigé là où il se voyait le plus.
 *
 * Désormais UNE carte : `.pp-id` porte la surface, la bordure et le rayon ; la
 * bannière déborde jusqu'à ses arêtes par marges négatives (pas de
 * `overflow: hidden`, qui rognerait le halo de la photo) ; `.pp-idt` ne dessine
 * plus rien. Valable pour les trois gabarits et toutes les largeurs — le
 * gabarit `minimal`, qui se veut sans carte, s'en retire explicitement.
 */
.pp-id {
  margin: 22px 0 24px;
  padding: var(--pp-pad, 18px);
  background: rgba(var(--pp-veil-color, 0,0,0), var(--pp-scrim-alpha, 0));
  border: 1px solid var(--pp-border, var(--line));
  border-radius: var(--radius);
}
.pp-banner {
  margin: calc(-1 * var(--pp-pad, 18px)) calc(-1 * var(--pp-pad, 18px)) 0;
  border: none;
  border-radius: calc(var(--radius) - 1px) calc(var(--radius) - 1px) 0 0;
  overflow: hidden;
  aspect-ratio: 4 / 1;
}
.pp-banner img { width: 100%; height: 100%; object-fit: cover; display: block }
.pp-idt {
  display: grid; justify-items: center; text-align: center; gap: 10px;
  padding: 18px 0 4px;
}
/* Sans bannière, la carte n'a pas besoin de son décalage de compensation. */
.pp-id > .pp-idt:first-child { padding-top: 4px }
.pp-avatar {
  width: 120px; height: 120px; border-radius: 50%; object-fit: cover;
  border: 3px solid var(--pp-accent, var(--rose)); background: var(--pp-bg, var(--bg));
}
/* Le cadre de la photo. Quatre allures réellement différentes, sans image. */
.pp-avatar[data-ring="halo"] { border-color: transparent;
  box-shadow: 0 0 0 3px var(--pp-accent, var(--rose)), 0 0 34px 6px var(--pp-accent, var(--rose)) }
.pp-avatar[data-ring="carre"] { border-radius: 22px }
.pp-avatar[data-ring="aucun"] { border-color: transparent; border-width: 0 }
.pp-tagline { color: var(--pp-accent, var(--rose)); font-size: 14.5px; font-weight: 600;
  letter-spacing: .01em; margin: -2px 0 0 }
.pp-banner + .pp-idt .pp-avatar { margin-top: -62px }
.pp-name { display: flex; align-items: center; gap: 10px; margin: 0; font-size: clamp(26px, 5vw, 38px); line-height: 1.12; text-wrap: balance }
.pp-badge { width: 28px; height: 28px; flex: none }
.pp-meta { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px 12px; color: var(--pp-text, var(--dim)); opacity: .8; font-size: 14px }
.pp-bio { max-width: 58ch; color: var(--pp-text, var(--tx)); opacity: .92; line-height: 1.6 }
.pp-since { color: var(--pp-text, var(--dim)); opacity: .7; font-size: 13.5px }

/* ── Liens ────────────────────────────────────────────────────────────────── */
.pp-links { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 10px }
.pp-link {
  display: flex; align-items: center; gap: 10px; min-width: 0;
  padding: 11px 14px; border-radius: 12px; text-decoration: none;
  border: 1px solid var(--pp-border, var(--line)); color: var(--pp-text, var(--tx));
  background: rgba(var(--pp-veil-color, 0,0,0), .14);
}
/* Un lien mis en avant prend toute la largeur et porte l'accent : c'est celui
   qu'on veut qu'on ouvre. */
.pp-link.featured { grid-column: 1 / -1; border-color: var(--pp-button, var(--rose));
  background: rgba(var(--pp-veil-color, 0,0,0), .26); padding: 14px 16px }
.pp-link-txt { display: grid; gap: 1px; min-width: 0 }
.pp-link-txt b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.pp-link-txt small { color: var(--pp-text, var(--dim)); opacity: .68; font-size: 12.5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.pp-sec-body { color: var(--pp-text, var(--tx)); opacity: .92; line-height: 1.6; margin: 0; max-width: 68ch }
.pp-link svg { flex: none; color: var(--pp-button, var(--rose)) }
.pp-link:hover { border-color: var(--pp-button, var(--rose)) }

/* ── Chiffres ─────────────────────────────────────────────────────────────── */
.pp-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 14px }
.pp-stat { display: grid; gap: 2px }
.pp-stat b { font-size: 26px; font-variant-numeric: tabular-nums; color: var(--pp-accent, var(--rose)); line-height: 1.1 }
.pp-stat small { color: var(--pp-text, var(--dim)); opacity: .75; font-size: 12.5px }

/* ── Contenus ─────────────────────────────────────────────────────────────── */
.pp-tag { padding: 14px 0; border-top: 1px solid var(--pp-hair, var(--line)) }
.pp-tag:first-of-type { border-top: none; padding-top: 0 }
.pp-tag-top { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 8px }
.pp-tag-top small { color: var(--pp-text, var(--faint)); opacity: .7 }
.pp-mudae { display: grid; gap: 10px; margin: 0; padding: 0; list-style: none }
.pp-mudae li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; align-items: baseline }
.pp-mudae small { grid-column: 1; color: var(--pp-text, var(--faint)); opacity: .7; font-size: 12.5px }
.pp-kak { color: var(--pp-accent, var(--rose)); font-size: 13.5px; font-weight: 600 }

/* ── Gabarit « fiche » : identité à gauche, contenus à droite ───────────────
 *
 * ⚠️ MESURÉ LE 18/09 : ce gabarit vit dans `main.wrap.narrow`, plafonné à
 * 860 px. Les deux colonnes tombaient donc à `320px 470px` — IDENTIQUES à
 * 1024 px et à 1440 px, et sur un écran de 1920 px les trois quarts de la
 * largeur restaient vides pendant que le contenu s'entassait dans 470 px.
 *
 * Une fiche à deux colonnes est faite pour occuper la largeur ; elle sort donc
 * du gabarit étroit, qui reste le bon réglage pour la vitrine (centrée) et la
 * minimale (typographique).
 */
body[data-layout="fiche"] main.narrow { max-width: 1180px }

@media (min-width: 900px) {
  [data-layout="fiche"] .pp-app { display: grid; grid-template-columns: 320px minmax(0, 1fr); gap: 26px; align-items: start }
  [data-layout="fiche"] .pp-id { position: sticky; top: 84px; margin: 22px 0 0; --pp-pad: 20px }
  [data-layout="fiche"] .pp-banner { aspect-ratio: 5 / 2 }
  [data-layout="fiche"] .pp-banner + .pp-idt .pp-avatar { margin-top: -50px }
  [data-layout="fiche"] .pp-avatar { width: 96px; height: 96px }
  [data-layout="fiche"] .pp-name { font-size: 24px }
  [data-layout="fiche"] .pp-links { grid-template-columns: 1fr }
}
/* Sous 1100 px, 320 px d'identité laissent trop peu au contenu. */
@media (min-width: 900px) and (max-width: 1099px) {
  [data-layout="fiche"] .pp-app { grid-template-columns: 264px minmax(0, 1fr); gap: 20px }
  [data-layout="fiche"] .pp-avatar { width: 84px; height: 84px }
}

/* ── Gabarit « minimal » : que du texte, aligné à gauche ──────────────────── */
[data-layout="minimal"] .pp-id { background: none; border: none; padding: 0 }
[data-layout="minimal"] .pp-idt { justify-items: start; text-align: left; grid-template-columns: auto minmax(0, 1fr); gap: 6px 18px; margin-top: 14px; padding: 0 }
[data-layout="minimal"] .pp-avatar { width: 72px; height: 72px; grid-row: span 2; border-width: 2px }
[data-layout="minimal"] .pp-name { font-size: clamp(24px, 4.4vw, 32px); align-self: end }
[data-layout="minimal"] .pp-meta { justify-content: flex-start }
[data-layout="minimal"] .pp-bio, [data-layout="minimal"] .pp-since { grid-column: 1 / -1 }
[data-layout="minimal"] .pp-block { background: none; border: none; border-top: 1px solid var(--pp-hair, var(--line)); border-radius: 0; padding: 18px 0 0; backdrop-filter: none }
[data-layout="minimal"] .pp-links { display: flex; flex-wrap: wrap; gap: 8px }
[data-layout="minimal"] .pp-link { padding: 7px 12px; border-radius: 100px; background: none }
[data-layout="minimal"] .pp-stat b { font-size: 20px }

@media (max-width: 560px) {
  .pp-avatar { width: 96px; height: 96px }
  .pp-banner + .pp-idt .pp-avatar { margin-top: -48px }
  [data-layout="minimal"] .pp-idt { grid-template-columns: 1fr }
  [data-layout="minimal"] .pp-avatar { grid-row: auto }
}

/* ── /premium : l'éditeur de page ────────────────────────────────────────────
 *
 * Deux colonnes : les réglages à gauche, l'aperçu à droite, qui suit le
 * défilement. L'aperçu est un VRAI rendu — mêmes classes .pp-* que la page
 * publique — simplement enfermé dans un cadre : les calques y sont `absolute`
 * au lieu de `fixed`, et rien d'autre ne change.
 */
/* `hidden` doit gagner contre nos propres `display:` — sinon un avertissement
   masqué reste affiché (vu sur chaque ligne de lien, alors que l'adresse était
   valide). Les autres pages du site portent déjà la même règle. */
.ce-app [hidden] { display: none !important }
.ce-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; align-items: start }
.ce-controls { display: grid; gap: 18px; min-width: 0 }
@media (max-width: 1000px) { .ce-grid { grid-template-columns: minmax(0, 1fr) } }

/* ── Onglets ───────────────────────────────────────────────────────────────
 * Une colonne unique atteignait douze mille pixels sur un téléphone. Les
 * onglets ne sont pas un ornement : ils rendent la page parcourable.
 */
.ce-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 18px;
  border-bottom: 1px solid var(--line); padding-bottom: 10px }
.ce-tab {
  padding: 8px 14px; border-radius: 10px; cursor: pointer; font: inherit; font-size: 14px;
  background: none; border: 1px solid transparent; color: var(--dim);
}
.ce-tab:hover { color: var(--tx); border-color: var(--line2) }
.ce-tab.on { background: var(--rose-dim); border-color: var(--rose); color: var(--tx); font-weight: 600 }

/* Sans aperçu à côté, la colonne se BORNE : un champ de texte de 1 400 px de
   large ne se lit pas, il se parcourt. Dans la grille, l'aperçu occupe déjà
   la moitié droite et la borne n'a plus lieu d'être. */
.ce-controls { max-width: 760px }
.ce-controls.wide { max-width: none }
.ce-grid > .ce-controls { max-width: none }
.ce-sub { font-size: 13px; color: var(--dim); font-weight: 600; letter-spacing: .01em;
  margin-top: 4px; padding-top: 14px; border-top: 1px solid var(--line) }
.ce-sec > .ce-sub:first-of-type { margin-top: 0; padding-top: 0; border-top: none }

/* ── Palettes ──────────────────────────────────────────────────────────────── */
.ce-identity { display: grid; gap: 10px; justify-items: start }
.ce-presets { display: flex; flex-wrap: wrap; gap: 8px }
.ce-preset {
  display: flex; align-items: center; gap: 4px; padding: 7px 11px 7px 8px; cursor: pointer;
  background: var(--card2); border: 1px solid var(--line2); border-radius: 100px; color: var(--tx); font: inherit; font-size: 13px;
}
.ce-preset:hover { border-color: var(--rose) }
.ce-preset i { width: 12px; height: 12px; border-radius: 50%; background: var(--dot); display: block;
  border: 1px solid rgba(255,255,255,.14) }
.ce-preset span { margin-left: 5px; text-transform: capitalize }

/* ── Chiffres ──────────────────────────────────────────────────────────────── */
/* ── Le catalogue des avantages ────────────────────────────────────────────
 * Des fiches, pas des cartes uniformes : celles qui sont déjà réglées portent
 * un filet à gauche, les autres restent sobres. On lit d'abord ce qui est fait.
 */
.ce-perks { display: grid; grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); gap: 12px }
.ce-perk {
  padding: 14px 16px 13px; border-radius: 12px; min-width: 0;
  background: var(--card2); border: 1px solid var(--line);
  border-left: 3px solid var(--line2);
}
.ce-perk.on { border-left-color: var(--rose); background: var(--card) }
.ce-perk-top { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 8px; margin-bottom: 6px }
.ce-perk-top b { font-size: 14.5px }
.ce-perk p { color: var(--dim); font-size: 13.5px; line-height: 1.5; margin: 0 }
.ce-perk-where { display: block; margin-top: 8px; color: var(--faint); font-size: 11.5px;
  text-transform: uppercase; letter-spacing: .05em }
.ce-pill { font-size: 12px; padding: 2px 8px; border-radius: 100px; border: 1px solid var(--line2); color: var(--faint) }
.ce-pill.on { border-color: var(--rose); color: var(--rose) }
.ce-pill.free { border-color: var(--ok); color: var(--ok) }

.ce-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 14px }
.ce-stat { display: grid; gap: 2px }
.ce-stat b { font-size: 25px; font-variant-numeric: tabular-nums; color: var(--rose); line-height: 1.1 }
.ce-stat small { color: var(--faint); font-size: 12.5px }

.ce-bar {
  display: flex; flex-wrap: wrap; gap: 14px 18px; align-items: center; justify-content: space-between;
  padding: 14px 16px; margin-bottom: 18px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
}
.ce-bar-actions { display: flex; flex-wrap: wrap; gap: 8px }
/* Le bouton pendant l'écriture : il garde sa place (pas de saut de mise en
   page) et montre qu'il travaille. */
.btn.busy { opacity: .75; cursor: progress }
.ce-switch { display: flex; align-items: flex-start; gap: 11px; cursor: pointer; min-width: 0 }
.ce-switch input { margin-top: 2px; flex: none; width: 20px; height: 20px; accent-color: var(--rose) }
.ce-switch-txt { display: grid; gap: 2px; min-width: 0 }
.ce-switch-txt small { color: var(--faint); font-size: 12.5px; max-width: 46ch }

.ce-sec { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px }
.ce-sec h2 { font-size: 15px; margin-bottom: 14px }
.ce-sec > * + * { margin-top: 14px }
.ce-field { display: grid; gap: 6px }
.ce-label { font-size: 13px; color: var(--dim) }
/* `display: block` et pas seulement une couleur : deux indications qui se
   suivent se collaient l'une à l'autre au milieu d'une phrase. */
.ce-hint { display: block; color: var(--faint); font-size: 12.5px }
.ce-app input[type="text"], .ce-app input[type="url"], .ce-app textarea, .ce-app select {
  width: 100%; padding: 9px 11px; border-radius: 10px;
  background: var(--card2); border: 1px solid var(--line2); color: var(--tx); font: inherit;
}
.ce-app textarea { resize: vertical; min-height: 88px; line-height: 1.55 }
.ce-app input[type="file"] { display: none }

/* Mises en page : une vignette par gabarit, choisie au clavier comme à la souris. */
.ce-layouts { display: grid; grid-template-columns: repeat(auto-fit, minmax(188px, 1fr)); gap: 10px }
.ce-layout { display: flex; gap: 10px; align-items: flex-start; padding: 11px 12px; cursor: pointer;
  border: 1px solid var(--line2); border-radius: 12px; background: var(--card2) }
.ce-layout:has(input:checked) { border-color: var(--rose); background: var(--rose-dim) }
.ce-layout input { margin-top: 2px; accent-color: var(--rose) }
.ce-layout b { display: block; font-size: 14px }
.ce-layout small { color: var(--faint); font-size: 12px; line-height: 1.4 }

.ce-colors-wrap { display: grid; gap: 12px; justify-items: start }
.ce-colors { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; width: 100% }
/* Deux couleurs seulement (le QR) : elles n'ont pas à s'étaler sur cinq colonnes. */
.ce-colors.pair { grid-template-columns: repeat(2, minmax(0, 140px)) }
@media (max-width: 520px) { .ce-colors { grid-template-columns: repeat(3, minmax(0, 1fr)) } }
.ce-color { display: grid; justify-items: center; gap: 6px; font-size: 12.5px; color: var(--dim) }
.ce-color input[type="color"] {
  width: 100%; height: 38px; padding: 0; cursor: pointer;
  border: 1px solid var(--line2); border-radius: 10px; background: none;
}
.ce-effects { display: grid; gap: 12px }
.ce-range { display: grid; grid-template-columns: minmax(0, 1fr) 48px; gap: 10px; align-items: center }
.ce-range input[type="range"] { width: 100%; accent-color: var(--rose) }
.ce-range-val { font-family: var(--mono); font-size: 12.5px; color: var(--faint); text-align: right }

.ce-images { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px }
@media (max-width: 520px) { .ce-images { grid-template-columns: repeat(2, minmax(0, 1fr)) } }
.ce-image { display: grid; gap: 8px; justify-items: stretch; min-width: 0 }
.ce-image b { font-size: 13px; color: var(--dim); font-weight: 500 }
.ce-thumb { width: 100%; aspect-ratio: 16 / 9; border-radius: 10px; overflow: hidden;
  border: 1px solid var(--line2); background: var(--card2) }
.ce-thumb img { width: 100%; height: 100%; object-fit: cover; display: block }
.ce-thumb.empty { background:
  repeating-linear-gradient(45deg, rgba(255,255,255,.04) 0 8px, transparent 8px 16px), var(--card2) }
.ce-image-actions { display: flex; flex-wrap: wrap; gap: 6px }
.ce-image-actions .btn { flex: 1 1 auto; text-align: center; white-space: nowrap }
/* Les boutons de sauvegarde gardent leur taille : ce ne sont pas des barres. */
.ce-actions { display: flex; flex-wrap: wrap; gap: 8px }

.ce-blocks { list-style: none; display: grid; gap: 8px; margin: 0; padding: 0 }
.ce-block { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; gap: 10px; align-items: center;
  padding: 9px 12px; border: 1px solid var(--line2); border-radius: 10px; background: var(--card2) }
.ce-block input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--rose) }
/* L'étiquette occupe TOUTE la hauteur de la ligne : au doigt, on vise la
   ligne entière, pas la case de 24 px. C'est elle qui bascule la case. */
.ce-block-name { font-size: 14px; cursor: pointer; align-self: stretch;
  display: flex; align-items: center; min-height: 40px }
.ce-move {
  width: 36px; height: 36px; border-radius: 8px; cursor: pointer; font-size: 14px;
  background: var(--card); border: 1px solid var(--line2); color: var(--tx);
}
@media (max-width: 560px) { .ce-move { width: 44px; height: 44px } }
.ce-move:disabled { opacity: .35; cursor: default }
.ce-move:hover:not(:disabled) { border-color: var(--rose); color: var(--rose) }

.ce-sections { display: grid; gap: 14px }
.ce-sec { display: grid; gap: 8px; padding: 14px; border: 1px solid var(--line2); border-radius: 12px;
  background: var(--card2); justify-items: start }
.ce-sec input, .ce-sec textarea { width: 100% }
.ce-featured { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--dim);
  cursor: pointer; white-space: nowrap }
.ce-featured input { width: 18px; height: 18px; accent-color: var(--rose) }

.ce-links { display: grid; gap: 8px }
/* La ligne d'un lien porte désormais six contrôles : elle se donne deux
   rangées plutôt que de comprimer chaque champ. */
.ce-link { row-gap: 6px }
.ce-link input[type="text"]:nth-of-type(2) { grid-column: 1 / 4 }
.ce-link { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) auto auto auto auto; gap: 8px; align-items: center }
.ce-link .ce-bad { grid-column: 1 / -1; color: var(--warn) }
.ce-app input[aria-invalid="true"] { border-color: var(--warn) }
@media (max-width: 640px) {
  .ce-link { grid-template-columns: minmax(0, 1fr) auto auto auto }
  .ce-link input[type="url"] { grid-column: 1 / -1 }
  .ce-link select { grid-column: 1 }
}

.ce-checks { display: flex; flex-wrap: wrap; gap: 8px 14px }
.ce-checks.column { flex-direction: column; gap: 10px }
.ce-check { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; font-size: 14px }
.ce-check input { width: 20px; height: 20px; accent-color: var(--rose); flex: none; margin-top: 1px }
.ce-check.wide { align-items: flex-start }
.ce-check small { display: block; color: var(--faint); font-size: 12.5px; line-height: 1.4; margin-top: 2px }

/* ── Fac-similé d'un message Discord ───────────────────────────────────────
 * Les couleurs sont celles de Discord en thème sombre, pas celles du site :
 * l'imitation n'a d'intérêt que si elle ressemble à l'endroit où le tag
 * s'affichera. Elle ne prétend pas être une capture (le texte à côté le dit).
 */
.ce-tagpreview { --dc-bg: #313338; --dc-card: #2B2D31; --dc-tx: #DBDEE1; --dc-dim: #949BA4 }
.dc-msg { background: var(--dc-bg); border-radius: 10px; padding: 12px 14px; color: var(--dc-tx); font-size: 14px; line-height: 1.5 }
.dc-msg.card {
  background: var(--dc-card); border-left: 4px solid var(--dc-accent, var(--rose));
  border-radius: 4px 8px 8px 4px; padding: 14px 16px; display: grid; gap: 10px;
}
.dc-plain { color: var(--dc-tx) }
.dc-head { display: flex; align-items: center; gap: 10px; min-width: 0 }
.dc-av { width: 36px; height: 36px; border-radius: 50%; flex: none }
.dc-title { display: flex; align-items: center; gap: 4px; min-width: 0; font-size: 14px }
.dc-name { color: var(--dc-dim) }
.dc-badge { width: 16px; height: 16px }
.dc-body { color: var(--dc-tx) }
.dc-image { height: 92px; border-radius: 6px; background:
  repeating-linear-gradient(45deg, rgba(255,255,255,.06) 0 10px, rgba(255,255,255,.02) 10px 20px) }
.dc-footer { color: var(--dc-dim); font-size: 12px }
.dc-buttons { display: flex; flex-wrap: wrap; gap: 8px }
.dc-btn { background: #4E5058; color: #fff; border-radius: 4px; padding: 6px 12px; font-size: 13.5px; font-weight: 500 }
.dc-btn.link { background: #4E5058; position: relative; padding-right: 26px }
.dc-btn.link::after { content: "↗"; position: absolute; right: 9px; opacity: .8 }

.ce-qrshot { display: grid; gap: 8px; justify-items: start }
.ce-qrshot img { width: 150px; height: 150px; border-radius: 10px; border: 1px solid var(--line2); background: #fff }

/* Le cadre de la photo : chaque choix se montre, il ne se lit pas. */
.ce-rings { display: flex; flex-wrap: wrap; gap: 10px }
.ce-ring { display: flex; align-items: center; gap: 8px; padding: 8px 12px 8px 9px; cursor: pointer;
  font-size: 13.5px; border: 1px solid var(--line2); border-radius: 10px; background: var(--card2) }
.ce-ring input { accent-color: var(--rose) }
.ce-ring i { width: 26px; height: 26px; flex: none; border-radius: 50%; background: var(--card);
  border: 2px solid var(--rose) }
.ce-ring.halo i { border-color: transparent; box-shadow: 0 0 0 2px var(--rose), 0 0 10px 2px var(--rose) }
.ce-ring.carre i { border-radius: 7px }
.ce-ring.aucun i { border-color: transparent; border-width: 0; background: var(--line2) }
.ce-ring:has(input:checked) { border-color: var(--rose); background: var(--rose-dim) }

.ce-gwstyles { display: flex; flex-wrap: wrap; gap: 8px }
.ce-gwstyle { display: flex; align-items: center; gap: 7px; padding: 8px 12px; cursor: pointer; font-size: 13.5px;
  border: 1px solid var(--line2); border-radius: 10px; background: var(--card2) }
.ce-gwstyle input { accent-color: var(--rose) }
.ce-gwstyle::before { content: ""; width: 12px; height: 12px; border-radius: 3px }
.ce-gwstyle.primary::before   { background: #5865F2 }
.ce-gwstyle.secondary::before { background: #4E5058 }
.ce-gwstyle.success::before   { background: #248046 }
.ce-gwstyle.danger::before    { background: #DA373C }
.ce-gwstyle:has(input:checked) { border-color: var(--rose); background: var(--rose-dim) }

.dc-h2 { font-size: 16px; font-weight: 700; color: #fff }
.dc-dim { color: var(--dc-dim); font-size: 13.5px }
.dc-btn.gw-primary   { background: #5865F2 }
.dc-btn.gw-secondary { background: #4E5058 }
.dc-btn.gw-success   { background: #248046 }
.dc-btn.gw-danger    { background: #DA373C }
.dc-footer { white-space: pre-line }

.ce-locked { background: var(--card); border: 1px solid var(--line); border-radius: var(--radius); padding: 26px }
.ce-locked h2 { font-size: 17px; margin-bottom: 8px }
.ce-locked p { color: var(--dim); max-width: 56ch }

.ce-previewbox { position: sticky; top: 84px; display: grid; gap: 10px; min-width: 0 }
.ce-previewhead { display: grid; gap: 2px }
.ce-previewhead small { color: var(--faint); font-size: 12.5px }
.ce-preview { max-height: 76vh; overflow: auto; border-radius: var(--radius); border: 1px solid var(--line) }
/* Sur téléphone l'aperçu reste BORNÉ : sans plafond, la page atteignait dix
   mille pixels et l'aperçu poussait les réglages hors de portée. Il défile
   dans son propre cadre, comme sur grand écran. */
@media (max-width: 1000px) { .ce-previewbox { position: static } .ce-preview { max-height: 60vh } }
@media (max-width: 560px) {
  /* Zone tactile confortable : la ligne entière est cliquable, pas la case. */
  .ce-check, .ce-switch, .ce-block { min-height: 44px }
  /* Les cases elles-mêmes : 24 px au doigt, pas 20. */
  .ce-check input, .ce-switch input, .ce-block input[type="checkbox"],
  .ce-layout input, .ce-gwstyle input { width: 24px; height: 24px }
  .ce-tab { min-height: 40px }
}

/* Le cadre d'aperçu : mêmes styles que la page, calques enfermés. */
.pp-preview { position: relative; z-index: 0; min-height: 380px; background: var(--pp-bg, var(--bg)); color: var(--pp-text, var(--tx)) }
.pp-preview .pp-sky, .pp-preview .pp-veil { position: absolute }
.pp-preview .pp-app { position: relative; z-index: 1; padding: 18px }
.pp-preview .pp-id { margin-top: 0 }

.pp-foot { margin: 26px 0 64px; color: var(--faint); font-size: 13.5px }
/* Un lien de quinze pixels de haut ne se vise pas au doigt : la zone tactile
   passe à 44 px sans changer la mise en page (le remplissage est vertical). */
.pp-foot a, .pw-join a { display: inline-block; padding: 12px 4px }

/* Aperçu façon message Discord : ses couleurs à lui, pas celles du site. */
.dmsg {
  padding: 14px 16px; border: 1px solid var(--line); border-radius: 12px; background: #1E1F22;
  color: #DBDEE1; font-size: 14.5px; line-height: 1.45; overflow-wrap: anywhere;
}
.dmsg-body p { margin: 0; min-height: 1.45em; white-space: pre-wrap }
.dmsg-body h3, .dmsg-body h4, .dmsg-body h5 { margin: 6px 0 2px; color: #F2F3F5; white-space: pre-wrap }
.dmsg-body h3 { font-size: 21px }
.dmsg-body h4 { font-size: 18px }
.dmsg-body h5 { font-size: 15.5px }
.dmsg-body .dsub { font-size: 12px; color: #949BA4 }
.dmsg-body .dquote { padding-left: 10px; border-left: 4px solid #4E5058 }
.dmsg-body code { padding: 1px 4px; border-radius: 4px; background: #2B2D31; font-family: var(--mono); font-size: .88em }
.dmsg-body .dcode {
  margin: 4px 0; padding: 8px 10px; border-radius: 6px; background: #2B2D31;
  font-family: var(--mono); font-size: 13px; white-space: pre-wrap;
}
.dmsg-body mark.dfield { padding: 0 4px; border-radius: 4px; background: rgba(255,158,189,.18); color: var(--rose) }
.dmsg-empty { margin: 0; color: #949BA4; font-style: italic }

/* L'éditeur de tags */
.tg-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 16px }
.tg-bar .search { flex: 1 1 220px; min-width: 0 }
.tg-count { margin-right: auto; color: var(--faint); font-size: 13.5px }
.tg-grid { display: grid; grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); gap: 18px; align-items: start; margin-bottom: 64px }
.tg-list { display: grid; gap: 4px; max-height: 72vh; overflow: auto; padding-right: 4px }
.tg-item {
  display: grid; gap: 2px; min-width: 0; padding: 9px 12px; text-align: left; font: inherit; cursor: pointer;
  border: 1px solid transparent; border-radius: 10px; background: transparent; color: var(--dim);
}
.tg-item:hover { background: var(--card); color: var(--tx) }
.tg-item[aria-current="true"] { background: var(--rose-dim); border-color: rgba(255,158,189,.35); color: var(--tx) }
.tg-item b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; color: inherit }
.tg-item small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--faint); font-size: 12px }
.tg-pub { color: var(--ok); font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase }
.tg-edit textarea { min-height: 220px; font-family: var(--mono); font-size: 13.5px }
.tg-counter { margin: 0; color: var(--faint); font-size: 12.5px; text-align: right }
.tg-counter.over { color: var(--bad) }
.tg-note { margin: 0 0 14px; color: var(--faint); font-size: 12.5px }
.tg-actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 8px 0 22px }
.tg-preview h4 { margin: 0 0 8px; color: var(--faint); font-size: 12px; letter-spacing: .14em; text-transform: uppercase }
/* 100 % et pas 100vw : même raison que dialog.ask (la barre de défilement). */
dialog.tg-dlg { width: min(720px, calc(100% - 40px)); max-width: none }
dialog.tg-dlg h3 { margin: 0 0 6px; font-size: 18px }
.tg-import { width: 100%; min-height: 240px; font-family: var(--mono); font-size: 13px }
.tg-check { display: flex; align-items: center; gap: 9px; margin: 12px 0; color: var(--dim); font-size: 14px; cursor: pointer }
.tg-report { margin-bottom: 14px; color: var(--dim); font-size: 13.5px }
.tg-report ul { margin: 6px 0 0; padding-left: 18px; color: var(--faint) }

@media (max-width: 760px) {
  .tg-grid { grid-template-columns: minmax(0, 1fr) }
  .tg-list { max-height: 34vh }
}
