/* ============================================================================
   cockpit — le chrome servi par PHP
   Connexion et cadre d'application. Volontairement sobre : ces écrans ne sont
   pas le cockpit, ils y mènent. Ils empruntent sa palette et sa typographie
   pour qu'on ne change pas d'univers en passant de l'un à l'autre, mais ils ne
   miment ni bezel ni verre — un formulaire déguisé en instrument est un
   formulaire qu'on lit moins bien.
   ========================================================================= */

* { box-sizing: border-box; }

html, body {
  margin: 0;
  min-height: 100%;
  /* LE SOL DU SITE, et non la tôle du panneau : l'un se met en clair
     quand on le demande, l'autre jamais. */
  background: var(--fond);
  color: var(--texte-fort);
  font-family: var(--font-ui, system-ui, sans-serif);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--info); }

/* `hidden` doit gagner, toujours.

   La feuille du navigateur ne pose qu'un `display:none` de faible poids, que
   la moindre règle `display:flex` ou `display:grid` écrasée sur la même classe
   emporte — et l'élément qu'on croit caché reste à l'écran. On l'a payé deux
   fois, sur les cartes filtrées puis sur une barre d'onglets vide ; la règle
   ci-dessous ferme le sujet pour tout le chrome. */
[hidden] { display: none !important; }

/* ── Connexion ─────────────────────────────────────────────────────────────── */

.gate {
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px;
  /* Une lueur unique, en haut : la même source de lumière que celle qui
     modèle les bezels du panneau. */
  background: var(--fond);
}

.gate__box {
  width: 100%;
  max-width: 360px;
  /* CENTRÉ, contenu compris.

     La boîte l'était déjà — mais son contenu, lui, était aligné à gauche de
     360 px. Sur le formulaire de mot de passe cela ne se voyait pas : les
     champs occupent toute la largeur, donc la composition remplit la boîte.
     Sur le pavé de code, quatre cases n'en occupent que la moitié, et l'écran
     entier paraissait penché vers la gauche.

     Centrer le contenu règle les deux d'un coup et fait mieux : les deux modes
     de connexion se superposent alors au pixel près — même vignette, même
     signature, même axe. On passe de l'un à l'autre sans que rien ne bouge. */
  text-align: center;
}
.gate__brand { justify-content: center; }
.gate .pincells { justify-content: center; }

/* Deux exceptions, et deux seulement. Le TEXTE QU'ON TAPE reste calé à gauche :
   centrer une adresse en cours de frappe la fait glisser à chaque caractère. Et
   les PARAGRAPHES d'explication de la première connexion restent au fer à
   gauche — un pavé de quatre lignes centré ne se lit plus. */
/* Les cases d'un code n'en sont pas : un chiffre par case, qui ne glisse pas. */
.gate .field input:not([data-cell]),
.gate .notice,
.gate__intro,
.gate__note { text-align: left; }

/* La jauge de la page de connexion.

   Son aiguille monte une fois au chargement, de zéro à sa valeur, avec le même
   amortissement que les instruments du panneau : c'est un instrument qui
   s'anime, pas un logo qui s'agite. Elle ne bouge plus ensuite — une aiguille
   qui balaie en boucle serait une décoration, et l'on cesserait de croire à ce
   qu'elle indique. */
.gate__mark { margin-bottom: 14px; }
.gate__needle {
  transform-origin: 32px 34px;
  animation: gate-needle 1.1s cubic-bezier(.22, 1, .36, 1) both;
}
@keyframes gate-needle {
  from { transform: rotate(-100deg); }
  to   { transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
  .gate__needle { animation: none; }
}

.gate__brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 28px;
}
.gate__brand img { height: 18px; width: auto; opacity: .9; }
.gate__brand b {
  font-family: var(--font-ui);
  font-size: 15px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--texte-fort);
}

.field { margin-bottom: 14px; }
/* ── MAIS PAS DANS UNE RANGÉE DE COMMANDES ───────────────────────────────────
   Ces quatorze pixels séparent deux champs EMPILÉS dans un formulaire. Dans une
   `.filters`, qui est une rangée, ils ne séparent plus rien — et ils désalignent :
   `align-items: center` centre la boîte de MARGE, pas la boîte visible. Mesuré
   sur la rangée du palmarès : la case se posait à 548 quand le bouton, qui n'a
   pas de marge, se posait à 555. Sept pixels, et l'on voyait le bouton pendre
   plus bas que les listes à côté de lui. */
.filters .field { margin-bottom: 0; }
.field label {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--texte-faible);
}
.field input {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--creux);
  color: var(--texte-fort);
  font: inherit;
  font-variant-numeric: tabular-nums;
}
.field input:focus {
  outline: none;
  border-color: var(--info);
  box-shadow: 0 0 0 3px rgba(63, 200, 240, .14);
}

/* ── UN SEUL GABARIT DE BOUTON ───────────────────────────────────────────────
   Le bouton de base était dessiné pour UN écran : celui de la connexion, où il
   occupe toute la largeur du formulaire, sous le mot de passe. Partout
   ailleurs, il fallait le rattraper — `.btn--go`, `.btn--danger`, les boutons
   d'un dialogue et ceux d'une rangée d'actions redéclaraient tous les trois
   mêmes propriétés pour revenir à un bouton normal. Et là où personne n'avait
   pensé à le faire, deux tailles de bouton se retrouvaient côte à côte :
   « Choisir un paquet… » en 14 px à côté d'« Enregistrer » en 12, pour deux
   gestes du même ordre.

   LE CAS ORDINAIRE DEVIENT LA RÈGLE, et la pleine largeur devient ce qu'elle
   aurait toujours dû être : une exception nommée, `.btn--bloc`, posée sur les
   trois boutons des écrans de connexion. */
.btn {
  width: auto;
  padding: 8px 14px;
  border: 1px solid var(--bord-fort);
  border-radius: var(--radius);
  background: var(--filet);
  color: var(--texte-fort);
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  /* ── LA HAUTEUR NE SE NÉGOCIE PAS AVEC LA POLICE ────────────────────────
     `line-height: normal` laisse la MÉTRIQUE de la fonte décider, et deux
     boutons voisins qui n'ont pas la même taille de texte prennent alors des
     hauteurs sans rapport avec leur padding : on en a vu deux côte à côte à
     41 et 44 pixels, pour des paddings de 11 et 8. Une valeur explicite rend
     la hauteur calculable — padding compris — et l'alignement cesse d'être
     une surprise au changement de fonte. */
  line-height: 1.2;
}
/* L'EXCEPTION : un formulaire de connexion. Un bouton unique sous deux champs,
   qui doit se voir de loin et se viser sans précision — sur un écran tactile de
   local technique, la cible est le bouton, pas le mot qu'il porte. */
.btn--bloc { width: 100%; padding: 11px 14px; font-size: 14px; }

.btn:hover { border-color: var(--info); }
/* Un bouton qui est un lien reste un bouton : le soulignement du lien y ajoute
   un second signal pour la même chose, et le rend moins net. */
a.btn { text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
.btn:active { background: var(--surface); }

/* L'ambre et le rouge sont réservés à l'alarme sur le panneau. Ici, hors du
   cockpit, un message d'erreur de connexion n'est pas une alarme d'installation
   et peut donc emprunter le rouge sans brouiller la convention. */
.notice {
  margin-bottom: 16px;
  padding: 9px 12px;
  border-left: 2px solid var(--alerte);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: rgba(255, 59, 48, .08);
  color: var(--texte-alerte);
  font-size: 14px;
}
.notice--ok {
  border-left-color: var(--ok);
  background: rgba(33, 208, 122, .08);
  color: var(--ok);
}

.gate__foot {
  margin-top: 22px;
  font-size: 12px;
  letter-spacing: .06em;
  color: var(--texte-tenu);
}

/* ── La coquille du site ──────────────────────────────────────────────────────
   Menu latéral à catégories, barre supérieure, menu du compte. Le vocabulaire
   du panneau — lumière zénithale, gravure, cyan pour ce qui est sélectionné —
   sans ses instruments : on n'administre pas dans un cockpit, on y pilote. */

/* LA COQUILLE NE DÉFILE PAS — SON CONTENU, OUI.
 *
 * Toute la page défilait : le menu latéral et la barre supérieure partaient
 * vers le haut avec le catalogue qu'on parcourait, et l'on se retrouvait à
 * remonter jusqu'en haut pour changer d'écran. Ce sont des repères fixes —
 * c'est même leur seule raison d'être.
 *
 * La coquille occupe donc exactement la hauteur de la fenêtre, et c'est la
 * zone de contenu qui défile. Le `min-height: 0` sur les colonnes est ce qui
 * rend la chose possible : sans lui, un élément de boîte flexible refuse de
 * descendre sous la hauteur de son contenu et le débordement ressort en bas de
 * la page, hors d'atteinte. */
.app-shell {
  height: 100vh;
  height: 100dvh;
  display: flex;
  overflow: hidden;
}

/* ── Menu latéral ─────────────────────────────────────────────────────────── */

.side {
  flex: 0 0 208px;
  display: flex;
  flex-direction: column;
  background: var(--fond-creux);
  border-right: 1px solid var(--filet);
}

.side__brand {
  display: flex;
  align-items: center;
  gap: 9px;
  height: 46px;
  padding: 0 16px;
  border-bottom: 1px solid var(--filet);
  text-decoration: none;
}
/* ── LE LOGO SE MESURE SUR LE MOT D'À CÔTÉ, PAS SUR LA BARRE ────────────────
   À treize pixels, la marque était deux fois plus discrète que « COCKPIT » qui
   la suit — l'inverse de ce qu'une signature doit faire.

   LES DEUX RÉGLAGES VIENNENT D'UNE MESURE, pas de l'œil. Le logotype a été
   rasterisé et ses lignes comptées, une à une : sur 200 rangées, l'encre dense
   — le corps des minuscules — occupe les rangées 50 à 149. Donc la hampe et le
   point du « i » tiennent le quart du haut, le jambage du « g » le quart du
   bas, et LA LIGNE DE PIED EST À 74,5 % de la hauteur de l'image. « COCKPIT »,
   lui, pose ses capitales de 8,4 px sous le haut de sa ligne.

   D'où :

   LA TAILLE — 26 px, soit un corps de minuscules de 13 px contre 8,4 px de
   capitale. Un logotype ne se compare pas à une hauteur de police : à taille
   « égale » il paraît deux fois plus petit, parce que ses lettres n'occupent
   que la moitié de leur cadre — le reste est hampe, point et jambage. La
   marque doit dominer le mot qu'elle présente, et c'est ce rapport-là qui le
   fait.

   LE CALAGE — centrées dans la barre, les deux boîtes s'alignent sur leur
   MILIEU, et deux mots côte à côte s'alignent sur leur PIED. On remonte donc
   l'image de ce qu'il faut pour que les deux lignes de pied coïncident, le
   jambage du « g » débordant SOUS elles comme il se doit. Mon premier calcul
   se trompait de trois pixels : j'avais pris la hauteur de capitale pour une
   distance depuis le HAUT de la ligne, alors qu'elle se compte depuis le pied.
   Le dessin paraissait juste et ne l'était pas ; c'est la mesure qui l'a dit. */
/* ── LES LETTRES SE CENTRENT L'UNE SUR L'AUTRE ──────────────────────────────
   Trois essais, et c'est le plus simple qui gagne — mais il fallait les deux
   autres pour le savoir.

   Ligne de pied commune : typographiquement juste, et bancal ici. Le corps du
   logotype fait 13 px contre 8,4 px de capitale ; posés sur le même pied, le
   logo monte d'autant plus haut, et le couple entier penche vers le haut de la
   barre.

   Ce que l'œil cherche entre deux mots de TAILLES très différentes n'est pas
   leur pied mais leur MILIEU. Or les deux bandes de lettres sont déjà centrées
   dans leur propre boîte — le corps du logotype occupe les rangées 50 à 149 sur
   200, donc pile au milieu, et les capitales sont au milieu de leur ligne. Les
   centrer l'une sur l'autre revient donc à ne rien décaler du tout : `align-
   items: center` fait tout le travail, et les deux transformations que j'avais
   posées ne servaient qu'à défaire ce qu'il faisait bien. Mesuré : milieux à
   22,4 et 22,3, pour un axe de barre à 22,5. */
.side__brand img { height: 26px; opacity: .8; }
.side__brand span {
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--texte-fort);
}

.side__nav { flex: 1 1 auto; padding: 14px 0; overflow-y: auto; }

/* Le nom de catégorie est GRAVÉ, pas titré : il classe, il ne s'annonce pas. */
.side__group {
  margin: 16px 16px 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--texte-tenu);
}
.side__group:first-child { margin-top: 0; }

.side__link {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 16px;
  color: var(--texte-faible);
  font-size: 14px;
  text-decoration: none;
  transition: background .12s ease, color .12s ease;
}
.side__link i { width: 15px; font-size: 13px; text-align: center; }
.side__link:hover { background: rgba(255, 255, 255, .035); color: var(--texte-fort); }

/* L'entrée courante porte un LISERÉ, pas un fond plein : sur un menu sombre,
   un pavé coloré pèse plus que le contenu qu'il désigne. */
.side__link.is-on {
  color: var(--texte-fort);
  background: rgba(63, 200, 240, .07);
}
.side__link.is-on::before {
  content: "";
  position: absolute;
  left: 0; top: 6px; bottom: 6px;
  width: 2px;
  background: var(--info);
}
.side__link.is-on i { color: var(--info); }

.side__foot {
  padding: 10px 16px;
  border-top: 1px solid var(--filet);
  font-size: 11px;
  letter-spacing: .08em;
  color: var(--texte-tenu);
}
.side__foot i { margin-right: 5px; font-size: 11px; }
.side__foot em {
  margin-left: 6px;
  padding: 1px 5px;
  border: 1px solid var(--caution-bord);
  border-radius: 2px;
  color: var(--caution);
  font-style: normal;
  font-size: 11px;
  text-transform: uppercase;
}

/* ── Barre supérieure ─────────────────────────────────────────────────────── */

.main { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; min-height: 0; }

.topbar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 12px;
  height: 46px;
  padding: 0 14px;
  background: var(--surface);
  border-bottom: 1px solid var(--filet);
}
.topbar__title {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 700;
  color: var(--texte-fort);
}
.topbar__spacer { flex: 1 1 auto; }

/* Le filet qui sépare les OUTILS de l'IDENTITÉ, à la droite de la barre. Sa
   hauteur est celle d'un texte, pas celle de la barre : un trait pleine
   hauteur découperait le bandeau en deux au lieu de séparer deux voisins. */
.topbar__sep {
  flex: 0 0 auto;
  width: 1px;
  height: 20px;
  /* DEUX PIXELS VERS LA GAUCHE, ET ILS SE MESURENT. Le témoin est une cible de
     34 px autour d'une icône de 16 : neuf pixels d'air de chaque côté, que
     l'œil compte comme du vide. Le bouton du compte n'en porte que quatre.
     Posé au milieu géométrique, le filet se trouvait donc à 21 px de l'icône et
     à 17 px de l'avatar — c'est la distance aux MARQUES VISIBLES qui doit être
     égale, jamais celle aux boîtes. */
  margin-left: -2px;
  background: var(--bord);
}
.topbar__burger {
  display: none;
  padding: 6px 8px;
  border: 0;
  background: none;
  color: var(--texte-faible);
  cursor: pointer;
}

/* ── Menu du compte ───────────────────────────────────────────────────────── */

.usermenu { position: relative; }

.usermenu__button {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 4px 8px 4px 4px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
.usermenu__button:hover { border-color: var(--bord-fort); background: var(--surface-vive); }
.usermenu__button i { font-size: 11px; color: var(--texte-tenu); }

.avatar {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid var(--bord-fort);
  background: var(--filet);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 700;
  color: var(--texte-fort);
}

.usermenu__who { text-align: left; line-height: 1.25; }
.usermenu__who b { display: block; font-size: 13px; color: var(--texte-fort); }
.usermenu__who k {
  font-size: 11px;
  font-weight: 400;
  color: var(--info);
}

.usermenu__panel {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  z-index: 60;
  min-width: 236px;
  padding: 5px 0;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .7);
}
.usermenu__head {
  padding: 9px 14px 10px;
  border-bottom: 1px solid var(--filet);
  margin-bottom: 5px;
}
.usermenu__head b { display: block; font-size: 14px; color: var(--texte-fort); }
.usermenu__head k { font-size: 12px; color: var(--texte-tenu); }

