/* ==========================================================================
   LAYOUT SYSTEM — shared by build, airfact, trade
   Edit here; all three apps symlink this file.
   ========================================================================== */

/* Container System */
.container {
  width: 100%;
  max-width: var(--container-max-width);
  margin: 0 auto;
  padding: 0 var(--space-4);
}

.container-fluid {
  width: 100%;
  padding: 0 var(--space-4);
}

/* Main Layout Structure */
.main-content {
  margin-left: var(--sidebar-width);
  padding: var(--content-pad);
  /* Tighter gap between chrome and content — Linear/Vercel-style. The
     topbar already provides a horizontal divider line, so the content can
     sit close to it without feeling crowded. */
  padding-top: calc(var(--topbar-height, 80px) + var(--space-5));
  width: calc(100% - var(--sidebar-width));
  min-height: 100vh;
  box-sizing: border-box;
}

/* ──────────────────────────────────────────────────────────────────────────
   App Topbar — single chrome strip, absorbs the legacy mobile-header (left
   hamburger, mobile only) and per-page page-header (title left, actions
   right). Always renders for authenticated dash pages. Pinned to top, sits
   to the right of the sidebar on desktop and full-width on mobile. */
.app-topbar {
  position: fixed;
  top: 0;
  left: var(--sidebar-width);
  right: 0;
  height: var(--topbar-height, 80px);
  display: flex;
  align-items: center;
  gap: var(--space-6);
  /* Vertical padding matches the sidebar's outer padding (chrome rhythm).
     Horizontal padding matches .main-content so the title's left edge and
     the actions' right edge sit on the same vertical grid as the page body
     (table headers, cards, breadcrumbs all line up). */
  padding: var(--chrome-pad-y) var(--content-pad);
  background: var(--bg);
  z-index: var(--z-topbar);
  box-sizing: border-box;
}

/* ── UNE BARRE VIDE N'EST PAS UNE BARRE (2026-09-14, décision Olivier) ──────
   `.table-toolbar` porte déjà cette règle un étage plus bas. La barre du haut
   l'ignorait : une page sans `page_header()` laissait 80 px de bande nue, plus
   les 24 px que `.main-content` paie par-dessus.

   ⚠ LE JETON, PAS `display`. `.main-content` tire son `padding-top` de
   `--topbar-height` : masquer la barre sans toucher au jeton laisserait les
   104 px de gouttière. C'est donc `<body>` qui décide (PHP,
   `UIComponents::topbarEstVide()`), exactement comme `topbar-has-search`.

   ⚠ CETTE RÈGLE EST HORS DE TOUTE MEDIA QUERY, et le bloc mobile la CONTREDIT
   plus bas (56 px, pour le hamburger). L'ordre compte : le bloc mobile vient
   après. */
body.topbar-empty { --topbar-height: 0px; }
body.topbar-empty .app-topbar { display: none; }

/* Opt-in topbar search slot — topbar_search(). Sits between the title
   (flex:1) and the actions cluster. Bounded width, shrinks before it
   crowds; .search-wrapper's 280px floor is too rigid inside chrome.

   ⚠ LES DEUX DÉCLARATIONS PORTENT DEUX CLASSES, et ce n'est pas décoratif.
   Le slot rend `class="app-topbar-search topbar-search"` : il hérite donc
   aussi de `.topbar-search` (top-nav.css), le dialecte de la barre de
   navigation, qui pose `flex: 1 1 300px` et `min-width: 280px`. top-nav.css
   est importé APRÈS layout.css dans main.css — à spécificité égale (0,1,0)
   c'est lui qui gagne. `min-width` était déjà écrit en (0,2,0) ; `flex` ne
   l'était pas, et le `flex` CALCULÉ du slot valait donc `1 1 300px` : le
   champ GROSSISSAIT avec la fenêtre au lieu d'être borné, à rebours du
   commentaire ci-dessus. Mesuré sur comptoir /inbox — seul consommateur du
   slot jusqu'ici — 368 px à 1280, 448 à 1440, 568 à 1680, soit plus large
   que le bloc de titre. Corrigé le 2026-09-13 en alignant la spécificité de
   `flex` sur celle de `min-width`. Ne pas « simplifier » en retirant la
   seconde classe : ça réinstalle le défaut, en silence. */
.app-topbar-search.topbar-search { flex: 0 1 320px; min-width: 180px; }

/* ── LES ONGLETS DE LA PAGE, EN 2e RANGÉE DU CHROME (topbar_tabs) ───────────
   Rendu par `partials/topbar.php` quand la page appelle `topbar_tabs()`.

   ⚠ LA HAUTEUR EST UN JETON, PAS UNE RÈGLE SUR LA BARRE. `.app-topbar` est
   `fixed` : ce qu'elle mesure ne pousse rien. C'est `--topbar-height` que lit
   le `padding-top` de `.main-content` — réhausser l'un sans l'autre fait
   passer le chrome PAR-DESSUS le contenu, en silence. Même montage que
   `body.topbar-has-search` en mobile, et pour la même raison.

   ⚠ 102 px EST MESURÉ, PAS CHOISI. Banc `ops/ui-capture/bancs/onglets-dans-le-
   chrome`, montage `Bauto` : la barre est montée en `height: auto`, sa hauteur
   naturelle relevée, et le jeton doit la COUVRIR. Mon premier nombre (102) est
   passé au travers de la lecture et a été démasqué par la mesure : le
   soulignement de l'onglet actif finissait à y=103 pour une barre qui s'arrête
   à 102 — 1 px de bordure rendu hors du chrome, invisible à l'œil, faux quand
   même. Une hauteur de chrome est payée par son contenu le plus haut.
   Décomposition : 16 (padding haut) + 40 (rangée 1 : le champ et le bouton,
   pas le titre qui ne fait que 24) + 46 (la bande, soulignement compris).

   ⚠ 104 AU DÉPART, RAMENÉ À 102 LE 14-09 : les deux pages qui ont opté ont
   renoncé à leur sous-titre, la rangée 1 a perdu ses 2 px. Deux pixels ne se
   voient pas — mais ils faisaient écrire le jeton par le garde-fou JS à chaque
   chargement, donc bouger le contenu APRÈS la peinture. C'est-à-dire un flash,
   en plus petit. Un jeton qui ne colle pas exactement se paie toujours. */
