/* palace-tech — composants du systeme de design "Trianon".
   S'appuie sur les jetons de public/css/tokens.css. Les classes reprennent
   volontairement les noms deja utilises par public/js/*.js et le HTML genere
   cote client (thread-row, status-pill, card, tabbar...) : seule l'habillage
   change, la logique JS reste intacte. Voir docs/lots/LOT-PALACE-TECH.cadrage-ecrans.md
   §1 (couleurs, mise en page -- inchangees) et §1 bis (revision typographique
   du 2026-09-07, remplace le volet typo du §1 -- lisibilite d'abord, public
   technicien peu a l'aise avec le telephone). */

@import url('/css/fonts.css');

*, *::before, *::after { box-sizing: border-box; }

html { height: 100%; -webkit-text-size-adjust: 100%; }

body {
  min-height: 100%;
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--size-body);
  font-weight: var(--weight-body);
  line-height: var(--line-body);
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  -webkit-font-smoothing: antialiased;
  transition: background-color 0.15s ease, color 0.15s ease;
}

/* -- typographie (revision 2026-09-07, cadrage §1 bis) --
   Manrope partout, hierarchie par TAILLE et GRAISSE plutot que par taille
   encore plus petite ou par famille differente. Les capitales espacees ne
   restent que sur les etiquettes courtes (.caps, boutons, nav, badges) : sur
   un vrai titre (nom d'equipement, phrase de section), les capitales genent
   la lecture au lieu de l'aider -- retirees ici. */

h1 {
  font-family: var(--font-display);
  font-weight: var(--weight-page-title);
  font-size: var(--size-page-title);
  line-height: var(--line-title);
  margin: 0 0 0.3em;
  color: var(--ink);
}

h2 {
  font-family: var(--font-display);
  font-weight: var(--weight-section-title);
  font-size: var(--size-section-title);
  line-height: var(--line-title);
  margin: 0 0 0.4em;
  color: var(--ink);
}

/* sous-titre de carte (ex. "Checklist", "Assignation") : meme taille que le
   corps, mais 700 -- la hierarchie se lit a la graisse, jamais a une taille
   plus petite (cadrage §1 bis). */
h3 {
  font-family: var(--font-display);
  font-weight: var(--weight-strong);
  font-size: var(--size-body);
  line-height: var(--line-title);
  margin: 0 0 0.4em;
  color: var(--muted);
}

/* etiquette / eyebrow : SEULE categorie de texte qui reste en capitales
   (cadrage §1 bis, table "etiquettes et eyebrows"). h3.caps (ex. "PLAQUE
   SIGNALETIQUE", "HISTORIQUE") l'emporte sur la regle h3 ci-dessus par
   specificite -- volontaire : ce sont des eyebrows, pas des sous-titres. */
.caps {
  font-family: var(--font-display);
  font-weight: var(--weight-label);
  font-size: var(--size-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  margin: 0 0 0.5em;
  color: var(--ink);
}
h3.caps { color: var(--muted); }

/* .brand (topbar) : sur la plupart des ecrans c'est le SEUL titre visible
   (pas de h1 a cote) -- traite comme un titre de page (cadrage §1 bis),
   jamais en capitales/Jost ici (reserve a .sidebar-brand plus bas). */
.brand {
  display: block;
  font-family: var(--font-display);
  font-weight: var(--weight-page-title);
  font-size: var(--size-page-title);
  line-height: var(--line-title);
  margin: 0;
  color: var(--ink);
}

p { margin: 0 0 0.75em; }

/* texte important : valeurs, noms -- 700 des que l'information compte
   (cadrage §1 bis). */
strong, b, .strong { font-weight: var(--weight-strong); }

small, .hint, .meta {
  font-family: var(--font-body);
  font-size: var(--size-small);
  font-weight: var(--weight-body);
  color: var(--muted);
}

code, .mono, .data {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--size-data);
  font-weight: var(--weight-data);
  background: none;
}

a { color: var(--primary); text-decoration: none; }

/* lien secondaire : petites capitales soulignees sauge (cadrage §1) — une
   seule action pleine (.btn) par ecran, tout le reste est ce lien. */
.link-caps,
.btn-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: var(--tap-min);
  padding: 4px 2px;
  font-family: var(--font-display);
  font-size: var(--size-label);
  font-weight: var(--weight-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--accent-ink);
  background: none;
  border: none;
  border-bottom: 1px solid var(--accent);
  border-radius: 0;
  cursor: pointer;
  text-decoration: none;
  /* "← Fils" ne doit jamais casser la fleche de son libelle sur deux lignes. */
  white-space: nowrap;
}
.btn-secondary:hover, .link-caps:hover { opacity: 0.75; }
/* bouton secondaire PLEINE LARGEUR : jamais nowrap. Le nowrap herite de
   .btn-secondary sert les liens COMPACTS ("← Fils" ne doit jamais casser sa
   fleche) -- un bouton pleine largeur a toute la place pour passer a la
   ligne, et un texte long qui ne le fait pas deborde hors ecran a 320px
   ("Choisir dans la liste des équipements", "Voir le fil (messages, photos,
   vocaux)"...) : defilement horizontal trouve et corrige, tache petits
   ecrans 07/09. */
.btn-secondary.btn-block { width: 100%; white-space: normal; }

:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 1px;
}

/* -- trame -- */

.app-shell {
  max-width: 640px;
  margin: 0 auto;
  /* 112px (releve de 96px, revision typo 07/09) : la nav basse peut passer
     sur deux lignes en dessous de 400px (plancher 15px, jamais de texte plus
     petit) -- marge de securite pour qu'elle ne recouvre jamais le contenu. */
  padding: var(--space-mobile) var(--space-mobile) calc(112px + env(safe-area-inset-bottom));
  min-height: 100vh;
}

@media (min-width: 900px) {
  .app-shell { padding-left: var(--space-desktop); padding-right: var(--space-desktop); padding-top: 56px; }
}

/* -- palier bureau (cadrage §1, correctif controle 1 07/09) --------------
   A partir de 1024px, un VRAI palier bureau sur TOUTES les pages (pas
   seulement le tableau de bord, cf. passe-2-anomalies.md point 2) : largeur
   de lecture plafonnee a 1100px, marges de 96px deja posees ci-dessus a
   900px. .app-shell--wide (tableau de bord, grille a deux colonnes) garde un
   peu plus de largeur. Quand la page porte une navigation laterale
   (public/js/ui.js#mountDesktopSidebar, body.has-sidebar), le contenu se
   centre dans l'espace restant apres les 240px de la barre laterale. */
@media (min-width: 1024px) {
  .app-shell { max-width: 1100px; padding-bottom: 56px; }
  .app-shell.app-shell--wide { max-width: 1180px; }

  body.has-sidebar { padding-left: 240px; }
  body.has-sidebar nav.tabbar#primary-nav { display: none; }
}

/* -- navigation laterale bureau (remplace nav.tabbar a partir de 1024px) -- */

.sidebar-nav {
  display: none;
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  width: 240px;
  flex-direction: column;
  gap: 2px;
  background: var(--bg);
  border-right: var(--rule-width) solid var(--rule);
  padding: var(--space-lg) var(--space-md);
  overflow-y: auto;
}

/* Seule survivance de Jost (cadrage §1 bis) : la signature de marque de la
   barre laterale bureau, nulle part ailleurs. Jost n'est fetche qu'au poids
   500 (scripts/fetch-fonts.mjs) : conforme au plancher de graisse malgre la
   police differente. */
.sidebar-brand {
  font-family: var(--font-brand);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  font-size: 16px;
  color: var(--ink);
  margin-bottom: var(--space-lg);
}

