/* ── Recouvrement — interaction timeline ─────────────────────── */
.rec-timeline         { display: flex; flex-direction: column; gap: .1rem; }
.rec-timeline-item    { display: flex; gap: .75rem; padding: .625rem .5rem; border-bottom: 1px solid var(--border); }
.rec-timeline-icon    { flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%;
                         background: var(--color-surface-2); display: flex; align-items: center;
                         justify-content: center; color: var(--text-secondary); margin-top: .1rem; }
.rec-timeline-body    { flex: 1; min-width: 0; }
.rec-timeline-meta    { display: flex; align-items: center; flex-wrap: wrap; gap: .25rem; }
.rec-timeline-main    { margin-top: .2rem; font-size: 0.875rem; }
.rec-timeline-author  { margin-top: .15rem; font-size: 0.75rem; color: var(--text-muted); }
.rec-timeline-time    { font-size: 0.75rem; color: var(--text-muted); white-space: nowrap; }
.rec-timeline-badge   { margin-left: .4rem; }
.rec-timeline-muted   { color: var(--text-secondary); }
.modal-footer--left   { justify-content: flex-start; }

/* ── Recouvrement — aperçu du mail de relance (2026-09-13) ─────
   Trois classes qui remplacent quatre `style="…"` en ligne de l'ancienne vue
   `Recovery/Views/recouvrement.php`. Aucune couleur en dur : tout vient de
   `tokens.css`. */
.rel-apercu-entete { display: flex; align-items: center; justify-content: space-between;
                     margin-bottom: .5rem; }
.rel-apercu-corps  { border: 1px solid var(--border); border-radius: var(--radius);
                     padding: .75rem 1rem; font-size: 0.875rem;
                     max-height: 260px; overflow-y: auto; }
.rel-apercu-geste  { text-align: right; margin-bottom: .25rem; }

/* ── Recouvrement — la CELLULE de la colonne (2026-09-13) ──────
   La sous-ligne « quand · qui ». Elle réutilise `.cell-sub` du socle pour le
   pas vertical et la troncature ; ces deux classes-ci ne portent QUE la teinte
   du retard et l'appel au geste manquant.

   ⚠ LE CRAN `-fg` EST L'ENCRE. `--state-red` est une teinte de PASTILLE : posée
   en texte elle tombe à 2,5:1 de contraste. Sur un nombre qu'on lit à la
   volée, c'est le plus gros défaut possible. */
.rec-cell-echu     { color: var(--state-red-fg); font-weight: 600; }
.rec-cell-aplanif  { color: var(--text-muted); font-style: italic; }

/* ── Tenant / Organisation page ─────────────────────────────── */
.tenant-dashboard {
  display: grid;
  grid-template-columns: 320px 1fr;
  gap: var(--space-6);
  align-items: start;
}
@media (max-width: 900px) {
  .tenant-dashboard { grid-template-columns: 1fr; }
}
.tenant-left-column { display: flex; flex-direction: column; gap: var(--space-4); }
.tenant-right-column { min-width: 0; }

/* .logo-section / .logo-preview / .tenant-logo / .logo-placeholder /
   .logo-actions / .logo-recommendations rules live in
   app-core/public/assets/css/components/settings.css (Phase 5d, 2026-05-02). */

.search-section { background: var(--color-surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem; }
.search-controls { display: flex; gap: .5rem; }

.contact-link { display: inline-flex; align-items: center; gap: .4rem; font-size: .85rem; color: var(--text-secondary); text-decoration: none; padding: .25rem .5rem; border-radius: 4px; }
.contact-link:hover { background: var(--color-surface-2); color: var(--text); }
.address-block { display: flex; align-items: flex-start; gap: .5rem; font-size: .85rem; color: var(--text-secondary); font-style: normal; }
.row-item { padding: var(--space-3); border: 1px solid var(--border); border-radius: 6px; margin-bottom: var(--space-2); }

/* Sidebar rules (.sidebar, .sidebar-nav, .sidebar-brand, .app-launcher)
   live in shared app-core/public/assets/css/components/navigation.css.
   Tenant identity + logout moved to the topbar avatar menu (no sidebar
   footer anymore). */

/* App-specific variables */
:root {
  --logo-color-1: var(--primary);
  /* css-color-ok: jaune du logo Corelia, couleur de marque sans token */
  --logo-color-2: #ffbd59;

}

/* Micro-label type role (smaller than .text-xs). One canonical size for the
   sub-labels / meta hints that were scattered across .63–.7rem inline. */
.text-2xs { font-size: 0.6875rem; }

/* Group label inside a dropdown menu (e.g. the "Clôture" section of the
   affaire actions menu). Quiet, uppercase, non-clickable. */
.dropdown-section-label {
  padding: 0.25rem 1rem 0.15rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

/* ── Affaire detail — shared layout primitives ───────────────────────────────
   Replace the repeated inline display:grid/flex blocks in jobs/*.js + jobs.php
   so layout lives in CSS, not in string templates. */
/* Horizontal action toolbar (header button rows, section toolbars). */
.job-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
/* Two equal columns that stack on narrow screens (Récap masonry, settings). */
.job-two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
  align-items: start;
}
.job-two-col > .job-col-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}
@media (max-width: 768px) {
  .job-two-col { grid-template-columns: 1fr; }
}
/* Rentabilité waterfall — 3 equal columns (Prévu / Commandé / Payé). */
.job-waterfall {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
}
@media (max-width: 600px) {
  .job-waterfall { grid-template-columns: 1fr; }
}
/* Compact data tables inside Récap cards (achats par groupe, main d'œuvre,
   heures). One shared style instead of three near-identical inline blocks. */
.job-mini-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.75rem;
}
.job-mini-table th {
  font-weight: 500;
  color: var(--text-muted);
  padding: 0.2rem 0.4rem;
}
.job-mini-table td { padding: 0.3rem 0.4rem; }
.job-mini-table tfoot tr {
  font-weight: 700;
  border-top: 1px solid var(--border);
}
/* Avenant line editor row — template grid cloned by JS (jobs.php). */
.avenant-line-grid {
  display: grid;
  grid-template-columns: 1fr 70px 70px 100px 60px 100px 32px;
  gap: 0.4rem;
  align-items: start;
}

/* Line item row: hide entity-actions button border until hover */
.invoice-item-row .entity-actions-btn,
.quote-item-row .entity-actions-btn {
  border-color: transparent;
}
.invoice-item-row .entity-actions-btn:hover,
.quote-item-row .entity-actions-btn:hover {
  border-color: var(--gray-300);
}

/* ── Quote modal — full width ───────────────────────────────── */
.modal-full {
  width: 95vw;
  max-width: 1400px;
}
@media (max-width: 768px) {
  .modal-full {
    max-width: 95vw;
    margin: var(--space-4);
  }
}

/* ── Quote item cards ───────────────────────────────────────── */

.qi-cards {
  display: flex;
  flex-direction: column;
  gap: .625rem;
}

/* ── Quote header rows ── */
.quote-header-row {
  border-left: 4px solid var(--primary);
  /* css-color-ok: blue-50 fond de selection historique, pas de token */
  background: #eff6ff;
  border-radius: 4px;
  padding: .375rem .75rem .375rem .5rem;
}
.quote-header-row.qi-header-depth-2 { margin-left: 1rem; background: var(--gray-100); border-left-color: var(--gray-400); }
.quote-header-row.qi-header-depth-3 { margin-left: 2rem; background: transparent; border-left-color: var(--gray-300); }

.quote-header-row .header-description { border: none; background: transparent; box-shadow: none; font-weight: 700; padding: .25rem .5rem; }
.quote-header-row .header-description:focus { outline: none; box-shadow: none; }

.quote-header-row .qi-col-actions { visibility: hidden; margin-top: 0; flex-direction: row; align-items: center; }
.quote-header-row:hover .qi-col-actions,
.quote-header-row:focus-within .qi-col-actions { visibility: visible; }

.qi-header-preamble { margin-top: .25rem; padding-top: .25rem; border-top: 1px dashed var(--border); }
.qi-header-preamble textarea.form-input {
  field-sizing: content;
  resize: none;
  background: transparent;
  border: none;
  box-shadow: none;
  font-style: italic;
}

/* ── Card ── */
.qi-card {
  border: 1px solid var(--gray-200);
  border-radius: 8px;
  background: var(--surface);
  padding: .5rem .75rem .625rem;
  transition: box-shadow .15s ease, border-color .15s ease;
}
.qi-card:hover {
  border-color: var(--gray-300);
  box-shadow: 0 1px 4px rgba(0,0,0,.04);
}
.qi-card.qi-card-open {
  border-radius: 8px 8px 0 0;
  border-bottom-color: var(--gray-100);
}

/* Main row */
.qi-row {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
}
.qi-line-num {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--primary);
  color: var(--surface);
  font-size: .65rem;
  font-weight: 700;
  flex-shrink: 0;
  margin-top: 1.55rem; /* align with input centers */
}

/* Columns */
.qi-col {
  display: flex;
  flex-direction: column;
  gap: .2rem;
  min-width: 0;
}
.qi-col-desc  { flex: 1; }
.qi-col-qty   { width: 80px;  flex-shrink: 0; }
.qi-col-pu    { width: 120px; flex-shrink: 0; }
.qi-col-vat   { width: 100px; flex-shrink: 0; }
.qi-col-total { width: 120px; flex-shrink: 0; }
.qi-col-actions {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: .125rem;
  margin-top: 1.45rem; /* align with label+input on line rows */
}

/* Description cell */
.qi-f-desc {
  display: flex;
  align-items: flex-start;
  gap: .375rem;
  min-width: 0;
}
.qi-description-textarea {
  flex: 1;
  min-width: 0;
  resize: none;
  overflow: hidden;
  line-height: 1.4;
}

/* Total display */
.qi-total-value {
  font-weight: 700;
  color: var(--primary);
  white-space: nowrap;
  text-align: right;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
  background: var(--gray-50);
  line-height: 1.4;
}

/* Toggle */
.qi-toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: none;
  border-radius: var(--radius);
  background: none;
  cursor: pointer;
  color: var(--text-secondary);
  transition: transform .15s ease, color .15s ease, background .1s ease;
  flex-shrink: 0;
}
.qi-toggle-btn:hover { background: var(--gray-100); color: var(--text); }
.qi-toggle-btn.qi-toggle-open { transform: rotate(180deg); color: var(--primary); }

/* ── Detail panel ── */
.qi-detail {
  border: 1px solid var(--gray-200);
  border-top: none;
  border-radius: 0 0 8px 8px;
  margin-top: -.625rem;
  background: var(--gray-50);
}
.qi-detail-inner {
  padding: .75rem .875rem;
  display: flex;
  flex-direction: column;
  gap: .75rem;
}

/* Row 1: two groups side by side */
.qi-detail-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .75rem;
}

/* Group (Identification / Rentabilité) */
.qi-detail-group {
  display: flex;
  flex-direction: column;
  gap: .375rem;
}
.qi-detail-group-title {
  display: flex;
  align-items: center;
  gap: .3rem;
  font-size: .65rem;
  font-weight: 700;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: .04em;
  padding-bottom: .25rem;
  border-bottom: 1px solid var(--gray-200);
}
.qi-detail-group-title svg { opacity: .45; }

/* Pricing group accent */
.qi-detail-group-pricing .qi-detail-group-title {
  color: var(--state-blue-fg);
  /* css-color-ok: blue-200 lisere historique (--primary-200 n'existe nulle part), pas de token */
  border-bottom-color: #bfdbfe;
}
.qi-detail-group-pricing .qi-detail-group-title svg { opacity: .6; color: var(--primary); }

/* Detail field grids */
.qi-dg-fields { display: grid; gap: .375rem; }
.qi-dg-3 { grid-template-columns: 80px 1fr 1fr; }
.qi-dg-pricing { grid-template-columns: 1fr 1fr 1fr; }

/* Detail fields */
.qi-df {
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.qi-df-grow { flex: 1; min-width: 0; }
.qi-df input, .qi-df select {
  width: 100%;
}
.qi-df textarea {
  width: 100%;
  resize: vertical;
  min-height: 2.5rem;
}

/* Texts row */
.qi-detail-texts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .75rem;
}

/* Variables list (read-only, typically from Build conversion) */
.qi-detail-vars { margin-top: .75rem; }
.qi-detail-vars .qi-detail-group-title {
  font-size: .7rem;
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--text-secondary);
  margin-bottom: .4rem;
  display: flex;
  align-items: center;
  gap: .35rem;
}
.item-variables-display {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem;
}
.item-variables-display .qi-var-chip {
  background: var(--state-slate-bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: .15rem .5rem;
  font-size: .75rem;
  color: var(--text-primary);
}
.item-variables-display .qi-var-chip strong {
  color: var(--text-secondary);
  font-weight: 500;
  margin-right: .3rem;
}

/* Margin input with % suffix */
.qi-margin-input-wrap {
  position: relative;
  display: flex;
  align-items: center;
}
.qi-margin-input-wrap input { padding-right: 1.5rem; }
.qi-margin-suffix {
  position: absolute;
  right: .4rem;
  font-size: .7rem;
  color: var(--text-secondary);
  pointer-events: none;
}

/* Sell price read-only */
.qi-sell-price-display {
  font-size: .8125rem;
  font-weight: 600;
  color: var(--text);
  padding: .3rem .375rem;
  background: var(--gray-100);
  border-radius: var(--radius);
  line-height: 1.4;
}

/* Margin badge */
.qi-margin-indicator {
  margin-left: auto;
  font-size: .6rem;
  font-weight: 700;
  white-space: nowrap;
  padding: .1rem .35rem;
  border-radius: 3px;
  line-height: 1.2;
}
.qi-margin-indicator:empty { display: none; }
/* css-color-ok: green-100 fond de marge positive, pas de token (--state-green-bg est #d1fae5) */
.qi-margin-indicator.positive { color: var(--success); background: #dcfce7; }
/* css-color-ok: red-50 fond de marge negative, plus clair que --state-red-bg */
.qi-margin-indicator.negative { color: var(--error); background: #fef2f2; }

/* ── Deposit Picker ────────────────────────────────────────── */
.deposit-picker {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface-2);
}
.deposit-picker-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-2);
}
.deposit-picker-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
  font-size: 0.875rem;
}
.deposit-picker-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.deposit-picker-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.deposit-picker-info {
  flex: 1;
  min-width: 0;
}
.deposit-picker-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.8125rem;
  font-weight: 500;
}
.deposit-picker-meta {
  font-size: 0.75rem;
  color: var(--text-secondary);
}
.deposit-picker-source {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-size: 0.75rem;
}
.deposit-picker-source--invoice { color: var(--primary); }
.deposit-picker-source--external { color: var(--text-secondary); }
.deposit-picker-amount {
  flex-shrink: 0;
  width: 7rem;
}
.deposit-picker-amount .form-input {
  font-size: 0.8125rem;
  padding: 0.25rem 0.5rem;
  text-align: right;
}
.deposit-picker-amount .form-input:disabled {
  opacity: 0.4;
}
.deposit-picker-empty {
  padding: var(--space-3);
  text-align: center;
  color: var(--text-secondary);
  font-size: 0.8125rem;
}
.deposit-picker-footer {
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  font-weight: 600;
  font-size: 0.875rem;
}

/* ── Invoice Totals: deduction & net rows ──────────────────── */
.invoice-totals-deduction {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.3rem 0;
  font-size: 0.875rem;
  color: var(--error);
}
.invoice-totals-net {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.6rem 0 0.3rem;
  margin-top: 0.25rem;
  border-top: 2px solid var(--primary);
  font-weight: 700;
}
.invoice-totals-net-amount {
  color: var(--primary);
  font-size: 1.25rem;
}

/* Situation progress slider — custom track with locked zone */
.situation-percent-slider {
  -webkit-appearance: none;
  appearance: none;
  height: 6px;
  border-radius: 3px;
  outline: none;
  cursor: pointer;
  width: 100%;
  background: var(--gray-200);
}
.situation-percent-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--primary);
  cursor: pointer;
  border: 2px solid white;
  box-shadow: 0 1px 3px rgba(0,0,0,.25);
}
.situation-percent-slider::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--primary);
  cursor: pointer;
  border: 2px solid white;
  box-shadow: 0 1px 3px rgba(0,0,0,.25);
}

/* Airfact mark is landscape (~2:1) — cap its width in the sidebar brand box
   so it doesn't stretch edge-to-edge. */
.sidebar-brand-logo svg,
.sidebar-brand-logo img {
  max-width: 140px;
}

/* Quote totals summary card (Quotes view) — extracted verbatim from the inline
   styles in Quotes/Views/quotes.php (Tier-4 UX-debt migration). Byte-identical
   rules, so the rendering is unchanged; the markup just stops hard-coding them. */
.quote-totals {
  min-width: 18rem;
  background: var(--gray-100);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
}
.quote-totals-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.3rem 0;
  font-size: 0.875rem;
  color: var(--text-secondary);
}
.quote-totals-row.is-total {
  padding: 0.6rem 0 0.3rem;
  margin-top: 0.5rem;
  border-top: 1px solid var(--border);
  font-weight: 600;
  color: var(--text);
}
.quote-totals-amount {
  color: var(--primary);
  font-size: 1.125rem;
}

/* Le bloc #jobKpiBar (recette compacte des stat-cards du détail affaire) a été
   supprimé le 2026-08-27 : renderJobKpiBar est un no-op dans toutes les
   maquettes, plus aucun markup ne porte cet id. */

/* ── Cartes détail du cockpit ?v=2 (.job-cockpit) ────────────────────────────
   Motif « carte info label + lignes clé/valeur ». Les jumeaux #contractSummary
   (morts — la carte de synthèse cible #jvJobSummary) ont été retirés le 2026-08-27. */
.job-cockpit .job-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  background: var(--surface);
}
.job-cockpit .job-card-label {
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-muted);
  margin-bottom: 0.35rem;
}
.job-cockpit .job-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.4rem 0;
  font-size: 0.85rem;
}
.job-cockpit .job-sep {
  border-top: 1px solid var(--border);
  margin: 0.25rem 0;
}

/* ── Bannières d'alerte Récap (bord gauche teinté + titre à icône) ───────────
   Motif répété (ARC en retard, livraisons, marge sous cible, dépassement
   d'achats) — remplace les fonds/bordures hex en dur de recap.js. */
.job-cockpit .job-banner {
  margin-bottom: 0.875rem;
  padding: 0.75rem 1rem;
  border-left: 3px solid;
  border-radius: 6px;
}
.job-cockpit .job-banner-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 600;
}
.job-cockpit .job-banner--warning {
  background: var(--state-amber-bg);
  border-left-color: var(--warning);
  /* css-color-ok: yellow-800 historique, plus chaud que --state-amber-fg */
  color: #854d0e;
}
.job-cockpit .job-banner--danger {
  /* css-color-ok: red-50 historique — plus pâle que --state-red-bg */
  background: #fef2f2;
  border-left-color: var(--error);
  color: var(--state-red-fg);
}
.job-cockpit .job-banner-link {
  font-size: 0.8rem;
  text-decoration: underline;
  white-space: nowrap;
  cursor: pointer;
  color: inherit;
}
.job-cockpit .job-banner-chip {
  display: inline-block;
  margin: 0.15rem 0.3rem 0.15rem 0;
  padding: 0.2rem 0.5rem;
  background: var(--state-red-bg);
  color: var(--error);
  border-radius: 3px;
  font-size: 0.8rem;
  font-weight: 600;
}

/* .btn-toggle-group supprimé le 2026-08-28 — les bascules €/% des modales
   acompte / avance / situation utilisent .btn-group (canon app-core). */

/* Affaire « Client » tab — collapsible "Détail des postes du marché" section.
   The summary reads as a panel-title; the chevron rotates when open. */
.marche-postes > summary { user-select: none; }
.marche-postes > summary::-webkit-details-marker { display: none; }
.marche-postes > summary svg { transition: transform .15s ease; flex-shrink: 0; color: var(--text-secondary); }
.marche-postes[open] > summary svg { transform: rotate(90deg); }

/* Tableau du marché — bloc d'actions de ligne : flèches de déplacement puis
   menu ⋮. Même bloc sur un poste (dans sa cellule) et sur un en-tête de
   section (à droite du titre), pour que les deux s'alignent verticalement. */
.marche-row-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 2px;
  flex-shrink: 0;
}

.marche-move {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
}

.marche-move-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 16px;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 3px;
  color: var(--text-muted);
  cursor: pointer;
  transition: all var(--transition);
}

.marche-move-btn:hover:not(:disabled) {
  background: var(--gray-100);
  border-color: var(--gray-300);
  color: var(--text-primary);
}

/* Première / dernière position (et sous-section collée à son parent) : le
   serveur a dit non, la flèche le montre au lieu d'échouer au clic. */
.marche-move-btn:disabled {
  opacity: .25;
  cursor: default;
}

/* ── Marché = éditeur de plan ────────────────────────────────────────────────
   Sections repliables, rail d'insertion, poignée de glissement. Le tableau est
   rendu par le MÊME items.js dans les quatre vues d'affaire (rail / flow / v3 /
   v2) : ces règles les servent toutes les quatre. */

/* En-tête de section. La ligne est un seul <td colspan> : la mise en page vit
   ici, pas en style inline. */
.marche-header-row { background: var(--gray-50); }
.marche-header-row.is-avenant { background: var(--primary-light); }

.marche-header-cell {
  padding: .4rem .75rem;
  text-align: left;
  white-space: normal;
}
.marche-header-row.is-sub .marche-header-cell { padding-left: 1.5rem; }

/* La hauteur de la ligne est fixée par le bloc d'actions (deux flèches
   empilées + le menu ⋮, ~33 px), pas par le titre : on centre donc tout le
   cluster verticalement, chevron et badge compris. En `flex-start` le titre
   restait collé en haut pendant que les actions occupaient toute la hauteur. */
.marche-header-inner {
  display: flex;
  align-items: center;
  gap: .5rem;
}

.marche-header-lead {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}

.marche-header-main { flex: 1; min-width: 0; }

/* Titre de section : centré verticalement (via le cluster), TOUJOURS aligné à
   gauche — il ne suit pas l'alignement d'une colonne, c'est un titre de lot. */
.marche-header-title {
  font-weight: 700;
  font-size: .85rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--primary);
  text-align: left;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: break-word;
}
.marche-header-row.is-sub .marche-header-title {
  font-size: .78rem;
  text-transform: none;
  letter-spacing: normal;
}

/* Le descriptif d'une section est le cahier des charges du lot : contractuel,
   donc jamais tronqué à l'affichage. */
.marche-header-desc {
  font-weight: 400;
  font-size: .8rem;
  color: var(--text-secondary);
  text-transform: none;
  letter-spacing: normal;
  margin-top: .25rem;
  white-space: pre-line;
  word-break: break-word;
}

/* Ligne d'avenant. `--row-bg` SUFFIT : c'est le socle qui peint le `<tr>` à
   partir du jeton (tables.css), et la colonne d'actions collante le copie en
   `inherit`. Pas de `background` ici — il ferait un second endroit où la
   couleur est décidée. */
.marche-line-row.is-avenant {
  --row-bg: var(--primary-light);
  /* Survol DIFFÉRENT du repos — la seule raison qui reste de déclarer ce
     second jeton : la teinte se fonce d'un cran au lieu de retomber sur le
     gris commun, qui effacerait l'appartenance à l'avenant. */
  --row-bg-hover: color-mix(in srgb, var(--primary-light) 92%, var(--primary));
}
.marche-line-row > td:first-child { font-size: .875rem; }

/* Chevron de repli — pivote à l'ouverture, comme celui des <details>. */
.marche-chevron {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 3px;
  color: var(--text-secondary);
  cursor: pointer;
  flex-shrink: 0;
}
.marche-chevron svg { transition: transform .15s ease; }
.marche-chevron.is-open svg { transform: rotate(90deg); }
.marche-chevron:hover { background: var(--gray-100); border-color: var(--gray-300); }

/* Badge discret « 12 postes · 48 500,00 € ». Le total du pied de tableau reste,
   lui, la somme de TOUTES les lignes, repliées comprises. */
.marche-block-badge {
  flex-shrink: 0;
  font-size: .75rem;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--gray-100);
  border-radius: 3px;
  padding: .1rem .45rem;
  white-space: nowrap;
}

/* Repli : une ligne disparaît dès qu'un de ses ancêtres est replié — c'est
   `ancestors`, publié par le serveur, qui en décide. */
.marche-row-hidden { display: none; }

/* Poignée de glissement. Sur un poste elle se superpose au numéro de rang
   (colonne « # » à 44 px, table-layout:fixed) : rien ne se décale au survol. */
.marche-num { position: relative; }
.marche-num-val { transition: opacity .12s ease; }
.marche-line-row:hover .marche-num-val { opacity: 0; }

.marche-grip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  cursor: grab;
  opacity: 0;
  transition: opacity .12s ease;
}
.marche-num .marche-grip {
  position: absolute;
  inset: 0;
}
.marche-line-row:hover .marche-grip,
.marche-header-row:hover .marche-grip,
.marche-grip:focus-visible { opacity: 1; }
.marche-grip:active { cursor: grabbing; }

.marche-dragging { opacity: .4; }

/* Rail d'insertion / indicateur de dépôt — UN SEUL élément flottant, positionné
   au mousemove sur l'interstice le plus proche. `pointer-events:none` sur le
   filet : il ne doit pas voler le survol aux lignes qu'il chevauche.
   z-index au-dessus de la colonne d'actions collante (jusqu'à 1002 menu
   ouvert), sous les modales (2000). */
/* Le filet traverse toute la ligne, colonne d'actions comprise — c'est une
   frontière, elle ne s'interrompt pas. Son z-index le place au-dessus des
   lignes et de la colonne d'actions collante (z-index 1, 2 pour le `th`), mais
   TRÈS EN DESSOUS d'un menu ⋮ ouvert : la cellule passe alors à 1002 et le
   panneau à 1000+, donc le filet leur file dessous au lieu de les barrer. */
.marche-rail {
  position: fixed;
  z-index: 3;
  height: 2px;
  background: var(--primary);
  pointer-events: none;
}

/* Le « + » est centré sur l'interstice, pas accroché au bord du tableau :
   c'est au milieu que le regard est quand on vise une frontière entre deux
   lignes.

   L'anneau blanc n'est pas décoratif : le bouton et le filet sont tous deux en
   `var(--primary)`, donc sans lui leurs aplats fusionnent et le bouton se lit
   comme une grosseur sur le trait au lieu d'une pastille posée dessus. Il
   interrompt visuellement la ligne juste autour du « + ». */
.marche-rail-add {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: -10px;
  z-index: 1;
  box-sizing: border-box;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 2px solid var(--surface);
  border-radius: 50%;
  background: var(--primary);
  color: var(--surface);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  pointer-events: auto;
  box-shadow: 0 1px 4px rgb(0 0 0 / .25);
}

/* Pendant un glissement le rail n'est plus qu'un indicateur de dépôt. */
.marche-rail.is-drop .marche-rail-add { display: none; }

/* Mini-popover du rail : Poste ou Section, l'utilisateur tranche là où il a
   cliqué — le rail ne présume jamais de ce qu'on insère. */
.marche-insert-pop {
  position: fixed;
  z-index: 1006;
  display: flex;
  flex-direction: column;
  min-width: 150px;
  padding: .25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: 0 8px 28px rgb(0 0 0 / .16);
}

.marche-insert-choice {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  padding: .4rem .6rem;
  background: none;
  border: none;
  border-radius: var(--radius);
  color: var(--text-primary);
  font-size: .85rem;
  text-align: left;
  cursor: pointer;
}
.marche-insert-choice:hover,
.marche-insert-choice:focus-visible { background: var(--gray-100); }

/* Rentabilité card — collapsible detail (Récap). The summary reads as a quiet
   link; only the "Marge estimée" headline shows until expanded. */
.job-renta-detail > summary {
  cursor: pointer;
  color: var(--primary);
  user-select: none;
}
.job-renta-detail > summary:hover { text-decoration: underline; }

/* "Points d'attention" — fold extra banners beyond the first two. */
.job-attention-more > summary {
  cursor: pointer;
  color: var(--text-secondary);
  user-select: none;
  padding: 0.25rem 0;
}
.job-attention-more > summary:hover { color: var(--text); }

/* Danger-zone card (delete affaire, etc.) — red border + red title, no inline
   colour overrides in the JS. */
.card--danger { border-color: var(--error); }
.card--danger .card-title,
.card--danger .card-danger-title { color: var(--error); }

/* @-mention highlight in the affaire discussion thread. */
.md-content .mention { color: var(--primary); font-weight: 600; }

/* ════════════════════════════════════════════════════════════════════════════
   Affaire — détail v2 (cockpit). Behind ?v=2. Reuses the .job-card / .job-banner
   primitives (selectors broadened above) + the cockpit-specific layout below.
   Design tokens only (var(--…)) — no hardcoded colours in the v2 layer.
   ════════════════════════════════════════════════════════════════════════════ */

.job-cockpit { margin-bottom: 0.25rem; }

/* Topline — status badge (changeable) + reference + Build link */
.job-cockpit-topline {
  display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;
  margin-bottom: 0.9rem;
}
.job-cockpit-status { cursor: pointer; }

/* Funnel — 4 money cards, slightly denser than the default stat-card */
.job-cockpit-funnel { margin-bottom: 1rem; }
.job-cockpit-funnel .dash-stat-card { min-width: 190px; }
.job-cockpit-funnel .dash-stat-value { font-size: 1.3rem; }
.job-cockpit-funnel .dash-stat-label { font-size: 0.7rem; }
.job-amount-warning { color: var(--warning); font-weight: 600; }
.job-amount-success { color: var(--success); font-weight: 600; }

/* Points d'attention */
.job-cockpit-attention { margin-bottom: 1rem; }
.job-attention-more-body { margin-top: 0.5rem; }
.job-banner-list { list-style: none; margin: 0.35rem 0 0; padding: 0; font-size: 0.8rem; }
.job-banner-list li { margin: 0.15rem 0; }
.job-banner-itemlink { color: inherit; text-decoration: underline; }
.job-banner-chips { margin-top: 0.4rem; }

