/*
 * Le catalogue : ce qui se répète d'un écran à l'autre.
 *
 * La grammaire tient en trois axes, et un composant ne connaît rien d'autre :
 *
 *   `data-tone`     le sens — neutral, accent, success, warning, danger, info
 *   `data-variant`  la forme — plein par défaut, `subtle` bordé, `ghost` sans fond
 *   `data-size`     la taille
 *
 * L'état, lui, ne s'invente pas : il passe par les attributs que le navigateur et les lecteurs
 * d'écran connaissent déjà — `disabled`, `aria-busy`, `aria-current`, `aria-sort`,
 * `aria-selected`, `aria-disabled`.
 *
 * Un composant coloré n'a qu'une règle pour les six tons : `tokens.css` traduit `data-tone` en
 * variables locales — `--app-tone`, `-hover`, `-soft`, `-ink`, `-on` — et c'est tout ce que la
 * règle cite. Un ton de plus se déclarerait là-bas, sans reprendre une seule ligne d'ici.
 *
 * Aucune règle ne cite une couleur en dur, ni un jeton de l'étage 1, ni une taille ou un
 * espacement absolus.
 */

/* ═══ Boutons ══════════════════════════════════════════════════════════════════════════ */

.app-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--app-space-sm);
    padding: var(--app-space-md) var(--app-space-lg);
    border: 1px solid transparent;
    border-radius: var(--app-radius-ui);
    font: inherit;
    font-size: var(--app-text);
    font-weight: var(--app-weight-medium);
    line-height: var(--app-leading-none);
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    background: var(--app-accent);
    color: var(--app-on-accent);
    transition: background var(--app-speed) var(--app-ease),
                border-color var(--app-speed) var(--app-ease),
                color var(--app-speed) var(--app-ease);
}

.app-btn:hover {
    background: var(--app-accent-hover);
    text-decoration: none;
}

/* Le ton prend la place de l'accent. Deux règles, et les six tons existent. */
.app-btn[data-tone] {
    background: var(--app-tone);
    color: var(--app-tone-on);
}

.app-btn[data-tone]:hover {
    background: var(--app-tone-hover);
}

/* Bordé sur fond de surface : l'action secondaire d'un écran. */
.app-btn[data-variant="subtle"] {
    background: var(--app-surface);
    border-color: var(--app-line-control);
    color: var(--app-ink);
}

.app-btn[data-variant="subtle"]:hover {
    background: var(--app-hover);
}

.app-btn[data-variant="subtle"][data-tone] {
    background: var(--app-surface);
    border-color: var(--app-tone);
    color: var(--app-tone);
}

.app-btn[data-variant="subtle"][data-tone]:hover {
    background: var(--app-tone-soft);
}

/* Sans fond ni filet : annuler, fermer, revenir. */
.app-btn[data-variant="ghost"] {
    background: transparent;
    color: var(--app-ink-soft);
}

.app-btn[data-variant="ghost"]:hover {
    background: var(--app-hover);
    color: var(--app-ink);
}

.app-btn[data-variant="ghost"][data-tone] {
    background: transparent;
    color: var(--app-tone);
}

.app-btn[data-variant="ghost"][data-tone]:hover {
    background: var(--app-tone-soft);
}

.app-btn[data-size="sm"] {
    padding: var(--app-space-xs) var(--app-space-md);
    font-size: var(--app-text-sm);
}

.app-btn[data-size="lg"] {
    padding: var(--app-space-lg) var(--app-space-xl);
    font-size: var(--app-text-lg);
}

/* Carré, sans texte : l'étiquette accessible passe alors par `aria-label`. */
.app-btn[data-icon] {
    padding: var(--app-space-md);
    aspect-ratio: 1;
}

.app-btn[data-block] {
    display: flex;
    width: 100%;
}

.app-btn:disabled,
.app-btn[aria-disabled="true"] {
    opacity: .5;
    cursor: not-allowed;
    pointer-events: none;
}

/* Action en cours : le bouton reste lisible mais n'accepte plus de second clic. */
.app-btn[aria-busy="true"] {
    cursor: progress;
    pointer-events: none;
}

.app-btn[aria-busy="true"]::before {
    content: "";
    width: 1em;
    height: 1em;
    border: 2px solid currentcolor;
    border-top-color: transparent;
    border-radius: var(--app-radius-pill);
    animation: app-spin .7s linear infinite;
}

@keyframes app-spin {
    to {
        transform: rotate(1turn);
    }
}

/* Un mouvement perpétuel s'arrête quand le système demande moins d'animation. L'anneau reste
   affiché : c'est lui qui dit que l'action est en cours, pas sa rotation. */
@media (prefers-reduced-motion: reduce) {
    .app-btn[aria-busy="true"]::before {
        animation: none;
    }
}

/* ═══ Cartes ═══════════════════════════════════════════════════════════════════════════ */

.app-card {
    display: flex;
    flex-direction: column;
    gap: var(--app-space-md);
    padding: var(--app-space-xl);
    background: var(--app-surface);
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius);
    box-shadow: var(--app-shadow-sm);
}

.app-card[data-tone] {
    border-color: var(--app-tone);
}

.app-card-title {
    font-size: var(--app-text-lg);
    font-weight: var(--app-weight-bold);
}

.app-card-meta {
    font-size: var(--app-text-sm);
    color: var(--app-ink-muted);
}

.app-card-footer {
    display: flex;
    gap: var(--app-space-sm);
    padding-top: var(--app-space-md);
    border-top: 1px solid var(--app-line);
}

/* Carte cliquable : c'est la carte entière qui est le lien. */
.app-card[data-interactive] {
    cursor: pointer;
    transition: box-shadow var(--app-speed) var(--app-ease),
                border-color var(--app-speed) var(--app-ease);
}

.app-card[data-interactive]:hover {
    border-color: var(--app-line-strong);
    box-shadow: var(--app-shadow);
}

