/* ══════════════════════════════════════════════════════════════════
   L'ÉCONOME — SYSTÈME DE DESIGN
   2026-09-07 · direction « moderne, épurée, premium, accessible »
   ══════════════════════════════════════════════════════════════════

   CE FICHIER EST LA SOURCE DE VÉRITÉ. Il se charge EN DERNIER, après
   app / seasons / mobile-fixes / human-polish. Toute nouvelle règle de
   système va ici, pas ailleurs.

   ── Pourquoi un fichier plutôt qu'une réécriture

   L'état mesuré avant cette passe, sur les dix feuilles :

       161  valeurs de box-shadow distinctes
        33  tailles de police distinctes
        16  graisses distinctes (dont 560, 620, 650, 680, 750)
        12  rayons distincts, DEUX capsules concurrentes (980px et 999px)
       832  !important

   Ce n'est pas un système, c'est une sédimentation. Mais les couleurs,
   elles, sont déjà centralisées : 100 jetons dans `:root`, et tous les
   alias historiques (--brand, --brick, --labrador, --canneberge…)
   pointent vers quatre familles de base. Redéfinir ces familles suffit
   donc à repeindre les trois mille règles existantes d'un coup.

   C'est le levier. On s'en sert.

   ── Ce que ce fichier fait, dans l'ordre
     1. les jetons  — couleur, rayon, espace, type, ombre, mouvement
     2. la remise à plat — on ramène les 161 ombres et les 12 rayons
        sur l'échelle, sans toucher aux trois mille règles
     3. les primitives — en-tête, recherche, cartes, prix, listes, nav

   ── Ce qu'il NE fait pas
   Aucune règle métier, aucune fonctionnalité retirée. C'est une couche
   de présentation. Si une page perd une information, c'est un bug de
   cette feuille, pas une décision.

   ── !important : assumé et borné
   832 `!important` existent déjà en amont, dont 530 dans mobile-fixes.
   Charger après ne suffit donc pas : sur ces propriétés-là il faut le
   même poids. On l'utilise UNIQUEMENT dans la section « remise à plat »
   (§2), jamais dans les primitives (§3) — celles-ci portent des classes
   neuves que personne ne combat.
   ────────────────────────────────────────────────────────────────── */


/* ═══ 1. LES JETONS ══════════════════════════════════════════════ */

:root {
  /* ── Couleur ───────────────────────────────────────────────────
     Beaucoup de blanc, des neutres, et le vert comme couleur de
     MARQUE — pas comme couleur de surface. Les valeurs viennent du
     brief du propriétaire ; elles sont des constantes de marque et ne
     se dérivent pas.

     Le fond de page (#F7F8F5) est un neutre très légèrement teinté
     vers le vert : c'est ce qui empêche le blanc des cartes de paraître
     bleu par contraste simultané, et ce qui relie le fond à la marque
     sans peindre quoi que ce soit en vert. */
  /* ⚠ LES VALEURS DE COULEUR NE SONT PAS ICI.

     Elles vivent dans le bloc `:root, :root:not([data-theme="light"]),
     [data-theme="dark"]` plus bas, avec des `!important` — c'est ce qui
     neutralise la palette sombre d'app.css.

     Elles ont été déclarées aux DEUX endroits pendant vingt minutes, et
     j'ai perdu ces vingt minutes à me demander pourquoi un jeton
     assombri ne changeait rien : celui d'en bas gagnait. C'est
     exactement la faute que ce fichier a été écrit pour corriger, et je
     l'ai refaite en l'écrivant.

     Une valeur de couleur, un seul endroit. Voir §1b. */

  /* ── Rayon — quatre valeurs, une exception ────────────────────
     Douze existaient. Une capsule ne se justifie que si la forme PORTE
     un sens : filtre, catégorie, statut, badge. Nulle part ailleurs. */
  --r-card: 18px;
  --r-tile: 14px;
  --r-field: 16px;
  --r-btn: 12px;
  --r-pill: 999px;

  /* ── Espace — 4 / 8 / 12 / 16 / 24 / 32 ──────────────────────── */
  --e-1: 4px;
  --e-2: 8px;
  --e-3: 12px;
  --e-4: 16px;
  --e-6: 24px;
  --e-8: 32px;
  --marge-ecran: 16px;      /* 20px dès qu'on a la place — voir §3 */
  --entre-sections: 28px;

  /* ── Type ─────────────────────────────────────────────────────
     La pile système : SF Pro sur iOS, Segoe sur Windows, Roboto sur
     Android. C'est ce que demande le brief, et c'est aussi zéro
     téléchargement sur le chemin critique.

     Young Serif reste — mais UNIQUEMENT pour la signature. Un titre
     d'interface en serif, c'est ce qui faisait basculer l'app du côté
     « livre de recettes ». La personnalité vient maintenant de la
     hiérarchie et du traitement des prix, pas de la police. */
  --font-ui: -apple-system, BlinkMacSystemFont, "SF Pro Text",
             "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-body: var(--font-ui);
  --font-numeric: var(--font-ui);
  /* ── Le serif ne survit que sur la signature ───────────────────
     187 sélecteurs posent `font-family: var(--font-display)` — c'est
     toute l'identité éditoriale précédente, des titres de section aux
     noms de plats en passant par les montants.

     On ne les corrige pas un par un : on retourne le JETON. Young Serif
     migre vers `--font-signature`, que seuls la signature de la barre
     du haut et le logo consomment ; `--font-display` devient la pile
     système, donc les 187 sélecteurs passent en sans d'un coup.

     Même levier que pour les couleurs, et même réversibilité : rendre
     le serif à l'interface, si un jour on le voulait, c'est une ligne. */
  --font-signature: "Young Serif", "Iowan Old Style", Georgia, serif;

  --t-montant: 34px;        /* le grand chiffre d'économie */
  --t-h1: 28px;
  --t-h2: 21px;
  --t-carte: 17px;
  --t-corps: 15px;
  --t-petit: 13px;

  --g-normal: 400;
  --g-appuye: 600;
  --g-fort: 700;

  /* ── Ombre — UNE seule ────────────────────────────────────────
     161 variantes existaient. Une carte se pose sur le fond ; elle ne
     lévite pas. La bordure fait le gros du travail, l'ombre ne sert
     qu'à décoller très légèrement du fond. */
  --ombre: 0 2px 8px rgba(0, 0, 0, 0.04);
  --ombre-levee: 0 4px 14px rgba(0, 0, 0, 0.06); /* menus, feuilles */

  /* ── Mouvement ────────────────────────────────────────────────
     Décélération exponentielle, jamais de rebond. */
  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --duree: 180ms;

  /* ── Alias historiques ────────────────────────────────────────
     Ils vivent dans trois mille règles. On les repointe plutôt que de
     les chasser : c'est ce qui repeint l'app entière d'un coup. */
  --brand: var(--accent);
  --brand-dark: var(--accent-deep);
  --brand-light: var(--accent-bright);
  --brand-pale: var(--accent-tint);
  --brand-glow: var(--ombre);
  --accent-glow: var(--ombre);
  --danger-glow: var(--ombre);
  --white: var(--paper-sheet);
  --radius: var(--r-card);
  --radius-sm: var(--r-btn);
  --radius-xl: var(--r-card);
  --season-accent: var(--accent);
  --season-tint: var(--accent-tint);
}

