/* ============================================================
   PhotoFiche — feuille de style unique
   Langage visuel iOS 26 « Liquid Glass ».

   Principe directeur : le chrome qui flotte (barre supérieure, boutons,
   feuilles modales) est translucide et capte l'arrière-plan ; les surfaces
   qui portent de la donnée dense (tableau, champs de saisie) restent
   opaques. C'est ce contraste qui donne la profondeur — pas le flou partout.
   ============================================================ */

/* ---------- Réinitialisation ---------- */

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

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

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; border: 0; background: none; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 8px; }
::selection { background: var(--accent-soft); color: var(--text); }

/* ---------- Jetons de couleur — clair ---------- */

:root {
  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI Variable Text",
          "Segoe UI", Inter, system-ui, "Helvetica Neue", sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Mono", "JetBrains Mono", Menlo, monospace;

  --bg: #eef1f5;
  /* Fond volontairement sobre : un souvenir de couleur, pas un dégradé —
     l'app affiche désormais des tableaux denses (clients, commandes…) que
     le fond d'origine, plus décoratif, distrayait. Les nappes sont passées
     à l'ardoise en même temps que le reste : une nappe bleue sous une
     interface ardoise se voyait comme une tache. */
  --mesh-1: rgba(51, 65, 85, 0.05);
  --mesh-2: rgba(71, 85, 105, 0.04);
  --mesh-3: rgba(100, 116, 139, 0.03);

  /* Verre : le fond est très clair et très transparent, la lisibilité venant
     du flou et non de l'opacité. */
  --glass: rgba(255, 255, 255, 0.62);
  --glass-strong: rgba(255, 255, 255, 0.80);
  --glass-border: rgba(255, 255, 255, 0.70);
  --glass-edge: rgba(15, 23, 42, 0.08);
  --glass-shine: rgba(255, 255, 255, 0.95);
  --glass-shadow: 0 12px 40px -12px rgba(15, 23, 42, 0.28);
  /* Les deux reflets du matériau : le bord supérieur d'une carte, et le fond
     d'une zone de dépôt. En sombre, les deux valent `none`. */
  --sheen: linear-gradient(180deg, rgba(255, 255, 255, 0.22), transparent);
  --sheen-soft: linear-gradient(180deg, rgba(255, 255, 255, 0.14), transparent);

  /* La tuile de marque du rail. Elle portait un dégradé accent → violet,
     hérité de l'identité bleue : sur une palette monochrome, c'était le
     dernier endroit où une seconde teinte subsistait. */
  --brand-fill: var(--accent);
  --brand-shadow: 0 4px 14px -6px rgba(15, 23, 42, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.28);
  --brand-ink: #ffffff;

  /* Surfaces opaques : tableau, champs, cartes de contenu. */
  --surface: rgba(255, 255, 255, 0.96);
  --surface-2: #f5f7fa;
  --surface-3: #e7ebf1;
  --hairline: rgba(51, 65, 85, 0.13);

  --text: #0f172a;
  --text-2: #475569;
  --text-3: #94a3b8;

  /* L'accent EST l'ardoise : l'application n'a plus de couleur d'action.
     Ce qui distingue un bouton principal, c'est donc son aplat et son
     contraste, jamais sa teinte — voir .btn-primary. */
  --accent: #334155;
  --accent-hover: #1e293b;
  --accent-soft: rgba(51, 65, 85, 0.10);
  --accent-text: #ffffff;

  /* ---- Les trois niveaux d'état ----
     Le vert valide, et deux rouges se partagent le reste. Deux, et non un
     seul : « --warn » n'est pas que la pastille « à compléter », c'est
     aussi le cran du milieu de deux échelles vert → orange → rouge (score
     de correspondance web, contrôle logistique). Un rouge unique aurait
     fait disparaître ce cran, et une fiche à 80 % de correspondance aurait
     eu la même couleur qu'une fiche à 40 %. */
  --green: #0f8a4a;
  --green-soft: rgba(15, 138, 74, 0.13);
  --warn: #c94a34;
  --warn-soft: rgba(201, 74, 52, 0.13);
  --red: #911423;
  --red-soft: rgba(145, 20, 35, 0.12);
  --violet: #6d47c8;
  --violet-soft: rgba(109, 71, 200, 0.13);

  /* Ancien nom du cran d'attention, gardé tant que app.js écrit
     « pill-orange » : même valeur, un seul endroit à changer le jour où
     l'on renommera la classe. */
  --orange: var(--warn);
  --orange-soft: var(--warn-soft);

  --radius-xl: 30px;
  --radius-lg: 22px;
  --radius-md: 15px;
  --radius-sm: 10px;

  /* Départ net, arrivée douce, sans l'effet de rebond qui alourdissait
     les contrôles récurrents. */
  --spring: cubic-bezier(0.2, 0.75, 0.25, 1);
  --blur: saturate(180%) blur(28px);
  --blur-heavy: saturate(200%) blur(44px);
  --blur-panel: blur(14px);
  --blur-pop: blur(20px) saturate(1.7);
  --blur-head: blur(10px);
  --grain: 0.35;
}

/* ---------- Jetons de couleur — sombre ---------- */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* ---- Le sombre est plat ----
       Plus de nappes, plus de reflet, plus de flou : des aplats d'ardoise
       séparés par des filets. Trois raisons qui vont dans le même sens —
       un tableau dense se lit mieux sur un fond qui ne bouge pas ; le verre
       dépoli n'a plus rien à échantillonner une fois les nappes éteintes, il
       ne coûtait donc que du processeur ; et le monochrome ne supporte pas
       les effets, qui y ressemblent vite à de la saleté. */
    --bg: #0b0e13;
    --mesh-1: transparent;
    --mesh-2: transparent;
    --mesh-3: transparent;

    /* Opaques, et non plus translucides : --blur vaut `none` juste en
       dessous, un fond semi-transparent laisserait donc voir le contenu qui
       défile dessous au lieu de le flouter. */
    --glass: #171c24;
    --glass-strong: #1b212a;
    --glass-border: rgba(255, 255, 255, 0.09);
    --glass-edge: transparent;
    --glass-shine: transparent;
    /* Une ombre nulle, pas `none` : ce jeton est toujours employé dans une
       liste (« var(--glass-shadow), inset … »), où `none` rendrait toute la
       déclaration invalide et emporterait aussi les inset. */
    --glass-shadow: 0 0 0 transparent;
    --sheen: none;
    --sheen-soft: none;

    /* En sombre, la tuile ne s'allume pas : l'accent y est clair, une tuile
       pleine ferait un bloc lumineux dans le rail. Elle devient une surface,
       et c'est la marque qui porte l'accent. */
    --brand-fill: var(--surface-3);
    --brand-shadow: none;
    --brand-ink: var(--accent);

    --surface: #1b212a;
    --surface-2: #12161d;
    --surface-3: #1e242e;
    --hairline: rgba(255, 255, 255, 0.10);

    --text: #e9eef5;
    --text-2: #9aa5b5;
    --text-3: #6b7686;

    /* L'ardoise de nuit doit être plus CLAIRE que --text-2, sans quoi un
       lien ne se distinguerait plus d'un texte secondaire : monochrome, la
       teinte ne dit plus rien, seule la valeur le fait. */
    --accent: #c3d2e6;
    --accent-hover: #dde6f2;
    --accent-soft: rgba(195, 210, 230, 0.16);
    --accent-text: #0f172a;

    --green: #35d08a;
    --green-soft: rgba(53, 208, 138, 0.18);
    --warn: #f08a72;
    --warn-soft: rgba(240, 138, 114, 0.18);
    --red: #ff6a6a;
    --red-soft: rgba(255, 106, 106, 0.18);
    --violet: #a78bfa;
    --violet-soft: rgba(167, 139, 250, 0.18);

    --orange: var(--warn);
    --orange-soft: var(--warn-soft);

    /* L'intitulé d'une barre du Gantt tire moins vers sa teinte : le violet
       de nuit, à 80 %, ne donnait que 4,4:1 sur sa barre — sous le seuil
       d'un texte de 11,5 px. À 65 %, 5:1, et la teinte se lit encore. */
    --bar-ink: 65%;

    /* Rien à flouter derrière un aplat : le flou ne coûtait plus que du
       processeur, et sur mobile il se voit au défilement. */
    --blur: none;
    --blur-heavy: none;
    --blur-panel: none;
    --blur-pop: none;
    --blur-head: none;

    /* Le grain ne servait qu'à casser le banding des nappes ; sans nappes,
       il ne reste qu'une salissure sur l'aplat. */
    --grain: 0;
  }
}

/* Même aplat que ci-dessus, pour qui choisit le sombre à la main plutôt
   que de suivre le système. Les deux blocs doivent rester jumeaux : toute
   valeur ajoutée en haut se recopie ici, sinon le thème forcé garderait
   l'ancienne matière. */
:root[data-theme="dark"] {
  --bg: #0b0e13;
  --mesh-1: transparent;
  --mesh-2: transparent;
  --mesh-3: transparent;

  --glass: #171c24;
  --glass-strong: #1b212a;
  --glass-border: rgba(255, 255, 255, 0.09);
  --glass-edge: transparent;
  --glass-shine: transparent;
  --glass-shadow: 0 0 0 transparent;
  --sheen: none;
  --sheen-soft: none;

  --brand-fill: var(--surface-3);
  --brand-shadow: none;
  --brand-ink: var(--accent);

  --surface: #1b212a;
  --surface-2: #12161d;
  --surface-3: #1e242e;
  --hairline: rgba(255, 255, 255, 0.10);

  --text: #e9eef5;
  --text-2: #9aa5b5;
  --text-3: #6b7686;

  --accent: #c3d2e6;
  --accent-hover: #dde6f2;
  --accent-soft: rgba(195, 210, 230, 0.16);
  --accent-text: #0f172a;

  --green: #35d08a;
  --green-soft: rgba(53, 208, 138, 0.18);
  --warn: #f08a72;
  --warn-soft: rgba(240, 138, 114, 0.18);
  --red: #ff6a6a;
  --red-soft: rgba(255, 106, 106, 0.18);
  --violet: #a78bfa;
  --violet-soft: rgba(167, 139, 250, 0.18);

  --orange: var(--warn);
  --orange-soft: var(--warn-soft);

  --bar-ink: 65%;

  --blur: none;
  --blur-heavy: none;
  --blur-panel: none;
  --blur-pop: none;
  --blur-head: none;

  /* Le grain ne servait qu'à casser le banding des nappes ; sans nappes,
     il ne reste qu'une salissure sur l'aplat. */
  --grain: 0;
}

/* ============================================================
   Arrière-plan : nappes de couleur fixes que le verre échantillonne.
   ============================================================ */

.mesh {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(58% 44% at 12% 4%,  var(--mesh-1) 0%, transparent 62%),
    radial-gradient(48% 40% at 92% 12%, var(--mesh-2) 0%, transparent 60%),
    radial-gradient(64% 50% at 50% 98%, var(--mesh-3) 0%, transparent 62%);
}

/* Grain très léger : casse le dégradé pur, qui « bande » sur grand écran. */
.mesh::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
  mix-blend-mode: overlay;
}

/* ============================================================
   Matériau « verre »
   ============================================================ */

.glass {
  position: relative;
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg);
  box-shadow:
    var(--glass-shadow),
    inset 0 1px 0 var(--glass-shine),
    inset 0 0 0 1px var(--glass-edge);
}

/* Reflet spéculaire du bord supérieur — la signature du matériau en clair.
   En sombre, --sheen vaut `none` : il n'y a plus de matériau à signer, les
   surfaces y sont des aplats. Un jeton plutôt qu'une règle sombre en double,
   pour que les deux thèmes ne puissent pas diverger. */
.glass::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 45%;
  border-radius: inherit;
  background: var(--sheen);
  pointer-events: none;
}

/* Repli pour les navigateurs sans backdrop-filter : on remonte l'opacité,
   sinon le texte passe sur un fond quasi transparent. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass { background: var(--glass-strong); }
}

/* ============================================================
   Barre supérieure flottante
   ============================================================ */

/* La barre ne flotte plus : posée en haut, pleine largeur, elle rend une
   trentaine de pixels au contenu et supprime le double titre — c'est elle qui
   dit où l'on se trouve, la carte de tête ne le répète plus. */
.workbar {
  position: sticky; top: 0; z-index: 60;
  display: flex; align-items: center; gap: 12px;
  padding: 10px clamp(12px, 3vw, 28px);
  padding-top: max(10px, env(safe-area-inset-top));
  background: var(--surface);
  border-bottom: 1px solid var(--hairline);
}
.workbar-title {
  font-size: 16px; font-weight: 660; letter-spacing: -0.02em;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Le compteur repris du titre de carte : plus discret que le nom de la page. */
.workbar-title .muted { font-size: 13px; font-weight: 550; }
/* Le moteur d'analyse, posé contre le compteur : c'est lui qui consomme.
   Deux rangs dans la même pastille : la marque, puis la version exacte du
   modèle — celle qui change le prix et la réponse. */
.workbar-engine {
  flex: none;
  display: flex; align-items: center; gap: 6px;
  padding: 3px 4px 3px 10px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  font-size: 11.5px; font-weight: 560; color: var(--text-2);
  white-space: nowrap;
}
.workbar-engine:empty { display: none; }
/* Sans version à afficher, la pastille reprend sa marge de droite. */
.workbar-engine:not(:has(.engine-model)) { padding-right: 10px; }
.engine-model {
  font-family: var(--mono);
  font-size: 10.5px; font-weight: 520; letter-spacing: -0.01em;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--text-3);
  max-width: 170px; overflow: hidden; text-overflow: ellipsis;
}
/* La version est le premier sacrifice quand la barre manque de place : la
   marque seule suffit à savoir quel moteur répond, le survol donne le reste. */
@media (max-width: 1240px) {
  .engine-model { display: none; }
  .workbar-engine { padding-right: 10px; }
}

/* La recherche globale. Elle ressemble au champ qu'elle ouvre — c'est ce qui
   la fait reconnaître sans la lire — sans en être un : la saisie se fait dans
   la palette, où les résultats ont la place de s'afficher. */
.workbar-search {
  flex: none;
  display: inline-flex; align-items: center; gap: 9px;
  height: 34px;
  min-width: 236px;
  padding: 0 5px 0 11px;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  background: var(--surface-2);
  color: var(--text-3);
  font: inherit; font-size: 13px;
  cursor: text;
  transition: background var(--motion-fast) ease, border-color var(--motion-fast) ease,
              box-shadow var(--motion-fast) ease;
}
.workbar-search:hover { background: var(--surface-3); border-color: var(--text-3); }
.workbar-search:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.workbar-search svg { width: 15px; height: 15px; flex: none; }
.workbar-search-label {
  flex: 1 1 auto; min-width: 0; text-align: left;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.workbar-search-kbd {
  flex: none;
  font-family: var(--mono); font-size: 10px; font-weight: 620;
  letter-spacing: 0.02em;
  padding: 3px 7px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  color: var(--text-3);
}

/* Le menu burger n'existe que là où la barre latérale disparaît. La
   spécificité doit dépasser celle de .btn, déclaré plus bas dans la feuille. */
.workbar .workbar-menu { display: none; }
/* Sur fond opaque, le verre des boutons n'a rien derrière lui à laisser voir. */
.workbar .btn-glass {
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
.workbar .btn-glass:hover:not(:disabled) { background: var(--surface-3); }

/* Le titre de page vit dans la barre : la carte de tête ne le redit pas. */
.card-title[data-page-head] { display: none; }

.topbar-wrap {
  position: sticky;
  top: 0;
  z-index: 60;
  padding: 14px clamp(12px, 3vw, 28px) 0;
}

.topbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 11px 14px 11px 18px;
  border-radius: var(--radius-xl);
  -webkit-backdrop-filter: var(--blur-heavy);
  backdrop-filter: var(--blur-heavy);
  background: var(--glass);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-shine);
}

.brand { display: flex; align-items: center; gap: 11px; min-width: 0; }

.brand-mark {
  width: 34px; height: 34px;
  flex: none;
  border-radius: 11px;
  display: grid; place-items: center;
  background: var(--brand-fill);
  box-shadow: var(--brand-shadow);
}
.brand-mark svg { width: 19px; height: 19px; color: var(--brand-ink); }

.brand-text { min-width: 0; }
.brand-name {
  font-size: 15px; font-weight: 650; letter-spacing: -0.015em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.brand-sub {
  font-size: 11.5px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.topbar-spacer { flex: 1 1 auto; }
.topbar-actions { display: flex; align-items: center; gap: 8px; flex: none; }

/* ---------- Compteur de consommation ---------- */

.meter {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 13px 6px 11px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  flex: none;
}
.meter-dial { position: relative; width: 26px; height: 26px; flex: none; }
.meter-dial svg { width: 26px; height: 26px; transform: rotate(-90deg); }
.meter-dial circle { fill: none; stroke-width: 3.5; stroke-linecap: round; }
.meter-track { stroke: var(--hairline); }
.meter-fill  { stroke: var(--accent); transition: stroke-dashoffset 0.6s var(--spring), stroke 0.3s; }
.meter.is-warn  .meter-fill { stroke: var(--orange); }
.meter.is-full  .meter-fill { stroke: var(--red); }
.meter-label { line-height: 1.25; }
.meter-value { font-size: 12.5px; font-weight: 620; font-variant-numeric: tabular-nums; }
.meter-unit  { font-size: 10.5px; color: var(--text-3); }

/* ============================================================
   Boutons
   ============================================================ */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 0 15px;
  height: 36px;
  border-radius: 999px;
  font-size: 13.5px; font-weight: 560;
  white-space: nowrap;
  transition: transform 0.18s var(--spring), background 0.18s, box-shadow 0.18s, opacity 0.18s;
}
.btn:active { transform: scale(0.955); }
.btn svg { width: 16px; height: 16px; flex: none; }

.btn-primary {
  background: var(--accent);
  color: var(--accent-text);
  box-shadow: 0 5px 16px -5px var(--accent), inset 0 1px 0 rgba(255, 255, 255, 0.28);
}
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); }

.btn-glass {
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur-panel);
  backdrop-filter: var(--blur-panel);
  border: 1px solid var(--glass-border);
  box-shadow: inset 0 1px 0 var(--glass-shine);
}
.btn-glass:hover:not(:disabled) { background: var(--surface-3); }

.btn-plain { background: var(--surface-2); border: 1px solid var(--hairline); }
.btn-plain:hover:not(:disabled) { background: var(--surface-3); }

.btn-ghost { color: var(--text-2); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }

