/* ============================================================================
   FleetControl — compléments au design system

   Le design system est Tailwind CSS + daisyUI (ADR-004), servis depuis
   `frontend/vendor/`. Tout ce qui peut s'écrire en classes utilitaires est dans
   le balisage ; ce fichier ne garde que ce que Tailwind et daisyUI ne couvrent
   pas :

   - Leaflet, qui injecte son propre DOM et ignore nos classes ;
   - la convention de couleurs des états véhicule, partagée par le HTML, le
     JavaScript et les popups de carte (voir backend/fleet/states.py) ;
   - deux ou trois réglages typographiques propres à la télématique.

   Les couleurs sont lues dans les variables de thème daisyUI (`--su`, `--wa`,
   `--er`, `--in`, `--b1`, `--bc`…) : elles suivent donc la bascule clair/sombre
   sans duplication.
   ============================================================================ */

/* ============ CORRECTION DE THÈME ============ */
/* daisyUI pose `--b3` à 21,1 % en thème sombre, sous le `--b1` des cartes
   (25,3 %) : les 157 `border-base-300` du balisage passent donc *derrière* la
   surface qu'elles doivent délimiter, et les cartes perdent leur arête. Le
   remplissage suffit à les distinguer du fond, mais le contour disparaît.

   On remonte la seule clarté au-dessus de `--b1`, en conservant la teinte et
   la chroma du thème : rien d'autre ne bouge, et une bascule de thème daisyUI
   reprend ses propres valeurs. Le bandeau de télémétrie, dont les séparateurs
   sont des `--b3`, en profite aussi.

   Deux sélecteurs, parce que le thème sombre s'obtient de deux façons : par
   choix explicite (`data-theme`, posé par base.js) et par préférence système
   (bloc `@media` de themes.css, quand aucun choix n'est enregistré). Le
   troisième sélecteur de themes.css (`input.theme-controller`) n'est pas
   utilisé dans ce dépôt. */
[data-theme="dark"] { --b3: 33.5% 0.0165 252.417568; }

@media (prefers-color-scheme: dark) {
  /* `:not([data-theme="light"])` pour ne pas repeindre une page qu'un
     utilisateur a explicitement mise en clair sur un système sombre. */
  :root:not([data-theme="light"]) { --b3: 33.5% 0.0165 252.417568; }
}

/* ============ TONS EN TEXTE ============ */
/* Les tons daisyUI sont faits pour des aplats (texte sombre dessus). En
   texte sur un fond teinté, il leur faut une version contrastée par thème :
   plus sombre en clair, plus claire en sombre. Les badges s'en servent. */
:root {
  --su-t: 45% 0.13 160;
  --wa-t: 55% 0.13 75;
  --er-t: 52% 0.19 25;
  --in-t: 50% 0.13 232;
}
[data-theme="dark"] {
  --su-t: 80% 0.15 160; --wa-t: 86% 0.17 84; --er-t: 78% 0.16 22; --in-t: 78% 0.13 232;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --su-t: 80% 0.15 160; --wa-t: 86% 0.17 84; --er-t: 78% 0.16 22; --in-t: 78% 0.13 232;
  }
}

/* ============ BADGES ============ */
/* Fond teinté et texte dans le ton, au lieu de l'aplat daisyUI : un badge
   étiquette une ligne, il ne doit pas peser plus lourd qu'elle. Le point
   d'état (`status-dot`) garde sa couleur pleine et fait le rappel. Aucun
   gabarit ne change : ce sont les mêmes classes. */
.badge {
  border-color: transparent;
  font-weight: 500;
  letter-spacing: 0.005em;
  padding-inline: 0.55rem;
}
.badge.badge-sm { height: 1.2rem; font-size: 0.72rem; }
.badge.badge-xs { padding-inline: 0.4rem; }
.badge-success { background: oklch(var(--su) / 0.14); color: oklch(var(--su-t)); }
.badge-warning { background: oklch(var(--wa) / 0.18); color: oklch(var(--wa-t)); }
.badge-error   { background: oklch(var(--er) / 0.13); color: oklch(var(--er-t)); }
.badge-info    { background: oklch(var(--in) / 0.14); color: oklch(var(--in-t)); }
.badge-primary { background: oklch(var(--p) / 0.12); color: oklch(var(--p)); }
.badge-neutral { background: oklch(var(--bc) / 0.10); color: oklch(var(--bc) / 0.85); }
.badge-ghost   { background: oklch(var(--bc) / 0.06); color: oklch(var(--bc) / 0.75); }
/* Le compteur d'alertes sur la cloche reste plein : c'est un signal, pas une étiquette. */
.badge-error.badge-xs.absolute { background: oklch(var(--er)); color: oklch(var(--erc)); }

