/* ═══════════════════════════════════════════════════════════════════════════
   RESPONSIVE.CSS — la couche responsive commune (Advimotion Finance)

   Cible produit : toute l'application utilisable à partir de 390 px, sans scroll
   horizontal GLOBAL. Un tableau financier a le droit de défiler dans son cadre —
   c'est souvent la meilleure lecture, ce n'est pas un défaut.

   ── Pourquoi ce fichier existe ────────────────────────────────────────────────
   L'application n'a pas de couche de composants : `.toolbar` est déclarée 10 fois,
   `.kpi` 9 fois avec 5 `min-width` différents, 1 667 lignes de <style> vivent dans
   les pages. Une correction responsive écrite dans une page devrait être réécrite
   dix fois, et serait oubliée à la onzième. Ce fichier est l'endroit unique où une
   règle responsive s'écrit.

   ── Comment il est chargé ─────────────────────────────────────────────────────
   Par `@import` en tête de `master.css`, donc dans le <head>, donc **bloquant le
   rendu** : la mise en page mobile est en place au PREMIER rendu.

   L'injection par `master.js` a été essayée et MESURÉE : premier rendu à 64 ms
   avec la mise en page de bureau (`.kpi` à 210 px), puis reflow à 12 px. C'est un
   flash, et il est visible. L'`@import` ne coûte rien de mesurable en regard
   (FCP médian 36 ms contre 40 ms pour un <link> direct — dans le bruit).

   ── La convention `:root` ─────────────────────────────────────────────────────
   Chargé depuis le <head>, ce fichier passe AVANT les <style> des pages. À
   spécificité égale, la page gagnerait. **Toute règle porte donc le préfixe
   `:root`** — un seul cran de spécificité (0,2,0 au lieu de 0,1,0), uniforme,
   documenté et testé (tests/test_responsive.py).

   C'est le contraire d'un `!important` : la règle reste dépassable par une page
   qui aurait une vraie raison de le faire (sélecteur plus spécifique), au lieu
   d'être imposée. Un `!important` gagne contre tout, y compris contre le
   correctif suivant.

   ── Les trois points de rupture, écrits ici et nulle part ailleurs ────────────
     mobile    ≤  767 px   tiroir de navigation, une colonne, cibles à 40 px
     tablette  768–1023 px rail d'icônes conservé, densité réduite
     bureau    ≥ 1024 px   INCHANGÉ — l'audit du 06/09 n'y relève aucun défaut

   Un seul point de rupture mobile : l'audit montre des défauts identiques à 390 et
   à 430 px, sur les mêmes éléments. Deux valeurs seraient deux endroits à tenir.

   Vague 2 (07/09) : les dix pages hors coquille (gabarits templates/*.html et pages
   publiques) lient cette feuille directement, après leur <style> — voir §2.8 et §1.10.
   ═══════════════════════════════════════════════════════════════════════════ */



/* ── L'ORDRE DES SECTIONS EST UNE RÈGLE, PAS UNE PRÉSENTATION ────────────────
   Les plages se CHEVAUCHENT : à 390 px, `≤ 1023` et `≤ 767` s'appliquent toutes
   les deux. À spécificité égale, c'est la DERNIÈRE écrite qui gagne. La plage la
   plus étroite doit donc venir en dernier, sinon la règle générale annule le cas
   particulier.

   Ce fichier s'est fait prendre deux fois par le banc : `.tb-menu-btn` restait
   `display: none` à 390 px, et `select { min-width: 44px }` était annulé par le
   `min-width: 0` de la plage large. D'où l'ordre imposé — du plus large au plus
   étroit — et le test qui le vérifie (tests/test_responsive.py).
   ─────────────────────────────────────────────────────────────────────────── */
/* ═══════════════════════════════════════════════════════════════════════════
   0. BASE — toutes largeurs
   Écrit AVANT les media queries : une règle générale placée après elles les
   annulerait à spécificité égale. C'est l'erreur que le banc a attrapée au
   premier passage — le bouton menu restait `display: none` à 390 px.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Une modale plus haute que l'écran doit défiler, à 390 comme à 1440 px. */
:root .modal { max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); overflow-y: auto; }

/* Le bouton menu n'existe qu'en mobile (§1.3) ; ailleurs le rail est visible. */
:root .tb-menu-btn { display: none; }

/* ── Un tableau plus large que son cadre défile DANS son cadre — à toute largeur ──
   Cette règle vivait dans la plage « ≤ 1023 px », sur la foi de l'audit du 06/09 :
   « à 1024 px et au-dessus, aucun défaut ». Cette phrase était vraie de ce qu'on
   MESURAIT, pas du produit : le banc n'ouvrait que l'onglet par défaut de chaque
   écran. Dès qu'il a su cliquer les onglets (08/09), `/config` onglet « Facturation »
   a rendu un `table.cfg-tbl` de 1622 px dans une carte de 1152 px — 459 px de
   défilement horizontal GLOBAL, à 1440 px, bloquant.

   Elle remonte donc ici, sans borne de largeur, parce que c'est une règle de
   PRODUIT et non de gabarit : « un tableau financier a le droit de défiler dans son
   cadre » (en-tête de ce fichier) ne dit rien de la taille de l'écran.

   On rend le PARENT défilant plutôt que d'insérer un <div> : insérer un nœud
   casserait le JS des pages qui remontent le parentElement. `:has()` est
   nécessaire — un parent ne peut pas savoir autrement qu'il porte un tableau.
   Exclusion de `.filt-panel` (Pipeline) et `.scombo-pop` : ces popovers sont
   positionnés en absolu DANS le tableau, et un conteneur défilant les rognerait.
   Un correctif qui casse un écran sain n'en est pas un. */