.btn-danger { background: var(--red-soft); color: var(--red); }
.btn-danger:hover:not(:disabled) { background: var(--red); color: #fff; }

/* Effacement posé au bout d'une ligne : discret au repos, franc au survol —
   une rangée de boutons rouges dans un tableau se lirait comme une alerte. */
.btn-ghost.is-danger:hover:not(:disabled) { background: var(--red-soft); color: var(--red); }

/* Un bouton éteint se lit de la même façon quelle que soit sa variante, d'où
   cette règle placée après toutes les autres : elle remplace la teinte au lieu
   de la diluer. L'opacité seule ne pouvait pas y suffire — à 45 %, un bouton
   plein se délavait jusqu'au gris moyen pendant qu'un bouton clair bougeait à
   peine, si bien que deux boutons voisins dans le même état inactif (« Excel »
   et « Fournisseur » au bas du catalogue) semblaient de deux dessins
   différents. Le contraste du texte est volontairement faible : un libellé
   éteint doit se lire sans se proposer. */
.btn:disabled {
  cursor: not-allowed;
  transform: none;
  opacity: 1;
  background: var(--surface-2);
  color: var(--text-3);
  border: 1px solid var(--hairline);
  box-shadow: none;
}

.btn-icon { width: 36px; height: 36px; padding: 0; border-radius: 50%; }
.btn-sm { height: 30px; padding: 0 11px; font-size: 12.5px; }
.btn-sm svg { width: 14px; height: 14px; }

/* Un lien qui ouvre une page extérieure porte la même allure qu'un bouton :
   sans cette ligne, il reprendrait le bleu souligné du navigateur, seule tache
   de couleur non maîtrisée de l'interface. */
a.btn { color: inherit; text-decoration: none; }

/* ============================================================
   Menu du compte
   ============================================================ */

.user-menu {
  position: fixed;
  top: 68px;
  right: clamp(12px, 3vw, 28px);
  z-index: 70;
  width: 240px;
  padding: 14px;
  border-radius: var(--radius-lg);
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur-heavy);
  backdrop-filter: var(--blur-heavy);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-shine);
  animation: sheet-in 0.22s var(--spring);
}
.user-menu-name { font-size: 13.5px; font-weight: 620; }
.user-menu-role { font-size: 11.5px; color: var(--text-3); margin-top: 2px; }
.user-menu-sep { height: 1px; background: var(--hairline); margin: 11px 0; }
.user-menu-item {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 9px;
  border-radius: var(--radius-sm);
  font-size: 13px; font-weight: 520;
  color: var(--text);
  text-decoration: none;
  transition: background 0.16s;
  /* Le menu mêle des liens (<a>) et des actions (<button>) : ces règles
     leur donnent le même rendu. */
  width: 100%;
  text-align: left;
}
.user-menu-item:hover { background: var(--surface-2); }
.user-menu-item svg { width: 15px; height: 15px; color: var(--text-3); }
.user-menu-item.is-danger { color: var(--red); }
.user-menu-item.is-danger svg { color: var(--red); }

/* ============================================================
   Menu latéral (navigation entre pages — Catalogue, Clients…)
   ============================================================ */

.app-shell { display: flex; align-items: flex-start; min-height: 100vh; }

.sidebar {
  position: sticky; top: 0;
  width: 232px; flex: none;
  height: 100vh;
  display: flex; flex-direction: column;
  background: var(--surface);
  border-right: 1px solid var(--hairline);
  z-index: 55;
}

.sidebar-brand {
  display: flex; align-items: center; gap: 11px;
  padding: 18px 18px 16px;
  border-bottom: 1px solid var(--hairline);
  flex: none;
}
.sidebar-brand .brand-mark { width: 32px; height: 32px; border-radius: 10px; }
/* Logo importé : cadré comme la pastille par défaut, mais sans rogner un
   logo large — d'où le contain plutôt que le cover. */
.brand-logo {
  width: 32px; height: 32px; flex: none;
  border-radius: 9px;
  object-fit: contain;
  background: #fff;
  border: 1px solid var(--hairline);
  padding: 2px;
}
.sidebar-brand .brand-mark svg { width: 17px; height: 17px; }
.sidebar-brand .brand-name { font-size: 14.5px; }

/* Les réglages ne sont pas un écran de travail : un filet les détache du
   reste. Il se resserre avec le rail replié, où il n'a que des icônes à
   séparer. */
.sidebar-sep {
  height: 1px;
  margin: 8px 10px;
  background: var(--hairline);
}
body:not(.is-nav-wide) .sidebar-sep { margin-inline: 14px; }

.sidebar-nav {
  flex: 1 1 auto;
  padding: 14px 12px;
  display: flex; flex-direction: column; gap: 3px;
  overflow-y: auto;
  /* overflow-y: auto rend overflow-x automatique : sans ce hidden, le moindre
     dépassement d'un pixel fait apparaître une barre de défilement
     horizontale au pied du menu. */
  overflow-x: hidden;
}

.sidebar-link {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  font-size: 13.5px; font-weight: 550;
  color: var(--text-2);
  transition: background 0.16s, color 0.16s;
  text-align: left;
  width: 100%;
}
.sidebar-link:hover:not(.is-disabled) { background: var(--surface-2); color: var(--text); }
.sidebar-link.is-active { background: var(--accent-soft); color: var(--accent); }
.sidebar-link svg { width: 17px; height: 17px; flex: none; }
.sidebar-link span:not(.sidebar-badge) { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-link.is-disabled { opacity: 0.42; cursor: default; }

/* La pastille ne compte plus les fiches : elle annonce ce qui attend une
   action (voir refreshNavBadges). D'où l'orange plutôt que le gris — un
   compteur neutre se lit, une chose à faire doit se voir. */
.sidebar-badge {
  margin-left: auto;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 999px;
  display: grid; place-items: center;
  font-size: 10.5px; font-weight: 700;
  background: var(--orange-soft); color: var(--orange);
  flex: none;
}
/* Rien à faire, rien à voir : pas même un zéro, qui se lit comme une pastille
   allumée et fait vérifier une page où il n'y a rien. setNavBadge() pose bien
   l'attribut `hidden`, mais le `display: grid` ci-dessus l'emporte sur le
   `display: none` de la feuille du navigateur — d'où cette règle, comme pour
   .section[hidden] et .sidebar-scrim[hidden]. */
.sidebar-badge[hidden] { display: none; }
.sidebar-link.is-active .sidebar-badge { background: var(--orange); color: #fff; }

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

/* ---------- Repli du menu ----------
   Le rail est l'état normal : l'écran appartient aux données. Déployé, le
   menu passe PAR-DESSUS la page au lieu de la pousser — on le consulte, on ne
   réorganise pas son travail pour lui. */

.sidebar { width: 72px; transition: width var(--motion-med) ease; }
.sidebar-brand { justify-content: center; padding: 18px 8px 16px; }
.sidebar .brand-text { display: none; }
.sidebar-link { justify-content: center; padding: 11px; position: relative; }
.sidebar-link span:not(.sidebar-badge) { display: none; }
.sidebar-badge { position: absolute; margin-left: 0; transform: translate(11px, -11px); }

.sidebar-collapse {
  flex: none;
  display: flex; align-items: center; justify-content: center; gap: 11px;
  margin: 0 12px 12px;
  padding: 11px;
  border-radius: var(--radius-sm);
  color: var(--text-3); font-size: 12.5px; font-weight: 550;
  transition: background 0.16s, color 0.16s;
}
.sidebar-collapse:hover { background: var(--surface-2); color: var(--text); }
.sidebar-collapse svg { width: 16px; height: 16px; flex: none; transition: transform var(--motion-med) ease; }
.sidebar-collapse span { display: none; }

/* ---- Déployé : le menu montre les noms des pages ---- */
.is-nav-wide .sidebar { width: 232px; }
.is-nav-wide .sidebar-brand { justify-content: flex-start; padding: 18px 18px 16px; }
.is-nav-wide .sidebar .brand-text { display: block; }
.is-nav-wide .sidebar-link { justify-content: flex-start; padding: 9px 11px; }
.is-nav-wide .sidebar-link span:not(.sidebar-badge) { display: inline; }
.is-nav-wide .sidebar-badge { position: static; transform: none; margin-left: auto; }
.is-nav-wide .sidebar-collapse { justify-content: flex-start; padding: 9px 11px; }
.is-nav-wide .sidebar-collapse span { display: inline; }
.is-nav-wide .sidebar-collapse svg { transform: rotate(180deg); }

/* ---- Flottant : déployé par-dessus la page, sans la pousser ----
   C'est l'état du survol et du menu burger. Le mode choisi au bouton, lui,
   reprend sa place dans la mise en page. */
.is-nav-float .sidebar {
  position: fixed; top: 0; bottom: 0; left: 0;
  height: 100vh;
  box-shadow: 22px 0 48px -28px rgba(10, 20, 40, 0.4);
}

.sidebar-scrim {
  position: fixed; inset: 0; z-index: 54;
  background: rgba(10, 14, 22, 0.18);
  -webkit-backdrop-filter: blur(1px); backdrop-filter: blur(1px);
}
.sidebar-scrim[hidden] { display: none; }

/* ---------- Barre d'onglets — application installée ----------
   Sur mobile dans un navigateur, le menu burger suffit : la barre du bas
   entrerait en concurrence avec celle du navigateur. Installée en
   application, l'écran est à nous et le pouce tombe en bas. */
.tabbar { display: none; }

@media (max-width: 860px) {
  /* Le rail disparaît : sur un téléphone, 72 px, c'est un cinquième de
     l'écran. Le menu ne vient qu'à la demande, par le burger. */
  .sidebar { display: none; }
  .workbar .workbar-menu { display: inline-flex; }
  .is-nav-float .sidebar { display: flex; }
  .sidebar-collapse { display: none; }
}

@media (max-width: 860px) and (display-mode: standalone) {
  .tabbar { display: flex; }
  .shell { padding-bottom: calc(84px + env(safe-area-inset-bottom)); }
  /* La barre du bas couvre la navigation : le burger ferait double emploi,
     et la cinquième place mène déjà au reste des pages. */
  .workbar .workbar-menu { display: none; }
}
/* Repli pour iOS avant Safari 16.4, où display-mode n'est pas interrogeable :
   app.js pose la classe depuis navigator.standalone. */
@media (max-width: 860px) {
  .is-pwa .tabbar { display: flex; }
  .is-pwa .shell { padding-bottom: calc(84px + env(safe-area-inset-bottom)); }
  .is-pwa .workbar .workbar-menu { display: none; }
}

.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 80;
  padding: 6px 6px calc(6px + env(safe-area-inset-bottom));
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur-heavy);
  backdrop-filter: var(--blur-heavy);
  border-top: 1px solid var(--glass-border);
}
.tabbar-link {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 7px 2px 5px;
  border-radius: 12px;
  color: var(--text-3);
  font-size: 10.5px; font-weight: 560;
}
.tabbar-link svg { width: 21px; height: 21px; }
.tabbar-link span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.tabbar-link.is-active { color: var(--accent); }

/* ---------- Feuille de recherche et de navigation ---------- */

.nav-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 8px; }
.nav-tile {
  display: flex; align-items: center; gap: 10px;
  padding: 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  font-size: 13.5px; font-weight: 560;
  text-align: left;
  transition: background 0.16s, border-color 0.16s;
}
.nav-tile:hover { background: var(--surface-3); border-color: var(--text-3); }
.nav-tile.is-active { background: var(--accent-soft); border-color: transparent; color: var(--accent); }
.nav-tile.is-disabled { opacity: 0.45; cursor: default; }
.nav-tile svg { width: 17px; height: 17px; flex: none; }

/* Les résultats, par nature. L'en-tête d'un groupe dit ce que sont les lignes
   qui suivent : la nature n'a plus à être répétée sur chacune, et la ligne
   rend toute sa largeur au nom cherché. */
.nav-group + .nav-group { margin-top: 14px; }
.nav-group-head {
  display: flex; align-items: center; gap: 7px;
  padding: 0 4px 6px;
  font-size: 10.5px; font-weight: 640; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--text-3);
}
.nav-group-head svg { width: 13px; height: 13px; flex: none; }
.nav-group-count {
  font-size: 10px; font-weight: 620; letter-spacing: 0;
  padding: 1px 6px; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--hairline);
}

.nav-result {
  display: flex; align-items: center; gap: 11px;
  width: 100%;
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  text-align: left;
}
/* Une seule ligne se signale à la fois, et c'est la même pour la souris et
   pour les flèches : le survol déplace le curseur au lieu de s'ajouter à lui. */
.nav-result.is-cursor { background: var(--accent-soft); color: var(--accent); }
.nav-result.is-cursor .nav-result-sub,
.nav-result.is-cursor .nav-result-go { color: inherit; opacity: 0.75; }
.nav-result svg { width: 16px; height: 16px; flex: none; color: var(--text-3); }
/* Deux lignes empilées, pas deux morceaux de phrase : le nom d'abord, ce qui
   permet de le distinguer d'un homonyme en dessous. Des span en flux restaient
   côte à côte, et la coupure par points de suspension n'avait aucun effet sur
   des éléments en ligne. */
.nav-result-body { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; }
.nav-result-name { display: block; font-size: 13.5px; font-weight: 560; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-result-sub { display: block; font-size: 11.5px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Le chevron ne s'allume qu'à la ligne visée : huit chevrons alignés ne
   désignent plus rien. */
.nav-result-go { flex: none; display: inline-flex; opacity: 0; }
.nav-result.is-cursor .nav-result-go { opacity: 0.75; }
.nav-result-go svg { width: 15px; height: 15px; }
/* La photo ou le logo du résultat : on reconnaît la fiche avant d'en lire le
   nom. Ni loupe ni grossissement — la ligne entière est le lien. */
.nav-result .row-thumb,
.nav-result .row-thumb:hover { flex: none; border-radius: 8px; cursor: inherit; transform: none; }

/* Rappel des touches, sous les résultats. Sans clavier il n'y a rien à
   rappeler : la barre du bas ne s'affiche que sur les écrans larges. */
.nav-foot {
  display: none;
  flex: none;
  align-items: center; gap: 14px;
  padding: 10px 20px;
  border-top: 1px solid var(--hairline);
  font-size: 11.5px; color: var(--text-3);
}
.nav-foot kbd {
  font-family: var(--mono); font-size: 10px; font-weight: 620;
  padding: 2px 6px; margin-right: 5px;
  border-radius: 6px;
  background: var(--surface-2); border: 1px solid var(--hairline);
  color: var(--text-2);
}

/* ============================================================
   Mise en page
   ============================================================ */

.shell {
  max-width: 1560px;
  margin: 0 auto;
  padding: 14px clamp(12px, 3vw, 28px) 120px;
  display: flex; flex-direction: column; gap: 14px;
}

/* Aperçu carré d'un visuel de personnalisation (logo, favicon). */
.branding-tile {
  width: 56px; height: 56px; flex: none;
  border-radius: var(--radius-sm);
  display: grid; place-items: center;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  color: var(--text-3);
  cursor: pointer;
  transition: border-color 0.16s;
}
.branding-tile:hover { border-color: var(--accent); }

/* Barre d'actions en pied de carte — pendant non flottant de .sheet-foot,
   pour les sections de page normales (ex. la page Clients). */
.card-foot {
  display: flex; align-items: center; gap: 9px;
  margin-top: 16px; padding-top: 16px;
  border-top: 1px solid var(--hairline);
}

/* ---------- État de l'enregistrement automatique ----------
   À la place qu'occupait le bouton « Enregistrer ». La hauteur est celle d'un
   bouton : sans elle, le pied se réduirait en passant de « Enregistrement… »
   à un état plus court, et les boutons voisins sauteraient. */
.autosave-host { display: inline-flex; align-items: center; min-height: 30px; }
.autosave {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--text-3);
  white-space: nowrap;
}
.autosave svg { width: 13px; height: 13px; flex: none; }
.autosave-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.autosave.is-pending { color: var(--text-2); }
.autosave.is-saving { color: var(--text-2); }
.autosave.is-saving svg { animation: spin 0.9s linear infinite; }
.autosave.is-saved { color: var(--green); }
.autosave.is-error { color: var(--red); white-space: normal; }

/* Les actions de fin de ligne restent groupées à droite de leur cellule. */
.row-actions { display: flex; gap: 2px; justify-content: flex-end; }

.card { padding: 16px; }
.card-head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.card-title { font-size: 16px; font-weight: 640; letter-spacing: -0.015em; }
.card-sub { font-size: 12.5px; color: var(--text-3); margin-top: 1px; }
.card-head-spacer { flex: 1 1 auto; }

/* ============================================================
   Zone de dépôt
   ============================================================ */

.drop {
  position: relative;
  border-radius: var(--radius-lg);
  border: 1.5px dashed var(--glass-border);
  background: var(--sheen-soft);
  padding: 34px 22px;
  text-align: center;
  transition: border-color 0.22s, background 0.22s, transform 0.22s var(--spring);
}
.drop.is-over {
  border-color: var(--accent);
  background: var(--accent-soft);
  transform: scale(1.008);
}
.drop-icon {
  width: 54px; height: 54px;
  margin: 0 auto 13px;
  border-radius: 17px;
  display: grid; place-items: center;
  background: var(--accent-soft);
  color: var(--accent);
  transition: transform 0.4s var(--spring);
}
.drop.is-over .drop-icon { transform: translateY(-4px) scale(1.06); }
.drop-icon svg { width: 25px; height: 25px; }
.drop-title { font-size: 15.5px; font-weight: 600; letter-spacing: -0.01em; }
.drop-hint { font-size: 12.5px; color: var(--text-3); margin-top: 4px; }
.drop-actions { display: flex; flex-wrap: wrap; gap: 9px; justify-content: center; margin-top: 17px; }

.drop-mode {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--hairline);
  display: flex; flex-direction: column; align-items: center; gap: 8px;
}
.drop-mode-hint { font-size: 12px; color: var(--text-3); text-align: center; }

/* ---------- Galerie de vues ---------- */

.views { display: flex; gap: 6px; flex-wrap: wrap; }
.view-thumb {
  width: 34px; height: 34px;
  border-radius: 8px;
  object-fit: cover;
  background: var(--surface-3);
  border: 1px solid var(--hairline);
  cursor: zoom-in;
  transition: transform 0.2s var(--spring);
}
.view-thumb:hover { transform: scale(1.12); }

.view-count {
  position: absolute; right: 3px; bottom: 3px;
  min-width: 17px; height: 17px;
  padding: 0 4px;
  border-radius: 999px;
  display: grid; place-items: center;
  font-size: 10px; font-weight: 680;
  color: #fff;
  background: rgba(10, 20, 40, 0.74);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  pointer-events: none;
}
.thumb-wrap { position: relative; display: inline-block; line-height: 0; }

.detail-gallery { display: flex; flex-direction: column; gap: 7px; flex: none; }
.detail-gallery .view-thumb { width: 44px; height: 44px; border-radius: 10px; }

/* ---------- Vues d'une fiche produit, ajoutables et retirables ---------- */

.photo-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.photo-cell {
  position: relative;
  width: 92px; height: 92px;
  border-radius: 12px;
  overflow: hidden;
  background: var(--surface-3);
  border: 1px solid var(--hairline);
}
/* Le pointillé dit ce que le serveur ne sait pas encore. */
.photo-cell.is-pending { border: 1.5px dashed var(--accent); }
.photo-cell img { width: 100%; height: 100%; object-fit: cover; display: block; cursor: zoom-in; }