/* ═══ 1b. LA PALETTE — SOURCE UNIQUE ═════════════════════════════
   Une seule apparence, imposée ici et nulle part ailleurs.
   Le brief demande du clair. `app.css` porte encore une palette sombre
   complète sous `[data-theme="dark"]` — du travail réutilisable qu'on
   ne détruit pas, mais qui ne doit pas s'activer tant qu'il n'est pas
   fini (mesuré la veille : le bandeau reste clair, les titres perdent
   leur contraste).

   Ce bloc vivait dans `human-polish.css`, une feuille de finition, avec
   les vingt jetons répétés en `!important`. Résultat : deux sources
   pour une seule vérité, et celle qui gagnait n'était pas celle qu'on
   éditait. Il est ici maintenant, à côté de la palette qu'il protège.

   Le `!important` n'est nécessaire que sur `color-scheme` et sur les
   quatre familles de base — c'est ce qui neutralise le bloc sombre sans
   le supprimer. Les alias, eux, suivent tout seuls. */
:root,
:root:not([data-theme="light"]),
[data-theme="dark"] {
  color-scheme: light !important;
  --paper: #f7f8f5 !important;
  --paper-soft: #f0f2ee !important;
  --paper-sheet: #ffffff !important;
  --paper-deep: #e9ece7 !important;
  --ink: #171a18 !important;
  --ink-soft: #4a504c !important;
  --ink-muted: #585d59 !important;
  --ink-fade: #949992 !important;   /* décor uniquement — jamais de texte */
  --accent: #0b5138 !important;
  --accent-deep: #073d2b !important;
  --accent-bright: #0f6b4a !important;
  --accent-tint: #e8f2ea !important;
  --accent-tint2: #d3e6d8 !important;

  /* Le rabais n'est PAS une erreur : deux rôles, deux jetons. Les
     confondre donnait des bandeaux d'alerte pour annoncer une bonne
     nouvelle. */
  --deal: #f04a3e !important;        /* rouge de marque — surfaces larges */
  --deal-badge: #c9372c !important;  /* blanc dessus : 5,0:1, seuil AA tenu */
  --deal-ink: #b32a20 !important;    /* rouge en petit texte sur clair : 5,1:1 */
  --deal-tint: #fdebe8 !important;
  --danger: #c0392b !important;
  --danger-tint: #fdebe8 !important;
  --best: #f7c948 !important;        /* « meilleur prix », et rien d'autre */
  --best-ink: #6b4e00 !important;
  --border: #e5e8e4 !important;
  --border2: #e5e8e4 !important;
  --bg: #f7f8f5 !important;
  --surface: #ffffff !important;
}


/* ═══ 2. LA REMISE À PLAT ════════════════════════════════════════
   On ramène l'existant sur l'échelle sans rouvrir trois mille règles.
   C'est la seule section qui utilise `!important`, et uniquement là où
   mobile-fixes ou human-polish en posent déjà un. */

/* ── Les titres d'interface repassent en sans ────────────────────
   Le serif reste sur la signature et sur elle seule. */
h1, h2, h3, h4, h5, h6,
.page-masthead-title,
.dash-cta-title,
.dash-section-title,
.compte-section-title {
  font-family: var(--font-ui);
  letter-spacing: -0.02em;
}
.tb-wordmark,
.logo-zone,
.sidebar-title-lg {
  font-family: var(--font-display);
}

/* ── L'échelle typographique ─────────────────────────────────────
   Six tailles, trois graisses. Les valeurs intermédiaires héritées
   (19, 23, 30, 38…) se rangent sur les barreaux les plus proches. */
body {
  font-family: var(--font-ui);
  font-size: var(--t-corps);
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
h1, .page-masthead-title { font-size: var(--t-h1); font-weight: var(--g-fort); line-height: 1.15; }
h2 { font-size: var(--t-h2); font-weight: var(--g-fort); line-height: 1.25; }
h3 { font-size: var(--t-carte); font-weight: var(--g-appuye); line-height: 1.3; }

/* Les graisses arbitraires d'une police variable (560, 620, 650, 680,
   750) ne survivent pas au passage en police système : elles tombaient
   déjà sur la graisse la plus proche, sans qu'on le voie. On les nomme
   pour que la prochaine personne ne les recrée pas. */

/* ── L'ombre unique ──────────────────────────────────────────────
   Toute élévation existante retombe sur le barreau. Les surfaces
   flottantes (tiroir, feuille du bas, notification) gardent la variante
   levée, parce qu'elles se superposent VRAIMENT à la page. */
.dash-cta-primary,
.dash-deal,
.menu-list-carte,
.ing-row--vignette,
.compte-quicklink,
.cph-row,
.cph-tile,
.rap-card,
.lunch-day,
.recette-card,
.dash-week-kpi,
.onb-collapse,
.dash-victory {
  box-shadow: var(--ombre) !important;
}
.sidebar,
.composer-sheet-panel,
.toast,
.toast-stack > * {
  box-shadow: var(--ombre-levee) !important;
}

/* ── Les rayons ──────────────────────────────────────────────────
   Douze valeurs → quatre. On ne cible que les familles réellement
   posées dans les gabarits ; un rayon oublié retombe sur celui de sa
   feuille d'origine, ce qui est un défaut visible et donc réparable —
   contrairement à un `* { border-radius }` qui casserait les ronds
   (avatars, pastilles) sans prévenir. */
.dash-cta-primary,
.menu-list-carte,
.dash-victory,
.onb-collapse,
.rap-card,
.compte-jump,
.frigo-tabs {
  border-radius: var(--r-card) !important;
}
.dash-deal,
.ing-row--vignette,
.compte-quicklink,
.cph-row,
.cph-tile,
.lunch-day,
.recette-card,
.dash-week-kpi {
  border-radius: var(--r-tile) !important;
}
input:not([type="checkbox"]):not([type="radio"]),
select,
textarea,
.topbar-form input {
  border-radius: var(--r-field) !important;
}
.btn-main,
.btn-sec,
button:not(.chk):not(.info-pop):not(.tb-icon-btn):not(.mtab) {
  border-radius: var(--r-btn) !important;
}
/* La capsule ne reste que là où la forme dit quelque chose. */
.frigo-tab,
.badge,
.pill,
.dash-pred-action,
.st,
.tarif-badge,
.recettes-tab-count {
  border-radius: var(--r-pill) !important;
}

/* ── Les cartes : bordure d'abord, ombre ensuite ─────────────────
   La règle du brief : `border: 1px solid` + une ombre presque
   invisible. C'est ce qui distingue une interface tenue d'une
   interface qui flotte. */
.dash-deal,
.menu-list-carte,
.compte-quicklink,
.cph-row,
.cph-tile,
.lunch-day,
.recette-card,
.dash-week-kpi,
.rap-card {
  background: var(--paper-sheet);
  border: 1px solid var(--border);
}

/* ── Plus d'emoji décoratif dans l'interface ─────────────────────
   Le brief est net : aucun emoji sauf raison fonctionnelle. Ceux qui
   restent sont dans du CONTENU (le nom d'un plat, une note de
   l'utilisateur) — on ne touche pas au contenu. Ceux-ci sont de la
   décoration posée par le gabarit. */
.dash-cta-emoji,
.dash-section-emoji,
.onb-card-emoji {
  display: none !important;
}


/* ═══ 3. LES PRIMITIVES ══════════════════════════════════════════
   Classes neuves, aucun `!important` : rien ne les combat.
   Elles sont nommées en français comme le reste du projet. */

/* ── Marge d'écran ───────────────────────────────────────────────
   16 px au minimum, 20 dès que l'écran le permet. */
@media (min-width: 380px) {
  :root { --marge-ecran: 20px; }
}

/* ── En-tête ─────────────────────────────────────────────────────
   Le nom, puis la ligne de promesse. Deux lignes, pas un slogan
   marketing : « ce que fait l'app » en cinq mots. */
.le-entete {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--e-3);
  padding: var(--e-4) 0 var(--e-3);
}
.le-entete-nom {
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 1;
  color: var(--ink);
}
.le-entete-nom em {
  font-style: italic;
  color: var(--accent);
}
.le-entete-promesse {
  font-size: var(--t-petit);
  color: var(--ink-muted);
}