/* ═══ Étiquettes ═══════════════════════════════════════════════════════════════════════ */

.app-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--app-space-xs);
    padding: var(--app-space-2xs) var(--app-space-md);
    /* Le filet est discret tant que le thème le veut discret ; sur une palette à contraste
       renforcé, où tous les fonds sont noirs, c'est lui qui redonne sa forme à l'étiquette. */
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius-pill);
    font-size: var(--app-text-xs);
    font-weight: var(--app-weight-semibold);
    line-height: var(--app-leading);
    white-space: nowrap;
    background: var(--app-raised);
    color: var(--app-ink-soft);
}

.app-badge[data-tone] {
    background: var(--app-tone-soft);
    color: var(--app-tone-ink);
}

/* Pleine : ce qui doit s'attraper à travers un tableau entier. */
.app-badge[data-variant="solid"] {
    border-color: transparent;
    background: var(--app-neutral);
    color: var(--app-on-neutral);
}

.app-badge[data-variant="solid"][data-tone] {
    background: var(--app-tone);
    color: var(--app-tone-on);
}

/* ═══ Formulaires ══════════════════════════════════════════════════════════════════════ */

.app-field {
    display: flex;
    flex-direction: column;
    gap: var(--app-space-xs);
}

.app-field-label {
    font-size: var(--app-text-sm);
    font-weight: var(--app-weight-semibold);
    color: var(--app-ink-soft);
}

/* Un groupe de commandes est une question posée : `<fieldset>` la délimite et `<legend>` l'énonce,
   ce qu'un lecteur d'écran rappelle alors à chaque réponse. Les deux arrivent avec une mise en
   forme du navigateur, qu'il faut défaire. La légende reste hors de la disposition en colonne,
   d'où son écart posé à la main. */
fieldset.app-field {
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
}

.app-field legend {
    padding: 0;
    margin-bottom: var(--app-space-xs);
}

/* Les cases à cocher et les boutons radio sont écartés : ce sont des carrés et des ronds, et
   la mise en forme d'une ligne de saisie les déformerait. Ils ont leur composant plus bas. */
.app-field :is(select, textarea, input:not([type="checkbox"], [type="radio"])) {
    width: 100%;
    padding: var(--app-space-md);
    border: 1px solid var(--app-line-control);
    border-radius: var(--app-radius-ui);
    font: inherit;
    color: var(--app-ink);
    background: var(--app-surface);
    transition: border-color var(--app-speed) var(--app-ease),
                box-shadow var(--app-speed) var(--app-ease);
}

/*
 * ── Un champ ne descend jamais sous seize pixels ──────────────────────────────────────────────
 *
 * Ce n'est pas un choix de lisibilité, c'est une contrainte du navigateur : Safari sur iOS
 * agrandit la page dès qu'on touche un champ écrit plus petit, et n'en revient pas tout seul. La
 * personne se retrouve à faire défiler de côté un formulaire qu'elle voyait entier une seconde
 * plus tôt.
 *
 * La règle vaut à toutes les tailles de fenêtre, et ne souffre pas d'exception : un champ dense
 * sur un tableau de bord de bureau devient le même piège dès qu'on ouvre la page sur un
 * téléphone.
 */
.app-field :is(select, textarea, input:not([type="checkbox"], [type="radio"])) {
    font-size: max(1rem, var(--app-text));
    min-height: var(--app-touch);
}

/* Un champ qui annonce combien de caractères il porte n'a pas à prendre toute la ligne : un code
   à six chiffres dans un champ large se lit comme s'il en attendait trente. C'est `size` qui le
   dit, l'attribut que le navigateur emploie déjà pour cela, et lui qui fixe alors la largeur. */
.app-field input[size] {
    width: auto;
    align-self: start;
}

.app-field :is(select, textarea, input:not([type="checkbox"], [type="radio"])):focus {
    outline: none;
    border-color: var(--app-accent);
    box-shadow: var(--app-shadow-focus);
}

.app-field textarea {
    min-height: 6rem;
    resize: vertical;
}

.app-field :is(select, textarea, input:not([type="checkbox"], [type="radio"])):disabled {
    background: var(--app-raised);
    color: var(--app-ink-muted);
    cursor: not-allowed;
}

/* Un champ compact : ce qui se pose dans une barre, où la hauteur est comptée. Le plancher de
   seize pixels vaut ici comme ailleurs — un champ resserré reste un champ, et le navigateur ne
   fait pas la différence avant d'agrandir la page. */
.app-field[data-size="sm"] :is(select, textarea, input:not([type="checkbox"], [type="radio"])) {
    padding: var(--app-space-sm) var(--app-space-md);
    font-size: max(1rem, var(--app-text-sm));
}

.app-field-hint {
    font-size: var(--app-text-sm);
    color: var(--app-ink-muted);
}

.app-field-error {
    font-size: var(--app-text-sm);
    font-weight: var(--app-weight-medium);
    color: var(--app-danger);
}

/* Champ refusé : le filet rouge double le message, pour qui ne distingue pas la couleur. */
.app-field[data-invalid] :is(select, textarea, input:not([type="checkbox"], [type="radio"])) {
    border-color: var(--app-danger);
}

.app-field[data-invalid] :is(select, textarea, input:not([type="checkbox"], [type="radio"])):focus {
    box-shadow: 0 0 0 3px var(--app-danger-soft);
}

/* ═══ Champ accolé ═════════════════════════════════════════════════════════════════════ */

/* Un champ et son bouton n'en forment qu'un : une recherche, une unité, un choix suivi d'une
   action. Les angles qui se touchent s'effacent, et le filet qui se dédouble se recouvre. */
.app-input-group {
    display: flex;
    align-items: stretch;
}

.app-input-group > :is(input, select) {
    flex: 1;
    min-width: 0;
}

