/* COPIE du systeme de design d'AvoCare Desktop — ne pas modifier ici.
   Source   : AvoCareDesktop, branche trunk, commit 5e5b2fee63d63dd3a30bbb6c32a8331ef22590f5 (2026-09-26)
   Fichier  : static/ds-components.css (blob 7db436ac080406bff20db4b788258bfd3d30b506), lignes 1 a 1087 :
              sections 1 a 8 (boutons, champs, cases, tableaux, badges, retours, structure, entete de page,
              accessibilite), recopiees a l'identique. Le reste (menus de ligne, listes du poste) ne sert pas au portail.
   Document : docs/pilot-v8/09_UI_DESIGN_SYSTEM.md du meme commit */

/* ===========================================================================
   AvoCare — composants (phase H)
   ===========================================================================

   Chargé APRÈS ds.css. Ne contient que des composants construits sur les
   jetons de ds-tokens.css : aucune valeur littérale de couleur, de rayon ou
   de hauteur n'apparaît ici.

   Les définitions reprennent les noms de classes DÉJÀ employés par les 46
   gabarits (`.btn`, `.btn-primary`, `.form-input`, `.badge`, …). C'est
   délibéré : renommer aurait imposé de toucher 370 boutons avant qu'un seul
   écran ne s'améliore. En gardant les noms, tout le parc bénéficie
   immédiatement des états manquants — focus visible, désactivé, chargement —
   et les nouveaux composants s'ajoutent à côté.
   =========================================================================== */

/* ===========================================================================
   1. BOUTONS
   ---------------------------------------------------------------------------
   Ce qui manquait, et que l'inventaire a relevé : aucun `:focus-visible`,
   aucun état désactivé, aucun état de chargement, `.btn-warning` strictement
   identique à `.btn-primary`, et un `transform: translateY(-1px)` au survol
   sans garde de mouvement réduit.
   =========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-2);
  min-height: var(--ds-control-h);
  padding: 0 var(--ds-space-4);
  border: 1px solid transparent;
  border-radius: var(--ds-radius);
  font-family: inherit;
  font-size: var(--ds-text-sm);
  font-weight: var(--ds-weight-semi);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  position: relative;
  transition: background-color var(--ds-transition),
              border-color var(--ds-transition),
              color var(--ds-transition),
              box-shadow var(--ds-transition);
}

/* Toutes les icônes d'un bouton ont la même taille. Sans cela, deux boutons
   côte à côte n'ont pas la même hauteur optique. */
.btn > i,
.btn > svg {
  font-size: var(--ds-icon-size);
  flex: 0 0 auto;
}

/* Le focus doit se voir, et se voir SEULEMENT au clavier : `:focus-visible`
   évite l'anneau disgracieux après un clic souris tout en le garantissant à
   qui navigue au clavier. */
.btn:focus-visible,
.ds-field:focus-visible,
.form-input:focus-visible,
.ds-check input:focus-visible + .ds-check-box {
  outline: 2px solid transparent;
  outline-offset: 2px;
  box-shadow: var(--ds-focus-ring);
}

.btn:disabled,
.btn[aria-disabled="true"],
.btn.is-disabled {
  opacity: 0.55;
  cursor: not-allowed;
  box-shadow: none;
  pointer-events: none;
}

/* ── Variantes ──────────────────────────────────────────────────────────
   Une seule action principale par zone. Les autres ne doivent pas lui
   disputer l'attention. */
/* Principal : marine, icône en or clair — l'or reste un accent discret. */
.btn-primary {
  background: var(--ds-primary);
  color: var(--ds-primary-fg);
  border-color: var(--ds-primary);
}
.btn-primary > i,
.btn-primary > svg {
  color: var(--ds-btn-icon);
}
.btn-primary:hover:not(:disabled) {
  background: var(--ds-primary-hover);
  border-color: var(--ds-primary-hover);
}
.btn-primary:active:not(:disabled) {
  background: var(--ds-navy);
  border-color: var(--ds-navy);
}

.btn-secondary {
  background: var(--ds-card);
  color: var(--ds-text);
  border-color: var(--ds-border-strong);
}
.btn-secondary:hover:not(:disabled) {
  background: var(--ds-surface-2);
  border-color: var(--ds-text-3);
}

.btn-ghost {
  background: transparent;
  color: var(--ds-text-2);
  border-color: var(--ds-border);
}
.btn-ghost:hover:not(:disabled) {
  background: var(--ds-surface-2);
  color: var(--ds-text);
  border-color: var(--ds-border-strong);
}

