/* =========================================================
   ZouBisouBiz OÜ — feuille de style partagée
   Direction : impression RISO.
   Clair  = papierOffset des années 70, agence de com.
   Sombre = « disco », encre profonde + halos tournants.
   Les seules couleurs vivent dans :root / [data-theme="dark"].
   ========================================================= */

/* ─── tokens ─────────────────────────────────────────────── */
:root {
  --bg:      #fcf6ea;  /* papier offset */
  --surface: #fffdf7;  /* plaque claire */
  --surface-2: #f4ebd9;
  --fg:      #151514;  /* encre */
  --muted:   #6a6058;  /* encre diluée */
  --border:  #ddd0b8;
  --accent:  #b11821;  /* rouge RISO, mesuré sur le logo */
  --on-accent: #fffaf2;
  --accent-ink: #ffffff; /* texte sur aplat rouge */
  --accent-hover: #98151c; /* survol : plus sombre, contraste du texte augmente */

  --accent-soft: color-mix(in oklch, var(--accent) 12%, transparent);
  --fg-soft:     color-mix(in oklch, var(--fg) 7%, transparent);

  /* halos du mode disco — jamais utilisés pour du texte */
  --glow-a: #ff2e6a;
  --glow-b: #ffc233;
  --glow-c: #22e0d2;

  /* typo */
  --font-display: 'Archivo Black', 'Archivo', 'Helvetica Neue', Helvetica, sans-serif;
  --font-body:    'Archivo', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-mono:    'Space Mono', ui-monospace, 'JetBrains Mono', Menlo, monospace;

  --fs-h1: clamp(2.4rem, 5vw, 4.25rem);
  --fs-h2: clamp(1.9rem, 3.6vw, 3rem);
  --fs-h3: 1.3rem;
  --fs-lead: 1.15rem;
  --fs-body: 1.0125rem;
  --fs-meta: 0.82rem;

  --gap-xs: 8px;
  --gap-sm: 12px;
  --gap-md: 20px;
  --gap-lg: 32px;
  --gap-xl: 56px;
  --gap-2xl: clamp(56px, 8vw, 104px);
  --container: 1140px;
  --gutter: clamp(20px, 5vw, 40px);

  --radius: 12px;
  --radius-lg: 22px;

  --grain-opacity: 0.5;
  --shadow-1: 4px 4px 0 var(--fg);
  --shadow-soft: 0 18px 40px -28px color-mix(in oklch, var(--fg) 70%, transparent);
}

[data-theme="dark"] {
  --bg:      #14101a;
  --surface: #1d1725;
  --surface-2: #251d2f;
  --fg:      #f7efe0;
  --muted:   #b3a6bb;
  --border:  #372b45;
  --accent:  #ff4d61;
  --on-accent: #1a0d12;
  --accent-ink: #160a0f;
  --accent-hover: #ff7285; /* survol : plus clair, le texte sombre contraste plus */

  --accent-soft: color-mix(in oklch, var(--accent) 22%, transparent);
  --fg-soft:     color-mix(in oklch, var(--fg) 10%, transparent);

  --grain-opacity: 0.28;
  --shadow-1: 4px 4px 0 var(--accent);
  --shadow-soft: 0 18px 44px -26px color-mix(in oklch, #000 90%, transparent);
}

/* ─── base ───────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.6;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
  transition: background-color 0.4s ease, color 0.4s ease;
}
img, svg { display: block; max-width: 100%; }
a { color: inherit; }
button { font: inherit; cursor: pointer; }
p { text-wrap: pretty; }
/* 'balance' repartit equitablement les lignes, mais sur un titre long il
   peut le couper en 4 lignes courtes. On le reserve a h1 et on laisse
   h2/h3/(.lead) rejeter la ligne orpheline via 'pretty'. */
h1 { text-wrap: balance; }
h2, h3, h4 { text-wrap: pretty; }
::selection { background: var(--accent); color: var(--on-accent); }

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}
[data-theme="dark"] :focus-visible { outline-color: var(--glow-c); }

.skip-link {
  position: absolute; left: 12px; top: -100px;
  z-index: 100;
  background: var(--accent); color: var(--on-accent);
  padding: 12px 18px; border-radius: 8px;
  font-weight: 700; text-decoration: none;
  transition: top 0.2s ease;
}
.skip-link:focus { top: 12px; }

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

/* ─── fond : grain papier + halos disco réactifs ─────────── */
.bg-layer {
  position: fixed; inset: 0;
  pointer-events: none;
  z-index: 0;
}
.bg-grain {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  opacity: var(--grain-opacity);
  mix-blend-mode: multiply;
}
[data-theme="dark"] .bg-grain { mix-blend-mode: screen; opacity: calc(var(--grain-opacity) * 0.55); }

.bg-halftone {
  background-image: radial-gradient(currentColor 1.1px, transparent 1.2px);
  background-size: 9px 9px;
  color: var(--fg);
  opacity: 0.09;
  mask-image: radial-gradient(ellipse 80% 65% at 50% 0%, #000 20%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse 80% 65% at 50% 0%, #000 20%, transparent 78%);
}
[data-theme="dark"] .bg-halftone { opacity: 0.14; }

.bg-glows { position: fixed; inset: 0; pointer-events: none; z-index: 0; overflow: hidden; }
/* Le canvas du Game of Life est une boite a la taille de la fenetre,
   posee avant les halos : le motif est au fond, les lampes au-dessus
   restent des nappes de lumiere. opacity 0 = rien a peindre en mode
   clair, ou quand l'utilisateur demande moins de mouvement. */
.bg-life {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0;
  transition: opacity 0.6s ease;
}
[data-theme="dark"] .bg-life { opacity: 0.3; }
/* Halos en degrade radial et non en <div> flou : pas de filter: blur(),
   donc pas de dependance au moteur de rendu, et un cout bien moindre.
   Chaque halo est un disque qui suit la souris et le scroll. */
.bg-glow {
  position: absolute;
  width: 72vw; height: 72vw;
  opacity: 0;
  will-change: transform, opacity;
  pointer-events: none;
  /* La puissance est reecrite par life.js a chaque generation ; la
     transition transforme ces paliers en une rampe de lumiere
     continue plutot qu'en une suite de sauts. */
  transition: opacity 0.45s linear;
}
[data-theme="dark"] .bg-glow { opacity: 0.55; }
.bg-glow-1 {
  top: -30vw; left: -22vw;
  background: radial-gradient(circle closest-side, var(--glow-a) 0%, color-mix(in oklch, var(--glow-a) 40%, transparent) 38%, transparent 70%);
}
.bg-glow-2 {
  top: -24vw; left: 26vw;
  background: radial-gradient(circle closest-side, var(--glow-b) 0%, color-mix(in oklch, var(--glow-b) 38%, transparent) 38%, transparent 70%);
}
.bg-glow-3 {
  top: 64vw; left: -16vw;
  background: radial-gradient(circle closest-side, var(--glow-c) 0%, color-mix(in oklch, var(--glow-c) 40%, transparent) 38%, transparent 70%);
}

.page { position: relative; z-index: 1; }

/* ─── primitives ─────────────────────────────────────────── */
.container { max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
.section { padding-block: clamp(48px, 7vw, 88px); position: relative; }
/* La barre de navigation est collante : les ancres doivent
   s'arreter en dessous, sinon le titre passe dessous le header. */
.section[id] { scroll-margin-top: 96px; }
.section + .section { border-top: 1px solid var(--border); }
.section-head { max-width: 46ch; margin-bottom: var(--gap-xl); }
/* Titres de section : on autorise une largeur plus large pour eviter
   qu'un dernier mot se retrouve seul sur sa ligne. */
.section-head h2 { max-width: 22ch; }
/* Titre de section un peu plus discret que --fs-h2. Le dernier mot est
   colle au precedent par une espace insecable dans le HTML : la ligne
   orpheline disparait sans réduire la taille du texte. */
.nowrap-lg { max-width: none; font-size: clamp(1.75rem, 3vw, 2.55rem); }
/* Titre d'une page secondaire. Les titres traduits sont plus courts ou
   plus longs que le francais : la largeur est libre et l'equilibrage
   est laisse a 'pretty' pour eviter un decoupe en quatre lignes. */
.page-h1 { max-width: 18ch; text-wrap: pretty; }
.form-h2 {
  font-size: 1.35rem; text-transform: none;
  letter-spacing: -0.015em; margin: 0 0 8px;
  font-family: var(--font-body); font-weight: 700;
}
.stack { display: flex; flex-direction: column; }
.stack > * + * { margin-top: var(--gap-md); }
.row { display: flex; align-items: center; gap: var(--gap-md); }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--gap-md); flex-wrap: wrap; }
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gap-lg); }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gap-lg); }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap-md); }
.grid-2-1 { display: grid; grid-template-columns: 2fr 1fr; gap: var(--gap-xl); align-items: start; }
.rule { border: 0; border-top: 1px solid var(--border); margin: 0; }
.rule-strong { border: 0; border-top: 2px solid var(--fg); margin: 0; }

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

