/* Geist — self-hosted (CSP: font-src falls back to default-src 'self', so no
   external Google Fonts). Single variable file per subset; the wght axis (100..900)
   covers every weight token. Latin covers French (accents, œ, €); latin-ext is a
   safety net for rarer glyphs. */
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 100 900; font-display: swap;
  src: url('/static/fonts/geist-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
@font-face { font-family: 'Geist'; font-style: normal; font-weight: 100 900; font-display: swap;
  src: url('/static/fonts/geist-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }

:root {
  /* Design foundation: ../CLAUDE_DESIGN (shadcn/ui — white canvas, hairline
     borders, graphite neutrals, Geist type, compact, border-first/flat, no elevation).
     The neutrals, type, spacing and border-first philosophy follow that foundation; the
     one departure is the BRAND: a violet→magenta GRADIENT signature « Orchidée » (--brand-grad)
     carries the single high-contrast filled action (primary button, active nav item,
     logo), with the solid magenta --brand for accents (focus ring, active tab, links,
     control borders) and --brand-wash for tinted micro-surfaces. Only the functional
     status palette below (six semantic kinds) carries the OTHER chromatic colour, and
     never for actions. */
  /* ---- Precise colour code -------------------------------------------------
     Every coloured element (pastille/badge, flash banner, toast, stock alert)
     maps to ONE of six semantic kinds. Each kind = a solid ink + a soft wash:
       neutral  grey    --muted / --chip        draft, neutral, unknown
       info     blue    --info / --info-wash     sent, informational
       ok       green   --ok   / --ok-wash       success, accepted, paid, inflow
       warn     amber   --warn / --warn-wash     warning, adjustment
       bad      red     --bad  / --bad-wash      error, refused, cancelled, outflow
       accent   purple  --accent / --accent-wash invoiced
     The brand ink (--brand) is reserved for primary actions, never status. */
  --bg: #ffffff;
  --panel: #ffffff;
  --ink: #0a0a0a;
  --muted: #737373;
  --line: #e5e5e5;
  /* ===== BRAND « Orchidée » — signature couleur de tout le site ===============
     Point de bascule unique : ces tokens (jusqu'à --brand-wash) portent toute
     l'identité de marque ; en changer les valeurs reskinne l'app entière. Deux
     palettes alternatives (Améthyste, Crépuscule) vivent en blocs `html[data-theme=…]`
     juste après ce :root, et le thème actif se choisit d'un mot dans src/html.js
     (constante THEME → attribut data-theme). Les trois signatures sont présentées
     côte à côte sur /design.
     « Orchidée » : la marque est un DÉGRADÉ chaud violet → magenta (la signature du
     projet). --brand-grad = le dégradé, porté par les surfaces pleines (bouton primaire,
     item de nav actif, logo). --brand = le magenta plein équivalent pour tout usage solide
     (bordures de contrôle, focus, onglet actif, liens via --brand-d). --focus-ring = le
     halo de focus. (Le dégradé ne peut pas vivre dans --brand : une variable couleur ne
     se met pas dans un border/texte, d'où le couple plein + dégradé.) */
  --brand: #a21caf;
  --brand-d: #86198f;
  /* Les deux extrémités du dégradé Orchidée, disponibles en plein : --brand-deep
     (violet, début du dégradé) sert d'encre forte pour les chiffres clés et icônes
     de section ; --brand-light (magenta clair, fin du dégradé) pour les accents doux.
     Le dégradé reste la signature des surfaces pleines + des icônes/valeurs
     « brandées » (mask/background-clip), ce qui fait vivre les trois couleurs. */
  --brand-deep: #7c3aed;
  --brand-light: #e040c8;
  /* Le dégradé Orchidée, décomposé en deux tokens (source de vérité unique, partagée
     par les 3 thèmes) : --grad-angle = l'orientation de repos, --grad-stops = les trois
     arrêts de couleur. --brand-grad les recompose pour toutes les surfaces pleines
     (logo, icônes, clip-texte). Les surfaces ANIMÉES (bouton primaire, lame de nav)
     ré-assemblent le même dégradé en ajoutant --grad-rot à l'angle → rotation. */
  --grad-angle: 130deg;
  --grad-stops: #7c3aed 0%, #a21caf 50%, #e040c8 100%;
  --brand-grad: linear-gradient(var(--grad-angle), var(--grad-stops));
  /* --sheen = la bande de lumière douce qui balaie les surfaces à dégradé (bouton
     primaire, onglet de nav actif) pour leur donner du relief. Posée en 1ʳᵉ couche
     background AU-DESSUS du dégradé, animée par @keyframes brand-sheen. */
  --sheen: linear-gradient(110deg, transparent 38%, rgba(255,255,255,.45) 50%, transparent 62%);
  /* --grad-rot = l'angle de rotation ajouté à --grad-angle SUR les surfaces animées.
     Référencé EN DIRECT dans leur linear-gradient (pas via --brand-grad : un registered
     property animé enterré dans un autre custom property ne repeint pas de façon fiable).
     Enregistré en <angle> (cf. @property plus bas) pour une interpolation continue ;
     piloté par @keyframes brand-rotate. Au repos / reduced-motion il vaut 0deg → angle
     de base inchangé. */
  --grad-rot: 0deg;
  /* Couleur des liens texte : un magenta volontairement plus doux/moins saturé que le
     --brand-d, pour que les liens (très présents dans les tableaux) ne crient pas ;
     ils se renforcent au survol (--brand-d). */
  --link: #9d3aa6;
  --focus-ring: rgba(162, 28, 175, .20);
  /* Sidebar : surface graphite sombre neutre (« tout public » — l'ardoise ne tire ni vers
     le rose ni vers une couleur de marque), filet hairline clair translucide, item actif au
     dégradé de marque (la « lame » sheen+rotation), hover = voile clair. Le wordmark passe
     en blanc (logo-white.svg via html.js logo({light})), le glyph reste au dégradé Orchidée
     et le libellé + les icônes de nav sont en GRIS CLAIR (--sidebar-ink pour le texte,
     --sidebar-ic un cran plus doux pour les glyphes — le traitement « graphite sombre » du
     showcase /design/menu, blanc sur l'item actif) → marque lisible sur le menu sombre. */
  --sidebar: #17171b;
  --sidebar-ink: #c6c6cd;
  --sidebar-ic: #b6b6c0;
  --sidebar-line: rgba(255,255,255,.09);
  --sidebar-hover: rgba(255,255,255,.07);
  --sidebar-group: #7c7c86;
  --ok: #1f9d57;
  --ok-d: #137a3f;
  --ok-wash: #e4f6ec;
  --warn: #d98a00;
  --warn-d: #9a6300;
  --bad: #d23b3b;
  --bad-d: #b62a2a;
  --bad-wash: #fbe7e7;
  --info: #2566b8;
  --info-wash: #e6f0fb;
  --accent: #6b3fc0;
  --accent-wash: #ede4fb;
  --ink-2: #171717;
  --chip: #f2f2f2;
  --row-hover: #fafafa;
  /* Soft magenta wash for brand-tinted micro-surfaces (tile/combobox/chip hover,
     bulk-selected row) — the Orchidée camaïeu. */
  --brand-wash: #fbe8fb;
  /* Soft amber wash for the warn kind (was sharing the old orange brand-wash). */
  --warn-wash: #fdf3e2;
  /* Pure white for ink/fills sitting on a coloured surface (button label, sidebar
     text, badge ink) — one token so a future theme can repoint it. */
  --white: #fff;
  /* Soft red border of the danger button (between --bad and --bad-wash). */
  --bad-line: #f0c9c9;
  --radius: 10px;     /* inputs, buttons, nested panels */
  --radius-card: 14px; /* cards, tables, modal, auth box */
  /* Border-first/flat: cards & tables separate via the 1px hairline, no elevation.
     Only true overlays (modal / toast / combobox popup) keep a soft shadow. */
  --shadow: none;
  --shadow-pop: 0 6px 24px rgba(20,25,40,.16), 0 2px 6px rgba(20,25,40,.10);
  /* ---- Typography ----------------------------------------------------------
     One font family, one line-height, one size scale, one weight scale for the
     whole app. Never hardcode a font/size/weight in a rule — reference a token.
     The scale is named by its px value (1:1) so intent stays obvious. */
  --font-sans: 'Geist', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --lh: 1.5;
  --fs-10: 10px; --fs-11: 11px; --fs-12: 12px; --fs-13: 13px; --fs-14: 14px;
  --fs-15: 15px; /* body base */
  --fs-17: 17px; --fs-18: 18px; --fs-19: 19px; --fs-20: 20px; --fs-22: 22px; --fs-24: 24px; --fs-26: 26px;
  --fw-normal: 400; --fw-medium: 500; --fw-semibold: 600; --fw-bold: 700; --fw-heavy: 800;
  /* Form field widths — each input declares a content-appropriate size via a
     .field-<size> class (a weight needs far less room than an address). */
  --field-xs: 96px; --field-sm: 150px; --field-md: 230px; --field-lg: 330px; --field-xl: 460px;
  /* One control height for every interactive box — text inputs, selects, native date
     pickers and buttons — so any row of them lines up to the pixel. Native date inputs
     have a taller intrinsic box than text inputs; a shared fixed height (with
     box-sizing: border-box) tames them to the same line. --control-h-sm = the dense
     variant (btn-sm, list-filter controls). */
  --control-h: 34px; --control-h-sm: 28px;
  /* Max width of a data-entry column: a sectioned entity form reads as a column, not a
     band stretched across the whole content area (the « vide à droite » fix). Wide enough
     for the densest section (the supplier-offer table: fournisseur+réf+prix+cond+délai+
     défaut on one line) without sprawling like the old uncapped ~1150px. */
  --form-col: 960px;
  /* Narrower cap for a short inline form (a couple of fields): the card hugs its
     reading column instead of stretching the whole content band into a sparse box
     (the « Nouveau devis » empty-looking page). */
  --form-col-narrow: 560px;
  /* Zone/section titles (card <h3>, form-section legends): quiet graphite subheads
     in sentence case — no chromatic accent, no uppercase. Each area reads as its own
     block through weight and the hairline grid, not colour. (Nav groups use the
     lighter --sidebar-group; table headers keep their own muted uppercase.) */
  --title-zone: var(--ink);
  /* Author pastille palette — 8 deterministic hues (filled, white text). */
  --av-0: #2566b8; --av-1: #1f9d57; --av-2: #6b3fc0; --av-3: #d98a00;
  --av-4: #c0398b; --av-5: #0f8a8a; --av-6: #d23b3b; --av-7: #4a5568;
}

/* ===== Palettes de marque alternatives ======================================
   Orchidée (le défaut, dans :root ci-dessus) + ces deux signatures = les trois
   directions présentées sur /design. Le thème actif se pose via data-theme sur
   <html> (src/html.js, constante THEME) ; sans attribut, :root reste Orchidée.
   Chaque bloc redéclare exactement les mêmes tokens de marque que :root — basculer
   tout le site = changer le mot dans THEME. */
html[data-theme="amethyste"] {
  --brand: #7c3aed; --brand-d: #6d28d9;
  --brand-deep: #4c1d95; --brand-light: #a855f7;
  --grad-angle: 135deg; --grad-stops: #4c1d95 0%, #7c3aed 52%, #a855f7 100%;
  --link: #7458c0; --focus-ring: rgba(124, 58, 237, .22);
  /* La sidebar reste graphite (neutre) quel que soit le thème ; seule la lame active
     suit la marque. Donc pas d'override --sidebar* ici, juste le wash. */
  --brand-wash: #f1eafe;
}
html[data-theme="crepuscule"] {
  --brand: #5b50e6; --brand-d: #4338ca;
  --brand-deep: #312e81; --brand-light: #818cf8;
  --grad-angle: 150deg; --grad-stops: #312e81 0%, #6d28d9 48%, #818cf8 100%;
  --link: #5d54b8; --focus-ring: rgba(91, 80, 230, .22);
  --brand-wash: #eaeafe;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font: var(--fs-15)/var(--lh) var(--font-sans);
  color: var(--ink); background: var(--bg);
  display: flex; min-height: 100vh;
}
body.centered { display: flex; align-items: center; justify-content: center; }
a { color: var(--link); text-decoration: none; }
a:hover { color: var(--brand-d); text-decoration: underline; }
/* Display gravity (shadcn/Geist): headings are semibold with tight tracking; the
   page h1 is the one moment of typographic weight. Sentence case, graphite — never
   uppercase, never a brand colour. */
h1 { font-size: var(--fs-26); font-weight: var(--fw-semibold); letter-spacing: -.025em; margin: 0; }
h2 { font-size: var(--fs-19); font-weight: var(--fw-semibold); letter-spacing: -.02em; }
h3 { font-size: var(--fs-15); font-weight: var(--fw-semibold); letter-spacing: -.01em; color: var(--title-zone); margin: 0 0 9px; }
code { background: var(--chip); padding: 1px 5px; border-radius: 4px; font-size: var(--fs-13); }
code.hash { word-break: break-all; }
.muted { color: var(--muted); }
.hidden { display: none !important; }
.neg { color: var(--bad); font-weight: var(--fw-semibold); }
.pos { color: var(--ok); font-weight: var(--fw-semibold); }
.num, .right, .r { text-align: right; }
.num-col { text-align: right; font-variant-numeric: tabular-nums; }
.date-col { text-align: center; }
.center-col { text-align: center; font-variant-numeric: tabular-nums; }

/* breadcrumb on deep pages */
.crumbs { display: flex; gap: 7px; align-items: center; font-size: var(--fs-13); color: var(--muted); margin-bottom: 8px; flex-wrap: wrap; }
.crumbs a { color: var(--muted); }
.crumbs a:hover { color: var(--brand-d); }
.crumbs .sep { opacity: .5; }

/* sidebar */
.sidebar {
  width: 230px; background: var(--sidebar); color: var(--sidebar-ink);
  display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh; flex-shrink: 0;
  border-right: 1px solid var(--sidebar-line);
}
/* Brand lockup = <img> of the SVG logo (helper `logo()` in html.js). `.brand-logo`
   is the full wordmark (sidebar expanded + auth screens); `.brand-mark` is the
   glyph badge, shown only on the collapsed icon-rail (toggled in the 820px query). */
.brand { display: flex; align-items: center; gap: 9px; padding: 18px 18px; }
.brand-logo { height: 30px; width: auto; display: block; }
.brand-mark { height: 30px; width: 30px; display: none; }
.brand.big { justify-content: center; margin-bottom: 6px; }
.brand.big .brand-logo { height: 40px; }
/* Active company (multi-tenant): the silo this session is in, under the brand. */
.company { padding: 0 18px 8px; display: flex; align-items: baseline; gap: 8px; }
.company-name { color: var(--white); font-size: var(--fs-13); font-weight: var(--fw-bold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.company-switch { color: var(--sidebar-ink); font-size: var(--fs-11); text-decoration: none; white-space: nowrap; }
.company-switch:hover { color: var(--white); }
.global-search { padding: 0 10px 8px; }
.global-search input {
  width: 100%; background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.13);
  color: var(--white); border-radius: 8px; padding: 7px 10px; font-size: var(--fs-13);
}
.global-search input::placeholder { color: var(--sidebar-group); opacity: 1; }
.global-search input:focus { outline: none; border-color: var(--brand-light); background: rgba(255,255,255,.10); }
.search-more { margin: 8px 2px 0; font-size: var(--fs-13); }
.sidebar nav { display: flex; flex-direction: column; padding: 4px 10px; gap: 1px; overflow-y: auto; flex: 1; }
.nav-group { font-size: var(--fs-11); font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: .07em; color: var(--sidebar-group); padding: 6px 12px 2px; }
.nav-item { position: relative; display: flex; align-items: center; gap: 10px; padding: 2px 12px; border-radius: 8px; color: var(--sidebar-ink); font-size: var(--fs-15); font-weight: var(--fw-normal); transition: background .12s, color .12s; }
.nav-item:hover { background: var(--sidebar-hover); text-decoration: none; color: var(--white); }
/* Onglet sélectionné (la « lame » de marque) : même mécanique sheen que le bouton
   primaire, cadence légèrement décalée (8s) pour éviter un flash synchrone du chrome. */
.nav-item.active { background-image: var(--sheen), linear-gradient(calc(var(--grad-angle) + var(--grad-rot)), var(--grad-stops));
  background-origin: border-box; background-repeat: no-repeat;
  background-size: 220% 100%, 100% 100%; background-position: -160% 0, 0% 50%;
  color: var(--white); animation: brand-sheen 8s ease-in-out infinite, brand-rotate 14s linear infinite; }
.nav-ic { width: 22px; text-align: center; }
/* Glyphes de nav : gris clair (--sidebar-ic, le traitement « graphite sombre » du
   showcase), blanc sur l'item actif (fond dégradé). */
.nav-ic-img { width: 22px; height: 22px; }
/* Descendant selector (0,2,0) pour battre `.ic { background-color: currentColor }` :
   une couleur pleine, contrairement au dégradé (background-image), serait sinon écrasée
   par le currentColor de base. */
.nav-item .nav-ic-img { background-color: var(--sidebar-ic); }
.nav-item.active .nav-ic-img { background-color: var(--white); }

/* Icônes (source unique : html.js ICONS, rendues par icon()). La silhouette (SVG blanc,
   deux glyphes de statut en PNG) sert de MASQUE ; la couleur vient de `background`. `.ic` = base (masque + couleur =
   currentColor par défaut, donc l'icône suit la couleur du texte qui l'entoure) ;
   `.ic-f-<basename>` porte la source du masque (une par fichier) ; les tailles
   dépendent du contexte (nav ci-dessus, tuile, section, inline). */
.ic {
  display: inline-block; vertical-align: middle; width: 1em; height: 1em;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
  -webkit-mask-size: contain; mask-size: contain;
}
.ic-inline { width: 1em; height: 1em; vertical-align: -0.14em; }
/* Tuiles (chiffres clés) : icône remplie au dégradé Orchidée — fait vivre les trois
   couleurs de la marque. */
.tile-ic .ic { width: 34px; height: 34px; background: var(--brand-grad); }
/* Légendes de section de formulaire : magenta de marque plein (accent calme), un cran
   plus grande depuis que chaque section est une carte (ancre visuelle de l'en-tête). */
.form-section .sec-ic .ic { width: 18px; height: 18px; vertical-align: -0.22em; background-color: var(--brand); }
/* Source du masque, une règle par fichier d'icône (servi same-origin → CSP OK). */
.ic-f-tableau-de-bord  { -webkit-mask-image: url(/static/icons/tableau-de-bord.svg);  mask-image: url(/static/icons/tableau-de-bord.svg); }
.ic-f-statistiques     { -webkit-mask-image: url(/static/icons/statistiques.svg);     mask-image: url(/static/icons/statistiques.svg); }
.ic-f-catalogue        { -webkit-mask-image: url(/static/icons/catalogue.svg);        mask-image: url(/static/icons/catalogue.svg); }
.ic-f-mouvements       { -webkit-mask-image: url(/static/icons/mouvements.svg);       mask-image: url(/static/icons/mouvements.svg); }
.ic-f-fournisseur      { -webkit-mask-image: url(/static/icons/fournisseur.svg);      mask-image: url(/static/icons/fournisseur.svg); }
.ic-f-commandes        { -webkit-mask-image: url(/static/icons/commandes.svg);        mask-image: url(/static/icons/commandes.svg); }
.ic-f-factures-recues  { -webkit-mask-image: url(/static/icons/factures-recues.svg);  mask-image: url(/static/icons/factures-recues.svg); }
.ic-f-clients          { -webkit-mask-image: url(/static/icons/clients.svg);          mask-image: url(/static/icons/clients.svg); }
.ic-f-devis            { -webkit-mask-image: url(/static/icons/devis.svg);            mask-image: url(/static/icons/devis.svg); }
.ic-f-facture          { -webkit-mask-image: url(/static/icons/facture.svg);          mask-image: url(/static/icons/facture.svg); }
.ic-f-caisse           { -webkit-mask-image: url(/static/icons/caisse.svg);           mask-image: url(/static/icons/caisse.svg); }
.ic-f-banque           { -webkit-mask-image: url(/static/icons/banque.svg);           mask-image: url(/static/icons/banque.svg); }
.ic-f-export           { -webkit-mask-image: url(/static/icons/export.svg);           mask-image: url(/static/icons/export.svg); }
.ic-f-livraison        { -webkit-mask-image: url(/static/icons/livraison.svg);        mask-image: url(/static/icons/livraison.svg); }
.ic-f-reglages         { -webkit-mask-image: url(/static/icons/reglages.svg);         mask-image: url(/static/icons/reglages.svg); }
.ic-f-utilisateurs     { -webkit-mask-image: url(/static/icons/utilisateurs.svg);     mask-image: url(/static/icons/utilisateurs.svg); }
.ic-f-roles            { -webkit-mask-image: url(/static/icons/roles.svg);            mask-image: url(/static/icons/roles.svg); }
.ic-f-regles-globales  { -webkit-mask-image: url(/static/icons/regles-globales.svg);  mask-image: url(/static/icons/regles-globales.svg); }
.ic-f-historique       { -webkit-mask-image: url(/static/icons/historique.svg);       mask-image: url(/static/icons/historique.svg); }
.ic-f-prix             { -webkit-mask-image: url(/static/icons/prix.svg);             mask-image: url(/static/icons/prix.svg); }
.ic-f-sablier          { -webkit-mask-image: url(/static/icons/sablier.svg);          mask-image: url(/static/icons/sablier.svg); }
.ic-f-calendrier       { -webkit-mask-image: url(/static/icons/calendrier.svg);       mask-image: url(/static/icons/calendrier.svg); }
.ic-f-triangle_attention{ -webkit-mask-image: url(/static/icons/triangle_attention.png); mask-image: url(/static/icons/triangle_attention.png); }
.ic-f-coche_validation { -webkit-mask-image: url(/static/icons/coche_validation.png); mask-image: url(/static/icons/coche_validation.png); }
.nav-badge { position: absolute; top: 50%; right: 8px; transform: translateY(-50%); background: var(--bad); color: var(--white); font-size: var(--fs-11); font-weight: var(--fw-bold); line-height: 1; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; display: inline-flex; align-items: center; justify-content: center; }
.nav-badge-warn { background: var(--warn); }
/* Stock movement direction dot (ui.moveDir): green inflow / red outflow / grey
   adjustment — a loud cue prefixing the typed reason in the movement logs. */
.move-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 6px; vertical-align: 0.04em; }
.move-dot-in { background: var(--ok); }
.move-dot-out { background: var(--bad); }
.move-dot-adjust { background: var(--muted); }
/* Inline amber count pill flagging an article with missing price data. */
.alert-pill { display: inline-flex; align-items: center; justify-content: center; min-width: 18px; height: 18px; margin-left: 6px; padding: 0 5px; border-radius: 9px; background: var(--warn); color: var(--white); font-size: var(--fs-11); font-weight: var(--fw-bold); line-height: 1; vertical-align: middle; }
.alert-pill-block { background: var(--bad); }
.alert-pill-link { text-decoration: none; cursor: pointer; }
.alert-pill-link:hover { color: var(--white); filter: brightness(1.1); }
/* A pill on a column's alignment edge shoves the value off the shared edge and
   breaks row-to-row alignment. Global rule: the pill sits on the side OPPOSITE
   the column's text-alignment. Left/text columns: it trails (default margin-left).
   Right/numeric columns: the value + pill ride a reversed inline-flex wrapper
   (.num-pill, emitted by ui.table) so the pill leads and the figures stay
   flush-right. The wrapper — not the <td> — is the flex box, so the cell stays a
   real table-cell and its bottom border keeps aligning with the row's separator
   (a flex <td> drops out of table-cell layout and offsets the line). */
.num-pill { display: inline-flex; flex-direction: row-reverse; align-items: center; gap: 6px; }
.num-pill .alert-pill { margin: 0; }
.sidebar-foot { padding: 14px 16px; border-top: 1px solid var(--sidebar-line); }
.who { color: var(--white); font-size: var(--fs-14); display: flex; flex-direction: column; text-decoration: none; }
.who:hover { color: var(--white); opacity: .7; }
.who small { color: var(--sidebar-group); text-transform: uppercase; font-size: var(--fs-11); letter-spacing: .05em; }
.logout-form { margin: 0; }
/* Logout is a POST form (a GET link is CSRF-forceable); the button keeps the old
   link look via a reset so the sidebar is visually unchanged. */
.logout { color: var(--sidebar-ink); font-size: var(--fs-13); background: none; border: 0; padding: 0; cursor: pointer; font-family: inherit; }
.logout:hover { text-decoration: underline; }

/* content */
.content { flex: 1; min-width: 0; padding: clamp(18px, 2.2vw, 36px) clamp(18px, 3vw, 52px); }
/* Title and action buttons share the top row: title left, actions pinned
   top-right on the title baseline. Actions wrap below on narrow screens. */
.page-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px; margin-bottom: 14px; }
.page-head .actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: flex-end; margin-left: auto; }
.page-head .actions form { margin: 0; }

/* cards */
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 14px 18px; box-shadow: var(--shadow); margin-bottom: 14px; }
/* Paired cards stretch to a shared row height: a shorter card fills its cell
   instead of leaving page background beside the taller one (no empty zone). */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: stretch; margin-bottom: 14px; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; align-items: stretch; margin-bottom: 14px; }
