/* =========================================================
   PONT LEGACY — à supprimer avec app.css et Bootstrap
   =========================================================
   Le design system pose ses propres bases sur les éléments HTML (tokens/base.css) : elles
   remplacent celles du reboot Bootstrap, sur lesquelles les écrans pas encore refondus comptent
   encore. Ce fichier ne restitue que ce qui manquerait, et uniquement en tokens.

   Il est chargé après styles.css : ses sélecteurs d'élément (spécificité 0,0,1) l'emportent sur
   ceux de tokens/base.css, et perdent face à n'importe quelle classe .ds-* (0,1,0). Les composants
   du système gardent donc la main partout où ils s'appliquent.

   Chaque règle disparaît quand le dernier écran qui en dépend a basculé. Le fichier est vide à la
   fin de la refonte — c'est son critère de réussite. */

/* Fond de l'application.
   tokens/base.css pose body { background: var(--color-bg) }, soit du blanc, alors que readme.md
   prescrit --color-bg-subtle comme fond d'application, les surfaces de contenu restant blanches.
   Le système n'a pas de classe de coquille qui porte ce fond, et son sélecteur body l'emporte sur
   celui d'app.css (« html, body » ne cumule pas sa spécificité : chaque sélecteur de la liste vaut
   pour lui-même). Sans cette règle, toutes les cartes du portail sont blanches sur blanc.
   --bg résout sur --color-bg-subtle, y compris en sombre. */
body {
    background: var(--bg);
}

/* tokens/base.css remet les titres et les paragraphes à margin: 0, ce qui est le bon défaut pour
   des composants. Les écrans encore en Bootstrap s'appuient sur les marges du reboot : sans elles,
   tous les blocs de texte du portail se collent. Valeurs du reboot, exprimées en tokens
   (0,5 rem = --space-2, 1 rem = --space-4). */
h1, h2, h3, h4, h5, h6 {
    margin-block: 0 var(--space-2);
}

p {
    margin-block: 0 var(--space-4);
}

/* =========================================================
   CORRECTIF SYSTÈME — bascule de gabarit (data-viewport)
   =========================================================
   Défaut dans le système lui-même, pas dans le portail.

   css/mobile.css déclare, dans cet ordre :
     ligne 133  [data-viewport="phone"], [data-viewport="handset"] { display: none }
     ligne 142  .ds-rowcards { display: flex; ... }

   Les deux valent (0,1,0) : à spécificité égale, la dernière gagne. Un
   .ds-rowcards[data-viewport] reste donc affiché à TOUTES les largeurs. Appairé au conteneur de
   tableau que le système prescrit lui-même (commentaire de mobile.css ligne 141), le bureau montre
   à la fois le tableau ET la liste en cartes : la liste de dossiers apparaît deux fois. Constaté à
   1440 px sur TicketTable.razor, qui suit à la lettre la paire documentée.

   Correction ici plutôt que dans css/ds/ : le système est recopié verbatim et resynchronisé, une
   retouche sur place serait perdue. Le sélecteur porte la classe ET l'attribut (0,2,0), il passe
   donc devant sans !important.

   SEUIL — le portail bascule en cartes à 899 px et non à 599, donc sur le marqueur « phone » et
   non « handset ». Raison : à partir de 900 px la barre latérale fixe reprend 264 px, et entre 600
   et 899 px les sept colonnes de la file support n'étaient pas lisibles. Les deux marqueurs sont
   fournis par le système, on prend simplement l'autre paire.

   À RETIRER dès que le système corrige l'ordre de ses deux règles. */
.ds-rowcards[data-viewport="phone"] {
    display: none;
}

@media (max-width: 899px) {
    .ds-rowcards[data-viewport="phone"] {
        display: flex;
    }
}

/* Même défaut, deuxième victime : l'EN-TÊTE COMPACT du téléphone.

   Le système prescrit lui-même `.ds-appbar` + `data-viewport="phone"`. Or dans la fenêtre
   ≤ 899 px, sa règle de bascule pose `display: revert`. Or `revert` ne rend pas la valeur de
   `.ds-appbar` — il rend celle de l'origine précédente, c'est-à-dire la feuille du navigateur :
   pour un <header>, `block`. La barre perd donc son `display: grid` et ses trois zones
   (retour / titre / action) s'empilent verticalement. À l'écran, le bouton de menu tombe SOUS
   l'en-tête et se superpose au titre de la page — c'est exactement ce qu'on voyait à 390 px.

   `revert` est le mauvais mot-clé pour ce travail : le seul qui rende la valeur de la cascade
   auteur est `revert-layer`, et encore, à condition que les deux règles vivent dans des couches
   distinctes. Un système qui veut masquer puis rétablir doit soit remettre la valeur explicitement
   par composant, soit s'appuyer sur `@layer`.

   Ici on remet la valeur du système, avec la classe ET l'attribut (0,2,0). */
.ds-appbar[data-viewport="phone"] {
    display: none;
}

@media (max-width: 899px) {
    .ds-appbar[data-viewport="phone"] {
        display: grid;
    }
}

/* =========================================================
   CORRECTIF SYSTÈME — un bouton rendu en lien devient illisible au survol
   =========================================================
   Défaut dans le système, pas dans le portail.

   `tokens/base.css` pose `a:hover { color: var(--color-link-hover) }`. Spécificité 0,1,1 : ce
   sélecteur l'emporte sur `.ds-btn--primary` (0,1,0), qui pose `color: var(--brand-on-primary)`.
   Résultat : tout `<a class="ds-btn">` perd sa couleur de texte au survol et prend celle d'un lien.
   Sur les variantes à fond foncé, cela donne du bleu marine sur bleu marine — le libellé disparaît.
   Constaté sur « Fiche PDF » de l'en-tête de dossier.

   La vitrine ne montre que des `<button>` pour ses boutons, jamais un lien habillé en bouton : le
   défaut ne s'y voit donc pas. Mais un bouton qui NAVIGUE doit être un `<a>` — c'est ce qui donne le
   clic milieu, le « ouvrir dans un nouvel onglet » et l'annonce correcte aux lecteurs d'écran. Le
   portail en a quinze.

   On restitue ici la couleur de chaque variante, avec la classe ET l'élément (0,2,1) pour passer
   devant `a:hover`. Les valeurs sont recopiées de css/buttons.css, sans en inventer aucune.

   À RETIRER dès que le système protège ses boutons de sa propre règle sur les liens. */
