:root {
  /* LA CHARTE « RAYONS ».

     L'app se lit comme les panneaux d'un supermarché : chaque rayon a
     sa couleur (voir `rayonTon` dans views.js), et tout le reste est
     calme — une seule couleur d'action, et des neutres qui ne se battent
     pas avec les illustrations. Ce qui porte une couleur vive doit avoir
     une raison de la porter : le rayon, l'action principale, un état, un
     danger, une note santé.

     LES NEUTRES SE DÉMARQUENT DU FOND DES ILLUSTRATIONS. Toutes les
     images d'ingrédients et de plats sont posées sur un crème chaud
     (autour de #f9eddd, relevé sur leurs coins) et on ne peut pas les
     refaire. Deux essais ont été écartés : un papier gris-vert, qui en
     faisait des carrés jaunes découpés ; puis un papier crème tiré de ces
     mêmes images, où tout se confondait — vignettes, panneaux, fond — et
     où plus rien ne se détachait ; enfin un gris-bleu, lisible mais triste
     et froid pour une app de cuisine.

     Ce qui tient : les PANNEAUX presque blancs, sur lesquels la vignette
     crème ressort comme un objet posé, et un PAPIER SABLE autour — chaud,
     mais nettement plus soutenu que le crème des images, qui ne s'y
     confond pas puisqu'il ne s'y pose jamais directement. L'encre et les
     bords de l'app sont un brun très sombre plutôt qu'un gris. Le bleu
     nuit des actions fait le contrepoint froid : c'est lui qui évite le
     tout-beige.

     L'ACTION EST BLEU NUIT, PAS VERTE. Le vert est la couleur de « c'est
     bon » dans l'app (validée, synchronisée, fait) : un bouton vert se
     lisait comme un état atteint plutôt que comme un geste à faire.

     Les noms disent le RÔLE, pas la teinte : `--accent` reste l'action
     principale le jour où elle change de couleur. */
  --papier: #efe6dc;         /* le fond des écrans */
  --papier-2: #e3d8cb;       /* un cran plus foncé : jauges, contrôles segmentés */
  --surface: #fffdf9;        /* ce qui est posé sur le papier : panneaux, feuilles */
  --ink: #2b2622;
  --ink-soft: #6d645c;
  --ink-mute: #a39a90;       /* chevrons, poignées : ce qui se voit sans se lire */
  --accent: #2a4470;         /* l'action principale */
  --accent-fonce: #1f3457;   /* son appui, et le texte d'action sur fond clair */
  --accent-nuit: #16263f;    /* fond de sélection : lisible sous du blanc, même en petit */
  --accent-pale: #c3cbd9;    /* filets et pointillés d'une action */
  --accent-voile: #e8edf5;   /* le fond d'une zone où l'on ajoute quelque chose */
  --sage: #3f8f5a;           /* fait, validé, en ligne */
  --sage-deep: #2d6b45;
  --etiquette: #f6c945;      /* le jaune des étiquettes : favoris, repère du rail */
  --etiquette-fonce: #8f6400;
  --danger: #b3261e;
  --danger-pale: #f8dcd8;
  --note-bof: #b8432a;       /* la note santé du bas de l'échelle — pas un danger */
  --line: #e9dfd3;
  --line-fort: #d8cbbb;
  --shadow: 0 10px 30px rgba(43, 38, 34, 0.08);

  /* Ce qui ne se déduit pas des tokens ci-dessus, parce que le thème
     sombre les sépare : un fond de CHAMP (blanc ici, un cran plus clair que
     le panneau en sombre), la NUIT des bords de l'app (rail, bandeau,
     toast — foncée dans les deux thèmes), le texte posé sur l'encre, et
     l'appui d'une action (qui reste foncé quand `--accent-fonce`, couleur
     de TEXTE, devient clair). */
  --champ: #ffffff;
  --nuit: #2b2622;
  --ink-inverse: #ffffff;
  --accent-appui: #1f3457;
  /* Ce qui détache la pièce choisie d'un rail segmenté : une ombre sur le
     clair ; en sombre, où une ombre ne se voit pas, un liseré. */
  --piece-ombre: 0 1px 2px rgba(43, 38, 34, .18);
  /* Les pastilles d'une ligne (« Rayon », « À valider »…) : en sombre, le
     fond d'un cran du papier se confondait avec la cellule. */
  --pastille: var(--papier-2);
  --pastille-froide: var(--accent-voile);
  --pastille-ink: #5a5149;
  /* LA FEUILLE ET SON VOILE. La feuille (bottom sheet, dialogue) a son
     propre fond, et le voile posé sur l'écran dessous sa propre teinte :
     en sombre, un voile gris ÉCLAIRCISSAIT la page jusqu'au niveau de la
     feuille, et on ne voyait plus où elle commençait. */
  --feuille: #fffdf9;
  --feuille-creux: var(--papier);
  --feuille-bord: 0 0 #0000;
  --voile: 43, 38, 34;
  --voile-force: .38;
  /* Les notes santé sont des pastilles pleines sous du blanc : mêmes
     teintes dans les deux thèmes. */
  --note-bien: #2d6b45;
  --note-moyen: #8f6400;
  /* Les encarts teintés : note, proposition de l'IA, « le même article ». */
  --bien-voile: #eef5f0;  --bien-filet: #cfe2d7;
  --moyen-voile: #fdf3e0; --moyen-filet: #eedcbb;
  --bof-voile: #fdeee9;   --bof-filet: #f3d5cb;
  /* La proposition de l'IA était JAUNE : le crème des illustrations s'y
     fondait, et la vignette de la cellule disparaissait dans son fond.
     Une lavande pâle tranche avec le crème sans crier, et ne se confond
     avec aucune couleur d'état ni de rayon. */
  --ia-voile: #e9e3fd;    --ia-filet: #cbc1f4;
  --ia-trait: #6f5fd0;
  --ia-tag: #e2ddf8;      --ia-tag-ink: #4b3f9a;
  --meme-voile: #eef1f5;  --meme-filet: #ccd5e0; --meme-ink: #4a5c7a;
  --halo-chaud: #f5e9d6;  --halo-froid: #dde3ee;
  --saison-automne: #b5541c;
  --saison-hiver: #3a6fa8;
  color-scheme: light;

  --f-titre: "Bricolage Grotesque", system-ui, sans-serif;
  --f-texte: "Figtree", system-ui, sans-serif;

  /* QUATRE ARRONDIS, pas vingt-quatre.

     Le fichier en comptait presque autant que de blocs — 11, 13, 14,
     15 px — et deux boutons voisins ne se ressemblaient pas sans qu'on
     sache pourquoi. Un arrondi dit la taille de l'objet : étiquette,
     contrôle (bouton, champ), panneau, feuille. Les pastilles de choix
     restent rondes (999px) : c'est leur forme qui dit qu'on les coche. */
  --r-1: 8px;
  --r-2: 12px;
  --r-3: 16px;
  --r-4: 24px;

  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --composer: 188px;
  --tabbar: 72px;

  /* L'ÉCHELLE D'ESPACEMENT, pour tout CSS écrit à partir de maintenant.

     Le fichier s'est construit valeur par valeur — 6, 9, 10, 13, 14,
     18 px — et c'est ce qui donne des marges voisines qui ne s'accordent
     pas : un intertitre à 14 px de sa liste, le suivant à 18. Une marge
     neuve prend un cran de cette échelle ; les anciennes ne se réécrivent
     que quand on retouche le bloc, et en vérifiant avec `npm run shots`. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;   /* gouttière latérale d'un écran de téléphone */
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;
}

/* LE THÈME SOMBRE.

   Posé par `data-theme="dark"` sur <html> — voir src/theme.js : le choix
   se fait dans les Réglages (Automatique, Clair, Sombre), et
   « Automatique » passe au sombre de la tombée de la nuit au lever du
   jour. Le script en tête d'index.html pose l'attribut AVANT le premier
   affichage : sans lui, un soir, l'app s'ouvrait blanche une fraction de
   seconde avant de s'éteindre.

   Seuls les tokens changent ici. Un composant qui a besoin d'une règle à
   lui en sombre est un composant qui porte encore une couleur en dur.

   Les illustrations gardent leur fond crème : sur ce papier presque noir
   elles ressortent davantage encore, ce qui est ce qu'on cherchait. */
:root[data-theme="dark"] {
  --papier: #121418;
  --papier-2: #20242b;
  --surface: #1a1d23;
  --champ: #2e333c;
  --nuit: #0b0d10;
  --ink: #e7e9ed;
  --ink-soft: #a2a9b4;
  --ink-mute: #6e7682;
  --ink-inverse: #121418;
  --accent: #4a6db0;
  --accent-appui: #3b5a96;
  --piece-ombre: inset 0 0 0 1px #3d4450;
  --pastille: #363c47;
  --pastille-froide: #2f3a50;
  --pastille-ink: #cdd2da;
  /* En sombre, ce qui est devant est plus CLAIR : la feuille gagne deux
     crans sur la page, un liseré dessine son bord haut, et le voile est
     noir — il assombrit la page au lieu de la ternir. */
  --feuille: #252a31;
  --feuille-creux: #1c2026;
  --feuille-bord: inset 0 1px 0 rgba(255, 255, 255, .09);
  --voile: 0, 0, 0;
  --voile-force: .62;
  --accent-fonce: #a9c0ea;
  --accent-nuit: #3b5a96;
  --accent-pale: #34445f;
  --accent-voile: #1d2533;
  --sage: #3f8f5a;
  --sage-deep: #6cc08a;
  --etiquette: #f0c24a;
  --etiquette-fonce: #e3b640;
  --danger: #ff8a80;
  --danger-pale: #3a1e1c;
  --line: #2b3038;
  --line-fort: #3a404a;
  --shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
  --bien-voile: #19291f;  --bien-filet: #2c4a36;
  --moyen-voile: #2b2415; --moyen-filet: #4b3f20;
  --bof-voile: #2d1c18;   --bof-filet: #50302a;
  --ia-voile: #2a2645;    --ia-filet: #463f72;
  --ia-trait: #9d8ff0;
  --ia-tag: #463e7a;      --ia-tag-ink: #ddd7fb;
  --meme-voile: #202834;  --meme-filet: #384556; --meme-ink: #aebdd4;
  --halo-chaud: #2a2419;  --halo-froid: #1b2331;
  --saison-automne: #e38a55;
  --saison-hiver: #86acd9;
  color-scheme: dark;
}
/* Les couleurs de rayon sont choisies pour être lues sur blanc : sur un
   panneau presque noir, le bleu des laitiers ou le brun de l'animalerie
   tombaient sous le seuil. L'intitulé les éclaircit ; la bande et la
   coche, qui ne sont pas du texte, gardent la teinte franche. */
:root[data-theme="dark"] .cat { color: color-mix(in srgb, var(--rayon, var(--ink-soft)) 62%, #fff); }

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  min-height: 100%;
  background: var(--papier);
  color: var(--ink);
  font-family: var(--f-texte);
  font-size: 17px;
}

body {
  min-height: 100dvh;
  overscroll-behavior: none;
}

button, input, textarea, select {
  font: inherit;
  color: inherit;
}

button { border: 0; background: none; cursor: pointer; }

img { max-width: 100%; display: block; }

.screen {
  min-height: 100dvh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
}

/* ===== Auth ===== */
.auth {
  padding: calc(28px + var(--safe-t)) 22px calc(24px + var(--safe-b));
  justify-content: center;
  background:
    radial-gradient(1200px 500px at 80% -10%, var(--halo-chaud) 0%, transparent 55%),
    radial-gradient(900px 400px at -10% 110%, var(--halo-froid) 0%, transparent 50%),
    var(--papier);
}

.auth-mark {
  width: 108px;
  height: 108px;
  border-radius: var(--r-4);
  margin: 0 auto 18px;
  overflow: hidden;
  box-shadow: var(--shadow);
}
.auth-mark img { width: 100%; height: 100%; object-fit: cover; }

.auth h1 {
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 44px;
  margin: 0;
  text-align: center;
  letter-spacing: -0.03em;
}
.auth .tag {
  text-align: center;
  color: var(--ink-soft);
  margin: 6px 0 28px;
  font-weight: 500;
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  padding: 18px;
  box-shadow: var(--shadow);
}

/* `.field` et non `label.field` : la règle ne valait que pour les champs
   écrits en `<label>`, et deux d'entre eux sont des `<div>` — celui des
   autres noms d'un article, et la PRÉPARATION d'une recette, qui coiffe
   son intitulé d'un bouton micro et ne peut donc pas être un label. Ces
   deux-là n'avaient aucune marge basse : « TYPE DE PLAT » se retrouvait
   collé au bas de la zone de texte, comme s'il en était la légende. */
.field, .field-pick {
  display: block;
  margin-bottom: 14px;
}
/* Le libellé d'un champ, et lui seul.

   `:first-child` plutôt qu'un simple `>` : la règle attrapait aussi le
   `span` qui enveloppe un champ à propositions, posé juste après le
   libellé — et lui repassait sa casse et sa couleur.

   Elle vaut pour TOUS les intitulés d'une fiche, pas seulement pour ceux
   qui coiffent un `<input>` : « Type de plat », « Saison », « Autres
   noms » n'en avaient aucun et sortaient en gros et en noir, au milieu de
   petites capitales grises. Trois graisses pour trois intitulés qui
   disent la même chose — le nom de la section d'en dessous. */
.field > span:first-child,
.field-pick > span:first-child {
  display: block;
  font-size: 15px;
  font-weight: 700;
  color: var(--ink);
  margin-bottom: var(--sp-2);
}
/* LES SECTIONS D'UNE FICHE PARLENT COMME LES RAYONS DE LA LISTE.

   « 5 ingrédients », « Préparation », « Vous » découpent une fiche comme
   « Légumes » découpe la liste : même police de titre, même taille. Les
   intitulés de CHAMP restent dans la police du texte — ils nomment ce
   qu'on va taper, pas une partie de l'écran. Les petites capitales
   grises disaient les deux de la même voix. */
.sheet h4 {
  display: block;
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ink);
  margin-bottom: var(--sp-2);
}
/* Les zones longues n'ont plus la poignée du navigateur : elles ont la
   nôtre, juste dessous (voir `.grow-grip`). Celle du navigateur fait
   seize pixels dans un angle, ne se voit pas, et n'existe pas sur
   téléphone. */
.sheet textarea { resize: none; min-height: 96px; max-height: 60vh; }
/* La zone où l'on COLLE une recette entière. Six rangées montraient le
   titre et deux ingrédients : on collait à l'aveugle, sans pouvoir
   relire. Elle s'ouvre à la hauteur d'une demi-feuille.

   ET SA POIGNÉE SE VOIT. Celle du navigateur fait seize pixels dans un
   angle, sans contraste, et n'existe pas sur téléphone. Trois traits en
   biais posés dans le coin — le pictogramme que tout le monde reconnaît —
   sur une cible de trente pixels. Le trait est décoratif (`pointer-events:
   none`) : c'est la zone elle-même qui se saisit, dessous. */
.assist-text { min-height: 240px; }

.grow-box { position: relative; }
/* LA POIGNÉE : une barre pleine largeur, pas un angle de seize pixels.

   Le coin du navigateur ne se voit pas et, au doigt, emmène la feuille
   au lieu d'étirer la zone. Vingt-six pixels sur toute la largeur, c'est
   une cible qu'on atteint sans viser — et `touch-action: none` dit au
   navigateur que ce geste-là n'est pas un défilement. Le reste est dans
   `bindGrow` : capture du pointeur, et la hauteur suit le doigt. */
.grow-grip {
  display: grid;
  place-items: center;
  width: 100%;
  height: 26px;
  border: 0;
  background: transparent;
  touch-action: none;
  cursor: ns-resize;
}
/* DEUX TRAITS, ET RIEN D'AUTRE.

   La poignée portait un fond crème et un liseré, collés sous la zone :
   ça lui donnait l'air d'un second champ, vide, sous le premier. Ce
   qu'on veut dire tient dans le signe lui-même — la zone de touche fait
   toute la largeur et vingt-six pixels de haut, elle n'a pas besoin
   d'être peinte pour être atteinte.

   Un seul élément pour les deux traits : le second est son ombre portée,
   décalée vers le haut. Deux boîtes pour deux traits de trois pixels
   seraient deux fois plus de code pour le même dessin. */
.grow-grip i {
  width: 34px; height: 3px;
  border-radius: 2px;
  background: var(--line-fort);
  box-shadow: 0 -6px 0 var(--line-fort);
}
.grow-grip:active i { background: var(--accent-appui); box-shadow: 0 -6px 0 var(--accent-appui); }
.field input, .sheet input, .sheet textarea, .sheet select {
  width: 100%;
  border: 1.5px solid var(--line);
  background: var(--champ);
  border-radius: var(--r-3);
  padding: 14px 16px;
  outline: none;
  font-size: 16px; /* évite le zoom iOS */
}
.field input:focus, .sheet input:focus, .sheet textarea:focus, .sheet select:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px rgba(42, 68, 112, 0.15);
}

/* Le mot de passe et son œil : le bouton se pose DANS le champ, à
   droite, et le texte s'arrête avant lui. */
.champ-mdp > label > span {
  display: block;
  font-size: 15px;
  font-weight: 700;
  color: var(--ink);
  margin-bottom: var(--sp-2);
}
.champ-mdp-ligne { position: relative; }
.champ-mdp-ligne input { padding-right: 56px; }
.voir-mdp {
  position: absolute; top: 50%; right: 6px; transform: translateY(-50%);
  width: 44px; height: 44px;
  display: grid; place-items: center;
  border-radius: var(--r-2);
  color: var(--ink-soft);
}
.voir-mdp:hover { color: var(--ink); background: var(--papier-2); }
.voir-mdp:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.voir-mdp svg { width: 22px; height: 22px; }

/* Un bouton dessiné comme un champ : même bord, même hauteur, même
   graisse. Ce qu'il ouvre est le petit menu maison, pas la roue du
   système. */
.picker {
  width: 100%;
  min-height: 52px;
  display: flex;
  align-items: center;
  gap: 8px;
  border: 1.5px solid var(--line);
  background: var(--champ);
  border-radius: var(--r-3);
  padding: 8px 12px 8px 16px;
  font-size: 16px;
  font-weight: 600;
  text-align: left;
}
.picker-val { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-chev { display: grid; place-items: center; color: var(--ink-soft); flex: 0 0 auto; }
.picker-chev .ico { width: 18px; height: 18px; }
.picker:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 4px rgba(42, 68, 112, 0.15);
}
.picker:active { border-color: var(--accent); }
/* Une liste de choix peut être longue (les unités, les rayons) : elle
   défile dans le menu plutôt que de déborder de l'écran. */
.pop-menu { max-height: min(52vh, 360px); overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y; }

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 54px;
  padding: 0 18px;
  border-radius: var(--r-3);
  font-weight: 700;
  width: 100%;
}
.btn-primary {
  background: var(--accent);
  color: #fff;
}
.btn-primary:active { transform: translateY(1px); background: var(--accent-appui); }
.btn-ghost {
  background: var(--champ);
  border: 1.5px solid var(--line);
  color: var(--ink);
}
.btn-danger { background: var(--danger-pale); color: var(--danger); }
.hint { color: var(--ink-soft); font-size: 13px; line-height: 1.45; margin: 12px 2px 0; }

/* ===== LE TEXTE DES CARTES =====

   Une carte — la note santé, la famille d'un plat, une proposition
   d'allègement — n'a besoin que de trois voix, et chacune veut dire la
   même chose partout :

   TITRE      15 px / 800 / encre     ce dont la carte parle
   MENTION    12 px / 700 / encre douce   ce qu'il faut savoir en plus
   PROPOS     13 px / 700 / encre douce   une phrase, qui se lit

   et, dans une liste, une quatrième :

   ENTRÉE     14 px / 800 / encre     la ligne sur laquelle on agit

   Il y avait ici quatre graisses, cinq tailles et deux familles pour
   dire ces quatre choses-là, différemment d'une carte à l'autre : la
   même information changeait d'allure selon l'encart qui la portait, et
   on la relisait pour vérifier qu'elle disait bien la même chose.

   Écrit une fois, en un endroit : toute carte qui s'ajoute hérite du
   même vocabulaire sans avoir à le redécider. */
.health-txt strong,
.fold-txt strong { font-size: 15px; font-weight: 700; color: var(--ink); }
.health-txt small,
.fold-txt small { font-size: 12px; font-weight: 600; color: var(--ink-soft); }
.health-why,
.swap-lead,
.swap-why { font-size: 13px; font-weight: 600; line-height: 1.45; color: var(--ink-soft); }
.swap-lead { display: block; margin: 0 0 4px; }
.swap-why { display: block; margin-top: 2px; }
.swap-txt strong { display: block; font-size: 14px; font-weight: 700; color: var(--ink); line-height: 1.35; }

/* ===== Main ===== */
.top {
  position: sticky;
  top: 0;
  z-index: 5;
  padding: calc(10px + var(--safe-t)) 16px 10px;
  background: color-mix(in srgb, var(--papier) 88%, transparent);
  backdrop-filter: blur(16px);
}
.top-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.brand {
  font-family: var(--f-titre); font-stretch: 88%;
  font-weight: 800;
  font-size: 30px;
  letter-spacing: -0.03em;
  margin: 0;
  flex: 1;
}

.list-foot { padding: 12px 0 8px; }
.icon-btn {
  width: 46px;
  height: 46px;
  border-radius: var(--r-2);
  background: var(--surface);
  border: 1.5px solid var(--line);
  display: grid;
  place-items: center;
  font-size: 20px;
}
.status {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 600;
}
.dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--ink-mute);
}
.dot.on { background: var(--sage); box-shadow: 0 0 0 4px rgba(111, 162, 134, 0.2); }
.dot.warn { background: var(--etiquette); }

.progress {
  height: 8px;
  background: var(--papier-2);
  border-radius: 99px;
  overflow: hidden;
  margin-top: 10px;
}
.progress > i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--ink);
  border-radius: 99px;
  transition: width .35s ease;
}

/* UN SEUL DESSIN POUR « CHOISIR CE QU'ON REGARDE ».

   Les filtres de la liste étaient trois pastilles, les volets du
   garde-manger deux onglets orange dans un cadre blanc : deux objets
   différents pour le même geste. Les deux sont maintenant un rail
   segmenté — le choix courant est une pièce blanche posée dessus. Les
   pastilles rondes restent aux CHOIX qu'on coche (saisons, envies). */
.filters {
  display: flex;
  gap: 2px;
  width: fit-content;
  max-width: calc(100% - 2 * var(--sp-4));
  overflow-x: auto;
  margin: 4px var(--sp-4) var(--sp-2);
  padding: 3px;
  border-radius: var(--r-2);
  background: var(--papier-2);
  scrollbar-width: none;
}
.filters .chip,
.pane-tab {
  min-height: 40px;
  border: 0;
  border-radius: calc(var(--r-2) - 3px);
  background: transparent;
  color: var(--ink-soft);
  font-weight: 700;
}
.filters .chip.active,
.pane-tab.active {
  background: var(--champ);
  color: var(--ink);
  box-shadow: var(--piece-ombre);
}
.filters::-webkit-scrollbar { display: none; }
/* La pastille CENTRE ce qu'elle porte, verticalement comme
   horizontalement. Un bouton aligne son texte au milieu tant qu'il n'y a
   que du texte ; dès qu'on y glisse une tête de vingt pixels, la ligne de
   base reprend la main et le libellé se pose au bas de la pastille
   pendant que la tête reste au milieu. C'est ce qui se voyait sur « Aux
   fourneaux » : quatre smileys bien rangés et quatre prénoms trois pixels
   plus bas. Une boîte flexible n'a pas de ligne de base à suivre. */
.chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 38px;
  padding: 0 14px;
  border-radius: 999px;
  background: var(--champ);
  border: 1px solid var(--line);
  font-weight: 600;
  font-size: 14px;
}
.chip.active {
  background: var(--ink);
  color: var(--ink-inverse);
  border-color: var(--ink);
}

.list {
  flex: 1;
  padding: 4px var(--sp-4) calc(var(--tabbar) + 18px + var(--safe-b));
}

/* UN RAYON, UN PANNEAU.

   Chaque article était sa propre carte, ombrée, et le rayon un petit
   intertitre gris au-dessus : l'œil voyait huit cartes pareilles et
   devait lire pour savoir où finissait les laitiers. Le rayon est
   maintenant l'objet — un panneau blanc, bordé à gauche de sa couleur
   (`--rayon`, posée par views.js) — et ses articles en sont les lignes,
   séparées d'un filet. La coche prend la même couleur : on coche « dans
   le bleu ».

   La bande est une ombre intérieure et non une bordure : elle ne décale
   pas le contenu, et elle suit l'arrondi du panneau. */
.cat-group {
  margin-bottom: var(--sp-3);
  padding-left: 6px;
  border-radius: var(--r-3);
  background: var(--surface);
  box-shadow: inset 6px 0 0 var(--rayon, var(--ink-soft)), 0 0 0 1px var(--line);
  overflow: hidden;
}
.cat {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 14px 2px;
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--rayon, var(--ink-soft));
}
.item {
  display: grid;
  grid-template-columns: 40px 1fr 46px;
  gap: 8px;
  align-items: center;
  background: var(--surface);
  border-top: 1px solid var(--line);
  padding: 6px 6px 6px 10px;
  min-height: 58px;
}
.cat + .item { border-top-color: transparent; }
/* ===== Ranger un article dans un autre rayon, au doigt =====

   Rien ne change au repos : une ligne de liste reste une ligne de liste,
   et la page défile. `-webkit-touch-callout` seul est posé d'office —
   c'est la loupe d'iOS, qui s'invite au bout du même délai que le nôtre
   et vole le geste avant qu'il n'ait commencé. */
.item { -webkit-touch-callout: none; }
/* L'article qu'on tient reste en place, estompé : on voit d'où l'on
   part. C'est la copie qui suit le doigt. */
.item.lifted { opacity: .32; }
/* Pendant le déplacement, le doigt ne sélectionne rien et ne fait rien
   défiler : c'est l'article qui suit. */
body.dragging-list {
  -webkit-user-select: none;
  user-select: none;
}
.list-ghost {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  display: grid;
  grid-template-columns: 40px 1fr;
  align-items: center;
  gap: 8px;
  padding: 4px 12px 4px 6px;
  border-radius: var(--r-3);
  background: var(--surface);
  box-shadow: 0 12px 28px rgba(43, 38, 34, .28);
  transform: scale(1.03);
}
.list-ghost .check { display: none; }
/* OÙ ÇA PEUT ALLER, ET OÙ ÇA VA.

   Deux états, et il en faut deux. Le premier — un liseré sur TOUS les
   rayons — dit qu'il existe des cibles : sans lui, on tient un article
   au bout du doigt sans savoir ce qui l'attend. Le second dit laquelle,
   et il est franc, parce que c'est celui-là qu'on lit en une fraction de
   seconde avant de lâcher. */
body.dragging-list .cat-group {
  outline: 1.5px dashed var(--line-fort);
  outline-offset: 2px;
}
body.dragging-list .cat-group.over {
  outline: 2px solid var(--sage);
  background: var(--bien-voile);
}
/* L'intitulé du rayon visé se met à son tour en avant : c'est le mot
   qu'on lit pour vérifier qu'on vise juste. */
body.dragging-list .cat-group.over .cat { color: var(--sage-deep); }

.item-art {
  width: 40px; height: 40px;
  border-radius: var(--r-1);
  object-fit: cover;
  background: var(--paper-img);
}
.item-art.packed { object-fit: contain; }
.item.checked { opacity: 0.58; }
.item.checked .item-name { text-decoration: line-through; color: var(--ink-soft); }
/* Deux lignes empilées : le nom et sa quantité, puis la note. */
.item-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: stretch;
  gap: 2px;
  min-height: 46px;
  text-align: left;
}
.item-line {
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.item-name {
  font-weight: 700;
  font-size: 17px;
  line-height: 1.15;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.item-qty {
  flex: 0 0 auto;
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 600;
}
.item-meta {
  margin-top: 3px;
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 500;
}
/* Le rond à cocher.

   La zone tactile fait 46 px de côté — c'est elle qu'on vise, en marchant,
   d'une main. Le rond dessiné dedans n'en fait que 30 : un point d'appui
   trop grand a l'air d'un bouton à part entière, alors que c'est une
   case. La coche est là dès le départ, simplement rétractée : elle jaillit
   au lieu d'apparaître, et le geste se sent. */
.check {
  width: 46px;
  height: 46px;
  border: 0;
  background: none;
  display: grid;
  place-items: center;
  justify-self: end;
}
/* Carrée aux angles doux plutôt que ronde : une case qu'on coche, pas un
   bouton radio — il n'y a pas « un seul choix » dans une liste de courses. */
.check-ring {
  width: 30px;
  height: 30px;
  border-radius: var(--r-1);
  border: 2.5px solid var(--rayon, var(--accent));
  background: var(--champ);
  display: grid;
  place-items: center;
  transition: background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
.check-ring .ico {
  width: 18px;
  height: 18px;
  color: #fff;
  transform: scale(0.3);
  opacity: 0;
  transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.12s ease;
}
.check:active .check-ring { transform: scale(0.9); }
.item.checked .check-ring {
  background: var(--rayon, var(--sage));
  border-color: var(--rayon, var(--sage));
  box-shadow: inset 0 0 0 3px rgba(255, 255, 255, 0.16);
}
.item.checked .check-ring .ico { transform: scale(1); opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .check-ring, .check-ring .ico { transition: none; }
}

.empty {
  text-align: center;
  padding: 18px 10px 8px;
}
.empty img {
  width: min(320px, 86%);
  margin: 8px auto 8px;
  border-radius: var(--r-4);
}
.empty h2 {
  font-family: var(--f-titre); font-stretch: 88%;
  margin: 0 0 6px;
  font-size: 26px;
}
.empty p { margin: 0; color: var(--ink-soft); }

.composer input {
  width: 100%;
  border: 0;
  outline: none;
  padding: 12px 12px;
  background: transparent;
  font-size: 16px;
  font-weight: 600;
}

/* ===== Sheets / overlays ===== */
.overlay {
  position: fixed;
  inset: 0;
  background: rgba(var(--voile), 0);
  z-index: 20;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  transition: background .28s ease;
}
.overlay.in { background: rgba(var(--voile), var(--voile-force)); }
.sheet {
  width: min(560px, 100%);
  background: var(--feuille);
  border-radius: var(--r-4) var(--r-4) 0 0;
  padding: 8px 0 0;
  box-shadow: 0 -12px 40px rgba(0,0,0,.12), var(--feuille-bord);
  /* Toutes les feuilles s'ouvrent en grand : voir `measure()` dans
     shell.js. Les 8 % qui restent ne sont pas de la coquetterie — c'est
     la seule surface « en dehors » qu'on puisse toucher pour refermer
     quand la feuille n'a pas de croix. */
  max-height: 94dvh;
  height: 92dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transform: translateY(100%);
  transition: transform .32s cubic-bezier(.2,.8,.2,1);
}
.overlay.in .sheet { transform: translateY(0); }

.sheet-body {
  flex: 1;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
}
.sheet-content { padding: 0 18px calc(24px + var(--safe-b)); }
.handle {
  width: 44px; height: 5px; border-radius: 99px;
  background: var(--line-fort);
  margin: 8px auto 10px;
  flex: 0 0 auto;
  cursor: grab;
}
.sheet h3 {
  font-family: var(--f-titre); font-stretch: 88%;
  font-weight: 800;
  letter-spacing: -0.02em;
  font-size: 26px;
  line-height: 1.15;
  margin: 0 0 var(--sp-3);
}
/* UN TITRE DE FEUILLE N'EST PAS COLLÉ À CE QUI SUIT.

   Six pixels le séparaient du premier champ : « Ajouter à la liste »
   touchait « Article », comme si le titre était la légende du champ. Douze
   partout — sauf devant sa ligne de précision, qui lui appartient. */
.sheet h3:has(+ .sheet-meta) { margin-bottom: var(--sp-1); }
.sheet h4 { margin: 18px 0 9px; }
.sheet .field > span:first-child { margin-bottom: 9px; }
.sheet-meta {
  margin: 0 0 12px;
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 600;
  /* Elle cite souvent un nom de recette — « Une déclinaison de “…” » — et
     un nom de plat n'a pas de longueur maximale. Sans ça, la citation
     passait sous le bord droit de la feuille. */
  overflow-wrap: anywhere;
}
/* Les boutons du bas respirent.

   Ils étaient collés au dernier champ — 8 px — et on les prenait pour la
   suite du formulaire : « Supprimer » finissait à un demi-centimètre de
   la liste des ingrédients. Un cran d'air franc, et un filet au-dessus,
   disent que la fiche est finie et que ce qui suit décide de son sort. */
.sheet-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 26px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}
/* Deux boutons se partagent la largeur ; un seul la prend toute. Une
   moitié de bouton centrée au bas d'une fiche n'a l'air de rien. */
.sheet-actions:has(> :only-child) { grid-template-columns: 1fr; }
/* LE CADDIE NE DOIT PAS COUPER « AJOUTER À LA LISTE » EN DEUX.

   Sur un téléphone, deux moitiés égales laissent 170 px au bouton : le
   pictogramme plus le texte n'y tiennent plus, et le libellé passait sur
   deux lignes. Le bouton d'à côté (« Supprimer », « Retirer ») prend sa
   largeur naturelle et laisse le reste à celui qui porte l'icône. */
@media (max-width: 480px) {
  .sheet-actions:has(> :nth-child(2):last-child > .ico) { grid-template-columns: auto minmax(0, 1fr); }
  .sheet-actions:has(> :nth-child(2):last-child > .ico) > .btn { padding: 0 14px; white-space: nowrap; }
}
.qty-row { display: flex; gap: 8px; align-items: center; }
/* PAS DE PLATEAU SOUS LE COMPTEUR.

   Les trois pièces — moins, le nombre, plus — étaient posées sur un fond
   crème arrondi, qui les réunissait en un objet. Elles n'en avaient pas
   besoin : chacune porte déjà son fond blanc et son filet, et le plateau
   n'ajoutait qu'une couleur de plus au milieu d'un formulaire qui en a
   déjà. Reste l'espacement, qui est ce qui les tenait vraiment ensemble.

   Et les trois font LA MÊME HAUTEUR. Le champ était plus court de dix
   pixels que les boutons qui l'encadrent : posé au milieu, il creusait
   la rangée en son centre — exactement là où se lit le nombre. */
.stepper {
  display: flex; align-items: center; gap: 6px;
  background: transparent;
  border-radius: var(--r-3);
  padding: 0;
}
.stepper button {
  width: 44px; height: 44px; border-radius: var(--r-2);
  background: var(--champ); font-size: 22px; font-weight: 700;
  border: 1px solid var(--line);
}
.stepper b { min-width: 28px; text-align: center; }
/* Le nombre est un CHAMP, pas un affichage : c'est ce qui permet d'écrire
   « 500 » d'un coup au lieu de dix appuis sur « + ». Encore faut-il le
   savoir — il n'avait ni fond ni cadre, et se lisait comme le compteur
   d'entre les deux boutons. Un fond blanc et un filet fin le disent, sans
   lui donner l'allure d'un champ de formulaire : il reste au milieu de
   son compteur. */
.stepper .step-val {
  width: 62px; min-width: 0;
  border: 1px solid var(--line);
  background: var(--champ);
  padding: 0;
  text-align: center; font-weight: 700; font-size: 17px;
  font-variant-numeric: tabular-nums;
  border-radius: var(--r-2);
  height: 44px;
}
.stepper .step-val:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-pale);
}
.mini-step .step-val { width: 52px; font-size: 15px; height: 38px; border-radius: var(--r-2); }
.mini-step button { width: 38px; height: 38px; border-radius: var(--r-2); font-size: 19px; }

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--tabbar) + 16px + var(--safe-b));
  transform: translateX(-50%);
  background: var(--nuit);
  color: #fff;
  padding: 10px 16px;
  border-radius: var(--r-2);
  font-weight: 600;
  z-index: 40;
  max-width: 90%;
}