.sidebar-nav a {
  display: block;
  min-height: var(--tap-min);
  display: flex;
  align-items: center;
  padding: 11px 2px;
  font-family: var(--font-display);
  font-size: var(--size-label);
  font-weight: var(--weight-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
}
.sidebar-nav a:hover { color: var(--ink); }
.sidebar-nav a.active { color: var(--ink); border-bottom-color: var(--accent); }

@media (min-width: 1024px) {
  .sidebar-nav { display: flex; }
}

/* -- listes en grille de cartes a partir de 1024px (cadrage §1/§2.2,
   correctif controle 1 07/09) : equipements, fils, tickets -- les memes
   lignes (.thread-row, deja remplies par le JS existant, aucun changement de
   rendu) passent d'un empilement plein-largeur a une grille de 2-3 cartes
   aerees des que la place le permet. */
@media (min-width: 1024px) {
  #assets-list, #threads-list, #tickets-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: var(--space-sm) var(--space-md);
    /* Toutes les cartes d'une meme ligne ont la MEME hauteur (correctif du
       08/09, remarque de Mehdi : « toutes les boxes ne font pas la meme
       taille »). Sans ceci, une localisation qui tient sur deux lignes
       ("Local arrivee d'eau (adoucisseur / surpresseur)") allongeait sa seule
       carte et desalignait la ligne -- l'oeil lit alors un desordre la ou il
       n'y a qu'un libelle plus long. */
    grid-auto-rows: 1fr;
  }
  #assets-list .thread-row, #threads-list .thread-row, #tickets-list .thread-row,
  #assets-list .equip-mini {
    border: var(--rule-width) solid var(--rule);
    border-radius: var(--radius);
    padding: var(--space-sm) var(--space-md);
    align-items: flex-start;
    /* La carte occupe toute la hauteur de sa ligne : c'est ce qui rend le
       `grid-auto-rows: 1fr` ci-dessus visible. */
    height: 100%;
  }

  /* Tickets : les GROUPES de statut (.kanban-group, direct enfant de
     #tickets-list) sont d'une hauteur tres inegale (un groupe "Terminé" avec
     vingt tickets a cote d'un groupe "Sur site" vide) -- une grille figee en
     colonnes de meme largeur videait completement l'une des deux sur plus
     d'un ecran de hauteur (correctif controle 3 07/09). Colonnes CSS,
     equilibrees par hauteur (comportement par defaut de `columns`), plutot
     qu'une grille figee qui ignore la hauteur du contenu. */
  #tickets-list {
    columns: 2;
    column-gap: var(--space-lg);
  }
  #tickets-list .kanban-group {
    break-inside: avoid;
    -webkit-column-break-inside: avoid;
  }
}

/* -- fiche equipement en deux colonnes a partir de 1024px (cadrage §1/§2.2,
   correctif controle 1 07/09) : colonne gauche photo/etat/plaque/QR, colonne
   droite releves/echeances/tickets/historique. Voir public/equipement.html
   (.fiche-columns enveloppe deja les deux <div> existantes, aucun contenu
   deplace, seule la disposition change). */
@media (min-width: 1024px) {
  .fiche-columns {
    display: grid;
    grid-template-columns: minmax(280px, 380px) 1fr;
    gap: var(--space-lg);
    align-items: start;
  }

  /* La colonne gauche (photo/etat/plaque/QR) est structurellement plus courte
     que la droite (notice/releves/echeances/tickets/historique) : sans ceci,
     elle se figeait en haut de page en laissant plus de la moitie de la page
     vide en dessous (correctif controle 3 07/09, ~1360px sur 2420px mesures
     sur la fiche chaudiere n°2). Fixee (sticky) : elle suit le defilement de
     la colonne droite au lieu de flotter au-dessus d'un vide. */
  .fiche-col-left {
    position: sticky;
    top: 56px;
    align-self: start;
  }
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: 4px 0 28px;
  border-bottom: var(--rule-width) solid var(--rule);
  margin-bottom: var(--space-md);
}

/* .brand porte deja la taille "titre de page" (cadrage §1 bis) : c'est le
   seul titre visible sur la plupart des ecrans. Rien a redefinir ici. */
.topbar .brand { margin: 0; }

/* regroupe l'eventuelle action de la page + le lien Options (le "menu" du
   cadrage §1 : cinq entrees en bas, Options accessible d'ici). */
.topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

/* Sous 420px, la bascule de theme (3 boutons) ne tient jamais a cote
   d'OPTIONS sur une seule ligne (correctif controle 2 07/09 -- 481px sur un
   ecran de 390px, galerie.html) : elle passe sur sa propre ligne pleine
   largeur, OPTIONS reste aligne a droite du titre. */
@media (max-width: 420px) {
  .topbar-actions { flex-wrap: wrap; justify-content: flex-end; row-gap: 8px; }
  .topbar-actions #theme-switch-container { flex: 1 1 100%; order: 2; }
}

/* Sous 480px (elargi de 360px, correctif lisibilite 08/09 -- galerie.html
   mesuree a 494px de large sur un ecran de 390px : le selecteur de theme
   CLAIR/SOMBRE/SYSTEME a cote d'OPTIONS ne se repliait plus des que l'ecran
   depassait 360px), un titre de page long ("Équipements") a cote de DEUX
   actions ("Scanner" + "Options") deborde hors ecran (defilement horizontal
   trouve et corrige, tache petits ecrans 07/09 -- ni le titre ni les actions
   n'ont de quoi retrecir en dessous de leur propre texte). Le titre garde sa
   ligne pleine largeur, les actions passent dessous alignees a droite --
   jamais un titre tronque ni une action perdue. */
@media (max-width: 480px) {
  .topbar { flex-wrap: wrap; row-gap: 6px; }
  .topbar-actions { flex: 1 1 100%; justify-content: flex-end; }
}

/* en-tete de page : titre de page (h1, 30/34px 800) + sous-titre discret (cadrage §2). */
.page-header { margin-bottom: var(--space-md); }
.page-header h1 { margin-bottom: 0.15em; }
.page-header .subtitle {
  font-family: var(--font-body);
  font-style: italic;
  color: var(--muted);
  font-size: 1.05rem;
  margin: 0;
}

/* -- carte -- */

.card {
  background: var(--surface);
  border: var(--rule-width) solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--space-md);
  margin-bottom: var(--space-sm);
}

/* carte creme posee sur une photo, chevauchement assume (fiche equipement,
   cadrage §1 et §2.2). Utilisation : <div class="photo-frame"><img>
   <div class="card card-overlap">...</div></div> */
.photo-frame { position: relative; margin-bottom: calc(var(--space-lg) * -1); }
.photo-frame img, .photo-frame .photo-empty {
  display: block;
  width: 100%;
  /* 16:9 (correctif controle 2 07/09) : le 4:3 precedent laissait un aplat
     bien trop haut quand l'equipement n'a pas de photo -- la carte creme qui
     chevauche (.card-overlap) doit vraiment CHEVAUCHER, pas flotter au-dessus
     d'un vide. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius);
}
.photo-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  border: var(--rule-width) solid var(--rule);
  font-family: var(--font-display);
  font-weight: var(--weight-strong);
  color: var(--muted);
  font-size: var(--size-body);
  line-height: var(--line-title);
  text-align: center;
  padding: var(--space-sm);
}
.card-overlap {
  position: relative;
  margin: 0 var(--space-md);
  z-index: 1;
}

/* -- champs -- */

.field { margin-bottom: var(--space-md); }

/* etiquette AU-DESSUS du champ, toujours visible (cadrage §1 bis : "jamais
   un simple texte d'invite gris qui disparait a la saisie"). */
.field label {
  display: block;
  font-family: var(--font-display);
  font-size: var(--size-label);
  font-weight: var(--weight-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--muted);
  margin-bottom: var(--space-xs);
}

input[type='text'], input[type='email'], input[type='tel'], input[type='date'],
input[type='number'], input[type='password'], input[type='file'], textarea, select {
  width: 100%;
  font-family: var(--font-body);
  font-size: var(--size-body); /* 18px cadrage §1 bis -- jamais < 16px (zoom iOS) */
  font-weight: var(--weight-body);
  padding: 12px 14px;
  border: var(--rule-width) solid var(--rule);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--ink);
  min-height: var(--control-height);
}
/* releves numeriques : chiffres tabulaires (cadrage §1 — "donnees en IBM Plex Mono"). */
input[type='number'] { font-family: var(--font-mono); font-weight: var(--weight-data); }

