/* La base et les composants de Félix Wash.
 *
 * Ne contient aucune valeur littérale : tout vient de `brand.css`. Une couleur
 * écrite en dur ici est celle qu'on ne trouvera pas le jour où l'identité change.
 * Un `git grep -nE "#[0-9a-fA-F]{6}" static/app.css` doit rester muet.
 *
 * Les composants lisent --fw-control-h, --fw-row-h et --fw-gap sans savoir qui les
 * pose. C'est ce qui fait qu'un bouton est le MEME bouton dans les deux univers,
 * avec une hauteur différente. Un `.btn-worker` et un `.btn-admin` séparés seraient
 * deux boutons qui divergeraient au troisième changement.
 */

*, *::before, *::after { box-sizing: border-box; }

/* ── Coquilles et densités ───────────────────────────────────────────────────
 *
 * Les trois seules lignes qui séparent les deux univers. Tout le reste de ce
 * fichier lit ces variables sans savoir laquelle des deux classes est posée : un
 * bouton est le MEME bouton des deux côtés, avec une hauteur différente.
 *
 * `--fw-row-h: auto` chez l'ouvrier laisse la ligne de tableau respirer sur un
 * écran de téléphone ; les 32px de Félix font tenir un listing de 200 lignes.
 */
.shell-worker { --fw-control-h: 48px; --fw-row-h: auto; --fw-gap: var(--fw-space-4); }
.shell-admin  { --fw-control-h: 34px; --fw-row-h: 32px;  --fw-gap: var(--fw-space-3);
                font-size: var(--fw-text-sm); }

/* `width: 100%` alors que `margin: 0 auto` et `max-width` suffiraient en flux normal.
 *
 * Chez Félix, `.wrap` est un ÉLÉMENT DE GRILLE (voir `.layout`), et un élément de
 * grille dont la largeur est `auto` est normalement étiré à sa piste. Une `max-width`
 * empêche cet étirement : la spécification retombe alors sur un dimensionnement en
 * `fit-content`, c'est-à-dire la largeur MAX-CONTENT du contenu, plafonnée par la
 * `max-width`. Un tableau large impose donc sa largeur à `main`, qui déborde sa piste
 * — et c'est la PAGE qui défile latéralement, barre latérale comprise, au lieu du seul
 * tableau. `overflow-x: auto` sur `.table-scroll` n'y peut rien : la mesure max-content
 * traverse les conteneurs de défilement.
 *
 * `width: 100%` redonne à l'élément la largeur de sa piste, que `max-width` plafonne
 * ensuite. `.table-scroll` retrouve alors son rôle, et `.tabs` le sien.
 *
 * Défaut d'origine (FRONT-02), révélé par la barre d'onglets du paramétrage : le
 * listing débordait déjà, mais de peu et vers la droite, là où personne ne regardait. */
.wrap { width: 100%; box-sizing: border-box; margin: 0 auto; padding: var(--fw-space-4); }
.shell-worker .wrap { max-width: 34rem; }
.shell-admin .wrap { max-width: 72rem; padding: var(--fw-space-6) var(--fw-space-6); }

body {
  margin: 0;
  font-family: var(--fw-font-sans);
  font-size: var(--fw-text-base);
  line-height: 1.5;
  color: var(--fw-ink);
  background: var(--fw-canvas);
  -webkit-text-size-adjust: 100%;
}

/* ── La barre latérale de Félix ──────────────────────────────────────────────
 *
 * Elle remplace la barre horizontale d'avant, pour deux raisons distinctes.
 *
 * La première est de lisibilité : six entrées alignées en haut d'un écran se lisent
 * comme une frise et il faut les relire pour retrouver la sienne ; empilées, elles
 * se balaient d'un regard et l'entrée active se voit sans être cherchée.
 *
 * La seconde est structurelle : l'ancienne barre était un `flex` sans `wrap` dans
 * une page sans la moindre media query. Sur un écran étroit, elle débordait
 * silencieusement. Le repli est maintenant écrit (voir la fin du fichier).
 *
 * L'ordre reste celui du RYTHME et non du modèle de données : quotidien, mensuel,
 * rare. C'est le cœur de la refonte de FRONT — `django.contrib.admin` range par
 * table, Félix travaille par fréquence.
 */
/* `minmax(0, 1fr)` et non `1fr` : un élément de grille vaut `min-width: auto` par
 * défaut, donc une piste `1fr` ne descend jamais sous la largeur min-content de
 * son contenu. Avec un `1fr` nu, un tableau large impose sa largeur à la piste,
 * `.table-scroll` n'a plus l'occasion d'être plus étroit que sa table, et c'est
 * la PAGE qui déborde horizontalement au lieu du seul tableau. Ne pas
 * « simplifier » en `1fr`. */
.layout { display: grid; grid-template-columns: var(--fw-sidebar-w) minmax(0, 1fr); }

.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  gap: var(--fw-space-2);
  padding: var(--fw-space-5) var(--fw-space-3);
  background: var(--fw-surface);
  border-right: var(--fw-hairline) solid var(--fw-border);
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: var(--fw-space-3);
  padding: 0 var(--fw-space-3) var(--fw-space-4);
  font-size: var(--fw-text-lg);
  font-weight: var(--fw-weight-bold);
  letter-spacing: var(--fw-display-tracking);
  color: var(--fw-ink);
  text-decoration: none;
}

/* Le badge est un artwork blanc sur fond noir PLEIN — voir `brand/build_icons.py`, qui
 * explique pourquoi aucun dérivé n'est détouré. Il se pose donc en tuile, comme l'icône
 * d'application qu'il est, et non en logo posé sur la surface.
 *
 * `flex: none` parce qu'un `<img>` est compressible dans un conteneur flex : sans lui,
 * le badge s'écrase en losange dès que le nom manque de place — dans le repli
 * horizontal sous 900 px, où la barre entière tient sur une ligne. */