a.ds-btn--primary:hover {
    color: var(--brand-on-primary);
}

a.ds-btn--secondary:hover {
    color: var(--brand-ink);
}

a.ds-btn--ghost:hover {
    color: var(--color-text);
}

a.ds-btn--inverse:hover {
    color: var(--brand-ink);
}

/* Les deux valeurs qui suivent sont ecrites en clair dans css/buttons.css et n'ont pas de jeton :
   le fond de ces deux variantes est fonce (danger) ou vif (accent) dans les DEUX themes, donc leur
   texte ne bascule pas. Prendre --color-text-inverse a la place aurait donne du texte sombre sur
   fond rouge en theme sombre. On recopie donc le systeme a l'identique plutot que de deviner. */
a.ds-btn--danger:hover {
    color: #fff;
}

a.ds-btn--accent:hover {
    color: #1a1300;
}

/* =========================================================
   COUCHE DU PORTAIL — curseur des éléments interactifs
   =========================================================
   Ce bloc n'est PAS un correctif du système, contrairement à ce qui était écrit ici : vérification
   faite sur la copie locale du système, `.ds-tab`, `.ds-segmented__option`,
   `.ds-collapsible__summary` et `.ds-dropdown__option` posent tous `cursor: pointer`. Le système est
   complet sur ce point.

   Les éléments qui gardaient la flèche étaient les nôtres : nos propres `<button>` habillés par des
   classes maison — jetons de transition, déclencheur de la palette, tuiles du tableau de bord — dont
   la valeur par défaut du navigateur est `default`. D'où l'incohérence signalée en recette : la main
   sur un bouton du système, la flèche sur le bouton d'à côté.

   Une règle d'élément (0,0,1) suffit et ne masque rien : n'importe quelle règle `.ds-*` qui poserait
   un curseur passe devant. `:disabled` garde la main barrée, comme `.ds-btn:disabled` du système.

   À garder tant que le portail rend des boutons hors système. */
button:not(:disabled):not([aria-disabled="true"]),
summary,
[role="button"]:not([aria-disabled="true"]),
label:has(> input[type="checkbox"]),
label:has(> input[type="radio"]) {
    cursor: pointer;
}

button:disabled,
button[aria-disabled="true"],
[role="button"][aria-disabled="true"] {
    cursor: not-allowed;
}

/* =========================================================
   CORRECTIF SYSTÈME — une animation qui garde son état final emprisonne
   tout `position: fixed` de la page
   =========================================================
   Défaut du système, et le plus retors rencontré jusqu'ici : il explique les deux « comportements
   bizarres » signalés en recette — la modale de correction du contexte et la palette de commandes —
   ainsi que le menu « Actions rapides » qui passait sous les lignes suivantes du tableau.

   Le système écrit ses animations d'arrivée en `animation-fill-mode: both` :

       .ds-page-enter  { animation: ds-page-enter  … both; }
       .ds-stagger > * { animation: ds-fade-in-up  … both; }
       @keyframes ds-fade-in-up { from { transform: translateY(7px) } to { transform: none } }

   `both` = en arrière ET en avant. En avant, l'animation continue d'imposer son dernier
   keyframe une fois terminée — et `transform: none` s'y résout non pas en `none` mais en
   `matrix(1, 0, 0, 1, 0, 0)`. Une matrice identité ne déplace rien, mais elle reste « un transform
   autre que none » : elle crée donc, DÉFINITIVEMENT, un bloc conteneur et un contexte
   d'empilement.

   Deux conséquences, mesurées sur banc :

   · `position: fixed` à l'intérieur ne se cale plus sur la fenêtre mais sur cet élément. Le voile
     d'une modale, censé couvrir 1440 × 900, mesurait 1118 × 75 à 288 px du bord — soit exactement
     la boîte du panneau d'onglet. D'où un en-tête resté net, une barre latérale non voilée, et une
     modale rognée.

   · chaque enfant d'une cascade `ds-stagger` devient son propre contexte d'empilement. Sur un
     `<tbody class="ds-stagger">`, chaque ligne en est un, peints dans l'ordre du DOM : le menu
     ouvert sur la ligne 2 passait donc sous les lignes 3, 4, 5…

   `backwards` conserve ce qui sert — l'état de départ pendant le délai, sans quoi les lignes d'une
   cascade clignoteraient avant leur tour — et abandonne le seul état qui nuit. Après l'animation,
   la propriété revient à sa valeur de base, qui est déjà celle du dernier keyframe : rien ne change
   à l'écran, et le `transform` redevient `none`.

   À RETIRER dès que le système passe ses arrivées en `backwards`. */
/* PORTEE ELARGIE AU LOT 4a. Les trois selecteurs traites au lot precedent ne couvraient que ce
   qui avait ete signale. Le comptage exact du systeme donne 30 regles combinant `both` avec une
   image-cle qui touche `transform`, sur 26 selecteurs distincts — donc 26 endroits qui laissent
   une matrice identite. Parmi eux, ceux qui HEBERGENT des surcouches : .ds-modal, .ds-drawer,
   .ds-sheet, .ds-cmdk, .ds-megamenu, les menus deroulants.

   Critere de bascule, verifiable : on passe en `backwards` uniquement quand la DERNIERE image-cle
   vaut l'etat de base de l'element — `transform: none`, ou un `scale(1)` / `scaleX(1)` / `scaleY(1)`
   qui est la matrice identite. Dans ce cas l'etat final n'apporte rien et ne coute que le bloc
   conteneur.

   UNE exception, et elle est deliberee : `.ds-toast__bar` anime `ds-toast-countdown`, qui finit sur
   `scaleX(0)`. Ici l'etat final EST l'information — la barre doit rester vide. En `backwards`, elle
   remonterait d'un coup a pleine largeur en fin de compte a rebours. Elle garde donc `both`.

   A RETIRER des que le systeme passe ses arrivees en `backwards`. */
