/*
 * 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 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. */
.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: 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;
    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;
}

/* 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. */
.app-menu > summary {
    position: relative;
    list-style: none;
}

.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 ════════════════════════════════════════════════════════════════ */

.app-section-title {
    display: flex;
    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;
}

/* ═══ 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. */
.app-tabs {
    display: flex;
    gap: var(--app-space-xs);
    overflow-x: auto;
    box-shadow: inset 0 -1px var(--app-line);
}

.app-tabs-item {
    padding: var(--app-space-md) var(--app-space-lg);
    border-bottom: 2px solid transparent;
    font-size: var(--app-text);
    font-weight: var(--app-weight-medium);
    white-space: nowrap;
    color: var(--app-ink-muted);
}

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

/* L'onglet courant se marque par `aria-current`, que le lecteur d'écran annonce déjà. */
.app-tabs-item[aria-current="page"] {
    border-bottom-color: var(--app-accent);
    color: var(--app-accent);
}

/* ═══ 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);
}