/* ─── typo ───────────────────────────────────────────────── */
h1, .h1 {
  font-family: var(--font-display);
  font-size: var(--fs-h1);
  line-height: 0.98;
  letter-spacing: -0.03em;
  margin: 0;
  text-transform: uppercase;
}
h2, .h2 {
  font-family: var(--font-display);
  font-size: var(--fs-h2);
  line-height: 1.02;
  letter-spacing: -0.025em;
  margin: 0;
  text-transform: uppercase;
}
h3, .h3 { font-size: var(--fs-h3); font-weight: 700; line-height: 1.25; margin: 0; letter-spacing: -0.01em; }
.lead { font-size: var(--fs-lead); line-height: 1.55; color: var(--muted); max-width: 60ch; margin: 0; }
.eyebrow {
  font-family: var(--font-mono);
  font-size: 0.76rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 var(--gap-md);
  display: inline-flex; align-items: center; gap: 8px;
}
.eyebrow::before {
  content: ''; width: 22px; height: 3px;
  background: var(--accent);
  display: inline-block;
}
.meta { font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--muted); letter-spacing: 0.04em; }
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

/* ─── liens animés ───────────────────────────────────────── */
.link-u {
  position: relative;
  text-decoration: none;
  font-weight: 600;
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 2px;
  transition: background-size 0.28s cubic-bezier(0.2, 0.7, 0.3, 1), color 0.2s ease;
  padding-bottom: 1px;
}
.link-u:hover, .link-u:focus-visible { background-size: 100% 2px; }
.link-u:hover { color: var(--accent); }

/* ─── boutons ────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  min-height: 48px;
  padding: 12px 24px;
  border-radius: var(--radius);
  border: 2px solid transparent;
  font-family: var(--font-body);
  font-size: 0.98rem;
  font-weight: 700;
  letter-spacing: -0.005em;
  text-decoration: none;
  transition: transform 0.08s ease, background-color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}
.btn:active { transform: translateY(2px); }
.btn-primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent); box-shadow: var(--shadow-1); }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--on-accent); }
.btn-primary:focus-visible { background: var(--accent-hover); color: var(--on-accent); }
.btn-secondary { background: transparent; color: var(--fg); border-color: var(--fg); }
.btn-secondary:hover { background: var(--fg); color: var(--bg); }
.btn-secondary:focus-visible { background: var(--fg); color: var(--bg); }
.btn-ghost { background: transparent; color: var(--fg); border-color: transparent; padding-inline: 10px; }
.btn-ghost:hover { color: var(--accent); }
.btn[disabled] { opacity: 0.45; cursor: not-allowed; box-shadow: none; transform: none; }
.btn-arrow::after { content: '\2192'; transition: transform 0.18s ease; }
.btn-arrow:hover::after { transform: translateX(4px); }

/* ─── cartes ─────────────────────────────────────────────── */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: clamp(20px, 3vw, 30px);
  transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.card-lift:hover { transform: translateY(-4px); box-shadow: var(--shadow-soft); border-color: var(--accent); }
.card-text { color: var(--muted); margin: 0; }
/* Le grain de papier ne pardonne pas une derniere ligne d'un seul mot :
   .card-text et les paragraphes des .log-row rejettent la ligne veuve
   plutot que de la laisser tomber telle quelle. */
.card-text, .log-row p { text-wrap: pretty; }
.card-flat { background: transparent; border: 0; padding: 0; }
.card-flat + .card-flat { margin-top: var(--gap-lg); }
/* Listes de regles et d'adresses : le tiret est rendu par la liste
   elle-meme, pas grave dans le texte — une regle i18n ne peut pas
   porter un prefixe traduit. */
.rules, .plain-list {
  list-style: none; padding: 0; margin: 0;
  display: grid; gap: 12px;
  color: var(--muted); font-size: 0.95rem;
}
.rules li { padding-left: 18px; position: relative; }
.rules li::before {
  content: ''; position: absolute; left: 0; top: 0.66em;
  width: 9px; height: 2px; background: var(--accent);
}
.plain-list li { display: block; }
.card-num {
  font-family: var(--font-display);
  font-size: 2.4rem;
  line-height: 1;
  color: var(--accent);
  margin-bottom: var(--gap-sm);
}

.feature .feature-mark {
  width: 46px; height: 46px;
  display: grid; place-items: center;
  border: 2px solid var(--fg);
  border-radius: 12px;
  background: var(--surface);
  color: var(--accent);
  margin-bottom: var(--gap-md);
  transition: transform 0.25s ease, background-color 0.2s ease, color 0.2s ease;
}
.feature:hover .feature-mark { transform: rotate(-6deg) scale(1.05); background: var(--accent-hover); border-color: var(--accent-hover); color: var(--on-accent); }
.feature .feature-mark svg { width: 22px; height: 22px; }
.feature h3 { margin-bottom: 8px; }
.feature p { margin: 0; color: var(--muted); font-size: 0.97rem; }

/* ─── étiquettes ─────────────────────────────────────────── */
.pill {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px;
  background: var(--accent-soft);
  color: var(--accent);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 700;
}
.tag {
  display: inline-flex; align-items: center;
  padding: 5px 12px;
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.84rem;
  font-weight: 600;
}
.tag-list { display: flex; flex-wrap: wrap; gap: 8px; }

/* ─── logo ───────────────────────────────────────────────── */
.brandplate {
  position: relative;
  display: inline-grid; place-items: center;
  padding: var(--plate-pad-y) var(--plate-pad-x);
  /* Les deux memes valeurs pilotent le padding de la plaque ET
     l'ancrage de la couche vectorielle, qui doit occuper la boite de
     l'<img> et non toute la plaque. Une seule source, donc pas de
     decalage possible entre le repli et le rendu anime. */
  --plate-pad-x: 14px;
  --plate-pad-y: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: var(--shadow-soft);
  isolation: isolate;
}
.brandplate img { width: auto; height: 44px; filter: saturate(1.05); }
/* Le blason garde son ratio de trace (1042x730, avec la marge de 32
   ajoutee par le vectoriseur). Le --lg est plus grand mais reste pose
   sur la meme plaque que le --lg d'avant, en plus carre : un blason
   n'a pas besoin de 320px de large pour se lire. */
