
/* ---------------------------------------------------------------
   Manrope, et elle seule pour les titres.

   Une police d'affichage donne un visage a l'application la ou on la
   regarde longtemps : le titre de page, le titre d'une carte, le nom
   sur l'ecran de lancement. Elle ne descend jamais sur les chiffres.
   Ce n'est pas une preference : toute la feuille repose sur
   `tabular-nums` pour que les colonnes de montants s'alignent, et une
   police d'affichage qui n'embarque pas de chiffres tabulaires ferait
   danser la largeur d'un total a chaque frappe. Les montants gardent
   donc la police du systeme, declaree sur `body`.

   Auto-hebergee, sous-ensemble latin, 24,3 Ko. Le fichier vient de
   Google Fonts mais n'est pas servi par lui : un `@import` vers
   fonts.googleapis.com annoncerait chaque visite du tableau de bord a
   un tiers, et l'application est fermee par defaut.

   Le sous-ensemble latin porte U+0000-00FF (tous les accents francais
   et les guillemets obliques), U+2000-206F (l'apostrophe typographique)
   et U+20AC (l'euro). Un glyphe absent, comme le ⤒ des releves, tombe
   sur la police du systeme, caractere par caractere : rien ne casse.

   Aucun `?v=` sur ce fichier, volontairement. Le lien de prechargement
   d'`index.html` et l'`url()` ci-dessous doivent coincider a l'octet
   pres, sinon le navigateur telecharge la police deux fois. Une police
   ne change pas : le jour ou elle changerait, elle changerait de nom.
   --------------------------------------------------------------- */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 500 800;
  /* `swap` et non `optional` : avec le prechargement, la police est la
     avant le premier rendu dans la quasi-totalite des cas. `optional`
     laisserait le navigateur l'ignorer purement et simplement sur une
     connexion lente, et le titre resterait en police systeme toute la
     session — une identite qui n'apparait qu'une fois sur deux n'en
     est pas une. */
  font-display: swap;
  src: url(manrope-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
                 U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
                 U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF,
                 U+FFFD;
}

:root {
  color-scheme: light;
  --surface-1: #ffffff;
  --surface-2: #f1f4f8;
  --page: #f7f9fb;
  --sidebar: #ffffff;
  --sidebar-ink: #0d1117;
  --sidebar-muted: #55606e;
  --sidebar-faible: #8a94a3;
  --sidebar-hover: rgba(13, 17, 23, .05);
  --sidebar-active: rgba(13, 17, 23, .08);
  --sidebar-border: rgba(13, 17, 23, .10);
  --text-primary: #0d1117;
  --text-secondary: #4d5866;
  /* Le troisieme gris porte le texte le plus petit de l'application : intitules
     de colonnes a 11 px, bulles d'aide a 12, etiquettes de tuiles a 10,5. Il
     doit donc franchir 4,5:1 sur les trois fonds, et non seulement sur la page.
     #7b8794 donnait 3,66:1 sur une carte blanche et 3,32 sur `--surface-2` :
     sous le seuil la ou les caracteres sont les plus fins. #66707d rend 5,03,
     4,76 et 4,56, et garde un ecart net avec `--text-secondary`, a 7,23. */
  --muted: #66707d;
  --grid: #e4e9ef;
  --axis: #c3ccd6;
  --border: rgba(13, 17, 23, .09);
  --border-strong: rgba(13, 17, 23, .18);

  --series-1: #2173c6;
  --series-2: #048681;
  --series-3: #b8417e;
  --series-4: #9a539d;
  --series-5: #017ca7;
  --series-6: #818643;
  --series-7: #028293;
  --series-8: #55873b;
  --series-9: #505ea8;
  --series-10: #4a6d8c;
  --series-11: #5f8722;
  --series-12: #6b6976;

  --good: #0a8f5f;
  --good-text: #06724c;
  --warning: #b07d00;
  /* L'orange sert de texte, et pas seulement d'aplat : les ecarts au budget
     s'ecrivent avec, a 12 et 13 px. #c25f2b ne rendait que 4,23:1 sur blanc et
     3,83 sur `--surface-2`, sous le seuil aux deux endroits ou il s'affiche.
     #ab5022 rend 5,38, 5,10 et 4,88, et reste un orange : il ne glisse pas vers
     le rouge, qui a son propre sens dans cette application. */
  --serious: #ab5022;
  --critical: #cf3a3a;

  /* La zone de gestes du telephone, nommee une fois et declaree ici, hors de
     toute requete media : une variable definie sous un point de rupture ne
     vaudrait rien pour les regles qui vivent sous un autre.

     C'est `env(safe-area-inset-bottom)` : 34 px sur un iPhone a barre de gestes,
     zero partout ailleurs. Passer par une variable plutot que d'appeler `env()`
     a chaque endroit sert a la verifier. `env()` ne se simule pas dans un
     navigateur de bureau ; une variable se force le temps d'un controle, et la
     geometrie de l'iPhone devient mesurable sans iPhone. */
  --zone-geste: env(safe-area-inset-bottom, 0px);

  /* Les deux bandes qui restent en place quand on defile, sur telephone : la
     barre du haut, puis les sous-onglets quand la page en a.

     Un collant qui se pose sous un autre a besoin de connaitre sa hauteur. Elle
     ne se devine pas, donc elle se declare : `--h-sous-onglets` commande la
     hauteur de la barre autant que le decalage du bandeau, et les deux ne
     peuvent plus se desaccorder. */
  --h-barre-dessin: 54px;
  --h-barre: calc(var(--h-barre-dessin) + env(safe-area-inset-top, 0px));
  --h-sous-onglets: 53px;

  /* Une troisieme variable a vecu ici, `--h-colle`, qui portait la hauteur
     reellement clouee en haut selon que la barre etait la ou retiree. Les collants
     s'y accrochaient par leur `top`. Elle est partie avec ce mecanisme : animer le
     `top` d'un element collant fait recalculer sa contrainte de collage a chaque
     image, et le mouvement accrochait. Le collage est desormais fixe et c'est une
     translation qui deplace ces bandes, donc plus rien a faire varier. */

  /* L'accent de l'interface : le violet du logo. Tout ce qui dit « actif,
     survole, selectionne » le porte — le repere du menu, l'onglet courant, la
     pastille de plage retenue, les anneaux de focus, la selection de texte.

     Il ne rejoint pas les series de donnees, et c'est le point : `--series-1`
     peignait a la fois les liquidites et chaque etat actif de l'application.
     Une couleur qui sert de marque et de classe d'actif ne veut plus rien dire,
     et l'interface entiere prenait l'air d'un gabarit dont l'accent est le bleu
     le plus consensuel qui existe, pendant que la marque etait violette. */
  --accent: #6d3fc4;

  /* Le degrade du budget, defini une fois. La jauge de l'accueil le portait en
     dur dans son CSS pendant que les barres par categorie tiraient sur un
     jaune sans rapport : deux ecrans du meme sujet, deux couleurs. Les deux le
     lisent ici, donc ils ne peuvent plus diverger.

     Une seule declaration suffit pour les deux themes : un `var()` dans une
     variable se resout chez qui l'utilise, donc `--series-1` y prend la teinte
     du theme actif. */
  --degrade-budget: linear-gradient(90deg, var(--series-1), var(--series-3));

  --radius: 14px;        /* cartes, fenetres, tuiles */
  --radius-sm: 9px;      /* boutons, champs, controles */
  --radius-xs: 6px;      /* badges, petites pastilles carrees */
  --radius-pill: 999px;  /* pilules : onglets, jauges, etiquettes rondes */
  /* Trois niveaux d'ombre, et pas un par surface. `--shadow` pose (cartes),
     `--ombre-flottante` detache ce qui survole la page (menus, infobulles,
     panneau de notifications), `--ombre-fenetre` porte ce qui la recouvre
     (fenetres, tiroirs). Avant : six ombres noires ecrites en dur, les memes
     dans les deux themes — un voile a 45 % de noir sous un menu en theme
     clair pese trois fois trop lourd, c'est lui qui faisait « sombre colle
     sur clair ». Chaque theme regle desormais sa densite. */
  --shadow: 0 1px 2px rgba(13, 17, 23, .04);
  --ombre-flottante: 0 8px 24px rgba(13, 17, 23, .10);
  --ombre-fenetre: 0 20px 50px rgba(13, 17, 23, .16);
  --ombre-ovale: 0 -14px 34px rgba(13, 17, 23, .10);
  --fw-chiffre: 650;
  /*    La police du systeme d'abord, Inter ensuite. Inter etait demandee en
   premier. Elle est excellente et elle est aussi la police par defaut de tous
   les gabarits web : c'est le premier signal qui fait dire d'une application
   qu'elle sort d'un modele. Avec `system-ui` en tete, elle porte le caractere
   de l'appareil : SF Pro sur iPhone et Mac, Segoe UI Variable sur Windows,
   Roboto sur Android. C'est ce qui fait qu'une application a l'air
   d'appartenir au telephone plutot qu'au web. Inter reste en repli pour les
   systemes sans police d'interface.*/
  --font: system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI",
          Roboto, "Inter", ui-sans-serif, sans-serif;
  --font-titre: 'Manrope', var(--font);
  --font-nb: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --surface-1: #101114;
  --surface-2: #181a1f;
  --page: #08090b;
  --sidebar: #050506;
  --sidebar-ink: #f2f3f5;
  --sidebar-muted: #9a9ca3;
  --sidebar-faible: #5c5e66;
  --sidebar-hover: rgba(255, 255, 255, .05);
  --sidebar-active: rgba(255, 255, 255, .08);
  --sidebar-border: rgba(255, 255, 255, .07);
  --text-primary: #f2f3f5;
  --text-secondary: #9a9ca3;
  /* Meme regle qu'en clair, meme faute corrigee : #6a6c74 rendait 3,61:1 sur une
     carte et 3,32 sur `--surface-2`, aux tailles ou les caracteres sont les plus
     fins. #83858e rend 5,13, 5,42 et 4,73, et reste sous `--text-secondary`
     (6,88) : la hierarchie des trois gris tient toujours. */
  --muted: #83858e;
  --grid: #1e2025;
  --axis: #2c2f36;
  --border: rgba(255, 255, 255, .045);
  --border-strong: rgba(255, 255, 255, .11);

  --accent: #9a72e8;

  --series-1:  #65a7f2;   /* liquidités, monétaire      — azur */
  --series-2:  #35c5be;   /* actions, actifs de marché  — turquoise */
  --series-3:  #e06da4;   /* non coté                   — rose */
  --series-4:  #ca85cb;   /* immobilier, immobilier coté — mauve */
  --series-5:  #46b1e3;   /* crypto                     — bleu ciel */
  --series-6:  #b2b867;   /* métaux précieux            — or olive */
  --series-7:  #3fbbd0;   /* obligations                — cyan */
  --series-8:  #8ebe77;   /* multi-actifs               — vert */
  --series-9:  #8d9be2;   /* biens de valeur */
  --series-10: #6b8ca8;
  --series-11: #8fbf5a;
  --series-12: #9c9aa6;   /* et la teinte des classes sans attribution */

  --good: #26a17b;
  --good-text: #3ec49b;
  --warning: #d0a215;
  --serious: #e08048;
  --critical: #e0574f;

  --shadow: 0 1px 2px rgba(0, 0, 0, .5);
  --ombre-flottante: 0 8px 24px rgba(0, 0, 0, .45);
  --ombre-fenetre: 0 24px 60px rgba(0, 0, 0, .5);
  --ombre-ovale: 0 -14px 34px rgba(0, 0, 0, .3);
}

/* ---------------------------------------------------------------
   Le survol vit sous `@media (hover: hover)`, sans exception.

   Le correctif du 5 aout avait pose ce garde-fou sur une seule regle, celle du
   logo, parce qu'un seul symptome avait ete signale. Les soixante-neuf autres
   avaient le meme defaut et l'ont montre a la premiere occasion. Une regle
   ecrite pour un cas s'applique a tous ceux qui lui ressemblent : c'est la
   lecon que ce fichier note deja pour les bandeaux d'etablissement.

   Chaque bloc est enveloppe **sur place**. Cette feuille se joue a l'ordre
   autant qu'a la specificite — voir `.table-serree`, dont la regle doit rester
   apres `.data-view > table` — et regrouper les survols en fin de fichier
   aurait change qui gagne. Une requete media n'ajoute aucune specificite et ne
   deplace rien.

   Un selecteur qui melangeait survol et focus a ete coupe en deux : le clavier
   ne perd pas son indication en meme temps que le doigt.

   Un test l'exige desormais, et il se derive de la feuille : aucune occurrence
   de `:hover` ne peut vivre hors d'un de ces blocs. L'appui tactile, lui, garde
   ses `:active` et ses classes `.tape`, qui eux se levent. */
* { box-sizing: border-box; }

/* Le fond est porte par les deux : sur iPhone, la zone sous la barre
   d'onglets — encoche du bas, ou l'espace laisse par la barre de Safari qui
   se replie — reste parfois hors du canevas peint par `body` seul, et s'y
   affiche en noir.

   `min-height` sur le corps, et non `height`, et c'est un correctif.

   `height: 100%` plafonnait le corps a la hauteur de l'ecran quelle que soit la
   longueur de la page : 900 px pour un document de 2 062. Or le corps est la
   grille qui porte la barre laterale, donc la zone de grille de cette barre
   faisait 900 px, exactement sa propre hauteur. Un element colle n'a de course
   que dans son bloc conteneur : avec zero de marge, `position: sticky` ne
   collait rien du tout, et la barre s'en allait avec la page. Mesure a 1 280 x
   900 : `top` a -800 px pour un defilement de 800, donc pas un pixel de retenue.

   Ce n'est pas un choix qu'on aurait pu defendre. Le pied de cette barre porte
   le patrimoine net en permanence, au point que l'accueil a perdu une carte
   entiere au motif qu'« il est deja ecrit en permanence au pied de la barre
   laterale ». Un chiffre retire ailleurs parce qu'il est toujours la ne peut pas
   s'en aller au premier defilement.

   Le corps peut donc depasser l'ecran, et il ne descend jamais en dessous : le
   fond reste peint d'un bord a l'autre sur une page courte, ce qui etait la
   raison d'etre de cette ligne. La hauteur posee par `gelerFond()` est un style
   en ligne, elle gagne toujours sur celle-ci — le gel d'une fenetre ouverte
   n'est pas touche.

   Le `min-height` vit dans la regle `body` principale, plus bas, et non dans une
   seconde regle posee ici : la feuille en compte deja une seule, et un controle
   cherche « le bloc dont le selecteur est exactement body » pour verifier que
   les montants gardent la police du systeme. Une deuxieme regle du meme nom
   passait devant et lui faisait lire autre chose que ce qu'il croit lire. */
html { height: 100%; }
html, body { background: var(--page); }

/* Garde-fou : rien ne doit pouvoir élargir la fenêtre.
   Un seul élément trop large et le navigateur agrandit le viewport de mise en
   page — 375 px devenaient 446 — puis dézoome pour tout montrer. La barre
   d'onglets, posée en `fixed; bottom: 0`, se calait alors sur ce viewport
   gonflé, donc 97 px sous le bord visible : elle disparaissait en arrivant sur
   Marchés. `clip` plutôt que `hidden` : il ne crée pas de conteneur de
   défilement et ne casse donc pas les en-têtes collants. */
html, body { overflow-x: clip; max-width: 100%; }

/* Le zoom au double-tap, lui, se refuse — et seulement lui.

   `manipulation` vaut `auto` moins le zoom au double-tap. C'est celui qu'on
   declenche sans le vouloir : deux appuis rapproches sur un montant, et la page
   se recadre. Le defilement et le **pincement restent permis**, ce qui est la
   difference entre corriger un accident et confisquer un geste : un pincement se
   defait en pincant, un double-tap laisse sur place.

   Marche partout, iPhone compris, la ou le viewport n'y peut rien. Les regles qui
   posent `pan-x` ou `pan-y` sur un carrousel ou le corps d'une fenetre gagnent
   par specificite, et c'est ce qu'il faut : elles disent quelque chose de plus
   precis sur un geste precis. */
html { touch-action: manipulation; }

body {
  margin: 0;
  font-family: var(--font);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1, "cv05" 1;
  background: var(--page);
  color: var(--text-primary);
  display: grid;
  grid-template-columns: 244px 1fr;
  /* Et non `height: 100%`, qui plafonnait le corps a un ecran : cette grille est
     le bloc conteneur de la barre laterale, et une zone de grille exactement
     aussi haute que la barre ne laisse aucune course a son collage. Voir la note
     en tete de fichier, sous `html { height: 100% }`. */
  min-height: 100%;
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

/* La barre latérale ne défile pas avec la page, et elle défile en elle-même.

   Et il n'y a rien à y gagner. Si la barre du haut se retire sur téléphone,
   c'est que la place verticale y est comptée — 54 px sur 812. Ici la barre coûte
   de la largeur, jamais de la hauteur : la cacher ne rendrait pas une ligne de
   contenu.

   Mais la question désigne un vrai défaut, mesuré : dans une fenêtre de 560 px
   de haut, son contenu en fait 655. Le pied dépassait donc de 95 px, et
   `overflow: visible` sur un élément collé le rendait **inatteignable** — ni le
   patrimoine net, ni le témoin de sauvegarde, et aucun défilement ne pouvait les
   ramener. C'est le cas qu'on ne voit jamais en développant sur un grand écran.

   Elle défile donc en elle-même quand elle ne tient pas. `contain` pour que le
   geste ne se propage pas à la page une fois le bas atteint : sinon lire le pied
   de la barre ferait partir le contenu à côté. */
.sidebar {
  background: var(--sidebar);
  color: var(--sidebar-ink);
  display: flex;
  flex-direction: column;
  padding: 20px 14px;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  border-right: 1px solid var(--sidebar-border);
}

.brand {
  display: flex; gap: 11px; align-items: center; padding: 0 6px 22px;
  text-decoration: none; color: inherit; border-radius: var(--radius-sm);
}
.brand .brand-mark {
  transition: transform .18s cubic-bezier(.34, 1.28, .48, 1),
              box-shadow .18s ease, filter .18s ease;
}
/* Sous `hover: hover` seulement, et c'est un correctif.

   Sur un ecran tactile, `:hover` reste colle apres un appui jusqu'a ce qu'on
   touche ailleurs. Le halo violet restait donc allume sur le T une fois qu'on
   avait ouvert l'accueil — « quand je clique sur le T il reste en
   surbrillance ». Un survol qui ne se leve pas n'est plus un survol, c'est un
   etat.

   L'application connait deja ce garde-fou, pour la croix des notifications :
   une commande qui n'apparait qu'au pointeur n'existe pas sur telephone. Ici
   c'est l'inverse — un retour qui ne part jamais — mais c'est la meme cause et
   le meme remede.

   L'appui, lui, reste : il a une fin, celle du doigt qui se leve. */
@media (hover: hover) {
  .brand:hover .brand-mark {
    transform: translateY(-1px) scale(1.05);
    filter: brightness(1.08);
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 22%, transparent);
  }
}
/* L'appui, lui, reste — et il doit se voir.

   `:active` fait donc le travail, et il convient mieux : il finit quand le doigt
   se leve, alors qu'un `:hover` tactile attend qu'on touche ailleurs. Le
   mouvement est un enfoncement franc, pas la levee du survol — on appuie sur une
   touche, on ne la survole pas. */
.brand:active .brand-mark,
.brand.tape .brand-mark {
  transform: scale(.93);
  filter: brightness(1.12);
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 26%, transparent);
  transition-duration: .06s;
}
.brand:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
  .brand .brand-mark { transition: none; }
  @media (hover: hover) { .brand:hover .brand-mark { transform: none; } }
  .brand:active .brand-mark,
  .brand.tape .brand-mark { transform: none; }
}
.brand-vue { display: none; }
/* Le rayon suit celui du dessin, mesure sur le fichier source : 204 px de rayon
   sur une tuile de 880, soit 23 %. En pourcentage et non en pixels, parce que la
   meme image sert a 34 px dans la barre laterale et a 38 px dans le tiroir — un
   rayon fixe arrondissait deux fois differemment le meme coin.

   Et sans bordure : la nouvelle icone porte son propre liseré violet. Le
   `1px solid` d'avant existait parce que l'ancienne icone etait un carre plein
   bord a bord, qui avait besoin qu'on lui dessine un cadre. */
.brand-mark {
  width: 34px; height: 34px; border-radius: 23%; flex-shrink: 0; display: block;
  object-fit: cover;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong { font-size: 15px; letter-spacing: -.01em; }
.brand-text span { font-size: 11.5px; color: var(--sidebar-faible); }
.brand-sous b { font-weight: 600; color: var(--accent); }

.nav { display: flex; flex-direction: column; gap: 2px; }
.nav-tete { display: none; }
.nav a {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px; border-radius: var(--radius-sm);
  color: var(--sidebar-muted); text-decoration: none; font-size: 13.5px; font-weight: 500;
  transition: background .12s, color .12s;
}
.nav a .ic { width: 15px; text-align: center; opacity: .8; font-size: 12px; }
/* Les icones du menu sont des SVG au trait, comme la tabbar : les glyphes
   texte (◈ ◎ ▦) rendaient differemment selon la police du systeme et
   faisaient prototype a cote des vraies icones du bas. Meme classe `ic`,
   pour heriter des regles de largeur et de masquage existantes. */
.nav a svg.ic {
  width: 16px; height: 16px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}
.nav a { position: relative; }
@media (hover: hover) { .nav a:hover { background: var(--sidebar-hover); color: var(--sidebar-ink); } }
.nav a.active { background: var(--sidebar-active); color: var(--sidebar-ink); font-weight: 600; }
@media (hover: hover) { .nav a.active:hover { background: var(--sidebar-active); } }
.nav a.active::before {
  content: ''; position: absolute; left: 0; top: 7px; bottom: 7px; width: 2.5px;
  border-radius: 0 2px 2px 0; background: var(--accent);
}

.nav-groupe {
  margin: 14px 0 4px; padding: 0 11px;
  font-size: 10px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--sidebar-faible);
}
.nav-groupe:first-child { margin-top: 2px; }

.retour { display: none; }

.entete-actions { display: none; }

/* --- l'étiquette de démonstration ----------------------------------------
   Le mot le plus discret qui dise encore quelque chose : pas de fond plein, pas
   de couleur d'alerte, la taille d'une légende. Elle accompagne le nom de la
   page sans lui disputer la place, et elle tient à toutes les largeurs — une
   démonstration doit s'annoncer aussi sur grand écran.

   `[hidden]` explicite : la règle d'affichage qui suit l'emporterait sinon sur
   l'attribut, et l'étiquette resterait visible hors démonstration. */
.etiquette-demo {
  display: inline-block; flex: none; align-self: center;
  padding: 2px 7px; border-radius: var(--radius-pill);
  font-size: 9.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--sidebar-faible);
  border: 1px solid var(--sidebar-border);
  background: none;
  white-space: nowrap;
}
.etiquette-demo[hidden] { display: none; }

/* Sur téléphone, le tiroir « Plus » ne montre que ce que la barre du bas ne
   porte pas : deux lignes, qui n'ont pas besoin de trois en-têtes. La classe
   `dans-barre` est posée par `majOnglets()`, d'après la barre elle-même.

   Les titres restent, eux : « Réglages » dit ce qu'est Données, et sans lui
   l'entrée se lirait comme une suite du portefeuille. Seul se retire le titre
   dont toutes les entrées sont parties — `sans-suite`, posée par le même
   parcours. CSS ne sait pas dire « jusqu'au titre suivant » : `~` porte sur
   tous les frères, donc « Pilotage » aurait vu Comptes, trois lignes plus
   bas, et se serait cru encore utile. */
@media (max-width: 900px) {
  #nav a.dans-barre, #nav .nav-groupe.sans-suite { display: none; }
}

.sidebar-foot { margin-top: auto; display: flex; flex-direction: column; gap: 10px; padding-top: 18px; }
.sb-net {
  background: var(--sidebar-hover); border: 1px solid var(--sidebar-border);
  border-radius: var(--radius-sm); padding: 11px 12px; display: flex; flex-direction: column; gap: 2px;
}
/* Rien ne se surligne dans ce bloc.

   Le patrimoine net y restait allume en violet apres un clic, signale deux fois.
   Le nombre portait deja `user-select: none` et un `:focus-visible` — donc ni la
   selection de son propre texte ni le focus a la souris — et le defaut ne se
   reproduit pas dans un navigateur de bureau pilote. Restait une voie : une
   selection commencee ailleurs et tiree jusqu'ici, que `user-select: none` sur le
   seul nombre n'empeche pas de peindre dans tous les moteurs.

   La regle couvre donc le bloc entier et eteint la couleur de surbrillance a
   l'interieur. Aucune perte : ce sont trois lignes d'affichage, pas du texte
   qu'on copie, et le montant est deja masquable au clic. */
.sb-net, .sb-net * { user-select: none; -webkit-user-select: none; }
.sb-net ::selection { background: transparent; }
.sb-net::selection { background: transparent; }