.grid-2 > .card, .grid-3 > .card { margin-bottom: 0; height: 100%; }
@media (max-width: 820px) {
  .grid-2, .grid-3, .detail-head { grid-template-columns: 1fr; }
  .content { padding: 18px; }
  .sidebar { width: 62px; }
  .brand { justify-content: center; padding: 18px 0; }
  .nav-item { justify-content: center; }
  .nav-label, .nav-group, .who, .logout, .global-search, .company { display: none; }
  /* collapsed icon-rail: swap the wordmark for the glyph badge — sidebar ONLY, so
     the centred auth-screen lockup (.brand.big, outside .sidebar) is untouched. */
  .sidebar .brand-logo { display: none; }
  .sidebar .brand-mark { display: block; }
  /* icons-only rail: badge becomes a corner dot over the icon (not vertically
     centred, which is the desktop label case). */
  .nav-badge { top: 2px; right: 6px; transform: none; min-width: 16px; height: 16px; font-size: var(--fs-10); }
  .link-btn { width: 32px; height: 32px; }
}

/* quick actions (dashboard) */
.quick-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }

/* tiles */
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; margin-bottom: 18px; }
.tile { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 18px 16px; box-shadow: var(--shadow); display: flex; flex-direction: column; align-items: center; text-align: center; gap: 8px; color: var(--ink); transition: border-color .15s, background .15s; }
.tile:hover { text-decoration: none; border-color: var(--brand); background: var(--brand-wash); }
.tile-ic { line-height: 1; flex-shrink: 0; }
.tile-body { min-width: 0; max-width: 100%; }
/* Chiffre clé : grande icône dégradée au-dessus, valeur au dégradé (background-clip
   text) puis libellé, le tout centré — la valeur « parle » en couleur de marque. */