.ds-page-enter,
.ds-stagger > *,
.ds-anim-page,
.ds-anim-in,
.ds-anim-scale,
.ds-modal,
.ds-drawer,
.ds-sheet,
.ds-cmdk,
.ds-megamenu,
.ds-popover,
.ds-tooltip,
.ds-dropdown__menu,
.ds-notifs,
.ds-calendar,
.ds-help__panel,
.ds-selectionbar,
.ds-connection,
.ds-alert,
.ds-toast,
.ds-empty,
.ds-field__error,
.ds-chart__bar,
.ds-stat__value[data-changed="true"],
.ds-track__step[data-state="done"] .ds-track__line {
    animation-fill-mode: backwards;
}

/* Curseur des contrôles de sélection restés hors du système.

   Deux familles gardaient la flèche : les `<select>` natifs habillés en `.form-select` — un débris de
   Bootstrap qui traîne dans la barre d'outils du tableau de bord — et les champs de saisie qui font
   office de liste déroulante (`role="combobox"` de SearchDropdown : « Toute la file »).

   Le second cas mérite une justification : un combobox est un champ où l'on TAPE, et le curseur de
   texte y est normalement juste. Mais celui-ci se présente comme les trois filtres voisins, qui sont
   des boutons : à l'œil c'est une liste déroulante, et l'incohérence de curseur d'un filtre à l'autre
   se voit plus que la nuance de saisie. La main l'emporte.

   À retirer quand la barre d'outils du tableau de bord passera sur `.ds-select`, qui porte déjà le
   bon curseur. */
select,
.ds-dropdown input[role="combobox"] {
    cursor: pointer;
}

select:disabled,
.ds-dropdown input[role="combobox"]:disabled {
    cursor: not-allowed;
}

/* Titres du système en --brand-ink.

   Même motif que les titres du détail (voir ticket-detail.css) : le système laisse ses titres hériter
   de --color-text, un gris neutre. `--brand-ink` est le jeton que le système réserve au TEXTE de
   marque — il le distingue de `--brand-primary`, qui est pour les aplats — et il l'emploie lui-même
   sur l'onglet actif et sur l'en-tête de tableau au survol.

   Étendu ici aux titres de carte et aux titres de panneau repliable, qui sont les deux endroits où
   le portail écrit un titre. La marque redevient présente partout sans un aplat de plus, et le
   contraste monte. Réversible en supprimant ce bloc. */
.ds-card__title,
.ds-collapsible__title {
    color: var(--brand-ink);
}

/* =========================================================
   LACUNE SYSTEME — `.ds-badge` peut se casser sur deux lignes
   =========================================================
   La pastille « Réf. manuelle » de la liste passait sur deux lignes et débordait de son fond.

   `.ds-badge` (ds/css/buttons.css) est déclarée avec une hauteur FIGÉE et sans `white-space` :

       .ds-badge { height: 22px; ... text-transform: uppercase; letter-spacing: var(--tracking-wide) }

   Sa voisine immédiate, `.ds-status` (ds/css/patterns.css), a les deux protections — padding
   vertical au lieu d'une hauteur, ET `white-space: nowrap`. C'est donc un oubli du système, pas
   une intention : deux pastilles du même système, deux comportements.

   Le libellé est de surcroît allongé par les capitales et l'interlettrage, et le badge est le
   dernier enfant flexible de la colonne « Dossier », où le numéro est verrouillé et le type est
   tronqué : toute la compression tombait sur lui.

   `height: auto` + `min-height` conserve exactement l'apparence au repos et laisse la pastille
   grandir si une traduction est plus longue que le français.

   À RETIRER dès que le système donne un `white-space` à `.ds-badge`. */
.ds-badge {
    white-space: nowrap;
    height: auto;
    min-height: 22px;
}

/* =========================================================
   DEFAUT SYSTEME — le repli mobile de la barre de filtres laisse un vide de ~190 px
   =========================================================
   Signalé en recette : sur téléphone, le champ de recherche flottait au milieu d'un grand blanc.

   Le système déclare `.ds-filterbar__grow { flex: 1 1 240px }` (ds/css/patterns.css) puis, sous
   600 px, passe la rangée en colonne (ds/css/mobile.css) :

       .ds-filterbar__row { flex-direction: column; align-items: stretch }
       .ds-filterbar__row .ds-dropdown, .ds-filterbar__grow { width: 100% }

   Il corrige la largeur mais ne touche PAS à `flex`. Or `flex-basis` s'applique sur l'axe
   PRINCIPAL : en colonne, l'axe principal est vertical, donc les 240 px deviennent une HAUTEUR.
   Le champ garde ses 48 px (`--control-h-md` sous 600 px) et `.ds-input-group { align-items:
   center }` le centre — d'où ~96 px de vide au-dessus et autant en dessous.

   Le portail porte la classe sur un `.ds-input-group` et non sur l'`<input>` comme le fait la
   référence du système — parce que notre champ a une icône en préfixe, et que c'est le groupe,
   pas le champ, qui doit s'étirer sur ordinateur. Déplacer la classe casserait la rangée de
   bureau ; le vide se corrige donc ici, du côté de l'axe, et non dans le balisage.

   À RETIRER dès que le système neutralise `flex` dans son propre repli. */
@media (max-width: 599px) {
    .ds-filterbar__grow {
        flex: none;
    }
}

