/* ============================================================
   DESIGN SYSTEM v6 — COMPONENTS
   ============================================================
   Styles des éléments HTML natifs + composants (.badge, .avatar…).

   RÈGLE D'OR : ce fichier ne référence QUE des tokens
   sémantiques (--surface-*, --txt-*, --space-*, --layout-*,
   --shadow-*, --motion-*, --font-*, --weight-*, --leading-*,
   --tracking-*, --size-*…), jamais une primitive de couleur
   (--c-*) ou de police (--base-font-*), ni un sélecteur
   [data-theme-style]. Il est donc rigoureusement identique quel
   que soit le thème actif — seul tokens.css / styles/themes/theme-*.css
   changent le résultat visuel. Cette règle est vérifiée
   automatiquement par le plugin stylelint (tooling/) sur les
   primitives de couleur et de police ; l'échelle brute de taille,
   graisse et interlignage (--base-text-*, --base-weight-*, --base-leading-*, --base-tracking-*) reste
   utilisable pour le détail d'interface (badges, légendes…), au
   même titre que --base-space-* reste utilisable dans utilities.css —
   voir DOCUMENTATION.md « Garde-fou ».
   ============================================================ */


/* ============================================================
   1. RESET & DOCUMENT
   ============================================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html {
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  scroll-behavior: smooth;
}
body {
  font-family: var(--font-body);
  font-size: var(--size-body);
  font-weight: var(--weight-body);
  line-height: var(--leading-body);
  color: var(--text);
  background: var(--surface-base);
  transition: background-color var(--motion-normal), color var(--motion-normal);
}

/* Propriétés standard d'abord (Firefox, et Chromium ≥ 121 qui les fait
   primer sur ::-webkit-scrollbar) ; les pseudo-éléments préfixés
   restent le repli pour Safari et les Chromium plus anciens. Sans les
   standard, les barres de défilement n'étaient thémées QUE sur
   Chromium. */
html {
  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
  scrollbar-width: thin;
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--scrollbar-track); }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: var(--layout-radius-badge); }
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }

:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }
::selection { background: var(--selection-background); color: var(--selection-foreground); }


/* ============================================================
   2. TITRES
   ============================================================ */
:is(h1, h2, h3, h4, h5, h6) {
  font-family: var(--font-heading);
  font-weight: var(--weight-heading);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  color: var(--text);
}
h1 { font-size: var(--size-h1); font-weight: var(--weight-display); letter-spacing: var(--tracking-display); margin-bottom: var(--space-heading-gap-lg); }
h2 { font-size: var(--size-h2); margin-bottom: var(--space-heading-gap-lg); }
h3 { font-size: var(--size-h3); margin-bottom: var(--space-heading-gap-sm); }
h4 { font-size: var(--size-h4); margin-bottom: var(--space-heading-gap-sm); }
h5 { font-size: var(--size-h5); margin-bottom: var(--space-heading-gap-sm); }
h6 { font-size: var(--size-h6); font-weight: var(--weight-ui); color: var(--text-secondary); margin-bottom: var(--space-heading-gap-sm); }

:is(section, article, .card) :is(h2, h3, h4) { margin-top: var(--space-heading-top-gap); }


/* ============================================================
   3. TEXTE
   ============================================================ */
p { margin-bottom: var(--space-stack-gap); color: var(--text-secondary); line-height: var(--leading-paragraph); }
a { color: var(--text-link); text-decoration: none; font-weight: var(--base-weight-medium); transition: color var(--motion-fast); text-underline-offset: 3px; }
a:hover { color: var(--text-link-hover); text-decoration: underline; }
strong, b { font-weight: var(--weight-strong); color: var(--text); }
em, i { font-style: italic; }
small { font-size: var(--size-small); color: var(--text-tertiary); }
mark {
  background: var(--status-warning-background); color: var(--status-warning-foreground);
  padding: var(--space-4xs) var(--space-3xs); border-radius: var(--layout-radius-button-sm); font-weight: var(--base-weight-medium);
}
abbr[title] { text-decoration: underline dotted var(--text-tertiary); cursor: help; }
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
sup { top: -0.5em; } sub { bottom: -0.25em; }


/* ============================================================
   4. LISTES
   ============================================================ */
ul, ol { margin-bottom: var(--space-stack-gap); padding-left: var(--space-list-indent); color: var(--text-secondary); }
li { margin-bottom: var(--space-list-item-gap); line-height: var(--leading-paragraph); }
li::marker { color: var(--text-marker); }
li > :is(ul, ol) { margin-top: var(--space-list-item-gap); margin-bottom: var(--base-space-0); }
dl { margin-bottom: var(--space-stack-gap); }
dt { font-weight: var(--base-weight-semi); color: var(--text); margin-bottom: var(--space-3xs); }
dd { color: var(--text-secondary); margin-bottom: var(--space-list-item-gap); padding-left: var(--space-stack-gap); }


/* ============================================================
   5. CODE
   ============================================================ */
code {
  font-family: var(--font-code); font-size: 0.875em; font-weight: var(--base-weight-medium);
  background: var(--action-secondary); color: var(--text-code);
  padding: var(--space-code-padding); border-radius: var(--layout-radius-button-sm);
  border: 1px solid var(--border-muted);
}

pre {
  margin-bottom: var(--space-group-gap); padding: var(--space-pre-padding);
  background: var(--surface-sunken); border: 1px solid var(--border-muted);
  border-radius: var(--layout-radius-card); overflow-x: auto;
  font-family: var(--font-code); font-size: var(--base-text-sm);
  line-height: var(--leading-paragraph); color: var(--text-secondary);
}
pre > code { background: none; border: none; padding: 0; font-size: inherit; color: inherit; }

kbd {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 1.6em; padding: var(--space-code-padding);
  font-family: var(--font-code); font-size: var(--base-text-xs); font-weight: var(--base-weight-medium);
  line-height: var(--base-leading-snug); color: var(--text);
  background: var(--surface-raised); border: 1px solid var(--border);
  border-bottom-width: 2px; border-radius: var(--layout-radius-button-sm);
  box-shadow: var(--shadow-kbd);
}
samp { font-family: var(--font-code); font-size: 0.875em; color: var(--status-success-foreground); }
var { font-family: var(--font-code); font-style: italic; color: var(--text-code-accent); }


