@import url("/assets/variables-82c3ee19.css");
@import url("/assets/fulgore/animations-f847f8b4.css");
@import url("/assets/components-f44d7150.css");
@import url("/assets/fulgore/loading_link-a312c367.css");
@import url("/assets/fulgore/image_viewer-569b36c6.css");
@import url("/assets/fulgore/form-5beaed61.css");
@import url("/assets/fulgore/input-cc93d1fd.css");
@import url("/assets/fulgore/button-0de583fe.css");
@import url("/assets/fulgore/a11y-74ac36e9.css");

/* Brand override — replace the violet scale with your project's palette.
   Example with slate:

:root {
  --color-brand:         var(--color-slate-700);
  --color-brand-light:   var(--color-slate-500);
  --color-brand-dark:    var(--color-slate-900);
  --color-brand-surface: var(--color-slate-50);
  --color-brand-a10:     color-mix(in srgb, var(--color-brand) 10%, transparent);
  --color-brand-a20:     color-mix(in srgb, var(--color-brand) 20%, transparent);
}
*/

html {
  scroll-behavior: smooth;
}

html,
body {
  margin: 0;
  padding: 0;
  font-family: var(--font-family-base);
  color: var(--color-text);
  background: var(--color-surface);
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-family-display);
  font-weight: var(--font-weight-semibold);
  line-height: var(--line-height-tight);
  color: var(--color-text);
}

svg {
  fill: currentColor;
}

.hidden {
  display: none;
}

body.body--modal-open {
  overflow: hidden;
}

body.body--drawer-open {
  overflow: hidden;
}

/* ============================================================
   GABARIT — deux colonnes
   La sidebar et TOUT son repli mobile viennent de fulgore
   (`ui/shell/sidebar.css`, via la classe `ui-shell-sidebar` que
   `Shared::Sidebar` reconduit) : tiroir hors-écran sous 1024 px,
   colonne collante au-dessus. Rien de tout ça n'est redit ici. Ce
   bloc ne pose que ce que la gem ne peut pas savoir — le conteneur
   qui met les deux colonnes côte à côte, et la hauteur réelle de
   NOTRE barre. */

/* ⚠️ La gem fixe `--navbar-height` à 3.5rem (56 px), taillé pour une barre de
   back-office. Celle du hub en fait 4rem — POSÉE (`min-height: 4rem` sur
   `.shared-navbar`, `shared/navbar.css`), et non déduite de son contenu :
   c'est ce qui garde le token EXACT alors que la barre n'a pas le même
   contenu selon la largeur d'écran (la marque n'y apparaît que sous
   1024 px, `shared/navbar.css`). Un token dérivé du logo aurait dû suivre
   cette variation ; un token posé n'a pas à le faire.

   Deux lecteurs en dépendent : `.app-shell` ci-dessous (`min-height`) et
   `.flash-container` de fulgore (`shared/flash.css`,
   `top: calc(var(--navbar-height) + var(--space-4))`) — laissé désaccordé,
   l'un des deux mesurerait un dégagement faux sous la barre. */
:root {
  --navbar-height: 4rem;
}

/* Sur `/session/new` et `/password_reset/*`, il n'y a PAS de barre :
   `Shared::Navbar` porte un `display?` sur `logged_in?`. Le token doit
   donc y valoir zéro, parce qu'il DÉCRIT la hauteur de notre barre et
   qu'elle vaut réellement zéro sur ces écrans.

   Corriger la mesure plutôt que surcharger chaque `top` : ça rattrape
   d'un seul geste tous ceux qui la lisent, y compris ceux de la gem
   qu'on ne peut pas atteindre autrement. C'était le cas de
   `.flash-container` (fulgore, `shared/flash.css` : `position: fixed;
   top: calc(var(--navbar-height) + var(--space-4))`), qui posait le
   message d'erreur de connexion 96 px trop bas — le seul défaut
   VISIBLE des deux. Et de `.app-shell` ci-dessous, dont le `min-height`
   retranchait une barre absente.

   ⚠️ `0rem` et surtout PAS `0` : la gem écrit `calc(var(--navbar-height)
   + var(--space-4))`, et `calc(0 + 1rem)` est INVALIDE — additionner un
   <number> et une <length> n'est pas permis. La déclaration entière
   serait jetée, en silence, et le conteneur retomberait sur son `top`
   initial : un défaut plus difficile à voir que celui qu'on corrige. */