/* =========================================================
   AJOUT PORTAIL — le détail d'un ticket arrive par le bas
   =========================================================
   Le système n'a pas de transition de page à proprement parler : son `ds-page-enter` translate
   7 px en 240 ms. C'est juste, pour passer d'un écran de liste à un autre écran de liste — mais
   entre une liste et le dossier qu'on vient d'y ouvrir, rien ne signalait qu'on changeait de plan.

   La variante ci-dessous ne s'applique QUE sur l'arrivée d'un détail de ticket, posée depuis
   MainLayout. Elle réutilise la seule courbe du système et une durée en jeton : rien d'inventé
   sauf la course, qui est un jeton du portail (`--ps-page-rise`, dans app.css).

   `backwards` et jamais `both` : l'état final d'une animation qui touche `transform` laisse une
   matrice identité, qui fait de l'élément un bloc conteneur et emprisonne tout `position: fixed`
   de la page. C'est le piège rencontré trois fois dans ce dépôt.

   Pendant l'animation, le conteneur porte un `transform` : le bouton flottant monte donc avec la
   page. Pour une présentation en feuille c'est cohérent, et c'est la raison de ne pas allonger
   la durée.

   Durée ramenée de --duration-slower à --duration-base au lot 4c : « un peu longue », signalé en
   recette. La cause principale était ailleurs — le glissement d'onglet jouait aussi à l'arrivée, sur
   450 ms — mais la montée gagne elle aussi à être plus vive. */
.ps-page-enter--rise {
    animation-name: ps-page-rise;
    animation-duration: var(--duration-base);
    animation-timing-function: var(--ease);
    animation-fill-mode: backwards;
}

@keyframes ps-page-rise {
    from {
        opacity: 0;
        transform: translateY(var(--ps-page-rise));
    }

    to {
        opacity: 1;
        transform: none;
    }
}

/* Le système impose de neutraliser tout déplacement sous cette préférence. */
@media (prefers-reduced-motion: reduce) {
    .ps-page-enter--rise {
        animation-name: none;
    }
}

/* =========================================================
   MANQUE SYSTEME — le panneau repliable n'a aucun repli mobile
   =========================================================
   Signalé en recette à 390 px : sur « Vue d'ensemble opérationnelle » et « Traitement standard
   recommandé », le titre et la description du sommaire se replient sur une colonne de huit
   caractères, et le texte DÉBORDE par-dessus le contenu de droite.

   Trois pièces produisent le défaut, et aucune n'est fautive seule :

   · `.ds-collapsible__head` est `flex: 1; min-width: 0` — donc réductible jusqu'à zéro ;
   · le contenu de droite est incompressible : un `.ds-btn` est `white-space: nowrap`, et le portail
     verrouille en plus `.ticket-context-edit-button { flex: none }` ;
   · `.ds-collapsible__title` et `__desc` n'ont **ni `overflow`, ni `overflow-wrap`** : le texte ne
     se coupe donc pas, il sort de sa boîte et se peint par-dessus le voisin.

   `ds/css/mobile.css` (235 lignes) ne mentionne jamais `ds-collapsible` : le système n'a pas prévu
   ce repli. Il existait pourtant côté portail, dans un `CollapsiblePanel.razor.css` supprimé depuis
   — il masquait purement le slot d'en-tête sous 640 px, ce qui a obligé à DUPLIQUER dans le corps le
   bouton « Ajouter des fichiers » et les pastilles de décision. Le repli ci-dessous fait passer le
   slot à la ligne au lieu de le masquer, ce qui rend les deux duplications inutiles : elles ont été
   retirées au lot 4c.

   À RETIRER dès que le système fournit son propre repli. */
.ds-collapsible__title,
.ds-collapsible__desc {
    overflow-wrap: anywhere;
}

@media (max-width: 599px) {
    .ds-collapsible__summary {
        flex-wrap: wrap;
        gap: var(--space-2) var(--space-3);
    }

    /* Le bloc de titre garde toute la largeur, le contenu d'en-tête passe dessous. Le chevron reste
       sur la première ligne, aligné à droite du titre. */
    .ds-collapsible__head {
        flex: 1 1 100%;
        order: 1;
    }

    .ds-collapsible__chevron {
        order: 2;
    }

    .ds-collapsible__summary > :not(.ds-collapsible__head):not(.ds-collapsible__chevron) {
        order: 3;
        flex: 1 1 100%;
    }
}

/* =========================================================
   AJOUT PORTAIL — l'en-tête compact mange les cibles d'ancrage
   =========================================================
   `.ds-appbar` est `position: sticky` en tête du contenu, donc tout saut d'ancre amène la cible sous
   la barre. Aucun `scroll-padding-top` n'était déclaré. La valeur suit le jeton du système, encoche
   comprise. */
@media (max-width: 899px) {
    html {
        scroll-padding-top: calc(var(--header-h-mobile) + var(--safe-top) + var(--space-3));
    }
}

/* =========================================================
   AJOUT PORTAIL — le panneau de notifications n'a aucun ancrage dans le systeme
   =========================================================
   Le systeme livre la cloche (`.ds-bell`, `.ds-bell__dot`) et le panneau complet (`.ds-notifs` et
   ses trois zones), mais rien qui les relie : sa fiche « Gestion & pilotage » pose le panneau dans
   une colonne STATIQUE, cote a cote avec un autre bloc, et son gabarit de console ne montre que la
   cloche. L'ancrage est donc a nous — ecart 47.

   Il tient en quatre declarations parce que `.ds-bell` porte deja `position: relative` : aucun
   nouveau contexte de positionnement n'est cree. Meme forme que le menu du profil
   (`.ps-user-dropdown`), a une difference pres — le `z-index` prend le jeton du systeme au lieu du
   litteral 1050 que celui-la porte encore.

   Pas de piege de conteneur ici : l'en-tete porte un `backdrop-filter`, qui emprisonne les
   descendants en `position: fixed` (ecart 25) mais pas ceux en `absolute`. Et `.ds-header` n'a
   aucun `overflow`, donc rien ne rogne le panneau. Verifie, pas suppose. */
.ds-bell > .ds-notifs {
    position: absolute;
    inset-inline-end: 0;
    top: calc(100% + var(--space-2));
    z-index: var(--z-popover);
}

