/*
 * Habillage OPTIONNEL des composants `/react`.
 *
 *   @import 'tailwindcss';
 *   @import '@mister-guiiug/dev-pwa-config/tailwind-preset.css';
 *   @import '@mister-guiiug/dev-pwa-config/components.css';   ← opt-in
 *
 * POURQUOI. Les composants du paquet ne posent que des attributs `data-dwc` :
 * intentionnellement non stylés, pour que chaque app impose son univers. En
 * pratique, 11 apps sur 13 ont fini par réécrire à la main les mêmes 12 à 23
 * sélecteurs, et 7 ont carrément réimplémenté `EmptyState` plutôt que
 * d'habiller celui du paquet. Ce fichier ferme cet écart : il donne une base
 * correcte (accessible, cible tactile respectée) que l'app peut garder,
 * teinter, ou écraser.
 *
 * CONTRAT DE COULEUR. Aucune couleur de marque n'est imposée. Tout est lu dans
 * des variables que l'app définit où elle veut (`:root`, `@theme`, `[data-theme]`,
 * `.dark`…) ; chaque `var()` porte un repli neutre, donc l'import seul donne
 * déjà un rendu correct en clair ET en sombre. Un jeu de valeurs prêt à
 * l'emploi, au contraste vérifié, est livré à part :
 * `@import '@mister-guiiug/dev-pwa-config/tokens.css'`.
 *
 *   --dwc-surface            fond des panneaux            (repli : Canvas)
 *   --dwc-surface-2          fond en retrait              (repli : dérivé)
 *   --dwc-text               texte principal              (repli : CanvasText)
 *   --dwc-text-soft          texte secondaire             (repli : GrayText)
 *   --dwc-border             filets de séparation         (repli : dérivé)
 *   --dwc-border-strong      contour d'un contrôle, 3:1   (repli : --dwc-border)
 *   --dwc-primary            action principale            (repli : CanvasText)
 *   --dwc-primary-contrast   texte posé sur la primaire   (repli : Canvas)
 *   --dwc-primary-soft       fond teinté de marque        (repli : dérivé)
 *   --dwc-success            validé / en ligne            (repli : vert)
 *   --dwc-warning            en attente / dégradé         (repli : ambre)
 *   --dwc-danger             erreur / destructif          (repli : rouge)
 *   --dwc-info               information neutre           (repli : bleu)
 *   --dwc-radius             rayon des blocs              (repli : 0.75rem)
 *   --dwc-shadow             ombre des blocs              (repli : aucune)
 *   --dwc-header-row-height  hauteur de la rangée titre   (repli : 3.25rem)
 *
 * Les replis neutres utilisent les couleurs système CSS (`Canvas`, `CanvasText`,
 * `GrayText`), qui suivent d'elles-mêmes `color-scheme` : rien n'est codé en
 * dur. Les quatre tons d'état, eux, n'ont pas d'équivalent système — leur repli
 * passe par `light-dark()`.
 *
 * PRIORITÉ. Tout est en `@layer components`, la couche Tailwind prévue pour ça :
 * les utilitaires (`bg-primary`, `rounded-none`…) et tout CSS non « layered » de
 * l'app l'emportent. Importer ce fichier ne verrouille donc rien.
 *
 * UN SEUL PIÈGE À L'ÉCRASEMENT. Redonner sa propre animation d'entrée aux
 * panneaux (`sheet-panel`, `confirm-panel`, `toast`) est le cas le plus tentant
 * — et le seul qui casse autre chose que l'apparence. Si elle porte
 * `animation-fill-mode: both` ou `forwards`, le panneau garde une transformation
 * après son entrée, devient bloc conteneur de ses descendants `position: fixed`
 * et leur ferme un contexte d'empilement : le voile d'un `ConfirmDialog` ouvert
 * depuis une feuille se replie alors sur la feuille au lieu de couvrir l'écran.
 * Utiliser `backwards`. Le détail est en fin de fichier, sur `@keyframes
 * dwc-rise`.
 *
 * DEUX RENDUS QUE PERSONNE NE REGARDE. Le contraste forcé et l'impression
 * remplacent ou suppriment les couleurs sans prévenir : un aplat de marque y
 * disparaît, et le texte clair posé dessus avec lui. Les deux blocs en fin de
 * fichier rattrapent ce que ces modes cassent — voir leurs commentaires pour le
 * détail, et `test/components-css.test.mjs` pour les garde-fous.
 */