.sidebar-brand img {
  width: 2.25rem;
  height: 2.25rem;
  flex: none;
  border-radius: var(--fw-radius);
}

.sidebar-nav { display: flex; flex-direction: column; gap: 2px; }

.sidebar-nav a {
  display: flex;
  align-items: center;
  gap: var(--fw-space-3);
  padding: var(--fw-space-2) var(--fw-space-3);
  border-radius: var(--fw-radius);
  color: var(--fw-muted);
  font-weight: var(--fw-weight-medium);
  text-decoration: none;
}
.sidebar-nav a:hover { background: var(--fw-surface-alt); color: var(--fw-ink); }

/* L'entrée active porte TROIS marques — fond, couleur, graisse — parce qu'une seule
 * ne survit pas à toutes les conditions de lecture : le fond disparaît à l'impression
 * et pour un daltonien le bleu pâle est un gris. La graisse, elle, reste. */
.sidebar-nav a.on {
  background: var(--fw-accent-bg);
  color: var(--fw-accent-ink);
  font-weight: var(--fw-weight-semibold);
}

/* Les icônes sont des SVG au trait posés en ligne, pas des emoji : à cette taille un
 * emoji est rendu par la police du système, donc différent sur chaque poste, et il
 * apporte une couleur qu'on n'a pas choisie au milieu d'une palette qu'on a choisie. */
.sidebar-nav svg { width: 18px; height: 18px; flex: none; stroke-width: 1.75; }

/* `margin-top: auto` colle le bloc d'identité en bas sans qu'on ait à connaître la
 * hauteur de ce qui précède — donc sans le recalculer quand une entrée s'ajoute. */
.sidebar-foot {
  margin-top: auto;
  padding-top: var(--fw-space-4);
  border-top: var(--fw-hairline) solid var(--fw-border);
}
.sidebar-user {
  padding: 0 var(--fw-space-3) var(--fw-space-3);
  line-height: 1.3;
}
.sidebar-user strong { display: block; font-weight: var(--fw-weight-semibold); }
.sidebar-user span { font-size: var(--fw-text-xs); color: var(--fw-subtle); }
.sidebar-foot button { width: 100%; }

/* La barre du haut de l'ouvrier : le chemin de retour et la déconnexion. Lancée
 * depuis l'écran d'accueil, la PWA n'a pas de barre d'adresse — donc pas de
 * bouton retour du navigateur. Chaque écran porte le sien. */
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--fw-space-3); margin-bottom: var(--fw-space-5);
  min-height: var(--fw-control-h);
}
/* Les actions secondaires de la barre ouvrier — « Aide », « Déconnexion ». Elles
 * gardent une cible tactile pleine hauteur alors qu'elles ne se voient presque pas :
 * discret à l'œil ne veut pas dire petit sous le pouce. */
.topbar-actions { display: flex; align-items: center; gap: var(--fw-space-1); }
.topbar-link {
  display: inline-flex; align-items: center;
  min-height: var(--fw-control-h); padding: 0 var(--fw-space-2);
  color: var(--fw-muted); font-size: var(--fw-text-sm); text-decoration: none;
}
.topbar-link:hover { color: var(--fw-ink); }

/* ── Éléments ────────────────────────────────────────────────────────────────
 *
 * L'échelle de titres est le second levier de la refonte après le fond froid.
 * Avant, `h1` valait 1.375rem en graisse 500 : à peine plus qu'un intertitre, et
 * la page n'avait pas de point d'entrée pour l'œil.
 */
h1, h2, h3 {
  font-weight: var(--fw-weight-semibold);
  line-height: 1.25;
  margin: 0 0 var(--fw-space-3);
  color: var(--fw-ink);
}
h1 {
  font-size: var(--fw-text-2xl);
  font-weight: var(--fw-weight-bold);
  letter-spacing: var(--fw-display-tracking);
}
.shell-admin h1 { font-size: var(--fw-text-3xl); }
h2 { font-size: var(--fw-text-lg); }
h3 { font-size: var(--fw-text-base); }
p { margin: 0 0 var(--fw-space-3); }
ul, ol { margin: 0 0 var(--fw-space-3); padding-left: var(--fw-space-5); }

/* Le soulignement n'est pas décoratif ici : l'accent est un bleu marine profond,
 * assez sombre pour se confondre avec du texte courant. C'est le trait qui dit
 * « lien », pas la couleur. */
a { color: var(--fw-accent); text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: var(--fw-accent-hover); }

/* Le focus visible n'est pas décoratif : c'est ce qui permet de remplir le
 * formulaire au clavier. Jamais de `outline: none` sans remplacement. */
:focus-visible {
  outline: 2px solid var(--fw-accent);
  outline-offset: 1px;
  box-shadow: 0 0 0 4px var(--fw-focus-ring);
}

/* ── L'en-tête d'écran ───────────────────────────────────────────────────────
 *
 * Le titre, sa phrase de situation, et l'action principale sur la même ligne. La
 * phrase n'est pas un ornement : c'est elle qui dit à Félix CE QU'IL REGARDE —
 * quelle période, quel périmètre — au moment où il ouvre l'écran, plutôt qu'en
 * relisant les filtres.
 */
.page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--fw-space-4); flex-wrap: wrap;
  margin-bottom: var(--fw-space-5);
}
.page-head h1 { margin: 0; }
.page-head .meta { margin: var(--fw-space-1) 0 0; }
.page-head-actions { display: flex; gap: var(--fw-space-2); flex-wrap: wrap; }

/* ── Contrôles ───────────────────────────────────────────────────────────────
 * Hauteur pilotée par la densité, apparence commune.
 */