/* Deux etats de la liste que le systeme ne couvre pas.

   L'etat vide : `.ds-empty` reserve `2 x --space-16` de respiration verticale (ecart 45), ce qui est
   juste pour une pleine page et absurde dans un panneau de 400 px qu'on vient d'ouvrir. Une phrase
   centree suffit a repondre.

   L'attente : les barres du systeme (`.ds-skeleton--text`) n'ont pas de conteneur. Trois barres
   valent mieux qu'un tourniquet, que le systeme proscrit, et mieux qu'un gabarit detaille — le
   panneau se remplit en une requete. */
.ps-notifs__empty {
    margin: 0;
    padding: var(--space-6) var(--space-4);
    text-align: center;
}

.ps-notifs__loading {
    display: grid;
    gap: var(--space-4);
    padding: var(--space-4);
}

/* Le point de non-lu sur le bouton de menu de l'en-tete compact.

   Sous 900 px l'en-tete de bureau est en `display: none`, donc la cloche n'y est pas : c'est le
   defaut corrige au lot 4c pour la recherche et la deconnexion, et il ne faut pas le refaire. Mais
   l'AppBar n'a qu'UNE action a droite, par une decision ecrite dans MobileNav.razor. On ne la
   contredit pas : le point se pose sur le bouton de menu, et la liste vit dans la feuille.

   `.ds-appbar__action` n'est pas `position: relative` dans le systeme — il faut le poser ici, sinon
   le point se placerait par rapport a l'AppBar entiere. */
.ds-appbar__action:has(.ds-bell__dot) {
    position: relative;
}

/* =========================================================
   AJOUT PORTAIL — le systeme n'a aucune animation de SORTIE
   =========================================================
   `craft.css` ne donne que des entrees : fade-in, fade-in-down, fade-in-up, slide-in. Rien pour un
   element qui s'en va. Or « tout marquer comme lu » vide le panneau de notifications, et faire
   disparaitre cinq lignes d'un seul coup ne dit pas ce qui vient de se passer. Ecart 53.

   LA CASCADE PART DE LA PREMIERE LIGNE : c'est l'ordre de lecture, et c'est ce qui donne
   l'impression que la liste se vide plutot que de s'effacer. Le rang vient de `--ps-exit-index`,
   pose par le balisage.

   Le plafond a huit rangs est ce qui empeche vingt entrees de faire une seconde d'attente. Le
   systeme plafonne de la meme facon ses cascades d'entree, a la quatrieme
   (`.ds-popover > .ds-menu-item:nth-child(n + 4)`).

   `animation-fill-mode: forwards`, ET C'EST LE SEUL ENDROIT OU JE L'ECRIS. Une animation de sortie
   doit tenir son etat final : sans lui, les lignes reparaissent le temps d'une image avant que le
   serveur ne les retire. La contrepartie est connue et documentee aux ecarts 21 et 33 — `forwards`
   sur un `transform` laisse une matrice, donc un bloc conteneur pour tout descendant en
   `position: fixed`. Sans danger ici, verifie : ces lignes sont retirees dans la foulee et ne
   contiennent qu'une icone et deux libelles.

   La duree totale est tenue en accord avec NotificationCenterState.ClearAnimationDuration, qui est
   ce que le serveur attend avant de vider la liste. Les deux valeurs doivent bouger ensemble :
   8 x 30 ms + 200 ms = 440 ms. */
.ds-notif[data-leaving="true"] {
    animation: ps-notif-exit var(--duration-base) var(--ease) forwards;
    animation-delay: calc(min(var(--ps-exit-index, 0), 8) * 30ms);
    pointer-events: none;
}

@keyframes ps-notif-exit {
    from {
        opacity: 1;
        transform: translateX(0);
    }

    to {
        opacity: 0;
        transform: translateX(var(--space-8));
    }
}

/* Mouvement reduit : les lignes ne glissent pas. Elles disparaissent apres le meme temps mort, le
   serveur ne pouvant pas lire une requete de media — un aller-retour JavaScript pour economiser
   440 ms couterait plus qu'il ne rapporte. */
@media (prefers-reduced-motion: reduce) {
    .ds-notif[data-leaving="true"] {
        animation: none;
        opacity: 0;
    }
}

/* ============================================================================================
   LARGEUR DU PANNEAU DES LISTES DEROULANTES — defaut du systeme, corrige ici.

   `patterns.css` pose sur `.ds-dropdown__menu` : `position: absolute; left: 0; right: 0`. Les deux
   decalages opposes CLOUENT le panneau a la largeur exacte de son declencheur. Or le declencheur est
   dimensionne par son libelle court — « A traiter », « Tous les types » — et les OPTIONS, elles, sont
   des phrases : « Mon produit ne fonctionne pas ou est defectueux », « Tout deselectionner ».

   Releve en recette sur la file support : un panneau de 105 px ou chaque option se coupait sur trois
   a cinq lignes, et « Tout deselectionner » s'affichait « Tou deselect ». Ce n'est pas un defaut de
   nos donnees : la vitrine du systeme ne montre que des options d'un ou deux mots, donc le defaut ne
   s'y voit pas — comme pour `a.ds-btn` au survol (ecart 12).

   La correction est ici et non dans `css/ds/` : cette feuille est recopiee verbatim depuis le
   catalogue, on ne l'edite pas a la main.

   `right: auto` libere la largeur, `min-width` garantit qu'on n'est JAMAIS plus etroit que le
   declencheur (100 %) ni qu'une largeur ou une phrase tient (15rem), `width: max-content` s'ajuste au
   contenu reel, et `max-width` empeche un libelle long de sortir de la fenetre — le second terme est
   la borne qui compte sur telephone. */
.ds-dropdown__menu {
    right: auto;
    min-width: max(100%, 15rem);
    width: max-content;
    max-width: min(24rem, calc(100vw - var(--space-8)));
}