.settings-block { margin-bottom: 18px; }
.settings-block h4 { margin: 0 0 8px; }
.pane-tabs.apparence { grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 0; }
.settings-block .hint + .btn { margin-top: var(--sp-3); }

/* ===== Les Réglages, rangés en deux entrées =====

   Une page de réglages qui déroule tout est une page où l'on ne trouve
   rien : les huit encarts pesaient tous pareil, et celui qu'on cherchait
   était toujours au milieu. Deux entrées ouvrent chacune leur écran, et
   chacune répond à une question — « qui je suis » et « comment je remets
   le garde-manger d'aplomb ».

   Le gabarit est celui des lignes du reste de l'app : une vignette, deux
   lignes de texte, un chevron. On n'invente pas une forme pour les
   Réglages. */
/* HUIT PIXELS ENTRE UN TITRE ET CE QU'IL TITRE.

   Six séparaient « Réglages » de la première entrée : la ligne semblait
   accrochée au nom de l'écran plutôt que posée dessous. La même mesure
   vaut pour les intitulés de section — « VOUS », « ILLUSTRATIONS » — et
   pour ce qui les suit, sans quoi chaque sous-partie respirait
   autrement selon que son premier élément portait ou non une marge
   haute.

   Les marges de deux frères se FONDENT en une seule, la plus grande :
   les huit pixels posés ici l'emportent sur les six du titre, et il n'y
   a rien à retrancher ailleurs. */
.sheet h3 + .settings-block { margin-top: 8px; }
/* Les deux menus — Réglages, Entretien du garde-manger — laissent 16 px
   pleins sous leur titre. La marge du titre est retirée plutôt que
   comptée : fondues ou additionnées selon le conteneur, les deux marges
   ne donneraient pas le même écart partout. */
.sheet h3:has(+ .set-navs) { margin-bottom: 0; }
.sheet h3 + .set-navs { margin-top: 16px; }
.settings-block > h4 { margin-bottom: 8px; }
.settings-block > h4 + * { margin-top: 0; }

.set-navs { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
.set-nav {
  display: grid;
  grid-template-columns: 42px minmax(0, 1fr) 22px;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px 14px;
  text-align: left;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
}
.set-nav:active { background: var(--papier); transform: translateY(1px); }
.set-nav-ico {
  display: grid; place-items: center;
  width: 42px; height: 42px;
  border-radius: var(--r-2);
  background: var(--papier-2);
  color: var(--accent-fonce);
}
.set-nav-ico .ico { width: 22px; height: 22px; }
.set-nav-txt { min-width: 0; display: block; }
.set-nav-txt b { display: block; font-size: 16px; }
.set-nav-txt small {
  display: block;
  margin-top: 2px;
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.35;
}
.set-nav-go { color: var(--ink-soft); display: grid; place-items: center; }
.set-nav-go .ico { width: 20px; height: 20px; transform: rotate(-90deg); }

/* Les plats écartés : des lignes de recette (`.prod`), la croix dans la
   dernière case. La feuille a déjà ses marges — la ligne n'en rajoute
   pas. */
.skip-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.skip-list .prod { margin: 0; }
.skip-row .voice-x { justify-self: center; }

.presence { display: inline-flex; align-items: center; gap: 6px; }

body.shop .top { display: none; }
body.shop .item { min-height: 58px; }
body.shop .item-name { font-size: 17px; }
body.shop .filters:not(.shop-filters) { display: none; }
body.shop .tabbar { display: none; }
body.shop .list { padding-bottom: calc(18px + var(--safe-b)); }
.shop-bar { padding: calc(8px + var(--safe-t)) 12px 8px; }
.shop-progress { margin-bottom: 8px; }
.shop-row { display: flex; align-items: center; gap: 8px; }
/* En mode magasin le rail prend toute la rangée, la croix à sa droite :
   ses marges d'écran n'ont plus lieu d'être, la rangée porte déjà les siennes. */
.shop-filters { flex: 1; width: auto; max-width: none; margin: 0; overflow: auto; }
/* La sortie du mode magasin : la MÊME croix que celle des feuilles
   glissantes (voir `.sheet-close`), au format de la rangée de filtres
   qu'elle termine. Un carré, pas un mot : c'est le signe de la sortie
   dans cette app, et il laisse la place aux trois filtres. */
.btn-exit-shop {
  flex: 0 0 auto;
  width: 36px; height: 36px;
  padding: 0;
  border-radius: var(--r-2);
  background: var(--champ);
  border: 1.5px solid var(--line);
  color: var(--ink-soft);
  font-weight: 700;
  font-size: 14px;
  display: grid;
  place-items: center;
}
.btn-exit-shop:active { background: var(--papier); }

/* `translateZ(0)` n'est pas une coquetterie.

   Sur iOS, un élément `position: fixed` qui n'a pas sa propre couche de
   composition est repeint par le moteur APRÈS le contenu défilant : le
   temps d'un défilement rapide — et le semainier est la page la plus
   longue de l'app — la barre reste à la position qu'elle avait, donc
   sous le bord de l'écran, puis revient d'un coup quand le doigt
   s'arrête. On la voit disparaître et réapparaître.

   La promouvoir en couche à part la fait suivre le cadre plutôt que le
   document. C'est la parade habituelle, et elle ne coûte qu'une couche
   de plus — la barre en a de toute façon une, elle porte un flou
   d'arrière-plan. */
.tabbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 12;
  transform: translateZ(0);
  will-change: transform;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 0;
  padding: 0 10px var(--safe-b);
  background: var(--surface);
  border-top: 1px solid var(--line);
}
/* L'onglet actif n'est plus un bouton plein et ombré : trois pavés dont
   un orange criaient plus fort que la liste elle-même. Un trait d'encre
   au bord haut suffit à dire où l'on est. */
.tab {
  position: relative;
  min-height: 60px;
  font-weight: 600;
  background: none;
  color: var(--ink-soft);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  font-size: 12px;
}
.tab b { font-size: 22px; line-height: 1; font-weight: 700; filter: contrast(1.15); }
.tab.on { color: var(--ink); font-weight: 700; }
.tab.on::before {
  content: "";
  position: absolute;
  top: 0; left: 22%; right: 22%;
  height: 3px;
  border-radius: 0 0 3px 3px;
  background: var(--ink);
}

.catalog-top {
  position: sticky;
  top: 0;
  z-index: 6;
  padding: calc(10px + var(--safe-t)) 16px 10px;
  background: color-mix(in srgb, var(--papier) 88%, transparent);
  backdrop-filter: blur(16px);
}
body.shop .catalog-top { padding-top: 10px; }
.search-row {
  display: flex;
  gap: 8px;
  align-items: center;
}
/* Le champ de recherche et les boutons qui commandent la même liste.

   `flex` plutôt qu'une grille : le champ prend ce qui reste, et les
   boutons gardent leur largeur naturelle — celle de leur texte pour
   « Nouveau », un carré pour l'entonnoir. À la saisie, ils disparaissent
   du gabarit et le champ récupère toute la ligne, sans transition qui
   ferait sauter la page. */
.search-row.solo {
  display: flex;
  align-items: stretch;
  gap: 8px;
  margin-top: 8px;
}
.search-row.solo { align-items: center; }
.search-row.solo .search-box { flex: 1; min-width: 0; align-self: stretch; }
/* L'entonnoir garde EXACTEMENT l'allure qu'il a dans la barre de titre du
   garde-manger — même rayon, même contour clair sur blanc. C'est le même
   bouton, posé une ligne plus bas parce que la feuille n'a pas de barre
   de titre ; s'il changeait d'aspect en changeant de place, on croirait à
   un autre bouton.

   Et la même HAUTEUR que le champ, partout : posés côte à côte, deux
   pixels d'écart se voient — c'est ce qui donne l'impression que l'un des
   deux flotte. La règle vit sur `.filter-btn` (plus bas) plutôt qu'ici,
   pour valoir aussi dans la barre de titre, où l'entonnoir n'a pas de
   champ à côté de lui mais doit rester le même bouton. */
.search-row.solo .icon-btn { flex: 0 0 auto; }
.search-row.solo .btn-new {
  flex: 0 0 auto;
  min-height: var(--search-h);
  border-radius: var(--r-3);
}
/* Le seul bouton plein de l'écran : c'est le geste qu'on ne trouve nulle
   part ailleurs, et il doit se voir sans lire. */
.btn-new {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 0 15px;
  background: var(--accent);
  color: #fff;
  font-size: 15px;
  font-weight: 700;
  white-space: nowrap;
}
.btn-new:active { background: var(--accent-appui); transform: translateY(1px); }
.search-row .back, .top-row .back {
  width: 46px; height: 46px;
  border-radius: var(--r-3);
  background: var(--champ);
  border: 1px solid var(--line);
  font-size: 20px;
}
/* Une seule hauteur pour tout ce qui compose une rangée de recherche :
   le champ, l'entonnoir, « Nouveau ». Écrite une fois, elle ne peut plus
   diverger d'un écran à l'autre. */
:root { --search-h: 48px; }

.search-box {
  display: flex;
  align-items: center;
  background: var(--champ);
  border: 1.5px solid var(--line);
  border-radius: var(--r-3);
  min-height: var(--search-h);
  padding: 0 12px;
}
.search-box input {
  width: 100%;
  border: 0;
  outline: none;
  background: transparent;
  font-size: 16px;
  font-weight: 600;
  min-width: 0;
}
/* DANS UNE FEUILLE AUSSI.

   `.sheet input` habille tous les champs d'une feuille — bord, coins
   ronds, rembourrage, et au focus un liseré terracotta avec sa lueur. Un
   champ de recherche en héritait alors qu'il a déjà sa boîte : on
   obtenait un cadre dans un cadre, et surtout un rectangle rouge posé de
   travers au premier appui, là où les mêmes recherches, dans une page,
   ne montrent rien du tout.

   La boîte reprend donc la main : le champ redevient transparent, et
   c'est elle qui s'éclaire — une seule fois, à la bonne forme. */
.sheet .search-box input {
  border-radius: 0;
  padding: 0;
  border: 0;
}
.sheet .search-box input:focus { box-shadow: none; border-color: transparent; }
.search-box:focus-within { border-color: var(--accent); }
.crumb {
  margin-top: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-soft);
}

/* Le garde-manger n'a plus de grille de cases : rayons, sous-rayons,
   articles et recettes sont tous des LIGNES, dans le même `.catalog-list`
   (voir `rayonRow` dans views.js). Les styles `.grid`, `.tile`,
   `.tile-thumb`, `.tile-art` et `.tile-tools` ont disparu avec elles. */

/* ===== Les entêtes : rayon, sous-rayon, article =====

   L'illustration remplissait toute la bande, recadrée, avec le titre
   posé dessus dans un dégradé noir. Deux ennuis : les illustrations
   livrées sont CARRÉES sur fond crème, donc une bande large en montrait
   surtout le fond ; et ce fond crème sur une page crème ne dessinait
   aucune limite — l'entête ne se voyait pas.

   Renversement : l'aplat passe DERRIÈRE, franc et hachuré, et
   l'illustration se pose dessus en plus petit, légèrement de travers,
   débordant en haut et en bas. Elle redevient une image qu'on regarde,
   et non une texture.

   La teinte dit où l'on est : celle du RAYON, posée en ligne par
   views.js et sheets.js (`--hero-bg`, voir `rayonTon`) — la fiche des
   Courgettes est verte comme la bande des Légumes dans la liste. Ce qui
   n'appartient à aucun rayon, une recette, prend l'encre. Toutes les
   teintes portent du blanc au-dessus de 4,5:1 : la bande a un titre ET
   un sous-titre à faire lire. */
.hero,
.sheet-hero {
  --hero-bg: var(--accent-fonce);
  --hero-edge: rgba(255, 255, 255, .22);
  position: relative;
  border-radius: var(--r-3);
  background-color: var(--hero-bg);
  /* Les hachures : le motif « papier découpé » des illustrations, repris
     au fond. Larges et très pâles — fines, elles moiraient sur les écrans
     à forte densité ; marquées, elles rendaient le titre illisible.

     Elles ont été ÉLARGIES d'un bon tiers : à 12 px de trait pour 26 px
     de pas, sur une bande de 100 px de haut, on comptait dix barres et le
     fond se lisait comme une texture rayée. Deux fois plus larges, ce
     sont quatre ou cinq obliques franches — un motif, pas un grésil. */
  background-image:
    repeating-linear-gradient(135deg,
      rgba(255, 255, 255, .12) 0 22px,
      rgba(255, 255, 255, 0) 22px 48px);
  box-shadow: inset 0 0 0 1.5px var(--hero-edge);
  color: #fff;
}
.hero.t-cat, .sheet-hero.t-cat,
.hero.t-sub, .sheet-hero.t-sub,
.hero.t-prod, .sheet-hero.t-prod,
.hero.t-recipe, .sheet-hero.t-recipe { --hero-bg: var(--nuit); }

/* La marge du bas est large : la vignette pend SOUS la bande, il lui faut
   la place de le faire sans mordre ce qui suit. */
.hero {
  margin: 6px var(--sp-4) 44px;
  /* Huit dixièmes de la hauteur de l'illustration (120 px avec sa
     bordure) : la bande est assez haute pour tenir un titre sur deux
     lignes, l'image la dépasse encore assez pour qu'on voie qu'elle est
     POSÉE dessus. Les deux valeurs vont ensemble — toucher à l'une sans
     l'autre casse la proportion. */
  min-height: 96px;
  display: flex;
  align-items: center;
}
.hero-open {
  display: flex;
  align-items: center;
  /* Il doit faire toute la hauteur de la bande, sinon la vignette pend
     sous LUI et non sous elle : le débord se voyait à peine. */
  align-self: stretch;
  gap: 14px;
  width: 100%;
  padding: 0 12px 0 0;
  text-align: left;
  color: inherit;
  border-radius: var(--r-3);
}
.hero-open:active { transform: scale(.995); }

/* La vignette : plus petite que la bande, encadrée, posée un peu de
   travers, et calée en BAS À GAUCHE de façon à pendre franchement sous la
   bande. Centrée et à peine débordante, elle ressemblait à un logo posé
   dans un bandeau ; accrochée par le haut et lâchée par le bas, elle se
   lit comme une photo collée sur une page.

   `align-self` plutôt qu'une marge négative haute et basse : le débord
   reste le même que le titre tienne sur une ou trois lignes. */
/* Plus de cadre blanc, plus d'ombre portée.

   Le liseré blanc de 3 px faisait un carré blanc autour d'une image qui
   est déjà posée sur un aplat crème : deux cadres l'un dans l'autre, et
   le plus voyant des deux n'était pas l'illustration. L'ombre, elle,
   décollait la vignette comme une photo collée — joli une fois, lourd sur
   chaque écran du garde-manger, et illisible sur les teintes foncées.

   À la place, un filet à peine plus foncé que le papier de
   l'illustration : il ferme la forme sans rien ajouter. C'est la même
   idée que `--paper-img` et il se règle au même endroit. */
.hero-art {
  flex: 0 0 auto;
  align-self: flex-end;
  display: block;
  width: 116px;
  height: 116px;
  margin: 0 0 -34px 14px;
  border-radius: var(--r-3);
  border: 2px solid var(--paper-img-edge);
  background: var(--paper-img);
  overflow: hidden;
  transform: rotate(-2.5deg);
}
.hero-art > img { width: 100%; height: 100%; object-fit: cover; }
.hero-art > img.packed { object-fit: contain; }

/* L'APPAREIL PHOTO SUR LA VIGNETTE, à la création d'une fiche.

   Une image ne dit pas d'elle-même qu'elle s'ouvre : c'est le même
   constat que pour la tête du cuisinier, et la même réponse — un
   pictogramme posé dans un coin, sur un rond clair qui le décolle du
   dessin quel qu'il soit. En bas à droite plutôt qu'au centre : au
   centre il masquerait ce qu'on est en train de choisir, et sur un carré
   vide il ressemblerait à l'illustration elle-même. */
.hero-art { position: relative; }
.hero-art-pick {
  position: absolute;
  right: 5px; bottom: 5px;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 1.5px solid var(--paper-img-edge);
  background: var(--surface);
  color: var(--accent-fonce);
  box-shadow: 0 2px 6px rgba(43, 38, 34, .18);
}
.hero-art-pick .ico { width: 16px; height: 16px; stroke-width: 2; }
.hero-art-pick:active { background: var(--papier); transform: scale(.92); }

.hero-cap { min-width: 0; flex: 1; padding: 12px 0; }
/* Le sous-titre était à 82 % d'opacité : sur un aplat coloré, cela
   revient à écrire en gris clair, et le contraste retombait sous le seuil
   pendant qu'on croyait écrire en blanc. Il est plein, et c'est la
   graisse qui le distingue du titre. */
.hero-cap small {
  display: block;
  font-weight: 700;
  font-size: 13px;
  color: rgba(255, 255, 255, .95);
}
/* UN TITRE QUI SE COUPE SE COUPE AVEC UN TRAIT D'UNION.

   « anywhere » rompt le mot au pixel près, sans rien : « Cabilla / ud au
   four » se lit comme deux mots dont aucun n'existe. La césure
   automatique fait le travail dans les règles — elle coupe entre deux
   syllabes et pose le trait — et la coupure brutale ne reste qu'en
   dernier recours, pour un mot plus large que la bande. */
.hero-cap strong {
  display: block;
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 25px;
  line-height: 1.08;
  letter-spacing: -0.03em;
  overflow-wrap: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}
/* ===== Le fil d'Ariane =====

   Il appartient à la barre de recherche qui le précède, pas à l'entête
   qui le suit : collé au champ, détaché de la bande. L'inverse — de l'air
   au-dessus, rien en dessous — le faisait lire comme le début de l'entête
   et donnait deux titres l'un sur l'autre. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px;
  margin: -6px 0 14px;
  padding: 0 16px;
  font-size: 13px;
  font-weight: 700;
}
.crumb {
  padding: 4px 8px;
  border-radius: var(--r-2);
  color: var(--accent-fonce);
  text-decoration: underline;
  text-decoration-color: var(--accent-pale);
  text-underline-offset: 3px;
}
.crumb:active { background: var(--papier-2); }
.crumb.on { padding: 4px 2px; color: var(--ink-soft); text-decoration: none; }
/* Les chevrons du chemin.

   Ils étaient posés dans le flux du texte, en `align-items: center` sur
   une rangée dont les enfants n'ont pas la même hauteur : le « › » d'un
   glyphe de 13 px se calait sur le milieu de sa propre boîte, pas sur la
   ligne de base des noms d'étage, et il flottait un ou deux pixels trop
   haut — visible surtout entre deux noms, où l'on a les trois côte à
   côte. Une boîte de largeur fixe, centrée sur toute la hauteur de la
   ligne, et le chevron retombe pile entre les deux noms.

   Les noms d'étage sont soulignés et le chevron ne doit pas l'être : il
   n'est pas un lien, il est la ponctuation entre deux liens. */
.crumb-step { display: inline-flex; align-items: stretch; gap: 0; min-width: 0; }
.crumb-sep {
  flex: 0 0 auto;
  width: 18px;
  display: inline-grid;
  place-items: center;
  color: var(--ink-mute);
  text-decoration: none;
}
.crumb-sep .ico { width: 13px; height: 13px; stroke-width: 2.6; }
.crumbs .crumb { display: inline-flex; align-items: center; }

/* Sur un écran étroit, un titre de sous-rayon un peu long tenait sur
   trois lignes et poussait la bande à la hauteur d'une carte. */
@media (max-width: 420px) {
  .hero { min-height: 84px; }
  .hero-art { width: 100px; height: 100px; margin-bottom: -30px; }
  .hero-cap strong { font-size: 21px; }
}

/* LES LIGNES DU GARDE-MANGER, DANS UN PANNEAU — comme les articles de
   la liste. Chaque ligne porte un filet en haut et à gauche ; ceux qui
   tombent au bord du panneau sont rognés par lui (`overflow: hidden`),
   ce qui dessine une grille propre à une colonne comme à deux, même
   quand la dernière rangée est incomplète. */
.catalog-list { margin: 0 var(--sp-4) calc(var(--tabbar) + 22px + var(--safe-b)); }
.catalog-list:has(> .prod) {
  border-radius: var(--r-3);
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--line);
  overflow: hidden;
}
.catalog-list > .prod {
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: 0 -1px 0 var(--line), -1px 0 0 var(--line);
}
.pane-tabs { display: flex; gap: 8px; margin-top: 10px; justify-content: center; }
.pane-tabs .chip { flex: 1; display: flex; align-items: center; justify-content: center; max-width: 160px; }
.icon-btn .funnel { width: 22px; height: 22px; fill: currentColor; }

.week-days { padding: 0 var(--sp-4) calc(var(--tabbar) + 20px + var(--safe-b)); }
.day-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-3); padding: 12px; margin-bottom: 10px; }
.day-name { font-weight: 700; margin-bottom: 8px; }
.day-name > span {
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 19px;
  letter-spacing: -0.01em;
}
.slot { margin-bottom: 10px; }
.slot-h { display: flex; justify-content: space-between; color: var(--ink-soft); font-size: 13px; margin-bottom: 6px; }
.slot-rec { display: flex; align-items: center; gap: 8px; text-align: left; }
.slot-rec img { width: 44px; height: 44px; border-radius: var(--r-2); object-fit: cover; }
.slot-empty { flex: 1; border: 1.5px dashed var(--accent-pale); border-radius: var(--r-2); min-height: 44px; font-weight: 700; color: var(--accent-fonce); background: var(--accent-voile); padding: 0 10px; }
.slot-row { display: flex; gap: 6px; align-items: center; }

/* UN CRÉNEAU REMPLI, SUR TÉLÉPHONE : le plat, ses boutons à droite, et
   l'assiette EN DESSOUS.

   La grille était laissée à son placement automatique — deux colonnes,
   et chaque enfant prend la case suivante. Ça marchait tant qu'il y en
   avait exactement trois (le plat, les boutons, l'assiette). Sur un jour
   PASSÉ ou une semaine validée, il n'y a pas de boutons : le plat et
   l'assiette se retrouvaient côte à côte, et le nom du plat, écrasé à
   trente pixels, disparaissait derrière ses points de suspension.

   Chaque enfant dit donc où il va. La poignée et la rangée de boutons ne
   coexistent jamais — l'une est pour la semaine en suivi, l'autre pour la
   semaine en préparation — et occupent la même case. */
.slot-filled {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 6px;
  align-items: center;
}
.slot-filled > .slot-rec { grid-column: 1; grid-row: 1; }
.slot-filled > .slot-grip,
.slot-filled > .slot-actions-row { grid-column: 2; grid-row: 1; }
.slot-filled > .side-row { grid-column: 1 / -1; }
.slot-actions-row { display: flex; gap: 6px; }
/* Les boutons d'un créneau sont des boutons secondaires comme les autres
   — entonnoir, flèches de semaine, étoile : fond blanc, filet, arrondi
   de contrôle. Ils étaient crème à liseré orangé, une famille à part
   qu'on ne retrouvait nulle part ailleurs. */
.slot-filled .mini, .slot-filled .regen, .regen {
  width: 48px; height: 48px; border-radius: var(--r-2);
  background: var(--surface); font-size: 22px; font-weight: 700;
  display: grid; place-items: center;
  border: 1.5px solid var(--line);
  color: var(--ink-soft);
}
/* LES DEUX BOUTONS POSÉS À CÔTÉ D'UN PLAT SONT PLUS PETITS QUE CEUX D'UN
   CRÉNEAU VIDE, et c'est voulu.

   Un créneau vide propose un geste principal — « ＋ Ajouter un plat » —
   et les deux autres sont sur la même rangée, à la même hauteur : ils se
   valent. Un créneau REMPLI, lui, montre d'abord le plat ; « ✨ » et « ✕ »
   n'y sont que des retouches, et deux carrés de 48 px collés à une
   vignette de recette pesaient autant qu'elle.

   La mesure est reprise des ingrédients d'une fiche recette (`.ing-pick`,
   `.voice-x`) : là-bas aussi, ce sont des boutons de retouche posés au
   bout d'une ligne qui porte le contenu. Même rôle, même taille — c'est
   ce qui fait qu'une app se tient. La couleur, elle, ne bouge pas : ces
   deux-là restent des boutons secondaires (fond blanc, filet), et pas des
   boutons dangereux. */
.slot-filled .regen, .slot-filled .mini {
  width: 36px; height: 36px; font-size: 16px;
}
.mini { width: 36px; height: 36px; border-radius: var(--r-2); background: var(--surface); border: 1.5px solid var(--line); font-weight: 700; }
.prod { margin-left: 0; margin-right: 0; width: 100%; }

/* Un trait ENTRE les lignes, pas après la dernière : sous le dernier
   article, il flottait tout seul au-dessus des boutons de la feuille. */
.linked {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 2px;
  border-top: 1px solid var(--line);
  font-weight: 600;
  text-align: left;
}
.linked:first-of-type { border-top: 0; }
.linked span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.linked img { width: 36px; height: 36px; border-radius: var(--r-2); object-fit: cover; flex: 0 0 auto; }
/* Le groupe entier est encadré : on voit où commence et où finit la liste. */
.linked-list {
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  padding: 2px 12px;
  margin-bottom: 14px;
  background: var(--surface);
}

.prod {
  display: grid;
  grid-template-columns: 52px 1fr 52px;
  gap: 8px;
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  padding: 8px;
  margin: 0 var(--sp-4) 8px;
  min-height: 64px;
}
.prod-ico {
  width: 52px; height: 52px;
  border-radius: var(--r-2);
  object-fit: cover;
  background: var(--paper-img);
}
.prod-ico.packed { object-fit: contain; }
.prod-ico.emoji {
  display: grid; place-items: center;
  font-size: 26px;
}
.prod-add {
  width: 48px; height: 48px;
  border-radius: var(--r-3);
  background: var(--accent);
  color: #fff;
  font-size: 28px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  padding: 0 0 3px;
}

/* La coche du garde-manger ouvert pour DÉSIGNER un ingrédient : le même
   bouton, un autre signe (voir `addGlyph` dans views.js). Le rembourrage
   bas du « + » n'a plus lieu d'être — un pictogramme est déjà centré
   dans son dessin. */
.prod-add-check { padding: 0; }
.prod-add-check .ico { width: 24px; height: 24px; }
/* Le caddie « ajouter à la liste » : un pictogramme, déjà centré dans son
   dessin — le rembourrage bas du « + » n'a plus lieu d'être. */
.prod-add-cart, .prod .prod-add.prod-add-cart { padding: 0; }
.prod-add-cart .ico { width: 24px; height: 24px; stroke-width: 2; }
/* Le même caddie devant le texte d'un bouton « Ajouter à la liste ». */
.btn > .ico { width: 20px; height: 20px; flex: 0 0 auto; }
/* Choisir un plat pour un créneau : là, toute la ligne est le bouton, et
   le « ＋ » n'est qu'un repère. */
.prod-pick { text-align: left; }
/* Le « + » d'un plat qu'on choisit n'est qu'un repère — toute la ligne
   est le bouton. Il se centre donc dans sa case, comme n'importe quel
   pictogramme : collé à 4 px du bord droit d'une colonne large de 52, il
   penchait, et le rembourrage bas hérité du vrai bouton le remontait
   encore de trois pixels. */
.prod-pick .prod-add {
  width: 40px; height: 40px;
  font-size: 24px;
  margin: 0 auto;
  padding: 0;
  pointer-events: none;
}
.prod-pick:active { background: var(--accent-voile); }
/* La coche du « même article » : on désigne, on n'ajoute pas. Elle occupe
   la case du « ＋ », sans son rembourrage bas — un pictogramme est déjà
   centré dans son dessin. */
.prod-pick .prod-check { padding: 0; }
.prod-check .ico { width: 22px; height: 22px; }


@media (min-width: 720px) {
  .tabbar { left: 50%; transform: translateX(-50%); width: min(560px, 100%); }
}

@media (min-width: 720px) {
  .screen.main { max-width: 560px; margin: 0 auto; border-left: 1px solid var(--line); border-right: 1px solid var(--line); background: var(--papier); }
  .composer { left: 50%; transform: translateX(-50%); width: min(560px, 100%); }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}