.sb-label { font-size: 11px; color: var(--sidebar-faible); text-transform: uppercase; letter-spacing: .06em; }
.sb-net strong { font-size: 20px; font-weight: 700; letter-spacing: -.03em; color: var(--sidebar-ink); }
.sb-delta { font-size: 12px; color: #7d8896; }
.sb-delta.up { color: var(--good-text); }
.sb-delta.down { color: var(--critical); }
.saved { font-size: 10.5px; color: #5d6875; text-align: center; transition: color .2s; }
.saved.flash { color: var(--good); }

.burger, .nav-backdrop, .tabbar { display: none; }

.burger {
  width: 38px; height: 38px; flex-shrink: 0; padding: 0;
  border: 1px solid var(--sidebar-border); border-radius: var(--radius-sm);
  background: var(--sidebar-hover); cursor: pointer;
  place-items: center; position: relative;
}
.burger span, .burger span::before, .burger span::after {
  content: ''; display: block; position: absolute; left: 50%; width: 17px; height: 2px;
  background: var(--sidebar-ink); border-radius: 2px; transform: translateX(-50%);
  transition: transform .18s ease, opacity .18s ease;
}
.burger span { top: 50%; margin-top: -1px; }
.burger span::before { top: -6px; }
.burger span::after  { top: 6px; }
body.nav-open .burger span { background: transparent; }
body.nav-open .burger span::before { transform: translateX(-50%) translateY(6px) rotate(45deg); }
body.nav-open .burger span::after  { transform: translateX(-50%) translateY(-6px) rotate(-45deg); }

.main { min-width: 0; display: flex; flex-direction: column; }

.topbar {
  display: flex; align-items: flex-end; justify-content: space-between; gap: 16px;
  padding: 26px 30px 18px;
  border-bottom: 1px solid var(--border);
  background: var(--page);
  position: sticky; top: 0; z-index: 20;
}
.topbar h1 { margin: 0; font-size: 20px; font-weight: 680; letter-spacing: -.02em; }
.topbar .sub { margin: 3px 0 0; color: var(--muted); font-size: 12.5px; }
.topbar-actions { display: flex; gap: 8px; flex-shrink: 0; align-items: center; }

/* Pied du menu : les reglages d'affichage, a l'ecart des destinations.
   `margin-top: auto` les pousse tout en bas — d'ou le `flex: 1` sur le menu,
   sans quoi la rangee restait collee sous « Preferences ».

   L'alignement se calcule : le retrait des liens du menu moins le rembourrage
   propre du bouton, pour que le bord gauche des icones tombe sur celui du
   texte au-dessus. */
.nav { flex: 1; }
.nav-outils {
  display: flex; gap: 2px; margin-top: auto; padding-top: 16px;
  padding-left: calc(var(--nav-retrait, 11px) - var(--oeil-marge, 6px));
}
.nav-outils .btn-oeil { opacity: .6; }
#themeToggle .ic { font-size: 15px; line-height: 17px; display: block; width: 17px; text-align: center; }

.btn-oeil {
  background: none; border: none; cursor: pointer;
  color: var(--muted); opacity: .45;
  line-height: 0; padding: 6px;
  border-radius: var(--radius-xs); transition: opacity .15s, color .15s, background .15s;
}
@media (hover: hover) { .btn-oeil:hover { opacity: 1; color: var(--text-secondary); background: var(--surface-2); } }
/* L'œil de la barre du haut partage l'état allumé et l'animation de barre, sans
   partager la géométrie : il est rond, comme la cloche et le profil, et
   `.btn-rond` s'en charge. Étendre les sélecteurs existants plutôt que d'en
   recopier trois — une seconde série de règles pour le même dessin aurait
   divergé au premier retouchage. */
:is(.btn-oeil, .btn-oeil-entete)[aria-pressed="true"] { opacity: 1; color: var(--accent); }

.btn-oeil svg {
  width: 17px; height: 17px; display: block;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}

svg.oeil-masque {
  width: 1.05em; height: 1.05em; vertical-align: -.15em; display: inline-block;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
  opacity: .72;
}

:is(.btn-oeil, .btn-oeil-entete) .o-barre {
  stroke-dasharray: 22; stroke-dashoffset: 22;
  transition: stroke-dashoffset .22s ease;
}
:is(.btn-oeil, .btn-oeil-entete)[aria-pressed="true"] .o-barre { stroke-dashoffset: 0; }
:is(.btn-oeil, .btn-oeil-entete)[aria-pressed="true"] .o-pupille { opacity: 0; transition: opacity .15s; }

.barre-etat { display: flex; justify-content: flex-end; margin: -8px 0 -10px; }

.etat-cours {
  display: inline-flex; align-items: center; gap: 7px;
  background: none; border: none; cursor: pointer;
  padding: 6px 9px; border-radius: var(--radius-sm);
  font-size: 12px; font-weight: 500; letter-spacing: .01em;
  color: var(--muted);
  transition: color .15s, background .15s;
}
@media (hover: hover) { .etat-cours:hover:not(:disabled) { color: var(--text-secondary); background: var(--surface-2); } }
.etat-cours:disabled { cursor: default; }

.etat-cours .pt {
  width: 6px; height: 6px; border-radius: 50%; flex: none;
  background: var(--muted);
}
.etat-cours.frais .pt { background: var(--pos, var(--series-2)); }
.etat-cours.tiede .pt { background: var(--warn, #d9a441); }
.etat-cours.hs .pt { background: var(--neg, #d4574e); opacity: .7; }
.etat-cours.encours .pt { animation: pouls 1s ease-in-out infinite; }
@keyframes pouls { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }

.etat-maj { font-size: 13px; line-height: 1; opacity: .45; transition: opacity .15s; }
@media (hover: hover) { .etat-cours:hover .etat-maj { opacity: 1; } }
.etat-cours.tiede .etat-maj, .etat-cours.hs .etat-maj { opacity: .9; }
.etat-cours.encours .etat-maj { animation: tourne .9s linear infinite; opacity: 1; }
@keyframes tourne { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .etat-cours.encours .etat-maj { animation: none; } }

/* Les champs de saisie portent des nombres bruts : ils ne passent pas par
   le formatage, donc le masque doit les couvrir en CSS. Les points de
   `text-security` gardent la largeur du champ, la valeur reste modifiable. */
body.discret input[type="number"],
body.discret input.acct-nom + input,
body.discret .acct-val {
  -webkit-text-security: disc; text-security: disc;
  color: var(--muted);
}

/* Le contenu s'arrete de grandir, la page continue de se centrer.

   Sur un ecran de 2 000 px, la colonne de contenu en faisait 1 750, et une
   courbe de patrimoine a douze points s'y etirait sur toute la largeur : des
   pixels depenses a eloigner les reperes les uns des autres, pas a montrer
   davantage. Les cartes a deux colonnes devenaient deux bandes, et l'oeil
   parcourait 1 700 px pour aller d'un intitule a son montant.

   1 480 px : de quoi tenir une grille a trois colonnes confortable — le seuil
   au-dela duquel `g-3` cesse de gagner quelque chose — sans imposer de marge
   sur les 1 440 et 1 512 px courants. En dessous, `margin: 0 auto` ne fait
   rien ; au-dela, il centre. */
.view {
  padding: 22px 30px 64px; display: flex; flex-direction: column; gap: 18px;
  max-width: 1480px; margin: 0 auto; width: 100%;
}

/* Les titres portent Manrope. La regle vise les elements, pas une liste de
   classes : `.topbar h1`, `.card-head h2`, `.modal-head h2`, `.acct-head h3`
   auraient fait quatre selecteurs a tenir a jour, et le titre ajoute demain
   serait reste en police systeme sans que rien ne le signale. Un titre est
   un titre parce qu'il est un `h`, pas parce qu'on a pense a l'inscrire.

   Les deux ecritures du nom s'ajoutent explicitement — c'est meme la raison
   d'etre de cette police. L'ecran de lancement le pose en grand une seconde ;
   la barre laterale le porte en permanence, et c'est celui-la qu'on voit. Les
   deux, ou aucun : une marque qui change de police d'un ecran a l'autre n'en
   est plus une.

   Rien d'autre. Aucun de ces elements ne porte de montant — verifie : les
   seules valeurs interpolees dans un titre sont un millesime (« Objectif a
   fin 2026 ») et un rang (« Allocation 2 »), qui ne s'alignent en colonne
   nulle part. Le jour ou un titre afficherait un total, il faudrait le
   sortir d'ici ou verifier que Manrope porte bien `tnum`. */
h1, h2, h3,
.lancement-marque span,
.brand-text strong {
  font-family: var(--font-titre);
}

.t-value, .hero-value, .acct-value, .jour-eur, .dep-somme,
td, th, .sb-net strong {
  font-variant-numeric: tabular-nums;
}
.t-value { letter-spacing: -.02em; }

/* Safari iOS peint le texte des <button> en bleu, comme un lien, et le fait
   par `-webkit-text-fill-color` — qui l'emporte sur `color`. Depuis que les
   tuiles et les cartes sont cliquables, tous les montants qu'elles portent
   virraient au bleu sur iPhone, alors qu'ils restaient normaux ailleurs.
   On rend donc la couleur du remplissage a `color`, et on coupe l'habillage
   natif que Safari applique par-dessus notre propre style. */
button, [role="button"] {
  -webkit-text-fill-color: currentColor;
  -webkit-appearance: none; appearance: none;
  -webkit-tap-highlight-color: transparent;
}

/* Ceinture et bretelles avec le `format-detection` de l'en-tete : si Safari
   enveloppe malgre tout un montant dans un lien telephonique, celui-ci ne
   doit pas se distinguer du texte autour. */
a[href^="tel:"], a[x-apple-data-detectors] {
  color: inherit !important; -webkit-text-fill-color: currentColor !important;
  text-decoration: none !important; font: inherit !important;
  pointer-events: none;
}

.btn {
  font: inherit; font-size: 13px; font-weight: 550;
  padding: 8px 14px; border-radius: var(--radius-sm);
  border: 1px solid transparent; background: var(--text-primary); color: var(--page);
  cursor: pointer; transition: opacity .12s, background .12s;
  text-decoration: none; display: inline-flex; align-items: center; justify-content: center;
}
@media (hover: hover) { .btn:hover { opacity: .85; } }
/* Le meme anneau que les vingt-cinq autres composants focalisables de
   l'application. Sans lui, le bouton s'en remettait a l'anneau par defaut du
   navigateur : la moitie de l'interface disait « tu es ici » en violet, l'autre
   moitie dans le dialecte du systeme, et un bouton plein clair sur fond clair
   n'y montrait presque rien. `outline-offset` positif : l'anneau se pose autour
   du bouton, pas dedans, ou il rognerait le libelle. */
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn.ghost { background: var(--surface-1); color: var(--text-primary); border-color: var(--border-strong); }
@media (hover: hover) { .btn.ghost:hover { background: var(--surface-2); opacity: 1; } }
.btn.sm { padding: 6px 11px; font-size: 12.5px; }
/* Pleine largeur, pour l'action principale d'une fenetre. Le bouton de photo du
   jour etait un `sm ghost` pose au-dessus de douze champs a remplir : discret
   devant une grille, il se lisait comme une option alors qu'il fait tout le
   travail. Une action qui remplace douze gestes doit occuper la largeur des
   douze. */
.btn.pleine { display: block; width: 100%; padding: 11px 14px; font-size: 13.5px; }
.btn.danger { background: color-mix(in oklab, var(--critical) 90%, #000); color: #fff; }
.btn.icon { padding: 4px 8px; background: transparent; color: var(--muted); border-color: transparent; font-size: 14px; }
@media (hover: hover) { .btn.icon:hover { color: var(--critical); background: var(--surface-2); } }
/* La cible du doigt, plus grande que le dessin. Un bouton-icone mesurait 22 a
   29 px selon l'endroit, quand le minimum tenable au doigt est 24 : sous cette
   taille, on atteint son voisin une fois sur trois. L'agrandir pour de vrai
   ferait un aplat la ou il faut un signe discret — le seul moyen d'avoir les
   deux est de separer la boite cliquable du dessin.

   `inset` negatif plutot qu'un remplissage : le remplissage pousserait les
   voisins et deplacerait la rangee entiere. Le pseudo-element, lui, ne prend
   aucune place dans le flux. Il faut un `position: relative` sur le bouton, et
   surtout pas de fond : c'est une surface a viser, pas a voir. */
.btn.icon { position: relative; }
/* Six pixels, et pas huit, y compris pour les `xs` de 22 px : a huit, la croix
   de la carte de rappel mordait de deux pixels sur le bord de « Plus tard », son
   voisin a six pixels d'ecart. Une cible etendue ne doit jamais empieter sur une
   autre — et surtout pas quand c'est l'action destructive qui gagne du terrain
   sur celle qui ne fait que reporter. Six suffisent : 34 px pour le plus petit
   des boutons-icones, contre 24 demandes. */
.btn.icon::after { content: ''; position: absolute; inset: -6px; }
.btn.icon:is([data-action^="del-"], [data-action^="supprimer"], [data-action^="retirer"]) {
  color: color-mix(in oklab, var(--critical) 72%, var(--muted));
}
@media (hover: hover) { .btn.icon:is([data-action^="del-"], [data-action^="supprimer"], [data-action^="retirer"]):hover {
  color: #fff; background: color-mix(in oklab, var(--critical) 90%, #000);
} }

.champ-vider {
  position: fixed; z-index: 60;
  width: 20px; height: 20px; padding: 0;
  display: grid; place-items: center;
  font: inherit; font-size: 15px; line-height: 1;
  border: none; border-radius: 50%; cursor: pointer;
  background: var(--surface-2); color: var(--text-secondary);
  box-shadow: 0 0 0 1px var(--border-strong);
}
@media (hover: hover) { .champ-vider:hover { color: var(--critical); box-shadow: 0 0 0 1px var(--critical); } }
input.champ-vidable::-webkit-outer-spin-button,
input.champ-vidable::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
input.champ-vidable[type="number"] { -moz-appearance: textfield; }

input, select, textarea {
  font: inherit; font-size: 13px; color: var(--text-primary);
  background: var(--surface-1); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm); padding: 7px 9px; width: 100%;
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
input[type=number] { text-align: right; font-variant-numeric: tabular-nums; }

/* `hidden` doit primer : une règle de classe comme `.field { display: flex }`
   l'emporte sinon sur le `display: none` du navigateur, et l'élément reste
   visible alors que le code le croit masqué. */
[hidden] { display: none !important; }

.field { display: flex; flex-direction: column; gap: 5px; }
.field label { font-size: 11.5px; color: var(--text-secondary); font-weight: 550; }
.valeur-figee {
  margin: 0; align-self: start; padding: 7px 0;
  font-size: 13px; color: var(--text-secondary); font-style: italic;
}

/*      Un menu deroulant sait deja quelle largeur il lui faut : celle de sa
        plus longue option. C'est `.field`, colonne flex, qui l'etirait de
        force sur toute la carte — d'ou « 1 000 € » dans une boite de 320 px.
        On lui rend sa largeur naturelle. Un menu de montants se serre, un
        menu de noms de comptes reste large, sans qu'on ait a les distinguer.
        Le plafond reste, pour l'option a rallonge — celui d'un texte libre, et
        non 20 em : « Oui, chercher les cours automatiquement » demande 21,3 em
        et « Automatique (place de reference du titre) » 22,5, donc les deux se
        coupaient au milieu d'un mot. Un menu natif tronque sans rien dire ;
        `text-overflow` ajoute l'ellipse quand le conteneur, lui, se resserre.

        `min(28em, 100%)` et non 28 em seuls : un menu prend la largeur de sa
        plus longue option sans regarder ou il est pose, et 28 em valent 364 px
        quand la carte n'en offre que 311 a 375 px. Il sortait alors de l'ecran,
        ou les pixels au-dela du bord sont perdus. */
.field select {
  width: auto; align-self: start;
  max-width: min(28em, 100%); text-overflow: ellipsis;
}
/* Un menu se dimensionne sur ce qu'il montre, pas sur sa plus longue option.

   C'est le principe ; la mise en oeuvre demande deux regles parce qu'aucune ne
   suffit seule. `width: auto` prend la largeur de la plus longue option, et une
   liste de quarante-quatre places de cotation etale donc son menu sur 292 px pour
   afficher « Automatique », onze caracteres. A l'inverse, un plafond serre pour
   tout le monde couperait « Oui, chercher les cours automatiquement », qui est la
   valeur choisie et non une option lointaine. Le CSS ne connait pas l'option
   selectionnee : il ne peut pas trancher entre ces deux cas.

   Le plafond general reste donc large — la valeur choisie doit tenir — et les
   listes qui portent une option nettement plus longue que ce qu'on y choisit
   d'ordinaire prennent `menu-serre`. Douze em valent 192 px sur telephone, ou le
   navigateur impose 16 px aux controles de formulaire, et 156 sur ecran : de quoi
   ecrire « Euronext Paris » en entier. Les deux ou trois noms plus longs de la
   liste s'y coupent, avec l'ellipse pour le dire — c'est la contrepartie assumee,
   et la liste deroulee, elle, reste dessinee par le systeme a sa vraie largeur.

   `select.menu-serre` et non `.menu-serre` seul : a specificite egale avec
   `.field select`, c'est l'ordre qui tranche, et une classe nue perdrait. */
select.menu-serre { max-width: min(12em, 100%); }
.field input[type="text"], .field input:not([type]) { max-width: 28em; }
.field :is(input[type="number"], input[type="date"]) { max-width: 9em; }
.field input[maxlength] { max-width: 11em; }
table .field :is(input, select) { max-width: none; }

/* Dans une grille, un champ remplit sa cellule.

   Les plafonds ci-dessus ont leur raison hors grille : un champ seul sur une
   ligne n'a pas besoin d'une boite de 320 px pour « 1 000 € ». Mais dans une
   rangee, ils donnaient quatre largeurs differentes sur une meme fiche — 117 px
   pour un nombre, 160 pour un menu, 299 pour un texte serre, 364 pour un texte
   libre — et rien ne s'alignait avec rien. C'est la grille qui decide de la
   largeur d'une cellule ; le champ, lui, l'occupe.

   Le resultat tient les deux : deux champs d'une `g-2` sont egaux, trois d'une
   `g-3` aussi, et les rangees successives s'alignent puisqu'elles partagent la
   largeur de la carte. Un champ hors grille garde son plafond.

   `align-self` est remis a son defaut : `.field select` le passait a `start`,
   ce qui laissait le menu a sa largeur naturelle meme avec `width: 100%`.

   Enfant direct seulement, et c'est essentiel : une cellule de grille est un
   enfant de la grille, jamais un descendant. Le combinateur descendant attrapait
   le moindre champ pose dans une carte posee dans une grille, et « Français »
   s'affichait alors dans un menu de 442 px sur la page Preferences. Un champ
   n'est etire que s'il est lui-meme la cellule. */
.grid > .field :is(input, select, textarea) {
  max-width: none; width: 100%; align-self: stretch;
}

.card {
  background:
    linear-gradient(var(--surface-1), var(--surface-1)) padding-box,
    linear-gradient(180deg,
      color-mix(in oklab, var(--text-primary) 9%, var(--border)),
      var(--border) 42%) border-box;
  border: 1px solid transparent;
  border-radius: var(--radius); padding: 18px;
  box-shadow: var(--shadow);
}
.card.tight { padding: 14px 16px; }
.card-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-bottom: 14px;
}
.card-head h2 {
  margin: 0; font-size: 16px; font-weight: 680; letter-spacing: -.015em;
  color: var(--text-primary);
}
.card-head .hint { font-size: 12px; color: var(--muted); }

/* Deux boutons du meme geste passent a la ligne ensemble, ou pas du tout, et
   mesurent pareil.

   En-tete de carte souple, ils tombaient un par un : « + Rentree » restait en
   haut a droite contre le selecteur d'annee, « + Depense » descendait seul a
   gauche sous le sous-titre, a l'autre bout de la rangee. Deux entrees de la
   meme paire lues comme deux commandes sans rapport. Un seul element de flex,
   donc un seul point de rupture.

   Une grille a colonnes egales, et non un flex, pour la meme raison que les
   rangees de boutons d'une fiche : en flex, la longueur du libelle decide de la
   largeur, et deux boutons du meme geste finissaient a 77 et 120 px l'un
   au-dessus de l'autre. Des colonnes en `1fr` dans un conteneur de largeur
   naturelle prennent toutes la taille de la plus large : les deux boutons
   mesurent celui qui en demande le plus, sans qu'aucun libelle se replie. */
.paire-btn {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 8px;
}

.sous-titre-carte {
  margin: 22px 0 10px; font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-secondary);
  border-top: 1px solid var(--grid); padding-top: 16px;
}

.th-etab { display: block; font-weight: 500; font-size: 10.5px; color: var(--muted); }

.lien-nu {
  padding: 0; border: 0; background: none;
  font: inherit; color: var(--accent); cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
@media (hover: hover) { .lien-nu:hover { opacity: .75; } }
.lien-nu:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }

.champ-lecture {
  margin: 0; padding: 9px 2px; font-size: 13.5px;
  color: var(--text-secondary);
}

/* L'etablissement sous le nom d'un champ de releve. `min-height` plutot que
   rien : la ligne garde sa place quand elle est vide — le cas d'un nom qui
   porte deja sa banque — et les douze champs de la grille restent alignes.
   Sans elle, un seul libelle sur deux lignes decalait son champ d'un cran. */
.f-etab {
  display: block; min-height: 1.15em; line-height: 1.15;
  font-weight: 500; font-size: 11px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Le ⤒ d'un mois a venir s'eteint : « les montants actuels » n'ont pas de sens
   dans un mois qui n'a pas eu lieu, et l'action y est refusee. Un bouton qui a
   l'air disponible et se derobe au clic est pire que celui qui l'annonce.

   La regle est ici, et non a cote de ses voisines `.snap` : ce bloc-la est
   ecrit deux fois a l'identique dans la feuille, et une troisieme copie
   n'aiderait personne. Sa specificite depasse celle de `.btn.icon.snap`, donc
   sa place dans le fichier ne change rien. */
.btn.icon.snap.snap-avenir { opacity: .3; }
@media (hover: hover) { .btn.icon.snap.snap-avenir:hover { color: var(--muted); background: none; } }

.avert {
  margin: 0 0 14px; padding: 9px 12px;
  border-left: 3px solid var(--warning);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: color-mix(in oklab, var(--warning) 9%, transparent);
  color: var(--text-primary); font-size: 12.5px; line-height: 1.45;
}

.grid { display: grid; gap: 16px; }
/* Un enfant de grille refuse par defaut de descendre sous sa largeur
   min-content : une carte au contenu large (un libelle qui ne se coupe pas,
   un tableau) poussait alors toute la page en debordement horizontal. Les
   `minmax(0, …)` ci-dessous protegent les colonnes, pas leur contenu — d'ou
   ce plancher leve explicitement. La regle existait deja cote mobile ; elle
   manquait sur grand ecran, ou le debordement passait plus inapercu. */
.grid > *, .card, .table-wrap { min-width: 0; }
.g-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.g-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.g-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.g-auto { grid-template-columns: repeat(auto-fit, minmax(172px, 1fr)); }
/* `align-items: start` : chaque carte prend sa hauteur, pas celle de sa
   voisine. Par defaut une grille etire ses elements, si bien que « Depenses du
   mois en cours » — un grand chiffre et trois ecarts — s'allongeait sur les
   480 px de la carte d'a cote et montrait surtout du vide. Deux cartes de
   hauteurs differentes se lisent mieux qu'une carte a moitie remplie. */
.g-hero { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); align-items: start; }
.g-2-1 { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }

.hero {
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--surface-2) 70%, var(--surface-1)), var(--surface-1));
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 24px 26px;
  display: flex; flex-direction: column; gap: 16px;
}
.hero-label {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .08em; color: var(--text-secondary);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.hero-label > span:first-child { min-width: 9.8em; }
.hero-oeil { margin-left: auto; padding: 9px; }
.hero-oeil svg { width: 16px; height: 16px; }

.seg-mini { padding: 1px; }
.seg-mini button {
  font-size: 10.5px; letter-spacing: .04em; padding: 2px 8px; border-radius: var(--radius-xs);
}
.hero-sous { font-size: 12px; margin-top: 2px; text-transform: none; letter-spacing: 0; }

.bien { padding: 12px 0; border-bottom: 1px solid var(--grid); }
.bien:last-child { border-bottom: none; }
.bien-tete { display: flex; align-items: baseline; gap: 12px; margin-bottom: 8px; }
.bien-tete b { margin-left: auto; font-size: 17px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.bien .kv { margin: 0 0 10px; }
.pret-vif {
  margin: 4px 0 12px; padding: 8px 10px;
  background: var(--surface-2); border-radius: var(--radius-sm);
}
.pret-vif .kv { margin: 6px 0 0; }
.pret { padding: 12px 0; border-bottom: 1px solid var(--grid); }
.pret:last-of-type { border-bottom: none; }
.pret .goal-bar { margin-top: 4px; }

/* Montant qu'on peut masquer d'un geste. Il ne se deguise pas en bouton :
   rien ne change au repos, seuls le curseur et un leger voile au survol
   disent qu'il repond.

   Ce bloc precede volontairement les regles qui donnent leur taille aux
   montants : `font: inherit` neutralise le style que le navigateur impose
   aux <button>, et doit donc ceder ensuite a `.hero-value`. Place apres, il
   ramenait le patrimoine de 52 px a 14 px. */
.val-masquable {
  background: none; border: none; padding: 0; cursor: pointer;
  color: inherit; font: inherit; letter-spacing: inherit; text-align: left;
  display: block; transition: opacity .15s;
  user-select: none; -webkit-user-select: none;
  -webkit-touch-callout: none;
}
@media (hover: hover) { .val-masquable:hover { opacity: .72; } }
.val-masquable:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

.hero-value { font-size: clamp(38px, 5vw, 54px); font-weight: 600; letter-spacing: -.035em; line-height: 1; margin: 8px 0 0; }
.hero-barre {
  display: flex; height: 10px; border-radius: var(--radius-pill); overflow: hidden;
  background: var(--grid);
}
.hero-barre > i { display: block; height: 100%; flex: 0 0 auto; }
.hero-barre > i:last-child { flex: 1 1 auto; }

.hero-deltas { display: flex; gap: 20px; flex-wrap: wrap; }
.hero-delta { display: flex; flex-direction: column; gap: 1px; }
.hero-delta span { font-size: 11px; color: var(--muted); }
.hero-delta b { font-size: 14px; font-weight: 600; }

.up { color: var(--good-text); }
.down { color: var(--critical); }
.flat { color: var(--muted); }

/* Un depassement leger, celui qui se rattrape sur le mois suivant. Meme
   graduation que les barres du graphique, qui la tiennent de
   `niveauDepassement()` : un mois orange sur le dessin et rouge dans le tableau
   juste en dessous, ce serait deux jugements sur le meme mois. */
.tiede { color: var(--serious); }

/* Un credit en cours n'est pas une alerte. Le rouge de `.down` dit « quelque
   chose ne va pas » — un depassement de budget, une moins-value — et il
   s'affichait sur le capital restant du, qui n'est ni bon ni mauvais : c'est
   une soustraction choisie, dans une addition qui la mentionne. Le signe moins
   porte deja tout le sens, la couleur n'a rien a ajouter. */
.dette { color: var(--text-secondary); }

.tile {
  background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 15px 16px; display: flex; flex-direction: column; gap: 5px;
  position: relative; overflow: hidden;
  transition: border-color .15s ease, transform .15s ease, background .15s ease;
}
.tile::before {
  content: ''; position: absolute; left: 0; right: 0; top: 0; height: 2px;
  background: var(--tile-color, var(--series-1));
  opacity: .85; transition: opacity .15s ease;
}
.tile .t-label {
  font-size: 10.5px; font-weight: 650; letter-spacing: .07em; text-transform: uppercase;
  color: var(--muted); display: flex; align-items: center;
}
.tile .t-value { font-size: 24px; font-weight: var(--fw-chiffre); letter-spacing: -.02em; line-height: 1.05; }
.tile .t-meta { font-size: 11.5px; color: var(--text-secondary); display: flex; align-items: center; gap: 8px; }
.tile-link {
  font: inherit; text-align: left; cursor: pointer; color: var(--text-primary);
}
@media (hover: hover) { .tile-link:hover {
  border-color: var(--border-strong);
  background: color-mix(in oklab, var(--tile-color, var(--series-1)) 5%, var(--surface-1));
  transform: translateY(-1px);
} }
@media (hover: hover) { .tile-link:hover::before { opacity: 1; } }
.tile-link:active { transform: translateY(0); }
.tile-link:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.tile .t-go {
  position: absolute; top: 11px; right: 12px; font-size: 15px; color: var(--muted);
  opacity: 0; transition: opacity .15s ease, transform .15s ease;
}
@media (hover: hover) { .tile-link:hover .t-go { opacity: 1; transform: translateX(2px); } }

@keyframes flash-target {
  0%, 100% { box-shadow: var(--shadow); }
  15%, 60% { box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 55%, transparent), var(--shadow); }
}
.flash-target { animation: flash-target 1.6s ease; border-radius: var(--radius); }
@media (prefers-reduced-motion: reduce) { .flash-target { animation: none; outline: 2px solid var(--accent); } }

.goal { display: flex; flex-direction: column; gap: 10px; }

.runway { list-style: none; margin: 14px 0 0; padding: 0; }
.rw-ligne { padding: 10px 0; border-bottom: 1px solid var(--border); }
.rw-ligne:last-child { border-bottom: none; padding-bottom: 2px; }
.rw-haut, .rw-bas { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.rw-bas { margin-top: 3px; }
.rw-lab { font-weight: 550; min-width: 0; }
.rw-val { flex: none; white-space: nowrap; font-variant-numeric: tabular-nums; }
.rw-note { min-width: 0; font-size: 12px; line-height: 1.45; color: var(--muted); }
.rw-mois { flex: none; }

/* Le montant et sa légende côte à côte, pas aux deux bouts de la carte.

   `space-between` marchait sur un téléphone, où 343 px font une paire serrée.
   Sur un écran de 1 280 px il jetait le chiffre tout à gauche et la phrase tout
   à droite, avec neuf cents pixels de vide entre les deux : deux îlots au lieu
   d'une lecture. « Ça manque de professionnalisme » — c'est d'abord ça.

   Alignés à gauche, ils se lisent d'un trait à toutes les largeurs. La barre,
   elle, garde la pleine largeur : c'est une jauge, elle doit se comparer à son
   propre vide. */
.goal-top { display: flex; justify-content: flex-start; gap: 12px; align-items: baseline; }
.goal-top b { font-size: 20px; letter-spacing: -.02em; }
.goal-top-empile { flex-wrap: wrap; gap: 2px 12px; }
.goal-top-empile .muted { min-width: 0; font-size: 12.5px; }
.goal-bar { height: 12px; border-radius: var(--radius-pill); background: var(--surface-2); overflow: hidden; border: 1px solid var(--border); }
/* L'accent, et non le dégradé du budget.

   La barre portait `--degrade-budget`, bleu vers rose, créé pour accorder la
   jauge de l'accueil et les barres de catégories du Budget. Sur Projection,
   aucun autre élément ne le porte : le graphique juste en dessous parle en azur
   et en turquoise. La barre était donc la seule couleur décorative de l'écran,
   et un dégradé qui ne représente aucune quantité se lit comme de l'ornement.
   « Ça ne colle pas au graphique plus bas » : les deux ne partageaient
   littéralement aucune couleur. */
.goal-fill { height: 100%; border-radius: var(--radius-pill); background: var(--accent); }
.goal-foot { display: flex; justify-content: flex-start; gap: 12px; font-size: 12px; color: var(--text-secondary); }
.goal-vide {
  flex-direction: row; align-items: center; justify-content: space-between;
  gap: 12px; font-size: 13.5px; color: var(--text-secondary);
}
.goal-vide .muted { flex: none; }

tr.mois-attendu > td { background: color-mix(in oklab, var(--warning) 9%, transparent); }
tr.mois-attendu > td.sticky-col { box-shadow: inset 3px 0 0 var(--warning); }
.marque-attendu { margin-left: 6px; color: var(--warning); font-size: 12px; }
.note-relance { background: color-mix(in oklab, var(--warning) 12%, var(--surface-1));
  border-color: color-mix(in oklab, var(--warning) 38%, transparent);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.note-relance .btn { margin-left: auto; flex: none; }

.card.plan { border-color: color-mix(in oklab, var(--accent) 30%, var(--border)); }
.plan-phrase {
  margin: 0 0 14px; font-size: 14px; line-height: 1.65;
  color: var(--text-primary);
}
.plan-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 18px; }
.plan-cols h3 {
  margin: 0 0 6px; font-size: 10.5px; font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; color: var(--muted);
}
.plan-cols ul { margin: 0; padding-left: 16px; line-height: 1.85; font-size: 13px; }

.perimetre {
  margin: 0 0 12px; font-size: 12.5px; line-height: 1.6;
  color: var(--text-secondary);
}
.perimetre.exclues { margin-top: -6px; }
.perimetre-tete {
  margin: 0; padding: 2px 0 2px 12px;
  border-left: 2px solid color-mix(in oklab, var(--accent) 55%, transparent);
}
.perimetre .mois-lien { font-size: 12.5px; }
/* Un lien pose dans une precision de pied de liste : il herite de la taille et
   de la couleur du `.sub` qui le porte, sinon un bouton de 13 px surgit au
   milieu d'un texte de 11 px. */
dt .sub .mois-lien { font: inherit; color: inherit; }
.reeq { list-style: none; margin: 4px 0 0; padding: 0; }
.reeq-groupe {
  font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--muted); padding: 2px 0 6px;
}
.reeq-groupe-suite { margin-top: 6px; padding-top: 12px; border-top: 1px solid var(--border-strong, var(--border)); }
.reeq-groupe + .reeq-ligne { padding-top: 2px; }

.reeq-ligne { padding: 12px 0; border-bottom: 1px solid var(--border); }
.reeq-ligne:last-child { border-bottom: none; padding-bottom: 2px; }
.reeq-haut, .reeq-bas { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.reeq-nom { font-weight: 600; min-width: 0; }
.reeq-val { white-space: nowrap; font-variant-numeric: tabular-nums; }
.reeq-droite { flex: none; display: inline-flex; align-items: center; gap: 4px; }
.reeq-droite .btn.icon.xs { padding: 2px 5px; font-size: 12px; }

.reeq-jauge {
  position: relative; height: 8px; margin: 8px 0 7px;
  border-radius: var(--radius-pill); background: var(--surface-2);
}
/* Une jauge de cible, deux couleurs et non trois.

   Le bleu de `series-1` peignait la barre sous la cible : c'est la teinte des
   liquidites, elle n'avait rien a dire ici. Un degrade de l'accent la remplace —
   c'est la page des objectifs, et le violet y est la couleur de ce qu'on decide.

   Le depassement porte le meme violet. Il a ete rouge, puis ambre, et les deux
   etaient faux pour la meme raison : detenir plus que sa cible n'est pas un
   incident, c'est un ecart a arbitrer, exactement comme en detenir moins. Une
   couleur d'alerte sur la moitie des cas faisait d'une page ou rien ne va mal la
   plus alarmante de l'application.

   Et elle ne servait a rien : le repere de cible est dessine sur la piste, donc
   le cote se lit a la position de la barre, pas a sa teinte. Le vert reste seul
   a dire quelque chose — la cible est atteinte, la seule nouvelle de la carte
   qui merite sa couleur. */
.reeq-reel { position: absolute; left: 0; top: 0; bottom: 0; border-radius: var(--radius-pill); }
.reeq-reel.sous, .reeq-reel.sur {
  background: linear-gradient(90deg,
    color-mix(in oklab, var(--accent) 55%, transparent), var(--accent));
}
.reeq-reel.ok { background: var(--good); }
.reeq-cible {
  position: absolute; top: -3px; bottom: -3px; width: 2px;
  background: var(--text-primary); opacity: .75; border-radius: 2px;
}
.reeq-jauge.ouvrable {
  display: block; width: 100%; border: 0; padding: 0; cursor: pointer;
  -webkit-appearance: none; appearance: none;
  transition: background .15s ease;
}
.reeq-jauge.ouvrable::after {
  content: ''; position: absolute; left: 0; right: 0; top: -7px; bottom: -6px;
}
@media (hover: hover) { .reeq-jauge.ouvrable:hover { background: var(--surface-3, var(--border)); } }
.reeq-jauge.ouvrable:active > .reeq-reel { opacity: .8; }
.reeq-jauge.ouvrable:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px;
}
.reeq-bas { font-size: 12.5px; }
.reeq-part { display: inline-flex; align-items: baseline; gap: 5px; min-width: 0;
  color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.reeq-ecart { flex: none; white-space: nowrap; font-weight: 600; }
/* Un arbitrage a faire, ni bon ni mauvais : la couleur de la page, pas celle
   d'une alerte. « A la cible » garde le gris de `.muted`, qui dit qu'il n'y a
   rien a lire de plus. */
.reeq-ecart.a-faire { color: var(--text-secondary); }
.reeq-ecart.a-faire b, .montant-plan { color: var(--text-primary); font-weight: 650; }

.reeq-ecart.a-faire b.renfort, .montant-plan.renfort { color: var(--good-text); }
.reeq-pied { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }

/* La cible se choisit dans une liste, plus au clavier : `width: auto` laisse le
   selecteur prendre la largeur de son plus long libelle, « 100 », au lieu des
   3,4 em taillees pour un champ de saisie. */
.cible-champ {
  width: auto; padding: 3px 6px; font-size: 12.5px;
  border: 1px solid var(--border-strong); background: var(--surface-1);
  text-align: right; font-variant-numeric: tabular-nums;
}
.cible-champ:focus { border-color: var(--accent); }
@media (hover: hover) { .cible-champ:hover { border-color: var(--accent); } }
.reeq-part .u { color: var(--muted); }

.role-barre {
  margin-top: 9px;
  display: flex; height: 14px; border-radius: var(--radius-pill); overflow: hidden;
  background: var(--surface-2); gap: 2px;
}
.role-part { display: flex; height: 100%; gap: 2px; border-radius: var(--radius-pill); overflow: hidden; }
.role-barre i { display: block; height: 100%; background: var(--c, var(--series-1)); }
.role-barre i.vide { width: 100%; background: var(--surface-2); }
.role-barre i.raye, .role-legende i.raye {
  background-image: repeating-linear-gradient(135deg,
    rgba(0,0,0,.34) 0 3px, transparent 3px 6px);
}
/* La barre d'un role s'ouvre sur ses placements, comme les jauges de cibles.

   Un bouton qui l'enveloppe, et non la barre elle-meme faite bouton : elle
   porte `overflow: hidden` — c'est ce qui decoupe ses segments en pilule — et
   un pseudo-element de zone sensible y serait donc rogne. Le bouton prend les
   marges a son compte : la barre garde ses nueve pixels du dessus, le bouton
   ajoute cinq pixels sous elle et les reprend en marge negative, donc rien ne
   bouge et la zone sensible fait 28 pixels. Les jauges de cibles, plus serrees
   entre leurs voisines, n'en offrent que 21.

   Le survol eclaircit la piste, pas les segments : c'est la piste qui est le
   bouton. Neutre, comme partout ailleurs, le violet restant a ce qu'on decide. */
.role-barre-lien {
  display: block; width: 100%; border: 0; background: none; cursor: pointer;
  padding: 0 0 5px; margin: 0 0 -5px;
  -webkit-appearance: none; appearance: none;
}
.role-barre-lien > .role-barre { transition: background .15s ease; }
@media (hover: hover) { .role-barre-lien:hover > .role-barre { background: var(--surface-3, var(--border)); } }
/* Les segments sont maintenant petits-enfants de la piste, pas enfants : ils
   vivent dans `.role-part`. Un selecteur en enfant direct ne les atteindrait
   plus, et l'appui perdrait son retour visuel. */
.role-barre-lien:active > .role-barre i { opacity: .8; }
.role-barre-lien:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--radius-pill); }
.role-legende { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 5px; font-size: 11.5px; color: var(--text-secondary); }
.role-legende span { display: inline-flex; align-items: center; gap: 5px; }
.role-legende i { width: 8px; height: 8px; border-radius: 2px; background: var(--c, var(--series-1)); }

.liste-mobile { display: none; }

/* Une liste qui est la lecture principale, sur tous les ecrans. `.liste-mobile`
   s'efface au-dela de 768 px, parce qu'elle est le repli d'une grille ; celle-ci
   non, parce qu'aucune grille ne l'attend au-dessus — la liste EST l'affichage.
   Les relevés et le journal des ventes la portent. Le nom ne dit plus « releves »
   pour cette raison : la deuxieme carte a en avoir besoin s'est retrouvee devant
   une classe qui parlait de la premiere, et a repris `.liste-mobile`, invisible
   sur un ecran large. */
.liste-principale { display: block; }
.mlist.mlist-empile { flex-direction: column; align-items: stretch; gap: 9px; }
.mlist-empile .ml-haut { display: flex; align-items: center; gap: 12px; }
.ml-poches {
  display: flex; height: 8px; border-radius: var(--radius-pill); overflow: hidden;
  background: var(--surface-2); gap: 2px;
}
.ml-part { display: flex; height: 100%; gap: 2px; border-radius: var(--radius-pill); overflow: hidden; }
.ml-poches i { display: block; height: 100%; }

tr.ligne-ouvre { cursor: pointer; }
@media (hover: hover) { tr.ligne-ouvre:hover { background: var(--surface-2); } }
tr.ligne-ouvre .mois-lien { pointer-events: none; }
.mlist {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 12px 2px; background: none; border: none;
  border-bottom: 1px solid var(--border);
  font: inherit; color: var(--text-primary); text-align: left; cursor: pointer;
}
.mlist:last-child { border-bottom: none; }
.mlist-groupe { margin-top: 2px; }
.mlist-groupe > .mlist:last-child { border-bottom: none; }
.lien-vue { color: var(--series-1); text-decoration: none; }
@media (hover: hover) { .lien-vue:hover { text-decoration: underline; } }
/* Le mois en cours se repere dans la liste comme il se repere dans le
   tableau. La regle passe avant `:active`, sinon l'appui n'aurait plus de
   retour visuel sur cette ligne-la, la seule qu'on touche souvent. */
.mlist.mois-courant { background: color-mix(in oklab, var(--accent) 7%, transparent); }
.mlist:active { background: var(--surface-2); }
.ml-nom { flex: 1 1 auto; min-width: 0; font-weight: 600; }
.ml-nom .sub {
  display: block; margin-top: 2px; font-size: 11.5px; font-weight: 400;
  color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ml-chiffres {
  flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: 1px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.ml-chiffres b { font-size: 14px; }
.ml-chiffres span { font-size: 12px; font-weight: 600; }
.ml-chev { flex: none; color: var(--muted); font-size: 18px; line-height: 1; }

@media (max-width: 767px) {
  .liste-mobile { display: block; }
  .large-seulement { display: none; }
}

/* ---------------- Flux du budget ----------------
   Les barres des categories d'un mois, dans la carte de saisie. Elles ont servi
   un temps aux deux cartes du haut de Budget, et c'est precisement ce qui les
   faisait se ressembler : un meme composant pour deux questions. Le partage du
   revenu a maintenant sa forme, juste en dessous.
   `.flow-lien` etait parti avec : plus une seule de ces barres ne menait
   quelque part. `.flow-pct` revient, dans le panneau qui montre tous les postes
   — la quatrieme colonne de la grille l'attendait deja. */
.flow { display: flex; flex-direction: column; gap: 10px; }
/* La colonne du pourcentage n'existe que si un pourcentage l'occupe.

   `:has()` plutôt qu'une classe de variante : la mise en page suit ce que la
   ligne contient, et la prochaine carte qui emploiera cette grille n'aura pas à
   se souvenir d'un modificateur. C'est le mécanisme déjà retenu pour le bandeau
   d'établissement, qui se pose sous les sous-onglets quand la page en a. */
.flow-row { display: grid; grid-template-columns: 118px minmax(0, 1fr) auto; align-items: center; gap: 10px; }
.flow-row:has(.flow-pct) { grid-template-columns: 118px minmax(0, 1fr) auto 52px; }
.flow-label { font-size: 12.5px; color: var(--text-secondary); }
.flow-bar { height: 9px; border-radius: var(--radius-pill); background: var(--surface-2); overflow: hidden; }
.flow-bar div { height: 100%; border-radius: var(--radius-pill); }
.flow-val { font-size: 13.5px; font-variant-numeric: tabular-nums; }
.flow-pct { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; }

/* Une ligne qui porte son cout annuel doit se donner a toucher.

   Le selecteur vise `[data-aide]` et non `.flow-row` : la carte du Budget emploie
   la meme grille pour six lignes qui, elles, ouvrent le panneau au clic. Deux
   comportements sur une meme classe demandent deux selecteurs, sinon la premiere
   evolution de l'un deteint sur l'autre.

   La zone deborde de 6 px de chaque cote sans decaler la grille — marge negative
   contre remplissage — parce qu'une rangee de 9 px de barre est plus fine que le
   doigt qui la vise. Le survol reste sous `hover: hover` : au doigt il ne se leve
   pas, et la ligne resterait allumee apres l'appui. */
.flow-row[data-aide] {
  cursor: pointer; border-radius: var(--radius-sm);
  margin: -3px -6px; padding: 3px 6px;
  transition: background-color .12s;
}
@media (hover: hover) {
  .flow-row[data-aide]:hover { background: var(--surface-2); }
}
.flow-row[data-aide]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; background: var(--surface-2);
}

.sans-veuve { white-space: nowrap; }

.bascules { display: flex; flex-direction: column; }
.bascule {
  display: flex; align-items: center; gap: 14px; width: 100%;
  padding: 12px 2px; background: none; border: none;
  border-bottom: 1px solid var(--border);
  font: inherit; color: var(--text-primary); text-align: left; cursor: pointer;
}
.bascule:last-child { border-bottom: none; }
.bascule:active { background: var(--surface-2); }
.bascule-txt { flex: 1 1 auto; min-width: 0; font-weight: 600; }
.bascule-txt .sub {
  display: block; font-weight: 400; font-size: 12px; color: var(--muted);
  margin-top: 2px;
}
/* La piste : grise eteinte, accent allumee. Le pouce glisse en `transform`,
   jamais en `left` — la composition reste sur le GPU. */
.bascule-piste {
  flex: none; position: relative; width: 42px; height: 25px;
  border-radius: var(--radius-pill); background: var(--surface-2);
  border: 1px solid var(--border-strong);
  transition: background .2s ease, border-color .2s ease;
}
.bascule-piste i {
  position: absolute; top: 2px; left: 2px; width: 19px; height: 19px;
  border-radius: 50%; background: var(--text-secondary);
  transition: transform .22s cubic-bezier(.34, 1.28, .48, 1), background .2s ease;
}
.bascule.on .bascule-piste {
  background: var(--accent);
  border-color: color-mix(in oklab, var(--accent) 70%, transparent);
}
.bascule.on .bascule-piste i { transform: translateX(17px); background: #fff; }
.bascule:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); }
@media (prefers-reduced-motion: reduce) {
  .bascule-piste, .bascule-piste i { transition: none; }
}

.flow-lien {
  display: block; width: 100%; text-align: left;
  background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit;
  cursor: pointer; border-radius: var(--radius-sm);
}
@media (hover: hover) { .flow-lien:hover .flow-val { color: var(--accent); } }
.flow-lien:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

.flux-total {
  display: flex; align-items: baseline; gap: 8px; width: 100%;
  background: none; border: none; padding: 2px 0 12px;
  font: inherit; color: var(--text-primary); text-align: left; cursor: pointer;
}
.flux-total b { font-size: 24px; font-weight: var(--fw-chiffre); letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.flux-total span { font-size: 12.5px; }
.flux-chev { margin-left: auto; color: var(--muted); font-size: 18px; line-height: 1; }
@media (hover: hover) { .flux-total:hover b { color: var(--accent); } }
@media (hover: hover) { .flux-total:hover .flux-chev { color: var(--text-secondary); } }
.flux-total:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--radius-xs); }

.flux-legende { display: flex; flex-direction: column; margin-top: 4px; }
/* La regle vient apres `.repart-haut` a dessein : meme composant, ecart plus
   serre ici, et c'est l'ordre qui tranche a specificite proche. */
.flux-legende .repart-haut {
  padding: 9px 0; border-bottom: 1px solid var(--border); width: 100%;
}
.flux-legende > :last-child { border-bottom: none; }
.flux-lien {
  background: none; border: none; font: inherit; color: inherit;
  text-align: left; cursor: pointer;
}
@media (hover: hover) { .flux-lien:hover .repart-nom { color: var(--accent); } }
.flux-lien:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

.table-wrap { overflow-x: auto; margin: 0 -4px; padding: 0 4px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th {
  text-align: right; font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); padding: 0 10px 9px; white-space: nowrap; border-bottom: 1px solid var(--border);
  position: sticky; top: 0; background: var(--surface-1); z-index: 1;
}
th:first-child, td:first-child { text-align: left; }
td { padding: 9px 10px; border-bottom: 1px solid var(--grid); text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
@media (hover: hover) { tbody tr:hover td { background: color-mix(in oklab, var(--surface-2) 70%, transparent); } }

tr.jalon-retenu td:first-child,
tr.jalon-retenu td:first-child .muted { color: var(--accent); }
tr.jalon-retenu td:first-child { font-weight: 650; }
tbody tr:last-child td { border-bottom: none; }
tfoot td { font-weight: 650; border-top: 1px solid var(--border-strong); border-bottom: none; padding-top: 11px; }
/* Une cellule qui porte un nom porte du texte, pas un nombre : elle s'aligne a
   gauche. `td` est a droite par defaut, ce qui est juste pour des montants, et
   seule la premiere colonne repassait a gauche. Toutes les cellules `.name`
   etaient premieres sauf une, la note du mois, qui se retrouvait donc alignee
   comme un chiffre : on lisait le debut de la phrase et la fin sortait du cadre. */
td.name { font-weight: 550; text-align: left; }

/* La note d'un mois est de la prose, et la seule du tableau : elle se replie sur
   plusieurs lignes au lieu d'etre coupee. `td` est en `nowrap` parce qu'un montant
   ne se coupe pas en deux ; une phrase, si. La borne haute evite qu'une note
   bavarde pousse les colonnes de chiffres hors de l'ecran. */
td.prose {
  white-space: normal; text-align: left;
  min-width: 16em; max-width: 26em; overflow-wrap: anywhere;
}
th.prose { text-align: left; }

/* Pastille de couleur devant un libelle : elle rattache la ligne du tableau a
   sa part du graphique juste au-dessus. En `inline-block` avec une marge a
   droite plutot qu'en flex, pour que le libelle continue de se replier
   normalement sur une cellule etroite. Le leger halo la detache d'un fond
   sombre sans lui donner de contour franc. */
.teinte {
  display: inline-block; width: 9px; height: 9px; border-radius: 3px;
  margin-right: 8px; flex-shrink: 0; vertical-align: baseline;
  background: var(--c, var(--muted));
  box-shadow: 0 0 0 1px color-mix(in oklab, var(--c, var(--muted)) 35%, transparent);
}
td.name .teinte ~ .sub { padding-left: 17px; }
td .sub, dt .sub { display: block; font-size: 11px; color: var(--muted); font-weight: 400; }
.tag {
  display: inline-block; padding: 2px 8px; border-radius: var(--radius-pill); font-size: 10.5px; font-weight: 600;
  background: var(--surface-2); color: var(--text-secondary); border: 1px solid var(--border); letter-spacing: .01em;
  white-space: nowrap; flex: none;
}
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 3px; margin-right: 7px; vertical-align: baseline; }

table.editable td { padding: 4px 5px; }
table.editable input, table.editable select {
  padding: 5px 7px; font-size: 12.5px; border-color: transparent; background: transparent;
  border-bottom: 1px dashed color-mix(in oklab, currentColor 38%, transparent);
}
table.editable input.derived { border-bottom-color: transparent; }
@media (hover: hover) { table.editable input:hover, table.editable select:hover { border-color: var(--border-strong); background: var(--surface-1); } }
table.editable input:focus, table.editable select:focus { background: var(--surface-1); }
table.editable td.name input { text-align: left; font-weight: 550; }

table.editable select.rev-periode {
  display: block; width: auto; max-width: 100%; margin: 4px 0 0 auto;
}
table.editable .rev-estime {
  display: flex; align-items: center; gap: 6px; cursor: pointer; margin-top: 4px;
}
table.editable .rev-estime input[type="checkbox"] {
  width: auto; margin: 0; padding: 0; border: none; accent-color: var(--accent);
}
.rev-liste { display: flex; flex-direction: column; gap: 14px; }
.rev-source {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 12px; background: var(--surface-2);
}
.rev-tete { display: flex; align-items: flex-end; gap: 8px; }
.rev-nom { flex: 1; min-width: 0; }
.rev-suppr { flex: none; margin-bottom: 2px; }
.rev-champs {
  display: grid; gap: 10px; margin-top: 10px;
  grid-template-columns: repeat(auto-fit, minmax(9em, 1fr));
}
.rev-champs .field select, .rev-champs .field input { width: 100%; }
.rev-pied {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 10px; font-size: 11.5px;
}
.rev-estime { display: flex; align-items: center; gap: 6px; cursor: pointer; color: var(--muted); }
.rev-estime input[type="checkbox"] {
  width: auto; margin: 0; padding: 0; border: none; accent-color: var(--accent);
}
.rev-total {
  display: flex; align-items: baseline; gap: 10px; margin-top: 14px;
  padding-top: 12px; border-top: 1px solid var(--grid);
}
.rev-total b { margin-left: auto; font-size: 16px; font-variant-numeric: tabular-nums; }

table.editable input.derived { color: var(--text-secondary); font-size: 12px; }
table.editable input.bad { border-color: var(--critical); background: color-mix(in oklab, var(--critical) 10%, transparent); }
.sym-cell { white-space: nowrap; }
.sym-cell input { width: calc(100% - 26px); display: inline-block; }
.sym-cell input:only-child { width: 100%; }
.btn.icon.xs { padding: 2px 5px; font-size: 12px; vertical-align: middle; }
@media (hover: hover) { .btn.icon.xs:hover { color: var(--accent); } }

.sticky-col { position: sticky; left: 0; background: var(--surface-1); z-index: 2; }
@media (hover: hover) { tbody tr:hover .sticky-col { background: color-mix(in oklab, var(--surface-2) 92%, var(--surface-1)); } }
.sticky-fin {
  position: sticky; right: 0; background: var(--surface-1); z-index: 2;
  box-shadow: inset 1px 0 0 var(--grid);
}
@media (hover: hover) { tbody tr:hover .sticky-fin { background: color-mix(in oklab, var(--surface-2) 92%, var(--surface-1)); } }

.chart { position: relative; width: 100%; }
/* `overflow: visible` laisse les etiquettes d'axe deborder du viewBox, ce
   qui est voulu. Mais il laissait aussi une serie hors echelle peindre par
   dessus les cartes suivantes : les series sont donc decoupees dans le SVG
   lui-meme, et le conteneur borne les degats. */
/* Le SVG du graphique lui-meme, pas n'importe quel SVG a l'interieur : les
   infobulles vivent dans `.chart` et peuvent contenir un montant masque.
   En selecteur descendant, cette regle etirait le petit oeil a 100 % de la
   largeur de la carte. */
.chart > svg { display: block; width: 100%; height: auto; overflow: visible; }
.chart { contain: paint; }

/* Rien ne se selectionne dans un graphique.
   Un appui maintenu sur le dessin declenchait la selection de texte du
   telephone : la zone passait en bleu, avec ses deux poignees et la loupe, et
   il fallait toucher ailleurs pour s'en debarrasser. Les etiquettes d'axe sont
   bien du texte, techniquement, mais personne ne vient copier « 30 k€ » : le
   geste attendu sur un graphique est de le parcourir, pas de le selectionner.

   `touch-callout` supprime en plus le menu contextuel et la loupe d'iOS, que
   `user-select` seul laisse apparaitre. La regle s'arrete au graphique : les
   textes d'aide et les montants des cartes restent copiables. */
.chart, .chart * {
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}

.card:has(> .chart), .card:has(> * > .chart) {
  -webkit-user-select: none; user-select: none;
  -webkit-touch-callout: none;
}
.card:has(.chart) :is(.kv, table, p, .data-view) {
  -webkit-user-select: text; user-select: text;
  -webkit-touch-callout: default;
}
.chart text { font-family: var(--font); }
.tick { font-size: 10.5px; fill: var(--muted); font-variant-numeric: tabular-nums; }
.tick-strong { fill: var(--text-secondary); font-weight: 600; }
.rb-label { font-size: 12.5px; fill: var(--text-primary); dominant-baseline: middle; }
.rb-val { font-size: 12.5px; fill: var(--text-primary); dominant-baseline: middle; font-variant-numeric: tabular-nums; font-weight: 550; }
.rb-pct { fill: var(--muted); font-weight: 400; }
@media (hover: hover) { .rb:hover .rb-label { fill: var(--accent); } }
.donut-val { font-size: 19px; font-weight: 650; fill: var(--text-primary); letter-spacing: -.02em; }
.donut-lab { font-size: 11px; fill: var(--muted); }
.slice { transition: opacity .12s; cursor: default; }
@media (hover: hover) { .chart:hover .slice { opacity: .55; } }
@media (hover: hover) { .chart .slice:hover { opacity: 1; } }

.chart-tip {
  position: absolute; pointer-events: none; z-index: 9;
  background: var(--surface-1); border: 1px solid var(--border-strong); border-radius: var(--radius-sm);
  padding: 9px 11px; box-shadow: var(--shadow); font-size: 12.5px; min-width: 170px;
}
.tt-head { font-weight: 650; margin-bottom: 6px; font-size: 12.5px; }
.tt-sous { margin: -4px 0 6px; font-size: 11px; color: var(--text-secondary); }

.tri-jour { display: inline-flex; align-items: baseline; gap: 4px; }
/* `nowrap` : la fleche de tri est un `::after` dont le contenu commence par une
   espace, et cette espace est secable. Dans la colonne « Poids », large de 50 px
   au telephone, le navigateur coupait donc la : l'intitule sur une ligne, la
   fleche en dessous, et la cellule passait de 15 a 28 px de haut — mesure a
   375 px : la fleche tombait sous son intitule, ou elle ne designe plus rien.

   Une fleche separee de son intitule ne dit plus de quoi elle parle : elle
   flotte sous une colonne et pourrait aussi bien designer la voisine. Les deux
   ne se separent plus. */
.tri-jour .th-tri {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
  white-space: nowrap;
}
@media (hover: hover) { .tri-jour .th-tri:hover { color: var(--text-primary); } }
.tri-jour.desc .th-tri::after { content: ' ↓'; }
.tri-jour.asc  .th-tri::after { content: ' ↑'; }
.tri-jour .th-tri:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
.tt-row { display: flex; align-items: center; gap: 7px; padding: 2px 0; color: var(--text-secondary); }
.tt-row b { margin-left: auto; color: var(--text-primary); font-variant-numeric: tabular-nums; }
.tt-total { border-top: 1px solid var(--grid); margin-top: 5px; padding-top: 5px; font-weight: 600; }
.tt-note { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--grid); color: var(--muted); font-size: 11.5px; white-space: normal; max-width: 260px; }
.sw { width: 9px; height: 9px; border-radius: 3px; flex-shrink: 0; }

.legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 10px; font-size: 12px; color: var(--text-secondary); }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
.legend i.legend-bande {
  width: 16px; height: 0; border-radius: 0;
  border-top: 1px dashed var(--muted);
}