.app-input-group > :is(input, select, .app-btn):not(:last-child) {
    border-top-right-radius: 0;
    border-bottom-right-radius: 0;
}

.app-input-group > :is(input, select, .app-btn):not(:first-child) {
    margin-left: -1px;
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
}

/* Le champ passe devant quand il prend le focus : son filet ne doit pas passer sous le bouton. */
.app-input-group > :is(input, select):focus {
    position: relative;
}

/* ═══ Cases à cocher, boutons radio, interrupteur ══════════════════════════════════════ */

/* Un `<label>` qui contient sa commande : le texte devient une zone de clic, et l'étiquette
   est rattachée sans qu'un identifiant soit à inventer. */
.app-check {
    display: flex;
    align-items: flex-start;
    gap: var(--app-space-md);
    cursor: pointer;
}

.app-check input {
    flex: none;
    /* Le calcul recentre la commande sur la première ligne du texte : posée sur la ligne de
       base, elle flotterait au-dessus dès que l'étiquette passe sur deux lignes. */
    margin: calc((var(--app-leading) * 1em - 1.15em) / 2) 0 0;
    width: 1.15em;
    height: 1.15em;
    font: inherit;
    /* La case, sa coche et son bord sont peints par le navigateur, qui les accorde au
       `color-scheme` en vigueur : l'accent lui suffit pour choisir une encre qui y tranche, et
       le filet qu'il dessine tient les 3:1 d'une commande jusque sur un fond noir. Un filet
       ajouté ici resterait carré autour d'un bouton radio, la forme d'un contrôle natif
       n'obéissant pas à `border-radius`. */
    accent-color: var(--app-accent);
    cursor: inherit;
}

.app-check[data-tone] input {
    accent-color: var(--app-tone);
}

.app-check:has(input:disabled) {
    color: var(--app-ink-muted);
    cursor: not-allowed;
}

.app-check input:disabled {
    opacity: .5;
}

/* Le même contrôle, dessiné en interrupteur : deux états qui s'appliquent sur-le-champ, là où
   une case à cocher attend l'envoi du formulaire.

   Le curseur est peint dans le fond du contrôle, et non dans un pseudo-élément : les champs de
   formulaire sont des éléments remplacés, dont le contenu généré ne s'affiche pas partout. */
.app-check[data-variant="switch"] input {
    margin-top: calc((var(--app-leading) * 1em - 1.35em) / 2);
    width: 2.4em;
    height: 1.35em;
    appearance: none;
    border: 1px solid var(--app-line-control);
    border-radius: var(--app-radius-pill);
    background-color: var(--app-raised);
    background-image: radial-gradient(circle .45em at center, var(--app-line-control) 96%, transparent 100%);
    background-repeat: no-repeat;
    background-size: 1.35em 1.35em;
    background-position: left center;
    transition: background-color var(--app-speed) var(--app-ease),
                background-position var(--app-speed) var(--app-ease),
                border-color var(--app-speed) var(--app-ease);
}

.app-check[data-variant="switch"] input:checked {
    border-color: var(--app-accent);
    background-color: var(--app-accent);
    background-image: radial-gradient(circle .45em at center, var(--app-on-accent) 96%, transparent 100%);
    background-position: right center;
}

.app-check[data-variant="switch"][data-tone] input:checked {
    border-color: var(--app-tone);
    background-color: var(--app-tone);
    background-image: radial-gradient(circle .45em at center, var(--app-tone-on) 96%, transparent 100%);
}

/* ═══ Contrôle segmenté ════════════════════════════════════════════════════════════════ */

/* Quelques réponses exclusives, toutes visibles à la fois : un filtre, une vue, une période.
   Ce sont des boutons radio — la commande reste celle que le navigateur et le clavier
   connaissent, seule sa peinture change. */
.app-segmented {
    display: inline-flex;
    /* Une largeur définie, donc jamais étirée par la colonne qui l'accueille : le contrôle
       tient à ses réponses, et non à la place disponible. */
    width: fit-content;
    gap: var(--app-space-2xs);
    padding: var(--app-space-2xs);
    background: var(--app-raised);
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius-ui);
}

.app-segmented label {
    position: relative;
    display: flex;
}

/* Transparent et non masqué : un contrôle sur lequel le focus peut encore se poser. */
.app-segmented input {
    position: absolute;
    inset: 0;
    margin: 0;
    opacity: 0;
    cursor: pointer;
}

.app-segmented span {
    padding: var(--app-space-xs) var(--app-space-md);
    border-radius: calc(var(--app-radius-ui) * .7);
    font-size: var(--app-text-sm);
    font-weight: var(--app-weight-medium);
    white-space: nowrap;
    color: var(--app-ink-soft);
}

.app-segmented input:checked + span {
    background: var(--app-surface);
    color: var(--app-ink);
    box-shadow: var(--app-shadow-sm);
}

.app-segmented input:focus-visible + span {
    outline: var(--app-focus-width) solid var(--app-accent);
    outline-offset: var(--app-focus-offset);
}

.app-segmented input:disabled + span {
    opacity: .5;
    cursor: not-allowed;
}

/* ═══ Tables ═══════════════════════════════════════════════════════════════════════════ */

/* La table déborde en défilant dans son propre cadre : la page, elle, ne défile jamais
   horizontalement.

   Le cadre est positionné pour que ce soit vrai jusqu'au bout. Un élément en position absolue
   — une mention réservée aux lecteurs d'écran dans une cellule, par exemple — se place sinon
   par rapport à la page, échappe au découpage du cadre, et élargit la page de toute la largeur
   dont la table dépasse. */
.app-table-wrap {
    position: relative;
    min-width: 0;
    overflow-x: auto;
    background: var(--app-surface);
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius);
}

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

.app-table :is(th, td) {
    padding: var(--app-space-md) var(--app-space-lg);
    text-align: left;
    border-bottom: 1px solid var(--app-line);
}