.brandplate--lg { padding: 26px 30px; border-radius: 20px;
                  --plate-pad-x: 30px; --plate-pad-y: 26px; }
.brandplate--lg img { height: auto; width: min(240px, 62vw); }
/* La plaque peut etre un <span> (dans la page) ou un <figure> (dans le
   brand kit, ou elle porte une legende). Un <figure> a une marge par
   defaut : sans cette remise a zero elle deborderait de son conteneur. */
.brandplate { margin: 0; }
.brandplate--lg figcaption {
  margin-top: 10px;
  font: var(--fs-meta)/1.4 var(--font-mono);
  color: var(--muted);
  text-align: center;
  max-width: 24ch;
}
/* effet de mis-registration RISO au survol */
.brandplate::before, .brandplate::after {
  /* Meme ancrage que la couche vive : le faux calage doit epouser la
     boite de l'image, pas deborder sur le padding de la plaque. */
  content: ''; position: absolute;
  top: var(--plate-pad-y); right: var(--plate-pad-x);
  bottom: var(--plate-pad-y); left: var(--plate-pad-x);
  background-size: contain; background-position: center; background-repeat: no-repeat;
  opacity: 0; pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
/* Les chemins sont declares ICI, pas dans un attribut style du HTML.
   Une url() relative est resolue contre la feuille de style qui la
   consomme, jamais contre le document : un « assets/logo.jpg » ecrit
   dans le HTML devenait « /assets/assets/logo.jpg » et le faux calque
   RISO du logo chargeait un 404 — invisible tant qu'on ne regardait
   pas les requetes du serveur. Depuis assets/, le nom suffit. */
/* Le blason seul est partout : le lockup (embleme + mot-symbole) est
   retire de la page. Une seule image pour les deux plaques, donc une
   seule geometrie a maintenir — le lockup reste dans assets/ comme
   document de marque, plus utilise par le site. */
.brandplate        { --plate-img: url("zoubisoubiz-emblem.svg"); }
.brandplate--lg    { --plate-img: url("zoubisoubiz-emblem.svg"); }
.brandplate::before {
  background-image: var(--plate-img);
  transform: translate(-2px, 1px); mix-blend-mode: multiply;
}
.brandplate::after {
  background-image: var(--plate-img);
  transform: translate(2px, -1px); mix-blend-mode: screen;
}
.brandplate:hover::before, .brandplate:hover::after { opacity: 0.45; }
.brandplate:hover img { animation: riso-pop 0.7s cubic-bezier(0.3, 1.4, 0.4, 1); }
@keyframes riso-pop {
  0% { transform: scale(1) rotate(0); }
  40% { transform: scale(1.07) rotate(-1.5deg); }
  100% { transform: scale(1) rotate(0); }
}

/* ─── logo vectorise : couche animee ────────────────────────
   L'emblème est un vector : la masse rouge et les quatre étincelles
   sont des noeuds distincts, donc animables séparément. La couche
   vive se pose par-dessus le <img>, qui reste en place comme repli
   sans JavaScript et sous prefers-reduced-motion. */
.logo-live {
  /* La couche vive doit occuper EXACTEMENT la boite de l'<img> qu'elle
     remplace, pas la plaque entiere. En positionnant sur inset:0 elle
     s'etendait sur le padding : mesure en navigation, le blason rendu
     faisait 81px de large contre 53px pour le repli, soit 28px de saut
     et un embleme a fleur de plaque. On recale donc sur la meme boite
     que l'image — le padding de la plaque, deduit en CSS pur plutot que
     recopie en dur, pour qu'un changement de padding ne puisse pas
     recreer le decalage. */
  position: absolute;
  top: var(--plate-pad-y); right: var(--plate-pad-x);
  bottom: var(--plate-pad-y); left: var(--plate-pad-x);
  display: grid; place-items: center;
  pointer-events: none;
  --lean-x: 0; --lean-y: 0;
  color: var(--accent);
}
.logo-live svg {
  width: 100%; height: 100%;
  overflow: visible;
}
.logo-live .zbb-mass {
  transform: translate(calc(var(--lean-x) * 1.6px), calc(var(--lean-y) * 1.6px))
             scale(calc(1 + 0.035 * var(--lean-abs, 0)));
  transform-box: fill-box; transform-origin: center;
  transition: transform 0.45s cubic-bezier(0.22, 0.8, 0.3, 1);
}
/* Les etincelles scintillent sur des periodes et phases distinctes :
   un synchronisme se lirait comme un clignement. */
.logo-live .zbb-spark {
  transform-box: fill-box; transform-origin: center;
  animation: zbb-twinkle var(--dur, 3s) var(--delay, 0s) ease-in-out infinite;
  color: var(--accent);
}
.brandplate.is-far-right .zbb-spark:nth-of-type(3),
.brandplate.is-far-left  .zbb-spark:nth-of-type(2) { animation-duration: 1.1s; }
@keyframes zbb-twinkle {
  0%, 100% { transform: scale(0.86) rotate(0deg); opacity: 0.75; }
  50%      { transform: scale(1.14) rotate(22deg); opacity: 1; }
}
/* ─── le lit lumineux : les dix decoupes de l'embleme ────────────────
   L'embleme n'est pas dessine : la masse rouge est un seul chemin
   « evenodd » dont les ouvertures sont des trous. Le fond de page
   transparait donc a travers, et rien d'autre. Cette couche exploite
   exactement ca : un pave dessine DERRIERE la masse, sur la forme des
   dix decoupes, et seul ce pave luit.

   En mode clair le pave est du papier creme : le blason reste un tirage
   RISO plat, comme l'original imprime. En mode disco il devient une
   source lumineuse dont la puissance suit la densite de cellules
   vivantes du moteur de Conway (--life, ecrit par logo.js) : le blason
   et le fond respirent ensemble, au meme systeme et non par hasard.

   LE GAIN EST EXPLICITE, et il est calcule en JavaScript, pas ici. La
   densite reelle d'une grille de Conway en equilibre ne varie pas de 0
   a 1 : mesuree sur ce moteur (60 echantillons / 9 s) elle reste entre
   0.0594 et 0.0709, soit une plage de 0.012. Un mapping « life * 3.4 »
   laissait donc le pave inchange a l'ecle — couple techniquement, mort
   visuellement. logo.js ramene cette plage utile vers 0-1 et ecrit le
   resultat dans --glow-power. Le CSS ne peut pas diviser deux nombres
   sans unites : c'est aussi pour ca que le calcul vit en JS.

   ET LE PAVE, C'EST UN DETAIL QUI FAIT TOUT. La taille reelle des
   decoupes depend de la taille du blason : mesuree, elle tombe a
   7 px dans la barre de navigation (hauteur 44px) contre 26 px sur la
   grande figure du hero. Un pave rose de 7 px a 0.35 d'opacite ne se
   voit pas — il ne peut pas, quelle que soit la couleur. On ne peut
   donc pas appliquer le meme traitement partout : la meme surface
   relative produit une lueur invisible en petit et écrasante en grand.

   La solution n'est pas de grossir le pave — il deborderait de ses
   decoupes, et la masse deviendrait un aplat informe. C'est de faire
   porter l'eclat par la MASSE elle-meme, dont la surface ne change
   pas d'echelle relative, et de n'offerir le pave lumineux qu'aux
   grandes tailles. Concretement : --emblem-w est mesuree par logo.js,
   qui fait aussi le calcul de gain en JavaScript (le CSS ne sait pas
   diviser deux nombres sans unites) ; le pave n'existe qu'au-dela de
   120px de large, et sous ce seuil la masse prend un halo degrade a la
   place. Une seule idee, deux traitements adaptes a la surface
   disponible — pas trois.

   Deux variables, donc deux seuils, tous deux exprimes en pixels et en
   fraction : --life 0..1 (densite remise a l'echelle par logo.js) et
   --emblem-w (largeur reelle en px). Aucun nombre de donnee dans le
   CSS : les deux sont ecrits par le JS, donc le CSS ne peut pas mentir
   sur ce qui alimente la lueur. */
.logo-live { --life: 0; --emblem-w: 0px; }
.logo-live .zbb-bed { fill: var(--surface); }
[data-theme="dark"] .logo-live .zbb-mass {
  color: #d61f2b;
  /* Sous 120px de large, aucune decoupe n'est assez grande pour laisser
     passer une lueur. La masse resout alors la legerte par son propre
     halo : c'est ce qui fait respirer le petit blason de la barre.
     L'intensite vient de --glow-power (0..1), calcule par logo.js. */
  filter: drop-shadow(0 0 6px color-mix(in oklch, var(--glow-a) calc(18% + 34% * var(--glow-power, 0)), transparent));
}
[data-theme="dark"] .logo-live .zbb-hole-glow {
  fill: var(--glow-a);
  opacity: calc(0.34 + 0.60 * var(--glow-power, 0));
  animation: zbb-hole-pulse var(--hdur, 2.4s) var(--hdelay, 0s) ease-in-out infinite;
  transform-box: fill-box; transform-origin: center;
}
/* Le pave n'apparait que la ou il peut etre vu : sous 120px de large
   ses decoupes mesurent 7px et aucune lueur n'y serait perceptible. */
[data-theme="dark"] .logo-live .zbb-bed {
  opacity: clamp(0, calc((var(--emblem-w) - 120px) / 120px), 1);
}
@keyframes zbb-hole-pulse {
  0%, 100% { transform: scale(0.97); }
  50%      { transform: scale(1.05); }
}
/* Le rouge du blason dans le disco : l'accent mesure sur le logo, pas
   la teinte vive du fond, sinon la masse rouge se confond avec les
   etincelles. */
[data-theme="dark"] .logo-live .zbb-mass { color: #d61f2b; }

/* Sous mouvement reduit, la couche reste en place, sans scintillement
   ni respiration : le blason est alors strictement le fichier SVG. */
.brandplate.is-still .logo-live .zbb-spark,
.brandplate.is-still .logo-live .zbb-hole-glow { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .logo-live .zbb-spark { animation: none; }
  [data-theme="dark"] .logo-live .zbb-hole-glow { animation: none; }
  .logo-live .zbb-mass { transform: none; }
  .brandplate:hover img { animation: none; }
}
/* Le blason n'a pas d'encre noire : c'etait la raison du papier creme
   porte par la plaque du lockup. Sans le mot-symbole, la grande plaque
   suit normalement les tokens et s'assombrit en disco — une seule
   geometrie pour les deux plaques, deux themes, aucune plaque a traiter
   a part. */
[data-theme="dark"] .brandplate--lg { box-shadow: 0 18px 40px -28px rgba(0, 0, 0, 0.9); }

/* L'img reste la source de verite pour l'accessibilite et le repli :
   il n'est masque que lorsque la couche vectorielle est montee. */
.brandplate[data-logo-live="1"] > img { opacity: 0; }
.brandplate.is-focused .logo-live { filter: drop-shadow(0 0 6px color-mix(in oklch, var(--accent) 55%, transparent)); }
.brandplate.is-focused { border-color: var(--accent); }

/* ─── chrome : navigation ────────────────────────────────── */
.topnav {
  position: sticky; top: 0; z-index: 40;
  background: color-mix(in oklch, var(--bg) 90%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--border);
}
.topnav-inner { display: flex; align-items: center; justify-content: space-between; gap: var(--gap-md); padding-block: 12px; }
.topnav nav { display: flex; align-items: center; gap: clamp(14px, 2.4vw, 30px); }
.topnav nav a {
  position: relative;
  font-size: 0.95rem; font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  padding-block: 6px;
  transition: color 0.2s ease;
}
.topnav nav a::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  height: 2px; background: var(--accent);
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.25s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.topnav nav a:hover { color: var(--fg); }
.topnav nav a:hover::after, .topnav nav a[aria-current="page"]::after { transform: scaleX(1); }
.topnav nav a[aria-current="page"] { color: var(--fg); }
/* Le CTA de la nav est un <a> : sa couleur de lien ne doit pas
   l'emporter sur celle du bouton plein. */
.topnav nav a.btn-primary { color: var(--on-accent); }
.topnav nav a.btn-primary:hover,
.topnav nav a.btn-primary:focus-visible { color: var(--on-accent); background: var(--accent-hover); }
.topnav nav a.btn-primary::after { display: none; }

.nav-toggle {
  display: none;
  min-width: 48px; min-height: 48px;
  background: transparent; border: 2px solid var(--fg); border-radius: 10px;
  color: var(--fg); font-weight: 700; font-size: 0.85rem;
  align-items: center; justify-content: center;
}
.nav-toggle:hover { background: var(--fg); color: var(--bg); }

.theme-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 8px 14px;
  background: transparent; color: var(--fg);
  border: 2px solid var(--border); border-radius: 999px;
  font-family: var(--font-mono); font-size: 0.74rem;
  letter-spacing: 0.1em; text-transform: uppercase; font-weight: 700;
  transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
}
.theme-toggle:hover { border-color: var(--fg); background: var(--fg-soft); color: var(--fg); }

/* La boule de discotheque : elle rend le mode lisible d'un coup d'oeil
   et donne au bouton un objet, pas un simple libelle. En mode clair
   elle reste une boule d'encre mate, imprimee, coherente avec le papier
   des annees 70 ; en mode disco elle tourne et attrape les trois
   teintes des lampes dans ses facettes. */
.theme-toggle__ball {
  position: relative;
  flex: none;
  width: 17px; height: 17px;
  border-radius: 50%;
  background: var(--fg);
  box-shadow: 0 0 0 2px color-mix(in oklch, var(--fg) 26%, transparent);
  overflow: hidden;
  transition: background-color 0.25s ease, box-shadow 0.25s ease;
}
/* Les facettes : rayons en coin x couronnes en cercle, d'ou la grille
   de petits miroirs. Aucune image, seulement deux degrades. */
.theme-toggle__ball::before {
  content: "";
  position: absolute; inset: 0;
  background:
    repeating-conic-gradient(from 0deg, var(--bg) 0deg 8deg, transparent 8deg 16deg),
    repeating-radial-gradient(circle at 50% 50%, var(--bg) 0 1.4px, transparent 1.4px 3.4px);
  opacity: 0.5;
}
/* L'etalon : un reflet clair. Invisible sur le papier, il clignote en
   mode disco — c'est lui qui donne le « swung ». */
.theme-toggle__ball::after {
  content: "";
  position: absolute;
  top: 1.5px; left: 4.5px;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: #fff;
  opacity: 0;
  transition: opacity 0.25s ease;
}
[data-theme="dark"] .theme-toggle__ball {
  background: conic-gradient(
    from 0deg, var(--glow-a), var(--glow-b), var(--glow-c),
    var(--glow-a), var(--glow-b), var(--glow-c), var(--glow-a)
  );
  box-shadow: 0 0 0 2px color-mix(in oklch, var(--glow-c) 45%, transparent),
              0 0 10px color-mix(in oklch, var(--glow-a) 55%, transparent);
  animation: zbb-ball-spin 7s linear infinite;
}
[data-theme="dark"] .theme-toggle__ball::before { opacity: 0.38; }
[data-theme="dark"] .theme-toggle__ball::after { animation: zbb-ball-twinkle 1.9s ease-in-out infinite; }

@keyframes zbb-ball-spin {
  from { filter: hue-rotate(0deg) rotate(0deg); }
  to   { filter: hue-rotate(0deg) rotate(360deg); }
}
@keyframes zbb-ball-twinkle {
  0%, 100% { opacity: 0.15; }
  50%      { opacity: 0.95; }
}
/* Le survol garde la boule en mouvement, sans lui changer de teinte :
   seule sa taille change, le contraste du texte est intact. */
.theme-toggle:hover .theme-toggle__ball { box-shadow: 0 0 0 3px color-mix(in oklch, var(--fg) 34%, transparent); }
[data-theme="dark"] .theme-toggle:hover .theme-toggle__ball { box-shadow: 0 0 0 3px color-mix(in oklch, var(--glow-c) 55%, transparent), 0 0 14px color-mix(in oklch, var(--glow-a) 65%, transparent); }

/* ─── langue ─────────────────────────────────────────────── */
.nav-tools { display: flex; align-items: center; gap: 10px; }
.langpick { position: relative; display: flex; align-items: center; }
.lang-select {
  min-height: 44px;
  padding: 8px 34px 8px 14px;
  background: transparent;
  color: var(--fg);
  border: 2px solid var(--border);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  appearance: none;
  cursor: pointer;
  transition: border-color 0.2s ease, background-color 0.2s ease;
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: calc(100% - 17px) calc(50% + 2px), calc(100% - 12px) calc(50% + 2px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.lang-select:hover { border-color: var(--fg); background-color: var(--fg-soft); }
.lang-select:focus-visible { border-color: var(--accent); }
.lang-state {
  position: absolute;
  top: -5px; right: -4px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent);
  font-family: var(--font-mono);
  font-size: 0.58rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  pointer-events: none;
}
.lang-state[hidden] { display: none; }
/* Le <select> natif n'affiche pas le HTML des <option> : le badge de
   maturite vit donc sur un element voisin, pose par le moteur i18n. */
.lang-select option { background: var(--surface); color: var(--fg); }

/* Sur mobile, le libelle complet (« FR · Français ») ecrase le logo : on
   ne garde que le code ISO a deux lettres. Le <select> natif ne sait pas
   changer le texte de ses <option> selon la media query, donc le code est
   un <span> qui RECOUVRE le select : ce dernier reste visible pour
   l'assistance technique, focusable, et conserve toutes ses options. */
.lang-code {
  display: none;
  position: relative;
  z-index: 1;
  min-height: 44px;
  padding: 8px 30px 8px 13px;
  border: 2px solid var(--border);
  border-radius: 999px;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-mono);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  line-height: 26px;
  pointer-events: none;
  transition: border-color 0.2s ease, background-color 0.2s ease;
}
.lang-code::after {
  content: '';
  position: absolute; top: 50%; right: 13px;
  width: 7px; height: 7px;
  margin-top: -6px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
}

@media (max-width: 860px) {
  .lang-code { display: block; }
  .lang-select {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    min-height: 44px;
    margin: 0; padding: 0; border: 0;
    opacity: 0;          /* invisible mais toujours au premier plan */
    background: transparent;
    background-image: none;
    font-size: 1rem;     /* taille de lecture mobile, meme si invisible */
    cursor: pointer;
  }
  /* Le focus et le survol se lisent sur la pastille, pas sur le select. */
  .langpick:hover .lang-code { border-color: var(--fg); background: var(--fg-soft); }
  .langpick:has(.lang-select:focus-visible) .lang-code {
    border-color: var(--accent);
    outline: 2px solid var(--accent);
    outline-offset: 2px;
  }
}

@media (max-width: 860px) {
  .nav-toggle { display: inline-flex; }
  .topnav nav {
    position: absolute; left: 0; right: 0; top: 100%;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    padding: 8px var(--gutter) 16px;
    display: none;
  }
  .topnav nav[data-open="true"] { display: flex; }
  .topnav nav a { padding-block: 14px; border-bottom: 1px solid var(--border); font-size: 1.05rem; }
  .topnav nav .nav-cta { margin-top: 14px; }
  .nav-tools { gap: 8px; }
  .theme-toggle { padding: 8px 12px; }
  .lang-select { padding: 8px 30px 8px 11px; font-size: 0.7rem; }
}
/* Sous 430px la barre devient trop large : le theme passe en bouton
   icone et la langue garde son libelle court. */
@media (max-width: 430px) {
  .theme-toggle [data-theme-label] { display: none; }
  .theme-toggle { padding: 8px; min-width: 44px; justify-content: center; }
  .nav-tools { gap: 6px; }
}

/* ─── pied de page ───────────────────────────────────────── */
.pagefoot {
  position: relative; z-index: 1;
  border-top: 1px solid var(--border);
  padding-block: var(--gap-xl);
  background: var(--surface);
  font-size: 0.92rem;
}
.pagefoot .row-between { gap: var(--gap-lg); }
.footer-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: var(--gap-lg); }
@media (max-width: 760px) { .footer-grid { grid-template-columns: 1fr; } }
.footer-grid h3 { font-family: var(--font-mono); font-size: 0.74rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 12px; }
.footer-grid ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.foot-about { margin: 16px 0 0; color: var(--muted); max-width: 34ch; }
.footer-grid a { color: var(--muted); }
.footer-grid a:hover { color: var(--accent); }
.footer-note { margin-top: var(--gap-lg); padding-top: var(--gap-md); border-top: 1px solid var(--border); color: var(--muted); font-size: 0.86rem; }