/* ============ PALETTE CATÉGORIELLE ============ */
/* Quatre teintes pour les graphiques qui découpent une quantité en parts
   (coûts, répartitions) ou tracent une seule série. Aucune n'est un ton
   d'état : le vert, l'ambre et le bleu veulent dire « en mouvement »,
   « au ralenti », « hors ligne » partout ailleurs, un graphique ne doit pas
   les employer pour dire « entretien » ou « amortissement ».
   Lues par base.js (`couleursCategorielles`), jamais en classe utilitaire.
   Même double sélecteur que `--b3` ci-dessus pour le thème sombre. */
:root {
  --cat1: 52% 0.20 275;   /* indigo, proche du primaire : la série unique */
  --cat2: 58% 0.11 195;   /* sarcelle */
  --cat3: 58% 0.17 335;   /* prune */
  --cat4: 55% 0.03 260;   /* ardoise */
  --cat5: 48% 0.09 60;    /* bronze : la cinquième teinte des trajets du jour, loin de l'ambre du ralenti */
}
[data-theme="dark"] {
  --cat1: 70% 0.17 275; --cat2: 72% 0.11 195; --cat3: 72% 0.15 335; --cat4: 68% 0.03 260; --cat5: 70% 0.09 60;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --cat1: 70% 0.17 275; --cat2: 72% 0.11 195; --cat3: 72% 0.15 335; --cat4: 68% 0.03 260; --cat5: 70% 0.09 60;
  }
}

/* ============ MARQUE ============ */
/* L'orange de la marque n'existe qu'ici : sur le logo (rail, connexion).
   Jamais sur un bouton ni sur un état — il serait confondu avec l'ambre
   d'avertissement. Seul de sa couleur à l'écran, il se voit d'autant mieux.
   Même valeur dans les deux thèmes : c'est un logo, pas un texte à lire
   (3,2:1 sur blanc). */
:root { --marque: #FF6600; }
.marque { color: var(--marque); }

/* ============ ALPINE ============ */
/* Masque un bloc jusqu'à l'initialisation d'Alpine, sinon il apparaît déplié
   pendant un frame. */
[x-cloak] { display: none !important; }

/* ============ TYPOGRAPHIE ============ */
/* Chiffres tabulaires et zéro barré sur tout ce qui est monospacé :
   immatriculations, IMEI, coordonnées, relevés capteurs. Sans « tnum », une
   valeur qui se rafraîchit décale la colonne ; sans « zero », un 0 se confond
   avec un O dans un numéro de châssis. */
.font-mono,
code, kbd, samp, pre {
  font-feature-settings: "tnum", "zero";
  font-variant-numeric: tabular-nums;
}

/* Titres de page et de carte en Barlow Semi Condensed : même famille que le
   texte courant, largeur différente. Un titre se distingue par sa forme, sans
   grossir. Posé sur les éléments plutôt que sur une classe : les 60 titres des
   gabarits sont tous des h1/h2/h3, aucun balisage à toucher. Une classe de
   police explicite (`.plate`, `.font-mono`) garde la main. */
h1, h2, h3 {
  font-family: "Barlow Semi Condensed", "Barlow", system-ui, sans-serif;
  letter-spacing: 0.005em;
}

/* Plaque d'immatriculation : rendu « estampé », inspiré de la plaque physique.
   600 est la graisse la plus forte vendorisée pour Plex Mono. */
.plate {
  font-family: "IBM Plex Mono", ui-monospace, monospace;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* ============ BARRES DE DÉFILEMENT ============ */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: oklch(var(--bc) / 0.2);
  border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover { background: oklch(var(--bc) / 0.35); }

/* Utilitaire : bande de filtres défilable sans barre visible. */
.no-scrollbar { scrollbar-width: none; -ms-overflow-style: none; }
.no-scrollbar::-webkit-scrollbar { display: none; }

/* ============ DÉFILEMENT HORIZONTAL SIGNALÉ ============ */
/* Un onglet daisyUI est un inline-flex qui replie : à 400 px, l'icône passe
   au-dessus du libellé et la rangée se coupe. Icône et libellé restent sur
   une ligne ; c'est la rangée (`defile-x`) qui défile. */
.tabs .tab { flex-wrap: nowrap; white-space: nowrap; }

/* Rangée qui défile sans barre (onglets du suivi). base.js pose
   `defile-gauche` / `defile-droite` selon la position ; le bord estompé dit
   « ça continue », et disparaît au bout. Masque plutôt qu'ombre : c'est le
   contenu lui-même qui s'efface, pas une bande peinte par-dessus. */
.defile-x { overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; }
.defile-x::-webkit-scrollbar { display: none; }
.defile-x.defile-droite {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 44px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 44px), transparent);
}
.defile-x.defile-gauche {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 44px);
  mask-image: linear-gradient(to right, transparent, #000 44px);
}
.defile-x.defile-gauche.defile-droite {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 44px, #000 calc(100% - 44px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 44px, #000 calc(100% - 44px), transparent);
}