/* ============================================================
   6. BLOCKQUOTE
   ============================================================ */
blockquote {
  margin-bottom: var(--space-group-gap); padding: var(--space-blockquote-padding);
  border-left: 4px solid var(--border-accent);
  background: var(--surface-raised); border-radius: 0 var(--layout-radius-card) var(--layout-radius-card) 0;
  font-size: var(--base-text-md); color: var(--text-secondary); font-style: italic;
}
blockquote p { margin-bottom: var(--space-2xs); }
blockquote p:last-child { margin-bottom: 0; }
cite { display: block; font-size: var(--base-text-sm); font-style: normal; font-weight: var(--base-weight-medium); color: var(--text-tertiary); margin-top: var(--space-2xs); }
cite::before { content: '— '; }

blockquote[data-variant="success"] { border-left-color: var(--status-success-foreground); background: var(--status-success-background); }
blockquote[data-variant="warning"] { border-left-color: var(--status-warning-foreground); background: var(--status-warning-background); }
blockquote[data-variant="danger"]  { border-left-color: var(--status-danger-foreground); background: var(--status-danger-background); }
blockquote[data-variant="info"]    { border-left-color: var(--status-info-foreground); background: var(--status-info-background); }


/* ============================================================
   7. HR
   ============================================================ */
hr { border: none; height: 1px; background: var(--border); margin: var(--space-section-gap) 0; }
hr[data-variant="subtle"]   { background: var(--border-muted); margin: var(--space-stack-gap) 0; }
hr[data-variant="gradient"] { height: 2px; background: var(--brand-gradient); border-radius: var(--layout-radius-badge); }


/* ============================================================
   8. TABLEAUX
   ============================================================ */
table { width: 100%; border-collapse: collapse; margin-bottom: var(--space-group-gap); font-size: var(--base-text-sm); }
thead { border-bottom: 2px solid var(--border); }
th {
  text-align: left; padding: var(--space-table-cell-padding);
  font-weight: var(--base-weight-semi); font-size: var(--base-text-xs);
  text-transform: uppercase; letter-spacing: var(--base-tracking-wider); color: var(--text-tertiary);
}
td { padding: var(--space-table-cell-padding); color: var(--text-secondary); border-bottom: 1px solid var(--border-muted); }
tbody tr { transition: background var(--motion-fast); }
tbody tr:hover { background: var(--action-secondary); }

table[data-variant="striped"] tbody tr:nth-child(even) { background: var(--surface-raised); }
table[data-variant="striped"] tbody tr:hover { background: var(--action-secondary-hover); }

/* Une table trop large défile dans son propre cadre au lieu de déborder
   la page. display:block fait de la table un bloc scrollable ; ses lignes
   conservent leur mise en forme dans une table anonyme. Pas de wrapper
   requis dans le HTML — l'élément <table> nu suffit.

   Le déclencheur est la largeur du CONTENEUR, pas celle de l'écran : une
   table placée dans une colonne étroite débordait jusqu'ici en silence
   sur grand écran. Le repli @media couvre les tables sans conteneur
   ancêtre ; même seuil, donc comportement mobile inchangé. */
@container (max-width: 768px) {
  table { display: block; overflow-x: auto; }
}
@media (max-width: 768px) {
  table { display: block; overflow-x: auto; }
}


/* ============================================================
   9. BOUTONS
   ============================================================ */
:where(button, .btn) {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-inline-gap); padding: var(--space-button-padding-y) var(--space-button-padding-x);
  font-family: var(--font-ui); font-size: var(--base-text-sm); font-weight: var(--weight-ui);
  line-height: var(--base-leading-tight); letter-spacing: var(--tracking-ui);
  text-decoration: none; color: var(--text-on-action);
  background: var(--action-primary); border: 1px solid var(--border-action);
  border-radius: var(--layout-radius-button); cursor: pointer;
  transition: background var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast);
  user-select: none; white-space: nowrap; box-shadow: var(--shadow-button);
}
:where(button, .btn):hover { background: var(--action-primary-hover); box-shadow: var(--shadow-button-hover); }
:where(button, .btn):active { transform: scale(0.97); }

:is(button, .btn)[data-variant="secondary"] {
  background: var(--action-secondary); color: var(--text); border-color: var(--border); box-shadow: none;
}
:is(button, .btn)[data-variant="secondary"]:hover {
  background: var(--action-secondary-hover); border-color: var(--border-strong); box-shadow: none;
}
:is(button, .btn)[data-variant="ghost"] { background: transparent; color: var(--text-secondary); box-shadow: none; }
:is(button, .btn)[data-variant="ghost"]:hover { background: var(--action-secondary); color: var(--text); box-shadow: none; }
/* --text-on-danger et non --text-on-action : le rouge de --action-danger est clair en
   thème sombre, où seul un texte foncé reste lisible dessus (le blanc y
   tombe à 3,5:1). */
:is(button, .btn)[data-variant="danger"] { background: var(--action-danger); color: var(--text-on-danger); border-color: transparent; }
:is(button, .btn)[data-variant="danger"]:hover { background: var(--action-danger-hover); box-shadow: var(--shadow-glow-danger); }
:is(button, .btn)[data-variant="gradient"] { background: var(--brand-gradient); color: var(--text-on-action); border: none; box-shadow: var(--shadow-button-hover); }
:is(button, .btn)[data-variant="gradient"]:hover { box-shadow: var(--shadow-glow-accent); filter: brightness(1.1); }

:is(button, .btn)[data-size="sm"] { padding: var(--space-button-padding-y-sm) var(--space-button-padding-x-sm); font-size: var(--base-text-xs); border-radius: var(--layout-radius-button-sm); }
:is(button, .btn)[data-size="lg"] { padding: var(--space-button-padding-y-lg) var(--space-button-padding-x-lg); font-size: var(--base-text-md); border-radius: var(--layout-radius-button-lg); }

:is(button, .btn)[aria-label]:empty,
:is(button, .btn).icon { padding: var(--space-button-padding-y); aspect-ratio: 1; }

:is(button, .btn):disabled,
:is(button, .btn)[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; pointer-events: none; }

/* Lien habillé en bouton (<a class="btn">) : neutralise la couleur
   et le soulignement du style de lien, que les variantes ci-dessus
   re-précisent ensuite si besoin (spécificité supérieure). */