body.body--signed-out {
  --navbar-height: 0rem;
}

.app-shell {
  display: flex;
  align-items: flex-start;
  min-height: calc(100dvh - var(--navbar-height));
}

/* ── LE JEU, QUI N'EST PAS UNE PAGE DU HUB ────────────────────────────────
   Ces deux règles servent `layouts/game.html.erb` et lui seul. Elles vivent
   ici et non dans le sidecar de `Game::Screen` parce qu'elles habillent le
   GABARIT — `.game-body` n'existe dans aucun composant, exactement comme
   `.app-shell` au-dessus.

   ⚠️ `100dvh` ET PAS `100vh` : sur un téléphone, `vh` compte la hauteur
   barre d'adresse RÉTRACTÉE, donc le bas du jeu passe sous la barre au
   chargement et n'en ressort qu'au premier défilement — qu'une page sans
   défilement n'aura jamais. Même unité que `.app-shell`, pour la même
   raison.

   ⚠️ `overflow: hidden` : la page ne défile pas. Sans lui, le moindre
   dépassement d'un pixel donne une page qui glisse sous le pouce alors
   qu'on croyait piloter le jeu. */
.game-body {
  margin: 0;
  overflow: hidden;
  background: var(--color-surface);
}

/* Centre le jeu dans l'écran, quelle que soit sa forme. Le canevas garde ses
   proportions par ses seules dimensions intrinsèques (cf. `screen.css`) : ce
   conteneur ne fait que lui donner la place et le poser au milieu. */
.game-body__main {
  display: flex;
  align-items: center;
  justify-content: center;

  width: 100%;
  height: 100dvh;
}

/* ⚠️ DANS L'APPLICATION INSTALLÉE, `dvh` COMPTE UNE BARRE QUI N'EXISTE PAS, et
   c'est MESURÉ, pas supposé : le jeu affichait ses propres chiffres sur
   l'iPhone de JC le 2026-08-08, et la page lui annonçait `812 x 325` là où
   l'écran en fait 375. Les 50 px manquants sont la hauteur de la barre de
   Safari en paysage — celle que l'application n'a pas.

   Le champ mesure 352 px de haut : à 325 il était coupé en haut et en bas, à
   375 il tient avec 23 px de marge. C'est tout l'écart.

   ⚠️ `dvh` RESTE LE DÉFAUT, et la règle du dessus ne bouge pas : dans Safari
   la barre existe vraiment, et c'est elle qui avait fait écarter `vh` (cf. le
   commentaire de `.game-body`). Seule l'application installée fait exception,
   et `display-mode: standalone` est précisément ce qui la nomme. */
@media (display-mode: standalone) {
  .game-body__main {
    height: 100vh;
  }
}

/* `flex: 1` + `max-width` + `margin-inline: auto` cohabitent sans se
   contredire : la colonne grandit jusqu'à 720 px, puis les marges auto
   absorbent l'espace restant et la centrent dans ce que la sidebar lui
   laisse. Le plafond de lisibilité tient donc SANS toucher un seul
   sélecteur de page — c'est pour ça que le `max-width` reste posé sur
   `.app-main` lui-même plutôt que sur ses enfants.

   `min-width: 0` autorise le rétrécissement sous la largeur intrinsèque
   du contenu : sans lui, une galerie de sprites large pousserait la
   sidebar hors de l'écran au lieu de défiler dans sa propre colonne. */