.photo-drop {
  position: absolute; top: 4px; right: 4px;
  width: 24px; height: 24px;
  display: grid; place-items: center;
  border-radius: 50%;
  color: #fff;
  background: rgba(10, 20, 40, 0.62);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  transition: background 0.16s;
}
.photo-drop:hover { background: var(--red); }

.photo-add {
  width: 92px; height: 92px;
  border-radius: 12px;
  border: 1.5px dashed var(--glass-border);
  background: transparent;
  color: var(--text-3);
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  font-size: 11.5px; font-weight: 560;
  transition: border-color 0.16s, color 0.16s;
}
.photo-add:hover { border-color: var(--accent); color: var(--accent); }

/* ---------- Réglages repliables ---------- */

.settings { margin-top: 15px; }
.settings > summary {
  list-style: none;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 12px 6px 10px;
  border-radius: 999px;
  font-size: 12.5px; font-weight: 550; color: var(--text-2);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  cursor: pointer;
  transition: background 0.18s;
}
.settings > summary::-webkit-details-marker { display: none; }
.settings > summary:hover { background: var(--surface-3); color: var(--text); }
.settings > summary svg { width: 14px; height: 14px; transition: transform 0.28s var(--spring); }
.settings[open] > summary svg { transform: rotate(90deg); }

.settings-body {
  margin-top: 13px;
  padding: 16px;
  border-radius: var(--radius-md);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  display: grid;
  gap: 14px;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  animation: sheet-in 0.3s var(--spring);
}

/* ============================================================
   Champs de saisie
   ============================================================ */

.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field-label {
  font-size: 11px; font-weight: 620; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--text-3);
}
.field-hint { font-size: 11.5px; color: var(--text-3); }

/* Retour en tête d'un formulaire de sous-page. */
.form-back { margin: 0 0 14px; }
.form-back .btn { padding-left: 6px; }
.form-back .btn svg { margin-right: -1px; }
/* Dans un tiroir, la zone défilante est connue : le bouton peut y rester collé
   en haut, et le fond opaque évite que le formulaire ne défile au travers. */
.sheet-body > .form-back {
  position: sticky; top: -18px; z-index: 3;
  margin: -18px -20px 12px; padding: 14px 20px 10px;
  background: var(--surface);
  border-bottom: 1px solid var(--hairline);
}

/* Deux entrées d'un même code : le chiffre de tête, puis le code lui-même.
   Le chiffre est une liste et non un champ libre — il n'a que huit ou neuf
   valeurs possibles, et aucune saisie invalide n'a alors à être rattrapée. */
/* ---------- EAN-14 et leur code-barres ----------
   Colis et palette côte à côte, chacun avec son code-barres juste dessous.
   Une grille à part plutôt que la grille des codes : mêlés au SKU et à
   l'EAN-13, les deux dessins étiraient une rangée sur deux. */
.gtin14-grid {
  display: grid; gap: 14px 12px; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.grid-2 + .gtin14-grid { margin-top: 12px; }
.gtin14-cell { min-width: 0; }

.gtin-bc {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  margin-top: 10px;
  padding: 12px;
  border-radius: var(--radius-md);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  transition: opacity 0.18s;
}
.gtin-bc:empty { display: none; }
.gtin-bc.is-loading { opacity: 0.55; }
/* Le symbole se lit sur blanc quel que soit le thème : c'est ce que verra un
   lecteur, et ce qui sera collé dans un document. */
.gtin-bc-sheet {
  width: 100%; max-width: 330px;
  padding: 10px 8px 7px;
  border-radius: 9px;
  background: #fff;
  color: #000;
  box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.08);
  text-align: center;
}
.gtin-bc-sheet svg { display: block; width: 100%; height: auto; }
.gtin-bc-hri {
  margin-top: 4px;
  font-family: "Courier New", Courier, monospace;
  font-size: 13px; font-weight: 700; letter-spacing: 0.06em;
  user-select: all;
}
.gtin-bc-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; }
.gtin-bc-note { margin: 0; padding: 6px 4px; font-size: 12px; color: var(--text-3); text-align: center; }
.gtin-bc.is-warn .gtin-bc-note { color: var(--orange); }

.field-combo { display: flex; gap: 8px; min-width: 0; }
/* Mot de passe d'un compte neuf et ses trois boutons : le champ garde de quoi
   lire seize caractères, les boutons passent dessous sur un écran étroit. */
.field-combo.is-password { flex-wrap: wrap; align-items: center; }
.field-combo.is-password > .input { flex: 1 1 12rem; min-width: 12rem; font-family: var(--mono); }
.field-combo.is-password > .btn { flex: none; }
/* Une adresse à recopier et son bouton : le champ prend la place restante et
   le bouton garde la sienne ; sous 860 px, ils passent sur deux lignes plutôt
   que d'écraser l'URL jusqu'à l'illisible. */
.field-combo.is-copy { flex-wrap: wrap; align-items: center; }
.field-combo.is-copy > .input { flex: 1 1 16rem; min-width: 11rem; }
.field-combo.is-copy > .btn { flex: none; }
.field-combo .is-indicator { flex: none; width: 72px; }
.field-combo .is-indicator .pf-select-btn {
  padding: 9px 24px 9px 10px;
  font-variant-numeric: tabular-nums; font-weight: 600;
}
.field-combo .is-indicator .pf-select-btn svg { right: 7px; }

.input, .textarea, .select {
  width: 100%;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--hairline);
  font-size: 13.5px;
  transition: border-color 0.18s, box-shadow 0.18s;
}
.input:focus, .textarea:focus, .select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3.5px var(--accent-soft);
}
/* ---------- Champs obligatoires ----------
   L'astérisque annonce, le rouge signale — mais seulement après une tentative
   de génération. Un formulaire qui s'ouvre déjà en alerte n'apprend rien à
   personne, et on finit par ne plus voir le rouge. */
.req { color: var(--red); font-weight: 700; }
.field-req {
  display: none;
  margin-top: 4px;
  font-size: 11.5px; font-weight: 560; color: var(--red);
}
.field.is-missing .field-req { display: block; }
.field.is-missing .input,
.field.is-missing .textarea,
.field.is-missing .pf-select-btn {
  border-color: var(--red);
  background: var(--red-soft);
}
.field.is-missing .input:focus,
.field.is-missing .textarea:focus,
.field.is-missing .pf-select-btn:focus-visible,
.field.is-missing .pf-select.is-open .pf-select-btn {
  border-color: var(--red);
  box-shadow: 0 0 0 3.5px var(--red-soft);
}

/* Un champ en lecture seule doit se voir comme tel, sinon on tape dedans. */
.input[readonly] { background: var(--surface-2); color: var(--text-2); cursor: default; }
.input[readonly]:focus { border-color: var(--hairline); box-shadow: none; }

.textarea { resize: vertical; min-height: 66px; line-height: 1.5; }
.select { appearance: none; padding-right: 32px; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%238b93a1' stroke-width='1.8' stroke-linecap='round'%3E%3Cpath d='M4 6.5l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center; background-size: 15px;
}

/* ---------- Menus déroulants ----------
   La liste d'un <select> natif appartient au système : impossible à styler,
   elle plaquait un rectangle bleu de Windows au milieu de l'interface. Le
   <select> reste là, sous le bouton, et garde la valeur ; ce qui suit dessine
   ce que l'on voit. Voir pfSelectEnhance() dans app.js.

   Le champ fermé reprend exactement les cotes d'un .input : les deux se
   côtoient dans les mêmes grilles, un écart d'un pixel s'y verrait. */

.pf-select { position: relative; width: 100%; min-width: 0; }
.pf-select > select {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  opacity: 0; pointer-events: none;
}

.pf-select-btn {
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  padding: 9px 32px 9px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--hairline);
  font-size: 13.5px;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
}
.pf-select-btn:hover:not(:disabled) { border-color: var(--text-3); }
.pf-select-btn:focus-visible,
.pf-select.is-open .pf-select-btn {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3.5px var(--accent-soft);
}
.pf-select-btn:disabled { opacity: 0.55; cursor: not-allowed; background: var(--surface-2); }

.pf-select-value {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pf-select-btn > svg {
  position: absolute; right: 10px;
  width: 15px; height: 15px;
  color: var(--text-3);
  transition: transform 0.2s var(--spring);
}
.pf-select.is-open .pf-select-btn > svg { transform: rotate(180deg); }
.pf-select.is-open.is-up .pf-select-btn > svg { transform: rotate(0deg); }

/* ---------- Le panneau flottant, commun à tous les menus ----------
   La liste d'un <select> habillé et le menu « … » d'une ligne de liste sont
   le même objet : un panneau posé sous <body>, calé sur un bouton, qui porte
   des lignes cliquables. Il n'a donc qu'un dessin, ici.

   Le menu « … » en avait eu un seul à lui, recopié à la main. Il peignait son
   fond avec « var(--surface-1) » et son texte avec « var(--text-1) » — deux
   variables qui n'ont jamais existé : le panneau s'ouvrait sans aucun fond,
   et l'on lisait ses intitulés par-dessus la page. Une copie ne se maintient
   pas ; celle-ci a donc disparu au profit de ces règles-là.

   Il flotte : c'est du chrome, donc du verre — à l'inverse des surfaces de
   données, qui restent opaques. Position fixe et parent <body>, parce qu'un
   tableau qui défile rognerait un panneau rendu à l'intérieur. Le placement
   est dans pfMenuPlace(), app.js. */
.pf-menu {
  position: fixed; z-index: 250;
  padding: 5px;
  border-radius: 14px;
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur-pop);
  backdrop-filter: var(--blur-pop);
  border: 1px solid var(--glass-border);
  box-shadow: 0 14px 40px -12px rgba(10, 20, 40, 0.34), inset 0 1px 0 var(--glass-shine);
  overflow-y: auto; overscroll-behavior: contain;
  animation: pf-menu-in var(--motion-fast) ease-out both;
}
.pf-menu.is-up { transform-origin: bottom center; }

.pf-menu-item {
  display: flex; align-items: center; gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: 0; border-radius: 9px;
  background: none;
  font: inherit; font-size: 13.5px;
  text-align: left;
  color: var(--text);
  cursor: pointer;
}
.pf-menu-item > span {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pf-menu-item > svg { width: 15px; height: 15px; flex: none; }
/* L'icône de tête dit de quoi parle la ligne, la marque de fin dit ce qui est
   choisi : la première suit le texte, la seconde porte l'accent. */
.pf-menu-item > svg:first-child { color: var(--text-2); }
.pf-menu-item > .pf-menu-mark { color: var(--accent); }
/* Le survol, le clavier et le focus désignent la même chose : une seule marque. */
.pf-menu-item.is-active,
.pf-menu-item:focus-visible { background: var(--surface-3); outline: none; }
.pf-menu-item.is-current { font-weight: 600; }
.pf-menu-item:disabled { opacity: 0.45; cursor: not-allowed; background: none; }
/* Une action destructrice porte sa couleur jusque dans son icône : c'est elle
   qu'on voit avant de lire l'intitulé. */
.pf-menu-item.is-danger { color: var(--red); }
.pf-menu-item.is-danger > svg:first-child { color: inherit; }

/* Un trait avant ce qui ne se rattrape pas : la corbeille ne doit pas se
   cliquer dans l'élan de la ligne du dessus. */
.pf-menu-sep { height: 1px; margin: 5px 6px; background: var(--hairline); }

/* Intitulé de groupe dans un menu : « Les plus courantes », « Autres devises ». */
.pf-menu-group {
  padding: 7px 10px 3px;
  font-size: 10.5px; font-weight: 620; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--text-3);
}
/* Le trait sépare un groupe de ce qui le précède à l'écran : un groupe dont la
   recherche a écarté tout ce qui venait avant lui passe en tête, sans trait. */
.pf-menu-item:not([hidden]) ~ .pf-menu-group { margin-top: 5px; padding-top: 9px; border-top: 1px solid var(--hairline); }

/* Vignette d'une option — photo de produit, logo d'annuaire (optionThumb,
   app.js). Dans une liste, ce n'est pas un aperçu à agrandir : ni loupe ni
   grossissement au survol, qui feraient trembler la ligne qu'on vise. Le carré
   vide garde la place d'une option sans vignette, pour que les noms restent
   alignés. */
.pf-thumb { flex: none; display: inline-flex; font-style: normal; }
.pf-thumb .row-thumb,
.pf-thumb .row-thumb:hover { border-radius: 6px; cursor: inherit; transform: none; }
.pf-thumb.is-gap { width: 30px; height: 30px; }
.pf-select-btn .pf-thumb .row-thumb { border-radius: 5px; }

/* Code puis précision : les codes ont tous trois lettres, la colonne fixe
   aligne les noms les uns sous les autres. */
.pf-menu-key { display: inline-block; min-width: 2.7em; font-weight: inherit; }
.pf-menu-sub { margin-left: 6px; font-size: 12.5px; font-weight: 400; color: var(--text-3); }

/* ---------- La recherche d'une liste déroulante ----------
   Chaque liste s'ouvre sur un champ qui la filtre (pfSelectEnhance, app.js).
   Le panneau devient une colonne : le champ ne bouge pas, seule la liste
   défile sous lui. Ouvert vers le haut, le champ passe en bas — contre le
   bouton, là où sont déjà le regard et la souris, et immobile pendant que la
   liste rétrécit au fil de la frappe. */
.pf-menu.has-search {
  display: flex; flex-direction: column;
  overflow: hidden;
}
.pf-menu.has-search.is-up { flex-direction: column-reverse; }

/* Le champ de recherche des listes de l'application (.search), sans ce qui le
   calibrait pour une barre d'outils : ni plafond de largeur, ni croissance —
   dans une colonne, sa base de 210 px en aurait fait la hauteur. La marge
   laisse la place à son halo de focus, que le panneau rognerait. */
.search.pf-menu-search { flex: none; min-width: 0; max-width: none; margin: 1px 1px 5px; }
.pf-menu.is-up > .search.pf-menu-search { margin: 5px 1px 1px; }
.pf-menu-search > .input { padding-top: 7px; padding-bottom: 7px; }

.pf-menu-list {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
}

/* Écartés par la recherche : l'attribut hidden ne l'emporte pas seul sur le
   display d'une ligne. */
.pf-menu-item[hidden], .pf-menu-group[hidden] { display: none; }

.pf-menu-empty { padding: 10px; font-size: 13px; color: var(--text-3); text-align: center; }

/* ---------- Drapeaux ----------
   Toile de 30 × 20 posée en 21 × 14. Le liseré intérieur détache les
   drapeaux à fond blanc (Japon, Pologne…) du menu clair qui les porte. */
.flag {
  position: relative;
  display: inline-block; flex: none;
  width: 21px; height: 14px;
  border-radius: 3px;
  overflow: hidden;
  vertical-align: middle;
}
.flag svg { display: block; width: 100%; height: 100%; }
.flag::after {
  content: ""; position: absolute; inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.16);
  pointer-events: none;
}
/* Plus précis que « .pf-menu-item > span », qui étire l'intitulé : le
   drapeau, lui, garde sa taille. */
.pf-menu-item > .flag, .pf-select-btn > .flag { flex: none; overflow: hidden; }