.usermenu__item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 14px;
  border: 0;
  background: none;
  color: var(--texte-faible);
  font: inherit;
  font-size: 14px;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.usermenu__item i { width: 14px; font-size: 13px; color: var(--texte-tenu); }
.usermenu__item:hover { background: rgba(255, 255, 255, .04); color: var(--texte-fort); }
.usermenu__item--out { color: var(--texte-alerte); }
.usermenu__item--out:hover { background: rgba(255, 59, 48, .1); color: var(--texte-alerte); }
.usermenu__sep { height: 1px; margin: 5px 0; background: var(--filet); }

/* DEUX RÉGLAGES VOISINS ONT BESOIN QU'ON LES DISTINGUE. Le thème et la palette
   des instruments se ressemblent — mêmes boutons, mêmes icônes rondes — et
   posés l'un sous l'autre sans un mot, on ne sait plus lequel habille quoi.
   Un intitulé sur le second suffit : le premier, en tête du groupe, se lit de
   lui-même. */
.usermenu__titre {
  display: block;
  padding: 4px 10px 2px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--texte-tenu);
}

/* ── LE CHOIX DU THÈME, DANS LE MENU DU COMPTE ───────────────────────────────
   Trois boutons côte à côte plutôt qu'une bascule : une bascule à deux états ne
   peut pas dire « suis le système », et c'est précisément l'état qu'on veut par
   défaut sur une machine qui s'assombrit le soir toute seule.

   Ils sont SERRÉS et sans bordure propre : ce n'est pas une action du menu,
   c'est un réglage posé dedans — leur donner l'apparence des lignes au-dessus
   les ferait lire comme « aller quelque part ». */
.usermenu__theme { display: flex; gap: 4px; padding: 6px 10px; }
.usermenu__theme-b {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 7px 4px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  color: var(--texte-tenu);
  font: inherit;
  font-size: 11px;
  letter-spacing: .04em;
  cursor: pointer;
  transition: color .12s ease, background .12s ease, border-color .12s ease;
}
.usermenu__theme-b i { font-size: 14px; }
.usermenu__theme-b:hover { color: var(--texte); background: var(--surface-vive); }
.usermenu__theme-b.is-on {
  border-color: var(--info);
  color: var(--info);
  background: var(--surface-vive);
}

/* ── Étroit ───────────────────────────────────────────────────────────────── */

@media (max-width: 820px) {
  .side {
    position: fixed;
    inset: 0 auto 0 0;
    z-index: 70;
    transform: translateX(-100%);
    transition: transform .18s ease;
  }
  .side.is-open { transform: none; }
  .topbar__burger { display: block; }
  .usermenu__who { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .side, .side__link, .usermenu__button { transition: none; }
}

/* ── Le site ──────────────────────────────────────────────────────────────────
   La page d'où l'on choisit un tableau. Elle emprunte la matière du panneau —
   les tuiles sont des touches de sélection, comme celles qui appellent une page
   système sur un ECAM — sans en imiter les instruments : on choisit ici, on
   mesure là-bas. */

.site {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* La barre de défilement prend sa place une fois pour toutes : sans cela, un
     écran qui passe de « tout tient » à « ça déborde » se décale de sa largeur
     au moment précis où l'on cherche à lire. */
  scrollbar-gutter: stable;
  /* NOMMÉE parce qu'une seconde règle en dépend : un en-tête de tableau collant
     se cale contre la boîte de REMBOURRAGE de ce conteneur, et doit remonter
     d'exactement cette valeur pour venir au ras du bord. Voir `.poolkeys__t th`.
     Deux nombres égaux écrits deux fois auraient fini par diverger. */
  --site-marge: 44px;
  padding: var(--site-marge) 24px;
  display: flex;
  justify-content: center;
}

/* SEUL LE CONTENU DE L'ONGLET DÉFILE.
 *
 * Sur un écran qui présente une liste — le catalogue d'instruments — faire
 * défiler la page entière emporte le titre, les onglets et la recherche. On
 * remonte alors chercher le champ de recherche, on redescend pour voir ce
 * qu'il a filtré. Ces trois-là sont des repères : ils restent, et c'est la
 * liste qui bouge sous eux. */
.site--frame { overflow: hidden; }
.site--frame .site__inner {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.site--frame .lib {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  scrollbar-gutter: stable;
  padding-bottom: 16px;
}

/* ── LES ÉCRANS PRENNENT TOUTE LA LARGEUR ────────────────────────────────────
   Ils étaient bridés à 860 pixels, la bonne mesure pour du TEXTE — au-delà,
   l'œil peine à retrouver le début de la ligne suivante. Mais ces écrans ne
   racontent pas : ils ALIGNENT. Une liste de raccordements, une grille
   d'instruments, un journal où l'heure est à gauche et le détail à droite. Les
   brider, c'était laisser la moitié d'un écran large vide tout en tronquant le
   message d'erreur qu'on venait lire.

   LA MESURE DU TEXTE EST DÉPLACÉE, PAS SUPPRIMÉE : elle passe du conteneur aux
   paragraphes, seuls concernés. C'est la même règle qu'avant, appliquée à ce
   qui en avait besoin plutôt qu'à tout ce qui se trouvait autour. */
.site__inner { width: 100%; max-width: none; }

/* Un paragraphe d'explication garde une longueur de ligne lisible, où qu'il
   soit posé. `ch` et non des pixels : la mesure suit la police. */
.site__hint, .site__none { max-width: 100ch; }

.site h1 {
  margin: 0 0 22px;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 700;
  color: var(--texte-faible);
}

.site__empty { margin: 0 0 8px; color: var(--texte-fort); }
.site__hint { margin: 0; color: var(--texte-tenu); font-size: 14px; }
/* ── CE QUI EXPLIQUE SE DÉTACHE DE CE QU'IL EXPLIQUE ─────────────────────────
   `.site__hint` naît sans marge, parce qu'il se pose à des endroits trop
   différents pour qu'une valeur convienne partout. Dans un panneau d'onglet,
   en revanche, il est toujours la même chose : la phrase qui annonce ce qui
   suit. Chaque vue posait donc sa propre marge en `style=`, ce qui marchait —
   jusqu'à ce qu'un `style="margin:0 0 14px"` remette aussi les côtés à zéro et
   décale le paragraphe par rapport aux champs qu'il annonce. La règle est donc
   ici, une fois, et les vues n'ont plus à s'en occuper. */
[data-panel] > .site__hint { margin-bottom: 14px; }

/* ── UN PLANCHER D'INTERLIGNE POUR TOUT UN PANNEAU ───────────────────────────

   Chaque bloc portait sa propre marge basse, et le compte tombait juste tant
   qu'on n'ajoutait rien. Puis un titre de section, un bandeau de tuiles et un
   tableau sont arrivés sans marge à eux : trois endroits où deux blocs se
   touchaient — mesuré sur `/tsdata`, `H3 → P`, `.dcfstat → tableau` et, dans le
   catalogue, la ligne d'état collée à la liste.

   Poser la marge manquante au cas par cas aurait rouvert le même trou au bloc
   suivant. Cette règle donne un MINIMUM à tout enfant direct de panneau, et
   comme deux marges verticales voisines se fondent en prenant la plus grande,
   elle n'ajoute rien là où une marge existe déjà : les 14 px sous une phrase
   d'annonce restent 14. Elle ne comble que les trous.

   Elle tient parce qu'un panneau est un bloc sans rembourrage — c'est la
   condition de la fusion des marges. En faire une grille ou une boîte flexible
   la romprait en silence, et les marges s'additionneraient.

   ── L'ATTRIBUT EST DOUBLÉ, ET CE N'EST PAS UNE COQUETTERIE ──────────────────

   `.cards { margin: 0 }` et `.hint { margin: 0 }` sont des remises à zéro de
   COMPOSANT : elles existent parce que ces blocs se posent partout, et qu'aucune
   marge ne conviendrait partout. À spécificité égale, c'est l'ordre du fichier
   qui tranche — elles sont plus bas, elles gagnaient, et la liste du catalogue
   restait collée à sa ligne d'état malgré la règle.

   Doubler l'attribut dit ce qu'on veut dire : dans un panneau, c'est le
   CONTENEUR qui décide du rythme, pas le composant qui décide de n'en avoir
   aucun. Le composant garde sa remise à zéro partout ailleurs. */
[data-panel][data-panel] > * + * { margin-top: 12px; }
.site__hint code {
  padding: 2px 6px;
  border-radius: 3px;
  background: var(--creux);
  color: var(--info);
}

.site__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 10px;
}

.tile {
  position: relative;
  display: block;
  padding: 16px 40px 15px 16px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--surface);
  text-decoration: none;
  transition: border-color .12s ease;
}
.tile:hover { border-color: var(--info); }
.tile:focus-visible { outline: 2px solid var(--info); outline-offset: 2px; }

.tile b {
  display: block;
  margin-bottom: 4px;
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .04em;
  line-height: 1.3;
  color: var(--texte-fort);
}
/* Même couple que sur une carte, donc même hiérarchie : le titre porte le gras,
   la phrase qui l'explique ne le porte plus. */
.tile k {
  display: block;
  font-size: 11.5px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--texte-faible);
}
.tile i {
  position: absolute;
  right: 16px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 12px;
  color: var(--texte-tenu);
}
.tile:hover i { color: var(--info); }

.site__none { margin: 14px 0 0; color: var(--texte-tenu); font-size: 14px; }

/* ── Titre de section et actions ──────────────────────────────────────────── */

.site__head {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 18px;
}
.site__head h1 { margin: 0; flex: 1 1 auto; }

.btn--go { border-color: var(--info-bord); color: var(--info); }
.btn--go:hover { border-color: var(--info); }
/* UN BOUTON QUI RESTE ENFONCÉ. La trace complète coule tant qu'on ne l'arrête
   pas : le bouton doit le montrer, sinon on la croit refermée. */
.btn--go.is-on {
  border-color: var(--caution);
  background: rgba(240, 192, 96, .12);
  color: var(--caution);
}
.btn--go i { margin-right: 6px; font-size: 11px; }

/* ── Recherche et filtres ─────────────────────────────────────────────────── */

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
}

.search {
  position: relative;
  display: block;
  flex: 1 1 240px;
  max-width: 340px;
}
.search i {
  position: absolute;
  left: 11px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 12px;
  color: var(--texte-tenu);
  pointer-events: none;
}
.search input {
  width: 100%;
  padding: 8px 12px 8px 30px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--creux);
  color: var(--texte-fort);
  font: inherit;
  font-size: 14px;
}
.search input:focus {
  outline: none;
  border-color: var(--info);
  box-shadow: 0 0 0 3px rgba(63, 200, 240, .12);
}

/* ── UN INTERRUPTEUR POSÉ SUR UNE LIGNE ──────────────────────────────────────
   `.switch` porte `margin: 8px 0 0`, ce qui est juste dans un formulaire
   empilé — il y respire sous le champ qui le précède. Sur une RANGÉE, cette
   même marge le fait descendre de ses voisins : mesuré à 3,5 pixels plus bas
   que les boutons d'à côté. Encore une règle générale appliquée hors de son
   cas, comme `.btn { width: 100% }` avant elle. */
.filters .switch { margin: 0; }

/* LES PASTILLES ONT DÉMÉNAGÉ dans `controls.css` : la palette d'insertion et le
   moniteur réseau s'en servent DANS un cockpit, qui ne charge pas cette
   feuille. Elles y arrivaient sans le moindre style. */

/* L'ONGLET QUI DOIT ATTIRER L'ŒIL DEPUIS UN AUTRE ONGLET. Réservé aux
   anomalies, et seulement quand il y en a : un onglet toujours allumé
   n'alerte plus personne. */
.tab--hot em { border-color: var(--alerte); color: var(--texte-alerte); }

/* ── L'état du battement, au-dessus des sauvegardes ──────────────────────────
   Une sauvegarde automatique réglée SANS battement ne se fait pas. C'est le
   genre de protection qu'on croit en place et qui n'y est pas — pire que pas
   de protection du tout, puisqu'on cesse d'y penser. La ligne le dit donc en
   toutes lettres, et en rouge quand c'est le cas. */
.tickstate {
  margin: 0 0 14px;
  padding: 10px 13px;
  border: 1px solid var(--bord);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--creux);
  font-size: 13px;
  line-height: 1.5;
  color: var(--texte-faible);
}
.tickstate i { margin-right: 7px; font-size: 12px; }
.tickstate.is-ok  { border-left-color: var(--ok); }
.tickstate.is-ok i { color: var(--ok); }
.tickstate.is-bad { border-left-color: var(--alerte); color: var(--texte-alerte); }
.tickstate.is-bad i { color: var(--alerte); }
.tickstate code {
  padding: 1px 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, .06);
  font-size: 12px;
}

/* ── Éprouver le relais de courriel ──────────────────────────────────────────
   Les étapes s'empilent et gardent leur verdict : on lit d'un coup d'œil
   jusqu'où l'on est allé. C'est le contraire d'un « ça ne marche pas », qui
   laisse reprendre quatre réglages au hasard. */
.mailtest { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--bord); }
.mailtest__row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.mailtest__row input {
  flex: 1 1 260px;
  max-width: 340px;
  padding: 8px 12px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--creux);
  color: var(--texte-fort);
  font: inherit;
  font-size: 14px;
}
.mailtest__row input:focus {
  outline: none;
  border-color: var(--info);
  box-shadow: 0 0 0 3px rgba(63, 200, 240, .12);
}

.mailtest__steps { margin: 14px 0 0; padding: 0; list-style: none; }
.mailtest__s {
  display: grid;
  grid-template-columns: 16px minmax(0, auto) minmax(0, 1fr);
  align-items: baseline;
  gap: 9px;
  padding: 5px 0;
  font-size: 13px;
}
.mailtest__s i { font-size: 11px; }
.mailtest__s b { font-weight: 500; color: var(--texte); }
.mailtest__s k { color: var(--texte-tenu); font-size: 12px; }
.mailtest__s.is-ok i  { color: var(--ok); }
.mailtest__s.is-bad i { color: var(--alerte); }
.mailtest__s.is-bad b { color: var(--texte-alerte); }
/* UNE ÉTAPE SANS VERDICT — « aucune autorité déclarée », « la réponse n'est pas
   du JSON », le conseil qui suit un échec. Ni verte ni rouge : un constat peint
   en rouge fait chercher une panne là où il n'y a qu'un renseignement. Son
   texte n'a pas de libellé à sa gauche, il occupe donc les deux colonnes. */
.mailtest__s > k:nth-child(2) { grid-column: 2 / -1; }

/* ── LA FORME D'UNE RÉPONSE DE SERVICE ───────────────────────────────────────
   Chasse fixe et colonnes alignées : on vient y LIRE des noms de champs pour
   les recopier dans un décodeur, et deux chemins qui ne commencent pas à la
   même colonne ne se comparent pas d'un regard. Elle défile plutôt que de
   pousser la page : une réponse de service peut faire quelques milliers de
   caractères, et ce n'est pas une raison pour perdre les champs du dessus. */
/* ── CE QUE LES POOLS COÛTENT, EN QUELQUES NOMBRES ───────────────────────────
   Des tuiles, et non un tableau : on ne COMPARE pas ces valeurs entre elles —
   des clés, des millisecondes, un pourcentage et un âge n'ont pas d'unité
   commune. On les lit une par une, et une grille qui se replie les garde
   lisibles sur un écran étroit comme sur un mur. */
.dcfstat {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 1px;
  background: var(--bord);
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  overflow: hidden;
}
.dcfstat__c { padding: 12px 14px; background: var(--surface); }
.dcfstat__c b {
  display: block;
  font-size: 19px;
  font-weight: 500;
  color: var(--texte-fort);
  font-variant-numeric: tabular-nums;
}
.dcfstat__c k { display: block; margin-top: 2px; font-size: 12px; color: var(--texte-tenu); }
/* La couleur ne dit qu'une chose : que ce nombre-là demande qu'on s'en occupe. */
.dcfstat__c.is-warn b { color: var(--caution); }
.dcfstat__c.is-bad b  { color: var(--alerte); }

/* ── LA SOLLICITATION D'UN POOL ──────────────────────────────────────────────
   Une seule série, donc pas de légende : le titre de la carte nomme déjà le
   pool. Le trait porte la teinte d'information ; les CHIFFRES restent en encre,
   jamais en couleur de série — la couleur dit l'identité, pas la valeur.

   Marque fine (2px) et aire très discrète : l'aire donne le volume d'un coup
   d'œil, le trait porte la forme, et l'une ne doit pas manger l'autre. */
.spark { display: inline-flex; align-items: center; gap: 9px; }
.spark svg { display: block; overflow: visible; }

