
/* ========================================================================== */
/* Jetons                                                                     */
/* ========================================================================== */

:root {
  color-scheme: light dark;

  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;

  /* Largeur de page UNIQUE. Toutes les pages s'y alignent, en-tête compris :
     c'est ce qui fait qu'on ne voit pas le contenu « sauter » en changeant
     d'onglet. Une seule valeur à changer pour toute l'application. */
  --page-width: 1180px;
  /* Largeur de la modale d'ajout de produits. Un JETON et non un nombre posé
     dans la règle : une largeur en dur de plus de mille pixels ressemble à une
     largeur de page, et un test interdit précisément d'en semer ailleurs que
     dans les jetons. Celle-ci DÉPASSE délibérément --page-width — la modale
     flotte au-dessus de la page, elle ne s'y aligne pas, et ses lignes portent
     six informations là où le tableau en porte trois. */
  --modal-wide-width: 2160px;
  --topbar-height: 58px;
  /*
   * Hauteur du sous-header, FIXE et identique sur toutes les pages.
   *
   * ⚠️ Elle ne dépend d'aucun contenu, et c'est le point. La bande se
   * dimensionnait sur ce qu'elle portait : un titre long qui passait à la ligne
   * la faisait grandir, et l'écran d'à côté n'avait pas la même hauteur de
   * chrome — le contenu « sautait » d'un onglet à l'autre. Une valeur fixe
   * permet aussi de la rendre AVANT les données, à sa hauteur définitive.
   */
  --subheader-height: 56px;
  /* Hauteur de la barre d'actions fixée en bas. Sert deux fois : à la barre
     elle-même, et à la marge que le contenu réserve dessous. */
  --actionbar-height: 60px;
  /* Hauteur de la barre des sections d'une commande, FIXE pour la même raison
     que celle du sous-header : elle est collante, et tout ce qui se cale
     dessous — le saut vers une section — se décalerait si elle grandissait. */
  --sectionnav-height: 44px;

  --radius-sm: 4px;
  --radius: 8px;
  --radius-lg: 14px;

  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
          Arial, "Noto Sans", sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  --fs-xs: 11px;
  --fs-sm: 12.5px;
  --fs-md: 14px;
  --fs-lg: 16px;
  --fs-xl: 20px;
  --fs-2xl: 26px;

  /* Hauteur de ligne de référence. Une seule valeur, héritée partout ; les
     rares dérogations (titres, prose, zone de saisie) sont nommées. */
  --line: 1.3;
  --line-prose: 1.55;

  /* Neutres réchauffés pour s'accorder à #292928. Des gris bleutés à côté
     d'un noir chaud donnent un ensemble qui « jure » sans qu'on sache dire
     pourquoi : la teinte des neutres doit suivre celle de la couleur
     principale. */
  --bg: #f5f4f2;
  --surface: #ffffff;
  --surface-2: #faf9f7;
  --surface-3: #eeece8;
  --border: #e2e0dc;
  --border-strong: #c8c5bf;
  --text: #292928;
  --text-dim: #6a6862;
  --text-faint: #94918a;

  /* Couleur principale. 14,6:1 sur blanc, dans les deux sens : elle sert
     aussi bien de fond de bouton que de couleur de texte. */
  --accent: #292928;
  --accent-hover: #3d3d3b;
  --accent-text: #ffffff;
  --accent-soft: #ecebe8;

  /* Fond d'un contrôle DÉSACTIVÉ. Un cran seulement sous la surface : il doit
     se distinguer d'un champ actif sans donner l'impression d'un trou dans le
     formulaire. C'est la SEULE chose qui distingue les deux à l'œil quand le
     champ porte déjà du texte — l'opacité, elle, rendrait aussi le contenu
     illisible, or on veut pouvoir LIRE une commande verrouillée. */
  --disabled-bg: #f2f0ed;

  --danger: #a02020;
  --danger-soft: #fbeaea;
  --ok: #1c6b45;
  --ok-soft: #e6f3ec;
  --warn: #8a5a00;
  --warn-soft: #fdf3df;
  --info-soft: #e9eef5;

  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow: 0 2px 8px rgba(16, 24, 40, 0.08);
  --shadow-lg: 0 12px 40px rgba(16, 24, 40, 0.18);

  --focus: 0 0 0 3px rgba(41, 41, 40, 0.26);
  /* Bague de focus d'un CHAMP : la même partout, teintée d'accent.
     Elle était recopiée telle quelle à quatre endroits — champ, sélecteur de
     quantité, sélecteur maison, barre de recherche — et une seule des quatre
     aurait suffi à diverger le jour où l'on change la couleur. */
  --ring: 0 0 0 3px var(--accent-soft);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #171716;
    --surface: #1f1f1d;
    --surface-2: #262623;
    --surface-3: #2f2f2b;
    --border: #383833;
    --border-strong: #4c4c46;
    --text: #eceae6;
    --text-dim: #a8a5a0;
    --text-faint: #7d7a74;

    /* #292928 sur fond sombre serait invisible : la couleur principale
       s'inverse. C'est son RÔLE — la surface qui porte l'action — qui est
       conservé, pas sa valeur. */
    --accent: #eceae6;
    --accent-hover: #ffffff;
    --accent-text: #292928;
    --accent-soft: #33322e;

    --disabled-bg: #1b1b19;

    --danger: #f08a8a;
    --danger-soft: #3a2224;
    --ok: #7fcda4;
    --ok-soft: #1e3229;
    --warn: #e3b569;
    --warn-soft: #322818;
    --info-soft: #232a33;

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
    --shadow-lg: 0 16px 44px rgba(0, 0, 0, 0.6);

    --focus: 0 0 0 3px rgba(232, 231, 228, 0.30);
  }
}

/* ========================================================================== */
/* Socle                                                                      */
/* ========================================================================== */

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

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

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-md);
  /* Hauteur de ligne de RÉFÉRENCE, héritée par tout le document. 1,5 était
     calibré pour du texte courant ; cette interface est faite de libellés,
     de cellules et de pastilles d'une ou deux lignes, où l'interligne se lit
     comme du vide. Les blocs de prose gardent la leur, plus haute. */
  line-height: var(--line);
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/* Les titres serrent un peu plus : à 26 px, 1,3 laisse un blanc visible entre
   deux lignes d'un même titre. */
h1, h2, h3 { margin: 0; font-weight: 600; line-height: 1.2; }
h1 { font-size: var(--fs-2xl); letter-spacing: -0.02em; }
h2 { font-size: var(--fs-xl); letter-spacing: -0.01em; }
h3 { font-size: var(--fs-lg); }
p { margin: 0 0 var(--sp-3); }

a { color: var(--accent); }

:focus-visible {
  outline: none;
  box-shadow: var(--focus);
  border-radius: var(--radius-sm);
}

.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;
}

.skip-link {
  position: absolute; left: var(--sp-3); top: -60px; z-index: 100;
  background: var(--surface); color: var(--text); padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--border); border-radius: var(--radius);
  transition: top 0.15s;
}
.skip-link:focus { top: var(--sp-3); }

/* ========================================================================== */
/* Ossature                                                                   */
/* ========================================================================== */

.topbar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: var(--sp-4);
  padding: 0; height: var(--topbar-height);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

/* L'en-tête traverse toute la largeur, bordure comprise, mais son CONTENU
   s'aligne sur la même colonne que les pages.
   
   ⚠️ TROIS COLONNES, PAS UNE RANGÉE SOUPLE. La navigation est à gauche, le
   logo au CENTRE, les actions à droite. Un simple flex avec « margin-left:
   auto » sur les actions centrerait le logo par rapport à ce qui reste, donc
   plus du tout au milieu de l'écran : il glisserait dès qu'un onglet est
   ajouté ou qu'un libellé change de longueur. Les deux colonnes latérales ont
   donc la même base (1fr) et le centre se dimensionne sur son contenu. */
.topbar-inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: var(--sp-4);
  height: 100%; width: 100%;
  max-width: var(--page-width); margin: 0 auto;
  padding: 0 var(--sp-5);
}

/* Emplacement réservé au logo. Vide aujourd'hui : il tient la place et
   l'alignement pour que son arrivée ne déplace rien. */
.brand-slot { justify-self: center; width: 116px; height: 30px; }

/* Collée au bord gauche de la colonne de contenu : aucune marge de départ. */
.topnav { display: flex; gap: var(--sp-1); justify-self: start; }
.topnav a {
  padding: var(--sp-2) var(--sp-3); border-radius: var(--radius);
  color: var(--text-dim); text-decoration: none; font-weight: 500;
  font-size: var(--fs-sm);
}
.topnav a:hover { background: var(--surface-3); color: var(--text); }
.topnav a[aria-current="page"] { background: var(--accent-soft); color: var(--accent); }

.topbar-end { justify-self: end; display: flex; align-items: center; gap: var(--sp-1); }

/* Onglet courant, pour un bouton-icône (Paramètres). */
.btn-icon.is-active { background: var(--accent-soft); color: var(--accent); }

main { max-width: var(--page-width); margin: 0 auto; padding: var(--sp-5); }

/* ========================================================================== */
/* Sous-header : le titre de la page et son fil d'Ariane                      */
/* ========================================================================== */

/*
 * UNE BANDE, PAS UN BLOC DE CONTENU.
 *
 * Le titre vivait à l'intérieur de <main>, donc dans son rembourrage : il ne
 * pouvait pas porter de bande traversant toute la largeur, et rien ne le
 * séparait du corps de la page. Il est maintenant à la même hauteur
 * d'abstraction que la barre de navigation — chrome, pas contenu — et suit la
 * même règle : la bordure traverse tout l'écran, le CONTENU s'aligne sur la
 * colonne des pages.
 */