/* Une action destructive ne ressemble JAMAIS à une action ordinaire. C'est la
   seule variante dont la couleur porte à elle seule un avertissement — d'où
   l'anneau de focus rouge, cohérent avec elle. */
.btn-danger {
  background: var(--ds-danger);
  color: var(--ds-text-inverse);
  border-color: var(--ds-danger);
}
.btn-danger:hover:not(:disabled) {
  background: var(--ds-danger-hover);
  border-color: var(--ds-danger-hover);
}
.btn-danger:focus-visible {
  box-shadow: var(--ds-focus-ring-danger);
}

.btn-success {
  background: var(--ds-success);
  color: var(--ds-text-inverse);
  border-color: var(--ds-success);
}
.btn-success:hover:not(:disabled) {
  filter: brightness(0.92);
}

/* `.btn-warning` était strictement identique à `.btn-primary` — même bleu
   nuit, même survol. Une variante qui ne varie pas est un piège : on la
   choisit en croyant signaler quelque chose. Elle porte maintenant la couleur
   d'attention. */
.btn-warning {
  background: var(--ds-warn-bg);
  color: var(--ds-warn);
  border-color: var(--ds-warn-border);
}
.btn-warning:hover:not(:disabled) {
  background: var(--ds-warn-border);
}

.btn-link {
  background: transparent;
  border-color: transparent;
  color: var(--ds-accent);
  padding-inline: var(--ds-space-1);
  min-height: auto;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.btn-link:hover:not(:disabled) {
  color: var(--ds-accent-hover);
}

/* ── Tailles ────────────────────────────────────────────────────────────*/
.btn-sm {
  min-height: var(--ds-control-h-compact);
  padding: 0 var(--ds-space-3);
  font-size: var(--ds-text-xs);
}
.btn-lg {
  min-height: var(--ds-control-h-large);
  padding: 0 var(--ds-space-5);
  font-size: var(--ds-text-base);
}

/* ── Bouton icône ───────────────────────────────────────────────────────
   Carré, sans texte visible. Il DOIT porter un nom accessible : le gabarit
   fournit `aria-label`, et le contrôle de qualité vérifie qu'aucun bouton
   icône n'en est dépourvu. Une icône seule ne dit rien à un lecteur d'écran. */
.btn-icon {
  width: var(--ds-icon-btn);
  min-width: var(--ds-icon-btn);
  height: var(--ds-icon-btn);
  min-height: var(--ds-icon-btn);
  padding: 0;
}
.btn-icon.btn-sm {
  width: var(--ds-icon-btn-compact);
  min-width: var(--ds-icon-btn-compact);
  height: var(--ds-icon-btn-compact);
  min-height: var(--ds-icon-btn-compact);
}

/* ── Chargement ─────────────────────────────────────────────────────────
   Le texte reste EN PLACE et garde sa largeur : il devient seulement
   transparent. Le remplacer par « Chargement… » ferait sauter la mise en page
   au moment précis où l'utilisateur attend un retour. */
.btn.is-loading {
  color: transparent !important;
  pointer-events: none;
  position: relative;
}
.btn.is-loading > i,
.btn.is-loading > svg,
.btn.is-loading > span {
  visibility: hidden;
}
.btn.is-loading::after {
  content: "";
  position: absolute;
  inset-block-start: calc(50% - 0.5em);
  inset-inline-start: calc(50% - 0.5em);
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-block-start-color: transparent;
  border-radius: 50%;
  color: var(--ds-text-inverse);
  animation: ds-spin var(--ds-duration-spin) linear infinite;
}
.btn-secondary.is-loading::after,
.btn-ghost.is-loading::after,
.btn-warning.is-loading::after {
  color: var(--ds-text-2);
}

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

/* ── Groupe de boutons ──────────────────────────────────────────────────
   Les rayons intérieurs disparaissent, dans les deux sens de lecture. */
.ds-btn-group {
  display: inline-flex;
}
.ds-btn-group > .btn:not(:first-child) {
  border-start-start-radius: 0;
  border-end-start-radius: 0;
  margin-inline-start: -1px;
}
.ds-btn-group > .btn:not(:last-child) {
  border-start-end-radius: 0;
  border-end-end-radius: 0;
}
.ds-btn-group > .btn:hover,
.ds-btn-group > .btn:focus-visible {
  z-index: 1;
}

/* ===========================================================================
   2. CHAMPS
   =========================================================================== */

.ds-field-group {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-1);
  min-width: 0;
}