a.btn, a.btn:hover { color: var(--text-on-action); text-decoration: none; }


/* ============================================================
   10. FORMULAIRES
   ============================================================ */
label { display: block; font-size: var(--base-text-sm); font-weight: var(--base-weight-medium); color: var(--text-secondary); margin-bottom: var(--space-2xs); }

input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]),
textarea, select {
  display: block; width: 100%; padding: var(--space-input-padding-y) var(--space-input-padding-x);
  font-family: var(--font-ui); font-size: var(--base-text-sm); line-height: var(--base-leading-normal);
  color: var(--text); background: var(--surface-base);
  /* --border-field, pas --border : le fond du champ est celui de la page, donc
     la bordure est le SEUL indice visuel de la zone de saisie — elle doit
     tenir le 3:1 des composants d'interface (WCAG 1.4.11), là où --border
     (séparateurs) descend à 1,2:1. */
  border: 1px solid var(--border-field); border-radius: var(--layout-radius-input);
  transition: border-color var(--motion-fast), box-shadow var(--motion-fast);
}
:is(input, textarea, select)::placeholder { color: var(--text-tertiary); }
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]):hover,
textarea:hover, select:hover { border-color: var(--border-strong); }
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]):focus,
textarea:focus, select:focus { border-color: var(--border-focus); box-shadow: var(--shadow-focus-ring); outline: none; }
:is(input, textarea, select):disabled { opacity: 0.5; cursor: not-allowed; background: var(--action-secondary); }

textarea { min-height: 6rem; resize: vertical; }
/* Chevron dessiné en dégradés plutôt qu'en SVG data-URI : un data-URI
   fige sa couleur (elle ne peut pas lire var(), et currentColor n'y est
   pas hérité), si bien que la flèche restait le même gris bleuté dans
   les six thèmes et les deux modes — la seule couleur en dur du système,
   et la seule échappant au garde-fou (qui ignore le contenu des url()).
   Deux demi-carrés en biais forment le chevron ; ils suivent --text-tertiary.
   Un ::after serait plus simple, mais les pseudo-éléments ne sont pas
   rendus sur <select>, qui est un élément remplacé. */
select {
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--text-tertiary) 50%),
    linear-gradient(135deg, var(--text-tertiary) 50%, transparent 50%);
  background-position:
    right calc(var(--space-xs) + 5px) center,
    right var(--space-xs) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: var(--space-lg);
}
fieldset { border: 1px solid var(--border); border-radius: var(--layout-radius-card); padding: var(--space-fieldset-padding); margin-bottom: var(--space-group-gap); }
/* Groupement sémantique sans cadre visuel (ex. paire de radios
   dans un formulaire déjà encadré) — évite les styles inline. */
fieldset[data-variant="plain"] { border: none; padding: 0; margin: 0; }
legend { font-size: var(--base-text-sm); font-weight: var(--base-weight-semi); color: var(--text); padding: 0 var(--space-2xs); }

/* Switch */
input[type="checkbox"][role="switch"] {
  --toggle: 1.5rem;
  position: relative; width: calc(var(--toggle) * 1.8); height: var(--toggle);
  /* Piste éteinte en --border-field (le token calibré pour matérialiser un
     contrôle, ≥3:1 sur le fond) et non --border-strong : c'est le contraste
     entre la piste et le pommeau qui porte l'état on/off, et --border-strong
     est un gris de bordure trop clair pour un pommeau clair (1,5:1). */
  appearance: none; background: var(--border-field); border-radius: var(--layout-radius-badge);
  cursor: pointer; transition: background var(--motion-fast); border: none;
  vertical-align: middle;
}
input[type="checkbox"][role="switch"]::before {
  content: ''; position: absolute; top: 2px; left: 2px;
  width: calc(var(--toggle) - 4px); height: calc(var(--toggle) - 4px);
  background: var(--text-on-action); border-radius: var(--layout-radius-badge);
  transition: transform var(--motion-spring);
  box-shadow: var(--shadow-button);
}
input[type="checkbox"][role="switch"]:checked { background: var(--action-primary); }
input[type="checkbox"][role="switch"]:checked::before { transform: translateX(calc(var(--toggle) * 0.8)); }

/* Cible de pointage des contrôles compacts (WCAG 2.2 — 2.5.8) : la case
   dessinée fait 18px, sous le minimum de 24×24. Un pseudo-élément centré
   étend la zone cliquable à --layout-control-target sans changer le
   dessin ; l'écart de 8px entre deux contrôles empilés (--space-2xs)
   garantit que deux cibles ne se chevauchent pas. */
:is(input[type="checkbox"]:not([role]), input[type="radio"])::before {
  content: ''; position: absolute; top: 50%; left: 50%;
  width: var(--layout-control-target); height: var(--layout-control-target);
  transform: translate(-50%, -50%);
}

/* Checkbox */
input[type="checkbox"]:not([role]) {
  appearance: none; width: 1.125rem; height: 1.125rem;
  border: 2px solid var(--border-field); border-radius: var(--layout-radius-checkbox);
  cursor: pointer; transition: background var(--motion-fast), border-color var(--motion-fast);
  vertical-align: middle; position: relative;
}
input[type="checkbox"]:not([role]):checked { background: var(--action-primary); border-color: var(--action-primary); }
input[type="checkbox"]:not([role]):checked::after {
  content: ''; position: absolute; top: 1px; left: 4px;
  width: 5px; height: 9px; border: solid var(--text-on-action); border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

/* Radio */
input[type="radio"] {
  appearance: none; width: 1.125rem; height: 1.125rem;
  border: 2px solid var(--border-field); border-radius: var(--layout-radius-badge);
  cursor: pointer; transition: border-color var(--motion-fast);
  vertical-align: middle; position: relative;
}
input[type="radio"]:checked { border-color: var(--action-primary); }
input[type="radio"]:checked::after {
  content: ''; position: absolute; top: 3px; left: 3px;
  width: 8px; height: 8px; background: var(--action-primary); border-radius: var(--layout-radius-badge);
}

/* Range */
input[type="range"] {
  appearance: none; width: 100%; height: 6px;
  background: var(--action-secondary); border-radius: var(--layout-radius-badge); cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
  appearance: none; width: 20px; height: 20px;
  /* --border-focus et non --action-primary : le curseur ne porte pas de texte, il
     doit se détacher de sa piste (--action-secondary). --action-primary est calibré pour
     porter --text-on-action, ce qui le rapproche trop de la piste dans les
     thèmes sombres ; --border-focus est la teinte de marque calibrée pour
     rester visible sur une surface. */
  background: var(--border-focus); border-radius: var(--layout-radius-badge);
  border: 2px solid var(--surface-base); box-shadow: var(--shadow-button);
  cursor: pointer; transition: transform var(--motion-spring);
}
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.15); }