/*
 * ⚠️ COLLÉE SOUS LA BARRE DE NAVIGATION, et de hauteur FIXE.
 *
 * « top: var(--topbar-height) » : la barre de navigation est elle-même collée
 * à 0 sur 58 px, donc le sous-header doit commencer là où elle finit. Sans ce
 * décalage il se collerait à 0 et passerait DERRIÈRE elle.
 *
 * « z-index: 25 » se lit dans l'échelle complète : en-tête de tableau 5,
 * sous-header 25, barre de navigation 30, liste d'un sélecteur 40, modale 50,
 * notifications 60. Le sous-header passe donc au-dessus du contenu qui défile
 * et sous la barre de navigation — l'inverse le ferait glisser par-dessus elle.
 */
.subheader {
  position: sticky; top: var(--topbar-height); z-index: 25;
  height: var(--subheader-height);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
/*
 * « nowrap » est la conséquence directe de la hauteur fixe : autoriser le
 * retour à la ligne dans une bande de hauteur fixe ferait sortir la deuxième
 * ligne de la bande. Tout tient donc sur une ligne, et ce qui dépasse se
 * tronque — voir min-width sur le titre.
 */
.subheader-inner {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: nowrap;
  height: 100%; max-width: var(--page-width); margin: 0 auto;
  padding: 0 var(--sp-5);
}

/*
 * ⚠️ « min-width: 0 » sur le titre, et c'est ce qui rend la bande fluide.
 *
 * Un élément flex refuse par défaut de descendre sous la largeur de son
 * contenu : un titre long repousserait donc les actions hors de la colonne,
 * sans jamais se tronquer. Avec cette ligne, le titre cède — il est le plus
 * long et le plus devinable des trois.
 *
 * ⚠️ Et il est LE SEUL à céder : le fil porte « flex: 0 0 auto ». Tant que les
 * deux pouvaient rétrécir, ils rétrécissaient ensemble, proportionnellement à
 * leur longueur — un titre long tronquait donc aussi le chemin, qui est court
 * et qu'on ne devine pas. Sur écran étroit le fil ne rétrécit pas non plus, il
 * DISPARAÎT (voir la media query plus bas) : mieux vaut l'ôter en entier que le
 * réduire à « Comm… ».
 */
.subheader-title {
  font-size: var(--fs-xl); letter-spacing: -0.01em;
  min-width: 0; flex: 0 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.crumbs { display: flex; align-items: center; min-width: 0; flex: 0 0 auto; }
.crumb { display: inline-flex; align-items: center; min-width: 0; }
.crumb-sep { display: inline-flex; color: var(--text-faint); }
/* Le chevron suit la taille du texte qu'il sépare : à 15 px entre deux mots de
   20 px, il paraissait tombé d'une autre ligne. */
.crumb-sep svg { width: 18px; height: 18px; }
/*
 * ⚠️ LA MÊME TAILLE QUE LE TITRE, et c'est le point.
 *
 * Le fil et le titre forment UNE ligne — « Commandes › SOV1042 » — depuis que
 * l'ordre va du plus loin au plus proche. Passer de 12,5 px sur l'ancêtre à
 * 20 px sur la page courante faisait lire deux éléments sans rapport posés
 * côte à côte, au lieu d'un chemin.
 *
 * La HIÉRARCHIE tient donc à la couleur et à la graisse — l'ancêtre est atténué
 * et normal, la page courante pleine et semi-grasse — pas à la taille. C'est
 * suffisant pour distinguer où l'on est, et cela laisse le chemin se lire d'un
 * trait.
 */
.crumb a {
  color: var(--text-dim); text-decoration: none;
  font-size: var(--fs-xl); letter-spacing: -0.01em;
  padding: 0 var(--sp-2); border-radius: var(--radius-sm);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.crumb a:hover { background: var(--surface-3); color: var(--text); }

.page-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; align-items: center; }
/*
 * ⚠️ Dans la bande, les actions ne passent JAMAIS à la ligne.
 *
 * Le conteneur d'actions autorise le retour à la ligne — c'est utile ailleurs.
 * Ici, une deuxième rangée sortirait de la bande, dont la hauteur est fixe :
 * les boutons dépasseraient sous la bordure. Ils gardent donc leur largeur et
 * c'est le titre qui cède, ce qui est le bon ordre — un titre tronqué reste
 * devinable, un bouton coupé ne se clique pas.
 */
.subheader .page-actions { flex-wrap: nowrap; flex: none; }

/*
 * Sur écran étroit, la bande garde sa hauteur : c'est le FIL qui disparaît, pas
 * le titre qui passe à la ligne. Le fil est la première chose dont on peut se
 * passer — la barre de navigation offre déjà le même retour — et le sacrifier
 * garde le titre et les actions lisibles sans toucher à la hauteur.
 */
@media (max-width: 700px) {
  .subheader-inner { padding: 0 var(--sp-3); gap: var(--sp-2); }
  .crumbs { display: none; }
}

/* ========================================================================== */
/* Surfaces                                                                   */
/* ========================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
}
.card + .card { margin-top: var(--sp-4); }
.card-head {
  padding: var(--sp-4) var(--sp-5);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
}
.card-head h2, .card-head h3 { margin: 0; }
.card-head .hint { color: var(--text-dim); font-size: var(--fs-sm); }
.card-body { padding: var(--sp-5); }

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

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  padding: 8px 14px; min-height: 36px;
  font: inherit; font-size: var(--fs-md); font-weight: 600;
  border-radius: var(--radius); border: 1px solid var(--border-strong);
  background: var(--surface); color: var(--text);
  cursor: pointer; white-space: nowrap;
  transition: background 0.12s, border-color 0.12s, opacity 0.12s;
}
.btn:hover:not(:disabled) { background: var(--surface-3); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-primary {
  background: var(--accent); border-color: var(--accent); color: var(--accent-text);
}
.btn-primary:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }

.btn-danger { color: var(--danger); border-color: var(--danger); background: transparent; }
.btn-danger:hover:not(:disabled) { background: var(--danger-soft); }

.btn-ghost { border-color: transparent; background: transparent; color: var(--text-dim); }
.btn-ghost:hover:not(:disabled) { background: var(--surface-3); color: var(--text); }

.btn-block { width: 100%; justify-content: center; }
.btn-sm { min-height: 30px; padding: 4px 10px; font-size: var(--fs-sm); }
.btn-icon { padding: 6px; min-height: 32px; width: 32px; }
.btn svg { width: 15px; height: 15px; flex: none; }

/* ========================================================================== */
/* Formulaires                                                                */
/* ========================================================================== */

.field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.field > label { font-size: var(--fs-sm); font-weight: 600; color: var(--text); }
.field .optional { font-weight: 400; color: var(--text-faint); }
/* L'astérisque d'un champ obligatoire. Il porte la couleur de l'accent et non
   celle du danger : il annonce une exigence, il ne signale pas une faute — et
   un formulaire vierge tout en rouge apprend à ignorer le rouge. */
.field .required-mark { color: var(--accent); font-weight: 600; }

.input, .select, .textarea {
  font: inherit; font-size: var(--fs-md);
  width: 100%; padding: 8px 11px; min-height: 38px;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
}
/*
 * ══════════════════════════════════════════════════════════════════════════
 * ⚠️ AUCUN SURVOL SUR UN CONTRÔLE DÉSACTIVÉ.
 *
 * Un champ grisé qui réagit au survol PROMET une interaction qu'il refusera :
 * on approche la souris, la bordure s'anime, on clique, rien. Sur une commande
 * vérifiée — où TOUS les champs sont désactivés d'un coup — la page entière
 * réagissait ainsi, et rien à l'écran ne disait pourquoi le clic ne faisait
 * rien.
 *
 * Le fond gris, lui, dit l'état SANS attendre le survol. C'est l'inverse du
 * bon compromis précédent : montrer l'état au repos, ne rien promettre au
 * passage.
 * ══════════════════════════════════════════════════════════════════════════
 */
.input:hover:not(:disabled),
.select:hover:not(:disabled),
.textarea:hover:not(:disabled) { border-color: var(--text-faint); }

.input:disabled, .select:disabled, .textarea:disabled {
  background: var(--disabled-bg);
  color: var(--text-dim);
  cursor: not-allowed;
}
/*
 * ⚠️ AUCUN CHAMP DE COMMANDE N'EST MULTILIGNE, ET C'EST UNE DÉCISION.
 *
 * La note de livraison en portait une ; elle tient désormais sur UNE ligne
 * (voir « singleLine » dans orders.js), parce qu'un contrôle qui invite à taper
 * Entrée pour replier ensuite le retour à la ligne est un piège.
 *
 * Cette règle sert le PROMPT du serveur MCP, et lui seul : de la prose, lue
 * par un agent, dont les paragraphes comptent.
 */
.textarea { min-height: 96px; resize: vertical; line-height: var(--line-prose); }
.input[aria-invalid="true"], .select[aria-invalid="true"], .textarea[aria-invalid="true"] {
  border-color: var(--danger);
}
.input::placeholder, .textarea::placeholder { color: var(--text-faint); }
.textarea.mono { font-size: var(--fs-sm); }
/*
 * ⚠️ UNE SEULE RÈGLE POUR TOUT CE QUI EST MONOSPACÉ DANS UN CHAMP.
 *
 * Le monospace n'est pas une seconde typographie : c'est le même système, et
 * chaque emploi doit se justifier — une référence de commande alignée, le texte
 * technique d'un prompt, un chemin de champ où le point et la virgule ne
 * doivent pas se confondre. Les regrouper ici, plutôt qu'une règle par usage,
 * est ce qui empêche la police machine de se répandre par petites additions.
 */
.input.mono, .textarea.mono, .prompt-label { font-family: var(--mono); }
.input.align-right { text-align: right; }

/* ========================================================================== */
/* Sélecteur de quantité                                                      */
/* ========================================================================== */

/*
 * Trois éléments dans UN cadre : « moins », le champ, « plus ».
 *
 * Le cadre appartient au groupe, jamais aux éléments. Trois bordures
 * adjacentes se dédoublent aux jonctions et se décalent d'un pixel dès qu'un
 * bouton passe en survol ; ici les boutons n'ont que leur filet de séparation.
 *
 * La hauteur est celle de tous les autres contrôles (38 px) : un sélecteur plus
 * bas que le champ de prix voisin décale toute la ligne.
 *
 * La largeur, elle, appartient au CONTENANT. Une largeur fixe de 116 px posée
 * ici débordait de la colonne de quantité d'une ligne de commande : le groupe
 * s'imposait à la grille au lieu de s'y ranger. Le groupe remplit donc sa
 * colonne, avec un plancher qui garde les deux boutons cliquables et un
 * plafond qui l'empêche de s'étirer sur toute la largeur en écran étroit.
 */
.qs {
  display: inline-flex; align-items: stretch;
  min-height: 38px;
  width: 100%; min-width: 104px; max-width: 140px;
  background: var(--surface);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  overflow: hidden;
}
/* Le sélecteur de quantité est un CONTENEUR : c'est son champ qui porte
   « disabled », d'où le « :has ». Sans lui, la bordure s'anime au survol d'un
   groupe entièrement inerte. */
.qs:not(:has(:disabled)):hover { border-color: var(--text-faint); }
.qs:has(:disabled) { background: var(--disabled-bg); }
.qs:focus-within {
  border-color: var(--accent);
  box-shadow: var(--ring);
}

.qs-btn {
  flex: 0 0 32px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; border: 0; background: none; cursor: pointer;
  color: var(--text-dim);
}
.qs-btn:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }
/* Plancher ou plafond atteint : le bouton reste EN PLACE, estompé. Le retirer
   ferait sauter la largeur du groupe d'un clic à l'autre. */
.qs-btn:disabled { color: var(--text-faint); cursor: default; }
.qs-btn svg { width: 15px; height: 15px; }

.qs-input {
  flex: 1 1 auto; min-width: 0;
  font: inherit; font-size: var(--fs-md);
  text-align: center;
  padding: 0; border: 0; background: none; color: var(--text);
  font-variant-numeric: tabular-nums;
  /* Le cadre du groupe porte déjà l'anneau de focus. */
  outline: none;
}
/* Filets de séparation internes, sur le CHAMP : les poser sur les boutons
   laisserait un filet visible quand un bouton est estompé. */
.qs-input { border-left: 1px solid var(--border); border-right: 1px solid var(--border); }

.field-error { font-size: var(--fs-sm); color: var(--danger); }
.field-note { font-size: var(--fs-sm); color: var(--text-dim); }

.grid { display: grid; gap: var(--sp-4); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.span-2 { grid-column: span 2; }

@media (max-width: 860px) {
  .grid-2 { grid-template-columns: minmax(0, 1fr); }
  .span-2 { grid-column: auto; }
  main { padding: var(--sp-4) var(--sp-3); }
  .topbar-inner { padding: 0 var(--sp-3); gap: var(--sp-2); }
  .brand-slot { width: 72px; }
}

fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
legend { padding: 0; }


/* Choix du mode de livraison */
/*
 * TOUS les modes sur une seule ligne.
 *
 * La grille était fixée à trois colonnes ; le quatrième mode — la livraison
 * planifiée — passait donc seul à la ligne, sous les trois autres, comme s'il
 * appartenait à un autre groupe. « auto-fit » les répartit tous sur une rangée
 * quel qu'en soit le nombre, et n'en fait descendre que lorsque la place
 * manque réellement.
 */
/*
 * ⚠️ « .check-cards » PARTAGE CET HABIT, ET C'EST DÉLIBÉRÉ.
 *
 * Les consignes de traitement sont des cases à cocher — des choix
 * INDÉPENDANTS, là où les modes de livraison s'excluent. La différence est
 * sémantique, et le style n'a aucune raison d'en porter une seconde : une case
 * native de 13 px perdue dans un formulaire ne se voit pas et ne se touche pas
 * au doigt, quel que soit le sens qu'on lui donne.
 *
 * Les sélecteurs ci-dessous emploient « :has(input:checked) », qui ne demande
 * rien au type du contrôle : ils fonctionnent tels quels sur une case à cocher.
 * La classe supplémentaire n'existe donc que pour qu'un futur écart de style
 * ait où se poser — et pour que le HTML dise ce qu'il est.
 */
.radio-cards, .check-cards {
  display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}

.radio-card {
  position: relative; display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4); cursor: pointer;
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  background: var(--surface);
}
.radio-card:not(:has(:disabled)):hover { border-color: var(--text-faint); }
.radio-card:has(:disabled) { background: var(--disabled-bg); color: var(--text-dim); cursor: not-allowed; }
.radio-card input { margin: 0; width: 16px; height: 16px; flex: none; accent-color: var(--accent); }
.radio-card:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }
.radio-card:has(input:focus-visible) { box-shadow: var(--focus); }
.radio-card .rc-title { font-weight: 600; font-size: var(--fs-sm); }
.radio-card .rc-price { font-size: var(--fs-sm); color: var(--text-dim); }