/* ─── nouvelles pages : tableau de traitement, légende ─────── */
/* La légende d'un tableau doit être lue par un lecteur d'écran sans
   doubler l'information visible : .visually-hidden la retire du flux,
   .sr-only-note la garde dans la lecture mais visible pour qui veut. */
.ds-table caption { text-align: left; margin-bottom: 10px; }
.ds-table caption.visually-hidden { position: absolute; }
/* Les deux tableaux de la page légale sont denses : on garde la ligne
   lisible sur mobile en faisant passer la première colonne au-dessus
   plutôt qu'en défilant horizontalement. */
@media (max-width: 760px) {
  .ds-table--stack thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .ds-table--stack tbody tr { display: block; padding: 14px 0; border-bottom: 1px solid var(--border); }
  .ds-table--stack tbody th,
  .ds-table--stack tbody td { display: block; border: 0; padding: 2px 0; }
  .ds-table--stack tbody th { font-family: var(--font-body); font-size: 1rem; text-transform: none; letter-spacing: 0; color: var(--fg); font-weight: 700; margin-bottom: 4px; }
  .ds-table--stack tbody td.num-col,
  .ds-table--stack tbody td.num { text-align: left; }
  .ds-table--stack tbody tr:hover { background: transparent; }
}

/* ─── hero ───────────────────────────────────────────────── */
.hero { padding-block: clamp(40px, 6vw, 76px) clamp(48px, 7vw, 92px); }
.hero-split { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: clamp(28px, 5vw, 64px); align-items: center; }
.hero-cta { display: flex; gap: var(--gap-sm); flex-wrap: wrap; align-items: center; margin-top: var(--gap-lg); }
.hero-lead { margin-top: var(--gap-md); }
.hero-meta { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-top: var(--gap-lg); }
.hero-meta span { font-family: var(--font-mono); font-size: 0.8rem; color: var(--muted); letter-spacing: 0.05em; }
@media (max-width: 920px) { .hero-split { grid-template-columns: 1fr; } }

