/* ===========================
   NALIE DESIGN SYSTEM
   =========================== */

:root {
  /* Couleurs principales */
  --primary:       #4f46e5;
  --primary-dark:  #3730a3;
  --primary-light: #e0e7ff;
  --success:       #059669;
  --success-light: #d1fae5;
  --warning:       #d97706;
  --warning-light: #fef3c7;
  --danger:        #dc2626;
  --danger-light:  #fee2e2;
  --info:          #0891b2;
  --info-light:    #cffafe;

  /* Neutres — GRIS TEINTÉS BLEU-MARINE, et non gris neutres.
     ───────────────────────────────────────────────────────────────────
     Relevé dans le manager OVHcloud : le texte des titres y est en
     #131a3f et celui des cellules en #3a4a87. Ce ne sont pas des gris,
     ce sont des bleus très désaturés, et c'est ce qui donne à l'ensemble
     son air posé — un gris neutre à côté d'un bleu de marque paraît
     sale, alors qu'un gris qui tire vers la même teinte paraît choisi.

     Les DEUX bouts de la rampe sont repris tels quels d'OVH (900 et
     600) ; les stops intermédiaires sont interpolés. Les noms restent
     « gray-* » : soixante-deux écrans les appellent, et les renommer
     n'apporterait rien qu'un diff illisible. */
  --gray-50:  #f7f8fc;
  --gray-100: #eef1f8;
  --gray-200: #dee3f0;
  --gray-300: #c2c9de;
  --gray-400: #8d97b8;
  --gray-500: #5e6a91;
  --gray-600: #3a4a87;
  --gray-700: #2c3a6d;
  --gray-800: #1e2858;
  --gray-900: #131a3f;

  /* Layout
     ───────────────────────────────────────────────────────────────────
     PREMIÈRE VERSION FAUSSE, CORRIGÉE D'APRÈS L'ORIGINAL.

     J'avais supposé que les extranets récents posaient des cartes
     blanches sur un fond gris. Mesuré dans le manager OVHcloud, le
     `body` est en blanc pur : le gris n'apparaît que sur deux surfaces,
     le bandeau de titre et la ligne d'en-tête des tableaux. Le tableau
     lui-même n'est pas dans une carte du tout — ni bordure, ni ombre,
     ni rayon : il est posé à même le blanc.

     La conséquence est contre-intuitive et mérite d'être écrite : ce qui
     fait l'allure aérée n'est pas le nombre de surfaces, c'est leur
     RARETÉ. Chaque encadré ajouté est une frontière de plus à lire.

     Les rayons redescendent donc à 6/8 px : ils n'ont plus à détacher
     des cartes d'un fond, seulement à adoucir des boutons et des
     champs. Et les ombres deviennent presque nulles, réservées à ce qui
     flotte réellement (menu déroulant, dialogue, calculatrice). */
  --sidebar-width: 248px;
  --header-height: 60px;
  --border-radius: 6px;
  --border-radius-lg: 8px;
  /* Hauteur commune à tout ce qui se pose dans une barre d'actions :
     bouton d'icône, menu déroulant, liste de choix. Elles avaient dérivé
     à 26, 30 et 38 px côte à côte. */
  --h-commande: 38px;

  --shadow-sm: 0 1px 2px rgba(19,26,63,0.05);
  --shadow:    0 2px 4px -2px rgba(19,26,63,0.06), 0 4px 12px -2px rgba(19,26,63,0.08);
  --shadow-lg: 0 4px 6px -2px rgba(19,26,63,0.05), 0 12px 24px -4px rgba(19,26,63,0.10);

  /* Typographie
     ───────────────────────────────────────────────────────────────────
     La base passe de 14 à 16 px, avec un interligne de 24 px. C'est
     l'écart le plus visible avec l'original, et le plus simple à rater :
     on croit qu'un extranet moderne se reconnaît à ses couleurs, alors
     qu'il se reconnaît d'abord à ce qu'il écrit plus gros.

     Sans danger pour les largeurs : `html` ne porte aucune taille, donc
     les 436 valeurs en `rem` de cette feuille — dont toutes les colonnes
     calibrées u-col-* — restent indexées sur les 16 px du navigateur et
     ne bougent pas d'un pixel. Seul ce qui hérite de `body` grandit. */
  --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-size-base: 16px;
  --line-height: 1.5;

  /* Le bandeau de titre : la seule surface teintée d'un écran de liste.
     OVH y met un dégradé lavande ; ici une teinte dérivée de l'indigo
     maison, pour que la parenté se voie sans copier la marque. */
  --bandeau-bg: linear-gradient(105deg, #eef0fb 0%, #e6e9f8 60%, #dfe3f4 100%);

  /* ── Compatibility aliases ──────────────────────────────────────────
     Every page across the app (admin/*, services/*, auth/*, the KNX
     module) was built against these variable names in the old
     assets/css/style.css / heures.css. Keeping the names and pointing
     them at the new tokens means all existing markup re-themes for
     free, with zero markup changes. --shadow/--shadow-lg/--primary are
     already shared names above and need no alias. */
  /* Fond de page BLANC. Voir la note sur la rareté des surfaces plus haut :
     le gris est réservé au bandeau de titre et à l'en-tête de tableau. */
  --page-bg:      #ffffff;
  --bg:           var(--page-bg);
  --card-bg:      #ffffff;
  /* --surface était appelé par trois règles (choix des groupes médians, zone
     de dépôt, titre de médian) sans avoir jamais été déclaré : la propriété
     était simplement invalide et le fond restait transparent. Invisible tant
     que le fond de page frôlait le blanc, la faute se voyait dès qu'il a
     foncé. Déclaré ici, l'intention d'origine s'applique enfin. */
  --surface:      var(--card-bg);
  --text:         var(--gray-800);
  --text-muted:   var(--gray-500);
  --border:       var(--gray-200);
  --success-bg:   var(--success-light);
  --success-text: #065f46;
  --error-bg:     var(--danger-light);
  --error-text:   #991b1b;
  --radius:       var(--border-radius-lg);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: var(--font);
  font-size: var(--font-size-base);
  line-height: var(--line-height);
  color: var(--gray-800);
  background: var(--page-bg);
  min-height: 100vh;
}

/* Les liens prennent la couleur de leur texte, visités ou non.
   Sans cette règle, le navigateur applique son bleu puis son violet : dans une
   même liste, deux lignes s'affichaient de couleurs différentes selon qu'on
   avait ouvert la fiche ou non, ce qui laissait croire à une différence de
   nature entre les enregistrements.
   Le soulignement au survol conserve l'indication qu'on peut cliquer ; les
   boutons, eux, portent déjà la leur et n'en ont pas besoin. */
/* :where() ne compte pas dans le calcul de spécificité, et c'est tout l'intérêt
   ici : écrit « a:visited », le sélecteur pèse autant qu'une classe et l'emporte
   sur .btn-primary — les boutons bleus déjà cliqués perdaient leur texte blanc.
   Enveloppé, la règle ne pèse que son élément et s'efface devant n'importe quel
   habillage de composant. */
a:where(:link, :visited) { color: inherit; text-decoration: none; }
a:where(:link, :visited):where(:hover) { text-decoration: underline; }
a.btn:hover, a.sidebar-link:hover, a.filter-pill:hover, a.service-card:hover { text-decoration: none; }

/* ========== LAYOUT ========== */

.app-wrapper {
  display: flex;
  min-height: 100vh;
}

/* SIDEBAR
   ─────────────────────────────────────────────────────────────────────
   Claire, et COLLÉE AU BORD — séparée du contenu par un simple cheveu.

   J'avais d'abord détaché la colonne de 10 px, en croyant que c'était ce
   qui donnait aux extranets récents leur air aéré. Vérification faite
   dans le manager OVHcloud : la colonne y touche le bord et descend sur
   toute la hauteur. Détachée, elle ajoutait une ombre, un rayon et une
   frontière de plus à lire — l'inverse de l'effet recherché.

   Un fond différent du contenu n'est même plus nécessaire : la colonne
   et la page sont toutes deux blanches, et seule la bordure les sépare. */
.sidebar {
  width: var(--sidebar-width);
  background: var(--card-bg);
  color: var(--gray-700);
  border-right: 1px solid var(--gray-200);
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0; left: 0; bottom: 0;
  z-index: 200;
  transition: transform 0.25s ease;
  overflow-y: auto;
}

.sidebar-logo {
  padding: 18px 16px;
  border-bottom: 1px solid var(--gray-100);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Le symbole seul remplace le monogramme : il peut être nettement plus grand
   qu'un logo comportant du texte, puisque rien n'y a besoin d'être lu. */
.sidebar-logo img { height: 56px; width: auto; display: block; }
.sidebar-logo-mark {
  width: 32px; height: 32px;
  border-radius: 8px;
  background: var(--primary);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  font-weight: 700;
}

/* Les séparateurs de section disparaissent : sur fond clair, un trait tous
   les quatre items hachait la colonne. L'intitulé de section et l'espace
   suffisent à regrouper. */
.sidebar-section {
  padding: 6px 8px;
}

/* Casse normale, comme partout ailleurs dans Nalie depuis que les
   en-têtes de tableau ont quitté les capitales. J'avais gardé les
   majuscules ici en invoquant OVH, qui écrit « TRANSVERSE » — mais chez
   eux c'est une étiquette posée au-dessus d'une liste toujours ouverte.
   Ici l'intitulé est devenu un BOUTON qu'on clique : un bouton se lit
   comme un mot, et 10 px capitalisés espacés se lisent mal et se visent
   mal. La taille monte donc aussi. */
.sidebar-section-label {
  font-size: 0.8125rem;
  font-weight: 600;
  /* gray-600 et non gray-400 : sur le fond sombre d'avant, gray-400 était
     un gris clair bien détaché ; sur blanc il tombe à 2,5:1, sous le seuil
     lisible. Et depuis que l'intitulé est un bouton, il remonte encore
     d'un cran — une commande ne doit pas être plus pâle que ce qu'elle
     commande. */
  color: var(--gray-600);
  padding: 9px 8px;
}

/* ── NOUVEAUTÉS ──────────────────────────────────────────────────────
   Une colonne étroite, et c'est délibéré : c'est le seul écran de Nalie
   qui se lit comme un texte suivi. La borne est posée en `ch`, sur la
   mesure du texte lui-même, et non sur la page — la règle générale reste
   la pleine largeur. */
.changelog { max-width: 72ch; }
.changelog-entree + .changelog-entree {
  margin-top: 2rem;
  padding-top: 2rem;
  border-top: 1px solid var(--gray-100);
}
.changelog-date { font-size: 0.8125rem; color: var(--gray-500); margin-bottom: 4px; }
.changelog-titre { font-size: 1.25rem; font-weight: 600; color: var(--gray-900); margin: 0 0 12px; }
.changelog-liste { margin: 0; padding-left: 1.25rem; }
.changelog-liste li { margin-bottom: 8px; line-height: 1.6; }

/* ── BOUTON EN ICÔNE SEULE ───────────────────────────────────────────
   Pour les actions dont le symbole est compris sans lecture : partager,
   modifier, revenir.

   Une règle vaut d'être écrite, parce qu'elle décide de tout le reste :
   une icône seule n'est acceptable que si l'action est RÉVERSIBLE ou
   inoffensive. Supprimer, valider, envoyer gardent leur mot — une icône
   mal reconnue s'y paie trop cher. Et chacune porte un title ET un
   aria-label : le premier pour la souris, le second pour qui n'a ni
   souris ni vue de l'écran.

   44 px de côté : c'est la cible tactile minimale, déjà retenue ailleurs
   dans cette feuille. Un bouton d'icône est précisément le cas où l'on
   est tenté de descendre à 28 px. */
.btn-icone {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--h-commande);
  height: var(--h-commande);
  padding: 0;
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius);
  background: var(--card-bg);
  color: var(--gray-600);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.btn-icone:where(:link, :visited) { color: var(--gray-600); }
.btn-icone:hover {
  background: var(--gray-50);
  color: var(--gray-900);
  border-color: var(--gray-300);
  text-decoration: none;
}
/* 44 px sur téléphone : la cible tactile minimale, pour toute la rangée
   et pas seulement pour les icônes — un menu déroulant se vise au doigt
   aussi mal qu'un bouton. */
@media (max-width: 768px) { :root { --h-commande: 44px; } }

/* La liste d'état de la fiche dossier est de la même rangée : elle suit.
   Sans ça elle reste à 30 px et casse l'alignement à elle seule. */
.actions-cell .form-control-sm { height: var(--h-commande); }

/* ── MENU D'ACTIONS ──────────────────────────────────────────────────
   Un bouton qui déploie une liste d'actions. Il réunit les « + Ajouter … »
   qui étaient éparpillés, un par section.

   Ce qu'on gagne n'est pas seulement de la place : les quatre boutons
   étaient posés chacun au-dessus de sa section, si bien qu'il fallait
   faire défiler la page pour savoir ce qu'on pouvait ajouter à un
   dossier. Réunis, la réponse tient en un clic et au même endroit.

   Ce qu'on perd, et il faut le dire : une action dans un menu est moins
   visible qu'un bouton posé. C'est acceptable ici parce qu'aucune de ces
   actions n'est une étape obligée — on ouvre un dossier pour le
   consulter, pas pour y ajouter un contrat. */
.menu-actions { position: relative; display: inline-block; }

/* Le bouton qui déploie le menu.
   ─────────────────────────────────────────────────────────────────────
   Il portait .btn-secondary, et jurait : dans la même rangée, la liste
   d'état mesurait 30 px, lui 26, les icônes 38, et il était le seul sur
   fond gris. Trois hauteurs et deux fonds pour quatre commandes qui font
   le même travail.

   D'où un jeton plutôt qu'une valeur recopiée : --h-commande fixe la
   hauteur de tout ce qui se pose dans une barre d'actions. Si on la
   change un jour, elle change pour les quatre à la fois — c'est
   précisément la divergence qu'on vient de corriger qu'on s'interdit de
   reproduire.

   Même fond blanc et même bordure en cheveu que .btn-icone : rien ne
   distingue « + Ajouter » d'une icône sinon qu'il porte un mot. */
.menu-actions-bouton {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: var(--h-commande);
  padding: 0 12px;
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius);
  background: var(--card-bg);
  color: var(--gray-600);
  font: inherit;
  font-size: 0.875rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.menu-actions-bouton:hover {
  background: var(--gray-50);
  color: var(--gray-900);
  border-color: var(--gray-300);
}
/* Le chevron tourne quand le menu s'ouvre : le bouton dit dans quel état
   il est, plutôt que de laisser la liste le dire à sa place. */
.menu-actions-bouton .menu-actions-chevron {
  font-size: 11px;
  line-height: 1;
  transition: transform 0.18s ease;
}
.menu-actions-bouton[aria-expanded="true"] .menu-actions-chevron { transform: rotate(180deg); }
.menu-actions-liste {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 230px;
  background: var(--card-bg);
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 6px;
  z-index: 300;
}
.menu-actions-liste[hidden] { display: none; }
.menu-actions-liste a,
.menu-actions-liste button {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 10px;
  border: none;
  background: none;
  border-radius: var(--border-radius);
  font: inherit;
  font-size: 0.875rem;
  color: var(--gray-700);
  text-align: left;
  cursor: pointer;
}
.menu-actions-liste a:where(:link, :visited) { color: var(--gray-700); }
.menu-actions-liste a:hover,
.menu-actions-liste button:hover {
  background: var(--gray-50);
  color: var(--gray-900);
  text-decoration: none;
}
.menu-actions-liste .menu-actions-icone { width: 18px; text-align: center; flex-shrink: 0; }

/* ── CHAMPS VIDES ────────────────────────────────────────────────────
   Un champ sans valeur n'affiche plus « — » : il disparaît. Sur une fiche
   dossier, quatre tirets alignés occupaient autant de place que quatre
   renseignements et n'apprenaient rien.

   Mais SEULEMENT EN LECTURE. En édition (.editing), tout revient : sans
   ça, un champ jamais rempli deviendrait impossible à remplir — l'écran
   cacherait la seule chose qu'on vient y faire. C'est le piège de cette
   optimisation, et c'est pour ça que la règle est écrite avec :not(). */
.project-info:not(.editing) [data-info-field].est-vide,
.project-info:not(.editing) .est-vide { display: none; }

/* ── LE CADRE DANS LE CADRE ──────────────────────────────────────────
   .project-info portait son propre fond, sa bordure et son ombre. Posée
   dans un .card-body, elle dessinait un second encadré à quinze pixels
   du premier, sans rien séparer de plus. La carte suffit. */
.card-body > .project-info {
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  padding: 0;
}

/* ── LISTE D'AUTOCOMPLÉTION ──────────────────────────────────────────
   Partagée : les communes s'en servaient déjà sous le nom
   .commune-suggestions, les adresses KNX la reprennent. Deux listes qui se
   comporteraient différemment dans la même application s'apprendraient
   deux fois. */
.auto-suggestions {
  position: absolute;
  z-index: 400;
  max-height: 320px;
  overflow-y: auto;
  background: var(--card-bg);
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 4px;
}
.auto-suggestions[hidden] { display: none; }
.auto-suggestion {
  display: block;
  width: 100%;
  padding: 8px 10px;
  border: none;
  background: none;
  border-radius: var(--border-radius);
  font: inherit;
  font-size: 0.875rem;
  color: var(--gray-700);
  text-align: left;
  cursor: pointer;
}
.auto-suggestion:hover,
.auto-suggestion.est-surligne { background: var(--gray-50); color: var(--gray-900); }
/* L'adresse en tête, en chiffres tabulaires : les 0/1/12 s'alignent d'une
   ligne à l'autre, ce qui rend la liste parcourable d'un coup d'œil. */
.auto-suggestion-clef {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--primary-dark);
  margin-right: 2px;
}
.auto-suggestion-note {
  display: block;
  font-size: 0.75rem;
  color: var(--gray-500);
  margin-top: 1px;
}

/* ── GRILLE DE BOUTONS ───────────────────────────────────────────────
   Le nombre de colonnes vient de l'appareil, par une variable posée sur
   l'élément : c'est la seule façon de dessiner une grille dont la forme
   est une donnée et non une classe connue d'avance. */
/* Le formulaire au-dessus, la grille en dessous. Côte à côte, cela tient
   tant qu'un appareil a quatre boutons ; à dix, la grille prend toute la
   largeur et le formulaire se retrouve comprimé. */
.kx-button-panel--empile { flex-direction: column; gap: 16px; }

/* ── LE FORMULAIRE D'UN BOUTON, SUR UNE LIGNE ────────────────────────
   Empilé, il faisait défiler la page entre deux boutons : on cliquait un
   bouton, on descendait remplir, on remontait cliquer le suivant. Quatre
   champs de suite se lisent aussi bien côte à côte, et la grille reste
   visible pendant la saisie — ce qui est tout l'intérêt de l'avoir
   au-dessus.

   Les largeurs sont données en `flex-basis` et non en pourcentage :
   l'étiquette et l'adresse ont besoin de place, les notes s'accommodent
   de ce qui reste. Sur un écran étroit, la ligne se replie d'elle-même en
   deux, puis en colonne. */
/*
   UNE GRILLE, ET NON UN FLUX QUI S'ENROULE.

   En flex avec retour à la ligne, chaque champ se plaçait où il restait de
   la place : l'adresse, plus haute parce qu'elle porte un lien en dessous,
   décalait ses voisins, et la rangée partait en escalier. Une grille pose
   des colonnes fixes et des lignes qui s'alignent — c'est exactement ce
   qu'on demande ici.

   Trois colonnes, deux lignes : étiquette, action, adresse ; puis notes,
   appui long, enregistrement. Les champs qui portent une aide en dessous
   ne débordent plus sur les autres, puisque chaque cellule est
   indépendante.
*/
.kx-button-config-form--ligne {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px 14px;
  align-items: start;
  padding: 14px;
  background: var(--gray-50);
  border-radius: var(--border-radius-lg);
}
.kx-button-config-form--ligne h4 {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 0.9375rem;
  color: var(--gray-900);
}
.kx-button-config-form--ligne .form-group,
.kx-button-config-form--ligne .kx-address-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin: 0;
  min-width: 0;
}
.kx-button-config-form--ligne label {
  font-size: 0.75rem;
  color: var(--gray-500);
  margin: 0;
}
/* L'appui long se déplie sur toute la largeur : c'est un second jeu de
   champs, pas une cellule de plus dans la rangée. */
.kx-button-config-form--ligne .kx-appui-long {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px 14px;
  align-items: start;
}
.kx-button-config-form--ligne .kx-appui-long-ouvrir { margin: 0; align-self: center; }

/* L'état du bouton — ce que la déduction a proposé, une action disparue —
   occupe la dernière cellule de la rangée. L'enregistrement, lui, a quitté
   le formulaire : il porte maintenant sur l'appareil entier. */
.kx-button-config-form--ligne .kx-button-etat {
  align-self: center;
  font-size: 0.8125rem;
  min-height: 1em;          /* sinon la rangée se tasse quand il est vide */
}

/* ── `hidden` DOIT TOUJOURS GAGNER ───────────────────────────────────
   Un `display` posé par une classe l'emporte sur l'attribut `hidden`, qui
   ne vaut qu'un `display: none` de feuille par défaut.

   Ce piège s'est refermé DEUX FOIS sur ce même formulaire : d'abord sur la
   zone de configuration entière, qui s'affichait avant tout clic ; puis sur
   le bloc d'appui long, qui restait déplié alors que le lien
   « + Ajouter un appui long » s'affichait juste à côté — l'écran proposait
   d'ouvrir ce qui était déjà ouvert.

   La règle est donc posée une fois pour TOUT ce que le formulaire contient,
   et non élément par élément. `!important` est ici l'intention même : rien
   de ce qu'une classe décide ne doit rendre visible un élément que le code
   a masqué. */
.kx-button-config-form--ligne[hidden],
.kx-button-config-form--ligne [hidden] { display: none !important; }