/* Firefox : règle séparée, jamais groupée avec le sélecteur -webkit-.
   Un sélecteur inconnu invalide TOUTE la règle qui le contient — les
   deux moteurs perdraient alors leur style. Sans ces trois lignes, le
   curseur gardait son apparence native sur Firefox, hors du thème. */
input[type="range"]::-moz-range-thumb {
  width: 20px; height: 20px;
  background: var(--border-focus); border-radius: var(--layout-radius-badge);
  border: 2px solid var(--surface-base); box-shadow: var(--shadow-button);
  cursor: pointer; transition: transform var(--motion-spring);
}
input[type="range"]::-moz-range-thumb:hover { transform: scale(1.15); }
input[type="range"]::-moz-range-track {
  height: 6px; background: var(--action-secondary); border-radius: var(--layout-radius-badge);
}

/* Color */
input[type="color"] {
  appearance: none; width: var(--layout-color-input-size); height: var(--layout-color-input-size);
  border: 2px solid var(--border); border-radius: var(--layout-radius-button);
  padding: 2px; cursor: pointer; background: none;
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
input[type="color"]::-webkit-color-swatch { border: none; border-radius: var(--layout-radius-button-sm); }

/* Contraste élevé (Windows High Contrast / forced-colors).
   Les contrôles en appearance:none perdent leurs fonds et ombres :
   sans repli, l'état coché d'une checkbox/switch/radio devient
   invisible. On rend l'état lisible avec les COULEURS SYSTÈME —
   les seules respectées dans ce mode (elles reflètent le thème
   choisi par l'utilisateur, pas la marque, donc hors périmètre du
   garde-fou couleurs). Highlight = sélection, Canvas/CanvasText =
   fond/texte. */
@media (forced-colors: active) {
  /* Checkbox : fond de sélection + coche contrastée */
  input[type="checkbox"]:not([role]):checked { background-color: Highlight; }
  input[type="checkbox"]:not([role]):checked::after { border-color: HighlightText; }

  /* Switch : piste et pommeau bornés visuellement dans les 2 états */
  input[type="checkbox"][role="switch"] { border: 1px solid CanvasText; }
  input[type="checkbox"][role="switch"]::before { background: CanvasText; }
  input[type="checkbox"][role="switch"]:checked { background: Highlight; }
  input[type="checkbox"][role="switch"]:checked::before { background: HighlightText; }

  /* Radio : pastille interne visible à l'état coché */
  input[type="radio"]:checked::after { background: Highlight; }

  /* Range : le curseur (thumb) reste visible malgré la perte d'ombre.
     Deux règles, une par moteur : groupées, un sélecteur inconnu les
     invaliderait toutes les deux. */
  input[type="range"]::-webkit-slider-thumb { background: Highlight; }
  input[type="range"]::-moz-range-thumb { background: Highlight; }
}


/* ============================================================
   11. PROGRESS & METER
   ============================================================ */
/* Firefox ne connaît pas ::-webkit-progress-bar : c'est le fond de
   l'élément lui-même qui fait office de piste. Le poser ici sert les
   deux moteurs (Chromium le recouvre avec ::-webkit-progress-bar). */
progress {
  appearance: none; width: 100%; height: 8px; border: none;
  background: var(--action-secondary);
  border-radius: var(--layout-radius-badge); overflow: hidden; margin-bottom: var(--space-stack-gap);
}
progress::-webkit-progress-bar { background: var(--action-secondary); border-radius: var(--layout-radius-badge); }
progress::-webkit-progress-value { background: var(--brand-gradient); border-radius: var(--layout-radius-badge); transition: width var(--motion-slow); }
progress::-moz-progress-bar { background: var(--brand-gradient); border-radius: var(--layout-radius-badge); }
progress[data-size="sm"] { height: 4px; }
progress[data-size="lg"] { height: 12px; }

meter {
  appearance: none; width: 100%; height: 8px; border: none;
  background: var(--action-secondary);
  border-radius: var(--layout-radius-badge); overflow: hidden; margin-bottom: var(--space-stack-gap);
}
meter::-webkit-meter-bar { background: var(--action-secondary); border-radius: var(--layout-radius-badge); border: none; }
meter::-webkit-meter-optimum-value { background: var(--status-success-foreground); border-radius: var(--layout-radius-badge); }
meter::-webkit-meter-suboptimum-value { background: var(--status-warning-foreground); border-radius: var(--layout-radius-badge); }
meter::-webkit-meter-even-less-good-value { background: var(--status-danger-foreground); border-radius: var(--layout-radius-badge); }

/* Firefox : la valeur passe par ::-moz-meter-bar, et le seuil atteint
   par les pseudo-classes :-moz-meter-* portées sur l'élément — sans
   quoi la jauge restait vert système, sans distinction d'état. */
meter::-moz-meter-bar { background: var(--status-success-foreground); border-radius: var(--layout-radius-badge); }
meter:-moz-meter-sub-optimum::-moz-meter-bar { background: var(--status-warning-foreground); }
meter:-moz-meter-sub-sub-optimum::-moz-meter-bar { background: var(--status-danger-foreground); }


/* ============================================================
   12. DETAILS / SUMMARY
   ============================================================ */
details {
  border: 1px solid var(--border); border-radius: var(--layout-radius-card);
  margin-bottom: var(--space-xs); overflow: hidden;
}
summary {
  padding: var(--space-details-padding); font-weight: var(--base-weight-semi);
  cursor: pointer; user-select: none;
  display: flex; align-items: center; justify-content: space-between;
  list-style: none; transition: background var(--motion-fast);
}
summary::-webkit-details-marker { display: none; }
summary::after {
  content: ''; width: 8px; height: 8px;
  border-right: 2px solid var(--text-tertiary); border-bottom: 2px solid var(--text-tertiary);
  transform: rotate(-45deg); transition: transform var(--motion-spring);
  flex-shrink: 0; margin-left: var(--space-stack-gap);
}
summary:hover { background: var(--action-secondary); }
details[open] summary::after { transform: rotate(45deg); }
details > :not(summary) { padding: 0 var(--space-details-padding-x) var(--space-details-padding-x); }

details + details { margin-top: calc(-1 * var(--space-xs)); border-top: none; border-top-left-radius: 0; border-top-right-radius: 0; }
details:has(+ details) { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }


/* ============================================================
   13. DIALOG
   ============================================================ */
dialog {
  border: 1px solid var(--border-muted); border-radius: var(--layout-radius-dialog);
  padding: var(--space-dialog-padding); background: var(--surface-base); color: var(--text);
  box-shadow: var(--shadow-dialog); max-width: min(90vw, 520px); width: 100%;
}
dialog::backdrop { background: var(--surface-backdrop); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
dialog header { margin-bottom: var(--space-stack-gap); }
dialog header :is(h2, h3) { margin-top: 0; margin-bottom: var(--space-3xs); }
/* <form method="dialog"> est la zone d'actions recommandée
   (fermeture native, valeur du bouton dans dialog.returnValue —
   voir HTML5-SEMANTIQUE.md) ; <footer> reste supporté. */
dialog :is(footer, form) { margin-top: var(--space-group-gap); display: flex; gap: var(--space-xs); justify-content: flex-end; }


/* ============================================================
   14. CARTES — .card
   ============================================================
   Classe, pas l'élément <article> nu : une carte est une décision
   de PRÉSENTATION, <article> une décision de SENS (contenu
   autonome — voir HTML5-SEMANTIQUE.md). Les deux se combinent
   librement : <article class="card"> pour un billet autonome
   présenté en carte, <div class="card"> pour un simple bloc
   visuel. Styler l'élément nu poussait à choisir la balise pour
   son apparence — l'anti-pattern exact que la charte interdit. */
.card {
  /* Conteneur de requête : ce qu'on place dans une carte s'adapte à la
     carte, pas à l'écran. Sa largeur vient de son parent (grille ou
     flux), le confinement inline est donc sans effet de bord. */
  container-type: inline-size;
  background: var(--surface-raised); border: 1px solid var(--border-muted);
  border-radius: var(--layout-radius-card); padding: var(--space-card-padding); margin-bottom: var(--space-stack-gap);
  transition: box-shadow var(--motion-normal), border-color var(--motion-normal), transform var(--motion-normal);
  box-shadow: var(--shadow-card);
}
.card[data-variant="elevated"] { box-shadow: var(--shadow-card-elevated); }
.card[data-variant="interactive"] { box-shadow: var(--shadow-card-interactive); cursor: pointer; }
.card[data-variant="interactive"]:hover { box-shadow: var(--shadow-card-interactive-hover); border-color: var(--border-strong); transform: translateY(-2px); }
.card[data-variant="glass"] {
  background: var(--surface-glass);
  backdrop-filter: blur(16px) saturate(180%); -webkit-backdrop-filter: blur(16px) saturate(180%);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-card-elevated);
}
/* Pas d'overflow:hidden ici : l'anneau du ::before est positionné en
   inset:-1px, donc à l'EXTÉRIEUR de la bordure — un overflow:hidden
   sur cet élément le découpe entièrement avant qu'il ne s'affiche. */
.card[data-variant="glow"] { position: relative; }
.card[data-variant="glow"]::before {
  content: ''; position: absolute; inset: -1px; border-radius: inherit; padding: 1px;
  background: var(--brand-gradient);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor; mask-composite: exclude;
  opacity: 0; transition: opacity var(--motion-normal); pointer-events: none;
}
.card[data-variant="glow"]:hover::before { opacity: 1; }

.card > :is(h1, h2, h3, h4):first-child { margin-top: 0; }


/* ============================================================
   15. ALERTES — .alert
   ============================================================
   Classe, pas l'élément <aside> nu : <aside> a un rôle ARIA
   implicite « complementary » (landmark) — styler l'élément nu
   transformait chaque alerte en landmark et poussait à choisir
   la balise pour son apparence. <aside> reste réservé au contenu
   tangentiel (voir HTML5-SEMANTIQUE.md) ; une alerte est un
   <div class="alert"> (+ role="status"/"alert" si insérée
   dynamiquement). */
/* Pas de display:flex : le contenu réel mélange texte et éléments
   inline (code, strong, a) sans wrapper — en flex, chaque bout de
   texte devient son propre item et le paragraphe se scinde en
   colonnes au lieu de se lire comme un flux normal. */
.alert {
  padding: var(--space-blockquote-padding); border-radius: var(--layout-radius-card);
  border: 1px solid var(--status-info-border); background: var(--status-info-background);
  color: var(--status-info-foreground); font-size: var(--base-text-sm);
  line-height: var(--base-leading-relaxed); margin-bottom: var(--space-stack-gap);
}
.alert[data-variant="success"] { background: var(--status-success-background); color: var(--status-success-foreground); border-color: var(--status-success-border); }
.alert[data-variant="warning"] { background: var(--status-warning-background); color: var(--status-warning-foreground); border-color: var(--status-warning-border); }
.alert[data-variant="danger"]  { background: var(--status-danger-background); color: var(--status-danger-foreground); border-color: var(--status-danger-border); }
.alert p { color: inherit; margin-bottom: var(--space-3xs); }
.alert p:last-child { margin-bottom: 0; }


/* ============================================================
   16. NAVIGATION
   ============================================================ */
nav ul { list-style: none; padding: 0; display: flex; gap: var(--space-3xs); margin-bottom: 0; }
/* :not(.btn) — sinon un lien habillé en bouton placé dans une barre de
   navigation (le CTA d'un header, cas le plus banal qui soit) se fait
   voler son style : « nav a » (0,0,2) bat le « :where(button, .btn) »
   des boutons, dont la spécificité est nulle par construction. Le
   padding rétrécissait à 8/12px au lieu de 10/20px, et « nav a:hover »
   repeignait un bouton primaire en gris au survol. */
nav a:not(.btn) {
  display: inline-flex; align-items: center; gap: var(--space-inline-gap);
  padding: var(--space-2xs) var(--space-xs); font-size: var(--base-text-sm); font-weight: var(--base-weight-medium);
  color: var(--text-secondary); border-radius: var(--layout-radius-button);
  transition: color var(--motion-fast), background var(--motion-fast); text-decoration: none;
}
nav a:not(.btn):hover { color: var(--text); background: var(--action-secondary); text-decoration: none; }

/* --text-link et non --action-primary : --action-primary est une couleur de FOND
   d'action, calibrée pour porter --text-on-action par-dessus ; utilisée
   comme texte sur --status-info-background, elle tombe sous 4,5:1 dans plusieurs
   thèmes. --text-link est la couleur de lien du thème, calibrée pour être
   lue sur une surface. */
nav a:not(.btn)[aria-current] { color: var(--text-link); background: var(--status-info-background); }

nav[data-variant="breadcrumb"] ul { gap: 0; }
nav[data-variant="breadcrumb"] li + li::before { content: '/'; padding: 0 var(--space-2xs); color: var(--text-tertiary); }
nav[data-variant="breadcrumb"] a:not(.btn) { padding: var(--space-3xs) 0; border-radius: 0; background: none; }


/* ============================================================
   17. STRUCTURE DE PAGE
   ============================================================ */
/* Lien d'évitement (WCAG 2.4.1) — premier élément focalisable de la
   page, hors écran jusqu'à ce qu'il reçoive le focus clavier. Le header
   étant sticky, un utilisateur au clavier traverse sinon toute la
   navigation avant d'atteindre le contenu. */
.skip-link {
  /* fixed, pas absolute : le focus peut arriver sur ce lien alors que la
     page est déjà défilée (Maj+Tab, retour arrière) — ancré au viewport,
     il reste visible dans tous les cas. */
  position: fixed; left: var(--space-2xs); top: var(--space-2xs);
  z-index: var(--layout-layer-skip);
  padding: var(--space-2xs) var(--space-sm);
  background: var(--action-primary); color: var(--text-on-action);
  border-radius: var(--layout-radius-button);
  font-size: var(--base-text-sm); font-weight: var(--weight-ui);
  box-shadow: var(--shadow-dialog);
  white-space: nowrap;
  transform: translateY(-200%);
  transition: transform var(--motion-fast);
}
/* :focus et non :focus-visible — ce lien n'est atteint qu'au clavier ou
   par script (focus programmatique après une navigation) : dans les deux
   cas il doit apparaître, or :focus-visible reste inactif sur un
   .focus() programmatique. */
.skip-link:focus { transform: translateY(0); color: var(--text-on-action); text-decoration: none; }

body > header {
  padding: var(--space-header-padding);
  border-bottom: 1px solid var(--border-muted);
  background: var(--surface-glass);
  backdrop-filter: blur(12px) saturate(180%);
  -webkit-backdrop-filter: blur(12px) saturate(180%);
  box-shadow: var(--shadow-header);
  min-height: var(--layout-header-min-height);
  position: sticky; top: 0; z-index: var(--layout-layer-header);
}
body > header > * { max-width: var(--layout-content-width); margin-inline: auto; width: 100%; }
/* Sous 768px, la barre logo + navigation (≈ 670px de contenu) ne
   tient plus sur une ligne. On laisse le nav et sa liste passer à la
   ligne, et on masque les ancres de section — redondantes sur mobile,
   où la page défile — pour garder un header sticky compact : la marque
   et le sélecteur de thème (la vraie fonctionnalité du catalogue)
   restent visibles. La hauteur variable est déjà suivie par le
   ResizeObserver (voir index.html) pour le scroll-margin des ancres. */
@media (max-width: 768px) {
  body > header nav { flex-wrap: wrap; row-gap: var(--space-2xs); }
  body > header nav ul { flex-wrap: wrap; justify-content: flex-end; }
  body > header nav li:has(> a[href^="#"]) { display: none; }
  body > header nav strong small { display: none; } /* tagline décorative : masquée pour un header sticky compact */
}
body > footer {
  padding: var(--space-footer-padding); border-top: 1px solid var(--border-muted);
  color: var(--text-tertiary); font-size: var(--base-text-sm); text-align: center;
}
/* <main> est conteneur de requête : c'est la racine du contenu, garantie
   par la charte HTML (règle single-main). Toute grille ou table de la
   page a donc un conteneur ancêtre, sans que l'auteur ait à y penser. */
body > main {
  container-type: inline-size;
  max-width: var(--layout-content-width); margin: 0 auto; padding: var(--space-main-padding);
}
section { margin-bottom: var(--space-section-gap); }
/* Le header est sticky : sans ça, une ancre (#tokens…) scrolle la
   section jusque sous le header, qui la recouvre. --header-height
   (mesuré en JS, voir index.html) suit la hauteur réelle du header —
   variable selon le contenu/viewport/thème — avec repli sur le token
   statique si le script n'a pas encore tourné (ou est désactivé). */
section[id] { scroll-margin-top: calc(var(--header-height, var(--layout-header-min-height)) + var(--space-sm)); }

figure { margin-bottom: var(--space-group-gap); border-radius: var(--layout-radius-card); overflow: hidden; }
figure :is(img, video) { display: block; width: 100%; height: auto; border-radius: var(--layout-radius-card); }
figcaption { padding: var(--space-xs) var(--space-3xs); font-size: var(--base-text-sm); color: var(--text-tertiary); text-align: center; }
img { max-width: 100%; height: auto; }


/* ============================================================
   18. TEXTE EN DÉGRADÉ — .text-gradient (couleur de marque)
   ============================================================ */
.text-gradient {
  background: var(--brand-gradient); -webkit-background-clip: text;
  -webkit-text-fill-color: transparent; background-clip: text;
}


/* ============================================================
   19. BADGES — .badge
   ============================================================ */
.badge {
  display: inline-flex; align-items: center; gap: var(--space-3xs);
  padding: var(--space-badge-padding-y) var(--space-badge-padding-x);
  font-size: var(--base-text-xs); font-weight: var(--base-weight-medium); line-height: var(--base-leading-snug);
  border-radius: var(--layout-radius-badge); border: 1px solid var(--border);
  background: var(--action-secondary); color: var(--text-secondary);
}
.badge[data-variant="primary"] { background: var(--status-primary-background); color: var(--status-primary-foreground); border-color: var(--status-primary-border); }
.badge[data-variant="success"] { background: var(--status-success-background); color: var(--status-success-foreground); border-color: var(--status-success-border); }
.badge[data-variant="warning"] { background: var(--status-warning-background); color: var(--status-warning-foreground); border-color: var(--status-warning-border); }
.badge[data-variant="danger"]  { background: var(--status-danger-background); color: var(--status-danger-foreground); border-color: var(--status-danger-border); }
.badge[data-variant="info"]    { background: var(--status-info-background); color: var(--status-info-foreground); border-color: var(--status-info-border); }


/* ============================================================
   20. AVATARS
   ============================================================ */
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--layout-avatar-size); height: var(--layout-avatar-size); border-radius: var(--layout-radius-avatar);
  background: var(--brand-gradient); color: var(--text-on-action);
  font-weight: var(--base-weight-bold); font-size: var(--base-text-sm);
  overflow: hidden; flex-shrink: 0;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar[data-size="sm"] { width: var(--layout-avatar-size-sm); height: var(--layout-avatar-size-sm); font-size: var(--base-text-xs); }