/*
 * ══════════════════════════════════════════════════════════════════════════
 * LA BARRE DES SECTIONS — collée SOUS le sous-header.
 * ══════════════════════════════════════════════════════════════════════════
 *
 * « top: calc(--topbar-height + --subheader-height) » : les deux bandes
 * au-dessus sont elles-mêmes collantes, l'une à 0 sur 58 px, l'autre à 58 sur
 * 56 px. Celle-ci commence donc là où la seconde finit. Sans ce calcul, elle
 * se collerait à 0 et passerait DERRIÈRE les deux.
 *
 * « z-index: 24 » se lit dans l'échelle complète : en-tête de tableau 5,
 * BARRE DES SECTIONS 24, sous-header 25, barre de navigation 30, liste d'un
 * sélecteur 40, modale 50, notifications 60. Elle passe donc au-dessus du
 * contenu qui défile et SOUS le sous-header — l'inverse la ferait glisser
 * par-dessus le titre de la page au moment où l'on remonte.
 *
 * ⚠️ Elle défile HORIZONTALEMENT sur un écran étroit plutôt que de passer à la
 * ligne : une barre fixée qui grandit d'une ligne change la hauteur du chrome
 * en cours de route, et tout ce qui se cale dessous — le décalage du saut vers
 * une section — se décale d'autant. « scrollbar-width: none » retire la barre
 * de défilement, qui sur 44 px de haut mangerait un quart de la surface
 * cliquable.
 */
.sectionnav {
  position: sticky; top: calc(var(--topbar-height) + var(--subheader-height)); z-index: 24;
  height: var(--sectionnav-height);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}
.sectionnav-inner {
  display: flex; align-items: center; gap: var(--sp-1);
  height: 100%;
  max-width: var(--page-width); margin: 0 auto; padding: 0 var(--sp-5);
  overflow-x: auto; scrollbar-width: none;
}
.sectionnav-inner::-webkit-scrollbar { display: none; }
.sectionnav a {
  flex: none;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm); font-weight: 500;
  color: var(--text-dim); text-decoration: none; white-space: nowrap;
}
/* Le même fond de survol que la navigation du haut et que le fil d'Ariane :
   ces trois barres font le même geste — mener ailleurs dans le document — et
   une quatrième nuance de gris n'apprendrait rien à personne. */
.sectionnav a:hover { color: var(--text); background: var(--surface-3); }
.sectionnav a:focus-visible { box-shadow: var(--focus); outline: none; }
/* La section qu'on lit. Elle est marquée par « aria-current », donc l'état
   visuel et l'état annoncé ne peuvent pas diverger : il n'y a qu'un attribut. */
.sectionnav a[aria-current="true"] { color: var(--accent); background: var(--accent-soft); }

/*
 * Un champ et son unite.
 *
 * L unite est POSEE SUR le champ, pas mise a cote : un « % » dans une colonne
 * voisine ferait retrecir le champ de sa largeur, et l ensemble cesserait de se
 * lire comme un seul controle. Le rembourrage droit du champ lui laisse la
 * place, et « pointer-events: none » fait que cliquer sur l unite met le focus
 * dans le champ — c est ce qu on vise quand on clique juste a cote du nombre.
 */
.input-unit { position: relative; display: block; }
.input-unit .input { padding-right: 28px; }
.input-unit .unit {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  pointer-events: none;
  font-size: var(--fs-sm); color: var(--text-dim);
}

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

/*
 * ⚠️ CE CONTENEUR EST UN CONTEXTE DE DÉFILEMENT, ET C'EST LÀ QUE L'EN-TÊTE SE
 * CASSAIT.
 *
 * « overflow-x: auto » fait calculer « overflow-y: auto » par la même
 * occasion : l'élément devient un conteneur de défilement, et un thead en
 * position sticky s'y colle À LUI, plus au viewport. Le décalage
 * « top: var(--topbar-height) » poussait donc l'en-tête 58 px VERS LE BAS À
 * L'INTÉRIEUR du tableau, où il recouvrait les premières lignes — un en-tête
 * flottant au milieu des données, désaligné des colonnes.
 *
 * « top: 0 » supprime le décalage : dans un conteneur qui ne défile pas
 * verticalement, sticky est alors inopérant et l'en-tête se comporte en
 * en-tête normal. S'il défile — tableau plus haut que la fenêtre du
 * conteneur — il se colle proprement en haut de celui-ci.
 */
/*
 * LE CADRE DU TABLEAU EST PORTÉ PAR LE CONTENEUR, PAS PAR LES CELLULES.
 *
 * Une bordure posée sur les cellules de bord donne toujours un cadre approximatif :
 * elle se dédouble aux angles, disparaît sur la dernière ligne, et se décale
 * d'un pixel dès qu'une colonne est alignée à droite. Le conteneur porte donc
 * le cadre et les coins arrondis, et « overflow: hidden » suffit à faire suivre
 * les angles par l'en-tête et par la dernière ligne.
 *
 * ⚠️ « overflow: hidden » sur l'axe vertical rendrait le thead collant
 * inopérant : on garde donc « overflow-x: auto », qui coupe les angles
 * horizontalement, et les coins de l'en-tête sont arrondis explicitement.
 */