/* Le declencheur, lui, garde sa largeur : c'est le panneau qui s'elargit, pas la barre de filtres.
   `overflow-wrap` plutot que `word-break` : on coupe DANS un mot seulement s'il ne tient pas seul,
   pas des que la ligne est pleine. */
.ds-dropdown__option,
.ds-dropdown__clear {
    overflow-wrap: break-word;
}

/* ============================================================================================
   BARRE DE SELECTION FLOTTANTE — un `sticky` que son ancetre empechait de coller.

   `patterns.css` la pose en `position: sticky; bottom: var(--space-6)`, ce qui est le bon choix. Elle
   ne collait pourtant pas : releve en recette, il fallait descendre en bas de la page pour la voir.

   LA CAUSE EST L'ANCETRE, PAS LA REGLE. La file support est rendue dans
   `<section class="ticket-list-shell ds-card">`, et `.ds-card` porte `overflow: hidden` (il lui donne
   ses coins coupes). Un ancetre dont l'overflow n'est pas `visible` devient le SCROLLPORT de tout
   `sticky` qu'il contient — et cette carte ne defile pas, elle grandit. La barre n'avait donc aucune
   marge pour coller et restait a sa place dans le flux, en bas de la carte.

   C'est la TROISIEME fois qu'un `overflow` met un `sticky` hors service dans ce portail : d'abord
   `overflow-x: hidden` sur le `body` (corrige en `clip`), puis un `overflow: auto` qui rognait un
   menu, et maintenant celui-ci.

   `position: fixed` ne depend d'aucun ancetre — ni de son overflow, ni de son bloc conteneur, ni de
   sa hauteur. C'est exactement le raisonnement deja tenu pour la barre laterale (shell.css), et il
   vaut ici pour la meme raison. Retirer `overflow: hidden` de `.ds-card` serait l'autre voie, mais
   elle coute les coins coupes de TOUTES les cartes du portail pour un seul composant.

   UN FIXE PEUT ETRE PIEGE PAR UN ANCETRE TRANSFORME, et c'est le piege documente en 80 lignes plus
   haut dans ce fichier. Verifie ici : le seul ancetre anime est `.ds-page-enter`, que ce pont ramene
   justement de `both` a `backwards` — il ne laisse donc AUCUNE matrice residuelle une fois
   l'animation finie. Et la barre n'existe qu'apres une case cochee, bien apres l'entree de page.

   Le decalage lateral suit celui de la barre laterale, au meme jeton et au meme seuil de 900 px que
   `.app-shell__body` : les trois valeurs ne peuvent pas desaccorder. La reserve de 88 px sous la
   liste reste necessaire — plus encore qu'avant, la barre passant maintenant PAR-DESSUS le contenu.
   Elle a d'ailleurs change de porteur : elle etait sur la CARTE, qu'elle faisait grandir a chaque
   selection (effet de bord releve en recette), et elle est passee sur la zone de contenu — la barre
   etant hors flux, il n'y a plus de raison de deformer la carte pour lui faire de la place.

   ELLE EST PLUS ETROITE QUE LA CARTE, et c'est voulu : une barre d'actions aussi large que la liste
   se lit comme une seconde rangee du tableau. `width: max-content` la taille sur ses boutons — donc
   elle grandit avec eux et jamais au-dela —, `margin-inline: auto` la centre dans la bande, et le
   plafond de 72rem l'empeche de rejoindre la largeur de la carte (le contenu monte a
   `--container-xl`) sur un tres grand ecran.

   Le decalage lateral suit celui de la barre laterale, au meme jeton et au meme seuil de 900 px que
   `.app-shell__body` : les trois valeurs ne peuvent pas desaccorder. Sous 600 px, c'est `mobile.css`
   du systeme qui garde la main sur les decalages (et sa marge de securite basse) — d'ou une regle
   bornee a la bande intermediaire plutot qu'une regle de base qui l'ecraserait. */
.ds-selectionbar {
    position: fixed;
}

@media (min-width: 600px) and (max-width: 899px) {
    .ds-selectionbar {
        inset-inline: var(--space-4);
    }
}

@media (min-width: 900px) {
    .ds-selectionbar {
        inset-inline-start: calc(var(--sidebar-w) + var(--space-6));
        inset-inline-end: var(--space-6);
        width: max-content;
        max-width: 72rem;
        margin-inline: auto;
    }
}

/* ============================================================================================
   HAUTEUR DES CONTROLES DE LA BARRE DE FILTRES — trois controles, trois hauteurs.

   Releve en recette : le filtre par assignation etait visiblement plus haut que les deux autres, et
   la case a cocher ne s'alignait avec aucun des trois. Mesure des regles du systeme :

   · `.ds-dropdown__trigger` (statuts, types) : `min-height: 40px`, AUCUN padding vertical, et
     `font: var(--type-body-sm)`. Hauteur reelle : 40 px.
   · `.ds-input` (assignation, et le champ de recherche) : `min-height: var(--control-h-md)` soit
     40 px, MAIS `padding: var(--space-2) …` soit 8 px en haut et en bas, et `font: var(--type-body)`
     — une police plus grande. Contenu 24 px + 16 px de padding + 2 px de bordure = 42 px, donc le
     `min-height` est depasse et ne borne rien.

   Deux pixels d'ecart pour deux controles cote a cote dans la meme rangee. La hauteur est fixee au
   jeton pour les deux, et le padding vertical du champ retire — c'est lui qui poussait au-dela.

   LA TYPOGRAPHIE AUSSI, ET C'EST LE SECOND PASSAGE. Au premier, la hauteur seule avait ete corrigee
   en laissant volontairement les polices tranquilles ; la recette a montre que c'etait insuffisant —
   « sa police est plus grande et plus foncee ». Le compte exact, apres le relevement d'echelle
   d'app.css :

   · `.ds-dropdown__trigger` → `--type-body-sm` = 400 / 14 px / 1.5
   · `.ds-input` ET `.ds-choice` → `--type-body` = 400 / 16 px / 1.65

   La rangee melangeait donc DEUX tailles : 14 px pour les deux deroulants, 16 px pour le champ de
   recherche, le filtre par assignation et la case a cocher. Tout passe sur `--type-body-sm`, le role
   que le systeme emploie DEJA pour son propre declencheur — on aligne sur le sien, on n'en invente
   pas un.

   Sur le « plus foncee » : les deux textes d'invite partagent exactement le meme jeton de couleur
   (`--color-text-subtle`, via `.ds-input::placeholder` et `.ds-dropdown__value--placeholder`). Il n'y
   avait donc aucun ecart de teinte — c'est la taille qui donnait l'impression, deux points de plus a
   graisse egale posant plus d'encre. La taille reglee, l'impression tombe avec elle.

   Portee a `.ds-filterbar` : les champs des formulaires gardent leur corps de lecture, ou ils sont
   seuls et ou rien ne les jouxte. */