input, select, textarea, button, .btn {
  font: inherit;
  border-radius: var(--fw-radius);
}
input:not([type="checkbox"]):not([type="radio"]), select, .btn, button {
  min-height: var(--fw-control-h);
}
input:not([type="checkbox"]):not([type="radio"]), select, textarea {
  width: 100%;
  padding: 0 var(--fw-space-3);
  border: var(--fw-hairline) solid var(--fw-border-strong);
  background: var(--fw-surface);
  color: var(--fw-ink);
}
input:not([type="checkbox"]):not([type="radio"]):hover, select:hover, textarea:hover {
  border-color: var(--fw-muted);
}
/* Les cases à cocher sont exclues des deux règles ci-dessus — un carré de 48 px
 * serait absurde, et une largeur de 100 % encore plus — mais **rien d'autre ne les
 * dimensionnait** : elles retombaient au défaut du navigateur, ~13 px, sous le pouce
 * d'un ouvrier debout dans un garage. Pico leur donnait 1.25em ; en le retirant
 * (FRONT-01) on a emporté ça aussi, sans que rien ne le signale.
 *
 * La taille se **dérive** de la densité au lieu d'ajouter une quatrième variable de
 * coquille : 24 px côté ouvrier, 17 px côté gestion. C'est ce qui permet à la règle
 * de ne pas savoir dans quelle densité elle est rendue (FRONT-02). */
input[type="checkbox"], input[type="radio"] {
  width: calc(var(--fw-control-h) / 2);
  height: calc(var(--fw-control-h) / 2);
  margin: 0;
  accent-color: var(--fw-accent);
}

textarea { padding: var(--fw-space-2) var(--fw-space-3); min-height: 4rem; }

/* Le libellé gagne en poids et perd en gris : avant, il était plus pâle que la
 * valeur qu'il annonce, ce qui inversait la hiérarchie de lecture d'un formulaire.
 *
 * Essayé puis retiré : la micro-étiquette en majuscules du tableau. Sur les huit
 * champs de l'écran d'encodage, elle transformait le formulaire en suite de cris —
 * et un ouvrier debout lit les libellés à chaque véhicule, pas une fois. Les
 * majuscules restent réservées aux en-têtes de tableau, qu'on lit UNE fois avant de
 * parcourir vingt lignes en dessous. */
label {
  display: block;
  font-size: var(--fw-text-sm);
  font-weight: var(--fw-weight-semibold);
  color: var(--fw-ink);
  margin-bottom: var(--fw-space-1);
}

/* Les trois classes que `django.contrib.admin` fournissait aux fragments de
 * formulaire du listing, et qu'il ne fournit plus depuis sa sortie de l'admin. */
.form-row { margin-bottom: var(--fw-space-4); }
.help { font-size: var(--fw-text-xs); color: var(--fw-muted); margin-top: var(--fw-space-1); }
/* `form.as_p` rend l'aide d'un champ dans un `<span class="helptext">`. Un seul écran
 * ouvrier en affiche une — celle de la demi-heure de pause, qui empêche une double
 * déduction — et elle n'était pas stylée du tout : elle sortait au corps du texte
 * courant, collée à l'`<input>`, et se lisait comme une consigne de saisie. */
.helptext {
  display: block;
  font-size: var(--fw-text-xs);
  color: var(--fw-muted);
  margin-top: var(--fw-space-1);
}
.errorlist {
  list-style: none; padding: 0; margin: var(--fw-space-1) 0 0;
  color: var(--fw-danger); font-size: var(--fw-text-sm);
}

fieldset {
  border: var(--fw-hairline) solid var(--fw-border);
  border-radius: var(--fw-radius-lg);
  background: var(--fw-surface);
  padding: var(--fw-gap);
  margin: 0 0 var(--fw-space-4);
}
fieldset > h2:first-child { margin-top: 0; }

/* Le repli des filtres facultatifs du listing (F3-02). Trois `<select multiple>`
 * dépliés poussaient le listing sous la ligne de flottaison alors qu'ils sont vides
 * neuf fois sur dix. Un `<details>` natif plutôt qu'un panneau en JS : fermé, ses
 * champs restent dans le formulaire et partent quand même en GET.
 *
 * Pas de `display: flex` sur le `<summary>` : cela retire le triangle natif de Chrome,
 * et il faudrait alors en dessiner un — le seul repère qui dit que le bloc s'ouvre. */
details.filters {
  border-top: var(--fw-hairline) solid var(--fw-border);
  margin-top: var(--fw-space-3);
  padding-top: var(--fw-space-3);
}
details.filters > summary {
  cursor: pointer;
  padding: var(--fw-space-2) 0;
  font-size: var(--fw-text-sm);
  color: var(--fw-muted);
}
details.filters > summary:hover { color: var(--fw-ink); }
details.filters[open] > summary { color: var(--fw-ink); }

/* Une colonne par filtre tant que la largeur le permet, empilées ensuite : ouverts,
 * les trois sélecteurs tiennent sur une bande et non sur trois écrans. */
.filters-grid {
  display: grid;
  gap: var(--fw-gap);
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  margin-top: var(--fw-space-3);
}
.filters-grid .form-row { margin-bottom: 0; }

/* Les options de ces trois filtres sont des cases à cocher (`CheckboxSelectMultiple`)
 * et non un `<select multiple>` : celui-ci exigeait un CTRL+clic que personne ne
 * devine, et un clic simple y REMPLACE la sélection au lieu de l'étendre — Félix
 * sortait une facture amputée d'une marque sans que l'écran le dise.
 *
 * Le sélecteur distingue les deux niveaux de `<label>` par la parenté : `> label` est
 * le titre du champ (« Marques »), qui reste discret ; ceux imbriqués sont les options,
 * qui doivent se lire et se cliquer. Django ne pose pas de classe sur ces libellés, il
 * n'y a donc rien d'autre à quoi s'accrocher. */
.filters-grid .form-row div label {
  display: flex;
  align-items: center;
  gap: var(--fw-space-2);
  margin: 0;
  padding: var(--fw-space-1) 0;
  font-size: var(--fw-text-sm);
  font-weight: var(--fw-weight-medium);
  color: var(--fw-ink);
  cursor: pointer;
}
.filters-grid .form-row div label:hover { color: var(--fw-accent); }
/* Sans ça, le conteneur flex écrase la case sur une option au libellé long. */
.filters-grid input[type="checkbox"] { flex: none; }