/* L'entête porte du contenu, pas une mention accessoire : elle prend l'encre atténuée, et non
   l'estompée, qui ne tiendrait pas 4,5:1 sur un fond relevé. */
.app-table th {
    font-size: var(--app-text-xs);
    font-weight: var(--app-weight-bold);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--app-ink-soft);
    background: var(--app-raised);
}

.app-table tbody tr:last-child :is(th, td) {
    border-bottom: 0;
}

.app-table tbody tr:hover {
    background: var(--app-hover);
}

/* La ligne retenue se marque par `aria-selected`, que le lecteur d'écran annonce déjà. La
   règle suit celle du survol : une ligne retenue le reste sous le pointeur. */
.app-table tbody tr[aria-selected="true"] {
    background: var(--app-accent-soft);
}

/* La colonne des actions se serre contre le bord et ne se coupe jamais en deux lignes. */
.app-table :is(th, td)[data-actions] {
    width: 1%;
    text-align: right;
    white-space: nowrap;
}

/* La colonne de tête — une icône, un repère — se serre contre le bord et ne se coupe jamais. */
.app-table :is(th, td)[data-slot="lead"] {
    width: 1%;
    white-space: nowrap;
}

/*
 * Un tableau qui se range en lignes sur un écran étroit.
 *
 * Faire glisser un tableau de côté, au doigt, pour lire une ligne n'est pas tenable : sous 40 em,
 * un tableau qui porte `data-stack` pose chaque ligne en deux lignes de texte, comme un journal
 * d'appels. Trois emplacements, que la cellule choisit par `data-slot` :
 *
 *   - `lead` : en tête de la première ligne, avant le titre — une icône, un repère ;
 *   - `title` : la première ligne, celle qu'on lit d'abord ;
 *   - `aside` : à droite de la première ligne, pour l'action ;
 *   - sans emplacement : la seconde ligne, atténuée, où les cellules se suivent.
 *
 * Une longue liste reste ainsi lisible : une ligne en prend deux, et non une par colonne. Pour
 * garder l'étiquette de chaque colonne, `data-stack="labels"` pose une fiche à la place, où chaque
 * valeur reprend à sa gauche le `data-label` de sa cellule.
 *
 * L'entête reste dans la page, hors de vue : ses étiquettes ont changé de place, pas de lecteur.
 */
@media (max-width: 39.99em) {
    .app-table[data-stack] thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .app-table[data-stack],
    .app-table[data-stack] tbody {
        display: block;
    }

    .app-table[data-stack] tbody tr {
        border-bottom: 1px solid var(--app-line);
    }

    .app-table[data-stack] tbody tr:last-child {
        border-bottom: 0;
    }

    /* Les lignes : le titre et l'action sur la première, le reste sur la seconde. Le filet de
       `::before` ne sert qu'à renvoyer la seconde ligne sous la première. */
    .app-table[data-stack=""] tbody tr {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: var(--app-space-2xs) var(--app-space-md);
        padding: var(--app-space-md) var(--app-space-lg);
    }

    .app-table[data-stack=""] tbody tr::before {
        content: "";
        order: 2;
        flex-basis: 100%;
        height: 0;
    }

    .app-table[data-stack=""] tbody :is(th, td) {
        order: 3;
        width: auto;
        padding: 0;
        border-bottom: 0;
        text-align: left;
        white-space: normal;
        font-size: var(--app-text-sm);
        color: var(--app-ink-soft);
    }

    .app-table[data-stack=""] tbody :is(th, td)[data-slot="lead"] {
        display: flex;
        align-items: center;
        order: -1;
        flex: none;
    }

    .app-table[data-stack=""] tbody :is(th, td)[data-slot="title"] {
        order: 0;
        flex: 1 1 0;
        min-width: 0;
        font-size: var(--app-text);
        font-weight: var(--app-weight-semibold);
        color: var(--app-ink);
    }

    .app-table[data-stack=""] tbody :is(th, td)[data-slot="aside"] {
        order: 1;
        flex: none;
    }

    /* Les fiches : chaque valeur reprend à sa gauche l'étiquette de sa colonne. */
    .app-table[data-stack="labels"] tbody tr {
        display: block;
        padding: var(--app-space-sm) 0;
    }

    .app-table[data-stack="labels"] tbody :is(th, td) {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: var(--app-space-lg);
        width: auto;
        padding: var(--app-space-sm) var(--app-space-lg);
        border-bottom: 0;
        text-align: right;
        white-space: normal;
    }

    .app-table[data-stack="labels"] tbody :is(th, td)::before {
        content: attr(data-label);
        flex: none;
        font-size: var(--app-text-xs);
        font-weight: var(--app-weight-bold);
        text-align: left;
        text-transform: uppercase;
        letter-spacing: .04em;
        color: var(--app-ink-soft);
    }

    .app-table[data-stack="labels"] tbody :is(th, td):not([data-label]) {
        justify-content: flex-start;
    }

    .app-table[data-stack="labels"] tbody :is(th, td):not([data-label])::before {
        content: none;
    }

    /* Une cellule vide ne prend pas de place : son étiquette n'aurait rien en face. */
    .app-table[data-stack] tbody :is(th, td):empty {
        display: none;
    }
}

/* Le tri se porte sur l'entête : `aria-sort` dit dans quel sens la colonne est rangée, et la
   flèche ne fait que le montrer. Elle reste muette pour un lecteur d'écran, qui lit déjà
   l'attribut.

   Le lien prend toute la cellule, dont il reçoit la marge intérieure : c'est l'entête entière
   qui devient la cible, et non la seule hauteur d'une ligne de texte — trop mince pour un
   doigt. */
.app-table th:has(.app-table-sort) {
    padding: 0;
}