/* Rentabilité — collapsible (masqué par défaut) */
.job-cockpit-renta { margin-top: 0.25rem; }
.job-cockpit-renta-summary {
  cursor: pointer; user-select: none; color: var(--primary);
  font-size: 0.875rem; display: flex; align-items: center; gap: 0.4rem;
  list-style: none; padding: 0.4rem 0;
}
.job-cockpit-renta-summary::-webkit-details-marker { display: none; }
.job-cockpit-renta-summary svg { transition: transform 0.15s ease; flex-shrink: 0; }
.job-cockpit-renta[open] > .job-cockpit-renta-summary svg { transform: rotate(90deg); }
.job-cockpit-renta-body {
  display: grid; grid-template-columns: 3fr 2fr; gap: 1rem;
  align-items: start; padding-top: 0.6rem;
}
.job-cockpit-renta-body > .job-card:first-child { grid-row: span 2; }
@media (max-width: 900px) { .job-cockpit-renta-body { grid-template-columns: 1fr; } }

.job-renta-header { margin-bottom: 0.6rem; }

/* Waterfall columns (Prévu / Commandé / Payé) */
.job-renta-col {
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 0.65rem 0.8rem; background: var(--surface); min-width: 0;
}
.job-renta-col-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.3rem; }
.job-renta-col-label { font-size: 0.63rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); }
.job-renta-achats { margin: 0.1rem 0; }
.job-renta-modline { margin: 0.1rem 0; color: var(--text-secondary); }
.job-renta-col-sep { border-top: 1px solid var(--border); margin: 0.35rem 0; }
.job-renta-col-marge { font-size: 1rem; font-weight: 700; white-space: nowrap; }
.job-renta-nomod { margin-bottom: 0.5rem; }
.job-renta-cascade-sep { margin-top: 0.75rem; }

/* Mini-table blocks (achats par groupe, main d'œuvre) */
.job-renta-block { margin-top: 0.75rem; padding-top: 0.6rem; border-top: 1px dashed var(--border); }
.job-renta-block-title { font-size: 0.63rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 0.35rem; }
.job-th-left { text-align: left; font-weight: 500; }
.job-th-right { text-align: right; font-weight: 500; }
.job-num-cell { text-align: right; white-space: nowrap; }
.job-num-bold { font-weight: 700; }
.job-num-muted { color: var(--text-secondary); }
.job-num-danger { color: var(--error); font-weight: 600; }
.job-num-warning { color: var(--warning); font-weight: 600; }
.job-renta-foot { font-weight: 700; border-top: 1px solid var(--border); }
.job-row-muted { color: var(--text-muted); font-style: italic; }
.job-group-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 0.35rem; vertical-align: middle; flex-shrink: 0; }

/* Heures card */
.job-hours-table { margin-top: 0.25rem; }
.job-hours-other { margin-top: 0.4rem; }
.job-hours-foot { margin-top: 0.5rem; }

/* Projets Build liés (cockpit summary) */
.job-build-list { list-style: none; margin: 0; padding: 0; }
.job-build-li { display: flex; align-items: flex-start; gap: 0.6rem; padding: 0.5rem 0; border-top: 1px solid var(--border); }
.job-build-main { flex: 1; min-width: 0; }
.job-build-link { color: var(--primary); text-decoration: none; font-weight: 600; font-size: 0.875rem; }

/* Détail des postes — collapsible (v2 Facturation tab) */
.job-postes > summary { user-select: none; cursor: pointer; list-style: none; display: flex; align-items: center; gap: 0.4rem; }
.job-postes > summary::-webkit-details-marker { display: none; }
/* ⚠ LE CHEVRON, ET LUI SEUL — d'où le `>`. Ces deux règles visaient tout `svg`
   DESCENDANT du titre. Elles ne touchaient rien tant que le titre ne portait
   qu'un chevron ; depuis que la barre d'outils y vit (14-09-2026), un
   descendant non scopé aurait tourné de 90° CHAQUE icône de la barre et de son
   menu (＋, ⋮, dossier, PDF, tableur) dès que le bloc est ouvert — c'est-à-dire
   par DÉFAUT. Une rotation ne rougit pas : elle se lit comme une icône mal
   dessinée. */
.job-postes-summary > svg { transition: transform 0.15s ease; flex-shrink: 0; color: var(--text-secondary); }
.job-postes[open] > .job-postes-summary > svg { transform: rotate(90deg); }
.job-postes-body { padding-top: 0.6rem; }

/* Onglet Fournisseurs — sections par groupe d'achat + factures dépliées sous
   leur BC (P1+P2, 2026-08-28). Fond = --bg : la ligne de section et la
   sous-ligne se distinguent du corps sans couleur de composant. */
.achats-group-row { --row-bg: var(--bg); }
.achats-group-row td { cursor: pointer; user-select: none; }
.achats-group-row .achats-group-chevron { display: inline-flex; vertical-align: -2px; color: var(--text-secondary); }
.achats-group-row .achats-group-chevron svg { transition: transform 0.15s ease; }
.achats-group-row.is-collapsed .achats-group-chevron svg { transform: rotate(-90deg); }
.achats-expander-cell { padding-right: 0; }
.achats-expander { display: inline-flex; align-items: center; border: 1px solid var(--border); border-radius: 4px; background: var(--surface); color: var(--text-secondary); padding: 0.15rem 0.25rem; cursor: pointer; }
.achats-expander svg { transition: transform 0.15s ease; }
.achats-expander.is-open svg { transform: rotate(90deg); }
/* Sous-ligne de facture. UNE déclaration : le socle peint le `<tr>` depuis le
   jeton et la colonne d'actions collante le copie. Posée cellule par cellule
   (`.achats-sub-row td { background }`), la teinte laissait la DERNIÈRE
   blanche — le menu ⋯ au bout d'une ligne grise, vu le 2026-09-13 sur
   /jobs/35#fournisseurs. Pas de `--row-bg-hover` : sans lui le survol retombe
   sur `--row-bg`, donc la ligne garde sa teinte. */
.achats-sub-row { --row-bg: var(--bg); }
.achats-sub-row .achats-sub-indent { display: inline-flex; align-items: center; gap: 0.3rem; padding-left: 1.4rem; color: var(--text-secondary); }
.achats-sub-row .achats-sub-indent a { color: var(--primary); text-decoration: none; }
/* LA BARRE D'OUTILS DES POSTES VIT SUR LA LIGNE DE TITRE (14-09-2026, demande
   Olivier : « remonter les boutons »). `.job-postes-head` — la rangée « aide à
   gauche / barre à droite » qui vivait sous le titre — n'existe plus : ses
   trois règles ne servaient qu'à réparer un débordement que la barre ne peut
   plus produire. Elle mesurait 662 px une fois « Tout replier » dévoilé, contre
   366 px d'aide, et débordait de 12 px à 1440 ; elle en fait moins de 200
   depuis que ses cinq boutons sont devenus « Ajouter un poste » + un débord ⋮
   (partials/_marche-postes-toolbar.php).
   `margin-left: auto` reste le seul ancrage à droite qui tienne quand la ligne
   se replie pour de bon — `justify-content: space-between` n'a plus rien à
   répartir sur une ligne seule, et collait la barre à GAUCHE. */
.job-postes-summary { flex-wrap: wrap; row-gap: 0.4rem; }
.job-postes-tools { margin-left: auto; }
/* MÊME IDIOME, onglet Facturation (14-09-2026) : la barre des gestes vit sur la
   ligne de titre de la chronologie. `.panel-title` est DÉJÀ un flex du socle —
   il n'y a donc qu'un ancrage à poser, pas une disposition à refaire. */
.jv-factu-titre { flex-wrap: wrap; row-gap: 0.4rem; }
.jv-factu-tools { margin-left: auto; }
/* Le titre est cliquable de bout en bout (`cursor: pointer` sur le
   `<summary>`) ; la barre, elle, ne replie rien — voir la garde de
   jobs/marche-outline.js. Le curseur doit le dire AVANT le clic. */
.job-postes-summary > .job-postes-tools { cursor: default; }
/* L'aide reste, seule, en tête du corps — plus de rangée à partager. */
.job-postes-help { margin: 0 0 0.6rem; }

/* ═══════════════════════════════════════════════════════════════════════════
   Affaire — détail V3 (refonte). Behind ?v=3.
   100 % composants CANONIQUES (dash-stats-row, dash-stat-card, dash-card /
   dash-alert-card, .dash-table, badge). Ici uniquement le strict nécessaire qui
   n'existe pas déjà dans le design system.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Réglages : onglet présent mais nav-button masqué (piloté par l'engrenage). */
#progressDetailTabs .nav-tab[data-tab="parametres"] { display: none; }

/* Ligne d'identité — badge statut cliquable + référence + pastille Build. */
.job3-idrow {
  display: flex; align-items: center; gap: var(--space-3);
  flex-wrap: wrap; margin-bottom: var(--space-4);
}
.job3-idrow .badge[role="button"] { cursor: pointer; }
.job3-ref { font-size: 0.8125rem; color: var(--text-muted); display: inline-flex; align-items: center; gap: 0.25rem; }

/* Marge en carte : la valeur secondaire (%) discrète à côté du montant. */
.dash-stat-value .job3-rate { font-size: 0.95rem; font-weight: 600; opacity: 0.75; margin-left: 0.25rem; }

/* Tableau Rentabilité 3 colonnes (Prévu/Commandé/Payé) — réutilise dash-stats-row. */
.job3-renta-cols { margin-bottom: var(--space-4); }
.job3-renta-foot { font-weight: 600; }

/* Titres de section pour la profondeur du Suivi (listes réutilisées). */
.job3-depth-title { margin-top: var(--space-8); margin-bottom: var(--space-3); }

/* ═══════════════════════════════════════════════════════════════════════════
   Affaire — onglet Synthèse : compte d'exploitation (jbg = job budget grid).
   TROIS tableaux séparés (Chiffre d'affaires · Achats · Main-d'œuvre) fermés
   par le bandeau « Marge ». Ils partagent la même grille : table-layout fixed
   + colgroup identique, donc les colonnes chiffrées restent alignées d'un
   tableau à l'autre alors que chaque bloc garde ses propres en-têtes.
   ═══════════════════════════════════════════════════════════════════════════ */
.jbg-sect { margin-top: var(--space-6); }
.jbg-sect:first-child { margin-top: 0; }
.jbg-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-2);
}
.jbg-head .panel-title { margin: 0; }
.jbg-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.jbg-actions .btn { font-size: 0.78rem; }
.jbg-hint { font-size: 0.72rem; color: var(--text-muted); margin: 0 0 var(--space-2); }
.jbg-empty { margin: 0; }
.jbg-wrap { margin: 0; }

/* min-width : sous cette largeur le .table-container scrolle au lieu d'écraser
   les colonnes (table-layout: fixed ne sait pas se rétrécir proprement).
   Sélecteurs doublés .table.jbg-table : `.table th` (0,1,1) et `table td`
   l'emportent sinon sur une classe seule — alignement et padding sautent. */
/* ⚠ CINQ COLONNES DE CHIFFRES depuis le 12-09-2026 (« % » en plus) : les
   largeurs ont été RESSERRÉES pour que la somme tienne dans la colonne
   principale à 1280 px en mode rail (≈ 990 px utiles). Les élargir sans bouger
   `min-width` ne casse rien tout de suite — le tableau se met simplement à
   scroller horizontalement sur les écrans moyens, ce qui ne se voit qu'à 1024. */
/* ⚠ RESSERRÉ ENCORE LE 12-09-2026, pour la fusion Synthèse + Finance : le
   tableau n'a plus toute la largeur de la page mais la colonne de DROITE, soit
   ≈ 610 px à 1280 avec la barre latérale dépliée. Le « € » retiré des cellules
   avait déjà rendu de la place ; ces largeurs-ci sont les plus petites où
   AUCUN nombre ne se coupe (vérifié par la sonde à cinq largeurs). */
/* ⚠ RESSERRÉ UNE TROISIÈME FOIS LE 12-09-2026 (soir), et c'est la contrepartie
   directe des centimes retirés : « 329 584,46 » demandait 100 px, « 329 584 »
   en demande 72. Les 64 px rendus vont à la colonne des LIBELLÉS, qui coupait
   (« Total des coûts dire… », « Prestation de Servi… ») — c'est-à-dire au seul
   endroit du tableau où l'on perdait de l'information. */
.table.jbg-table { table-layout: fixed; width: 100%; min-width: 570px; }
.table.jbg-table col.jbg-c-num { width: 88px; }
.table.jbg-table col.jbg-c-var { width: 84px; }
.table.jbg-table col.jbg-c-pct { width: 62px; }
/* ── LA DENSITÉ, ET QUI PASSE DEVANT ───────────────────────────────────────
   ⚠ DANS UN ÉTAT FINANCIER, LE SUJET EST LE NOMBRE. Le libellé nomme la ligne,
   il ne se lit qu'une fois ; la colonne de chiffres, elle, se parcourt. Les
   deux étaient au même ton et à la même taille, et le regard se posait sur les
   mots. Le nombre prend l'encre la plus foncée du socle, le libellé descend
   d'un cran — ce qui, au passage, laisse les TOTAUX ressortir en gras sans
   avoir à les grossir. */
.table.jbg-table th, .table.jbg-table td { padding: 0.42rem 0.55rem; }
.table.jbg-table td { font-size: 0.85rem; }
.table.jbg-pl tbody td.jbg-poste { color: var(--text-secondary); font-size: 0.82rem; }
.table.jbg-pl tbody td.jbg-num   { color: var(--text-primary); }
/* ⚠ `.jbg-cts` A DISPARU AVEC LES CENTIMES (12-09-2026, soir). Les montants du
   compte de résultat sont arrondis à l'euro (`_jbgEur`, `decimals: 0`) : il n'y
   a plus de partie décimale à mettre en retrait. La règle part avec le balisage
   — une classe qui ne peint plus rien re-décore le premier qui la réutilise. */

/* ── LE RETRAIT : LA MARCHE D'ESCALIER D'UN COMPTE DE RÉSULTAT ─────────────
   Un détail est SOUS son sous-total, pas à côté. `td.jbg-poste` ne porte que
   les lignes de détail — les totaux rendent un `<td>` nu, les sections un
   `<td colspan>` — donc le retrait se pose là, une fois, sans classe à
   distribuer sur cinq sites de rendu. */
.table.jbg-pl tbody td.jbg-poste { padding-left: 1.5rem; }
.table.jbg-table th.jbg-num, .table.jbg-table td.jbg-num {
  text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums;
}
.table.jbg-table td.jbg-poste { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jbg-code { color: var(--text-muted); font-size: 0.75rem; }
.jbg-muted { color: var(--text-muted); }
.jbg-secondary { color: var(--text-secondary); }
.jbg-danger { color: var(--error); }
.jbg-ok     { color: var(--state-pine-dot); }

/* ── LE COMPTE DE RÉSULTAT DE L'AFFAIRE ───────────────────────────────────
   « Un peu plus comme un P&L financier », puis « une UI plus reporting
   financier / tableaux financiers » (Olivier, 12-09-2026). C'étaient TROIS
   tableaux indépendants, chacun repartant de ses propres en-têtes. Un compte de
   résultat DESCEND — produits, charges, résultat — et chaque ligne se lit dans
   les mêmes colonnes que celle du dessus ; les sous-totaux s'enchaînent au lieu
   de fermer trois fois.

   LA GRAMMAIRE VISUELLE EST CELLE D'UN ÉTAT FINANCIER, ET ELLE TIENT EN UNE
   RÈGLE : ON RANGE PAR DES FILETS, PAS PAR DES FONDS. Le tableau a QUATRE
   niveaux — poste · sous-total · total · résultat — et un fond n'en distingue
   que deux (peint / pas peint). Six lignes de total peintes du même gris
   (`.table-totals`) ne hiérarchisaient plus rien : « Total achats » pesait
   autant que la marge. Chaque niveau a donc son trait, du plus fin au plus
   fort, et le dernier est DOUBLE — le soulignement de la dernière ligne, qui
   n'existe qu'une fois par état.
   ⚠ C'est la seule raison pour laquelle ce tableau n'utilise pas le composant
   central `.table-totals`. Pas un dialecte de plus : un rangement sur quatre
   crans, que le composant central ne sait pas faire.

   ⚠ LES SECTIONS SONT DES LIGNES, PAS DES TITRES DE BLOC : un `<h3>` hors du
   tableau romprait l'alignement des colonnes à chaque changement de section,
   qui est précisément ce que le lecteur suit du regard. */

/* ⚠ LE RAPPORT EST UN CONTENEUR DE REQUÊTE, et ce n'est pas un détail : la
   largeur dont ce tableau dispose ne se déduit PAS de celle de la fenêtre. La
   barre latérale prend 250 px au repos et 68 px en mode rail — à 1024 px de
   fenêtre, ce tableau a 726 px dans un cas et 908 px dans l'autre. Un `@media`
   se tromperait dans l'un des deux, toujours. (Même raison que `.jv-rangee`.) */
.jbg-rapport { container-type: inline-size; }

/* ── LE RAPPORT EST UNE FEUILLE, PAS UNE CARTE ─────────────────────────────
   Le cartouche flottait au-dessus d'un tableau encarté : deux objets, un titre
   et une carte, pour un seul document. Ils tiennent désormais sur le même
   papier — titre, colonnes, légende — et c'est le cadre extérieur qui porte le
   bord. Le `.table-container` garde son `overflow-x`, il perd son dessin. */
.jbg-rapport {
    background: var(--surface); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: var(--shadow);
    /* Le rembourrage LATÉRAL est plus serré que le vertical : chaque pixel pris
       ici est un pixel de moins pour les cinq colonnes de chiffres. */
    padding: var(--space-5) var(--space-4) var(--space-4);
}
/* ⚠ DANS LA CARTE DE SYNTHÈSE, LE RAPPORT N'EST PAS UNE SECONDE CARTE. Un
   cadre dans un cadre, c'est deux bordures et deux rembourrages pour un seul
   objet — et ici les 40 px de rembourrage latéral sont exactement ce qui
   manquait aux cinq colonnes de chiffres pour tenir sans ascenseur. Il garde
   son cartouche et ses filets : c'est eux qui en font un état financier, pas
   sa bordure. */
/* ⚠ DESCENDANT, PAS ENFANT DIRECT : le rapport vit dans une enveloppe
   (`.jbg-rapport-hote`), au bout de la zone d'état. Un `>` ne matcherait rien,
   et les 40 px de rembourrage qu'on croit rendre aux colonnes de chiffres y
   resteraient — mesuré : 569 px utiles pour un tableau qui en réclame 604, il
   se remet à défiler. */
.jv-zone--etat .jbg-rapport {
    background: none; border: 0; border-radius: 0; box-shadow: none; padding: 0;
}
.jbg-rapport-hote { margin-top: var(--space-6); min-width: 0; }
.jbg-rapport .jbg-pl-wrap {
    background: none; border: 0; border-radius: 0; box-shadow: none;
}

/* Le CARTOUCHE — ce qu'on arrête, à quelle date, dans quelle unité. */
.jbg-rapport-head {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-3);
}
.jbg-rapport-titre {
    margin: 0; font-size: 1rem; font-weight: 700; color: var(--text-primary);
    letter-spacing: 0.01em;
}
.jbg-rapport-meta { margin: 2px 0 0; font-size: 0.75rem; color: var(--text-muted); }

/* L'en-tête de colonnes d'un état financier est un TRAIT, pas une bande : le
   seul fond peint du tableau attirait l'œil en haut, là où il n'y a que des
   étiquettes. Le trait, lui, ouvre la colonne de chiffres. */
.table.jbg-pl thead th {
    background: none; border-bottom: 1px solid var(--gray-300);
    color: var(--text-muted);
}

.table.jbg-table tr.jbg-pl-section td {
    padding: var(--space-5) 0.6rem 0.35rem; border-bottom: 1px solid var(--border);
    font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
    color: var(--text-muted);
}
.table.jbg-table tr:first-child.jbg-pl-section td { padding-top: 0.35rem; }
/* ⚠ JAMAIS `display: flex` SUR UN `<td>` : la cellule sort de l'algorithme de
   tableau, cesse d'honorer son `colspan` et se rétrécit à son contenu — le filet
   de la sous-section s'arrêtait au milieu de la ligne. La rangée flex est un
   `<div>` À L'INTÉRIEUR. (Même règle que pour un `<th>`, déjà écrite au socle.) */
.table.jbg-table tr.jbg-pl-sub td {
    padding: var(--space-4) 0.6rem 0.25rem; font-weight: 600; color: var(--text);
}
.jbg-pl-subrow { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); }
.jbg-pl-sub .jbg-actions { display: inline-flex; gap: var(--space-2); }

/* ── LES QUATRE CRANS ──────────────────────────────────────────────────────
   1. poste       : rien (la rangée nue) ;
   2. sous-total  : filet fin + demi-gras                     (.jbg-pl-total) ;
   3. total       : filet plein sombre + gras                 (+ .jbg-pl-fort) ;
   4. résultat    : gras, et la DERNIÈRE ligne double-soulignée (.jbg-pl-resultat
                    / .is-finale).
   Le fond reste blanc partout : c'est le papier. */
/* ⚠ UN FILET DE 1 px NE SE VOIT PAS DANS UN TABLEAU EN `border-collapse`.
   Le socle trace déjà une ligne sous chaque rangée ; à largeur ÉGALE, c'est la
   bordure du HAUT (celle de la rangée du dessus) qui gagne la fusion, et le
   filet du sous-total est purement et simplement remplacé par la hairline
   ordinaire. Mesuré : `border-top-width` calculé à 0 px sur « Total achats »,
   c'est-à-dire un sous-total sans filet, dans un tableau dont toute la
   hiérarchie repose sur ses filets. La fusion se gagne par la LARGEUR : 2 px
   pour un sous-total, 3 px pour un total. Ne les ramenez pas à 1 px. */
.table.jbg-table tr.jbg-pl-total td {
    border-top: 2px solid var(--gray-300); font-weight: 600; color: var(--text-primary);
}
/* ⚠ UN TOTAL COLLÉ SOUS SON TITRE DE SECTION N'A RIEN À SÉPARER. Le titre pose
   déjà un filet sous lui ; le total en pose un au-dessus de lui — deux traits à
   trois pixels d'écart, sur la section « Produits » d'une affaire sans avenant
   (le cas le plus courant), où le marché est à la fois la seule ligne et le
   total. Les deux `<tr>` sont frères dans le même `<tbody>` : l'adjacence est
   réelle ici, contrairement à celle qui prétend traverser deux hôtes. */
.table.jbg-table tr.jbg-pl-section + tr.jbg-pl-total td { border-top: 0; }
.table.jbg-table tr.jbg-pl-fort td {
    border-top: 3px solid var(--gray-400); font-weight: 700;
}
/* ── UN SOLDE INTERMÉDIAIRE N'EST NI UN TOTAL NI UN RÉSULTAT ───────────────
   « Marge sur achats » ne totalise rien : elle RETRANCHE. Elle se lit entre
   deux sous-sections, et le lecteur doit voir tout de suite qu'elle change de
   nature — sinon elle passe pour un troisième total d'achats. Elle emprunte
   donc la typographie des TITRES de section (petites capitales espacées, qui
   disent « on change de registre ») avec l'encre et le gras d'un total, qui
   disent « ceci est un montant qui compte ». */
.table.jbg-table tr.jbg-pl-marge td {
    border-top: 0; padding-top: 0.6rem; padding-bottom: 0.6rem;
    font-weight: 700; color: var(--text-primary);
}
.table.jbg-table tr.jbg-pl-marge td:first-child {
    font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em;
}

.table.jbg-table tr.jbg-pl-resultat td {
    border-top: 0; font-weight: 700; color: var(--text-primary);
    font-size: 0.95rem; padding-top: 0.55rem; padding-bottom: 0.55rem;
}
/* Le double filet ne souligne QUE la dernière ligne chiffrée de l'état. S'il y
   en avait deux, il ne dirait plus « c'est fini ». */
.table.jbg-table tr.jbg-pl-resultat.is-finale td { border-bottom: 3px double var(--gray-400); }
/* La note qui commente le résultat — sous le double filet, donc hors de l'état. */
.table.jbg-table tr.jbg-pl-glissement td {
    border-top: 0; padding-top: 0.45rem; font-size: 0.8rem; font-weight: 400;
}
.table.jbg-table tr.jbg-pl-glissement td:first-child { color: var(--text-muted); }

/* La colonne « % » se lit EN DESCENDANT : plus discrète que les euros qu'elle
   qualifie, mais alignée comme eux. */
/* ⚠ « 100,0 % » EST LA VALEUR LA PLUS LARGE DE CETTE COLONNE, et c'est celle
   d'un poste consommé à ras — donc fréquente, et la plus importante à lire. La
   piste est calée dessus : à 62 px elle la coupait (mesuré sur l'affaire 309,
   trois postes à 100 %). Le rognage d'un padding est ce qui permet de la tenir
   sans reprendre 8 px aux colonnes d'euros. */
.table.jbg-table th.jbg-num:last-child,
.table.jbg-table td.jbg-pct { padding-left: 0.3rem; }
.table.jbg-table td.jbg-pct { color: var(--text-secondary); font-size: 0.8rem; }
.table.jbg-table tr.jbg-pl-total td.jbg-pct,
.table.jbg-table tr.jbg-pl-resultat td.jbg-pct { font-size: 0.85rem; color: inherit; }

/* ── LES FILETS DE RANGÉE S'EFFACENT ──────────────────────────────────────
   Le socle trace une ligne sous CHAQUE rangée. Dans un tableau de liste c'est
   ce qui guide l'œil ; dans un état financier, vingt filets identiques mettent
   la rangée « Transport » au même rang que « Total des charges » et écrasent la
   hiérarchie qu'on vient de construire au-dessus. Ils ne disparaissent pas pour
   autant — sans aucun guide, suivre une ligne sur 900 px de large est pénible :
   ils passent au gris le plus clair du socle, assez pour guider, trop peu pour
   ranger. Les traits qui RANGENT restent les quatre crans ci-dessus. */
.table.jbg-pl tbody td { border-bottom-color: var(--gray-100); }
.table.jbg-pl tbody tr.jbg-pl-total td,
.table.jbg-pl tbody tr.jbg-pl-fort td,
.table.jbg-pl tbody tr.jbg-pl-resultat td,
.table.jbg-pl tbody tr.jbg-pl-section td { border-bottom-color: var(--border); }

/* L'APPEL DE NOTE et sa légende — la précision d'audit (« budget corrigé »)
   quitte la ligne, où elle se répétait neuf fois, pour le bas du tableau. */
/* ⚠ `line-height: 0` N'EST PAS UN ORNEMENT. Un `<sup>` hérite de la hauteur de
   ligne de sa rangée et la REHAUSSE : les rangées portant un appel de note
   faisaient 39 px, les autres 37. Le pas vertical constant est précisément ce
   que cette refonte est allée chercher — un appel de note qui le casse rend le
   remède pire que le mal. (Le `<sup>` reste un `<sup>` : c'est le balisage qui
   dit « ceci renvoie à une note », pas la position.) */
.jbg-note {
    color: var(--text-muted); font-size: 0.7em; line-height: 0; margin-left: 1px; cursor: help;
}
.jbg-legende {
    margin: var(--space-2) 0 0; font-size: 0.72rem; color: var(--text-muted);
}

/* ── LE SURVOL NE PEINT QUE LES LIGNES DE DÉTAIL ───────────────────────────
   Le socle peint `.table tbody tr:hover` — donc AUSSI les titres de section et
   les lignes de total, qui se retrouvaient sur fond gris au passage de la
   souris : le fond qu'on vient de retirer à la hiérarchie revenait par le
   survol. Ici il ne sert qu'à une chose, suivre une ligne de détail sur 900 px
   de large jusqu'à sa colonne. Les rangées de détail sont celles qui portent un
   `td.jbg-poste`. */
.table.jbg-pl tbody tr:hover { background: none; }
.table.jbg-pl tbody tr:has(td.jbg-poste):hover { background: var(--gray-50); }

/* Un montant négatif : entre parenthèses (cf. `_jbgMontant`) ET en rouge. */
.jbg-neg { color: var(--error); }
.jbg-warning { color: var(--warning); }
.jbg-success { color: var(--success); }
.jbg-strong { font-weight: 600; }
.jbg-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.5rem; vertical-align: middle; }
/* ── UN BUDGET SE SAISIT DANS SA CELLULE ──────────────────────────────────
   ⚠ TROIS SIGNES, ET PAS UN DE PLUS. Une valeur modifiable qui ne ressemble à
   rien ne se découvre jamais ; une valeur modifiable qui crie transforme un
   état financier en formulaire. Même grammaire que la cellule éditable du
   socle (`.cell-edit`, 10-09) :
     · PERMANENT — un filet pointillé sous le nombre, rien d'autre ;
     · au SURVOL DE LA RANGÉE — le cadre complet apparaît ;
     · au FOCUS — il passe au bleu de la marque, sur fond blanc.
   La phrase du cartouche (« les budgets se modifient dans le tableau ») fait le
   reste : un pointillé ne se remarque qu'une fois qu'on sait le chercher. */
.jbg-saisie {
  width: 100%; box-sizing: border-box;
  border: 1px solid transparent; border-bottom: 1px dashed var(--gray-200);
  border-radius: var(--radius-sm); background: none;
  /* ⚠ LES MARGES NÉGATIVES ABSORBENT LES BORDURES. Un champ borné de 1 px en
     haut et en bas fait 2 px de plus qu'un nombre en texte : la rangée qui en
     porte un était plus haute que celle qui n'en porte pas (37 px contre 35,
     mesuré sur la ligne « À ventiler », la seule sans budget saisissable). Le
     pas vertical constant, c'est tout ce qu'on est allé chercher deux fois. */
  padding: 0 3px; margin: -1px -3px;
  font: inherit; color: inherit; text-align: right;
  font-variant-numeric: tabular-nums;
}
.table.jbg-pl tbody tr:hover .jbg-saisie { border-color: var(--border); border-bottom-style: solid; }
.jbg-saisie:hover, .jbg-saisie:focus {
  border-color: var(--primary); border-bottom-style: solid;
  background: var(--surface); outline: none;
}
/* Écriture en vol : la valeur est à l'écran, le serveur n'a pas encore répondu. */
.jbg-saisie.is-saving { opacity: 0.5; }
/* La sous-ligne d'heures devient un champ : elle garde sa taille de sous-ligne,
   sinon la cellule change de hauteur selon qu'elle est éditable ou non. */
.jbg-sub .jbg-saisie { font-size: inherit; }