.tile-val { font-size: var(--fs-26); font-weight: var(--fw-bold); letter-spacing: -.01em; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  background: var(--brand-grad); -webkit-background-clip: text; background-clip: text; color: transparent; -webkit-text-fill-color: transparent; }
.tile-lbl { color: var(--muted); font-size: var(--fs-13); margin-top: 4px; }
/* Sub-line under the label: the quiet qualifier of an amount KPI (« 2 devis »). */
.tile-sub { color: var(--muted); font-size: var(--fs-12); font-weight: var(--fw-medium); margin-top: 3px; }
.tile.warn { border-color: var(--warn); background: var(--warn-wash); }
/* Tuile d'alerte : la sémantique ambre prime sur la marque (valeur ET icône). */
.tile.warn .tile-val { background: none; color: var(--warn); -webkit-text-fill-color: var(--warn); }
.tile.warn .tile-ic .ic { background: var(--warn); }
/* Tuile « mauvaise » (retard / impayé en souffrance) : rouge sémantique, même priorité
   sur la marque que l'ambre. */
.tile.bad { border-color: var(--bad); background: var(--bad-wash); }
.tile.bad .tile-val { background: none; color: var(--bad); -webkit-text-fill-color: var(--bad); }
.tile.bad .tile-ic .ic { background: var(--bad); }
/* Tuile « bonne » (objectif atteint : 100 % rapproché…) : vert sémantique, même
   priorité sur la marque — couleur = statut, pas décor. */
.tile.ok { border-color: var(--ok); background: var(--ok-wash); }
.tile.ok .tile-val { background: none; color: var(--ok-d); -webkit-text-fill-color: var(--ok-d); }
.tile.ok .tile-ic .ic { background: var(--ok); }

/* dense key/value grid — multi-column facts on detail pages (reusable) */
.kv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px 20px; }
.kv-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kv-cell .kv-k { font-size: var(--fs-12); color: var(--muted); }
.kv-cell .kv-v { font-size: var(--fs-15); font-weight: var(--fw-semibold); word-break: break-word; }
/* Editable docHead fields (regime/validity): own deterministic 2-up row so each
   field's input + Enregistrer button stay side by side instead of wrapping in the
   auto-fit kv-grid. */
.kv-edit-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 20px; margin-top: 12px; }
.kv-edit-row .inline-date { flex-wrap: nowrap; }

/* tables */
.table-wrap { overflow-x: auto; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card); box-shadow: var(--shadow); }
.table-wrap table { margin: 0; }
table { width: 100%; border-collapse: collapse; }
th { text-align: left; font-size: var(--fs-12); text-transform: uppercase; letter-spacing: .03em; color: var(--muted); padding: 5px 13px; border-bottom: 2px solid var(--line); background: var(--panel); }
/* Standard data-table alignment. CELLS: text columns read left, numbers right
   (tabular figures), dates and pure-% centered. HEADERS: every column title is centred
   (rule just below), whatever its cell alignment. The default here is wrapped in
   :where() (specificity 0,0,1 — element only) so the nature class ui.table puts on BOTH
   th and td wins on its own: .num-col → right (line 141), .date-col / .center-col →
   center, plus the manual .num / .right utilities of hand-written rows. Scoped to real
   column grids (.table-wrap, table.mini); the label→value blocks table.kv /
   table.totals keep their own base alignment. .td-actions (right) and .bulk-cell
   (center) keep their single-class win on the cells. */
:where(.table-wrap, table.mini) th, :where(.table-wrap, table.mini) td { text-align: left; }
/* Column titles are always centred — beats every nature class on the header (0,1,1 >
   0,1,0), so a new column centres its header with zero per-call work while its cells
   keep the nature alignment above. Scoped like the default; table.kv row-labels (left)
   are untouched. */
.table-wrap th, table.mini th { text-align: center; }
td { padding: 4px 13px; border-bottom: 1px solid var(--line); line-height: 1.35; }
.table-wrap tbody tr:last-child td { border-bottom: 0; }
.card .table-wrap { border: 0; box-shadow: none; border-radius: 0; background: transparent; }
tbody tr { transition: background .12s; }
tbody tr:hover { background: var(--row-hover); }
tbody tr[data-href] { cursor: pointer; }
/* Sortable headers (ui.table → app.js [data-sort]): discreet hover affordance +
   a direction arrow on the active column. The neutral ↕ hint shows only on
   hover/focus so resting headers stay clean; the active column keeps a solid
   ▲/▼. The reserved indicator width avoids any layout shift between states. */
th.th-sort { cursor: pointer; user-select: none; white-space: nowrap; transition: background .12s, color .12s; }
th.th-sort:hover, th.th-sort:focus-visible { background: var(--chip); color: var(--ink-2); }
th.th-sort:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.sort-ind { display: inline-block; width: 1em; margin-left: 2px; font-size: var(--fs-10); text-align: center; vertical-align: middle; }
/* Phantom mirror of .sort-ind on the left: every header is centred now, so the
   (label + trailing indicator) block centres and shifts the visible label left by half
   the arrow's footprint. An equal-footprint spacer on the left re-centres the label
   itself — applied to every sortable header. */
th.th-sort::before { content: ''; display: inline-block; width: 1em; margin-right: 2px; font-size: var(--fs-10); vertical-align: middle; }
.sort-ind::after { content: '↕'; opacity: 0; transition: opacity .12s; }
th.th-sort:hover .sort-ind::after, th.th-sort:focus-visible .sort-ind::after { opacity: .4; }
th[aria-sort="ascending"] .sort-ind::after { content: '▲'; opacity: 1; }
th[aria-sort="descending"] .sort-ind::after { content: '▼'; opacity: 1; }
table.mini td, table.mini th { padding: 6px 8px; font-size: var(--fs-13); }
table.mini { border: 0; }

/* Bulk selection (ui.bulkList → app.js [data-bulk]): a mailbox-style list whose
   rows can be checkbox-selected to run a grouped action. The « Sélectionner » toggle
   rides the filter row; the checkbox column and the tools strip are dormant until
   it adds .bulk-on, so the inactive list costs no extra line. */