/* ── LE CADRAN DE LA MICRO-TRACE ─────────────────────────────────────────────

   Une vignette sans fond disparaît quand la courbe est plate : à zéro demande
   et zéro appel, les deux traits se couchent au bas du cadre et la case paraît
   VIDE. On ne sait alors plus si rien ne s'est passé ou si l'instrument est
   éteint — deux états qui ne se ressemblent en rien.

   Le creux et les filets rendent le cadran visible sans rien ajouter à lire :
   on voit l'instrument, donc on comprend que l'aiguille est à zéro. `--creux`
   est la surface en retrait de la maison — celle des champs de saisie — et elle
   se renverse toute seule entre le thème sombre et le clair.

   TRÈS PEU DE CONTRASTE, et c'est la condition : la grille est un repère, pas
   une donnée. Si elle se remarque, elle concurrence les deux traits qui, eux,
   portent le renseignement. */
.spark__fond { fill: var(--creux); }
.spark__grille {
  fill: none;
  stroke: var(--bord);
  stroke-width: .5;
  opacity: .5;
}
.spark__l {
  fill: none;
  stroke: var(--info);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.spark__a { fill: var(--info); opacity: .10; stroke: none; }

/* LA DEMANDE DES COCKPITS — un trait d'encre, sans aire.
   Il BORNE le coût sans lui disputer l'œil : c'est l'écart entre les deux qui
   porte le renseignement (« quarante demandes, quatre appels »), et une
   seconde teinte vive ferait deux séries qui se concurrencent là où l'une
   n'est que la toise de l'autre. */
.spark__l--cli { stroke: var(--texte-tenu); stroke-width: 1.5; opacity: .55; }

/* LES PASTILLES DE LÉGENDE — ce qui rattache un nombre à son trait sans
   colorer le nombre lui-même : la couleur dit l'identité, pas la valeur. */
.spark__key {
  display: inline-block;
  width: 8px; height: 2px;
  border-radius: 1px;
  vertical-align: middle;
  margin: 0 4px 1px 0;
  background: var(--info);
}
.spark__key--cli { background: var(--texte-tenu); opacity: .55; }
/* DEUX LIGNES, ET NON UNE — la légende porte deux séries, et les mettre bout à
   bout pousserait les boutons hors de la carte sur un écran étroit. Empilées,
   elles tiennent exactement la hauteur de la courbe : rien ne s'allonge, et
   chaque nombre est en face de son trait.

   ── ELLE PASSE DEVANT LA COURBE, ET C'EST UNE QUESTION D'ALIGNEMENT ────────

   La légende était posée ENTRE la courbe et les boutons. Or sa largeur dépend
   des nombres qu'elle porte — « 0 appels/min » et « 1 254 appels/min » ne font
   pas la même longueur — tandis que la courbe, elle, fait toujours 120 px. Un
   bloc élastique coincé entre deux blocs fixes déplace donc la courbe d'une
   carte à l'autre, et deux micro-traces voisines ne se comparent plus : c'est
   exactement ce qu'on vient leur demander.

   Remise devant, la légende absorbe la variation À GAUCHE, contre le vide de la
   rangée. La courbe retrouve une colonne stable juste avant les boutons, qui
   restent au bord. On lit alors dans l'ordre : ce que ça vaut, la forme que ça
   prend, ce qu'on peut en faire.

   Le `min-width` achève le travail pour les petits nombres : sans lui, une
   rangée à zéro laisserait la courbe glisser de deux centimètres vers la
   gauche par rapport à sa voisine. Il ne borne rien — un nombre plus long
   pousse simplement le bloc. */
.spark k {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 15ch;
  line-height: 1.3;
  font-size: 12px;
  color: var(--texte-tenu);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.spark k i { font-style: normal; opacity: .6; }
.spark--vide { color: var(--texte-tenu); opacity: .5; }

/* ── LE CONTENU D'UN POOL, DÉPLIÉ ────────────────────────────────────────────
   Un tableau, et non des cartes : ce sont les mêmes colonnes répétées, et l'œil
   descend le long des âges pour s'arrêter sur ce qui dépasse. Il défile plutôt
   que de pousser la page — un pool peut porter des milliers de clés. */
/* ── PAS DE MARGE EN HAUT D'UN TIROIR QUI DÉFILE ────────────────────────────
   `.card__drawer` en pose douze pixels, et un en-tête `sticky` s'arrête SOUS
   la marge du conteneur qui défile : il restait donc une bande de treize
   pixels, entre le bord et l'en-tête, où les lignes passaient derrière lui en
   défilant. On voyait un repère et des chiffres traverser les titres de
   colonnes — ce qui se lit comme un défaut d'affichage, et qui en était un.

   La respiration vient alors du rembourrage des cellules d'en-tête, qui
   suffit : le titre de colonne se pose sur le bord du tiroir, et rien ne
   glisse au-dessus. */
/* ── UN TIROIR DÉPLIÉ MONTRE TOUT ────────────────────────────────────────────
   Il bornait sa hauteur à 420 px et défilait pour lui-même. Deux défilements
   emboîtés, c'est déjà un de trop : on cherche une clé dans huit cents, la
   molette agit sur le cadre ou sur la page selon l'endroit du curseur, et l'on
   ne sait plus où l'on en est. Déplier veut dire montrer.

   La conséquence assumée : le tiroir du site 266 fait 40 000 pixels de haut.
   C'est long, et c'est ce qu'on a demandé — la page défile, ce qui est le seul
   défilement dont on a vraiment l'habitude. */
.poolkeys { padding-left: 14px; }
/* Qualifié par les DEUX classes : `.card__drawer` pose son rembourrage en
   raccourci, plus bas dans ce fichier, et à spécificité égale c'est l'ordre qui
   tranche — une longhand isolée se faisait écraser sans bruit. */
.card__drawer.poolkeys { padding-top: 0; }
/* La respiration perdue en haut revient DANS l'en-tête, qui est opaque : elle
   se voit pareil, et plus rien ne glisse derrière. */
.poolkeys__t thead th { padding-top: 14px; }
.poolkeys__t { width: 100%; border-collapse: collapse; font-size: 13px; }
/* ── L'EN-TÊTE SE CALE CONTRE `main.site`, LE VRAI CONTENEUR QUI DÉFILE ──────

   Depuis que le tiroir ne défile plus pour lui-même, `sticky` ne se résout plus
   contre lui mais contre le premier ancêtre qui défile — `main.site`. C'est une
   bonne nouvelle : les titres de colonnes restent visibles sur huit cents
   lignes, ce qu'un tableau simplement long ne permettait pas.

   Mais `sticky` se cale contre la boîte de REMBOURRAGE, et `.site` en pose
   quarante-quatre pixels : à `top: 0`, l'en-tête s'immobilisait quarante-quatre
   pixels trop bas, et les lignes défilaient dans la bande au-dessus de lui. On
   le fait donc remonter d'autant — il vient alors au ras du bord, et plus rien
   ne passe derrière. Même défaut, même raisonnement que la correction faite
   jadis à l'intérieur du tiroir ; seul le conteneur a changé.

   La barre du haut de l'application est `static` : elle s'en va avec le
   contenu, et il n'y a donc rien à éviter au-dessus. */
.poolkeys__t th {
  position: sticky;
  top: calc(-1 * var(--site-marge, 44px));
  z-index: 1;
  padding: 6px 10px;
  text-align: left;
  font-weight: 500;
  color: var(--texte-tenu);
  background: var(--surface);
  border-bottom: 1px solid var(--bord);
}
.poolkeys__t td {
  padding: 5px 10px;
  border-bottom: 1px solid var(--filet);
  color: var(--texte);
  vertical-align: baseline;
}
.poolkeys__t tr:last-child td { border-bottom: 0; }
/* LES NOMBRES S'ALIGNENT À DROITE ET EN CHASSE TABULAIRE : deux valeurs qui ne
   commencent pas à la même colonne ne se comparent pas d'un regard. */
.poolkeys__t .num { text-align: right; font-variant-numeric: tabular-nums; }
.poolkeys__t .num.is-warn { color: var(--caution); }
/* Un point qui mange la moitié du budget du tour le prend aux autres : il se
   lit comme une anomalie, pas comme une réserve. */
.poolkeys__t .num.is-bad { color: var(--alerte); }
.poolkeys__t code { font-size: 12px; color: var(--texte-tenu); }
/* LE REPÈRE PASSE DEVANT LA CLÉ — c'est le nom qu'on reconnaît, et il se lit
   donc en encre pleine ; la clé reste à côté, pâle, parce qu'elle sert à
   copier et à chercher, pas à identifier d'un coup d'œil. */
.poolkeys__t code.is-name { color: var(--texte); }
.poolkeys__t k { display: block; margin-top: 2px; font-size: 12px; color: var(--texte-tenu); }
/* La phrase du dictionnaire, sous sa clé. Un peu plus claire que le motif
   d'erreur qui partage la même balise dans la colonne d'état : l'un dit ce que
   le point EST, l'autre pourquoi il ne répond pas, et l'œil doit pouvoir les
   distinguer sans les lire. Bornée en largeur pour ne pas étirer la colonne. */
.poolkeys__t .poolkeys__dit {
  max-width: 34ch;
  color: var(--texte-faible);
  font-family: var(--font-ui);
  line-height: 1.35;
}
.poolkeys__t .tag--bad { color: var(--texte-alerte); }
.poolkeys__t .tag--warn { color: var(--caution); }

.dcfshape {
  margin: 14px 0 0;
  padding: 12px 14px;
  max-height: 340px;
  overflow: auto;
  border: 1px solid var(--bord);
  border-radius: 6px;
  background: rgba(0, 0, 0, .22);
  color: var(--texte-tenu);
  font: 12px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
  /* ── CE QUI NE SE FORMATE PAS SE REPLIE ────────────────────────────────
     Le corps brut est mis en forme dès qu'il se décode — c'est le cas
     ordinaire. Mais la sonde TRONQUE à quatre mille caractères, et un JSON
     coupé ne se décode plus : il repartait alors sur une seule ligne de
     plusieurs milliers de caractères, qu'il fallait faire défiler à
     l'horizontale pour ne rien y voir. `pre-wrap` le replie ; `anywhere`
     permet de couper au milieu d'une clé, qui n'a pas d'espace où se
     rompre. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  tab-size: 2;
}

/* ── L'AVANCEMENT D'UN IMPORT ────────────────────────────────────────────────
   Un panneau qui se lit de haut en bas : le titre et le pour-cent, la barre,
   puis le détail. Il vit dans la PAGE et non dans un dialogue — un import de
   site dure, et un modal qui resterait ouvert tout ce temps interdirait à la
   fois de regarder ailleurs et de s'arrêter. */
.tsimport {
  margin: 0 0 12px;
  padding: 12px 14px;
  border: 1px solid var(--bord);
  border-radius: 8px;
  background: var(--surface);
}
.tsimport__tete {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}
.tsimport__tete b { font-size: 13px; }
/* LE POUR-CENT CONTRE LA BARRE, et non au milieu d'une phrase : l'œil cherche
   le chiffre là où il voit le trait. En chasse tabulaire, sans quoi il danse à
   chaque dizaine franchie. */
.tsimport__part {
  margin-left: auto;
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  color: var(--info);
}
.tsimport__barre {
  height: 6px;
  border-radius: 3px;
  background: var(--filet);
  overflow: hidden;
}
.tsimport__barre i {
  display: block;
  width: 0;
  height: 100%;
  background: var(--info);
  /* La barre AVANCE sous les yeux plutôt que de sauter d'un lot à l'autre :
     un saut se lit comme un à-coup, une progression comme un travail. */
  transition: width .25s ease;
}
.tsimport .hint { margin: 8px 0 0; }
/* Le nom de la série en cours, plus discret que les comptes : il RENSEIGNE, il
   ne se surveille pas. */
.tsimport__courant {
  margin-top: 2px !important;
  font-size: 12px;
  opacity: .7;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── UN BOUTON DE SECOND RANG ────────────────────────────────────────────────
   Arrêter un import n'est pas un geste dangereux : rien ne se perd, ce qui est
   pris reste. Le rouge du danger l'aurait fait peser plus lourd que le travail
   qu'il interrompt — et un bouton qu'on hésite à toucher est un bouton qui ne
   sert pas. */
.btn--petit {
  padding: 5px 10px;
  font-size: 12px;
}

/* ── Le journal du système ────────────────────────────────────────────────────
   UNE LIGNE PAR ÉVÉNEMENT, en chasse fixe, et l'heure toujours à la même
   colonne. C'est la seule mise en page qui permette de PARCOURIR : l'œil
   descend le long des heures et s'arrête sur ce qui dépasse. Des cartes
   empilées, si soignées soient-elles, obligeraient à lire chaque bloc.

   La couleur ne dit qu'une chose — le niveau — et le rouge reste réservé à ce
   qui a échoué, comme partout ailleurs dans cette maison. */
.log {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--fond-creux);
  overflow: hidden;
}

.log__l {
  display: grid;
  /* L'heure et l'icône à largeur FIXE : ce sont les deux repères verticaux qui
     rendent la liste parcourable. Le reste prend ce qui reste. */
  grid-template-columns: 74px 16px minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 10px;
  padding: 5px 12px;
  border-top: 1px solid var(--surface-vive);
  font-size: 13px;
  line-height: 1.45;
}
.log__l:first-child { border-top: 0; }

.log__l time {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--texte-tenu);
  font-variant-numeric: tabular-nums;
}
.log__l i { font-size: 11px; color: var(--texte-tenu); }
.log__l b { font-weight: 500; color: var(--texte); }

/* LE DÉTAIL S'EFFACE SANS DISPARAÎTRE : on ne le lit que lorsqu'on s'arrête sur
   une ligne, et il ne doit pas concurrencer ce qui s'est passé. */
.log__l k {
  justify-self: end;
  text-align: right;
  font-size: 12px;
  color: var(--texte-tenu);
}

/* ── UNE LIGNE QUI EN DIT PLUS ───────────────────────────────────────────
   Seules les erreurs se déplient : elles seules portent une pile. Le curseur
   l'annonce, et la ligne ouverte laisse la place au rapport — qui garde sa
   chasse fixe et ses retours à la ligne, parce que c'est ainsi qu'une pile se
   lit et se recopie. */
.log__l--plus { cursor: pointer; }
.log__l--plus:hover { background: rgba(255, 255, 255, .03); }
.log__l--plus.is-open { display: block; padding-bottom: 12px; }
.log__l--plus.is-open time,
.log__l--plus.is-open i,
.log__l--plus.is-open b,
.log__l--plus.is-open k { display: inline; margin-right: 8px; }

.log__plus { margin-top: 10px; }
.log__trace {
  margin: 0;
  padding: 11px 13px;
  max-height: 340px;
  overflow: auto;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--fond);
  color: var(--texte);
  font: 12px/1.6 var(--font-mono);
  white-space: pre-wrap;
  word-break: break-word;
  /* ON PEUT LE SÉLECTIONNER, même si le bouton existe : le presse-papiers est
     refusé hors contexte sûr, et le geste manuel doit rester possible. */
  user-select: text;
}
.log__copier { margin-top: 9px; }

.log__l--ok b    { color: var(--ok); }
.log__l--warn b  { color: var(--caution); }
.log__l--error   { background: rgba(255, 90, 80, .05); }
.log__l--error b { color: var(--texte-alerte); }
.log__l--error i { color: var(--alerte); }

/* Sur un écran étroit, le détail passe DESSOUS plutôt que d'écraser le texte :
   c'est ce qui s'est passé qu'on veut lire d'abord. */
@media (max-width: 720px) {
  .log__l {
    grid-template-columns: 62px 16px minmax(0, 1fr);
  }
  .log__l k { grid-column: 3; justify-self: start; text-align: left; }
}

.site__headnote.is-bad  { color: var(--texte-alerte); }
.site__headnote.is-warn { color: var(--caution); }

/* ── Les listes ───────────────────────────────────────────────────────────────
   À PLAT. Le relief — dégradé zénithal, arêtes internes, ombre portée —
   appartient au panneau d'instruments, où il dit une chose vraie : que les
   widgets sont des pièces encastrées. Ici il ne dirait rien, sinon que
   l'interface imite le cockpit sans en être un. Une surface, un liseré, et le
   cyan pour ce qu'on désigne. */

.cards { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

/* ── DEUX COLONNES, SUR LES LISTES QUI LE MÉRITENT ───────────────────────────
   Une liste de cockpits tient en deux lignes de texte par entrée, et sur un
   écran large elle laissait les deux tiers de la page vides : on descendait
   chercher en bas ce qui aurait pu tenir sous les yeux.

   DEUX, ET PAS `auto-fill` : une grille qui passe à trois puis quatre colonnes
   selon la fenêtre change de forme sous l'utilisateur, et l'on perd le repère
   visuel qu'on avait de « l'endroit où se trouve tel cockpit ». Deux colonnes
   au-delà du seuil, une en dessous, et rien entre les deux.

   LE SEUIL EST CELUI DU CONTENU, pas celui d'un appareil : une carte porte un
   titre, une ligne de caractéristiques qui se replie volontiers, et trois
   boutons. En dessous de ~520 px par colonne la ligne de caractéristiques
   passe à trois lignes et la carte devient plus haute que large — on aurait
   gagné une colonne pour perdre la lisibilité.

   C'est un MODIFICATEUR, et non le comportement de `.cards` : la même classe
   sert aux comptes, aux sites, aux webhooks, dont les cartes portent des
   contenus bien plus longs. */
@media (min-width: 1100px) {
  .cards--colonnes { grid-template-columns: 1fr 1fr; }
}

.card {
  position: relative;
  display: flex;
  align-items: stretch;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--surface);
  transition: border-color .12s ease, background .12s ease;
}
.card:hover { border-color: var(--bord-vif); background: var(--surface-vive); }


.card__open {
  flex: 1 1 auto;
  padding: 13px 12px 12px 14px;
  text-decoration: none;
  min-width: 0;
}
/* TROIS REGISTRES, TROIS POIDS. Une carte dit trois choses de rang inégal :
   ce que c'est (le titre), ce qu'il en est (les caractéristiques), et à quoi
   c'est attaché (le propriétaire, la machine, le cockpit affiché). Tant que le
   `k` était une GRAVURE — 9 px, capitales, interlettré — son gras était
   structurel : à cette taille-là, une capitale maigre se délite. La révision de
   la police a retiré les capitales et l'interlettrage sans retirer le gras, et
   la phrase secondaire s'est retrouvée au même poids que le titre : trois
   lignes égales, aucune hiérarchie, le regard ne sait plus par où entrer.

   Le gras part avec la gravure. Ce qui distingue désormais les trois registres,
   c'est la taille, le poids ET le gris — les trois ensemble, pour que la
   hiérarchie tienne encore quand l'écran est lu de biais ou en plein jour. */

/* L'ENFANT DIRECT, et non n'importe quel `b` : une phrase de caractéristiques
   met parfois un mot en avant — la dernière valeur reçue par un webhook, par
   exemple — et ce `b`-là héritait du titre, `display: block` compris. La valeur
   partait donc à la ligne en corps de titre, au milieu de sa propre phrase. */
.card__open > b {
  display: block;
  margin-bottom: 3px;
  font-family: var(--font-ui);
  font-size: 13.5px;
  font-weight: 700;
  letter-spacing: .04em;
  line-height: 1.3;
  color: var(--texte-fort);
}
.card__open k {
  display: block;
  font-size: 11.5px;
  font-weight: 400;
  line-height: 1.5;
  color: var(--texte-faible);
}

/* ── DES SEGMENTS, PAS UNE PHRASE ─────────────────────────────────────────────
   Une ligne de caractéristiques n'est pas une phrase : c'est une énumération de
   quatre ou cinq faits sans lien entre eux. Écrite comme une phrase — les mots
   à la queue leu leu, séparés par des points médians au milieu du texte — elle
   se lit comme un bloc gris qu'on parcourt en entier pour y trouver le seul
   fait qu'on cherchait. L'œil n'a aucune prise : rien ne dit où finit « Poste
   fixe » et où commence « paysage ».

   CHAQUE CARACTÉRISTIQUE EST DONC UN ÉLÉMENT, et la séparation vient du CSS.
   Un filet vertical plutôt qu'un point médian, avec de l'air de part et
   d'autre : le filet est plus discret qu'un point à hauteur de texte (il ne
   ressemble à aucune lettre, on ne le lit pas) et il est plus franc (il tranche
   la ligne sur toute la hauteur du texte, là où le point flotte au milieu).

   LE FILET RESTE COLLÉ AU SEGMENT QU'IL SUIT — il est engendré DANS le span,
   sans espace devant. Le seul point de coupure est l'espace entre deux spans :
   une ligne qui se replie tombe donc toujours entre deux caractéristiques
   entières, jamais entre un nombre et son unité. Un filet qui se retrouve en
   bout de ligne après un repli annonce la suite, exactement comme une virgule.

   UN SEGMENT NE SE COUPE JAMAIS : `nowrap`. « 48×25 cases » et « 87
   instruments » sont des faits indivisibles ; un nombre seul en fin de ligne ne
   veut rien dire. */

:is(.card__open, .viewer__main) k > span { white-space: nowrap; }
:is(.card__open, .viewer__main) k > span:not(:last-child)::after {
  content: '';
  display: inline-block;
  width: 1px;
  height: .9em;
  /* ── DE L'AIR AUTOUR DU FILET ──────────────────────────────────────────
     Huit pixels à 11,5 px de corps laissaient les faits se toucher : sur une
     ligne comme « 819 clés en cache sur 819 archivées │ 4 868 864
     enregistrements », on ne voyait plus du premier coup d'œil où finit un
     fait et où commence le suivant — le filet séparait, il n'aérait pas.

     Douze, et l'écart réel est de douze avant, quinze après : le blanc entre
     deux `span` s'y ajoute. Cette asymétrie ne se voit pas — un filet n'est
     pas une lettre — et la corriger coûterait un `font-size: 0` sur le
     conteneur, qui casserait le repli. */
  margin-left: 12px;
  margin-right: 12px;
  vertical-align: -.12em;
  background: var(--bord-fort);
}
/* Ce qui porte une VALEUR se détache du mot qui la nomme : dans une ligne de
   faits, c'est le chiffre qu'on vient chercher. */
:is(.card__open, .viewer__main) k > span em:not(.tag) {
  font-style: normal;
  font-weight: 600;
  color: var(--texte);
}
/* UNE MESURE RESTE DANS SA PHRASE. Ce qu'un webhook a reçu en dernier est la
   seule chose qu'on vient vraiment vérifier : elle s'appuie, mais elle ne
   quitte pas la ligne — sinon la phrase qui la porte (« °C · il y a 25 h »)
   se retrouve orpheline sous un nombre qui ne dit plus de quoi il parle. */
.card__open k b {
  display: inline;
  font-size: inherit;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--texte-fort);
}