/* ⚠ CE JETON EST UNE VALEUR DE PREMIÈRE PEINTURE, PLUS UNE VÉRITÉ.
   Depuis le 2026-09-14 (demande Olivier : « une seule ligne de header »), c'est
   core.js qui publie la hauteur RÉELLE de la barre après avoir décidé si la
   bande tient sur la ligne du titre — voir le bloc « LA BANDE D'ONGLETS DU
   CHROME » là-bas. On garde ici la HAUTE des deux valeurs : un jeton trop grand
   laisse un blanc pendant une image, un jeton trop petit fait passer le chrome
   PAR-DESSUS le contenu. Et si le JS ne tourne pas, deux rangées restent
   lisibles — une ligne mal cadrée, non. */
body.topbar-has-tabs { --topbar-height: 102px; }
.app-topbar--tabs {
  /* `height: auto` : c'est le contenu qui décide, et core.js le RELÈVE. La
     barre reste `fixed`, donc sa hauteur ne pousse rien par elle-même. */
  height: auto;
  /* ⚠ `--chrome-row-height`, PAS `--topbar-height` : ce dernier vaut 104 sur
     cette page (body.topbar-has-tabs), il figerait donc la barre à 104 px même
     quand tout tient sur une ligne — et core.js publierait 104, indéfiniment.
     Le plancher, c'est UNE rangée de chrome. */
  min-height: var(--chrome-row-height);
  flex-wrap: wrap;
  align-content: center;
  row-gap: 0;
}

/* ── LA VARIANTE « UNE SEULE LIGNE » N'EST PLUS ICI (2026-09-14) ────────────
   Ses déclarations sont émises PAR LA PAGE, dans une requête de média dont le
   seuil est mesuré — voir `UIComponents::topbarTabsFirstPaintCss`. Elles ont
   vécu ici, pilotées par une classe que posait un script de pré-peinture : la
   page peignait alors l'état à deux rangées avant de sauter à une ligne, trois
   fois de suite, sans que j'aie jamais trouvé l'instant fautif.

   ⚠ NE PAS LES RAPATRIER. Deux copies de la même mise en page — une par classe
   ici, une par requête de média là — divergeraient à une largeur particulière,
   c'est-à-dire là où personne ne regarde. Ce fichier ne garde que l'état à DEUX
   RANGÉES, qui est le repli : lisible, et juste quand rien ne s'exécute. */

/* ── L'ORDRE DE LA RANGÉE (décision Olivier, 14-09, 3e passe) ───────────────
       titre · … · onglets · … · recherche · Actions
   Le bouton d'actions ferme la rangée, au bord ; la recherche vient à sa
   gauche ; les onglets flottent au milieu de ce qui reste.

   Porté par `--compact` : la grappe de droite est la même avec ou sans onglets.

   ⚠ CELA DÉPLACE LA RECHERCHE PAR RAPPORT À LA RÈGLE DU 13-09, qui l'ancrait
   au bord pour qu'elle tombe au MÊME x sur toutes les pages. Son bord gauche
   dépend désormais de la largeur du bouton d'actions — donc du libellé, donc
   de la traduction. C'est borné : sous la règle « un seul contrôle d'en-tête »,
   la grappe va de 102 à 187 px (mesuré sur les 13 listes d'airfact). Et cela ne
   vaut QUE pour les pages qui ont demandé le chrome compact.

   ⚠ POSÉ SUR `--tabs`, ET VALABLE AUX DEUX MISES EN PAGE : sinon l'ordre des deux blocs
   s'inverserait en rétrécissant la fenêtre, au passage à deux rangées — une
   rangée qui se réarrange quand on redimensionne se lit comme un bug. L'ordre
   du DOM, lui, ne bouge pas : le partiel rend la bande en DERNIER pour qu'elle
   reste la dernière à se replier en mobile. */
.app-topbar--compact .app-topbar-actions { order: 2; }
.app-topbar--compact .app-topbar-search  { order: 3; }

/* ⚠ ET LA GOUTTIÈRE SE RESSERRE ENTRE CES DEUX-LÀ (demande Olivier, 14-09).
   La barre porte UNE gouttière (`column-gap: var(--space-6)`, 24 px) et elle
   vaut pour tous ses enfants. Le bouton d'actions et le champ de recherche
   forment pourtant une seule grappe — ce qu'on FAIT et ce avec quoi on
   RESTREINT, au même bord — et 24 px les séparait comme deux blocs étrangers.
   Une marge gauche négative retranche la différence : 24 − 12 = 12 px entre
   eux, la gouttière pleine partout ailleurs. C'est le seul levier possible,
   `column-gap` ne sachant pas s'appliquer par paire. */
.app-topbar--compact .app-topbar-search {
  margin-left: calc(var(--space-3) - var(--space-6));
}


.app-topbar-tabs {
  /* Rangée pleine, en DERNIER — après la recherche (`order: 10`), qui doit
     rester la première des deux à passer à la ligne en mobile. */
  flex: 1 1 100%;
  order: 11;
  min-width: 0;
  align-self: flex-end;
  /* Le soulignement de l'onglet actif tombe SUR le filet du chrome
     (`.app-topbar::after`, bottom: 0) : la marge négative annule le padding
     bas de la barre. C'est ce pour quoi la variante `in-header` a été écrite
     — « the active tab's bottom border sits flush with the header's bottom
     border » (navigation.css). Sans elle, deux traits parallèles à 16 px. */
  margin-bottom: calc(var(--chrome-pad-y) * -1);
}
/* La bande ne porte NI bordure NI marge ici : `topbarTabs()` les coupe déjà
   côté PHP (`border: false`, `mb: false`). Cette règle est la ceinture — un
   appelant qui monterait la bande à la main ne dessinerait pas deux filets. */
.app-topbar-tabs .nav-tabs-container { margin-bottom: 0; border-bottom: 0; }