.app-table-sort {
    display: flex;
    align-items: center;
    gap: var(--app-space-xs);
    padding: var(--app-space-md) var(--app-space-lg);
    color: inherit;
    text-decoration: none;
}

.app-table-sort:hover {
    color: var(--app-ink);
    text-decoration: none;
}

.app-table-sort::after {
    content: "↕" / "";
    opacity: .45;
}

.app-table th[aria-sort="ascending"] .app-table-sort::after {
    content: "↑" / "";
    opacity: 1;
}

.app-table th[aria-sort="descending"] .app-table-sort::after {
    content: "↓" / "";
    opacity: 1;
}

/* ═══ Listes ═══════════════════════════════════════════════════════════════════════════ */

/*
 * Une liste d'objets : chacun sur sa ligne, et au bout ce qu'il y a à en faire. C'est ce qu'une
 * table devient quand elle n'a qu'une colonne à montrer — un en-tête à répéter n'y apprendrait
 * rien, et sur un écran étroit l'action passe sous ce qu'elle vise au lieu de le comprimer.
 *
 * `list-style` est retiré : la puce dit « énumération » une seconde fois, après le filet qui
 * sépare déjà les lignes. Le `<ul>`, lui, reste — c'est lui qui annonce le nombre d'objets à un
 * lecteur d'écran.
 */
.app-list {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
}

.app-list-item {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--app-space-md);
    padding: var(--app-space-md) 0;
    border-bottom: 1px solid var(--app-line);
}

/* Le premier et le dernier ne creusent pas l'écart que la carte a déjà posé. */
.app-list-item:first-child {
    padding-top: 0;
}

.app-list-item:last-child {
    padding-bottom: 0;
    border-bottom: 0;
}

.app-list-title {
    font-weight: var(--app-weight-medium);
}

.app-list-meta {
    font-size: var(--app-text-sm);
    color: var(--app-ink-muted);
}

/* ═══ Alertes ══════════════════════════════════════════════════════════════════════════ */

.app-alert {
    display: flex;
    gap: var(--app-space-md);
    padding: var(--app-space-md) var(--app-space-lg);
    border-radius: var(--app-radius-ui);
    border-left: 3px solid var(--app-ink-muted);
    font-size: var(--app-text);
    background: var(--app-raised);
    color: var(--app-ink);
}

.app-alert[data-tone] {
    background: var(--app-tone-soft);
    border-left-color: var(--app-tone);
    color: var(--app-tone-ink);
}

/* ═══ État vide ════════════════════════════════════════════════════════════════════════ */

/* Ce que montre une liste qui n'a rien à montrer : elle dit pourquoi, et donne la porte de
   sortie. Un écran vide sans un mot se lit comme une panne. */
.app-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--app-space-sm);
    padding: var(--app-space-3xl) var(--app-space-xl);
    border: 1px dashed var(--app-line-strong);
    border-radius: var(--app-radius);
    text-align: center;
    color: var(--app-ink-muted);
}

.app-empty-title {
    font-size: var(--app-text-lg);
    font-weight: var(--app-weight-bold);
    color: var(--app-ink);
}

/* ═══ Liste de définitions ═════════════════════════════════════════════════════════════ */

/* Les paires intitulé/valeur d'une fiche. Sur un écran étroit, la valeur passe sous son
   intitulé plutôt que de s'y serrer. */
.app-dl {
    display: grid;
    gap: var(--app-space-md) var(--app-space-xl);
    margin: 0;
}

.app-dl dt {
    font-size: var(--app-text-sm);
    font-weight: var(--app-weight-semibold);
    color: var(--app-ink-soft);
}

.app-dl dd {
    margin: 0;
}

@media (min-width: 40em) {
    .app-dl {
        grid-template-columns: minmax(6rem, 14rem) 1fr;
    }
}

/* ═══ Statistique ══════════════════════════════════════════════════════════════════════ */

/* Un chiffre et ce qu'il compte. Ce qui se pose en tête d'un tableau de bord. */
.app-stat {
    display: flex;
    flex-direction: column;
    gap: var(--app-space-xs);
    padding: var(--app-space-xl);
    background: var(--app-surface);
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius);
    box-shadow: var(--app-shadow-sm);
}

.app-stat-label {
    font-size: var(--app-text-xs);
    font-weight: var(--app-weight-bold);
    text-transform: uppercase;
    letter-spacing: .04em;
    color: var(--app-ink-soft);
}

.app-stat-value {
    font-size: var(--app-text-2xl);
    font-weight: var(--app-weight-heavy);
    line-height: var(--app-leading-none);
}

.app-stat[data-tone] .app-stat-value {
    color: var(--app-tone);
}

.app-stat-hint {
    font-size: var(--app-text-sm);
    color: var(--app-ink-muted);
}

/* ═══ Progression ══════════════════════════════════════════════════════════════════════ */

/* L'avancement se pose sur l'instance : `style="--app-progress: 60%"`. Le rôle et les valeurs
   se déclarent en ARIA sur l'élément, seule chose qu'un lecteur d'écran puisse annoncer. */
.app-progress {
    overflow: hidden;
    height: var(--app-space-md);
    border: 1px solid var(--app-line-control);
    border-radius: var(--app-radius-pill);
    background: var(--app-raised);
}

.app-progress::after {
    content: "";
    display: block;
    height: 100%;
    /* Le repli rend visible l'avancement que l'instance a oublié de poser. */
    width: var(--app-progress, 0%);
    background: var(--app-accent);
    transition: width var(--app-speed-slow) var(--app-ease);
}

.app-progress[data-tone]::after {
    background: var(--app-tone);
}

/* ═══ Fil d'étapes ═════════════════════════════════════════════════════════════════════ */

/* Où l'on en est dans une suite d'écrans. L'étape courante se marque par `aria-current="step"`
   et les précédentes par `data-done` : le numéro devient alors une coche, muette pour un
   lecteur d'écran puisque l'attribut porte déjà le sens. */