@keyframes pf-menu-in {
  from { opacity: 0; transform: translateY(-4px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}

/* ---------- Interrupteur ---------- */

.switch { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }
.switch input { position: absolute; opacity: 0; pointer-events: none; }
.switch-track {
  width: 44px; height: 27px; flex: none;
  border-radius: 999px;
  background: var(--surface-3);
  border: 1px solid var(--hairline);
  position: relative;
  transition: background 0.26s var(--spring);
}
.switch-thumb {
  position: absolute; top: 2px; left: 2px;
  width: 21px; height: 21px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.24);
  transition: transform 0.26s var(--spring);
}
.switch input:checked + .switch-track { background: var(--accent); border-color: transparent; }
.switch input:checked + .switch-track .switch-thumb { transform: translateX(17px); }
.switch-text { font-size: 13px; }

/* ---------- Contrôle segmenté ---------- */

.segmented {
  display: inline-flex;
  padding: 3px;
  border-radius: 999px;
  background: var(--surface-3);
  border: 1px solid var(--hairline);
  gap: 2px;
}
.segmented button {
  padding: 5px 13px;
  border-radius: 999px;
  font-size: 12.5px; font-weight: 550;
  color: var(--text-2);
  transition: background 0.22s var(--spring), color 0.22s, box-shadow 0.22s;
}
.segmented button[aria-selected="true"] {
  background: var(--surface);
  color: var(--text);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}

/* La barre d'onglets de la page Paramètres. Le conteneur est en flex pour que
   la piste garde la largeur de ses boutons — un inline-flex enfant direct de
   la colonne .shell s'étirerait sur toute la largeur. Sur un écran étroit,
   quatre onglets ne tiennent pas : la barre défile plutôt que de se casser. */
.settings-tabs { display: flex; overflow-x: auto; scrollbar-width: none; }
.settings-tabs::-webkit-scrollbar { display: none; }
.settings-tabs .segmented { flex: none; }

/* Dans une rangée de champs, le contrôle segmenté est un champ : il en prend
   la largeur et la hauteur, et ses options se partagent la place à égalité.
   Laissé à sa largeur de contenu, il s'étirait quand même — un enfant de
   conteneur flex voit son inline-flex ramené à flex, et la piste grise
   s'allongeait toute seule au-delà de ses boutons. */
.field > .segmented {
  width: 100%;
  min-height: 40px;
  align-items: stretch;
}
.field > .segmented button {
  flex: 1 1 0; min-width: 0;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 5px 8px;
  white-space: nowrap;
}

/* ============================================================
   File d'analyse
   ============================================================ */

.queue { display: flex; flex-direction: column; gap: 9px; margin-top: 15px; }

.queue-item {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 13px;
  border-radius: var(--radius-md);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  animation: row-in 0.34s var(--spring);
}
/* Élément déposé, pas encore envoyé à l'analyse : il porte un champ de
   précisions, donc il s'aligne en haut plutôt qu'au centre. */
.queue-item.is-draft { align-items: flex-start; }
.queue-item.is-draft .queue-note { margin-top: 7px; }

.queue-thumb {
  width: 44px; height: 44px; flex: none;
  border-radius: 11px;
  object-fit: cover;
  background: var(--surface-3);
}
.queue-body { flex: 1 1 auto; min-width: 0; }
.queue-name {
  font-size: 13.5px; font-weight: 560;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.queue-state { font-size: 12px; color: var(--text-3); margin-top: 1px; }
.queue-item.is-error .queue-state { color: var(--red); white-space: normal; }
.queue-item.is-done  .queue-state { color: var(--green); }

/* Barre de progression déterminée — estimation par le temps écoulé (aucun
   signal réel du serveur : une analyse est un seul aller-retour, pas un flux). */
.queue-progress { display: flex; align-items: center; gap: 8px; margin-top: 7px; }
.queue-bar {
  flex: 1 1 auto;
  height: 3px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
}
.queue-bar-fill {
  display: block; height: 100%; width: 0%;
  border-radius: 999px;
  background: var(--accent);
  transition: width 0.3s linear;
}
.queue-pct {
  flex: none;
  font-size: 11px; font-variant-numeric: tabular-nums;
  color: var(--text-3);
  min-width: 30px; text-align: right;
}

/* ---------- Anneau de progression ---------- */

.spinner {
  width: 18px; height: 18px; flex: none;
  border-radius: 50%;
  border: 2px solid var(--accent-soft);
  border-top-color: var(--accent);
  animation: spin 0.75s linear infinite;
}

/* ============================================================
   Tableau
   ============================================================ */

.table-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 9px;
  margin-bottom: 14px;
}

.search {
  position: relative;
  flex: 1 1 210px;
  min-width: 160px;
  max-width: 320px;
}
.search svg {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  width: 15px; height: 15px; color: var(--text-3); pointer-events: none;
}
.search input { padding-left: 33px; }

/* Barre de défilement horizontal dupliquée en haut du tableau : sur un
   tableau long, la barre native (en bas de .table-scroll) n'est atteignable
   qu'après avoir déjà tout parcouru. Un simple div dont la largeur interne
   mime celle du tableau, avec sa propre scrollbar, synchronisé par JS. */
.table-scroll-top {
  overflow-x: auto;
  overflow-y: hidden;
  height: 16px;
  border: 1px solid var(--hairline);
  border-bottom: 0;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  background: var(--surface);
}
.table-scroll-top > div { height: 1px; }
.table-scroll-top.hide { display: none; }
/* Le tableau perd son propre arrondi supérieur quand la barre du haut est
   visible juste au-dessus, pour que les deux se lisent comme un seul bloc. */
.table-scroll-top:not(.hide) + .table-scroll {
  border-top: 0;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
}

.table-scroll {
  border-radius: var(--radius-md);
  border: 1px solid var(--hairline);
  overflow: auto;
  max-height: 68vh;
  background: var(--surface);
  /* La donnée dense reste sur une surface opaque : le verre ici nuirait
     à la lisibilité au lieu de la servir. */
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}

table { border-collapse: separate; border-spacing: 0; width: max-content; min-width: 100%; }

thead th {
  position: sticky; top: 0; z-index: 3;
  background: var(--surface-2);
  -webkit-backdrop-filter: var(--blur-head);
  backdrop-filter: var(--blur-head);
  padding: 10px 13px;
  text-align: left;
  font-size: 11px; font-weight: 640; letter-spacing: 0.035em; text-transform: uppercase;
  color: var(--text-2);
  white-space: nowrap;
  border-bottom: 1px solid var(--hairline);
}

tbody td {
  padding: 9px 13px;
  border-bottom: 1px solid var(--hairline);
  vertical-align: top;
  font-size: 13px;
  max-width: 300px;
}
tbody tr { transition: background 0.14s; }
tbody tr:hover { background: var(--surface-2); }
tbody tr.is-clickable { cursor: pointer; }
tbody tr.is-selected { background: var(--accent-soft); }
tbody tr:last-child td { border-bottom: 0; }

/* Colonnes épinglées à gauche : case à cocher, photo et nom du produit
   restent visibles pendant le défilement horizontal. Photo et nom n'existent
   que si leur colonne est active (réglage Colonnes) — le décalage (`left`,
   posé en style inline par le JS) et le trait de séparation (`.col-pin-end`,
   posé sur la dernière colonne réellement présente) s'adaptent en conséquence. */
.col-pin-1, .col-pin-2, .col-pin-3 { position: sticky; z-index: 2; background: var(--surface); }
.col-pin-1 { left: 0; width: 44px; }
/* left de .col-pin-2 : sans valeur ici, l'élément sticky n'a aucun point
   d'ancrage et défile comme une colonne normale — c'est ce qui faisait
   « perdre » la photo au moindre défilement horizontal. */
.col-pin-2 { left: 44px; }
thead .col-pin-1, thead .col-pin-2, thead .col-pin-3 { z-index: 4; background: var(--surface-2); }
tbody tr:hover .col-pin-1, tbody tr:hover .col-pin-2, tbody tr:hover .col-pin-3 { background: var(--surface-2); }
tbody tr.is-selected .col-pin-1, tbody tr.is-selected .col-pin-2, tbody tr.is-selected .col-pin-3 { background: #e5eeff; }
:root[data-theme="dark"] tbody tr.is-selected .col-pin-1,
:root[data-theme="dark"] tbody tr.is-selected .col-pin-2,
:root[data-theme="dark"] tbody tr.is-selected .col-pin-3 { background: #1c2635; }
.col-pin-end::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 1px;
  background: var(--hairline);
}

.cell-clamp {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3;
  line-clamp: 3; overflow: hidden;
  white-space: pre-line;
}
.cell-mono { font-family: var(--mono); font-size: 12px; }
.cell-num { font-variant-numeric: tabular-nums; white-space: nowrap; }

.row-thumb {
  width: 46px; height: 46px;
  border-radius: 10px;
  object-fit: cover;
  background: var(--surface-3);
  cursor: zoom-in;
  transition: transform 0.22s var(--spring);
}
.row-thumb:hover { transform: scale(1.09); }

/* Les actions restent dans la colonne Produit, qui est épinglée à gauche :
   elles évitent de traverser le tableau pour ouvrir une fiche ou relancer
   une analyse. */
.table-photo-stack { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.table-quick-actions { display: flex; gap: 4px; }
.table-quick-action { width: 28px; height: 28px; }
.table-quick-action svg { width: 14px; height: 14px; }

.row-name { font-weight: 570; }
.row-brand { font-size: 11.5px; color: var(--text-3); margin-top: 1px; }

/* Ce qu'annonce la pastille du menu, sur la ligne concernée. Le détail est
   dans l'intitulé de survol : la liste reste lisible, et la réponse est à un
   geste. */
.row-todo {
  display: inline-flex; align-items: center; gap: 4px;
  margin-top: 3px;
  font-size: 11.5px; font-weight: 560;
  color: var(--orange);
  cursor: help;
}
.row-todo svg { width: 12px; height: 12px; flex: none; }
/* En tête du formulaire, la liste complète tient sur plusieurs lignes. */
.row-todo.is-block { display: flex; align-items: flex-start; margin-top: 6px; cursor: default; }
.row-todo.is-block svg { margin-top: 2px; }

/* ---------- Case à cocher ---------- */

.check {
  appearance: none;
  width: 19px; height: 19px;
  border-radius: 6px;
  border: 1.5px solid var(--text-3);
  background: var(--surface);
  cursor: pointer;
  position: relative;
  transition: background 0.18s, border-color 0.18s;
  flex: none;
}
.check:checked { background: var(--accent); border-color: var(--accent); }
.check:checked::after {
  content: ""; position: absolute; inset: 0;
  background: no-repeat center / 12px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='white' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3.2L13 5'/%3E%3C/svg%3E");
}
.check:indeterminate { background: var(--accent); border-color: var(--accent); }
.check:indeterminate::after {
  content: ""; position: absolute; inset: 0;
  background: no-repeat center / 11px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' stroke='white' stroke-width='2.4' stroke-linecap='round'%3E%3Cpath d='M4 8h8'/%3E%3C/svg%3E");
}

/* ============================================================
   Pastilles et indicateurs
   ============================================================ */

.pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2.5px 9px;
  border-radius: 999px;
  font-size: 11.5px; font-weight: 580;
  white-space: nowrap;
}
.pill svg { width: 12px; height: 12px; }
.pill-accent { background: var(--accent-soft); color: var(--accent); }
.pill-green  { background: var(--green-soft);  color: var(--green); }
.pill-orange { background: var(--orange-soft); color: var(--orange); }
.pill-red    { background: var(--red-soft);    color: var(--red); }
.pill-violet { background: var(--violet-soft); color: var(--violet); }
.pill-mute   { background: var(--surface-3);   color: var(--text-2); }

/* Jauge de certitude */
.confidence { display: flex; align-items: center; gap: 7px; }
.confidence-bar {
  width: 42px; height: 5px;
  border-radius: 999px;
  background: var(--surface-3);
  overflow: hidden;
  flex: none;
}
.confidence-bar span { display: block; height: 100%; border-radius: 999px; transition: width 0.5s var(--spring); }
.confidence-val { font-size: 12px; font-weight: 580; font-variant-numeric: tabular-nums; }

/* ============================================================
   État vide
   ============================================================ */

.empty { padding: 54px 22px; text-align: center; }
.empty-icon {
  width: 62px; height: 62px; margin: 0 auto 15px;
  border-radius: 19px;
  display: grid; place-items: center;
  background: var(--surface-2);
  color: var(--text-3);
}
.empty-icon svg { width: 28px; height: 28px; }
.empty-title { font-size: 15.5px; font-weight: 620; }
.empty-text { font-size: 13px; color: var(--text-3); margin-top: 5px; max-width: 400px; margin-inline: auto; }

/* ============================================================
   Feuille modale (bottom sheet iOS)
   ============================================================ */

.scrim {
  position: fixed; inset: 0; z-index: 100;
  background: rgba(8, 10, 14, 0.42);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  animation: fade-in 0.26s ease;
}
.scrim[hidden] { display: none; }

.sheet {
  position: fixed; z-index: 101;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  width: min(920px, 100%);
  max-height: 92vh;
  display: flex; flex-direction: column;
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur-heavy);
  backdrop-filter: var(--blur-heavy);
  border: 1px solid var(--glass-border);
  border-bottom: 0;
  box-shadow: 0 -18px 60px -14px rgba(0, 0, 0, 0.5), inset 0 1px 0 var(--glass-shine);
  animation: sheet-up 0.42s var(--spring);
}
.sheet[hidden] { display: none; }

.sheet-grabber {
  width: 38px; height: 5px;
  margin: 9px auto 0;
  border-radius: 999px;
  background: var(--text-3);
  opacity: 0.4;
  flex: none;
}

.sheet-head {
  display: flex; align-items: center; gap: 12px;
  padding: 13px 20px 13px;
  border-bottom: 1px solid var(--hairline);
  flex: none;
}
.sheet-title { font-size: 16.5px; font-weight: 650; letter-spacing: -0.02em; }
.sheet-sub { font-size: 12.5px; color: var(--text-3); margin-top: 1px; }
.sheet-head-spacer { flex: 1 1 auto; }

.sheet-body {
  padding: 18px 20px 22px;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}

.sheet-foot {
  display: flex; align-items: center; gap: 9px;
  padding: 13px 20px;
  padding-bottom: max(13px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--hairline);
  background: var(--glass);
  flex: none;
}
.sheet-foot-spacer { flex: 1 1 auto; }

/* Les codes de secours, à leur unique affichage : en grille, en chasse fixe et
   assez grands pour être recopiés à la main sans hésiter entre deux
   caractères — c'est souvent sur un papier qu'ils finissent. */
.recovery-codes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
  gap: 8px;
  margin-top: 6px;
}
.recovery-codes code {
  display: block;
  padding: 10px 6px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-align: center;
  user-select: all;
}

/* Quatre commandes ne tiennent pas sur 375 px : l'espaceur prend toute la
   largeur, ce qui coupe la barre en deux lignes nettes au lieu de laisser le
   bouton « Fermer » sortir de l'écran. */
@media (max-width: 860px) {
  .sheet-foot { flex-wrap: wrap; }
  .sheet-foot-spacer { flex: 1 0 100%; height: 0; }
}

/* Sur grand écran la feuille se centre en fenêtre plutôt que collée en bas. */
@media (min-width: 860px) and (min-height: 700px) {
  .sheet {
    bottom: auto; top: 50%;
    transform: translate(-50%, -50%);
    border-radius: var(--radius-xl);
    border-bottom: 1px solid var(--glass-border);
    max-height: 86vh;
    animation: sheet-in 0.36s var(--spring);
  }
  .sheet-grabber { display: none; }
  .sheet-head { padding-top: 17px; }

  /* La recherche n'est pas une feuille de réglages mais une palette : plus
     étroite — une ligne de résultat n'a pas besoin de 920 px —, un peu
     au-dessus du centre où le regard se porte, et haute de ce qu'elle a à
     montrer plutôt que des 86 % réservés aux formulaires. */
  #navSheet {
    width: min(620px, calc(100vw - 48px));
    top: 42%;
    max-height: min(600px, 74vh);
  }
  #navSheet .nav-foot { display: flex; }
  /* Le champ est le sujet de la palette : il en a la taille. */
  #navSheet .search input { height: 42px; font-size: 14.5px; padding-left: 39px; }
  #navSheet .search svg { left: 14px; width: 17px; height: 17px; }
}

/* ============================================================
   Détail produit
   ============================================================ */

.detail-hero { display: flex; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
.detail-photo {
  width: 148px; height: 148px; flex: none;
  border-radius: var(--radius-md);
  object-fit: cover;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
}
.detail-hero-body { flex: 1 1 260px; min-width: 0; }
.detail-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 9px; }

.section { margin-top: 22px; }
.section:first-child { margin-top: 0; }
/* Le contrôle logistique apparaît et disparaît au fil du recalcul à la frappe :
   la règle est posée ici plutôt que laissée à la feuille du navigateur. */
.section[hidden] { display: none; }
.section-title {
  display: flex; align-items: center; gap: 8px;
  font-size: 11px; font-weight: 660; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--text-3);
  margin-bottom: 10px;
}
/* Seul contexte à icône du projet sans cette règle : sans elle, le SVG prend
   sa taille intrinsèque par défaut du navigateur au lieu de 14px. */
.section-title svg { width: 14px; height: 14px; flex: none; }
.section-title::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--hairline); }

.grid-2 { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }

/* Cartes entrepôt/contact — listes imbriquées du formulaire client. */
.rel-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 10px; }
.rel-card {
  position: relative;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  padding: 14px 40px 14px 14px;
}
.rel-remove { position: absolute; top: 8px; right: 8px; }
.rel-empty { font-size: 12.5px; color: var(--text-3); margin: 0 0 10px; }
.rel-warehouse-picks { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 9px; }
.rel-pick {
  display: flex; align-items: center; gap: 6px;
  font-size: 12.5px;
  padding: 5px 10px 5px 8px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--surface);
  cursor: pointer;
  user-select: none;
}
.rel-pick input { accent-color: var(--accent); }

.kv {
  padding: 10px 13px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
}
.kv-key { font-size: 10.5px; font-weight: 620; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-3); }
.kv-val { font-size: 13.5px; font-weight: 520; margin-top: 3px; word-break: break-word; }
.kv-val.is-num { font-variant-numeric: tabular-nums; }

/* ---------- Journal ---------- */

/* Les compteurs du journal sont des boutons — chacun ouvre l'onglet qui le
   détaille —, mais gardent l'allure d'une case de lecture. */