/* ── Boutons ─────────────────────────────────────────────────────────────────── */
.btn, button {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--fw-space-2);
  padding: 0 var(--fw-space-4);
  border: var(--fw-hairline) solid var(--fw-border-strong);
  border-radius: var(--fw-radius);
  background: var(--fw-surface);
  color: var(--fw-ink);
  font-weight: var(--fw-weight-medium);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
}
.btn:hover, button:hover { background: var(--fw-surface-alt); border-color: var(--fw-muted); }

/* Sur mobile il n'y a pas de survol : sans retour au `:active`, l'ouvrier qui ne
 * voit rien bouger appuie une seconde fois. */
.btn:active, button:active { transform: translateY(1px); }

/* Un bouton d'accent par écran, plus au besoin le bouton d'envoi du bon — c'est la
 * seule cohabitation prévue, et les deux teintes disent deux choses différentes :
 * bleu = encoder, orange = faire sortir un document de l'application. Tout le reste
 * est neutre. */
.btn-primary {
  background: var(--fw-accent); border-color: var(--fw-accent);
  color: var(--fw-on-accent);
  font-weight: var(--fw-weight-semibold);
}
.btn-primary:hover { background: var(--fw-accent-hover); border-color: var(--fw-accent-hover); color: var(--fw-on-accent); }

/* L'envoi du bon à la concession, et rien d'autre — voir le commentaire de
 * `--fw-send` dans `brand.css` pour le motif de cette seconde teinte. */
.btn-send {
  background: var(--fw-send); border-color: var(--fw-send);
  color: var(--fw-on-accent);
  font-weight: var(--fw-weight-semibold);
}
.btn-send:hover { background: var(--fw-send-hover); border-color: var(--fw-send-hover); color: var(--fw-on-accent); }

/* Un bouton désactivé doit se lire comme désactivé AVANT d'être touché : c'est ce qui
 * évite l'appui, pas ce qui le refuse. `not-allowed` ne se voit pas sur un téléphone,
 * d'où le contraste baissé, qui lui se voit. Et le `:active` est neutralisé — un
 * bouton qui s'enfonce sans rien faire se lit comme une application qui a planté. */
.btn:disabled, button:disabled {
  background: var(--fw-surface-alt);
  border-color: var(--fw-border);
  color: var(--fw-muted);
  cursor: not-allowed;
}
.btn:disabled:hover, button:disabled:hover { background: var(--fw-surface-alt); border-color: var(--fw-border); }
.btn:disabled:active, button:disabled:active { transform: none; }

.btn-block { width: 100%; }
.btn-danger { color: var(--fw-danger); border-color: var(--fw-border-strong); }
.btn-danger:hover { border-color: var(--fw-danger); }
/* Un bouton discret pour les actions de ligne — « Modifier », « Ouvrir ». Il ne
 * pèse rien tant qu'on ne le survole pas, ce qui laisse la ligne se lire d'abord. */
.btn-quiet { border-color: transparent; background: transparent; color: var(--fw-muted); }
.btn-quiet:hover { border-color: var(--fw-border-strong); color: var(--fw-ink); }

/* ── Cartes ──────────────────────────────────────────────────────────────────
 * Le même objet aux deux densités. L'ombre ne crée pas la séparation — c'est
 * `--fw-canvas` qui la crée — elle ne fait que la confirmer.
 */
.card {
  background: var(--fw-surface);
  border: var(--fw-hairline) solid var(--fw-border);
  border-radius: var(--fw-radius-lg);
  box-shadow: var(--fw-shadow-sm);
  padding: var(--fw-gap);
  margin-bottom: var(--fw-space-4);
}
.card > :last-child { margin-bottom: 0; }
.card-title {
  font-weight: var(--fw-weight-semibold);
  font-size: var(--fw-text-base);
  margin: 0 0 var(--fw-space-1);
}
/* Une carte à en-tête : le titre à gauche, le compteur ou l'action à droite. */
.card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--fw-space-3); margin-bottom: var(--fw-space-3);
}
.card-head > * { margin: 0; }

/* Les actions d'une carte, côte à côte et à parts égales.
 *
 * `.row` ne convient pas ici : ses enfants gardent leur largeur naturelle, ce qui est
 * bon dans un formulaire — « Enregistrer » n'a pas à faire la taille d'« Annuler » —
 * et mauvais sur une carte de téléphone, où deux cibles tactiles de largeurs
 * différentes se touchent mal. Seuls les boutons s'étirent : un jeton posé là
 * (« Bon envoyé ») garde sa taille de jeton. */
.card-actions { display: flex; align-items: center; gap: var(--fw-space-2); flex-wrap: wrap; }
.card-actions > .btn, .card-actions > button { flex: 1 1 auto; }

.meta { color: var(--fw-muted); font-size: var(--fw-text-sm); margin: 0 0 var(--fw-space-2); }
.stack > * + * { margin-top: var(--fw-gap); }
.row { display: flex; gap: var(--fw-space-2); flex-wrap: wrap; align-items: center; }

/* ── Le rail de la journée (ouvrier) ─────────────────────────────────────────
 *
 * Un `Passage` EST une plage horaire — un ouvrier, un garage, une date, une heure de
 * début et une de fin. L'écran les listait comme des cartes empilées, ce qui perdait
 * la seule chose que la donnée dit d'elle-même : leur ordre dans la journée.
 *
 * Le rail ne décore donc pas, il encode. Et il encode surtout le cas qui compte :
 * **un passage ouvert n'a pas d'embout**. Le trait part vers le bas en pointillé et
 * ne se referme pas. C'est la traduction visuelle d'une règle qui, jusqu'ici, ne se
 * lisait nulle part — un passage sans heure de fin vaut zéro minute, et il est le
 * premier responsable des heures sous-estimées en fin de mois.
 *
 * Vertical, donc natif sur un téléphone tenu à une main.
 */