@layer components {
  /* ── Bases partagées ───────────────────────────────────────────────────── */

  /*
   * Toute commande du paquet respecte la cible tactile de 2,75 rem (~44 px).
   * C'est le principal intérêt d'une base partagée : une taille `sm` locale
   * finit toujours par passer sous le seuil.
   */
  :where(
    [data-dwc='error-banner-retry'],
    [data-dwc='error-banner-dismiss'],
    [data-dwc='error-boundary-reset'],
    [data-dwc='error-boundary-backup'],
    [data-dwc='share-button'],
    [data-dwc='update-banner-update'],
    [data-dwc='update-button'],
    [data-dwc='update-banner-dismiss'],
    [data-dwc='update-banner-ignore'],
    [data-dwc='offline-ready-dismiss'],
    [data-dwc='consent-accept'],
    [data-dwc='consent-refuse'],
    [data-dwc='consent-section-action'],
    [data-dwc='pwa-install-actions'] button,
    [data-dwc='footer-source'],
    [data-dwc='footer-sponsor'],
    [data-dwc='footer-issues'],
    [data-dwc='family-source'],
    [data-dwc='family-sponsor'],
    [data-dwc='family-app'],
    [data-dwc='family-app-repo']
  ) {
    min-height: 2.75rem;
  }

  :where(
    [data-dwc='error-banner-retry'],
    [data-dwc='error-boundary-reset'],
    [data-dwc='error-boundary-backup'],
    [data-dwc='share-button'],
    [data-dwc='update-banner-update'],
    [data-dwc='update-button'],
    [data-dwc='update-banner-dismiss'],
    [data-dwc='update-banner-ignore'],
    [data-dwc='offline-ready-dismiss'],
    /* Accepter et refuser partagent la MÊME règle, donc la même taille, le
       même contraste et la même cible tactile. C'est la traduction en CSS de
       « refuser coûte un clic comme accepter » : un bouton de refus plus
       discret rendrait le consentement irrégulier sans qu'aucun test de
       comportement ne s'en aperçoive. */
    [data-dwc='consent-accept'],
    [data-dwc='consent-refuse'],
    /* Le retrait, dans un écran de réglages, est un VRAI bouton — pas le lien
       de pied de page de `consent-settings` : c'est le geste de la section,
       et il doit se voir autant que l'« Accepter » qu'il défait. */
    [data-dwc='consent-section-action'],
    [data-dwc='pwa-install-actions'] button
  ) {
    font: inherit;
    font-size: 0.875em;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.4rem 0.85rem;
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.7);
    background: var(--dwc-surface, Canvas);
    color: var(--dwc-text, CanvasText);
    cursor: pointer;
  }

  /* ── EmptyState ────────────────────────────────────────────────────────── */

  [data-dwc='empty-state'] {
    display: grid;
    justify-items: center;
    gap: 0.35rem;
    text-align: center;
    padding: var(--spacing-fluid-lg, 1.5rem);
    border: 1px dashed
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: var(--dwc-radius, 0.75rem);
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
  }

  [data-dwc='empty-state-icon'] {
    color: var(--dwc-primary, CanvasText);
    line-height: 0;
  }

  [data-dwc='empty-state-title'] {
    margin: 0;
    font-size: var(--text-fluid-lg, 1.125rem);
    font-weight: 600;
  }

  [data-dwc='empty-state-desc'] {
    margin: 0;
    max-width: 40ch;
    font-size: var(--text-fluid-sm, 0.9rem);
    color: var(--dwc-text-soft, GrayText);
  }

  /* Le contenu libre glissé dans l'état vide. Sans règle, il héritait du
     `justify-items: center` de la grille et se rétrécissait à son contenu :
     un tableau ou un formulaire posé là arrivait comprimé au milieu. */
  [data-dwc='empty-state-body'] {
    justify-self: stretch;
    margin-top: var(--spacing-fluid-sm, 0.75rem);
  }

  [data-dwc='empty-state-action'] {
    margin-top: var(--spacing-fluid-sm, 0.75rem);
  }

  /* ── ErrorBanner ───────────────────────────────────────────────────────── */

  [data-dwc='error-banner'] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding: var(--spacing-fluid-sm, 0.75rem) var(--spacing-fluid-md, 1rem);
    border: 1px solid;
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.8);
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  /* La sévérité n'est jamais portée par la seule couleur : le composant pose
     aussi `role=alert` vs `role=status`, et le message reste explicite. */
  [data-dwc='error-banner'][data-severity='error'] {
    border-color: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
    background: color-mix(
      in srgb,
      var(--dwc-danger, light-dark(#b91c1c, #fb7185)) 12%,
      var(--dwc-surface, Canvas)
    );
  }

  [data-dwc='error-banner'][data-severity='warning'] {
    border-color: var(--dwc-warning, light-dark(#b45309, #fbbf24));
    background: color-mix(
      in srgb,
      var(--dwc-warning, light-dark(#b45309, #fbbf24)) 14%,
      var(--dwc-surface, Canvas)
    );
  }

  [data-dwc='error-banner'][data-severity='info'] {
    border-color: var(--dwc-primary, CanvasText);
    background: var(
      --dwc-primary-soft,
      color-mix(in srgb, CanvasText 6%, Canvas)
    );
  }

  [data-dwc='error-banner-message'] {
    flex: 1 1 16rem;
  }

  [data-dwc='error-banner-dismiss'] {
    font: inherit;
    font-size: 1.1rem;
    line-height: 1;
    min-width: 2.75rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: inherit;
    cursor: pointer;
  }

  /* ── ErrorBoundary (UI de repli) ───────────────────────────────────────── */

  [data-dwc='error-boundary'] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding: var(--spacing-fluid-md, 1rem);
    border: 1px solid var(--dwc-danger, light-dark(#b91c1c, #fb7185));
    border-radius: var(--dwc-radius, 0.75rem);
    background: color-mix(
      in srgb,
      var(--dwc-danger, light-dark(#b91c1c, #fb7185)) 10%,
      var(--dwc-surface, Canvas)
    );
  }

  [data-dwc='error-boundary-title'] {
    margin: 0;
    flex: 1 1 100%;
    font-size: var(--text-fluid-lg, 1.125rem);
    font-weight: 600;
  }

  /* La référence à citer au support : discrète, mais copiable d'un geste —
     elle est faite pour être collée dans un ticket. */
  [data-dwc='error-boundary-reference'] {
    margin: 0;
    flex: 1 1 100%;
    font-size: var(--text-fluid-xs, 0.8rem);
    color: var(--dwc-text-soft, GrayText);
    user-select: all;
  }

  /*
   * Montée à la RACINE — l'écran blanc évité. Trois apps (mister-footcoach,
   * miss-carbook, mister-puzzle) recopient le même bloc pour ce cas : la
   * frontière n'y est pas une bannière perdue en haut de page, c'est un ÉCRAN
   * centré. `#root` est le point de montage Vite des dix-sept apps ;
   * `:where()` le neutralise pour que la spécificité reste celle de
   * l'attribut, écrasable au sélecteur près. Le cadre et le fond teintés de
   * danger disparaissent, comme dans les trois copies : plein écran, ils
   * crieraient plus fort que le message.
   */
  :where(#root) > [data-dwc='error-boundary'] {
    min-height: 100dvh;
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: center;
    justify-content: center;
    gap: 1rem;
    padding: var(--spacing-fluid-lg, 1.5rem);
    text-align: center;
    border: 0;
    border-radius: 0;
    background: var(--dwc-surface, Canvas);
    color: var(--dwc-text, CanvasText);
  }

  /* En colonne centrée, la pleine largeur des textes n'a plus à se forcer. */
  :where(#root) > [data-dwc='error-boundary'] [data-dwc='error-boundary-title'],
  :where(#root)
    > [data-dwc='error-boundary']
    [data-dwc='error-boundary-reference'] {
    flex: none;
    max-width: 40ch;
  }

  /* ── SyncStatusBadge ───────────────────────────────────────────────────── */

  [data-dwc='sync-status'] {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.3rem 0.7rem;
    border-radius: 999px;
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
    font-size: var(--text-fluid-xs, 0.8rem);
  }

  /* La pastille double le libellé, elle ne le remplace pas. */
  [data-dwc='sync-status']::before {
    content: '';
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 999px;
    background: var(--dwc-text-soft, GrayText);
  }

  [data-dwc='sync-status'][data-status='synced']::before {
    background: var(--dwc-success, light-dark(#15803d, #4ade80));
  }

  [data-dwc='sync-status'][data-status='pending']::before {
    background: var(--dwc-warning, light-dark(#b45309, #fbbf24));
  }

  [data-dwc='sync-status'][data-status='error']::before {
    background: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
  }

  /* ── PwaInstallPrompt & UpdatePromptBanner ─────────────────────────────── */

  [data-dwc='pwa-install-prompt'],
  [data-dwc='update-banner'],
  [data-dwc='offline-ready'] {
    padding: var(--spacing-fluid-md, 1rem);
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: var(--dwc-radius, 0.75rem);
    background: var(
      --dwc-primary-soft,
      color-mix(in srgb, CanvasText 6%, Canvas)
    );
    box-shadow: var(--dwc-shadow, none);
  }

  [data-dwc='pwa-install-title'] {
    margin: 0;
    font-size: var(--text-fluid-lg, 1.125rem);
    font-weight: 600;
  }

  [data-dwc='pwa-install-desc'] {
    margin: 0.15rem 0 var(--spacing-fluid-sm, 0.75rem);
    font-size: var(--text-fluid-sm, 0.9rem);
    color: var(--dwc-text-soft, GrayText);
  }

  /* Là où le navigateur n'expose aucune invite (iOS, Safari), cette ligne
     n'est plus un argument de vente mais LA marche à suivre : on lui rend
     l'encre du texte courant. Du gris atténué sur un aplat `primary-soft`
     est précisément la composition que le relevé de contraste du 06/09 a
     trouvée sous AA — et ici elle porterait la seule information utile. */
  [data-dwc='pwa-install-prompt'][data-method='instructions']
    [data-dwc='pwa-install-desc'] {
    color: inherit;
  }

  [data-dwc='pwa-install-actions'] {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  [data-dwc='update-banner'],
  [data-dwc='offline-ready'] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
  }

  /*
   * SA PLACE, QUAND LA BARRE EST COLLÉE. `AppUpdates` rend le bandeau APRÈS
   * `children` : en flux, tout en bas du document. Sous `BottomNav
   * placement="fixed"`, il finissait hors écran puis, page déroulée, dans les
   * 4,5 rem que la barre couvre — « Recharger » n'était atteignable par
   * personne. Mesuré dans mister-miss-koh le 06/09/2026, en 375×812 : bord bas
   * du bandeau à 812 px, haut de la barre à 759 px. Le squelette et
   * miss-lookhouse avaient le même défaut sans l'avoir vu.
   *
   * Fixé au-dessus de la barre — le plancher vient de `:root`, voir BottomNav —
   * sous l'en-tête (30) et les toasts (70). Onze apps flottent déjà ce bandeau
   * en bas d'écran ; celles qui le placent elles-mêmes gardent la main, leur
   * CSS n'étant pas « layered ».
   *
   * SOUS CONDITION, ET C'EST VOULU. Sans barre collée, rien ne bouge : treize
   * apps placent le bandeau elles-mêmes, dont deux EN HAUT de l'écran
   * (`top: 0`), où un `bottom` venu d'ici s'ajouterait à leur `top` et
   * étirerait le bandeau sur toute la hauteur. Adopter `placement="fixed"`,
   * c'est aussi retirer son placement maison du bandeau.
   *
   * Deux gardes pour qui adopte la barre sans le faire tout de suite : une
   * hauteur POSÉE, pour qu'un `top` d'app rende ce `bottom` ignoré au lieu de
   * s'y ajouter (sur-contrainte, CSS 2 §10.6.4) ; une largeur POSÉE plutôt
   * qu'un `max-width`, pour qu'un centrage à `left: 50%` + `translateX(-50%)`
   * (trois apps) ne se retrouve pas avec un `right` en plus et un bandeau
   * réduit de moitié — les trois côtés posés, `right` est ignoré.
   */
  :root:has([data-dwc='bottom-nav'][data-placement='fixed'])
    [data-dwc='update-banner'],
  :root:has([data-dwc='bottom-nav'][data-placement='fixed'])
    [data-dwc='offline-ready'] {
    position: fixed;
    /* Voir la règle du placement demandé, plus bas : les deux font la même
       chose et ne doivent pas diverger sur ce point non plus. */
    box-sizing: border-box;
    inset-inline: 0;
    bottom: calc(var(--_dwc-bottom-clearance) + 0.5rem);
    z-index: 25;
    width: min(100% - 2 * var(--spacing-fluid-sm, 0.75rem), 40rem);
    height: max-content;
    margin-inline: auto;
    /* `backwards`, jamais `both` — voir « Animations » en fin de fichier. */
    animation: dwc-rise 0.22s ease-out backwards;
  }

  /*
   * ET LA MÊME PLACE, DEMANDÉE DIRECTEMENT : `placement="fixed"`.
   *
   * La règle ci-dessus exige une barre basse, du socle, ET déclarée : cinq apps
   * sur vingt remplissaient cette condition au relevé du 16/09/2026. Les quinze
   * autres recevaient cette boîte DANS LE FLUX, en fin de document — et six ont
   * réécrit le placement à la main, avec des `z-index` allant de 25 à 9999.
   * La prop le rend demandable sans barre, et les six copies peuvent partir.
   *
   * UNE RÈGLE À PART, ET NON CES SÉLECTEURS AJOUTÉS À LA LISTE DU DESSUS. Une
   * liste de sélecteurs est tout ou rien : un navigateur sans `:has()` jetterait
   * la règle ENTIÈRE, et ce placement-ci — qui n'a pourtant besoin de rien de
   * moderne — tomberait avec elle. `test/components-css.test.mjs` l'interdit,
   * et il a rattrapé cette faute à l'écriture même de ce bloc.
   */
  [data-dwc='update-banner'][data-placement='fixed'],
  [data-dwc='offline-ready'][data-placement='fixed'] {
    position: fixed;
    /*
     * `border-box` POSÉ ICI, et c'est un banc d'essai qui l'a montré. Le socle
     * ne pose aucun `box-sizing` : il compte sur le préréglage de l'app, que
     * les vingt ont. Sur une page qui n'en a pas, `width` s'ajoute au
     * `padding` — la boîte dépasse de ses trois côtés posés, `margin-inline:
     * auto` rend un `-10px`, et le bandeau sort de l'écran. C'est précisément
     * la sur-contrainte contre laquelle la règle voisine met en garde.
     * Sous un préréglage, cette ligne ne change rien.
     */
    box-sizing: border-box;
    inset-inline: 0;
    bottom: calc(
      var(--_dwc-bottom-clearance, max(env(safe-area-inset-bottom), 0px)) +
        0.5rem
    );
    z-index: 25;
    width: min(100% - 2 * var(--spacing-fluid-sm, 0.75rem), 40rem);
    height: max-content;
    margin-inline: auto;
    /* `backwards`, jamais `both` — voir « Animations » en fin de fichier. */
    animation: dwc-rise 0.22s ease-out backwards;
  }

  [data-dwc='update-banner-title'],
  [data-dwc='offline-ready-title'] {
    flex: 1 1 12rem;
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  /*
   * LES GESTES FONT BLOC, ET LE PRINCIPAL SE VOIT.
   *
   * Les boutons étaient frères du titre, dans une même ligne souple : sur
   * 393 px, le titre et « Mettre à jour » la remplissaient, et « Plus tard
   * (4 h) » tombait seul sur la suivante, à gauche (mister-miss-koh, relevé du
   * 30/09/2026). Les deux portaient en plus le même habillage neutre : rien ne
   * disait lequel était le geste attendu.
   *
   * NEUF APPS DU PARC L'AVAIENT CORRIGÉ CHEZ ELLES, chacune à sa façon. Huit
   * peignaient « Mettre à jour » de leur couleur principale (miss-badminton,
   * miss-contraction, miss-dice, miss-genius, mister-footcoach,
   * mister-miss-koh, mister-molkky, mister-puzzle), quatre passaient le titre
   * sur sa propre ligne, trois marquaient l'attente. Le socle reprend ce
   * qu'elles avaient en commun.
   *
   * Groupés dans `update-banner-actions`, les boutons restent ensemble : à
   * côté du titre quand la place le permet, dessous sinon, alignés à droite
   * comme les gestes d'une boîte de dialogue. `flex-wrap` sur les actions
   * aussi : sur 320 px, ils s'empilent plutôt que de déborder.
   */
  [data-dwc='update-banner-title'] {
    font-weight: 600;
  }

  [data-dwc='update-banner-actions'] {
    display: flex;
    flex: 1 1 auto;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.5rem;
  }

  /* La couleur principale, avec la paire de jetons de la variante `primary`
     de `Button`, dont les palettes garantissent déjà le contraste. La bordure
     prend la teinte du fond plutôt que `transparent` : en contraste forcé,
     une bordure transparente effacerait le contour du bouton. */
  [data-dwc='update-banner-update'] {
    border-color: var(--dwc-primary, CanvasText);
    background: var(--dwc-primary, CanvasText);
    color: var(--dwc-primary-contrast, Canvas);
    font-weight: 600;
  }

  /* Le survol de la variante `primary` de `Button`, et pour la même raison :
     il ne repeint pas le bouton (voir la section « Button »). */
  @media (hover: hover) {
    [data-dwc='update-banner-update']:hover:not([aria-busy='true']) {
      transform: translateY(-1px);
      box-shadow: 0 4px 12px
        color-mix(in srgb, var(--dwc-primary, CanvasText) 35%, transparent);
    }
  }

  [data-dwc='update-banner-update'][aria-busy='true'] {
    cursor: progress;
    opacity: 0.8;
  }

  /* ── ConsentBanner ─────────────────────────────────────────────────────── */

  /*
   * SA PROPRE SECTION, ET PAS UN AJOUT À CELLE DU BANDEAU DE MISE À JOUR.
   * Les deux se ressemblent — même boîte, même disposition — et il aurait été
   * plus court de les mettre sur la même règle. Mais la découpe en morceaux
   * suit les sections : le consentement aurait alors demandé d'importer
   * `install-prompt.css`, et une app qui ne monte ni invite d'installation ni
   * bandeau de mise à jour aurait payé les deux. Quatorze lignes redites
   * valent mieux qu'un morceau qui ment sur son nom.
   *
   * Les DEUX boutons, eux, restent sur la règle partagée des « Bases » : c'est
   * elle qui garantit qu'accepter et refuser ont la même taille, le même
   * contraste et la même cible tactile.
   */
  [data-dwc='consent-banner'] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding: var(--spacing-fluid-md, 1rem);
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: var(--dwc-radius, 0.75rem);
    background: var(
      --dwc-primary-soft,
      color-mix(in srgb, CanvasText 6%, Canvas)
    );
    box-shadow: var(--dwc-shadow, none);
  }

  /*
   * SA PLACE — ET IL N'EN AVAIT AUCUNE.
   *
   * Cette section habillait la boîte du bandeau et ne la plaçait pas :
   * `position: static`, donc en fin de flux, là où les apps le montent —
   * c'est-à-dire sous le pied de page. Mesuré en production sur mister-cim10
   * le 16/09/2026, en 375 × 812 : boîte de 753 à 891 px pour une fenêtre de
   * 812, recouverte par la barre basse dont le bord haut est à 756. Le bandeau
   * qui DEMANDE le consentement était sous la ligne de flottaison ET derrière
   * la navigation.
   *
   * LES MÊMES VALEURS QUE LE BANDEAU DE MISE À JOUR, REDITES. La découpe en
   * morceaux suit les sections : partager le bloc obligerait une app qui ne
   * monte que le consentement à importer `install-prompt.css`. C'est le même
   * arbitrage qu'en tête de section, et la même conclusion — mais ici la
   * duplication porte des NOMBRES, alors on la dit.
   */
  [data-dwc='consent-banner'][data-placement='fixed'] {
    position: fixed;
    /* Voir la règle jumelle du bandeau de mise à jour : sans préréglage,
       `width` s'ajoute au `padding` et la boîte sort de l'écran. */
    box-sizing: border-box;
    inset-inline: 0;
    bottom: calc(
      var(--_dwc-bottom-clearance, max(env(safe-area-inset-bottom), 0px)) +
        0.5rem
    );
    z-index: 26;
    width: min(100% - 2 * var(--spacing-fluid-sm, 0.75rem), 40rem);
    height: max-content;
    margin-inline: auto;
    /* `backwards`, jamais `both` — voir « Animations » en fin de fichier. */
    animation: dwc-rise 0.22s ease-out backwards;
  }

  /*
   * LA QUESTION PASSE DEVANT — PRÉCÉDENCE, PAS EMPILEMENT.
   *
   * Les deux bandeaux visent désormais le même bas d'écran. Un empilement
   * demanderait la HAUTEUR de celui du dessous, que CSS ne connaît pas : tout
   * décalage serait un nombre magique, faux dès que le message passe à la
   * ligne.
   *
   * La précédence, elle, est exacte — et c'est déjà l'idiome du composant :
   * `update-prompt-banner.js` fait taire le « prêt hors ligne » dès qu'une
   * version attend. Même raisonnement ici, d'un cran au-dessus : le
   * consentement est TRANSIENT par construction (un clic et il ne revient
   * plus), là où le bandeau de mise à jour revient à la vérification suivante.
   * Rien n'est perdu, et la question qu'il faut trancher reste lisible.
   *
   * SEULEMENT ENTRE SURFACES DU SOCLE : les deux côtés exigent
   * `data-placement="fixed"`. Une app qui place elle-même son bandeau garde
   * exactement ce qu'elle avait.
   */
  :root:has([data-dwc='consent-banner'][data-placement='fixed'])
    [data-dwc='update-banner'][data-placement='fixed'],
  :root:has([data-dwc='consent-banner'][data-placement='fixed'])
    [data-dwc='offline-ready'][data-placement='fixed'] {
    display: none;
  }

  [data-dwc='consent-message'] {
    margin: 0;
    flex: 1 1 18rem;
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  /*
   * `flex-wrap` sur le bandeau ET sur ses actions : sous 360 px, le message
   * passe au-dessus et les deux boutons restent côte à côte. Les empiler
   * mettrait « Refuser » sous la ligne de flottaison sur un petit écran —
   * autrement dit un refus plus coûteux qu'une acceptation, obtenu par la mise
   * en page plutôt que par le nombre de clics.
   */
  [data-dwc='consent-actions'] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
  }

  [data-dwc='consent-policy'] {
    font-size: 0.875em;
    color: inherit;
  }

  /*
   * LE RÉGLAGE : UN BOUTON QUI DOIT AVOIR L'AIR D'UN LIEN DE PIED DE PAGE.
   *
   * C'est un `<button>` et non un `<a>` — il ne mène nulle part, il rappelle le
   * bandeau — mais sa place est la rangée de liens du pied, à côté de
   * `footer-source` et `footer-issues`. Sans ce dégraissage il y arriverait
   * avec le fond, le cadre et la police du système : un gros bouton gris au
   * milieu de trois liens.
   *
   * IL N'EST PAS SUR LA RÈGLE PARTAGÉE DES BOUTONS, et c'est la raison d'être
   * de ces lignes. Cette règle-là garantit qu'« Accepter » et « Refuser » ont
   * la même taille et la même cible tactile — l'égalité que la CNIL demande
   * ENTRE LES DEUX FACES d'un choix. Ici il n'y a pas deux faces : un seul
   * bouton, qui rouvre la question. Lui donner la stature des deux autres le
   * ferait passer pour une troisième option.
   */
  [data-dwc='consent-settings'] {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--dwc-text-soft, GrayText);
    cursor: pointer;
  }

  [data-dwc='consent-settings']:hover {
    color: var(--dwc-primary, CanvasText);
  }

  /*
   * L'ACTION EST SOULIGNÉE, L'ÉTAT NE L'EST PAS. Les deux morceaux ne jouent
   * pas le même rôle : « Mesure d'audience : acceptée » est un constat,
   * « Modifier mon choix » est ce sur quoi on clique. Souligner l'ensemble
   * ferait du constat une promesse de navigation.
   */
  [data-dwc='consent-settings-action'] {
    text-decoration: underline;
  }

  /*
   * LA SECTION D'UN ÉCRAN DE RÉGLAGES : UNE COLONNE, ET RIEN QUI S'IMPOSE.
   *
   * Elle vit dans l'habillage de l'app qui l'accueille — ses cartes, ses
   * titres, sa typographie. On n'y pose que l'ordre de lecture (titre, ce qui
   * est mesuré, l'état, le geste) et des marges nulles, pour qu'elle se glisse
   * dans une carte comme dans une liste. Le bouton, lui, est sur la règle
   * partagée des « Bases » : cible tactile et aspect de bouton.
   */
  [data-dwc='consent-section'] {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }

  [data-dwc='consent-section-title'],
  [data-dwc='consent-section-state'] {
    margin: 0;
  }

  [data-dwc='consent-section-text'] {
    margin: 0;
    max-width: 60ch;
    font-size: var(--text-fluid-sm, 0.9rem);
    color: var(--dwc-text-soft, GrayText);
  }

  /*
   * LE REPLI D'INFORMATION, SUR SA PROPRE LIGNE.
   *
   * `flex-basis: 100%` et `order: 1` le posent SOUS les deux boutons. Sans eux
   * il s'insérerait dans le flex du bandeau, entre le message et les actions
   * sur un écran large : « En savoir plus » viendrait s'intercaler entre la
   * question et les réponses. Ce serait rendre l'acceptation plus proche que
   * le refus par la mise en page — exactement ce que le composant évite par
   * le nombre de clics.
   */
  [data-dwc='consent-policy-details'] {
    flex-basis: 100%;
    order: 1;
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  [data-dwc='consent-policy-summary'] {
    cursor: pointer;
    /* Cible tactile : un résumé se touche au doigt, pas seulement au curseur. */
    padding-block: 0.5rem;
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }

  /* ── PrivacyNotice ─────────────────────────────────────────────────────── */

  /*
   * HABILLÉ ICI, ET C'EST UNE EXCEPTION ASSUMÉE.
   *
   * Le contrat du paquet est que les composants ne posent que des marqueurs,
   * à charge de l'app de les habiller. Celui-ci est un texte de CONFORMITÉ :
   * rendu en `<dl>` nu, les descriptions se décalent d'une tabulation et les
   * termes ne se distinguent pas du corps. Un texte illisible ne vaut pas
   * information, et c'est une information que le RGPD exige. Tout reste
   * écrasable, et aucune couleur de marque n'est imposée.
   */
  [data-dwc='privacy-notice'] {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  [data-dwc='privacy-notice-title'] {
    margin: 0;
    font-size: 1rem;
    font-weight: 600;
  }

  [data-dwc='privacy-notice-list'] {
    margin: 0;
    display: grid;
    gap: 0.5rem 1rem;
  }

  /*
   * Deux colonnes dès qu'il y a la place, une seule en dessous : un terme de
   * six mots et sa description de trente ne tiennent pas côte à côte sur un
   * téléphone.
   */
  @media (min-width: 32rem) {
    [data-dwc='privacy-notice-list'] {
      grid-template-columns: minmax(8rem, 14rem) 1fr;
    }
  }

  [data-dwc='privacy-notice-term'] {
    font-weight: 600;
    color: var(--dwc-text-soft, GrayText);
  }

  /*
   * Un `<dd>` porte un `margin-inline-start: 40px` par défaut du navigateur :
   * dans une grille, il décale la colonne de droite sans aucune raison.
   */
  [data-dwc='privacy-notice-detail'] {
    margin: 0;
  }

  /* ── AppFooter & FamilyApps ────────────────────────────────────────────── */

  [data-dwc='app-footer'],
  [data-dwc='family-links'] {
    display: flex;
    flex-wrap: wrap;
    gap: var(--spacing-fluid-md, 1rem);
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  /*
   * `footer-issues` EST DANS CETTE LISTE DEPUIS QU'IL EXISTE, ET IL Y MANQUAIT.
   *
   * `AppFooter issues` (4.4.0) pose un troisième lien à côté de `footer-source`
   * et `footer-sponsor` ; seuls les deux premiers étaient habillés. Le
   * troisième héritait donc de la couleur du texte courant : sur un fond
   * sombre, noir sur violet nuit — contraste 1,08, relevé « serious » par axe
   * sur mister-qowa. Huit applications l'affichent en production.
   *
   * La leçon n'est pas le lien manquant, c'est qu'aucun test ne pouvait le
   * voir : rien ne croisait les marqueurs que `react/` ÉMET avec ceux que
   * cette feuille habille. C'est désormais le rôle de `marqueursOrphelins`
   * dans `test/components-css.test.mjs`, avec la liste écrite de ceux qui
   * n'ont volontairement pas de règle ici.
   */
  [data-dwc='footer-source'],
  [data-dwc='footer-sponsor'],
  [data-dwc='footer-issues'],
  [data-dwc='family-source'],
  [data-dwc='family-sponsor'] {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--dwc-text-soft, GrayText);
    text-decoration: none;
  }

  [data-dwc='footer-source']:hover,
  [data-dwc='footer-sponsor']:hover,
  [data-dwc='footer-issues']:hover,
  [data-dwc='family-source']:hover,
  [data-dwc='family-sponsor']:hover {
    color: var(--dwc-primary, CanvasText);
    text-decoration: underline;
  }

  /*
   * SUR UN TÉLÉPHONE, LES TROIS LIENS TIENNENT SUR UNE RANGÉE.
   *
   * « Code source », « M'offrir un café », « Signaler un problème » : en 13 px
   * avec 12 px d'écart, il leur faut 374 px, et une colonne de téléphone en
   * offre 343 sur un écran de 375 px, 361 sur un de 393. Le troisième passait
   * seul à la ligne, 56 px plus bas. Deux applications l'avaient corrigé
   * chacune de son côté : mister-miss-koh sous 30rem, miss-devises#4 sous
   * 28rem.
   *
   * Resserrés (12 px, écarts de 8 et 4 px), ils en prennent 337. Et quand le
   * signalement ferme le pied de page, `flex: 1 1 0%` ne le compte qu'à son
   * mot le plus long au moment de remplir la rangée, puis l'élargit jusqu'à
   * sa largeur naturelle (`max-width: max-content`) : si la place manque
   * encore, son libellé se replie dans la rangée au lieu de la quitter. Le
   * retour à la ligne reste permis, donc rien ne déborde, et ce que
   * l'application ajoute (`children`, `links`, `version`, `after`) se range
   * comme avant.
   *
   * `max-width`, la seule de la feuille : au-delà de 30rem, la règle de base
   * s'applique telle quelle, et il n'y a rien à restaurer.
   */
  @media (max-width: 30rem) {
    [data-dwc='app-footer'] {
      gap: 0.5rem;
      font-size: 0.75rem;
    }

    [data-dwc='app-footer'] > [data-dwc='footer-source'],
    [data-dwc='app-footer'] > [data-dwc='footer-sponsor'],
    [data-dwc='app-footer'] > [data-dwc='footer-issues'] {
      gap: 0.25rem;
    }

    [data-dwc='app-footer'] > [data-dwc='footer-issues']:last-child {
      flex: 1 1 0%;
      max-width: max-content;
    }
  }

  /*
   * Le numéro de version : discret par défaut, lisible quand même.
   *
   * `--dwc-text-soft` est un gris SECONDAIRE, pas un gris décoratif : il tient
   * le 4.5:1 dans les seize palettes. Le pied de page est justement l'endroit
   * où l'on est tenté de descendre plus bas — c'est aussi celui qu'on lit quand
   * on remplit un rapport de bug.
   */
  /*
   * Le retour du partage : un TEXTE, jamais une couleur seule. La région
   * `status` existe dès le premier rendu — insérée en même temps que son
   * message, elle ne serait pas lue de façon fiable — d'où le `:not(:empty)`
   * qui ne l'habille que lorsqu'elle dit quelque chose.
   */
  [data-dwc='share-button-group'] {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
  }

  [data-dwc='share-button-status']:not(:empty) {
    color: var(--dwc-text-soft, GrayText);
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  [data-dwc='app-version'] {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4rem;
    margin: 0;
    color: var(--dwc-text-soft, GrayText);
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  [data-dwc='app-version-value'] {
    font-variant-numeric: tabular-nums;
  }

  a[data-dwc='app-version-value'] {
    color: inherit;
    text-decoration: none;
  }

  a[data-dwc='app-version-value']:hover {
    color: var(--dwc-primary, CanvasText);
    text-decoration: underline;
  }

  /*
   * `[data-dwc='app-version-details']` n'a PLUS de règle, et c'est délibéré :
   * il portait `opacity: 0.85`. Le bloc `app-version` est déjà en
   * `--dwc-text-soft` ; une opacité par-dessus compose une TROISIÈME couleur,
   * plus pâle que le rôle « soft » choisi par la palette, et sur laquelle
   * aucune palette ne s'est prononcée. À 0.85 elle tombait sous 4,5:1 dans
   * QUATORZE des trente-six palettes du parc — relevé par axe sur `/profil` de
   * mister-family-map le 06/09/2026 (#707871 sur #fefcf6 : 4,43 pour 4,5
   * exigé), à la première exécution réelle d'une suite a11y qui n'avait jamais
   * tourné en CI. La discrétion voulue est le rôle du jeton, pas d'un voile
   * posé dessus. `themes.test.mjs` interdit son retour.
   */

  /*
   * Les deux annonces portent leur TEXTE : la pastille colorée n'est qu'un
   * renfort. C'est la règle que `Badge` a fait respecter après l'échec de
   * contraste d'une variante pleine dans onze thèmes sur quatorze.
   */
  [data-dwc='app-version-updated'],
  [data-dwc='app-version-available'] {
    padding: 0.1rem 0.45rem;
    border-radius: var(--dwc-radius, 0.75rem);
    background: var(
      --dwc-primary-soft,
      color-mix(in srgb, CanvasText 6%, Canvas)
    );
    color: var(--dwc-text, CanvasText);
    font-size: 0.85em;
  }

  [data-dwc='family-apps-title'] {
    margin: var(--spacing-fluid-sm, 0.75rem) 0;
    font-size: var(--text-fluid-lg, 1.125rem);
  }

  [data-dwc='family-app-list'] {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.5rem;
    grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  }

  /*
   * `layout="list"` : une colonne, quelle que soit la largeur. TREIZE apps du
   * parc reposaient cette règle à la main — la grille responsive suppose une
   * page, alors qu'elle est presque toujours rendue dans un tiroir de réglages
   * ou une carte de profil, où la seconde colonne n'a jamais la place de
   * naître. Le défaut ne change pas pour autant : quatre apps la rendent bien
   * en pleine page, et la grille y gagne.
   */
  [data-dwc='family-apps'][data-layout='list'] [data-dwc='family-app-list'] {
    grid-template-columns: 1fr;
  }

  [data-dwc='family-app-groups'] {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
  }

  [data-dwc='family-app-group'] {
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.8);
  }

  [data-dwc='family-app-group'] [data-dwc='family-app-list'] {
    padding: 0 0.5rem 0.5rem;
  }

  /* Le triangle de l'agent utilisateur est remplacé par le chevron dessiné
     plus bas : `list-style: none` seul ne le retire pas dans WebKit, qui le
     rend par `::-webkit-details-marker`. Il faut les deux. */
  [data-dwc='family-app-group-summary'] {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.6rem 0.75rem;
    cursor: pointer;
    font-weight: 500;
    list-style: none;
  }

  [data-dwc='family-app-group-summary']::-webkit-details-marker {
    display: none;
  }

  [data-dwc='family-app-group-summary']::after {
    content: '';
    width: 0.45rem;
    height: 0.45rem;
    margin-inline-start: auto;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translate(-0.1rem, -0.1rem);
    opacity: 0.55;
  }

  [data-dwc='family-app-group'][open]
    > [data-dwc='family-app-group-summary']::after {
    transform: rotate(-135deg) translate(-0.16rem, -0.16rem);
  }

  /* Le compte n'est pas décoratif : sans lui, un groupe replié ne dit pas
     s'il cache une application ou six. */
  [data-dwc='family-app-group-count'] {
    padding: 0.05rem 0.45rem;
    border-radius: 999px;
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
    color: var(--dwc-text-soft, GrayText);
    font-size: 0.8em;
    font-weight: 400;
  }

  [data-dwc='family-app'] {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.6rem 0.75rem;
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.8);
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
    text-decoration: none;
    color: inherit;
    transition:
      border-color 0.15s ease,
      transform 0.15s ease;
  }

  /* Le liseré seul ne disait pas que la carte est cliquable : HUIT apps sur
     treize avaient ajouté ce même relèvement d'un pixel. Il entre ici. */
  [data-dwc='family-app']:hover {
    border-color: var(--dwc-primary, CanvasText);
    transform: translateY(-1px);
  }

  /*
   * Variante `showRepoLinks` : la carte gagne un second lien, vers le DÉPÔT.
   * Deux destinations distinctes = deux ancres, jamais une ancre imbriquée
   * (invalide, et un lecteur d'écran n'en annonce qu'une). C'est donc le `<li>`
   * qui devient la carte visible, et le lien application qui rend sa bordure.
   */
  [data-dwc='family-app-item'][data-with-repo] {
    display: flex;
    align-items: center;
    gap: 0.15rem;
    padding-inline-end: 0.35rem;
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.8);
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
  }

  [data-dwc='family-app-item'][data-with-repo]:hover {
    border-color: var(--dwc-primary, CanvasText);
  }

  [data-dwc='family-app-item'][data-with-repo] > [data-dwc='family-app'] {
    flex: 1 1 auto;
    min-width: 0;
    border-color: transparent;
    background: none;
  }

  [data-dwc='family-app-repo'] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    min-width: 2.75rem;
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.6);
    color: var(--dwc-text-soft, GrayText);
  }

  [data-dwc='family-app-repo']:hover {
    color: var(--dwc-primary, CanvasText);
    background: var(
      --dwc-primary-soft,
      color-mix(in srgb, CanvasText 6%, Canvas)
    );
  }

  [data-dwc='family-app-icon'] {
    width: 2.5rem;
    height: 2.5rem;
    flex: none;
    border-radius: 0.7rem;
  }

  /* Repli du composant quand l'icône distante échoue : initiale du nom. */
  span[data-dwc='family-app-icon'] {
    display: grid;
    place-items: center;
    background: var(--dwc-primary, CanvasText);
    color: var(--dwc-primary-contrast, Canvas);
    font-weight: 700;
  }

  [data-dwc='family-app-body'] {
    display: grid;
    gap: 0.1rem;
    min-width: 0;
    flex: 1 1 auto;
  }

  /* ONZE apps sur treize avaient ajouté ce `flex-wrap`. Sans lui, un nom long
     suivi de son badge de maturité déborde de la carte au lieu de passer à la
     ligne — d'autant plus en `layout: 'list'`, où la carte n'a que la largeur
     du tiroir. */
  [data-dwc='family-app-head'] {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
  }

  [data-dwc='family-app-name'] {
    font-weight: 600;
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  [data-dwc='family-app-desc'] {
    font-size: var(--text-fluid-xs, 0.8rem);
    color: var(--dwc-text-soft, GrayText);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  [data-dwc='maturity'] {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 0.1rem 0.4rem;
    border-radius: 999px;
    border: 1px solid currentColor;
  }

  /*
   * `--dwc-success` et `--dwc-warning` sont des couleurs de SIGNAL, calibrées
   * pour une pastille ou un liseré — pas pour du texte de 0.68rem (~10,9 px),
   * que WCAG soumet au seuil 4,5:1. Employées telles quelles, elles tombaient
   * sous ce seuil dans quinze palettes sur trente-six (`stable`) et douze
   * (`beta`) : le libellé de maturité était illisible dans presque la moitié
   * du parc, sur l'écran même qui liste les autres apps.
   *
   * Le mélange vers `--dwc-text` corrige sans imposer de couleur : il pousse
   * la teinte vers celle du texte de l'app, donc vers le contraste, dans les
   * deux schémas — plus sombre en clair, plus clair en sombre. 60 % est le
   * point où les TRENTE-SIX palettes passent (mesuré, cf. `themes.test.mjs`),
   * en gardant la teinte reconnaissable ; ne pas remonter cette valeur sans
   * rejouer le test.
   */
  [data-dwc='maturity'][data-maturity='stable'] {
    color: color-mix(
      in srgb,
      var(--dwc-success, light-dark(#15803d, #4ade80)) 60%,
      var(--dwc-text, CanvasText)
    );
  }

  [data-dwc='maturity'][data-maturity='beta'] {
    color: color-mix(
      in srgb,
      var(--dwc-warning, light-dark(#b45309, #fbbf24)) 60%,
      var(--dwc-text, CanvasText)
    );
  }

  [data-dwc='maturity'][data-maturity='alpha'] {
    color: var(--dwc-text-soft, GrayText);
  }

  [data-dwc='family-app-arrow'] {
    color: var(--dwc-text-soft, GrayText);
    line-height: 0;
    flex: none;
  }

  /* ── Button ────────────────────────────────────────────────────────────── *
   * Quatre apps avaient convergé sur `primary | secondary | ghost | danger`.
   * `outline` complète le jeu. La cible tactile tient à TOUTES les tailles :
   * c'est le défaut que les copies locales laissaient passer en `sm`.
   * ────────────────────────────────────────────────────────────────────── */

  [data-dwc='button'] {
    font: inherit;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    border: 1px solid transparent;
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.7);
    cursor: pointer;
    text-decoration: none;
    /* `sm` reste tapable : on joue sur le padding, jamais sous 2,75 rem. */
    min-height: 2.75rem;
  }

  [data-dwc='button'][data-size='sm'] {
    font-size: var(--text-fluid-xs, 0.8rem);
    padding-inline: 0.75rem;
  }

  [data-dwc='button'][data-size='md'] {
    font-size: var(--text-fluid-sm, 0.9rem);
    padding-inline: 1rem;
  }

  [data-dwc='button'][data-size='lg'] {
    font-size: var(--text-fluid-base, 1rem);
    padding-inline: 1.5rem;
    min-height: 3.25rem;
  }

  [data-dwc='button'][data-block] {
    width: 100%;
  }

  [data-dwc='button'][data-icon-only] {
    padding-inline: 0;
    aspect-ratio: 1;
    min-width: 2.75rem;
  }

  [data-dwc='button'][data-variant='primary'] {
    background: var(--dwc-primary, CanvasText);
    color: var(--dwc-primary-contrast, Canvas);
  }

  [data-dwc='button'][data-variant='secondary'] {
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
    color: var(--dwc-text, CanvasText);
    /* Contour d'un contrôle : même règle que le champ de saisie. */
    border-color: var(
      --dwc-border-strong,
      var(--dwc-border, color-mix(in srgb, CanvasText 40%, Canvas))
    );
  }

  [data-dwc='button'][data-variant='outline'] {
    background: transparent;
    color: var(--dwc-primary, CanvasText);
    border-color: currentColor;
  }

  [data-dwc='button'][data-variant='ghost'] {
    background: transparent;
    color: var(--dwc-text, CanvasText);
  }

  [data-dwc='button'][data-variant='danger'] {
    background: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
    color: var(--dwc-primary-contrast, Canvas);
  }

  /*
   * LE SURVOL NE REPEINT PAS LE BOUTON. Il passait par `filter:
   * brightness(1.08)`, qui éclaircit le fond ET l'encre : sous une encre
   * claire, le fond se rapproche d'elle. Sur les 38 palettes de `themes.js`,
   * le contraste baissait au survol dans 20 pour `primary` comme pour `danger`,
   * et passait sous 4,5:1 dans onze cas qui le tenaient au repos : huit en
   * `danger`, deux en `outline`, mister-molkky clair en `primary` (4,99 →
   * 4,39). Aucun sens de variation n'est sûr pour toutes les paires : foncer
   * aide sous le blanc et nuit sous une encre sombre.
   *
   * Le survol se dit donc sans toucher aux couleurs, et le contraste au survol
   * EST celui du repos, que les palettes garantissent. Le relèvement d'un pixel
   * est celui de la carte d'application, que huit apps avaient écrit ; le halo
   * des deux aplats, de leur propre teinte, est celui que mister-miss-koh
   * posait déjà sur ses boutons.
   *
   * Sous `(hover: hover)` seulement : sur un écran tactile, `:hover` COLLE
   * après le tap (relevé par mister-miss-koh), et le bouton restait éclairci.
   */
  @media (hover: hover) {
    [data-dwc='button']:hover:not(:disabled):not([aria-disabled='true']) {
      transform: translateY(-1px);
    }

    [data-dwc='button'][data-variant='primary']:hover:not(:disabled):not(
        [aria-disabled='true']
      ) {
      box-shadow: 0 4px 12px
        color-mix(in srgb, var(--dwc-primary, CanvasText) 35%, transparent);
    }

    [data-dwc='button'][data-variant='danger']:hover:not(:disabled):not(
        [aria-disabled='true']
      ) {
      box-shadow: 0 4px 12px
        color-mix(
          in srgb,
          var(--dwc-danger, light-dark(#b91c1c, #fb7185)) 35%,
          transparent
        );
    }
  }

  /*
   * LES DEUX FAÇONS D'ÊTRE BLOQUÉ, habillées pareil. Seul `:disabled` l'était,
   * alors que ce bouton bloque AUSSI par `aria-disabled` — pour `loading`, et
   * depuis peu pour le `disabledProps` de `react/use-action-guard`. Un bouton
   * gardé avait donc l'air ACTIF tout en étant inerte : le pire des deux
   * mondes, et exactement ce qui a fait retomber `bac-sable` (#23) sur
   * `disabled` natif, qui vole le focus et empêche de découvrir le motif.
   */
  [data-dwc='button']:disabled,
  [data-dwc='button'][aria-disabled='true'] {
    opacity: 0.45;
    filter: none;
    cursor: not-allowed;
  }

  [data-dwc='button-spinner'] {
    width: 1em;
    height: 1em;
    flex: none;
    border-radius: 999px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    animation: dwc-spin 0.7s linear infinite;
  }

  /* ── Field ─────────────────────────────────────────────────────────────── */

  [data-dwc='field'] {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
  }

  [data-dwc='field-label'] {
    font-size: var(--text-fluid-sm, 0.9rem);
    font-weight: 600;
  }

  [data-dwc='field-control'] {
    font: inherit;
    /* 16 px minimum : en dessous, iOS zoome au focus. */
    font-size: max(1rem, var(--text-fluid-sm, 0.9rem));
    width: 100%;
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    color: var(--dwc-text, CanvasText);
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
    /*
     * `border-strong`, pas `border` : le pourtour d'un champ est la seule chose
     * qui dit où cliquer. WCAG 1.4.11 lui demande 3:1 — ce qu'un filet de
     * séparation discret ne tient pas (test/tokens.test.mjs).
     */
    border: 1px solid
      var(
        --dwc-border-strong,
        var(--dwc-border, color-mix(in srgb, CanvasText 40%, Canvas))
      );
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.7);
  }

  [data-dwc='field-control'][data-multiline] {
    min-height: 5rem;
    resize: vertical;
  }

  [data-dwc='field'][data-invalid] [data-dwc='field-control'] {
    border-color: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
  }

  [data-dwc='field-hint'] {
    margin: 0;
    font-size: var(--text-fluid-xs, 0.8rem);
    color: var(--dwc-text-soft, GrayText);
  }

  /* Le bouton « forcer la mise à jour » et sa phrase d'explication : empilés,
     l'indice sous le bouton. Le groupe n'avait qu'une règle d'impression — il
     s'affichait donc en ligne, l'indice collé au bouton. */
  [data-dwc='update-button-group'] {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
  }

  [data-dwc='update-button-hint'] {
    margin: 0;
    font-size: var(--text-fluid-xs, 0.8rem);
    color: var(--dwc-text-soft, GrayText);
  }

  [data-dwc='field-error'] {
    margin: 0;
    font-size: var(--text-fluid-xs, 0.8rem);
    color: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
  }

  /* ── Skeleton ──────────────────────────────────────────────────────────── */

  [data-dwc='skeleton-group'] {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }

  /* Le libellé est pour les lecteurs d'écran uniquement. */
  [data-dwc='skeleton-label'] {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  [data-dwc='skeleton'] {
    display: block;
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
    animation: dwc-pulse 1.6s ease-in-out infinite;
  }

  [data-dwc='skeleton'][data-radius='sm'] {
    border-radius: 0.25rem;
  }
  [data-dwc='skeleton'][data-radius='md'] {
    border-radius: 0.5rem;
  }
  [data-dwc='skeleton'][data-radius='lg'] {
    border-radius: var(--dwc-radius, 0.75rem);
  }
  [data-dwc='skeleton'][data-radius='full'] {
    border-radius: 999px;
  }

  /* ── Sheet ─────────────────────────────────────────────────────────────── */

  [data-dwc='sheet'] {
    position: fixed;
    inset: 0;
    z-index: 50;
    display: flex;
    align-items: flex-end;
    justify-content: center;
  }

  @media (min-width: 40rem) {
    [data-dwc='sheet'] {
      align-items: center;
    }
  }

  [data-dwc='sheet-backdrop'] {
    position: absolute;
    inset: 0;
    background: rgb(0 0 0 / 0.45);
  }

  [data-dwc='sheet-panel'] {
    position: relative;
    width: 100%;
    max-height: 88dvh;
    /* Colonne, et c'est le CORPS qui défile : un pied épinglé dans un panneau
       défilant défilerait avec lui, ce qui est exactement ce qu'on veut éviter. */
    display: flex;
    flex-direction: column;
    padding: var(--spacing-fluid-md, 1rem);
    /* Home bar iOS : le contenu ne se glisse pas dessous. */
    padding-bottom: calc(
      var(--spacing-fluid-md, 1rem) + max(env(safe-area-inset-bottom), 0px)
    );
    background: var(--dwc-surface, Canvas);
    color: var(--dwc-text, CanvasText);
    border-radius: calc(var(--dwc-radius, 0.75rem) * 1.6)
      calc(var(--dwc-radius, 0.75rem) * 1.6) 0 0;
    box-shadow: var(--dwc-shadow, none);
    outline: none;
    /* `backwards`, jamais `both` — voir « Animations » en fin de fichier : un
       panneau qui garde une transformation après son entrée emprisonne les
       descendants `position: fixed`, dont le ConfirmDialog qu'on ouvre depuis
       une feuille. */
    animation: dwc-rise 0.22s ease-out backwards;
  }

  @media (min-width: 40rem) {
    [data-dwc='sheet-panel'] {
      max-width: 30rem;
      border-radius: calc(var(--dwc-radius, 0.75rem) * 1.6);
    }
  }

  [data-dwc='sheet-head'] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: var(--spacing-fluid-sm, 0.75rem);
  }

  [data-dwc='sheet-title'] {
    margin: 0;
    font-size: var(--text-fluid-lg, 1.125rem);
  }

  [data-dwc='sheet-body'] {
    flex: 1 1 auto;
    overflow-y: auto;
    /* Le défilement tactile ne doit pas entraîner la page derrière. */
    overscroll-behavior: contain;
  }

  /* Barre d'actions épinglée : reste visible pendant que le corps défile.
     Quinze des vingt-trois feuilles de miss-uwh en dépendent. */
  [data-dwc='sheet-footer'] {
    flex: none;
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-top: var(--spacing-fluid-sm, 0.75rem);
    padding-top: var(--spacing-fluid-sm, 0.75rem);
    border-top: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
  }

  [data-dwc='sheet-close'] {
    font: inherit;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.75rem;
    min-height: 2.75rem;
    flex: none;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--dwc-text-soft, GrayText);
    cursor: pointer;
  }

  /* ── Stat ──────────────────────────────────────────────────────────────── */

  [data-dwc='stat'] {
    margin: 0;
    display: grid;
    gap: 0.15rem;
    padding: var(--spacing-fluid-md, 1rem);
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: var(--dwc-radius, 0.75rem);
    background: var(--dwc-surface, Canvas);
    box-shadow: var(--dwc-shadow, none);
  }

  /* Le <dt> porte l'icône : plus d'en-tête <div>, qui rendait le <dl>
     invalide. La mise en ligne libellé / icône passe ici. */
  [data-dwc='stat-label'] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    font-size: var(--text-fluid-sm, 0.9rem);
    color: var(--dwc-text-soft, GrayText);
  }

  [data-dwc='stat-icon'] {
    color: var(--dwc-text-soft, GrayText);
    line-height: 0;
  }

  [data-dwc='stat-value'] {
    margin: 0;
    font-size: var(--text-fluid-2xl, 1.75rem);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
  }

  [data-dwc='stat-delta'] {
    margin: 0;
    font-size: var(--text-fluid-xs, 0.8rem);
    font-variant-numeric: tabular-nums;
    color: var(--dwc-text-soft, GrayText);
  }

  [data-dwc='stat-delta'][data-trend='up'] {
    color: var(--dwc-success, light-dark(#15803d, #4ade80));
  }

  [data-dwc='stat-delta'][data-trend='down'] {
    color: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
  }

  /* Libellé de tendance : lu, pas vu — la couleur ne porte pas seule le sens. */
  [data-dwc='stat-trend-label'] {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* ── Card ──────────────────────────────────────────────────────────────── *
   * Dix apps avaient une carte ; les deux copies de genius et uwh ne
   * différaient que par le nom de la variable de surface. Ce bloc est ce
   * qu'elles écrivaient toutes : fond, filet, rayon, padding — sur les jetons
   * `--dwc-*`, que l'app teinte où elle veut.
   * ─────────────────────────────────────────────────────────────────────── */

  [data-dwc='card'] {
    display: block;
    padding: var(--spacing-fluid-md, 1rem);
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: var(--dwc-radius, 0.75rem);
    background: var(--dwc-surface, Canvas);
    color: var(--dwc-text, CanvasText);
    box-shadow: var(--dwc-shadow, none);
  }

  /* Le contenu touche les bords ; le rayon est conservé par `overflow`. */
  [data-dwc='card'][data-padding='none'] {
    padding: 0;
    overflow: hidden;
  }

  /* Une carte-lien ou une carte-bouton garde la couleur du texte : le lien
     bleu souligné sur toute une surface est ce que les copies évitaient à
     la main. Le focus, lui, reste visible. */
  a[data-dwc='card'],
  button[data-dwc='card'] {
    color: inherit;
    text-decoration: none;
    text-align: inherit;
    font: inherit;
    cursor: pointer;
  }

  a[data-dwc='card']:focus-visible,
  button[data-dwc='card']:focus-visible {
    outline: 2px solid var(--dwc-primary, CanvasText);
    outline-offset: 2px;
  }

  [data-dwc='card-header'] {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
  }

  [data-dwc='card-heading'] {
    min-width: 0;
  }

  [data-dwc='card-title'] {
    margin: 0;
    font-size: var(--text-fluid-base, 1rem);
    font-weight: 600;
    line-height: 1.3;
  }

  [data-dwc='card-subtitle'] {
    margin: 0.15rem 0 0;
    font-size: var(--text-fluid-xs, 0.8rem);
    color: var(--dwc-text-soft, GrayText);
  }

  [data-dwc='card-action'] {
    flex: none;
  }

  /* ── Sparkline, BarChart & Gauge ───────────────────────────────────────── *
   * Les composants (`/react/sparkline`) rendent la géométrie, jamais la
   * couleur : tout leur tracé passe par `currentColor`. L'habillage pose donc
   * UNE encre — la primaire — sur le conteneur ; recolorer un graphique, c'est
   * une règle `color` de l'app, rien de plus. Les dimensions sont des défauts
   * (courbe et jauge portent les leurs par props), pas des contraintes.
   * ────────────────────────────────────────────────────────────────────── */

  [data-dwc='sparkline'] {
    display: inline-flex;
    color: var(--dwc-primary, CanvasText);
    line-height: 0;
    vertical-align: middle;
  }

  [data-dwc='bars'] {
    display: inline-flex;
    align-items: flex-end;
    gap: 0.125rem;
    width: 7.5rem;
    height: 2rem;
    color: var(--dwc-primary, CanvasText);
    vertical-align: middle;
  }

  /* La hauteur vient du composant (proportion en %) ; la largeur d'ici. */
  [data-dwc='bars-bar'] {
    flex: 1 1 0;
    min-width: 0.125rem;
    background: currentColor;
    border-radius: 0.125rem 0.125rem 0 0;
  }

  [data-dwc='gauge'] {
    display: inline-block;
    width: 7.5rem;
    height: 0.5rem;
    border-radius: 999px;
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
    color: var(--dwc-primary, CanvasText);
    overflow: hidden;
    vertical-align: middle;
  }

  [data-dwc='gauge-fill'] {
    display: block;
    height: 100%;
    background: currentColor;
    border-radius: inherit;
  }

  /* L'alternative textuelle calculée par `describeSeries` : lue, pas vue —
     même motif que `skeleton-label`. Le composant pose aussi `sr-only`, mais
     l'habillage ne doit pas dépendre d'un utilitaire que l'app a pu purger. */
  [data-dwc='sparkline-text'],
  [data-dwc='bars-text'] {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* ── Badge ─────────────────────────────────────────────────────────────── *
   * Un ton SÉMANTIQUE (ce que la pastille veut dire), décliné en trois
   * intensités. La couleur du ton est résolue une fois dans `--_dwc-tone`,
   * puis réutilisée par les trois variantes.
   * ────────────────────────────────────────────────────────────────────── */

  [data-dwc='badge'] {
    --_dwc-tone: var(--dwc-text-soft, GrayText);
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.15rem 0.55rem;
    border: 1px solid transparent;
    border-radius: 999px;
    font-size: var(--text-fluid-xs, 0.8rem);
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
  }

  /* L'axe de taille demandé par mister-doc pour ses pastilles de calendrier :
     `md` est l'ancien rendu, `xs` tient dans une case de 1,5 rem. */
  [data-dwc='badge'][data-size='sm'] {
    padding: 0.1rem 0.45rem;
    font-size: var(--text-fluid-2xs, 0.72rem);
  }
  [data-dwc='badge'][data-size='xs'] {
    gap: 0.2rem;
    padding: 0 0.35rem;
    font-size: 0.68rem;
    line-height: 1.4;
  }

  [data-dwc='badge'][data-tone='brand'] {
    --_dwc-tone: var(--dwc-primary, CanvasText);
  }
  [data-dwc='badge'][data-tone='success'] {
    --_dwc-tone: var(--dwc-success, light-dark(#15803d, #4ade80));
  }
  [data-dwc='badge'][data-tone='warning'] {
    --_dwc-tone: var(--dwc-warning, light-dark(#b45309, #fbbf24));
  }
  [data-dwc='badge'][data-tone='danger'] {
    --_dwc-tone: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
  }
  [data-dwc='badge'][data-tone='info'] {
    --_dwc-tone: var(--dwc-info, light-dark(#1d4ed8, #60a5fa));
  }

  /*
   * Deux variantes seulement, et c'est un choix.
   *
   * Une variante « pleine » (ton en aplat, texte inversé) a été essayée puis
   * retirée : avec UNE seule couleur par ton, le texte posé dessus ne peut pas
   * tenir 4,5:1 partout. Le contrôle de contraste du showroom la voyait échouer
   * dans 11 thèmes sur 14 — un ambre ou un vert clair ne portent pas de texte
   * blanc. Il aurait fallu une seconde couleur par ton (« 600 » pour le fond),
   * c'est-à-dire doubler le contrat pour un gain cosmétique.
   *
   * Les pastilles réellement utilisées dans la famille sont d'ailleurs
   * teintées, pas pleines (miss-uwh : fond à 14 %, texte foncé).
   */
  /*
   * Le texte n'est PAS le ton brut. Un ambre ou un vert de marque posés tels
   * quels sur un fond clair tombent à 2:1 — mesuré sur les 14 thèmes du
   * showroom. On mélange donc le ton avec la couleur de texte du thème :
   * quasi-noire en clair, quasi-blanche en sombre. La teinte est conservée,
   * le contraste devient tenable, et aucun `light-dark()` n'est nécessaire
   * puisque `--dwc-text` suit déjà le schéma.
   *
   * C'est exactement ce que miss-uwh faisait à la main (#166534 au lieu de son
   * vert de token) — sauf qu'ici c'est dérivé, donc valable pour tout thème.
   */
  [data-dwc='badge'] {
    --_dwc-tone-ink: color-mix(
      in srgb,
      var(--_dwc-tone) 48%,
      var(--dwc-text, CanvasText)
    );
  }

  [data-dwc='badge'][data-variant='soft'] {
    background: color-mix(in srgb, var(--_dwc-tone) 16%, transparent);
    color: var(--_dwc-tone-ink);
  }

  [data-dwc='badge'][data-variant='outline'] {
    color: var(--_dwc-tone-ink);
    border-color: color-mix(in srgb, var(--_dwc-tone) 55%, transparent);
  }

  /* ── ConfirmDialog ─────────────────────────────────────────────────────── *
   * Même charpente que `Sheet` (voile + panneau centré), rôle différent :
   * `alertdialog`. Les sept copies mesurées divergeaient sur le bouton mis en
   * avant — ici c'est TOUJOURS l'annulation qui reçoit le focus, et c'est la
   * confirmation qui est peinte, jamais l'inverse.
   * ────────────────────────────────────────────────────────────────────── */

  [data-dwc='confirm'] {
    position: fixed;
    inset: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: max(env(safe-area-inset-top), 1rem)
      max(env(safe-area-inset-right), 1rem)
      max(env(safe-area-inset-bottom), 1rem)
      max(env(safe-area-inset-left), 1rem);
  }

  [data-dwc='confirm-backdrop'] {
    position: absolute;
    inset: 0;
    background: rgb(0 0 0 / 0.55);
  }

  [data-dwc='confirm-panel'] {
    position: relative;
    width: 100%;
    max-width: 24rem;
    max-height: 88dvh;
    overflow-y: auto;
    padding: var(--spacing-fluid-md, 1rem);
    background: var(--dwc-surface, Canvas);
    color: var(--dwc-text, CanvasText);
    border-radius: calc(var(--dwc-radius, 0.75rem) * 1.4);
    box-shadow: var(--dwc-shadow, none);
    outline: none;
    animation: dwc-rise 0.18s ease-out backwards;
  }

  [data-dwc='confirm-title'] {
    margin: 0;
    font-size: var(--text-fluid-lg, 1.125rem);
  }

  [data-dwc='confirm-body'] {
    margin-top: 0.35rem;
    font-size: var(--text-fluid-sm, 0.9rem);
    color: var(--dwc-text-soft, GrayText);
  }

  [data-dwc='confirm-actions'] {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-top: var(--spacing-fluid-md, 1rem);
  }

  [data-dwc='confirm-cancel'],
  [data-dwc='confirm-confirm'] {
    font: inherit;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 2.75rem;
    padding-inline: 1rem;
    border-radius: var(--dwc-radius, 0.75rem);
    cursor: pointer;
  }

  [data-dwc='confirm-cancel'] {
    border: 1px solid
      var(
        --dwc-border-strong,
        var(--dwc-border, color-mix(in srgb, CanvasText 40%, Canvas))
      );
    background: var(--dwc-surface, Canvas);
    color: var(--dwc-text, CanvasText);
  }

  [data-dwc='confirm-confirm'] {
    border: 1px solid var(--dwc-primary, CanvasText);
    background: var(--dwc-primary, CanvasText);
    color: var(--dwc-primary-contrast, Canvas);
  }

  /* Mono-action (`cancelLabel: null`) : seul dans la rangée, le bouton la
     prend en entier — le rendu de l'`ErrorModal` de mister-puzzle, dont
     l'action unique était déjà pleine largeur. `:only-child` suffit : pas de
     nouvel attribut.
     CE N'EST PAS UN RENDU UNIVERSEL. L'alerte de mister-cim10 était au
     contraire COMPACTE et alignée à droite (`justify-content: flex-end` de la
     rangée + `min-width: 86px`), comme la confirmation à deux boutons juste à
     côté ; sa migration (#27) a donc dû reposer l'écart chez elle. C'est une
     identité d'app, pas un défaut : `flex: 0 0 auto; min-width: …` en CSS non
     « layered » reprend la main en deux lignes. */
  [data-dwc='confirm-confirm']:only-child {
    flex: 1;
  }

  /* Destructif : la couleur ne porte pas seule le sens — le libellé passe à
     « Supprimer » côté composant. Elle le renforce, c'est tout. */
  [data-dwc='confirm'][data-destructive] [data-dwc='confirm-confirm'] {
    border-color: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
    background: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
  }

  [data-dwc='confirm-cancel'][aria-disabled='true'],
  [data-dwc='confirm-confirm'][aria-disabled='true'] {
    opacity: 0.55;
    cursor: progress;
  }

  /* ── Toast ─────────────────────────────────────────────────────────────── *
   * Deux régions vivantes, montées en permanence et VIDES au repos : un
   * lecteur d'écran n'annonce une insertion que dans une région déjà présente.
   * Le message, lui, ne porte aucun rôle — c'est la double annonce constatée
   * dans deux apps.
   * ────────────────────────────────────────────────────────────────────── */

  [data-dwc='toast-viewport'] {
    position: fixed;
    inset-inline: 0;
    /* Le plancher : la zone sûre iOS, ou la barre basse collée quand il y en
       a une — un message ne surgit jamais SOUS la barre (voir BottomNav). */
    bottom: var(--_dwc-bottom-clearance);
    z-index: 70;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: var(--spacing-fluid-sm, 0.75rem);
    /* Le conteneur ne doit pas intercepter les clics de la page en dessous ;
       chaque message les reprend. */
    pointer-events: none;
  }

  [data-dwc='toast-region'] {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
  }

  [data-dwc='toast'] {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    width: 100%;
    max-width: 26rem;
    padding: 0.65rem 0.75rem;
    font-size: var(--text-fluid-sm, 0.9rem);
    background: var(--dwc-surface, Canvas);
    color: var(--dwc-text, CanvasText);
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-inline-start: 4px solid var(--dwc-info, light-dark(#1d4ed8, #60a5fa));
    border-radius: var(--dwc-radius, 0.75rem);
    box-shadow: var(--dwc-shadow, none);
    animation: dwc-rise 0.18s ease-out backwards;
  }

  [data-dwc='toast'][data-tone='success'] {
    border-inline-start-color: var(--dwc-success, light-dark(#15803d, #4ade80));
  }

  /* `danger` est le mot de la famille (celui de `Badge`), `error` son ancien
     nom — les deux rendent le même trait tant que les apps n'ont pas migré. */
  [data-dwc='toast'][data-tone='error'],
  [data-dwc='toast'][data-tone='danger'] {
    border-inline-start-color: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
  }

  [data-dwc='toast-message'] {
    flex: 1 1 auto;
    min-width: 0;
  }

  /* L'action — « Annuler » après une suppression. UN TEXTE, PAS UN APLAT, et
     ce n'est pas une préférence : un aplat demanderait un contour transparent
     (invisible en contraste forcé) et un libellé en `--dwc-primary-contrast`
     (blanc sur blanc à l'impression, les fonds étant retirés). Le soulignement
     la distingue du message sans que la couleur porte seule le sens. */
  [data-dwc='toast-action'] {
    font: inherit;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 2.75rem;
    padding-inline: 0.5rem;
    /* Marge négative : la cible tactile reste entière sans épaissir le
       message. */
    margin-block: -0.65rem;
    flex: none;
    border: 0;
    border-radius: var(--dwc-radius, 0.75rem);
    background: none;
    color: var(--dwc-primary, CanvasText);
    text-decoration: underline;
    cursor: pointer;
  }

  [data-dwc='toast-close'] {
    font: inherit;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.75rem;
    min-height: 2.75rem;
    /* Marge négative : la cible tactile reste entière sans épaissir le
       message. */
    margin: -0.65rem -0.4rem -0.65rem 0;
    flex: none;
    border: 0;
    border-radius: 999px;
    background: none;
    color: var(--dwc-text-soft, GrayText);
    cursor: pointer;
  }

  /* ── AppHeader ─────────────────────────────────────────────────────────── *
   * Le troisième côté du cadre. Neuf apps écrivaient cette mise en page :
   * collant, zone sûre iOS sur le padding du <header>, fond translucide,
   * filet en bas, un titre qui se tronque, une rangée d'actions.
   * ─────────────────────────────────────────────────────────────────────── */

  [data-dwc='app-header'] {
    position: relative;
    z-index: 30;
    padding-top: env(safe-area-inset-top, 0px);
    border-bottom: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    background: color-mix(in srgb, var(--dwc-surface, Canvas) 92%, transparent);
    color: var(--dwc-text, CanvasText);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
  }

  [data-dwc='app-header'][data-sticky] {
    position: sticky;
    top: 0;
  }

  [data-dwc='app-header-row'] {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: var(--dwc-header-row-height, 3.25rem);
    padding: 0.5rem max(env(safe-area-inset-right, 0px), 1rem) 0.5rem
      max(env(safe-area-inset-left, 0px), 1rem);
  }

  [data-dwc='app-header-title'] {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    font-size: var(--text-fluid-lg, 1.125rem);
    font-weight: 700;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  [data-dwc='app-header-leading'],
  [data-dwc='app-header-actions'] {
    flex: none;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
  }

  /* Le retour : cible de 2,75 rem, sans bord, dans la couleur du texte —
     un lien comme un bouton. */
  [data-dwc='app-header-back'] {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.75rem;
    min-height: 2.75rem;
    margin-inline-start: -0.5rem;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: inherit;
    text-decoration: none;
    cursor: pointer;
  }

  [data-dwc='app-header-back']:focus-visible {
    outline: 2px solid var(--dwc-primary, CanvasText);
    outline-offset: 2px;
  }

  /* Lien d'évitement d'`AppShell` : invisible tant qu'il n'a pas le focus —
     même boîte que `bottom-nav-sr`, sans dépendre des utilitaires Tailwind. */
  [data-dwc='app-shell-skip'] {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  [data-dwc='app-shell-skip']:focus,
  [data-dwc='app-shell-skip']:focus-visible {
    position: fixed;
    top: max(env(safe-area-inset-top, 0px), 0.5rem);
    left: max(env(safe-area-inset-left, 0px), 0.5rem);
    z-index: 100;
    width: auto;
    height: auto;
    margin: 0;
    padding: 0.5rem 0.75rem;
    overflow: visible;
    clip: auto;
    white-space: normal;
    border-radius: var(--dwc-radius, 0.75rem);
    background: var(--dwc-surface, Canvas);
    color: var(--dwc-text, CanvasText);
    border: 1px solid
      var(
        --dwc-border-strong,
        var(--dwc-border, color-mix(in srgb, CanvasText 40%, Canvas))
      );
    text-decoration: none;
  }

  [data-dwc='app-header-extra'] {
    padding: 0 max(env(safe-area-inset-right, 0px), 1rem) 0.5rem
      max(env(safe-area-inset-left, 0px), 1rem);
  }

  /* ── PageContainer ─────────────────────────────────────────────────────── *
   * Centré, borné à un palier, zones sûres comprises — celle du bas surtout,
   * sans laquelle le dernier bouton d'une vue colle à la barre d'onglets.
   * ─────────────────────────────────────────────────────────────────────── */

  [data-dwc='page-container'] {
    width: 100%;
    margin-inline: auto;
    padding: var(--spacing-fluid-md, 1rem)
      max(env(safe-area-inset-right, 0px), 1rem)
      calc(var(--spacing-fluid-lg, 1.5rem) + env(safe-area-inset-bottom, 0px))
      max(env(safe-area-inset-left, 0px), 1rem);
  }

  [data-dwc='page-container'][data-padding='none'] {
    padding: 0;
  }

  [data-dwc='page-container'][data-width='sm'] {
    max-width: 28rem;
  }
  [data-dwc='page-container'][data-width='md'] {
    max-width: 36rem;
  }
  [data-dwc='page-container'][data-width='lg'] {
    max-width: 48rem;
  }
  [data-dwc='page-container'][data-width='xl'] {
    max-width: 64rem;
  }
  [data-dwc='page-container'][data-width='full'] {
    max-width: none;
  }

  /* La place d'une barre basse collée (`BottomNav placement="fixed"`) : sans
     ce dégagement, le dernier élément de la vue passe sous la barre. Après
     `data-padding='none'` exprès — réserver n'est pas une marge. L'empreinte
     elle-même n'est écrite qu'une fois, sur `:root`, dans la section BottomNav :
     les toasts et le bandeau de mise à jour lisent la même. */
  [data-dwc='page-container'][data-reserve='bottom-nav'] {
    padding-bottom: var(--_dwc-bottom-nav-reserve);
  }

  /* ── LoginForm & MfaChallenge ──────────────────────────────────────────── *
   * Deux formulaires d'un seul écran : une colonne, des champs espacés, une
   * erreur qui se voit, des boutons pleine largeur. L'app pose la carte ou
   * la page autour.
   * ─────────────────────────────────────────────────────────────────────── */

  [data-dwc='login-form'],
  [data-dwc='mfa-challenge'] {
    display: grid;
    gap: var(--spacing-fluid-sm, 0.75rem);
  }

  [data-dwc='login-form-title'],
  [data-dwc='mfa-challenge-title'] {
    margin: 0;
    font-size: var(--text-fluid-xl, 1.35rem);
    font-weight: 700;
    line-height: 1.2;
  }

  [data-dwc='mfa-challenge-hint'] {
    margin: 0;
    font-size: var(--text-fluid-sm, 0.9rem);
    color: var(--dwc-text-soft, GrayText);
  }

  [data-dwc='login-form-error'],
  [data-dwc='mfa-challenge-error'] {
    margin: 0;
    padding: 0.6rem 0.8rem;
    border-radius: var(--dwc-radius, 0.75rem);
    background: color-mix(
      in srgb,
      var(--dwc-danger, light-dark(#b91c1c, #fb7185)) 12%,
      transparent
    );
    color: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  [data-dwc='login-form-footer'] {
    font-size: var(--text-fluid-sm, 0.9rem);
    text-align: center;
  }

  /* ── BottomNav ─────────────────────────────────────────────────────────── *
   * L'onglet actif n'est JAMAIS distingué par la seule couleur : le composant
   * pose `aria-current`, ajoute un texte lu, et l'habillage ci-dessous y
   * ajoute un trait. Quatre des sept copies mesurées n'avaient que l'encre.
   * ────────────────────────────────────────────────────────────────────── */

  [data-dwc='bottom-nav'] {
    position: relative;
    display: flex;
    align-items: stretch;
    border-top: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    background: var(--dwc-surface, Canvas);
    padding-bottom: max(env(safe-area-inset-bottom), 0px);
  }

  /* Collée au bas de la fenêtre : `placement="fixed"`. Le socle habillait la
     barre sans la placer, et huit dépôts recopiaient cette règle (sept apps
     et le squelette, relevé du 05/09/2026). La place qu'elle occupe se
     réserve côté contenu : `<PageContainer reserve="bottom-nav">`. */
  [data-dwc='bottom-nav'][data-placement='fixed'] {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 20;
  }

  /*
   * L'EMPREINTE DU CHROME, ÉCRITE UNE FOIS. Quatre règles ont besoin des
   * mêmes hauteurs : la rangée d'`AppHeader`, la réserve de `PageContainer`,
   * le plancher des toasts et celui du bandeau de mise à jour. Figées à
   * plusieurs endroits, elles finiraient par diverger — c'est ce qui est
   * arrivé aux replis de couleur (audit du 06/09/2026). Une variable PUBLIQUE
   * et deux PRIVÉES (`--_dwc-*`, hors du contrat) :
   *
   *   --dwc-header-row-height     hauteur de la rangée titre (apps peuvent
   *                               la surcharger) ;
   *   --_dwc-bottom-nav-reserve   ce que la barre couvre : 4,5 rem + zone sûre ;
   *   --_dwc-bottom-clearance     ce que les surfaces flottantes dégagent en
   *                               bas — la zone sûre seule, ou la barre entière
   *                               quand il y en a une.
   *
   * SUR LA RACINE, ET PAS SUR LA BARRE. Une propriété personnalisée ne descend
   * que vers les descendants : posée sur `[data-placement='fixed']`, elle
   * n'atteindrait ni le bandeau ni les toasts, qui sont ses voisins. `:has()`
   * remonte donc la condition à `:root`. Un navigateur qui l'ignore (Firefox
   * avant 121 — largement disponible depuis 2026) jette cette seule règle : le
   * plancher reste la zone sûre, rien d'autre ne casse. D'où une règle À PART,
   * jamais glissée dans une liste de sélecteurs partagée — une liste est tout
   * ou rien, et `test/components-css.test.mjs` y veille.
   */
  :root {
    --dwc-header-row-height: 3.25rem;
    --_dwc-bottom-nav-reserve: calc(
      4.5rem + max(env(safe-area-inset-bottom), 0px)
    );
    --_dwc-bottom-clearance: max(env(safe-area-inset-bottom), 0px);
  }

  :root:has([data-dwc='bottom-nav'][data-placement='fixed']) {
    --_dwc-bottom-clearance: var(--_dwc-bottom-nav-reserve);
  }

  [data-dwc='bottom-nav-item'],
  [data-dwc='bottom-nav-more'] {
    font: inherit;
    font-size: var(--text-fluid-xs, 0.8rem);
    font-weight: 600;
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    /* Au-delà des 2,75 rem du contrat : une barre d'onglets se vise au pouce,
       et la plus petite copie mesurée tombait à ~38 px. */
    min-height: 3.25rem;
    padding: 0.35rem 0.25rem;
    border: 0;
    /* Le trait actif est réservé dès le repos : l'onglet ne bouge pas de deux
       pixels quand il devient courant. */
    border-block-start: 2px solid transparent;
    background: none;
    color: var(--dwc-text-soft, GrayText);
    text-decoration: none;
    cursor: pointer;
  }

  [data-dwc='bottom-nav-item'][data-current],
  [data-dwc='bottom-nav-more'][data-current] {
    color: var(--dwc-primary, CanvasText);
    border-block-start-color: var(--dwc-primary, CanvasText);
  }

  [data-dwc='bottom-nav-icon'] {
    display: inline-flex;
    line-height: 0;
  }

  /* LE CLIC QUI RÉPOND. Pendant que le morceau de la destination arrive,
     `BottomNav navigate` pose `data-pending` sur l'entrée cliquée et met le
     rôle d'icône `busy` à la place de la sienne : c'est lui qui tourne. Sous
     `prefers-reduced-motion`, la liste de `base` l'arrête — la pastille reste,
     immobile et déjà distincte de l'icône qu'elle remplace, et `aria-busy` le
     dit de toute façon. */
  [data-dwc='bottom-nav-item'][data-pending] [data-dwc='bottom-nav-icon'],
  [data-dwc='bottom-nav-drawer-item'][data-pending]
    [data-dwc='bottom-nav-icon'] {
    animation: dwc-spin 1s linear infinite;
  }

  /* Le libellé d'un onglet ne pousse pas la barre : à cinq entrées sur un
     écran de 320 px, un mot long débordait sur son voisin. */
  [data-dwc='bottom-nav-label'] {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  [data-dwc='bottom-nav-badge'] {
    position: absolute;
    transform: translate(0.9rem, -0.6rem);
    min-width: 1.05rem;
    padding-inline: 0.2rem;
    font-size: 0.65rem;
    line-height: 1.05rem;
    text-align: center;
    border-radius: 999px;
    background: var(--dwc-danger, light-dark(#b91c1c, #fb7185));
    color: var(--dwc-primary-contrast, Canvas);
  }

  [data-dwc='bottom-nav-drawer'] {
    position: absolute;
    inset-inline: 0.5rem;
    bottom: calc(100% + 0.5rem);
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.25rem;
    padding: 0.35rem;
    background: var(--dwc-surface, Canvas);
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: var(--dwc-radius, 0.75rem);
    box-shadow: var(--dwc-shadow, none);
  }

  [data-dwc='bottom-nav-drawer-item'] {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 2.75rem;
    padding-inline: 0.6rem;
    font-size: var(--text-fluid-sm, 0.9rem);
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.7);
    color: var(--dwc-text, CanvasText);
    text-decoration: none;
  }

  [data-dwc='bottom-nav-drawer-item'][data-current] {
    color: var(--dwc-primary, CanvasText);
  }

  /* Lu, pas vu — l'état courant et le sens d'une pastille. */
  [data-dwc='bottom-nav-sr'] {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* ── ThemeToggle ───────────────────────────────────────────────────────── *
   * L'état courant n'est pas porté par la seule icône : le composant recalcule
   * son nom accessible à chaque rendu. L'habillage ci-dessous ne fait que le
   * rendre visible.
   * ────────────────────────────────────────────────────────────────────── */

  [data-dwc='theme-toggle'] {
    font: inherit;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    min-width: 2.75rem;
    min-height: 2.75rem;
    padding-inline: 0.5rem;
    border: 1px solid transparent;
    border-radius: 999px;
    background: none;
    color: var(--dwc-text-soft, GrayText);
    cursor: pointer;
  }

  [data-dwc='theme-toggle'][data-theme-state='dark'],
  [data-dwc='theme-toggle'][data-theme-state='light'] {
    color: var(--dwc-text, CanvasText);
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
  }

  [data-dwc='theme-toggle-icon'] {
    display: inline-flex;
    line-height: 0;
  }

  [data-dwc='theme-toggle-label'] {
    font-size: var(--text-fluid-sm, 0.9rem);
  }

  /* Groupe thème + langue (+ densités) : une seule étiquette accessible. */
  [data-dwc='chrome-prefs'] {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.25rem;
  }

  /* ── SegmentedControl ─────────────────────────────────────────────────── *
   * Onglets « pilule » : le conteneur porte le fond en retrait, le segment
   * actif est marqué par `aria-selected` — l'état accessible EST l'état
   * visuel, il n'y a pas de classe à synchroniser.
   * ────────────────────────────────────────────────────────────────────── */

  [data-dwc='segmented'] {
    display: inline-flex;
    gap: 0.25rem;
    padding: 0.25rem;
    border: 1px solid
      var(--dwc-border, color-mix(in srgb, CanvasText 18%, Canvas));
    border-radius: 999px;
    background: var(--dwc-surface-2, color-mix(in srgb, CanvasText 4%, Canvas));
  }

  [data-dwc='segmented'][data-full-width] {
    display: flex;
    width: 100%;
  }

  [data-dwc='segmented'][data-full-width] > [data-dwc='segmented-tab'] {
    flex: 1;
  }

  [data-dwc='segmented-tab'] {
    font: inherit;
    min-height: 2.25rem;
    padding-inline: 0.9rem;
    border: none;
    border-radius: 999px;
    background: none;
    color: var(--dwc-text-soft, GrayText);
    font-size: var(--text-fluid-sm, 0.9rem);
    cursor: pointer;
  }

  [data-dwc='segmented'][data-size='sm'] > [data-dwc='segmented-tab'] {
    min-height: 1.9rem;
    padding-inline: 0.7rem;
  }

  [data-dwc='segmented-tab'][aria-selected='true'] {
    background: var(--dwc-surface, Canvas);
    color: var(--dwc-text, CanvasText);
    box-shadow: 0 1px 2px color-mix(in srgb, CanvasText 20%, transparent);
  }

  /* ── ConnectionBanner ─────────────────────────────────────────────────── *
   * Ton « avertissement », pas « erreur » : être hors ligne est un état,
   * pas une faute. Le composant ne se rend qu'après son délai anti-
   * clignotement — aucune animation à neutraliser ici.
   * ────────────────────────────────────────────────────────────────────── */

  [data-dwc='connection-banner'] {
    /*
     * `flex` centré plutôt que `block` + `text-align`. Le `label` accepte un
     * nœud React, et la copie dont ce composant est promu (mister-qowa) y
     * passait une icône SUIVIE d'un texte : en `block`, l'icône se posait sur
     * la ligne de base au lieu d'être centrée avec le texte, et l'app devait
     * remettre un `flex items-center justify-center gap-2` par-dessus. Pour un
     * enfant textuel unique — le cas de toutes les autres apps — le rendu est
     * identique : un seul enfant en ligne, centré.
     */
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    border: 1px solid var(--dwc-warning, light-dark(#b45309, #fbbf24));
    border-radius: calc(var(--dwc-radius, 0.75rem) * 0.8);
    background: color-mix(
      in srgb,
      var(--dwc-warning, light-dark(#b45309, #fbbf24)) 14%,
      var(--dwc-surface, Canvas)
    );
    color: var(--dwc-text, CanvasText);
    font-size: var(--text-fluid-sm, 0.9rem);
    text-align: center;
  }

  /* ── Animations ────────────────────────────────────────────────────────── */

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

  @keyframes dwc-pulse {
    50% {
      opacity: 0.45;
    }
  }

  /* DEUX CHOIX À NE PAS « CORRIGER ».
   *
   * 1. Pas d'image-clé `to`. L'état final est celui de l'élément lui-même,
   *    quel qu'il soit : l'app peut décaler ou incliner un panneau sans que
   *    l'animation ne réécrive sa position d'arrivée.
   *
   * 2. `animation-fill-mode: backwards`, jamais `both` ni `forwards`.
   *    C'est la seule chose qui compte pour l'imbrication, et elle ne se
   *    devine pas : `forwards` RETIENT la valeur d'arrivée après la fin, et
   *    une transformation retenue reste une transformation — même réduite à
   *    l'identité. Un `transform` autre que `none` fait de l'élément le bloc
   *    conteneur de ses descendants `position: fixed` et lui ouvre un
   *    contexte d'empilement. Mesuré : `both` laisse
   *    `matrix(1, 0, 0, 1, 0, 0)` sur le panneau, `backwards` laisse `none`.
   *
   *    Ce que ça cassait : `ConfirmDialog` est `position: fixed; inset: 0;
   *    z-index: 60`, et on l'ouvre le plus souvent DEPUIS une feuille, donc
   *    à l'intérieur de `sheet-panel`. Avec `both`, son voile se repliait sur
   *    les dimensions de la feuille au lieu de couvrir l'écran, et son
   *    `z-index` restait prisonnier du panneau : la boîte de confirmation
   *    s'affichait en transparence, par-dessus le formulaire. Idem pour
   *    `toast-viewport`, `fixed` lui aussi.
   *
   *    `backwards` ne coûte rien à l'entrée : sans `to`, l'état d'arrivée de
   *    l'animation est DÉJÀ l'état naturel de l'élément — il n'y a donc rien
   *    à retenir, et rien ne saute à la fin. Les deux choix se tiennent.
   *
   * Une app qui réécrit l'entrée en CSS non « layered » écrase cette règle et
   * rouvre la brèche. Le garde-fou de `test/components-css.test.mjs` ne voit
   * que ce fichier ; côté app, c'est à relire à la main. */
  @keyframes dwc-rise {
    from {
      transform: translateY(1rem);
      opacity: 0;
    }
  }

  /* Le mouvement est un confort, jamais une information : on le retire quand
     l'utilisateur l'a demandé, sans rien casser d'autre.
     LA LISTE DOIT SUIVRE `dwc-rise`. Elle ne couvrait que `sheet-panel`, alors
     que `confirm-panel` et `toast` portent la MÊME entrée : une alerte et un
     message surgissaient donc toujours, réglage ignoré. Relevé par la
     migration de mister-puzzle (#14), qui a dû reposer la règle chez elle.
     Depuis, `test/components-css.test.mjs` exige que chaque règle animée y
     figure — le bandeau de mise à jour y est entré avec son entrée. */
  @media (prefers-reduced-motion: reduce) {
    [data-dwc='button-spinner'],
    [data-dwc='bottom-nav-icon'],
    [data-dwc='skeleton'],
    [data-dwc='sheet-panel'],
    [data-dwc='confirm-panel'],
    [data-dwc='toast'],
    [data-dwc='update-banner'],
    [data-dwc='offline-ready'],
    [data-dwc='consent-banner'] {
      animation-duration: 0.001ms;
      animation-iteration-count: 1;
    }

    /* La carte d'application ne s'anime pas, elle TRANSITIONNE : son
       relèvement au survol échappe donc à la règle ci-dessus, qui n'éteint
       que les `animation`. */
    [data-dwc='family-app'] {
      transition-duration: 0.001ms;
    }
  }

  /* ── Contraste forcé ───────────────────────────────────────────────────── *
   * En contraste forcé (« contraste élevé » de Windows, thèmes à contraste
   * accru), le navigateur REMPLACE d'autorité `color`, `background-color`,
   * `border-color`, et SUPPRIME `box-shadow`. Ce fichier repose entièrement sur
   * des variables et des `color-mix()` : tout cela est donc écrasé.
   *
   * L'essentiel survit — c'est le bénéfice d'avoir bâti sur les couleurs
   * système. Trois régressions subsistent pourtant, et elles ne se devinent
   * pas :
   *
   *   1. `transparent` n'est PAS forcé. Un bouton primaire perd son aplat,
   *      garde sa bordure transparente, et devient un simple texte flottant.
   *      Idem pour la pastille `soft`, qui perd sa forme.
   *   2. `box-shadow` disparaît. Le panneau modal n'a aucune bordure : il se
   *      confond avec son propre voile, lui aussi ramené à `Canvas`.
   *   3. Ce qui n'existait QUE par sa couleur de fond n'existe plus : le
   *      squelette de chargement et la pastille de synchro.
   *
   * La correction consiste à NOMMER les couleurs système — elles, conservées
   * telles quelles — plutôt qu'à figer nos teintes avec `forced-color-adjust:
   * none`, qui reviendrait à passer outre le réglage de l'utilisateur.
   * ────────────────────────────────────────────────────────────────────── */

  @media (forced-colors: active) {
    /* 1 — rendre le contour visible là où il était transparent. Une seule
       règle suffit : les variantes qui posent déjà `border-color` sont plus
       spécifiques, et leur couleur est de toute façon forcée. */
    [data-dwc='button'],
    [data-dwc='badge'],
    [data-dwc='theme-toggle'] {
      border-color: currentColor;
    }

    /* Le halo du survol disparaît ici avec tous les `box-shadow` : la paire
       système prévue pour la mise en évidence le remplace, en respectant le
       réglage de l'utilisateur. Sous `(hover: hover)`, comme le survol
       ordinaire. */
    @media (hover: hover) {
      [data-dwc='button']:hover:not(:disabled):not([aria-disabled='true']),
      [data-dwc='update-banner-update']:hover:not([aria-busy='true']) {
        background: Highlight;
        color: HighlightText;
      }
    }

    /* `opacity` n'est pas forcée : le bouton désactivé resterait à
       pleine couleur système sous 45 % d'opacité, donc lisible et trompeur.
       `GrayText` est le signal que les technologies d'assistance attendent. */
    [data-dwc='button']:disabled,
    [data-dwc='button'][aria-disabled='true'] {
      opacity: 1;
      color: GrayText;
      border-color: GrayText;
    }

    /* Le bouton de mise à jour, pour la même raison que `Button` : `opacity`
       n'est pas forcée. */
    [data-dwc='update-banner-update'][aria-busy='true'] {
      opacity: 1;
      color: GrayText;
      border-color: GrayText;
    }

    /* 2 — séparer le panneau de son voile. `outline` avec un décalage négatif
       plutôt qu'une bordure : dessiné à l'intérieur, il ne décale rien et ne
       peut pas être rogné par le bord de l'écran. */
    [data-dwc='sheet-panel'] {
      outline: 1px solid CanvasText;
      outline-offset: -1px;
    }

    /* 3 — redonner un corps à ce qui n'était qu'un aplat. La pastille de
       synchro perd en revanche sa distinction par ton : c'est sans perte, le
       composant écrit toujours l'état en toutes lettres à côté. */
    [data-dwc='skeleton'] {
      outline: 1px solid GrayText;
      outline-offset: -1px;
    }

    [data-dwc='sync-status']::before {
      background: CanvasText;
    }

    /* 4 — les surfaces ajoutées ensuite, même raison qu'au point 2 : la boîte
       de confirmation, la notification, le tiroir de navigation, et le bandeau
       de mise à jour depuis qu'il flotte au-dessus de la barre collée. */
    [data-dwc='confirm-panel'],
    [data-dwc='toast'],
    [data-dwc='bottom-nav-drawer'],
    [data-dwc='update-banner'],
    [data-dwc='offline-ready'] {
      outline: 1px solid CanvasText;
      outline-offset: -1px;
    }

    /* 5 — l'onglet courant. En contraste forcé, `--dwc-primary` et
       `--dwc-text-soft` sont remplacés par la MÊME encre système : la
       distinction par la couleur disparaît entièrement. Le trait, lui, reste —
       et c'est précisément pourquoi le composant le double d'un texte lu. */
    [data-dwc='bottom-nav-item'][data-current],
    [data-dwc='bottom-nav-more'][data-current] {
      border-block-start-color: Highlight;
    }

    /* 6 — barres et jauge n'existaient que par leurs aplats, comme le
       squelette : encre système sur le remplissage, contour sur le rail. La
       courbe, elle, est un trait `currentColor` : déjà forcée correctement. */
    [data-dwc='bars-bar'],
    [data-dwc='gauge-fill'] {
      background: CanvasText;
    }

    [data-dwc='gauge'] {
      outline: 1px solid GrayText;
      outline-offset: -1px;
    }
  }

  /* ── Impression ────────────────────────────────────────────────────────── *
   * Les navigateurs SUPPRIMENT les fonds à l'impression, mais gardent la
   * couleur du texte. Un libellé blanc posé sur la primaire devient donc blanc
   * sur blanc : le bouton s'imprime vide. C'est le seul défaut réellement
   * invisible à l'écran de toute cette feuille.
   * ────────────────────────────────────────────────────────────────────── */

  @media print {
    /* Ce qui n'a de sens que devant un écran. Une invitation à installer
       l'application n'a rien à faire sur un bilan imprimé. */
    [data-dwc='sheet'],
    [data-dwc='confirm'],
    [data-dwc='toast-viewport'],
    [data-dwc='bottom-nav'],
    [data-dwc='pwa-install-prompt'],
    [data-dwc='update-banner'],
    [data-dwc='offline-ready'],
    [data-dwc='update-button-group'] {
      display: none;
    }

    /* Texte sur aplat → encre système, et contour pour compenser le fond
       disparu. Sélecteurs alignés sur ceux des variantes, sinon ces dernières
       l'emporteraient. */
    [data-dwc='button'][data-variant='primary'],
    [data-dwc='button'][data-variant='danger'],
    [data-dwc='confirm-confirm'],
    [data-dwc='update-banner-update'],
    [data-dwc='bottom-nav-badge'],
    span[data-dwc='family-app-icon'] {
      color: CanvasText;
      border-color: CanvasText;
    }

    /* L'animation se fige à l'opacité qu'elle avait au moment du rendu : le
       squelette s'imprimerait plus ou moins pâle, au hasard. */
    [data-dwc='skeleton'] {
      animation: none;
      opacity: 1;
    }

    [data-dwc='stat'],
    [data-dwc='sheet-panel'],
    [data-dwc='pwa-install-prompt'] {
      box-shadow: none;
    }
  }

  /*
   * DERNIÈRE RÈGLE DU FICHIER, et c'est délibéré.
   *
   * L'attribut `hidden` ne masque que via la feuille de style du navigateur :
   * la moindre règle d'auteur posant un `display` le neutralise, et l'élément
   * réapparaît. Presque tous les composants ci-dessus déclarent un `display` —
   * la feuille modale du showroom s'affichait ainsi par-dessus la page dès le
   * chargement, alors qu'elle portait bien `hidden`.
   *
   * Une seule règle couvre tout le jeu. Placée en dernier, elle l'emporte aussi
   * sur les sélecteurs de même spécificité (`[data-dwc='x'][data-y]`).
   */
  [data-dwc][hidden] {
    display: none;
  }
}