/* Bottom separator inset to match the content frame — a full-width
   border-bottom would extend past the title on each side and misalign
   with the table/card edges below. Pseudo-element keeps the line in
   sync with content padding via the shared --content-pad token. */
.app-topbar::after {
  content: "";
  position: absolute;
  left: var(--content-pad);
  right: var(--content-pad);
  bottom: 0;
  height: 1px;
  background: var(--border);
}

/* ── LE FILET DU CHROME S'EFFACE AU REPOS (décision Olivier, 14-09) ─────────
   « On peut virer la ligne qui sépare le header du reste de la page. »

   ⚠ MAIS PAS AU DÉFILEMENT, et ce n'est pas un compromis timide : `.app-topbar`
   est `fixed` et son fond est `var(--bg)`, LE MÊME que celui de la page. Le
   filet est donc la seule chose qui marque le bord une fois la page défilée —
   sans lui, les lignes d'un tableau glissent sous la barre et disparaissent
   dans une surface identique, sans bord. Au repos, il n'y a rien à séparer :
   la barre est en haut du document, la ligne ne dit rien que la géométrie ne
   dise déjà.

   `opacity` plutôt que `display` ou `height` : le pseudo-élément garde sa
   géométrie, donc rien ne bouge quand il apparaît — même exigence que pour le
   bouton d'effacement DANS le champ de recherche (rien de ce qui apparaît après
   le premier peint ne doit déplacer quoi que ce soit).

   ⚠ PORTÉ PAR `--compact`, PAS PAR `--tabs` (2026-09-14, 2e passe) : rien
   là-dedans ne dépend des onglets. Tant que la règle portait la classe des
   onglets, l'appliquer à une page qui n'en a pas — `/jobs` — aurait obligé à
   lui en inventer. Un nom par idée. Les pages qui n'ont rien demandé gardent
   leur filet. */
.app-topbar--compact::after {
  opacity: 0;
  transition: opacity var(--transition);
}
.app-topbar--compact.is-scrolled::after { opacity: 1; }

/* ── LE CONTENU MONTE JUSQU'AU CHROME (décision Olivier, 14-09) ─────────────
   « Je veux que le main remonte jusqu'au header, vu qu'il n'y a plus de trait. »

   `.main-content` réserve partout `--topbar-height` PLUS une gouttière (20 px
   sur le bureau, 16 en mobile). Cette gouttière existait parce qu'un filet
   séparait les deux : elle l'empêchait de coller au contenu. Le filet effacé,
   elle ne sépare plus rien — elle ajoute du vide sous une barre qui a déjà le
   même fond que la page.

   Le contenu fournit sa propre limite : le premier bloc d'une page de liste est
   une `.table-container` blanche sur fond gris, son bord EST la séparation.

   ⚠ (0,2,0) VOLONTAIREMENT. `.main-content` porte son `padding-top` en (0,1,0)
   sur le bureau ET dans la media query mobile — une media query n'ajoute aucune
   spécificité. Une seule règle à (0,2,0) couvre donc les deux, et il n'y a pas
   deux endroits où la valeur se décide. Bornée aux pages qui ont opté pour les
   onglets dans le chrome. */
body.chrome-compact .main-content { padding-top: var(--topbar-height); }

.app-topbar-hamburger {
  display: none;
  background: transparent;
  border: none;
  padding: var(--space-2);
  cursor: pointer;
  color: var(--text);
  border-radius: var(--radius);
}
.app-topbar-hamburger:hover { background: var(--color-surface-2); }

.app-topbar-title {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
}

/* Title + subtitle stack vertically inside this wrapper; the back arrow
   (when present) sits as a row-sibling of this wrapper so it renders
   LEFT of the title block, not stacked above it. */
.app-topbar-title-text {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  min-width: 0;
  flex: 1 1 auto;
}

.app-topbar-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin-right: var(--space-2);
  border-radius: var(--radius);
  color: var(--text-secondary);
  text-decoration: none;
  flex-shrink: 0;
}
.app-topbar-back:hover { background: var(--color-surface-2); color: var(--text); }

/* ── LES GESTES : RANGÉS PAR DÉFAUT, DÉPLIÉS SI LA PAGE A MESURÉ LA PLACE ───
   `page_actions(['deplie_des' => N])` rend les DEUX formes ; la requête de
   média émise dans le `<head>` (voir `UIComponents::pageActionsFirstPaintCss`)
   montre la forme dépliée au-dessus du seuil.

   ⚠ LE REPLI EST « RANGÉ », et ce n'est pas arbitraire : c'est la forme de la
   règle du 13-09 (deux gestes ou plus ⇒ un seul bouton « Actions »). Si la
   requête ne s'applique pas — seuil non déclaré, feuille en retard, largeur
   insuffisante — la page retombe sur la règle, pas sur un entre-deux. */
.page-actions-deplie { display: none; }
.page-actions-replie { display: inline-flex; }

/* ── UN ONGLET SANS GESTE N'A PAS DE GRAPPE (2026-09-14) ────────────────────
   Les menus de gestes sont rendus tous ensemble, un par onglet, et masqués sauf
   celui de l'onglet actif. Un onglet qui n'en déclare aucun laisse donc un
   `.app-topbar-page-actions` vide — qui garderait ses deux gouttières de 24 px,
   prises sur le bloc de titre. `:has()` fait découler la mise en page de la
   PRÉSENCE d'un menu visible, plutôt que de la déclarer une seconde fois
   (même motif que `.table-toolbar` dans tables.css). */
.app-topbar-page-actions:not(:has(> .shell-actions:not([hidden]))):has(> .shell-actions) {
  display: none;
}