/* select natif : meme habillage que les autres champs, jamais les coins
   arrondis / la fleche grise du systeme (cadrage §1 "ni arrondi ni ombre",
   correctif controle 2 07/09). Chevron dessine en CSS (deux traits), pas
   d'image externe. */
select {
  appearance: none;
  -webkit-appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 20px) calc(50% - 3px), calc(100% - 14px) calc(50% - 3px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: 34px;
}

textarea { min-height: 88px; resize: vertical; }

input:focus, select:focus, textarea:focus {
  border-color: var(--primary);
  outline: none;
}

/* message d'erreur de formulaire, sous le champ concerné (tache petits
   ecrans 07/09) : jamais la couleur seule -- toujours un texte explicite
   (deja le cas, cf. JS) EN PLUS de la couleur d'alerte, et jamais la taille
   15px du .hint generique -- c'est le message que l'oeil doit repérer en
   premier. Applique par JS via classList.toggle('field-error', estUneErreur)
   sur l'element de retour du formulaire (jamais un inline style.color, pour
   garder taille+couleur dans un seul endroit). */
.field-error {
  font-size: var(--size-data);
  font-weight: var(--weight-strong);
  color: var(--alert);
}

/* -- bouton principal -- */

button, .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--control-height);
  padding: 12px 22px;
  font-family: var(--font-display);
  font-size: var(--size-button);
  font-weight: var(--weight-button);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  border-radius: var(--radius);
  border: var(--rule-width) solid var(--primary);
  background: var(--primary);
  color: var(--primary-ink);
  cursor: pointer;
  box-shadow: var(--shadow);
}

button:hover, .btn:hover { opacity: 0.9; }
/* Contraste lisible plutot qu'une simple opacite reduite (correctif
   lisibilite 08/09) : "SUIVANT" en texte plein marine a 40% d'opacite
   devenait illisible sur la carte creme -- rien ne disait au technicien
   POURQUOI le bouton etait inactif. Memes jetons que le texte secondaire
   (deja AA sur --surface, cadrage §1 bis), jamais la couleur d'action a
   moitie effacee. */
button:disabled, .btn:disabled {
  opacity: 1;
  background: var(--muted-faint);
  color: var(--muted);
  border-color: var(--rule);
  cursor: not-allowed;
}
button:disabled:hover, .btn:disabled:hover { opacity: 1; }

.btn-block { width: 100%; }

/* Deux boutons pleine largeur cote a cote (ex. "Passer cette etape" +
   "Suivant", equipement-nouveau.html) : sous 480px ils se chevauchent, deux
   actions concurrentes illisibles a l'endroit precis ou l'utilisateur choisit
   entre photographier la plaque et saisir a la main (correctif lisibilite
   08/09). Empiles verticalement, pleine largeur, ecart 8px conserve. */
.wizard-actions { display: flex; gap: 8px; margin-top: var(--space-sm); }
@media (max-width: 480px) {
  .wizard-actions { flex-direction: column; }
}

/* -- badges d'etat -- */

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 6px 12px;
  border: var(--rule-width) solid var(--rule);
  border-radius: var(--radius);
  font-family: var(--font-display);
  font-size: var(--size-label);
  font-weight: var(--weight-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  background: var(--muted-faint);
  color: var(--muted);
  /* Jamais sur deux lignes dans un pavé (badge "NON ASSIGNÉ", "MANQUANTE"...) :
     le badge garde sa taille, c'est le titre a cote qui cede la place. */
  white-space: nowrap;
  flex-shrink: 0;
}

/* Filet (correctif lisibilite 08/09) : la source du badge "FAITE À hh:mm PAR
   ..." est corrigee (rondes.html ne compose plus ce texte dans le badge),
   mais un badge d'etat imprevu, long, sur un tres petit ecran garde ce
   garde-fou plutot que de forcer un defilement horizontal de toute la page. */
@media (max-width: 400px) {
  .status-pill { white-space: normal; }
}

.status-pill.ok, .status-pill.badge-normale { background: var(--accent-faint); color: var(--accent-ink); border-color: var(--accent); }
.status-pill.warn { background: var(--warn-faint); color: var(--warn-ink); border-color: var(--warn); }
.status-pill.danger, .status-pill.badge-critique { background: var(--alert-faint); color: var(--alert); border-color: var(--alert); }

/* badge d'etat calcule d'un equipement (cadrage §2.2) : sain / a surveiller /
   en derive / en panne / hors service — reserve pour la fiche equipement riche. */
.badge-sain { background: var(--accent-faint); color: var(--accent-ink); border-color: var(--accent); }
.badge-surveiller { background: var(--warn-faint); color: var(--warn-ink); border-color: var(--warn); }
.badge-derive { background: var(--alert-faint); color: var(--alert); border-color: var(--alert); }
.badge-panne { background: var(--alert); color: var(--alert-ink); border-color: var(--alert); }
.badge-hors-service { background: var(--muted-faint); color: var(--muted); border-color: var(--rule); }

/* -- listes a filets -- */

.thread-row > div:first-child, .priority-row > div:first-child { min-width: 0; }

.thread-row, .round-card {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-sm);
  padding: 16px 2px;
  border-bottom: var(--rule-width) solid var(--rule);
  text-decoration: none;
  color: inherit;
}
.thread-row:last-child, .round-card:last-child { border-bottom: none; }
.thread-row .title, .round-card .title {
  font-family: var(--font-body);
  font-weight: var(--weight-strong);
  font-size: var(--size-strong);
}
/* Aucun mot du titre ne doit pouvoir deborder de sa colonne (correctif
   lisibilite 08/09) : sur un mot long ("démarrage", "convecteur"), le titre
   passait sous le badge de priorite et devenait illisible avant meme la
   media query ci-dessous. */
.thread-row .title { overflow-wrap: anywhere; }

.round-card { display: block; }

/* Sous 480px, le badge (priorite, statut...) occupe une colonne fixe qui ne
   laisse presque plus rien au titre (correctif lisibilite 08/09 -- badge
   .status-pill en white-space:nowrap + flex-shrink:0 preneur d'~180px sur
   320px, titre compresse a une centaine de pixels, casse a un mot par ligne
   et deborde SOUS le badge, invisible car recouvert : ni debordement d'ecran
   ni texte sous 15px, donc invisible au diagnostic automatique). Meme patron
   que .equip-mini plus bas : le badge passe sur sa PROPRE ligne, le titre
   recupere toute la largeur. */
@media (max-width: 480px) {
  .thread-row {
    display: grid;
    grid-template-columns: 1fr;
    row-gap: 6px;
  }
  .thread-row .status-pill { justify-self: start; white-space: normal; }
  .thread-row .badges { align-items: flex-start; }
}