.rail { list-style: none; margin: 0; padding: 0; }
/* Le rail suit le bloc de boutons, et `margin: 0` ci-dessus efface la marge que tout
 * `ol` porte par défaut : les deux se touchaient. Or ils ne disent pas la même chose —
 * au-dessus « ce que je viens faire », en dessous « ce que j'ai déjà fait ». Le blanc
 * est ce qui sépare les deux intentions.
 *
 * Sur l'adjacence plutôt que sur `.rail` : le rail sert aussi dans la galerie de
 * recette, qui n'a pas de bloc d'actions à en détacher. Et sur le conteneur plutôt que
 * sur `.rail-item:first-child` : les `::before` / `::after` de l'item sont posés en
 * absolu par rapport à lui, un `padding-top` y décalerait le trait et la pastille sans
 * bouger l'heure. */
.stack + .rail { margin-top: var(--fw-space-6); }

/* ── La validation du client sur l'écran du bon (D15) ────────────────────────
 *
 * Le seul bloc de l'application lu par quelqu'un qui n'a pas de compte : l'ouvrier
 * tend son téléphone au responsable du garage. Deux conséquences de forme.
 *
 * La case garde sa taille native au lieu d'être étirée par la densité ouvrier
 * (`--fw-control-h: 48px`) : une case à cocher de 48 px de haut ne ressemble plus à
 * une case. C'est la zone cliquable qui doit être grande, pas le carré — d'où le
 * `label` en `flex` qui l'englobe avec son texte.
 *
 * `align-items: flex-start` parce que la mention tient sur trois lignes sur un
 * téléphone : centrée, la case flotterait au milieu du paragraphe. */
.consent-check {
  display: flex;
  align-items: flex-start;
  gap: var(--fw-space-3);
  cursor: pointer;
}
.consent-check input[type="checkbox"] {
  width: 1.25rem;
  height: 1.25rem;
  min-height: 0;
  margin-top: 2px;
  flex: none;
}
.rail-item {
  position: relative;
  padding-left: var(--fw-space-6);
  padding-bottom: var(--fw-space-2);
}
/* Le trait. Il part du centre de la pastille et descend jusqu'en bas de l'élément ;
 * le dernier le coupe, sinon la journée aurait l'air de continuer. */
.rail-item::before {
  content: "";
  position: absolute;
  left: 5px; top: var(--fw-space-4); bottom: 0;
  width: 2px;
  background: var(--fw-border);
}
.rail-item:last-child::before { display: none; }
/* La pastille. Pleine = le passage est clôturé, on sait ce qu'il a duré. */
.rail-item::after {
  content: "";
  position: absolute;
  left: 0; top: var(--fw-space-2);
  width: 12px; height: 12px;
  border-radius: var(--fw-radius-pill);
  border: 2px solid var(--fw-accent);
  background: var(--fw-accent);
}
/* Creuse et ambre = le passage est encore ouvert. La couleur d'état dit « regarde »,
 * la pastille vide dit « il manque quelque chose ». Les deux disent la même chose,
 * de deux façons, parce que le bleu et l'ambre se ressemblent en niveaux de gris. */
.rail-item-open::after {
  border-color: var(--fw-warning);
  background: var(--fw-surface);
}
.rail-item-open::before {
  background: linear-gradient(var(--fw-border) 60%, transparent 0);
  background-size: 2px 8px;
}
.rail-time {
  display: block;
  font-family: var(--fw-font-mono);
  font-size: var(--fw-text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--fw-muted);
  margin: 0 0 var(--fw-space-1);
  padding-top: 1px;
}
.rail-item-open .rail-time { color: var(--fw-warning); }

/* ── Tableaux ────────────────────────────────────────────────────────────────
 * La densité de Félix. --fw-row-h vaut `auto` chez l'ouvrier, ce qui laisse la
 * ligne respirer sur un écran de téléphone.
 */
table { width: 100%; border-collapse: collapse; font-size: var(--fw-text-sm); }
th, td {
  height: var(--fw-row-h);
  padding: var(--fw-space-2) var(--fw-space-3);
  border-bottom: var(--fw-hairline) solid var(--fw-border);
  text-align: left;
}
/* ── La micro-étiquette ───────────────────────────────────────────────────────
 *
 * Un seul endroit pour les trois qui la portent : en-tête de tableau, libellé de
 * tuile, étiquette de filtre. Les cinq propriétés étaient recopiées telles quelles
 * dans les trois règles — `brand.css` prévient déjà que ces valeurs « vont toujours
 * ensemble », mais nommer les variables n'empêche pas de les réunir trois fois. La
 * quatrième copie, dans `admin-theme.css`, reste : ce fichier ne peut pas lire les
 * classes de celui-ci.
 *
 * Ce qui est propre à chacun reste dans sa propre règle, plus bas.
 *
 * L'en-tête devient donc une micro-étiquette : c'est le détail qui fait le plus pour
 * qu'un tableau se lise comme un tableau tenu et non comme une grille brute. Il cesse
 * de concurrencer les données, qui sont ce qu'on vient lire. */
th,
.stat-label,
.filter-field > span {
  font-size: var(--fw-label-size);
  letter-spacing: var(--fw-label-tracking);
  text-transform: uppercase;
  font-weight: var(--fw-weight-semibold);
  color: var(--fw-subtle);
}
th {
  white-space: nowrap;
  border-bottom-color: var(--fw-border-strong);
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: var(--fw-surface-alt); }

/* Le pied de tableau porte des `<th>` — c'est le balisage correct pour un total —
 * mais ce ne sont pas des étiquettes : c'est LE chiffre que Félix vient lire. Sans
 * cette reprise, la micro-étiquette ci-dessus lui appliquait des capitales et un
 * corps de 11 px, et le total du mois devenait la plus petite chose du tableau. */