/* ── CE QUI QUALIFIE LE TITRE (topbar_title_aside, 2026-09-14) ──────────────
   Un contrôle d'ÉTAT collé au titre : la fenêtre de mois du facturier, un
   exercice comptable. Il ne rétrécit pas — un sélecteur à demi lisible ne
   sélectionne rien — et c'est donc le bloc de titre qui rend la place, comme il
   le fait déjà face à la grappe d'actions.

   ⚠ `margin-left: auto` LE POUSSE À DROITE, contre le champ de recherche
   (décision Olivier, 14-09 — il a d'abord été collé au titre). Tout le mou
   passe donc DEVANT lui. La gouttière qui le sépare de la recherche est celle,
   resserrée, du chrome compact : 12 px, comme entre « Actions » et la
   recherche — les trois forment une seule grappe au bord droit. */
.app-topbar-aside {
  display: flex;
  align-items: center;
  flex-shrink: 0;
  margin-left: auto;
}
.app-topbar-aside:empty { display: none; }

/* ⚠ UNE BARRE POSÉE DANS LE CHROME N'HÉRITE PAS DE SES GOUTTIÈRES. Le composant
   de tickets rendait ses contrôles dans `.table-toolbar-right`, qui porte
   `gap: var(--space-3)` ; l'hôte que la page pose dans la barre du haut, lui,
   n'est qu'un `<div>` nu — et tout s'y est retrouvé collé bout à bout (capture
   d'Olivier : « pourquoi ils sont petits et tous collés »). La gouttière
   appartient à la RANGÉE, pas au composant : elle est donc rendue ici. */
.tk-barre-chrome {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* ⚠ UNE SEULE HAUTEUR DE CONTRÔLE DANS LA RANGÉE. Mesuré avant correctif :
   puces à 22 px, segments à 28, bouton primaire à 30 — TROIS hauteurs côte à
   côte dans 80 px de chrome. C'est la moitié du « petits et tous collés »
   d'Olivier ; l'autre moitié était la gouttière absente juste au-dessus.

   Chacune de ces hauteurs est juste CHEZ ELLE : 22 px pour une puce posée
   au-dessus d'un tableau (sa géométrie est documentée dans `tables.css`), 30
   pour un bouton primaire dans une barre d'outils. Alignées dans une rangée de
   chrome, elles ne le sont plus — on les cale donc ICI, et ici seulement.

   ⚠ ET CETTE HAUTEUR EST CELLE D'UN ONGLET (15-09-2026, Olivier : « les
   boutons doivent être aussi gros que les autres onglets »). 28 px les rendait
   toutes égales entre elles, ce qui répondait au « trois hauteurs » — mais pas
   au voisinage : sur `/clients`, `/settings` et les autres pages à bande, la
   barre du haut porte des onglets de 45,5 px, et la rangée de `/tickets`
   restait deux fois plus menue pour la même place dans le même chrome.

   La valeur n'est pas relevée sur une capture, elle est ÉCRITE COMME L'ONGLET
   LA FABRIQUE (`.nav-tabs--in-header .nav-tab`, navigation.css) : deux fois
   son padding vertical, plus une ligne de son corps, plus son filet bas.
   Mesuré à 45,5 px des deux côtés. Un onglet qui change de recette rend ce
   calc faux ET visible — une constante de 45.5px, elle, aurait menti en
   silence. Le corps suit aussi (`0.8125rem`), sinon des contrôles de la taille
   d'un onglet porteraient encore un texte de 11,5 px.

   ⚠ CSS SEULEMENT, ET SOUS `.tk-barre-chrome`. Le même composant est monté
   dans l'onglet Tickets d'une affaire et sur xori.app, où la barre n'est PAS
   dans le chrome : y toucher au balisage (la taille des icônes, les libellés)
   déplacerait trois écrans pour une demande qui n'en vise qu'un. */
.tk-barre-chrome {
  --tk-barre-h: calc(2 * 0.75rem + 1.5 * 0.8125rem + 2px);
}
.tk-barre-chrome .btn,
.tk-barre-chrome .ms-trigger {
  height: var(--tk-barre-h);
  font-size: 0.8125rem;
}

/* ⚠ UNE GRAPPE D'ACTIONS VIDE GARDE SES GOUTTIÈRES. `topbar.php` rend toujours
   `.app-topbar-actions` (elle porte aussi le slot de contexte d'app) : sur une
   page sans geste, c'est un `<div>` de largeur nulle qui s'intercale quand même
   entre ses voisins et leur coûte 24 px. Mesuré sur `/previsionnel` : 36 px
   entre la fenêtre de mois et la recherche au lieu des 12 voulus.
   `:empty` ne suffit PAS — le partiel rend des retours à la ligne et des
   commentaires, donc des nœuds de texte : il faut interroger les ENFANTS. */
.app-topbar--compact .app-topbar-actions:not(:has(> *)) { display: none; }

/* ⚠ CE QUI QUALIFIE NE DOIT PAS CONCURRENCER CE QU'IL QUALIFIE. Le navigateur
   de période porte son libellé en 1,1 rem / 700 — juste, au-dessus d'une grille
   où il EST le titre. Collé au `<h1>` de la page (1,25 rem / 600), il se lit
   comme un second titre : deux gras côte à côte, et l'œil ne sait plus lequel
   nomme la page. On le subordonne ici, et ici seulement — la forme du composant
   ne change pas là où il est chez lui. */
.app-topbar-aside .period-nav-title {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--text-secondary);
}
/* Le libellé garde son plancher de 160 px : il empêche les chevrons de sauter
   quand on passe de « mai » à « septembre ». Mais dans le chrome, la gouttière
   de 0,75 rem de chaque côté s'ajoute à celle de la barre — on la rend. */
.app-topbar-aside .period-nav-label { padding: 0; }
/* Le bloc de titre cesse d'absorber le mou dès qu'il a ce voisin : c'est la
   marge `auto` du voisin qui doit prendre la place, sinon les deux se la
   disputent et le logement n'atteint jamais le bord. */
.app-topbar:has(> .app-topbar-aside) .app-topbar-title { flex: 0 1 auto; }

/* ── LE TITRE COMME CHEMIN DE RETOUR (2026-09-14) ───────────────────────────
   Quand la page a un état par défaut sans onglet (« Synthèse »), le titre y
   ramène. Il ne DOIT PAS ressembler à un lien : c'est toujours le titre de la
   page, et un souligné bleu en ferait un contrôle de plus dans une barre qui
   n'en manque pas. L'affordance se donne au SURVOL, où elle est demandée. */