.form-label,
.ds-label {
  display: block;
  font-size: var(--ds-text-sm);
  font-weight: var(--ds-weight-semi);
  color: var(--ds-text);
  margin-block-end: var(--ds-space-1);
}

/* L'astérisque du champ obligatoire est doublée d'un texte pour les lecteurs
   d'écran : une couleur et un signe ne suffisent pas à porter l'information. */
.ds-label-required::after {
  content: " *";
  color: var(--ds-danger);
}

.form-input,
.ds-field {
  width: 100%;
  min-height: var(--ds-control-h);
  padding: 0 var(--ds-space-3);
  border: 1px solid var(--ds-border-strong);
  border-radius: var(--ds-radius);
  background: var(--ds-card);
  color: var(--ds-text);
  font-family: inherit;
  font-size: var(--ds-text-base);
  outline: none;
  transition: border-color var(--ds-transition), box-shadow var(--ds-transition);
}

textarea.form-input,
textarea.ds-field {
  min-height: calc(var(--ds-control-h) * 2.5);
  padding-block: var(--ds-space-2);
  line-height: var(--ds-leading-normal);
  resize: vertical;
}

.form-input:hover:not(:disabled):not([readonly]),
.ds-field:hover:not(:disabled):not([readonly]) {
  border-color: var(--ds-text-3);
}

.form-input:focus,
.ds-field:focus {
  border-color: var(--ds-focus);
  box-shadow: var(--ds-focus-ring);
}

.form-input:disabled,
.ds-field:disabled {
  background: var(--ds-surface-3);
  color: var(--ds-text-3);
  cursor: not-allowed;
}

.form-input[readonly],
.ds-field[readonly] {
  background: var(--ds-surface-2);
  color: var(--ds-text-2);
}

/* Champ en erreur : bordure ET message. La bordure seule ne dit pas ce qui
   ne va pas, et un daltonien ne la distingue pas de l'état normal. */
.ds-field.is-invalid,
.form-input.is-invalid,
.ds-field[aria-invalid="true"] {
  border-color: var(--ds-danger);
}
.ds-field.is-invalid:focus,
.form-input.is-invalid:focus {
  box-shadow: var(--ds-focus-ring-danger);
}

.ds-field-help {
  font-size: var(--ds-text-xs);
  color: var(--ds-text-2);
}

/* Aide en INFOBULLE (décision du 2026-09-22). Un repère « i » suit le libellé ;
   le texte apparaît au survol ou au focus du champ, sous le libellé, sans
   pousser la mise en page. Il reste dans le document : aria-describedby
   l'annonce même masqué. */
.ds-field-group.has-help {
  position: relative;
}
.ds-field-group.has-help > .ds-label::after {
  content: "i";
  display: inline-grid;
  place-items: center;
  inline-size: 1.1em;
  block-size: 1.1em;
  margin-inline-start: var(--ds-space-1);
  border: 1px solid var(--ds-border-strong);
  border-radius: var(--ds-radius-pill);
  color: var(--ds-text-3);
  font-size: 0.8em;
  font-weight: var(--ds-weight-bold);
  font-style: normal;
  line-height: 1;
  vertical-align: 0.1em;
}
.ds-field-group.has-help > .ds-label-required::after {
  content: " *";
  display: inline;
  border: 0;
  margin: 0;
  inline-size: auto;
  block-size: auto;
  color: var(--ds-danger);
  font-size: inherit;
}
.ds-field-group.has-help > .ds-field-help {
  position: absolute;
  inset-block-start: 100%;
  inset-inline-start: 0;
  z-index: var(--ds-z-topbar);
  max-inline-size: min(36ch, 100%);
  margin-block-start: var(--ds-space-1);
  padding: var(--ds-space-2) var(--ds-space-3);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-navy);
  color: var(--ds-text-inverse);
  box-shadow: var(--ds-shadow);
  line-height: var(--ds-leading-normal);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity var(--ds-transition), visibility var(--ds-transition);
}
.ds-field-group.has-help:hover > .ds-field-help,
.ds-field-group.has-help:focus-within > .ds-field-help {
  opacity: 1;
  visibility: visible;
}

.ds-field-error {
  display: flex;
  align-items: flex-start;
  gap: var(--ds-space-1);
  font-size: var(--ds-text-xs);
  font-weight: var(--ds-weight-medium);
  color: var(--ds-danger);
}