.app-steps {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--app-space-sm);
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: app-step;
}

.app-steps-item {
    display: flex;
    align-items: center;
    gap: var(--app-space-sm);
    counter-increment: app-step;
    font-size: var(--app-text-sm);
    color: var(--app-ink-muted);
}

.app-steps-item::before {
    content: counter(app-step);
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 1.9em;
    height: 1.9em;
    border: 1px solid var(--app-line-control);
    border-radius: var(--app-radius-pill);
    font-weight: var(--app-weight-bold);
}

/* Le trait qui relie une étape à la suivante prend la place qui reste. */
.app-steps-item:not(:last-child) {
    flex: 1;
}

.app-steps-item:not(:last-child)::after {
    content: "";
    flex: 1;
    min-width: var(--app-space-lg);
    height: 1px;
    background: var(--app-line-strong);
}

.app-steps-item[data-done] {
    color: var(--app-ink-soft);
}

.app-steps-item[data-done]::before {
    content: "✓" / "";
    border-color: var(--app-accent);
    background: var(--app-accent);
    color: var(--app-on-accent);
}

.app-steps-item[data-done]::after {
    background: var(--app-accent);
}

.app-steps-item[aria-current="step"] {
    font-weight: var(--app-weight-medium);
    color: var(--app-ink);
}

.app-steps-item[aria-current="step"]::before {
    border-color: var(--app-accent);
    color: var(--app-accent);
}

/* ═══ Accordéon ════════════════════════════════════════════════════════════════════════ */

/* Un `<details>` : le navigateur porte l'ouverture, la fermeture et l'annonce de l'état. Le
   même attribut `name` sur plusieurs d'entre eux n'en laisse qu'un ouvert à la fois. */
.app-accordion {
    background: var(--app-surface);
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius);
}

.app-accordion > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--app-space-sm);
    padding: var(--app-space-lg) var(--app-space-xl);
    font-weight: var(--app-weight-semibold);
    cursor: pointer;
    list-style: none;
}

.app-accordion > summary::-webkit-details-marker {
    display: none;
}

/* Le chevron : deux filets d'un carré, tournés. Il suit l'encre du titre, sans jeton de plus. */
.app-accordion > summary::after {
    content: "";
    flex: none;
    width: .5em;
    height: .5em;
    border-right: 2px solid currentcolor;
    border-bottom: 2px solid currentcolor;
    rotate: 45deg;
    transition: rotate var(--app-speed) var(--app-ease);
}

.app-accordion[open] > summary::after {
    rotate: 225deg;
}

.app-accordion-body {
    padding: 0 var(--app-space-xl) var(--app-space-xl);
}

/* ═══ Modale ═══════════════════════════════════════════════════════════════════════════ */

/* Un `<dialog>` ouvert par `showModal()` : le navigateur porte la pile, le piège au clavier
   et la fermeture par Échap. */
.app-modal {
    width: min(32rem, calc(100vw - var(--app-space-2xl)));
    padding: var(--app-space-xl);
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius);
    background: var(--app-surface);
    color: var(--app-ink);
    box-shadow: var(--app-shadow-pop);
}

.app-modal::backdrop {
    background: var(--app-backdrop);
}

.app-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--app-space-sm);
    margin-bottom: var(--app-space-lg);
}

.app-modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--app-space-sm);
    margin-top: var(--app-space-xl);
}

/* ═══ Menu déroulant ═══════════════════════════════════════════════════════════════════ */

/* Un `<details>` : il s'ouvre et se ferme sans script, et le comportement `menu` y ajoute ce
   que le navigateur ne donne pas — la fermeture par Échap et par un clic au-dehors. */
.app-menu {
    position: relative;
}

/* Positionné pour qu'une pastille puisse s'accrocher à son coin. Le déclencheur s'ouvre au
   clic : le pointeur le dit au survol. */
.app-menu > summary {
    position: relative;
    list-style: none;
    cursor: pointer;
}

.app-menu > summary::-webkit-details-marker {
    display: none;
}

/* La largeur ne dépasse jamais celle de l'écran : un panneau large sur un téléphone déborderait
   du côté où il s'accroche, hors d'atteinte. */
.app-menu-panel {
    position: absolute;
    top: calc(100% + var(--app-space-xs));
    right: 0;
    z-index: var(--app-z-dropdown);
    display: flex;
    flex-direction: column;
    min-width: 12rem;
    max-width: calc(100vw - var(--app-space-lg) * 2);
    padding: var(--app-space-xs);
    background: var(--app-surface);
    border: 1px solid var(--app-line);
    border-radius: var(--app-radius-ui);
    box-shadow: var(--app-shadow-pop);
}

/* Un panneau qui montre une grille plutôt qu'une liste : il lui faut la largeur de plusieurs
   colonnes, que douze rem ne donnent pas. */
.app-menu-panel[data-size="lg"] {
    min-width: 22rem;
}

/*
 * Un menu qui vit en bas de l'écran — le compte, au pied d'un menu latéral — ouvre son panneau
 * vers le haut. Vers le bas, il tomberait sous le bord de la fenêtre, ou hors du tiroir qui le
 * porte, et personne ne le verrait.
 */
.app-menu[data-drop="up"] .app-menu-panel {
    top: auto;
    bottom: calc(100% + var(--app-space-xs));
}

/* Le panneau s'ouvre vers la droite quand le bouton est à gauche de l'écran. */
.app-menu[data-align="start"] .app-menu-panel {
    right: auto;
    left: 0;
}

.app-menu-item {
    display: flex;
    align-items: center;
    gap: var(--app-space-sm);
    width: 100%;
    padding: var(--app-space-sm) var(--app-space-md);
    border: 0;
    border-radius: var(--app-radius-ui);
    font: inherit;
    font-size: var(--app-text);
    text-align: left;
    text-decoration: none;
    white-space: nowrap;
    background: transparent;
    color: var(--app-ink);
    cursor: pointer;
}