.site__none { margin: 14px 0 0; color: var(--texte-tenu); font-size: 14px; }

@media (prefers-reduced-motion: reduce) {
  .card { transition: none; }
}

/* ── Étiquettes et propriétaire ───────────────────────────────────────────── */

.tag {
  display: inline-block;
  margin-left: 7px;
  padding: 1px 6px;
  border: 1px solid var(--bord-fort);
  border-radius: 2px;
  font-family: var(--font-ui);
  font-size: 11px;
  font-style: normal;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--texte-faible);
  vertical-align: 1px;
}
.tag--pub { border-color: var(--info-bord); color: var(--info); }
/* Hors service : gris, jamais ambre. L'ambre est réservé à l'alarme, et un
   cockpit qu'on a soi-même arrêté n'est pas une anomalie. */
.tag--off { border-color: var(--bord-fort); color: var(--inactif); }
.card--off .card__open > b { color: var(--inactif); }
.card--off .card__open k, .card--off .card__owner { opacity: .6; }

/* L'ATTACHE — le troisième registre. Elle se rattache à la ligne du dessus
   plutôt que de flotter, et son icône ne pèse pas autant que son texte : une
   icône au corps du texte fait une tache, alors qu'elle n'est là que pour dire
   de quelle NATURE est l'attache — une personne, une machine, un cockpit. On la
   descend d'un point et on la tient dans le gris le plus faible de la carte. */

.card__owner {
  display: block;
  margin-top: 5px;
  font-size: 11px;
  font-style: normal;
  font-weight: 400;
  text-decoration: none;
  letter-spacing: .02em;
  line-height: 1.4;
  color: var(--texte-tenu);
}
.card__owner i { margin-right: 5px; font-size: 10px; opacity: .8; }

/* ── Formulaires de gestion ───────────────────────────────────────────────────
   Une feuille de réglages, pas un formulaire de saisie. La différence tient à
   ce qu'on y fait : on ne remplit pas des champs vides, on relit des valeurs
   existantes et l'on en change une. D'où deux colonnes — l'intitulé et son
   explication d'un côté, la valeur de l'autre — et un filet entre les lignes :
   l'œil descend la colonne de gauche, s'arrête sur ce qu'il cherche, et trouve
   la valeur en face. Empilé, chaque réglage occupe trois lignes et l'on perd de
   vue lequel on règle. */

.site__sub {
  margin: 32px 0 12px;
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 700;
  color: var(--texte-faible);
}
.site__sub--danger { color: var(--alerte); }

.panelform {
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--surface);
  overflow: hidden;
}

/* ── LE CADRE VAUT POUR TOUS LES FORMULAIRES, ONGLETS COMPRIS ────────────────
   Il a existé ici une exception : un formulaire qui porte des panneaux à
   onglets perdait son cadre, au motif que le trait de la barre d'onglets
   délimitait déjà la zone. À l'usage c'est faux. Le trait des onglets dit où
   s'arrête LA BARRE, pas où commence le contenu : sous lui, les champs
   flottaient sur le fond de la page, sans rien qui dise jusqu'où allait ce
   qu'on était en train de régler, et l'écran paraissait inachevé à côté de
   ceux qui gardaient leur cadre — la fiche d'un compte, par exemple, dont les
   onglets sont pourtant bâtis pareil.

   Un cadre unique pour tous : le même objet se présente de la même façon,
   quelle que soit la manière dont son formulaire est découpé à l'intérieur. */

/* ── CE QUI N'EST PAS UNE RANGÉE DE CHAMPS RESPIRE QUAND MÊME ────────────────
   Le retrait intérieur de ce cadre est porté par `.row`, et par elle seule.
   C'était sans conséquence tant qu'un panneau ne contenait que des champs ;
   dès qu'on y pose autre chose — un paragraphe d'explication, une ligne
   d'état, une barre de boutons, une liste de cartes — cet élément-là colle au
   bord, à côté de champs qui, eux, sont en retrait. L'œil voit alors deux
   alignements là où il n'y a qu'une colonne.

   On donne donc le même retrait à tout enfant direct d'un panneau qui n'est pas
   une rangée. La marge du bas des rangées reste leur affaire ; ici on ne touche
   qu'aux côtés, et à l'espace vertical de ce qui flotte. */
/* DES MARGES, ET NON DU PADDING. Première version : `padding-left` sur ces
   mêmes éléments — elle écrasait le retrait PROPRE de ceux qui en ont un. La
   bannière d'état porte `padding: 10px 13px` pour décoller son icône de son
   cadre ; la règle générique le remettait à dix-huit d'un côté et à zéro de
   l'autre, et l'icône se retrouvait collée au bord. Une marge pousse la boîte
   sans toucher à ce qu'il y a dedans. */
.panelform > [data-panel] > :not(.row) {
  margin-left: 18px;
  margin-right: 18px;
}
/* Le premier et le dernier ne doivent pas toucher le haut ni le bas non plus. */
.panelform > [data-panel] > :not(.row):first-child { margin-top: 15px; }
.panelform > [data-panel] > :not(.row):last-child { margin-bottom: 15px; }

/* ── ET L'IMBRICATION INVERSE, QUI EXISTE AUSSI ──────────────────────────────
   La règle ci-dessus ne voyait qu'un cas : le panneau DANS le formulaire, un
   seul formulaire pour tous les onglets (écran Réglages). Il y en a un second,
   et il est même le plus juste quand chaque onglet s'enregistre seul : le
   formulaire DANS le panneau, un par onglet (écrans Sécurité et DCF).

   Là, ce qui n'est pas une rangée est un enfant DIRECT du formulaire, et aucun
   sélecteur ne l'attrapait : le paragraphe d'explication et le bloc d'épreuve
   collaient au cadre, à deux pixels du trait, pendant que les champs voisins
   étaient en retrait de dix-huit. L'œil voyait deux alignements là où il n'y a
   qu'une colonne — exactement le défaut que la règle précédente corrigeait
   pour l'autre imbrication.

   `:not([data-panel])` est indispensable : sans lui, la règle pousserait aussi
   le panneau du premier cas, et ses rangées se retrouveraient en retrait
   double. */
.panelform > :not(.row):not([data-panel]) {
  margin-left: 18px;
  margin-right: 18px;
}
.panelform > :not(.row):not([data-panel]):first-child { margin-top: 15px; }
.panelform > :not(.row):not([data-panel]):last-child { margin-bottom: 15px; }
.row {
  display: grid;
  grid-template-columns: minmax(160px, 240px) minmax(0, 1fr);
  align-items: start;
  gap: 6px 28px;
  padding: 15px 18px;
  border-bottom: 1px solid var(--filet);
}
.row:last-child { border-bottom: 0; }

.row > label:not(.switch) {
  grid-column: 1;
  grid-row: 1;
  margin: 0;
  padding-top: 8px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: none;
  color: var(--texte-fort);
}

/* Tout le reste occupe la seconde colonne, et s'empile dedans. */
/* TOUT ce qui n'est pas l'intitulé occupe la seconde colonne. La liste est
   explicite plutôt qu'un `.row > *` : l'intitulé, lui, doit rester en première
   colonne, et un sélecteur universel l'y enverrait aussi. Un élément oublié ici
   retombe en première colonne, où il se retrouve à l'étroit — c'est ce qui est
   arrivé au bloc du lien public. */
.row > input,
.row > select,
.row > .pick,
.row > .chips,
.row > .switch,
.row > .checks,
.row > .readonly,
.row > .share,
.row > .hint {
  grid-column: 2;
}

.row input[type="text"], .row input[type="email"], .row input[type="password"],
.row input[type="number"], .row input:not([type]), .row select {
  width: 100%;
  max-width: 360px;
  padding: 8px 11px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--creux);
  color: var(--texte-fort);
  font: inherit;
  font-size: 14px;
}
.row input[type="number"] { max-width: 130px; }
.row input:focus, .row select:focus {
  outline: none;
  border-color: var(--info);
  box-shadow: 0 0 0 3px rgba(63, 200, 240, .12);
}

/* La ligne d'action tient sur toute la largeur et se détache du reste. */
.row--actions {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 18px;
  background: var(--fond-creux);
}
.row--actions .btn { width: auto; }

.hint {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--texte-tenu);
}
.hint code {
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--creux);
  color: var(--info);
  font-size: 12px;
}
.readonly { margin: 0; padding-top: 8px; font-size: 14px; color: var(--texte-fort); }

.formstate { font-size: 12px; letter-spacing: .08em; color: var(--texte-tenu); }
.formstate.is-ok { color: var(--ok); }
.formstate.is-bad { color: var(--texte-alerte); }
.formstate.is-busy { color: var(--info); }

/* En colonne étroite, les deux colonnes n'ont plus de place : on empile, mais
   seulement là — imposer l'empilement partout aurait coûté la lisibilité de la
   feuille sur les écrans où elle tient. */
@media (max-width: 700px) {
  .row { grid-template-columns: 1fr; gap: 6px; }
  .row > label:not(.switch) { grid-column: 1; padding-top: 0; }
  .row > input, .row > select, .row > .pick, .row > .chips, .row > .switch,
  .row > .checks, .row > .readonly, .row > .share, .row > .hint { grid-column: 1; }
}

/* Interrupteurs et listes de cases */
/* ── L'interrupteur ────────────────────────────────────────────────────────
   Un vrai interrupteur, et non une case à cocher recolorée.

   La distinction n'est pas cosmétique : une case dit « je choisis, je
   validerai plus tard », un interrupteur dit « c'est l'état en ce moment ».
   Dans une application de supervision, presque tout est un état — et la case
   native, en plus, se dessine différemment sur chaque système.

   La piste EST l'input : styler l'élément lui-même plutôt qu'un faux élément
   posé à côté conserve gratuitement le clavier, le focus et le clic sur le
   libellé, qu'un `<span>` décoratif obligerait à réinventer. */






/* Le `<span>` que posaient certains formulaires n'a plus d'objet : la piste est
   l'input. On le masque plutôt que de le retirer partout, pour ne pas laisser
   un trou dans les gabarits qui l'écrivent encore. */


.checks {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 2px 14px;
  margin-top: 6px;
}