.app-main {
  flex: 1;
  min-width: 0;
  padding-block: var(--space-4);
  padding-inline: var(--space-4);
}

@media (min-width: 768px) {
  .app-main {
    max-width: 720px;
    margin-inline: auto;
  }

  /* UNE exception au plafond ci-dessus, et une seule : la fiche de carte, qui
     porte depuis le 2026-08-05 un encadré « Voir également » à sa droite.

     Elle existe parce que le plafond est posé sur le CONTENU (`box-sizing:
     content-box` — mesuré : `max-width: 720` rend une boîte de 752 avec ses
     deux paddings de 16). Un encadré de 18rem pris DANS ces 720 laissait 408px
     au texte, contre 720 avant : mesuré au navigateur à 1920, pas estimé. À
     1040, le texte retrouve 728px et l'encadré s'ajoute à côté au lieu de
     mordre dedans. Tranché par JC avec les trois largeurs sous les yeux.

     ⚠️ La fiche de carte devient donc la SEULE page large du hub : passer d'une
     liste à une carte élargit la colonne sous l'œil. C'est le prix accepté, pas
     un effet de bord — l'autre voie était d'élargir tout le hub, écartée parce
     que le plafond de 720 est un choix de lisibilité assumé (cf. le commentaire
     juste au-dessus), pas un reste.

     `:has()` plutôt qu'une classe posée par le layout : la connaissance reste
     en CSS, et le projet s'en sert déjà (images/index.css:33). Si un navigateur
     ne le connaît pas, la page reste à 720 et le texte à 408 — dégradé, jamais
     cassé. */
  .app-main:has(> .cards-show) {
    max-width: 1040px;
  }
}

/* ⚠️ La gem ne fournit AUCUN fond pour sa sidebar en tiroir :
   `nav_sidebar_controller` bloque bien le défilement du corps
   (`body--drawer-open`, nav_sidebar_controller.js:74) mais n'écoute pas
   Échap et ne pose rien par-dessus le contenu. Sans ce fond, le seul
   moyen de refermer le tiroir est de re-viser le bouton ☰ qu'il
   recouvre à moitié.

   Il vit ici et pas dans fulgore : la gem est suivie par Termina,
   Fatima, TuComptes et Agahim, un ajout s'y négocie par le gate
   `fulgore-api-stability`.

   `--z-raised` moins 1 : le fond doit passer SOUS la sidebar (qui
   porte `--z-raised`) et AU-DESSUS du contenu. Le lier au token plutôt
   que d'écrire 9 en dur évite qu'un futur ajustement de l'échelle les
   désolidarise en silence. */
.app-backdrop {
  position: fixed;
  inset: 0;
  z-index: calc(var(--z-raised) - 1);
  background: rgb(0 0 0 / 0.5);

  opacity: 0;
  visibility: hidden;
  transition:
    opacity var(--duration-slow) var(--easing-out),
    visibility var(--duration-slow);
}

/* `body--drawer-open` n'est posé QUE sous 1024 px (le contrôleur teste
   `isMobile` avant de basculer la classe) : le fond ne peut donc pas
   apparaître derrière la sidebar collante du desktop. */
body.body--drawer-open .app-backdrop {
  opacity: 1;
  visibility: visible;
}

/* Au-dessus de 1024 px il n'y a pas de tiroir — la sidebar est une colonne —
   donc pas de fond à taper. Le retirer plutôt que le laisser invisible n'est
   pas cosmétique : `visibility: hidden` laisse la BOÎTE en place, et un `<div>`
   `fixed; inset: 0` posé en haut de page ressemble à s'y méprendre à une barre
   d'en-tête collante. L'axe `headerClearance` de `visual-render` s'y laisse
   prendre et mesure le dégagement du contenu sous un « header » qui couvre tout
   l'écran (offender `h1` / header `div.app-backdrop`, gap −748 px à 390×844).
   ⚠️ Ce fichier a longtemps dit « non reproductible au passage suivant » : c'est
   FAUX, et la phrase a coûté une suspicion de régression en n22. Le faux positif
   est parfaitement déterministe — il tombe à TOUT viewport sous 1024 px, et
   seulement là, puisque c'est la règle ci-dessous qui le fait disparaître
   au-dessus. Le voir n'est jamais le signe qu'on vient de casser quelque chose.
   Aucun défaut réel — `visibility: hidden` suffit à le sortir du hit-testing et
   de l'arbre d'accessibilité — mais autant ne pas laisser traîner la boîte là
   où elle ne sert à rien. */