.bulk-tools { display: none; align-items: center; gap: 10px; flex-wrap: wrap; }
.bulk.bulk-on .bulk-tools { display: flex; margin-bottom: 12px; }
.bulk-count { font-size: var(--fs-13); color: var(--muted); min-width: 8ch; }
.bulk-picks { display: flex; gap: 6px; flex-wrap: wrap; }
.chip { padding: 4px 11px; border: 1px solid var(--line); border-radius: 999px; background: var(--panel);
  color: var(--ink-2); font: inherit; font-size: var(--fs-13); cursor: pointer; transition: background .12s, color .12s; }
.chip:hover { background: var(--chip); color: var(--ink); }
.bulk-cell { display: none; width: 1px; white-space: nowrap; text-align: center; }
.bulk.bulk-on .bulk-cell { display: table-cell; }
.bulk.bulk-on tbody tr:has(.bulk-cb:checked) { background: var(--brand-wash); }
.bulk.bulk-on tbody tr[data-href] { cursor: pointer; }
table.kv th { width: 160px; text-transform: none; font-size: var(--fs-13); color: var(--muted); border: 0; font-weight: var(--fw-medium); vertical-align: top; }
table.kv td { border: 0; }
table.totals { width: auto; margin-left: auto; margin-top: 14px; min-width: 280px; }
table.totals th, table.totals td { padding: 3px 14px; }
table.totals th { text-transform: none; border: 0; color: var(--ink); font-weight: var(--fw-medium); }
table.totals td { border: 0; text-align: right; font-variant-numeric: tabular-nums; }
table.totals tr.grand th, table.totals tr.grand td { font-size: var(--fs-17); font-weight: var(--fw-bold); border-top: 2px solid var(--line); padding-top: 8px; }
/* A grand row that is the whole total (no preceding line, e.g. an order's « Total
   HT ») has nothing to separate from — drop its top border/padding. */
table.totals tr.grand:first-child th, table.totals tr.grand:first-child td { border-top: 0; padding-top: 3px; }
/* Internal margin row: dashed separator marks it as a display-only artifact, never on the PDF. */
table.totals tr.margin-row th, table.totals tr.margin-row td { border-top: 1px dashed var(--line); padding-top: 8px; }
table.totals tr.margin-row th { color: var(--muted); }
/* In-place draft line edit: inputs bound (via the HTML form attribute) to the
   edit form in the actions cell; fill their column, no field-label chrome. */
.line-edit-input { width: 100%; min-width: 56px; margin: 0; }

/* Document detail header (quote / purchase order): the info block (left) sits
   beside a content-width totals card (right). The right column is sized to the
   totals (`auto`), so it stays narrower than the info block; both cards stretch to
   a shared row height. (Distinct from print.css `.doc-head`, the printed header.) */
.detail-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 16px; align-items: stretch; margin-bottom: 14px; }
.detail-head > .card { margin-bottom: 0; height: 100%; }
.detail-head table.totals { margin-top: 0; }
/* Editable global-discount row inside the totals table: compact controls in the
   value cell so the editor reads as one more totals line — no boxed-out look. */
.totals .discount-row td { padding-top: 5px; padding-bottom: 7px; }
.totals-discount { display: inline-flex; align-items: center; gap: 6px; }
.totals-discount .discount-input input { width: 64px; }
.totals-discount input, .totals-discount select { height: auto; padding: 3px 7px; font-size: var(--fs-13); border-radius: 6px; }
.totals-discount .btn { min-height: 0; padding: 4px 9px; font-size: var(--fs-13); }

/* Invoice settlement row: payment entry form (left) beside the Payé / Reste dû
   totals (bottom-right), aligned to the form's baseline rather than stacked. */
.pay-settle { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 24px; }
.pay-settle > form { flex: 1 1 var(--field-lg); }
.pay-settle > table.totals { margin-top: 0; }

/* Per-line discount: a number field glued to its unit selector (% / €). In the
   line-form it hugs its content; in the edit row it fills the column. */
.discount-input { display: inline-flex; gap: 4px; align-items: stretch; }
.discount-input input { width: 72px; margin: 0; }
.discount-input select { width: auto; margin: 0; }
td .discount-input { display: flex; width: 100%; }
td .discount-input input { width: 100%; min-width: 44px; }

/* badges (pastilles) — content is flex-centred so the pill height is even and
   the label sits dead-centre, exactly like the sidebar alert pastille.
   Colour code: the six canonical kinds below; status names are aliases. */
.badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 20px; padding: 1px 10px; border-radius: 20px;
  font-size: var(--fs-12); font-weight: var(--fw-semibold); line-height: 1;
  background: var(--chip); color: var(--muted); white-space: nowrap; vertical-align: middle;
}
/* canonical kinds */
.badge-neutral { background: var(--chip); color: var(--muted); }
.badge-info    { background: var(--info-wash); color: var(--info); }
.badge-ok      { background: var(--ok-wash); color: var(--ok-d); }
.badge-warn    { background: var(--warn-wash); color: var(--warn-d); }
.badge-bad     { background: var(--bad-wash); color: var(--bad-d); }
.badge-accent  { background: var(--accent-wash); color: var(--accent); }
/* status aliases -> kinds */
.badge-draft     { background: var(--chip); color: var(--muted); border: 1px solid var(--line); }
.badge-sent      { background: var(--info-wash); color: var(--info); }
.badge-accepted  { background: var(--ok-wash); color: var(--ok-d); }
.badge-refused,
.badge-cancelled { background: var(--bad-wash); color: var(--bad-d); }
.badge-invoiced  { background: var(--accent-wash); color: var(--accent); }
/* Status dot (ui.badge {dot}) : pastille de la couleur du texte en tête d'un badge de
   STATUT (devis/facture/commande/retard) → l'état se repère d'un coup d'œil. Réservé
   aux statuts (jamais une catégorie Kit/Service/source), donc le point dit « état ». */
.badge-dot::before { content: ""; flex-shrink: 0; width: 6px; height: 6px; border-radius: 50%; background: currentColor; margin-right: 6px; }

/* author pastille — initials of who performed/created the row. Round, centred,
   deterministic colour. Permanent stamp on every line; tracing at a glance. */
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 50%; flex-shrink: 0;
  font-size: var(--fs-11); font-weight: var(--fw-bold); line-height: 1; letter-spacing: .02em;
  color: var(--white); background: var(--muted); vertical-align: middle;
  user-select: none;
}
.avatar.av-empty { background: var(--chip); color: var(--muted); font-weight: var(--fw-semibold); }
.av-0 { background: var(--av-0); } .av-1 { background: var(--av-1); }
.av-2 { background: var(--av-2); } .av-3 { background: var(--av-3); }
.av-4 { background: var(--av-4); } .av-5 { background: var(--av-5); }
.av-6 { background: var(--av-6); } .av-7 { background: var(--av-7); }
/* pastille + full name, for detail pages ("Créé par"). */
.author { display: inline-flex; align-items: center; gap: 8px; }

/* toasts — transient success/info feedback, bottom-right, auto-dismissing.
   Errors stay as a top banner (.flash-error) so they're read in context. */
.toasts { position: fixed; right: 18px; bottom: 18px; z-index: 60;
  display: flex; flex-direction: column; gap: 10px; max-width: min(360px, calc(100vw - 36px)); }
.toast {
  display: flex; align-items: flex-start; gap: 10px;
  background: var(--panel); border: 1px solid var(--line); border-left: 4px solid var(--muted);
  border-radius: 10px; padding: 12px 14px; box-shadow: var(--shadow-pop);
  font-size: var(--fs-14); font-weight: var(--fw-medium); color: var(--ink);
  animation: toast-in .22s ease-out;
}
.toast.leaving { animation: toast-out .18s ease-in forwards; }
.toast .toast-ic { flex-shrink: 0; font-size: var(--fs-15); line-height: 1.3; }
.toast .toast-msg { flex: 1; min-width: 0; }
.toast .toast-x { background: none; border: 0; cursor: pointer; color: var(--muted);
  font-size: var(--fs-18); line-height: 1; padding: 0 2px; flex-shrink: 0; }
.toast .toast-x:hover { color: var(--ink); }
.toast-success { border-left-color: var(--ok); }
.toast-success .toast-ic { color: var(--ok); }
.toast-info { border-left-color: var(--info); }
.toast-info .toast-ic { color: var(--info); }
@keyframes toast-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateY(12px); } }
@media (prefers-reduced-motion: reduce) { .toast, .toast.leaving { animation: none; } }

/* confirm modal — one reusable dialog for every guarded/irreversible action
   (see app.js confirmModal + ui.confirmAttrs). Centred over a dimmed backdrop. */
.modal-overlay {
  position: fixed; inset: 0; z-index: 80; display: flex; align-items: center; justify-content: center;
  padding: 20px; background: rgba(20,25,40,.42); animation: modal-fade .14s ease-out;
}
.modal-overlay[hidden] { display: none; }
.modal {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card);
  padding: 22px 24px; width: 100%; max-width: 420px; box-shadow: var(--shadow-pop);
  animation: modal-pop .16s ease-out;
}
.modal-title { margin: 0 0 8px; font-size: var(--fs-18); }
.modal-msg { margin: 0; color: var(--ink-2); font-size: var(--fs-14); line-height: 1.55; white-space: pre-line; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 22px; }
body.modal-open { overflow: hidden; }
@keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-pop { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .modal-overlay, .modal { animation: none; } }

/* Sheen — balayage lumineux des surfaces à dégradé (bouton primaire, onglet de nav actif,
   showcase). La lueur attend hors-champ (55% du cycle) puis traverse en diagonale ; le
   dégradé Orchidée reste fixe dessous (2ᵉ couche). Lent mais perceptible. */
@keyframes brand-sheen {
  0%, 55% { background-position: -160% 0, 0% 50%; }
  100%    { background-position:  260% 0, 0% 50%; }
}
/* Rotation lente du dégradé Orchidée (cf. --grad-rot). --grad-rot doit être enregistré
   en <angle> : un custom property non typé n'interpolerait pas et l'angle sauterait par
   à-coups au lieu de tourner en douceur. Un tour complet par cycle = mouvement continu,
   jamais figé. Posé en 2ᵉ animation (avec brand-sheen) sur les surfaces concernées. */
@property --grad-rot {
  syntax: '<angle>';
  inherits: true;
  initial-value: 0deg;
}
@keyframes brand-rotate {
  to { --grad-rot: 360deg; }
}
@media (prefers-reduced-motion: reduce) {
  .btn-primary, .nav-item.active { animation: none; }
}

/* buttons — système unifié (réf. vivante : /design/boutons). Deux axes : EMPHASE
   (neutre/marque, plein→discret) × INTENTION (la couleur porte le sens). Géométrie
   unique ; graisse : fonds pleins 600 (porter sur la couleur), autres 500. */
/* min-height locks the box so siblings align regardless of content: an icon
   (15px) would otherwise make a button 1px taller than a text-only one (14px
   line-box), misaligning header action rows. */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: var(--control-h); padding: 8px 14px; border-radius: var(--radius); font-size: var(--fs-14);
  font-family: inherit; font-weight: var(--fw-medium); line-height: 1; border: 1px solid transparent;
  cursor: pointer; white-space: nowrap; user-select: none;
  transition: background .12s, border-color .12s, color .12s, filter .12s; }