.app-menu-item:hover {
    background: var(--app-hover);
    text-decoration: none;
}

.app-menu-item[data-tone] {
    color: var(--app-tone);
}

.app-menu-item[data-tone]:hover {
    background: var(--app-tone-soft);
}

.app-menu-separator {
    margin: var(--app-space-xs) 0;
}

/*
 * La tête d'un panneau : qui est connecté, ou de quoi la liste parle et ce qu'on peut en faire
 * en bloc. Elle ne défile pas avec le corps — c'est ce qui la distingue d'une entrée.
 */
.app-menu-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--app-space-md);
    padding: var(--app-space-md);
    border-bottom: 1px solid var(--app-line);
}

/*
 * Le corps d'un panneau, quand ce qu'il porte peut être long : trente notifications pousseraient
 * sinon le panneau bien au-delà de l'écran. La hauteur se règle par `--app-menu-height` sur
 * l'instance, et se mesure en `rem` : elle suit la taille de texte du lecteur.
 */
.app-menu-body {
    display: flex;
    flex-direction: column;
    max-height: var(--app-menu-height, 18rem);
    overflow-y: auto;
}

/*
 * La pastille d'un déclencheur : elle dit qu'il y a du nouveau, pas combien. Le compte, quand il
 * compte, s'écrit dans une étiquette. Le halo la détache du fond sur lequel elle se pose, quelle
 * que soit la palette.
 */
.app-dot {
    position: absolute;
    top: 0;
    right: 0;
    width: .55rem;
    height: .55rem;
    border-radius: var(--app-radius-pill);
    background: var(--app-accent);
    box-shadow: 0 0 0 2px var(--app-surface);
}

.app-dot[data-tone] {
    background: var(--app-tone);
}

/* ═══ Titres de section ════════════════════════════════════════════════════════════════ */

/*
 * Ce qui coiffe une section : un dessin, un nom, et ce qui le qualifie — des pastilles, un
 * décompte, une date.
 *
 * **La rangée passe à la ligne quand elle ne tient plus.** Une rangée sans consigne de repli ne
 * se replie pas : elle pousse son dernier élément hors de l'écran, et c'est la page entière qui
 * se met à défiler de côté. Le cas n'est pas théorique — le bandeau d'une fiche de document, sur
 * un téléphone de trois cent quatre-vingt-dix pixels, y sortait sa date de vingt-trois pixels.
 *
 * Cette ligne ne change rien tant que rien ne dépasse : elle ne se voit que dans le cas qu'elle
 * répare.
 */
.app-section-title {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--app-space-sm);
    margin-bottom: var(--app-space-lg);
    padding-bottom: var(--app-space-sm);
    border-bottom: 1px solid var(--app-line);
}

/* ═══ Grilles ══════════════════════════════════════════════════════════════════════════ */

/* Le nombre de colonnes est un plafond, pas une consigne : chaque colonne cesse de rétrécir
   sous 15rem et la grille se replie d'elle-même. */
.app-grid {
    display: grid;
    gap: var(--app-space-lg);
    grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
}

.app-grid[data-cols="2"] {
    grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
}

.app-grid[data-cols="4"] {
    grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
}

/* ═══ Avatar ═══════════════════════════════════════════════════════════════════════════ */

.app-avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border-radius: var(--app-radius-pill);
    font-size: var(--app-text-sm);
    font-weight: var(--app-weight-bold);
    background: var(--app-accent-soft);
    color: var(--app-accent-ink);
    overflow: hidden;
}

.app-avatar[data-tone] {
    background: var(--app-tone-soft);
    color: var(--app-tone-ink);
}

.app-avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* ═══ Compte ═══════════════════════════════════════════════════════════════════════════ */

/* Le déclencheur du menu du compte : la pastille, et à côté le nom de qui est connecté. Un nom
   trop long s'arrête sur des points de suspension plutôt que d'élargir le menu latéral. */
.app-account {
    display: flex;
    align-items: center;
    gap: var(--app-space-sm);
    width: 100%;
    padding: var(--app-space-xs);
    border-radius: var(--app-radius-ui);
    color: var(--app-ink);
}

.app-account:hover {
    background: var(--app-hover);
}

.app-account-name {
    min-width: 0;
    overflow: hidden;
    font-size: var(--app-text);
    font-weight: var(--app-weight-medium);
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ═══ Navigation principale ════════════════════════════════════════════════════════════ */

/* Les grandes zones de l'application. La page courante se marque par `aria-current`, comme
   partout ailleurs. En colonne, c'est le menu latéral de la charpente. */
.app-nav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--app-space-xs);
}

.app-nav[data-orientation="vertical"] {
    flex-direction: column;
    flex-wrap: nowrap;
}


.app-nav-item {
    padding: var(--app-space-xs) var(--app-space-md);
    border-radius: var(--app-radius-ui);
    font-size: var(--app-text);
    font-weight: var(--app-weight-medium);
    color: var(--app-ink-soft);
    text-decoration: none;
}

.app-nav-item:hover {
    background: var(--app-hover);
    color: var(--app-ink);
    text-decoration: none;
}

.app-nav-item[aria-current="page"] {
    background: var(--app-accent-soft);
    color: var(--app-accent-ink);
}

/* ═══ Fil d'Ariane ═════════════════════════════════════════════════════════════════════ */

/* Le chemin parcouru pour arriver ici. Le séparateur est dessiné et non lu : un lecteur
   d'écran annonce déjà une liste, et n'a que faire d'une barre oblique entre deux liens. */
.app-breadcrumb {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--app-space-xs) var(--app-space-sm);
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--app-text-sm);
    color: var(--app-ink-muted);
}