.avatar[data-size="lg"] { width: var(--layout-avatar-size-lg); height: var(--layout-avatar-size-lg); font-size: var(--base-text-lg); }

.avatar-group { display: flex; }
.avatar-group > .avatar { margin-left: -8px; border: 2px solid var(--surface-base); }
.avatar-group > .avatar:first-child { margin-left: 0; }

/* Palette de fantaisie fixe et universelle (pas l'identité de
   marque par défaut, qui elle suit --brand-gradient) — exception
   délibérée, documentée dans DOCUMENTATION.md. */
/* stylelint-disable design-system/no-raw-primitives */
.avatar[data-variant="warm"]   { background: var(--base-gradient-warm); }
.avatar[data-variant="cool"]   { background: var(--base-gradient-cool); }
.avatar[data-variant="aurora"] { background: var(--base-gradient-aurora); }
.avatar[data-variant="sunset"] { background: var(--base-gradient-sunset); }
/* stylelint-enable design-system/no-raw-primitives */


/* ============================================================
   21. SKELETON — aria-busy
   ============================================================ */
[aria-busy="true"],
.skeleton {
  background: linear-gradient(90deg, var(--action-secondary) 25%, var(--surface-raised) 50%, var(--action-secondary) 75%);
  background-size: 200% 100%;
  animation: ds-shimmer var(--motion-shimmer-duration) ease-in-out infinite;
  border-radius: var(--layout-radius-button-sm);
  color: transparent !important;
  pointer-events: none; user-select: none;
}
[aria-busy="true"] * { visibility: hidden; }