/* Champ avec icône ou suffixe : l'espace réservé suit le sens de lecture. */
.ds-field-wrap {
  position: relative;
  display: flex;
  align-items: center;
  min-width: 0;
}
.ds-field-wrap > .ds-field-icon {
  position: absolute;
  inset-inline-start: var(--ds-space-3);
  color: var(--ds-text-3);
  font-size: var(--ds-icon-size);
  pointer-events: none;
}
.ds-field-wrap.has-icon .form-input,
.ds-field-wrap.has-icon .ds-field {
  padding-inline-start: calc(var(--ds-space-3) * 2 + var(--ds-icon-size));
}
.ds-field-suffix {
  position: absolute;
  inset-inline-end: var(--ds-space-3);
  font-size: var(--ds-text-xs);
  color: var(--ds-text-3);
  pointer-events: none;
}
.ds-field-wrap.has-suffix .form-input,
.ds-field-wrap.has-suffix .ds-field {
  padding-inline-end: calc(var(--ds-space-6) + var(--ds-space-3));
}

/* ===========================================================================
   3. CASES À COCHER, RADIOS, INTERRUPTEURS
   ---------------------------------------------------------------------------
   Le contrôle natif est CONSERVÉ et rendu transparent par-dessus la boîte
   dessinée : le clavier, le lecteur d'écran, l'état indéterminé et la
   validation de formulaire continuent de fonctionner sans une ligne de
   JavaScript. Un composant maison aurait dû réimplémenter tout cela, et
   l'aurait mal fait.
   =========================================================================== */

.ds-check {
  display: flex;
  align-items: flex-start;
  gap: var(--ds-space-2);
  position: relative;
  cursor: pointer;
  padding-block: var(--ds-space-1);
  min-width: 0;
}
.ds-check.is-disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

/* Le contrôle natif couvre exactement la boîte : c'est lui qui reçoit le
   clic et le focus. */
.ds-check > input {
  position: absolute;
  width: 18px;
  height: 18px;
  margin: 0;
  opacity: 0;
  cursor: inherit;
  inset-inline-start: 0;
  inset-block-start: var(--ds-space-1);
}

.ds-check-box {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin-block-start: 1px;
  border: 1.5px solid var(--ds-border-strong);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-card);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: background-color var(--ds-transition),
              border-color var(--ds-transition);
}
.ds-check-radio .ds-check-box {
  border-radius: 50%;
}

/* Le glyphe de coche est dessiné en CSS : il porte l'information par la
   FORME, pas seulement par la couleur de fond. */
.ds-check-box::after {
  content: "";
  width: 10px;
  height: 6px;
  border: 2px solid var(--ds-text-inverse);
  border-block-start: none;
  border-inline-end: none;
  transform: rotate(-45deg) translate(1px, -1px);
  opacity: 0;
  transition: opacity var(--ds-duration-fast) var(--ds-ease);
}
.ds-check-radio .ds-check-box::after {
  width: 8px;
  height: 8px;
  border: none;
  border-radius: 50%;
  background: var(--ds-text-inverse);
  transform: none;
}

.ds-check > input:checked + .ds-check-box {
  background: var(--ds-accent);
  border-color: var(--ds-accent);
}
.ds-check > input:checked + .ds-check-box::after {
  opacity: 1;
}

/* État indéterminé : un trait, pas une coche. Il se distingue donc du coché
   sans dépendre de la couleur. */
.ds-check > input:indeterminate + .ds-check-box {
  background: var(--ds-accent);
  border-color: var(--ds-accent);
}
.ds-check > input:indeterminate + .ds-check-box::after {
  opacity: 1;
  width: 10px;
  height: 0;
  border: none;
  border-block-start: 2px solid var(--ds-text-inverse);
  transform: none;
}

.ds-check > input:focus-visible + .ds-check-box {
  box-shadow: var(--ds-focus-ring);
}
.ds-check > input:disabled + .ds-check-box {
  background: var(--ds-surface-3);
  border-color: var(--ds-border);
}

/* Toute la ligne est cliquable — le libellé fait partie du `<label>`. */
.ds-check-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  font-size: var(--ds-text-base);
  color: var(--ds-text);
  line-height: var(--ds-leading-tight);
}
.ds-check-desc {
  font-size: var(--ds-text-xs);
  color: var(--ds-text-2);
  line-height: var(--ds-leading-normal);
}

/* Interrupteur : même contrôle natif, autre habillage. Réservé aux réglages
   qui s'appliquent immédiatement ; un formulaire à valider utilise une case. */