.reading-row, .checklist-item {
  padding: 14px 2px;
  border-bottom: var(--rule-width) solid var(--rule);
}
.reading-row:last-child, .checklist-item:last-child { border-bottom: none; }
/* Nom de la mesure TOUJOURS sur sa propre ligne, jamais en flux inline avec
   le champ (defaut navigateur d'un <label> nu) : sur un nom long ("Pression
   primaire (bar)", "Température de départ (°C)"), le champ de saisie se
   retrouvait colle au milieu d'une ligne de texte enroulee -- illisible a
   bout de bras (tache petits ecrans 07/09, capture mesuree a 320px). Le
   champ (inline-block) et son badge (inline) restent group es juste dessous. */
.reading-row label {
  display: block;
  font-weight: var(--weight-strong);
  margin-bottom: 6px;
}
/* bornes attendues ("Attendu entre X et Y") : 17px, jamais le 15px du .hint
   generique -- c'est la donnee de reference que le technicien compare a bout
   de bras au relevé (24px) juste au-dessus (tache petits ecrans 07/09). */
.reading-row .hint {
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: var(--size-data);
  font-weight: var(--weight-data);
  color: var(--muted);
}
/* releve saisi de ronde : lu a bout de bras, 24px 700 (cadrage §1 bis --
   c'est la donnee qu'on lit a bout de bras, jamais la plus petite). */
.reading-row input {
  max-width: 140px;
  font-size: var(--size-reading);
  font-weight: var(--weight-reading);
  min-height: 56px;
}

.checklist-item {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: var(--tap-min);
}
.checklist-item input[type='checkbox'] { width: 28px; height: 28px; flex-shrink: 0; accent-color: var(--primary); }

.kanban-group { margin-bottom: var(--space-lg); }
.kanban-group h3 {
  font-family: var(--font-display);
  font-size: var(--size-label);
  font-weight: var(--weight-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  margin: 0 0 var(--space-xs);
  padding-bottom: var(--space-xs);
  border-bottom: var(--rule-width) solid var(--rule);
}
/* Groupe "Clos" (cadrage §2.6 : les six statuts sont listés, clos inclus) :
   toujours en dernier, visuellement en retrait -- un ticket clos ne doit pas
   attirer autant l'oeil qu'un ticket qui demande encore une action. */
.kanban-group.clos { opacity: 0.6; }

/* Filtres de la liste des tickets (équipement/prestataire/priorité). */
.filter-row { display: flex; gap: var(--space-sm); flex-wrap: wrap; margin-bottom: var(--space-md); }
.filter-row .field { flex: 1 1 160px; margin-bottom: 0; }

/* Repliable en mobile seulement (correctif controle 3 07/09) : le bouton
   "Filtrer" n'existe qu'en dessous de 600px, les filtres restent toujours
   visibles en bureau (le bouton reste display:none, .collapsed n'est jamais
   applique par le JS a cette largeur). */
@media (max-width: 599px) {
  #filters-toggle { display: inline-flex !important; }
  .filter-row.collapsed { display: none; }
}

/* -- fil / messages -- */

.msg { margin-bottom: var(--space-sm); max-width: 88%; }
.msg.mine { margin-left: auto; text-align: right; }
.msg .bubble {
  display: inline-block;
  background: var(--surface);
  border: var(--rule-width) solid var(--rule);
  border-radius: var(--radius);
  padding: 10px 14px;
  text-align: left;
  font-family: var(--font-body);
  font-size: var(--size-strong);
}
.msg.mine .bubble { background: var(--accent-faint); border-color: var(--accent); }
/* Acteur (nom lisible, 700 -- "texte important"), horodatage seul en Plex
   Mono (cadrage §1 bis/§2.4) : .actor-name porte le nom, .mono (regle
   globale plus haut, 17px/600) porte la date -- jamais l'UUID technique
   affiche. Le .mono herite desormais de la regle globale (plus de taille
   reduite ici -- plancher 15px). */
.msg .actor { font-size: var(--size-label); font-weight: var(--weight-label); color: var(--muted); margin-bottom: 4px; }
.msg .actor .actor-name { font-family: var(--font-body); font-weight: var(--weight-strong); }

/* feuille de capture : photo / vocal / texte, fixee en bas d'ecran (cadrage §1 et §2.4). */
.composer {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  padding: var(--space-sm) var(--space-mobile) calc(var(--space-sm) + env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: var(--rule-width) solid var(--rule);
  display: flex;
  gap: 8px;
  max-width: 640px;
  margin: 0 auto;
}
@media (min-width: 900px) { .composer { max-width: 640px; } }
.composer input[type='text'] { flex: 1 1 auto; min-width: 0; }
.composer button { flex: 0 0 auto; padding: 12px 14px; }
/* PHOTO, VOCAL et ENVOYER : une seule action pleine marine par ecran
   (cadrage §1), et sur cet ecran c'est la carte de proposition IA qui la
   porte ("CREER LE TICKET") -- ENVOYER reste donc une action secondaire au
   meme traitement que PHOTO/VOCAL, jamais un deuxieme plein marine
   (correctif controle 2 07/09). */
.composer button[type='button'],
.composer button[type='submit'] {
  background: var(--bg);
  color: var(--ink);
  border-color: var(--rule);
}

/* Sous 420px, le champ texte ne doit JAMAIS ceder sa place aux trois boutons
   (PHOTO/VOCAL/ENVOYER) : le champ passe seul sur sa ligne, PHOTO+VOCAL se
   partagent la ligne suivante, ENVOYER reste seul en plein marine en bas
   (correctifs visuels 07/09 -- le champ se reduisait a ~40px, un seul "E"
   visible). */
@media (max-width: 420px) {
  .composer { flex-wrap: wrap; }
  .composer input[type='text'] { flex: 1 1 100%; order: -1; }
  .composer button[type='button'] { flex: 1 1 auto; padding: 10px 6px; font-size: var(--size-label); min-height: var(--tap-min); }
  .composer button[type='submit'] { flex: 1 1 100%; }
}

/* -- en-tete de fil : sujet (equipement / ticket / zone), cadrage §2.4 -- */

.thread-subject {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}
/* Cible tactile du lien vers la fiche equipement/ticket (correctif lisibilite
   08/09) : mesuree a 117x24px, tres en-dessous du plancher 48px (cadrage §1
   bis) alors que c'est une action frequente, souvent gantee. */
a.thread-subject {
  min-height: var(--tap-min);
  padding: 6px 0;
}
.thread-subject-thumb {
  width: 52px;
  height: 52px;
  flex-shrink: 0;
  object-fit: cover;
  border-radius: var(--radius-vignette);
  border: var(--rule-width) solid var(--rule);
  background: var(--surface);
}
/* Repli sans photo : le nom complet ne rentre pas lisiblement dans un carre
   de 52px (plancher 15px = "Ventilo-convecteur" sur 4 lignes tassees) --
   initiales seules (window.PalaceUI.initials, cadrage §1 bis), le nom
   complet reste toujours affiche a cote dans .thread-subject-body .name. */
.thread-subject-thumb.empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: var(--weight-page-title);
  font-size: 20px;
  color: var(--muted);
  text-align: center;
}
.thread-subject-body { min-width: 0; flex: 1 1 auto; }
/* Jamais d'ellipse sur le nom de l'equipement en tete de fil (correctif
   lisibilite 08/09) : "CTA n°1 (H…" au lieu de "CTA n°1 (Hall)" sur petit
   ecran -- meme principe que .equip-mini-body .title ci-dessus. */
.thread-subject-body .name {
  font-family: var(--font-body);
  font-weight: var(--weight-strong);
  font-size: var(--size-strong);
  overflow-wrap: anywhere;
}
.thread-subject-body .reason { font-size: var(--size-label); color: var(--muted); margin-top: 2px; }

/* -- galerie photo dans le fil (miniatures carrees, cadrage §2.4) -- */

.photo-thumb {
  width: 132px;
  height: 132px;
  object-fit: cover;
  border-radius: var(--radius-vignette);
  border: var(--rule-width) solid var(--rule);
  cursor: pointer;
  display: block;
}

/* Galerie de plusieurs miniatures hors du fil (fiche ticket : pièces
   jointes, cadrage §2.6) -- même vignette carrée, disposée en grille. */
.photo-gallery { display: flex; flex-wrap: wrap; gap: var(--space-sm); }
.photo-gallery .photo-thumb { cursor: default; }

.lightbox {
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 10, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  padding: var(--space-md);
}
/* Bug trouve en QA visuelle (07/09, session rondes) : sans cette regle, l'attribut
   `hidden` sur .lightbox (fil.html, ronde.html) est ecrase par le `display: flex`
   ci-dessus -- l'auteur gagne toujours sur la feuille de style par defaut du
   navigateur, MEME quand `hidden` est pose -- et l'ecran ENTIER reste couvert par
   l'overlay (inset: 0), bloquant tout clic derriere. Jamais un display sur .lightbox
   sans ce garde-fou. */
.lightbox[hidden] {
  display: none;
}
.lightbox img { max-width: 100%; max-height: 92vh; border-radius: var(--radius); }
.lightbox-close {
  position: absolute;
  top: var(--space-md);
  right: var(--space-md);
  background: none;
  border: 1px solid rgba(255, 255, 255, 0.6);
  color: #fff;
  min-height: var(--tap-min);
  padding: 6px 14px;
}

/* -- message vocal : lecteur + transcription en dessous, cadrage §2.4 -- */

.audio-msg audio { width: 100%; display: block; margin-bottom: 8px; }
.audio-transcript {
  font-family: var(--font-body);
  font-size: var(--size-strong);
  font-style: italic;
  color: var(--ink);
}
.audio-transcript.pending { color: var(--muted); font-style: italic; }
.audio-transcript.failed { color: var(--warn-ink); font-style: normal; }

/* -- carte de proposition IA (cadrage §2.4 point 2) : creme, distincte du
   fil de messages, jamais confondue avec une bulle -- filet marine a gauche,
   jamais d'ombre (charte §1). -- */

.proposal-card {
  background: var(--surface);
  border: var(--rule-width) solid var(--rule);
  border-left: 3px solid var(--primary);
  border-radius: var(--radius);
  padding: var(--space-md);
  margin: var(--space-md) 0;
}
.proposal-card h3 { margin-bottom: var(--space-sm); }
.proposal-field { margin-bottom: 10px; }
.proposal-field .label {
  display: block;
  font-family: var(--font-display);
  font-size: var(--size-label);
  font-weight: var(--weight-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--muted);
  margin-bottom: 2px;
}
.proposal-hypotheses { margin: 0; padding-left: 1.2em; }
.proposal-hypotheses li { margin-bottom: 4px; }
.proposal-sources { margin: 4px 0 0; padding: 0; list-style: none; }
.proposal-sources li {
  font-family: var(--font-mono);
  font-size: var(--size-small);
  font-weight: var(--weight-data);
  color: var(--muted);
  padding: 2px 0;
}
.proposal-sources li::before { content: '— '; }
.proposal-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: var(--space-sm); align-items: center; }
.proposal-actions .btn { flex: 1 1 auto; min-width: 160px; }
.proposal-card.state-refus, .proposal-card.state-erreur, .proposal-card.state-indisponible, .proposal-card.state-plafond {
  border-left-color: var(--warn);
}
.proposal-card.state-en-cours {
  border-left-color: var(--rule);
  color: var(--muted);
  font-style: italic;
}
.proposal-card .meta { margin-top: var(--space-sm); }