.ticker { overflow: hidden; border-block: 1px solid var(--border); padding-block: 12px; background: var(--surface); }
.ticker__track { display: flex; gap: 44px; width: max-content; animation: ticker 38s linear infinite; }
.ticker__track span { font-family: var(--font-mono); font-size: 0.78rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); }
.ticker__track span::after { content: ' \25C6'; color: var(--accent); margin-left: 44px; }
@keyframes ticker { from { transform: translateX(0); } to { transform: translateX(-50%); } }

/* ─── liste éditoriale ───────────────────────────────────── */
.log-row {
  display: grid; grid-template-columns: 132px 1fr 120px;
  gap: var(--gap-lg); padding: 24px 0;
  border-top: 1px solid var(--border); align-items: baseline;
  transition: background-color 0.2s ease;
}
.log-row:hover { background: var(--fg-soft); }
.log-row h3 { font-size: 1.1rem; }
.log-row p { margin: 6px 0 0; color: var(--muted); font-size: 0.95rem; max-width: 62ch; }
.log-row .pull { text-align: right; }
@media (max-width: 760px) {
  .log-row { grid-template-columns: 1fr; gap: 6px; }
  .log-row .pull { text-align: left; }
}

/* ─── tableau ────────────────────────────────────────────── */
.ds-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
.ds-table th, .ds-table td { padding: 14px 12px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; }
.ds-table th { color: var(--muted); font-weight: 700; font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; }
.ds-table tbody tr:hover { background: var(--fg-soft); }
.ds-table .num-col { font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; }
.table-scroll { overflow-x: auto; }