button.journal-stat {
  display: block; width: 100%;
  font: inherit; color: inherit; text-align: left;
  cursor: pointer;
  transition: border-color var(--motion-fast) ease;
}
button.journal-stat:hover { border-color: var(--accent); }
.journal-stat .kv-key,
.journal-stat-val,
.journal-stat-sub { display: block; }
.journal-stat-val {
  margin-top: 4px;
  font-size: 22px; font-weight: 640; letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.journal-stat-sub { font-size: 11.5px; color: var(--text-3); margin-top: 1px; }
.journal-stat.is-alert .journal-stat-val { color: var(--red); }

/* Détail et erreur d'une ligne : une réponse SMTP peut être longue, elle se
   replie plutôt que d'étirer la colonne. */
.journal-detail { white-space: normal; overflow-wrap: anywhere; }
.journal-error { color: var(--red); font-weight: 560; }
/* Une adresse IPv6 compte 39 caractères : en chasse fixe et en 13 px, elle
   écrasait le reste de la ligne. */
.journal-ip { font-family: var(--mono); font-size: 12px; color: var(--text-2); white-space: nowrap; }

/* Le filtre par compte, dans la barre du journal : habillé, le menu prendrait
   toute la ligne — il garde la largeur d'un nom. */
.pf-select.journal-user { width: auto; flex: 0 1 220px; min-width: 160px; }
/* Sur téléphone, il prend la ligne, comme la recherche au-dessus de lui. */
@media (max-width: 860px) {
  .pf-select.journal-user { flex: 1 1 100%; }
}

/* ---------- Rôles ---------- */

/* Un droit par case : l'interrupteur, son nom, et ce qu'il engage. Deux
   colonnes dès que la place le permet — une grille de quatorze lignes d'un
   seul tenant se lit mal. */
.role-perms { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
.role-perm {
  display: flex; align-items: flex-start;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
}
.role-perm .switch-track { flex: none; margin-top: 1px; }
.role-perm .switch-text { display: flex; flex-direction: column; gap: 2px; }
.role-perm-hint { font-size: 11.5px; color: var(--text-3); }
.role-perm.is-locked { opacity: 0.7; cursor: not-allowed; }

/* ============================================================
   Fenêtre de confirmation

   Au-dessus de tout, feuilles comprises : une confirmation peut naître d'un
   bouton placé dans une feuille ouverte. Même verre que les feuilles, même
   voile — c'est la même famille d'objets.
   ============================================================ */

.dialog-layer {
  position: fixed; inset: 0; z-index: 300;
  display: grid; place-items: center;
  padding: 20px;
}
.dialog-scrim {
  position: absolute; inset: 0;
  background: rgba(8, 10, 14, 0.42);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  animation: scrim-in var(--motion-fast) ease-out both;
}
.dialog {
  position: relative;
  width: min(440px, 100%);
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  padding: 22px 22px 18px;
  border-radius: var(--radius-xl);
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur-heavy);
  backdrop-filter: var(--blur-heavy);
  border: 1px solid var(--glass-border);
  box-shadow: 0 24px 70px -16px rgba(0, 0, 0, 0.5), inset 0 1px 0 var(--glass-shine);
  animation: dialog-pop var(--motion-med) ease-out both;
}
.dialog-icon {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  margin-bottom: 12px;
  border-radius: 12px;
  background: var(--accent-soft); color: var(--accent);
}
.dialog-icon.is-danger { background: var(--red-soft); color: var(--red); }
.dialog-icon svg { width: 20px; height: 20px; }
.dialog-title { font-size: 16.5px; font-weight: 650; letter-spacing: -0.02em; line-height: 1.3; }
.dialog-text {
  margin-top: 7px;
  font-size: 13.5px; line-height: 1.5; color: var(--text-2);
  white-space: pre-line;
}
.dialog-list {
  margin: 12px 0 0; padding: 8px 12px;
  list-style: none;
  max-height: 170px; overflow-y: auto;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  font-size: 12.5px;
}
.dialog-list li + li { margin-top: 3px; }
.dialog-foot { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 9px; margin-top: 18px; }
@keyframes dialog-pop {
  from { opacity: 0; transform: translateY(8px) scale(0.98); }
  to { opacity: 1; transform: none; }
}

.bullets { margin: 0; padding-left: 0; list-style: none; display: flex; flex-direction: column; gap: 7px; }
.bullets li {
  display: flex; gap: 9px;
  font-size: 13.5px;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
}
.bullets li::before { content: "◆"; color: var(--accent); font-size: 9px; line-height: 1.7; flex: none; }
.bullets.is-benefit li::before { content: "✔"; color: var(--green); font-size: 11px; line-height: 1.35; }

/* ---------- Prix relevés chez les concurrents ----------
   Le prix et la page qui le porte ne se séparent jamais : dans le tableau
   comme dans la fiche, la ligne entière est un lien. Le montant reste en
   chiffres tabulaires pour que deux enseignes se comparent à la verticale. */

.prices { display: flex; flex-direction: column; gap: 2px; }
.price-link {
  display: flex; align-items: baseline; gap: 7px;
  font-size: 12.5px;
  color: inherit; text-decoration: none;
}
a.price-link:hover .price-shop { text-decoration: underline; }

.price-amount { font-variant-numeric: tabular-nums; font-weight: 620; white-space: nowrap; }
.price-amount.is-lg { font-size: 15.5px; margin-left: auto; }
.price-shop {
  color: var(--accent); font-weight: 540;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.price-more { font-size: 11.5px; color: var(--text-3); }

.price-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.price-list li {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
}
.price-row-main { flex: 1 1 200px; min-width: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.price-title {
  flex: 1 1 140px; min-width: 0;
  font-size: 12px; color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.store-block {
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.65;
  white-space: pre-wrap;
  padding: 14px;
  border-radius: var(--radius-md);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  max-height: 340px;
  overflow-y: auto;
}

/* ---------- Rapport de contrôle logistique ---------- */

.audit { display: flex; flex-direction: column; gap: 7px; }
.audit-line {
  display: flex; gap: 10px;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  line-height: 1.45;
  border: 1px solid transparent;
}
.audit-line svg { width: 15px; height: 15px; flex: none; margin-top: 1px; }
.audit-ok      { background: var(--green-soft);  color: var(--green); }
.audit-fixed   { background: var(--accent-soft); color: var(--accent); }
.audit-info    { background: var(--surface-2);   color: var(--text-2); border-color: var(--hairline); }
.audit-warning { background: var(--orange-soft); color: var(--orange); }
.audit-error   { background: var(--red-soft);    color: var(--red); }

/* ---------- Schéma de palette ---------- */

.pallet-viz { display: flex; gap: 16px; flex-wrap: wrap; align-items: flex-start; }
.pallet-svg {
  flex: none;
  border-radius: var(--radius-md);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  padding: 12px;
}
.pallet-stats { flex: 1 1 200px; display: grid; gap: 9px; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }

/* Vue de dessus : le viewBox porte les cotes de la palette en cm, le SVG
   s'adapte à la place disponible plutôt que d'imposer une largeur en pixels. */
.pallet-top { display: block; width: 232px; max-width: 100%; height: auto; }
.pallet-legend {
  display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 8px;
  font-size: 10.5px; color: var(--text-3);
}
.pallet-legend span { display: inline-flex; align-items: center; gap: 5px; }
.pallet-legend i {
  width: 11px; height: 11px; border-radius: 2px; border: 1px solid; flex: none;
}


/* ---------- Configurations de palette enregistrées ---------- */

/* La barre des configurations, au-dessus du reste de la page : choisir, nommer,
   enregistrer. Une rangée sur grand écran, une colonne sur téléphone — les
   boutons descendent sous les champs plutôt que de les comprimer. */
.pal-plans {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  margin-bottom: 18px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--hairline);
}
.pal-plans-pick { flex: 2 1 240px; margin: 0; }
.pal-plans-name { flex: 2 1 200px; margin: 0; }
.pal-plans-acts { flex: none; display: flex; gap: 8px; padding-bottom: 1px; }
/* La précision passe sous toute la rangée : intercalée entre deux champs, elle
   les aurait désalignés. */
.pal-plans-hint { flex: 1 0 100%; margin: 0; }

/* Cinq gabarits — Europe, demi, quart, US, personnalisée — ne tiennent pas sur
   une ligne d'un téléphone. Ce contrôle-ci passe donc à la ligne, et troque la
   pilule pour un rectangle arrondi : une pilule sur deux rangées se lit comme
   deux contrôles distincts. Les autres contrôles segmentés de l'application
   gardent leur forme, ils n'ont que deux ou trois options. */
#palPresetSeg {
  display: flex;
  flex-wrap: wrap;
  border-radius: var(--radius-md);
}
#palPresetSeg button { border-radius: calc(var(--radius-md) - 4px); }

/* ---------- Palette à plusieurs références ---------- */

/* Une référence ou un étage : un encadré par ligne, plutôt qu'une grille de
   champs. Au-delà de deux références, une grille continue ne laisse plus voir
   où finit l'une et où commence l'autre. */
.mix-row {
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  padding: 12px;
  margin-bottom: 10px;
}
.mix-row:last-child { margin-bottom: 0; }

.mix-row-head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 12px;
}
.mix-row-head .mix-label { flex: 2 1 160px; }
.mix-row-head .mix-pick  { flex: 3 1 200px; }
.mix-row-head .mix-h     { flex: 0 1 120px; margin: 0; }
.mix-row-head .mix-del   { flex: none; margin-left: auto; padding: 0 9px; }
.mix-row-head .mix-del svg { width: 14px; height: 14px; }

/* La pastille porte la lettre qui désigne la référence sur le schéma : c'est
   elle qui relie le formulaire au dessin, et elle tient sans la couleur. */
.mix-badge {
  flex: none; min-width: 26px; text-align: center;
  padding: 3px 7px; border-radius: 6px;
  border: 1px solid var(--hairline);
  font-size: 11.5px; font-weight: 700; letter-spacing: 0.02em;
}
.mix-badge.is-floor {
  background: var(--surface-3); color: var(--text-2);
  font-weight: 600; letter-spacing: 0;
}

.mix-seg { margin-bottom: 12px; }

.mix-check {
  display: flex; align-items: center; gap: 8px; margin-top: 10px;
  font-size: 12px; color: var(--text-2); cursor: pointer;
}
.mix-check input { flex: none; }

/* Le liseré reprend la teinte de la référence posée sur la zone : on lit
   l'affectation sans descendre jusqu'au schéma. */
.mix-zones { margin-top: 2px; }
.mix-zone { border-left: 3px solid var(--zone-col); padding-left: 9px; }

/* Zones ajustées aux cartons : leur nombre se règle à la main, d'où une croix
   par zone et un bouton d'ajout sous la grille. La croix reste discrète — on
   la cherche quand on en a besoin, elle ne doit pas concurrencer le menu. */
.mix-zone-head {
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  min-height: 16px;
}
.mix-zone-del {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; padding: 0; flex: none;
  border: 0; border-radius: 6px; background: transparent;
  color: var(--text-3); cursor: pointer;
}
.mix-zone-del:hover { background: var(--surface-3); color: var(--text); }
.mix-zone-del svg { width: 12px; height: 12px; }
.mix-zones-foot {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-top: 10px;
}
.mix-zones-foot .field-hint { flex: 1 1 260px; }

/* ---------- Palette à plusieurs références : le résultat ---------- */

.mix-res-grid {
  display: grid; gap: 10px; margin-top: 18px;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.mix-res {
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--ref-col);
  border-radius: var(--radius-md);
  background: var(--surface-2);
  padding: 11px 13px;
}
.mix-res-head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-bottom: 7px; font-size: 13px;
}
.mix-res-head .row-thumb,
.mix-res-head .row-thumb:hover { flex: none; border-radius: 7px; cursor: default; transform: none; }
.mix-res-line {
  display: flex; align-items: baseline; gap: 6px;
  font-size: 12px; color: var(--text-2); line-height: 1.55;
}
.mix-dot {
  width: 8px; height: 8px; border-radius: 2px; flex: none;
  transform: translateY(-1px);
}

.mix-floor-res {
  margin-top: 18px; padding-top: 14px;
  border-top: 1px solid var(--hairline);
}
/* ---------- Vue 3D ---------- */

.pal3d { margin-top: 14px; border-top: 1px solid var(--hairline); padding-top: 12px; }
.pal3d > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center; gap: 7px;
  font-size: 12.5px; font-weight: 600; color: var(--text-2); user-select: none;
}
.pal3d > summary::-webkit-details-marker { display: none; }
.pal3d > summary svg { width: 14px; height: 14px; flex: none; }
.pal3d > summary:hover { color: var(--text); }
.pal3d-body { margin-top: 12px; }
/* Largeur bornée : au-delà, le canevas s'étire sans que la palette y gagne —
   elle reste haute et étroite, et le rendu se contente de creuser des marges. */
.pal3d-canvas {
  display: block; width: 100%; max-width: 520px; margin: 0 auto; min-height: 240px;
  border-radius: var(--radius-md); border: 1px solid var(--hairline);
  background: var(--surface-2);
  /* Sans quoi le navigateur mobile interprète le glissement comme un défilement. */
  touch-action: none;
}
.pal3d-canvas:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Le bloc étant déplié d'emblée, le canevas s'efface tant qu'il n'y a rien à
   dessiner. La règle est explicite : le display:block ci-dessus l'emporterait
   sinon sur celui de l'attribut hidden. */
.pal3d-canvas[hidden] { display: none; }
.pal3d-foot {
  display: flex; align-items: center; gap: 10px; margin: 8px auto 0; max-width: 520px; flex-wrap: wrap;
}
.pal3d-foot .field-hint { flex: 1 1 200px; }

/* ============================================================
   Étiquettes palette

   Volontairement en noir sur blanc et en millimètres : la même mise en page
   sert l'aperçu à l'écran et la feuille A4. Une étiquette par page.
   ============================================================ */

.etiq-preview { display: flex; flex-direction: column; gap: 18px; }

/* Entête de contrôle, à l'écran seulement — voir renderEtiqLabels(). Il
   appartient à l'interface, pas à l'étiquette : il en reprend donc les
   couleurs, pas le noir sur blanc du document. */
.etiq-item { display: flex; flex-direction: column; gap: 8px; }
.etiq-caption {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  font-size: 12.5px; color: var(--text-3);
}
.etiq-caption-main { font-size: 13.5px; font-weight: 640; color: var(--text); }

.etiq {
  background: #fff; color: #000;
  font-family: Arial, Helvetica, sans-serif;
  border: 1.5px solid #000;
  width: 182mm; max-width: 100%;
  box-sizing: border-box;
}
/* En-tête centré : le logo occupe toute la largeur en haut, la raison sociale
   dessous, puis l'adresse. C'est la disposition qu'attend un cariste qui lit
   l'étiquette de loin — l'identité de l'expéditeur avant tout le reste. */
.etiq-head {
  display: flex; flex-direction: column; align-items: center; gap: 1.5mm;
  padding: 3.5mm 4mm; border-bottom: 1.5px solid #000; text-align: center;
}
.etiq-head img { max-height: 16mm; max-width: 70mm; object-fit: contain; }
.etiq-company { font-size: 14pt; font-weight: 700; line-height: 1.15; }
.etiq-address { font-size: 8pt; line-height: 1.35; }
.etiq-legal { font-size: 7pt; line-height: 1.3; }

.etiq-row { display: flex; border-bottom: 1px solid #000; }
.etiq-row:last-of-type { border-bottom: 0; }
.etiq-cell { padding: 2mm 2.5mm; border-right: 1px solid #000; min-width: 0; }
.etiq-cell:last-child { border-right: 0; }
/* Pas de mise en capitales : les libellés sont écrits tels qu'ils doivent
   paraître, et les exports Word et PDF n'ont pas de text-transform. Le seul
   moyen que les trois rendus disent exactement la même chose. */
.etiq-lbl { font-size: 6.5pt; font-weight: 700; letter-spacing: 0.03em; }
.etiq-val { font-size: 10pt; word-break: break-word; }
.etiq-val.mono { font-family: "Courier New", Courier, monospace; }
/* Le libellé et la donnée occupent chacun sa cellule : les deux se lisent en
   colonne d'une étiquette à l'autre, et la grille est celle des exports. */
.etiq-cell .etiq-lbl + .etiq-val { margin-top: 0.8mm; }
.etiq-sub { font-size: 8pt; font-weight: 400; }
.etiq-c-a { flex: 0 0 19.5%; }
.etiq-c-b { flex: 0 0 26.9%; }
.etiq-c-c { flex: 0 0 22.2%; }
.etiq-c-d { flex: 1 1 31.4%; }
.etiq-c-full { flex: 1 1 100%; }

/* Le destinataire occupe toute la hauteur du bloc de gauche : une colonne
   flexible remplace le rowspan d'un tableau, sans en avoir la rigidité. Les
   deux moitiés reprennent les largeurs du modèle — libellés et valeurs de
   gauche d'un côté, destinataire de l'autre. */
.etiq-split { display: flex; border-bottom: 1.5px solid #000; }
.etiq-split-left { flex: 0 0 46.4%; min-width: 0; border-right: 1px solid #000; }
/* Les deux colonnes du bloc de gauche sont exprimées en part de l'étiquette
   entière, comme dans les exports : ramenées à la largeur du bloc, elles
   valent 42 % et 58 % — sans quoi le libellé de la date passerait à la ligne. */
.etiq-split-left .etiq-c-a { flex: 0 0 42%; }
.etiq-split-left .etiq-c-b { flex: 1 1 58%; }
.etiq-split-left .etiq-row:last-of-type { border-bottom: 0; }
.etiq-split-right { flex: 1 1 53.6%; min-width: 0; display: flex; flex-direction: column; }
.etiq-split-right .etiq-cell { border-right: 0; }
.etiq-dest-lbl { border-bottom: 1px solid #000; }
.etiq-dest-cell { flex: 1 1 auto; display: flex; align-items: center; }
.etiq-dest { font-size: 10pt; line-height: 1.4; }
/* Les cases du numéro de palette portent leurs propres bordures. */
.etiq-pallet-cell { padding: 0; }

/* Numéro de palette : lisible à distance, c'est ce qu'un cariste cherche. */
.etiq-pallet { display: flex; align-items: stretch; }
.etiq-pallet-box {
  flex: 1 1 0; text-align: center; padding: 1mm;
  font-size: 26pt; font-weight: 700; line-height: 1.05;
  border-right: 1px solid #000;
}
.etiq-pallet-box:last-child { border-right: 0; }

.etiq-name {
  font-size: 13pt; font-weight: 700; text-align: center;
  padding: 3mm 2mm; border-bottom: 1px solid #000;
}

.etiq-bc {
  border-top: 1.5px solid #000; padding: 3mm 2mm;
  display: flex; flex-direction: column; align-items: center; gap: 1mm;
}
.etiq-bc-type { font-size: 7pt; font-weight: 700; text-transform: uppercase; }
/* Le texte lisible reste du texte, jamais une image : net à l'impression,
   et vérifiable à l'œil face au code scanné. */
.etiq-bc-hri { font-family: "Courier New", Courier, monospace; font-size: 8.5pt; }
.etiq-bc svg { display: block; width: 100%; max-width: 162mm; height: auto; }

#etiqPrintArea { display: none; }

@media print {
  /* Seules les étiquettes s'impriment : ni menu, ni formulaire, ni aperçu. */
  body > *:not(#etiqPrintArea) { display: none !important; }
  #etiqPrintArea { display: block !important; }

  @page { size: A4 portrait; margin: 14mm; }

  .etiq {
    width: auto; border-width: 1pt;
    /* Une palette par feuille : c'est ce qu'on colle sur le film. */
    page-break-after: always; break-after: page;
  }
  .etiq:last-child { page-break-after: auto; break-after: auto; }
  .etiq-bc svg { max-width: none; }
}

/* ============================================================
   Sélecteur de colonnes
   ============================================================ */

.col-groups { display: flex; flex-direction: column; gap: 18px; }
.col-group-title {
  font-size: 11px; font-weight: 660; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--text-3); margin-bottom: 8px;
}
.col-list { display: flex; flex-direction: column; gap: 5px; }
.col-item {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  font-size: 13.5px;
  transition: background 0.16s;
}
.col-item:hover { background: var(--surface-3); }
.col-item-name { flex: 1 1 auto; min-width: 0; }
.col-move { display: flex; gap: 3px; flex: none; }
.col-move button {
  width: 26px; height: 26px;
  border-radius: 7px;
  display: grid; place-items: center;
  color: var(--text-3);
  transition: background 0.16s, color 0.16s;
}
.col-move button:hover:not(:disabled) { background: var(--surface); color: var(--text); }
.col-move button:disabled { opacity: 0.28; cursor: not-allowed; }
.col-move svg { width: 14px; height: 14px; }

/* ============================================================
   Caméra
   ============================================================ */

.cam-stage {
  position: relative;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #000;
  aspect-ratio: 4 / 3;
}
.cam-stage video, .cam-stage img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cam-frame {
  position: absolute; inset: 9%;
  border-radius: 18px;
  border: 2px solid rgba(255, 255, 255, 0.5);
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.18);
  pointer-events: none;
}
.cam-tip {
  position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%);
  padding: 6px 13px;
  border-radius: 999px;
  font-size: 12px;
  color: #fff;
  background: rgba(0, 0, 0, 0.5);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  white-space: nowrap;
}
.cam-shutter {
  width: 62px; height: 62px;
  border-radius: 50%;
  background: #fff;
  border: 4px solid rgba(255, 255, 255, 0.35);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.3);
  transition: transform 0.16s var(--spring);
  margin: 0 auto;
  display: block;
}
.cam-shutter:active { transform: scale(0.9); }

/* ============================================================
   Aperçu photo plein écran
   ============================================================ */

.lightbox {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  padding: 26px;
  background: rgba(5, 7, 11, 0.82);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  animation: fade-in 0.22s ease;
  cursor: zoom-out;
}
.lightbox[hidden] { display: none; }
.lightbox img {
  max-width: min(560px, calc(100vw - 48px));
  max-height: min(68vh, 480px);
  object-fit: contain;
  border-radius: var(--radius-lg);
  box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6);
  animation: lightbox-image-in var(--motion-fast) ease-out both;
}

/* ============================================================
   Tableaux en cartes — écrans étroits

   Neuf colonnes sur 375 px ne se lisent pas : on défile latéralement, on perd
   la ligne, on renonce. Chaque ligne devient donc une carte, et seules les
   cellules marquées `data-card` y paraissent — les autres restent dans le
   balisage pour l'écran large, mais disparaissent ici. Ce qui compte tient
   sur deux lignes ; le reste s'ouvre dans la fiche.

   Un tableau façon tableur (.is-grid — celui du rétroplanning) y échappe : ses
   colonnes sont le propos, et une carte ne se parcourt pas cellule par
   cellule. Il défile latéralement, ses premières colonnes figées.
   ============================================================ */

@media (max-width: 860px) {
  .table-scroll:not(.is-grid) {
    overflow: visible;
    max-height: none;
    border: 0;
    background: none;
    border-radius: 0;
  }
  .table-scroll-top { display: none !important; }

  .table-scroll:not(.is-grid) table { display: block; width: auto; }
  .table-scroll:not(.is-grid) thead { display: none; }
  .table-scroll:not(.is-grid) tbody { display: flex; flex-direction: column; gap: 8px; }

  .table-scroll:not(.is-grid) tr {
    display: grid;
    grid-template-columns: auto auto minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 10px; row-gap: 5px;
    padding: 11px 12px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-md);
    background: var(--surface);
  }
  .table-scroll:not(.is-grid) tr.is-selected { background: var(--accent-soft); border-color: transparent; }
  .table-scroll:not(.is-grid) tbody tr:last-child td { border-bottom: 0; }

  .table-scroll:not(.is-grid) td {
    display: block;
    border: 0;
    padding: 0;
    max-width: none;
    white-space: normal;
  }
  /* Les colonnes figées n'ont plus de sens sans défilement latéral : le style
     inline qui les positionne doit céder. */
  .table-scroll:not(.is-grid) .col-pin-1,
  .table-scroll:not(.is-grid) .col-pin-2,
  .table-scroll:not(.is-grid) .col-pin-3 { position: static; left: auto !important; width: auto; background: none; }

  .table-scroll:not(.is-grid) td:not([data-card]) { display: none; }
  /* La ligne est explicite : la méta occupant toute la largeur, une cellule
     sans rangée désignée irait se placer sous elle. */
  .table-scroll:not(.is-grid) td[data-card="pick"]    { grid-column: 1; grid-row: 1; }
  .table-scroll:not(.is-grid) td[data-card="media"]   { grid-column: 2; grid-row: 1; }
  .table-scroll:not(.is-grid) td[data-card="head"]    { grid-column: 3; grid-row: 1; min-width: 0; }
  .table-scroll:not(.is-grid) td[data-card="actions"] { grid-column: 4; grid-row: 1; }
  /* Un tableau sans case ni vignette — comptes, journal — : sa cellule de tête
     reprend la place des deux colonnes vides, et leurs gouttières avec. */
  .table-scroll:not(.is-grid).is-textual td[data-card="head"] { grid-column: 1 / 4; }
  .table-scroll:not(.is-grid) td[data-card="meta"] {
    grid-column: 1 / -1; grid-row: 2;
    font-size: 12px; line-height: 1.45; color: var(--text-2);
  }
  /* Les fiches d'une commande dépliée : pleine largeur, sous la carte. Sans
     rangée désignée, la règle « td sans data-card » les ferait disparaître —
     une commande dépliée n'aurait plus rien à montrer sur mobile. */
  .table-scroll:not(.is-grid) td[data-card="sheets"] { grid-column: 1 / -1; grid-row: 3; }
  .table-scroll:not(.is-grid) td[data-card="meta"]:empty { display: none; }
  .table-scroll:not(.is-grid) .row-actions { gap: 0; }
  /* Le pied d'une carte porte parfois quatre boutons : ils passent à la
     ligne plutôt que de sortir de l'écran. */
  .card-foot { flex-wrap: wrap; }
  /* La cellule de tête peut porter plusieurs lignes : elles s'y empilent. */
  .table-scroll:not(.is-grid) td[data-card="head"] .cell-clamp { -webkit-line-clamp: 2; }
}