.modal {
  position: fixed; inset: 0; z-index: 100;
  display: grid; place-items: center; padding: 20px;
  background: rgba(0,0,0,.55); backdrop-filter: blur(3px);
  animation: modal-fond .15s ease;
}
/* Pas de flou derrière la fenêtre sur téléphone.

   `backdrop-filter` demande au compositeur de re-flouter tout ce qui se trouve
   derrière, **à chaque image**, pendant que le panneau se déplace au-dessus.
   Sur un écran de bureau ça passe ; sur un iPhone, le fond à flouter est une
   page de trois mille pixels que Safari doit d'abord rasteriser. La première
   ouverture trouve ces couches déjà prêtes — la page n'a pas bougé depuis un
   moment — les suivantes arrivent après un `render()` qui a reconstruit toute
   la vue et ses graphiques, donc tout est à repeindre. C'est exactement la
   forme du symptôme.

   Ce qu'on perd est presque rien : sous un voile noir à 55 %, trois pixels de
   flou ne se voient pas. Ce qu'on garde, c'est l'assombrissement, qui fait tout
   le travail de séparation.

   Même raisonnement que pour l'ovale du bas, dont `ETAT.md` note déjà qu'il est
   le suspect no 1 des accrocs pendant un geste : un flou et une animation ne
   doivent pas se disputer le même compositeur. */