.btn:hover { text-decoration: none; }
.btn .ic-inline { width: 15px; height: 15px; }
/* fonds pleins = texte blanc sur couleur saturée → 600 */
.btn-primary, .btn-secondary, .btn-success, .btn-danger, .btn-warn, .btn-info { font-weight: var(--fw-semibold); }

/* — Emphase — */
/* background-image (pas `background`) + background-origin: border-box : le bord 1px reste
   net (cf. commentaire d'origine). Le hover NE redéclare PAS `background`. */
.btn-primary { background-image: var(--sheen), linear-gradient(calc(var(--grad-angle) + var(--grad-rot)), var(--grad-stops));
  background-origin: border-box; background-repeat: no-repeat;
  background-size: 220% 100%, 100% 100%; background-position: -160% 0, 0% 50%;
  color: var(--white); animation: brand-sheen 6.5s ease-in-out infinite, brand-rotate 12s linear infinite; }
.btn-primary:hover { color: var(--white); filter: brightness(1.07); }
.btn-secondary { background: var(--ink); color: var(--white); }
.btn-secondary:hover { background: color-mix(in srgb, var(--ink), #fff 16%); }
.btn-soft { background: var(--brand-wash); color: var(--brand-d); }
.btn-soft:hover { background: color-mix(in srgb, var(--brand-wash), var(--brand) 12%); }
.btn-outline { background: var(--white); border-color: var(--line); color: var(--ink); }
.btn-outline:hover { border-color: var(--muted); }
.btn-quiet { background: transparent; color: var(--link); }
.btn-quiet:hover { background: var(--brand-wash); color: var(--brand-d); }

/* — Intentions sémantiques : plein (fort) | doux (retrait) — */
.btn-success { background: var(--ok); color: var(--white); }
.btn-success:hover { filter: brightness(1.07); }
.btn-success-soft { background: var(--ok-wash); color: var(--ok-d); }
.btn-success-soft:hover { background: color-mix(in srgb, var(--ok-wash), var(--ok) 12%); }
.btn-danger { background: var(--bad); color: var(--white); }
.btn-danger:hover { filter: brightness(1.07); }
.btn-danger-outline { background: var(--white); border-color: var(--bad-line); color: var(--bad); }
.btn-danger-outline:hover { background: var(--bad); border-color: var(--bad); color: var(--white); }
.btn-danger-soft { background: var(--bad-wash); color: var(--bad-d); }
.btn-danger-soft:hover { background: color-mix(in srgb, var(--bad-wash), var(--bad) 12%); }
.btn-warn { background: var(--warn); color: var(--white); }
.btn-warn:hover { filter: brightness(1.07); }
.btn-warn-soft { background: var(--warn-wash); color: var(--warn-d); }
.btn-warn-soft:hover { background: color-mix(in srgb, var(--warn-wash), var(--warn) 12%); }
/* « Info » en violet --accent (plus mat que --brand) → garde l'esprit du site, pas un bleu. */
.btn-info { background: var(--accent); color: var(--white); }
.btn-info:hover { filter: brightness(1.08); }
.btn-info-soft { background: var(--accent-wash); color: var(--accent); }
.btn-info-soft:hover { background: color-mix(in srgb, var(--accent-wash), var(--accent) 12%); }

/* — Tailles & états — */
.btn-sm { min-height: var(--control-h-sm); padding: 5px 10px; font-size: var(--fs-13); }
.btn-sm .ic-inline { width: 14px; height: 14px; }
.btn.block, .btn-block { width: 100%; justify-content: center; margin-top: 6px; }
.btn:disabled, .btn.btn-disabled { opacity: .5; cursor: not-allowed; pointer-events: none; filter: none; animation: none; }
.link { color: var(--link); }
.link:hover { color: var(--brand-d); }
/* delete cross — small, prominent red button (clear target, easy to hit) */
.link-btn { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; padding: 0; border: 1px solid var(--bad-wash); border-radius: 7px; background: var(--bad-wash); color: var(--bad-d); font-size: var(--fs-15); line-height: 1; cursor: pointer; transition: background .12s, color .12s, border-color .12s; }
.link-btn:hover { background: var(--bad); border-color: var(--bad); color: var(--white); text-decoration: none; }
.link-btn.danger { color: var(--bad-d); font-size: var(--fs-15); }
/* neutral icon button (edit) — same target as the delete cross, non-destructive color */
.link-btn.neutral { border-color: var(--line); background: var(--white); color: var(--ink-2); }
.link-btn.neutral:hover { background: var(--ink); border-color: var(--ink); color: var(--white); }

/* Action column (ui.table empty-header column): every row's buttons grouped and
   right-aligned (it is always the trailing column, so they hug the table edge),
   laid out side by side and wrapping to stacked when cramped. One factored rule —
   no route lays out its own action cell. In-row buttons are real `.btn` (quiet /
   danger-soft, btn-sm) — the action column hosts them without bespoke styling. */
.td-actions { text-align: right; vertical-align: middle; }
.col-actions { display: inline-flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px; }
.col-actions form { margin: 0; }
/* Exploded kit component sub-line: indented, muted, under its kit head line. */
.sub-line { color: var(--ink-2); }

/* Opt-in product reference appended inline after a quote line label, with its
   toggle button sitting on the same line (the <form> would break it otherwise). */
.line-ref { color: var(--muted); }

/* forms */
.field { display: flex; flex-direction: column; gap: 4px; }
.field > span { font-size: var(--fs-13); font-weight: var(--fw-semibold); color: var(--ink-2); }
.field .req { color: var(--bad); margin-left: 3px; font-style: normal; }
.field small { color: var(--muted); font-size: var(--fs-12); line-height: 1.3; }
/* Inline help marker (ui.helpIcon): a plain hairline white « ? » disc at a zone
   title (field label / section legend). Its clarifying note rides the CSS-only
   popover below, shown on hover/focus — it replaces the grey notes that used to sit
   under every field. The disc itself stays inert (no hover state, no shadow); only
   the popover appears. One look site-wide. */
.help-q {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; margin-left: 5px; flex: none; vertical-align: middle;
  border: 1px solid var(--line); border-radius: 50%; background: var(--white);
  color: var(--muted); font-size: var(--fs-11); font-weight: var(--fw-bold);
  font-style: normal; text-transform: none; letter-spacing: 0; line-height: 1; cursor: help;
}
.help-pop {
  position: absolute; top: calc(100% + 6px); left: 50%; transform: translateX(-50%);
  z-index: 30; width: max-content; max-width: 250px; padding: 7px 10px;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel);
  box-shadow: var(--shadow-pop); color: var(--ink-2); font-size: var(--fs-12);
  font-weight: var(--fw-normal); font-style: normal; text-transform: none;
  letter-spacing: 0; line-height: 1.4; text-align: left; white-space: normal;
  opacity: 0; visibility: hidden; pointer-events: none;
}
.help-q:hover .help-pop, .help-q:focus .help-pop, .help-q:focus-within .help-pop {
  opacity: 1; visibility: visible;
}
.help-pop a { color: var(--link); pointer-events: auto; }
input, select, textarea {
  font: inherit; padding: 7px 11px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--white); color: var(--ink); width: 100%;
  /* Checkbox/radio tick adopts the Orchidée brand magenta (never the browser default blue). */
  accent-color: var(--brand);
}
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--focus-ring); }
/* Shared control height: every single-line input + select sits at --control-h, the
   same box as a button, so dates/selects/text/buttons align on one row (the native
   date input's taller intrinsic box is the reason — box-sizing + a fixed height pins
   it). Wrapped in :where() to carry ZERO specificity, so any compact context
   (.totals-discount, filters…) overrides it with a plain class. Multi-line textarea,
   and checkbox/radio/file inputs (intrinsic sizing) opt out. */
input:where(:not([type=checkbox]):not([type=radio]):not([type=file])), select {
  height: var(--control-h); padding-top: 0; padding-bottom: 0;
}
/* Free-text areas (address, notes, description) read best at a normal text
   measure — cap them so they never stretch edge-to-edge like a full-width band. */
textarea { max-width: 60ch; min-height: 64px; }
/* Form fields share one layout vocabulary across two contexts. Inline route forms
   keep the FLEX flow here (each field at its declared .field-<size> width, no grow,
   wrapping when cramped, every input bottom-aligned so a 2-line label never pushes its
   input below single-line neighbours). Sectioned entity forms switch the same grid to
   a 12-COLUMN CSS grid (.form-section .form-grid, rule just below) where each size maps
   to a column span. The .field-<size> classes therefore carry BOTH a flex-basis (used
   in flex) and a grid-column span (used in grid); each property is inert in the other
   context, so one vocabulary drives both. */
.form-grid { display: flex; flex-wrap: wrap; gap: 12px 16px; align-items: end; }
.form-grid > :where(.field, [data-show-for]) { flex: 0 1 var(--field-md); min-width: 0; grid-column: span 4; }
.field-xs { flex-basis: var(--field-xs); grid-column: span 2; }
.field-sm { flex-basis: var(--field-sm); grid-column: span 3; }
.field-md { flex-basis: var(--field-md); grid-column: span 4; }
.field-lg { flex-basis: var(--field-lg); grid-column: span 6; }
.field-xl { flex-basis: var(--field-xl); grid-column: span 8; }
.field-full, .form-grid > .field:has(textarea), .form-actions { flex-basis: 100%; grid-column: 1 / -1; }
.form-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; align-items: center; }
/* Caisse period-closing row (select + native date + button on one line): the shared
   --control-h aligns them automatically; this just gives the closures table below room. */
.close-form { margin-bottom: 18px; }
/* A short inline form (combobox + notes) stays a compact card hugging its reading
   column, not a sparse band across the whole content area. */
.form-narrow { max-width: var(--form-col-narrow); }
@media (max-width: 640px) { .form-grid > :where(.field, [data-show-for]) { flex-basis: 100%; } }

/* Sticky action bar at the foot of an entity form (ui.formActions): ONE convention for the
   article / client / supplier / settings forms — it replaces the old split between header
   actions (article) and a bottom .form-actions (the others). Sticks to the viewport bottom
   so the primary action stays reachable on a long form; a hairline top + page background
   keep content legible scrolling under it. flex-basis 100% lets it span a flex form-grid
   (settings) as well as a block .form-sections stack. */
.form-bar { position: sticky; bottom: 0; z-index: 20; flex-basis: 100%;
  display: flex; gap: 8px; align-items: center; justify-content: flex-end;
  margin-top: 14px; padding: 12px 0; background: var(--bg); border-top: 1px solid var(--line); }

/* Sectioned entity forms (article / client / fournisseur / réglages): the section's
   field grid becomes a shared 12-column grid so fields align between rows and a tuned
   row fills its width — the fix for the ragged right edge and scattered gaps (the
   .field-<size> spans above map each field; on the narrow rail every field stacks). */
.form-section .form-grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); align-items: end; }
@media (max-width: 640px) { .form-section .form-grid > * { grid-column: 1 / -1; } }

/* Catalogue import wizard (routes/products-import.js). */
/* Upload step: a single drag-and-drop target — no text, no buttons. The whole
   zone is a <label> wrapping a hidden file input (clicking opens the picker); the
   dashed border is the affordance, the inner children are pointer-transparent so
   every drag event targets the zone itself (reliable dragover/leave toggling). */