/* ── UNE VALEUR QUI SE MODIFIE LÀ OÙ ELLE SE LIT (`.jv-edit`) ───────────
   Le déclencheur d'édition en place de la fiche affaire : jalon « Signature »,
   réception, référence marché (carte de synthèse) et responsable / activité
   (sous-titre de page). Le mécanisme est dans `window.jobEditable`
   (jobs/settings.js) ; ici, seulement sa forme.

   ⚠ TROIS SIGNES, ET PAS UN DE PLUS — la MÊME grammaire que `.jbg-saisie`
   (compte de résultat) et que `.cell-edit` du socle (liste des affaires), parce
   que c'est le même geste à trois endroits :
     · PERMANENT — un filet pointillé sous la valeur, rien d'autre ;
     · au SURVOL DU BLOC — le cadre se dessine ;
     · au FOCUS — il passe au bleu de la marque, sur fond de surface.

   ⚠ UN <button> QUI DOIT RESSEMBLER À SON VOISIN. Sans `font: inherit`, la
   valeur d'un jalon (1,15 rem / 700 / chiffres tabulaires) retombe au style de
   bouton du navigateur — 13,33 px, graisse normale, chasse proportionnelle : la
   rangée de quatre jalons se désaligne entièrement pour le seul qui s'édite.

   ⚠ LES MARGES NÉGATIVES ABSORBENT LA BORDURE. Un contrôle borné de 1 px fait
   2 px de plus qu'un texte : la rangée qui en porte un serait plus haute que sa
   voisine. Même remède que `.jbg-saisie`, pour la même raison et au même prix. */
.jv-edit {
  font: inherit; color: inherit; letter-spacing: inherit;
  font-variant-numeric: inherit; text-align: inherit;
  background: none; border: 1px solid transparent;
  border-bottom: 1px dashed var(--gray-300);
  border-radius: var(--radius-sm);
  padding: 0 3px; margin: -1px -3px;
  cursor: pointer;
}
/* Le cadre apparaît au survol du BLOC, pas du bouton : un pointillé de 1 px ne
   se remarque qu'une fois qu'on sait le chercher, et on ne survole pas une
   valeur qu'on ne savait pas modifiable. */
.jv-bloc:hover .jv-edit,
#progressPageSubtitle:hover .jv-edit { border-color: var(--border); border-bottom-style: solid; }
.jv-edit:hover, .jv-edit:focus-visible {
  border-color: var(--primary); border-bottom-style: solid;
  background: var(--surface); outline: none;
}
/* Une valeur absente le DIT, et reste cliquable — c'est même là que le clic
   sert le plus (145 affaires sans date de signature, 212 sans responsable). */
.jv-edit.is-empty { color: var(--text-secondary); }
/* Écriture en vol : la valeur est à l'écran, le serveur n'a pas encore répondu. */
.jv-edit.is-saving { opacity: 0.55; pointer-events: none; }

/* ── LE SOUS-TITRE DE PAGE PAIE SON APPUI PLUS CHER ────────────────────
   MESURÉ AU NAVIGATEUR (banc `edition-en-place`, zoom ×3) : le même filet
   `--gray-300` qui se lit très bien sous un jalon de la carte — 18 px, graisse
   700, encre primaire — disparaît sous le sous-titre, qui est à 14 px en
   `--text-secondary`. Un signe d'affordance qui ne se voit pas ne signale rien,
   et le champ finit à zéro d'usage sans que personne ne sache pourquoi : c'est
   exactement ce qui est arrivé à « responsable » et « date de prochaine action »
   sur l'écran de recouvrement (0 saisie sur 610 fiches en trois mois).
   Deux corrections, et pas une de plus :
     · un cran de gris en plus — le contraste du FILET suit celui de son TEXTE ;
     · 2 px sous la ligne, parce que la boîte du bouton déborde d'1 px en bas
       (les marges négatives qui tiennent le pas vertical) et que, sans eux, le
       pointillé est ROGNÉ par le bord du bloc — il existe et ne se voit pas. */
#progressPageSubtitle { padding-bottom: 2px; }
#progressPageSubtitle .jv-edit { border-bottom-color: var(--gray-400); }

/* Le champ ouvert EN PLACE prend la forme de la valeur qu'il masque — même
   police, même chasse, même gabarit de bordure : seule la teinte change. */
.jv-edit-champ {
  font: inherit; color: inherit; font-variant-numeric: inherit;
  background: var(--surface); max-width: 100%;
  border: 1px solid var(--primary); border-bottom: 1px solid var(--primary);
  border-radius: var(--radius-sm);
  padding: 0 3px; margin: -1px -3px; outline: none;
}

/* ── RETIRER UN GROUPE D'ACHATS : LA CROIX VIT DANS LE RETRAIT ─────────────
   Un état financier n'ouvre pas une colonne d'actions pour un geste qui ne
   concerne qu'une poignée de lignes — la colonne serait vide partout ailleurs,
   et les cinq colonnes de chiffres perdraient de la largeur au profit du vide.
   La marche d'escalier du compte de résultat (`td.jbg-poste`, 1,5 rem de
   retrait) est déjà là, sur TOUTES les lignes de détail : la croix s'y pose,
   hors flux, donc sans décaler un libellé ni rehausser une rangée — le pas
   vertical constant est ce que cette refonte est allée chercher deux fois.
   ⚠ ELLE NE SE MONTRE QU'AU SURVOL DE SA RANGÉE, comme le cadre de la cellule
   éditable juste au-dessus : même grammaire, même déclencheur. `opacity` et
   non `display`, pour que le clavier puisse l'atteindre — au focus elle se
   montre d'elle-même. */
.table.jbg-pl tbody td.jbg-poste { position: relative; }
.jbg-suppr {
  position: absolute; left: 0.15rem; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; padding: 0;
  border: none; border-radius: var(--radius-sm); background: none;
  color: var(--text-muted); cursor: pointer; line-height: 0;
  opacity: 0; transition: opacity 0.12s ease, color 0.12s ease, background 0.12s ease;
}
.table.jbg-pl tbody tr:hover .jbg-suppr { opacity: 1; }
.jbg-suppr:hover, .jbg-suppr:focus-visible {
  opacity: 1; color: var(--error); background: var(--gray-100);
}
.jbg-suppr:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* ── LE CADENAS : L'IMPOSSIBILITÉ SE DIT, ELLE NE SE DEVINE PAS ────────────
   Même logement, même déclencheur que la croix — c'est ce qui en fait une
   RÉPONSE à la question « pourquoi pas ici ? » plutôt qu'un second signe à
   apprendre. Ce qui change est ce qui doit changer : le glyphe (un cadenas,
   pas une croix pâle — une croix grisée se lit comme une croix qu'on n'a pas
   su rendre active), et le curseur, qui promet une explication et non un
   geste. Il ne prend pas la couleur d'alerte : ce n'est pas une erreur, c'est
   un état — le rouge est réservé aux dépassements de cette page. */
.jbg-suppr--tenu { cursor: help; color: var(--text-muted); }
.jbg-suppr--tenu:hover, .jbg-suppr--tenu:focus-visible {
  color: var(--text-secondary); background: var(--gray-100);
}
.jbg-rapport-indice { font-style: italic; }
.table.jbg-table tr.jbg-row-muted td { color: var(--text-muted); }

/* « À faire » — tableau d'actions à colonnes ancrées.
   La carte n'est pas dans un .dash-grid, donc la dash-table garde le
   table-layout: auto : la colonne d'action n'est pas fixée et le bouton
   (btn = white-space:nowrap) finit collé au bord droit du tableau — il
   déborde de la carte en vue contrainte et se disperse en vue large.
   On reproduit ici le table-layout: fixed du .dash-grid : le libellé prend
   la place restante (tronqué + title), montant et bouton sont ancrés à
   droite, dans la carte, quelle que soit la largeur. */
.dash-table--todo { table-layout: fixed; }
.dash-table--todo td.todo-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dash-table--todo td.todo-amount  { width: 130px; }
.dash-table--todo td.todo-action  { width: 120px; white-space: nowrap; }

/* ═══════════════════════════════════════════════════════════════════════════
   Maquettes de direction « job-vnext » — RAIL (?v=rail) & FLOW (?v=flow).
   Ossatures neuves bâties sur les tokens + composants canoniques.
   ═══════════════════════════════════════════════════════════════════════════ */
.jv-ref { font-size: var(--t-meta); color: var(--text-muted); display: inline-flex; align-items: center; gap: 0.2rem; }
/* `.jv-rate` (le taux collé au montant dans une carte KPI) est parti le
   12-09-2026 avec `jvStatStrip`, son unique appelant. */

/* ── `body.jv-focus` RETIRÉ le 12-09-2026, et à ne pas réintroduire ─────────
   Ces quatre règles masquaient la barre latérale de l'appli sur la page
   d'affaire (`display: none`), plus le hamburger mobile, pour récupérer les
   250 px de la barre. Le commentaire d'origine disait que « la nav de
   l'affaire à gauche devient la seule barre » : c'est faux, la « rail » de
   cette page était la colonne de 320 px à DROITE — elle-même supprimée depuis
   le 12-09-2026. On perdait donc la navigation de l'appli, sans rien à la
   place, en échange de largeur.
   La barre a désormais un MODE RAIL (68 px au repos, déplié AU SURVOL en
   RECOUVRANT le contenu — app-core/…/navigation.css) : on garde le menu ET la
   largeur, le marché n'existe plus. Demande Olivier sur /jobs/440.
   ⚠ Si une page a de nouveau besoin de toute la largeur, c'est le mode rail
   qu'il faut lui suggérer, pas un `display: none` sur la navigation. */

/* ⚠ LA CARTE « À FAIRE » A ÉTÉ SUPPRIMÉE LE 12-09-2026 (« c'est du bruit, on
   mettra les alertes dans chaque onglet si nécessaire »), et ses ~20 règles
   `.jv-todo-*` avec elle — deux blocs, ici et plus bas dans « Affaire (rail) ».
   Le catalogue des douze signaux qu'elle portait, et l'onglet qui devrait
   reprendre chacun, est conservé dans `job-vnext/shared.js` : c'est le point de
   départ de la redistribution annoncée. */

/* ── RAIL : onglets + contenu, PLEINE LARGEUR ────────────────────────────── */
/* ⚠ Le volet fixe de 320 px à droite — celui qui donnait son nom à l'ossature —
   a été SUPPRIMÉ le 12-09-2026 (demande Olivier sur /jobs/440) : son contenu vit
   désormais en tête de l'onglet Synthèse (`.jv-summary`). `.jv-rail` n'a donc
   plus de grille à deux colonnes ; les deux enveloppes restent parce que
   `renderJobAccessDenied` remplace `.jv-rail` et que les sélecteurs d'onglets de
   rail.js pendent à `.jv-rail-main`. Le nom est historique. */
.jv-rail { min-width: 0; }
.jv-rail-main { min-width: 0; }
.jv-rail-content { min-width: 0; }

/* Navigation = composant central .nav-tabs / .nav-tab (réutilisé, pas dupliqué). */
.jv-rail-main .nav-tabs { flex-wrap: wrap; }
/* Un libellé ne passe jamais à la ligne DANS son bouton : sinon jvFitTabs
   mesure une barre qui « tient » alors que « Pièces jointes » s'est plié sur
   deux lignes. */
.jv-rail-main .nav-tab { white-space: nowrap; }

/* Mesure « les huit onglets tiennent-ils sur une ligne ? » (jvFitTabs, rail.js) :
   le temps d'une lecture de scrollWidth, pas de retour à la ligne. */
.jv-rail-main .nav-tabs.jv-tabs-measure { flex-wrap: nowrap; overflow: hidden; }
/* ══════════════════════════════════════════════════════════════════════════
   LA SYNTHÈSE D'UNE AFFAIRE — UN SEUL SYSTÈME
   (refonte du 12-09-2026, 5e passe : « repars de zéro, cela doit être
   consistent » — Olivier.)

   LE SYSTÈME. Il n'y a qu'UN objet dans cette carte, LA CELLULE :

       INTITULÉ      0,68 rem · 700 · capitales · --text-muted
       Valeur        1,15 rem · 700 · --text-primary · chiffres tabulaires
       [piste]       4 px, facultative — seulement quand la valeur est une PART
       sous-ligne    0,72 rem · --text-muted

   Trois crans de typographie dans toute la carte, plus le HÉROS — 3 rem, une
   seule fois par écran. Tout en est fait : les trois taux de marge, les trois
   clés de la répartition, les quatre jalons, les quatre jauges. Les bandes sont
   des RANGÉES de cellules ; le reste (une barre, un filet) est du dessin.

   ⚠ DEUX VOCABULAIRES DE CLASSES POUR UN SEUL CRAN, ET C'EST VOULU. Les jauges
   gardent le balisage du SOCLE (`dash-progress-*`, dont une sonde compare le
   jumeau JS et le PHP à l'octet) : le réécrire ici forkerait un composant
   partagé pour une question de mise en page. C'est donc le CSS qui les fait
   converger — chaque cran est déclaré UNE fois, pour les deux vocabulaires.
   On n'ajuste pas le socle, on ajuste son CONTEXTE.

   ⚠ Le préfixe `jv-` reste celui de la page d'affaire ; `jv-rail-top` survit
   parce que c'est la rangée d'identité, seul morceau de l'ancien volet droit
   (supprimé le 12-09-2026) qui avait sa raison d'être ailleurs que dans un volet.
   ══════════════════════════════════════════════════════════════════════════ */
.jv-rail-top { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
/* ── LA TÊTE DE LA CARTE D'AFFAIRE (15-09-2026, 2ᵉ passe) ────────────────────
   Le matin, « Client : X · Resp. Y · Activité Z » était descendu du sous-titre
   de page dans la carte, en UNE PHRASE (`.jv-identite`, retirée ici). Olivier,
   le soir, sur /jobs/121 : « ça doit être à droite avec badge pour responsable
   et pour activité ; à gauche au-dessus d'Exécution, on met le titre (éditable)
   et on peut sélectionner le client. »

   DEUX VERSANTS, et ils ne répondent pas à la même question :
     · GAUCHE — QUI EST CETTE AFFAIRE : son titre, son client, puis son état
       (badge, référence, passerelles Build/Plan) ;
     · DROITE — QUI S'EN OCCUPE : responsable et activité, en badges.
   Une phrase les mettait sur un seul fil, dans l'ordre où on les avait écrits ;
   deux versants les mettent dans l'ordre où on les cherche.

   ⚠ CETTE RÈGLE EST BORNÉE À LA CARTE D'AFFAIRE (`.jv-tete-affaire`), PAS À
   `.jv-rail-top`. Ce dernier est la rangée de tête GÉNÉRIQUE : le tableau de
   bord (`dashboard.js`), `/clients#synthese` et `/fournisseurs#afaire` la
   rendent aussi, avec un tout autre contenu. Lui donner deux versants d'ici
   aurait déplacé trois écrans qui n'ont rien demandé.

   ⚠ `flex: 1 1 320px` SUR LE VERSANT GAUCHE, pas `1 1 auto` : un titre long
   pousserait les deux badges hors de la carte, et `min-width: 0` est ce qui
   autorise le texte à se replier au lieu de déborder. Sous ~640 px de carte,
   les deux versants s'empilent tout seuls (`flex-wrap`) — les badges passent
   sous l'état, pas à côté.

   ⚠ LE TITRE EST AU CRAN DE LA VALEUR, PAS À CELUI DE L'OUVERTURE. `--t-ouverture`
   (1,6 rem) est déjà pris par le marché, dans la colonne des vitaux : deux
   textes de même taille, dont l'un en haut à gauche, auraient donné deux têtes
   à la carte — le défaut exact que le 13-09 avait corrigé en ramenant le marché
   de 1,9 à 1,6. La première place suffit à dire le rang. */
/* ⚠ `flex: 1 1 auto` N'EST PAS DÉCORATIF, ET C'EST LE PIÈGE DE CETTE TÊTE.
   `.jv-bande-tete` est elle-même un conteneur flex : sans cette ligne, la
   rangée est un ITEM à `flex: 0 1 auto`, donc large comme SON CONTENU — mesuré
   au banc, 283 px de blanc entre les badges et le bord droit de la carte. Le
   `justify-content: space-between` ci-dessous répartit alors dans une boîte
   trop courte, et « à droite » veut dire « à droite de rien ». La version
   d'avant ne s'en apercevait pas : sa ligne d'identité prenait `flex: 0 0 100%`
   et forçait la largeur par accident. */
.jv-tete-affaire {
  flex: 1 1 auto;
  display: flex; align-items: flex-start; justify-content: space-between;
  flex-wrap: wrap; gap: var(--space-4) var(--space-6);
}
.jv-tete-qui {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: var(--space-2); min-width: 0; flex: 1 1 320px;
}
.jv-tete-titre {
  margin: 0; font-size: var(--t-valeur); font-weight: 700; line-height: 1.25;
  color: var(--text-primary); max-width: 100%;
}
/* Le client : au cran du corps, sous le titre. L'icône est un repère, pas un
   ornement — elle dit « tiers » là où le texte ne porte plus de préfixe
   (« Client : » a disparu avec la phrase). */
.jv-tete-client {
  display: flex; align-items: center; gap: 0.35rem; min-width: 0; max-width: 100%;
  font-size: var(--t-corps); color: var(--text-secondary);
}
.jv-tete-client svg { color: var(--text-muted); flex: 0 0 auto; }
.jv-tete-client > span { min-width: 0; }
/* L'état : la rangée que `.jv-rail-top` portait à elle seule jusqu'ici. */
.jv-tete-etat {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; min-width: 0;
}
/* ── LES DEUX BADGES D'ATTRIBUTION ──────────────────────────────────────────
   ⚠ CHACUN PORTE SON INTITULÉ, et ce n'est pas du remplissage : « Anaïs Bertin »
   et « Fabrication » côte à côte sans étiquette forment une énumération qu'il
   faut décoder. Avec l'intitulé en capitales devant, ce sont deux cases d'une
   fiche — et c'est le MÊME gabarit pour les deux, parce que deux faits de même
   nature veulent une seule grammaire (la leçon de « deux nombres qu'on compare »).
   ⚠ `align-items: center` et un fond de surface : le badge doit tenir DEBOUT
   sur la rangée du titre sans se lire comme un bouton. Le déclencheur d'édition
   qu'il contient garde sa propre affordance (`.jv-edit`, pointillé). */
.jv-tete-meta { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-2); }
.jv-meta {
  display: inline-flex; align-items: center; gap: 0.4rem; max-width: 100%;
  padding: 5px 10px; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
}
.jv-meta svg { color: var(--text-muted); flex: 0 0 auto; }
.jv-meta-nom {
  font-size: var(--t-label); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-muted); white-space: nowrap;
}
.jv-meta-val { font-size: var(--t-corps); color: var(--text-primary); min-width: 0; }
/* ── UNE VALEUR QUI NE PEUT PLUS CHANGER, ET QUI DIT POURQUOI ───────────────
   Le client d'une affaire déjà facturée. Ce n'est ni une permission ni un état
   d'affaire : c'est une donnée FACTURÉE, et le serveur refuse en 409. Aucune
   affordance — il n'y a rien à cliquer — mais le curseur d'aide, parce que la
   raison est dans l'infobulle et qu'une valeur figée sans explication se lit
   comme un écran cassé. */
.jv-fige { cursor: help; }
/* ── LE SÉLECTEUR OUVERT EN PLACE (`type: 'entite'`) ────────────────────────
   Le déclencheur est masqué, ce bloc prend sa place le temps du choix. Il porte
   sa propre largeur : le nom qu'on tape n'a aucune raison de tenir dans la
   largeur du nom qu'on remplace, et un champ de 60 px sur « Choisir un
   client » ne se cherche pas, il se subit. */
.jv-edit-entite { display: inline-block; vertical-align: baseline; min-width: 260px; max-width: 100%; }
.jv-edit-entite > input[type="text"] { width: 100%; }
/* Le périmètre en mots, dans l'en-tête de la carte du tableau de bord
   (« 12 derniers mois · tous les statuts ») : au cran du corps, en retrait. */
.jv-tete-perimetre { font-size: var(--t-corps); color: var(--text-muted); }
/* ── Tableau de bord : la pile, le fil, les tableaux (13-09-2026) ──────────
   Une PILE = des cellules l'une sous l'autre dans la colonne étroite (« ce
   mois-ci », « créances ») — le même cran que les jauges empilées de l'affaire. */
.jv-pile { display: flex; flex-direction: column; gap: var(--space-4); }
/* ── DEUX ou TROIS blocs CÔTE À CÔTE, à parts égales (13-09-2026 tard) ──────
   La carte n'a plus de colonnes : chaque bloc prend la pleine largeur et
   s'empile, et seuls les blocs de MÊME NATURE (deux listes de chiffres, trois
   listes de documents) se posent côte à côte — leurs hauteurs se suivent parce
   que leur contenu se compte pareil. C'est ce qui supprime le blanc
   STRUCTUREL : deux colonnes dont l'une porte un graphe et l'autre une liste
   n'ont aucune raison de finir ensemble.
   ⚠ Le filet de groupe (`.jv-zone > .jv-groupe`) ne s'applique pas ici : dans
   un duo, un trait au-dessus de chaque colonne ferait deux traits sur la même
   ligne. C'est la BANDE qui porte le sien. */
.jv-duo, .jv-trio { display: grid; gap: var(--space-6); margin-top: var(--space-6);
                    padding-top: var(--space-6); border-top: 1px solid var(--border); }
.jv-duo  { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.jv-trio { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.jv-duo > .jv-groupe, .jv-trio > .jv-groupe { margin-top: 0; padding-top: 0; border-top: 0; }
@container (max-width: 820px) {
  .jv-duo, .jv-trio { grid-template-columns: 1fr; }
}
/* Les blocs empilés directement dans une bande : même rythme que dans une zone. */
.jv-bande > .jv-groupe, .jv-bande > .jv-repartition {
  margin-top: var(--space-6); padding-top: var(--space-6); border-top: 1px solid var(--border);
}
.jv-bande > .jv-bande-titre + .jv-marge, .jv-bande > .jv-marge { margin-top: 0; padding-top: 0; border-top: 0; }
/* Les deux BLOCS de la carte du tableau de bord (13-09 soir) : un titre qui
   dit son périmètre (« suit les filtres » / « indépendant des filtres »), puis
   sa grille à deux colonnes. Le filet entre eux vient de `.jv-bande +
   .jv-bande`. Le titre est au cran des titres de bloc, pas plus : c'est un
   repère de lecture, pas une section de page. */
.jv-bande-titre { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-5); }
.jv-bande-titre .jv-bloc-titre { font-size: var(--t-corps); letter-spacing: 0.04em; color: var(--text-secondary); }
.jv-bande-titre .jv-bloc-titre .jv-bc-unite { font-size: var(--t-meta); letter-spacing: 0; }
.jv-bande--portefeuille .jv-bande-titre .jv-bloc-titre { color: var(--primary); }
/* Les puces de filtre dans l'en-tête de la carte Portefeuille (13-09) : le
   composant des en-têtes de la liste des affaires (`.ms-trigger--chip`),
   poussées à droite. Les panneaux s'ancrent à DROITE : à gauche ils
   sortiraient de la carte pour les dernières puces. */
.jv-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-left: auto; }
.jv-chips .ms-panel { left: auto; right: 0; }
.jv-chips-dates { display: inline-flex; align-items: center; gap: 4px; font-size: var(--t-meta); color: var(--text-muted); }
.jv-chips-dates .form-input { height: 22px; padding: 0 6px; font-size: 0.72rem; width: auto; }

/* ── LES BADGES DE FILTRE VIVENT DANS LE SOCLE (app-core/tables.css) depuis le
   2026-09-14 : la rangée de filtres de `table_list()` les rend elle aussi, et
   deux écritures d'une même chose divergent au premier ajustement.
   NE RESTE ICI que ce qui est PROPRE au tableau de bord : ses puces sont
   alignées à DROITE, ses badges doivent l'être avec elles. Sur une liste, les
   puces partent du bord gauche — l'alignement ne peut donc pas vivre dans la
   règle partagée. */
#synthBadges { justify-content: flex-end; }
/* Le fil des derniers documents : une bande pleine largeur sous les colonnes. */
.jv-tableaux .jv-fil-listes, .jv-bc-tete .jv-fil-listes { font-size: var(--t-meta); color: var(--text-muted); white-space: nowrap; }
.jv-fil-listes a { color: var(--primary); font-weight: 600; text-decoration: none; }
.jv-fil-listes a:hover { text-decoration: underline; }
/* Le fil est un TABLEAU depuis le 13-09-2026 : des en-têtes, et l'affaire dans
   SA colonne. Les pistes fixes encadrent les deux colonnes souples (document,
   tiers) — sans elles, `table-layout: fixed` les répartit à parts égales et un
   nom de fournisseur long vole la place du montant. */
/* Six colonnes dans la colonne large (~690 px à 1280) : les pistes fixes
   encadrent la seule colonne souple, l'affaire — c'est elle qui porte le nom
   le plus long, et c'est elle qui doit respirer. */
.jv-fil-table col.jv-fil-c-doc     { width: 136px; }
.jv-fil-table col.jv-fil-c-tiers   { width: 18%; }
/* ⚠ « ARC reçu » dans son badge demande 100 px : c'est le plus long des
   cinq vocabulaires d'état qui fixe cette piste, pas le plus courant. */
.jv-fil-table col.jv-fil-c-statut  { width: 106px; }
.jv-fil-table col.jv-fil-c-val     { width: 92px; }
.jv-fil-table col.jv-fil-c-date    { width: 46px; }
/* L'affaire sur UNE ligne : sa référence en tête, son nom à la suite, en
   retrait — deux faits, une ligne, et l'ellipse mange le nom avant la
   référence, qui est ce qu'on cite. */