@media (max-width: 900px) {
  .modal { backdrop-filter: none; -webkit-backdrop-filter: none; }
}
.modal[hidden] { display: none; }
:root {
  --duree-fenetre: .34s;
  --duree-fenetre-sortie: .26s;
}

@keyframes modal-fond { from { opacity: 0 } to { opacity: 1 } }
@keyframes modal-entree { from { opacity: 0; transform: translate3d(0, 10px, 0) scale(.985) } to { opacity: 1; transform: none } }

.modal-panel {
  width: min(30em, 100%);
  /* Voir la note sur `dvh` dans le bloc telephone plus bas : `vh` ignore la
     barre d'adresse, `dvh` la suit. */
  max-height: min(80vh, 640px);
  max-height: min(80dvh, 640px);
  /* La fenetre est une grille, et un element de grille recoit `min-width:
     auto` : il refuse de descendre sous la largeur minimale de son contenu,
     et cette regle bat `width: 100%` comme `max-width: 100%`. Sur un ecran de
     375 px, le panneau s'etalait a 399 px des qu'une fiche portait un contenu
     un peu large — le tableau, les valeurs, tout finissait 5 px au-dela du
     bord, hors d'atteinte puisque la page coupe ce qui depasse. Le contenu se
     replie desormais, comme il aurait toujours du. */
  min-width: 0;
  display: flex; flex-direction: column;
  background: var(--surface-1); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--ombre-fenetre);
  animation: modal-entree var(--duree-fenetre) cubic-bezier(.22, .68, .28, 1);
  /* Le panneau annonce ce qu'il va animer : Safari lui donne alors sa propre
     couche avant la premiere image, au lieu de la creer en cours de route — ce
     qui coute une trame et se voit. Il ne vit que le temps d'une fenetre
     ouverte, donc la couche ne survit a rien.
     `translate3d` plutot que `translateY` dans les images-cles, pour la meme
     raison : c'est le vieux remede des animations qui accrochent sur iOS, et ce
     fichier l'applique deja aux bandes collantes du haut. */
  will-change: transform, opacity;
}
.modal-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding: 16px 18px 12px; border-bottom: 1px solid var(--grid);
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
.modal-head h2 { margin: 0; font-size: 16px; letter-spacing: -.01em; }
.modal-sub { margin: 3px 0 0; font-size: 12.5px; color: var(--muted); }
.modal-body {
  padding: 6px 18px 14px; overflow-y: auto; overscroll-behavior: contain;
  /* Defilement inertiel a l'interieur d'une couche `fixed`. Sur iOS, un
     conteneur defilant place dans un calque fixe, pendant que `body` porte
     `overflow: hidden`, peut refuser le geste : c'est le vieux defaut du
     verrouillage de defilement. Cette ligne ne coute rien la ou tout va bien et
     debloque le cas ou ca coince. */
  -webkit-overflow-scrolling: touch;
}
/* Un glissement qui part d'un champ doit defiler la fiche, pas deplacer le
   curseur. La fiche d'une ligne de marche est presque entierement faite de
   champs : sans cette declaration, le navigateur peut lire le geste comme une
   selection de texte et la fiche parait bloquee. `pan-y` lui dit que la
   verticale appartient au conteneur. */
.modal-body :is(input, select, textarea) { touch-action: pan-y; }
.modal-foot {
  padding: 12px 18px 16px; border-top: 1px solid var(--grid);
  display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap;
}
.fiche-danger {
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--grid);
}
.fiche-danger .hint { margin: 8px 0 0; }

.modal-champs { display: flex; flex-direction: column; gap: 10px; padding: 12px 0 4px; }
.modal-champs.champs-cote { flex-direction: row; gap: 10px; }
.modal-champs.champs-cote > .field { flex: 1; min-width: 0; }
.modal-champs.champs-cote input { max-width: 100%; }
.champ-inline { max-width: 8.5em; text-align: right; margin-left: auto; display: block; }
/* L'unite d'un champ de saisie, collee a lui. `margin-left: auto` sur le champ
   pousse deja le couple a droite : le symbole ne doit donc pas en reprendre un,
   sinon il partirait seul au bord. Discret comme une unite, jamais comme une
   valeur — c'est le nombre qu'on lit, pas la monnaie. */
.champ-unite { color: var(--muted); font-size: 12.5px; margin-left: 6px; flex: none; }
.champ-large { max-width: 11em; margin-left: 0; }
.champ-court { max-width: 17em; }

.plafond { margin-top: 8px; max-width: 22em; }
.plafond-jauge {
  height: 5px; border-radius: var(--radius-pill); background: var(--surface-2);
  overflow: hidden; margin-bottom: 5px;
}
.plafond-jauge > i { display: block; height: 100%; background: var(--series-1); }
.plafond.plein .plafond-jauge > i { background: var(--warning); }

.table-compacte { max-width: 38em; }

.card-link {
  font: inherit; text-align: left; color: inherit; cursor: pointer; position: relative;
  transition: transform .12s, border-color .12s;
}
@media (hover: hover) { .card-link:hover { transform: translateY(-1px); border-color: var(--border-strong); } }
.card-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-link .t-go { top: 15px; right: 16px; }

.card-cliquable { position: relative; }
.card-couvre {
  position: absolute; inset: 0; width: 100%; padding: 0;
  background: none; border: none; border-radius: var(--radius); cursor: pointer;
}
@media (hover: hover) { .card-couvre:hover { background: color-mix(in oklab, var(--surface-2) 50%, transparent); } }
.card-couvre:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.card-cliquable > *:not(.card-couvre) { position: relative; pointer-events: none; }
.card-cliquable :is(.aide, .val-masquable, .segmented, .plage, .btn-oeil,
                    .hero-delta-reglable, .rappel-sorties,
                    input, select, textarea) { pointer-events: auto; }

/* Un des trois ecarts du haut se regle : celui de l'objectif de depenses.

   Il doit se distinguer des deux autres, qui ne sont que des lectures, sans
   pour autant se deguiser en bouton — trois blocs alignes dont un seul serait
   encadre casserait la ligne. Un pointille sous le montant suffit : c'est le
   signe qu'on peut le toucher, et il ne prend pas de place.

   Il herite de `.hero-delta` pour sa mise en page, et remet a zero ce qu'un
   <button> apporte de son cote : la bordure, le fond, l'alignement centre et la
   police du systeme. Sans ce dernier point, « Objectif mensuel » ne serait pas
   dans la meme fonte que « Ecart » juste a cote. */
.hero-delta-reglable {
  border: 0; background: none; padding: 0; margin: 0; font: inherit;
  color: inherit; text-align: left; cursor: pointer;
}
.hero-delta-reglable > span { color: var(--accent); }
.hero-delta-reglable > b {
  color: var(--accent);
  border-bottom: 1px dashed color-mix(in oklab, var(--accent) 55%, transparent);
  text-align: center;
  display: inline-flex; align-items: baseline; gap: 3px; justify-content: center;
}
.hero-delta-reglable .hero-delta-chev {
  font-size: 12px; font-weight: 600; opacity: .75; line-height: 1;
}
@media (hover: hover) { .hero-delta-reglable:hover > b { border-bottom-color: var(--accent); } }
.hero-delta-reglable:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--radius-xs);
}

/* `.champ-objectif` vivait ici : un champ discret, souligne d'un pointille, pose
   parmi les trois ecarts du haut de la brique des depenses. Il etait joli et il
   ne se trouvait pas — un reglage a besoin de son intitule, pas d'etre pres du
   regard. L'objectif est redevenu un champ etiquete au bas de la carte, comme
   dans l'onglet Charges fixes, et cette regle est partie avec lui. */

.modal-panel::before { content: none; }
@media (max-width: 900px) {
  .modal-panel { touch-action: pan-y; }
  .modal-panel::before {
    content: ''; display: block; width: 44px; height: 4px; border-radius: var(--radius-pill);
    background: var(--border-strong, var(--border));
    margin: 9px auto 0; padding: 0; flex: none;
    box-sizing: content-box; border-top: 9px solid transparent;
    border-bottom: 9px solid transparent; background-clip: content-box;
  }
}

.cpt-entete { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; }

.cat-retiree td { opacity: .62; }
.cat-retiree .tag { margin-left: 8px; }
/* Les deux gestes d'une categorie tiennent dans une cellule : en colonnes
   separees, le tableau debordait de son cadre a 375 px et la croix passait hors
   de l'ecran. `nowrap` pour qu'ils restent cote a cote plutot que de s'empiler. */
.cat-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; white-space: nowrap; }

/* Le nom d'une fiche — compte ou etablissement — en titre plutot qu'en champ
   de saisie. Il se glisse entre l'etiquette de nature et le montant : plus gros
   que la premiere, plus discret que le second, parce que c'est le sujet de la
   page et non sa reponse. Un `h2` et non un `div` : il porte la police des
   titres comme tous les autres, et c'est bien un titre au sens du document.

   Une seule classe pour les deux fiches. Deux auraient diverge le jour ou l'une
   change de taille. */
.fiche-nom {
  display: flex; align-items: center; gap: 8px;
  margin: 2px 0 0; font-size: 19px; font-weight: 680; letter-spacing: -.02em;
  color: var(--text-primary);
}

.lien-etab { margin-top: 12px; display: inline-flex; align-items: center; gap: 8px; }

.cpt-resume { margin: 0 0 4px; padding: 0 4px; }
.cpt-net { font-size: clamp(26px, 4vw, 34px); font-weight: 700; letter-spacing: -.03em; margin: 4px 0 2px; font-variant-numeric: tabular-nums; }

.cpt-liste { display: flex; flex-direction: column; gap: 14px; }
.cpt-groupe { background: var(--surface-1); border: 1px solid var(--border); border-radius: var(--radius); overflow: clip; }

.cpt-gtitre {
  display: flex; align-items: center; gap: 10px;
  background: linear-gradient(to right,
    color-mix(in oklab, var(--teinte) 16%, var(--surface-2)),
    var(--surface-2) 65%);
  padding: 0 12px 0 0;
  position: sticky; top: 72px; z-index: 5;
  border-bottom: 1px solid var(--border);
  box-shadow: inset 3px 0 0 var(--teinte);
}
.cpt-pastille {
  flex: none; width: 9px; height: 9px; border-radius: 3px;
  background: var(--teinte);
}
.cpt-gplier {
  display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;
  background: none; border: none; cursor: pointer;
  padding: 13px 0 13px 14px; font: inherit; color: var(--text-primary); text-align: left;
}
.cpt-corps { box-shadow: inset 3px 0 0 color-mix(in oklab, var(--teinte) 38%, transparent); }

/*    Le pli anime : une grille dont la rangee passe de 0fr a 1fr. C'est la seule
   facon d'animer une hauteur inconnue sans la mesurer — `height: auto` ne se
   transitionne pas. La visibilite part APRES le mouvement (delai en fermant,
   aucun en ouvrant) : elle sort le contenu replie du clavier et des lecteurs,
   le role que tenait l'attribut hidden quand le pli etait sec.*/
.cpt-pli {
  display: grid; grid-template-rows: 0fr; visibility: hidden;
  transition: grid-template-rows .28s cubic-bezier(.22, .61, .36, 1), visibility 0s .28s;
}
.cpt-pli.ouvert {
  grid-template-rows: 1fr; visibility: visible;
  transition: grid-template-rows .28s cubic-bezier(.22, .61, .36, 1), visibility 0s;
}
/* `min-height: 0` : sans lui, le contenu impose sa hauteur minimale a la
   rangee et la grille ne ferme jamais tout a fait. */
.cpt-pli > .cpt-corps { overflow: hidden; min-height: 0; }
@media (prefers-reduced-motion: reduce) {
  .cpt-pli, .cpt-pli.ouvert { transition: none; }
}
.cpt-gnom { font-size: 15px; font-weight: 700; letter-spacing: -.01em; display: flex; flex-direction: column; }
.cpt-gnom .sub { font-weight: 500; }
.cpt-gtotal { margin-left: auto; text-align: right; font-weight: 700; font-variant-numeric: tabular-nums; display: flex; flex-direction: column; }
.cpt-gtotal .sub { font-weight: 500; }
.cpt-chev { color: var(--muted); font-size: 15px; flex: none; }
.cpt-groupe.replie .cpt-gtitre { border-bottom: none; }

.sub { display: block; font-size: 11px; color: var(--muted); font-weight: 500; }

.cpt-swipe { position: relative; overflow: hidden; touch-action: pan-y; }
.cpt-actions {
  position: absolute; inset: 0 0 0 auto; display: flex; align-items: center; gap: 6px;
  padding: 0 10px; background: var(--surface-2);
}
.cpt-ligne {
  position: relative; z-index: 1;
  display: flex; align-items: center; gap: 12px; width: 100%;
  background: var(--surface-1); border: none; border-bottom: 1px solid var(--border);
  padding: 12px 16px; font: inherit; color: var(--text-primary); text-align: left; cursor: pointer;
}
.cpt-swipe:last-child .cpt-ligne { border-bottom: none; }
@media (hover: hover) { .cpt-ligne:hover { background: var(--surface-2); } }
/* `break-word`, jamais `anywhere` : cette derniere autorise la colonne a se
   reduire a une lettre, et le nom s'imprime a la verticale des que la ligne
   porte aussi un champ de saisie. Le plancher garantit un mot lisible. */
.cpt-nom { min-width: 6em; flex: 1; font-weight: 600; overflow-wrap: break-word; }
.plc-ligne { flex-wrap: wrap; }
.plc-ligne .champ-inline { max-width: 7.5em; }
.cpt-val { text-align: right; font-weight: 650; font-variant-numeric: tabular-nums; flex: none; }
.spark-mini { flex: none; opacity: .9; }

.plc-ligne {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 16px; border-bottom: 1px solid var(--border);
  font-size: 13px;
}
.plc-ligne:last-child { border-bottom: none; }
.card > .plc-ligne { padding-left: 0; padding-right: 0; }

.plc-placement {
  display: grid; grid-template-columns: minmax(0, 1fr) 12em 8em 0.9em;
  align-items: center; gap: 10px; flex-wrap: nowrap;
}
/* `max-width` autant que `min-width` : une piste de grille fixe ne retient
   pas un contenu plus large que lui. Sans ces deux bornes, la liste
   deroulante « Auto, Bloqué » gardait sa largeur naturelle et venait couvrir
   le montant a sa droite. */
.plc-placement .plc-dispo {
  justify-self: stretch; min-width: 0; max-width: 100%;
}
.dispo-reglable { position: relative; display: inline-block; max-width: 100%; }
.dispo-reglable > .tag {
  border-bottom: 1px dashed color-mix(in oklab, currentColor 45%, transparent);
}
.dispo-reglable > select {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer; border: 0; padding: 0; margin: 0;
  -webkit-appearance: none; appearance: none;
}
.dispo-reglable:focus-within > .tag {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.plc-placement .plc-dispo .tag {
  display: block; width: fit-content; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis;
}
.plc-placement .cpt-val { justify-self: end; }
.plc-lien {
  width: 100%; font: inherit; font-size: 13px; color: var(--text-primary);
  background: none; border: none; border-bottom: 1px solid var(--border);
  text-align: left; cursor: pointer;
}
.plc-lien:last-child { border-bottom: none; }
@media (hover: hover) { .plc-lien:hover { background: color-mix(in oklab, var(--surface-2) 60%, transparent); } }
.plc-lien:active { background: var(--surface-2); }
.plc-lien .cpt-chev { justify-self: end; }
.plc-entete {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); font-weight: 600; padding-top: 6px; padding-bottom: 6px;
}
@media (max-width: 479px) {
  .plc-placement { grid-template-columns: minmax(0, 1fr) auto; }
  .plc-placement .plc-dispo { grid-column: 1; grid-row: 2; }
  .plc-placement .cpt-val { grid-column: 2; grid-row: 1 / span 2; }
  .plc-entete .plc-dispo { display: none; }
  .plc-entete > span:first-child::after { content: " · disponibilité"; }
}

/* La ligne qui porte un bouton « Modifier » cede sa derniere colonne.

   Les 0.9em du bout etaient tailles pour un chevron. Le bouton y serait entre
   au chausse-pied, alors que la meme rangee sans bouton n'a aucune raison de
   reserver sa largeur : `:has()` fait dependre la grille de son contenu, comme
   ailleurs dans cette feuille pour les pourcentages de flux.

   Ces regles viennent apres celles de `.plc-placement`, y compris apres la
   requete de media a 479 px : meme specificite, c'est l'ordre qui tranche. */
.plc-placement:has(.plc-modif) {
  grid-template-columns: minmax(0, 1fr) 12em 8em auto;
}
.plc-modif { justify-self: end; white-space: nowrap; }
@media (max-width: 479px) {
  .plc-placement:has(.plc-modif) { grid-template-columns: minmax(0, 1fr) auto; }
  .plc-placement:has(.plc-modif) .cpt-val { grid-column: 2; grid-row: 1; }
  .plc-placement:has(.plc-modif) .plc-dispo { grid-column: 1; grid-row: 2; }
  .plc-placement:has(.plc-modif) .plc-modif { grid-column: 2; grid-row: 2; }
}

.tag.mob-immediat { color: var(--good-text); border-color: color-mix(in oklab, var(--good) 40%, transparent); }
.tag.mob-differe  { color: var(--text-secondary); }
.tag.mob-lent     { color: var(--warning); border-color: color-mix(in oklab, var(--warning) 40%, transparent); }
.tag.mob-bloque   { color: var(--serious); border-color: color-mix(in oklab, var(--serious) 40%, transparent); }
.rw-hors { opacity: .72; }

/* Variante sobre du bouton rouge : la couleur passe dans le texte et le
   liseré, pas en aplat. Elle n'etait pas limitee a `.ghost` — a specificite
   egale avec la regle du bouton plein, elle en repeignait le texte en rouge
   sans toucher le fond, lui aussi rouge. « Cloturer et supprimer » dans
   Comptes etait illisible. */
.btn.ghost.danger {
  background: var(--surface-1); color: var(--critical); font-weight: 600;
  border-color: color-mix(in oklab, var(--critical) 75%, transparent);
}
@media (hover: hover) { .btn.ghost.danger:hover {
  background: color-mix(in oklab, var(--critical) 12%, var(--surface-1));
  color: var(--critical);
} }

