/* IBM Plex Sans/Mono auto-hébergées (app/static/fonts/, licence SIL OFL —
   voir ibm-plex-LICENSE.txt) plutôt que chargées depuis Google Fonts : la
   version packagée en .exe (PyInstaller, voir CLAUDE.md) doit fonctionner
   sans connexion internet. Remplace le pur system-ui d'avant (aucune
   personnalité typographique propre à B-Facture) — Plex, dessinée pour du
   logiciel d'entreprise, reste sobre en corps de texte mais a assez de
   caractère en gras pour porter les titres/KPI ; sa variante Mono (déjà
   utilisée ponctuellement sur .ref-code) est étendue aux montants pour un
   rendu "grand livre" aux chiffres bien alignés. */
@font-face {
  font-family: "IBM Plex Sans"; font-style: normal; font-weight: 400;
  font-display: swap; src: url("fonts/ibm-plex-sans-400.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans"; font-style: normal; font-weight: 500;
  font-display: swap; src: url("fonts/ibm-plex-sans-500.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans"; font-style: normal; font-weight: 600;
  font-display: swap; src: url("fonts/ibm-plex-sans-600.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans"; font-style: normal; font-weight: 700;
  font-display: swap; src: url("fonts/ibm-plex-sans-700.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-weight: 500;
  font-display: swap; src: url("fonts/ibm-plex-mono-500.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono"; font-style: normal; font-weight: 600;
  font-display: swap; src: url("fonts/ibm-plex-mono-600.woff2") format("woff2");
}

:root {
  --font-sans: "IBM Plex Sans", -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: "IBM Plex Mono", "SF Mono", "Cascadia Code", Consolas, "Courier New", monospace;
  /* Palette B-In-Tech alignée sur la charte validée (aperçu "Design system") :
     navy #262f39 + orange #fd782d, plutôt que le bleu-ardoise générique
     d'avant. */
  --primary: #fd782d;
  --primary-2: #262f39;
  --primary-soft: #fed4bc;
  /* Orange dédié au fond des boutons pleins (texte blanc) : le --primary
     "vif" ci-dessus n'offre qu'un contraste d'environ 2.7:1 avec du blanc
     (bien sous le seuil WCAG AA de 4.5:1 pour du texte normal) — d'où le
     texte perçu comme flou/peu lisible malgré un rendu net. Même teinte,
     juste assombrie (~4.6:1) ; --primary reste inchangé pour les usages
     texte-sur-clair (liens, badges, bordures) qui n'ont pas ce problème. */
  --primary-btn: #c85f1f;
  /* Gris sourds, JAMAIS de blanc pur #fff : le blanc franc sur cartes/
     tableaux/champs avait été jugé agressif à l'œil en thème clair. Cette
     décision tient — B-Web (l'admin du site B-Tech) utilise bien #ffffff,
     mais on garde ici le gris sourd plutôt que de s'aligner sur ce point.

     En revanche l'ÉCART entre les deux était trop faible (2026-08-28) :
     #e6e9ed contre #eef1f4 ne donnait qu'un rapport de 1,07 — autrement dit
     une carte ne se détachait pratiquement pas de la page, d'où une
     impression générale de platitude malgré une charte par ailleurs bien
     appliquée. Le fond descend et la surface remonte, sans jamais atteindre
     le blanc : rapport porté à 1,19, et tous les textes y gagnent au passage
     (corps 11,97 -> 12,53 ; texte discret 6,07 -> 6,35). */
  --bg: #dfe3ea;
  --surface: #f4f6f9;
  /* Dégradé de fond + halo orange + filigrane : l'identité visuelle de
     l'écran de connexion, étendue à toute l'app (2026-08-28, demande
     utilisateur).
     
     Il SUIT LE THÈME, ce qui n'était pas le cas avant : le dégradé était
     écrit en dur avec `var(--primary-2)` au milieu, or ce token s'inverse
     entre les deux thèmes (#262f39 en clair, #e9ebef en sombre). L'écran de
     connexion se retrouvait donc sombre en thème CLAIR et clair en thème
     SOMBRE — exactement l'inverse de ce qu'on attend.
     
     Le stop le plus foncé est calé sur la lisibilité, pas sur l'esthétique :
     un <h1> de page repose directement sur ce fond (jamais dans une carte).
     À #ced5e1, le texte discret garde 4,66:1 — au-dessus du seuil AA de 4,5
     — et les cartes s'en détachent encore nettement (1,36). */
  --hero-grad: linear-gradient(160deg, #eaeff5 0%, #dae1eb 55%, #ced5e1 100%);
  --hero-glow: color-mix(in srgb, var(--primary) 26%, transparent);
  /* `soft-light` plutôt qu'une opacité : le filigrane s'imprime dans le
     dégradé au lieu de flotter par-dessus, et il reste discret sans avoir à
     fabriquer deux PNG pré-atténués (un par thème). */
  --hero-watermark-blend: soft-light;
  /* Trame hexagonale : la signature de la charte B-In-Tech (reprise de la
     carte de visite, 2026-08-28). Traits blancs dans le SVG, teintés ici par
     l'opacité — un seul fichier sert les deux thèmes. En clair, la trame doit
     rester à la limite du perceptible : elle habille, elle ne doit jamais
     entrer en concurrence avec un tableau de chiffres. */
  /* Fin du dégradé de MARQUE (liserés de bandeau, marqueurs de titre,
     séparateurs). Un token à part, parce que `--primary-2` s'INVERSE entre
     les thèmes : #262f39 en clair mais #e9ebef en sombre. Les onze dégradés
     de marque viraient donc à l'orange doré -> blanc en thème sombre, une
     gamme qui n'est pas la nôtre. Ici la fin reste toujours du côté sombre
     de la charte, déclinée par thème. */
  --brand-grad-end: #2b3644;
  /* L'argent métallique du « B » de la carte de visite : la déclinaison
     sombre naturelle de la charte, et surtout pas du blanc. */
  --brand-grad-end: #8d97a6;
  --hex-size: 76px;
  /* Bordure légèrement approfondie en conséquence : sur une surface plus
     claire, l'ancienne #d6dae0 devenait trop discrète pour dessiner le
     contour d'une carte ou d'un champ. */
  --border: #cfd6df;
  --text: #262f39;
  --text-muted: #545b63;
  --danger: #dc2626;
  --danger-bg: #f4baba;
  --success: #1e7a3d;
  --success-bg: #b7d4c1;
  --warning: #8a6410;
  --warning-bg: #dacdb3;
  --info: #0891b2;
  --info-bg: #b0dce6;
  /* Même principe que --primary-btn ci-dessus : --info est trop clair pour
     du texte blanc sur fond plein (~3.2:1) — turquoise assombrie (~4.6:1)
     dédiée au fond de bouton/pastille pleine (bascule Atelier/Comptoir). */
  --info-btn: #0e7490;
  /* Type de vente (Atelier/Comptoir, mode garage) — paire dédiée, distincte
     de --primary/--info déjà utilisées pour le type de client (pro/
     particulier) dans la même vue Factures : les deux codes couleur ne
     doivent jamais se chevaucher, sinon la couleur d'une ligne devient
     ambiguë (client pro ou vente comptoir ?). */
  --atelier: #6d28d9;
  --atelier-bg: #d0baf3;
  --atelier-btn: #5b21b6;
  --comptoir: #0d9488;
  --comptoir-bg: #b2ddd9;
  --comptoir-btn: #0f766e;
  --draft: #545b63;
  --draft-bg: #c8cbcd;
  /* "Modifier" (icon-btn.edit) — dédié, distinct de --primary (trop proche
     visuellement de --warning en fond clair, la paire d'origine) et de
     --atelier (déjà réservé aux badges type de vente du mode garage). */
  --edit: #4f46e5;
  --edit-bg: #c7c4f7;
  /* Dédiée au menu latéral "Magasins" (mode multi-succursale) — bleu
     franc, seule teinte du menu encore libre une fois les 9 autres items
     répartis sur les couleurs d'accent existantes (voir base.html). */
  --store: #2563eb;
  --store-bg: #bcd0f7;
  /* Ombre un peu plus présente (2026-08-28), en complément de l'écart
     fond/surface élargi ci-dessus : c'est la combinaison des deux qui fait
     qu'une carte se lit comme posée sur la page plutôt que fondue dedans.
     Reste volontairement douce — une ombre franche jurerait avec des gris
     sourds. */
  --shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 2px 6px rgba(16, 24, 40, 0.07);
  --radius: 12px;
  --tint-orange-bg: #fed4bc;
  --tint-orange-border: #fdc79b;
  --tint-green-bg: #b7d4c1;
  --tint-green-border: #b9e3c2;
  --tint-amber-bg: #dacdb3;
  --tint-amber-border: #f2dd9e;
}
/* Retour utilisateur (2026-08-17) : tous les fonds "doux" (-bg/-soft) du
   thème clair étaient bien trop proches, en luminosité perçue, de
   --bg (#e6e9ed)/--surface (#eef1f4) — un calcul de contraste WCAG
   (luminance relative) donnait ~1.0-1.1 partout, quasiment
   imperceptible (ex: le fond rouge du bouton "Supprimer" ne se voyait
   pas du tout), contre ~4-5 en thème sombre où le même principe
   fonctionnait déjà bien. Toutes les teintes ci-dessus recalculées à un
   mélange fixe de 32% couleur d'accent / 68% blanc — vise ~1.3-1.5 de
   contraste contre --surface (nettement perceptible) tout en gardant
   l'icône/le texte de la couleur d'accent lisible par-dessus (contraste
   icône-sur-fond ≥ ~2 partout, souvent ≥ 3-4). Le thème sombre n'est pas
   concerné, jamais signalé comme problème. */

[data-theme="dark"] {
  --primary: #ff9152;
  --primary-2: #e9ebef;
  --primary-soft: #3a2a1c;
  /* Contrairement à l'aperçu de validation (qui égalise --ember et
     --ember-strong en sombre), on garde ici une teinte distincte assombrie
     pour le fond des boutons pleins : le texte des boutons reste blanc fixe
     (voir .btn.primary), et #ff9152 est trop clair pour ce texte blanc
     (même souci de contraste qu'en thème clair, non résolu dans l'aperçu).
     #cc4a00 était déjà la valeur en place et passe l'AA. */
  --primary-btn: #cc4a00;
  /* Même correctif de PROFONDEUR qu'en thème clair (2026-08-28) : le fond
     et les cartes n'étaient séparés que d'un rapport de 1,10, donc rien ne
     se détachait. Le fond descend, la surface ne bouge pas — en sombre, une
     ombre portée ne crée presque aucune élévation, c'est donc l'écart de
     valeur qui doit faire tout le travail. Rapport porté à 1,15. */
  --bg: #0f1118;
  --surface: #1c1f2b;
  /* Le dégradé sombre est celui de l'écran de connexion d'origine — c'est
     lui que l'utilisateur voulait garder. Il vit désormais dans le thème
     SOMBRE, sa place logique. */
  --hero-grad: linear-gradient(160deg, #1b212a 0%, #262f39 55%, #0c0f14 100%);
  --hero-glow: color-mix(in srgb, var(--primary) 30%, transparent);
  --hero-watermark-blend: soft-light;
  /* Plus lisible sur fond sombre, comme sur la carte de visite elle-même où
     la trame se voit nettement dans l'anthracite. */
  --hex-size: 76px;
  --border: #2c3040;
  --text: #e9ebef;
  --text-muted: #aab0bf;
  --danger: #f87171;
  --danger-bg: #3a1c22;
  --success: #4ade80;
  --success-bg: #123423;
  --warning: #fbbf24;
  --warning-bg: #3a2c0c;
  --info: #22d3ee;
  --info-bg: #123c47;
  --info-btn: #0e7490;
  --atelier: #a78bfa;
  --atelier-bg: #2c1f4a;
  --atelier-btn: #5b21b6;
  --comptoir: #2dd4bf;
  --comptoir-bg: #0f3a35;
  --comptoir-btn: #0f766e;
  --draft: #aab0bf;
  --draft-bg: #2c3040;
  --edit: #818cf8;
  --edit-bg: #292a4a;
  --store: #60a5fa;
  --store-bg: #1c355c;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.4);
  --radius: 12px;
  --tint-orange-bg: #3a2a1c;
  --tint-orange-border: #6b4527;
  --tint-green-bg: #123423;
  --tint-green-border: #1f5c3c;
  --tint-amber-bg: #3a2c0c;
  --tint-amber-border: #6b4f14;
}

* { box-sizing: border-box; }

body {
  font-family: var(--font-sans);
  background: var(--bg);
  color: var(--text);
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  transition: background 0.15s ease, color 0.15s ease;
}

/* Couleur de lien unique (remplace le bleu/mauve par défaut du navigateur,
   peu lisible en thème sombre), cohérente avec l'accent orange utilisé
   partout ailleurs dans l'app. Une règle auteur (même de faible spécificité)
   l'emporte déjà sur le style ":visited" mauve par défaut du navigateur
   (origine "auteur" prioritaire sur "user-agent"), donc pas besoin d'une
   règle ":visited" séparée ici — en ajouter une ferait remonter la
   spécificité au point de repasser devant .btn/.icon-btn (couleur de texte
   explicite sur fond coloré) une fois le lien visité, rendant leur texte
   illisible. */
a { color: var(--primary); }
a:hover { filter: brightness(1.1); }

header.topbar {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 14px 24px;
  display: flex;
  align-items: center;
  gap: 4px;
  position: sticky;
  top: 0;
  z-index: 10;
  box-shadow: inset 0 -3px 0 0 transparent;
  border-image: linear-gradient(90deg, var(--primary), var(--brand-grad-end)) 1;
}

/* Bandeau "mode admin" — pleine largeur de la colonne de contenu (le menu
   latéral reste visible/utilisable à côté, voir .app-shell), collé en
   haut de cette colonne au scroll (top:0 — n'apparaît qu'en contexte
   tenant, jamais sous l'ancienne topbar de repli, voir show_sidebar dans
   base.html) pour qu'il soit impossible à manquer pendant qu'on travaille
   sur le compte d'un client. */
/* .sticky-header-group (voir base.html) porte le position:sticky pour
   elle ET .view-toolbar ensemble — ne pas en remettre un ici, deux sticky
   sœurs au même top:0 se superposent au lieu de s'empiler (voir le
   commentaire dans base.html). */
.sticky-header-group { position: sticky; top: 0; z-index: 11; }
.admin-banner {
  display: flex; justify-content: space-between; align-items: center; gap: 16px;
  background: var(--danger); color: white;
  padding: 12px 24px; font-size: 15px; font-weight: 600;
}
.admin-banner svg { vertical-align: -3px; margin-right: 2px; }
/* [B8] L'admin sur SON PROPRE compte : le rouge d'alerte de
   .admin-banner signale « attention, vous agissez au nom de quelqu'un
   d'autre » — hors sujet quand on est chez soi. Bandeau vert, qui ne
   reste là que pour porter le retour vers le panneau admin. */
.admin-banner.own-account { background: var(--success); }

/* Même structure que .admin-banner (sticky-header-group, voir base.html)
   mais une couleur distincte : un rappel de rendez-vous n'a pas la même
   gravité qu'un avertissement "vous êtes en train d'agir pour un client"
   — jamais les confondre visuellement. */
.appointment-reminder-banner {
  display: flex; justify-content: space-between; align-items: center; gap: 16px;
  background: var(--info); color: white;
  padding: 12px 24px; font-size: 15px; font-weight: 600;
}
.appointment-reminder-banner svg { vertical-align: -3px; margin-right: 2px; }

/* En dessous d'un téléphone standard, la pastille "Mon compte" (nom
   d'entreprise inclus, longueur variable) prend trop de place à côté de
   la marque — nom masqué (chevron seul) pour que la topbar reste sur
   une ligne sans forcer tout le corps de page à défiler horizontalement. */
@media (max-width: 640px) {
  .account-name { display: none; }
  .account-trigger { padding: 7px 10px; }
}

/* Bouton "retour en haut" — flottant, discret jusqu'à ce qu'on ait
   scrollé (voir le script en bas de base.html), pour ne pas gêner sur
   une page déjà courte. */
#back-to-top {
  position: fixed; right: 24px; bottom: 24px; z-index: 15;
  width: 44px; height: 44px; border-radius: 50%; border: none;
  background: var(--primary-btn); color: white; font-weight: 700;
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--shadow); cursor: pointer;
  opacity: 0; pointer-events: none; transform: translateY(8px);
  transition: opacity 0.18s ease, transform 0.18s ease;
}
#back-to-top.visible { opacity: 1; pointer-events: auto; transform: translateY(0); }
#back-to-top:hover { filter: brightness(1.05); }
.nav-icon { display: inline-flex; }
/* Icônes SVG en ligne (voir _icons.html) : un léger décalage vertical les
   aligne sur la ligne de base du texte qui les accompagne — sans ça elles
   flottent un peu haut, l'alignement par défaut des <svg> inline étant
   pensé pour du texte à leur propre baseline, pas la ligne du paragraphe. */
.ico { display: inline-block; vertical-align: -3px; flex-shrink: 0; }
/* Petit repère vertical dégradé, étiré sur toute la hauteur d'une ligne
   flex (align-self: stretch, hérite de align-items: stretch du parent) —
   pour séparer des colonnes de contenu (ex: les 3 exports ciblés de
   Paramètres → Comptabilité). */
.col-sep {
  width: 2px;
  align-self: stretch;
  border-radius: 1px;
  background: linear-gradient(180deg, var(--primary), var(--brand-grad-end));
  opacity: 0.35;
  flex-shrink: 0;
}
/* Simple étiquette de marque, plus un lien (l'accès au tableau de bord se
   fait désormais via sa propre icône dans le menu) — même dégradé texte
   qu'avant. */
header.topbar .brand {
  display: flex;
  align-items: center;
  gap: 9px;
}
/* Logo B-Facture (2026-08-27) : mot-symbole complet à la place de l'ancien
   icône B-In-Tech + texte "B-Facture" séparés — voir le même changement
   sur .sidebar-brand (base.html) pour le contexte. */
header.topbar .brand .topbar-brand-logo {
  height: 34px;
  width: auto;
  object-fit: contain;
}

.account-menu { position: relative; }
.account-trigger {
  display: flex; align-items: center; gap: 7px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 999px;
  padding: 7px 14px; font-size: 14px; font-weight: 600; color: var(--text); cursor: pointer;
}
.account-trigger:hover { filter: brightness(0.97); box-shadow: none; transform: none; }
/* Même dégradé texte que la marque "B-Facture" — uniquement sur le nom
   (pas tout le bouton, qui garde son fond/sa bordure neutres) ; poids
   700 (au lieu de 600 hérité de .account-trigger) pour rester net une
   fois rempli par le dégradé plutôt qu'une simple couleur plate. */
.account-name {
  background: linear-gradient(90deg, var(--primary), var(--brand-grad-end));
  -webkit-background-clip: text; background-clip: text; color: transparent;
  font-weight: 700;
}
.account-chevron { font-size: 10px; color: var(--text-muted); transition: transform 0.15s ease; }
.account-menu.open .account-chevron { transform: rotate(180deg); }
.account-dropdown {
  display: none; position: absolute; top: calc(100% + 8px); right: 0; min-width: 210px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: var(--shadow); padding: 6px; z-index: 20;
}
.account-menu.open .account-dropdown { display: block; }
.account-dropdown form { margin: 0; }
/* `justify-content:flex-start` explicite : la règle générale des boutons pose
   `justify-content:center` (voir `button, .btn`), qui s'applique donc aussi aux
   entrées de menu qui sont des <button> — « Déconnexion », « Supprimer ». Elles
   se retrouvaient centrées au milieu d'entrées <a> alignées à gauche, dans les
   deux menus déroulants de l'app. `text-align:left` ne suffit pas à le corriger :
   il ne pilote pas l'axe principal d'un conteneur flex. */
.account-menu .account-dropdown-item {
  display: flex; align-items: center; justify-content: flex-start; gap: 10px;
  width: 100%; text-align: left;
  background: none; border: none; border-radius: 8px; padding: 9px 10px; font-size: 14px;
  font-weight: 500; color: var(--text); cursor: pointer; text-decoration: none;
}
.account-menu .account-dropdown-item:hover { background: var(--bg); box-shadow: none; transform: none; filter: none; }
.account-menu .account-dropdown-item.danger { color: var(--danger); }

/* Menu déroulant "Actions" en tête de page (Factures/Stock/Achats) — même
   mécanique JS que le menu "Mon compte" du topbar (.account-menu réutilisé
   tel quel), juste habillé comme un .btn.secondary normal pour s'intégrer
   à la barre d'actions de la page plutôt que la pastille arrondie du
   topbar. Chaque item colore son icône + son texte (style inline par
   item) pour rester repérable au premier coup d'œil. */
.actions-menu .account-trigger {
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  padding: 9px 16px; font-weight: 600;
}
.actions-menu .account-dropdown { min-width: 240px; }
.actions-menu .account-dropdown-item { font-weight: 600; }

/* Bandeau de couleur à gauche du menu, une teinte différente par page
   (même esprit que .config-section.accent-* dans Paramètres) — repère
   visuel rapide pour savoir quel menu "Actions" est ouvert. */
.actions-menu .account-dropdown { border-left: 4px solid var(--border); }
.actions-menu.accent-primary .account-dropdown { border-left-color: var(--primary); }
.actions-menu.accent-secondary .account-dropdown { border-left-color: var(--primary-2); }
.actions-menu.accent-info .account-dropdown { border-left-color: var(--info); }
.actions-menu.accent-success .account-dropdown { border-left-color: var(--success); }
.actions-menu.accent-warning .account-dropdown { border-left-color: var(--warning); }
.actions-menu.accent-danger .account-dropdown { border-left-color: var(--danger); }

/* ===========================================================================
   Menu latéral (Phase 6, chantier UI/véhicules/TVA, 2026-08-22)
   Remplace la topbar horizontale UNIQUEMENT sur les pages qui affichaient
   déjà une nav (voir show_sidebar dans base.html) — l'ancienne
   header.topbar (règles ci-dessus, inchangées) reste le repli pour un
   admin non-impersonating, un rôle "accountant" ou une page hors
   connexion, où ni nav ni recherche n'apparaissaient déjà avant ce
   chantier.
   =========================================================================== */
.app-shell { display: flex; align-items: flex-start; flex: 1 0 auto; width: 100%; min-height: 100vh; }
.app-content {
  flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 100vh;
  /* Dégradé + halo orange + filigrane du logo, comme sur l'écran de
     connexion mais en beaucoup plus sourd — ici on travaille, on ne
     contemple pas.

     COUCHES DE FOND et non un pseudo-élément : un ::before positionné
     obligerait à donner un z-index à `main` pour passer au-dessus, ce qui
     créerait un contexte d'empilement... et ferait alors passer les MODALES
     (z-index 100, à l'intérieur de main) SOUS le menu latéral (z-index 20,
     contexte frère). Un fond, lui, peint toujours sous le contenu de son
     élément sans créer de contexte : aucun risque de ce genre.

     La lisibilité est préservée par construction : tout le texte dense
     (tableaux, formulaires) vit dans des `.card` opaques, et les seuls
     éléments posés directement sur ce fond — les <h1> de page — gardent un
     contraste de 8,4:1 au minimum. */
  /* Pas de trame hexagonale sur le fond des vues : essayée le 2026-08-28,
     retirée aussitôt — « trop chargé » sur des pages déjà denses en tableaux.
     Elle revient en RAPPEL LOCALISÉ en pied de menu, là où il n'y a rien à
     lire (voir .sidebar-bottom). Un motif répété sur toute la surface de
     travail concurrence les chiffres ; le même motif sur 90px de haut, dans
     une zone vide, signe sans gêner. */
  background-image:
    url("images/bfacture-icon.png"),
    radial-gradient(circle at 8% 0%, var(--hero-glow), transparent 38%),
    var(--hero-grad);
  background-repeat: no-repeat;
  background-position: right -70px bottom -50px, center, center;
  background-size: 380px, cover, cover;
  background-attachment: fixed;
  background-blend-mode: var(--hero-watermark-blend), normal, normal;
}
/* La topbar de repli (admin/accountant/déconnecté) n'a plus besoin de son
   propre sticky "flottant pleine largeur" — elle ne partage plus jamais
   l'écran avec le menu latéral (mutuellement exclusifs, voir
   show_sidebar), mais reste correcte telle quelle imbriquée ici. */

.sidebar {
  width: 232px; flex-shrink: 0;
  background: var(--surface);
  /* Bord droit en dégradé plutôt qu'un trait gris uniforme : c'est LA
     séparation entre le menu et la zone de travail, elle mérite le liseré de
     marque comme le bandeau d'actions.

     Le dégradé part de l'orange EN HAUT, exactement là où le bandeau
     horizontal part de l'orange à GAUCHE (voir .view-toolbar) : les deux
     liserés se rejoignent donc dans la même teinte à l'angle, au lieu de se
     croiser sur deux couleurs différentes. Il descend ensuite vers
     l'anthracite/argent, ce qui allège visuellement le bas du menu.

     `border-image` s'applique à toutes les bordures, mais seules celles qui
     ont une épaisseur sont peintes — d'où un seul `border-right` déclaré.
     Même mécanique que .view-toolbar. */
  border-right: 2px solid var(--border);
  border-image: linear-gradient(180deg, var(--primary) 0%, var(--brand-grad-end) 55%, transparent 100%) 1;
  display: flex; flex-direction: column;
  /* PAS de overflow-y:auto ici : fixer un axe d'overflow force l'autre à
     "auto" aussi (règle CSS peu connue) — .sidebar-nav ci-dessous en
     avait besoin, et ça rognait silencieusement les bulles .tip qui
     débordent volontairement à DROITE en menu réduit (trouvé en
     vérifiant réellement la bulle au survol, pas en le supposant). Un
     menu qui dépasserait 100vh sur un écran très court reste un risque
     mineur accepté plutôt que ce compromis. */
  position: sticky; top: 0; height: 100vh;
  transition: width 0.16s ease;
  /* position:sticky crée son propre contexte d'empilement : sans z-index
     explicite ici, les bulles .tip (z-index:30) qui débordent à DROITE du
     menu réduit restaient invisibles, peintes SOUS .app-content (élément
     suivant dans le DOM, sans z-index non plus — départage alors par
     ordre du DOM, pas par le z-index des enfants de chaque contexte).
     Trouvé en vérifiant réellement la bulle au survol, pas en le
     supposant. */
  z-index: 20;
}
html[data-sidebar="collapsed"] .sidebar { width: 68px; }

.sidebar-top {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 16px 14px; border-bottom: 1px solid var(--border); flex-shrink: 0;
}
.sidebar-brand {
  display: flex; align-items: center; gap: 9px; min-width: 0;
  text-decoration: none;
}
/* Logo B-Facture (2026-08-27) : deux variantes du même fichier, permutées
   selon l'état du menu — voir base.html pour le pourquoi des deux <img>
   plutôt qu'une seule redimensionnée en CSS (le lockup complet perdrait
   le mot-symbole en dessous de sa largeur mini lisible, l'emblème seul se
   voit mieux carré dans les 68px du menu réduit qu'écrasé/rogné). */
.brand-logo-full { height: 36px; width: auto; max-width: 150px; object-fit: contain; flex-shrink: 0; }
.brand-logo-collapsed { display: none; width: 34px; height: 34px; object-fit: contain; flex-shrink: 0; }
html[data-sidebar="collapsed"] .brand-logo-full { display: none; }
html[data-sidebar="collapsed"] .brand-logo-collapsed { display: block; }
/* Réduit : logo et bouton empilés (au lieu de côte à côte) — les deux ne
   tiennent pas sur une seule ligne de 68px de large. Le bouton reste
   TOUJOURS visible ici, dans les deux états : le masquer en réduit
   forçait à cliquer le logo pour ré-étendre le menu, un contrôle que
   rien ne signalait comme cliquable (signalé par l'utilisateur — il a
   dû chercher comment ré-ouvrir le menu). */
html[data-sidebar="collapsed"] .sidebar-top {
  flex-direction: column; justify-content: center; padding: 14px 8px; gap: 8px;
}

.sidebar-toggle-btn {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 30px; height: 30px; border-radius: 8px; border: 1px solid var(--border);
  background: var(--bg); color: var(--text-muted); cursor: pointer;
}
.sidebar-toggle-btn:hover { color: var(--text); box-shadow: none; transform: none; filter: none; }
.sidebar-toggle-btn .ico { transform: rotate(180deg); transition: transform 0.16s ease; }
/* Malgré son nom, le path "angles-left" de _icons.html pointe en réalité
   vers la DROITE une fois rendu (vérifié visuellement, pas supposé du
   nom) — donc rotate(180deg) par défaut pour pointer vers la gauche
   (= réduire, l'action du bouton menu étendu), et remis à 0 une fois
   réduit pour repointer vers la droite (= étendre). Pas d'icône
   "angles-right" dédiée dans _icons.html, la retourner en CSS évite
   d'en ajouter une. */
html[data-sidebar="collapsed"] .sidebar-toggle-btn .ico { transform: rotate(0deg); }
/* Bulle d'aide à DROITE plutôt qu'en dessous (comportement par défaut de
   .tt, pensé pour une barre horizontale) : centrée sous un bouton de
   68px de large, elle déborderait hors de l'écran à gauche. Même
   traitement que .sidebar-link.tt ci-dessous. */
html[data-sidebar="collapsed"] .sidebar-toggle-btn.tt .tip {
  top: 50%; left: calc(100% + 10px); bottom: auto;
  transform: translate(-4px, -50%);
}
html[data-sidebar="collapsed"] .sidebar-toggle-btn.tt:hover .tip,
html[data-sidebar="collapsed"] .sidebar-toggle-btn.tt:focus-visible .tip {
  transform: translate(0, -50%);
}

.sidebar-search { position: relative; display: flex; align-items: center; padding: 12px 14px; flex-shrink: 0; }
.sidebar-search-icon { position: absolute; left: 24px; color: var(--text-muted); pointer-events: none; display: flex; }
.sidebar-search input[type=search] {
  width: 100%; padding: 8px 12px 8px 32px; border-radius: 20px; font-size: 13px;
}
html[data-sidebar="collapsed"] .sidebar-search { padding: 12px 8px; }
html[data-sidebar="collapsed"] .sidebar-search-icon { left: 18px; }
html[data-sidebar="collapsed"] .sidebar-search input[type=search] { padding-left: 30px; }

.sidebar-nav { display: flex; flex-direction: column; gap: 2px; padding: 6px 10px; flex: 1; }
/* Rappel de la trame hexagonale de la charte, en PIED DE MENU seulement.
   Essayée d'abord en fond de toutes les vues (2026-08-28), retirée aussitôt :
   « trop chargé » sur des pages déjà denses. Ici elle occupe une zone
   naturellement vide, sous le dernier lien — elle signe sans jamais
   concurrencer une donnée à lire.

   Pseudo-élément traité comme un ITEM FLEX (`flex: 1`) et non en position
   absolue : il prend l'espace restant de la colonne, donc il ne peut par
   construction chevaucher ni un lien du menu, ni le bloc compte en dessous.
   Un `mask` le fait naître du bas et disparaître vers le haut. */
.sidebar-nav::after {
  content: "";
  flex: 1 1 auto;
  min-height: 40px;
  margin: 10px -10px -6px;
  background: url("images/hex-pattern.svg") repeat bottom center / 46px;
  -webkit-mask-image: linear-gradient(to top, #000 15%, transparent 92%);
  mask-image: linear-gradient(to top, #000 15%, transparent 92%);
  mix-blend-mode: var(--hero-watermark-blend);
  opacity: 0.75;
  pointer-events: none;
}
/* Menu réduit : la trame reste, resserrée pour tenir dans 68px. */
html[data-sidebar="collapsed"] .sidebar-nav::after { background-size: 34px; }
.sidebar-link {
  display: flex; align-items: center; gap: 12px; min-width: 0;
  padding: 9px 12px; border-radius: 10px;
  color: var(--nav-color, var(--text-muted)); text-decoration: none; font-size: 14px; font-weight: 600;
  transition: color 0.12s ease, background-color 0.12s ease;
}
.sidebar-link:hover, .sidebar-link:focus-visible, .sidebar-link.current {
  background-color: color-mix(in srgb, var(--nav-color, var(--primary)) 14%, transparent);
}
/* Boîte d'icône de LARGEUR FIXE : les glyphes Font Awesome n'ont pas tous
   le même ratio (cart-shopping est en 576x512, file-invoice en 384x512), et
   la largeur rendue est dérivée de ce ratio — les libellés du menu ne
   démarraient donc pas au même x d'une entrée à l'autre. On centre le glyphe
   dans une boîte fixe plutôt que de forcer sa largeur, ce qui l'étirerait.
   24px couvre le plus large des glyphes du menu ("users" et "link", 22,5px). */
/* Boîte d'icône de LARGEUR FIXE : les glyphes Font Awesome n'ont pas tous le
   même ratio (cart-shopping est en 576x512, file-invoice en 384x512), et la
   largeur rendue est dérivée de ce ratio — les libellés du menu ne
   démarraient donc pas au même x d'une entrée à l'autre. On centre le glyphe
   dans une boîte fixe plutôt que de forcer sa largeur, ce qui l'étirerait.
   24px couvre le plus large des glyphes du menu ("users" et "link", 22,5px).

   Pas de pastille de fond ici : elle avait été ajoutée pour rattraper les
   contours noirs des icônes OpenMoji sur fond sombre (essai du 2026-08-26,
   abandonné). Les glyphes Font Awesome héritent de --nav-color et n'en ont
   aucun besoin. */
.sidebar-link .nav-icon {
  flex-shrink: 0; display: inline-flex;
  width: 24px; justify-content: center; align-items: center;
}

/* Icônes OpenMoji : conservées sur les GRANDES surfaces uniquement (titres de
   la page d'aide, checklist de démarrage), où leur taille les rend lisibles.
   Elles portent leurs propres couleurs et n'héritent donc pas de currentColor. */
.emo { display: inline-block; vertical-align: -4px; flex-shrink: 0; }
.sidebar-link-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Le libellé permanent double le rôle de l'info-bulle .tip (héritée de
   .tt) : pas besoin des deux à la fois quand le menu est étendu. */
.sidebar-link .tip { display: none; }
html[data-sidebar="collapsed"] .sidebar-link { justify-content: center; padding: 9px; }
html[data-sidebar="collapsed"] .sidebar-link-label { display: none; }
html[data-sidebar="collapsed"] .sidebar-link .tip { display: block; }
/* Bulle à DROITE de l'icône (pas en dessous, contrairement à .tt par
   défaut, pensé pour une barre horizontale) — une colonne d'icônes
   collées les unes aux autres n'a pas la place d'afficher une bulle en
   dessous sans chevaucher l'item suivant. */
html[data-sidebar="collapsed"] .sidebar-link.tt .tip {
  top: 50%; left: calc(100% + 10px); bottom: auto;
  transform: translate(-4px, -50%);
}
html[data-sidebar="collapsed"] .sidebar-link.tt:hover .tip,
html[data-sidebar="collapsed"] .sidebar-link.tt:focus-visible .tip {
  transform: translate(0, -50%);
}

.sidebar-bottom { padding: 10px; border-top: 1px solid var(--border); flex-shrink: 0; }
/* Le menu "Mon compte" du bas de sidebar réutilise .account-menu/
   .account-dropdown tels quels (voir plus haut) — seule la direction
   d'ouverture change (vers le HAUT : en bas d'écran, un dépliant classique
   vers le bas sortirait de la fenêtre). Bouton pleine largeur plutôt que
   la pastille arrondie de la topbar, plus cohérent avec le reste du menu
   latéral. */
.sidebar-account-menu { width: 100%; }
.sidebar-account-menu .account-trigger {
  width: 100%; border-radius: 10px; justify-content: space-between;
}
.sidebar-account-menu .account-dropdown {
  top: auto; bottom: calc(100% + 8px); left: 0; right: 0; min-width: 0; width: 100%;
}
/* Chevron inversé par rapport à .account-menu.open .account-chevron
   (règle générique, pensée pour la topbar dont le menu s'ouvre vers le
   BAS) : ce menu-ci s'ouvre vers le HAUT (voir .account-dropdown
   ci-dessus), donc fermé il pointe vers le HAUT (annonce que le
   contenu va apparaître au-dessus) et une fois ouvert il repointe vers
   le BAS. Sélecteurs à spécificité égale (0,3,0) que la règle
   générique .account-menu.open .account-chevron — placés après elle
   dans le fichier pour gagner le départage par ordre de source. */
.sidebar-account-menu .account-chevron { transform: rotate(180deg); }
.sidebar-account-menu.open .account-chevron { transform: rotate(0deg); }
/* Réduit : le nom (texte) et le chevron disparaissent, ne laissant plus
   rien dans le bouton — un avatar-lettre (initiale du nom de compte) le
   remplace, seul indice visuel qu'il s'agit bien du menu compte une fois
   le texte masqué. Bulle .tip (nom complet) au survol, comme les items de
   nav réduits. */
/* Avatar-initiale en HEXAGONE : la forme des badges d'icône de la carte de
   visite B-In-Tech, et celle de l'emblème lui-même. Pointe en haut, comme le
   logo.

   Dégradé parti de `--primary-btn` et non de `--primary` : l'orange vif ne
   donne que 2,66:1 avec du blanc (le fichier le documente déjà pour les
   boutons), l'initiale y était donc à peine lisible. `--primary-btn` monte à
   4,11:1, et l'autre extrémité reste l'anthracite de la charte dans les deux
   thèmes — pas `--brand-grad-end`, dont la déclinaison argent ferait
   redescendre le blanc à 2,95:1. */
.account-avatar {
  width: 28px; height: 30px; flex-shrink: 0;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  align-items: center; justify-content: center; font-size: 12px; font-weight: 800;
  background: linear-gradient(135deg, var(--primary-btn), #2b3644); color: #fff;
  /* Toujours visible, y compris menu étendu (2026-08-28) : il ne servait
     qu'en menu réduit, où il remplaçait le nom. Le garder à côté du nom donne
     au bloc compte la même structure « badge + libellé » que le reste du
     menu — et que la carte de visite. */
  display: flex;
}
.sidebar-account-menu .account-trigger { gap: 10px; }
.sidebar-account-menu .account-name {
  font-weight: 700; letter-spacing: 0.2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: left; flex: 1; min-width: 0;
}
.sidebar-account-menu .tip { display: none; }
html[data-sidebar="collapsed"] .sidebar-account-menu .account-name,
html[data-sidebar="collapsed"] .sidebar-account-menu .account-chevron { display: none; }
/* (l'avatar est désormais visible dans les deux états — voir .account-avatar) */
html[data-sidebar="collapsed"] .sidebar-account-menu .account-trigger { justify-content: center; padding: 7px; }
html[data-sidebar="collapsed"] .sidebar-account-menu .account-dropdown { left: 0; width: 260px; }
html[data-sidebar="collapsed"] .sidebar-account-menu.tt .tip {
  display: block; top: 50%; left: calc(100% + 10px); bottom: auto; transform: translate(-4px, -50%);
}
html[data-sidebar="collapsed"] .sidebar-account-menu.tt:hover .tip,
html[data-sidebar="collapsed"] .sidebar-account-menu.tt:focus-visible .tip { transform: translate(0, -50%); }

@media (max-width: 900px) {
  /* Sous une largeur tablette, le menu reste toujours réduit (icônes
     seules) quelle que soit la préférence mémorisée — sur un écran
     étroit, 232px de menu prendraient une part disproportionnée de la
     largeur disponible pour le contenu (tableaux notamment). */
  .sidebar { width: 68px; }
  .brand-logo-full, .sidebar-toggle-btn { display: none; }
  .brand-logo-collapsed { display: block; }
  .sidebar-top { justify-content: center; padding: 16px 8px; }
  .sidebar-brand { cursor: default; }
  .sidebar-search { padding: 12px 8px; }
  .sidebar-search-icon { left: 18px; }
  .sidebar-search input[type=search] { padding-left: 30px; }
  .sidebar-link { justify-content: center; padding: 9px; }
  .sidebar-link-label { display: none; }
  .sidebar-link .tip { display: block; }
  .sidebar-link.tt .tip { top: 50%; left: calc(100% + 10px); bottom: auto; transform: translate(-4px, -50%); }
  .sidebar-link.tt:hover .tip, .sidebar-link.tt:focus-visible .tip { transform: translate(0, -50%); }
  .sidebar-account-menu .account-name, .sidebar-account-menu .account-chevron { display: none; }
  .sidebar-account-menu .account-avatar { display: flex; }
  .sidebar-account-menu .account-trigger { justify-content: center; padding: 7px; }
  .sidebar-account-menu .account-dropdown { left: 0; width: 260px; }
  .sidebar-account-menu.tt .tip {
    display: block; top: 50%; left: calc(100% + 10px); bottom: auto; transform: translate(-4px, -50%);
  }
  .sidebar-account-menu.tt:hover .tip, .sidebar-account-menu.tt:focus-visible .tip { transform: translate(0, -50%); }
}

/* Puce de légende (ex: liste des factures, pro/particulier) — même
   séparateur dégradé que h1::before/h2::before/.summary-text::before,
   repris ici à petite échelle pour remplacer un simple "●". */
.legend-separator {
  display: inline-block; width: 4px; height: 14px; border-radius: 2px;
  background: linear-gradient(180deg, var(--primary), var(--brand-grad-end));
  vertical-align: -2px; margin-right: 4px;
}

.theme-toggle {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 999px;
  width: 34px;
  height: 34px;
  cursor: pointer;
  font-size: 15px;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Bascule Atelier/Comptoir (mode garage) : glissière à deux positions,
   bascule instantanée sans rechargement (voir setSaleType() côté JS). */
.sale-type-switch {
  position: relative;
  display: inline-flex;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px;
  width: fit-content;
}
.sale-type-switch::before {
  content: "";
  position: absolute;
  top: 3px; bottom: 3px; left: 3px;
  width: calc(50% - 3px);
  background: var(--atelier-btn);
  border-radius: 999px;
  transition: transform 0.2s ease, background-color 0.2s ease;
}
/* Même teinte que les pastilles de statut de la vue Factures
   (.status-dot.atelier = violet, .status-dot.comptoir = turquoise) —
   la bascule reprend visuellement le code couleur déjà utilisé ailleurs
   pour distinguer les deux types de vente. Paire dédiée, volontairement
   différente de --primary/--info (type de client pro/particulier) pour
   ne jamais se confondre dans la même vue Factures. */
.sale-type-switch.comptoir::before { transform: translateX(100%); background: var(--comptoir-btn); }
.sale-type-switch .sale-type-option {
  position: relative;
  z-index: 1;
  background: none;
  border: none;
  padding: 7px 18px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 0.2s ease;
  box-shadow: none;
}
.sale-type-switch .sale-type-option:hover { transform: none; box-shadow: none; filter: none; }
.sale-type-switch .sale-type-option.active { color: white; }

main {
  max-width: 980px;
  margin: 28px auto;
  padding: 0 20px 60px;
  /* flex:1 sur ce bloc (le body est en colonne flex, voir plus haut) est ce
     qui colle réellement le footer en bas de la fenêtre sur les pages
     courtes : sans ça, l'espace vide restant se retrouvait après le footer
     au lieu d'être absorbé ici, et le footer flottait au milieu de l'écran. */
  flex: 1 0 auto;
  width: 100%;
}
/* Les pages à base de tableau (factures, stock, clients, achats, admin...)
   profitent d'un écran de PC plutôt que de rester bridées à 980px — fluide
   (min(1500px, 95vw)) pour continuer à rétrécir proprement sur un écran
   plus petit, tablette ou smartphone (voir overflow-x:auto sur chaque
   tableau, filet de sécurité si jamais ça ne suffit pas). Plutôt que
   d'élargir TOUTES les pages (formulaires courts, connexion...) où ça
   n'apporterait rien, seules les pages qui en profitent vraiment passent
   cette classe via {% block main_class %} — voir la liste des templates
   concernés (listes + panneau admin). */
main.main-wide { max-width: min(1500px, 95vw); }
h1 { font-size: 23px; letter-spacing: -0.02em; }
/* Même dégradé que h2::before ci-dessous, décliné plus grand pour les
   titres de vue (Clients, Factures...). Volontairement laissé en
   inline-block plutôt qu'un h1 en display:flex (comme h2) : certaines
   pages centrent leur h1 via un style inline text-align:center (connexion,
   mot de passe oublié) — passer h1 en flex ignorerait ce text-align et
   casserait ce centrage. */
h1::before { content: ""; width: 5px; height: 21px; border-radius: 2.5px; background: linear-gradient(180deg, var(--primary), var(--brand-grad-end)); display: inline-block; vertical-align: -3px; margin-right: 10px; }
h2 { font-size: 16px; margin-top: 28px; display: flex; align-items: center; gap: 8px; }
h2::before { content: ""; width: 4px; height: 15px; border-radius: 2px; background: linear-gradient(180deg, var(--primary), var(--brand-grad-end)); display: inline-block; }
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 22px;
  margin-bottom: 20px;
  box-shadow: var(--shadow);
}
/* margin-bottom par défaut : sans ça, un bouton (ou tout autre contenu)
   placé juste après un tableau, dans la même carte, colle directement à
   sa dernière ligne — aucune marge ne les sépare autrement. */
table { width: 100%; border-collapse: collapse; margin-bottom: 14px; }
table:last-child { margin-bottom: 0; }
th, td { text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--border); font-size: 14px; }
th { color: var(--text-muted); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
tr:last-child td { border-bottom: none; }
form .field { margin-bottom: 14px; }
label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: var(--text); }
input[type=text], input[type=number], input[type=email], input[type=password],
input[type=date], input[type=datetime-local], input[type=file], input[type=color], input[type=search], input[type=tel], select, textarea {
  width: 100%; padding: 9px 11px; border: 1px solid var(--border); border-radius: 10px;
  font-size: 14px; background: var(--surface); color: var(--text);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-soft);
}
textarea { min-height: 70px; }
/* Icône du sélecteur de date native (Chrome/Edge/Safari) : noire par
   défaut quel que soit le thème, donc invisible sur un champ en thème
   sombre (fond --surface foncé). Inversée en thème sombre pour rester
   lisible ; laissée telle quelle en thème clair où elle est déjà visible. */
input[type=date]::-webkit-calendar-picker-indicator, input[type=datetime-local]::-webkit-calendar-picker-indicator { cursor: pointer; border-radius: 4px; }
[data-theme="dark"] input[type=date]::-webkit-calendar-picker-indicator, [data-theme="dark"] input[type=datetime-local]::-webkit-calendar-picker-indicator { filter: invert(1); }
.row { display: flex; gap: 16px; flex-wrap: wrap; }
.row .field { flex: 1; min-width: 160px; }
/* inline-flex + gap plutôt qu'inline-block : en inline-block, l'icône et le
   libellé ne sont séparés que par l'espace du HTML, qui se réduit à presque
   rien à cette taille de police — le texte paraissait collé au pictogramme sur
   tous les boutons de l'app. Le `gap` donne un écart constant, et
   `align-items:center` recentre verticalement l'icône quel que soit son ratio,
   ce que le `vertical-align:-3px` de .ico ne faisait qu'approximer.
   Même correctif et même raison que .badge plus bas. */
button, .btn {
  /* Coin inférieur droit BISEAUTÉ, repris des boutons du site B-In-Tech
     (2026-08-28). Le biseau est peint par un dégradé à arrêt net plutôt que
     découpé au `clip-path` : `clip-path` rognerait aussi l'anneau de focus
     du clavier, qui deviendrait invisible. Ici la boîte reste rectangulaire,
     seul le fond est biseauté — le focus reste entièrement dessiné.

     Réservé aux boutons ORANGE : les variantes .secondary/.danger/.warning
     redéfinissent `background`, donc perdent naturellement le biseau, et
     retrouvent leurs angles arrondis plus bas. C'est ce qui fait ressortir
     l'action principale au milieu des autres. */
  --chamfer: 11px;
  background: linear-gradient(315deg, transparent var(--chamfer), var(--primary-btn) var(--chamfer));
  color: white; border: none; border-radius: 0;
  padding: 9px 16px; font-size: 14px; cursor: pointer; text-decoration: none;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-weight: 600; transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease;
}
/* Halo coloré au survol (repris du site vitrine B-Web/B-In-Tech, ex: le
   bouton "appeler" de GBMotos) : une ombre teintée et large, bien plus
   flatteuse qu'une ombre neutre — chaque variante utilise sa propre
   couleur (currentColor reprend le texte du bouton, déjà accordé à sa
   couleur de fond pour danger/warning). */
button:hover, .btn:hover { filter: brightness(1.08); transform: translateY(-1px); box-shadow: 0 14px 26px -14px var(--primary-btn); }
button:active, .btn:active { transform: translateY(0); }
button.secondary, .btn.secondary { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
button.secondary:hover, .btn.secondary:hover { box-shadow: var(--shadow); }
button.danger, .btn.danger { background: var(--danger-bg); color: var(--danger); }
button.warning, .btn.warning { background: var(--warning-bg); color: var(--warning); }
button.danger:hover, .btn.danger:hover, button.warning:hover, .btn.warning:hover { box-shadow: 0 14px 26px -14px currentColor; }
button.link { background: none; border: none; color: var(--primary); padding: 0; font-weight: 600; cursor: pointer; }
button.link:hover { transform: none; box-shadow: none; filter: none; text-decoration: underline; }
.btn-sm, button.btn-sm { padding: 6px 12px; font-size: 13px; border-radius: 0; --chamfer: 8px; }

/* Angles arrondis conservés pour tout ce qui n'est PAS le bouton d'action
   orange. Placé après .btn-sm pour gagner le départage par ordre de source
   sur un `<button class="secondary btn-sm">`, où les deux règles ont la même
   spécificité. */
button.secondary, .btn.secondary,
button.danger, .btn.danger,
button.warning, .btn.warning,
button.link, .btn.link { border-radius: 10px; }
button.secondary.btn-sm, .btn.secondary.btn-sm { border-radius: 8px; }

/* Case à cocher compacte pour activer/désactiver un mode (panneau admin) —
   remplace un bouton pleine largeur "✅ Activé / ☐ Désactivé" par ligne,
   qui prenait trop de place répété sur 3 colonnes. */
.admin-mode-toggle { width: 22px; height: 22px; cursor: pointer; accent-color: var(--success); }

/* Boutons Voir/Modifier/Supprimer en icône (pastille colorée) — évite un
   texte répété sur chaque ligne des tableaux (clients, articles). */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 10px; border: none; cursor: pointer;
  transition: transform 0.12s ease, filter 0.12s ease; padding: 0;
}
.icon-btn:hover { transform: translateY(-1px); filter: brightness(1.05); box-shadow: 0 10px 18px -9px currentColor; }
.icon-btn:active { transform: translateY(0); }
.icon-btn.view { background: var(--primary-soft); color: var(--primary); }
.icon-btn.edit { background: var(--edit-bg); color: var(--edit); }
.icon-btn.delete { background: var(--danger-bg); color: var(--danger); }
.icon-btn.send { background: var(--info-bg); color: var(--info); }
.icon-btn.sell { background: var(--success-bg); color: var(--success); }
/* [E2] Réactiver un utilisateur : action de remise en service, pas une
   action neutre ni destructrice. Classe propre plutôt qu'un recyclage de
   .sell, dont le nom n'aurait rien dit ici. */
.icon-btn.reactivate { background: var(--success-bg); color: var(--success); }
/* Indicateur de chargement générique pendant un envoi d'email (voir le
   gestionnaire "submit" dans base.html) — mêmes règles pour la variante
   icône seule (listes) et texte (vues détail). */
.is-sending { opacity: 0.55; cursor: wait !important; pointer-events: none; }
.icon-btn.restock { background: var(--info-bg); color: var(--info); }
.icon-btn.restock.active { background: var(--info); color: white; }
/* Bouton texte (pas juste une icône) pour la réintégration au stock d'une
   note de crédit — rendu volontairement plus visible qu'une simple icône,
   pour ne plus être manqué lors d'un vrai retour physique. */
.restock-btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; font-size: 13px; font-weight: 600; white-space: nowrap;
  border: none; border-radius: 8px; cursor: pointer;
  background: var(--info-bg); color: var(--info);
  transition: background 0.12s ease, color 0.12s ease;
}
.restock-btn:hover { filter: brightness(1.05); box-shadow: 0 10px 18px -9px currentColor; }
.restock-btn.active { background: var(--info); color: #fff; }
.icon-btn.discount { background: var(--success-bg); color: var(--success); font-size: 15px; font-weight: 700; }
.icon-btn.pdf { background: var(--primary-soft); color: var(--primary); }
.icon-btn.peppol { background: var(--success-bg); color: var(--success); }
.icon-btn.refund { background: var(--danger-bg); color: var(--danger); }
.icon-btn.vat { background: var(--info-bg); color: var(--info); }
.icon-btn.note { background: var(--draft-bg); color: var(--draft); }
.icon-btn[disabled], .icon-btn.disabled { cursor: default; opacity: 0.7; }
.icon-btn[disabled]:hover, .icon-btn.disabled:hover { transform: none; filter: none; box-shadow: none; }

/* Info-bulle générique (menu du haut, boutons icône de ligne...) : le
   libellé n'apparaît qu'au survol/focus clavier, à la place du "title"
   natif du navigateur (lent, moche, incohérent selon le thème). Inversion
   bg/texte via --text/--bg pour rester lisible sur les deux thèmes sans
   jeu de tokens dédié. Poser la classe .tt sur l'élément déclencheur
   (déjà en position relative) et .tip sur le <span> qui porte le texte. */
.tt { position: relative; }
.tt .tip {
  position: absolute; top: calc(100% + 8px); left: 50%;
  transform: translate(-50%, -4px);
  background: var(--text); color: var(--bg);
  font-size: 11.5px; font-weight: 700; letter-spacing: 0.01em;
  padding: 5px 9px; border-radius: 7px; white-space: nowrap;
  opacity: 0; pointer-events: none; z-index: 30;
  transition: opacity 0.12s ease, transform 0.12s ease;
  box-shadow: var(--shadow);
}
.tt:hover .tip, .tt:focus-visible .tip { opacity: 1; transform: translate(-50%, 0); }
/* Les boutons icône de ligne (tableaux) affichent leur info-bulle
   AU-DESSUS plutôt qu'en dessous (comportement par défaut de .tt
   ci-dessus, pensé pour le menu du haut) : "en dessous" chevauche le
   bouton de la ligne suivante dans un tableau dense et s'affiche masqué
   derrière lui. */
.icon-btn.tt .tip { top: auto; bottom: calc(100% + 8px); transform: translate(-50%, 4px); }
.icon-btn.tt:hover .tip, .icon-btn.tt:focus-visible .tip { transform: translate(-50%, 0); }
/* Variante multi-ligne (contenu du .tip normal = nowrap) — utilisée pour
   prévisualiser le contenu d'un modèle de lignes au survol (voir Stock →
   Modèles de lignes) : une liste d'articles ne tient pas sur une ligne. */
.tt-list .tip { white-space: pre-line; text-align: left; max-width: 240px; font-weight: 600; }

/* Nav d'ancres en haut d'une page à plusieurs sections (ex: Paramètres) —
   scroll-margin-top compense le header sticky pour que la section visée
   n'apparaisse pas cachée dessous. */
html { scroll-behavior: smooth; }
.card { scroll-margin-top: 84px; }
.config-nav {
  display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px;
}
/* Même correctif que sur les boutons : ces pastilles de navigation portent
   une icône devant leur libellé, collée au texte tant qu'on restait sur le
   flux inline par défaut. */
.config-nav a {
  padding: 6px 14px; border-radius: 999px; font-size: 13px; font-weight: 600;
  background: var(--surface); border: 1px solid var(--border); color: var(--text-muted);
  text-decoration: none;
  display: inline-flex; align-items: center; gap: 7px;
}
.config-nav a:hover { color: var(--primary); border-color: var(--primary); }

/* Sections repliables de la vue Paramètres (chacune = son propre
   formulaire/bouton "Enregistrer") — <details>/<summary> natifs, aucun
   JS nécessaire pour le pliage. */
.config-section summary {
  cursor: pointer; font-size: 16px; font-weight: 700; list-style: none;
  display: flex; align-items: center; gap: 10px; justify-content: space-between;
}
.config-section summary::-webkit-details-marker { display: none; }
/* Même séparateur dégradé que h1::before/h2::before (voir plus haut) —
   posé ici sur un pseudo-élément dédié plutôt que sur summary::before
   directement, pour ne jamais entrer en conflit avec le chevron
   (summary::after) : les deux cohabitent sans se marcher dessus. */
.config-section summary .summary-text::before {
  content: ""; width: 4px; height: 16px; border-radius: 2px;
  background: linear-gradient(180deg, var(--primary), var(--brand-grad-end));
  display: inline-block; margin-right: 10px; vertical-align: -3px;
}
/* Titre + résumé d'une ligne sous chaque titre de section (voir
   summary-caption ci-dessous) — groupés en colonne pour rester lisibles
   même repliés, contrairement au corps de la section qui ne se voit
   qu'une fois dépliée. */
.summary-group { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.summary-caption {
  font-size: 12px; font-weight: 500; text-transform: none; letter-spacing: normal;
  color: var(--text-muted); white-space: normal;
}
.config-section summary::after {
  content: "▾"; color: var(--text-muted); font-size: 13px; transition: transform 0.15s ease;
}
.config-section[open] summary::after { transform: rotate(180deg); }
.config-section[open] summary { margin-bottom: 18px; padding-bottom: 14px; border-bottom: 1px solid var(--border); }
/* Bandeau de couleur par section (même principe que les tuiles du
   dashboard, voir .dash-tile-accent-* plus bas) — un repère visuel rapide
   pour se situer parmi plusieurs sections repliées, purement décoratif
   (pas de sens métier comme sur le dashboard, juste pour varier). */
.config-section { border-left: 4px solid var(--border); }
.config-section.accent-primary { border-left-color: var(--primary); }
.config-section.accent-secondary { border-left-color: var(--primary-2); }
.config-section.accent-info { border-left-color: var(--info); }
.config-section.accent-success { border-left-color: var(--success); }
.config-section.accent-warning { border-left-color: var(--warning); }
.config-section.accent-danger { border-left-color: var(--danger); }
/* [E4] Tuile repliable IMBRIQUÉE dans une section de Paramètres (modèles
   d'email). Volontairement plus discrète que la section parente — pas de
   fond de carte, un liseré fin, un titre plus petit : sans cette
   différence de poids, deux niveaux de tuiles identiques donneraient
   l'impression d'être remonté d'un cran au lieu d'être descendu. */
.config-subsection {
  border: 1px solid var(--border); border-left: 3px solid var(--primary-2);
  border-radius: 8px; padding: 12px 14px; margin-bottom: 10px;
  background: var(--bg);
}
.config-subsection > summary {
  cursor: pointer; list-style: none; font-size: 14px; font-weight: 600;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.config-subsection > summary::-webkit-details-marker { display: none; }
.config-subsection > summary::after {
  content: "▾"; color: var(--text-muted); font-size: 12px; transition: transform 0.15s ease;
}
.config-subsection[open] > summary::after { transform: rotate(180deg); }
.config-subsection[open] > summary {
  margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--border);
}
/* Le liseré dégradé de .config-section summary .summary-text::before ferait
   doublon à ce niveau : deux marqueurs verticaux imbriqués se lisent mal. */
.config-subsection > summary .summary-text::before { content: none; }
/* Sous-groupes à l'intérieur d'une section (ex: Facturation, devenue trop
   dense pour rester un seul bloc) — même traitement que "Réseaux sociaux"/
   "Texte libre" du pied de page, formalisé en classe réutilisable. */
.config-subhead {
  margin: 22px 0 8px; font-size: 11px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--text-muted);
  position: relative; padding-top: 14px;
}
/* Même séparateur stylisé que .col-sep (dégradé --primary ->
   --primary-2), version horizontale courte au-dessus de chaque
   sous-titre — sépare proprement les sous-sections d'un même menu de
   Paramètres (ex: Paiement/TVA/Échéances/Autres dans Facturation) sans
   qu'il faille l'ajouter à la main à chaque nouvel endroit : porté par
   .config-subhead lui-même, donc automatique partout où cette classe
   est déjà utilisée. */
.config-subhead::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 40px; height: 2px; border-radius: 1px;
  background: linear-gradient(90deg, var(--primary), var(--brand-grad-end));
  opacity: 0.45;
}
/* Le tout premier sous-titre d'un panneau (juste après son résumé
   d'accordéon) n'a pas besoin de son propre séparateur — le panneau
   lui-même (bordure d'accent + titre) joue déjà ce rôle. Deux sélecteurs
   pour les deux structures existantes : sous-titre en premier enfant
   direct d'un <form> (Facturation...) ou directement du panneau
   (Sécurité...). */
.config-section form > .config-subhead:first-child,
.config-section > .config-subhead:first-child {
  margin-top: 0; padding-top: 0;
}
.config-section form > .config-subhead:first-child::before,
.config-section > .config-subhead:first-child::before {
  display: none;
}
.config-var-legend { margin: 6px 0 0; }
.config-var-legend code {
  background: var(--bg); border-radius: 4px; padding: 1px 5px; font-size: 11.5px; color: var(--text);
}

.field-confirm { display: block; margin-top: 4px; font-size: 12px; font-weight: 600; color: var(--success); }
/* Même famille visuelle que .tint (icône + fond teinté + bordure) plutôt
   qu'un simple aplat de couleur — cohérent avec les encarts info. */
.flash {
  display: flex; gap: 11px; align-items: flex-start;
  padding: 13px 16px; border-radius: var(--radius); margin-bottom: 16px;
  font-size: 14px; line-height: 1.5; border: 1px solid transparent;
}
.flash > svg { flex-shrink: 0; margin-top: 1px; }
.flash.success { background: var(--tint-green-bg); border-color: var(--tint-green-border); color: var(--success); }
.flash.error { background: var(--danger-bg); border-color: color-mix(in srgb, var(--danger) 35%, transparent); color: var(--danger); }
/* Bannière informative (pas un message flash de résultat d'action) — texte
   plus long qu'un flash classique, donc seuls le fond/la bordure sont
   teintés (pas toute la couleur du texte, pour rester lisible sur un
   paragraphe entier) ; voir credit_note_form.html pour le premier usage. */
.flash.info { background: var(--info-bg); border-color: color-mix(in srgb, var(--info) 35%, transparent); }
.muted { color: var(--text-muted); font-size: 13px; }
/* 6px verticalement rendait les lignes visuellement collées les unes aux
   autres, en particulier sur l'ajout multiple d'articles où chaque cellule
   contient un champ de saisie. 10px en haut/bas les aère sans allonger
   démesurément un tableau de plusieurs dizaines de lignes. */
.lines-table th, .lines-table td { padding: 10px 6px; }
/* Un <select> a un plancher de largeur basé sur le texte de ses options
   (min-width:auto par défaut), qui ignore table-layout:fixed — une seule
   option un peu longue ("+ Nouvelle catégorie...", un long nom de
   fournisseur) suffisait à faire déborder sa cellule et déclencher
   l'ascenseur horizontal de la table même quand l'écran avait largement la
   place (TODO_CORRECTIFS_B-FACTURE.md item 42, ajout multiple d'articles).
   min-width:0 laisse le select se comprimer à la largeur réellement
   allouée par la colonne. */
.lines-table td select { min-width: 0; }
/* "Ajouter plusieurs articles" (#bulk-lines-table) : la note "Article
   existant — la quantité s'ajoutera" (voir article_form_bulk.html)
   n'apparaît que dans la cellule Nom d'une ligne sélectionnée, la
   rendant plus haute que les autres — par défaut (vertical-align:
   middle des cellules de tableau), les autres champs de CETTE ligne se
   recentraient alors verticalement dans la ligne agrandie, décalés par
   rapport au champ Nom resté en haut de sa cellule. Épinglé en haut
   pour que tous les champs d'une même ligne restent alignés quelle que
   soit la hauteur prise par une cellule voisine. */
#bulk-lines-table td { vertical-align: top; }
.lines-table th.col-designation, .lines-table td.col-designation { width: 40%; }
.lines-table th.col-ref, .lines-table td.line-ref { width: 90px; white-space: nowrap; }
/* Ligne de note de crédit décochée ("ne pas inclure") — grisée visuellement
   mais jamais désactivée (`disabled` retirerait ses champs de la soumission
   du formulaire, décalant l'alignement positionnel des autres lignes). */
.lines-table tr.line-excluded { opacity: 0.45; pointer-events: none; }
.lines-table tr.line-excluded .line-include-checkbox { opacity: 1; pointer-events: auto; }
/* Pas besoin des flèches +/- du navigateur sur les lignes de facture (qté,
   prix, remise) — juste un input classique, plus net. */
.lines-table input[type=number] { -moz-appearance: textfield; }
.lines-table input[type=number]::-webkit-outer-spin-button,
.lines-table input[type=number]::-webkit-inner-spin-button {
  -webkit-appearance: none; margin: 0;
}
.line-total-htva, .line-total-ttc { white-space: nowrap; font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
/* inline-flex + gap (pas inline-block + marges internes à l'icône) : les
   icônes utilisées ici (icon_dot 8px pour un statut, icon_wrench/
   icon_register/icon_check_circle 18px par défaut pour tout le reste)
   n'ont jamais la même taille native, ce qui faisait varier la hauteur
   totale du badge d'un type à l'autre et les désalignait verticalement
   quand plusieurs badges de types différents sont côte à côte (ex:
   statut + type de vente sur la vue facture) — voir aussi .badge .ico
   ci-dessous qui uniformise la taille affichée de l'icône elle-même. */
.badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px; border-radius: 999px; font-size: 12px; background: var(--primary-soft); color: var(--primary); font-weight: 600; margin-left: 6px; vertical-align: middle; }
.badge .ico { width: 9px; height: 9px; flex-shrink: 0; }

/* Référence/SKU d'article : longueurs très variables selon la source
   (import Excel, saisie manuelle...) — un rectangle discret en police à
   chasse fixe reste uniforme quel que soit le format, contrairement à la
   pastille colorée (.badge) pensée pour un court libellé humain. */
.ref-code {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 12px;
  padding: 2px 7px;
  border-radius: 6px;
  background: var(--primary-soft);
  border: 1px solid var(--primary);
  color: var(--primary);
  vertical-align: middle;
}
.badge-success { background: var(--success-bg); color: var(--success); }
.badge-danger { background: var(--danger-bg); color: var(--danger); }
.badge-info { background: var(--info-bg); color: var(--info); }
.badge-atelier { background: var(--atelier-bg); color: var(--atelier); }
.badge-comptoir { background: var(--comptoir-bg); color: var(--comptoir); }
.badge-warning { background: var(--warning-bg); color: var(--warning); }
.badge-draft { background: var(--draft-bg); color: var(--draft); }

/* Encarts colorés (info/succès/attente) — nouveau composant introduit avec
   la charte B-In-Tech, n'existait pas avant (ex: rappel mode Peppol activé,
   confirmation de sauvegarde, devis en attente de relance). */
.tint {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 16px;
  font-size: 13.5px;
  line-height: 1.5;
}
.tint .tint-icon { flex-shrink: 0; font-size: 16px; line-height: 1; }
.tint-green { background: var(--tint-green-bg); border: 1px solid var(--tint-green-border); color: var(--success); }
.tint-amber { background: var(--tint-amber-bg); border: 1px solid var(--tint-amber-border); color: var(--warning); }

/* Tableau "aéré" : pas de grille verticale, lignes hairline seulement, plus
   d'air. border-collapse et border-bottom:1px sur td sont déjà hérités de
   la règle générique `table`/`th, td` ci-dessus (même valeur) — seuls le
   padding et l'épaisseur du filet d'en-tête diffèrent vraiment ici, donc
   ce sont les seules déclarations gardées (la redondance précédente ne
   causait pas de rendu visiblement faux — border-collapse fusionne les
   filets adjacents en un seul trait quoi qu'il arrive — mais dupliquait
   sans raison la même valeur dans deux règles). */
.table-airy th { border-bottom: 2px solid var(--border); padding: 11px 12px; }
.table-airy td { padding: 14px 12px; transition: background 0.1s ease; }
.table-airy tr:hover td { background: var(--bg); }
table tr:hover td { transition: background 0.1s ease; }

/* Récapitulatif des totaux façon PDF : aligné à droite, aéré */
.totals-summary { max-width: 320px; margin-left: auto; margin-top: 18px; }
.totals-summary > div { display: flex; justify-content: space-between; padding: 6px 0; font-size: 14px; color: var(--text-muted); }
.totals-summary > div > span:last-child { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.totals-summary-main { border-top: 2px solid var(--primary); margin-top: 6px; padding-top: 10px !important; font-weight: 700; font-size: 17px !important; color: var(--primary) !important; }
.totals-summary-bold { font-weight: 700; color: var(--text) !important; }

tr.row-overdue td:first-child { border-left: 3px solid var(--danger); }
tr.row-overdue { background: var(--danger-bg); }
/* .stat-value/.stat-label : chiffre + légende des tuiles KPI du dashboard
   (contenu brut des partials dashboard_tiles/_*.html, sans wrapper dédié
   — c'est .dash-tile qui porte le cadre, voir plus bas). */
.stat-value {
  font-size: 26px; font-weight: 800; color: var(--text); line-height: 1.15;
  font-family: var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
}
.stat-label {
  font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-top: 4px;
  font-family: var(--font-sans); font-weight: 700;
}

/* Tableau de bord personnalisable : grille de tuiles réordonnables par
   glisser-déposer (HTML5 natif, vanilla JS — voir dashboard.html). Chaque
   tuile déclare sa taille (small/wide/full) via une classe, l'ordre lui-même
   vient du DOM (reconstruit après un drop et envoyé au serveur). */
/* align-items:start est le correctif clé : par défaut, une grille CSS
   étire chaque tuile à la hauteur de la plus grande de sa rangée — une
   petite tuile chiffrée à côté d'un tableau de 8 lignes se retrouvait
   avec un immense vide. Chaque tuile ne prend maintenant que la hauteur
   de son propre contenu. */
/* grid-auto-flow: dense comble les trous quand l'ordre des tuiles
   (personnalisable par glisser-déposer, voir dashboard_layout_save) ne
   groupe pas parfaitement les tuiles de même taille — une petite tuile
   plus loin dans l'ordre remonte remplir un trou plutôt que de laisser
   du vide. L'ordre par défaut (DASHBOARD_TILES dans main.py) groupe déjà
   small/wide/full à la suite, donc ça ne change rien tant que l'ordre
   reste "propre" — filet de sécurité pour les dispositions personnalisées
   ou héritées d'anciennes versions (moins de tuiles qu'aujourd'hui). */
/* Disposition façon "masonry" : grid-auto-rows fixe une petite unité de
   mesure, et chaque tuile reçoit son propre grid-row-end:span N calculé
   en JS (voir dashboard.html) d'après sa hauteur RÉELLE de contenu —
   sans ça, la hauteur de chaque ligne de la grille s'aligne sur sa tuile
   la plus haute et les voisines plus courtes laissent un vide en dessous
   (grid-auto-flow:dense seul ne résout que les trous de COLONNE, pas les
   écarts de HAUTEUR entre tuiles d'une même ligne). align-items:start
   reste le filet de sécurité tant que le JS n'a pas encore mesuré/posé
   les spans (chargement, JS désactivé). */
/* Bandeau checklist de démarrage — affiché une fois après la toute
   première acceptation des CGU (voir accept_terms() dans main.py),
   au-dessus de la grille de tuiles pour rester visible tant qu'il n'est
   pas masqué explicitement (contrairement aux tuiles, jamais repliable
   ni réordonnable). */
.onboarding-checklist { border: 1px solid var(--primary-soft); margin-top: 18px; }
.onboarding-checklist-list { list-style: none; padding: 0; margin: 10px 0 0; display: flex; flex-direction: column; gap: 8px; }
.onboarding-checklist-list li { display: flex; align-items: center; gap: 10px; font-size: 14px; }
.onboarding-checklist-list li.done { color: var(--text-muted); }
.onboarding-checklist-list li.done a, .onboarding-checklist-list li.done span:last-child { text-decoration: line-through; }

.dash-grid {
  /* Le navigateur applique son propre « ancrage de défilement » quand la
     hauteur d'un élément change ; il entrait en concurrence avec la
     compensation explicite du dépliage de tuile (voir dashboard.html,
     évènement `toggle`) et rendait le résultat imprévisible. On coupe le
     mécanisme automatique ici pour que la compensation manuelle soit la
     seule à agir. */
  overflow-anchor: none; display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-flow: dense; grid-auto-rows: 4px; gap: 14px; margin: 18px 0; align-items: start; }
/* Hauteur plancher commune à toutes les petites tuiles chiffrées : sans
   ça, une légende courte ("Client(s)") et une longue sur deux lignes
   ("Total TVA (taux 21 %) — période personnalisée du ... au ...")
   donnent des hauteurs naturelles différentes, et la disposition
   "masonry" (qui mesure la vraie hauteur de chaque tuile, voir plus bas)
   les alignerait alors en rangées inégales au lieu du bloc bien net
   attendu pour ce groupe de tuiles. Les tuiles wide/full, elles,
   affichent des contenus trop variables (tableau, graphique...) pour
   qu'une hauteur commune ait un sens — non concernées. */
.dash-tile-small {
  grid-column: span 1;
  min-height: 112px;
  display: flex;
  flex-direction: column;
}
/* Centre le chiffre+légende dans la hauteur commune ci-dessus au lieu de
   les laisser collés en haut avec du vide en dessous pour les libellés
   courts — un chiffre qui "flotte" au milieu d'une carte lit mieux qu'un
   chiffre plaqué en haut d'une carte à moitié vide. */
.dash-tile-small .dash-tile-content { flex: 1; display: flex; flex-direction: column; justify-content: center; }
/* Toutes les tuiles "grandes" (wide ET full) prennent toute la largeur
   et s'empilent sous la rangée de petites tuiles — même règle pour les
   deux tailles désormais (wide n'occupait qu'une demi-largeur avant,
   pouvant se retrouver côte à côte avec une autre wide selon l'ordre :
   incohérent avec "full", qui a toujours été pleine largeur). Les deux
   classes restent distinctes dans DASHBOARD_TILES/dashboard.html (elles
   partagent déjà .dash-tile-title, seul grid-column changeait) au cas
   où une taille intermédiaire redeviendrait utile plus tard. */
.dash-tile-wide, .dash-tile-full { grid-column: 1 / -1; }
@media (max-width: 720px) {
  .dash-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 460px) {
  .dash-grid { grid-template-columns: 1fr; }
}
.dash-tile {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 16px 18px;
  box-shadow: var(--shadow);
}
/* Titre des tuiles wide/full — c'est aussi le <summary> du <details> qui
   les rend repliables (voir dashboard_tiles/_*.html) : les 8 petites
   tuiles chiffrées n'utilisent jamais cette classe, seulement
   .stat-value/.stat-label, donc rien ici ne les affecte. */
.dash-tile-title {
  margin: 0 0 10px;
  font-size: 15px;
  font-family: var(--font-sans);
  font-weight: 700;
  letter-spacing: -0.01em;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  list-style: none;
}
.dash-tile-title::-webkit-details-marker { display: none; }
/* Même séparateur dégradé que h2::before/.config-section — cohérence
   visuelle entre les titres de tuiles et le reste de l'appli. */
.dash-tile-title::before {
  content: ""; width: 4px; height: 15px; border-radius: 2px;
  background: linear-gradient(180deg, var(--primary), var(--brand-grad-end));
  display: inline-block; flex-shrink: 0;
}
/* Chevron repli/dépli — seul élément avec margin-left:auto : il absorbe
   tout seul l'espace restant et se retrouve donc plaqué à droite, que la
   tuile ait ou non un bouton d'action supplémentaire (ex: "Voir toutes
   les factures" sur _recent_invoices.html) entre le titre et lui. */
.dash-tile-title::after {
  content: "▾"; margin-left: auto; flex-shrink: 0;
  color: var(--text-muted); font-size: 12px; transition: transform 0.15s ease;
}
details[open] > .dash-tile-title::after { transform: rotate(180deg); }
.dash-tile-content details:not([open]) { margin-bottom: 0; }
/* Repliée, la tuile n'a plus de contenu sous le titre pour justifier les
   10px de margin-bottom de .dash-tile-title (pensés pour espacer le
   titre du contenu quand la tuile est ouverte) — ce blanc résiduel en
   bas, sans équivalent en haut (16px de padding symétrique sur .dash-tile
   lui-même), décalait visuellement le titre vers le haut au lieu de le
   centrer verticalement dans la tuile repliée. */
.dash-tile-content details:not([open]) .dash-tile-title { margin-bottom: 0; }
.dash-tile-kpi { font-size: 22px; font-weight: 800; margin-bottom: 10px; font-variant-numeric: tabular-nums; }
.dash-tile-kpi .muted { font-size: 12px; font-weight: 500; }
.dash-tile-handle {
  display: none;
  position: absolute;
  top: 10px; right: 12px;
  font-size: 16px;
  color: var(--text-muted);
  cursor: grab;
  user-select: none;
}
/* Mode édition : la tuile entière devient déplaçable, le contenu (liens,
   boutons vers d'autres pages) est neutralisé le temps du glisser-déposer
   pour éviter une navigation accidentelle en pleine réorganisation. */
.dash-grid.edit-mode .dash-tile { cursor: grab; border-style: dashed; border-color: var(--primary); }
.dash-grid.edit-mode .dash-tile-handle { display: block; }
.dash-grid.edit-mode .dash-tile-content { pointer-events: none; opacity: 0.85; }
.dash-tile.dragging { opacity: 0.4; }
.trend-up { color: var(--success); font-weight: 700; font-size: 12px; }
.trend-down { color: var(--danger); font-weight: 700; font-size: 12px; }

/* Accent par tuile (voir DASHBOARD_TILES["color"] dans main.py) : une
   bordure + le chiffre principal reprennent la couleur qui a du sens pour
   la statistique (argent dû = rouge, croissance = vert...), plutôt qu'un
   habillage purement décoratif. */
.dash-tile-accent-primary { border-left: 4px solid var(--primary); }
.dash-tile-accent-primary .stat-value, .dash-tile-accent-primary .dash-tile-kpi { color: var(--primary); }
.dash-tile-accent-secondary { border-left: 4px solid var(--primary-2); }
.dash-tile-accent-secondary .stat-value, .dash-tile-accent-secondary .dash-tile-kpi { color: var(--primary-2); }
.dash-tile-accent-info { border-left: 4px solid var(--info); }
.dash-tile-accent-info .stat-value, .dash-tile-accent-info .dash-tile-kpi { color: var(--info); }
.dash-tile-accent-success { border-left: 4px solid var(--success); }
.dash-tile-accent-success .stat-value, .dash-tile-accent-success .dash-tile-kpi { color: var(--success); }
.dash-tile-accent-warning { border-left: 4px solid var(--warning); }
.dash-tile-accent-warning .stat-value, .dash-tile-accent-warning .dash-tile-kpi { color: var(--warning); }
.dash-tile-accent-danger { border-left: 4px solid var(--danger); }
.dash-tile-accent-danger .stat-value, .dash-tile-accent-danger .dash-tile-kpi { color: var(--danger); }

/* Variantes colorées des barres .pricebar-fill (par défaut grises), pour
   les graphiques du dashboard — même palette que les accents de tuile. */
.pricebar-fill.accent-primary { background: var(--primary); }
.pricebar-fill.accent-secondary { background: var(--primary-2); }
.pricebar-fill.accent-info { background: var(--info); }
.pricebar-fill.accent-success { background: var(--success); }
.pricebar-fill.accent-warning { background: var(--warning); }
.pricebar-fill.accent-danger { background: var(--danger); }

/* Fiche client : blocs bien séparés */
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 24px; }
.info-grid dt { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); margin-bottom: 2px; }
.info-grid dd { margin: 0 0 10px 0; font-size: 14px; }

/* Pied de page du site (marque du logiciel B-In-Tech, pas de la facture).
   Bandeau plein largeur, couleur de marque fixe (indépendante du thème
   clair/sombre : c'est une signature visuelle, pas une zone de contenu),
   toujours collé en bas de la fenêtre grâce à `main { flex: 1 0 auto }`
   ci-dessus, même sur une page avec peu de contenu. */
footer.site-footer {
  background: #1c222c;
  color: #aab0bf;
  font-size: 13px;
  flex-shrink: 0;
  width: 100%;
  border-top: 3px solid var(--primary);
}
.site-footer-inner {
  max-width: 1100px;
  margin: 0 auto;
  /* Compacté le 2026-08-28 (retour utilisateur : « il prend vraiment
     beaucoup de place »). Le pied de page mesurait ~195px sur CHAQUE page
     d'une application de travail — acceptable sur un site vitrine, pas ici.
     Sa hauteur venait de DEUX contraintes simultanées, pas d'une seule : le
     logo (88px) et le bloc contact (4 lignes, ~88px). Réduire le seul logo
     n'aurait donc rien changé, le contact prenant le relais. Les deux sont
     resserrés ensemble : ~195px -> ~141px. */
  padding: 18px 24px 12px;
}
.site-footer-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 18px;
}
.site-footer-identity {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 16px;
}
/* Logo de la page de connexion : hauteur fixe (le fichier est bien plus
   grand, on ne veut pas dépendre de ses dimensions natives) et respiration
   au-dessus de l'encadré, qui a lui-même sa propre marge haute. */
.login-logo {
  display: block; height: 160px; width: auto; max-width: 100%;
  margin: 0 auto 26px;
}

/* Fond d'écran de la page de connexion (retour utilisateur 2026-08-27,
   inspiré de la charte du logo B-Facture fourni : navy/orange) — dégradé
   FIXE, volontairement indépendant du thème clair/sombre comme le pied de
   page (.site-footer, voir plus haut) : c'est un moment de marque, pas une
   zone de contenu à lire. Ciblé via :has() sur .login-page-bg plutôt qu'un
   bloc {% block body_class %} (qui n'existe pas dans base.html) — n'affecte
   donc que la page de connexion, jamais les autres pages qui partagent la
   même .app-content (mot de passe oublié, MFA, rôle comptable...). */
/* Écran de connexion : la même identité que le reste de l'app, mais assumée
   — halo plus large et filigrane plus grand. C'est le seul écran vu avant
   toute connexion, donc le seul où la marque se présente d'elle-même. */
.app-content:has(.login-page-bg) {
  position: relative;
  background-position: right -40px bottom -60px, center, center;
  background-size: 560px, cover, cover;
}
.app-content:has(.login-page-bg) .topbar { background: transparent; }
/* Le bandeau du haut et son bouton de thème ne repassent en clair QUE sur
   fond sombre. Avant, `color: #fff` était appliqué dans les deux thèmes :
   en thème clair, l'icône devenait blanche sur un fond devenu clair. */
[data-theme="dark"] .app-content:has(.login-page-bg) .topbar {
  border-bottom-color: color-mix(in srgb, white 12%, transparent);
}
[data-theme="dark"] .app-content:has(.login-page-bg) #theme-toggle-standalone { color: #fff; }
.login-page-bg { position: relative; z-index: 1; }

/* Ni border-radius ni box-shadow depuis le passage à un logo TRANSPARENT
   (2026-08-26) : une ombre portée s'applique à la boîte de l'élément, pas au
   dessin — elle traçait un rectangle flottant autour du vide. Les arrondis,
   eux, ne rognaient plus rien. Le logo se pose directement sur le fond sombre
   du pied de page, ce pour quoi il est fait. */
.site-footer-logo {
  /* 88 -> 56px : le lockup reste parfaitement lisible, et c'est l'écran de
     CONNEXION qui porte le moment de marque (.login-logo, inchangé), pas ce
     pied de page répété sur chaque page. */
  height: 56px;
  width: auto;
}
.site-footer-tagline { font-size: 12.5px; color: #8b90a0; }
/* Interligne et écart resserrés : ces 4 lignes dictaient la hauteur de la
   rangée autant que le logo. */
.site-footer-contact { display: flex; flex-direction: column; gap: 2px; line-height: 1.3; text-align: right; }
.site-footer-contact span { display: inline-flex; align-items: center; gap: 6px; justify-content: flex-end; }
.site-footer-contact svg { flex-shrink: 0; opacity: 0.75; }
.site-footer-copy {
  font-size: 12px;
  color: #6f7484;
  text-align: center;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.site-footer-contact a, footer.site-footer a { color: #c3c7d3; text-decoration: none; }
.site-footer-contact a:hover, footer.site-footer a:hover { color: var(--primary); text-decoration: underline; }

@media (max-width: 640px) {
  .site-footer-inner { padding: 24px 20px 16px; }
  .site-footer-row { flex-direction: column; align-items: flex-start; }
  .site-footer-contact { text-align: left; }
}
.line-cost-warning { color: var(--danger); font-size: 12px; margin-top: 4px; }

/* Graphique "prix par fournisseur" (barres en CSS, pas de lib JS) */
.pricebars { display: flex; flex-direction: column; gap: 12px; }
.pricebar-row { display: grid; grid-template-columns: 160px 1fr 80px; align-items: center; gap: 12px; }
.pricebar-label { font-size: 13px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pricebar-track { background: var(--border); border-radius: 6px; height: 18px; overflow: hidden; }
.pricebar-fill { height: 100%; background: var(--text-muted); border-radius: 6px; }
.pricebar-fill.best { background: var(--success); }
.pricebar-value { font-size: 13px; color: var(--text-muted); text-align: right; }
@media (max-width: 640px) {
  .pricebar-row { grid-template-columns: 1fr; gap: 4px; }
  .pricebar-value { text-align: left; }
}

/* Recherche d'articles du catalogue (lignes de facture) */
.article-suggestions {
  display: none; position: absolute; left: 0; right: 0; top: 100%; z-index: 20;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: var(--shadow); max-height: 240px; overflow-y: auto; margin-top: 4px;
}
.article-suggestion {
  display: flex; justify-content: space-between; align-items: center; gap: 10px;
  padding: 8px 12px; cursor: pointer; font-size: 13.5px;
}
.article-suggestion:hover { background: var(--bg); }
.article-suggestion-meta { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.article-suggestion.category-row { font-weight: 600; color: var(--primary); }

/* Liste à cases à cocher de la modale "Ajouter plusieurs articles" —
   même look que .article-suggestion mais dans un conteneur toujours
   visible (pas un dropdown positionné en absolu) et sans le survol qui
   suggère une sélection immédiate au clic (ici on coche, on ne clôt pas). */
.multi-article-list {
  max-height: 320px; overflow-y: auto; border: 1px solid var(--border);
  border-radius: 8px; margin-top: 10px;
}
.multi-article-row {
  display: flex; align-items: center; gap: 10px; padding: 8px 12px;
  font-size: 13.5px; border-bottom: 1px solid var(--border); cursor: pointer;
}
.multi-article-row:last-child { border-bottom: none; }
.multi-article-row:hover { background: var(--bg); }
.multi-article-row input[type="checkbox"] { width: auto; flex-shrink: 0; }
.multi-article-row .article-suggestion-meta { margin-left: auto; }

/* Puces de la sélection en cours (preview sous la liste à cocher) —
   chaque puce se retire individuellement sans rouvrir la liste complète. */
/* Grille à 3 colonnes (nom · quantité · retrait) plutôt qu'une rangée de
   pastilles : en pastilles, un nom long étirait toute la ligne et les champs
   de quantité ne s'alignaient jamais entre eux — la zone devenait un pavé
   illisible dès 5 ou 6 articles (retour utilisateur, capture à l'appui).
   La colonne du nom absorbe la largeur restante et tronque proprement, les
   deux autres gardent une taille fixe, donc tout reste aligné. */
.multi-article-preview {
  display: grid; grid-template-columns: 1fr 76px 28px;
  align-items: center; gap: 4px 10px;
  max-height: 240px; overflow-y: auto;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: 10px;
}
.multi-article-pick-name {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13.5px;
}
/* `input.` et pas seulement `.` : la règle générique input[type=number] pose
   width:100% avec une spécificité de 0-1-1, qui l'emportait sur une simple
   classe (0-1-0) — le champ s'étirait alors sur toute la cellule. */
input.multi-article-pick-qty {
  width: 100%; padding: 4px 8px; font-size: 13px;
  text-align: right; font-variant-numeric: tabular-nums;
}
input.multi-article-pick-qty::-webkit-outer-spin-button,
input.multi-article-pick-qty::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.multi-article-pick-remove {
  border: none; background: none; cursor: pointer; color: var(--text-muted);
  width: 24px; height: 24px; border-radius: 50%; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.multi-article-pick-remove:hover { background: var(--border); color: var(--danger); }
.multi-article-chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface-alt, var(--bg)); border: 1px solid var(--border);
  border-radius: 20px; padding: 4px 6px 4px 12px; font-size: 12.5px;
}
.multi-article-chip-remove {
  border: none; background: none; cursor: pointer; color: var(--text-muted);
  width: 18px; height: 18px; border-radius: 50%; display: inline-flex;
  align-items: center; justify-content: center; font-size: 11px; padding: 0;
}
.multi-article-chip-remove:hover { background: var(--border); color: var(--text); }

/* Puces "réseau social" — icône maison (pas de vrai logo de marque, on
   reste hors-ligne et on évite de reproduire des assets protégés), même
   usage dans le formulaire des Paramètres et sur le PDF de facture. */
.social-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 5px; font-size: 9px; font-weight: 800;
  color: white; vertical-align: middle; margin-right: 3px;
}
.social-icon.ig { background: linear-gradient(135deg, #f58529, #dd2a7b, #8134af); }
.social-icon.fb { background: #1877f2; }
.social-icon.tt { background: #111827; }
.social-icon.web { background: var(--primary); font-size: 11px; }
.actions { display: flex; gap: 10px; align-items: center; }
/* Barre d'actions en haut d'une fiche détail (chantier menu latéral,
   phase suivante) — repris du menu déroulant "Actions ▾" (une action par
   page, jamais assez pour justifier de le cacher derrière un clic une
   fois la place rendue par la sidebar). Même traitement visuel que
   l'ancienne .topbar horizontale (fond, filet dégradé, collée en haut) :
   décision explicite de l'utilisateur — retrouver "l'emplacement du
   menu d'avant", avant même le bandeau d'impersonation admin. Rendue via
   {% block view_toolbar %} dans base.html (PAS le premier élément de
   {% block content %}) : elle vit hors de <main>, au même niveau que
   .admin-banner et l'ancienne .topbar, donc pleine largeur de
   .app-content sans marge négative à calculer pour s'extraire du
   padding de <main>. */
/* Groupe d'éléments collés à DROITE du bandeau d'actions : le bouton
   d'action principal ("Nouvelle facture"...) puis l'icône « ? » (voir
   _help_link.html).

   Seul le PREMIER du groupe porte `margin-left:auto`. Deux marges auto dans
   un même conteneur flex se partagent l'espace libre au lieu de le donner
   entièrement au premier : le bouton principal se retrouvait alors renvoyé
   au milieu de la barre au lieu de rester à côté du « ? ». */
.view-toolbar-right,
.view-toolbar-help { margin-left: auto; }
.view-toolbar-right ~ .view-toolbar-help { margin-left: 0; }

/* Rouge : seule couleur de la palette qu'aucun bouton d'action n'utilise, donc
   la seule qui distingue l'aide du reste au premier coup d'œil. Sans elle, le
   bouton se noyait dans la barre et passait inaperçu. Padding resserré : il n'a
   pas de libellé, donc pas besoin de la largeur d'un bouton texte. */
/* Bouton d'aide : rouge PLEIN et non plus un glyphe rouge sur fond neutre —
   il se noyait encore parmi les autres actions (retour utilisateur
   2026-08-28). Le rouge n'est pas un avertissement ici, c'est simplement la
   seule couleur de la palette qu'aucun bouton d'action n'utilise.

   `color: var(--surface)` fait tout le travail dans les deux thèmes : en
   clair c'est un quasi-blanc sur rouge vif (4,7:1), en sombre un anthracite
   sur rouge clair (5,9:1). Du blanc fixe serait tombé à 2,77:1 en sombre. */
/* Sélecteur à DEUX classes, pas `.view-toolbar-help` seul : le bouton porte
   `class="btn secondary view-toolbar-help"`, et `.btn.secondary` (0,2,0) pèse
   plus lourd qu'une classe isolée (0,1,0) — le fond rouge aurait été écrasé
   par le fond neutre, quel que soit l'ordre dans le fichier. À spécificité
   égale, c'est la position plus bas dans le fichier qui tranche. */
.btn.view-toolbar-help {
  background: var(--danger); color: var(--surface);
  border-color: transparent; border-radius: 10px;
  padding-left: 11px; padding-right: 11px;
}
.btn.view-toolbar-help:hover { box-shadow: 0 14px 26px -14px var(--danger); }

.view-toolbar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  border-image: linear-gradient(90deg, var(--primary), var(--brand-grad-end)) 1;
  padding: 12px 24px;
}
/* Les icônes de contrôle d'une ligne de facture (note/remise/TVA/suppr.)
   doivent toujours rester sur un seul niveau — un bloc 2x2 rend mal dès
   qu'il n'y en a que 3 (TVA absente si le réglage n'est pas activé). */
.lines-table td.actions { flex-wrap: nowrap; }
/* Ligne pleine largeur pour la saisie de note (sous la ligne d'article
   concernée), repliée par défaut — voir showNote()/cancelNote(). */
.lines-table tr.note-row td { padding-top: 0; padding-bottom: 10px; }
.lines-table tr.note-row .note-input-row {
  display: flex; align-items: center; gap: 8px; color: var(--draft);
}
.lines-table tr.note-row input[name=line_note] { flex: 1; }
/* Remise/TVA de ligne : input + bouton d'annulation côte à côte, mêmes
   idées que la ligne de note (annuler = revenir à l'état par défaut, pas
   juste masquer une valeur qui resterait soumise en douce). */
.lines-table .discount-input, .lines-table .vatrate-input {
  display: inline-flex; align-items: center; gap: 4px;
}
/* Barre de filtres sur deux lignes stables : la 1ère (recherche + tri)
   ne bouge jamais, la 2e (période éventuellement dépliée en "du"/"au",
   filtres spécifiques à la vue) peut s'allonger sans jamais perturber la
   première — notamment quand "Personnalisé" ajoute deux champs de date. */
.search-bar { display: flex; flex-direction: column; gap: 10px; margin-bottom: 16px; }
.search-bar-row { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.search-bar select { width: auto; }
#period-custom-fields { display: flex; gap: 10px; }
#period-custom-fields input[type=date] { max-width: 160px; }

/* Légende des statuts de facture : épinglée en haut de la liste, reste
   visible même en scrollant une longue liste (200 lignes...). */
.status-legend {
  position: sticky; top: 0; z-index: 5; background: var(--surface);
  border-bottom: 1px solid var(--border); margin: -22px -22px 16px; padding: 12px 22px;
  font-size: 13px; color: var(--text-muted); border-radius: 12px 12px 0 0;
  text-align: center;
}
/* Variante pour une légende qui n'est pas tout en haut de la carte (ex:
   sous un titre) : pas de "débordement" vers le haut, coins non arrondis. */
.status-legend.inline { margin: 12px -22px 16px; border-radius: 0; border-top: 1px solid var(--border); }
.search-bar input { max-width: 320px; }
/* Ligne de recherche seule sur toute la largeur disponible (ex: Stock,
   filtres magasin/catégorie/quantité négative sur la ligne du dessus) —
   contrairement à .search-bar input ci-dessus, pensé pour un champ de
   recherche à côté d'autres contrôles sur la même ligne. */
.search-bar-row-full input[type=text] { max-width: none; flex: 1; }

/* Légendes cliquables (_status_legend.html, _client_type_legend.html,
   _sale_type_legend.html) : simple soulignement au clic, dans le style
   déjà en place pour chaque composant plutôt qu'un style inventé à part. */
.filter-link { text-decoration: none; }
.filter-link.active { text-decoration: underline; text-underline-offset: 3px; }

/* Puce d'état pleine couleur (remplace les emoji, peu distincts selon
   police/OS) — une classe de couleur par état, priorité gérée côté
   template (une seule pastille affichée par ligne). */
.status-dot {
  display: inline-block; width: 10px; height: 10px; border-radius: 50%;
  flex: none;
}
.status-dot.draft { background: var(--draft); }
.status-dot.overdue { background: var(--danger); }
.status-dot.refund { background: var(--info); }
.status-dot.balance { background: var(--warning); }
.status-dot.paid { background: var(--success); }
.status-dot.open { background: var(--primary); }
.status-dot.closed { background: var(--text-muted); }
.status-dot.paused { background: var(--warning); }
.status-dot.atelier { background: var(--atelier); }
.status-dot.comptoir { background: var(--comptoir); }
.status-dot.vente-vehicule { background: var(--warning); }

/* En-tête de colonne triable par clic (Numéro/Client/Date...) */
.sortable-th a { display: inline-flex; align-items: center; gap: 4px; color: inherit; text-decoration: none; }
.sortable-th a:hover { color: var(--primary); }
.sort-indicator { font-size: 11px; color: var(--primary); }
.totals-preview { display: flex; gap: 24px; flex-wrap: wrap; justify-content: flex-end; text-align: right; padding: 14px 0; border-top: 1px dashed var(--border); margin-top: 10px; }
.totals-preview div { font-size: 14px; }
.totals-preview strong { display: block; font-size: 18px; }
.totals-preview-main strong { color: var(--primary); font-size: 24px; }

/* Ventilation TVA par taux dans l'aperçu en direct des formulaires (et
   reprise telle quelle dans la modale de confirmation) — colonne compacte
   alignée à droite, sous la ligne "TVA". Le `strong` repasse en inline :
   `.totals-preview strong` le met en block pour les grands totaux, ce qui
   casserait ces lignes en deux. */
.vat-breakdown { display: flex; flex-direction: column; gap: 2px; align-items: flex-end; }
.vat-breakdown-line { display: flex; gap: 8px; align-items: baseline; font-size: 12.5px; white-space: nowrap; }
.totals-preview .vat-breakdown-line strong { display: inline; font-size: 13px; }

/* Modale de confirmation (aperçu avant création/modification de facture) —
   pas de dépendance externe, overlay + carte centrée. */
.modal-overlay {
  display: none; position: fixed; inset: 0; background: rgba(15, 18, 26, 0.55);
  align-items: center; justify-content: center; z-index: 100; padding: 20px;
}
.modal-overlay.open { display: flex; }
.modal-box {
  background: var(--surface); border-radius: 14px; box-shadow: var(--shadow);
  padding: 24px; max-width: 560px; width: 100%;
}
.modal-box h2 { margin-top: 0; }
/* Les modales de création rapide (article à la volée...) ne sont pas dans
   un <form> (pas de soumission réelle, juste des boutons JS) et n'héritent
   donc jamais du "form .field { margin-bottom: 14px }" qui aère les
   formulaires classiques — sans ça les rangées de champs empilées se
   touchaient (retour utilisateur 2026-08-20). Le sélecteur cible aussi
   #quick-client-vehicle-fields (retour 2026-08-27) : ses lignes ne sont
   pas des enfants DIRECTS de .modal-box (imbriquées dans ce wrapper, voir
   _quick_client_modal.html), donc le ">" seul les ratait complètement —
   c'était la vraie cause de la section véhicule visuellement tassée. */
.modal-box > .row, .modal-box > .field,
#quick-client-vehicle-fields > .row, #quick-client-vehicle-fields > .field { margin-bottom: 14px; }
.modal-preview { display: flex; flex-direction: column; gap: 6px; font-size: 14px; margin: 14px 0 20px; }
.modal-preview .muted { font-size: 12px; }
.modal-preview .table-airy th, .modal-preview .table-airy td { padding: 6px 8px; font-size: 13px; }
/* margin-top garantit un espace avant la rangée de boutons quel que soit
   ce qui précède (tableau, liste, div sans marge propre) — avant ce
   correctif, "Annuler"/"Confirmer" pouvaient coller directement au
   contenu du dessus selon le contenu de la modale. */
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }

/* Vue en cartes du stock de véhicules à vendre (mode garage) — largeur
   auto-adaptative plutôt qu'un nombre de colonnes fixe (contrairement au
   tableau de bord) : une photo de moto a besoin de plus de place qu'une
   tuile de chiffre. */
.vehicle-stock-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 18px; margin: 18px 0; }
.vehicle-stock-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  overflow: hidden; display: flex; flex-direction: column; box-shadow: var(--shadow);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.vehicle-stock-card:hover { transform: translateY(-2px); box-shadow: 0 4px 14px rgba(16, 24, 40, 0.12); }
/* Toute la zone photo+infos est cliquable (mène à la fiche détail) — seule
   la barre d'actions en bas reste hors du lien, pour ne jamais imbriquer
   un <form>/bouton dans un <a>. */
.vehicle-stock-card-link {
  display: flex; flex-direction: column; flex: 1; color: inherit; text-decoration: none;
}
.vehicle-stock-photo {
  /* Hauteur fixe plutôt qu'aspect-ratio : reste fiable même en enfant
     flex/grid imbriqué (certains moteurs de rendu ne recalculent pas
     toujours la hauteur issue d'aspect-ratio dans ce contexte, ce qui
     pouvait laisser la zone photo à 0px de haut — l'image existait bien
     mais restait invisible). */
  height: 170px; flex-shrink: 0; background: var(--bg); display: flex; align-items: center;
  justify-content: center; color: var(--text-muted); overflow: hidden;
}
.vehicle-stock-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }
.vehicle-stock-body { padding: 14px 16px; flex: 1; display: flex; flex-direction: column; gap: 4px; }
.vehicle-stock-title { font-weight: 700; font-size: 16px; }
.vehicle-stock-meta { font-size: 13px; color: var(--text-muted); }
.vehicle-stock-price { font-size: 19px; font-weight: 800; color: var(--primary); margin-top: 4px; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.vehicle-status-badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; width: fit-content; }
.vehicle-status-badge.in_stock { background: var(--success-bg); color: var(--success); }
.vehicle-status-badge.reserved { background: var(--warning-bg); color: var(--warning); }
.vehicle-status-badge.sold { background: var(--draft-bg); color: var(--draft); }
.vehicle-stock-actions { display: flex; gap: 6px; padding: 10px 14px; border-top: 1px solid var(--border); }
.vehicle-stock-photos-strip { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
.vehicle-stock-photos-strip .photo-thumb {
  position: relative; width: 110px; height: 82px; border-radius: 8px; overflow: hidden;
  border: 1px solid var(--border); cursor: grab; transition: opacity 0.12s ease, box-shadow 0.12s ease;
  /* Sans ça, un clic-glisser réel (souris/trackpad) peut faire gagner la
     sélection de texte native du navigateur contre le dragstart HTML5 —
     rien ne bouge, juste un rectangle de sélection qui apparaît (retour
     utilisateur du 2026-08-20). */
  user-select: none;
}
/* Repère visuel discret pendant le drag : la vignette déplacée s'estompe,
   celle qu'on survole se surligne pour indiquer où elle va se poser. */
.vehicle-stock-photos-strip .photo-thumb.dragging { opacity: 0.35; cursor: grabbing; }
.vehicle-stock-photos-strip .photo-thumb img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  /* Une <img> est nativement "draggable" par le navigateur (glisser pour
     l'enregistrer/l'ouvrir) — sans ces deux lignes, ce comportement natif
     peut prendre le pas sur le dragstart du parent .photo-thumb selon le
     navigateur. pointer-events:none fait passer tout événement souris
     directement au parent ; -webkit-user-drag:none coupe la source de
     drag native de l'image elle-même, en clé de voûte. */
  pointer-events: none; -webkit-user-drag: none;
}
.vehicle-stock-photos-strip .photo-thumb button {
  position: absolute; top: 3px; right: 3px;
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; padding: 0; border-radius: 6px; border: none;
  background: rgba(15, 18, 26, 0.65); color: white; cursor: pointer;
  transition: background-color 0.12s ease;
}
.vehicle-stock-photos-strip .photo-thumb button:hover { background: rgba(15, 18, 26, 0.85); transform: none; }
.vehicle-stock-photos-strip .photo-thumb button svg { width: 13px; height: 13px; }
/* Carrousel de la fiche détail — grande image + vignettes cliquables,
   aucune dépendance externe (comme le reste de l'app). */
.vehicle-carousel { margin: 14px 0; }
.vehicle-carousel-main {
  position: relative; width: 100%; max-width: 560px; height: 380px;
  border-radius: 12px; overflow: hidden; background: var(--bg);
}
.vehicle-carousel-main img { width: 100%; height: 100%; object-fit: cover; display: block; }
.vehicle-carousel-placeholder {
  width: 100%; max-width: 560px; height: 380px; border-radius: 12px;
  background: var(--bg); color: var(--text-muted);
  display: flex; align-items: center; justify-content: center;
}
.vehicle-carousel-placeholder svg { width: 48px; height: 48px; }
.carousel-nav {
  position: absolute; top: 50%; transform: translateY(-50%); width: 36px; height: 36px;
  border-radius: 50%; background: rgba(15, 18, 26, 0.55); color: white; border: none;
  font-size: 20px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: background-color 0.12s ease;
}
.carousel-nav:hover { background: rgba(15, 18, 26, 0.8); filter: none; transform: translateY(-50%); }
.carousel-nav.prev { left: 10px; }
.carousel-nav.next { right: 10px; }
.vehicle-carousel-thumbs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; max-width: 560px; }
.carousel-thumb {
  width: 74px; height: 55px; object-fit: cover; border-radius: 8px; cursor: pointer;
  border: 2px solid transparent; opacity: 0.65; transition: opacity 0.12s ease, border-color 0.12s ease;
}
.carousel-thumb:hover { opacity: 0.9; }
.carousel-thumb.active { border-color: var(--primary); opacity: 1; }
.vehicle-carousel-main img { cursor: zoom-in; }

/* Variante réduite — fiche client (plusieurs véhicules affichés à la
   suite), contrairement à la fiche véhicule de stock qui a toute la page
   pour un seul véhicule. La lightbox plein écran reste identique dans les
   deux cas (voir .vehicle-lightbox), seul l'aperçu inline rétrécit. */
.vehicle-carousel.compact .vehicle-carousel-main { width: 220px; max-width: 220px; height: 150px; }
.vehicle-carousel.compact .vehicle-carousel-thumbs { max-width: 220px; }
.vehicle-carousel.compact .carousel-thumb { width: 44px; height: 33px; }
.vehicle-carousel.compact .carousel-nav { width: 28px; height: 28px; font-size: 16px; }

/* Vue plein écran (lightbox) du carrousel — même principe de navigation
   que le carrousel de la fiche, réutilise carouselIndex/CAROUSEL_URLS et
   les boutons .carousel-nav. */
.vehicle-lightbox {
  display: none; position: fixed; inset: 0; z-index: 1000;
  background: rgba(10, 12, 18, 0.92);
  align-items: center; justify-content: center;
}
.vehicle-lightbox.open { display: flex; }
.vehicle-lightbox-img-wrap { position: relative; display: flex; align-items: center; justify-content: center; }
.vehicle-lightbox-img-wrap img { max-width: 90vw; max-height: 85vh; object-fit: contain; border-radius: 8px; display: block; }
.vehicle-lightbox-close {
  position: absolute; top: 18px; right: 22px; width: 42px; height: 42px; border-radius: 50%;
  background: rgba(255, 255, 255, 0.12); color: white; border: none;
  font-size: 24px; line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: background-color 0.12s ease;
}
.vehicle-lightbox-close:hover { background: rgba(255, 255, 255, 0.25); }
/* Flèches ancrées aux bords de l'écran (pas de l'image, dont la largeur
   varie selon le format de la photo) — fond plus opaque + bordure que le
   carrousel normal, pour rester bien visibles sur n'importe quelle photo,
   claire ou sombre. */
.vehicle-lightbox-nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 1;
  width: 52px; height: 52px; border-radius: 50%;
  background: rgba(30, 32, 40, 0.65); border: 1px solid rgba(255, 255, 255, 0.35);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
  color: white; font-size: 26px; line-height: 1; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background-color 0.12s ease, transform 0.12s ease;
}
.vehicle-lightbox-nav:hover { background: rgba(30, 32, 40, 0.9); transform: translateY(-50%) scale(1.05); }
.vehicle-lightbox-nav.prev { left: 18px; }
.vehicle-lightbox-nav.next { right: 18px; }
@media (max-width: 640px) {
  .vehicle-lightbox-nav { width: 42px; height: 42px; font-size: 22px; }
  .vehicle-lightbox-nav.prev { left: 8px; }
  .vehicle-lightbox-nav.next { right: 8px; }
}

/* ---------------------------------------------------------------------
   Agenda — vue mois façon Google Agenda (voir appointments_list.html).
--------------------------------------------------------------------- */
/* Un SEUL grid pour l'en-tête des jours ET les cellules (2026-09-02) :
   deux grids `repeat(7, 1fr)` séparés (même template, mais calculés
   indépendamment par le moteur de rendu) peuvent dériver de quelques
   pixels l'un par rapport à l'autre selon le navigateur/le zoom — un
   décalage cumulatif de plus en plus visible colonne après colonne,
   remonté par l'utilisateur une fois l'agenda rempli de vrais rendez-vous
   et d'événements importés. Un grid unique élimine complètement cette
   classe de bug : les cellules d'en-tête sont juste les 7 premiers
   enfants, ancrées aux mêmes pistes de colonnes que tout le reste. */
.agenda-month-grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px;
}
.agenda-month-header-cell {
  text-align: center; font-size: 12px; font-weight: 700; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: 0.04em; padding-bottom: 2px;
}
.agenda-month-cell {
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px; min-height: 130px; display: flex; flex-direction: column; gap: 4px;
}
.agenda-month-cell.out-of-month { opacity: 0.45; }
.agenda-month-cell.is-today { border-color: var(--primary); border-width: 2px; }
.agenda-month-cell-num { font-size: 13px; font-weight: 700; }
.agenda-month-cell a.agenda-month-cell-num { color: inherit; text-decoration: none; }

.agenda-chip {
  display: block; background: var(--accent-soft, var(--bg)); color: var(--text);
  border-left: 3px solid var(--primary); border-radius: 4px; padding: 3px 7px;
  font-size: 12px; line-height: 1.4; text-decoration: none; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.agenda-chip:hover { filter: brightness(0.97); }
/* Événement importé d'un agenda externe (voir app/agenda_ics_import.py) :
   jamais cliquable (lecture seule, aucun id côté B-Facture) — bordure et
   curseur différents pour ne pas laisser croire qu'il s'ouvre comme un
   vrai rendez-vous. min-width:0 est nécessaire pour qu'un enfant flex
   accepte de rétrécir sous sa largeur naturelle : sans lui, le texte
   pousserait la puce (et potentiellement la cellule) plus large que sa
   colonne au lieu de se faire tronquer par l'ellipse du span interne. */
.agenda-chip-external {
  border-left-color: var(--text-muted); cursor: default;
  display: flex; align-items: center; gap: 4px; min-width: 0;
}
.agenda-chip-external:hover { filter: none; }
.agenda-chip-external .ico { width: 10px; height: 10px; flex-shrink: 0; opacity: 0.75; }
.agenda-chip-external-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.agenda-chip-more { font-size: 11.5px; color: var(--text-muted); padding: 2px 6px; cursor: pointer; }
/* Repli natif <details>/<summary> plutôt que du JS : "+N de plus" déplie
   les rendez-vous restants du jour directement dans la cellule (agrandit
   la ligne de la grille), sans page ni route supplémentaire. */
.agenda-month-more summary::-webkit-details-marker { display: none; }
.agenda-month-more summary { list-style: none; }
.agenda-month-more[open] summary { color: var(--text); font-weight: 600; }
.agenda-month-more .agenda-chip { margin-top: 4px; }

@media (max-width: 900px) {
  .agenda-month-grid { grid-template-columns: repeat(1, 1fr); }
  .agenda-month-header-cell { display: none; }
}

/* [P2] Vue d'aide : mêmes tuiles repliables que Paramètres/dashboard,
   en plus léger — chaque section garde son propre code couleur (posé sur
   son <h2> d'origine, jamais touché ici), seul le repli/dépli est ajouté.
   Pas de .config-section (qui impose sa propre structure summary-group/
   summary-text et son propre jeu de couleurs .accent-*) : ça aurait
   effacé la couleur distincte de chaque section au profit d'un jeu de
   couleurs générique sans rapport avec leur icône/thème. */
.help-section > summary {
  cursor: pointer; list-style: none; display: flex; align-items: center;
  justify-content: space-between; gap: 10px;
}
.help-section > summary::-webkit-details-marker { display: none; }
.help-section > summary h2 { margin: 0; flex: 1; }
.help-section > summary::after {
  content: "▾"; color: var(--text-muted); font-size: 15px; flex-shrink: 0;
  transition: transform 0.15s ease;
}
.help-section[open] > summary::after { transform: rotate(180deg); }
.help-section[open] > summary { margin-bottom: 14px; }

/* ---------------------------------------------------------------------------
   Affichage sur téléphone (2026-09-05)

   Devenu réellement actif ce jour-là : la balise <meta name="viewport"> qui
   manquait dans base.html rendait toutes les règles @media inertes sur
   mobile. Les blocs qui précèdent (menu latéral sous 900px, grilles du
   tableau de bord sous 720/460px...) reprennent donc vie du même coup.

   Priorité au PORTAIL CLIENT : c'est la seule page que les clients finaux
   ouvrent, et presque toujours depuis un lien reçu par email ou SMS, donc
   sur téléphone. Le reste de l'app est utilisé au bureau.
--------------------------------------------------------------------------- */
@media (max-width: 640px) {
  /* Récupérer de la largeur utile : 40px de marges + 44px de rembourrage de
     carte, sur un écran de 360px, c'est près du quart de l'espace. */
  main { margin: 16px auto; padding: 0 12px 40px; }
  .card { padding: 16px; border-radius: 12px; }
  h1 { font-size: 20px; }

  /* Le titre du document et sa pastille de statut sont côte à côte en
     `space-between` : sous 640px ils se chevauchaient. */
  .portal-doc-head { flex-wrap: wrap; gap: 10px; }

  /* En-tête d'entreprise : logo + nom + adresse sur une ligne. Le nom passe
     sous le logo plutôt que d'être écrasé à quelques caractères. */
  .portal-header { flex-wrap: wrap; }

  /* Cible tactile pleine largeur — c'est l'action principale de la page. */
  .portal-pdf-btn { display: flex; width: 100%; justify-content: center; }

  /* Le bloc de totaux est aligné à droite sur 320px : sur un écran étroit,
     il vaut mieux qu'il occupe toute la largeur, les montants restent
     alignés à droite entre eux. */
  .totals-summary { max-width: none; }

  /* Le tableau des lignes garde son overflow-x:auto (voir portal_view.html)
     comme filet, mais on lui évite d'y recourir dans la plupart des cas. */
  /* Deux colonnes fixes sans regle mobile jusqu'ici : sur le portail d'un
     ORDRE DE REPARATION, la ligne "Vehicule" (marque + modele + plaque) se
     retrouvait ecrasee dans ~140px. Utilise aussi par les fiches vehicule
     et client de l'app, qui y gagnent pareillement. */
  .info-grid { grid-template-columns: 1fr; gap: 8px; }

  .portal-card .table-airy { font-size: 13px; }
  .portal-card .table-airy th, .portal-card .table-airy td { padding-left: 6px; padding-right: 6px; }
}