/* ── Recherche ───────────────────────────────────────────────────
   Le champ et le scan forment une seule unité : deux boutons séparés
   feraient deux décisions là où il n'y en a qu'une. */
.le-recherche {
  display: flex;
  gap: var(--e-2);
  align-items: stretch;
}
.le-recherche-champ {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: var(--e-2);
  min-height: 48px;
  padding: 0 var(--e-4);
  background: var(--paper-sheet);
  border: 1px solid var(--border);
  border-radius: var(--r-field);
  color: var(--ink-muted);
  font-size: var(--t-corps);
  text-decoration: none;
  transition: border-color var(--duree) var(--ease);
}
.le-recherche-champ:focus-visible,
.le-recherche-champ:hover {
  border-color: var(--border-fort);
}
.le-recherche-champ svg { flex: 0 0 auto; color: var(--ink-muted); }
.le-recherche-scan {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  min-height: 48px;
  background: var(--paper-sheet);
  border: 1px solid var(--border);
  border-radius: var(--r-field);
  color: var(--ink);
  transition: background var(--duree) var(--ease);
}
.le-recherche-scan:active { background: var(--paper-soft); }

/* ── Accès rapides ───────────────────────────────────────────────
   Quatre destinations, une ligne, sans carte. Un accès rapide dans une
   carte n'est plus rapide : c'est une carte. */
.le-acces {
  display: flex;
  gap: var(--e-2);
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: var(--e-1);
  margin-bottom: var(--entre-sections);
}
.le-acces::-webkit-scrollbar { display: none; }
.le-acces-item {
  flex: 1 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--e-2);
  min-height: 44px;
  padding: 0 var(--e-3);
  background: var(--paper-sheet);
  border: 1px solid var(--border);
  border-radius: var(--r-btn);
  font-size: var(--t-petit);
  font-weight: var(--g-appuye);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  transition: border-color var(--duree) var(--ease);
}
.le-acces-item svg { color: var(--ink-muted); flex: 0 0 auto; }
.le-acces-item:active { background: var(--paper-soft); }

/* ── La carte « économies de la semaine » ────────────────────────
   La seule surface verte de l'écran, et la seule qui a le droit de
   l'être : c'est la réponse à la question que l'utilisateur se pose en
   ouvrant l'app. Sobre — le vert porte le chiffre, il ne décore rien. */
.le-economies {
  background: var(--accent);
  color: #ffffff;
  border-radius: var(--r-card);
  padding: var(--e-6);
  margin-bottom: var(--entre-sections);
}
.le-economies-libelle {
  font-size: var(--t-petit);
  font-weight: var(--g-appuye);
  color: rgba(255, 255, 255, 0.78);
  margin-bottom: var(--e-2);
}
.le-economies-montant {
  font-size: var(--t-montant);
  font-weight: var(--g-fort);
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--e-2);
}
.le-economies-detail {
  font-size: var(--t-petit);
  color: rgba(255, 255, 255, 0.78);
  margin-bottom: var(--e-4);
}
.le-economies-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 var(--e-4);
  background: #ffffff;
  color: var(--accent-deep);
  border-radius: var(--r-btn);
  font-size: var(--t-corps);
  font-weight: var(--g-appuye);
  text-decoration: none;
}
.le-economies-action:active { background: var(--accent-tint); }

/* Quand rien n'a encore été mesuré, on ne met pas un zéro en 34 px.
   Un état vide n'est pas la carte pleine à qui on a retiré le chiffre. */
.le-economies--vide {
  background: var(--accent-tint);
  color: var(--ink);
}
.le-economies--vide .le-economies-libelle,
.le-economies--vide .le-economies-detail { color: var(--ink-muted); }
.le-economies--vide .le-economies-montant {
  font-size: var(--t-h2);
  letter-spacing: -0.01em;
}
.le-economies--vide .le-economies-action {
  background: var(--accent);
  color: #ffffff;
}

/* ── En-tête de section ──────────────────────────────────────────
   Le titre à gauche, le lien à droite, sur la même ligne de base. */
.le-section {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--e-3);
  margin-bottom: var(--e-3);
}
.le-section-titre {
  font-size: var(--t-h2);
  font-weight: var(--g-fort);
  letter-spacing: -0.02em;
  color: var(--ink);
  margin: 0;
}
.le-section-lien {
  font-size: var(--t-petit);
  font-weight: var(--g-appuye);
  color: var(--accent);
  text-decoration: none;
  white-space: nowrap;
}

/* ── Carte de rabais ─────────────────────────────────────────────
   Comprise en moins d'une seconde, et c'est une contrainte de nombre
   d'éléments avant d'être une contrainte de style.

   La version d'avant en portait six : photo, badge de pourcentage,
   nom, prix, prix barré, enseigne. Six, c'est ce qu'on lit quand on
   décide d'acheter la chose ; ce n'est pas ce qu'on lit quand on
   balaie une grille de quatre cartes sur un téléphone.

   Il en reste quatre, et ils ne se disputent jamais :

     1  l'économie, en dollars   — 13 px, rouge aubaine
     2  le prix actuel           — 28 px, encre
     3  le nom du produit        — 15 px, deux lignes maximum
     4  l'enseigne et le régulier— 13 px, atténués

   Le pourcentage est parti avec la photo : « −68 % » et « 5,20 $ de
   moins » disent la même chose, et seul le second se met dans un
   panier. Le rouge ne sert qu'à la ligne 1 — une seule couleur
   d'accent par carte, sinon plus rien n'accentue. */