/* ============ CARTES LEAFLET ============ */
/* Leaflet crée ses conteneurs en JS : ils n'héritent d'aucune classe Tailwind.
   La hauteur doit venir d'ici, sinon la carte se replie à 0 pixel. */
.map-container {
  min-height: 320px;
  height: 100%;
  width: 100%;
  z-index: 0; /* sous les dropdowns daisyUI (z-[1]) et les modales (z-50) */
  background: oklch(var(--b3));
}
.map-container.full { min-height: 100%; }

.leaflet-container { font: inherit; }

/* Fond de carte en thème sombre : les tuiles OpenStreetMap sont claires, et
   aucun fournisseur ne sert plus de fond sombre dessiné sans clé d'API
   (Stadia et Carto répondent 401 / filigrane). On assombrit donc les tuiles
   elles-mêmes : inversion, rotation de teinte pour que l'eau reste bleue et
   les parcs verts, puis désaturation et contraste pour que les noms de rues
   se lisent. Seul le `tile-pane` est filtré : marqueurs, tracés et fenêtres
   gardent leurs couleurs. Mêmes deux sélecteurs que `--b3` plus haut. */
[data-theme="dark"] .leaflet-tile-pane {
  filter: invert(0.92) hue-rotate(180deg) grayscale(0.45) brightness(0.9) contrast(1.05);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .leaflet-tile-pane {
    filter: invert(0.92) hue-rotate(180deg) grayscale(0.45) brightness(0.9) contrast(1.05);
  }
}

/* Attribution : même fond blanc en dur ; suit le thème comme les popups. */
.leaflet-container .leaflet-control-attribution {
  background: oklch(var(--b1) / 0.8);
  color: oklch(var(--bc) / 0.7);
}
.leaflet-container .leaflet-control-attribution a { color: inherit; }

/* Popups : Leaflet applique un fond blanc en dur, illisible en thème sombre. */
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
  background: oklch(var(--b1));
  color: oklch(var(--bc));
  border-radius: var(--rounded-box, 0.5rem);
  box-shadow: 0 4px 16px -2px rgb(0 0 0 / 0.45);
}
.leaflet-popup-content { margin: 0.75rem 0.9rem; }
.leaflet-popup-content-wrapper a { color: oklch(var(--p)); }

/* Contrôles Leaflet (zoom, attribution) alignés sur le thème. */
.leaflet-bar a,
.leaflet-bar a:hover {
  background: oklch(var(--b1));
  color: oklch(var(--bc));
  border-bottom-color: oklch(var(--bc) / 0.15);
}
.leaflet-control-attribution {
  background: oklch(var(--b1) / 0.8);
  color: oklch(var(--bc) / 0.6);
}
.leaflet-control-attribution a { color: oklch(var(--bc) / 0.8); }

/* ============ ÉTATS VÉHICULE ============ */
/* Convention unique, miroir de backend/fleet/states.py :
   mouvement = succès (vert), ralenti = avertissement (ambre),
   stationné = neutre (gris), hors ligne = information (bleu).
   Le rouge reste réservé aux alertes, jamais à un état normal. */
.status-dot {
  display: inline-block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 9999px;
  flex-shrink: 0;
  background: oklch(var(--n));
}
.status-moving  { background: oklch(var(--su)); }
.status-idle    { background: oklch(var(--wa)); }
.status-stopped { background: oklch(var(--er)); }
.status-offline { background: oklch(var(--in)); }
.status-alarm   { background: oklch(var(--er)); }