.app-topbar-home {
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius);
  transition: color var(--transition);
}
.app-topbar-home:hover { color: var(--primary); }

.app-topbar-h1 {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 0;
  color: var(--text);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  line-height: 1.2;
  min-width: 0;
  letter-spacing: -0.01em;
}

/* Breadcrumb chain — parents rendered as links, current page is the
   .app-topbar-text leaf. Separator (›) sits between every crumb. */
.app-topbar-crumbs {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}
.app-topbar-crumbs:empty { display: none; }
.app-topbar-crumb {
  font-weight: 500;
  color: var(--text-secondary);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color var(--transition);
}
a.app-topbar-crumb:hover { color: var(--text); }
.app-topbar-crumb-sep {
  color: var(--text-muted);
  font-weight: 400;
  flex-shrink: 0;
}

/* Subtitle — stacks below the title as a sibling of the h1 (the
   .app-topbar-title parent is flex-direction:column). Single line, ellipsis
   on overflow, muted weight so the title stays the primary anchor. */
.app-topbar-subtitle {
  margin: 2px 0 0;
  font-size: 0.8125rem;
  font-weight: 400;
  color: var(--text-muted);
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Hide the element when no text has been set (e.g. subtitle_id provided
   but JS hasn't populated it yet) — prevents an empty line nudging the
   title off-center. */
.app-topbar-subtitle:empty { display: none; }
.app-topbar-icon { display: inline-flex; align-items: center; color: var(--text-secondary); flex-shrink: 0; }
.app-topbar-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-topbar-subtitle {
  margin: 0;
  font-size: 0.8125rem;
  color: var(--text-muted, var(--text-secondary));
  line-height: 1.3;
  font-weight: 400;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.75;
}

/* La barre du haut a DEUX grappes à droite depuis le 2026-09-13, et la
   distinction n'est pas cosmétique :

     .app-topbar-actions  → ce qui appartient à la PAGE (contexte d'app,
                            actions d'en-tête). Largeur VARIABLE : mesuré sur
                            les 7 listes d'airfact, de 143 px (/products) à
                            537 px (/supplier-orders).
     .app-topbar-account  → le COMPTE (notifications, avatar). Largeur
                            CONSTANTE, sur toutes les pages de toutes les apps.

   La recherche est posée ENTRE les deux, et c'est ce qui la rend trouvable :
   ancrée sur la grappe constante, elle tombe à la même abscisse partout ;
   ancrée après le titre (sa place jusqu'ici) elle dérivait de 400 px selon le
   nombre de boutons de la page.

   Les deux grappes sont `flex-shrink: 0` : quand la place manque, ce sont le
   titre et la recherche qui cèdent — jamais un bouton, qui deviendrait
   illisible ou se ferait couper. */
.app-topbar-actions,
.app-topbar-account {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-shrink: 0;
}
/* Une grappe vide ne doit pas consommer la gouttière de la barre : sans ça,
   une page sans actions d'en-tête laisse un trou de 24 px entre le titre et
   la recherche — et en mobile, où les actions de page sont masquées, la
   grappe est vide sur TOUTES les pages. */
.app-topbar-actions:empty { display: none; }

.app-topbar-page-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
/* UNE hauteur pour tous les contrôles de texte de la barre (2026-09-13).
   `.btn` n'en déclare aucune : elle se déduit de `padding: 12px` + la hauteur
   du CONTENU, donc de la taille de l'ICÔNE — 42 px avec une icône de 16,
   40 px avec une de 14. Deux boutons voisins pouvaient donc différer de 2 px
   selon l'icône qu'on leur avait mise, ce qui n'est le choix de personne.
   40 px est aussi la hauteur du champ de recherche (`.topbar-search`), qui
   leur est adjacent : la rangée s'aligne. La cloche et l'avatar gardent leurs
   34 px — ce sont des boutons ICÔNE ronds, pas des contrôles de texte, et
   leur taille est une décision à part (mode rail, 2026-09-12). */
.app-topbar-page-actions .btn { height: 40px; }

.app-topbar-divider {
  width: 1px;
  height: 28px;
  background: var(--border);
  margin: 0 var(--space-1);
}

/* Slot générique de contexte d'app (topbar.php) — vide par défaut ; le
   CONTENU (et son éventuel popover) est stylé par l'app qui le remplit. */
.app-topbar-context {
  display: flex;
  align-items: center;
  position: relative;
}

/* Inbox bell — same chrome-strip footprint as the avatar so the two read as
   a paired set. Popover is wired by topbar-inbox.js, paints from
   /api/v1/notifications and stays in sync via the `xz-inbox-changed` event
   that the approval drawer + Xori FAB bell already broadcast. */
.app-topbar-inbox { position: relative; }

.app-topbar-iconbtn {
  position: relative;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--color-surface-2);
  color: var(--text);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  transition: background var(--transition), border-color var(--transition);
}
.app-topbar-iconbtn:hover { background: var(--border); }
.app-topbar-iconbtn[aria-expanded="true"] { border-color: var(--primary); }
.app-topbar-iconbtn svg { display: block; }

.app-topbar-inbox-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  /* css-color-ok: red-600 historique du badge non-lu (--danger n'a jamais existé, ce hex a toujours été la valeur rendue) */
  background: var(--error-hover);
  color: var(--surface);
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 16px;
  text-align: center;
  pointer-events: none;
}
/* rustine `[hidden]` RETIRÉE le 2026-09-06 : `base.css` porte la règle globale (CSS-06). */

.app-topbar-inbox-popover {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  width: 360px;
  max-width: 92vw;
  max-height: 480px;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  z-index: var(--z-popover);
  overflow: hidden;
}
/* rustine `[hidden]` RETIRÉE le 2026-09-06 : `base.css` porte la règle globale (CSS-06). */

/* On narrow phones the right-anchored absolute popover would extend off the
   left edge of the viewport. Span fixed across the viewport instead so the
   inbox stays fully visible whatever the device width. */