/* Libelle discret "moteur" (defaut 2, 07/09) : sous la carte de proposition
   IA ET sous la phrase de synthese du tableau de bord -- petites capitales,
   gris chaud, jamais d'emoji (cadrage §1). ".synthese-banner p" est desormais
   restreinte a .synthese-phrase (voir plus haut, correctif 08/09) : elle ne
   peut plus deborder sur .engine-label ni .meta, quelle que soit la
   specificite relative des deux regles. */
.engine-label {
  display: block;
  font-family: var(--font-display);
  font-style: normal;
  font-size: var(--size-label);
  font-weight: var(--weight-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--muted);
  margin-top: 4px;
}
.synthese-banner .engine-label { margin: 4px 0 0; }

/* -- filtres de la liste des fils (cadrage §2.4 point 4) -- */

.filter-pills { display: flex; gap: 6px; overflow-x: auto; margin-bottom: var(--space-md); padding-bottom: 2px; }
.filter-pills button {
  flex-shrink: 0;
  min-height: var(--tap-min);
  padding: 6px 16px;
  border-radius: var(--radius);
  border: var(--rule-width) solid var(--rule);
  background: var(--bg);
  color: var(--muted);
  font-family: var(--font-display);
  font-size: var(--size-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  cursor: pointer;
  box-shadow: none;
}
.filter-pills button.active { background: var(--primary); color: var(--primary-ink); border-color: var(--primary); }

/* Sous 420px, "Non lus" (le plus long des 4 filtres) ne doit jamais finir
   coupe au bord de l'ecran (correctif controle 2 07/09) : on resserre le
   remplissage, JAMAIS la taille de texte (plancher 15px, cadrage §1 bis). */
@media (max-width: 420px) {
  .filter-pills button { padding: 6px 10px; letter-spacing: 0.05em; }
}

.thread-row .last-message {
  font-size: var(--size-label);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-top: 2px;
}
.thread-row .title-line { display: flex; align-items: center; gap: 8px; min-width: 0; }
.unread-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--primary);
  flex-shrink: 0;
  display: inline-block;
}
.thread-row .badges { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }

/* -- feuille de capture vocale : enregistrement direct, compteur, limite 5 min -- */

.recording-bar {
  display: none;
  align-items: center;
  gap: 10px;
  padding: 10px var(--space-mobile);
  background: var(--alert-faint);
  color: var(--alert);
  font-family: var(--font-mono);
  font-size: var(--size-data);
  font-weight: var(--weight-data);
  max-width: 640px;
  margin: 0 auto;
}
.recording-bar.active { display: flex; }
.recording-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--alert);
  animation: pulse-rec 1.2s ease-in-out infinite;
}
@keyframes pulse-rec { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.recording-bar .stop-btn {
  margin-left: auto;
  min-height: var(--tap-min);
  padding: 6px 14px;
  background: var(--alert);
  border-color: var(--alert);
  color: var(--alert-ink);
}

/* -- tableau de bord intelligent (cadrage §2.3) -- */
/* Bureau : deux colonnes ; mobile : une. Aucun compteur geant -- des cartes
   creme a filets, chaque section ouvre l'ecran concerne. */

.synthese-banner {
  background: var(--surface);
  border: var(--rule-width) solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-lg) var(--space-md);
  margin-bottom: var(--space-lg);
}
/* Restreint au SEUL paragraphe de synthese (correctif lisibilite 08/09) :
   ".synthese-banner p" visait TOUT paragraphe enfant, dont .meta et
   .engine-label -- l'intention documentee sur .engine-label ("specificite
   volontairement plus forte pour ne jamais heriter de l'italique/grande
   taille") n'etait donc jamais atteinte : 0,1,1 (.synthese-banner p)
   l'emportait quand meme sur 0,1,0. Cible desormais uniquement le paragraphe
   de phrase, jamais .meta ni .engine-label. */