.repart { padding: 6px 16px; }
.repart-ligne {
  display: flex; flex-direction: column; gap: 6px; width: 100%;
  background: none; border: none; border-bottom: 1px solid var(--border);
  padding: 13px 0; font: inherit; color: var(--text-primary);
  text-align: left; cursor: pointer;
}
.repart-ligne:last-child { border-bottom: none; }
@media (hover: hover) { .repart-ligne:hover .repart-nom { color: var(--accent); } }
.repart-haut { display: flex; align-items: baseline; gap: 9px; }
.repart-haut .dot { margin: 0; flex: none; }
.repart-nom { font-weight: 600; min-width: 0; overflow-wrap: break-word; }
.repart-haut b { margin-left: auto; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.repart-pct { color: var(--muted); font-size: 12px; min-width: 3.6em; text-align: right; flex: none; }
.repart-barre {
  display: block; height: 5px; border-radius: var(--radius-pill);
  background: var(--surface-2); overflow: hidden;
}
.repart-barre i { display: block; height: 100%; border-radius: var(--radius-pill); }
.repart-pied { margin: 4px 0 12px; padding-top: 12px; border-top: 1px solid var(--border); }
.repart-base { margin: 12px 0 0; padding-top: 12px; border-top: 1px solid var(--border); }
/* Un montant ne se coupe pas apres la virgule. La regle mobile qui fait
   replier les cellules de tableau — necessaire pour les libelles longs —
   l'emportait sur `nowrap` : ces deux selecteurs sont assez specifiques
   pour la battre, et assez etroits pour ne toucher que des montants. */
.repart-pied dd,
.card .repart-total td:not(:first-child),
.grid > div > table.repart-total td:not(:first-child) { white-space: nowrap; }

.modal-sub.avec-action {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.modal-sub.avec-action > .btn { margin-left: auto; }
.liq-groupe { border-bottom: 1px solid var(--border); }
/* Le sommaire est un bouton et non plus un `<summary>` : `<details>` ne
   s'anime pas, et le pli en grille exige un conteneur qu'on pilote a la
   classe. Il en garde l'allure exacte. */
.liq-sommaire {
  width: 100%; background: none; border: 0; margin: 0;
  font: inherit; color: inherit; cursor: pointer; text-align: left;
  display: flex; align-items: center; gap: 10px;
  padding: 11px 0; font-weight: 650;
}
.liq-sommaire[aria-expanded="true"] .cpt-chev { transform: rotate(180deg); }
.liq-corps { overflow: hidden; min-height: 0; }
.liq-sommaire b { margin-left: auto; font-variant-numeric: tabular-nums; }
.liq-groupe .cpt-chev { transition: transform .15s; }
.liq-ligne {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0 6px 12px; font-size: 13px;
}
.liq-ligne .champ-inline { max-width: 8em; }

@media (max-width: 900px) {
  .spark-mini { display: none; }        /* la variation chiffrée suffit */
  .cpt-entete .kv { margin-left: 0 !important; width: 100%; }
}

.confirm-panel { width: min(26em, 100%); }
.confirm-panel .modal-head { border-bottom: none; padding-bottom: 4px; }
.confirm-panel .modal-body p { margin: 0; color: var(--text-secondary); line-height: 1.55; }
.confirm-panel .modal-body p[hidden] { display: none; }
.confirm-panel .modal-foot { border-top: none; padding-top: 4px; }

.modal-total {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 12px 0 10px; border-bottom: 1px solid var(--grid); margin-bottom: 4px;
}
.modal-total b { font-size: 24px; font-weight: var(--fw-chiffre); letter-spacing: -.02em; }
.modal-total span { font-size: 12px; color: var(--muted); }
.modal-body table td { padding: 8px 6px; }
.modal-body table td:first-child { padding-left: 0; }
.modal-body table td:last-child { padding-right: 0; }
/* Un libelle et sa precision se replient dans une fenetre. La regle generale
   `td { white-space: nowrap }` vaut pour les tableaux de page, qui vivent dans
   un conteneur defilant : la colonne s'elargit, on defile, rien n'est perdu.
   Une fenetre n'a pas ce conteneur. Une note longue — « 16 EUR de marge · Mois
   calme, gros stock complements » — poussait donc le tableau hors du cadre et
   emportait la colonne des montants avec elle : « 1 18… » pour 1 189 EUR, sous
   une barre de defilement horizontale.
   Seule la premiere colonne cede : les montants gardent leur `nowrap`, sans
   quoi « 1 189,00 EUR » se casserait entre le nombre et sa devise. */
.modal-body table td.name { white-space: normal; }
.modal-body { overflow-x: hidden; }

@media (max-width: 900px) {
  .modal { place-items: end center; padding: 0; }
  .modal-panel {
    width: 100%;
    /* `vh` sur telephone mesure la fenetre barre d'adresse repliee, pas celle
       qu'on voit. La feuille faisait donc 88 % d'une hauteur plus grande que
       l'ecran : son pied, et avec lui la fin du corps defilant, tombaient sous
       la barre du navigateur. On croyait que le defilement etait casse, alors
       que la fenetre depassait par le bas.
       `dvh` suit la fenetre reellement visible. La declaration en `vh` reste
       au-dessus pour les navigateurs qui ignorent `dvh` : meme propriete, la
       derniere comprise gagne. */
    max-height: 88vh;
    max-height: 88dvh;
    border-radius: var(--radius) var(--radius) 0 0; border-bottom: none;
    padding-bottom: env(safe-area-inset-bottom, 0px);
    animation: modal-feuille var(--duree-fenetre) cubic-bezier(.22, .68, .28, 1);
  }
  @keyframes modal-feuille { from { transform: translate3d(0, 100%, 0) } to { transform: none } }
  .modal-foot .btn { flex: 1; justify-content: center; }
}

/* « Enregistrer et en ajouter une autre » prend sa ligne, sous les deux autres.
   Six mots ne se mettent pas au coude a coude avec « Annuler » et « Ajouter » :
   a 375 px les trois boutons faisaient 107 px chacun et le libelle se pliait en
   quatre. Le pied se replie deja, mais le repli tombait ou il tombait ; une
   ligne declaree vaut mieux qu'un repli subi.

   Cette regle vient **apres** le bloc telephone, et ce n'est pas un rangement :
   `.modal-foot .btn { flex: 1 }` y pose `flex-basis: 0`, meme specificite que
   `.modal-foot .btn-encore`. Ecrite avant, elle perdait — mesure : trois boutons
   sur une seule ligne. Meme piege que `.table-serree`, deja note dans
   `CLAUDE.md` : a specificite egale, c'est l'ordre qui tranche. */
.modal-foot .btn-encore { flex: 1 0 100%; justify-content: center; }

.lancement {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center;
  background: var(--page);
  transition: opacity .34s ease, visibility .34s;
}
.lancement.parti { opacity: 0; visibility: hidden; pointer-events: none; }
/* Le nom se colle au logo, le slogan respire dessous : `gap` uniforme les
   aurait espaces pareil, et les trois mots auraient eu l'air d'une troisieme
   information au lieu d'une precision sur la deuxieme. */
.lancement-marque { display: grid; justify-items: center; gap: 12px; }
.lancement-marque img {
  /* 23 %, le rayon du dessin — voir `.brand-mark`. A 20 px sur 72, l'arrondi
     depassait celui de la tuile et lui rognait les coins. */
  border-radius: 23%; display: block;
  animation: marque-monte .5s cubic-bezier(.16,1,.3,1) both;
}
.lancement-marque span {
  font-size: 17px; font-weight: 680; letter-spacing: -.01em; color: var(--text-primary);
  animation: marque-monte .5s cubic-bezier(.16,1,.3,1) .09s both;
}
.lancement-slogan {
  margin-top: -4px;
  font-size: 12.5px; font-style: normal; font-weight: 500;
  letter-spacing: .01em; color: var(--text-secondary);
  animation: marque-monte .5s cubic-bezier(.16,1,.3,1) .18s both;
}
/* Un seul mot accentue, celui que l'application promet en propre. En
   `var(--accent)` et non en violet ecrit en dur : c'est la couleur de la marque,
   elle suit le theme, et l'ecrire deux fois la laisserait diverger. Le poids
   monte d'un cran, pas plus — un mot en gras violet sur trois suffit, deux
   feraient une enseigne. */
.lancement-slogan b { font-weight: 650; color: var(--accent); }
@keyframes marque-monte {
  from { opacity: 0; transform: translateY(10px) scale(.94); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .lancement-marque img, .lancement-marque span, .lancement-slogan { animation: none; }
  .lancement { transition: opacity .01s; }
}

.file-banner {
  display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; gap: 12px; align-items: center;
  margin: 18px 30px -4px; padding: 12px 15px; border-radius: var(--radius-sm);
  font-size: 12.5px; color: var(--text-secondary);
  background: color-mix(in oklab, var(--warning) 12%, var(--surface-1));
  border: 1px solid color-mix(in oklab, var(--warning) 38%, transparent);
}
.file-banner b { color: var(--text-primary); }
.file-banner code {
  background: var(--surface-2); padding: 1px 5px; border-radius: 4px;
  font-size: 11.5px; border: 1px solid var(--border);
}
@media (max-width: 900px) { .file-banner { margin: 14px 16px -4px; } }

.data-view { margin-top: 14px; border-top: 1px solid var(--grid); padding-top: 10px; }
.data-view summary {
  cursor: pointer; font-size: 12px; color: var(--muted); font-weight: 550;
  list-style: none; display: inline-flex; align-items: center; gap: 6px; padding: 3px 0;
}
.data-view summary::-webkit-details-marker { display: none; }
.data-view summary::before { content: '▸'; transition: transform .15s; display: inline-block; }
.data-view[open] summary::before { transform: rotate(90deg); }
@media (hover: hover) { .data-view summary:hover { color: var(--text-primary); } }
.data-view table { margin-top: 10px; }
.data-view .table-wrap { max-height: 320px; overflow-y: auto; }

.chevron-role {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0;
  font: inherit; font-size: 20px; font-weight: 700; line-height: 1;
  background: none; border: none; border-radius: 50%; cursor: pointer;
  color: var(--accent);
  transition: transform .18s cubic-bezier(.22, .61, .36, 1),
              background .14s ease, opacity .14s ease;
  opacity: .8;
}
.chevron-role.ouvert { transform: rotate(90deg); }
@media (hover: hover) { .chevron-role:hover {
  opacity: 1; background: color-mix(in oklab, var(--accent) 16%, transparent);
} }
@media (hover: hover) { .chevron-role.ouvert:hover { transform: rotate(90deg) scale(1.08); } }
@media (hover: hover) { .chevron-role:not(.ouvert):hover { transform: scale(1.08); } }
.chevron-role:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.contrib-liste {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--grid);
}
.contrib-jeton {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 6px 5px 11px; border-radius: var(--radius-pill);
  background: var(--surface-2); border: 1px solid var(--border);
  font-size: 12.5px; white-space: nowrap;
}
.contrib-jeton .muted { font-variant-numeric: tabular-nums; }

details.pli-reglages { margin-top: 2px; }
details.pli-reglages > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px;
  border: 1px solid var(--border-strong, var(--grid)); border-radius: var(--radius);
  background: var(--surface-2);
  font-size: 12.5px; font-weight: 550; color: var(--text-primary);
}
details.pli-reglages > summary::-webkit-details-marker { display: none; }
@media (hover: hover) { details.pli-reglages > summary:hover { border-color: var(--accent); } }
.pli-valeurs { flex: 1 1 auto; min-width: 0; }
.pli-action {
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  color: var(--accent);
}
.pli-action::after { content: '▾'; display: inline-block; transition: transform .15s; }
details.pli-reglages[open] .pli-action::after { transform: rotate(180deg); }

.acct-head { flex-wrap: wrap; row-gap: 6px; }
.acct-nom {
  flex: 1 1 auto; min-width: 9em; width: auto;
  font: inherit; font-size: 14px; font-weight: 680; color: var(--text-primary);
  background: transparent; border: 1px solid transparent; border-radius: var(--radius-xs);
  padding: 3px 6px; margin: -3px -6px; text-align: left;
}
@media (hover: hover) { .acct-nom:hover { border-color: var(--border); background: var(--surface-2); } }
.acct-nom:focus { border-color: var(--accent); background: var(--surface-2); outline: none; }

select.annee {
  width: auto; padding: 5px 8px; font-size: 12.5px; font-weight: 550;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius-sm); color: var(--text-primary);
}

.mois-lien {
  font: inherit; font-weight: 680; color: var(--text-primary);
  background: none; border: none; padding: 0; cursor: pointer;
  text-align: left; text-decoration: underline; text-decoration-style: dotted;
  text-underline-offset: 3px;
  text-decoration-color: color-mix(in oklab, currentColor 45%, transparent);
}
@media (hover: hover) { .mois-lien:hover { text-decoration-color: currentColor; } }

.dep-total {
  display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap;
  padding-bottom: 12px; margin-bottom: 4px; border-bottom: 1px solid var(--grid);
}
.dep-somme { font-size: 28px; font-weight: 720; letter-spacing: -.02em; }
.dep-ecart { font-size: 13px; font-weight: 600; }
.dep-grille {
  display: grid; gap: 10px 14px;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.dep-grille input { text-align: right; }
.champ-somme { position: relative; display: block; }
.champ-somme > input { padding-left: 30px; }
.somme-plus {
  position: absolute; left: 5px; top: 50%; transform: translateY(-50%);
  width: 20px; height: 20px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; font-size: 14px; line-height: 1; font-weight: 700;
  color: var(--text-secondary); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: var(--radius-xs); cursor: pointer;
}
@media (hover: hover) { .somme-plus:hover { color: var(--accent); border-color: var(--accent); } }
.somme-plus:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.aide {
  /* `inline-flex` : le « ? » etait centre par le bouton qu'il n'est plus. */
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; padding: 0; margin-left: 5px;
  border-radius: var(--radius-pill); border: 1px solid var(--border);
  background: var(--surface-2); color: var(--muted);
  font: inherit; font-size: 10px; font-weight: 700; line-height: 1;
  cursor: help; vertical-align: middle; flex-shrink: 0;
  position: relative;
}
.aide:focus-visible {
  color: var(--text-primary); border-color: var(--text-secondary);
  outline: 2px solid var(--accent); outline-offset: 2px;
}
.aide::after {
  content: ''; position: absolute; inset: -6px;
  border-radius: var(--radius-pill);
}
@media (hover: hover) { .aide:hover {
  color: var(--text-primary); border-color: var(--text-secondary); outline: none;
} }
/* Un intitule de colonne qui porte son explication.

   Le « ? » rond ne tient pas dans une colonne de 48 px, ou « Poids » occupe
   deja 40 pixels : c'est donc l'intitule qui declenche le panneau. Le pointille
   sous le mot dit qu'il y a quelque chose a lire, sans rien ajouter en largeur.

   `<i>` plutot qu'un `<span>` : les regles qui masquent des colonnes sous
   460 px visent les `span` de l'en-tete par `nth-child`, et un span imbrique
   aurait fait douter en relisant, meme sans casser le compte. Le style le
   redresse, un `<i>` n'a rien d'italique a faire ici. */
.col-aide {
  font-style: normal; cursor: help;
  border-bottom: 1px dotted color-mix(in oklab, currentColor 55%, transparent);
}
.col-aide:focus-visible {
  color: var(--text-secondary); border-bottom-color: currentColor; outline: none;
}
@media (hover: hover) { .col-aide:hover {
  color: var(--text-secondary); border-bottom-color: currentColor; outline: none;
} }
/* En `fixed`, et au-dessus de tout. */
.aide-panneau {
  position: fixed; z-index: 210; max-width: 290px;
  padding: 9px 11px; border-radius: var(--radius-sm);
  background: var(--surface-1); border: 1px solid var(--border);
  box-shadow: var(--ombre-flottante);
  font-size: 12px; line-height: 1.5; color: var(--text-secondary);
  pointer-events: none;
}
.aide-panneau[hidden] { display: none; }

/* La jauge du tirer-pour-rafraichir, sur Marches.

   En `fixed` et hors du flux : elle ne pousse rien et n'a pas a etre reservee
   dans la mise en page d'une carte. Posee sous l'en-tete, centree, invisible
   tant que le doigt n'a rien tire — son opacite suit la course.

   Le disque se remplit de violet quand le seuil est franchi : c'est le signal
   qu'on peut lacher. Sans lui, on tire au hasard en esperant avoir assez tire. */
.tirer {
  position: fixed; left: 50%; top: calc(var(--h-barre) + 4px);
  z-index: 60; opacity: 0; pointer-events: none;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--surface-1); border: 1px solid var(--border-strong);
  box-shadow: var(--ombre-flottante);
  transform: translate(-50%, 0);
  display: grid; place-items: center;
}
.tirer > i {
  display: block; width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid var(--text-secondary); border-top-color: transparent;
}
.tirer.prete { border-color: var(--accent); }
.tirer.prete > i { border-color: var(--accent); border-top-color: transparent; }
.tirer.tourne > i { animation: tirer-tourne .7s linear infinite; }
@keyframes tirer-tourne { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .tirer.tourne > i { animation-duration: 2s; }
}

.jour-total { display: flex; align-items: baseline; flex-wrap: wrap; gap: 10px 16px; margin: 4px 0 18px; }
.jour-eur { font-size: clamp(30px, 6vw, 44px); font-weight: 720; letter-spacing: -.02em; }
.jour-pct { font-size: 17px; font-weight: 650; }
.jour-note { font-size: 12px; color: var(--text-secondary); flex-basis: 100%; }
.jour-total.up .jour-eur, .jour-total.up .jour-pct { color: var(--good); }
.jour-total.down .jour-eur, .jour-total.down .jour-pct { color: var(--critical); }

/* Bandeau de repères, défilement libre.
   Le `scroll-snap-type` accrochait le geste : sept tuiles de 104 px qu'on
   pousse d'un pouce, et le doigt se faisait reprendre à chaque case. Un
   ruban d'indices se parcourt, il ne se feuillette pas.
   `overscroll-behavior-x` empêche le geste horizontal de déborder sur la
   page — sur iOS, arriver au bout déclenchait le retour arrière. */
/* Les familles de reperes : des onglets de texte, pas des pastilles pleines.

   Le ruban en dessous porte deja des tuiles encadrees ; deux etages de boites
   se disputeraient le regard, et c'est le contenu qui doit gagner. Un souligne
   sur l'onglet actif suffit a dire ou l'on est, comme sur CNBC.

   `touch-action: pan-x` et exclusion du tirage : cette barre defile a
   l'horizontale, juste au-dessus d'un ruban qui defile aussi, a l'endroit
   precis ou le geste de rafraichissement s'arme. Voir `monteTirerRafraichir`,
   qui ecarte `.reperes` et cette barre du meme geste. */
.reperes-familles {
  position: relative;
  display: flex; gap: 4px; overflow-x: auto; margin-bottom: 8px;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain; touch-action: pan-x;
}
.reperes-familles::-webkit-scrollbar { display: none; }
.rp-famille {
  flex: none; background: none; border: none; cursor: pointer;
  padding: 5px 9px 7px; font: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--muted); white-space: nowrap;
  transition: color .2s ease;
}
@media (hover: hover) { .rp-famille:hover { color: var(--text-secondary); } }
.rp-famille.on { color: var(--text-primary); }

/* Le soulignement glisse d'un onglet a l'autre.

   Un `border-bottom` pose sur l'onglet actif sautait : la bordure disparait
   d'un mot et reapparait sur l'autre, sans rien entre les deux. Un seul trait,
   deplace en `transform`, fait le trajet — et c'est ce trajet qui dit « ces
   quatre onglets sont une seule chose a quatre etats », la ou deux bordures
   clignotantes disent « deux boutons independants ».

   `transform` et `width`, pas `left` : la composition se fait sur le GPU et la
   mise en page n'est pas recalculee, donc soixante images par seconde meme sur
   un telephone modeste. La courbe part vite et finit doucement, avec un cheveu
   de depassement — c'est ce leger excedent qui donne l'impression d'un objet
   qui a une masse plutot que d'une valeur qui change.

   La classe `glisse` n'arrive qu'apres le premier placement : au chargement, un
   trait qui arrive en glissant depuis la gauche serait un mouvement que
   personne n'a demande. */
.rp-curseur {
  position: absolute; left: 0; bottom: 0; height: 2px;
  background: var(--accent); border-radius: 2px;
  pointer-events: none;
}
.rp-curseur.glisse {
  transition: transform .34s cubic-bezier(.34, 1.28, .48, 1),
              width .34s cubic-bezier(.34, 1.28, .48, 1);
}
@media (prefers-reduced-motion: reduce) {
  .rp-curseur.glisse { transition: none; }
}

/* Le ruban se repose quand on change de famille, et il vient du bon cote.

   Trois choses ont change. La duree passe a 0,42 s et l'ecart a 55 ms, donc le
   dernier arrive vers 0,64 s : assez lent pour se voir, assez court pour ne pas
   se faire attendre. Le mouvement devient lateral, parce que le ruban est
   lateral — une tuile qui monte de 7 px dans une barre horizontale ne raconte
   rien. Et il porte le sens du trajet : on passe d'Indices a Crypto, les
   nouvelles tuiles entrent par la droite, comme la page suivante d'un carrousel.
   C'est `--sens`, pose par la vue selon la position des deux familles dans la
   liste.

   Elle ne joue qu'au changement de famille, jamais au rafraichissement des
   cours : la classe est posee par le clic sur un onglet, pas par le rendu. Une
   barre qui frissonne toutes les cinq minutes fatiguerait. */
@keyframes repere-entre {
  from { opacity: 0; transform: translateX(calc(var(--sens, 1) * 22px)) scale(.97); }
  to   { opacity: 1; transform: none; }
}
.reperes.rp-entre > .repere {
  animation: repere-entre .42s cubic-bezier(.22, .61, .36, 1) backwards;
}
.reperes.rp-entre > .repere:nth-child(1) { animation-delay: 0ms; }
.reperes.rp-entre > .repere:nth-child(2) { animation-delay: 55ms; }
.reperes.rp-entre > .repere:nth-child(3) { animation-delay: 110ms; }
.reperes.rp-entre > .repere:nth-child(4) { animation-delay: 165ms; }
.reperes.rp-entre > .repere:nth-child(n+5) { animation-delay: 220ms; }
@media (prefers-reduced-motion: reduce) {
  .reperes.rp-entre > .repere { animation: none; }
}

.reperes {
  display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  touch-action: pan-x;
}
.reperes::-webkit-scrollbar { display: none; }
@media (max-width: 767px) {
  /* 16 px : la marge laterale de `.view` sous 900 px. */
  .reperes { margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; }
}
.rp-tete { display: flex; align-items: center; gap: 6px; }
.rp-tete .rp-nom { flex: 1; min-width: 0; }
.rp-seance {
  flex: none; width: 12px; height: 12px;
  fill: none; stroke: currentColor; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}
.rp-seance.open  { color: var(--good); }
.rp-seance.pre,
.rp-seance.post  { color: var(--warning); }
.rp-seance.close { color: var(--muted); fill: currentColor; stroke: none; }

.repere {
  flex: none;
  display: grid; gap: 1px; min-width: 104px;
  padding: 8px 11px; border-radius: var(--radius-sm);
  background: var(--surface-1); border: 1px solid var(--border);
  font: inherit; color: inherit; text-align: left; cursor: pointer;
  transition: border-color .12s;
}
@media (hover: hover) { .repere:hover { border-color: var(--accent); } }
.rp-nom { font-size: 10.5px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--muted); white-space: nowrap; }
.rp-prix { font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
.rp-unite { font-size: 11px; font-weight: 500; color: var(--text-secondary); margin-left: 2px; }
.rp-var { font-size: 11.5px; font-weight: 600; font-variant-numeric: tabular-nums; }

.jour-lignes { display: flex; flex-direction: column; gap: 2px; }
.jour-ligne {
  display: grid; align-items: center;
  column-gap: 10px; row-gap: 1px;
  grid-template-columns: minmax(0, 1fr) 58px 76px 92px;
  font-size: 13px; padding: 6px 0;
  border-bottom: 1px solid color-mix(in oklab, var(--grid) 60%, transparent);
}
.jour-ligne:last-child { border-bottom: none; }
.jl-nom, .jl-nom .mois-lien {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  display: block; max-width: 100%;
}
.jour-ligne.entete {
  font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--muted); padding-bottom: 4px;
}
/* `justify-content` et non `text-align` : l'en-tete triable EST un conteneur
   flex — `.tri-jour` porte `display: inline-flex`, blockifie en `flex` par la
   grille qui le contient. `text-align: right` ne deplace donc aucun de ses
   enfants, et la regle etait inerte depuis qu'elle existait : les intitules
   restaient colles a gauche pendant que les chiffres s'alignaient a droite.
   « Les noms des colonnes sont pas centres. »

   Mesure a 852 px : la colonne « Poids » finit a 183, son en-tete s'arretait a
   168 — quinze pixels de decalage, et le bloc du nom, large, rendait l'ecart
   visible sur toute la rangee.

   `text-align: right` reste : il sert aux cellules qui ne sont pas des
   conteneurs flex, et les deux ensemble couvrent les deux cas sans qu'on ait a
   savoir lequel s'applique. */
.jour-ligne.entete span:not(:first-child) { text-align: right; justify-content: flex-end; }
.jl-nom { font-weight: 600; overflow: hidden; }
.jl-nom .sub {
  display: flex; align-items: center; gap: 5px;
  font-size: 11px; color: var(--muted); font-weight: 400; margin-top: 1px;
}
.jl-poids, .jl-pct, .jl-eur {
  text-align: right; font-variant-numeric: tabular-nums;
}
.jl-eur { font-weight: 650; }
/* Le mouvement du jour, sur sa propre rangée, en pleine largeur.

   Logé dans la colonne du nom, il était coupé net — « 588,77 $US → 5… » — la
   colonne faisant 110 px à 375 px pour un texte qui en demande 150, et `.jl-nom`
   portant `overflow: hidden` pour les noms longs. C'est un sous-titre de la
   ligne, pas du nom : `1 / -1` lui donne la ligne entière, et il ne peut plus
   déborder de quoi que ce soit.

   La clôture de la veille en gris, la flèche plus discrète encore, le cours du
   jour en gras et de la couleur du texte : l'œil doit tomber sur le prix
   d'aujourd'hui — c'est lui qu'on cherche — et trouver d'où il vient sans avoir
   à le chercher. */