/* Un véhicule en mouvement respire : c'est le seul point animé de l'interface,
   ce qui le rend repérable dans une liste de cent lignes. */
.status-moving { animation: fc-pulse 2s ease-in-out infinite; }
@keyframes fc-pulse {
  0%, 100% { box-shadow: 0 0 0 0 oklch(var(--su) / 0.55); }
  70%      { box-shadow: 0 0 0 5px oklch(var(--su) / 0); }
}

/* Marqueurs de la carte : même convention, sur un divIcon Leaflet. */
.lm-dot {
  display: block;
  width: 12px;
  height: 12px;
  border-radius: 9999px;
  border: 2px solid oklch(var(--b1));
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.5);
  background: oklch(var(--n));
}
.lm-dot.moving  { background: oklch(var(--su)); }
.lm-dot.idle    { background: oklch(var(--wa)); }
.lm-dot.stopped { background: oklch(var(--er)); }
.lm-dot.offline { background: oklch(var(--in)); }
.lm-dot.alarm   { background: oklch(var(--er)); }

/* Groupe de véhicules sur la carte (Leaflet.markercluster, tracking/map.html) :
   un anneau réparti par état, le nombre au centre. Les couleurs sont celles de
   .lm-dot, par variables pour le conic-gradient posé en ligne. */
.lm-groupe { --lm-moving: oklch(var(--su)); --lm-idle: oklch(var(--wa)); --lm-stopped: oklch(var(--er)); --lm-offline: oklch(var(--in)); position: relative; display: block; border-radius: 9999px; box-shadow: 0 2px 8px rgb(0 0 0 / 0.35); cursor: pointer; transition: transform 0.15s ease; }
.lm-groupe:hover { transform: scale(1.1); }
.lm-groupe-anneau { position: absolute; inset: 0; border-radius: 9999px; }
.lm-groupe-centre {
  position: absolute; inset: 5px; border-radius: 9999px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; line-height: 1;
  background: oklch(var(--b1)); color: oklch(var(--bc)); border: 1.5px solid oklch(var(--b1));
  font-family: "IBM Plex Mono", ui-monospace, monospace;
}
.lm-groupe-centre b { font-size: 14px; font-weight: 700; }
.lm-groupe-centre i { font-size: 8px; font-style: normal; text-transform: uppercase; opacity: 0.6; }

/* ============ CARTES D'INDICATEUR ============ */
/* Les 45 cartes de KPI de l'application partagent une seule et même chaîne de
   classes : `stat bg-base-100 rounded-box border border-base-300`. On s'y
   accroche directement plutôt que d'ajouter une classe dans douze templates —
   aucun balisage n'est modifié, et une carte ajoutée demain suit la règle sans
   qu'on ait à y penser.

   Le trait donne à la rangée de KPI une arête gauche continue, qui se balaie
   verticalement du regard. C'est l'apport principal : l'icône, alignée à
   droite, ne joue pas ce rôle.

   Sa couleur n'est pas une nouvelle convention : `:has()` va la chercher sur
   la `stat-figure` de la carte, qui la déclare déjà. Le trait ne peut donc
   jamais diverger de l'icône. Une carte sans figure colorée (`opacity-40`, ou
   pas de figure du tout) garde le trait neutre ci-dessous. Sur un navigateur
   sans `:has()`, tout le monde retombe sur ce neutre : la mise en page tient,
   seule la couleur se perd. */
.stat.rounded-box.border-base-300 { position: relative; }
.stat.rounded-box.border-base-300::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.875rem;
  bottom: 0.875rem;
  width: 3px;
  border-radius: 3px;
  /* Neutre dérivé de la couleur de texte, et non du token `--n` : celui-ci est
     un gris sombre dans les *deux* thèmes, ce qui fait disparaître le trait
     sur une carte sombre. */
  background: oklch(var(--bc) / 0.35);
}
.stat.rounded-box.border-base-300:has(.stat-figure.text-success)::before { background: oklch(var(--su)); }
.stat.rounded-box.border-base-300:has(.stat-figure.text-warning)::before { background: oklch(var(--wa)); }
.stat.rounded-box.border-base-300:has(.stat-figure.text-error)::before   { background: oklch(var(--er)); }
.stat.rounded-box.border-base-300:has(.stat-figure.text-info)::before    { background: oklch(var(--in)); }
.stat.rounded-box.border-base-300:has(.stat-figure.text-primary)::before { background: oklch(var(--p)); }