/* ── DISTINGUER LE MÉTIER DE SES ACTIONS ─────────────────────────────
   Le titre de rubrique et les actions se ressemblaient trop : même
   graisse, même famille, à deux pixels de taille près. On lisait une
   liste plate de trente lignes dont certaines étaient des titres.

   Trois marques cumulées, parce qu'une seule ne suffit pas quand on
   parcourt vite : un fond, des capitales espacées — ici elles sont
   justifiées, c'est une étiquette de regroupement et non un mot à lire —
   et un retrait des actions sous leur rubrique. */
.kx-actions-metier-titre {
  background: var(--gray-100);
  color: var(--gray-600);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.6875rem;
}
.kx-actions-metier-titre:hover { background: var(--gray-200); color: var(--gray-900); }
.kx-actions-metier-corps { padding-left: 10px; }
.kx-actions-metier + .kx-actions-metier { margin-top: 14px; }

.kx-button-grid {
  display: grid;
  /* `auto` et non `1fr` : une colonne se dimensionne sur son contenu, pas
     sur une part de la largeur disponible. Avec 1fr, deux boutons
     s'étalaient sur tout l'écran et dix se serraient à rien — la grille
     changeait d'échelle selon l'appareil, alors qu'un bouton fait la même
     taille partout. */
  grid-template-columns: repeat(var(--kx-colonnes, 1), auto);
  gap: 6px;
  width: fit-content;
  max-width: 100%;
  /* Au-delà, on défile plutôt que de rapetisser : un appareil à quatre
     colonnes sur un écran étroit doit rester lisible. */
  overflow-x: auto;
}

/*
   LA TAILLE D'UN BOUTON, ET CE QU'ELLE DOIT TENIR.

   Le calcul : un poussoir de dix boutons fait cinq lignes sur deux
   colonnes, ou trois sur quatre. À 96 px de large, quatre colonnes tiennent
   dans 400 px — la moitié d'un écran d'ordinateur portable, et l'appareil
   reste dessiné à une échelle plausible.

   La hauteur est fixe, et c'est délibéré : des boutons de hauteurs
   différentes selon la longueur de l'étiquette ne ressembleraient plus à
   un appareil. L'étiquette est donc bornée à deux lignes et tronquée
   au-delà — le nom entier se lit dans le formulaire, qui est juste
   au-dessus.
*/
.kx-button-grid .kx-device-button {
  width: 96px;
  min-width: 0;
  height: 62px;
  min-height: 0;
  padding: 4px 6px;
  gap: 2px;
  overflow: hidden;
}
.kx-button-grid .kx-device-button-number { font-size: 0.75rem; line-height: 1; }
.kx-button-grid .kx-device-button-label {
  font-size: 0.6875rem;
  line-height: 1.15;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
.kx-button-vide { width: 96px; height: 62px; }
/* Une case vide occupe sa place sans rien afficher : resserrer les boutons
   ferait ressembler la grille à un autre appareil que celui posé au mur. */
.kx-button-vide { display: block; }

/* L'appui long, replié. Le lien reste discret : c'est une exception, pas
   une étape du réglage d'un bouton. */
.kx-appui-long { margin: 0; }
.kx-appui-long-ouvrir { display: inline-block; margin: 2px 0 12px; font-size: 0.875rem; }
.kx-appui-long-ouvrir[hidden] { display: none; }

/* ── RÉDUIRE LE MENU ─────────────────────────────────────────────────
   La colonne s'efface entièrement et le contenu prend toute la largeur.

   Effacer et non rétrécir en rail d'icônes : un rail reprend la moitié
   de la largeur gagnée pour des symboles qu'il faut deviner. Quand on
   demande l'écran entier, c'est l'écran entier.

   La classe est posée sur <html> par le script du <head> — voir
   header.php. Sur <html> et non sur <body>, parce que <body> n'existe
   pas encore au moment où il faut décider. */
.sidebar-reduire {
  display: flex;
  align-items: center;
  gap: 8px;
  width: calc(100% - 16px);
  margin: 4px 8px 12px;
  padding: 9px 10px;
  border: none;
  border-top: 1px solid var(--gray-100);
  border-radius: var(--border-radius);
  background: none;
  color: var(--gray-500);
  font: inherit;
  font-size: 0.8125rem;
  text-align: left;
  cursor: pointer;
  /* Poussé en bas de la colonne : c'est une commande qu'on règle une fois,
     pas une entrée de navigation. */
  margin-top: auto;
}
.sidebar-reduire:hover { background: var(--gray-50); color: var(--gray-900); }
.sidebar-reduire-fleche { font-size: 14px; line-height: 1; }

.menu-reduit .sidebar { transform: translateX(-100%); }
.menu-reduit .main-content { margin-left: 0; }

/* Le ☰ de la barre reparaît sur grand écran : sans lui, le menu effacé
   serait sans retour. C'est la seule façon de le rouvrir, donc il ne peut
   pas rester caché. */
.menu-reduit .sidebar-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* La transition ne porte que sur ce qui bouge. `transform` et `margin`
   s'animent sans redessiner la page ; animer `width` ferait recalculer
   la mise en page du tableau à chaque image. */
.main-content { transition: margin-left 0.25s ease; }

/* ── SECTIONS REPLIABLES ─────────────────────────────────────────────
   Comme le menu du manager OVHcloud : chaque rubrique s'ouvre au clic.
   Les trois premiers liens — Accueil, Contacts, Dossiers — restent
   toujours visibles : ils ne sont pas une rubrique, ce sont les entrées
   qu'on utilise à chaque visite, et les replier ferait payer un clic à
   chaque fois pour économiser trois lignes.

   Repliable ne veut pas dire refermé d'office : la section qui contient
   la page affichée est ouverte dès le rendu, sinon le menu contredirait
   l'écran. */
.sidebar-section-bouton {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  background: none;
  border: none;
  cursor: pointer;
  font: inherit;
  text-align: left;
}
.sidebar-section-bouton:hover { color: var(--gray-700); }
.sidebar-chevron {
  font-size: 13px;
  line-height: 1;
  transition: transform 0.18s ease;
  /* Fermé, le chevron pointe vers la droite ; ouvert, vers le bas. La
     rotation part de -90° pour que l'animation aille dans le sens de
     l'ouverture. */
  transform: rotate(-90deg);
}
.sidebar-section--pliable.est-ouverte .sidebar-chevron { transform: rotate(0deg); }

/* La fermeture passe par `display: none` et non par une hauteur animée :
   une section fermée ne doit pas rester atteignable au clavier, ce qu'une
   hauteur nulle avec overflow caché laisserait faire. */
.sidebar-section--pliable .sidebar-section-corps { display: none; }
.sidebar-section--pliable.est-ouverte .sidebar-section-corps { display: block; }

.sidebar-link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  color: var(--gray-700);
  text-decoration: none;
  font-size: 13.5px;
  /* Arrondi : l'entrée active devient une pastille dans la colonne, et non
     une bande qui la traverse de bord à bord. */
  border-radius: var(--border-radius);
  transition: background 0.15s, color 0.15s;
  cursor: pointer;
  border: none;
  background: none;
  width: 100%;
  text-align: left;
}

.sidebar-link:hover {
  background: var(--gray-100);
  color: var(--gray-900);
}

/* Actif : un fond teinté et un texte de couleur, plutôt qu'un aplat plein.
   Sur une colonne claire, l'aplat indigo pleine largeur pèse trop lourd et
   tire l'œil loin du contenu, qui est ce qu'on vient lire. */
.sidebar-link.active {
  background: var(--primary-light);
  color: var(--primary-dark);
  font-weight: 500;
}

.sidebar-link .icon {
  width: 18px;
  text-align: center;
  flex-shrink: 0;
  font-size: 15px;
}

/* Pastille de rappel dans le menu — migrations restant à appliquer. Poussée à
   droite pour rester lisible quelle que soit la longueur du libellé. */
.sidebar-badge {
  margin-left: auto;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 10px;
  /* Pastille TEINTÉE, plus pleine. Un rond rouge vif se justifiait sur une
     colonne sombre, où il fallait ce contraste pour exister ; sur la colonne
     claire il devient l'élément le plus criard de l'écran alors qu'il ne
     compte que des lignes en attente. Le fond pâle et le texte foncé se
     lisent aussi bien et ne hurlent pas. */
  background: var(--danger-light, #fee2e2);
  color: #991b1b;
  font-size: 11px;
  font-weight: 700;
  text-align: center;
}

/* MAIN CONTENT
   La colonne est revenue au bord : la largeur réservée redevient sa seule
   largeur, sans la marge qu'exigeait la version détachée. */
.main-content {
  margin-left: var(--sidebar-width);
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  width: 100%;
}

/* TOP BAR
   ─────────────────────────────────────────────────────────────────────
   Épurée : plus de bandeau blanc barré d'un trait et souligné d'une ombre.

   Le raisonnement tient à ce qu'on regarde. Une barre blanche sur fond gris
   crée une troisième surface, en concurrence avec la barre latérale et les
   cartes, pour ne porter qu'un titre et un menu de compte. Posée sur le fond
   de page, elle disparaît en tant qu'objet et laisse les cartes seules à
   détacher l'information.

   Le fond reste OPAQUE malgré tout : la barre est collante, et une barre
   transparente laisserait défiler les lignes d'un tableau sous le titre.

   Le cheveu du bas revient, en revanche : depuis que la page est blanche,
   la barre et le contenu ont la même couleur, et sans trait un tableau
   qui défile semble passer À TRAVERS le titre plutôt que derrière. */
.topbar {
  height: var(--header-height);
  background: var(--page-bg);
  border-bottom: 1px solid var(--gray-200);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 24px;
  position: sticky;
  top: 0;
  z-index: 100;
}

/* BANDEAU DE DÉMONSTRATION — sous la barre du haut, sur chaque page de la
   société bac à sable (includes/header.php). Ambre, comme les avertissements :
   il ne signale pas une erreur, il prévient. */
.bandeau-demo {
  background: var(--warning-light);
  color: #92400e;
  border-bottom: 1px solid #fde68a;
  padding: 8px 24px;
  font-size: 0.85rem;
  line-height: 1.45;
}

.topbar-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--gray-800);
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: 12px;
}

.topbar-user {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 6px 10px;
  border-radius: var(--border-radius);
  transition: background 0.15s;
  text-decoration: none;
  color: var(--gray-700);
  font-size: 13px;
}

.topbar-user:hover { background: var(--gray-100); }

/* CALCULATRICE — petite fenêtre flottante, dans l'esprit de la calculatrice
   d'Apple : fond sombre, touches rondes, opérateurs en orange. Elle se déplace
   en attrapant sa barre de titre. */
.calc-zone { position: relative; display: flex; align-items: center; }

.calc-bouton {
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--gray-200);
  background: #fff;
  color: var(--gray-700);
  border-radius: var(--border-radius);
  cursor: pointer;
  font-size: 14px;
  transition: background 0.15s, border-color 0.15s;
}
.calc-bouton:hover { background: var(--gray-100); }
.calc-bouton[aria-expanded="true"] { background: var(--primary); border-color: var(--primary); color: #fff; }

.calc-fenetre {
  position: absolute;
  top: calc(100% + 10px);
  right: 0;
  width: 288px;
  padding: 0 14px 16px;
  background: #1c1c1e;
  border-radius: 14px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.08);
  z-index: 200;
  user-select: none;
}
.calc-fenetre:focus { outline: none; }
/* Sans cette règle, le display du conteneur écraserait l'attribut hidden. */
.calc-fenetre[hidden] { display: none; }
/* Une fois déplacée, la fenêtre quitte l'ancrage sous le bouton. */
.calc-fenetre.calc-libre { position: fixed; top: auto; right: auto; }