@media (min-width: 1024px) {
  .app-backdrop {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .app-backdrop {
    transition: none;
  }
}

/* ============================================================
   BRIQUES PARTAGÉES
   Une classe monte ici quand un DEUXIÈME composant s'en sert : en
   sidecar elle appartient à son composant, ici elle appartient au hub.
   Règle posée en n21 pour `.shared-search-field__label` (restée en
   sidecar, un seul usage) ; voici sa première vraie occasion. */

/* Les trois commandes à icône seule de la barre — le menu ☰, la cloche
   et l'interrupteur de thème — portaient ces mêmes déclarations,
   recopiées entre `shared/navbar.css` et `shared/activity_bell.css`.
   L'auditeur CSS les re-signalait à chaque passage sans jamais pouvoir
   les remonter : les trois composants héritent directement de
   `BaseComponent`, il n'existe aucun parent où pousser la règle — il
   fallait créer l'endroit, pas déplacer la règle.

   Boîte carrée de 32 px à dessein : une icône seule tient dans le même
   gabarit que les autres commandes, un libellé texte redéclencherait le
   débordement mesuré en 390 px (cf. l'en-tête de `shared/navbar.css`).

   ⚠️ RIEN sur `display` dans la règle d'icône ci-dessous, et ce n'est
   pas un oubli. Les deux icônes du thème sont allumées et éteintes par
   `.shared-navbar__theme-icon--sun` / `--moon` (`shared/navbar.css`),
   qui pèsent (0,1,0). Un `display: block` posé ici pèserait (0,1,1),
   gagnerait, et laisserait le soleil et la lune allumés en même temps.
   La TAILLE se partage ; l'AFFICHAGE reste au composant qui le
   gouverne. Poser `display` ici demanderait de remonter aussi les deux
   bascules, donc de faire entrer la logique de thème dans une brique
   que la cloche partage — exactement ce qu'on ne veut pas. */
.shared-icon-button {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;

  width: 32px;
  height: 32px;
  padding: var(--space-1);
  border: none;
  border-radius: var(--radius-md);

  background: transparent;
  color: var(--color-text-soft);

  cursor: pointer;

  &:hover {
    color: var(--color-text);
  }

  &:focus-visible {
    outline: none;
    box-shadow: var(--focus-ring);
  }
}

/* En px et non `--icon-size-*` : la barre ne s'appuyait pas sur ce token
   pour ses icônes de commande, on reprend sa convention locale plutôt
   que d'en inventer une nouvelle au moment de partager. */
.shared-icon-button svg {
  width: 18px;
  height: 18px;
}

/* `.visually-hidden` (masquage PERMANENT, sans media query) a vécu ici pour
   le libellé du champ de recherche — parti depuis le commit du 2026-08-03 qui
   fait porter ce nom par `aria-label` directement dans la gem
   (`Shared::SearchField` passe `label:` à `UI::Inputs::Search`, fulgore
   `dfde2b0`). Plus aucun appelant dans le hub (vérifié par grep avant retrait).

   La recette permanente vit désormais dans la gem : `.ui-visually-hidden`,
   `fulgore/a11y.css` (importée plus haut) — c'est elle qu'on utilise pour tout
   nouveau besoin de masquage permanent (premier appelant : la marque « non
   lu » d'`Activities::Item`, `activities/item.css`).

   `.visually-hidden-sm` RESTE ici, seule : elle porte un point de rupture à
   640 px propre à ce hub (le texte du bouton « Nouvelle carte », masqué
   seulement quand la place manque, `shared/navbar.rb:70`), qui n'a rien à
   faire dans une gem partagée par cinq projets — une media query n'a pas sa
   place dans une primitive transverse.

   ⚠️ Masqué à l'ŒIL, pas retiré : `display: none` ou `visibility: hidden`
   sortiraient l'élément de l'arbre d'accessibilité, ce qui annulerait tout
   l'intérêt de la recette. Le 1px + `clip-path: inset(50%)` est la seule
   combinaison qui garde l'élément annonçable par un lecteur d'écran tout en
   le retirant du flux visuel. */
@media (max-width: 639px) {
  .visually-hidden-sm {
    position: absolute;
    width: 1px;
    height: 1px;
    /* Aligné sur `.ui-visually-hidden` (fulgore) : cette marge négative reprend
       le pixel que l'élément occupe encore dans le flux. Elle manquait ici —
       dérive de recopie relevée par l'audit CSS du 2026-08-03, pas un choix. */
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* ============================================================
   MODE SOMBRE — TOKENS SÉMANTIQUES
   Deux déclencheurs portent les MÊMES déclarations : la préférence
   système (`@media prefers-color-scheme`) et le choix explicite
   (`[data-theme="dark"]`, posé par cookie serveur + contrôleur
   Stimulus). CSS ne sait pas faire matcher un même bloc par une
   media query ET un sélecteur d'attribut à la fois — feature média
   et sélecteur DOM sont deux mécanismes indépendants, il n'existe
   pas de « OR » entre les deux sans dupliquer le bloc (ou sans JS).
   Le bloc de tokens est donc recopié une fois entre les deux ; toute
   modification future doit toucher les DEUX.

   On ne redéfinit QUE la couche sémantique de variables.css :
   surfaces, texte, marque, statuts, focus, ombres. Les échelles
   brutes (--color-neutral-*, --color-violet-*…) ne bougent jamais —
   c'est justement ce qui permet à tout composant discipliné de
   basculer sans qu'on le touche. */

@media (prefers-color-scheme: dark) {
  /* :not([data-theme="light"]) laisse un choix explicite « clair »
     annuler la préférence système sombre. */
  :root:not([data-theme="light"]) {
    color-scheme: dark; /* bascule scrollbars, sélecteurs natifs, champs date */

    /* Surfaces & bordures */
    --color-surface:        var(--color-neutral-950);
    --color-surface-subtle: var(--color-neutral-900);
    --color-surface-raised: var(--color-neutral-800);
    --color-surface-alt:    var(--color-neutral-800);
    --color-border:         var(--color-neutral-800);
    --color-border-strong:  var(--color-neutral-600);

    /* Texte — neutral-50 et non neutral-0 : du blanc pur sur du
       quasi-noir vibre à la lecture. --color-text-inverted s'inverse
       aussi, sinon tout ce qui écrit « en négatif » (bouton plein
       brand, pastille --strong) disparaît. */
    --color-text:          var(--color-neutral-50);
    --color-text-soft:     var(--color-neutral-300);
    --color-text-subtle:   var(--color-neutral-400);
    --color-text-muted:    var(--color-neutral-500);
    --color-text-disabled: var(--color-neutral-600);
    --color-text-inverted: var(--color-neutral-950);

    /* Marque — le violet-600 de jour devient illisible sur fond noir.
       ⚠️ --color-brand-dark s'ÉCLAIRCIT ici au lieu de foncer, et ce n'est pas
       une faute de frappe : ce token sert de fond au SURVOL du bouton primaire
       (`button.css:41`), dont le texte est --color-text-inverted, lui-même
       devenu sombre juste au-dessus. Garder un violet-600 y assombrissait un
       fond déjà surmonté d'encre sombre — contraste mesuré 2.37, sous le
       plancher de 3, alors qu'il DOIT monter au survol. Mesuré à 6.5 avec
       violet-300. Ses autres consommateurs (curseur et anneau de
       `range_slider.css`) y gagnent pour la même raison : tous posent cette
       couleur sur du sombre. */
    --color-brand:         var(--color-violet-400);
    --color-brand-light:   var(--color-violet-300);
    --color-brand-dark:    var(--color-violet-300);
    --color-brand-surface: var(--color-violet-950);
    /* --color-brand-a10/a20, --color-accent et --focus-ring dérivent
       de --color-brand par color-mix : ils suivent seuls, ne pas les
       redéclarer ici. */

    /* Statuts — les échelles de statut s'arrêtent à 900 (ou 700), il
       n'y a pas de 950 pour bâtir un fond sombre : on le compose au
       color-mix, le mécanisme qu'emploie déjà fulgore pour ses a10/a20. */
    --color-danger:          var(--color-red-500);
    --color-danger-dark:     var(--color-red-700);
    --color-danger-surface:  color-mix(in srgb, var(--color-red-500) 20%, var(--color-neutral-950));
    --color-success-surface: color-mix(in srgb, var(--color-green-500) 20%, var(--color-neutral-950));
    --color-warning-surface: color-mix(in srgb, var(--color-amber-500) 20%, var(--color-neutral-950));
    --color-info-surface:    color-mix(in srgb, var(--color-blue-500) 20%, var(--color-neutral-950));

    /* Redéfinir le fond d'une pastille de statut ne suffit pas : `pill.css` et
       `flash.css` posent le token de statut LUI-MÊME comme couleur de texte
       par-dessus. Laissés au ton de jour sur ces fonds sombres, ils tombaient à
       4.33:1 (--completed, --lmnp_vat), sous le seuil AA du texte normal — ces
       pastilles sont en 12px semibold, elles n'ont pas droit au seuil « grand
       texte ». On les remonte vers le blanc.

       --color-danger est délibérément ABSENT de cette liste : seul de la série,
       il sert aussi d'APLAT (bouton de suppression, `button.css:62`, que le hub
       rend via UI::Buttons::ConfirmDestroy). L'éclaircir délaverait le bouton
       « supprimer » en rose pâle. Le texte danger est traité là où il se pose :
       l'override de pastille plus bas. */
    --color-success:      color-mix(in srgb, var(--color-green-500) 75%, var(--color-neutral-0));
    --color-warning:      color-mix(in srgb, var(--color-amber-500) 75%, var(--color-neutral-0));
    --color-warning-text: color-mix(in srgb, var(--color-amber-500) 75%, var(--color-neutral-0));
    --color-info:         color-mix(in srgb, var(--color-blue-500) 75%, var(--color-neutral-0));
    /* --color-danger-a10/a20 et --color-warning-a10 dérivent de
       --color-danger / --color-warning par color-mix : suivent seuls. */

    /* Focus — l'anneau neutre de jour (neutral-300) éblouit sur fond
       noir. --focus-ring et --focus-ring-danger dérivent de
       --color-brand-a20 / --color-danger-a10 : ils suivent seuls. */
    --focus-ring-neutral: 0 0 0 3px var(--color-neutral-600);

    /* Ombres — une ombre noire à 10 % n'existe plus sur du noir. On
       les renforce pour qu'un panneau surélevé se détache encore un
       peu ; l'essentiel du relief passe désormais par
       --color-surface-raised. */
    --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.4);
    --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.5), 0 2px 4px -2px rgb(0 0 0 / 0.5);
    --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.55), 0 4px 6px -4px rgb(0 0 0 / 0.55);
    --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.6), 0 8px 10px -6px rgb(0 0 0 / 0.6);
    --shadow-pill-strong: 0 2px 6px rgb(0 0 0 / 0.5);
  }
}