/* Points d'intérêt : une pastille carrée, pour ne jamais se confondre avec le
   point rond d'un véhicule — c'est la forme qui distingue, pas la couleur. */
.lm-poi {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  background: oklch(var(--p));
  color: oklch(var(--pc));
  font-size: 13px;
  box-shadow: 0 1px 5px rgb(0 0 0 / 0.45);
}
/* Sur la page des lieux, la pastille prend la teinte de sa catégorie
   (`data-rang`, même échelle que les trajets du jour, plus bas). */
.lm-poi[data-rang] { background: var(--teinte); color: #fff; }
/* Étiquette permanente sous le marqueur (page des lieux) : le nom du site en
   petit, sur fond de carte, sans la flèche du tooltip Leaflet. */
.poi-etiquette {
  padding: 1px 6px;
  border: 0;
  border-radius: 999px;
  background: oklch(var(--n));
  color: oklch(var(--nc));
  font: 600 10px/16px "IBM Plex Mono", ui-monospace, monospace;
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.35);
}
.poi-etiquette::before { display: none; }

/* ============ PANNEAU FLOTTANT DE LA CARTE ============ */
/* Le rail de la carte en direct flotte au-dessus du fond de plan plutôt que de
   le repousser : la carte occupe toute la page, et le panneau laisse voir ce
   qu'il masque. Écrit ici et pas en classes utilitaires — c'est déjà le cas de
   tout ce que daisyUI ne couvre pas, et ça ne dépend pas de ce que le build
   navigateur de Tailwind génère.

   `backdrop-filter` est ce qui rend le texte lisible : sans le flou, un fond à
   55 % laisse passer les rues et les étiquettes du fond de plan, et le
   panneau devient illisible. D'où le repli ci-dessous. */
.panneau-verre {
  background: oklch(var(--b1) / 0.55);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
}

/* Les cartes de véhicule laissent elles aussi passer le fond, un peu moins. */
.carte-verre { background: oklch(var(--b1) / 0.72); }

/* Navigateur sans `backdrop-filter` : on remonte l'opacité jusqu'à ce que le
   texte redevienne lisible. On perd l'effet, jamais la lecture. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .panneau-verre { background: oklch(var(--b1) / 0.95); }
  .carte-verre   { background: oklch(var(--b1)); }
}

/* ============ TABLES DE DONNÉES ============ */
/* Une table de flotte a huit colonnes. Quand la fenêtre se resserre, le
   navigateur préfère casser les cellules plutôt que déborder : « 48 250 km »
   se retrouve sur deux lignes, un badge d'état passe à la ligne au milieu de
   son libellé, et la table devient illisible bien avant d'être trop étroite.

   On force donc chaque cellule à rester d'un bloc : le conteneur
   `overflow-x-auto` prend le relais et la table défile horizontalement, ce qui
   se lit. Les cellules de texte libre (objet d'une facture, description d'un
   trajet) reprennent le retour à la ligne avec `whitespace-normal`. */
.table :where(th, td) { white-space: nowrap; }
.table :where(th, td).whitespace-normal { white-space: normal; }

/* Pied de table : daisyUI donne aux `tfoot th` le style des en-têtes (petit,
   gris, sans graisse), ce qui rend une ligne « Total » plus faible que les
   lignes qu'elle somme. Le total reprend la couleur et le corps du contenu,
   avec la graisse qui le distingue. */
.table tfoot {
  color: inherit;      /* daisyUI pose le gris sur tfoot, pas sur th */
  font-size: inherit;
  line-height: inherit;
  font-weight: 600;
}

/* Indice de défilement : quand la table dépasse son conteneur, une ombre se
   pose sur le bord vers lequel il reste du contenu et disparaît en bout de
   course. Technique des fonds à attache mixte : les deux voiles (couleur de
   la carte) suivent le contenu, les deux ombres restent fixes ; le voile
   recouvre l'ombre tant que le bord est atteint. Aucun script, aucune classe
   à poser : tout conteneur `overflow-x-auto` dont l'enfant direct est une
   `.table` en bénéficie. Une ligne survolée (fond opaque) masque l'ombre le
   temps du survol, sans conséquence. */