.calc-barre {
  height: 30px;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: grab;
  margin: 0 -14px;
  padding: 0 14px;
}
.calc-barre:active { cursor: grabbing; }
.calc-titre {
  font-size: 11px;
  color: #8e8e93;
  letter-spacing: 0.02em;
}
.calc-fermer {
  width: 11px; height: 11px;
  border-radius: 50%;
  border: 0;
  background: #ff5f57;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.calc-fermer:hover { background: #ff3b30; }

.calc-ecran {
  text-align: right;
  padding: 2px 4px 10px;
  overflow: hidden;
}
.calc-ecran-apercu {
  min-height: 16px;
  font-size: 12px;
  color: #8e8e93;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.calc-ecran-valeur {
  font-size: 42px;
  line-height: 1.15;
  font-weight: 300;
  color: #fff;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.calc-touches { display: grid; grid-template-columns: repeat(4, 1fr); gap: 9px; }

.calc-t {
  height: 58px;
  border: 0;
  border-radius: 50%;
  background: #505050;
  color: #fff;
  font-size: 24px;
  font-weight: 400;
  cursor: pointer;
  transition: filter 0.1s;
}
.calc-t:hover  { filter: brightness(1.25); }
.calc-t:active { filter: brightness(1.5); }

.calc-t-fonction { background: #a5a5a5; color: #1c1c1e; font-weight: 500; }
.calc-t-op       { background: #ff9f0a; color: #fff; font-size: 27px; }
/* L'opérateur en attente s'inverse : on voit d'un coup d'œil ce qu'on est en
   train de poser après avoir tapé le premier nombre. */
.calc-t-op.est-actif { background: #fff; color: #ff9f0a; }

.calc-t-zero {
  grid-column: span 2;
  border-radius: 30px;
  text-align: left;
  padding-left: 25px;
}

/* Sortie : discrète au repos, rouge au survol. Un bouton toujours rouge à côté
   du nom attire l'œil sur l'action la moins fréquente de la barre. */
.topbar-sortie {
  width: 34px; height: 34px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--border-radius);
  color: var(--gray-500);
  font-size: 14px;
  transition: background 0.15s, color 0.15s;
}
.topbar-sortie:hover { background: var(--danger-light, #fee2e2); color: var(--danger, #dc2626); text-decoration: none; }

.user-avatar {
  width: 30px; height: 30px;
  border-radius: 50%;
  background: var(--primary);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 600;
  flex-shrink: 0;
}

/* PAGE CONTENT
   La garniture passe par un jeton : le bandeau de titre doit la compenser
   en marge négative pour aller au bord, et sur téléphone elle change. Deux
   endroits à garder d'accord — un seul chiffre. */
.page-content {
  --page-pad: 24px;
  padding: var(--page-pad);
  flex: 1;
  /* Plus de plafond à 1400 px. Sur un écran large, il laissait un tiers de
     la fenêtre vide à droite pendant que les colonnes des tableaux se
     tronquaient à gauche — le pire des deux mondes.

     Le plafond existait pour la raison habituelle : une ligne de texte de
     2000 px est illisible, l'œil perd sa ligne en revenant à la marge.
     Sauf que Nalie n'affiche presque jamais de prose ; elle affiche des
     tableaux, qui eux profitent de chaque pixel. Là où il y a vraiment du
     texte suivi — la phrase sous un titre de page — la borne est posée sur
     le paragraphe lui-même, en `ch`, et non sur la page entière. C'est le
     bon endroit : elle suit le texte au lieu de contraindre tout le reste. */
  max-width: none;
  width: 100%;
}
.page-content.full-width { max-width: 100%; }

.page-header {
  margin-bottom: 24px;
}

/* 2,25 rem = 36 px, graisse 600 : la valeur relevée chez OVH.
   Nalie titrait à 22 px en graisse 700. Plus petit ET plus gras — c'est
   le réglage d'un titre qui doit s'imposer faute de place. Plus grand et
   moins gras s'impose mieux, et laisse le gras aux mots qui comptent
   dans la page. */
.page-header h1 {
  font-size: 2.25rem;
  font-weight: 600;
  color: var(--gray-900);
  line-height: 1.2;
}

/* La phrase sous le titre n'est plus une mention en petits caractères :
   chez OVH elle explique à quoi sert la section, en pleine taille. */
.page-header p {
  font-size: 1rem;
  color: var(--gray-600);
  margin-top: 8px;
  max-width: 70ch;
}

/* ══════════════════════════════════════════════════════════════════════
   GABARIT DE LISTE
   ══════════════════════════════════════════════════════════════════════
   Relevé dans le manager OVHcloud, où les trois pièces vont ensemble :

     fil d'Ariane  →  situe la page dans l'arborescence
     bandeau       →  le titre, en grand, ET une phrase qui dit à quoi
                      sert la section
     barre d'outils→  l'action principale à gauche, chercher et filtrer
                      à droite

   La phrase d'intention est la pièce que Nalie n'avait nulle part, et
   c'est celle qui manquait le plus. Un écran qui affiche « Dossiers »
   puis un tableau suppose que le lecteur sait déjà ce qu'il regarde ;
   celui qui ajoute « Chaque affaire suivie pour un client » l'apprend à
   celui qui ne le sait pas, sans gêner celui qui le sait. */

.bandeau {
  background: var(--bandeau-bg);
  /* Au bord : la garniture de .page-content est compensée pour que la
     teinte file jusqu'aux bords, comme chez OVH. Sans ça le bandeau
     flotte en carte, ce qui est précisément le patron qu'on quitte. */
  margin: calc(var(--page-pad, 24px) * -1) calc(var(--page-pad, 24px) * -1) 0;
  padding: 16px var(--page-pad, 24px) 28px;
}
/* Le fil d'Ariane se place DANS le bandeau, pas au-dessus.
   Vu à l'écran : posé dessus, il laissait une bande blanche de 30 px
   entre la barre du haut et la teinte — une lame de fond qui n'avait
   aucune raison d'être là. Chez OVH il est dans la teinte, et c'est
   cohérent : il appartient au titre, il en est l'adresse. */
.bandeau .fil-ariane { margin-bottom: 8px; }

/* ── L'EN-TÊTE DE PAGE ───────────────────────────────────────────────
   Le fil d'Ariane EN HAUT, collé sous la barre : c'est la première ligne
   de la page, sans marge au-dessus. Le nom de la page en dessous, en
   grand. Le tout dans la teinte, comme le manager OVHcloud.

   Sans phrase d'intention : leurs sections portent des noms que le client
   ne connaît pas (« Bare Metal Cloud »), d'où l'explication sous le titre.
   « Dossiers » n'en a pas besoin. On garde la forme, on laisse la pièce
   qui ne répond à aucun besoin ici. */
/* ── L'EN-TÊTE DE PAGE ───────────────────────────────────────────────
   Le fil d'Ariane est dans la BARRE DU HAUT (voir header.php), et le bloc
   de titre que chaque écran porte déjà — .admin-header ou .page-header —
   prend la teinte juste en dessous. À l'écran : fil d'Ariane collé tout
   en haut, nom de la page en grand dessous, teinte filant jusqu'aux
   bords. Le gabarit du manager OVHcloud.

   Pourquoi le CSS plutôt que les cinquante-neuf pages : le <h1> de chaque
   écran reste le sien. Aucun titre ne peut devenir faux, puisqu'aucun
   n'est réécrit. Et un écran qui n'a ni .admin-header ni .page-header
   garde simplement son fil d'Ariane, sans rien de cassé.

   La classe .a-fil-ariane est posée par header.php. Elle est nécessaire
   parce que le fil d'Ariane a quitté .page-content pour la barre : un
   :has() ne pouvait plus le trouver depuis le contenu. */
/* Le fil d'Ariane dans la barre : il doit se COUPER, jamais pousser.
   ─────────────────────────────────────────────────────────────────────
   Sur iPhone, « Aménagement d'une cuisine — GRANGE » repliait le fil sur
   trois lignes, qui passaient sous les icônes : la barre a une hauteur
   fixe, et ce qui déborde ne la repousse pas, il la traverse.

   Donc nowrap, et min-width:0 — sans ce zéro, un élément flex refuse de
   descendre sous la largeur de son contenu et l'ellipsis n'arrive jamais
   à s'appliquer. C'est le piège classique, et il ne se voit qu'à
   l'écran. */
.topbar .fil-ariane--auto {
  margin: 0;
  font-size: 0.9375rem;
  flex-wrap: nowrap;
  min-width: 0;
  overflow: hidden;
}
.topbar .fil-ariane--auto > * { flex-shrink: 0; }
.topbar .fil-ariane--auto [aria-current="page"] {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex-shrink: 1;
}
.fil-ariane-section { color: var(--gray-500); }
.topbar .fil-ariane--auto [aria-current="page"] { color: var(--gray-900); font-weight: 600; }

/* Sur téléphone, seul le dernier maillon reste. Les ancêtres coûtent la
   largeur dont le nom de la page a besoin, et la remontée se fait de
   toute façon par le menu — qui est à un pouce du fil d'Ariane. */
@media (max-width: 768px) {
  .topbar .fil-ariane--auto a,
  .topbar .fil-ariane--auto .fil-ariane-sep,
  .topbar .fil-ariane--auto .fil-ariane-section { display: none; }

  /* Le prénom laisse la place au nom de la page : il occupait 104 px
     des 375 d'un iPhone pour une information qu'on connaît déjà —
     personne n'a besoin qu'on lui rappelle qui il est. La pastille aux
     initiales reste, elle sert de repère au compte et de cible de clic
     vers la fiche. */
  .topbar-user span:not(.user-avatar) { display: none; }
  .topbar-user { padding: 6px; }
}

/* Le bloc de gauche de la barre doit pouvoir rétrécir, lui aussi : sinon
   il garde la largeur de son contenu et recouvre les actions de droite,
   quelles que soient les règles posées à l'intérieur. */
.topbar > .d-flex:first-child { min-width: 0; flex: 1 1 auto; }
.topbar > .topbar-actions { flex: 0 0 auto; }

/* :first-child, et c'est tout le sujet. .admin-header ne désigne pas
   seulement l'en-tête d'une page : la fiche dossier s'en sert aussi pour
   titrer chacune de ses sections — Contrats, Matériel installé, Nature
   des travaux. Sans cette borne, la teinte du bandeau se répétait en
   travers de la page à chaque section, et l'écran devenait un empilement
   de bandes grises. Vu à l'écran, pas dans le code.

   Le premier enfant de .admin-page ou de .page-content, donc : les
   en-têtes de section sont, eux, imbriqués dans un .chantier-contrats ou
   un .chantier-hub et ne peuvent pas satisfaire cette condition. */
.main-content.a-fil-ariane .admin-page > .admin-header:first-child,
.main-content.a-fil-ariane .page-content > .admin-header:first-child,
.main-content.a-fil-ariane .admin-page > .page-header:first-child,
.main-content.a-fil-ariane .page-content > .page-header:first-child {
  background: var(--bandeau-bg);
  margin: calc(var(--page-pad, 24px) * -1) calc(var(--page-pad, 24px) * -1) 24px;
  padding: 18px var(--page-pad, 24px) 22px;
  align-items: flex-end;
}
.admin-header h1 { font-size: 2.25rem; font-weight: 600; color: var(--gray-900); line-height: 1.15; }

/* Nalie porte DEUX gabarits de titre : .admin-header sur soixante-deux
   écrans, .page-header sur deux. Le second n'est pas une coquille à
   supprimer — il sert là où le titre est accompagné d'une phrase — mais
   il doit prendre la teinte comme l'autre, sinon deux écrans voisins ne
   commencent pas pareil. */
.main-content.a-fil-ariane .page-header p { color: var(--gray-600); margin-top: 6px; }

/* Le .admin-page enveloppe le contenu. S'il ouvrait une marge en haut,
   elle se glisserait entre la barre et le bandeau et couperait la teinte
   du haut de la page. */
.main-content.a-fil-ariane .admin-page { margin-top: 0; }

/* L'ACCUEIL n'a pas de fil d'Ariane — il EST la racine — donc pas de
   .a-fil-ariane et pas de teinte automatique. Il la prend quand même :
   chez OVH c'est le grand aplat « Bonjour Grégory ! » qui ouvre la page,
   et c'est la seule page où le bandeau porte un vrai message plutôt
   qu'une étiquette de section. */
.main-content:not(.a-fil-ariane) .page-content > .page-header:first-child {
  background: var(--bandeau-bg);
  margin: calc(var(--page-pad, 24px) * -1) calc(var(--page-pad, 24px) * -1) 24px;
  padding: 32px var(--page-pad, 24px) 36px;
}
.page-header h1 { font-size: 2.25rem; font-weight: 600; color: var(--gray-900); line-height: 1.15; }

/* ── FOND DE PAGE GRIS, CONTENU SUR SURFACES BLANCHES ────────────────
   Posé par $pageFondGris. Deux familles d'écrans s'en servent : l'accueil,
   qui aligne une dizaine de blocs, et les LISTES, où le gris encadre le
   tableau.

   CE QUI A CHANGÉ D'AVIS, ET POURQUOI C'EST ÉCRIT ICI. Le fond des listes
   était passé au blanc en septembre après avoir relevé la page
   « Hébergements » du manager OVHcloud, où le tableau est posé à même le
   blanc. La note disait : un cadre autour d'un tableau pleine largeur ne
   délimite rien. Puis la page « Domaines & DNS » du même manager a montré
   l'inverse — fond gris, tableau en surface blanche à bord arrondi.

   OVH n'est donc pas d'accord avec lui-même d'une section à l'autre, et
   aucune mesure ne pouvait trancher : c'est un choix, pas un relevé. Grégory
   a tranché pour le gris sur les listes. Le blanc reste partout ailleurs —
   fiches et formulaires — parce que les passer au gris obligerait à faire
   de chacun une surface, soit une soixantaine d'écrans à reprendre pour un
   liseré.

   Le bandeau de titre garde sa teinte et reste au bord : le gris ne
   commence qu'après lui, comme chez OVH. */
.page-grise { background: #f4f5f8; }

/* LA LISTE DEVIENT UNE SURFACE.
   Sur fond blanc, le tableau se délimitait tout seul — le vide autour de
   lui suffisait. Sur gris, il flotte : la bordure et le rayon ne sont pas
   une décoration ajoutée, ils remplacent ce que le blanc faisait
   gratuitement.

   L'en-tête passe à un blanc à peine teinté plutôt qu'au gris habituel :
   sur une surface blanche posée sur gris, un en-tête gris rappellerait le
   fond de page et couperait la carte en deux. */
.page-grise .table-responsive {
  background: var(--card-bg);
  border: 1px solid var(--gray-200);
  border-radius: 10px;
}
.page-grise .data-table th { background: #fbfcfe; }
.page-grise .data-table th:first-child { border-top-left-radius: 10px; }
.page-grise .data-table th:last-child  { border-top-right-radius: 10px; }
/* La dernière ligne n'a pas de trait : il doublerait la bordure de la
   carte, à un pixel près, et ce genre de doublon se voit. */
.page-grise .data-table tbody tr:last-child td { border-bottom: none; }

/* La colonne collante s'adosse à la carte, donc au blanc : laissée au gris
   clair, elle dessinait une bande grise au milieu d'une surface blanche dès
   qu'on faisait défiler. */
.page-grise .table-responsive .data-table th.u-col-sticky { background: #fbfcfe; }
.page-grise .table-responsive .data-table td.u-col-sticky,
.page-grise .data-table tbody tr:hover td.u-col-sticky { background: var(--card-bg); }
/* .form-card ne portait AUCUN style. Sept écrans l'utilisaient pour leur
   formulaire de création, en attendant d'une classe nommée « carte »
   qu'elle en dessine une : ils affichaient un formulaire nu. Sur fond
   blanc le défaut passait inaperçu ; sur gris, le formulaire flotterait.
   La classe tient enfin ce que son nom annonce. */
.form-card {
  background: var(--card-bg);
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 16px 18px;
  margin-bottom: 18px;
}

.page-grise .cockpit-tuile,
.page-grise .stat-card,
.page-grise .service-card,
.page-grise .form-card,
.page-grise .card {
  background: var(--card-bg);
  border: 1px solid var(--gray-200);
  border-radius: 12px;
  box-shadow: none;
}
/* Le liseré de gauche des tuiles reste : il porte un sens (alerte,
   attention, neutre) que ni la couleur de fond ni le titre ne disent. */
.page-grise .cockpit-tuile { border-left-width: 4px; }
.page-grise .stat-card,
.page-grise .service-card,
.page-grise .cockpit-tuile { padding: 18px 20px; }

.bandeau-titre {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.bandeau h1 {
  font-size: 2.25rem;
  font-weight: 600;
  color: var(--gray-900);
  line-height: 1.15;
  margin: 0;
}
.bandeau-intention {
  font-size: 1rem;
  color: var(--gray-600);
  margin: 8px 0 0;
  /* 70 caractères : au-delà, l'œil perd sa ligne en revenant à la marge.
     Le bandeau étant pleine largeur, sans cette borne la phrase courrait
     sur 160 caractères et personne ne la lirait. */
  max-width: 70ch;
}

.barre-outils {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 20px 0 16px;
}
/* Tout ce qui suit est repoussé à droite : chercher et filtrer agissent
   sur la liste déjà à l'écran, tandis que l'action principale, à gauche,
   en crée une entrée de plus. Les séparer évite de cliquer « Nouveau »
   en voulant filtrer. */
.barre-outils-fin {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* Le champ de recherche des listes. Il vivait dans le <style> de la page
   Dossiers ; remonté ici parce que la barre d'outils est partagée et que
   les autres listes vont le reprendre.

   LA LOUPE EST DANS LE CHAMP, pas à côté. Chez OVH elle est posée sur le
   bord gauche, à l'intérieur : elle dit ce qu'est le champ sans coûter une
   étiquette au-dessus ni un mot à côté. Un champ nu de 260 px dans une
   barre d'outils se confond avec un champ de saisie ordinaire — et sur une
   page qui en porte d'autres, on tape sa recherche dans le mauvais.

   Le padding gauche de 34 px est ce qui fait que le texte ne passe pas
   SOUS la loupe : sans lui, l'icône et la première lettre se superposent
   dès la première frappe. */
.champ-recherche-wrap { position: relative; display: inline-flex; align-items: center; }
.champ-recherche-wrap .champ-recherche { padding-left: 34px; }
.champ-recherche-loupe {
  position: absolute;
  left: 11px;
  width: 14px;
  height: 14px;
  color: var(--gray-400);
  pointer-events: none;   /* sinon le clic sur la loupe ne donne pas le focus */
}

.champ-recherche {
  width: 260px;
  max-width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius);
  font-size: 0.875rem;
  font-family: inherit;
  background: var(--card-bg);
  color: var(--text);
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.champ-recherche:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-light);
}
@media (max-width: 700px) {
  /* Sur téléphone la barre passe en colonne : le champ prend la largeur
     plutôt que de rester à 260 px collé à droite. */
  .barre-outils, .barre-outils-fin { width: 100%; }
  .barre-outils-fin { margin-left: 0; }
  .champ-recherche-wrap { width: 100%; }
  .champ-recherche { width: 100%; }
}

/* Première colonne sur deux lignes : le nom en lien, la précision en
   dessous. Chez OVH, « eilan-informatique.fr » se lit sous le libellé de
   l'hébergement plutôt que dans une colonne à part — une colonne de plus
   coûte de la largeur à toutes les lignes pour une information qui ne se
   lit qu'avec la première.
   Écrit « .data-table td.… » et non « .cellule-double » seul : à
   spécificité égale, le nowrap de .data-table td l'emporterait. */
.data-table td.cellule-double { white-space: normal; }
.cellule-double-titre { display: block; font-weight: 500; color: var(--primary-dark); }
.cellule-double-sous  { display: block; font-size: 0.8125rem; color: var(--gray-500); margin-top: 2px; }

.breadcrumb, .fil-ariane {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.875rem;
  color: var(--gray-500);
  margin-bottom: 10px;
  flex-wrap: wrap;
}
.fil-ariane a:where(:link, :visited) { color: var(--primary-dark); }
.fil-ariane a:hover { text-decoration: underline; }
.fil-ariane-sep { color: var(--gray-300); }
.fil-ariane [aria-current="page"] { color: var(--gray-600); }
.breadcrumb a { color: var(--primary); text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb-sep { color: var(--gray-300); }

/* ========== CARDS ========== */
.card {
  background: #fff;
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--gray-200);
  overflow: hidden;
}

.card-header {
  padding: 16px 20px;
  border-bottom: 1px solid var(--gray-100);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.card-header h2, .card-header h3 {
  font-size: 15px;
  font-weight: 600;
  color: var(--gray-800);
}

.card-body { padding: 20px; }

.card-footer {
  padding: 12px 20px;
  background: var(--gray-50);
  border-top: 1px solid var(--gray-100);
}

/* Service cards for dashboard */
.service-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 16px;
}

.service-card {
  background: #fff;
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius-lg);
  padding: 20px;
  text-decoration: none;
  color: inherit;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  transition: box-shadow 0.15s, border-color 0.15s, transform 0.15s;
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}

.service-card:hover {
  box-shadow: var(--shadow);
  border-color: var(--primary);
  transform: translateY(-1px);
}

.service-card-icon {
  width: 44px; height: 44px;
  border-radius: 10px;
  background: var(--primary-light);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  margin-bottom: 4px;
}

.service-card-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--gray-800);
}

.service-card-desc {
  font-size: 12.5px;
  color: var(--gray-500);
  line-height: 1.4;
}

/* ========== FORMS ========== */
.form-group { margin-bottom: 16px; }

.form-label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: var(--gray-700);
  margin-bottom: 6px;
}

.form-label .required { color: var(--danger); margin-left: 2px; }

.form-control {
  display: block;
  width: 100%;
  padding: 9px 12px;
  font-size: 14px;
  font-family: var(--font);
  color: var(--gray-800);
  background: #fff;
  border: 1px solid var(--gray-300);
  border-radius: var(--border-radius);
  transition: border-color 0.15s, box-shadow 0.15s;
  line-height: 1.4;
}

.form-control:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(79,70,229,0.1);
}

.form-control:disabled, .form-control[readonly] {
  background: var(--gray-100);
  color: var(--gray-500);
  cursor: not-allowed;
}

.form-control.is-invalid { border-color: var(--danger); }
.form-text { font-size: 12px; color: var(--gray-500); margin-top: 4px; }
.invalid-feedback { font-size: 12px; color: var(--danger); margin-top: 4px; }

/* ========== BUTTONS ========== */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 9px 16px;
  font-size: 13.5px;
  font-weight: 500;
  font-family: var(--font);
  border: 1px solid transparent;
  border-radius: var(--border-radius);
  cursor: pointer;
  text-decoration: none;
  transition: all 0.15s;
  white-space: nowrap;
  line-height: 1;
}

.btn:disabled { opacity: 0.6; cursor: not-allowed; }

.btn-primary   { background: var(--primary); color: #fff; border-color: var(--primary); }
.btn-primary:hover { background: var(--primary-dark); border-color: var(--primary-dark); }

.btn-success   { background: var(--success); color: #fff; border-color: var(--success); }
.btn-danger    { background: var(--danger);  color: #fff; border-color: var(--danger); }
.btn-warning   { background: var(--warning); color: #fff; border-color: var(--warning); }

.btn-outline {
  background: transparent;
  color: var(--gray-700);
  border-color: var(--gray-300);
}
.btn-outline:hover { background: var(--gray-100); }

.btn-outline-primary {
  background: transparent;
  color: var(--primary);
  border-color: var(--primary);
}
.btn-outline-primary:hover { background: var(--primary-light); }

.btn-sm { padding: 6px 12px; font-size: 12.5px; }
.btn-lg { padding: 12px 20px; font-size: 15px; }
.btn-icon { padding: 8px; }

/* ========== TABLES ========== */
.table-wrapper {
  width: 100%;
  overflow-x: auto;
  border-radius: var(--border-radius-lg);
  border: 1px solid var(--gray-200);
  background: #fff;
}

table.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13.5px;
}

/* Le jumeau de .data-table th, resté en arrière. Les deux classes
   coexistent depuis l'ancienne feuille ; l'une est passée en casse
   normale et pas l'autre, si bien qu'un écran titrait ses colonnes
   autrement que son voisin selon la classe qu'il avait reprise. */
.table th {
  background: var(--gray-50);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--gray-900);
  padding: 12px 16px;
  text-align: left;
  border-bottom: 1px solid var(--gray-200);
  white-space: nowrap;
}

.table td {
  padding: 12px 16px;
  border-bottom: 1px solid var(--gray-100);
  color: var(--gray-700);
  vertical-align: middle;
}

.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover td { background: var(--gray-50); }

/* ========== BADGES ========== */
/* Les pastilles d'état.
   11,5 px, c'était la taille d'une note de bas de page pour une
   information qu'on lit en diagonale — « Actif », « Archivé », un rôle.
   OVH les écrit à 13 px avec de l'air autour : à peine plus grand, mais
   la pastille devient un objet qu'on repère au lieu d'un mot à déchiffrer. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 11px;
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.3;
  border-radius: 999px;
  white-space: nowrap;
}

.badge-primary { background: var(--primary-light); color: var(--primary-dark); }
.badge-success { background: var(--success-light); color: var(--success); }
.badge-warning { background: var(--warning-light); color: var(--warning); }
.badge-danger  { background: var(--danger-light);  color: var(--danger); }
.badge-gray    { background: var(--gray-100); color: var(--gray-600); }

/* ========== ALERTS ========== */
.alert {
  padding: 12px 16px;
  border-radius: var(--border-radius);
  font-size: 13.5px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 16px;
}
.alert[hidden] { display: none; }

.alert-success { background: var(--success-light); color: #065f46; border: 1px solid #a7f3d0; }
.alert-danger  { background: var(--danger-light);  color: #991b1b; border: 1px solid #fca5a5; }
.alert-warning { background: var(--warning-light); color: #92400e; border: 1px solid #fde68a; }
.alert-info    { background: var(--info-light);    color: #164e63; border: 1px solid #a5f3fc; }

/* ========== STAT CARDS ========== */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}

.stat-card {
  background: #fff;
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius-lg);
  padding: 16px;
  box-shadow: var(--shadow-sm);
}

.stat-label { font-size: 12px; color: var(--gray-500); font-weight: 500; }
.stat-value { font-size: 24px; font-weight: 700; color: var(--gray-900); margin: 4px 0; line-height: 1; }
.stat-sub   { font-size: 12px; color: var(--gray-400); }

/* ========== MOBILE SIDEBAR TOGGLE ========== */
.sidebar-toggle {
  display: none;
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px;
  color: var(--gray-700);
  font-size: 20px;
}

.sidebar-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.4);
  z-index: 150;
}

/* ========== RESPONSIVE ========== */
@media (max-width: 768px) {
  .sidebar {
    /* La colonne touche de nouveau le bord : -100 % suffit à la sortir. */
    transform: translateX(-100%);
  }
  .sidebar.open {
    transform: translateX(0);
  }
  .sidebar-overlay.open {
    display: block;
  }
  .main-content {
    margin-left: 0;
  }
  .sidebar-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .page-content {
    /* Le jeton, pas la propriété : le bandeau de titre lit --page-pad pour
       calculer sa marge négative, et déborderait de 8 px si on changeait
       seulement le padding ici. */
    --page-pad: 16px;
  }
  .service-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .stat-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .table th, .table td {
    padding: 10px 12px;
    font-size: 12.5px;
  }
  .topbar { padding: 0 16px; }
}

@media (max-width: 480px) {
  .service-grid { grid-template-columns: 1fr 1fr; }
  .stat-grid { grid-template-columns: 1fr 1fr; }
}

/* ========== UTILITIES ========== */
.d-flex { display: flex; }
.align-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-8 { gap: 8px; }
.gap-12 { gap: 12px; }
.gap-16 { gap: 16px; }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mb-16 { margin-bottom: 16px; }
.mb-24 { margin-bottom: 24px; }
.text-muted { color: var(--gray-500); }
.text-sm { font-size: 12.5px; }
.fw-600 { font-weight: 600; }
.w-100 { width: 100%; }

/* =====================================================================
   LEGACY COMPATIBILITY LAYER
   =====================================================================
   Everything below re-themes the existing app's class vocabulary (used
   across ~100 pages: admin/company tables, auth-card pages, the KNX
   project module, timesheets/navette) onto the tokens above, so no
   markup elsewhere in the app had to change. New pages (login,
   dashboard) use only the design-system classes above; every other
   page keeps its existing classes and inherits the new look through
   the rules in this section. */

/* Auth-card pages (register.php, change_password.php, profile.php,
   verify_email.php, resend_verification.php, migrate.php) */
.auth-container {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: calc(100vh - var(--header-height) - 48px);
}

.auth-card {
  background-color: var(--card-bg);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 2.5rem;
  width: 100%;
  max-width: 420px;
}

.auth-card-wide { max-width: 640px; }

.auth-card h1 {
  margin-bottom: 1.5rem;
  font-size: 1.6rem;
  text-align: center;
}

.auth-switch {
  text-align: center;
  margin-top: 1.25rem;
  color: var(--text-muted);
  font-size: 0.95rem;
}

.auth-switch a {
  color: var(--primary);
  font-weight: 600;
  text-decoration: none;
}

.auth-switch a:hover { text-decoration: underline; }

@media (max-width: 767px) {
  .auth-card { padding: 1.75rem; }
}

/* Grille du formulaire de contact. La règle de base vivait dans un attribut
   style= sur la balise, ce qui l'emportait sur la version mobile ci-dessous :
   le formulaire restait à deux colonnes sur un téléphone, chacune trop étroite
   pour son champ. Les deux règles sont désormais ici, où elles se répondent. */
.contact-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

@media (max-width: 768px) {
  .contact-form-grid { grid-template-columns: 1fr; }
  /* Un champ déclaré sur deux colonnes n'en a plus qu'une à occuper. */
  .contact-form-grid > [style*="grid-column"] { grid-column: auto !important; }
}

/* Forms: existing markup styles bare <input>/<textarea>/<select> tags
   inside .form-group rather than adding a .form-control class — mirror
   .form-control's look onto that existing selector. */
.form-group label {
  display: block;
  margin-bottom: 0.4rem;
  font-weight: 500;
  font-size: 0.92rem;
  color: var(--gray-700);
}

.form-group input,
.form-group textarea,
.form-group select {
  width: 100%;
  padding: 9px 12px;
  border: 1px solid var(--gray-300);
  border-radius: var(--border-radius);
  font-size: 1rem;
  font-family: inherit;
  color: var(--gray-800);
  background: #fff;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.form-group textarea { resize: vertical; }

.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}

input[readonly] {
  background-color: var(--gray-100);
  color: var(--text-muted);
  cursor: not-allowed;
}

.form-hint { color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1rem; }
.form-hint-inline { font-weight: 400; font-size: 0.82rem; color: var(--text-muted); }
/* .form-row n'avait aucun style non plus — trente-trois blocs dans le
   dépôt. Sans display, l'étiquette et le champ restent en ligne et se
   collent : « Nom * [Ex : Télésurveillance] » sur une seule ligne, sans
   respiration, et l'aide en dessous qui déborde sur le champ suivant.
   Le gabarit est celui du reste du système : intitulé au-dessus. */
.form-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 14px;
  max-width: 100%;
}
.form-row > label {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--gray-600);
  margin: 0;
}
.form-row > input,
.form-row > select,
.form-row > textarea {
  padding: 8px 12px;
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius);
  font-size: 0.875rem;
  font-family: inherit;
  background: var(--card-bg);
  color: var(--text);
}
.form-row > input:focus,
.form-row > select:focus,
.form-row > textarea:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-light);
}
.form-row > .form-hint { margin: 0; }

.form-actions { display: flex; gap: 0.75rem; }
.form-actions .btn { flex: 0 0 auto; }

.admin-form {
  background-color: var(--card-bg);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--gray-200);
  padding: 1.75rem;
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 1.25rem;
  max-width: 480px;
}

