/* MeshCore MQTT Monitoring — theme moderne clair/sombre.
 * Couleurs par role (palette data-viz validee) : statut good/critical fixes,
 * teinte serie unique (bleu) pour les magnitudes. Sombre par defaut via
 * prefers-color-scheme, bascule manuelle par data-theme sur <html>. */

:root {
  color-scheme: light;
  --page:        #f9f9f7;
  --surface:     #fcfcfb;
  --surface-2:   #f0efec;
  --ink:         #0b0b0b;
  --ink-2:       #52514e;
  --muted:       #898781;
  --hairline:    rgba(11, 11, 11, 0.10);
  --grid:        #e1e0d9;
  --accent:      #2a78d6;
  --accent-soft: rgba(42, 120, 214, 0.12);
  --good:        #0ca30c;
  --good-text:   #006300;
  --warning:     #b97f00;
  --critical:    #d03b3b;
  /* series des courbes multi-relais (vue Charge) — palette categorielle
     validee daltonisme, ordre FIXE par relais (jamais recyclee) */
  --s1: #256abf; --s2: #c2410c; --s3: #00937f;
  --s4: #7c3aed; --s5: #c02066; --s6: #647006;
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --shadow: 0 1px 2px rgba(11, 11, 11, 0.05), 0 4px 16px rgba(11, 11, 11, 0.04);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --page:        #0d0d0d;
    --surface:     #1a1a19;
    --surface-2:   #242422;
    --ink:         #ffffff;
    --ink-2:       #c3c2b7;
    --muted:       #898781;
    --hairline:    rgba(255, 255, 255, 0.10);
    --grid:        #2c2c2a;
    --accent:      #3987e5;
    --accent-soft: rgba(57, 135, 229, 0.16);
    --good:        #0ca30c;
    --good-text:   #0ca30c;
    --warning:     #fab219;
    --critical:    #e66767;
    --s1: #4d8ee0; --s2: #e76b1d; --s3: #0aa38f;
    --s4: #9b7df1; --s5: #e5539c; --s6: #8c9c12;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 20px rgba(0, 0, 0, 0.35);
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --page:        #0d0d0d;
  --surface:     #1a1a19;
  --surface-2:   #242422;
  --ink:         #ffffff;
  --ink-2:       #c3c2b7;
  --muted:       #898781;
  --hairline:    rgba(255, 255, 255, 0.10);
  --grid:        #2c2c2a;
  --accent:      #3987e5;
  --accent-soft: rgba(57, 135, 229, 0.16);
  --good:        #0ca30c;
  --good-text:   #0ca30c;
  --warning:     #fab219;
  --critical:    #e66767;
  --s1: #4d8ee0; --s2: #e76b1d; --s3: #0aa38f;
  --s4: #9b7df1; --s5: #e5539c; --s6: #8c9c12;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 20px rgba(0, 0, 0, 0.35);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* ---- Coquille d'application ----------------------------------------------
 * Avant : la page entiere defilait, et chaque vue calculait sa hauteur avec un
 * nombre magique (« 100vh - 230px », « - 150px », « - 96px »). Ces constantes
 * ne tombaient juste sur aucun gabarit reel : d'ou des blancs sous certaines
 * vues et un debordement sur d'autres, selon la hauteur de l'en-tete.
 * Desormais : la fenetre est une colonne flex — en-tete, contenu, pied — et
 * SEUL le contenu defile, a l'interieur de la vue qui en a besoin. Plus aucune
 * vue n'a besoin de connaitre la hauteur de ses ancetres. */
html { height: 100%; }
body {
  background: var(--page);
  color: var(--ink);
  font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
  height: 100%;
  display: flex; flex-direction: column;
  overflow: hidden;              /* le scroll GLOBAL disparait */
}
a { color: var(--accent); text-decoration: none; }
.muted { color: var(--muted); font-size: 12.5px; }

/* ---- Header ------------------------------------------------------------- */
.topbar {
  /* 2000 et non 20 : les panneaux Leaflet de la carte montent a z-index 1000
     (controles, popups). A 20, le menu utilisateur se depliait DERRIERE la
     carte — il ne semblait fonctionner que depuis le tableau de bord. Les
     modales restent au-dessus (11000+). */
  position: relative; z-index: 2000; flex: none;
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 10px 20px;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--hairline);
}
.brand { display: flex; align-items: center; gap: 9px; font-size: 15.5px; font-weight: 650; }
.brand-mark { color: var(--accent); font-size: 18px; }
.brand-thin { font-weight: 400; color: var(--ink-2); }
.topbar-right { display: flex; align-items: center; gap: 10px; }
.iapc-logo { height: 26px; display: block; border-radius: 4px; }

.pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 11px; border-radius: 999px;
  border: 1px solid var(--hairline); background: var(--surface);
  font-size: 12.5px; color: var(--ink-2); white-space: nowrap;
}
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
.dot.up   { background: var(--good); }
.dot.down { background: var(--critical); }
.dot.pulse.on { background: var(--good); animation: pulse 2s ease-out infinite; }
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--good) 45%, transparent); }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
.icon-btn {
  border: 1px solid var(--hairline); background: var(--surface); color: var(--ink-2);
  border-radius: 999px; width: 30px; height: 30px; cursor: pointer; font-size: 15px;
  display: inline-flex; align-items: center; justify-content: center; padding: 0;
}
.icon-btn:hover { color: var(--ink); border-color: var(--muted); }

/* [hidden] doit TOUJOURS gagner : tout display explicite (.icon-btn en
   inline-flex, etc.) bat sinon le style UA et re-affiche des elements caches
   par permission (incident : bouton Liste noire visible sans le droit ban). */
[hidden] { display: none !important; }

/* ---- Layout ------------------------------------------------------------- */
main {
  max-width: 1720px; width: 100%; margin: 0 auto;
  flex: 1 1 auto; min-height: 0;     /* min-height:0 : sans lui un enfant flex
                                        refuse de retrecir et deborde */
  display: flex; flex-direction: column;
  overflow: hidden;                  /* c'est la VUE qui defile, pas main */
  padding: 0 20px;                   /* la gouttiere verticale passe aux vues */
}
/* Chaque vue remplit la hauteur disponible. */
main > * { flex: 1 1 auto; min-height: 0; }

/* Vues qui EMPILENT des panneaux : elles defilent a l'interieur. */
#overview, #view-admin, #view-usb, #view-companion, #view-maint,
#view-cli, #view-journal, #view-frames, #view-synthese, #view-reglages {
  overflow-y: auto; overflow-x: hidden; padding: 18px 0 24px;
}
/* Vue Synthese : premiere version (tableaux) rangee en sous-onglets — le
   bandeau de tuiles reste toujours visible, chaque sous-vue tient sur un
   ecran sans defiler. */
.syn-cle { font-family: var(--mono); font-size: 12px; }
#view-synthese .tele-subtabs { margin-bottom: 14px; }
/* Tuiles compactes : six tuiles sur UNE ligne d'un portable, pas deux. */
#view-synthese .flood-stats { gap: 10px; margin-bottom: 12px; }
#view-synthese .fs-bloc { flex-basis: 130px; padding: 8px 12px; }
#view-synthese .fs-v { font-size: 20px; margin: 1px 0; }
#view-synthese .fs-s { font-size: 11px; }
/* Deux colonnes par sous-vue : empiles, les tableaux depassaient l'ecran. */
.syn-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 0 28px; align-items: start; }
@media (max-width: 1100px) { .syn-2col { grid-template-columns: 1fr; } }
.syn-plus { font-size: 12px; color: var(--muted); padding-top: 6px; }
.syn-alerte {
  padding: 8px 0; font-size: 14px; line-height: 1.5;
  border-bottom: 1px solid var(--hairline);
}
.syn-alerte:last-of-type { border-bottom: 0; }

/* ---- Visite guidee -------------------------------------------------------
   Un voile assombrit la page, un halo detoure l'element explique, la bulle
   porte le texte et les boutons. Au-dessus de la topbar (z 2000) et des
   panneaux Leaflet, sous rien : c'est un mode a part entiere. */
#visite-voile { position: fixed; inset: 0; background: rgba(0, 0, 0, .5); z-index: 11500; }
#visite-halo {
  position: fixed; z-index: 11510; pointer-events: none;
  border: 2px solid var(--accent); border-radius: 12px;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 35%, transparent),
              0 0 26px color-mix(in srgb, var(--accent) 45%, transparent);
  transition: left .25s ease, top .25s ease, width .25s ease, height .25s ease;
}
#visite-bulle {
  position: fixed; z-index: 11520; width: min(400px, calc(100vw - 20px));
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 14px; padding: 16px 18px 14px; box-shadow: var(--shadow);
}
.visite-titre { font-size: 16px; font-weight: 700; margin-bottom: 6px; }
.visite-texte { font-size: 13.5px; color: var(--ink-2); line-height: 1.55; }
.visite-actions { display: flex; align-items: center; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
.visite-etape { font-size: 12px; color: var(--muted); margin-right: auto; font-variant-numeric: tabular-nums; }
.visite-principal { background: var(--accent); border-color: var(--accent); color: #fff; }
.visite-principal:hover { filter: brightness(1.1); }
#visite-passer { color: var(--muted); }

/* Campagne « appliquer les adverts au parc » : liste de nodes a cocher,
   suivi ligne par ligne. */
.advcamp-liste {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(min(230px, 100%), 1fr));
  gap: 6px 16px; margin: 12px 0;
}
.advcamp-node { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; }
.advcamp-ligne {
  display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
  padding: 5px 0; font-size: 13px; border-bottom: 1px solid var(--hairline);
}
.advcamp-ligne:last-of-type { border-bottom: 0; }
.advcamp-fin { margin-top: 8px; font-weight: 650; }
#advcamp-etat { margin-top: 10px; max-height: 240px; overflow-y: auto; }
/* Sous-onglet Automatisation : cibles + mot de passe PARTAGES en tete, puis
   des cadres d'action au meme gabarit — leger et lisible (retour 28.08). */
.auto-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 14px; margin-bottom: 16px;
}
/* Grand ecran : les quatre actions sur UNE ligne (demande 28.08). */
@media (min-width: 1100px) { .auto-grid { grid-template-columns: repeat(4, 1fr); } }
.auto-carte {
  border: 1px solid var(--hairline); border-radius: 12px; background: var(--surface-2);
  padding: 14px 16px 12px; display: flex; flex-direction: column; gap: 9px; min-width: 0;
}
.auto-carte h3 { margin: 0; font-size: 14.5px; font-weight: 650; }
.auto-champ { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; }
.auto-champ input { width: 110px; }
.auto-cmd {
  font-family: var(--mono); font-size: 12px; color: var(--muted);
  background: var(--surface); border-radius: 6px; padding: 4px 9px;
}
.auto-boutons { display: flex; gap: 8px; flex-wrap: wrap; }
.auto-note { font-size: 11.5px; color: var(--muted); line-height: 1.45; }
.auto-carte > .btn { align-self: flex-start; }
.auto-pwrow { margin: 10px 0 6px; max-width: 540px; }
.auto-pwrow input { width: 100%; }
/* Curseur temporel de la couverture (carte) */
.map-cov-temps-bloc { display: flex; align-items: center; gap: 8px; width: 100%; }
.map-cov-temps-bloc input[type="range"] { flex: 1 1 120px; min-width: 100px; accent-color: var(--accent); }

/* Trames — affichage simplifie : resume des ecoutes, depliable au clic. */
.rx-resume { margin-top: 3px; font-size: 12px; color: var(--muted); }
.rx-via { margin-top: 2px; font-size: 12px; color: var(--muted); }
.rx-via b { color: var(--ink-2); font-weight: 600; }
.via-fp { font-family: var(--mono); font-size: 11px; }
.rx-toggle { cursor: pointer; user-select: none; }
.rx-toggle:hover { color: var(--ink-2); }
.rx-chev { font-weight: 400; }

/* Admin RESTREINT (advert-config et/ou sites-config, sans admin) : l'onglet
   Admin ne montre que les interfaces couvertes par ces droits. Tout le
   reste — comptes, alertes, maintenance, Config IAPC — disparait. */
body.admin-restreint #view-admin .adm-subtabs { display: none; }
body.admin-restreint.dr-sites #view-admin .adm-subtabs { display: flex; }
body.admin-restreint.dr-sites:not(.dr-advert) #adm-subtab-general { display: none; }
body.admin-restreint.dr-sites #adm-subtab-iapc { display: none; }
body.admin-restreint #adm-view-iapc { display: none; }
body.admin-restreint:not(.dr-sites) #adm-view-sites { display: none; }
body.admin-restreint #adm-view-general > .panel { display: none; }
body.admin-restreint.dr-advert #adm-view-general > .panel.adm-adverts { display: block; }
/* Les ROLES distribuent les droits de tout le monde : editer un role, c'est
   editer tous les comptes qui le portent. Reserve au droit `admin` complet —
   un compte ouvert seulement aux fiches de site ou aux adverts n'a rien a y
   faire. Le serveur refuse deja /api/roles sans `admin` ; ceci evite de
   montrer une porte qui se fermerait au clic. */
body.admin-restreint #adm-subtab-roles,
body.admin-restreint #adm-view-roles { display: none; }

/* Le ⓘ CLIGNOTE tant que le compte n'a pas suivi (ou passe) la visite :
   c'est l'invitation. La classe est posee par majVisiteBadge(), retiree des
   que la visite est terminee ou passee. */
#visite-btn.attire {
  color: var(--accent); border-color: var(--accent);
  animation: visite-appel 1.5s ease-in-out infinite;
}
@keyframes visite-appel {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent); opacity: 1; }
  50%      { box-shadow: 0 0 0 7px color-mix(in srgb, var(--accent) 0%, transparent); opacity: .55; }
}
@media (prefers-reduced-motion: reduce) {
  #visite-btn.attire { animation: none; }   /* l'accent de couleur suffit */
}
/* Vues a mise en page interne : rien ne deborde, les enfants gerent leur
   propre defilement (carte plein cadre, salons en trois colonnes). */
#view-map { overflow: hidden; padding: 0; }
#view-salons { overflow: hidden; padding: 14px 0; display: flex; flex-direction: column; }
/* Telemetrie : l'en-tete de sous-onglets reste fixe, la sous-vue defile. */
#view-tele { overflow: hidden; padding: 18px 0 0; display: flex; flex-direction: column; }
#view-tele > .tele-subtabs { flex: none; }
#tele-view-nodes, #tele-view-radio, #tele-view-load,
#tele-view-prop, #tele-view-vois, #tele-view-flood {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-bottom: 20px;
}
/* 1720 et non plus 1400 : le tableau de telemetrie compte 14 colonnes et
   se faisait tronquer des qu'on en ajoutait une. Un max-width ne force
   rien — sur un ecran plus etroit la page occupe la fenetre comme avant. */

/* ---- Tuiles stats -------------------------------------------------------- */
.stats-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  margin-bottom: 14px;
}
.stat-card {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 12px; padding: 13px 16px 11px; box-shadow: var(--shadow);
}
.stat-card .k {
  font-size: 11px; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 4px;
}
.stat-card .v { font-size: 27px; font-weight: 650; line-height: 1.1; }
.stat-card .sub { font-size: 12px; color: var(--ink-2); margin-top: 3px; }

/* ---- Panneaux (gateways / types) ----------------------------------------- */
.panels {
  display: grid; gap: 12px; grid-template-columns: 1fr 1fr;
  margin-bottom: 16px;
}
@media (max-width: 860px) { .panels { grid-template-columns: 1fr; } }
.panel {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 12px; padding: 14px 16px; box-shadow: var(--shadow);
  display: flex; flex-direction: column;   /* la liste interne peut remplir la hauteur */
}
.panel-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 10px; }
.panel-head h2 { margin: 0; font-size: 13.5px; font-weight: 650; }
.panel-sub { font-size: 12px; color: var(--muted); }