.jl-mouv {
  grid-column: 1 / -1;
  display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
  font-size: 11px; color: var(--muted); font-weight: 400;
  font-variant-numeric: tabular-nums;
}
/* La clôture de la veille occupe une largeur fixe, calée à droite.

   « Que penses-tu de mettre le prix en blanc sur une troisième ligne, aligné
   avec le prix gris ? » Le constat est juste : les prix du jour démarraient en
   escalier, puisqu'ils suivent une clôture dont la largeur change à chaque
   ligne — « 588,77 $US » ne fait pas la largeur de « 19,24 € ».

   Mais une troisième ligne coûte un tiers de hauteur par titre, et c'est
   exactement ce qu'on venait de retirer pour que la carte cesse d'être lourde.
   Elle perdrait aussi la lecture « de ceci vers cela » que la flèche porte :
   deux prix empilés ne disent plus lequel est l'ancien.

   Une largeur fixe suffit, et elle aligne les trois d'un coup : les clôtures
   occupent la même boîte, donc les flèches partent du même pixel, donc les prix
   du jour aussi.

   Calée à **gauche**, et c'est un second correctif. À droite, le vide se
   creusait devant le montant : « 6,23 € » démarrait 40 px après le bord, sous
   l'en-tête POIDS, et la ligne semblait appartenir à cette colonne plutôt qu'au
   ticker au-dessus d'elle — « le prix ne doit pas aller sous le poids ». Le même
   vide existe toujours, mais il est passé de l'autre côté du montant, entre lui
   et sa flèche, où rien ne le lit comme un décalage.

   `6.6em` sur la police de ce sous-titre : la valeur la plus large qu'on y
   rencontre est « 588,77 $US », et les chiffres sont tabulaires. */
.jl-prix { color: var(--text-primary); font-weight: 650; }
.jl-prix.jl-fige { color: var(--text-secondary); font-weight: 600; }
/* La mention de fraîcheur se cale à droite, contre le bord de la carte.

   « Le mot cours d'hier n'est pas du tout aligné en vertical » : elle suivait
   le mouvement, dont la largeur change à chaque ligne — « 6,23 € → 6,22 € » ne
   fait pas la largeur de « 588,77 $US → 589,90 $US » — donc elle démarrait en
   escalier. Poussée à droite, les cinq mentions finissent au même pixel et
   forment leur propre colonne, sans qu'il faille en déclarer une.

   `margin-left: auto` plutôt qu'une largeur fixe : « pas coté aujourd'hui » et
   « cours d'hier à 22:00 » n'ont pas la même longueur, et une colonne dimensionnée
   sur la plus longue laisserait un vide au milieu de la ligne le reste du temps. */
.jl-quand { margin-left: auto; padding-left: 8px; white-space: nowrap; }

.pastille { width: 7px; height: 7px; border-radius: var(--radius-pill); flex-shrink: 0; display: inline-block; }
.pastille.open { background: var(--good); animation: bat 2s ease-in-out infinite; }
.pastille.pre, .pastille.post { background: var(--warning); }
.pastille.close { background: var(--muted); }
@keyframes bat { 0%, 100% { opacity: 1 } 50% { opacity: .35 } }
@media (prefers-reduced-motion: reduce) { .pastille.open { animation: none } }

@media (max-width: 460px) {
  .jour-ligne { grid-template-columns: minmax(0, 1fr) 58px 70px 74px; column-gap: 8px; }
}

.plage { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.plage select {
  width: auto; padding: 5px 6px; font-size: 11.5px;
  color: var(--text-secondary); background: var(--surface-2);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
}
.plage select.on {
  color: var(--text-primary); border-color: color-mix(in oklab, var(--accent) 55%, transparent);
  background: color-mix(in oklab, var(--accent) 16%, var(--surface-2));
  font-weight: 650;
}

.barre-recherche {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
}
.barre-recherche #symQuery {
  flex: 1 1 260px; max-width: 340px; min-width: 0;
}
/* Le plafond vient de `menu-serre`, pose sur la balise : celui qui vivait ici,
   220 px, etait plus large que lui et le rendait inoperant. Deux plafonds sur le
   meme menu, c'est le plus grand qui ne sert jamais. */
.barre-recherche select {
  flex: 0 1 auto; width: auto;
  font-size: 12px; color: var(--text-secondary);
}

.prix-cell > div, .prix-cell { white-space: nowrap; }
.prix-cell input { width: calc(100% - 20px); }
.prix-cell .dev {
  display: inline-block; width: 16px; text-align: left;
  font-size: 11px; color: var(--text-secondary); cursor: help;
}
.prix-cell .dev.etr { color: var(--series-4); font-weight: 700; }

th .u {
  font-size: 10px; font-weight: 600; opacity: .55;
  margin-left: 3px; text-transform: none;
}

/* ---------------- Rappel de saisie mensuelle ----------------
   Une pastille collée au mot, pas un crayon renvoyé au bout de la ligne.

   Le crayon était plus parlant en théorie — il dit ce qu'on attend, écrire
   quelque chose — mais `margin-left: auto` l'expédiait à l'autre extrémité du
   menu. Détaché de « Relevés » par cinq centimètres de vide, incliné et
   rouge, il se lisait comme une rayure sur l'écran plutôt que comme un
   rappel, et on ne savait pas à quelle ligne il se rapportait.

   Ambre, comme la bannière de rappel : un relevé qui attend n'est pas une
   panne. La pastille est petite et posée contre le mot — c'est le voisinage
   qui porte le sens, pas la taille. */
.badge {
  flex-shrink: 0; align-self: center;
  width: 6px; height: 6px; border-radius: var(--radius-pill); margin-left: -4px;
  background: var(--warning); color: transparent;
  font-size: 0; line-height: 0; overflow: hidden;
  animation: pastille-rappel 2.4s ease-in-out infinite;
}
.badge[hidden] { display: none; }

@keyframes pastille-rappel {
  0%, 100% { opacity: .5;  box-shadow: 0 0 0 0 color-mix(in oklab, var(--warning) 40%, transparent); }
  50%      { opacity: 1;   box-shadow: 0 0 0 3px color-mix(in oklab, var(--warning) 22%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .badge { animation: none; opacity: 1; }
}

.rappel {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  font: inherit; font-size: 13px; color: var(--text-secondary); cursor: pointer;
  padding: 13px 16px; border-radius: var(--radius);
  background: color-mix(in oklab, var(--warning) 10%, var(--surface-1));
  border: 1px solid color-mix(in oklab, var(--warning) 38%, transparent);
  transition: border-color .12s, transform .12s;
}
@media (hover: hover) { .rappel:hover { border-color: var(--warning); transform: translateY(-1px); } }
.rappel b { color: var(--text-primary); }
.rappel-pastille {
  width: 9px; height: 9px; border-radius: var(--radius-pill); background: var(--warning); flex-shrink: 0;
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--warning) 22%, transparent);
}
.rappel-texte { flex: 1 1 140px; }

/* Les sorties du rappel, groupees a droite. Elles arrivent apres
   `.note-relance .btn`, qui pose `margin-left: auto` sur chaque bouton :
   a specificite egale, c'est l'ordre qui tranche, et trois marges
   automatiques dans une meme rangee se partagent l'espace au lieu de
   pousser le groupe au bout. */
.rappel-sorties {
  display: flex; align-items: center; gap: 6px;
  margin-left: auto; flex: none;
}
/* `nowrap` : a 375 px, « Aller à la ligne » se pliait sur trois lignes et
   « Plus tard » sur deux, ce qui triplait la hauteur du bandeau. */
.rappel-sorties .btn { margin-left: 0; white-space: nowrap; }

:is(.note-relance, .note:has(> .rappel-sorties)) { flex-wrap: wrap; }
:is(.note-relance, .note:has(> .rappel-sorties)) > span:not(.rappel-sorties) {
  flex: 1 1 220px;
}
.rappel-sorties .btn.ghost { color: var(--text-secondary); }
.rappel-sorties .btn.icon.xs { font-size: 13px; padding: 3px 7px; }
@media (hover: hover) { .rappel-sorties .btn.icon.xs:hover { color: var(--text-primary); background: var(--surface-2); } }

.snap-cell { width: 34px; padding: 0 !important; text-align: center; }
.btn.icon.snap { font-size: 13px; padding: 5px 7px; color: var(--muted); }
@media (hover: hover) { .btn.icon.snap:hover { color: var(--accent); background: var(--surface-2); } }
.btn.icon.snap.snap-attendu {
  color: var(--page); background: var(--accent); font-weight: 700;
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 22%, transparent);
}
@media (hover: hover) { .btn.icon.snap.snap-attendu:hover {
  background: color-mix(in oklab, var(--accent) 82%, white);
  color: var(--page);
} }
tr.mois-courant td { background: color-mix(in oklab, var(--accent) 7%, transparent); }

@keyframes flash-row {
  0%, 100% { background: transparent; }
  15%, 60% { background: color-mix(in oklab, var(--accent) 24%, transparent); }
}
tr.flash-target td { animation: flash-row 1.6s ease; }

.checks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.chk {
  display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; gap: 11px; align-items: center;
  padding: 11px 13px; border-radius: var(--radius-sm); font-size: 13px;
  border: 1px solid var(--border); background: var(--surface-2);
}
.chk-ic { font-size: 14px; text-align: center; }
.chk-error { border-color: color-mix(in oklab, var(--critical) 45%, transparent);
             background: color-mix(in oklab, var(--critical) 10%, var(--surface-1)); }
.chk-warn  { border-color: color-mix(in oklab, var(--warning) 45%, transparent);
             background: color-mix(in oklab, var(--warning) 10%, var(--surface-1)); }
.chk-info  { border-color: var(--border); background: var(--surface-2); }
.chk-action { border-color: color-mix(in oklab, var(--warning) 45%, transparent);
              background: color-mix(in oklab, var(--warning) 10%, var(--surface-1)); }
.chk .btn { flex-shrink: 0; text-decoration: none; }

th.sortable { user-select: none; white-space: nowrap; }
th.sortable .th-tri {
  all: unset; cursor: pointer; font: inherit; color: inherit;
  display: inline-flex; align-items: baseline; gap: 0;
}
th.sortable .th-tri:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) { th.sortable .th-tri:hover { color: var(--text-primary); } }
th.sortable .th-tri::after { content: '⇅'; opacity: .3; margin-left: 5px; font-size: 10px; }
th.sortable.asc  .th-tri::after { content: '↑'; opacity: 1; color: var(--accent); }
th.sortable.desc .th-tri::after { content: '↓'; opacity: 1; color: var(--accent); }

.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.row.end { justify-content: flex-end; }
.spacer { flex: 1; }
.muted { color: var(--muted); }
.small { font-size: 12px; }
.empty {
  color: var(--text-secondary); font-size: 13px; line-height: 1.55;
  padding: 22px 20px; text-align: center;
  background: color-mix(in oklab, var(--surface-2) 55%, transparent);
  border: 1px dashed var(--border-strong); border-radius: var(--radius-sm);
}
td .empty, th .empty { background: none; border: none; }

.note {
  background: color-mix(in oklab, var(--warning) 12%, var(--surface-1));
  border: 1px solid color-mix(in oklab, var(--warning) 38%, transparent);
  border-radius: var(--radius-sm); padding: 11px 13px; font-size: 12.5px; color: var(--text-secondary);
  display: flex; gap: 9px; align-items: flex-start;
}
.note b { color: var(--text-primary); }

/* La colonne des intitules etait en `auto` : une legende un peu longue —
   « croissance reelle du patrimoine » — la poussait a 250 px et il ne restait
   que 52 px pour le montant, qui se cassait en deux lignes. C'est la valeur
   qui doit prendre ce dont elle a besoin, et l'intitule ce qui reste : lui
   peut se replier, pas un chiffre. */
.kv { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 14px; font-size: 13px; }
.kv dt { color: var(--text-secondary); min-width: 0; }
.kv dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; font-weight: 550;
         white-space: nowrap; }
/* Variante pour les listes qui portent du texte et non des montants : le
   `nowrap` ci-dessus existe pour qu'un montant ne se coupe jamais entre le
   nombre et sa devise, mais applique a une phrase il elargit la grille jusqu'a
   la sortir de sa fenetre. Voir la carte d'identite d'une ligne de titres.

   Les colonnes s'inversent du meme coup. En `1fr auto`, une valeur qui a le
   droit de se replier reclame quand meme tout l'espace, et c'est le libelle
   qui se retrouve ecrase — « Nom officiel » s'ecrivait une lettre par ligne.
   Ici le libelle prend ce qu'il lui faut, la valeur prend le reste.

   Le bloc mobile plus bas redeclare `.kv dt` : les regles qui doivent le
   battre y sont reprises, meme specificite, c'est l'ordre qui tranche. */
.kv-texte { grid-template-columns: auto minmax(0, 1fr); }
.kv-texte dd { white-space: normal; overflow-wrap: anywhere; }
.kv-texte dt { overflow-wrap: normal; min-width: max-content; }

.acct-card { display: flex; flex-direction: column; gap: 12px; }
.acct-head { display: flex; align-items: center; gap: 10px; }
.acct-head h3 { margin: 0; font-size: 14px; }
.acct-value { font-size: 22px; font-weight: var(--fw-chiffre); letter-spacing: -.02em; }

.segmented { display: inline-flex; background: var(--surface-2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 2px; }
.segmented button {
  font: inherit; font-size: 12.5px; font-weight: 550; border: none; background: transparent; color: var(--text-secondary);
  padding: 7px 12px; border-radius: var(--radius-xs); cursor: pointer;
}
.segmented button.on { background: var(--surface-1); color: var(--text-primary); box-shadow: var(--shadow); }

.sous-onglets {
  display: flex; margin-bottom: 2px;
  position: sticky; top: 0; z-index: 6;
  padding: 6px 0 8px;
  /* Aucun fond, et aucun flou : seules les pastilles portent une surface.

     Trois etats se sont succede. Un aplat `var(--page)`, #08090b entre des cartes a
     #101114 : une bande sombre que rien ne demandait, « il y a toujours un truc
     noir autour des boutons ». Puis du verre a 72 % : un aplat plus discret, mais
     toujours un aplat, et un flou dessine une bande aussi surement qu'une couleur.
     Puis rien, ce qui etait la demande — « le fond des boutons onglet, pas les
     boutons eux memes ».

     Ce que ce fond servait : masquer le contenu qui defile sous une bande
     collante. Il n'en reste que les quatorze pixels de remplissage, six en haut et
     huit en bas, ou une carte peut se montrer en passant. Les pastilles, elles,
     couvrent toute la largeur sur telephone et gardent leur propre surface : ce
     qu'on lit ne depend jamais de ce fond. */
  background: transparent;
  min-height: var(--h-sous-onglets); box-sizing: border-box;
}
.sous-onglets .segmented button { padding: 6px 16px; font-size: 13px; }

.sous-onglets .segmented { background: var(--surface-1); position: relative; }

/* Des cellules égales à toutes les largeurs, et non seulement au téléphone.

   Le lavis se déplace par index : il suppose des cases identiques. Sur
   ordinateur, `.segmented` se dimensionne sur son texte — « Cible » contre
   « Patrimoine », presque du simple au double — et le lavis tombait donc à
   côté de l'onglet qu'il devait souligner. Le défaut dormait derrière
   « Objectifs | Patrimoine », deux mots de longueur voisine ; il s'est vu le
   jour où le premier est devenu « Cible ».

   Une grille de colonnes égales le rend structurel, et non plus dépendant
   d'un `flex: 1` posé dans une seule requête média. La largeur reste celle du
   contenu : les colonnes prennent celle du libellé le plus long. */
.sous-onglets .segmented {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
}

/* Le lavis glisse d'un sous-onglet a l'autre, comme la pastille de la barre du
   bas. Meme raison : un fond par bouton disparait d'un cote et reapparait de
   l'autre, et le lien entre les deux est a deviner.

   L'effet est reserve aux sous-onglets, et c'est une contrainte de geometrie et
   non un choix. Ici les cellules sont egales — leurs boutons portent `flex: 1`,
   mesure faite : 169 et 169 a deux, 112 trois fois a trois. Les autres
   `.segmented` de l'application, Net / Brut ou le filtre de role, se
   dimensionnent sur leur texte : 46 et 50 pixels. Un deplacement par index y
   tomberait a cote, et il faudrait mesurer en JavaScript a chaque rendu. Ils
   gardent donc leur fond par bouton.

   Le nombre de cellules se lit dans le DOM plutot que d'etre passe par le
   gabarit : `:nth-child(3):last-child` veut dire « le troisieme est le dernier »,
   donc il y en a trois. Une variable posee a la main dans les quatre gabarits
   qui rendent ce composant aurait fini par se desaccorder de l'un d'eux. */
.sous-onglets .segmented:has(> :nth-child(2):last-child) { --n: 2; }
.sous-onglets .segmented:has(> :nth-child(3):last-child) { --n: 3; }
.sous-onglets .segmented:has(> :nth-child(4):last-child) { --n: 4; }
.sous-onglets .segmented::before {
  content: ''; position: absolute; z-index: 0;
  top: 2px; bottom: 2px; left: 2px;
  width: calc((100% - 4px) / var(--n, 2));
  border-radius: var(--radius-xs);
  background: color-mix(in oklab, var(--accent) 16%, var(--surface-2));
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 42%, transparent);
  transform: translateX(calc(var(--onglet, 0) * 100%));
  transition: transform .3s cubic-bezier(.32, 1.12, .5, 1);
}
.sous-onglets .segmented:has(> :nth-child(1).on) { --onglet: 0; }
.sous-onglets .segmented:has(> :nth-child(2).on) { --onglet: 1; }
.sous-onglets .segmented:has(> :nth-child(3).on) { --onglet: 2; }
.sous-onglets .segmented:has(> :nth-child(4).on) { --onglet: 3; }
.sous-onglets .segmented:not(:has(> .on))::before { opacity: 0; }

@keyframes appui-barre {
  0%   { transform: scale(1); }
  30%  { transform: scale(.978); }
  100% { transform: scale(1); }
}
.sous-onglets .segmented.tape { animation: appui-barre .38s cubic-bezier(.3, 1.5, .5, 1); }
@media (prefers-reduced-motion: reduce) {
  .sous-onglets .segmented::before { transition: none; }
  .sous-onglets .segmented.tape { animation: none; }
}
.sous-onglets .segmented button { position: relative; z-index: 1; }
.sous-onglets .segmented button.on {
  background: none;
  color: var(--text-primary);
}
@media (hover: hover) { .sous-onglets .segmented button:not(.on):hover {
  background: color-mix(in oklab, var(--accent) 8%, transparent);
  color: var(--text-primary);
} }

@media (max-width: 900px) {
  /* Le collage reste sous la barre deployee, et ne bouge jamais : c'est la
     translation, plus haut, qui remonte cette bande quand la barre se retire. Un
     `top` qui suivrait l'etat de la barre ferait recalculer la contrainte de
     collage a chaque image du mouvement. */
  .sous-onglets { top: var(--h-barre); }

  /* Le bandeau d'un etablissement se colle sous les sous-onglets, et non a la
     meme hauteur qu'eux.

     Il portait `top: calc(54px + …)`, exactement la valeur des sous-onglets : les
     deux se disputaient la bande sous la barre du haut, et le bandeau gagnait.
     Le point de rupture avait glisse lui aussi — la regle vivait sous 900 px
     quand la barre du haut n'existe qu'a partir de 768 : entre les deux, le
     bandeau se collait a 54 px sous une topbar de 72, donc derriere elle.

     `:has()` plutot qu'une valeur unique : une page sans sous-onglets ne doit pas
     laisser 53 px de vide au-dessus de son premier bandeau. La condition se lit
     dans le DOM, elle ne se declare pas dans un gabarit. */
  .view:has(.sous-onglets) .cpt-gtitre {
    top: calc(var(--h-barre) + var(--h-sous-onglets));
  }
  .view:not(:has(.sous-onglets)) .cpt-gtitre { top: var(--h-barre); }
  .sous-onglets .segmented { display: flex; width: 100%; }
  .sous-onglets .segmented button { flex: 1; padding: 8px 12px; }
}

.toast {
  position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%);
  background: var(--text-primary); color: var(--page); padding: 10px 18px; border-radius: var(--radius-pill);
  font-size: 13px; font-weight: 550; z-index: 100; box-shadow: var(--shadow);
}
.toast-action {
  position: relative;
  margin-left: 12px; padding: 0 0 0 12px;
  border: 0; border-left: 1px solid color-mix(in oklab, var(--page) 28%, transparent);
  background: none; color: var(--page);
  font: inherit; font-weight: 650; text-decoration: underline;
  text-underline-offset: 2px; cursor: pointer;
}
/* La zone tactile depasse le texte. Un pouce vise mal, et il vise ce bouton-ci
   juste apres avoir compris qu'il s'est trompe de mois : 17 px de haut, c'est
   la hauteur des lettres, pas celle d'un doigt.

   Par un pseudo-element et non par du remplissage : `padding` grandit la boite
   de ligne et la pilule passait de 37 a 54 px de haut. Une marge negative ne la
   rattrape pas — elle n'agit pas sur la hauteur de la boite de ligne d'un
   element en ligne. Ce calque-la est hors flux, donc invisible a la mise en
   page, et les clics qu'il recoit sont ceux du bouton. */
.toast-action::after {
  content: ''; position: absolute;
  top: -12px; bottom: -12px; left: 0; right: -8px;
}
@media (hover: hover) { .toast-action:hover { opacity: .75; } }