.skeleton          { display: block; height: 14px; }
.skeleton.wide     { width: 60%; }
.skeleton.narrow   { width: 40%; height: 12px; }
.skeleton.tag      { width: 80px; height: 28px; border-radius: var(--layout-radius-tag); }
.skeleton.tag-lg   { width: 100px; height: 28px; border-radius: var(--layout-radius-tag); }
.skeleton.block    { height: 70px; width: 100%; }


/* ============================================================
   22. UTILITAIRES DE VALEUR PONCTUELLE
   Valeurs ad hoc via variable CSS inline plutôt que des
   dizaines de classes data-bg="color-shade".
   ============================================================ */
[style*="--bg"]     { background: var(--bg); }
[style*="--shadow"] { box-shadow: var(--shadow); }
[style*="--radius"] { border-radius: var(--radius); }
[style*="--w"]      { width: var(--w); }


/* ============================================================
   23. PATTERNS COMPOSÉS
   ============================================================
   Les sections précédentes habillent des éléments HTML natifs ; celles
   qui suivent composent plusieurs éléments en un motif d'interface.
   Toutes tiennent SANS JavaScript — un motif qui en exigerait n'aurait
   pas sa place dans un système qui promet zéro dépendance runtime (voir
   DOCUMENTATION.md, « Patterns composés », pour les deux motifs
   volontairement absents et pourquoi).
   ============================================================ */