.ds-switch .ds-check-box {
  width: 34px;
  height: 19px;
  border-radius: var(--ds-radius-pill);
  justify-content: flex-start;
  padding: 2px;
}
.ds-switch .ds-check-box::after {
  content: "";
  width: 13px;
  height: 13px;
  border: none;
  border-radius: 50%;
  background: var(--ds-text-3);
  opacity: 1;
  transform: none;
  transition: transform var(--ds-transition), background-color var(--ds-transition);
}
.ds-switch > input:checked + .ds-check-box::after {
  background: var(--ds-text-inverse);
  transform: translateX(15px);
}
[dir="rtl"] .ds-switch > input:checked + .ds-check-box::after {
  transform: translateX(-15px);
}

/* ===========================================================================
   4. TABLEAUX
   ---------------------------------------------------------------------------
   Un tableau large défile DANS SON CONTENEUR. Laisser la page entière défiler
   horizontalement emporte la barre latérale et l'en-tête avec elle.
   =========================================================================== */

.ds-table-wrap {
  width: 100%;
  overflow-x: auto;
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  background: var(--ds-card);
  /* Sans cela, un tableau large pousse la mise en page au lieu de défiler. */
  min-width: 0;
}

.ds-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--ds-text-base);
}

.ds-table thead th {
  position: sticky;
  inset-block-start: 0;
  z-index: 1;
  background: var(--ds-surface-2);
  color: var(--ds-text-2);
  font-size: var(--ds-text-xs);
  font-weight: var(--ds-weight-semi);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  text-align: start;
  padding: var(--ds-space-2) var(--ds-space-3);
  border-block-end: 1px solid var(--ds-border);
  white-space: nowrap;
}

.ds-table tbody td {
  padding: var(--ds-space-2) var(--ds-space-3);
  color: var(--ds-text);
  border-block-end: 1px solid var(--ds-border-light);
  vertical-align: middle;
}

/* Hauteur de ligne CONSTANTE. L'inventaire a relevé des lignes plus hautes
   que les autres selon la présence d'une seconde ligne de texte : la densité
   du tableau variait alors avec les données. */
.ds-table tbody tr {
  height: var(--ds-control-h-large);
}
.ds-table tbody tr:hover {
  background: var(--ds-surface-2);
}
.ds-table tbody tr:last-child td {
  border-block-end: none;
}

.ds-table.is-compact tbody tr {
  height: var(--ds-control-h);
}
.ds-table.is-compact tbody td,
.ds-table.is-compact thead th {
  padding-block: var(--ds-space-1);
}

/* Colonnes numériques : alignées sur la fin, chiffres de chasse fixe. */
.ds-table .ds-col-num,
.ds-table th.ds-col-num,
.ds-table td.ds-col-num {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

.ds-table .ds-col-actions {
  width: 1%;
  white-space: nowrap;
  text-align: end;
}

/* Les actions de ligne restent visibles. L'inventaire a relevé qu'elles
   étaient toutes masquées derrière un menu, y compris la plus fréquente. */
.ds-row-actions {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-1);
  justify-content: flex-end;
}

.ds-table th.is-sortable {
  cursor: pointer;
  user-select: none;
}
.ds-table th.is-sortable:hover {
  color: var(--ds-text);
}

/* ===========================================================================
   5. BADGES ET STATUTS
   ---------------------------------------------------------------------------
   Les badges existants portaient un nom de COULEUR (`badge-red`,
   `badge-gold`). Changer la couleur d'un statut imposait alors de modifier le
   nom de classe à chaque site d'appel. Les tons ci-dessous portent un SENS ;
   le registre des statuts, côté serveur, fait la correspondance.
   =========================================================================== */

.ds-badge,
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-1);
  padding: 2px var(--ds-space-2);
  border: 1px solid transparent;
  border-radius: var(--ds-radius-sm);
  font-size: var(--ds-text-xs);
  font-weight: var(--ds-weight-semi);
  line-height: 1.6;
  white-space: nowrap;
}

.ds-badge > i {
  font-size: 0.75em;
}

/* Tons sémantiques. Chaque badge combine fond, texte ET bordure : sur un
   écran en niveaux de gris, la bordure reste distinctive. */