.ds-filterbar .ds-dropdown__trigger,
.ds-filterbar .ds-input {
    min-height: var(--control-h-md);
    height: var(--control-h-md);
    padding-block: 0;
    font: var(--type-body-sm);
}

/* La case a cocher s'aligne sur les trois autres. `.ds-choice` du systeme pose
   `align-items: flex-start` et `padding-block: var(--space-2)` — juste pour une case suivie d'un
   libelle de deux lignes avec sa description, ce qui est son cas d'usage courant. Dans une rangee de
   filtres, le libelle tient sur une ligne et cet alignement en haut decale la case de quelques pixels
   vers le haut par rapport au centre des controles voisins.

   `min-height` egale a celle des controles + centrage : la rangee etant elle-meme en
   `align-items: center`, les quatre elements partagent alors exactement le meme axe. Le `margin-top`
   de 1 px de la boite, qui compensait l'alignement en haut, n'a plus lieu d'etre.

   Sa POLICE suit le meme alignement que les trois controles (voir plus haut) : elle etait en
   `--type-body`, donc son libelle etait ecrit 2 px plus grand que « Tous les statuts » juste a cote.
   Une rangee ou quatre elements portent deux tailles ne se lit pas comme une rangee. */
.ds-filterbar .ds-choice {
    align-items: center;
    min-height: var(--control-h-md);
    padding-block: 0;
    font: var(--type-body-sm);
}

/* Le `margin-top: 1px` du systeme compensait son alignement en haut. Centre, il ne compense plus
   rien et decale la case d'un pixel. */
.ds-filterbar .ds-choice__box {
    margin-top: 0;
}

/* ============================================================================================
   LA MARGE PARASITE DE TOUTES LES CASES A COCHER DU PORTAIL — et elle ne concerne pas que la
   barre de filtres.

   `css/accessibility.css` pose, sur TOUS les `label` du portail :

       label { font-weight: 500; margin-bottom: var(--space-2); display: block; color: var(--text); }

   C'est juste pour un libelle de formulaire classique — au-dessus de son champ, en bloc, avec un
   peu d'air en dessous. Mais `.ds-choice` EST un `label`, et le sien est en ligne a cote de sa case.
   Il heritait donc de 8 px de marge basse dont il n'a que faire.

   CONSEQUENCE MESUREE AU BANC, et c'est elle qu'on voyait sans la nommer : dans une rangee en
   `align-items: center`, c'est la boite de MARGE qui est centree. Une case de 40 px suivie de 8 px de
   marge occupe 48 px ; la rangee fait donc 48 px et le contenu visible se retrouve colle en haut —
   soit un decalage de 4 px vers le haut par rapport aux contro	les voisins. Exactement l'ecart
   releve trois fois de suite en recette, et exactement `-4` au banc.

   La regle est ici et non dans `.ds-filterbar` a dessein : le defaut vaut pour CHAQUE case a cocher
   du portail, pas seulement celles de la barre de filtres. Le retrait ne colle pas les cases
   empilees d'un formulaire — leur espacement vertical vient du `padding-block` que le systeme leur
   donne, pas de cette marge de reboot.

   Specificite : `.ds-choice` (0,1,0) passe devant `label` (0,0,1), et cette feuille est chargee
   apres. Les deux autres declarations de la regle sont deja neutralisees ailleurs — `display: block`
   par le `inline-flex` du systeme, `font-weight: 500` par le role typographique. Seule la marge
   passait. */
.ds-choice {
    margin-block: 0;
}

/* ============================================================================================
   LA FEUILLE DE NAVIGATION N'A PAS DE NIVEAU DE DEPLIAGE — manque du systeme, comble ici.

   `ds/css/mobile.css` livre `.ds-sheet-nav` et son entree a 52 px, et rien d'autre : pas de
   sous-niveau, pas de ligne d'information, pas de disclosure. C'est cohérent avec sa fiche, qui
   veut une feuille « rien qu'une liste d'entrées ». Mais deux contenus du portail ne sont pas des
   entrées de navigation et ne tiennent pas dans ce moule :

   · les NOTIFICATIONS, dont la hauteur dépend du nombre de messages. Posées à plat, dix
     notifications repoussaient « Outils » et « Compte » hors de l'écran ;
   · les LANGUES, quatre entrées de 52 px pour une seule information utile — celle en cours.

   Les deux passent derrière une entrée qui se déplie DANS LE FLUX. Le systeme interdit qu'un
   popover s'empile dans une feuille : il n'y en a pas ici, rien n'est positionné ni flottant, et
   c'est la même mécanique que le sous-menu de langue de l'en-tête de bureau.

   À RETIRER si le système finit par livrer un `.ds-sheet-nav__disclosure`. */

/* C'est LUI qui pousse pastille et chevron en fin de ligne, et pas un `margin-inline-start: auto`.
   Le système en pose déjà un sur `.ds-badge` dans une entrée de feuille (mobile.css) ; un second sur
   le chevron aurait partagé l'espace libre en deux et fait flotter la pastille au milieu de la
   ligne. Un libellé qui prend tout le reste ne laisse rien à partager : les marges automatiques du
   système se résolvent à 0 et les deux jetons se rangent à droite, dans l'ordre du balisage. */