/* Choix explicite « sombre » — mêmes déclarations que ci-dessus, cf.
   le commentaire en tête de bloc sur l'impossibilité de les partager
   entre une media query et un sélecteur d'attribut. */
:root[data-theme="dark"] {
  color-scheme: dark;

  --color-surface:        var(--color-neutral-950);
  --color-surface-subtle: var(--color-neutral-900);
  --color-surface-raised: var(--color-neutral-800);
  --color-surface-alt:    var(--color-neutral-800);
  --color-border:         var(--color-neutral-800);
  --color-border-strong:  var(--color-neutral-600);

  --color-text:          var(--color-neutral-50);
  --color-text-soft:     var(--color-neutral-300);
  --color-text-subtle:   var(--color-neutral-400);
  --color-text-muted:    var(--color-neutral-500);
  --color-text-disabled: var(--color-neutral-600);
  --color-text-inverted: var(--color-neutral-950);

  /* --color-brand-dark s'éclaircit : c'est le fond du SURVOL, sous une encre
     devenue sombre. Raison détaillée dans le bloc media query ci-dessus. */
  --color-brand:         var(--color-violet-400);
  --color-brand-light:   var(--color-violet-300);
  --color-brand-dark:    var(--color-violet-300);
  --color-brand-surface: var(--color-violet-950);

  --color-danger:          var(--color-red-500);
  --color-danger-dark:     var(--color-red-700);
  --color-danger-surface:  color-mix(in srgb, var(--color-red-500) 20%, var(--color-neutral-950));
  --color-success-surface: color-mix(in srgb, var(--color-green-500) 20%, var(--color-neutral-950));
  --color-warning-surface: color-mix(in srgb, var(--color-amber-500) 20%, var(--color-neutral-950));
  --color-info-surface:    color-mix(in srgb, var(--color-blue-500) 20%, var(--color-neutral-950));

  /* Encre des statuts remontée vers le blanc ; --color-danger en est exclu
     parce qu'il sert aussi d'aplat. Détail dans le bloc media query. */
  --color-success:      color-mix(in srgb, var(--color-green-500) 75%, var(--color-neutral-0));
  --color-warning:      color-mix(in srgb, var(--color-amber-500) 75%, var(--color-neutral-0));
  --color-warning-text: color-mix(in srgb, var(--color-amber-500) 75%, var(--color-neutral-0));
  --color-info:         color-mix(in srgb, var(--color-blue-500) 75%, var(--color-neutral-0));

  --focus-ring-neutral: 0 0 0 3px var(--color-neutral-600);

  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.4);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.5), 0 2px 4px -2px rgb(0 0 0 / 0.5);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.55), 0 4px 6px -4px rgb(0 0 0 / 0.55);
  --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.6), 0 8px 10px -6px rgb(0 0 0 / 0.6);
  --shadow-pill-strong: 0 2px 6px rgb(0 0 0 / 0.5);
}