.synthese-banner p.synthese-phrase {
  font-family: var(--font-body);
  font-style: italic;
  font-size: var(--size-section-title);
  line-height: var(--line-body);
  color: var(--ink);
  margin: 0 0 8px;
}
.synthese-banner .meta { margin: 0; }

.dashboard-columns { display: grid; grid-template-columns: 1fr; gap: var(--space-md); align-items: start; }
@media (min-width: 900px) {
  .dashboard-columns { grid-template-columns: 1fr 1fr; }
}
.dashboard-column { display: flex; flex-direction: column; gap: var(--space-md); }

/* liste priorisee ("A traiter en premier") : filets, pas de compteur geant. */
.priority-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: 12px 2px;
  border-bottom: var(--rule-width) solid var(--rule);
  text-decoration: none;
  color: inherit;
}
.priority-row:last-child { border-bottom: none; }
.priority-row .title { font-family: var(--font-body); font-weight: var(--weight-strong); font-size: var(--size-strong); }

/* mini-carte equipement (photo + raison) dans "Equipements critiques".
   Grille plutot que flex (correctifs visuels 07/09) : la vignette a une
   largeur fixe, le corps prend le reste (jamais moins que 0, donc jamais
   pousse hors de son couloir), le badge d'etat reste en FIN de ligne, jamais
   pousse en dessous. */
.equip-mini {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-sm);
  padding: 12px 2px;
  border-bottom: var(--rule-width) solid var(--rule);
  text-decoration: none;
  color: inherit;
}
.equip-mini:last-child { border-bottom: none; }
.equip-mini-thumb {
  width: 52px;
  height: 52px;
  object-fit: cover;
  border-radius: var(--radius-vignette);
  border: var(--rule-width) solid var(--rule);
  flex-shrink: 0;
  background: var(--surface);
}
/* Repli sans photo : initiales seules (voir .thread-subject-thumb.empty
   plus haut, meme raison -- plancher 15px et carre de 52px incompatibles
   avec un nom complet). */
.equip-mini-thumb.empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: var(--weight-page-title);
  font-size: 20px;
  color: var(--muted);
  text-align: center;
}
.equip-mini-body { min-width: 0; }
/* Jamais d'ellipse sur le nom d'un equipement, a AUCUNE largeur (correctif
   lisibilite 08/09) : "Ventilo-convecteur n°1 (aile …" ne permettait plus de
   distinguer l'aile Est de l'aile Ouest sur le poste bureau (1440px), alors
   qu'il restait du blanc a droite du badge. Un nom sur deux lignes vaut
   toujours mieux qu'un nom tronque -- deja le principe retenu sous 400px
   (media query plus bas), etendu ici a toutes les largeurs. */
.equip-mini-body .title {
  font-family: var(--font-body);
  font-weight: var(--weight-strong);
  font-size: var(--size-strong);
  white-space: normal;
  overflow-wrap: anywhere;
}
.equip-mini-body .reason { font-size: var(--size-label); color: var(--muted); margin-top: 2px; }
.equip-mini .status-pill { flex-shrink: 0; }

/* Sous 400px, la colonne "auto" du badge (largeur fixe, "HORS SERVICE" ou
   "EN DÉRIVE") ne laisse presque plus rien a `minmax(0, 1fr)` : le nom
   tombait a une seule lettre suivie de l'ellipse, la raison s'empilait mot
   par mot sur une largeur ridicule (defilement/troncature mesures a 320px
   avec des donnees reelles, tache petits ecrans 07/09 -- exactement le
   "aucune troncature de mot important" a eviter). Le badge passe sur sa
   PROPRE ligne, pleine largeur du corps restant : le nom et la raison
   recuperent toute la largeur disponible au lieu de disputer une poignee de
   pixels a un badge qui ne peut pas retrecir. */
@media (max-width: 400px) {
  .equip-mini {
    grid-template-columns: 56px 1fr;
    grid-template-areas: "thumb body" "thumb badge";
    row-gap: 6px;
  }
  .equip-mini-thumb { grid-area: thumb; }
  .equip-mini-body { grid-area: body; }
  .equip-mini-body .title { white-space: normal; overflow: visible; text-overflow: clip; }
  .equip-mini .status-pill { grid-area: badge; justify-self: start; white-space: normal; }
}

.dashboard-subgroup { margin-bottom: var(--space-sm); }
.dashboard-subgroup:last-child { margin-bottom: 0; }
.dashboard-subgroup h3 { margin-bottom: 4px; }

/* -- etat vide : aplat creme, nom en capitales, jamais d'icone generique -- */

/* Casse de phrase, jamais des capitales espacees sur une phrase entiere
   (correctif lisibilite 08/09) : les capitales avec interlettrage sont
   faites pour des etiquettes de deux ou trois mots (cadrage §1 bis) -- sur
   une phrase entiere ("Aucune anomalie photographiée.") elles suppriment la
   silhouette des mots et ralentissent la lecture, precisement pour un
   lecteur presbyte. Les capitales restent reservees aux etiquettes de
   section (.caps, h3.caps). */
.empty-state {
  text-align: center;
  background: var(--surface);
  border: var(--rule-width) solid var(--rule);
  border-radius: var(--radius);
  color: var(--muted);
  padding: 48px var(--space-md);
  font-family: var(--font-body);
  font-weight: var(--weight-body);
  font-size: var(--size-body);
}

.skeleton { color: var(--muted); font-style: italic; font-family: var(--font-body); }

/* -- bannieres -- */

.offline-banner, .pending-banner {
  display: none;
  border: var(--rule-width) solid var(--warn);
  background: var(--warn-faint);
  color: var(--warn-ink);
  padding: 10px 14px;
  border-radius: var(--radius);
  font-size: var(--size-label);
  font-weight: var(--weight-label);
  margin-bottom: var(--space-sm);
}
.pending-banner { border-color: var(--primary); background: var(--accent-faint); color: var(--ink); }
.offline-banner.visible, .pending-banner.visible { display: block; }

.progress-bar {
  height: 4px;
  border-radius: var(--radius);
  background: var(--rule);
  overflow: hidden;
  margin: 10px 0;
}
.progress-bar > div {
  height: 100%;
  background: var(--primary);
  transition: width 0.2s ease;
}

.form-row { display: flex; gap: var(--space-sm); align-items: flex-end; }
.form-row .field { flex: 1; margin-bottom: 0; }

/* Sous 600px, deux champs cote a cote finissent tronques (correctif controle
   3 07/09 : selecteurs "Technicien in ▾", "Ex : Froid Servic" illisibles sur
   390px) : une seule colonne, jamais un choix ou une saisie a l'aveugle sur
   telephone. */
@media (max-width: 600px) {
  .form-row { flex-direction: column; align-items: stretch; }
  .form-row .field { margin-bottom: var(--space-md); }
  .form-row .field:last-child { margin-bottom: 0; }
}

.scan-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius);
  overflow: hidden;
  background: #000;
  margin-bottom: var(--space-sm);
  border: var(--rule-width) solid var(--rule);
}
.scan-frame video { width: 100%; height: 100%; object-fit: cover; display: block; }

/* -- navigation basse : cinq entrees, icone + libelle, separateurs filets --
   Icone SVG en ligne au-dessus du libelle (tache petits ecrans 07/09) :
   public technicien peu a l'aise, gante, presbyte -- l'icone porte la
   reconnaissance immediate, le libelle reste TOUJOURS entier a cote (jamais
   d'icone seule). `stroke="currentColor"` dans le SVG recupere directement
   la couleur du lien (--muted / --primary actif), aucune regle de couleur a
   dupliquer ici. */