.app-breadcrumb li {
    display: flex;
    align-items: center;
    gap: var(--app-space-sm);
}

/* La hauteur d'une ligne de texte ne fait pas une cible pour un doigt : le lien en gagne de
   quoi être visé sans que le fil s'épaississe à l'œil. */
.app-breadcrumb a {
    padding-block: var(--app-space-xs);
}

.app-breadcrumb li + li::before {
    content: "/" / "";
    color: var(--app-ink-muted);
}

.app-breadcrumb [aria-current="page"] {
    font-weight: var(--app-weight-medium);
    color: var(--app-ink);
}

/* ═══ Onglets ══════════════════════════════════════════════════════════════════════════ */

/* Les onglets défilent dans leur propre bande quand ils ne tiennent plus : ils gardent alors
   leur hauteur, là où un repli les ferait grandir de deux lignes et casserait l'alignement du
   trait. Le filet est une ombre intérieure et non une bordure — une bordure demanderait aux
   onglets de la recouvrir d'une marge négative, que le défilement découperait. */
/* La largeur minimale nulle est ce qui rend le défilement effectif. Posée dans une rangée
   souple, la bande refuserait autrement de descendre sous la largeur de ses onglets : elle ne
   défilerait jamais et pousserait la page entière à déborder de côté. */
.app-tabs {
    display: flex;
    min-width: 0;
    gap: var(--app-space-xs);
    overflow-x: auto;
    box-shadow: inset 0 -1px var(--app-line);
}

/* Le trait est déjà réservé par les onglets au repos : porté par le seul onglet courant, il
   grandirait la bande d'autant au moment de le marquer. */
.app-tabs-item {
    padding: var(--app-space-md) var(--app-space-lg);
    border-bottom: 3px solid transparent;
    font-size: var(--app-text);
    font-weight: var(--app-weight-medium);
    white-space: nowrap;
    color: var(--app-ink-muted);
}

/* Le survol s'arrête à l'encre adoucie : l'encre pleine appartient à l'onglet courant, et deux
   onglets de même couleur ne se départageraient plus. */
.app-tabs-item:hover {
    color: var(--app-ink-soft);
    text-decoration: none;
}

/* L'onglet courant se marque par `aria-current`, que le lecteur d'écran annonce déjà.
   Il se distingue par la clarté et la graisse, non par la teinte : entre un accent et une encre
   éteinte, l'œil ne lit qu'un écart de luminosité — sur un mot de cette taille, deux teintes
   d'égale clarté se ressemblent. La couleur reste au trait, qui la porte sans rien devoir dire
   à lui seul. */
.app-tabs-item[aria-current="page"] {
    border-bottom-color: var(--app-accent);
    font-weight: var(--app-weight-semibold);
    color: var(--app-ink);
}

/* ═══ Pagination ═══════════════════════════════════════════════════════════════════════ */

.app-pagination {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--app-space-xs);
    margin: 0;
    padding: 0;
    list-style: none;
}

.app-pagination-item {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.25rem;
    padding: var(--app-space-xs) var(--app-space-sm);
    border: 1px solid transparent;
    border-radius: var(--app-radius-ui);
    font-size: var(--app-text);
    font-weight: var(--app-weight-medium);
    color: var(--app-ink-soft);
    text-decoration: none;
}

.app-pagination-item:hover {
    background: var(--app-hover);
    color: var(--app-ink);
    text-decoration: none;
}

.app-pagination-item[aria-current="page"] {
    background: var(--app-accent-soft);
    border-color: var(--app-accent);
    color: var(--app-accent-ink);
}

/* Le pas qui n'existe pas : la première page quand on y est déjà. Il reste affiché pour que la
   barre ne change pas de forme d'une page à l'autre. */
.app-pagination-item[aria-disabled="true"] {
    opacity: .5;
    pointer-events: none;
}

/* Les pages qu'on ne montre pas, entre deux tronçons. */
.app-pagination-gap {
    padding: 0 var(--app-space-xs);
    color: var(--app-ink-muted);
}

/* ═══ La mesure du doigt ═══════════════════════════════════════════════════════════════ */

/*
 * Sur un écran qu'on tient d'une main, tout ce qui se touche atteint `--app-touch`. En dessous,
 * le doigt masque sa propre cible au moment de la viser : on ne corrige pas, on manque.
 *
 * La hauteur ne suffit pas seule. Un élément qui n'était pas déjà une boîte flexible garderait
 * son texte collé en haut d'une cible devenue deux fois plus grande, et l'on croirait la zone
 * sensible plus petite qu'elle n'est. Chacun reçoit donc aussi son alignement.
 *
 * La marge intérieure reste celle du bureau : c'est un plancher, pas un étirement. Un bouton qui
 * porte deux mots larges garde la taille que son contenu lui donne.
 */
@media (max-width: 37.5rem) {
    :is(.app-btn, .app-menu-item, .app-nav-item, .app-tabs-item, .app-pagination-item) {
        min-height: var(--app-touch);
    }

    /* Un bouton qui n'a qu'un dessin est aussi large que haut : sa marge intérieure seule le
       laissait à un cheveu de la mesure, et une cible carrée ne se manque pas d'un côté
       seulement. */
    .app-btn[data-icon] {
        min-width: var(--app-touch);
    }

    :is(.app-nav-item, .app-tabs-item) {
        display: flex;
        align-items: center;
    }

    /*
     * La case elle-même garde sa taille — c'est un dessin que le navigateur rend, et le grandir
     * le rendrait grossier. C'est son intitulé qui porte la cible : il coche déjà au clic, et il
     * s'étend sur toute la ligne.
     */
    .app-check {
        min-height: var(--app-touch);
    }

    /* Un numéro de page est étroit par nature : sans largeur minimale, la hauteur seule
       laisserait une cible haute et fine. */
    .app-pagination-item {
        min-width: var(--app-touch);
    }
}