/* Choix explicite « clair » — y compris sur système sombre : ramène
   aussi les widgets natifs (scrollbars, date pickers) en mode clair. */
:root[data-theme="light"] {
  color-scheme: light;
}

/* ============================================================
   MODE SOMBRE — PASTILLES DOUCES (ui-pill)
   Les variantes déjà branchées sur les tokens sémantiques
   (--draft, --in_progress, --review, --completed, --archived,
   --active, --pending, --lmnp, --lmnp_vat) basculent seules avec
   variables.css : ne pas les toucher. Les variantes --strong posent
   un aplat saturé + texte blanc (pill.css) : lisibles tel quel sur
   fond noir, ne pas les toucher non plus.

   Restent les pastilles « douces » : pill.css leur pose un fond
   clair -100 et un texte foncé -700, qui resteraient des taches
   lumineuses sur fond noir. On recompose fond et texte au color-mix
   à partir du point -500 de l'échelle brute — regroupées par teinte
   pour ne pas répéter la formule 21 fois.

   ⚠️ .ui-pill--strong.ui-pill--brand (pill.css) pèse (0,2,0). Un
   sélecteur `:root[data-theme="dark"] .ui-pill--brand` pèse (0,3,0)
   et l'écraserait : un pill qui porte À LA FOIS .ui-pill--brand et
   .ui-pill--strong matcherait les deux règles, et la nôtre gagnerait
   à tort, cassant l'aplat plein de la variante strong. Chaque
   sélecteur ci-dessous porte donc :not(.ui-pill--strong) — NE PAS LE
   RETIRER, y compris dans la branche media query. */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .ui-pill--brand:not(.ui-pill--strong),
  :root:not([data-theme="light"]) .ui-pill--violet:not(.ui-pill--strong),
  :root:not([data-theme="light"]) .ui-pill--admin:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-violet-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-violet-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--info:not(.ui-pill--strong),
  :root:not([data-theme="light"]) .ui-pill--blue:not(.ui-pill--strong),
  :root:not([data-theme="light"]) .ui-pill--accountant:not(.ui-pill--strong),
  :root:not([data-theme="light"]) .ui-pill--read:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-blue-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-blue-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--success:not(.ui-pill--strong),
  :root:not([data-theme="light"]) .ui-pill--green:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-green-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-green-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--danger:not(.ui-pill--strong),
  :root:not([data-theme="light"]) .ui-pill--red:not(.ui-pill--strong),
  :root:not([data-theme="light"]) .ui-pill--suspended:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-red-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-red-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--warning:not(.ui-pill--strong),
  :root:not([data-theme="light"]) .ui-pill--amber:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-amber-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-amber-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--neutral:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-neutral-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-neutral-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--indigo:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-indigo-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-indigo-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--teal:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-teal-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-teal-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--lime:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-lime-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-lime-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--orange:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-orange-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-orange-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--pink:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-pink-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-pink-500) 65%, var(--color-neutral-0));
  }

  :root:not([data-theme="light"]) .ui-pill--brown:not(.ui-pill--strong) {
    background: color-mix(in srgb, var(--color-brown-500) 18%, var(--color-neutral-950));
    color: color-mix(in srgb, var(--color-brown-500) 65%, var(--color-neutral-0));
  }
}