tfoot th {
  font-size: var(--fw-text-sm);
  letter-spacing: normal;
  text-transform: none;
  font-weight: var(--fw-weight-bold);
  color: var(--fw-ink);
  border-top: 2px solid var(--fw-border-strong);
  border-bottom: none;
}
tfoot th.num { font-family: var(--fw-font-mono); font-variant-numeric: tabular-nums; }

/* Les colonnes de chiffres passent en chasse fixe : c'est ce qui aligne les
 * décimales d'une colonne de montants, et ce qui fait qu'un châssis mal tapé se
 * repère sur la ligne du dessus. */
td.num, th.num { text-align: right; }
td.num { font-family: var(--fw-font-mono); font-variant-numeric: tabular-nums; }
.mono { font-family: var(--fw-font-mono); font-variant-numeric: tabular-nums; }

/* L'ABSENCE de montant, et non un montant à zéro.
 *
 * C'est la règle la plus coûteuse du projet : quand aucun tarif ne correspond, le
 * montant vaut `None` et JAMAIS `0` — un zéro se totalise, une absence se corrige.
 * Une ligne à tarifer ressemblait pourtant à une ligne comme une autre, et c'est
 * l'export qui finissait par la découvrir. Elle a maintenant une forme à elle : un
 * tiret cadratin à la place du chiffre, en ambre. Le vide se lit comme du vide. */
.amount-missing {
  font-family: var(--fw-font-mono);
  color: var(--fw-warning);
}

/* Un tableau large ne pousse pas la page : il défile dans son propre cadre. Sans ça
 * le listing de Félix, ouvert sur une tablette, décalait toute la mise en page vers
 * la droite — y compris la barre latérale, qui n'y est pour rien. */
.table-scroll { overflow-x: auto; margin: 0 calc(-1 * var(--fw-gap)); padding: 0 var(--fw-gap); }

/* ── Bandeaux d'état ─────────────────────────────────────────────────────────── */
.message {
  padding: var(--fw-space-3) var(--fw-space-4);
  border-radius: var(--fw-radius);
  border: var(--fw-hairline) solid transparent;
  border-left-width: 3px;
  margin-bottom: var(--fw-space-4);
}
.message > :last-child { margin-bottom: 0; }
.message-success { background: var(--fw-success-bg); border-color: var(--fw-success); color: var(--fw-success); }
/* L'alerte de doublon (F4-03) : ni une erreur — l'ouvrier a le droit de passer
 * outre, et un relavage est parfois légitime — ni un succès. */
.message-warning { background: var(--fw-warning-bg); border-color: var(--fw-warning); color: var(--fw-warning); }
.message-error   { background: var(--fw-danger-bg);  border-color: var(--fw-danger);  color: var(--fw-danger); }

.badge {
  display: inline-block; padding: 2px var(--fw-space-2);
  border-radius: var(--fw-radius-pill);
  font-size: var(--fw-text-xs);
  font-weight: var(--fw-weight-semibold);
  line-height: 1.5;
  background: var(--fw-accent-bg); color: var(--fw-accent-ink);
}
.badge-warning { background: var(--fw-warning-bg); color: var(--fw-warning); }
.badge-danger  { background: var(--fw-danger-bg);  color: var(--fw-danger); }
/* Les jetons existaient depuis `brand.css` ; seule la variante manquait. Elle sert
   au statut « Réglé » d'une demande de support (F8). */
.badge-success { background: var(--fw-success-bg); color: var(--fw-success); }
/* Le jeton neutre : un compteur qui ne veut rien dire de particulier. Sans lui, un
   « 0 » s'affichait en bleu de marque et se lisait comme une alerte discrète. */
.badge-quiet { background: var(--fw-surface-alt); color: var(--fw-muted); }

/* Les jetons de type de véhicule. La classe se compose dans le gabarit à partir de
 * l'enum — `chip-{{ row.vehicle_type|lower }}` donne `chip-vn`, `chip-vo`, `chip-vr`,
 * `chip-demo`, `chip-location` — donc un SIXIÈME type ajouté au modèle sortirait sans
 * couleur plutôt qu'avec la mauvaise. C'est la dégradation qu'on veut : silencieuse et
 * neutre. Elle a servi : `Location` est arrivé le 24/08, et l'écran est resté lisible
 * le temps que cette ligne soit écrite. */
.chip-vn       { background: var(--fw-type-vn-bg);       color: var(--fw-type-vn-ink); }
.chip-vo       { background: var(--fw-type-vo-bg);       color: var(--fw-type-vo-ink); }
.chip-vr       { background: var(--fw-type-vr-bg);       color: var(--fw-type-vr-ink); }
.chip-demo     { background: var(--fw-type-demo-bg);     color: var(--fw-type-demo-ink); }
.chip-location { background: var(--fw-type-location-bg); color: var(--fw-type-location-ink); }

/* ── Le bandeau de chiffres du jour ──────────────────────────────────────────
 *
 * Trois ou quatre nombres alignés, en gros, avec leur libellé en dessous. C'est la
 * seule chose de l'application qui affiche un chiffre POUR LUI-MÊME plutôt qu'au bout
 * d'une ligne de tableau — d'où le corps 3xl, qui serait déplacé partout ailleurs.
 */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--fw-space-3);
  margin-bottom: var(--fw-space-4);
}
.stat {
  background: var(--fw-surface);
  border: var(--fw-hairline) solid var(--fw-border);
  border-radius: var(--fw-radius-lg);
  box-shadow: var(--fw-shadow-sm);
  padding: var(--fw-space-4);
}
.stat-value {
  display: block;
  font-family: var(--fw-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--fw-text-2xl);
  font-weight: var(--fw-weight-bold);
  letter-spacing: var(--fw-display-tracking);
  line-height: 1.1;
}
/* La micro-étiquette est posée plus haut, avec `th`. Ne reste ici que ce qui lui est
 * propre : elle passe sous le chiffre, en bloc. */