nav.tabbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  display: flex;
  background: var(--bg);
  border-top: var(--rule-width) solid var(--rule);
  padding-bottom: env(safe-area-inset-bottom);
}

nav.tabbar a {
  flex: 1;
  text-align: center;
  padding: 8px 2px 8px;
  font-family: var(--font-display);
  font-size: var(--size-label);
  font-weight: var(--weight-label);
  line-height: 1.15;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--muted);
  text-decoration: none;
  min-height: var(--tap-min);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  border-left: var(--rule-width) solid var(--rule);
}
nav.tabbar a:first-child { border-left: none; }

.nav-icon { width: 22px; height: 22px; flex-shrink: 0; }

/* Sous 400px, "EQUIPEMENTS" touche les filets de sa case (cinq entrees dans
   un cinquieme d'ecran) : abreger plutot que cesurer (correctif lisibilite
   08/09 -- `hyphens: auto` cassait "ÉQUIPEMENTS"/"TICKETS"/"BILAN" en
   fragments de syllabe sur 2-3 lignes, illisibles d'un coup d'oeil en
   marchant pour un public peu a l'aise avec le telephone). Le libelle
   HTML porte deja la forme courte pour ce nav (voir public/*.html,
   `<a data-label="…">` garde le nom complet pour la barre laterale bureau
   via mountDesktopSidebar) ; on resserre seulement l'interlettrage, JAMAIS
   la taille de texte (plancher 15px, cadrage §1 bis), pour que "TICKETS"
   tienne aussi sur une seule ligne. */
@media (max-width: 400px) {
  nav.tabbar a { letter-spacing: 0.01em; padding-left: 1px; padding-right: 1px; }
  nav.tabbar .nav-label { letter-spacing: 0.04em; white-space: nowrap; }
}
nav.tabbar a.active { color: var(--primary); }

/* Ecrans de detail (fil, fiche equipement, ronde) : la navigation basse
   mobile n'a pas lieu d'exister (composeur/actions deja fixes en bas), mais
   la barre laterale bureau DOIT survivre a 1024px+ (correctif controle 2
   07/09 -- ces ecrans perdaient toute navigation sur poste fixe, seul un lien
   "<- Fils" abrege). #primary-nav y est donc pose masque (attribut `hidden`,
   jamais affiche sur mobile) : mountDesktopSidebar() le lit quand meme pour
   construire .sidebar-nav. Meme piege de specificite que .lightbox[hidden]
   plus haut -- `nav.tabbar{display:flex}` gagnerait sinon sur `[hidden]`. */
nav.tabbar[hidden] { display: none; }

/* Composeur du fil : reste centre sur la LARGEUR RESTANTE une fois la barre
   laterale posee (correctif controle 2 07/09), jamais plaque sur toute la
   largeur de l'ecran derriere elle. */
@media (min-width: 1024px) {
  body.has-sidebar .composer { left: 240px; }
}

/* Photo du point de ronde / fiche equipement : ne doit jamais occuper 700px
   de haut au milieu d'une page bureau vide (correctif controle 2 07/09). */
@media (min-width: 1024px) {
  .photo-frame img, .photo-frame .photo-empty { max-height: 420px; }
}

/* -- bascule de theme (Options) -- */

.theme-switch {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  border: var(--rule-width) solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
  width: fit-content;
  max-width: 100%;
}
.theme-switch button {
  border: none;
  border-radius: 0;
  background: var(--bg);
  color: var(--muted);
  min-height: var(--tap-min);
  padding: 8px 18px;
}
.theme-switch button + button { border-left: var(--rule-width) solid var(--rule); }
.theme-switch button.active { background: var(--primary); color: var(--primary-ink); }
.theme-switch button:hover { opacity: 1; }

/* Sous 420px, "fit-content" additionne les trois boutons ("Clair"/"Sombre"/
   "Système") cote a cote sans jamais retrecir : deborde hors ecran des que
   sa carte fait moins de ~300px de large (defilement horizontal trouve sur
   public/parametres.html a 320px, tache petits ecrans 07/09). Doit venir
   APRES la regle de base ci-dessus (meme specificite : sinon l'ordre du
   fichier la fait perdre, piege trouve en verifiant le rendu reel). */
@media (max-width: 420px) {
  .theme-switch { width: 100%; }
  .theme-switch button { flex: 1 1 0; }
}

/* -- compatibilite : anciens jetons --pt-* references en ligne dans le HTML
   genere cote client (fil.html, ticket.html, ronde.html...). On les alias
   plutot que de reecrire chaque gabarit de message. -- */
:root {
  --pt-ink-muted: var(--muted);
  --pt-danger: var(--alert);
  --pt-ok: var(--accent);
}

/* -- galerie interne (public/galerie.html) -- */

.swatch { display: flex; align-items: center; gap: var(--space-sm); margin-bottom: var(--space-xs); }
.swatch .chip { width: 28px; height: 28px; border-radius: var(--radius); border: var(--rule-width) solid var(--rule); flex-shrink: 0; }
.gallery-section { margin-bottom: var(--space-lg); padding-bottom: var(--space-lg); border-bottom: var(--rule-width) solid var(--rule); }
.gallery-section:last-child { border-bottom: none; }
.gallery-row { display: flex; flex-wrap: wrap; gap: var(--space-sm); align-items: center; margin-bottom: var(--space-sm); }

/* -- ronde guidee (public/ronde.html, cadrage §2.5) -- */

/* Aplat de repli (point sans photo) : une BANDE courte, jamais le ratio 16:9
   plein-largeur de .photo-frame (qui n'affichait qu'un nom en capitales dans
   un grand rectangle vide, 360px mobile / 430px bureau -- correctif controle
   3 07/09). Seul ce point d'entree est concerne : une photo REELLE garde son
   ratio 16:9 habituel (.photo-frame img, inchange), donc la hauteur de bloc
   ne varie que pour le repli, jamais pour un point avec photo. */
.photo-frame .photo-empty.round-point-empty {
  aspect-ratio: auto;
  height: 140px;
}
@media (min-width: 1024px) {
  .photo-frame .photo-empty.round-point-empty { height: 200px; }
}

/* La carte qui chevauche (nom + localisation) doit se DISTINGUER de l'aplat
   de repli qu'elle recouvre -- meme jeton --surface pour les deux sinon
   (cadrage §1 : "carte creme qui chevauche la photo"), le chevauchement
   devient invisible et se lit comme une mise en page cassee (correctif
   controle 3 07/09). Fond blanc pur sur l'aplat creme -- jamais applique
   quand une VRAIE photo est dessous (deja suffisamment contrastee). */
.photo-empty + .card-overlap,
.photo-frame:has(.photo-empty) + .card-overlap {
  background: var(--bg);
}

/* Une fois le point confirme, #confirm-card (qui portait la marge de
   degagement sous la carte-nom qui chevauche la photo) disparait et
   #step-form-card (checklist/releves) devient le voisin direct de cette
   carte-nom -- sans cette marge il s'y colle, "Checklist" partiellement
   recouvert (defilement mesure a 320px, tache petits ecrans 07/09). Sans
   effet quand `hidden` (display:none). */
#step-form-card { margin-top: var(--space-lg); }