@media (max-width: 480px) {
  .app-topbar-inbox-popover {
    position: fixed;
    top: var(--topbar-height, 80px);
    left: var(--space-2);
    right: var(--space-2);
    width: auto;
    max-width: none;
  }
}

.app-topbar-inbox-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--border);
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--text);
}
.app-topbar-inbox-link {
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--primary);
  text-decoration: none;
}
.app-topbar-inbox-link:hover { text-decoration: underline; }

.app-topbar-inbox-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: var(--space-1);
}
.app-topbar-inbox-loading,
.app-topbar-inbox-empty,
.app-topbar-inbox-error {
  padding: var(--space-4) var(--space-3);
  text-align: center;
  color: var(--text-secondary);
  font-size: 0.875rem;
}

.app-topbar-inbox-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--text);
}
.app-topbar-inbox-row:hover { background: var(--color-surface-2); }
.app-topbar-inbox-row.unread { background: var(--primary-light); }
.app-topbar-inbox-row.unread:hover { background: var(--primary-light); filter: brightness(0.97); }
.app-topbar-inbox-row.requires-action { border-left: 3px solid var(--warning); }

.app-topbar-inbox-avatar {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--color-surface-2);
  border: 1px solid var(--border);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.app-topbar-inbox-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.app-topbar-inbox-title {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.app-topbar-inbox-body-line {
  font-size: 0.8125rem;
  color: var(--text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.app-topbar-inbox-time {
  font-size: 0.75rem;
  color: var(--text-secondary);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.app-topbar-inbox-cta {
  color: var(--warning);
  font-weight: 500;
}

/* User avatar + popover */
.app-topbar-user { position: relative; }

.app-topbar-avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--color-surface-2);
  color: var(--text);
  font-weight: 600;
  font-size: 0.8125rem;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  transition: background var(--transition), border-color var(--transition);
}
.app-topbar-avatar:hover { background: var(--border); }
.app-topbar-avatar[aria-expanded="true"] { border-color: var(--primary); }

.app-topbar-user-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 280px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: var(--space-1);
  z-index: var(--z-popover);
}

/* Apps grid inside the user menu — replaces the old sidebar app launcher.
   Compact tile row that wraps to a second row past 4 apps. Tracks are
   minmax(max-content, 1fr) so the widest nowrap label sets the column
   width (the popover grows past its min-width) — names never crop. */
.app-topbar-user-apps {
  display: grid;
  grid-template-columns: repeat(4, minmax(max-content, 1fr));
  gap: var(--space-2);
  padding: var(--space-3);
}
.app-topbar-user-app {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  text-decoration: none;
  transition: background var(--transition), border-color var(--transition), transform var(--transition);
}
.app-topbar-user-app:hover {
  border-color: var(--primary);
  transform: translateY(-1px);
}
.app-topbar-user-app.is-current {
  border-color: var(--primary);
  background: var(--primary-light);
  cursor: default;
  pointer-events: none;
}
.app-topbar-user-app img {
  height: 28px;
  width: auto;
  max-width: 100%;
  object-fit: contain;
}
.app-topbar-user-app-fallback {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 28px;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--text-secondary);
}
.app-topbar-user-app-label {
  font-size: 0.6875rem;
  font-weight: 500;
  line-height: 1.2;
  color: var(--text-secondary);
  white-space: nowrap;
}
.app-topbar-user-app.is-current .app-topbar-user-app-label { color: var(--primary); }

.app-topbar-user-divider {
  height: 1px;
  background: var(--border);
  margin: var(--space-1) calc(-1 * var(--space-1));
}
/* ⚠ JAMAIS DEUX TRAITS DE SUITE. `.app-topbar-user-info` porte DÉJÀ un
   `border-bottom` : un filet posé juste après en trace un second, à deux pixels
   du premier. Ça ne se voyait pas tant que la grille d'apps s'intercalait entre
   les deux ; elle est sortie du menu le 2026-09-14 et le double trait est
   apparu — sur le menu du compte comme sur celui de l'organisation.
   La règle vaut mieux qu'un `if` dans chaque gabarit : elle tient quel que soit
   l'ordre des sections, et pour les menus à venir. */
.app-topbar-user-info + .app-topbar-user-divider { display: none; }

.app-topbar-user-links { padding: var(--space-1) 0; }
.app-topbar-user-favicon { border-radius: var(--radius-sm); }
.app-topbar-user-link-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1 1 auto;
  min-width: 0;
}

/* Cross-app transition overlay — same Corelia pulse animation used on the
   login-pending and logout views, so going from one app to another feels
   like the same family of moments. Hidden by default; topbar JS toggles it
   on app-tile clicks. The next app's first paint replaces it naturally. */
.app-transition-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  z-index: var(--z-float);
}
/* rustine `[hidden]` RETIRÉE le 2026-09-06 : `base.css` porte la règle globale (CSS-06). */
.app-transition-mark {
  display: block;
  width: 160px;
  height: 160px;
  /* object-fit:contain guards against any non-square viewBox surprises in
     the SVG — the logo is preserved in its native aspect ratio inside the
     square box rather than being stretched to fill. */
  object-fit: contain;
  animation: app-transition-pulse 1.1s ease-in-out infinite;
  transform-origin: center;
  will-change: transform, filter;
}
@keyframes app-transition-pulse {
  0%, 100% { transform: scale(0.94); filter: drop-shadow(0 4px 12px rgba(25,49,83,0.08)); }
  50%      { transform: scale(1.0);  filter: drop-shadow(0 10px 28px rgba(25,49,83,0.22)); }
}
@media (prefers-reduced-motion: reduce) {
  .app-transition-mark { animation: none; }
}

.app-topbar-user-info {
  padding: var(--space-3);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-1);
}
.app-topbar-user-name { font-weight: 600; font-size: 0.875rem; color: var(--text); }
.app-topbar-user-email { font-size: 0.75rem; color: var(--text-secondary); margin-top: 2px; word-break: break-all; }