/* La colonne de méta ne sert qu'aux cartes : sur grand écran, elle n'existe
   que dans le balisage. */
td[data-card="meta"], th[data-card="meta"] { display: none; }
@media (max-width: 860px) {
  td[data-card="meta"] { display: block; }
}

/* ============================================================
   Notifications
   ============================================================ */

.toasts {
  position: fixed;
  bottom: max(20px, env(safe-area-inset-bottom));
  left: 50%; transform: translateX(-50%);
  z-index: 300;
  display: flex; flex-direction: column; gap: 9px;
  align-items: center;
  pointer-events: none;
  width: min(460px, calc(100vw - 26px));
}
.toast {
  display: flex; align-items: center; gap: 10px;
  padding: 11px 17px;
  border-radius: 999px;
  font-size: 13.5px; font-weight: 520;
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur-heavy);
  backdrop-filter: var(--blur-heavy);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-shine);
  animation: toast-in 0.4s var(--spring);
  pointer-events: auto;
  max-width: 100%;
}
.toast.is-leaving { animation: toast-out 0.28s ease forwards; }
.toast svg { width: 17px; height: 17px; flex: none; }
.toast-ok    svg { color: var(--green); }
.toast-warn  svg { color: var(--orange); }
.toast-error svg { color: var(--red); }
.toast span { min-width: 0; }

/* ============================================================
   Bandeau de configuration manquante
   ============================================================ */

.banner {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 14px 17px;
  border-radius: var(--radius-lg);
  background: var(--orange-soft);
  border: 1px solid color-mix(in srgb, var(--orange) 32%, transparent);
  font-size: 13.5px;
  color: var(--text);
}
.banner svg { width: 19px; height: 19px; color: var(--orange); flex: none; margin-top: 1px; }
.banner code {
  font-family: var(--mono); font-size: 12.5px;
  padding: 1px 5px; border-radius: 5px;
  background: rgba(0, 0, 0, 0.08);
}
:root[data-theme="dark"] .banner code { background: rgba(255, 255, 255, 0.12); }

/* Le bandeau qui renseigne au lieu d'avertir : « ces champs viennent de telle
   fiche ». L'orange est réservé à ce qui bloque — l'employer ici ferait crier
   au loup sur une information neutre. */
.banner-info {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 28%, transparent);
}
.banner-info svg { color: var(--accent); }
.banner-body { flex: 1 1 auto; min-width: 0; }

/* ============================================================
   Animations
   ============================================================ */

@keyframes spin      { to { transform: rotate(360deg); } }
@keyframes fade-in   { from { opacity: 0; } to { opacity: 1; } }
@keyframes row-in    { from { opacity: 0; transform: translateY(-7px); } to { opacity: 1; transform: none; } }
@keyframes sheet-up  { from { transform: translate(-50%, 100%); } to { transform: translate(-50%, 0); } }
@keyframes sheet-in  { from { opacity: 0; transform: translate(-50%, -50%) scale(0.96); }
                       to   { opacity: 1; transform: translate(-50%, -50%) scale(1); } }
@keyframes toast-in  { from { opacity: 0; transform: translateY(16px) scale(0.94); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateY(10px) scale(0.96); } }

/* La feuille centrée sur grand écran n'utilise pas la même transformation :
   sheet-in est réécrite dans la requête média correspondante. */
@media (max-width: 859px) {
  @keyframes sheet-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* ============================================================
   Écran de connexion
   ============================================================ */

.login-wrap { min-height: 100vh; display: grid; place-items: center; padding: 26px; }
.login { width: min(390px, 100%); padding: 30px; text-align: center; }
.login .brand-mark { width: 52px; height: 52px; border-radius: 16px; margin: 0 auto 17px; }
.login .brand-mark svg { width: 27px; height: 27px; }
.login-title { font-size: 20px; font-weight: 680; letter-spacing: -0.025em; }
.login-sub { font-size: 13.5px; color: var(--text-3); margin-top: 5px; }
.login form { margin-top: 22px; display: flex; flex-direction: column; gap: 11px; }
.login-error { font-size: 13px; color: var(--red); text-align: left; }
.login-notice { font-size: 13px; color: var(--text-2); text-align: left; }

/* Dispense de second facteur : cochée d'avance, mais lisible et décochable —
   c'est le seul endroit où l'on choisit de faire confiance à un poste. */
.login-trust {
  display: flex; align-items: flex-start; gap: 9px;
  font-size: 13px; color: var(--text-2); text-align: left; cursor: pointer;
}
.login-trust input { margin: 2px 0 0; flex: none; accent-color: var(--accent); }
.login-trust-hint {
  font-size: 11.5px; color: var(--text-3); text-align: left;
  margin: -4px 0 0 25px;
}

.code-input {
  text-align: center;
  font-family: var(--mono);
  font-size: 24px;
  font-weight: 650;
  letter-spacing: 0.35em;
  padding-left: 8px; /* compense visuellement le tracking pour un centrage propre */
}

.login-links {
  margin-top: 16px;
  display: flex; align-items: center; justify-content: center; gap: 9px 9px;
  flex-wrap: wrap;
  font-size: 12.5px;
}
/* « Renvoyer le code » est un bouton, donc un formulaire à lui seul, au milieu
   de liens qui n'en sont pas. Sans cette ligne il hérite du « margin-top » de
   « .login form » et retombe d'un cran sous ses voisins ; display:contents
   efface la boîte du formulaire, et le bouton s'aligne comme un lien. */
.login-links form { display: contents; }
/* Deux liens longs ne tiennent pas côte à côte sur un téléphone : sans
   « nowrap » ils se coupent en plein milieu (« Utiliser un code de / secours »).
   Insécables, ils passent à la ligne entiers. */
.login-link {
  color: var(--accent);
  font-weight: 550;
  background: none;
  text-decoration: none;
  white-space: nowrap;
}
.login-link:hover { text-decoration: underline; }
.login-links-sep { color: var(--text-3); }

/* ============================================================
   Utilitaires et responsive
   ============================================================ */

.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;
}
.hide { display: none !important; }
.mono { font-family: var(--mono); }
.nowrap { white-space: nowrap; }
.muted { color: var(--text-3); }

@media (max-width: 900px) {
  .brand-sub { display: none; }
  .workbar-engine { display: none; }
  .meter-label { display: none; }
  .meter { padding: 6px 8px; }
  .btn-label-wide { display: none; }
  /* Sans place pour l'annoncer, la recherche redevient une loupe — et le
     raccourci clavier n'a de toute façon pas de sens sans clavier. */
  .workbar-search {
    min-width: 0; width: 34px;
    padding: 0; justify-content: center;
  }
  .workbar-search-label, .workbar-search-kbd { display: none; }
}

/* ---------- Écrans étroits : formulaires et feuilles ----------
   Sur 375 px, chaque marge se paie en largeur de champ. On resserre le cadre,
   pas le contenu : les zones de saisie gardent leur hauteur, seules les
   gouttières maigrissent. */

@media (max-width: 860px) {
  .card { padding: 14px; }
  .drop { padding: 26px 14px; }
  .detail-photo { width: 100%; height: 190px; }

  /* Les vingt pixels de chaque côté d'une feuille valent deux caractères de
     plus par ligne de saisie. */
  .sheet-body { padding: 14px 14px 20px; }
  .sheet-head { padding: 12px 14px; }
  .sheet-foot { padding: 12px 14px; padding-bottom: max(12px, env(safe-area-inset-bottom)); }

  /* Un titre de fiche peut être long : deux lignes, puis on coupe, pour que la
     croix de fermeture reste à sa place. */
  .sheet-title {
    font-size: 15.5px;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
  }

  /* Une fiche produit fait plusieurs milliers de pixels de haut : le titre de
     section reste visible pendant qu'on la parcourt, sans quoi on ne sait plus
     ce qu'on lit. */
  .sheet-body .section-title {
    position: sticky; top: -14px; z-index: 2;
    padding: 6px 0;
    margin-left: -14px; margin-right: -14px;
    padding-left: 14px; padding-right: 14px;
    background: var(--glass-strong);
    -webkit-backdrop-filter: var(--blur-panel); backdrop-filter: var(--blur-panel);
  }

  .rel-card { padding: 12px 38px 12px 12px; }
  .section { margin-top: 18px; }
}

@media (max-width: 620px) {
  body { font-size: 14.5px; }
  .table-scroll { max-height: none; }
  tbody td { max-width: none; }
}

/* ============================================================
   Motion pass — rapide, sobre et cohérent
   ============================================================ */

:root {
  --motion-fast: 140ms;
  --motion-med: 220ms;
}

/* Une seule courbe, sans rebond : les changements restent perceptibles sans
   ralentir la consultation de données. */
.card {
  transition: box-shadow var(--motion-med) ease;
}
.card:hover { transform: none; }

.sidebar-link {
  transition: background var(--motion-fast) ease, color var(--motion-fast) ease;
}
.sidebar-link:hover:not(.is-disabled) { transform: none; }

.btn {
  transition: transform var(--motion-fast) ease, background var(--motion-fast) ease,
              box-shadow var(--motion-fast) ease, opacity var(--motion-fast) ease;
}
.btn:hover:not(:disabled) { transform: translateY(-1px); }
.btn:active:not(:disabled) { transform: translateY(0); }

/* Les changements de page ne déplacent pas le contenu : le fondu évite tout
   décalage du tableau pendant le rendu de la page suivante. */
.page-enter { animation: page-arrive var(--motion-med) ease-out both; }
.page-enter .card { animation: none; }
.page-exit { animation: none; pointer-events: none; }

/* Les lignes se renouvellent souvent après une recherche ou une correction :
   les animer à chaque rendu donnait une impression de clignotement. */
.mesh, tbody tr { animation: none; }

.scrim { animation: scrim-in var(--motion-fast) ease-out both; }
.lightbox {
  background: rgba(8, 10, 14, 0.42);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  animation: scrim-in var(--motion-fast) ease-out both;
}
.lightbox img { animation: lightbox-image-in var(--motion-fast) ease-out both; }
.sheet { animation: sheet-pop var(--motion-med) ease-out both; }
@media (min-width: 860px) and (min-height: 700px) {
  .sheet { animation: sheet-pop-desktop var(--motion-med) ease-out both; }
}
.toast { animation: toast-pop var(--motion-med) ease-out both; }
.toast.is-leaving { animation: toast-dismiss var(--motion-fast) ease-in forwards; }

tbody tr { transition: background var(--motion-fast) ease; }
tbody tr:hover { transform: none; box-shadow: none; }
.row-thumb, .view-thumb { transition: none; }
.row-thumb:hover, .view-thumb:hover { transform: none; }

/* Le choix de langue conserve la lecture immédiate d'un segment, puis
   confirme le nouveau choix par une brève apparition, au même rythme que
   le curseur des interrupteurs de colonnes. */
.switch-track { transition: background var(--motion-fast) ease, border-color var(--motion-fast) ease; }
.switch-thumb { transition: transform var(--motion-fast) ease; }
.segmented button[aria-selected="true"] { animation: segment-select var(--motion-fast) ease-out; }