/* "point 3 sur 8" : petite capitale discrete au-dessus de la carte du point. */
.round-position {
  display: block;
  font-family: var(--font-display);
  font-weight: var(--weight-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  font-size: var(--size-label);
  color: var(--muted);
  text-align: center;
  margin-bottom: var(--space-xs);
}

/* releve chiffre hors bornes pendant la SAISIE (avant meme d'envoyer) : ocre
   = ecart leger, brique = ecart franc (public/js/reading-severity.js). Le
   badge "hors bornes"/"tres hors bornes" est pose a cote, jamais seulement
   la couleur (accessibilite -- ne jamais coder une information par la
   couleur seule). */
input.severity-warn { border-color: var(--warn); background: var(--warn-faint); color: var(--warn-ink); }
input.severity-alert { border-color: var(--alert); background: var(--alert-faint); color: var(--alert); }
.reading-row .severity-badge {
  display: inline-block;
  margin-left: 8px;
  font-family: var(--font-display);
  font-weight: var(--weight-label);
  font-size: var(--size-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
}
.reading-row .severity-badge.warn { color: var(--warn-ink); }
.reading-row .severity-badge.alert { color: var(--alert); }

/* bouton "Passer ce point" : discret, jamais la meme force visuelle que
   "Point suivant" (une seule action pleine marine par ecran, cadrage §1). */
.btn-skip {
  background: transparent;
  border-color: var(--rule);
  color: var(--muted);
}

/* resume de fin de ronde (et detail d'une ronde passee, meme gabarit) */
.summary-group { margin-bottom: var(--space-lg); }
.summary-group h3 {
  font-family: var(--font-display);
  font-weight: var(--weight-label);
  font-size: var(--size-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  margin-bottom: var(--space-xs);
}
.summary-row {
  padding: 12px 2px;
  border-bottom: var(--rule-width) solid var(--rule);
}
.summary-row:last-child { border-bottom: none; }
.summary-row .title { font-family: var(--font-body); font-weight: var(--weight-strong); font-size: var(--size-strong); }
.summary-row .skip-reason { font-size: var(--size-label); color: var(--warn-ink); margin-top: 2px; }
.summary-photos { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin-top: 6px; }

/* liste des rondes (public/rondes.html) : historique des 7 derniers jours */
.history-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-sm);
  padding: 12px 2px;
  border-bottom: var(--rule-width) solid var(--rule);
}
.history-row:last-child { border-bottom: none; }
.history-row .meta { text-align: right; }

/* -- inventaire par plaque, planche d'etiquettes, boite de depot (cadrage §2.7) -- */

/* parcours en etapes (equipement-nouveau.html) : indicateur de progression
   sobre, sans icone -- capitales espacees comme le reste du systeme (§1). */
.wizard-steps {
  display: flex;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
  font-family: var(--font-display);
  font-weight: var(--weight-label);
  font-size: var(--size-label);
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--muted);
}
.wizard-steps .step { padding-bottom: 6px; border-bottom: 2px solid transparent; }
.wizard-steps .step.active { color: var(--ink); border-bottom-color: var(--primary); }
.wizard-steps .step.done { color: var(--accent-ink); }

/* champ pre-rempli par l'IA mais INCERTAIN (cadrage §2.7 : "marques en ocre et
   modifiables") -- meme jeton que le releve hors bornes (input.severity-warn),
   jamais une couleur nouvelle. */
.field-uncertain input, .field-uncertain select {
  border-color: var(--warn);
  background: var(--warn-faint);
  color: var(--warn-ink);
}
.field-uncertain label::after { content: ' (à confirmer)'; text-transform: none; letter-spacing: 0; color: var(--warn-ink); }

/* boite de depot : zone de glisser-deposer (bureau) + selection de fichiers
   (mobile), meme zone pour les deux -- pas de composant distinct par plateforme. */
.dropzone {
  border: 2px dashed var(--rule);
  border-radius: var(--radius);
  padding: 28px 16px;
  text-align: center;
  color: var(--muted);
  cursor: pointer;
}
.dropzone.dragover { border-color: var(--primary); background: var(--surface); color: var(--ink); }

.proposal-row {
  padding: 10px 2px;
  border-bottom: var(--rule-width) solid var(--rule);
}
.proposal-row:last-child { border-bottom: none; }
.proposal-row .proposal-title { font-family: var(--font-body); font-weight: var(--weight-strong); font-size: var(--size-strong); }
.proposal-row .proposal-meta { font-size: var(--size-label); color: var(--muted); margin: 2px 0 6px; }
.proposal-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.proposal-actions button { padding: 6px 12px; font-size: var(--size-label); min-height: var(--tap-min); }
.proposal-actions .btn-ignore { background: var(--bg); color: var(--muted); border-color: var(--rule); }

/* -- selecteur de fichier stylise (cadrage §1, correctif controle 3 07/09) --
   input[type=file] natif MASQUE (jamais "Choose File"/"No file chosen" en
   anglais, bouton systeme blanc casse en sombre) : un bouton .btn-secondary
   pose sur un <label for>, le nom retenu affiche en dessous en IBM Plex
   Mono. Un seul patron pour prestataire.html/documents.html/
   equipement-nouveau.html partout ou un input[type=file] existe. */
.file-picker { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.file-picker input[type='file'] {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.file-picker-name {
  font-family: var(--font-mono);
  font-size: var(--size-small);
  font-weight: var(--weight-data);
  color: var(--muted);
}

/* liste a cocher (selection d'equipements pour une planche d'etiquettes) */
.check-list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 2px;
  min-height: var(--tap-min);
  border-bottom: var(--rule-width) solid var(--rule);
}
.check-list-row:last-child { border-bottom: none; }
.check-list-row input[type='checkbox'] { width: 26px; height: 26px; flex-shrink: 0; accent-color: var(--primary); }

/* -- magasin (LOT-PALACE-PIECE) -- */

/* La quantite en stock est la donnee qu'on lit a bout de bras, debout devant
   une etagere, souvent avec des gants : meme traitement que le releve de
   ronde (24px, graisse de lecture, chiffres tabulaires du Plex Mono), jamais
   la taille du texte secondaire. */
.stock-quantite {
  font-family: var(--font-mono);
  font-size: var(--size-reading);
  font-weight: var(--weight-reading);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  margin-top: 6px;
}

/* Deux gestes cote a cote (« posée » / « reçue ») : cibles 48px et 8px
   d'ecart, conformement au cadrage §1 bis -- on les manipule avec des gants. */
.ligne-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}
.ligne-actions .btn-secondary {
  min-height: 48px;
  flex: 1 1 auto;
}

/* Carte d'alerte : la bordure et le fond portent l'information, JAMAIS la
   couleur seule -- le titre dit ce qui se passe (regle des petits ecrans). */
.card-alert {
  border-color: var(--alert);
  background: var(--alert-faint);
}
.card-alert .title { color: var(--alert); font-weight: var(--weight-strong); }

/* Piste d'approvisionnement releguee parce qu'elle vient d'un domaine ecarte
   par l'etablissement : elle reste VISIBLE et dit pourquoi elle est en bas
   [P14]. Jamais masquee, jamais supprimee en silence. */
.piste-releguee { opacity: 0.85; }
.piste-releguee .meta { color: var(--muted); }

/* Mention de moteur en MODE ESSAI : elle doit sauter aux yeux, pas se fondre
   dans le gris des autres mentions. Un diagnostic factice pris pour un vrai
   serait bien pire que la depense qu'il evite -- et c'est le meme ecran qui
   sert a montrer le produit au directeur technique. */
.engine-label--essai {
  color: var(--alert);
  font-weight: var(--weight-strong);
  background: var(--alert-faint);
  border: var(--rule-width) solid var(--alert);
  border-radius: var(--radius);
  padding: 8px 12px;
  letter-spacing: 0.04em;
}