/* ─── formulaire ─────────────────────────────────────────── */
.field { display: flex; flex-direction: column; gap: 7px; }
.field > label, .fieldset > legend { font-family: var(--font-mono); font-size: 0.76rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); font-weight: 700; }
.required { color: var(--accent); }
.input, .textarea, .select {
  width: 100%;
  min-height: 48px;
  padding: 12px 14px;
  border: 2px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
  font: inherit; font-size: 1rem;
  transition: border-color 0.18s ease, background-color 0.18s ease, box-shadow 0.18s ease;
}
.textarea { min-height: 140px; resize: vertical; line-height: 1.55; }
.select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%); background-position: calc(100% - 20px) 50%, calc(100% - 14px) 50%; background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; padding-right: 44px; }
.input:hover, .textarea:hover, .select:hover { border-color: var(--fg); }
.input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.input[aria-invalid="true"], .textarea[aria-invalid="true"], .select[aria-invalid="true"] { border-color: var(--accent); background: var(--accent-soft); }
.field-error { color: var(--accent); font-size: 0.86rem; font-weight: 700; }
.field-error:empty { display: none; }
.field-hint { color: var(--muted); font-size: 0.84rem; }
.checkbox { display: grid; grid-template-columns: 26px 1fr; gap: 12px; align-items: start; }
.checkbox input[type="checkbox"] { width: 26px; height: 26px; margin: 2px 0 0; accent-color: var(--accent); }
.form-status { padding: 16px 18px; border-radius: var(--radius); border: 2px solid var(--border); font-weight: 600; }
.form-status[data-state="ok"] { border-color: var(--accent); background: var(--accent-soft); color: var(--fg); }
.form-status[data-state="error"] { border-color: var(--accent); background: var(--accent-soft); color: var(--fg); }

/* ─── divers ─────────────────────────────────────────────── */
.callout {
  border: 2px solid var(--fg);
  border-left-width: 8px;
  border-radius: var(--radius-lg);
  padding: 24px 26px;
  background: var(--surface);
}
[data-theme="dark"] .callout { border-left-color: var(--glow-b); }
.callout h3 { margin-bottom: 8px; }
.callout p { margin: 0; color: var(--muted); font-size: 0.95rem; }

.roadmap-item {
  display: grid; grid-template-columns: 150px 1fr; gap: var(--gap-lg);
  padding: 28px 0; border-top: 1px solid var(--border);
}
.roadmap-item h3 { margin-bottom: 6px; }
.roadmap-item p { margin: 0; color: var(--muted); max-width: 62ch; }
.roadmap-when { font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--fg); font-weight: 700; }
.roadmap-when span { display: block; color: var(--muted); font-weight: 400; margin-top: 6px; text-transform: none; letter-spacing: 0.02em; }
@media (max-width: 760px) { .roadmap-item { grid-template-columns: 1fr; gap: 10px; } }

.empty-state {
  border: 2px dashed var(--border);
  border-radius: var(--radius-lg);
  padding: clamp(24px, 4vw, 40px);
  background: var(--surface);
}
.empty-state h3 { margin-bottom: 8px; }
.empty-state p { color: var(--muted); margin: 0 0 var(--gap-md); max-width: 58ch; }

.social-item { display: flex; align-items: center; justify-content: space-between; gap: var(--gap-md); padding: 18px 0; border-top: 1px solid var(--border); }
.social-item:first-child { border-top: 0; }
.social-item .handle { font-family: var(--font-mono); font-size: 0.84rem; color: var(--muted); }
.social-mark { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 10px; background: var(--fg); color: var(--bg); font-family: var(--font-mono); font-weight: 700; font-size: 0.9rem; flex: none; }
.social-item:hover .social-mark { background: var(--accent-hover); color: var(--on-accent); }

.sr-only-note { font-size: 0.86rem; color: var(--muted); }

/* ─── canaux de publication (socials.html) ────────────────
   Quatre lignes, une par type de canal. L'ordre est celui du
   projet : image+texte, conversation, notes courtes, audio.    */
.chan {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--gap-lg);
  align-items: start;
  padding: 26px 0;
  border-top: 1px solid var(--border);
}
.chan:first-of-type { border-top: 0; }
.chan__kind {
  font-family: var(--font-mono);
  font-size: 0.76rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--fg);
}
.chan__desc { color: var(--muted); font-size: 0.95rem; margin: 6px 0 0; max-width: 56ch; }
.chan__h { font-size: 1.1rem; margin: 0; }
.chan__links { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

/* Chaque canal est un lien secondaire, jamais un bouton plein :
   un seul CTA principal par ecran, le reste est du texte.      */
.chan__link {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 10px 14px;
  border: 1.5px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
  font-family: var(--font-mono);
  font-size: 0.86rem;
  text-decoration: none;
  white-space: nowrap;
  transition: border-color 0.16s, background-color 0.16s, transform 0.16s;
}
.chan__link:hover { border-color: var(--fg); background: var(--surface-2); transform: translateY(-1px); }
.chan__link:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.chan__link::after { content: '\2197'; font-size: 0.9em; }
.chan__link--todo { color: var(--muted); border-style: dashed; }
.chan__link--todo::after { content: none; }

.chan__todo {
  display: inline-flex; align-items: center; gap: 8px;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.chan__todo::before {
  content: ''; width: 8px; height: 8px; border-radius: 50%;
  background: var(--border); flex: none;
}
.chan--blog {
  margin-top: var(--gap-lg);
  border-top: 1px solid var(--border);
  padding-bottom: 0;
}
.chan__platform {
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 10px 14px;
  font-family: var(--font-mono);
  font-size: 0.76rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1.5px solid var(--border);
  border-radius: 8px;
  white-space: nowrap;
}
@media (max-width: 860px) {
  .chan { grid-template-columns: 1fr; gap: 14px; }
  .chan__links { justify-content: flex-start; }
}

/* ─── diagrammes de pipeline (solutions.html) ──────────────
   Construits par tools/build-diagrams.py. Une seule balise SVG
   par schéma, sans feuille de style Mermaid : toutes les couleurs
   viennent d'ici, donc le meme fichier est juste en mode clair et
   en mode disco. Le SVG est un enfant de .zdia-stage, ce qui
   permet de viser ses blocs (`.zdia .node`) — impossible quand
   le SVG est un <img>. Chaque noeud porte data-znode, donc le
   survol et la liste de details se raccordent par cle, pas par
   geometrie.                                                    */
.zdia-section { scroll-margin-top: 96px; }
.zdia-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: var(--gap-lg); }
.zdia-tab {
  appearance: none; font: inherit; font-family: var(--font-mono);
  font-size: 0.82rem; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 11px 16px; min-height: 44px; cursor: pointer;
  background: var(--surface); color: var(--fg);
  border: 1.5px solid var(--fg); border-radius: 999px;
  box-shadow: none; transition: background 0.16s, color 0.16s, box-shadow 0.16s, transform 0.16s;
}
.zdia-tab:hover { background: var(--surface-2); transform: translateY(-1px); }
.zdia-tab[aria-selected="true"] {
  background: var(--accent); border-color: var(--accent);
  color: var(--on-accent); box-shadow: var(--shadow-1);
}
.zdia-panel { margin-top: var(--gap-lg); }
.zdia-panel[hidden] { display: none; }
.zdia-panel-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 18px; margin-bottom: var(--gap-md); }
.zdia-panel-head h3 { margin: 0; }
.zdia-panel-head .meta { max-width: 62ch; }