@keyframes page-arrive {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes scrim-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes lightbox-image-in {
  from { opacity: 0; transform: scale(0.985); }
  to { opacity: 1; transform: scale(1); }
}
@keyframes sheet-pop {
  from { opacity: 0; transform: translateX(-50%) translateY(14px); }
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes sheet-pop-desktop {
  from { opacity: 0; transform: translate(-50%, calc(-50% + 10px)); }
  to { opacity: 1; transform: translate(-50%, -50%); }
}
@keyframes toast-pop {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes toast-dismiss {
  to { opacity: 0; transform: translateY(6px); }
}
@keyframes segment-select {
  from { opacity: 0.72; box-shadow: none; }
  to { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .card:hover,
  .row-thumb:hover,
  .view-thumb:hover,
  .btn:hover:not(:disabled),
  .btn:active:not(:disabled),
  .sidebar-link:hover:not(.is-disabled) {
    transform: none;
  }
}

/* ------------------------------------------------------------
   Prix et devise (fiche commande, fiche produit)

   Le montant d'abord : il ne descend jamais sous la largeur d'un prix
   lisible. La devise et le bouton de conversion forment un bloc d'un seul
   tenant (.price-unit) ; quand la colonne est trop étroite pour loger les
   deux — la fiche commande aligne jusqu'à sept champs par rangée —, ce bloc
   passe sous le montant plutôt que de l'écraser. Une grille à trois colonnes
   faisait l'inverse : le montant payait seul chaque pixel donné à la devise,
   et le prix devenait illisible dès que le drapeau est arrivé.
   ------------------------------------------------------------ */
.price-row {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px;
}
.price-row > .input { flex: 1 1 6.5rem; min-width: 6.5rem; }
.price-unit { display: flex; align-items: center; gap: 6px; flex: none; }
/* Drapeau, trois lettres et chevron, au plus juste. */
.price-unit .price-currency { flex: none; width: 6rem; }
select.price-currency { padding-inline: 8px; }
.price-unit .pf-select-btn { gap: 6px; padding-left: 9px; padding-right: 24px; }
.price-unit .pf-select-btn > svg { right: 6px; }

/* La contrevaleur en euros, sous le prix : une lecture, pas une saisie. */
[data-eur]:empty { display: none; }
[data-eur] { color: var(--text-3); }
[data-eur] strong { color: var(--text-2); font-weight: 600; }

/* Le menu « … » d'une ligne de liste avait ici son propre dessin. Il n'en a
   plus : c'est le panneau flottant commun (.pf-menu, plus haut) qui le porte,
   comme les listes déroulantes. */

/* ------------------------------------------------------------
   Documents d'une commande

   Une ligne par document : ce qu'il est à gauche, ce qu'on peut en faire à
   droite. Sous 860 px les boutons passent sous l'intitulé plutôt que de le
   comprimer — un nom de document tronqué ne se devine pas.
   ------------------------------------------------------------ */
.doc-row {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 0;
  border-top: 1px solid var(--hairline);
}
.doc-row:first-of-type { border-top: 0; }
.doc-name { flex: 1 1 16rem; min-width: 0; }

/* Le picto de la forme du fichier, en tête de ligne : c'est lui qui se repère
   avant qu'on lise le nom, et qui distingue un classeur d'un document Word. */
.doc-icon {
  flex: none;
  display: grid; place-items: center;
  width: 38px; height: 38px;
  border-radius: 10px;
  background: var(--surface-2);
  color: var(--text-2);
}
.doc-icon svg { width: 20px; height: 20px; }

/* Ce que le document couvre — une fiche, ou un n° de commande fournisseur.
   Dit sous le nom plutôt que sur le bouton : c'est la nature du document, pas
   ce que fait le clic. */
.doc-scope {
  margin-top: 3px;
  font-size: 11.5px; font-weight: 560;
  color: var(--text-3);
}

/* L'aperçu occupe la feuille : un PDF regardé dans un timbre-poste n'apprend
   rien, et c'est justement pour ne pas l'ouvrir ailleurs qu'on le montre. */
.doc-preview-frame {
  display: block;
  width: 100%;
  height: min(70vh, 900px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-lg);
  background: var(--surface-2);
}

@media (max-width: 860px) {
  .doc-row > .row-actions { flex: 1 1 100%; }
}

/* Les actions d'une fiche, en bout de carte dépliée : posées en haut à droite
   plutôt que dans le flux, pour ne pas décaler la grille à deux colonnes qui
   porte déjà la fiche et ses jalons. */
.rel-card.is-clickable { cursor: pointer; }
.rel-card-actions { position: absolute; top: 8px; right: 8px; }
/* La case de sélection, en tête de carte — à la place qu'occupe la case d'une
   ligne de tableau, pour que le geste soit le même d'un écran à l'autre. */
.rel-card-pick { position: absolute; top: 14px; left: 14px; }
/* La place de la case est réservée par une classe, et non par « :has » : le
   décalage doit tenir même là où ce sélecteur n'existe pas encore. */
.rel-card.has-pick { padding-left: 42px; }
.rel-card.is-selected { border-color: var(--accent); background: var(--accent-soft); }
/* La carte réserve leur place : son padding droit ne ménageait qu'un bouton,
   celui de retrait d'un entrepôt. Trois y tiennent maintenant. */
.rel-card[data-sheet-card] { padding-right: 124px; }
@media (max-width: 860px) {
  /* Sous 860 px la carte se resserre : les boutons reviennent dans le flux
     plutôt que de recouvrir le nom du produit. */
  .rel-card[data-sheet-card] { padding-right: 14px; }
  .rel-card-actions { position: static; justify-content: flex-end; margin-top: 8px; }
}

/* Le produit et son client, chacun avec son image.
   La vignette garde sa taille, le texte prend le reste et se tronque plutôt
   que de pousser la colonne — un tableau de commandes en a huit. */
.order-client { display: flex; align-items: center; gap: 9px; min-width: 0; }
.sheet-card-head { display: flex; align-items: flex-start; gap: 11px; min-width: 0; }
.sheet-card-head > .row-thumb { flex: none; }

/* ============================================================
   Rétroplanning

   Deux vues d'un même jeu de fiches (renderPlanGantt et renderPlanGrid,
   app.js). Les couleurs d'étape passent par deux variables, --tone et
   --tone-soft, posées par une classe tone-… : la légende, les barres, les
   jalons et le panneau de dates en tirent tous la leur, et une étape ne peut
   donc pas avoir deux couleurs selon l'endroit où on la regarde.
   ============================================================ */

.tone-violet { --tone: var(--violet); --tone-soft: var(--violet-soft); }
.tone-accent { --tone: var(--accent); --tone-soft: var(--accent-soft); }
.tone-green  { --tone: var(--green);  --tone-soft: var(--green-soft); }
.tone-orange { --tone: var(--orange); --tone-soft: var(--orange-soft); }
.tone-red    { --tone: var(--red);    --tone-soft: var(--red-soft); }
.tone-mute   { --tone: var(--text-3); --tone-soft: var(--surface-3); }

/* ---------- Barre d'outils et filtres ---------- */

.plan-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
.plan-nav { display: flex; align-items: center; gap: 2px; min-width: 0; }
.plan-nav .btn-icon { width: 32px; height: 32px; }
.plan-nav .btn-icon svg { width: 16px; height: 16px; }
.plan-nav > .btn-sm { margin: 0 2px; }

.plan-period-wrap { position: relative; display: inline-flex; margin-left: 6px; }
.plan-period {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 3px 10px; border-radius: var(--radius-sm);
  text-align: left;
  transition: background 0.16s;
}
.plan-period:hover { background: var(--surface-2); }
.plan-period > svg { width: 16px; height: 16px; color: var(--text-3); flex: none; }
.plan-period-label { display: flex; flex-direction: column; line-height: 1.2; min-width: 132px; }
.plan-period-label b { font-size: 15px; font-weight: 650; letter-spacing: -0.015em; }
.plan-period-label small { font-size: 11.5px; color: var(--text-3); white-space: nowrap; }
/* Le champ ne se voit pas : il ne sert qu'à ouvrir le calendrier du système,
   posé sous le titre de la période. */
.plan-jump {
  position: absolute; left: 10px; bottom: 0;
  width: 1px; height: 1px; padding: 0; border: 0;
  opacity: 0; pointer-events: none;
}

/* ---------- Le zoom de la frise ----------
   Un groupe d'un seul tenant — moins, le cran, plus, ajuster — plutôt que
   quatre boutons épars : ils agissent tous sur la même chose, et le cran
   affiché au milieu est à la fois l'état et la commande. */

.plan-zoom {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 2px; border-radius: 999px;
  background: var(--surface-3); border: 1px solid var(--hairline);
}
.plan-zoom .btn-icon { width: 28px; height: 28px; border-radius: 999px; }
.plan-zoom .btn-icon svg { width: 15px; height: 15px; }
.plan-zoom .btn-icon:disabled { opacity: 0.3; cursor: default; }
.plan-zoom-level {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  min-width: 108px; padding: 3px 10px 4px;
  border-radius: 999px; background: var(--surface);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
  transition: background 0.16s;
}
.plan-zoom-level:hover { background: var(--surface-2); }
.plan-zoom-name { font-size: 12.5px; font-weight: 600; color: var(--text); white-space: nowrap; }
.plan-zoom-name .is-short { display: none; }
/* Les barreaux de l'échelle : où l'on en est, et ce qu'il reste de chaque
   côté. Le cran courant est plus haut que les autres, et non d'une autre
   couleur : sur une palette monochrome, la hauteur se lit de plus loin. */
.plan-zoom-rungs { display: flex; align-items: flex-end; gap: 2px; height: 6px; }
.plan-zoom-rungs i {
  width: 3px; height: 3px; border-radius: 1px;
  background: color-mix(in srgb, var(--text-3) 60%, transparent);
}
.plan-zoom-rungs i.is-on { height: 6px; background: var(--accent); }

.plan-filters { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.plan-filters .search { flex: 1 1 240px; max-width: 360px; }
.pf-select.plan-filter { width: auto; flex: 0 1 200px; min-width: 150px; }
.plan-cascade { margin-left: 4px; }
/* « Filtres » ne sert que sur un téléphone, où les listes se replient. */
.plan-filters-toggle { display: none; }
.plan-filters-toggle[aria-expanded="true"] { background: var(--surface-3); }
.plan-filters-count {
  display: inline-grid; place-items: center;
  min-width: 18px; height: 18px; padding: 0 5px;
  border-radius: 999px;
  background: var(--accent); color: var(--accent-text);
  font-size: 11px; font-weight: 650; font-variant-numeric: tabular-nums;
}
.plan-filters-count[hidden] { display: none; }

/* ---------- Légende, qui sert aussi d'interrupteurs ---------- */

.plan-legend { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.plan-legend-chips { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; min-width: 0; }
.plan-chip {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 11px 4px 9px;
  border-radius: 999px;
  border: 1px solid var(--hairline);
  background: var(--surface);
  font-size: 12px; color: var(--text-3);
  transition: border-color 0.16s, color 0.16s, background 0.16s;
}
.plan-chip:hover { border-color: var(--text-3); }
.plan-chip.is-on { color: var(--text); background: var(--surface-2); }
.plan-chip:not(.is-on) .plan-swatch { opacity: 0.3; }

.plan-swatch {
  display: inline-block; flex: none;
  width: 18px; height: 8px; border-radius: 3px;
  background: var(--tone);
}
.plan-swatch.is-mark { width: 9px; height: 9px; border-radius: 2px; transform: rotate(45deg); }
.plan-swatch.is-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--surface); border: 2px solid var(--text-2); }
.plan-swatch.is-link { height: 0; border-radius: 0; background: none; border-top: 2px dashed var(--tone); }
.plan-swatch.is-hatch {
  background: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--warn) 65%, transparent) 0 2px, transparent 2px 5px);
  border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent);
}

/* ---------- Gantt ---------- */

/* Deux voies dans chaque ligne : les jalons en haut, les barres en bas. Sur
   une seule, le losange du closing tombait pile sur le début du transport et
   en mangeait l'intitulé — et le bord qu'on attrape pour l'ajuster. */
.gantt {
  --gantt-label: 290px;
  --row-h: 48px;
  --bar-h: 22px;
  --lane-bar: 31px;   /* le milieu des barres, depuis le haut de la ligne */
  --lane-ms: 11px;    /* le milieu des jalons */
  --ms-size: 11px;
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md);
  background: var(--surface);
  overflow: hidden;
}
/* Un seul conteneur défile dans les deux sens : intitulés collés à gauche,
   échelle collée en haut, et rien à synchroniser à la main. */
.gantt-scroll {
  position: relative;
  overflow: auto;
  max-height: 70vh;
  overscroll-behavior: contain;
  /* Deux doigts zooment la frise (app.js, « Gantt — le pincement ») : sans
     cela, ils agrandiraient la page entière. Un doigt, lui, fait toujours
     défiler. */
  touch-action: pan-x pan-y;
}
/* Pendant le pincement, la frise s'étire d'une seule transformation, posée
   sur ce qui porte des dates et jamais sur les intitulés. Le vrai dessin
   vient au lâcher : le texte étiré ne dure que le temps du geste. */
.gantt.is-pinching .gantt-scale,
.gantt.is-pinching .gantt-grid,
.gantt.is-pinching .gantt-track {
  transform: translateX(var(--pinch-x, 0px)) scaleX(var(--pinch-k, 1));
  transform-origin: 0 0;
}
.gantt-scroll.is-panning { cursor: grabbing; user-select: none; }
.gantt-canvas { position: relative; min-width: 100%; }

.gantt-head {
  position: sticky; top: 0; z-index: 6;
  display: flex;
  background: var(--surface-2);
  border-bottom: 1px solid var(--hairline);
}
.gantt-corner {
  position: sticky; left: 0; z-index: 7;
  flex: none; width: var(--gantt-label);
  display: flex; align-items: flex-end;
  padding: 0 12px 7px;
  background: var(--surface-2);
  border-right: 1px solid var(--hairline);
  font-size: 11px; font-weight: 640; letter-spacing: 0.035em; text-transform: uppercase;
  color: var(--text-2);
}
.gantt-scale { position: relative; flex: none; }
.gantt-tier { position: relative; height: 22px; }
.gantt-tier:first-child { height: 26px; }
.gantt-tier + .gantt-tier { border-top: 1px solid var(--hairline); }
.gantt-tick {
  position: absolute; top: 0; bottom: 0;
  display: flex; align-items: center;
  border-left: 1px solid var(--hairline);
  font-size: 11.5px; color: var(--text-2);
  white-space: nowrap;
  /* clip et non hidden : hidden ferait de la case un conteneur de
     défilement, et l'intitulé ne pourrait plus rester collé au bord. */
  overflow: clip;
}
/* L'intitulé d'un mois commencé plus tôt reste lisible au bord gauche. */
.gantt-tick > span { position: sticky; left: calc(var(--gantt-label) + 4px); padding: 0 6px; }
.gantt-tier:first-child .gantt-tick { font-size: 12.5px; font-weight: 630; color: var(--text); }
.gantt-tier.is-day .gantt-tick { justify-content: center; font-size: 11px; font-variant-numeric: tabular-nums; }
.gantt-tier.is-day .gantt-tick > span { position: static; padding: 0; }
.gantt-tick.is-weekend { background: var(--surface-3); color: var(--text-3); }
.gantt-tick.is-today { background: var(--red); color: #fff; font-weight: 650; }
.gantt-tick.is-zoomable { cursor: zoom-in; transition: background 0.14s, color 0.14s; }
.gantt-tick.is-zoom-out { cursor: zoom-out; }
.gantt-tick.is-recenter { cursor: pointer; }
.gantt-tick.is-zoomable:hover { background: var(--accent-soft); color: var(--accent); }
/* Aujourd'hui, dans l'échelle : le trait de la frise continue jusqu'en haut,
   et une pointe au pied de l'échelle le désigne. Une étiquette écrite aurait
   recouvert l'intitulé du mois ou du trimestre. */
.gantt-now-mark {
  position: absolute; top: 0; bottom: 0; z-index: 1;
  width: 2px; margin-left: -1px;
  background: color-mix(in srgb, var(--red) 55%, transparent);
  pointer-events: none;
}
.gantt-now-mark::after {
  content: ""; position: absolute; left: 50%; bottom: 0;
  margin-left: -5px;
  border: 5px solid transparent; border-bottom-color: var(--red);
}

.gantt-body { position: relative; }
.gantt-grid {
  position: absolute; top: 0; bottom: 0; left: var(--gantt-label);
  z-index: 0;
  pointer-events: none;
}
.gantt-line { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--hairline); opacity: 0.5; }
.gantt-line.is-major { opacity: 1; }
.gantt-weekend { position: absolute; top: 0; bottom: 0; z-index: -1; background: var(--surface-2); }
.gantt-now { position: absolute; top: 0; bottom: 0; z-index: 3; width: 2px; margin-left: -1px; background: var(--red); opacity: 0.7; }

/* Les fermetures de l'usine d'une fiche (planSheetClosures, app.js), sur sa
   ligne et derrière ses barres : chaque usine a les siennes. Hachurées et non
   grisées : un aplat se confondrait avec les week-ends, qui ne disent pas la
   même chose — une usine chinoise tourne le samedi, pas au Nouvel An. En
   orange, la couleur de ce qui demande attention, jamais pleine. */
.gantt-closure {
  position: absolute; top: 3px; bottom: 3px; z-index: 0;
  border-radius: 4px;
  background: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--warn) 18%, transparent) 0 2px, transparent 2px 7px);
  border-inline: 1px solid color-mix(in srgb, var(--warn) 32%, transparent);
}
/* L'estimation, quand le fournisseur n'a rien saisi : plus pâle, et bordée
   de tirets, comme ce qui n'est pas encore sûr. */
.gantt-closure.is-estimate {
  background: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--warn) 10%, transparent) 0 2px, transparent 2px 7px);
  border-inline: 1px dashed color-mix(in srgb, var(--warn) 32%, transparent);
}
/* Sur la barre de production, là où elle enjambe la fermeture : c'est sur
   elle que la frise mentirait, puisque la bande passe dessous. */
.gantt-bar-closure {
  position: absolute; top: 0; bottom: 0;
  background: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--warn) 50%, transparent) 0 2px, transparent 2px 6px);
  pointer-events: none;
}
.gantt-bar-closure.is-estimate {
  background: repeating-linear-gradient(135deg,
    color-mix(in srgb, var(--warn) 30%, transparent) 0 2px, transparent 2px 6px);
}

/* ---------- La ligne de visée ----------
   La colonne du jour pointé, teintée à peine, bordée d'un trait ; sa date
   dans une pastille en pied d'échelle. Sous les barres et les jalons, qu'elle
   ne doit jamais cacher. */
.gantt-cursor {
  position: absolute; top: 0; bottom: 0; left: 0; z-index: 0;
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  border-left: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  pointer-events: none;
}
.gantt-cursor-chip {
  position: absolute; left: 0; bottom: 3px; z-index: 2;
  display: flex; flex-direction: column; align-items: center;
  padding: 2px 8px 3px; border-radius: 7px;
  background: var(--accent); color: var(--accent-text);
  font-size: 11px; font-weight: 520; line-height: 1.3; white-space: nowrap;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 2px 8px rgba(10, 20, 40, 0.18);
  pointer-events: none;
}
.gantt-cursor-chip[hidden] { display: none; }
.gantt-cursor-chip b { font-weight: 650; }
.gantt-cursor-chip small { font-size: 10px; opacity: 0.85; }

/* ---------- La vue d'ensemble ----------
   Tout le planning filtré, au-dessus de la frise, aligné sur elle : le coin
   de gauche tient la place de la colonne des intitulés. Un trait par
   commande, en ardoise ; gris quand elle est livrée, rouge quand ses dates
   se contredisent — les deux seuls états qui changent ce qu'on y cherche. */
.gantt-map {
  display: flex; height: 34px;
  border-bottom: 1px solid var(--hairline);
  background: var(--surface-2);
}
.gantt-map-corner {
  flex: none; width: var(--gantt-label);
  display: flex; flex-direction: column; justify-content: center;
  padding: 0 12px;
  border-right: 1px solid var(--hairline);
  font-size: 10.5px; font-weight: 640; letter-spacing: 0.035em; text-transform: uppercase;
  color: var(--text-2);
}
.gantt-map-corner small {
  font-size: 10.5px; font-weight: 520; letter-spacing: 0; text-transform: none;
  color: var(--text-3); font-variant-numeric: tabular-nums;
}
.gantt-map-track {
  position: relative; flex: 1 1 auto; min-width: 0;
  overflow: hidden; cursor: pointer;
  /* Au doigt, sur une tablette, le cadre se glisse : la page ne défile pas. */
  touch-action: none;
}
.gantt-map-order {
  position: absolute; height: 3px; border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 72%, transparent);
}
.gantt-map-order.is-done { background: color-mix(in srgb, var(--text-3) 50%, transparent); }
.gantt-map-order.is-issue { background: var(--red); }
.gantt-map-now { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--red); opacity: 0.7; }
.gantt-map-view {
  position: absolute; top: 2px; bottom: 2px; min-width: 6px;
  border: 1.5px solid var(--accent); border-radius: 5px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  cursor: grab;
}
.gantt-map.is-dragging .gantt-map-view { cursor: grabbing; background: color-mix(in srgb, var(--accent) 17%, transparent); }

.gantt-row { position: relative; display: flex; height: var(--row-h); }
.gantt-row + .gantt-row { border-top: 1px solid var(--hairline); }
.gantt-row.is-order + .gantt-row.is-sheet { border-top-color: transparent; }
/* Une commande ouvre son groupe d'un filet plus marqué : on voit où finit la
   précédente sans compter les lignes. */
.gantt-row.is-order + .gantt-row.is-order,
.gantt-row.is-sheet + .gantt-row.is-order { border-top-color: color-mix(in srgb, var(--text-3) 45%, transparent); }
/* La ligne survolée se suit jusqu'au bout de la frise, pas seulement dans son
   intitulé : c'est là-bas, à droite, qu'on perd le fil. */
.gantt-row.is-sheet:hover { background: color-mix(in srgb, var(--surface-2) 75%, transparent); }

.gantt-label {
  position: sticky; left: 0; z-index: 4;
  flex: none; width: var(--gantt-label); min-width: 0;
  display: flex; align-items: center; gap: 8px;
  padding: 0 10px 0 12px;
  /* Opaque : --surface est très légèrement translucide, et le texte des
     barres passées dessous au défilement transparaissait. */
  background: linear-gradient(var(--surface), var(--surface)), var(--surface-2);
  border-right: 1px solid var(--hairline);
  cursor: pointer;
}
.gantt-row.is-sheet .gantt-label { padding-left: 40px; }
.gantt-row.is-sheet:hover .gantt-label { background: var(--surface-2); }
.gantt-row.is-order .gantt-label { padding-left: 4px; background: var(--surface-2); cursor: default; }
.gantt-row.is-order { background: color-mix(in srgb, var(--surface-2) 60%, transparent); }
.gantt-thumb { display: flex; flex: none; }
.gantt-row.is-focus .gantt-label { box-shadow: inset 3px 0 0 var(--accent); }
.gantt-row.is-flash { animation: gantt-flash 1.8s ease-out; }
.gantt-row.is-flash .gantt-label { animation: gantt-flash-label 1.8s ease-out; }
@keyframes gantt-flash { 0%, 35% { background: var(--accent-soft); } 100% { background: transparent; } }
@keyframes gantt-flash-label {
  0%, 35% { background: color-mix(in srgb, var(--accent) 16%, var(--surface)); }
  100% { background: var(--surface); }
}