/* Boutons d'action compacts, dans une ligne de liste */
/* ── LES BOUTONS S'ALIGNENT SUR LE TITRE ─────────────────────────────────────
   Ils agissent sur l'objet que le titre nomme ; c'est donc à sa hauteur qu'on
   les attend, et non au milieu d'un bloc dont la hauteur dépend du nombre de
   caractéristiques. Centrés, ils descendaient d'une ligne sur une carte qui en
   porte trois et remontaient sur celle qui n'en porte qu'une — la rangée de
   boutons ondulait le long d'une liste dont les cartes n'ont pas toutes la
   même hauteur. En haut, elle tient une ligne droite.

   Le retrait du haut place le bouton à cheval sur la ligne du titre : 8 px de
   moins que le retrait de `.card__open`, soit la moitié de ce qu'un bouton de
   30 px dépasse d'une ligne de titre. */
/* ── TOUT CE QUI EST À DROITE PARTAGE UN CENTRE ──────────────────────────────

   Le groupe d'actions était collé en HAUT, avec huit pixels de rembourrage pour
   le mettre au niveau du titre. Cela tenait tant qu'il ne portait que des
   boutons de trente pixels sur une carte d'une ligne.

   Une micro-trace en fait quarante-quatre, et une ligne de faits se replie sur
   deux ou trois lignes : mesuré sur la carte du site 266, la rangée faisait 97
   pixels, la trace se centrait à 597, les boutons à 590, et le chevron — qui,
   lui, se centre sur toute la rangée — à 616. Trois alignements différents dans
   une même ligne, et cela se voyait.

   `center` les met d'accord : la trace, la légende, les boutons et le chevron
   tombent tous sur le centre de la rangée, quelle que soit sa hauteur. Le
   rembourrage vertical part avec — il servait à compenser l'ancrage en haut, et
   il décalerait maintenant le groupe de quatre pixels sous le centre. */
.card__inline {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 12px 0 6px;
}
.card__inline select {
  padding: 5px 8px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--creux);
  color: var(--texte-fort);
  font: inherit;
  font-size: 13px;
}
/* LES BOUTONS-ICÔNE ONT DÉMÉNAGÉ dans `controls.css`, avec les pastilles : un
   cockpit en affiche quatre — les croix de fermeture du catalogue, du plan des
   variables et de la recherche — et il ne charge pas cette feuille. */

.card--stack { flex-direction: column; align-items: stretch; }
.card--stack .checks { padding: 0 16px 14px; }

/* Zone sensible */
.danger {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 16px;
  border: 1px solid #3A2422;
  border-radius: var(--radius);
  background: rgba(255, 59, 48, .04);
}
.danger div { flex: 1 1 auto; }
.danger b { display: block; font-size: 14px; color: var(--texte-fort); }
.danger k { font-size: 12px; color: var(--texte-tenu); }
/* La TEINTE est dans `dialog.css`, la seule feuille que chargent à la fois le
   panneau et les écrans de gestion — voir là-bas. Il ne reste ici que la
   géométrie propre à ces écrans. */
.btn--danger i { margin-right: 6px; }

/* ── Onglets ──────────────────────────────────────────────────────────────────
   Une page de réglages qui déroule tout se parcourt en cherchant, alors qu'on
   sait toujours ce qu'on vient régler. Les onglets nomment les intentions.
   Le liseré sous l'onglet ouvert, plutôt qu'un fond plein : c'est un repère,
   pas une commande, et il doit peser moins que ce qu'il désigne. */

/* LA BARRE SE DÉCOLLE DE CE QU'IL Y A AU-DESSUS. Elle suivait jusqu'ici un
   paragraphe d'explication, qui portait sa propre marge basse ; posée sous un
   bandeau de chiffres, qui n'en a pas, elle venait toucher son cadre et les
   deux ne faisaient plus qu'un bloc. La marge est donc à elle : en flux normal
   elle fusionne avec celle du paragraphe quand il y en a un, et l'espace reste
   le même là où il était déjà juste. */
.tabs {
  display: flex;
  gap: 2px;
  margin-top: 18px;
  margin-bottom: 18px;
  border-bottom: 1px solid var(--bord);
}
/* ── UN FILET ENTRE DEUX GROUPES D'ONGLETS ───────────────────────────────────
   Quand une barre mêle des onglets qui MONTRENT et des onglets qui RÈGLENT, la
   suite se lit comme une liste plate et l'on ne voit pas qu'elle a deux
   moitiés. Un filet vertical suffit — c'est le même geste qu'un `border-top`
   entre deux blocs répétés, et il ne coûte aucune place. */
.tab--apres {
  margin-left: 14px;
  padding-left: 20px;
  border-left: 1px solid var(--bord);
}

.tab {
  position: relative;
  padding: 9px 14px;
  border: 0;
  background: none;
  color: var(--texte-faible);
  font: inherit;
  font-size: 13px;
  letter-spacing: .04em;
  cursor: pointer;
}
.tab:hover { color: var(--texte-fort); }
.tab.is-on { color: var(--texte-fort); }
.tab.is-on::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 2px;
  background: var(--info);
}
.tab:focus-visible { outline: 1px solid var(--info); outline-offset: -2px; }
.tab i { margin-right: 7px; font-size: 12px; }
.tab em {
  margin-left: 7px;
  padding: 1px 5px;
  border-radius: 999px;
  background: var(--bord);
  color: var(--texte-faible);
  font-style: normal;
  font-size: 11px;
}

/* ── L'étoile ─────────────────────────────────────────────────────────────────
   Creuse au repos, pleine et ambrée une fois posée. Discrète : elle est à la
   disposition de qui veut s'en servir, elle ne réclame rien à qui l'ignore. */

/* Elle porte `.iconbtn` comme les autres boutons de la carte : seule sa couleur
   la distingue, et seulement une fois posée.

   LE SÉLECTEUR PASSE PAR `.card__inline` À DESSEIN : `controls.css` est chargé
   après cette feuille, et son `.iconbtn:hover` — de même poids qu'un
   `.card__star:hover` — l'emportait. L'étoile posée virait au cyan dès qu'on la
   survolait, c'est-à-dire exactement au moment où l'on s'apprête à la retirer.
   L'ambre est la couleur du favori ; elle ne doit pas dépendre de l'ordre des
   feuilles. */
.card__inline .card__star:hover { color: var(--caution); }
.card__inline .card__star.is-on,
.card__inline .card__star.is-on:hover { color: var(--caution); }

/* ── SANS CADRE DANS UNE LISTE ───────────────────────────────────────────────
   Un cadre autour d'une icône sert à dire « ceci est un bouton » là où rien
   d'autre ne le dit — dans une barre d'outils, au milieu d'un formulaire. Sur
   une carte, la rangée EST déjà l'endroit des boutons : le cadre n'apprend
   rien, et quatre petits rectangles alignés à droite de chaque ligne finissent
   par peser plus lourd que le contenu qu'ils accompagnent.

   Le cadre reste déclaré, en transparent : la boîte garde ses dimensions, et
   rien ne se décale au survol. C'est le FOND qui répond alors, avec la couleur
   — un aplat discret sous le seul bouton visé, plus franc à lire qu'un liseré
   d'un pixel sur un écran regardé de loin. */
.card__inline .iconbtn { border-color: transparent; }
.card__inline .iconbtn:hover { border-color: transparent; background: var(--filet); }
/* ET DANS UN TABLEAU DE TIROIR, pour exactement la même raison : la ligne est
   déjà l'endroit du bouton, et une colonne de petits rectangles pèse plus lourd
   que les chiffres qu'elle borde. Le tableau vit d'ailleurs DANS une carte —
   sans cette règle, le même bouton portait un cadre à deux centimètres d'un
   autre qui n'en avait pas. */
.poolkeys__t .iconbtn { border-color: transparent; }
.poolkeys__t .iconbtn:hover { border-color: transparent; background: var(--filet); }
/* SAUF CE QUI RESTE À FAIRE. Un cadre qui ne se voit plus nulle part redevient
   un signal quand il reparaît quelque part : le raccordement dont le compte
   n'est pas encore connecté garde le sien, et c'est le seul de la liste. */
.card__inline .iconbtn--go {
  border-color: var(--info-bord);
  background: var(--info-creux);
}
.card__inline .iconbtn--go:hover { border-color: var(--info); background: var(--info-creux); }

/* ── Interrupteurs ────────────────────────────────────────────────────────────
   Jamais de case à cocher. L'interrupteur dit un ÉTAT EN COURS — activé,
   désactivé — là où la case dit un choix qu'on validera plus tard. Dans une
   application de supervision, presque tout est un état.

   La case reste en dessous, cachée : c'est elle qui porte la sémantique, le
   clavier et les technologies d'assistance. On n'en change que l'apparence. */




/* Le curseur du toggle : un rond qui glisse. */











/* ── Dialogues ────────────────────────────────────────────────────────────────
   Deux formes, deux intentions. La SHEET monte du bas et occupe la largeur :
   on y crée et l'on y modifie, en gardant sous les yeux la liste d'où l'on
   vient. La boîte CENTRÉE arrête : elle sert aux suppressions, et son propos
   est qu'on s'arrête pour lire. */




/* Boîte centrée */


/* Sheet : pleine largeur, ancrée en bas */











/* La liste des conséquences : c'est elle qui fait la différence avec un
   « êtes-vous sûr ? ». On y cherche celle qu'on n'avait pas prévue. */


















/* ── Saisie d'un code ─────────────────────────────────────────────────────────
   Des cases, une par chiffre. Elles disent combien de chiffres sont attendus et
   combien sont posés — un champ de mot de passe ne dit ni l'un ni l'autre — et
   elles ouvrent le pavé numérique sur un écran tactile. */

.gate__who {
  margin: 0 0 20px;
  font-size: 14px;
  color: var(--texte-faible);
}
.gate__who b { color: var(--texte-fort); }


/* La case est une BOÎTE qui contient le champ. Un `<input>` est un élément
   remplacé : il ne peut pas porter de pseudo-élément, donc pas de curseur
   dessiné. C'est la boîte qui le porte. */






/* LE CURSEUR. Une barre qui clignote dans la case active, pour dire qu'on peut
   taper tout de suite — sans elle, quatre cases vides ressemblent à un dessin
   et l'on cherche où cliquer. `steps(1)` donne le battement net d'un curseur de
   terminal ; une transition douce ferait luire, ce qui ne dit pas la même
   chose. Il disparaît dès que la case porte un chiffre : elle n'attend plus. */







/* Un repère supplémentaire : au repos, la PREMIÈRE case est marquée, de sorte
   qu'on voie où commencer avant même d'avoir cliqué. */

@media (prefers-reduced-motion: reduce) {
  /* Sans clignotement, la barre reste allumée : le repère demeure, c'est le
     battement qu'on retire. */
  .pincell:has(input:focus):not(.is-set)::after { animation: none; opacity: 1; }
}





/* ── Choix restreint ──────────────────────────────────────────────────────────
   Des radios en cartes, jamais une liste déroulante. Le `<select>` cache ses
   options jusqu'au clic : on ne peut ni les comparer, ni dire sous chacune ce
   qu'elle entraîne. Réservé aux listes COURTES et fermées — une liste ouverte
   ou longue garde une déroulante. */












/* Petits boutons de texte, dans un dialogue : ils proposent, ils ne commandent
   pas — d'où l'apparence d'un lien plutôt que d'un bouton. */



/* Dans un dialogue, les cases sont plus petites et alignées à gauche : elles y
   côtoient d'autres champs, et les garder à la taille de la page de connexion
   les ferait passer pour l'essentiel du formulaire. */




/* La page d'accueil de première connexion respire davantage que la connexion :
   elle explique, là où l'autre se contente de demander. */
.gate__box--wide { max-width: 440px; }
.gate__intro { margin: 0 0 10px; font-size: 15px; line-height: 1.5; color: var(--texte-fort); }
.gate__note {
  margin: 0 0 22px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--texte-tenu);
}
.btn--ghost {
  margin-top: 8px;
  border-color: transparent;
  background: none;
  color: var(--texte-faible);
  font-weight: 400;
  letter-spacing: .06em;
  text-transform: none;
}
.btn--ghost:hover { border-color: var(--bord-fort); color: var(--texte-fort); }

/* ── Messages d'une page à l'autre ────────────────────────────────────────── */

/* ── Le bandeau du SERVEUR ────────────────────────────────────────────────
   Ce que le serveur avait à dire est rendu dans la page, avant tout script :
   `shell.js` le verse ensuite dans la pile de notifications et retire la
   barre. Ces styles restent malgré tout — c'est le FILET. Si le module ne se
   charge pas, le message doit rester lisible plutôt que de disparaître avec
   le JavaScript qui devait le porter. */
.flashbar { padding: 12px 24px 0; }
.flashbar__item {
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 860px;
  margin: 0 auto 8px;
  padding: 10px 14px;
  border-left: 2px solid var(--info);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: rgba(63, 200, 240, .07);
  font-size: 14px;
  color: var(--texte-fort);
}
.flashbar__item i { font-size: 12px; color: var(--info); }
.flashbar__item--ok { border-left-color: var(--ok); background: rgba(33, 208, 122, .07); }
.flashbar__item--ok i { color: var(--ok); }
.flashbar__item--error { border-left-color: var(--alerte); background: rgba(255, 59, 48, .07); }
.flashbar__item--error i { color: var(--alerte); }

/* ── Liste déroulante ─────────────────────────────────────────────────────────
   Le `<select>` reste là, masqué : il porte la valeur, il part avec le
   formulaire, et une page dont le script n'aurait pas tourné resterait
   utilisable. Ce qu'on voit est dessiné par-dessus. */
















/* L'entrée courante porte une coche : la couleur seule ne suffit pas à
   distinguer « survolé » de « choisi » quand le pointeur passe dessus. */



/* ── Lien public ──────────────────────────────────────────────────────────── */

.share {
  display: flex;
  gap: 20px;
  margin-top: 14px;
  padding: 16px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--creux);
}

/* À l'écran, le code SUIT LE THÈME : un carré blanc au milieu d'un panneau
   sombre saute aux yeux comme une erreur de rendu. À l'impression, il repasse
   en noir sur blanc — un code inversé dévore l'encre, et les lecteurs
   industriels ne le lisent pas tous. */
.share__code { flex: 0 0 auto; text-align: center; }

.share__qr {
  width: 132px;
  height: 132px;
  padding: 8px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--fond);
}
.share__qr--print { border-color: #fff; background: #fff; }
.share__qr svg { display: block; width: 100%; height: 100%; }
.share__code .linkbtn { margin-top: 8px; font-size: 12px; }

.share__body { flex: 1 1 auto; min-width: 0; }

.share__url { display: flex; gap: 6px; margin-bottom: 14px; }
.share__url input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 11px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--fond);
  color: var(--info);
  /* Une URL qu'on relit caractère par caractère avant de l'envoyer : la
     monospace du système, comme tout ce qui s'écrit littéralement. */
  font-family: var(--font-mono);
  font-size: 13px;
}

.share__row { display: flex; align-items: center; gap: 10px; }
.share__row label {
  font-size: 12px;
  font-weight: 600;
  color: var(--texte-fort);
}
.share__row input[type="date"] {
  width: auto;
  padding: 7px 10px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--fond);
  color: var(--texte-fort);
  font: inherit;
  font-size: 13px;
}
.share .btn--danger { margin-top: 14px; }

@media (max-width: 700px) {
  .share { flex-direction: column; }
  .share__qr { width: 120px; height: 120px; }
}

/* ── Cockpits groupés par site ─────────────────────────────────────────────
   L'onglet « Sites » n'empile pas des cartes de site : il pose un TITRE de
   section et laisse les cockpits en dessous, exactement les mêmes cartes que
   dans la liste. Un encadré par site ajouterait une boîte autour de boîtes et
   ferait perdre la continuité de lecture d'une installation à l'autre. */
.sitegroups { display: grid; gap: 22px; }
.sitegroup__head {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 8px; padding: 0 0 7px;
  border-bottom: 1px solid var(--bord);
  font: 700 12px/1 var(--font-ui);
  letter-spacing: .08em; text-transform: uppercase; color: var(--texte-faible);
}
.sitegroup__head i { color: var(--info); font-size: 12px; }
.sitegroup__head span { color: var(--texte-fort); letter-spacing: .06em; }
.sitegroup__head k {
  font: 400 12px/1 var(--font-ui); letter-spacing: .02em;
  text-transform: none; color: var(--texte-faible);
}
.sitegroup__head .tag { margin-left: 0; }
/* La poignée de tri disparaît ici : l'ordre enregistré est celui de la liste,
   et une poignée qui ne fait rien se lit comme une panne. */