.ds-badge-neutral  { background: var(--ds-surface-2);  color: var(--ds-text-2);   border-color: var(--ds-border); }
.ds-badge-success  { background: var(--ds-success-bg); color: var(--ds-success);  border-color: var(--ds-success-border); }
.ds-badge-warn     { background: var(--ds-warn-bg);    color: var(--ds-warn);     border-color: var(--ds-warn-border); }
.ds-badge-danger   { background: var(--ds-danger-bg);  color: var(--ds-danger);   border-color: var(--ds-danger-border); }
.ds-badge-info     { background: var(--ds-info-bg);    color: var(--ds-info);     border-color: var(--ds-info-border); }
.ds-badge-accent   { background: var(--ds-gold-dim);   color: var(--ds-gold-text);border-color: var(--ds-gold-light); }
.ds-badge-category { background: var(--ds-category-bg); color: var(--ds-category); border-color: var(--ds-category-border); }

/* Anciens noms, conservés pour ne pas casser 245 sites d'appel. Ils lisent
   désormais les mêmes jetons. */
.badge-green  { background: var(--ds-success-bg); color: var(--ds-success); }
.badge-red    { background: var(--ds-danger-bg);  color: var(--ds-danger); }
.badge-gray   { background: var(--ds-surface-2);  color: var(--ds-text-2); }
.badge-yellow { background: var(--ds-warn-bg);    color: var(--ds-warn); }
.badge-blue   { background: var(--ds-info-bg);    color: var(--ds-info); }
.badge-gold   { background: var(--ds-gold-dim);   color: var(--ds-gold-text); }
.badge-purple { background: var(--ds-category-bg); color: var(--ds-category); }

/* Pastille d'état : un point coloré devant le libellé. Le point porte le ton
   d'un coup d'œil ; le libellé, le sens — la couleur n'est jamais seule. */
/* inline-block : dans une cellule de tableau fixe, la pastille passe en
   inline-block (troncature) et un point en ligne n'aurait plus de taille. */
.ds-dot::before {
  content: "";
  display: inline-block;
  vertical-align: 0.1em;
  margin-inline-end: var(--ds-space-1);
  flex: 0 0 auto;
  inline-size: 6px;
  block-size: 6px;
  border-radius: var(--ds-radius-pill);
  background: var(--ds-neutral-dot);
}
.ds-badge.ds-dot,
.badge.ds-dot { border-color: transparent; gap: 0; }
.ds-badge-success.ds-dot::before, .badge-green.ds-dot::before  { background: var(--ds-success-dot); }
.ds-badge-warn.ds-dot::before,    .badge-yellow.ds-dot::before { background: var(--ds-warn-dot); }
.ds-badge-danger.ds-dot::before,  .badge-red.ds-dot::before    { background: var(--ds-danger-dot); }
.ds-badge-info.ds-dot::before,    .badge-blue.ds-dot::before   { background: var(--ds-info-dot); }
.ds-badge-category.ds-dot::before,.badge-purple.ds-dot::before { background: var(--ds-category-dot); }
.ds-badge-accent.ds-dot::before,  .badge-gold.ds-dot::before   { background: var(--ds-gold); }
.ds-badge-neutral.ds-dot,
.badge-gray.ds-dot { background: var(--ds-neutral-bg); color: var(--ds-neutral); }

/* ===========================================================================
   6. RETOURS À L'UTILISATEUR
   ---------------------------------------------------------------------------
   Un message doit dire ce qui s'est passé, ce que cela signifie, et ce que
   l'utilisateur peut faire ensuite. La structure ci-dessous réserve une place
   à chacun des trois.
   =========================================================================== */

.ds-alert {
  display: flex;
  align-items: flex-start;
  gap: var(--ds-space-3);
  padding: var(--ds-space-3) var(--ds-space-4);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius);
  background: var(--ds-card);
  font-size: var(--ds-text-base);
  color: var(--ds-text);
}
.ds-alert > i {
  font-size: var(--ds-icon-size);
  margin-block-start: 2px;
  flex: 0 0 auto;
}
.ds-alert-body { flex: 1 1 auto; min-width: 0; }
.ds-alert-title {
  font-weight: var(--ds-weight-semi);
  margin-block-end: 2px;
}
.ds-alert-text {
  color: var(--ds-text-2);
  font-size: var(--ds-text-sm);
  line-height: var(--ds-leading-normal);
}
.ds-alert-actions {
  display: flex;
  gap: var(--ds-space-2);
  margin-block-start: var(--ds-space-2);
}