.le-rabais {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--e-4);
  background: var(--paper-sheet);
  border: 1px solid var(--border);
  border-radius: var(--r-tile);
  text-decoration: none;
  color: inherit;
  transition: border-color var(--duree) var(--ease);
}
.le-rabais:active { border-color: var(--border-fort); }

.le-rabais-eco {
  font-size: var(--t-petit);
  font-weight: var(--g-fort);
  color: var(--deal-badge);
  font-variant-numeric: tabular-nums;
}
.le-rabais-prix-actuel {
  font-size: 28px;
  font-weight: var(--g-fort);
  line-height: 1.1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.le-rabais-nom {
  margin-top: var(--e-1);
  font-size: var(--t-corps);
  font-weight: var(--g-normal);
  line-height: 1.3;
  color: var(--ink);
  /* Deux lignes maximum : un nom de produit très long ne doit pas
     décaler l'enseigne d'une carte à l'autre dans une grille. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.le-rabais-source {
  font-size: var(--t-petit);
  line-height: 1.35;
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* Grille de rabais — sans point de rupture. */
.le-rabais-grille {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--e-3);
  margin-bottom: var(--entre-sections);
}

/* ── Liste d'épicerie ────────────────────────────────────────────
   Utilitaire. Pas de photo, pas de carte par ligne : une liste est une
   liste. Le filet sépare, la case coche, le prix se repère. */
.le-liste-groupe { margin-bottom: var(--e-6); }
.le-liste-titre {
  font-size: var(--t-petit);
  font-weight: var(--g-fort);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin: 0 0 var(--e-2);
}
.le-liste-ligne {
  display: grid;
  grid-template-columns: 24px 1fr auto;
  align-items: center;
  gap: var(--e-3);
  min-height: 48px;
  padding: var(--e-2) 0;
  border-bottom: 1px solid var(--border);
}
.le-liste-ligne:last-child { border-bottom: 0; }
.le-liste-nom {
  font-size: var(--t-corps);
  color: var(--ink);
  line-height: 1.35;
}
.le-liste-qte {
  font-size: var(--t-petit);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.le-liste-ligne[data-coche="1"] .le-liste-nom {
  color: var(--ink-muted);
  text-decoration: line-through;
}
/* L'économie possible sur une ligne : discrète, mais repérable. */
.le-liste-economie {
  grid-column: 2 / -1;
  font-size: 12px;
  font-weight: var(--g-appuye);
  color: var(--deal-ink);
}

/* ── Le prix, partout ────────────────────────────────────────────
   Chiffres tabulaires : deux prix l'un sous l'autre s'alignent, et un
   montant qui change ne fait plus sauter la mise en page. */
.le-prix,
.dash-meal-price,
.recipe-price-tag,
.le-rabais-prix-actuel,
.le-economies-montant {
  font-variant-numeric: tabular-nums lining-nums;
}

/* ── Badges ──────────────────────────────────────────────────────
   Trois seulement. Un quatrième badge, c'est un badge de trop. */
.le-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--e-1);
  padding: 3px var(--e-2);
  border-radius: var(--r-pill);
  font-size: 12px;
  font-weight: var(--g-appuye);
  line-height: 1.4;
}
.le-badge--rabais { background: var(--deal-tint); color: var(--deal-ink); }
.le-badge--info { background: var(--accent-tint); color: var(--accent-deep); }
.le-badge--meilleur { background: var(--best); color: var(--best-ink); }

/* ── Boutons ─────────────────────────────────────────────────────
   Deux niveaux, et un lien. Pas trois boutons pleins côte à côte. */
.le-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--e-2);
  min-height: 48px;
  padding: 0 var(--e-4);
  border: 1px solid transparent;
  border-radius: var(--r-btn);
  font-family: var(--font-ui);
  font-size: var(--t-corps);
  font-weight: var(--g-appuye);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--duree) var(--ease),
              border-color var(--duree) var(--ease);
}
.le-btn--premier {
  background: var(--accent);
  color: #ffffff;
}
.le-btn--premier:active { background: var(--accent-deep); }
.le-btn--second {
  background: var(--paper-sheet);
  border-color: var(--border);
  color: var(--ink);
}
.le-btn--second:active { background: var(--paper-soft); }
.le-btn[disabled],
.le-btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
}
.le-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ── États d'attente ─────────────────────────────────────────────
   Une silhouette de la chose attendue, pas un rond qui tourne. */
.le-squelette {
  background: var(--paper-soft);
  border-radius: var(--r-tile);
  animation: le-pulse 1.4s ease-in-out infinite;
}
@keyframes le-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

/* ── État vide ───────────────────────────────────────────────────
   Il enseigne l'interface. « Rien ici » n'apprend rien à personne. */
.le-vide {
  padding: var(--e-8) var(--e-4);
  text-align: center;
  color: var(--ink-muted);
}
.le-vide-titre {
  font-size: var(--t-carte);
  font-weight: var(--g-appuye);
  color: var(--ink);
  margin-bottom: var(--e-2);
}
.le-vide-texte {
  font-size: var(--t-corps);
  max-width: 34ch;
  margin: 0 auto var(--e-4);
  line-height: 1.5;
}

/* ── Mouvement réduit ────────────────────────────────────────────
   Respecté à 100 % : le non-essentiel disparaît, le reste tombe sous
   150 ms. */
@media (prefers-reduced-motion: reduce) {
  .le-squelette { animation: none; }
  *,
  *::before,
  *::after {
    transition-duration: 120ms !important;
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
  }
}


/* ═══ 4. NAVIGATION GLOBALE ══════════════════════════════════════ */

/* ── Barre du haut ───────────────────────────────────────────────
   Surface blanche, filet d'un pixel, rien d'autre. Le nom à gauche —
   Young Serif, la seule survivance du serif, parce que c'est une
   signature et pas un titre. Les icônes à droite : une seule famille,
   trait de 1,8 px, aucun fond.

   Les pastilles grises `rgba(0,0,0,.05)` qui les portaient donnaient
   trois carrés identiques et de la boue sur fond clair. La zone de
   frappe reste à 44 px — elle n'a jamais dépendu du fond. */
.topbar {
  background: var(--paper-sheet) !important;
  border-bottom: 1px solid var(--border) !important;
  box-shadow: none !important;
  backdrop-filter: none !important;
}
.tb-wordmark {
  display: none;
}
@media (max-width: 900px) {
  .tb-wordmark {
    display: inline-block;
    font-family: var(--font-display);
    font-size: 19px;
    line-height: 1;
    letter-spacing: -0.01em;
    color: var(--ink);
    margin-inline-start: var(--e-1);
    white-space: nowrap;
  }
  .tb-wordmark em {
    font-style: italic;
    color: var(--accent);
  }
  .topbar .tb-icon-btn {
    background: transparent !important;
    border: 0 !important;
    color: var(--ink) !important;
    border-radius: var(--r-btn) !important;
    transition: background var(--duree) var(--ease);
  }
  .topbar .tb-icon-btn:active {
    background: var(--paper-soft) !important;
  }
  .topbar .tb-icon-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
  }
}