@media (max-width: 1180px) {
  .g-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .g-hero, .g-2-1 { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 900px) {
  body { grid-template-columns: minmax(0, 1fr); }
  .sidebar { position: static; height: auto; max-width: 100%; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 10px; padding: 12px; }
  .brand { padding: 0; }
  .burger { display: none; }

  .nav {
    position: fixed; left: 0; right: 0; bottom: 0; top: auto; z-index: 60;
    max-height: min(78%, 560px);
    flex-direction: column; gap: 4px;
    padding: 24px 16px calc(78px + var(--zone-geste));
    background: var(--sidebar);
    border-radius: 20px 20px 0 0;
    box-shadow: var(--ombre-ovale);
    overflow-y: auto; overscroll-behavior: contain;
    transform: translateY(100%);
    transition: transform .24s cubic-bezier(.16, 1, .3, 1);
    visibility: hidden;
  }
  body.nav-open .nav { transform: none; visibility: visible; }

  .nav::after {
    content: ''; position: absolute; left: 50%; top: 9px;
    width: 38px; height: 4px; margin-left: -19px; border-radius: var(--radius-pill);
    background: var(--sidebar-ink); opacity: .2; pointer-events: none;
  }
  body.nav-open .sidebar { z-index: 60; }
  @media (prefers-reduced-motion: reduce) { .nav { transition: none; } }
  .nav a { padding: 13px 14px; font-size: 15px; }
  .nav a .ic { display: inline-block; width: 20px; font-size: 14px; }
  .nav a svg.ic { width: 20px; height: 20px; }

  /* Sous la barre latérale (z-index 40), pas au-dessus : celle-ci étant en
     `fixed`, elle forme son propre contexte d'empilement — le z-index du
     menu qu'elle contient ne peut pas dépasser le sien. */
  .nav-backdrop {
    display: block; position: fixed; inset: 0; z-index: 30;
    background: rgba(0,0,0,.5); backdrop-filter: blur(2px);
  }
  .nav-backdrop[hidden] { display: none; }

  .burger { position: relative; z-index: 70; }
  .nav a { white-space: nowrap; padding: 7px 10px; }
  .sidebar-foot { display: none; }
  /* 10px : le retrait reellement applique aux liens ici — `.nav a` est
     redeclare plus bas dans ce meme bloc, et c'est cette valeur qui gagne. */
  .nav-outils { --nav-retrait: 10px; --oeil-marge: 10px; padding-top: 10px; padding-bottom: 4px; }
  .nav-outils .btn-oeil { padding: var(--oeil-marge); }
  .nav-outils .btn-oeil svg, #themeToggle .ic { width: 20px; height: 20px; font-size: 18px; line-height: 20px; }

  /* La barre du haut se retire quand on descend, revient quand on remonte.

     Elle etait clouee a l'ecran en permanence, pour que le burger reste
     atteignable — un burger qui n'existe plus, remplace par le bouton profil et
     par la barre du bas. Ce qu'elle gelait, mesure a 375 x 812 : 54 px, plus 53 de
     sous-onglets, plus 94 pour l'ovale du bas et sa garde, soit 201 px, un quart
     de l'ecran, et 44 de plus pour la barre de statut d'un iPhone.

     Le motif est celui du marche, et il tient en une condition : ce qui part au
     defilement descendant revient au premier geste vers le haut, sans qu'il faille
     remonter en haut de page. C'est `enterAlways` de Material 3, et c'est ce que
     font Instagram, X, LinkedIn et la barre d'adresse de Chrome. Une barre qui ne
     reviendrait qu'en haut de page ne serait pas un standard, ce serait une perte.

     Ce qui colle sous elle la suit par une translation de meme duree, plus bas dans
     ce bloc : sans quoi les sous-onglets resteraient a 54 px du bord en laissant
     une bande vide au-dessus d'eux.

     `env(safe-area-inset-top)` reste dans le remplissage : en application
     installee, le haut de la page passe sous l'encoche. */
  .sidebar {
    position: fixed; top: 0; left: 0; right: 0; z-index: 40;
    height: var(--h-barre);
    padding: 8px 12px; padding-top: calc(8px + env(safe-area-inset-top, 0px));
    gap: 8px; flex-wrap: nowrap;
    /* Rien ne se rogne ici, et c'est un correctif.

       Le défilement interne posé sur la barre latérale — pour qu'un écran court
       ne rende pas son pied inatteignable — vaut pour le rail vertical de
       l'ordinateur. Sur téléphone, ce même sélecteur est une bande de 54 px, et
       `overflow: auto` en fait un conteneur qui découpe tout ce qui dépasse.

       Le panneau des notifications vit dedans, en absolu, et descend à 306 px :
       il était donc entièrement rogné. « Le bouton notifications ne marche plus
       sur téléphone, rien ne s'affiche » — il marchait, on ne le voyait pas.
       Mesure : boîte du panneau de 56 à 306, mais au milieu de cette boîte c'est
       la page qui répondait au doigt.

       C'est le cas qu'on ne voit jamais sur un grand écran, où la barre fait
       toute la hauteur et n'a rien à découper. */
    overflow: visible;
    /* Plus de bandeau : le logo, le titre et les deux boutons se posent sur la
       page, et rien ne les encadre.

       Le fond etait `var(--sidebar)`, soit #050506 contre #08090b pour la page :
       trois valeurs de rouge d'ecart, assez pour dessiner une bande en haut de
       l'ecran sans qu'aucune bordure ne le demande. « Il y a du noir autour des
       deux boutons, ça fait bizarre, c'est un bandeau noir. » Et le bas de l'ecran
       porte du verre pendant que le haut portait un aplat : deux materiaux pour
       deux bandes du meme ecran.

       Ce que ce fond servait — detacher la barre du contenu qui passe dessous — ne
       vaut plus depuis qu'elle se retire au defilement. Reste une fenetre de 64 px,
       le seuil avant retrait, pendant laquelle du contenu peut effleurer le titre.
       Les deux boutons portent leur propre disque, ils ne dependent pas de ce
       fond ; le titre est ce qui se paie, et il est redit par l'onglet allume en
       bas de l'ecran. */
    background: transparent;
    border-right: none; border-bottom: none;
    /* Le mouvement du retour ne se declare pas ici : la regle de bascule de theme
       vit plus loin dans la feuille et ecraserait cette transition, `transform`
       compris. Il est donc pose la-bas, avec les couleurs, dans une declaration
       que rien ne suit. Chercher « couleurs et retrait dans la meme declaration ».

       Pas de `will-change: transform` ici, et ce n'est pas un oubli. Il promeut la
       barre sur sa propre couche — ce qu'on veut — mais il cree aussi un bloc
       conteneur pour tous ses descendants en `position: fixed`. Or le tiroir des
       reglages est un enfant de cette barre : il se calait donc sur une boite de
       54 px en haut de l'ecran au lieu de la fenetre, et s'ouvrait hors champ.
       « Le menu parametres ne fonctionne plus, il apparait hors ecran sur tel. »

       Le `translate3d` du retrait produit le meme effet, mais seulement pendant
       qu'il est applique — et le bouton qui ouvre ce tiroir vit precisement dans la
       barre retiree, donc hors d'atteinte a ce moment-la. `app.js` rend la barre
       avant d'ouvrir le tiroir, ce qui ferme le dernier chemin. */
  }
  .main { padding-top: var(--h-barre); }

  body.haut-cache .sidebar {
    /* `translate3d` et non `translateY` : la troisieme coordonnee, meme nulle,
       force le compositeur a prendre l'element en charge. C'est le remede des
       animations qui accrochent sur iOS, et il ne coute rien ailleurs. */
    transform: translate3d(0, -100%, 0);
    transition-duration: .2s;
    transition-timing-function: cubic-bezier(.4, 0, 1, 1);
  }
  /* Ce qui colle sous la barre suit le meme mouvement, aux memes durees et aux
     memes courbes : deux vitesses feraient decrocher les sous-onglets de la barre
     qu'ils accompagnent, et c'est exactement ce qui se lit comme un defaut.

     Par `transform` et non par `top`, et c'est le correctif de fluidite. Animer
     `top` sur un element collant fait recalculer sa contrainte de collage a chaque
     image, sur le fil principal ; une translation vit sur le compositeur. Le
     collage reste donc fixe, sous la barre deployee, et c'est la translation qui
     remonte ces bandes quand elle part.

     Rien ne bouge en haut de page : ces regles n'existent que dans l'etat retire,
     lequel demande d'avoir defile au-dela du seuil. Ces bandes sont alors deja
     collees, donc les translater ne les decale pas par-dessus un contenu.

     La course vaut le dessin de la barre et jamais sa hauteur totale : l'encoche
     reste, et elle reste par construction. C'est ce qui la met hors de portee
     d'une distraction — j'ai perdu cette garde une fois, en ecrivant un zero, et
     les sous-onglets sont remontes derriere l'heure de l'iPhone. */
  .sous-onglets {
    transition: transform .42s cubic-bezier(.3, .55, .25, 1);
  }
  body.haut-cache .sous-onglets {
    transform: translate3d(0, calc(-1 * var(--h-barre-dessin)), 0);
    transition-duration: .2s;
    transition-timing-function: cubic-bezier(.4, 0, 1, 1);
  }
  @media (prefers-reduced-motion: reduce) {
    .sidebar, .sous-onglets,
    body.haut-cache .sidebar, body.haut-cache .sous-onglets { transition: none; }
  }

  /* Le bandeau d'etablissement ne se translate pas : il se gare plus haut.

     Il etait dans la regle ci-dessus, et c'etait un contresens de nature. Les
     sous-onglets sont une bande unique, en haut de l'ecran, toujours collee : la
     translater, c'est la deplacer. Les bandeaux, eux, sont six, chacun dans sa
     carte, et la plupart sont encore dans le flux de leur groupe pendant que la
     barre se retire. Les translater tous de 54 px sortait chaque titre de sa
     carte — et `.cpt-groupe` porte `overflow: clip`, donc il ne reapparaissait
     meme pas au-dessus : il etait **decoupe**. La bande restait, vide, parce que
     la place d'un element colle reste dans le flux. « Enorme bug en defilant
     comptes vers le bas sur mobile, les titres de banque disparaissent. »

     Ce qui doit suivre la barre n'est pas le bandeau, c'est l'endroit ou il se
     gare. On deplace donc `top` et jamais l'element : hors collage, `top` ne fait
     rien du tout, ce qui est exactement la garantie qui manquait.

     Sans transition, a dessein. Animer le `top` d'un element colle fait
     recalculer sa contrainte a chaque image, sur le fil principal — c'est
     precisement ce que la session precedente a retire de ces bandes. Le bandeau
     gare remonte donc d'un coup, puis les sous-onglets glissent par-dessus et le
     decouvrent : il passe sous eux le temps du geste, ce qui se lit comme un
     balayage plutot que comme un saut. Le reste du temps il n'y a rien a animer,
     puisque `top` ne s'applique qu'a un element colle.

     `var(--h-barre) - var(--h-barre-dessin)` vaut l'encoche, et rien d'autre :
     la meme garde que celle des sous-onglets, obtenue par construction plutot que
     par un zero ecrit a la main. */
  body.haut-cache .view:has(.sous-onglets) .cpt-gtitre {
    top: calc(var(--h-barre) - var(--h-barre-dessin) + var(--h-sous-onglets));
  }
  body.haut-cache .view:not(:has(.sous-onglets)) .cpt-gtitre {
    top: calc(var(--h-barre) - var(--h-barre-dessin));
  }

  /* Une sous-page garde sa barre, et c'est l'exception que tient iOS aussi : la
     barre qui porte « retour » ne se cache pas. Elle y porte aussi le titre de la
     fiche regardee, que la barre du bas ne dit pas — celle-ci designe la rubrique,
     pas l'ecran. Une commande ne part pas comme une etiquette.

     Le refus vit dans `app.js`, qui ne pose pas la classe : le faire ici
     demanderait de neutraliser en meme temps le retrait et la variable, soit deux
     regles qui se contredisent a lire. */

  .topbar { display: none; }

  .brand-sous, .brand-text strong { display: none; }
  .brand-vue {
    display: block; font-size: 15px; font-weight: 680;
    color: var(--sidebar-ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .view { padding: 14px 16px 48px; gap: 14px; }
  .g-2, .g-3, .g-4 { grid-template-columns: minmax(0, 1fr); }
  .g-tuiles { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .g-tuiles > :last-child:nth-child(odd) { grid-column: 1 / -1; }
  .g-tuiles .tile { padding: 12px 13px; }
  .g-tuiles .t-value { font-size: 19px; }
  /* Ce texte n'est pas une aide, c'est la base du chiffre au-dessus, et il
     revient donc. Il a ete masque par `font-size: 0` pour deux raisons dont
     aucune ne tient plus : tronque il faisait du bruit — il ne l'est plus, il se
     replie sur deux lignes — et entier il donnait a chaque tuile sa propre
     hauteur — `grid-auto-rows: 1fr`, deux lignes plus bas, les egalise depuis.

     Ce qu'il coute : 56 px sur la grille des quatre tuiles de Budget, mesure a
     375 px. Ce qu'il rend : « 1 404 € » n'est une moyenne que si l'on sait « sur
     7 mois clos, hors charges fixes ». Un dividende sans son diviseur n'est pas
     un chiffre plus court, c'est un chiffre faux — et la regle de la maison veut
     qu'un rapport dise sur quelle base il se calcule, sur le petit ecran comme
     sur le grand.

     Dix et demi et non onze et demi : la tuile ne fait que 167 px de large. */
  .g-tuiles .t-meta { font-size: 10.5px; flex-wrap: wrap; gap: 4px 6px; }
  .g-tuiles { grid-auto-rows: 1fr; }
  .view, .topbar { padding-left: 16px; padding-right: 16px; }

  .view, .view > *, .grid, .grid > *, .card, .hero, .tile { min-width: 0; }
  .table-wrap { max-width: 100%; }

  .topbar { flex-wrap: wrap; align-items: flex-start; }
  .topbar-actions { width: 100%; flex-wrap: wrap; }
  .topbar h1 { font-size: 19px; }
  .hero-value { font-size: 34px; }
  .card, .hero { padding: 15px; }

  .card-head { flex-wrap: wrap; gap: 6px 10px; }
  /* Un bouton seul qui passe a la ligne reste a droite. `space-between` distribue
     ligne par ligne : seul sur la seconde, il partait a gauche, et la carte
     voisine dont le titre est plus court gardait le sien a droite. Deux cartes
     l'une sous l'autre avec leur action a deux endroits differents.

     Une paire, elle, va a gauche, et ce n'est pas une exception de confort. Un
     bouton seul est un accessoire du titre : il en suit le bord. Une paire de
     commandes est un bloc a elle seule, et un en-tete qui la replie porte deja
     d'autres blocs replies au-dessus — les plages du journal des ventes, le
     selecteur d'annee des apports — tous cales a gauche. Alignee a droite, elle
     etait la seule rangee de l'en-tete a ne pas partager son bord avec les
     autres. `space-between` la laisse au depart de sa ligne, il n'y a donc rien
     a poser. */
  .card-head > .btn:last-child { margin-left: auto; }
  .card-head .row { flex-wrap: wrap; }
  .row { flex-wrap: wrap !important; }
  .hero-deltas { gap: 12px 18px; }

  /* Listes descriptives : le libelle garde sa largeur, la valeur se replie.

     La regle precedente faisait l'inverse — libelle flexible, valeur
     intouchable — et `overflow-wrap: anywhere` sur le libelle ramenait sa
     largeur minimale a une seule lettre. Une valeur longue, comme le nom
     officiel d'un ETF, prenait alors toute la place et le libelle
     s'imprimait verticalement, un caractere par ligne.

     `break-word` coupe un mot trop long sans autoriser cette reduction :
     contrairement a `anywhere`, il ne compte pas dans la largeur minimale. */
  .kv { grid-template-columns: minmax(0, 1fr) auto; gap: 5px 10px; }
  .kv dt { min-width: 0; overflow-wrap: break-word; }

  .kv-texte { grid-template-columns: auto minmax(0, 1fr); }
  .kv-texte dt { min-width: max-content; overflow-wrap: normal; }

  /* La colonne du pourcentage tient aussi sous 900 px : sans elle dans la
     grille, le `.flow-pct` du panneau passerait a la ligne suivante et chaque
     poste occuperait deux lignes.

     C'est le « gros espace vide a droite » signale trois fois. Il ne se voyait
     pas sur la carte des charges fixes, qui remplit sa quatrieme colonne — donc
     la moitie des cartes concernees paraissait juste. */
  .flow-row { grid-template-columns: 88px minmax(0, 1fr) auto; }
  .flow-row:has(.flow-pct) { grid-template-columns: 88px minmax(0, 1fr) auto 42px; }

  .chk { grid-template-columns: 20px minmax(0, 1fr); }
  .chk .btn { grid-column: 2; justify-self: start; }

  /* iOS Safari zoome automatiquement sur un champ dont le texte fait moins
     de 16px, et **ne dezoome jamais ensuite**. C'est la cause la plus probable
     du « si on zoom ça reste zoomé » : on touche un champ, la page se recadre,
     et rien ne la remet.

     `!important` est ici une garantie de comportement, pas une commodite. La
     regle etait deja la et deux selecteurs la battaient **par specificite**, pas
     par ordre : `select.annee` (0,1,1) et `table.editable input.derived`
     (0,2,2) contre `select` (0,0,1). Mesure a 375 px : sept champs sous les
     16 px, dont les six selecteurs d'annee. Une taille decorative ne doit pas
     pouvoir rallumer le zoom automatique, et la liste des exceptions n'a pas a
     etre tenue a la main — elle se serait perimee au premier habillage suivant. */
  input, select, textarea { font-size: 16px !important; }

  .table-wrap { -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; }

  /* Un tableau hors conteneur défilant reste un tableau : passé en `block`
     il cessait d'occuper sa carte et ses colonnes se tassaient à gauche,
     laissant la moitié de la largeur vide. Il garde donc sa mise en page de
     tableau, occupe toute la carte, et c'est le texte qui se replie. */
  .card > table, .grid > div > table, .data-view > table {
    width: 100%; max-width: 100%; table-layout: auto;
  }
  .card > table td, .grid > div > table td, .data-view > table td {
    white-space: normal; overflow-wrap: anywhere;
  }

  /* Tableau etroit tenu dans sa carte, sans conteneur defilant. Un champ de
     saisie porte une largeur intrinseque — vingt caracteres par defaut — que
     la mise en page automatique respecte : la colonne « Source » reclamait
     154 px des 311 disponibles, le tableau debordait de la carte, et rien ne
     pouvait le rattraper puisque `body` est en `overflow-x: clip`. La croix de
     suppression tombait derriere le bord de l'ecran.
     La regle suit celle du dessus, qui impose `table-layout: auto` avec la
     meme specificite : declaree avant, la mienne perdait le depart.
     Reservee aux tableaux qui doivent tenir — les grands tableaux de Budget
     vivent dans un `.table-wrap` et gardent leurs colonnes lisibles. */
  table.table-serree { table-layout: fixed; width: 100%; }
  table.table-serree input, table.table-serree select { width: 100%; min-width: 0; }
  table.table-serree td, table.table-serree th { padding-left: 4px; padding-right: 4px; }

  table.cols-nom-chiffres th:nth-child(2) { width: 84px; }
  table.cols-nom-chiffres th:nth-child(3) { width: 44px; }
  table.cols-nom-chiffres td:last-child,
  table.cols-nom-chiffres th:last-child { width: 34px; }

  table.cols-nom-action, table.cols-nom-action tbody,
  table.cols-nom-action tr, table.cols-nom-action td { display: block; width: auto; }
  table.cols-nom-action tr {
    padding: 10px 0; border-bottom: 1px solid var(--grid);
  }
  table.cols-nom-action tr:last-child { border-bottom: none; }
  table.cols-nom-action td { border: none; padding: 0; text-align: left; white-space: normal; }
  table.cols-nom-action td.name { overflow-wrap: anywhere; }
  table.cols-nom-action td:last-child { margin-top: 8px; }
  /* Un menu s'etire parce que ses options portent de longs noms et qu'il les
     tronque sans rien dire. Un bouton de deux mots n'a rien a etirer : etendu
     sur toute la carte, il devient un aplat clair par resultat, et six titres
     cherches se lisent comme six boutons separes par du texte. Il garde donc sa
     largeur naturelle ; `btn sm` fait deja plus de 24 px de haut, la cible du
     doigt est tenue. */
  table.cols-nom-action select { width: 100%; }

  .sticky-col { position: static; }

  .table-wrap table.editable { min-width: 880px; }   /* une colonne de plus : Classe + Rôle */
  table.editable th, table.editable td { white-space: nowrap; }
  /* Sauf la prose : cette regle-ci empeche un montant de se couper en deux, ce
     qui est juste, mais elle est plus specifique que `td.prose` et lui reprenait
     son repli. Une phrase se replie a toutes les largeurs, et d'autant plus sur
     un ecran etroit. */
  table.editable td.prose { white-space: normal; }
  table.editable input, table.editable select { min-width: 4.5em; }
  table.editable td.name input { min-width: 9em; }
  table.editable select[data-path$=".account"] { min-width: 9.5em; }
  table.editable select[data-path$=".category"] { min-width: 7em; }
  .sym-cell input { min-width: 6em; }
}

@media (max-width: 900px) {

  /* Le logo reste au-dessus du panneau qu'il ouvre. Le tiroir couvre tout
     l'écran et le recouvrait : plus rien ne le refermait, puisque c'est lui le
     bouton, et le voile est sous le panneau. Ne restait que le balayage, qui
     ne s'annonce pas. Même traitement que le burger sur tablette, pour la même
     raison — et `.nav` réserve déjà 66 px de haut pour ce passage. */
  /* Le logo redevient un logo : c'est le bouton profil qui ouvre la feuille. La
     barre garde `flex: 1` pour que le nom de la page prenne la place et se
     tronque proprement. */
  .brand { padding: 2px 6px 2px 0; flex: 1; }

  body.sous-page .brand-mark { display: none; }
  body.sous-page .retour { display: grid; }
  .retour {
    width: 34px; height: 38px; flex: none; padding: 0; margin-right: -2px;
    place-items: center; cursor: pointer;
    background: none; border: none; color: var(--sidebar-ink);
    border-radius: var(--radius-sm);
  }
  .retour:active { background: var(--sidebar-hover); }
  .retour:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
  .retour svg {
    width: 22px; height: 22px; display: block;
    fill: none; stroke: currentColor; stroke-width: 2.1;
    stroke-linecap: round; stroke-linejoin: round;
  }

  .retour-page { display: none; }

  .entete-actions { display: flex; align-items: center; gap: 8px; position: relative; }
  .btn-rond {
    width: 38px; height: 38px; flex: none; padding: 0;
    display: grid; place-items: center; position: relative;
    border-radius: var(--radius-pill); cursor: pointer;
    border: 1px solid var(--sidebar-border); background: var(--sidebar-hover);
    color: var(--sidebar-ink);
    transition: background .15s, transform .12s;
  }
  .btn-rond:active { transform: scale(.94); }
  .btn-rond svg {
    width: 19px; height: 19px;
    fill: none; stroke: currentColor; stroke-width: 1.7;
    stroke-linecap: round; stroke-linejoin: round;
  }
  .btn-rond[aria-expanded="true"] { background: var(--sidebar-actif, var(--sidebar-border)); }
  .btn-rond:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

  .pastille-ronde {
    position: absolute; top: 1px; right: 1px;
    width: 9px; height: 9px; border-radius: var(--radius-pill);
    background: var(--critical);
    box-shadow: 0 0 0 2px var(--sidebar);
  }

  .popover-notifs {
    position: absolute; top: calc(100% + 10px); right: 0; z-index: 70;
    width: min(330px, calc(100vw - 24px));
    max-height: min(60vh, 420px); overflow-y: auto; overscroll-behavior: contain;
    background: var(--surface-1); color: var(--text-primary);
    border: 1px solid var(--border-strong); border-radius: var(--radius);
    box-shadow: var(--ombre-fenetre);
    padding: 6px;
    transform-origin: top right;
    animation: notifs-entree .16s cubic-bezier(.2, 1, .3, 1);
  }
  .popover-notifs[hidden] { display: none; }
  @keyframes notifs-entree {
    from { opacity: 0; transform: scale(.94) translateY(-6px); }
    to   { opacity: 1; transform: none; }
  }
  @media (prefers-reduced-motion: reduce) {
    .popover-notifs { animation: none; }
  }

  .notif-tete {
    display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
    padding: 6px 8px 8px;
  }
  .notif-tete b { font-size: 13.5px; }
  .notif-tete span { font-size: 11.5px; color: var(--muted); margin-left: auto; }
  .notif-reglages { flex: none; padding: 2px 4px; opacity: .6; }
  @media (hover: hover) { .notif-reglages:hover { opacity: 1; } }
  /* `fill` et `stroke` viennent de l'attribut du SVG : trois points pleins, pas
     trois anneaux. La regle generale des icones au trait ne s'applique donc pas
     ici, et c'est voulu. */
  .notif-reglages svg { width: 16px; height: 16px; display: block; }

  .notif-ligne {
    display: flex; align-items: flex-start; gap: 2px; border-radius: var(--radius-sm);
  }
  @media (hover: hover) { .notif-ligne:hover { background: var(--surface-2); } }
  .notif-ligne + .notif-ligne { border-top: 1px solid var(--border); border-radius: 0; }
  .notif-corps {
    display: flex; gap: 9px; flex: 1; min-width: 0; text-align: left;
    padding: 9px 4px 9px 8px; border: none; background: none; border-radius: var(--radius-sm);
    font: inherit; color: inherit; cursor: pointer;
  }
  .notif-x { margin: 6px 5px 0 0; flex: none; opacity: .55; }
  @media (hover: hover) { .notif-x:hover { opacity: 1; } }
  @media (hover: hover) {
    .notif-x { opacity: 0; }
    .notif-ligne:hover .notif-x, .notif-x:focus-visible { opacity: 1; }
  }
  .notif-ic { flex: none; width: 16px; font-size: 13px; line-height: 1.5; }
  .notif-ligne.n-action .notif-ic { color: var(--warning); }
  .notif-ligne.n-error  .notif-ic { color: var(--critical); }
  .notif-ligne.n-warn   .notif-ic { color: var(--serious); }
  .notif-ligne.n-info   .notif-ic { color: var(--muted); }
  .notif-txt { min-width: 0; }
  .notif-txt b { display: block; font-size: 13px; font-weight: 600; }
  .notif-txt span { font-size: 12px; color: var(--text-secondary); }
  .notif-vide { padding: 16px 10px 18px; text-align: center; color: var(--muted); font-size: 13px; }

  .notif-fermer {
    display: block; width: 100%; padding: 5px 0 3px; margin-top: 2px;
    border: none; background: none; cursor: pointer;
    border-top: 1px solid var(--border);
    color: var(--muted); border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  }
  @media (hover: hover) { .notif-fermer:hover { background: var(--surface-2); color: var(--text-secondary); } }
  .notif-fermer svg {
    width: 20px; height: 20px; margin: 0 auto; display: block;
    fill: none; stroke: currentColor; stroke-width: 1.9;
    stroke-linecap: round; stroke-linejoin: round;
  }

  /* Une pilule flottante, translucide, detachee des bords.

     Elle etait un bandeau plein colle en bas, separe du contenu par un filet.
     Le detacher change deux choses au-dela du style : la page continue de se
     voir sous la barre, donc on garde le sentiment de ce qui defile, et les
     coins de l'ecran redeviennent du vide — un bandeau pleine largeur donne
     un plancher, une pilule laisse respirer.

     Le flou fait le travail que faisait le filet : c'est lui qui detache la
     barre du contenu, pas une bordure.

     L'opacite est a 22 % et le flou a 12 px. Cinq reglages ont mene la : 72, 62,
     46, 30, puis ceci. Les deux premiers montaient le flou a mesure qu'ils
     baissaient le fond, par prudence, et c'etait le mauvais reflexe — ce qui
     donne l'impression de lire a travers, c'est l'opacite basse, tandis qu'un
     flou plus lourd cache davantage au lieu de devoiler. Les deux descendent
     donc ensemble, et on lit maintenant ce qui passe sous la barre.

     C'est le plancher. En dessous, le fond ne separe plus assez la barre du
     contenu : le liseré et l'ombre portee restent seuls a la dessiner, et sur
     une carte claire elle cesse de se lire comme un objet pose devant.

     A ce niveau, c'est le texte qui porte la lisibilite, plus le fond. Les
     libelles et les traits des icones ont une ombre portee courte, la methode
     des titres poses sur une image : invisible sur fond sombre, elle tient le
     contraste quand un aplat clair passe dessous. Sans elle, 30 % rendrait
     « Allocation » illisible des que la courbe monte. C'est aussi ce qui permet
     a une barre de cinq libelles de 10,5 px d'aller aussi bas qu'une barre qui
     n'aurait que des icones. Verifie a 375 px dans les deux themes, avec les
     aplats du graphique d'evolution derriere.

     L'insert de securite passe du `padding` au `bottom` : il ne gonfle plus la
     hauteur de la barre, il l'ecarte du bord. Sur un telephone a encoche, la
     pilule se pose donc au-dessus de la barre de gestes au lieu de l'englober. */
  /* Le fond et le flou n'existent que dans l'ovale. Sous l'ovale, rien.

     Le diagnostic visait la mauvaise piece. Ce n'etait pas la pilule qui laissait
     une bande morte, c'etait sa jupe : 200 px de `var(--page)` peints sous elle,
     qui masquaient le contenu au lieu de le laisser defiler. Une barre pleine
     largeur a bien supprime la bande, mais en la remplaçant par un flou d'un bord
     a l'autre — « tout le bas apparait flou au lieu de juste l'ovale ».

     Donc : la pilule revient, et plus rien n'est peint autour d'elle. Le contenu
     defile jusqu'au bord bas de l'ecran, zone de gestes comprise. C'est ce qu'on
     veut voir, et c'est exactement ce que la jupe empechait.

     Pas de jupe, et pas besoin d'une. Elle existait pour le cas ou le navigateur
     decale la fenetre visible — clavier qui se referme, barre d'outils qui se
     replie — et decouvre une bande sous un element en `fixed`. `html, body`
     portent `background: var(--page)` : le canvas est deja peint de la couleur de
     la page, rien ne peut y virer au noir. */
  .tabbar {
    display: grid; grid-template-columns: repeat(5, 1fr);
    position: fixed; z-index: 65;
    left: 10px; right: 10px;
    /* La zone de gestes ecarte la pilule du bord par `bottom` et non par
       `padding` : elle ne gonfle pas la hauteur de la barre, elle la souleve
       au-dessus de l'endroit ou le systeme attend son geste.

       La zone de gestes ne se paie plus en entier, et c'est le troisieme reglage
       de la journee : 43 px sous la pilule, puis 38, puis 20. « Trop haute », deux
       fois, puis « legerement trop haute ».

       Ce qui borne la descente n'est pas la zone reservee mais le trait du
       systeme : sur un iPhone, il fait 5 px de haut et vit dans les treize
       premiers pixels du bord. La moitie de la zone plus trois pixels laisse donc
       sept pixels entre le bas de l'ovale et le haut de ce trait. Aller plus bas
       les ferait se toucher, et deux barres qui se chevauchent se lisent comme un
       defaut d'alignement, pas comme un dessin serre.

       Le `max()` garde le cas inverse, celui qu'on ne voit pas en developpant sur
       un telephone a encoche : `env(safe-area-inset-bottom)` vaut zero sur un
       iPhone SE, sur la plupart des Android et dans toute fenetre ou le systeme ne
       reserve rien. Sans plancher, la pilule y collerait au bord physique. */
    bottom: max(10px, calc(3px + var(--zone-geste) / 2));
    border-radius: var(--radius-pill);
    background:
      linear-gradient(to bottom,
        rgba(255, 255, 255, .10),
        rgba(255, 255, 255, 0) 58%),
      color-mix(in oklab, var(--surface-1) 11%, transparent);
    backdrop-filter: blur(18px) saturate(190%) brightness(1.06);
    -webkit-backdrop-filter: blur(18px) saturate(190%) brightness(1.06);
    border: 1px solid color-mix(in oklab, var(--border-strong) 70%, transparent);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .16),
                inset 0 -1px 0 rgba(0, 0, 0, .22),
                0 12px 32px -10px rgba(0, 0, 0, .55),
                0 2px 6px -2px rgba(0, 0, 0, .35);
  }
  .tabbar a, .tabbar button {
    position: relative;
    display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
    min-height: 52px; padding: 7px 2px 8px;
    background: none; border: none; cursor: pointer; text-decoration: none;
    font: inherit; font-size: 10.5px; font-weight: 550; letter-spacing: .01em;
    text-shadow: 0 1px 2px color-mix(in oklab, var(--page) 78%, transparent),
                 0 0 7px color-mix(in oklab, var(--page) 55%, transparent);
    /* L'encre discrète des annotations est trop faible ici : à 10,5 px, une
       destination doit se lire, pas se deviner. Mesuré 3,2 contre le fond
       avec `--muted`, 6,4 avec celle-ci. */
    color: var(--text-secondary);
    transition: color .14s;
  }
  .tabbar svg {
    width: 22px; height: 22px; display: block;
    fill: none; stroke: currentColor; stroke-width: 1.7;
    stroke-linecap: round; stroke-linejoin: round;
  }
  .tabbar a.on, .tabbar button.on { color: var(--text-primary); }
  .tabbar a.on svg, .tabbar button.on svg { color: var(--accent); }

  /* La pastille glisse d'un onglet a l'autre.

     Elle etait posee sur chaque onglet, en `::before` de l'onglet actif. Une
     pastille par onglet ne peut pas se deplacer : elle disparaissait d'un cote
     et reapparaissait de l'autre, et le lien entre les deux etait a deviner.
     Il n'y en a plus qu'une, portee par la barre, et c'est elle qu'on voit
     traverser — le mouvement dit d'ou l'on vient.

     Sa position ne demande aucun JavaScript. La barre est une grille de cinq
     colonnes egales, donc chaque case fait exactement 20 %, et `:has()` lit
     directement dans le DOM quel onglet porte la classe. Rien a tenir
     d'accord : pas de variable a mettre a jour au changement de page, pas
     d'ecouteur de redimensionnement. La regle suit ce qui est vrai.

     Le deplacement passe par `transform` et non par `left` : c'est la seule
     facon d'obtenir un glissement fluide, et le pourcentage d'un `translateX`
     se compte sur la largeur de l'element, non sur celle du parent. D'ou le
     `100% + 4px` — la pastille est en retrait de 4 px, il faut les rendre pour
     avancer d'une case pleine, sinon l'ecart s'accumule et le cinquieme onglet
     se retrouve decale de 16 px. */
  .tabbar::before {
    content: ''; position: absolute;
    /* Cinq pixels de chaque cote, sans la zone de gestes : la pilule est
       soulevee par son `bottom`, pas gonflee par un remplissage, donc sa boite
       ne contient plus les 34 px du geste du systeme. Les avoir comptes ici
       quand ils etaient dans le remplissage etait juste ; les garder
       maintenant remonterait la pastille au-dessus des libelles. */
    top: 5px; bottom: 5px; left: 2px;
    width: calc(20% - 4px);
    border-radius: var(--radius-pill);
    background: color-mix(in oklab, var(--text-primary) 10%, transparent);
    transform: translateX(calc(var(--onglet, 0) * (100% + 4px)));
    transition: transform .34s cubic-bezier(.32, 1.12, .5, 1);
    /* Sous le contenu, au-dessus du fond de la barre : `.tabbar` est un
       contexte d'empilement, donc un enfant negatif se pose entre les deux. */
    z-index: -1;
  }
  /* L'index de l'onglet actif, lu dans le DOM. Cinq regles courtes plutot
     qu'une variable posee par le script : une variable aurait pu se desaccorder
     de la classe `.on`, ces regles ne peuvent pas. */
  .tabbar:has(> :nth-child(1).on) { --onglet: 0; }
  .tabbar:has(> :nth-child(2).on) { --onglet: 1; }
  .tabbar:has(> :nth-child(3).on) { --onglet: 2; }
  .tabbar:has(> :nth-child(4).on) { --onglet: 3; }
  .tabbar:has(> :nth-child(5).on) { --onglet: 4; }
  .tabbar:not(:has(> .on))::before { opacity: 0; }
  @media (prefers-reduced-motion: reduce) {
    .tabbar::before { transition: none; }
  }

  .tabbar a:active, .tabbar button:active { color: var(--text-secondary); }

  .tab-pastille {
    position: absolute; top: 7px; left: 50%; margin-left: 7px;
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--warning);
    box-shadow: 0 0 0 2px var(--surface-1);
  }
  .tab-pastille[hidden] { display: none; }

  .tabbar a.rebond svg { animation: rebond-onglet .32s cubic-bezier(.33, 0, .3, 1); }
  @keyframes rebond-onglet {
    0%   { transform: scale(1); }
    32%  { transform: scale(.9); }
    100% { transform: scale(1); }
  }

  .tabbar.tape { animation: appui-barre .38s cubic-bezier(.3, 1.5, .5, 1); }
  @media (prefers-reduced-motion: reduce) {
    .tabbar.tape { animation: none; }
  }
  @media (prefers-reduced-motion: reduce) {
    .tabbar a.rebond svg { animation: none; }
  }

  .nav-outils { margin-top: 12px; }

  .nav-tete {
    display: flex !important; align-items: center; gap: 12px;
    padding: 14px 12px; margin-bottom: 10px;
    background: var(--sidebar-hover); border: 1px solid var(--sidebar-border);
    border-radius: var(--radius);
  }
  .nav-tete-txt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
  .nav-tete .sb-label { color: var(--sidebar-faible); }
  .nav-tete strong {
    font-size: 22px; font-weight: 720; letter-spacing: -.03em;
    color: var(--sidebar-ink); font-variant-numeric: tabular-nums;
    background: none; border: none; padding: 0; text-align: left; font-family: inherit;
  }

  .view { padding-bottom: calc(76px + var(--zone-geste)); }

  /* Toute vue doit dépasser l'écran, ne serait-ce que d'un pixel.
     iOS replie la barre d'outils de Safari dès qu'on défile, ce qui agrandit
     la zone visible et fait descendre la barre d'onglets, en `fixed`. Une page
     qui ne défile pas la laisse déployée. Comptes était la seule vue dans ce
     cas — 812 px de contenu pour 812 px d'écran, marge exactement zéro, quand
     les autres débordent de 470 à 4 136 px — si bien qu'en y arrivant la barre
     d'onglets remontait de la hauteur de la barre d'outils, puis redescendait
     en repartant. Le saut venait de l'écart entre les pages, pas de la barre.
     Ce pixel garantit le même comportement partout. Il est survenu le jour où
     la brique patrimoine a quitté Comptes : la page est passée juste sous le
     seuil, et le réglage dépend donc du nombre de comptes. On ne le laisse
     plus au hasard. */
  .view { min-height: calc(100vh + 1px); }

  .toast {
    bottom: auto; top: calc(var(--h-barre) + 8px);
    max-width: calc(100% - 32px); text-align: center;
  }

  body { overscroll-behavior-y: contain; }

}

@media print {
  .sidebar, .topbar-actions, .btn { display: none !important; }
  body { grid-template-columns: minmax(0, 1fr); }
}

* { scrollbar-width: thin; scrollbar-color: var(--border-strong) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--border-strong); border-radius: var(--radius-pill);
  border: 3px solid transparent; background-clip: content-box;
}
@media (hover: hover) { *::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: content-box; } }