.ds-alert-success { background: var(--ds-success-bg); border-color: var(--ds-success-border); }
.ds-alert-success > i { color: var(--ds-success); }
.ds-alert-warn    { background: var(--ds-warn-bg);    border-color: var(--ds-warn-border); }
.ds-alert-warn > i { color: var(--ds-warn); }
.ds-alert-danger  { background: var(--ds-danger-bg);  border-color: var(--ds-danger-border); }
.ds-alert-danger > i { color: var(--ds-danger); }
.ds-alert-info    { background: var(--ds-info-bg);    border-color: var(--ds-info-border); }
.ds-alert-info > i { color: var(--ds-info); }

/* État vide : il doit proposer une SUITE, pas seulement constater une
   absence. « Aucun dossier » sans bouton laisse l'utilisateur sans recours. */
.ds-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--ds-space-3);
  padding: var(--ds-space-8) var(--ds-space-4);
  text-align: center;
  color: var(--ds-text-2);
}
.ds-empty-icon {
  font-size: 1.8rem;
  color: var(--ds-text-3);
  opacity: 0.5;
}
.ds-empty-title {
  font-size: var(--ds-text-md);
  font-weight: var(--ds-weight-semi);
  color: var(--ds-text);
}
.ds-empty-text {
  font-size: var(--ds-text-sm);
  max-width: 46ch;
  line-height: var(--ds-leading-normal);
}

/* Squelette de chargement : il occupe la place du contenu attendu, pour que
   la mise en page ne saute pas à l'arrivée des données. */
.ds-skeleton {
  background: linear-gradient(90deg,
              var(--ds-surface-2) 25%,
              var(--ds-surface-3) 37%,
              var(--ds-surface-2) 63%);
  background-size: 400% 100%;
  border-radius: var(--ds-radius-sm);
  animation: ds-shimmer var(--ds-duration-long) ease infinite;
}
@keyframes ds-shimmer {
  from { background-position: 100% 50%; }
  to   { background-position: 0 50%; }
}

.ds-progress {
  height: 6px;
  border-radius: var(--ds-radius-pill);
  background: var(--ds-surface-3);
  overflow: hidden;
}
.ds-progress > span {
  display: block;
  height: 100%;
  background: var(--ds-accent);
  transition: inline-size var(--ds-duration-slow) var(--ds-ease);
}

/* ===========================================================================
   7. STRUCTURE DE PAGE
   ---------------------------------------------------------------------------
   Politique de largeur : les écrans de DONNÉES prennent toute la largeur
   disponible ; seuls les formulaires narratifs sont bornés. Une application
   de poste enfermée dans une colonne centrale gaspille l'écran qu'on lui a
   donné.
   =========================================================================== */

.ds-page {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-4);
  min-width: 0;
}

.ds-page-form {
  max-width: var(--ds-form-max-w);
}

.ds-section {
  background: var(--ds-card);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  padding: var(--ds-space-4);
  min-width: 0;
}
.ds-section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ds-space-3);
  margin-block-end: var(--ds-space-3);
}
.ds-section-title {
  font-size: var(--ds-text-md);
  font-weight: var(--ds-weight-semi);
  color: var(--ds-text);
}

/* Barre d'actions : filtres à un bout, actions à l'autre, compteur au milieu.
   Une seule barre par écran — l'inventaire a relevé des boutons dispersés en
   haut, au milieu et en bas de la même page. */
.ds-actionbar {
  display: flex;
  align-items: center;
  gap: var(--ds-space-3);
  flex-wrap: wrap;
  min-height: var(--ds-actionbar-h);
  padding: var(--ds-space-2) var(--ds-space-3);
  background: var(--ds-card);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  min-width: 0;
}
.ds-actionbar-spacer { flex: 1 1 auto; }
.ds-actionbar-count {
  font-size: var(--ds-text-sm);
  color: var(--ds-text-2);
  font-variant-numeric: tabular-nums;
}

/* Grille de vignettes chiffrées. UNE seule famille visuelle : l'inventaire a
   relevé deux traitements différents pour la même information sur le même
   écran. */
.ds-kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--ds-space-3);
}
.ds-kpi {
  display: flex;
  flex-direction: column;
  gap: var(--ds-space-1);
  padding: var(--ds-space-3) var(--ds-space-4);
  background: var(--ds-card);
  border: 1px solid var(--ds-border);
  border-radius: var(--ds-radius-lg);
  min-width: 0;
}
.ds-kpi-label {
  font-size: var(--ds-text-xs);
  font-weight: var(--ds-weight-semi);
  text-transform: uppercase;
  letter-spacing: var(--ds-tracking-caps);
  color: var(--ds-text-2);
}
.ds-kpi-value {
  font-size: var(--ds-text-kpi);
  font-weight: var(--ds-weight-bold);
  line-height: 1.1;
  color: var(--ds-text);
  font-variant-numeric: tabular-nums;
}
.ds-kpi-hint {
  font-size: var(--ds-text-xs);
  color: var(--ds-text-3);
}
/* Accent porté par une bordure de début, pas par un fond coloré : la vignette
   reste lisible et la couleur ne devient pas décorative. */