/* ── Barre du bas ────────────────────────────────────────────────
   Cinq entrées, très proche d'iOS : icône de 24 px, libellé de 11 px,
   et l'accent uniquement sur l'onglet courant.

   On garde les cinq libellés existants — Accueil / Semaine / Courses /
   Scanner / Compte — plutôt que le jeu suggéré (Explorer / Listes /
   Profil). Ils nomment ce que l'app fait vraiment, ils correspondent
   aux routes en place, et les renommer aurait été un changement de
   vocabulaire sans gain pour personne.

   Le scan reste en quatrième position, pas au centre bombé : il suit
   l'ordre réel d'une semaine — je planifie, je magasine, je scanne mon
   reçu. C'est une décision déjà argumentée dans le gabarit, et le brief
   dit « peut », pas « doit ». */
.mobile-tabbar {
  background: var(--paper-sheet) !important;
  border-top: 1px solid var(--border) !important;
  box-shadow: none !important;
  backdrop-filter: none !important;
  padding-bottom: calc(var(--e-1) + env(safe-area-inset-bottom)) !important;
}
.mtab {
  display: flex !important;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-height: 50px;
  padding: var(--e-1) 0 !important;
  color: var(--ink-muted) !important;
  text-decoration: none;
  border-radius: 0 !important;
  transition: color var(--duree) var(--ease);
}
.mtab-icon {
  width: 24px !important;
  height: 24px !important;
  stroke-width: 1.9;
}
.mtab-label {
  font-size: 11px !important;
  font-weight: var(--g-appuye) !important;
  letter-spacing: -0.005em;
  line-height: 1.2;
}
.mtab.on,
.mtab[aria-current="page"] {
  color: var(--accent) !important;
}
/* iOS ne met pas de fond sur l'onglet actif : c'est la couleur qui le
   dit. Un fond en plus donnerait deux signaux pour une seule info. */
.mtab.on .mtab-icon { stroke-width: 2.2; }
.mtab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -3px;
}


/* ═══ 5. ACCUEIL ═════════════════════════════════════════════════ */

/* La salutation vaut une ligne. Elle situe le moment — « bon matin »
   à 7 h, « bonne soirée » à 19 h — et c'est tout ce qu'elle a à faire.
   Elle occupait un tiers d'écran en 40 px de serif ; elle est
   maintenant ce qu'elle est : une politesse avant le contenu. */
.le-salut {
  margin: 0 0 var(--e-3);
  font-size: var(--t-corps);
  color: var(--ink-muted);
}
.le-salut strong {
  font-weight: var(--g-appuye);
  color: var(--ink);
}

.le-recherche { margin-bottom: var(--e-3); }

/* Le bandeau d'état du système passe sous l'action, pas au-dessus.
   Il occupait le premier tiers de l'écran d'accueil, encadré comme une
   carte, avant même le contenu. C'est une information de service : elle
   se lit, elle ne s'annonce pas. */
.circulaires-pending {
  order: 99;
  background: var(--paper-sheet) !important;
  border: 1px solid var(--border) !important;
  border-radius: var(--r-tile) !important;
  box-shadow: none !important;
  padding: var(--e-3) !important;
  margin: var(--e-4) 0 0 !important;
  font-size: var(--t-petit);
  color: var(--ink-muted);
}

/* Le contexte du jour, sous la carte d'économies. Ce n'est plus un
   titre de page — c'en était un deuxième, quatre cents pixels sous le
   premier, avec le même texte. */