.app-topbar-user-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-2) var(--space-3);
  background: transparent;
  border: none;
  border-radius: var(--radius);
  font-size: 0.875rem;
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
  text-align: left;
}
.app-topbar-user-item:hover { background: var(--color-surface-2); }
.app-topbar-user-item-danger:hover { background: rgba(239,68,68,0.08); color: var(--error); }
/* ⚠ `flex: none` — SANS LUI L'ICÔNE SE FAIT ÉCRASER. `.app-topbar-user-item` est
   une rangée flex, et un enfant y est rétrécissable par défaut : à côté d'un
   libellé long (« Longages Menuiseries »), le `<svg>` perdait de la largeur sans
   en perdre en hauteur — toutes les icônes du menu rendaient un glyphe déformé.
   Constat Olivier le 2026-09-14, sur le menu d'organisation ; le défaut existait
   déjà pour les liens partagés du dossier, dont les libellés sont aussi longs. */
.app-topbar-user-icon { display: inline-flex; align-items: center; flex: none; color: var(--text-secondary); }
/* Et c'est le LIBELLÉ qui se coupe, jamais l'icône : `min-width: 0` parce qu'un
   enfant de flex refuse de rétrécir sous son contenu sans lui. */
.app-topbar-user-item > .app-topbar-user-texte {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.app-topbar-user-item-danger:hover .app-topbar-user-icon { color: var(--error); }

/* Content Layout Patterns */
.content-layout {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  width: 100%;
}

.content-layout.cards {
  align-items: flex-start;
  justify-content: flex-start;
}

.content-layout.rows {
  flex-direction: column;
}

.content-layout.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-8);
}

/* Card Layout Constraints */
.content-layout.cards > .card {
  flex: 0 0 auto;
  width: 100%;
  min-width: 320px;
  max-width: 480px;
}

.cards-constrained-sm > .card { max-width: 400px; }
.cards-constrained > .card    { max-width: 480px; }
.cards-constrained-lg > .card { max-width: 640px; }

/* Page Structure */
.page-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: var(--space-6);
}

.page-header-content {
  flex: 1;
}

.page-header-content h1 {
  font-size: 1.875rem;
  font-weight: 700;
  margin: 0 0 var(--space-2) 0;
  color: var(--text);
  line-height: 1.2;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.page-header-subtitle,
.header-subtitle {
  color: var(--text-secondary);
  font-size: 1rem;
  margin: 0;
  line-height: 1.4;
}

.page-header-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  flex-shrink: 0;
  margin-left: var(--space-8);
}

/* Section Organization */
.section-group {
  margin-bottom: var(--space-10);
}

.section-header {
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-3);
  border-bottom: 2px solid var(--border);
}

.section-title {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--text);
  margin: 0;
}

/* Panel Title — inline heading for sub-panels, drawers, modal sections.
   Used by section_header() / UIComponents::sectionHeader().
   Smaller than .section-title (which is for top-level page sections) and
   independent of .card-title (which is card-bound). */
.panel-title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
  color: var(--text);
  margin: 0 0 var(--space-3);
  font-size: 0.9rem;
}

.panel-title > svg {
  flex-shrink: 0;
}

.panel-title-sm { font-size: 0.8125rem; }
.panel-title-lg { font-size: 1rem; }
.panel-title-xl { font-size: 1.125rem; }

/* Right-aligned action slot (badge, button) inside a .panel-title */
.panel-title-actions {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

/* Vertical rhythm for a section_header (.panel-title) used as a TOP-LEVEL
   page heading sitting directly above a full-width table/card — the common
   "report page" shape (état de rapprochement, grand livre, balance, TVA…).
   .panel-title carries no top margin by design (its in-card / modal use sits
   flush under .card-header), so a bare section_header collided with the
   preceding table and the gaps read as inconsistent. Scoped to the sibling
   adjacency so in-card (.card-header .panel-title) and modal headings are
   untouched; margins collapse with the previous element's margin-bottom so
   the spacing never doubles. */
.table-container + .panel-title,
.dash-card + .panel-title,
.dash-grid + .panel-title,
.dash-stats-row + .panel-title {
  margin-top: var(--space-8);
}

/* NOTE: .stat-card (no prefix) is deliberately NOT defined here. The shared
   dashboard pattern is .dash-stat-card / .dash-stats-row / .dash-grid — see
   dashboard.css (rendered by stat_card() / window.statCard()).
   History: trade's fork was deleted 2026-04-29 (Phase 0f), build's
   (project-styles.css) was migrated to stat_card() on 2026-08-27 — no app
   defines an unprefixed .stat-card anymore, and none should again. */

/* Auth Content Layout (login / register / forgot-password pages) */
.auth-content-layout {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  width: 100%;
}

.auth-card-logo {
  display: flex;
  justify-content: center;
  margin-bottom: 1.5rem;
}

.auth-card {
  background: white;
  border-radius: 12px;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
  padding: 2rem;
  width: 100%;
  max-width: 480px;
}

/* Embed layout (iframe mode — no sidebar, no chrome) */
body.embed-layout {
  background: var(--bg);
  overflow-x: hidden;
}

body.embed-layout #app {
  display: block;
}

.embed-content {
  padding: 1rem 1.5rem;
  max-width: 100%;
}