.table-wrap {
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

/*
 * UNE hauteur de ligne, UNE graisse, UNE taille. Toute cellule qui déroge
 * doit avoir une raison nommée : un lien de commande (row-link) et un titre de
 * produit (cell-title) en ont une, un prix et un SKU n'en ont pas.
 */
/*
 * ══════════════════════════════════════════════════════════════════════════
 * ⚠️ « table-layout: fixed » : LES COLONNES NE BOUGENT PLUS AVEC LEUR CONTENU.
 * ══════════════════════════════════════════════════════════════════════════
 *
 * En « auto », un navigateur mesure tout le tableau avant de le peindre et
 * donne à chaque colonne la largeur que son contenu réclame. Deux
 * conséquences, toutes deux mauvaises ici :
 *
 * 1. LES COLONNES SAUTENT D'UNE PAGE À L'AUTRE. Un client nommé
 *    « Établissements Vandenberghe-Lacroix » élargit sa colonne ; la page
 *    suivante ne le contient pas, la colonne se rétracte, et tout le tableau
 *    glisse latéralement sous l'œil qui suivait une ligne.
 * 2. L'EN-TÊTE COLLANT NE PROMET PLUS RIEN. Il reste en haut pendant qu'on
 *    défile, mais si les largeurs dépendaient du contenu visible, ce serait
 *    l'accord entre le titre et la colonne qu'on perdrait.
 *
 * En « fixed », seule la PREMIÈRE ligne décide — ici le <colgroup> — et le
 * contenu s'y plie. Le tableau se peint aussi sans être mesuré entièrement,
 * donc dès la première ligne reçue.
 *
 * ⚠️ LE PLANCHER DE 720 px EST INDISPENSABLE, il ne relève pas du confort. Sous
 * cette largeur, huit pour cent — la colonne du menu — tomberaient sous les
 * 48 px et le déclencheur déborderait de sa cellule. « .table-wrap » porte déjà
 * « overflow-x: auto » : en dessous, on fait défiler, ce qui est la bonne
 * réponse sur un téléphone. Au-dessus, il ne sert à rien.
 */
table.data {
  width: 100%; min-width: 720px;
  table-layout: fixed;
  border-collapse: collapse; font-size: var(--fs-sm);
}
table.data th, table.data td {
  text-align: left; padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--border); vertical-align: middle;
  font-weight: 400;
}
table.data thead th {
  position: sticky; top: 0; z-index: 5;
  background: var(--surface-2);
  font-size: var(--fs-xs); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--text-dim); white-space: nowrap;
  /* Même filet que partout ailleurs : l'en-tête se distingue déjà par sa
     surface, sa casse et sa graisse. Un trait plus épais par-dessus tout cela
     ne séparait pas mieux, il alourdissait. */
  border-bottom: 1px solid var(--border);
}
table.data thead th:first-child { border-top-left-radius: var(--radius-lg); }
table.data thead th:last-child { border-top-right-radius: var(--radius-lg); }
table.data tbody tr:hover { background: var(--surface-2); }
/*
 * ⚠️ « tbody:last-child tr:last-child », et NON « tbody tr:last-child ».
 *
 * C'est ici que les filets entre lignes disparaissaient. Sur l'écran Produits,
 * chaque produit est son PROPRE <tbody> — c'est ce qui le rend survolable d'un
 * bloc. « tbody tr:last-child » apparie donc la dernière ligne de CHAQUE
 * groupe, c'est-à-dire, pour un catalogue de produits simples, absolument
 * toutes les lignes : le tableau n'avait plus un seul filet, et rien dans la
 * règle ne le laissait deviner.
 *
 * Seule la toute dernière ligne du tout dernier groupe s'en passe : le cadre
 * du conteneur ferme déjà le tableau, et un filet de plus le doublerait.
 */
table.data tbody:last-child tr:last-child > td { border-bottom: 0; }
table.data td.num, table.data th.num { text-align: right; font-variant-numeric: tabular-nums; }
/*
 * ⚠️ L'ÉCHELLE DES LARGEURS, ET ELLE EST POSÉE SUR LES <col>.
 *
 * Ce ne sont plus des largeurs *souhaitées* — elles étaient sur les <th> et
 * « table-layout » valait « auto », donc une colonne au contenu plus large
 * s'élargissait quand même, ce qui revenait à ne rien fixer du tout.
 *
 * Neuf valeurs, et chaque tableau compose les siennes pour faire exactement
 * 100 %. Neuf, parce qu'on a cherché à réutiliser : cinq tableaux, vingt-trois
 * colonnes, et neuf nombres suffisent. Une valeur de plus serait une valeur que
 * personne ne saurait distinguer de sa voisine.
 */
table.data col.w-6 { width: 6%; }
table.data col.w-8 { width: 8%; }
table.data col.w-14 { width: 14%; }
table.data col.w-16 { width: 16%; }
table.data col.w-18 { width: 18%; }
table.data col.w-20 { width: 20%; }
table.data col.w-22 { width: 22%; }
table.data col.w-30 { width: 30%; }
table.data col.w-40 { width: 40%; }

/*
 * ⚠️ CE QUI DÉBORDE, MAINTENANT QUE PLUS RIEN NE S'ÉLARGIT.
 *
 * En « fixed », un mot plus large que sa colonne ne la pousse plus : il SORT,
 * et vient se peindre par-dessus la colonne voisine. « overflow-wrap: anywhere »
 * le coupe — c'est le cas d'une adresse e-mail longue, d'une URL de retour ou
 * d'un jeton, tous présents dans ces tableaux.
 *
 * Les lignes qui ne doivent PAS se replier — un nom suivi de sa référence, un
 * titre de produit, une date — se terminent par des points de suspension : sur
 * deux lignes, elles feraient respirer les rangées de façon irrégulière et le
 * tableau perdrait le peigne régulier qu'on vient de lui donner.
 */
table.data td { overflow-wrap: anywhere; }
table.data td.nowrap,
table.data .row-main,
table.data .row-sub,
table.data .cell-title {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/*
 * ⚠️ « display: block », SANS QUOI LA TRONCATURE NE FAIT RIEN.
 *
 * « overflow » ne s'applique pas à un élément en ligne non remplacé : sur le
 * <span> du titre de produit, les trois déclarations ci-dessus étaient
 * ignorées **en silence**. Le titre sortait de sa colonne de 40 % et venait se
 * peindre par-dessus le SKU — « … en caisse bois2 déclinaisons » — ce qui ne
 * ressemblait pas à un débordement mais à une faute de données.
 *
 * « .row-main » et « .row-sub » sont déjà des <div>, donc des blocs : c'est
 * pour cela qu'eux tronquaient correctement, et c'est ce qui rendait le défaut
 * difficile à voir — deux des trois cas marchaient.
 */
table.data .cell-title { display: block; }
table.data .row-main { font-weight: 600; }
table.data .row-sub { color: var(--text-dim); font-size: var(--fs-xs); }

/*
 * Lignes groupées : un PRODUIT porte ses DÉCLINAISONS en lignes secondaires.
 *
 * La hiérarchie se lit par la graisse et par un filet vertical, jamais par une
 * indentation seule : une indentation disparaît dès que la colonne se réduit,
 * et le tableau redevient une liste plate où produit et déclinaison ne se
 * distinguent plus.
 */
/* Un filet sous CHAQUE ligne, déclinaisons comprises. Les groupes n'en
   portaient qu'un, en bas, et une déclinaison se lisait alors comme une suite
   du texte de la ligne au-dessus plutôt que comme une ligne à part. */
/* Le titre du produit est la SEULE derogation de graisse : c est lui qui
   distingue une ligne principale d une ligne secondaire. */
table.data tr.tr-product .cell-title { font-weight: 600; }
table.data tr.tr-variant > td.cell-label {
  padding-left: var(--sp-6); position: relative; color: var(--text-dim);
}
table.data tr.tr-variant > td.cell-label::before {
  content: ''; position: absolute;
  left: var(--sp-5); top: 0; bottom: 0;
  border-left: 1px solid var(--border);
}
table.data tr.tr-product:hover, table.data tr.tr-variant:hover { background: transparent; }
table.data tbody.group:hover { background: var(--surface-2); }

.price-range { white-space: nowrap; }

/*
 * Vignette produit.
 *
 * La cellule est de largeur FIXE et l image de hauteur fixe : sans cela, la
 * hauteur des lignes suivrait le format de chaque photo, et un seul portrait
 * dans la liste ferait respirer tout le tableau de travers.
 */
/* ⚠️ PLUS DE LARGEUR ICI. Sous « table-layout: fixed », une largeur posée sur
   une cellule du corps n'est jamais consultée — seule la première ligne
   décide. Elle vit dans le <colgroup> (6 %), et ce qui reste est l'espacement,
   qui, lui, s'applique toujours. */
table.data td.cell-thumb { padding-right: 0; }
.thumb {
  width: 32px; height: 32px; border-radius: var(--radius-sm);
  object-fit: cover; display: block; background: var(--surface-2);
}
.thumb-empty {
  width: 32px; height: 32px; border-radius: var(--radius-sm);
  background: var(--surface-2); display: block;
}

.row-link {
  color: inherit; text-decoration: none; font-weight: 600;
  font-family: var(--mono); font-size: var(--fs-sm);
}
.row-link:hover { color: var(--accent); text-decoration: underline; }

/* ========================================================================== */
/* Pastilles                                                                  */
/* ========================================================================== */

.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px; border-radius: 999px;
  font-size: var(--fs-xs); font-weight: 600; white-space: nowrap;
  background: var(--surface-3); color: var(--text-dim);
}
.badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
/*
 * ⚠️ Le nom de classe suit le CODE du statut, pas son libellé : « to_verify »
 * et non « a-verifier ». C'est ce qui garantit qu'ajouter un statut dans
 * config.js suffit à lui donner une pastille, sans table de correspondance à
 * tenir de ce côté-ci.
 *
 * « À vérifier » prend le ton d'un avertissement : c'est une attente, pas un
 * état stable, et rien ne doit rester dans cet état sans qu'on le remarque.
 */