.jv-fil-aff { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jv-fil-aff em { font-style: normal; color: var(--text-secondary); }
/* La puce de type précède le NUMÉRO, en sous-ligne : elle dit de quel document
   il s'agit sans voler la ligne du nom. */
.jv-fil-table .jv-bc-ref .jv-fil-kind { margin-right: 6px; font-size: 0.625rem; padding: 1px 5px; }
.jv-fil-table col.jv-fil-c-val     { width: 96px; }
/* Le type précède le numéro DANS la colonne du document : une piste de plus
   écrasait les deux colonnes souples (mesuré à 57 px pour « Document »).
   ⚠ `span.jv-fil-doc` et non `.jv-fil-doc` : la règle de troncature de rôle
   `.jv-bc-qui > span:not(.jv-bc-ref)` vaut (0,2,1) et lui imposait
   `display: block` — la puce passait AU-DESSUS du numéro. */
/* ⚠ TROIS classes, pas deux : `.jv-bc-qui > span:not(.jv-bc-ref)` vaut AUSSI
   (0,2,1) et se déclare CENT LIGNES PLUS BAS — à spécificité égale, c'est
   l'ordre qui tranche, et la puce repassait au-dessus du numéro. */
/* ⚠ PAS `.jv-fil-table` EN TÊTE : les trois listes de documents emploient le
   même motif (nom + badge sur une ligne) dans `.jv-bc-table--docs`, et la
   règle de rôle `.jv-bc-qui > span:not(.jv-bc-ref)` (0,2,1) leur imposait
   `display: block` — le badge passait sous le nom. */
.jv-bc-table .jv-bc-qui > span.jv-fil-doc { display: flex; align-items: center; gap: 6px; min-width: 0; }
.jv-fil-doc a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Les colonnes d'un tableau ne se TOUCHENT pas : sans gouttière, « MONTANT » et
   « STATUT » s'écrivaient collés (« MONTANTSTATUT ») et un nom de fournisseur
   long touchait la référence d'affaire. */
/* ⚠ `thead`/`tbody` explicites : `.jv-fil-table thead th { padding: 0 0 6px }`
   est déclarée plus bas à spécificité ÉGALE et remettait padding-left à 0. */
.jv-fil-table thead tr th + th, .jv-fil-table tbody tr td + td { padding-left: 10px; }
.jv-fil-table td { vertical-align: middle; }
.jv-fil-table thead th { padding: 0 0 6px; font-size: var(--t-label); font-weight: 700; text-transform: uppercase;
                         letter-spacing: 0.02em; color: var(--text-muted); text-align: left; border-bottom: 1px solid var(--border); }
.jv-fil-table thead th.text-right { text-align: right; }
.jv-fil-table tbody tr:first-child td { padding-top: 0.5rem; }
/* Le tiers et l'affaire TRONQUENT : la règle de rôle de `.jv-bc-qui` ne les
   atteint pas, ils ne sont pas dans cette cellule-là. */
.jv-fil-tiers, .jv-fil-affaire, .jv-fil-num { min-width: 0; }
.jv-fil-table .jv-bc-val { white-space: nowrap; }
.jv-fil-tiers > span, .jv-fil-affaire > span, .jv-fil-num > a, .jv-fil-num > span {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.jv-fil-num a { color: var(--primary); font-weight: 600; text-decoration: none; }
.jv-fil-num a:hover { text-decoration: underline; }
.jv-fil-statut { text-align: right; white-space: nowrap; }
.jv-fil-date { text-align: right; white-space: nowrap; font-size: var(--t-meta); color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* Le type du document : une étiquette au cran de l'intitulé, une teinte par
   type — les MÊMES que les courbes de la chronologie (vente en violet,
   commandes en bleu) pour que le fil et le graphe parlent la même langue. */
.jv-fil-kind { display: inline-block; font-size: var(--t-label); font-weight: 700; text-transform: uppercase;
               letter-spacing: 0.02em; padding: 2px 6px; border-radius: 3px; white-space: nowrap;
               background: var(--gray-100); color: var(--text-secondary); }
.jv-fil-kind.is-facture { background: var(--state-violet-bg); color: var(--state-violet-fg); }
.jv-fil-kind.is-devis   { background: var(--state-blue-bg); color: var(--primary); }
.jv-fil-kind.is-bc      { background: var(--state-blue-bg); color: var(--state-blue-fg); }
.jv-fil-kind.is-ff      { background: var(--state-amber-bg); color: var(--state-amber-fg); }
.jv-fil-kind.is-affaire { background: var(--state-green-bg); color: var(--state-green-fg); }
.synth-plus td { text-align: left; }
/* La ligne de total d'une liste compacte : un filet au-dessus, comme le pied
   d'un tableau de chiffres. */
.jv-bc-table tr.synth-total td { border-top: 2px solid var(--border); border-bottom: 0; padding-top: 0.5rem; }
/* Le sélecteur de mois d'un bloc : à droite du titre, panneau ancré à droite. */
.jv-bc-tete .jv-chips { margin-left: auto; }
.synth-plus a { font-size: var(--t-meta); font-weight: 600; color: var(--primary); text-decoration: none; }

/* ── LA CARTE ET SES BANDES ────────────────────────────────────────────────
   ⚠ LE RYTHME EST PORTÉ PAR LE BLOC LUI-MÊME, PAS PAR UN SÉLECTEUR DE FRÈRES.
   Il l'a été (`.jv-bloc + .jv-bloc { margin-top }`) et il ne marchait pas : la
   carte de l'essentiel vit dans `#jvJobSummary` et la charge de l'atelier dans
   `#contractRentaContent` — deux hôtes FRÈRES, donc les cartes ne le sont pas,
   et `+` n'a jamais pu s'appliquer entre elles. Mesuré au navigateur : 0 px
   entre les deux cadres, qui se touchaient.
   ⚠ Le filet est porté par `.jv-bande + .jv-bande`, jamais par une bordure
   écrite dans chaque bande : une bande qui ne se rend pas (pas d'avancement sur
   une affaire neuve) laisserait sinon un trait orphelin en bas de la carte. */
/* ⚠ IL N'Y A PAS DE GRILLE EXTÉRIEURE, ET C'EST UNE CORRECTION (12-09-2026,
   soir). J'en avais posé une — carte de 300 px à gauche, compte de résultat à
   côté — ce qui écrasait la carte à la largeur d'une colonne : son PROPRE
   partage interne se repliait alors, et les jalons comme la chronologie
   tombaient à gauche sous les jauges. Olivier : « ça aurait dû rester à
   droite, je t'ai juste demandé de rajouter le compte de résultat à droite et
   la liste des bons de commande à gauche ».
   Le partage est DANS la carte, et c'est aujourd'hui celui des trois zones
   (`.jv-synthese`, plus bas) : une piste étroite pour les vitaux et les achats,
   une large pour l'état du chantier. */

/* ⚠ LA CARTE A UNE MESURE (13-09-2026). Sans plafond, elle prenait 2 206 px sur
   un écran de 2504 : les quatre barres d'avancement y mesuraient 443 px chacune
   et le graphe atteignait 15:1. Un écran plus large doit rendre du BLANC, pas
   des lignes plus longues — et le blanc se pose DEHORS, pas dans le cadre : une
   zone bornée à l'intérieur d'une carte qui, elle, court jusqu'au bord laisse un
   vide qu'on lit comme un bloc qui n'a pas chargé.
   1 560 = 272 (la piste étroite) + 20 (gouttière) + 1 200 (la mesure de la zone
   d'état) + 48 de rembourrage + les bordures. */
.jv-essentiel { container-type: inline-size; max-width: 1560px;
  /* ── L'ÉCHELLE TYPOGRAPHIQUE DE LA CARTE (13-09-2026) ──────────────────
     RELEVÉ AVANT : 15 tailles, 5 graisses et 12 encres pour dire cinq choses.
     Cinq de ces tailles tenaient dans 1,1 px les unes des autres (10,4 · 10,56 ·
     10,88 · 11,2 · 11,52) — personne ne les distingue, ce sont des accidents de
     cascade, pas des crans. Un écran qui parle quinze fois pour dire cinq rôles
     ne se lit pas plus finement : il se lit moins bien, parce que plus rien ne
     signale ce qui compte.
     SIX CRANS, UN PAR RÔLE. Chaque règle de la carte cite un jeton ; plus aucune
     n'écrit un nombre. Ajouter un cran demande alors de nommer le rôle qu'il
     sert — et c'est exactement la question qu'on veut avoir à se poser.
     ⚠ LES VALEURS SONT CELLES DE LA PLATEFORME, PAS LES MIENNES. Relevé sur
     build + app-core : `0.875rem` (114 emplois) et `0.75rem` (113) dominent
     largement, puis `0.6875rem` — le socle n'a AUCUN jeton de taille, mais il a
     un usage. Mon premier jet (0,85 / 0,72 / 0,68) en était trois voisins à 3 %,
     c'est-à-dire trois accidents de plus. Recalés, deux effets : une promotion
     future de cette échelle vers `app-core/tokens.css` devient un non-événement,
     et le `.badge` du socle (12 px) tombe exactement sur `--t-meta` — un cran de
     moins sur l'écran.
     ⚠ `--t-label` reste plus petit que `--t-meta` : l'intitulé est en CAPITALES,
     elles se lisent ~8 % plus large et supportent un corps plus petit — et
     « FACTURATION CLIENT HT » doit tenir dans une cellule de 155 px.
     ⚠ LA PORTÉE EST LA CARTE, et toutes les règles qui citent ces jetons sont
     rendues par `jvSyntheseCarte` — donc DEDANS (vérifié : `.jv-cell`,
     `.jv-rangee`, `.jv-bloc-titre` et `.jv-ref` n'ont pas d'autre appelant dans
     `public/assets/js`). Une de ces classes réutilisée HORS de la carte y
     perdrait sa taille en silence : un `var()` introuvable invalide la
     déclaration entière, et le texte retombe sur la taille héritée. */
  --t-heros:     3rem;       /* le seul chiffre qui répond — une fois par écran */
  /* ⚠ 1,6 rem, pas 1,9 (13-09-2026, 2ᵉ passe). Un nombre LONG à 30 px porte plus
     d'encre qu'un nombre court à 48 : « 697 311,66 € » pesait autant que le
     héros, et la colonne avait deux têtes. Mesuré en rendant les quatre
     traitements côte à côte (bancs/marge-vitaux, page `/banc/variantes`). Le
     marché garde l'encre du texte et la première place — c'est la TAILLE qui
     cède, pas son rang. */
  --t-ouverture: 1.6rem;     /* le marché : grand, mais sans verdict */
  --t-valeur:    1.125rem;   /* tout chiffre qu'on cite au téléphone */
  --t-corps:     0.875rem;   /* une ligne de liste, un nom, un montant de rang 2 */
  --t-meta:      0.75rem;    /* ce qui explique : sous-lignes, notes, légendes */
  --t-label:     0.6875rem;  /* l'intitulé en capitales */
}
.jv-bloc { margin-bottom: var(--space-6); }
/* ── « FACTURATION PRÉVUE » EST DANS LA CARTE (14-09-2026, 2ᵉ passe) ────────
   Olivier : « facturation prévue doit être dans la colonne de droite au-dessous
   des dates ». Son hôte est donc un `.jv-groupe` de la zone d'état, écrit par
   `jvSyntheseCarte` — il prend le rythme et le filet des autres groupes sans une
   règle de plus, et la borne de 1 560 px qu'il portait pendant une heure est
   devenue inutile : la carte le borne.
   ⚠ MAIS UN GROUPE VIDE LAISSE UN TRAIT ORPHELIN. Le bloc rend une chaîne VIDE
   quand l'affaire n'a pas de ligne prévisionnel servie — et l'hôte, lui, est
   toujours écrit. Sans cette règle, ces affaires-là gagnaient un filet et 24 px
   de blanc entre les jalons et la chronologie, pour annoncer rien. C'est la même
   leçon que `.jv-zone > .jv-groupe` : le filet appartient à ce qui se rend.
   ⚠ `:empty` EXIGE ZÉRO NŒUD, espaces compris — c'est ce qui le rend sûr ici :
   `renderFacturationPrevue` n'écrit RIEN quand elle n'a rien à dire (elle ne
   pose pas une chaîne vide), et l'hôte du gabarit n'a pas d'espace entre ses
   balises. */
#jobPrevisionnelWrap:empty { display: none; }
/* ⚠ ET DANS LA CARTE, IL PARLE LE DIALECTE DE LA CARTE. Le bloc vient de
   l'onglet Facturation, où son titre est un `.panel-title` — 1 rem, gras, encre
   de texte. Posé entre « SIGNATURE » et « OÙ EN EST-ON, MOIS PAR MOIS », tous
   deux en `--t-label` capitales grises, il pesait comme un titre de PAGE au
   milieu d'une carte : le seul cran de titre que la carte n'avait pas. Six crans
   typographiques, un par rôle, c'est la règle de `.jv-essentiel` — un septième
   n'aurait nommé aucun rôle de plus.
   ⚠ LA PORTÉE EST `.jv-zone >` : sur la v2, qui n'a pas de carte, le bloc garde
   son `.panel-title` d'origine — c'est là-bas un titre de section, et il en a le
   rang. Une règle posée sur le seul `#jobPrevisionnelWrap` aurait rapetissé un
   titre juste sur l'autre ossature, sans que rien ne le dise. */
.jv-zone > #jobPrevisionnelWrap .jbg-head .panel-title,
.jv-zone > #jobPrevisionnelWrap .jbg-head .panel-title > span {
  font-size: var(--t-label); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--text-muted);
}
.jv-bande + .jv-bande { margin-top: var(--space-6); padding-top: var(--space-6); border-top: 1px solid var(--border); }
/* Une BANDE posée sous une grille à deux colonnes (le fil des documents) :
   `.jv-bande + .jv-bande` ne l'atteint pas — sa voisine du dessus est la
   grille, pas une bande. Sans cette règle, le fil colle à la chronologie. */
.jv-synthese + .jv-bande { margin-top: var(--space-6); padding-top: var(--space-6); border-top: 1px solid var(--border); }
.jv-bande-tete { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
                 margin-bottom: var(--space-5); flex-wrap: wrap; row-gap: var(--space-2); }
/* ── LES BADGES REMONTENT SUR LA RANGÉE DU PÉRIMÈTRE (2026-09-14) ──────────
   `.jv-filtres` (socle, tables.css) prend `flex: 0 0 100%` : dans la rangée de
   filtres d'une liste, les badges se posent SOUS les puces qui les ont
   produits. Ici les puces sont montées d'un cran, dans le cadre du titre — la
   rangée du dessous ne portait plus que « 287 affaires », seule à gauche. Les
   badges la partagent donc, alignés à droite comme les puces au-dessus d'eux.
   ⚠ Ils enroulent TOUJOURS (le socle le dit) : trois clients retenus font deux
   lignes de badges, et c'est la bande qui grandit, pas la carte qui déborde. */
.jv-bande-tete > .jv-filtres { flex: 1 1 auto; justify-content: flex-end; }
.jv-bloc-titre { margin: 0; font-size: var(--t-label); font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); }
.jv-bloc-vide  { margin: 0; font-size: var(--t-corps); color: var(--text); }
.jv-bloc-note  { margin: var(--space-4) 0 0; font-size: var(--t-meta); color: var(--text-muted); }
.jv-bande-note { margin: var(--space-4) 0 0; display: flex; flex-wrap: wrap; align-items: baseline;
                 gap: 0 var(--space-2); font-size: var(--t-meta); color: var(--text-muted); }
.jv-bande-note strong { color: var(--text-primary); font-weight: 600; font-variant-numeric: tabular-nums; }
.jv-rep-sep { color: var(--text-muted); }

/* ── LES DERNIERS BONS DE COMMANDE ────────────────────────────────────────
   Trois colonnes dans 340 px : qui (+ sa référence et sa date en sous-ligne),
   combien, et quand ça arrive. Le fournisseur prend tout le reste et coupe à
   l'ellipse — « Aluminios Cortizo S.A.U. » ne tient pas, et c'est très bien :
   c'est un libellé, pas un chiffre. Les deux colonnes de droite, elles, ne
   coupent JAMAIS (cf. le compte de résultat : un nombre coupé est un nombre
   faux). */
/* ⚠ `table-layout: fixed` EST LA CONDITION DE L'ELLIPSE. En disposition auto,
   une cellule s'ÉLARGIT à son contenu : « Aluminios Cortizo S.A.U. » poussait la
   colonne de droite hors de la carte, et `text-overflow` ne coupait rien
   puisque rien ne débordait de la cellule. La colonne de droite est donc bornée,
   la première prend ce qui reste — et c'est elle, seule, qui coupe. */
.jv-bc-table { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: var(--t-corps); }
.jv-bc-table col.jv-bc-c-val { width: 118px; }
/* ⚠ 140 px POUR LES DEUX LISTES DE FACTURES, et c'est mesuré. Leur seconde
   ligne porte une date ET un état (« 09/09/2026 · Validée », 20 caractères) là
   où un bon de commande n'écrit qu'une date et un mot (« 21/09/2026 prévue »).
   À 118 px, le banc a vu DEUX montants coupés sur cinq — et un montant coupé
   est un montant faux. La gauche y perd 22 px : elle ne porte que du texte, qui
   tronque proprement avec son infobulle. */
.jv-bc-table--docs col.jv-bc-c-val { width: 140px; }
.jv-bc-table td { font-size: inherit; padding: 0.4rem 0; border-bottom: 1px solid var(--gray-100); vertical-align: top; }
.jv-bc-table tr:last-child td { border-bottom: 0; }
.jv-bc-unite { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--text-muted); }
/* Le titre et la bascule sur la même ligne — et la bascule passe DESSOUS quand
   la colonne est trop étroite pour les deux (272 px, ça arrive). */
.jv-bc-tete { display: flex; align-items: baseline; justify-content: space-between;
              gap: var(--space-2); flex-wrap: wrap; margin-bottom: var(--space-2); }
.jv-bc-tete .jv-bloc-titre { margin: 0; }
/* ── LES BONS DE COMMANDE : TROIS FENÊTRES PUIS UN CLASSEMENT ──────────────
   Un seul bloc porte deux temps (le rythme, puis chez qui) : le second titre
   est un SOUS-titre, pas un bloc voisin — il se lit plus petit et se sépare
   par un filet, faute de quoi les deux tableaux empilés se fusionnent du
   regard alors qu'ils ne comptent pas la même chose. */
.jv-bc-tete--sous { margin-top: var(--space-4); padding-top: var(--space-3);
                    border-top: 1px solid var(--border-light); }
.jv-bc-tete--sous .jv-bloc-titre { font-size: var(--t-meta); text-transform: uppercase;
                    letter-spacing: 0.04em; color: var(--text-muted); font-weight: 600; }
.jv-bc-fenetres { gap: var(--space-3); }
/* L'HISTOGRAMME DES SIX MOIS. Six colonnes dans 272 px : la hauteur porte le
   NOMBRE de BC, la lettre l'initiale du mois. Le mois COURANT est en creux —
   il est incomplet, et une barre pleine plus courte se lirait comme une chute
   (cf. « un nombre DANS une forme dit ce que la forme mesure »). */
/* ⚠ LA PISTE PORTE SA PROPRE HAUTEUR (`b`). Une hauteur en % se résout sur la
   boîte PARENTE : posée sur la colonne, qui porte aussi son étiquette, les
   grandes barres débordaient et quatre mois très différents (178 · 148 · 232 ·
   231) se peignaient à la même taille. */
.jv-bcm { display: flex; align-items: flex-end; gap: 4px; margin: var(--space-3) 0 0; }
.jv-bcm-col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; }
.jv-bcm-col > b { display: flex; align-items: flex-end; height: 44px;
  border-bottom: 1px solid var(--border-light); }
.jv-bcm-col > b > i { display: block; width: 100%; border-radius: 2px 2px 0 0;
  background: var(--state-blue-dot); }
.jv-bcm-col.is-courant > b > i { background: transparent;
  box-shadow: inset 0 0 0 1px var(--state-blue-dot); }
/* LE MOIS QU'ON REGARDE (la période choisie), à distinguer du mois qu'on VIT :
   celui-ci est plein et souligné, celui-là en creux. Deux marques, deux sens —
   « incomplet » ne doit pas se lire sur un juin terminé depuis trois mois. */
.jv-bcm-col.is-mise > b { border-bottom-color: var(--primary); border-bottom-width: 2px; }
.jv-bcm-col.is-mise > b > i { background: var(--primary); }
.jv-bcm-col.is-mise > em { color: var(--text-primary); font-weight: 600; }
.jv-bcm-col > em { display: block; text-align: center; font-style: normal;
  font-size: var(--t-meta); color: var(--text-muted); line-height: 1.4; }
/* LA PART DE CHAQUE FOURNISSEUR dans les six mois. ⚠ SPÉCIFICITÉ (0,3,0) : la
   règle de rôle `.jv-bc-qui > span:not(.jv-bc-ref)` vaut (0,2,1) et imposerait
   à cette barre la troncature d'un texte — elle doit la perdre. */
.jv-bc-table .jv-bc-qui > .jv-bc-part { display: block; height: 3px; margin-top: 4px;
  border-radius: 2px; background: var(--gray-200); overflow: hidden; }
.jv-bc-table .jv-bc-qui > .jv-bc-part > i { display: block; height: 100%;
  background: var(--state-blue-dot); }
/* La ligne du RESTE ferme le classement : elle appartient au total, pas au
   palmarès — filet au-dessus, encre sourde, et pas de barre. */
.jv-bc-table .jv-bc-reste > td { border-top: 1px solid var(--border-light); color: var(--text-muted); }
.jv-bc-table .jv-bc-reste .jv-bc-qui > span { font-weight: 400; }
/* ⚠ LE MENU EST CELUI DU SOCLE (`.dropdown-container` / `.dropdown-menu`,
   ouvert par `core.js` via `.btn-actions`). Ce qui est ajusté ici, c'est la
   TAILLE du déclencheur — un bouton de barre d'outils dans un bloc de 272 px
   pèserait plus que le titre qu'il accompagne — et la largeur du menu, dont le
   minimum du socle (180 px) dépasse la colonne. */
.jv-bc-tri .btn { padding: 2px 8px; font-size: var(--t-meta); font-weight: 600; line-height: 1.5; }
.jv-bc-tri .btn svg { margin-left: 1px; }
.jv-bc-tri .dropdown-menu { min-width: 148px; }
.jv-bc-tri .dropdown-item { padding: 0.45rem 0.75rem; font-size: var(--t-corps); color: var(--text); }
/* L'option courante se marque : le bouton la nomme déjà, le menu doit la
   confirmer — sinon on ne sait pas laquelle on s'apprête à quitter. */
.jv-bc-tri .dropdown-item.is-actif { font-weight: 700; color: var(--primary); }
.jv-bc-qui { min-width: 0; }
.jv-bc-qui a { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text); font-weight: 500; }
/* ⚠ LE LIBELLÉ TRONQUE QUEL QUE SOIT SON BALISAGE. La règle ci-dessus ne
   visait que le lien : un libellé sans page où aller (un groupe d'achats, un
   fournisseur sans fiche) est un <span>, et « Remplissage (vitrages - tôles) »
   s'écrivait PAR-DESSUS son montant — vu à l'écran par Olivier, 13-09. */