::selection { background: color-mix(in oklab, var(--accent) 18%, transparent); }
:is(button, a, input, select, summary):focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-xs);
}

/* --- Entrée de vue : les cartes montent en cascade très courte ---
   Vingt-cinq millisecondes d'écart suffisent à donner le sentiment que la page
   se compose, sans jamais faire attendre.

   Sous `.vue-entre` seulement, que render() ne pose qu'à l'arrivée sur une vue.
   La cascade jouait sur chaque re-rendu, donc à chaque frappe dans un champ :
   l'application clignotait précisément quand on s'en servait. */
@keyframes carte-entre {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: none; }
}
.view.vue-entre > * {
  animation: carte-entre .3s cubic-bezier(.22, .61, .36, 1) backwards;
}
.view.vue-entre > *:nth-child(1) { animation-delay: 0ms; }
.view.vue-entre > *:nth-child(2) { animation-delay: 25ms; }
.view.vue-entre > *:nth-child(3) { animation-delay: 50ms; }
.view.vue-entre > *:nth-child(4) { animation-delay: 75ms; }
.view.vue-entre > *:nth-child(n+5) { animation-delay: 90ms; }

/* Les jauges poussent de zéro à leur valeur à l'arrivée, une fois les cartes
   posées. `scaleX` et non `width` : la barre s'étire sans re-calculer la mise
   en page, soixante images par seconde même sur un téléphone modeste. Le
   repère de cible (.reeq-cible) reste immobile : c'est un point fixe, le voir
   glisser dirait qu'il bouge avec la valeur. */
@keyframes barre-pousse { from { transform: scaleX(0); } }
.vue-entre :is(.repart-barre, .hero-barre, .flow-bar, .goal-bar, .reeq-jauge, .role-barre)
  > :is(i:not(.reeq-cible), div) {
  transform-origin: left center;
  animation: barre-pousse .55s cubic-bezier(.22, .61, .36, 1) .14s backwards;
}

.card { transition: border-color .15s ease; }
@media (hover: hover) { .card:hover { border-color: color-mix(in oklab, var(--border-strong) 70%, transparent); } }

.btn { transition: opacity .12s, background .12s, transform .08s; }
.btn:active { transform: scale(.97); }

tbody tr { transition: background .12s ease; }
@media (hover: hover) { tbody tr:hover { background: color-mix(in oklab, var(--surface-2) 60%, transparent); } }

.tick { font-size: 10px; }
.chart-tip {
  border-radius: var(--radius-sm); padding: 10px 12px;
  box-shadow: var(--ombre-flottante);
  backdrop-filter: blur(6px);
}
.chart-tip .tt-head {
  font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); margin-bottom: 6px;
}

body, .card, .tile, .topbar {
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}
/* La barre du haut porte la sienne, couleurs et retrait dans la meme declaration.

   Elle etait dans la liste au-dessus, qui arrive apres le bloc telephone : meme
   specificite, ecrite plus loin, donc c'est elle qui gagnait et le `transform` du
   retour disparaissait de la liste des proprietes animees. La barre sautait au lieu
   de glisser — « ça disparait et reapparait, c'est pas une animation » — et seul le
   retour etait touche, le retrait ayant sa propre regle plus specifique. Mesure sur
   place : `transition-property` valait « background-color, border-color, color ».

   C'est le piege que `CLAUDE.md` nomme deja pour `.table-serree` : a specificite
   egale, c'est l'ordre qui tranche. Une seule declaration ici l'evite pour de bon,
   puisque plus rien ne vient apres elle. Le `transform` ne coute rien sur un grand
   ecran, ou la barre n'en porte jamais. */
/*   Ce n'etait pas la duree, c'etait la courbe. `cubic-bezier(.17, .84, .44, 1)` a
   une pente de 4,9 a l'origine : mesure sur les 54 px de course, elle en faisait
   **43 en 100 ms** et 49 en 150. Les 300 ms declarees etaient donc une illusion —
   la barre etait arrivee, il ne restait que cinq pixels a parcourir pendant la
   moitie du temps. On ne voyait pas un glissement, on voyait une apparition suivie
   d'une attente.

   `(.3, .55, .25, 1)` sur 420 ms garde une pente de 1,8 a l'origine — la barre
   part au doigt, elle ne traine pas — mais elle passe 29 px a 100 ms, 40 a 150,
   47 a 200 : on la voit traverser. Essaye et rejete : `(.4, 0, .2, 1)`, la courbe
   standard de Material, qui ne bouge que d'un pixel dans les 30 premieres
   millisecondes et se lit comme un retard a l'allumage.

   Le depart, lui, ne change pas : ce qui s'en va peut aller vite. */
.sidebar {
  transition: background-color .18s ease, border-color .18s ease, color .18s ease,
              transform .42s cubic-bezier(.3, .55, .25, 1);
}

/* --- Fermeture des fenêtres : le geste inverse de l'ouverture ---
   L'entrée était animée, la sortie coupait net : `hidden` tombait d'un coup.
   C'est pourtant la sortie qu'on regarde toujours. `masquerModal()` garde la
   fenêtre visible 170 ms avec cette classe, puis la cache pour de bon ;
   `pointer-events: none` pendant ce répit, pour qu'un doigt pressé ne clique
   pas dans une fenêtre en train de partir. */
.modal.modal-ferme { pointer-events: none; animation: modal-fond-sort .17s ease forwards; }
.modal.modal-ferme .modal-panel { animation: modal-sort var(--duree-fenetre-sortie) ease forwards; }
@keyframes modal-fond-sort { to { opacity: 0; } }
@keyframes modal-sort { to { opacity: 0; transform: translate3d(0, 8px, 0) scale(.985); } }
@media (max-width: 900px) {
  .modal.modal-ferme .modal-panel { animation: feuille-sort var(--duree-fenetre-sortie) cubic-bezier(.4, 0, 1, 1) forwards; }
  @keyframes feuille-sort { to { transform: translate3d(0, 100%, 0); } }
}

@keyframes toast-entre {
  from { opacity: 0; transform: translateX(-50%) translateY(10px) scale(.94); }
  to   { opacity: 1; transform: translateX(-50%); }
}
@keyframes toast-sortie {
  to { opacity: 0; transform: translateX(-50%) translateY(6px) scale(.97); }
}
.toast { animation: toast-entre .32s cubic-bezier(.26, 1.25, .48, 1); }
.toast.toast-sort { animation: toast-sortie .32s ease forwards; }

@keyframes tip-pose { from { opacity: 0; transform: translateY(3px) scale(.98); } }
.chart-tip { animation: tip-pose .15s cubic-bezier(.22, .61, .36, 1); }

/* --- Dépliants : le contenu glisse au lieu d'apparaître ---
   `interpolate-size` permet d'animer vers `auto`, ce qu'aucune hauteur en dur
   ne fait proprement. Navigateur trop vieux : la règle est ignorée et le
   dépliant s'ouvre d'un coup, comme avant — c'est une politesse, pas une
   fondation. */
:root { interpolate-size: allow-keywords; }
details::details-content {
  block-size: 0; overflow: clip;
  transition: block-size .3s cubic-bezier(.22, .61, .36, 1),
              content-visibility .3s allow-discrete;
}
details[open]::details-content { block-size: auto; }

.segmented button, .plage button {
  transition: background .16s ease, color .16s ease, transform .08s ease;
}
.segmented button:active, .plage button:active { transform: scale(.94); }

/* La courbe d'evolution se decouvre de gauche a droite, a l'arrivee.

   Les jauges poussaient de zero, les cartes montaient de 7 px, et le graphique
   — la plus grande surface de l'ecran — apparaissait d'un coup. Il etait
   paradoxalement le moins vivant de la page.

   Un balayage plutot qu'un stroke-dashoffset par trace : c'est un graphique en
   aires empilees, animer chaque courbe demanderait la longueur de chaque chemin
   et ne dirait rien des aires. Le dessin entier se decouvre, dans le sens du
   temps, ce qui est exactement ce que la courbe raconte.

   `clip-path: inset()` s'anime sans recalculer la mise en page. Sous
   `.vue-entre` seulement, comme `barre-pousse` : a l'arrivee sur une vue, jamais
   a la frappe ni au rafraichissement. Le leger retard laisse les cartes se
   poser d'abord. */
@keyframes chart-balaye {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 -2% 0 0); }
}
.vue-entre .chart-trace {
  animation: chart-balaye .78s cubic-bezier(.3, .8, .4, 1) .1s backwards;
}
@media (prefers-reduced-motion: reduce) {
  .vue-entre .chart-trace { animation: none; }
}

.vue-entre.barre-immobile > .sous-onglets { animation: none; }

@keyframes valeur-change {
  0%   { opacity: .3; }
  30%  { opacity: 1; }
  100% { opacity: 1; }
}
@keyframes valeur-lavis {
  0%   { opacity: 0; }
  18%  { opacity: .9; }
  100% { opacity: 0; }
}
.valeur-maj {
  position: relative;
  animation: valeur-change .9s ease;
}
.valeur-maj::before {
  content: ''; position: absolute; inset: -3px -6px; z-index: -1;
  border-radius: var(--radius-xs); pointer-events: none;
  background: color-mix(in oklab, var(--accent) 26%, transparent);
  /* Eteint au repos, et c'est tout le correctif. `valeur-lavis` finit a
     `opacity: 0`, mais une animation sans `fill-mode` rend a l'element la valeur
     de sa regle des qu'elle s'acheve — donc 1, l'opacite par defaut. Le lavis
     restait peint pour toujours : un aplat violet sur le patrimoine de la barre
     laterale, signale trois fois, qu'on a pris pour une selection de texte puis
     pour un reste de survol. Mesure sur place : animation terminee, opacite du
     pseudo-element a 1.

     `opacity: 0` au repos plutot que `animation-fill-mode: forwards` : le
     premier eteint le lavis dans tous les cas, y compris si l'animation ne joue
     jamais ou si elle est interrompue. La classe `valeur-maj`, elle, n'est
     jamais retiree — et elle n'a pas a l'etre, une fois l'etat de repos eteint. */
  opacity: 0;
  animation: valeur-lavis 1.1s cubic-bezier(.22, .61, .36, 1);
}
@media (prefers-reduced-motion: reduce) {
  .valeur-maj, .valeur-maj::before { animation: none; }
  .valeur-maj::before { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

.an52 { margin-top: 14px; }
.an52-tete, .an52-pied {
  display: flex; justify-content: space-between; gap: 10px; font-size: 11.5px;
}
.an52-tete { margin-bottom: 6px; font-weight: 600; }
.an52-pied { margin-top: 5px; }
.an52-piste {
  position: relative; height: 6px; border-radius: var(--radius-pill);
  background: linear-gradient(90deg,
    color-mix(in oklab, var(--critical) 45%, var(--surface-2)),
    var(--surface-2) 50%,
    color-mix(in oklab, var(--good) 45%, var(--surface-2)));
}
.an52-piste i {
  position: absolute; top: 50%; width: 3px; height: 14px; border-radius: 2px;
  background: var(--text-primary); transform: translate(-50%, -50%);
  box-shadow: 0 0 0 2px var(--surface-1);
}

/* ---------------- Budget : le resume, puis les deux tableaux ----------
   Trois cartes a la suite, sur toute la largeur, et l'ordre du balisage n'est
   pas le bon : `order` le corrige sans qu'on ait a deplacer soixante lignes de
   gabarit.

   « Epargne mensuelle » vient en premier, juste sous « Ce qui sort chaque
   mois » : c'est un resume de six lignes — revenus, charges, depenses, epargne
   theorique, taux, objectif — et il se lit avant les tableaux qui le detaillent.
   Il fermait la page, donc apres cent lignes de saisie, alors qu'il repond a la
   question qu'on se pose en arrivant. Le commentaire d'avant annoncait deja
   qu'il « passe devant » : l'ordre disait le contraire.

   La colonne interieure s'efface en `display: contents` : ses deux cartes
   deviennent des elements de la grille exterieure, et l'ordre se decide alors
   sur les trois d'un coup. */
.budget-charges > .grid { display: contents; }
.budget-charges > .grid > .card:nth-child(1) { order: 1; } /* Epargne mensuelle */
.budget-charges > .card { order: 2; }                      /* Charges fixes */
.budget-charges > .grid > .card:nth-child(2) { order: 3; } /* Autres depenses */

.sous-onglets .pastille-onglet {
  display: inline-block; width: 6px; height: 6px; border-radius: var(--radius-pill);
  margin-left: 6px; vertical-align: 2px;
  background: var(--warning);
}

.grid:has(> :only-child) { grid-template-columns: minmax(0, 1fr); }

.bandeau-demo {
  margin: 12px 18px 0; padding: 9px 14px; border-radius: var(--radius-sm);
  border: 1px dashed var(--border-strong, var(--grid));
  background: var(--surface-2); color: var(--text-secondary);
  font-size: 12.5px; text-align: center;
}
.bandeau-demo b { color: var(--text-primary); }
.charges-tete { display: flex; flex-wrap: wrap; gap: 18px 28px; align-items: flex-end; }
.ct-chiffre { display: flex; flex-direction: column; gap: 2px; }
.ct-chiffre b { font-size: 28px; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.ct-chiffre span { font-size: 12.5px; }
.ct-cote { display: flex; gap: 26px; flex-wrap: wrap; margin-left: auto; }
.ct-petit { display: flex; flex-direction: column; gap: 2px; }
.ct-petit span { font-size: 11.5px; }
.ct-petit b { font-size: 16px; font-variant-numeric: tabular-nums; }
@media (max-width: 560px) {
  .ct-cote { margin-left: 0; gap: 22px; }
}

.repart-credits {
  display: flex; align-items: baseline; gap: 10px; width: 100%;
  margin-top: 10px; padding: 11px 0 2px;
  border-top: 1px solid var(--border);
  background: none; border-left: 0; border-right: 0; border-bottom: 0;
  font: inherit; color: var(--text-secondary); text-align: left; cursor: pointer;
}
.repart-credits b { margin-left: auto; font-variant-numeric: tabular-nums; }
@media (hover: hover) { .repart-credits:hover span:first-child { color: var(--text-primary); } }
.repart-credits:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

/* ---------------- Le champ qui vient d'être enregistré ----------------
   Tout ce qui se saisit dans une page entre dans l'état à la frappe, et le seul
   témoin était « Sauvegardé ✓ » au bas de la barre latérale : dix pixels et demi,
   dans le tiroir fermé sur téléphone. On tapait dans le vide.

   Un liseré vert sur le champ qu'on vient de remplir, huit dixièmes de seconde.
   `box-shadow` et non `border` : une bordure déplacerait le contenu d'un pixel à
   chaque frappe, et le champ sauterait sous le doigt. */
@keyframes champ-ecrit {
  0%   { box-shadow: 0 0 0 2px color-mix(in oklab, var(--good) 70%, transparent); }
  70%  { box-shadow: 0 0 0 2px color-mix(in oklab, var(--good) 55%, transparent); }
  100% { box-shadow: 0 0 0 2px transparent; }
}
.champ-ecrit { animation: champ-ecrit .9s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .champ-ecrit { animation: none; box-shadow: 0 0 0 2px color-mix(in oklab, var(--good) 60%, transparent); }
}

/* Les rangees de boutons d'une fiche : la validation, au bas de la carte des
   champs, et la vie du compte, dans la carte « Actions ». Une seule classe pour
   les deux, et une seule geometrie : deux rangees de boutons a quelques pixels
   l'une de l'autre, l'une plus large ou plus haute que l'autre, se lisent comme
   un oubli — que la bordure d'une carte les separe ou non.

   Une grille, et non un flex, et c'est ce qui les rend comparables. En flex la
   longueur du libelle decide de la largeur — 79 px pour « Archiver », 157 pour
   « Clôturer et supprimer » — alors qu'ici deux colonnes de meme fraction
   donnent la meme largeur a tous, quel que soit le mot. Les deux cartes ayant la
   meme largeur, leurs colonnes coincident d'une carte a l'autre.

   Une seule colonne sous 768 px : a 375 px deux colonnes laissent 152 px, ou
   « Clôturer et supprimer » se plie en deux lignes. Sa rangee prenait alors
   17 px de plus que sa voisine. Empiles, les quatre boutons sont identiques et
   la cible est plus large sous le pouce.

   Au-dela, deux colonnes poussees a droite, la ou se trouve le bouton plein de
   toutes les fenetres de l'application. Les colonnes sont plafonnees et non la
   rangee, et la difference n'est pas cosmetique : un plafond sur la rangee la
   reduit a la largeur des boutons, et le filet d'`apres-champs`, dessine sur
   elle, cesse de traverser la carte pour flotter au-dessus des deux boutons
   comme un trait sans raison. La rangee garde donc toute la largeur, et
   `justify-content` pousse les colonnes. Sans plafond du tout, un bouton
   prendrait les 928 px de la carte sur un ecran de 1280. */
.fiche-actes {
  display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr);
}
.fiche-actes .btn { width: 100%; }
.fiche-actes + .fiche-actes { margin-top: 8px; }
.fiche-actes.apres-champs {
  margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--grid);
}
@media (min-width: 768px) {
  .fiche-actes {
    grid-template-columns: repeat(2, minmax(0, 14em));
    justify-content: end;
  }
}

.cpt-section {
  font-size: 12px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-secondary);
  margin: 26px 0 10px; display: flex; align-items: baseline; gap: 8px;
  flex-wrap: wrap;
}
.cpt-section:first-child { margin-top: 4px; }
.cpt-section .sub {
  font-size: 11px; font-weight: 500; text-transform: none;
  letter-spacing: 0; color: var(--text-secondary);
  opacity: .8;
}