.dropzone {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; min-height: 260px; padding: 40px; text-align: center; cursor: pointer;
  border: 2px dashed var(--line); border-radius: var(--radius-card);
  background: var(--panel); transition: border-color .12s ease, background .12s ease;
}
.dropzone:hover, .dropzone:focus-within, .dropzone.dragover { border-color: var(--brand); background: var(--brand-wash); }
.dropzone > * { pointer-events: none; }
.dropzone .ic { width: 44px; height: 44px; background: var(--brand-grad); }
.dropzone-title { font-size: var(--fs-17); font-weight: var(--fw-semibold); color: var(--ink); }
.dropzone-hint { font-size: var(--fs-13); color: var(--muted); }
.import-resume { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.import-resume p { margin: 0; }
.import-progress-row { display: flex; align-items: center; gap: 10px; margin: 4px 0; }
.import-progress { flex: 1; height: 10px; border: 1px solid var(--line); border-radius: 999px;
  overflow: hidden; appearance: none; -webkit-appearance: none; background: var(--brand-wash); }
.import-progress::-webkit-progress-bar { background: var(--brand-wash); }
.import-progress::-webkit-progress-value { background: var(--brand-grad); border-radius: 999px; }
.import-progress::-moz-progress-bar { background: var(--brand); }
.import-pct { font-variant-numeric: tabular-nums; font-weight: var(--fw-semibold); color: var(--ink-2); min-width: 3.5ch; text-align: right; }
.import-warnings { margin: 8px 0 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; }
.import-warnings li { font-size: var(--fs-13); color: var(--ink-2); }
/* Mapping field→column tables: the column <select>s become comboboxes (≥8 file
   columns), whose absolute popup would be clipped — and a vertical scrollbar shown —
   by the table-wrap's overflow. These tables never need horizontal scroll, so let
   them overflow visible: the search popup escapes the block. */
.map-cols .table-wrap { overflow: visible; }
/* Tighter writing fields in the mapping: shorter selects/combo inputs + slimmer rows.
   No row separator: the per-row <select> already gives the line its own frame, so the
   horizontal rule only adds visual noise here. */
.map-cols select, .map-cols .combo-input { padding: 3px 9px; }
.map-cols td { padding-top: 3px; padding-bottom: 3px; border-bottom: 0; }
/* Import preview: keep each cell on one line (the table-wrap scrolls horizontally),
   rather than wrapping a long value onto two rows. */
.preview-cols th, .preview-cols td { white-space: nowrap; }

/* Titled form sections: each is its OWN CARD (border + radius + padding) so a form reads
   as a stack of labelled cards — the « fiche » pattern of the read views — rather than one
   undifferentiated wall. The sectioned entity form (.form-sections) is no longer a card
   itself; it's a transparent stack capped to the reading column (--form-col), left in the
   content area (no band stretched edge-to-edge — the « vide à droite » fix). The <legend>
   is floated so the fieldset draws a COMPLETE border (a legend otherwise notches the top
   border); the field grid clears below it. Sections stack with a margin; side-by-side
   sections (import « Colonnes supplémentaires », .grid-2) drop it. */
.form-sections { max-width: var(--form-col); }
.form-section { border: 1px solid var(--line); border-radius: var(--radius-card); padding: 14px 18px; margin: 0; min-width: 0; background: var(--panel); }
.form-section + .form-section { margin-top: 14px; }
.grid-2 > .form-section + .form-section { margin-top: 0; }
.form-section > legend { float: left; display: flex; align-items: center; gap: 8px; width: 100%; font-size: var(--fs-15); font-weight: var(--fw-semibold); letter-spacing: -.01em; color: var(--ink); padding: 0; margin-bottom: 12px; }
.form-section > .form-grid, .form-section > .rules-list, .form-section > .perm-grid { clear: both; }

/* Role editor: name field then a checkbox matrix, one fieldset per module. */
.role-form > .field { margin-bottom: 18px; }
.perm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); gap: 8px 18px; }
.perm-chk { display: flex; align-items: center; gap: 8px; font-size: var(--fs-14); cursor: pointer; }
.perm-chk input { width: auto; margin: 0; }

/* Validation rules editor: each rule is a row with a toggle, a mode/threshold
   control cluster pinned right, and a help line spanning underneath. */
.rules-list { display: flex; flex-direction: column; gap: 14px; }
.rule-row { display: grid; grid-template-columns: 1fr auto; gap: 6px 16px; align-items: center;
  padding-bottom: 12px; border-bottom: 1px solid var(--line); }