.stat-label {
  display: block;
  margin-top: var(--fw-space-1);
}
/* Un chiffre qui appelle une action se teinte, les autres restent à l'encre. Un
 * bandeau où tout est coloré ne hiérarchise plus rien. */
.stat-attention .stat-value { color: var(--fw-warning); }

/* La carte qui demande quelque chose porte un filet de couleur sur sa tranche. C'est
 * ce qui permet de balayer le tableau de bord sans lire : ce qui est calme est calme.
 * Réglé par le gabarit selon le compteur, pas en dur — une carte à zéro n'alerte pas. */
.card-attention { border-left: 3px solid var(--fw-warning); }
.card-done { border-left: 3px solid var(--fw-success); }

/* ── Le guide ────────────────────────────────────────────────────────────────
 *
 * Le seul écran du projet qui soit une page de LECTURE. D'où une mesure limitée à
 * ~62 caractères et un interlignage plus large : les règles du reste de l'app sont
 * réglées pour des formulaires et des tableaux, pas pour de la prose.
 */
.guide { max-width: 44rem; }
.guide p, .guide li { line-height: 1.65; }
.guide-toc { columns: 2; column-gap: var(--fw-space-6); list-style: none; padding: 0; margin: 0; }
.guide-toc li { break-inside: avoid; margin-bottom: var(--fw-space-2); }

/* Le numéro de section. Numéroter n'est légitime que si l'ordre porte une
 * information — ici oui : on se connecte avant d'ouvrir un passage, et on ouvre un
 * passage avant d'encoder un véhicule. C'est une marche à suivre, pas une liste. */
.guide-section { margin-top: var(--fw-space-8); }
.guide-section > h2 {
  display: flex; align-items: baseline; gap: var(--fw-space-3);
  padding-bottom: var(--fw-space-2);
  border-bottom: 2px solid var(--fw-accent);
  font-size: var(--fw-text-xl);
}
.guide-num {
  font-family: var(--fw-font-mono);
  font-size: var(--fw-text-sm);
  color: var(--fw-accent);
  flex: none;
}
/* L'encadré « à retenir » — une règle qu'on ne peut pas deviner depuis l'écran. */
.guide-note {
  border-left: 3px solid var(--fw-accent);
  background: var(--fw-accent-bg);
  border-radius: 0 var(--fw-radius) var(--fw-radius) 0;
  padding: var(--fw-space-3) var(--fw-space-4);
  margin: var(--fw-space-4) 0;
}
.guide-note > :last-child { margin-bottom: 0; }

/* ── Onglets ─────────────────────────────────────────────────────────────────
 *
 * La navigation SECONDAIRE — celle qui reste à l'intérieur d'un écran. La barre
 * latérale dit où on est dans l'application, les onglets disent où on est dans
 * l'écran. C'est le seul composant que la refonte n'avait pas eu à produire : jusqu'au
 * paramétrage en onglets (10/08), rien n'avait plus d'un niveau.
 *
 * Ce sont des `<a href="?onglet=…">`, pas des boutons pilotés en JavaScript : chaque
 * onglet est une URL, donc il se recharge, se met en favori et s'ouvre dans un
 * nouvel onglet du navigateur. Le composant marche sans une ligne de script.
 *
 * L'onglet actif porte TROIS marques, pour la même raison que `.sidebar-nav a.on` :
 * le trait survit à l'impression, la graisse survit au daltonisme, la couleur porte
 * quand les deux autres passent inaperçues.
 */
.tabs {
  display: flex;
  gap: var(--fw-space-1);
  margin-bottom: var(--fw-space-5);
  border-bottom: var(--fw-hairline) solid var(--fw-border);
  /* Sept onglets ne tiennent pas sur une tablette en portrait. Ils défilent
   * horizontalement plutôt que de se replier : un menu déroulant demanderait le
   * premier JavaScript du projet, et c'est déjà le choix fait pour la barre
   * latérale sous 900 px. */
  overflow-x: auto;
}

.tabs a {
  display: flex;
  align-items: center;
  gap: var(--fw-space-2);
  flex: none;
  padding: var(--fw-space-3) var(--fw-space-4);
  /* Le trait de l'onglet actif se pose SUR la bordure du conteneur. Transparent au
   * repos plutôt qu'absent : sans lui, l'onglet se décalerait de deux pixels vers le
   * haut au survol de son voisin actif. */
  border-bottom: 2px solid transparent;
  margin-bottom: calc(-1 * var(--fw-hairline));
  color: var(--fw-muted);
  font-weight: var(--fw-weight-medium);
  white-space: nowrap;
  text-decoration: none;
}
.tabs a:hover { color: var(--fw-ink); border-bottom-color: var(--fw-border-strong); }

.tabs a.on {
  color: var(--fw-accent-ink);
  font-weight: var(--fw-weight-semibold);
  border-bottom-color: var(--fw-accent);
}

/* ── Barre de filtres ────────────────────────────────────────────────────────
 *
 * Distincte du `details.filters` du listing mensuel, et pas par goût : là-bas les
 * filtres sont un formulaire d'interrogation qu'on déplie, ici ils naviguent dans une
 * liste déjà affichée. Repliés, ils ne serviraient pas — on les veut sous la main.
 *
 * Les listes SE PARTAGENT la ligne (`flex: 1 1 11rem`) au lieu de prendre chacune la
 * largeur de sa plus longue option. Laissées à leur largeur naturelle, « Tous les
 * garages » occupait une rangée à lui seul et les quatre menus des tarifs se
 * répartissaient sur trois lignes inégales — une barre de filtres qu'il faut relire
 * pour retrouver son menu ne fait pas gagner le temps qu'elle promet.
 *
 * La base de 11rem est ce qui les fait tenir à quatre sur une ligne dans les 72rem de
 * la coquille, et se replier proprement à deux quand la fenêtre rétrécit.
 */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--fw-space-3);
  margin-bottom: var(--fw-space-4);
}
.filter-field {
  display: flex;
  flex-direction: column;
  gap: var(--fw-space-1);
  flex: 1 1 11rem;
}
.filter-field select { width: 100%; }
/* L'étiquette est une micro-capitale, comme un en-tête de tableau : elle nomme la
 * liste sans venir concurrencer la valeur choisie, qui est ce qu'on relit. Elle est
 * donc rendue par la règle groupée, plus haut — c'est littéralement la même chose. */