/* ── Carte dépliable et table clé/valeur ──────────────────────────────────
   Le tiroir sert à ce qu'on édite EN REGARDANT l'entité : une table de
   métadonnées se remplit ligne à ligne, et un dialogue modal qu'on ouvre et
   referme pour chaque paire transforme une saisie continue en va-et-vient. */
.card--fold { flex-direction: column; align-items: stretch; }
/* ── LA RANGÉE OCCUPE LA CARTE ───────────────────────────────────────────────
   `.card__row` était déclarée DEUX fois dans cette feuille — la cause n°1 des
   défauts d'alignement ici. Et aucune des deux ne l'étirait : une carte sans
   tiroir reste en `flex-direction: row`, sa rangée n'y prenait donc que la
   largeur de son contenu, et le groupe de boutons se calait juste après le
   texte au lieu du bord droit. Cela se voyait sur les sauvegardes, où deux
   lignes de longueurs différentes donnaient deux colonnes de boutons
   différentes. */
.card__row {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: stretch;
}
.card__row .card__open { flex: 1; }

.card__fold {
  flex: 0 0 auto;
  width: 34px;
  border: 0; background: none; cursor: pointer;
  color: var(--texte-faible);
  font-size: 12px;
}
.card__fold:hover { color: var(--texte-fort); }
/* Le chevron TOURNE, il ne change pas de dessin : la rotation dit l'état en
   conservant le même objet à l'œil. */
.card__fold i { transition: transform .14s ease; }
.card--fold.is-open .card__fold i { transform: rotate(90deg); }

.card__drawer {
  border-top: 1px solid var(--bord);
  padding: 12px 14px 12px 34px;
  background: var(--fond-creux);
}
.card__drawer-actions {
  display: flex; align-items: center; gap: 12px;
  margin-top: 10px;
}






/* La CLÉ est un identifiant, pas une phrase : la police à chasse fixe le dit
   sans avoir à l'expliquer, et rend visible l'espace en trop. */



/* ── Données externes ──────────────────────────────────────────────────────
   Trois écrans qui montrent des ÉCHANGES : ce qui part, ce qui revient, ce
   qu'on en tire. D'où la place faite au texte brut — un extrait de réponse,
   des en-têtes, une trace — que rien ne remplace quand un appel échoue. */

.probe { font-size: 13px; }
.probe__grid {
  display: grid;
  grid-template-columns: 92px 1fr;
  gap: 4px 12px;
  align-items: baseline;
}
.probe__grid k {
  font: 700 11px/1.6 var(--font-ui);
}
.probe__grid b { font-weight: 500; color: var(--texte-fort); word-break: break-all; }
.probe__bad { color: var(--texte-alerte); }

.probe__more { margin-top: 10px; }
.probe__more summary {
  cursor: pointer;
  font: 700 11px/1 var(--font-ui);
}
.probe__more summary:hover { color: var(--info); }
.probe__more pre {
  margin: 8px 0 0;
  padding: 10px;
  max-height: 260px;
  overflow: auto;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--fond);
  color: var(--texte);
  font: 400 12px/1.5 var(--font-mono);
  white-space: pre-wrap;
  word-break: break-word;
}

.tag--ok   { border-color: var(--ok-bord); color: var(--ok); }
/* Ambre ici, et NON rouge : un connecteur injoignable ou une source sans
   mappage sont des choses à finir de régler, pas des alarmes d'installation. */
.tag--warn { border-color: var(--caution-bord); color: var(--caution); }
/* `.tag--bad` MANQUAIT, et rien ne le disait : `hookups.js` l'emploie depuis
   toujours pour « injoignable » et « inscription retirée », mais aucune règle
   ne le déclarait — l'étiquette sortait au gris des autres, c'est-à-dire en
   disant le contraire de ce qu'elle voulait dire. Un nom de classe inventé ne
   lève aucune erreur ; il ne fait rien, ce qui est la panne la plus longue à
   voir (déjà rencontrée avec `tone: 'green'`). */
.tag--bad  { border-color: var(--alerte-bord); color: var(--texte-alerte); }

/* Le studio de mappage. */
.studio__bar { display: flex; align-items: center; gap: 10px; margin: 12px 0; }
.studio__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.studio__field { display: block; }
.studio__field k {
  display: block; margin-bottom: 4px;
  font: 700 11px/1 var(--font-ui);
}
.studio__field input,
.studio__line input,
.studio__line select {
  width: 100%;
  min-width: 0;
  padding: 6px 8px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--fond);
  color: var(--texte-fort);
  font: 400 13px/1.2 var(--font-mono);
}
.studio__field input:focus,
.studio__line input:focus,
.studio__line select:focus { outline: none; border-color: var(--info-bord); }

.studio__map { margin-top: 14px; }
.studio__map .kv__head {
  display: grid;
  grid-template-columns: minmax(160px, 1fr) minmax(160px, 1.6fr) 130px;
  gap: 8px;
}
.studio__line {
  display: grid;
  grid-template-columns: minmax(160px, 1fr) minmax(160px, 1.6fr) 130px;
  gap: 8px;
  align-items: center;
  padding: 5px 0;
  border-top: 1px solid var(--filet);
}
.studio__target code {
  font: 500 13px/1.4 var(--font-mono);
  color: var(--info);
}
.studio__target k {
  display: block;
  font: 400 11px/1.4 var(--font-ui);
  color: var(--texte-faible);
}

.studio__rows { margin-top: 12px; overflow-x: auto; }
.studio__rows table { border-collapse: collapse; width: 100%; font-size: 13px; }
.studio__rows th {
  padding: 5px 8px;
  border-bottom: 1px solid var(--bord-fort);
  text-align: left;
  font: 700 11px/1 var(--font-ui);
  white-space: nowrap;
}
.studio__rows td {
  padding: 5px 8px;
  border-bottom: 1px solid var(--filet);
  color: var(--texte);
  font-family: var(--font-mono);
  font-size: 12px;
  white-space: nowrap;
}

/* Les cibles. */
.target { margin-bottom: 26px; }
.target__body {
  display: grid;
  grid-template-columns: minmax(280px, 1fr) minmax(240px, 1fr);
  gap: 18px;
  margin-top: 10px;
}
@media (max-width: 860px) { .target__body { grid-template-columns: 1fr; } }




.target__source {
  display: block;
  padding: 8px 10px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--surface);
  text-decoration: none;
}
.target__source + .target__source { margin-top: 6px; }
.target__source:hover { border-color: var(--bord-vif); }
.target__source b { display: block; font-size: 14px; color: var(--texte-fort); }
.target__source k { font-size: 12px; color: var(--texte-faible); }
.target__source.is-off { opacity: .55; }

/* ── Bibliothèque d'instruments ────────────────────────────────────────────
   Un mur de vignettes, chacune montrant un instrument VIVANT dans son
   panneau. La carte reste sobre : c'est l'instrument qu'on regarde, et un
   cadre trop dessiné entrerait en concurrence avec son bezel. */
.site--wide .site__inner { max-width: 1500px; }
.site__headnote { font-size: 13px; color: var(--texte-faible); }

/* ── Les postes qui affichent un cockpit ────────────────────────────────────
   Une ligne par poste, sous la carte du cockpit. Elles sont dans la carte et
   non à côté : la question « qui regarde ça ? » ne se pose qu'à propos d'un
   cockpit donné, et une seconde liste à mettre en regard obligerait à faire
   l'appariement des yeux. */
/* Le RETRAIT est celui de la carte, pas zéro : la carte elle-même n'a pas de
   marge intérieure — ce sont ses éléments qui la portent (`.card__open`,
   `.card__inline`). Une liste posée à même la carte se retrouvait donc collée
   aux bords, décalée vers la gauche par rapport au titre juste au-dessus. */
/* ── Les cockpits d'un site ─────────────────────────────────────────────────
   Une rangée de portes, sur la carte du site. Ce ne sont pas des étiquettes :
   chacune mène quelque part, et c'est pour cela qu'on ouvre cette page. */
.cocklinks {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 0 14px 12px 42px;
}
.cocklink {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 4px 10px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--texte);
  font-size: 13px;
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cocklink i { font-size: 11px; color: var(--texte-faible); }
.cocklink:hover {
  border-color: var(--info-bord);
  background: var(--info-creux);
  color: var(--texte-fort);
}
.cocklink:hover i { color: var(--info); }
/* Hors service : la porte existe encore, elle ne mène plus à l'exploitation. */
.cocklink.is-off { opacity: .5; }
.cocklink.is-off i { color: var(--texte-tenu); }

.viewers { list-style: none; margin: 0; padding: 0 12px 12px 14px; }
.viewer {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  border-top: 1px solid var(--filet);
}
.viewer__main { flex: 1 1 auto; min-width: 0; }
.viewer__main b { display: block; font-size: 13px; font-weight: 600; color: var(--texte); }
/* Le même couple titre / caractéristiques qu'une carte, donc la même règle de
   segments : voir `.card__open k` plus haut. */
.viewer__main k { display: block; font-size: 12px; line-height: 1.5; color: var(--texte-tenu); }
.site__headnote.is-bad { color: var(--alerte); }

.lib {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
  /* Les rangées prennent la taille de ce qu'elles portent, et NE SE LAISSENT
     PAS COMPRIMER.

     Dès que la grille reçoit une hauteur définie — c'est le cas depuis qu'elle
     défile dans son cadre plutôt qu'avec la page — des rangées `auto` se
     répartissent la place disponible au lieu de suivre leur contenu. Les
     vignettes se sont retrouvées écrasées à la moitié de leur hauteur : le
     nom et la taille de l'instrument, qui vivent sous la scène, étaient
     rognés, et l'on choisissait un instrument sans savoir combien de cases il
     prend. `min-content` les rend à leur contenu, `start` les empile depuis le
     haut au lieu de les étaler. */
  grid-auto-rows: min-content;
  align-content: start;
  gap: 12px;
}

.lib__tile {
  /* ── LES PIEDS DES CARTES S'ALIGNENT ────────────────────────────────────
     Une rangée prend la hauteur de la plus haute de ses vignettes, et les
     cartes s'y étirent — mais la SCÈNE, elle, gardait sa hauteur propre : le
     nom de l'instrument se retrouvait à cinq hauteurs différentes sur une même
     rangée, et la grille devenait un escalier. La scène prend donc la place qui
     reste, exactement comme dans la palette d'insertion (`.picker__stage`),
     dont ce catalogue-ci est le jumeau. L'instrument ne grandit pas pour
     autant : sa taille vient du pas commun, et il reste centré. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  overflow: hidden;
  transition: border-color .12s ease, background .12s ease;
}
.lib__tile:hover { border-color: var(--bord-vif); background: var(--surface-vive); }

.lib__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1 1 auto;
  min-height: 176px;
  padding: 14px;
  /* Le fond du panneau, pas celui de la carte : un instrument se juge sur la
     tôle sur laquelle il sera vissé, pas sur du blanc. */
  background: var(--panel-bg);
  border-bottom: 1px solid var(--bord);
}
.lib__panel {
  position: relative;
  min-height: 0;
  /* La vignette n'a pas de trame : elle montre une pièce, pas un panneau en
     cours de composition. */
  background-image: none;
}

.lib__foot { padding: 9px 11px; }
.lib__foot b {
  display: block;
  font-size: 14px; font-weight: 600; color: var(--texte-fort);
}
.lib__foot k { font-size: 12px; color: var(--texte-faible); }

/* ── L'atelier ─────────────────────────────────────────────────────────────
   Il s'ouvre par la droite et non en dialogue centré : on règle une échelle en
   regardant l'aiguille, et une boîte au milieu recouvrirait ce qu'on observe. */
.bench {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: min(560px, 100vw);
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--bord);
  background: var(--fond-creux);
  box-shadow: -18px 0 40px -20px rgba(0, 0, 0, .8);
  z-index: 60;
}
.bench__head {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--bord);
}
.bench__head div { flex: 1; min-width: 0; }
.bench__head b { display: block; font-size: 14px; color: var(--texte-fort); }
.bench__head k { font-size: 12px; color: var(--texte-faible); }

.bench__stage {
  display: flex; align-items: center; justify-content: center;
  padding: 18px;
  min-height: 220px;
  background: var(--panel-bg);
  border-bottom: 1px solid var(--bord);
}

.bench__size {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 14px;
  border-bottom: 1px solid var(--bord);
  font-size: 12px; color: var(--texte-faible);
}
.bench__size label { display: flex; align-items: center; gap: 6px; }
.bench__size input {
  width: 58px;
  padding: 4px 6px;
  border: 1px solid var(--bord); border-radius: var(--radius);
  background: var(--fond); color: var(--texte-fort);
  font: 400 13px/1.2 var(--font-ui);
}
.bench__hint { margin-left: auto; }
.bench__modes {
  display: flex; align-items: center; gap: 6px;
  padding: 9px 14px;
  border-bottom: 1px solid var(--bord);
}
.bench__modes .bench__hint { margin: 0 auto 0 0; }

/* OÙ L'INSTRUMENT EST POSÉ — au-dessus des réglages, parce que c'est un
   renseignement sur ce qu'on s'apprête à toucher : régler les défauts d'un
   type qui équipe six cockpits n'est pas le même geste que régler ceux d'un
   type qu'on vient de découvrir. */
.bench__usage {
  padding: 10px 14px;
  border-bottom: 1px solid var(--bord);
}
.bench__usage .bench__hint { margin: 0 0 6px; font-size: 12px; }
.bench__dim { color: var(--texte-tenu); }

/* ── Jetons ───────────────────────────────────────────────────────────────────
   Ce qui est LÀ se voit ; ce qui n'y est pas ne prend pas de place. C'est la
   différence avec une liste d'interrupteurs, dont la hauteur suit le nombre de
   comptes et non le nombre de membres.

   ── ILS S'APPELAIENT `.chip`, COMME LES FILTRES ─────────────────────────────

   Et ce n'est pas le même objet : un filtre est un BOUTON qu'on presse, un
   jeton est une ÉTIQUETTE qui porte une croix. D'où son rembourrage
   asymétrique — dix pixels à gauche, quatre à droite pour loger la croix — et
   celui de son groupe, quatorze pixels sous la rangée.

   Déclarés après les filtres dans cette même feuille, ils les écrasaient
   PARTOUT : les pastilles d'appareil de la page d'accueil, celles du moniteur
   réseau et toutes les autres héritaient d'un décalage prévu pour une croix
   qu'elles n'ont pas — libellés poussés vers la droite, groupes trop hauts,
   rangées désalignées de sept pixels avec leurs voisins.

   C'est la deuxième fois que ce conflit se produit : `.pick` désignait déjà la
   liste déroulante ET les pastilles d'ajout, jusqu'à ce qu'on renomme les
   secondes en `chipadd`. Un composant qui n'est pas l'autre porte un autre nom. */

/* ── LES JETONS ONT DÉMÉNAGÉ ─────────────────────────────────────────────────
   `.jetons`, `.jeton`, `.jeton__x`, `.jeton--implicit` et `.jetons__none` sont
   dans `controls.css`. Le dialogue les emploie (`ask({type:'jetons'})`), et un
   dialogue s'ouvre AUSSI depuis un cockpit — qui ne charge pas cette feuille.
   Ils s'y affichaient donc nus. Voir `feuilles-css-des-deux-mondes`. */

/* ── Chercher pour ajouter ────────────────────────────────────────────────────
   Une liste déroulante suppose qu'on parcourt ; à deux cents comptes on
   cherche un nom qu'on a déjà en tête. */

.pick { position: relative; display: inline-block; }

.pick__field {
  width: 150px;
  padding: 4px 10px;
  border: 1px dashed var(--bord-vif);
  border-radius: 999px;
  background: none;
  color: var(--texte-fort);
  font: inherit;
  font-size: 12px;
}
.pick__field::placeholder { color: var(--texte-tenu); }
.pick__field:hover { border-color: var(--info-bord); }
.pick__field:focus {
  width: 220px;
  border-style: solid;
  border-color: var(--info-bord);
  outline: none;
}

.pick__list {
  position: absolute;
  left: 0; top: calc(100% + 4px);
  z-index: 20;
  min-width: 220px;
  max-height: 240px;
  overflow-y: auto;
  list-style: none;
  margin: 0;
  padding: 4px;
  border: 1px solid var(--bord-fort);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .65);
}
.pick__list[hidden] { display: none; }