.jv-bc-qui > span:not(.jv-bc-ref) { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.jv-bc-qui a:hover { color: var(--primary); }
/* ⚠ ELLE TRONQUE, ELLE NE REPLIE PAS. Elle ne portait qu'une référence courte
   (« BC-281 · 11/09/2026 ») ; depuis qu'une facture fournisseur y écrit le nom
   de son fournisseur, elle peut dépasser — et une sous-ligne qui passe à la
   ligne DÉCALE la rangée suivante, donc décale les montants les uns par rapport
   aux autres. */
.jv-bc-ref { display: block; font-size: var(--t-meta); color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* La colonne de droite porte DEUX grandeurs, l'une sous l'autre : le montant
   puis la livraison. Alignées à droite toutes les deux, elles se descendent du
   regard comme deux colonnes — c'est ce qui remplace la troisième piste. */
.jv-bc-val { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums;
  color: var(--text-primary); padding-left: var(--space-2) !important; }
.jv-bc-liv { display: block; font-size: var(--t-meta); font-weight: 400; color: var(--text-muted); }
/* La NATURE de la date — confirmée par le fournisseur, simplement prévue, ou
   rien. Sans elle, trois dates de statuts très différents se ressemblent. */
.jv-bc-note { color: var(--text-muted); }
.jv-bc-retard { color: var(--error); font-weight: 600; }
.jv-bc-retard .jv-bc-note { color: var(--error); }
.jv-bc-ok { color: var(--state-pine-dot); }
.jv-bc-inconnu { color: var(--text-muted); }
/* Le seul état d'une facture fournisseur qui DEMANDE un geste (« En attente » =
   quelque chose bloque, cf. la colonne État de /supplier-invoices). Les autres
   restent muets : une colonne n'a qu'une teinte à dépenser, et « Validée » est
   le cas ordinaire. */
.jv-bc-attente { color: var(--state-amber-fg); }
/* ── LE PIED D'UNE LISTE BORNÉE ────────────────────────────────────────────
   ⚠ IL N'EST PAS DÉCORATIF : la carte montre 10 factures fournisseur sur 70.
   Sans cette ligne, « 10 » se lit « il y en a 10 » — et l'onglet où elles
   vivent toutes n'est nommé nulle part. */
.jv-bc-pied { margin: var(--space-2) 0 0; font-size: var(--t-meta); }
.jv-bc-pied a { color: var(--primary); font-weight: 600; text-decoration: none; }
.jv-bc-pied a:hover { text-decoration: underline; }

/* ── LA RANGÉE — autant de pistes que de cellules ──────────────────────────
   ⚠ JAMAIS UN `repeat(N)` EN DUR, ET JAMAIS `auto-fit`. Le compte en dur laisse
   une piste VIDE dès qu'il manque une cellule (une affaire sans encaissement n'a
   que trois jauges) : un quart de carte blanc au bout d'une rangée, qui se lit
   comme une cellule qui n'a pas chargé. `auto-fit`, lui, ne sait pas éviter
   l'orpheline — son plancher ne décide que du nombre de pistes, et il existe
   toujours une largeur où trois tiennent et quatre non. Le rendu POSE donc son
   compte (`--jv-n`), et les replis sont 2 puis 1 : jamais 3, qui laisserait la
   quatrième seule sur sa rangée.
   ⚠ ET LES SEUILS PORTENT SUR LA CARTE, PAS SUR LA FENÊTRE. La largeur utile
   dépend aussi de la barre latérale (250 px au repos, 68 px en mode rail) : deux
   fenêtres de 1280 px n'offrent pas la même carte, et un `@media` se tromperait
   dans l'un des deux cas. */
/* ── LES DEUX COLONNES DE LA CARTE ────────────────────────────────────────
   Demande Olivier (12-09-2026) : « [les jauges] plutôt en colonne sous la marge
   projetée, et les autres infos à droite ». À GAUCHE les VITAUX — le chiffre qui
   répond, puis les quatre parts qui disent où en est l'exécution ; à DROITE ce
   qui explique — à quoi on se compare, où part l'argent, et quand.
   ⚠ LA COLONNE DE GAUCHE EST BORNÉE. Au-delà de ~340 px, « 168 481,50 € sur
   697 311,66 € » cesse de replier et la colonne mange la place du graphe, qui
   est le seul objet de la carte à avoir vraiment besoin de largeur. */
/* ⚠ `--space-7` N'EXISTE PAS (le socle va de 1 à 6, puis 8, 10, 12) et
   `var(--inconnu)` SANS REPLI est invalide au calcul : la propriété retombe à sa
   valeur initiale. Écrit ici, la gouttière valait 0 et le rembourrage de la
   colonne de droite aussi — les deux colonnes se touchaient, texte contre filet.
   Rien ne le signale : ni erreur, ni avertissement, juste deux colonnes collées.
   `php app-core/cli/probe-css-tokens.php --app=airfact` les compte.
   ⚠ ET PAS D'`align-items: start` : le filet est le `border-left` de la colonne
   de droite, il ne peut donc mesurer que ce que cette colonne mesure. En `start`
   elle s'arrête à son contenu — le trait s'interrompait au milieu de la carte,
   la colonne de gauche étant deux fois plus haute. En étirement (le défaut), il
   court sur toute la hauteur. */
/* ⚠ UNE MARGE À GAUCHE, LE CONTENU À DROITE — et la colonne de gauche a
   MAIGRI le 12-09-2026 (soir). Elle valait `minmax(400px, 460px)`, ce qui
   laissait 490 px à droite sur un écran de 1280 avec la barre latérale : le
   compte de résultat, qui en réclame 604, s'y serait mis à défiler
   horizontalement. 280 px suffisent aux metrics — un taux, quatre jauges, dix
   commandes — et rendent à droite ce dont un tableau de chiffres a besoin.
   Sous 820 px de carte, les deux colonnes s'empilent (requête de CONTENEUR :
   la largeur utile dépend de la barre latérale, pas de la fenêtre). */
/* ── TROIS ZONES, ET L'ORDRE DU DOM EST L'ORDRE DE LECTURE ────────────────
   Refonte du 13-09-2026. Le partage s'appelait `.jv-split` et ses deux fentes
   étaient nommées par POSITION (`-g` / `-d`) : la colonne de gauche était UN
   élément contenant marché + marge + dix bons de commande, et aucune règle CSS
   ne pouvait en extraire le troisième enfant. Replié sur un portable (carte de
   732 px, sous le seuil), l'écran servait donc les BC AVANT les jalons, les
   jauges et la chronologie — « où en est-on » commençait à 872 px, deux écrans
   plus bas, derrière le bloc le moins urgent de la carte.
   Les zones sont maintenant nommées par RÔLE et posées dans l'ordre de
   priorité ; à deux colonnes, la grille les RANGE (`grid-template-areas`) sans
   que le DOM bouge — donc sans que l'ordre de lecture, de tabulation ni celui
   du lecteur d'écran changent.
   ⚠ `grid-template-rows: auto 1fr` N'EST PAS DÉCORATIF. La zone d'état enjambe
   les deux rangées ; sur deux pistes intrinsèques, la grille lui distribuerait
   sa hauteur excédentaire, la première rangée grandirait, et les bons de
   commande décrocheraient de la marge par un trou de plusieurs centaines de
   pixels. Une piste FLEXIBLE en seconde position laisse la première se régler
   sur son seul contenu.
   ⚠ ET CHAQUE ZONE EST SON PROPRE CONTENEUR : les rangées de cellules replient
   sur la largeur de LEUR colonne, jamais sur celle de la carte. À 1280 px, une
   carte de 982 laisse 272 px à la zone étroite, où quatre cellules n'auraient
   que 60 px — « 697 311,66 € » en demande 105. */
.jv-synthese { display: flex; flex-direction: column; }
.jv-zone { min-width: 0; display: flex; flex-direction: column; container-type: inline-size; }
/* Empilées, les zones se séparent d'un filet — la règle vaut pour la colonne
   unique ET pour les deux zones qui partagent la piste étroite. */
.jv-zone + .jv-zone { margin-top: var(--space-6); padding-top: var(--space-6);
                      border-top: 1px solid var(--border); }
/* Le rythme INTERNE d'une zone. ⚠ Le filet est porté par le GROUPE lui-même :
   un groupe qui ne se rend pas (affaire sans planning, affaire neuve sans
   avancement) emporte son trait avec lui. Un `border-bottom` sur le précédent,
   ou un sélecteur d'adjacence, laisserait un trait orphelin en bas de zone. */
.jv-zone > .jv-groupe { margin-top: var(--space-6); padding-top: var(--space-6);
                        border-top: 1px solid var(--border); }
/* ⚠ LE PREMIER D'UNE ZONE N'A RIEN À SÉPARER : son filet se poserait juste sous
   le haut de la carte, et un trait qui ne sépare rien est un trait qu'on
   remarque. */
.jv-zone > .jv-groupe:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
/* ── LA RÉPARTITION PREND LE FILET DES AUTRES GROUPES (14-09-2026) ─────────
   Elle n'en avait pas, et pour une raison qui a cessé d'être vraie : elle SUIVAIT
   la chronologie et la prolongeait (où part l'argent, après combien il en reste),
   si bien qu'un trait entre les deux aurait séparé ce qui se lit d'un trait.
   Olivier l'a demandée AU-DESSUS de « Où en est-on, mois par mois » ; elle tombe
   donc maintenant entre « Facturation prévue » et la chronologie, deux blocs
   qu'elle ne prolonge ni l'un ni l'autre. Sans filet, elle se lisait comme la
   suite du premier.
   ⚠ CE N'EST PAS UNE RÈGLE DE PLUS : c'est celle de `.jv-zone > .jv-groupe`,
   qu'on cesse d'écarter. Les deux crans de marge qu'elle portait pour son compte
   (mesurés avant la refonte, quand la colonne était une boîte flex à gouttière)
   n'ont plus lieu d'être — le groupe porte son rythme comme les autres. */
.jv-zone > .jv-repartition { margin-top: var(--space-6); padding-top: var(--space-6);
                             border-top: 1px solid var(--border); }

@container (min-width: 820px) {
  .jv-synthese { display: grid; column-gap: var(--space-5);
                 grid-template-columns: 272px minmax(0, 1fr);
                 grid-template-rows: auto 1fr;
                 grid-template-areas: "vitaux etat"
                                      "achats etat"; }
  .jv-zone--vitaux { grid-area: vitaux; }
  /* ⚠ UNE MESURE, comme un bloc de texte. Rien ne bornait la zone : à 2504 px
     de fenêtre elle en prenait 1 864, et chaque barre d'avancement mesurait
     443 px — une longueur qu'on ne compare plus. Un écran plus large doit
     donner plus de BLANC, pas des lignes plus longues. Le plafond laisse au
     compte de résultat bien au-delà des 604 px qu'il réclame. */
  .jv-zone--etat { max-width: 1200px; }
  .jv-zone--achats { grid-area: achats; }
  /* ⚠ LE FILET EST UN `border-left` PORTÉ PAR LA ZONE D'ÉTAT, jamais un
     `border-right` sur celle d'en face : la zone étroite peut se réduire à la
     marge seule, et un trait posé du mauvais côté resterait orphelin. */
  /* ⚠ LE SÉLECTEUR PORTE L'ADJACENCE, ET C'EST UNE QUESTION DE SPÉCIFICITÉ.
     `.jv-zone--etat` seul (0,1,0) perd contre `.jv-zone + .jv-zone` (0,2,0) : la
     zone gardait son filet du haut ET recevait celui de gauche, décalée de 49 px
     sous les jalons. Mesuré — l'écran le disait, la cascade non. */
  .jv-zone + .jv-zone--etat { grid-area: etat; margin-top: 0; padding-top: 0; border-top: 0;
                              border-left: 1px solid var(--border); padding-left: var(--space-5); }
}

/* ⚠ LES SEUILS SONT MESURÉS, SUR LA COLONNE. La cellule la plus large d'une
   rangée de quatre — « 697 311,66 € » — demande 105 px ; quatre d'entre elles et
   trois gouttières de 24 en demandent 492. En dessous, deux pistes (234 px) ;
   en dessous encore, une seule. Jamais trois : la quatrième resterait orpheline. */
.jv-rangee { display: grid; grid-template-columns: repeat(var(--jv-n, 4), minmax(0, 1fr)); gap: var(--space-5) var(--space-6); }
@container (max-width: 500px) { .jv-rangee { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container (max-width: 250px) { .jv-rangee { grid-template-columns: 1fr; } }
/* ── UNE RANGÉE QUI N'EN EST PAS UNE (14-09-2026) ──────────────────────────
   Les jauges d'avancement sont remontées dans la colonne de 272 px, où elles
   doivent s'EMPILER : à deux pistes, chacune tombe à 124 px, et c'est très
   exactement le reproche qui les avait chassées de cette colonne le 12-09 au
   soir. Deux jauges de 272 px valent mieux que quatre de 124.
   ⚠ `--jv-n: 1` NE SUFFIT PAS, et c'est le piège : les deux règles de conteneur
   ci-dessus écrivent `grid-template-columns` EN DUR, sans passer par le jeton —
   à 272 px la rangée repliait donc à deux pistes malgré son `--jv-n: 1`, sans
   la moindre erreur, et c'est la largeur PEINTE (124 px) qui l'a dit.
   ⚠ LA SPÉCIFICITÉ EST DOUBLÉE À DESSEIN (0,2,0), même idiome que
   `.jv-synthese.jv-synthese--paire` : à (0,1,0) elle dépendrait de l'ordre des
   règles dans le fichier, et une réorganisation la rendrait inerte en silence. */
.jv-rangee.jv-rangee--pile { grid-template-columns: 1fr; }

/* ── L'ATOME : LA CELLULE ──────────────────────────────────────────────────
   Les deux vocabulaires convergent ici, et chaque cran n'est écrit qu'une fois. */
.jv-cell { display: flex; flex-direction: column; gap: 2px; min-width: 0;
           color: inherit; text-decoration: none; border-radius: var(--radius); }
.jv-cell[title] { cursor: help; }
a.jv-cell { cursor: pointer; }
a.jv-cell:hover .jv-cell-nom,
a.jv-cell:hover .dash-progress-label { color: var(--text-secondary); }
a.jv-cell:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }

/* ⚠ L'INTITULÉ TIENT SUR UNE LIGNE, ET C'EST UNE CONTRAINTE D'ALIGNEMENT, pas
   d'esthétique. En rangée de quatre dans la colonne de droite, chaque cellule
   fait ~155 px ; « FACTURATION CLIENT HT » en demandait 159 avec l'espacement
   d'origine et passait seule à la ligne — sa valeur se retrouvait un cran plus
   bas que celle de ses trois voisines, et les quatre barres ne s'alignaient
   plus. L'espacement des lettres est ce qui coûte le moins à la lisibilité :
   0,06em sur 21 caractères, c'est 8 px rendus — et ça n'a pas suffi.
   ⚠ ET LE MARCHÉ EST LE MÊME INTITULÉ. Il portait sa propre règle, à
   0,06em et sans réserve : posé au-dessus des deux autres dans la même
   colonne, il en était le seul à coller à son chiffre. Un écran qui écrit
   trois fois le même objet doit l'écrire une fois. */
.jv-cell-nom,
.jv-marche-nom,
.jv-cell--jauge .dash-progress-label { font-size: var(--t-label); font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.03em; color: var(--text-muted); display: flex; align-items: flex-start; gap: 0.4rem;
    min-width: 0; line-height: 1.4; }
/* ⚠ LA RÉSERVE DE HAUTEUR APPARTIENT À LA RANGÉE QUI ALIGNE, PAS À LA CELLULE.
   C'est l'idiome des jalons (« une cellule qui sera remplie plus tard réserve
   sa sous-ligne ») : deux lignes d'intitulé partout, même quand une seule est
   écrite, pour que les quatre valeurs démarrent à la même hauteur et que les
   quatre barres s'alignent. Le prix est une ligne blanche dans trois cellules
   sur quatre — et il ne se paie que là où il achète quelque chose.
   ⚠ POSÉE SUR `.jv-cell-nom` NU, elle suivait la cellule PARTOUT : dans la
   colonne étroite (272 px), où la paire s'empile et n'a donc plus rien à
   aligner, elle ouvrait 15,4 px de blanc sous « MARGE PROJETÉE » et sous
   « MARGE BUDGET » — mais pas sous « MARCHÉ HT », qui n'a jamais eu la règle.
   Trois étiquettes, deux rythmes, et c'est ce que l'œil appelle « pas
   harmonieux ». Mesuré au banc (bancs/marge-vitaux), pas estimé. */
.jv-rangee .jv-cell-nom,
.jv-rangee .jv-cell--jauge .dash-progress-label { min-height: 2.8em; }
/* ⚠ LA PAIRE N'A JAMAIS RIEN EU À ALIGNER PAR LE HAUT DES VALEURS : un taux à
   3 rem et un autre à 1,125 ne se posent pas sur la même ligne, quoi qu'on
   réserve. Ce qu'elle aligne, ce sont ses INTITULÉS — et ils tiennent chacun sur
   une ligne. La réserve n'y achetait donc rien, ni empilée ni côte à côte ;
   elle ne faisait que pousser les deux chiffres de 15,4 px vers le bas. */
.jv-rangee--paire .jv-cell-nom { min-height: 0; }
/* Une seule piste : il n'y a plus de voisine avec qui s'aligner. */
@container (max-width: 250px) { .jv-rangee .jv-cell-nom { min-height: 0; } }
.jv-cell-unite { font-style: normal; font-weight: 600; opacity: 0.75; }
/* ── LE MARCHÉ — LA PREMIÈRE INFORMATION, EN HAUT DE LA COLONNE ────────────
   ⚠ GRAND, MAIS PAS HÉROS. 1,9 rem contre 3 rem : il se lit en premier parce
   qu'il est EN HAUT, pas parce qu'il est le plus gros. Un seul chiffre par
   écran a droit à l'échelle du héros ; et entre une constante (le marché, qui
   ne bouge pas et n'a pas de verdict) et une variable qui peut virer au rouge
   (la marge), c'est la seconde qui doit crier.
   ⚠ ET IL NE PORTE AUCUNE COULEUR D'ÉTAT. Le vert du héros dit « dans le
   budget » ; un marché vert ne voudrait rien dire, et deux verts côte à côte
   diraient que le premier est lui aussi un verdict. */
/* ⚠ MÊME GOUTTIÈRE QUE `.jv-cell` (2 px) ET MÊME PAS QUE LA RANGÉE (`--space-5`) :
   le marché est le premier des trois faits de la colonne, pas un en-tête d'un
   autre genre. Il était à 1 px de son chiffre quand ses deux voisines étaient à
   4, et l'écart qui le suivait ne citait aucun cran commun. */
.jv-marche-tete { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--space-5); }
/* ⚠ 600, PAS 700. Le gras des valeurs du socle est calibré pour un corps de
   liste (14 px), où il sert à détacher un nombre d'un texte. À 30 et 48 px, la
   TAILLE détache déjà : le gras n'ajoute que du poids d'encre, et c'est ce qui
   donnait à la colonne son air de crier. Les deux chiffres d'affichage de la
   carte descendent donc d'un cran ; les valeurs de liste ne bougent pas. */
.jv-marche-val {
  font-size: var(--t-ouverture); font-weight: 600; line-height: 1.1; letter-spacing: -0.02em;
  color: var(--text-primary); font-variant-numeric: tabular-nums;
}
/* ⚠ `.jv-cell-base` A DISPARU AVEC LA SOUS-LIGNE « sur X de marché » : le
   marché est remonté en tête de colonne, il n'a plus à se répéter sous le
   héros. Le dire deux fois sur 200 px de haut, c'est le dire une fois de trop. */

.jv-cell-val,
.jv-cell--jauge .dash-progress-meta { font-size: var(--t-valeur); font-weight: 700; color: var(--text-primary);
    font-variant-numeric: tabular-nums; line-height: 1.3; min-width: 0; }
/* Une case sans donnée le DIT, et ne se fait pas passer pour une valeur. */
.jv-cell-val.is-vide, .jv-cell-val.is-attente { color: var(--text-muted); font-weight: 600; }

.jv-cell-sub,
.jv-cell--jauge .dash-progress-sub { font-size: var(--t-meta); font-weight: 400; color: var(--text-muted);
    line-height: 1.45; margin-top: 1px; }
.jv-cell-sub strong { color: var(--text-primary); font-weight: 600; font-variant-numeric: tabular-nums; }
.jv-cell-sub a { color: var(--primary); font-weight: 600; text-decoration: none; }
.jv-cell-sub a:hover { text-decoration: underline; }

/* La pastille de couleur d'une clé de légende — le même objet que le segment
   qu'elle nomme, en petit. */
.jv-puce { width: 9px; height: 9px; border-radius: 2px; flex: none; }
.jv-puce.is-achats { background: var(--state-blue-dot); }
.jv-puce.is-mo     { background: var(--state-amber-dot); }
.jv-puce.is-marge  { background: var(--state-pine-dot); }
.jv-puce.is-perte  { background: var(--error); }
/* « Sans atterrissage » (tableau de bord) : le marché des affaires dont la
   marge de fin ne se calcule pas encore — un GRIS, ce n'est pas un poste. */
.jv-puce.is-reste  { background: var(--gray-300); }

/* ── LE HÉROS — la SEULE exception d'échelle, une fois par écran ───────────
   ⚠ L'ENCRE DU HÉROS N'EST PAS LA TEINTE DU STATUT. Les tokens `--success` /
   `--warning` sont calibrés pour des FONDS de pastille et des remplissages :
   posés en texte sur la carte blanche, `--success` (#10b981) tombe à 2,5:1 et
   `--warning` (#f59e0b) à 2,2:1 — sous le plancher de 3:1 du gros texte. Le plus
   gros chiffre de l'écran était donc le moins lisible. On prend les crans
   FONCÉS des mêmes teintes (5,5:1 · 7,1:1 · 6,8:1), et le vert est EXACTEMENT
   celui du segment de marge : le héros EST ce segment, ils le disent de la même
   couleur. Mesuré au navigateur, pas estimé — voir la sonde. */
/* ⚠ LE TAUX ET SON MONTANT SUR LA MÊME LIGNE, ALIGNÉS PAR LA BASE. Le montant
   vivait en sous-ligne, en gras, pendant que la marge budget écrivait le sien
   en APPOINT sur la ligne de son taux : deux nombres de même nature, qu'on
   vient précisément comparer, rendus dans deux grammaires différentes et à deux
   rangs différents. Ils se lisent maintenant tous les deux « taux, puis
   montant », ce qui rend la sous-ligne au QUALIFICATIF — le verdict ici,
   le glissement en face.
   ⚠ `flex-wrap` N'EST PAS DÉCORATIF : « −100,0 % » et un montant à sept chiffres
   ne tiennent pas dans les 272 px de la colonne étroite. Le montant passe alors
   à la ligne au lieu de sortir de la carte. */
/* ⚠ AUCUNE MARGE EN HAUT. À `line-height: 1` un corps de 48 px porte déjà, au
   dessus de ses capitales, l'espace que sa fonte réserve à ses accents : les
   2 px ajoutés s'y empilaient, et le trou sous « MARGE PROJETÉE » faisait le
   double de celui sous « MARCHÉ HT » — deux crans pour le même geste. C'est la
   gouttière de la cellule (2 px) qui vaut pour les trois. */
.jv-cell--hero .jv-cell-val { font-size: var(--t-heros); font-weight: 600; line-height: 1; letter-spacing: -0.025em;
    margin: 0 0 4px; display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 0.5rem; }
/* La sous-ligne du héros est une sous-ligne comme les autres : le `.badge` du
   socle (12 px) tombe exactement sur `--t-meta`, donc sur le même cran que le
   glissement écrit en face. Elle était à `--t-corps` quand elle portait un
   montant — elle n'en porte plus. */
/* ⚠ L'ENCRE SUIT LA CELLULE, PAS UN ANCÊTRE. Elle dépendait de `.jv-marge--x`
   posée sur la bande : le héros sorti dans sa colonne aurait perdu sa couleur le
   jour où on le rendrait ailleurs. Une cellule qui porte une encre porte la
   classe qui la décide. */
/* ⚠ ET L'ENCRE NE CHANGE QUE QUAND LE CHIFFRE LUI-MÊME EST ANORMAL (13-09-2026,
   2ᵉ passe — « un peu plus sobre »). Le héros était peint à la teinte de son
   verdict : sur une affaire saine, le plus gros objet de l'écran était un vert
   saturé, et l'aplat vert du badge lui répondait quinze pixels plus bas. Deux
   masses colorées pour dire « tout va bien », c'est-à-dire pour ne rien dire —
   la couleur ne se remarque plus quand elle est le cas ordinaire.
   Le chiffre est donc à l'encre du texte, et la couleur est rendue au VERDICT,
   en petit (`.jv-verdict`). Seule exception : une marge NÉGATIVE, qui est une
   anomalie du chiffre et pas un jugement porté dessus — elle reste rouge.
   ⚠ Ce qui se perd, assumé : le héros n'est plus EXACTEMENT de la teinte du
   segment de marge de la barre « où part l'argent ». Ils ne vivent plus dans la
   même zone depuis le 13-09 au matin, et c'est la pastille du verdict qui porte
   désormais la teinte de famille. */
.jv-cell--hero.is-success .jv-cell-val,
.jv-cell--hero.is-warning .jv-cell-val { color: var(--text-primary); }
.jv-cell--hero.is-danger  .jv-cell-val { color: var(--state-red-fg); }
.jv-cell--hero.is-neutral .jv-cell-val { color: var(--text-secondary); }
/* Une cellule ORDINAIRE en alerte (tableau de bord : « Factures en retard »
   quand il y en a) — même encre que le héros en danger, jamais la pastille. */
.jv-cell.is-danger .jv-cell-val { color: var(--state-red-fg); }
/* ⚠ ET LA MÊME RÈGLE VAUT POUR LE PETIT TEXTE, où elle est PLUS exigeante : le
   plancher d'un corps de 11 px est 4,5:1, pas 3:1. Les utilitaires du socle
   (`.text-success` → `--success` #10b981, `.text-error` → `--error` #ef4444)
   valent 2,5:1 et 3,8:1 sur blanc — « 59 341,65 € au-dessus du budget » et
   « +10,3 pts » étaient donc les deux lignes les moins lisibles de la carte,
   alors qu'elles portent un verdict. On reprend les crans FONCÉS, c'est-à-dire
   exactement les encres du héros : un écran qui dit « au-dessus du budget » en
   petit et en grand doit le dire de la même couleur.
   ⚠ Portée à la CARTE : le socle sert des dizaines d'écrans posés sur d'autres
   fonds, on ne touche pas à ses utilitaires. */
.jv-essentiel .text-success { color: var(--state-pine-dot); }
.jv-essentiel .text-error   { color: var(--state-red-fg); }
/* Le verdict se pose CONTRE le chiffre qu'il juge — il vivait au bout de la
   rangée d'identité, à l'autre extrémité de la carte, à hauteur de la RÉFÉRENCE
   de l'affaire, qui n'a pas de verdict. */
/* ── LE VERDICT — un point et des mots, jamais un aplat ────────────────────
   Il se pose CONTRE le chiffre qu'il juge — il vivait au bout de la rangée
   d'identité, à l'autre extrémité de la carte, à hauteur de la RÉFÉRENCE de
   l'affaire, qui n'a pas de verdict. Il est seul sur sa sous-ligne depuis que le
   montant est remonté sur la ligne du taux : il commence au même bord que les
   trois étiquettes et les trois chiffres de la colonne.
   ⚠ CE N'EST PLUS UN `.badge` DU SOCLE (13-09-2026, 2ᵉ passe). Les treize
   variantes du socle sont des PASTILLES PLEINES : dans une colonne de 272 px,
   posée sous un chiffre de 48 px, « Dans le budget » devenait un rectangle vert
   de 150 px — la deuxième masse colorée d'un bloc qui n'a rien d'alarmant à
   dire. La pastille pleine garde sa place là où elle marque une LIGNE parmi
   d'autres (une liste, un en-tête) ; ici, où il n'y a qu'un objet à qualifier,
   le point suffit à porter la teinte.
   ⚠ LE MOT RESTE, ET IL RESTE REDONDANT AVEC LA COULEUR : daltonisme, impression
   en noir et blanc. Le point ne remplace pas le verdict, il l'accompagne — même
   doctrine que `.badge-status` du socle, dont il reprend la géométrie (8 px,
   0,85 d'opacité, gouttière de 6 px).
   ⚠ L'ENCRE EST LE CRAN `-fg`, jamais `-dot` : sur 12 px, le plancher est 4,5:1
   et les crans `-dot` sont calibrés pour un fond de pastille. */
.jv-verdict { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; white-space: nowrap; }
.jv-verdict::before { content: ''; width: 8px; height: 8px; border-radius: 50%;
    background: currentColor; opacity: 0.85; flex: none; }
.jv-verdict.is-success { color: var(--state-pine-fg); }
.jv-verdict.is-success::before { background: var(--state-pine-dot); opacity: 1; }
.jv-verdict.is-warning { color: var(--state-amber-fg); }
.jv-verdict.is-warning::before { background: var(--state-amber-dot); opacity: 1; }
.jv-verdict.is-danger  { color: var(--state-red-fg); }
.jv-verdict.is-danger::before  { background: var(--state-red-dot); opacity: 1; }

/* ── LA PAIRE : la marge projetée et la marge budget, CÔTE À CÔTE ─────────
   Elles ne veulent rien dire l'une sans l'autre : séparées par le filet des deux
   colonnes, il fallait traverser la carte pour les comparer.
   ⚠ TROIS RANGS, LES MÊMES DES DEUX CÔTÉS : l'intitulé, le taux suivi de son
   montant, puis ce qui qualifie (le verdict ici, le glissement en face). Tant
   que le héros écrivait son montant en sous-ligne, les deux cellules n'avaient
   ni le même nombre de lignes ni le même ordre, et il fallait un alignement par
   le BAS plus un `padding-bottom` de rattrapage pour que ça retombe à peu près
   juste — deux corrections pour un défaut de grammaire. Les rangs étant
   désormais identiques, l'alignement par le HAUT suffit : les deux intitulés
   partent de la même ligne, et tout ce qui suit aussi.
   ⚠ Le héros garde sa piste : `auto` le laisserait grossir avec son badge et
   pousser la marge budget hors de la colonne sur une affaire en danger. */
/* ⚠ PAS DEUX PISTES ÉGALES. `jvRangee` en pose deux de même largeur ; or le
   héros tient en ~150 px et la marge budget en demande ~230 (« 34,5 %
   240 411,20 € »). À 1280 px la colonne fait 460, chaque piste 220, et le
   montant SORTAIT de la carte — mesuré au navigateur, invisible à la lecture.
   Le héros prend donc ce qu'il lui faut, la marge budget prend le reste. */
.jv-rangee--paire { grid-template-columns: auto minmax(0, 1fr); align-items: start; gap: var(--space-5); }
/* ⚠ LA MARGE BUDGET EST UNE RÉFÉRENCE, PAS UNE SECONDE RÉPONSE. Son taux était
   à l'encre du texte et au même gras que le marché : trois nombres noirs
   empilés, dont un seul répond à la question de la colonne. Elle passe au cran
   SECONDAIRE — on la lit quand on la cherche, elle ne se dispute plus le regard
   avec le chiffre qu'elle sert à juger. */
.jv-rangee--paire .jv-cell:not(.jv-cell--hero) .jv-cell-val { color: var(--text-secondary); }
/* Sous ~520 px de carte, la paire s'empile : deux chiffres côte à côte dont l'un
   est à 3 rem n'ont plus de place, et un montant coupé ne compare rien. */
@container (max-width: 390px) { .jv-rangee--paire { grid-template-columns: 1fr; } }

/* ── OÙ PART L'ARGENT — une barre, trois segments, 100 % = le marché ───────
   ⚠ Elle a REMPLACÉ une jauge à deux barres Budget / Projetée (12-09-2026,
   « pas clair ») : deux longueurs presque identiques à comparer sur une piste
   dont le maximum — 1,25 × la plus grande valeur — n'était écrit nulle part.
   Ici l'échelle est le MARCHÉ, elle est écrite au-dessus, et le héros est
   littéralement le segment vert. */
.jv-repartition { margin-top: var(--space-6); }
/* Le titre du bloc (14-09-2026) prend le MÊME pas que celui de la chronologie —
   c'est le seul autre `.jv-bloc-titre` de cette colonne, et deux titres de même
   rang qui ne respirent pas pareil se lisent comme deux rangs. */
.jv-repartition .jv-bloc-titre { margin-bottom: var(--space-3); }
/* ⚠ 14 px, PAS 22. Une barre est un repère, pas un bandeau : à 22 px sur 1 300
   de large, trois aplats saturés devenaient l'objet le plus lourd de l'écran et
   écrasaient le chiffre qui est la réponse. */
.jv-rep-barre { position: relative; display: flex; height: 14px; border-radius: 4px; overflow: hidden;
                background: var(--color-surface-2); }
.jv-rep-seg   { display: block; height: 100%; min-width: 2px; }
/* ⚠ LE BLANC SÉPARE, PAS UN TRAIT — ET IL NE PREND PAS DE LARGEUR. Deux aplats
   qui se touchent se lisent comme un seul dégradé ; il faut 2 px de la couleur
   de la carte entre eux. Mais un vrai `gap` de flex les rétrécirait, et la piste
   ne fermerait plus sur le marché — l'assertion qui prouve qu'aucun poste ne
   manque tomberait, et avec elle la seule garantie de la barre. On peint donc le
   blanc EN DEDANS, sur le bord gauche de chaque segment qui en suit un autre :
   la géométrie ne bouge pas d'un pixel, seule la peinture change. */
.jv-rep-seg + .jv-rep-seg { box-shadow: inset 2px 0 0 var(--surface); }
/* Les trois teintes sont VALIDÉES contre la carte blanche (bandes de clarté,
   plancher de chroma, séparation deutan/protan/tritan, contraste ≥ 3:1) — le
   vert vif `--state-green-dot` échouait au contraste (2,2:1) et se confondait
   avec l'ambre sous deutéranopie (ΔE 7,1). Le cran pine passe les six. */
.jv-rep-seg.is-achats { background: var(--state-blue-dot); }
.jv-rep-seg.is-mo     { background: var(--state-amber-dot); }
.jv-rep-seg.is-marge  { background: var(--state-pine-dot); }
.jv-rep-seg.is-reste  { background: var(--gray-300); }
/* ── LES REPÈRES DE BUDGET (14-09-2026) ────────────────────────────────────
   Un trait par poste, là où son segment finirait s'il tenait son budget : la
   queue entre le repère et la fin du segment EST le dépassement, sans un chiffre
   de plus.
   ⚠ ILS VIVENT AU-DESSUS DE LA BARRE, PAS DEDANS, et pour deux raisons qui se
   cumulent :
     · `.jv-rep-barre` porte `overflow: hidden` (il faut bien que les aplats
       s'arrêtent à l'arrondi) — un repère qui dépasse en haut y serait ROGNÉ ;
     · un trait de 2 px POSÉ SUR les aplats se confondrait avec la séparation
       entre deux segments, qui est exactement ça : 2 px peints en dedans. Deux
       traits identiques pour deux sens opposés, c'est pire que pas de repère.
   La couche a la largeur de la barre, donc les `left: %` tombent au même pixel.
   ⚠ ET ELLE EST `aria-hidden` : chaque repère porte déjà son poste, son budget
   et son écart dans un `title`, et la rangée de clés dit les montants. Une
   lecture d'écran n'a rien à faire d'une frise de traits. */
/* ⚠ 7 px ET L'ENCRE DE TEXTE, pas 5 px en gris secondaire — premier jet, relu à
   l'écran : les repères étaient là et ne se VOYAIENT pas, ce qui est le seul
   défaut qu'un repère ne peut pas se permettre. Ils restent fins (2 px) : c'est
   un repère, pas une quatrième série. */
.jv-rep-budgets { position: relative; height: 7px; margin-bottom: 3px; }
.jv-rep-budget  { position: absolute; bottom: 0; width: 2px; height: 7px;
                  margin-left: -1px; background: var(--text); opacity: 0.55; border-radius: 1px; }
/* ── LA LÉGENDE DU REPÈRE ───────────────────────────────────────────────────
   « Zéro texte ajouté » valait pour les CHIFFRES : aucun montant ne bouge. Mais
   une VARIABLE neuve du dessin doit être nommée, sinon les traits sont du bruit
   — personne ne devine « budget » devant une frise de tirets, et le `title` au
   survol n'existe pas pour qui ne survole pas. Elle se pose au bout du titre,
   là où il y a de la place, et disparaît avec les repères. */
.jv-repartition .jv-bloc-titre { display: flex; align-items: baseline; gap: var(--space-3); }
.jv-rep-budget-cle { font-size: var(--t-meta); font-weight: 400; letter-spacing: 0;
                     text-transform: none; color: var(--text-muted); display: inline-flex;
                     align-items: center; gap: 0.3rem; }
.jv-rep-budget-cle i { display: inline-block; width: 2px; height: 9px; border-radius: 1px;
                       background: var(--text); opacity: 0.55; }
/* Le repère « ici s'arrête le marché » — posé SEULEMENT quand le coût le
   dépasse. Sans lui, une affaire en perte rendrait une barre pleine d'achats,
   impossible à distinguer d'une affaire à marge nulle. */
.jv-rep-marche { position: absolute; top: -2px; bottom: -2px; left: var(--jv-marche, 100%); width: 2px;
                 background: var(--text); opacity: 0.75; }
.jv-rangee--legende { margin-top: var(--space-4); }
/* ── LES ACHATS, DÉCOMPOSÉS PAR GROUPE ────────────────────────────────────
   ⚠ UNE SEULE TEINTE À DOUZE VALEURS, pas douze teintes. Les groupes sont un
   DÉTAIL des achats, pas douze catégories concurrentes ; et il n'existe pas
   douze teintes qui se distinguent (le validateur refuse déjà deux bleus
   voisins). Le rang porte l'information : du plus gros au plus petit.
   La liste répète EXACTEMENT les valeurs de la barre — même ordre, même
   opacité — sinon elle ne serait pas une légende mais un second tableau. */
.jv-rep-groupes { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4);
                  margin: var(--space-4) 0 0; font-size: var(--t-meta); color: var(--text-muted); }
.jv-rep-groupe { display: inline-flex; align-items: center; gap: 0.35rem; white-space: nowrap; }
.jv-rep-groupe strong { color: var(--text-primary); font-weight: 600; font-variant-numeric: tabular-nums; }
.jv-rep-groupe i { width: 8px; height: 8px; border-radius: 2px; flex: none;
                   background: var(--state-blue-dot); }

/* Les jauges s'empilent : elles se lisent ENSEMBLE — c'est l'état d'avancement
   de l'affaire — et elles expliquent le chiffre qui les surplombe. */
/* ⚠ « SUR X » PASSE À LA LIGNE, ET C'EST CE QUI REND LA RANGÉE POSSIBLE.
   « 342 809,42 € sur 697 311,66 € » demande 220 px ; une cellule de rangée de
   quatre en offre 155 dans la colonne de droite. Laissée libre, la ligne se
   coupait N'IMPORTE OÙ — « 342 809,42 € sur » puis « 697 311,66 € » — et les
   quatre cellules prenaient des hauteurs différentes. C'est exactement le
   défaut qui avait fait passer ces jauges en colonne le matin même. En bloc, la
   coupure tombe là où elle a un sens : la valeur, puis ce à quoi elle se
   rapporte. Chaque ligne tient, et les quatre cellules ont la même hauteur. */
.jv-cell--jauge .jv-av-sur { display: block; }

/* ⚠ LES JAUGES SONT UNE RANGÉE, PAS UNE COLONNE (12-09-2026, soir). C'est
   `jvRangee` qui pose la grille et son nombre de pistes ; ce bloc n'a plus qu'à
   ne rien imposer. Un `flex-direction: column` resté ici les remettrait en pile
   DANS la rangée — une piste par jauge, chacune pleine hauteur. */
.jv-avancements { min-width: 0; }

/* ── LES JAUGES — le balisage du socle, rangé au cran de la cellule ────────
   ⚠ ON N'AJUSTE PAS LE SOCLE, ON AJUSTE SON CONTEXTE. `dash-progress-head` est
   un `flex` en `space-between` : l'intitulé et le montant se repoussaient aux
   deux bouts d'une cellule large, avec un demi-mètre d'écran entre la question
   et sa réponse. Empilés, ils redeviennent la cellule — intitulé, valeur, piste,
   reste à faire — et les quatre jauges s'alignent sur les quatre jalons du
   dessus. */
.jv-cell--jauge .dash-progress-row  { display: flex; flex-direction: column; gap: 2px; }
.jv-cell--jauge .dash-progress-head { display: flex; flex-direction: column; align-items: stretch; gap: 2px;
                             margin-bottom: 0; font-size: inherit; }
/* ⚠ UN SECOND NOMBRE SUR LA LIGNE DE VALEUR EST UN APPOINT, PAS UNE RÉPONSE.
   « sur 697 311,66 € » (la base d'une jauge) et « 240 411,20 € » (le montant de
   la marge budget) disent de quoi le premier nombre est la part : au même corps
   et au même gras, la cellule rendrait deux nombres de force égale et ne
   répondrait plus à sa propre question. Un seul cran, deux noms — l'un vient du
   balisage du socle, l'autre du nôtre. */
/* ⚠ ET C'EST UN NOMBRE, PAS UNE NOTE : `--text-secondary` et non `--text-muted`,
   des chiffres TABULAIRES comme partout ailleurs, et jamais l'encre d'état du
   héros — un montant peint en vert se lirait comme un second verdict, alors
   qu'il n'en porte aucun. L'espacement négatif du héros ne descend pas non plus
   dans un corps de 14 px, où il serrerait les chiffres. */
.jv-av-sur,
.jv-cell-appoint { color: var(--text-secondary); font-weight: 400; font-size: var(--t-corps);
    font-variant-numeric: tabular-nums; letter-spacing: normal; }
/* ⚠ UN `<strong>` DANS UN PARENT À 700 CALCULE À 900. `font-weight: bolder` est
   RELATIF : posé dans la ligne de valeur d'une jauge (déjà à 700), il rendait un
   cran que rien d'autre de la carte n'emploie — quatre montants plus gras que le
   héros lui-même. La valeur porte sa graisse ; ce qu'elle contient l'hérite. */
.jv-cell--jauge .dash-progress-meta strong { font-weight: inherit; }
/* Le nom d'un fournisseur est du CORPS, pas un demi-gras : le cran 500 n'existe
   nulle part ailleurs sur cette carte. */
.jv-bc-qui a { font-weight: 400; }
/* ⚠ UNE JAUGE À ZÉRO DOIT SE VOIR COMME UNE JAUGE. Le socle peint la piste en
   `--gray-100` : posée sur la carte BLANCHE, elle disparaît, et une affaire qui
   n'a encore rien facturé ni engagé — trois jauges à 0 % — n'offre plus que
   trois filets fantômes là où il faut lire « rien n'est parti ». Un cran plus
   foncé, et seulement DANS cette carte : le socle sert des dizaines de tableaux
   de bord posés sur d'autres fonds, on n'y touche pas. */
.jv-cell--jauge .dash-progress-track { height: 4px; border-radius: 2px; background: var(--gray-200); margin-top: 5px; }
.jv-cell--jauge .dash-progress-fill  { border-radius: 2px; }
/* ⚠ LA CARTE N'A QUE QUATRE ENCRES. Les variantes du socle sont peintes avec
   `--success` (#10b981) et `--warning` (#f59e0b) — les crans VIFS. Posés à
   quatre centimètres du vert pine du segment de marge, ils donnaient DEUX verts
   dans la même carte : exactement l'incohérence qu'on est en train de retirer.
   On les range sur les quatre teintes de la carte, ici seulement — le socle
   sert des dizaines de tableaux de bord et garde les siennes. */
.jv-cell--jauge .dash-progress-fill--success { background: var(--state-pine-dot); }
.jv-cell--jauge .dash-progress-fill--info    { background: var(--state-blue-dot); }
.jv-cell--jauge .dash-progress-fill--warning { background: var(--state-amber-dot); }
.jv-cell--jauge .dash-progress-fill--danger  { background: var(--state-red-dot); }
.jv-cell--jauge .dash-progress-sub   { margin-top: 4px; }

/* ── LA CHRONOLOGIE — deux teintes, trois séries ──────────────────────────
   ⚠ LES DEUX AIRES SONT LA MÊME TEINTE À DEUX VALEURS. `commande` et `achat`
   sont le MÊME poste (les achats) à deux stades : l'enveloppe engagée, et ce qui
   en est déjà facturé. Trois teintes auraient demandé deux bleus — et deux
   bleus ne se distinguent pas (ΔE 13,9 en vision normale, sous le plancher de
   15). Le bleu garde ici le sens qu'il a dans la barre de répartition juste en
   dessous : Achats.
   ⚠ LE VIOLET VIENT DU SOCLE DEPUIS LE 14-09-2026 (Olivier : « garde les
   couleurs de la plateforme, consistency please »). Il était choisi à la main —
   #7c3aed et sa valeur basse #a78bfa — avec, en commentaire, « aucun token ne la
   porte ». C'était FAUX : `--state-violet-fg` / `--state-violet-dot` existent
   dans `app-core/tokens.css`, et l'encre des étiquettes de flux (plus bas) les
   employait DÉJÀ. L'écran portait donc deux violets pour une seule série.
   ⚠ « POURQUOI LE CA EST EN VIOLET ? » (Olivier, 14-09-2026) — LA RÉPONSE EST
   MESURÉE, et elle vaut d'être écrite ici une fois pour toutes : parce que les
   trois autres teintes de la carte sont DÉJÀ prises. Le bleu est « Achats »,
   l'ambre « Main-d'œuvre », le vert « Marge » — et la facturation client est une
   QUATRIÈME variable, pas une des trois. Balayage des familles du socle, ΔE00
   contre le plus proche voisin de la carte, et contraste de la valeur basse sur
   blanc (plancher 3:1 pour un objet graphique) :
       violet   pire voisin 18,4 (bleu)   ·  8,98 / 4,23   ✓
       teal     pire voisin 13,1 (vert)   ·  7,58 / 2,49   ✗✗
       sky      pire voisin 14,8 (bleu)   ·  7,56 / 2,77   ✗✗
       indigo   pire voisin 12,0 (bleu)   ·  9,93 / 4,47   ✗
       navy     pire voisin 14,3 (bleu)   · 13,08 / 3,85   ✗
       graphite couple 14,4 (trop proche) · 14,63 / 7,58   ✗
   Le violet est la SEULE famille qui tienne le plancher de 15 contre tous ses
   voisins ET les 3:1 de sa valeur basse. Un navy — « la couleur de la
   plateforme » au sens littéral — est un bleu : il croiserait la courbe des
   commandes à ΔE00 14,3, dans le même dessin.
   La paire du socle est meilleure sur tout ce qu'on mesurait :
     · séparation du couple      ΔE00 18,6 (protan 18,7) — l'ancienne : 22,2 / 17,2
     · contre le bleu « Achats » ΔE00 30,5 (protan 24,9) — l'ancienne : 23,8 / 12,6
     · sur le fond de la carte   8,98:1 et 4,23:1        — l'ancienne : 5,70 / 2,72
   La valeur basse passe ainsi le plancher de 3:1 des objets graphiques, qu'elle
   ne tenait pas. L'encodage secondaire (l'aire contre le trait, le pointillé)
   RESTE : deux violets ne se départagent toujours pas en protanopie. */
.jv-chrono { min-width: 0; container-type: inline-size; }
.jv-chrono .jv-bloc-titre { margin-bottom: var(--space-3); }
/* ⚠ `preserveAspectRatio: none` ÉTIRE AUSSI LES TRAITS. Le graphe doit prendre
   toute la largeur de sa colonne quelle qu'elle soit ; sans
   `vector-effect: non-scaling-stroke`, une courbe de 2 px se rendrait à 0,7 px
   sur une colonne large et à 5 px sur une étroite. */
/* ⚠ LE GRAPHE DÉCLARE UN RAPPORT, JAMAIS UNE HAUTEUR (13-09-2026). Il portait
   `height: 120px` et prenait toute la largeur de sa colonne : son rapport était
   donc une variable LIBRE, et il atteignait 15:1 sur un écran large (1 843 px de
   tracé pour 120 px de haut, mesuré à 2504). Une marche de +185 k€ y devient une
   pente douce — le dessin ment sur sa raideur, `preserveAspectRatio: none`
   étirant la géométrie sans jamais toucher la hauteur.
   Le plancher garde EXACTEMENT la hauteur d'avant aux largeurs ordinaires
   (779 px de tracé ⇒ 97 px calculés, donc 120), le rapport ne joue qu'au-delà,
   et le plafond empêche le graphe de manger l'écran. */
/* ⚠ `aspect-ratio` NE CONVIENT PAS ICI, ET C'EST MESURÉ : le rapport est
   BIDIRECTIONNEL. Sous le plancher de 120 px, la hauteur devient définie et
   Chromium en RE-DÉDUIT la largeur — 120 × 8 = 960 px dans une colonne de 640,
   soit 316 px de débordement et un ascenseur dans la carte. La hauteur se règle
   donc sur une unité de CONTENEUR : 12 % de la largeur du graphe, bornée. */
.jv-chrono-cadre { height: clamp(120px, 12cqi, 260px); }
.jv-chrono-svg { display: block; width: 100%; height: 100%; }
/* ⚠ LES GRADUATIONS SONT DU HTML POSÉ SUR LE GRAPHE, pas du `<text>` SVG :
   `preserveAspectRatio: none` déforme AUSSI les glyphes, et les étiquettes
   sortaient étirées à l'horizontale. Les traits, eux, restent dans le SVG —
   un trait horizontal étiré reste un trait horizontal. */
/* ⚠ LES GRADUATIONS SONT À GAUCHE, ET LE TRACÉ COMMENCE APRÈS. Posées à droite
   sur le graphe, elles se superposaient à la fin des courbes — c'est-à-dire à
   l'endroit le plus lu, celui où l'on regarde où l'on en est aujourd'hui. La
   gouttière est la même pour les dates, sinon « 03/26 » ne tombe plus sous le
   premier point. */
.jv-chrono-cadre { position: relative; padding-left: 46px; }
.jv-chrono-echelle { position: absolute; inset: 0; pointer-events: none; }
.jv-chrono-echelle span { position: absolute; left: 0; transform: translateY(50%);
                          font-size: var(--t-meta); color: var(--text-muted); line-height: 1;
                          font-variant-numeric: tabular-nums; }
.jv-chrono-grille { stroke: var(--border); stroke-width: 1; vector-effect: non-scaling-stroke; }
/* ── L'AXE DES MOIS ────────────────────────────────────────────────────────
   Chaque étiquette est posée À L'ABSCISSE DE SON POINT, jamais répartie par un
   `space-between` : les points du tracé vont de 0 à W (le premier COLLE au bord
   gauche, le dernier au bord droit), pas de centre de case en centre de case —
   une répartition les décalerait toutes d'une demi-case. La rangée et le graphe
   partagent la même origine (46 px de gouttière) et la même largeur, donc un
   pourcentage y désigne la même colonne.
   ⚠ HAUTEUR EXPLICITE : les étiquettes sont absolues, la rangée retomberait à
   zéro et la légende viendrait se coller sous le tracé. */
.jv-chrono-dates { position: relative; height: 0.85rem; margin: var(--space-1) 0 0 46px;
                   font-size: var(--t-meta); color: var(--text-muted); font-variant-numeric: tabular-nums; }
.jv-chrono-date { position: absolute; top: 0; line-height: 1; white-space: nowrap; }
.jv-chrono-date--centre { transform: translateX(-50%); }
/* Les mois projetés sont plus pâles : l'axe dit lui aussi où la mesure s'arrête. */
.jv-chrono-date.is-prevu { opacity: 0.6; }
/* ── LES MOUVEMENTS DE FACTURATION, POSÉS SUR LE TRACÉ (13-09-2026) ──────
   Une étiquette par mois où la courbe BOUGE — « +174 k€ », « −16 k€ » —,
   posée à l'abscisse de son point et juste au-dessus de lui (`bottom` en %,
   `margin-bottom` pour le décollement). Le cadre porte déjà `position:
   relative` pour l'échelle.
   ⚠ LA GRAISSE DÉPARTAGE LE MESURÉ DU PRÉVU, PAS LA TEINTE : la valeur basse du
   violet, celle du pointillé, ne vaut que 4,2:1 sur blanc — assez pour un objet
   graphique, pas pour une encre de chiffre.
   ⚠ L'ENCRE EST LE CRAN `-fg`, et depuis le 14-09-2026 la COURBE l'est aussi :
   les deux étaient deux violets différents, l'un choisi à la main pour le tracé,
   l'autre pris au socle pour l'encre. C'est ce cran-là que la règle réclamait —
   une étiquette peut tomber DANS l'aplat des achats (bleu à 34 %, soit
   ~rgb(198,216,248)), où l'ancien tracé #7c3aed ne valait plus que 3,8:1.
   `--state-violet-fg` tient 9:1 sur la carte et ~6:1 dans l'aplat le plus
   sombre. Un cran de moins sur l'écran, et la courbe et son chiffre disent enfin
   la même chose. */
.jv-chrono-flux-zone { position: absolute; top: 0; right: 0; bottom: 0; left: 46px; }
.jv-chrono-flux { position: absolute; line-height: 1; white-space: nowrap;
                  margin-bottom: 9px; font-size: var(--t-meta); font-weight: 600;
                  font-variant-numeric: tabular-nums; pointer-events: none;
                  color: var(--state-violet-fg); }
.jv-chrono-flux--centre { transform: translateX(-50%); }
.jv-chrono-flux.is-prevu { font-weight: 400; }
.jv-chrono-aire  { stroke: none; }
/* ⚠ `.jv-chrono-aire.is-commande` A ÉTÉ RETIRÉE LE 13-09-2026 avec le tracé
   qu'elle peignait : deux bleus se recouvraient et le mélange donnait un
   troisième bleu qui n'était la série de personne. Les commandes sont un TRAIT.
   Une règle qui ne peint plus rien survit des années et redécore le premier qui
   réutilise la classe. */
.jv-chrono-aire.is-achat    { fill: var(--state-blue-dot); opacity: 0.34; }
.jv-chrono-trait { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
                   vector-effect: non-scaling-stroke; }
.jv-chrono-trait.is-commande { stroke: var(--state-blue-dot); }
.jv-chrono-trait.is-vente    { stroke: var(--state-violet-fg); }
/* ── LA FACTURATION PRÉVUE — MÊME SÉRIE, STADE SUIVANT ─────────────────────
   Le violet CLAIR est déjà « Prévu » dans le graphe de l'onglet Facturation
   (`.jbs-ca-bar.is-prevu`) : le lecteur qui passe d'un onglet à l'autre retrouve
   la même teinte pour la même chose. Le POINTILLÉ est l'encodage secondaire —
   celui qui reste quand la nuance ne se voit plus (le clair passe sous 3:1 sur
   blanc, et deux violets ne se départagent pas en protanopie).
   ⚠ `vector-effect: non-scaling-stroke` garde AUSSI le pas des tirets constant :
   sans lui, `preserveAspectRatio: none` les étirerait avec le tracé, et une
   colonne large les rendrait en traits pleins. */
.jv-chrono-trait.is-prevu    { stroke: var(--state-violet-dot); stroke-dasharray: 5 4; }
/* La COUTURE : là où la mesure s'arrête et où la prévision commence. Un filet
   discret — il sépare deux régimes de lecture, il ne concurrence pas les
   courbes. */
.jv-chrono-couture { stroke: var(--border); stroke-width: 1; stroke-dasharray: 3 3;
                     vector-effect: non-scaling-stroke; }
/* ⚠ UNE BANDE DE SURVOL DOIT ÊTRE MUETTE. `fill="transparent"` ne suffit pas :
   la cascade de la plateforme donne un contour aux formes SVG, et les sept
   bandes se dessinaient en rectangles noirs par-dessus les courbes. Le
   `pointer-events: all` est ce qui garde le survol malgré un remplissage nul. */
.jv-chrono-survol { fill: transparent; stroke: none; pointer-events: all; }

/* ── LA LÉGENDE : UNE LIGNE QUAND ELLE TIENT, DEUX COLONNES SINON ──────────
   13-09-2026, demande Olivier : « mettre la légende sur une seule ligne ». Les
   quatre clés y tiennent dès que le graphe est large — et il l'est sur les
   écrans où cette page se travaille. En dessous, on RETOMBE sur les deux
   colonnes fixes, jamais sur un repli libre : à quatre clés, un flux laisse
   « Achats facturés » seul sur une seconde ligne (mesuré : une ligne à 779 px
   de colonne, deux à 619 et 676), et ce 3+1 a l'air d'un accident. Les deux
   colonnes rangent les paires comme le dessin les groupe.
   ⚠ LE SEUIL EST UNE REQUÊTE DE CONTENEUR PORTÉE PAR `.jv-chrono`, pas une
   `@media` : la largeur utile du graphe dépend de la barre latérale et du
   partage de la carte, donc deux fenêtres de 1280 px n'offrent pas la même
   légende. Et il est posé sur le PARENT : un élément ne peut pas interroger sa
   propre largeur. */
.jv-chrono-legende { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
                     gap: var(--space-2) var(--space-5);
                     margin-top: var(--space-3); font-size: var(--t-meta); color: var(--text-muted); }
@container (min-width: 800px) {
  .jv-chrono-legende { grid-template-columns: repeat(4, auto); justify-content: start;
                       column-gap: var(--space-6); }
}
.jv-chrono-cle { display: inline-flex; align-items: center; gap: 0.4rem; white-space: nowrap; min-width: 0; }
.jv-chrono-cle strong { color: var(--text-primary); font-weight: 600; font-variant-numeric: tabular-nums; }
.jv-chrono-cle i { width: 9px; height: 9px; border-radius: 2px; flex: none; }
/* ⚠ LA CLÉ MONTRE LA FORME DE SA SÉRIE, PAS SEULEMENT SA TEINTE (13-09-2026,
   demande Olivier : « pour facturation client, prévu et commandes, ce doit être
   des traits »). Trois séries sont TRACÉES : leur clé est un trait de 16 px.
   Une seule est peinte en APLAT — les achats facturés — et elle garde son carré
   pâle, bordé pour rester visible à 9 px. Une pastille carrée pour une courbe
   faisait chercher une surface qui n'existe pas.
   ⚠ ET LE PRÉVU PORTE SON POINTILLÉ, au pas exact du tracé (5 / 4) : c'est
   l'encodage qui dit « ceci n'est pas mesuré », le violet clair ne suffisant pas
   à le dire (il ne se départage pas du foncé en protanopie). */
.jv-chrono-cle i.is-vente,
.jv-chrono-cle i.is-commande,
.jv-chrono-cle i.is-prevu    { width: 16px; height: 2px; border-radius: 1px; }
/* css-color-ok: idem, la pastille de la série */
.jv-chrono-cle i.is-vente    { background: var(--state-violet-fg); }
.jv-chrono-cle i.is-commande { background: var(--state-blue-dot); }
/* css-color-ok: idem, le trait pointillé de la série prévue */
.jv-chrono-cle i.is-prevu    { border-radius: 0;
                               background: repeating-linear-gradient(to right,
                                   var(--state-violet-dot) 0 5px, transparent 5px 9px); }
.jv-chrono-cle i.is-achat    { background: var(--state-blue-dot); opacity: 0.34;
                               box-shadow: inset 0 0 0 1px var(--state-blue-dot); }

/* ── BLOC CHARGE DE L'ATELIER — deuxième carte, MÊME système ───────────────
   Le graphe est rendu par le composant PARTAGÉ (`planning-affaire.js`), habillé
   par `planning-affaire.css` d'app-core, et servi aussi par PLAN. On ne touche
   donc pas à la feuille partagée : tout ce qui suit est porté par
   `.jv-charge-bloc`, c'est-à-dire par CET écran et lui seul.
   Sa propre tête porte déjà « Charge de l'atelier » et la maille — on la masque,
   le bloc a la sienne, avec le lien vers l'onglet qui l'édite. */
.jv-charge-bloc .pl-charge-tete { display: none; }
.jv-charge-bloc .pl-charge-sous { margin-top: 0; }
.jv-charge-bloc .jv-bande-tete  { margin-bottom: var(--space-4); }
/* ⚠ UNE CARTE DANS UNE CARTE N'EST PAS UNE HIÉRARCHIE, C'EST UN DOUBLON. Le
   composant se pose sur un panneau gris à coins arrondis — juste, dans une
   colonne de planning où il est un bloc parmi d'autres ; ici il EST le contenu
   de la carte, et le gris dessinait un second cadre à 12 px du premier. */
.jv-charge-bloc .pl-charge { background: transparent; padding: 0; border-radius: 0; }
/* ⚠ DEUX VERTS DANS UN MÊME ÉCRAN, C'ÉTAIT L'INCOHÉRENCE LA PLUS VISIBLE. Le
   graphe peint « dans la capacité » avec `--state-green-dot` (#22c55e, le cran
   VIF) à quatre centimètres du segment de marge en pine (#047857) : deux verts
   qui ne veulent pas dire deux choses, juste deux réglages. On range le verdict
   de capacité sur les encres de la carte — le SENS ne bouge pas (vert = dans la
   capacité, rouge = au-dessus, et la légende le dit en toutes lettres), seul le
   cran change. Un aplat large veut de toute façon un cran plus foncé qu'un point
   de 8 px : c'est la même règle que pour la barre de répartition. */
.jv-charge-bloc .pl-charge-barre.is-ok,
.jv-charge-bloc .pl-charge-cle.is-ok    { background: var(--state-pine-dot); }
.jv-charge-bloc .pl-charge-barre.is-over,
.jv-charge-bloc .pl-charge-cle.is-over  { background: var(--error-hover); }
/* Les intitulés de série (« Fabrication », « Pose ») prennent le cran des
   intitulés de cellule : c'est la même chose — un nom au-dessus d'une mesure. */
.jv-charge-bloc .pl-charge-nom { font-size: 0.68rem; font-weight: 700; text-transform: uppercase;
                                 letter-spacing: 0.06em; color: var(--text-muted); }
.jv-charge-bloc .pl-charge-sous,
.jv-charge-bloc .pl-charge-legende { font-size: 0.72rem; color: var(--text-muted); }
.jv-charge-bloc .pl-charge-sous { font-style: normal; }
.dash-card-link { font-size: 0.78rem; font-weight: 600; color: var(--primary); text-decoration: none; white-space: nowrap; }
.dash-card-link:hover { text-decoration: underline; }

/* Le trait de séparation avant les trois tableaux : ce qui suit n'est plus la
   synthèse, c'est son détail. */
.jv-detail-titre { margin: var(--space-8) 0 var(--space-4); padding-top: var(--space-5);
                   border-top: 1px solid var(--border); font-size: 0.68rem; font-weight: 700;
                   text-transform: uppercase; letter-spacing: 0.07em; color: var(--text-muted); }

/* Écran étroit : le héros rapetisse. ⚠ LE REPLI DES RANGÉES N'EST PAS ICI — il
   est en requête de CONTENEUR (voir `.jv-rangee`), la largeur utile ne se
   déduisant pas de celle de la fenêtre. Ne reste ici que la taille du chiffre,
   qui est bien une affaire d'écran. */
@media (max-width: 720px) {
  .jv-cell--hero .jv-cell-val { font-size: calc(var(--t-heros) * 0.77); }
}

/* ── FLOW : page unique qui défile ───────────────────────────────────────── */
.jv-flow { max-width: 960px; }
.jv-flow-summary { margin-bottom: var(--space-8); }
/* `.jv-flow-idrow` est partie le 12-09-2026 : flow rend désormais la MÊME
   rangée d'identité que les autres ossatures (`.jv-rail-top`, jvSyntheseTete).
   Deux rangées pour la même chose, c'était deux endroits où les corriger. */
.jv-flow-section { margin-bottom: var(--space-10); scroll-margin-top: calc(var(--topbar-height) + var(--space-4)); }
.jv-flow-h2 {
  display: flex; align-items: center; gap: 0.5rem; margin: 0 0 var(--space-4);
  font-size: 1.05rem; font-weight: 700; padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.jv-flow-h2 svg { color: var(--text-secondary); }

/* ── Invoice list: late-row highlight ─────────────────────────────────────────
   Facture échue et non soldée (classe posée par le JS). La teinte est déclarée
   UNE fois, sur la ligne, via --row-bg / --row-bg-hover : la colonne d'actions
   collante — opaque par construction — suit toute seule, comme le survol (voir
   tables.css). Remplace un `> td { background !important }` qui rejouait le
   mécanisme cellule par cellule pour battre le blanc de la colonne collante :
   même rendu, mais une seule façon de teinter une ligne dans toute la base.

   ⚠ N'Y REMETTEZ PAS DE `background`. Un fond déclaré ici entre en concurrence
   de SPÉCIFICITÉ avec le survol du socle, et c'est ce qui avait cassé la ligne
   le 2026-09-09 : écrite `.table tbody tr.invoice-row-late` (0,2,2), la teinte
   de REPOS battait `.table tbody tr:hover` (0,2,2, mais importé plus tôt) — le
   `<tr>` restait cloué à #fee2e2 pendant que sa colonne d'actions passait seule
   à #fdd5d5. Un JETON ne se bat pas : il est lu par la règle du socle, quelle
   que soit la spécificité du sélecteur qui le pose. C'est tout l'intérêt.
   Mesuré par D4/D5 de cli/probe-invoice-overdue-rule.php. */
tr.invoice-row-late {
  --row-bg: var(--state-red-bg);
  /* css-color-ok: survol de ligne rouge, cran manuel entre --state-red-bg et red-200 — pas de token */
  --row-bg-hover: #fdd5d5;
}
/* ⚠ UNE OMBRE INTERNE, PAS UN `border-left` (retour Olivier, 15-09 : « on ne
   devrait pas voir la bordure derrière le header »).
   `.table` est en `border-collapse: collapse`, où les bords appartiennent à la
   TABLE et non aux cellules : ce liseré était peint sur la couche du tableau,
   que le fond d'un `<th>` collant ne couvre pas. Depuis que l'en-tête est figé
   (`'pane'`), une ligne en retard qui passait sous l'en-tête y laissait voir
   son liseré — 24 px de rouge par-dessus les titres de colonnes, MESURÉS au
   pixel (`ops/ui-capture/bancs/liste-figee-onglets`, section L).
   Une ombre interne, elle, est peinte dans la couche de LA CELLULE, donc sous
   l'en-tête positionné. Même arbitrage que le filet sous le `<thead>` en mode
   `pane`, et pour la même raison.
   ⚠ Elle ne prend AUCUNE place : les premières cellules de toutes les lignes
   s'alignent enfin, alors que le bord en décalait une sur deux de 2 px. */
tr.invoice-row-late > td:first-child {
  box-shadow: inset 4px 0 0 var(--error-hover);
}
/* La mention « en retard » sous l'échéance (colonne Date). Elle appliquait
   déjà LA règle — celle du serveur, App\Services\InvoiceOverdueRule — mais sa
   classe n'était déclarée dans AUCUNE feuille : elle s'écrivait donc dans le
   gris de `.cell-sub`, indistinguable d'une échéance ordinaire. L'écran disait
   « en retard » sans que rien ne le montre.
   La couleur est posée SUR l'élément : une déclaration directe bat la couleur
   HÉRITÉE de `.cell-sub`, quelle que soit la spécificité de celle-ci — c'est
   la cascade, pas un concours de sélecteurs. Mesuré par
   cli/probe-invoice-overdue-rule.php (chromium sans tête). */
.text-late {
  color: var(--state-red-fg);
  font-weight: 600;
}


/* ══════════════════════════════════════════════════════════════════════════
   L'ONGLET RÉGLAGES D'UNE AFFAIRE — UNE PILE DE SECTIONS, UN SEUL FILET
   (refonte du 12-09-2026 : « page construite par additions successives, lui
   redonner une cohérence » — Olivier.)

   Il y avait TROIS dialectes de bloc sur cet onglet : `panel-title` coiffé d'un
   filet écrit EN LIGNE (`const sep = 'border-top:1px…'`, recopié six fois dans
   settings.js), un titre + tableau, et une `.card card--danger` — la seule
   carte de la page. Un seul objet reste : LA SECTION.

       .jv-sect        un titre (section_header) + son contenu, coiffé d'un filet
       .jv-sect--tete  la première : pas de filet au-dessus
       .jv-sect-actions la barre qui FERME le formulaire

   ⚠ LE FILET EST PORTÉ PAR LA SECTION, JAMAIS PAR UNE ADJACENCE. Trois des sept
   sections vivent dans des hôtes DIFFÉRENTS — le formulaire, `#deductionsCardWrap`,
   `#buildProjectsWrap` — et un sélecteur d'adjacence ne traverse pas une
   frontière d'hôte : `.jbg-sect + .jbg-sect` (l. 1810), qui dit la même chose
   pour l'onglet Finance, ne pourrait pas servir ici. C'est la leçon du 0 px
   mesuré entre les deux cartes de la Synthèse.
   ══════════════════════════════════════════════════════════════════════════ */
.jv-sect { margin-top: var(--space-8); padding-top: var(--space-6); border-top: 1px solid var(--border); }
.jv-sect--tete { margin-top: 0; padding-top: 0; border-top: 0; }
/* Le rythme entre sections appartient à la SECTION : le dernier enfant ne
   rajoute pas sa propre marge basse par-dessus (une `.table-container`, un
   `.form-layout`). */
.jv-sect > :last-child { margin-bottom: 0; }

/* La barre qui ferme le formulaire : la note à gauche, le bouton à droite.
   Elle dit ce que le bouton couvre — au-dessous, les blocs s'appliquent seuls. */
.jv-sect-actions { display: flex; align-items: center; justify-content: flex-end;
                   gap: var(--space-4); flex-wrap: wrap; margin-top: var(--space-6); }
.jv-sect-actions .form-help { margin: 0 auto 0 0; max-width: 70ch; }

/* Une valeur que la page ne possède PAS (le client d'une affaire déjà facturée,
   le marché déduit de ses postes, la retenue tenue par les déductions) se rend
   en LIGNE, pas en champ grisé : un `<input readonly>` invite à taper dedans. */
/* Elle se pose sur la même ligne de base que le texte d'un champ voisin —
   le rembourrage vertical de `.form-input` (--space-3), sans sa bordure : dans
   une rangée à deux colonnes, une valeur figée et un champ se lisent ensemble. */
.jv-sect-fige { display: flex; flex-direction: column; gap: 2px; min-width: 0;
                padding: var(--space-3) 0; }
.jv-sect-fige-valeur { font-size: 1rem; font-weight: 500; color: var(--text);
                       font-variant-numeric: tabular-nums; }

/* La PROSE d'un bloc (état vide des déductions, des projets Build, motif de
   suppression) est un enfant DIRECT de la section — à la différence de l'aide
   d'un champ, qui vit dans son `.form-group`. Elle respire ; la dernière, non. */
.jv-sect > p.form-help { margin-bottom: var(--space-4); }
.jv-sect > p.form-help:last-child { margin-bottom: 0; }

/* ── Affaire (rail) — UX 2026-08-25 ─────────────────────────────────────── */
/* Onglet « Plus » : le bouton reprend l'apparence .nav-tab, le menu est le
   composant central .dropdown-menu (toggle générique core.js sur .btn-actions). */
.jv-tab-more { display: inline-flex; position: relative; }
/* `hidden` (jvFitTabs) doit primer sur le display ci-dessus — même raison que
   .nav-tab[hidden] dans navigation.css. */
.jv-tab-more[hidden] { display: none; }
.jv-tab-more .dropdown-menu { min-width: 180px; }
.jv-tab-more .dropdown-item.active { font-weight: 600; color: var(--primary); }

/* Ligne mise à jour par une action : un clignotement bref, puis rien. */
@keyframes jv-row-flash { 0% { background: var(--primary-light); } 100% { background: transparent; } }
tr.row-flash > td { animation: jv-row-flash 1.6s ease-out; }

/* Badge-action (groupe d'achat cliquable) : un badge qui est un bouton. */
.badge.badge-action { cursor: pointer; border: 0; font: inherit; font-size: inherit; line-height: inherit; }
.badge.badge-action:hover { filter: brightness(0.93); }
.badge.badge-action:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* Tableaux de synthèse : aide au survol d'une icône, % en sous-ligne, badge d'état. */
.jbg-help { display: inline-flex; vertical-align: middle; color: var(--text-muted); margin-left: 0.3rem; cursor: help; }
.jbg-sub { font-size: 0.7rem; font-weight: 400; color: var(--text-muted); line-height: 1.1; }
.jbg-state { margin-left: 0.35rem; vertical-align: middle; }
.jbg-sect + .jbg-sect { margin-top: var(--space-8); padding-top: var(--space-6); border-top: 1px solid var(--border); }
.jbg-head .panel-title { font-size: 1rem; }

/* ⚠ Le bloc « écran étroit » qui remettait le VOLET en bande horizontale
   (`.jv-rail-summary-col …`, 11 règles) est parti avec le volet le 12-09-2026,
   et le repli de « À faire » sur son titre est parti avec la carte, le même
   jour. Les blocs de la Synthèse se replient désormais chacun chez eux
   (les `.jv-rangee` de la carte de synthèse) : ils sont dans le flux du contenu, pas
   dans une colonne à part. */
/* ── QUAND LA PLACE MANQUE, LES COLONNES PARTENT DANS L'ORDRE INVERSE DE LEUR
   UTILITÉ, et `min-width` descend AVEC elles — sinon le cadre continue de
   scroller horizontalement pour une largeur que le tableau ne réclame plus.
   L'ordre est : Engagé (une prévision), puis % (un rapport qu'on peut refaire
   de tête depuis les deux colonnes voisines), puis Budget en dernier recours.
   Réalisé et Reste ne partent jamais : c'est là qu'on en est.
   Colonnes : 1 Poste · 2 Budget · 3 Engagé · 4 Réalisé · 5 Reste · 6 %. */
/* ── QUAND LA PLACE MANQUE ────────────────────────────────────────────────
   ⚠ ON NE MASQUE PLUS DE COLONNE. Trois mécanismes ont été mesurés, aucun ne
   tient dans un `table-layout: fixed` :
     · `display: none` sur le `<col>` seul — la piste reste ALLOUÉE (136 px de
       blanc au bout de la rangée à 1024 px, et la colonne des libellés réduite
       d'autant) ;
     · `display: none` sur les `th`/`td` — les cellules restantes se DÉCALENT
       d'un cran sur la liste des `<col>` : « Réalisé » héritait de la largeur
       de « Engagé », et la dernière colonne de celle de sa voisine ;
     · `visibility: collapse` sur le `<col>` — correct sur la colonne, mais le
       tableau RÉTRÉCIT au lieu de redistribuer (592 px rendus dans un cadre de
       724, `min-width` ignoré).
   Ce défaut existait déjà, avant le 12-09-2026, dans le repli à 768 px : il y
   mangeait DEUX pistes. Personne ne l'avait vu parce que personne n'avait
   mesuré la page étroite.
   Les cinq colonnes de chiffres TIENNENT donc à toutes les largeurs utiles, en
   étant simplement plus étroites (et la colonne des libellés coupe à l'ellipse,
   ce qu'elle fait déjà). Sous 560 px — le téléphone — le tableau garde sa
   largeur et c'est le cadre qui défile, comme le dit `min-width`. */
@container (max-width: 560px) {
  .table.jbg-table th, .table.jbg-table td { padding: 0.5rem 0.45rem; }
}

/* ── Échéancier de facturation / facturation prévue (2026-08-25) ────────── */
.jbp-plan { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; font-size: 0.85rem; margin: 0 0 var(--space-3); }
/* Défilement infini (2026-09-10) : les deux chevrons vivent HORS de la boîte
   défilante — ils restent visibles où qu'on en soit dans la bande, et la bande
   garde `min-width: 0` pour pouvoir rétrécir sous son contenu (sans quoi elle
   ne défilerait jamais et pousserait le chevron droit hors de l'écran).
   ⚠ Les cellules ont une largeur FIXE (`flex: 0 0 auto`), pas partagée : sans
   débordement, `scrollWidth === clientWidth` et le navigateur n'émet AUCUN
   événement de défilement — le chargement aux bords n'aurait jamais de
   déclencheur. C'est la contrepartie assumée du scroll infini. */
.jbp-wrap { display: flex; align-items: stretch; gap: 0.35rem; }
.jbp-wrap .jbp-strip { flex: 1 1 auto; min-width: 0; }
.jbp-nav {
    flex: 0 0 auto; width: 26px; padding: 0; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    font-size: 1.1rem; line-height: 1; color: var(--text-secondary);
    border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--surface);
}
.jbp-nav:hover:not(:disabled) { background: var(--gray-50); color: var(--primary); border-color: var(--primary); }
.jbp-nav:disabled { opacity: 0.45; cursor: default; }
.jbp-strip { display: flex; gap: 0; overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); }
.jbp-legend, .jbp-cell { display: grid; grid-template-rows: auto auto auto; row-gap: 2px; padding: 0.45rem 0.5rem; min-width: 92px; border-right: 1px solid var(--border); }
.jbp-cell { flex: 0 0 auto; }
.jbp-legend { flex: 0 0 auto; min-width: 56px; color: var(--text-muted); font-size: 0.7rem; background: var(--gray-50); position: sticky; left: 0; z-index: 1; }
.jbp-legend span:not(:first-child) { line-height: 1.35; }
.jbp-cell:last-child { border-right: 0; }
.jbp-cell--past { background: var(--gray-50); }
.jbp-cell--current { box-shadow: inset 0 2px 0 var(--primary); }
.jbp-m { font-size: 0.72rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.03em; }
.jbp-y { margin-left: 0.2rem; opacity: 0.6; }
.jbp-v { font-size: 0.82rem; text-align: right; font-variant-numeric: tabular-nums; line-height: 1.35; }
.jbp-prev { font-weight: 600; }
.jbp-manual { color: var(--primary); }
.jbp-real { color: var(--text-secondary); }
/* Bande éditable (page affaire, 2026-08-26) : la ligne Prévu est un champ par
   mois — même saisie que le facturier (/previsionnel .prev-input). */