.canton-filter {
  font-size: 12px; padding: 4px 8px; border-radius: 8px;
  border: 1px solid var(--hairline); background: var(--page); color: var(--ink);
  max-width: 160px;
}
.ren-tag { font-size: 10.5px; color: var(--muted); font-weight: 500; }
/* Type de reseau du node : INET (internet) / HNET (HAMNET failover actif) */
.net-badge {
  display: inline-block; font-size: 9.5px; font-weight: 700; letter-spacing: .3px;
  padding: 1px 5px; border-radius: 5px; margin-left: 5px; vertical-align: middle;
  border: 1px solid transparent;
}
.net-badge.inet { color: #2563a8; background: rgba(37, 99, 168, .12); border-color: rgba(37, 99, 168, .35); }
.net-badge.hnet { color: #b5651d; background: rgba(181, 101, 29, .14); border-color: rgba(181, 101, 29, .40); }
@media (prefers-color-scheme: dark) {
  .net-badge.inet { color: #7db6f0; background: rgba(125, 182, 240, .14); border-color: rgba(125, 182, 240, .34); }
  .net-badge.hnet { color: #e0a86a; background: rgba(224, 168, 106, .16); border-color: rgba(224, 168, 106, .40); }
}
:root[data-theme="dark"] .net-badge.inet { color: #7db6f0; background: rgba(125, 182, 240, .14); border-color: rgba(125, 182, 240, .34); }
:root[data-theme="dark"] .net-badge.hnet { color: #e0a86a; background: rgba(224, 168, 106, .16); border-color: rgba(224, 168, 106, .40); }
:root[data-theme="light"] .net-badge.inet { color: #2563a8; background: rgba(37, 99, 168, .12); border-color: rgba(37, 99, 168, .35); }
:root[data-theme="light"] .net-badge.hnet { color: #b5651d; background: rgba(181, 101, 29, .14); border-color: rgba(181, 101, 29, .40); }

/* Gateways : grille de CARTES (dot + nom + badge reseau, puis statut + meta) */
.gw-list {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 6px;
  align-content: start; grid-auto-rows: min-content;
  flex: 1 1 auto; min-height: 0; max-height: 620px; overflow-y: auto; padding-right: 2px;
}
/* carte compacte : tout sur une ligne (dot · nom · reseau · etat · chips) */
.gw-card {
  display: flex; flex-direction: row; align-items: center; gap: 4px 8px;
  flex-wrap: wrap; border: 1px solid var(--hairline);
  border-radius: 9px; background: var(--surface-2); padding: 6px 10px;
  transition: border-color .15s, background .15s, box-shadow .15s;
}
.gw-card.offline { opacity: .6; }
.gw-card:hover {
  background: color-mix(in srgb, var(--surface-2) 88%, var(--ink));
  box-shadow: 0 1px 6px rgba(0, 0, 0, .12);
}
.gw-card-name {
  font-weight: 700; font-size: 13px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.gw-card-status { font-size: 11.5px; color: var(--muted); white-space: nowrap; }
/* chips repoussees a droite : les noms restent alignes d'une carte a l'autre */
.gw-card-meta { display: flex; flex-wrap: wrap; gap: 4px 5px; margin-left: auto; }
.gw-meta-item {
  font-size: 10.5px; color: var(--muted); background: var(--surface);
  border: 1px solid var(--hairline); border-radius: 5px; padding: 1px 6px;
}
.gw-meta-item.cant { text-transform: uppercase; letter-spacing: .3px; font-weight: 600; }
.gw-meta-item.fw { font-weight: 600; font-variant-numeric: tabular-nums; }
/* Revision de carte (V4.2 / V4.3) : voisine de la version firmware, mais ce
   n'est PAS la meme information — l'une se met a jour, l'autre est le
   materiel. Teinte legerement soutenue pour qu'on ne les confonde pas au
   premier coup d'oeil. */
.gw-meta-item.board {
  font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--ink-2);
}
/* SSID : un nom de reseau peut etre long, on le tronque plutot que de
   deformer la carte — la valeur complete reste dans l'infobulle. */
.gw-meta-item.ssid {
  max-width: 16ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* barres horizontales par type — teinte unique, extremite arrondie 4px */
.type-bars { display: flex; flex-direction: column; gap: 9px; }
.type-row {
  display: grid; grid-template-columns: 96px 1fr 50px;
  align-items: center; gap: 10px; font-size: 12.5px;
}
.type-row .t {
  color: var(--ink); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.type-bar {
  height: 10px; border-radius: 5px; min-width: 0; overflow: hidden;
  background: var(--accent-soft);
}
.type-bar-fill {
  display: block; height: 100%; border-radius: 5px; min-width: 3px;
  background: var(--accent);
}
.type-row .n {
  text-align: right; font-variant-numeric: tabular-nums;
  font-weight: 700; color: var(--ink);
}

/* ---- Onglets canaux ------------------------------------------------------- */
.tabs {
  display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 12px;
  padding: 4px; background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 11px; width: fit-content; max-width: 100%;
}
.tab {
  padding: 6px 14px; border-radius: 8px; cursor: pointer; user-select: none;
  font-size: 13px; color: var(--ink-2);
  display: inline-flex; align-items: center; gap: 7px;
}
.tab:hover { background: var(--surface-2); color: var(--ink); }
.tab.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.tab .lock { font-size: 11px; }
.tab .badge { font-size: 10.5px; color: var(--muted); }

/* ---- Etat du reseau : liste verticale type status-page -------------------- */
.health-list {
  border: 1px solid var(--hairline); border-radius: 14px;
  background: var(--surface); box-shadow: var(--shadow);
  margin-bottom: 14px; overflow: hidden;
}
.hl-head {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 10px 16px; border-bottom: 1px solid var(--hairline); font-size: 13.5px;
}
/* ETAT DU RESEAU REPLIABLE — partout, plus seulement sur mobile (retour du
   28.08 : les cartes de statut mangeaient 350 px avant le premier contenu).
   La ligne de tete dit deja s'il y a un incident et lequel ; le detail
   s'ouvre d'un chevron, et l'etat survit en localStorage. */
.hl-head { cursor: pointer; user-select: none; }
.hl-head::after {
  content: "▾"; margin-left: auto; flex: none;
  color: var(--muted); font-size: 12px;
}
.hl-head .hl-time { margin-left: 0; }
body.sante-repliee .hl-head::after { content: "▸"; }
body.sante-repliee .hl-rows { display: none; }
body.sante-repliee .hl-head { border-bottom: 0; }
.health-list.ok   .hl-head { background: color-mix(in srgb, var(--good-text) 6%, transparent); }
.health-list.warn .hl-head { background: color-mix(in srgb, var(--warning) 8%, transparent); }
.health-list.crit .hl-head { background: color-mix(in srgb, var(--critical) 7%, transparent); }
.hl-title b { font-weight: 700; }
.hl-time { font-size: 11px; color: var(--muted); white-space: nowrap; }
/* Cinq domaines empiles sur cinq lignes pleine largeur, c'etait 200 px de
   hauteur pour cinq mots utiles — et autant de defilement en moins pour le
   reste. Ils passent en grille : deux ou trois par rangee selon la largeur,
   sans qu'aucun detail ne disparaisse. */
.hl-rows {
  display: grid; gap: 1px; background: var(--hairline);
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
}
.hl-row {
  display: block; width: 100%;
  padding: 8px 14px; border: none; background: var(--surface);
  cursor: pointer; text-align: left; font: inherit; font-size: 13px; color: var(--ink);
}
.hl-row .hl-ligne1 { display: flex; align-items: baseline; gap: 8px; }
.hl-row:hover { background: var(--surface-2); }
.hl-glyph { font-weight: 800; width: 16px; text-align: center; flex: none; }
.hl-row.ok   .hl-glyph { color: var(--good-text); }
.hl-row.warn .hl-glyph { color: var(--warning); }
.hl-row.crit .hl-glyph { color: var(--critical); }
.hl-row.na   .hl-glyph { color: var(--muted); }
.hl-name { font-weight: 650; flex: none; }
/* Le detail passe SOUS le nom : cote a cote, il obligeait la rangee a la
   largeur du plus bavard et ramenait la grille a une seule colonne. */
.hl-detail { color: var(--muted); display: block; margin-left: 24px; line-height: 1.35; }
.hl-row.crit .hl-detail, .hl-row.warn .hl-detail { color: var(--ink); }
@media (max-width: 640px) {
  .hl-rows { grid-template-columns: 1fr; }
}
.stat-card.click { cursor: pointer; transition: border-color .12s; }
.stat-card.click:hover { border-color: var(--accent); }
/* liens radio : paires bidirectionnelles, les plus graves d'abord */
/* Le conteneur porte desormais la synthese et les groupes : c'est a
   l'interieur de chaque groupe que la grille multi-colonnes doit vivre. */
.radiolink-pairs { display: block; }
.rl-grille { display: grid; grid-template-columns: repeat(auto-fill, minmax(255px, 1fr)); gap: 10px; }
.rl-pair {
  border: 1px solid var(--hairline); border-radius: 11px;
  padding: 9px 12px; background: var(--surface-2);
}
.rl-pair.lost  { border-color: color-mix(in srgb, var(--critical) 55%, transparent); }
.rl-pair.quiet { border-color: color-mix(in srgb, var(--warning) 55%, transparent); }
.rl-pair .n { font-weight: 650; font-size: 12.5px; margin-bottom: 5px; }
.rl-dir { font-size: 12px; color: var(--muted); display: flex; gap: 6px;
  align-items: baseline; margin-top: 2px; }
.rl-dir b { color: var(--ink); font-weight: 600; }
/* conteneur d'origine (classe posee par JS) */
.radiolink-chips { display: flex; flex-wrap: wrap; gap: 8px; }

/* ---- Supervision ---------------------------------------------------------- */
.panels-3 { grid-template-columns: 1fr 1fr 1fr; }
@media (max-width: 1100px) { .panels-3 { grid-template-columns: 1fr; } }

/* Disponibilite : cartes avec barre de disponibilite */
/* DEUX colonnes fixes, et non un auto-fill : le panneau occupe la moitie
   droite de la rangee, ou 210 px de largeur minimale ne laissaient tenir
   qu'une seule colonne. Six nodes s'empilaient alors sur six lignes face aux
   cartes larges des Gateways, et la rangee etait bancale. Deux colonnes de
   trois equilibrent la hauteur des deux panneaux. */
.uptime-list {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;
  align-content: start;
}
/* En dessous, deux colonnes deviendraient illisibles : on repasse a une. */
@media (max-width: 900px) {
  .uptime-list { grid-template-columns: 1fr; }
}
.up-card {
  display: flex; flex-direction: column; gap: 6px;
  border: 1px solid var(--hairline);
  border-radius: 12px; background: var(--surface-2); padding: 10px 12px;
}
.up-card.offline { opacity: .6; }
.up-card-top { display: flex; align-items: center; gap: 7px; }
.up-card-name {
  font-weight: 700; font-size: 13px; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.up-pct { font-variant-numeric: tabular-nums; font-weight: 700; font-size: 13px; }
.up-pct.bad { color: var(--critical); }
.up-pct.mid { color: var(--warning); }
.up-pct.good { color: var(--good-text); }
.up-bar { height: 6px; border-radius: 4px; overflow: hidden;
  background: color-mix(in srgb, var(--muted) 20%, transparent); }
.up-bar-fill { display: block; height: 100%; border-radius: 4px; }
.up-bar-fill.good { background: var(--good); }
.up-bar-fill.mid  { background: var(--warning); }
.up-bar-fill.bad  { background: var(--critical); }
.up-card-meta { display: flex; flex-wrap: wrap; gap: 4px 6px; }

/* Incidents : TIMELINE d'episodes de coupure (offline -> retour, avec duree) */
.inc-ep { display: grid; grid-template-columns: 14px 1fr; gap: 9px; }
.inc-ep-rail { position: relative; display: flex; justify-content: center; }
.inc-ep-rail::before {
  content: ""; position: absolute; top: -4px; bottom: -4px; width: 2px;
  background: var(--hairline);
}
.inc-ep:first-child .inc-ep-rail::before { top: 10px; }
.inc-ep:last-child .inc-ep-rail::before { bottom: auto; height: 14px; }
.inc-ep-dot {
  position: relative; z-index: 1; width: 10px; height: 10px; border-radius: 50%;
  margin-top: 8px; background: var(--critical);
  box-shadow: 0 0 0 3px var(--surface);
}
.inc-ep.live .inc-ep-dot { animation: inc-pulse 1.6s ease-in-out infinite; }
@keyframes inc-pulse {
  0%, 100% { box-shadow: 0 0 0 3px var(--surface), 0 0 0 4px color-mix(in srgb, var(--critical) 55%, transparent); }
  50%      { box-shadow: 0 0 0 3px var(--surface), 0 0 0 7px color-mix(in srgb, var(--critical) 12%, transparent); }
}
.inc-ep-body {
  border: 1px solid var(--hairline);
  border-radius: 10px; background: var(--surface-2); padding: 7px 11px;
  display: flex; flex-direction: column; gap: 2px;
}
.inc-ep-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.inc-ep-node { font-weight: 700; font-size: 12.5px; }
.inc-ep-dur {
  font-size: 12px; font-weight: 700; color: var(--critical);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.inc-ep-times { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }

/* Incidents : cartes de SYNTHESE par node (vue par defaut, cliquables) */
.inc-node-card {
  display: flex; flex-direction: column; gap: 3px; width: 100%; text-align: left;
  border: 1px solid var(--hairline);
  border-radius: 11px; background: var(--surface-2); padding: 9px 12px; cursor: pointer;
  font: inherit; color: inherit; transition: background .15s, border-color .15s, box-shadow .15s;
}
.inc-node-card:hover {
  background: color-mix(in srgb, var(--surface-2) 88%, var(--ink));
  border-color: var(--accent); box-shadow: 0 1px 6px rgba(0, 0, 0, .12);
}
.inc-nc-top { display: flex; align-items: center; gap: 7px; }
.inc-nc-name {
  font-weight: 700; font-size: 13px; flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.inc-nc-count { font-size: 11px; font-weight: 700; color: var(--critical); white-space: nowrap; }
.inc-nc-meta { font-size: 11px; color: var(--muted); }

.uptime-row-legacy {
  display: grid; grid-template-columns: 10px 1fr auto auto;
  align-items: center; gap: 9px; font-size: 12.5px;
}
.uptime-row .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.uptime-row .name .ch { font-weight: 400; color: var(--muted); }
.uptime-row .pct { font-variant-numeric: tabular-nums; font-weight: 650; }
.uptime-row .pct.bad { color: var(--critical); }
.uptime-row .pct.mid { color: var(--warning); }
.uptime-row .pct.good { color: var(--good-text); }
.uptime-row .meta { color: var(--muted); font-size: 11px; }

.alerts-list { display: flex; flex-direction: column; gap: 6px;
  flex: 1 1 auto; min-height: 120px; max-height: 560px; overflow-y: auto; padding-right: 4px; }
.alert-row { display: flex; gap: 8px; font-size: 12px; align-items: baseline; }
.alert-row .when { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.alert-row .k { font-weight: 700; }
.alert-row .k.offline { color: var(--critical); }
.alert-row .k.online { color: var(--good-text); }
.alert-row .k.stale { color: var(--warning); }
.alert-row .m { color: var(--ink-2); }

/* courbe de debit en tete du panneau Broker (2 series, meme unite, un seul axe) */
.broker-spark { margin-bottom: 12px; }
.bk-spark-head { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px;
  margin-bottom: 6px; font-size: 11.5px; color: var(--ink-2); }
.bk-spark-title { font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); margin-right: auto; }
.bk-spark-head .lg { display: inline-flex; align-items: center; gap: 5px; }
.bk-spark-head .sw { width: 9px; height: 9px; border-radius: 2px; flex: none; }
.bk-svg { height: 96px; }
.bk-ylab { min-width: 26px; font-size: 9.5px; }
.bk-spark-foot { font-size: 10px; color: var(--muted); text-align: right; margin-top: 2px;
  font-family: var(--mono); }
.bk-wait { font-size: 11.5px; padding: 22px 0; text-align: center; }

.broker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }
.broker-kv {
  border: 1px solid var(--hairline); border-radius: 10px;
  background: var(--surface-2); padding: 8px 10px;
}
.broker-kv .k { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.broker-kv .v { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums;
  margin-top: 3px; line-height: 1.2; }
.broker-kv .bk-sub { font-size: 10px; color: var(--muted); margin-top: 1px;
  font-variant-numeric: tabular-nums; }
/* messages jetes par le broker : 0 = sain, tout le reste doit sauter aux yeux */
.broker-kv.bk-bad { border-color: var(--critical); }
.broker-kv.bk-bad .v { color: var(--critical); }
.broker-kv.bk-ok .v { color: var(--good-text); }

.traffic-panel { margin-bottom: 16px; overflow: hidden; }
#traffic-chart { width: 100%; max-width: 100%; }
.traffic-bars {
  display: flex; align-items: flex-end; gap: 2px;
  height: 84px; width: 100%; max-width: 100%; overflow: hidden;
  border-bottom: 1px solid var(--grid);
}
.traffic-bars .tbar {
  flex: 1 1 0; min-width: 0; background: var(--accent);
  border-radius: 2px 2px 0 0;
}
/* heures sans trafic : fin trait discret au ras de la ligne de base */
.traffic-bars .tbar.empty { background: var(--grid); height: 2px; border-radius: 0; }
.traffic-axis { display: flex; gap: 2px; margin-top: 4px; width: 100%; max-width: 100%; }
.traffic-axis span {
  flex: 1 1 0; min-width: 0; text-align: center;
  font-size: 10px; color: var(--muted); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden;
}

/* ---- Menu utilisateur (dropdown sous le bouton) --------------------------- */
.user-menu { position: relative; }
.user-menu .caret { font-size: 10px; opacity: 0.7; }
.user-dropdown {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 50;
  min-width: 210px; background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 10px; box-shadow: var(--shadow); padding: 6px; overflow: hidden;
}
.user-dd-head {
  font-size: 11.5px; color: var(--muted); padding: 6px 10px 8px;
  border-bottom: 1px solid var(--hairline); margin-bottom: 4px;
}
.user-dd-item {
  display: block; width: 100%; text-align: left; background: none; border: none;
  color: var(--ink); font: inherit; font-size: 13px; padding: 8px 10px;
  border-radius: 7px; cursor: pointer;
}
.user-dd-item:hover { background: var(--surface-2); }
.user-dd-item.danger { color: var(--critical); }

/* ---- Login integre du site ------------------------------------------------ */
/* fond (header + contenu) floute tant qu'on n'est pas authentifie */
body.app-locked .topbar,
body.app-locked main,
body.app-locked footer {
  filter: blur(7px); pointer-events: none; user-select: none;
}
.app-login {
  /* 10000 et non 1000 : la barre du haut est passee a z-index 2000 (pour que
     le menu utilisateur ne se deplie plus derriere la carte Leaflet). L'ecran
     de connexion se retrouvait donc DESSOUS, et la barre — certes floutee et
     inerte — peignait par-dessus lui. Les modales (11000+) restent au-dessus. */
  position: fixed; inset: 0; z-index: 10000;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--page) 55%, transparent);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
}
/* l'attribut hidden doit l'emporter sur le display:flex ci-dessus */
.app-login[hidden] { display: none; }
.app-login-card {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 16px; box-shadow: var(--shadow);
  padding: 28px 30px; width: min(380px, 92%);
  display: flex; flex-direction: column; gap: 8px;
}
.app-login-brand { font-size: 16px; font-weight: 650; display: flex; align-items: center; gap: 8px; }
.app-login-brand .brand-mark { color: var(--accent); }
.app-login-sub { font-size: 12.5px; color: var(--muted); margin-bottom: 14px; }
.app-login-card label {
  font-size: 11px; font-weight: 600; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--muted); margin-top: 6px;
}
.app-login-card input { font-size: 14px; padding: 10px 12px; }
.app-login-card .btn { justify-content: center; margin-top: 16px; padding: 10px; }
.app-login-err { font-size: 12.5px; color: var(--critical); min-height: 16px; margin-top: 6px; }

/* ---- Vue CLI -------------------------------------------------------------- */
.cli-gw-field { min-width: 170px; }
.cli-conn { font-size: 12.5px; color: var(--good-text); font-weight: 600; }
.cli-conn::before { content: "● "; }

/* barre mot de passe inline (apparait au moment d'envoyer) */
.cli-pwbar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  background: var(--surface-2); border: 1px solid var(--hairline);
  border-radius: 10px; padding: 10px 12px; margin-bottom: 10px;
}
.cli-pwbar[hidden] { display: none; }
.cli-pwbar-label { font-size: 13px; color: var(--ink-2); }
.cli-pwbar input { flex: 1 1 180px; font-size: 14px; padding: 8px 11px; }
.cli-pwbar-err { font-size: 12.5px; color: var(--critical); }

.cli-bar { display: flex; gap: 10px; align-items: end; flex-wrap: wrap; margin-bottom: 10px; }
.cli-bar .field.grow { flex: 1 1 240px; }
/* ---- Piloter, refonte : etapes numerotees + cartes + palette d'actions --- */
.cli-step-h {
  display: flex; align-items: center; gap: 10px;
  font-size: 11px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--muted);
  border-top: 1px solid var(--hairline); padding-top: 12px; margin: 14px 0 10px;
}
.cli-step-h:first-of-type { border-top: none; padding-top: 0; margin-top: 4px; }
.cli-step-s { font-weight: 400; text-transform: none; letter-spacing: 0; }
.cli-canton-l { display: flex; align-items: center; gap: 6px;
  font-weight: 400; text-transform: none; letter-spacing: 0; }
.cli-canton-l select { font-size: 12.5px; padding: 4px 8px; }
.cli-gw-cards {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 10px; margin-bottom: 4px;
}
.cli-gw-card {
  display: flex; flex-direction: column; gap: 2px; text-align: left;
  padding: 10px 12px; border: 1px solid var(--hairline); border-radius: 12px;
  background: var(--surface-2); cursor: pointer;
  transition: border-color .12s, background .12s;
}
.cli-gw-card:hover { border-color: var(--accent); }
/* Sans regle, le navigateur pose son propre anneau apres un clic : un
   rectangle vif qui ignore les coins arrondis et jure avec la carte. On le
   remplace par un anneau assorti, et on le garde pour la navigation au
   CLAVIER — le supprimer tout court rendrait l'entonnoir inutilisable sans
   souris. */
.cli-gw-card:focus { outline: none; }
.cli-gw-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cli-gw-card.active { border-color: var(--accent); background: var(--accent-soft); }
.cli-gw-card.off { opacity: 0.6; }
.cli-gw-card:disabled { opacity: 0.4; cursor: default; }
.cli-gw-card .t { font-size: 13px; font-weight: 650; color: var(--ink); }
.cli-gw-card .d { font-size: 11px; color: var(--muted); font-family: var(--mono); }
.cli-gw-card .k { font-size: 11px; color: var(--muted); margin-top: 2px; }
.cli-gw-card .k.ok { color: var(--good-text); font-weight: 600; }
.cli-cats { margin: 0 0 10px; }
.cli-actions {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 8px; margin-bottom: 4px;
}
.cli-action {
  display: flex; flex-direction: column; gap: 3px; text-align: left;
  padding: 9px 12px; border: 1px solid var(--hairline); border-radius: 10px;
  background: var(--surface-2); cursor: pointer;
  transition: border-color .12s, background .12s;
}
.cli-action:hover { border-color: var(--accent); background: var(--accent-soft); }
.cli-action .t { font-size: 12.5px; font-weight: 600; color: var(--ink); }
.cli-action code { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.cli-quick { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.cli-chip {
  font-size: 12px; border: 1px solid var(--hairline); background: var(--surface-2);
  color: var(--ink-2); border-radius: 999px; padding: 3px 11px; cursor: pointer;
  font-family: var(--mono);
}
.cli-chip:hover { color: var(--ink); border-color: var(--accent); }
.cli-chip.active { color: var(--ink); border-color: var(--accent); background: var(--accent-soft); }
.frame-presets { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.cli-console {
  background: #0d0d0d; color: #d6d6cf; border: 1px solid var(--hairline);
  border-radius: 8px; padding: 12px 14px; margin: 0;
  font-family: var(--mono); font-size: 12.5px; line-height: 1.5;
  min-height: 200px; max-height: 48vh; overflow-y: auto; white-space: pre-wrap;
}
.cli-console .cmd { color: #6da7ec; }
.cli-console .ok { color: #3fb950; }
.cli-console .err { color: #f85149; }
.cli-console .sys { color: #898781; }

/* ---- Vue Admin ------------------------------------------------------------ */
.admin-users { display: grid; gap: 8px; }
.admin-user-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  border: 1px solid var(--hairline); border-radius: 10px;
  padding: 9px 12px; background: var(--surface-2); font-size: 13px;
}
.admin-user-row .u { font-weight: 650; min-width: 120px; }
.admin-user-row .perms { color: var(--ink-2); font-size: 12px; flex: 1; }
.admin-user-row .perms .pchip {
  display: inline-block; background: var(--accent-soft); color: var(--accent);
  border-radius: 6px; padding: 1px 7px; margin: 1px 3px 1px 0; font-size: 11px; font-weight: 600;
}
.admin-perms { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
.admin-perms label {
  display: inline-flex; align-items: center; gap: 6px; font-size: 13px;
  border: 1px solid var(--hairline); border-radius: 999px; padding: 5px 12px; cursor: pointer;
}
.admin-perms input { width: auto; }
.admin-actions { display: flex; align-items: center; gap: 10px; }
.admin-err { font-size: 12.5px; color: var(--critical); }
.admin-user-row .btn.del { color: var(--critical); border-color: var(--hairline); }

/* ---- Vue Maintenance ------------------------------------------------------ */
.maint-gw-field { max-width: 320px; margin-bottom: 14px; }
.maint-block {
  border: 1px solid var(--hairline); border-radius: 12px;
  padding: 14px 16px; margin-bottom: 14px; background: var(--surface);
}
.maint-block h3 { margin: 0 0 4px; font-size: 13.5px; font-weight: 650; }
.maint-hint { font-size: 12px; color: var(--muted); margin: 0 0 12px; }
.maint-grid {
  display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  align-items: end; margin-bottom: 12px;
}
.maint-grid .field.btns { align-self: end; }
@media (max-width: 640px) { .maint-grid { grid-template-columns: 1fr; } }

/* ---- Vue carte ------------------------------------------------------------ */
.view-tabs { margin: 0 0 16px; }
.map-panel { padding-bottom: 16px; }
#map {
  border-radius: 8px; border: 1px solid var(--hairline);
  background: var(--surface-2);
}
/* Fonds CLAIRS assombris en mode sombre. La classe est posee par le JS selon
   le fond choisi : inverser un fond deja sombre (Dark Matter) ou une photo
   satellite le rendrait meconnaissable. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) #map.fond-inversible .leaflet-tile {
    filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(0.88);
  }
}
:root[data-theme="dark"] #map.fond-inversible .leaflet-tile {
  filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(0.88);
}
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
  background: var(--surface); color: var(--ink);
}
.node-popup { font: 12.5px/1.5 system-ui, sans-serif; }
.node-popup .n { font-weight: 650; font-size: 13.5px; margin-bottom: 2px; }
.node-popup .d { color: var(--ink-2); }
.node-popup code { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.node-popup .ntag {
  font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 1px 6px; border-radius: 99px; vertical-align: 1px;
  background: var(--surface-2); color: var(--ink-2);
}
.node-popup .ntag.chat { background: rgba(224, 72, 63, 0.15); color: #e0483f; }

/* ---- Marqueurs nodes (divIcon) : type + etat ------------------------------ */
.nmark {
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%; box-sizing: border-box;
  color: #fff; border: 2px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
  position: relative;   /* ancre de la pastille de charge canal */
}
/* pastille de charge canal (ChUtil du dernier /tele) sur les gateways actifs */
.nmark .nload {
  position: absolute; top: -4px; right: -4px; width: 11px; height: 11px;
  border-radius: 50%; border: 2px solid #fff; box-sizing: border-box;
}
.nload.ok   { background: #2fa25c; }  /* < 10 % : reseau sain */
.nload.warn { background: #e0a10e; }  /* 10-25 % : charge */
.nload.hot  { background: #d03b3b; }  /* > 25 % : sature */

/* Plancher de bruit du recepteur (onglet Telemetrie). Memes couleurs que la
   charge canal, pour qu'un coup d'oeil suffise : vert = le node entend loin,
   ambre = degrade, rouge = sourd aux liaisons faibles. Le texte reste lisible
   sans la couleur (valeur en dBm), la couleur n'est qu'un renfort. */
.nf-ok   { color: #2fa25c; font-weight: 600; }  /* <= -105 dBm : bon site */
.nf-warn { color: #e0a10e; font-weight: 600; }  /* -105 a -95 dBm : exploitable */
.nf-hot  { color: #d03b3b; font-weight: 600; }  /* > -95 dBm : sourd aux liaisons faibles */

/* Etat du recepteur GPS (tableau Telemetrie et fiches carte). Un node SANS GPS
   reste affiche en gris neutre : c'est un etat normal, pas une anomalie. Seul
   "pas de fix" est colore en alerte — un recepteur actif qui ne capte plus. */
.gps-fix   { color: var(--good-text); font-weight: 600; }
.gps-nofix { color: var(--warning); font-weight: 600; }
/* gps=0 mais la carte EST equipee : ni vert (pas de fix) ni gris neutre —
   c'est un reglage a faire, pas une fatalite. */
.gps-off-hw { color: var(--ink-2); font-weight: 600; }

/* Bloc GPS de la fiche node : libelle a gauche, valeur alignee a droite, en
   chasse fixe pour que coordonnees et altitude s'alignent d'une ligne a
   l'autre. Meme lecture que l'ecran du relais. */
.gps-block { margin-top: 3px; }
.gps-block .gr { display: flex; justify-content: space-between; gap: 12px; }
.gps-block .gk { color: var(--muted); }
.gps-block .gv { font-family: var(--mono); font-size: 11.5px; color: var(--ink-2); }
.gps-block .gv.muted { color: var(--muted); }

/* Coordonnees et altitude du tableau Telemetrie : chasse fixe pour que les
   colonnes s'alignent d'une ligne a l'autre. */
.tele-pos { font-family: var(--mono); font-size: 11.5px; white-space: nowrap; }
.tele-pos-src { font-family: system-ui, sans-serif; font-size: 10px; opacity: 0.75;
  display: block;         /* sous les coordonnees, pas a cote */
  white-space: normal; }  /* et libre de se replier : .tele-pos est en nowrap
                             pour ne jamais couper une coordonnee, la mention de
                             provenance n'a pas besoin de cette contrainte */

/* Carte du tableau GPS — meme habillage que les vignettes de courbes, qu'elle
   remplace a cet endroit. */
.nmark svg { width: 17px; height: 17px; display: block; }
.nmark.gw   { background: #1d2733; }  /* relais (antenne, style MeshCore) */
.nmark.comp { width: 18px; height: 18px; background: #e0483f; }  /* compagnon : point rouge */
/* hors ligne (pas d'advert < 24 h) : gris CLAIR + liseré rouge — contraste
   franc avec le sombre des actifs, lisible sur fond de carte claire ET sombre */
.nmark.off  { background: #b7bec6; border-color: #d03b3b; color: #5c646d; }
.nmark.comp.off { background: #b7bec6; }
.nmark.mini {
  width: 16px; height: 16px; border-width: 1px; box-shadow: none;
  display: inline-flex; vertical-align: -3px;
}
.nmark.mini svg { width: 11px; height: 11px; }
.nmark.comp.mini { width: 12px; height: 12px; }

/* etiquettes permanentes (toggle "Etiquettes") */
.leaflet-tooltip.nlabel {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--hairline); border-radius: 6px;
  padding: 2px 7px; font-size: 11px; line-height: 1.3;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.leaflet-tooltip.nlabel::before { display: none; }  /* pas de fleche */
.nlabel .db { color: var(--muted); font-family: var(--mono); font-size: 10px; }

/* barre positions compagnon + conteneur carte/legende */
.map-trackbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 8px 0 10px;
}
.map-trackbar select { padding: 5px 8px; font-size: 13px; }
.trackbar-label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
/* bouton "recadrer" (controle Leaflet sous le zoom) */
.leaflet-recenter { font-size: 19px; line-height: 26px; }
/* description du firmware selectionne (onglet Maintenance) */
.fw-desc { font-size: 12px; color: var(--ink-2); margin-top: 4px; line-height: 1.4; }

/* ---- Modale config gateway ------------------------------------------------ */
.gw-card.clickable { cursor: pointer; }
.gw-card.clickable:hover { border-color: var(--accent); }
/* fond floute derriere la modale */
#gw-modal {
  background: rgba(12, 12, 12, 0.42);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
}
.gwm-card {
  max-width: 980px; width: 94vw; max-height: 90vh; overflow-y: auto;
  padding: 30px 38px;
}
.gwm-head { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
.gwm-pwrow { display: flex; gap: 8px; margin-top: 8px; }
.gwm-pwrow input { flex: 1; }
/* deux colonnes : etat (lecture) | formulaire d'edition — pas de scroll */
.gwm-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 40px; }
@media (max-width: 860px) { .gwm-cols { grid-template-columns: 1fr; } }
.gwm-h3 {
  margin: 0 0 12px; font-size: 12px; font-weight: 700;
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted);
}
.gwm-grid {
  display: grid; grid-template-columns: minmax(120px, auto) 1fr;
  gap: 8px 18px; font-size: 13.5px; line-height: 1.45; margin-bottom: 14px;
}
.gwm-form {
  display: grid; grid-template-columns: minmax(140px, auto) 1fr;
  gap: 8px 14px; align-items: center; font-size: 13px; margin-bottom: 14px;
}
.gwm-form label { color: var(--muted); font-weight: 600; }
.gwm-form input { padding: 6px 10px; font-size: 13px; }
.gwm-details { margin: 10px 0 0; font-size: 13px; }
.gwm-details summary { cursor: pointer; font-weight: 600; color: var(--ink-2); }
.gwm-details > *:not(summary) { margin-top: 8px; }
.gwm-k { color: var(--muted); font-weight: 600; }
.gwm-v { color: var(--ink); word-break: break-all; }
.gwm-tree { margin: 0; font: 11.5px/1.45 var(--mono); white-space: pre; }
.gwm-actions { display: flex; gap: 8px; align-items: center; }
.gwm-actions input { flex: 1; }
.gwm-log {
  margin: 10px 0 0; padding: 8px 10px; max-height: 140px; overflow-y: auto;
  background: var(--surface-2); border: 1px solid var(--hairline); border-radius: 8px;
  font: 11.5px/1.5 var(--mono); white-space: pre-wrap;
}

/* ---- Flasher USB ---------------------------------------------------------- */
.usb-intro, .usb-note { font-size: 13px; color: var(--ink-2); line-height: 1.55; max-width: 78ch; }
.usb-note { margin-top: 12px; }
.usb-err { color: var(--critical); font-size: 13px; }
#view-usb esp-web-install-button { display: inline-block; margin-top: 10px; }
/* guide cycle de vie COMPACT : bandeau horizontal 1 USB -> 2 flash -> 3 OTA */
.usb-steps { display: flex; gap: 10px; margin: 2px 0 14px; flex-wrap: wrap; }
.usb-step {
  flex: 1 1 220px; display: flex; gap: 9px; align-items: center;
  background: var(--surface-2); border: 1px solid var(--hairline);
  border-radius: 9px; padding: 7px 11px; font-size: 12px; line-height: 1.35;
  color: var(--ink-2);
}
.usb-step strong { color: var(--ink); }
.usb-step .n {
  flex: 0 0 auto; width: 21px; height: 21px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent); font-weight: 700; font-size: 12px;
}
/* aide repliable */
.usb-help { margin-top: 10px; font-size: 12.5px; color: var(--ink-2); max-width: 78ch; }
.usb-help summary { cursor: pointer; font-weight: 600; color: var(--ink); }
.usb-help ul { margin: 8px 0 0 18px; display: flex; flex-direction: column; gap: 5px; }
.usb-h3 { margin: 18px 0 8px; font-size: 13.5px; font-weight: 650; }
.usb-validate { display: flex; align-items: center; gap: 12px; margin-top: 12px; }
#usb-flash-zone { margin-top: 4px; }
#map-track-node { min-width: 180px; max-width: 320px; }
.map-wrap { position: relative; }
.map-legend {
  position: absolute; right: 10px; bottom: 10px; z-index: 500;
  display: flex; gap: 12px; align-items: center;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  border: 1px solid var(--hairline); border-radius: 8px;
  padding: 5px 10px; font-size: 11.5px; color: var(--ink-2);
  box-shadow: var(--shadow); pointer-events: none;
}
.map-legend > span { display: inline-flex; align-items: center; gap: 5px; }
.leg-link { width: 20px; height: 0; border-top: 2px dashed #6b7280; display: inline-block; }
@media (max-width: 640px) { .map-legend { display: none; } }

/* badge SNR au milieu d'un lien radio entre relais */
.snr-badge {
  position: absolute; transform: translate(-50%, -50%);
  background: var(--accent); color: #fff;
  font-size: 12px; font-weight: 650; white-space: nowrap;
  padding: 3px 9px; border-radius: 8px; letter-spacing: .01em;
  border: 1.5px solid rgba(255, 255, 255, .35);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
}
/* Vue large : les etiquettes se chevauchent en grappe et deviennent du bruit
   (retour 28.08) — elles n'apparaissent qu'a partir du zoom 11 (classe posee
   par la carte) ; en dessous, le SNR est au survol du lien. */
.leaflet-container:not(.snr-zoom-ok) .snr-badge { display: none; }

/* ---- Filtres --------------------------------------------------------------- */
.filters {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: end;
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 12px; padding: 12px 14px; margin-bottom: 12px; box-shadow: var(--shadow);
}
.field { display: flex; flex-direction: column; gap: 4px; }
.field.grow { flex: 1 1 240px; }
.field.btns { flex-direction: row; gap: 8px; }
.field label {
  font-size: 11px; font-weight: 600; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--muted);
}
input, select {
  background: var(--page); color: var(--ink);
  border: 1px solid var(--hairline); border-radius: 8px;
  padding: 7px 10px; font: inherit; font-size: 13.5px; min-width: 0;
}
input:focus, select:focus {
  outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);
  outline-offset: 0; border-color: var(--accent);
}
.field:not(.grow) input[type="number"] { width: 90px; }

.btn {
  border: 1px solid var(--hairline); background: var(--surface); color: var(--ink-2);
  border-radius: 8px; padding: 7px 14px; font: inherit; font-size: 13px;
  cursor: pointer; white-space: nowrap;
}
.btn:hover { color: var(--ink); border-color: var(--muted); }
.btn:disabled { opacity: 0.4; cursor: default; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.btn.primary:hover { filter: brightness(1.08); }

/* ---- Toolbar + interrupteur live ------------------------------------------ */
.toolbar { display: flex; align-items: center; gap: 14px; margin-bottom: 10px; flex-wrap: wrap; }
.toolbar .spacer { flex: 1; }
.switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13px; }
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch .track {
  width: 34px; height: 19px; border-radius: 999px; background: var(--surface-2);
  border: 1px solid var(--hairline); position: relative; transition: background 0.15s;
  display: inline-block;
}
.switch .thumb {
  position: absolute; top: 2px; left: 2px; width: 13px; height: 13px;
  border-radius: 50%; background: var(--muted); transition: transform 0.15s, background 0.15s;
}
.switch input:checked + .track { background: var(--accent-soft); border-color: var(--accent); }
.switch input:checked + .track .thumb { transform: translateX(15px); background: var(--accent); }

/* ---- Tableau ---------------------------------------------------------------- */
.table-wrap {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 12px; box-shadow: var(--shadow);
  overflow: auto; max-height: 64vh;
}
table { border-collapse: collapse; width: 100%; font-size: 13px; }
thead th {
  position: sticky; top: 0; z-index: 5;
  background: var(--surface); text-align: left;
  font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--muted); padding: 10px 12px;
  border-bottom: 1px solid var(--grid);
}
thead th.num { text-align: right; }
tbody td {
  padding: 7px 12px; border-bottom: 1px solid var(--grid);
  white-space: nowrap; font-variant-numeric: tabular-nums;
}
tbody td.num { text-align: right; }
tbody tr:hover { background: var(--surface-2); }
tbody tr:last-child td { border-bottom: none; }

td.msg {
  max-width: 340px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap;
}
/* vrai message dechiffre = mis en avant ; le reste = discret */
td.msg.msg-txt { color: var(--ink); font-weight: 600; }
td.msg.msg-sys { color: var(--ink-2); }
td.msg.msg-enc, td.msg.msg-lock { color: var(--muted); font-style: italic; }
td.msg.msg-proto { color: var(--muted); font-size: 12px; }
td.hex {
  font-family: var(--mono); font-size: 11.5px; color: var(--muted);
  max-width: 190px; overflow: hidden; text-overflow: ellipsis;
}

/* tags de type : puce coloree fixe + libelle texte (jamais couleur seule) */
.tag {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11.5px; font-weight: 600; color: var(--ink-2);
  background: var(--surface-2); border: 1px solid var(--hairline);
  border-radius: 6px; padding: 2px 8px;
}
.tag::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--muted); flex: none;
}
.tag[data-t="ADVERT"]::before   { background: var(--accent); }
.tag[data-t="GRP_TXT"]::before  { background: #008300; }
.tag[data-t="ACK"]::before      { background: #1baf7a; }
.tag[data-t="TXT_MSG"]::before  { background: #eb6834; }
.tag[data-t="PATH"]::before     { background: #4a3aa7; }
.tag[data-t="TRACE"]::before    { background: #e87ba4; }

.route-F { color: var(--accent); font-weight: 600; }
.route-D { color: var(--ink-2); }

.rssi-good { color: var(--good-text); font-weight: 600; }
.rssi-mid  { color: var(--warning); }
.rssi-bad  { color: var(--critical); }

tr.new-row td { animation: rowIn 1.4s ease-out; }
@keyframes rowIn {
  0% { background: var(--accent-soft); }
  100% { background: transparent; }
}

.empty { padding: 34px; text-align: center; color: var(--muted); }

/* ---- Pager / footer --------------------------------------------------------- */
.pager {
  display: flex; align-items: center; justify-content: center;
  gap: 14px; margin-top: 14px;
}
footer {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 7px 18px; color: var(--muted); font-size: 11.5px;
  border-top: 1px solid var(--hairline); margin-top: 0;
  flex: none;                        /* jamais comprime, jamais etire */
}
footer img { height: 18px; vertical-align: middle; border-radius: 3px; }

/* ---- Responsive -------------------------------------------------------------- */
/* Tablette : les grilles multi-colonnes repassent en 1 colonne */
@media (max-width: 820px) {
  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; }
  .panels { grid-template-columns: 1fr; }
  .filters { gap: 8px; }
  .filters .field { flex: 1 1 140px; }
}

/* Mobile */
@media (max-width: 640px) {
  main { padding: 12px 10px 28px; }
  .topbar { padding: 8px 12px; gap: 8px; flex-wrap: wrap; }
  .brand-thin { display: none; }
  .brand { font-size: 14px; }
  .topbar-right { gap: 6px; }
  .topbar .pill { padding: 3px 8px; font-size: 11px; }
  #live-pill { display: none; }              /* garde l'essentiel (broker) */
  .iapc-logo { height: 22px; }

  /* tuiles stats : 2 par ligne, compactes */
  .stats-grid { grid-template-columns: 1fr 1fr; gap: 8px; }
  .stat-card { padding: 10px 12px 9px; }
  .stat-card .v { font-size: 22px; }

  /* filtres empiles pleine largeur */
  .filters { flex-direction: column; align-items: stretch; }
  .filters .field, .filters .field.grow { flex: none; width: 100%; }
  .filters .field.btns { flex-direction: row; }
  .filters .field.btns .btn { flex: 1; }
  .field:not(.grow) input[type="number"] { width: 100%; }

  /* onglets : defilement horizontal si trop larges */
  .tabs { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .tab { white-space: nowrap; }

  /* barre CLI empilee */
  .cli-bar { flex-direction: column; align-items: stretch; }
  .cli-bar .field, .cli-bar #cli-preset, .cli-gw-field { max-width: none; width: 100%; }
  .cli-console { font-size: 12px; max-height: 42vh; }
  .panel-head { flex-wrap: wrap; }

  /* tableau : masquer colonnes secondaires, garder l'essentiel lisible */
  td.msg { max-width: 120px; }
  td.hex, thead th:last-child { display: none; }
  .gwcol, thead th:nth-child(3) { display: none; }   /* colonne GW */
  table { font-size: 12px; }
  thead th, tbody td { padding: 6px 8px; }

  /* carte + login plein ecran adaptes */
  #map { height: 52vh; min-height: 300px; }
  .app-login-card { width: 92%; padding: 22px 18px; }
  .cli-bar { flex-direction: column; align-items: stretch; }
  .cli-bar .field, .cli-bar .cli-gw-field { width: 100%; min-width: 0; }
  .cli-pwbar { flex-direction: column; align-items: stretch; }
  .toolbar { flex-wrap: wrap; gap: 8px; }
  footer { flex-direction: column; gap: 4px; text-align: center; }
}

/* Tres petits ecrans : stats en 1 colonne */
@media (max-width: 380px) {
  .stats-grid { grid-template-columns: 1fr; }
}

/* groupes repliables du formulaire gateway */
.gwm-group { margin: 4px 0 10px; font-size: 13px; }
.gwm-group summary {
  cursor: pointer; font-weight: 650; color: var(--ink-2);
  padding: 6px 0; border-top: 1px solid var(--hairline);
}
.gwm-group[open] summary { color: var(--ink); }
.gwm-group .gwm-form { margin: 8px 0 4px; }

/* editeur de regions + colonnes sans scroll global */
.gwm-cols { align-items: start; }
.gwm-col { max-height: 68vh; overflow-y: auto; padding-right: 4px; }
.gwr-list { display: flex; flex-direction: column; gap: 2px; margin: 6px 0 10px; }
.gwr-row {
  display: flex; align-items: center; gap: 8px;
  font: 12.5px/1.5 var(--mono);
}
.gwr-name { color: var(--ink); }
.gwr-home { color: var(--accent); }
.gwr-nf { color: var(--warning); font-size: 10px; }
.gwr-del {
  border: none; background: none; color: var(--muted); cursor: pointer;
  font-size: 12px; padding: 0 4px; border-radius: 4px;
}
.gwr-del:hover { color: var(--critical); background: var(--surface-2); }
.gwr-add input { max-width: 150px; }
.gwr-add select { max-width: 120px; }
.gwm-log { max-height: 96px; }

/* poubelle incidents */
.alerts-trash { font-size: 13px; }
.alerts-trash:hover { color: var(--critical); border-color: var(--critical); }
.inc-nc-trash {
  margin-left: 6px; cursor: pointer; opacity: 0.45; font-size: 12px;
}
.inc-nc-trash:hover { opacity: 1; }

/* modale de confirmation */
.modal-blur {
  background: rgba(12, 12, 12, 0.42);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
}
.cfm-card { max-width: 440px; padding: 28px 32px; }
.cfm-msg { margin: 12px 0 20px; font-size: 13.5px; line-height: 1.55; color: var(--ink-2);
  white-space: pre-line; }   /* les \n des messages (multi-lignes) restent visibles */


/* Saisie de la modale generique (askTexte) : reprend la mise en forme des
   champs de connexion, dont cette fenetre partage deja la carte. */
.cfm-input { width: 100%; box-sizing: border-box; margin: -6px 0 18px;
  padding: 9px 11px; font-size: 13.5px; font-family: inherit;
  color: var(--ink); background: var(--page);
  border: 1px solid var(--hairline); border-radius: 7px; }
.cfm-input:focus { outline: 2px solid var(--accent); outline-offset: 1px;
  border-color: transparent; }.cfm-actions { justify-content: flex-end; }
.btn.danger { background: var(--critical); border-color: var(--critical); color: #fff; font-weight: 600; }
.btn.danger:hover { filter: brightness(1.08); color: #fff; }

/* onglets dans la topbar (centres) + carte plein ecran */
/* UNE SEULE LIGNE. Avec 9 onglets, le retour a la ligne renvoyait "Admin"
   tout seul en dessous — disgracieux et deroutant (l'onglet semblait
   appartenir a autre chose). On resserre pour que le cas courant tienne, et
   s'il manque vraiment de place la barre DEFILE horizontalement au lieu de
   se casser en deux. min-width: 0 est indispensable : sans lui un element
   flex refuse de retrecir sous la taille de son contenu, et overflow ne
   s'active jamais. */
/* UNE LIGNE, SANS DEFILEMENT. Neuf onglets doivent tenir entre la marque et
   les indicateurs d'etat. Ni retour a la ligne (Admin se retrouvait seul en
   dessous), ni barre de defilement (des onglets deviennent invisibles sans
   qu'on le sache). La place se prend donc AILLEURS : c'est la marque qui
   s'efface quand l'ecran retrecit — elle ne porte aucune information, les
   onglets si. */
.topbar-tabs {
  margin: 0; flex: 0 1 auto; min-width: 0; gap: 2px; padding: 3px;
  display: flex; flex-wrap: nowrap; justify-content: center;
}
.topbar-tabs .tab { white-space: nowrap; padding: 5px 10px; font-size: 12.5px; }

/* Degradation de la marque, par paliers, avant que les onglets ne souffrent. */
@media (max-width: 1650px) { .brand-thin { display: none; } }
@media (max-width: 1400px) { .brand-name { display: none; } }

/* Barre d'onglets sur SA PROPRE LIGNE quand le compte voit beaucoup de vues.
   Avec l'onglet Synthese, un compte admin (13 onglets) ne tenait plus sur la
   largeur MEME sur un grand ecran : la marque et « Tableau de bord » se
   chevauchaient, l'onglet Admin sortait du cadre. La classe est posee par
   applyPerms() d'apres le NOMBRE d'onglets visibles — un compte lecture a
   4 onglets garde l'en-tete sur une seule ligne. Le defilement horizontal ne
   sert que de filet si meme la pleine largeur ne suffit pas. */
/* Scope > 860 px : en dessous, c'est le menu hamburger d'initMobile() qui
   presente la navigation — ces regles d'etalement le perturberaient. */
@media (min-width: 861px) {
  body.nav-large .topbar { flex-wrap: wrap; row-gap: 2px; padding-bottom: 6px; }
  body.nav-large .topbar-tabs {
    order: 3; flex-basis: 100%; justify-content: center;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  body.nav-large .topbar-tabs::-webkit-scrollbar { display: none; }
}
/* Sur une largeur moyenne, la premiere ligne (marque + pastilles + compte +
   logo) peut encore se serrer : le logo IAPC est decoratif, il s'efface en
   premier — il reste dans le pied de page. */
@media (max-width: 1150px) { .topbar .iapc-logo { display: none; } }
/* Vue carte : la page NE DEFILE PAS et la carte occupe tout l'espace sous
   l'en-tete. Les regles precedentes dataient de la mise en page a une colonne
   et imposaient « height: calc(100vh - 252px) » au #map. Leur specificite
   (body.map-view #map) l'emportait sur celle de la regle de mise en page,
   quel que soit l'ordre : trois correctifs successifs ont ete ecrases avant
   d'agir, et la bande blanche de ~250 px restait. */
body.map-view { overflow: hidden; }
body.map-view main { max-width: none; padding: 0; }
body.map-view footer { display: none; }
@media (max-width: 900px) { .topbar-tabs { order: 3; flex-basis: 100%; } }

/* recherche de node sur la carte */
.map-search { position: relative; flex: 0 1 300px; }
.map-trackbar { flex-wrap: wrap; }
.map-search-input, #map-search-input {
  width: 100%; padding: 8px 12px; font-size: 13.5px;
  border: 1px solid var(--hairline); border-radius: 9px;
  background: var(--page); color: var(--ink);
}
.map-search-results {
  position: absolute; z-index: 600; left: 0; right: 0; top: calc(100% + 4px);
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 9px; box-shadow: var(--shadow); overflow: hidden; max-height: 320px; overflow-y: auto;
}
.map-search-item {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 8px 12px; cursor: pointer; font-size: 13px;
}
.map-search-item:hover { background: var(--surface-2); }
.map-search-item code { font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
.map-search-empty { padding: 10px 12px; color: var(--muted); font-size: 12.5px; }
.node-flash { animation: nodeflash 0.5s ease-in-out 3; }
@keyframes nodeflash { 50% { filter: drop-shadow(0 0 8px var(--accent)) brightness(1.4); } }

/* lien .zip DFU affiche/copiable */
.dfu-url-row { display: flex; gap: 8px; align-items: center; margin: 6px 0; flex-wrap: wrap; }
.dfu-url {
  font-family: var(--mono); font-size: 11.5px; color: var(--accent);
  background: var(--surface-2); border: 1px solid var(--hairline);
  border-radius: 6px; padding: 5px 8px; word-break: break-all; user-select: all; flex: 1 1 240px;
}

/* ---- RAK4631 : flash sans fil (Mac/Linux + nRF Connect) ---- */
.rk-flash {
  display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 4px 0 6px;
}
.rk-flash .rk-note { grid-column: 1 / -1; }
.rk-method {
  border: 1px solid var(--hairline); background: var(--surface-2);
  border-radius: 12px; padding: 12px 14px;
}
.rk-method-h { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.rk-method-p { font-size: 12.5px; color: var(--muted); margin: 0 0 8px; line-height: 1.45; }
.rk-cmd { display: flex; align-items: stretch; gap: 6px; margin-bottom: 8px; }
.rk-ble-btns { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.rk-method .rk-bar { margin: 2px 0 4px; }
.rk-cmd code {
  font-family: var(--mono); font-size: 11.5px; color: var(--accent);
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 6px; padding: 6px 8px; word-break: break-all; user-select: all; flex: 1;
}
.btn.mini { padding: 3px 10px; font-size: 11.5px; white-space: nowrap; }
.rk-note { font-size: 12px; color: var(--muted); line-height: 1.5; margin: 2px 0 0; }
.rk-nozip { font-size: 12px; color: var(--muted); }
.badge-ok, .badge-soft {
  font-size: 10.5px; font-weight: 600; padding: 1px 8px; border-radius: 999px;
}
.badge-ok { color: #1c7d3e; background: rgba(28, 125, 62, 0.14); }
.badge-soft { color: var(--muted); background: color-mix(in srgb, var(--muted) 18%, transparent); }
details.rk-cfg, details.rk-cable { margin-top: 8px; }
/* la config RAK n'est pas du texte d'aide : lever le max-width:78ch de .usb-help
   pour que la grille remplisse toute la largeur (colonnes multiples, moins de scroll) */
details.rk-cfg, details.rk-cable { max-width: none; }
#usb-rak-zone .maint-grid { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
@media (max-width: 720px) { .rk-flash { grid-template-columns: 1fr; } }

/* ---- RAK : badge temps, minuteur + barre de progression ---- */
.badge-time { font-size: 10.5px; font-weight: 600; padding: 1px 8px; border-radius: 999px;
  color: var(--accent); background: var(--accent-soft); }
.rk-timer { grid-column: 1 / -1; display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 2px 0 2px; }
.rk-bar { flex: 1 1 240px; height: 10px; border-radius: 999px; background: var(--surface-2);
  border: 1px solid var(--hairline); overflow: hidden; min-width: 160px; }
.rk-bar-fill { height: 100%; width: 0%; border-radius: 999px; background: var(--accent);
  transition: width .4s linear; }
.rk-bar-fill.done { background: #1c7d3e; }

/* ---- RAK : selecteur de profil IAPC / Libre ---- */
.rk-mode-field { max-width: 480px; margin-bottom: 10px; }
.rk-mode-hint { font-size: 12.5px; color: var(--muted); line-height: 1.5;
  margin: 8px 0 4px; padding: 8px 10px; border-radius: 8px;
  background: var(--accent-soft); }
#rk-libre { margin-top: 8px; }

/* ---- case "flash complet" (RAK / compagnon) ---- */
.cmp-chk { display: flex; align-items: flex-start; gap: 8px; margin: 10px 0 4px;
  font-size: 12.5px; color: var(--ink-2); line-height: 1.45; cursor: pointer; max-width: 70ch; }
.cmp-chk input { margin-top: 2px; flex: none; }
#cmp-erase-box, #rk-erase-box { margin: 4px 0 2px; }

/* ---- Campagne OTA (onglet Maintenance) : cibles + barre d'etapes ---- */
.camp-targets { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0; }
.camp-target { display: flex; align-items: center; gap: 7px; padding: 6px 11px;
  border: 1px solid var(--hairline); border-radius: 9px; background: var(--surface-2);
  cursor: pointer; font-size: 13px; }
.camp-target.off { opacity: .5; cursor: not-allowed; }
.camp-ver { font-family: var(--mono); font-size: 11px; color: var(--ink-2); }
.camp-note { font-size: 11px; color: var(--warning); }
.camp-note.ok { color: var(--good-text); }     /* deja a jour */
.camp-note.warn { font-weight: 600; }          /* downgrade : ambre appuye */
.camp-actions { display: flex; align-items: center; gap: 10px; margin: 8px 0; flex-wrap: wrap; }
.camp-sub { font-size: 12px; color: var(--muted); }
.camp-progress { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
.camp-row { border: 1px solid var(--hairline); border-radius: 10px; padding: 8px 12px;
  background: var(--surface); }
.camp-row.done { border-color: var(--good); }
.camp-row.failed { border-color: var(--critical); }
.camp-row.skipped { opacity: .55; }
.camp-row-head { display: flex; gap: 10px; align-items: baseline; margin-bottom: 5px; }
.camp-row-state { font-size: 12px; color: var(--muted); }
.camp-row.failed .camp-row-state { color: var(--critical); font-weight: 600; }
.camp-row.done .camp-row-state { color: var(--good-text); }
.camp-steps { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; font-size: 12px; }
.camp-step { padding: 2px 9px; border-radius: 999px; background: var(--surface-2);
  color: var(--muted); white-space: nowrap; }
.camp-step.done { background: var(--accent-soft); color: var(--ink); }
/* etape EN COURS : ambre pulse — se distingue du bleu "fait" et du rouge "echec" */
.camp-step.active { background: var(--warning); color: #fff;
  animation: camp-pulse 1.2s ease-in-out infinite; }
@keyframes camp-pulse { 50% { opacity: .6; } }
.camp-step.fail { background: var(--critical); color: #fff; }
.camp-sep { color: var(--muted); }
.camp-bar { height: 6px; border-radius: 3px; background: var(--surface-2);
  margin-top: 7px; overflow: hidden; }
.camp-bar i { display: block; height: 100%; background: var(--warning); transition: width .5s; }
.camp-msg { font-size: 12px; color: var(--ink-2); margin-top: 4px; }
.camp-msg.err { color: var(--critical); }

/* poubelle par coupure (vue detail d'un node, permission alerts-purge) */
.inc-ep-trash { margin-left: 8px; cursor: pointer; opacity: .45; flex: none; }
.inc-ep-trash:hover { opacity: 1; }

/* icones poubelle SVG : alignement + couleur au survol */
.inc-nc-trash svg, .inc-ep-trash svg { display: block; }
.inc-nc-trash, .inc-ep-trash { display: inline-flex; align-items: center; }
.inc-nc-trash:hover, .inc-ep-trash:hover { color: var(--critical); }

/* ---- Onglet Telemetrie ---- */
.tele-latest { margin-bottom: 14px; overflow-x: auto; }
.tele-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.tele-table th { text-align: left; font-size: 11px; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted); padding: 6px 10px;
  border-bottom: 1px solid var(--hairline); }
.tele-table td { padding: 6px 8px; border-bottom: 1px solid var(--hairline); }
/* Les deux colonnes qui faisaient deborder le tableau. Leur texte est
   volontairement explicite ("declaree · GPS sans fix" vaut mieux qu'un tiret) :
   plutot que de raccourcir les libelles, on les laisse passer a la ligne dans
   leur cellule. La largeur du tableau cesse alors de dependre de la longueur
   d'une phrase. */
.tele-table td.tele-c-gps, .tele-table td.tele-c-pos {
  white-space: normal; line-height: 1.3; }
.tele-table td.tele-c-gps { max-width: 15ch; }
.tele-table td.tele-c-pos { max-width: 20ch; }
.tele-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 900px) { .tele-charts { grid-template-columns: 1fr; } }
.tele-chart-box { border: 1px solid var(--hairline); border-radius: 10px;
  padding: 10px 12px; background: var(--surface); }
.tele-chart-box h3 { margin: 0 0 8px; font-size: 12.5px; font-weight: 650; }
.tele-svg { width: 100%; height: 110px; display: block; }
.tele-axis { display: flex; justify-content: space-between; gap: 8px;
  font-size: 11px; color: var(--muted); margin-top: 4px; font-family: var(--mono); }

/* barre de selection des courbes + ligne selectionnee du tableau */
.tele-chartbar { display: flex; align-items: center; gap: 8px; margin: 2px 0 10px;
  font-size: 13px; color: var(--ink-2); }
.tele-table tr[data-gw] { cursor: pointer; }
.tele-table tr[data-gw]:hover td { background: var(--surface-2); }
.tele-table tr.sel td { background: var(--accent-soft); }

/* echelles des graphes telemetrie */
.tele-plot { display: flex; gap: 6px; align-items: stretch; }
.tele-ylab { display: flex; flex-direction: column; justify-content: space-between;
  min-width: 40px; text-align: right; font-size: 10px; color: var(--muted);
  font-family: var(--mono); padding: 1px 0; flex: none; }
.tele-grid line { stroke: var(--grid); stroke-width: 1; }
/* les graphes sont etires horizontalement (preserveAspectRatio none) : sans
   ceci, les segments verticaux des courbes ressortent 2-3x plus epais que les
   horizontaux — trait constant et fin quelle que soit la largeur d'ecran */
.tele-svg path, .tele-svg line, .tele-svg circle { vector-effect: non-scaling-stroke; }
/* Repere de changement de firmware sur les courbes de telemetrie. Trait fin et
   discret : il doit situer l'evenement sans jamais concurrencer la mesure. */
.tele-vmark { stroke: var(--warning); stroke-width: 1.2; stroke-dasharray: 3 3;
  opacity: .75; }
/* vue Charge : graphes pleine largeur (une courbe par relais) + legende */
.load-chart-wrap { border: 1px solid var(--hairline); border-radius: 10px;
  padding: 16px 18px 14px; background: var(--surface); margin-bottom: 8px; }
.load-svg { height: 260px; }
.load-legend { display: flex; flex-wrap: wrap; gap: 8px 20px; margin-top: 14px;
  padding-top: 12px; border-top: 1px solid var(--hairline);
  font-size: 12px; color: var(--ink-2); }
.load-legend .lg { display: inline-flex; align-items: center; gap: 6px; }
.load-legend .sw { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.tele-svg { flex: 1 1 auto; min-width: 0; }
.tele-axis { margin-left: 46px; }
.tele-meta { font-size: 11px; color: var(--muted); margin-top: 3px;
  font-family: var(--mono); text-align: center; }

/* section Radio de l'onglet Telemetrie */
.tele-radio-head h3 { margin: 16px 0 8px; font-size: 13px; font-weight: 650; }
/* vue Charge : plus d'air entre les deux grands graphes */
#tele-view-load .tele-radio-head h3 { margin: 26px 0 4px; font-size: 14px; }
#tele-view-load .tele-radio-head span.muted { display: block; margin-bottom: 10px; }

/* sous-onglets Nodes / Radio de l'onglet Telemetrie */
.tele-subtabs { display: flex; gap: 4px; margin-left: 12px; }
.tele-subtabs .tab { font-size: 12px; padding: 4px 12px; }
/* sous-onglets de la vue Admin : nav autonome au-dessus des panneaux */
.adm-subtabs { margin: 0 0 14px; }
.adm-subtabs .tab { font-size: 13px; padding: 6px 16px; }
/* comptes en cartes (Admin > Comptes & reglages) */
.admin-user-cards {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 10px;
}
.admin-user-card {
  display: flex; flex-direction: column; gap: 7px;
  padding: 12px 14px; border: 1px solid var(--hairline); border-radius: 12px;
  background: var(--surface-2);
}
.admin-user-card .au-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.admin-user-card .u { font-weight: 650; font-size: 13.5px; }
.admin-user-card .au-n { font-size: 11px; color: var(--muted); white-space: nowrap; }
.admin-user-card .perms { display: flex; flex-wrap: wrap; gap: 4px; }
.admin-user-card 
/* campagne d'application de la config IAPC (Admin > Config IAPC) */
.iapc-camp-row { display: flex; align-items: center; gap: 10px; padding: 5px 0; cursor: pointer; }
.iapc-camp-row.off { opacity: 0.55; }
.iapc-camp-row .iapc-camp-pw { margin-left: auto; width: 220px; font-size: 13px; padding: 6px 10px; }
.iapc-diff { font-size: 12.5px; margin: 3px 0 0 24px; }
.iapc-diff.ok { color: var(--good-text); }
.iapc-diff.ko { color: var(--critical); font-weight: 600; }
.iapc-diff.info { color: var(--muted); }

/* matrice qui-entend-qui (sous-onglet Radio) */
.radio-matrix-wrap { overflow-x: auto; margin-bottom: 6px; }
.rm-table { border-collapse: collapse; font-size: 12px; font-family: var(--mono); }
.rm-table th { font-size: 10.5px; color: var(--muted); font-weight: 600;
  padding: 5px 10px; text-align: center; border: 1px solid var(--hairline); }
.rm-table tbody th { text-align: right; }
.rm-table td { padding: 5px 10px; text-align: center; border: 1px solid var(--hairline); }
.rm-table td.rm-ok { background: rgba(12, 163, 12, .14); color: var(--good-text); font-weight: 600; }
.rm-table td.rm-lost { background: rgba(208, 59, 59, .16); color: var(--critical); font-weight: 600; }
.rm-table td.rm-stale { background: rgba(185,127,0,.14); color: var(--warning); font-weight: 600; }
.rm-table td.rm-none, .rm-table td.rm-na { color: var(--muted); }

/* ===== Registre des compagnons (onglet Compagnons) ======================= */
/* Un tableau, pas un accordeon : la liste ne contient que NOS nodes (une
   poignee), tout tient donc sur une ligne par node et se compare d'un coup
   d'oeil — ce qu'un accordeon interdit par construction. */
.cmp-list-bar {
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0;
}
.cmp-list-bar input {
  flex: 1 1 200px; min-width: 160px; padding: 6px 10px;
  border: 1px solid var(--hairline); border-radius: 8px;
  background: var(--surface); color: var(--ink); font-size: 13px;
}
.cmp-list-bar select {
  padding: 6px 8px; border: 1px solid var(--hairline); border-radius: 8px;
  background: var(--surface); color: var(--ink); font-size: 12.5px;
}
/* Le tableau defile HORIZONTALEMENT dans son conteneur : 7 colonnes dont une
   adresse ne tiennent pas sur un ecran etroit, et la page entiere ne doit
   jamais partir en scroll lateral. */
.cmp-list { overflow-x: auto; max-height: 560px; overflow-y: auto; }
.cmp-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.cmp-table thead th {
  position: sticky; top: 0; z-index: 1; background: var(--surface);
  text-align: left; font-size: 10.5px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: .03em;
  padding: 7px 10px; border-bottom: 1px solid var(--hairline); white-space: nowrap;
}
.cmp-table td {
  padding: 7px 10px; border-bottom: 1px solid var(--hairline);
  vertical-align: top; white-space: nowrap;
}
.cmp-table tbody tr:hover { background: var(--surface-2); }
/* ligne positionnee = cliquable (ouvre la modale carte + trace des positions) */
.cmp-table tbody tr[data-lat] { cursor: pointer; }
.cmpm-period { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.cmpm-period select { font-size: 12.5px; padding: 4px 8px; }
.cmp-c-name b { font-weight: 600; }
/* Cle publique et horodatages secondaires : en dessous, discrets — la ligne
   reste unique mais l'information technique ne concurrence pas le nom. */
.cmp-pk, .cmp-sub {
  display: block; font-size: 10.5px; color: var(--muted); font-weight: 400;
}
.cmp-pk { font-family: var(--mono); }
.cmp-table code { font-family: var(--mono); font-size: 11.5px; }
.cmp-stale { color: var(--warning); }
.cmp-c-addr { white-space: normal; min-width: 190px; max-width: 320px; }
.cmp-c-act { white-space: nowrap; }
.cmp-c-act .btn { padding: 1px 8px; font-size: 11px; margin-right: 4px; }
/* Modale carte d'un node : plus large que les modales de formulaire, la carte
   a besoin de place. Hauteur en vh pour rester utilisable sur un portable. */
#cmp-map-modal { z-index: 1200; }
/* Les dialogues generiques s'empilent AU-DESSUS de toute autre modale : sans
   cela, une confirmation ouverte depuis la liste noire (#ban-modal, plus bas
   dans le DOM a z-index egal) se peignait DERRIERE elle -> bouton sans effet. */
#confirm-modal { z-index: 11000; }
#pwask-modal   { z-index: 11500; }
#pwask-modal .gwm-pwrow { margin: 10px 0 4px; }
#pwask-modal #pwask-input { width: 100%; }
.cmpm-card { max-width: 1100px; width: 94vw; }
.cmpm-body { display: flex; gap: 12px; align-items: stretch; }
#cmp-map {
  height: min(58vh, 520px); min-height: 260px; flex: 1; min-width: 0;
  border-radius: 10px; border: 1px solid var(--hairline);
  background: var(--surface-2);
}
/* panneau lateral : liste des positions (date, coordonnees, adresse).
   C'est une zone de defilement ; la barre est FORCEE visible (styler
   ::-webkit-scrollbar desactive les barres en surimpression de macOS, qui
   n'apparaissent qu'au defilement) et un compteur colle en tete dit combien
   de positions la liste contient — sans cela, 17 points sur la carte et 8
   visibles dans le panneau passaient pour une liste incomplete. */
#cmpm-pts {
  width: 300px; flex: none; overflow-y: auto; max-height: min(58vh, 520px);
  border: 1px solid var(--hairline); border-radius: 10px; padding: 0 4px 4px;
  scrollbar-width: thin; scrollbar-color: var(--muted) transparent;
}
#cmpm-pts::-webkit-scrollbar { width: 10px; }
#cmpm-pts::-webkit-scrollbar-track { background: transparent; }
#cmpm-pts::-webkit-scrollbar-thumb {
  background: var(--hairline); border-radius: 6px; border: 2px solid var(--surface);
}
#cmpm-pts::-webkit-scrollbar-thumb:hover { background: var(--muted); }
.cmpm-pts-head {
  position: sticky; top: 0; z-index: 1; background: var(--surface);
  padding: 7px 9px 6px; font-size: 12px; border-bottom: 1px solid var(--hairline);
}
.cmpm-pts-more { float: right; font-size: 11px; color: var(--accent); font-weight: 600; }
.cmpm-pt {
  padding: 7px 9px; border-radius: 8px; cursor: pointer; font-size: 12px;
  border-bottom: 1px solid var(--hairline);
}
.cmpm-pt:last-child { border-bottom: none; }
.cmpm-pt:hover, .cmpm-pt.sel, .cmpm-pt.hot { background: var(--surface-2); }
.cmpm-pt.sel { box-shadow: inset 3px 0 0 var(--accent); }
.cmpm-pt .t { font-weight: 650; }
.cmpm-pt .t .n { color: var(--muted); font-weight: 400; }
.cmpm-pt code { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.cmpm-pt .addr { color: var(--muted); margin-top: 1px; }
@media (max-width: 760px) {
  .cmpm-body { flex-direction: column; }
  #cmpm-pts { width: 100%; max-height: 260px; }
}
/* « deja a nous » dans les resultats de recherche (Adopter un compagnon) */
.cmp-badge-ok {
  font-size: 10px; font-weight: 700; color: var(--good-text);
  border: 1px solid var(--good-text); border-radius: 4px; padding: 0 4px;
  white-space: nowrap;
}
/* profils d'advert (Portable / Fixe) au-dessus des champs d'intervalle */
.cmp-profils {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 6px 0 8px;
  font-size: 12.5px;
}
.cmp-profil-btn.active {
  border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); font-weight: 650;
}
#cmpm-foot { margin-top: 8px; }

.cmp-badge-wait {
  font-size: 10px; font-weight: 700; color: var(--warning);
  border: 1px solid var(--warning); border-radius: 4px; padding: 0 4px;
  margin-left: 5px;
}

/* Identifiant technique (gw_id) en sous-titre du nom lisible. Discret mais
   jamais absent : c'est lui qu'il faut connaitre pour viser le bon node en
   CLI, en campagne OTA ou en liste noire. Monospace = on le lit comme une
   cle, pas comme un mot. */
.gw-id-sub {
  display: inline-block; margin-left: 6px;
  font-family: var(--mono); font-size: 10.5px; font-weight: 400;
  color: var(--muted); white-space: nowrap;
}

/* ===== Vue Propagation (sonde passive de bout en bout) ==================== */
/* Tuiles de captation : magnitude + tendance. La tendance porte un LIBELLE —
   vert et ambre sont indiscernables en deuteranopie, la couleur ne fait que
   renforcer un texte deja explicite. */
.prop-cards { display: flex; flex-wrap: wrap; gap: 10px; margin: 10px 0 4px; }
.prop-card {
  flex: 1 1 190px; min-width: 190px; max-width: 280px;
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 10px; padding: 10px 12px; box-shadow: var(--shadow);
}
.prop-card-gw { font-weight: 700; font-size: 13px; }
.prop-card-rate {
  font-family: var(--mono); font-size: 26px; line-height: 1.15;
  font-variant-numeric: tabular-nums; margin: 2px 0;
}
.prop-card-unit { font-size: 14px; color: var(--muted); margin-left: 2px; }
.prop-card-trend { font-size: 11.5px; font-weight: 600; color: var(--ink-2); }
.prop-card-trend.ok   { color: var(--good-text); }
.prop-card-trend.warn { color: var(--warning); }
.prop-card-trend.bad  { color: var(--critical); }
.prop-card-trend.na   { color: var(--muted); font-weight: 500; }
.prop-card-meta { font-size: 11px; color: var(--muted); margin-top: 3px; }

/* Matrice : rampe SEQUENTIELLE d'une seule teinte (l'accent), du clair au
   fonce. La valeur est ecrite dans chaque case — la couleur ne porte jamais
   l'information seule. Bordure epaisse couleur surface = 2 px de respiration
   entre les aplats. */
/* Paliers de la rampe, en variables : la matrice ET sa legende y puisent, donc
   elles ne peuvent pas diverger. Le mode sombre a SES PROPRES paliers (plus
   hauts, plus ecartes) : sur fond noir, un aplat de faible opacite ne se
   detache pas du fond — un simple decalque du mode clair ecrasait le bas de
   l'echelle (verifie sur capture). */
:root {
  --ramp-1: 14%; --ramp-2: 25%; --ramp-3: 34%; --ramp-4: 46%; --ramp-5: 58%;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --ramp-1: 20%; --ramp-2: 36%; --ramp-3: 52%; --ramp-4: 68%; --ramp-5: 88%;
  }
}
:root[data-theme="dark"] {
  --ramp-1: 20%; --ramp-2: 36%; --ramp-3: 52%; --ramp-4: 68%; --ramp-5: 88%;
}
.prop-matrix td { border: 2px solid var(--surface) !important; padding: 4px 9px; }
.prop-cell { color: var(--ink); }
.prop-cell[data-s="1"] { background: color-mix(in srgb, var(--accent) var(--ramp-1), transparent); }
.prop-cell[data-s="2"] { background: color-mix(in srgb, var(--accent) var(--ramp-2), transparent); }
.prop-cell[data-s="3"] { background: color-mix(in srgb, var(--accent) var(--ramp-3), transparent); }
.prop-cell[data-s="4"] { background: color-mix(in srgb, var(--accent) var(--ramp-4), transparent); }
.prop-cell[data-s="5"] { background: color-mix(in srgb, var(--accent) var(--ramp-5), transparent); }
.prop-pct { font-variant-numeric: tabular-nums; font-weight: 600; }
.prop-hop {
  display: inline-block; margin-left: 5px; min-width: 15px;
  font-size: 10px; font-weight: 700; color: var(--ink-2);
  background: var(--surface); border-radius: 4px; padding: 0 3px;
}
.prop-hop-direct { color: var(--good-text); }

.prop-legend {
  display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  font-size: 11px; color: var(--muted); margin: 8px 0 2px;
}
.prop-legend-label { font-weight: 600; color: var(--ink-2); }
.prop-ramp { display: inline-flex; gap: 2px; }
.prop-ramp i { width: 22px; height: 11px; border-radius: 2px; }
.prop-ramp i[data-s="1"] { background: color-mix(in srgb, var(--accent) var(--ramp-1), transparent); }
.prop-ramp i[data-s="2"] { background: color-mix(in srgb, var(--accent) var(--ramp-2), transparent); }
.prop-ramp i[data-s="3"] { background: color-mix(in srgb, var(--accent) var(--ramp-3), transparent); }
.prop-ramp i[data-s="4"] { background: color-mix(in srgb, var(--accent) var(--ramp-4), transparent); }
.prop-ramp i[data-s="5"] { background: color-mix(in srgb, var(--accent) var(--ramp-5), transparent); }
.prop-legend-sep {
  width: 1px; height: 13px; background: var(--hairline); margin: 0 4px;
}

/* Redondance : barres horizontales, extremite arrondie 4 px cote donnee. */
.prop-redundancy { margin: 10px 0 4px; max-width: 620px; }
.prop-red-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.prop-red-label {
  flex: 0 0 68px; text-align: right; font-size: 11.5px; color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.prop-red-track { flex: 1 1 auto; height: 15px; background: var(--surface-2); border-radius: 4px; }
.prop-red-bar {
  height: 100%; background: var(--accent); border-radius: 0 4px 4px 0; min-width: 2px;
}
/* les paquets a UN seul temoin sont l'information critique de ce graphe :
   teinte de statut + etiquette texte (jamais la couleur seule) */
.prop-red-bar.alone { background: var(--critical); }
.prop-red-val {
  flex: 0 0 165px; font-size: 11.5px; color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.prop-tag-alone {
  display: inline-block; margin-left: 4px; font-size: 10px; font-weight: 700;
  color: var(--critical); border: 1px solid var(--critical);
  border-radius: 4px; padding: 0 4px;
}

/* case "répète" par region (editeur de regions, modale gateway) */
.gwr-rep { display: inline-flex; align-items: center; gap: 3px; font-size: 11px;
  color: var(--ink-2); margin-right: 8px; cursor: pointer; }
.gwr-rep input { margin: 0; }

/* kickban : bouton popup carte + panneau Admin liste noire */
.np-actions { margin-top: 8px; }
.np-ban { font-size: 11px; padding: 4px 10px; }
.ban-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.ban-row { display: flex; align-items: center; gap: 10px; padding: 6px 10px;
  border: 1px solid var(--hairline); border-radius: 8px; background: var(--surface); }
.ban-row code { font-size: 12px; }
.ban-note { font-size: 12px; color: var(--muted); }

/* liste noire pilotee depuis la carte : copie de cle + ajout */
.np-key { user-select: all; }
.np-copy, .ban-copy { width: 22px; height: 22px; font-size: 12px; margin-left: 4px;
  vertical-align: middle; }
.ban-add-row { display: flex; gap: 8px; margin: 12px 0; flex-wrap: wrap; }
.ban-add-row input { flex: 1 1 160px; padding: 7px 10px; border-radius: 8px;
  border: 1px solid var(--hairline); background: var(--page); color: var(--ink);
  font-size: 13px; }
.ban-add-row input:first-child { font-family: var(--mono); flex-basis: 180px; }

/* bouton liste noire : icone compacte integree a l'en-tete de la carte.
   align-self: center car .panel-head est en align-items:baseline (les toggles
   s'alignent sur leur texte, pas l'icone ronde). */
.map-ban-icon { font-size: 13px; align-self: center; }
.map-ban-icon:hover { border-color: var(--critical); color: var(--critical); }

/* ---- Page de maintenance plein ecran (mode maintenance, menu admin) ------ */
#maint-overlay {
  position: fixed; inset: 0; z-index: 12000;
  background: radial-gradient(1200px 800px at 50% 28%, #0e1b33 0%, #070d1a 55%, #04070f 100%);
  color: #dbe7ff; display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
#maint-overlay[hidden] { display: none; }
/* grille "sol" en perspective qui defile lentement */
.mo-grid {
  position: absolute; inset: -60% -30%;
  background-image:
    linear-gradient(rgba(90,140,255,.08) 1px, transparent 1px),
    linear-gradient(90deg, rgba(90,140,255,.08) 1px, transparent 1px);
  background-size: 46px 46px;
  transform: perspective(650px) rotateX(55deg) translateY(10%);
  animation: mo-slide 16s linear infinite;
}
@keyframes mo-slide { to { background-position: 0 46px, 46px 0; } }
.mo-inner { position: relative; text-align: center; padding: 32px 24px; max-width: 660px; }
.mo-brand { font-size: 13px; letter-spacing: .16em; text-transform: uppercase; color: #8fb0e8; margin-bottom: 28px; }
.mo-brand .brand-mark { color: #5aa0ff; }
/* radar : anneaux qui pulsent + balayage rotatif + noeuds satellites */
.mo-radar { position: relative; width: 190px; height: 190px; margin: 0 auto 30px; }
.mo-ring { position: absolute; inset: 0; border: 1px solid rgba(90,160,255,.4); border-radius: 50%; animation: mo-pulse 3s ease-out infinite; }
.mo-ring.r2 { animation-delay: 1s; } .mo-ring.r3 { animation-delay: 2s; }
@keyframes mo-pulse { 0% { transform: scale(.32); opacity: .95; } 100% { transform: scale(1.18); opacity: 0; } }
.mo-sweep {
  position: absolute; inset: 7%; border-radius: 50%;
  background: conic-gradient(from 0deg, rgba(90,160,255,.55), transparent 20%);
  animation: mo-rot 4s linear infinite;
}
@keyframes mo-rot { to { transform: rotate(360deg); } }
.mo-core { position: absolute; left: 50%; top: 50%; width: 14px; height: 14px; margin: -7px; border-radius: 50%; background: #5aa0ff; box-shadow: 0 0 20px 5px rgba(90,160,255,.75); }
.mo-sat { position: absolute; width: 7px; height: 7px; border-radius: 50%; background: #9fd0ff; animation: mo-blink 2.2s infinite; }
.mo-sat.s1 { left: 13%; top: 25%; } .mo-sat.s2 { right: 9%; top: 42%; animation-delay: .7s; } .mo-sat.s3 { left: 30%; bottom: 11%; animation-delay: 1.4s; }
@keyframes mo-blink { 0%, 100% { opacity: .2; } 50% { opacity: 1; } }
.mo-title { font-size: 34px; font-weight: 700; margin: 0 0 10px; letter-spacing: .02em; color: #f2f7ff; }
.mo-msg { font-size: 17px; color: #a9c2ea; margin: 0 0 26px; min-height: 22px; }
/* petit terminal de statut */
.mo-term {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13.5px; text-align: left; line-height: 2;
  background: rgba(8,16,32,.78); border: 1px solid rgba(90,160,255,.28);
  border-radius: 10px; padding: 14px 18px;
  box-shadow: 0 10px 44px rgba(0,0,0,.55);
}
.mo-prompt { color: #5aa0ff; margin-right: 9px; }
.mo-dim { color: #7e97c4; }
.mo-caret { margin-left: 6px; color: #9fd0ff; animation: mo-blink 1s steps(1) infinite; }
.mo-foot { margin-top: 28px; font-size: 12px; letter-spacing: .24em; text-transform: uppercase; color: #5c729c; }
@media (max-width: 560px) {
  .mo-title { font-size: 26px; }
  .mo-radar { width: 150px; height: 150px; }
  .mo-term { font-size: 12px; }
}

/* ===== Parcours de flash RAK4631 ========================================= */
/* Deux etapes numerotees, UN bouton principal. Tout le reste est replie :
   l'ancienne version alignait boutons et cases a cocher sans dire ce qui
   devait se passer, on ne distinguait pas un succes d'un echec. */
.rak-steps {
  margin: 10px 0 6px; padding-left: 22px; display: flex; flex-direction: column;
  gap: 12px; font-size: 13px;
}
.rak-steps > li { padding-left: 2px; }
.rak-steps code { font-family: var(--mono); font-size: 12px; }
.rak-ops { margin: 8px 0 4px; display: flex; gap: 8px; flex-wrap: wrap; }
.rak-status {
  font-size: 12.5px; line-height: 1.5; min-height: 18px; margin-top: 4px;
}
.rak-ok { color: var(--good-text); font-weight: 600; }
.rak-ko { color: var(--critical); font-weight: 600; }
.rak-warn { color: var(--warning); font-weight: 600; }
.rak-foot { font-size: 11.5px; color: var(--muted); margin: 8px 0 0; }
.rak-foot code { font-family: var(--mono); }

.rak-more { margin-top: 10px; border-top: 1px solid var(--hairline); padding-top: 8px; }
.rak-more > summary {
  cursor: pointer; font-size: 12px; color: var(--muted); list-style: none;
}
.rak-more > summary::-webkit-details-marker { display: none; }
.rak-more > summary::before { content: "▸ "; }
.rak-more[open] > summary::before { content: "▾ "; }
.rak-more-row {
  display: flex; gap: 10px; align-items: flex-start; margin: 10px 0 0;
  font-size: 12px;
}
.rak-more-row .btn { flex: 0 0 auto; padding: 3px 10px; font-size: 12px; }
.rak-more-txt { line-height: 1.5; }
.rak-after { margin-top: 12px; }
.rak-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
.rak-or { font-size: 12px; color: var(--ink-2); align-self: center; }
.rak-steps .rak-more { border-top: none; padding-top: 4px; margin-top: 6px; }
.rak-seq-intro { margin: 8px 0 4px; }
.rak-seq-3 { flex: 0 0 auto; font-weight: 700; padding: 3px 10px; font-size: 12px; }
.rak-more > summary b { color: var(--ink); }

/* ===== PASSE MOBILE COMPLETE (2026-07-30) ================================ */
/* Regroupee en fin de fichier pour primer sur les regles anterieures de meme
   specificite. Objectif : app REELLEMENT utilisable au telephone — rien qui
   deborde de l'ecran, cibles tactiles >= 40 px, pas de zoom iOS parasite. */

/* Onglets topbar : sous 900 px ils passent sous la marque en pleine largeur ;
   ils doivent DEFILER horizontalement (avant : nowrap sans overflow -> les
   derniers onglets sortaient de l'ecran, invisibles et inatteignables). */
@media (max-width: 900px) {
  .topbar { flex-wrap: wrap; }
  .topbar-tabs {
    order: 3; flex-basis: 100%;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    justify-content: flex-start;
    scrollbar-width: none;
  }
  .topbar-tabs::-webkit-scrollbar { display: none; }
  .topbar-tabs .tab { padding: 7px 12px; }   /* cible tactile plus haute */
}

@media (max-width: 640px) {
  /* iOS zoome automatiquement sur tout champ < 16 px : on garantit 16 px. */
  input, select, textarea { font-size: 16px; }

  main { padding: 10px 8px 24px; }
  .panel { padding: 12px 12px; }

  /* Rien ne doit imposer une largeur > ecran : les longues chaines cassent. */
  .rk-cmd { flex-wrap: wrap; }
  .rk-cmd code {
    white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-all;
    max-width: 100%;
  }
  .gwm-log, .cli-console { overflow-x: auto; max-width: 100%; }
  .usb-help { max-width: none; }

  /* Bandeau « 1-2-3 » du flasher : etapes empilees. */
  .usb-steps { flex-direction: column; gap: 6px; }
  .usb-step { flex: none; }

  /* Boutons principaux : pleine largeur, hauteur tactile. */
  .btn:not(.mini) { min-height: 42px; }
  .rk-ble-btns { flex-direction: column; align-items: stretch; }
  .rk-ble-btns .btn { width: 100%; text-align: center; justify-content: center; }
  #usb-rak-shortcut { width: 100%; }
  .usb-validate { flex-wrap: wrap; }
  .rak-ops, .rak-more-row { flex-wrap: wrap; }
  .rk-timer { flex-wrap: wrap; }

  /* Modales : plein ecran au telephone (la config gateway etait rognee). */
  .gwm-card {
    width: 100vw; max-width: none; max-height: 100dvh;
    border-radius: 0; padding: 16px 14px;
  }
  .gwm-cols { gap: 10px; }
  .cfm-card { width: 92vw; padding: 20px 18px; }

  /* Selecteurs et champs du flasher a la largeur de l'ecran. */
  #usb-fw, .field select, .field input { max-width: 100%; width: 100%; }
}

/* ===== Modale de configuration RAK (cable serie) ======================== */
.rkcfg-card { max-width: 920px; width: 94vw; max-height: 92vh; overflow-y: auto; text-align: left; }
.rkcfg-card .app-login-brand { text-align: left; }
.rkc-port-row { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }
.rkc-port-info { font-size: 13px; color: var(--muted); }
.rkc-port-info.ok { color: var(--good-text); font-weight: 600; }
.rkc-modes { display: flex; gap: 10px; margin-bottom: 14px; }
.rkc-mode {
  flex: 1; display: flex; gap: 9px; align-items: flex-start; cursor: pointer;
  border: 1px solid var(--hairline); border-radius: 10px; padding: 10px 12px;
  background: var(--surface-2); font-size: 13px;
}
.rkc-mode:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.rkc-mode input { margin-top: 3px; }
.rkc-mode small { color: var(--muted); }
.rkc-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
.rkc-sep {
  grid-column: 1 / -1; font-size: 11px; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--muted); margin: 8px 0 0;
  border-top: 1px solid var(--hairline); padding-top: 10px;
}
.rkc-grid input:disabled, .rkc-grid select:disabled {
  background: var(--surface-2); color: var(--muted); cursor: not-allowed; opacity: 0.75;
}
@media (min-width: 760px) { .rkc-grid { grid-template-columns: 1fr 1fr 1fr; } }
@media (max-width: 560px) { .rkc-grid { grid-template-columns: 1fr; } .rkc-modes { flex-direction: column; } }

/* ===== Choix de methode de flash RAK (gros boutons) + grande modale ====== */
.rk-choose { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 4px 0 8px; }
.rk-choose-btn {
  display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
  padding: 16px 18px; border: 1px solid var(--hairline); border-radius: 14px;
  background: var(--surface-2); cursor: pointer; text-align: left;
  transition: border-color .12s, transform .06s, background .12s;
}
.rk-choose-btn:hover { border-color: var(--accent); background: var(--accent-soft); }
.rk-choose-btn:active { transform: translateY(1px); }
.rk-choose-i { font-size: 26px; line-height: 1; }
.rk-choose-t { font-size: 15px; font-weight: 650; color: var(--ink); }
.rk-choose-s { font-size: 12px; color: var(--muted); }
.rk-choose-esp { grid-template-columns: repeat(2, 1fr); }
/* formulaire de flash ESP32 dans la grande modale : colonnes assez larges
   pour ne pas tronquer selects et placeholders (4 colonnes sinon) */
#esp-flash-content .maint-grid { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }

.rk-big-card {
  max-width: 900px; width: 94vw; max-height: 92vh; overflow-y: auto; text-align: left;
}
.rk-big-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 14px;
}
.rk-big-head .app-login-brand { text-align: left; margin: 0; }
/* dans la grande modale, les 2 methodes BLE s'empilent proprement */
#rk-big-body .rk-flash { display: block; }
#rk-big-body .rk-method { margin-bottom: 14px; }
@media (max-width: 720px) {
  .rk-choose { grid-template-columns: 1fr; }
  .rk-big-card { width: 100vw; max-width: none; max-height: 100dvh; border-radius: 0; }
}

/* ===== Assistant pas-a-pas (flash cable RAK) ============================= */
.wiz-top { display: flex; align-items: center; gap: 12px; margin: 2px 0 16px; }
.wiz-bar { flex: 1; height: 8px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
.wiz-bar-fill { height: 100%; width: 25%; border-radius: 999px; background: var(--accent); transition: width .25s ease; }
.wiz-count { font-size: 12px; font-weight: 600; color: var(--muted); white-space: nowrap; }
.wiz-step { min-height: 200px; font-size: 14px; line-height: 1.55; }
.wiz-step p { margin: 0 0 12px; }
.wiz-title { margin: 0 0 14px; font-size: 17px; font-weight: 650; }
.wiz-step code { font-family: var(--mono); font-size: 12.5px; background: var(--surface-2); padding: 1px 6px; border-radius: 5px; }
.wiz-dl { margin: 16px 0; }
.wiz-dl .btn { font-size: 14px; padding: 10px 20px; }
.wiz-ok { color: var(--good-text); }
.wiz-warn { color: var(--critical); background: color-mix(in srgb, var(--critical) 8%, transparent);
  border-radius: 8px; padding: 10px 12px; }
.wiz-nav { display: flex; justify-content: space-between; gap: 12px; margin-top: 18px;
  border-top: 1px solid var(--hairline); padding-top: 14px; }
.wiz-nav .btn { min-width: 130px; }
.wiz-nav .btn:disabled { opacity: 0.4; cursor: default; }

/* ---- Trames : regroupement des receptions d'un meme paquet --------------- */
/* Un paquet entendu par quatre relais occupait quatre lignes, reliees par un
   hex tronque illisible. Groupe, il tient sur une ligne dont la seconde moitie
   dit QUI l'a entendu et a quelle force — la lecture qui interesse vraiment. */
.grp .msg { line-height: 1.5; }
.rx-line { margin-top: 3px; display: flex; flex-wrap: wrap; gap: 4px 10px; }
.rx { font-size: 11.5px; color: var(--ink-2); white-space: nowrap;
  font-family: var(--mono); }
.rx b { font-weight: 600; margin-left: 4px; }
.rx.rssi-good b { color: #2fa25c; }
.rx.rssi-mid  b { color: #e0a10e; }
.rx.rssi-bad  b { color: #d03b3b; }
.nrx { font-size: 11.5px; font-weight: 600; color: var(--ink-2);
  background: var(--surface-2); border-radius: 999px; padding: 1px 7px; }
.msg-sep { color: var(--muted); margin: 0 2px; }
.chk { display: inline-flex; align-items: center; gap: 6px; font-size: 13px;
  color: var(--ink-2); margin-left: 14px; cursor: pointer; }

/* ---- Voisinage : qui entend qui ------------------------------------------ */
/* Mise en page reprise de meshcore.alfare.net (fiche relais + tableau des
   voisins), mais aux couleurs du monitoring : une vue qui detonne se lit mal,
   et on veut pouvoir passer d'un onglet a l'autre sans changer de repere. */
.vois-bar { display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  margin-bottom: 14px; font-size: 13px; color: var(--ink-2); }
.vois-bar select { font-size: 13px; }

.vois-tete { border: 1px solid var(--hairline); border-radius: 10px;
  background: var(--surface); padding: 14px 16px; margin-bottom: 14px; }
.vt-nom { font-weight: 650; font-size: 15px; margin-bottom: 8px; }
.vt-l { font-size: 12.5px; color: var(--ink-2); line-height: 1.75;
  font-family: var(--mono); }

.vois-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.vois-table th { text-align: left; font-size: 11px; text-transform: uppercase;
  letter-spacing: .05em; color: var(--muted); padding: 6px 10px;
  border-bottom: 1px solid var(--hairline); white-space: nowrap; }
.vois-table td { padding: 6px 10px; border-bottom: 1px solid var(--hairline); }
.vois-table tr:hover td { background: var(--surface-2); }
.vois-table .vn { font-weight: 600; }
.vois-table .vid { font-family: var(--mono); font-size: 12px; color: var(--ink-2); }
.vois-table .vd { font-family: var(--mono); font-size: 11.5px; color: var(--muted);
  white-space: nowrap; }
/* Une empreinte qui colle a deux nodes connus n'est PAS attribuee : mieux vaut
   un trou signale qu'un lien invente. */
.vois-amb { color: var(--warning); font-weight: 600; cursor: help; }
.vois-vide { padding: 22px; text-align: center; line-height: 1.7; }
.vois-legende { font-size: 12.5px; color: var(--ink-2); margin-bottom: 10px;
  line-height: 1.6; }

/* Matrice : le tableau peut etre large, il defile SEUL sans pousser la page. */
.mx-wrap { overflow-x: auto; }
.mx .mx-gw { font-family: var(--mono); font-size: 10.5px; text-transform: none;
  letter-spacing: 0; }
.mx-v { text-align: right; font-family: var(--mono); font-size: 12px; }
.mx-0 { text-align: center; color: var(--muted); }
.mx-faible { color: var(--ink-2); }
.mx-moy    { color: #e0a10e; font-weight: 600; }
.mx-fort   { color: #2fa25c; font-weight: 700; }
/* Entendu par TOUS les relais : le noeud est central dans le maillage. */
.mx-central { color: #2fa25c; font-weight: 700; }

/* Fenetre a defilement : la page ne s'allonge pas avec le nombre de voisins,
   et l'en-tete du tableau reste visible pendant qu'on parcourt la liste. */
.vois-scroll { max-height: 460px; overflow-y: auto; border: 1px solid var(--hairline);
  border-radius: 10px; background: var(--surface); }
.vois-scroll .vois-table th { position: sticky; top: 0; z-index: 1;
  background: var(--surface); box-shadow: inset 0 -1px 0 var(--hairline); }
.vois-titre { font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin: 4px 0 6px; font-weight: 650; }
/* Cle publique du relais, comme sur la vue de reference : sous le nom, en
   monospace, tronquee — c'est un identifiant qu'on reconnait, pas qu'on lit. */
.vt-pk { font-family: var(--mono); font-size: 11.5px; color: var(--muted);
  margin: -4px 0 8px; word-break: break-all; }
/* Empreinte partagee par plusieurs nodes : on affiche les NOMS candidats. Le
   lecteur reconnait souvent celui qui est plausible chez lui — un algorithme,
   non. Le « ou » reste discret pour que les noms se lisent d'abord. */
.amb-ou { color: var(--muted); font-weight: 400; font-style: italic; }
/* Cle publique complete dans la colonne ID. Les 2 premiers octets — ceux qui
   circulent reellement dans le chemin — sont en gras ; le reste vient de notre
   registre et se lit en retrait. */
.pk-full { font-family: var(--mono); font-size: 11.5px; letter-spacing: -.2px; }
.pk-full b { color: var(--ink); font-weight: 700; }

/* Colonnes de la vue Voisinage : valeurs CENTREES sous leur titre. Les noms
   restent a gauche — un nom se lit du bord, pas du milieu. */
.vois-table th:not(:first-child), .vois-table td:not(.vn) { text-align: center; }
.vois-table .vid, .vois-table .vd { text-align: center; }
.pk-court { font-family: var(--mono); font-size: 11.5px; color: var(--muted); }
/* La vue occupe la hauteur disponible et RIEN DE PLUS : c'est le tableau qui
   defile, jamais la page. Un ascenseur de page en plus de celui du tableau
   oblige a viser le bon, et fait perdre l'en-tete du relais des qu'on descend.
   Le flex evite le nombre magique : l'en-tete prend ce qu'il lui faut, la
   fenetre prend le reste. min-height: 0 est indispensable — sans lui un enfant
   flex refuse de retrecir sous sa hauteur de contenu et deborde quand meme. */
#tele-view-vois { display: flex; flex-direction: column; }
#vois-carte, #vois-matrice-vue { display: flex; flex-direction: column;
  flex: 1 1 auto; min-height: 0; }
.vois-scroll { flex: 1 1 auto; min-height: 0; max-height: none; }
/* La barre de filtres et l'en-tete du relais ne doivent pas s'etirer. */
.vois-bar, .vois-tete, .vois-titre, .vois-legende { flex: 0 0 auto; }

/* Legende des couleurs de la colonne Nom. Une couleur sans legende n'informe
   personne : noir = identifie sans doute, ambre = plusieurs candidats possibles,
   gris = absent de notre registre. */
.vois-leg { float: right; font-weight: 400; text-transform: none; letter-spacing: 0;
  font-size: 11px; color: var(--muted); }
.vois-leg span { display: inline-block; width: 8px; height: 8px; border-radius: 2px;
  margin: 0 4px 0 12px; vertical-align: baseline; }
.lg-ok  { background: var(--ink); }
.lg-amb { background: var(--warning); }
.lg-non { background: var(--muted); }
/* Alignement : on cible aussi les cellules nommees, pour passer devant les
   regles generiques thead th.num / tbody td.num definies plus haut. */
.vois-table thead th:not(:first-child),
.vois-table tbody td:not(.vn) { text-align: center !important; }
.vois-sep { width: 1px; height: 18px; background: var(--hairline); }
.vois-bar input[type="search"] { width: 160px; }
.vois-bar input[type="number"] { width: 56px; }
.vois-row { cursor: pointer; }
.vois-row:hover td { background: var(--accent-soft); }
/* Bloc de choix : sa propre zone, entre le message et la rangee d'actions.
   Boutons empiles et pleine largeur — plusieurs actions de portees differentes
   se lisent en colonne, pas cote a cote ou l'on clique la mauvaise. */
.cfm-choix { display: flex; flex-direction: column; align-items: stretch;
  gap: 8px; margin: 16px 0 4px; }
.cfm-choix .btn { width: 100%; justify-content: center; }
.cfm-choix .btn svg { margin-right: 6px; vertical-align: -2px; }
/* Liaison asymetrique : un sens vit, l'autre est perdu. C'est le cas le plus
   parlant de la matrice — il designe un defaut d'emission ou de reception,
   la ou un lien absent des deux cotes peut n'etre qu'une question de distance. */
.rm-asym { outline: 2px solid var(--warning); outline-offset: -2px; font-weight: 700; }
/* Lien mort dans les deux sens : present mais efface. Il ne doit pas se
   confondre avec le tiret « jamais entendu » — d'ou le point, plus dense. */
.rm-mort { color: var(--hairline); text-align: center; cursor: help; }
.rm-chk { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px;
  color: var(--ink-2); margin-bottom: 8px; cursor: pointer; }
/* Direct ou relaye : un voisin DIRECT renseigne sur la portee radio reelle,
   un voisin relaye dit seulement que le node existe quelque part. */
.lien-dir { color: #2fa25c; font-weight: 650; }
.lien-mix { color: #e0a10e; font-weight: 600; }
.lien-rel { color: var(--muted); }
/* Marque « banni » dans la liste des voisins : visible seulement quand on
   demande a les montrer, pour pouvoir les retrouver et les debannir. */
.vois-banni { font-size: 10.5px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em; color: #d03b3b; border: 1px solid currentColor;
  border-radius: 4px; padding: 0 4px; margin-left: 6px; cursor: help; }

/* ---- Perimetre regional --------------------------------------------------
   Les etats fautifs sont colores et pas seulement affiches : un relais mal
   configure ne se signale par AUCUN compteur, il emet et personne ne relaie. */
.rgn-mal { color: var(--danger, #f87171); font-weight: 600; }
.rgn-tag {
  display: inline-block; margin: 1px 3px 1px 0; padding: 1px 7px;
  border: 1px solid var(--border); border-radius: 10px; font-size: 12px;
  white-space: nowrap;
}
.rgn-nu { color: var(--warn, #fbbf24); border-color: currentColor; }
.rgn-def { border-color: var(--ok, #4ade80); color: var(--ok, #4ade80); font-weight: 600; }
.vt-rgn { line-height: 1.9; }
/* Regions COTE A COTE sur une seule ligne : empilees, elles cassaient la
   hauteur des lignes et rendaient le tableau illisible des qu'un node
   traversait deux perimetres. La cellule defile si besoin plutot que de
   repousser les colonnes suivantes. */
/* La colonne Region prend la place disponible : c'est la seule dont le contenu
   est variable et non bornable — un voisin peut arriver par plusieurs
   perimetres. Les colonnes de dates et d'identifiants sont de largeur fixe, on
   la leur reprend. Un nom de region ne se coupe jamais en deux (nowrap sur
   l'etiquette), mais l'ensemble passe a la ligne s'il le faut. */
.vois-table td.vrgn {
  text-align: center; white-space: normal;
  width: 28%; min-width: 210px; line-height: 1.85;
}
.vois-table .rgn-tag { white-space: nowrap; }
.vois-table .vn { width: 26%; }
.vois-table .vid, .vois-table .vd { width: 1%; white-space: nowrap; }
.vt-rgn { display: flex; align-items: baseline; flex-wrap: wrap; gap: 3px; }
/* Separateur entre deux regions : « ch-fr ch » se lisait comme un seul nom. */
.rgn-sep { color: var(--hairline); margin: 0 6px; user-select: none; }

/* ---- Bavards : classement des emetteurs d'adverts ------------------------
   La medaille est ironique : ce podium designe les trois qui coutent le plus
   au reseau, pas les trois meilleurs. Les couleurs vont donc du neutre au
   rouge, jamais au vert. */
.flood-podium { display: flex; gap: 12px; flex-wrap: wrap; margin: 4px 0 16px; }
.fp-carte {
  flex: 1 1 200px; min-width: 190px; padding: 12px 14px; border-radius: 10px;
  border: 1px solid var(--hairline); background: var(--surface-2); text-align: center;
}
.fp-0 { border-color: #d4a017; }
.fp-1 { border-color: #9aa3ad; }
.fp-2 { border-color: #b07a45; }
.fp-med { font-size: 26px; line-height: 1.1; }
.fp-nom { font-weight: 700; margin: 4px 0 2px; word-break: break-word; }
.fp-n { font-size: 20px; font-weight: 700; }
.fp-int, .fp-v { font-size: 12px; color: var(--muted); }
.fp-v { margin-top: 4px; font-weight: 600; }
.fv-excessif { color: var(--danger, #f87171); font-weight: 700; }
.fv-soutenu { color: var(--warn, #fbbf24); font-weight: 600; }
.fv-normal { color: var(--muted); }
.fv-inconnu { color: var(--muted); font-style: italic; }

.flood-stats { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.fs-bloc {
  flex: 1 1 180px; padding: 10px 12px; border-radius: 8px;
  border: 1px solid var(--hairline); background: var(--surface);
}
.fs-t { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.fs-v { font-size: 22px; font-weight: 700; margin: 2px 0; }
.fs-s { font-size: 11px; color: var(--muted); line-height: 1.35; }

.flood-types { margin-bottom: 16px; }
.ft-titre { font-size: 13px; font-weight: 600; margin-bottom: 6px; }
.ft-l { display: flex; align-items: center; gap: 10px; margin: 3px 0; font-size: 12px; }
.ft-n { flex: 0 0 90px; font-family: var(--mono); }
.ft-bar { flex: 1 1 auto; height: 8px; background: var(--surface-2); border-radius: 4px; overflow: hidden; }
.ft-bar i { display: block; height: 100%; background: var(--accent, #60a5fa); }
.ft-v { flex: 0 0 190px; text-align: right; color: var(--muted); font-family: var(--mono); }

.flood-decl { color: var(--ok, #4ade80); font-weight: 600; }
.flood-note { margin-top: 14px; font-size: 12px; line-height: 1.5; max-width: 900px; }
.flood-table td.vn { font-weight: 600; }
/* Region connue mais non relayee ici : une regle assumee, pas une anomalie —
   donc en teinte d'attention et non en rouge d'erreur. */
.rgn-refus { color: var(--warn, #fbbf24); border-color: currentColor; }
.adv-cfg-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin: 4px 0 8px; }
.adv-cfg-head h3 { margin: 0; font-size: 15px; }
.adv-table { width: auto; min-width: 560px; margin-bottom: 18px; }
.adv-table td, .adv-table th { text-align: center; }
.adv-table td:first-child, .adv-table th:first-child { text-align: left; }
/* Une periode incomplete affichee comme complete fait sous-estimer tout le
   monde : on le dit dans le tableau, pas dans une note de bas de page. */
.flood-partiel {
  margin: 4px 0 8px; padding: 7px 10px; border-radius: 6px; font-size: 12px;
  border: 1px solid var(--warn, #fbbf24); color: var(--warn, #fbbf24);
}
.flood-bar { margin: 2px 0 12px; }
.flood-bar input[type="search"] { min-width: 200px; }
.flood-bar input[type="number"] { width: 68px; }

/* ---- Bavards : plier ce qui explique, garder ce qui repond ----------------
   Sept blocs de meme poids visuel enterraient la seule chose que la page
   affirme : qui inonde le reseau. Le podium et le classement restent nus, le
   reste s'ouvre a la demande. */
.flood-det {
  margin: 14px 0 0; border-top: 1px solid var(--hairline); padding-top: 10px;
}
.flood-det > summary {
  cursor: pointer; font-weight: 600; font-size: 13px; padding: 4px 0;
  list-style: none; display: flex; align-items: baseline; gap: 8px;
}
.flood-det > summary::-webkit-details-marker { display: none; }
.flood-det > summary::before {
  content: "▸"; color: var(--muted); font-size: 11px; transition: transform .12s;
}
.flood-det[open] > summary::before { content: "▾"; }
.flood-det > summary:hover { color: var(--accent, #60a5fa); }
.flood-det .fd-h { font-weight: 400; color: var(--muted); font-size: 12px; }
.flood-det > *:not(summary) { margin-top: 8px; }
.flood-note { margin-top: 4px; }
.adv-cfg-head { margin: 0 0 4px; }

/* ---- Liens radio : trois groupes nommes ----------------------------------
   Les paires etaient triees du pire au meilleur mais toutes de meme
   apparence : rien ne disait ou s'arretait le probleme et ou commencait le
   normal. On ne masque rien, on nomme les sections. */
.rl-synth {
  font-size: 13px; font-weight: 600; margin: 0 0 10px;
  padding-bottom: 8px; border-bottom: 1px solid var(--hairline);
}
.rl-groupe { margin-bottom: 12px; }
/* Chevrons par groupe (28.08, « trop de scroll ») : titre cliquable, la
   grille se replie ; Actifs est replie par defaut — voir rlEtatGroupes(). */
.rl-gtitre { cursor: pointer; user-select: none; }
.rl-gtitre .rl-chev { color: var(--muted); font-size: 12px; margin-left: 6px; }
.rl-groupe.rl-ferme .rl-grille { display: none; }
.rl-groupe.rl-ferme { margin-bottom: 6px; }
.rl-gtitre {
  font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
  font-weight: 700; margin: 0 0 6px; color: var(--muted);
}
.rl-g0 .rl-gtitre { color: var(--danger, #f87171); }
.rl-g1 .rl-gtitre { color: var(--warn, #fbbf24); }
.rl-g2 .rl-gtitre { color: var(--ok, #4ade80); }
/* La duree de silence etait en infobulle : personne ne survole une liste. */
.rl-age { color: var(--muted); font-size: 11.5px; }
.rl-neuf {
  margin-left: 6px; padding: 0 6px; border-radius: 8px; font-size: 10.5px;
  border: 1px dashed var(--hairline); color: var(--muted);
}

/* ---- Tableau de bord en sous-onglets -------------------------------------
   Cinq sujets s'empilaient sur une page unique. Le bandeau d'etat et les
   tuiles restent au-dessus : ils disent quel onglet merite un regard, et les
   pastilles empechent qu'un incident se cache derriere un onglet ferme. */
.dash-wrap { padding-top: 0; }
.dash-subtabs { flex-wrap: wrap; }
.dash-badge {
  display: inline-block; min-width: 17px; padding: 0 5px; margin-left: 5px;
  border-radius: 9px; font-size: 10.5px; font-weight: 700; line-height: 16px;
  text-align: center; background: var(--surface-2); color: var(--muted);
}
.dash-badge.grave { background: var(--critical); color: #fff; }
.dash-wrap > div[id^="dash-view-"] > .panel,
.dash-wrap > div[id^="dash-view-"] > .panels > .panel { box-shadow: none; }
/* La periode vit dans un selecteur en haut de page : une moyenne « une toutes
   les 3,4 h » se lit sinon comme un rythme instantane, alors qu'elle porte sur
   sept jours. Le rappel suit donc la donnee, pas le filtre. */
.periode-rappel { color: var(--muted); font-weight: 400; font-size: 11.5px; }

/* ---- Carte : une seule barre de controles --------------------------------
   L'en-tete melait titre, quatre interrupteurs et un bouton d'action, puis une
   seconde rangee melait recherche et suivi de compagnon. Deux barres
   heterogenes, sans rapport avec les filtres du reste de l'application. */
/* Le suivi de compagnon est un usage rare et d'une autre nature : deplie a la
   demande, il ne pese plus sur la page a chaque ouverture de la carte. */
.map-suivi { margin: 0 0 10px; }
.map-suivi > summary {
  cursor: pointer; font-weight: 600; font-size: 13px; padding: 3px 0;
  list-style: none; display: flex; align-items: baseline; gap: 8px;
}
.map-suivi > summary::-webkit-details-marker { display: none; }
.map-suivi > summary::before { content: "▸"; color: var(--muted); font-size: 11px; }
.map-suivi[open] > summary::before { content: "▾"; }
.map-suivi > summary:hover { color: var(--accent, #60a5fa); }
.map-suivi .map-trackbar { padding: 6px 0 2px; }
/* La carte est le contenu : elle gagne la place reprise aux barres. */
.mf-titre2 { margin-top: 8px; padding-top: 7px; border-top: 1px solid var(--hairline); }
/* L'attribution des tuiles doit rester lisible sur les fonds satellite. */
.leaflet-control-attribution {
  background: color-mix(in srgb, var(--surface) 82%, transparent) !important;
  color: var(--muted) !important; font-size: 10px;
}
.leaflet-control-attribution a { color: var(--ink-2) !important; }

/* ---- Carte : deux colonnes, et de bord a bord -----------------------------
   L'emprise du reseau est plus LARGE que haute, et l'ecran aussi : la ressource
   rare est la hauteur, pas la largeur. Une colonne laterale la libere en
   consommant une largeur dont on a de la marge — la ou les barres empilees
   mangeaient la hauteur, et les cartes flottantes masquaient le terrain. */
.map-split {
  /* minmax(0, 1fr) et non 1fr : « 1fr » vaut « minmax(auto, 1fr) », et le
     minimum automatique empeche la colonne de descendre sous la largeur
     min-content de son contenu. Une carte Leaflet peut reclamer bien plus que
     la place disponible, la colonne deborde alors et le rendu se decale. */
  display: grid; grid-template-columns: 320px minmax(0, 1fr); gap: 0;
  align-items: stretch; overflow: hidden;
  /* La hauteur vient de la COQUILLE : la vue remplit ce que main lui laisse.
     Plus de nombre magique, plus de calcul JS (--carte-haut) — c'etait un
     pansement sur l'absence de coquille, et il ne tombait juste que sur un
     seul gabarit d'en-tete. */
  margin: 0; flex: 1 1 auto; min-height: 0;
  border-top: 1px solid var(--hairline);
}
/* La vue carte est une colonne : sa grille prend toute la hauteur restante. */
#view-map { display: flex; flex-direction: column; }
.map-aside {
  display: flex; flex-direction: column; gap: 0; overflow-y: auto;
  background: var(--surface); border-right: 1px solid var(--hairline);
}
.ma-bloc { padding: 12px 16px; border-bottom: 1px solid var(--hairline); }
.ma-titre { margin: 0 0 3px; font-size: 15px; }
.ma-sum { font-size: 12px; color: var(--muted); line-height: 1.5; }
.ma-bloc .map-search { position: relative; }
.ma-bloc .map-search input { width: 100%; padding: 7px 10px; font-size: 13px; }
.ma-bloc #map-fond, .ma-bloc select { width: 100%; font-size: 12.5px; padding: 5px 8px; }
.ma-bloc .chk { display: flex; align-items: center; gap: 7px; font-size: 13px; margin: 4px 0; }
.ma-bloc .map-trackbar { display: flex; flex-direction: column; gap: 6px; padding: 0; }
.ma-legende .map-legend {
  position: static; display: flex; flex-direction: column; align-items: flex-start;
  gap: 6px; background: none; border: none; box-shadow: none; padding: 0;
}
.ma-pied { margin-top: auto; padding: 12px 16px; position: sticky; bottom: 0;
  background: var(--surface); border-top: 1px solid var(--hairline); z-index: 2; }
.ma-pied .btn { width: 100%; }
/* La carte occupe tout le reste, sans bordure ni arrondi : bord a bord. */
/* POSITIONNEMENT ABSOLU, et non « height: 100% ». Sur un enfant de grille dont
   la rangee est auto-dimensionnee, une hauteur en pourcentage ne resout pas :
   la carte gardait sa taille intrinseque pendant que la colonne laterale, elle,
   s'etirait — d'ou la bande vide sous la carte. Un enfant absolu cale sur les
   quatre bords remplit son parent quelle que soit la facon dont sa hauteur a
   ete calculee. */
.map-wrap { position: relative; min-height: 0; min-width: 0; overflow: hidden; }
/* height: auto EXPLICITE. Une regle mobile declare « height: 52vh » sur #map ;
   ne pas redeclarer la propriete ici la laisserait s'appliquer, et « inset: 0 »
   ne suffirait pas a etirer la carte. C'est deux fois de suite qu'une hauteur
   heritee d'une ancienne mise en page a mange le bas de la carte. */
#map {
  position: absolute; inset: 0; height: auto; min-height: 0;
  border: none; border-radius: 0;
}

@media (max-width: 900px) {
  .map-split { grid-template-columns: 1fr; height: auto; }
  .map-aside { border-right: none; border-bottom: 1px solid var(--hairline); }
  .map-wrap { height: 62vh; min-height: 400px; }
}

/* ---- Profil de terrain ----------------------------------------------------
   La modale generique fait 480 px : un profil de 30 km y serait illisible. */
#prof-modal { z-index: 11200; }
.prof-card { max-width: 980px; width: 94vw; }
.prof-head { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 8px; }
.prof-sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
.prof-h { font-size: 12px; color: var(--muted); white-space: nowrap; align-self: center; }
.prof-h input { width: 52px; padding: 3px 5px; font-size: 12px; }
.prof-verdict {
  font-size: 13.5px; margin: 4px 0 10px; padding: 8px 12px;
  border: 1px solid var(--hairline); border-radius: 8px; background: var(--surface-2);
}
.prof-svg { width: 100%; height: 300px; display: block; }
.pg-grille { stroke: var(--hairline); stroke-width: 1; }
.pg-axe { fill: var(--muted); font-size: 10px; }
.pg-sol { fill: color-mix(in srgb, var(--ink) 22%, transparent); }
.pg-terrain { fill: none; stroke: var(--ink-2); stroke-width: 1.4; }
.pg-visee { fill: none; stroke: var(--ok, #4ade80); stroke-width: 2; stroke-dasharray: 6 4; }
.pg-fresnel { fill: color-mix(in srgb, var(--accent, #60a5fa) 22%, transparent); }
.pg-legende { display: flex; gap: 16px; flex-wrap: wrap; font-size: 11.5px; color: var(--muted); margin-top: 8px; }
.pg-legende i { display: inline-block; width: 16px; height: 3px; margin-right: 5px; vertical-align: middle; }
.pg-legende .lg-terrain { background: var(--ink-2); }
.pg-legende .lg-visee { background: var(--ok, #4ade80); }
.pg-legende .lg-fresnel { background: color-mix(in srgb, var(--accent, #60a5fa) 55%, transparent); height: 8px; }
.prof-note { font-size: 11.5px; line-height: 1.5; margin-top: 10px; }
/* Deux gestes de portees differentes dans la bulle d'un noeud : bannir MASQUE,
   oublier EFFACE. Le second est neutre et non rouge — il n'est pas plus grave,
   il est autre, et les peindre pareil inviterait a les confondre. */
.np-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.np-actions .btn { font-size: 11.5px; padding: 4px 9px; }
.np-forget { display: inline-flex; align-items: center; gap: 5px; }
.np-forget svg { width: 12px; height: 12px; }

/* ---- Double authentification ---------------------------------------------
   Les codes de secours ne sont montres QU'UNE fois : ils doivent etre gros,
   selectionnables et impossibles a confondre. */
.tfa-card { max-width: 520px; width: 94vw; }
.tfa-form { display: flex; align-items: flex-end; gap: 10px; margin: 10px 0; flex-wrap: wrap; }
.tfa-form label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); flex: 1 1 180px; }
.tfa-form input { padding: 7px 10px; font-size: 14px; }
.tfa-qr { display: flex; justify-content: center; margin: 10px 0; }
.tfa-qr svg { width: 190px; height: 190px; background: #fff; padding: 8px; border-radius: 8px; }
.tfa-secret code { font-size: 13px; letter-spacing: .08em; user-select: all; }
.tfa-codes {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px;
  margin: 12px 0; padding: 12px; border: 1px solid var(--hairline);
  border-radius: 8px; background: var(--surface-2);
}
.tfa-codes code { font-size: 15px; letter-spacing: .06em; user-select: all; text-align: center; }
#otp-code { width: 100%; padding: 10px 12px; font-size: 20px; letter-spacing: .22em; text-align: center; }

/* Superposition des dialogues — TROISIEME fois que ce piege se referme.
   Tous les ecrans plein cadre partagent .app-login (z-index 1000) : a egalite,
   c'est le DERNIER du document qui gagne. #app-login est en fin de fichier,
   il recouvrait donc la demande de code, invisible et inaccessible.
   Regle a retenir : tout nouveau dialogue plein cadre declare son rang ICI. */
#otp-modal { z-index: 11600; }   /* au-dessus de l'ecran de connexion */
#tfa-modal { z-index: 11100; }

/* ---- Infobulle des courbes ------------------------------------------------
   Les graphes n'affichaient que leurs extremes : dater un pic imposait une
   requete en base. La valeur, l'heure et la date sont desormais sous la
   souris. */
.tele-plot { position: relative; }
.cg-bulle {
  position: absolute; top: 6px; z-index: 40; pointer-events: none;
  background: color-mix(in srgb, var(--surface) 96%, transparent);
  border: 1px solid var(--hairline); border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0,0,0,.22); padding: 7px 10px;
  font-size: 12px; line-height: 1.5; white-space: nowrap;
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
.cg-t { font-weight: 700; margin-bottom: 3px; font-variant-numeric: tabular-nums; }
.cg-l { display: flex; align-items: center; gap: 6px; }
.cg-l b { margin-left: auto; font-variant-numeric: tabular-nums; }
.cg-p { width: 9px; height: 3px; border-radius: 2px; flex: none; }
.cg-trait {
  position: absolute; top: 0; bottom: 0; width: 1px; z-index: 30;
  background: var(--muted); opacity: .55; pointer-events: none;
}

/* ---- Journal / carnet de bord ---------------------------------------------
 * Une ligne de temps unique pour deux natures d'evenements : ce que les relais
 * ont subi (rail gris, pastille coloree par gravite) et ce que des humains leur
 * ont fait (rail accentue, pastille pleine). La difference doit se voir SANS
 * lire l'etiquette : c'est ce qui permet de balayer trois mois d'un coup d'oeil.
 */
.jr-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 14px; margin-bottom: 14px; flex-wrap: wrap;
}
.jr-title { margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -0.01em; }
.jr-sub { font-size: 12.5px; color: var(--muted); margin-top: 3px; line-height: 1.5; }
.jr-head-actions { display: flex; gap: 8px; }

.jr-chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.jr-chip {
  border: 1px solid var(--hairline); background: var(--surface-2); color: var(--ink-2);
  border-radius: 999px; padding: 4px 11px; font-size: 12px; cursor: pointer;
  white-space: nowrap; transition: border-color .12s, color .12s, background .12s;
}
.jr-chip:hover { color: var(--ink); border-color: var(--muted); }
.jr-chip.on {
  background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent); font-weight: 600;
}
.jr-chip-sep {
  width: 1px; height: 16px; background: var(--hairline); margin: 0 3px; flex: none;
}
.jr-chip-lbl {
  font-size: 10.5px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--muted); margin-right: 2px;
}

/* Filtres actifs : chaque critere retirable seul, sans vider tout le reste */
.jr-actifs { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 12px; }
.jr-actifs:empty { display: none; }
.jr-tag {
  display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px;
  background: var(--accent-soft); color: var(--accent); font-weight: 600;
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  border-radius: 999px; padding: 3px 6px 3px 10px;
}
.jr-tag b { font-weight: 700; }
.jr-tag button {
  border: 0; background: none; color: inherit; cursor: pointer; font-size: 13px;
  line-height: 1; padding: 0 3px; opacity: .7;
}
.jr-tag button:hover { opacity: 1; }

.jr-stats { margin-bottom: 14px; }
.jr-stats .stat-card .v.crit { color: var(--critical); }
.jr-stats .stat-card .v.warn { color: var(--warning); }

/* Dossier du relais selectionne */
.jr-dossier { margin-bottom: 14px; }
.jr-dossier-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px;
}
.jr-d-item .k {
  font-size: 10.5px; font-weight: 600; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted);
}
.jr-d-item .v { font-size: 19px; font-weight: 650; font-variant-numeric: tabular-nums; }
.jr-d-item .v.bad { color: var(--critical); }
.jr-d-item .v.mid { color: var(--warning); }
.jr-d-item .v.good { color: var(--good-text); }

/* ---- Le flux lui-meme ---- */
.jr-flux { display: flex; flex-direction: column; }
.jr-jour {
  position: sticky; top: 0; z-index: 2;
  background: var(--page); padding: 12px 0 7px;
  font-size: 11.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); border-bottom: 1px solid var(--hairline); margin-bottom: 4px;
}
.jr-jour .n { font-weight: 500; text-transform: none; letter-spacing: 0; margin-left: 8px; }

.jr-ligne {
  display: grid; grid-template-columns: 62px 16px 1fr; gap: 0 10px;
  align-items: start; padding: 7px 8px 7px 4px; border-radius: 8px;
  cursor: pointer; transition: background .1s;
}
.jr-ligne:hover { background: var(--surface-2); }
.jr-ligne .jr-h {
  font-family: var(--mono); font-size: 11.5px; color: var(--muted);
  font-variant-numeric: tabular-nums; padding-top: 2px; text-align: right;
}
/* rail vertical continu : c'est lui qui fait lire l'ensemble comme UNE
   chronologie et non comme une suite de lignes independantes */
.jr-rail { position: relative; display: flex; justify-content: center; align-self: stretch; }
.jr-rail::before {
  content: ""; position: absolute; top: 0; bottom: -7px; width: 1px;
  background: var(--grid);
}
.jr-ligne:last-child .jr-rail::before { bottom: auto; height: 11px; }
.jr-dot {
  position: relative; z-index: 1; width: 9px; height: 9px; border-radius: 50%;
  margin-top: 4px; background: var(--surface); border: 2px solid var(--muted); flex: none;
}
.jr-ligne.crit .jr-dot { border-color: var(--critical); }
.jr-ligne.warn .jr-dot { border-color: var(--warning); }
.jr-ligne.info .jr-dot { border-color: var(--grid); }
/* action humaine : pastille PLEINE et accentuee — on distingue d'un coup d'oeil
   ce qui a ete decide de ce qui est arrive */
.jr-ligne.humain .jr-dot { background: var(--accent); border-color: var(--accent); }
.jr-ligne.humain.crit .jr-dot { background: var(--critical); border-color: var(--critical); }
.jr-ligne.humain.warn .jr-dot { background: var(--warning); border-color: var(--warning); }

.jr-corps { min-width: 0; }
.jr-l1 { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.jr-kind {
  font-size: 10.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-2); background: var(--surface-2); border: 1px solid var(--hairline);
  border-radius: 5px; padding: 1px 6px; white-space: nowrap; flex: none;
}
.jr-ligne.crit .jr-kind { color: var(--critical); border-color: color-mix(in srgb, var(--critical) 35%, transparent); background: color-mix(in srgb, var(--critical) 8%, transparent); }
.jr-ligne.warn .jr-kind { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 35%, transparent); background: color-mix(in srgb, var(--warning) 8%, transparent); }
.jr-ligne.humain .jr-kind { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, transparent); background: var(--accent-soft); }
.jr-txt { font-size: 13px; color: var(--ink); line-height: 1.45; overflow-wrap: anywhere; }
.jr-ligne.cli .jr-txt { font-family: var(--mono); font-size: 12.5px; }
.jr-ref {
  font-size: 11.5px; font-weight: 600; border-radius: 5px; padding: 1px 7px;
  border: 1px solid var(--hairline); background: var(--surface-2); color: var(--ink-2);
  cursor: pointer; white-space: nowrap; flex: none;
}
.jr-ref:hover { border-color: var(--accent); color: var(--accent); }
.jr-ref.qui { color: var(--accent); background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 25%, transparent); }
.jr-echec { color: var(--critical); font-weight: 700; font-size: 11.5px; flex: none; }

/* Rattachement cause -> effet : le coeur de l'onglet. Un reboot qui suit une
   commande n'est plus une coincidence a reconstituer de tete. */
.jr-cause {
  font-size: 11.5px; color: var(--muted); margin-top: 2px;
  display: flex; align-items: baseline; gap: 5px;
}
.jr-cause b { color: var(--ink-2); font-weight: 600; }

.jr-detail {
  margin-top: 7px; padding: 9px 11px; border-radius: 8px;
  background: var(--surface); border: 1px solid var(--hairline);
  font-family: var(--mono); font-size: 11.5px; color: var(--ink-2);
  white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.55;
}
.jr-detail .jr-d-actions { margin-top: 8px; display: flex; gap: 8px; font-family: system-ui, sans-serif; }

.jr-vide { padding: 26px; text-align: center; color: var(--muted); font-size: 13px; }
.jr-note {
  font-size: 12px; color: var(--muted); background: var(--surface-2);
  border: 1px solid var(--hairline); border-left: 3px solid var(--accent);
  border-radius: 8px; padding: 9px 12px; margin-bottom: 12px; line-height: 1.5;
}
@media (max-width: 720px) {
  .jr-ligne { grid-template-columns: 50px 14px 1fr; }
  .jr-head-actions { width: 100%; }
}

/* ---- Journal : vues rapides ------------------------------------------------
 * Un menu deroulant suppose qu'on sache deja ce qu'on cherche : il faut ouvrir
 * « Type », lire quinze entrees techniques (offline, hopdrift, deafgw...) et
 * deviner laquelle repond a « ce relais a-t-il redemarre cette nuit ». Ces
 * boutons posent la question a la place de l'exploitant — et portent leur
 * compte, pour qu'on voie ce qu'il y a a regarder AVANT de cliquer. */
.jr-vues {
  display: flex; flex-wrap: wrap; gap: 7px; margin: 0 0 12px;
}
.jr-vue {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--hairline); background: var(--surface); color: var(--ink-2);
  border-radius: 9px; padding: 6px 11px; font: inherit; font-size: 12.5px;
  cursor: pointer; white-space: nowrap;
  transition: border-color .12s, color .12s, background .12s;
}
.jr-vue:hover { color: var(--ink); border-color: var(--muted); }
.jr-vue-i { font-size: 13px; line-height: 1; opacity: .85; }
.jr-vue-n {
  font-size: 11.5px; font-weight: 700; font-variant-numeric: tabular-nums;
  background: var(--surface-2); border-radius: 999px; padding: 1px 7px;
  color: var(--ink-2); min-width: 20px; text-align: center;
}
.jr-vue.on {
  background: var(--accent-soft); color: var(--accent); font-weight: 600;
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.jr-vue.on .jr-vue-n { background: var(--accent); color: #fff; }
/* Un compte a zero reste CLIQUABLE — repondre « aucun redemarrage cette
   semaine » est une reponse, pas une impasse — mais s'efface pour que l'oeil
   aille d'abord ou il y a quelque chose. */
.jr-vue.vide { opacity: .45; }
.jr-vue.vide:hover { opacity: .8; }
@media (max-width: 720px) {
  .jr-vues { gap: 5px; }
  .jr-vue { padding: 5px 9px; font-size: 12px; }
}

/* ---- Journal : frise de densite -------------------------------------------
 * Deux teintes de donnees seulement, et c'est mesure : le validateur de palette
 * ne donnait que 7,1 d'ecart perceptuel (deuteranopie) entre l'orange « a
 * surveiller » et le rouge « critique », sous le seuil de 8. Bleu de magnitude
 * + rouge critique montent a 23,8 en clair et 19,2 en sombre. La gravite
 * intermediaire n'est pas perdue : elle reste dans le flux et l'infobulle.
 * L'activite humaine est marquee par une FORME (un carre sous l'axe) en encre
 * neutre, pas par une troisieme couleur a departager. */
.jr-frise {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 12px; padding: 11px 14px 8px; margin-bottom: 14px;
  box-shadow: var(--shadow);
}
.jf-tete {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-bottom: 6px;
}
.jf-titre { font-size: 13px; font-weight: 650; }
.jf-sub { font-size: 11.5px; color: var(--muted); }
.jf-leg { margin-left: auto; display: flex; gap: 12px; flex-wrap: wrap; }
.jf-li {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 11.5px; color: var(--ink-2);   /* jamais la couleur de la serie */
}
.jf-sw { width: 9px; height: 9px; border-radius: 2px; display: inline-block; flex: none; }
.jf-sw.b { background: var(--accent); }
.jf-sw.c { background: var(--critical); }
.jf-sw.h { background: var(--ink-2); width: 7px; height: 7px; }

.jf-plot { position: relative; }
.jf-svg { display: block; max-width: 100%; }
.jf-b { fill: var(--accent); }
.jf-c { fill: var(--critical); }
.jf-h { fill: var(--ink-2); }
.jf-axe { stroke: var(--grid); stroke-width: 1; }      /* axe recessif */
.jf-lab { fill: var(--muted); font-size: 10.5px; font-family: var(--mono); }
/* Zone de survol : viser une barre de 3 px a la souris est une epreuve, viser
   sa colonne ne l'est pas. */
.jf-z { fill: transparent; cursor: pointer; }
.jf-z:hover { fill: var(--accent-soft); }

.jf-bulle {
  position: absolute; bottom: calc(100% - 44px); transform: translateX(-50%);
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: 8px; padding: 7px 10px; font-size: 11.5px; line-height: 1.5;
  color: var(--ink); box-shadow: var(--shadow); pointer-events: none;
  white-space: nowrap; z-index: 3;
}
.jf-tc { color: var(--critical); font-weight: 600; }
.jf-astuce { color: var(--muted); font-size: 10.5px; }
@media (max-width: 720px) {
  .jf-leg { margin-left: 0; width: 100%; }
}

/* Absence d'auteur EXPLIQUEE plutot que laissee en blanc : « antérieur à la
   traçabilité » et « non déclenché depuis cette interface » sont deux réponses
   differentes, et toutes deux valent mieux que le silence. */
.jr-cause.na { color: var(--muted); font-style: italic; }
/* Annotation : la parole de l'exploitant, ajoutee a cote de la ligne sans rien
   effacer. Liseré accentué a gauche pour qu'on voie tout de suite que c'est un
   humain qui parle, et non la machine qui constate. */
.jr-note {
  margin-top: 5px; padding: 5px 10px; font-size: 12px; line-height: 1.5;
  color: var(--ink); background: var(--surface-2);
  border-left: 2px solid var(--accent); border-radius: 0 7px 7px 0;
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
}
.jr-note-sig { margin-left: auto; font-size: 11px; color: var(--muted);
  font-variant-numeric: tabular-nums; white-space: nowrap; }

/* ============================================================================
 * Admin — comptes utilisateurs (refonte)
 * Dix-huit cases a cocher a plat ne se raisonnent pas : on cochait des mots
 * isoles sans voir qu'on donnait « le pilotage » ou « le firmware ». Les cartes
 * disent le ROLE et les FAMILLES de droits ; l'edition passe par une modale ou
 * les permissions sont groupees et ou des roles pre-etablis posent un jeu
 * coherent en un clic.
 * ========================================================================== */

.au-top { display: flex; align-items: center; gap: 10px; }

.au-id { display: flex; flex-direction: column; min-width: 0; }

.au-badges { display: flex; flex-wrap: wrap; gap: 6px; }
.au-badge {
  font-size: 11px; color: var(--ink-2); background: var(--surface);
  border: 1px solid var(--hairline); border-radius: 6px; padding: 2px 8px;
  font-variant-numeric: tabular-nums;
}
.au-badge.fa { color: var(--good-text); border-color: color-mix(in srgb, var(--good) 35%, transparent); }
.au-badge.na { color: var(--muted); }

.au-fams { display: flex; flex-wrap: wrap; gap: 5px; }
.au-fam {
  font-size: 11px; color: var(--ink-2); background: var(--surface);
  border: 1px solid var(--hairline); border-radius: 999px; padding: 2px 9px;
}
.au-fam.vide { font-style: italic; color: var(--muted); }

/* ---- Modale compte ---- */
.acct-card {
  max-width: 640px; width: min(640px, 94vw); max-height: 90vh; overflow-y: auto;
  text-align: left; padding: 22px 24px;
}
.acct-head { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; }
.acct-idrow { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.acct-idrow .field { flex: 1 1 200px; }

.acct-roles { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
.acct-roles-lbl {
  font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; color: var(--muted);
}
.acct-role-btns { display: flex; gap: 6px; flex-wrap: wrap; }
.acct-role {
  border: 1px solid var(--hairline); background: var(--surface-2); color: var(--ink-2);
  border-radius: 999px; padding: 5px 13px; font: inherit; font-size: 12.5px; cursor: pointer;
}
.acct-role:hover { color: var(--ink); border-color: var(--muted); }
.acct-role.on {
  background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600;
}
.acct-role.vider { color: var(--muted); }

.acct-perms-head {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: 10px; padding-top: 6px; border-top: 1px solid var(--hairline);
}
.acct-perms-head > span:first-child { font-weight: 650; font-size: 13px; }
.acct-permcount { font-size: 12px; color: var(--accent); font-weight: 600; }

.acct-perms { display: flex; flex-direction: column; gap: 12px; }
.acct-grp { border: 1px solid var(--hairline); border-radius: 10px; padding: 10px 12px; background: var(--surface-2); }
.acct-grp-head { display: flex; align-items: baseline; gap: 9px; margin-bottom: 8px; }
.acct-grp-all { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; }
.acct-grp-all input { width: auto; margin: 0; }
.acct-grp-t { font-weight: 650; font-size: 13px; }
.acct-grp-aide { font-size: 11.5px; color: var(--muted); }
.acct-grp-perms { display: flex; flex-wrap: wrap; gap: 7px; }
.acct-perm {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  font-size: 12.5px; color: var(--ink-2);
  border: 1px solid var(--hairline); background: var(--surface); border-radius: 8px; padding: 5px 10px;
}
.acct-perm input { width: auto; margin: 0; }
.acct-perm.on { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent);
  background: var(--accent-soft); font-weight: 500; }
/* Ecarts au role : un droit AJOUTE au-dela de ce que le role donne, ou RETIRE
   de ce qu'il donne. Sans ce reperage, un « Operateur » discretement modifie
   est indiscernable d'un Operateur standard — et c'est precisement ce qu'un
   administrateur a besoin de voir d'un coup d'oeil. Le trait pointille marque
   l'exception ; la couleur en donne le sens. */
.acct-perm.sup { border-style: dashed;
  border-color: color-mix(in srgb, var(--good) 55%, transparent);
  color: var(--good-text); background: color-mix(in srgb, var(--good) 8%, transparent); }
.acct-perm.ret { border-style: dashed; opacity: .8;
  border-color: color-mix(in srgb, var(--critical) 45%, transparent);
  color: var(--critical); text-decoration: line-through; }
.acct-actions { margin-top: 18px; padding-top: 4px; }
@media (max-width: 560px) {
  
}

/* ---- Admin ---------------------------------------------------------------
 * La largeur etait bridee a 1180 px du temps des tuiles carrees de comptes :
 * au-dela, la grille laissait un vide a droite. Les comptes sont passes en
 * LIGNES (voir .au-liste), qui s'etendent naturellement — la contrainte ne
 * servait plus qu'a rogner l'onglet sur les grands ecrans. On garde la limite
 * generale de <main> (1720 px), commune a toutes les vues. */
#adm-view-general .panel { padding: 18px 20px; }

/* Etat dans le coin haut-gauche, discret */

/* Actions : « Modifier » clair + deux icones sobres (activer/desactiver, supprimer) */

.au-ed {
  padding: 6px 16px; font-size: 12.5px; border-radius: 9px;
  background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 30%, transparent);
  color: var(--accent); font-weight: 600;
}
.au-ed:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
.au-icon {
  width: 34px; height: 34px; flex: none; border-radius: 9px;
  border: 1px solid var(--hairline); background: var(--surface); color: var(--ink-2);
  font-size: 15px; cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: border-color .12s, color .12s, background .12s;
}
.au-icon:hover:not(:disabled) { color: var(--ink); border-color: var(--muted); }
.au-icon.del:hover:not(:disabled) { color: var(--critical); border-color: var(--critical); }
.au-icon:disabled { opacity: .35; cursor: not-allowed; }

/* Reglage des alertes : compact, discret, sous les comptes */
.adm-alertes { background: var(--surface-2); box-shadow: none; }
.adm-alertes .panel-head h2 { font-size: 12.5px; }
.adm-alertes-row { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; }
.adm-alertes-row .field { flex: 1 1 280px; max-width: 440px; }
.adm-alertes-row .btn { flex: none; }

/* ---- Mode Regions de la carte ---------------------------------------------
 * Un ANNEAU autour du marqueur : la sante regionale s'ajoute a la lecture sans
 * remplacer la pastille de charge ni l'etat en ligne. Vert = scope, jaune =
 * scope admin MeshCore (non scope), gris = firmware muet. Le point violet
 * signale un relais qui recoit une region qu'il ne connait pas. */
.nmark { position: relative; }
.nrgn {
  position: absolute; inset: -4px; border-radius: 50%;
  border: 2.5px solid transparent; pointer-events: none;
}
.nrgn.ok  { border-color: var(--good); }
.nrgn.sans_scope { border-color: var(--warning); }
.nrgn.inconnu { border-color: var(--muted); opacity: .55; }
/* RELAIS TIERS : anneau TIRETE, et une couleur qui n'est ni celle du vert
   « configure » ni celle de l'avertissement. La difference doit se voir sans
   ouvrir la bulle — sinon on croirait avoir verifie une configuration qu'on ne
   peut pas lire. Un anneau tirete se dessine avec un fond conique : `border`
   ne fait pas de pointilles proprement a ce diametre. */
.nrgn.obs, .nrgn.obs_nu {
  border: 0;
  background:
    repeating-conic-gradient(from 0deg, #7dd3fc 0deg 12deg, transparent 12deg 24deg);
  -webkit-mask: radial-gradient(circle, transparent 0 46%, #000 46% 50%, transparent 50%);
          mask: radial-gradient(circle, transparent 0 46%, #000 46% 50%, transparent 50%);
  opacity: .9;
}
.nrgn.obs_nu {
  background:
    repeating-conic-gradient(from 0deg, var(--muted) 0deg 12deg, transparent 12deg 24deg);
  opacity: .7;
}
.nrgn-warn {
  position: absolute; top: -6px; right: -6px; width: 10px; height: 10px;
  border-radius: 50%; background: #8b5cf6; border: 2px solid var(--surface);
  pointer-events: none;
}

.mrl-note {
  margin-top: 6px; font-size: 10.5px; line-height: 1.45; color: var(--muted);
}
.rgn-obs { color: #7dd3fc; }

/* Legende du mode Regions (colonne laterale de la carte) */
.ma-rgn-leg {
  margin-top: 10px; padding: 10px 12px; border: 1px solid var(--hairline);
  border-radius: 10px; background: var(--surface-2);
  display: flex; flex-direction: column; gap: 6px;
}
.mrl-t { font-size: 11px; font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted); }
.mrl-l { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ink-2); }
.mrl-l b { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--ink); }
.mrl-dot { width: 10px; height: 10px; border-radius: 50%; flex: none;
  border: 2.5px solid transparent; }
.mrl-dot.ok   { border-color: var(--good); }
.mrl-dot.obs  { border-color: #7dd3fc; background: #7dd3fc; }
.mrl-dot.ss   { border-color: var(--warning); }
.mrl-dot.inc  { border-color: var(--muted); }
.mrl-dot.warn { background: #8b5cf6; border: none; width: 9px; height: 9px; }

/* Bloc regions de la bulle d'un relais */
.rgn-bloc { margin-top: 4px; font-weight: 500; }
.rgn-liste { color: var(--muted); }
.rgn-preuves { display: flex; flex-wrap: wrap; gap: 4px; align-items: baseline; }
.rgn-preuve {
  font-size: 10.5px; border: 1px solid var(--hairline); border-radius: 999px;
  padding: 1px 7px; background: var(--surface-2); color: var(--ink-2);
}
.rgn-preuve b { font-variant-numeric: tabular-nums; }
.rgn-preuve.brut { color: var(--muted); font-style: italic; }
.rgn-preuve.mal { color: #8b5cf6; border-color: color-mix(in srgb, #8b5cf6 45%, transparent); }
.rgn-alerte { color: #8b5cf6; font-weight: 600; margin-top: 3px; }

/* Etat regional dans l'en-tete du Voisinage */
.rgn-etat { font-weight: 600; border-radius: 6px; padding: 1px 7px; font-size: 11.5px; }
.rgn-etat.ok  { color: var(--good-text); background: color-mix(in srgb, var(--good) 12%, transparent); }
.rgn-etat.ss  { color: var(--warning); background: color-mix(in srgb, var(--warning) 12%, transparent); }
.rgn-etat.inc { color: var(--muted); background: var(--surface-2); }

/* ---- Thème « Propagation » (option, inspiré de RelayLens) ------------------
 * N'écrase PAS les thèmes clair/sombre : c'est un data-theme a part, active par
 * une coche. Vert neon sur fond quasi noir, comme relaylens.mesh.chaun14.fr.
 * On REDEFINIT tous les jetons pour que toute l'app suive, sans retoucher un
 * seul composant. */
:root[data-theme="propagation"] {
  /* Palette relevee sur RelayLens (assets/index-*.css) : fond bleu-ardoise
     tres sombre, texte gris-bleu NEUTRE, le vert reserve a l'accent. Ma
     premiere version teintait tout en vert, y compris le texte — d'ou un rendu
     « terminal des annees 80 » la ou l'original reste sobre et lisible. */
  color-scheme: dark;
  --page:        #071014;
  --surface:     #0c181d;
  --surface-2:   #101f25;
  --ink:         #dce8ea;
  --ink-2:       #a8bcc2;
  --muted:       #789097;
  --hairline:    #203138;
  --grid:        #1a2c33;
  --accent:      #62e6a7;
  --accent-soft: rgba(98, 230, 167, 0.12);
  --good:        #62e6a7;
  --good-text:   #7aeeb6;
  --warning:     #e8c05a;
  --critical:    #ff7a7a;
  /* series : l'accent, le bleu maison de RelayLens, puis des teintes froides */
  --s1: #62e6a7; --s2: #55b8ff; --s3: #b968ff;
  --s4: #e8c05a; --s5: #ff7a9c; --s6: #6fd9d0;
  --shadow: 0 1px 2px rgba(0,0,0,.55), 0 8px 28px rgba(0,0,0,.45);
  --ramp-1: 22%; --ramp-2: 38%; --ramp-3: 54%; --ramp-4: 70%; --ramp-5: 90%;
}
/* Quelques touches « néon » propres au thème, sans toucher aux autres */
:root[data-theme="propagation"] body { background:
  radial-gradient(1200px 600px at 70% -10%, rgba(98,230,167,.05), transparent 60%), var(--page); }
:root[data-theme="propagation"] .tab.active { text-shadow: 0 0 12px rgba(98,230,167,.5); }
:root[data-theme="propagation"] .brand-mark { text-shadow: 0 0 14px rgba(98,230,167,.6); }
:root[data-theme="propagation"] .stat-card .v { text-shadow: 0 0 16px rgba(98,230,167,.25); }

/* ---- Salons publics MeshCore ---------------------------------------------- */
.sln-split { display: grid; grid-template-columns: 270px 1fr 360px; gap: 14px;
  align-items: stretch; flex: 1 1 auto; min-height: 0; }
@media (max-width: 1100px) { .sln-split { grid-template-columns: 250px 1fr; }
  .sln-mid { display: none; } }
.sln-list { background: var(--surface); border: 1px solid var(--hairline); border-radius: 12px;
  box-shadow: var(--shadow); display: flex; flex-direction: column; overflow: hidden; height: 100%; }
.sln-head { display: flex; align-items: center; gap: 8px; padding: 12px 14px 8px; }
.sln-titre { margin: 0; font-size: 15px; font-weight: 650; flex: 1; }
.sln-note { font-size: 11px; color: var(--muted); padding: 0 14px 10px; line-height: 1.45;
  border-bottom: 1px solid var(--hairline); }
.sln-items { overflow-y: auto; padding: 6px; display: flex; flex-direction: column; gap: 2px; }
.sln-item { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border: 0;
  background: none; border-radius: 9px; cursor: pointer; text-align: left; width: 100%;
  color: var(--ink); font: inherit; }
.sln-item:hover { background: var(--surface-2); }
/* Oeil de masquage, en haut a droite de la carte de salon. Discret au repos —
   c'est une action rare — mais toujours atteignable au doigt : sur telephone il
   n'y a pas de survol, donc pas d'apparition au survol. */
.sln-item { position: relative; }
.sln-oeil {
  position: absolute; top: 5px; right: 6px; line-height: 1;
  padding: 3px 4px; border-radius: 6px; font-size: 12px;
  opacity: .35; cursor: pointer; user-select: none;
}
.sln-item:hover .sln-oeil { opacity: .8; }
.sln-oeil:hover { opacity: 1; background: var(--surface); }
/* Un salon masque reste lisible quand on demande a les voir, mais il doit se
   distinguer d'un coup d'oeil de ceux qui sont bien affiches. */
.sln-item.masque { opacity: .5; }
.sln-item.masque .sln-item-nom { text-decoration: line-through; }
.sln-plus {
  flex: none; width: 24px; height: 24px; padding: 0; line-height: 1;
  background: var(--surface-2); color: var(--ink-2); font-size: 14px;
  border: 1px solid var(--hairline); border-radius: 7px; cursor: pointer;
}
.sln-plus:hover { color: var(--accent); border-color: var(--accent); }
.sln-ajout {
  display: flex; flex-direction: column; gap: 6px; padding: 8px 12px 10px;
  border-bottom: 1px solid var(--hairline); background: var(--surface-2);
}
.sln-ajout-l1 { display: flex; gap: 6px; }
.sln-ajout input {
  flex: 1; min-width: 0; padding: 6px 9px; font: inherit; font-size: 12.5px;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--hairline); border-radius: 7px;
}
.sln-ajout input:focus { outline: none; border-color: var(--accent); }
.sln-ajout .btn { flex: none; padding: 6px 12px; font-size: 12.5px; min-height: 0; }
.sln-ajout-aide { font-size: 10.5px; line-height: 1.45; }
.sln-masques-barre {
  display: flex; align-items: center; gap: 8px; padding: 6px 12px;
  border-bottom: 1px solid var(--hairline); font-size: 11px; color: var(--muted);
}
.sln-masques-barre span { flex: 1; }
.sln-masques-barre .btn { padding: 3px 9px; font-size: 11px; min-height: 0; }
.sln-item.on { background: var(--accent-soft); }
.sln-hash { width: 26px; height: 26px; flex: none; border-radius: 7px; background: var(--surface-2);
  display: flex; align-items: center; justify-content: center; color: var(--accent); font-weight: 700; }
.sln-item.on .sln-hash { background: var(--accent); color: #fff; }
.sln-item-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.sln-item-nom { font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sln-item-sub { font-size: 11px; color: var(--muted); }
.sln-item-when { font-size: 10.5px; color: var(--muted); white-space: nowrap; }
.sln-main { background: var(--surface); border: 1px solid var(--hairline); border-radius: 12px;
  box-shadow: var(--shadow); display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.sln-main-head { padding: 12px 16px; border-bottom: 1px solid var(--hairline); }
.sln-mh-nom { font-size: 16px; font-weight: 650; }
/* Compositeur : troisieme enfant de .sln-main, en « flex: none ». La coquille
   est deja une colonne flex avec .sln-msgs en flex:1 — l'inserer ne fait que
   raccourcir le defilement, sur grand ecran comme sur telephone. */
.sln-envoi {
  flex: none; padding: 10px 16px; border-bottom: 1px solid var(--hairline);
  background: var(--surface-2);
}
.sln-envoi-ligne { display: flex; gap: 8px; align-items: center; }
.sln-envoi-ligne input {
  flex: 1; min-width: 0; padding: 9px 12px; border-radius: 9px;
  border: 1px solid var(--hairline); background: var(--surface); color: var(--ink);
  font: inherit; font-size: 14px;
}
.sln-envoi-ligne input:focus { outline: none; border-color: var(--accent); }
/* Pied du compositeur : trois informations sur une seule rangee, c'etait une
   de trop. Le nom de l'expediteur se faisait ecraser a zero et le compteur
   d'octets se retrouvait tronque. Elles passent a la ligne quand il le faut —
   chacune avec une base minimale, sans quoi flex les comprime jusqu'a les
   faire disparaitre. */
.sln-envoi-pied {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px;
  margin-top: 6px; font-size: 11px; color: var(--muted);
}
.sln-envoi-gw {
  display: flex; align-items: center; gap: 6px;
  flex: 1 1 260px; min-width: 0;
}
.sln-envoi-gw > span { flex: none; }
.sln-envoi-gw select {
  flex: 1 1 auto; min-width: 0;
  background: var(--surface); color: var(--ink); font: inherit; font-size: 11px;
  border: 1px solid var(--hairline); border-radius: 7px; padding: 3px 6px;
}
.sln-envoi-nom {
  flex: 1 1 150px; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Toujours a droite de sa rangee, et jamais rogne : c'est la seule information
   qui empeche d'envoyer un message perdu d'avance. */
.sln-envoi-octets { flex: none; margin-left: auto; font-variant-numeric: tabular-nums; }
.sln-envoi-octets.trop { color: var(--critical); font-weight: 700; }
.sln-envoi-alerte { color: var(--warning); }
.sln-msgs { flex: 1; overflow-y: auto; padding: 12px 16px; }
.sln-jour {
  display: flex; align-items: center; gap: 10px; margin: 16px 0 10px;
  font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: .08em;
}
.sln-jour::before, .sln-jour::after {
  content: ""; flex: 1; height: 1px; background: var(--hairline);
}
/* Un message = une CARTE ENCADREE (facon RelayLens). Le cadre fait tout le
   travail de lecture : on distingue les messages sans avoir a suivre
   l'indentation ou la couleur de l'expediteur. */
.sln-card {
  border: 1px solid var(--hairline); border-radius: 10px;
  background: var(--surface-2); padding: 10px 12px; margin-bottom: 8px;
  transition: border-color .12s;
}
.sln-card:hover { border-color: color-mix(in srgb, var(--accent) 35%, transparent); }
.sln-card-h { display: flex; align-items: baseline; gap: 10px; margin-bottom: 6px; }
.sln-card-exp {
  font-weight: 700; font-size: 12px; color: var(--accent);
  letter-spacing: .02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sln-card-ts {
  margin-left: auto; flex: none; font-size: 10.5px; color: var(--muted);
  font-family: var(--mono); font-variant-numeric: tabular-nums;
}
.sln-card-txt {
  font-size: 13.5px; line-height: 1.5; color: var(--ink);
  overflow-wrap: anywhere; white-space: pre-wrap;
}
.sln-card-f { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.sln-tag {
  font-size: 10px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase;
  color: var(--accent); background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
  border-radius: 5px; padding: 2px 7px; white-space: nowrap;
}
.sln-tag.alt { color: var(--ink-2); background: var(--surface);
  border-color: var(--hairline); text-transform: none; letter-spacing: 0; }

/* En-tete du salon : bandeau discret, nom en grand */
.sln-mh-eyebrow {
  font-size: 10px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase;
  color: var(--accent); margin-bottom: 2px;
}
.sln-mh-sub { font-size: 11.5px; color: var(--muted); }
@media (max-width: 720px) { .sln-split { grid-template-columns: 1fr; height: auto; } }

/* ---- Thème Propagation : esthétique générale (salle de contrôle néon) ------
 * Tout est scopé sous [data-theme="propagation"] : clair/sombre sont intacts.
 * On ne se contente pas de la palette — on ajoute les halos, les bordures qui
 * luisent, la carte sombre et la typo technique qui font le look RelayLens. */
:root[data-theme="propagation"] {
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
}
:root[data-theme="propagation"] .topbar {
  border-bottom: 1px solid rgba(98,230,167,.16);
  box-shadow: 0 1px 24px rgba(98,230,167,.05);
  background: linear-gradient(180deg, rgba(98,230,167,.03), transparent);
}
:root[data-theme="propagation"] .brand { letter-spacing: .01em; }
:root[data-theme="propagation"] .brand-thin { color: var(--accent); opacity: .75;
  text-transform: uppercase; letter-spacing: .12em; font-size: 11px; }

/* Onglets : soulignement néon sous l'actif */
:root[data-theme="propagation"] .view-tabs .tab.active {
  color: var(--accent); background: rgba(98,230,167,.08);
  box-shadow: inset 0 -2px 0 var(--accent);
}
:root[data-theme="propagation"] .tab:hover { color: var(--accent); }

/* Tuiles de stats : gros chiffres qui luisent, labels espacés en capitales */
:root[data-theme="propagation"] .stat-card,
:root[data-theme="propagation"] .panel,
:root[data-theme="propagation"] 
:root[data-theme="propagation"] .stat-card .v {
  color: var(--accent); font-family: var(--mono); font-variant-numeric: tabular-nums;
  text-shadow: 0 0 20px rgba(98,230,167,.35);
}
:root[data-theme="propagation"] .stat-card .k {
  text-transform: uppercase; letter-spacing: .14em; font-size: 10.5px;
}

/* Boutons primaires : verre vert lumineux */
:root[data-theme="propagation"] .btn.primary {
  background: rgba(98,230,167,.16); border-color: var(--accent); color: #d7f7e6;
  box-shadow: 0 0 18px rgba(98,230,167,.22); text-shadow: 0 0 8px rgba(98,230,167,.4);
}
:root[data-theme="propagation"] .btn.primary:hover { background: rgba(98,230,167,.28); }

/* Champs : fond très sombre, focus qui glow */
:root[data-theme="propagation"] input,
:root[data-theme="propagation"] select {
  background: #071014; border-color: rgba(98,230,167,.2); color: var(--ink);
}
:root[data-theme="propagation"] input:focus,
:root[data-theme="propagation"] select:focus {
  border-color: var(--accent); box-shadow: 0 0 0 3px rgba(98,230,167,.14);
}

/* En-têtes de tableaux : capitales espacées, ligne néon */
:root[data-theme="propagation"] thead th {
  text-transform: uppercase; letter-spacing: .08em; font-size: 10.5px;
  color: var(--accent); border-bottom-color: rgba(98,230,167,.22);
}

/* Marqueurs de la carte : halo vert (echo des cercles RelayLens) */
:root[data-theme="propagation"] .nmark.gw {
  background: #0c181d; box-shadow: 0 0 14px rgba(98,230,167,.55);
  border: 1px solid rgba(98,230,167,.5);
}
:root[data-theme="propagation"] .nmark.gw svg { filter: drop-shadow(0 0 4px rgba(98,230,167,.7)); }

/* Carte sombre : filtre les tuiles OSM claires en fond quasi noir teinté */
:root[data-theme="propagation"] .leaflet-tile {
  filter: grayscale(.85) invert(1) brightness(.72) contrast(1.05) hue-rotate(120deg) saturate(.55);
}
:root[data-theme="propagation"] .leaflet-container { background: #071014; }

/* Barres de défilement fines et vertes */
:root[data-theme="propagation"] ::-webkit-scrollbar { width: 10px; height: 10px; }
:root[data-theme="propagation"] ::-webkit-scrollbar-thumb {
  background: rgba(98,230,167,.22); border-radius: 6px; }
:root[data-theme="propagation"] ::-webkit-scrollbar-thumb:hover { background: rgba(98,230,167,.4); }
:root[data-theme="propagation"] ::-webkit-scrollbar-track { background: transparent; }

/* ---- Salons : colonne carte (propagation) + vues -------------------------- */
.sln-mid { height: 100%; }
.sln-map-wrap { position: relative; height: 100%; border: 1px solid var(--hairline);
  border-radius: 12px; overflow: hidden; box-shadow: var(--shadow); background: var(--surface-2); }
#sln-map { position: absolute; inset: 0; }
.sln-map-vide { position: absolute; inset: 0; display: flex; align-items: center;
  justify-content: center; color: var(--muted); font-size: 12.5px; pointer-events: none;
  background: var(--surface-2); z-index: 500; text-align: center; padding: 20px; }
/* Pastille de propagation : disque sombre, anneau vert, compte au centre */
.sln-cmark {
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; background: #0c181d; color: #d7f7e6; font-weight: 700;
  font-size: 12px; font-variant-numeric: tabular-nums;
  border: 2px solid var(--accent); box-shadow: 0 0 12px rgba(98,230,167,.5);
}

/* Thème Propagation : les cadres des messages luisent comme sur RelayLens */
:root[data-theme="propagation"] .sln-card {
  background: linear-gradient(180deg, rgba(98,230,167,.03), transparent 60%), #0c181d;
  border-color: rgba(98,230,167,.2);
}
:root[data-theme="propagation"] .sln-card:hover {
  border-color: rgba(98,230,167,.45); box-shadow: 0 0 18px rgba(98,230,167,.08);
}
:root[data-theme="propagation"] .sln-card-exp { text-shadow: 0 0 10px rgba(98,230,167,.35); }
:root[data-theme="propagation"] .sln-item.on { box-shadow: inset 2px 0 0 var(--accent); }

/* ---- Thème Propagation : les CADRES partout dans le site ------------------
 * L'esthetique RelayLens ne tient pas qu'a la couleur : c'est le CADRE qui
 * structure la lecture. Chaque unite de contenu — carte de passerelle, ligne de
 * journal, episode d'incident, paire radio, resultat de recherche — devient un
 * bloc encadre au liseré vert, au lieu d'une ligne posee sur un aplat. Tout est
 * scopé sous le thème : clair et sombre restent inchangés. */
:root[data-theme="propagation"] .gw-card,
:root[data-theme="propagation"] .up-card,
:root[data-theme="propagation"] .inc-node-card,
:root[data-theme="propagation"] .inc-ep-body,
:root[data-theme="propagation"] .rl-pair,
:root[data-theme="propagation"] .cli-gw-card,
:root[data-theme="propagation"] .health-list,
:root[data-theme="propagation"] .filters,
:root[data-theme="propagation"] .jr-frise,
:root[data-theme="propagation"] .sln-map-wrap,
:root[data-theme="propagation"] .sln-item.on {
  background: linear-gradient(180deg, rgba(98,230,167,.03), transparent 60%), var(--surface-2);
  border: 1px solid rgba(98,230,167,.2);
  border-radius: 10px;
}
:root[data-theme="propagation"] .gw-card:hover,
:root[data-theme="propagation"] .up-card:hover,
:root[data-theme="propagation"] .inc-node-card:hover,
:root[data-theme="propagation"] .rl-pair:hover,
:root[data-theme="propagation"] .cli-gw-card:hover {
  border-color: rgba(98,230,167,.45);
  box-shadow: 0 0 18px rgba(98,230,167,.10);
}

/* Entonnoir « Installer un firmware » : l'etat CHOISI doit rester visible en
   theme Propagation. La regle de theme ci-dessus (0,3,0) bat .cli-gw-card.active
   (0,2,0) sur background ET sur border, qui est un raccourci. Sans cette regle
   on ne voit pas ce qu'on vient de selectionner — redhibitoire dans un
   entonnoir, ou tout l'interet est de savoir ou l'on en est. */
:root[data-theme="propagation"] .cli-gw-card.active {
  background: var(--accent-soft);
  border-color: var(--accent);
  box-shadow: 0 0 14px rgba(98,230,167,.18);
}

/* Lignes de journal : chacune dans son cadre, comme les cartes de message */
:root[data-theme="propagation"] .jr-ligne {
  border: 1px solid rgba(98,230,167,.14); border-radius: 9px;
  background: rgba(10,16,20,.6); margin-bottom: 5px; padding-right: 10px;
}
:root[data-theme="propagation"] .jr-ligne:hover {
  border-color: rgba(98,230,167,.4); background: rgba(98,230,167,.05);
}

/* Lignes de la liste de sante : encadrees plutot que separees par un trait */
:root[data-theme="propagation"] .hl-row {
  border: 1px solid rgba(98,230,167,.12); border-radius: 8px;
  margin: 4px 6px; background: rgba(10,16,20,.5);
}
:root[data-theme="propagation"] .hl-row:hover { border-color: rgba(98,230,167,.35); }

/* Tableaux : lignes espacees et encadrees, en cartes plutot qu'en grille */
:root[data-theme="propagation"] tbody tr {
  background: rgba(10,16,20,.45);
}
:root[data-theme="propagation"] tbody tr:hover { background: rgba(98,230,167,.06); }
:root[data-theme="propagation"] tbody td { border-bottom: 1px solid rgba(98,230,167,.10); }

/* Sous-onglets et chips : contour net */
:root[data-theme="propagation"] .tab { border: 1px solid transparent; border-radius: 8px; }
:root[data-theme="propagation"] .tab.active { border-color: rgba(98,230,167,.35); }
:root[data-theme="propagation"] .jr-vue,
:root[data-theme="propagation"] .jr-chip,
:root[data-theme="propagation"] .acct-role,
:root[data-theme="propagation"] .btn {
  border-color: rgba(98,230,167,.22);
}
:root[data-theme="propagation"] .btn:hover { border-color: rgba(98,230,167,.5); color: var(--accent); }

/* Etiquettes de section : petites capitales espacees, vertes (signature RelayLens) */
:root[data-theme="propagation"] .panel-head h2,
:root[data-theme="propagation"] .field label,
:root[data-theme="propagation"] .jr-jour,
:root[data-theme="propagation"] .sln-jour {
  text-transform: uppercase; letter-spacing: .1em;
}
:root[data-theme="propagation"] .panel-head h2 { color: var(--accent); font-size: 12px; }

/* ---- Thème Propagation : la MISE EN PAGE, pas seulement les couleurs -------
 * Ce qui fait le look RelayLens autant que le vert : un BANDEAU de mesures
 * d'un seul tenant (au lieu de tuiles separees), des etiquettes minuscules en
 * capitales tres espacees, et de grands nombres en chasse fixe. */
:root[data-theme="propagation"] .stats-grid {
  gap: 0;
  border: 1px solid var(--hairline); border-radius: 12px; overflow: hidden;
  background: var(--surface);
}
:root[data-theme="propagation"] .stats-grid .stat-card {
  border: 0; border-right: 1px solid var(--hairline); border-radius: 0;
  box-shadow: none; background: transparent;
  display: flex; align-items: baseline; gap: 14px; padding: 16px 20px;
}
:root[data-theme="propagation"] .stats-grid .stat-card:last-child { border-right: 0; }
:root[data-theme="propagation"] .stats-grid .stat-card .k {
  font-size: 10px; letter-spacing: .16em; color: var(--muted); margin: 0;
}
:root[data-theme="propagation"] .stats-grid .stat-card .v {
  margin-left: auto; font-size: 26px; line-height: 1;
}
:root[data-theme="propagation"] .stats-grid .stat-card .sub { display: none; }

/* Etiquettes de section facon RelayLens : minuscules, tres espacees, vertes */
:root[data-theme="propagation"] .panel-sub,
:root[data-theme="propagation"] .sln-item-sub,
:root[data-theme="propagation"] 
/* Identifiants techniques en chasse fixe (cles, gw_id, versions) */
:root[data-theme="propagation"] code,
:root[data-theme="propagation"] .gw-id-sub,
:root[data-theme="propagation"] .au-badge,
:root[data-theme="propagation"] .sln-card-ts {
  font-family: var(--mono);
}
/* Titres de vue : le nom en grand, precede d'un filet vert */
:root[data-theme="propagation"] .sln-mh-nom,
:root[data-theme="propagation"] .jr-title {
  font-size: 19px; letter-spacing: -.01em;
}
/* Polices proches de RelayLens SI elles sont installees localement — aucune
   requete reseau ajoutee : ce monitoring doit rester utilisable sur HAMNET et
   hors ligne, une police bloquante serait un mauvais echange. */
:root[data-theme="propagation"] {
  font-family: Manrope, "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "DM Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

/* ---- Trames : rendre le tableau LISIBLE ------------------------------------
 * Dix colonnes toutes en « nowrap » forcaient un defilement horizontal, et la
 * seule colonne qu'on vient vraiment lire — le message — etait plafonnee a
 * 340 px puis tronquee. On fixe la largeur des colonnes TECHNIQUES (elles ont
 * un contenu de taille connue) et on donne tout le reste au message. */
#view-frames table { table-layout: fixed; }
#view-frames thead th:nth-child(1) { width: 98px; }   /* Heure « 21:39:12.34 » entiere */
#view-frames thead th:nth-child(2) { width: 74px; }   /* Canal   */
#view-frames thead th:nth-child(3) { width: 132px; }  /* GW      */
#view-frames thead th:nth-child(4) { width: 96px; }   /* Type    */
#view-frames thead th:nth-child(5) { width: 62px; }   /* Route   */
#view-frames thead th:nth-child(6) { width: 78px; }   /* RSSI    */
#view-frames thead th:nth-child(7) { width: 58px; }   /* SNR     */
#view-frames thead th:nth-child(8) { width: 54px; }   /* Len     */
#view-frames thead th:nth-child(9) { width: auto; }   /* Message : tout le reste */
#view-frames thead th:nth-child(10) { width: 104px; } /* Hex     */

/* La hauteur de ligne SUIT le contenu (28.08) : le plafond de deux lignes
   (-webkit-line-clamp) rognait les resumes de l'affichage simplifie et le
   detail des ecoutes une fois deplie. La colonne message est celle qu'on
   vient lire — elle prend la place qu'il lui faut. */
#view-frames td.msg {
  max-width: none; white-space: normal; overflow: visible;
  line-height: 1.4; word-break: break-word;
}
/* Les colonnes techniques restent sur une ligne et s'effacent proprement. */
#view-frames tbody td { overflow: hidden; text-overflow: ellipsis; }
#view-frames td.hex {
  font-family: var(--mono); font-size: 11px; color: var(--muted);
  white-space: nowrap;
}
/* Alternance discrete : sur une ligne large, l'oeil perd le fil entre la
   premiere et la derniere colonne. */
#view-frames tbody tr:nth-child(even) { background: color-mix(in srgb, var(--ink) 3%, transparent); }
#view-frames tbody tr:hover { background: var(--accent-soft); }

/* ===========================================================================
 * DEFILEMENT : une regle, une seule
 * ---------------------------------------------------------------------------
 * Apres plusieurs correctifs empiles qui se contredisaient (une vue qui defile
 * ET une region interne qui defile = deux barres imbriquees ; une vue en
 * « overflow: hidden » sans region defilante = contenu inatteignable), on
 * revient a une regle unique et verifiable :
 *
 *   1. le corps de page ne defile JAMAIS (coquille flex) ;
 *   2. CHAQUE VUE est son propre et unique ascenseur ;
 *   3. deux exceptions, batie pour cela : la carte et les salons, dont les
 *      colonnes defilent separement (elles sont cote a cote, jamais imbriquees) ;
 *   4. aucune liste interne ne defile a l'interieur d'une vue defilante — on
 *      leve donc leurs plafonds « max-height ».
 *
 * Consequence assumee : sur un grand ecran, une vue au contenu court ne descend
 * pas jusqu'en bas. C'est honnete — mieux vaut un blanc qu'un cadre etire sur
 * du vide, ou qu'un contenu rogne.
 * ========================================================================== */
#overview, #view-admin, #view-usb, #view-companion, #view-maint,
#view-cli, #view-journal, #view-frames, #view-tele, #view-synthese {
  display: block; overflow-y: auto; overflow-x: hidden; padding: 18px 0 24px;
}
/* Les deux vues a colonnes : elles ne defilent pas, leurs colonnes si. */
#view-map { overflow: hidden; padding: 0; display: flex; flex-direction: column; }
#view-salons { overflow: hidden; padding: 14px 0; display: flex; flex-direction: column; }

/* Les sous-vues suivent leur vue : aucune ne defile pour son compte. */
#tele-view-nodes, #tele-view-radio, #tele-view-load,
#tele-view-prop, #tele-view-vois, #tele-view-flood,
#dash-view-gw, #dash-view-inc, #dash-view-radio,
#dash-view-traffic, #dash-view-broker {
  overflow: visible; min-height: 0;
}
/* Plafonds leves : ces listes vivent dans une vue qui defile deja. */
#overview .gw-list, #overview .uptime-list, #overview .alerts-list,
#view-companion .cmp-list, #tele-view-vois .vois-scroll, #view-frames .table-wrap {
  max-height: none; overflow: visible;
}
/* La console CLI et celle de la maintenance gardent LEUR defilement : ce sont
   des journaux qu'on relit, et ils ont une hauteur bornee — pas d'imbrication
   avec la vue tant qu'ils ne s'etirent pas. */
#view-cli .cli-console, #view-maint .cli-console {
  max-height: 46vh; overflow-y: auto;
}

/* ---- Maintenance : tenir dans la fenetre sans rien rogner ------------------
 * Les sous-onglets ont deja retire une tache sur deux. Ce qui reste de trop,
 * ce sont des hauteurs prises par des elements SECONDAIRES : la console
 * reservait a elle seule jusqu'a 48 % de la fenetre (min 200 px), le texte
 * d'introduction trois lignes, et chaque titre d'etape 36 px de marges.
 * On les resserre plutot que de figer la vue : la vue garde son unique
 * ascenseur, il n'a simplement plus de raison d'apparaitre sur un ecran normal. */
#view-maint .cli-console { min-height: 130px; max-height: 30vh; }
#view-maint .usb-intro {
  font-size: 12px; line-height: 1.45; color: var(--muted); margin: 0 0 10px;
}
#view-maint .cli-step-h { padding-top: 9px; margin: 9px 0 7px; }
#view-maint .maint-block { padding: 12px 14px; margin-bottom: 10px; }
#view-maint .maint-hint { margin: 0 0 8px; }
#view-maint .maint-grid { gap: 8px; margin-bottom: 9px; }
#view-maint .cli-gw-cards { gap: 7px; }
/* Le titre « Console » n'a plus besoin de son filet separateur : la console
   qui suit est deja un bloc visuellement distinct. */
#view-maint .maint-console-h { border-top: 0; padding-top: 0; margin-top: 6px; }

/* ---- Telemetrie : des graphes en GRILLE, pas en pile ----------------------
 * Une courbe par relais et par paire de relais, empilees les unes sous les
 * autres, cela fait vite deux ou trois hauteurs d'ecran a faire defiler pour
 * comparer deux liaisons — alors que comparer est justement ce qu'on vient
 * faire. En grille, quatre a six courbes tiennent dans un ecran et se lisent
 * cote a cote. Les graphes sont des SVG etires : les reduire ne coute aucune
 * lisibilite horizontale, seulement de l'amplitude verticale. */
.tele-charts {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  align-content: start;
}
.tele-charts .tele-chart-box { margin: 0; }
#view-tele .tele-svg { height: 88px; }
#view-tele .tele-chart-box { padding: 8px 10px; }
#view-tele .tele-chart-box h3 { margin: 0 0 5px; font-size: 12px; }
#view-tele .tele-meta { margin-top: 2px; }
/* La vue Charge trace une courbe par relais : meme traitement. */
#tele-view-load .load-chart-wrap { margin-bottom: 10px; }
#view-tele .load-svg { height: 210px; }

/* Vue Charge : trois mesures cote a cote plutot qu'empilees. Sur un ecran
   large elles tiennent en une seule hauteur — et se COMPARENT, ce qui est tout
   l'interet (une charge qui monte pendant que le bruit monte aussi ne raconte
   pas la meme chose qu'une charge seule). */
.load-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  align-items: start;
}
.load-bloc { min-width: 0; }
.load-bloc .tele-radio-head { margin-top: 0; }

/* ---- Journal : les commandes restent, seul le flux defile -----------------
 * « Toute la page scrolle » : la vue etait l'ascenseur, donc chercher un
 * evenement faisait disparaitre en haut les filtres et la frise dont on venait
 * de se servir. Il fallait remonter pour changer un critere.
 * On rend le flux defilant et on fige le reste — ce qui n'etait pas tenable
 * tant que l'empilement de commandes depassait la fenetre. Trois economies l'ont
 * rendu possible : la bande de statistiques (supprimee, elle repetait les
 * boutons de vue rapide), une frise plus basse, et des blocs de filtres
 * resserres. */
#view-journal { display: flex; flex-direction: column; overflow: hidden; }
#view-journal > * { flex: none; }
/* Huit lignes par page : le flux n'a plus besoin de defiler. On garde « auto »
   comme filet — une ligne annotee ou tres longue ne doit pas deborder. */
#view-journal > .jr-flux {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 4px;
}
/* La pagination reste collee en bas, toujours atteignable. */
#view-journal > .pager { flex: none; padding-top: 4px; }
/* Commandes resserrees : c'est ce qui libere la place du flux. */
#view-journal .jr-head { margin-bottom: 8px; }
#view-journal .filters { padding: 9px 12px; margin-bottom: 8px; gap: 8px; }
#view-journal .jr-vues { margin-bottom: 8px; }
#view-journal .jr-frise { padding: 8px 12px 5px; margin-bottom: 8px; }
#view-journal .jr-dossier { padding: 10px 12px; margin-bottom: 8px; }
#view-journal .pager { margin-top: 8px; }

/* GARDE-FOU : sur une fenetre basse (ou avec beaucoup de filtres ouverts), les
   commandes peuvent depasser a elles seules. On rend alors la vue defilante :
   mieux vaut retrouver le defilement d'ensemble que voir le bas rogne. */
@media (max-height: 760px) {
  #view-journal { display: block; overflow-y: auto; }
  #view-journal > .jr-flux { overflow: visible; min-height: 0; }
}

/* ---- Telemetrie : sous-onglets figes, sous-vue defilante -------------------
 * Meme principe que le journal : chercher dans une courbe ne doit pas faire
 * disparaitre en haut les sous-onglets et le selecteur de periode dont on vient
 * de se servir. Le panneau prend la hauteur, l'en-tete reste, et SEULE la
 * sous-vue active defile — un ascenseur, pas deux. */
#view-tele { display: flex; flex-direction: column; overflow: hidden; }
#view-tele > .panel {
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
}
#view-tele > .panel > .panel-head { flex: none; }
#view-tele > .panel > div[id^="tele-view-"] {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 4px;
}
/* La barre de periode de chaque sous-vue reste en haut de son defilement. */
#view-tele .tele-chartbar {
  position: sticky; top: 0; z-index: 3; background: var(--surface);
  padding: 2px 0 8px; margin-top: 0;
}
#view-tele .tele-radio-head { margin-top: 12px; }

@media (max-height: 760px) {
  #view-tele { display: block; overflow-y: auto; }
  #view-tele > .panel { display: block; }
  #view-tele > .panel > div[id^="tele-view-"] { overflow: visible; }
}

/* Les notes de bas de vue sont des rappels, pas du contenu : discretes. */
#view-tele .maint-hint, #view-tele .flood-note {
  font-size: 11px; line-height: 1.4; margin: 8px 0 0;
}

/* ---- Carte : la vraie cause des dalles manquantes -------------------------
 * Une regle tardive imposait « .map-wrap { height: 62vh } ». Leaflet etait donc
 * dimensionne a 62 % de la fenetre, quelle que soit la place que la coquille
 * lui laissait reellement : trop petit, il ne demandait pas les dalles du bas ;
 * trop grand, il debordait. C'etait une hauteur magique de plus, en « vh » —
 * mon balayage precedent ne cherchait que les « calc(100vh - N) » et l'a ratee.
 * Le conteneur remplit desormais sa cellule de grille, et #map le remplit a son
 * tour (position absolue, inset 0). */
.map-split > .map-wrap { height: auto; min-height: 0; align-self: stretch; }

/* Badge « maintenance active » : l'administrateur ne voit PAS la banniere de
   maintenance — il doit pouvoir continuer a travailler pendant que les autres
   comptes sont bloques. Sans repere, rien ne lui rappelle que le site est ferme
   aux autres : on l'affiche donc en permanence dans la barre du haut, et un
   clic mene au reglage. */
.maint-badge {
  display: inline-flex; align-items: center; gap: 6px;
  background: color-mix(in srgb, var(--warning) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning) 55%, transparent);
  color: var(--warning); font: inherit; font-size: 11.5px; font-weight: 650;
  border-radius: 999px; padding: 4px 11px; cursor: pointer; white-space: nowrap;
  animation: maint-pulse 2.4s ease-in-out infinite;
}
.maint-badge:hover { background: color-mix(in srgb, var(--warning) 28%, transparent); }
.maint-badge[hidden] { display: none; }
@keyframes maint-pulse { 0%,100% { opacity: 1; } 50% { opacity: .62; } }
@media (max-width: 1200px) { .maint-badge { font-size: 0; gap: 0; padding: 6px 9px; } }

/* Sortie discrete de la page de maintenance : un administrateur doit pouvoir
   atteindre l'ecran de connexion, sinon activer la maintenance enfermerait tout
   le monde dehors — lui compris, puisqu'on ne peut pas savoir qu'un visiteur
   est admin avant qu'il se connecte. */
.mo-admin {
  display: block; margin: 10px auto 0; background: none; border: 0;
  color: inherit; opacity: .45; font: inherit; font-size: 11.5px;
  text-decoration: underline; cursor: pointer;
}
.mo-admin:hover { opacity: .9; }

/* ---- Tableau de bord > Trafic : des graphes qui respirent ------------------
 * Les deux panneaux etaient tasses : barres de 84 px de haut pour le trafic
 * horaire, barres de 10 px d'epaisseur pour la repartition par type. La
 * sous-vue s'etire deja sur toute la hauteur disponible — c'est cette place,
 * jusque-la perdue, qu'on leur donne. Aucun defilement ajoute : les deux
 * panneaux grandissent AVEC leur conteneur, ils ne le debordent jamais. */
#overview #dash-view-traffic > .panels {
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
  align-items: stretch;
}
#overview #dash-view-traffic .panel { min-height: 0; }

/* Trafic horaire : la zone de barres prend toute la hauteur restante du
   panneau au lieu d'un plafond fixe. */
#overview #dash-view-traffic .traffic-panel { margin-bottom: 0; }
#overview #dash-view-traffic #traffic-chart {
  flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
}
#overview #dash-view-traffic .traffic-bars {
  flex: 1 1 auto; height: auto; min-height: 120px; gap: 3px;
}
#overview #dash-view-traffic .traffic-axis { flex: none; }

/* Repartition par type : barres plus epaisses et lignes plus aerees, la liste
   occupe la hauteur du panneau plutot que de se tasser en haut. */
#overview #dash-view-traffic .type-bars {
  flex: 1 1 auto; min-height: 0; justify-content: space-evenly; gap: 6px;
}
#overview #dash-view-traffic .type-bar { height: 14px; border-radius: 7px; }
#overview #dash-view-traffic .type-row {
  grid-template-columns: 110px 1fr 58px; font-size: 13px;
}
/* Le selecteur de thème occupe une demi-ligne : l'explication tient a cote. */
.acct-theme-aide { justify-content: flex-end; font-size: 11.5px; line-height: 1.4; }

/* Etat des etapes de l'assistant RAK : le compte rendu de l'action en cours. */
.wiz-dl { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.wiz-tel {
  font-size: 12px; color: var(--accent); cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
.wiz-tel:hover { color: var(--ink); }
.wiz-etat { margin-top: 10px; font-size: 12.5px; line-height: 1.55; }
.wiz-etat:empty { display: none; }

/* ---- Page « Mon compte » --------------------------------------------------
   Un en-tete « hero » (avatar, indicatif, nom d'emission, theme) puis une
   grille de cartes D'EGALE HAUTEUR : la premiere version alignait trois cartes
   de tailles disparates sur toute la largeur de l'ecran — tassee a gauche,
   vide a droite. Largeur bornee : au-dela, une page de reglages ne gagne rien. */
#view-compte { overflow-y: auto; padding: 20px 0 32px; }
.cpt-cadre { max-width: 1120px; margin: 0 auto; }
.cpt-hero {
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  padding: 22px 26px; margin-bottom: 16px;
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--accent) 9%, var(--surface)),
    var(--surface) 55%);
  border: 1px solid var(--hairline); border-radius: 16px; box-shadow: var(--shadow);
}
.cpt-avatar {
  width: 62px; height: 62px; flex: none; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; font-weight: 800; letter-spacing: .02em;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
  border: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);
}
.cpt-hero-txt { flex: 1 1 260px; min-width: 0; }
.cpt-nom { font-size: 24px; font-weight: 800; letter-spacing: -.01em; }
.cpt-emet { font-size: 13.5px; margin-top: 3px; color: var(--ink-2); }
.cpt-emet b { color: var(--accent); font-weight: 700; }
.cpt-emet b.manque { color: var(--warning); }
.cpt-emet-note { font-size: 11px; margin-top: 2px; line-height: 1.4; }
.cpt-hero-droite { flex: none; }
.cpt-theme-l {
  display: flex; flex-direction: column; gap: 5px;
  font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted);
}
.cpt-theme-l select {
  background: var(--surface); color: var(--ink); font: inherit; font-size: 13px;
  border: 1px solid var(--hairline); border-radius: 9px; padding: 8px 12px;
}
.cpt-grille {
  display: grid; grid-template-columns: 1fr 1fr; gap: 14px;
  align-items: stretch;             /* cartes d'EGALE hauteur par rangee */
}
.cpt-carte { margin: 0; padding: 18px 20px; border-radius: 14px; }
.cpt-large { grid-column: 1 / -1; }
.cpt-note { display: block; font-size: 11px; font-weight: 400; line-height: 1.45; margin-top: 2px; }
.cpt-ligne {
  display: flex; align-items: center; justify-content: space-between; gap: 14px;
  padding: 12px 0; border-bottom: 1px solid var(--hairline);
}
.cpt-carte .cpt-ligne:last-child { border-bottom: 0; padding-bottom: 2px; }
.cpt-etat-ok {
  font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--good-text); background: color-mix(in srgb, var(--good-text) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--good-text) 30%, transparent);
  border-radius: 999px; padding: 4px 11px;
}
/* Permissions : par famille, avec icone et libelle lisible. */
.cpt-permgroupes {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 14px 22px;
}
.cpt-pg-t {
  font-size: 10.5px; letter-spacing: .09em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 7px;
}
.cpt-pg-flags { display: flex; flex-wrap: wrap; gap: 6px; }
.cpt-flag {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12.5px; font-weight: 600; padding: 6px 12px 6px 8px;
  border-radius: 999px; color: var(--ink);
  background: var(--surface-2);
  border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--hairline));
}
.cpt-flag-i { font-size: 13px; line-height: 1; }
/* Salons personnels : pastilles avec le diese en médaillon, croix au survol. */
.cpt-sous { font-size: 12.5px; font-weight: 700; margin: 12px 0 8px; }
.cpt-carte .cpt-sous:first-of-type { margin-top: 2px; }
.cpt-puces { display: flex; flex-wrap: wrap; gap: 7px; }
.cpt-puce {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 12.5px; font-weight: 600; padding: 5px 10px 5px 6px;
  border-radius: 999px; background: var(--surface-2);
  border: 1px solid var(--hairline);
}
.cpt-puce-h {
  width: 20px; height: 20px; flex: none; border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px; color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.cpt-puce.prive .cpt-puce-h { background: none; }
.cpt-alias {
  font-size: 9.5px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); border: 1px solid var(--hairline);
  border-radius: 4px; padding: 1px 4px; margin-left: 5px;
}
.cpt-puce button {
  border: 0; background: none; color: var(--muted); cursor: pointer;
  font: inherit; font-size: 12px; padding: 0 3px;
}
.cpt-puce button:hover { color: var(--critical); }
.cpt-vide { font-size: 12px; color: var(--muted); line-height: 1.5; }
/* Badge « ajouté » dans la liste des salons. */
.sln-mien { color: var(--accent); font-weight: 700; }

/* ===========================================================================
 * MOBILE — tout ce qui suit est enferme dans des @media : la version bureau
 * n'est jamais touchee. Regle de travail : on n'a pas modifie une seule regle
 * existante, on ne fait qu'ajouter des surcharges sous un seuil de largeur.
 *
 * Le fil directeur : la coquille d'application (fenetre figee, defilement
 * interne) est faite pour un grand ecran. Sur un telephone, la hauteur est la
 * ressource rare et les barres du navigateur apparaissent et disparaissent :
 * on rend donc la main au defilement classique, avec une hauteur en « dvh »
 * (dynamic viewport height) qui suit ces barres au lieu de les ignorer.
 * ========================================================================== */
@media (max-width: 860px) {
  /* 100dvh et non 100% : sur iOS et Android, la barre d'adresse se retracte au
     defilement. Avec une hauteur figee, le bas de la page passait dessous. */
  html, body { height: 100dvh; }

  /* main reprend une gouttiere raisonnable et NE defile pas lui-meme : ce sont
     les vues qui defilent, comme sur grand ecran. La regle 640px lui remettait
     un remplissage vertical qui, dans un conteneur a debordement masque,
     rognait le bas de chaque vue. */
  /* PLEINE LARGEUR. Sur 390 px, dix pixels de gouttiere de chaque cote plus
     treize de remplissage de panneau, c'etaient 46 px — 12 % de l'ecran — que
     le contenu n'avait pas. On resserre les deux : le texte gagne 26 px sans
     que rien ne touche le bord. */
  main { padding: 0 5px; }

  /* Barre du haut compacte : la marque cede la place au menu. */
  .topbar { padding: 6px 10px; gap: 8px; flex-wrap: nowrap; }
  .brand-name, .brand-thin { display: none; }
  .iapc-logo { display: none; }

  /* NAVIGATION — MENU HAMBURGER.
     Onze onglets ne tiennent pas sur 390 px. En ruban defilant, l'onglet actif
     se retrouvait souvent hors champ : on ouvrait l'appli sans voir OU l'on
     etait, et il fallait faire glisser la barre pour trouver « Admin ». On les
     replie donc derriere un bouton, et on ecrit en clair le nom de la vue
     courante a cote — la question « ou suis-je » se resout d'un coup d'oeil.
     Le HTML n'est pas touche : bouton, voile et titre sont crees par
     initMobile(), donc le bureau ne les voit jamais. */
  .nav-burger {
    display: inline-flex; flex-direction: column; justify-content: center;
    align-items: center; gap: 4px; flex: none;
    width: 42px; height: 42px; padding: 0;
    background: transparent; border: 1px solid var(--hairline); border-radius: 10px;
    cursor: pointer;
  }
  .nav-burger span {
    display: block; width: 18px; height: 2px; border-radius: 2px;
    background: var(--ink); transition: transform .18s, opacity .18s;
  }
  body.nav-ouvert .nav-burger { border-color: var(--accent); }
  body.nav-ouvert .nav-burger span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
  body.nav-ouvert .nav-burger span:nth-child(2) { opacity: 0; }
  body.nav-ouvert .nav-burger span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

  .nav-titre {
    flex: 1 1 auto; min-width: 0; font-size: 14.5px; font-weight: 650;
    color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  /* Le panneau : ancre sous la barre du haut, hauteur bornee, il defile s'il
     le faut. « --nav-haut » est mesure en JS — la barre change de hauteur avec
     le badge de maintenance, une valeur en dur se serait desalignee. */
  .topbar-tabs {
    display: none;
    position: fixed; left: 0; right: 0; top: var(--nav-haut, 54px);
    /* « width: auto » n'est PAS redondant : .tabs impose « width: fit-content »,
       qui l'emporte sur le couple left/right et reduisait le tiroir au tiers
       de l'ecran. */
    width: auto; max-width: none;
    flex-direction: column; align-items: stretch; gap: 2px;
    max-height: calc(100dvh - var(--nav-haut, 54px)); overflow-y: auto;
    padding: 8px; margin: 0; z-index: 2100;
    background: var(--surface);
    border: 0; border-bottom: 1px solid var(--hairline); border-radius: 0;
    box-shadow: 0 18px 40px rgba(0, 0, 0, .45);
  }
  body.nav-ouvert .topbar-tabs { display: flex; }
  /* Specificite volontairement portee a « body.nav-ouvert » : le palier 560 px
     redeclare .topbar-tabs .tab avec un remplissage de bureau. */
  body.nav-ouvert .topbar-tabs .tab {
    padding: 13px 14px; font-size: 15px; border-radius: 9px;
    text-align: left; min-height: 46px; display: flex; align-items: center;
  }
  /* Le voile passe SOUS la barre du haut (2000) : le bouton reste cliquable
     pour refermer, et le contexte de la page transparait derriere. */
  .nav-voile {
    position: fixed; inset: 0; z-index: 1900; display: none;
    background: rgba(0, 0, 0, .48); backdrop-filter: blur(1px);
  }
  body.nav-ouvert .nav-voile { display: block; }
  .pill { padding: 3px 8px; font-size: 11px; }
  .pill span:not(.dot) { display: none; }   /* on garde la pastille de couleur */

  /* DEBORDEMENTS DE GRILLE. Un element de grille a « min-width: auto » : un mot
     insecable plus large que sa colonne ELARGIT la colonne au lieu d'etre
     coupe. C'est ce qui faisait sortir de quelques pixels l'etat du reseau et
     les panneaux du tableau de bord — assez pour que la bordure droite tombe
     hors de l'ecran. On autorise la cesure. */
  .panels > *, .hl-rows > *, .hl-row { min-width: 0; }
  .hl-row .hl-ligne1 { flex-wrap: wrap; }
  .hl-name, .hl-detail, .hl-title { overflow-wrap: anywhere; }
  .hl-head { flex-wrap: wrap; }

  /* SOUS-ONGLETS : ils defilaient horizontalement, donc les derniers etaient
     invisibles ET rien ne signalait qu'il y en avait d'autres. Ils sont peu
     nombreux : on les laisse passer a la ligne. « main .tabs » et non « .tabs »
     tout court : la barre du haut est AUSSI un .tabs, et c'est desormais le
     tiroir vertical du menu — il ne doit surtout pas repasser en ligne. */
  /* Series de menus CENTREES. Quatre ou cinq entrees calees a gauche laissent
     un vide a droite qui donne l'impression d'un rendu casse — d'autant plus
     quand elles passent sur deux rangees. On centre : la barre reste lisible
     comme un tout.
     « main .tabs » et non « .tabs » : la barre du haut est aussi un .tabs, mais
     c'est devenu le tiroir VERTICAL du menu — y centrer reviendrait a centrer
     ses entrees sur la HAUTEUR. */
  main .tabs { flex-wrap: wrap; overflow-x: visible; justify-content: center; }
  .jr-vues, .jr-chips, .frame-presets { justify-content: center; }

  /* Cartes de passerelle : la grille reclamait des colonnes de 330 px minimum,
     soit plus large que la place disponible — les cartes sortaient du cadre. */
  .gw-list { grid-template-columns: 1fr; max-height: none; }
  .gw-card { min-width: 0; }
  .gw-card-name { min-width: 0; overflow-wrap: anywhere; }

  /* Cibles tactiles : 40 px minimum, recommandation d'accessibilite. Un bouton
     de 28 px se rate une fois sur trois avec le pouce. */
  /* jr-vue et jr-chip en sont exclus : ce sont des pastilles de filtre, il y en
     a sept a douze d'affilee, et a 40 px elles occupaient quatre rangees. */
  .btn, .icon-btn, .au-icon, .acct-role, .sln-item {
    min-height: 40px;
  }
  .btn { padding: 9px 14px; }
  select, input { min-height: 40px; }

  /* Vues : gouttiere verticale portee par la vue, comme sur grand ecran. */
  #overview, #view-admin, #view-usb, #view-companion, #view-maint,
  #view-cli, #view-journal, #view-frames, #view-tele, #view-synthese { padding: 12px 0 20px; }

  /* CARTE — LA CARTE D'ABORD. Dans le DOM, la colonne de reglages precede la
     carte : en pile verticale, on tombait donc sur sept cents pixels de cases a
     cocher avant d'apercevoir le terrain. Sur la vue dont la carte EST le sujet,
     c'etait le pire ordre possible. On inverse par « order », sans toucher au
     HTML — donc sans rien changer au bureau.
     Les reglages deviennent un tiroir replie par defaut (voir initMobile). */
  #view-map { overflow-y: auto; display: block; padding: 0; }
  /* « min-height » et non « height » : replie, le tiroir de reglages ne fait que
     46 px et la carte doit prendre TOUT le reste — une hauteur figee en dvh
     laissait un demi-ecran de vide dessous. Deplie, la pile depasse la vue et
     c'est la vue qui defile. */
  .map-split { display: flex; flex-direction: column; overflow: visible; min-height: 100%; }
  .map-split > .map-wrap { order: -1; flex: 1 1 auto; height: auto; min-height: 320px; }
  .map-aside {
    order: 1; border-right: none; border-top: 1px solid var(--hairline);
    max-height: none; overflow: visible;
  }
  /* Tiroir : tout se replie sauf la barre de titre que le JS insere. */
  .map-aside.replie > .ma-bloc,
  .map-aside.replie > .ma-pied { display: none; }
  .ma-tiroir {
    display: flex; align-items: center; justify-content: space-between;
    width: 100%; padding: 12px 14px; background: var(--surface-2);
    border: 0; border-bottom: 1px solid var(--hairline);
    color: var(--ink); font: inherit; font-size: 13.5px; font-weight: 650;
    cursor: pointer; min-height: 44px;
  }
  .ma-tiroir .chev { color: var(--muted); font-size: 12px; }

  /* Tiroir generique : meme mecanique pour les blocs de filtres, qui tenaient
     un a trois ecrans avant le premier resultat. Le bouton est cree par
     initMobile(), il n'existe donc pas sur grand ecran. */
  .mob-tiroir {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    width: 100%; min-height: 44px; padding: 11px 14px; margin: 0 0 10px;
    background: var(--surface); border: 1px solid var(--hairline); border-radius: 10px;
    color: var(--ink); font: inherit; font-size: 13.5px; font-weight: 650; cursor: pointer;
  }
  .mob-tiroir .chev { color: var(--muted); font-size: 12px; }
  section.filters.replie:not(.mob-partiel) { display: none; }
  .jr-vues.replie { display: none; }
  /* Le libelle porte la selection en cours : il peut etre long, il tronque. */
  .mob-tiroir .lbl { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  /* Repli partiel : les pastilles de periode (.field.grow) restent, seules les
     dates manuelles et le bouton Appliquer disparaissent. */
  section.filters.mob-partiel.replie > .field:not(.grow) { display: none; }
  section.filters.mob-partiel > .mob-tiroir { margin: 0; }
  /* Reglages plus compacts : la colonne etait dessinee pour 320 px de large,
     etiree sur 390 px elle devenait spacieuse a l'exces. */
  .ma-bloc { padding: 9px 12px; }
  .ma-titre { font-size: 13px; }
  .map-aside .chk { font-size: 13px; padding: 4px 0; }
  .map-aside label { gap: 8px; }

  /* SALONS — une seule colonne. La liste devient une bande horizontale en
     haut : elle reste atteignable sans quitter la conversation, la ou un
     empilement vertical aurait pousse les messages hors de l'ecran. */
  #view-salons { padding: 10px 0; }
  .sln-split { display: flex; flex-direction: column; gap: 10px; }
  .sln-mid { display: none; }              /* la carte de propagation attend le grand ecran */
  .sln-list { flex: none; height: auto; max-height: none; }
  .sln-note { display: none; }
  /* LISTE VERTICALE REPLIEE, pas une bande a faire glisser. Le ruban
     horizontal cachait la moitie des salons et rien n'annoncait qu'il y en
     avait d'autres — on faisait glisser au hasard. Le tiroir (voir initMobile)
     porte le nom du salon ouvert et deroule la liste complete au toucher. */
  .sln-items {
    flex-direction: column; flex-wrap: nowrap; gap: 3px; padding: 6px;
    overflow-x: hidden; overflow-y: auto; max-height: 52dvh;
  }
  .sln-items.replie { display: none; }
  /* La carte de salon est dessinee pour une colonne de 270 px ; sur 380 px
     elle tient sans rien couper. On garde nom, compteur et horodatage. */
  .sln-item {
    flex: 0 0 auto; width: 100%; max-width: none;
    gap: 10px; padding: 9px 11px; border-radius: 9px; min-height: 46px;
  }
  .sln-item.on { box-shadow: inset 2px 0 0 var(--accent); }
  /* Au doigt, l'oeil doit rester visible et faire une vraie cible. */
  .sln-oeil { opacity: .75; font-size: 15px; padding: 6px 7px; top: 3px; right: 3px; }
  .sln-masques-barre .btn { min-height: 32px; }
  /* Page compte : une colonne, hero resserre. */
  .cpt-grille { grid-template-columns: 1fr; }
  .cpt-hero { padding: 16px 15px; gap: 13px; }
  .cpt-avatar { width: 50px; height: 50px; font-size: 18px; }
  .cpt-nom { font-size: 20px; }
  .cpt-hero-droite { flex: 1 1 100%; }
  .cpt-theme-l select { width: 100%; min-height: 40px; }
  .cpt-permgroupes { grid-template-columns: 1fr; }

  /* iOS zoome sur tout champ sous 16 px : la mise en page sauterait. */
  .sln-ajout input { font-size: 16px; min-height: 38px; }
  .sln-ajout .btn { min-height: 38px; }
  .sln-plus { width: 34px; height: 34px; font-size: 17px; }
  .sln-item-nom { font-size: 14px; }
  .sln-item-sub { font-size: 11px; }
  .sln-hash { width: 24px; height: 24px; font-size: 12px; border-radius: 7px; }
  .sln-main { flex: 1 1 auto; min-height: 0; }
  /* Compositeur sur telephone : la zone de saisie doit faire 16 px minimum,
     sinon iOS zoome tout seul sur le champ et la mise en page saute. */
  .sln-envoi { padding: 9px 11px; }
  .sln-envoi-ligne input { font-size: 16px; min-height: 42px; }
  .sln-envoi-ligne .btn { min-height: 42px; padding: 9px 13px; }
  /* Sur 390 px, le selecteur prend sa rangee, le nom et les octets la suivante. */
  .sln-envoi-gw { flex: 1 1 100%; order: -1; }
  .sln-envoi-gw select { min-height: 32px; }
  .sln-envoi-nom { flex: 1 1 auto; }

  /* TRAMES — dix colonnes ne tiennent pas. On garde ce qu'on vient lire :
     l'heure, la passerelle et le message. Le reste (canal, type, route, RSSI,
     SNR, longueur, hex) reste accessible sur grand ecran ; l'escamoter vaut
     mieux qu'un defilement horizontal ou qu'un texte de 6 px. */
  #view-frames table { table-layout: auto; }
  #view-frames thead th:nth-child(2), #view-frames tbody td:nth-child(2),
  #view-frames thead th:nth-child(4), #view-frames tbody td:nth-child(4),
  #view-frames thead th:nth-child(5), #view-frames tbody td:nth-child(5),
  #view-frames thead th:nth-child(6), #view-frames tbody td:nth-child(6),
  #view-frames thead th:nth-child(7), #view-frames tbody td:nth-child(7),
  #view-frames thead th:nth-child(8), #view-frames tbody td:nth-child(8),
  #view-frames thead th:nth-child(10), #view-frames tbody td:nth-child(10) {
    display: none;
  }
  #view-frames thead th:nth-child(1) { width: 62px; }
  #view-frames thead th:nth-child(3) { width: 92px; }
  #view-frames thead th:nth-child(9) { width: auto; }
  #view-frames td.msg { -webkit-line-clamp: 3; }

  /* JOURNAL — les commandes prennent trop de hauteur : la vue redevient
     defilante, sinon il ne resterait rien pour les evenements. */
  #view-journal { display: block; overflow-y: auto; }
  #view-journal > .jr-flux { overflow: visible; }
  .jr-head-actions { width: 100%; }

  /* TELEMETRIE — meme raison. */
  #view-tele { display: block; overflow-y: auto; }
  #view-tele > .panel { display: block; }
  #view-tele > .panel > div[id^="tele-view-"] { overflow: visible; }

  /* TELEMETRIE EN FICHES. Treize colonnes ne se lisent pas sur un telephone :
     le tableau devenait un ruban qu'il fallait faire glisser colonne par
     colonne pour lire UNE ligne. Chaque ligne devient une fiche « intitule /
     valeur » — les intitules viennent de data-l, pose par initMobile(). Le
     tableau reste un <table> dans le DOM : le bureau ne voit aucun changement. */
  .tele-latest, #tele-gps-table, .cmp-list { overflow-x: visible; }
  .tele-table, .cmp-table { display: block; width: 100%; }
  .tele-table thead, .cmp-table thead { display: none; }
  .tele-table tbody, .cmp-table tbody { display: block; }
  .tele-table tr, .cmp-table tr {
    display: block; padding: 9px 11px; margin-bottom: 8px;
    background: var(--surface-2); border: 1px solid var(--hairline); border-radius: 10px;
  }
  .tele-table tr.sel { border-color: var(--accent); }
  /* « border: 0 » ne suffit pas : le theme Propagation pose un filet sous
     chaque cellule de tableau (tbody td), plus specifique que cette regle. En
     mode fiche il dessinait un trait en travers de chaque valeur. */
  .tele-table td, .cmp-table td,
  :root[data-theme="propagation"] .tele-table tbody td,
  :root[data-theme="propagation"] .cmp-table tbody td {
    display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
    padding: 3px 0; border: 0; font-size: 13px; max-width: none;
  }
  .tele-table td::before, .cmp-table td::before {
    content: attr(data-l); flex: none;
    color: var(--muted); font-size: 10px; letter-spacing: .07em; text-transform: uppercase;
  }
  /* Une fiche de treize lignes dont neuf tirets n'apprend rien : on masque les
     mesures absentes. La colonne existe toujours sur grand ecran. */
  .tele-table td[data-vide], .cmp-table td[data-vide] { display: none; }
  /* Premiere cellule = titre de la fiche, et bouton d'ouverture. */
  .tele-table td:first-child, .cmp-table td:first-child {
    display: flex; align-items: baseline; gap: 8px;
    font-size: 15px; font-weight: 700;
    padding: 0 0 6px; margin-bottom: 5px; border-bottom: 1px solid var(--hairline);
  }
  .tele-table td:first-child::before, .cmp-table td:first-child::before { display: none; }
  /* Le chevron est un pseudo-element : rien n'est ajoute au DOM, donc
     l'observateur de mutations qui repose les intitules ne se declenche pas. */
  .tele-table td:first-child::after, .cmp-table td:first-child::after {
    content: "▸"; margin-left: auto; flex: none;
    color: var(--muted); font-size: 12px; font-weight: 400;
  }
  .tele-table tr.ouvert td:first-child::after,
  .cmp-table tr.ouvert td:first-child::after { content: "▾"; }
  /* Le filet du titre de fiche, lui, doit rester : on le repose au meme niveau
     de specificite que la regle de theme qui vient de l'effacer. */
  :root[data-theme="propagation"] .tele-table tbody tr.ouvert td:first-child,
  :root[data-theme="propagation"] .cmp-table tbody tr.ouvert td:first-child {
    border-bottom: 1px solid var(--hairline);
  }
  /* FICHES REPLIEES PAR DEFAUT. Six nodes deployes faisaient six ecrans de
     defilement. Repliee, la fiche garde le nom et la fraicheur de la mesure
     (derniere cellule) : de quoi reperer un node muet sans rien ouvrir. */
  .tele-table tr:not(.ouvert) td:not(:first-child):not([data-resume]),
  .cmp-table tr:not(.ouvert) td:not(:first-child):not([data-resume]) { display: none; }
  .tele-table tr:not(.ouvert) td:first-child,
  .cmp-table tr:not(.ouvert) td:first-child {
    padding-bottom: 0; margin-bottom: 0; border-bottom: 0;
  }
  .tele-table tr:not(.ouvert) td[data-resume],
  .cmp-table tr:not(.ouvert) td[data-resume] { font-size: 12px; padding-top: 5px; }
  /* « max-width: 15ch » sur ces deux colonnes venait du mode tableau : en fiche
     il enfermait la valeur dans une boite etroite au milieu de la ligne. */
  .tele-table td.tele-c-gps, .tele-table td.tele-c-pos {
    white-space: normal; max-width: none;
  }
  .tele-table td { text-align: right; }
  .tele-table td::before { text-align: left; }
  .tele-table td:first-child { text-align: left; }

  /* ECHELLE TYPOGRAPHIQUE — les tuiles et bandeaux sont dessines pour un grand
     ecran : a 390 px, « 776173 » en 27 px depasse de sa carte et le bandeau
     d'etat coupe ses titres en deux. On resserre l'echelle sans rien retirer. */
  .stats-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; }
  .stat-card { padding: 10px 12px; }
  .stat-card .v { font-size: 19px; }
  .stat-card .k { font-size: 9.5px; letter-spacing: .08em; }
  /* Le theme Propagation redessine la carte de statistique (une ligne, valeur
     a droite en 26 px) avec une specificite superieure : sans le nommer ici,
     l'echelle mobile ne s'appliquait tout simplement pas et les chiffres
     restaient enormes. Deux mesures a l'appui : fs=26px au lieu de 19. */
  :root[data-theme="propagation"] .stats-grid .stat-card { padding: 11px 13px; gap: 10px; }
  :root[data-theme="propagation"] .stats-grid .stat-card .v { font-size: 19px; }
  :root[data-theme="propagation"] .stats-grid .stat-card .k { font-size: 9.5px; letter-spacing: .08em; }
  .hl-row { padding: 7px 12px; }
  .stat-card .sub { font-size: 10.5px; }
  .hl-title, .hl-head { font-size: 13px; }
  .hl-name { font-size: 12.5px; }
  .hl-detail { font-size: 12px; margin-left: 20px; }
  .panel { padding: 12px 10px; }
  .panel-head h2 { font-size: 13px; }
  /* Le pied de page mange une bande sur un ecran ou chaque ligne compte. */
  footer { display: none; }
  .load-grid, 
  .adm-alertes-row .field { max-width: none; }
  #adm-view-general { max-width: none; }

  /* Modales plein ecran : sur 380 px de large, une fenetre centree avec des
     marges ne laisse plus de place au contenu. */
  .app-login-card, .acct-card, .gwm-card, .cfm-card {
    width: 100vw; max-width: none; max-height: 100dvh;
    border-radius: 0; padding: 18px 16px;
  }
  .acct-idrow { flex-direction: column; }
  .acct-idrow .field { flex: 1 1 auto; }
}

/* --- Telephone etroit ------------------------------------------------------ */
@media (max-width: 560px) {
  
  /* JOURNAL : en-tete et pastilles resserres. L'en-tete (titre, trois lignes
     d'explication, deux boutons d'export) plus les pastilles de periode
     faisaient deux ecrans avant la premiere ligne du journal. On garde tout,
     en plus petit — l'explication est clampee a deux lignes. */
  .jr-title { font-size: 16px; }
  .jr-sub {
    font-size: 11px; line-height: 1.4;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  /* En-tete du journal sur UNE rangee : titre a gauche, exports a droite. La
     regle 720 px leur donnait toute la largeur, donc une rangee a eux seuls —
     quatre-vingt-dix pixels pris a la chronologie. Et une seule ligne
     d'explication : la couverture exacte se relit dans le tiroir Periode. */
  .jr-head { flex-direction: row; align-items: flex-start; gap: 10px; }
  .jr-head-actions { width: auto; flex: none; }
  .jr-head-actions .btn { min-height: 34px; padding: 6px 11px; font-size: 12px; }
  .jr-sub { -webkit-line-clamp: 1; }
  .jr-chip, .jr-vue { min-height: 34px; padding: 6px 10px; font-size: 12.5px; }
  .jr-chip-lbl { font-size: 10px; }

  /* 44 px ne suffisaient pas a « 15:16:45 » : l'heure sortait de sa colonne. */
  .jr-ligne { grid-template-columns: 52px 12px 1fr; }
  .jr-ligne .jr-h { font-size: 10.5px; }
  .sln-item { max-width: none; }
  .map-split > .map-wrap { min-height: 280px; }
  .topbar-tabs .tab { padding: 7px 9px; font-size: 12.5px; }
  /* Le badge de maintenance garde sa pastille mais perd son texte. */
  .maint-badge { font-size: 0; padding: 8px 10px; gap: 0; }
  .maint-badge::before { content: "🛠"; font-size: 13px; }
}

/* Classement Bavards : scope par lequel les adverts arrivent (26.08.2026) */
.flood-scope-chip { display: inline-block; padding: 0 7px; border-radius: 10px;
  background: rgba(127, 127, 127, .18); font-size: 11px; white-space: nowrap; margin: 1px 3px 1px 0; }
.flood-scope-chip.fsc-nu { color: var(--danger, #f87171); background: rgba(248, 113, 113, .15); }
.flood-scope { max-width: 220px; }

/* Incidents : case « volontaires » (coupures provoquees par une action) */
.inc-vol { display: inline-flex; align-items: center; gap: 4px; font-size: 12px;
  color: var(--muted, #888); cursor: pointer; margin-right: 6px; white-space: nowrap; }
.inc-vol input { margin: 0; }
.inc-ep-cause { color: var(--muted, #888); font-size: 12px; }
/* Incidents : reconnexion immediate (< 20 s) = micro-perte reseau, pas une panne */
.inc-reco { color: var(--muted, #888); font-weight: normal; }
.inc-ep.reco .inc-ep-dot { opacity: .45; }
.inc-ep.reco .inc-ep-dur { color: var(--muted, #888); }

/* ---- Regles radio (phase 1 du Dossier Radio) ------------------------------ */
.etsi-gauge { position: relative; height: 8px; width: 120px; border-radius: 4px;
  background: var(--surface-2); overflow: hidden; display: inline-block; vertical-align: middle; margin-right: 6px; }
.etsi-gauge > span { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 4px; background: var(--good); }
.etsi-gauge.warn > span { background: var(--warning); }
.etsi-gauge.crit > span, .etsi-gauge.depasse > span { background: var(--critical); }
.etsi-gauge::after { content: ""; position: absolute; top: -2px; bottom: -2px; width: 2px;
  left: 50%; background: var(--warning); opacity: .7; }        /* repere 5 % (moitie de 10 %) */
.etsi-etat { font-weight: 600; }
.etsi-etat.ok { color: var(--good-text); } .etsi-etat.warn { color: var(--warning); }
.etsi-etat.crit, .etsi-etat.depasse { color: var(--critical); }
.etsi-nc { color: var(--critical); font-weight: 600; }
.bruit-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(340px, 100%), 1fr)); gap: 14px; margin-bottom: 14px; }
.bruit-map { border: 1px solid var(--hairline); border-radius: 10px; padding: 10px 12px; }
.bruit-map h4 { margin: 0 0 6px; font-size: 12.5px; font-weight: 650; }
.bruit-rows { display: grid; grid-template-columns: 52px repeat(24, 1fr); gap: 2px; font-size: 10px; }
.bruit-lab { color: var(--muted); font-variant-numeric: tabular-nums; line-height: 14px; }
.bruit-h { color: var(--muted); text-align: center; }
.bruit-cell { height: 14px; border-radius: 2px; background: var(--surface-2); }
.bruit-cell[data-s="1"] { background: color-mix(in srgb, var(--accent) var(--ramp-1), transparent); }
.bruit-cell[data-s="2"] { background: color-mix(in srgb, var(--accent) var(--ramp-2), transparent); }
.bruit-cell[data-s="3"] { background: color-mix(in srgb, var(--accent) var(--ramp-3), transparent); }
.bruit-cell[data-s="4"] { background: color-mix(in srgb, var(--accent) var(--ramp-4), transparent); }
.bruit-cell[data-s="5"] { background: color-mix(in srgb, var(--accent) var(--ramp-5), transparent); }
.bruit-legend { font-size: 11px; color: var(--muted); margin-top: 6px; }
.tele-auto-note { margin-top: 4px; }
.tele-auto-note b.warn { color: var(--warning); } .tele-auto-note b.crit { color: var(--critical); }
.auto-cell.warn { color: var(--warning); font-weight: 600; } .auto-cell.crit { color: var(--critical); font-weight: 600; }
.tele-meteo { margin-top: 6px; }
.meteo-verdict { font-size: 12px; margin-top: 2px; }
.meteo-verdict.propagation { color: var(--s3); } .meteo-verdict.stable { color: var(--muted); } .meteo-verdict.materiel { color: var(--warning); }
.sites-table input, .sites-table select { width: 72px; font-size: 12.5px; padding: 3px 5px; }
.sites-table select { width: 92px; }
.sites-table th small { display: block; font-weight: 400; text-transform: none; color: var(--muted); }

/* ---- Bilan de liaison (phase 2) ------------------------------------------- */
.lb-verdict { font-weight: 600; }
.lb-verdict.confortable { color: var(--good-text); } .lb-verdict.juste { color: var(--warning); }
.lb-verdict.critique { color: var(--critical); } .lb-verdict.na { color: var(--muted); font-weight: 400; }
.lb-vue.degage { color: var(--good-text); } .lb-vue.partiel { color: var(--warning); } .lb-vue.obstrue { color: var(--critical); }
.lb-table tr.sel { background: var(--accent-soft); }
.lb-profil { margin: 10px 0 16px; }
.lb-profil svg { width: 100%; height: 220px; display: block; }
.lb-profil .lb-terrain { fill: color-mix(in srgb, var(--ink-2) 28%, transparent); stroke: var(--ink-2); stroke-width: 1; vector-effect: non-scaling-stroke; }
.lb-profil .lb-fresnel { fill: color-mix(in srgb, var(--accent) 18%, transparent); }
.lb-profil .lb-los { stroke: var(--accent); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
.lb-profil .lb-obst { stroke: var(--critical); stroke-width: 1; stroke-dasharray: 3 3; vector-effect: non-scaling-stroke; }
.lb-profil .lb-ant { stroke: var(--ink); stroke-width: 2; vector-effect: non-scaling-stroke; }
.lb-legende { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 12px; color: var(--ink-2); margin-top: 6px; }
.lb-defauts { font-size: 11px; color: var(--muted); }
.pw-cmd { font-family: var(--mono); font-size: 12px; background: var(--surface-2); padding: 1px 6px; border-radius: 4px; }
@media (max-width: 640px) {
  .lb-profil svg { height: 160px; }
  .bruit-rows { grid-template-columns: 40px repeat(24, 1fr); font-size: 9px; }
}

/* ---- Atlas des routes / couverture (phase 3) ------------------------------ */
.routes-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); gap: 12px; margin-bottom: 14px; }
.routes-card { border: 1px solid var(--hairline); border-radius: 10px; padding: 10px 12px; }
.routes-card h4 { margin: 0 0 6px; font-size: 13px; font-weight: 650; display: flex; justify-content: space-between; gap: 8px; }
.routes-card h4 span { font-weight: 400; color: var(--muted); font-size: 12px; }
.routes-bar { display: grid; grid-template-columns: minmax(0, 1fr) 46px; align-items: center; gap: 8px; font-size: 12px; margin: 3px 0; }
.routes-bar .rb-nom { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.routes-bar .rb-nom.flou { color: var(--muted); font-style: italic; }
.routes-bar .rb-track { position: relative; height: 6px; background: var(--surface-2); border-radius: 3px; grid-column: 1 / 2; }
.routes-bar .rb-track > span { position: absolute; left: 0; top: 0; bottom: 0; background: var(--accent); border-radius: 3px; }
.routes-bar .rb-track > span.any { background: color-mix(in srgb, var(--accent) 35%, transparent); }
.routes-bar .rb-pct { font-variant-numeric: tabular-nums; text-align: right; color: var(--ink-2); }
.routes-sans { margin: 8px 0 16px; font-size: 13px; }
.routes-sans .rs-perdues { display: flex; flex-wrap: wrap; gap: 4px 8px; margin-top: 6px; }
.routes-sans .rs-perdues span { background: var(--surface-2); border-radius: 4px; padding: 1px 6px; font-size: 12px; }
.routes-sans .rs-impact { margin-top: 6px; }
.rl-nom.flou { color: var(--muted); font-style: italic; }
.map-covbar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin: 4px 0 0 22px; font-size: 12px; }
.map-cov-legende { display: inline-flex; align-items: center; gap: 2px; color: var(--muted); }
.cov-swatch { display: inline-block; width: 12px; height: 10px; border-radius: 2px; }
.cov-swatch[data-s="1"], .cov-cell-1 { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.cov-swatch[data-s="2"], .cov-cell-2 { background: color-mix(in srgb, var(--accent) 38%, transparent); }
.cov-swatch[data-s="3"], .cov-cell-3 { background: color-mix(in srgb, var(--accent) 54%, transparent); }
.cov-swatch[data-s="4"], .cov-cell-4 { background: color-mix(in srgb, var(--accent) 72%, transparent); }
.cov-swatch[data-s="5"], .cov-cell-5 { background: color-mix(in srgb, var(--accent) 92%, transparent); }

/* ---- Ping radio (Telemetrie > Radio) et ping MQTT (Synthese) ---------- */
.trace-form { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; margin: 6px 0 10px; font-size: 13px; }
.trace-form label { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); }
.trace-path { font-family: var(--mono); width: 13ch; max-width: none; }
.trace-hop { white-space: nowrap; }
.trace-table tr.trace-ko td { opacity: 0.75; }
.btn-mini { padding: 1px 9px; font-size: 11.5px; line-height: 1.6; }
.syn-ping-cell { white-space: nowrap; }

/* ---- Tableau Nodes (Telemetrie) : douze colonnes qui tiennent dans la page.
   Retour a la ligne autorise dans chaque cellule, sous-valeurs en retrait
   (.tele-sub), seules les valeurs courtes restent insecables (.tele-nw). Le
   tableau ne doit JAMAIS faire defiler la page horizontalement (30.08). */
.tele-table.tele-nodes { table-layout: auto; font-size: 12.5px; }
.tele-table.tele-nodes th { padding: 6px 6px; white-space: normal; line-height: 1.25; }
.tele-table.tele-nodes td { padding: 5px 6px; white-space: normal; line-height: 1.35; vertical-align: top; }
.tele-table.tele-nodes td.tele-c-gps { max-width: none; }
.tele-sub { color: var(--muted); font-size: 11.5px; }
/* Separateur des colonnes composites (GPS · POSITION, BRUIT · version) : meme
   taille que l'en-tete du tableau (12.5px) pour qu'il se voie, mais plus pale
   que les valeurs pour ne pas leur disputer la lecture. A 11.5px il etait trop
   discret et les valeurs se recollaient a l'oeil. */
.tele-sep { color: var(--muted); font-size: 12.5px; opacity: .55; }
.tele-nw { white-space: nowrap; }
.trace-plus { margin-top: 6px; }
.trace-plus a { color: var(--muted); text-decoration: underline dotted; }

/* ---- Cle publique complete (modale) + salons surveilles (liste noire) ---- */
.cle-card { max-width: 560px; }
.cle-nom { font-weight: 650; margin: 4px 0 10px; }
.cle-l { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin-top: 10px; }
.cle-val { display: block; font-family: var(--mono); font-size: 12.5px; word-break: break-all; padding: 6px 8px;
  border: 1px solid var(--hairline); border-radius: 8px; background: var(--page); margin-top: 4px; user-select: all; }
.cle-actions { display: flex; gap: 8px; margin-top: 12px; }
code.np-key, td.hex, b.syn-cle, .ban-row > code { cursor: pointer; }
code.np-key:hover, td.hex:hover, b.syn-cle:hover, .ban-row > code:hover { text-decoration: underline dotted; }
.ban-salons { margin-top: 14px; border-top: 1px solid var(--hairline); padding-top: 10px; }
.ban-salons-row { display: flex; gap: 8px; align-items: center; margin: 8px 0; }
.ban-salons-row input { flex: 1; }
.ban-salon-btn { font-size: 11.5px; }

/* ---- Assistant de ban par etapes ------------------------------------ */
.ban-wizard { padding: 8px 0 4px; }
.ban-wizard h3 { margin: 10px 0 8px; font-size: 14px; }
.bw-steps { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; max-width: 100%; }
.bw-step { white-space: nowrap; }
.bw-step { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--muted);
  padding: 2px 8px; border: 1px solid var(--hairline); border-radius: 999px; }
.bw-step.on { color: var(--ink); border-color: var(--accent); background: var(--accent-soft); }
.bw-step.done { color: var(--accent); }
.bw-cands { display: flex; flex-direction: column; gap: 6px; }
.bw-cand { text-align: left; padding: 8px 10px; border: 1px solid var(--hairline); border-radius: 10px; background: var(--page); color: var(--ink); cursor: pointer; }
.bw-cand:hover { border-color: var(--accent); }
.bw-cand code { font-family: var(--mono); font-size: 11.5px; color: var(--muted); margin-left: 6px; }
.bw-node { padding: 8px 10px; border: 1px solid var(--hairline); border-radius: 10px; margin: 6px 0; }
.bw-node code { font-family: var(--mono); font-size: 12px; margin-left: 6px; }
.bw-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
.bw-salons { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 4px 14px; margin: 6px 0; }
.bw-libre { margin-top: 6px; }
.bw-libre input { width: 100%; }
.bw-recap { margin: 6px 0 0 18px; line-height: 1.6; }
.bw-recap code { font-family: var(--mono); font-size: 12px; }
.bw-res { padding: 4px 0; }
.bw-res.ko { color: var(--critical); }

/* Couverture : nappe diffuse plutot que carres (30.08). Le flou porte sur le
   volet entier, les disques se fondent entre eux ; le survol garde l'infobulle. */
.leaflet-couverture-pane { filter: blur(11px); opacity: .9; }
/* Les classes .cov-cell-N portaient un `background`, sans effet sur un trace
   SVG : toutes les mailles sortaient dans le bleu par defaut de Leaflet. Sur un
   <path> c'est `fill` qui compte, et une regle CSS prime sur l'attribut. */
.leaflet-couverture-pane path.cov-cell-1 { fill: #3b8bff; fill-opacity: .16; }
.leaflet-couverture-pane path.cov-cell-2 { fill: #3b8bff; fill-opacity: .22; }
.leaflet-couverture-pane path.cov-cell-3 { fill: #3b8bff; fill-opacity: .28; }
.leaflet-couverture-pane path.cov-cell-4 { fill: #3b8bff; fill-opacity: .36; }
.leaflet-couverture-pane path.cov-cell-5 { fill: #3b8bff; fill-opacity: .46; }

/* Messages detectes des bannis (modale Liste noire) */
.ban-msgs { margin-top: 14px; border-top: 1px solid var(--hairline); padding-top: 10px; }
.bm-bloc { margin: 8px 0 12px; }
.bm-tete code { font-family: var(--mono); font-size: 11.5px; margin-left: 6px; }
.bm-adv { font-size: 12px; margin: 2px 0 4px; }
.bm-table td.bm-txt { white-space: normal; max-width: 40ch; }
.bw-fin-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin: 8px 0 6px; }
.bw-fin-row h3 { margin: 0; }
.bw-result { max-height: 28vh; overflow: auto; padding-right: 4px; }

/* ---- Liste noire = panneau lateral droit (30.08) -----------------------
   Toute la hauteur de l'ecran, en-tete et saisie fixes, corps defilant ; le
   tableau des messages a sa propre zone. La fenetre entiere ne defile plus. */
#ban-modal { justify-content: flex-end; align-items: stretch; }
#ban-modal .app-login-card { width: min(1280px, 96vw); max-width: none; height: 100vh; max-height: 100vh;
  margin: 0; border-radius: 0; border-width: 0 0 0 1px; padding: 22px 30px 16px; overflow: hidden; gap: 6px; }
#ban-modal .gwm-head { margin-bottom: 10px; }
#ban-modal .ban-corps { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
#ban-modal #ban-cols { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 6px; }
.ban-liste-tete { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 12px; margin: 2px 0 8px; }
.ban-card { border: 1px solid var(--hairline); border-radius: 12px; padding: 8px 12px; margin: 8px 0; }
.ban-card .ban-row { border: none; padding: 0; margin: 0; }
.bm-node { margin-top: 4px; }
.bm-stats { font-size: 12px; margin-bottom: 4px; }
#ban-modal .ban-wizard { display: flex; flex-direction: column; min-height: 0; height: 100%; overflow: hidden; }
#ban-modal .ban-wizard > .bw-steps { flex: 0 0 auto; }
#ban-modal .ban-wizard > h3, #ban-modal .ban-wizard > p, #ban-modal .ban-wizard > label, #ban-modal .ban-wizard > .bw-node { flex: 0 0 auto; }
#ban-modal .ban-wizard > .bw-cands, #ban-modal .ban-wizard > .bw-salons, #ban-modal .ban-wizard > .bw-recap,
#ban-modal .ban-wizard > .bw-result { flex: 1 1 auto; min-height: 0; overflow-y: auto; max-height: none; }
#ban-modal .ban-wizard > .bw-actions { flex: 0 0 auto; margin-top: auto; padding-top: 12px; border-top: 1px solid var(--hairline); }
#ban-modal .ban-row { flex-wrap: wrap; }
@media (max-width: 1000px) { #ban-modal .ban-cols { grid-template-columns: 1fr; } }
#ban-modal .bm-table-wrap { border: 1px solid var(--hairline); border-radius: 10px; }
#ban-modal .bm-table-wrap thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
@media (max-width: 900px) { #ban-modal .app-login-card { width: 100vw; padding: 14px 12px; } }
.bm-surv { margin-left: 6px; font-size: 11.5px; color: var(--accent); text-decoration: underline dotted; white-space: nowrap; }
/* La fenetre « Cle publique » s'ouvre depuis d'autres modales (liste noire,
   fiche…) : elle doit passer DEVANT elles, pas derriere (30.08). */
#cle-modal { z-index: 12000; }
details.bm-bloc > summary.bm-tete { cursor: pointer; list-style: none; padding: 6px 0; }
details.bm-bloc > summary.bm-tete::before { content: "▸"; display: inline-block; width: 14px; color: var(--muted); transition: transform .15s; }
details.bm-bloc[open] > summary.bm-tete::before { transform: rotate(90deg); }
details.bm-bloc > summary::-webkit-details-marker { display: none; }

.ban-etat { font-size: 12px; margin-left: 10px; white-space: nowrap; }
.ban-card .ban-row > b { margin-right: 4px; }

/* ---- Tampon BANNI (30.08) : partout ou un noeud banni apparait encore ---- */
.tampon { display: inline-block; margin: 0 8px 0 6px; padding: 1px 7px; border: 2px solid var(--critical);
  border-radius: 4px; color: var(--critical); font-weight: 800; font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; transform: rotate(-6deg); opacity: .9; vertical-align: middle; line-height: 1.3;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--critical) 35%, transparent); }
.tampon.mini { font-size: 9.5px; padding: 0 5px; margin: 0 6px 0 4px; border-width: 1.5px; letter-spacing: .12em; }
.sln-tag.banni { color: var(--critical); border-color: color-mix(in srgb, var(--critical) 50%, transparent); }

/* ============================ REGLAGES (onglet dedie) ======================
   Navigation laterale + cartes ; chaque section tient dans un ecran.

   REGLE D'ALIGNEMENT (la capture du 31.08 montrait le contraire) :
   un libelle sur deux lignes decalait son champ par rapport a celui d'a cote.
   La correction tient en trois points :
     1. chaque champ est une COLONNE flex qui remplit toute la hauteur de sa
        cellule (`height: 100%`) ;
     2. son libelle pousse l'input vers le BAS (`margin-top: auto`), donc tous
        les inputs d'une rangee partagent la meme ligne de base, quel que soit
        le nombre de lignes du libelle ;
     3. les libelles reservent deux lignes (`min-height: 2.6em`) pour que les
        rangees ne sautent pas quand un texte passe de une a deux lignes.
   ========================================================================= */
.rg-shell { display: grid; grid-template-columns: 236px minmax(0, 1fr);
  gap: 20px; align-items: start; }
/* Le deroulant des sections n'existe que sur telephone (cree par le script,
   masque ici) : sur grand ecran la barre laterale les montre toutes. */
#rg-nav-select { display: none; }

/* -- navigation -- */
.rg-nav { position: sticky; top: 0; display: flex; flex-direction: column;
  gap: 2px; background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 10px; padding: 10px; }
.rg-nav-titre { font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--muted); font-weight: 700; padding: 2px 8px 8px; }
.rg-nav-item { display: flex; align-items: center; gap: 10px; width: 100%;
  background: none; border: 0; border-radius: 7px; padding: 8px 10px;
  cursor: pointer; text-align: left; color: inherit; font: inherit;
  transition: background .12s ease; }
.rg-nav-item:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
.rg-nav-item.active { background: color-mix(in srgb, var(--accent) 15%, transparent);
  box-shadow: inset 2px 0 0 var(--accent); }
.rg-nav-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.rg-nav-item > span:last-child { display: flex; flex-direction: column;
  line-height: 1.3; min-width: 0; }
.rg-nav-item b { font-size: 12.5px; font-weight: 600; }
.rg-nav-item em { font-size: 10.5px; font-style: normal; color: var(--muted); }
.rg-nav-item.active b { color: var(--accent); }
.rg-nav-ico { font-size: 15px; width: 20px; text-align: center; flex: none; }

/* -- corps : le bandeau d'etat est DANS le flux, au-dessus du titre -- */
.rg-corps { min-width: 0; display: flex; flex-direction: column; gap: 14px; }
.rg-entete { display: flex; justify-content: flex-end; align-items: center;
  gap: 8px; flex-wrap: wrap; min-height: 22px; }
.rg-etat { display: flex; gap: 6px; flex-wrap: wrap; align-items: center;
  justify-content: flex-end; }
.rg-puce { font-size: 10.5px; letter-spacing: .03em; padding: 3px 10px;
  border-radius: 99px; border: 1px solid var(--border); color: var(--muted);
  white-space: nowrap; }
.rg-puce.on { color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, transparent); }
.rg-flash { font-size: 11px; color: var(--accent); white-space: nowrap;
  animation: rgfade 2.2s ease forwards; }
@keyframes rgfade { 0%, 70% { opacity: 1; } 100% { opacity: 0; } }

.rg-titre h2 { margin: 0 0 3px; font-size: 17px; letter-spacing: -.01em;
  line-height: 1.2; }
.rg-titre p { margin: 0; font-size: 12px; color: var(--muted); line-height: 1.45; }

.rg-carte { background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 10px; padding: 16px; }
.rg-carte + .rg-carte { margin-top: 12px; }
.rg-carte h3 { margin: 0 0 12px; font-size: 12.5px; font-weight: 600; }
.rg-carte > p.muted { margin: -6px 0 14px; font-size: 11.5px; line-height: 1.5; }

/* -- grille des alertes -- */
.rg-table { width: 100%; border-collapse: collapse; font-size: 12.5px;
  table-layout: fixed; }
.rg-table th { text-align: left; font-weight: 600; font-size: 10px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--muted);
  padding: 0 8px 9px; border-bottom: 1px solid var(--border); }
.rg-table th:not(:first-child) { text-align: center; width: 122px; }
.rg-table td { padding: 8px; vertical-align: middle;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 45%, transparent); }
.rg-table td.rg-case { text-align: center; }
.rg-table tr:last-child td { border-bottom: none; }
.rg-crit { display: inline-block; font-size: 9px; text-transform: uppercase;
  letter-spacing: .1em; color: var(--critical);
  border: 1px solid color-mix(in srgb, var(--critical) 40%, transparent);
  border-radius: 3px; padding: 1px 5px; margin-left: 7px; vertical-align: 1px; }

.rg-switch { display: inline-block; position: relative; width: 34px; height: 19px;
  cursor: pointer; vertical-align: middle; }
.rg-switch input { position: absolute; opacity: 0; width: 100%; height: 100%;
  margin: 0; cursor: pointer; }
.rg-switch i { position: absolute; inset: 0; border-radius: 99px;
  background: color-mix(in srgb, var(--muted) 30%, transparent);
  transition: background .15s ease; pointer-events: none; }
.rg-switch i::after { content: ""; position: absolute; top: 2px; left: 2px;
  width: 15px; height: 15px; border-radius: 50%; background: #fff;
  transition: transform .15s ease; }
.rg-switch input:checked + i { background: var(--accent); }
.rg-switch input:checked + i::after { transform: translateX(15px); }
.rg-switch input:focus-visible + i { outline: 2px solid var(--accent);
  outline-offset: 2px; }

/* -- CHAMPS : c'est ici que se joue l'alignement -- */
.rg-champs { display: grid; gap: 16px 18px; align-items: stretch;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.rg-champs .field { display: flex; flex-direction: column; height: 100%;
  min-width: 0; margin: 0; }
.rg-champs .field label { display: flex; align-items: flex-start; gap: 6px;
  font-size: 11px; line-height: 1.3; margin: 0 0 5px;
  min-height: 2.6em;              /* deux lignes reservees : rangees stables */
  text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.rg-champs .field input { width: 100%; margin-top: auto; }
.rg-bornes { margin-left: auto; font-size: 10px; color: var(--muted);
  font-variant-numeric: tabular-nums; white-space: nowrap; flex: none;
  text-transform: none; letter-spacing: 0; }
/* Cases a cocher : toute la largeur, jamais en colonne a cote d'un champ. */
.rg-inline { display: flex; align-items: center; gap: 9px; font-size: 12.5px;
  cursor: pointer; padding: 6px 0; }
.rg-inline.rg-large { grid-column: 1 / -1; }
.rg-inline input[type="checkbox"] { flex: none; margin: 0; }
.rg-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 16px; padding-top: 13px;
  border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); }
.rg-actions .muted { font-size: 11px; }
.rg-reco { color: var(--warning, #d0a24c); cursor: help; flex: none; }

/* -- destinataires -- */
.rg-puces { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 13px;
  min-height: 28px; align-items: center; }
.rg-adresse { display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; padding: 4px 5px 4px 11px; border-radius: 99px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); }
.rg-x { display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; background: none; border: 0; color: var(--muted);
  cursor: pointer; font-size: 15px; line-height: 1; padding: 0; border-radius: 50%; }
.rg-x:hover { color: var(--critical);
  background: color-mix(in srgb, var(--critical) 15%, transparent); }
.rg-ajout { display: flex; gap: 8px; max-width: 470px; }
.rg-ajout input { flex: 1; min-width: 0; }
.rg-ajout .btn { flex: none; }
.rg-carnet { margin-top: 15px; padding-top: 13px; display: flex; gap: 7px;
  flex-wrap: wrap; align-items: center;
  border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); }
.rg-carnet .muted { font-size: 11px; margin-right: 3px; }
.rg-rappel { font-size: 11.5px; padding: 4px 11px; border-radius: 99px;
  background: none; border: 1px dashed var(--border); color: var(--muted);
  cursor: pointer; font-family: inherit; }
.rg-rappel:hover { color: var(--accent); border-color: var(--accent);
  border-style: solid; }

/* -- regions : cartes de hauteur EGALE, alignees en grille -- */
.rg-regions { display: grid; gap: 8px; align-items: stretch;
  grid-template-columns: repeat(auto-fill, minmax(186px, 1fr)); }
.rg-region { display: flex; flex-direction: column; justify-content: center;
  gap: 2px; text-align: left; padding: 10px 13px; border-radius: 8px;
  background: none; cursor: pointer; border: 1px solid var(--border);
  color: inherit; font: inherit; min-height: 52px;
  transition: border-color .12s ease, background .12s ease; }
.rg-region:hover { border-color: var(--accent); }
.rg-region.active { border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent); }
.rg-region b { font-size: 12.5px; }
.rg-region.active b { color: var(--accent); }
.rg-region em { font-size: 10.5px; font-style: normal; color: var(--muted);
  font-variant-numeric: tabular-nums; }

/* -- divers -- */
.rg-essais { display: flex; gap: 8px; flex-wrap: wrap; }
.rg-etat-secret { font-size: 10px; text-transform: uppercase; letter-spacing: .08em;
  color: var(--muted); margin-left: auto; flex: none; }
.rg-etat-secret.on { color: var(--accent); }
.rg-avert { font-size: 12px; line-height: 1.55;
  border-left: 3px solid var(--warning, #d0a24c); padding: 11px 15px;
  background: color-mix(in srgb, var(--warning, #d0a24c) 8%, transparent);
  border-radius: 0 6px 6px 0; }
.rg-avert code { font-size: 11px; }

/* -- RESPONSIVE : memes ruptures que le reste de l'application -- */
@media (max-width: 860px) {
  .rg-shell { grid-template-columns: 1fr; gap: 12px; }
  /* Le menu des sections devient une barre qui DEFILE. Rien ne le disait :
     une section ajoutee en bout de liste (Telegram, 04.09.2026) sortait de
     l'ecran et paraissait absente. Le degrade sur le bord droit montre qu'il
     reste quelque chose a atteindre ; il s'efface quand on arrive au bout
     (mask-image sur le conteneur qui defile). */
  .rg-nav { position: static; flex-direction: row; overflow-x: auto;
    padding: 7px; gap: 5px; -webkit-overflow-scrolling: touch;
    scroll-snap-type: x proximity; }
  /* Le degrade n'apparait que s'il RESTE quelque chose a droite : la classe
     est posee par le script au defilement. Un masque permanent aurait rogne
     la derniere section une fois qu'on l'a atteinte. */
  .rg-nav.rg-deborde {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 26px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 26px), transparent); }
  .rg-nav::-webkit-scrollbar { display: none; }
  .rg-nav-item { scroll-snap-align: start; }
  .rg-nav-titre { display: none; }
  .rg-nav-item { flex: 0 0 auto; padding: 7px 12px; }
  .rg-nav-item em { display: none; }
  .rg-nav-item.active { box-shadow: inset 0 -2px 0 var(--accent); }

  /* Menu deroulant des sections : il REMPLACE la barre sur telephone. Dix
     sections dont sept hors ecran, rien ne disait qu'on pouvait faire glisser
     — la nouveaute passait pour absente (retour du 06.09). Le deroulant montre
     toujours la section courante. */
  .rg-nav { display: none; }
  #rg-nav-select {
    display: block; width: 100%; margin-bottom: 12px;
    font-size: 16px;            /* en dessous, iOS zoome la page a l'ouverture */
    min-height: 44px; padding: 10px 12px;
    border: 1px solid var(--border); border-radius: 9px;
    background: var(--surface-2); color: var(--text); }
  .rg-entete, .rg-etat { justify-content: flex-start; }
  .rg-table th:not(:first-child) { width: 84px; }
  .rg-carte { padding: 13px; }
}

@media (max-width: 560px) {
  .rg-champs { grid-template-columns: 1fr; gap: 13px; }
  .rg-champs .field label { min-height: 0; }   /* une colonne : plus de rangee a aligner */
  .rg-regions { grid-template-columns: 1fr 1fr; }
  .rg-table { font-size: 11.5px; }
  .rg-table th:not(:first-child) { width: 62px; }
  .rg-table th, .rg-table td { padding-left: 4px; padding-right: 4px; }
  .rg-crit { display: none; }
  .rg-ajout { flex-direction: column; max-width: none; }
  .rg-ajout .btn, .rg-essais .btn { width: 100%; }
  .rg-essais { flex-direction: column; }
  .rg-titre h2 { font-size: 15.5px; }
}

/* ---- Mon compte : abonnement personnel aux alertes ---- */
.cpt-alertes .cpt-ab { padding: 11px 0;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 45%, transparent); }
.cpt-alertes .cpt-ab:last-of-type { border-bottom: none; }
.cpt-ab-tete { display: flex; align-items: center; gap: 9px; cursor: pointer;
  margin-bottom: 8px; font-size: 13px; }
.cpt-ab-tete input { flex: none; margin: 0; }
.cpt-ab input[type="email"], .cpt-ab input[type="text"] { width: 100%; }
/* Un champ actif sous une case decochee laisse croire qu'il sert : on le grise. */
.cpt-ab input:disabled { opacity: .45; cursor: not-allowed; }
.cpt-ab .cpt-note { margin-top: 6px; line-height: 1.45; }
.cpt-ab-actions { display: flex; align-items: center; gap: 12px; margin-top: 14px;
  padding-top: 12px; flex-wrap: wrap;
  border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); }
.cpt-ab-actions .muted, .cpt-al-err { font-size: 11.5px; }
.cpt-al-err { color: var(--critical); }

/* Avertissement qui n'apparait qu'au moment ou l'on DECOCHE une sortie
   collective (Telegram) : discret, mais visible. */
.cpt-avert { margin-top: 7px; padding: 7px 9px; border-radius: 7px; line-height: 1.5;
  color: var(--warning, #d8a657);
  background: color-mix(in srgb, var(--warning, #d8a657) 10%, transparent);
  border: 1px solid color-mix(in srgb, var(--warning, #d8a657) 32%, transparent); }

/* Grille personnelle des conditions (Mon compte > Mes alertes) */
.cpt-kinds { display: grid; gap: 6px 14px; margin: 8px 0 2px;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
.cpt-kind { display: flex; align-items: center; gap: 8px; font-size: 12px;
  cursor: pointer; padding: 3px 0; min-width: 0; }
.cpt-kind input { flex: none; margin: 0; }
.cpt-kind span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cpt-kind-crit { font-size: 9px; font-style: normal; text-transform: uppercase;
  letter-spacing: .09em; color: var(--critical); flex: none; }
/* Grise sans masquer : l'information reste lisible, elle n'est juste pas active. */
.cpt-inactif { opacity: .42; }
.cpt-alertes input[readonly] { cursor: text; opacity: .8; }
@media (max-width: 560px) { .cpt-kinds { grid-template-columns: 1fr; } }
.rg-note { font-size: 11.5px; color: var(--muted); line-height: 1.5;
  margin: 0 0 12px; padding-left: 10px;
  border-left: 2px solid color-mix(in srgb, var(--accent) 35%, transparent); }
/* Condition coupee pour tout le reseau : visible mais inoperante.
   pointer-events sur le LABEL : un label reste cliquable meme quand sa case est
   disabled — le clic sur le texte basculait donc la case malgre tout. */
.cpt-kind-off { opacity: .5; pointer-events: none; }
.cpt-kind-off input { cursor: not-allowed; }
.cpt-kind-coupe { font-size: 9px; font-style: normal; text-transform: uppercase;
  letter-spacing: .09em; color: var(--muted); flex: none;
  border: 1px solid var(--border); border-radius: 3px; padding: 0 4px; }
/* Le conteneur devient l'element de grille : il doit se comporter comme le
   label qu'il enveloppe, sinon les colonnes s'ecrasent. */
.cpt-kind-hote { display: block; min-width: 0; }
.cpt-kind-hote > .cpt-kind { width: 100%; }

/* ---- Comptes : LIGNES denses (le parc vise 20 a 30 comptes) --------------
   Les cartes faisaient 200 px de haut : trois par ligne, il fallait defiler
   pour trouver quelqu'un. Une ligne de 34 px en montre une vingtaine d'un coup.
   Colonnes FIXES pour que les noms, les roles et les droits s'alignent
   verticalement — c'est ce qui rend une liste balayable plutot que lue. */
.au-liste { display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.au-l { display: grid; align-items: center; gap: 10px;
  grid-template-columns: 24px minmax(120px, 1.4fr) minmax(90px, 1fr) 54px 26px 10px auto;
  padding: 5px 12px; font-size: 12.5px;
  border-bottom: 1px solid color-mix(in srgb, var(--border) 45%, transparent); }
.au-l:last-child { border-bottom: none; }
.au-l:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
.au-l.off { opacity: .55; }

.au-l-av { width: 24px; height: 24px; border-radius: 50%; display: flex;
  align-items: center; justify-content: center; font-size: 11px; font-weight: 700;
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--accent); flex: none; }
.au-l-nom { font-weight: 600; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
.au-l-nom em { font-style: normal; font-size: 9.5px; text-transform: uppercase;
  letter-spacing: .09em; color: var(--accent); margin-left: 6px; }
.au-l-role { color: var(--muted); overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: 11.5px; }
.au-l-droits { font-variant-numeric: tabular-nums; text-align: right;
  font-size: 11.5px; color: var(--muted); }
.au-l-droits i { font-style: normal; opacity: .55; }
.au-l-2fa { text-align: center; font-size: 11px; color: var(--muted); }
.au-l-2fa.on { color: var(--accent); }
/* Pastille d'etat : la couleur ET la place disent l'etat, sans mot a lire. */
.au-l-etat { width: 7px; height: 7px; border-radius: 50%; justify-self: center; }
.au-l-etat.on { background: var(--accent); }
.au-l-etat.off { background: var(--muted); opacity: .5; }
.au-l-act { display: flex; gap: 5px; align-items: center; justify-self: end; }
.au-l-act .btn { padding: 3px 10px; font-size: 11.5px; }
.au-l-act .au-icon { padding: 3px 7px; font-size: 12px; }

@media (max-width: 860px) {
  /* Le role et le compteur de droits sortent : le nom et les actions priment. */
  .au-l { grid-template-columns: 24px 1fr 26px 10px auto; }
  .au-l-role, .au-l-droits { display: none; }
}
.admin-filtre { width: 100%; max-width: 320px; margin-bottom: 10px; }
.admin-filtre.vide { border-color: var(--critical); }
.rg-avert-doublon { margin: 12px 0 0; font-size: 11.5px; }
.rg-avert-doublon > div { margin-bottom: 4px; }
.rg-avert-note { opacity: .8; margin-top: 6px; }

/* ---- Floodeurs : qui fait monter notre duty cycle ------------------------ */
/* Vue distincte de « Bavards » : ici on mesure le cout radio (taille x sauts)
   de TOUS les types de messages, pas seulement les adverts. */
.duty-chk { margin-left: 14px; font-size: 13px; color: var(--muted); cursor: pointer; }
.duty-chk input { margin-right: 5px; vertical-align: middle; }

.duty-scopes { margin: 8px 0 12px; }
.duty-scopes h3 { margin: 0 0 8px; font-size: 14px; font-weight: 600; }
.duty-scope-list { display: flex; flex-direction: column; gap: 3px; }
.duty-sc { display: grid; grid-template-columns: 110px 1fr 130px; align-items: center; gap: 9px; }
.duty-sc-nom { font-size: 13px; font-weight: 600; }
.duty-sc-bar { background: var(--grid); border-radius: 3px; height: 11px; overflow: hidden; }
.duty-sc-fill { height: 100%; border-radius: 3px; }
/* Un scope a nous n'est jamais un candidat au reglage : la couleur le dit. */
.duty-sc-tiers { background: var(--critical); }
.duty-sc-notre { background: var(--good); }
.duty-sc-val { font-size: 12px; text-align: right; font-variant-numeric: tabular-nums; }
.duty-badge-notre { font-size: 10px; background: var(--good); color: #fff;
  padding: 1px 5px; border-radius: 3px; vertical-align: middle; }

.duty-card { border: 1px solid var(--hairline); border-left: 3px solid var(--warning);
  border-radius: 5px; padding: 7px 10px; margin-bottom: 6px; background: var(--surface); }
.duty-card.duty-grave { border-left-color: var(--critical); }
.duty-card.duty-traite { opacity: .62; }
.duty-head { display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; flex-wrap: wrap; }
.duty-titre { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.duty-emp { font-family: var(--mono); font-weight: 700; font-size: 13px;
  background: var(--grid); padding: 0 5px; border-radius: 3px; }
.duty-nom { font-weight: 600; }
.duty-anon { color: var(--muted); font-style: italic; }
.duty-score { font-size: 11px; color: var(--muted); border: 1px solid var(--hairline);
  border-radius: 3px; padding: 0 4px; }
.duty-reveil { font-size: 11px; background: var(--critical); color: #fff;
  padding: 1px 6px; border-radius: 3px; }
.duty-cout { font-size: 13px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.duty-meta { font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.duty-motifs { margin: 4px 0 2px; padding-left: 16px; font-size: 12px; }
.duty-motifs li { margin-bottom: 1px; }
.duty-conf { font-size: 11.5px; font-style: italic; color: var(--muted); margin: 2px 0; }
.duty-det { margin: 3px 0; font-size: 12px; }
.duty-det summary { cursor: pointer; color: var(--muted); }
.duty-cand { border-left: 3px solid var(--hairline); padding: 5px 9px; margin: 6px 0; }
.duty-cand.dc-credible { border-left-color: var(--warning); background: var(--grid); }
.duty-cand.dc-ecarte { opacity: .68; }
.dc-tete { display: flex; gap: 8px; align-items: baseline; }
.dc-score { font-size: 11px; color: var(--muted); margin-left: auto; }
.dc-raisons { margin: 3px 0 0; padding-left: 15px; font-size: 12px; list-style: none; }
.dc-pour { color: var(--good-text); }
.dc-contre { color: var(--critical); }
.duty-decision { font-size: 11.5px; margin-top: 4px; padding: 4px 7px;
  background: var(--grid); border-radius: 4px; }
.duty-actions { display: flex; gap: 7px; align-items: center; margin-top: 5px; flex-wrap: wrap; }
.duty-noban { font-size: 12px; font-style: italic; }
.duty-vide { padding: 20px; text-align: center; }

@media (max-width: 720px) {
  .duty-sc { grid-template-columns: 90px 1fr; }
  .duty-sc-val { grid-column: 2; text-align: left; }
  .duty-head { flex-direction: column; gap: 4px; }
}

/* Boutons de la carte : la taille par defaut des formulaires rendait chaque
   signalement trop haut pour qu'on en voie plusieurs a l'ecran. */
.duty-actions .btn { padding: 3px 9px; font-size: 12px; }

/* Preuves d'un signalement : cadence, chemin, trames reelles. */
.duty-cadence { font-size: 11.5px; margin: 3px 0; }
.duty-auto { background: var(--warning); color: var(--page); font-size: 10px;
  padding: 0 5px; border-radius: 3px; margin-left: 5px; }
.duty-voisins { font-size: 12px; }
.dv-l { display: grid; grid-template-columns: 34px 46px 1fr; gap: 7px;
  padding: 2px 0; align-items: baseline; }
.dv-emp { font-family: var(--mono); font-weight: 700; background: var(--grid);
  border-radius: 3px; text-align: center; }
.dv-part { font-variant-numeric: tabular-nums; color: var(--muted); text-align: right; }
.dv-noms { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dv-note { font-size: 11px; margin-top: 5px; font-style: italic; }
.duty-trames { width: 100%; border-collapse: collapse; font-size: 11.5px; margin-top: 4px; }
.duty-trames th { text-align: left; font-weight: 600; color: var(--muted);
  border-bottom: 1px solid var(--hairline); padding: 2px 6px 3px; }
.duty-trames td { padding: 2px 6px; border-bottom: 1px solid var(--hairline);
  font-variant-numeric: tabular-nums; }
/* Le chemin est long : il defile dans sa cellule au lieu d'elargir la page. */
.dt-path { font-family: var(--mono); font-size: 10.5px; max-width: 260px;
  overflow-x: auto; white-space: nowrap; }

/* Cle publique COMPLETE d'un candidat (16 hex) : identification exacte, par
   opposition a l'octet du chemin qui ne designe qu'un relais. */
.dv-cle { display: inline-block; margin-right: 8px; }
.dv-cle code { font-family: var(--mono); font-size: 10px; color: var(--muted);
  background: var(--grid); padding: 0 4px; border-radius: 3px; margin-left: 4px; }

/* --- Deploiement des regions (Reglages > Regions) ------------------------- */
.rgd-ref {
  border: 1px solid var(--hairline);
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 12px;
  background: var(--surface-2);
}
.rgd-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
.rgd-chip {
  font-family: var(--mono);
  font-size: 11px;
  padding: 2px 7px;
  border-radius: 10px;
  border: 1px solid var(--hairline);
  background: var(--surface);
  color: var(--ink-2);
}
.rgd-table { width: 100%; border-collapse: collapse; margin: 10px 0; }
.rgd-table th, .rgd-table td {
  padding: 7px 8px;
  border-bottom: 1px solid var(--hairline);
  text-align: left;
  vertical-align: middle;
}
.rgd-table th { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.rgd-table td.num, .rgd-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.rgd-table label { display: flex; align-items: center; gap: 7px; cursor: pointer; }
.rgd-ok      { color: var(--good-text); }
.rgd-manque  { color: var(--warning); }
.rgd-surplus { color: var(--critical); }
.rgd-propre  { color: var(--accent); font-size: 12px; }
.rgd-exc {
  font-size: 11px; padding: 1px 6px; border-radius: 9px;
  background: var(--accent-soft); color: var(--accent);
}
.rgd-opt { display: inline-flex; align-items: center; gap: 6px; margin-left: 10px; font-size: 13px; }
.rgd-camp {
  margin-top: 12px; padding: 10px 12px;
  border: 1px solid var(--hairline); border-radius: 8px; background: var(--surface-2);
}
.rgd-etat {
  font-size: 11px; padding: 2px 7px; border-radius: 10px;
  border: 1px solid var(--hairline); white-space: nowrap;
}
.rgd-ok, .rgd-conforme  { color: var(--good-text); }
.rgd-echec              { color: var(--critical); }
.rgd-partiel, .rgd-incertain { color: var(--warning); }
.rgd-lecture, .rgd-application { color: var(--accent); }
.rgd-table tr.rgd-mort td { opacity: .55; }
.rgd-mort-txt { color: var(--muted); font-style: italic; }

/* Compte qui n'a que le droit `regions` (ex. HB3YNV) : l'onglet Reglages lui
   est ouvert, mais REDUIT a la seule section Regions. Meme principe que
   body.admin-restreint pour l'onglet Administration. Le masquage est ici un
   confort d'affichage — c'est le serveur qui refuse les autres reglages. */
body.reglages-restreint .rg-nav-item { display: none; }
body.reglages-restreint .rg-nav-item[data-section="regions"] { display: flex; }

/* --- Grille regions x relais --------------------------------------------- */
.rgd-scroll { overflow-x: auto; margin: 10px 0; }
.rgd-grille { border-collapse: collapse; font-size: 13px; }
.rgd-grille th, .rgd-grille td {
  border: 1px solid var(--hairline);
  padding: 0;
}
/* En-tetes de colonnes a la verticale : 14 regions a plat ne tiennent pas, et
   une grille qui deborde de l'ecran ne se lit plus. */
.rgd-col {
  height: 96px; width: 30px; vertical-align: bottom; padding-bottom: 6px !important;
  background: var(--surface-2);
}
.rgd-col span {
  display: block; writing-mode: vertical-rl; transform: rotate(180deg);
  font-family: var(--mono); font-size: 11px; font-weight: 500;
  color: var(--ink-2); white-space: nowrap; margin: 0 auto;
}
.rgd-col-hors span { color: var(--accent); font-style: italic; }
.rgd-col-hors { background: var(--accent-soft); }
.rgd-node {
  text-align: left !important; padding: 6px 10px !important;
  white-space: nowrap; background: var(--surface-2);
  position: sticky; left: 0; z-index: 2;
}
.rgd-node label { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; }
.rgd-cell { text-align: center; width: 30px; }
.rgd-cell input { margin: 6px auto; display: block; cursor: pointer; }
.rgd-cell.rgd-add { background: rgba(12, 163, 12, 0.16); }
.rgd-cell.rgd-del { background: rgba(208, 59, 59, 0.16); }
.rgd-delta { padding: 6px 10px !important; white-space: nowrap; font-size: 12px; }
.rgd-ligne-tout th, .rgd-ligne-tout td { background: var(--surface-2); }
.rgd-mini {
  width: 100%; border: 0; background: none; cursor: pointer;
  font-family: var(--mono); font-size: 11px; color: var(--muted); padding: 4px 0;
}
.rgd-mini:hover { background: var(--accent-soft); color: var(--accent); }
.rgd-legende {
  display: flex; flex-wrap: wrap; gap: 14px; align-items: center;
  font-size: 12px; margin-top: 8px;
}
.rgd-legende i { display: inline-block; width: 11px; height: 11px; border-radius: 2px;
  vertical-align: -1px; margin-right: 4px; border: 1px solid var(--hairline); }
.rgd-sw.rgd-add { background: rgba(12, 163, 12, 0.35); }
.rgd-sw.rgd-del { background: rgba(208, 59, 59, 0.35); }
.rgd-morts { margin-top: 6px; font-size: 12px; }
.rgd-cell.rgd-muet { background: repeating-linear-gradient(45deg,
  transparent, transparent 3px, var(--hairline) 3px, var(--hairline) 6px); }
.rgd-sw.rgd-muet { background: repeating-linear-gradient(45deg,
  transparent, transparent 3px, var(--muted) 3px, var(--muted) 6px); }
/* Region parente : verrouillee tant que ses enfants sont la. Le motif est
   donne au clic sur la cellule (la case desactivee n'emet aucun clic). */
.rgd-cell.rgd-parent { background: var(--surface-2); }
.rgd-cell.rgd-parent input { opacity: .45; }
.rgd-sw.rgd-parent { background: var(--grid); }
.rgd-cell[data-bloc] { cursor: help; }

/* ===========================================================================
 * Reglages > Relais (grille radio) et Maintenance > Sauvegardes — 06.09.2026
 * ========================================================================= */
body.reglages-restreint.reglages-radio .rg-nav-item[data-section="relais"] { display: flex; }
body.reglages-restreint.reglages-stopflood .rg-nav-item[data-section="stopflood"] { display: flex; }
/* Sans le droit `stopflood`, la section n'apparait pas non plus en mode COMPLET :
   la regle ci-dessus ne joue qu'en mode restreint, et un admin-restreint sans ce
   droit verrait sinon une section que le serveur lui refuse. */
body:not(.reglages-stopflood) .rg-nav-item[data-section="stopflood"] { display: none; }

/* Zone de test de l'anti-flood : outil de validation, mise a l'ecart visuelle
   du pilotage courant pour qu'on ne la declenche pas par megarde. */
.sf-test { margin-top: 16px; padding: 10px 12px; border: 1px dashed var(--border);
  border-radius: 8px; background: var(--surface-2); }
.sf-test summary { cursor: pointer; font-weight: 600; }
.btn-sm { padding: 2px 8px; font-size: 12px; min-width: 0; }
.sf-seuil-cell { white-space: nowrap; }

/* Barre d'actions des deux grilles (Regions comprise : elle utilisait la
   classe sans qu'aucune regle n'existe, ses boutons s'empilaient a la ligne). */
.rgd-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 12px 0; }
.rgd-actions label { display: inline-flex; align-items: center; gap: 7px; font-size: 13px; }
.rgd-actions select { font: inherit; padding: 5px 8px; }

/* Cellules de saisie. `.rgd-cell` (grille des regions) impose « width: 30px »
   pour ses cases a cocher : la meme classe portant un champ de 5,5 em, le
   tableau debordait. La largeur est rendue au contenu ici. */
.rdg-table td.rdg-cell { width: auto; padding: 4px 6px; }
.rdg-table td.rdg-cell input, .rdg-table td.rdg-cell select {
  width: 5.5em; min-width: 4.5em; font: inherit; padding: 4px 6px; text-align: right; }
.rdg-table td.rdg-cell select { text-align: left; }
.rdg-table td.rdg-cell.rgd-add input, .rdg-table td.rdg-cell.rgd-add select { outline: 2px solid currentColor; }
.rdg-table th { white-space: nowrap; }
/* Ligne de reference : les valeurs du reseau, posables d'un appui. Les boutons
   heritent de .rgd-mini (grille des regions) mais portent une VALEUR, pas un
   compteur : ils ont besoin de respirer un peu plus. */
.rdg-table .rdg-ref .rgd-mini { font-size: 12px; padding: 5px 6px; color: var(--text); }
.rdg-table .rdg-ref .rgd-mini:hover { background: var(--accent-soft); color: var(--accent); }
.rdg-table td.rdg-ref-cell { text-align: center; padding: 2px 4px; }
/* Lecture d'un seul relais : le geste courant, on regle un relais a la fois.
   Discret au repos, il s'anime au survol de la ligne. */
/* Le nom et son etat d'un cote, le bouton de lecture de l'autre. */
.rdg-table td.rgd-node { display: flex; align-items: center; gap: 8px; justify-content: space-between; }
.rdg-nom { min-width: 0; }
.rdg-play { border: 0; background: none; cursor: pointer; padding: 0 6px;
  font-size: 12px; color: var(--muted); opacity: .55; transition: opacity .12s; }
.rdg-table tr:hover .rdg-play { opacity: 1; }
.rdg-play:hover { color: var(--accent); }
.rdg-play:disabled { cursor: default; opacity: .3; }
.rdg-play:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.rdg-table th small { font-weight: normal; opacity: .7; }
.sauv-table td .mono, .rgd-table td.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; }

/* -- TELEPHONE ------------------------------------------------------------
 * Meme seuil que le reste de l'application (860 px). Onze colonnes de reglages
 * ne tiennent pas sur un telephone : la grille devient une FICHE PAR RELAIS,
 * « intitule / champ », comme la telemetrie. Les intitules viennent de data-l,
 * pose par le rendu (rdgRend) et non par initMobile : ces tableaux sont
 * reconstruits a chaque tour de sondage, un balayage unique au chargement ne
 * les aurait jamais vus. */
@media (max-width: 860px) {
  .rgd-actions { gap: 7px; }
  .rgd-actions .btn { flex: 1 1 auto; min-height: 40px; }   /* cible tactile */

  .rdg-table, .rdg-table tbody, .rdg-table tr { display: block; width: 100%; }
  .rdg-table thead { display: none; }
  .rdg-table tr {
    border: 1px solid var(--hairline); border-radius: 9px;
    background: var(--surface-2); padding: 9px 11px; margin-bottom: 10px; }
  .rdg-table td, .rdg-table td.rdg-cell {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    width: auto; border: none; border-bottom: 1px solid var(--hairline);
    padding: 7px 0; text-align: left; }
  .rdg-table tr td:last-child { border-bottom: none; }
  .rdg-table td.rdg-cell::before {
    content: attr(data-l); flex: 1 1 auto; min-width: 0;
    color: var(--muted); font-size: 12px; }
  /* Le nom du relais reste en tete de fiche, en pleine largeur. */
  .rdg-table td:first-child { padding-bottom: 9px; font-size: 14px; }
  .rdg-table td:first-child .rdg-nom { flex: 1 1 auto; }
  .rdg-play { opacity: 1; min-width: 44px; min-height: 40px; font-size: 16px;
    border: 1px solid var(--border); border-radius: 8px; }
  /* 16px : en dessous, iOS ZOOME la page a la mise au point du champ et ne
     revient jamais en arriere — la grille se retrouve deux fois trop large. */
  .rdg-table td.rdg-cell input, .rdg-table td.rdg-cell select {
    flex: 0 0 auto; width: 6.5em; min-height: 40px; font-size: 16px; }
  /* La ligne « reference » n'a pas de sens en mode fiche (elle vaut pour tout
     le parc, pas pour un relais) : elle devient un bandeau de boutons en tete
     de grille, un par reglage, avec son intitule. */
  .rdg-table thead { display: block; }
  .rdg-table thead tr:first-child { display: none; }
  .rdg-table tr.rdg-ref { display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
    border: 1px dashed var(--border); background: none; margin-bottom: 12px; }
  .rdg-table tr.rdg-ref th.rgd-node {
    flex: 1 0 100%; position: static; background: none;
    font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
  .rdg-table tr.rdg-ref td.rdg-ref-cell { display: block; width: auto; border: none; padding: 0; }
  .rdg-table tr.rdg-ref .rgd-mini {
    width: auto; min-height: 36px; padding: 6px 10px;
    border: 1px solid var(--border); border-radius: 7px; }
  .rdg-table tr.rdg-ref td.rdg-ref-cell::before {
    content: attr(data-l); display: block; font-size: 10px; color: var(--muted);
    margin-bottom: 2px; }

  /* La teinte « modifie / refuse » sur une ligne de fiche : un liseré a
     gauche se lit mieux qu'un fond, qui bavait sur toute la largeur. */
  .rdg-table td.rdg-cell.rgd-add, .rdg-table td.rdg-cell.rgd-del {
    background: none; box-shadow: inset 3px 0 0 currentColor; padding-left: 8px; }

  /* Recapitulatif de campagne et sauvegardes : tableaux etroits mais qui
     debordent quand meme sur un telephone -> on les laisse defiler seuls
     plutot que de pousser la page entiere de cote. */
  .rgd-camp { padding: 9px 10px; overflow-x: auto; }
  .rgd-camp .rgd-table, .sauv-table { font-size: 12px; min-width: 480px; }
  .rgd-camp table, .sauv-table { display: table; }
  #sauv-corps .rgd-scroll, #sauv-detail .rgd-scroll { -webkit-overflow-scrolling: touch; }
  #sauv-detail table { min-width: 420px; }
}