.filter-bar .meta { margin: 0; }

/* La composition d'un groupe de garages : une case par garage, en colonnes.
 *
 * Django rend `CheckboxSelectMultiple` dans un `<div>` porteur de l'id du champ — d'où
 * ce sélecteur d'identifiant, le seul du fichier. Le sous-classer pour lui poser une
 * classe coûterait un gabarit de widget pour un unique champ. */
#id_garages {
  columns: 2;
  padding: var(--fw-space-2) 0;
}
#id_garages div { break-inside: avoid; }
#id_garages label { font-weight: var(--fw-weight-normal); display: inline; margin-left: var(--fw-space-1); }
#id_garages input { width: auto; }

/* ── Pagination ──────────────────────────────────────────────────────────────
 *
 * Seule la grille tarifaire en a besoin : l'import déplie chaque ligne du CSV en un
 * tarif par garage du groupe. Les six autres référentiels tiennent sous le seuil, et
 * le bloc ne se rend alors pas du tout.
 */
.pagination {
  display: flex;
  align-items: center;
  gap: var(--fw-space-3);
  margin-top: var(--fw-space-4);
}
.pagination .meta { margin: 0; }

/* ── Le hub de réglages ──────────────────────────────────────────────────────
 *
 * Ce qui reste des cartes de FRONT-10 : la porte de secours vers `/admin/`. Les six
 * cartes de référentiels sont devenues les onglets ci-dessus, mais la correction
 * d'une journée close et le changement de mot de passe y vivent encore, et il n'y a
 * aucune raison de les banaliser en les rangeant avec le paramétrage — c'est le seul
 * endroit d'où un montant déjà arrêté peut être réécrit.
 */
.settings-section + .settings-section { margin-top: var(--fw-space-7); }
.settings-section > h2 { margin-bottom: var(--fw-space-4); }

.settings-grid {
  display: grid;
  gap: var(--fw-space-4);
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}
.settings-card {
  display: flex; flex-direction: column;
  background: var(--fw-surface);
  border: var(--fw-hairline) solid var(--fw-border);
  border-radius: var(--fw-radius-lg);
  box-shadow: var(--fw-shadow-sm);
  padding: var(--fw-gap);
  color: inherit;
  text-decoration: none;
}
.settings-card:hover {
  border-color: var(--fw-accent);
  box-shadow: var(--fw-shadow-md);
  color: inherit;
}
.settings-card strong { font-size: var(--fw-text-base); font-weight: var(--fw-weight-semibold); }
.settings-card .meta { margin: var(--fw-space-1) 0 0; }

.inline-form { display: inline; margin: 0; }
.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* ── Repli ───────────────────────────────────────────────────────────────────
 *
 * Les seules media queries du projet. Elles n'existaient pas : la coquille ouvrier
 * tenait sur un téléphone par sa largeur fixe (34rem), et celle de Félix ne tenait
 * pas du tout — un `flex` sans `wrap` dans une page de 78rem.
 *
 * Le seuil est à 900px parce que c'est la largeur en dessous de laquelle la barre
 * latérale coûte plus qu'elle ne rapporte : le listing, qui est l'écran le plus
 * large, passe sous la lisibilité avant que la barre ne gêne vraiment.
 */
@media (max-width: 900px) {
  /* `minmax(0, …)` pour la même raison qu'au-dessus : une colonne unique déborde
   * tout autant qu'une piste de droite. */
  .layout { grid-template-columns: minmax(0, 1fr); }

  /* La barre latérale redevient une barre du haut. Elle défile horizontalement
   * plutôt que de se replier dans un menu : six entrées tiennent en deux gestes de
   * pouce, et un menu déroulant demanderait le premier JavaScript du projet. */
  .sidebar {
    position: static;
    height: auto;
    flex-direction: row;
    align-items: center;
    gap: var(--fw-space-3);
    padding: var(--fw-space-2) var(--fw-space-3);
    border-right: none;
    border-bottom: var(--fw-hairline) solid var(--fw-border);
    overflow-x: auto;
  }
  .sidebar-brand { padding: 0 var(--fw-space-2) 0 0; white-space: nowrap; }
  .sidebar-nav { flex-direction: row; gap: var(--fw-space-1); }
  .sidebar-nav a { white-space: nowrap; }
  .sidebar-foot {
    margin-top: 0; margin-left: auto;
    padding-top: 0; padding-left: var(--fw-space-3);
    border-top: none; border-left: var(--fw-hairline) solid var(--fw-border);
  }
  /* L'identité disparaît, pas la déconnexion : le nom est une confirmation, le
   * bouton est une action. On coupe l'accessoire, jamais l'outil. */
  .sidebar-user { display: none; }

  .shell-admin .wrap { padding: var(--fw-space-4); }
  .shell-admin h1 { font-size: var(--fw-text-2xl); }
  .guide-toc { columns: 1; }
}

/* La zone sûre du bas sur un iPhone : sans elle, le dernier bouton de l'écran
 * d'encodage passe sous la barre de gestes et devient intouchable. */
@supports (padding: env(safe-area-inset-bottom)) {
  .shell-worker .wrap { padding-bottom: calc(var(--fw-space-6) + env(safe-area-inset-bottom)); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .btn:active, button:active { transform: none; }
}