.jbp-strip--editable .jbp-cell { min-width: 104px; }
.jbp-input {
    width: 100%; min-width: 0; text-align: right; padding: 0.2rem 0.35rem; font-size: 0.82rem; font-weight: 600;
    line-height: 1.35; font-variant-numeric: tabular-nums;
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--bg); color: var(--text);
}
.jbp-input:focus { border-color: var(--primary); outline: none; }
.jbp-input.jbp-manual { color: var(--primary); border-color: var(--primary); }
.jbp-cell--past .jbp-input { background: var(--surface); }
/* ── Le TABLEAU d'échéances (2026-09-12) ────────────────────────────────────
   L'échéancier d'une affaire se saisit en LIGNES — un mois, un libellé, un
   montant — juste au-dessus de la bande, qui n'est plus qu'une lecture.
   `table-layout: fixed` + `.jbs-scroll` : un champ `type="month"` réclame ~150 px
   au navigateur, quatre colonnes ne tiennent donc pas sous ~520 px. Le tableau
   garde SON ascenseur horizontal plutôt que de faire déborder la page — une
   barre de défilement sur la page entière pour quatre colonnes serait payée par
   tout le reste de l'onglet. */
.jbs { margin: 0 0 var(--space-4); }
.jbs-scroll { overflow-x: auto; }
.jbs-table { width: 100%; min-width: 520px; table-layout: fixed; border-collapse: collapse; }
.jbs-table th {
    text-align: left; font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em;
    color: var(--text-secondary); padding: 0 0.4rem 0.35rem; border-bottom: 1px solid var(--border);
}
.jbs-table td { padding: 0.2rem 0.4rem; vertical-align: middle; }
/* 168 px : « September 2026 » + l'icône du sélecteur natif, mesurés — à 150 px
   le champ rognait le millésime des mois longs. */