.zdia-col { display: grid; gap: var(--gap-md); min-width: 0; }
.zdia-wrap { margin: 0; min-width: 0; }
.zdia-stage {
  /* La scene est centree quand le schema tient dedans, et alignee a
     gauche quand il est plus large — `safe` fait exactement cette
     bascule. Sans lui, un contenu trop large deborderait des deux cotes
     et sa partie gauche deviendrait inatteignable au defilement. */
  display: grid; place-content: safe center; justify-items: center;
  position: relative; background: var(--surface);
  border: 1.5px solid var(--fg); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1); padding: clamp(16px, 2.6vw, 34px);
  /* La scene defile, la page jamais. `min-width:0` est indispensable :
     sans lui la boite s'elargit a son contenu, le schema d'infrastructure
     (1529 unites) deborde de la page et celle-ci acquiert un defilement
     horizontal. */
  min-width: 0; max-width: 100%;
  max-height: min(84vh, 840px); overflow: auto; scrollbar-width: thin;
}
.zdia-stage .zdia { display: block; height: auto; max-width: 100%; width: auto; }
.zdia-stage .zdia text { font-family: var(--font-body); }

/* .zdia-frame a exactement la taille rendu du schema : les pastilles
   d'outil se positionnent en % sur cette boite, donc elles restent
   alignees sur leur bloc que le schema soit en largeur ou en hauteur.
   C'est aussi la seule boite dont la geometrie vaut pour tous les
   schemas, larges ou hauts. */
/* Le schema garde sa taille naturelle. Deux raisons : la taille de
   police du SVG est en unites de diagramme, donc le reduire rend le
   texte illisible (le chemin d'infrastructure tombe a 7 px si on le
   comprime dans la colonne) ; et la boite du schema est la reference
   des pastilles d'outil, qui restent donc alignees sur leur bloc. */
.zdia-frame { position: relative; display: block; width: calc(var(--w, 480) * 1px); }
.zdia-frame .zdia { display: block; width: 100%; height: auto; }

/* Blocs : papier creme, filet encre. Pas d'aplat rouge ailleurs que
   sur les classes `added` — le rouge reste l'accent, pas un aplat. */
.zdia .node rect, .zdia .node circle, .zdia .node ellipse,
.zdia .node polygon, .zdia .node path {
  fill: var(--bg); stroke: var(--fg); stroke-width: 1.6px;
  transition: fill 0.18s, stroke 0.18s;
}
/* text-anchor est indispensable : les <tspan> sont poses en x=0 sur un
   groupe translate() au CENTRE du bloc. Sans cet attribut, le texte part
   vers la droite et deborde du cadre. */
.zdia .node .label, .zdia .node .label text, .zdia .node text,
.zdia .node .text-outer-tspan, .zdia .node .text-inner-tspan {
  fill: var(--fg); color: var(--fg); text-anchor: middle;
}
.zdia .node.added rect, .zdia .node.added polygon, .zdia .node.added circle {
  fill: var(--accent); stroke: var(--accent);
}
.zdia .node.added text, .zdia .node.added .text-inner-tspan { fill: var(--on-accent); color: var(--on-accent); }
.zdia .node.added .label { color: var(--on-accent); }
.zdia .node.added:hover rect, .zdia .node.added:hover polygon { fill: var(--accent-hover); stroke: var(--accent-hover); }

.zdia .cluster rect { fill: none; stroke: var(--border); stroke-width: 1.2px; }
.zdia .cluster-label { fill: var(--muted); color: var(--muted); }
.zdia .cluster-label text, .zdia .cluster-label .text-outer-tspan,
.zdia .cluster-label .text-inner-tspan { fill: var(--muted); color: var(--muted); text-anchor: middle; }
.zdia .edgePaths path, .zdia .flowchart-link {
  stroke: var(--fg); fill: none; stroke-width: 1.6px;
  stroke-linecap: round; stroke-linejoin: round;
}
.zdia .edgePaths .edge-pattern-dashed { stroke: var(--muted); stroke-dasharray: 5 4; }
.zdia .marker { fill: var(--fg); stroke: var(--fg); }
.zdia .edgeLabel, .zdia .edgeLabel rect, .zdia .edgeLabel .labelBkg { fill: var(--surface); stroke: none; }
.zdia .edgeLabel text, .zdia .edgeLabel .text-outer-tspan,
.zdia .edgeLabel .text-inner-tspan { fill: var(--muted); color: var(--muted); text-anchor: middle; }

/* Survol d'un bloc : la pastille d'outil apparait, le bloc se leve.
   Le contraste du texte ne baisse jamais — on change l'aplat. */
.zdia .node:hover rect, .zdia .node:hover polygon, .zdia .node:hover circle, .zdia .node:hover path {
  fill: var(--surface-2); stroke: var(--accent);
}
/* Une seule pastille par schema, posee a l'execution. Voir
   assets/pipelines.js : la position est calculee contre la boite
   reelle du bloc, donc jamais sur son etiquette. */
.zdia-readout {
  position: absolute; z-index: 4; pointer-events: none; white-space: nowrap;
  font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.04em;
  padding: 4px 8px; border-radius: 6px;
  background: var(--fg); color: var(--bg);
  border: 1px solid var(--fg);
  opacity: 0; transition: opacity 0.14s;
}
.zdia-readout[data-on="1"] { opacity: 1; }
.zdia-readout--left, .zdia-readout--right, .zdia-readout--above, .zdia-readout--below { opacity: 0; }
.zdia-readout--left[data-on="1"], .zdia-readout--right[data-on="1"],
.zdia-readout--above[data-on="1"], .zdia-readout--below[data-on="1"] { opacity: 1; }

/* Sur le tactile il n'y a pas de survol : le nom de l'outil est deja
   dans la liste de details, qui est toujours presente au DOM. */