:root .card:has(> table):not(:has(.filt-panel)):not(:has(.scombo-pop)),
:root .cfg-card:has(> table) {
  overflow-x: auto;
}

/* Le cadre défilant CANONIQUE — même règle, même raison, donc même portée.
   Il vivait dans la plage « ≤ 1023 px » et il y est resté quand la règle `:has()`
   ci-dessus en est sortie le 08/09. C'est le MÊME défaut de forme, une seconde
   fois : une borne de largeur héritée d'une mesure et non d'un raisonnement.

   Il est pire ici qu'ailleurs, parce que ce fichier désigne `.tbl-cadre` comme
   « ce qu'on écrit désormais » : la classe recommandée était la seule à ne pas
   défiler au bureau. Les sept variantes historiques (`.bwrap`, `.treso-wrap`,
   `.hs-table-wrap`, `.cfg-scroll`) déclarent, elles, `overflow-x: auto` sans
   aucune borne dans leur propre page — vérifié.

   Coût mesuré du déplacement : NUL aujourd'hui. `.tbl-cadre` n'est portée par
   aucun élément du dépôt (`grep`, 08/09) — c'est un piège posé pour le premier
   qui l'utilisera, pas une régression en cours. */
:root .tbl-cadre {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

/* Une image ne déborde jamais de son conteneur. */
:root img, :root svg:not(.ic) { max-width: 100%; }
/* …sauf l'icône d'un bouton flex : `max-width: 100 %` d'un enfant flex se résout
   contre un conteneur qui se dimensionne sur lui — l'icône rétrécit dès que la
   colonne est serrée (vu à 1024 px sur /entities, vague 2 : la flèche d'« Ouvrir »
   perdait 4 px, seule différence AVANT/APRÈS du bureau). */
:root .enter > svg { max-width: none; flex: 0 0 auto; }


/* ═══════════════════════════════════════════════════════════════════════════
   1. MOBILE ET TABLETTE — ≤ 1023 px
   Ce qui vaut aussi sur 768–1023 px : les tableaux, les grilles, les barres
   d'outils. À 1024 px et au-dessus, l'audit ne relève AUCUN défaut : rien ici
   ne doit y descendre.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 1023px) {

  /* ── 2.1 Tableaux : dans leur cadre, jamais dans la page ──────────────────
     111 <table> pour 21 conteneurs défilants : la règle existe, elle n'est
     appliquée qu'une fois sur cinq. Huit tableaux (jusqu'à 858 px, 278 lignes)
     sont posés dans un `div.card` en `display:block` et débordent la page.

     On rend le PARENT défilant plutôt que d'insérer un <div> autour du tableau :
     insérer un nœud casserait le JS des pages qui remontent le parentElement.

     `:has()` est nécessaire — un parent ne peut pas savoir autrement qu'il porte
     un tableau. Exclusion de `.filt-panel` (Pipeline) : ce popover est positionné
     en absolu DANS le tableau, et un conteneur défilant le rognerait. Un
     correctif qui casse un écran sain n'en est pas un. */
  /* Le cadre défilant canonique `.tbl-cadre` VIVAIT ICI. Il est remonté en §0 :
     voir « Un tableau plus large que son cadre défile DANS son cadre ». */

  /* ── 2.2 Onglets MRR : ils défilent, ils ne débordent plus ────────────────
     `.main-tabs` alignait 6 onglets de 91 à 134 px, soit ~600 px sur 390 : c'est
     l'essentiel des +544 px mesurés sur `/`. La règle est dans `style.css`, pas
     dans `app.html` — corrigée ici, elle ne touche pas au chantier moteurs. */
  :root .main-tabs {
    overflow-x: auto; overscroll-behavior-x: contain;
    scrollbar-width: none; padding: 0 12px;
  }
  :root .main-tabs::-webkit-scrollbar { display: none; }
  :root .main-tab { flex: 0 0 auto; }
  :root .sub-menu { scrollbar-width: none; overscroll-behavior-x: contain; }
  :root .sub-menu::-webkit-scrollbar { display: none; }

  /* ── 2.3 Barres d'outils : elles se replient ──────────────────────────────
     `flex-wrap` sur le conteneur, `min-width: 0` sur les enfants — sans le
     second, un enfant flex garde la largeur de son contenu et déborde quand
     même. Le `flex-wrap` sur les enfants est inerte s'ils ne sont pas eux-mêmes
     des conteneurs flex ; il rattrape ceux qui le sont (#filterChips). */
  :root .toolbar, :root .bhead, :root .filters, :root .inline-form,
  :root .year-tabs, :root .vtabs, :root .row, :root .kpis, :root .dkpis {
    flex-wrap: wrap;
  }
  :root .toolbar > *, :root .bhead > *, :root .inline-form > * { min-width: 0; }
  /* Un <label style="display:flex"> qui enveloppe un <select> (admin.html:475,
     « Assistant IA ») : le select est alors un enfant flex, et son `min-width: auto`
     lui rend ses 416 px de libellé le plus long. Vu au banc de la vague 2 (+102 px
     sur /admin@390), invisible tant que la page ne chargeait pas la couche. */
  :root .inline-form > label:has(> select) { flex: 1 1 auto; max-width: 100%; min-width: 0; }
  :root .inline-form > label > select        { flex: 1 1 0; min-width: 0; }
  /* Et le formulaire lui-même est `inline-flex` : une boîte en ligne se dimensionne
     sur son contenu, et un select de 416 px le maintient à 469 px quelle que soit la
     carte. Dans une carte (pas dans une cellule de tableau), il devient un flex de
     bloc : il prend la largeur de la carte, et ses enfants se replient dedans. */
  :root .card > form.inline-form { display: flex; }
  :root .toolbar > * { flex-wrap: wrap; }

  /* ── 2.4 Champs : aucun ne sort du cadre ──────────────────────────────────
     Corrige à lui seul les 5 `champ-hors-ecran` mesurés sur /admin et /config. */
  :root input:not([type="checkbox"]):not([type="radio"]),
  :root select, :root textarea {
    max-width: 100%; min-width: 0;
  }

  /* ── 2.5 Grilles : `min(100%, X)` est LA correction ───────────────────────
     `repeat(auto-fit, minmax(420px, 1fr))` ne descend jamais sous 420 px :
     `auto-fit` replie le NOMBRE de colonnes, jamais leur minimum. Dans un
     conteneur de 278 px la colonne reste à 420 px — d'où les +118 px du
     dashboard. `min(100%, 420px)` dit « 420, sauf si la place manque ». */
  :root .charts2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); }
  /* L'UNIQUE `!important` de ce fichier, et la raison est nommée : `app.html`
     pose ces grilles en STYLE D'ATTRIBUT — `style="grid-template-columns:
     repeat(5,1fr)"` sur #projKpis, #encaissKpis, #nonrecKpis, #retentionKpis,
     #sellsyLinesKpis — et #kpiRow en sélecteur d'ID (app.html:156, 6 colonnes,
     mesurées à 47 px de large sur 390 px). Un style d'attribut ne se contredit
     qu'avec `!important` : aucun sélecteur, si spécifique soit-il, ne le bat.
     Il est ici parce que `app.html` est gelé par le chantier moteurs ; il part en
     phase B, quand ces cinq attributs pourront être retirés à la source. */
  :root .kpi-row {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)) !important;
  }
  :root .grid    { grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
  :root .tiles   { grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr)); }
  :root .pipe-kpis { grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); }
  /* `repeat(4, 1fr)` en dur (style.css:345) : quatre colonnes de 78 px sur 390.
     Même défaut, même correction que ci-dessus — `#hsKpis` n'est qu'un id de
     BALISE, aucun sélecteur d'id ne le style : `:root .hs-kpis` (0,2,0) passe
     devant `.hs-kpis` (0,1,0) sans rien forcer. */
  :root .hs-kpis { grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr)); }

  /* `/sync`, cartes de source : `1fr auto` (sync.html:45) laisse 150 px à la
     première colonne sur 390. Une colonne sous 768, comme `.grid-2` en §2.8 —
     la deuxième porte des commandes, pas de la donnée. */
  :root .src-card { grid-template-columns: 1fr; }

  /* ── 2.6 Cartes KPI : un seul contrat, au lieu de neuf ────────────────────
     `.kpis` est tantôt un flex, tantôt une grille selon la page. Les deux jeux de
     propriétés cohabitent sans dommage : celles qui ne s'appliquent pas au mode
     de disposition du conteneur sont simplement ignorées. */
  :root .kpis, :root .dkpis {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--kpi-base, 160px)), 1fr));
  }
  :root .kpi, :root .dcard, :root .fkpi {
    min-width: 0;
    flex: 1 1 var(--kpi-base, 160px);
  }
  :root .kpis .kpi { min-width: 0; }   /* facturation.html:14, seul sélecteur (0,2,0) */

  /* ── 2.7 Graphiques ───────────────────────────────────────────────────────
     `.chart-box { width: 420px }` est une largeur figée dans une grille qui, elle,
     s'adapte. Et 440 px de haut sur un écran de 844 occupe la moitié de la page
     pour une courbe qu'on ne lit pas. */
  :root .chart-box  { width: auto; min-width: 0; }
  :root .chart-wrap { height: clamp(200px, 45vw, 340px); }

  /* ── 2.8 Gabarits autonomes — vague 2 ─────────────────────────────────────
     /admin, /admin/roadmap, /admin/corbeille, /entities, /login, /forgot,
     /reset, /logout et les deux pages publiques ne passent pas par la coquille :
     ils lient cette feuille eux-mêmes, APRÈS leur <style>. Le préfixe `:root`
     reste nécessaire : un lien placé après ne gagne qu'à spécificité égale, et
     l'ordre d'inclusion n'est pas une garantie qu'on veut tenir à la main.

     Les points de rupture locaux de ces pages (900, 700, 820, 640 px) ont été
     retirés en même temps : leurs règles vivent ici, aux trois valeurs communes.
     Tablette 768–1023 : les grilles à deux colonnes de /admin passent à une
     (elles portent des formulaires larges) et la barre décorative d'/entities
     s'efface — c'est la plage où le rail applicatif n'est déjà plus qu'icônes. */
  :root .grid-2 { grid-template-columns: 1fr; }                /* admin.html, ex-900 px */
  :root aside.sidebar, :root .hide-sm { display: none; }       /* entities.html, ex-820 px */

  /* L'en-tête des trois pages d'administration : un flex en `nowrap` qui, à
     390 px, pousse « Déconnexion » hors de l'écran (+26 px sur /admin/corbeille,
     seul coupable de son débordement). Ciblé par son voisin `nav.adm-tabs`, qui
     n'existe nulle part ailleurs — `header` seul désignerait aussi la topbar de
     l'application (master.js). */
  :root body > header:has(+ nav.adm-tabs) { flex-wrap: wrap; row-gap: 4px; }
  :root body > header:has(+ nav.adm-tabs) > a,
  :root .adm-tabs a, :root .card nav.toc a, :root .login-card .forgot {
    min-height: 40px; display: inline-flex; align-items: center;
  }
  :root .card nav.toc a { display: flex; }
  :root .login-card .forgot { justify-content: center; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   2. TABLETTE — 768 à 1023 px
   Le rail d'icônes est conservé (décision du 06/09). Il n'a besoin que d'être
   atteignable au doigt : sur une tablette tactile, 35 px de haut se ratent
   autant que sur un téléphone.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 768px) and (max-width: 1023px) {
  :root .app-sidebar .sb-item { min-height: 44px; }
  :root .sm-item              { min-height: 42px; }
  :root .tb-ent-name          { max-width: 150px; }
  :root .brick-page           { padding: 16px 16px; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   3. MOBILE — ≤ 767 px
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 767px) {

  /* ── 1.1 Coquille : le rail disparaît, le contenu récupère toute la largeur ──
     `master.css` réserve 64 px en permanence sous 1100 px. Sur 390 px c'est 16 %
     de l'écran pris par des icônes sans libellé, et rien pour les afficher. */
  :root body            { padding-left: 0; }
  :root body::before    { display: none; }          /* squelette navy du rail */
  :root body::after     { left: 0; }                /* bande blanche de la topbar */
  :root .app-topbar     { left: 0; }

  /* ── 1.2 La barre latérale devient un tiroir ──────────────────────────────
     Hors écran par défaut, ouverte par le bouton menu. Les libellés reviennent :
     ils sont DÉJÀ dans le DOM, seule la règle 1100 px les masque. Le tiroir n'a
     donc besoin d'aucun changement de balisage — et il est plus lisible que le
     rail d'icônes muettes qu'il remplace. */
  :root .app-sidebar {
    width: 272px; max-width: 84vw;
    padding: 16px 12px;
    transform: translateX(-100%);
    transition: transform .22s ease-out;
    will-change: transform;
  }
  :root body.nav-ouverte .app-sidebar {
    transform: none;
    box-shadow: 0 0 48px rgba(8, 20, 38, .45);
  }
  /* Le tiroir passe AU-DESSUS de tout le reste. Sa valeur de bureau (40) le
     plaçait sous le bandeau « jeu de test » (60, collé en haut), qui recouvrait
     la première entrée de navigation. Aucune mesure de largeur ne voit ça — c'est
     la capture d'écran qui l'a montré. */
  :root .app-sidebar { z-index: 70; }
  :root .nav-voile   { z-index: 69; }
  /* Les libellés, rendus au tiroir. */
  :root .app-sidebar .sb-item span   { display: inline; }
  :root .app-sidebar .sb-sec         { display: block; padding: 14px 11px 5px; }
  :root .app-sidebar .sb-logo        { display: block; padding: 4px 10px 16px; }
  :root .app-sidebar .sb-logo .wm    { display: inline; }
  :root .app-sidebar .sb-item {
    justify-content: flex-start; gap: 11px; padding: 10px 11px; min-height: 44px;
  }
  :root .app-sidebar .sb-item.on::before { left: -12px; }

  /* Le voile : il rend le tiroir fermable au doigt, sans viser une croix. */
  :root .nav-voile {
    position: fixed; inset: 0;
    background: rgba(8, 20, 38, .5);
    opacity: 0; pointer-events: none; transition: opacity .18s;
  }
  :root body.nav-ouverte .nav-voile { opacity: 1; pointer-events: auto; }
  /* Tiroir ouvert : la page dessous ne défile plus, sinon on perd sa place. */
  :root body.nav-ouverte { overflow: hidden; }

  /* ── 1.3 Topbar : une seule ligne, par ordre d'utilité décroissante ────────
     Mesuré : le contenu utile faisait 508 px pour 326 px disponibles, et le
     bouton de thème comme l'avatar sortaient de l'écran. La cause n'est pas leur
     largeur (20 et 30 px) mais un conteneur flex qui refuse de céder : sans
     `min-width: 0`, un enfant flex ne descend jamais sous la largeur de son
     contenu. C'est le mécanisme exact du débordement. */
  :root .app-topbar        { padding: 8px 10px; gap: 8px; }
  :root .app-topbar > *    { min-width: 0; }
  :root .tb-sync           { display: none; }       /* l'info est aussi dans /sync */
  :root .tb-entity         { padding: 6px 9px; }
  :root .tb-ent-name       { max-width: 92px; }
  :root .tb-menu           { max-width: calc(100vw - 20px); }

  /* Le bouton menu n'existe qu'ici : au-dessus, le rail est visible. */
  :root .tb-menu-btn       { display: flex; }

  /* ── 1.4 Conteneur de page ────────────────────────────────────────────────
     24 px de padding latéral sur 390 px, c'est 12 % du contenu. */
  :root .brick-page        { padding: 14px 12px; }

  /* ── 1.4bis En-tête de l'onglet « Pipe commercial » (app.html:440) ─────────
     Des neuf lignes d'en-tête d'onglet d'app.html, c'est la SEULE posée en
     `display:flex` inline SANS `flex-wrap:wrap` : ses trois groupes de boutons
     sortaient de l'écran à 390 px (vu par la validation visuelle du 07/09,
     jamais par le banc, qui ne mesure que l'onglet par défaut). `flex-wrap`
     n'est pas dans le style inline : la règle s'applique sans !important.
     app.html est gelé (chantier moteurs) : la correction vit ici. */
  :root #tab-pipe > div[style*="display:flex"] { flex-wrap: wrap; row-gap: 8px; }
  :root #tab-pipe > div[style*="display:flex"] .spacer { flex-basis: 0; }

  /* ── 1.5 Sous-menu de brique : il défile déjà, il doit être atteignable ─── */
  :root .sub-menu          { padding: 0 12px; height: 46px; }
  :root .sm-item           { padding: 0 12px; min-height: 44px; }

  /* ── 1.6 Modales : feuille du bas ─────────────────────────────────────────
     `.modal` valait `width: 460px; max-width: 90vw` — sans `max-height`. Une
     modale plus haute que l'écran n'était pas défilable : son bouton de
     validation était inatteignable. Le banc ne voit pas ce défaut (il mesure
     l'horizontale) ; il fallait le corriger quand même.
     Ancrée en bas : c'est la zone qu'on atteint au pouce. */
  :root .modal-overlay.active { align-items: flex-end; }
  :root .modal {
    width: 100%; max-width: none;
    border-radius: 16px 16px 0 0;
    padding: 20px 16px calc(16px + env(safe-area-inset-bottom, 0px));
    max-height: 88vh; max-height: 88dvh;
    overflow-y: auto;
  }
  :root .modal .actions {
    position: sticky; bottom: 0;
    background: var(--surface); padding: 12px 0 0; margin-top: 16px;
  }

  /* ── 1.7 Cibles tactiles — 42 occurrences, la plus utilisée en tête ───────
     Sous 40 px, une cible se rate une fois sur trois au doigt.

     Règle générale plutôt qu'une liste de classes : une liste laisse passer tout
     ce qui sera écrit demain (le banc a trouvé `.preset-btn`, que je n'avais pas
     prévue). Deux exceptions, chacune pour une raison :
       · DANS un tableau — les grilles financières sont pleines de boutons et de
         champs de cellule ; les porter à 40 px multiplierait la hauteur des
         lignes et rendrait le tableau illisible. Le tableau défile, il ne se
         parcourt pas au pouce ligne à ligne.
       · les liens en ligne — un lien au milieu d'une phrase ne se dimensionne
         pas sans casser l'interligne. */
  :root button, :root select, :root textarea,
  :root input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]),
  :root a.btn, :root .btn, :root .btn-tool, :root .vtab, :root .year-tab,
  :root .enter, :root .logout {
    min-height: 40px;
  }
  :root .tb-icon-btn, :root .tb-avatar { min-width: 40px; }
  :root select { min-width: 44px; }
  /* Un bouton de 15 px de large se rate autant qu'un bouton de 15 px de haut —
     y compris dans un tableau : la colonne s'élargit, le tableau défile déjà. */
  :root button { min-width: 40px; }

  /* Le prix caché de la ligne ci-dessus, et il n'est pas intuitif : un
     `min-width` EXPLICITE remplace le `min-width: auto` d'un enfant flex — or
     c'est précisément ce `auto` qui l'empêche de rétrécir sous son contenu. En
     posant 40 px, on a retiré la protection : les onglets du sous-menu se sont
     mis à se chevaucher (67 px de boîte pour 123 px de texte, mesuré sur
     /opex@390 ; le texte en `nowrap` débordait sur le voisin).

     Aucune mesure de largeur ne voyait ce défaut — la page ne débordait pas, les
     onglets s'écrasaient les uns sur les autres. C'est la capture d'écran qui l'a
     montré. On rend donc explicite ce que `auto` faisait tout seul. */
  :root .sm-item, :root .main-tab, :root .vtab, :root .year-tab, :root .tb-icon-btn,
  :root .toolbar > button, :root .toolbar > select, :root .toolbar > a,
  :root .bhead > button, :root .inline-form > button, :root .year-tabs > * {
    flex-shrink: 0;
  }

  /* L'exception « DANS un tableau » ci-dessus a une exception, et elle est
     MESURÉE : un en-tête de tri. Le banc en a relevé sept sur quatre écrans
     (`/` onglets Encaissements, New biz HubSpot, Suivi AM ; `/facturation`
     Liste des factures) — des `th` qui portent un `onclick`, donc des COMMANDES
     et non des cellules de données.

     Le motif de l'exclusion ne s'y applique pas : un en-tête, c'est UNE ligne
     par tableau, pas une par enregistrement. Le porter à 40 px ajoute quelques
     pixels une seule fois, là où le faire sur `tr[onclick]` ou sur un champ de
     cellule multiplierait la hauteur par le nombre de lignes. C'est exactement
     la raison écrite plus haut, appliquée dans l'autre sens.

     `[onclick]` et pas une classe : c'est le marqueur que la sonde elle-même
     utilise pour décider qu'un élément est cliquable (mesures.js §4), et
     `app.html` comme `facturation.html` stylent déjà `thead th[onclick]:hover`.
     On vise donc ce que le produit déclare, pas ce qu'on suppose.

     `height` ET PAS `min-height`, et c'est la mesure qui l'a dit : la première
     version de cette règle écrivait `min-height: 40px` et n'a RIEN changé — les
     en-têtes sont restés à 29, 31 et 36 px au banc. Une cellule de tableau
     (`display: table-cell`) ignore `min-height` ; sa hauteur vient de la ligne, et
     `height` y est traité comme un minimum. Écrit de mémoire, ce correctif aurait
     été livré vert et inopérant. */
  :root th[onclick] { height: 40px; min-width: 40px; }

  /* Une LIGNE de section cliquable — `/` onglet MRR projeté, `tr.proj-section`
     qui replie une section de la projection. 27 px mesurés : elle rate de 3 px
     l'échappatoire « ligne large ≥ 30 px » de la sonde, ce qui en dit surtout la
     limite. Ce n'est pas une ligne de données : il y en a quelques-unes par
     tableau, comme un en-tête, donc l'exclusion « dans un tableau » ne s'applique
     pas davantage ici. La hauteur se pose sur les CELLULES, pour la raison
     ci-dessus. */
  :root tr.proj-section.proj-toggle > td { height: 40px; }

  /* DEUX CIBLES QUE LA COUCHE NE PREND PAS, et la mesure dit pourquoi — la
     référence figée ne doit pas les excuser en silence :

     · `/opex` @grand-livre, `input.gl-per` (12 × 40 px). J'ai écrit
       `:root input.gl-per { min-width: 40px }` et MESURÉ le résultat :
       `min-width` restait à `0px`. §2.4 pose `min-width: 0` sur
       `:root input:not([type=checkbox]):not([type=radio]):not([type=hidden])`,
       soit (0,3,1) contre (0,2,1) — la règle générale gagne, et aucune plage de
       média n'y change rien. On peut monter la spécificité ; on ne doit PAS :
       `table.gl` est en `table-layout: fixed` et la cellule mesure 28 px. Un
       champ de 40 px y déborderait sur la colonne voisine, sans que la colonne
       s'élargisse. Ce qu'il faut, c'est une largeur de colonne — une décision
       qui appartient à `web/opex.html`, donc à REVENUS. ROUTÉ.

     · `/budget` @opex, `input.ichk` (13 × 13 px), la case à cocher « Récup. »
       de la grille de BP. La règle générale exclut délibérément les cases à
       cocher (« sa cible réelle est son ÉTIQUETTE »), et la sonde applique la
       même exception — mais celle-ci n'a PAS d'étiquette : elle est posée nue
       dans `td.lbl > div`. Lui donner 40 px la rendrait laide ; lui donner une
       étiquette est un changement de BALISAGE dans `web/budget.html`, gelé pour
       RESPONSIVE. ROUTÉ à REVENUS.

     Les deux restent donc dans la référence, et le message de régénération les
     nomme. « Accepté et nommé » n'est pas « excusé en silence ». */

  /* Un lien SEUL dans son bloc est un élément de navigation, pas de la prose :
     il se vise au doigt et doit donc faire 40 px. Un lien au milieu d'une phrase
     est exclu — l'agrandir casserait l'interligne du texte. */
  :root li > a, :root .toolbar a, :root td > a:only-child, :root .bhead a,
  /* Et le cas qu'aucune classe ne nomme : un lien qui côtoie un BOUTON dans le
     même conteneur est une commande, pas de la prose. C'est le seul repère
     disponible quand l'en-tête est un <div style="display:flex"> anonyme —
     facturation.html:38, et il y en a d'autres. La classe manquante est un
     travail de phase B ; la règle, elle, dit quelque chose de vrai. */
  :root div:has(> button) > a {
    min-height: 40px; display: inline-flex; align-items: center;
  }

  /* Une case à cocher ne s'agrandit pas sans devenir laide : c'est son ÉTIQUETTE
     qui est la cible réelle, et c'est elle qu'on porte à 40 px. */
  :root label:has(> input[type="checkbox"]), :root label:has(> input[type="radio"]) {
    min-height: 40px; display: flex; align-items: center; gap: 8px;
  }

  /* Les intitulés de section du tiroir : 10 px passait sur un rail de 64 px où
     ils étaient masqués ; dans un tiroir on les lit. */
  :root .app-sidebar .sb-sec { font-size: 11px; }

  /* ── 1.8 Saisie : 16 px évite le zoom automatique d'iOS ───────────────────
     Restreint aux formulaires et modales : l'appliquer aux champs de CELLULE
     ferait exploser la largeur des tableaux financiers, qui en sont pleins. */
  :root .modal input, :root .modal select, :root .modal textarea,
  :root form:not(.inline-form) input, :root form:not(.inline-form) select {
    font-size: 16px; min-height: 40px;
  }

  /* ── 1.9 Densité des cartes KPI ───────────────────────────────────────────
     Neuf déclarations locales, cinq `min-width` (150/160/170/180/210). On ne les
     supprime pas ici — on lève seulement le plancher qui empêche le repli. */
  :root { --kpi-base: 140px; }

  /* ── 1.10 Gabarits autonomes — vague 2, densité mobile ────────────────────
     Mêmes intentions que 1.4 pour la coquille : sur 390 px, 20 px de marge de
     page plus 24 px de marge de carte laissaient 302 px au contenu. */
  :root body > header:has(+ nav.adm-tabs) { padding: 12px 14px; gap: 10px; }
  :root body > header:has(+ nav.adm-tabs) ~ main { padding: 0 10px; margin-top: 16px; gap: 16px; }
  :root body > header:has(+ nav.adm-tabs) ~ main > .card { padding: 16px 12px; }
  :root .adm-tabs { padding: 0 10px; overflow-x: auto; scrollbar-width: none; }
  :root .adm-tabs::-webkit-scrollbar { display: none; }
  :root .adm-tabs a { flex-shrink: 0; }
  :root .card nav.toc { columns: 1; }                          /* admin_roadmap.html, ex-700 px */
  :root .card td .file { font-size: 11px; }                    /* lisibilité : 10,5 px → 11 px */
  :root aside.sidebar + .main > .content { padding: 16px 12px; } /* entities.html */
  :root aside.sidebar + .main > .topbar  { padding: 8px 12px; }

  /* ── 1.11 Lisibilité : plus aucun texte sous 11 px ────────────────────────
     Les huit derniers défauts `texte-sous-11px` du banc (390 et 430 px, sur /,
     /dashboard, /pnl, /abonnements) : sept styles écrits entre 9 et 10,5 px dans
     des écrans GELÉS pour ce chantier (app.html et web/{dashboard,pnl,
     abonnements}.html appartiennent au chantier moteurs). Ils sont corrigés
     depuis la couche, sans toucher une seule ligne de ces fichiers — c'est
     précisément ce pour quoi elle existe, et le procédé est déjà celui de
     `.card td .file` ci-dessus.

     Aucun de ces sélecteurs n'a besoin de plus que le `:root` de la convention :
       `.upsell-indicator` (style.css:239, 9 px)      0,1,0 → 0,2,0
       `.dcard .s`         (dashboard.html:22, 10,5)  0,2,0 → 0,3,0
       `.pnl-tbl thead th` (dashboard.html:32, 10,5)  0,1,2 → 0,2,2
       `.todef`            (pnl.html:54, 10 px)       0,1,0 → 0,2,0
       `.kpi .lbl`         (abonnements.html:15, 10,5) 0,2,0 → 0,3,0

     11 px et pas davantage : c'est le seuil de la sonde, et le plus petit écart
     visuel possible avec le bureau, qui n'est pas touché (règle sous 768 px —
     au-dessus, la densité d'origine est conservée). */
  :root .upsell-indicator  { font-size: 11px; }
  :root .dcard .s          { font-size: 11px; }
  :root .pnl-tbl thead th  { font-size: 11px; }
  :root .todef             { font-size: 11px; }
  :root .kpi .lbl          { font-size: 11px; }

  /* ── 1.12 Lisibilité : ce que le banc NE VOIT PAS ─────────────────────────
     Les sous-lignes du budget (`web/budget.html:177-178`, 10,5 px) n'existent
     dans le DOM qu'après un clic sur le chevron d'une ligne parente : le banc
     mesure la page telle qu'elle est SERVIE et n'ouvre jamais un dépliant de
     grille financière (il ne clique que des verbes qui « ouvrent » un panneau,
     jamais un expander de tableau — `OUVRENT` dans banc.py). Le défaut est donc
     réel et invisible à la mesure : il ne sortira qu'un jour, chez un
     utilisateur qui déplie une ligne sur son téléphone.

     On le corrige ici pour la même raison que §1.11, et le test le tient — c'est
     le seul filet possible pour ce qui échappe au navigateur.

     Ne sont PAS relevés, et c'est délibéré : les CHAMPS de cellule du budget
     (`input.icell` 10 px, `input.ilbl` 10,5 px, `select.isel` 9,5 px). §1.8 dit
     pourquoi — agrandir la saisie DANS une grille financière fait exploser la
     largeur du tableau. Un correctif qui casse un écran sain n'en est pas un. */
  :root table.bpx tr.itemrow td.lbl,
  :root table.bpx tr.itemrow td.num { font-size: 11px; }

  /* ── 1.13 Lisibilité : ce que le JEU DE DONNÉES a révélé ──────────────────
     `amorce.py` ne semait aucune ligne de BP : `/budget` et une partie d'`/opex`
     étaient mesurés à vide. Avec 6 lignes de BP, 4 sous-lignes et leurs valeurs
     mensuelles, le banc a vu deux familles de texte de plus. Celle-ci en est une :
     l'en-tête du tableau « budget vs réel » (`web/opex.html:123`, 9,5 px), soit
     47 cellules sur /opex@390 — les douze mois, les cumuls et les totaux.

     Le tableau défile déjà dans son cadre (§ 2.1) : 1,5 px de plus sur l'en-tête
     élargit à peine des colonnes qu'on fait défiler de toute façon. */
  :root table.bvr thead th { font-size: 11px; }

  /* ── 1.14 Lisibilité : ce que les ONGLETS ont rendu visible ───────────────
     La référence figée du lot F photographie 15 `texte-sous-11px`. Ce n'est pas
     un quitus : c'est une liste de travaux, et elle est traitée ici.

     Onze sont pris par la couche, chacun au premier cran de spécificité que la
     convention `:root` donne — aucun n'a demandé de forcer quoi que ce soit, et
     l'UNIQUE `!important` du fichier (§2.5) reste unique :

       `.hint code`         style.css:387     10,5 px  (0,1,1 → 0,2,1)  /@mrr-projete
       `table.bpx thead th` budget.html:76    10 px    (0,1,2 → 0,2,2)  /budget @opex,capex,masse-salariale
       `table.q th`         opex.html:48      10 px    (0,1,2 → 0,2,2)  /opex @reglages
       `table.gl th`        opex.html:111     10 px    (0,1,2 → 0,2,2)  /opex @grand-livre
       `.kpi .k`            pipeline.html:30  10,5 px  (0,2,0 → 0,3,0)  /pipeline @previsionnel
       `.opt .st`           sync.html:54      10,5 px  (0,2,0 → 0,3,0)  /sync @sources
       `.pstate`            sync.html:69      10,5 px  (0,1,0 → 0,2,0)  /sync @connecteurs
       `.dchip`             sync.html:72      10,5 px  (0,1,0 → 0,2,0)  /sync @connecteurs
       `.pill`              config.html:22    10,5 px  (0,1,0 → 0,2,0)  /config @facturation, @correspondance-clients

     QUATRE N'ÉTAIENT PAS PRIS au lot F, tous écrits en STYLE D'ATTRIBUT, que seul
     un second `!important` battrait. Ils ont été ROUTÉS à REVENUS (`app.html` et
     `web/budget.html` sont gelés pour RESPONSIVE), qui a livré `37cbad2`.

     MESURE du 08/09 20h06, sur l'arbre exact (production `7e26e94`, banc
     20260908-201037, référence `bed906bb…`, même jeu d'amorce) : les SEPT défauts
     de cette liste ont DISPARU. Le banc rend `bloquant 0 · gênant 0 · cosmétique 0`
     sur ses 178 mesures, et `opex#grand-livre` rend toujours ses 145 lignes — ce
     n'est donc pas une dérive de données, c'est du code.

     QUATRE étaient déjà tombés avec `37cbad2` (compteur de département, span de
     trésorerie, largeur rendue à `input.gl-per` par `table.gl { min-width: 900px }`).

     LES TROIS QUE CETTE SECTION DÉCRIVAIT COMME « RESTANTS » SONT TOMBÉS AVEC
     `ed21057` (lot 7 de REVENUS). C'est REVENUS qui les a corrigés, dans les fichiers
     qui lui appartiennent, comme cette section le demandait :

       budget#opex@390       texte 9,5 px    les quatre spans « Σ items / calculé /
                                             Σ groupe » de `web/budget.html`
       budget#opex@390       cible tactile   la case « ligne vérifiée » `input.ichk`
       mrr-app#suivi-am@390  texte 10,5 px   la légende `#amPipelineLegend` d'`app.html`

     Cette section ne décrit donc plus aucun travail en attente, et RESPONSIVE ne
     route plus rien à REVENUS de ce chef. Elle est conservée parce que les onze
     règles ci-dessous sont ce qui tient les onze premiers défauts : les retirer les
     ferait revenir. Corollaire pour la référence figée : elle portait encore ces
     trois entrées et elle a été régénérée délibérément avec ce lot.

     L'UNIQUE `!important` du fichier (§2.5) reste unique — il l'est resté sur les
     quatorze défauts de cette section, pris ou routés. */
  :root .hint code          { font-size: 11px; }
  :root table.bpx thead th  { font-size: 11px; }
  :root table.q th          { font-size: 11px; }
  :root table.gl th         { font-size: 11px; }
  :root .kpi .k             { font-size: 11px; }
  :root .opt .st            { font-size: 11px; }
  :root .pstate             { font-size: 11px; }
  :root .dchip              { font-size: 11px; }
  :root .pill               { font-size: 11px; }

  /* ── 1.15 Lisibilité : ce que le banc ne voit que si l'on SURVOLE ──────────
     REVENUS a routé quatre familles de texte sous 11 px avec sa mesure, en précisant
     ce qui les rend prenables : elles vivent dans une FEUILLE DE STYLE (le bloc
     `<style>` de `web/budget.html`), pas en style d'attribut. Trois sont prises ici,
     au premier cran de spécificité que la convention `:root` donne (0,1,0 → 0,2,0) ;
     aucune ne demande de forcer quoi que ce soit :

       `.tcat-inh`  web/budget.html:170   9,5 px    31 occurrences   catégorie héritée
       `.ord-btn`   web/budget.html:204   8 px     128 occurrences   flèches ▲ ▼
       `.rn-btn`    web/budget.html:221  10 px     179 occurrences   ✏ + ▤ ⤵

     POURQUOI LE BANC NE LES VOIT PAS, et pourquoi ce n'est pas un quitus : `.ord-btn`
     et `.rn-btn` sont en `visibility:hidden` tant que la ligne n'est pas survolée
     (`tr:hover`), et le banc ne survole rien ; `.tcat-inh` n'apparaît que sur une
     sous-ligne dont la catégorie de trésorerie est HÉRITÉE. Même motif qu'en §1.12,
     autre déclencheur — et sur un écran tactile, « au survol » veut dire « au
     premier contact du doigt » : ces trois-là sont vues par l'utilisateur mobile.
     `visibility:hidden` conserve la place : le passage à 11 px n'insère aucune boîte
     nouvelle, il en agrandit une déjà réservée.

     LA QUATRIÈME EST REFUSÉE, ET C'EST MESURÉ, PAS SUPPOSÉ. Les 338 `option` sous
     11 px héritent des `select` de `web/budget.html` (`select.tcat-sel` et
     `select.tm-sec` 9,5 px, `table.bpx select.isel` 9,5 px, `select.modecell` 10 px),
     que §1.8 exclut délibérément. Une règle `:root option { font-size: 11px }` PREND
     — le style calculé passe bien de 9,5 px à 11 px — et ne change RIEN de ce que
     l'utilisateur voit : le contrôle FERMÉ dessine son texte à la taille du `select`,
     et la liste déroulée est dessinée par le SYSTÈME. Témoin poussé à l'absurde pour
     que la conclusion ne tienne pas à une marge de mesure : à `option{font-size:30px}`,
     la largeur du contrôle fermé ne bouge pas d'un pixel (95,00 px avant, 95,00 px
     après, `select.isel` ; 87,00 px pour `select.tcat-sel`). Écrire cette règle ferait
     taire 338 mesures sans corriger un seul défaut — c'est l'inverse du métier de
     cette couche. On ne l'écrit pas, et on le dit. Sonde reproductible dans l'archive
     du lot (`sonde_option.py`, même Chromium épinglé que le banc). */
  :root .tcat-inh { font-size: 11px; }
  :root .ord-btn  { font-size: 11px; }
  :root .rn-btn   { font-size: 11px; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   4. DIVERS
   ═══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  :root .app-sidebar, :root .nav-voile { transition: none; }
}