.jbs-th-mois     { width: 168px; }
.jbs-th-montant  { width: 150px; text-align: right; }
.jbs-th-actions  { width: 72px; }
.jbs-table .form-input { margin: 0; padding: 0.3rem 0.45rem; font-size: 0.85rem; }
.jbs-montant-champ, .jbs-montant { text-align: right; font-variant-numeric: tabular-nums; }
.jbs-montant { font-weight: 600; }
/* Un mois PASSÉ porte une échéance qui n'entre plus dans le « reste à
   positionner » : il s'efface d'un ton, il ne s'alarme pas. */
.jbs-row--passe .form-input, .jbs-row--passe td { color: var(--text-secondary); }
.jbs-row--passe .jbs-mois { background: var(--gray-50); }
.jbs-vide td { padding: var(--space-4) 0.4rem; color: var(--text-muted); font-size: 0.85rem; }
/* ⚠ PAS DE `display: flex` SUR UN `<td>`. Il cesse alors d'être une cellule de
   tableau : il ne participe plus à l'alignement de sa rangée, `vertical-align:
   middle` devient inopérant, et les deux boutons remontent en haut de la ligne
   pendant que le champ reste centré — l'écart se voit tout de suite (constaté
   par Olivier, 12-09-2026). La cellule reste donc une CELLULE, et ce sont les
   boutons qui s'alignent, en `inline-flex`. */
.jbs-actions { text-align: right; white-space: nowrap; }
.jbs-actions .btn-icon { display: inline-flex; vertical-align: middle; margin-left: 0.15rem;
                         background: transparent; width: 26px; height: 26px; color: var(--text-secondary); }
.jbs-actions .btn-icon:hover { background: var(--gray-50); color: var(--primary); }
.jbs-actions .btn-icon.text-error:hover { color: var(--error); }
/* Pied : le bouton d'ajout à gauche, les sommes à droite — elles se replient
   sous lui quand la largeur manque plutôt que de se serrer. */
/* Le pied du formulaire, DANS LA MODALE : le bouton d'ajout à gauche, la ligne
   vive à droite — « positionné » et le verdict, recalculés à chaque frappe,
   sous les lignes qu'on tape. La jauge complète, elle, reste sur l'onglet. */
.jbs-pied { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-3); }
.jbs-form-tot { display: flex; align-items: baseline; gap: var(--space-4); font-size: 0.82rem; color: var(--text-secondary); }
.jbs-form-tot b { color: var(--text); font-variant-numeric: tabular-nums; }
.jbs-form-verdict.is-ok     { color: var(--state-green-fg); font-weight: 600; }
.jbs-form-verdict.is-manque { color: var(--state-amber-fg); font-weight: 600; }
.jbs-form-verdict.is-trop   { color: var(--state-red-fg);   font-weight: 600; }
/* Le titre du graphe et le bouton qui ouvre la modale, sur la même ligne. */
.jbs-ca-tete { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-3); }
.jbs-ca-tete .jbs-ca-titre { margin: 0; }
/* ⚠ La barre « échéancier modifié » a été retirée le 12-09-2026 avec le passage
   en modale : un Annuler / Enregistrer explicite en pied de modale rend inutile
   un témoin de brouillon dans le flux de la page. */

/* ── Les trois blocs s'empilent (2026-09-12) ────────────────────────────────
   Jauge, formulaire, graphe. Le graphe et le formulaire ont d'abord vécu en
   DEUX COLONNES (363 + 583 px) ; l'axe à deux étages y a mis fin : 44 semaines
   réclament 880 px de règle, et 363 n'en donnaient pas le quart. Arbitrage
   d'Olivier : le repère de semaines vaut la colonne.
   Le formulaire, lui, est BORNÉ — à 966 px sa colonne « Libellé » ferait 576 px
   de champ libre pour trois mots. */

/* ── La jauge du marché (2026-09-12) ────────────────────────────────────────
   UNE barre pour l'affaire, pas une par mois : « facturé » contre « à
   facturer » est une part d'un TOUT, et ce tout est le marché. Trois parts qui
   bouclent dessus — facturé + positionné + non positionné.
   La forme est celle que prescrit un rapport à une LIMITE : une piste, remplie,
   jamais deux tranches d'un camembert. Et la piste EST la troisième part (le
   non positionné) : pas de troisième teinte, la teinte neutre du socle suffit. */
.jbs-jauge { margin: 0 0 var(--space-4); }
.jbs-jauge-tete { display: flex; align-items: baseline; gap: var(--space-2); margin-bottom: var(--space-2);
                  font-size: 0.72rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.03em; }
.jbs-jauge-tete b { font-size: 0.85rem; color: var(--text); text-transform: none; letter-spacing: 0; font-variant-numeric: tabular-nums; }
/* ⚠ MÊME ÉPAISSEUR ET MÊME ARRONDI QUE `.jv-rep-barre` (14-09-2026, Olivier :
   « harmoniser l'épaisseur de la jauge Facturation prévue et le où il part »).
   Les deux barres sont maintenant DANS la même colonne, à deux blocs d'écart,
   et elles disent la même famille de chose — un tout découpé en parts. À 10 px
   contre 14, la plus fine se lisait comme un objet de rang inférieur, ce qu'elle
   n'est pas. 14 px est la valeur de la carte, mesurée : à 22 px trois aplats
   saturés devenaient l'objet le plus lourd de l'écran.
   ⚠ Le fond reste `--gray-200` et non `--color-surface-2` : ici la piste EST la
   troisième part (le non positionné), elle porte une valeur — là-bas elle n'est
   qu'un fond, la barre couvre toujours 100 %. */
.jbs-jauge-piste { display: flex; height: 14px; border-radius: 4px; overflow: hidden; background: var(--gray-200); }
.jbs-jauge-part { height: 100%; }
/* 2 px de FOND entre deux parts : sans ce filet, deux valeurs d'un même violet
   se touchent et la frontière disparaît là où elle compte le plus. */
.jbs-jauge-part + .jbs-jauge-part { border-left: 2px solid var(--surface); box-sizing: border-box; }
.jbs-jauge-part.is-facture { background: var(--state-violet-fg); }
.jbs-jauge-part.is-positionne { background: var(--state-violet-dot); }
/* ── LE REPÈRE DE L'ENCAISSÉ (14-09-2026) ──────────────────────────────────
   « On pourrait ajouter l'encaissé HT ? » — oui, mais en REPÈRE et pas en part :
   l'encaissé est un SOUS-ENSEMBLE du facturé, pas une part du marché. Les trois
   parts bouclent sur le marché, et c'est la seule garantie de ce dessin.
   ⚠ AU-DESSUS DE LA PISTE, comme les repères de budget : `.jbs-jauge-piste` porte
   `overflow: hidden` et rognerait le trait, et 2 px POSÉS SUR les parts se
   confondraient avec le filet qui les sépare — qui fait exactement 2 px.
   ⚠ PAS DE TEINTE NEUVE : le violet porte déjà deux valeurs (facturé /
   positionné, ΔE00 18,6) et une troisième les mettrait sous le plancher de 15.
   Un trait d'encre n'est pas une teinte de série — il ne concourt pas, il situe. */
.jbs-jauge-reperes { position: relative; height: 7px; margin-bottom: 3px; }
.jbs-jauge-repere  { position: absolute; bottom: 0; width: 2px; height: 7px;
                     margin-left: -1px; background: var(--text); opacity: 0.55; border-radius: 1px; }
.jbs-jauge-cles { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); margin-top: var(--space-2);
                  font-size: 0.8rem; color: var(--text-secondary); }
/* La clé de l'encaissé porte un TRAIT, pas une pastille : la forme de la clé
   répète la forme du dessin — trois carrés qui bouclent, puis un repère. */
.jbs-jauge-encaisse i { display: inline-block; width: 2px; height: 11px; border-radius: 1px;
                        margin-right: 5px; vertical-align: middle;
                        background: var(--text); opacity: 0.55; }
.jbs-jauge-cles b { color: var(--text); font-variant-numeric: tabular-nums; }
/* Le pourcentage est une PRÉCISION du montant, pas un second chiffre : il se
   range d'un cran en arrière, sinon l'œil hésite entre les deux. */
.jbs-jauge-cles em { font-style: normal; color: var(--text-muted); font-variant-numeric: tabular-nums; }
/* ⚠ LA TAILLE DE `.jv-puce` (9 px), pas une quatrième. La carte porte déjà trois
   familles de pastilles — `.jv-puce` (9), `.jv-rep-groupe i` (8) et celle-ci
   (10) : trois accidents pour un seul rôle. Celle-ci s'aligne sur la pastille de
   CLÉ, qui est ce qu'elle est. */
.jbs-jauge-cles i { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 5px; vertical-align: middle; }
.jbs-jauge-cles i.is-facture { background: var(--state-violet-fg); }
.jbs-jauge-cles i.is-positionne { background: var(--state-violet-dot); }
.jbs-jauge-cles i.is-reste { background: var(--gray-200); }
/* Le verdict porte l'encre ACCESSIBLE de son état (`-fg`), jamais la teinte de
   pastille : `--success` posé en texte tombe à 2,5:1. */
.jbs-jauge-verdict.is-ok b, .jbs-jauge-verdict.is-ok { color: var(--state-green-fg); }
.jbs-jauge-verdict.is-manque b, .jbs-jauge-verdict.is-manque { color: var(--state-amber-fg); }
.jbs-jauge-verdict.is-trop b, .jbs-jauge-verdict.is-trop { color: var(--state-red-fg); }
/* La sur-facturation du MARCHÉ (14-09-2026) : même encre que « de trop » — les
   deux disent un dépassement, et deux rouges pour deux dépassements
   n'apprendraient rien de plus. */
.jbs-jauge-verdict.is-surfacture b, .jbs-jauge-verdict.is-surfacture { color: var(--state-red-fg); }
.jbs-jauge-verdict em { color: inherit; opacity: 0.75; }
/* L'alerte des échéances datées d'un mois passé : elle DIT le geste, donc elle
   n'est pas une note grise en fin de ligne — c'est une phrase, sur sa ligne. */
.jbs-jauge-alerte { display: flex; align-items: flex-start; gap: 0.4rem; margin: var(--space-2) 0 0;
                    font-size: 0.78rem; color: var(--state-amber-fg); }
.jbs-jauge-alerte svg { flex: 0 0 auto; margin-top: 1px; }
.jbs-vol { min-width: 0; }
.jbs-vol--ca { margin-top: 0; }

/* ── Le CA mensuel, en barres groupées ──────────────────────────────────────
   Géométrie reprise de `.dash-chart-*` (app-core dashboard.css), la famille que
   le tableau de bord emploie pour « Commandé / Facturé ». Classes PROPRES parce
   qu'elle ne descend pas sous zéro : ses hauteurs partent d'un fond en
   `flex-end`, et un avoir rend le réel d'un mois NÉGATIF (−16 425,90 € sur
   l'affaire 35 en juillet 2026). Ici la zone est coupée en deux — `--jbs-ca-pos`
   au-dessus du zéro, `--jbs-ca-neg` en dessous — et leur somme fait la hauteur. */
.jbs-ca-titre { font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em;
                color: var(--text-secondary); margin: 0 0 var(--space-3); }
.jbs-ca-scroll { overflow-x: auto; }
/* ⚠ UNE SEULE DÉFINITION DE PISTES POUR LES QUATRE RANGÉES (montants, barres,
   mois, semaines) : `--jbs-ca-tracks` est posée sur la piste et héritée par
   chacune. Quatre grilles déclarées séparément se désaligneraient au premier
   arrondi, et un axe décalé d'une demi-colonne est pire qu'un axe absent.
   Chaque MOIS occupe `span N` pistes, N étant son nombre de SEMAINES : un mois
   de cinq semaines est réellement plus large qu'un mois de quatre. */
/* ⚠ NOMMER LES QUATRE RANGÉES, pas `.jbs-ca-piste > *` : la rangée des barres
   est un PETIT-FILS de la piste (`.jbs-ca-zone > .jbs-ca-cols`, la zone servant
   d'ancrage à la ligne de zéro). Le sélecteur d'enfant direct posait la grille
   sur la zone et laissait les colonnes s'empiler en blocs — mesuré à l'écran :
   un graphe de 2 500 px de haut. */
.jbs-ca-mnts, .jbs-ca-cols, .jbs-ca-axe { display: grid; grid-template-columns: var(--jbs-ca-tracks); }
.jbs-ca-zone { position: relative; }
.jbs-ca-col { position: relative; display: flex; flex-direction: column; border-radius: 3px; }
/* Le mois courant est le repère du graphe : une bande, pas une couleur de plus. */
.jbs-ca-col.is-courant { background: var(--gray-100); }
.jbs-ca-mois.is-courant { color: var(--text); font-weight: 600; }
.jbs-ca-haut { height: var(--jbs-ca-pos); display: flex; align-items: flex-end;   justify-content: center; gap: 2px; }
.jbs-ca-bas  { height: var(--jbs-ca-neg); display: flex; align-items: flex-start; justify-content: center; gap: 2px; }
/* Une barre dans un bandeau de mois qui fait maintenant ~96 px : 16 px de large
   y paraissaient un trait. Elle reste MINCE (un tiers du bandeau), pas une
   colonne pleine — c'est la hauteur qui porte la valeur. */
.jbs-ca-bar { width: 30%; max-width: 26px; }
.jbs-ca-bar.is-haut { border-radius: 3px 3px 0 0; }
.jbs-ca-bar.is-bas  { border-radius: 0 0 3px 3px; }
/* css-color-ok: valeur basse du violet « Facturation client » — paire validée ΔE 16,2 protan / 18,9 normal ; aucun token ne la porte */
.jbs-ca-bar.is-prevu { background: var(--state-violet-dot); }
/* css-color-ok: le violet « Facturation client » de la chronologie (jv-chrono-trait.is-vente) — même série, même teinte */
.jbs-ca-bar.is-reel  { background: var(--state-violet-fg); }
/* La cible de survol couvre la colonne ENTIÈRE : viser une barre de 16 px de
   large et 4 px de haut n'est pas un geste qu'on demande à quelqu'un. */
.jbs-ca-cible { position: absolute; inset: 0; cursor: default; }
/* Le zéro n'est peint QUE s'il y a du négatif — un avoir. Collé sous les
   barres, il ne ferait que redoubler leur pied ; là il sépare deux sens, et il
   doit se voir : `--border` à 1 px sur du blanc ne se voit pas. */
.jbs-ca-zero { position: absolute; left: 0; right: 0; height: 1px; background: var(--gray-300); pointer-events: none; }
/* Le montant de chaque colonne, en k€, dans sa propre rangée au-dessus du
   tracé — elle partage la géométrie de `.jbs-ca-cols`, donc chaque cellule
   tombe exactement sur sa colonne. Empilés quand un mois porte les deux séries.
   `overflow: hidden` volontaire : mieux vaut une valeur rognée qu'une valeur
   qui se couche sur celle du mois voisin. */
.jbs-ca-mnts { margin-bottom: 4px; align-items: flex-end; }
.jbs-ca-mnt { display: flex; flex-direction: column; align-items: center; gap: 0;
              font-size: 0.68rem; line-height: 1.25; color: var(--text-secondary);
              font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; }
.jbs-ca-val.is-reel { color: var(--text); font-weight: 600; }
/* ── Les deux étages de l'axe (12-09-2026) ──────────────────────────────
   Le MOIS coiffe ses semaines, la SEMAINE porte son numéro ISO. Les deux
   rangées héritent des pistes du tracé : un mois est un BANDEAU sur ses
   semaines, pas une étiquette centrée au hasard. */
.jbs-ca-axe { margin-top: 3px; }
.jbs-ca-axe-mois { border-top: 1px solid var(--border); padding-top: 3px; }
.jbs-ca-mois { text-align: center; font-size: 0.7rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden;
               border-left: 1px solid var(--border); }
.jbs-ca-mois:first-child { border-left: 0; }
.jbs-ca-mois.is-courant { color: var(--text); font-weight: 600; }
.jbs-ca-an { margin-left: 0.15rem; opacity: 0.6; }
.jbs-ca-sem { text-align: center; font-size: 0.6rem; color: var(--text-muted); font-variant-numeric: tabular-nums; overflow: hidden; }
.jbs-ca-sem.is-courant { color: var(--primary); font-weight: 600; }
.jbs-ca-legende { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); margin-top: var(--space-3);
                  font-size: 0.72rem; color: var(--text-secondary); }
.jbs-ca-legende i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 4px; vertical-align: middle; }
/* css-color-ok: même paire que les barres ci-dessus */
.jbs-ca-legende i.is-prevu { background: var(--state-violet-dot); }
/* css-color-ok: même paire que les barres ci-dessus */
.jbs-ca-legende i.is-reel  { background: var(--state-violet-fg); }
.jbs-ca-rogne { color: var(--text-muted); }
.jbs-ca-vide { margin: 0; padding: var(--space-5) 0; color: var(--text-muted); font-size: 0.8rem; }

/* La barre de pilotage du facturier (.prev-chip*) a migré sur la famille
   canonique .kpi-chip (app-core dashboard.css) le 2026-08-27 — ne pas la
   réintroduire ici. */

/* ── Séries de numérotation (Réglages → Numérotation / Équipe, 2026-08-26) ── */
.numbering-block-title { font-size: .75rem; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: .05em; margin-bottom: var(--space-3); }
.numbering-sep { margin: var(--space-4) 0; }
.numbering-series-table .numbering-col-prefix { width: 160px; }
.numbering-series-table .numbering-col-format { width: 220px; }
.numbering-series-table td.is-muted { opacity: .55; }
.numbering-series-table .seq-prefix { text-transform: uppercase; font-family: monospace; font-weight: 600; }
.numbering-series-table .form-input { margin: 0; }
.numbering-series-code { font-size: .72rem; font-family: monospace; }
.numbering-preview { font-family: monospace; color: var(--primary); margin-right: var(--space-2); }
.numbering-lock-hint { display: flex; align-items: center; gap: .3rem; margin-top: .3rem; font-size: .72rem; white-space: normal; }
.numbering-modal-intro, .numbering-tab-intro { margin: .2rem 0 var(--space-4); }
.equipe-col-series { width: 260px; }
.equipe-series-select { margin: 0; }

/* Bandeau de contexte d'un document (modale facture / acompte / avance) :
   « Émettre depuis », « Série ». Libellé À CÔTÉ du contrôle, aperçu à droite. */
.doc-context-row { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); padding: var(--space-3) var(--space-4); background: var(--color-surface-2); border: 1px solid var(--border); border-radius: var(--radius); }
.doc-context-row .form-label { margin: 0; white-space: nowrap; }
.doc-context-row .form-input { flex: 1; margin: 0; min-width: 0; }
.doc-context-row-hint { margin-left: auto; white-space: nowrap; font-size: .8125rem; color: var(--text-secondary); }
.doc-context-row-hint strong { font-family: monospace; color: var(--text); }
.doc-context-row .badge { margin-left: var(--space-1); }

/* LA BARRE DE L'ONGLET « TEMPS & PLANNING » (12-09-2026) — tout ce qui reste
   de l'ancien onglet « Planning » d'airfact.

   D'OÙ ON PART : 145 lignes de `.jv-plan-*` habillaient une grille jour ×
   ressource, une bande de charge sur seize semaines et une modale d'allocation
   à trois modes — une RÉ-IMPLÉMENTATION complète de la fenêtre de Plan. L'écran
   est désormais le COMPOSANT PARTAGÉ d'app-core, avec sa propre feuille
   (`components/planning-affaire.css`), symlinkée ici : le MÊME fichier que dans
   plan. Il ne reste donc à habiller que le CHÂSSIS — la barre qui porte le
   bouton d'enregistrement, que le composant ne possède pas (c'est ce qui lui
   permet de vivre dans un onglet ici et dans une fenêtre là-bas).

   ⚠ LA BARRE EST REVENUE, MAIS PAS LE BOUTON D'ENREGISTREMENT (14-09-2026).
   Elle était partie le 12-09 avec lui (« tableau purement consultatif pour le
   moment ») ; elle porte désormais UN seul bouton, « Planifier la chaîne »,
   qui ouvre une MODALE — l'écran, lui, reste en lecture. La distinction tient :
   on ne pose pas des journées une par une depuis la fiche commerciale, mais
   poser le CALENDRIER d'une affaire se décide sur sa date de livraison, donc
   ici. */
.jv-plan-barre {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    margin: 0 0 var(--space-3);
}
/* ⚠ LA POUSSÉE VIENT DU TITRE, PAS D'UN `space-between` (15-09-2026). La barre
   portait UN bouton, et `space-between` suffisait à le coller à droite. Avec
   DEUX, il aurait écarté les boutons l'un de l'autre jusqu'aux deux bords —
   deux gestes de même famille, séparés par toute la largeur de l'écran. Le
   titre pousse, les boutons restent groupés, et une troisième entrée un jour ne
   redispersera rien. */
.jv-plan-barre-titre {
    margin-right: auto;
    font-size: 0.72rem;
    font-weight: 700;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.03em;
}
/* L'AVIS DU CHÂSSIS — ce que le composant partagé ne peut pas savoir, parce que
   Plan n'a aucune notion de taux horaire : le PRIX de ces heures est une donnée
   d'Airfact. Il ne paraît que quand un type d'heures pointé n'a jamais reçu de
   coût, et il DIT la conséquence (« comptées en heures, pas en euros ») plutôt
   que de laisser un écran répondre 0,00 €. */
.jv-plan-avis {
    display: flex;
    align-items: baseline;
    gap: 0.4rem;
    flex-wrap: wrap;
    margin: 0 0 var(--space-3);
    padding: 0.45rem 0.6rem;
    border-radius: 6px;
    background: var(--state-amber-bg);
    color: var(--state-amber-fg);
    font-size: 0.8rem;
}
.jv-plan-avis svg { flex: none; align-self: center; }
.jv-plan-avis a { color: inherit; text-decoration: underline; }

/* LA FENÊTRE « NOUVELLE ALLOCATION » (15-09-2026) — deux règles, revenues avec
   le geste qu'elles habillent.

   Elles étaient parmi les 145 lignes retirées le 12-09 ; ce sont les SEULES qui
   décrivaient la modale et non la grille. Le reste de la fenêtre est peint par
   le socle (`form-group`, `form-layout`, `nav-tabs--pill`, `modal-*`) : une
   feuille propre à cette modale serait une seconde définition du formulaire. */
.jv-plan-mode { margin-bottom: var(--space-3); }
/* ⚠ `min-height` ET PAS `display:none` QUAND C'EST VIDE. L'aperçu apparaît dès
   qu'une date est saisie ; sans hauteur réservée, la note et le pied de la
   fenêtre sauteraient d'une ligne à la première frappe — sous le curseur. */
.jv-plan-preview {
    font-size: 0.85rem;
    color: var(--text-secondary);
    min-height: 1.2em;
    margin: var(--space-2) 0;
}


/* ── Bouton de connexion Stripe ──────────────────────────────────────────
   Couleur de MARQUE (blurple Stripe), pas une couleur du système — elle n'a
   pas de token et n'en aura pas. Une seule définition, plus jamais en
   style="" dans une vue. */