/* ─── details ─────────────────────────────────────────────── */
.zdia-details { border-top: 1.5px solid var(--fg); }
.zdia-details > summary {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 14px 4px; min-height: 44px; cursor: pointer;
  font-family: var(--font-mono); font-size: 0.82rem;
  letter-spacing: 0.1em; text-transform: uppercase;
  list-style: none; transition: color 0.16s;
}
.zdia-details > summary::-webkit-details-marker { display: none; }
.zdia-details > summary:hover { color: var(--accent); }
.zdia-details > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.zdia-details__n {
  display: grid; place-items: center; min-width: 26px; height: 26px; padding: 0 6px;
  border: 1.5px solid var(--fg); border-radius: 999px; font-size: 0.72rem;
  transition: background 0.16s, color 0.16s, border-color 0.16s;
}
.zdia-details[open] > summary .zdia-details__n { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.zdia-nodes { list-style: none; margin: 0 0 4px; padding: 0; display: grid; gap: 2px; }
.zdia-nodes li { padding: 12px 4px; border-top: 1px solid var(--border); transition: background 0.16s; }
.zdia-nodes li:first-child { border-top: 1px solid var(--border); }
.zdia-nodes li:hover { background: var(--surface-2); }
.zdia-nodes__h { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin: 0 0 4px; }
.zdia-nodes__h > span:first-child { font-weight: 600; }
.zdia-nodes__tool { font-family: var(--font-mono); font-size: 0.74rem; color: var(--accent); }
.zdia-nodes__p { margin: 0; color: var(--muted); font-size: 0.94rem; max-width: 78ch; }

/* ─── colonne de droite ────────────────────────────────────── */
.zdia-side { display: grid; gap: var(--gap-md); align-content: start; }
.zdia-side-block h3 {
  margin: 0 0 10px; font-family: var(--font-mono); font-size: 0.8rem;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
}
.zdia-side-block p { margin: 0 0 8px; max-width: 62ch; }
.zdia-side-block .rules { margin-top: 0; }
.zdia-first { font-weight: 600; color: var(--fg); }
/* Le lien vers le source reste un lien texte : l'accent rouge reste
   sur les deux nœuds `added` du schema, qui sont le seul aplat. */
.zdia-side-block--src .link-u { display: inline-block; margin-bottom: 10px; }

/* ─── version mobile : le texte seul ───────────────────────── */
.zdia-mobile { display: none; }


/* ─── mouvement réduit ───────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  .ticker__track { animation: none; }
  .bg-glow { opacity: 0.28; }
  /* Le motif cellulaire reste visible mais fige : on garde une image
     du systeme sans imposer de mouvement a l'utilisateur. */
  [data-theme="dark"] .bg-life { opacity: 0.22; }
}


/* ─── pipeline : reflow ───────────────────────────────────── */
@media (min-width: 900px) {
  .zdia-section .zdia-panel .zdia-col,
  .zdia-section .zdia-panel { display: grid; gap: var(--gap-lg); }
  /* le diagramme occupe la colonne de gauche, les trois blocs celle de droite */
  .zdia-section .zdia-panel:has(.zdia-col) {
    grid-template-columns: minmax(0, 1fr) 300px;
    grid-template-areas: "head head" "col side";
    align-items: start;
  }
  .zdia-section .zdia-panel > .zdia-col, .zdia-section .zdia-panel > .zdia-side { min-width: 0; }
  .zdia-section .zdia-panel > .zdia-panel-head { grid-area: head; }
  .zdia-section .zdia-panel > .zdia-col { grid-area: col; }
  .zdia-section .zdia-panel > .zdia-side { grid-area: side; }
}
@media (max-width: 899px) {
  .zdia-mobile { display: grid; gap: 10px; margin: 0; padding-left: 1.35em; }
  .zdia-mobile li { font-size: 0.98rem; }
  .zdia-stage { padding: 18px 14px; }
  /* pas de defilement horizontal sur la page : le schema se retire
     au profit de la version texte, qui porte la meme information. */
  .zdia-wrap { display: none; }
  .zdia-readout { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .zdia-tab, .zdia-readout, .zdia-nodes li, .zdia .node rect, .zdia .node polygon { transition: none; }
}

/* -- le signe d'attention ------------------------------------ */
/* Un arc de lumiere rouge fait le tour de la cible. Le rouge est
   celui du logo (--accent), pas une teinte de mode : le signe dit
   « quelque chose se passe ici », pas « tu es en disco ». */
.attract-halo,
.attract-arc {
  position: absolute;
  inset: -3px;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.34s ease;
}
.attract-halo.is-on,
.attract-arc.is-on { opacity: 1; }

/* La lueur : une ombre portee autour de la boite de bordure. Un
   degrade radial en closest-side dessinerait une ellipse — sur un
   bouton large et bas elle deborderait dans tous les sens. La
   box-shadow suit le rayon reel de la cible, donc le lisere reste
   colle au bouton capsule comme au blason carre. */
.attract-halo {
  box-shadow:
    0 0 0 1px color-mix(in oklch, var(--accent) 34%, transparent),
    0 0 18px 5px color-mix(in oklch, var(--accent) 30%, transparent);
  animation: zbb-attract-breathe 2.6s ease-in-out infinite;
}

/* L'arc : un degrade conique decoupe sur la bande de bordure de 2px.
   Le masque a deux couches en `exclude` garde le centre transparent
   et ne peint que le contour — le libelle du bouton n'est jamais
   recouvert, meme sur la cible la plus etroite.

   Le degrade est a DEUX tons : un coeur clair et une aureole rouge.
   Un anneau d'une seule teinte disparait sur le bouton rouge — le
   signe se posait exactement sur l'element qu'il devait signaler, et
   sur un aplat rouge il n'y a plus rien a voir. Le coeur clair
   donne le lisere, l'aureole donne le rouge du logo. Le coeur se
   lit sur les quatre fonds du site (papier, plaque, aplat rouge,
   encre disco) ; un rouge seul ne se lit sur aucun des deux
   derniers. */
@property --attract-from {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}
.attract-arc {
  border: 2px solid transparent;
  background: conic-gradient(from var(--attract-from, 0deg),
    transparent 0deg,
    transparent 212deg,
    color-mix(in oklch, var(--on-accent) 90%, #fff) 296deg,
    var(--on-accent) 322deg,
    color-mix(in oklch, var(--accent) 78%, var(--on-accent)) 350deg,
    transparent 360deg) border-box;
  -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#fff 0 0) padding-box exclude, linear-gradient(#fff 0 0) border-box;
  animation: zbb-attract-spin 3.4s linear infinite;
}

/* En disco l'accent est deja un corail vif : on garde le meme
   deux tons, seule la largeur de la lueur change, la ou la page
   est sombre et l'ombre portée ne se voit plus. */
[data-theme="dark"] .attract-halo {
  box-shadow:
    0 0 0 1px color-mix(in oklch, var(--accent) 46%, transparent),
    0 0 24px 8px color-mix(in oklch, var(--accent) 34%, transparent);
}

@keyframes zbb-attract-spin {
  from { --attract-from: 0deg; }
  to   { --attract-from: 360deg; }
}
@keyframes zbb-attract-breathe {
  0%, 100% { opacity: 0.5; }
  50%      { opacity: 1; }
}

/* Sous mouvement reduit l'arc ne tourne plus : il reste un lisere
   fixe, present mais non anime. Couper l'anneau entier supprimerait
   le seul signal qui dit « il y a une autre couche ici » — on garde
   l'information, on retire le mouvement. */
@media (prefers-reduced-motion: reduce) {
  .attract-arc, .attract-halo { animation: none; }
  /* Meme specificite que la regle .is-on : sans le selecteur de classe
     supplementaire, celle-ci gardait l'opacite de l'etat allume et la
     respiration etait seulement arretee, pas attenuee. */
  .attract-arc.is-on { opacity: 1; }
  .attract-halo.is-on { opacity: 0.75; }
}

@media print {
  .topnav, .bg-layer, .bg-glows, .bg-life, .ticker, .theme-toggle, .langpick,
  .attract-halo, .attract-arc { display: none !important; }
  body { background: #fff; color: #000; }
  .card, .callout, .pagefoot { border-color: #000; }
}