.pick__hit {
  display: block;
  width: 100%;
  padding: 5px 8px;
  border: 0;
  border-radius: 3px;
  background: none;
  color: var(--texte);
  font: inherit;
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.pick__hit k { display: block; font-size: 11px; color: var(--texte-tenu); }
/* Le premier résultat est DÉJÀ désigné : taper trois lettres et valider doit
   suffire, sans avoir à descendre d'un cran pour choisir ce qui est évident. */
.pick__hit.is-on, .pick__hit:hover { background: var(--filet); color: var(--texte-fort); }
.pick__hit.is-on { box-shadow: inset 2px 0 0 var(--info); }

.pick__none { padding: 6px 8px; font-size: 12px; color: var(--texte-tenu); }

.chips__none { font-size: 12px; color: var(--texte-tenu); }

/* L'inventaire d'une forme : REPLIÉ par défaut.

   Le résumé suffit à savoir s'il y a lieu d'ouvrir, et déplié en permanence il
   repoussait les réglages hors de l'écran sur une forme posée dans huit
   cockpits. Le triangle est celui du navigateur : on ne redessine pas une
   affordance que tout le monde connaît déjà. */

.usage > summary {
  cursor: pointer;
  list-style-position: outside;
}
.usage > summary::marker { color: var(--texte-tenu); }
.usage > summary:hover { color: var(--texte-fort); }

.usage__list { list-style: none; margin: 8px 0 0; padding: 0; }
.usage__row + .usage__row { margin-top: 6px; }
.usage__row {
  padding: 6px 8px;
  border: 1px solid var(--filet);
  border-radius: var(--radius);
  background: var(--surface);
}
.usage__row b { display: block; font-size: 13px; font-weight: 600; color: var(--texte); }
.usage__row k { display: block; margin-top: 4px; font-size: 12px; color: var(--texte-tenu); }

/* Chaque PAGE est un lien à part : c'est la page qui porte l'instrument, et
   c'est donc elle qu'on veut ouvrir — un lien vers le cockpit entier laisse
   ensuite chercher. */
.usage__page {
  display: inline-block;
  margin: 2px 4px 0 0;
  padding: 2px 7px;
  border: 1px solid var(--bord);
  border-radius: 2px;
  color: var(--texte-faible);
  font-size: 11px;
  letter-spacing: .04em;
  text-decoration: none;
}
.usage__page:hover { border-color: var(--info-bord); color: var(--info); }

/* `min-height: 0` : sans lui, un élément de flexbox ne descend pas sous la
   hauteur de son contenu, et le défilement promis par `overflow-y` ne se
   déclenche jamais — le formulaire déborde du panneau au lieu de défiler
   dedans. */
/* ── L'ÉTABLI NE MONTRE PAS SES BARRES ───────────────────────────────────────
   Même raison qu'ailleurs dans cette maison : on règle un instrument en
   REGARDANT l'aiguille au-dessus du formulaire, et une barre posée contre le
   bord d'un volet de cinq cent soixante pixels prend l'œil à chaque mouvement
   de molette pour ne rien apprendre — la longueur d'un formulaire de réglages
   n'est pas un renseignement dont on se sert.

   Le défilement, lui, reste entier : molette, glissement tactile, flèches et
   tabulation vers un champ hors vue. Seule la barre disparaît. */
.bench__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 4px 14px 14px;
  scrollbar-width: none;
}
.bench__body::-webkit-scrollbar { width: 0; height: 0; }
/* Les ACTIONS, dans l'en-tête. Elles passent à la ligne plutôt que de pousser
   le nom hors de vue : sur un instrument dont on règle les défauts, le nom est
   ce qui dit à quoi l'on s'apprête à toucher. */
.bench__head { flex-wrap: wrap; }
/* Assez spécifique pour battre `.bench__head div`, qui donne `flex: 1` à tous
   les blocs de l'en-tête : ici on ne veut pas d'élasticité, une barre d'outils
   prend la place de ses outils et pas un pixel de plus. */
.bench__head .bench__actions {
  display: flex;
  gap: 6px;
  flex: 0 0 auto;
}
.bench__actions .iconbtn { width: 30px; height: 30px; font-size: 13px; }
.bench__actions .iconbtn--go {
  border-color: var(--info-bord);
  background: var(--info-creux);
  color: var(--info);
}
.bench__actions .iconbtn--go:hover { border-color: var(--info); }


@media (max-width: 720px) { .bench { width: 100vw; } }

/* ── LES RACCORDEMENTS : LE TIROIR DES RESSOURCES ────────────────────────────
   Une installation réelle rend des dizaines de lampes. Ce tiroir sert à
   CHOISIR celles qu'on veut voir, et sa forme doit le dire : une collection
   par bloc, son geste de découverte à droite, et les ressources en pastilles
   qu'on coche — jamais une liste déroulante, où l'on ne voit pas ce qu'on a
   déjà pris. */
/* UNE COLLECTION PAR LIGNE, et son geste au bout de la ligne. Le bouton de la
   maison occupe toute la largeur (`.btn { width: 100% }`) : ici il écrasait le
   libellé, qui passait à deux lignes pendant que « Découvrir » s'étalait sur
   huit cents pixels. C'est `.btn--go` — le bouton compact, déjà employé
   ailleurs — qui convient, et le trait entre deux collections dit où l'une
   s'arrête. */
/* ── UNE ICÔNE PAR NATURE DE CARTE ───────────────────────────────────────────
   Deux listes se ressemblaient : des comptes chez un service, et l'inscription
   de cockpit chez ce même service. Le titre les distinguait, la carte non —
   et c'est la carte qu'on regarde. L'icône du SERVICE marque un raccordement
   (l'ampoule de Hue, demain l'onduleur) ; une carte d'identité marque les
   identifiants d'application. */
.card__icon {
  display: flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  width: 38px;
  color: var(--texte-faible);
  font-size: 15px;
}
.card:hover .card__icon { color: var(--info-fond); }
.card__icon--app { color: var(--texte-tenu); }

.hk__coll { padding: 12px 0; border-top: 1px solid var(--filet); }
.hk__coll:first-of-type { border-top: 0; }
.hk__collhead {
  display: flex; align-items: center;
  gap: 12px; margin-bottom: 8px;
}
.hk__collhead b { flex: 1 1 auto; font-size: 13px; color: var(--texte); }
.hk__collhead .btn { flex: 0 0 auto; }
.hk__found { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.hk__pick {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 9px;
  border: 1px solid var(--bord-fort); border-radius: 3px;
  background: var(--surface);
  font-size: 13px; color: var(--texte);
  cursor: pointer;
}
.hk__pick:hover { border-color: var(--info-bord); }
/* CE QUI EST RETENU SE VOIT SANS LIRE LA CASE : sur trente pastilles, la coche
   seule oblige à balayer trente fois. */
.hk__pick:has(input:checked) {
  border-color: var(--info-bord);
  background: var(--info-creux);
  color: var(--info);
}
/* « Retenir » clôt la liste des pastilles : sa propre ligne, à droite, là où
   l'œil arrive après avoir coché. Posé dans le flot, il se lisait comme une
   pastille de plus — et large de toute la ligne, comme une bannière. */
.hk__keep { flex: 1 0 100%; display: flex; justify-content: flex-end; margin-top: 4px; }
.hk__none { font-size: 12px; color: var(--texte-tenu); }
/* Déclarer un jeu de plus : un geste par service, en bas de la liste — là où
   l'on arrive après avoir lu ce qu'on a déjà. */
.hk__add { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
/* « Ne prête aucune action » DOIT se remarquer sans crier : en gras, la ligne
   de renseignements passait à deux lignes et pesait plus que le nom du
   raccordement. Une teinte suffit. */
.hk__rien { font-style: normal; color: var(--texte-faible); }
/* SOUS QUELLE INSCRIPTION on parle au service : le nom se lit sur la carte,
   avec la même icône que dans l'onglet où on le règle — c'est ce qui relie les
   deux listes sans qu'on ait à l'expliquer. */
.hk__idn { font-style: normal; color: var(--texte-faible); }
.hk__idn i { margin-right: 5px; font-size: 11px; opacity: .75; }

/* ── LE JOURNAL DES ORDRES ───────────────────────────────────────────────────
   « Est-ce que ça pilote vraiment la lampe ? » Sans trace, la seule réponse
   était d'aller regarder la lampe. Quatre colonnes, et la dernière porte le
   verdict : c'est elle qu'on cherche, donc elle est au bout de la ligne, où
   l'œil arrive. */
.hk__log { width: 100%; margin: 8px 0 14px; border-collapse: collapse; font-size: 12px; }
.hk__log td { padding: 5px 8px 5px 0; border-bottom: 1px solid var(--surface-vive); vertical-align: top; }
.hk__log tr:last-child td { border-bottom: 0; }
.hk__log .hk__when { width: 78px; color: var(--texte-tenu); font-variant-numeric: tabular-nums; }
.hk__log .hk__what { color: var(--texte); }
.hk__log .hk__what b { color: var(--texte-fort); font-weight: 600; }
.hk__log .hk__what code { color: var(--info-fond); font-size: 12px; }
.hk__log .hk__who { width: 120px; color: var(--texte-tenu); }
.hk__log .hk__verdict { width: 130px; text-align: right; }
/* Un refus ne se cache pas au milieu des réussites : c'est ce qu'on vient
   chercher ici neuf fois sur dix. */
.hk__log tr.is-bad .hk__what b { color: var(--texte-alerte); }
.hk__vide { margin: 8px 0 14px; }
/* La cadence de relève, à côté de la collection qu'elle règle : c'est là qu'on
   la cherche, et non dans un écran de réglages où elle serait séparée de ce
   qu'elle concerne. */
.hk__poll { flex: 0 0 auto; padding: 0; font-size: 12px; color: var(--texte-faible); }
.hk__poll:hover { color: var(--info); }
.hk__poll i { margin-right: 5px; font-size: 11px; opacity: .8; }
.hk__poll em { margin-left: 6px; font-style: normal; color: var(--info-fond); }


/* ── LA PAGE DE SANTÉ ────────────────────────────────────────────────────────
   Le verdict d'abord, en une ligne : c'est la réponse à la seule question qu'on
   se pose en arrivant. Le détail vient après, pour qui veut savoir POURQUOI.

   La couleur est portée par le BORD GAUCHE, comme pour la cartouche et la
   bannière d'état — c'est l'idiome de la maison pour « voici de quelle nature
   est ce bloc », et il se lit sans lire. */
.verdict {
  display: flex;
  align-items: center;
  gap: 11px;
  margin: 0 0 18px;
  padding: 13px 16px;
  border: 1px solid var(--bord);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--creux);
}
.verdict i { font-size: 17px; }
.verdict b {
  font-family: var(--font-ui);
  font-size: 14px;
  font-weight: 700;
}
.verdict k { margin-left: auto; font-size: 13px; color: var(--texte-faible); }

.verdict.is-ok      { border-left-color: var(--ok); }
.verdict.is-ok i, .verdict.is-ok b           { color: var(--ok); }
.verdict.is-inconnu { border-left-color: var(--bord-vif); }
.verdict.is-inconnu i, .verdict.is-inconnu b { color: var(--texte-faible); }
.verdict.is-alerte  { border-left-color: var(--caution); }
.verdict.is-alerte i, .verdict.is-alerte b   { color: var(--caution); }
.verdict.is-panne   { border-left-color: var(--alerte); }
.verdict.is-panne i, .verdict.is-panne b     { color: var(--texte-alerte); }

/* ── UN CONTRÔLE ─────────────────────────────────────────────────────────────
   Trois lignes au plus : ce qu'on contrôle, ce qu'on a mesuré, et — seulement
   s'il y a lieu — LE GESTE à faire. Un écran de santé qui dit « mémoire :
   128 Mo » sans dire si c'est bien ou mal n'est pas un écran de santé. */
.controles { margin: 0; padding: 0; list-style: none; display: grid; gap: 6px; }
.controle {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  padding: 11px 14px;
  border: 1px solid var(--filet);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--surface);
}
.controle i { margin-top: 2px; font-size: 13px; }
.controle b { display: block; font-size: 13px; font-weight: 600; color: var(--texte-fort); }
.controle span { display: block; margin-top: 2px; font-size: 13px; color: var(--texte-faible); }
/* LE GESTE SE DISTINGUE DE LA MESURE : l'un se lit, l'autre se fait. */
.controle em {
  display: block;
  margin-top: 5px;
  font-style: normal;
  font-size: 13px;
  color: var(--texte);
}
.controle.is-ok      { border-left-color: var(--ok-bord); }
.controle.is-ok i    { color: var(--ok); }
.controle.is-inconnu { border-left-color: var(--bord-vif); }
.controle.is-inconnu i { color: var(--texte-tenu); }
.controle.is-alerte  { border-left-color: var(--caution); }
.controle.is-alerte i { color: var(--caution); }
.controle.is-panne   { border-left-color: var(--alerte); }
.controle.is-panne i { color: var(--texte-alerte); }
/* Ce qui va bien n'a pas à crier : on le voit vert, on passe. */
.controle.is-ok b, .controle.is-ok span { color: var(--texte-faible); }

/* ── LES CHIFFRES DE L'INSTALLATION ──────────────────────────────────────────
   Ce qu'on vient vérifier quand rien ne sonne : ce que pèsent les données, ce
   qui reste de place, combien d'archives on garde. Un contrôle au vert dit
   « rien à faire » et se range en bas de la liste ; ces chiffres-là, on les
   LIT — d'où une grille, où l'œil balaie sans rien ouvrir.

   L'ÉTIQUETTE AU-DESSUS, LA VALEUR EN DESSOUS : dans une grille, l'œil descend
   une colonne avant de traverser une ligne, et c'est la valeur qu'on cherche. */
.chiffres {
  margin: 0 0 4px;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
  gap: 1px;
  background: var(--filet);
  border: 1px solid var(--filet);
  border-radius: var(--radius);
  overflow: hidden;
}
/* LA VALEUR EST CALÉE EN BAS, et c'est nécessaire : une étiquette qui passe sur
   deux lignes pousserait sa valeur d'une ligne, et les chiffres d'une même
   rangée ne seraient plus sur un axe. Le défaut est invisible tant qu'aucun
   libellé n'est long — c'est-à-dire jusqu'au premier qu'on ajoute. */
.chiffres li {
  display: flex;
  flex-direction: column;
  padding: 9px 13px 10px;
  background: var(--surface);
}
.chiffres k {
  display: block;
  font-size: 11px;
  color: var(--texte-tenu);
}
.chiffres b {
  display: block;
  margin-top: auto;
  padding-top: 3px;
  font: 600 15px/1.2 var(--font-ui);
  font-variant-numeric: tabular-nums;
  color: var(--texte-fort);
}

/* ── LE TIROIR D'UNE PORTE ENTRANTE ──────────────────────────────────────────
   Trois choses à copier — l'adresse, le jeton, la ligne prête à coller — et ce
   qui est arrivé. Elles sont EN ÉVIDENCE et non repliées derrière un bouton
   « révéler » : celui qui ouvre ce tiroir vient précisément les chercher, et
   la carte fermée, elle, n'en montre rien. */
.hkin__part { margin: 0 0 14px; }
.hkin__title {
  display: block;
  margin-bottom: 5px;
  font-size: 11px;
  color: var(--texte-tenu);
}
.hkin__note { display: block; margin-top: 5px; font-size: 12px; color: var(--texte-tenu); }
.hkin__note code { font-size: 12px; color: var(--texte-faible); }

/* UN SECRET SE COPIE, il ne se retape pas : le curseur le dit, et un clic
   suffit. La police à chasse fixe n'est pas un ornement — c'est ce qui permet
   de distinguer un zéro d'un O quand il faut vérifier à l'œil. */
.hkin__code {
  display: block;
  padding: 9px 12px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--fond);
  color: var(--texte);
  font: 13px/1.5 var(--font-mono);
  word-break: break-all;
  cursor: copy;
}
.hkin__code:hover { border-color: var(--info-bord); color: var(--texte-fort); }
.hkin__code--bloc { white-space: pre-wrap; word-break: normal; }

.hkin__log { margin: 0; padding: 0; list-style: none; }
.hkin__l {
  display: grid;
  grid-template-columns: 148px 1fr auto;
  gap: 10px;
  align-items: baseline;
  padding: 5px 0;
  border-top: 1px solid var(--surface-vive);
  font-size: 12px;
}
.hkin__l:first-child { border-top: 0; }
.hkin__l time { color: var(--texte-tenu); font-family: var(--font-mono); }
.hkin__l span { color: var(--texte); }
.hkin__l k { color: var(--texte-tenu); white-space: nowrap; }
.hkin__l.is-bad span { color: var(--texte-alerte); }

.hkin__acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }

/* ── LA JAUGE DE CADENCE DU PALMARÈS ─────────────────────────────────────────

   Personne ne sait de mémoire si huit cents enregistrements par jour est
   beaucoup. Ce qui se voit en revanche du premier coup d'œil, c'est qu'une
   ligne en fait quarante fois plus que sa voisine — et c'est celle-là qu'on
   vient chercher.

   La barre est donc RELATIVE à la plus forte de la page, jamais à une norme :
   ce qui est bavard sur une chaufferie est ordinaire sur un compteur. Elle se
   pose DERRIÈRE le nombre et non à côté — une colonne de plus aurait poussé
   l'étendue hors de l'écran, et le chiffre reste ce qu'on lit. */
.tspal {
  position: relative;
  display: inline-block;
  padding: 1px 6px;
  border-radius: 3px;
  /* Le dégradé s'arrête net à `--part` : c'est une jauge, pas un fondu. */
  background: linear-gradient(to right,
              color-mix(in srgb, var(--av-cyan) 22%, transparent) var(--part, 0%),
              transparent var(--part, 0%));
}

/* ── UN SECOND SUJET DANS UN PANNEAU ─────────────────────────────────────────

   La maison ne découpait jamais un panneau : chaque onglet portait un sujet, et
   c'était juste. « Outils » en porte deux — ce qui coûte, et ce qui est faux —
   et ils ne se rangent pas l'un dans l'autre : on vient voir le premier pour
   décider quoi purger, le second pour savoir si l'on peut croire ce qu'on lit.

   Pas de composant de plus pour autant : une règle sur l'élément, là où la
   coupure existe. Le filet dit la frontière, la marge la respire, et le reste
   du panneau ne change pas. */
/* L'attribut est doublé comme celui du plancher d'interligne plus haut : sans
   cela le plancher, plus spécifique, ramenait à douze pixels la respiration de
   trente qui marque une coupure de sujet. Une coupure doit se voir. */
[data-panel][data-panel] > h3 {
  /* TRENTE AU-DESSUS, SEIZE EN DESSOUS. Un titre doit être plus près de ce
     qu'il annonce que de ce qui le précède — c'est la proximité qui dit le
     groupement. Mais douze pixels ne suffisaient pas ici : le titre fait quinze
     pixels en gras, et la phrase qui suit commence elle aussi par un mot en
     gras. Les deux lignes se lisaient comme un seul bloc, et l'on ne voyait
     plus le titre. */
  margin: 30px 0 16px;
  padding-top: 22px;
  border-top: 1px solid var(--bord);
  font-size: 15px;
  color: var(--texte-fort);
}

/* ── UN TABLEAU MONTRÉ EN ENTIER ─────────────────────────────────────────────

   `.poolkeys` est le tiroir d'une carte : hauteur bornée, défilement propre,
   en-tête collant. C'est juste là où il vit — une série de huit cents clés
   dépliée sous une carte chasserait tout ce qui la suit.

   Le palmarès n'est pas dans une carte. Sa longueur se choisit au-dessus de
   lui, et la borner une seconde fois ferait défiler dans un cadre ce qu'on
   vient de demander en entier.

   ── ET L'EN-TÊTE CESSE DE COLLER, PARCE QU'IL LE FAUT ───────────────────────

   `position: sticky` se résout contre le plus proche ancêtre qui DÉFILE. Sans
   conteneur qui défile, ce n'est plus le tiroir mais la PAGE : l'en-tête se
   serait décroché pour flotter sous la barre du haut, par-dessus le reste de
   l'écran. Un tableau montré en entier n'a de toute façon rien à quoi coller —
   on le lit d'un bout à l'autre. */
/* Plus d'exception pour le palmarès : maintenant que plus rien ne défile pour
   soi-même, son en-tête colle contre `main.site` comme celui des tiroirs, et
   les titres de colonnes tiennent sur deux cents lignes. */

/* ── LES RAPPORTS ───────────────────────────────────────────────────────────
   Deux colonnes : la recette à gauche, le document à droite. Le partage n'est
   pas égal et ne doit pas l'être — on compose en regardant l'aperçu, pas le
   formulaire, et une colonne de champs plus large ne rendrait rien de plus.

   L'APERÇU COLLE. Un rapport de quatre sections dépasse la hauteur d'écran ;
   sans `sticky`, faire défiler jusqu'à la dernière section ferait sortir le
   document du champ — c'est-à-dire la seule chose qu'on regarde. */
.rap {
  display: grid;
  grid-template-columns: minmax(320px, 420px) 1fr;
  gap: 24px;
  align-items: start;
}
.rap__form { min-width: 0; }
.rap__form textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--creux);
  color: var(--texte-fort);
  font: inherit;
  resize: vertical;
}
.rap__form textarea:focus {
  outline: none;
  border-color: var(--info);
  box-shadow: 0 0 0 3px rgba(63, 200, 240, .14);
}

.rap__sections-tete {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 22px 0 10px;
}
.rap__sections-tete h2 { margin: 0; font-size: 14px; flex: 1 1 auto; }

.rap__apercu {
  position: sticky;
  top: 16px;
  min-width: 0;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--creux);
  overflow: hidden;
}
.rap__apercu-tete {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--bord);
}
.rap__apercu-tete b { font-size: 12px; }
.rap__apercu-tete .hint { flex: 1 1 auto; margin: 0; font-size: 11px; }

/* ── LE DOCUMENT GARDE SON FOND BLANC ───────────────────────────────────────
   Même en thème sombre, et c'est voulu : un rapport s'imprime et s'ouvre dans
   un client de courriel. L'afficher sur fond sombre montrerait une page qui
   n'existe pas, et ferait juger une mise en page sur des couleurs qu'elle
   n'aura jamais. */
#rap-cadre {
  display: block;
  width: 100%;
  height: 72vh;
  border: 0;
  background: #fff;
}

/* ── UNE SECTION ────────────────────────────────────────────────────────────
   Encadrée, parce qu'ici l'encadrement SERT : il dit où finit une section et où
   commence la suivante, dans une pile de blocs qui portent tous les mêmes
   champs. C'est l'inverse de la règle du document rendu, où rien ne se répète. */
.rapsec {
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  padding: 12px;
  margin-bottom: 10px;
  background: var(--creux);
}
.rapsec__tete { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.rapsec__tete input[data-titre] {
  flex: 1 1 auto;
  min-width: 0;
  padding: 7px 10px;
  border: 1px solid var(--bord);
  border-radius: var(--radius);
  background: var(--fond);
  color: var(--texte-fort);
  font: inherit;
  font-size: 12px;
}
.rapsec__tete input[data-titre]:focus { outline: none; border-color: var(--info); }

.rapsec__agregats { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.rapsec__agregats label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border: 1px solid var(--bord);
  border-radius: 999px;
  font-size: 11px;
  color: var(--texte-faible);
  cursor: pointer;
  user-select: none;
}
.rapsec__agregats label:has(input:checked) {
  border-color: var(--info-bord);
  color: var(--info);
}
.rapsec__agregats input { accent-color: var(--info); margin: 0; }

.rapsec__lignes { list-style: none; margin: 0 0 10px; padding: 0; }
.rapsec__lignes li {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 0;
  border-bottom: 1px solid var(--filet);
}
.rapsec__lignes li:last-child { border-bottom: 0; }
.rapsec__lignes input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 5px 8px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: transparent;
  color: var(--texte-fort);
  font: inherit;
  font-size: 12px;
}
.rapsec__lignes input:focus {
  outline: none;
  border-color: var(--info);
  background: var(--fond);
}
/* La clé sous le libellé : on la lit pour vérifier, on ne la modifie pas. */
.rapsec__lignes code {
  font-size: 10px;
  color: var(--texte-faible);
  white-space: nowrap;
}

.rapsec__ajout { display: flex; gap: 8px; flex-wrap: wrap; }
.rapsec__ajout .search { flex: 1 1 140px; min-width: 0; }

@media (max-width: 1100px) {
  .rap { grid-template-columns: 1fr; }
  .rap__apercu { position: static; }
  #rap-cadre { height: 60vh; }
}

/* ── DES ONGLETS À L'INTÉRIEUR D'UN VOLET ───────────────────────────────────
   Ceux du haut de page découpent l'ÉCRAN ; ceux-ci découpent une colonne. Leur
   marge haute les décollerait du titre de la colonne pour rien, et leur taille
   d'origine les mettrait au même rang que ceux de l'écran — deux barres
   d'onglets de même poids, dont on ne saurait plus laquelle commande l'autre. */
.tabs--fin { margin-top: 0; margin-bottom: 14px; }
.tabs--fin .tab { padding: 6px 10px; font-size: 12px; }

/* ── LES ÉDITIONS, DANS LE TIROIR D'UNE CARTE ───────────────────────────────
   Une liste dans une liste : elle doit se lire comme un relevé, pas comme une
   seconde collection de cartes. D'où les filets seuls, sans fond ni bord —
   c'est la discipline du document rendu, appliquée à l'écran. */
/* ── LA PROGRESSION D'UNE GÉNÉRATION, SUR LA CARTE DU RAPPORT ───────────────
   Le texte se centre à côté des icônes ; la jauge est un filet sur toute la
   largeur, au pied de la rangée — comme celui de la barre d'état de l'éditeur. */
.card__row { position: relative; }
.rapgen {
  align-self: center;
  flex: 0 1 auto; min-width: 0; max-width: 360px; padding: 0 10px;
  font-size: 12px; color: var(--info);
}
.rapgen[hidden] { display: none; }
.rapgen__t { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rapgen__j {
  position: absolute; left: 0; right: 0; bottom: -1px;
  height: 2px; overflow: hidden; pointer-events: none;
}
.rapgen__j i { display: block; height: 100%; width: 0; background: var(--info); transition: width .3s ease; }

.raped { list-style: none; margin: 0; padding: 0; }
.raped li {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 0;
  border-top: 1px solid var(--filet);
  font-size: 12px;
}
.raped li:first-child { border-top: 0; }
.raped__quand { flex: 0 0 auto; font-variant-numeric: tabular-nums; color: var(--texte-fort); }

/* ── LA PASTILLE D'ÉTAT, EN TÊTE DE LIGNE ────────────────────────────────────
   On ouvre ce tiroir pour une question précise — « est-ce parti ? », « qu'est-ce
   qui a cloché mardi ? » — et non pour lire quinze lignes. La couleur y répond
   avant la lecture : le rouge saute aux yeux, le reste se laisse survoler. */
.raped__etat {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 20px; height: 20px;
  border-radius: 50%;
  font-size: 9px;
}
.raped li.is-emis .raped__etat {
  background: color-mix(in srgb, var(--ok) 15%, transparent);
  color: var(--ok);
}
.raped li.is-archive .raped__etat { background: var(--creux); color: var(--texte-tenu); }
.raped li.is-incomplet .raped__etat {
  background: color-mix(in srgb, var(--caution) 15%, transparent);
  color: var(--caution);
}
.raped li.is-erreur .raped__etat {
  background: color-mix(in srgb, var(--alerte) 18%, transparent);
  color: var(--texte-alerte);
}
.raped__quoi { flex: 1 1 auto; min-width: 0; color: var(--texte-faible); }
.raped__quoi b { color: var(--texte-fort); font-weight: 600; }
.raped__actions { flex: 0 0 auto; display: flex; gap: 4px; }
/* PAS DE CADRE AUX ICÔNES. C'est la règle de la maison, et `.card__inline` la
   pose déjà pour les boutons d'une carte : un rang d'icônes encadrées met
   autant de rectangles côte à côte qu'il y a de gestes, et la carte se lit
   alors comme une barre d'outils plutôt que comme une ligne. Le cadre ne
   revient qu'au survol, là où il désigne. */
.raped__actions .iconbtn { border-color: transparent; }
.raped__actions .iconbtn:hover { border-color: transparent; background: var(--filet); }
/* Une édition dont le lien public est ouvert : ça se voit sans l'ouvrir. */
.raped__actions .iconbtn.is-on { color: var(--info); }
.raped__none { padding: 8px 0; font-size: 12px; color: var(--texte-tenu); }

/* ── CE QUI N'EST PAS SORTI SE LIT AVEC CE QUI EST SORTI ─────────────────────
   Et en tête, parce que c'est ce qui appelle une décision : une édition parue
   se consulte quand on en a besoin, un refus demande qu'on aille voir. Au bas
   de la liste, il aurait été caché derrière l'historique — c'est-à-dire pas
   signalé du tout.

   Le filet de gauche porte l'alerte, et non le fond : une bande rouge sur
   quatre lignes ferait paniquer pour trois séries muettes qui reviendront au
   prochain relevé. */
/* ── LA TÊTE DU JOURNAL DES ÉCHECS ───────────────────────────────────────────
   Le compte, et de quoi faire le ménage. Le bouton est DISCRET et nommé :
   « Effacer ce journal » dit ce qu'il efface — les lignes rouges — et non les
   documents, qui ont été envoyés à des gens et se suppriment un par un. */
.raped__tete {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
  color: var(--texte-alerte);
  font-size: 11.5px;
  font-weight: 600;
}
/* Le ménage est une ICÔNE : le compte à côté dit déjà de quoi il s'agit, et un
   bouton nommé au bout d'une ligne de trois mots pèse plus lourd que ce qu'il
   fait. Son intitulé vit dans l'infobulle, où il précise ce qui n'est PAS
   touché — les documents parus. */
.raped__tete .iconbtn {
  width: 24px; height: 24px;
  border-color: transparent;
  color: var(--texte-tenu);
  font-size: 11px;
}
.raped__tete .iconbtn:hover { border-color: var(--alerte); color: var(--texte-alerte); }

.raped--rates { margin-bottom: 10px; }
.raped--rates li {
  /* Des deux côtés : le fond teinté fait une boîte, et une boîte dont le texte
     touche le bord se lit comme un débordement. */
  padding: 8px 10px 8px 9px;
  border-top: 0;
  border-left: 2px solid var(--alerte);
  margin-bottom: 2px;
  background: color-mix(in srgb, var(--alerte) 6%, transparent);
  border-radius: 0 4px 4px 0;
}
/* ── UNE ERREUR QUI MÈNE QUELQUE PART LE DIT ─────────────────────────────────
   La flèche ne paraît qu'au survol, mais le curseur change tout de suite : on
   apprend qu'une ligne est cliquable en la survolant, pas en lisant une icône
   de plus sur toutes les autres. */
.raped--rates li.is-ouvrable { cursor: pointer; }
.raped--rates li.is-ouvrable:hover {
  background: color-mix(in srgb, var(--alerte) 12%, transparent);
}
.raped__aller { opacity: 0; color: var(--texte-alerte); font-size: 11px; }
.raped--rates li.is-ouvrable:hover .raped__aller { opacity: 1; }

.raped--rates .raped__quand { color: var(--texte-alerte); }
.raped--rates .raped__quoi b { color: var(--texte-alerte); }

/* Le motif sous la ligne : « 3 séries sans donnée » se lit, « erreur » non. */
.raped__quoi k {
  display: block;
  margin-top: 1px;
  color: var(--texte-faible);
  font-size: 11px;
  font-style: normal;
}
/* D'où venait la demande — à la main, ou le planificateur. Un refus qu'on a
   provoqué en cliquant se corrige tout de suite ; un refus nocturne demande
   qu'on aille voir. */
.raped__source {
  color: var(--texte-tenu);
  font-size: 10.5px;
  font-style: normal;
}

/* ── À QUI LE DOCUMENT EST PARTI ─────────────────────────────────────────────
   Les adresses de l'envoi, telles qu'elles étaient CE JOUR-LÀ : « émis à 3 »
   oblige à ouvrir les réglages pour voir la liste d'aujourd'hui, qui n'est pas
   forcément celle de l'envoi. */
.raped__qui {
  display: block;
  margin-top: 1px;
  overflow: hidden;
  color: var(--texte-tenu);
  font-size: 10.5px;
  font-style: normal;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* La phrase du dictionnaire, sous la clé, dans les tableaux de clés — le même
   objet que `.poolkeys__dit` côté DCF, et la même retenue : elle explique, elle
   n'identifie pas. C'est la CLÉ qui fait référence, et elle reste au-dessus. */
.tskey__dit {
  display: block;
  max-width: 42ch;
  margin-top: 1px;
  color: var(--texte-faible);
  font-family: var(--font-ui);
  font-size: 11.5px;
  line-height: 1.35;
}

/* ── Ce qu'on donne au planificateur (Réglages › Battement) ─────────────────── */
.tickacces { display: grid; gap: 14px; margin: 14px 0 18px; }
.tickacces__ligne { display: grid; gap: 6px; }
.tickacces__ligne .hint { margin: 0; }
.tickacces__pied .hint { margin: 0; }
