/* NPV_Tools — charte Dstny (cf. skill dstny-brand-guidelines) */
:root {
    --dstny-teal: #00A5B5;
    --dstny-teal-wcag: #118199;
    --dstny-orange: #FF9015;
    --dstny-purple: #B41284;
    --dstny-dark-blue: #005C87;
    --dstny-dark: #38344D;
    --dstny-black: #261616;

    --surface: #fcfcfb;
    --page: #f4f6f7;
    --border: rgba(11, 11, 11, 0.10);
    --text-secondary: #52514e;
    --text-muted: #898781;

    --good: #0ca30c;
    --warning: #fab219;
    --critical: #d03b3b;
}

* { box-sizing: border-box; }

body {
    margin: 0;
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    background: var(--page);
    color: var(--dstny-black);
    font-size: 15px;
    line-height: 1.5;
}

a { color: var(--dstny-dark-blue); }

.npv-nav {
    background: var(--dstny-teal-wcag);
    color: #fff;
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 0 16px;
}
.npv-nav .brand {
    font-weight: 700;
    padding: 14px 16px 14px 0;
    margin-right: 8px;
    letter-spacing: 0.02em;
}
.npv-nav a {
    color: #fff;
    text-decoration: none;
    padding: 14px 12px;
    opacity: 0.85;
    border-bottom: 3px solid transparent;
}
.npv-nav a:hover { opacity: 1; }
.npv-nav a.active {
    opacity: 1;
    border-bottom-color: var(--dstny-orange);
    font-weight: 600;
}
.npv-nav .spacer { flex: 1; }
.npv-nav .user { opacity: 0.85; padding: 14px 8px; font-size: 13px; }
.scan-badge {
    display: inline-flex; align-items: center; gap: 6px;
    background: rgba(255,255,255,0.14); border-radius: 20px;
    padding: 5px 12px; font-size: 12.5px; white-space: nowrap;
}
.scan-spinner {
    display: inline-block; width: 12px; height: 12px;
    border: 2px solid rgba(255,255,255,0.4); border-top-color: #fff;
    border-radius: 50%; animation: npv-spin 0.8s linear infinite;
}

main { max-width: 1180px; margin: 0 auto; padding: 24px 20px 48px; }
/* Page Graphiques (2026-09-14, demande explicite : "utilise 20% de largeur
   de page en plus") : 1180px * 1.2 = 1416px, scopé à cette seule page via
   body.npv-page-graphiques (cf. _layout.php) — les autres pages (Vue
   liste/Imports/Admin) gardent 1180px. */
body.npv-page-graphiques main { max-width: 1416px; }

/* Page Dashboard (2026-09-17, demande explicite : "utiliser tout l'espace en
   largeur disponible et faire en gros 2 colonnes, c'est à dire pouvoir avoir
   2 produit qui se partage la meme ligne") — plus large que Graphiques
   (1416px) : il faut de la place pour 2 grilles de 3 vignettes CHACUNE côte à
   côte (cf. .dashboard-produits-grid plus bas). 2200px dépasse la largeur de
   la quasi-totalité des écrans de bureau réels (jusqu'à ~1920px) : `main`
   utilise donc de fait "tout l'espace disponible" sur ces écrans (min(max-
   width, largeur dispo) retombe sur la largeur dispo), tout en évitant des
   lignes démesurées sur un moniteur ultra-large. */
body.npv-page-dashboard main { max-width: 2200px; }

h1 { font-size: 22px; margin: 0 0 4px; color: var(--dstny-dark); }
h2 { font-size: 17px; margin: 28px 0 12px; color: var(--dstny-dark); }
h3 { font-size: 14px; margin: 18px 0 8px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.03em; }
/* Sous-titre à l'intérieur d'une .card (2026-08-26, "Top partenaires" : deux
   graphiques empilés dans un même modal ont besoin d'un intitulé chacun,
   distinct du titre h3 du modal lui-même). */
h4 { font-size: 13px; margin: 0 0 6px; color: var(--dstny-dark); font-weight: 600; }
.subtitle { color: var(--text-secondary); margin: 0 0 24px; font-size: 14px; }

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 18px 20px;
    margin-bottom: 18px;
}

.stat-row { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 20px; }
/* Tuiles déplacées dans la colonne de droite d'un modal (2026-09-30, cf.
   npvPlaceStatTiles, npv-shared-charts.js) : une tuile par ligne, pleine
   largeur de la colonne. */
.stat-row.stat-row-side { flex-direction: column; flex-wrap: nowrap; gap: 10px; margin-bottom: 0; }
.stat-row.stat-row-side .stat-tile { min-width: 0; width: 100%; box-sizing: border-box; }

/* Bandeau fixe "Évolution du nombre de service" du Dashboard (2026-09-17,
   cf. dashboard.php pour le détail) — `position: sticky` plutôt que `fixed` :
   reste ancré en haut de `main` tant qu'on n'a pas scrollé jusqu'à lui, PUIS
   colle au sommet de la fenêtre en scrollant dans les favoris plus bas, sans
   jamais se superposer au contenu situé AU-DESSUS de lui (h1, etc.) —
   contrairement à `fixed`, qui aurait fallu compenser par un padding-top
   artificiel. z-index au-dessus du contenu qui défile dessous, mais sous les
   modals/l'overlay de soumission (jusqu'à 2000, cf. plus bas dans ce fichier).
   Fond opaque (var(--surface), pas transparent) : indispensable pour un
   élément sticky, sous peine de voir le contenu qui défile dessous à travers. */
.dash-evo-banner {
    position: sticky;
    top: 0;
    z-index: 50;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 14px 18px 16px;
    margin-bottom: 24px;
    box-shadow: 0 2px 10px rgba(38, 22, 22, 0.08);
}
/* 2026-09-18 (v141, retour explicite avec capture annotée : "Il va falloir
   réduire la hauteur de la zone avec les stats. remonte les stats sur la
   meme ligne que le titre") : .dash-evo-banner-toolbar (période, dates,
   période résolue) vit maintenant DANS ce head, à côté du <h2> sur la même
   ligne au lieu d'une rangée séparée juste en dessous -- une rangée de
   hauteur en moins avant la grille de colonnes. `align-items: center`
   (plutôt que `baseline` comme avant, quand ce head ne contenait que le
   <h2>) : aligne proprement le titre avec les selects/inputs de la
   toolbar, de hauteur différente. Reste en flex-wrap : sur petit écran, la
   toolbar repasse sous le titre plutôt que déborder (cf. media query plus
   bas), même comportement visuel qu'avant ce correctif. */
.dash-evo-banner-head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 12px 20px; flex-wrap: wrap; margin-bottom: 12px;
}
.dash-evo-banner-head h2 { margin: 0; }
.dash-evo-banner-period { color: var(--text-secondary); font-size: 13px; white-space: nowrap; }
/* Barre d'outils du bandeau (2026-09-18, v140 ; recomposée v141, cf.
   commentaire de .dash-evo-banner-head ci-dessus) : sélecteur de période +
   dates personnalisées + période résolue affichée, sur une seule ligne qui
   passe à la ligne (flex-wrap) plutôt que déborder sur petit écran — reprend
   les mêmes contrôles/ids que #periode sur graphiques.php (npvDateRangeFrom),
   juste avec un style plus compact adapté à un simple bandeau. */
.dash-evo-banner-toolbar {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
}
/* Étiquette à CÔTÉ de son contrôle (v141), plus au-dessus (deux lignes par
   champ avant ce correctif) : c'est la moitié de la hauteur supprimée par
   le passage de la toolbar sur la ligne du titre qui aurait été perdue si
   chaque champ était resté sur deux lignes. */
.dash-evo-periode-field { display: flex; align-items: center; gap: 6px; }
.dash-evo-periode-field label { font-size: 12.5px; color: var(--text-secondary); white-space: nowrap; }
.dash-evo-periode-field select,
.dash-evo-periode-field input[type=date] {
    padding: 5px 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    font-size: 13.5px;
    background: #fff;
}

/* Carrousel du bandeau (2026-09-22, retour explicite : "TU ne peux pas faire
   un caroussel ? [...] bascule toutes les 5s, vers le nouveau bandeau (à la
   meme place)") — 2 diapositives dans le MÊME bandeau : la vue par produit
   existante (.cols-grid, ci-dessous, inchangée) et une nouvelle vue "SIP
   Trunk — types d'équipement les plus présents" (.stat-row/.stat-tile, même
   gabarit que npvRenderStatTiles partout ailleurs). Cf. dashboard.php
   ("Carrousel du bandeau") pour le JS (auto 5s, flèches, pause au survol,
   respecte prefers-reduced-motion). N'apparaît QUE si SIP Trunk a des
   données — sinon .dash-evo-carousel ne contient qu'une seule diapo, sans
   flèches/dots (ceux-ci ne sont alors pas rendus par PHP), et se comporte
   visuellement comme un simple conteneur (ce bloc CSS reste inerte). */
.dash-evo-carousel { display: flex; align-items: stretch; gap: 8px; }
/* Hauteur du bandeau fixée sur la PLUS HAUTE des 2 diapos (2026-09-22, v157,
   retour explicite : "la hauteur du bandeau soit fixé en prenant en compte
   le bandeau le plus haut. Car là dès que le caroussel bouge, ca fait bouger
   toute la page car un des bandeau statistique est moins haut que l'autre")
   — les 2 diapos partagent maintenant la MÊME cellule de grille CSS
   (`grid-area: 1/1` sur chacune, ci-dessous), au lieu du `display:none` sur
   la diapo inactive qu'utilisait la v154 : une diapo cachée par `display`
   sort entièrement du flux et ne compte plus dans la hauteur du conteneur,
   qui se réduisait donc à la SEULE diapo affichée (2 hauteurs différentes
   selon la diapo -> le bandeau, et toute la page sous lui, "sautait" à
   chaque bascule). Une ligne de grille se dimensionne au contraire sur son
   contenu le plus haut, MÊME quand ce contenu n'est pas la diapo visible —
   la diapo inactive reste donc dans le flux (juste masquée visuellement via
   `visibility`/`opacity`, jamais `display`, et neutralisée aux interactions
   via `pointer-events`), et le bandeau garde EN PERMANENCE la hauteur de sa
   plus haute diapo. Purement CSS, donc reste correct sans aucune mesure JS
   même si le contenu d'une diapo change de hauteur plus tard (ex. plus de
   types d'équipement). */
.dash-evo-carousel-viewport {
    flex: 1 1 auto; min-width: 0; position: relative;
    display: grid;
    /* v158 (2026-09-22, retour explicite : "peux tu centrer verticalement
       les statistiques ?") — sans ceci, l'`align-items` par défaut d'une
       grille CSS ("stretch") étire CHAQUE diapo sur toute la hauteur de la
       ligne (fixée sur la plus haute des deux depuis la v157, cf. plus
       haut), laissant son propre contenu collé en haut avec du vide en
       dessous quand cette diapo est plus courte que l'autre. `center` fait
       garder à chaque diapo sa hauteur naturelle et la centre verticalement
       dans cet espace au lieu de l'étirer. */
    align-items: center;
}
.dash-evo-carousel-slide {
    grid-area: 1 / 1 / 2 / 2;
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
}
.dash-evo-carousel-slide.active {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
    animation: npvFadeIn 0.25s ease;
}
@media (prefers-reduced-motion: reduce) {
    .dash-evo-carousel-slide.active { animation: none; }
}
@keyframes npvFadeIn { from { opacity: 0; } to { opacity: 1; } }
/* Diapo 2 du carrousel ("SIP Trunk — types d'équipement les plus présents")
   (2026-09-22, retour explicite : "peux tu centrer les nouvelles
   statistiques, là elles sont justifié à gauche") — centrée horizontalement,
   contrairement à .stat-row par défaut (justifiée à gauche partout ailleurs
   dans l'app, ex. "Types d'équipement" sur Graphiques) : ciblé sur son id
   précis plutôt que sur .stat-row lui-même, pour ne changer QUE cette
   vignette-ci. Son titre a migré dans le <h2> du bandeau (cf. dashboard.php,
   data-title), donc plus de légende séparée ici. */