.ps-sheet-nav__grow {
    flex: 1;
    min-width: 0;
}

/* La valeur courante d'une ligne d'information — la langue active. Muette : c'est un état, pas une
   action, et la ligne entière est déjà cliquable. */
.ps-sheet-nav__value {
    font: var(--type-body-sm);
    color: var(--color-text-muted);
    white-space: nowrap;
}

/* L'enveloppe du sous-niveau REPREND LA COLONNE de `.ds-sheet-nav`. Mesuré au banc : sans elle, les
   sous-entrées faisaient 136, 98, 107 et 128 px de large au lieu des 321 px de la feuille. Un
   `<button>` en `display: flex` se dimensionne sur son contenu ; il ne s'étirait que parce que
   `.ds-sheet-nav` est une colonne flex et qu'il en était enfant DIRECT. Passé d'un niveau, il perd
   cet étirement — et quatre lignes de largeurs différentes se lisent comme un défaut d'alignement. */
.ps-sheet-nav__sublist {
    display: flex;
    flex-direction: column;
}

/* Sous-niveau. Le retrait aligne le libellé sur celui des entrées de premier niveau (8 px de marge
   interne + icône + gouttière), ce qui suffit à lire la hiérarchie sans deuxième couleur de fond.
   48 px et non 52 : la cible tactile reste franchie (`--touch-comfortable`) et la différence de
   hauteur dit elle aussi le niveau. */
.ps-sheet-nav__item--sub {
    padding-inline-start: var(--space-10);
    min-height: var(--touch-comfortable);
}

/* La liste de notifications dépliée. Elle porte déjà ses propres marges internes ; il ne manquait
   qu'un filet d'air pour ne pas coller à l'entrée qui l'ouvre. */
.ps-sheet-notifs {
    padding-block: var(--space-1);
}

/* Le système ne stylise que `[aria-current="page"]`, qui vaut pour un lien de navigation. Les
   langues portent `aria-current="true"` — la valeur juste pour un choix dans un groupe, pas une
   page — et n'avaient donc AUCUN état actif : seule une coche les distinguait, invisible pour qui
   balaye la liste. Le fond de marque reste réservé à la page courante ; ici, la graisse suffit. */
.ds-sheet-nav__item[aria-current="true"] {
    font-weight: var(--weight-semibold);
    color: var(--brand-ink);
}

/* ============================================================================================
   LE REPLI MOBILE DE LA BARRE DE FILTRES EMPILE QUATRE CHAMPS EN QUATRE LIGNES — defaut du
   systeme, corrige ici.

   `ds/css/mobile.css` passe la rangee en colonne sous 600 px :

       .ds-filterbar__row { flex-direction: column; align-items: stretch }

   Une ligne par controle. La file du support en compte quatre — recherche, statuts, types, file —
   plus la case a cocher : cinq lignes de 48 px, soit ~280 px de filtres avant le premier ticket.
   Sur un ecran de 812 px, un tiers de la hauteur est consomme par des champs qu'on ne touche pas.

   Une grille `auto-fit` remplace la colonne. Elle n'introduit AUCUN nouveau point de rupture et ne
   suppose pas un nombre de controles — il varie selon la vue, `types` et `file` etant
   conditionnels : deux colonnes des 320 px de large, trois ou quatre des que la place existe. Sur
   telephone les quatre champs tiennent donc en deux lignes, et en tablette en une seule, ou ils
   etaient empiles pour rien.

   `:first-of-type` et non `.ds-filterbar__row` : la SECONDE rangee de la barre porte le compteur de
   resultats et les pastilles de criteres. Elle garde le repli en colonne du systeme, qui lui va —
   le compteur au-dessus, les pastilles en dessous.

   À RETIRER dès que le système replie sa barre en grille plutôt qu'en colonne. */
@media (max-width: 899px) {
    .ds-filterbar__row:first-of-type {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
        align-items: center;
    }

    /* La case à cocher n'est pas un champ de filtre : elle qualifie la liste entière et son libellé
       est une phrase. Elle prend sa propre ligne au lieu de se retrouver dans une demi-colonne. */
    .ds-filterbar__row:first-of-type .ds-choice {
        grid-column: 1 / -1;
    }
}

/* ============================================================================================
   LA MARGE INTERNE DE L'EN-TETE DE CARTE NE SE RESSERRE JAMAIS — manque du systeme, comble ici.

   `ds/css/layout.css` pose `.ds-card__header { padding: var(--space-5) var(--space-6) }`, soit
   20 px en haut et en bas et 24 px de chaque côté, et `ds/css/mobile.css` ne le reprend pas. Sur
   375 px, ces 48 px horizontaux sont pris sur la seule chose qui a besoin de largeur — le texte de
   l'en-tête, qui gagne une ligne à chaque fois qu'il en manque.

   MESURE AU BANC sur l'en-tête d'« Étapes du dossier », à 375 px : 158 px avec la marge du système,
   127 px avec `--space-4`. Trente et un pixels, dont l'essentiel vient d'une ligne de texte en
   moins, pas des 8 px de marge verticale.

   PAS DE `flex-wrap: wrap` ICI, et c'est un resultat de mesure, pas une omission. Il a été essayé :
   il fait passer le second bloc sous le premier, qui gagne alors la pleine largeur — et l'en-tête
   passe de 127 à 168 px. Le bloc de droite est étroit (une étiquette et une pastille, ~130 px) ;
   les ~197 px qui restent au bloc de gauche lui suffisent, et le renvoyer à la ligne coûte plus
   cher que ce que l'élargissement rapporte. La supposition de départ — « chaque mot passe à la
   ligne » — était fausse pour ce contenu.

   À RETIRER dès que le système resserre lui-même ses marges internes sous 600 px. */
@media (max-width: 599px) {
    .ds-card__header {
        padding: var(--space-4);
    }
}