.overflow-x-auto:has(> .table) {
  background:
    linear-gradient(90deg, oklch(var(--b1)) 40%, oklch(var(--b1) / 0)) left,
    linear-gradient(90deg, oklch(var(--b1) / 0), oklch(var(--b1)) 60%) right,
    radial-gradient(farthest-side at 0 50%, oklch(var(--bc) / 0.28), transparent) left,
    radial-gradient(farthest-side at 100% 50%, oklch(var(--bc) / 0.28), transparent) right;
  background-repeat: no-repeat;
  background-size: 48px 100%, 48px 100%, 12px 100%, 12px 100%;
  background-attachment: local, local, scroll, scroll;
}

/* ============ BANDEAU DE SOURCE ============ */
/* `<details>` natif : le marqueur du navigateur est masqué, remplacé par le
   chevron du gabarit, qui tourne à l'ouverture. Sans script : ouverture,
   clavier et lecteur d'écran sont ceux du navigateur. */
.bandeau-source > summary { list-style: none; }
.bandeau-source > summary::-webkit-details-marker { display: none; }
.bandeau-source-chevron { transition: transform 0.15s ease; }
.bandeau-source[open] .bandeau-source-chevron { transform: rotate(180deg); }

/* ============ VALEURS NULLES ============ */
/* Posé par le filtre `montant` (fleet/templatetags/francais.py) : un zéro
   dans une table de coûts s'efface devant les montants, sans disparaître. */
.valeur-nulle { opacity: 0.35; }

/* ============ ÉTATS VIDES ============ */
/* Le décor dit ce qui manque (partials/empty.html). Discret : c'est un fond,
   le message reste ce qu'on lit. Couleurs dérivées du texte, donc valables
   dans les deux thèmes. */
.etat-vide { position: relative; }
.etat-vide-icone { position: relative; z-index: 1; }

/* Graphique : la grille horizontale d'un tracé, sans tracé. */
.etat-vide-graphique {
  background-image: repeating-linear-gradient(
    to bottom, transparent 0 27px, oklch(var(--bc) / 0.07) 27px 28px
  );
  background-size: 100% 28px;
}

/* Carte : le contour en pointillé d'une zone, comme une géofence sans tracé. */
.etat-vide-carte::before {
  content: "";
  position: absolute;
  inset: 14px 18%;
  border: 1.5px dashed oklch(var(--bc) / 0.18);
  border-radius: 40% 55% 45% 60% / 55% 40% 60% 45%;
  pointer-events: none;
}

/* ============ COLONNES TRIABLES ============ */
/* Posé par base.js sur les en-têtes portant `data-sort`. La flèche double
   annonce qu'une colonne est triable avant qu'on ait cliqué : sans elle,
   l'interaction n'existe que pour qui l'a déjà découverte. */
.table th[data-sort] { cursor: pointer; user-select: none; }
.table th[data-sort]:hover { color: oklch(var(--bc)); }
.table th[data-sort]::after {
  content: "\2195";
  margin-left: 0.3rem;
  font-size: 0.7em;
  opacity: 0.35;
}
.table th[data-sort][aria-sort="ascending"]::after  { content: "\2191"; opacity: 1; color: oklch(var(--p)); }
.table th[data-sort][aria-sort="descending"]::after { content: "\2193"; opacity: 1; color: oklch(var(--p)); }

/* ============ ACCESSIBILITÉ ============ */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .status-moving { animation: none !important; box-shadow: none !important; }
}

/* ============ HISTORIQUE SUR LA CARTE ============ */
/* Les trajets d'une journée sont numérotés et colorés par leur rang, avec la
   palette catégorielle ci-dessus — jamais un ton d'état. Le même numéro
   coloré ouvre la ligne dans le panneau et marque le départ sur la carte.
   Les tracés eux-mêmes lisent la teinte en JavaScript (window.couleur). */
.trajet-rang, .lm-rang {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 9999px;
  background: var(--teinte, oklch(var(--n)));
  color: #fff;
  font-family: ui-monospace, "JetBrains Mono", monospace;
  font-size: 12px;
  font-weight: 700;
  flex-shrink: 0;
}
.lm-rang { border: 2px solid oklch(var(--b1)); box-shadow: 0 1px 4px rgb(0 0 0 / 0.4); }
.lm-rang[data-estompe] { opacity: 0.45; }
[data-rang="1"] { --teinte: oklch(var(--cat1)); }
[data-rang="2"] { --teinte: oklch(var(--cat2)); }
[data-rang="3"] { --teinte: oklch(var(--cat3)); }
[data-rang="4"] { --teinte: oklch(var(--cat4)); }
[data-rang="5"] { --teinte: oklch(var(--cat5)); }