@media (min-width: 768px) {
  .admin-form { grid-template-columns: repeat(2, 1fr); max-width: 640px; }
  .admin-form .form-group-full,
  .admin-form .form-hint,
  .admin-form .form-actions { grid-column: 1 / -1; }
}

@media (min-width: 1024px) {
  .admin-form { max-width: 720px; }
}

/* Buttons not covered by the design system's own button classes but
   still used across existing markup */
.btn-block { display: block; width: 100%; margin-top: 0.5rem; }

.btn-secondary { background: var(--gray-100); color: var(--gray-700); border: 1px solid var(--gray-200); }
.btn-secondary:hover { background: var(--gray-200); }
.btn-secondary.active { background: var(--primary); color: #fff; border-color: var(--primary); }
.btn-secondary.active:hover { background: var(--primary-dark); border-color: var(--primary-dark); }

.btn-link {
  background: none;
  border: none;
  padding: 0;
  color: var(--primary);
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
}
.btn-link:hover { text-decoration: underline; }

/* Icon action buttons (view/edit/archive/delete/restore) used in every
   admin/data-table row */
.icon-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  font-size: 1rem;
  line-height: 1;
  color: var(--text-muted);
  background: none;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  text-decoration: none;
  transition: color 0.15s ease, background-color 0.15s ease;
}
.icon-action:hover { text-decoration: none; }
.icon-view:hover    { color: #2563eb; background-color: #dbeafe; }
.icon-edit:hover    { color: var(--warning); background-color: var(--warning-light); }
.icon-archive:hover { color: var(--gray-600); background-color: var(--gray-100); }
.icon-delete:hover  { color: var(--danger); background-color: var(--danger-light); }
.icon-restore:hover { color: var(--success); background-color: var(--success-light); }
.icon-key:hover     { color: var(--warning); background-color: var(--warning-light); }
.icon-copy:hover    { color: var(--primary); background-color: var(--primary-light); }
.icon-rights:hover  { color: var(--primary); background-color: var(--primary-light); }

/* Old-naming alert modifier still used across the app */
.alert-error { background: var(--danger-light); color: #991b1b; border: 1px solid #fca5a5; }
.alert ul { list-style: disc; padding-left: 1.25rem; }

/* Admin pages: list header + tables (admin/*, company/*, services/*) */
/* Même échelle que .page-header h1 : les deux gabarits de titre avaient
   divergé (1,9 rem ici, 22 px là), si bien que deux écrans voisins ne
   titraient pas pareil selon celui des deux qu'ils avaient repris. */
.admin-page h1 { font-size: 2.25rem; font-weight: 600; margin-bottom: 0.4rem; }

.admin-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 1rem;
  margin-bottom: 1.5rem;
}
.admin-header h1 { margin-bottom: 0; }

/* 1,125 rem = 18 px, la valeur relevée sur les titres de carte d'OVH. */
.admin-subheading { font-size: 1.125rem; font-weight: 600; margin: 2rem 0 0.85rem; color: var(--gray-900); }

.roles-summary { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.role-pill {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background-color: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.4rem 0.9rem;
  font-size: 0.88rem;
}
.role-pill-count { font-weight: 700; color: var(--text); }

/* Mirrors .table-wrapper/.table under the class names actually used in
   markup everywhere (.table-responsive/.data-table) */
/* Le tableau n'est PLUS dans une carte.
   ─────────────────────────────────────────────────────────────────────
   Ni bordure, ni ombre, ni rayon, ni fond : il est posé à même le blanc,
   comme dans le manager OVHcloud. L'encadré n'apportait rien — le
   tableau occupe déjà toute la largeur, donc le cadre ne délimitait
   qu'une zone dont personne ne doutait, en ajoutant une ligne à lire.

   Le conteneur reste, uniquement pour son défilement horizontal et pour
   les colonnes collantes qui s'y adossent. */
.table-responsive {
  width: 100%;
  overflow-x: auto;
}

.data-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }

/* En-tête en CASSE NORMALE.
   Nalie écrivait « N° FACTURE » en 11,5 px gris espacés — le registre de
   l'étiquette technique, hérité des tableaux de bord de la décennie
   précédente. OVH écrit « N° Facture » en 14 px foncé, comme une phrase.
   Même contenu, et pourtant c'est ce détail qui date le plus un écran.

   Conséquence à ne pas manquer : à casse normale, un intitulé n'est plus
   une étiquette posée au-dessus d'une colonne, c'est son titre. Il doit
   donc suivre l'alignement de sa colonne — voir .num juste en dessous. */
.data-table th {
  background: var(--gray-50);
  color: var(--gray-900);
  font-weight: 600;
  font-size: 0.875rem;
  padding: 12px 16px;
  text-align: left;
  border-bottom: 1px solid var(--gray-200);
  white-space: nowrap;
}
/* 14 px de garniture verticale : la ligne atteint les 52 px relevés chez
   OVH, contre 40 auparavant. C'est le second écart le plus voyant après
   la taille du texte, et celui qui fait respirer une longue liste. */
.data-table td {
  padding: 14px 16px;
  border-bottom: 1px solid var(--gray-100);
  color: var(--gray-600);
  white-space: nowrap;
  vertical-align: middle;
}
/* Colonnes de chiffres : l'en-tête suit l'alignement de sa colonne. Les valeurs
   étaient calées à droite et les titres à gauche, si bien que chaque intitulé
   désignait visuellement la colonne d'à côté. */
.data-table th.num, .data-table td.num { text-align: right; white-space: nowrap; }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover td { background: var(--gray-50); }

.actions-cell { display: flex; align-items: center; gap: 0.35rem; }
.inline-form { display: inline; }

.cat-swatch {
  display: inline-block;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 4px;
  border: 1px solid var(--border);
  vertical-align: middle;
}

/* Small ON/OFF toggle switch (e.g. company/service authorization grid) */
.switch { position: relative; display: inline-block; width: 40px; height: 22px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch-slider {
  position: absolute; inset: 0; cursor: pointer; background: var(--border);
  transition: background-color 0.15s; border-radius: 22px;
}
.switch-slider::before {
  position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px;
  background: #fff; transition: transform 0.15s; border-radius: 50%;
}
.switch input:checked + .switch-slider { background: var(--primary); }
.switch input:checked + .switch-slider::before { transform: translateX(18px); }
.switch input:disabled + .switch-slider { opacity: 0.5; cursor: not-allowed; }

/* Badge modifiers used across admin/company tables */
.badge-role-superadmin   { background-color: #ede9fe; color: #5b21b6; }
.badge-role-admincompany { background-color: var(--primary-light); color: var(--primary-dark); }
.badge-role-user         { background-color: var(--gray-100); color: var(--text-muted); }
.badge-active            { background-color: var(--success-light); color: var(--success); }
.badge-archived          { background-color: var(--danger-light); color: var(--danger); }
.badge-pending           { background-color: var(--warning-light); color: #92400e; }
.badge-dali              { background-color: #ede9fe; color: #5b21b6; }
.badge-absence           { background-color: var(--warning-light); color: #92400e; white-space: nowrap; }

/* Companies */
.company-logo-thumb {
  width: 40px; height: 40px;
  object-fit: contain;
  border-radius: 6px;
  border: 1px solid var(--border);
  background-color: #fff;
}
.company-logo-placeholder { color: var(--text-muted); }

/* Service permissions */
.permissions-list { display: flex; flex-direction: column; gap: 0.6rem; margin-bottom: 0.5rem; }
.permission-item {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  background-color: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.6rem 0.85rem;
  cursor: pointer;
  font-size: 0.92rem;
}
.permission-item input { margin-top: 0.2rem; }
.permission-item-disabled { opacity: 0.6; cursor: not-allowed; }

/* Dashboard-style tile grid also used by chantiers/view.php's project hub */
.dash-section { margin-bottom: 2.5rem; }
.dash-section-title {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  /* Casse normale ici aussi : c'est un titre de section, au même titre
     que .admin-subheading, et il s'écrivait en capitales minuscules
     pendant que l'autre s'écrivait normalement. */
  font-size: 1rem;
  font-weight: 600;
  color: var(--gray-700);
  margin-bottom: 1rem;
  padding-bottom: 0.55rem;
  border-bottom: 1px solid var(--border);
}
.dash-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; align-items: stretch; }
.dash-grid > .service-card { display: flex; flex-direction: column; width: 100%; min-width: 0; box-sizing: border-box; flex: none; }
.dash-grid .service-card h3,
.service-card h3 { font-size: 1.05rem; font-weight: 700; margin-bottom: 0.4rem; color: var(--text); }
.service-card p { font-size: 0.9rem; color: var(--text-muted); }
.service-icon {
  width: 44px; height: 44px;
  border-radius: 10px;
  background: var(--primary-light);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  margin-bottom: 4px;
}
@media (min-width: 600px) { .dash-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .dash-grid { grid-template-columns: repeat(3, 1fr); } }

/* Dashboard welcome panel for self-registered users awaiting rights */
.welcome-panel {
  background-color: var(--info-light);
  border: 1px solid #a5f3fc;
  border-radius: var(--radius);
  padding: 1.5rem;
  margin-bottom: 1.5rem;
}
.welcome-panel h2 { margin: 0 0 0.75rem; font-size: 1.3rem; }
.welcome-panel p { margin: 0 0 0.5rem; line-height: 1.6; }
.welcome-panel-divider { border: none; border-top: 1px solid #a5f3fc; margin: 1rem 0; }
.welcome-panel-grid { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 0.75rem; }
.welcome-panel-item {
  flex: 1 1 180px;
  min-width: 180px;
  background-color: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1rem;
}
.welcome-panel-item .welcome-panel-icon { font-size: 2em; margin-bottom: 0.4rem; }
.welcome-panel-item p { margin-top: 0.35rem; font-size: 0.9em; color: var(--text-muted); }

/* Mon compte (/auth/profile.php) */
.account-divider { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
.account-company-card { background-color: var(--bg); border-radius: var(--radius); padding: 1.25rem 1.5rem; }
.account-company-card p { margin: 0 0 0.5rem; font-size: 0.92rem; }
.account-company-card p:last-child { margin-bottom: 0; }

/* Inline field status (username availability check on /register.php) */
.field-status { margin: 0.35rem 0 0; font-size: 0.85rem; min-height: 1.1em; }
.field-status--checking { color: var(--text-muted); }
.field-status--checking::before {
  content: '';
  display: inline-block;
  width: 0.7em; height: 0.7em;
  margin-right: 0.4em;
  border: 2px solid var(--border);
  border-top-color: var(--primary);
  border-radius: 50%;
  animation: field-status-spin 0.6s linear infinite;
  vertical-align: -0.1em;
}
@keyframes field-status-spin { to { transform: rotate(360deg); } }
.field-status--available { color: var(--success-text); }
.field-status--taken, .field-status--invalid { color: var(--error-text); }

/* Zone column hidden on the timesheet entry table (services/heures/saisie.php) */
.col-zone { display: none; }

/* =====================================================================
   KNX project module (services/knx_project/*, chantiers/*)
   ===================================================================== */
.kx-project-layout { display: grid; grid-template-columns: 1fr; align-items: start; gap: 1.5rem; }
.kx-project-main { min-width: 0; }
.kx-project-summary { display: flex; flex-direction: column; gap: 1.5rem; min-width: 0; }
.kx-project-summary h1, .kx-project-summary h2, .kx-project-summary h3 { text-align: left; }
aside.kx-project-summary h2,
aside.kx-project-summary h3,
aside.kx-project-summary .kx-section-title,
aside.kx-project-summary .kx-summary-title,
aside.kx-project-summary .kx-group-label,
aside.kx-project-summary .kx-group-row {
  text-align: left !important;
  justify-content: flex-start !important;
}

.kx-summary-card { background-color: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow-sm); border: 1px solid var(--gray-200); padding: 1.25rem 1.5rem; }
.kx-summary-title { font-size: 1.05rem; margin: 0 0 0.9rem; text-align: left; }
.kx-summary-row { display: flex; flex-direction: column; gap: 0.15rem; padding: 0.5rem 0; border-bottom: 1px solid var(--border); font-size: 0.88rem; }
.kx-summary-label { font-weight: 600; }
.kx-summary-detail { color: var(--text-muted); }
.kx-summary-total { display: flex; justify-content: space-between; align-items: center; padding-top: 0.85rem; margin-top: 0.35rem; font-weight: 700; font-size: 1rem; }

.kx-actions-list { display: flex; flex-direction: column; }
/* Une action tient sur UNE ligne : la catégorie est remontée en titre de
   rubrique, donc elle n'a plus à se répéter sous chaque nom. Trente
   actions occupaient l'écran entier pour un mot qui revenait cinq fois
   d'affilée. */
.kx-action-row { cursor: default; padding: 4px 6px; }
.kx-action-info { display: flex; align-items: center; min-width: 0; flex: 1 1 auto; }
.kx-action-name {
  font-weight: 500;
  font-size: 0.875rem;
  /* Tronqué plutôt que replié : une action qui passe à la ligne casse la
     régularité de la liste, et c'est cette régularité qui la rend
     parcourable d'un coup d'œil. Le nom entier reste dans l'infobulle. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Les rubriques de métier. Un titre discret et un compte : c'est ainsi
   qu'un intégrateur cherche — « les volets », pas « la douzième action ». */
.kx-actions-metier + .kx-actions-metier { margin-top: 12px; }
/* Le titre de rubrique est un BOUTON : il plie et déplie. D'où la remise
   à plat des styles par défaut — sans elle, il arriverait avec le fond
   gris et la bordure d'un bouton de formulaire. */
.kx-actions-metier-titre {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  margin: 0 0 2px;
  padding: 4px 6px;
  border: none;
  background: none;
  border-radius: var(--border-radius);
  font: inherit;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--gray-500);
  text-align: left;
  cursor: pointer;
}
.kx-actions-metier-titre:hover { background: var(--gray-50); color: var(--gray-700); }
.kx-actions-metier-chevron {
  font-size: 11px;
  line-height: 1;
  transition: transform 0.18s ease;
  transform: rotate(-90deg);
}
.kx-actions-metier.est-ouverte .kx-actions-metier-chevron { transform: rotate(0deg); }

/* display:none et non hauteur nulle : une rubrique repliée ne doit pas
   rester atteignable au clavier, ni ses actions rester glissables. */
.kx-actions-metier-corps { display: none; }
.kx-actions-metier.est-ouverte .kx-actions-metier-corps { display: block; }

.kx-modele-creer { margin-bottom: 16px; }
.kx-modele-creer > summary {
  cursor: pointer;
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--primary-dark);
  padding: 6px 2px;
}
.kx-actions-metier-compte {
  font-weight: 500;
  color: var(--gray-400);
  font-variant-numeric: tabular-nums;
}
/* Les commandes de la ligne ne se montrent qu'au survol : sur trente
   lignes, soixante icônes permanentes font plus de bruit que la liste
   elle-même. Elles restent atteignables au clavier — :focus-within — car
   une commande qui n'apparaît qu'à la souris n'existe pas pour qui n'en a
   pas. */
.kx-action-row .actions-cell { opacity: 0; transition: opacity 0.12s ease; }
.kx-action-row:hover .actions-cell,
.kx-action-row:focus-within .actions-cell { opacity: 1; }
/* .kx-action-key n'est plus affichée : la clef technique doublait le
   libellé français juste au-dessus, en moins lisible, et coûtait la
   moitié de la hauteur de la liste. Elle vit désormais dans un attribut
   data — c'est elle qu'on dépose sur un bouton, mais on n'a pas besoin de
   la lire pour ça. */

/* ── FORMULAIRES REPLIÉS ─────────────────────────────────────────────
   Modifier un appareil, modifier une fiche du référentiel. Repliés parce
   qu'on ouvre ces écrans pour LIRE : régler des boutons, relire un
   catalogue. Un formulaire déployé d'office ferait payer sa hauteur à
   chaque visite pour un geste occasionnel. */
.kx-device-modifier, .kx-modele-edition { margin: 0 0 12px; }
.kx-device-modifier > summary,
.kx-modele-edition > summary {
  cursor: pointer;
  font-size: 0.875rem;
  color: var(--gray-500);
  padding: 6px 2px;
  list-style: revert;
}
.kx-device-modifier > summary:hover,
.kx-modele-edition > summary:hover { color: var(--primary); }

.kx-device-modifier-form, .kx-modele-edition-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  padding: 12px;
  margin-top: 6px;
  background: var(--gray-50);
  border-radius: var(--border-radius-lg);
}
.kx-device-modifier-form label, .kx-modele-edition-form label {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 0.8125rem;
  color: var(--gray-600);
}
.kx-modele-notes { flex: 1 1 260px; }
.kx-modele-retirer { flex-direction: row !important; align-items: center; gap: 6px !important; }

/* La ligne d'édition appartient à celle du dessus : pas de survol propre,
   sinon on croirait à deux entrées distinctes du référentiel. */
.kx-modele-edition-ligne td { padding-top: 0; }
.data-table tbody tr.kx-modele-edition-ligne:hover td { background: none; }

/* La vignette du produit : petite à dessein. Elle sert à reconnaître, pas
   à regarder — grand format, un tableau de trente lignes deviendrait un
   écran à faire défiler. */
.kx-modele-cellule { display: flex; align-items: center; gap: 10px; }
.kx-modele-noms { display: block; min-width: 0; }
.kx-modele-vignette {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  object-fit: contain;
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius);
  background: var(--card-bg);
}
.kx-modele-vignette--vide {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--gray-300);
  font-size: 16px;
}

/* ── GLISSER UNE ACTION SUR UN BOUTON ────────────────────────────────
   Les actions sont déjà listées à côté de la grille : les y prendre
   évite de rouvrir une liste déroulante et de rechoisir le même intitulé
   pour la vingtième fois. */
.kx-action-row { cursor: grab; }
.kx-action-row:active { cursor: grabbing; }
.kx-action-poignee {
  color: var(--gray-300);
  font-size: 13px;
  line-height: 1;
  flex-shrink: 0;
  margin-right: 2px;
}
.kx-action-row:hover .kx-action-poignee { color: var(--gray-500); }
/* La ligne glissée s'efface pendant le geste : on suit le curseur, pas
   la ligne d'origine, et la laisser pleinement visible fait douter de ce
   qu'on transporte. */
.kx-action-row.est-glissee { opacity: 0.45; }

/* ── L'ACTION ARMÉE ──────────────────────────────────────────────────
   Le clic remplace le glisser comme voie principale : cliquer une action
   l'arme, cliquer un bouton l'y pose. Le glisser demandait d'appuyer, de
   maintenir, de viser et de relâcher — quatre gestes enchaînés sans
   erreur, sur une ligne qui porte déjà une édition en ligne.

   L'état armé doit se voir de loin : on regarde la grille, pas la liste,
   au moment où l'on va cliquer le bouton. D'où un fond franc et non un
   simple liseré. */
.kx-action-row.est-armee {
  background: var(--primary-light);
  box-shadow: inset 3px 0 0 var(--primary);
}
.kx-action-row.est-armee .kx-action-name { color: var(--primary-dark); font-weight: 600; }

/* Le curseur dit où le clic va servir : sur toute la page tant qu'une
   action est armée, en cible sur les boutons qui l'acceptent. */
body.kx-action-armee .kx-device-button { cursor: copy; }
body.kx-action-armee .kx-device-button:hover {
  border-color: var(--primary);
  background: var(--primary-light);
}

/* La cible se déclare franchement. Un liseré discret ne suffit pas quand
   la main est occupée à tenir un glissement : on ne relit pas l'écran, on
   le regarde du coin de l'œil. */
.kx-device-button.est-cible,
.kx-button-config-form [data-field="action_short"].est-cible {
  border-color: var(--primary);
  background: var(--primary-light);
  box-shadow: 0 0 0 3px var(--primary-light);
}

/* Le champ d'action accepte lui aussi le dépôt et le clic armé : le
   curseur doit le dire, sinon on continue de viser la grille. */
body.kx-action-armee .kx-button-config-form [data-field="action_short"] {
  cursor: copy;
  border-color: var(--primary);
}

/* ── SÉLECTION DE LIGNES ET OPÉRATIONS EN MASSE ──────────────────────
   Le <label> occupe toute la cellule : sur une ligne de 52 px, viser une
   case de 14 px demande de la précision, et c'est le geste qu'on répète
   trente fois de suite. */
.selection-cellule {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 4px;
  cursor: pointer;
}
.selection-case,
[data-selection-tout] { width: 16px; height: 16px; cursor: pointer; accent-color: var(--primary); }

/* La ligne cochée se voit d'un coup d'œil : sans elle, sur une liste
   triée, on ne sait plus ce qu'on a pris trois pages plus haut. */
.data-table tbody tr:has(.selection-case:checked) td { background: var(--primary-light); }

/* ── LA BARRE D'OPÉRATIONS ───────────────────────────────────────────
   Collée en bas de la fenêtre, et non posée sous le tableau : sur une liste
   de cent lignes, une barre en fin de page oblige à faire défiler jusqu'en
   bas après avoir coché en haut. */
.barre-masse {
  position: sticky;
  bottom: 12px;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 10px 16px;
  flex-wrap: wrap;
  margin-top: 16px;
  padding: 12px 16px;
  background: var(--gray-900);
  color: #fff;
  border-radius: 10px;
  box-shadow: 0 6px 24px rgba(19, 26, 63, 0.28);
}
/* hidden doit gagner : la barre est en flex, et un display posé par une
   classe l'emporte sur l'attribut. Le piège s'est déjà refermé deux fois
   sur le formulaire des boutons KNX. */
.barre-masse[hidden] { display: none !important; }

.barre-masse-compteur { font-weight: 600; font-size: 0.9375rem; margin-right: auto; }
.barre-masse-groupe { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.barre-masse .inline-input {
  height: 32px;
  padding: 0 8px;
  border: 1px solid var(--gray-600);
  border-radius: var(--border-radius);
  background: var(--card-bg);
  color: var(--gray-900);
  font-size: 0.875rem;
  max-width: 14rem;
}
/* Les boutons secondaires sont sur fond foncé : la version claire y
   disparaîtrait. */
.barre-masse .btn-secondary {
  background: transparent;
  color: #fff;
  border-color: var(--gray-500);
}
.barre-masse .btn-secondary:hover { background: var(--gray-800); border-color: var(--gray-400); }

@media (max-width: 767px) {
  /* Sur téléphone la barre passe en colonne, compteur en tête : c'est le
     nombre qu'on vient vérifier avant d'agir. */
  .barre-masse { flex-direction: column; align-items: stretch; bottom: 0; border-radius: 10px 10px 0 0; }
  .barre-masse-compteur { margin-right: 0; text-align: center; }
  .barre-masse-groupe { justify-content: space-between; }
  .barre-masse .inline-input { flex: 1 1 auto; max-width: none; }
}

/* ── AJOUTER UN APPAREIL ─────────────────────────────────────────────
   Une carte avec de vrais intitulés, et non dix champs nus sur une
   rangée. Le repère n'est plus l'ordre mémorisé des cases mais le mot
   au-dessus de chacune — c'est ce qui permet de revenir au formulaire
   après trois semaines sans le relire en entier.

   La grille se replie à une colonne sous 768 px : sur le téléphone on
   ajoute un appareil debout devant le mur, et deux champs côte à côte y
   sont deux champs trop étroits. */
.kx-ajout-appareil {
  background-color: var(--card-bg);
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 16px 18px;
  margin-bottom: 20px;
}
.kx-ajout-modele {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  margin-bottom: 14px;
}
.kx-ajout-modele select { width: 100%; max-width: 520px; }
.kx-ajout-modele .form-hint { margin: 4px 0 0; }

.kx-ajout-ligne {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px 14px;
  align-items: start;
  margin-bottom: 12px;
}
.kx-ajout-champ { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.kx-ajout-champ--large  { grid-column: span 2; }
.kx-ajout-champ--etroit { max-width: 130px; }
.kx-ajout-appareil label {
  font-size: 0.75rem;
  color: var(--gray-500);
  margin: 0;
}
.kx-ajout-appareil .inline-input { width: 100%; }

/* L'attribut hidden du champ « nouvelle pièce » doit l'emporter sur le
   display de la colonne — même piège que dans le formulaire des boutons. */
.kx-ajout-appareil [hidden] { display: none !important; }

.kx-ajout-disposition {
  border-top: 1px solid var(--gray-200);
  padding-top: 10px;
  margin-bottom: 12px;
}
.kx-ajout-disposition > summary {
  cursor: pointer;
  font-size: 0.8125rem;
  color: var(--primary);
  list-style: none;
  padding: 2px 0;
}
.kx-ajout-disposition > summary::-webkit-details-marker { display: none; }
.kx-ajout-disposition > summary::before { content: '+\00a0'; }
.kx-ajout-disposition[open] > summary::before { content: '\2212\00a0'; }
.kx-ajout-disposition[open] > summary { margin-bottom: 10px; }

.kx-ajout-actions { display: flex; justify-content: flex-end; }

@media (max-width: 767px) {
  .kx-ajout-ligne { grid-template-columns: 1fr; }
  .kx-ajout-champ--large, .kx-ajout-champ--etroit { grid-column: auto; max-width: none; }
  .kx-ajout-actions .btn { width: 100%; }
}

.kx-actions-aide {
  font-size: 0.8125rem;
  color: var(--gray-500);
  margin: -4px 0 10px;
}

/* « Posée, à enregistrer » : un état qui n'est ni un succès ni une
   erreur. Sans lui, un dépôt suivi d'un départ de la page perdrait le
   réglage sans que rien ne l'ait annoncé. */
.kx-button-etat.is-attente { color: var(--warning, #b45309); font-weight: 600; }
.kx-button-etat.is-error   { color: var(--error-text); font-weight: 600; }
.kx-action-add-form { margin-top: 0.5rem; padding-top: 0.75rem; border-top: 1px solid var(--border); }
.kx-action-add-form .inline-input { flex: 1 1 auto; width: auto; min-width: 0; }

.project-info { position: relative; background-color: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow-sm); border: 1px solid var(--gray-200); padding: 1.25rem 3.25rem 1.25rem 1.5rem; margin-bottom: 1.5rem; }
.project-info p { margin-bottom: 0.4rem; }
.project-info p:last-child { margin-bottom: 0; }
.kx-integrator-block { margin-bottom: 0.75rem; padding-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
.kx-integrator-details { display: flex; flex-direction: column; gap: 0.15rem; margin-top: 0.25rem; color: var(--text-muted); }
.kx-info-edit-btn, .kx-info-edit-actions button { background: none; border: none; cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0.3rem; border-radius: 6px; color: var(--text-muted); }
.kx-info-edit-btn { position: absolute; top: 0.85rem; right: 0.85rem; }
.kx-info-edit-actions { position: absolute; top: 0.85rem; right: 0.85rem; display: flex; gap: 0.35rem; }
/* LE CRAYON NE MARCHAIT PAS, ET VOICI POURQUOI.

   Le crayon et le groupe Enregistrer / Annuler sont posés au même coin, l'un
   sur l'autre : c'est voulu, l'un remplace l'autre à l'entrée en édition.
   Mais `display: flex` l'emportait sur l'attribut `hidden` du groupe — le
   même piège que sur le formulaire des boutons KNX et la barre en masse. Le
   groupe restait donc affiché PAR-DESSUS le crayon, avec « Annuler » pile à
   sa place : on voyait un crayon et une disquette, on cliquait le crayon, et
   c'est Annuler qui recevait le clic. Rien ne se passait.

   Vérifié au navigateur : elementFromPoint() au centre du crayon rendait le
   bouton « Annuler ». */
.kx-info-edit-actions[hidden],
.kx-info-edit-btn[hidden] { display: none !important; }
.kx-info-edit-btn:hover, .kx-info-edit-actions button:hover { background-color: var(--bg); color: var(--text); }
.kx-info-edit-actions button:disabled { opacity: 0.5; cursor: default; }
.kx-info-display input.inline-input { width: 100%; max-width: 320px; }
.kx-info-display textarea.inline-input { width: 100%; max-width: 480px; font-family: inherit; resize: vertical; }
.kx-info-image-edit { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.5rem; }
.kx-info-empty { color: var(--text-muted); }
.project-info.is-saving { pointer-events: none; opacity: 0.7; }

/* KNX group address hierarchy tree */
.kx-tree { display: flex; flex-direction: column; gap: 1.25rem; }
.kx-main-group { background-color: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow-sm); border: 1px solid var(--gray-200); padding: 1.25rem; }
.kx-middle-group { margin-top: 1rem; margin-left: 1.5rem; padding-left: 1rem; border-left: 2px solid var(--border); }
.kx-group-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.75rem; margin: -0.35rem -0.5rem 0.75rem -0.5rem; padding: 0.35rem 0.5rem; border-radius: var(--radius); cursor: pointer; transition: background-color 0.15s ease; }
.kx-group-row:hover { background-color: var(--bg); }
.kx-toggle { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; background: none; border: none; margin: 0; padding: 0.25rem; font-size: 0.75rem; line-height: 1; color: var(--text-muted); cursor: pointer; transform: rotate(90deg); transition: transform 0.2s ease; }
.collapsed > .kx-group-row .kx-toggle { transform: rotate(0deg); }
.kx-collapsible { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 0.25s ease; }
.kx-collapsible-inner { overflow: hidden; min-height: 0; }
.collapsed > .kx-collapsible { grid-template-rows: 0fr; }
.kx-group-label { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; text-align: left; }
.kx-main-row .kx-group-label strong { font-size: 1.1rem; }
.kx-main-row { justify-content: flex-start; }
.kx-main-row .kx-group-label { justify-content: flex-start; text-align: left; }
.kx-main-row .actions-cell { margin-left: auto; }
.kx-middle-row .kx-group-label strong { font-size: 1rem; }
.kx-address-badge { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 6px; background-color: var(--bg); border: 1px solid var(--border); font-family: 'Courier New', Courier, monospace; font-weight: 700; font-size: 0.85rem; color: var(--text); }
.kx-address-table { margin-left: 1rem; }
.kx-product-group { margin-top: 0.35rem; }
.kx-product-row { margin: 0 -0.5rem; }
.kx-product-room { flex: 0 0 160px; color: var(--text-muted); font-size: 0.9rem; }
.kx-product-name { flex: 1 1 auto; font-weight: 600; }
.kx-product-addresses { margin: 0.25rem 0 0.5rem 1.75rem; width: auto; }
.kx-product-addresses td { color: var(--text-muted); font-size: 0.85rem; padding: 0.4rem 1rem; }

.kx-quick-add-form { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; background-color: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow-sm); border: 1px solid var(--gray-200); padding: 1rem 1.25rem; margin-bottom: 1.25rem; }
.kx-quick-add-form select.inline-input, .kx-quick-add-form input.inline-input { width: auto; min-width: 160px; }
.kx-quick-add-form .kx-quick-add-name { flex: 1 1 200px; }
.kx-dali-field { display: flex; align-items: center; gap: 0.4rem; font-size: 0.9rem; color: var(--text); cursor: pointer; }
.kx-dali-field[hidden] { display: none; }
.kx-advanced-options { flex-basis: 100%; font-size: 0.9rem; }
.kx-advanced-options summary { cursor: pointer; color: var(--primary); }
.kx-advanced-options-body { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.6rem; }
.kx-advanced-options-body select.inline-input { width: auto; min-width: 80px; }

/* CSV import page */
.kx-import-form { display: flex; flex-direction: column; gap: 1.5rem; max-width: 720px; }
/* Ligne d'un groupe que le fichier n'apporte pas : c'est la seule information
   utile du tableau de comparaison, elle doit sauter aux yeux sans qu'on lise
   colonne par colonne. */
.data-table tr.kx-import-perdu td { background: var(--danger-light, #fef2f2); }

.kx-import-dropzone { background-color: var(--card-bg); border: 2px dashed var(--border); border-radius: var(--radius); padding: 2.5rem 1.5rem; text-align: center; cursor: pointer; transition: border-color 0.15s ease, background-color 0.15s ease; }
.kx-import-dropzone:hover, .kx-import-dropzone:focus-visible { border-color: var(--primary); }
.kx-import-dropzone.is-dragover { border-color: var(--primary); background-color: var(--primary-light); }
.kx-import-dropzone-text { color: var(--text-muted); margin: 0; }
.kx-import-filename { margin: 0.75rem 0 0; font-weight: 600; color: var(--text); }
.kx-import-preview h2 { margin-bottom: 0.75rem; }
.kx-empty { color: var(--text-muted); font-size: 0.9rem; font-style: italic; padding: 0.5rem 0; }

/* Building levels & rooms */
.kx-levels-section { margin: 1.75rem 0 1.5rem; }
.kx-project-summary .kx-levels-section { margin: 0; }
.kx-section-row .admin-subheading { margin: 0; text-align: left; }
.kx-levels-tree { display: flex; flex-direction: column; gap: 0.75rem; }
.kx-project-summary .kx-levels-tree { gap: 0.5rem; }
.kx-level-group { background-color: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow-sm); border: 1px solid var(--gray-200); padding: 1.25rem; }
.kx-project-summary .kx-level-group { padding: 0.85rem; }
.kx-level-row .kx-group-label { font-weight: 600; text-align: left; }
.kx-rooms-list { display: flex; flex-direction: column; margin: 0.5rem 0 0.5rem 1.5rem; padding-left: 1rem; border-left: 2px solid var(--border); }
.kx-project-summary .kx-rooms-list { margin: 0.4rem 0 0.4rem 1rem; padding-left: 0.65rem; }
.kx-room-row { cursor: default; }

/* Middle groups panel (project sidebar) */
.kx-middle-groups-section { margin: 0; }
.kx-middle-groups-tree { display: flex; flex-direction: column; gap: 0.5rem; }
.kx-mdg-main-group { background-color: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow-sm); border: 1px solid var(--gray-200); padding: 0.85rem; }
.kx-mdg-main-row .kx-group-label strong { font-size: 1rem; }
.kx-mdg-list { display: flex; flex-direction: column; margin: 0.4rem 0 0.4rem 1rem; padding-left: 0.65rem; border-left: 2px solid var(--border); }
.kx-mdg-row { cursor: default; }
.kx-mdg-blocked { color: var(--text-muted); font-size: 0.8rem; font-style: italic; }
.kx-mdg-actions { margin-top: 0.35rem; }
.kx-drag-handle { display: inline-flex; flex-shrink: 0; cursor: grab; color: var(--text-muted); font-size: 0.95rem; line-height: 1; padding: 0 0.15rem; user-select: none; }
.kx-drag-handle:active { cursor: grabbing; }
.dragging { opacity: 0.4; }
.kx-room-level-field { display: none; }
.editing .kx-room-level-field { display: inline-flex; }
.kx-inline-add-form { display: flex; align-items: center; gap: 0.5rem; padding: 0.35rem 0.5rem; }
.kx-inline-add-form .inline-input { width: auto; flex: 0 1 240px; }
.kx-inline-add-level { margin-top: 0.75rem; }
.kx-add-confirm { background: none; border: none; color: var(--primary); font-size: 1.1rem; line-height: 1; padding: 0.35rem 0.5rem; cursor: pointer; border-radius: 6px; }
.kx-add-confirm:hover { background-color: var(--bg); }

/* Group address form */
.address-index-group .address-index-inputs { display: flex; align-items: center; gap: 0.5rem; }
.address-index-inputs input[type="number"] { width: 80px; }
.current-logo { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.6rem; color: var(--text-muted); font-size: 0.88rem; }
.kx-project-image-thumb { width: 80px; height: 80px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); }
.kx-checkbox-label { display: inline-flex; align-items: center; gap: 0.4rem; cursor: pointer; }
.kx-project-image { max-width: 100%; border-radius: var(--radius); margin-top: 0.75rem; display: block; }

/* Inline editing (admin/company/knx tables) */
.row-lock { position: relative; background: none; border: none; cursor: pointer; font-size: 1rem; line-height: 1; padding: 0.2rem; border-radius: 6px; }
.row-lock:hover { background-color: var(--bg); }
.row-lock.is-saving { color: transparent; }
.row-lock.is-saving::after {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 14px; height: 14px; margin: -7px 0 0 -7px;
  border: 2px solid var(--border); border-top-color: var(--primary);
  border-radius: 50%; animation: row-lock-spin 0.6s linear infinite;
}
@keyframes row-lock-spin { to { transform: rotate(360deg); } }

.inline-input { width: 100%; min-width: 80px; padding: 0.35rem 0.5rem; border: 1px solid var(--primary); border-radius: 6px; font-size: 0.88rem; font-family: inherit; }
.name-inline-inputs, .group-inline-inputs, .address-inline-inputs { display: flex; gap: 0.5rem; align-items: center; }
.address-inline-inputs { flex-wrap: wrap; white-space: normal; }
.kx-group-readonly { font-size: 0.8rem; color: var(--text-muted); margin-left: 0.4rem; }
.kx-group-readonly-error { color: var(--error-text); }
.name-inline-inputs .inline-input { width: 110px; min-width: 0; }
.group-inline-inputs .inline-input[data-part="group_index"], .address-inline-inputs .inline-input { width: 64px; min-width: 0; text-align: center; }
.group-inline-inputs .inline-input[data-part="name"] { width: 160px; min-width: 0; }

@keyframes row-flash-success { 0% { background-color: var(--success-bg); } 100% { background-color: transparent; } }
@keyframes row-flash-error { 0% { background-color: var(--error-bg); } 100% { background-color: transparent; } }
.row-flash-success { animation: row-flash-success 1.2s ease-out; }
.row-flash-error { animation: row-flash-error 1.2s ease-out; }
.inline-row-error, .inline-row-error-row td { color: var(--error-text); font-size: 0.85rem; padding-top: 0.25rem; padding-bottom: 0.6rem; }
/* Boîte de dialogue de confirmation, centrée.
   Remplace le « Supprimer ? ✓ ✗ » qu'on insérait dans la cellule d'actions et
   qui débordait de sa colonne sur les tables calibrées. Voir confirm-dialog.js. */
.nalie-confirm {
  /* Le navigateur centre un <dialog> modal avec « margin: auto ». La règle
     « * { margin: 0 } » en tête de cette feuille l'écrase, et la boîte se
     collait en haut à gauche. On repose donc le centrage explicitement :
     inset:0 + margin:auto sur un élément de taille automatique le place au
     milieu, horizontalement ET verticalement. */
  position: fixed;
  inset: 0;
  margin: auto;
  height: fit-content;
  border: 0;
  border-radius: 10px;
  padding: 0;
  max-width: 27rem;
  width: calc(100% - 2rem);
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.24);
  color: var(--text);
  background: var(--card-bg, #fff);
}
.nalie-confirm::backdrop { background: rgba(15, 23, 42, 0.45); }
.nalie-confirm-corps { padding: 1.25rem 1.4rem 1.1rem; margin: 0; }
.nalie-confirm-texte {
  margin: 0 0 1.1rem;
  font-size: 0.98rem;
  line-height: 1.45;
}
.nalie-confirm-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Cases des filtres d'un widget et de ses bénéficiaires : des listes de cases
   qui s'étalent sur la largeur plutôt que de faire une colonne à défiler. */
.widget-filtre-cases,
.widget-beneficiaires {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  margin-top: 0.3rem;
}
.widget-beneficiaires { gap: 0.5rem 1.4rem; }

/* Aperçu du logo dans les réglages de la société : un fond quadrillé aurait
   été plus juste pour montrer la transparence, mais un cadre clair suffit à
   juger du rendu sur un document. */
.societe-logo-apercu {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  align-items: center;
  padding: 0.6rem 0.8rem;
  margin-bottom: 0.6rem;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 8px;
}
.societe-logo-apercu img { max-height: 4.5rem; max-width: 14rem; }

/* =====================================================================
   Bon d'intervention — pensé pour une tablette
   =====================================================================
   Gros champs, peu de colonnes, tout sur une page. Le technicien est chez
   le client, debout : chaque champ doit se viser au doigt sans effort.
   ===================================================================== */
.bon-page .form-group { margin-bottom: 0.9rem; }
.bon-form textarea,
.bon-form input[type="text"],
.bon-form input[type="date"] {
  width: 100%;
  /* 16px minimum : en deçà, iOS zoome au focus et le formulaire se décale. */
  font-size: 16px;
  padding: 0.6rem 0.7rem;
}
.bon-grille {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0 1rem;
}
.bon-entete {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 2rem;
  padding: 0.7rem 0.9rem;
  margin-bottom: 1rem;
  background: var(--gray-50, #f9fafb);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.bon-entete .lab {
  display: block;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-muted);
}

.bon-signatures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1rem;
  margin-bottom: 1rem;
}
.signature-bloc .lab {
  display: block;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-muted);
  margin-bottom: 0.2rem;
}