.ds-kpi.is-danger  { border-inline-start: 3px solid var(--ds-danger); }
.ds-kpi.is-warn    { border-inline-start: 3px solid var(--ds-warn); }
.ds-kpi.is-success { border-inline-start: 3px solid var(--ds-success); }
.ds-kpi.is-accent  { border-inline-start: 3px solid var(--ds-gold); }

/* Carte de chiffres COMPACTE (bandeau au-dessus d'une liste, ds.css) : plus
   petite que la vignette, parce que la liste passe d'abord (retour du
   2026-09-22). Le chiffre descend d'un cran, la tuile d'icône aussi. */
.ds-kpi-card {
  padding: var(--ds-space-3) var(--ds-space-4);
  border-radius: var(--ds-radius-lg);
  box-shadow: var(--ds-shadow-sm);
}
.ds-kpi-card:hover { transform: none; box-shadow: var(--ds-shadow); }
.ds-kpi-card.active {
  background: var(--ds-cream);
  border-color: var(--ds-cream-border);
  border-inline-start-color: var(--ds-gold);
}
.ds-kpi-card .ds-kpi-value {
  font-size: var(--ds-text-kpi-sm);
  line-height: var(--ds-leading-tight);
}
.ds-kpi-icon {
  inline-size: 2rem;
  block-size: 2rem;
  border-radius: var(--ds-radius);
}

/* Tuiles d'icône teintées : cinq teintes douces, sans valeur sémantique. */
.ds-tile-gold   { background: var(--ds-tile-gold-bg);   color: var(--ds-tile-gold-fg); }
.ds-tile-blue   { background: var(--ds-tile-blue-bg);   color: var(--ds-tile-blue-fg); }
.ds-tile-green  { background: var(--ds-tile-green-bg);  color: var(--ds-tile-green-fg); }
.ds-tile-rose   { background: var(--ds-tile-rose-bg);   color: var(--ds-tile-rose-fg); }
.ds-tile-violet { background: var(--ds-tile-violet-bg); color: var(--ds-tile-violet-fg); }

/* Carte active : fond crème, bordure or adoucie. */
.card.is-active,
.ds-card-active {
  background: var(--ds-cream);
  border-color: var(--ds-cream-border);
}

/* ===========================================================================
   7 bis. ENTÊTE DE PAGE
   ---------------------------------------------------------------------------
   Titre, phrase, une seule action principale au bout de la ligne. Pas de
   sur-titre (décision du 2026-09-22). Le bloc passe à la ligne plutôt que de
   déborder : jamais de défilement horizontal.
   =========================================================================== */
.ds-page-header {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--ds-space-3) var(--ds-space-4);
  margin-block-end: var(--ds-stack-gap);
  min-inline-size: 0;
}
.ds-page-header-text {
  flex: 1 1 20rem;
  min-inline-size: 0;
}
.ds-page-title {
  margin: 0;
  font-size: var(--ds-text-xl);
  font-weight: var(--ds-weight-bold);
  line-height: var(--ds-leading-tight);
  color: var(--ds-text);
  overflow-wrap: anywhere;
}
.ds-page-lead {
  margin: var(--ds-space-1) 0 0;
  font-size: var(--ds-text-base);
  line-height: var(--ds-leading-normal);
  color: var(--ds-text-2);
}
.ds-page-header-action {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--ds-space-2);
}

/* ===========================================================================
   8. ACCESSIBILITÉ
   =========================================================================== */

/* Contenu réservé aux lecteurs d'écran. Sert notamment à nommer les boutons
   icône et à annoncer les erreurs de formulaire. */
.ds-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Lien d'évitement : premier élément focusable de la page, invisible tant
   qu'il n'a pas le focus. */
.ds-skip-link {
  position: absolute;
  inset-block-start: var(--ds-space-2);
  inset-inline-start: var(--ds-space-2);
  z-index: var(--ds-z-toast);
  transform: translateY(-200%);
  transition: transform var(--ds-transition);
}
.ds-skip-link:focus {
  transform: translateY(0);
}