.rule-row:last-child { border-bottom: 0; padding-bottom: 0; }
.rule-toggle { display: flex; align-items: center; gap: 9px; cursor: pointer; min-width: 0; }
.rule-toggle input { width: auto; margin: 0; flex: 0 0 auto; }
.rule-name { font-size: var(--fs-14); font-weight: var(--fw-semibold); color: var(--ink-2); }
.rule-controls { display: flex; align-items: center; gap: 8px; }
.rule-mode { width: auto; }
.rule-thr { display: inline-flex; align-items: center; gap: 5px; }
.rule-thr input { width: 80px; }
.rule-unit { color: var(--muted); font-size: var(--fs-13); }
.rule-head { display: flex; align-items: center; gap: 2px; min-width: 0; }
.rule-phases { display: inline-flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
@media (max-width: 560px) { .rule-row { grid-template-columns: 1fr; } .rule-controls { justify-content: flex-start; } }

/* full-width field (repeatable refs / prices span the whole section row) */
.field-wide { flex-basis: 100%; grid-column: 1 / -1; }

/* Light inset panel — the quote/order « + Ajouter une ligne » block (.line-add). */
.subblock { background: var(--chip); border: 1px solid var(--line); border-radius: var(--radius); padding: 11px 13px; }

/* repeatable rows (additional refs, multiple sale/purchase prices) */
.repeat { display: flex; flex-direction: column; gap: 8px; }
/* Column-header guide row: each cell mirrors the matching input's flex width so the
   label sits above its column. Hidden on the narrow rail where rows wrap and the
   inputs' own placeholders take over. */
.repeat-head { display: flex; gap: 8px; align-items: center; padding: 0 1px; flex-wrap: wrap; }
.repeat-head span { font-size: var(--fs-11); font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.repeat-head .rh-text { flex: 0 1 var(--field-md); min-width: 110px; }
.repeat-head .rh-num { flex: 0 1 130px; min-width: 90px; }
.repeat-head .rh-combo { flex: 0 1 var(--field-md); min-width: 160px; }
.repeat-head .rh-narrow { flex: 0 1 84px; min-width: 64px; }
.repeat-head .rh-def { padding-left: 18px; white-space: nowrap; }
.repeat-head .rh-x { flex: 0 0 26px; }
@media (max-width: 820px) { .repeat-head { display: none; } }
/* Conditional column-header guide: show it only when there is ≥ 1 row. An empty block
   (no refs / no supplier / no kit component) hides the « LIBELLÉ / RÉFÉRENCE… » header
   floating over nothing and shows a discreet placeholder instead; adding the first row
   (JS) makes [data-rows] non-empty → header back, placeholder gone, all via CSS. */
.repeat:has([data-rows]:empty) .repeat-head { display: none; }
.repeat-none { display: none; font-size: var(--fs-13); color: var(--muted); padding: 1px 1px 2px; }
.repeat:has([data-rows]:empty) .repeat-none { display: block; }
/* Rows live inside [data-rows]; the gap must be there, not only between .repeat's
   direct children, otherwise stacked rows touch. */
.repeat [data-rows] { display: flex; flex-direction: column; gap: 8px; }
.repeat-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* Each repeatable-row field keeps a fixed width (no flex-grow) so a label/ref never
   stretches edge-to-edge across the full-width section — same discipline as the
   form-grid fields. Shrinks to its min-width when the row wraps on a narrow rail. */
.repeat-row input[type=text] { flex: 0 1 var(--field-md); min-width: 110px; margin: 0; }
.repeat-row input[type=number] { flex: 0 1 130px; min-width: 90px; margin: 0; }
/* A bare <select> in a repeatable row (import-wizard column picker) holds the same fixed
   width; a ≥8-option list is auto-enriched into a .combo (styled above) by app.js. */
.repeat-row select { flex: 0 1 var(--field-md); min-width: 110px; margin: 0; }
.repeat-row .radio-def { display: flex; align-items: center; gap: 5px; font-size: var(--fs-13); color: var(--ink-2); white-space: nowrap; }
.repeat-row .radio-def input { width: auto; margin: 0; }
/* A combobox picker (supplier / kit component) keeps a fixed width too, never a full row. */
.repeat-row .combo { flex: 0 1 var(--field-md); min-width: 160px; width: auto; }
.repeat [data-add-row] { align-self: flex-start; }
/* Supplier-offer row (« Fournisseurs & achat »): packaging / lead-time stay narrow
   so the dense row fits supplier + ref + price + cond. + délai + préféré on a line. */
.offer-row input[name="offer_pack"], .offer-row input[name="offer_lead"] { flex: 0 1 84px; min-width: 64px; }

/* Quick filters on list pages — one factored look, identical to the /comptabilite
   period selector: each control carries its label ABOVE it (`.filter-sel` mirrors a
   `.field`), and the whole row bottom-aligns (`align-items: end`) so the labelled
   controls and the (full-height, not btn-sm) buttons sit on the same baseline. */
.filters { display: flex; gap: 16px; align-items: end; flex-wrap: wrap; margin: -2px 0 14px; }
.filter-sel { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
/* Weight the LABEL only (mirror of `.field > span`) — never the container, or the
   bold would cascade into the date/select value text (the « graisse » that made the
   filter dates look heavier than the comptabilite ones). */
.filter-sel > span { font-size: var(--fs-13); font-weight: var(--fw-semibold); color: var(--ink-2); }
.filter-sel select { width: auto; padding: 6px 10px; }
/* Date fields sized like the comptabilite period inputs (.field-sm) instead of the
   native intrinsic width, so every Du/Au box matches across the app. */
.filter-sel input[type="date"] { width: var(--field-sm); }
/* Match the combobox input's vertical padding (6px) so the box height lines up and
   the centered checkbox content sits on the input's vertical center, not its baseline. */
.filter-chk { display: flex; align-items: center; gap: 7px; padding: 6px 0; font-size: var(--fs-13); font-weight: var(--fw-semibold); color: var(--ink-2); cursor: pointer; }
.filter-chk input { width: auto; }

.searchbar { display: flex; gap: 8px; margin-bottom: 14px; }
.searchbar input { max-width: 420px; }

/* quick stock movement form — stacked lines for readability:
   1) type + qté  2) motif  3) prix + validation */
.quick-move { display: flex; flex-direction: column; gap: 10px; }
.quick-move .qm-line { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.quick-move .qm-qty { flex: 1; min-width: 90px; }
.quick-move .price-free { flex: 1; min-width: 130px; }
.quick-move .qm-line .btn { margin-left: auto; }

/* selects/blocks sized to their content (e.g. price pickers), capped to width.
   Reusable across the app wherever a control should hug its text. */
.price-select, .w-content { width: auto; max-width: 100%; }
.line-form { display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; padding-top: 16px; border-top: 1px solid var(--line); }
.line-form .field { margin: 0; }
/* Add-article block: hosted in a subblock panel at the top of the lines card, so
   it owns its own separation — drop the line-form's own top border/padding. */
.line-add { margin-bottom: 16px; }
/* Single-row layout: every control stays on one line; the Article picker flexes to
   absorb the slack while the others keep fixed widths, and « + Ajouter » is pinned
   to the right edge (margin-left:auto) so its position never moves when fields
   toggle (Libellé hides / Tarif shows / PUHT hides). */
.line-add .line-form { padding-top: 0; border-top: 0; flex-wrap: nowrap; }
.line-add .line-form > .field { flex: 0 0 150px; min-width: 0; }
.line-add .line-form [data-lf-product] { flex: 1 1 180px; }
.line-add .line-form [data-lf-label],
.line-add .line-form [data-lf-tariff] { flex-basis: 190px; }
.line-add .line-form .field:has([data-qty]) { flex-basis: 80px; }
.line-add .line-form [data-lf-price] { flex-basis: 110px; }
.line-add .line-form button[type="submit"] { margin-left: auto; flex: 0 0 auto; }
/* Uniform control height across the row: number inputs are intrinsically taller
   than selects, and the submit button shorter than both — pin them equal so every
   field and « + Ajouter » line up (bottom-aligned). */
.line-form input, .line-form select, .line-form .combo-input,
.line-form button[type="submit"] { height: 38px; }
/* Article picker: fixed width — never grow into the space freed when the
   « Libellé » field hides on selection, nor stretch for a long label. */
.line-form [data-lf-product] { flex: 0 0 560px; max-width: 100%; }
/* Carton conditioning (F4): the pack hint sits under the « Cartons » input,
   muted as info (« ×N / carton »), amber when the qty isn't a whole carton —
   which also tints the qty input border. Non-blocking. */
.line-form [data-lf-pack] { white-space: nowrap; }
.line-form [data-lf-pack].warn { color: var(--warn-d); font-weight: var(--fw-semibold); }
.line-form input.warn { border-color: var(--warn); }

/* Searchable comboboxes — one type-to-filter UI for every long choice list
   (clients, articles, fournisseurs, familles…). The native <select> stays as the
   form control but is hidden once enhanced; the .combo-input drives the search
   and the popup lists at most 5 suggestions. See app.js initCombobox. */
.combo { position: relative; display: block; width: 100%; }
.combo-native { display: none; }
.combo-input { width: 100%; text-overflow: ellipsis; }
.combo-pop {
  position: absolute; z-index: 50; left: 0; right: 0; top: calc(100% + 4px);
  margin: 0; padding: 4px; list-style: none; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-pop);
  max-height: 260px; overflow-y: auto;
}
.combo-pop[hidden] { display: none; }
.combo-opt { padding: 8px 10px; border-radius: 6px; cursor: pointer; font-size: var(--fs-14); }
.combo-opt:hover, .combo-opt.active { background: var(--brand-wash); }
.combo-empty, .combo-more { padding: 8px 10px; font-size: var(--fs-12); color: var(--muted); }
/* « Kit » marker on a kit suggestion in the article picker. */
.combo-tag { margin-left: 6px; padding: 1px 6px; border-radius: 6px; background: var(--info-wash); color: var(--info); font-size: var(--fs-11); vertical-align: middle; }
/* filters & line-form keep their content-hugging width on the combo too */
.filter-sel .combo, .line-form .price-select ~ .combo { width: auto; }
.filter-sel .combo-input { width: auto; min-width: 150px; padding: 6px 10px; }
/* Chip picker (product form « Équivalents / substituts ») : a remote combobox whose
   picks become removable pills. */
.chip-picker { display: flex; flex-direction: column; gap: 8px; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chips:empty { display: none; }
.chip-tag { display: inline-flex; align-items: center; gap: 6px; padding: 3px 5px 3px 11px;
  border: 1px solid var(--line); border-radius: 999px; background: var(--brand-wash);
  color: var(--ink); font-size: var(--fs-13); }
.chip-x { border: 0; background: none; cursor: pointer; color: var(--muted);
  font-size: var(--fs-15); line-height: 1; padding: 0 3px; border-radius: 999px; }
.chip-x:hover { color: var(--bad); }
.chip-picker .combo { max-width: 360px; }
.veh-form { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.veh-form input { width: auto; flex: 1; min-width: 80px; }

/* compact inline date editor (order expected delivery) + reception inputs */
.inline-date { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.inline-date input { width: auto; margin: 0; }
.recv-table input[type=number] { width: 110px; margin: 0; }
/* avoir partiel: per-line credit quantities (same shape as .recv-table) */
.avoir-table input[type=number] { width: 110px; margin: 0; }
.avoir-table input[type=checkbox] { width: auto; margin: 0 6px 0 0; }
.kit-credit { display: inline-flex; align-items: center; white-space: nowrap; }

/* misc */
.empty { text-align: center; padding: 30px 20px; color: var(--muted); }
.empty p { margin-bottom: 14px; }
/* Ancre d'état vide (ui.emptyState {icon}) : glyphe du domaine en grand, teinté muet
   (couleur héritée via currentColor) et adouci → la zone vide est « habitée », pas nue. */
.empty-ic { margin-bottom: 14px; }
.empty-ic .ic { width: 40px; height: 40px; opacity: .55; }
/* in-context banner — used for errors (read where the action happened).
   Success/info feedback goes to bottom-right toasts instead. */
.flash { display: flex; align-items: center; gap: 9px; padding: 10px 16px; border-radius: var(--radius);
  margin-bottom: 14px; font-size: var(--fs-14); font-weight: var(--fw-medium); border-left: 4px solid transparent; }
.flash::before { font-size: var(--fs-15); line-height: 1; }
.flash-success { background: var(--ok-wash); color: var(--ok-d); border-left-color: var(--ok); }
.flash-success::before { content: "\2713"; }
.flash-error { background: var(--bad-wash); color: var(--bad-d); border-left-color: var(--bad); }
.flash-error::before { content: "\26A0"; }
.flash-info { background: var(--info-wash); color: var(--info); border-left-color: var(--info); }
.flash-info::before { content: "\2139"; }
.flash-warn { background: var(--warn-wash); color: var(--warn-d); border-left-color: var(--warn); }
.flash-warn::before { content: "\26A0"; }
.desc { margin-top: 10px; color: var(--muted); white-space: pre-wrap; }

/* auth */
/* Aizawa attractor hero behind the auth card (public/fx/aizawa.js): a fixed
   canvas paints its own deep-violet background + orbiting « Orchidée » glow,
   the white card floats above it. Fallback (no JS): the canvas stays dark. */
.fx-aizawa { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 0; display: block; background: #080410; }

/* Idle screen-saver overlay on app pages (public/app.js initVeille): after
   inactivity it covers everything (above sidebar/content/toasts/modal) with the
   same Aizawa attractor; the first gesture wakes it. Sits dark + transparent
   until armed, fades in on `.veille-on`, captures the wake gesture only then. */
.veille { position: fixed; inset: 0; z-index: 1000; background: #080410; opacity: 0; transition: opacity .4s ease; pointer-events: none; }
.veille[hidden] { display: none; }
.veille-on { opacity: 1; pointer-events: auto; }

body.centered { position: relative; }
body.centered .auth-box { position: relative; z-index: 1; box-shadow: 0 24px 60px rgba(8, 4, 16, 0.5); }
.auth-box { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 28px; width: 360px; box-shadow: var(--shadow); }
.auth-box h2 { margin: 4px 0 6px; text-align: center; }
.auth-box p { text-align: center; }
.auth-box form { margin-top: 14px; display: flex; flex-direction: column; gap: 12px; }

/* pagination */
.pager { display: flex; gap: 12px; align-items: center; justify-content: center; margin-top: 16px; }

/* tab bar (sibling list views) */
.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin-bottom: 14px; flex-wrap: wrap; }
.tabs .tab { display: inline-flex; align-items: center; gap: 7px; padding: 8px 13px; color: var(--muted);
  border-bottom: 2px solid transparent; margin-bottom: -1px; font-size: var(--fs-14); font-weight: var(--fw-medium); }
.tabs .tab:hover { color: var(--ink); }
.tabs .tab.active { color: var(--brand-d); border-bottom-color: var(--brand); }
.tab-count { background: var(--chip); color: var(--muted); border-radius: 999px; padding: 1px 8px; font-size: var(--fs-12); font-weight: var(--fw-semibold); }
.tabs .tab.active .tab-count { background: var(--brand-wash); color: var(--brand-d); }

/* categories (familles / sous-familles) */
.inline-add { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; }
.inline-add input { width: auto; flex: 1; min-width: 180px; }
.inline-add .field { margin: 0; flex: 0 1 220px; }
.inline-add.card { max-width: 640px; }

/* Block text selection / I-beam cursor on UI chrome; keep it where copy/typing matters */
body { -webkit-user-select: none; -ms-user-select: none; user-select: none; }
input, textarea, select, [contenteditable], td, code, pre {
  -webkit-user-select: text; -ms-user-select: text; user-select: text;
}

/* ── Design showcase — les 3 signatures explorées (slug /design) ───────────────
   Référence vivante des directions « violet dégradé ». Orchidée a été RETENUE et est
   désormais le primaire global (:root --brand/--brand-grad) → `.dpv-orchidee` reflète
   exactement le chrome réel. Chaque `.dpv-<nom>` repointe juste un dégradé `--g` + un
   violet plein `--dpv-*` sur un mock du chrome ; tout reste isolé sous `.dpv-*`. */
.dpv-intro { max-width: 64ch; color: var(--muted); margin-bottom: 18px; }
.dpv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); gap: 18px; }
.dpv-card { border: 1px solid var(--line); border-radius: var(--radius-card); overflow: hidden; background: var(--panel); display: flex; flex-direction: column; }
.dpv-swatch { height: 96px; background: var(--g); position: relative; }
.dpv-swatch-name { position: absolute; left: 16px; bottom: 11px; color: var(--white); font-weight: var(--fw-heavy); font-size: var(--fs-20); letter-spacing: -.01em; text-shadow: 0 1px 3px rgba(0,0,0,.28); }
.dpv-chosen { font-size: var(--fs-11); font-weight: var(--fw-bold); background: rgba(255,255,255,.22); padding: 1px 8px; border-radius: 999px; vertical-align: middle; }
.dpv-swatch-hex { position: absolute; right: 14px; bottom: 12px; color: rgba(255,255,255,.92); font-size: var(--fs-11); font-weight: var(--fw-semibold); letter-spacing: .02em; }
.dpv-body { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 12px; }
.dpv-desc { color: var(--muted); font-size: var(--fs-13); }
.dpv-frame { display: flex; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
.dpv-side { width: 138px; flex-shrink: 0; background: var(--dpv-side); border-right: 1px solid var(--line); padding: 10px 8px; display: flex; flex-direction: column; gap: 2px; }
.dpv-brand { display: flex; align-items: center; gap: 8px; font-weight: var(--fw-bold); font-size: var(--fs-14); color: var(--ink); padding: 2px 4px 10px; }
.dpv-logo { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border-radius: 7px; background: var(--g); color: var(--white); font-weight: var(--fw-heavy); font-size: var(--fs-13); }
/* couleur figée (pas `--sidebar-ink`, désormais clair pour le menu graphite réel) :
   ce mock reste une sidebar CLAIRE, l'encre doit rester graphite. */
.dpv-nav-item { display: flex; align-items: center; gap: 7px; padding: 5px 8px; border-radius: 7px; font-size: var(--fs-13); font-weight: var(--fw-medium); color: #525252; }
.dpv-nav-item .ic { width: 15px; height: 15px; background-color: var(--dpv-strong); }
.dpv-nav-item.hover { background: var(--dpv-wash); color: var(--dpv-ink); }
.dpv-nav-item.hover .ic { background-color: var(--dpv-ink); }
.dpv-nav-item.active { background: var(--g); color: var(--white); font-weight: var(--fw-semibold); }
.dpv-nav-item.active .ic { background-color: var(--white); }
.dpv-main { flex: 1; min-width: 0; padding: 12px; display: flex; flex-direction: column; gap: 11px; background: var(--panel); }
.dpv-tools { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.dpv-btn { display: inline-flex; align-items: center; padding: 6px 12px; border-radius: var(--radius); font-size: var(--fs-13); font-weight: var(--fw-bold); border: 1px solid transparent; }
.dpv-btn-primary { background-image: var(--g); background-origin: border-box; color: var(--white); font-weight: var(--fw-semibold); }
.dpv-btn-ghost { background: var(--white); border-color: var(--line); color: var(--ink); font-weight: var(--fw-normal); }
.dpv-btn-soft { background: var(--dpv-wash); color: var(--dpv-ink); }
.dpv-chips { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.dpv-chip { font-size: var(--fs-11); font-weight: var(--fw-bold); padding: 2px 9px; border-radius: 999px; background: var(--dpv-wash); color: var(--dpv-ink); }
.dpv-chip-solid { background: var(--g); color: var(--white); }
.dpv-link { color: var(--dpv-ink); font-weight: var(--fw-semibold); font-size: var(--fs-13); }
.dpv-input { width: 100%; border: 1px solid var(--dpv-strong); border-radius: var(--radius); padding: 6px 9px; font-size: var(--fs-13); color: var(--ink); background: var(--white); box-shadow: 0 0 0 3px var(--dpv-ring); }
.dpv-input::placeholder { color: var(--muted); }
.dpv-meta { font-size: var(--fs-11); color: var(--muted); }

/* Trois signatures — même âme violette, trois températures.
   Améthyste = violet royal équilibré ; Orchidée = camaïeu chaud vers le magenta
   (= primaire global) ; Crépuscule = camaïeu froid (vers l'indigo/bleu nuit). */
.dpv-amethyste  { --g: linear-gradient(135deg, #4c1d95 0%, #7c3aed 52%, #a855f7 100%); --dpv-strong: #7c3aed; --dpv-ink: #5b21b6; --dpv-wash: #f1eafe; --dpv-ring: rgba(124,58,237,.22); --dpv-side: #faf7ff; }
.dpv-orchidee   { --g: linear-gradient(130deg, #7c3aed 0%, #a21caf 50%, #e040c8 100%); --dpv-strong: #a21caf; --dpv-ink: #86198f; --dpv-wash: #fbe8fb; --dpv-ring: rgba(162,28,175,.20); --dpv-side: #fdf6fd; }
.dpv-crepuscule { --g: linear-gradient(150deg, #312e81 0%, #6d28d9 48%, #818cf8 100%); --dpv-strong: #5b50e6; --dpv-ink: #4338ca; --dpv-wash: #eaeafe; --dpv-ring: rgba(91,80,230,.22); --dpv-side: #f6f6ff; }

/* ---- Showcase menu latéral (/design/menu) ---------------------------------
   Trois traitements du FOND de la sidebar (le rose pâle actuel n'est pas « tout
   public »). Chrome identique ; chaque `.smv-<key>` repointe les tokens locaux du
   menu (`--m*`) + son fond. Mock isolé sous `.smv-*` (zéro impact sur le chrome réel,
   tant qu'aucune piste n'est retenue). routes/design.js. */
.smv-intro { max-width: 70ch; color: var(--muted); margin-bottom: 18px; }
.smv-intro strong { color: var(--ink); font-weight: var(--fw-semibold); }
.smv-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 22px 18px; }
.smv-card { display: flex; flex-direction: column; gap: 11px; }
.smv-head { display: flex; flex-direction: column; gap: 3px; }
.smv-name { font-weight: var(--fw-semibold); font-size: var(--fs-17); letter-spacing: -.01em; color: var(--ink); }
.smv-chosen { font-size: var(--fs-11); font-weight: var(--fw-bold); color: var(--brand-d); vertical-align: middle; }
.smv-desc { color: var(--muted); font-size: var(--fs-13); line-height: 1.45; }
.smv-frame { display: flex; height: 348px; border: 1px solid var(--line); border-radius: var(--radius-card); overflow: hidden; }
/* Sidebar mock : layout commun, couleurs via les tokens --m* posés par la variante. */
.smv-side { width: 186px; flex-shrink: 0; display: flex; flex-direction: column;
  color: var(--mink); border-right: 1px solid var(--mline); padding: 12px 9px; }
.smv-brand { display: flex; align-items: center; gap: 8px; padding: 3px 5px 12px;
  font-weight: var(--fw-bold); font-size: var(--fs-15); color: var(--mtitle); }
.smv-logo { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px;
  border-radius: 7px; background: var(--brand-grad); color: var(--white); font-weight: var(--fw-heavy);
  font-size: var(--fs-14); flex-shrink: 0; }
.smv-search { font-size: var(--fs-12); color: var(--mmuted); background: var(--msearch);
  border: 1px solid var(--msearch-line); border-radius: 8px; padding: 6px 9px; margin-bottom: 8px; }
.smv-nav { display: flex; flex-direction: column; gap: 1px; flex: 1; }
.smv-group { font-size: var(--fs-10); font-weight: var(--fw-bold); text-transform: uppercase;
  letter-spacing: .07em; color: var(--mgroup); padding: 9px 8px 2px; }
.smv-item { display: flex; align-items: center; gap: 9px; padding: 5px 8px; border-radius: 8px;
  font-size: var(--fs-13); font-weight: var(--fw-medium); color: var(--mink); }
.smv-item .ic { width: 16px; height: 16px; background-color: var(--mic); flex-shrink: 0; }
.smv-item.hover { background: var(--mhover); color: var(--mhover-ink); }
.smv-item.hover .ic { background-color: var(--mhover-ink); }
.smv-foot { padding-top: 10px; margin-top: 4px; border-top: 1px solid var(--mline); }
.smv-who { display: flex; flex-direction: column; font-size: var(--fs-12); color: var(--mtitle); }
.smv-who small { color: var(--mgroup); text-transform: uppercase; font-size: var(--fs-10); letter-spacing: .05em; }
/* Aperçu du contenu à droite — juste pour situer le contraste du menu. */
.smv-stage { flex: 1; min-width: 0; background: var(--bg); padding: 14px; display: flex; flex-direction: column; gap: 9px; }
.smv-stage-h { height: 15px; width: 46%; border-radius: 6px; background: var(--chip); }
.smv-stage-row { height: 44px; border-radius: var(--radius); border: 1px solid var(--line); }
.smv-stage-row.short { width: 62%; }
/* Item actif « lame » de marque (graphite + blanc) : même mécanique sheen+rotation que
   le chrome réel (.nav-item.active). La variante dégradé porte le mouvement sur tout le fond. */
.smv-graphite .smv-item.active, .smv-blanc .smv-item.active {
  background-image: var(--sheen), linear-gradient(calc(var(--grad-angle) + var(--grad-rot)), var(--grad-stops));
  background-origin: border-box; background-repeat: no-repeat;
  background-size: 220% 100%, 100% 100%; background-position: -160% 0, 0% 50%;
  color: var(--white); font-weight: var(--fw-semibold);
  animation: brand-sheen 8s ease-in-out infinite, brand-rotate 14s linear infinite; }
.smv-graphite .smv-item.active .ic, .smv-blanc .smv-item.active .ic { background-color: var(--white); }

/* 1 — Dégradé plein : le fond entier porte le dégradé Orchidée animé, texte blanc. */
.smv-degrade {
  background-image: var(--sheen), linear-gradient(calc(var(--grad-angle) + var(--grad-rot)), var(--grad-stops));
  background-repeat: no-repeat; background-size: 220% 100%, 100% 100%; background-position: -160% 0, 0% 50%;
  animation: brand-sheen 8s ease-in-out infinite, brand-rotate 14s linear infinite;
  --mink: rgba(255,255,255,.84); --mtitle: #fff; --mgroup: rgba(255,255,255,.62); --mic: rgba(255,255,255,.84);
  --mhover: rgba(255,255,255,.12); --mhover-ink: #fff; --mmuted: rgba(255,255,255,.72);
  --mline: rgba(255,255,255,.18); --msearch: rgba(255,255,255,.14); --msearch-line: rgba(255,255,255,.26); }
.smv-degrade .smv-logo { background: rgba(255,255,255,.18); }
.smv-degrade .smv-item.active { background: rgba(255,255,255,.20); color: #fff; font-weight: var(--fw-semibold); }
.smv-degrade .smv-item.active .ic { background-color: #fff; }

/* 2 — Graphite sombre : ardoise neutre, marque sur le logo + l'item actif. */
.smv-graphite {
  background: #17171b;
  --mink: #c6c6cd; --mtitle: #fff; --mgroup: #6e6e79; --mic: #b6b6c0;
  --mhover: rgba(255,255,255,.07); --mhover-ink: #fff; --mmuted: #8a8a94;
  --mline: rgba(255,255,255,.09); --msearch: rgba(255,255,255,.06); --msearch-line: rgba(255,255,255,.13); }

/* 3 — Blanc épuré : blanc franc, encre graphite, icônes magenta, marque sur l'actif. */
.smv-blanc {
  background: #fff;
  --mink: #404048; --mtitle: var(--ink); --mgroup: #8a8a94; --mic: var(--brand);
  --mhover: #f4f4f5; --mhover-ink: var(--ink); --mmuted: var(--muted);
  --mline: var(--line); --msearch: #fff; --msearch-line: var(--line); }
.smv-blanc .smv-item.hover .ic { background-color: var(--brand); }

@media (prefers-reduced-motion: reduce) {
  .smv-degrade, .smv-graphite .smv-item.active, .smv-blanc .smv-item.active { animation: none; }
}

/* ---- Showcase boutons (/design/boutons) ----------------------------------
   Layout du showcase uniquement. Le système de boutons est **adopté** : la page rend
   désormais de vrais `.btn-*` (défini plus haut, bloc « buttons »), donc ces helpers
   `.btv-*` ne portent plus que la grille/les libellés, jamais de style de bouton. */
.btv-intro { max-width: 72ch; color: var(--muted); margin-bottom: 18px; }
.btv-intro strong { color: var(--ink); font-weight: var(--fw-semibold); }
.btv-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(196px, 1fr)); gap: 18px 16px; }
.btv-cell { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.btv-name { font-size: var(--fs-13); font-weight: var(--fw-semibold); color: var(--ink); }
.btv-usage { font-size: var(--fs-12); color: var(--muted); line-height: 1.4; }
.btv-blockwrap { max-width: 360px; margin-top: 16px; }