.voice-wrap { padding: 8px 16px calc(var(--tabbar) + 20px + var(--safe-b)); }
.voice-mic {
  width: 84px; height: 84px; border-radius: 50%;
  margin: 8px auto 10px; display: grid; place-items: center;
  background: var(--champ); border: 3px solid var(--accent-pale);
  font-size: 32px; box-shadow: var(--shadow);
}
.voice-mic.on {
  background: var(--accent); color: #fff; border-color: var(--accent);
  animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse {
  50% { transform: scale(1.06); box-shadow: 0 0 0 10px rgba(42, 68, 112,.18); }
}
.voice-hint { text-align: center; color: var(--ink-soft); font-weight: 600; margin: 0 8px 10px; }
.voice-live {
  background: var(--champ); border: 1.5px dashed var(--accent-pale); border-radius: var(--r-3);
  padding: 12px; min-height: 52px; font-weight: 600; margin-bottom: 14px;
}
/* Une ligne dictée tient sur deux rangées.

   Tout sur une seule — vignette, nom, ce qui a été dit, compteur, croix —
   écrasait le nom à deux ou trois lettres visibles sur un téléphone. Le
   nom occupe donc sa rangée, les commandes la suivante. */
.voice-row {
  display: grid;
  grid-template-columns: 44px 1fr 36px;
  grid-template-areas:
    "ico  name x"
    "foot foot foot";
  gap: 8px 10px;
  align-items: center;
  background: var(--champ); border: 1px solid var(--line); border-radius: var(--r-3);
  padding: 10px; margin-bottom: 8px;
}
/* Les placements en grille sont portés par les ENFANTS DIRECTS d'une
   ligne dictée, pas par les classes seules : `.voice-x` sert aussi de
   bouton « retirer » aux ingrédients d'une recette, et un `grid-area`
   qui ne correspond à rien y créait deux colonnes fantômes — la carte
   d'ingrédient s'étalait alors sur trois rangées. */
.voice-row > .voice-ico { grid-area: ico; }
.voice-row > .voice-name { grid-area: name; }
.voice-row > .voice-x { grid-area: x; }
.voice-row > .voice-foot { grid-area: foot; }

.voice-ico { width: 44px; height: 44px; padding: 0; border-radius: var(--r-2); overflow: hidden; }
.voice-ico img { width: 44px; height: 44px; object-fit: cover; }
.voice-name { min-width: 0; }
.voice-name strong {
  display: block; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.voice-name small {
  display: block; color: var(--ink-soft); font-weight: 500; font-size: 13px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.voice-name small .kind-tag { margin-right: 6px; }
/* RETIRER UNE LIGNE, PARTOUT LE MÊME BOUTON.

   Un ingrédient, une ligne dictée, une variante rattachée : c'est le
   même geste, il n'a aucune raison d'avoir trois dessins. Une seule
   règle les porte tous les trois — changer d'avis ici les change
   ensemble, ce qui est exactement ce qu'on veut. */
.voice-x, .variant-off {
  width: 36px; height: 36px;
  display: grid; place-items: center;
  border-radius: var(--r-2);
  background: var(--danger-pale);
  color: var(--danger);
  font-size: 14px;
  font-weight: 700;
}
.voice-x:active, .variant-off:active { background: #b3261e; color: #fff; }
.voice-foot { display: flex; align-items: center; gap: 8px; }
.voice-foot .integrate { margin: 0; }
.voice-foot .stepper { flex: 0 0 auto; }

/* Reconnu ou non : la différence décide de ce qui partira sur la liste,
   elle mérite mieux qu'un pointillé. */
.voice-row.unknown { border-style: dashed; background: var(--accent-voile); }
.voice-group {
  margin: 14px 0 8px;
  font-size: 15px; font-weight: 700;
  color: var(--ink);
}
.voice-group.soft { color: var(--ink-soft); }
.voice-thinking {
  margin: 0 0 10px; padding: 10px 12px;
  border-radius: var(--r-2); background: var(--accent-voile);
  color: var(--accent-fonce); font-weight: 600; font-size: 14px;
}
.linkish { color: var(--accent-fonce); font-weight: 700; font-size: 12px; padding: 0; }

/* « Ajouter au garde-manger », sous un ingrédient ou un article dicté que
   l'app ne connaît pas.

   C'était un mot souligné de rien, posé sous une ligne de texte : on
   lisait un commentaire — « intégrer au garde-manger », dont acte — sans
   voir qu'il fallait appuyer dessus. Il porte donc maintenant les trois
   signes que l'app donne partout ailleurs à ce qui crée quelque chose :
   un « + », une pastille, et le pointillé de « Nouvelle sous-catégorie »
   et des cases vides du semainier. */
.integrate {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  margin: 6px 0 2px;
  padding: 0 12px;
  border-radius: 999px;
  border: 1.5px dashed var(--accent-pale);
  background: var(--accent-voile);
  color: var(--accent-fonce);
  font-weight: 700;
  font-size: 13px;
  line-height: 1;
  text-align: left;
}
.integrate::before {
  content: "＋";
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
}
.integrate:active {
  background: var(--accent);
  border-color: var(--accent);
  border-style: solid;
  color: #fff;
  transform: translateY(1px);
}
/* L'entête d'une fiche : la même bande hachurée que dans le garde-manger,
   la vignette accrochée à GAUCHE et lâchée bien plus bas. Centrée, elle
   ressemblait à un logo dans un bandeau ; posée à gauche et débordant de
   moitié, elle fait de la bande un papier à en-tête et le titre qui suit
   se range naturellement sous elle. */
/* ===== L'AIR SOUS L'ENTÊTE, LE MÊME PARTOUT =====

   La bande laisse son carré d'illustration DÉBORDER par le bas. Ce qui
   sépare l'entête du contenu n'est donc pas sa marge, mais ce qu'il en
   reste une fois le débordement retiré — et le débordement change avec
   la taille de la vignette, qui change avec le type de fiche et la
   taille de l'écran. Résultat : quatre marges écrites à la main, quatre
   écarts réels différents, tous autour de dix pixels, et personne pour
   s'en apercevoir avant de les mesurer.

   Deux mesures, donc, et une seule addition :

   `--hero-over`  ce dont l'illustration déborde (varie avec la fiche) ;
   `--hero-gap`   l'air qu'on veut voir sous elle (le même partout) ;
   `--hero-tilt`  ce que sa légère inclinaison lui fait gagner en plus.

   Le dernier n'est pas un ajustement à vue : l'illustration est posée de
   travers de deux degrés et demi, et un carré de cent huit pixels tourné
   d'autant descend trois pixels plus bas que sa boîte. Sans le compter,
   l'air mesuré valait treize pixels là où on en avait écrit seize.

   Chaque déclinaison ne redéclare que son débordement ; la marge, elle,
   se calcule. Une vignette qui grandit ne peut plus manger l'air. */
.sheet-hero {
  --hero-over: 40px;
  --hero-gap: 16px;
  --hero-tilt: 3px;
  min-height: 96px;
  margin: 6px 0 calc(var(--hero-over) + var(--hero-gap) + var(--hero-tilt));
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  gap: 12px;
}
.sheet-hero .hero-art {
  width: 118px; height: 118px;
  margin: 0 0 calc(var(--hero-over) * -1) 16px;
  border-radius: var(--r-3);
}
.sheet-hero .hero-art > img { width: 100%; height: 100%; }

/* ----- l'entête qui porte son titre -----

   Une fiche commençait par une bande colorée muette, puis, deux
   centimètres plus bas, un titre en noir sur crème et une ligne grise :
   la bande ne disait pas ce qu'on regardait, et le titre ne disait pas ce
   qu'il titrait. Réunis, ils font un papier à en-tête — « SOUS-CATÉGORIE
   / Agrumes », « ARTICLE / Citrons jaunes » — et la fiche commence par
   l'information plutôt que par une image.

   L'illustration rétrécit d'autant : elle avait la bande pour elle seule,
   elle la partage maintenant avec deux lignes de texte. À 118 px il ne
   restait pas de quoi écrire « Sous-catégorie » sur un téléphone.

   Le texte descend SOUS la rangée de boutons plutôt que de la contourner :
   l'étoile, le crayon et le « … » occupent le coin haut droit, et un
   titre qui passerait dessous serait illisible une fois sur deux, selon
   sa longueur. Le rembourrage haut du texte est calé sur leur hauteur —
   les deux valeurs vont ensemble. */
.sheet-hero.with-cap {
  --hero-over: 28px;
  min-height: 112px;
  padding-right: 14px;
}
.sheet-hero.with-cap .hero-art {
  width: 92px; height: 92px;
  margin: 0 0 calc(var(--hero-over) * -1) 14px;
  border-radius: var(--r-3);
}
/* Le rembourrage haut dépasse la rangée de boutons de quelques pixels :
   collée juste dessous, la ligne de renseignements se lisait comme leur
   légende. Le titre respire d'autant. */
.sheet-hero .hero-cap {
  flex: 1;
  min-width: 0;
  padding: 56px 0 12px;
}
/* Le surtitre d'une bande est un intitulé de section comme les autres :
   même corps, même graisse, même espacement que « NOM » ou
   « PRÉPARATION » plus bas dans la fiche. Seule la couleur change — du
   blanc, puisqu'il est posé sur l'aplat.

   IL S'ÉCRIT EN ENTIER. Il tenait sur trois lignes au plus, la quatrième
   passant sous des points de suspension — au motif qu'une ligne de
   recette est longue et repousserait le titre. Elle le repousse, en
   effet, et c'est très bien : la bande n'a pas de hauteur fixe, elle a
   une hauteur MINIMALE, et rien en dessous ne dépend de la sienne.
   Couper, en revanche, escamote au hasard la moitié de ce qu'on annonce
   — et c'était toujours la même moitié, la fin. */
.sheet-hero .hero-cap small {
  font-size: 13px;
  font-weight: 700;
  line-height: 1.25;
  margin-bottom: 3px;
  overflow-wrap: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}
.sheet-hero .hero-cap strong { font-size: 21px; }

/* LA TÊTE DU CUISINIER MORD SUR LE TITRE.

   Elle est posée au coin bas droit de la bande, et un nom de plat un peu
   long finissait dessous : « Cabillaud au four » passait sous une
   pastille de quarante pixels. On lui rend sa place — le texte s'arrête
   avant, et revient à la ligne comme il le fait déjà pour les titres
   longs. */
.sheet-hero.with-cook .hero-cap { padding-right: 52px; }

/* Sur un écran étroit, un cran plus petit : la ligne d'une recette
   — « REPAS · AVEC ACCOMPAGNEMENT · COURT · FAIT AUJOURD'HUI » — y prend
   trois lignes à treize pixels, deux à douze. On ne perd rien, on
   resserre ; c'est le seul réglage qui vaille, puisqu'on n'ampute plus. */
@media (max-width: 480px) {
  .sheet-hero .hero-cap small { font-size: 12px; }
  .sheet-hero .hero-cap { padding-top: 50px; }
  .sheet-hero.with-cook .hero-cap { padding-right: 48px; }
  .sheet-hero.with-state.with-cook .hero-cap { padding-right: 14px; }
}
/* Des boutons un peu plus bas : c'est ce qui laisse la place au titre
   dessous sans allonger la bande. */
.sheet-hero.with-cap .hero-tools { top: 8px; right: 8px; }
.sheet-hero.with-cap .hero-btn { width: 36px; height: 36px; border-radius: var(--r-2); }
.sheet-hero.with-cap .hero-btn .ico { width: 19px; height: 19px; }
/* ----- qui cuisine, au coin bas droit de la bande -----

   Le coin opposé aux outils, et le seul autre endroit de la bande où
   rien ne passe : l'illustration déborde à gauche, le titre s'arrête
   avant. Elle déborde vers le bas comme l'illustration, ce qui la pose
   SUR la limite entre la bande et la fiche — elle appartient au plat,
   pas à l'image.

   VACANT : quand personne n'est aux fourneaux, elle reste, en gris et en
   pointillés. C'est ce qui pose la question sans un mot — et c'est tout
   le point : le réglage vivait dans le menu « … », où personne ne va
   chercher ce dont il ignore l'existence. */
/* LA RANGÉE DU COIN : l'état du plat, puis la tête de qui le cuisine.

   Elles étaient une seule pastille posée en absolu. À deux, il faut les
   ranger l'une à côté de l'autre — et l'état porte un MOT, donc sa
   largeur change avec lui. Une rangée en `flex` le fait sans qu'on ait
   rien à mesurer ; deux positions calculées auraient demandé de connaître
   la largeur de la première pour poser la seconde.

   C'est la rangée qui déborde maintenant vers le bas, et les pastilles
   qui la suivent : elles se posent SUR la limite entre la bande et la
   fiche, parce qu'elles appartiennent au plat, pas à l'image. */
.hero-badges {
  position: absolute;
  right: 14px; bottom: -16px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.hero-cook {
  position: relative;
  width: 40px; height: 40px;
  flex: 0 0 auto;
  display: grid; place-items: center;
  border-radius: 50%;
  border: 2.5px solid var(--surface);
  background: color-mix(in srgb, var(--ton, #c39a6b) 34%, var(--surface));
  font-size: 20px;
  line-height: 1;
  padding: 0;
  box-shadow: 0 3px 10px rgba(43, 38, 34, .22);
}
/* TOUT LE MONDE : deux visages, et pas de grisaille.

   On avait mis une silhouette grise et décolorée — elle disait
   « quelqu'un, qu'on ne connaît pas », donc une case à remplir. Or
   « n'importe qui fait ce plat » est le cas le plus courant et le plus
   normal : ce n'est pas un manque. Deux visages le disent d'eux-mêmes,
   sur un fond simplement plus sourd que celui d'une personne. */
.hero-cook.tous {
  width: auto;
  min-width: 58px;
  padding: 0 8px;
  border-radius: 999px;
  background: var(--papier);
  font-size: 22px;
}
.hero-cook:active { transform: scale(.92); }

/* Le crayon du coin : il dit que la tête s'ouvre. Petit, posé dehors, et
   dans les couleurs de la fiche — il annonce, il n'appelle pas. */
.hero-cook-pen {
  position: absolute;
  right: -3px; bottom: -3px;
  width: 19px; height: 19px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--surface);
  color: var(--accent-fonce);
  box-shadow: 0 1px 4px rgba(43, 38, 34, .22);
}
.hero-cook-pen .ico { width: 11px; height: 11px; stroke-width: 2.4; }
.hero-cook.fixe .hero-cook-pen { display: none; }
/* La tête de qui a ajouté un article : la même pastille que le
   cuisinier, sans crayon — elle ne se change pas, elle constate. */
.hero-par { font-size: 18px; font-weight: 700; }

/* La pastille du menu porte deux têtes : elle a besoin d'un peu plus de
   large que la colonne d'un emoji seul. */
.pop-menu .duo { font-size: 19px; }

/* EN LECTURE, la tête n'est pas un bouton : elle dit, elle n'ouvre pas.
   Sans l'ombre portée qui, sur une pastille ronde, signale partout
   ailleurs dans l'app qu'on peut appuyer. */
.hero-cook.fixe {
  box-shadow: none;
  border-width: 2px;
  cursor: default;
}

/* Le chemin du retour, en haut de la bande d'une fiche en modification.
   Posé sur l'aplat, au-dessus du titre : c'est la première chose qu'on
   lit, et la seule sortie qui n'écrit rien. */
.hero-back {
  position: absolute;
  top: 8px; left: 14px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px 5px 8px;
  border-radius: var(--r-2);
  background: rgba(255, 255, 255, .2);
  box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, .3);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
}
.hero-back:active { background: rgba(255, 255, 255, .34); }
/* La bande fait de la place au lien : le surtitre descend d'autant, sinon
   ils se recouvrent sur un écran étroit. */
.sheet-hero.with-back .hero-cap { padding-top: 44px; }
/* Sans rangée d'outils (voir `sansOutils` dans sheets.js), le titre n'a
   plus rien à laisser passer au-dessus de lui : la bande se resserre, à
   la hauteur qu'il faut pour que la vignette pende toujours dessous. */
.sheet-hero.sans-outils:not(.with-back) .hero-cap { padding-top: 16px; }
.sheet-hero.with-cap.sans-outils { min-height: 88px; }
.sheet-hero.with-cap.with-state.sans-outils { min-height: 112px; }
/* La bande sans titre est plus courte : la rangée remonte d'autant pour
   ne pas se poser dans le vide sous elle. */
.sheet-hero:not(.with-cap) .hero-badges { bottom: -14px; }

/* ----- l'état de la recette, à gauche de la tête -----

   Même hauteur que la tête, même débordement, même bordure de papier :
   les deux se lisent comme une seule rangée. Le contour et l'encre disent
   l'attente ; le vert plein dit que c'est fait — c'est le vert de
   « Valider la semaine », en haut du semainier, et c'est la même chose
   qu'on fait à l'échelle d'un plat.

   Le mot est là parce qu'une coche seule ne dit pas dans quel sens elle
   va : creuse, elle se lit aussi bien « pas encore » que « bouton pour
   décocher ». */
.hero-state {
  position: relative;
  height: 40px;
  flex: 0 1 auto;
  min-width: 0;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 13px 0 10px;
  border-radius: 999px;
  border: 2.5px solid var(--surface);
  background: var(--surface);
  color: var(--ink);
  font-size: 13.5px;
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
  box-shadow: 0 3px 10px rgba(43, 38, 34, .22);
}
.hero-state b { font-weight: 700; }
.hero-state .ico { width: 20px; height: 20px; flex: 0 0 auto; }
/* Le « ? » de l'état « à valider ».

   Une coche creuse ressemblait à la coche pleine en plus pâle — donc à
   « validée, mais grisée » — quand elle dit le contraire. Le point
   d'interrogation ne ressemble à rien d'autre. C'est le signe qui
   couvrait la vignette du plat ; il n'a pas disparu, il a trouvé un
   endroit qui l'accompagne d'un mot. */
.hero-state-q {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 20px; height: 20px;
  border-radius: 50%;
  border: 2px solid currentColor;
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 13px; font-weight: 600;
  line-height: 1;
}
.hero-state.ok { background: var(--bien-voile); color: var(--sage-deep); }
.hero-state:active { transform: scale(.96); }
/* En lecture seule, elle constate : ni ombre, ni réaction à l'appui. */
.hero-state.fixe {
  box-shadow: none;
  border-width: 2px;
  cursor: default;
}
.hero-state.fixe:active { transform: none; }

/* LA BANDE S'ALLONGE POUR LA RANGÉE.

   La tête seule mordait sur la droite du titre, et 52 px de réserve
   suffisaient. Une gélule qui porte un mot en demande le triple : le
   titre n'aurait plus eu de quoi s'écrire. La rangée prend donc sa propre
   bande, SOUS le texte — d'où la hauteur en plus, et le rembourrage bas
   qui laisse huit pixels entre le titre et la coche.

   Vingt-quatre pixels de la rangée sont dans la bande, seize dépassent :
   32 − 24 = 8, et c'est l'air demandé. */
.sheet-hero.with-cap.with-state { min-height: 140px; }
.sheet-hero.with-state .hero-cap { padding-bottom: 32px; }
.sheet-hero.with-state.with-cook .hero-cap { padding-right: 14px; }

.sheet-hero.busy .hero-art > img { filter: blur(2px) saturate(.7); }
.season-field { margin-top: 16px; }
.season-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.prep-row { display: flex; gap: 6px; flex-wrap: wrap; }

/* Sur le volet Articles, l'entonnoir est posé AVANT la liste plutôt que
   dedans : les rayons se rendent en grille sur grand écran, et un bloc
   de filtres rangé dans une case de 320 px n'annoncerait plus rien. Il
   reprend donc à la main les marges latérales de la liste. */
.filter-block.loose { margin: 0 14px 12px; }
.catalog-top .filter-block.loose { margin: 10px 0 8px; }
/* Dans une FEUILLE, les lignes de la liste vont d'un bord à l'autre — pas
   de gouttière de 14 px comme sur l'écran. L'encart, lui, la gardait :
   posé au-dessus de rayons pleine largeur, il paraissait rétréci de
   quatorze pixels de chaque côté. Il prend la même largeur qu'eux. */
.sheet .filter-block.loose { margin-left: 0; margin-right: 0; }
/* DANS SA FEUILLE, L'ENCART N'A PLUS DE CADRE.

   Il en portait un pour se détacher de la liste au milieu de laquelle il
   s'ouvrait. Posé dans une feuille, ce cadre dessine une boîte dans une
   boîte — deux bords à deux millimètres l'un de l'autre, qui ne
   séparent rien. La feuille EST le cadre. */
.sheet-live .filter-block.bare {
  border: 0;
  background: transparent;
  padding: 0;
  margin: 0;
}
/* Le PREMIER intitulé seulement : c'est le titre de la feuille. « Trier
   par », plus bas, reste un sous-titre — sans quoi la feuille annoncerait
   deux fois qu'elle commence. */
/* Ce sont des titres de feuille : ils prennent la taille de `.sheet h3`
   (vingt pixels ici, vingt-six à côté, pour le même rôle). */
.sheet-live .filter-block.bare > .filter-title:first-child {
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin-bottom: var(--sp-3);
}
.filter-block {
  margin: 0 0 14px;
  padding: 10px 12px 6px;
  border: 1.5px solid var(--line);
  border-radius: var(--r-3);
  background: var(--champ);
}
/* La mise en rangée du titre — l'intitulé à gauche, « Tout enlever » à
   droite — vit plus bas, avec les autres réglages de l'encart. */
.filter-title {
  font-weight: 700;
  font-size: 13px;
  margin: 0 0 8px;
  color: var(--ink-soft);
}
.filter-line { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }

/* Chaque filtre dit sur quoi il porte.

   Les quatre rangées se suivaient sans étiquette, et chacune commençait
   par un « Tout ». Tant qu'il n'y avait que trois types de plat on
   devinait ; l'accompagnement en a fait quatre, et « Tout · Repas ·
   Accompagnement · Apéro · Dessert » s'est mis à se lire comme cinq
   choses de même rang — « Tout » passant pour l'une d'elles plutôt que
   pour leur somme.

   Le nom du critère devant sa rangée, en petites capitales, règle la
   question sans rien ajouter à la hauteur : c'est une ligne de plus par
   filtre, et c'est ce qui rend les quatre lisibles d'un coup. */
.filter-row { margin-bottom: 4px; }
.filter-lbl {
  display: block;
  margin: 0 2px 6px;
  font-size: 15px;
  font-weight: 700;
  color: var(--ink);
}
/* Un cran d'air avant chaque intitulé : sans lui, les quatre groupes se
   suivaient sans respiration et l'intitulé se lisait comme la suite de la
   rangée du dessus plutôt que comme le titre de celle du dessous. */
.filter-row + .filter-row { margin-top: 12px; }
.icon-btn.on { border-color: var(--accent); background: var(--papier); }
/* ===== « L'ajouter aussi à la liste » =====

   C'était la case à cocher du système d'exploitation, posée nue au milieu
   d'une fiche : un carré bleu de vingt-deux pixels entre des champs à
   bord crème et des pastilles terracotta. Elle ne ressemblait à rien
   d'autre dans l'app — et surtout pas à la seule autre case qu'on y
   trouve, le rond de la liste de courses.

   Elle emprunte donc les deux vocabulaires qu'elle a sous les yeux : le
   CADRE d'un champ de formulaire (même bord, même rayon, même hauteur,
   même halo au focus) et la COCHE de la liste — sauge quand c'est fait,
   et la même détente au ressort.

   `appearance: none` sur l'input plutôt qu'un faux carré à côté : la case
   reste une vraie case, avec son clavier, son lecteur d'écran et son
   étiquette cliquable. */
.check-row {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 54px;
  margin: 8px 0 14px;
  padding: 0 16px;
  border: 1.5px solid var(--line);
  border-radius: var(--r-3);
  background: var(--champ);
}
.check-row:has(input:focus-visible) {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px rgba(42, 68, 112, .15);
}
.check-row input {
  appearance: none;
  -webkit-appearance: none;
  width: 26px; height: 26px; flex: 0 0 26px;
  margin: 0;
  padding: 0;
  border: 2px solid var(--accent);
  border-radius: var(--r-1);
  background: var(--champ);
  box-shadow: inset 0 0 0 3px rgba(42, 68, 112, .09);
  display: grid;
  place-items: center;
  transition: background .16s ease, border-color .16s ease, box-shadow .16s ease;
}
/* La coche est là dès le départ, simplement rétractée : elle jaillit au
   lieu d'apparaître — c'est le geste de la liste de courses. */
.check-row input::after {
  content: "";
  width: 12px; height: 7px;
  margin-top: -3px;
  border-left: 2.5px solid #fff;
  border-bottom: 2.5px solid #fff;
  transform: rotate(-45deg) scale(.3);
  opacity: 0;
  transition: transform .18s cubic-bezier(.34, 1.56, .64, 1), opacity .12s ease;
}
.check-row input:checked {
  background: var(--sage);
  border-color: var(--sage);
  box-shadow: inset 0 0 0 3px rgba(255, 255, 255, .16);
}
.check-row input:checked::after { transform: rotate(-45deg) scale(1); opacity: 1; }
.check-row input:active { transform: scale(.92); }
.check-row span { line-height: 1.2; font-weight: 600; }

@media (prefers-reduced-motion: reduce) {
  .check-row input, .check-row input::after { transition: none; }
}
/* LA CARTE D'UN INGRÉDIENT, EN DEUX RANGÉES.

   La première porte ce qui décrit l'ingrédient — sa vignette, son nom, sa
   quantité, la croix qui l'efface. La seconde, seulement quand il est
   inconnu du garde-manger, porte les deux sorties qui s'offrent : le
   créer, ou le rattacher à un article existant.

   Elle traverse les trois colonnes, et c'est tout le point : rangées dans
   la colonne du milieu, les deux pastilles disposaient de deux cent
   cinquante pixels sur un téléphone et en demandaient deux cent
   soixante-dix. La seconde passait à la ligne, seule, et se lisait comme
   une troisième chose plutôt que comme l'alternative de la première. */
.ing-card {
  display: grid;
  grid-template-columns: 40px 1fr 36px;
  grid-template-areas:
    "ico main croix"
    "tags tags tags";
  gap: 8px; align-items: start;
  background: var(--champ); border: 1px solid var(--line);
  border-radius: var(--r-3); padding: 8px; margin-bottom: 8px;
  /* Le trait d'insertion du réordonnancement se pose dessus. */
  position: relative;
}
.ing-card > .ing-side { grid-area: ico; }
.ing-card > .ing-main { grid-area: main; }
.ing-card > .ing-side.right { grid-area: croix; }
.ing-card > .ing-tags { grid-area: tags; }
/* Les deux colonnes de boutons, empilées.

   Un ingrédient venu d'une proposition gagne un bouton de chaque côté,
   EXACTEMENT sous celui du dessus et de la même taille : le « ⓘ » sous
   la vignette raconte ce qui a changé, la flèche sous la croix le
   défait. Même gabarit, même colonne, même écart — sans quoi ils
   auraient l'air ajoutés après coup. */
/* LES DEUX COLONNES DE BOUTONS SUIVENT LES DEUX RANGÉES DU MILIEU.

   La colonne du milieu porte le nom (42 px) puis la quantité (42 px). Les
   colonnes qui l'encadrent portaient leurs boutons à leur propre taille,
   empilés sans rapport avec elle : la vignette faisait 40, la croix 36,
   le « ⓘ » et la flèche 36 — et chacun tombait deux ou trois pixels à
   côté de la ligne qu'il accompagne.

   Les rangées sont donc DÉCLARÉES, à la hauteur de ce qu'elles longent, et
   chaque bouton se centre dans la sienne. Rien ne se calcule plus à la
   main : changer la hauteur d'un champ déplace ses voisins avec lui. */
.ing-side {
  display: grid;
  grid-template-rows: var(--ing-row) var(--qty-h, 42px);
  gap: 6px;
  justify-items: center;
  align-content: start;
}
.ing-side > * { align-self: center; }
.ing-side.right { justify-items: end; }

/* ===== UNE SEULE HAUTEUR POUR TOUT CE QUI BORDE UN INGRÉDIENT =====

   La vignette faisait 40, la croix 36, le « ⓘ » 40 d'un côté et 36 de
   l'autre, et les deux pastilles « ＋ au garde-manger » / « ＝ déjà un
   article » 30. Cinq mesures pour cinq boutons de la même carte : rien
   ne s'alignait, et le regard butait sur chaque écart.

   36 px partout — la taille d'une cible qu'on vise du pouce sans qu'elle
   pèse dans une liste de douze lignes. La vignette garde ses 40 : ce
   n'est pas un bouton, c'est une image, et elle est la seule chose à
   regarder de cette colonne. */
/* `--ing-row` est la hauteur du champ du nom, et `--qty-h` celle de la
   rangée des quantités (posée plus bas, avec le compteur). Les deux
   colonnes de boutons s'y accrochent. */
.ing-card { --ing-btn: 36px; --ing-row: 42px; }
/* Le « ⓘ » et la flèche de retour longent la rangée des quantités : ils
   en prennent la hauteur, comme le champ, les deux boutons du compteur et
   le sélecteur d'unité qui la composent. */
.ing-side-btn {
  width: var(--ing-btn); height: var(--qty-h, 42px);
  display: grid; place-items: center;
  border-radius: var(--r-2);
  /* La couleur de l'encart qui a fait la proposition : c'est de là que
     ces deux boutons viennent, et c'est ce qui les rattache à lui. */
  background: var(--surface);
  border: 1.5px solid var(--accent-pale);
  color: var(--accent-fonce);
}
.ing-side-btn .ico { width: 19px; height: 19px; }
.ing-side-btn:active { background: var(--accent); border-color: var(--accent); color: #fff; }

/* LA POIGNÉE D'UN INGRÉDIENT.

   Le même dessin qu'au semainier — dans cette app, « ⠿ » veut dire
   « prends-moi et déplace-moi » — et la même règle de survie :
   `touch-action: none`, sans quoi le navigateur confisque le geste au
   premier millimètre en croyant qu'on fait défiler la page.

   Elle prend la seconde rangée de sa colonne, celle des quantités : donc
   exactement sous la croix, et à la hauteur du sélecteur d'unité. */
.ing-grip {
  width: var(--ing-btn); height: var(--qty-h, 42px);
  display: grid; place-items: center;
  background: none; border: 0; padding: 0;
  color: var(--ink-mute);
  font-size: 18px; line-height: 1; letter-spacing: -2px;
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.ing-grip:active { cursor: grabbing; color: var(--accent-fonce); }
/* LA CARTE SUIT LE DOIGT — une copie, pas l'originale.

   Le même geste que sur la liste de courses, et pour la même raison :
   une ligne qui reste plantée pendant qu'on la déplace ne se lit pas
   comme un déplacement. La copie flotte au-dessus de tout, l'originale
   reste à sa place en transparence pour dire d'où l'on part, et rien ne
   change de rang avant le lâcher. */
.ing-card.ing-lifted { opacity: .32; }
.ing-card.ing-ghost {
  position: fixed;
  z-index: 60;
  margin: 0;
  pointer-events: none;
  border-color: var(--accent);
  box-shadow: 0 12px 28px rgba(43, 38, 34, .28);
  transform: scale(1.02);
}
/* Les étiquettes du bas d'une carte — « ＋ au garde-manger », « ＝ déjà
   un article » — n'ont rien à faire dans une copie qu'on promène. */
.ing-card.ing-ghost .ing-tags { display: none; }
/* OÙ L'INGRÉDIENT VA TOMBER — UN TRAIT, ET RIEN D'AUTRE.

   Les autres cartes ont porté un liseré pointillé le temps du geste, pour
   dire « voilà les places ». C'était une place de trop : la liste entière
   se mettait à clignoter au moment précis où l'on cherche UN endroit, et
   les pointillés faisaient lire les lignes comme des trous plutôt que
   comme des ingrédients. Le trait suffit — il désigne, il n'énumère
   pas. */
/* Le trait d'insertion : au bord haut ou au bord bas de la carte
   franchie, selon le côté où l'on lâchera. Un trait plutôt qu'un fond :
   il dit un ENTRE-DEUX, ce qu'un rectangle allumé ne sait pas dire. */
#r-ings.reordering .ing-card.ing-over::before {
  content: "";
  position: absolute;
  left: 8px; right: 8px;
  height: 3px;
  border-radius: 999px;
  background: var(--accent);
}
#r-ings.reordering .ing-card.ing-over-up::before { top: -5px; }
#r-ings.reordering .ing-card.ing-over-down::before { bottom: -5px; }

/* La croix répond au NOM : c'est la ligne qu'elle efface, et la première
   rangée de sa colonne est à sa hauteur — elle s'y centre d'elle-même,
   sans décalage à calculer. */
.ing-qty { display: flex; gap: 6px; align-items: center; margin-top: 6px; }
.ing-qty .picker { flex: 1; min-height: 40px; border-radius: var(--r-2); padding: 6px 8px 6px 12px; font-size: 15px; }
#p-to-list, #r-to-list { width: 100%; margin: 8px 0 12px; }
/* Quatre pictogrammes, et rien d'autre.

   Il y avait le nom sous chaque icône, en 10 px : trop petit pour être lu
   d'un coup d'œil, assez gros pour voler la moitié du bouton à l'icône —
   qui plafonnait donc à 22 px, trop petite pour se passer du nom. Les
   deux se gênaient, et on lisait quatre étiquettes avant de choisir.

   Sans le mot, l'icône double de taille et les quatre saisons se
   reconnaissent instantanément : une pousse, un soleil, une feuille, un
   flocon. Le nom reste dans l'infobulle et pour le lecteur d'écran, où il
   ne coûte aucune place. */
/* Fond BLANC et bord visible au repos : le crème sur le crème de la
   fiche ne dessinait aucun bouton — on voyait quatre icônes flottantes,
   et rien ne disait qu'on pouvait appuyer dessus. C'est le même vêtement
   que les pastilles « Court / Moyen / Long » juste au-dessus, qui font le
   même travail. */
.season {
  display: grid; place-items: center;
  min-height: 44px; padding: 2px;
  border-radius: var(--r-2);
  background: var(--champ);
  border: 1.5px solid var(--line);
  color: var(--ink);
}
/* Chaque saison garde sa couleur au repos — c'est ce qui les rend
   reconnaissables d'un coup d'œil, sans lire les libellés. Sélectionnée,
   l'icône passe en blanc : c'est le fond qui porte alors la couleur. */
/* La même hauteur que les puces « Court / Moyen / Long » juste au-dessus :
   deux rangées de boutons qui font le même travail n'ont pas à avoir deux
   tailles. L'icône se règle sur ce qui reste. */
.season .ico { width: 26px; height: 26px; stroke-width: 1.7; }
.season[data-season="spring"] { color: var(--sage-deep); }
.season[data-season="summer"] { color: var(--etiquette-fonce); }
.season[data-season="autumn"] { color: var(--saison-automne); }
.season[data-season="winter"] { color: var(--saison-hiver); }
.season.on { color: #fff; }
/* La même icône, sur une pastille de filtre : plus grande elle aussi,
   puisque c'est tout ce que la pastille montre. */
.season-ico { display: inline-flex; }
.season-ico .ico { width: 26px; height: 26px; vertical-align: middle; }
.chip.chip-ico { padding: 4px 12px; min-width: 52px; justify-content: center; }
/* Le printemps est vert : c'est la seule saison qu'une pousse
   représente, et le rose la faisait lire comme une fleur d'été. */
.chip [data-saison="spring"], .chip.saison-spring { color: var(--sage-deep); }
.chip.saison-summer { color: var(--etiquette-fonce); }
.chip.saison-autumn { color: var(--saison-automne); }
.chip.saison-winter { color: var(--saison-hiver); }
.chip.active[class*="saison-"] { color: #fff; }
/* Sélection d'une saison.

   Le libellé est parti, mais le fond sélectionné reste profond : c'est
   lui qui doit trancher avec les trois autres boutons, et un terracotta
   clair ne se distinguait pas du crème de repos.

   Et l'emoji, lui, ne se peint pas : 🍂 est une feuille brune, et
   sélectionnée elle disparaissait dans le bouton — quelle que soit la
   couleur du fond. Les quatre saisons sont donc dessinées (icons.js) et
   suivent `currentColor`, comme les onglets l'avaient déjà fait pour la
   même raison. */
.season.on {
  background: var(--accent-nuit);
  color: #fff;
  border-color: var(--accent-nuit);
}

/* Les autres noms d'un article : un champ par alias, une croix pour
   retirer, et une ligne d'ajout en dessous. */
.alias-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
.alias-row input {
  flex: 1; min-width: 0; min-height: 44px;
  border: 1.5px solid var(--line); border-radius: var(--r-2); padding: 0 10px; background: var(--champ);
}
.alias-del {
  flex: 0 0 auto; width: 44px; min-height: 44px;
  border-radius: var(--r-2); border: 1.5px solid var(--line);
  background: var(--champ); color: var(--danger); font-weight: 700;
}
.alias-del:active { background: var(--danger-pale); }
.alias-new { display: flex; gap: 6px; align-items: center; }
.alias-new input {
  flex: 1; min-width: 0; min-height: 44px;
  border: 1.5px solid var(--line); border-radius: var(--r-2); padding: 0 10px; background: var(--champ);
}
.alias-new .btn { flex: 0 0 auto; width: auto; padding: 0 14px; min-height: 44px; }

/* ---------- la recette en lecture ----------

   Compacte parce qu'on la lit debout, souvent d'une main : une ligne par
   ingrédient, pas de champ de saisie, et la préparation d'un bloc. */

.ing-read { list-style: none; margin: 0; padding: 0; }
.ing-read li + li { border-top: 1px solid var(--line); }
.ing-read button {
  display: flex; align-items: center; gap: 10px;
  width: 100%; padding: 7px 2px;
  background: none; border: 0; text-align: left;
}
.ing-read img {
  width: 34px; height: 34px; flex: 0 0 auto;
  border-radius: var(--r-2); object-fit: cover; background: var(--paper-img);
}
.ing-read-name {
  flex: 1; min-width: 0;
  font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ing-read b { flex: 0 0 auto; color: var(--ink-soft); font-size: 14px; font-weight: 700; }
/* Le « + » d'un ingrédient que le garde-manger ne connaît pas : appuyer
   sur la ligne l'y ajoute. */
.ing-read-new {
  flex: 0 0 auto;
  width: 26px; height: 26px;
  display: grid; place-items: center;
  border-radius: 999px;
  border: 1.5px dashed var(--accent-pale);
  background: var(--accent-voile);
  color: var(--accent-fonce); font-weight: 700; font-size: 13px;
}
.ing-read button:active { background: var(--accent-voile); }
/* ===== LA PRÉPARATION, EN LECTURE =====

   Un pavé en `pre-wrap` où l'on cherchait des yeux, entre deux tours de
   casserole, où l'on en était. Des étapes numérotées, assez espacées
   pour qu'on retrouve sa ligne après avoir levé la tête, et un numéro
   posé DANS LA MARGE : le texte reste aligné sur une seule colonne, ce
   qui est la moitié du travail quand on lit debout. */
.prep-steps {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: prep;
}
.prep-steps li {
  counter-increment: prep;
  position: relative;
  padding: 0 0 0 34px;
  margin-bottom: 14px;
  line-height: 1.6;
}
.prep-steps li:last-child { margin-bottom: 0; }
.prep-steps li::before {
  content: counter(prep);
  position: absolute;
  left: 0; top: 1px;
  width: 24px; height: 24px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--papier-2);
  color: var(--accent-fonce);
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.prep-one { margin: 0; line-height: 1.6; }

/* LA NOTE D'UNE RECETTE, EN LECTURE.

   Même interligne que la préparation — c'est du texte suivi, on le lit
   de la même manière — mais dans l'encre douce : ce n'est pas la recette,
   c'est ce qu'on en dit. */
.recipe-note { margin: 0; line-height: 1.6; color: var(--ink-soft); }

/* UN INGRÉDIENT NOMMÉ DANS LE TEXTE.

   Souligné, pas coloré : la couleur, dans cette app, dit un jugement —
   la note d'un plat, l'état d'une synchro. Ici il ne s'agit que de dire
   « celui-là, tu peux appuyer dessus ». Le trait pointillé est ce que
   tout le monde lit comme « il y a quelque chose de plus à savoir », et
   il ne coupe pas la phrase en deux. */
.ing-ref {
  display: inline;
  padding: 0;
  font: inherit;
  color: inherit;
  border-bottom: 2px dotted var(--accent-pale);
}
.ing-ref:active { background: var(--papier-2); border-radius: 4px; }
.pop-thumb { width: 24px; height: 24px; border-radius: var(--r-1); object-fit: cover; }

.ing-ico { width: 40px; height: 40px; padding: 0; border-radius: var(--r-2); overflow: hidden; }
.ing-ico img { width: 40px; height: 40px; object-fit: cover; }
.ing-main input {
  width: 100%;
  min-height: var(--ing-row, 42px);
  border: 1.5px solid var(--line);
  border-radius: var(--r-2);
  padding: 0 8px;
}

/* ===== UNE SEULE HAUTEUR PAR RANGÉE DE QUANTITÉ =====

   Le « − », le nombre et le « + » se donnaient leur hauteur chacun de leur
   côté, et le sélecteur d'unité posé au bout en avait une troisième. Sur
   une ligne d'ingrédient, ça donnait 37, 41 et 39 pixels côte à côte : les
   deux boutons paraissaient rabougris à côté du champ, et rien n'alignait
   la rangée.

   La cause était invisible en lisant les règles du compteur : le champ
   hérite du `min-height: 42px` de `.ing-main input` juste au-dessus, qui a
   la même force que la sienne et vient après. Plutôt que d'ajouter un
   `!important` ou une classe de plus, la rangée POSE sa hauteur, une fois,
   et ses trois pièces la prennent. C'est la même idée que `--search-h`
   pour la barre de recherche : une mesure écrite à un seul endroit ne peut
   plus diverger.

   Deux rangées, deux mesures : celle d'un ingrédient est serrée (on en
   empile dix), celle d'une fiche d'article suit la hauteur des champs
   voisins. */
.ing-qty { --qty-h: 42px; }
.qty-row { --qty-h: 52px; }
.ing-qty .stepper button, .ing-qty .step-val, .ing-qty .picker,
.qty-row .stepper button, .qty-row .step-val, .qty-row .picker {
  height: var(--qty-h);
  min-height: var(--qty-h);
}
/* Les coins suivent la taille : un carré de 42 px trop arrondi devient
   une pastille. */
.ing-qty .stepper button, .ing-qty .step-val { border-radius: var(--r-2); }
.week-nav { display: flex; gap: 8px; padding: 0 2px 8px; }

/* ---------- semainier : préparer, puis suivre ---------- */

/* Le bouton qui clôt la préparation vit en bas de la liste, là où l'on
   arrive quand on a fini de remplir la semaine. */
.week-footer { padding: 4px 2px 8px; }
.week-footer .btn-primary {
  display: flex; align-items: center; justify-content: center; gap: 8px;
}
.week-footer .btn-primary .ico { width: 20px; height: 20px; flex: 0 0 auto; }
.week-footer .btn[disabled] { opacity: .5; }

/* Le jour même, en suivi : c'est la seule chose qu'on vient vérifier en
   ouvrant l'app un soir, il doit se voir d'un coup d'œil. */
/* Aujourd'hui : une bande d'encre au bord haut, comme l'onglet actif —
   « vous êtes ici » se dit de la même façon partout. */
.day-card.today { box-shadow: inset 0 5px 0 var(--ink); padding-top: 17px; }

/* ---------- déplacer un plat d'un jour à l'autre ---------- */

/* La poignée dit que la ligne se saisit. Discrète : c'est un geste qu'on
   découvre en essayant, pas une commande de plus à lire. Son habillage
   est plus bas, avec sa cible de 44 px. */
.slot-filled[data-drag-slot] {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  touch-action: pan-y;         /* la page défile tant que rien n'est soulevé */
  -webkit-user-select: none;
  user-select: none;
  cursor: grab;                /* à la souris, tout le bloc se saisit */
}
.slot-filled[data-drag-slot]:active { cursor: grabbing; }
.slot-filled.lifted { opacity: .35; }

/* Pendant le déplacement, le doigt ne doit rien sélectionner ni faire
   défiler : c'est le plat qui suit. */
body.dragging-plan { touch-action: none; -webkit-user-select: none; user-select: none; }

.plan-ghost {
  position: fixed;
  z-index: 60;
  pointer-events: none;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: 6px;
  padding: 8px 10px;
  border-radius: var(--r-3);
  background: var(--surface);
  box-shadow: 0 12px 28px rgba(43, 38, 34, .28);
  transform: scale(1.03);
}
.plan-ghost .slot-actions-row,
.plan-ghost .side-row { display: none; }
/* Soulevé d'un appui long, en préparation : le fantôme ne porte que le
   plat, sans poignée à côté. */
.plan-ghost.held { grid-template-columns: minmax(0, 1fr); }

/* En préparation, le plat se soulève d'un appui long (voir dragplan.js) :
   rien ne doit se sélectionner ni ouvrir la loupe d'iOS pendant le délai. */
.slot-filled[data-hold-slot] .slot-rec {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* LA CARTE DU JOUR NE S'ALLUME PLUS : seul le créneau visé se signale.
   Survoler la carte à côté des créneaux allume celui où le plat tombera
   (voir `cibleSous`). On dépose SUR le midi, pas « quelque part dans
   mercredi » — et un jour à un seul créneau montrait deux indicateurs
   pour le même endroit. */
.slot[data-drop-slot].over {
  border-radius: var(--r-2);
  background: var(--bien-voile);
  box-shadow: 0 0 0 3px rgba(111, 162, 134, .28);
}
/* Le créneau vide d'un jour déjà servi : invisible en temps normal — un
   midi sans plat prévu n'est pas une information — et montré le temps
   d'un déplacement, parce qu'un endroit qu'on ne voit pas ne se vise
   pas. */
.slot-drop-only { display: none; }
body.dragging-plan .slot-drop-only { display: block; }
body.dragging-plan .day-empty { display: none; }
.slot-drop-hint {
  margin: 0;
  min-height: 44px;
  display: grid;
  place-items: center;
  border: 1.5px dashed var(--bien-filet);
  border-radius: var(--r-2);
  color: var(--ink-soft);
  font-weight: 700;
  font-size: 13px;
}
.day-name { display: flex; justify-content: space-between; align-items: center; gap: 8px; }

.regen {
  width: 48px; height: 48px; border-radius: var(--r-2);
  font-size: 24px; font-weight: 700;
}
/* Le créneau mis de côté.

   Une bande hachurée sur toute la largeur, exactement de la hauteur du
   bouton « ＋ Ajouter un plat » qu'elle remplace : la place du repas est
   gardée, elle est simplement barrée. Les hachures disent « rien ici »
   sans le mot, et l'étiquette dit ce que l'appui fera. */
.slot-closed {
  display: block;
  width: 100%;
  min-height: 44px;
  border-radius: var(--r-2);
  border: 1.5px dashed var(--line-fort);
  color: var(--ink-soft);
  font-weight: 700;
  font-size: 13px;
  background-color: var(--papier-2);
  background-image: repeating-linear-gradient(
    45deg,
    rgba(43, 38, 34, .055) 0 7px,
    transparent 7px 14px
  );
}
.slot-closed:active { background-color: var(--papier-2); }
.slot-closed-lbl { opacity: .85; }
/* La même bande, à l'échelle d'une JOURNÉE entière : elle remplace les
   deux créneaux, elle prend donc leur hauteur à tous les deux. C'est ce
   qui empêche une carte de jour ignoré de se ratatiner à côté de ses
   voisines, et ce qui fait qu'elle se lit de loin comme « ce jour-là,
   rien ». Elle est centrée : rien ne la précède ni ne la suit dans la
   carte, contrairement au créneau qui a sa ligne « Midi / Soir ». */
.slot-closed.as-day {
  display: grid;
  place-items: center;
  min-height: 96px;
  margin: 2px 0 4px;
  flex: 1;
}
/* Sur grand écran, les cartes de jour ont une hauteur plancher pour que
   la grille reste régulière : la bande prend alors toute la place qui
   reste sous le nom du jour, plutôt que de laisser un fond blanc en
   dessous d'elle — ce blanc se lisait comme un créneau vide de plus. */
.day-card.skipped { display: flex; flex-direction: column; }
/* La carte ne se grise pas en bloc : c'est la bande qui dit l'état, et le
   nom du jour doit rester lisible — on le cherche, justement, pour savoir
   lequel on a mis de côté. */

/* ============================================================
   Compléments : dialogues, connexion, rail d'écran large
   ============================================================ */

/* --- confirmation (remplace confirm() natif, bloqué dans les PWA iOS) --- */
.confirm-overlay { align-items: center; padding: 20px; z-index: 30; background: rgba(var(--voile), var(--voile-force)); }
.confirm-box {
  background: var(--feuille);
  border-radius: var(--r-4);
  padding: 22px 20px 16px;
  width: min(420px, 100%);
  box-shadow: var(--shadow);
}
.confirm-box h3 { font-family: var(--f-titre); font-stretch: 88%; font-size: 22px; margin: 0 0 8px; }
.confirm-box p { margin: 0 0 16px; color: var(--ink-soft); line-height: 1.45; }
.confirm-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }

/* --- connexion --- */
.auth-error {
  background: var(--danger-pale);
  color: var(--danger);
  border-radius: var(--r-2);
  padding: 10px 12px;
  margin: 0 0 12px;
  font-weight: 600;
  line-height: 1.4;
}
.auth-links { display: flex; justify-content: space-between; gap: 12px; margin-top: 12px; }
.auth .btn-ghost { margin-top: 12px; }

/* CE QU'ON EST VENU FAIRE — invitation, offre choisie, accès offert —
   écrit au-dessus du formulaire, dans l'encart d'une action. */
.auth-intention {
  margin: 0 0 var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-2);
  background: var(--accent-voile);
  border: 1px solid var(--accent-pale);
  color: var(--accent-fonce);
  font-weight: 600;
  line-height: 1.4;
}
.auth-site {
  display: block;
  margin: var(--sp-4) auto 0;
  text-align: center;
  color: var(--accent-fonce);
  font-weight: 700;
  font-size: 14px;
}
.auth .auth-links a.linkish { font-size: 12px; }

/* LES LIMITES DU MODE GRATUIT : un encart jaune d'étiquette, pas un
   rouge d'erreur — c'est une information qu'on doit lire, pas un
   problème. */
.gratuit-limites {
  margin: var(--sp-2) 0 var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-2);
  background: var(--moyen-voile);
  border: 1px solid var(--moyen-filet);
  font-size: 14px;
  line-height: 1.45;
}
.gratuit-limites strong { display: block; margin-bottom: var(--sp-1); }
.gratuit-limites ul { margin: 0; padding-left: 1.1em; color: var(--ink-soft); }
.gratuit-limites li + li { margin-top: 2px; }

/* Les offres, dans l'app : un encart par formule. L'offre payante porte
   son prix en grand et un seul bouton plein (l'année) ; le mois n'est
   qu'un lien dessous (voir `offresCartes` dans views.js). */
.offres-app { display: flex; flex-direction: column; gap: var(--sp-3); margin-bottom: var(--sp-3); }
/* La pastille déborde au-dessus de l'encart : l'écart laisse sa place. */
.offres-duo { display: grid; gap: var(--sp-5); margin-top: var(--sp-4); }
.offre-encart {
  position: relative;
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-5) var(--sp-4) var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  background: var(--surface);
  text-align: left;
}
.offre-encart.choisie { border: 2px solid var(--accent); }
.offre-pastille {
  position: absolute; top: 0; left: var(--sp-4); transform: translateY(-50%);
  margin: 0; padding: 2px 10px;
  border-radius: var(--r-1);
  background: var(--etiquette); color: #2b2622;
  font-size: 12px; font-weight: 700;
}
.offre-pastille.discrete { background: var(--papier-2); color: var(--ink-soft); }
.offre-encart-tete h4 { margin: 0; font-family: var(--f-titre); font-stretch: 88%; font-size: 24px; line-height: 1.1; }
.offre-sous { margin: 2px 0 0; color: var(--ink-soft); font-size: 14px; }
.offre-prix p { margin: 0; }
.offre-prix-grand { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 var(--sp-2); }
.offre-prix-grand b { font-family: var(--f-titre); font-stretch: 88%; font-size: 40px; line-height: 1; font-variant-numeric: tabular-nums; }
.offre-prix-grand span { color: var(--ink-soft); font-weight: 600; }
.offre-prix-suite { color: var(--ink-soft); font-size: 14px; margin-top: var(--sp-1) !important; }
.offre-plus { margin: 0; padding-left: 1.1em; color: var(--ink-soft); font-size: 14px; line-height: 1.5; }
.offre-plus b { color: var(--ink); }
.offre-encart .gratuit-limites { margin: 0; }
.offre-actions { display: grid; gap: var(--sp-2); margin-top: auto; }
.offre-actions .btn { margin: 0; }
.offre-lien-mois {
  justify-self: center;
  padding: var(--sp-1) var(--sp-2);
  color: var(--ink-soft); font-size: 13px; font-weight: 600;
  text-decoration: underline; text-underline-offset: 3px;
  text-align: center;
}
.offre-lien-mois:hover { color: var(--ink); }
.offre-lien-mois:disabled { opacity: .5; }

/* Sur ordinateur, la feuille s'élargit pour poser les deux formules
   côte à côte : à 620 px, deux encarts se lisaient en colonnes de
   quatre mots. */
@media (min-width: 900px) {
  .sheet:has(.offres-duo) { width: min(880px, 100%); }
  .offres-duo { grid-template-columns: 1fr 1.15fr; align-items: stretch; }
  .offre-encart.choisie .offre-prix-grand b { font-size: 48px; }
}

/* ---------- ce qui est réservé aux abonnés (ui/premium.js) ----------

   Grisé, mais pas éteint : le bouton reste appuyable, c'est l'appui qui
   ouvre la bulle. Le cadenas dit pourquoi c'est gris sans qu'il faille
   essayer. */
[data-premium] { position: relative; opacity: .55; filter: grayscale(.9); cursor: help; }
[data-premium]::after {
  content: "";
  position: absolute;
  top: 6px; right: 6px;
  width: 14px; height: 14px;
  background: var(--ink-soft);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 10V7a5 5 0 0 1 10 0v3h1a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h1zm2 0h6V7a3 3 0 0 0-6 0v3z'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 10V7a5 5 0 0 1 10 0v3h1a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h1zm2 0h6V7a3 3 0 0 0-6 0v3z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.assist-way[data-premium] { padding-right: 24px; }
.gen-verrou { position: relative; }
.gen-verrou .chip { pointer-events: none; }
button.prod.ai-verrou { width: 100%; text-align: left; font: inherit; color: inherit; }

.premium-bulle {
  position: fixed;
  z-index: 1000;
  width: min(320px, calc(100vw - 24px));
  padding: var(--sp-4);
  border-radius: var(--r-3);
  background: var(--champ);
  color: var(--ink);
  border: 1px solid var(--line-fort);
  box-shadow: 0 16px 40px rgba(var(--voile), .22);
}
.premium-bulle::before {
  content: "";
  position: absolute;
  top: -7px;
  left: calc(var(--pointe, 50%) - 7px);
  width: 12px; height: 12px;
  background: inherit;
  border-left: 1px solid var(--line-fort);
  border-top: 1px solid var(--line-fort);
  transform: rotate(45deg);
}
.premium-bulle.dessus::before { top: auto; bottom: -7px; transform: rotate(225deg); }
.premium-bulle p { margin: 0 0 var(--sp-3); line-height: 1.45; }
.premium-actions { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2); }
.premium-actions .btn { margin: 0; }

/* ---------- gestion abonnement (ui/abonnement.js) ---------- */
.abo-nom { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2); align-items: end; }
.abo-nom .field { margin: 0; }
.abo-nom .btn { margin: 0; width: auto; }
.abo-places { font-family: var(--f-texte); font-size: 13px; font-weight: 600; color: var(--ink-soft); margin-left: var(--sp-2); }
.abo-membres { display: flex; flex-direction: column; gap: var(--sp-2); margin-bottom: var(--sp-3); }
.abo-membre {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-2);
  background: var(--surface);
}
.abo-membre.invite { border-style: dashed; border-color: var(--accent-pale); background: var(--accent-voile); }
.abo-membre-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.abo-membre-txt small { color: var(--ink-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.abo-membre .btn { margin: 0; width: auto; min-height: 36px; padding: 0 var(--sp-3); font-size: 14px; }
.abo-lien { color: var(--sage-deep); }
/* SA PROPRE LIGNE SE RECONNAÎT. L'adresse du compte connecté est ce
   qu'on vient vérifier quand on se demande « suis-je sur le bon
   compte ? » : elle passe en texte plein sous le prénom, au lieu de se
   perdre en gris derrière le rôle. */
.abo-membre.moi { background: var(--accent-voile); border-color: var(--accent-pale); }
.abo-vous { font-weight: 600; color: var(--ink-soft); }
.abo-email { font-weight: 700; color: var(--accent-fonce); overflow-wrap: anywhere; }
.abo-actions { display: grid; gap: var(--sp-3); margin-top: var(--sp-3); }
.abo-actions .btn { margin: 0; }
.sync-state { margin: 0 0 10px; font-weight: 600; color: var(--ink-soft); }
.sync-state.ok { color: var(--sage-deep); }

/* --- détails de liste --- */
/* Sous le nom, et sur deux lignes au plus : une note est une remarque,
   pas un paragraphe, et une ligne de liste doit rester une ligne de
   liste. */
.item-note {
  min-width: 0;
  font-size: 13px;
  color: var(--ink-soft);
  font-weight: 500;
  line-height: 1.3;
  overflow-wrap: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.voice-step b { min-width: 46px; }

/* --- recherche : croix d'effacement --- */
.search-box { position: relative; }
.search-box input { padding-right: 44px; }
.search-clear {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: 34px; height: 34px; border-radius: var(--r-2);
  color: var(--ink-soft); font-weight: 700; font-size: 15px;
}

/* --- feuilles --- */
/* L'assombrissement suit le doigt pendant le glissé (--dim vient de shell.js). */
.overlay.in { background: rgba(var(--voile), calc(var(--voile-force) * var(--dim, 1))); }
/* Une feuille recouverte par une autre : elle reste visible derrière,
   inerte, et laisse celle du dessus assombrir la page pour deux. */
.overlay.under { pointer-events: none; background: transparent; }
.handle {
  width: 52px; height: 6px;
  position: relative;
  transition: background .15s ease, width .15s ease;
}
/* Zone tactile confortable autour d'un trait volontairement fin. */
.handle::after { content: ""; position: absolute; inset: -14px -40px; }
.handle:hover, .handle:focus-visible { background: var(--accent); width: 64px; outline: none; }
.sheet-body { overscroll-behavior: contain; }
body.sheet-open { overflow: hidden; }

/* La croix d'une feuille qui tient lieu d'écran : posée sur la feuille,
   pas dans son contenu, donc toujours à portée quand on descend. */
.sheet { position: relative; }
.sheet-close {
  position: absolute;
  top: 10px; right: 12px;
  z-index: 3;
  width: 38px; height: 38px;
  border-radius: var(--r-2);
  background: var(--papier);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 15px; font-weight: 700;
  display: grid; place-items: center;
}
.sheet-close:active { background: var(--papier); }
/* ELLE PASSE AU-DESSUS DE TOUT.

   L'entête collant du garde-manger (onglets + recherche) monte jusqu'au
   bord de la zone défilante et porte un `z-index` de 6 : la croix, à 3,
   se retrouvait DESSOUS. On voyait l'onglet « Articles » recouvrir le
   seul bouton qui referme la feuille, et l'appui tombait sur l'onglet. */
.sheet-close { z-index: 9; }
/* Et l'entête rentre son bord droit pendant qu'on défile, pour que la
   croix ne se pose sur rien. Au repos elle est deux rangées plus haut :
   une encoche permanente à droite du champ de recherche ne se serait pas
   expliquée. La classe est posée par shell.js, au défilement. */
.sheet.scrolled .catalog-top,
.sheet.scrolled .pantry-pick > .catalog-top { padding-right: 46px; }
.sheet .catalog-top { transition: padding-right .18s ease; }
@media (prefers-reduced-motion: reduce) {
  .sheet .catalog-top { transition: none; }
}
.sheet-page [data-sheet-page] > h3:first-child { padding-right: 46px; }
/* Le garde-manger ouvert en feuille garde SON fond.

   La feuille est en `--paper`, presque blanc : c'est ce qu'il faut à un
   formulaire, dont les champs sont blancs et doivent s'en détacher. Mais
   ici la feuille ne contient pas un formulaire, elle contient un écran de
   l'app — les mêmes lignes, les mêmes cases blanches sur le crème de la
   maison. Sur du presque-blanc, les lignes disparaissaient : on avait le
   garde-manger, décoloré.

   On le reconnaît à ses onglets, qui n'existent que là. */
.sheet-page:has(.pane-tabs),
.sheet-page:has(.catalog-top) { background: var(--feuille-creux); }
/* Dans une feuille, la liste n'a pas de barre d'onglets à éviter. */
.sheet .catalog-list { margin: 0 0 10px; }

.sheet .voice-wrap { padding-left: 0; padding-right: 0; padding-bottom: 8px; }
.sheet .search-row { margin-bottom: 10px; }
.sheet .catalog-top { padding-bottom: 10px; }
/* Dans une feuille, `.sheet input` habille tous les champs — bordure,
   coins, et surtout 14 px de rembourrage haut et bas. Appliqué au champ
   de recherche, il le poussait à 53 px pendant que l'entonnoir d'à côté
   en faisait 48 : cinq pixels d'écart, et l'un des deux semblait flotter.
   Le cadre, ici, c'est `.search-box` ; l'input qu'il contient n'est qu'un
   texte. */
.sheet .search-box input {
  padding: 0 44px 0 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}
.sheet .hero { margin-left: 0; margin-right: 0; }
/* Dans la feuille, l'entête reste collé en haut du contenu qui défile :
   les deux onglets et la recherche ne partent pas avec la liste. */
.sheet .catalog-top { padding: 6px 0 8px; }
.sheet .search-row.solo { max-width: none; }

/* Picker garde-manger / ingrédients : 8 dp au-dessus du titre et des
   onglets, 8 dp au-dessus et au-dessous du champ — pas davantage. */
.sheet:has(.pantry-pick) .handle { margin-bottom: 8px; }
.pantry-pick > h3 { margin: 0; }
.pantry-pick > .sheet-meta { margin: 4px 0 0; }
.pantry-pick .catalog-top { padding: 0; }
.pantry-pick .pane-tabs { margin-top: 8px; }
.sheet .pantry-pick .search-row,
.sheet .pantry-pick .search-row.solo {
  margin-top: 8px;
  margin-bottom: 8px;
}

.sheet .prod { margin-left: 0; margin-right: 0; }

/* --- rail d'onglets : visible seulement sur grand écran --- */
.rail-brand, .rail-foot, .rail-only { display: none; }

/* ============================================================
   Tablette et au-delà (≥ 720 px) : les listes passent sur deux colonnes
   ============================================================ */

/* ENTRE 720 ET 899 PX, UNE COLONNE DE TÉLÉPHONE AU MILIEU D'UN ÉCRAN VIDE.

   L'app y gardait sa mise en page de téléphone, bornée à 560 px et
   centrée : sur une tablette de 820 px en portrait, un tiers de l'écran
   partait en marges, et la liste de courses tenait sur une seule colonne
   qu'il fallait faire défiler. La colonne s'élargit à 760 px, et tout ce
   qui est une LISTE prend deux colonnes — les mêmes règles qu'au-delà de
   900 px, où elles vivaient seules. La navigation, elle, reste en bas :
   le rail latéral ne commence qu'à 900 px (`isPhone()`). */
@media (min-width: 720px) {
  /* Deux colonnes pour la liste. Les colonnes CSS, et non une grille :
     un rayon garde ses articles sous son intertitre et se déplace d'un
     bloc, au lieu de laisser un trou quand il ne contient qu'une ligne. */
  .list { columns: 2; column-gap: var(--sp-4); }
  .cat-group { break-inside: avoid; page-break-inside: avoid; }
  .empty, .list-foot { column-span: all; }
  .list-foot { display: flex; gap: 10px; }
  .list-foot .btn { flex: 1; }

  .catalog-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 0; align-content: start; }
  /* Un intertitre coupe la liste : il tient donc toute la largeur. Rangé
     dans une colonne, il se retrouvait à côté des lignes qu'il annonce au
     lieu d'être au-dessus, et l'ordre de lecture n'avait plus de sens. */
  .catalog-list .empty, .catalog-list .filter-block {
    grid-column: 1 / -1;
  }
  /* Un écran vide EST la liste : `<div class="empty catalog-list">` porte
     les deux classes à la fois. La grille s'appliquait donc à son propre
     contenu, et « Aucun favori » se retrouvait dans une colonne de 320 px
     avec sa phrase d'explication à côté, sur la même ligne, au lieu d'être
     dessous. Rien à ranger en colonnes quand il n'y a rien. */
  .catalog-list.empty { display: block; }

  /* Le semainier : deux cartes de jour par rangée plutôt qu'une. */
  .week-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: var(--sp-3);
    align-items: start;
  }
  .week-grid { margin-bottom: 10px; }
  .week-grid .day-card { margin-bottom: 0; }
}
@media (min-width: 720px) and (max-width: 899px) {
  .screen.main { max-width: 760px; }
  .tabbar { width: min(760px, 100%); }
}

/* ============================================================
   Écran large : le même contenu, rangé autrement
   ============================================================ */

@media (min-width: 900px) {
  html, body { font-size: 16px; }

  .auth { max-width: 460px; margin: 0 auto; justify-content: center; }

  /* La barre du bas devient une colonne à gauche. */
  .tabbar {
    left: 0; right: auto; top: 0; bottom: 0;
    width: 248px;
    transform: none;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 18px 14px calc(18px + var(--safe-b));
    border-top: 0;
    border-right: 1px solid var(--line);
    background: var(--nuit);
    color: #fff;
  }
  /* LE RAIL EST À L'ENCRE : il encadre l'app au lieu d'en être un
     panneau de plus, et le contenu reste seul sur le papier. */
  .rail-brand {
    display: flex; align-items: center; gap: 10px;
    padding: 4px 10px 18px;
    font-family: var(--f-titre); font-stretch: 88%;
    font-size: 24px; font-weight: 800;
    letter-spacing: -0.02em;
  }
  .rail-brand img { width: 34px; height: 34px; border-radius: var(--r-2); }
  .rail-foot { display: flex; flex-direction: column; gap: 4px; margin-top: auto; }
  .rail-only { display: flex; }

  .tab {
    flex: 0 0 auto;
    flex-direction: row;
    justify-content: flex-start;
    text-align: left;
    gap: 12px;
    min-height: 50px;
    padding: 0 14px;
    border: 0;
    border-radius: var(--r-2);
    background: none;
    color: #c3c9d2;
    font-size: 15px;
  }
  .tab b { font-size: 20px; }
  .tab.on { background: rgba(255, 255, 255, .08); color: #fff; }
  .tab.on::before {
    top: 10px; bottom: 10px; left: 0; right: auto;
    width: 4px; height: auto;
    border-radius: 0 3px 3px 0;
    background: var(--etiquette);
  }
  .tab:hover:not(.on) { background: rgba(255, 255, 255, .05); color: #fff; }
  .rail-foot .tab { font-size: 14px; color: #c3c9d2; }

  /* Le contenu occupe le reste, avec une largeur de lecture confortable.
     La largeur est portée par le conteneur, pas par des marges « auto »
     sur chaque bloc : dans une colonne flex, une marge auto fait
     rétrécir l'enfant au lieu de le centrer. */
  .screen.main {
    max-width: calc(248px + 1180px);
    margin: 0 0 0 248px;
    border: 0;
    padding: 0 clamp(16px, 4vw, 50px);
  }
  .top, .catalog-top { padding-left: 0; padding-right: 0; padding-top: 26px; }
  .list, .week-days, .voice-wrap { padding-left: 0; padding-right: 0; }
  .filters, .catalog-list { margin-left: 0; margin-right: 0; }
  .list, .week-days { padding-bottom: 48px; }
  .catalog-list { margin-bottom: 48px; }

  /* Ces deux boutons vivent déjà dans le rail. */
  #btn-shop, #btn-settings { display: none; }
  .brand { font-size: 34px; }

  .prod { margin: 0; }
  .list { column-gap: 18px; }
  /* Le semainier sur grand écran : le bilan à gauche, les jours à droite.

     Le bilan tenait toute la largeur — jusqu'à 1180 px — pour quatre
     lignes de chiffres et une note : un mètre de vide à droite, et le
     lundi repoussé sous la ligne de flottaison. Posé en colonne, il
     occupe une place à sa mesure, reste sous les yeux pendant qu'on
     parcourt la semaine, et rend aux jours toute la largeur restante.

     Sans bilan (une semaine en préparation), pas de colonne : les jours
     prennent tout. */
  .week-layout { display: block; }
  .week-layout.with-sum {
    display: grid;
    grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
    gap: 0 24px;
    align-items: start;
  }
  /* La colonne de gauche entière colle en haut : le bilan ET le bouton
     qui le suit. « Ajouter les ingrédients à la liste » referme la
     semaine — sur téléphone il est après dimanche, là où l'on arrive en
     ayant tout lu ; ici il n'y a rien à lire en descendant, et il se
     retrouvait tout en bas de la colonne des jours, à côté d'un mètre de
     vide sous le bilan. */
  .week-layout.with-sum .week-aside {
    position: sticky;
    top: 22px;
  }
  .week-layout.with-sum .week-sum { margin: 2px 0 0; }
  .week-aside .week-footer { padding: 12px 0 0; }
  .week-aside .week-footer .btn { max-width: none; width: 100%; }

  /* Des cartes larges, qui reviennent à la ligne. Comprimer sept colonnes
     dans la largeur de l'écran donnait des noms de plats en trois
     lettres : on garde la taille des cartes et on accepte deux ou trois
     rangées, plutôt qu'un défilement horizontal où la fin de semaine se
     cache. */
  .week-days {
    display: flex;
    flex-direction: column;
    gap: 14px;
    padding: 2px 0 18px;
  }
  .week-days > .days-past { margin-bottom: 0; }

  /* LA GRILLE SE RÈGLE SUR CE QUI RESTE DE LA SEMAINE.

     `auto-fill` gardait les sept colonnes possibles même quand il ne
     restait qu'un jour : un dimanche, la carte d'aujourd'hui tenait
     260 px en haut à gauche, et le reste de l'écran était vide. Avec
     `auto-fit`, les colonnes sans carte disparaissent et les cartes
     présentes se partagent la largeur. Une carte large pose alors ses
     deux créneaux côte à côte (`.day-card` plus bas), et un créneau
     large remet ses boutons à droite du plat (`@container slot`). */
  .week-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 14px;
    align-items: start;
    margin-bottom: 0;
  }

  /* Ni le bouton ni le bloc des jours passés ne sont des jours : ils ne
     prennent pas une case de la grille, ils la traversent.

     Le bouton occupait une case à lui seul, à la suite de dimanche,
     comme s'il en était un huitième. Il tient toute la largeur, et il y
     est CENTRÉ : c'est lui qui referme la semaine, et une pastille de
     380 px calée contre le bord gauche sous sept colonnes de cartes se
     lit comme un reste de la dernière rangée.

     Le bloc replié des jours passés, lui, tient une ligne au-dessus de
     tout : c'est un en-tête de semaine, pas une carte parmi les autres. */
  .week-footer { padding: 4px 0 0; display: flex; justify-content: center; }
  .week-footer .btn { max-width: 380px; width: 100%; }
  /* LES CARTES NE RÉSERVENT PLUS DE HAUTEUR À VIDE.

     Un plancher de 230 px les alignait entre elles, au prix d'un fond
     blanc de cent pixels sous un jour qui ne porte qu'un plat — et de
     quatre cartes vides, hautes comme les autres, sur une semaine qu'on
     n'a pas encore remplie. Chacune tient donc à sa hauteur.

     La RANGÉE, elle, garde la hauteur de la plus haute : c'est
     `align-items: start` sur la grille, déjà posé — les cartes plus
     courtes ne s'étirent pas, et l'espace qui reste sous elles est le
     fond de la page, pas du blanc en trop. */
  .day-card {
    margin-bottom: 0;
    padding: 14px;
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    column-gap: 24px;
  }
  .day-card > * { flex: 1 0 100%; min-width: 0; }
  /* Midi et soir passent côte à côte dès que la carte leur laisse 260 px
     chacun — c'est-à-dire quand il reste peu de jours à afficher. */
  .day-card > .slot { flex: 1 1 260px; container: slot / inline-size; }
  .day-name { font-size: 16px; margin-bottom: 12px; }
  .slot { margin-bottom: 14px; }
  .slot-rec img, .slot-rec .thumb-wrap img { width: 46px; height: 46px; }

  /* Les actions passent sous le plat, ET sous ses accompagnements.

     Dans une carte, le nom de la recette mérite toute la largeur — d'où
     l'empilement. Mais « régénérer » et « vider » agissent sur le CRÉNEAU
     entier, accompagnements compris : les laisser entre le plat et son
     riz les faisait lire comme les boutons du plat seul, et on hésitait à
     appuyer. En bas de la pile, ils reprennent la portée qu'ils ont.

     `order` plutôt qu'un changement de gabarit : sur téléphone la
     disposition est autre (les actions à droite du plat), et le HTML doit
     rester le même pour les deux. */
  .slot-filled { display: block; }
  .slot-filled:not([data-drag-slot]) {
    display: flex;
    flex-direction: column;
    align-items: stretch;
  }
  .slot-filled .side-row { order: 1; }
  .slot-filled .slot-actions-row { order: 2; }
  /* Sauf s'il se saisit. Là, deux colonnes : le plat et ses
     accompagnements empilés à gauche, la poignée seule à droite, centrée
     sur toute la hauteur du bloc.

     En simple rangée, les accompagnements devenaient un troisième élément
     de la même ligne : le riz se retrouvait à côté de la poignée, et la
     poignée au milieu de nulle part. Or elle saisit le CRÉNEAU entier —
     le plat part avec son riz —, donc sa place est en face de
     l'ensemble, pas en face de la première ligne. */
  .slot-filled[data-drag-slot] {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 0 8px;
  }
  .slot-filled[data-drag-slot] .slot-rec,
  .slot-filled[data-drag-slot] .side-row { grid-column: 1; }
  .slot-filled[data-drag-slot] .slot-grip { grid-column: 2; grid-row: 1; }
  /* Deux lignes à enjamber seulement s'il y a quelque chose en dessous. */
  .slot-filled[data-drag-slot]:has(.side-row) .slot-grip { grid-row: 1 / span 2; }
  .slot-filled[data-drag-slot] .slot-rec { margin-bottom: 0; }
  .slot-rec { width: 100%; margin-bottom: 8px; }
  .slot-actions-row { display: flex; gap: 8px; }
  .slot-filled .regen, .slot-filled .mini {
    flex: 1; width: auto; height: 36px; min-height: 36px;
    border-radius: var(--r-2); font-size: 16px;
  }
  .slot-row { flex-wrap: wrap; gap: 8px; }
  .slot-row .slot-empty { flex: 1 0 100%; }
  .slot-row .regen, .slot-row .chip { flex: 1; }

  /* UN CRÉNEAU LARGE REPREND LA RANGÉE DU TÉLÉPHONE.

     L'empilement (plat, puis deux boutons pleine largeur) est fait pour
     une colonne de 260 px. Dans un créneau de 500 px, il donnait deux
     boutons de 240 px sous un nom de plat qui n'occupait que la moitié de
     sa ligne. Au-delà de 400 px, les boutons reviennent à droite du plat,
     à leur taille, et les accompagnements restent dessous. */
  @container slot (min-width: 400px) {
    .slot-filled:not([data-drag-slot]) {
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      align-items: center;
      column-gap: 12px;
    }
    .slot-filled:not([data-drag-slot]) .slot-rec { grid-column: 1; margin-bottom: 0; }
    .slot-filled:not([data-drag-slot]) .slot-actions-row { grid-column: 2; grid-row: 1; }
    .slot-filled:not([data-drag-slot]) .side-row { grid-column: 1 / -1; margin-top: 8px; }
    .slot-filled .regen, .slot-filled .mini { flex: 0 0 auto; width: 36px; }
    .slot-row { flex-wrap: nowrap; }
    .slot-row .slot-empty { flex: 1 1 auto; }
    .slot-row .regen, .slot-row .chip { flex: 0 0 auto; }
  }

  .hero { min-height: 96px; }

  .pane-tabs { justify-content: flex-start; }

  /* LES ONGLETS ET LA RECHERCHE SUR UNE SEULE RANGÉE.

     Les deux onglets du garde-manger s'étiraient sur toute la largeur du
     contenu — deux boutons de 580 px pour deux mots — et la recherche
     prenait une rangée de plus en dessous. L'entête collant mangeait
     ainsi 190 px au-dessus d'une liste qu'on vient parcourir. Sur grand
     écran, les onglets gardent une largeur de segment et la recherche
     prend le reste de la même ligne. Ce qui suit (filtres, fil d'Ariane,
     bande du rayon) reste sur toute la largeur. Dans une feuille, rien ne
     change : la place y est comptée autrement. */
  .screen.main > .catalog-top {
    display: grid;
    grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
    column-gap: var(--sp-3);
    align-items: center;
  }
  .screen.main > .catalog-top > * { grid-column: 1 / -1; }
  .screen.main > .catalog-top > .pane-tabs { grid-column: 1; margin-top: 10px; }
  .screen.main > .catalog-top > .search-row { grid-column: 2; margin-top: 10px; }
  /* La barre de recherche et l'encart Filtres ont la même largeur que
     les onglets au-dessus. Le champ prend ce qui reste : sur Recettes
     il pousse « Nouveau » jusqu'au bord droit de l'onglet Articles. */
  .search-row.solo { max-width: none; margin-left: 0; }
  .filter-block.loose { margin-left: 0; margin-right: 0; }

  /* Une feuille devient une boîte de dialogue centrée. */
  .overlay { align-items: center; padding: 24px; }
  .sheet {
    width: min(620px, 100%);
    border-radius: var(--r-4);
    height: auto;
    max-height: 84dvh;
    transform: translateY(24px) scale(.98);
    opacity: 0;
    transition: transform .24s cubic-bezier(.2,.8,.2,1), opacity .18s ease;
  }

  .overlay.in .sheet { transform: none; opacity: 1; }
  .sheet-body { padding-bottom: 18px; }
  .sheet-actions { grid-template-columns: repeat(2, minmax(0, 200px)); justify-content: end; }

  .toast { left: auto; right: 28px; bottom: 28px; transform: none; }

  /* Mode magasin : la colonne reste, l'entête s'efface comme sur mobile. */
  /* Le mode magasin n'existe pas sur ordinateur : c'est un écran de
     rayon, tenu à une main. La barre et l'entête restent donc en place. */
  body.shop .tabbar { display: flex; }
  body.shop .top { display: block; }
  body.shop .shop-bar { display: none; }
  body.shop .filters:not(.shop-filters) { display: flex; }
}

@media (min-width: 1400px) {
  .list { columns: 3; }
}

/* Souris : de quoi voir ce qui est cliquable. */
@media (hover: hover) and (pointer: fine) {
  .item:hover, .catalog-list > .prod:hover { background: var(--accent-voile); }
  .prod:hover { border-color: var(--accent-pale); }
  .chip:hover:not(.active) { background: var(--papier); }
  .icon-btn:hover:not(.on) { background: var(--papier); }
  .btn-primary:hover { background: var(--accent-appui); }
}

/* ============================================================
   Pictogrammes, hauteurs, propositions de saisie
   ============================================================ */

/* Les onglets portent des SVG en `currentColor` : ils prennent la couleur
   du libellé — encre douce au repos, encre pleine une fois actifs, clairs
   sur le rail d'ordinateur. Un emoji, lui, gardait ses couleurs quel que
   soit le fond. */
.tab b { display: grid; place-items: center; }
.ico { width: 24px; height: 24px; display: block; }
.tab.on .ico { stroke-width: 2; }
.rail-foot .ico { width: 22px; height: 22px; }

/* Le champ de recherche a déjà sa grande croix : on masque celle,
   minuscule, que le navigateur ajoute sur les champs `type=search`. */
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
input[type="search"] { appearance: none; }

/* L'entête du garde-manger ne doit pas sauter entre Recettes et Articles :
   le bouton « filtres » n'existe que d'un côté, on réserve sa place. */
.catalog-top .top-row { min-height: 46px; }

/* Le chemin et la bande sont maintenant DANS l'entête collante : ils ont
   leurs marges de bloc de page — pensées pour flotter au milieu du corps
   — et il faut les rendre à la rangée. L'entête porte déjà son propre
   rembourrage horizontal. */
.catalog-top .crumbs { margin: 2px 0 8px; padding: 0 2px; }
.catalog-top .hero { margin: 0 2px 30px; }

/* Les trois actions d'un créneau du semainier ont la même hauteur. */
.slot-empty { min-height: 48px; display: flex; align-items: center; }
/* « IGNORER » EST UNE ACTION, PAS UN CHOIX.

   Il était pastille ronde dans l'entête du jour et rectangle de 48 px
   sous le créneau : le même mot, le même geste, deux dessins. Les deux
   prennent la forme des boutons (arrondi de contrôle) ; seules les
   pastilles qu'on coche restent rondes. */
.slot-row .chip, .slot-filled .chip { min-height: 48px; }
.slot-row .chip, .slot-filled .chip, .day-name .chip,
.week-today { border-radius: var(--r-2); }
.slot-row .regen { min-height: 48px; }
/* Les retouches d'un plat déjà posé gardent leur taille de retouche —
   voir plus haut, la règle qui les aligne sur les ingrédients d'une
   fiche recette. */
.slot-filled .regen, .slot-filled .mini { min-height: 36px; }

/* --- propositions de saisie --- */
.ac {
  position: absolute;
  top: calc(100% + 6px);
  left: 0; right: 0;
  z-index: 14;
  margin: 0; padding: 6px;
  list-style: none;
  background: var(--champ);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  box-shadow: var(--shadow);
  max-height: 320px;
  overflow: auto;
  overscroll-behavior: contain;
}
.ac li { margin: 0; }
.ac-row {
  display: grid;
  grid-template-columns: 36px 1fr;
  gap: 10px;
  align-items: center;
  width: 100%;
  padding: 7px 8px;
  border-radius: var(--r-2);
  text-align: left;
}
.ac-row img, .ac-ico {
  width: 36px; height: 36px;
  border-radius: var(--r-2);
  object-fit: cover;
  background: var(--paper-img);
  display: grid; place-items: center;
  font-size: 16px;
}
/* Sur grand écran, une seule ligne : le nom à gauche, sa nature et son
   rayon en pastilles à droite. Empilé, chaque proposition coûterait le
   double de hauteur — et une liste de six n'entrerait plus sous le
   champ. */
.ac-txt {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 8px;
}
/* DEUX LIGNES PLUTÔT QU'UN NOM COUPÉ.

   « Crème fraîche épaisse entière » perdait sa moitié droite derrière
   trois petits points, et « Cham… » ne dit pas si c'est du champignon ou
   du champagne. Le nom a donc droit à deux lignes, et la césure
   automatique pose un vrai trait d'union quand un mot doit être coupé au
   milieu — c'est la règle française, et sans elle un mot rompu se lit
   comme deux mots.

   Deux lignes et pas plus : au-delà, une proposition prend la place de
   deux et la liste ne se parcourt plus d'un coup d'œil. */
.ac-txt strong {
  flex: 1;
  min-width: 0;
  font-size: 15px; font-weight: 700;
  line-height: 1.25;
  white-space: normal;
  overflow-wrap: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.ac-tags {
  flex: 0 1 auto;
  min-width: 0;
  display: flex; align-items: center; gap: 5px;
}
.ac-tags .kind-tag { margin-right: 0; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Le rayon est une indication, pas une nature : plus discret. */
.kind-tag.soft {
  background: transparent;
  border: 1px solid var(--line-fort);
  color: var(--ink-soft);
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
}
/* SUR UN ÉCRAN ÉTROIT, LE NOM ET SES PASTILLES S'EMPILENT.

   Côte à côte, il ne restait au nom qu'une cinquantaine de pixels : on
   lisait « Crème… » et il fallait choisir à l'aveugle. Le rayon
   disparaissait alors pour lui rendre de la place — et on perdait
   l'information qui départage justement deux articles au nom voisin.

   Empilés, les deux tiennent : le nom sur toute la largeur, ses deux
   lignes et sa césure ; les pastilles dessous, où le rayon a de nouveau
   de quoi s'écrire en entier. */
@media (max-width: 430px) {
  .ac-txt {
    display: block;
  }
  .ac-tags { margin-top: 3px; }
  .ac-tags .kind-tag { max-width: none; }
}
/* Le même gabarit, une intention différente : la ligne n'ouvre pas une
   fiche, elle ajoute. Un « + » à droite et un fond tiède le disent, sans
   toucher à la hauteur — six propositions doivent tenir sous le champ. */
.ac-row.adds {
  grid-template-columns: 36px 1fr 26px;
  background: var(--accent-voile);
}
/* Dans les feuilles « piocher », la liste de suggestions est blanche —
   le crème du garde-manger se lisait comme le fond de la page. */
.ac-pick {
  background: var(--champ);
}
.ac-pick .ac-row.adds,
.ac-pick .ac-row.on,
.ac-pick .ac-row:hover {
  background: var(--champ);
}
.ac-plus {
  display: grid; place-items: center;
  width: 26px; height: 26px;
  border-radius: var(--r-1);
  background: var(--accent);
  color: #fff;
  font-size: 17px; font-weight: 700;
  line-height: 1;
}
.ac-plus.go { background: transparent; color: var(--ink-soft); font-size: 19px; }
.ac-plus-check .ico { width: 17px; height: 17px; }
.ac-plus .ico { width: 17px; height: 17px; stroke-width: 2; }
.ac-row.on, .ac-row:hover { background: var(--papier); }
.ac-row.on { outline: 2px solid var(--accent-pale); outline-offset: -2px; }

/* ============================================================
   Feuille glissante : le comportement Android, sur téléphone
   ============================================================ */

@media (max-width: 899px) {
  /* La feuille fait toute la hauteur de l'écran ; c'est `translateY`,
     piloté par shell.js, qui décide de la part visible. Le contenu qui
     dépasse est simplement sous le bord de l'écran, jusqu'à ce qu'on
     tire la feuille vers le haut. */
  .sheet {
    height: 100dvh;
    max-height: none;
    transform: translateY(100%);
    will-change: transform;
  }

  /* Collée en haut : les coins s'aplatissent et la poignée descend sous
     la barre d'état, comme sur Android. */
  .sheet.at-top {
    border-radius: 0;
    padding-top: var(--safe-t);
  }
  /* Repliée, c'est shell.js qui possède le geste ; dépliée, on rend le
     défilement vertical au navigateur. Les deux sont aussi posés en
     ligne par shell.js : ceci n'est que l'état de départ. */
  .sheet-body { overflow-y: hidden; touch-action: none; }
  .sheet.at-top .sheet-body { overflow-y: auto; touch-action: pan-y; }

  /* Sans ça, tirer la feuille déclencherait aussi le « tirer pour
     rafraîchir » du navigateur. */
  .overlay { touch-action: none; }
  .sheet-body { overscroll-behavior: contain; }
}

/* ============================================================
   Note santé
   ============================================================ */

/* Une lettre, une couleur. Jamais seule : elle est toujours suivie du
   nom du plat, et son `title` donne le libellé en toutes lettres —
   la couleur ne porte donc aucune information à elle seule. */
.hgrade {
  display: inline-grid;
  place-items: center;
  width: 21px; height: 21px;
  margin-right: 7px;
  border-radius: var(--r-1);
  font-size: 12px;
  font-weight: 700;
  color: #fff;
  vertical-align: -3px;
}
.hgrade.big {
  width: 44px; height: 44px;
  border-radius: var(--r-2);
  font-size: 22px;
  margin: 0;
  vertical-align: 0;
}
/* Les trois tons de la note : sur la pastille des listes comme sur celle
   du tableau de détail. */
.t-good .hgrade, .hgrade.t-good, .hd-note.t-good { background: var(--note-bien); }
.t-mid .hgrade, .hgrade.t-mid, .hd-note.t-mid { background: var(--note-moyen); }
.t-soft .hgrade, .hgrade.t-soft, .hd-note.t-soft { background: var(--note-bof); }

.health-card {
  background: var(--papier);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  margin: 0 0 14px;
  overflow: hidden;
}
.health-card.t-good { background: var(--bien-voile); border-color: var(--bien-filet); }
.health-card.t-mid { background: var(--moyen-voile); border-color: var(--moyen-filet); }
.health-card.t-soft { background: var(--bof-voile); border-color: var(--bof-filet); }

/* Tout l'entête est le bouton : la cible fait toute la largeur. */
.health-head {
  display: grid;
  grid-template-columns: 44px 1fr 22px;
  gap: 12px;
  align-items: center;
  width: 100%;
  padding: 10px 12px;
  text-align: left;
}
.health-txt { min-width: 0; }
/* Tailles, graisses et couleurs : voir « le texte des cartes », en haut
   de cette feuille. Ici, seulement ce qui est propre à cette carte. */
.health-txt strong { display: block; }
.health-txt small { display: block; }
.health-why { display: block; margin-top: 6px; }
.health-chev {
  display: grid; place-items: center;
  color: var(--ink-soft);
  transition: transform .18s ease;
}
.health-chev .ico { width: 20px; height: 20px; }
.health-card.open .health-chev { transform: rotate(180deg); }
/* Le pied de la carte : ce qu'on peut FAIRE de cette note. Il est séparé
   du reste par un filet dans la teinte de la carte — c'est la même
   carte, pas un bouton posé dessous. */
.health-foot {
  padding: 10px 12px 12px;
  border-top: 1px solid rgba(43, 38, 34, .08);
}
/* Le bouton qui DEMANDE les propositions occupe la ligne : c'est le seul
   geste offert par le pied tant qu'il n'y a rien à montrer. Ceux des
   propositions, eux, se tiennent au bout de leur rangée — d'où le `>`,
   qui ne prend que le premier. */
.health-foot > .btn { width: 100%; background: color-mix(in srgb, var(--champ) 70%, transparent); }
.health-foot > .btn:active { background: var(--champ); }

#r-health.busy { opacity: .55; pointer-events: none; }

/* QUAND ON REDEMANDE SON AVIS À GEMINI, LA CARTE CHATOIE SUR PLACE.

   Elle était remplacée par un squelette générique — une pastille grise et
   deux barres — qui ne ressemblait pas à ce qui allait arriver et faisait
   sauter la fiche de cent pixels au retour. La vraie carte reste en
   place, à sa taille et dans sa teinte : c'est SON contenu qui chatoie,
   la note, la phrase et les chiffres. On voit l'endroit où la réponse va
   tomber, et rien ne bouge quand elle tombe.

   Réservé au « Recalculer la note » du menu, où l'on attend le réseau et
   où rien d'autre à l'écran ne le dit. « Analyser », lui, ne s'en sert
   pas : son estimation est instantanée, et ce qui prend du temps se dit
   dans le pied de la carte. */
/* PENDANT L'ATTENTE, LA CARTE PERD SA COULEUR.

   Elle garde celle de la note d'AVANT — verte, ambre ou rosée — pendant
   qu'on en calcule une autre : le premier signal qu'on lit sur cet
   encart, c'est sa teinte, et elle annonçait un résultat qui n'était pas
   encore là. Un gris-sable neutre le temps du calcul, et la couleur
   revient avec la réponse, une seule fois, quand elle veut dire quelque
   chose. */
.health-card.busy {
  pointer-events: none;
  background: var(--papier);
  border-color: var(--line);
}
.health-card.busy .hgrade,
.health-card.busy .health-txt strong,
.health-card.busy .health-txt small,
.health-card.busy .health-why,
.health-card.busy .health-chev {
  color: transparent !important;
  border-radius: var(--r-1);
  background-image: linear-gradient(100deg, var(--papier-2) 30%, var(--papier) 50%, var(--papier-2) 70%);
  background-size: 300% 100%;
  animation: shimmer 1.25s linear infinite;
}
.health-card.busy .hgrade { border-color: transparent; }
.health-card.busy .health-txt strong,
.health-card.busy .health-txt small,
.health-card.busy .health-why { display: block; min-height: 1em; }
.health-card.busy .health-chev .ico { visibility: hidden; }
@media (prefers-reduced-motion: reduce) {
  .health-card.busy * { animation: none; }
}

/* ===== Résultats de recherche : dire ce qu'on regarde ===== */
.kind-tag {
  display: inline-block;
  margin-right: 7px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--pastille);
  color: var(--pastille-ink);
  font-size: 12px;
  font-weight: 700;
}
/* « Rayon » et « Sous-rayon » sont des NATURES, pas des états : plus de
   vert, qui dans cette app veut dire « c'est bon ». Le mot suffit à les
   distinguer ; le sous-rayon garde un fond à peine plus froid. */
.prod.kind-cat .kind-tag { background: var(--pastille); color: var(--pastille-ink); }
.prod.kind-sub .kind-tag { background: var(--pastille-froide); color: var(--pastille-ink); }
/* Deux natures de plus, dans le même costume : d'où vient la ligne, et
   où en est-on avec elle.

   LE JAUNE dit « inventé à l'instant » — c'est la teinte de l'étincelle
   qui accompagne l'IA partout ailleurs dans l'app. LE GRIS dit « noté,
   jamais goûté » : un état d'attente ne prend pas de couleur, sans quoi
   il se lirait comme une qualité du plat. */
.kind-tag.ai { background: var(--ia-tag); color: var(--ia-tag-ink); }
.kind-tag.totry { background: var(--pastille); color: var(--pastille-ink); }

/* ===== Note santé posée sur la vignette ===== */
.thumb-wrap { position: relative; display: block; flex: 0 0 auto; }
.hgrade.on-img {
  position: absolute;
  left: -5px; bottom: -5px;
  margin: 0;
  width: 22px; height: 22px;
  border-radius: var(--r-1);
  border: 2px solid var(--surface);
  font-size: 12px;
  box-shadow: 0 2px 6px rgba(43, 38, 34, .18);
}
.slot-rec .hgrade.on-img { left: -4px; bottom: -4px; }

/* ===== Détail de la note santé ===== */
.health-detail {
  margin: 0;
  padding: 12px 14px 14px;
  border-top: 1px solid rgba(0, 0, 0, .07);
}
.hd-title {
  font-size: 14px; font-weight: 700; color: var(--ink);
  margin: 12px 0 6px;
}
.hd-title:first-child { margin-top: 0; }
.hd-row {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 4px 0; font-size: 14px; line-height: 1.3;
}
.hd-row b { white-space: nowrap; font-variant-numeric: tabular-nums; }
.hd-row.up b { color: var(--sage-deep); }

.hd-row.total {
  margin-top: 6px; padding-top: 8px;
  border-top: 1px solid var(--line);
  font-weight: 700;
}
.hd-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.hd-table th {
  text-align: right; font-size: 12px; color: var(--ink-soft); padding-bottom: 4px;
}
.hd-table th:first-child { text-align: left; }
.hd-table td { padding: 5px 0; border-top: 1px solid var(--line); text-align: right; }
.hd-table td:first-child { text-align: left; font-weight: 600; }
.hd-table td small { display: block; font-weight: 500; color: var(--ink-soft); font-size: 11px; }
.hd-note {
  display: inline-grid; place-items: center;
  min-width: 30px; padding: 1px 6px;
  border-radius: 999px; color: #fff; font-weight: 700; font-size: 12px;
}
.health-detail .hint { margin-top: 10px; }

#ij-prompt, #ij-answer { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.4; }
#ij-progress { margin-top: 12px; }
#ij-progress .progress { margin-bottom: 8px; }

.import-preview {
  width: 100%; max-height: 220px; object-fit: contain;
  border-radius: var(--r-3); background: var(--paper-img); margin-bottom: 12px;
}

/* ===== Menus ancrés ===== */
.pop-menu {
  position: fixed;
  z-index: 60;
  min-width: 216px;
  max-width: min(320px, calc(100vw - 20px));
  padding: 6px;
  background: var(--champ);
  border: 1.5px solid var(--line);
  border-radius: var(--r-3);
  box-shadow: 0 14px 34px rgba(43, 38, 34, .18);
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity .12s ease, transform .12s ease;
}
.pop-menu.in { opacity: 1; transform: none; }
.pop-menu button {
  width: 100%;
  display: grid;
  grid-template-columns: 24px 1fr;
  gap: 10px;
  align-items: center;
  min-height: 46px;
  padding: 6px 10px;
  border-radius: var(--r-2);
  font-weight: 700;
  text-align: left;
}
.pop-menu button:hover, .pop-menu button:focus-visible { background: var(--papier); outline: none; }
/* Les pictogrammes dessinés se règlent sur la colonne de 24 px et
   prennent la couleur du texte — comme partout ailleurs. */
.pop-menu button > span:first-child .ico { width: 21px; height: 21px; color: var(--ink-soft); }
.pop-menu button.danger { color: var(--danger); }
.pop-label small { display: block; font-weight: 500; font-size: 12px; color: var(--ink-soft); }

/* ===== Le bandeau des variantes =====

   Sous l'entête, avant la note : ce sont des chemins de traverse — « la
   même chose, en végé » — pas le contenu de la fiche. Ils défilent à
   l'horizontale parce qu'une famille compte deux ou trois membres, et
   qu'empilés ils repousseraient les ingrédients d'un écran.

   Le débordement est SUR CE BLOC seul (`data-inner-scroll`, que les
   gestes de feuille connaissent) : sans lui, le doigt posé sur une carte
   emporterait la feuille au lieu de faire défiler le bandeau. */

.variant-add {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 58px;
  padding: 0 16px;
  border: 1px dashed var(--accent-pale);
  border-radius: var(--r-3);
  background: transparent;
  color: var(--accent-fonce);
  font-weight: 700;
  font-size: 14px;
}
.variant-add:active { background: var(--accent-voile); }

/* ===== LA CARTE QUI SE REPLIE =====

   Le haut d'une fiche de recette porte deux résumés qu'on déplie quand on
   veut le détail : la famille du plat et sa note santé. Ils avaient deux
   vêtements — une carte teintée à entête cliquable pour la note, un
   intertitre nu suivi d'une liste pour la famille — et on lisait deux
   objets de nature différente là où il n'y en a qu'une.

   `fold-card` est ce vêtement, écrit une fois. La note garde le sien
   (`health-card`, plus bas), qui en est la déclinaison colorée : c'est la
   couleur qui porte le jugement, et elle n'a pas de sens sur une liste de
   variantes. */
/* PAPIER BLANC, LISERÉ FIN. La carte reprenait le crème du fond de
   feuille : posée sur lui, elle n'avait plus de contour à elle et son
   entête flottait au milieu de la page. Le papier la détache, le liseré
   la borne — c'est le vêtement des lignes du garde-manger, et une liste
   de recettes n'a pas de raison d'en porter un autre.

   De l'air AU-DESSUS : elle suit la bande colorée de l'entête, dont
   l'illustration déborde par le bas. Collée, la première ligne se lisait
   comme accrochée à la vignette. */
.fold-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  margin: 0 0 14px;
  overflow: hidden;
}
#r-variant-of.fold-card,
#ass-copy-of.fold-card {
  background: transparent;
  border: 0;
  border-radius: 0;
  overflow: visible;
}
#r-variant-of.fold-card { margin-bottom: 0; }
#r-variant-of .fold-head.as-title,
#ass-copy-of .fold-head.as-title {
  padding: 0 0 8px;
}
#r-variant-of .fold-body { padding: 0; }
#r-variant-of .fold-txt small { display: none; }
#r-variant-of .fold-txt strong,
#ass-copy-of .fold-txt strong {
  font-size: 15px;
  font-weight: 700;
  color: var(--ink);
}
/* L'air sous l'entête ne dépend plus de ce qui suit : il est le même
   pour une carte, un champ ou un titre (voir `--hero-gap`, tout en haut
   des règles de l'entête). */
.fold-head {
  display: grid;
  grid-template-columns: 1fr 22px;
  gap: 12px;
  align-items: center;
  width: 100%;
  padding: 10px 14px;
  background: transparent;
  text-align: left;
}
/* Sans chevron — « Variante de », sur une fiche neuve, n'a rien à
   replier : une seule ligne, qu'on voit déjà. */
.fold-head.as-title { grid-template-columns: 1fr; cursor: default; }
.fold-txt { min-width: 0; }
.fold-txt strong { display: block; }
.fold-txt small { display: block; }
.fold-chev {
  display: grid; place-items: center;
  color: var(--ink-soft);
  transition: transform .18s ease;
}
.fold-chev .ico { width: 20px; height: 20px; }
.fold-card.open .fold-chev { transform: rotate(180deg); }
.fold-body { padding: 0 10px 10px; }

/* Le filet entre deux cartes du haut de fiche. Sans lui, la famille et la
   note se touchent et se lisent comme un seul bloc — la note paraissait
   rangée DANS les variantes. */
.sheet-sep {
  height: 1px;
  margin: 0 4px 14px;
  background: var(--line);
}
.sheet-sep.variant-sep {
  margin-top: 16px;
  margin-bottom: 16px;
}
#r-variant-of + .sheet-sep.variant-sep { margin-top: 0; }

/* « Rattacher » prend toute la largeur en tête de liste : c'est la
   première ligne, et une pastille à moitié large au-dessus de lignes
   pleines se lirait comme un reste du bandeau d'avant. */
.variants-card .variant-add {
  width: 100%;
  box-sizing: border-box;
  justify-content: center;
  margin-bottom: 16px;
}
/* La ligne d'une variante EST une ligne de garde-manger — même vignette,
   même note santé posée dessus, même tête — moins l'étoile et moins le
   « + ». La grille perd donc sa dernière colonne, et n'en retrouve une
   que si la croix « détacher » est là. */
/* DANS UNE CARTE BLANCHE, LES LIGNES N'ONT PLUS DE CADRE.

   Chacune portait le sien — c'est ce qu'il faut sur le fond crème d'une
   liste, où rien d'autre ne les sépare. Posées dans un encart qui a déjà
   son liseré, elles dessinaient trois boîtes dans une boîte. Un filet
   entre deux voisines suffit à dire où l'une s'arrête. */
.variants-card .variant-row {
  grid-template-columns: 52px 1fr;
  margin: 0;
  padding: 8px 4px;
  background: transparent;
  border: 0;
  border-radius: 0;
}
.variants-card .variant-row + .variant-row { border-top: 1px solid var(--line); }
/* DANS LA CARTE DES VARIANTES, LE NOM S'ÉCRIT EN ENTIER.

   `.item-name` coupe sur une ligne partout ailleurs, et c'est le bon
   réglage pour une liste dense qu'on parcourt à la verticale : trente
   lignes de même hauteur se balaient d'un coup d'œil.

   Ici il n'y a pas trente lignes, il y en a deux ou trois — et le nom est
   la SEULE chose qui les distingue. « Gratin dauphinois aux pommes de
   terre nouvelles et à l… » et « Gratin dauphinois aux pommes de terre
   nouvelles et à l… » sont deux déclinaisons différentes qu'on détache à
   pile ou face. La carte a la place, le nom la prend. */
.variants-card .variant-row .item-name,
.need-card .variant-row .item-name {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  overflow-wrap: anywhere;
}
/* « Nécessaire pour » : 4 dp d'air au-dessus et au-dessous du filet
   entre deux plats, plutôt que le rembourrage plus large des variantes. */
/* 4 dp entre les pastilles (note + cuisinier, qui débordent de 5 px
   sous la vignette) et le filet — pas entre l'image et le filet. */
/* 8 dp entre les pastilles (note + cuisinier, qui débordent de 5 px
   sous la vignette) et le filet — pas entre l'image et le filet. */
.need-card .variant-row { padding-top: 8px; padding-bottom: 13px; }
.variants-card .variant-row:has(.variant-off) {
  grid-template-columns: 52px 1fr 36px;
}
.variants-card .variant-row.parent-chosen {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 16px;
  padding: 8px;
  background: var(--champ);
  border: 1.5px solid var(--line);
  border-radius: var(--r-3);
}
/* La croix d'une variante EST celle d'un ingrédient : elles partagent
   leur règle, plus haut (`.voice-x, .variant-off`). Retirer une ligne
   est le même geste où qu'on soit, il n'a pas à changer de dessin. */

/* Les deux portes du haut de « Ajouter un plat » : écrire le plat du
   soir, ou partir de ce qu'il reste. Les mêmes cartes que les façons de
   remplir une recette — c'est le même geste, « quelque chose remplit ça
   à ma place » — et deux colonnes dès qu'il y a la place. */
.pick-ways {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 8px;
  margin: 0 0 14px;
}
.quick-recipe {
  display: flex;
  align-items: stretch;
  gap: 8px;
  margin: 0 0 12px;
}
.quick-recipe input {
  flex: 1;
  min-width: 0;
}
.quick-recipe .prod-add {
  flex: 0 0 auto;
  align-self: center;
}

/* ===== Semainier ===== */

/* REMPLIR LA SEMAINE : le geste principal, à la taille du geste.

   Il vivait dans un pictogramme de 46 px en haut à droite, au milieu de
   trois autres, et il n'engendrait rien lui-même : il ouvrait un panneau
   de critères dont le bouton du bas remplissait la semaine. Autant dire
   que personne ne le trouvait.

   Il tient donc toute la largeur, en tête des jours, à la couleur
   d'action — c'est la seule chose de cet écran qu'on vient faire un
   dimanche soir. L'étincelle est la même que celle des créneaux, dans un
   rond de verre : c'est elle qui dit « c'est l'app qui choisit », et le
   mot à côté dit ce qu'on obtient. Les critères, eux, sont derrière la
   roue crantée de l'entête. */
.week-fill {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-height: 62px;
  margin: 0 0 12px;
  padding: 12px 18px;
  border: 0;
  border-radius: var(--r-3);
  background: var(--accent);
  color: #fff;
  box-shadow: 0 6px 16px rgba(var(--voile), .16);
}
.week-fill-txt {
  font-family: var(--f-titre);
  font-stretch: 88%;
  font-size: 20px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.week-fill-ico {
  flex: 0 0 auto;
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: 999px;
  background: rgba(255, 255, 255, .16);
}
.week-fill-ico .ico { width: 22px; height: 22px; }
.week-fill:active { transform: translateY(1px); background: var(--accent-appui); }

/* ---- l'apparition d'un plat tiré au sort ----

   Sept plats posés en une fraction de seconde, ça se voit mal : l'écran
   se recompose, et rien ne dit ce qui vient d'arriver. Chaque créneau
   rempli arrive donc en fondu montant, légèrement agrandi, et une bande
   de lumière traverse la vignette une fois — le « ça y est » qu'on
   attendait de l'étincelle.

   Le retard vient du RANG du créneau (`--magie`, posé par le rendu) :
   c'est ce qui fait la cascade du lundi au dimanche plutôt que sept
   apparitions simultanées. Plafonné, sinon le dimanche d'une semaine
   entière attendrait une seconde pour rien.

   Purement décoratif : rien ne dépend de l'animation, et les appareils
   qui demandent moins de mouvement n'ont qu'un fondu. */
@keyframes plat-magique {
  from { opacity: 0; transform: translateY(6px) scale(.96); }
  60%  { opacity: 1; }
  to   { opacity: 1; transform: none; }
}
/* Le reflet se déplace par sa POSITION DE FOND, pas par une translation :
   translaté, il serait sorti de sa vignette, et il aurait fallu un
   `overflow: hidden` qui tranchait la pastille de note posée à cheval sur
   le coin (voir `.hgrade.on-img`). */
@keyframes plat-lueur {
  from { background-position: -40% 0; opacity: 1; }
  to   { background-position: 140% 0; opacity: 0; }
}
.slot-magic {
  animation: plat-magique .5s cubic-bezier(.2, .9, .3, 1) both;
  animation-delay: calc(min(var(--magie, 0), 8) * 65ms);
}
/* La bande de lumière est posée SUR la vignette, pas sur le créneau : un
   reflet qui balaie une photo se lit comme une photo qu'on découvre ;
   le même reflet sur toute la ligne se lit comme un défaut d'affichage. */
.slot-magic .thumb-wrap::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: var(--r-2);
  background: linear-gradient(105deg, transparent 38%,
              rgba(255, 255, 255, .7) 50%, transparent 62%);
  background-size: 260% 100%;
  background-repeat: no-repeat;
  animation: plat-lueur .7s ease-out both;
  animation-delay: calc(min(var(--magie, 0), 8) * 65ms + 120ms);
  pointer-events: none;
}
@keyframes plat-fondu { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .slot-magic { animation: plat-fondu .2s linear both; animation-delay: 0s; }
  .slot-magic .thumb-wrap::after { display: none; }
}

/* ---- l'écran de bienvenue du semainier ----

   IL TIENT SUR UN ÉCRAN, SANS DÉFILEMENT — c'est la contrainte qui décide
   de toute la mise en page. Un onboarding qu'on fait défiler est un
   onboarding qu'on abandonne : le geste qu'on attend (cocher, puis
   appuyer) doit être visible d'emblée, sinon on ne le fait pas.

   D'où trois blocs et un seul élastique : le dessin prend ce qui reste.
   Sur un grand téléphone il respire ; sur un petit écran en paysage il
   se réduit à une bande, et le texte, la grille et le bouton restent
   entiers. C'est le bon ordre de sacrifice — l'image séduit, les trois
   autres font le travail. */
/* LA HAUTEUR SE PREND SUR CE QUI RESTE, PAS SUR L'ÉCRAN.

   `height: 100dvh` sur la section suffisait tant qu'elle était seule ;
   elle ne l'est pas. Un bandeau la coiffe souvent — la démo, le rappel
   du mode gratuit — et il mesure jusqu'à 90 px : la section faisait alors
   un écran de haut EN DESSOUS du bandeau, et le bouton « Remplir ma
   semaine » tombait hors champ. C'est-à-dire précisément le geste pour
   lequel l'écran existe.

   C'est donc `#app` qui fait un écran de haut, et la section qui prend ce
   qui reste — quel que soit le nombre de bandeaux. La classe est posée
   sur le corps de page par la boucle de rendu (voir main.js), comme pour
   le mode magasin : ces règles ne doivent toucher que cet écran-là. */
body.onb-plein #app {
  height: 100dvh;
  max-height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.screen.onb {
  flex: 1 1 auto;
  min-height: 0;
  /* Le bloc entier est CENTRÉ, dessin compris. Le dessin prenait toute la
     place restante et s'y centrait tout seul : sur un grand téléphone,
     ça donnait un tiers d'écran vide entre le bandeau et lui, et le texte
     collé en bas. Centrer l'ensemble répartit le blanc des deux côtés. */
  justify-content: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4) calc(var(--tabbar) + var(--safe-b) + var(--sp-3));
  box-sizing: border-box;
  text-align: center;
}
.onb-art {
  flex: 0 1 auto;
  min-height: 0;
  display: grid;
  place-items: center;
}
/* `dvh` plutôt qu'une hauteur fixe : c'est le dessin qui cède quand
   l'écran est court (un téléphone en paysage, un clavier ouvert), et
   jamais la grille ni le bouton. */
.onb-svg { width: 100%; height: auto; max-height: min(260px, 30dvh); }
.onb-bg { fill: var(--papier-2); }
.onb-card rect:first-child { fill: var(--surface); stroke: var(--line); stroke-width: 1.5; }
.onb-card.front rect:first-child { stroke: var(--accent-pale); }
.onb-plate { fill: var(--accent-voile); }
.onb-plate.a { fill: var(--sage); opacity: .34; }
.onb-plate.c { fill: var(--etiquette); opacity: .45; }
.onb-yolk { fill: var(--accent); opacity: .55; }
.onb-line { fill: var(--ink-mute); opacity: .5; }
.onb-line.soft { opacity: .28; }
.onb-star { fill: var(--accent); opacity: .85; }
.onb-star.big { opacity: 1; }

.onb-body { flex: 0 0 auto; }
.onb-title {
  font-family: var(--f-titre);
  font-stretch: 88%;
  font-size: clamp(22px, 6vw, 28px);
  letter-spacing: -0.02em;
  margin: 0 0 var(--sp-2);
}
.onb-say {
  color: var(--ink-soft);
  font-size: 15px;
  line-height: 1.45;
  margin: 0 auto var(--sp-3);
  max-width: 34ch;
}

/* LA GRILLE DES CRÉNEAUX, LE PLUS SERRÉ POSSIBLE.

   Quatorze cases, deux intitulés et sept jours sur la largeur d'un
   téléphone : chaque colonne fait 38 px à 375 px de large. C'est sous les
   44 px des cibles confortables, et c'est assumé ici — la grille est un
   APERÇU qu'on ajuste, pas un formulaire qu'on remplit ; le geste par
   défaut est de ne rien toucher et d'appuyer sur le bouton. Les créneaux
   se règlent ensuite un par un dans le semainier, où ils ont toute la
   place.

   Les jours passés sont éteints et ne répondent pas : on ne prévoit pas
   le déjeuner de lundi un jeudi. Ils restent VISIBLES pour que la semaine
   garde ses sept colonnes — une grille qui rétrécirait selon le jour se
   relirait de travers. */
.onb-grid {
  display: grid;
  grid-template-columns: auto repeat(7, minmax(0, 1fr));
  gap: var(--sp-1);
  align-items: center;
  margin: 0 0 var(--sp-2);
}
.onb-corner { display: block; }
.onb-day, .onb-slot-lbl {
  color: var(--ink-soft);
  font-size: 12px;
  font-weight: 700;
}
.onb-day.past { opacity: .4; }
.onb-slot-lbl { padding-right: var(--sp-1); text-align: right; }
.onb-pick {
  display: grid;
  place-items: center;
  height: 34px;
  border-radius: var(--r-1);
  background: var(--champ);
  border: 1.5px solid var(--line);
  color: var(--ink-mute);
}
.onb-pick.on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
/* Un jour derrière nous : hachuré comme un créneau ignoré ailleurs dans
   l'app, et muet. Le même dessin pour le même fait. */
.onb-pick.past {
  background: repeating-linear-gradient(-45deg,
    transparent 0 5px, var(--line) 5px 6px);
  border-style: dashed;
  opacity: .5;
}
.onb-tick { display: none; }
.onb-pick.on .onb-tick { display: grid; }
.onb-tick .ico { width: 17px; height: 17px; }

.onb-count { color: var(--ink-soft); font-size: 13px; margin: 0 0 var(--sp-3); }
.onb-go { max-width: 420px; margin: 0 auto; }
.onb-go[disabled] { opacity: .45; }
.onb-go-ico { display: grid; place-items: center; }
.onb-go-ico .ico { width: 20px; height: 20px; }

/* Sur ordinateur, l'écran de bienvenue reste une colonne centrée : étalé
   sur 1180 px, le dessin devenait une fresque et la grille des créneaux
   une frise de quatorze cases de 140 px. */
@media (min-width: 900px) {
  .screen.onb {
    max-width: 640px;
    margin: 0 auto;
    padding-bottom: var(--sp-5);
  }
  .onb-svg { max-height: 320px; }
  .onb-pick { height: 42px; }
  .onb-say { font-size: 16px; }
}

/* Un groupe de puces a besoin d'air : « Type de plat » et « Temps »
   se touchaient presque. */
.field-choice { margin-bottom: 18px; }
.prep-row { gap: 8px; }
.prep-row .chip { min-height: 44px; }

/* Le menu de l'illustration remplace le bouton ✨. */
/* Les boutons de l'entête de carte : l'étoile, le crayon, le menu.

   Ils sont posés sur l'illustration, en haut à droite — la seule surface
   libre d'une fiche, et celle que le pouce atteint sans faire défiler.
   Fond opaque et ombre : une vignette claire avale un bouton translucide. */
.hero-tools {
  position: absolute; top: 10px; right: 10px;
  display: flex; align-items: center; gap: 6px;
}
.hero-btn {
  width: 40px; height: 40px;
  border-radius: var(--r-2);
  background: rgba(255, 255, 255, .94);
  color: #23272e;
  display: grid; place-items: center;
  box-shadow: 0 2px 8px rgba(43, 38, 34, .16);
}
.hero-btn .ico { width: 20px; height: 20px; stroke-width: 2; }
.hero-menu {
  font-size: 22px; font-weight: 700;
  line-height: 1; padding-bottom: 8px;
}
.hero-fav { color: var(--ink-mute); }
.hero-fav.on { color: var(--etiquette); }
.hero-fav.on .ico { fill: var(--etiquette); stroke: var(--etiquette-fonce); }

/* Propositions sous un champ d'ingrédient : même présentation que la
   recherche d'article. */
/* La rangée du nom : le champ, et la loupe qui va le remplir depuis le
   garde-manger. La liste des propositions reste ancrée sur le CHAMP —
   elle complète ce qu'on tape, elle n'a pas à passer sous le bouton. */
.ing-name-row { display: flex; align-items: stretch; gap: 6px; }
.ing-field { position: relative; display: block; flex: 1; min-width: 0; }
.ing-field .ac { top: calc(100% + 4px); max-height: 236px; }
.ing-main input { width: 100%; }
/* Aussi haute que le champ qu'elle sert : c'est la même rangée, et deux
   hauteurs voisines qui diffèrent de trois pixels se voient. */
.ing-pick {
  flex: 0 0 auto;
  width: var(--ing-btn, 36px);
  height: 36px;
  align-self: center;
  display: grid; place-items: center;
  border-radius: var(--r-2);
  background: var(--champ);
  border: 1px solid var(--line);
  color: var(--ink-soft);
}
.ing-pick .ico { width: 19px; height: 19px; }
.ing-pick:active { background: var(--accent); border-color: var(--accent); color: #fff; }

/* Un nom de plat trop long doit s'abréger, pas déborder de sa carte. */
.slot-rec { min-width: 0; }
.slot-rec > div { min-width: 0; }
.slot-rec .item-name, .slot-rec .item-meta {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.slot-filled { min-width: 0; }

/* Les boutons d'entête portent des SVG : même taille que le texte des
   emojis qu'ils remplacent. */
.icon-btn .ico { width: 22px; height: 22px; }

/* ============================================================
   Les trois volets du garde-manger
   ============================================================ */

/* De vrais onglets, sur toute la largeur.

   Trois pastilles centrées ne disaient pas qu'on change de contenu : elles
   avaient exactement l'allure des filtres posés juste en dessous, et on
   les prenait pour eux. Des onglets pleine largeur, avec leur pictogramme
   et le trait sous celui qui est actif, se lisent d'un coup — c'est la
   forme que tout le monde connaît. */
.pane-tabs {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 2px;
  margin-top: 10px;
  background: var(--papier-2);
  border-radius: var(--r-2);
  padding: 3px;
  max-width: none;
}
.pane-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 0 6px;
  font-size: 14px;
  position: relative;
}
.pane-tab .ico { width: 20px; height: 20px; }
.pane-ico { display: grid; place-items: center; flex: 0 0 auto; }
/* L'ÉTOILE DANS UNE PASTILLE DE FILTRE.

   Les favoris ont quitté les onglets pour l'entonnoir de chaque volet.
   L'étoile y est pleine — c'est un état, pas une invite : elle dit « on
   ne montre que ceux-là », là où l'étoile creuse posée sur une ligne
   propose de la remplir. */
.chip-star {
  flex: 0 0 auto;
  display: inline-grid;
  place-items: center;
  margin: 0 6px 0 -2px;
  color: var(--etiquette);
}
.chip-star .ico { width: 17px; height: 17px; fill: currentColor; }
.chip.active .chip-star { color: #fff; }

@media (max-width: 380px) {
  /* Sur les petits écrans, le pictogramme suffit à l'onglet inactif. */
  .pane-tab:not(.active) .pane-lbl { display: none; }
}

/* La grille des rayons réserve la hauteur de la barre d'onglets sous
   elle — c'est ce qu'il faut quand elle finit la page. Dans les favoris
   elle est prise en sandwich entre deux listes : ce coussin devenait un
   trou de cent pixels au milieu de l'écran. */

/* ============================================================
   Favoris
   ============================================================ */

/* L'étoile posée sur une ligne : creuse, elle propose ; pleine, elle
   confirme. Elle ne prend pas plus de place que le « + » d'à côté — c'est
   une ligne de liste, pas une barre d'outils. */
.fav-btn {
  width: 44px; height: 44px;
  flex: 0 0 auto;
  display: grid; place-items: center;
  border-radius: var(--r-2);
  color: var(--ink-mute);
}
.fav-btn .ico { width: 23px; height: 23px; }
/* Le jaune d'étiquette, pas le vert d'action : l'étoile doit se
   distinguer du « + » plein posé juste à côté, sinon les deux boutons se
   lisent comme un seul bloc. */
.fav-btn.on { color: var(--etiquette); }
.fav-btn.on .ico { fill: var(--etiquette); stroke: var(--etiquette-fonce); }
.fav-btn:active { background: var(--papier); }

/* Les trois gestes d'une ligne de rayon : ouvrir la fiche, étoiler,
   verser dans la liste.

   MÊME TAILLE TOUS LES TROIS. Ils étaient à 34, 42 et 48 px, chacun ayant
   sa bonne raison — l'étoile porte un état, le « + » est le geste
   quotidien — mais trois tailles côte à côte se lisent comme une
   hiérarchie, et il n'y en a pas : selon le moment, on vient ranger, on
   vient lire, on vient acheter. Une colonne de boutons alignés se vise
   aussi mieux du pouce qu'un escalier.

   C'est la COULEUR qui les distingue maintenant, pas la taille : le « + »
   plein de la couleur d'action, l'étoile jaune quand elle est posée, le
   « ⓘ » en contour. */
.prod .fav-btn,
.prod .prod-add {
  width: 40px; height: 40px;
  flex: 0 0 auto;
  border-radius: var(--r-2);
  display: grid;
  place-items: center;
}
/* L'étoile passe devant le « ⓘ » et prend son contour.

   Elle était nue entre deux boutons dessinés — un creux au milieu de la
   rangée, qui la faisait lire comme une décoration plutôt que comme une
   cible. Elle vient aussi EN PREMIER : c'est le geste le plus léger des
   trois, et on lit de gauche à droite en allant du plus anodin (ranger
   en favori) au plus engageant (verser dans la liste).

   Trois contours identiques, une seule couleur de remplissage : c'est le
   « + » plein qui reste le geste principal, et l'étoile posée se
   signale par son jaune, pas par sa taille. */
.prod .fav-btn {
  background: var(--surface);
  border: 1.5px solid var(--line);
  color: var(--ink-soft);
}

.prod .fav-btn:active { background: var(--papier); transform: translateY(1px); }
.prod .fav-btn .ico { width: 22px; height: 22px; }
.prod .fav-btn.on { color: var(--etiquette); border-color: var(--etiquette); }
.prod .prod-add { font-size: 24px; padding: 0 0 3px; }

/* ============================================================
   Filtres : les enlever, et savoir qu'il y en a
   ============================================================ */

/* LE TITRE NE BOUGE PAS QUAND « TOUT ENLEVER » APPARAÎT.

   Le bouton, plus haut que la ligne de texte, faisait grandir la rangée
   au premier filtre posé : le mot « Filtres » descendait de quatre
   pixels, et tout l'encart avec lui. Une hauteur réservée d'avance, et
   la rangée ne dépend plus de ce qu'elle contient. */
.filter-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 28px;
}
#btn-filters-done { margin-top: 14px; }
/* Trier n'est pas filtrer : le sous-titre le dit, parce que les deux
   rangées de pastilles se ressemblent trop pour se distinguer seules. */
.filter-title.sub {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
}

/* Le rang d'un tri, quand plusieurs sont posés. Un petit jeton dans la
   pastille : il dit dans quel ordre on les a demandés, sans allonger le
   texte du critère. */
.chip-rank {
  display: inline-grid;
  place-items: center;
  width: 17px; height: 17px;
  margin-right: 5px;
  border-radius: 50%;
  background: rgba(255, 255, 255, .28);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .45);
  font-size: 11px;
  font-weight: 800;
  line-height: 1;
}
.filter-hint {
  margin: -2px 2px 8px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-soft);
}
.filter-hint b { color: var(--ink); }

.filter-reset {
  font-size: 12px;
  font-weight: 700;
  color: var(--accent-fonce);
  text-transform: none;
  letter-spacing: 0;
  padding: 4px 8px;
  border-radius: var(--r-2);
  background: var(--papier);
}

/* ---------- avatars ----------

   Une pastille ronde : le smiley, sur un fond teinté de la couleur des
   cheveux. La teinte n'est pas un ornement — les smileys d'Unicode ne
   distinguent ni le châtain du brun ni le court du long, et deux
   personnes d'une même maison se ressemblent vite. Le fond tranche là où
   le dessin ne peut pas.

   `--ton` est posé sur l'élément par le rendu ; la pastille en tire un
   fond très clair et un liseré franc, ce qui la rend lisible sur crème
   comme sur blanc. */
.avatar {
  --ton: #c39a6b;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  display: inline-grid;
  place-items: center;
  border-radius: 50%;
  border: 2px solid var(--ton);
  background: color-mix(in srgb, var(--ton) 16%, var(--surface));
  font-size: 18px;
  line-height: 1;
  padding: 0;
}
.avatar.sm { width: 28px; height: 28px; font-size: 15px; border-width: 1.5px; }
/* « TOUT LE MONDE » : deux têtes, dans une pastille ALLONGÉE.

   Deux visages ne tiennent pas dans le rond d'un seul sans devenir
   illisibles à vingt-huit pixels. La pastille s'étire donc en gélule —
   ce qui la distingue aussi, de loin, d'une pastille de personne : on
   n'a même pas besoin de reconnaître les têtes.

   Les deux `<i>` portent la même taille et la même hauteur de ligne :
   deux emojis posés côte à côte dans du texte se décalent d'un pixel ou
   deux selon la police, et l'un des deux paraît plus gros. */
.duo {
  display: inline-flex;
  align-items: center;
  gap: 0;
  line-height: 1;
}
.duo i {
  font-style: normal;
  font-size: .62em;
  line-height: 1;
  display: inline-block;
}
.duo i + i { margin-left: -.14em; }

.avatar.tous {
  width: auto;
  min-width: 42px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--papier);
  /* Un liseré qui se voit, comme celui d'une tête seule : il était de la
     couleur des filets, et la puce « tout le monde » semblait sans bord à
     côté des têtes cerclées de leur couleur. */
  border-color: var(--ink-mute);
  font-size: 17px;
}
/* LA TÊTE D'UN JOUR PASSÉ.

   Elle garde sa forme et son smiley — c'est ce qu'on vient lire — mais
   elle perd sa couleur : plus de liseré à la teinte de la personne, un
   fond gris très clair, et le dessin lui-même désaturé. Dans une app où
   la couleur dit « on peut agir ici », une pastille en couleur qui ne
   répond plus est une promesse en l'air.

   `filter` plutôt qu'une autre teinte : le smiley est un caractère, on
   ne peut pas lui donner une couleur, seulement lui retirer la sienne. */
.avatar.fane {
  border-color: var(--line);
  background: var(--papier);
  filter: grayscale(1);
  opacity: .82;
}
.avatar.fane:active { transform: none; }
.avatar.lg { width: 52px; height: 52px; font-size: 27px; }
.avatar.xl { width: 76px; height: 76px; font-size: 40px; border-width: 3px; }
.avatar:active { transform: scale(.92); }

/* Sur la liste, la tête se pose à gauche de la ligne d'état. */
.status .avatar { margin-right: 2px; }

/* Sur une ligne de recette, elle se pose SUR la vignette, JUSTE À CÔTÉ
   de la note santé.

   Une colonne de plus aurait coûté trente-cinq pixels au nom du plat, et
   c'est le nom qu'on lit — d'où la vignette. Elle a d'abord occupé le
   coin opposé, en haut à droite : deux pastilles en diagonale sur
   cinquante-six pixels, que l'œil lisait comme deux signaux sans
   rapport, et dont chacun demandait son propre aller-retour.

   Elles en ont un : ce sont les deux choses qu'on veut savoir d'un plat
   avant de l'ouvrir — ce qu'il vaut, et à qui il est. Au même bord, à la
   même taille, elles se lisent d'un seul mouvement. */
.avatar.by-grade {
  position: absolute;
  left: 20px; bottom: -5px;
  width: 22px; height: 22px;
  font-size: 12px;
  border-width: 2px;
  border-color: var(--surface);
  background: color-mix(in srgb, var(--ton) 42%, var(--surface));
  box-shadow: 0 2px 6px rgba(43, 38, 34, .18);
}

/* Dans un créneau du semainier, elle se pose au bout de la rangée
   « Midi / Soir » : c'est une signature, pas une action, et c'est la
   seule ligne qui ait de la place à droite. */
.slot-h { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

/* La tête dans une pastille de filtre : « Aux fourneaux · 🧑 Sacha ».
   Elle tient dans la hauteur du texte, sans faire grandir la pastille. */
.chip-tete {
  flex: 0 0 auto;
  display: inline-grid;
  place-items: center;
  width: 20px; height: 20px;
  margin: 0 6px 0 -2px;
  border-radius: 50%;
  border: 1.5px solid var(--ton, #c39a6b);
  background: color-mix(in srgb, var(--ton, #c39a6b) 18%, var(--surface));
  font-size: 11px;
  line-height: 1;
}
.chip.active .chip-tete { border-color: rgba(255, 255, 255, .75); }

/* Le réglage : l'aperçu d'abord, en grand, la rangée dessous. */
.avatar-preview {
  display: grid;
  justify-items: center;
  gap: 8px;
  margin: 4px 0 16px;
}

/* Sur l'écran d'accueil, le bloc se distingue du champ qui le précède
   sans devenir une carte de plus : un filet, un intitulé, et l'air qu'il
   faut pour ne pas coller au prénom. */
.auth-avatar { margin: 4px 0 14px; padding-top: 12px; border-top: 1px solid var(--line); }
.auth-avatar .field-lbl {
  display: block;
  margin: 0 2px 10px;
  font-size: 15px; font-weight: 700; color: var(--ink);
}
.auth-avatar .filter-row { margin-bottom: 10px; }
.auth-avatar .filter-hint { margin-bottom: 0; }

/* ---------- qui cuisine, et combien de fois ---------- */

.quota-list { display: grid; gap: 6px; }
.quota-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px 6px 6px;
  border-radius: var(--r-2);
  background: var(--papier);
}
.quota-who { flex: 1; min-width: 0; display: flex; align-items: center; gap: 9px; font-size: 14px; }
.quota-who b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.quota-step {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 4px;
  background: var(--champ);
  border-radius: var(--r-2);
  padding: 3px;
}
.quota-step button {
  width: 36px; height: 36px; border-radius: var(--r-2);
  border: 1px solid var(--line);
  background: var(--champ);
  font-size: 19px; font-weight: 700;
}
.quota-step b { min-width: 26px; text-align: center; font-variant-numeric: tabular-nums; }

/* Le prénom et la tête, côte à côte dans les Réglages. */
.moi-row { display: flex; align-items: flex-end; gap: 12px; }
.moi-row .avatar { margin-bottom: 6px; }

/* ---------- le bandeau de démo ----------

   Il est volontairement voyant, et il ne s'en va pas : une démo qui ne
   dit pas qu'elle en est une est une démonstration de rien. Sur une
   capture d'écran comme sur un téléphone tendu, on doit lire en une
   seconde que les notes, les échanges et les recettes lues sont des
   textes écrits d'avance. */
.demo-bar {
  position: sticky;
  top: 0;
  z-index: 15;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 14px calc(9px + 0px);
  padding-top: calc(9px + var(--safe-t, 0px));
  background: var(--nuit);
  color: #e6e9ee;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.35;
}
.demo-tag {
  flex: 0 0 auto;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--etiquette);
  color: #23272e;
  font-size: 11px;
  font-weight: 800;
}
.demo-txt { flex: 1; min-width: 0; }
.demo-quit {
  flex: 0 0 auto;
  min-height: 32px;
  padding: 0 12px;
  border-radius: var(--r-2);
  border: 1px solid rgba(230, 233, 238, .35);
  background: none;
  color: #e6e9ee;
  font-weight: 700;
  font-size: 12.5px;
}
.demo-quit:active { background: rgba(230, 233, 238, .16); }

/* Le chevron qui replie. Entre la phrase et « Quitter » : c'est la phrase
   qu'il concerne, et le geste doit se lire de gauche à droite — je lis, je
   replie, je sors. */
.demo-fold {
  flex: 0 0 auto;
  width: 30px; height: 30px;
  display: grid; place-items: center;
  border-radius: var(--r-2);
  color: rgba(230, 233, 238, .8);
  font-size: 13px;
  line-height: 1;
}
.demo-fold:active { background: rgba(230, 233, 238, .16); }

/* Replié, le bandeau tient sur une ligne — la pastille « Démo » et
   « Quitter » restent, seule la phrase se raccourcit. On ne peut donc
   jamais croire qu'on est dans la vraie app. */
.demo-bar.petit { padding-top: calc(5px + var(--safe-t, 0px)); padding-bottom: 5px; }

/* Le bandeau du mode gratuit : le jaune d'une information à lire, pas le
   noir de la démo — ici, c'est pour de vrai, seulement limité. */
.gratuit-bar {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  margin: calc(var(--sp-2) + var(--safe-t, 0px)) var(--sp-4) 0;
  padding: var(--sp-3) var(--sp-2) var(--sp-3) var(--sp-4);
  border-radius: var(--r-2);
  background: var(--moyen-voile);
  border: 1px solid var(--moyen-filet);
  color: var(--ink);
  font-size: 14px; line-height: 1.4;
}
.gratuit-bar-txt { flex: 1; min-width: 0; }
.gratuit-bar-txt b { margin-right: 4px; }
.gratuit-bar-lien { padding: 0; margin-left: 2px; color: var(--accent-fonce); font-weight: 700; text-decoration: underline; }
.gratuit-bar-x { flex: none; width: 36px; height: 36px; margin: -6px 0; border-radius: var(--r-1); color: var(--ink-soft); font-size: 16px; }
.gratuit-bar-x:hover { background: var(--papier-2); }
@media (min-width: 900px) {
  .gratuit-bar { margin-left: calc(248px + var(--sp-6)); margin-right: var(--sp-6); }
}
.demo-bar.petit .demo-txt { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Sur téléphone, la phrase entière prend deux lignes et pousse tout
   l'écran : on garde le mot qui compte, et le détail reste à portée de
   lecteur d'écran. */
@media (max-width: 560px) {
  .demo-txt { font-size: 12px; }
  .demo-bar { gap: 8px; padding-left: 10px; padding-right: 10px; }
}
@media (min-width: 900px) {
  /* Le rail occupe la gauche : le bandeau ne coiffe que le contenu. */
  .demo-bar { margin-left: 248px; }
}

/* ---------- remplir une fiche de recette ---------- */

/* L'intitulé d'un champ et son micro sur la même ligne. L'intitulé garde
   ses petites capitales grises (voir plus haut) ; le micro se pose au
   bout, sans rien pousser. */
.field-head {
  display: flex !important;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.field-head label { text-transform: inherit; letter-spacing: inherit; font: inherit; color: inherit; }
.h4-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 18px 0 9px;
}
.h4-row h4 { flex: 1; }

/* LE MICRO NE DOIT PAS SOULEVER SON INTITULÉ.

   Il fait 34 px de haut, l'intitulé qui l'accompagne en fait 16 : posé
   dans la même rangée, il l'écartait du champ de deux fois la marge
   prévue, et le premier champ d'une fiche se retrouvait poussé d'un
   demi-centimètre. La rangée a donc une hauteur fixe et porte elle-même
   la marge basse, une seule fois ; le bouton n'en ajoute plus. */
.field-head { min-height: 34px; margin-bottom: 6px !important; }
.h4-row { min-height: 34px; }
.h4-row h4 { margin-top: 0; margin-bottom: 0; }
.mic-btn {
  flex: 0 0 auto;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: var(--r-2);
  border: 1.5px solid var(--line);
  background: var(--champ);
  color: var(--ink-soft);
}
.mic-btn .ico { width: 18px; height: 18px; }
.mic-btn:active { background: var(--papier); }
/* En train d'écouter : le bouton se remplit et respire, comme le gros
   micro de l'écran de dictée. On doit voir d'un coup d'œil lequel des
   trois micros est ouvert. */
.mic-btn.on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  animation: mic-pulse 1.4s ease-in-out infinite;
}
@keyframes mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(42, 68, 112, .45); }
  50% { box-shadow: 0 0 0 6px rgba(42, 68, 112, 0); }
}
@media (prefers-reduced-motion: reduce) { .mic-btn.on { animation: none; } }

/* Les quatre façons de remplir la fiche, posées dans la fiche.

   Deux colonnes de cartes plutôt qu'une liste : ce sont quatre chemins
   de même rang, et une liste verticale aurait fait croire à un ordre de
   préférence. Chacune porte son pictogramme en grand — c'est ce qu'on
   reconnaît avant de lire, et « site web » et « photo d'un plat » ne se
   confondent pas en dessin. */
.assist {
  margin: 0 0 18px;
  padding: 0;
  border: 0;
  background: transparent;
}
.assist-lead {
  display: block;
  margin: 0 0 var(--sp-2);
  font-size: 15px;
  font-weight: 700;
  color: var(--ink);
}
.assist-ways { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.assist-way {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 62px;
  padding: 8px 10px;
  border-radius: var(--r-3);
  border: 1.5px solid var(--line);
  background: var(--champ);
  text-align: left;
}
.assist-ico {
  flex: 0 0 auto;
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: var(--r-2);
  background: var(--papier);
  color: var(--accent-fonce);
}
.assist-ico .ico { width: 21px; height: 21px; }
.assist-txt { min-width: 0; }
.assist-txt strong { display: block; font-size: 14px; line-height: 1.2; }
.assist-txt small { display: block; color: var(--ink-soft); font-size: 11.5px; font-weight: 600; line-height: 1.25; }
.assist-way.on {
  border-color: var(--accent);
  background: var(--papier);
  box-shadow: 0 0 0 3px rgba(42, 68, 112, .14);
}
.assist-way.on .assist-ico { background: var(--accent); color: #fff; }

.assist-form { margin-top: 10px; }
.assist-open {
  padding: 12px;
  border-radius: var(--r-3);
  background: var(--champ);
  border: 1.5px solid var(--line);
}
.assist-open-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 10px;
  font-size: 14px;
}
.assist-close {
  width: 32px; height: 32px; border-radius: var(--r-2);
  display: grid; place-items: center;
  color: var(--ink-soft); font-size: 15px; font-weight: 700;
}
.assist-close:active { background: var(--papier); }
.assist-open .hint { margin: 8px 2px 0; }
.assist-open .btn { margin-top: 10px; }
.import-preview {
  display: block;
  width: 100%;
  max-height: 220px;
  object-fit: contain;
  margin-top: 10px;
  border-radius: var(--r-2);
  background: var(--papier);
}
@media (max-width: 380px) { .assist-ways { grid-template-columns: 1fr; } }

/* LE CHATOIEMENT DE REMPLISSAGE.

   Pendant que la page est lue, la fiche ne peut rien montrer de vrai — et
   un tourniquet au milieu de l'écran ne dirait pas OÙ le texte va
   arriver. Les champs eux-mêmes se mettent donc à chatoyer : c'est un
   brouillon de ce qui vient, posé exactement à sa place.

   Rien n'est cliquable le temps de l'attente : un « Enregistrer » appuyé
   sur une fiche à moitié remplie enregistrerait la moitié. */
/* LE CHATOIEMENT GARDE LA FORME DE LA FICHE.

   Tout ce qui n'était pas l'encart d'import devenait un rectangle gris,
   entête comprise : on perdait la bande teintée, le carré de
   l'illustration et les intitulés — c'est-à-dire tout ce qui dit quelle
   fiche on regarde et où le texte va tomber. Six barres empilées, ça ne
   ressemble plus à rien, et ça n'annonce rien.

   Ce qui reste : le décor (entête, filets) et les REPÈRES (« Nom »,
   « Préparation », « Ingrédients »). Ce qui chatoie : les endroits qui
   vont se remplir, à leur place et à leur taille. */
.sheet.filling .sheet-content > *:not(#r-assist) { pointer-events: none; }

/* L'entête garde sa couleur et son carré ; seul le titre écrit dessus
   s'efface, puisque c'est lui qui va changer. */
.sheet.filling .hero-cap { visibility: hidden; }

/* Les intitulés pâlissent sans disparaître : la fiche reste lisible
   comme un plan de ce qui arrive. */
.sheet.filling .field > span,
.sheet.filling .h4-row h4,
.sheet.filling .sheet-content > h4 { opacity: .5; }

.sheet.filling .field input,
.sheet.filling .field textarea,
.sheet.filling .field .picker,
.sheet.filling .prep-row,
.sheet.filling .season-row,
.sheet.filling .serves-row,
.sheet.filling #r-health,
.sheet.filling #r-variants,
.sheet.filling #r-variant-of,
.sheet.filling #r-ings,
.sheet.filling .sheet-content > button,
.sheet.filling .sheet-actions {
  position: relative;
  color: transparent !important;
  border-color: transparent !important;
  box-shadow: none !important;
  background-color: transparent;
  background-image: linear-gradient(100deg, var(--papier-2) 30%, var(--papier) 50%, var(--papier-2) 70%);
  background-size: 300% 100%;
  border-radius: var(--r-2);
  min-height: 44px;
  animation: shimmer 1.25s linear infinite;
}
/* Ce qui est DANS un bloc qui chatoie ne doit pas transparaître : un
   pictogramme ou une pastille garde sa couleur, elle, et tacherait le
   dégradé. */
.sheet.filling .prep-row *,
.sheet.filling .season-row *,
.sheet.filling .serves-row *,
.sheet.filling #r-health *,
.sheet.filling #r-variants *,
.sheet.filling #r-variant-of *,
.sheet.filling #r-ings *,
.sheet.filling .sheet-actions *,
.sheet.filling .sheet-content > button * { visibility: hidden; }
@keyframes shimmer {
  from { background-position: 150% 0; }
  to { background-position: -150% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .sheet.filling .sheet-content *,
  .sheet.filling .sheet-content > * { animation: none; }
}

/* Pour combien de personnes.

   Une rangée, pas un champ de formulaire : c'est un réglage qu'on
   manipule en lisant — on est six ce soir, on appuie deux fois sur « + »
   et les quantités suivent. Il se pose donc juste au-dessus des
   ingrédients, à portée de pouce, et la phrase d'explication passe
   dessous plutôt que de couper la rangée en deux. */
.serves-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 12px;
  padding: 10px 12px;
  border-radius: var(--r-3);
  background: var(--papier);
  box-shadow: inset 0 0 0 1px rgba(43, 38, 34, .07);
}
.serves-lbl {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-weight: 700;
  font-size: 14px;
}
.serves-lbl .ico { width: 19px; height: 19px; color: var(--ink-soft); }
/* Le compteur de convives est posé sur la rangée crème : le plateau
   blanc qui le doublait dessinait un deuxième bloc dans le premier, et
   les trois pièces qu'il portait ont déjà leur fond blanc. Elles font,
   là aussi, exactement la même hauteur. */
.serves-step { background: transparent; }
.serves-step .step-val {
  width: 46px; height: 40px;
  text-align: center; font-weight: 700;
  border-radius: var(--r-2);
}
.serves-step button { width: 40px; height: 40px; border-radius: var(--r-2); }
.serves-unit { font-weight: 700; font-size: 14px; color: var(--ink-soft); }
.serves-hint {
  flex: 1 0 100%;
  color: var(--ink-soft);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
}

/* Changer l'illustration d'une fiche : ce qu'on remplace, en grand.

   La vignette d'aujourd'hui est montrée au-dessus des deux chemins —
   sans elle, on ouvre cet écran sans savoir laquelle des trois fiches
   ouvertes on est en train de modifier. */
.img-pick-now {
  display: grid;
  justify-items: center;
  gap: 8px;
  margin: 0 0 14px;
}
.img-pick-now img {
  width: 132px;
  height: 132px;
  border-radius: var(--r-3);
  object-fit: cover;
  background: var(--paper-img, var(--papier));
  box-shadow: 0 6px 18px rgba(43, 38, 34, .12);
}
.img-pick-now img.packed { object-fit: contain; }
.img-pick-now .hint { margin: 0; text-align: center; }

/* Les critères de génération du semainier.

   Le même encart que les filtres de recettes — mêmes pastilles, mêmes
   intitulés en petites capitales — parce que c'est le même geste : on
   dit ce qu'on cherche.

   IL VIT MAINTENANT DANS UNE FEUILLE. Il se dépliait dans l'entête du
   semainier, sous la date : six rangées de pastilles poussaient la
   semaine vers le bas, on réglait sans plus voir ce qu'on allait
   remplir, et sur téléphone le bouton « Proposer » tombait hors de
   l'écran. La feuille monte par-dessus et se referme d'un glissé ; le
   cadre et l'ombre de l'encart n'ont donc plus lieu d'être — la feuille
   les porte déjà. */
.gen-block {
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
}
/* La croix de la feuille occupe le coin haut droit : sans cette réserve,
   « Valeurs d'origine » passait dessous. */
.gen-block .filter-title { padding-right: 44px; }
.sheet-page .gen-block > .filter-title:first-child {
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin-bottom: var(--sp-3);
}
/* Dans la feuille de la roue crantée, le nom de la feuille est le seul
   grand titre : « Proposer une semaine » y redevient une étiquette de
   section comme sa voisine (voir `weekSettingsPanel`). */
.week-settings-h {
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.02em;
  margin: 0 0 var(--sp-4);
  padding-right: 44px;
}
.sheet-page .week-settings .gen-block > .filter-title:first-child {
  font-family: var(--f-texte);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: normal;
  color: var(--ink-soft);
  margin-bottom: 8px;
}
.gen-block .filter-title span { display: inline-flex; align-items: center; gap: 6px; }
.gen-block .filter-title .ico { width: 17px; height: 17px; }
.gen-block .btn-primary { margin-top: 4px; }
.gen-block .filter-hint { margin-bottom: 10px; }

@media (min-width: 900px) {
  /* Sur grand écran, les rangées de critères tiennent côte à côte :
     empilées, la feuille était plus haute que l'écran alors qu'il lui
     reste un mètre de large. */
  .gen-block {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 22px;
  }
  .gen-block .filter-title,
  .gen-block .filter-hint,
  .gen-block .pref-pair,
  .gen-block .wide-row,
  .gen-block .btn-primary { grid-column: 1 / -1; }
  /* Les deux listes d'ingrédients forment leur propre paire, et se
     partagent la largeur elles-mêmes : dans le flot commun, elles se
     retrouvaient l'une au-dessus de l'autre dès qu'un nombre impair de
     rangées les précédait — et ce nombre change selon qu'il y a des
     essais en attente et une clé d'IA. Un blanc à droite de la rangée
     d'avant vaut mieux qu'une mise en page qui se réarrange toute seule. */
  .gen-block .pref-pair {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 22px;
  }
  .gen-block .pref-pair .filter-row { margin-bottom: 10px; }
  /* La répartition des plats tient la largeur : c'est une liste de
     personnes, pas une rangée de pastilles, et elle laissait la moitié
     droite de l'encart vide. */
  .gen-block .filter-row:has(.quota-list) { grid-column: 1 / -1; }
  .gen-block .quota-list { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
  .gen-block .filter-row + .filter-row { margin-top: 0; }
  .gen-block .filter-row { margin-bottom: 10px; }
  .gen-block .btn-primary { max-width: 320px; }
}

/* Une liste filtrée ressemble à une liste vide : sans ce compteur sur
   l'entonnoir, on cherchait pourquoi la moitié des recettes manquait —
   alors qu'un filtre posé la veille était simplement resté. */
.filter-btn {
  position: relative;
  width: var(--search-h);
  height: var(--search-h);
}
.filter-dot {
  position: absolute;
  top: -4px; right: -4px;
  min-width: 19px; height: 19px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--accent-appui);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 19px;
  text-align: center;
  border: 2px solid var(--papier);
}

/* ============================================================
   Semainier : naviguer dans l'année
   ============================================================ */

.week-nav {
  display: grid;
  grid-template-columns: 44px 1fr 44px;
  align-items: center;
  gap: 8px;
  padding: 8px 0 2px;
}
.week-arrow {
  width: 44px; height: 44px;
  border-radius: var(--r-2);
  background: var(--surface);
  border: 1.5px solid var(--line);
  display: grid; place-items: center;
  color: var(--ink);
}
.week-arrow .ico { width: 20px; height: 20px; }
.week-arrow[disabled] { opacity: .35; }
.week-when { text-align: center; min-width: 0; }
.week-when strong {
  display: block;
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 19px;
  letter-spacing: -.02em;
}
.week-when small {
  display: block;
  color: var(--ink-soft);
  font-weight: 600;
  font-size: 12px;
  white-space: normal;
  overflow-wrap: anywhere;
  line-height: 1.35;
  padding: 0 4px;
}
.week-sub {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0 .35em;
}
.week-quand::after { content: "·"; margin-left: .35em; }
@media (max-width: 899px) {
  .week-sub { display: block; }
  .week-quand { display: block; }
  .week-quand::after { content: none; margin: 0; }
  .week-plats { display: block; }
}
/* Le retour au présent, dans la rangée du titre. Une flèche et un verbe :
   sans eux, « Cette semaine » se lisait comme le titre de la semaine
   affichée, pas comme le bouton qui y ramène. */
.week-today {
  min-height: 38px;
  color: var(--accent-fonce);
  border-color: var(--accent-pale);
  display: inline-flex;
  align-items: center;
  gap: 3px;
  white-space: nowrap;
}
.week-today .ico { width: 15px; height: 15px; flex: 0 0 auto; }

/* SUR TÉLÉPHONE, LE TITRE PREND SA LIGNE.

   La rangée du semainier porte jusqu'à quatre pièces : « Semainier », le
   retour à aujourd'hui, le « ✨ » et la coche de validation. Additionnées,
   elles font plus de 390 px, et c'est la coche — le bouton qui clôt la
   semaine — qui sortait de l'écran.

   Le titre passe donc seul sur la première ligne et les trois boutons se
   rangent en dessous, alignés à droite. Rien ne rétrécit, rien ne se
   coupe : c'est la seule mise en page qui tienne quel que soit le nombre
   de boutons affichés. */
/* Le droit de passer à la ligne est donné SANS CONDITION : c'est le
   filet, et il tient quelle que soit la longueur des mots. */
.week-top { flex-wrap: wrap; row-gap: 8px; }
/* LES TROIS BOUTONS RESTENT À CÔTÉ DU TITRE.

   Le filet précédent coupait la rangée SANS CONDITION sous 520 px : le
   titre prenait sa ligne, et « ✨ » et la coche tombaient dessous — y
   compris sur la semaine en cours, où le retour à aujourd'hui ne
   s'affiche pas et où les trois pièces tenaient très largement. On
   perdait une rangée d'écran, et surtout le haut de l'écran ne disait
   plus d'un coup d'œil ce qu'on pouvait faire de la semaine.

   Ce qui déborde n'est pas la rangée, c'est un intitulé : « Retour
   aujourd'hui » demande 169 px à lui seul. Il se dit donc plus court
   quand la place manque — le pictogramme porte déjà le « retour », le mot
   qui reste est celui qui compte — et le titre descend d'un cran. Les
   quatre pièces tiennent alors sur 390 px, et le filet ne sert plus que
   d'ultime recours. */
.wt-short { display: none; }
@media (max-width: 560px) {
  .week-top .wt-long { display: none; }
  .week-top .wt-short { display: inline; }
  .week-top .week-today { padding: 0 10px; font-size: 13px; }
  .week-top:has(.week-today) .brand { font-size: 24px; }
}
/* Un cran de plus sous 400 px, où les quatre pièces jouent au pixel près.
   Le titre ne rétrécit QUE lorsque le retour à aujourd'hui est là :
   sur la semaine en cours — c'est-à-dire neuf fois sur dix — il garde
   ses vingt-huit pixels, et les deux boutons tiennent à côté sans peine. */
@media (max-width: 400px) {
  .week-top:has(.week-today) .brand { font-size: 22px; }
}
.icon-btn.off { opacity: .4; }
.icon-btn .ico { width: 22px; height: 22px; }
/* Valider la semaine est le geste qui clôt la préparation : la coche est
   verte, comme tout ce qui dit « c'est bon » dans l'app. Nue, sans le
   cercle qui la faisait ressembler au « ⓘ » des cases du garde-manger. */
#btn-week-lock { color: var(--sage-deep); border-color: var(--bien-filet); background: var(--bien-voile); }
#btn-week-lock .ico { width: 23px; height: 23px; stroke-width: 2.6; }

/* ============================================================
   Le bilan d'une semaine suivie
   ============================================================ */

/* Il se lit avant les jours, juste sous la date : c'est le résumé de ce
   qui suit. Un fond plus soutenu que le reste de l'écran — la page est
   déjà crème, un encart crème clair ne se détachait pas — et une seule
   ligne visible : on vient d'abord voir les jours, la note moyenne suffit
   à savoir si l'on a envie d'en lire plus. */
/* Le bilan porte le costume des autres encarts de l'app — celui de la
   note santé d'une fiche (`.health-card`), qui dit la même chose au
   singulier : même trait, même arrondi, mêmes teintes, et PAS D'OMBRE.
   Il avait la sienne, plus épaisse et posée sur un liseré de 4 px : au
   milieu d'un écran où rien d'autre ne décolle, c'est le seul bloc qui
   flottait. */
.week-sum {
  margin: 6px 14px 12px;
  border-radius: var(--r-3);
  border: 1px solid var(--line);
  background: var(--surface);
  overflow: hidden;
}
.week-sum.t-good { background: var(--bien-voile); border-color: var(--bien-filet); }
.week-sum.t-mid { background: var(--moyen-voile); border-color: var(--moyen-filet); }
.week-sum.t-soft { background: var(--bof-voile); border-color: var(--bof-filet); }
/* La rangée du haut : la note, ce qu'elle vaut, la courbe, le chevron.

   Le chevron est le dernier élément, collé au bord droit — c'est là
   qu'on le cherche pour déplier quoi que ce soit. La courbe se glisse
   juste avant, SANS cadre ni pastille blanche : elle en avait une, et
   sur un encart déjà coloré elle passait pour l'action principale alors
   qu'on l'ouvre une fois par mois. */
.week-sum-head { display: flex; align-items: center; gap: 4px; padding: 10px 12px; }
.week-sum-open {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: 44px 1fr;
  gap: 12px;
  align-items: center;
  text-align: left;
}
.week-sum-fold {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 1fr;
  width: 34px;
  height: 40px;
  place-items: center;
}
.week-sum-txt { min-width: 0; }
.week-sum-txt strong { display: block; font-size: 16px; }
.week-sum-txt small { display: block; color: var(--ink-soft); font-weight: 600; font-size: 12px; }
.week-sum-chev {
  display: grid; place-items: center;
  color: var(--ink-soft);
  transition: transform .18s ease;
}
.week-sum-chev .ico { width: 20px; height: 20px; }
.week-sum.open .week-sum-chev { transform: rotate(180deg); }
.week-sum-chart {
  width: 38px; height: 38px;
  flex: 0 0 auto;
  border-radius: var(--r-2);
  background: none;
  border: 0;
  display: grid; place-items: center;
  color: var(--ink-soft);
}
.week-sum-chart:active { background: rgba(43, 38, 34, .07); }
.week-sum-chart .ico { width: 20px; height: 20px; }

/* Le détail du bilan : QUATRE LIGNES QUI S'ALIGNENT.

   Elles étaient posées en `space-between` — libellé à gauche, pastille à
   droite, et le chiffre quelque part au milieu, à un endroit différent
   sur chacune. On lisait quatre lignes bancales là où on voulait comparer
   quatre mesures.

   Trois colonnes fixes, donc : le libellé prend ce qui reste, les
   chiffres se calent à droite de leur colonne, les pastilles ouvrent la
   leur. Le « par repas » se glisse sous le libellé sans décaler le reste,
   et les chiffres sont en chasse fixe pour que les unités tombent les
   unes sous les autres. */
.week-sum-detail { padding: 2px 10px 10px; }
.week-sum-rows {
  border-radius: var(--r-3);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px rgba(43, 38, 34, .08);
  overflow: hidden;
}
.sum-row {
  display: grid;
  grid-template-columns: 1fr auto 72px;
  grid-template-areas:
    "label value level"
    "hint  value level";
  align-items: center;
  column-gap: 12px;
  padding: 9px 12px;
  font-size: 14px;
}
.sum-row + .sum-row { box-shadow: inset 0 1px 0 rgba(43, 38, 34, .08); }
.sum-label { grid-area: label; font-weight: 700; align-self: end; }
.sum-hint {
  grid-area: hint;
  align-self: start;
  font-size: 11px;
  font-weight: 600;
  color: var(--ink-soft);
}
.sum-val {
  grid-area: value;
  justify-self: end;
  display: flex; align-items: baseline; gap: 4px;
  font-size: 17px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.sum-val small { font-size: 11px; font-weight: 700; color: var(--ink-soft); }
.sum-row .nlevel { grid-area: level; justify-self: start; margin: 0; }
.week-sum-foot { margin: 8px 4px 0; font-size: 12px; color: var(--ink-soft); font-weight: 600; }

/* ============================================================
   « peu », « moyen », « beaucoup »
   ============================================================ */

/* Le mot dit la quantité, la couleur dit ce qu'on en pense — et les deux
   ne vont pas toujours ensemble : peu de sucre est une bonne nouvelle,
   peu de protéines non. Voir health.js, `level`. */
.nlevel {
  display: inline-block;
  margin-left: 8px;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .02em;
  color: #fff;
  vertical-align: 1px;
}
.nlevel.t-good { background: var(--note-bien); }
.nlevel.t-mid { background: var(--note-moyen); }
.nlevel.t-soft { background: var(--note-bof); }

/* ============================================================
   Les jours passés : présents, mais qui ne réclament rien
   ============================================================ */

/* SEMAINE COURANTE — un seul bloc pour tout ce qui est passé.

   Chaque jour avait son propre repli : un jeudi, c'étaient trois blocs
   l'un sous l'autre à franchir avant d'arriver à aujourd'hui, et trois
   gestes pour relire la semaine. Ils tiennent maintenant dans un unique
   bandeau replié, en tête de liste, qui dit ce qu'on y a mangé. */
.days-past {
  background: transparent;
  border: 1px dashed var(--line-fort);
  border-radius: var(--r-3);
  margin-bottom: 10px;
  padding: 0;
}
.days-past[open] { background: var(--surface); border-style: solid; padding: 0 10px 8px; }
.days-past > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 46px;
  padding: 0 12px;
  cursor: pointer;
  list-style: none;
  font-size: 14px;
}
.days-past[open] > summary { padding: 0 2px; }
.days-past > summary::-webkit-details-marker { display: none; }
.days-past-name { font-weight: 700; flex: 0 0 auto; color: var(--ink-soft); }
.days-past-plats {
  flex: 1;
  min-width: 0;
  color: var(--ink-soft);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.days-past-chev { display: grid; place-items: center; color: var(--ink-mute); transition: transform .18s ease; }
.days-past-chev .ico { width: 18px; height: 18px; }
.days-past[open] .days-past-chev { transform: rotate(180deg); }
.days-past-body { display: flex; flex-direction: column; gap: 8px; padding-top: 6px; }
.days-past-body .day-card { margin-bottom: 0; }
/* Déplié sur grand écran, le bloc rend ses jours en grille comme les
   autres : empilés en colonne, quatre cartes pleine largeur feraient
   trois écrans là où la semaine entière en tient un. */
@media (min-width: 900px) {
  .days-past-body {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 14px;
    align-items: start;
    padding-top: 10px;
  }
}

/* SEMAINE PASSÉE — les jours restent ouverts, simplement fanés.

   Là, tout est derrière nous : replier n'épargne rien, il faudrait tout
   rouvrir pour lire quoi que ce soit. On grise donc les cellules, et on
   les laisse tranquilles. */
.day-faded {
  background: var(--surface);
  border-color: var(--line);
  color: var(--ink-soft);
}
.day-faded .day-name span { color: var(--ink-soft); }
.day-faded .slot-rec img { filter: saturate(.55); }
.day-faded .item-name { color: var(--ink-soft); font-weight: 700; }
.day-faded .slot { opacity: .82; }

/* ============================================================
   Les accompagnements, sous le plat qui les appelle
   ============================================================ */

/* Ils sont posés SOUS le plat, en retrait, et visiblement plus petits :
   c'est ce qui dit qu'ils lui appartiennent et qu'ils ne sont pas deux
   plats de plus. Le trait vertical à gauche fait le lien. */
.side-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 0 2px 10px;
  padding-left: 10px;
  border-left: 2px solid var(--line);
}
.side-add {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 34px;
  padding: 0 12px;
  border-radius: 999px;
  border: 1.5px dashed var(--accent-pale);
  background: var(--accent-voile);
  color: var(--ink-soft);
  font-size: 13px;
  font-weight: 700;
}
.side-add:active { background: var(--papier-2); color: var(--accent-fonce); }
.side-chip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  min-height: 34px;
  padding: 3px 4px 3px 3px;
  border-radius: 999px;
  border: 1.5px solid var(--line);
  background: var(--champ);
  max-width: 100%;
}
.side-open {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  text-align: left;
  padding: 0 2px 0 0;
}
.side-open img {
  width: 28px; height: 28px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--paper-img);
  flex: 0 0 auto;
}
.side-txt { min-width: 0; line-height: 1.05; }
.side-txt small {
  display: block;
  font-size: 11px;
  font-weight: 700;
  color: var(--ink-soft);
}
.side-txt strong {
  display: block;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.side-x {
  flex: 0 0 auto;
  width: 26px; height: 26px;
  border-radius: 50%;
  display: grid; place-items: center;
  color: var(--ink-soft);
  font-size: 13px;
}
.side-x:active { background: var(--papier); }

/* Le réglage qui dépend du type de plat : la nature d'un accompagnement,
   ou « ce plat en demande un ». Il vit sous les pastilles de type, décalé,
   pour qu'on voie qu'il en découle. */
.sub-choice {
  margin: 10px 0 0;
  padding: 10px 0 0;
  border-top: 1px solid var(--line);
}
.sub-choice > span {
  display: block;
  margin-bottom: 6px;
  font-size: 13px;
  font-weight: 700;
  color: var(--ink-soft);
}

/* ============================================================
   La poignée de déplacement, à droite
   ============================================================ */

/* Elle est passée du côté du pouce, et c'est elle seule qui saisit le
   plat. `touch-action: none` est l'essentiel : il prévient le navigateur,
   AVANT le premier millimètre, que ce geste-là ne lui appartient pas.
   Sans lui, il décidait que c'était un défilement et coupait le
   déplacement en plein vol — c'est la raison pour laquelle ça ne marchait
   jamais sur téléphone. */
.slot-filled[data-drag-slot] {
  grid-template-columns: 1fr auto;
  align-items: center;
  cursor: default;
  touch-action: auto;          /* la page redevient défilable partout ailleurs */
  -webkit-user-select: auto;
  user-select: auto;
}
/* Une poignée, pas un bouton. Encadrée et remplie, elle avait le poids
   d'une action — alors qu'elle ne fait rien tant qu'on ne la tire pas.
   Il ne reste que les points, assez sombres pour se voir sur le blanc de
   la carte. La cible du doigt, elle, garde ses 44 px. */
.slot-grip {
  width: 44px;
  height: 44px;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  color: var(--ink-mute);
  font-size: 19px;
  line-height: 1;
  letter-spacing: -2px;
  padding: 0;
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
.slot-grip:active { cursor: grabbing; color: var(--accent-fonce); }

/* ----- LA COLONNE DE DROITE D'UN CRÉNEAU EN SUIVI -----

   Deux choses s'y empilent : la tête de qui cuisine, dans la rangée
   « Midi / Soir », et la poignée de déplacement sur la ligne du plat.
   Elles étaient toutes deux collées au bord droit — ce qui suffit quand
   elles ont la même largeur, et ne suffit plus dès qu'elles diffèrent :
   la tête d'une personne fait 28 px, la gélule « tout le monde » une
   quarantaine, et la poignée garde ses 44 px de cible tactile. Trois
   largeurs, trois centres, une colonne qui zigzague.

   Toutes deux vont donc AU BORD DROIT, et c'est le DESSIN qui s'y cale,
   pas la boîte : la poignée garde ses 44 px de cible tactile — on la
   saisit au pouce — mais ses points se rangent contre le bord, à l'aplomb
   de la tête. Le `-2px` d'espacement du caractère laissait par-dessus le
   marché un blanc à sa droite ; il est rendu.

   Seulement en suivi : en préparation, la rangée du dessous porte deux
   boutons pleins et n'a rien à empiler. */
.slot-drag .slot-grip {
  justify-items: end;
  /* Trois pixels, et ils ne sont pas arbitraires.

     Mesuré, le bord droit des points tombait à un pixel de celui de la
     tête : aligné, donc. Il ne le PARAÎT pas — la tête est un disque, et
     un disque bordé se lit comme rentrant là où une grille de points, qui
     est un rectangle plein, se lit comme sortant. L'œil demande à la
     forme droite de reculer un peu pour que les deux soient sur le même
     axe. C'est le même correctif optique qu'on applique à un rond posé
     dans une colonne de carrés. */
  padding-right: 3px;
}

/* ============================================================
   Le temps que ça calcule
   ============================================================ */

/* Un encart qui disparaît puis revient trois secondes plus tard donne
   l'impression que rien ne se passe. On garde la place, à la bonne
   hauteur, avec le battement qui dit « ça travaille ». */
.skel-card {
  margin: 0 0 14px;
  padding: 12px;
  border-radius: var(--r-3);
  border: 1.5px solid var(--line);
  background: var(--surface);
}
.skel-head { display: grid; grid-template-columns: 44px 1fr; gap: 12px; align-items: center; }
.skel-lines { display: block; }
.skel-badge, .skel-line {
  display: block;
  border-radius: var(--r-1);
  background: linear-gradient(90deg, var(--papier-2) 25%, var(--papier) 50%, var(--papier-2) 75%);
  background-size: 240% 100%;
  animation: skel 1.3s ease-in-out infinite;
}
.skel-badge { width: 44px; height: 44px; border-radius: var(--r-2); }
.skel-line { height: 11px; margin-bottom: 7px; }
.skel-line.w40 { width: 40%; }
.skel-line.w50 { width: 50%; }
.skel-line.w60 { width: 60%; }
.skel-line.w70 { width: 70%; }
.skel-line.w90 { width: 90%; }
.skel-say { margin: 10px 0 0; font-size: 13px; font-weight: 600; color: var(--ink-soft); }

@keyframes skel {
  0% { background-position: 140% 0; }
  100% { background-position: -40% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .skel-badge, .skel-line { animation: none; }
}

/* ============================================================
   Les propositions de l'IA, une fiche à la fois
   ============================================================ */

.propo { padding: 0; }
.propo-card {
  background: var(--champ);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  padding: 12px 8px;
}
.propo-actions {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 8px;
}
.propo-actions .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
.propo-actions .ico { width: 18px; height: 18px; }
.propo-ings li { align-items: center; }
.propo-prefs {
  margin: 0 0 12px;
  padding: 10px 12px;
  background: var(--champ);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
}
.propo-prefs p {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  line-height: 1.4;
}
.propo-prefs p + p { margin-top: 6px; }
.propo-prefs strong {
  display: inline;
  color: var(--ink-soft);
  font-size: 13px;
  margin-right: 6px;
}
.propo-ing-img {
  width: 32px; height: 32px;
  border-radius: var(--r-2);
  object-fit: cover;
  background: var(--paper-img);
  flex: 0 0 auto;
}
.propo-ban {
  flex: 0 0 auto;
  width: 28px; height: 28px;
  border-radius: var(--r-1);
  font-weight: 700;
  color: var(--danger);
}
.invent-criteres { margin: 0 0 12px; }

.filter-row.pref-row .filter-line {
  display: block;
}
.pref-ings {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
}
.pref-ing {
  display: grid;
  grid-template-columns: 1fr 36px;
  gap: 6px;
  align-items: center;
  /* Une hauteur de ligne, tenue par toutes : celle d'une vignette de 40 px
     dans ses huit pixels de rembourrage, plus l'interligne du nom. La
     ligne d'ajout s'y range comme les autres, et deux colonnes voisines
     finissent au même endroit. */
  min-height: 60px;
  background: var(--champ);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  padding: 8px;
}
.pref-ing-add { grid-template-columns: 40px 1fr 36px; }
.pref-ing-main {
  display: grid;
  grid-template-columns: 40px 1fr;
  gap: 6px;
  align-items: center;
  min-width: 0;
  text-align: left;
}
.pref-ing-ico {
  width: 40px; height: 40px;
  border-radius: var(--r-2);
  overflow: hidden;
  background: var(--paper-img);
  display: grid; place-items: center;
}
.pref-ing-ico .prod-ico,
.pref-ing-ico img {
  width: 40px; height: 40px;
  border-radius: var(--r-2);
  object-fit: cover;
}
.pref-ing-name {
  min-width: 0;
  font-weight: 700;
  font-size: 15px;
  overflow-wrap: anywhere;
}
.pref-ing-add .ing-name-row { min-width: 0; }
/* LA LIGNE D'AJOUT FAIT LA MÊME HAUTEUR QUE LES AUTRES.

   Le champ héritait du gabarit des formulaires — `.sheet input`, et ses
   quatorze pixels de rembourrage haut et bas — et montait à 51 px là où
   une ligne d'ingrédient en fait 40 : la case d'ajout dépassait de dix
   pixels ses voisines, et les deux colonnes « à retrouver » et « à
   éviter » ne finissaient jamais au même endroit.

   La hauteur est celle de la vignette d'à côté, qui est ce qui donne sa
   hauteur à une ligne d'ingrédient. Les deux valeurs vont ensemble. */
.pref-ing-add input {
  width: 100%;
  height: 40px;
  min-height: 40px;
  padding: 0 10px;
}
/* LE BLANC TOMBE AU BAS DE LA COLONNE, PAS AU MILIEU.

   Les deux colonnes ont été tirées un temps à la même hauteur, la ligne
   d'ajout collée au bas de la plus courte. C'était une mauvaise lecture
   de l'alignement : « à retrouver » avec deux ingrédients et « à
   éviter » avec un seul creusaient alors un trou entre le dernier
   ingrédient et le champ d'ajout — un vide à l'intérieur d'une liste, là
   où l'œil attend la suite.

   Les lignes s'empilent donc à partir du haut, toutes de la même
   hauteur : le champ d'ajout d'une colonne se trouve exactement en face
   d'un ingrédient de l'autre, et ce qui reste de place s'accumule
   là où il ne gêne pas — sous la colonne la plus courte. */
.skel-recipe { min-height: 420px; padding: 16px 12px 24px; }
.skel-ings {
  display: grid;
  grid-template-columns: 36px 1fr;
  gap: 8px 10px;
  align-items: center;
  margin: 16px 0;
}
.skel-badge.sm { width: 36px; height: 36px; }
.propo-head { display: flex; align-items: center; gap: 12px; }
.propo-txt { min-width: 0; }
.propo-txt strong {
  display: block;
  font-family: var(--f-titre); font-stretch: 88%;
  font-size: 21px; line-height: 1.15;
  overflow-wrap: break-word;
  -webkit-hyphens: auto; hyphens: auto;
}
.propo-txt small { display: block; margin-top: 2px; color: var(--ink-soft); font-weight: 600; font-size: 13px; }
.propo-lead { margin: 10px 0 0; color: var(--ink-soft); font-weight: 500; line-height: 1.5; }
.propo-card h4 { margin: 16px 0 8px; }
.propo-ings {
  list-style: none; margin: 0; padding: 0;
  width: 100%;
}
.propo-ings li {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 2px;
  border-top: 1px solid var(--line);
  font-size: 14px;
}
.propo-ings li:first-child { border-top: 0; }
.propo-ing-name {
  flex: 1;
  min-width: 0;
  text-align: left;
  font-weight: 600;
}
.propo-ing-qty {
  flex: 0 0 auto;
  text-align: left;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
  font-weight: 700;
}
.propo-ings b { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--ink-soft); }

.propo .skel-card { margin: 0; }

/* ============================================================
   Les échanges d'ingrédients
   ============================================================ */

/* ===== LES PROPOSITIONS D'ALLÈGEMENT, DANS LA CARTE DE LA NOTE =====

   Elles avaient leur carte : papier blanc, liseré terracotta, ombre
   portée. Deux cadres l'un sous l'autre pour un seul sujet — et un
   ombrage que rien d'autre dans l'app ne porte. Elles vivent maintenant
   dans le pied de la carte de la note, sous son filet, et n'apportent
   aucune bordure.

   UNE GRAISSE FORTE PAR LIGNE. Il y avait ici quatre graisses, trois
   tailles et une pastille en capitales pour dire trois choses. Ce qui
   reste suit l'échelle du reste des cartes (voir « le texte des
   cartes », plus bas) : la proposition en gras, la raison en encre
   douce. */
.health-foot .swap-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 16px 0 0;
}
/* SEIZE PIXELS DE PART ET D'AUTRE DU FILET.

   Il n'en avait que dix en dessous et zéro au-dessus : la proposition
   précédente venait toucher le trait, et les lignes se lisaient comme un
   tableau serré plutôt que comme des propositions distinctes. La marge
   est POSÉE AU-DESSUS du filet (elle est hors bordure), le rembourrage
   en dessous — c'est ce qui laisse la dernière ligne finir au ras du
   pied de carte, sans blanc en trop. */
.health-foot .swap-row + .swap-row {
  border-top: 1px solid rgba(43, 38, 34, .08);
  margin-top: 16px;
}
.swap-txt { flex: 1 1 auto; min-width: 0; }
.swap-arrow { color: var(--accent-fonce); }
/* Le bouton d'une proposition est un bouton comme les autres : même
   hauteur, même rayon. Il était rogné à 40 px, seul de son espèce. */
.health-foot .swap-do {
  flex: 0 0 auto;
  width: auto;
  margin: 0;
  padding: 0 16px;
  white-space: nowrap;
  background: color-mix(in srgb, var(--champ) 75%, transparent);
}
.health-foot .swap-do:active { background: var(--champ); }

/* L'ATTENTE : la phrase AU-DESSUS du chatoiement.

   Elle était écrite dedans, sur un dégradé qui bouge — donc illisible au
   moment précis où elle est la seule chose à lire. */
.swap-wait { padding-top: 4px; }
/* LE CHATOIEMENT S'ÉTAIT ARRÊTÉ ICI, ET ON VOYAIT POURQUOI SANS LE VOIR :
   deux barres immobiles au lieu d'un balayage.

   La carte de la note est sur fond miel, où le dégradé crème des
   chatoiements ordinaires ne se voit pas. On avait donc reposé un
   `background` plus sombre — mais `background` est un raccourci, et il
   remplaçait le dégradé par un aplat. L'animation continuait de déplacer
   la position d'un fond qui n'avait plus rien à déplacer.

   On ne redéfinit donc que les COULEURS du dégradé, en gardant sa forme
   et sa taille : le balayage reprend, en plus sombre. */
.swap-wait .skel-line {
  background-image: linear-gradient(90deg,
    rgba(43, 38, 34, .11) 25%, rgba(43, 38, 34, .03) 50%, rgba(43, 38, 34, .11) 75%);
}

/* Sur une rangée étroite, le bouton passe sous la phrase : c'est le sens
   de lecture — d'abord ce qu'on propose, ensuite ce qu'on en fait. */
@container swaps (max-width: 430px) {
  .health-foot .swap-row { display: block; }
  .health-foot .swap-do { width: 100%; margin-top: 10px; }
}
.health-foot { container: swaps / inline-size; }

/* Ce qui vient d'être remplacé se signale dans la liste des ingrédients :
   sans ça on redescend chercher lequel a changé, sur une fiche de quinze
   lignes toutes pareilles.

   Les deux mentions — « remplace X » et « ＋ au garde-manger » — tiennent
   sur la même ligne, en pastilles. Empilées, l'une posée sur un bouton
   pleine largeur, elles faisaient trois étages sous un simple nom. */
/* Côte à côte, jamais l'une sous l'autre : ce sont deux réponses à la
   même question, et une réponse posée seule sur sa ligne passe pour
   autre chose. La rangée a maintenant la largeur qu'il leur faut. */
.ing-tags {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  margin: 2px 0 0;
}
.ing-tag {
  display: inline-flex;
  align-items: center;
  min-height: var(--ing-btn, 36px);
  justify-content: center;
  flex: 1 1 auto;
  min-width: 0;
  padding: 0 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
}
/* Le retour en arrière au bout de la rangée : carré comme la croix qu'il
   a quittée, haut comme les pastilles qu'il rejoint. Il ne s'étire pas —
   c'est un pictogramme, pas une phrase. */
.ing-tag.undo {
  flex: 0 0 auto;
  margin-left: auto;
  width: var(--ing-btn, 36px);
  padding: 0;
  background: var(--surface);
  border: 1.5px solid var(--accent-pale);
  color: var(--accent-fonce);
  border-radius: var(--r-2);
}
.ing-tag.undo .ico { width: 19px; height: 19px; }
.ing-tag.undo:active { background: var(--accent); border-color: var(--accent); color: #fff; }

.ing-tag.add {
  background: var(--accent-voile);
  border: 1.5px dashed var(--accent-pale);
  color: var(--accent-fonce);
}
.ing-tag.add:active {
  background: var(--accent);
  border-color: var(--accent);
  border-style: solid;
  color: #fff;
}
/* « Déjà un article » est le voisin de « ＋ au garde-manger », et il ne
   doit pas se confondre avec lui : le premier CRÉE, le second RELIE.

   PAS EN VERT. La sauge est la couleur du « c'est bon » dans cette app —
   la note santé, l'état de synchro, le « ✓ » d'un article déjà sur la
   liste. Posée sur un bouton, elle promettait un état atteint là où il
   n'y a qu'une action offerte. L'ardoise ne dit rien d'autre que
   « appuie ici » : un trait plein, une encre sobre, à côté du pointillé
   d'action de ce qui reste à faire. */
.ing-tag.same {
  background: var(--meme-voile);
  border: 1.5px solid var(--meme-filet);
  color: var(--meme-ink);
}
.ing-tag.same:active {
  background: var(--meme-ink);
  border-color: var(--meme-ink);
  color: #fff;
}
/* La carte entière dit qu'elle a changé : un liseré plus gras, dans la
   couleur de l'encart qui l'a proposé. */
.ing-card.swapped {
  border-width: 2px;
  border-color: var(--accent-pale);
  background: var(--accent-voile);
}

/* ============================================================
   L'évolution des semaines
   ============================================================ */

.chart-wrap { margin: 4px 0 8px; }
.chart { width: 100%; height: auto; display: block; }
.chart-grid { stroke: var(--line); stroke-width: 1; }
.chart-axis { fill: var(--ink-soft); font-size: 8px; font-weight: 600; }
.chart-area { fill: rgba(111, 162, 134, .16); }
.chart-line {
  fill: none;
  stroke: var(--sage-deep);
  stroke-width: 2.2;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.chart-dot { fill: var(--sage-deep); }
/* La semaine qu'on regardait en ouvrant la courbe : on doit pouvoir s'y
   repérer sans compter les points. */
.chart-dot.on { fill: var(--accent); stroke: #fff; stroke-width: 2; }
.chart-legend { margin: 6px 0 0; font-size: 13px; color: var(--ink-soft); font-weight: 600; text-align: center; }
.chart-legend b { color: var(--ink); }

/* ============================================================
   Détail de la note : un tableau plus large que l'écran
   ============================================================ */

/* Six colonnes ne tiennent pas sur un téléphone. Plutôt que d'en retirer,
   le tableau défile pour son propre compte — et `data-inner-scroll` dit à
   la feuille glissante de ne pas lui voler le geste. */
.hd-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  margin: 0 -2px;
}
/* Six colonnes sur 350 px : chacune doit se serrer. La note est la
   dernière, donc la première à sortir de l'écran — et c'est celle qu'on
   vient lire. On rétrécit le texte et on laisse le nom de l'ingrédient
   passer à la ligne : le tableau tient alors sans défiler sur la plupart
   des téléphones, et défile sur les plus étroits. */
.hd-scroll .hd-table { min-width: 0; font-size: 12px; }
.hd-scroll .hd-table th { font-size: 10px; padding-left: 3px; }
.hd-scroll .hd-table td { padding-left: 3px; }
.hd-scroll .hd-table td:first-child { padding-left: 0; white-space: normal; }
.hd-scroll .hd-note { min-width: 22px; }

/* ============================================================
   Propositions de saisie : elles doivent défiler
   ============================================================ */

/* Cinq propositions dépassaient du bas de la feuille, et le premier
   mouvement du doigt emportait la feuille au lieu de la liste. Elle a
   donc sa propre hauteur et son propre défilement ; `data-inner-scroll`
   (shell.js) fait le reste. */
.ac {
  max-height: min(46vh, 320px);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  touch-action: pan-y;
}

/* ============================================================
   Une case de plus sur la ligne : l'étoile
   ============================================================ */

/* La ligne d'un article ou d'une recette tenait en trois cases — vignette,
   texte, « + ». L'étoile en fait une quatrième, et sans le dire à la
   grille, elle passait à la ligne en emportant le « + » avec elle.

   Elle est plus étroite que le « + » : c'est un geste occasionnel posé à
   côté d'un geste quotidien, et la différence de taille le dit. */
.prod:has(.fav-btn) { grid-template-columns: 52px 1fr 44px 44px; gap: 6px; }
.prod.ai-suggest { grid-template-columns: 52px 1fr 44px; gap: 6px; }
/* LA LIGNE DE L'IA A SON FOND.

   Elle est en tête d'une liste de recettes qu'on POSSÈDE, et elle n'en
   est pas une : c'est une proposition, qui disparaît si on la passe. Rien
   ne la distinguait de sa voisine hormis l'étincelle de vingt pixels
   posée sur sa vignette — on l'ouvrait en croyant ouvrir une recette du
   garde-manger.

   Un jaune très pâle, et un liseré de la même famille : assez pour qu'on
   voie au premier coup d'œil que la première ligne n'est pas comme les
   autres, pas assez pour qu'elle se lise comme une alerte. L'étiquette,
   elle, dit le mot. */
.prod.ai-suggest {
  background: var(--ia-voile);
  border-color: var(--ia-filet);
}
/* UNE BANDE, COMME LES RAYONS. Un fond seul ne suffisait pas : trop pâle,
   la cellule se confondait avec le papier gris-bleu de l'écran ; trop
   franc, il criait plus fort que les recettes qu'on possède. La bande
   violette sur le bord gauche — la forme qui dit « bloc à part » dans la
   liste de courses — marque la proposition quel que soit le fond, et le
   voile peut rester doux. Les filets de la grille sont repris : cette
   ombre remplace celle des lignes du panneau. */
.catalog-list > .prod.ai-suggest {
  box-shadow: inset 5px 0 0 var(--ia-trait), 0 -1px 0 var(--line), -1px 0 0 var(--line);
  padding-left: 13px;
}
.prod.ai-suggest .ai-skip {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: var(--r-2);
  background: var(--surface);
  border: 1.5px solid var(--line);
  color: var(--ink-soft);
}
.prod.ai-suggest .ai-skip .ico { width: 20px; height: 20px; }
.prod.ai-suggest .ai-skip:active { background: var(--papier); transform: translateY(1px); }
.hero-cook.ia .ico { width: 20px; height: 20px; color: var(--accent-fonce); }
.prod.ai-skel .skel-badge.prod-ico {
  width: 52px; height: 52px; border-radius: var(--r-3);
}
.prod.ai-skel .skel-line { margin-bottom: 6px; }
.prod.ai-skel .skel-line:last-child { margin-bottom: 0; }
.prod.ai-skel .ai-skip-ph {
  width: 40px; height: 40px;
  border-radius: var(--r-2);
}

/* Sans ça, `1fr` refuse de descendre sous la largeur du plus long mot :
   « Salade saumon & avocat » élargissait sa ligne et elle seule, et la
   liste n'était plus alignée. */
.prod > button, .prod > div { min-width: 0; }
.prod .item-name { overflow-wrap: anywhere; }

/* ============================================================
   Le papier des illustrations
   ============================================================ */

/* Les illustrations livrées sont carrées, posées sur un crème de charte.
   Montrées en entier dans un cadre plus large que haut, elles laissent
   deux bandes de part et d'autre : ce sont ces bandes-là qu'on peint de
   la même couleur, sinon la jointure se voit.

   Trois autres pistes ont été essayées et écartées, elles valent d'être
   dites : le crème générique de l'app (trop rose — dix-sept points de
   bleu au-dessus, et deux traits verticaux bien nets) ; la colonne du
   bord étirée (couleur juste au raccord, mais un aplat là où
   l'illustration a un léger dégradé, et l'œil lisait une bande) ; le
   miroir de l'image (raccord parfait, mais il répétait le sujet — six
   poires au lieu de deux).

   La valeur ci-dessous est la moyenne des bords, relevée sur un
   échantillon des illustrations livrées : leur fond ne varie que de
   quelques points, un aplat suffit à condition d'être le bon. C'est la
   seule valeur à revoir si le crème de la charte change un jour. */
:root { --paper-img: #f8eddb; --paper-img-edge: #e3d1b3; }

/* Un cadre nettement plus large que haut donne des bandes larges : on
   relève un peu la hauteur là où il y a la place, pour que l'illustration
   reste le sujet. */
@media (min-width: 900px) {
  /* Seul le débordement change : l'air sous l'illustration, lui, ne
     dépend pas de la taille de l'écran (voir `--hero-gap`). */
  .sheet-hero { --hero-over: 44px; min-height: 108px; }
  .sheet-hero .hero-art { width: 132px; height: 132px; }
  .sheet-hero.with-cap { --hero-over: 32px; min-height: 120px; }
  .sheet-hero.with-cap.with-state { min-height: 148px; }
  .sheet-hero.with-cap .hero-art { width: 108px; height: 108px; }
  .sheet-hero .hero-cap strong { font-size: 24px; }
}

/* ===== LE MODE CUISINE =====

   Une étape à la fois, écrite pour être lue d'un mètre, les mains
   prises. Tout ce qui suit part de là : la taille du texte, la hauteur
   des boutons, et le fait que RIEN ne bouge pendant qu'on lit — les
   minuteurs réécrivent leurs chiffres, jamais la page.

   La rangée du titre « Préparation » d'abord : le titre à gauche, la
   porte d'entrée à droite, sur la même ligne de base. */
.prep-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin: 18px 0 var(--sp-2);
}
/* Le titre garde ses marges de titre ; la rangée n'en ajoute pas. */
/* La marge vit sur la rangée, pas sur le titre : portée par le titre,
   elle le poussait sous la ligne du bouton « Mode cuisine » voisin. */
.prep-head h4 { margin: 0; }
.sheet .prep-head + .prep-steps,
.sheet .prep-head + .prep-one { margin-top: 0; }
.cook-go {
  width: auto;
  flex: 0 0 auto;
  padding: 0 14px;
  min-height: 40px;
  gap: 7px;
  white-space: nowrap;
  background: color-mix(in srgb, var(--champ) 75%, transparent);
}
.cook-go .ico { width: 19px; height: 19px; }
.cook-go:active { background: var(--champ); }

/* La jauge d'avancement : trois pixels qui disent où l'on en est. Une
   ligne de texte l'aurait dit aussi, mais on la lit ; celle-ci se voit. */
.cook-jauge {
  height: 4px;
  border-radius: 99px;
  background: var(--papier-2);
  overflow: hidden;
  margin: 2px 0 18px;
}
.cook-jauge i {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: 99px;
  transition: width .25s ease;
}

/* L'ÉTAPE. Vingt pixels et une interligne large : c'est la seule chose
   qu'on vient lire, et on la lit de loin. */
.cook-step {
  margin: 0;
  font-size: 20px;
  line-height: 1.55;
  font-weight: 500;
  color: var(--ink);
}
.cook-h { margin: 22px 0 8px; }

/* Les ingrédients de l'étape : la même ligne que dans la fiche, en
   lecture seule — ici on ne va pas ouvrir une fiche d'article, on a les
   mains dans le saladier. La quantité est ce qu'on vient chercher, donc
   elle est en encre franche et non en gris. */
.cook-ings { list-style: none; margin: 0; padding: 0; }
.cook-ings li {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 2px;
}
.cook-ings li + li { border-top: 1px solid var(--line); }
.cook-ings img {
  width: 34px; height: 34px; flex: 0 0 auto;
  border-radius: var(--r-2); object-fit: cover; background: var(--paper-img);
}
.cook-ings span {
  flex: 1; min-width: 0;
  font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cook-ings b {
  flex: 0 0 auto;
  font-size: 15px; font-weight: 700;
  color: var(--accent-fonce);
  font-variant-numeric: tabular-nums;
}

/* LES MINUTEURS. Plusieurs à la fois — le four, la sauce, les pâtes —
   empilés sous l'étape, chacun avec sa pause et sa croix. Celui qui a
   sonné passe au vert et le dit en toutes lettres : un « 0:00 » se
   confond avec un minuteur qu'on vient de poser. */
.cook-timers { margin-top: 22px; }
.cook-timer {
  display: flex; align-items: center; gap: 10px;
  background: var(--champ);
  border: 1.5px solid var(--line);
  border-radius: var(--r-3);
  padding: 8px 8px 8px 14px;
  margin-bottom: 8px;
}
.cook-timer-nom {
  flex: 1; min-width: 0;
  font-weight: 600; font-size: 14px;
  color: var(--ink-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cook-timer b {
  flex: 0 0 auto;
  font-size: 19px; font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.cook-timer.done {
  border-color: var(--sage);
  background: var(--bien-voile);
}
.cook-timer.done b { color: var(--sage-deep); font-size: 15px; }
.cook-timer-btn {
  flex: 0 0 auto;
  width: 36px; height: 36px;
  border-radius: var(--r-2);
  background: var(--papier);
  border: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 13px; font-weight: 700;
  display: grid; place-items: center;
}
.cook-timer-btn:active { background: var(--papier); }
.cook-timer-add { display: flex; flex-wrap: wrap; gap: 8px; }
/* Les durées toutes faites, dépliées sous le bouton. Une grille plutôt
   qu'une rangée : dix pastilles alignées à la file donnent des lignes
   ragées, et on cherche « 20 min » dans un escalier. */
.cook-timer-choix {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
  gap: 8px;
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}
.cook-timer-choix .chip { justify-content: center; }

/* LA DURÉE ÉCRITE À LA MAIN : deux champs et un bouton, sur une rangée.
   Les champs sont étroits — on y tape deux chiffres — et l'unité se lit
   à leur droite, comme sur une horloge. */
.cook-timer-libre {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
}
.cook-timer-libre label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-soft);
}
.cook-timer-libre input {
  width: 66px;
  min-height: 40px;
  padding: 0 10px;
  text-align: center;
  border-radius: var(--r-2);
  border: 1.5px solid var(--line);
  background: var(--champ);
  font-size: 15px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* Les flèches natives volent vingt pixels sur soixante-six, et on tape
   deux chiffres au clavier. */
.cook-timer-libre input::-webkit-outer-spin-button,
.cook-timer-libre input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.cook-timer-libre input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
.cook-timer-libre .chip { margin-left: auto; }
.cook-chip {
  display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap;
}
.cook-chip .ico { width: 17px; height: 17px; }

/* LA NAVIGATION COLLE AU BAS DE LA FEUILLE.

   On change d'étape vingt fois dans une recette, et toujours avec un
   seul doigt, celui qui n'est pas dans la pâte. Le bouton doit être au
   même endroit à chaque fois — donc en bas, collé, quelle que soit la
   longueur de l'étape qu'on vient de lire. */
.cook-nav {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 26px;
  padding: 12px 0 calc(6px + var(--safe-b));
  background: linear-gradient(to bottom, transparent 0%, var(--feuille) 24%);
}
.cook-nav .btn { min-height: 52px; gap: 8px; }
.cook-nav .btn[disabled] { opacity: .4; }
.cook-nav .ico { width: 19px; height: 19px; }

/* Le « ? » de la pastille de filtre : le même signe, en petit, à la
   place où l'étoile des favoris se tient sur la ligne du dessus. */
.chip-mark {
  flex: 0 0 auto;
  display: inline-grid;
  place-items: center;
  width: 17px; height: 17px;
  margin: 0 6px 0 -2px;
  border-radius: 50%;
  background: var(--papier-2);
  color: var(--accent-fonce);
  font-size: 12px; font-weight: 700;
  line-height: 1;
}
.chip.active .chip-mark { background: rgba(255, 255, 255, .28); color: #fff; }
/* VALIDÉE EST VERTE, À VALIDER EST GRISE — au repos comme une fois choisies.

   Les deux marques avaient la couleur d'action : deux ronds pareils, et
   il fallait lire le signe pour savoir lequel était lequel. Le vert est
   la couleur de « c'est bon » partout ailleurs (la recette validée, la
   synchro), le gris celle de ce qui attend. Pleines, elles restent
   lisibles sur la pastille blanche comme sur la pastille foncée. */
.chip-mark.ok, .chip.active .chip-mark.ok { background: var(--sage); color: #fff; }
.chip-mark.attente, .chip.active .chip-mark.attente { background: var(--ink-mute); color: #fff; }

/* « JE L'AI FAITE » PREND LA PLACE DE L'ÉTOILE.

   Même gabarit, même contour, et la couleur d'ACTION de la maison. Deux
   teintes ont été essayées avant : le vert de « Valider la semaine »,
   qui était le bon mot et la mauvaise couleur — au bout d'une ligne, à
   côté du « + », un rond vert se lit comme un état du plat plutôt que
   comme un bouton ; puis le miel de l'étoile, trop doré et trop proche
   du favori qu'il remplace, au point qu'on croyait voir une étoile mal
   dessinée. La couleur d'action (`--accent`) est celle de tout ce qui
   AGIT dans cette app — et le caddie de la même rangée en est le plein.
   La coche, elle, a pris la couleur ET le contour de l'étoile au repos :
   c'est la même case, seul le signe change. Colorée, elle devenait un
   bouton d'une autre famille qui disputait l'attention au caddie. */
.prod .fav-btn.totry-btn {
  color: var(--ink-soft);
  border-color: var(--line);
  background: var(--surface);
}
.prod .fav-btn.totry-btn:active { background: var(--papier); }
.prod .fav-btn.totry-btn .ico { width: 23px; height: 23px; }

/* Une case hors de portée : elle reste, éteinte. Effacer « 3 » d'une
   semaine où il ne reste que deux créneaux ferait croire qu'il n'a jamais
   existé — et la rangée changerait de longueur d'une semaine à l'autre. */
.chip.off,
.chip:disabled {
  opacity: .42;
  cursor: default;
}
.chip.off:active { transform: none; }

/* Le compte des essais en attente, au bout de la rangée de l'encart de
   génération : une indication, pas un bouton. */
.filter-note {
  align-self: center;
  color: var(--ink-soft);
  font-size: 12px;
  font-weight: 600;
}

/* ---------- ce que mange la famille (ui/foyer.js) ---------- */
.foyer-avis {
  display: flex; gap: var(--sp-3); align-items: flex-start;
  margin: var(--sp-2) 0 var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-2);
  background: var(--accent-voile);
  border: 1px solid var(--accent-pale);
  font-size: 14px; line-height: 1.45;
}
.foyer-avis p { margin: 0; color: var(--ink-soft); }
.foyer-avis b { color: var(--ink); }
.foyer-avis-ico { flex: none; color: var(--accent); margin-top: 1px; }
.foyer-avis-ico svg { width: 22px; height: 22px; }
.foyer-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.foyer-chip { flex-direction: column; gap: 0; line-height: 1.2; padding-block: 6px; }
.foyer-chip small { font-weight: 500; font-size: 12px; opacity: .75; }
.foyer-hint { margin-top: 0; margin-bottom: var(--sp-2); }
.foyer-deja { margin: 0 2px var(--sp-4); }
.foyer-bilan { color: var(--accent-fonce); }
.foyer-masquees {
  margin: var(--sp-3) 0; padding: var(--sp-3) var(--sp-4);
  text-align: center; font-size: 13px; color: var(--ink-soft);
}
.foyer-masquees .linkish { margin-left: var(--sp-1); }

/* ---------- partager une recette (ui/partage.js) ---------- */
.partage-apercu {
  display: grid; place-items: center;
  margin: var(--sp-2) 0 var(--sp-4);
  min-height: 220px;
  border-radius: var(--r-3);
  background: var(--papier-2);
  overflow: hidden;
}
.partage-apercu img { display: block; width: min(100%, 280px); height: auto; border-radius: var(--r-2); margin: var(--sp-3); box-shadow: 0 8px 28px rgba(43, 38, 34, .18); }
.partage-attente { color: var(--ink-soft); font-size: 14px; }
.partage-duo { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
.partage-duo .btn { margin: 0; font-size: 15px; padding: 0 10px; }

/* Les pages qui engagent Pasto, au pied des Réglages. */
.set-legal { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--sp-1) var(--sp-4); margin: var(--sp-4) 0 0; font-size: 13px; }
.set-legal a { color: var(--ink-soft); }