.badge-to_verify { background: var(--warn-soft); color: var(--warn); }
.badge-verified { background: var(--ok-soft); color: var(--ok); }
/* « Finalisée » est un ACCOMPLISSEMENT, pas une attente : le ton de l'accent,
   plus affirmé que le vert d'une relecture faite, parce que c'est le seul
   statut d'où l'on ne revient pas. */
.badge-finalized { background: var(--info-soft); color: var(--accent); }
/* Statuts hérités : des commandes enregistrées avant la bascule les portent
   encore, et elles ne sont pas réécrites. */
.badge-draft { background: var(--surface-3); color: var(--text-dim); }
.badge-confirmed { background: var(--info-soft); color: var(--accent); }
.badge-fulfilled { background: var(--ok-soft); color: var(--ok); }
.badge-cancelled { background: var(--danger-soft); color: var(--danger); }
.badge-plain::before { display: none; }

/* ========================================================================== */
/* Mise en page et texte                                                      */
/* ========================================================================== */

/*
 * L'ESPACEMENT APPARTIENT AU CONTENEUR, PAS À SES ENFANTS.
 *
 * ⚠️ Cette feuille a remplacé trente-huit attributs « style » en ligne, dont
 * treize divs vides de la forme « height:12px » servant à écarter deux
 * éléments. Ils existaient parce qu'aucun conteneur ne portait d'espacement :
 * chaque écran en reposait un, avec sa propre valeur, et corriger le rythme
 * vertical demandait de retrouver treize décisions éparpillées.
 *
 * Un « gap » sur le conteneur les rend tous inutiles. Ce n'est pas un
 * remplacement de la cale par une classe de cale — c'est sa suppression.
 */