/* Mobile Responsive */
@media (max-width: 768px) {
  .app-topbar {
    left: 0;
    /* On mobile, content padding shrinks to chrome-pad-x-self (16px) for
       both the topbar and main-content — they share the same horizontal
       grid as the sidebar's column padding. */
    padding: var(--chrome-pad-y) var(--chrome-pad-x-self);
  }
  .app-topbar::after {
    left: var(--chrome-pad-x-self);
    right: var(--chrome-pad-x-self);
  }
  .app-topbar-hamburger { display: inline-flex; }
  /* On mobile we trade visible page actions for screen real estate — they
     stay accessible inside the page body if the page also renders them. */
  .app-topbar-page-actions,
  .app-topbar-divider { display: none; }
  /* La grappe d'actions devient alors VIDE DE CONTENU VISIBLE — mais pas
     `:empty` : le <div> des actions de page est toujours là, seulement masqué.
     Elle garderait donc ses deux gouttières de 24 px, prises sur le titre.
     On la retire, SAUF si elle porte le contexte d'app (le slot générique :
     l'exercice comptable de bal, par exemple), qui n'est pas sacrifié en
     mobile et doit rester atteignable. */
  .app-topbar-actions:not(:has(.app-topbar-context)) { display: none; }

  /* Topbar search — contrairement aux page-actions, la recherche n'est PAS
     sacrifiée en mobile (c'est un verbe de contenu, pas une action
     secondaire) : elle passe en 2e rangée pleine largeur. La topbar étant
     fixed avec height:var(--topbar-height), la classe body réhausse la var
     — lue aussi par le padding-top de .main-content, donc pas de
     chevauchement. 132px = 80 (rangée 1 inchangée) + 44 (input) + 8. */
  body.topbar-has-search { --topbar-height: 132px; }

  /* Les onglets prennent une rangée de plus, et la bande défile
     horizontalement (règle `.nav-tabs-container` de responsive.css). Les deux
     classes se CUMULENT : une page à onglets qui porte aussi la recherche paie
     les deux rangées. Écrire les trois cas séparément plutôt qu'un `calc()`
     sur le jeton — `--topbar-height` est lu par `.main-content`, un `calc()`
     qui se référencerait lui-même ne se résout pas. */
  /* ⚠ 104 ET 136 SONT MESURÉS (banc, montages `Nauto@390` et `Mauto@390`).
     Mes deux premiers nombres — 126 et 178, obtenus en additionnant le jeton
     mobile existant et la hauteur d'une bande — sur-payaient de 22 et 42 px.
     Un jeton trop GRAND ne se voit pas : il ne casse rien, il pousse juste le
     contenu de 42 px sous une barre à moitié vide, sur l'écran où la place est
     la plus chère. Le banc borne donc des deux côtés.

     ⚠ UN SEUL JETON EST RÉÉCRIT ICI. Le cas « onglets seuls » mesure 104 px en
     mobile comme sur le bureau — la rangée 1 y perd la grappe d'actions
     (masquée) mais gagne le sous-titre, et les deux se compensent. Le redéclarer
     à l'identique ferait croire à une valeur mobile PROPRE, qu'il faudrait
     ensuite tenir à deux endroits. Seul le cumul avec la recherche change. */
  /* ⚠ 132,5 px, ET LA DÉCIMALE EST VOULUE. La hauteur naturelle de ce cumul
     vaut 132,52 ; écrit à 136, le jeton était 3,5 px trop grand, le garde-fou
     JS le réécrivait à chaque chargement et le contenu remontait APRÈS la
     peinture. Un flash, en plus petit. Arrondir au pixel supérieur « pour faire
     propre » revient à racheter ce défaut. */
  body.topbar-has-tabs.topbar-has-search { --topbar-height: 132.5px; }
  body.topbar-has-tabs .app-topbar { flex-wrap: wrap; align-content: flex-start; }

  /* ⚠ EN MOBILE, UNE BARRE VIDE RESTE — RÉDUITE, PAS SUPPRIMÉE. Elle porte le
     hamburger, seul moyen d'ouvrir la barre latérale : et depuis le 2026-09-14
     c'est aussi le seul chemin vers les notifications, l'organisation et le
     lanceur d'apps. La replier ici enfermerait l'utilisateur dans sa page.
     Sans titre ni gestes, la hauteur tombe à la cible du bouton. */
  body.topbar-empty { --topbar-height: 56px; }
  /* ⚠ ET ON LA RÉAFFICHE : la règle de bureau la masque
     (`body.topbar-empty .app-topbar { display: none }`), et une hauteur de
     56 px sur une barre masquée ne rendrait qu'une gouttière vide — le
     hamburger, lui, aurait disparu. */
  body.topbar-empty .app-topbar { display: flex; }
  body.topbar-has-search .app-topbar { flex-wrap: wrap; align-content: flex-start; }
  /* DEUX rangées, pas trois. `flex-wrap` décide du repli sur les tailles
     HYPOTHÉTIQUES des items, jamais sur leurs tailles rétrécies : avec
     `flex: 1 1 auto`, la base du bloc de titre est son CONTENU (~300 px à
     390 px de fenêtre), donc hamburger + titre + compte dépassaient la ligne
     et le bloc compte partait à la ligne AVANT que le titre n'ait rétréci
     d'un pixel. La recherche, en `order: 10`, se retrouvait en 3e rangée —
     hors de la barre, qui est à hauteur FIXE : mesuré, elle finissait à
     y=173 pour une barre qui s'arrête à 132, par-dessus le contenu.
     Base nulle ⇒ taille hypothétique nulle ⇒ plus de repli, le titre prend
     ce qui reste et s'ellipse. ⚠ Sur le BUREAU la même déclaration est
     NUISIBLE (mesuré : titre à 139 px au lieu de 208 à 1280) — elle est donc
     bornée à la media query, volontairement. */
  .app-topbar-title { flex: 1 1 0; }
  /* `flex` porte DEUX classes ici aussi : une media query n'ajoute aucune
     spécificité, donc la règle de bureau (0,2,0) écraserait un `flex` écrit
     en (0,1,0) — la 2e rangée mobile serait restée large de 320 px. */
  .app-topbar-search {
    order: 10;
    margin-top: calc(-1 * var(--space-2));
  }
  .app-topbar-search.topbar-search { flex: 1 1 100%; }

  .main-content {
    margin-left: 0;
    padding-top: calc(var(--topbar-height, 80px) + var(--space-4));
    width: 100%;
    padding-left: var(--chrome-pad-x-self);
    padding-right: var(--chrome-pad-x-self);
  }

  .content-layout.cards {
    flex-direction: column;
  }

  .content-layout.cards > .card {
    max-width: none;
    width: 100%;
  }

  .content-layout.split {
    grid-template-columns: 1fr;
  }

  .page-header {
    flex-direction: column;
    gap: var(--space-4);
    text-align: left;
    padding: var(--space-6);
  }

  .page-header-actions {
    width: 100%;
    margin-left: 0;
  }
}