/* Le cadre de signature. position: relative pour y loger le bouton Effacer. */
.signature-pad {
  position: relative;
  border: 2px dashed var(--border);
  border-radius: 8px;
  background: #fff;
  height: 11rem;
  margin-bottom: 0.5rem;
}
.signature-pad.a-signe { border-style: solid; border-color: var(--primary, #4f46e5); }
.signature-pad canvas {
  display: block;
  width: 100%;
  height: 100%;
  /* SANS CECI, RIEN NE FONCTIONNE AU DOIGT : le premier mouvement ferait
     défiler la page au lieu de tracer. */
  touch-action: none;
  cursor: crosshair;
}
.signature-effacer { position: absolute; top: 0.4rem; right: 0.4rem; }

/* Signature figée, une fois le bon clos. */
.signature-figee {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #fff;
  height: 9rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.signature-figee img { max-height: 8rem; max-width: 100%; }

.bon-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-top: 1.2rem;
}
/* Le bouton de signature se vise au pouce : c'est le geste du moment. */
.btn-lg { font-size: 1.05rem; padding: 0.8rem 1.4rem; }

/* =====================================================================
   Natures d'un dossier et filtres de la liste
   ===================================================================== */

/* Cases à cocher des natures, au formulaire du dossier : des blocs assez larges
   pour porter la description, qui dit ce que la nature recouvre. */
.dossier-natures {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  gap: 0.6rem;
  margin: 0.5rem 0 0.75rem;
}
.dossier-nature {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: pointer;
}
.dossier-nature:hover { background: var(--gray-50, #f9fafb); }
/* :has() marque la case cochée sans JavaScript. Là où il manque, le bloc reste
   parfaitement utilisable : seule la mise en valeur disparaît. */
.dossier-nature:has(input:checked) {
  border-color: var(--primary, #4f46e5);
  background: var(--primary-light, #eef2ff);
}
.dossier-nature-icone { font-size: 1.2rem; line-height: 1.2; }
.dossier-nature-texte { display: flex; flex-direction: column; gap: 0.1rem; }
.dossier-nature-texte small { color: var(--text-muted); font-size: 0.78rem; }

/* Barre de filtres de la liste des dossiers. */
.dossiers-filtres {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 1.5rem;
  align-items: flex-end;
  padding: 0.85rem 1rem;
  margin-bottom: 1rem;
  background: var(--card-bg, #fff);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
}
.dossiers-filtres-groupe { border: 0; padding: 0; margin: 0; }
/* « Nature », « État » — et non « NATURE », « ÉTAT ». Même raison que
   les en-têtes de tableau : ce sont des mots, pas des étiquettes de
   panneau de commande. */
.dossiers-filtres-groupe legend {
  padding: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--gray-700);
  margin-bottom: 0.35rem;
}
.dossiers-filtres-groupe { display: flex; flex-direction: column; }
.dossiers-filtres-groupe > label { display: inline-flex; }
.filtre-case {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-right: 0.9rem;
  font-size: 0.88rem;
  cursor: pointer;
  white-space: nowrap;
}
/* Les cases d'une même famille sur une ligne, et les familles côte à côte :
   l'ensemble tient sur une seule bande au lieu d'une colonne à défiler. */
.dossiers-filtres-groupe { flex-direction: row; flex-wrap: wrap; align-items: center; }
.dossiers-filtres-groupe legend { width: 100%; margin-bottom: 0.3rem; }
.dossiers-filtres-actions { display: flex; gap: 0.4rem; margin-left: auto; }
.dossiers-filtres-compte { margin: -0.5rem 0 0.75rem; font-size: 0.85rem; }

/* Nature de dossier sans écran dédié — Électricité, Courant faible.
   Ni lien, ni badge « en cours de développement » : la carte constate, elle
   n'invite pas à cliquer. */
.service-card--etiquette { cursor: default; }

/* Colonne d'actions maintenue à vue, quoi qu'il arrive au défilement.

   Une table large déborde de son conteneur : les boutons, qui sont en dernière
   colonne, partaient hors de l'écran et il fallait défiler horizontalement pour
   archiver un dossier. Or c'est précisément la colonne dont on a besoin sans
   chercher. Collée à droite, elle reste sous la main et le reste défile dessous.

   Le fond opaque est indispensable : sans lui, le texte des colonnes passerait
   visiblement sous les boutons. L'ombre portée marque la césure. */
.table-responsive .data-table th.u-col-sticky,
.table-responsive .data-table td.u-col-sticky {
  position: sticky;
  right: 0;
  z-index: 2;
  background: var(--card-bg, #fff);
  box-shadow: -6px 0 8px -6px rgba(0, 0, 0, 0.18);
}
.table-responsive .data-table th.u-col-sticky { background: var(--gray-50, #f9fafb); z-index: 3; }
/* Le survol d'une ligne doit teinter la cellule collante comme les autres,
   sinon elle reste blanche et semble détachée de sa ligne. */
.data-table tbody tr:hover td.u-col-sticky { background: var(--gray-50, #f9fafb); }

@media (max-width: 768px) {
  /* Sur un écran étroit, figer une colonne mange la largeur utile : on rend
     la main au défilement normal. */
  .table-responsive .data-table th.u-col-sticky,
  .table-responsive .data-table td.u-col-sticky { position: static; box-shadow: none; }
}

/* Ligne de tableau menant à une fiche : le survol annonce déjà la cible,
   le curseur la confirme. Voir table-row-link.js. */
.data-table tr[data-row-href] { cursor: pointer; }
/* Pendant l'édition en ligne, la ligne ne navigue plus : le curseur ne doit pas
   promettre une ouverture qui n'aura pas lieu. */
.data-table tr[data-row-href].editing,
.data-table tr[data-row-href].is-saving { cursor: default; }
.data-table tr[data-row-href]:focus-visible {
  outline: 2px solid var(--primary, #4f46e5);
  outline-offset: -2px;
}

/* KNX tree toolbar (Tout développer / Tout réduire) */
.kx-tree-toolbar { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.75rem; font-size: 0.85rem; }
.kx-tree-toolbar .btn-link { padding: 0.1rem 0.2rem; }
.kx-tree-toolbar-sep { color: var(--border); }

/* Reserved-character ("\") field validation */
.kx-field-error { color: var(--error-text); font-size: 0.82rem; margin-top: 0.25rem; }
.inline-input.kx-field-invalid { border-color: var(--error-text); }
button.kx-save-disabled, button:disabled.kx-save-disabled { opacity: 0.5; cursor: not-allowed; }

/* Rooms hint banner */
.kx-hint-banner { display: flex; align-items: flex-start; gap: 0.6rem; }
.kx-hint-banner-icon { flex-shrink: 0; }
.kx-hint-banner-body { flex: 1; }
.kx-hint-banner-link { display: inline-block; margin-left: 0.35rem; font-weight: 600; }
.kx-hint-dismiss { display: block; margin-top: 0.4rem; padding: 0; background: none; border: none; cursor: pointer; font-size: 0.82rem; font-family: inherit; color: inherit; text-decoration: underline; }

/* Reserved-separator informative note */
.kx-reserved-char-note { margin-bottom: 1rem; font-size: 0.85rem; color: var(--text-muted); }
.kx-reserved-char-note summary { cursor: pointer; }
.kx-reserved-char-note p { margin: 0.4rem 0 0; }
.kx-reserved-char-note code { background-color: var(--bg); padding: 0.05rem 0.35rem; border-radius: 4px; }

/* Programmation module: devices & button configuration */
.kx-devices-tree { display: flex; flex-direction: column; gap: 1rem; margin-top: 1rem; }
.kx-device-group { background-color: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow-sm); border: 1px solid var(--gray-200); padding: 1.25rem; }
.kx-device-group-title { margin: 0 0 0.75rem; font-size: 1rem; color: var(--text-muted); }
.kx-device-group-item { border-top: 1px solid var(--border); }
.kx-device-group-item:first-of-type { border-top: none; }
.kx-device-row { margin: 0; padding: 0.6rem 0.5rem; }
.kx-device-meta { color: var(--text-muted); font-size: 0.85rem; }
.kx-device-detail { padding: 1rem 0.5rem 1.25rem; }
.kx-button-panel { display: flex; flex-wrap: wrap; gap: 1.5rem; }
/* .kx-button-grid est maintenant une vraie grille — la règle est posée
   plus haut, avec le reste du module KNX. Celle qui vivait ici, en
   display:flex, l'emportait à spécificité égale parce qu'elle venait
   après : la grille s'affichait sur une seule ligne quelles que soient
   les colonnes déclarées.

   .kx-button-col disparaît avec elle : plus aucun balisage ne la produit
   depuis que les boutons sont posés case par case. */
.kx-device-button {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.25rem; width: 110px; min-height: 64px; padding: 0.5rem;
  background-color: var(--bg); border: 2px solid var(--border); border-radius: 8px;
  cursor: pointer; text-align: center; transition: border-color 0.15s ease, background-color 0.15s ease;
}
.kx-device-button:hover { border-color: var(--primary); }
.kx-device-button.is-selected { border-color: var(--primary); background-color: var(--primary-light); }
.kx-device-button-number { font-weight: 700; font-size: 0.85rem; color: var(--text-muted); }
.kx-device-button-label { font-size: 0.85rem; word-break: break-word; }
.kx-button-config { flex: 1 1 280px; min-width: 260px; }
.kx-button-config-placeholder { color: var(--text-muted); font-style: italic; }
.kx-button-config-form h4 { margin: 0 0 0.75rem; }
.kx-address-field-inputs { display: flex; gap: 0.5rem; }
.kx-address-field-inputs .kx-address-select, .kx-address-field-inputs .kx-address-custom { flex: 1 1 auto; }
.kx-address-toggle { display: inline-block; margin-top: 0.35rem; font-size: 0.82rem; }
.kx-save-success { color: var(--success-text); }
.kx-save-error { color: var(--error-text); }

/* ── UN SEUL ENREGISTREMENT, POUR TOUT L'APPAREIL ────────────────────

   La barre reste sous la grille, à une place fixe. Sous le formulaire,
   elle aurait sauté de vingt centimètres à chaque pliage de l'appui
   long, et l'on aurait cliqué à côté.

   NE PAS RÉSERVER LA PLACE DU FORMULAIRE. `display: contents` efface la
   boîte qui l'enveloppe : quand le formulaire est masqué, il ne reste
   rien — ni hauteur, ni interligne de la colonne flex. Un bloc vide de
   260 px sous la grille, pendant qu'on ne fait que consulter, ne dit
   rien et occupe tout.
*/
.kx-button-panel--empile > .kx-button-config { display: contents; }

.kx-device-enregistrer {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.kx-device-save-status { font-size: 0.8125rem; }
.kx-device-save-status.is-attente { color: var(--warning, #b45309); font-weight: 600; }

/* Le bouton porte la trace de ce qui attend d'être envoyé. Sans elle, on
   règle six boutons, on en oublie un, et rien à l'écran ne dit lequel —
   le compteur annonce « 5 à enregistrer » sans les nommer. */
.kx-device-button.est-modifie {
  border-color: var(--warning, #b45309);
  box-shadow: inset 3px 0 0 var(--warning, #b45309);
}

/* Mobile (< 768px): stacked KNX tree, scrollable tables */
@media (max-width: 767px) {
  .kx-address-table, .kx-product-addresses {
    display: block; overflow-x: auto; -webkit-overflow-scrolling: touch;
    white-space: nowrap; margin-left: 0;
  }
  .kx-group-row { flex-direction: column; align-items: stretch; }
  .kx-toggle { align-self: flex-start; }
  .kx-product-room, .kx-product-name { width: 100%; }
  .actions-cell { width: 100%; flex-wrap: wrap; justify-content: flex-end; }
  .kx-middle-group, .kx-rooms-list { margin-left: 0.75rem; padding-left: 0.75rem; }
  .kx-product-addresses { margin-left: 0.75rem; }
}

@media (min-width: 1024px) {
  .kx-project-layout { grid-template-columns: minmax(0, 65fr) minmax(280px, 35fr); }
  .kx-project-summary { position: sticky; top: calc(var(--header-height) + 20px); }
}

/* =====================================================================
   Feuilles d'heures / Fiche navette (services/heures/*)
   ===================================================================== */
.heures-filter-bar { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; align-items: center; margin-bottom: 1.5rem; }
.heures-filter-bar label { display: flex; align-items: center; gap: .4rem; font-size: .9rem; }

.heures-week-nav { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1.2rem; flex-wrap: wrap; }
.heures-week-title { text-align: center; flex: 1; }
.heures-week-title h1 { margin: 0 0 .2rem; }
.heures-week-dates { font-size: .9rem; color: var(--text-muted); margin-right: .6rem; }
.heures-contract { display: block; font-size: .85rem; color: var(--text-muted); margin-top: .3rem; }

.heures-table-wrap { overflow-x: auto; margin-bottom: 1rem; }
.heures-table { border-collapse: collapse; min-width: 900px; width: 100%; font-size: .88rem; }
.heures-table th, .heures-table td { border: 1px solid var(--gray-200); padding: .35rem .45rem; text-align: center; white-space: nowrap; }
.heures-table th { background: var(--gray-100); font-size: .82rem; font-weight: 600; }
.heures-table td.day-name { font-weight: 600; text-align: left; width: 6rem; }
.day-date { display: block; color: var(--text-muted); font-size: .85em; font-weight: 400; }
.heures-table td.day-name-today { background-color: var(--info-light); }
.day-date-today { font-weight: 700; text-decoration: underline; color: var(--info); }

.heures-table input[type="text"], .heures-table input[type="number"], .heures-table input[type="time"], .heures-table select {
  border: 1px solid var(--gray-300); border-radius: 4px; padding: .2rem .35rem; font-size: .85rem;
  background: #fff; width: 100%; box-sizing: border-box;
}
.heures-table td:nth-child(2) { min-width: 9rem; }
.heures-table td.time-field { min-width: 6.5rem; }
.heures-table td.meal-cell { width: 3.5rem; }
.heures-table td.zone-cell { width: 4rem; }
.heures-table td.total-cell { width: 4.5rem; font-weight: 600; }

.heures-day-row.day-absent .time-field input { background: var(--gray-100); color: var(--gray-400); cursor: not-allowed; }

.zone-label { display: inline-block; padding: .1rem .45rem; border-radius: 3px; font-size: .8rem; font-weight: 700; background: var(--info-light); color: var(--info); min-width: 2rem; }
.zone-label:empty::before, .zone-label[data-zone=""]::before { content: '—'; color: var(--gray-400); font-weight: 400; }

.heures-totals-row td { background: var(--gray-50); font-weight: 600; border-top: 2px solid var(--gray-300); }
.heures-totals-row .overtime-cell { text-align: left; font-weight: 400; padding-left: .8rem; }

.heures-actions { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.heures-back { margin-top: .5rem; }

.save-status { font-size: .85rem; transition: opacity .3s; }
.save-status.saving { color: var(--text-muted); }
.save-status.saved { color: var(--success); }
.save-status.error { color: var(--danger); }

.text-success { color: var(--success); }
.text-danger { color: var(--danger); }

.navette-export-btns { display: flex; gap: .6rem; }

/* Le mois encadré par ses deux flèches : la lecture va de gauche à droite, le
   titre reste au centre et ne bouge pas d'un mois à l'autre. */
.navette-mois { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.navette-mois h1 { margin: 0; }
.navette-block { margin-bottom: 2rem; border: 1px solid var(--gray-200); border-radius: 6px; overflow: hidden; }
.navette-employee-header { display: flex; align-items: center; justify-content: space-between; background: var(--gray-100); padding: .55rem .8rem; border-bottom: 1px solid var(--gray-200); font-size: .95rem; }
.navette-base { font-size: .85rem; color: var(--text-muted); }
.navette-table { margin: 0; font-size: .84rem; }
.navette-table th, .navette-table td { padding: .3rem .5rem; text-align: center; }
.navette-table td:first-child { text-align: left; }

.heures-viewing-other-banner {
  background: var(--warning-light); border: 1px solid #fde68a; border-radius: 6px;
  padding: 12px 16px; margin-bottom: 16px; display: flex; align-items: center;
  justify-content: space-between; gap: 1rem;
}
.heures-viewing-other-banner a { font-size: .9em; white-space: nowrap; }

.navette-actions-cell { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.navette-absences-cell { display: flex; flex-direction: column; gap: .3rem; align-items: center; }
.navette-absence-entry { display: flex; flex-direction: column; align-items: center; }
.navette-absence-note { display: block; color: var(--text-muted); font-size: .8em; margin-top: .1rem; }

/* =====================================================================
   Media category chips (services/media/upload.php, view.php, index.php)
   ===================================================================== */
.category-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }

.category-chip { display: inline-flex; cursor: pointer; }
.category-chip input[type="checkbox"] { display: none; }

.chip-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border: 1.5px solid var(--gray-200);
  border-radius: 20px;
  font-size: 12.5px;
  background: #fff;
  color: var(--gray-700);
  transition: border-color 0.15s, background 0.15s;
  cursor: pointer;
  user-select: none;
}

.category-chip input:checked + .chip-label {
  border-color: var(--primary);
  background: var(--primary-light);
  color: var(--primary-dark);
  font-weight: 500;
}

.chip-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  display: inline-block;
}

/* =====================================================================
   Two-column layout utility (e.g. chantiers/view.php: dossier + contact)
   ===================================================================== */
/* ── LA FICHE D'UN DOSSIER, EN PANNEAUX ──────────────────────────────
   Même charte que les listes : fond gris, contenu sur surfaces blanches.
   Ici chaque SECTION est un panneau — Contrats, Matériel installé, Nature
   des travaux, Médias, Documents — titre compris.

   Le titre est DANS le panneau, et pas posé au-dessus comme sur les
   listes. La différence tient à ce qu'on regarde : une liste n'a qu'un
   objet, son titre est celui de la page ; une fiche en empile sept, et un
   titre posé sur le gris se rattache à l'œil au panneau du dessus autant
   qu'à celui du dessous.

   Ces trois conteneurs n'avaient aucun style partagé : .chantier-hub et
   .chantier-danger-zone vivaient dans le <style> de la page, .media-grid
   y était recopiée à l'identique de la fiche contact. */
.chantier-contrats,
.chantier-hub { margin-top: 20px; }

.chantier-danger-zone { margin-top: 32px; }

.page-grise .chantier-contrats,
.page-grise .chantier-hub,
.page-grise .chantier-danger-zone {
  background: var(--card-bg);
  border: 1px solid var(--gray-200);
  border-radius: 10px;
  padding: 18px 20px;
}

/* Un rayon unique sur la page. .card en portait 12 et les panneaux 10 :
   empilés, deux coins de rayons voisins se voient — pas assez pour qu'on
   sache quoi, assez pour que ce soit bancal. */
.page-grise .card { border-radius: 10px; }

/* LA ZONE DE SUPPRESSION NE DOIT PAS RESSEMBLER AUX AUTRES.
   Elle était séparée par un trait horizontal ; devenue panneau blanc, elle
   avait exactement l'allure de la section Médias juste au-dessus. Un liseré
   rouge sur le bord gauche la redistingue — c'est le seul endroit de la
   fiche d'où l'on ne revient pas. */
.page-grise .chantier-danger-zone {
  border-left: 3px solid var(--danger);
}

/* Pas de carte dans la carte : le tableau est déjà porté par le panneau,
   son propre cadre doublerait le trait à deux pixels près. L'en-tête
   reprend le gris habituel, puisqu'il n'est plus la tête d'une surface. */
.page-grise .chantier-contrats .table-responsive,
.page-grise .chantier-hub .table-responsive {
  background: transparent;
  border: none;
  border-radius: 0;
}
.page-grise .chantier-contrats .data-table th,
.page-grise .chantier-hub .data-table th { background: var(--gray-50); }
.page-grise .chantier-contrats .data-table th:first-child,
.page-grise .chantier-hub .data-table th:first-child,
.page-grise .chantier-contrats .data-table th:last-child,
.page-grise .chantier-hub .data-table th:last-child { border-radius: 0; }

/* Le titre d'une section. Il portait un style en ligne — margin-top:2rem
   sur l'un, margin-bottom:0.75rem sur l'autre — donc cinq écarts entre
   sept sections qui se ressemblent. Dans un panneau, la marge du haut
   n'a plus lieu d'être : c'est le panneau qui espace. */
.bloc-titre { margin: 0 0 12px; }
.bloc-titre > h2 { margin: 0; }

.two-col-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  align-items: start;
}
@media (max-width: 768px) {
  .two-col-grid { grid-template-columns: 1fr !important; }
}

/* =====================================================================
   Pagination générique (assets/js/pagination.js)
   ===================================================================== */

/* Masquage par la pagination. Distinct de [hidden], qui reste réservé aux
   filtres des pages : les deux mécanismes doivent pouvoir coexister sans que
   l'un efface l'état de l'autre. */
.is-paged-out { display: none !important; }

/* =====================================================================
   TÉLÉPHONE — les tableaux deviennent des cartes
   =====================================================================

   Sur 375 px, un tableau de huit colonnes ne se lit qu'en le balayant de
   gauche à droite, et la colonne qui identifie la ligne sort de l'écran dès
   le premier défilement. Sous 700 px, chaque ligne devient donc une carte :
   le nom en titre, puis « Intitulé   valeur » ligne par ligne.

   Les étiquettes sont posées par table-cards.js depuis le <thead> : rien à
   écrire dans les soixante-deux écrans concernés, et l'intitulé ne peut pas
   diverger de son en-tête puisque c'est le même texte.

   LE PIÈGE À NE PAS REFAIRE : ne jamais redonner `display` à un <tr> sans
   exclure [hidden] et .is-paged-out. Le premier est posé par les filtres de
   recherche, le second par le paginateur ; un `display: block` inconditionnel
   ferait réapparaître toutes les lignes masquées, et la recherche comme la
   pagination cesseraient de fonctionner — silencieusement, en apparence
   seulement sur mobile.
   ===================================================================== */
@media (max-width: 700px) {
  /* .page-grise est repris ici EXPRÈS. Sa règle porte deux classes, donc
     elle l'emporterait sur celle-ci malgré la requête média : le conteneur
     resterait une carte blanche enveloppant les cartes de chaque ligne,
     une surface dans une surface. Les lignes sont déjà des cartes en
     dessous de 700 px ; le conteneur n'a plus rien à délimiter. */
  .table-responsive,
  .page-grise .table-responsive {
    /* Plus rien à faire défiler horizontalement : la carte tient dans la
       largeur, et un conteneur qui défile encore piège le geste de balayage. */
    overflow-x: visible;
    background: transparent;
    border: none;
    box-shadow: none;
  }

  .data-table { font-size: 14px; }

  /* L'en-tête a fait son travail : ses intitulés sont passés dans les
     cellules. L'afficher en plus les répéterait. */
  .data-table thead { display: none; }

  .data-table,
  .data-table tbody { display: block; width: 100%; }

  .data-table tbody tr:not([hidden]):not(.is-paged-out) {
    display: block;
    background: var(--card-bg);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);
    margin-bottom: 0.6rem;
    padding: 0.75rem 0.9rem;
  }

  .data-table tbody tr:hover td { background: transparent; }

  .data-table td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    width: auto !important;   /* neutralise les u-col-* calibrés pour le bureau */
    padding: 0.25rem 0;
    border-bottom: none;
    /* Le nowrap du bureau est ce qui interdisait toute mise en page étroite. */
    white-space: normal;
    text-align: left !important;
  }

  /* Casse normale, comme les en-têtes de tableau sur grand écran.
     Vu à l'écran : l'étiquette de carte était restée en MAJUSCULES quand
     l'en-tête est passé en casse normale, si bien que le même intitulé
     s'écrivait « État » sur un PC et « ÉTAT » sur un téléphone. Ici le
     rôle d'étiquette est déjà porté par la position et par la couleur
     sourde — les capitales n'ajoutaient qu'une incohérence. */
  .data-table td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto;
    max-width: 45%;
    font-size: 0.8125rem;
    color: var(--text-muted);
    font-weight: 500;
  }

  /* La première cellule fait le titre de la carte : pleine largeur, plus
     grande, et sans étiquette — on ne titre pas « Dossier : » au-dessus du
     nom du dossier. */
  .data-table td[data-carte-titre] {
    display: block;
    font-size: 15px;
    font-weight: 600;
    color: var(--gray-800);
    padding-bottom: 0.4rem;
    margin-bottom: 0.35rem;
    border-bottom: 1px solid var(--gray-100);
  }

  /* Une cellule vide n'apprend rien : sur huit colonnes, elle ne produirait
     que du bruit à sauter avant l'information utile. */
  .data-table td[data-carte-vide]:not([data-carte-titre]) { display: none; }

  /* Ligne de service — « aucun résultat », totaux : elle couvre la largeur. */
  .data-table tbody tr[data-carte-pleine]:not([hidden]):not(.is-paged-out) td {
    display: block;
    text-align: center !important;
  }

  /* Les actions se rangent en bas de carte, alignées à droite et aérées :
     c'est là qu'on les cherche, et elles doivent rester distinctes au doigt. */
  .data-table td.actions-cell {
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding-top: 0.6rem;
    margin-top: 0.35rem;
    border-top: 1px solid var(--gray-100);
  }
  .data-table td.actions-cell::before { display: none; }

  /* Un pied de tableau (totaux) garde sa forme de bloc. */
  .data-table tfoot { display: block; }
  .data-table tfoot tr { display: block; padding: 0.5rem 0.9rem; }
  .data-table tfoot td { display: flex; }
}

/* =====================================================================
   TÉLÉPHONE — cibles tactiles
   =====================================================================
   Les icônes d'action font 1.75rem (28 px) : confortable à la souris,
   trop petit au doigt, où la recommandation commune d'Apple et de Google
   est 44 px. Sur une ligne qui porte un cadenas, un crayon et une corbeille
   côte à côte, 28 px font qu'on se trompe de bouton — et l'un des trois
   supprime.

   On agrandit la ZONE sans grossir le dessin : l'icône garde sa taille,
   c'est sa surface cliquable qui s'étend.
   ===================================================================== */
@media (max-width: 768px) {
  .icon-action,
  .row-lock,
  .btn-link.icon-action {
    min-width: 44px;
    min-height: 44px;
    width: auto;
    height: auto;
  }

  /* Les cases à cocher et boutons radio des formulaires : mêmes doigts. */
  .form-group input[type="checkbox"],
  .form-group input[type="radio"],
  .kx-median-case input,
  label > input[type="checkbox"] {
    width: 1.35rem;
    height: 1.35rem;
    vertical-align: middle;
  }

  /* Un bouton doit rester atteignable sans viser. */
  .btn, .btn-sm { min-height: 40px; }

  /* Les barres d'actions passent à la ligne au lieu de déborder. */
  .actions-cell, .form-actions, .admin-header .actions-cell {
    flex-wrap: wrap;
  }

  /* iOS zoome sur un champ dont la police fait moins de 16 px, et ne
     dézoome pas ensuite : la page reste agrandie après la saisie. */
  .form-group input,
  .form-group textarea,
  .form-group select,
  .form-control,
  .inline-input {
    font-size: 16px;
  }
}

/* =====================================================================
   SUGGÉRER UNE AMÉLIORATION
   =====================================================================
   Un bouton flottant, présent sur chaque page. En bas à GAUCHE et non à
   droite : la droite est l'endroit où l'on met la main pour faire défiler au
   pouce, et où viennent les boutons d'action des tableaux. À gauche, il ne
   recouvre rien de ce qu'on manipule.
   ===================================================================== */
.suggestion-bouton {
  position: fixed;
  left: 1rem;
  bottom: 1rem;
  z-index: 120;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem 0.9rem;
  border: 1px solid var(--gray-200);
  border-radius: 2rem;
  background: var(--card-bg, #fff);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  font: inherit;
  font-size: 0.88rem;
  color: var(--text-muted);
  /* Discret au repos : il doit être trouvable sans réclamer l'attention à
     chaque page. */
  opacity: 0.75;
  transition: opacity 0.15s ease, border-color 0.15s ease;
}
.suggestion-bouton:hover { opacity: 1; border-color: var(--primary); color: var(--primary); }

.suggestion-dialogue {
  position: fixed;
  inset: 0;
  margin: auto;
  height: fit-content;
  width: min(34rem, calc(100vw - 2rem));
  border: none;
  border-radius: 0.6rem;
  padding: 1.25rem;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
}
.suggestion-dialogue::backdrop { background: rgba(0, 0, 0, 0.35); }

.suggestion-zone-ligne {
  margin: 0.6rem 0 0;
  padding: 0.5rem 0.7rem;
  background: var(--primary-light, #eff6ff);
  border-radius: 0.35rem;
  font-size: 0.87rem;
}
.suggestion-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.9rem; }
.suggestion-etat { min-height: 1.2em; margin: 0.5rem 0 0; font-size: 0.87rem; }
.suggestion-etat--ok { color: var(--success, #15803d); }
.suggestion-etat--erreur { color: var(--danger, #b91c1c); font-weight: 600; }

/* ── Mode « désigner la zone » ───────────────────────────────────────────
   Le curseur change sur TOUTE la page : c'est ce qui dit qu'on n'est plus en
   train de s'en servir mais de la montrer. Sans ce signal, on cliquerait en
   croyant agir. */
body.suggestion-en-visee, body.suggestion-en-visee * { cursor: crosshair !important; }

.suggestion-surligne {
  outline: 3px solid var(--primary, #4f46e5) !important;
  outline-offset: 2px;
  background: rgba(79, 70, 229, 0.08) !important;
}

#suggestion-visee-barre {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: 1.25rem;
  z-index: 3000;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 1rem;
  border-radius: 2rem;
  background: var(--gray-800, #1f2937);
  color: #fff;
  font-size: 0.9rem;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.3);
}
#suggestion-visee-annuler {
  background: rgba(255, 255, 255, 0.15);
  border: none;
  color: #fff;
  border-radius: 1rem;
  padding: 0.3rem 0.8rem;
  cursor: pointer;
  font: inherit;
}

@media (max-width: 768px) {
  /* Le texte tombe, l'ampoule reste : sur un écran étroit, la pastille suffit
     et ne mange pas la largeur utile. */
  .suggestion-bouton-texte { display: none; }
  .suggestion-bouton { padding: 0.7rem; min-width: 44px; min-height: 44px; justify-content: center; }
}

@media print {
  .suggestion-bouton, .suggestion-dialogue, #suggestion-visee-barre { display: none !important; }
}

/* =====================================================================
   TÉLÉPHONE — en-têtes de page et fiches
   =====================================================================
   Relevé sur la fiche d'un dossier, en 375 px :

    · la barre du haut affiche le titre complet de la page — « Prise renforcée
      — Allichon-Gibet — Dossier » — qui passait à la ligne trois fois et
      poussait tout le contenu vers le bas, pour une information que le titre
      juste en dessous répète déjà ;
    · ce titre-là, à 1.9rem, occupait à son tour trois lignes ;
    · .admin-header aligne son contenu avec space-between : les boutons
      partaient se ranger à droite d'une deuxième ligne, en escalier ;
    · .project-info est une carte DANS une carte — deux bordures, deux ombres,
      deux fois 20 px de marge, plus 3.25rem réservés à droite pour un crayon.
      Il ne restait qu'une colonne de texte étroite au milieu de beaucoup de
      blanc.
   ===================================================================== */
@media (max-width: 768px) {
  /* Une seule ligne, coupée proprement : le titre de page reste un repère,
     pas un paragraphe. */
  .topbar-title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 15px;
  }
  .topbar > .d-flex { min-width: 0; flex: 1 1 auto; }
  .topbar-actions { flex: 0 0 auto; }

  .admin-page h1 { font-size: 1.35rem; line-height: 1.25; }
  .admin-page h2, .admin-subheading { font-size: 1.1rem; }

  /* En colonne : le titre, puis les actions alignées à gauche sous lui.
     L'alignement à droite d'un retour à la ligne donnait un escalier. */
  .admin-header {
    flex-direction: column;
    align-items: stretch;
    gap: 0.6rem;
    margin-bottom: 1rem;
  }
  .admin-header .actions-cell { justify-content: flex-start; }

  /* La carte imbriquée redevient un simple bloc : le cadre extérieur suffit
     à dire que c'est une carte. */
  .project-info {
    border: none;
    box-shadow: none;
    background: transparent;
    padding: 0 2.75rem 0 0;
    margin-bottom: 0;
  }
  .kx-info-edit-btn, .kx-info-edit-actions { top: 0; right: 0; }

  .card-body { padding: 14px; }
  .page-header { margin-bottom: 1rem; }

  /* Un champ d'édition en ligne prenait 320 px au maximum, soit presque toute
     la largeur moins les marges : il déborde. */
  .kx-info-display input.inline-input { max-width: 100%; }
}

/* ── LA BARRE DE PAGINATION, FORME OVHCLOUD ──────────────────────────
   Centrée, sans encadré, avec les pages numérotées. Voir la note en tête
   de buildControls() dans pagination.js pour le pourquoi.

   Pas de carte : le tableau n'est plus dans une carte non plus, et un
   encadré sous un tableau qui n'en a pas dessine une frontière là où il
   n'y a rien à délimiter. */
.nalie-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px 20px;
  margin-top: 16px;
  padding: 10px 0 4px;
  font-size: 0.875rem;
  color: var(--gray-500);
}

.nalie-pagination-size { display: flex; align-items: center; gap: 12px; }

.nalie-pagination-select {
  height: 32px;
  padding: 0 8px;
  border: 1px solid var(--gray-200);
  border-radius: var(--border-radius);
  background: var(--card-bg);
  color: var(--gray-900);
  font-size: 0.875rem;
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.nalie-pagination-select:hover { border-color: var(--gray-300); }
.nalie-pagination-select:focus-visible {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-light);
}

/* Plus de pilule grise autour du compteur : c'est du texte de service, il
   n'a pas à se présenter comme une étiquette cliquable. */
.nalie-pagination-count {
  color: var(--gray-500);
  font-variant-numeric: tabular-nums;
}

.nalie-pagination-nav { display: flex; align-items: center; gap: 4px; }
.nalie-pagination-pages { display: flex; align-items: center; gap: 4px; }

/* Le bouton de page : carré, 32 px, comme chez OVH. La page courante est
   PLEINE et non simplement soulignée — sur une rangée de chiffres gris,
   une nuance de gris ne dit pas assez lequel est le bon. */
.nalie-page-btn {
  min-width: 32px;
  height: 32px;
  padding: 0 8px;
  border: 1px solid transparent;
  border-radius: var(--border-radius);
  background: transparent;
  color: var(--gray-600);
  font-size: 0.875rem;
  font-family: inherit;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background-color 0.12s, color 0.12s, border-color 0.12s;
}
.nalie-page-btn:hover { background: var(--gray-100); color: var(--gray-900); }
.nalie-page-btn:focus-visible {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-light);
}
.nalie-page-btn.est-courante,
.nalie-page-btn.est-courante:hover {
  background: var(--gray-900);
  color: #fff;
  font-weight: 600;
}

.nalie-page-fleche { border-color: var(--gray-200); font-size: 1.1rem; line-height: 1; }
.nalie-page-trou { padding: 0 2px; color: var(--gray-400); user-select: none; }

.nalie-pagination button[disabled] {
  opacity: 0.35;
  cursor: not-allowed;
  background: transparent;
}

@media (max-width: 640px) {
  /* Sur téléphone la barre passe en deux rangées : les numéros d'abord,
     puisque c'est ce qu'on vient y chercher. */
  .nalie-pagination { flex-direction: column-reverse; gap: 12px; }
  .nalie-pagination-size { width: 100%; justify-content: center; }
}

/* =====================================================================
   Calibrage des colonnes et des champs sur la donnée
   =====================================================================
   Principe : une donnée de largeur connue (date, montant, quantité, état)
   ne doit pas occuper l'espace d'une donnée de largeur variable (nom,
   libellé, adresse). Sans contrainte, le navigateur répartit la largeur
   au prorata du contenu et une colonne « Date » finit aussi large qu'un
   nom de dossier.

   Usage : poser une classe u-* sur le <th>/<td> ou sur le .form-group.
   ===================================================================== */

/* ── Colonnes de tableau ──────────────────────────────────────────── */
.u-col-date     { width: 7rem;  white-space: nowrap; font-variant-numeric: tabular-nums; }
.u-col-datetime { width: 10rem; white-space: nowrap; font-variant-numeric: tabular-nums; }
.u-col-money    { width: 8rem;  white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
.u-col-num      { width: 5.5rem; white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
.u-col-pct      { width: 5rem;  white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
.u-col-badge    { width: 8rem;  white-space: nowrap; }
/* 7 rem et non 6. À 96 px, trois icônes d'action en demandaient 106 : la
   cellule débordait de dix pixels, et comme .actions-cell est en
   `overflow: visible` (pour que les menus déroulants puissent sortir), le
   conteneur se mettait à défiler horizontalement. Sur fond blanc la barre
   de défilement passait inaperçue ; dans la carte blanche des listes, elle
   barre le bas du tableau. Le symptôme a changé de place, la cause était
   là depuis le début. */
/* La colonne des cases : juste assez pour la cible tactile, pas plus. Elle
   prend de la place à toutes les lignes, y compris à qui ne coche jamais. */
.u-col-selection { width: 2.75rem; white-space: nowrap; text-align: center; }

.u-col-actions  { width: 7rem;  white-space: nowrap; }
/* Trois icônes ne tiennent pas dans 6 rem : 3 × 1,75 + 2 × 0,35 = 5,95, soit
   pile la largeur, sans la moindre marge. Au-delà de deux boutons, prendre
   celle-ci. */
.u-col-actions-large { width: 8.5rem; white-space: nowrap; }
.u-col-short    { width: 10rem; white-space: nowrap; }
.u-col-medium   { width: 14rem; }

/* Colonne élastique : absorbe la largeur restante. À poser sur la seule
   colonne dont le contenu est réellement variable. */
.u-col-grow     { width: auto; }

/* Action posée au-dessus du bloc Documents d'une fiche. */
.doc-bloc-actions { margin: 0 0 0.75rem; }

/* =====================================================================
   Listes d'achats
   ===================================================================== */

/* Formulaire d'ajout en une ligne : le nom prend la place, la quantité en
   réclame peu, et le bouton reste au bout. Sur mobile, tout s'empile. */
.achats-ajout {
  display: flex;
  gap: 0.6rem;
  align-items: flex-end;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}
.achats-ajout .form-group { margin: 0; }
.achats-ajout-nom  { flex: 1 1 16rem; }
.achats-ajout-qte  { flex: 0 0 5rem; }
.achats-ajout-note { flex: 1 1 12rem; }

/* Les trois états. La couleur porte l'urgence : rouge ce qui n'est pas commandé,
   ambre ce qu'on attend, vert ce qui est là. */
.achat-etat {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
}
.achat-etat-a-commander { background: #fee2e2; color: #991b1b; }
.achat-etat-commande    { background: #fef3c7; color: #92400e; }
.achat-etat-disponible  { background: #dcfce7; color: #166534; }

/* Une ligne reçue s'efface sans disparaître : elle reste vérifiable. */
.data-table tr.achat-fait td { opacity: 0.62; }

/* Statut d'un dossier. « Étude » se distingue de « En cours » sans crier :
   ce n'est pas une alerte, c'est une étape. */
.badge-etude { background: #e0e7ff; color: #3730a3; }

/* Sélecteur de statut dans l'en-tête d'une fiche : à la hauteur des boutons
   voisins, sans prendre leur place.

   Le commentaire disait déjà cela — mais la règle écrivait `height: auto`,
   qui laisse la hauteur suivre la garniture et non les voisins. Elle
   valait 30 px à côté de boutons de 38. Un commentaire qui décrit une
   intention que le code n'exécute pas est pire qu'absent : il dispense de
   vérifier. La hauteur est désormais celle du jeton partagé, donc vraie
   par construction. */
.dossier-statut-form .form-control-sm {
  height: var(--h-commande);
  padding: 0 1.6rem 0 0.6rem;
  font-size: 0.875rem;
  width: auto;
}

/* Étiquette réservée aux lecteurs d'écran : le sélecteur a besoin d'un label,
   l'en-tête n'a pas la place de l'afficher. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* =====================================================================
   Cockpit de l'accueil
   =====================================================================
   Des tuiles d'alerte, déplaçables. Le ton donne le registre au premier
   coup d'œil : une bordure gauche colorée, assez discrète pour que trois
   tuiles côte à côte ne ressemblent pas à un sapin de Noël.
   ===================================================================== */
.cockpit-entete {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.75rem;
}
.cockpit-entete .dash-section-title { margin: 0; }
.cockpit-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; }

/* Quatre colonnes plutôt que trois, et des rangées de hauteur identique.

   Avec « align-items: start », chaque tuile prenait la hauteur de son contenu :
   une tuile d'une ligne à côté d'une tuile de six laissait un grand vide sous la
   première. grid-auto-rows: 1fr donne à toutes les rangées la même hauteur, et
   les tuiles s'étirent pour la remplir — d'où aussi le plafond de cinq lignes
   côté PHP, sans lequel la plus longue étirerait tout le reste. */
/* DEUX COLONNES INÉGALES, comme l'accueil du manager OVHcloud.
   ─────────────────────────────────────────────────────────────────────
   Quatre colonnes de tuiles étroites, c'était quatre encadrés côte à côte
   qui ne tenaient chacun que trois lignes avant de couper. Le contenu du
   cockpit est fait de LISTES — les derniers dossiers, les contrats à
   renouveler, les garanties qui expirent — et une liste a besoin de
   largeur pour que chaque entrée tienne sur une ligne.

   Deux colonnes, dont une large : la colonne de gauche porte ce qui se
   lit ligne à ligne, celle de droite ce qui tient en un chiffre ou deux.
   Le rangement reste celui de l'utilisateur, puisque les tuiles se
   déplacent à la souris — on fournit la forme, pas l'ordre.

   minmax(0, …) et non 2fr/1fr seuls : sans le zéro, une entrée longue
   empêche la colonne de se rétrécir et la grille déborde. Le dépôt s'est
   déjà fait prendre là-dessus avec l'ancienne grille. */
.cockpit-grille {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: 16px;
  align-items: start;
}
@media (max-width: 1000px) { .cockpit-grille { grid-template-columns: minmax(0, 1fr); } }

.cockpit-tuile {
  background: var(--card-bg, #fff);
  border: 1px solid var(--border);
  border-left: 4px solid var(--gray-300, #d1d5db);
  /* Même rayon et même ombre que .card : les tuiles de l'accueil étaient les
     seules surfaces à 8 px et sans relief, ce qui les faisait lire comme des
     encadrés plutôt que comme les cartes qu'elles sont. Le liseré de gauche
     reste : lui porte un sens (alerte, attention, neutre). */
  border-radius: var(--border-radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 0.75rem 0.9rem 0.5rem;
  /* Colonne flex : le pied « Ouvrir → » est poussé en bas par margin-top:auto,
     donc aligné d'une tuile à l'autre quel que soit le nombre de lignes. */
  display: flex;
  flex-direction: column;
  /* touch-action: none sur la poignée seulement — voir plus bas. Ici, on
     laisse le défilement de la page parfaitement normal. */
}
.cockpit-ton-alerte    { border-left-color: #dc2626; }
.cockpit-ton-attention { border-left-color: #d97706; }
.cockpit-ton-neutre    { border-left-color: var(--primary, #4f46e5); }

.cockpit-tuile-entete {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-bottom: 0.5rem;
}
.cockpit-tuile-titre {
  font-weight: 600;
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cockpit-tuile-titre:where(:link, :visited) { color: var(--text); }
.cockpit-tuile-titre:hover { color: var(--primary, #4f46e5); text-decoration: underline; }
.cockpit-compteur {
  background: var(--gray-100, #f3f4f6);
  border-radius: 999px;
  padding: 0.05rem 0.45rem;
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
}

/* La poignée doit rester saisissable au doigt : touch-action none empêche le
   navigateur de partir en défilement dès le premier pixel de mouvement. */
.cockpit-poignee {
  cursor: grab;
  color: var(--text-muted);
  touch-action: none;
  user-select: none;
  padding: 0 0.15rem;
  line-height: 1;
}
.cockpit-poignee:active { cursor: grabbing; }

.cockpit-masquer {
  border: 0;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1;
  padding: 0.1rem 0.2rem;
}
.cockpit-masquer:hover { color: var(--danger, #b91c1c); }

.cockpit-lignes { list-style: none; margin: 0; padding: 0; }
.cockpit-lignes li {
  display: flex;
  gap: 0.5rem;
  align-items: baseline;
  padding: 0.4rem 0;
  border-top: 1px solid var(--gray-100, #f3f4f6);
}
.cockpit-lignes li:first-child { border-top: 0; }

/* Le lien prend la place disponible et pousse la valeur à droite : d'une
   ligne à l'autre, l'échéance tombe toujours au même endroit. */
.cockpit-ligne-lien {
  flex: 1 1 auto;
  min-width: 0;
  display: block;
  text-decoration: none;
}
.cockpit-ligne-titre {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cockpit-ligne-detail {
  display: block;
  font-size: 0.78rem;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cockpit-ligne-valeur {
  flex: 0 0 auto;
  font-size: 0.8rem;
  color: var(--text-muted);
  white-space: nowrap;
}
.cockpit-lignes li.est-urgent .cockpit-ligne-valeur {
  color: var(--danger, #b91c1c);
  font-weight: 600;
}

/* Le pied « Ouvrir → » a disparu : le titre porte le lien. */

/* Une tuile sans réponse le dit, au centre de l'espace qui lui reste — d'où le
   margin auto, qui la place au milieu de la hauteur commune aux tuiles. */
.cockpit-vide {
  margin: auto 0;
  padding: 0.6rem 0;
  color: var(--text-muted);
  font-size: 0.86rem;
  font-style: italic;
  text-align: center;
}

/* Panneau « Mes widgets » : chacun choisit ce qu'il affiche parmi ce que
   l'administrateur lui a mis à disposition. */
.cockpit-panneau {
  background: var(--card-bg, #fff);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.9rem 1rem;
  margin-bottom: 1rem;
}
.cockpit-panneau[hidden] { display: none; }
.cockpit-panneau-liste {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: 0.4rem 1rem;
}
.cockpit-panneau-liste .filtre-case { margin-right: 0; }
.cockpit-panneau-liste small { display: block; font-size: 0.75rem; }

/* Pendant le déplacement : la tuile saisie s'allège, les autres se calment
   pour qu'on voie où elle va tomber. */
.cockpit-tuile.is-saisie {
  opacity: 0.55;
  transform: scale(0.985);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.14);
}
.cockpit-grille.est-en-reorganisation .cockpit-tuile { transition: transform 0.12s ease; }
.cockpit-grille.est-en-reorganisation .cockpit-ligne-lien { pointer-events: none; }

/* ── Autocomplétion d'une entreprise (Annuaire des Entreprises) ───
   Deux lignes par suggestion : la raison sociale, puis SIREN et commune —
   c'est ce qui départage deux entreprises homonymes, et il y en a. */
.entreprise-suggestions {
  position: absolute;
  z-index: 80;
  max-height: 18rem;
  overflow-y: auto;
  background: var(--card-bg, #fff);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.entreprise-suggestions[hidden] { display: none; }
.entreprise-suggestion {
  display: block;
  width: 100%;
  padding: 0.45rem 0.65rem;
  border: 0;
  background: none;
  text-align: left;
  cursor: pointer;
  color: var(--text);
}
.entreprise-suggestion:hover,
.entreprise-suggestion.is-surligne { background: var(--gray-100, #f3f4f6); }
.entreprise-nom { display: block; font-size: 0.9rem; }
.entreprise-detail {
  display: block;
  font-size: 0.78rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
/* Une entreprise radiée reste proposée — elle peut être un ancien client — mais
   elle s'annonce comme telle. */
.entreprise-suggestion.est-cessee .entreprise-nom { color: var(--text-muted); }

/* ── Autocomplétion du nom de produit ────────────────────────────
   Mêmes règles que les suggestions de communes : deux listes déroulantes
   d'aspect différent pour le même geste n'apprendraient rien à personne. */
.achat-suggestions {
  position: absolute;
  z-index: 80;
  max-height: 15rem;
  overflow-y: auto;
  background: var(--card-bg, #fff);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.achat-suggestions[hidden] { display: none; }
.achat-suggestion {
  display: block;
  width: 100%;
  padding: 0.4rem 0.65rem;
  border: 0;
  background: none;
  text-align: left;
  font-size: 0.88rem;
  cursor: pointer;
  color: var(--text);
}
.achat-suggestion:hover,
.achat-suggestion.is-surligne { background: var(--gray-100, #f3f4f6); }

/* Laisse le texte revenir à la ligne dans une cellule.
   Écrit `.data-table td.u-wrap` et non `.u-wrap` : `.data-table td` impose
   white-space: nowrap, et une classe seule perd à spécificité égale — le même
   piège que pour les autres utilitaires de cellule. */
.data-table td.u-wrap { white-space: normal; }
.data-table--calibrated td.u-wrap { overflow: visible; text-overflow: clip; }

/* Tronque proprement au lieu de faire déborder la ligne.

   `max-width: 0` est la ruse classique pour qu'une cellule de tableau
   accepte de tronquer — mais elle ne vaut QUE pour `table-layout: auto`,
   où le navigateur la lit comme « ne réclame aucune largeur ». Dans une
   table `fixed`, elle est prise au mot : elle l'emporte sur la largeur
   déclarée et la colonne tombe à la largeur de ses seules marges.

   C'est ce qui se passait sur Dossiers : la colonne Adresse, pourtant
   déclarée à 14 rem, mesurait 32 px et n'affichait rien. Vu à l'écran
   seulement — le CSS se lit sans rien laisser deviner, et les deux
   règles sont à cinq cents lignes l'une de l'autre.

   Dans une table calibrée la ruse est de toute façon inutile : la
   troncature est déjà assurée par .data-table--calibrated td. */
.u-truncate {
  max-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* table-layout: fixed fait respecter les largeurs ci-dessus ; sans lui le
   navigateur les traite comme de simples suggestions. */
.data-table--calibrated { table-layout: fixed; width: 100%; }
.data-table--calibrated td { overflow: hidden; text-overflow: ellipsis; }
/* Un TEXTE trop long se coupe proprement ; un BOUTON coupé devient
   inatteignable, et rien ne l'annonce — l'utilisateur conclut que la
   fonction ne marche pas. Les cellules d'action ne se rognent donc jamais. */
.data-table--calibrated td.actions-cell { overflow: visible; }

/* La correction : dans une table calibrée, on retire la borne. La largeur
   déclarée par u-col-* reprend la main, et la troncature continue d'être
   assurée par la règle générale des cellules calibrées. */
.data-table--calibrated th.u-truncate,
.data-table--calibrated td.u-truncate { max-width: none; }

@media (max-width: 768px) {
  /* Sur mobile la largeur fixe est contre-productive : on rend la main au
     navigateur et le conteneur .table-responsive prend le défilement. */
  .data-table--calibrated { table-layout: auto; }
  .u-truncate { max-width: none; }
}

/* ── Champs de formulaire ─────────────────────────────────────────── */
/* Un champ date étiré sur toute la largeur d'un formulaire est illisible :
   l'œil ne sait plus quelle zone est saisissable. */
.form-group.u-field-date     input,
.form-group.u-field-date     select { max-width: 11rem; }
.form-group.u-field-money    input { max-width: 10rem; text-align: right; }
.form-group.u-field-num      input { max-width: 8rem;  text-align: right; }
.form-group.u-field-short    input,
.form-group.u-field-short    select { max-width: 16rem; }
.form-group.u-field-medium   input,
.form-group.u-field-medium   select { max-width: 24rem; }
.form-group.u-field-postal   input { max-width: 8rem; }
.form-group.u-field-phone    input { max-width: 13rem; }
.form-group.u-field-full     input,
.form-group.u-field-full     select,
.form-group.u-field-full     textarea { max-width: none; }

@media (max-width: 640px) {
  .form-group[class*="u-field-"] input,
  .form-group[class*="u-field-"] select { max-width: none; }
}

/* Sans classe explicite, un input de type date/number reste borné : c'est le
   défaut raisonnable, une date n'a jamais besoin de 40rem. */
.form-group input[type="date"],
.form-group input[type="time"] { max-width: 11rem; }
.form-group input[type="number"] { max-width: 12rem; }

/* ══ BARRE DE RECHERCHE + PASTILLES DE FILTRE ══════════════════════════════
   Motif commun aux listings : un champ de recherche arrondi à gauche, des
   pastilles de filtre à droite. Les classes gardent leur préfixe « contrats- »
   d'origine — les renommer aurait touché six écrans pour un gain nul — mais le
   style est ici, et non plus dans la seule page Contrats qui l'avait défini
   localement pendant que cinq autres l'utilisaient sans le charger.          */
/* ── Barre de recherche + filtres ───────────────────────────────────────── */
/* ── LES PASTILLES DE FILTRE ─────────────────────────────────────────
   Elles s'appelaient .contrats-filters et .contrats-search, du nom du
   premier écran qui les avait portées. Dix écrans les utilisaient ensuite
   — carburant, parc, documents, journaux, personnel — et aucun ne parlait
   de contrats. Un nom qui ment sur son périmètre fait hésiter à le
   modifier : on ne sait plus qui on casse.

   .filter-pill était en plus définie DEUX FOIS, ici sous .contrats-filters
   et dans le <style> de la page Dossiers, avec des tailles différentes.
   Les pastilles n'avaient donc pas la même allure selon l'écran, et la
   page qui en portait une copie ne le disait pas. Une seule définition,
   sans portée : c'est un composant, il ne doit rien devoir à son parent. */
.barre-filtres { display: flex; gap: 6px; flex-wrap: wrap; }

.filter-pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 0.45rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--card-bg);
    color: var(--text);
    font-size: 0.88rem;
    font-weight: 500;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.filter-pill:hover:not(:disabled) {
    border-color: var(--primary);
    color: var(--primary);
}
.filter-pill.active {
    background: var(--primary);
    border-color: var(--primary);
    color: #fff;
    font-weight: 600;
}
.filter-pill--disabled,
.filter-pill:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

}

/* Le rattrapage DALI se pose sous le total, séparé : c'est une action, pas une
   ligne de résumé. */
/* Sorties d'actionneur par produit, portées par le groupe principal. Le libellé
   est HORS de la cellule éditable : inline-edit.js réécrit le contenu de la
   cellule après enregistrement, un suffixe placé dedans disparaîtrait au premier
   changement.                                                                 */
.kx-sorties-label { font-size: 0.78rem; color: var(--text-muted); white-space: nowrap; }
.kx-sorties-valeur { min-width: 1.5rem; text-align: right; font-variant-numeric: tabular-nums; }
.kx-sorties-saisie { max-width: 5rem; }

/* Choix des groupes médians dans l'ajout rapide, pour les groupes principaux à
   0 sortie. Occupe toute la largeur de la barre de saisie, qui est en flex.   */
.kx-medians-choix {
  flex-basis: 100%;
  margin-top: 0.5rem;
  padding: 0.6rem 0.75rem;
  background: var(--surface);
  border: 1px dashed var(--border);
  border-radius: 6px;
}
.kx-medians-choix[hidden] { display: none; }
.kx-medians-groupe[hidden] { display: none; }
.kx-medians-titre {
  display: block;
  margin-bottom: 0.4rem;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-muted);
}
/* ══ INTÉGRATIONS ══════════════════════════════════════════════════════════
   Une carte par service du catalogue, configuré ou non : une intégration qu'on
   ne voit pas est une intégration dont on ignore l'existence.                 */
.integ-liste { display: flex; flex-direction: column; gap: 0.6rem; margin-top: 1rem; }
.integ-carte {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--card-bg, #fff);
  color: inherit;
}
.integ-carte:hover { border-color: var(--primary); text-decoration: none; }
.integ-icone { font-size: 1.6rem; line-height: 1; }
.integ-corps { flex: 1; min-width: 0; }
.integ-nom { font-weight: 700; }
.integ-desc { font-size: 0.85rem; color: var(--text-muted); }
.integ-etat { white-space: nowrap; }

/* ══ REGISTRE DU PERSONNEL ═════════════════════════════════════════════════
   L'état d'échéance doit se lire d'un coup d'œil : c'est la seule information
   du registre qui appelle une action.                                        */
.pers-etat {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
}
.pers-etat-perime   { background: var(--danger-light, #fef2f2); color: var(--danger, #b91c1c); }
.pers-etat-proche   { background: #fffbeb; color: #b45309; }
.pers-etat-valide   { background: #f0fdf4; color: #15803d; }
/* Sans échéance n'est pas « valide » : c'est hors surveillance, et le gris le dit
   mieux que le vert. */
.pers-etat-sans_fin { background: var(--gray-100, #f3f4f6); color: var(--text-muted); }

.pers-echeances { margin: 0.5rem 0 0; padding-left: 1.2rem; }
.pers-echeances li { margin: 0.15rem 0; }
.pers-sorti { opacity: 0.6; }
.pers-sans-compte {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-style: italic;
  margin-left: 0.35rem;
}
/* ── Fichiers d'un matériel ────────────────────────────────────────────────
   La boîte de liaison reprend la mise en page de la boîte de confirmation :
   même centrage, même contournement du `* { margin: 0 }` de la feuille, qui
   écrase la marge automatique des <dialog> et les colle en haut à gauche. */
.mat-lier-dialogue {
  position: fixed;
  inset: 0;
  margin: auto;
  height: fit-content;
  width: min(32rem, calc(100vw - 2rem));
  border: none;
  border-radius: 0.6rem;
  padding: 1.25rem;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.25);
}
.mat-lier-dialogue::backdrop { background: rgba(0, 0, 0, 0.35); }

.mat-lier-resultats { margin-top: 0.6rem; max-height: 16rem; overflow-y: auto; }

/* Chaque résultat est un bouton pleine largeur : la liste EST le choix, il n'y
   a pas de « valider » derrière, donc la cible doit être franche. */
.mat-lier-choix {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 0.35rem;
  background: var(--surface, #fff);
  cursor: pointer;
  margin-bottom: 0.35rem;
  font: inherit;
}
.mat-lier-choix:hover { border-color: var(--primary); background: var(--primary-light, #eff6ff); }

.pers-identite p { margin: 0.2rem 0; }

/* Tailles : le résumé se lit d'un coup d'œil, donc un peu plus gros que le
   corps du texte. */
.pers-tailles-resume { font-size: 1.05rem; margin: 0 0 0.35rem; }

/* Une ligne sans aucune taille est la seule information utile du récapitulatif :
   c'est elle qu'on cherche pour relancer quelqu'un, elle doit sauter aux yeux
   sans qu'on lise colonne par colonne. */
.data-table tr.pers-tailles-vide td { background: var(--warning-light, #fffbeb); }
.pers-actions-bas { margin: 1rem 0 0.5rem; display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }

/* Grille des symboles d'habilitation : elle reprend la disposition du titre
   imprimé, pour que ce qu'on coche à l'écran se retrouve à la même place sur le
   PDF — c'est ce qui rend la relecture possible. */
.hab-grille td { vertical-align: top; font-size: 0.82rem; }
.hab-grille .hab-domaine { width: 12%; }
.hab-symbole { display: block; margin: 0.1rem 0; cursor: pointer; }
.hab-inline { display: inline-flex; align-items: center; gap: 0.3rem; margin-right: 1rem; cursor: pointer; }
/* Un fieldset désactivé garde la mise en page du formulaire ; sans bordure, la
   seule différence visible est le grisé des champs, ce qui suffit. */
.hab-fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
.hab-emettre { margin-top: 1.5rem; display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.pers-form { max-width: 34rem; margin-top: 0.75rem; }

/* ══ PARTAGE DES PHOTOS AU CLIENT ══════════════════════════════════════════ */
.mp-phase { display: block; margin: 0.2rem 0; cursor: pointer; }
.mp-compte { color: var(--text-muted); font-size: 0.82rem; margin-left: 0.4rem; }
/* Une phase vide se signale sans se barrer la route : elle reste partageable. */
.mp-compte-vide { font-style: italic; }
.mp-lien {
  width: 100%;
  margin-top: 0.4rem;
  font-family: 'Courier New', Courier, monospace;
  font-size: 0.8rem;
}
/* Dans la colonne d'actions, le lien ne doit pas pousser le tableau en largeur. */
.mp-lien-court { max-width: 11rem; margin: 0 0.35rem 0 0; }

/* ══ COMPLÉTION D'UN PRODUIT KNX ═══════════════════════════════════════════
   Les lignes déjà pourvues s'effacent visuellement : ce qui compte sur cet
   écran, c'est ce qui manque.                                                */
.kx-completion-produit { margin-bottom: 1.25rem; }
.kx-completion-type {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 1rem;
}
.kx-completion-type label { font-weight: 600; }
.kx-completion-deja { opacity: 0.55; }
.kx-completion-index { width: 4.5rem; }

/* Une case déjà prise par autre chose doit se distinguer d'une case libre AVANT
   d'enregistrer : c'est le seul écart que l'intégrateur doit trancher lui-même. */
.kx-completion-occupee { background: var(--danger-light, #fef2f2); }
.kx-completion-alerte {
  font-size: 0.78rem;
  color: var(--danger, #b91c1c);
  font-weight: 600;
}
.kx-completion-alerte[hidden] { display: none; }

/* ══ AUTOCOMPLÉTION DES COMMUNES ═══════════════════════════════════════════
   La liste est posée dans <body> et positionnée en coordonnées de page : dans
   le flux, elle serait rognée par le premier parent à overflow caché — la carte
   du formulaire de contact, notamment.                                       */
.commune-suggestions {
  position: absolute;
  z-index: 80;
  max-height: 15rem;
  overflow-y: auto;
  background: var(--card-bg, #fff);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.commune-suggestions[hidden] { display: none; }
.commune-suggestion {
  display: block;
  width: 100%;
  padding: 0.4rem 0.65rem;
  border: 0;
  background: none;
  text-align: left;
  font-size: 0.88rem;
  cursor: pointer;
  color: var(--text);
}
.commune-suggestion:hover,
.commune-suggestion.is-surligne { background: var(--gray-100, #f3f4f6); }
.commune-cp {
  display: inline-block;
  min-width: 3.6rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* ══ BIBLIOTHÈQUES DE DOCUMENTS ════════════════════════════════════════════
   La zone confidentielle doit se distinguer d'un coup d'œil dans une liste
   mélangée : c'est le seul repère qui dise, avant d'ouvrir, qu'une pièce ne se
   transfère pas sans y penser.                                              */
.doc-bloc { margin-top: 1.5rem; }
.doc-bloc:first-child { margin-top: 0; }

/* Vignettes de médias. Recopiées à l'identique dans le <style> de la fiche
   dossier ET de la fiche contact : deux exemplaires d'un même composant,
   donc deux occasions de n'en corriger qu'un. */
.media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }
.media-grid-item {
    display: block; position: relative; aspect-ratio: 1; overflow: hidden;
    border-radius: 8px; background: var(--gray-50); border: 1px solid var(--border);
}
.media-grid-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.media-grid-item .no-thumb { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 2rem; }
.media-type-badge {
    position: absolute; top: 4px; right: 4px; background: rgba(0,0,0,0.6);
    color: #fff; border-radius: 4px; padding: 2px 5px; font-size: 11px;
}
.doc-titre { font-weight: 600; }

/* Zones de dépôt, une par bibliothèque. Elles restent côte à côte tant qu'il y a
   la place, et passent l'une sous l'autre en dessous : deux cibles étroites se
   confondent au doigt, et se tromper de zone est précisément ce qu'il faut
   éviter. */
.doc-zones {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1rem;
  margin: 1rem 0 0.5rem;
}
.doc-zones-formats { margin-bottom: 1.25rem; }

.doc-depot-rattachement { max-width: 34rem; margin-top: 1rem; }
.doc-depot-rattachement label { display: block; font-weight: 600; margin-bottom: 0.25rem; }
.doc-depot-rattachement select { width: 100%; }

.doc-zone-depot {
  position: relative;
  padding: 1.1rem 1rem;
  border: 2px dashed var(--border);
  border-radius: 10px;
  background: var(--surface);
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}
.doc-zone-depot:hover { border-color: var(--primary); }

/* L'état de survol doit être franc : c'est le seul retour visuel qui confirme
   qu'on lâchera dans la bonne bibliothèque. */
.doc-zone-depot.is-survol {
  border-color: var(--primary);
  border-style: solid;
  background: var(--primary-light, #eff6ff);
}
.doc-zone-depot.is-envoi { opacity: 0.7; cursor: progress; }

/* Un fichier collé attend qu'on désigne sa bibliothèque : les deux cadres
   s'animent pour dire que le prochain clic range, et n'ouvre plus le sélecteur
   de fichiers. Sans ce signal, le clic ferait autre chose que d'habitude sans
   que rien ne l'annonce. */
.doc-zone-depot.is-colle {
  border-style: solid;
  border-color: var(--primary);
  animation: doc-colle-pulse 1.4s ease-in-out infinite;
}
@keyframes doc-colle-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(79, 70, 229, 0.25); }
  50%      { box-shadow: 0 0 0 5px rgba(79, 70, 229, 0); }
}
@media (prefers-reduced-motion: reduce) {
  .doc-zone-depot.is-colle { animation: none; background: var(--primary-light, #eff6ff); }
}

.doc-zone-depot--confidentiel { border-color: var(--danger, #b91c1c); }
.doc-zone-depot--confidentiel:hover,
.doc-zone-depot--confidentiel.is-survol {
  border-color: var(--danger, #b91c1c);
  background: var(--danger-light, #fef2f2);
}

.doc-zone-titre { font-weight: 700; font-size: 0.95rem; }
.doc-zone-aide { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.2rem; }
.doc-zone-consigne { font-size: 0.82rem; color: var(--text-muted); margin-top: 0.6rem; }
.doc-depot-etat { font-size: 0.82rem; margin-top: 0.5rem; min-height: 1.1em; }
.doc-depot-etat--ok { color: var(--success, #15803d); }
.doc-depot-etat--erreur { color: var(--danger, #b91c1c); font-weight: 600; }

/* Hors flux plutôt que display:none : un champ réellement masqué n'est pas
   ouvrable par .click() sur certains navigateurs. */
.doc-zone-champ {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}
.doc-zone { font-size: 0.82rem; color: var(--text-muted); white-space: nowrap; }
.doc-zone--confidentiel {
  color: var(--danger, #b91c1c);
  font-weight: 600;
}

.kx-median-case {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0 0.9rem 0.3rem 0;
  font-size: 0.86rem;
  cursor: pointer;
}

.kx-summary-dali { margin-top: 0.75rem; }
.kx-summary-dali button { width: 100%; }

/* ══ TRI DES COLONNES ══════════════════════════════════════════════════════
   Piloté par assets/js/table-sort.js, actif sur tout tableau dont un en-tête
   porte la classe. La flèche indique le sens en cours ; ↕ signale simplement
   qu'on peut cliquer.                                                       */
/* ── L'INDICATEUR DE TRI ─────────────────────────────────────────────
   Deux flèches opposées (⇅) et non une double flèche verticale (↕) : le
   glyphe d'OVH montre les deux sens possibles côte à côte, celui de Nalie
   ressemblait à une poignée de redimensionnement.

   Il est TOUJOURS là, en gris pâle, sur toute colonne triable. Un
   indicateur qui n'apparaît qu'au survol ne se découvre qu'en promenant
   la souris sur l'en-tête, ce que personne ne fait avant d'avoir deviné
   que le tri existe.

   L'espace insécable avant le glyphe le colle à son intitulé : une espace
   ordinaire laisse la ligne se couper entre le titre et sa flèche. */
.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
.sortable::after {
  content: '\00a0\21c5';
  font-size: 12px;
  color: var(--gray-400);
  font-weight: 400;
}
.sortable:hover::after { color: var(--gray-600); }
.sortable.asc::after  { content: '\00a0\2191'; color: var(--primary); font-weight: 700; }
.sortable.desc::after { content: '\00a0\2193'; color: var(--primary); font-weight: 700; }
/* La colonne triée se désigne aussi par son intitulé : sur une table large,
   la flèche seule se cherche. */
.data-table th.sortable.asc, .data-table th.sortable.desc { color: var(--primary-dark); }