#dash-evo-sip-types-tiles { justify-content: center; }
.dash-evo-carousel-arrow {
    flex: none; align-self: center;
    width: 30px; height: 30px; border-radius: 50%;
    border: 1px solid var(--border); background: var(--surface);
    color: var(--text-secondary); font-size: 16px; line-height: 1;
    cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.dash-evo-carousel-arrow:hover { background: rgba(0, 165, 181, 0.08); color: var(--dstny-teal-wcag); }
/* Bouton pause (2026-09-22, v156, retour explicite : "Ajouter un bouton
   'pause'") — même gabarit que les flèches (.dash-evo-carousel-arrow) ; état
   actif (paused = true, cf. dashboard.php/setPaused) mis en évidence par la
   couleur d'accent, en plus du changement d'icône ⏸ -> ▶ déjà suffisant pour
   comprendre l'état sans dépendre de la seule couleur. */
.dash-evo-carousel-pause.is-paused { color: var(--dstny-teal-wcag); border-color: var(--dstny-teal-wcag); }
.dash-evo-carousel-dots { display: flex; justify-content: center; gap: 7px; margin-top: 10px; }
.dash-evo-carousel-dots .dot {
    width: 7px; height: 7px; border-radius: 50%; padding: 0;
    border: none; background: var(--border); cursor: pointer;
}
.dash-evo-carousel-dots .dot.active { background: var(--dstny-teal-wcag); }

/* Grille "3 colonnes hausse / stable / baisse" (2026-09-18, v140, retour
   explicite : "peux tu me faire 'une ligne par produit', mais avec 3
   colonnes ? [...] Faudrait afficher que les colonnes avec des produits." +
   confirmation "utilise bien la largeur de la page (et centre) pour éviter
   d'avoir des retours à la ligne") — remplace l'ancienne .stat-row de ce
   bandeau (npvRenderStatTiles), cf. npvRenderTrendColumns dans
   assets/npv-shared-charts.js. `--n-cols` est recalculé en JS à chaque rendu
   selon le nombre de colonnes réellement non vides (1 à 3) : minmax(260px,
   420px) garantit une largeur minimale par colonne pour ne jamais faire
   déborder une ligne-produit sur deux lignes, et justify-content:center
   évite qu'une seule colonne visible ne s'étire de façon disproportionnée
   sur toute la largeur du bandeau tout en utilisant l'espace disponible. */
.cols-grid {
    display: grid;
    grid-template-columns: repeat(var(--n-cols, 3), minmax(260px, 420px));
    justify-content: center;
    gap: 16px;
}
@media (max-width: 720px) {
    .cols-grid { grid-template-columns: 1fr; }
}
.trend-col { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.trend-col-head {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 8px 12px;
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.trend-col-head .count { margin-left: auto; font-weight: 600; color: var(--text-muted); text-transform: none; letter-spacing: normal; }
.trend-col-head.up { color: var(--good); background: rgba(12, 163, 12, 0.07); }
.trend-col-head.down { color: var(--critical); background: rgba(208, 59, 59, 0.07); }
.trend-col-head.flat { color: var(--text-muted); background: rgba(137, 135, 129, 0.08); }
.trend-col-head svg { flex: none; }
.line-row { display: flex; flex-direction: column; }
.line-item {
    display: grid;
    grid-template-columns: 1.3fr 0.8fr 1fr;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    border-top: 1px solid var(--border);
    font-size: 13px;
}
.line-row .line-item:first-child { border-top: none; }
.line-id {
    display: flex;
    align-items: center;
    gap: 8px;
    font-weight: 600;
    color: var(--dstny-dark);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.line-id .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.line-value { font-weight: 700; font-variant-numeric: normal; color: var(--dstny-dark); white-space: nowrap; }
.line-net { font-weight: 700; font-variant-numeric: normal; display: flex; gap: 5px; align-items: baseline; white-space: nowrap; }
.line-net .pct { font-weight: 500; font-size: 11.5px; opacity: 0.8; }
.line-net.up { color: var(--good); }
.line-net.down { color: var(--critical); }
.line-net.flat { color: var(--text-muted); }
.line-item[title] { cursor: help; }
.stat-tile {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 14px 18px;
    min-width: 180px;
}
.stat-tile .label { font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.stat-tile .value { font-size: 26px; font-weight: 700; color: var(--dstny-dark); font-variant-numeric: normal; }
.stat-tile .delta { font-size: 13px; margin-top: 2px; }
.stat-tile .delta.up { color: var(--good); }
.stat-tile .delta.down { color: var(--critical); }
/* Delta net (2026-09-12) : solde ajoutes - supprimes, ni "bon" ni "mauvais"
   en soi (contrairement à .up/.down qui sont vert/rouge) -- encre neutre de
   la marque, cohérent avec la ligne "delta net" déjà tracée sur le graphique
   "Ajouts / Suppressions" (DELTA_COLOR côté JS). */
.stat-tile .delta.net { color: var(--dstny-dark-blue); font-weight: 600; }

/* ---- Vignette produit unifiée (2026-09-16, v125, retour explicite : "je
   veux que tu retravailles l'affichage des légendes sous les tableaux
   existants" -- reprend le gabarit validé lors de la maquette de discussion :
   identité → chiffre clé → période → net en évidence (avec le delta en % du
   parc) → détail créés/supprimés en retrait → barre de proportion → tendance
   récente. Un seul markup pour npvRenderStatTiles (détail ajoutés/supprimés
   disponible) ET npvRenderOutboundStatTiles (delta simple, pas de détail) :
   la ligne détail/barre est simplement absente du HTML quand la donnée
   n'existe pas pour ce graphique, jamais vidée à moitié (cf. commentaire dans
   npv-shared-charts.js). npvRenderDeltasStatTiles (résumé agrégé "Tous
   produits") n'est PAS concerné par ce correctif : conserve son gabarit
   .label/.value existant ci-dessus, hors périmètre de cette demande. */
.stat-tile-id {
    display: flex; align-items: center; gap: 7px;
    font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--text-muted);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stat-tile-id .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.stat-tile-value { font-size: 26px; font-weight: 700; color: var(--dstny-dark); font-variant-numeric: normal; line-height: 1.15; margin-top: 4px; }
.stat-tile-period { font-size: 11.5px; color: var(--text-muted); margin-top: -2px; }
.stat-tile-net { display: flex; align-items: baseline; gap: 6px; font-size: 14px; font-weight: 700; margin-top: 4px; }
.stat-tile-net .pct { font-size: 12px; font-weight: 600; opacity: 0.85; }
.stat-tile-net.up { color: var(--good); }
.stat-tile-net.down { color: var(--critical); }
.stat-tile-net.flat { color: var(--text-muted); }
.stat-tile-breakdown { font-size: 12px; color: var(--text-secondary); margin-top: 3px; }
.stat-tile-meter {
    height: 5px; border-radius: 3px; background: var(--border);
    overflow: hidden; display: flex; margin-top: 6px;
}
.stat-tile-meter .seg-up { background: var(--good); }
.stat-tile-meter .seg-down { background: var(--critical); }
.stat-tile-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; gap: 8px; }
.stat-tile-spark { display: block; flex: none; }
.badge-warning { background: rgba(250, 178, 25, 0.16); color: #8a5a18; white-space: nowrap; }

table { width: 100%; border-collapse: collapse; font-size: 14px; background: var(--surface); }
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); }
th { color: var(--text-secondary); font-weight: 600; font-size: 12.5px; text-transform: uppercase; letter-spacing: 0.03em; }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
tr:hover td { background: rgba(0, 165, 181, 0.05); }

.badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; }
.badge-added { background: rgba(12, 163, 12, 0.12); color: var(--good); }
.badge-removed { background: rgba(208, 59, 59, 0.12); color: var(--critical); }
/* Nature d'un opérateur IP (onglet Admin > Opérateurs IP, 2026-08-21). */
.badge-cloud { background: rgba(0, 165, 181, 0.12); color: var(--dstny-teal-wcag); }
.badge-internet { background: rgba(0, 92, 135, 0.12); color: var(--dstny-dark-blue); }
.badge-inconnu { background: rgba(137, 135, 129, 0.15); color: var(--text-muted); }

/* Admin > Produits (2026-09-02) : une ligne par produit sur 2 sous-lignes
   (nom+libellé+bouton, puis les cases à cocher juste dessous) plutôt qu'une
   seule ligne flex-wrap qui cassait n'importe où selon la longueur des labels
   une fois la 3ème case ("Masquer complètement de l'interface") ajoutée. */
.produit-row { padding: 10px 0; border-bottom: 1px solid var(--border); }
.produit-row:last-child { border-bottom: none; }
.produit-row-main { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.produit-row-key { min-width: 160px; }
.produit-row-label { flex: 1 1 200px; max-width: 320px; padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; }
.produit-row-flags { display: flex; gap: 8px 20px; flex-wrap: wrap; margin-top: 8px; }
.produit-row-flags label { display: flex; align-items: center; gap: 5px; font-size: 13px; white-space: nowrap; }

.filters { display: flex; gap: 12px; flex-wrap: wrap; align-items: end; margin-bottom: 18px; }
/* Période personnalisée (2026-09-30, cf. npvCustomRangeReady,
   npv-shared-charts.js) : "Au" antérieur à "Du" — rien n'est rechargé tant
   que la plage n'est pas cohérente, le champ fautif est signalé. */
.filters input[type=date].npv-date-invalid { border-color: var(--critical); box-shadow: 0 0 0 1px var(--critical); }
.filters label { display: block; font-size: 12.5px; color: var(--text-secondary); margin-bottom: 4px; }
.filters select, .filters input[type=text], .filters input[type=date], .filters input[type=number], .filters input[type=password] {
    padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: #fff;
}

.btn {
    display: inline-block;
    background: var(--dstny-teal-wcag);
    color: #fff;
    border: none;
    padding: 8px 16px;
    border-radius: 6px;
    font-size: 14px;
    cursor: pointer;
    text-decoration: none;
}
.btn:hover { background: var(--dstny-dark-blue); }
.btn.secondary { background: #fff; color: var(--dstny-teal-wcag); border: 1px solid var(--dstny-teal-wcag); }
.btn.danger { background: #fff; color: var(--critical); border: 1px solid var(--critical); }
.btn.orange { background: var(--dstny-orange); }

/* Bouton discret façon lien, pour une action secondaire au milieu d'une
   phrase (2026-09-21, interrupteur "recalcul automatique en pause" — admin.php,
   onglet Admin, bandeau toujours visible) — jamais l'action principale d'un
   écran, donc pas de fond plein ni de bordure comme .btn. */
.btn-link {
    background: none;
    border: none;
    padding: 0;
    color: var(--dstny-teal-wcag);
    text-decoration: underline;
    cursor: pointer;
    font-size: inherit;
}
.btn-link:hover { color: var(--dstny-dark-blue); }

.alert { padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 14px; }
.alert-success { background: rgba(12, 163, 12, 0.10); color: #075c07; }
.alert-error { background: rgba(208, 59, 59, 0.10); color: #8a2222; }

/* Bandeau persistant (2026-08-20) — même look que .alert-error, mais volontairement
   PAS la classe .alert : celle-ci disparaît automatiquement après 6s (cf. _layout.php),
   ce qui ne doit jamais arriver ici tant que la condition sous-jacente (modifications
   en attente d'un recalcul) est toujours vraie. */
.pending-banner {
    padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 14px;
    background: rgba(208, 59, 59, 0.10); color: #8a2222;
}

.chip-list { display: flex; gap: 8px; flex-wrap: wrap; }
/* "Produits inclus" (2026-09-29, retour explicite : "aligne verticalement
   tous les choix") — un item de choix par ligne plutôt que le flux horizontal
   par défaut de .chip-list ci-dessus. Classe à part (jamais une modif directe
   de .chip-list) : ce picker est la SEULE utilisation actuelle de .chip-list,
   mais un futur usage générique de .chip-list ne doit pas hériter de cet
   alignement vertical sans le demander explicitement. */
.chip-list-vertical { flex-direction: column; flex-wrap: nowrap; align-items: flex-start; }
.chip {
    display: flex; align-items: center; gap: 6px;
    background: #fff; border: 1px solid var(--border); border-radius: 999px;
    padding: 4px 6px 4px 12px; font-size: 13px;
}
.chip .swatch { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
/* « Produits inclus » = légende de couleurs (2026-10-01, choix « A ») :
   produit décoché en gris, pastille vide — il n'a plus de courbe. */
.produit-chip .swatch { flex: 0 0 auto; }
.produit-chip:has(.produit-cb:not(:checked)) { color: var(--text-muted); }
.produit-chip:has(.produit-cb:not(:checked)) .swatch { background: transparent !important; box-shadow: inset 0 0 0 2px #c9c7c1; }
.produit-chip.has-data:has(.produit-cb:not(:checked)) { color: inherit; }
.produit-chip.has-data:has(.produit-cb:not(:checked)) .swatch { background: var(--c) !important; box-shadow: none; }
.produit-chip.no-data { color: var(--text-muted); }
.produit-chip.no-data .swatch { background: transparent !important; box-shadow: inset 0 0 0 2px #c9c7c1; }
.produit-note { font-size: 10.5px; color: var(--text-muted); margin-left: 2px; white-space: nowrap; }
.produit-chip:has(.sda-op-cb:not(:checked)) { color: var(--text-muted); }
.produit-chip:has(.sda-op-cb:not(:checked)) .swatch { background: transparent !important; box-shadow: inset 0 0 0 2px #c9c7c1; }
.chip button {
    border: none; background: transparent; color: var(--text-muted); cursor: pointer;
    width: 20px; height: 20px; border-radius: 50%; font-size: 13px; line-height: 1;
}
.chip button:hover { background: rgba(0,0,0,0.06); color: var(--critical); }

/* Bascule cloud (C) / lien internet (I) par règle IP (2026-08-21) — remplace
   l'ancienne classification globale par opérateur (badge-cloud/badge-internet
   ci-dessus, désormais réservés à l'affichage en lecture seule du rollup). */
.chip-flag-btn {
    border: 1px solid var(--border); background: #fff; color: var(--text-muted);
    cursor: pointer; padding: 1px 7px; border-radius: 999px; font-size: 11px;
    font-weight: 700; line-height: 1.6;
}
.chip-flag-btn:hover { opacity: 0.75; }
.chip-flag-btn.active.cloud { background: rgba(0, 165, 181, 0.15); color: var(--dstny-teal-wcag); border-color: var(--dstny-teal-wcag); }
.chip-flag-btn.active.internet { background: rgba(0, 92, 135, 0.15); color: var(--dstny-dark-blue); border-color: var(--dstny-dark-blue); }

.login-wrap { display: flex; align-items: center; justify-content: center; min-height: 90vh; }
.login-card { width: 340px; }

.muted { color: var(--text-muted); font-size: 13px; }
.legend-list { display: flex; gap: 16px; flex-wrap: wrap; font-size: 13px; margin-top: 8px; }
.legend-list .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; vertical-align: middle; }

/* Vue tableau + copie Excel, sur les 18 modals de graphiques (2026-09-01,
   retour explicite : "Que tu proposes toujours une vue tableau (un bouton)
   [...] Ce tableau doit etre copiable dans un excel avec un simple bouton").
   Bouton "Voir en tableau" toujours au même endroit (juste sous le
   graphique/sa légende, dans .chart-modal-main) quel que soit le modal —
   volontairement PAS dans .chart-modal-side comme l'ancienne version
   Évolution-only, dont les filtres diffèrent trop d'un modal à l'autre pour
   y caler un bouton de façon uniforme. .table-scroll porte sa propre barre de
   défilement horizontale (tableaux larges, ex. Top créations avec beaucoup de
   produits en colonnes) sans jamais faire défiler la modale entière. */
.table-toolbar { margin: 10px 0 4px; }
.table-view-toolbar { display: flex; justify-content: flex-end; margin-bottom: 10px; }
.table-scroll { overflow-x: auto; }

/* Bouton "➕ Ajouter au Dashboard" (2026-09-14, Dashboard personnel ; revu
   2026-09-29) — juste à côté du bouton "🔗 Partager" existant sur les 28
   modals (même .table-toolbar, cf. commentaire ci-dessus). Anciennement une
   étoile ☆/★ togglable ; remplacée par un bouton d'action simple sans état
   à refléter (cf. commentaire détaillé sur npvAddChartToDashboard,
   graphiques.php) — chaque clic ajoute la vue courante au Dashboard, ce qui
   permet plusieurs favoris du même graphique avec des filtres différents.
   Gérer/retirer un favori reste une action du Dashboard uniquement (bouton
   "✕ Retirer" de dashboard.php). */
.favorite-add-btn:disabled { opacity: 0.6; cursor: default; }

/* Étoile ★ "Retirer du Dashboard" des modals natifs (dashboard.php, 2026-09-14
   — INCHANGÉE par la révision du 2026-09-29 ci-dessus : ce bouton n'existe
   QUE dans dashboard.php, toujours déjà favorisé par construction (cf. son
   commentaire détaillé sur npvDashNativeRemoveFavorite), donc pas concerné
   par le remplacement de l'étoile togglable de graphiques.php). */
.favorite-star-btn {
    font-size: 16px;
    line-height: 1;
    padding: 7px 12px;
}
.favorite-star-btn.is-favorited {
    color: var(--dstny-orange);
    border-color: var(--dstny-orange);
}
.favorite-star-btn:disabled { opacity: 0.6; cursor: default; }

/* Menu déroulant + recherche pour les pickers "Types affichés" / "Types
   d'équipement affichés" (2026-08-25, retour explicite : "je veux un menu
   déroulant avec un champ de recherche, là c'est beaucoup trop grand" — la
   liste à puces (.chip-list) permanente prenait trop de place quand elle
   contient beaucoup d'entrées) — également repris tel quel par "Opérateurs
   affichés" (retour explicite suivant : "même menu déroulant"). Fermé par
   défaut (juste le bouton résumé) ; ouvrir révèle la recherche, les
   raccourcis "N premiers", puis la liste (déjà classée décroissant par
   volume côté JS, cf. npvSortRankedTypes) — cette dernière porte SA PROPRE
   barre de scroll (max-height ci-dessous) pour ne jamais forcer le modal
   entier à grandir démesurément ; le modal, lui, grandit déjà et défile tout
   seul au besoin (cf. .chart-modal-box overflow-y:auto plus haut — "agrandi
   le modal verticalement si besoin avec une barre de scroll"). */
.npv-select-picker { margin-top: 4px; }
.npv-select-toggle {
    width: 100%; display: flex; justify-content: space-between; align-items: center;
    background: #fff; border: 1px solid var(--border); border-radius: 6px;
    padding: 8px 12px; font-size: 13px; color: var(--dstny-dark); cursor: pointer;
}
.npv-select-toggle:hover { border-color: var(--dstny-teal-wcag); }
.npv-select-toggle .npv-select-caret { color: var(--text-muted); transition: transform 0.15s ease; }
.npv-select-toggle.open .npv-select-caret { transform: rotate(180deg); }
.npv-select-panel {
    margin-top: 8px; border: 1px solid var(--border); border-radius: 6px;
    padding: 10px; background: #fff;
}
.npv-select-search {
    width: 100%; box-sizing: border-box; padding: 6px 10px; margin-bottom: 8px;
    border: 1px solid var(--border); border-radius: 6px; font-size: 13px;
}
.npv-select-quick { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
.npv-select-quick button {
    border: 1px solid var(--border); background: #fff; color: var(--text-secondary);
    padding: 3px 10px; border-radius: 999px; font-size: 12px; cursor: pointer;
}
.npv-select-quick button:hover { border-color: var(--dstny-teal-wcag); color: var(--dstny-teal-wcag); }
.npv-select-list { display: flex; flex-direction: column; gap: 2px; max-height: 260px; overflow-y: auto; }
.npv-select-row {
    display: flex; align-items: center; gap: 8px;
    padding: 5px 8px; border-radius: 6px; font-size: 13px; cursor: pointer;
}
.npv-select-row:hover { background: rgba(0, 165, 181, 0.06); }
.npv-select-row input[type=checkbox] { flex: none; }
/* display:flex + gap (2026-08-25, correctif — retour explicite : "il manque un
   espace entre le drapeau et le nom de l'opérateur" dans le picker "Opérateurs
   affichés") : sans ça, le <img> du drapeau (npvFlagImgHtml) et le texte du nom
   se touchaient directement, aucun espace entre les deux n'était injecté côté
   JS. Un picker sans drapeau (types d'équipement) n'a simplement rien à séparer
   ici, gap sans effet visible. */
.npv-select-row-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: flex; align-items: center; gap: 4px; }
.npv-select-row-stats { flex: none; color: var(--text-muted); font-size: 12px; }
.npv-select-empty { padding: 6px 8px; color: var(--text-muted); font-size: 13px; }
/* Picker à cases à cocher placé DANS un bloc .filters (2026-09-30,
   "Partenaires spécifiques" des classements Top créations/résiliations) :
   `.filters label` (display:block) et `.chart-modal-side .filters input`
   (width:100%) cassaient la ligne (case seule sur sa ligne, nom et nombre
   empilés) — on rétablit la mise en page normale d'une ligne de picker. */
.filters .npv-select-row { display: flex; margin-bottom: 0; color: inherit; font-size: 13px; }
.chart-modal-side .filters .npv-select-row input[type=checkbox] { width: auto; }

/* Classement horizontal "Top progression"/"Top baisse" (2026-08-24) — assez
   haut pour rester lisible jusqu'à 20 barres ("Nombre affiché" du modal) sans
   que Chart.js ne les tasse ; la hauteur réelle du graphique suit de toute
   façon le nombre de barres (categoryPercentage par défaut), ce conteneur ne
   fait que fixer un plafond raisonnable. */
.rank-bar-wrap { position: relative; height: 420px; }

/* "Répartition par produit" / "Répartition des types d'équipement" — barres
   individuelles classées, une par entité (2026-08-25, remplace l'ancienne
   barre empilée unique — cf. npvRenderRepartitionChart, même style que
   .rank-bar-wrap ci-dessus). Réutilisé tel quel (2026-08-25, suite) par
   "Répartition par opérateur" / "Opérateurs par équipement" — cf.
   npvRenderOperatorRankChart, même mécanique de hauteur dynamique.
   position:relative nécessaire pour que npvPositionRankBarLabels() positionne
   son overlay nom+drapeau en absolu par rapport à ce conteneur. Hauteur =
   valeur de repli avant premier rendu ; chaque fonction de rendu la réapplique
   en inline à chaque (re)rendu, proportionnelle au nombre de barres affichées
   (REPART_ROW_HEIGHT côté JS) — contrairement à .rank-bar-wrap (toujours 5
   barres), ces pickers permettent d'en afficher beaucoup plus ("Tout"). */
.repartition-bar-wrap { position: relative; height: 90px; }

.tabs-toggle { display: flex; gap: 6px; margin-bottom: 12px; flex-wrap: wrap; align-items: center; }
.tabs-toggle button {
    background: #fff; border: 1px solid var(--border); padding: 6px 14px; border-radius: 6px;
    font-size: 13px; cursor: pointer; color: var(--text-secondary);
}
.tabs-toggle button.active { background: var(--dstny-teal-wcag); color: #fff; border-color: var(--dstny-teal-wcag); }
/* Séparateur avant "Vue liste"/"Imports" dans #admin-tabs (2026-09-14) — ce
   trait fin marque la coupure entre les onglets "opérationnels" (Produits/
   Types/Opérateurs IP/Maintenance) et ceux ajoutés ensuite ("Vue liste"/
   "Imports", puis "Édition"), sans casser l'alignement du groupe de
   boutons. Ce sont maintenant de VRAIS onglets de cette page comme les
   autres (2026-09-18, cf. commentaire admin.php) — seul le regroupement
   visuel qu'apportait ce séparateur reste pertinent. */
.tabs-toggle-sep { width: 1px; align-self: stretch; background: var(--border); margin: 0 2px; }

/* Onglet "Édition" (2026-09-18, retour explicite : "ajoute moi un sous menu
   dans Admin qui permet de : Renommer le titre d'un graphique / Ajouter une
   légende sous le titre / Ajouter des informations dans une info bulle
   [...] il faudrait que je m'y retrouve dans la liste des graphiques pour
   etre sur de faire la modification sur le bon"). */
.npv-edition-search-wrap { margin-bottom: 16px; }
#npv-edition-search {
    width: 100%; max-width: 420px; padding: 8px 12px; border: 1px solid var(--border);
    border-radius: 6px; font-size: 14px; background: #fff;
}
.npv-edition-group-title { margin-top: 26px; }
.npv-edition-group-title:first-of-type { margin-top: 0; }
.npv-edition-row-head {
    display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; flex-wrap: wrap;
}
.npv-edition-row-head strong { font-size: 15px; color: var(--dstny-dark); }
.npv-edition-slug {
    font-size: 11.5px; color: var(--text-muted); background: rgba(0,0,0,0.045);
    padding: 2px 7px; border-radius: 999px;
}
/* 2 colonnes (Titre affiché / Aide) depuis le 2026-09-24 (fusion
   Légende+Info-bulle en un seul champ "Aide") — grid-template-columns
   réglée ici en conséquence (2026-09-24, retour explicite : "utilise la
   largeur disponible à droite") : elle réservait jusqu'ici encore 3
   colonnes égales (repeat(3, 1fr)), un reliquat de l'ancien formulaire à 3
   champs (Titre/Légende/Info-bulle) jamais mis à jour lors de la fusion —
   la 3ᵉ colonne restait vide, ce qui écrasait "Titre affiché" ET "Aide"
   dans seulement 2 des 3 tiers de la largeur disponible au lieu de se
   partager toute la ligne. "Titre affiché" (un texte court, une ligne)
   garde une largeur compacte ; "Aide" (un paragraphe, plusieurs lignes)
   prend tout le reste. */
.npv-edition-fields { display: grid; gap: 12px; grid-template-columns: minmax(220px, 320px) 1fr; }
@media (max-width: 900px) {
    .npv-edition-fields { grid-template-columns: 1fr; }
}
.npv-edition-fields label { display: block; font-size: 12.5px; color: var(--text-secondary); }
.npv-edition-fields input[type=text], .npv-edition-fields textarea {
    display: block; width: 100%; margin-top: 5px; padding: 7px 10px; border: 1px solid var(--border);
    border-radius: 6px; font-size: 14px; background: #fff; font-family: inherit; resize: vertical;
    box-sizing: border-box;
}
.npv-edition-save-bar { position: sticky; bottom: 0; background: var(--page); padding: 14px 0 4px; margin-top: 8px; }

/* Bandeau calendrier des imports (2026-08-26, page Imports) — grille 7
   colonnes (lundi -> dimanche), une case = un jour. Réutilise les mêmes
   couleurs sémantiques que les badges ok/erreur du tableau juste en dessous
   (--good/--critical) plutôt que d'introduire une nouvelle palette. */
.npv-import-calendar {
    display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; max-width: 420px;
}
/* 3 mois côte à côte (2026-10-01) : les 2 mois précédents puis le mois choisi (à droite), chacun
   avec son titre ; repassent les uns sous les autres sur écran étroit. */
.npv-cal-months { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 28px; }
.npv-cal-month .npv-import-calendar { max-width: none; }
.npv-cal-month-title {
    text-align: center; font-weight: 600; font-size: 14px; color: var(--text-secondary);
    margin-bottom: 8px; text-transform: capitalize;
}
.npv-cal-month-center .npv-cal-month-title { color: var(--dstny-dark); }
@media (max-width: 900px) {
    .npv-cal-months { grid-template-columns: 1fr; }
    .npv-cal-month .npv-import-calendar { max-width: 420px; }
}
.npv-cal-weekday {
    text-align: center; font-size: 11px; font-weight: 600; color: var(--text-muted);
    text-transform: uppercase; letter-spacing: 0.03em; padding-bottom: 4px;
}
.npv-cal-day {
    aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center;
    border-radius: 6px; font-size: 13px; color: var(--text-secondary);
    background: #fff; border: 1px solid var(--border); text-decoration: none;
}
.npv-cal-blank { border: none; background: transparent; }
.npv-cal-day-ok {
    background: rgba(12, 163, 12, 0.12); color: var(--good); font-weight: 600;
    border-color: rgba(12, 163, 12, 0.3); cursor: pointer;
}
.npv-cal-day-ok:hover { background: var(--good); color: #fff; }
.npv-cal-day-error { background: rgba(208, 59, 59, 0.10); color: var(--critical); border-color: rgba(208, 59, 59, 0.25); }
.npv-cal-day-today { box-shadow: 0 0 0 2px var(--dstny-teal-wcag) inset; }
.npv-cal-legend-dot {
    display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 4px; vertical-align: middle;
}
.npv-cal-legend-ok { background: var(--good); }
.npv-cal-legend-error { background: var(--critical); }

/* Modal de suivi (ex: recalcul de l'historique) — overlay plein écran + spinner. */
.modal-overlay {
    position: fixed; inset: 0; background: rgba(38, 22, 22, 0.45);
    display: flex; align-items: center; justify-content: center; z-index: 1000;
}
.modal-box {
    background: var(--surface); border-radius: 10px; padding: 28px 32px;
    max-width: 380px; text-align: center; box-shadow: 0 8px 28px rgba(0,0,0,0.22);
}
.modal-box .spinner {
    width: 36px; height: 36px; margin: 0 auto 16px;
    border: 4px solid var(--border); border-top-color: var(--dstny-teal-wcag);
    border-radius: 50%; animation: npv-spin 0.8s linear infinite;
}
.modal-box p { margin: 4px 0; }
.modal-box .modal-title { font-weight: 600; color: var(--dstny-dark); }
@keyframes npv-spin { to { transform: rotate(360deg); } }

/* Overlay de blocage à l'envoi d'un formulaire (2026-09-16, cf. _layout.php)
   — même fond que .modal-overlay ci-dessus, mais z-index au-dessus de TOUT
   (y compris un modal de graphique resté ouvert, jusqu'à 1100 ailleurs dans
   ce fichier) : posé pendant le vrai aller-retour navigateur d'un POST, pas
   pendant un job en arrière-plan (déjà couvert par l'icône de suivi, .scan-badge). */
.npv-submit-overlay {
    display: none;
    position: fixed; inset: 0; background: rgba(38, 22, 22, 0.45);
    align-items: center; justify-content: center; z-index: 2000;
    flex-direction: column; gap: 16px;
}
.npv-submit-overlay.visible { display: flex; }
.npv-submit-overlay .spinner {
    width: 40px; height: 40px;
    border: 4px solid rgba(255,255,255,0.35); border-top-color: #fff;
    border-radius: 50%; animation: npv-spin 0.8s linear infinite;
}
.npv-submit-overlay-text { color: #fff; font-size: 15px; font-weight: 600; text-align: center; padding: 0 20px; }

/* Grille de graphiques en vignettes 2 par ligne + modal d'agrandissement
   (2026-08-20, sur demande explicite : "en mettre deux par ligne, mais si on
   clic dessus ca ouvre un modal pour le voir en grand"). */
.charts-grid {
    display: grid;
    /* 3 par ligne (2026-09-14, demande explicite : "affiche 3 graphiques par
       ligne [...] réduit la taille des graphiques si nécessaire") — la
       hauteur des vignettes (.chart-tile-canvas-wrap, 175px) ne change pas
       ("toutes les vignettes graphiques doivent faire la même taille",
       2026-08-24) : seule leur largeur se réduit mécaniquement, la grille
       étant plus large (cf. body.npv-page-graphiques main ci-dessus) pour
       amortir une partie de ce rétrécissement. Repli à 2 puis 1 colonne sur
       les écrans plus étroits, cf. les 2 media queries ci-dessous. */
    grid-template-columns: repeat(3, 1fr);
    gap: 18px;
    margin-bottom: 18px;
}
@media (max-width: 1100px) {
    .charts-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 760px) {
    .charts-grid { grid-template-columns: 1fr; }
}

/* 3 sous-sections empilées du détail Metacentrex (Clients / Utilisateurs /
   Groupements, 2026-08-21) — chacune avec sa propre charts-grid de 2
   vignettes (Évolution + Ajouts/Suppressions).
   Les <h2> "Clients"/"Utilisateurs"/"Groupements" ont été supprimés le
   2026-09-01 (retour explicite : "Supprimes les 3 noms de sections et
   applique la meme CSS aux titre des graphiques") : chaque vignette porte
   maintenant son propre titre complet (.chart-tile-label ci-dessous, cf.
   graphiques.php) plutôt que de dépendre d'un h2 de groupe au-dessus.
   Les deux règles `.meta-subsection .chart-tile-label`/`.meta-subsection:
   first-of-type .chart-tile-label` (margin-top 32px/28px) qui reprenaient
   ICI, sur le titre lui-même, les marges qu'avait cet ancien h2 de
   sous-section ont été RETIRÉES le 2026-09-29 (suite 95, retour explicite :
   « Les graphiques ne sont toujours pas alignés sur le Dashboard entre deux
   produits ») : posée le 2026-09-18 avec l'introduction de
   `.npv-chart-heading-row` (cf. plus bas), la paire équivalente
   `.meta-subsection .npv-chart-heading-row`/`.meta-subsection:first-of-type
   .npv-chart-heading-row` (mêmes 32px/28px, sur le WRAPPER cette fois) était
   censée devenir la SEULE source de vérité pour cette marge — mais les 2
   anciennes règles sur `.chart-tile-label` n'avaient jamais été retirées, et
   leur spécificité CSS (le sélecteur `:first-of-type` compte comme une
   classe) les faisait rester prioritaires sur le `margin:0` de
   `.npv-chart-heading-row > .chart-tile-label` (cf. plus bas) qui était
   pourtant censé les neutraliser — doublant silencieusement l'espacement
   au-dessus de CHAQUE titre Metacentrex (32px+28px ou 28px+28px selon la
   sous-section) par rapport à tous les autres graphiques de l'application.
   Invisible en page Graphiques normale (un espacement un peu plus généreux
   n'y attire pas l'œil), ce doublon gonflait en revanche la hauteur RÉELLE
   de la vignette Metacentrex isolée sur le Dashboard (cf. npvSizeDashFrame,
   graphiques.php) de 28px par rapport aux vignettes des autres blocs
   produit — chacun ayant sa propre grille indépendante (cf.
   `.dashboard-produits-grid` plus bas), ce décalage de hauteur se répercutait
   sur toutes les lignes de vignettes suivantes de ce bloc. Aucune perte de
   rythme vertical sur la page Graphiques : les valeurs 32px/28px restent
   appliquées à l'identique, juste désormais UNIQUEMENT via le wrapper. */
.meta-subsection { margin-bottom: 8px; }

/* Titres de catégorie "[Produit]"/"Statistiques" / "Equipements" /
   "Opérateurs" sur la page Graphiques (2026-08-25, retour explicite "il faut
   aussi me créer des sections dans la page SIP Trunk") — coiffent des
   grilles de vignettes déjà partagées avec les autres vues, cf.
   npvSelectProduit.
   Style "étiquette + règle" (2026-08-31, retour explicite : "on ne fait pas
   trop la différence entre les catégories et les titres" — jusque-là ces
   titres de CATÉGORIE partageaient le même style que les titres de CHAQUE
   GRAPHIQUE en dessous, rendus par un <h2> ordinaire eux aussi, donc rien ne
   les distinguait visuellement. Choix retenu parmi 3 propositions vues sur
   maquette : petite capitale espacée + couleur de marque, puis un filet fin
   sur toute la largeur — un vrai changement de "chapitre" sans bloc de fond
   ni pastille, pour ne jamais concurrencer les graphiques eux-mêmes.
   17,129,153 = --dstny-teal-wcag en RGB (pas de nouvelle variable ajoutée
   pour un usage aussi ponctuel). */
.produit-section-title {
    margin: 32px 0 16px;
    padding-bottom: 8px;
    border-bottom: 2px solid rgba(17, 129, 153, 0.35);
    font-size: 12.5px;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--dstny-teal-wcag);
}

/* Titre "Statistiques" pliable/dépliable du Dashboard (2026-09-22, v157,
   retour explicite : « Ajoute un titre "Statistiques" en reprennant la CSS
   des sections de graphiques Favoris. Et que l'utilisateur puisse reduire ou
   non le bandeau » ; corrigé en v158, retour explicite avec capture
   annotée : « Le titre "Statistiques" ne reprend pas la CSS des autres
   titres des sections de favoris (par exemple Rainbow sur mon image) » — la
   v157 reprenait `.produit-section-title` (le style des titres de
   CATÉGORIE internes à Graphiques, ex. "Equipements"/"Opérateurs" : petite
   capitale sans fond), pas celui des sections de FAVORIS du Dashboard
   lui-même. Reprend maintenant `.produit-banner` TEL QUEL (bandeau plein
   teal, texte blanc — MÊME classe que "Rainbow"/"Tous les produits" plus bas
   sur cette page, cf. dashboard.php) ; ce modificateur remplace seulement le
   curseur "grab" (glisser-déposer, sans objet ici) par "pointer", ajoute le
   style focus clavier, et espace le titre du bandeau juste en dessous —
   jamais de couleur/fond en double, déjà fournis par la classe partagée. Le
   caret ▾ se positionne à droite SANS règle supplémentaire : la mise en page
   flex + `.produit-banner-label { flex: 1 }` (déjà en place pour le libellé
   des autres bandeaux produit) suffisent à pousser tout élément suivant vers
   la droite, exactement comme la poignée ⠿ pousse le libellé vers la
   gauche sur les bandeaux produit ordinaires. */
.produit-banner.dash-evo-section-toggle {
    cursor: pointer;
    touch-action: auto;
    margin-bottom: 12px;
}
.produit-banner.dash-evo-section-toggle:active { cursor: pointer; }
.dash-evo-section-toggle:focus-visible {
    outline: 2px solid #fff;
    outline-offset: -4px;
}
/* Même idiome de rotation que .npv-select-caret (cf. plus haut) : ▾ pointe
   vers le bas par défaut (bandeau déplié), et pivote pour pointer vers la
   droite une fois le bandeau replié. */
.dash-evo-section-caret {
    display: inline-block;
    font-size: 11px;
    transition: transform 0.15s ease;
}
.dash-evo-section-toggle.is-collapsed .dash-evo-section-caret { transform: rotate(-90deg); }

.chart-tile {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 14px 16px;
    cursor: pointer;
    transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
}
.chart-tile:hover {
    box-shadow: 0 4px 14px rgba(0,0,0,0.08);
    border-color: var(--dstny-teal-wcag);
    transform: translateY(-1px);
}
/* Style aligné sur le <h2> ordinaire (2026-09-01 : "applique la meme CSS aux
   titre des graphiques", suite à la suppression des <h2> de sous-section
   Metacentrex Clients/Utilisateurs/Groupements — cf. .meta-subsection
   ci-dessus) : mêmes font-size/color que la règle globale `h2` d'assets/style.css
   (17px, var(--dstny-dark)) ; margin-top pour le contexte Metacentrex piloté
   par `.meta-subsection .npv-chart-heading-row` plus bas (2026-09-29, suite
   95 : seule source de vérité désormais, cf. son commentaire détaillé —
   .chart-tile-label lui-même reste à margin:0, comme tous les autres
   titres). font-weight explicite (h2 est gras par défaut du navigateur ; ce
   <div> ne l'est pas). */
.chart-tile-label {
    font-size: 17px; font-weight: 700; color: var(--dstny-dark); margin-bottom: 12px;
}
/* Ligne titre + icône "ⓘ" d'info-bulle (Admin > Édition, 2026-09-18, retour
   explicite : "Ajouter des informations dans une info bulle qui sera à coté
   du titre en mode vignette uniquement. (faudra ajouter une icone)"). Le
   titre de vignette (<h2> ou .chart-tile-label ci-dessus) et le bouton
   d'info sont FRÈRES dans ce conteneur flex, jamais l'un dans l'autre (cf.
   commentaire détaillé sur npv_chart_vignette_heading(), Stats.php — 2
   titres sur 28 sont réécrits en JS via .textContent, ce qui effacerait une
   icône imbriquée). Le wrapper reprend donc lui-même la marge que portait
   jusqu'ici le titre seul (28px 0 12px pour un <h2> ordinaire, cf. règle
   globale h2 plus haut ; 32px/28px en contexte Metacentrex, cf.
   `.meta-subsection .npv-chart-heading-row` plus bas) et le titre passe à
   margin:0 une fois à l'intérieur, pour ne jamais doubler cet espacement
   (2026-09-29, suite 95 : ce doublon existait réellement pour Metacentrex
   jusqu'à ce jour — cf. commentaire détaillé sur .meta-subsection
   ci-dessus). */
.npv-chart-heading-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin: 28px 0 12px;
}
.npv-chart-heading-row > h2,
.npv-chart-heading-row > .chart-tile-label {
    margin: 0;
    min-width: 0;
}
.meta-subsection .npv-chart-heading-row { margin-top: 32px; margin-bottom: 12px; }
.meta-subsection:first-of-type .npv-chart-heading-row { margin-top: 28px; }
.npv-info-btn {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: none;
    cursor: help;
    line-height: 1;
    color: var(--dstny-teal-wcag);
    opacity: 0.7;
    padding: 3px;
    border-radius: 50%;
}
.npv-info-btn:hover, .npv-info-btn:focus-visible {
    opacity: 1;
    background: rgba(17, 129, 153, 0.12);
    outline: none;
}
/* SVG (2026-09-21) remplaçant l'ancien caractère Unicode "ⓘ", cf.
   npv_chart_vignette_heading() dans Stats.php — taille fixe pour un rendu
   identique sur tous les navigateurs/OS, contrairement au glyphe Unicode
   dont l'apparence dépendait de la police système. */
.npv-info-btn svg {
    width: 15px;
    height: 15px;
    display: block;
}
/* Légende personnalisée sous le titre de vignette — ANCIENNE classe, plus
   utilisée depuis le 2026-09-24 (suppression du sous-titre de vignette, cf.
   npv_chart_vignette_heading(), Stats.php) ; conservée pour ne pas laisser de
   référence orpheline si un jour rappelée, mais aucun élément du DOM ne la
   porte plus. */
.npv-chart-legend {
    margin: -6px 0 12px;
    font-size: 13px;
    color: var(--text-muted);
}
/* Contenu "Aide" affiché sous le titre d'un modal agrandi (2026-09-24, retour
   explicite : "tu reprends le titre et en dessous tu colles le contenu de
   Aide") — sur les 28 modals de graphiques.php ET les modals natifs de
   dashboard.php (cf. npv_graph_aide(), Stats.php). Absent du DOM tant qu'il
   n'est pas renseigné (même principe que l'ancien .npv-chart-legend
   ci-dessus). `white-space: pre-line` respecte les sauts de ligne saisis
   dans le champ "Aide" (Admin > Édition) sans avoir besoin de nl2br() côté
   PHP. */
.chart-modal-aide {
    margin: 4px 0 16px;
    font-size: 14px;
    color: var(--text-muted);
    white-space: pre-line;
}
/* Puce de date en coin, sur TOUTES les vignettes de la page principale
   (2026-09-01, retour explicite : "il faudrait m'ajouter les dates pour tous
   les graphiques [...] Un badge avec la date peut suffire [...] pas besoin
   d'écrire en énorme mais ça doit rester visible" — choix validé sur
   maquette parmi 4 propositions : "A — Puce en coin"). Remplace la
   précédente version "en gros" (2026-08-28, gros texte teal en gras sous le
   titre), qui ne couvrait que 5 vignettes sur ~18 ; toutes les vignettes en
   ont désormais une, "instantané T" (une seule date, "Au JJ") comme "en
   période" (plage résolue par défaut, "Du JJ au JJ" — cf. npvInitTileDateBadges
   pour les graphiques en période, qui n'ont pas de fetch dédié à la
   vignette).
   2026-09-01 (correctif) : une première version en `position:absolute`
   flottait au-dessus de `.chart-tile-canvas-wrap` sans réserver d'espace,
   et chevauchait donc visuellement `.chart-tile-hint-corner` ("Agrandir ⤢")
   qui vit dans ce même coin — repéré sur capture d'écran après tests. La
   puce est désormais un bloc en flux normal (premier enfant de
   `.chart-tile`), alignée à droite via `margin-left:auto`, sur sa PROPRE
   ligne au-dessus de `.chart-tile-canvas-wrap` : elle pousse mécaniquement
   la zone canvas plus bas au lieu de se superposer à "Agrandir ⤢", quelle
   que soit la longueur du texte ("Au JJ" vs "Du JJ au JJ"). Comme toutes
   les vignettes portent désormais une puce, la hauteur ajoutée est uniforme
   partout (pas de rupture du "toutes les vignettes graphiques doivent faire
   la même taille").
   .snapshot-date-banner : inchangée, en gros sous le titre DANS LE MODAL
   agrandi uniquement (retour explicite "Pour le modal tu peux laisser comme
   ça pour l'instant") — les deux classes divergent donc désormais, alors
   qu'elles partageaient la même règle avant ce jour. */
.chart-tile-date-badge {
    display: block;
    width: fit-content;
    margin: 0 0 8px auto;
    font-weight: 600;
    font-size: 11px;
    color: var(--dstny-teal-wcag);
    background: rgba(17, 129, 153, 0.09);
    border-radius: 999px;
    padding: 2px 9px;
    white-space: nowrap;
}
.chart-tile-date-badge:empty { display: none; margin: 0; }
.snapshot-date-banner {
    font-weight: 700; font-size: 15px; color: var(--dstny-teal-wcag);
    margin: 2px 0 8px;
}
.chart-tile-canvas-wrap {
    position: relative;
    height: 175px;
}
/* .chart-tile-canvas-wrap-short (130px) retirée de toutes les vignettes le
   2026-08-24 sur demande explicite : "toutes les vignettes graphiques
   doivent faire la même taille" — la règle reste ici, dormante, au cas où un
   ancien lien/gabarit y ferait encore référence, mais plus aucune vignette de
   graphiques.php ne porte cette classe. */
.chart-tile-canvas-wrap-short {
    height: 175px;
}
.chart-tile-hint-corner {
    position: absolute; top: 4px; right: 6px; z-index: 2;
    font-size: 11px; color: var(--text-muted);
    background: rgba(255,255,255,0.75); padding: 1px 7px; border-radius: 999px;
    pointer-events: none;
}

/* Emplacement où un filtre partagé (cf. #produit-picker-shared /
   #meta-periode-shared dans graphiques.php) est déplacé par JS quand son
   modal s'ouvre — pas de style propre, juste de l'espace pour l'accueillir. */
.picker-slot:empty { display: none; }
.picker-slot { margin-bottom: 4px; }

/* Modal d'agrandissement des graphiques : réutilise .modal-overlay/.modal-box
   mais avec un format nettement plus grand pour accueillir le canvas plein
   format + ses stat-tiles/tableau/légende d'origine.
   2026-08-21, demande explicite : "augmente la taille des modals des
   graphiques +30% hauteur, et 50% largeur" — largeur 920px -> 1380px (×1.5,
   plafond viewport relevé à 96vw pour que ça s'applique vraiment sur les
   grands écrans) ; hauteur 88vh -> 97vh (×1.3 donnerait 114vh, impossible
   au-delà de la fenêtre — 97vh est le maximum pratique, quasi plein écran).
   2026-08-24, demande explicite : +20% de largeur supplémentaire — 1380px -> 1656px. */
.chart-modal-box {
    width: min(1656px, 96vw);
    max-width: none;
    max-height: 97vh;
    overflow-y: auto;
    text-align: left;
    position: relative;
    padding: 32px 28px 24px;
}
.modal-close {
    position: absolute;
    top: 12px; right: 12px;
    width: 30px; height: 30px;
    border: none; border-radius: 50%;
    background: rgba(56, 52, 77, 0.08);
    color: var(--dstny-dark);
    font-size: 15px; line-height: 1; cursor: pointer;
}
.modal-close:hover { background: rgba(208, 59, 59, 0.12); color: var(--critical); }

/* Disposition en 2 colonnes des modals graphiques (2026-08-24, v27, demande
   explicite : "au lieu de mettre les élément à cocher au dessus je veux une
   colonne à droite et le graphique à gauche") — remplace l'empilement
   vertical d'origine (filtres/picker en haut, puis graphique). Colonne de
   droite à largeur fixe : les filtres (select, chip-list) n'ont pas besoin de
   s'étirer, et une largeur fixe garde le graphique aussi large que possible.
   Sous 900px, la colonne de droite repasse au-dessus du graphique (ordre -1)
   plutôt qu'en dessous : on garde les filtres visibles sans scroller d'abord
   tout le graphique. */
.chart-modal-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    gap: 8px 28px;
    align-items: start;
}
.chart-modal-main { min-width: 0; }
.chart-modal-side {
    display: flex;
    flex-direction: column;
    gap: 16px;
}
.chart-modal-side .card,
.chart-modal-side .filters {
    margin-bottom: 0;
}
.chart-modal-side .filters {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
}
.chart-modal-side .filters > div { width: 100%; }
.chart-modal-side .filters select,
.chart-modal-side .filters input {
    width: 100%;
}
@media (max-width: 900px) {
    .chart-modal-layout { grid-template-columns: 1fr; }
    .chart-modal-side { order: -1; }
}

/* --- Partage de graphique par lien (2026-09-11) ----------------------------
   Un lien "?share=<slug>&..." (cf. npvApplySharedView, graphiques.php) ouvre
   directement UN graphique en pleine page plutôt que dans son modal habituel
   par-dessus la grille — demande explicite : « une page (pleine page) avec le
   graphique ». La barre de nav (.npv-nav, avec connexion/déconnexion déjà en
   place) reste affichée — seule la grille de vignettes/onglets produit sous
   elle est masquée ; le modal ciblé (marqué .npv-shared-fullpage par JS) perd
   son statut de fenêtre flottante dimmée (.modal-overlay) et devient le
   contenu normal de la page, plein-largeur, sous la nav. Le × habituel est
   remplacé (npvEnterSharedViewMode) par un lien "← Retour à l'application". */
body.npv-shared-view main > *:not(.npv-shared-fullpage) {
    display: none !important;
}
/* 2026-09-11, retour explicite : "j'aimerai avoir uniquement le contenu du
   modal, voir le compte en haut à droite et le bouton deconnexion. Cache les
   menu de l'app (import, graphique...)" — masque les 4 liens de menu (+
   pastilles de suivi de scan en arrière-plan, elles aussi internes à l'app)
   via la classe .npv-nav-link posée sur eux SEULS dans _layout.php ; la
   marque "NPV_Tools", le nom du compte connecté et "Déconnexion" restent tels
   quels, non concernés par cette classe. */
body.npv-shared-view .npv-nav-link {
    display: none !important;
}
.modal-overlay.npv-shared-fullpage {
    position: static;
    background: none;
    display: block !important;
    padding: 0;
}
.modal-overlay.npv-shared-fullpage .chart-modal-box {
    width: 100%;
    max-width: none;
    max-height: none;
    box-shadow: none;
    border-radius: 0;
    margin: 0;
    padding: 20px clamp(16px, 4vw, 40px) 48px;
}
.npv-shared-back {
    position: static;
    display: inline-flex;
    align-items: center;
    width: auto;
    height: auto;
    border-radius: 999px;
    padding: 7px 16px;
    margin-bottom: 16px;
    font-size: 13px;
    font-weight: 600;
    text-decoration: none;
}

/* --- Dashboard personnel : vignette de graphique favorisé --------------------
   Une vignette de dashboard.php est une <iframe> vers graphiques.php rechargé
   avec `?share=<slug>&...&embed=1` (cf. npvApplySharedView/
   npvIsolateTileForEmbed, graphiques.php) — PAS le modal en grand format :
   retour explicite après une première version qui l'utilisait ("il faut que
   tu m'affiches des vignettes sur l'écran dashboard, pas le graphique
   directement. Là c'est illisible"). `embed=1` isole EXACTEMENT la même
   petite vignette compacte que celle de la grille normale de Graphiques
   (titre + canvas "-thumb", cf. npvIsolateTileForEmbed qui masque tout le
   reste de la page) et pose .npv-dash-tile-view sur le <body> DE L'IFRAME,
   utilisé ici uniquement pour retirer la nav (superflue dans une si petite
   vignette).

   Taille (2026-09-14, correctif v101, retour explicite : "pourquoi tu ne me
   fais pas des vignettes de la même taille que dans Graphiques ? Pourquoi
   j'ai des chariots pour faire défiler ? Pourquoi tu fais compliquer ?") —
   les versions précédentes forçaient la grille interne de l'iframe à 1 seule
   colonne PLEINE LARGEUR DE L'IFRAME (`grid-template-columns: 1fr`), une
   largeur physique de ~370px sur le Dashboard contre ~447px pour une
   vignette de Graphiques (grille à 3 colonnes sur une page élargie à
   1416px) : proportions différentes, habillage de texte/légende différent
   à cette largeur plus étroite, débordements selon le contenu. Plutôt que
   de rapiécer ces marges au cas par cas, l'iframe est maintenant rendue à
   sa largeur NATURELLE de bureau (NPV_EMBED_NATURAL_WIDTH, cf. dashboard.php
   — assez large pour que graphiques.php adopte sa mise en page normale à 3
   colonnes, main à pleine largeur 1416px, EXACTEMENT comme un vrai écran de
   Graphiques), plus grande que la vignette elle-même, puis réduite d'un seul
   bloc via `transform: scale(...)` (calculé en JS, cf.
   npvSizeDashFrame) pour tenir dans l'emplacement du Dashboard — un rendu
   RIGOUREUSEMENT identique à Graphiques, juste plus petit, sans jamais
   avoir besoin de recalculer une mise en page différente ni de deviner une
   hauteur. `.dash-tile-frame-wrap` fait ancre de positionnement + fenêtre de
   recadrage (`overflow: hidden`) : aucune barre de défilement ne peut plus
   apparaître, quel que soit le contenu, puisque rien n'est jamais comprimé
   dans un espace trop petit — seulement peint en plus petit. */
body.npv-dash-tile-view { background: transparent; }
body.npv-dash-tile-view .npv-nav { display: none !important; }

/* Hauteur de titre UNIFORME sur les vignettes du Dashboard (2026-09-29, suite
   95, retour explicite : « Les graphiques ne sont toujours pas alignés sur le
   Dashboard entre deux produits ») — chaque bloc produit (`.produit-group`,
   cf. plus bas) a sa PROPRE grille de vignettes indépendante
   (`.dashboard-produits-grid` : 2 colonnes, une par produit) : la LARGEUR des
   vignettes est déjà rigoureusement identique d'un bloc à l'autre (mêmes 3
   colonnes internes, cf. `.charts-grid`), mais sans cette règle leur HAUTEUR
   ne l'était pas — elle dépend de la hauteur RÉELLE de la vignette isolée
   mesurée par `npvSizeDashFrame` (graphiques.php), elle-même dépendante du
   nombre de lignes sur lequel s'enroule son titre (`.npv-chart-heading-row`,
   cf. Stats.php/npv_chart_vignette_heading()). Un titre plus long enroulé sur
   3 lignes dans un bloc produit (ex. "Metacentrex — Clients — Ajouts /
   Suppressions — 3 derniers mois") rendait donc sa vignette plus haute que
   ses voisines d'un AUTRE bloc produit dont les titres tenaient sur 2 lignes
   (ex. "Évolution des Ajouts / Suppressions des services — 3 derniers
   mois") — décalant toutes les lignes de vignettes SUIVANTES d'un bloc par
   rapport à l'autre (2 grilles indépendantes : rien ne les force autrement à
   partager une même hauteur de ligne).
   Clampé à 2 lignes (troncature avec « … » au-delà) : large sur les ~294px
   d'une colonne de vignette pour tout titre de graphique existant (la
   période, elle, ne fait plus partie de ce texte depuis la suite 96
   ci-dessous — cf. .npv-dash-period-badge) — le titre complet reste de toute
   façon consultable en agrandissant la vignette (clic dessus).
   🚨 La réservation de hauteur (2026-09-29, suite 99, retour explicite :
   « j'ai toujours un ecart de fou entre le titre et le badge », capture
   d'écran à l'appui) ne porte PLUS sur le titre lui-même (`min-height`
   retiré ci-dessous) : la poser directement sur le texte du titre réservait
   certes la bonne hauteur TOTALE, mais LÀ où elle se trouve — dans la boîte
   du titre — donc AVANT le badge, qui la suit immédiatement ; un titre
   tenant sur 1 seule ligne laissait ainsi un grand vide entre son texte et
   le badge. Une 1re tentative (déplacer le `min-height` sur le conteneur
   `.npv-dash-tile-header` plutôt que sur le titre) s'est révélée
   insuffisante : en flexbox, les marges des enfants ne "collapsent" JAMAIS
   (contrairement au flux normal), donc `margin-top` de `.npv-chart-heading-
   row` (28px/32px selon contexte, cf. plus haut) s'ajoutait intégralement à
   la hauteur du conteneur — un `min-height` fixe ne suffisait plus à
   uniformiser quoi que ce soit puisque le contenu réel le dépassait déjà
   dans tous les cas observés, et variait ENCORE selon 1 ou 2 lignes de
   titre. Solution retenue : la réservation se fait désormais en JS
   (`npvIsolateTileForEmbed`, graphiques.php) — mesure du nombre de lignes
   RÉELLEMENT occupées par le titre, et ajout d'un `padding-top` sur
   `.npv-dash-tile-header` compensant exactement la différence avec le
   maximum (2 lignes) — cf. son commentaire détaillé.
   🚨 Ce padding va bien en HAUT du conteneur, pas en bas (2026-09-29, suite
   100, retour explicite : « tu as modifié l'écart entre le badge et le
   graphique... fallait garder l'existant ») : une 1re version le posait en
   `padding-bottom`, déplaçant le vide entre le titre et le badge vers un
   vide entre le badge et le graphique — un écart qui, lui, n'avait jamais
   posé problème et n'aurait jamais dû changer. En le posant en `padding-
   top`, l'écart titre→badge ET l'écart badge→graphique restent TOUS LES
   DEUX fixes (12px chacun), quel que soit le nombre de lignes du titre ;
   seul l'espace entre le bandeau de section et le titre varie légèrement
   pour un titre court — jamais signalé comme gênant. Hauteur totale de la
   vignette inchangée dans tous les cas (alignement entre blocs produit
   toujours garanti).
   Scopé à `.npv-dash-tile-view` (posé uniquement sur le <body> de l'iframe
   isolée d'une vignette de Dashboard, cf. plus haut) : n'affecte JAMAIS la
   page Graphiques elle-même, où chaque titre reste affiché en entier, sans
   troncature ni hauteur forcée. */
body.npv-dash-tile-view .npv-chart-heading-row {
    align-items: flex-start;
}
body.npv-dash-tile-view .npv-chart-heading-row > h2,
body.npv-dash-tile-view .npv-chart-heading-row > .chart-tile-label {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    line-height: 1.3;
}
body.npv-dash-tile-view .npv-info-btn {
    margin-top: 2px;
}
/* Conteneur titre+badge (2026-09-29, suite 99 — cf. commentaire détaillé
   ci-dessus) : regroupe `.npv-chart-heading-row` et `.npv-dash-period-badge`
   (ajoutés dedans par `npvIsolateTileForEmbed`, graphiques.php) en une seule
   colonne flex, simple boîte de mise en page — AUCUNE hauteur minimale
   posée ici : la compensation "titre sur 1 seule ligne" est calculée et
   appliquée en JS (`header.style.paddingBottom`, cf. npvIsolateTileForEmbed)
   plutôt qu'en CSS statique, cf. commentaire détaillé plus haut sur
   pourquoi une valeur CSS fixe ne pouvait pas fonctionner ici. */
body.npv-dash-tile-view .npv-dash-tile-header {
    display: flex;
    flex-direction: column;
}

/* Badge de période (2026-09-29, suite 96, retour explicite : « Peux tu mettre
   la durée à la ligne et pas à la suite du titre. Et je voudrais sous forme
   de badge (que ca ressorte bien) » — jusqu'à la suite 95, la période était
   du texte concaténé à la fin du titre ("Titre — 3 derniers mois"), sur la
   même ligne/dans la même police que lui). Élément à part, injecté par
   `npvIsolateTileForEmbed` (graphiques.php) dans `.npv-dash-tile-header` —
   donc SOUS le titre, sur sa propre ligne, jamais dans le même bloc de
   texte. Fond plein (pas la simple teinte légère de `.chart-tile-date-badge`
   ci-dessous, réservée à la puce de dates en coin) : c'est ce fond plein +
   le blanc du texte qui fait "ressortir" le badge, demandé explicitement —
   une pastille plus discrète se serait fondue dans le reste de la vignette.
   N'existe QUE sur une vignette isolée du Dashboard : jamais injecté par
   aucun autre code chemin, donc aucun besoin de le scoper à
   `.npv-dash-tile-view` (il n'apparaît nulle part ailleurs). */
.npv-dash-period-badge {
    display: block;
    width: fit-content;
    margin: 0 0 12px;
    padding: 4px 12px;
    background: var(--dstny-teal-wcag);
    color: #fff;
    font-size: 12px;
    font-weight: 700;
    border-radius: 999px;
    white-space: nowrap;
}
/* Version "réservation sans affichage" (2026-09-29, suite 101, retour
   explicite : « Il faut quand meme que les graphiques restent alignés sur
   la meme ligne lorsqu'il n'y a pas de badge. Là ca provoque un décallage.
   »). Certains graphiques n'ont pas de notion de période (ex.
   "repartition", cf. NPV_SHARE_DEFS dans graphiques.php) : leur favori n'a
   donc jamais de badge, contrairement à leurs voisins de grille qui, eux,
   en ont un — un écart de hauteur d'en-tête entre les deux, cassant
   l'alignement. `npvIsolateTileForEmbed` (graphiques.php) crée désormais
   TOUJOURS un `.npv-dash-period-badge`, avec cette classe en plus quand la
   période est inconnue : MÊME élément, MÊME boîte, juste invisible — la
   réservation de hauteur est donc garantie identique par construction
   (mesurée par le navigateur), jamais une valeur copiée à la main qui
   risquerait de se désynchroniser si le badge change de taille un jour. */
.npv-dash-period-badge-placeholder {
    visibility: hidden;
}

/* 3 vignettes par ligne (2026-09-14, correctif v105, demande explicite : "Tu
   peux remettre 3 par lignes stp ?" — après un aller-retour à 2 en v102) :
   `.dashboard-grid` ne surcharge plus `grid-template-columns` — la grille du
   Dashboard hérite donc telle quelle du comportement de `.charts-grid`
   (3 colonnes, repli à 2 puis 1 sur écran plus étroit, cf. ci-dessus),
   exactement comme Graphiques. La classe `.dashboard-grid` reste posée dans
   le HTML (cf. dashboard.php) uniquement comme repère pour le JS (retrouver
   la grille d'une vignette retirée, cf. npvDashRemoveFavorite) — plus aucune
   règle CSS ne s'y accroche ici. */

/* Sections produit + catégorie (2026-09-14, correctif v104 -> v106, retour
   explicite après la v105 : "pour les produits tu as utilisé la nomenclature
   'Catégorie' des Graphique[s]. Trouve une autre facon d'afficher les
   produits. Et il faut afficher les graphiques par catégorie aussi.") —
   remplace les 2 pastilles posées SUR chaque vignette en v102 (retour : pas
   ce qui était demandé, et ça cassait l'aspect "identique à Graphiques" tout
   juste corrigé en v103), puis le titre produit unique en `.produit-
   section-title` de la v105 (retour : même nomenclature visuelle que les
   titres de CATÉGORIE de Graphiques pour un niveau différent, et la
   catégorie n'était plus affichée du tout).
   dashboard.php imbrique désormais chaque produit dans un `.produit-group`,
   coiffé d'un `.produit-banner` (bandeau plein, ci-dessous — "Option 1"
   parmi 3 maquettes proposées et validée par l'utilisateur), puis chaque
   catégorie de ce produit dans un `.produit-category-block` (repère pour le
   JS, cf. npvDashRemoveFavorite, aucune règle CSS dessus) coiffé d'un
   `<h2 class="produit-section-title">` — LA MÊME classe que les titres
   "Equipements"/"Opérateurs"/"Pack NFR" de Graphiques : elle y désigne déjà
   une CATÉGORIE, donc aucune règle supplémentaire nécessaire ici pour ce
   niveau, elle s'applique telle quelle. .dash-tile n'a donc toujours AUCUN
   bandeau ni pastille propre : juste un simple conteneur de positionnement
   (pour le bouton "Retirer" flottant, cf. .dash-tile-remove plus bas) — la
   SEULE bordure/fond visible sur une vignette reste celle du vrai
   .chart-tile à l'intérieur de l'iframe (cf. historique v103 ci-dessus),
   rigoureusement identique à Graphiques.
   Catégories supprimées le 2026-09-17 (retour explicite : "supprimer les
   catégories, ça prend trop de place") : `.produit-category-block` et
   `.produit-section-title` ne sont plus posés dans le HTML du Dashboard,
   chaque `.produit-group` ne contient plus qu'une seule grille de vignettes
   (cf. `.produit-group .dashboard-grid` plus bas). Paragraphe ci-dessus
   conservé tel quel pour l'historique — .produit-banner reste inchangé. */
.produit-banner {
    background: var(--dstny-teal-wcag);
    color: #fff;
    padding: 10px 16px;
    border-radius: 8px;
    font-size: 14.5px;
    font-weight: 700;
    letter-spacing: 0.01em;
    /* Marge haute retirée le 2026-09-17 (passage à .dashboard-produits-grid,
       2 colonnes) : l'espacement ENTRE bandeaux vient désormais du `gap` de
       la grille (cf. plus bas), uniforme quelle que soit la position d'un
       produit dans la grille — l'ancienne marge fixe + son correctif
       `:first-of-type` ne distinguaient que le tout premier bandeau du
       document, alors qu'avec 2 colonnes CHAQUE produit de la première ligne
       (pas seulement le tout premier) a besoin d'une marge haute nulle. */
    margin: 0;
    /* Poignée de glisser-déposer (2026-09-17, retour explicite : "je puisse
       déplacer un produit complet dans le Dashboard") : tout le bandeau sert
       de poignée (aucun élément interactif dessus) — cf. npvDashInitDragReorder,
       plus bas dans ce fichier, pour le "pourquoi" du Pointer Events manuel
       plutôt que du Drag and Drop HTML5 natif. `touch-action: none` empêche le
       geste de défilement tactile de concurrencer le glissement sur mobile. */
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: grab;
    touch-action: none;
    user-select: none;
}
.produit-banner:active { cursor: grabbing; }
.produit-banner-grip { font-size: 15px; opacity: 0.8; line-height: 1; }
.produit-banner-label { flex: 1; }

/* Grille 2 colonnes de PRODUITS (2026-09-17, cf. dashboard.php pour le détail
   du "pourquoi") : chaque `.produit-group` (bandeau + toutes ses catégories/
   vignettes) devient une cellule, 2 par ligne sur un écran assez large pour
   que ça reste lisible — repli à 1 colonne en dessous (comportement identique
   à aujourd'hui). `align-items: start` : la hauteur d'un produit ne s'étire
   jamais pour égaler celle de son voisin de ligne (nombre de favoris souvent
   différent d'un produit à l'autre). */
.dashboard-produits-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 32px 28px;
    align-items: start;
}
@media (max-width: 1400px) {
    .dashboard-produits-grid { grid-template-columns: 1fr; }
}
/* Grille de vignettes d'un produit : suit directement son bandeau, sans
   plus aucun titre de catégorie entre les deux (catégories supprimées le
   2026-09-17, retour explicite : "supprimer les catégories, ça prend trop
   de place" — cf. dashboard.php pour le détail). Un petit espacement propre,
   `.produit-banner` n'ayant plus lui-même de marge basse depuis le passage
   à `.dashboard-produits-grid`. */
.produit-group .dashboard-grid {
    margin-top: 14px;
}
.dash-tile {
    position: relative;
}
.dash-tile-frame-wrap {
    position: relative;
    width: 100%;
    overflow: hidden;
    /* Hauteur de repli affichée brièvement avant que le JS (npvSizeDashFrame,
       dashboard.php) ne mesure le contenu réel de l'iframe et ne calcule le
       facteur d'échelle — remplacée dès que possible, jamais définitive. */
    height: 320px;
    transition: height 0.15s ease;
}
.dash-tile-frame {
    position: absolute;
    top: 0;
    left: 0;
    border: 0;
    transform-origin: top left;
    /* width/height/transform réels posés par npvSizeDashFrame (dashboard.php)
       une fois la largeur naturelle de rendu connue — valeurs de repli ici
       uniquement pour l'instant très bref avant ce calcul. */
    width: 1460px;
    height: 320px;
}
/* Boutons d'une vignette (2026-09-17, correctif v133 — retour explicite :
   "le bouton pour déplacer une vignette tombe sur le titre") : le "✕"
   flottait DANS le coin de la vignette depuis la v104 ("il n'y a plus
   d'espace dédié où poser ce bouton [...] pour rester lisible même s'il
   chevauche le titre du graphique en dessous" — un chevauchement accepté à
   l'époque), et poser la nouvelle poignée "⠿" de glisser-déposer juste à côté
   (coin opposé, en haut à GAUCHE) recouvrait ELLE, à coup sûr, le titre du
   graphique (rendu par Graphiques lui-même à l'intérieur de l'iframe, en
   haut à gauche de chaque vignette) — contrairement au "✕" à droite, dont le
   chevauchement restait rare (uniquement pour un titre anormalement long).
   La v133 avait déplacé les 2 boutons dans une VRAIE barre au-dessus de
   l'iframe, avec un titre dupliqué à côté — retour explicite (même jour,
   après coup) : "je ne t'ai pas demandé d'ajouter un titre avec les deux
   boutons, je t'ai dit de mettre les deux boutons au niveau du titre [...]
   tu as défoncé toute l'interface". Solution retenue (v135) : PAS de
   nouvelle barre, PAS de titre dupliqué — les 2 boutons restent flottants
   par-dessus l'iframe (`position: absolute`), au même niveau que le titre
   (en haut de la vignette), mais tous les deux groupés en haut à DROITE
   (`.dash-tile-actions`) — la poignée "⠿" quitte le coin gauche où elle
   chevauchait systématiquement le texte, pour rejoindre le "✕" à droite, là
   où le chevauchement reste rare comme il l'a toujours été pour ce bouton.

   **Déplacés en bas à droite (2026-09-29, suite 98)** : retour explicite
   après une maquette à 3 options (« il faut revoir le positionnement des
   deux boutons, c'est ingérable là ») — depuis l'ajout du badge de période
   (suite 96) sous le titre, le haut de la vignette est devenu plus chargé
   (titre + badge), et les 2 boutons flottant par-dessus y étaient plus
   gênants qu'avant. Option retenue (« Option 3 » de la maquette) : titre et
   badge restent groupés en haut, intouchés par quoi que ce soit ; les 2
   boutons descendent en bas à droite, par-dessus la zone du graphique —
   un chevauchement resté sans gêne réelle, comme c'était déjà le cas pour
   le "✕" en haut avant la v133 (aucun texte à cet endroit, seulement le
   tracé du graphique). */
.dash-tile-actions {
    position: absolute;
    bottom: 8px;
    right: 8px;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 6px;
}
.dash-tile-remove, .dash-tile-drag-handle {
    width: 26px; height: 26px;
    border: none; border-radius: 50%;
    background: var(--surface);
    box-shadow: 0 1px 4px rgba(38, 22, 22, 0.25);
    font-size: 13px; line-height: 1;
    flex: 0 0 auto;
}
.dash-tile-remove {
    color: var(--dstny-dark);
    cursor: pointer;
}
.dash-tile-remove:hover { background: var(--critical); color: #fff; }
.dash-tile-drag-handle {
    color: var(--text-secondary);
    font-size: 14px;
    cursor: grab;
    touch-action: none;
}
.dash-tile-drag-handle:active { cursor: grabbing; }
.dash-tile-drag-handle:hover { background: var(--dstny-teal-wcag); color: #fff; }

/* État visuel pendant un glissement (produit ou vignette, cf.
   npvDashInitDragReorder JS) : l'élément réellement déplacé passe en
   `position: fixed` et suit le pointeur (`.npv-drag-floating`), pendant
   qu'un espace réservé (`.npv-drag-placeholder`) matérialise, parmi les
   autres éléments, l'endroit où il atterrirait si on relâchait maintenant. */
.npv-drag-floating {
    position: fixed;
    z-index: 1200;
    pointer-events: none;
    box-shadow: 0 10px 28px rgba(38, 22, 22, 0.3);
    opacity: 0.95;
    transform: scale(1.02);
}
.npv-drag-placeholder {
    border: 2px dashed var(--dstny-teal-wcag);
    border-radius: 10px;
    background: rgba(17, 129, 153, 0.07);
    box-sizing: border-box;
}

/* Modal d'agrandissement du Dashboard (2026-09-14, correctif v103, puis
   correctif v108 — retour explicite : "j'ai une fenetre dans une fenetre").
   Jusqu'en v107, `.dash-modal-box` était une boîte centrée quasi à la même
   largeur que `.chart-modal-box` (la vraie boîte du graphique, chargée à
   l'intérieur de l'iframe, cf. commentaire v107 dans graphiques.php) — deux
   boîtes arrondies de taille presque identique, chacune avec son propre
   bouton "✕" dans son coin, imbriquées l'une dans l'autre : un effet visible
   de "fenêtre dans une fenêtre" (+ la vraie nav de Graphiques, affichée
   depuis la v107 À L'INTÉRIEUR de l'iframe, qui doublait la nav réelle du
   Dashboard juste au-dessus). `.dash-modal-box` occupe maintenant TOUT
   l'écran (aucun cadre, aucun arrondi, aucun bouton "✕" à elle) : l'iframe
   affiche donc la page Graphiques exactement comme si on l'avait ouverte en
   plein écran — une seule nav, un seul modal, un seul "✕" (celui, RÉEL, du
   graphique lui-même) — cf. npvDashOpenModal/npvDashCloseModal, dashboard.php,
   et le clic sur la vignette isolée dans npvIsolateTileForEmbed,
   graphiques.php, qui appelle npvDashOpenModal via `window.parent`. */
.dash-modal-box {
    width: 100vw;
    height: 100vh;
    max-width: none;
    max-height: none;
    padding: 0;
    position: relative;
    overflow: hidden;
    border-radius: 0;
    box-shadow: none;
}
/* Seule façon de quitter TOUT l'overlay et revenir au vrai Dashboard (le
   "✕" du graphique, lui, ne fait que refermer CE graphique — cf. v107) :
   un bouton distinct, en haut à GAUCHE (le "✕" d'un graphique est toujours
   en haut à DROITE de sa propre boîte, cf. .modal-close) et avec un libellé
   explicite plutôt qu'un second rond "✕" — pour ne jamais pouvoir se
   confondre avec le bouton de fermeture du graphique affiché par-dessus.
   Même famille visuelle que `.npv-shared-back` (bouton "← Retour à
   l'application" du lien de partage 🔗, cf. graphiques.php) : un rond "✕"
   aurait recréé exactement le problème qu'on corrige ici. */
.dash-modal-back {
    position: absolute;
    top: 14px; left: 14px;
    z-index: 1010;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border: none;
    border-radius: 999px;
    padding: 8px 16px;
    background: var(--dstny-dark);
    color: #fff;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    box-shadow: 0 2px 8px rgba(38, 22, 22, 0.35);
}
.dash-modal-back:hover { background: var(--dstny-black); }
#dash-modal-frame {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
    background: var(--surface);
}

/* --- Drapeaux pays (2026-08-24, v34) ---------------------------------------
   Remplace l'emoji Unicode ("regional indicator") par un vrai fichier SVG
   auto-hébergé (assets/flags/, paquet MIT "flag-icons") — demande explicite :
   "je veux des drapeaux partout et pas du texte", certains systèmes
   (notamment Windows) affichant l'emoji drapeau comme du texte brut (les 2
   lettres du code pays) plutôt qu'un vrai drapeau. */
.npv-flag {
    display: inline-block; flex: none;
    width: 18px; height: 13px; object-fit: cover; border-radius: 2px;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);
    vertical-align: middle;
}
.npv-flag-placeholder {
    display: inline-block; flex: none;
    width: 18px; height: 13px; border-radius: 2px;
    border: 1px dashed #b8b6b0;
    vertical-align: middle;
}

/* Sélecteur de drapeau (Admin > Opérateurs IP, 2026-08-24, v34) — remplace le
   <select> natif : fermé, n'affiche QUE le drapeau (ou le placeholder si
   aucun) ; ouvert, une liste déroulante partagée (un seul nœud DOM réutilisé
   par tous les sélecteurs de la page, cf. npvFlagPickerOpen()) présente
   chaque pays avec son drapeau ET son nom (demande explicite). */
.npv-flagpicker { position: relative; display: inline-block; }
.npv-flagpicker-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 26px; padding: 0;
    border: 1px solid var(--border); border-radius: 6px; background: #fff; cursor: pointer;
}
.npv-flagpicker-btn:hover { border-color: var(--dstny-teal-wcag); }
.npv-flagpicker-panel {
    position: absolute; top: calc(100% + 4px); left: 0; z-index: 40;
    width: 260px; max-height: 320px; overflow: hidden;
    background: #fff; border: 1px solid var(--border); border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
    display: flex; flex-direction: column;
}
.npv-flagpicker-panel[hidden] { display: none; }
.npv-flagpicker-search {
    margin: 8px; padding: 6px 8px;
    border: 1px solid var(--border); border-radius: 6px; font-size: 13px;
}
.npv-flagpicker-options { overflow-y: auto; padding: 4px 0 8px; }
.npv-flagpicker-option {
    display: flex; align-items: center; gap: 8px; width: 100%;
    padding: 6px 12px; border: none; background: transparent; cursor: pointer;
    font-size: 13px; text-align: left; color: var(--dstny-black);
}
.npv-flagpicker-option:hover, .npv-flagpicker-option.active { background: rgba(0, 165, 181, 0.10); }

/* Bloc "drapeau du groupe" à côté du bloc "drapeau de l'opérateur", même
   formulaire (Admin > Opérateurs IP, 2026-08-24, v34.1) — une bordure
   pointillée à gauche + un fond très légèrement teinté suffisent à montrer
   que ce sélecteur est un 2ᵉ réglage distinct, pas une variante du premier
   (retour explicite : "je pourrais avoir deux pays différents [...] je ne
   vois qu'une zone"). */
.npv-group-flag-block {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 3px 8px 3px 10px; margin-left: 2px;
    border-left: 2px dashed var(--border);
    background: rgba(0, 165, 181, 0.05);
    border-radius: 0 6px 6px 0;
}
.npv-group-flag-label { white-space: nowrap; }

/* Badge "auto" sur un drapeau AUTO-déduit des IP déjà en cache, tant qu'aucun
   choix manuel n'a été enregistré (Admin > Opérateurs IP, 2026-08-24, v36) —
   cf. npv_operator_auto_pays_map()/npv_group_auto_pays_map(). Purement
   informatif (le bouton reste cliquable pour choisir manuellement) : une
   petite pastille en surimpression suffit, pas besoin d'un style intrusif. */
.npv-flagpicker-auto .npv-flagpicker-btn { border-style: dashed; }
.npv-flag-auto-badge {
    display: inline-block; margin-left: 3px;
    font-size: 9px; line-height: 1; letter-spacing: 0.02em; text-transform: uppercase;
    color: var(--text-muted); background: #efeee9;
    border-radius: 3px; padding: 2px 3px; vertical-align: middle;
}

/* Accordéon Opérateurs IP — identité (opérateur + groupe) et liste des AS
   (2026-08-25, refonte UX sur maquette validée) : l'ancienne ligne unique
   entassait nom, groupe, 2 drapeaux et 2 boutons ; les AS en chips wrap
   horizontal écrasaient les boutons Cloud/Lien internet les uns contre les
   autres ("Cloudens", illisible). Remplacé par une grille "Opérateur"/
   "Groupe" à 2 lignes de même aspect (1 seul Enregistrer), et par une liste
   verticale d'AS avec des boutons Cloud/Lien internet à fond + icône,
   nettement séparés. */
.id-grid {
    display: grid;
    grid-template-columns: 96px 1fr 44px;
    gap: 10px 12px;
    align-items: center;
}
.id-label {
    font-size: 12px; font-weight: 600; color: var(--text-secondary);
    text-transform: uppercase; letter-spacing: 0.03em;
}
.id-input {
    padding: 7px 10px; border: 1px solid var(--border); border-radius: 6px;
    font-size: 14px; background: #fff; color: var(--dstny-black); width: 100%;
}
.id-actions { grid-column: 1 / -1; display: flex; gap: 8px; justify-content: flex-end; margin-top: 2px; }

.section-label {
    font-size: 13px; font-weight: 600; color: var(--text-secondary);
    text-transform: uppercase; letter-spacing: 0.03em; margin: 20px 0 8px;
}
.as-list { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: #fff; }
.as-row {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 10px 14px; border-bottom: 1px solid var(--border);
}
.as-row:last-child { border-bottom: none; }
.as-id { font-size: 14px; font-weight: 600; }
.as-prio { font-size: 12.5px; color: var(--text-muted); margin-left: 6px; font-weight: 400; }
.as-actions { display: flex; align-items: center; gap: 8px; }
.npv-as-toggle-btn {
    display: inline-flex; align-items: center; gap: 6px;
    border: 1px solid rgba(11, 11, 11, 0.16); background: var(--page); color: var(--text-secondary);
    padding: 6px 12px; border-radius: 999px; font-size: 12.5px; font-weight: 600;
    line-height: 1; cursor: pointer;
}
.npv-as-toggle-btn svg { width: 14px; height: 14px; flex: none; }
.npv-as-toggle-btn:hover { opacity: 0.85; }
.npv-as-toggle-btn.active.cloud { background: rgba(0, 165, 181, 0.15); color: var(--dstny-teal-wcag); border-color: var(--dstny-teal-wcag); }
.npv-as-toggle-btn.active.internet { background: rgba(0, 92, 135, 0.15); color: var(--dstny-dark-blue); border-color: var(--dstny-dark-blue); }
.npv-as-del-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; border-radius: 50%; color: var(--text-muted);
    border: none; background: transparent; cursor: pointer; padding: 0;
}
.npv-as-del-btn svg { width: 15px; height: 15px; }
.npv-as-del-btn:hover { background: rgba(208, 59, 59, 0.10); color: var(--critical); }

/* Mots-clés d'un type d'équipement (Admin > Types d'équipement trunk,
   2026-08-25) — même langage visuel que la liste "AS associés" ci-dessus
   (.as-list/.as-row) : liste verticale bordée au lieu de puces en wrap
   horizontal, bouton de suppression .npv-as-del-btn réutilisé tel quel. */
.kw-list { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: #fff; }
.kw-row {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    padding: 10px 14px; border-bottom: 1px solid var(--border);
}
.kw-row:last-child { border-bottom: none; }
.kw-motif { font-size: 14px; font-weight: 600; }
.kw-prio { font-size: 12.5px; color: var(--text-muted); margin-left: 6px; font-weight: 400; }

/* Libellés de ligne (nom + drapeau) du classement horizontal "Top
   progression"/"Top baisse" (2026-08-24, v34) — même principe que ci-dessus :
   l'axe Y garde ses graduations (texte transparent, cf. npvRenderGrowthChart)
   uniquement pour que Chart.js réserve automatiquement la bonne largeur de
   marge selon la longueur réelle des noms ; le texte + drapeau visibles sont
   peints par-dessus en HTML, positionnés sur l'ordonnée RÉELLE de chaque
   barre (chart.getDatasetMeta(0).data[i].y). */
.rank-bar-labels { position: absolute; inset: 0; pointer-events: none; }
.rank-bar-label-row {
    position: absolute; left: 0; right: 8px; transform: translateY(-50%);
    display: flex; align-items: center; gap: 6px; overflow: hidden;
    font-size: 12px; font-weight: 600; color: #38344D; white-space: nowrap; text-overflow: ellipsis;
}

/* Info-bulle externe "composition du groupe" (2026-08-25, retour explicite :
   "je veux voir, lorsque je passe la souris sur une des lignes horizontales
   en mode Groupe, le tableau des différents éléments qui composent le
   groupe") — un seul élément partagé par tous les classements à bascule
   Groupe/Opérateur (npvShowExternalTooltip, graphiques.php). Positionnée en
   coordonnées PAGE (scrollX/scrollY + rect du canevas, calculées côté JS),
   pas relative à un ancêtre — jamais rognée par l'overflow du modal. z-index
   au-dessus de .modal-overlay (1000) : elle doit rester visible par-dessus
   un modal de graphique ouvert. */
.npv-group-tooltip {
    position: absolute; z-index: 1100; pointer-events: none;
    background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.18); padding: 8px 10px;
    font-size: 12px; color: var(--dstny-dark); max-width: 320px;
    opacity: 0; transition: opacity 0.08s ease;
}
.npv-group-tooltip.visible { opacity: 1; }
.npv-group-tooltip-title { display: flex; align-items: center; gap: 4px; font-weight: 700; margin-bottom: 4px; white-space: nowrap; }
.npv-group-tooltip table { border-collapse: collapse; width: 100%; }
.npv-group-tooltip td, .npv-group-tooltip th { padding: 2px 10px 2px 0; text-align: left; white-space: nowrap; }
.npv-group-tooltip td:first-child, .npv-group-tooltip th:first-child { display: flex; align-items: center; gap: 4px; }
.npv-group-tooltip th { font-weight: 600; color: var(--text-secondary); font-size: 11px; border-bottom: 1px solid var(--border); }
.npv-group-tooltip td:last-child, .npv-group-tooltip th:last-child { text-align: right; padding-right: 0; }
/* Pastille de couleur devant le nom de série — info-bulle "Evolution"/
   "Ajouts/Suppressions" (2026-08-25, retour explicite "Harmonise le design
   de tous les infos bulle comme celle de groupe"), même table-shell que la
   composition de groupe ci-dessus. */
.npv-group-tooltip-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex: none; }
/* Variante "groupée" de l'info-bulle ci-dessus (2026-09-22, retour explicite :
   "on garde le graphique existant [bleu/rouge], mais l'overlay remonte plus
   de détail [...] Là on a la valeur brute seulement") — utilisée UNIQUEMENT
   par le graphique "Ajouts/Suppressions" en vue "Tous les produits"
   (npvShowGroupedDeltaTooltip) : 2 en-têtes "Ajoutés"/"Supprimés" (+ leur
   total, coloré en JS via DIVERGING_UP/DOWN), chacun suivi du détail par
   produit dans le même format table+pastille que ci-dessus, puis le solde
   net. Structure ajoutée ici, couleurs de signe passées inline par le JS
   (mêmes constantes que les barres du graphique, pas de nouvelle variable
   CSS pour rester cohérent avec DIVERGING_UP/DIVERGING_DOWN déjà définies
   côté JS). */
.npv-group-tooltip-subhead {
    display: flex; justify-content: space-between; gap: 10px; font-weight: 700;
    margin-top: 6px; padding-top: 5px; border-top: 1px solid var(--border);
}
.npv-group-tooltip-subhead:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.npv-group-tooltip-net {
    display: flex; justify-content: space-between; gap: 10px; font-weight: 700;
    margin-top: 6px; padding-top: 5px; border-top: 1px solid var(--border);
}

/* Modals : réglages en barre au-dessus, colonne de droite alignée sur le
   graphique (2026-10-01, choix « C », cf. npvRelayoutChartModals dans
   npv-shared-charts.js). */
/* En-tête 2 colonnes : titre / aide / date à gauche, réglages à droite. */
.chart-modal-top {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 28px;
    align-items: end; margin-bottom: 14px; padding-right: 36px; /* place du ✕ */
}
.chart-modal-head-main { min-width: 0; }
.chart-modal-head-main > h3 { margin-top: 0; }
.chart-modal-head-main > :last-child { margin-bottom: 0; }
.chart-modal-top .snapshot-date-banner { margin-bottom: 0; }
.chart-modal-toolbar { display: flex; flex-wrap: wrap; gap: 12px 16px; align-items: flex-end; justify-content: flex-end; max-width: 760px; }
@media (max-width: 900px) {
    .chart-modal-top { grid-template-columns: 1fr; padding-right: 0; }
    .chart-modal-toolbar { justify-content: flex-start; max-width: none; }
}
.chart-modal-toolbar:empty { display: none; }
.chart-modal-toolbar .filters { margin-bottom: 0; flex-direction: row; flex-wrap: wrap; align-items: flex-end; justify-content: flex-end; gap: 12px 16px; }
.chart-modal-toolbar .filters > div { min-width: 170px; }
.chart-modal-toolbar .filters select,
.chart-modal-toolbar .filters input[type=date] { min-width: 170px; }
.chart-modal-toolbar .filters > .npv-select-row { min-width: 0; align-self: center; }
.chart-modal-toolbar .npv-select-list .npv-select-row { justify-content: flex-start; }
.chart-modal-toolbar .filters .npv-select-row input[type=checkbox] { width: auto; min-width: 0; }
.chart-modal-toolbar .npv-select-picker { position: relative; min-width: 250px; margin-top: 0; }
.chart-modal-toolbar .npv-select-panel {
    position: absolute; z-index: 30; right: 0; left: auto; top: calc(100% + 4px); margin-top: 0;
    width: max(100%, 300px); max-height: 360px; overflow: auto;
    box-shadow: 0 8px 24px rgba(38, 22, 22, 0.14);
}
.chart-modal-head-main > p.muted { margin: 8px 0 0; }
.chart-modal-layout--noside { grid-template-columns: minmax(0, 1fr); }
/* 1er bloc de la colonne de droite aligné pile sur le haut du graphique. */
.chart-modal-side > :first-child, .chart-modal-side > .stat-row { margin-top: 0; }
/* Emplacements vides (picker non affiché, message vide) : hors flux, sinon
   l'écart de la colonne (gap) décale le 1er bloc visible. */
.chart-modal-side > :empty { display: none; }
.chart-modal-side > .picker-slot:has(> [style*="display: none"]:only-child) { display: none; }

/* Bandeau Statistiques du Dashboard (2026-10-01, choix « A ») : une icône
   devant chaque valeur, chiffres et pourcentages alignés à droite dans leurs
   propres colonnes (sous-grille commune à toutes les lignes d'une colonne de
   tendance), nom du produit réduit de 20 %, fins traits entre les blocs,
   légende des icônes sous le bandeau. */
.cols-grid--calls { grid-template-columns: repeat(var(--n-cols, 3), minmax(300px, 1fr)); }
.cols-grid--calls .line-row { display: grid; grid-template-columns: minmax(0, 1.08fr) auto auto auto auto auto; }
.cols-grid--calls .line-item--a { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; gap: 0; padding: 0; }
.line-item--a > * { padding-top: 8px; padding-bottom: 8px; }
.line-item--a .line-id { padding-left: 12px; padding-right: 6px; min-width: 0; }
.line-item--a .line-id-txt { overflow: hidden; text-overflow: ellipsis; }
.lc { display: flex; align-items: center; justify-content: flex-end; gap: 5px; white-space: nowrap; font-variant-numeric: tabular-nums; font-size: 13px; }
.lc-num { font-weight: 700; padding-right: 2px; }
.lc-pct { font-weight: 500; font-size: 11px; opacity: 0.85; padding-left: 4px; padding-right: 8px; }
.line-item--a > .lc-pct:last-child { padding-right: 12px; }
.lc-sep { border-left: 1px solid var(--border); padding-left: 8px; }
.lc-ink { color: var(--dstny-dark); }
/* Espace entre le nombre de services et le trait de la colonne suivante
   (2026-10-01, retour explicite : « la valeur est collée à la colonne »). */
.lc-num.lc-ink { padding-right: 10px; }
.lc.up { color: var(--good); }
.lc.down { color: var(--critical); }
.lc.flat { color: var(--text-muted); }
.lc .line-ico { flex: none; opacity: 0.55; color: var(--dstny-dark); }
.lc .line-ico-trend { flex: none; }
/* Icônes justifiées à gauche de leur colonne, chiffres à droite. */
.lc-num > svg:first-child { margin-right: auto; }
.lc-num { gap: 6px; }
.trend-legend { grid-column: 1 / -1; display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 20px; font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
.trend-legend span { display: inline-flex; align-items: center; gap: 5px; }
.trend-legend .line-ico { opacity: 0.6; color: var(--dstny-dark); }
@media (max-width: 720px) {
    .cols-grid--calls { grid-template-columns: 1fr; }
}

/* « Solde arrivées / départs par partenaire » (2026-10-02) : phrase
   d'explication de la vue active (Meilleurs élèves / Plus mauvais). */
.partner-net-explain { font-size: 12.5px; margin: 0 0 6px; }

/* En-tête compact « pastilles » (2026-10-02, choix D1 — d'abord sur « Solde
   arrivées / départs par partenaire », réutilisable sur les autres modals en
   ajoutant .chart-modal--pills à leur .chart-modal-box) : titre et filtres
   sur la même ligne, légende dessous ; chaque filtre = une pastille d'une
   ligne avec son libellé à l'intérieur ; plage de dates en badge en haut à
   gauche du tableau (.chart-card-date). */
.chart-modal--pills .chart-modal-top { grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 6px 20px; margin-bottom: 10px; }
.chart-modal--pills .chart-modal-head-main { display: contents; }
.chart-modal--pills .chart-modal-head-main > h3 { grid-column: 1; grid-row: 1; margin: 0; }
.chart-modal--pills .chart-modal-head-main > :not(h3) { grid-column: 1 / -1; margin: 0; }
.chart-modal--pills .chart-modal-toolbar { grid-column: 2; grid-row: 1; max-width: none; align-items: center; }
.chart-modal--pills .chart-modal-toolbar .filters { align-items: center; gap: 8px; }
.chart-modal--pills .chart-modal-toolbar .filters > div {
    min-width: 0; display: inline-flex; align-items: center; gap: 4px; height: 30px; box-sizing: border-box;
    border: 1px solid var(--border); border-radius: 999px; background: #fff; padding: 0 4px 0 12px;
}
.chart-modal--pills .chart-modal-toolbar .filters > div > label { margin: 0; font-size: 12.5px; color: var(--text-muted); white-space: nowrap; }
.chart-modal--pills .chart-modal-toolbar .filters select,
.chart-modal--pills .chart-modal-toolbar .filters input[type=date] {
    min-width: 0; border: 0; background: transparent; padding: 4px 6px; font-size: 13px; height: 28px; color: var(--dstny-dark); cursor: pointer;
}
/* Bascule Classement : pastille double, sans libellé. */
.chart-modal--pills .chart-modal-toolbar .filters > div:has(> .tabs-toggle) { border: 0; padding: 0; background: transparent; }
.chart-modal--pills .chart-modal-toolbar .filters > div:has(> .tabs-toggle) > label { display: none; }
.chart-modal--pills .tabs-toggle { margin: 0; gap: 0; flex-wrap: nowrap; }
.chart-modal--pills .tabs-toggle button { height: 30px; padding: 0 13px; border-radius: 0; font-size: 12.5px; white-space: nowrap; }
.chart-modal--pills .tabs-toggle button + button { margin-left: -1px; }
.chart-modal--pills .tabs-toggle button:first-child { border-radius: 999px 0 0 999px; }
.chart-modal--pills .tabs-toggle button:last-child { border-radius: 0 999px 999px 0; }
/* Sélecteur « Partenaires » : bouton fondu dans la pastille. */
.chart-modal--pills .chart-modal-toolbar .npv-select-picker { min-width: 0; }
.chart-modal--pills .chart-modal-toolbar .npv-select-toggle { width: auto; gap: 6px; border: 0; background: transparent; padding: 4px 8px 4px 4px; }
@media (max-width: 900px) {
    .chart-modal--pills .chart-modal-top { grid-template-columns: 1fr; }
    .chart-modal--pills .chart-modal-toolbar { grid-column: 1; grid-row: auto; justify-content: flex-start; }
}
.chart-card-date, .snapshot-date-banner.chart-card-date {
    display: inline-block; max-width: 100%; margin: 0 0 6px; padding: 3px 10px; border-radius: 999px;
    font-weight: 600; font-size: 12px; line-height: 1.4; color: var(--dstny-teal-wcag); background: rgba(17, 129, 153, 0.09);
}
.chart-card-date:empty, .snapshot-date-banner.chart-card-date:empty { display: none; }

/* Imports SDA (2026-10-05) : import déposé, pas encore traité. */
.npv-cal-day-pending { background: rgba(255, 144, 21, 0.14); color: #a85a00; border-color: rgba(255, 144, 21, 0.35); }
.npv-cal-legend-pending { background: #FF9015; }
.badge-pending { background: rgba(255, 144, 21, 0.16); color: #a85a00; }

/* Imports APNF (2026-10-05) : jour avec fichier complet hebdomadaire. */
.npv-cal-day-ful { background: rgba(0, 92, 135, 0.14); color: #005C87; border-color: rgba(0, 92, 135, 0.35); }
.npv-cal-legend-ful { background: #005C87; }

/* Créations de société / fermetures (Portabilité des SDA, 2026-10-05). */
.sda-life { display: flex; flex-wrap: wrap; gap: 8px; margin: 6px 0 0; font-size: 12.5px; }
.sda-life:empty { display: none; }
.sda-life-pill { display: inline-flex; align-items: baseline; gap: 4px; padding: 3px 10px; border-radius: 999px; background: #f1eff0; color: var(--text, #38344D); }
.sda-life-pill strong { font-variant-numeric: tabular-nums; }
.sda-life-pill--in { box-shadow: inset 3px 0 0 #1baf7a; }
.sda-life-pill--out { box-shadow: inset 3px 0 0 #e34948; }

/* Répartition des clients sous « Opérateurs d'origine / de destination » (2026-10-06, choix C). */
.sda-split { margin: 10px 0 2px; }
.sda-split-bar { display: flex; gap: 2px; height: 22px; border-radius: 6px; overflow: hidden; }
.sda-split-bar div { display: flex; align-items: center; justify-content: center; min-width: 22px; font-size: 11.5px; font-weight: 700; font-variant-numeric: tabular-nums; }
.sda-split-legend { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 7px; font-size: 12.5px; color: var(--text-secondary, #4a4760); }
.sda-split-legend span { display: inline-flex; align-items: center; gap: 6px; }
.sda-split-legend i { width: 10px; height: 10px; border-radius: 2px; display: inline-block; flex: none; }
.sda-split-legend strong { color: var(--dstny-dark, #38344D); font-variant-numeric: tabular-nums; }
.sda-split-total { font-size: 12px; color: var(--text-muted); margin-top: 4px; }

/* Modals Arrivées / Départs de SDA : 3 graphiques (2026-10-06). */
.sda-sub-title { margin: 0 0 2px; font-size: 13.5px; color: var(--dstny-dark, #38344D); }
.sda-sub-hint { margin: 0 0 6px; font-size: 11.5px; color: var(--text-muted); }
.sda-sub-empty { font-size: 12.5px; margin: 6px 0 2px; }
.sda-sub-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px; margin-top: 12px; }
.sda-sub-grid .rank-bar-wrap { height: auto; min-height: 60px; }

/* Page Graphiques : catégories en cartes dans une colonne à gauche, pleine
   largeur (2026-10-06, choix « L1 / V4 »). Classe .npv-cat-layout posée par JS
   (jamais en vignette de Dashboard `embed=1`, ni en vue partagée plein écran). */
.npv-cat-off { display: none !important; }
body.npv-cat-layout:not(.npv-shared-view):not(.npv-dash-tile-view) main { max-width: 1920px; }
.npv-cat-shell { display: flex; gap: 22px; align-items: flex-start; }
.npv-cat-nav { flex: none; width: 210px; position: sticky; top: 12px; display: flex; flex-direction: column; gap: 6px; }
.npv-cat-nav-title { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); padding: 2px 4px 4px; }
.npv-cat-card { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; width: 100%; text-align: left; background: #fff; border: 1px solid var(--border, #e1e0d9); border-radius: 8px; padding: 9px 12px; font: inherit; cursor: pointer; color: var(--dstny-dark, #38344D); }
.npv-cat-card:hover { border-color: #9fd3db; }
.npv-cat-card:focus-visible { outline: 2px solid var(--dstny-teal-wcag, #118199); outline-offset: 2px; }
.npv-cat-card-label { font-weight: 600; font-size: 13.5px; }
.npv-cat-card-count { font-size: 11.5px; color: var(--text-muted); }
.npv-cat-card.active { border-color: var(--dstny-teal-wcag, #118199); box-shadow: 0 0 0 1px var(--dstny-teal-wcag, #118199); background: #f3fbfc; }
.npv-cat-card.active .npv-cat-card-label { color: #005C87; }
.npv-cat-main { flex: 1; min-width: 0; }
/* Plus de largeur => plus de vignettes par ligne (4 sur un écran large).
   Les grilles de PREMIER niveau d'une même catégorie (ex. Mouvements des SDA :
   classements + courbes) s'enchaînent dans une seule grille (display:
   contents) : plus de case vide en fin de ligne. La page ne pilote ces grilles
   que par leur style inline (display none / ''), toujours prioritaire.
   Les sous-blocs Metacentrex (paires Évolution / Ajouts-Suppressions) gardent
   leur propre grille. */
body.npv-cat-layout .npv-cat-main { display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 18px; align-content: start; }
body.npv-cat-layout .npv-cat-main > * { grid-column: 1 / -1; }
body.npv-cat-layout .npv-cat-main > .charts-grid { display: contents; }
body.npv-cat-layout .npv-cat-main > #produit-buttons { margin-bottom: 0; }
body.npv-cat-layout .npv-cat-main .charts-grid { grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); }
@media (max-width: 900px) {
    .npv-cat-shell { flex-direction: column; }
    .npv-cat-nav { position: static; width: 100%; flex-direction: row; flex-wrap: wrap; }
    .npv-cat-nav-title { width: 100%; }
    .npv-cat-card { width: auto; }
}
/* Cartes d'une même ligne alignées même si un titre passe sur 2 lignes
   (2026-10-06) : le titre reste en haut, la vignette est calée en bas de sa case. */
body.npv-cat-layout .npv-cat-main .charts-grid > * { display: flex; flex-direction: column; }
body.npv-cat-layout .npv-cat-main .charts-grid > * > .chart-tile { margin-top: auto; }

/* Produits en sous-menu du bandeau sur Graphiques (2026-10-06, choix « S2 »
   avec le soulignement orange de « S3 ») : « Graphiques › » puis les produits
   (#produit-buttons, déplacé par npvInitCategoryTabs). Le trait orange passe
   du lien « Graphiques » au produit sélectionné, en bas du bandeau.
   Animation à l'arrivée sur la page uniquement (aucune au changement de
   produit : la page ne se recharge pas). */
.npv-nav-sub { display: flex; align-items: stretch; align-self: stretch; min-width: 0; }
.npv-nav-sub[hidden] { display: none; }
.npv-nav-sub::before { content: "›"; display: flex; align-items: center; font-size: 20px; opacity: 0.7; padding: 0 6px 0 0; margin-left: -4px; }
.npv-nav-sub #produit-buttons { display: flex; flex-wrap: nowrap; gap: 0; margin: 0; align-items: stretch; overflow-x: auto; scrollbar-width: none; }
.npv-nav-sub #produit-buttons::-webkit-scrollbar { display: none; }
.npv-nav-sub #produit-buttons button {
    background: none; border: 0; border-radius: 0; border-bottom: 3px solid transparent;
    color: #fff; opacity: 0.82; padding: 0 10px; font-size: 14px; white-space: nowrap;
    display: flex; align-items: center; gap: 4px; cursor: pointer;
    animation: npvNavSubIn 0.35s cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
}
.npv-nav-sub #produit-buttons button:hover { opacity: 1; }
.npv-nav-sub #produit-buttons button.active { opacity: 1; font-weight: 700; background: none; border-color: transparent; border-bottom-color: var(--dstny-orange); color: #fff; }
.npv-nav-sub #produit-buttons button:focus-visible { outline: 2px solid #fff; outline-offset: -4px; }
.npv-nav-sub #produit-buttons button .muted { color: inherit; opacity: 0.75; font-size: 10.5px; font-weight: 400; }
body.npv-prod-in-nav .npv-nav a.active[href*="graphiques.php"] { border-bottom-color: transparent; }
.npv-nav-sub::before { animation: npvNavSubFade 0.25s backwards; }
.npv-nav-sub #produit-buttons button:nth-child(2) { animation-delay: 0.12s; }
.npv-nav-sub #produit-buttons button:nth-child(3) { animation-delay: 0.16s; }
.npv-nav-sub #produit-buttons button:nth-child(4) { animation-delay: 0.20s; }
.npv-nav-sub #produit-buttons button:nth-child(5) { animation-delay: 0.24s; }
.npv-nav-sub #produit-buttons button:nth-child(6) { animation-delay: 0.28s; }
.npv-nav-sub #produit-buttons button:nth-child(7) { animation-delay: 0.32s; }
.npv-nav-sub #produit-buttons button:nth-child(8) { animation-delay: 0.36s; }
.npv-nav-sub #produit-buttons button:nth-child(n+9) { animation-delay: 0.40s; }
.npv-nav-sub #produit-buttons button:first-child { animation-delay: 0.08s; }
@keyframes npvNavSubIn { from { opacity: 0; transform: translateX(-14px); } }
@keyframes npvNavSubFade { from { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
    .npv-nav-sub::before, .npv-nav-sub #produit-buttons button { animation: none; }
}
/* Écran étroit : les produits passent sur une 2e ligne du bandeau, défilable
   horizontalement si besoin. */
@media (max-width: 900px) {
    body.npv-prod-in-nav .npv-nav { flex-wrap: wrap; }
    body.npv-prod-in-nav .npv-nav-sub { order: 10; flex-basis: 100%; }
}

/* « Changements de produit (churn) » en diagramme de flux (2026-10-06, choix C1). */
.npv-sankey { width: 100%; height: 100%; }
.npv-sankey svg { width: 100%; height: 100%; display: block; overflow: visible; }
.npv-sankey-full { height: 380px; }
.npv-sankey-flow { opacity: 0.35; transition: opacity 0.15s; }
.npv-sankey-flow:hover { opacity: 0.6; }
.npv-sankey-label { fill: var(--dstny-dark, #38344D); font-weight: 600; }
.npv-sankey-num { fill: var(--dstny-dark, #38344D); font-weight: 700; pointer-events: none; }
.npv-sankey-minus { fill: #c0392b; font-weight: 400; }
.npv-sankey-plus { fill: #1e8449; font-weight: 400; }
.npv-sankey-axis { fill: var(--text-muted); letter-spacing: 0.06em; }
.npv-sankey-empty { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 13px; }
.npv-sankey-trip { font-size: 12.5px; padding: 5px 0; border-bottom: 1px solid #f0efe9; width: 100%; }
.npv-sankey-trip > div:first-child { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; font-weight: 600; }
.npv-sankey-trip > div.muted { font-size: 11.5px; margin-top: 1px; }
.npv-sankey-trip .swatch { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
.npv-sankey-half { height: 260px; }
/* Churn effectif / en cours (2026-10-07, onglets V2). */
.npv-sankey-flow { stroke-width: 0; }
.npv-sankey-flow-wip { opacity: 0.24; stroke-width: 1; stroke-dasharray: 4 3; stroke-opacity: 0.9; }
.npv-sankey-flow-wip:hover { opacity: 0.45; }
.npv-sankey-wip { fill: #9a4b00; font-weight: 400; }
.npv-churn-tabs { display: flex; gap: 6px; margin: 0 0 10px; }
.npv-churn-tab { font: inherit; font-size: 13px; padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border); background: #fff; color: var(--text-secondary); cursor: pointer; }
.npv-churn-tab b { margin-left: 4px; }
.npv-churn-tab:hover { border-color: #118199; }
.npv-churn-tab.active { background: #f3fbfc; border-color: #118199; color: #005C87; font-weight: 700; box-shadow: 0 0 0 1px #118199 inset; }
.npv-churn-tab:focus-visible { outline: 2px solid #118199; outline-offset: 2px; }
.npv-churn-progress { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; font-size: 12px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.npv-churn-bar { display: flex; width: 180px; height: 10px; border-radius: 3px; overflow: hidden; background: #eee; }
.npv-churn-bar i { display: block; background: #118199; }
.npv-churn-bar i.rest { background: repeating-linear-gradient(45deg, #DF7500 0 3px, #fff1e0 3px 6px); }
.npv-churn-ages { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.npv-churn-ages em { font-style: normal; background: #f4f5f7; border-radius: 8px; padding: 1px 8px; }
.npv-churn-ages em.old { background: #fde8e6; color: #9b2a1e; font-weight: 600; }
.npv-sankey-same { font-size: 10.5px; font-weight: 600; background: #eef6f8; color: #0b6577; border-radius: 8px; padding: 1px 6px; }
/* Mouvements des SDA (2026-10-06, retour explicite : « mettre Évolution des
   arrivées [...] sous Arrivées de SDA et Évolution des départs [...] sous
   Départs de SDA. On aura une ligne de 3 et de 2 ») : les courbes
   d'évolution commencent toujours une nouvelle ligne. */
/* Mouvements des SDA (2026-10-07) : 4 vignettes sur la 1re ligne, 3 sur la 2e
   (data-cat posé par le script des catégories, graphiques.php). */
@media (min-width: 1100px) {
    body.npv-cat-layout .npv-cat-main[data-cat="Mouvements des SDA"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
body.npv-cat-layout .npv-cat-main > #charts-grid-sda-partners > :first-child { grid-column-start: 1; }

/* Info-bulle « exclusifs / partagés » des graphiques Arrivées / Départs de SDA
   (2026-10-07, choix « W2 ») : filet + note sous le détail par produit. */
.npv-group-tooltip:has(.npv-tooltip-note) { max-width: 420px; }
.npv-group-tooltip tr.npv-tooltip-sep td { display: table-cell; padding: 0; height: 7px; border-bottom: 1px solid var(--border); }
.npv-group-tooltip tr.npv-tooltip-sep + tr td { padding-top: 6px; }
.npv-tooltip-note { margin-top: 5px; font-size: 11px; color: var(--text-secondary); white-space: normal; line-height: 1.4; }
/* Info-bulle par produit (2026-10-07, choix « Y2 »). */
.npv-group-tooltip:has(.npv-tt-prod) { max-width: 340px; min-width: 280px; }
.npv-tt-prod { padding: 5px 0 4px; border-top: 1px solid var(--border); }
.npv-tt-prod-head { display: flex; align-items: center; gap: 6px; font-weight: 700; white-space: nowrap; }
.npv-tt-prod-n { margin-left: auto; padding-left: 12px; font-weight: 600; }
.npv-tt-prod-n small { font-weight: 400; color: var(--text-secondary); }
.npv-tt-prod-bar { display: flex; height: 20px; border-radius: 3px; overflow: hidden; margin: 5px 0 3px; background: #eee; }
.npv-tt-prod-bar i { display: flex; align-items: center; justify-content: center; min-width: 22px; font-style: normal; font-size: 12px; font-weight: 700; line-height: 1; white-space: nowrap; color: #fff; }
.npv-tt-prod-bar i.sh { color: #00496B; }
.npv-tt-prod-legend { display: flex; justify-content: space-between; gap: 10px; font-size: 11px; font-weight: 600; color: #38344D; white-space: nowrap; }
.npv-tt-prod-legend span.sh { margin-left: auto; }
.npv-tt-prod-key { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: 10.5px; color: var(--text-secondary); margin-top: 4px; padding-top: 5px; border-top: 1px solid var(--border); }
.npv-tt-prod-key span { display: inline-flex; align-items: center; gap: 4px; }
.npv-tt-prod-key i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }

/* ===== Effet de chargement (2026-10-07, choix L4 = L1 + L2) =====
   L1 : fine barre orange sous le bandeau, du clic sur un lien jusqu'à la page
   prête (cf. _layout.php). L2 : squelette animé dans chaque vignette tant que
   son graphique n'est pas dessiné (graphiques.php, dashboard.php). */
.npv-nav { position: relative; }
.npv-topbar { position: absolute; left: 0; bottom: -3px; height: 3px; width: 0; z-index: 60; pointer-events: none;
    background: linear-gradient(90deg, #FF9015, #FFB760); box-shadow: 0 0 8px rgba(255, 144, 21, 0.65);
    border-radius: 0 2px 2px 0; opacity: 0; transition: width 8s cubic-bezier(0.1, 0.7, 0.2, 1), opacity 0.3s ease; }
.npv-topbar.run { opacity: 1; }
.npv-topbar.fin { transition: width 0.25s ease, opacity 0.3s ease 0.25s; opacity: 0; }
.npv-skel { position: absolute; inset: 0; z-index: 4; display: flex; flex-direction: column; gap: 12px; padding: 6px 4px 4px;
    background: var(--surface, #fcfcfb); transition: opacity 0.35s ease; pointer-events: none; }
.dash-tile-frame-wrap > .npv-skel { padding: 16px 18px 14px; border: 1px solid var(--border); border-radius: 8px; }
.npv-skel.done { opacity: 0; }
.npv-skel-title { height: 12px; width: 55%; }
.npv-skel-bars { flex: 1; display: flex; align-items: flex-end; gap: 8px; min-height: 0; }
.npv-skel-bars i { flex: 1; display: block; }
.npv-skel-title, .npv-skel-bars i { background: #eceef0; border-radius: 4px; position: relative; overflow: hidden; }
.npv-skel-title::after, .npv-skel-bars i::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%);
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.75), transparent); animation: npv-skel-shim 1.3s infinite; }
@keyframes npv-skel-shim { 100% { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) {
    .npv-skel-title::after, .npv-skel-bars i::after { animation: none; }
    .npv-topbar, .npv-skel { transition: none; }
}

/* ===== Dashboard 2 (2026-10-07, choix D4, administrateurs) =====
   Résumé d'une ligne dépliable à la place du bandeau « Statistiques », et
   bandeaux produit des favoris en simples titres. Tout est préfixé par
   .npv-page-dashboard2 : le Dashboard n'est pas touché. */
.dash2-sum { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; padding: 10px 16px; margin-bottom: 12px; font-size: 13px; }
.dash2-sum-main b { font-size: 18px; margin: 0 2px; }
.dash2-sum .up { color: #1e8449; } .dash2-sum .down { color: #c0392b; } .dash2-sum .flat { color: var(--text-muted); }
.dash2-pills, .dash2-chips { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.dash2-pill { border-radius: 10px; padding: 1px 9px; font-size: 11.5px; font-weight: 600; }
.dash2-pill.up { background: #e8f5ea; color: #1e8449; } .dash2-pill.flat { background: #f0f0ee; color: var(--text-secondary); } .dash2-pill.down { background: #fde8e6; color: #c0392b; }
.dash2-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; border: 1px solid var(--border); border-radius: 10px; padding: 1px 9px; background: #fff; }
.dash2-chip .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.dash2-sum-btn { margin-left: auto; font: inherit; font-size: 12px; font-weight: 600; color: #005C87; background: #f3fbfc; border: 1px solid #b9dfe5; border-radius: 6px; padding: 4px 12px; cursor: pointer; }
.dash2-sum-btn:hover { border-color: #118199; }
.dash2-sum-btn:focus-visible { outline: 2px solid #118199; outline-offset: 2px; }
.dash2-sum-btn span { display: inline-block; transition: transform 0.15s ease; }
.dash2-sum-btn.open span { transform: rotate(180deg); }
.npv-page-dashboard2 .dash-evo-banner { margin-bottom: 14px; }
.npv-page-dashboard2 .produit-banner { background: transparent; color: #005C87; border-radius: 0; padding: 4px 0 6px; border-bottom: 1px solid #cfe3e7; box-shadow: none; }
.npv-page-dashboard2 .produit-banner .produit-banner-label, .npv-page-dashboard2 .produit-banner .produit-banner-grip { color: #005C87; }
/* Dashboard 2 — choix R1 (2026-10-07, retour explicite : « R1 et tu peux aller
   jusqu'à 5 par ligne [...] Occupe l'espace ») : un produit par ligne sur toute
   la largeur, vignettes en grille de 3 à 5 par ligne selon la largeur d'écran
   (≥ 340 px chacune, jamais plus de 5). Le Dashboard n'est pas touché.
   Passé à 4 par ligne au maximum (2026-10-07, « 4 par ligne pour voir ? »). */
body.npv-page-dashboard2 main { max-width: none; }
.npv-page-dashboard2 .dashboard-produits-grid { grid-template-columns: 1fr; gap: 26px; }
.npv-page-dashboard2 .produit-group .dashboard-grid {
    grid-template-columns: repeat(auto-fill, minmax(max(340px, calc((100% - 3 * 18px) / 4)), 1fr));
    margin-top: 10px;
}
@media (max-width: 760px) {
    .npv-page-dashboard2 .produit-group .dashboard-grid { grid-template-columns: 1fr; }
}
/* Dashboard 2 : vignette sans marge au-dessus du titre (classe posée par
   dashboard2.php sur la page de la vignette, jamais sur le Dashboard ni sur
   Graphiques). La réserve pour les titres d'une seule ligne (alignement des
   graphiques d'une même ligne) est conservée. */
body.npv-dash-tile-view.npv-dash2-tile .npv-chart-heading-row { margin-top: 0; }
/* Dashboard 2 — choix C4 (2026-10-07) : un cadre blanc par produit, titre en
   haut avec un repère à la couleur du produit (--npv-prod-color, posé par
   dashboard2.php) et le nombre de graphiques. Remplace le titre souligné. */
.npv-page-dashboard2 .produit-group { background: #fff; border: 1px solid #d9dde2; border-radius: 10px; padding: 0 16px 16px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); }
.npv-page-dashboard2 .produit-group .produit-banner { color: var(--dstny-dark, #38344D); font-size: 16px; font-weight: 800; padding: 11px 0 10px; border-bottom: 1px solid #eceae4; gap: 10px; }
.npv-page-dashboard2 .produit-group .produit-banner::before { content: ''; width: 4px; height: 20px; border-radius: 2px; background: var(--npv-prod-color, #118199); order: -1; }
.npv-page-dashboard2 .produit-group .produit-banner .produit-banner-label { color: var(--dstny-dark, #38344D); }
.npv-page-dashboard2 .produit-group .produit-banner .produit-banner-grip { color: var(--text-muted); }
.npv-page-dashboard2 .dash2-group-count { font-size: 12px; font-weight: 400; color: var(--text-muted); }
.npv-page-dashboard2 .produit-group .dashboard-grid { margin-top: 12px; }
/* Info-bulle « T3 » des partenaires en porta (2026-10-07). */
.npv-group-tooltip:has(.npv-tt-op) { min-width: 300px; max-width: 420px; }
.npv-tt-op { display: grid; grid-template-columns: minmax(70px, max-content) 100px 1fr; gap: 6px; align-items: center; margin: 4px 0; font-size: 12px; white-space: nowrap; }
.npv-tt-op-l { font-weight: 600; display: inline-flex; align-items: center; gap: 5px; overflow: hidden; text-overflow: ellipsis; }
.npv-tt-op-bar { display: flex; align-items: center; gap: 2px; }
.npv-tt-op-bar i { display: block; height: 12px; border-radius: 2px; }
.npv-tt-op-v { font-size: 11px; color: var(--text-secondary); }
.npv-tt-op-v b { color: #38344D; font-size: 12px; }
.npv-tt-op-prods { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--border); font-size: 11px; }
.npv-tt-op-prods > span { display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--border); border-radius: 9px; padding: 0 7px; }
.npv-tt-op-v b.npv-tt-op-pct { color: #005C87; }
/* Info-bulle « S1 » des partenaires en porta (2026-10-07). */
.npv-group-tooltip:has(.npv-tt-pct) { min-width: 270px; max-width: 360px; }
.npv-tt-pct { display: grid; grid-template-columns: minmax(0, 1fr) 46px 62px; gap: 6px; align-items: center; margin: 5px 0 2px; font-size: 12px; }
.npv-tt-pct-n { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.npv-tt-pct-bar { height: 7px; background: #eee; border-radius: 2px; overflow: hidden; }
.npv-tt-pct-bar i { display: block; height: 100%; }
.npv-tt-pct-v { text-align: right; font-weight: 800; color: #005C87; font-variant-numeric: tabular-nums; }
.npv-tt-pct small { grid-column: 1 / -1; margin: -3px 0 0 15px; color: var(--text-secondary); font-size: 10.5px; }
/* Carte de chaleur « Évolution des arrivées / départs de SDA par opérateur »
   (2026-10-07, choix E4) : opérateurs en lignes, pas de temps en colonnes. */
.npv-heat-scroll { overflow-x: auto; }
.npv-heat table { border-collapse: separate; border-spacing: 3px; width: 100%; font-size: 11.5px; font-variant-numeric: tabular-nums; }
.npv-heat th { font-weight: 600; color: var(--text-secondary); font-size: 10.5px; white-space: nowrap; padding: 2px 4px; text-transform: none; letter-spacing: 0; }
.npv-heat thead th { text-align: center; }
.npv-heat tbody th { text-align: right; color: #38344D; font-size: 12px; padding-right: 8px; position: sticky; left: 0; background: var(--surface, #fcfcfb); }
.npv-heat tbody th .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
.npv-heat td { min-width: 26px; height: 26px; text-align: center; border-radius: 4px; font-weight: 700; cursor: default; }
.npv-heat td.z { background: #f4f4f2; }
.npv-heat td.tot, .npv-heat th.tot { background: none; color: #38344D; font-weight: 700; padding-left: 8px; text-align: right; }
.npv-heat tr.sum th, .npv-heat tr.sum td.tot { font-weight: 800; }
.npv-heat tr.sum td { box-shadow: inset 0 -2px 0 rgba(56, 52, 77, 0.25); }
.npv-heat-compact { height: 100%; display: flex; align-items: center; overflow: hidden; }
.npv-heat-compact table { border-spacing: 2px; table-layout: fixed; }
.npv-heat-compact th { width: 76px; font-size: 10px; text-align: right; overflow: hidden; text-overflow: ellipsis; color: #38344D; }
.npv-heat-compact td { min-width: 0; height: 18px; border-radius: 2px; }
/* Comparaison par opérateurs cochés (2026-10-07, choix C1 + « Total »). */
.npv-heat tbody tr[data-code] { cursor: pointer; }
.npv-heat tbody tr[data-code]:hover th { color: #005C87; }
.npv-heat tbody tr.sel-row th { color: #005C87; }
.npv-heat tbody tr.sel-row td:not(.z):not(.tot) { outline: 2px solid #38344D; outline-offset: -2px; }
.npv-heat-hint { font-size: 11px; margin: 6px 0 0; }
.npv-evo-lines-title { display: block; font-size: 13px; margin-bottom: 6px; }
.npv-evo-lines-wrap { position: relative; height: 280px; }
.npv-evo-ops { display: flex; flex-direction: column; gap: 0; max-height: 125px; overflow-y: auto; padding-right: 4px; scrollbar-width: thin; scrollbar-color: #b9dfe5 transparent; }
.npv-evo-op { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 3px 2px; cursor: pointer; border-radius: 4px; }
.npv-evo-op:hover { background: #f3fbfc; }
.npv-evo-op.sum-tog { border-top: 1px solid var(--border); padding-top: 6px; margin-top: 4px; color: var(--text-secondary); }
.npv-evo-op .dash { width: 16px; border-top: 2px dashed #8a879b; flex: none; }
.npv-evo-op .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.npv-evo-op .lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.npv-evo-op em { margin-left: auto; font-style: normal; color: var(--text-secondary); font-size: 10.5px; white-space: nowrap; }
.npv-link-btn { background: none; border: 0; padding: 4px 0 0; color: #005C87; font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer; }
.npv-link-btn:hover { text-decoration: underline; }
/* Info-bulle « M4 » : mini-barre découpée par produit (2026-10-07). */
.npv-tt-pct-bar.npv-tt-pct-prod { display: flex; gap: 1px; height: 9px; background: transparent; }
.npv-tt-pct-bar.npv-tt-pct-prod i { height: 100%; }
.npv-tt-pct small { display: flex; flex-wrap: wrap; gap: 0 8px; }
.npv-tt-pct-p { display: inline-flex; align-items: center; gap: 3px; white-space: nowrap; }
.npv-tt-pct-p .npv-group-tooltip-dot { width: 6px; height: 6px; margin: 0; }
.npv-group-tooltip:has(.npv-tt-pct) { max-width: 400px; }

/* Évolution des portas par opérateur (2026-10-07) : 5 opérateurs visibles + ascenseur ; liste « Produit affiché » (choix « P3 bis »). */
.npv-evo-ops .npv-evo-op { height: 25px; padding: 0 2px; flex: none; }
.npv-evo-ops::-webkit-scrollbar { width: 6px; }
.npv-evo-ops::-webkit-scrollbar-thumb { background: #b9dfe5; border-radius: 3px; }
.npv-evo-prods { display: flex; flex-direction: column; gap: 1px; }
.npv-evo-prod { display: flex; align-items: center; gap: 6px; font-size: 12px; height: 25px; padding: 0 4px; cursor: pointer; border-radius: 5px; }
.npv-evo-prod:hover { background: #f3fbfc; }
.npv-evo-prod.on { background: #eef7f9; font-weight: 700; }
.npv-evo-prod input { margin: 0; }
.npv-evo-prod .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.npv-evo-prod .lbl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.npv-evo-prod em { margin-left: auto; font-style: normal; color: var(--text-secondary); font-size: 10.5px; font-weight: 400; white-space: nowrap; }
/* Admin > Édition : « Appliquer à tous les produits » (2026-10-08). */
.npv-edition-apply-all { margin-left: auto; padding: 4px 10px; font-size: 12.5px; }
.npv-edition-apply-msg { font-size: 12px; flex-basis: 100%; text-align: right; }
.npv-edition-apply-msg:empty { display: none; }
/* Panneau de survol « Appliquer à tous les produits » (2026-10-08). */
.npv-edition-apply-wrap { position: relative; margin-left: auto; }
.npv-edition-apply-wrap .npv-edition-apply-all { margin-left: 0; }
.npv-edition-apply-pop { display: none; position: absolute; right: 0; top: calc(100% + 6px); z-index: 50; width: 440px; max-width: 80vw;
    background: #fff; border: 1px solid #c9cbd1; border-radius: 8px; box-shadow: 0 8px 22px rgba(0,0,0,.16); padding: 10px 12px; font-size: 12px; color: #38344D; text-align: left; }
.npv-edition-apply-wrap.open .npv-edition-apply-pop { display: block; }
.npv-edition-apply-pop ul { list-style: none; margin: 0; padding: 0; max-height: 320px; overflow-y: auto; }
.npv-edition-apply-pop li { padding: 6px 0; border-top: 1px solid #eee; }
.npv-edition-apply-pop li:first-child { border-top: 0; }
.npv-eap-title { font-weight: 700; margin-bottom: 4px; color: #00496B; }
.npv-eap-head { display: flex; gap: 8px; align-items: baseline; }
.npv-eap-prod { margin-left: auto; font-size: 11px; background: #e3f3f5; color: #005C87; border-radius: 9px; padding: 0 8px; font-weight: 600; white-space: nowrap; }
.npv-eap-line { color: #6b6980; margin-top: 2px; }
.npv-eap-line em { font-style: italic; }
.npv-eap-note { margin-top: 6px; color: #6b6980; font-style: italic; }
.npv-eap-same { color: #5aa02c; }
/* Aperçu réel du graphique dans le panneau de survol (2026-10-08). */
.npv-edition-apply-pop { width: 820px; }
.npv-edition-apply-pop ul { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; max-height: 70vh; }
.npv-edition-apply-pop li, .npv-edition-apply-pop li:first-child { border: 1px solid #eee; border-radius: 6px; padding: 8px; }
.npv-eap-thumb { height: 230px; margin: 6px 0 4px; border-radius: 4px; overflow: hidden; background: #f6f6f4; }
.npv-eap-thumb iframe { width: 143%; height: 329px; border: 0; transform: scale(0.7); transform-origin: 0 0; pointer-events: none; }
.npv-edition-apply-all.npv-edition-apply-done { border-color: #5aa02c; color: #3d7a1c; }
.npv-sankey-compact .npv-sankey-flow { opacity: 0.28; }
/* Barre d'actions verticale des modals (2026-10-08, choix « R1 ») : onglet
   accroché à l'extérieur du bord droit, hauteur = boutons, icônes seules,
   nom au survol (info-bulle à gauche). */
.npv-modal-frame { display: flex; align-items: flex-start; max-width: 100%; }
.npv-modal-frame > .chart-modal-box { width: min(1656px, calc(96vw - 48px)); }
.npv-modal-rail {
    flex: none; width: 46px; margin-top: 18px; padding: 6px 0;
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    background: #fff; border: 1px solid #c9cbd1; border-left: 0; border-radius: 0 10px 10px 0;
    box-shadow: 4px 4px 14px rgba(0, 0, 0, 0.12);
}
.npv-rail-btn {
    position: relative; width: 34px; height: 34px; padding: 0; margin: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: none; border: 1px solid transparent; border-radius: 8px;
    color: #005C87; cursor: pointer; font-size: 0;
}
.npv-rail-btn svg { width: 18px; height: 18px; display: block; pointer-events: none; }
.npv-rail-btn:hover, .npv-rail-btn:focus-visible { background: #eef7f9; border-color: #cfe8ec; outline: none; }
.npv-rail-btn.on { background: #118199; color: #fff; }
.npv-rail-close { color: #6b6980; }
.npv-rail-close.modal-close { position: relative; top: auto; right: auto; width: 34px; height: 34px; border-radius: 8px; background: none; }
.npv-rail-close.modal-close:hover { background: rgba(208, 59, 59, 0.12); color: var(--critical, #c0392b); }
.npv-rail-sep { width: 24px; border-top: 1px solid #e3e3e3; margin: 3px 0; }
.npv-rail-btn.npv-rail-done { color: #a9b7bb; cursor: default; }
.npv-rail-btn.npv-rail-done:hover { background: #f4f6f7; border-color: transparent; }
.npv-rail-btn.npv-rail-busy { opacity: .55; cursor: progress; }
.npv-rail-btn[data-tip]:hover::after, .npv-rail-btn.npv-rail-flash::after {
    content: attr(data-tip); position: absolute; right: 42px; top: 50%; transform: translateY(-50%);
    background: #38344D; color: #fff; font-size: 12px; font-weight: 500; line-height: 1.3;
    padding: 5px 9px; border-radius: 6px; white-space: nowrap; pointer-events: none; z-index: 5;
}
.npv-rail-btn[data-tip]:hover::before, .npv-rail-btn.npv-rail-flash::before {
    content: ''; position: absolute; right: 38px; top: 50%; margin-top: -4px;
    border: 4px solid transparent; border-left-color: #38344D; pointer-events: none; z-index: 5;
}
.npv-nav-link { white-space: nowrap; }
/* Trajets groupés par produit quitté (2026-10-08, choix « G1 »). */
.npv-trips { margin-top: 8px; display: flex; flex-direction: column; gap: 10px; }
.npv-trip-head { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: 13px; padding-bottom: 4px; border-bottom: 1px solid #e6e5df; }
.npv-trip-head .swatch, .npv-trip-dst .swatch { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex: none; margin-right: 0; }
.npv-trip-tot { margin-left: auto; color: #c0392b; font-weight: 600; font-size: 12px; white-space: nowrap; }
.npv-trip-tot small { color: var(--text-muted, #6b6980); font-weight: 400; font-size: 11px; }
.npv-trip-row { display: grid; grid-template-columns: 14px minmax(0, 1fr) auto 56px; gap: 6px; align-items: center; font-size: 12.5px; padding: 4px 4px 4px 6px; border-bottom: 1px solid #f3f2ee; border-radius: 4px; cursor: default; }
.npv-trip-row:hover { background: #f3fbfc; }
.npv-trip-ar { color: #9a98a8; }
.npv-trip-dst { display: inline-flex; align-items: center; gap: 5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.npv-trip-same { color: #9a98a8; font-size: 11.5px; }
.npv-trip-n { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.npv-trip-sda { text-align: right; color: var(--text-muted, #6b6980); font-size: 11.5px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.npv-sankey-focus .npv-sankey-flow { opacity: 0.12; }
.npv-sankey-focus .npv-sankey-flow.hl { opacity: 0.7; }

/* Bandeau « Statistiques » replié, groupes par état (2026-10-08, choix « S1 ») */
.dash2-grp { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 6px; border-radius: 9px; padding: 4px 6px 4px 9px; }
.dash2-grp.up { background: #e8f5ea; } .dash2-grp.flat { background: #f0f0ee; } .dash2-grp.down { background: #fde8e6; }
.dash2-grp-h { font-size: 11.5px; font-weight: 700; margin-right: 2px; white-space: nowrap; }
.dash2-grp .dash2-chip { border-color: transparent; }
.dash2-grp.flat .dash2-chip { background: transparent; color: var(--text-muted); padding: 1px 4px; }
.dash2-sum .dash2-chips:empty { display: none; }

/* Diapo 3 du bandeau « Statistiques » (2026-10-08, choix « H4 ») */
.dash-mv { display: grid; grid-template-columns: 1fr 1fr; gap: 18px 28px; }
.dash-mv-col h4 { margin: 0 0 6px; font-size: 13px; }
.dash-mv-col h4.up, .dash-mv-val b.up { color: var(--good, #1e8449); }
.dash-mv-col h4.down, .dash-mv-val b.down { color: var(--critical, #c0392b); }
.dash-mv-row { display: grid; grid-template-columns: minmax(0, 170px) minmax(60px, 1fr) 140px; gap: 10px; align-items: center; font-size: 12.5px; margin: 7px 0; }
.dash-mv-name { display: flex; align-items: center; gap: 6px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dash-mv-name .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.dash-mv-bar { height: 16px; background: #f3f2ee; border-radius: 3px; overflow: hidden; display: flex; }
.dash-mv-bar i { display: block; height: 100%; flex: none; }
.dash-mv-bar i.add, .dash-mv-leg i.add { background: #7cc68b; }
.dash-mv-bar i.rem, .dash-mv-leg i.rem { background: #e59a92; }
.dash-mv-leg { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 14px; align-items: center; font-size: 11.5px; color: var(--text-muted); margin-bottom: -8px; }
.dash-mv-leg i { display: inline-block; width: 18px; height: 9px; border-radius: 2px; vertical-align: middle; margin-right: 4px; }
.dash-mv-val { white-space: nowrap; font-variant-numeric: tabular-nums; }
.dash-mv-val small { color: var(--text-muted); font-weight: 500; }
.dash-mv-empty { margin: 4px 0; font-size: 12.5px; }
@media (max-width: 760px) { .dash-mv { grid-template-columns: 1fr; } }

/* Migrations d'équipement / de version (2026-10-09, « F1–F4 ») */
.npv-evo-hint { font-size: 10.5px; color: var(--text-muted); margin: 1px 0 6px; }
.npv-evo-reset { background: none; border: 0; padding: 0; margin-top: 6px; font: inherit; font-size: 11px; font-weight: 700; color: #005C87; cursor: pointer; }
.npv-eqmig-minus { color: #c0392b !important; font-weight: 700; }
.npv-eqmig-plus { color: #2e7d32 !important; font-weight: 700; }
.npv-eqmig-chip { display: inline-flex; align-items: center; background: #eef7f9; border: 1px solid #b9dfe5; border-radius: 12px; padding: 0 8px; font-size: 11px; font-weight: 600; color: #005C87; margin-right: 4px; }
.npv-eqmig-pick { max-height: 252px; overflow-y: auto; padding-right: 4px; scrollbar-width: thin; scrollbar-color: #b9dfe5 transparent; }
.npv-eqmig-pick .npv-evo-prod { flex: none; }

/* Valeur destination avec détail IP Dstny au survol (2026-10-09) */
.npv-eqmig-ip, .npv-sankey-ip { cursor: help; text-decoration: underline dotted; text-underline-offset: 2px; }

/* Info-bulles HTML des migrations (2026-10-09, choix « B3 ») */
.npv-eqtip { position: fixed; z-index: 3000; min-width: 230px; }
.npv-eqtip .npv-eqtip-sub { font-weight: 400; color: var(--text-secondary); margin-left: 6px; }
.npv-eqtip .npv-eqtip-arrow { font-weight: 400; color: var(--text-secondary); }
.npv-eqtip td.npv-eqtip-n { font-weight: 700; font-variant-numeric: tabular-nums; }
.npv-eqtip td.npv-eqtip-p { color: var(--text-secondary); padding-left: 8px; font-variant-numeric: tabular-nums; text-align: right; }
.npv-eqtip .npv-eqtip-bar { display: flex; gap: 2px; height: 8px; border-radius: 4px; overflow: hidden; margin: 4px 0 6px; }
.npv-eqtip .npv-eqtip-bar i { display: block; }
.npv-eqtip .npv-eqtip-h { font-size: 10.5px; color: var(--text-secondary); letter-spacing: .04em; text-transform: uppercase; margin: 7px 0 2px; }
.npv-eqtip .npv-eqtip-more { color: var(--text-secondary); }
.npv-eqtip table.npv-eqtip-sep { border-top: 1px solid var(--border); margin-top: 3px; padding-top: 3px; }

/* Garde-fou des vignettes de diagrammes de flux (2026-10-12) : rien ne déborde. */
.chart-tile .npv-sankey, .chart-tile .npv-sankey svg { overflow: hidden; }

/* 3e diapo : migrations d'équipement (2026-10-12, maquette « M1 ») */
.dash-mv-bar i.add-mig, .dash-mv-leg i.add-mig, .dash-mv-sw.add-mig { background: repeating-linear-gradient(135deg, #9fd5ad 0 3px, #c9e8d1 3px 6px); }
.dash-mv-bar i.rem-mig, .dash-mv-leg i.rem-mig, .dash-mv-sw.rem-mig { background: repeating-linear-gradient(135deg, #eeaaa3 0 3px, #f6d0cb 3px 6px); }
.dash-mv-bar i.add, .dash-mv-leg i.add, .dash-mv-sw.add { background: #2e9e4f; }
.dash-mv-bar i.rem, .dash-mv-leg i.rem, .dash-mv-sw.rem { background: #d0473b; }
.dash-mv-sw { display: inline-block; width: 14px; height: 8px; border-radius: 2px; flex: none; }
.dash-mv-row { cursor: default; }
.npv-eqtip td.dash-mv-up { color: #1e8449; }
.npv-eqtip td.dash-mv-down { color: #c0392b; }
.npv-eqtip tr.dash-mv-tot td { border-top: 1px solid var(--border); padding-top: 4px; }


/* =====================================================================
   THÈME DSTNY « V3a » (2026-10-16, maquette mockups/dstny-ui-v3a.html
   validée — design system .claude/skills/dstny-ui-kit). Bloc volontairement
   placé EN FIN de fichier : il surcharge les règles ci-dessus sans les
   modifier (retour arrière = supprimer ce bloc + revenir à l'ancien
   _layout.php). Sidebar ouverte (réductible en icônes + infobulle), barre du
   haut (jobs + compte), produits de Graphiques en sous-menu de la sidebar,
   catégories en pills au-dessus de la grille, police Inter,
   cartes arrondies à ombre douce, CTA en pill dégradé cyan.
   ===================================================================== */
:root {
    --dstny-cyan: #1BA9B5; --dstny-hover: #00758D; --dstny-orange-ui: #FE9305;
    --gray-50: #F9FAFB; --gray-100: #F3F4F6; --gray-200: #E5E7EB; --gray-300: #D1D5DB; --gray-400: #9CA3AF;
    --gray-500: #6B7280; --gray-600: #4B5563; --gray-700: #374151; --gray-800: #1F2937; --gray-900: #111827;
    --cyan-50: #EAF8FA; --red-50: #FEECEC; --green-50: #E7F8F1;
    --radius-xs: 5px; --radius-sm: 10px; --radius-lg: 20px; --radius-xl: 24px; --radius-pill: 999px;
    --shadow-block: 0 4px 12px rgba(17, 24, 39, .06); --shadow-hover: 0 8px 24px rgba(17, 24, 39, .10);
    --shadow-brand: 0 8px 18px rgba(0, 117, 141, .22);
    --cta-gradient: linear-gradient(90deg, #32C8D1 0%, #1BA9B5 55%, #00758D 100%);
    /* Variables historiques rebranchées sur les tokens du kit */
    --dstny-teal: var(--dstny-cyan); --dstny-teal-wcag: var(--dstny-hover); --dstny-dark-blue: var(--dstny-hover);
    --dstny-dark: var(--gray-900); --dstny-black: var(--gray-900);
    --surface: #fff; --page: var(--gray-50); --border: var(--gray-200);
    --text-secondary: var(--gray-600); --text-muted: var(--gray-500);
    --npv-side-w: 240px;
}
html.npv-side-collapsed { --npv-side-w: 76px; }
body { font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif; font-size: 14px; color: var(--gray-900); }
:focus-visible { outline: 2px solid rgba(27, 169, 181, .45); outline-offset: 2px; }
body:not(.npv-shared-view):not(.npv-dash-tile-view) { padding-left: var(--npv-side-w); transition: padding-left .18s ease; }

/* --- Sidebar --- */
.npv-nav {
    position: fixed; left: 0; top: 0; bottom: 0; width: var(--npv-side-w); z-index: 900;
    flex-direction: column; align-items: stretch; gap: 4px; padding: 20px 12px;
    background: #fff; color: var(--gray-700); border-radius: 0 var(--radius-xl) var(--radius-xl) 0;
    box-shadow: var(--shadow-block); transition: width .18s ease;
}
body.npv-shared-view .npv-nav { display: none; }
.npv-nav .brand { padding: 0 0 18px 10px; margin: 0; text-align: left; font-size: 20px; font-weight: 600; letter-spacing: 0; white-space: nowrap; }
.npv-nav .brand .b1 { color: var(--dstny-orange-ui); }
.npv-nav .brand .b2 { color: var(--dstny-cyan); }
html.npv-side-collapsed .npv-nav .brand { text-align: center; padding-left: 0; }
html.npv-side-collapsed .npv-nav .brand-full { display: none; }
html.npv-side-collapsed .npv-nav .brand-short { display: inline; }
.npv-nav a {
    display: flex; align-items: center; justify-content: flex-start; gap: 10px; min-height: 40px; padding: 0 12px;
    color: var(--gray-700); opacity: 1; border: 0; border-radius: var(--radius-sm); font-weight: 500;
    position: relative; transition: background .12s ease, color .12s ease;
}
html.npv-side-collapsed .npv-nav a { justify-content: center; padding: 0; }
.npv-nav a:hover { background: var(--cyan-50); color: var(--dstny-hover); }
.npv-nav a.active { background: var(--dstny-cyan); color: #fff; font-weight: 600; box-shadow: var(--shadow-brand); }
.npv-ico { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
.npv-nav-label { white-space: nowrap; }
html.npv-side-collapsed .npv-nav-label, html.npv-side-collapsed .npv-nav-sub { display: none; }
html.npv-side-collapsed .npv-nav a:hover::after {
    content: attr(data-tip); position: absolute; left: calc(100% + 12px); top: 50%; transform: translateY(-50%);
    background: #fff; color: var(--gray-800); box-shadow: var(--shadow-hover); border-radius: var(--radius-xs);
    padding: 6px 10px; font-size: 12.5px; font-weight: 500; white-space: nowrap; pointer-events: none;
}
.npv-side-toggle {
    display: flex; align-items: center; justify-content: flex-start; gap: 10px; background: none; border: 0;
    font: inherit; color: var(--gray-600); padding: 8px 0 8px 8px; cursor: pointer; border-radius: var(--radius-sm);
}
html.npv-side-collapsed .npv-side-toggle { justify-content: center; padding-left: 0; }
.npv-side-toggle .circle { width: 28px; height: 28px; border: 1px solid var(--gray-300); border-radius: 50%; display: grid; place-items: center; transition: transform .18s ease; transform: rotate(180deg); }
html.npv-side-collapsed .npv-side-toggle .circle { transform: none; }
.npv-side-toggle:hover { color: var(--dstny-hover); }
.npv-side-toggle:hover .circle { border-color: var(--dstny-cyan); }

/* --- Barre du haut : jobs + compte --- */
/* Barre du haut flottante (2026-10-16, retour explicite : bande vide en haut
   depuis le passage du compte dans la sidebar) : elle ne prend plus de place
   dans la page ; les badges de jobs flottent en haut à droite seulement
   quand ils sont affichés. */
.npv-top { position: fixed; top: 10px; right: 20px; z-index: 950; display: flex; justify-content: flex-end; align-items: center; gap: 12px; padding: 0; pointer-events: none; }
.npv-top > * { pointer-events: auto; }
.npv-top .scan-badge { box-shadow: var(--shadow-hover); }
body.npv-dash-tile-view .npv-top { display: none !important; }
.npv-top .user { display: inline-flex; align-items: center; gap: 8px; background: #fff; border: 1px solid var(--gray-200); border-radius: var(--radius-pill); padding: 4px 14px 4px 4px; font-size: 13px; color: var(--gray-800); box-shadow: var(--shadow-block); }
.npv-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--dstny-cyan); color: #fff; display: grid; place-items: center; font-size: 11px; font-weight: 700; }
.npv-logout { font-size: 13px; font-weight: 500; color: var(--gray-600); text-decoration: none; border: 1px solid transparent; border-radius: var(--radius-pill); padding: 6px 12px; }
.npv-logout:hover { color: #EF4444; background: var(--red-50); }
.scan-badge { background: var(--cyan-50); color: var(--dstny-hover); font-weight: 500; }
.scan-spinner { border-color: rgba(27, 169, 181, .3); border-top-color: var(--dstny-cyan); }
.npv-topbar { position: fixed; top: 0; bottom: auto; left: 0; z-index: 1000; background: var(--cta-gradient); box-shadow: 0 0 8px rgba(27, 169, 181, .55); }
main { padding-top: 20px; }

/* --- Produits de Graphiques : sous-entrées de la sidebar --- */
.npv-nav-sub { display: block; margin: 2px 0 8px 22px; padding: 0; background: none; box-shadow: none; align-self: auto; }
.npv-nav-sub[hidden] { display: none; }
.npv-nav-sub::before { content: none; }
.npv-nav-sub #produit-buttons { display: flex; flex-direction: column; flex-wrap: nowrap; gap: 2px; margin: 0; overflow: visible; }
.npv-nav-sub #produit-buttons button {
    display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; white-space: nowrap;
    color: var(--gray-600); opacity: 1; padding: 7px 10px; border: 0; border-radius: var(--radius-xs);
    background: transparent; font: inherit; font-size: 13px; font-weight: 500; transition: background .12s ease, color .12s ease;
    animation: none; /* l'apparition en glissé de l'ancien bandeau n'a plus de sens dans la sidebar */
}
.npv-nav-sub #produit-buttons button::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--gray-300); flex: none; }
.npv-nav-sub #produit-buttons button:hover { background: var(--cyan-50); color: var(--dstny-hover); }
.npv-nav-sub #produit-buttons button.active { background: var(--cyan-50); color: var(--dstny-hover); font-weight: 600; border: 0; box-shadow: none; }
.npv-nav-sub #produit-buttons button.active::before { background: var(--dstny-cyan); }
.npv-nav-sub #produit-buttons button:focus-visible { outline: 2px solid rgba(27, 169, 181, .45); outline-offset: 2px; }
.npv-nav-sub #produit-buttons button .muted { color: var(--gray-400); font-size: 10px; opacity: 1; }
.npv-nav { overflow-y: auto; scrollbar-width: thin; }
html.npv-side-collapsed .npv-nav { overflow: visible; }
/* --- Composants --- */
h1 { font-size: 24px; font-weight: 700; }
.card, .stat-tile, .dash-evo-banner { border: 0; border-radius: var(--radius-lg); box-shadow: var(--shadow-block); }
.chart-tile { border: 1px solid transparent; border-radius: var(--radius-lg); box-shadow: var(--shadow-block); }
.chart-tile:hover { box-shadow: var(--shadow-hover); border-color: var(--dstny-cyan); }
.chart-tile-date-badge { border-radius: var(--radius-pill); }
.produit-banner { background: var(--cta-gradient); border-radius: var(--radius-sm); }
.produit-section-title { color: var(--dstny-hover); border-bottom-color: rgba(27, 169, 181, .35); }
.btn { background: var(--cta-gradient); border-radius: var(--radius-pill); padding: 8px 18px; font-weight: 600; font-size: 13px; transition: box-shadow .12s ease, transform .12s ease; }
.btn:hover { background: var(--cta-gradient); box-shadow: var(--shadow-brand); }
.btn:active { transform: translateY(1px); box-shadow: none; }
.btn:disabled { background: var(--gray-200); color: var(--gray-50); box-shadow: none; cursor: not-allowed; transform: none; }
.btn.secondary { background: #fff; color: var(--dstny-cyan); border: 1px solid var(--dstny-cyan); }
.btn.secondary:hover { background: var(--cyan-50); box-shadow: none; }
.btn.danger { background: #fff; color: #EF4444; border: 1px solid #F8B4B4; }
.btn.danger:hover { background: var(--red-50); box-shadow: none; }
.btn.orange { background: var(--dstny-orange-ui); }
.filters select, .filters input[type=text], .filters input[type=date], .filters input[type=number], .filters input[type=password] { border-radius: var(--radius-xs); font-family: inherit; }
.tabs-toggle button { border-radius: var(--radius-pill); font-family: inherit; }
.tabs-toggle button:hover { border-color: var(--dstny-cyan); color: var(--dstny-hover); }
.tabs-toggle button.active { background: var(--dstny-cyan); border-color: var(--dstny-cyan); }
th { font-size: 11px; color: var(--gray-500); font-weight: 700; }
tr:hover td { background: var(--cyan-50); }
.alert { border-radius: var(--radius-sm); }

/* Catégories de Graphiques : pills horizontales au-dessus de la grille */
.npv-cat-shell { flex-direction: column; align-items: stretch; gap: 18px; }
.npv-cat-nav { position: static; width: auto; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 8px; }
.npv-cat-nav-title { display: none; }
.npv-cat-card {
    flex-direction: row; align-items: center; gap: 8px; width: auto; padding: 7px 8px 7px 14px;
    border: 1px solid var(--gray-200); border-radius: var(--radius-pill); box-shadow: none; color: var(--gray-700); transition: all .12s ease;
}
.npv-cat-card:hover { border-color: var(--dstny-cyan); color: var(--dstny-hover); }
.npv-cat-card-label { font-size: 13px; font-weight: 500; }
.npv-cat-card-count { font-size: 11px; font-weight: 600; background: var(--gray-100); color: var(--gray-500); border-radius: var(--radius-pill); padding: 1px 8px; }
.npv-cat-card.active { background: var(--dstny-cyan); border-color: var(--dstny-cyan); box-shadow: var(--shadow-brand); }
.npv-cat-card.active .npv-cat-card-label { color: #fff; font-weight: 600; }
.npv-cat-card.active .npv-cat-card-count { background: rgba(255, 255, 255, .25); color: #fff; }
/* Dashboard : synthèse en carte blanche, groupes hausse/baisse teintés */
.dash2-sum { background: #fff; border: 0; border-radius: var(--radius-lg); box-shadow: var(--shadow-block); padding: 14px 20px; }
.dash2-sum-main { color: var(--gray-600); }
.dash2-sum-main b { font-size: 22px; color: var(--gray-900); }
.dash2-sum .up { color: #047857; } .dash2-sum .down { color: #B91C1C; }
.dash2-grp { border-radius: var(--radius-pill); padding: 4px 6px 4px 12px; }
.dash2-grp.up { background: var(--green-50); } .dash2-grp.down { background: var(--red-50); } .dash2-grp.flat { background: var(--gray-100); }
.dash2-chip { border-radius: var(--radius-pill); }
.dash2-sum-btn { background: #fff; border: 1px solid var(--dstny-cyan); color: var(--dstny-cyan); border-radius: var(--radius-pill); padding: 6px 14px; }
.dash2-sum-btn:hover { background: var(--cyan-50); border-color: var(--dstny-cyan); }.dash-mv-bar { border-radius: var(--radius-pill); }

/* Sidebar réduite : au survol de « Graphiques », les produits s'affichent dans
   un menu flottant (2026-10-16, retour explicite : changer de produit sans
   rouvrir le menu complet). Position verticale calée par _layout.php. */
html.npv-side-collapsed .npv-nav a[href*="graphiques.php"]:hover + .npv-nav-sub:not([hidden]),
html.npv-side-collapsed .npv-nav .npv-nav-sub:not([hidden]):hover {
    /* left/padding : la zone transparente recouvre la marge de la sidebar,
       pour que la souris passe du lien au menu sans le refermer. */
    display: block; position: absolute; left: calc(100% - 12px); top: var(--npv-sub-top, 110px); z-index: 950;
    margin: 0; padding: 0 0 0 22px; width: max-content; min-width: 200px;
}
html.npv-side-collapsed .npv-nav .npv-nav-sub #produit-buttons {
    background: #fff; border-radius: var(--radius-sm); box-shadow: var(--shadow-hover); padding: 6px;
}
html.npv-side-collapsed .npv-nav:has(.npv-nav-sub:not([hidden])) a[href*="graphiques.php"]:hover::after { display: none; }

/* Nom « stats.dstny » (2026-10-16, même style que documentations.dstny) et
   compte + Déconnexion en bas de la sidebar. */
.npv-nav .brand-short { display: none; }
.npv-brand-title { font-weight: 600; letter-spacing: 0; }
.npv-brand-title .b1 { color: var(--dstny-orange-ui); }
.npv-brand-title .b2 { color: var(--dstny-cyan); }
.npv-side-account { display: flex; flex-direction: column; gap: 4px; border-top: 1px solid var(--gray-200); margin: 8px 0 4px; padding-top: 10px; }
.npv-side-user { display: flex; align-items: center; gap: 10px; min-height: 40px; padding: 0 8px; font-size: 13px; font-weight: 600; color: var(--gray-800); position: relative; overflow: hidden; }
.npv-side-user .npv-nav-label { overflow: hidden; text-overflow: ellipsis; }
.npv-nav a.npv-side-logout { color: var(--gray-600); font-size: 13px; }
.npv-nav a.npv-side-logout:hover { background: var(--red-50); color: #EF4444; }
html.npv-side-collapsed .npv-side-user { justify-content: center; padding: 0; overflow: visible; }
html.npv-side-collapsed .npv-side-user:hover::after {
    content: attr(data-tip); position: absolute; left: calc(100% + 12px); top: 50%; transform: translateY(-50%);
    background: #fff; color: var(--gray-800); box-shadow: var(--shadow-hover); border-radius: var(--radius-xs);
    padding: 6px 10px; font-size: 12.5px; font-weight: 500; white-space: nowrap; pointer-events: none;
}
.npv-top-account { display: none; align-items: center; gap: 12px; }
body.npv-shared-view .npv-top-account { display: inline-flex; }

/* Carrousel du Dashboard, diapo « SIP Trunk — Types d'équipement » (2026-10-19,
   retour explicite : « le nombre de vignettes s'adapte à la place disponible,
   avec un max à 8 ») : autant de colonnes de 150 px minimum que la largeur le
   permet ; les vignettes en trop (les moins présentes, en fin de liste) partent
   sur des lignes de hauteur nulle, masquées — jamais de retour à la ligne visible. */
#dash-evo-sip-types-tiles {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    grid-template-rows: auto; grid-auto-rows: 0; column-gap: 10px; row-gap: 0;
    flex-wrap: nowrap; margin: 0; padding: 0; overflow: hidden;
}
/* Bordure plutôt qu'ombre : l'ombre serait rognée par overflow: hidden. */
#dash-evo-sip-types-tiles .stat-tile { min-width: 0; padding: 12px 14px; box-shadow: none; border: 1px solid var(--gray-200); }
#dash-evo-sip-types-tiles .stat-tile:nth-child(n+9) { display: none; }
#dash-evo-sip-types-tiles .stat-tile-value { font-size: 22px; }
#dash-evo-sip-types-tiles .stat-tile-period { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#dash-evo-sip-types-tiles .stat-tile-breakdown { font-size: 11.5px; line-height: 1.35; }
#dash-evo-sip-types-tiles .stat-tile-spark { max-width: 100%; }
/* Mobile : la sidebar redevient une barre en haut */
@media (max-width: 900px) {
    body:not(.npv-shared-view):not(.npv-dash-tile-view) { padding-left: 0; }
    .npv-nav { position: static; width: auto; flex-direction: row; flex-wrap: wrap; align-items: center; border-radius: 0 0 var(--radius-lg) var(--radius-lg); padding: 10px 14px; }
    .npv-nav .brand { padding: 0 10px 0 0; }
    .npv-nav a { padding: 0 12px; }
    .npv-side-toggle, .npv-nav a:hover::after { display: none !important; }
    html.npv-side-collapsed .npv-nav-sub:not([hidden]) { display: block; }
    .npv-nav-sub { order: 10; flex-basis: 100%; margin: 6px 0 0; }
    .npv-nav-sub #produit-buttons { flex-direction: row; flex-wrap: wrap; }
    .npv-nav-sub #produit-buttons button { width: auto; }
    .npv-nav-label { display: inline !important; }
    .npv-nav a { justify-content: flex-start !important; padding: 0 12px !important; }
    .npv-nav .brand-full { display: inline !important; } .npv-nav .brand-short { display: none !important; }
    .npv-side-account { flex-direction: row; border-top: 0; margin: 0; padding: 0; }
    .npv-top { top: auto; bottom: 12px; right: 12px; flex-wrap: wrap; }
    .npv-cat-nav { position: static; }
}