.stack { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.stack-sm { gap: var(--sp-2); }
.stack-lg { gap: var(--sp-5); }

.row { display: flex; flex-wrap: wrap; gap: var(--sp-2); min-width: 0; }
.row-lg { gap: var(--sp-3); }
/* Aligne les BAS : ce dont a besoin un champ posé à côté d'un bouton, dont la
   hauteur n'inclut pas le libellé du champ. */
.row-end { align-items: flex-end; }

/* Pousse ce qui suit vers la droite. Un élément nommé, là où quatre écrans
   posaient un span vide portant « flex:1 ». */
.push { flex: 1 1 auto; }

/*
 * Les marges d'un paragraphe viennent du conteneur : le paragraphe n'en porte
 * aucune. « margin:0 0 8px » s'était répandu dans dix appels avec quatre
 * valeurs différentes, et chacune était invisible depuis les autres.
 */
.text { margin: 0; font-size: var(--fs-sm); color: var(--text); }
.text-dim { color: var(--text-dim); }
.text-strong { font-weight: 600; color: var(--text); }
.text-danger { color: var(--danger); }

/*
 * Bloc de commande à recopier. La police est monospacée : une commande shell
 * lue dans une police proportionnelle fait confondre l tiret et l underscore,
 * et un secret recopié de travers renvoie diagnostiquer ailleurs.
 */
.code {
  margin: 0;
  font-family: var(--mono); font-size: var(--fs-xs); line-height: var(--line-prose);
  background: var(--surface-3); color: var(--text);
  padding: var(--sp-3); border-radius: var(--radius);
  /* Défilement horizontal, PAS de retour à la ligne : une commande coupée en
     deux se recopie en deux morceaux, et le second est oublié. */
  overflow-x: auto;
}
/* Sauf pour une réponse brute, qui n'a pas de fin de ligne utile. */
.code-wrap { white-space: pre-wrap; overflow-x: visible; }

.badge-list { display: flex; flex-wrap: wrap; gap: var(--sp-1); }

/*
 * ⚠️ UN NOM ET SA PASTILLE, SUR UNE SEULE LIGNE.
 *
 * « Hors Shopify » vivait dans un .badge-list — conteneur de niveau BLOC —
 * donc sur une deuxième ligne, et son texte se trouvait en plus décalé vers la
 * droite par le rembourrage du médaillon. Deux désalignements pour une
 * information de deux mots.
 *
 * « align-items: center » centre la pastille sur la ligne du nom. « flex: none »
 * sur elle décide qui cède quand la colonne se resserre : c'est le nom, jamais
 * la pastille — une pastille tronquée à « Hors Sho… » ne dirait plus rien,
 * alors qu'un nom raccourci reste identifiable par le reste de la ligne.
 */
.cell-inline { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.cell-inline > .badge { flex: none; }


/* ========================================================================== */
/* Sélecteur                                                                  */
/* ========================================================================== */

/*
 * LA LISTE S'OUVRE SOUS LE CHAMP, SUR SA LARGEUR EXACTE.
 *
 * ⚠️ C'est ce que ni le « select » natif ni « datalist » ne garantissent. La
 * liste d'un « select » est dessinée par le système — police, couleurs et
 * position hors de portée de cette feuille — et Chrome place celle d'un
 * « datalist » À DROITE du champ quand la place manque en dessous. Deux listes
 * déroulantes qui ne se ressemblent pas dans le même formulaire apprennent à
 * l'opérateur que l'interface est approximative.
 *
 * « position: absolute » dans un parent « relative » : la liste sort du flux,
 * donc elle ne pousse rien, et son « width: 100% » la cale exactement sur le
 * champ.
 */
.select-wrap { position: relative; min-width: 0; }

.select-trigger {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; min-height: 38px;
  padding: 0 var(--sp-3);
  font: inherit; font-size: var(--fs-md); text-align: left;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  cursor: pointer;
}
.select-trigger:hover:not(:disabled) { border-color: var(--text-faint); }
.select-trigger:disabled { background: var(--disabled-bg); color: var(--text-dim); cursor: not-allowed; }
.select-trigger[aria-expanded="true"], .select-trigger:focus-visible {
  outline: none; border-color: var(--accent); box-shadow: var(--ring);
}
.select-value { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.select-caret { display: inline-flex; color: var(--text-faint); flex: none; }
.select-caret svg { width: 16px; height: 16px; }
/* Le chevron pivote quand la liste est ouverte : l'état se lit sans compter
   sur la seule présence de la liste, qui peut être hors du champ de vision. */
.select-trigger[aria-expanded="true"] .select-caret { transform: rotate(180deg); }

.select-list {
  position: absolute; z-index: 40;
  top: calc(100% + 4px); left: 0; width: 100%;
  max-height: 280px; overflow-y: auto;
  padding: var(--sp-1);
  background: var(--surface);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.select-option {
  padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-sm);
  font-size: var(--fs-sm); color: var(--text); cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.select-option:hover, .select-option.is-active { background: var(--surface-2); }
/* L'option COURANTE se distingue de l'option SURVOLÉE : la première dit ce qui
   est enregistré, la seconde ce qu'on s'apprête à choisir. */
.select-option[aria-selected="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.select-empty { padding: var(--sp-3); font-size: var(--fs-sm); color: var(--text-dim); text-align: center; }

/* ========================================================================== */
/* Menu d'actions d'une ligne                                                 */
/* ========================================================================== */

/*
 * ⚠️ SES PROPRES CLASSES, ET NON CELLES DU SÉLECTEUR.
 *
 * Le menu leur ressemble — même surface, même rayon — et c'est une
 * ressemblance, pas une identité : un sélecteur choisit une VALEUR parmi des
 * options équivalentes, ce menu déclenche des GESTES dont l'un est destructeur.
 * Le jour où l'un des deux doit changer, une classe partagée obligerait à
 * choisir entre les casser tous les deux ou en dériver un cas particulier.
 *
 * ILS DIVERGENT DÉJÀ, et c'est précisément la démonstration : le sélecteur
 * s'ouvre DANS le flux, collé à son champ, et une ombre légère suffit à le
 * décoller ; le menu s'ouvre en « position: fixed », détaché de tout, et il lui
 * faut l'ombre portée pour qu'on lise de quelle profondeur il vient.
 *
 * Les valeurs viennent des mêmes variables : la ressemblance est tenue par les
 * jetons, pas par la classe.
 */

/* ⚠️ « width: 1% » A DISPARU D'ICI. Elle demandait au navigateur de serrer la
   colonne au plus près de son contenu — une négociation que « table-layout:
   fixed » ne fait plus. La largeur est déclarée dans le <colgroup> de la table
   des commandes (8 %), au même endroit que toutes les autres. */
td.cell-actions { padding-left: 0; }

.menu-wrap { display: flex; justify-content: flex-end; }

/*
 * ══════════════════════════════════════════════════════════════════════════
 * ⚠️ LE DÉCLENCHEUR EST UN « btn btn-ghost btn-icon », ET C'EST TOUT LE SUJET.
 * ══════════════════════════════════════════════════════════════════════════
 *
 * Il portait « btn-icon menu-trigger » — sans « btn », donc sans le socle. Il en
 * héritait un rayon de 0 (un carré à angles vifs dans une feuille où tout est
 * arrondi à 8), une bordure de 2 px (le défaut du navigateur, « .btn » en pose
 * 1), aucune transition, et un survol écrit sur place qui **descendait**
 * l'échelle des surfaces : « background: var(--surface) », c'est-à-dire le fond
 * du tableau lui-même, repeint par-dessus une ligne déjà surlignée. Le bouton
 * se lisait comme un TROU percé dans la ligne — en clair comme en sombre.
 *
 * « .btn-ghost » fait déjà exactement ce qu'il faut : rien au repos, un fond au
 * survol qui MONTE l'échelle (« --surface-3 »). La règle locale l'avait réinventé
 * à l'envers. Ce qui reste ici est donc le peu qui soit vraiment propre à ce
 * bouton-là : sa discrétion au repos, sa forme carrée, et son état ouvert.
 */

/* ⚠️ PLUS EFFACÉ QUE LE « ghost » ORDINAIRE au repos. Un bouton visible sur
   chaque ligne ferait une colonne qui pèserait autant que les données — et il
   y en a un par commande. Il monte d'un cran au survol de SA ligne : le geste
   se propose quand on s'approche, pas avant. */
.menu-trigger { color: var(--text-faint); }
/* ⚠️ ET AU CLAVIER AUSSI. « tr:hover » ne se déclenche pas quand on tabule :
   sans cette seconde condition, celui qui navigue au clavier reçoit une bague
   de mise au point autour de trois points restés presque invisibles. */
tr:hover .menu-trigger, .menu-trigger:focus-visible { color: var(--text-dim); }

/* ⚠️ UN CARRÉ, PAS UN RECTANGLE. « .btn-icon » pose « width: 32px » et un
   « min-height » — sans hauteur ferme, la hauteur de ligne de la cellule
   l'étirait à 36 px. Un bouton d'icône à angles arrondis qui n'est pas carré
   se voit tout de suite : les coins du haut et ceux du bas ne se répondent
   plus. */
.menu-trigger { height: 32px; }

/*
 * ══════════════════════════════════════════════════════════════════════════
 * ⚠️ L'APPUI PORTE DÉJÀ LA COULEUR DE CE QU'IL VA OUVRIR.
 * ══════════════════════════════════════════════════════════════════════════
 *
 * Aucune règle « :active » n'existait nulle part dans la feuille : appuyer ne
 * répondait rien, et l'état pressé était le sosie exact du survol.
 *
 * Le menu s'ouvre au RELÂCHEMENT. Peindre l'appui de la teinte qu'aura l'état
 * ouvert fait de l'appui et de l'ouverture un seul mouvement — au lieu d'un
 * éclair beige suivi d'un basculement au noir. On voit où l'on va pendant
 * qu'on y va.
 *
 * ⚠️ ET REFERMER RÉPOND AUSSI. Sans la seconde règle, la fermeture serait le
 * seul geste de la série sans réponse : on appuierait sur un carré déjà plein
 * et rien ne bougerait. « --accent-hover » est le cran voisin, dans le bon sens
 * dans les deux thèmes.
 *
 * « :not(:disabled) » n'est pas ici une garde de sémantique — ce bouton n'est
 * jamais désactivé — mais de SPÉCIFICITÉ : sans lui la règle retombe à deux
 * classes et « .btn-ghost:hover:not(:disabled) », qui en compte trois,
 * l'écrase en silence.
 *
 * Un enfoncement (« transform: scale ») disait la même chose, mais il couple
 * une décoration à une MESURE : RowMenu.placer() lit le rectangle du
 * déclencheur pour poser un menu en « position: fixed », et un rectangle
 * transformé y entrerait tel quel.
 */
.menu-trigger:active:not(:disabled) {
  background: var(--accent); border-color: var(--accent); color: var(--accent-text);
}
.menu-trigger[aria-expanded="true"]:active:not(:disabled) {
  background: var(--accent-hover); border-color: var(--accent-hover);
}

/*
 * ⚠️ OUVERT : LE PLEIN, ET NON UN BEIGE DE PLUS.
 *
 * L'état valait « --surface-2 » — la couleur exacte d'une ligne survolée : le
 * seul bouton de la page dont un panneau dépend ne se distinguait de rien.
 * Trois surfaces séparées par 4 % de luminance ne suffisent pas à porter trois
 * états ; le contraste inversé, lui, ne se confond avec aucun.
 *
 * Et il le faut : le menu est en « position: fixed », détaché de sa ligne. Sans
 * un déclencheur franchement marqué, rien ne dit plus de QUELLE commande sort
 * le panneau qu'on est en train de lire.
 */
/* ⚠️ ET IL FAUT LES DEUX SELECTEURS. « .btn-ghost:hover:not(:disabled) »
   compte trois classes ; « .menu-trigger[aria-expanded] » n en compte que
   deux. Le survol l emportait donc, et l etat ouvert disparaissait tant que le
   pointeur restait sur le bouton — c est-a-dire juste apres l avoir clique,
   precisement quand on lit le menu. Le second selecteur remet les deux a
   egalite, et l ordre de la feuille tranche en faveur de celui-ci. */
.menu-trigger[aria-expanded="true"],
.menu-trigger[aria-expanded="true"]:hover {
  background: var(--accent); border-color: var(--accent); color: var(--accent-text);
}

/* Les trois points sont des segments de longueur nulle arrondis : à 15 px ils
   sont maigres. Deux points de plus les rendent lisibles sans les épaissir. */
.menu-trigger svg { width: 17px; height: 17px; }

/*
 * ══════════════════════════════════════════════════════════════════════════
 * ⚠️ « position: fixed », ET C'EST LA SEULE FAÇON QUE LE MENU SORTE DU TABLEAU.
 * ══════════════════════════════════════════════════════════════════════════
 *
 * Le tableau vit dans « .table-wrap { overflow-x: auto } ». La règle CSS est sans
 * appel : quand un axe vaut « auto », l'autre ne peut plus valoir « visible » — il
 * devient « auto » lui aussi. Le conteneur découpe donc dans les DEUX directions,
 * et un menu en « position: absolute » y était tranché net, ou pire, ajoutait une
 * barre de défilement au tableau entier.
 *
 * « fixed » le sort de tout ancêtre à défilement. En échange il ne suit plus
 * rien : ses coordonnées sont mesurées à l'ouverture (voir RowMenu) et tout
 * défilement le referme. Les valeurs ci-dessous ne sont qu'un repli pour le
 * cas où la mesure est impossible.
 *
 * « z-index: 45 » se lit dans l'échelle complète : en-tête de tableau 5,
 * sous-header 25, barre d'actions 28, barre de navigation 30, liste déroulante
 * 40 — le menu passe au-dessus de tous — puis modale 50 et bandeau 60, qui
 * passent au-dessus de lui. Un menu au-dessus d'une modale recouvrirait la
 * question qu'on vient de lui poser.
 */
.menu-list {
  position: fixed; z-index: 45;
  top: 0; right: 0; min-width: 232px;
  padding: var(--sp-1);
  background: var(--surface);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  text-align: left;
}

/*
 * Une entrée : l'icône, puis le libellé.
 *
 * ⚠️ L'ICÔNE EST CELLE DU BOUTON DE LA FICHE, parce qu'elle vient du même
 * descripteur. Un geste se reconnaît à sa forme, qu'on le déclenche depuis la
 * liste ou depuis la commande — et une colonne d'icônes alignées se parcourt
 * plus vite qu'une colonne de mots.
 *
 * « flex: none » sur l'icône : c'est le libellé qui cède si la place manque,
 * jamais le signe qui identifie le geste.
 */
.menu-item {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-sm);
  font-size: var(--fs-sm); color: var(--text); cursor: pointer; white-space: nowrap;
}
.menu-item svg {
  width: 15px; height: 15px; flex: none;
  /* Le signe se lit au second regard : il guide, il ne crie pas. Sauf sur le
     geste destructeur, où il porte la même couleur que son libellé. */
  color: var(--text-faint);
}
.menu-item:hover { background: var(--surface-2); }
.menu-item:hover svg { color: currentColor; }
.menu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Le geste destructeur porte sa couleur DANS le menu aussi : la barre
   d'actions le distingue par le rouge, le menu doit le distinguer pareil —
   sinon « Supprimer » se lit comme « Archiver » à un pixel près. */
.menu-item-danger, .menu-item-danger svg { color: var(--danger); }
.menu-item-danger:hover { background: var(--danger-soft); }

/*
 * L'écart qui sépare les gestes ordinaires de ceux qui engagent. Il est posé
 * par le décideur (« separate »), pas par la position : le jour où un geste
 * s'intercale, la respiration reste du bon côté.
 *
 * ⚠️ UN ESPACE, ET PLUS UN TRAIT. La règle portait « border-top: 1px solid » :
 * c'était la SEULE bordure à l'intérieur du menu, et elle traversait une
 * colonne de boutons qui, eux, n'en portent aucune. Un menu dont le cadre
 * extérieur dit déjà « ceci est un panneau » n'a pas besoin d'un second trait
 * pour dire « ceci est un groupe » — l'espacement le dit, et il le dit sans
 * couper la surface cliquable en deux au survol.
 */
.menu-item-cut { margin-top: var(--sp-2); }

/* Désactivé : lisible, mais sans promesse. Un fond, jamais une opacité — on
   veut pouvoir LIRE ce qu'on ne peut pas faire, et l'infobulle dit pourquoi. */
.menu-item-off, .menu-item-off svg { color: var(--text-faint); }
.menu-item-off { cursor: not-allowed; }
.menu-item-off:hover { background: transparent; }
.menu-item-off:hover svg { color: var(--text-faint); }

/* ========================================================================== */
/* Prompt du serveur MCP                                                      */
/* ========================================================================== */

/*
 * Un segment : son nom, son état, le geste qui le remet au défaut, puis son
 * aide et le texte lui-même.
 *
 * ⚠️ Le nom du segment est en monospace comme le texte qu'il désigne : ce sont
 * des chemins de champs — « customer.billingAddress.city » — et une police
 * proportionnelle y fait confondre le point et la virgule.
 */
.prompt-segment { display: flex; flex-direction: column; gap: var(--sp-2); }
.prompt-label { font-size: var(--fs-sm); word-break: break-word; }

/* ========================================================================== */
/* Pagination                                                                 */
/* ========================================================================== */

/* Au CENTRE, et deux flèches. Le compte « 50 commandes affichées » comptait ce
   qu'on a sous les yeux, jamais ce qu'on veut savoir — combien il en reste. */
.pager {
  display: flex; justify-content: center; align-items: center; gap: var(--sp-2);
  margin-top: var(--sp-4);
}

/* Le repérage, entre les deux flèches. Tabulaire : sans cela les chiffres
   changent de largeur d'une page à l'autre et les flèches dansent. */
.pager-label {
  font-size: var(--fs-sm); color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  min-width: 12ch; text-align: center;
}

/* ========================================================================== */
/* Barre de filtres                                                           */
/* ========================================================================== */

.filters {
  display: flex; gap: var(--sp-3); align-items: flex-end; flex-wrap: wrap;
  margin-bottom: var(--sp-4);
}
.filters .field { flex: 0 1 auto; }
/* La barre de recherche n'a pas de libellé au-dessus d'elle : elle s'aligne
   donc sur le BAS des autres champs, comme leur contrôle, et non sur leur
   libellé — sinon elle flotterait un cran trop haut dans la rangée. */
.filters-search { flex: 1 1 280px; min-width: 0; }
/* Un filtre garde une largeur lisible sans s'étirer : « Tous les statuts » est
   le plus long libellé qu'il ait à porter. */
.filters-item { flex: 0 0 200px; min-width: 0; }

/* ========================================================================== */
/* Barre de recherche                                                         */
/* ========================================================================== */

/*
 * Une loupe À GAUCHE, dans le cadre du champ.
 *
 * ⚠️ Le cadre appartient au GROUPE, comme pour le sélecteur de quantité, et
 * pour la même raison : une icône posée à côté d'un champ encadré donne deux
 * boîtes, et l'anneau de focus n'en entoure qu'une. Ici, le champ est nu et le
 * groupe porte bordure, survol et focus — l'ensemble se comporte comme un
 * unique contrôle.
 *
 * L'icône est en « position: absolute » plutôt qu'en flex : le champ occupe
 * ainsi toute la largeur du groupe, donc toute sa surface est cliquable. Une
 * icône en flex laisserait une bande morte de 34 px à gauche, où un clic ne
 * donnerait pas le focus au champ.
 */
.searchbar {
  position: relative;
  /* « stretch » et non « center » : le champ remplit la hauteur du groupe,
     donc toute la boîte donne le focus au clic. Centré, il laissait neuf
     pixels morts en haut et en bas, où cliquer ne faisait rien. */
  display: flex; align-items: stretch;
  min-height: 38px;
  background: var(--surface);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
}
.searchbar:not(:has(:disabled)):hover { border-color: var(--text-faint); }
.searchbar:has(:disabled) { background: var(--disabled-bg); }
.searchbar:focus-within {
  border-color: var(--accent);
  box-shadow: var(--ring);
}
/*
 * ⚠️ « top: 0; bottom: 0 » n'est pas décoratif.
 *
 * Un enfant en position absolue ne suit PAS l'align-items de son parent flex
 * au-delà de sa position statique : avec « stretch » sur le groupe et aucune
 * contrainte verticale ici, la loupe se posait en HAUT de la boîte au lieu de
 * son milieu. Le bloc s'étire donc sur toute la hauteur, et c'est son propre
 * align-items qui centre le dessin.
 */
.searchbar-icon {
  position: absolute; left: var(--sp-3); top: 0; bottom: 0;
  display: inline-flex; align-items: center;
  color: var(--text-faint); pointer-events: none;
}
.searchbar-icon svg { width: 16px; height: 16px; }
.searchbar-input {
  flex: 1 1 auto; min-width: 0;
  font: inherit; font-size: var(--fs-md);
  /* Le rembourrage gauche laisse la place à la loupe : 12 px de marge, 16 px
     d'icône, 10 px de respiration. */
  padding: 0 var(--sp-3) 0 38px;
  border: 0; background: none; color: var(--text);
  /* Le cadre du groupe porte déjà l'anneau de focus. */
  outline: none;
}
.searchbar-input::placeholder { color: var(--text-faint); }
/* La croix native de WebKit sur un input[type=search] ne suit ni la couleur ni
   la taille du reste : on la retire, la sélection au clavier suffit. */
.searchbar-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }

/* Date de livraison planifiée : révélée par le mode choisi, jamais recréée. */
.scheduled-date { margin-top: var(--sp-4); max-width: 260px; }

.pagination {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
  margin-top: var(--sp-4);
  color: var(--text-dim); font-size: var(--fs-sm);
}

/* ========================================================================== */
/* Lignes de produits                                                         */
/* ========================================================================== */

/* ========================================================================== */
/* Modale d'ajout de produits                                                 */
/* ========================================================================== */

/* Plus large que la modale de confirmation : elle porte une LISTE, pas une
   phrase. La hauteur est plafonnée et c'est la liste seule qui défile, pour que
   la recherche et le bouton d'ajout restent sous les yeux. */
.modal-wide { max-width: var(--modal-wide-width); width: 100%; }
/* Le corps d'une modale de confirmation est un texte secondaire, donc atténué ;
   celui-ci porte des noms de produits, qui sont le contenu principal. */
.modal-wide .modal-body {
  max-height: min(68vh, 620px); overflow-y: auto; color: var(--text);
}

.pm-list { display: flex; flex-direction: column; }
/* Liste vide : le message respire au lieu de se coller au champ de recherche. */
.pm-empty { padding: var(--sp-4); }
.pm-row {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) 0;
  border-bottom: 1px solid var(--border);
}
.pm-row:last-child { border-bottom: 0; }
.pm-main { flex: 1; min-width: 0; }
.pm-title { font-size: var(--fs-sm); }
.pm-meta { font-size: var(--fs-xs); color: var(--text-dim); }
.pm-price {
  font-size: var(--fs-sm); color: var(--text-dim);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Ici, et seulement ici, la largeur est figée : la modale est une liste de
   lignes en flex, et un groupe élastique ferait danser la colonne des prix
   d'une ligne à l'autre. La base flex l'emporte sur width: 100%. */
.pm-row .qs { flex: 0 0 116px; }

.modal-foot .hint { font-size: var(--fs-sm); color: var(--text-dim); }

.line-items { display: flex; flex-direction: column; gap: var(--sp-2); }
/* Le titre et sa précision doivent pouvoir se tronquer : sans min-width: 0, un
   nom de produit long pousse les colonnes de prix hors de la grille. */
.li-main { min-width: 0; }

/* ⚠️ SEPT COLONNES : vignette, produit, quantite, prix, REMISE, total, retrait.
   La remise s intercale entre le prix et le total, a l aplomb de ce qu elle
   modifie. La colonne de quantite reste la TROISIEME : un test lit sa largeur
   a cette position pour verifier qu elle contient le plancher du selecteur de
   quantite, et deplacer la colonne aurait fait comparer la largeur du prix. */
.line-item {
  display: grid; align-items: center; gap: var(--sp-3);
  grid-template-columns: 44px minmax(0, 1fr) 116px 132px 92px 116px 34px;
  padding: var(--sp-3); border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2);
}
@media (max-width: 980px) {
  .line-item { grid-template-columns: 44px minmax(0, 1fr) 34px; }
  .line-item .li-qty, .line-item .li-price, .line-item .li-discount { grid-column: 2 / 3; }
  .line-item .li-total { grid-column: 2 / 4; text-align: left; }
}

.li-thumb {
  width: 44px; height: 44px; border-radius: var(--radius-sm);
  object-fit: cover; background: var(--surface-3); border: 1px solid var(--border);
}
.li-thumb.placeholder { display: flex; align-items: center; justify-content: center; color: var(--text-faint); }
.li-title { font-weight: 600; overflow-wrap: anywhere; }
.li-meta { font-size: var(--fs-xs); color: var(--text-dim); overflow-wrap: anywhere; }
.li-total { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
/* ⚠️ LE BRUT BARRE NE PARAIT QU AVEC UNE REMISE. Il est pose au-dessus du net,
   plus petit et attenue : ce qui compte est le montant que la ligne coute, et
   le montant barre n est la que pour dire de combien on est parti. Le masquer
   avec [hidden] plutot que le retirer garde la hauteur de la cellule stable
   d une frappe a l autre. */
.li-gross {
  font-size: var(--fs-xs); font-weight: 400; color: var(--text-dim);
  text-decoration: line-through;
}
.li-gross[hidden] { display: none; }
.li-net { font-variant-numeric: tabular-nums; }
.li-drift {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 3px; font-size: var(--fs-xs); color: var(--warn);
}
.li-drift button {
  font: inherit; font-size: var(--fs-xs); color: var(--accent); background: none;
  border: 0; padding: 0; cursor: pointer; text-decoration: underline;
}

/* ========================================================================== */
/* Sélecteur de produits                                                      */
/* ========================================================================== */


/* ========================================================================== */
/* Totaux                                                                     */
/* ========================================================================== */

.totals { display: flex; flex-direction: column; gap: var(--sp-2); }
.totals-row {
  display: flex; justify-content: space-between; gap: var(--sp-4);
  font-size: var(--fs-sm); font-variant-numeric: tabular-nums;
}
.totals-row span:first-child { color: var(--text-dim); }
.totals-row.grand {
  padding-top: var(--sp-3); margin-top: var(--sp-1);
  border-top: 1px solid var(--border);
  font-size: var(--fs-lg); font-weight: 600;
}
.totals-row.grand span:first-child { color: var(--text); }


/*
 * Le formulaire occupe TOUTE la colonne de contenu.
 *
 * Il était en deux colonnes, la seconde de 320 px portant un récapitulatif
 * collant. Deux conséquences, et la seconde est la vraie : les champs étaient
 * comprimés sur les deux tiers de la page — une adresse et deux téléphones
 * dans une grille de deux colonnes de 320 px — et le total flottait à côté des
 * coordonnées du client, où il ne se rapporte à rien. Les montants découlent
 * des LIGNES : ils sont désormais sous elles, dans la même carte.
 */
/* ========================================================================== */
/* Barre d'actions                                                            */
/* ========================================================================== */

/*
 * FIXÉE en bas de l'écran, sur toute la largeur.
 *
 * Les actions closaient la page, en fin de flux. Sur une commande à huit
 * lignes, « Enregistrer » était à deux écrans de défilement du champ qu'on
 * venait de corriger : descendre pour valider, remonter pour vérifier,
 * redescendre. Fixée, la barre est toujours à portée.
 *
 * « position: fixed » et non « sticky » : sticky s'arrête aux bords de son
 * parent, donc la barre disparaîtrait dès qu'on dépasse le bas du formulaire.
 * Fixed la rive au viewport, ce qui est exactement ce qu'on veut d'une barre
 * d'actions.
 */
.actionbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 28;
  height: var(--actionbar-height);
  background: var(--surface);
  /*
   * ⚠️ UNE BORDURE, COMME L'EN-TÊTE. AUCUNE OMBRE.
   *
   * Cette barre portait « box-shadow: 0 -2px 8px rgba(16,24,40,.06) » alors que
   * la barre de navigation et le sous-header, qui font le même travail en haut
   * de page, n'ont qu'un trait d'un pixel. Les deux bords du même écran ne se
   * ressemblaient donc pas.
   *
   * Et cette ombre était un LITTÉRAL de thème clair, pas un jeton : en thème
   * sombre elle valait 6 % d'un gris bleuté sur un fond noir, c'est-à-dire
   * rien. La barre vivait déjà sans ombre pour la moitié des utilisateurs, et
   * personne n'y a trouvé à redire — c'est la réponse empirique à « faut-il la
   * réduire ou la retirer ».
   */
  border-top: 1px solid var(--border);
}
.actionbar-inner {
  display: flex; align-items: center; gap: var(--sp-3);
  height: 100%; max-width: var(--page-width); margin: 0 auto;
  padding: 0 var(--sp-5);
  /* Les actions débordent plutôt que de s'empiler : une barre de hauteur fixe
     ne peut pas accueillir une deuxième rangée. */
  overflow-x: auto;
}

/*
 * ⚠️ LA MARGE QUE LE CONTENU RÉSERVE SOUS LUI.
 *
 * La barre est en « fixed », donc hors du flux : elle recouvre le bas de la
 * page. Sans cette marge, la dernière carte passe dessous et son dernier champ
 * devient inatteignable — on peut le voir mais pas le cliquer.
 */
main.has-actionbar { padding-bottom: calc(var(--actionbar-height) + var(--sp-5)); }

@media (max-width: 700px) {
  .actionbar-inner { padding: 0 var(--sp-3); gap: var(--sp-2); }
}

/* ========================================================================== */
/* États                                                                      */
/* ========================================================================== */

.empty { padding: var(--sp-7) var(--sp-5); text-align: center; color: var(--text-dim); }
/* Hors carte — page Produits vide, page introuvable — l etat vide porte sa
   propre surface, sinon il flotte sur le fond de page sans rien pour le tenir. */
.empty-standalone {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}
.empty h3 { color: var(--text); margin-bottom: var(--sp-2); }
.empty p { max-width: 460px; margin: 0 auto var(--sp-4); font-size: var(--fs-sm); }
/* Sans action sous le message, la marge basse laisse un blanc pendant. */
.empty p:last-child { margin-bottom: 0; }

.notice {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  padding: var(--sp-3) var(--sp-4); border-radius: var(--radius);
  font-size: var(--fs-sm); border: 1px solid transparent;
}
.notice svg { flex: none; width: 16px; height: 16px; margin-top: 1px; }
/* Le corps prend toute la largeur restante : sinon un message long se replie
   dans une colonne étroite à côté de l'icône, au lieu d'occuper le bandeau. */
.notice > .stack { flex: 1 1 auto; }
.notice-info { background: var(--info-soft); color: var(--text); border-color: var(--border); }
.notice-warn { background: var(--warn-soft); color: var(--warn); border-color: currentColor; }
.notice-error { background: var(--danger-soft); color: var(--danger); border-color: currentColor; }
.notice-ok { background: var(--ok-soft); color: var(--ok); border-color: currentColor; }
.notice + .notice { margin-top: var(--sp-2); }
.notice p:last-child { margin-bottom: 0; }

.skeleton {
  height: 14px; border-radius: var(--radius-sm);
  background: linear-gradient(90deg, var(--surface-3) 25%, var(--surface-2) 50%, var(--surface-3) 75%);
  background-size: 200% 100%; animation: shimmer 1.3s infinite linear;
}
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; } }