/* Symbole d'événement (fleet.events) : sur la carte comme dans la liste, la
   même pastille, colorée par le ton de l'événement. Le ton neutre est tiré
   du texte, comme partout (`--n` est sombre dans les deux thèmes). */
.lm-evt {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 9999px;
  border: 2px solid oklch(var(--b1));
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.4);
  font-size: 13px;
  flex-shrink: 0;
}
.lm-evt.static { width: 22px; height: 22px; border: 0; box-shadow: none; font-size: 12px; }
.lm-evt.ton-neutral { background: oklch(var(--bc) / 0.75); color: oklch(var(--b1)); }
.lm-evt.ton-warning { background: oklch(var(--wa)); color: oklch(var(--wac)); }
.lm-evt.ton-error   { background: oklch(var(--er)); color: oklch(var(--erc)); }

/* Marqueur du rejeu : une flèche dans la teinte du trajet, tournée vers le
   cap par rejeu.js. Plus grande que les pastilles : c'est elle qu'on suit. */
.lm-rejeu {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 9999px;
  background: var(--teinte, oklch(var(--p)));
  color: #fff;
  border: 2px solid oklch(var(--b1));
  box-shadow: 0 2px 6px rgb(0 0 0 / 0.45);
  font-size: 16px;
  transition: transform 0.2s linear;
}
@media (prefers-reduced-motion: reduce) {
  .lm-rejeu { transition: none; }
}

/* ============ PANNEAU DROIT DE LA CARTE ============ */
/* Sous md, le panneau et la liste des véhicules sont deux feuilles ancrées en
   bas : une seule à la fois. Quand le panneau est ouvert (`panneau-ouvert`
   sur la racine, tracking/map.html), la liste s'efface ; la fermeture la
   ramène. À partir de md, rail et panneau ont chacun leur côté. */
@media (max-width: 767px) {
  .panneau-ouvert #rail-vehicules { display: none; }
}
/* Pendant un chargement htmx, le contenu précédent s'estompe au lieu de
   disparaître. */
#panneau-droit.htmx-request #panneau-droit-contenu { opacity: 0.5; transition: opacity 0.15s; }

/* Mobile, historique avec un trajet sélectionné : le lecteur occupe le haut
   de la carte, la feuille le bas. À 60 % de hauteur, la feuille ne laissait
   au tracé qu'une bande de quelques dizaines de pixels. Lecteur ouvert, elle
   descend à 42 % (son contenu défile) : le tracé garde environ un tiers de
   l'écran. */