/* css-color-ok: couleur de marque Stripe (blurple officiel) */
.btn-brand-stripe { background: #635bff; border-color: #635bff; white-space: nowrap; }
/* css-color-ok: appui de la couleur de marque Stripe, un cran plus sombre */
.btn-brand-stripe:hover { background: #5148e6; border-color: #5148e6; }

/* ── Import « reprise Excel » des bons de commande (/supplier-orders/import) ──
   Ligne non importable de l'aperçu : grisée mais lisible — le motif reste
   dans sa colonne. Même patron que .jbg-row-muted. */
.table tbody tr.so-import-row-muted td { color: var(--text-muted); }
/* Contrôle global « Affecter une affaire à toutes les lignes » dans la barre
   d'outils de l'aperçu : sans borne, le .form-input à 100 % se résout mal
   dans la barre flex. */
.table-toolbar .so-import-bulk-job { width: 380px; max-width: 100%; }
/* Cellules Affaire / Fournisseur de l'aperçu : la disposition AUTO peut
   écraser une colonne qui héberge un champ de recherche — borne basse pour
   que l'input reste utilisable. */
.so-import-pick-cell { min-width: 200px; }

/* ── Écran « Vendu » d'une affaire — LE TABLEAU DES DOCUMENTS ────────────────
   jobs/engagements.js (le rendu) + jobs/commandes.js (les gestes d'écriture)
   + Jobs/Views/partials/_customer-order-modals.php.
   Conception : docs/commandes-client-2026-09-10.md §15. TOKENS UNIQUEMENT :
   cette feuille vit dans components/, un hex n'y entre plus sans raison. */

/* Le tableau des documents ouvre l'écran ; le bordereau des postes le suit. */
.eng-block { margin-bottom: var(--space-6); }

/* ── LA HIÉRARCHIE, RENDUE VISIBLE ───────────────────────────────────────────
   Trois natures qui ne s'additionnent pas pareil cohabitent dans ce tableau :
   un devis VAUT le marché, un avenant s'y AJOUTE, un bon de commande vit À
   CÔTÉ. Le retrait n'est donc pas un ornement : c'est ce qui dit qu'un avenant
   se lit SOUS son devis, et que son montant est un delta, pas un montant. */
/* Le retrait OUVRE une gouttière, et c'est la même règle qui y suspend le
   coude : impossible d'avoir l'un sans l'autre.

   ⚠ CE QUI VIVAIT ICI, ET POURQUOI IL EST PARTI (15-09-2026, « mal présenté
   ici » — Olivier, sur /jobs/121#marche). Le coude était un NŒUD DU FLUX,
   `<span class="doc-tree">└</span>` posé devant l'identité. Or `.cell-main` est
   `display: block` (socle, tables.css) : tout ce qui le précède finit sur la
   ligne d'AVANT. Le coude se peignait donc seul, au-dessus du libellé, la
   rangée passait de deux lignes à trois — 66 → 87 px, mesuré — et le coude ne
   désignait plus rien. La règle s'appliquait ; elle désignait le vide.

   L'ANCRE est `.cell-main`, PAS la cellule. `.table td` est en
   `vertical-align: middle` : le contenu est CENTRÉ, donc aucun décalage depuis
   le haut de la cellule n'est stable — il changerait avec le nombre de lignes.
   Ancré sur le libellé, le bras tombe au milieu de SA ligne, toujours.
   Le montant du STEM est volontairement plus grand que toute cellule
   plausible : la cellule le COUPE en haut (`.table td` est en
   `overflow: hidden`, tables.css §« la cellule tronque par défaut »), et il
   rejoint ainsi le filet de la rangée mère. Borné tout de même, pour qu'un
   jour sans ce rognage il ne traverse pas la table. */
.table tbody tr.doc-row--child > td:first-child {
  padding-left: calc(var(--space-6) + var(--space-3));
}
.table tbody tr.doc-row--child > td:first-child .cell-main { position: relative; }
.table tbody tr.doc-row--child > td:first-child .cell-main::before {
  content: '';
  position: absolute;
  right: 100%; margin-right: var(--space-2);
  bottom: 50%;                       /* le BRAS, au milieu de la ligne du libellé */
  width: var(--space-2);
  height: var(--space-12);           /* le STEM, coupé par la cellule */
  border-left: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  border-bottom-left-radius: 3px;
}

/* Le delta d'un avenant est écrit SIGNÉ : « +6 400 » se lit comme un ajout,
   « 6 400 » se lirait comme un montant à empiler. */
.doc-delta--pos { color: var(--success); font-weight: 600; }
.doc-delta--neg { color: var(--error);   font-weight: 600; }

/* Une PROPOSITION (devis supplémentaire pas encore transformé en avenant)
   n'entre dans aucun total : son montant est mis entre parenthèses et en
   retrait de teinte, pour ne pas se lire comme une part du marché. */
.doc-amount--muted { color: var(--text-secondary); }

/* Le TIRET des colonnes qu'une ligne n'a pas en propre — un avenant est
   facturé DANS le marché, il n'a ni facturé ni reste. Jamais « 0,00 € » : un
   zéro se lirait « rien facturé », alors que la question ne se pose pas. */
.doc-none { color: var(--text-muted); }

/* Renvoi vers un devis (n° cliquable, dans la cellule d'identité). */
.doc-ref { color: var(--primary); text-decoration: none; }
.doc-ref:hover { text-decoration: underline; }

/* Un BC ANNULÉ reste LISIBLE mais grisé, et le serveur l'a déjà exclu des
   totaux. Le seul jeu de jetons : le socle peint, la colonne collante copie.
   ⚠ Le `background-color` retiré ici n'était pas inerte — écrit
   `.table tbody tr.eng-row--cancelled` (0,2,3), il battait
   `.table tbody tr:hover` (0,2,2) : la ligne restait CLOUÉE à --gray-50 au
   survol pendant que sa seule colonne d'actions passait à --gray-100. Le même
   défaut qu'`invoice-row-late` le 2026-09-09, encore vivant ici jusqu'au
   2026-09-13. */
.table tbody tr.eng-row--cancelled {
  --row-bg: var(--gray-50);
  --row-bg-hover: var(--gray-100);
}
.table tbody tr.eng-row--cancelled td,
.table tbody tr.eng-row--cancelled .cell-main { color: var(--text-muted); }

/* Le statut d'un BC se peint désormais avec `.badge-status-{teinte}` du SOCLE
   (badges.css), la teinte venant de config/statuses/customer-order.php.

   La famille locale `.co-status-received/-invoiced/-cancelled` a été RETIRÉE
   le 2026-09-11 (migration 146). Elle n'était pas fausse — elle était CLOSE :
   trois règles pour trois états, écrites quand l'ENUM en avait deux. Les trois
   états du régime ÉMIS (`draft`, `sent`, `signed`) n'y auraient eu aucune
   règle, et une classe sans règle ne lève rien : la pastille sort NUE, sans
   fond, sans couleur, lisible comme un défaut d'affichage. Le socle, lui,
   couvre déjà les quatre teintes employées. */

/* Renvois de la ligne de BON DE COMMANDE : le n° de facture ouvre la facture,
   le trombone ouvre le PDF du bon de commande. */
.co-invoice-link { color: var(--primary); text-decoration: none; }
.co-invoice-link:hover { text-decoration: underline; }
.co-doc-link {
  display: inline-flex; align-items: center; vertical-align: -0.15em;
  margin-left: var(--space-1); color: var(--text-muted);
}
.co-doc-link:hover { color: var(--primary); }

/* Ligne de totaux (window.tableGroupRow) : elle NOMME ses sous-totaux avant le
   total — « Marché … · Commandes … · Total vendu … » —, ce qui lève
   l'ambiguïté « les commandes s'ajoutent-elles au marché ? » en MONTRANT
   l'arithmétique. Elle n'est rendue que s'il y a quelque chose à additionner :
   sans avenant ni bon de commande, la ligne du devis dit déjà tout, et la
   répéter serait le défaut qu'on vient de retirer. */
.eng-totals-row .table-section-right { display: inline-flex; gap: var(--space-4); flex-wrap: wrap; }
.eng-total-part { color: var(--text-secondary); font-size: 0.8rem; }
.eng-total-part b { color: var(--text-primary); font-variant-numeric: tabular-nums; }

/* ── Grille de saisie des lignes de la modale ────────────────────────────────
   8 colonnes dans une modale `lg` : la grille défile HORIZONTALEMENT dans son
   propre cadre plutôt que de pousser la modale. Sans `min-width` sur la table,
   la disposition auto écrase la colonne du poste (qui héberge un champ de
   recherche) jusqu'à la rendre inutilisable. */
.co-line-grid { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); }
/* 560 et non 720 : la colonne « Poste du marché » (200 px de large à elle
   seule) a quitté la grille le 2026-09-11. Laisser l'ancienne largeur
   minimale forcerait un défilement horizontal sur une grille qui tient
   désormais dans la modale. */
.co-line-grid > .table { min-width: 560px; margin-bottom: 0; }
.co-line-grid .table td { vertical-align: top; }
.co-line-grid .form-input { font-size: 0.82rem; }
.co-line-cell-unit  { width: 90px; }
.co-line-cell-num   { width: 92px; }
.co-line-cell-num .form-input { text-align: right; }
.co-line-cell-del   { width: 44px; text-align: center; }

/* Les DEUX verbes sous la grille : « Ajouter une ligne » (vide) et
   « Reprendre un poste du marché… » (pré-remplie). Le second ne s'affiche que
   si l'affaire a des postes — `hidden` posé par jobs/commandes.js. */
.co-line-add { margin-top: var(--space-2); display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }

/* Le sélecteur de poste n'apparaît qu'au clic sur le raccourci. Borné en
   largeur : une liste déroulante pleine largeur redonnerait au marché la
   place qu'on vient de lui reprendre dans la grille. */
.co-from-item-picker { margin-top: var(--space-3); max-width: 420px; }

/* ── « Facture libre » : la modale d'ORIENTATION ─────────────────────────────
 *
 * Deux compartiments comptables qui ne se ressemblent que de loin — dans le
 * marché (plafond, DGD) ou commandé à part (bon de commande). Le choix est un
 * groupe de `radio` du socle (.radio / .radio-mark / .radio-label) : ici on ne
 * fait qu'EMPILER les options et laisser respirer le titre, sa conséquence et
 * le montant servi. Aucune couleur neuve, aucun hex. */
.fic-choices { display: flex; flex-direction: column; gap: var(--space-3); }

/* L'option est une CARTE cliquable : la cible est toute la boîte, pas la seule
 * pastille de 16 px. `align-items: flex-start` remonte la pastille sur la
 * première ligne du titre (le .radio du socle centre, ce qui la poserait au
 * milieu d'un bloc de trois lignes). */
.fic-choice {
    align-items: flex-start;
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
}
.fic-choice:hover { border-color: var(--primary); }
.fic-choice .radio-mark { margin-top: 2px; }

/* Le libellé porte trois lignes : ce qu'on choisit, ce que ça fait, et le
 * nombre. `.radio-label` est un <span> — on le passe en colonne. */
.fic-choice .radio-label { display: flex; flex-direction: column; gap: var(--space-1); }
.fic-title { font-weight: 600; color: var(--text); }
.fic-desc  { font-size: 0.8125rem; color: var(--text-secondary); }

/* Le nombre SERVI (reste à facturer du marché) — ou le motif d'une option
 * désarmée. Même emplacement, même discrétion. */
.fic-meta  { font-size: 0.8125rem; color: var(--text-muted); }

/* Option désarmée (la modale de bon de commande n'est pas dans la page) : elle
 * reste LISIBLE et dit pourquoi — une option qui disparaît se lit comme un bug. */
.fic-choice--off { opacity: 0.6; cursor: not-allowed; }
.fic-choice--off:hover { border-color: var(--border); }

/* ── Bon de commande depuis un PDF : bandeaux et attente ─────────────────────
 *
 * Le bandeau de pré-remplissage (modale de saisie) et le bandeau d'échec
 * (modale d'attente) sont des `alert()` du socle, portés par un conteneur qui
 * les MASQUE tant qu'ils n'ont rien à dire. `.alert` porte un `margin-bottom`
 * fait pour un empilement de page ; dans un formulaire, l'écart vient déjà du
 * `gap` du conteneur — le cumuler ouvrirait un trou sous le bandeau seul. */
.co-notice .alert { margin-bottom: 0; }

/* Le nom du fichier en cours de lecture. Un nom de PDF est souvent long et sans
 * espace : sans `overflow-wrap`, il élargirait la modale au lieu de revenir à
 * la ligne. */
.co-import-file,
.docimp-file {
    margin: 0;
    font-weight: 600;
    color: var(--text);
    overflow-wrap: anywhere;
}
.co-import-file:empty,
.docimp-file:empty { display: none; }

/* ── « Un devis ou un avenant (PDF) » : l'ANNONCE ────────────────────────────
 *
 * Le bandeau d'annonce et celui du refus sont les mêmes `alert()` du socle,
 * dans le même conteneur masquable — d'où le `.docimp-notice` ajouté à la
 * règle ci-dessus plutôt qu'une seconde copie.
 *
 * Deux phrases dans un seul bandeau, et l'ordre de lecture EST la décision :
 * l'ÉTAT constaté d'abord (« Cette affaire n'a pas encore de marché. »), sa
 * CONSÉQUENCE ensuite (« Le PDF importé deviendra son DEVIS INITIAL. »). C'est
 * la conséquence que l'utilisateur doit accepter avant de choisir un fichier :
 * elle est donc sur sa propre ligne, et en gras.
 *
 * Deux `<span>` plutôt qu'un `<br>` : le module les remplit en `textContent`,
 * et un saut de ligne ne se pose pas dans du texte. `:empty` évite l'interligne
 * fantôme tant que rien n'a été annoncé. */
.docimp-state,
.docimp-effect { display: block; }
.docimp-effect { font-weight: 600; margin-top: var(--space-1); }
.docimp-state:empty,
.docimp-effect:empty { display: none; }

/* Le mode d'emploi de la branche choisie, sous les zones d'état : vide tant que
 * rien n'est annoncé, il ne doit pas ouvrir un blanc dans la modale. */
#docImportHelp { margin: 0; }
#docImportHelp:empty { display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   IDENTITÉ D'ENTREPRISE — la liste de candidats (lot E, 2026-09-11)
   ═══════════════════════════════════════════════════════════════════════════

   Une rangée = un candidat proposé par l'annuaire, ou la réponse « Aucun de
   ces candidats ». Les deux partagent EXACTEMENT la même règle : même rangée,
   même gouttière, même trait. Répondre « aucun » doit être aussi facile que
   retenir un candidat — c'est la réponse qui protège la facture, puisque
   attacher un mauvais SIREN à un client est pire que pas de SIREN.

   Ces règles vivaient en `style=` dans `clients.js` : app-core interdit les
   styles de mise en page en ligne (« no inline grid/flex styles, ever »), et
   `audit-ui-helpers` ne les voit pas — ce qui n'en fait pas une permission.
   Aucun hex : tokens seulement. */
.identity-candidate {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .5rem 0;
    border-top: 1px solid var(--border);
}

/* `min-width: 0` est ce qui autorise l'ellipse de `.cell-truncate` dans un
   enfant de flex : sans lui, le texte pousse la rangée et le bouton sort. */
.identity-candidate-body {
    flex: 1;
    min-width: 0;
}

/* ══ LA LISTE DES POINTAGES A ENTIÈREMENT DISPARU (12-09-2026) ═════════════
   Trois classes vivaient ici — `.jv-temps-action` (le bouton « Saisir des
   heures »), `.jv-temps-detail-tete` et son bouton « Fermer » (le détail d'un
   pointeur, ouvert depuis une rangée du planning). Elles sont retirées avec
   `#timeEntriesContent` lui-même, en trois demandes du même jour : « s'il y a
   pas d'heures on affiche rien », puis « on n'est pas censés pouvoir saisir des
   heures ici », puis « pas besoin de pouvoir voir les saisies, on supprime le
   détail jour ».

   Les heures d'une affaire se lisent désormais DANS l'écran de planning, par
   fonction : le tableau « Salarié » en haut, la chronologie en dessous
   (`components/planning-affaire.css`, app-core). Une classe que plus rien ne
   pose invite à croire que le bloc existe encore. */


/* ══ ONGLET « ÉCHANGES » — LA LISTE DU SOCLE (15-09-2026) ══════════════════
   « Pourquoi /jobs/121#tickets et /jobs/121#echanges n'ont pas la même
   présentation ? » (Olivier). Les cartes de 760 px, leur barre, leurs trois
   états maison et leurs tailles propres ont été RETIRÉS : l'écran passe par la
   coquille du socle (`window.tableList`), la cellule d'identité du socle
   (`window.listRowTitle` / `components/list-row.css`) et les trois états du
   socle (`emptyState`, `loadingState`) — exactement comme l'onglet Tickets
   d'à côté, qui est le composant partagé `window.Tickets`.

   Il ne reste donc ici que ce qui est PROPRE à un échange : l'icône de canal,
   la rangée dépliée, et l'encre rouge de l'état injoignable. Tout le reste,
   c'est-à-dire tout ce qui décidait de l'apparence, n'est plus écrit deux
   fois. */

/* L'icône du canal, devant la pastille qui le nomme. Discrète : le mot porte
   l'information, le glyphe ne fait que l'ancrer. */
.jech-canal {
    display: inline-flex;
    vertical-align: middle;
    margin-right: .25rem;
    color: var(--text-muted);
}

/* ── La conversation DÉPLIÉE ───────────────────────────────────────────────
   Une seconde rangée, sous celle qu'on a ouverte, qui s'étend sur toutes les
   colonnes. Le rendu des bulles vient du SOCLE
   (`components/conversation-fil.css`, symlinké) : ce qui est ici n'habille que
   la charnière — le retrait par rapport à la rangée, et le pied d'actes. */
.jech-fil-row > td {
    background: var(--bg);
    padding: 0;
}

.jech-fil {
    padding: .75rem 1rem 1rem;
}

/* « Répondre » ouvre le fil DANS LA BOÎTE PARTAGÉE, jamais un composeur local :
   l'envoi vit là où sont la signature, le fil RFC et les identifiants du canal.
   Au PIED de la conversation, parce qu'on répond à ce qu'on vient de lire —
   et à droite, la position d'un acte terminal. */
.jech-fil-actes {
    display: flex;
    justify-content: flex-end;
    margin-top: .75rem;
}

/* ── LA BARRE DES PIÈCES JOINTES, en tête du fil déplié (15-09-2026) ────────
   Le compte, le dossier de rangement, « Tout ajouter ». Les puces elles-mêmes
   sont celles du socle (`.cfil-piece*`, `components/conversation-fil.css`) :
   ici on n'habille que ce qui est propre à cet écran — la rangée qui les
   résume. Elle passe à la ligne quand la place manque. */
.jech-pieces-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: .5rem .75rem;
    margin-bottom: .75rem;
    padding: .5rem .7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    font-size: 12px;
}
.jech-pieces-texte {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    color: var(--text-secondary);
}
.jech-pieces-texte strong { color: var(--text); font-weight: 600; }
.jech-pieces-texte svg { flex: none; color: var(--text-muted); }
.jech-pieces-actes {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: .5rem;
}
.jech-pieces-dossier {
    display: inline-flex;
    align-items: center;
    gap: .35rem;
    color: var(--text-secondary);
}
.jech-pieces-dossier .form-input { width: auto; }

/* L'état INJOIGNABLE emprunte le bloc `.empty-state` du socle et n'y change
   QUE l'encre de l'icône. Une variante `is-erreur` de `.empty-state` n'existe
   pas dans le socle, et l'inventer ici la ferait diverger de son jumeau PHP —
   c'est pourquoi la teinte est portée par l'enveloppe, pas par le bloc. */
.jech-injoignable .empty-state-icon {
    color: var(--state-red-fg);
    opacity: 1;
}


/* ─────────────────────────────────────────────────────────────────────────
   LA CARTE À ZONES, VARIANTE À DEUX FENTES (2026-09-14, Olivier : « grande
   carte à zones partout »)
   ─────────────────────────────────────────────────────────────────────────
   `/fournisseurs#afaire` rendait CINQ cartes dans une `.dash-grid` pendant que
   `/dashboard` et `/jobs/N#renta` rendaient UNE carte à zones. Trois écrans,
   deux dialectes — et les deux qui se ressemblent sont ceux qui partagent déjà
   leur module (`job-vnext/shared.js`).

   ⚠ **CE N'EST PAS UNE SECONDE GRILLE, C'EST UNE VARIANTE DE CELLE-CI.** Tout
   ce qui fait la carte est réemployé tel quel : `.jv-zone` (sa boîte, son
   `min-width: 0`, son `container-type`), le filet des zones EMPILÉES, le rythme
   interne de `.jv-groupe` et son premier sans trait, le seuil de 820 px et
   l'échelle typographique de `.jv-essentiel`. Seule change la RÉPARTITION.

   ⚠ **ET LES NOMS DE ZONE NE SONT PAS EMPRUNTÉS.** `--vitaux / --etat /
   --achats` nomment les rôles de la carte d'une AFFAIRE ; une file d'attente
   n'a ni vitaux ni état. Les recycler aurait donné trois noms qui mentent, et
   un nom qui ment coûte plus cher qu'une déclaration en double. La variante
   nomme donc SES rôles : ce qu'on FAIT, ce qu'on DÉCIDE.

   ⚠ **UNE FENTE ÉTROITE À GAUCHE, LA LARGE À DROITE** — comme la carte
   d'affaire, et c'est la 3e passe (14-09, Olivier : « j'ai dit que je voulais
   une colonne étroite à gauche et une colonne plus large à droite »). Ma 2e
   passe avait mis deux fentes ÉGALES en argumentant que 272 px couperait les
   verbes. L'argument était juste, la conclusion non : ce n'est pas la largeur
   qu'il fallait abandonner, c'est le `nowrap` des SOUS-LIGNES.

   ⚠ **272 px RESTE TROP ÉTROIT, ET C'EST MESURÉ** — pas supposé. Au navigateur,
   sur les vraies données :

     | fente  | verbes tronqués | autres débordements |
     |--------|-----------------|---------------------|
     | 272 px | **7**           | 2                   |
     | 300 px | 5               | 2                   |
     | 320 px | 2               | 2                   |
     | 340 px | 2               | **0**               |
     | 360 px | **0**           | **0**               |

   360 px est donc le PLANCHER : le premier cran où aucun geste n'est coupé.
   En dessous, « Assigner le fournisseur » et « Commande restée en brouillon »
   se plient en deux.

   ⚠ **MAIS LE VERBE N'EST PAS LE SEUL À MESURER**, et c'est le second axe :
   la colonne des comptes porte « dont 10 depuis plus de 7 j ». Croisé avec la
   largeur de la fente :

     | fente / colonne | verbes tronqués | sous-lignes pliées |
     |-----------------|-----------------|--------------------|
     | 360 / 104       | 0               | 7                  |
     | 360 / 150       | 0               | 6                  |
     | **400 / 160**   | **0**           | **3**              |
     | 440 / 170       | 0               | 1                  |

   **400 / 160** est le choix : les trois derniers pliages ne coûtent RIEN.
   Mesurée à chaque combinaison, la hauteur de la carte ne bouge pas d'un pixel
   (1 381 px) — c'est la zone de DROITE qui la fixe (écarts + achat + chat), et
   la gauche a de la hauteur à revendre. Élargir la fente pour gagner trois
   sous-lignes d'une ligne aurait pris 40 px de large à la zone qui, elle, en
   manque.

   ⚠ **ET SANS LE `white-space` CI-DESSOUS, AUCUNE LARGEUR N'AURAIT MARCHÉ.** La
   table refusait de descendre sous **525 px** de min-content : 36 cellules
   portent `nowrap` (la convention du socle pour les listes, `tables.css`). Une
   table qui ne rétrécit pas ne se contente pas d'être à l'étroit — elle
   DÉBORDE, et les comptes de la fente gauche se peignaient PAR-DESSUS le texte
   des écarts. Vu à l'écran, jamais dans une règle.

   ⚠ **LA SPÉCIFICITÉ EST DOUBLÉE À DESSEIN.** `.jv-synthese` pose ses colonnes
   et ses aires dans le même `@container` ; à égalité (0,1,0) c'est l'ordre du
   fichier qui trancherait, et un déplacement de bloc suffirait à rendre la
   variante inerte — sans erreur. `.jv-synthese.jv-synthese--paire` (0,2,0)
   gagne quel que soit l'ordre. Même leçon que le filet de `--etat`, déjà payée
   le 13-09. */
@container (min-width: 820px) {
  .jv-synthese.jv-synthese--paire {
    grid-template-columns: 400px minmax(0, 1fr);
    /* ⚠ ON ANNULE AUSSI LES AIRES ET LES RANGÉES. Sans `grid-template-areas:
       none`, les `grid-area` nommées de la carte d'affaire resteraient
       déclarées : une zone à venir qui porterait l'un de ces noms serait placée
       par un plan qui n'est plus le nôtre. Et `grid-template-rows: auto 1fr`
       fabriquerait une seconde rangée VIDE sous deux zones qui n'en occupent
       qu'une — un trou de plusieurs centaines de pixels au bas de la carte. */
    grid-template-areas: none;
    grid-template-rows: none;
  }
  /* Le filet passe du HAUT (zones empilées) à la GAUCHE (zones côte à côte).
     ⚠ Porté par la zone de DROITE, jamais un `border-right` sur celle d'en
     face : la fente de gauche peut se réduire à son contenu, et un trait posé
     du mauvais côté resterait orphelin. */
  .jv-synthese.jv-synthese--paire > .jv-zone + .jv-zone {
    margin-top: 0; padding-top: 0; border-top: 0;
    border-left: 1px solid var(--border);
    padding-left: var(--space-5);
  }
}

/* ⚠ UNE TABLE DU SOCLE DANS UNE ZONE N'A PLUS DE CARTE AUTOUR D'ELLE.
   `dash_table()` était rendu dans un `.dash-card`, qui portait son propre
   rembourrage ; posé nu dans une `.jv-zone`, il collerait au filet. Le pas
   vient de la zone, comme pour tous les autres groupes. */
.jv-zone > .jv-groupe > .dash-table { margin-bottom: 0; }

/* La rangée d'une file garde son curseur et son survol — c'est le même contrat
   que dans les panneaux qu'elle remplace. */
.jv-zone .af-file { cursor: pointer; }

/* ⚠ L'ATTENTE D'UNE CARTE N'A PLUS DE RÈGLE ICI : `.page-wait` (socle,
   `states.css`) la porte, parce que les listes en ont besoin du même jour et
   pour la même raison. Deux définitions du même bloc auraient divergé. */



/* ─────────────────────────────────────────────────────────────────────────
   CE QUI PERMET À LA FENTE ÉTROITE D'EXISTER (2026-09-14, 3e passe)
   ─────────────────────────────────────────────────────────────────────────
   ⚠ **LE VERBE NE PLIE JAMAIS, LA SOUS-LIGNE A LE DROIT.** C'est la seule
   répartition qui tienne : le verbe DISTINGUE les rangées les unes des autres
   (« Assigner le fournisseur » vs « Ventiler les lignes ») — plié en deux, il
   cesse de se balayer d'un coup d'œil ; la sous-ligne, elle, QUALIFIE (« dont
   43 depuis plus de 7 j ») et se lit très bien sur deux lignes.

   ⚠ **`table-layout: fixed` EST OBLIGATOIRE, PAS UN CONFORT.** En disposition
   auto, la largeur déclarée n'est qu'une suggestion : le navigateur élargit la
   table jusqu'à son contenu, et une table qui ne rétrécit pas DÉBORDE de sa
   fente au lieu d'y tenir. C'est mot pour mot la règle du socle sur les tables
   denses — elle ne s'appliquait pas ici parce que rien n'avait encore essayé de
   borner ces tables.

   ⚠ **CE N'EST PAS UNE SURCHARGE DU SOCLE**, c'est une règle de ZONE : elle
   porte `.jv-zone--rail`, donc elle ne touche aucune autre liste de la
   plateforme. Les `nowrap` de `tables.css` restent la règle partout ailleurs. */
.jv-zone--rail .dash-table--todo table { table-layout: fixed; width: 100%; }
/* ⚠ ET LES LARGEURS DE COLONNE SONT OBLIGATOIRES AVEC `fixed`. Sans elles, la
   table partage sa largeur À PARTS ÉGALES : 180 px pour le geste, 180 pour un
   nombre à deux chiffres — et « Assigner le fournisseur » repassait à l'ellipse
   alors que la fente, elle, était assez large. C'est la règle du socle sur les
   tables denses, et elle vaut ici mot pour mot : en `fixed`, TOUTE colonne
   porte sa largeur, sinon elle ne reçoit que le reste. */
.jv-zone--rail .dash-table--todo th:last-child,
.jv-zone--rail .dash-table--todo td:last-child { width: 160px; }
.jv-zone--rail .dash-table--todo td { white-space: normal; }
.jv-zone--rail .dash-table--todo .cell-sub { white-space: normal; }
/* Le verbe : une ligne, et l'ellipse plutôt que le pliage si un motif futur est
   plus long que tous ceux d'aujourd'hui. ⚠ Avec son `title` — une ellipse sans
   infobulle est une information PERDUE, pas raccourcie. */
.jv-zone--rail .dash-table--todo .cell-main {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── L'ACCUEIL EN TÊTE DE LA CARTE (2026-09-14, décision Olivier) ───────────
   `/dashboard` n'a plus d'en-tête de page : ce qui s'y trouvait n'était pas de
   la navigation — la barre latérale dit déjà où l'on est — mais du CONTENU. Le
   titre et le salut descendent donc ici, et la carte redevient ce qui NOMME
   l'écran.

   Le titre reprend le gabarit de `.app-topbar-h1` qu'il remplace ; le salut est
   une sous-ligne atténuée, comme l'était le sous-titre. Un filet les sépare du
   corps de la carte — c'est la seule chose que l'en-tête faisait et que la
   carte ne faisait pas. */
/* ⚠ CE CADRE PORTE AUSSI LES PUCES DE FILTRE depuis le 2026-09-14 (Olivier :
   « mettre les filtres dans le cadre du dessus »). D'où le flex : les mots à
   gauche, les puces poussées à droite par le `margin-left: auto` de
   `.jv-chips`. Il ENROULE — sous ~640 px de carte, les puces passent sous le
   salut plutôt que d'écraser le titre — et `row-gap` paie alors la gouttière
   que `justify-content` ne paie pas. */
.jv-accueil {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  row-gap: var(--space-2);
  padding: 0 0 var(--space-3);
  margin-bottom: var(--space-3);
  border-bottom: 1px solid var(--border);
}
/* Le titre et le salut sont UN bloc : sans cet emballage, le `<h1>` et le
   `<p>` deviennent deux articles de flex côte à côte et le salut se retrouve
   à droite du titre. `min-width: 0` parce qu'un enfant de flex refuse par
   défaut de descendre sous la largeur de son contenu. */
.jv-accueil-mots { min-width: 0; }
.jv-accueil-titre {
  margin: 0;
  font-size: 1.375rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--text);
}
.jv-accueil-salut {
  margin: 4px 0 0;
  font-size: 0.875rem;
  color: var(--text-secondary);
}