.spinner {
  width: 15px; height: 15px; flex: none;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: 50%; animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2.5s; } }

/* ========================================================================== */
/* Connexion                                                                  */
/* ========================================================================== */

.login-shell {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  padding: var(--sp-5);
}
/* Une carte détachée du flux qui la précède : l'historique d'une commande,
   posé sous le formulaire. Le formulaire n'a pas de marge basse, c'est donc
   à ce qui le suit de s'écarter. */
.card-spaced { margin-top: var(--sp-4); }

.login-card { width: 100%; max-width: 400px; }
/*
 * ⚠️ PAS DE REMBOURRAGE PROPRE. La carte portait « padding: var(--sp-6) », un
 * cran au-dessus de toutes les autres cartes de l'application : il avait été
 * calibré pour un titre, un paragraphe d'explication et un pied de page qui
 * n'existent plus. Il ne reste qu'un champ et un bouton, et le rembourrage
 * standard leur suffit — une exception qui ne sert plus rien est une exception
 * qu'il faut retirer, pas conserver « au cas où ».
 */
/* ⚠️ Aucune règle d'emplacement de logo pour la connexion ici, et ce n'est pas
   un oubli : l'écran de connexion n'en porte plus. Il était vide — un bloc
   réservé à une image qui n'existe pas se lit comme une image qui n'a pas
   chargé, et il poussait le champ vers le bas pour rien. L'emplacement reste
   dans l'en-tête, où il tient la colonne centrale d'une grille en trois
   parties ; ici, il n'y avait aucune grille à tenir. */

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