@media (max-width: 767px) {
  #live-map:has(#lecteur:not([hidden])) #panneau-droit,
  body:has(#lecteur:not([hidden])) #panneau-droit { max-height: 42%; }
}

/* ============ RAIL DE NAVIGATION ============ */
/* Quatre groupes titrés, chaque entrée avec son icône (base.html). Ce qui suit
   est ce que les classes utilitaires ne couvrent pas proprement : la forme de
   l'entrée et son état actif, partagés par vingt liens. */
.nav-lien {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 6px 0 6px 8px;
  border-left: 4px solid transparent; /* réservé : le liseré actif ne fait pas sauter la colonne */
  border-radius: 0 6px 6px 0;
  font-size: 12px;
  line-height: 20px;
  color: oklch(var(--bc) / 0.8);
  transition: background-color 0.15s, color 0.15s;
}
.nav-lien:hover { background: oklch(var(--b2) / 0.8); color: oklch(var(--bc)); }

/* La page courante : liseré et fond dans la couleur d'accent, texte en
   semi-gras. Le même liseré gauche que les cartes d'indicateur — un seul geste
   graphique pour dire « ici », d'un écran à l'autre. */
.nav-lien.actif {
  color: oklch(var(--p));
  font-weight: 600;
  background: oklch(var(--p) / 0.1);
  border-left-color: oklch(var(--p));
  padding-left: 10px;
}

/* Dock des pôles (niveau 1 du rail) : une icône et un mot par pôle ; le pôle
   ouvert est plein, le pôle de la page courante garde un repère même fermé. */
.nav-pole {
  position: relative; width: 100%; padding: 8px 0 6px; border-radius: 12px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  font-size: 10px; line-height: 12px; color: oklch(var(--bc) / 0.65);
  transition: background-color 0.15s, color 0.15s;
}
.nav-pole:hover { background: oklch(var(--b1)); color: oklch(var(--bc)); }
.nav-pole.ouvert { background: oklch(var(--p)); color: oklch(var(--pc)); font-weight: 700; box-shadow: 0 1px 2px oklch(0% 0 0 / 0.12); }
.nav-pole:focus-visible { outline: 2px solid oklch(var(--p)); outline-offset: 2px; }
.nav-pole-ici { position: absolute; left: 2px; top: 50%; width: 3px; height: 14px; margin-top: -7px; border-radius: 2px; background: oklch(var(--p)); }
.nav-pole.ouvert .nav-pole-ici { background: oklch(var(--pc)); }
.nav-pole-pastille { position: absolute; top: 6px; right: 10px; width: 8px; height: 8px; border-radius: 999px; background: oklch(var(--er)); box-shadow: 0 0 0 2px oklch(var(--b2)); }

.pouls { border: 1px solid transparent; transition: border-color 0.15s; }
.pouls:hover { border-color: oklch(var(--p) / 0.45); }
.pouls-moving  { background: oklch(var(--su)); color: oklch(var(--su)); }
.pouls-idle    { background: oklch(var(--wa)); color: oklch(var(--wa)); }
.pouls-stopped { background: oklch(var(--er)); color: oklch(var(--er)); }
.pouls-offline { background: oklch(var(--in)); color: oklch(var(--in)); }
.pouls-compte  { background: none; } /* seuls les segments sont remplis ; les compteurs n'ont que la couleur */

/* ============ TIROIR DE FORMULAIRE ============
   Formulaire court ouvert par-dessus la page (affectation depuis la matrice),
   chargé par HTMX dans #tiroir-contenu ; js/base.js l'ouvre et le ferme. */
.fc-tiroir { position: fixed; inset: 0; z-index: 60; pointer-events: none; }
.fc-tiroir-voile { position: absolute; inset: 0; background: oklch(0% 0 0 / 0.45); opacity: 0; transition: opacity 0.2s ease; }
.fc-tiroir-panneau {
  position: absolute; top: 0; right: 0; bottom: 0; width: min(100%, 30rem);
  background: oklch(var(--b1)); border-left: 1px solid oklch(var(--b3));
  box-shadow: -8px 0 24px oklch(0% 0 0 / 0.18);
  transform: translateX(100%); transition: transform 0.25s ease;
}
.fc-tiroir-ouvert { pointer-events: auto; }
.fc-tiroir-ouvert .fc-tiroir-voile { opacity: 1; }
.fc-tiroir-ouvert .fc-tiroir-panneau { transform: translateX(0); }
.fc-tiroir-tete { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; padding: 1rem 1.25rem; border-bottom: 1px solid oklch(var(--b3)); }
.fc-tiroir-corps { flex: 1; overflow-y: auto; padding: 1.1rem 1.25rem; }
.fc-tiroir-pied { display: flex; justify-content: flex-end; gap: 0.5rem; padding: 0.85rem 1.25rem; border-top: 1px solid oklch(var(--b3)); }
@media (prefers-reduced-motion: reduce) {
  .fc-tiroir-voile, .fc-tiroir-panneau { transition: none; }
}

/* ============ ÉTAPES DE L'IMPORT CSV ============
   Trois étapes numérotées (dépôt, colonnes, diagnostic) ; l'étape courante
   porte aria-current, les étapes passées la classe `fait`. */
.fc-etapes-import { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; font-size: 0.85rem; counter-reset: none; }
.fc-etapes-import li { display: flex; align-items: center; gap: 0.5rem; opacity: 0.55; }
.fc-etapes-import li span {
  display: inline-grid; place-items: center; width: 1.5rem; height: 1.5rem; border-radius: 999px;
  border: 1px solid oklch(var(--bc) / 0.3); font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 0.75rem;
}
.fc-etapes-import li[aria-current="step"] { opacity: 1; font-weight: 600; }
.fc-etapes-import li[aria-current="step"] span { background: oklch(var(--p)); border-color: oklch(var(--p)); color: oklch(var(--pc)); }
.fc-etapes-import li.fait { opacity: 0.85; }
.fc-etapes-import li.fait span { border-color: oklch(var(--su)); color: oklch(var(--su-t)); }