/* ── 23.1 Pagination ──
   Une liste de liens dans un <nav> nommé, la page courante portant
   aria-current="page" : l'information est dans le HTML, le CSS ne fait
   que la montrer. Les cibles respectent --layout-control-target. */
nav[data-variant="pagination"] ul {
  flex-wrap: wrap; justify-content: center; gap: var(--space-3xs);
}
nav[data-variant="pagination"] :is(a, span) {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--layout-control-target); min-height: var(--layout-control-target);
  padding: var(--space-3xs) var(--space-2xs);
  border-radius: var(--layout-radius-button);
  font-size: var(--base-text-sm); font-weight: var(--base-weight-medium);
}
/* Page courante : pleine, pas seulement colorée — le seul contraste de
   couleur ne suffirait pas à porter l'information (WCAG 1.4.1). */
nav[data-variant="pagination"] a:not(.btn)[aria-current="page"] {
  background: var(--action-primary); color: var(--text-on-action);
  border: var(--layout-border-width) solid var(--border-action);
}
/* Ellipse : décorative, retirée de l'arbre d'accessibilité côté HTML */
nav[data-variant="pagination"] span { color: var(--text-tertiary); }

/* ── 23.2 Infobulle ──
   Complément, jamais l'information essentielle : un pseudo-élément
   n'est pas restitué par tous les lecteurs d'écran. Pour une donnée
   indispensable, la mettre dans le flux (ou relier un texte visible via
   aria-describedby). Visible au survol ET au focus clavier — sans le
   second, l'infobulle n'existerait pas pour qui n'a pas de souris. */