/*
 * Pile flottante EN HAUT AU CENTRE. « pointer-events: none » sur la pile et
 * « auto » sur chaque toast : sans cela, une bande invisible en haut de l'écran
 * intercepterait les clics sur la barre de navigation, qui est juste dessous.
 */
.toasts {
  position: fixed; z-index: 60; top: var(--sp-4); left: 50%;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: var(--sp-2);
  width: min(560px, calc(100vw - 32px));
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex; gap: var(--sp-3); align-items: flex-start;
  width: 100%;
  padding: var(--sp-3) var(--sp-4);
  /* Aucune bordure : l ombre portee suffit a detacher la notification du fond,
     et le ton se lit deja sur son icone. Un filet colore a gauche ajoutait un
     troisieme signal pour la meme information. */
  background: var(--surface); border: 0;
  border-radius: var(--radius); box-shadow: var(--shadow-lg);
  font-size: var(--fs-sm);
  animation: toast-in 160ms ease-out;
}
@keyframes toast-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
}
.toast .toast-icon { flex: 0 0 auto; margin-top: 1px; }
.toast.ok .toast-icon { color: var(--ok); }
.toast.error .toast-icon { color: var(--danger); }
.toast.warn .toast-icon { color: var(--warn); }
.toast.info .toast-icon { color: var(--text-dim); }
.toast .toast-body { flex: 1; min-width: 0; }
.toast .toast-close {
  border: 0; background: none; cursor: pointer; color: var(--text-faint);
  padding: 0; line-height: 0; flex: 0 0 auto;
}
.toast .toast-close:hover { color: var(--text); }

/* ========================================================================== */
/* Boîte de dialogue                                                          */
/* ========================================================================== */

.modal-backdrop {
  position: fixed; inset: 0; z-index: 50;
  background: rgba(12, 16, 22, 0.55);
  display: flex; align-items: center; justify-content: center; padding: var(--sp-4);
}
.modal {
  width: 100%; max-width: 460px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
}
/* Rangée : un titre, et de quoi poser une croix de fermeture à droite. */
.modal-head {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-5) 0;
}
.modal-body { padding: var(--sp-4) var(--sp-5); font-size: var(--fs-sm); color: var(--text-dim); }
.modal-body strong { color: var(--text); }
.modal-foot {
  display: flex; justify-content: flex-end; gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-5); border-top: 1px solid var(--border);
}

/* ========================================================================== */
/* Chronologie                                                                */
/* ========================================================================== */

.timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.timeline li { display: flex; gap: var(--sp-3); font-size: var(--fs-sm); }
.timeline time { flex: none; width: 130px; color: var(--text-faint); font-variant-numeric: tabular-nums; font-size: var(--fs-xs); padding-top: 2px; }

/*
 * Onglets d'un écran.
 *
 * ⚠️ Le même vocabulaire visuel que la barre de navigation du haut (.topnav) :
 * un fond « accent-soft » sur l'élément courant, du texte atténué ailleurs.
 * Inventer une deuxième apparence de « rangée d'onglets » apprendrait deux fois
 * la même chose, et la seconde version dériverait de la première.
 *
 * Ce sont des BOUTONS et non des liens : un onglet montre une autre partie de
 * la même page, il ne mène pas ailleurs. La remise à plat du bouton est donc
 * explicite — sans elle, l'apparence dépendrait du navigateur.
 */
.tabs { display: flex; gap: var(--sp-1); margin-bottom: var(--sp-4); }
.tab {
  appearance: none; border: 0; background: none; cursor: pointer;
  font: inherit; font-size: var(--fs-sm); font-weight: 500;
  padding: var(--sp-2) var(--sp-3); border-radius: var(--radius);
  color: var(--text-dim);
}
.tab:hover:not(:disabled) { background: var(--surface-3); color: var(--text); }
.tab.current { background: var(--accent-soft); color: var(--accent); }

/* Intertitre à l'intérieur d'une carte. Il ne prend pas le style d'un titre de
   carte : ce n'en est pas un, et lui donner la même graisse ferait croire à une
   seconde carte. */
.card-subtitle { font-size: var(--fs-sm); color: var(--text-dim); margin: 0; }

/* Liste des manques d'une commande incomplete, dans un bandeau.
   « currentColor » : la puce prend la couleur du bandeau qui la porte, sans
   qu'un ton d'avertissement soit recopie ici — un second endroit qui decide
   d'une couleur finirait par ne plus s'accorder au premier. */
.gap-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.gap-list li { position: relative; padding-left: var(--sp-4); }
.gap-list li::before { content: ''; position: absolute; left: 4px; top: 7px; width: 4px; height: 4px; border-radius: 50%; background: currentColor; }

.kv { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: var(--sp-2) var(--sp-4); font-size: var(--fs-sm); }
.kv dt { color: var(--text-dim); }
.kv dd { margin: 0; overflow-wrap: anywhere; }
@media (max-width: 620px) { .kv { grid-template-columns: minmax(0, 1fr); gap: 2px var(--sp-4); } .kv dd { margin-bottom: var(--sp-2); } }