/* Choix explicite « sombre » — mêmes règles, même garde. */
:root[data-theme="dark"] .ui-pill--brand:not(.ui-pill--strong),
:root[data-theme="dark"] .ui-pill--violet:not(.ui-pill--strong),
:root[data-theme="dark"] .ui-pill--admin:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-violet-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-violet-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--info:not(.ui-pill--strong),
:root[data-theme="dark"] .ui-pill--blue:not(.ui-pill--strong),
:root[data-theme="dark"] .ui-pill--accountant:not(.ui-pill--strong),
:root[data-theme="dark"] .ui-pill--read:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-blue-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-blue-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--success:not(.ui-pill--strong),
:root[data-theme="dark"] .ui-pill--green:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-green-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-green-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--danger:not(.ui-pill--strong),
:root[data-theme="dark"] .ui-pill--red:not(.ui-pill--strong),
:root[data-theme="dark"] .ui-pill--suspended:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-red-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-red-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--warning:not(.ui-pill--strong),
:root[data-theme="dark"] .ui-pill--amber:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-amber-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-amber-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--neutral:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-neutral-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-neutral-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--indigo:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-indigo-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-indigo-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--teal:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-teal-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-teal-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--lime:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-lime-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-lime-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--orange:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-orange-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-orange-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--pink:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-pink-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-pink-500) 65%, var(--color-neutral-0));
}

:root[data-theme="dark"] .ui-pill--brown:not(.ui-pill--strong) {
  background: color-mix(in srgb, var(--color-brown-500) 18%, var(--color-neutral-950));
  color: color-mix(in srgb, var(--color-brown-500) 65%, var(--color-neutral-0));
}