[data-tooltip] { position: relative; }
[data-tooltip]::after {
  content: attr(data-tooltip);
  position: absolute; bottom: 100%; left: 50%;
  translate: -50% calc(-1 * var(--space-3xs));
  z-index: var(--layout-layer-tooltip);
  padding: var(--space-3xs) var(--space-2xs);
  background: var(--surface-inverse); color: var(--text-inverse);
  border-radius: var(--layout-radius-button-sm);
  font-size: var(--base-text-xs); font-weight: var(--base-weight-medium);
  line-height: var(--base-leading-snug);
  /* Largeur bornée avec retour à la ligne plutôt que white-space: nowrap.
     Centrée sur sa cible, une infobulle d'une seule ligne déborde de
     l'écran dès que la cible est près d'un bord (mesuré : texte tronqué
     sur un bouton à 24px du bord gauche). Le débordement n'est pas
     éliminé — seul le positionnement ancré CSS le ferait, et il n'est pas
     encore Baseline — mais il devient rare et lisible. */
  width: max-content; max-width: min(28ch, 90vw); text-align: center;
  /* white-space est héritée : sur un bouton, qui pose nowrap, l'infobulle
     héritait de l'interdiction de couper — le texte sortait de la boîte
     en une ligne interminable au lieu de s'y replier. */
  white-space: normal;
  box-shadow: var(--shadow-dialog);
  opacity: 0; pointer-events: none;
  transition: opacity var(--motion-fast);
}
[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::after { opacity: 1; }

/* ── 23.3 Notification ──
   .toast-region est la zone d'annonce (role="status" côté HTML, pour
   que l'ajout d'un message soit annoncé sans voler le focus) ; .toast
   est le message. L'insertion relève de l'application — le design
   system fournit la place et l'habillage, pas le déclencheur. */
.toast-region {
  position: fixed; inset-block-end: var(--space-sm); inset-inline-end: var(--space-sm);
  z-index: var(--layout-layer-toast);
  display: flex; flex-direction: column; gap: var(--space-2xs);
  max-width: min(90vw, var(--layout-toast-width));
  pointer-events: none;                    /* la zone ne bloque pas la page… */
}
.toast {
  pointer-events: auto;                    /* …chaque message reste cliquable */
  display: flex; align-items: flex-start; gap: var(--space-2xs);
  padding: var(--space-xs) var(--space-sm);
  background: var(--surface-raised); color: var(--text);
  border: var(--layout-border-width) solid var(--border);
  border-radius: var(--layout-radius-card);
  box-shadow: var(--shadow-dialog);
  font-size: var(--base-text-sm); line-height: var(--leading-body);
  animation: ds-slide-in-right var(--motion-normal) both;
}
.toast[data-variant="success"] { border-color: var(--status-success-border); background: var(--status-success-background); color: var(--status-success-foreground); }
.toast[data-variant="warning"] { border-color: var(--status-warning-border); background: var(--status-warning-background); color: var(--status-warning-foreground); }
.toast[data-variant="danger"]  { border-color: var(--status-danger-border); background: var(--status-danger-background); color: var(--status-danger-foreground); }
.toast[data-variant="info"]    { border-color: var(--status-info-border); background: var(--status-info-background); color: var(--status-info-foreground); }

/* ── 23.4 Panneau latéral ──
   Un <dialog> ouvert par .showModal() : le navigateur fournit déjà le
   piège de focus, la fermeture par Échap et le voile. Seule la
   géométrie change — d'où une variante plutôt qu'un composant. */
dialog[data-variant="drawer"] {
  max-width: min(90vw, var(--layout-drawer-width));
  height: 100dvh; max-height: 100dvh;
  margin: 0 0 0 auto;                      /* collé au bord de fin */
  border-radius: 0;
  animation: ds-slide-in-right var(--motion-normal) both;
}
dialog[data-variant="drawer"][data-side="start"] {
  margin-inline: 0 auto;
  animation-name: ds-fade-in;              /* l'entrée par la droite n'aurait pas de sens */
}

/* ── 23.5 État vide ──
   Ce qu'on montre quand il n'y a rien à montrer : un titre, une
   explication, et une action pour en sortir. */
.empty-state {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--space-2xs);
  padding: var(--space-xl) var(--space-md);
  text-align: center; color: var(--text-secondary);
}
.empty-state > :is(h1, h2, h3, h4, h5, h6) { margin: 0; color: var(--text); }
.empty-state > p { margin: 0; max-width: 45ch; }
/* Pictogramme décoratif : le sens vit dans le titre, pas dans l'icône */
.empty-state > [aria-hidden="true"] { font-size: var(--base-text-4xl); line-height: var(--base-leading-none); }

/* ── 23.6 Indicateur d'activité ──
   Pour une attente sans progression connue (sinon, <progress>). Le
   squelette (section 21) reste préférable quand on connaît la forme du
   contenu à venir. */
/* Piste transparente, arc visible sur deux bords : une piste colorée
   demanderait de contraster avec l'arc, or deux couleurs qui contrastent
   chacune avec le fond peuvent être identiques entre elles — en thème
   Puissant, --border et --action-primary le sont (rapport 1,00 : rien ne semblait
   tourner). Sans piste, la seule exigence est que l'arc se détache du
   fond. Et --border-focus plutôt que --action-primary, pour la même raison que le
   curseur de range : c'est la teinte de marque calibrée pour rester
   visible sur une surface, quand --action-primary l'est pour porter du texte. */
.spinner {
  display: inline-block;
  /* flex-shrink: 0 comme .avatar : placé dans un conteneur flex un peu
     serré, un cercle sans contenu se laisse écraser jusqu'à la largeur
     de ses bordures (mesuré : 4px de large pour 24 de haut). */
  flex-shrink: 0;
  width: var(--layout-control-target); height: var(--layout-control-target);
  border: 2px solid transparent;
  border-block-start-color: var(--border-focus);
  border-inline-end-color: var(--border-focus);
  border-radius: var(--layout-radius-badge);
  animation: ds-spin var(--motion-spin-duration) linear infinite;
}
.spinner[data-size="sm"] { width: var(--space-sm); height: var(--space-sm); border-width: 2px; }
.spinner[data-size="lg"] { width: var(--space-lg); height: var(--space-lg); border-width: 3px; }

@keyframes ds-spin { to { rotate: 360deg; } }

/* Le bloc prefers-reduced-motion global fige toute animation à une
   itération : un indicateur d'activité immobile mentirait sur l'état du
   système. On lui rend une pulsation d'opacité, perceptible sans
   mouvement, et on la laisse boucler. */
@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation: ds-pulse-opacity var(--motion-spin-duration) ease-in-out infinite !important;
    animation-iteration-count: infinite !important;
  }
}
@keyframes ds-pulse-opacity { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