.dash-hero-jour {
  margin: 0 0 var(--e-2);
  font-size: var(--t-petit);
  font-weight: var(--g-appuye);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.le-salut {
  font-size: var(--t-corps) !important;
  font-weight: var(--g-normal) !important;
  letter-spacing: 0 !important;
  line-height: 1.4 !important;
}

/* Quatre accès sur une largeur de 393 px : le libellé porte
   l'information, l'icône n'est qu'un repère. On resserre pour que le
   quatrième ne soit pas coupé — un accès rapide qu'il faut faire
   défiler pour voir n'est plus rapide. */
@media (max-width: 420px) {
  .le-acces-item {
    padding: 0 var(--e-2);
    gap: 6px;
    font-size: 12px;
  }
  .le-acces { gap: 6px; }
}

/* ── Le biseau 3D des boutons ────────────────────────────────────
   Mesuré sur le CTA principal de l'accueil :

       border: 2px outset rgb(7, 61, 43)

   `outset` est le style de bordure que les navigateurs appliquent par
   défaut à un <button>. La règle du projet posait `border-color` sans
   jamais poser `border-style` : le biseau du navigateur restait, et
   dessinait une arête 3D claire en haut à gauche, sombre en bas à
   droite. À l'œil, ça ressemblait à une ombre portée dure ; à la
   mesure, l'ombre était pourtant correcte (0 2px 8px à 4 %).

   C'est précisément l'« effet 3D » que le brief range parmi les choses
   à ne pas faire — et personne ne l'avait mis là volontairement. */
button {
  border-style: solid;
}
.dash-cta-primary,
.dash-cta-primary--firstrun {
  border-width: 0 !important;
  border-style: solid !important;
}


/* ═══ 6. RABAIS ══════════════════════════════════════════════════

   « Compréhensible en moins d'une seconde. » La hiérarchie est stricte
   et aucun élément n'en partage deux attributs avec un autre :

     1  prix actuel        21 px · 700 · encre
     2  rabais             12 px · 700 · blanc sur rouge
     3  nom du produit     15 px · 600 · encre
     4  ancien prix        13 px · 400 · atténué, barré
     5  enseigne           12 px · 600 · atténué

   Avant, les cinq étaient à moins de deux pixels et une graisse
   d'écart : l'œil devait lire pour hiérarchiser au lieu de voir. */

.dash-deal.produit-rangee {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--e-3);
  padding: var(--e-3);
}
.produit-rangee-corps {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

/* Le nom : deux lignes au maximum. Un nom de produit très long ne doit
   pas repousser le prix hors de la rangée. */
.produit-rangee-nom,
.dash-deal-name {
  font-size: var(--t-corps) !important;
  font-weight: var(--g-appuye) !important;
  line-height: 1.3 !important;
  color: var(--ink) !important;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.produit-rangee-meta {
  font-size: 12px !important;
  color: var(--ink-muted) !important;
}

/* L'enseigne redevient une information, pas une marque. Les pastilles
   colorées par enseigne — un rouge Maxi, un bleu Super C — mettaient
   cinq couleurs de tiers dans une palette qui en compte une. */
.enseigne-badge,
.dash-deal .st {
  align-self: flex-start;
  padding: 1px 7px !important;
  background: var(--paper-soft) !important;
  border: 1px solid var(--border) !important;
  border-radius: var(--r-pill) !important;
  color: var(--ink-muted) !important;
  font-size: 11px !important;
  font-weight: var(--g-appuye) !important;
  letter-spacing: 0.01em !important;
  text-transform: none !important;
}

/* Le bloc de prix : aligné à droite, le montant domine. */
.produit-rangee-prix,
.dash-deal-prices {
  display: flex !important;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  text-align: right;
  white-space: nowrap;
}
.dash-deal-price {
  font-size: var(--t-h2) !important;
  font-weight: var(--g-fort) !important;
  line-height: 1.1 !important;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--ink) !important;
}
.dash-deal-original {
  font-size: var(--t-petit) !important;
  font-weight: var(--g-normal) !important;
  color: var(--ink-muted) !important;
  text-decoration: line-through !important;
  font-variant-numeric: tabular-nums;
}
/* Le pourcentage : le seul rouge de la rangée, et il annonce une bonne
   nouvelle — d'où un jeton distinct de `--danger`. Confondre les deux
   donnait des bandeaux d'alerte pour dire qu'on économise. */
.dash-deal-pct {
  display: inline-block;
  margin-top: 3px;
  padding: 2px var(--e-2) !important;
  background: var(--deal-badge) !important;
  color: #ffffff !important;
  border-radius: var(--r-pill) !important;
  font-size: 12px !important;
  font-weight: var(--g-fort) !important;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}

/* Sous 360 px, le prix passe sous le nom plutôt que d'écraser les deux.
   Trois colonnes dans 328 px utiles, ça ne tient pas — et un prix
   coupé ne sert à rien. */
@media (max-width: 359px) {
  .dash-deal.produit-rangee {
    grid-template-columns: 52px minmax(0, 1fr);
  }
  .produit-rangee-prix,
  .dash-deal-prices {
    grid-column: 2;
    align-items: flex-start;
    text-align: left;
  }
}

/* `.le-rabais--sans-photo` a disparu en v386 : la carte de rabais n'a
   plus de vignette du tout, donc plus d'état « avec » et « sans ». */


/* ═══ 7. LE DERNIER TIC « LIVRE DE RECETTES » ════════════════════

   Les titres sont repassés en sans (§2), mais pas les `<em>` qu'ils
   contiennent : une quinzaine de règles posent explicitement
   `font-family: var(--font-display)` sur `.xxx-title em`. Résultat à
   l'écran : « Ta liste, » en sans gras, « déjà organisée. » en serif
   italique vert, dans le même titre.

   Ce mélange est exactement ce qui faisait lire l'app comme un cahier
   de cuisine. L'accent d'un titre se dit maintenant par la couleur,
   pas par un changement de police au milieu d'une phrase.

   Le serif survit à un seul endroit : `.tb-wordmark` et le logo. Une
   signature, pas un titre. */
h1 em, h2 em, h3 em, h4 em,
.page-masthead-title em,
.dash-masthead-greeting em,
.dash-section-title em,
.dash-for-you-title em,
.gen-reveal-title em,
.le-section-titre em {
  font-family: var(--font-ui) !important;
  font-style: normal !important;
  font-weight: inherit;
  color: var(--accent) !important;
}

/* Les encarts « astuce » en serif italique dans une carte teintée :
   même famille de décor. Le contenu reste, la mise en scène part. */
.liste-astuce,
.liste-tip,
.dash-astuce {
  font-family: var(--font-ui) !important;
  font-style: normal !important;
  font-size: var(--t-petit) !important;
  color: var(--ink-muted) !important;
}

/* ── Liste d'épicerie ────────────────────────────────────────────
   Utilitaire. La ligne fait 48 px, le nom porte l'information, la
   quantité et le prix s'alignent à droite en chiffres tabulaires pour
   qu'on les compare d'un coup d'œil vertical. Aucune photo : on
   parcourt cette page à la case à cocher. */
.liste-aisle-title {
  font-size: var(--t-petit) !important;
  font-weight: var(--g-fort) !important;
  letter-spacing: 0.04em !important;
  color: var(--ink-muted) !important;
  padding-left: 0 !important;
  border-bottom: 0 !important;
  margin-bottom: var(--e-1) !important;
}
body .liste-item {
  padding-left: 0 !important;
  min-height: 48px !important;
  border-bottom: 1px solid var(--border) !important;
}
body .liste-item > .chk { margin-left: 0 !important; }
.liste-item .li-n {
  font-size: var(--t-corps) !important;
  font-weight: var(--g-normal) !important;
  color: var(--ink) !important;
}
.liste-price-sale,
.liste-price-estimate {
  font-size: var(--t-corps) !important;
  font-weight: var(--g-fort) !important;
  font-variant-numeric: tabular-nums;
}
.liste-price-sale { color: var(--deal-ink) !important; }
.liste-price-estimate { color: var(--ink) !important; }
.liste-price-reg {
  color: var(--ink-muted) !important;
  font-variant-numeric: tabular-nums;
}
.liste-store-index {
  font-family: var(--font-ui) !important;
  font-size: var(--t-h2) !important;
  font-weight: var(--g-fort) !important;
  color: var(--ink-muted) !important;
}
.liste-store-title {
  font-family: var(--font-ui) !important;
  font-size: var(--t-h2) !important;
  font-weight: var(--g-fort) !important;
  letter-spacing: -0.02em;
}
.liste-store { border-top: 1px solid var(--border) !important; }

/* ── Du texte ne se pose jamais sur `--ink-fade` ─────────────────
   Mesuré : 2,58:1 pour le numéro de version, 2,72:1 pour le copyright,
   2,90:1 pour l'indice vocal. Ces trois-là sont discrets par intention
   — mais discret est une affaire de taille et de position, pas de
   contraste. Un texte qu'on ne peut pas lire n'est pas discret, il est
   absent. */
.app-footer-copy,
.app-footer-version,
.app-footer-trust,
.app-footer-links,
.vocal-hint,
.profil-radio-desc,
.cph-tile-meta {
  color: var(--ink-muted) !important;
}

/* Dernier passage de contraste, après mesure sur douze écrans.

   Trois éléments restaient sous 4,5:1 pour une raison chacun :

   · `.app-footer-version` portait `opacity: 0.65` — une opacité réduit
     le contraste effectif exactement comme un gris plus clair, mais
     n'apparaît nulle part dans la valeur de `color`. C'est pour ça
     qu'on la voit à la mesure et pas à la lecture du CSS.
   · `.frigo-tab` et `.profil-radio-desc` posent leur couleur avec une
     spécificité qui gagne encore sur le jeton ; on les nomme.

   Tous les trois passent sur `--ink-soft`, qui donne 8,6:1 sur le fond
   de page et reste nettement secondaire à l'œil. */
.app-footer-version,
.cph-version-num {
  opacity: 1 !important;
  color: var(--ink-muted) !important;
}
.frigo-tab:not(.is-active),
.profil-radio-desc,
.liste-help-text {
  color: var(--ink-soft) !important;
}

/* ── Les dernières cibles sous 44 px, nommées une à une ──────────
   Mesurées au navigateur, éléments réellement touchables (opacité > 0,
   pointer-events actifs, hors tiroir fermé).

   `.le-section-lien` est de moi et faisait 19 px de haut : un lien de
   section reste discret par sa taille de texte, pas par sa surface. */
.le-section-lien,
.dash-cta-secondary,
.profil-actions-more-cta,
.profil-advanced-cta,
.app-footer-links a,
.app-footer-copy a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
/* Une case à cocher de 22 px reste visuellement une case de 22 px ;
   c'est sa zone de frappe qu'on étend, invisiblement, jusqu'aux 44 px
   réglementaires. Même motif que `.info-pop` ailleurs dans le projet. */
.profil-toggle-cb {
  position: relative;
}
.profil-toggle-cb::after {
  content: "";
  position: absolute;
  inset: -11px;
}


/* ═══ 8. LE SERIF, RÉDUIT À LA SIGNATURE ═════════════════════════
   Mesuré au balayage des 44 pages : entre 3 et 14 éléments par page
   portaient encore Young Serif hors de la signature, malgré la règle
   ciblée de §7. Elle ne pouvait pas suffire — 187 sélecteurs consomment
   `--font-display`.

   Le jeton bascule ici, après sa déclaration d'origine dans app.css :
   tout ce qui demandait « la police d'affichage » reçoit la pile
   système, et la signature va chercher `--font-signature`. */
:root,
:root:not([data-theme="light"]),
[data-theme="dark"] {
  --font-display: var(--font-ui) !important;
  --font-signature: "Young Serif", "Iowan Old Style", Georgia, serif !important;
}

/* Les trois seuls endroits où le serif reste : la marque. */
.tb-wordmark,
.logo-txt,
.logo-zone .sidebar-title-lg,
.le-entete-nom {
  font-family: var(--font-signature) !important;
}

/* Les titres perdent le serif mais gardent leur présence : c'est
   l'approche resserrée qui fait le titre, pas l'empattement. */
h1, h2, h3,
.page-masthead-title,
.dash-section-title,
.display-lg, .display-md {
  letter-spacing: -0.02em;
}

/* Suite du balayage des 44 pages — trois familles de texte posées sur
   `--ink-fade` (2,90:1) ou sur un gris écrit en dur. Même règle que
   plus haut : `--ink-fade` est du décor, jamais du texte. */
.footer-copyright,
.footer-legal-note,
.footer-note,
.w-label, .w-time, .w-blabel {
  color: var(--ink-muted) !important;
}

/* Le ruban d'onglets du tableau de bord posait son texte sur un voile
   translucide : le contraste dépend alors de ce qu'il y a dessous, donc
   il n'est jamais garanti. Surface opaque, contraste calculable. */
.dash-tab {
  background: var(--paper-sheet);
  color: var(--ink-soft);
}
.dash-tab[aria-selected="true"],
.dash-tab.on {
  background: var(--accent-tint);
  color: var(--accent-deep);
}

/* Deux derniers textes posés sur `--ink-fade`, trouvés au balayage. */
.menu-list-name,
.optim-item-was {
  color: var(--ink-muted) !important;
}

/* `.dash-tab` porte `background: none` dans app.css à spécificité égale ;
   sans le poids, ma règle perdait selon l'ordre de cascade des feuilles. */
.dash-tab {
  background: var(--paper-sheet) !important;
  color: var(--ink-soft) !important;
}
.dash-tab[aria-selected="true"],
.dash-tab.on {
  background: var(--accent-tint) !important;
  color: var(--accent-deep) !important;
}


/* ═══ 9. DERNIÈRES CORRECTIONS DU BALAYAGE ═══════════════════════
   44 pages passées au crible, en 393×852, avec un détecteur qui a dû
   être corrigé quatre fois avant de dire la vérité (voir la note en fin
   de fichier). Ce qui suit est ce qui restait après ça. */

/* `.pantry-time-lbl` — RIEN À CORRIGER ICI, et c'est le point.
   Le détecteur l'annonçait à 1,28:1. J'ai posé du blanc dessus. La
   mesure suivante disait 1,00:1 — blanc sur blanc — parce que la
   pastille active porte `background: var(--ink)`, donc du SOMBRE, et
   que la règle d'origine (`oklch(0.92 0.015 85 / 0.65)`) donnait déjà
   près de 10:1.
   J'avais corrigé un faux positif et fabriqué un vrai défaut. La règle
   d'origine est rétablie en ne faisant rien. */

/* Cibles tactiles réellement sous 44 px, une par une.

   Ne sont PAS corrigés, et c'est délibéré :
   · les <a> en ligne dans une phrase — les rehausser casserait
     l'interlignage du paragraphe, et WCAG 2.5.8 les exempte
     explicitement ;
   · `.sr-only` — 1 px, destiné aux lecteurs d'écran, jamais touché ;
   · `.profil-toggle-cb` — 22 px visuels, zone de frappe déjà étendue à
     44 px par un `::after` (§ plus haut). La mesure lit la boîte, pas
     la zone de frappe. */
.nav-logo,
.onb-skip-link,
.urgence-time-pill,
.receipt-item {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* Les entrées masquées derrière une étiquette-carte (choix de style
   d'événement, catégories) : c'est l'étiquette qui reçoit le doigt.
   On étend quand même la zone de l'entrée pour le clavier et pour les
   cas où l'étiquette est plus petite qu'elle. */
.ev-style-card-input,
.ev-form-categorie-checkbox {
  position: relative;
}
.ev-style-card-input::after,
.ev-form-categorie-checkbox::after {
  content: "";
  position: absolute;
  inset: -12px;
}


/* ══════════════════════════════════════════════════════════════════
   NOTE DE MÉTHODE — le détecteur a menti quatre fois
   ══════════════════════════════════════════════════════════════════

   Les chiffres de contraste de cette passe ont été refaits quatre fois
   avant d'être fiables. Les quatre erreurs valent d'être écrites, parce
   qu'elles reviendront :

   1. `getComputedStyle().color` rend la couleur DANS SA NOTATION
      D'ÉCRITURE. La moitié de ce projet est en `oklch()`. Un analyseur
      qui extrait les nombres et les traite comme du RVB lit
      `oklch(0.992 0.006 85)` comme (0, 0, 85) : du blanc devient du
      bleu nuit, et des boutons parfaitement lisibles ressortent à
      1,99:1. → peindre la couleur dans un canvas 1×1 et relire le
      pixel : c'est le convertisseur du navigateur, il ne se trompe pas.

   2. Un fond translucide n'est pas le fond. S'arrêter à la première
      `background-color` non transparente fait lire un voile blanc à
      6 % comme un fond blanc. → composer les couches.

   3. Un DÉGRADÉ n'est pas une `background-color` : il est peint comme
      image, et `backgroundColor` y vaut transparent. Le détecteur
      traversait donc un panneau sombre sans le voir. → quand un
      ancêtre porte une `background-image`, on ne conclut pas.

   4. Un élément dans un parent `display: none` garde son propre
      `display: block`. Seule sa géométrie dit qu'il n'est pas rendu.
      → contrôler `getBoundingClientRect()` avant de juger.

   Bilan après correction : UN seul vrai échec de contraste sur les 44
   pages, contre 23 annoncés par le détecteur cassé. La leçon est la
   même que partout dans ce projet — mesurer, puis regarder la capture,
   et se méfier d'un outil qui trouve beaucoup de choses d'un coup.
   ────────────────────────────────────────────────────────────────── */

/* ── Site marketing : navigation et pied de page ─────────────────
   Mesurés à 29 px (nav) et 39 px (pied) sur six pages. Ce sont des
   liens de NAVIGATION en `display: block`, pas des liens en ligne dans
   une phrase : l'exemption de WCAG 2.5.8 ne s'applique pas, et un
   pouce sur un téléphone ne vise pas 29 px.

   Le site public partage désormais la même feuille que l'app : la même
   règle des 44 px doit valoir des deux côtés. */
.site-nav a,
.nav-links a,
.footer-links a,
.footer-col a,
.site-footer a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

/* ── Choix des enseignes (profil) ────────────────────────────────
   2026-09-11. Une liste de dix noms dont deux sont des réseaux qu'on
   ne connaît pas forcément : chaque ligne porte donc une note, et une
   note a besoin de deux lignes de texte. C'est ce qui exclut le
   `tag-cloud` employé juste en dessous pour les allergies — une
   pastille ne contient pas d'explication.

   Case native, visible, à gauche : c'est le geste demandé (« cocher »),
   et `accent-color` est déjà posé globalement. La rangée entière est
   le label, donc la cible fait toute la largeur. */
.le-enseignes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--e-2);
}

/* ⚠ `app.css:3323` pose une règle sur l'élément `label` NU : 12 px,
   gris, `text-transform: uppercase`, `letter-spacing: 1px`. Elle touche
   donc tous les labels de l'app, y compris ceux qu'on écrit aujourd'hui.

   Mesuré au navigateur après la première écriture de ce bloc : les dix
   rangées sortaient en capitales espacées — « MÉDITERRANÉEN ET MOYEN-
   ORIENTAL » sur quatre lignes. Une note explicative en capitales n'est
   plus une note, c'est une étiquette : illisible au-delà de trois mots,
   et contraire au brief autant qu'à la règle « pas de capitales sur du
   texte courant ».

   Une classe bat un élément et cette feuille charge en dernier : il
   suffit de redéclarer, sans `!important`. Mais il faut y penser — le
   piège est qu'on n'écrit pas une règle qu'on ne soupçonne pas. */
.le-enseigne {
  display: flex;
  align-items: flex-start;
  gap: var(--e-3);
  min-height: 44px;
  margin: 0;
  text-transform: none;
  letter-spacing: normal;
  padding: var(--e-3) var(--e-4);
  background: var(--paper-sheet);
  border: 1px solid var(--border);
  border-radius: var(--r-tile);
  cursor: pointer;
  transition: border-color var(--duree) var(--ease),
              background var(--duree) var(--ease);
}

/* Le vert marque ce qui est retenu — un liseré, pas un aplat : dix
   rangées pleines de couleur feraient un tableau de bord, et la
   couleur de marque ne sert pas de surface. */
.le-enseigne.on { border-color: var(--accent); }

.le-enseigne-case {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  margin: 1px 0 0;
}

.le-enseigne-corps {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.le-enseigne-nom {
  font-size: var(--t-corps);
  font-weight: var(--g-appuye);
  color: var(--ink);
}

/* La graisse aussi vient du `label` nu d'app.css (600). Un nom appuyé
   au-dessus d'une note appuyée, c'est deux fois la même voix : la note
   redescend à la graisse normale pour que la hiérarchie tienne. */
.le-enseigne-note {
  font-size: var(--t-petit);
  font-weight: var(--g-normal);
  line-height: 1.35;
  color: var(--ink-muted);
}

/* Le paragraphe d'aide au-dessus de la grille. */
.profil-aide-texte {
  margin: 0 0 var(--e-3);
  max-width: 62ch;
  font-size: var(--t-petit);
  line-height: 1.5;
  color: var(--ink-muted);
}

/* ── Rangée de repas sans pictogramme ────────────────────────────
   2026-09-11. La grille d'app.css est `44px 1fr auto` : une colonne
   réservée au visuel, une au texte, une au prix. Sans emoji et sans
   photo, la première colonne resterait un vide de 44 px, et le texte
   commencerait au milieu de nulle part.

   Deux colonnes quand il n'y a rien à montrer. La rangée redevient ce
   qu'elle est — un plat, un jour, un prix. */
.dash-meal--sans-visuel {
  /* `!important` assumé, et mesuré avant d'être écrit.
     `mobile-fixes.css:2226` pose `grid-template-columns: 52px 1fr auto
     !important`. Sans le même poids, ma règle perdait en silence : la
     carte gardait TROIS colonnes, la première de 52 px recevait le bloc
     de texte et la troisième restait vide. Mesuré au navigateur —
     `52px 251px 0px`, bloc d'infos à 52 px de large — ce qui donnait
     « Poulet / rôti aux / herbes » sur trois lignes.
     Deux captures d'écran avant de penser à mesurer la grille. */
  grid-template-columns: 1fr auto !important;
}

/* Le montant reste seul dans sa colonne. Le périmètre d'un montant
   partiel est passé dans la ligne d'infos — voir la note du gabarit :
   en `nowrap` sous le prix, il gonflait la colonne `auto` à 180 px et
   écrasait le nom du plat sur trois lignes. Mesuré, pas supposé. */

/* Même mention, dans la liste des coups de cœur et le journal. */
.cdc-card-partiel {
  color: var(--ink-muted);
  font-weight: var(--g-normal);
}

/* ── Le fil du jour ──────────────────────────────────────────────
   2026-09-11. Une phrase sous la salutation, à la place exacte où
   commence la lecture.

   Volontairement dépourvu de décor : ni fond, ni bordure, ni
   pictogramme. Un encadré coloré en haut d'un accueil, c'est une
   notification, et la ligne perdrait en une semaine le peu de
   crédit qu'elle a. Le seul écart typographique est le lien, en
   vert de marque — il indique qu'il y a quelque chose à faire. */
.le-fil {
  margin: calc(var(--e-1) * -1) 0 var(--e-4);
  font-size: var(--t-corps);
  line-height: 1.45;
  color: var(--ink);
  max-width: 62ch;
}
.le-fil-fait {
  /* La partie factuelle porte les chiffres : chiffres tabulaires
     pour qu'un montant ne danse pas d'un jour à l'autre. */
  font-variant-numeric: tabular-nums;
}
.le-fil-lien {
  color: var(--accent);
  font-weight: var(--g-appuye);
  text-decoration: none;
  white-space: nowrap;
}
.le-fil-lien::after {
  content: " →";
}