.gantt-label-text { flex: 1 1 auto; min-width: 0; }
.gantt-label-title { font-size: 12.5px; font-weight: 580; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gantt-row.is-order .gantt-label-title { font-size: 13px; font-weight: 670; }
.gantt-label-client { margin-left: 3px; font-weight: 520; color: var(--text-2); }
.gantt-label-sub {
  font-size: 11.5px; color: var(--text-2); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gantt-label .row-thumb { flex: none; cursor: inherit; }
.gantt-label .row-thumb:hover { transform: none; }
.gantt-fold { width: 28px; height: 28px; flex: none; }
.gantt-fold svg { width: 15px; height: 15px; }
.gantt-stage { flex: none; padding: 1px 7px; font-size: 10.5px; }
.gantt-alert { display: inline-flex; flex: none; color: var(--red); }
.gantt-alert svg { width: 15px; height: 15px; }

.gantt-track { position: relative; flex: 1 0 auto; }

/* Une phase : du premier jour au dernier, bornes comprises. */
.gantt-bar {
  position: absolute; top: var(--lane-bar); z-index: 1;
  height: var(--bar-h); margin-top: calc(var(--bar-h) / -2); min-width: 4px;
  display: flex; align-items: center;
  border-radius: 6px;
  /* La part déjà écoulée, jusqu'au trait d'aujourd'hui, s'estompe : un
     planning se lit pour ce qui reste à faire, c'est donc ce qui vient qui
     doit ressortir. --done vient de planDone(), app.js. */
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--tone) 9%, var(--surface)) var(--done, 0px),
    color-mix(in srgb, var(--tone) 28%, var(--surface)) var(--done, 0px));
  border: 1px solid color-mix(in srgb, var(--tone) 62%, transparent);
  color: color-mix(in srgb, var(--tone) var(--bar-ink, 80%), var(--text));
  transition: box-shadow 0.14s;
}
.gantt-bar:hover { z-index: 3; box-shadow: 0 0 0 3px color-mix(in srgb, var(--tone) 25%, transparent); }
.gantt-bar-label {
  /* Positionné pour passer devant la trame des fermetures, posée avant lui. */
  position: relative;
  flex: 1 1 auto; min-width: 0;
  padding: 0 9px;
  font-size: 11.5px; font-weight: 630;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;
}
.gantt-handle { position: absolute; top: -3px; bottom: -3px; z-index: 2; width: 10px; cursor: ew-resize; }
.gantt-handle.is-start { left: -5px; }
.gantt-handle.is-end { right: -5px; }
.gantt-handle::after {
  content: ""; position: absolute; top: 6px; bottom: 6px; left: 4px;
  width: 2px; border-radius: 2px;
  background: var(--tone);
  opacity: 0; transition: opacity 0.14s;
}
.gantt-bar:hover .gantt-handle::after { opacity: 0.9; }
.gantt:not(.is-editable) .gantt-handle { display: none; }

/* La commande entière, en barre de synthèse : un trait carré et ses deux
   pointes, comme dans tout Gantt. Arrondie et grise, elle ressemblait trait
   pour trait à la barre de défilement juste en dessous. La part écoulée
   s'estompe, comme sur les barres des fiches. */
.gantt-sum {
  --sum-ink: color-mix(in srgb, var(--text-2) 78%, transparent);
  --sum-past: color-mix(in srgb, var(--text-3) 40%, transparent);
  position: absolute; top: 50%; z-index: 1;
  height: 7px; margin-top: -5px; min-width: 4px;
  border-radius: 1px;
  background: linear-gradient(90deg, var(--sum-past) var(--done, 0px), var(--sum-ink) var(--done, 0px));
}
.gantt-sum::before, .gantt-sum::after {
  content: ""; position: absolute; top: 100%;
  border-top: 5px solid var(--sum-ink);
  pointer-events: none;
}
.gantt-sum::before { left: 0; border-right: 5px solid transparent; }
.gantt-sum::after { right: 0; border-left: 5px solid transparent; }
/* Une pointe déjà dépassée s'estompe avec le trait qu'elle termine. */
.gantt-sum.is-started::before, .gantt-sum.is-over::after { border-top-color: var(--sum-past); }
.gantt-sum:hover { --sum-ink: var(--text); }

.gantt-link {
  position: absolute; top: var(--lane-bar); z-index: 0;
  height: 0;
  border-top: 2px dashed color-mix(in srgb, var(--text-3) 65%, transparent);
  pointer-events: none;
}

.gantt-ms, .gantt-dot {
  position: absolute; top: var(--lane-ms); z-index: 2;
  width: 22px; height: 22px; margin: -11px 0 0 -11px;
  display: grid; place-items: center;
}
/* La tige descend du jalon jusqu'au pied des barres : le jour se lit sur la
   frise, et le losange ne recouvre plus rien. Elle ne se saisit pas — un
   doigt posé dessus attrape la barre qu'elle traverse. */
.gantt-ms::before, .gantt-dot::before {
  content: ""; position: absolute; left: 50%; top: 50%; z-index: -1;
  height: calc(var(--lane-bar) + var(--bar-h) / 2 - var(--lane-ms));
  border-left: 1px solid color-mix(in srgb, var(--tone) 55%, transparent);
  pointer-events: none;
}
.gantt-dot::before { border-left-style: dashed; border-left-color: color-mix(in srgb, var(--text-2) 55%, transparent); }
.gantt-ms > i {
  width: var(--ms-size); height: var(--ms-size); border-radius: 2px;
  background: var(--tone);
  transform: rotate(45deg);
  box-shadow: 0 0 0 2px var(--surface);
  transition: transform 0.14s;
}
.gantt-ms:hover > i { transform: rotate(45deg) scale(1.25); }
.gantt-dot > i {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--surface);
  border: 2px solid var(--text-2);
}
.gantt-bar.is-issue, .gantt-ms.is-issue > i { outline: 2px dashed var(--red); outline-offset: 2px; }

/* touch-action laisse le doigt faire défiler la frise depuis une barre : on
   ne la saisit qu'après un appui long (planArm, app.js). La souris n'est pas
   concernée, elle saisit d'emblée. */
.gantt.is-editable .gantt-bar,
.gantt.is-editable .gantt-sum,
.gantt.is-editable .gantt-ms,
.gantt.is-editable .gantt-dot {
  cursor: grab; touch-action: manipulation;
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
.gantt.is-editable .gantt-handle { cursor: ew-resize; }
.gantt-bar.is-lifted, .gantt-sum.is-lifted { z-index: 4; box-shadow: 0 0 0 3px color-mix(in srgb, var(--tone, var(--text-2)) 35%, transparent), 0 8px 18px -8px rgba(10, 20, 40, 0.45); }
.gantt-ms.is-lifted > i { transform: rotate(45deg) scale(1.5); }
.gantt-dot.is-lifted > i { transform: scale(1.5); }
body.is-plan-dragging, body.is-plan-dragging * { cursor: grabbing !important; user-select: none; }

/* Le jour cliqué dans une ligne vide, le temps de choisir quoi y poser. */
.gantt-anchor { position: absolute; top: 5px; bottom: 5px; border-radius: 4px; background: var(--accent-soft); pointer-events: none; }

.gantt-tip {
  position: fixed; z-index: 260;
  max-width: 320px;
  padding: 8px 11px;
  border-radius: 11px;
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur-pop);
  backdrop-filter: var(--blur-pop);
  border: 1px solid var(--glass-border);
  box-shadow: 0 12px 32px -12px rgba(10, 20, 40, 0.34), inset 0 1px 0 var(--glass-shine);
  font-size: 12px; line-height: 1.45; color: var(--text);
  pointer-events: none;
}
.gantt-tip > b { display: block; font-size: 12.5px; font-weight: 650; }
.gantt-tip small { display: block; margin-top: 3px; font-size: 11px; color: var(--text-3); }

/* ---------- Panneau des dates d'une fiche ---------- */

/* Le panneau commun est du verre, parce qu'un menu est du chrome. Celui-ci
   porte des champs de saisie posés au-dessus d'une frise dense : c'est une
   surface de données, et elle reste opaque, comme les tableaux. */
.pf-menu.plan-pop { padding: 12px; background: var(--surface); }
.plan-pop-body { width: min(350px, calc(100vw - 48px)); display: flex; flex-direction: column; gap: 10px; }
.plan-pop-head { display: flex; align-items: center; gap: 10px; }
.plan-pop-title { flex: 1 1 auto; min-width: 0; }
.plan-pop-title .row-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.plan-pop-head .row-thumb { cursor: default; }
.plan-pop-head .row-thumb:hover { transform: none; }
.plan-pop .row-todo.is-block { margin-top: 0; }
.plan-pop-steps { display: flex; flex-direction: column; gap: 5px; }
.plan-pop-step {
  display: grid; grid-template-columns: 18px minmax(0, 1fr) 148px;
  align-items: center; gap: 8px;
  font-size: 12.5px;
}
.plan-pop-step .plan-swatch { justify-self: center; width: 14px; }
.plan-pop-step .plan-swatch.is-mark, .plan-pop-step .plan-swatch.is-dot { width: 9px; }
.plan-pop-step .input { padding: 5px 8px; font-size: 12.5px; }
.plan-pop-facts { display: flex; flex-wrap: wrap; gap: 3px 12px; font-size: 11.5px; color: var(--text-2); }
.plan-pop-facts:empty { display: none; }
.plan-pop-shift { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }
.plan-pop-shift-btns { display: flex; gap: 4px; }
.plan-pop-shift-btns .btn { padding: 0 9px; }
.plan-pop-foot { display: flex; gap: 6px; flex-wrap: wrap; padding-top: 10px; border-top: 1px solid var(--hairline); }

/* ---------- Tableau façon Excel ---------- */

.plan-grid { max-height: 70vh; }
.plan-grid:focus { outline: none; }
.plan-grid:focus-visible { box-shadow: 0 0 0 3px var(--accent-soft); }
.plan-grid table { table-layout: fixed; }
.plan-grid thead th { padding: 8px 10px; cursor: pointer; user-select: none; }
.plan-grid thead th:hover { color: var(--text); }
.plan-th { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; overflow: hidden; }
.plan-th svg { width: 12px; height: 12px; flex: none; }
.plan-grid tbody td {
  position: relative;
  height: 34px; max-width: none;
  padding: 0 10px;
  vertical-align: middle;
  font-size: 12.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  border-right: 1px solid var(--hairline);
  user-select: none;
}
.plan-grid tbody tr:last-child td { border-bottom: 1px solid var(--hairline); }
.plan-grid .plan-pin { position: sticky; z-index: 2; background: var(--surface); }
.plan-grid thead .plan-pin { z-index: 4; background: var(--surface-2); }
.plan-grid tbody tr:hover .plan-pin { background: var(--surface-2); }

.plan-cell.is-editable { cursor: cell; }
.plan-cell.is-past { color: var(--text-3); }
.plan-cell.is-soon { background: var(--orange-soft); }
.plan-cell.is-bad { background: var(--red-soft); color: var(--red); font-weight: 600; }
.plan-grid .plan-cell.is-picked { background: var(--accent-soft); }
.plan-cell.is-active { box-shadow: inset 0 0 0 2px var(--accent); }
.plan-grid tr.has-issue > td:first-child { box-shadow: inset 3px 0 0 var(--red); }
.plan-grid tr.has-issue > td.is-active:first-child { box-shadow: inset 3px 0 0 var(--red), inset 0 0 0 2px var(--accent); }
.plan-grid .plan-cell.is-editing {
  padding: 0; overflow: visible;
  background: var(--surface);
  box-shadow: inset 0 0 0 2px var(--accent);
}
.plan-cell-input {
  width: 100%; height: 100%;
  padding: 0 10px;
  border: 0; outline: 0; background: transparent;
  color: var(--text);
}
/* Une date laisse la place du bouton calendrier au bout de sa saisie. */
.plan-cell-input:not(:only-child) { padding-right: 34px; }
.plan-cell-cal {
  position: absolute; right: 4px; top: 50%;
  width: 26px; height: 26px; margin-top: -13px;
  display: grid; place-items: center;
  border-radius: 6px;
  color: var(--text-3);
}
.plan-cell-cal:hover { background: var(--surface-3); color: var(--text); }
.plan-cell-cal svg { width: 14px; height: 14px; }
.plan-cell-picker {
  position: absolute; right: 0; bottom: 0;
  width: 1px; height: 1px; padding: 0; border: 0;
  opacity: 0; pointer-events: none;
}
.plan-cell-alert { display: inline-flex; align-items: center; gap: 5px; color: var(--red); }
.plan-cell-alert svg { width: 13px; height: 13px; flex: none; }
.plan-grid .pill { font-size: 11px; }

/* ---------- Pied de page et notification ---------- */

.plan-foot-count { flex: 0 1 auto; }
.plan-foot-help { flex: 1 1 260px; }
.plan-foot-alert {
  padding: 0; border: 0; background: none;
  color: var(--red); font: inherit;
  text-decoration: underline; text-underline-offset: 2px;
}

/* Le bouton d'une notification : « Annuler » après un déplacement. */
.toast-action {
  flex: none; margin-left: 2px;
  padding: 4px 11px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
  font-size: 12.5px; font-weight: 630;
}
.toast-action:hover { background: var(--accent); color: var(--accent-text); }

@media (max-width: 860px) {
  .gantt-row.is-sheet .gantt-label { padding-left: 12px; }
  /* Sur l'enveloppe et non sur la vignette : sans image, rowThumb() pose un
     display en style en ligne, qu'aucune règle ne pourrait écraser. */
  .gantt-thumb, .gantt-stage { display: none; }
  /* Vue et échelle sur une même ligne, la navigation en dessous. */
  .plan-bar > .card-head-spacer { display: none; }
  .plan-zoom { margin-left: auto; }
  .plan-nav { order: 3; width: 100%; }
  .plan-period-label { min-width: 0; }
  .pf-select.plan-filter { flex: 1 1 140px; }
  .plan-foot-help { display: none; }
  .plan-legend .btn-sm span { display: none; }

  /* Les listes se replient derrière « Filtres » : dépliées, elles prenaient
     trois lignes avant la première barre. La recherche reste à portée. */
  .plan-filters { gap: 6px; }
  .plan-filters .search { flex: 1 1 calc(100% - 120px); min-width: 0; max-width: none; }
  .plan-filters-toggle { display: inline-flex; flex: none; }
  .plan-filters:not(.is-open) > .plan-filter,
  .plan-filters:not(.is-open) > .plan-cascade { display: none; }

  /* La légende tient sur une ligne, qui défile au doigt, au lieu de trois. */
  .plan-legend { flex-wrap: nowrap; gap: 2px; }
  .plan-legend > .card-head-spacer { display: none; }
  .plan-legend-chips {
    flex: 1 1 auto; flex-wrap: nowrap;
    overflow-x: auto; scrollbar-width: none;
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
    mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
  }
  .plan-legend-chips::-webkit-scrollbar { display: none; }
  .plan-chip { flex: none; }
}

/* ---------- Gantt — téléphone ----------
   Plus de colonne d'intitulés (planLabelWidth vaut 0) : elle prenait la
   moitié de l'écran pour des noms tronqués à dix lettres. Chaque ligne porte
   son intitulé en tête, collé au bord gauche quand la frise défile, et ses
   jalons et barres dessous, sur toute la largeur. */
@media (max-width: 600px) {
  .gantt {
    --row-h: 62px;
    --bar-h: 18px;
    --lane-ms: 32px;
    --lane-bar: 49px;
    --ms-size: 10px;
  }
  .gantt-scroll { max-height: 72vh; }
  .gantt-corner { display: none; }
  /* Sur 320 px, la vue d'ensemble ne serait qu'un trait de plus au-dessus
     d'une frise qui prend déjà tout l'écran ; le doigt qui balaie la remplace. */
  .gantt-map { display: none; }
  .gantt-row { display: block; }
  .gantt-row.is-order { height: 50px; }
  .gantt-track { position: absolute; inset: 0; }
  .gantt-label,
  .gantt-row.is-order .gantt-label,
  .gantt-row.is-sheet .gantt-label,
  .gantt-row.is-sheet:hover .gantt-label {
    width: max-content; max-width: calc(100vw - 72px);
    height: 22px; margin-top: 3px;
    padding: 0 8px 0 14px; gap: 6px;
    background: none; border-right: 0;
  }
  .gantt-row.is-order .gantt-label { height: 28px; margin-top: 2px; padding-left: 2px; }
  .gantt-fold { width: 26px; height: 26px; }
  .gantt-row.is-order .gantt-sum { top: 38px; }
  .gantt-label-text { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
  .gantt-label-title { flex: 0 1 auto; min-width: 0; }
  .gantt-label-sub { flex: 0 4 auto; min-width: 0; font-size: 11px; }
  /* Sur une commande, ce sont ses dates qu'on garde, et le client qui cède. */
  .gantt-row.is-order .gantt-label-sub { flex-shrink: 0; }
  /* L'intitulé masque le quadrillage et le trait d'aujourd'hui qui passent
     dessous, dans la teinte de sa ligne : sans fond, le trait rouge barrait
     les noms. */
  .gantt-row.is-sheet .gantt-label { background: linear-gradient(var(--surface), var(--surface)), var(--surface-2); border-radius: 0 8px 8px 0; }
  .gantt-row.is-sheet:hover .gantt-label { background: color-mix(in srgb, var(--surface-2) 75%, var(--surface)); }
  .gantt-row.is-order .gantt-label { background: color-mix(in srgb, var(--surface-2) 60%, var(--surface)); border-radius: 0 8px 8px 0; }

  .plan-bar { gap: 8px; }
  .plan-bar .segmented button { padding: 5px 9px; font-size: 12px; }

  /* La barre d'outils en deux lignes, même sur 320 px : la vue et le zoom,
     puis la navigation. Le cran s'écrit court, « ajuster » passe dans la
     liste des crans, et les doubles flèches — un trimestre d'un coup —
     cèdent la place au doigt qui balaie. Le titre de la période est le seul
     à pouvoir rétrécir : il se coupe plutôt que de sortir de la carte. */
  .plan-zoom [data-plan-zoom="fit"] { display: none; }
  .plan-zoom-level { min-width: 62px; padding-inline: 8px; }
  .plan-zoom-name .is-long { display: none; }
  .plan-zoom-name .is-short { display: inline; }
  .plan-nav [data-plan-nav$="period"] { display: none; }
  .plan-period-wrap { flex: 1 1 auto; min-width: 0; margin-left: 2px; }
  .plan-period { min-width: 0; max-width: 100%; }
  .plan-period > svg { display: none; }
  .plan-period-label b,
  .plan-period-label small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* ---------- Ce qu'un champ calculé dit de lui-même ----------
   Sous le total, l'acompte, le poids et le volume de la fiche commande : la
   valeur que le calcul donne, et la façon dont il l'obtient. Le calcul est
   écrit en clair — « 24 cartons × 13,5 kg » — parce qu'un chiffre qu'on ne
   sait pas refaire ne se vérifie pas. */
.field-calc strong { font-weight: 640; }

/* Un bouton qui se lit comme un lien : « Reprendre le calcul » vit dans une
   ligne d'explication, pas dans une barre d'actions. */
.link-btn {
  padding: 0; border: 0; background: none;
  color: var(--accent); font: inherit;
  text-decoration: underline; text-underline-offset: 2px;
}
.link-btn:hover { color: var(--accent-hover); }
