/* Police manuscrite du logo. Le fichier doit être placé à assets/font/segoe-script.ttf
   (voir CLAUDE.md) — tant qu'il n'y est pas, --font-script retombe sur des polices
   cursives système. */
@font-face {
  font-family: "Segoe Script Idiaa";
  src: url("../font/segoe-script.ttf") format("truetype");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  font-family: var(--font-system);
  /* Verrouille le mouvement horizontal en mobile, demandé explicitement ("la page bouge
     un peu quand on glisse de gauche à droite") : sans ça, un geste tactile horizontal
     déclenche le comportement natif du navigateur (rebond d'overscroll, ou l'aperçu de
     navigation retour/avant par glissement depuis le bord — variable selon navigateur/
     OS), qui décale visiblement toute la page avant de se raviser. `touch-action: pan-y`
     indique qu'aucun geste horizontal (panoramique ou zoom, déjà bloqué par le viewport
     `user-scalable=no`) n'est un geste natif par défaut ; `overscroll-behavior-x: none`
     coupe en plus le rebond/chaînage horizontal côté défilement. N'affecte PAS le
     glissement tactile des lignes de liste (swipe-row.js) : ce composant lit les
     coordonnées tactiles brutes en JS et anime lui-même un `transform: translateX`, un
     mécanisme entièrement indépendant du geste natif du navigateur que ces deux
     propriétés désactivent. Le défilement vertical (scroll normal de la page) reste
     inchangé (`pan-y` l'autorise explicitement). */
  touch-action: pan-y;
  overscroll-behavior-x: none;
  /* Couleur seule ici — la photo (--bg-image, "none" en clair/sombre, voir
     variables.css) vit dans body::before ci-dessous, pas directement sur ce
     background-color/-image : une image ne peut pas être transitionnée en douceur via
     `transition: background-image` (aucun navigateur n'interpole un changement d'url()),
     donc le fondu à l'apparition demandé explicitement (voir body::before) a besoin
     d'une couche à part, avec sa propre opacité animable. */
  background-color: var(--bg);
  color: var(--text);
}

/* Photo de fond (mode Îdiää) : couche séparée en position:fixed, fondu d'apparition en
   CSS pur (@keyframes, pas de JS). Vide en clair/sombre (--bg-image: none) : rien à
   afficher ni animer, sans coût perceptible.
   position:fixed remplace ici l'ancien `background-attachment: fixed` posé directement
   sur html/body (ESSAI noté aux Points ouverts, CLAUDE.md, jugé peu fiable sur iOS
   Safari en PWA) : même effet (fond immobile au défilement) obtenu autrement.
   z-index:0, PAS négatif — piège rencontré (signalé par l'utilisateur, "fond blanc
   partout sauf login.php") : position:fixed + z-index négatif se peignait derrière le
   fond de page (canvas) lui-même dans son navigateur, invisible malgré des styles
   calculés entièrement corrects (opacity:1, bonne image, bonne taille — vérifié pas à
   pas via DevTools avant de trouver la cause). z-index négatif suit en théorie l'ordre
   d'empilement CSS standard (peint après le canvas, avant le contenu statique), mais ce
   n'est manifestement pas fiable partout pour cette combinaison précise. Avec
   z-index:0, il faut en contrepartie que TOUT contenu visible passe explicitement
   au-dessus — voir `.main-content { position: relative; z-index: 1 }` plus loin (les
   autres conteneurs de premier niveau — topbar, nav, popups, fab — ont déjà tous un
   z-index positif, aucun changement nécessaire pour eux). */
body::before {
  content: '';
  position: fixed;
  /* top/right/bottom/left plutôt que le raccourci `inset: 0` : aucun avantage ici (même
     résultat) mais un support navigateur plus récent — écarté par précaution. */
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 0;
  background-image: var(--bg-image);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  opacity: 0;
  /* PAS d'animation posée ici (contrairement à un essai précédent) — voir la règle
     html:not(.idiaa-auth-pending) plus bas : le fond ne doit commencer à apparaître
     qu'UNE FOIS l'interface (topbar/nav/contenu) déjà affichée, pas en même temps ni
     avant, demandé explicitement. */
}

@keyframes idiaa-bg-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* Démarre le fondu du fond SEULEMENT une fois html.idiaa-auth-pending retirée (voir
   juste plus bas) — c'est-à-dire au moment EXACT où l'interface devient visible
   (visibility:hidden -> visible, instantané) : l'ordre perçu devient "l'interface
   apparaît, PUIS le fond se fond dessus" plutôt que les deux en même temps depuis le
   tout premier paint (comportement précédent — le fond démarrait son fondu
   immédiatement, indépendamment de l'état "en attente" de l'interface, qui pouvait
   rester masquée jusqu'à 1-2s de plus le temps qu'auth-guard.js confirme la connexion
   via api/auth/me.php). Une animation CSS redémarre dès que la règle qui la pose
   commence à s'appliquer à l'élément — pas besoin de JS pour synchroniser les deux. */
html:not(.idiaa-auth-pending) body::before {
  animation: idiaa-bg-fade-in 0.8s ease forwards;
}

/* Anti-flash de contenu protégé — voir le script en tête de header.php (pose la classe
   avant le <body>) et auth-guard.js (la retire une fois la connexion confirmée, sinon
   redirige vers login.php sans jamais la retirer). visibility:hidden (pas display:none)
   pour ne rien casser côté layout/JS qui lirait des dimensions avant que auth-guard.js
   ait tranché. Le fond (body::before / background-color, tous les deux sur html/body,
   jamais concernés par ces classes) reste visible : pas de flash blanc, un écran "vide"
   le temps de la vérification (1-2s au pire selon la persistance Firebase locale). */
html.idiaa-auth-pending .topbar,
html.idiaa-auth-pending .nav-desktop,
html.idiaa-auth-pending .nav-mobile,
html.idiaa-auth-pending .main-content,
html.idiaa-auth-pending .fab {
  visibility: hidden;
}

/* Pages accessibles sans connexion (cgu.php/confidentialite.php, voir
   assets/js/public-page-guard.js) : posée quand onAuthChange confirme qu'il n'y a PAS de
   session — masque la sidebar/dock (liens qui renverraient de toute façon vers
   login.php), et retire la réservation d'espace de .nav-width-desktop côté body (voir la
   règle @media (min-width:900px) juste au-dessus) puisqu'aucune sidebar n'est visible
   pour la justifier. */
html.idiaa-guest-page .nav-desktop,
html.idiaa-guest-page .nav-mobile {
  display: none !important;
}

@media (min-width: 900px) {
  html.idiaa-guest-page body {
    padding-left: 0;
  }
}

body {
  min-height: 100vh;
  /* env(safe-area-inset-top) = zone occupée par l'heure/l'encoche/le signal en PWA
     plein écran iOS (status-bar-style "black-translucent" laisse le contenu passer
     dessous, viewport-fit=cover l'expose) ; vaut 0 ailleurs (desktop, Android...). */
  padding-top: calc(var(--topbar-h) + env(safe-area-inset-top, 0px));
  padding-bottom: calc(var(--nav-height-mobile) + env(safe-area-inset-bottom, 0px));
}

@media (min-width: 900px) {
  body {
    padding-bottom: 0;
    padding-left: var(--nav-width-desktop);
  }
}

a {
  color: var(--accent);
  text-decoration: none;
}

/* ---------- Tooltips (icônes d'action isolées, sans texte visible à côté) ---------- */
/* Attribut data-tooltip plutôt qu'un composant JS positionné (comme .th-filter__menu) :
   ces icônes n'ont pas besoin de s'échapper d'un conteneur overflow:hidden, un simple
   ::after (bulle) + ::before (flèche) suffit, affiché sur :hover/:focus-visible (clavier).
   Volontairement PAS d'attribut `title` en plus (éviterait un doublon avec la bulle grise
   native du navigateur) — data-tooltip reprend le même texte que l'aria-label existant,
   une seule chaîne à maintenir par icône.
   position:relative placé ICI, tôt dans le fichier : .avatar-camera-btn/.icon-btn.topbar-back
   (plus bas) ont déjà position:absolute — à spécificité égale, la déclaration la plus
   tardive dans le fichier gagne, donc en restant tôt ici, ces position:absolute existants
   continuent de l'emporter sans rien casser (vérifié avant d'écrire cette règle). */
[data-tooltip] {
  position: relative;
}

[data-tooltip]::before,
[data-tooltip]::after {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  opacity: 0;
  pointer-events: none;
  /* transition-delay explicite à 0s ici (pas juste omise) : à la disparition (survol
     terminé), cette règle de base reprend la main SANS délai — seule l'apparition
     (:hover/:focus-visible ci-dessous) en a un. Évite que la bulle traîne à l'écran
     après que le curseur ait quitté l'icône. */
  transition: opacity 0.15s ease;
  transition-delay: 0s;
  z-index: 150;
}

/* Bulle "inversée" (fond var(--text)/texte var(--bg)) : lisible dans les 3 thèmes sans
   nouveau token dédié. */
[data-tooltip]::after {
  content: attr(data-tooltip);
  bottom: calc(100% + 8px);
  background: var(--text);
  color: var(--bg);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.3;
  white-space: nowrap;
  padding: 6px 10px;
  border-radius: 6px;
  box-shadow: var(--shadow-md);
}

/* Petite flèche (triangle CSS classique : border-top coloré, reste transparent —
   pointe vers le bas, vers l'icône). */
[data-tooltip]::before {
  content: '';
  bottom: calc(100% + 3px);
  border: 5px solid transparent;
  border-top-color: var(--text);
}

/* Délai volontaire de 400ms avant apparition (demandé explicitement) : évite que les
   bulles clignotent quand la souris ne fait que traverser l'écran sans s'arrêter sur une
   icône — seul ce sens (apparition) a un délai, la disparition reste immédiate (voir la
   règle de base ci-dessus).
   :hover scopé à `(hover: hover)` (souris/trackpad réel) — sur tactile (tablette
   incluse, pas seulement mobile <900px), :hover se déclenche au tap et peut rester
   "collé" jusqu'au tap suivant ailleurs (bulle qui traîne à l'écran sans curseur à
   déplacer pour la faire disparaître) : autant ne jamais l'activer là où il n'a pas de
   sens. :focus-visible, lui, reste actif dans tous les cas (accessibilité clavier,
   indépendante de la capacité de survol — un clavier externe branché sur tablette doit
   continuer à déclencher la bulle). */
@media (hover: hover) {
  [data-tooltip]:hover::after,
  [data-tooltip]:hover::before {
    opacity: 1;
    transition-delay: 0.4s;
  }
}

[data-tooltip]:focus-visible::after,
[data-tooltip]:focus-visible::before {
  opacity: 1;
  transition-delay: 0.4s;
}

/* Variante "en dessous" — pour une icône trop proche du haut de l'écran pour que la
   bulle par défaut (au-dessus) reste visible (ex. .topbar-back, header.php) ou trop
   proche d'un bord latéral (voir data-tooltip-align plus bas). */
[data-tooltip][data-tooltip-pos="bottom"]::after {
  bottom: auto;
  top: calc(100% + 8px);
}

[data-tooltip][data-tooltip-pos="bottom"]::before {
  bottom: auto;
  top: calc(100% + 3px);
  border-top-color: transparent;
  border-bottom-color: var(--text);
}

/* Variante "à gauche" — icône dans une ligne de glissement tactile (.xxx-row-swipe,
   overflow:hidden pour ses coins arrondis pendant le geste) : ni au-dessus ni en dessous
   n'a la place de s'afficher entièrement (hauteur de ligne trop faible, ~50-70px, coupée
   par ce même overflow:hidden — bulle invisible, signalée par l'utilisateur, capture à
   l'appui), contrairement à la largeur (une ligne est toujours bien plus large que
   haute). La bulle s'ouvre donc horizontalement, à gauche de l'icône plutôt qu'au-dessus/
   en dessous — verticalement centrée sur l'icône (translateY, pas translateX). */
[data-tooltip][data-tooltip-pos="left"]::after {
  bottom: auto;
  top: 50%;
  left: auto;
  right: calc(100% + 8px);
  transform: translateY(-50%);
}

[data-tooltip][data-tooltip-pos="left"]::before {
  bottom: auto;
  top: 50%;
  left: auto;
  right: calc(100% + 3px);
  transform: translateY(-50%);
  border-top-color: transparent;
  border-left-color: var(--text);
}

/* Variante "alignée à droite" — icône trop proche du bord droit d'un panneau étroit
   (overflow:hidden) pour qu'une bulle centrée reste entière : la bulle s'aligne sur le
   bord droit de l'icône au lieu d'être centrée dessus (la flèche, elle, reste centrée
   sur l'icône — pointe toujours au bon endroit même décalée par rapport à la bulle). */
[data-tooltip][data-tooltip-align="end"]::after {
  left: auto;
  right: 0;
  transform: none;
}

/* Miroir de la variante ci-dessus — icône trop proche du bord GAUCHE (ex. premier bouton
   du composeur de chat, collé au bord de l'écran en mobile). */
[data-tooltip][data-tooltip-align="start"]::after {
  left: 0;
  transform: none;
}

/* Wrapper minimal pour poser data-tooltip sur un <svg> brut (icône "+" sans bouton
   autour) : ::after/::before (attr(data-tooltip)) ne se peint pas sur un élément <svg>
   lui-même dans Chromium (testé isolément) — seulement sur un élément HTML classique.
   inline-flex pour ne pas perturber la mise en page (dimensionnement au contenu, comme
   le <svg> qu'il remplace dans le flux). */
.icon-tooltip {
  display: inline-flex;
}

/* ---------- Icône menu (⋮) + menu contextuel (row-menu.js) ---------- */
/* Nouveau concept de liste, demandé explicitement : remplace l'ancienne icône de
   suppression en fin de ligne (hors du cadre) par une icône menu À L'INTÉRIEUR de la
   ligne, ouvrant un petit menu contextuel (Éditer/Partager/Supprimer selon la liste) —
   partagé par toutes les listes de l'app (recettes, amis, listes de courses, articles),
   une seule définition ici plutôt que dupliquée par page. */
/* Masqué entièrement par défaut/sur tactile (display:none, pas juste opacité) : les
   actions y sont révélées par glissement à la place (swipe-row.js), pas par ce menu —
   voir CLAUDE.md / Détection tactile vs pointeur. Sur pointeur fin (media query plus
   bas), redevient un inline-flex normal, masqué seulement par opacity et révélé par
   chaque appelant (règle posée sur SA propre classe de ligne, ex. ".data-table tbody
   tr:hover .row-menu-trigger" — recipes.js) — ou tant que son propre menu reste ouvert
   (.row-menu-open, posée par row-menu.js : sans ça, le déclencheur disparaîtrait dès que
   le curseur quitte la ligne pour aller cliquer une action du menu, lui-même hors de la
   ligne, ajouté à document.body). */
.row-menu-trigger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  /* Décale l'icône de 10px vers la gauche (demandé explicitement, trop proche du bord) —
     posée sur ce bouton partagé plutôt que sur chaque cellule/conteneur appelant : dans
     un <td> centré (text-align:center, recipes.js/shopping-lists.js/shopping-list-view.js)
     comme dans le dernier élément d'une ligne flex (.people-row__action, friends.js), une
     marge à droite décale le bouton vers la gauche de la même façon dans les deux cas. */
  margin-right: 10px;
  border: none;
  background: none;
  padding: 0;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 0.2s ease;
}

.row-menu-trigger svg {
  width: 100%;
  height: 100%;
}

/* Même couleur/transition de survol que les icônes "+" de l'app (.icon-plus:hover) —
   demandé explicitement. */
@media (hover: hover) and (pointer: fine) {
  .row-menu-trigger:hover {
    color: var(--svg-hover);
  }
}

@media (hover: hover) and (pointer: fine) {
  .row-menu-trigger {
    display: inline-flex;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease, color 0.2s ease;
  }
}

/* Tactile (mobile ET tablette), demandé explicitement — "on garde les points en fin de
   ligne à droite" : le déclencheur ⋮ n'était jusqu'ici jamais affiché en tactile
   (display:none par défaut, révélé uniquement par la media query :hover ci-dessus, qui
   ne s'applique jamais sur un écran tactile). Toujours visible ici, PAS de survol à
   attendre (ce mécanisme n'existe pas au doigt) : ouvre le même menu contextuel complet
   qu'au clic sur desktop (row-menu.js, inchangé). Le glissement, lui, ne révèle plus
   désormais qu'UNE seule action (Supprimer) par ligne — voir chaque page
   (recipes.js/friends.js/etc.) et les règles ...__actions-inner (fond rouge/icône
   blanche) plus bas dans ce fichier. Desktop (media query ci-dessus) inchangé. */
@media (hover: none), (pointer: coarse) {
  .row-menu-trigger {
    display: inline-flex;
    opacity: 1;
    pointer-events: auto;
  }
}

/* Menu contextuel : ajouté à document.body par row-menu.js (position:fixed, coordonnées
   directement compatibles avec getBoundingClientRect()) — jamais rogné par l'overflow:
   hidden d'un tableau/panneau englobant, contrairement à l'ancienne icône de suppression
   "en dehors" du tableau (.shopping-list-delete-outside, retirée avec ce changement). */
.row-menu {
  position: fixed;
  z-index: 200;
  min-width: 160px;
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* Même fond que les popups de l'app (.list-share-panel, .list-create-panel...) en
   mode Îdiää, demandé explicitement : panneau vitré
   (surface-alt translucide + flou), pas le fond plein var(--surface) des autres thèmes. */
:root[data-theme="idiaa"] .row-menu {
  background-color: var(--surface-alt);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

/* flex plutôt que block (icône SVG optionnelle, item.icon, à gauche du texte — gap 8px
   demandé explicitement) : "Effacer la discussion" (friends.js, pas d'icône fournie pour
   cette action précise) reste du texte seul, aligné sur les autres via ce même flex (pas
   de décalage entre lignes avec/sans icône, gap ne réserve rien s'il n'y a rien à côté). */
.row-menu__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  border: none;
  background: none;
  padding: 8px 10px;
  border-radius: 6px;
  font-size: 14px;
  color: var(--text);
  cursor: pointer;
  transition: background-color 0.15s ease;
}

/* Texte grossi de 50% en mobile uniquement (14px -> 21px), demandé explicitement —
   .row-menu est LE menu contextuel "⋮" partagé par toutes les listes de l'app
   (recettes, livres, amis, listes de courses, admin...), voir row-menu.js. Breakpoint
   900px (mise en page, pas hover/pointer) : même seuil que le reste de l'app pour
   distinguer mobile/desktop, voir CLAUDE.md / Détection tactile vs pointeur — ce
   réglage-ci porte sur la taille de police, pas la capacité de survol. Icônes
   (.row-menu__item-icon) non concernées, seul le texte a été demandé. */
@media (max-width: 899px) {
  .row-menu__item {
    font-size: 21px;
  }
}

.row-menu__item-icon {
  display: inline-flex;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  color: inherit;
}

.row-menu__item-icon svg {
  width: 100%;
  height: 100%;
}

/* Même couleur de survol que la sidebar (.nav-desktop__item:hover, nav.css) — demandé
   explicitement. var(--accent-tint) plutôt que var(--surface-alt) (valeur précédente) :
   pas besoin de scoper par thème, la sidebar elle-même n'a pas de règle Îdiää dédiée pour
   son survol, --accent-tint étant déjà défini dans les 3 thèmes (variables.css). */
@media (hover: hover) and (pointer: fine) {
  .row-menu__item:hover {
    background: var(--accent-tint);
  }
}

.row-menu__item--danger {
  color: var(--danger);
}

.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: calc(var(--topbar-h) + env(safe-area-inset-top, 0px));
  display: flex;
  align-items: stretch;
  padding-top: env(safe-area-inset-top, 0px);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  z-index: 40;
}

/* Refonte couleurs sombre en cours (voir variables.css) — valeur fixe demandée
   explicitement plutôt que --surface (qui sert aussi aux cards/popups/tableaux, pas
   concernés par cette demande). */
:root[data-theme="dark"] .topbar {
  background: rgb(39, 39, 39);
}

/* Colonne alignée sur .nav-desktop (même largeur, même logique de disparition sous
   900px — voir nav.css) : le logo y reste centré horizontalement et verticalement,
   quelle que soit la page. */
.topbar-brand {
  display: none;
  width: var(--nav-width-desktop);
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
}

.topbar-logo {
  font-family: var(--font-script);
  font-size: 32px;
  font-weight: bold;
  color: var(--text);
}

/* Titre de page. position:relative pour ancrer .icon-btn.topbar-back (voir plus bas,
   toutes tailles d'écran) en position:absolute plutôt qu'en élément flex concurrent du
   titre : sinon les
   deux se disputeraient l'espace disponible et fausseraient le calcul max-width/
   margin:auto du titre (qui doit correspondre exactement à celui de .main-content, sans
   amputation liée à la largeur d'un bouton). Portait aussi le bouton profil de la topbar
   avant son retrait (voir CLAUDE.md — contenu déplacé dans settings.php). */
.topbar-main {
  flex: 1;
  min-width: 0;
  position: relative;
  display: flex;
  align-items: center;
  /* Symétrique (16px des deux côtés) — était 16px/20px (bug signalé : titre décalé
     vers la gauche). .topbar-title (text-align:center) se centre sur la boîte de
     .topbar-title-inner ci-dessous : toute asymétrie ici (ou dans son padding à elle)
     se répercute directement en décalage visuel du titre, pas seulement de sa boîte. */
  padding: 0 16px;
}

.topbar-title-inner {
  width: 100%;
  /* Symétrique (48px des deux côtés), pas juste padding-right — même piège que
     .topbar-main ci-dessus : réserver la place du bouton profil qu'accueillait alors la
     topbar (40px + marge, voir CLAUDE.md — retiré depuis) UNIQUEMENT à droite décalait
     le centre visuel du titre vers la gauche (sa boîte n'était plus symétrique).
     Reproduit le même principe que la version desktop (.topbar-title-inner, media query
     plus bas) : padding symétrique plutôt qu'aligné sur un bord précis, un titre qui
     irait jusqu'au bord reste tronqué proprement (ellipsis sur .topbar-title). Padding
     conservé tel quel après le retrait du bouton (rien à réserver de plus, mais rien ne
     casse à le garder symétrique). */
  padding: 0 48px;
}

.topbar-title {
  margin: 0;
  font-size: 30px;
  font-weight: 650;
  color: var(--text);
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Bouton "Retour" (.topbar-back, header.php) — demandé explicitement, TOUTES TAILLES
   D'ÉCRAN (mobile, tablette, desktop — élargi depuis un périmètre mobile uniquement,
   voir CLAUDE.md) : les pages "secondaires" (sans entrée directe dans le dock/la
   sidebar, ex. recipe-view.php/shopping-list-view.php/chat.php...) n'ont sinon aucun
   moyen de revenir à leur page parente autrement qu'en reglissant le geste "retour" du
   système ou en retapant une URL — la sidebar desktop permet de changer de SECTION
   mais pas de revenir à une page parente précise (ex. le livre de recette d'où venait
   une fiche recette). Même position/taille que l'ex-bouton profil de la topbar (retiré
   depuis, voir CLAUDE.md), juste à gauche plutôt qu'à droite. PHP ne le rend que si
   $backHref est posé par la page (voir header.php) — les 5 pages principales (sans
   `$backHref`) n'affichent donc jamais ce bouton, quelle que soit la largeur d'écran.
   `.icon-btn.topbar-back` (2 classes) plutôt que `.topbar-back` seule : gagne la
   cascade face à la règle générique `.icon-btn` (display:inline-flex, plus bas dans ce
   fichier) indépendamment de l'ordre des règles dans le fichier, à spécificité égale
   sinon fragile (bug déjà rencontré une fois avec une seule classe, voir CLAUDE.md). */
.icon-btn.topbar-back {
  display: inline-flex;
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text);
  transition: color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .icon-btn.topbar-back:hover {
    color: var(--accent);
    background: transparent;
  }
}

/* Icône d'action de la topbar (.topbar-action), côté droit — symétrique à
   .topbar-back côté gauche (même position/taille/toutes tailles d'écran), générique
   pour n'importe quelle page qui pose $topbarActionId (voir header.php) — recipe-edit.php
   pour "Enregistrer" à ce jour. .topbar-title-inner a déjà 48px de padding symétrique
   des deux côtés (hérité de l'ex-bouton profil, voir CLAUDE.md / Topbar fixe) : assez
   de place pour cette icône sans rien recalculer. */
.icon-btn.topbar-action {
  display: inline-flex;
  position: absolute;
  right: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text);
  transition: color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .icon-btn.topbar-action:hover {
    color: var(--accent);
    background: transparent;
  }
}

@media (min-width: 900px) {
  .topbar-brand {
    display: flex;
  }

  .topbar-main {
    padding: 0;
  }

  /* Centré sur la largeur de .topbar-main (donc sur la zone de contenu, comme en
     mobile) plutôt qu'aligné à gauche — même boîte que .main-content (max-width 960 +
     margin:auto) pour rester centré sur le contenu réel même sur très grand écran,
     mais padding symétrique (48px des deux côtés, gauche comme droite) puisqu'il n'y a
     plus besoin de caler un bord précis sur quoi que ce soit — juste assez pour que le
     bouton profil (à droite) ne soit jamais recouvert. */
  .topbar-title-inner {
    max-width: 960px;
    margin: 0 auto;
    padding: 0 48px;
  }
}

/* .page-header : conteneur du <h1> encore utilisé directement par recipe-view.php
   (titre = nom de la recette, chargé dynamiquement en JS, distinct du système de
   $pageTitle statique — voir Design system / Titre de page dans le topbar) — pas par
   les pages listées ci-dessus, dont le titre vit maintenant dans le topbar.
   display:flex (demandé explicitement, icônes Partager/Éditer à droite du nom, voir
   .recipe-view-actions plus bas) : justify-content:space-between pousse .h1 à gauche et
   .recipe-view-actions à droite, sans changer le rendu des autres pages qui n'ont pas ce
   second enfant (un seul enfant flex se comporte comme avant, display:block ou non). */
.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 20px 0 16px;
}

/* Remonte "Partagé par" plus près du nom (5px au lieu des 16px habituels) quand il est
   affiché (recipe-view.php uniquement, voir Design system) — laisse le cas normal (pas
   de recette partagée) inchangé, :has() ne cible que ce cas précis. Même technique déjà
   utilisée ailleurs dans ce fichier (.idiaa-field:has(textarea)). */
.page-header:has(+ #recipeViewSharedBy:not([hidden])) {
  margin-bottom: 5px;
}

.page-header h1 {
  font-size: 28px;
  font-weight: 650;
  margin: 0;
}

/* Groupe entier Partager/Éditer/Favoris (recipe-view.php) — colonne : la ligne
   Partager/Éditer (.recipe-view-actions, inchangée juste en dessous) au-dessus, Favoris
   (nouveau) en dessous, aligné à droite (align-items:flex-end) donc juste sous "Éditer"
   (dernier icône de la ligne, le plus à droite) — demandé explicitement. flex-shrink:0
   repris ici (portait auparavant sur .recipe-view-actions directement, qui était le
   flex item direct de .page-header — c'est ce groupe qui l'est désormais) pour ne
   jamais se faire comprimer par un nom de recette long (.page-header h1 n'a lui aucune
   largeur maximale propre). */
.recipe-view-actions-group {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  flex-shrink: 0;
}

/* Icônes Partager/Éditer, ligne (imbriquée dans .recipe-view-actions-group ci-dessus). */
.recipe-view-actions {
  display: flex;
  gap: 12px;
}

/* color: var(--accent) (bleu — remplace var(--text), décision inversée sur demande
   explicite) + survol orange (var(--svg-hover)) — même transition que les autres icônes
   de l'app (.icon-plus, .chat-composer__icon-btn), sans le scale() de ces dernières
   (pas demandé ici). */
.recipe-view-actions__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: none;
  background: none;
  padding: 0;
  color: var(--accent);
  cursor: pointer;
  transition: color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .recipe-view-actions__btn:hover {
    color: var(--svg-hover);
  }
}

.recipe-view-actions__btn svg {
  width: 100%;
  height: 100%;
}

/* Favoris (recipe-view.php), demandé explicitement — bleu par défaut (var(--accent),
   comme Partager/Éditer ci-dessus), AUCUN survol (pas demandé, contrairement à
   Partager/Éditer) : juste le changement d'état au clic/tap. Le contour (stroke) suit
   `color` via currentColor (voir includes/icons.php), le remplissage (fill) est géré
   séparément juste en dessous — les deux doivent pouvoir varier indépendamment
   (contour toujours visible, remplissage seulement une fois favori). */
.recipe-view-favorite-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: none;
  background: none;
  padding: 0;
  color: var(--accent);
  cursor: pointer;
  transition: color 0.2s ease;
}

.recipe-view-favorite-btn svg {
  width: 100%;
  height: 100%;
}

.recipe-view-favorite-btn svg path {
  fill: none;
  transition: fill 0.2s ease;
}

/* Favori actif — rouge exact demandé (RGB(255,55,75), littéral plutôt qu'un token
   existant : proche mais distinct de --danger #ff453a, voir CLAUDE.md / Mode sombre
   pour d'autres cas de couleurs exactes demandées telles quelles). color: change le
   contour (stroke="currentColor", voir includes/icons.php) ; fill sur le <path>
   directement, le remplit — les deux à la même valeur pour un cœur plein uni. */
.recipe-view-favorite-btn.is-favorite {
  color: rgb(255, 55, 75);
}

.recipe-view-favorite-btn.is-favorite svg path {
  fill: rgb(255, 55, 75);
}

/* Bouton icône générique (hamburger, +, ...) */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--text);
  cursor: pointer;
}

@media (hover: hover) and (pointer: fine) {
  .icon-btn:hover {
    background: var(--surface-alt);
  }
}

.icon-btn svg {
  width: 22px;
  height: 22px;
}

/* Icônes de gauche/droite des en-têtes de popup (.list-header--icon, toutes les popups
   qui suivent ce design — Nouveau livre, Nouvelle liste, Partager une liste, Ingrédients,
   Nouvel ingrédient, Articles, Nouveau produit), demandé explicitement : "blanc avec un
   hover en bleu... je ne veux pas de rond qui apparaît autour au survol" — MODE SOMBRE
   UNIQUEMENT, précisé explicitement ensuite ("ces couleurs sont pour le thème sombre, on
   s'occupera du clair après") : clair/système/Îdiää gardent leur traitement générique
   .icon-btn (voir plus haut) inchangé pour l'instant. Surcharge scopée à
   `:root[data-theme="dark"] .list-header--icon .icon-btn` plutôt qu'à `.icon-btn`
   lui-même (générique, partagé par `.topbar-back`/`.topbar-action`/l'avatar, non
   concernés par cette demande). `background: transparent` au survol : neutralise le
   fond rond hérité de la règle générique (voir plus haut) plutôt que le retirer
   (garderait border-radius sans fond, déjà invisible, mais explicite ici pour la
   lisibilité). Bleu FIXE (#007aff, pas var(--accent)) : même précédent déjà établi
   ailleurs dans l'app (état coché des switches, icône active du dock mobile) pour un
   bleu qui reste bleu indépendamment des autres tokens du thème. */
:root[data-theme="dark"] .list-header--icon .icon-btn {
  color: #fff;
}

@media (hover: hover) and (pointer: fine) {
  :root[data-theme="dark"] .list-header--icon .icon-btn:hover {
    background: transparent;
    color: #007aff;
  }
}

.main-content {
  padding: 0 20px 24px;
  max-width: 960px;
  margin: 0 auto;
  /* Doit rester au-dessus de body::before (fond photo, voir plus haut) — celui-ci est en
     z-index:0 plutôt que négatif (piège rencontré : position:fixed + z-index négatif se
     peignait derrière le fond de page lui-même dans certains navigateurs, malgré des
     styles calculés corrects — vérifié via DevTools, capture à l'appui), donc n'importe
     quel contenu statique (non positionné) passerait sinon dessous. */
  position: relative;
  z-index: 1;
}

.card {
  border-radius: var(--radius-md);
  padding: 16px;
  margin-bottom: 12px;
}

.card--surface {
  background: var(--surface);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
}

.empty-state {
  text-align: center;
  color: var(--text-muted);
  padding: 60px 20px;
}

/* Bouton d'action flottant "+" — pas de bouton texte "Ajouter ..." */
.fab {
  position: fixed;
  right: 20px;
  bottom: calc(var(--nav-height-mobile) + env(safe-area-inset-bottom, 0px) + 20px);
  width: 56px;
  height: 56px;
  border-radius: 50%;
  border: none;
  background: var(--accent);
  color: var(--accent-contrast);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
  cursor: pointer;
  z-index: 20;
}

.fab svg {
  width: 26px;
  height: 26px;
}

@media (min-width: 900px) {
  .fab {
    bottom: 24px;
  }
}

.auth-card {
  max-width: 380px;
  margin: 12vh auto 0;
  padding: 0 20px;
}

.auth-logo {
  text-align: center;
  font-family: var(--font-script);
  font-size: 44px;
  font-weight: bold;
  color: var(--text);
}

.btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 13px 16px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}

.btn + .btn {
  margin-top: 10px;
}

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

.input {
  width: 100%;
  padding: 13px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--text-muted);
  background: var(--surface);
  color: var(--text);
  font-size: 14px;
  margin-bottom: 10px;
  box-sizing: border-box;
  transition: .3s;
}

/* Refonte couleurs sombre en cours (voir variables.css) — champs de saisie, valeur fixe
   demandée explicitement plutôt que --surface (partagée avec d'autres éléments non
   concernés par cette demande). */
:root[data-theme="dark"] .input {
  background: rgb(52, 53, 52);
  border-color: rgb(119, 111, 112);
}

/* Texte des placeholder, valeur fixe demandée explicitement (distincte de la couleur des
   icônes devant les champs, rgb(119, 112, 111) — chiffres proches mais pas identiques,
   ne pas fusionner les deux règles). */
:root[data-theme="dark"] .input::placeholder {
  color: rgb(119, 111, 112);
}

.input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 5px 0 var(--accent);
}

/* Champs à ligne unique uniquement (pas les textarea : description recette, étapes) */
.input:not(textarea) {
  height: 30px;
  padding-top: 0;
  padding-bottom: 0;
}

/* Champs multiligne (description recette, étapes) */
textarea.input {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  font-size: 14px;
  min-height: 50px;
}

/* iOS Safari zoome automatiquement la page au focus d'un champ dont la police fait
   moins de 16px — pas de zoom arrière fiable en JS après coup, la seule vraie
   correction est de passer sous ce seuil uniquement sur mobile (desktop garde 14px,
   le zoom auto n'existe pas sur ces navigateurs). */
@media (max-width: 899px) {
  .input {
    font-size: 16px;
  }
}


.divider {
  text-align: center;
  color: var(--text-muted);
  margin: 18px 0;
  font-size: 13px;
}

.form-error {
  color: var(--danger);
  font-size: 13px;
  min-height: 18px;
  margin: 4px 0 0;
}

.form-success {
  color: #34c759;
  font-size: 13px;
  min-height: 18px;
  margin: 4px 0 0;
}

.avatar-wrap {
  text-align: center;
  margin-bottom: 16px;
}

/* Cadre carré autour de la photo (image ou placeholder) : sert d'ancrage pour le badge
   caméra positionné en absolute (voir .avatar-camera-btn) — .avatar/.avatar--placeholder
   remplissent maintenant ce cadre à 100% au lieu de gérer eux-mêmes taille/centrage. */
.avatar-photo {
  position: relative;
  width: 96px;
  height: 96px;
  margin: 0 auto 12px;
}

/* Variante compacte pour une ligne de liste (.people-row, voir plus bas) : même cadre que
   .avatar-photo (photo ou repli silhouette), pas de badge caméra ni de centrage — 48px,
   même taille que la vignette .data-table__thumb (recettes) pour garder le même rythme
   visuel entre les listes de l'app. */
.avatar-photo--sm {
  position: relative;
  width: 48px;
  height: 48px;
  margin: 0;
  flex-shrink: 0;
}

.avatar-photo--sm .avatar--placeholder svg {
  width: 22px;
  height: 22px;
}

.avatar,
.avatar--placeholder {
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
}

.avatar--placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-alt);
  color: var(--text-muted);
}

.avatar--placeholder svg {
  width: 44px;
  height: 44px;
}

/* Badge caméra ("changer la photo"), en bas à droite de l'avatar — remplace l'ancien
   bouton texte .btn--neu. */
.avatar-camera-btn {
  position: absolute;
  bottom: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: 50%;
  border: 2px solid var(--surface);
  background: var(--surface);
  /* Noir -> bleu au survol, même transition que les autres icônes d'action de l'app. */
  color: var(--text);
  cursor: pointer;
  transition: color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .avatar-camera-btn:hover {
    color: var(--accent);
  }
}

.avatar-camera-btn svg {
  width: 16px;
  height: 16px;
}

.toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}

/* Sans accent-color, un input radio/checkbox natif garde le bleu par défaut du
   navigateur quel que soit le thème — invisible en clair (coïncide avec --accent bleu)
   mais détonne en sombre (pas la bonne nuance) et surtout en Îdiää (monochrome
   intégral, voir variables.css) : seule fuite de couleur restante, repérée à l'écran
   (radios de settings.php). */
input[type="radio"],
input[type="checkbox"] {
  accent-color: var(--accent);
}

.toggle-row:last-child {
  border-bottom: none;
}

/* recipe-edit.php : ".toggle-row" y est seul dans son ".form-row" (pas une liste de
   plusieurs toggles comme dans settings.php), donc la bordure séparatrice n'a pas lieu
   d'être — et ":last-child" ne s'applique pas puisque le champ thermostat est le
   dernier enfant du ".form-row", pas lui. Même padding/centrage vertical que le champ
   thermostat pour que les deux lignes s'alignent en desktop (voir ".form-row .form-group"). */
.form-row .toggle-row {
  border-bottom: none;
  justify-content: flex-start;
  gap: 20px;
}

#ovenTempGroup {
  align-items: center;
  padding: 12px 0;
}

/* .form-group définit display:flex (règle auteur), ce qui neutralise le display:none
   par défaut de l'attribut [hidden] (règle du user-agent stylesheet, toujours perdante
   face à une règle auteur) — même bug déjà rencontré/corrigé pour .th-filter__menu et
   .loading-overlay (voir plus bas). Sans ça, le champ thermostat reste visible même
   toggle "Utilise un four" désactivé. */
#ovenTempGroup[hidden] {
  display: none;
}

.toggle-row__label {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Corbeille "Supprimer les articles cochés" (shopping-list-view.php) — alignée avec le
   bord droit de la ligne (donc du tableau des articles juste en dessous, même largeur),
   demandé explicitement, SANS déplacer le switch (qui doit rester juste après le label,
   voir #shoppingListPage .toggle-row : justify-content:flex-start + gap:20px, réglage
   volontaire "Cuisson au four"). margin-left:auto sur ce SEUL bouton absorbe tout
   l'espace restant à sa gauche et le pousse jusqu'au bord — le switch, lui, n'a pas
   cette marge et reste packé contre le label comme avant.
   Couleur bleue + survol orange, demandé explicitement (remplace color:var(--text)/
   hover fond gris de .icon-btn générique) — même couple de couleurs que les autres
   icônes d'action de l'app (.icon-plus, #newArticleToggleBtn...). L'icône elle-même
   (idiaa_icon('delete')) suit déjà currentColor (stroke, pas fill), donc `color` suffit
   ici, contrairement aux icônes "+" brutes qui utilisent `fill`. */
#deleteCheckedItemsBtn {
  margin-left: auto;
  color: var(--accent);
}

@media (hover: hover) and (pointer: fine) {
  #deleteCheckedItemsBtn:hover {
    color: var(--svg-hover);
  }
}

.switch {
  position: relative;
  display: inline-block;
  width: 36px;
  height: 20px;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.switch__track {
  position: absolute;
  inset: 0;
  /* var(--border) est quasi invisible ici (à peine distinct de --surface une fois
     décoché) : --text-muted donne un vrai contraste tout en restant neutre. */
  background: var(--text-muted);
  border-radius: 999px;
  transition: 0.2s;
}

.switch__track::before {
  content: "";
  position: absolute;
  height: 16px;
  width: 16px;
  left: 2px;
  top: 2px;
  background: white;
  border-radius: 50%;
  transition: 0.2s;
}

/* Refonte couleurs sombre en cours (voir variables.css) — fond/bordure du toggle décoché
   (ex. "Cuisson au four") et couleur du rond, valeurs fixes demandées explicitement.
   L'état coché reste bleu fixe (#007aff, voir juste en dessous) — non concerné, décision
   déjà prise et documentée. */
/* box-shadow inset plutôt qu'un vrai border : un border ajoute à la containing-block
   utilisée par .switch__track::before (position:absolute, top/left:2px) — décalait le
   rond de 1px vers le bas/la droite (signalé : "le rond n'est plus centré, trop bas"),
   la valeur de top/left ne change pas mais la boîte à laquelle elle se réfère, elle,
   change. box-shadow ne participe jamais à la géométrie/au containing-block, purement
   visuel : même rendu bordé, sans décaler le rond. */
:root[data-theme="dark"] .switch__track {
  background: rgb(61, 61, 61);
  box-shadow: inset 0 0 0 1px rgb(92, 92, 92);
}

:root[data-theme="dark"] .switch__track::before {
  background: rgb(190, 190, 190);
}

/* Bleu FIXE (pas var(--accent)) — demandé explicitement pour les deux seuls toggles de
   ce composant ("Cuisson au four", recipe-edit.php ; "Masquer les articles cochés",
   shopping-list-view.php) : --accent vaut --text en mode Îdiää (monochrome intégral,
   voir variables.css), donc le toggle coché y aurait été noir/blanc plutôt que bleu.
   Même principe que .chat-composer__send-btn (couleur fixe, bypass --accent). */
.switch input:checked + .switch__track {
  background: #007aff;
}

.switch input:checked + .switch__track::before {
  transform: translateX(16px);
}

.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.4);
  align-items: center;
  justify-content: center;
  z-index: 50;
  padding: 20px;
}

.modal-overlay.is-open {
  display: flex;
}

.modal {
  width: 100%;
  max-width: 360px;
}

.modal h2 {
  margin-top: 0;
  font-size: 18px;
}

.list-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-radius: var(--radius-md);
  background: var(--surface);
  border: 1px solid var(--border);
  margin-bottom: 8px;
  color: var(--text);
  text-decoration: none;
}

/* Picker d'ingrédients : ligne = vignette + nom, pas de second élément à pousser à
   droite (contrairement à .list-item générique utilisée ailleurs, ex. friends.js). */
#ingredientPickerList .list-item {
  justify-content: flex-start;
  gap: 10px;
}

.list-item__meta {
  color: var(--text-muted);
  font-size: 13px;
}

.list-item__remove {
  border: none;
  background: transparent;
  color: var(--danger);
  font-size: 13px;
  cursor: pointer;
  padding: 4px 8px;
}

/* ---------- Ligne "personne" (avatar + nom + action) ---------- */
/* Même habillage que .list-item (fond, bordure, coins, ombre douce implicite via bordure)
   mais avec un avatar rond en tête et un emplacement optionnel pour une action à droite
   (bouton Accepter/Refuser/Annuler/Inviter, ou juste un aperçu de dernier message) — utilisé
   par la liste d'amis, les deux listes d'invitations (friends.php), les résultats de la
   popup d'ajout d'ami et la liste des conversations (chats.php). Un seul composant plutôt
   qu'une déclinaison par écran : ces quatre usages n'ont structurellement rien d'autre
   qu'un avatar + un nom + une zone de fin, pas de raison de dupliquer. */
.people-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-radius: var(--radius-md);
  background: var(--surface);
  border: 1px solid var(--border);
  margin-bottom: 8px;
  color: var(--text);
  text-decoration: none;
}

a.people-row:hover {
  border-color: var(--accent);
}

/* Ligne à case à cocher (popup "Partager" à plusieurs amis, shopping-lists.js) — la
   ligne ENTIÈRE est un <label> cliquable (coche l'<input> qu'elle enveloppe), demandé
   explicitement (même principe que .collections-dropdown__item, "Livres de recette").
   cursor:pointer explicite : .people-row de base ne le déclare pas (utilisée aussi bien
   en <a> qu'en <div> selon la page). */
.people-row--checkbox {
  cursor: pointer;
}

@media (hover: hover) and (pointer: fine) {
  .people-row--checkbox:hover {
    border-color: var(--accent);
  }
}

/* Case désactivée (ami pas Pro, popup d'invitation à un événement repas — voir
   meal-events.js/meal-event-view.js) : ligne visuellement inerte, pas de survol
   trompeur. */
.people-row--checkbox:has(input:disabled) {
  cursor: default;
  opacity: 0.5;
}

@media (hover: hover) and (pointer: fine) {
  .people-row--checkbox:has(input:disabled):hover {
    border-color: var(--border);
  }
}

/* Badge "message non lu" (friends.js) : même principe que .nav-badge-wrap (nav.css) —
   wrapper relatif dédié autour du seul avatar (pas toute la ligne), pour superposer le
   badge à son coin sans dépendre du reste de la ligne. .avatar-photo--sm est déjà
   position:relative (voir plus bas) mais ce n'est pas suffisant seul : avatarThumb()
   (avatar-format.js) ne laisse pas d'emplacement pour un enfant supplémentaire à
   l'intérieur de ce <div>, d'où ce wrapper posé autour depuis friends.js plutôt que dans
   avatarThumb() lui-même (qui reste générique, utilisé aussi là où ce badge n'a pas de
   sens — chats.php, popup profil...). */
.people-row__avatar-wrap {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
}

/* Couleur fixe (--unread-badge, variables.css) : reste bleue même en mode Îdiää
   monochrome, comme --danger ailleurs dans l'app — indicateur de notification, pas un
   élément soumis à --accent. */
.people-row__unread-badge {
  position: absolute;
  top: -2px;
  right: -10px;
  display: flex;
  align-items: center;
  justify-content: center;
  /* +60% par rapport à la taille d'origine (16px), demandé explicitement. */
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: var(--surface);
  color: var(--unread-badge);
}

.people-row__unread-badge svg {
  /* +60% par rapport à la taille d'origine (11px), demandé explicitement. */
  width: 18px;
  height: 18px;
}

.people-row__name {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Variante "deux lignes" (chats.php : nom + aperçu du dernier message) : colonne au lieu
   du simple span flex:1 de .people-row__name ci-dessus (usage à une seule ligne — liste
   d'amis, résultats de recherche). À l'intérieur, .people-row__name n'a plus besoin
   d'être lui-même flex:1 (déjà porté par ce wrapper). */
.people-row__info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.people-row__info .people-row__name {
  flex: none;
}

/* Aperçu du dernier message (chats.php), à l'intérieur de .people-row__info — sous le
   nom, même ligne verticale que .list-item__meta ailleurs dans l'app. */
.people-row__meta {
  display: block;
  color: var(--text-muted);
  font-size: 13px;
  font-weight: 400;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Horodatage en fin de ligne (chats.php) — hors de .people-row__info, ne doit jamais être
   compressé (texte toujours court, pas besoin d'ellipsis). */
.people-row__time {
  flex-shrink: 0;
  color: var(--text-muted);
  font-size: 12px;
}

/* Emplacement de fin (boutons Accepter/Refuser/Annuler/Inviter) : ne doit jamais être
   compressé par le nom (flex:1 ci-dessus), ni forcer la ligne à s'agrandir plus qu'il ne
   faut. */
.people-row__action {
  flex-shrink: 0;
  display: flex;
  gap: 8px;
}

/* ---------- Suppression d'un ami (liste d'amis, friends.php uniquement) ---------- */
/* Nouveau concept de liste (demandé explicitement) : l'ancienne icône de suppression
   visible au survol (flex sibling de .people-row-swipe__link, en fin de ligne) est
   remplacée par une icône menu À L'INTÉRIEUR de .people-row (voir .row-menu-trigger,
   plus haut dans ce fichier, et friends.js) — .people-row-swipe ne gère donc plus que le
   glissement tactile ici, le survol pointeur fin est entièrement pris en charge par
   .row-menu-trigger (opacity/pointer-events, règle scopée ci-dessous).
   Choix du mode de révélation tactile basé sur la CAPACITÉ D'ENTRÉE (hover/pointer), PAS
   sur la largeur d'écran (min/max-width:900px, utilisée partout ailleurs pour la mise en
   page sidebar/dock) : une tablette bascule en layout desktop (≥900px, sidebar visible)
   tout en étant pilotée au doigt, sans souris — voir CLAUDE.md / Détection tactile vs
   pointeur. */
.people-row-swipe {
  position: relative;
}

.people-row-swipe__link {
  /* .people-row (fond/bordure/coins/padding) posé sur ce même élément, voir friends.js —
     transform ci-dessous s'ajoute à son rendu habituel. */
  transition: transform 0.2s ease;
}

/* Pendant le glissement actif (voir friends.js) : pas de transition, sinon chaque micro-
   déplacement du doigt serait retardé de 0.2s et le suivi paraîtrait mou. Réactivée
   (classe retirée) au relâchement, pour l'animation d'accroche finale. */
.people-row-swipe.is-dragging .people-row-swipe__link {
  transition: none;
}

/* Révèle .row-menu-trigger (masqué par défaut sur pointeur fin, voir sa règle générique
   plus haut dans ce fichier) au survol de la ligne — ou tant que son propre menu reste
   ouvert (.row-menu-open, posée par row-menu.js). */
@media (hover: hover) and (pointer: fine) {
  .people-row-swipe:hover .row-menu-trigger,
  .people-row-swipe:focus-within .row-menu-trigger,
  .people-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  /* Bug corrigé (signalé par l'utilisateur, capture à l'appui) : sans cette règle,
     .people-row-swipe__actions (glissement tactile) restait un <div> normal, affiché en
     flux sous la ligne sur pointeur fin — visible en texte brut ("Clear conversation",
     repli anglais faute d'i18n chargé) au lieu d'être masqué. Oubliée ici lors de la
     généralisation (présente sur les 3 autres listes : .recipe-row-swipe__actions,
     .shopping-list-row-swipe__actions, .shopping-item-row-swipe__actions) — le menu
     s'en charge à la place sur pointeur fin, pas de colonne/espace réservé pour elle
     non plus ici (contexte flex, pas table). */
  .people-row-swipe__actions {
    display: none;
  }
}

.people-row-swipe__action {
  border: none;
  cursor: pointer;
  padding: 0;
}

@media (hover: none), (pointer: coarse) {
  .people-row-swipe {
    overflow: hidden;
    /* Même arrondi que .people-row (var(--radius-md)) : sans ça, les coins de la ligne
       qui glisse dépasseraient du fond révélé en dessous aux coins de .people-row-swipe. */
    border-radius: var(--radius-md);
    margin-bottom: 8px;
  }

  .people-row-swipe .people-row {
    /* La marge basse passe sur .people-row-swipe ci-dessus : sinon un espace de 8px
       resterait visible (fond de la ligne) entre le bas de .people-row (translaté) et le
       bas de .people-row-swipe, au lieu que le fond révélé occupe toute la hauteur. */
    margin-bottom: 0;
  }

  /* --reveal : largeur réelle de .people-row-swipe__actions (1 seule action ici, mais
     généralisé — voir swipe-row.js), posée en JS à l'ouverture. 64px de repli tant que
     JS n'a rien posé (identique à l'ancienne valeur fixe, avant généralisation). */
  .people-row-swipe.is-swiped .people-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 64px)));
  }

  .people-row-swipe__actions {
    display: flex;
    position: absolute;
    inset: 0 0 0 auto;
    /* Fond rouge, demandé explicitement — ne révèle plus qu'une seule action
       (Supprimer, voir friends.js), fond neutre plus nécessaire maintenant que ce
       n'est plus un choix entre plusieurs actions. */
    background: var(--danger);
    /* translateX(100%) : décale le bloc de SA PROPRE largeur (pas une valeur fixe codée
       en dur) — fonctionne quel que soit le nombre d'actions qu'il contient, PAS juste
       masqué derrière la ligne : en mode Îdiää, .people-row (posé sur
       .people-row-swipe__link) a un fond translucide (glassmorphism, voir
       variables.css) — le bloc resterait visible en transparence même correctement
       empilée en z-index (piège déjà rencontré ici avant généralisation). En le sortant
       réellement du cadre, c'est .people-row-swipe (overflow:hidden) qui le coupe, plus
       de dépendance à l'opacité d'un calque au-dessus. */
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .people-row-swipe.is-dragging .people-row-swipe__actions {
    transition: none;
  }

  .people-row-swipe.is-swiped .people-row-swipe__actions {
    transform: translateX(0);
  }

  .people-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    color: var(--accent-contrast);
  }

  .people-row-swipe__action svg {
    width: 28px;
    height: 28px;
  }
}

/* Section "invitations en attente" (friends.php) : masquée entièrement (hidden) tant que
   les deux sous-listes sont vides — voir friends.js. Chaque sous-liste n'ajoute son titre
   que si elle a du contenu (pas de "Envoyées" vide affiché seul). */
.friend-requests {
  margin-bottom: 20px;
}

.friend-requests h3 {
  margin: 0 0 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-muted);
}

.friend-requests > div:not(:empty) {
  margin-bottom: 16px;
}

.icon-picker {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
  margin: 10px 0;
}

.icon-picker__item {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-alt);
  color: var(--text-muted);
  cursor: pointer;
}

.icon-picker__item svg {
  width: 22px;
  height: 22px;
}

.icon-picker__item.is-selected {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--surface);
}

/* ---------- Listes de courses (shopping-lists.php) ---------- */
/* Liste des listes : même .data-table que la liste des recettes (index.php), voir
   shopping-lists.js — .data-table__thumb accepte déjà un <svg> de repli à la place d'un
   <img> (voir sa définition plus bas), pas de classe dédiée nécessaire ici. */

/* Partager/Supprimer une liste : nouveau concept de liste (demandé explicitement, voir
   CLAUDE.md) — icône menu (.row-menu-trigger, voir plus haut) À L'INTÉRIEUR de la ligne
   sur pointeur fin, mêmes actions révélées par glissement sur tactile (swipe-row.js) —
   remplace l'ancienne icône de suppression seule, positionnée EN DEHORS du tableau
   (.shopping-list-delete-outside, JS-positionnée dans une marge réservée — retirée avec
   ce changement, plus nécessaire : le menu vit dans une cellule normale du tableau,
   aucune contrainte d'échapper à son overflow:hidden). Même famille de technique que
   .recipe-row-swipe (index.php) : VRAIE ligne de tableau (<tr>/<td>, pas un <div>) —
   linkSelector correspond ici à PLUSIEURS <td> (vignette/nom/nombre) qui glissent
   ensemble (voir swipe-row.js). Pas de position:relative ici (sur le <tr>) : voir
   .shopping-list-row-swipe__actions plus bas pour pourquoi ce rôle est porté par la
   cellule d'actions elle-même plutôt que par la ligne. */

.shopping-list-row-swipe__link {
  transition: transform 0.2s ease;
}

/* "Partagé avec ..." sous le nom de la liste (shopping-lists.js), demandé explicitement
   — couleur/taille exactes fournies : gris RGB(119,112,111) (littéral, pas un token —
   même famille de choix que les couleurs exactes du mode sombre, voir CLAUDE.md),
   taille de police 20% plus petite que le 14px de base de .data-table td (14 * 0.8 =
   11.2px). display:block pour passer à la ligne sous le lien (tous deux inline par
   défaut sinon). */
.shopping-list-shared-with {
  display: block;
  color: rgb(119, 112, 111);
  font-size: 11.2px;
}

.shopping-list-row-swipe.is-dragging .shopping-list-row-swipe__link {
  transition: none;
}

/* td.xxx (élément + classe), pas juste .xxx : .data-table th/td (élément + classe aussi)
   règle la base en text-align:left avec la même spécificité — sans l'élément ici, cette
   règle perdait silencieusement (repéré en vérifiant le décalage de .row-menu-trigger,
   resté sans effet). text-align:right (pas center) : sans ça, la marge-droite posée sur
   .row-menu-trigger pour le décaler du bord n'aurait aucun effet visible (un élément
   centré n'est pas "collé" à un bord dont on pourrait l'écarter). */
td.shopping-list-row-swipe__menu {
  text-align: right;
}

@media (hover: hover) and (pointer: fine) {
  .shopping-list-row-swipe:hover .row-menu-trigger,
  .shopping-list-row-swipe:focus-within .row-menu-trigger,
  .shopping-list-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  /* Cellule de glissement tactile : ne sert pas ici (voir CLAUDE.md / Détection tactile
     vs pointeur) — le menu s'en charge, pas de colonne réservée pour elle non plus (pas
     de <col> dédié dans le colgroup, voir shopping-lists.php). */
  .shopping-list-row-swipe__actions {
    display: none;
  }
}

.shopping-list-row-swipe__action {
  border: none;
  /* Même correctif que .recipe-row-swipe__action (voir son commentaire, style.css) —
     sans ça, le fond opaque par défaut du bouton masquait le coin arrondi du conteneur. */
  background: transparent;
  cursor: pointer;
  padding: 0;
}

/* Positionne .shopping-list-row-swipe__actions-inner (voir plus bas) par rapport à
   CETTE cellule plutôt qu'à .shopping-list-row-swipe (le <tr>) — même bug iPhone/WebKit
   que .recipe-row-swipe__actions (voir son commentaire, style.css), corrigé de la même
   façon ici. */
.shopping-list-row-swipe__actions {
  position: relative;
}

/* Mobile/tablette : sortie du flux de la table (position:absolute) et superposée au bord
   droit de la ligne, révélée par glissement — .is-swiped posée en JS (swipe-row.js).
   Clippée par overflow:hidden de .data-table (déjà présent, pour ses coins arrondis)
   tant qu'elle n'est pas révélée. */
@media (hover: none), (pointer: coarse) {
  /* --reveal : largeur réelle de .shopping-list-row-swipe__actions-inner (1 ou 2 actions
     selon que la liste m'appartient ou non, voir shopping-lists.js), posée en JS à
     l'ouverture. 128px (64px × 2) de repli tant que JS n'a rien posé. */
  .shopping-list-row-swipe.is-swiped .shopping-list-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 128px)));
  }

  .shopping-list-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    /* top/bottom légèrement dépassants (-1px, pas 0) : couvre toute bordure/liseré de la
       ligne en dessous jusqu'au bord exact, y compris sous le coin arrondi — sans ça, un
       filet de la bordure de séparation des lignes pouvait rester visible pile à la
       jonction avec l'arrondi (repéré à l'usage, voir CLAUDE.md). */
    inset: -1px 0 -1px auto;
    /* Fond rouge, demandé explicitement — ne révèle plus qu'une seule action
       (Supprimer, voir shopping-lists.js), fond neutre "inversé" plus nécessaire
       maintenant que ce n'est plus un choix entre plusieurs actions. */
    background: var(--danger);
    /* Coins arrondis côté gauche seulement — même essai que .recipe-row-swipe__actions-inner
       (voir son commentaire, style.css). */
    border-radius: 15px 0 0 15px;
    /* translateX(100%) : décale le bloc de SA PROPRE largeur, quel que soit le nombre
       d'actions qu'il contient — voir le même choix sur .people-row-swipe__actions
       (friends.js) pour le détail (piège du glassmorphism Îdiää). */
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .shopping-list-row-swipe.is-dragging .shopping-list-row-swipe__actions-inner {
    transition: none;
  }

  .shopping-list-row-swipe.is-swiped .shopping-list-row-swipe__actions-inner {
    transform: translateX(0);
  }

  .shopping-list-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    color: var(--accent-contrast);
  }

  /* Partager/Supprimer : les deux en icône désormais (rowMenuShareIconSvg, même tracé
     que le menu contextuel ⋮, voir shopping-lists.js — Supprimer avait déjà
     deleteListIconSvg). */
  .shopping-list-row-swipe__action svg {
    width: 24px;
    height: 24px;
  }
}

/* ---------- Vue d'une liste de courses (shopping-list-view.php) ---------- */
.shopping-list-view-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.shopping-list-view-header h1 {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: 22px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.shopping-list-view-header__actions {
  display: flex;
  flex-shrink: 0;
  gap: 8px;
}

/* Bug signalé (capture à l'appui) : une ligne horizontale parasite apparaissait collée
   au-dessus du tableau des articles, juste sous "Masquer les articles cochés" — c'est en
   fait le border-bottom de base de .toggle-row (pensé pour une LISTE de toggles,
   settings.php, séparés les uns des autres, dernier sans bordure via :last-child) : ici
   ".toggle-row" est seule mais n'est PAS le dernier enfant de #shoppingListPage (le
   tableau des articles suit), donc :last-child ne s'applique jamais et la bordure reste
   — visible comme un séparateur collé au tableau. Même correctif que ".form-row
   .toggle-row" (recipe-edit.php, "Cuisson au four") : bordure retirée, toggle ramené
   contre le texte avec un gap de 20px au lieu d'être poussé à l'opposé
   (justify-content:space-between par défaut) — demandé explicitement, même rendu que
   "Cuisson au four" (voir screenshot). */
#shoppingListPage .toggle-row {
  border-bottom: none;
  justify-content: flex-start;
  gap: 20px;
}

.shopping-list-view-header__actions button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: none;
  background: none;
  padding: 0;
  cursor: pointer;
}

/* :not(.icon-plus) — bug trouvé en vérifiant l'animation du "+" (#articleAddBtn, demandé
   explicitement "même animation que celui d'ajout de recette") : ce bouton partage ce
   conteneur avec #shoppingListShareBtn (partager), qui doit lui virer à var(--accent) au
   survol comme les autres icônes de la topbar/popup profil — mais réutilisée telle
   quelle, cette règle plus spécifique que .icon-plus/.icon-plus:hover (classe + élément +
   pseudo, contre classe + pseudo) l'emportait AUSSI sur #articleAddBtn, écrasant sa
   couleur de base (var(--accent), bleu en clair/sombre) et sa couleur de survol
   (var(--svg-hover), orange) par var(--text)/var(--accent) — invisible en mode Îdiää où
   var(--accent) vaut déjà var(--text) (d'où le bug repéré précisément dans ce thème),
   mais déjà présent dans tous les thèmes (vérifié : #articleAddBtn rendu noir par défaut
   en clair au lieu du bleu attendu). #articleAddBtn n'a donc plus besoin d'aucune couleur
   ici, .icon-plus/.icon-plus:hover (style.css, plus haut) les fournit entièrement. */
.shopping-list-view-header__actions button:not(.icon-plus) {
  color: var(--text);
  transition: color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .shopping-list-view-header__actions button:not(.icon-plus):hover {
    color: var(--accent);
  }
}

.shopping-list-view-header__actions svg {
  width: 100%;
  height: 100%;
}

/* Liste des articles (shopping-list-view.php) : demandé explicitement "même forme que la
   liste des recettes" (.data-table, index.php) — un seul <table> plutôt qu'une pile de
   cards par catégorie (ancien .shopping-category-section/<h3>, supprimés). La catégorie
   devient une ligne d'en-tête À L'INTÉRIEUR du tableau (.shopping-category-row, voir plus
   bas) — demandé ainsi ("en entête la catégorie d'article") — donc pas de <thead> de
   colonnes séparé, ces en-têtes de catégorie en tiennent lieu. Une seule catégorie
   affichée si elle contient au moins un article (voir shopping-list-view.js /
   resolveCategoryId — Non classé toujours en dernier). */
.shopping-items-table {
  width: 100%;
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--surface);
  border-radius: 8px;
  border: 1px solid var(--text-muted);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  margin-bottom: 16px;
}

/* Refonte couleurs sombre en cours (voir variables.css / .data-table) — même traitement
   que .data-table pour cette liste. */
:root[data-theme="dark"] .shopping-items-table {
  background: rgb(43, 43, 43);
}

/* Mêmes valeurs que .data-table (liste des recettes) — classe dédiée plutôt que
   réutilisée telle quelle : nombre/ordre de colonnes différents (case à cocher +
   vignette + nom + quantité + suppression, contre vignette + nom + type + ... pour les
   recettes) — les règles column-spécifiques de .data-table (nth-child centré, padding
   du premier/second enfant) s'appliqueraient au mauvais endroit ici. Même principe que
   .recipe-ingredients, harmonisée avec .data-table mais gardée séparée (voir CLAUDE.md /
   Tableaux). */
.shopping-items-table th,
.shopping-items-table td {
  padding: 12px;
  text-align: left;
  vertical-align: middle;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}

.shopping-items-table tbody tr:last-child td {
  border-bottom: none;
}

.shopping-items-table__col--check {
  width: 40px;
}

.shopping-items-table__col--thumb {
  width: 60px;
}

/* width:100%, pas un <col> sans largeur — même correctif que .data-table__col--name
   (liste des recettes) pour le même bug iPhone potentiel (Safari/WebKit n'étend pas
   forcément un <col> sans largeur jusqu'au bord du tableau en table-layout:fixed). */
.shopping-items-table__col--name {
  width: 100%;
}

.shopping-items-table__col--qty {
  width: 90px;
}

/* Icône menu (⋮) — visible en tactile désormais aussi (voir CLAUDE.md / Détection
   tactile vs pointeur, le "⋮" ouvre le même menu contextuel complet qu'au clic sur
   desktop), donc réservée dans TOUS les cas maintenant, plus seulement sur pointeur
   fin. Bug rencontré en généralisant : sans cette réservation en tactile, la cellule du
   "⋮" restait forcée à 0px (table-layout:fixed) alors que le bouton, lui, redevenait
   visible/interactif — l'icône débordait alors hors de sa cellule sans être peinte
   correctement (piège Chromium déjà connu, voir le commentaire juste en dessous),
   invisible et non cliquable en pratique malgré des styles calculés corrects. */
.shopping-items-table__col--menu {
  width: 58px;
}

/* Colonne dédiée pour .shopping-item-row-swipe__actions (6e <td>, jamais dans le flux
   normal — display:none sur pointeur fin, position:absolute sur tactile) : largeur 0
   dans tous les cas, mais DÉCLARÉE (voir shopping-list-view.php pour le piège rencontré
   sans elle : une cellule "en trop" au-delà du nombre de <col>, combinée à une ligne
   d'en-tête de catégorie à colspan juste au-dessus, faussait le calcul de largeur de
   TOUTES les colonnes de la ligne suivante dans Chromium). */
.shopping-items-table__col--actions {
  width: 0;
}

/* Quantité : moins de place nécessaire sur petit écran (colonne nom déjà comprimée par
   case à cocher + vignette + quantité) — même seuil que le reste de l'app (900px,
   desktop/mobile), pas hover/pointer (question de place, pas de capacité d'entrée). */
@media (max-width: 899px) {
  .shopping-items-table__col--qty {
    width: 60px;
  }
}

/* En-tête de catégorie : même apparence que l'en-tête de colonnes de .data-table (fond
   var(--surface-alt), 600/15px) — pas cliquable/survolable comme une ligne d'article,
   volontairement exclue des règles de survol plus bas (scopées à
   .shopping-item-row-swipe, pas à "tbody tr" en général). */
.shopping-category-row th {
  color: var(--text);
  font-weight: 600;
  font-size: 15px;
  background: var(--surface-alt);
}

/* Refonte couleurs sombre en cours (voir variables.css / .data-table th) — même
   traitement pour ce header "de groupe" (une ligne .shopping-category-row par
   catégorie, à l'intérieur du tableau plutôt qu'un <thead> unique en tête). */
:root[data-theme="dark"] .shopping-category-row th {
  background: rgb(95, 95, 95);
}

/* Ligne "article" : VRAIE ligne de tableau (<tr>/<td>, pas un <div>) — même mécanique que
   .shopping-list-row-swipe (shopping-lists.js) et .people-row-swipe (friends.js), voir
   CLAUDE.md / Détection tactile vs pointeur. data-checked porté par le wrapper (pas
   juste la case à cocher) pour permettre le filtre CSS "masquer les cochés" plus bas sans
   avoir à interroger l'état de l'input. Pas de position:relative ici (sur le <tr>) : voir
   .shopping-item-row-swipe__actions plus bas pour pourquoi ce rôle est porté par la
   cellule d'actions elle-même plutôt que par la ligne. */

.shopping-item-row-swipe__link {
  transition: transform 0.2s ease;
}

.shopping-item-row-swipe.is-dragging .shopping-item-row-swipe__link {
  transition: none;
}

.shopping-item-row__checkbox {
  width: 20px;
  height: 20px;
  cursor: pointer;
}

.shopping-item-row__thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  color: var(--text-muted);
  overflow: hidden;
}

.shopping-item-row__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.shopping-item-row__thumb svg {
  width: 20px;
  height: 20px;
}

/* display:block (pas le <span> inline par défaut) : nécessaire pour que
   overflow/text-overflow/white-space (troncature "...") fassent effet de façon fiable —
   avant, .shopping-item-row__name comptait sur son rôle d'ENFANT FLEX (flex:1, min-width:0)
   pour sa contrainte de largeur ; dans un <td>, ce contexte n'existe plus, la largeur
   vient du <col> (celle-ci, sans classe dédiée, absorbe l'espace restant — même principe
   que la colonne "Recette" de .data-table). */
.shopping-item-row__name {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Article coché : nom barré/atténué (repli purement visuel — l'ordre/le groupe ne
   changent pas tant que "masquer les cochés" n'est pas activé, voir plus bas). */
.shopping-item-row-swipe[data-checked="true"] .shopping-item-row__name {
  color: var(--text-muted);
  text-decoration: line-through;
}

.shopping-item-row__qty {
  font-size: 13px;
  color: var(--text-muted);
}

/* td.xxx (élément + classe), pas juste .xxx : .shopping-items-table th/td (élément +
   classe aussi) règle la base en text-align:left avec la même spécificité — voir le même
   correctif sur .shopping-list-row-swipe__menu (style.css) pour le détail du piège.
   text-align:right (pas center) : la marge-droite de .row-menu-trigger a besoin d'un
   bord droit réel à écarter. */
td.shopping-item-row-swipe__menu {
  text-align: right;
}

@media (hover: hover) and (pointer: fine) {
  .shopping-item-row-swipe:hover .row-menu-trigger,
  .shopping-item-row-swipe:focus-within .row-menu-trigger,
  .shopping-item-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  /* Cellule de glissement tactile : ne sert pas ici (voir CLAUDE.md / Détection tactile
     vs pointeur) — le menu s'en charge, pas de colonne réservée pour elle non plus (pas
     de <col> dédié dans le colgroup, voir shopping-list-view.php). */
  .shopping-item-row-swipe__actions {
    display: none;
  }
}

.shopping-item-row-swipe__action {
  border: none;
  /* Même correctif que .recipe-row-swipe__action (voir son commentaire, style.css) —
     sans ça, le fond opaque par défaut du bouton masquait le coin arrondi du conteneur. */
  background: transparent;
  cursor: pointer;
  padding: 0;
}

/* Positionne .shopping-item-row-swipe__actions-inner (voir plus bas) par rapport à
   CETTE cellule plutôt qu'à .shopping-item-row-swipe (le <tr>) — même bug iPhone/WebKit
   que .recipe-row-swipe__actions (voir son commentaire, style.css), corrigé de la même
   façon ici. */
.shopping-item-row-swipe__actions {
  position: relative;
}

@media (hover: none), (pointer: coarse) {
  /* --reveal : largeur réelle de .shopping-item-row-swipe__actions-inner (une seule
     action ici, "Supprimer"), posée en JS à l'ouverture — voir swipe-row.js. 64px de
     repli tant que JS n'a rien posé (identique à l'ancienne valeur fixe, avant
     généralisation). */
  .shopping-item-row-swipe.is-swiped .shopping-item-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 64px)));
  }

  .shopping-item-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    /* top/bottom légèrement dépassants (-1px, pas 0) — voir le commentaire équivalent sur
       .shopping-list-row-swipe__actions-inner (style.css) pour la raison. */
    inset: -1px 0 -1px auto;
    /* Fond rouge, icône blanche (rowMenuDeleteIconSvg) — demandé explicitement. */
    background: var(--danger);
    /* Coins arrondis côté gauche seulement — même essai que .recipe-row-swipe__actions-inner
       (voir son commentaire, style.css). */
    border-radius: 15px 0 0 15px;
    /* translateX(100%) : décale le bloc de SA PROPRE largeur — voir le même choix sur
       .people-row-swipe__actions (friends.js) pour le détail (piège du glassmorphism
       Îdiää). */
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .shopping-item-row-swipe.is-dragging .shopping-item-row-swipe__actions-inner {
    transition: none;
  }

  .shopping-item-row-swipe.is-swiped .shopping-item-row-swipe__actions-inner {
    transform: translateX(0);
  }

  .shopping-item-row-swipe__action {
    color: var(--accent-contrast);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 64px;
  }

  .shopping-item-row-swipe__action svg {
    width: 28px;
    height: 28px;
  }
}

/* Toggle "masquer les articles cochés" (préférence locale par appareil, localStorage —
   pas dans Firestore, voir CLAUDE.md) : classe posée par shopping-list-view.js sur le
   conteneur englobant (désormais un <tbody>, voir shopping-list-view.php), pur CSS, pas
   de re-render du DOM à chaque bascule. display:none sur un <tr> masque bien la ligne
   entière. */
#shoppingCategories.is-hiding-checked .shopping-item-row-swipe[data-checked="true"] {
  display: none;
}

/* ---------- Popup de sélection d'article (bibliothèque de produits) ---------- */
/* Même famille que .ingredient-selector/.ingredient-list (picker d'ingrédient,
   recipe-edit.php) — classes dédiées plutôt que réutilisées telles quelles, même
   convention que le reste du projet (popups sans rapport ne partagent pas un nom de
   classe, même rendu identique). */
.article-selector {
  display: none;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  z-index: 100;
  padding: 20px;
}

.article-selector.is-open {
  display: flex;
}

.article-list {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 420px;
  max-height: 80vh;
  overflow: hidden;
  background-color: var(--surface);
  border: 1px solid var(--text-muted);
  border-radius: 8px;
  padding: 10px;
  box-shadow: 6px 6px 12px var(--shadow-neu);
}

.article-list .idiaa-field {
  margin: 0 0 10px;
}

/* Zone qui défile de la popup "Nouveau produit" (#newArticleForm/.article-list__body) —
   demandé explicitement : si le contenu (surtout la zone photo) dépasse la hauteur de
   la popup (max-height:80vh sur .article-list, overflow:hidden), il doit être possible
   de défiler jusqu'en bas plutôt que le voir coupé. overflow-x:hidden : filet de
   sécurité générique contre toute scrollbar horizontale transitoire (ex. un hover avec
   scale() sur un enfant), même précaution que .ingredient-list .zone-liste-ingr. Plain
   block flow à l'intérieur (pas display:flex) — chaque enfant garde sa propre marge
   comme avant l'ajout de ce wrapper. */
.article-list__body {
  overflow-y: auto;
  overflow-x: hidden;
  flex: 1 1 auto;
  min-height: 0;
}

/* Largeur alignée sur celle du champ "Nom du produit" (#newArticleName, même
   .article-list__body — 100% par défaut, comme tous les autres champs de cette
   popup) — demandé explicitement, remplace la largeur fixe 358px précédente
   (calée à l'époque sur #newIngredientImageZone, recipe-edit.php) qui la rendait
   plus étroite que le reste du formulaire. margin-bottom:20px (gap avec "Ajouter"
   sous elle) inchangé. */
#newArticleImageZone {
  margin-bottom: 20px;
}

/* Espace de 20px entre "Catégorie" et la zone photo juste en dessous, demandé
   explicitement — remplace les 10px par défaut de .article-list .idiaa-field
   (partagée par tous les champs de cette popup, voir plus haut) pour ce champ
   précis seulement. :has(#newArticleCategory) (id unique) plutôt qu'une classe
   dédiée : gagne la cascade sur .article-list .idiaa-field (2 classes) grâce à la
   spécificité de l'id porté par :has(), sans dépendre de l'ordre des règles. */
.form-group:has(#newArticleCategory) {
  margin-bottom: 20px;
}

/* .form-error (#newArticleError) réserve 22px (min-height:18px + margin-top:4px, voir
   sa règle de base) même vide — fausserait le gap de 13px demandé entre "Ajouter" et
   "Fermer" si elle restait dans le flux. Même technique que #profileForm/#emailForm/
   #passwordForm .form-error:empty (voir Popup profil, CLAUDE.md) : masquée tant que
   vide, réapparaît normalement dès qu'un message d'erreur y est posé. */
#newArticleForm .form-error:empty {
  display: none;
}

/* Quantité/Unité sur la même ligne (popup "Nouveau produit", shopping-list-view.php),
   demandé explicitement — toujours en ligne, pas seulement à partir de 900px comme
   .form-row (recipe-edit.php, pensé pour une page large qui peut aussi s'afficher en
   colonne sur petit écran) : cette popup reste une colonne étroite (max-width:420px)
   quel que soit le viewport, donc pas besoin/pas de sens à empiler ces deux champs même
   sur mobile. */
.new-article-qty-row {
  display: flex;
  gap: 16px;
}

.new-article-qty-row .idiaa-field {
  flex: 1;
  min-width: 0;
}

/* Unité + catégorie du formulaire "Nouvel ingrédient" (recipe-edit.php) — mêmes règles
   que .new-article-qty-row ci-dessus (toujours en ligne, popup toujours étroite quel que
   soit le viewport), classe séparée par cohérence avec le reste du projet (une classe par
   composant, même quand les règles sont identiques — voir CLAUDE.md / Tableaux pour la
   même remarque sur .recipe-ingredients vs .data-table). */
.new-ingredient-row {
  display: flex;
  gap: 16px;
}

.new-ingredient-row .idiaa-field {
  flex: 1;
  min-width: 0;
}

/* Sélecteur de période (popup "Nouvel événement" ET page dédiée mobile
   meal-event-create.php, même markup/mêmes ids) — date de début + date de fin côte à
   côte, même principe que .new-ingredient-row (unité/catégorie du picker
   d'ingrédient) : deux champs de largeur égale, jamais l'un des deux qui déborde.
   margin-bottom:20px (espacement demandé explicitement avec la ligne "Choisir une
   image :" juste en dessous) porté par le conteneur plutôt que par les champs
   eux-mêmes : voir .meal-event-date-range .idiaa-field ci-dessous, qui remet leur
   propre margin-bottom à 0 pour ne jamais cumuler les deux. */
.meal-event-date-range {
  display: flex;
  gap: 16px;
  margin-bottom: 20px;
}

/* margin-bottom:0 gagne par spécificité (2 classes) contre .form-group (1 classe,
   16px — appliqué hors popup, sur meal-event-create.php) comme contre
   .list-create-panel .idiaa-field (2 classes aussi mais déclarée plus tard dans le
   fichier, voir juste en dessous pour ce cas précis dans la popup). */
.meal-event-date-range .idiaa-field {
  flex: 1;
  min-width: 0;
  margin-bottom: 0;
}

/* Annule le margin-bottom:10px de .list-create-panel .idiaa-field (partagé par tous
   les champs de cette popup, voir plus bas) pour CES deux champs précis, DANS LA
   POPUP uniquement (meal-event-create.php n'a pas .list-create-panel, la règle
   ci-dessus suffit déjà là-bas) — la marge entre le groupe de dates et la suite vient
   de .meal-event-date-range lui-même (20px, ci-dessus), jamais des champs qu'il
   contient (sinon 10+20=30px). 3 classes (vs 2 pour .list-create-panel .idiaa-field) :
   gagne la cascade indépendamment de l'ordre des règles dans le fichier. */
.list-create-panel .meal-event-date-range .idiaa-field {
  margin-bottom: 0;
}

.article-list .zone-search-articles {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding-bottom: 10px;
}

.article-list .zone-search-articles .input {
  flex: 1;
  margin-bottom: 0;
}

.article-list .zone-search-articles svg {
  height: 26px;
  width: 26px;
  flex-shrink: 0;
  fill: var(--accent);
  cursor: pointer;
  transition: 0.2s;
}

@media (hover: hover) and (pointer: fine) {
  .article-list .zone-search-articles svg:hover {
    fill: var(--svg-hover);
    transform: scale(1.15);
  }
}

.article-list .zone-liste-articles {
  overflow-y: auto;
  max-height: 420px;
}

.zone-liste-articles .shopping-article-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  padding: 10px;
  border-radius: 8px;
  transition: background-color 0.2s;
  border-bottom: 1px solid var(--border);
}

.zone-liste-articles .shopping-article-item:hover {
  background-color: var(--accent-tint);
}

.zone-liste-articles .shopping-article-item img,
.zone-liste-articles .shopping-article-item svg:not(.shopping-article-item__add) {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  object-fit: contain;
  color: var(--text-muted);
}

.zone-liste-articles .shopping-article-item .nom_article {
  flex: 1;
  font-size: 14px;
  color: var(--text);
}

.zone-liste-articles .shopping-article-item .input-qte {
  flex-grow: 0;
  width: 60px;
  height: 30px;
  border: 1px solid var(--text-muted);
  border-radius: var(--radius-sm);
  outline: none;
  padding: 5px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  font-size: 14px;
  color: var(--text);
  background-color: var(--surface);
  box-sizing: border-box;
  transition: .3s;
}

.zone-liste-articles .shopping-article-item .input-qte:focus {
  border-color: var(--accent);
  box-shadow: 0 0 5px 0 var(--accent);
}

@media (max-width: 899px) {
  .zone-liste-articles .shopping-article-item .input-qte {
    font-size: 16px;
  }
}

/* Même refonte sombre que .zone-liste-ingr .ingredient-item .input-qte (recipe-edit.php)
   — ce champ (quantité d'un article, shopping-list-view.php) est la même classe
   .input-qte mais déclarée séparément (sélecteur différent), donc jamais couverte par
   cette règle : oubliée lors de la refonte sombre initiale, corrigée ici pour que
   TOUS les champs de saisie de l'app (demandé explicitement, y compris la recherche)
   restent cohérents entre eux. */
:root[data-theme="dark"] .zone-liste-articles .shopping-article-item .input-qte {
  background-color: rgb(52, 53, 52);
  border-color: rgb(119, 111, 112);
}

.zone-liste-articles .shopping-article-item .qte_article {
  font-size: 13px;
  color: var(--text-muted);
  width: 48px;
  flex-shrink: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.zone-liste-articles .shopping-article-item .shopping-article-item__action {
  position: relative;
  height: 26px;
  width: 26px;
  flex-shrink: 0;
}

.zone-liste-articles .shopping-article-item .shopping-article-item__action svg {
  position: absolute;
  inset: 0;
  height: 100%;
  width: 100%;
  transition: opacity 0.3s ease;
}

.zone-liste-articles .shopping-article-item .shopping-article-item__add {
  color: var(--accent);
  cursor: pointer;
  opacity: 1;
  transition: color 0.2s ease, transform 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .zone-liste-articles .shopping-article-item .shopping-article-item__add:hover {
    color: var(--svg-hover);
    transform: scale(1.15);
  }
}

.zone-liste-articles .shopping-article-item .shopping-article-item__check {
  opacity: 0;
  pointer-events: none;
}

.zone-liste-articles .shopping-article-item .shopping-article-item__action.is-added {
  pointer-events: none;
}

.zone-liste-articles .shopping-article-item .shopping-article-item__action.is-added .shopping-article-item__add {
  opacity: 0;
}

.zone-liste-articles .shopping-article-item .shopping-article-item__action.is-added .shopping-article-item__check {
  opacity: 1;
}

/* Hauteur 40px : même hauteur que "Enregistrer" sur recipe-edit.php (.form-actions
   .btn--neu, jamais surchargée là-bas), demandé explicitement — remplace les 30px
   précédents. Pas besoin d'agrandir .article-list en plus : le panneau n'a qu'un
   max-height:80vh (pas de hauteur fixe, voir sa définition plus haut), il absorbe donc
   ces +10px tout seul comme n'importe quel autre changement de contenu — .zone-liste-
   articles garde son propre max-height:420px indépendant, pas concerné non plus.
   Gap avec la liste (.zone-liste-articles) porté à 20px (margin-top, remplace les
   10px précédents), demandé explicitement. Bordure basse du bouton à 20px de la
   bordure basse de la popup, demandé explicitement : ce bouton est le dernier enfant
   de .article-list (pas de wrapper .article-list__body ici, contrairement à
   #newArticleForm), qui a déjà 10px de padding propre (voir sa définition plus haut) —
   margin-bottom:10px posé ici s'ajoute à ce padding pour les 20px voulus (10+10),
   plutôt que 20px qui en ferait 30 au total. */
/* #articlePickerCancel/#newArticleCancel/#createArticleBtn — HISTORIQUE : décrivaient
   ici les boutons "Fermer"/"Ajouter" pleine largeur en bas des popups "Articles"/
   "Nouveau produit" (shopping-list-view.php), puis des boutons TEXTE ("ok"). **État
   final, demandé explicitement pour toutes les popups de l'app** :
   #articlePickerCancel/#newArticleCancel portent désormais la même icône de fermeture
   que le "Retour" de la topbar (`idiaa_icon('back')`, classe générique `.icon-btn`,
   aucune règle dédiée nécessaire — même traitement que #pickerCancel/
   #newIngredientCancel, recipe-edit.php, voir plus haut), posée en en-tête de leur
   popup respective ; #newArticleToggleBtn/#createArticleBtn sont les icônes "+"
   correspondantes (voir juste en dessous pour leur règle, identique à
   #newIngredientToggleBtn/#createIngredientBtn). */

/* Icône "+" de l'en-tête de popup (#newArticleToggleBtn — ouvre "Nouveau produit") —
   mêmes règles que #newIngredientToggleBtn (en-tête de #pickerOverlay, recipe-edit.php),
   reprises à l'identique : même tracé SVG, même taille/couleur/hover. Ciblée par id
   (unique dans la page), même raison que #newIngredientToggleBtn (.list-header réutilisé
   ailleurs, un sélecteur par id reste sûr).
   #createArticleBtn RETIRÉE de ce groupe, demandé explicitement ("remplacer le + par une
   icône check comme celle pour valider un nouveau livre") : devenue un .icon-btn
   générique (idiaa_icon('save'), comme #collectionCreateSubmit) au lieu d'un <svg> brut
   dédié — suit désormais les règles génériques/scopées .icon-btn (voir plus haut dans ce
   fichier), plus besoin de règle fill: dédiée ici. */
#newArticleToggleBtn {
  height: 26px;
  width: 26px;
  flex-shrink: 0;
  fill: var(--accent);
  cursor: pointer;
  transition: 0.2s;
}

@media (hover: hover) and (pointer: fine) {
  #newArticleToggleBtn:hover {
    fill: var(--svg-hover);
    transform: scale(1.15);
  }
}

/* MODE SOMBRE UNIQUEMENT, demandé explicitement ("ces couleurs sont pour le thème
   sombre, on s'occupera du clair après") — blanc + survol bleu fixe (#007aff), même
   changement que :root[data-theme="dark"] .list-header--icon .icon-btn (voir sa règle,
   plus haut dans ce fichier) et même bleu fixe. Clair/système/Îdiää gardent le
   fill: var(--accent) / survol var(--svg-hover) (orange) ci-dessus, inchangé. */
:root[data-theme="dark"] #newArticleToggleBtn {
  fill: #fff;
}

@media (hover: hover) and (pointer: fine) {
  :root[data-theme="dark"] #newArticleToggleBtn:hover {
    fill: #007aff;
  }
}

/* ---------- En-tête de liste (ex. "Liste des recettes" + limite + icône +) ---------- */
.list-header {
  display: flex;
  align-items: flex-end;
  gap: 16px;
  margin-bottom: 16px;
}

.list-header h2 {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
}

/* Variante icône + titre (ex. "Illustration") : centrée verticalement, contrairement
   au flex-end par défaut (pensé pour titre + badge texte + icône "+" de index.php). */
.list-header--icon {
  align-items: center;
}

/* Variante "icône Fermer à gauche, titre CENTRÉ, icône Enregistrer à droite" (popup
   "Nouveau livre de recette", index.php — demandé explicitement) : les deux .icon-btn
   (40px chacun, symétriques de part et d'autre) laissent le h2 se centrer réellement
   au milieu de l'en-tête via flex:1, plutôt que collé à gauche comme le comportement
   par défaut de .list-header. */
.list-header--centered-title h2 {
  flex: 1;
  text-align: center;
}

/* Espaceur invisible, demandé explicitement (popup "Partage", shopping-lists.php) : sur
   un en-tête --centered-title qui n'a plus qu'UNE icône réelle (l'autre retirée, action
   fusionnée dans la première), prend la même largeur qu'un .icon-btn (40px) sans rien
   afficher ni intercepter de clic — pour que le h2 (flex:1 + text-align:center)
   continue à centrer le titre par rapport à TOUT l'en-tête, pas seulement par rapport à
   l'icône restante seule. */
.list-header__spacer {
  width: 40px;
  flex-shrink: 0;
}

.list-header__limit {
  font-size: 13px;
  color: var(--text-muted);
}

.list-header__add {
  margin-left: auto;
}

/* Regroupe la bascule liste/tuiles + le "+" (recipe-collection.php) — 10px d'écart entre
   les deux, demandé explicitement, plus fin que le gap:16px de .list-header (partagé par
   toutes les pages n'ayant qu'une seule icône "+", pas touché pour ne pas les affecter).
   margin-left:auto reprend le rôle de .list-header__add ci-dessus, porté ici par le
   conteneur plutôt que par chaque icône individuellement. */
.list-header__actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
}

/* Icône "+" simple (pas de bouton circulaire) : cf. charte fournie. */
.icon-plus {
  display: inline-flex;
  width: 26px;
  height: 26px;
  color: var(--accent);
  cursor: pointer;
  transition: transform 0.15s ease, color 0.2s ease;
  border: none;
  background: none;
  padding: 0;
}

@media (hover: hover) and (pointer: fine) {
  .icon-plus:hover {
    transform: scale(1.15);
    color: var(--svg-hover);
  }
}

/* "+" du menu "Livres de recette" (#collectionsDropdown, recipe-edit.php) — bleu,
   demandé explicitement : contrairement à idiaa_icon('plus') (fill="currentColor" dans
   le SVG lui-même, voir includes/icons.php), ce polygone est un <svg> brut sans fill —
   .icon-plus seule (qui ne pose que `color`) n'a donc aucun effet dessus. Même
   traitement que les autres "+" bruts de l'app (#newIngredientToggleBtn,
   #newArticleToggleBtn...) : fill dédié + hover orange. */
#newCollectionBtn {
  fill: var(--accent);
}

@media (hover: hover) and (pointer: fine) {
  #newCollectionBtn:hover {
    fill: var(--svg-hover);
  }
}

/* "+" en haut de liste (.list-header__add — recettes/amis/listes de courses/menus
   planifiés/bibliothèques settings.php+admin.php), réduit de 20% en mobile UNIQUEMENT
   (26px -> 20.8px), demandé explicitement — desktop inchangé, et les autres usages de
   .icon-plus (picker d'ingrédient, etc.) non concernés par cette demande précise. */
@media (max-width: 899px) {
  .list-header__add.icon-plus {
    width: 20.8px;
    height: 20.8px;
  }
}

/* Icône "panier" (Ingrédients de la recette -> liste de courses, recipe-view.php) : 20%
   plus grande que les .icon-plus habituelles (26px), demandé explicitement — 26 * 1.2 =
   31.2px, arrondi. Rien d'autre ne change (couleur/hover restent ceux de .icon-plus). */
#recipeAddIngredientsToListBtn {
  position: relative;
  width: 31px;
  height: 31px;
}

/* Panier/check empilés (même principe que .shopping-article-item__add/__check,
   shopping-list-view.js) : les deux icônes occupent tout le bouton en position:absolute,
   seule l'opacité change — nécessaire pour transitionner en douceur (un changement de
   contenu SVG, lui, ne peut pas être animé). .is-added posée en JS après un ajout
   réussi (recipe-view.js), jamais retirée ensuite (confirmation "déjà ajouté cette
   session", pas un état par liste). */
#recipeAddIngredientsToListBtn .icon-plus__cart,
#recipeAddIngredientsToListBtn .icon-plus__check {
  position: absolute;
  inset: 0;
  display: block;
  transition: opacity 0.3s ease;
}

#recipeAddIngredientsToListBtn .icon-plus__cart svg,
#recipeAddIngredientsToListBtn .icon-plus__check svg {
  width: 100%;
  height: 100%;
}

#recipeAddIngredientsToListBtn .icon-plus__check {
  opacity: 0;
  pointer-events: none;
}

#recipeAddIngredientsToListBtn.is-added .icon-plus__cart {
  opacity: 0;
}

#recipeAddIngredientsToListBtn.is-added .icon-plus__check {
  opacity: 1;
}

/* ---------- Tableaux (recettes, ingrédients d'une recette) ---------- */
.data-table {
  width: 100%;
  /* table-layout:auto (par défaut) recalcule la largeur des colonnes selon le contenu
     des lignes actuellement affichées : filtrer par type (recipes.js) changeait donc la
     largeur des colonnes selon les libellés visibles. fixed + <colgroup> (voir
     index.php) fige des largeurs indépendantes du contenu affiché. */
  table-layout: fixed;
  /* border-collapse:collapse faisait entrer la bordure du <table> en conflit avec les
     border-bottom des cellules, qui l'effaçaient sur les bords — separate (comme
     .recipe-ingredients) laisse la bordure du tableau s'afficher normalement. */
  border-collapse: separate;
  border-spacing: 0;
  background: var(--surface);
  /* Même rayon et bordure que .recipe-ingredients (tableau d'ingrédients,
     recipe-edit.php / recipe-view.php), pas --radius-md. */
  border-radius: 8px;
  border: 1px solid var(--text-muted);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  margin-bottom: 16px;
}

.data-table th,
.data-table td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}

.data-table__col--thumb {
  width: 70px;
}

/* Colonne "Recette" — width:100% (pas un <col> laissé sans largeur) pour absorber tout
   l'espace restant à côté des colonnes à largeur fixe : voir le commentaire sur ce <col>
   dans index.php pour le bug iPhone que ça corrige (Safari/WebKit n'étend pas forcément
   une colonne sans largeur déclarée jusqu'au bord du tableau en table-layout:fixed).
   Mélanger un % avec des px dans un même <colgroup> fixed est un usage standard,
   supporté de façon cohérente par tous les moteurs — contrairement à l'absence totale
   de valeur. */
.data-table__col--name {
  width: 100%;
}

/* .data-table__col--prep-time/--servings : largeurs (110px/130px) déclarées plus bas,
   dans le bloc @media (min-width:900px) qui gère aussi leur affichage — colonnes
   desktop-only, voir .data-table__col--optional pour pourquoi ce n'est plus ici. */

/* Colonne "Articles" (nombre non cochés) — shopping-lists.php. */
.data-table__col--shopping-count {
  width: 100px;
}

/* Icône menu (⋮, voir .row-menu-trigger plus haut) — partagée par .data-table
   (recettes), les listes de courses, admin.php, meal-events.php et les 4 bibliothèques
   de settings.php (même classe de base). Réservée dans TOUS les cas désormais (desktop
   ET tactile, voir CLAUDE.md / Détection tactile vs pointeur) — le "⋮" ouvre le même
   menu contextuel complet quel que soit l'appareil, plus seulement au clic sur
   desktop ; sans cette réservation en tactile, la cellule restait forcée à 0px
   (table-layout:fixed) pendant que le bouton lui-même redevenait visible/interactif,
   le débordant hors de sa cellule sans être peint correctement (bug rencontré en
   généralisant, piège Chromium déjà connu ci-dessous — invisible et non cliquable en
   pratique malgré des styles calculés corrects).
   58px (pas 40px) : piège rencontré en vérifiant le décalage de 10px demandé sur
   .row-menu-trigger (margin-right) — avec seulement 40px, le contenu (icône 24px +
   padding 12px de chaque côté de la cellule = 48px) dépassait déjà la largeur de la
   colonne ; Chromium ignore alors le text-align:right (et donc la marge) dans cette
   situation de débordement, l'icône restait plaquée au même endroit quelle que soit la
   marge. 58px laisse assez de place pour icône + padding + les 10px de marge sans
   débordement, condition nécessaire pour que le décalage prenne effet. */
.data-table__col--menu {
  width: 58px;
}

/* Colonne dédiée pour .recipe-row-swipe__actions / .shopping-list-row-swipe__actions
   (dernier <td>, jamais dans le flux normal — display:none sur pointeur fin,
   position:absolute sur tactile) : largeur 0 dans tous les cas, mais DÉCLARÉE (voir
   .shopping-items-table__col--actions, style.css, pour le piège rencontré sans elle). */
.data-table__col--actions {
  width: 0;
}

/* Type/Préparation/Personnes centrés sous leur en-tête (le bouton/texte de l'en-tête est
   inline-flex, donc centré comme le contenu par text-align). */
.data-table th:nth-child(3),
.data-table td:nth-child(3),
.data-table th:nth-child(4),
.data-table td:nth-child(4),
.data-table th:nth-child(5),
.data-table td:nth-child(5) {
  text-align: center;
}

/* Colonnes "Préparation" / "Personnes" : desktop uniquement (même seuil que le reste de
   l'app). Le <col> ET les cellules doivent être masqués ensemble — un <col> caché
   n'entraîne pas forcément l'effacement des cellules dans tous les moteurs, et une
   cellule cachée seule laisserait la largeur du <col> réservée (colonne "fantôme")
   avec table-layout:fixed. */
.data-table__col--optional,
.data-table__cell--optional {
  display: none;
}

/* Bug rencontré sur iPhone (Safari/WebKit), pas reproduit en vérification headless
   Chromium : en table-layout:fixed, WebKit ignore display:none sur un <col> pour le
   calcul des largeurs — la colonne "cachée" réserve quand même les 110px/130px déclarés
   plus bas (.data-table__col--prep-time/--servings), en plus de display:none. La colonne
   "Recette" (seule sans largeur fixe, qui absorbe l'espace restant) se retrouve donc
   amputée de cet espace fantôme, et le tableau entier rendu plus étroit que ses 100% —
   d'où le vide visible à droite de l'en-tête sur mobile. width:0 explicite ici (en plus
   de display:none) règle le problème dans tous les moteurs, sans dépendre de leur
   support de display:none sur <col>. */
.data-table__col--optional {
  width: 0;
}

@media (min-width: 900px) {
  .data-table__col--optional {
    display: table-column;
  }

  .data-table__cell--optional {
    display: table-cell;
  }

  /* Largeurs de .data-table__col--prep-time/--servings, desktop uniquement (voir
     .data-table__col--optional plus haut pour pourquoi ce n'est plus déclaré hors
     media query) — même spécificité que width:0, mais déclarée après dans le fichier
     ET seule active ici, donc l'emporte côté desktop. */
  .data-table__col--prep-time {
    width: 110px;
  }

  /* Les cellules n'affichent que l'icône + le nombre (voir recipes.js), mais l'en-tête a
     besoin de place pour "Personnes" + le chevron du filtre + le padding — 70px les
     coupait (rognés par overflow:hidden de .data-table). */
  .data-table__col--servings {
    width: 130px;
  }
}

/* Cellule image : padding réduit pour ne pas écraser la vignette (50px) dans les 70px
   de .data-table__col--thumb. */
.data-table td:first-child {
  padding: 10px;
}

.data-table td:nth-child(2) {
  padding-left: 0;
}

.data-table th {
  color: var(--text);
  font-weight: 600;
  font-size: 15px;
  background: var(--surface-alt);
}

/* Refonte couleurs sombre en cours (voir variables.css) — header/liste de TOUTES les
   listes de l'app, demandé explicitement. --surface/--surface-alt pas touchés (partagés
   avec d'autres éléments non concernés) : overrides ciblés à chaque famille de
   liste/tableau à la place (.data-table ici — couvre aussi ses variantes multi-classes
   admin-users-table/library-ingredients-table/library-simple-table, voir plus bas pour
   .shopping-items-table/.meal-event-planning-table/.recipe-ingredients, qui ne partagent
   pas .data-table comme classe de base). */
:root[data-theme="dark"] .data-table {
  background: rgb(43, 43, 43);
}

:root[data-theme="dark"] .data-table th {
  background: rgb(95, 95, 95);
}

.data-table tbody tr:last-child td {
  border-bottom: none;
}

/* Ligne entière cliquable (recipes.js gère la navigation en JS, pas juste le lien sur le
   nom) — survol léger du fond en desktop uniquement, demandé explicitement ("pas
   nécessaire en mode mobile" : au toucher, :hover resterait "collé" après un tap tant
   qu'on n'a pas touché ailleurs, plutôt gênant). Même teinte que les autres lignes de
   liste survolables de l'app (.zone-liste-ingr .ingredient-item, .recipe-share-item). */
@media (min-width: 900px) {
  .data-table tbody tr {
    cursor: pointer;
  }

  .data-table tbody tr:hover {
    background-color: var(--accent-tint);
  }
}

/* ---------- Éditer/Partager/Supprimer une recette (index.php) ---------- */
/* Nouveau concept de liste, demandé explicitement : la liste des recettes gagne une
   icône menu (voir .row-menu-trigger plus haut) + un glissement tactile révélant
   Éditer/Partager/Supprimer — n'existait pas avant (la suppression se faisait depuis le
   formulaire d'édition, "Supprimer" y a été retiré ; l'édition depuis la page de
   visualisation, "Éditer" y a été retiré aussi — voir recipe-edit.php/recipe-view.php).
   Même famille de technique que .shopping-list-row-swipe (shopping-lists.js) : VRAIE
   ligne de tableau (<tr>/<td>), pas un <div>. Pas de position:relative ici (sur le
   <tr>) : voir .recipe-row-swipe__actions plus bas pour pourquoi ce rôle est porté par
   la cellule d'actions elle-même plutôt que par la ligne. */

.recipe-row-swipe__link {
  transition: transform 0.2s ease;
}

.recipe-row-swipe.is-dragging .recipe-row-swipe__link {
  transition: none;
}

/* td.xxx (élément + classe), pas juste .xxx : .data-table th/td (élément + classe aussi)
   règle la base en text-align:left avec la même spécificité — voir le même correctif sur
   .shopping-list-row-swipe__menu (style.css) pour le détail du piège. text-align:right
   (pas center) : la marge-droite de .row-menu-trigger a besoin d'un bord droit réel à
   écarter. */
td.recipe-row-swipe__menu {
  text-align: right;
}

@media (hover: hover) and (pointer: fine) {
  .recipe-row-swipe:hover .row-menu-trigger,
  .recipe-row-swipe:focus-within .row-menu-trigger,
  .recipe-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  /* Cellule de glissement tactile : ne sert pas ici (voir CLAUDE.md / Détection tactile
     vs pointeur) — le menu s'en charge, pas de colonne réservée pour elle non plus (pas
     de <col> dédié dans le colgroup, voir index.php). */
  .recipe-row-swipe__actions {
    display: none;
  }
}

.recipe-row-swipe__action {
  border: none;
  /* Sans ça, le fond par défaut du navigateur (bouton natif, ex. gris clair) couvrait le
     bouton en plein sur toute sa hauteur, y compris le coin arrondi du premier bouton
     (celui qui touche le bord gauche de .recipe-row-swipe__actions-inner) — le
     border-radius du conteneur restait donc invisible, masqué par ce fond opaque. */
  background: transparent;
  cursor: pointer;
  padding: 0;
}

/* Positionne .recipe-row-swipe__actions-inner (voir plus bas) par rapport à CETTE
   cellule plutôt qu'à .recipe-row-swipe (le <tr>, voir plus haut) — bug rencontré sur
   iPhone (Safari/WebKit), pas reproduit en vérification headless Chromium : WebKit
   n'établit pas de façon fiable un <tr> comme "containing block" pour un descendant en
   position:absolute (contrairement à un <td>, qui fonctionne correctement) — l'ancien
   panneau d'actions (position:absolute directement sur cette cellule) remontait donc au
   premier ancêtre positionné plus haut qui, lui, fonctionnait (.data-table-wrap),
   s'étalant sur toute la hauteur du tableau au lieu de rester confiné à sa ligne. */
.recipe-row-swipe__actions {
  position: relative;
}

@media (hover: none), (pointer: coarse) {
  /* --reveal : largeur réelle de .recipe-row-swipe__actions-inner (3 actions ici), posée
     en JS à l'ouverture — voir swipe-row.js. 168px (56px × 3) de repli tant que JS n'a
     rien posé. */
  .recipe-row-swipe.is-swiped .recipe-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 168px)));
  }

  .recipe-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    /* top/bottom légèrement dépassants (-1px, pas 0) : couvre toute bordure/liseré de la
       ligne en dessous jusqu'au bord exact, y compris sous le coin arrondi — sans ça, un
       filet de la bordure de séparation des lignes pouvait rester visible pile à la
       jonction avec l'arrondi (repéré à l'usage). */
    inset: -1px 0 -1px auto;
    /* Fond rouge + icône blanche, demandé explicitement — ne révèle plus qu'une seule
       action (Supprimer, voir recipes.js), fond neutre "inversé" plus nécessaire
       maintenant que ce n'est plus un choix entre plusieurs actions. */
    background: var(--danger);
    /* Coins arrondis côté gauche seulement (bord "exposé" par le glissement) — le bord
       droit reste carré, déjà confondu avec celui, arrondi, du tableau lui-même
       (.data-table). 5px puis 15px (ajusté après un premier essai). */
    border-radius: 15px 0 0 15px;
    /* translateX(100%) : décale le bloc de SA PROPRE largeur, quel que soit le nombre
       d'actions qu'il contient — voir le même choix sur .people-row-swipe__actions
       (friends.js) pour le détail (piège du glassmorphism Îdiää). */
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .recipe-row-swipe.is-dragging .recipe-row-swipe__actions-inner {
    transition: none;
  }

  .recipe-row-swipe.is-swiped .recipe-row-swipe__actions-inner {
    transform: translateX(0);
  }

  .recipe-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    color: var(--accent-contrast);
  }

  /* Icône blanche (rowMenuDeleteIconSvg, même tracé que le menu contextuel ⋮, voir
     recipes.js) — plus de texte, `aria-label`/`data-tooltip` restent le seul texte
     accessible. */
  .recipe-row-swipe__action svg {
    width: 28px;
    height: 28px;
  }
}

/* ---------- Livres de recette (index.php, page d'accueil "Recettes") ---------- */
/* Même famille de technique que .recipe-row-swipe juste au-dessus (VRAIE ligne de
   tableau, glissement tactile ne révélant qu'une action Supprimer, menu ⋮ toujours
   visible) — copiée telle quelle, renommée. Concerne uniquement les VRAIS livres
   (.collection-row-swipe) : "Toutes les recettes" (.collection-row, sans "-swipe") n'a
   ni glissement ni menu, elle n'a donc pas besoin de ces règles. */

.collection-row-swipe__link {
  transition: transform 0.2s ease;
}

/* text-align:right — comportement de cellule NORMAL (vertical-align:middle par défaut
   du <td>, centré dans la hauteur de toute la LIGNE), pas display:flex directement ici
   : un essai précédent (display:flex sur le <td> lui-même) a fait remonter "⋮"/la
   poignée trop haut, désalignées du texte des autres cellules — un <td> en
   display:flex perd son vertical-align natif (qui centre dans la hauteur de la ligne)
   au profit d'un centrage flex qui ne centre lui que dans la hauteur du <td> LUI-MÊME,
   pas forcément identique selon les navigateurs. Le flux inline reste donc responsable
   de l'alignement VERTICAL (fiable, standard) ; c'est le wrapper
   .collection-row-swipe__menu-inner (voir plus bas, recipes.js) qui garantit
   maintenant que "⋮" et la poignée restent sur une même ligne HORIZONTALEMENT (bug
   antérieur : les deux finissaient empilés, la largeur de contenu disponible tombant
   pile sur leur largeur cumulée, sans marge d'erreur). */
td.collection-row-swipe__menu {
  text-align: right;
}

.collection-row-swipe__menu-inner {
  display: inline-flex;
  align-items: center;
}

/* Élargit la colonne "menu" (58px -> 90px, voir index.php) pour accueillir la poignée
   de glisser-déposer à côté du "⋮" — déclarée après .data-table__col--menu (même
   spécificité, classe unique), l'emporte donc par ordre de cascade sans avoir besoin
   d'être plus spécifique. */
.collections-col--menu {
  width: 90px;
}

/* Curseurs personnalisés (main ouverte/fermée) pour la poignée de glisser-déposer
   (index.php/recipes.js), fournis par l'utilisateur — variables plutôt que répétés
   dans chaque règle `cursor:` (3 usages : idle, actif, .idiaa-reorder-grabbing sur
   <body> plus bas), pour ne pas tripler ce data URI volumineux. SVG 32x32 (agrandi, demandé explicitement — 24x24 jugé trop petit face au
   curseur pointeur natif), point chaud 16 16 (centre de la main). */
:root {
  --drag-cursor-grab: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsPSJub25lIj48ZyBmaWxsPSIjZmZmZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0IDMpIj48cGF0aCBkPSJtMS4yNSA2LjI1Yy43MjQ0MDUyMS0uMTQ0ODgxMDQgMS40NDkwNjY3NS4yNDc2NjY4NyAxLjcyMzQzMjA4LjkzMzU4MDE5bC41MjY1Njc5MiAxLjMxNjQxOTgxdi02YzAtLjU1MjI4NDc1LjQ0NzcxNTI1LTEgMS0xczEgLjQ0NzcxNTI1IDEgMXYtMS41YzAtLjU1MjI4NDc1LjQ0NzcxNTI1LTEgMS0xczEgLjQ0NzcxNTI1IDEgMXYxYzAtLjU1MjI4NDc1LjQ0NzcxNTI1LTEgMS0xczEgLjQ0NzcxNTI1IDEgMXYxLjVjMC0uNTUyMjg0NzUuNDQ3NzE1MjUtMSAxLTEgLjU1MjI4NDcgMCAxIC40NDc3MTUyNSAxIDF2OGMwIDIuMjA5MTM5LTEuNzkwODYxIDQtNCA0LTIuMzUwODI3NCAwLTQuMzk5OTgzMTEtMS41OTk5MzI0LTQuOTcwMTQyNS0zLjg4MDU3bC0uMDI5ODU3NS0uMTE5NDMtMS45Mjk5MTIyOS0zLjg1OTgyNDU3Yy0uMjQwNDgxNjktLjQ4MDk2MzM5LS4wNDU1MzI3Ni0xLjA2NTgxMDE4LjQzNTQzMDYzLTEuMzA2MjkxODguMDc3NDAzODgtLjAzODcwMTk0LjE1OTYyMjA0LS4wNjY5MTE2My4yNDQ0ODE2Ni0uMDgzODgzNTV6Ij48L3BhdGg+PHBhdGggZD0ibTUuNSAxLjV2NiI+PC9wYXRoPjxwYXRoIGQ9Im03LjUgMS41djUiPjwvcGF0aD48cGF0aCBkPSJtOS41IDMuNXY0Ij48L3BhdGg+PC9nPjwvc3ZnPgo=") 16 16;
  --drag-cursor-grabbing: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAyMSAyMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBmaWxsPSJub25lIj48ZyBmaWxsPSIjZmZmZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg1IDYpIj48cGF0aCBkPSJtMi4zNTA3Mzc2NiAzLjUwNTQ4NTc0LjE0OTI2MjM0LS4wMDU0ODU3NHYtMWMwLS41NTIyODQ3NS40NDc3MTUyNS0xIDEtMXMxIC40NDc3MTUyNSAxIDF2LTFjMC0uNTUyMjg0NzUuNDQ3NzE1MjUtMSAxLTFzMSAuNDQ3NzE1MjUgMSAxdjFjMC0uNTUyMjg0NzUuNDQ3NzE1MjUtMSAxLTFzMSAuNDQ3NzE1MjUgMSAxdjFjMC0uNTUyMjg0NzUuNDQ3NzE1MjUtMSAxLTEgLjU1MjI4NDcgMCAxIC40NDc3MTUyNSAxIDF2NGMwIDIuNzYxNDIzNy0yLjIzODU3NjI1IDUtNSA1aC0uNWMtMi40ODUyODEzNyAwLTQuNS0yLjAxNDcxODYtNC41LTQuNXYtMi41YzAtMS4wNTQzNjE4LjgxNTg3Nzc5LTEuOTE4MTY1MTIgMS44NTA3Mzc2Ni0xLjk5NDUxNDI2eiI+PC9wYXRoPjxwYXRoIGQ9Im0yLjUgMi41djMiPjwvcGF0aD48cGF0aCBkPSJtNC41IDEuNXYyIj48L3BhdGg+PHBhdGggZD0ibTYuNSAxLjV2MiI+PC9wYXRoPjxwYXRoIGQ9Im04LjUgMi41djIiPjwvcGF0aD48L2c+PC9zdmc+Cg==") 16 16;
}

/* Poignée de glisser-déposer (réordonner mes livres, drag-reorder.js), demandé
   explicitement — même mécanique de révélation au survol que .row-menu-trigger
   juste au-dessus (masquée par défaut sur pointeur fin, révélée par
   .collection-row-swipe:hover ci-dessous ; TOUJOURS masquée en tactile — pas de media
   query dédiée nécessaire, la seule règle qui la rend visible est déjà scopée à
   (hover:hover) and (pointer:fine)). Curseurs personnalisés (main ouverte/fermée),
   fournis par l'utilisateur — encodés en data URI (SVG inline, 32x32, point chaud
   16 16 au centre de la main) plutôt que deux fichiers séparés, pour éviter une
   requête réseau de plus pour un si petit visuel. `grab`/`grabbing` gardés en repli
   (deuxième valeur de chaque liste `cursor`) si jamais le navigateur refuse l'image
   (taille non supportée, etc.). */
.collection-drag-handle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  /* Pas de margin-left ici : l'écart avec le "⋮" vient déjà du margin-right de
     .row-menu-trigger (10px, règle générique) — cumuler les deux aurait doublé l'écart
     à 18px. margin-right ici reprend plutôt le rôle que .row-menu-trigger avait seul
     avant (décoller du bord droit de la colonne), la poignée étant désormais le
     dernier élément de la cellule. */
  margin-right: 10px;
  border: none;
  background: none;
  padding: 0;
  color: var(--text-muted);
  cursor: var(--drag-cursor-grab), grab;
  transition: color 0.2s ease;
}

.collection-drag-handle svg {
  width: 100%;
  height: 100%;
}

.collection-drag-handle:active,
.collection-row-swipe.is-reorder-dragging .collection-drag-handle {
  cursor: var(--drag-cursor-grabbing), grabbing;
}

@media (hover: hover) and (pointer: fine) {
  .collection-drag-handle {
    display: inline-flex;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease, color 0.2s ease;
  }

  .collection-drag-handle:hover {
    color: var(--svg-hover);
  }

  .collection-row-swipe:hover .row-menu-trigger,
  .collection-row-swipe:focus-within .row-menu-trigger,
  .collection-row-swipe.row-menu-open .row-menu-trigger,
  .collection-row-swipe:hover .collection-drag-handle,
  .collection-row-swipe:focus-within .collection-drag-handle {
    opacity: 1;
    pointer-events: auto;
  }
}

/* Ligne saisie (en cours de glissement, souris comme tactile) : légèrement soulevée du
   reste de la liste — retour visuel standard d'un élément "attrapé" dans une liste
   triable, demandé implicitement par l'ajout d'une poignée de préhension. z-index
   au-dessus des lignes voisines (sinon l'ombre serait recouverte par la ligne
   suivante) ; position:relative nécessaire pour que ce z-index/l'ombre prennent effet
   sur une ligne de tableau. */
.collection-row-swipe.is-reorder-dragging {
  position: relative;
  z-index: 5;
  background: var(--surface);
  box-shadow: var(--shadow-md);
}

/* Empêche la sélection de texte au toucher, demandé explicitement — l'appui long
   (1s, drag-reorder.js) déclenche le glissement, mais le geste natif du navigateur
   "maintenir pour sélectionner le texte" se déclenche EN PARALLÈLE sur la même
   pression, sans lien avec notre minuteur JS (les deux observent le même appui,
   indépendamment).
   Rectification (premier essai insuffisant, signalé explicitement) : la règle ne
   ciblait que .collection-row-swipe elle-même, en comptant sur l'héritage de
   user-select — mais le nom du livre est un <a> (.collection-row-swipe__link a), et
   iOS Safari a son propre geste "callout" (copier/partager le lien) sur les ANCRES,
   distinct de la sélection de texte générique et pas toujours fiablement hérité
   depuis un ancêtre. Sélecteur universel (comme pour les curseurs personnalisés plus
   haut) : pose la règle sur CHAQUE descendant explicitement plutôt que de compter sur
   l'héritage, et cible l'<a> par une règle dédiée en plus. -webkit-touch-callout:none :
   coupe le menu contextuel/la loupe de sélection (génériques) ET le callout propre
   aux liens sur iOS. Scopé au tactile (hover:none/pointer:coarse) : ne touche pas la
   sélection au clic-glissé à la souris sur desktop, non concernée par cette demande
   (le glissement y passe uniquement par la poignée, jamais par le texte de la ligne). */
/* -webkit-user-drag:none sur l'<a> : complément du correctif ci-dessus, bug REDIAGNOSTIQUÉ
   après un second signalement ("un petit texte que l'on déplace, et je n'ai même plus
   l'effet de déplacement de ligne") — pas un problème de SÉLECTION de texte (déjà traité
   ci-dessus, user-select/-webkit-touch-callout), mais le glisser-déposer NATIF HTML5 du
   navigateur sur les <a> (draggable="true" par défaut sur tout lien, mécanisme
   INDÉPENDANT de user-select) : ce glissement natif produit exactement une "vignette"
   fantôme du texte du lien qui suit le doigt, ET prend le dessus sur le glissement
   personnalisé (drag-reorder.js, Pointer Events) — qui n'a alors plus jamais l'occasion
   de démarrer. Corrigé en DEUX temps : draggable="false" posé sur l'<a> lui-même
   (recipes.js, attribut HTML, la vraie source de vérité pour ce comportement) +
   -webkit-user-drag:none ici en complément défensif (WebKit/Safari plus fiable avec
   cette propriété qu'avec l'attribut seul, selon les versions). */
@media (hover: none), (pointer: coarse) {
  .collection-row-swipe,
  .collection-row-swipe *,
  .collection-row-swipe a {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
    -webkit-user-drag: none;
  }

  /* touch-action:none — TROISIÈME bug rencontré sur ce même geste, signalé explicitement
     ("je veux la déplacer vers le haut ou le bas, c'est tout l'écran qui bouge, comme si
     je scrollais") : `html, body` ont `touch-action: pan-y` (voir CLAUDE.md /
     Verrouillage du mouvement horizontal en mobile) — ce réglage autorise EXPLICITEMENT
     le panoramique vertical natif du navigateur. Une fois qu'un geste tactile est
     reconnu comme un panoramique autorisé par touch-action, la spec rend les événements
     touchmove/pointermove qui le composent NON ANNULABLES (event.cancelable devient
     false) : le event.preventDefault() déjà posé dans drag-reorder.js (onPointerMove,
     cas "glissement actif") ne servait donc RIEN une fois le mouvement réel commencé —
     le navigateur avait déjà pris la main pour faire défiler la page à la place de
     déplacer la ligne. Posé ICI (sur la ligne elle-même, pas globalement) pour reprendre
     la main sur ce point précis de l'app, sans toucher au comportement pan-y du reste de
     la page. Contrepartie assumée, compensée en JS (voir drag-reorder.js, mode
     "défilement manuel") : plus aucun défilement natif ne peut démarrer pour un toucher
     qui commence sur une ligne, y compris un simple geste de défilement de page qui n'a
     rien à voir avec l'appui long — drag-reorder.js le reproduit lui-même via
     window.scrollBy() dès qu'un mouvement dépasse le seuil d'annulation de l'appui long
     (le même geste qui, avant ce correctif, se contentait d'annuler le minuteur JS sans
     jamais empêcher le défilement natif de continuer). */
  .collection-row-swipe {
    touch-action: none;
  }
}

/* Curseur "main fermée" affiché sur TOUT l'écran pendant un glissement actif (classe
   posée sur <body> par drag-reorder.js), pas seulement au survol de la poignée elle-même
   (.collection-drag-handle:active ci-dessus) : un geste rapide sort facilement du
   rectangle 24x24 de la poignée pendant le glissement, sans que le curseur natif de
   l'élément survolé ne reflète cet état sinon. !important : l'emporte sur le curseur
   propre de n'importe quel élément survolé pendant le glissement (lien, bouton...). */
body.idiaa-reorder-grabbing,
body.idiaa-reorder-grabbing * {
  cursor: var(--drag-cursor-grabbing), grabbing !important;
}

/* ---------- Administration (admin.php, réservé à ADMIN_EMAILS) ---------- */
/* Volet statistiques : 3 chiffres alignés en ligne (desktop/tablette uniquement, cette
   page n'étant de toute façon jamais atteignable en dock mobile — voir nav-desktop.php),
   passe en colonne si la largeur manque plutôt que de comprimer les chiffres. */
.admin-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
  margin-top: 12px;
}

.admin-stat {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 140px;
}

/* Volet "Bibliothèque" (voir CLAUDE.md / Administration / Bibliothèque native) — rappel
   de portée (impact immédiat pour tous les comptes vs. valeurs de départ pour les
   nouveaux comptes uniquement) sous le titre de section, avant les listes elles-mêmes. */
.admin-library-subtitle {
  color: var(--text-muted);
  font-size: 14px;
  margin: 4px 0 20px;
}

.admin-stat__value {
  font-size: 32px;
  font-weight: 700;
  color: var(--text);
}

.admin-stat__label {
  font-size: 13px;
  color: var(--text-muted);
}

/* Cellule "Utilisateur" du tableau (avatar + pseudo/e-mail empilés) — même composant
   avatarThumb() que friends.js/chats.js (assets/js/avatar-format.js), taille
   .avatar-photo--sm (48px, déjà utilisée ailleurs pour garder le même rythme visuel). */
.admin-user-cell {
  display: flex;
  align-items: center;
  gap: 10px;
}

.admin-user-cell__info {
  display: flex;
  flex-direction: column;
  min-width: 0; /* autorise la troncature ci-dessous dans une cellule de largeur flexible */
}

.admin-user-cell__name {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.admin-user-cell__email {
  font-size: 13px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Resserrées (130px -> 100px), demandé explicitement — au profit de la colonne
   "Utilisateur" (.data-table__col--name, width:100%, absorbe tout le reste en
   table-layout:fixed). */
.admin-users-table__col--verified,
.admin-users-table__col--date {
  width: 100px;
}

.admin-users-table th.admin-users-table__col--verified,
.admin-users-table td.admin-users-table__col--verified,
.admin-users-table th.admin-users-table__col--date,
.admin-users-table td.admin-users-table__col--date {
  text-align: center;
}

/* Icône d'en-tête "Inscrit le" (idiaa_icon('calendar'), admin.php) — sans cette taille
   explicite, un <svg> sans width/height propre (juste un viewBox) se rend à la taille de
   remplacement par défaut du navigateur (~300×150px), beaucoup trop grand ici (même
   piège que .icon-plus/.th-sort svg, déjà résolu ailleurs de la même façon). */
.admin-users-table__col--date .icon-tooltip svg {
  width: 18px;
  height: 18px;
}

/* ---------- Colonne "Statut" (voir admin.php/admin.js) ----------
   Fusionne administrateur/gelé/accès gratuit offert en une seule colonne à icônes,
   plutôt que deux colonnes séparées (Abonnement/Accès gratuit) + les icônes posées à
   côté du nom (admin/gelé) — remplace cet ancien tableau, gardé un temps en double pour
   comparaison (essai tranché, voir CLAUDE.md / Administration). */
.admin-users-table__col--status {
  width: 130px;
}
.admin-users-table th.admin-users-table__col--status,
.admin-users-table td.admin-users-table__col--status {
  text-align: center;
}

.admin-status-icons {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.admin-status-icon {
  display: inline-flex;
  color: var(--text-muted);
}
.admin-status-icon svg {
  /* +50%, demandé explicitement (16px de base). */
  width: 24px;
  height: 24px;
}

/* Même teinte que son usage existant ailleurs sur cette page : trophée en accent — flocon
   et cadeau restent neutres (--text-muted). Administrateur (voir statusIconsHtml(),
   assets/js/admin.js) reprend la même teinte que son icône d'action "Nommer
   administrateur". */
.admin-status-icon--admin,
.admin-status-icon--pro {
  color: var(--accent);
}

/* Nom grisé (même teinte que l'e-mail juste en dessous) pour un compte gelé, demandé
   explicitement en remplacement de l'ancienne icône flocon à côté du nom (retirée à cet
   endroit, elle vit désormais uniquement dans la colonne "Statut"). */
.admin-user-cell__name--frozen {
  color: var(--text-muted);
}

/* Menu (⋮) / actions (glissement tactile) — mêmes largeurs et même raisonnement que
   .data-table__col--menu/--actions (index.php/style.css), gardées séparées ici plutôt
   que réutilisées telles quelles : nombre/ordre de colonnes différents (voir CLAUDE.md
   / Tableaux pour la même remarque sur .recipe-ingredients). Réservée dans TOUS les cas
   désormais (le "⋮" est visible en tactile aussi, voir .data-table__col--menu pour le
   détail du bug rencontré en généralisant). */
.admin-users-table__col--menu {
  width: 58px;
}

.admin-users-table__col--actions {
  width: 0;
}

td.admin-user-row-swipe__menu {
  text-align: right;
}

/* Ligne "utilisateur" : VRAIE ligne de tableau (<tr>/<td>, pas un <div>) — même mécanique
   que .recipe-row-swipe (index.php), demandé explicitement ("exactement comme la liste de
   recette") : icône menu (.row-menu-trigger) sur pointeur fin, mêmes actions révélées par
   glissement sur tactile (swipe-row.js). Pas de position:relative ici (sur le <tr>) : voir
   .admin-user-row-swipe__actions plus bas pour pourquoi ce rôle est porté par la cellule
   d'actions elle-même plutôt que par la ligne (même bug iPhone/WebKit que
   .recipe-row-swipe__actions, voir son commentaire, style.css). */
.admin-user-row-swipe__link {
  transition: transform 0.2s ease;
}

.admin-user-row-swipe.is-dragging .admin-user-row-swipe__link {
  transition: none;
}

@media (hover: hover) and (pointer: fine) {
  .admin-user-row-swipe:hover .row-menu-trigger,
  .admin-user-row-swipe:focus-within .row-menu-trigger,
  .admin-user-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  .admin-user-row-swipe__actions {
    display: none;
  }
}

.admin-user-row-swipe__action {
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0;
}

.admin-user-row-swipe__actions {
  position: relative;
}

@media (hover: none), (pointer: coarse) {
  /* --reveal : largeur réelle de .admin-user-row-swipe__actions-inner (une seule action
     désormais, Supprimer), posée en JS à l'ouverture — voir swipe-row.js. */
  .admin-user-row-swipe.is-swiped .admin-user-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 56px)));
  }

  .admin-user-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    inset: -1px 0 -1px auto;
    /* Fond rouge, icône blanche (rowMenuDeleteIconSvg) — demandé explicitement. Les
       autres actions (admin/accès/geler) ne vivent plus que dans le menu ⋮, voir
       admin.js. */
    background: var(--danger);
    border-radius: 15px 0 0 15px;
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .admin-user-row-swipe.is-dragging .admin-user-row-swipe__actions-inner {
    transition: none;
  }

  .admin-user-row-swipe.is-swiped .admin-user-row-swipe__actions-inner {
    transform: translateX(0);
  }

  .admin-user-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    color: var(--accent-contrast);
  }

  .admin-user-row-swipe__action svg {
    width: 28px;
    height: 28px;
  }
}

/* ---------- Bibliothèque (settings.php) — ingrédients pour l'instant, voir CLAUDE.md ---------- */
.library-section-title {
  margin: 12px 0 8px;
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
}

/* Colonnes "Unité" ET "Catégorie" réduites au profit du nom (colonne "Recette"/"Nom",
   .data-table__col--name, width:100% — absorbe automatiquement l'espace ainsi libéré,
   table-layout:fixed) — demandé explicitement, en deux temps (Unité d'abord, Catégorie
   ensuite). 90px/100px (130px chacune avant) — Catégorie un peu plus large qu'Unité :
   ses valeurs sont plus longues ("Boucherie et poissonnerie", "Produits surgelés"...),
   pas de white-space:nowrap sur ces cellules donc un nom trop long passe simplement à
   la ligne (pas tronqué), mais autant limiter les cas où ça arrive. */
.library-ingredients-table__col--unit {
  width: 90px;
}

.library-ingredients-table__col--category {
  width: 100px;
}

/* Listes de la Bibliothèque limitées à ~8 éléments visibles, défilement vertical pour le
   reste — demandé explicitement, sur les listes restantes (Ingrédients/Articles de
   course/Catégories d'article — "Types de plat" retirée de cette page, voir
   settings-library-lists.js). Ciblé via :has() plutôt qu'une classe dédiée sur chaque
   wrapper (même technique déjà utilisée ailleurs dans le projet, voir
   .idiaa-field:has(textarea)) — scopé précisément à settings.php, ces deux classes de
   table (avec vignette / nom seul) n'existant nulle part ailleurs dans l'app.
   overflow-y:auto (pas "scroll") : pas de barre vide en permanence si moins de 8
   éléments. Deux hauteurs différentes : une ligne avec vignette (50px) est bien plus
   haute qu'une ligne "nom seul" (Types de plat/Catégories), même nombre de lignes sinon
   très inégal visuellement entre les deux familles de liste. Scrollbar personnalisée
   déjà appliquée globalement en mode Îdiää (voir Design system), rendu natif dans les
   autres thèmes. */
.data-table-wrap:has(.library-ingredients-table) {
  max-height: 640px;
  overflow-y: auto;
}

.data-table-wrap:has(.library-simple-table) {
  max-height: 400px;
  overflow-y: auto;
}

/* En-tête de colonne fixe pendant le défilement — fond déjà opaque
   (var(--surface-alt), .data-table th) donc pas de chevauchement disgracieux avec les
   lignes qui défilent dessous. */
.data-table-wrap:has(.library-ingredients-table) thead th,
.data-table-wrap:has(.library-simple-table) thead th {
  position: sticky;
  top: 0;
  z-index: 1;
}

/* Petite icône "utilisateur" à droite du nom, demandé explicitement — distingue un
   ingrédient AJOUTÉ par le compte (éditable/supprimable en entier) d'un ingrédient
   NATIF de l'application (bibliothèque de base, personnalisable en unité/catégorie
   seulement, voir CLAUDE.md / Design system / Bibliothèques). Même icône que
   idiaa_icon('profile') (topbar/avatar) — revenu à cette icône sur demande explicite
   (essai avec un SVG fourni séparément abandonné), dupliquée en JS pour la même raison
   que le reste des icônes rendues dynamiquement (settings-library.js, pas de registre
   PHP accessible depuis du JS pur) — couleur var(--text), fond none, alignée
   horizontalement avec le texte (inline-flex + vertical-align:middle).
   Le <svg> lui-même n'est PAS stylé ici (voir .library-ingredient-row-swipe__owner-icon
   svg, plus bas dans ce fichier, après la règle .library-ingredient-row-swipe__link
   svg — nécessaire pour la primer en cascade, voir son commentaire). */
.library-ingredient-row-swipe__owner-icon {
  display: inline-flex;
  align-items: center;
  width: 12px;
  height: 12px;
  margin-left: 6px;
  background: none;
  color: var(--text);
  vertical-align: middle;
}

/* Rappel de portée dans la popup d'édition — visible UNIQUEMENT quand un ingrédient
   NATIF est en cours d'édition (voir settings-library.js, .native-hint togglée en JS). */
.library-ingredient-native-hint {
  color: var(--text-muted);
  font-size: 13px;
  margin: 0 0 12px;
}

.library-ingredients-table th.library-ingredients-table__col--unit,
.library-ingredients-table td.library-ingredients-table__col--unit,
.library-ingredients-table th.library-ingredients-table__col--category,
.library-ingredients-table td.library-ingredients-table__col--category {
  text-align: center;
}

/* Ligne "ingrédient" : même mécanique que .admin-user-row-swipe (admin.php) — icône menu
   (.row-menu-trigger) sur pointeur fin, mêmes actions révélées par glissement sur
   tactile, deux actions (Éditer/Supprimer, comme "Éditer"/"Supprimer" sur la liste des
   recettes, index.php — pas de "Partager", un ingrédient privé ne se partage pas). Pas de
   position:relative sur le <tr> — voir .library-ingredient-row-swipe__actions plus bas
   pour la même raison qu'ailleurs (bug iPhone/WebKit, voir CLAUDE.md). */
.library-ingredient-row-swipe__link {
  transition: transform 0.2s ease;
}

/* Vignette (iconThumb(), settings-library.js) : jamais dimensionnée jusqu'ici — bug
   signalé (image affichée à sa taille réelle, jusqu'à 128px, rognée par la colonne
   70px/overflow:hidden du tableau, voir .data-table__col--thumb). Alignée sur
   .data-table__thumb (liste des recettes, index.php), demandé explicitement — mêmes
   50px/object-fit:cover/coins arrondis/fond neutre, sans reprendre la classe elle-même
   (iconThumb() ne prend pas de paramètre de classe, partagé avec recipe-view.js/
   recipe-edit.js qui ont leur propre traitement via .ingredient-row). svg aussi couvert
   (icône de catégorie legacy, voir CLAUDE.md / Popup de sélection d'ingrédient) — même
   bug potentiel, pas juste l'upload photo. */
.library-ingredient-row-swipe__link img,
.library-ingredient-row-swipe__link svg {
  display: block;
  width: 50px;
  height: 50px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--surface-alt);
}

/* Bug corrigé : la règle ci-dessus (vignette de la cellule "image", 50×50px + fond) est
   un sélecteur descendant générique (.library-ingredient-row-swipe__link svg) — elle
   attrapait AUSSI l'icône "utilisateur" (.library-ingredient-row-swipe__owner-icon svg,
   voir plus haut), nichée dans la cellule NOM (même classe __link) juste à côté du
   texte, pas la cellule vignette. Résultat signalé par l'utilisateur : un rectangle
   blanc (fond var(--surface-alt), 50×50px) autour d'une icône censée faire 12px sans
   fond. Même spécificité que la règle ci-dessus (1 classe + 1 type) : DOIT être déclarée
   après elle pour gagner en cascade — ne pas déplacer avant. */
.library-ingredient-row-swipe__owner-icon svg {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 0;
  object-fit: initial;
  background: none;
}

.library-ingredient-row-swipe.is-dragging .library-ingredient-row-swipe__link {
  transition: none;
}

@media (hover: hover) and (pointer: fine) {
  .library-ingredient-row-swipe:hover .row-menu-trigger,
  .library-ingredient-row-swipe:focus-within .row-menu-trigger,
  .library-ingredient-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  .library-ingredient-row-swipe__actions {
    display: none;
  }
}

.library-ingredient-row-swipe__action {
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0;
}

.library-ingredient-row-swipe__actions {
  position: relative;
}

@media (hover: none), (pointer: coarse) {
  /* --reveal : largeur réelle de .library-ingredient-row-swipe__actions-inner (au plus
     une action désormais, Supprimer — absente pour un ingrédient NATIF, voir
     settings-library.js, la zone reste alors vide/sans effet), posée en JS à
     l'ouverture — voir swipe-row.js. */
  .library-ingredient-row-swipe.is-swiped .library-ingredient-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 56px)));
  }

  .library-ingredient-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    inset: -1px 0 -1px auto;
    /* Fond rouge, icône blanche (rowMenuDeleteIconSvg) — demandé explicitement. Éditer
       ne vit plus que dans le menu ⋮, voir admin-library.js/settings-library.js. */
    background: var(--danger);
    border-radius: 15px 0 0 15px;
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .library-ingredient-row-swipe.is-dragging .library-ingredient-row-swipe__actions-inner {
    transition: none;
  }

  .library-ingredient-row-swipe.is-swiped .library-ingredient-row-swipe__actions-inner {
    transform: translateX(0);
  }

  .library-ingredient-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    color: var(--accent-contrast);
  }

  .library-ingredient-row-swipe__action svg {
    width: 28px;
    height: 28px;
  }
}

/* Ligne "produit" (section "Bibliothèque > Produits", settings.php) : mêmes règles que
   .library-ingredient-row-swipe juste au-dessus, dupliquées sous un nom dédié — même
   convention que .recipe-row-swipe/.admin-user-row-swipe (composants identiques en CSS
   mais noms de classes séparés, voir CLAUDE.md / Tableaux). */
.library-article-row-swipe__link {
  transition: transform 0.2s ease;
}

.library-article-row-swipe__link img,
.library-article-row-swipe__link svg {
  display: block;
  width: 50px;
  height: 50px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--surface-alt);
}

.library-article-row-swipe.is-dragging .library-article-row-swipe__link {
  transition: none;
}

@media (hover: hover) and (pointer: fine) {
  .library-article-row-swipe:hover .row-menu-trigger,
  .library-article-row-swipe:focus-within .row-menu-trigger,
  .library-article-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  .library-article-row-swipe__actions {
    display: none;
  }
}

.library-article-row-swipe__action {
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0;
}

.library-article-row-swipe__actions {
  position: relative;
}

@media (hover: none), (pointer: coarse) {
  /* --reveal : largeur réelle de .library-article-row-swipe__actions-inner (une seule
     action désormais, Supprimer), posée en JS à l'ouverture — voir swipe-row.js. */
  .library-article-row-swipe.is-swiped .library-article-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 56px)));
  }

  .library-article-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    inset: -1px 0 -1px auto;
    /* Fond rouge, icône blanche (rowMenuDeleteIconSvg) — demandé explicitement. Éditer
       ne vit plus que dans le menu ⋮, voir settings-library-articles.js. */
    background: var(--danger);
    border-radius: 15px 0 0 15px;
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .library-article-row-swipe.is-dragging .library-article-row-swipe__actions-inner {
    transition: none;
  }

  .library-article-row-swipe.is-swiped .library-article-row-swipe__actions-inner {
    transform: translateX(0);
  }

  .library-article-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    color: var(--accent-contrast);
  }

  .library-article-row-swipe__action svg {
    width: 28px;
    height: 28px;
  }
}

/* Ligne "bibliothèque simple" (nom seul — Types de plat / Catégories d'article,
   settings.php / assets/js/simple-library-list.js) : contrairement à
   .library-ingredient-row-swipe/.library-article-row-swipe (dupliquées malgré des règles
   identiques, voir CLAUDE.md / Tableaux), une SEULE classe partagée par les deux listes
   ici — composant JS générique déjà partagé (simple-library-list.js), pas de raison de
   dupliquer aussi son CSS. Pas de règle de vignette (pas de colonne image, contrairement
   aux deux tableaux ci-dessus). Même hauteur de ligne que les listes à vignette
   (Ingrédients/Articles de course), demandé explicitement ("même si il n'y a pas de
   photo, on garde la même hauteur partout") : sans image, la cellule ne ferait que la
   hauteur du texte + padding — bien moins que la cellule vignette d'à côté (50px de
   photo + 12px de padding haut/bas = 74px, voir .library-ingredient-row-swipe__link
   img/svg plus haut). `height` sur une cellule de tableau se comporte comme un
   minimum (le contenu ne peut pas être écrasé en dessous), donc ce 74px fixé ici n'a
   aucun effet sur le texte lui-même (toujours centré verticalement par le
   `vertical-align` par défaut des cellules), juste sur la hauteur totale de la ligne. */
.library-simple-row-swipe__link {
  height: 74px;
  transition: transform 0.2s ease;
}

.library-simple-row-swipe.is-dragging .library-simple-row-swipe__link {
  transition: none;
}

@media (hover: hover) and (pointer: fine) {
  .library-simple-row-swipe:hover .row-menu-trigger,
  .library-simple-row-swipe:focus-within .row-menu-trigger,
  .library-simple-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  .library-simple-row-swipe__actions {
    display: none;
  }
}

.library-simple-row-swipe__action {
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0;
}

.library-simple-row-swipe__actions {
  position: relative;
}

@media (hover: none), (pointer: coarse) {
  /* --reveal : largeur réelle de .library-simple-row-swipe__actions-inner (une seule
     action désormais, Supprimer), posée en JS à l'ouverture — voir swipe-row.js. */
  .library-simple-row-swipe.is-swiped .library-simple-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 56px)));
  }

  .library-simple-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    inset: -1px 0 -1px auto;
    /* Fond rouge, icône blanche (rowMenuDeleteIconSvg) — demandé explicitement. Éditer
       ne vit plus que dans le menu ⋮, voir simple-library-list.js. */
    background: var(--danger);
    border-radius: 15px 0 0 15px;
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .library-simple-row-swipe.is-dragging .library-simple-row-swipe__actions-inner {
    transition: none;
  }

  .library-simple-row-swipe.is-swiped .library-simple-row-swipe__actions-inner {
    transform: translateX(0);
  }

  .library-simple-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    color: var(--accent-contrast);
  }

  .library-simple-row-swipe__action svg {
    width: 28px;
    height: 28px;
  }
}

/* Colonne "Type" filtrable (recipes.js) : bouton dans l'en-tête + menu contextuel,
   positionné en JS par rapport à .data-table-wrap (voir plus bas). */
.data-table-wrap {
  position: relative;
}

.th-filter,
.th-sort {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  font-weight: inherit;
  color: inherit;
  cursor: pointer;
}

.th-filter svg,
.th-sort svg {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}

/* Filtre actif : bouton (texte + symbole) en accent, comme les autres états actifs de
   l'app (nav, icônes "+"). */
.th-filter.is-active {
  color: var(--accent);
}

/* Tri "Recette" (recipes.js) : croissant par défaut, la flèche pivote à 180° au clic
   (ordre décroissant) avec une petite animation — même transition douce que le reste
   de l'app (0.2s ease). */
.th-sort svg {
  /* +80% par rapport aux 12px partagés avec .th-filter (chevrons Type/Personnes) :
     propre à l'icône de tri, ne touche pas les autres. */
  width: 22px;
  height: 22px;
  transition: transform 0.2s ease;
}

.th-sort.is-desc svg {
  transform: rotate(180deg);
}

.th-sort.is-desc {
  color: var(--accent);
}

.th-filter__menu {
  position: absolute;
  min-width: 160px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  overflow: hidden;
  z-index: 20;
  font-weight: 400;
  font-size: 14px;
}

.th-filter__menu[hidden] {
  display: none;
}

.th-filter__option {
  display: block;
  width: 100%;
  padding: 10px 14px;
  background: none;
  border: none;
  border-bottom: 1px solid var(--border);
  text-align: left;
  cursor: pointer;
  color: var(--text);
  font: inherit;
}

.th-filter__option:last-child {
  border-bottom: none;
}

@media (hover: hover) and (pointer: fine) {
  .th-filter__option:hover {
    background: var(--surface-alt);
  }
}

.th-filter__option.is-selected {
  color: var(--accent);
  font-weight: 600;
}

.data-table tbody a {
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 4px;
}

.data-table__thumb {
  width: 50px;
  height: 50px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--surface-alt);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
}

.data-table__thumb svg {
  width: 18px;
  height: 18px;
}

/* Liste des livres de recette (index.php/recipes.js) — icônes SVG de repli grossies de
   50% (50px -> 75px, icône interne 18px -> 27px), demandé explicitement. Sélecteur
   `span.data-table__thumb` (pas juste `.data-table__thumb`) : recipes.js rend un <span>
   autour d'un SVG pour "Toutes les recettes"/"Favoris" (toujours) et pour un vrai livre
   SANS couverture propre, mais un <img> pour un vrai livre AVEC une photo — cibler
   l'élément `span` précisément exclut donc cet <img> de cette règle (traitée séparément
   juste en dessous). Scopé aux lignes de CETTE liste (.collection-row = virtuelles ;
   .collection-row-swipe = vrais livres), .data-table__thumb restant à 50px partout
   ailleurs (recettes, ingrédients, articles, admin...) — classe générique partagée, pas
   touchée directement. */
.collection-row span.data-table__thumb,
.collection-row-swipe span.data-table__thumb {
  width: 75px;
  height: 75px;
}

.collection-row span.data-table__thumb svg,
.collection-row-swipe span.data-table__thumb svg {
  width: 27px;
  height: 27px;
}

/* Photo de couverture (<img>, ajoutée par l'utilisateur) — MÊME encombrement que
   l'icône SVG ci-dessus (75px), mais la photo elle-même doit rester à sa taille
   d'origine (50px), demandé explicitement (rectification du premier essai, qui avait
   agrandi la photo elle-même à 75px). Résolu par un padding plutôt qu'en laissant
   width/height à 50px : `* { box-sizing: border-box }` (tout en haut de ce fichier)
   fait qu'un padding RÉDUIT la zone de contenu au lieu de s'y ajouter — width/height à
   75px + padding 12.5px (75 - 12.5*2 = 50) donne donc un encombrement total de 75px
   avec une image effectivement dessinée sur 50px (object-fit:cover s'applique à la
   boîte de contenu, réduite par le padding, pas à la boîte totale). Le fond
   (var(--surface-alt), .data-table__thumb) reste visible dans le padding autour de la
   photo — même famille de rendu qu'un cadre, cohérent avec la boîte pleine (fond +
   icône) des repères sans photo ci-dessus. */
.collection-row img.data-table__thumb,
.collection-row-swipe img.data-table__thumb {
  width: 75px;
  height: 75px;
  padding: 12.5px;
}

/* Icônes/photos centrées sur un axe vertical commun, demandé explicitement — les deux
   variantes ci-dessus partagent désormais le même encombrement (75px), mais la colonne
   vignette (.data-table__col--thumb, 70px) reste plus étroite : sans ce centrage,
   chaque vignette serait plaquée au bord gauche de la colonne. `text-align: center`
   fonctionne ici car .data-table__thumb (<img> ET <span>) est inline-flex, donc de
   niveau "inline" pour ce calcul — plus spécifique que la règle générique
   `.data-table td { text-align: left }` (classe + :first-child vs classe seule),
   l'emporte donc sans !important. Le titre du livre (2ᵉ colonne) reste volontairement
   aligné à gauche sur sa première lettre, non concerné par cette règle (ciblée
   uniquement sur :first-child) — demandé explicitement, décalé de 20px séparément
   juste en dessous pour laisser de l'air à la vignette élargie. */
.collection-row td:first-child,
.collection-row-swipe td:first-child {
  text-align: center;
}

/* Titre décalé de 20px vers la droite, demandé explicitement — .data-table td:nth-child(2)
   a par défaut padding-left:0 (voir sa règle, plus haut dans ce fichier), collé contre la
   colonne vignette ; ce décalage laisse de l'air entre le titre et l'icône élargie
   ci-dessus, sans toucher cette règle générique partagée par les autres tableaux. */
.collection-row td:nth-child(2),
.collection-row-swipe td:nth-child(2) {
  padding-left: 20px;
}

/* Vue "tuiles" (recipe-collection.php), demandé explicitement — alternative à
   .data-table pour cette même liste, basculée en JS (#recipeListWrap/#recipeTiles,
   voir recipe-collection.js). auto-fill : autant de colonnes que la largeur le permet
   en desktop/tablette ; verrouillé à 2 en mobile ci-dessous (demandé explicitement). */
.recipe-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
}

/* .recipe-tiles définit display:grid (règle auteur), ce qui neutralise le display:none
   par défaut de l'attribut [hidden] (règle du user-agent stylesheet, toujours perdante
   face à une règle auteur de même spécificité) — même bug déjà rencontré/corrigé pour
   #ovenTempGroup/.th-filter__menu/.loading-overlay (voir leurs commentaires). Sans ça,
   les tuiles restaient visibles même en mode liste (signalé par l'utilisateur). */
.recipe-tiles[hidden] {
  display: none;
}

@media (max-width: 899px) {
  .recipe-tiles {
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
  }
}

.recipe-tile {
  position: relative; /* ancre .recipe-tile__menu-trigger (position:absolute) */
  display: flex;
  flex-direction: column;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--surface);
  overflow: hidden;
  color: var(--text);
  text-decoration: none;
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

/* Même box-shadow que le focus des champs (.input:focus) — var(--accent) suit déjà le
   thème (bleu clair/bleu sombre), aucune surcharge dark-mode séparée nécessaire. */
@media (hover: hover) and (pointer: fine) {
  .recipe-tile:hover {
    border-color: var(--accent);
    box-shadow: 0 0 5px 0 var(--accent);
  }
}

/* Menu "⋮" de la tuile — même menu contextuel que la vue liste (getRecipeMenuItems(),
   recipe-collection.js), demandé explicitement : rond opaque en haut à droite de la
   tuile, révélé au survol (souris/trackpad) ou toujours visible en tactile — même
   principe que .row-menu-trigger (voir CLAUDE.md / Détection tactile vs pointeur),
   juste positionné/habillé différemment (rond sur une photo plutôt qu'icône de
   cellule de tableau). Classe .row-menu-trigger conservée EN PLUS (JS s'appuie dessus,
   triggerSelector: '.row-menu-trigger') — les règles ci-dessous sont scopées à
   `.recipe-tile .recipe-tile__menu-trigger` (2 sélecteurs) pour l'emporter sur celles
   de `.row-menu-trigger` seule, indépendamment de l'ordre des règles dans ce
   fichier (même précaution que .icon-btn.topbar-back, voir CLAUDE.md — bug déjà
   rencontré une fois avec une seule classe). */
.recipe-tile .recipe-tile__menu-trigger {
  display: none;
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 1;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin: 0;
  border-radius: 50%;
  border: none;
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-sm);
}

.recipe-tile .recipe-tile__menu-trigger svg {
  width: 16px;
  height: 16px;
}

@media (hover: hover) and (pointer: fine) {
  .recipe-tile .recipe-tile__menu-trigger:hover {
    color: var(--svg-hover);
  }

  .recipe-tile .recipe-tile__menu-trigger {
    display: inline-flex;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease, color 0.2s ease;
  }

  .recipe-tile:hover .recipe-tile__menu-trigger,
  .recipe-tile:focus-within .recipe-tile__menu-trigger,
  .recipe-tile.row-menu-open .recipe-tile__menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }
}

/* Tactile (mobile ET tablette) — même raison que .row-menu-trigger : pas de survol au
   doigt, le rond reste donc toujours visible plutôt que révélé par un geste absent. */
@media (hover: none), (pointer: coarse) {
  .recipe-tile .recipe-tile__menu-trigger {
    display: inline-flex;
    opacity: 1;
    pointer-events: auto;
  }
}

/* Ratio ~5:3 (demandé explicitement — "à toi d'adapter le nombre de pixel") : padding-top
   en % force le ratio quelle que soit la largeur réelle de la tuile (auto-fill), sans
   dépendre du support de `aspect-ratio` (assez large aujourd'hui, mais cette technique
   n'a aucune dépendance). object-fit: cover pour ne jamais déformer la photo, comme
   .data-table__thumb. */
.recipe-tile__photo {
  position: relative;
  display: block;
  width: 100%;
  padding-top: 60%; /* 5:3 */
  background: var(--surface-alt);
}

.recipe-tile__photo img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.recipe-tile__name {
  padding: 10px 12px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Fond vitré cohérent avec .data-table/.people-row en mode Îdiää (voir Design system) —
   sinon la carte resterait pleinement opaque sur la photo de fond. */
:root[data-theme="idiaa"] .recipe-tile {
  background: var(--surface);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

/* Colonne "Personnes" : icône + nombre seulement (recipes.js), pas de mot "personnes". */
.data-table__servings {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* Même taille que .form-group__icon (champs des formulaires recette/profil). */
.data-table__servings-icon {
  width: 20px;
  height: 30px;
  flex-shrink: 0;
  color: var(--text-muted);
}

/* ---------- Ingrédients de la recette (recipe-edit.php) ---------- */
/* Décalées de 30px (largeur .form-group__icon + gap) pour s'aligner avec le texte des
   champs de saisie au-dessus, qui n'ont pas d'icône ici pour faire ce décalage naturellement. */
.titre-add-ingredient {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 30px 0 15px 30px;
  width: calc(100% - 30px);
}

/* Même format que "Illustration" (.list-header h2) : 20px / 600. */
.titre-add-ingredient h2 {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
}

.titre-add-ingredient svg {
  height: 26px;
  width: 26px;
  fill: var(--accent);
  cursor: pointer;
  transition: 0.2s;
}

@media (hover: hover) and (pointer: fine) {
  .titre-add-ingredient svg:hover {
    fill: var(--svg-hover);
    transform: scale(1.15);
  }
}

/* Même décalage de 30px que .titre-add-ingredient, pour aligner "Étapes de la
   recette" avec "Ingrédients de la recette" (et avec le texte des champs à icône
   au-dessus). */
.titre-section {
  margin: 30px 0 15px 30px;
  font-size: 20px;
  font-weight: 600;
}

/* Décalage de 30px pour s'aligner avec les champs à icône : seulement sur la page
   d'édition (#recipeIngredientsTable) — sur recipe-view.php (pas d'icônes au-dessus),
   .recipe-ingredients garde sa pleine largeur, alignée sous la photo. */
#recipeIngredientsTable {
  width: calc(100% - 30px);
  margin-left: 30px;
}

/* border-collapse/spacing explicites : bug corrigé, signalé par l'utilisateur sur
   capture — sans ça, l'espacement par défaut du navigateur (border-spacing:2px,
   séparé du border-collapse) laisse un petit espace entre la dernière ligne
   (.ingredient-row, qui porte son propre border-bottom) et le bord arrondi du
   tableau, invisible entre les lignes (déjà séparées par ce border-bottom) mais
   visible sous la toute dernière. Même correctif déjà appliqué à .data-table (voir
   plus haut) pour exactement la même raison — reproduit ici à l'identique. */
.recipe-ingredients {
  width: 100%;
  background-color: var(--surface);
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--text-muted);
  border-collapse: separate;
  border-spacing: 0;
}

/* Refonte couleurs sombre en cours (voir variables.css / .data-table) — même traitement
   que .data-table (pas de <th>/en-tête ici, juste le fond de la liste). */
:root[data-theme="dark"] .recipe-ingredients {
  background-color: rgb(43, 43, 43);
}

/* Espace avant "Étapes de la recette" (recipe-view.php) : scopé à cette page (pas
   .recipe-ingredients en général) pour ne pas toucher recipe-edit.php, où le titre
   "Étapes" a déjà sa propre marge (.titre-section). */
#recipeViewIngredientsTable {
  margin-bottom: 24px;
}

.recipe-ingredients td {
  padding: 12px;
  text-align: left;
}

.ingredient-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 1px solid var(--border);
}

/* Même correctif que #recipeViewIngredients ci-dessous (desktop, voir sa règle pour le
   détail), pour le mode mobile — colonne unique, signalé sur capture toujours visible
   après le premier correctif. Scopé à :last-child plutôt qu'au conteneur (contrairement
   au desktop) : .ingredient-row garde ici son display:flex par défaut (tbody/tr en
   layout table normal, pas grid), un <tr> reste table-row et ignore les margins — mais
   .ingredient-row a déjà display:flex, qui rend son propre margin effectif. En colonne
   unique (mobile ici, mais aussi recipe-edit.php à toute largeur, même classe partagée
   — voir CLAUDE.md), :last-child cible toujours sans ambiguïté la ligne visuellement la
   plus basse (pas de risque d'écart colonne gauche/droite comme sur la grille desktop,
   d'où le choix d'un mécanisme différent là-bas). */
@media (max-width: 899px) {
  .ingredient-row:last-child {
    margin-bottom: -2px;
  }
}

.ingredient-row img {
  width: 50px;
  height: 50px;
  object-fit: contain;
}

.ingredient-row .ligne-ingedient {
  flex: 1 1 auto;
  min-width: 0;
}

.ingredient-row svg {
  height: 30px;
  width: 30px;
  cursor: pointer;
}

/* Icône "Ajouter à la liste de courses" par ligne d'ingrédient (recipe-view.php),
   demandée explicitement en complément de l'icône "panier" du titre de section (tous
   les ingrédients d'un coup, voir juste au-dessus) — collée au bord droit de la ligne.
   flex-shrink:0 : ne doit jamais se faire comprimer par .ligne-ingedient, qui absorbe
   l'espace restant (flex:1, voir juste au-dessus). Même principe panier -> check vert
   que .icon-plus__cart/__check (recipe-view.js). Même taille que
   #recipeAddIngredientsToListBtn (31px, demandé explicitement — d'abord 20px, plus
   petite car pensée "contexte de ligne", égalisée ensuite sur demande). */
.ingredient-row__add-to-list {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 31px;
  height: 31px;
  flex-shrink: 0;
  margin-left: 12px;
  color: var(--accent);
  cursor: pointer;
  transition: color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .ingredient-row__add-to-list:hover {
    color: var(--svg-hover);
  }
}

.ingredient-row__add-to-list svg {
  width: 100%;
  height: 100%;
  cursor: pointer;
}

.ingredient-row__add-to-list__cart,
.ingredient-row__add-to-list__check {
  position: absolute;
  inset: 0;
  transition: opacity 0.3s ease;
}

.ingredient-row__add-to-list__check {
  opacity: 0;
}

.ingredient-row__add-to-list.is-added .ingredient-row__add-to-list__cart {
  opacity: 0;
}

.ingredient-row__add-to-list.is-added .ingredient-row__add-to-list__check {
  opacity: 1;
}

/* ---------- Popup de sélection d'ingrédient (fond flouté, comme .loading-overlay) ---------- */
.ingredient-selector {
  display: none;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  z-index: 100;
  padding: 20px;
}

.ingredient-selector.is-open {
  display: flex;
}

.ingredient-list {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 420px;
  max-height: 80vh;
  overflow: hidden;
  background-color: var(--surface);
  border: 1px solid var(--text-muted);
  border-radius: 8px;
  padding: 10px;
  box-shadow: 6px 6px 12px var(--shadow-neu);
}

.ingredient-list .zone-search-ingr {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
  gap: 10px;
  padding-bottom: 10px;
}

.ingredient-list .zone-search-ingr .input {
  flex: 1;
  margin-bottom: 0;
}

/* Pensée à l'origine pour l'icône "+" (nouvel ingrédient) qui vivait ici, à côté de la
   recherche — déplacée depuis dans l'en-tête de la popup (#newIngredientToggleBtn, voir
   sa règle dédiée plus bas et recipe-edit.php), demandé explicitement. Ne cible donc
   plus que le <svg> de .search-clear (imbriqué dans .search-field, à l'intérieur de
   cette zone) — sans effet visible dessus, une règle plus spécifique
   (.zone-search-ingr .search-field .search-clear svg, plus bas) réaffirme sa propre
   taille/couleur et l'emporte. Gardée telle quelle (pas de régression identifiée à la
   retirer, voir son commentaire) plutôt que supprimée. */
.ingredient-list .zone-search-ingr svg {
  height: 26px;
  width: 26px;
  flex-shrink: 0;
  fill: var(--accent);
  cursor: pointer;
  transition: 0.2s;
}

@media (hover: hover) and (pointer: fine) {
  .ingredient-list .zone-search-ingr svg:hover {
    fill: var(--svg-hover);
    transform: scale(1.15);
  }
}

/* "+" (nouvel ingrédient) déplacé dans l'en-tête de la popup, à droite (voir
   recipe-edit.php) — même style que son ancien emplacement dans .zone-search-ingr
   ci-dessus (repris ici plutôt que dupliqué via une classe partagée : le <svg> brut
   n'a pas fill="currentColor" comme idiaa_icon('plus'), donc dépend d'une règle
   `fill:` dédiée à son id, comme avant). Ciblé par id (#newIngredientToggleBtn,
   unique dans la page) plutôt que par une classe sur .list-header — .list-header est
   réutilisé sur de nombreuses autres pages, un sélecteur par id reste sûr sans risque
   de contamination croisée. */
#newIngredientToggleBtn {
  height: 26px;
  width: 26px;
  flex-shrink: 0;
  fill: var(--accent);
  cursor: pointer;
  transition: 0.2s;
}

/* MODE SOMBRE UNIQUEMENT, demandé explicitement ("ces couleurs sont pour le thème
   sombre, on s'occupera du clair après") — blanc, voir :root[data-theme="dark"]
   .list-header--icon .icon-btn plus haut dans ce fichier pour le même changement/même
   raison. Clair/système/Îdiää gardent fill: var(--accent) ci-dessus, inchangé. */
:root[data-theme="dark"] #newIngredientToggleBtn {
  fill: #fff;
}

/* Réduite de 10% en desktop UNIQUEMENT (26px -> 23,4px), demandé explicitement — même
   principe que .list-header__add.icon-plus (Page liste des recettes, réduite en
   mobile, desktop inchangé) mais dans l'autre sens : ici c'est le mobile qui garde la
   taille de base. Même seuil `900px` que le reste de l'app pour la mise en page
   desktop/mobile (voir CLAUDE.md / Détection tactile vs pointeur — ce seuil-ci reste
   dédié à la mise en page, pas à hover/pointer, ce réglage n'a rien à voir avec le
   tactile). */
@media (min-width: 900px) {
  #newIngredientToggleBtn {
    height: 23.4px;
    width: 23.4px;
  }
}

@media (hover: hover) and (pointer: fine) {
  #newIngredientToggleBtn:hover {
    fill: var(--svg-hover);
    transform: scale(1.15);
  }
  :root[data-theme="dark"] #newIngredientToggleBtn:hover {
    fill: #007aff;
  }
}

.ingredient-list .zone-liste-ingr {
  overflow-y: auto;
  max-height: 420px;
}

.zone-liste-ingr .ingredient-item {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
  padding: 10px;
  border-radius: 8px;
  transition: background-color 0.2s;
  border-bottom: 1px solid var(--border);
}

.zone-liste-ingr .ingredient-item:hover {
  background-color: var(--accent-tint);
}

.zone-liste-ingr .ingredient-item img,
.zone-liste-ingr .ingredient-item svg:not(.ingredient-item__add) {
  width: 40px;
  height: 40px;
  flex-shrink: 0;
  object-fit: contain;
  color: var(--text-muted);
}

.zone-liste-ingr .ingredient-item .nom_ingr {
  flex: 1;
  font-size: 14px;
  color: var(--text);
}

.zone-liste-ingr .ingredient-item .input-qte {
  flex-grow: 0;
  width: 60px;
  height: 30px;
  border: 1px solid var(--text-muted);
  border-radius: var(--radius-sm);
  outline: none;
  padding: 5px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  font-size: 14px;
  color: var(--text);
  background-color: var(--surface);
  box-sizing: border-box;
  transition: .3s;
}

.zone-liste-ingr .ingredient-item .input-qte:focus {
  border-color: var(--accent);
  box-shadow: 0 0 5px 0 var(--accent);
}

/* Doit venir après la règle de base ci-dessus (même spécificité) pour gagner sur mobile
   — voir la note sur le zoom iOS Safari plus haut dans ce fichier. */
@media (max-width: 899px) {
  .zone-liste-ingr .ingredient-item .input-qte {
    font-size: 16px;
  }
}

/* Refonte couleurs sombre en cours (voir variables.css) — même valeur fixe que les
   champs de saisie/zones d'image (.input/.upload-zone, rgb(52, 53, 52)) plutôt que
   var(--surface) (partagée avec d'autres éléments non concernés par cette demande) —
   .input-qte n'a pas la classe .input, donc pas déjà couverte par sa règle dédiée.
   border-color ajoutée dans la foulée (manquait ici, .input-qte restait sur
   var(--text-muted) — #98989f en sombre, un gris plus clair/visible que les
   rgb(119, 111, 112) de .input) : demandé explicitement que TOUS les champs de
   saisie de l'app, y compris les champs de recherche, partagent le même design que
   .input-qte — dans ce sens précis (bordure sombre), c'est .input-qte qui rejoint la
   valeur déjà standard de .input plutôt que l'inverse, pour qu'aucun champ ne
   détonne. */
:root[data-theme="dark"] .zone-liste-ingr .ingredient-item .input-qte {
  background-color: rgb(52, 53, 52);
  border-color: rgb(119, 111, 112);
}

/* Largeur fixe (pas min-width) : sinon une unité plus longue ("gousse" pour l'aïl)
   grandit et pousse .input-qte plus à gauche, désalignant les lignes entre elles. */
.zone-liste-ingr .ingredient-item .qte_ingr {
  font-size: 13px;
  color: var(--text-muted);
  width: 48px;
  flex-shrink: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Superpose l'icône "ajouter" et le check vert : fondu enchaîné de l'une vers l'autre
   au clic (voir .is-added, ajoutée en JS), plutôt que de remplacer le SVG dans le DOM. */
.zone-liste-ingr .ingredient-item .ingredient-item__action {
  position: relative;
  height: 26px;
  width: 26px;
  flex-shrink: 0;
}

.zone-liste-ingr .ingredient-item .ingredient-item__action svg {
  position: absolute;
  inset: 0;
  height: 100%;
  width: 100%;
  transition: opacity 0.3s ease;
}

/* color (pas fill) : le <path> du SVG a fill="currentColor"/stroke="currentColor" en
   attribut de présentation, qui ne suit que la propriété CSS "color", pas "fill". */
.zone-liste-ingr .ingredient-item .ingredient-item__add {
  color: var(--accent);
  cursor: pointer;
  opacity: 1;
  transition: color 0.2s ease, transform 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .zone-liste-ingr .ingredient-item .ingredient-item__add:hover {
    color: var(--svg-hover);
    transform: scale(1.15);
  }
}

.zone-liste-ingr .ingredient-item .ingredient-item__check {
  opacity: 0;
  pointer-events: none;
}

.zone-liste-ingr .ingredient-item .ingredient-item__action.is-added {
  pointer-events: none;
}

.zone-liste-ingr .ingredient-item .ingredient-item__action.is-added .ingredient-item__add {
  opacity: 0;
}

.zone-liste-ingr .ingredient-item .ingredient-item__action.is-added .ingredient-item__check {
  opacity: 1;
}

/* Menu déroulant "Livres de recette" (recipe-edit.php, #collectionsDropdown), demandé
   explicitement — remplace l'ancienne popup plein écran. Même famille visuelle que
   .row-menu (position:fixed, fond/bordure/ombre/glass Îdiää identiques) mais garde ses
   propres classes : contenu différent (cases à cocher + mini-formulaire "Nouveau livre"),
   et reste ouvert entre deux clics contrairement à .row-menu. */
.collections-dropdown {
  position: fixed;
  z-index: 200;
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  padding: 6px;
}

:root[data-theme="idiaa"] .collections-dropdown {
  background-color: var(--surface-alt);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

.collections-dropdown__list {
  max-height: 240px;
  overflow-y: auto;
}

/* Utilisée à la fois sur un <label> (Livres de recette, case à cocher) et sur un
   <button> (Thermostat du four, choix unique) — border/background/text-align/font
   réinitialisés pour que le second n'hérite pas du style bouton par défaut du
   navigateur (même resets que .row-menu__item, autre bouton de liste de l'app) ; sans
   effet sur le <label> existant, qui n'a aucun de ces styles par défaut. */
.collections-dropdown__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  border: none;
  background: none;
  text-align: left;
  font: inherit;
  padding: 8px 10px;
  border-radius: 6px;
  font-size: 14px;
  color: var(--text);
  cursor: pointer;
  transition: background-color 0.15s ease;
}

.collections-dropdown__item:hover {
  background-color: var(--accent-tint);
}

.collections-dropdown__item input[type="checkbox"] {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  cursor: pointer;
}

.collections-dropdown__new {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px 2px;
  border-top: 1px solid var(--border);
  margin-top: 4px;
}

.collections-dropdown__new .input {
  margin-bottom: 0;
}

/* #pickerCancel — HISTORIQUE : décrivait ici le bouton "Fermer" pleine largeur en bas
   de la popup (358px/30px/flex-shrink:0, centré via margin:auto), puis une icône
   `.icon-btn` seule à droite d'un en-tête "Ingrédient" (avec un padding-left artificiel
   sur `#ingredientList .list-header` pour compenser l'absence d'élément à gauche et
   garder le titre centré), puis un bouton TEXTE ("ok", `.list-header__ok`) en premier
   enfant de l'en-tête. **État final, demandé explicitement pour toutes les popups de
   l'app** : ce bouton texte "ok" est remplacé par la MÊME icône que le "Retour" de la
   topbar (`idiaa_icon('back')`, classe générique `.icon-btn` — aucune règle CSS dédiée
   nécessaire, contrairement à `.list-header__ok` ci-dessus qui a donc été retirée) —
   toujours en PREMIER enfant de l'en-tête (bord gauche), l'icône "+" (nouvel
   ingrédient, `#newIngredientToggleBtn`, voir plus bas) en DERNIER enfant (bord droit) :
   `.list-header--centered-title` (flex:1 + text-align:center sur le h2) centre
   naturellement le titre entre ces deux vrais éléments de bord. */

/* #newIngredientCancel — HISTORIQUE : décrivait ici le bouton "Fermer" pleine largeur
   en bas de la popup "Nouvel ingrédient" (358px/30px, centré via margin:auto), puis un
   bouton texte "ok". Même traitement que #pickerCancel ci-dessus (voir son commentaire
   HISTORIQUE juste au-dessus) : #newIngredientCancel porte désormais la même icône de
   fermeture (`.icon-btn`/`idiaa_icon('back')`), posée en PREMIER enfant de l'en-tête de
   cette même popup, symétrique à #pickerCancel. */

/* ---------- Section "Profil" (settings.php) ---------- */
/* Ex-popup ouverte depuis l'icône de la topbar, retirée sur demande explicite (voir
   CLAUDE.md) : devenue une section normale de settings.php, .card.card--surface comme
   "Apparence"/"Bibliothèque" ci-dessous (fond/bordure/ombre + flou Îdiää déjà couverts
   par cette classe générique partagée, rien de dédié à recréer ici). "Mon profil" +
   "Se déconnecter" sur la même ligne : réutilise .list-header/.list-header--icon
   (index.php...) plutôt qu'une classe de titre dédiée — même famille "titre + action à
   droite" que le "+" des autres listes de l'app. */
.profile-section__logout {
  /* .list-header n'a pas de justify-content:space-between (contrairement à l'ancien
     .profile-popup__header) — pousse ce bouton au bord droit, même principe que
     .list-header__add (icône "+"). */
  margin-left: auto;
}

/* Liens CGU/Confidentialité, dernier élément de la section (voir CLAUDE.md / Pages
   légales — un des emplacements déjà en place, ex-.profile-popup__legal). */
.profile-section__legal {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: 28px;
  font-size: 12px;
  color: var(--text-muted);
}

.profile-section__legal a {
  color: var(--text-muted);
  text-decoration: none;
}

.profile-section__legal a:hover {
  color: var(--accent);
  text-decoration: underline;
}

/* Bouton compact générique (neumorphique, hover accent -> svg-hover) : "Se déconnecter"
   (section Profil, settings.php), Accepter/Refuser/Annuler/Inviter
   (.people-row__action, friends.php), "Envoyer" du composeur de chat. Compose avec
   .idiaa-btn (même mécanisme que .btn--neu/.btn) pour le rendu Îdiää transparent/
   contour, sans toucher aux dimensions ici. */
.btn--pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px 14px;
  color: var(--accent);
  font-size: 13px;
  font-weight: 500;
  border-radius: 0.8em;
  background: var(--bg);
  cursor: pointer;
  border: 1px solid var(--ombre);
  box-shadow: 3px 3px 6px var(--ombre), -3px -3px 6px var(--surface);
  transition: all 0.2s ease-in-out;
  white-space: nowrap;
}

@media (hover: hover) and (pointer: fine) {
  .btn--pill:hover {
    color: var(--svg-hover);
    box-shadow: 3px 3px 6px var(--surface), -3px -3px 6px var(--surface);
  }
}

.btn--pill:disabled {
  opacity: 0.5;
  cursor: default;
  box-shadow: none;
}

.btn--pill:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Variante "danger" (Refuser) : rouge au lieu du bleu/accent par défaut. */
.btn--pill--danger {
  color: var(--danger);
}

@media (hover: hover) and (pointer: fine) {
  .btn--pill--danger:hover {
    color: var(--danger);
    opacity: 0.75;
  }
}

#credentialsSection {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

/* "Appareils connectés" (#sessionsSection) — même séparateur qu'#credentialsSection
   juste au-dessus (toujours visible, contrairement à cette dernière réservée aux
   comptes email/mot de passe, donc toujours la dernière section "de contenu" avant les
   liens légaux). */
#sessionsSection {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

.profile-sessions-hint {
  font-size: 13px;
  color: var(--text-muted);
  margin: 0 0 8px;
}

/* .toggle-row (settings.php) réutilisée telle quelle pour chaque ligne d'appareil —
   seul ajout : le bloc device/date à gauche n'est pas un <label>/texte seul comme dans
   son usage d'origine, mais deux lignes empilées (nom + date, voir
   assets/js/profile-sessions.js) ; ":last-child { border-bottom: none }" déjà couvert
   par la règle générique .toggle-row, rien à ajouter pour ça ici. */

/* "Changer le pseudo" / "Changer l'email" / "Changer le mot de passe" (section Profil) :
   pleine largeur de la section (comme les autres boutons de settings.php), hauteur 40px
   — remplace les 30px hérités des champs de saisie .input:not(textarea). Plus de
   centrage/largeur réduite (calc(100% - 30px)) : c'était propre à l'ancienne popup
   (420px de large), sans objet dans une section pleine largeur comme les autres cartes
   de cette page. */
#profileForm .btn--neu,
#emailForm .btn--neu,
#passwordForm .btn--neu {
  width: 100%;
  height: 40px;
  font-size: 14px;
}

/* Vides par défaut (pas encore d'erreur/succès), ces <p> ne doivent pas réserver
   d'espace : sinon impossible d'aligner le bouton juste sous le dernier champ avec le
   même espacement qu'entre deux champs. :empty les efface tant qu'ils n'ont pas de
   texte (JS les remplit via .textContent, ce qui les fait réapparaître automatiquement
   — aucun changement JS nécessaire). Scopé à ces formulaires : .form-error/.form-success
   gardent leur hauteur réservée habituelle partout ailleurs dans l'app. */
#profileForm .form-error:empty,
#emailForm .form-error:empty,
#emailForm .form-success:empty,
#passwordForm .form-error:empty,
#passwordForm .form-success:empty {
  display: none;
}

/* Le bouton colle au dernier champ avec le même gap qu'entre deux champs (10px) : marge
   nulle ici, la marge basse du champ (10px) suffit — les deux fusionnent (collapse)
   puisque .form-error/.form-success n'occupent plus d'espace entre les deux quand vides. */
#profileForm .form-actions,
#emailForm .form-actions,
#passwordForm .form-actions {
  margin-top: 0;
}

/* Écart entre les deux formulaires (bouton "Changer l'email" -> premier champ de
   #passwordForm), doublé (10px x2 x2 = 40px). */
#passwordForm {
  margin-top: 40px;
}

/* ---------- Popup d'ajout d'ami (friends.php) ---------- */
/* Mêmes valeurs que .ingredient-selector/.ingredient-list (picker d'ingrédient) — classe
   dédiée plutôt que réutilisée telle quelle, même convention qu'ailleurs dans l'app (deux
   popups sans rapport ne partagent pas un nom de classe, même quand leur rendu est
   identique). */
.friend-add-overlay {
  display: none;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  z-index: 100;
  padding: 20px;
}

.friend-add-overlay.is-open {
  display: flex;
}

.friend-add-panel {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 420px;
  max-height: 80vh;
  overflow: hidden;
  background-color: var(--surface);
  border: 1px solid var(--text-muted);
  border-radius: 8px;
  padding: 10px;
  box-shadow: 6px 6px 12px var(--shadow-neu);
}

.friend-add-panel .idiaa-field {
  margin: 0 0 10px;
}

.friend-add-panel__results {
  overflow-y: auto;
  max-height: 420px;
}

.friend-add-panel__empty {
  color: var(--text-muted);
  font-size: 13px;
  text-align: center;
  padding: 20px 10px;
}

/* Même principe que #libraryEditIngredientCancel (popup "Modifier l'ingrédient",
   settings.php) : pleine largeur, 30px, flex-shrink:0 pour ne pas se faire comprimer
   si .friend-add-panel__results dépasse l'espace disponible sous max-height:80vh. Les
   deux popups d'ingrédient de recipe-edit.php (picker ET "Nouvel ingrédient") n'ont
   plus de bouton "Fermer" en bas (remplacées par un en-tête à icônes, voir
   .ingredient-list #pickerCancel/#newIngredientCancel). */
.friend-add-panel #friendAddClose {
  width: 100%;
  margin-top: 10px;
  height: 30px;
  flex-shrink: 0;
}

/* Popup "Ajouter à l'écran d'accueil" (Safari iOS, #pwaIosHelpOverlay,
   includes/pwa-ios-help.php, voir assets/js/pwa-install.js) — même panneau
   .friend-add-panel que "Ajouter un repas" (meal-event-view.php), texte centré,
   icône de partage en visuel principal. */
.pwa-ios-help-panel {
  text-align: center;
}

.pwa-ios-help-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 8px 0;
  color: var(--text);
}

.pwa-ios-help-icon svg {
  width: 40px;
  height: 40px;
}

.pwa-ios-help-step {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-weight: 600;
  margin-bottom: 20px;
}

.pwa-ios-help-step svg {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

/* Même correctif que #mealEventSlotClose/#mealEventEntryClose (voir plus haut) : 20px
   entre la bordure basse de "Fermer" et celle de la popup — .friend-add-panel a déjà
   10px de padding propre, margin-bottom:10px s'y ajoute pour les 20px voulus. */
#pwaIosHelpClose {
  margin-bottom: 10px;
}

/* ---------- Popup de création de liste (shopping-lists.php) ---------- */
/* Même famille que .friend-add-overlay/.ingredient-selector — classe dédiée plutôt que
   réutilisée, même convention (popups sans rapport ne partagent pas de nom de classe). */
.list-create-overlay {
  display: none;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  z-index: 100;
  padding: 20px;
}

.list-create-overlay.is-open {
  display: flex;
}

.list-create-panel {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 420px;
  max-height: 80vh;
  overflow: hidden;
  background-color: var(--surface);
  border: 1px solid var(--text-muted);
  border-radius: 8px;
  padding: 10px;
  box-shadow: 6px 6px 12px var(--shadow-neu);
}

.list-create-panel .idiaa-field {
  margin: 0 0 10px;
}

/* Champ "Nom" des popups "Nouvel événement" (meal-events.php), "Nouveau livre de
   recette" (index.php) et "Nouvelle liste" (shopping-lists.php, même demande
   appliquée "exactement pareil" à chaque fois) — 20px avant ce qui suit (groupe de
   dates / ligne "Choisir une image :") plutôt que les 10px par défaut ci-dessus,
   demandé explicitement. :has(#xxxInput) (id unique) l'emporte sur .list-create-panel
   .idiaa-field (2 classes) grâce à la spécificité de l'id porté par :has(), sans
   dépendre de l'ordre des règles — même technique que
   .form-group:has(#newArticleCategory) (shopping-list-view.php). */
.idiaa-field:has(#mealEventNameInput),
.idiaa-field:has(#collectionNameInput),
.idiaa-field:has(#listNameInput) {
  margin-bottom: 20px;
}

/* Couverture du livre (photo ou icône) : 20px avant la ligne "Ou choisir une icône :"
   en dessous, demandé explicitement (remplace les 10px précédents, avant l'ajout de
   la ligne "Choisir une image :" juste au-dessus — voir .upload-zone-label). */
#collectionCreateImageZone {
  margin-bottom: 20px;
}

/* Ligne "Choisir une image :" (popups "Nouvel événement"/"Nouveau livre de recette"),
   au-dessus de la zone photo — même style que .collection-icon-grid__label ci-dessous
   (classe séparée : rôle différent, alignée sur la zone photo plutôt que la grille
   d'icônes), 5px avant la zone photo demandé explicitement ; le 20px au-dessus vient
   du champ "Nom" (ou du groupe de dates pour l'événement) juste au-dessus, pas d'ici. */
.upload-zone-label {
  margin: 0 0 5px;
  font-size: 12px;
  color: var(--text-muted);
}

/* 20px entre la zone photo et la ligne "Ou choisir une icône :" en dessous, demandé
   explicitement — #mealEventCreateImageZone n'a par défaut aucune marge propre (voir
   .upload-zone, règle générale). */
#mealEventCreateImageZone {
  margin-bottom: 20px;
}

/* 5px avant la grille d'icônes, demandé explicitement (mêmes deux popups que
   ci-dessus) — remplace les 6px d'origine. */
.collection-icon-grid__label {
  margin: 0 0 5px;
  font-size: 12px;
  color: var(--text-muted);
}

/* Grille scrollable (max-height propre, pas de wrapper __body dédié — voir
   .article-list__body pour l'alternative "popup entière défile" : ici seule la grille
   a besoin de défiler, le reste du panneau (nom, zone photo, en-tête) reste toujours
   visible) : ~78 icônes aujourd'hui, plus après (n'importe quel .svg déposé dans
   assets/img/icons-gallery/ rejoint automatiquement la liste, voir
   includes/collection-icons.php). Hauteur agrandie (168px -> 260px), demandé
   explicitement : les boutons "Ajouter"/"Fermer" du bas ont été retirés (remplacés
   par les icônes de l'en-tête, voir #collectionCreateCancel/Submit plus haut), place
   récupérée reversée à la grille. */
.collection-icon-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
  gap: 6px;
  max-height: 260px;
  overflow-y: auto;
  /* overflow-x explicite (bug corrigé au passage, repéré en creusant la demande sur
     .collection-icon-grid--full ci-dessous) : sans elle, overflow-x reste à sa valeur
     initiale 'visible', et la règle CSS "si l'un de overflow-x/overflow-y vaut
     'visible' et l'autre non, le 'visible' devient 'auto'" transformait silencieusement
     overflow-x en 'auto' dès que overflow-y:auto était posé seul — un balayage tactile
     horizontal restait donc possible dans cette grille, sans aucune raison de layout
     réelle (les colonnes ne débordent jamais, repeat(auto-fill, ...) s'adapte déjà à la
     largeur du conteneur). */
  overflow-x: hidden;
  margin-bottom: 10px;
}

/* Variante page dédiée (recipe-collection-edit.php, création d'un livre en mobile —
   voir CLAUDE.md). Historique : la grille s'étendait ici jusqu'en bas de la page, qui
   défilait alors comme un tout (max-height:none, overflow:visible) — décision
   INVERSÉE depuis, demandé explicitement ("seule la zone d'icône défile, les champs
   du dessus restent fixes") : voir body:has(#collectionCreateForm) ci-dessous, qui
   verrouille désormais le défilement du DOCUMENT entier sur cette page précise et ne
   laisse que la grille défiler en interne, dans l'espace restant sous les champs.
   touch-action:pan-y conservé (verrouille l'axe horizontal du balayage À L'INTÉRIEUR
   de la grille, même raison qu'ailleurs dans l'app, voir Design system / Verrouillage
   du mouvement horizontal en mobile) — toujours pertinent maintenant que la grille
   défile réellement en interne. */
.collection-icon-grid--full {
  touch-action: pan-y;
}

/* Verrouillage de la page pour "Nouveau livre de recette" mobile
   (recipe-collection-edit.php) — demandé explicitement. #collectionCreateForm (id
   unique à cette page) sert d'ancre à :has() pour cibler précisément CETTE page sans
   ajouter de classe dédiée dans le HTML partagé (.main-content/.idiaa-page-wrap,
   utilisés par de nombreuses autres pages qui doivent, elles, garder le défilement
   normal du document). Chaîne flex imbriquée body -> .main-content -> .idiaa-page-wrap
   -> #collectionCreateForm, chacune "flex:1 + min-height:0 + overflow:hidden" pour
   transmettre la hauteur disponible jusqu'au formulaire sans avoir à recalculer à la
   main la hauteur de la topbar/du dock (déjà réservée par le padding-top/bottom de
   body, voir plus haut — 100dvh moins ce padding, en box-sizing:border-box déjà
   global à l'app, donne exactement l'espace restant). 100dvh (pas 100vh) : évite que
   la barre d'adresse mobile qui va et vient ne fasse dépasser le contenu (même classe
   de piège que background-attachment:fixed en son temps, voir Design system / Points
   ouverts) — dvh suit la hauteur RÉELLE du viewport visible, vh non. */
body:has(#collectionCreateForm) {
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

body:has(#collectionCreateForm) .main-content,
body:has(#collectionCreateForm) .idiaa-page-wrap {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

#collectionCreateForm {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Tout, dans le formulaire, garde sa hauteur naturelle et reste fixe à l'écran — SAUF
   la grille d'icônes (règle suivante), seule à défiler dans l'espace restant. */
#collectionCreateForm > *:not(#collectionIconGrid) {
  flex-shrink: 0;
}

#collectionIconGrid {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

/* Étend la grille jusqu'en bas de l'écran, demandé explicitement — mobile uniquement
   (<900px, cette page n'existe de toute façon que là, voir CLAUDE.md, mais gardé
   explicite pour ne rien changer si jamais atteinte en desktop par URL directe).
   padding-bottom retiré des DEUX conteneurs qui en portent un ici (.main-content
   24px toutes apparences, .idiaa-page-wrap 24px Îdiää seulement) : sans les deux, la
   grille (flex:1, voir ci-dessus) resterait bornée avant le vrai bas de l'écran. Pas
   touché : padding-top/gauche/droite des deux (la mise en page du reste du formulaire,
   au-dessus, reste inchangée), ni le bas de .idiaa-page-wrap lui-même (bordure/coin
   arrondi conservés — juste le vide intérieur qui disparaît, pas le panneau). */
@media (max-width: 899px) {
  body:has(#collectionCreateForm) .main-content {
    padding-bottom: 0;
  }

  :root[data-theme="idiaa"] body:has(#collectionCreateForm) .idiaa-page-wrap {
    padding-bottom: 0;
  }
}

.collection-icon-grid__item {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 1;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface-alt);
  cursor: pointer;
  transition: border-color 0.2s ease;
}

.collection-icon-grid__item img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.collection-icon-grid__item.is-selected {
  border-color: var(--accent);
  border-width: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .collection-icon-grid__item:hover {
    border-color: var(--accent);
  }
}

/* 20px avant la ligne "Ou choisir une icône :" en dessous, demandé explicitement —
   même valeur que #collectionCreateImageZone/#mealEventCreateImageZone ci-dessus. Règle
   bare (pas de largeur/centrage) : couvre aussi #listCreateImageZone sur
   shopping-list-create.php (page dédiée mobile, voir plus bas — même enfant flex que
   #collectionCreateImageZone sur recipe-collection-edit.php, s'étire déjà pleine
   largeur sans règle dédiée). La popup (#listCreateOverlay), elle, garde en plus sa
   largeur fixe 358px via la règle scopée .list-create-panel juste en dessous, qui
   l'emporte (plus spécifique) sans dupliquer ce margin-bottom. */
#listCreateImageZone {
  margin-bottom: 20px;
}

/* Largeur fixe 358px, demandé explicitement à l'origine (même valeur que
   "Ajouter"/"Fermer" ci-dessous, popup "Nouvelle liste" UNIQUEMENT — pas la page dédiée
   ci-dessus, qui garde la pleine largeur comme les autres champs) — margin gauche/droite
   auto pour centrer, .list-create-panel étant un flex column (un enfant à largeur
   explicite ne "stretch" plus, même piège que #newIngredientImageZone, popup "Nouvel
   ingrédient"). */
.list-create-panel #listCreateImageZone {
  width: 358px;
  margin-left: auto;
  margin-right: auto;
}

/* Marge basse de la grille d'icônes alignée sur le padding latéral de la popup (10px,
   .list-create-panel), demandé explicitement — sans ces deux règles, l'espace réel en
   bas (marge propre de la grille + hauteur réservée par #listCreateError/
   #collectionCreateError même vides, voir .form-error) dépassait largement les 10px
   visibles à gauche/droite. #xxxCreateError:empty masque le message d'erreur tant
   qu'il est vide (comme #newArticleForm .form-error:empty, shopping-list-view.php)
   plutôt que de réserver sa hauteur (18px) pour rien ; #xxxIconGrid retombe alors
   directement sur le padding du panneau pour ses 10px du bas, sans marge propre
   supplémentaire. Même correctif appliqué à "Nouvelle liste" (shopping-lists.php),
   "Nouveau livre de recette" (index.php) et "Nouvel événement" (meal-events.php),
   demandé séparément pour chacune. */
#listCreateError:empty,
#collectionCreateError:empty,
#mealEventCreateError:empty {
  display: none;
}

#listIconGrid,
#collectionIconGrid,
#mealEventCreateIconGrid {
  margin-bottom: 0;
}

/* #listCreateSubmit/#listCreateCancel — HISTORIQUE : décrivaient ici les boutons
   "Ajouter"/"Fermer" pleine largeur en bas de la popup "Nouvelle liste" (358px/40px).
   Retirés, demandé explicitement — même traitement que la popup "Nouveau livre de
   recette" (#collectionCreateOverlay, index.php) : les deux ids sont désormais des
   icônes (.icon-btn) posées en en-tête (voir shopping-lists.php), aucune règle de
   largeur/hauteur dédiée nécessaire (taille standard de .icon-btn). */

/* ---------- Popup "Passe à Îdiää Pro" (assets/js/subscription-upsell.js) ---------- */
/* Même famille que .list-create-overlay/.ingredient-selector — classe dédiée plutôt que
   réutilisée telle quelle, même convention. Construite en JS et ajoutée à document.body
   (pas de markup PHP dupliqué par page), donc naturellement hors de .idiaa-page-wrap. */
.upsell-overlay {
  display: none;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  z-index: 100;
  padding: 20px;
}

.upsell-overlay.is-open {
  display: flex;
}

.upsell-panel {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 420px;
  max-height: 80vh;
  overflow: hidden;
  background-color: var(--surface);
  border: 1px solid var(--text-muted);
  border-radius: 8px;
  padding: 10px;
  box-shadow: 6px 6px 12px var(--shadow-neu);
}

.upsell-panel h2 {
  margin: 0 0 8px;
}

.upsell-panel__reason {
  color: var(--text-muted);
  font-size: 14px;
  margin: 0 0 12px;
}

.upsell-benefits {
  list-style: none;
  margin: 0 0 16px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.upsell-benefits li {
  font-size: 14px;
  padding-left: 24px;
  position: relative;
}

/* Coche devant chaque avantage — même vert que le check du picker d'ingrédient
   (#1ad569), couleur sémantique hors du monochrome Îdiää comme --danger ailleurs. */
.upsell-benefits li::before {
  content: '✓';
  position: absolute;
  left: 0;
  color: #1ad569;
  font-weight: 700;
}

.upsell-panel #upsellSubscribe,
.upsell-panel #upsellClose {
  width: 100%;
  height: 30px;
  flex-shrink: 0;
  margin-top: 10px;
}

:root[data-theme="idiaa"] .upsell-panel {
  background-color: var(--surface-alt);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

/* ---------- Popup de confirmation "Oui"/"Non" (confirm-dialog.js) ---------- */
/* Remplace window.confirm() quand ses boutons doivent être personnalisés — demandé
   explicitement ("Cancel" -> "Non", recipe-edit.js) : les boutons d'un window.confirm()
   natif ("OK"/"Cancel") sont imposés par le navigateur (langue système, pas celle de
   l'app) et ne peuvent être renommés par aucune API web. DOM injecté dynamiquement dans
   <body> au premier appel (comme .row-menu/.date-picker-panel), pas une popup statique
   par page — même famille visuelle que .upsell-overlay/.upsell-panel ci-dessus (fond
   assombri + flouté, panneau centré/bordé/ombré, glass Îdiää), classes dédiées plutôt
   que réutilisées telles quelles (deux popups sans rapport ne partagent pas de nom de
   classe, même convention qu'ailleurs dans l'app). */
.confirm-dialog-overlay {
  display: none;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  z-index: 100;
  padding: 20px;
}

.confirm-dialog-overlay.is-open {
  display: flex;
}

.confirm-dialog-panel {
  width: 100%;
  max-width: 360px;
  background-color: var(--surface);
  border: 1px solid var(--text-muted);
  border-radius: 8px;
  padding: 20px;
  box-shadow: 6px 6px 12px var(--shadow-neu);
}

.confirm-dialog-message {
  margin: 0 0 20px;
  font-size: 15px;
  color: var(--text);
  text-align: center;
}

.confirm-dialog-actions {
  display: flex;
  gap: 10px;
}

/* .btn--neu (120px/40px par défaut, voir sa définition) élargi ici : occupe toute la
   largeur disponible du panneau, à parts égales entre "Non"/"Oui". */
.confirm-dialog-btn {
  flex: 1;
  width: auto;
}

:root[data-theme="idiaa"] .confirm-dialog-panel {
  background-color: var(--surface-alt);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

/* ---------- Popup de partage de liste de courses (shopping-list-view.php) ---------- */
/* Même famille que .friend-add-overlay/.friend-add-panel (friends.php) — classe dédiée
   plutôt que réutilisée telle quelle, même convention (popups sans rapport ne partagent
   pas de nom de classe, même rendu identique). */
.list-share-overlay {
  display: none;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  z-index: 100;
  padding: 20px;
}

.list-share-overlay.is-open {
  display: flex;
}

.list-share-panel {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 420px;
  max-height: 80vh;
  overflow: hidden;
  background-color: var(--surface);
  border: 1px solid var(--text-muted);
  border-radius: 8px;
  padding: 10px;
  box-shadow: 6px 6px 12px var(--shadow-neu);
}

.list-share-panel__results {
  overflow-y: auto;
  max-height: 420px;
}

/* MODE SOMBRE UNIQUEMENT, demandé explicitement ("ces couleurs sont pour le thème
   sombre, on s'occupera du clair après", précisé sur une demande précédente — même
   principe repris ici) — précisé ensuite : le fond rgb(43,43,43) va sur CHAQUE LIGNE
   (.people-row), pas sur le conteneur de la liste (transparent, premier essai corrigé —
   voir juste au-dessus). */
:root[data-theme="dark"] .list-share-panel__results {
  background: transparent;
}

:root[data-theme="dark"] .list-share-panel .people-row {
  background: rgb(43, 43, 43);
}

.list-share-panel__empty {
  color: var(--text-muted);
  font-size: 13px;
  text-align: center;
  padding: 20px 10px;
}

/* .list-share-panel #listShareClose — HISTORIQUE : décrivait ici le bouton "Fermer"
   pleine largeur en bas de la popup (100%/30px), resté par erreur après le passage de
   #listShareClose à une icône .icon-btn en en-tête (voir shopping-lists.php) — sa
   spécificité (1 id) l'emportait sur .icon-btn (1 classe), étirant l'icône à 100% de
   large/30px de haut au lieu de ses 40x40 circulaires attendus. Retiré : bug repéré en
   nettoyant cette popup, jamais signalé mais réel (CSS mort dangereux, pas juste
   inutile). */

:root[data-theme="idiaa"] .list-share-panel {
  background-color: var(--surface-alt);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

/* Survol de ligne cliquable — scopée au sélecteur [data-friend-id], posé en JS sur
   chaque ligne d'ami quand cliquer dessus déclenche une action immédiate (partage
   d'une recette, recipe-view.js/recipe-collection.js — clic = envoi tout de suite,
   voir leur commentaire) : distinct de .people-row--checkbox (partage de liste de
   courses, shopping-lists.js), qui coche une case sans rien envoyer avant la
   validation groupée à la fermeture. */
.list-share-panel .people-row[data-friend-id] {
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.list-share-panel .people-row[data-friend-id]:hover {
  background-color: var(--accent-tint);
}

/* ---------- Popup "Partager" une recette (recipe-view.php/recipe-collection.php) ----------
   HISTORIQUE — classes dédiées (.recipe-share-people-*) retirées : cette popup réutilise
   désormais .list-share-overlay/.list-share-panel (design identique à la popup de partage
   de liste de courses, demandé explicitement, voir ces deux fichiers PHP). Le survol/
   curseur "pointeur" de ses lignes cliquables (data-friend-id, clic = partage immédiat)
   est déjà couvert par .list-share-panel .people-row[data-friend-id] ci-dessus, jamais
   retiré depuis le passage aux cases à cocher côté liste de courses — cette popup-ci en
   profite maintenant aussi, sans règle dédiée à écrire. #recipeShareOverlay (chat.php)
   reste une popup à part (choisir une RECETTE à partager dans la conversation ouverte),
   contenu différent malgré le nom proche, non concernée par ce changement. */

/* ---------- Popup de partage de recette (chat.php) ---------- */
/* Même famille que .friend-add-overlay ci-dessus — dédiée plutôt que réutilisée, même
   convention. */
.recipe-share-overlay {
  display: none;
  position: fixed;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  z-index: 100;
  padding: 20px;
}

.recipe-share-overlay.is-open {
  display: flex;
}

.recipe-share-panel {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 420px;
  max-height: 80vh;
  overflow: hidden;
  background-color: var(--surface);
  border: 1px solid var(--text-muted);
  border-radius: 8px;
  padding: 10px;
  box-shadow: 6px 6px 12px var(--shadow-neu);
}

.recipe-share-panel__list {
  overflow-y: auto;
  max-height: 420px;
}

/* Ligne recette cliquable : vignette carrée (même taille/traitement que .data-table__thumb,
   recettes) + nom — pas de bouton "ajouter" séparé, un clic sur la ligne envoie directement
   (voir chat.js), contrairement au picker d'ingrédient qui a une étape de quantité en plus. */
.recipe-share-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  border-radius: 8px;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background-color 0.2s;
  color: var(--text);
}

.recipe-share-item:hover {
  background-color: var(--accent-tint);
}

/* Même taille/traitement que .data-table__thumb (liste des recettes) — voir commentaire
   ci-dessus. */
.recipe-share-item img {
  width: 50px;
  height: 50px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  flex-shrink: 0;
}

.recipe-share-item__name {
  flex: 1;
  font-size: 14px;
}

.recipe-share-panel__empty {
  color: var(--text-muted);
  font-size: 13px;
  text-align: center;
  padding: 20px 10px;
}

/* ---------- Conversation (chat.php) ---------- */
/* Avatar + nom de l'ami en tête de page — le nom est déjà dans la topbar ($pageTitle),
   ce bandeau sert surtout à afficher l'avatar (voir chat.php pour le repli silhouette /
   chat.js pour son remplacement par la vraie photo). */
.chat-header {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}

.chat-header__name {
  font-size: 18px;
  font-weight: 600;
}

/* Fil de discussion : flux normal de la page (pas de zone à défilement interne) — toute
   .main-content défile, comme le reste de l'app. Plus simple/robuste qu'une hauteur
   calculée sur 100vh (fragile sur mobile, barre d'adresse iOS qui va et vient — même
   classe de piège déjà rencontrée avec background-attachment:fixed, voir CLAUDE.md). */
.chat-thread {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 4px 4px 12px;
}

.chat-bubble {
  max-width: 75%;
  padding: 10px 14px;
  border-radius: var(--radius-md);
  font-size: 14px;
  line-height: 1.4;
  word-break: break-word;
}

.chat-bubble--theirs {
  align-self: flex-start;
  background: var(--surface-alt);
  color: var(--text);
  border-bottom-left-radius: 4px;
}

.chat-bubble--mine {
  align-self: flex-end;
  background: var(--accent);
  color: var(--accent-contrast);
  border-bottom-right-radius: 4px;
}

.chat-bubble--image {
  padding: 4px;
}

.chat-bubble--image img {
  display: block;
  max-width: 100%;
  border-radius: calc(var(--radius-md) - 4px);
  cursor: pointer;
}

/* Carte recette dans une bulle : vignette + nom, cliquable vers recipe-view.php — mêmes
   couleurs que la bulle qui la contient (héritées), juste un fond légèrement creusé pour se
   distinguer d'un message texte. */
.chat-bubble--recipe {
  padding: 8px;
}

.chat-bubble__recipe-link {
  display: flex;
  align-items: center;
  gap: 10px;
  color: inherit;
  text-decoration: none;
}

.chat-bubble__recipe-link img {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  flex-shrink: 0;
}

.chat-bubble__time {
  display: block;
  margin-top: 4px;
  font-size: 11px;
  opacity: 0.7;
}

/* Barre de composition, collée en bas de .chat-page (voir plus bas) — décalée du dock
   mobile comme .fab (même variable --nav-height-mobile + zone de sécurité iOS). */
.chat-composer {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px;
  border-radius: var(--radius-md);
  background: var(--surface);
  border: 1px solid var(--border);
  position: sticky;
  bottom: calc(var(--nav-height-mobile) + env(safe-area-inset-bottom, 0px) + 10px);
}

@media (min-width: 900px) {
  .chat-composer {
    bottom: 10px;
  }
}

.chat-composer__input {
  flex: 1;
  margin-bottom: 0;
}

.chat-composer__icon-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: none;
  background: none;
  padding: 0;
  color: var(--accent);
  cursor: pointer;
  transition: color 0.2s ease, transform 0.15s ease;
}

@media (hover: hover) and (pointer: fine) {
  .chat-composer__icon-btn:hover {
    color: var(--svg-hover);
    transform: scale(1.15);
  }
}

.chat-composer__icon-btn svg {
  width: 100%;
  height: 100%;
}

/* "Envoyer" (#chatSendBtn) : couleur bleue fixe demandée explicitement, PAS var(--accent)
   comme les deux autres icônes du composeur — var(--accent) devient noir en mode Îdiää
   (monochrome intégral, voir CLAUDE.md / Design system), ce qui aurait rendu cette icône
   invisible/incohérente avec la demande. Même bleu que --accent en thème clair (#007AFF,
   iOS system blue), fixé pour rester bleu dans les 3 thèmes — même famille que les autres
   couleurs sémantiques fixes de l'app (--danger, --svg-hover, --unread-badge). Hérite du
   reste de .chat-composer__icon-btn (taille, survol → orange, transition). */
.chat-composer__send-btn {
  color: #007aff;
}

/* ---------- Formulaire recette : champ précédé d'une icône ---------- */
.form-group {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 16px;
}

.form-group__icon {
  width: 20px;
  height: 30px;
  flex-shrink: 0;
  color: var(--text-muted);
  display: flex;
  align-items: center;
}

/* Refonte couleurs sombre en cours (voir variables.css) — icônes devant les champs de
   saisie, valeur fixe demandée explicitement plutôt que --text-muted (partagée avec
   d'autres éléments non concernés par cette demande). */
:root[data-theme="dark"] .form-group__icon {
  color: rgb(119, 112, 111);
}

.form-group__icon svg {
  width: 100%;
  height: 100%;
}

.form-group .input {
  margin-bottom: 0;
}

/* ---------- Bouton "effacer" des champs de recherche (icône fournie par l'utilisateur) ----------
   Remplace la croix native du navigateur (input[type="search"]) et l'absence totale
   d'affichage sur les deux champs restés en type="text" (#articleSearch,
   shopping-list-view.php) — demandé explicitement pour tous les champs de recherche de
   l'app. Visibilité pilotée EN CSS PUR (:not(:placeholder-shown), même technique déjà
   fiable pour les libellés flottants des champs "à la login.php") plutôt que par une
   classe JS à faire basculer — un essai précédent (état géré en JS) restait bloqué
   visible en permanence, bug jamais diagnostiqué faute d'accès à un navigateur réel,
   entièrement retiré sur demande. Cette version ne peut pas rester "coincée" : elle ne
   fait que refléter l'état réel du champ à chaque instant, sans mémoire propre. */
.search-clear {
  display: none;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  /* 14.08px (12.8px + 10%, demandé explicitement — la valeur précédente était déjà
     80% des 16px d'origine). */
  width: 14.08px;
  height: 14.08px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  color: var(--text-muted);
  transition: color 0.2s ease;
}

.search-clear svg {
  width: 100%;
  height: 100%;
}

@media (hover: hover) and (pointer: fine) {
  .search-clear:hover {
    color: var(--text);
  }
}

/* Masque la croix native des input[type="search"] — .search-clear la remplace, les deux
   superposées auraient été redondantes/moches. Sans effet sur #articleSearch
   (type="text"), qui n'en a jamais eu. */
input[type="search"]::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}

/* Cas général : champ de recherche dans un .form-group (index.php, admin.php,
   settings.php, friends.php, meal-event-view.php — 11 des 13 champs de recherche de
   l'app). position:relative ajouté ici (nouveau, absent avant) : nécessaire pour
   positionner .search-clear en absolu par rapport à CE conteneur précis plutôt qu'un
   ancêtre plus lointain — sans effet sur les .form-group qui n'ont pas de champ de
   recherche (aucun de leurs enfants actuels n'est positionné en absolu hors du mode
   Îdiää, seul .idiaa-field__icon--shown l'est, et seulement dans ce thème précis). */
.form-group {
  position: relative;
}

.form-group input[type="search"].input {
  padding-right: 30px;
}

/* right: 13px (8px + 5px de décalage supplémentaire vers la gauche, demandé
   explicitement). */
.form-group .search-clear {
  right: 13px;
}

.form-group input[type="search"].input:not(:placeholder-shown) ~ .search-clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Mode Îdiää : l'icône loupe (.idiaa-field__icon--shown) occupe déjà le même coin droit
   (right:5px, voir Design system / Champs "à la login.php") — les deux se
   disputeraient le même emplacement une fois du texte saisi. Le bouton "effacer" prend
   sa place plutôt que de coexister à côté : masque la loupe dès que le champ contient
   du texte (même famille de correctif que le chevron de <select>, décalé pour la même
   raison). :has() déjà utilisé ailleurs dans le projet (.idiaa-field:has(textarea)). */
:root[data-theme="idiaa"] .idiaa-field:has(input[type="search"].input:not(:placeholder-shown)) .idiaa-field__icon--shown {
  display: none;
}

/* Cas particuliers : #articleSearch (shopping-list-view.php, .zone-search-articles) et
   #ingredientSearch (recipe-edit.php, .zone-search-ingr) — pas de .form-group ici, le
   champ partage sa ligne avec une icône "+" séparée (pas juste le champ seul comme les
   11 autres cas ci-dessus). Un wrapper dédié .search-field, autour du <input> SEUL (pas
   toute la ligne, qui déborderait sur l'icône "+"), reprend le flex:1 que portait
   jusqu'ici l'<input> directement (règle existante .zone-search-articles/ingr .input
   { flex:1 } désormais sans effet, .search-field n'étant pas display:flex — inoffensif,
   pas retirée pour ne pas risquer d'affecter d'autres sélecteurs qui s'appuient dessus). */
.search-field {
  position: relative;
  flex: 1;
  min-width: 0;
}

.zone-search-articles .search-field .input,
.zone-search-ingr .search-field .input {
  width: 100%;
  padding-right: 30px;
}

/* right: 13px (8px + 5px de décalage supplémentaire vers la gauche, demandé
   explicitement — même valeur que .form-group .search-clear ci-dessus). */
.search-field .search-clear {
  right: 13px;
}

.search-field .input:not(:placeholder-shown) ~ .search-clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* Bug corrigé, signalé par l'utilisateur sur capture (icône "effacer" toujours grosse
   et noire malgré .search-clear svg { width/height:100%; } plus haut) : ces deux zones
   ont chacune une règle générique ".zone-search-articles/ingr svg" (26px, fill:
   var(--accent), pensée pour la seule icône "+" du champ) — un sélecteur non scopé qui
   attrape AUSSI le <svg> de .search-clear, imbriqué dans .search-field à l'intérieur
   de cette même zone. Plus spécifique (3 classes + type, contre 1 classe + type pour
   .search-clear svg), cette règle gagnait la cascade quel que soit l'ordre de
   déclaration dans le fichier — d'où l'icône à 26px teintée en accent (noir en Îdiää,
   thème monochrome où --accent vaut --text) au lieu de 12.8px/var(--text-muted).
   Corrigé en réaffirmant ici, avec une spécificité encore supérieure, la taille et la
   couleur voulues (fill: currentColor, qui suit color: var(--text-muted) posé sur le
   bouton .search-clear lui-même). */
.zone-search-articles .search-field .search-clear svg,
.zone-search-ingr .search-field .search-clear svg {
  width: 100%;
  height: 100%;
  fill: currentColor;
}


/* Paires de champs (type/personnes, préparation/cuisson, four/thermostat) sur une
   seule ligne en desktop uniquement — en mobile chaque .form-group/.toggle-row reste
   empilé normalement (comportement par défaut, pas de règle nécessaire). */
@media (min-width: 900px) {
  .form-row {
    display: flex;
    gap: 16px;
  }

  .form-row .form-group,
  .form-row .toggle-row {
    flex: 1;
    min-width: 0;
  }

  /* .form-group a margin-bottom:16px (espacement vertical par défaut entre champs
     empilés en mobile), mais .toggle-row n'en a pas — en desktop, ce delta de marge
     fausse le stretch/centrage du .form-row (le contenu du toggle "Utilise un four"
     se retrouve ~8px plus bas que le champ thermostat). Neutralisé seulement ici
     (le mobile empilé garde son espacement normal). */
  #ovenTempGroup {
    margin-bottom: 0;
  }
}

.form-group select.input {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23007aff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 16px;
  padding-right: 36px;
}

/* ---------- Zone d'upload (illustration recette / avatar) ---------- */
.upload-zone {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100px;
  border-radius: var(--radius-md);
  border: 1px dashed var(--border);
  background: var(--surface-alt);
  color: var(--accent);
  cursor: pointer;
  overflow: hidden;
}

/* Refonte couleurs sombre en cours (voir variables.css) — mêmes zones "image" que les
   champs de saisie (.input), même valeur fixe demandée explicitement plutôt que
   --surface-alt (partagée avec d'autres éléments non concernés par cette demande). */
:root[data-theme="dark"] .upload-zone {
  background: rgb(52, 53, 52);
}

/* Description déplacée sous la zone d'illustration (recipe-edit.php) — 20px d'écart,
   demandé explicitement, **toutes apparences**. :has() cible le .form-group englobant
   sans id/classe dédiée sur ce conteneur précis — spécificité (id dans :has()) l'emporte
   sur la règle Îdiää de .idiaa-field (margin-top: 8px) sans avoir à la toucher. */
.form-group:has(#recipeDescription) {
  margin-top: 20px;
}

/* Même largeur/décalage que le textarea des étapes (icône 20px + gap 10px au-dessus) :
   .upload-zone est block-level et prend toute la largeur du formulaire par défaut,
   30px plus large que le textarea côté gauche. Scopé à #recipeImageZone (recipe-edit.php)
   uniquement — .upload-zone seule n'a pas ce contexte d'icônes. */
#recipeImageZone {
  width: calc(100% - 30px);
  margin-left: 30px;
}

.upload-zone svg {
  width: 32px;
  height: 32px;
}

.upload-zone img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* #createIngredientBtn — HISTORIQUE : décrivait ici le bouton "Ajouter" pleine largeur
   en bas de la popup "Nouvel ingrédient" (358px/30px, .btn). Retiré, demandé
   explicitement — même traitement que "+" de #pickerOverlay (voir CLAUDE.md / Popup de
   sélection d'ingrédient) : #createIngredientBtn est désormais l'icône "+" posée en
   DERNIER enfant de l'en-tête de cette même popup (voir #newIngredientToggleBtn
   ci-dessus pour la règle de style réutilisée, et plus bas pour la sienne, identique). */

/* Même largeur que le champ "Nom"/la ligne unité+catégorie (358px, cohérence visuelle
   du panneau) — margin gauche/droite auto pour centrer, .ingredient-list étant un flex
   column (un enfant à largeur explicite ne "stretch" plus). */
#newIngredientImageZone {
  width: 358px;
  margin-left: auto;
  margin-right: auto;
}

/* Icône "+" de l'en-tête (popup "Nouvel ingrédient") — mêmes règles que
   #newIngredientToggleBtn (en-tête de #pickerOverlay, voir plus haut), reprises à
   l'identique : même tracé SVG, même taille/couleur/hover/réduction desktop. Ciblé par
   id (#createIngredientBtn, unique dans la page), même raison que
   #newIngredientToggleBtn (.list-header réutilisé ailleurs, un sélecteur par id reste
   sûr). */
#createIngredientBtn {
  height: 26px;
  width: 26px;
  flex-shrink: 0;
  fill: var(--accent);
  cursor: pointer;
  transition: 0.2s;
}

/* MODE SOMBRE UNIQUEMENT, demandé explicitement ("ces couleurs sont pour le thème
   sombre, on s'occupera du clair après") — blanc, voir :root[data-theme="dark"]
   .list-header--icon .icon-btn plus haut dans ce fichier pour le même changement/même
   raison. Clair/système/Îdiää gardent fill: var(--accent) ci-dessus, inchangé. */
:root[data-theme="dark"] #createIngredientBtn {
  fill: #fff;
}

@media (min-width: 900px) {
  #createIngredientBtn {
    height: 23.4px;
    width: 23.4px;
  }
}

@media (hover: hover) and (pointer: fine) {
  #createIngredientBtn:hover {
    fill: var(--svg-hover);
    transform: scale(1.15);
  }
  :root[data-theme="dark"] #createIngredientBtn:hover {
    fill: #007aff;
  }
}

/* Popup "Modifier l'ingrédient" (settings.php / Bibliothèque) — reprend le même schéma
   de boutons pleine largeur en bas de popup que la popup "Nouvel ingrédient"
   (recipe-edit.php) AVAIT à l'origine (#createIngredientBtn/#newIngredientCancel,
   depuis passés en en-tête à icônes, voir plus haut — pas suivi ici, cette popup-ci
   édite un ingrédient existant plutôt qu'en créer un, périmètre différent). Sur une
   popup séparée (page différente, mêmes classes .ingredient-selector/.ingredient-list
   génériques mais pas les mêmes id). Même popup pour éditer un ingrédient privé et
   personnaliser un ingrédient natif (unité/catégorie uniquement, voir Bibliothèques),
   boutons identiques
   dans les deux cas. Hauteur 40px : même hauteur que "Enregistrer" sur recipe-edit.php
   (.form-actions .btn--neu, jamais surchargée là-bas), demandé explicitement — remplace
   les 30px précédents. */
#libraryEditIngredientSave {
  height: 40px;
  padding: 0 16px;
  width: 358px;
  margin-left: auto;
  margin-right: auto;
}

#libraryEditIngredientCancel {
  width: 358px;
  margin: 10px auto 20px;
  height: 40px;
  flex-shrink: 0;
}

#libraryEditIngredientImageZone {
  width: 358px;
  margin-left: auto;
  margin-right: auto;
}

:root[data-theme="idiaa"] #libraryEditIngredientSave {
  margin-top: 20px;
}

/* Popup "Modifier le produit" (settings.php / Bibliothèque > Articles de course) —
   demandé explicitement, **toutes apparences** : "Enregistrer" décollé de 20px de la
   zone d'image (`.upload-zone` n'a par défaut aucune marge propre, voir plus haut —
   trop collé sans ça) ; "Fermer" passé en pleine largeur comme "Enregistrer" (`.btn`,
   100% par défaut) au lieu des 120px par défaut de `.btn--neu`. Contrairement à
   `#libraryEditIngredientSave`/`#libraryEditIngredientCancel` ci-dessus (358px/30px,
   popup distincte), cette popup n'avait encore aucune règle dédiée — tailles par
   défaut de `.btn`/`.btn--neu` inchangées, même principe que le correctif équivalent
   sur `#adminEditIngredientSave`/`#adminEditIngredientCancel` (admin.php /
   Bibliothèque native). */
#libraryEditArticleSave {
  margin-top: 20px;
}

/* Bordure basse du bouton à 20px de la bordure basse de la popup, demandé
   explicitement : ce bouton est le dernier enfant de .ingredient-list (pas de wrapper
   type .article-list__body ici), qui a déjà 10px de padding propre (voir sa
   définition plus haut) — margin-bottom:10px posé ici s'ajoute à ce padding pour les
   20px voulus (10+10), plutôt que 20px qui en ferait 30 au total. */
#libraryEditArticleCancel {
  width: 100%;
  margin-bottom: 10px;
}

/* Popup "Modifier/Nouvel ingrédient de base" (admin.php / Bibliothèque native) — voir
   assets/js/admin-library.js. Demandé explicitement, **toutes apparences** :
   - Zone d'image en 128×128, TAILLE RÉELLE (pas juste une taille d'affichage) —
     resizeImage(file, 128) côté JS produit désormais des images à cette dimension max
     (au lieu de 200px avant), alignée avec ce cadre. Centrée (margin gauche/droite
     auto), même raison que #libraryEditIngredientImageZone ci-dessus (.ingredient-list
     est un flex column, un enfant à largeur explicite ne "stretch" plus).
   - "Enregistrer" (.admin-edit-ingredient-save) décollé de 20px de cette zone
     (.upload-zone n'a par défaut aucune marge propre, voir plus haut — 2px un temps,
     élargi à 20px sur demande explicite).
   - "Fermer" (#adminEditIngredientCancel) passé en pleine largeur, comme "Enregistrer"
     (.btn, 100% par défaut) — au lieu des 120px par défaut de .btn--neu.
   - "Enregistrer" aligné sur la même hauteur que "Fermer" (40px, hauteur par défaut de
     .btn--neu, non modifiée ici) — .btn (classe de base d'"Enregistrer") n'a pas de
     hauteur explicite par défaut (juste un padding vertical de 13px), d'où le besoin de
     la fixer içi. */
.admin-edit-ingredient-image-zone {
  width: 128px;
  height: 128px;
  margin-left: auto;
  margin-right: auto;
}

.admin-edit-ingredient-save {
  margin-top: 20px;
  height: 40px;
}

#adminEditIngredientCancel {
  width: 100%;
}

/* Popups "Types de plat"/"Catégories d'article" (valeurs de départ, admin.php /
   Bibliothèque native) — "Fermer" passé en pleine largeur, comme "Enregistrer" (.btn,
   100% par défaut), demandé explicitement — au lieu des 120px par défaut de .btn--neu,
   même correctif que #adminEditIngredientCancel juste au-dessus. */
#adminEditTypeCancel,
#adminEditCategoryCancel {
  width: 100%;
}

/* Mêmes popups, bibliothèques PRIVÉES cette fois (settings.php / Bibliothèques —
   Types de plat/Catégories d'article) — même correctif, demandé explicitement pour
   celles-ci aussi. */
#libraryEditTypeCancel,
#libraryEditCategoryCancel {
  width: 100%;
}

/* "Fermer" (.btn--neu, 40px de base — jamais surchargé ici) et "Enregistrer" (.btn,
   pas de hauteur propre : ~44-46px selon le navigateur, contenu + padding vertical
   13px) n'avaient pas la même hauteur — demandé explicitement de les aligner. Fixé sur
   "Enregistrer" plutôt que sur "Fermer" : même traitement déjà appliqué aux autres
   boutons ".btn" de l'app (#libraryEditIngredientSave) — 40px explicite + padding
   horizontal seul (le padding vertical de .btn empêcherait height de faire effet,
   box-sizing:border-box global). */
#libraryEditTypeSave,
#libraryEditCategorySave {
  height: 40px;
  padding: 0 16px;
}

/* Popup "Modifier le type de plat" (settings.php / Bibliothèques) — bordure basse du
   bouton "Fermer" à 20px de la bordure basse de la popup, demandé explicitement : ce
   bouton est le dernier enfant de .ingredient-list (#libraryEditTypeForm, pas de
   wrapper type .article-list__body ici), qui a déjà 10px de padding propre (voir sa
   définition plus haut) — margin-bottom:10px posé ici s'ajoute à ce padding pour les
   20px voulus (10+10), plutôt que 20px qui en ferait 30 au total. */
#libraryEditTypeCancel {
  margin-bottom: 10px;
}

/* Popup "Modifier la catégorie" (settings.php / Bibliothèques) — même correctif que
   #libraryEditTypeCancel ci-dessus, demandé explicitement pour celle-ci aussi : dernier
   enfant de .ingredient-list (#libraryEditCategoryForm), 10px de padding propre du
   panneau + 10px de margin-bottom ici = 20px entre les deux bordures basses. */
#libraryEditCategoryCancel {
  margin-bottom: 10px;
}

/* ---------- Page de visualisation d'une recette (lecture seule) ---------- */
.recipe-view-photo {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  /* Petit espace pour décoller l'image de la topbar en mobile (écrasé par les 10px de
     la règle desktop ci-dessous). */
  margin-top: 12px;
  margin-bottom: 16px;
}

/* Mobile : empilée normalement (image, puis titre/type en dessous) — pas de règle
   spécifique nécessaire, .recipe-view-header reste en flux bloc normal. */

/* Desktop uniquement : image à gauche (50% de la largeur), titre + type à droite,
   au même niveau vertical. */
@media (min-width: 900px) {
  .recipe-view-header {
    display: flex;
    align-items: flex-start;
    gap: 24px;
  }

  .recipe-view-photo {
    width: 50%;
    flex-shrink: 0;
    margin-top: 10px;
    margin-bottom: 0;
  }

  .recipe-view-header__info {
    flex: 1;
  }

  /* TEST : ingrédients de la recette sur 2 colonnes, desktop uniquement (voir
     renderIngredients() dans recipe-view.js pour --ingredient-rows). Le tbody devient
     un conteneur grid avec un nombre de lignes/colonne fixé par ingrédient,
     grid-auto-flow:column remplit d'abord la colonne de gauche : si le total est
     impair, c'est elle qui récupère la ligne en plus. Pour revenir en arrière :
     supprimer ce bloc + la ligne équivalente dans recipe-view.js. */
  #recipeViewIngredients {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: repeat(var(--ingredient-rows, 1), auto);
    grid-auto-flow: column;
    column-gap: 24px;
    /* Reste 2px de border-bottom (.ingredient-row) visibles sous la dernière ligne
       malgré border-spacing:0 sur .recipe-ingredients (voir plus haut) — signalé sur
       capture. #recipeViewIngredients (grid, donc marge respectée normalement,
       contrairement à un vrai <tbody> en layout table) tiré vers le haut de 2px : la
       table (auto height, overflow:hidden) absorbe ce négatif en se faisant d'autant
       plus courte, faisant chevaucher son propre bord bas sur ces 2px plutôt que les
       laisser visibles. Fonctionne quel que soit le nombre d'ingrédients (propriété du
       conteneur, pas un calcul lié au nombre de lignes). */
    margin-bottom: -2px;
  }
}

/* "Partagé par {nom}" : au-dessus des autres faits (type, personnes...), aligné à gauche
   avec le nom de la recette — pas de colonne d'icône commune ici (voir
   .recipe-view-facts__icon plus bas), contrairement aux autres faits. Demandé
   explicitement. Sa présence (affichée seulement quand la recette a une provenance,
   recipe-view.js) décale naturellement .recipe-view-facts vers le bas via son propre
   flux normal — aucun décalage à calculer à la main. */
.recipe-view-shared-by {
  display: flex;
  align-items: center;
  gap: 8px;
  /* 15px avant le premier fait (type de plat, etc.) — demandé explicitement. Marges
     verticales adjacentes entre éléments de bloc qui se suivent (celle-ci et
     .recipe-view-facts { margin-top: 8px }) fusionnent au plus grand des deux : 15px
     l'emporte sans avoir à toucher à .recipe-view-facts, qui garde son 8px pour les
     autres cas où rien ne le précède directement (aucun, dans les faits, mais pas de
     raison de le changer pour autant). */
  margin: 0 0 15px;
  font-size: 13px;
  color: var(--text-muted);
}

/* Même taille que la plupart des icônes de .recipe-view-facts (16px) — pas de colonne
   fixe de 60px ici (voir commentaire ci-dessus), juste un petit espace après l'icône
   (gap sur .recipe-view-shared-by). */
.recipe-view-shared-by__icon {
  display: flex;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
}

.recipe-view-shared-by__icon svg {
  width: 100%;
  height: 100%;
}

.recipe-view-facts {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 8px;
}

.recipe-view-facts__item {
  display: flex;
  align-items: center;
  color: var(--text-muted);
  font-size: 13px;
}

/* Colonne de largeur fixe (60px) autour de chaque icône, centrée dedans : les icônes
   ont des tailles différentes (16px, 21px pour "personnes"...) mais leur centre tombe
   toujours à 30px du bord gauche — un axe vertical commun d'une ligne à l'autre. Le
   texte démarre juste après cette colonne, donc à 30px de cet axe, aligné à gauche sur
   toutes les lignes quelle que soit la largeur de l'icône. */
.recipe-view-facts__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 60px;
  flex-shrink: 0;
}

.recipe-view-facts__icon svg {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

/* +30% par rapport aux 16px partagés avec les autres icônes (préparation/cuisson/four) :
   propre à l'icône "personnes", ne touche pas les autres. */
.recipe-view-facts__item--servings .recipe-view-facts__icon svg {
  width: 21px;
  height: 21px;
}

.recipe-view-steps {
  margin: 0;
  /* Puces décalées de 30px par rapport au bord gauche du texte "Étapes de la recette"
     (remplace le padding par défaut du navigateur sur <ul>, ~40px). */
  padding-left: 30px;
  list-style: disc;
}

.recipe-view-steps li {
  margin-bottom: 8px;
}

.recipe-view-steps li:last-child {
  margin-bottom: 0;
}

.view-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 24px;
}

.view-actions .btn--neu {
  width: 100%;
}

/* Desktop : mêmes dimensions/position que Enregistrer/Annuler sur la page d'édition
   (120px, côte à côte, alignés à droite) — Éditer à gauche, Retour à droite (même
   ordre visuel qu'Enregistrer/Annuler). */
@media (min-width: 900px) {
  .view-actions {
    flex-direction: row;
    justify-content: flex-end;
  }

  .view-actions .btn--neu {
    width: 120px;
  }
}

/* ---------- Boutons neumorphiques (actions de formulaire) ---------- */
.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 20px;
}

.btn--neu {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 120px;
  height: 40px;
  color: var(--accent);
  padding: 0;
  font-size: 13px;
  border-radius: 0.8em;
  background: var(--bg);
  cursor: pointer;
  border: 1px solid var(--ombre);
  box-shadow: 6px 6px 12px var(--ombre), -6px -6px 12px var(--surface);
  transition: all 0.2s ease-in-out;
}

@media (hover: hover) and (pointer: fine) {
  .btn--neu:hover {
    cursor: pointer;
    color: var(--svg-hover);
    box-shadow: 6px 6px 12px var(--surface), -6px -6px 12px var(--surface);
  }
}

/* Aucun bouton de l'app ne neutralisait le contour de focus par défaut du navigateur
   (seuls les champs .input le font, voir .input:focus plus haut) — remplacé par un
   anneau thématisé plutôt que supprimé complètement (accessibilité clavier), au cas où
   le rendu par défaut du navigateur serait la cause d'un artefact visuel repéré sur
   "Changer le pseudo" (popup profil) : un bandeau clair sous le bouton après clic, pas
   reproduit dans les vérifications Playwright de cette session (headless) — possible
   différence de rendu navigateur réel/`outline:auto`, pas confirmée avec certitude. */
.btn:focus-visible,
.btn--neu:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (max-width: 899px) {
  .form-actions {
    flex-direction: column;
  }

  .btn--neu {
    width: 100%;
    font-size: 15px;
  }
}

/* ---------- Overlay de chargement (enregistrement recette / profil) ---------- */
.loading-overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.2);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  z-index: 100;
}

.loading-overlay[hidden] {
  display: none;
}

.loading-spinner rect {
  animation: idiaa-spinner-fade 1.2s linear infinite;
}

/* Les <rect> sont positionnées une fois pour toutes via rotate() (voir
   loading-overlay.php) et ne bougent jamais : seule leur couleur est animée,
   ce qui donne l'illusion que le spinner tourne. */
@keyframes idiaa-spinner-fade {
  0% {
    fill: var(--spinner-strong);
  }
  16% {
    fill: var(--spinner-soft);
  }
  100% {
    fill: var(--spinner-soft);
  }
}

/* Barres de défilement verticales personnalisées — Îdiää uniquement, demandé
   explicitement, toutes zones qui défilent (pas de sélecteur par zone à tenir à jour).
   Deux jeux de propriétés nécessaires, aucun des deux seul ne couvrant tous les
   navigateurs :
   - scrollbar-color/scrollbar-width : standard (Firefox, et Chromium récent qui les
     supporte aussi en plus de son propre mécanisme ci-dessous). scrollbar-color est
     une propriété héritée (voir MDN) : posée une fois sur :root[data-theme="idiaa"],
     elle s'applique à toutes les zones qui défilent en dessous sans avoir à les
     cibler une par une.
   - ::-webkit-scrollbar* : Chrome/Safari/Edge (WebKit/Blink), qui l'utilisent pour le
     détail du rendu (thumb/track/hover) même quand scrollbar-color est supporté aussi.
     Pas hérité (pseudo-élément propre à chaque boîte qui défile, pas une propriété
     CSS) : posé via le sélecteur universel (*) pour couvrir toutes les zones (page
     entière, .article-list__body, .zone-liste-ingr...) sans les lister. */
:root[data-theme="idiaa"] {
  scrollbar-color: var(--text-muted) transparent;
  scrollbar-width: thin;
}

:root[data-theme="idiaa"] *::-webkit-scrollbar {
  width: 10px;
}

:root[data-theme="idiaa"] *::-webkit-scrollbar-track {
  background: transparent;
}

:root[data-theme="idiaa"] *::-webkit-scrollbar-thumb {
  background-color: var(--text-muted);
  border: none;
}

:root[data-theme="idiaa"] *::-webkit-scrollbar-thumb:hover {
  background-color: var(--text);
}

/* Barres de défilement personnalisées en mode sombre — demandé explicitement "pour
   toute l'application (pages + popup)", valeurs RGB exactes fournies. Remplace un
   essai précédent scopé aux seules popups (voir CLAUDE.md / Mode sombre pour
   l'historique) : posé sur `:root[data-theme="dark"] *`, même portée que le mécanisme
   Îdiää ci-dessus, mais avec ses propres couleurs (fond de piste distinct du curseur,
   contrairement à Îdiää où la piste est transparente) :
   - Fond (piste, `::-webkit-scrollbar-track`) : `rgb(43, 43, 43)`.
   - Bordure (de la piste) et curseur (`::-webkit-scrollbar-thumb`, même valeur pour
     les deux, demandé ainsi) : `rgb(119, 112, 111)`.
   - Flèches haut/bas (`::-webkit-scrollbar-button`, WebKit uniquement — Chrome/Safari/
     Edge ; ne rend rien sur les OS/thèmes qui ne dessinent pas ces boutons par défaut,
     ce réglage n'a alors aucun effet visible mais reste inoffensif) : même
     `rgb(119, 112, 111)`.
   `scrollbar-color`/`scrollbar-width` (Firefox, et Chromium récent en plus de son
   propre mécanisme) ne prennent que deux couleurs (curseur, piste) — pas de bordure ni
   de boutons distincts dans cette API standard, contrairement à `::-webkit-scrollbar*`
   qui permet ce détail. */
:root[data-theme="dark"] {
  scrollbar-color: rgb(119, 112, 111) rgb(43, 43, 43);
  scrollbar-width: thin;
}

:root[data-theme="dark"] *::-webkit-scrollbar {
  width: 10px;
}

:root[data-theme="dark"] *::-webkit-scrollbar-track {
  background: rgb(43, 43, 43);
  border: 1px solid rgb(119, 112, 111);
}

:root[data-theme="dark"] *::-webkit-scrollbar-thumb {
  background-color: rgb(119, 112, 111);
  border: none;
}

:root[data-theme="dark"] *::-webkit-scrollbar-button {
  background-color: rgb(119, 112, 111);
}

/* Page "Nouveau livre de recette" (recipe-collection-edit.php, mobile — voir CLAUDE.md) :
   masquage de la scrollbar du DOCUMENT — HISTORIQUE, devenu inutile et retiré. À
   l'époque, la page entière défilait (grille d'icônes étendue jusqu'en bas, voir
   .collection-icon-grid--full plus haut) et sa scrollbar était masquée visuellement
   (overflow toujours actif). Depuis que seule la grille défile en interne
   (body:has(#collectionCreateForm), voir plus haut) et que le document lui-même est
   verrouillé (overflow:hidden), il n'y a structurellement plus de scrollbar document à
   masquer sur cette page — ce correctif n'avait donc plus rien à faire. */

/* Refonte couleurs sombre en cours (voir variables.css) — valeurs fixes demandées
   explicitement pour le fond ET la bordure de TOUTES les popups de l'app (picker
   d'ingrédient/d'article, ajout d'ami, partage de recette/de liste, création de
   liste/livre, upsell, calendrier personnalisé, confirmation "Oui"/"Non" — la section
   "Profil" n'en fait plus partie depuis son déplacement dans settings.php, voir
   CLAUDE.md), plutôt que
   --surface/--text-muted (partagés avec les cards/tableaux, pas concernés par ces
   demandes) — le fond vaut le même --bg que le fond de page en sombre (variables.css),
   pas une coïncidence : demandé explicitement à cette valeur précise. Bordure proche
   mais distincte de celle de .input (rgb(119, 111, 112)) — mêmes trois chiffres, ordre
   différent, comme .form-group__icon (voir CLAUDE.md / Mode sombre). */
:root[data-theme="dark"] .ingredient-list,
:root[data-theme="dark"] .article-list,
:root[data-theme="dark"] .friend-add-panel,
:root[data-theme="dark"] .recipe-share-panel,
:root[data-theme="dark"] .list-create-panel,
:root[data-theme="dark"] .list-share-panel,
:root[data-theme="dark"] .upsell-panel,
:root[data-theme="dark"] .date-picker-panel,
:root[data-theme="dark"] .confirm-dialog-panel {
  background-color: rgb(28, 28, 28);
  border-color: rgb(119, 112, 111);
}

/* Même famille de refonte, valeur fixe demandée explicitement — mais TRANSLUCIDE
   (alpha 0.6, d'abord essayé à 0.8 — ajusté depuis), contrairement aux popups
   ci-dessus (opaques) : listes déroulantes
   (.collections-dropdown — "Livres de recette", recipe-edit.php ; .th-filter__menu —
   filtre de colonne, index.php/admin.php/etc.) et menu contextuel "⋮" (.row-menu,
   partagé par toutes les listes de l'app). Flou ajouté ensuite (backdrop-filter,
   demandé explicitement) : n'a d'effet visible que grâce à cette transparence — même
   valeur (20px) que le flou déjà appliqué à ces mêmes composants en mode Îdiää (voir
   plus bas dans ce fichier), pour rester cohérent entre les deux thèmes. */
:root[data-theme="dark"] .collections-dropdown,
:root[data-theme="dark"] .th-filter__menu,
:root[data-theme="dark"] .row-menu {
  background-color: rgba(28, 28, 28, 0.6);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

/* Info-bulle ([data-tooltip], voir sa règle de base tout en haut de ce fichier) : même
   valeur, demandée dans le même message — remplace le fond "bulle inversée"
   (var(--text), gris clair en sombre) par ce translucide sombre. color repassé à
   var(--text) (au lieu de var(--bg), presque noir) pour rester lisible sur ce nouveau
   fond ; la petite flèche (::before) suit la même couleur que la bulle. Flou ajouté
   ensuite sur la bulle uniquement (pas sur ::before, un simple triangle en bordure CSS
   sans réelle surface à flouter). */
:root[data-theme="dark"] [data-tooltip]::after {
  background: rgba(28, 28, 28, 0.6);
  color: var(--text);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

:root[data-theme="dark"] [data-tooltip]::before {
  border-top-color: rgba(28, 28, 28, 0.6);
}

/* Même bordure que les popups ci-dessus, pour les listes (tableaux) de l'app —
   .data-table couvre aussi ses variantes multi-classes (admin-users-table/
   library-ingredients-table/library-simple-table), voir CLAUDE.md / Tableaux —
   plutôt que --text-muted (partagé avec d'autres usages non concernés). */
:root[data-theme="dark"] .data-table,
:root[data-theme="dark"] .recipe-ingredients,
:root[data-theme="dark"] .shopping-items-table,
:root[data-theme="dark"] .meal-event-planning-table {
  border-color: rgb(119, 112, 111);
}

/* ---------------------------------------------------------------------------
   Mode "Îdiää" (data-theme="idiaa") : --surface/--surface-alt deviennent
   translucides via variables.css (identité login.php), ce qui suffit pour la
   couleur. Ce bloc ajoute juste le flou (backdrop-filter) sur les panneaux de
   premier niveau posés directement sur la photo de fond — pas sur les éléments
   imbriqués à l'intérieur (inputs, badges, lignes de tableau...) : empiler des
   backdrop-filter n'apporte rien visuellement et coûte cher au rendu.
   --------------------------------------------------------------------------- */
:root[data-theme="idiaa"] .topbar,
:root[data-theme="idiaa"] .nav-desktop,
:root[data-theme="idiaa"] .nav-mobile,
:root[data-theme="idiaa"] .card--surface,
:root[data-theme="idiaa"] .data-table,
:root[data-theme="idiaa"] .recipe-ingredients,
/* Ajouté avec le planning (meal-event-view.php) — bug corrigé, signalé sur capture :
   sans cette ligne, la table héritait bien de --surface (translucide en Îdiää, voir
   variables.css) mais SANS le flou qui la rend lisible, laissant transparaître la
   photo de fond brute et différemment par ligne (racine du rendu "cassé" de la
   capture) — même piège que les autres panneaux de cette liste, juste oublié ici. */
:root[data-theme="idiaa"] .meal-event-planning-table,
:root[data-theme="idiaa"] .th-filter__menu,
:root[data-theme="idiaa"] .ingredient-list,
:root[data-theme="idiaa"] .friend-add-panel,
:root[data-theme="idiaa"] .recipe-share-panel,
:root[data-theme="idiaa"] .list-create-panel,
:root[data-theme="idiaa"] .article-list {
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

/* Fond légèrement plus opaque que les autres panneaux vitrés (--surface,
   rgba(255,255,255,0.4)) — --surface-alt (rgba(255,255,255,0.55) en Îdiää, voir
   variables.css), demandé explicitement pour cette famille de popups (picker
   d'ingrédient/d'article, ajout d'ami, partage de recette, création de liste de
   courses/de livre). */
:root[data-theme="idiaa"] .ingredient-list {
  background-color: var(--surface-alt);
}

:root[data-theme="idiaa"] .friend-add-panel,
:root[data-theme="idiaa"] .recipe-share-panel,
:root[data-theme="idiaa"] .list-create-panel,
:root[data-theme="idiaa"] .article-list {
  background-color: var(--surface-alt);
}

/* Composeur du chat : --surface-alt plutôt que --surface par défaut, pour se distinguer
   du fil de messages en dessous — pas de flou propre ici (retiré, voir plus haut) : c'est
   maintenant un élément IMBRIQUÉ dans .idiaa-page-wrap (.chat-page, voir chat.php), qui
   porte déjà le flou du panneau — même logique que .recipe-instructions-field/
   .upload-zone sur recipe-edit.php, qui utilisent aussi --surface-alt sans flou propre
   pour se démarquer du reste du formulaire. */
:root[data-theme="idiaa"] .chat-composer {
  background-color: var(--surface-alt);
}

/* .idiaa-page-wrap (page liste des recettes, index.php) : conteneur englobant tous les
   éléments de la page. Décollé du topbar de 20px dans TOUS les thèmes (pas seulement
   Îdiää) : depuis que le titre de page a été sorti de .main-content vers la topbar
   (voir Design system / Titre de page dans le topbar), plus rien ne crée d'espace
   naturel au-dessus du premier champ en clair/sombre/système — sans ce margin-top, le
   champ de recherche colle directement au topbar. En Îdiää, seul thème où ce div est
   visuellement un panneau (voir plus bas), ce même margin-top le décolle lui aussi. */
.idiaa-page-wrap {
  margin-top: 20px;
}

/* Carte "Apparence" de settings.php (#appearanceSection) : même cause que
   .idiaa-page-wrap ci-dessus (rien ne décolle plus .main-content du topbar depuis que
   le titre de page en est sorti) — décollée dans TOUS les thèmes, comme
   .idiaa-page-wrap (élargi depuis un premier essai Îdiää-only, voir CLAUDE.md : le
   collage direct au topbar en clair/sombre/système, repéré à cette occasion mais pas
   corrigé dans un premier temps, s'est avéré concerné par la demande finalement).
   settings.php n'a pas de .idiaa-page-wrap (pas de panneau englobant sur cette page).
   Un temps portée par #settingsLegalLinks (liens CGU/Confidentialité, alors premier
   élément de la page) plutôt que directement ici — retirés depuis (voir CLAUDE.md /
   Pages légales), la règle revient donc sur #appearanceSection, redevenu le premier
   élément réel de la page. */
#appearanceSection {
  margin-top: 20px;
}

/* Raccourci "Installer l'application PWA" (#pwaInstallSection, settings.php) : même
   décollement du topbar que #appearanceSection ci-dessus (20px, toutes apparences) —
   utile seulement quand elle est effectivement affichée, voir plus bas. */
#pwaInstallSection {
  margin-top: 20px;
}

/* .btn n'a pas de contrainte de taille sur les SVG qu'il contient (contrairement à
   .btn--pill, voir .meal-event-header__actions svg) — sans ça, l'icône "pwa" (SVG sans
   width/height propres, seulement un viewBox) s'afficherait à sa taille intrinsèque par
   défaut du navigateur (300×150), bien trop grande dans ce bouton. */
#pwaInstallSettingsBtn svg {
  /* Doublée (18px -> 36px), demandé explicitement. */
  width: 36px;
  height: 36px;
}

/* MOBILE UNIQUEMENT, demandé explicitement (équivalent desktop/tablette : sidebar, voir
   nav-desktop.php/nav.css). Masquée en permanence à partir de 900px, quel que soit
   l'état de l'attribut `hidden` posé/retiré par assets/js/pwa-install.js — `!important`
   nécessaire : Chrome/Edge DESKTOP déclenchent aussi `beforeinstallprompt` (le JS ne
   connaît pas la largeur d'écran, seul mécanisme à décider "ce navigateur peut
   installer"), donc l'attribut `hidden` peut très bien être retiré sur desktop — seule
   cette règle CSS garantit qu'elle n'y apparaît jamais (le raccourci équivalent existe
   déjà dans la sidebar à ces largeurs). */
@media (min-width: 900px) {
  #pwaInstallSection {
    display: none !important;
  }
}

/* Premier élément RÉEL de <main> en HTML (avant #appearanceSection), mais reste
   masquée la plupart du temps (hidden par défaut, voir plus haut) : porte donc son
   propre décollement du topbar (même valeur/raison que .idiaa-page-wrap/
   #appearanceSection ci-dessus) UNIQUEMENT utile quand elle est effectivement affichée
   — dans ce cas précis (mobile + installation proposable), #appearanceSection juste en
   dessous n'a plus besoin du sien (elle récupère l'espacement inter-cards normal via
   .card { margin-bottom }, comme n'importe quelle carte qui n'est pas la première).
   Scopé à <900px (même borne que la règle de masquage ci-dessus, dans l'autre sens) :
   sur desktop, #pwaInstallSection peut avoir son `hidden` retiré par le JS (voir plus
   haut) sans jamais être réellement affichée — ce cas ne doit surtout pas neutraliser
   le décollement d'#appearanceSection à ces largeurs, où elle reste le premier élément
   visible. */
@media (max-width: 899px) {
  #pwaInstallSection:not([hidden]) + #appearanceSection {
    margin-top: 0;
  }
}

/* Apparence "vitrée" du panneau — visible SEULEMENT en mode Îdiää, pas de règle hors
   du sélecteur scopé ci-dessous, donc `div` neutre en clair/sombre/système à part le
   margin-top ci-dessus. Reprend les caractéristiques du `.wrapper` de login.php
   (login.css) — fond vitré, coins 20px, flou 20px, ombre — via les tokens de l'app
   plutôt que les `--login-*` locaux, à deux différences volontaires près :
   - bordure en `var(--border)` (couleur de la bordure droite de la sidebar, nav.css)
     et non `var(--text)` (valeur d'origine du `.wrapper`, pleine opacité) — demandé
     pour rester cohérent avec le reste du chrome de l'app plutôt qu'avec login.php ;
   - pas d'`overflow: hidden` : sur `.wrapper` il sert à couper les panneaux
     connexion/inscription qui glissent horizontalement (spécifique à login.php) — omis
     ici, sinon les menus de filtre du tableau (`.th-filter__menu`, positionnés hors du
     `<table>` pour ne pas être rognés par son propre `overflow:hidden`, voir plus haut)
     seraient rognés à leur tour par ce nouveau conteneur englobant. */
:root[data-theme="idiaa"] .idiaa-page-wrap {
  background: var(--surface);
  /* Même couleur que la bordure droite de la sidebar (nav.css, .nav-desktop) plutôt
     que var(--text) (valeur d'origine du .wrapper de login.php, pleine opacité) —
     demandé pour cohérence avec le reste du chrome de l'app. */
  border: 1px solid var(--border);
  border-radius: 20px;
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
  box-shadow: 0 0 30px var(--shadow-md);
  padding: 24px;
}

/* .idiaa-field : mise en forme réutilisable "à la login.php" (.input-box de
   login.css — soulignement, libellé flottant) pour n'importe quel champ ailleurs dans
   l'app — EN MODE ÎDIÄÄ UNIQUEMENT, partout où ces classes sont utilisées (recherche
   recettes, popup profil...). .idiaa-field s'ajoute aux classes existantes (ex.
   .form-group), ne les remplace pas : sans le sélecteur scopé ci-dessous,
   .idiaa-field__label reste display:none et le champ garde son rendu .input habituel
   (bordé, placeholder visible) — c'est ce qui se passe en clair/sombre/système,
   intouché.
   Icône : PAS gérée ici, volontairement — deux classes distinctes selon que l'icône
   existait déjà avant (.idiaa-field__icon--shown) ou non (.idiaa-field__icon), voir
   leurs définitions plus bas — un champ qui utilise la mauvaise classe perd son icône
   dans tous les autres thèmes, piège déjà rencontré (recipe-edit.php). */
.idiaa-field__label {
  display: none;
}

:root[data-theme="idiaa"] .idiaa-field {
  display: block;
  position: relative;
  border-bottom: 2px solid var(--text);
  padding: 5px 0;
  /* Longhand plutôt que le raccourci "margin: 8px 0 24px" — historique : évitait un
     conflit avec une règle de largeur/centrage propre à l'ex-popup profil (voir
     CLAUDE.md, section retirée depuis dans settings.php). Plus de raison de conflit
     aujourd'hui, mais aucun effet à revenir sur ces longhand non plus (même rendu). */
  margin-top: 8px;
  margin-bottom: 24px;
}

:root[data-theme="idiaa"] .idiaa-field .input {
  width: 100%;
  height: auto;
  border: none;
  border-radius: 0;
  /* background-color (pas le raccourci `background`) : `background: transparent`
     réinitialise aussi background-repeat/position/size à leurs valeurs par défaut
     (dont `repeat`) — piège rencontré sur les <select> (chevron, voir plus bas),
     dont le fond carrelait au lieu de s'afficher une seule fois. Repéré à l'écran
     (Playwright), pas évident sur le papier. */
  background-color: transparent;
  /* Pas de font-size ici : celui de .input (14px, 16px en mobile pour éviter le zoom
     auto iOS, voir plus haut) reste en place — login.css n'a pas cette contrainte
     (stylesheet indépendant, pas de champ testé en usage réel sur mobile). */
  color: var(--text);
  padding: 0 32px 0 5px;
  margin-bottom: 0;
  box-sizing: border-box;
}

:root[data-theme="idiaa"] .idiaa-field .input::placeholder {
  /* Le texte reste dans l'attribut (nécessaire pour :placeholder-shown ci-dessous),
     juste invisible — le libellé flottant affiche le même texte par-dessus. */
  color: transparent;
}

/* Même halo au focus que .input:focus/.input-qte:focus (0 0 5px 0 var(--accent)),
   demandé explicitement ("je veux le même pour tous les champs de l'application, même
   pour les champs de recherche... pour les menus déroulants aussi") — ce halo était
   jusqu'ici neutralisé (box-shadow:none) pour TOUT champ .idiaa-field (recherche, nom de
   recette, personnes/temps, mais aussi les champs lecture seule qui ouvrent un menu
   déroulant — #recipeCollectionsDisplay "Livres de recette", #recipeOvenTempDisplay
   thermostat — eux aussi wrappés en .idiaa-field). border-color reste transparent (design
   "soulignement" d'Îdiää, pas de cadre visible au repos) — seul le halo est réactivé. */
:root[data-theme="idiaa"] .idiaa-field .input:focus {
  box-shadow: 0 0 5px 0 var(--accent);
  border-color: transparent;
}

:root[data-theme="idiaa"] .idiaa-field__label {
  display: block;
  position: absolute;
  top: 50%;
  left: 5px;
  transform: translateY(-50%);
  font-size: 14px;
  color: var(--text);
  font-weight: 500;
  pointer-events: none;
  transition: 0.3s;
}

/* Flotte au focus OU dès que le champ est rempli — équivalent de ":valid" côté
   login.php (utilisable là-bas car le champ email est "required", invalide tant qu'il
   est vide) : la plupart des champs ici n'ont pas cette contrainte de validité (ou
   type=search n'en a pas du tout), ":not(:placeholder-shown)" est l'équivalent
   robuste ("le champ n'affiche plus son placeholder" = "il a une valeur"), même effet
   visuel, sans dépendre de required/type. */
:root[data-theme="idiaa"] .idiaa-field .input:focus ~ .idiaa-field__label,
:root[data-theme="idiaa"] .idiaa-field .input:not(:placeholder-shown) ~ .idiaa-field__label {
  top: -6px;
}

/* Icône à droite, DEUX classes selon le contexte — ne pas les confondre (piège déjà
   rencontré, voir recipe-edit.php) :
   - .idiaa-field__icon--shown : repositionne une icône DÉJÀ affichée par défaut
     (.form-group__icon, visible hors Îdiää) — pas de display:none ici, sinon
     l'icône disparaîtrait dans TOUS les autres thèmes (spécificité égale à
     .form-group__icon, gagnerait car déclarée après dans ce fichier). Utilisée par
     le champ de recherche (index.php) et les champs texte/nombre/textarea de
     recipe-edit.php, qui avaient tous déjà une icône avant Îdiää.
   - .idiaa-field__icon (plus bas) : CRÉE l'icône elle-même, masquée par défaut —
     pour des champs qui n'avaient AUCUNE icône avant (popup profil uniquement). */
:root[data-theme="idiaa"] .idiaa-field__icon--shown {
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  color: var(--text);
}

.idiaa-field__icon {
  display: none;
}

:root[data-theme="idiaa"] .idiaa-field__icon {
  display: block;
  position: absolute;
  right: 5px;
  top: 50%;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  color: var(--text);
}

/* .idiaa-field contenant un textarea (recipe-edit.php : description, étapes) :
   libellé/icône calés en haut (comme le texte d'un textarea démarre en haut) plutôt
   que centrés verticalement (top:50%/translateY(-50%) mettrait le libellé au milieu
   d'un bloc de plusieurs lignes, pas au niveau de la première ligne de texte). Un peu
   de padding-top sur le textarea lui-même pour ne pas chevaucher le libellé/l'icône
   avant qu'ils ne s'écartent (focus/rempli). */
:root[data-theme="idiaa"] .idiaa-field:has(textarea) .idiaa-field__label,
:root[data-theme="idiaa"] .idiaa-field:has(textarea) .idiaa-field__icon,
:root[data-theme="idiaa"] .idiaa-field:has(textarea) .idiaa-field__icon--shown {
  top: 14px;
  transform: none;
}

:root[data-theme="idiaa"] .idiaa-field textarea.input {
  padding-top: 22px;
}

/* Textarea "Étapes de la recette" (recipe-edit.php) : PAS le traitement .idiaa-field
   des autres champs (icône restée à gauche, comme dans les autres thèmes, demandé
   explicitement) — juste un fond aligné sur celui de la zone d'illustration juste en
   dessous (var(--surface-alt), .upload-zone) au lieu du var(--surface) par défaut de
   .input, pour que les deux se distinguent visuellement du reste du formulaire de la
   même façon. */
:root[data-theme="idiaa"] .recipe-instructions-field {
  background: var(--surface-alt);
}

/* Chevron des <select> (.form-group select.input, style.css) : couleur bleue codée en
   dur (#007aff, fixe dans les autres thèmes) — remonte au monochrome en Îdiää, même
   tracé (juste la couleur du "stroke" changée, %231d1d1f = #1d1d1f encodé pour un data
   URI). Pas de var(--text) possible dans un data URI (pas d'évaluation CSS dedans),
   valeur medium recopiée telle quelle : à mettre à jour si --text change un jour.
   Décalé à gauche (34px au lieu de 12px) pour laisser la place à l'icône de champ
   (.idiaa-field__icon--shown, 20px de large, ancrée à droite) : les deux se lisent
   "chevron puis icône" en partant de la droite, comme demandé. padding-right élargi
   d'autant (54px au lieu de 36px) pour que le texte du select ne passe pas dessous. */
:root[data-theme="idiaa"] .form-group select.input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231d1d1f' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-position: right 34px center;
  padding-right: 54px;
}

/* #adminEditIngredientCategory (admin.php / Bibliothèque native) n'a PAS d'icône de
   champ, contrairement aux autres select concernés par la règle ci-dessus (retirée sur
   demande explicite, voir admin.php) — chevron repositionné à sa place par défaut
   (12px, comme .form-group select.input de base) et padding-right réduit d'autant,
   élargissant le champ visible d'autant (36px au lieu de 54px, plus besoin de réserver
   la place de l'icône). */
:root[data-theme="idiaa"] #adminEditIngredientCategory {
  background-position: right 12px center;
  padding-right: 36px;
}

/* .idiaa-btn : même design que .btn de login.php (login.css) — fond transparent,
   contour, coins 6px, ombre portée, survol = fond teinté + léger agrandissement — EN
   MODE ÎDIÄÄ UNIQUEMENT. S'ajoute à .btn--neu (pas de remplacement) sur les boutons
   pleine largeur de la popup profil ("Changer le pseudo/l'email/le mot de passe",
   "Fermer") et les boutons Enregistrer/Annuler de recipe-edit.php. Dimensions
   volontairement PAS touchées (chaque contexte garde les siennes — 30px/14px pour la
   popup, 120px/40px pour recipe-edit) : re-dessine seulement la couleur/le contour, ne
   touche pas à la mise en page déjà réglée ailleurs. --accent-tint vaut déjà
   rgba(29,29,31,0.1) en Îdiää, exactement --login-text-tint de login.css : réutilisé
   tel quel. Pas de token existant pour l'ombre (rgba(29,29,31,0.4), --login-shadow-btn
   côté login.css, stylesheet indépendant) : valeur reprise telle quelle. */
:root[data-theme="idiaa"] .idiaa-btn {
  background: transparent;
  border: 1px solid var(--text);
  border-radius: 6px;
  box-shadow: 6px 6px 12px rgba(29, 29, 31, 0.4);
  color: var(--text);
  font-weight: 500;
  transition: transform 0.2s ease, background-color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  :root[data-theme="idiaa"] .idiaa-btn:hover {
    background: var(--accent-tint);
    color: var(--text);
    box-shadow: 6px 6px 12px rgba(29, 29, 31, 0.4);
    transform: scale(1.02);
  }
}

/* .btn--pill ("Se déconnecter" section Profil, Accepter/Refuser/Annuler/Inviter,
   composeur de chat) : même design Îdiää que .idiaa-btn ci-dessus (fond transparent,
   contour var(--text), ombre, survol = fond teinté + agrandissement) — mais **pas** la
   classe .idiaa-btn elle-même (coins 6px / ombre 6px 6px 12px, dimensionnés pour les
   gros boutons pleine largeur) : .btn--pill reste volontairement compact (voir sa
   règle de base plus haut), donc override dédié à ses proportions — coins/ombre à
   moitié plus petits (0.8em déjà en place, ombre 3px 3px 6px comme sa version
   neumorphique de base, déjà à échelle réduite par rapport à .btn--neu : 3px/6px
   contre 6px/12px). .btn--pill--danger garde volontairement --danger (pas var(--text)) :
   les couleurs sémantiques restent hors du monochrome Îdiää, comme --danger/
   .form-success ailleurs dans l'app (voir CLAUDE.md). */
:root[data-theme="idiaa"] .btn--pill {
  background: transparent;
  border: 1px solid var(--text);
  box-shadow: 3px 3px 6px rgba(29, 29, 31, 0.4);
  color: var(--text);
}

@media (hover: hover) and (pointer: fine) {
  :root[data-theme="idiaa"] .btn--pill:hover {
    background: var(--accent-tint);
    box-shadow: 3px 3px 6px rgba(29, 29, 31, 0.4);
    color: var(--text);
    transform: scale(1.02);
  }
}

:root[data-theme="idiaa"] .btn--pill--danger {
  color: var(--danger);
  border-color: var(--danger);
}

/* Répété dans le hover (pas de nouvelle valeur) : évite qu'un autre survol de bouton
   n'écrase cette couleur — scopé au pointeur fin comme tous les hover de boutons/icônes
   de l'app (voir CLAUDE.md / Détection tactile vs pointeur), demandé explicitement. */
@media (hover: hover) and (pointer: fine) {
  :root[data-theme="idiaa"] .btn--pill--danger:hover {
    color: var(--danger);
    border-color: var(--danger);
  }
}

/* ---------- Pages légales (cgu.php / confidentialite.php) ---------- */
/* Document long-form dans .idiaa-page-wrap > .card--surface (réutilisés tels quels,
   voir index.php) : largeur de lecture confortable plutôt que la largeur pleine du
   panneau, qui irait jusqu'à 960px sinon. */
.legal-doc {
  max-width: 720px;
  margin: 0 auto;
}

.legal-doc h1 {
  font-size: 26px;
  margin: 0 0 16px;
}

.legal-doc__notice {
  display: flex;
  gap: 10px;
  background: var(--surface-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin: 0 0 28px;
  font-size: 13.5px;
  color: var(--text-muted);
  line-height: 1.6;
}

.legal-doc h2 {
  font-size: 18px;
  margin: 34px 0 10px;
}
.legal-doc h2:first-of-type {
  margin-top: 0;
}

.legal-doc p,
.legal-doc li {
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--text);
}

.legal-doc p {
  margin: 0 0 12px;
}

.legal-doc ul {
  margin: 0 0 12px;
  padding-left: 22px;
}
.legal-doc li {
  margin-bottom: 6px;
}

.legal-doc__callout {
  background: var(--surface-alt);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 12px 16px;
  margin: 0 0 16px;
}
.legal-doc__callout p:last-child {
  margin-bottom: 0;
}

/* Champs encore à compléter (identité légale de l'éditeur...) — mis en évidence pour ne
   pas passer inaperçus avant une mise en ligne, même logique que le bandeau de brouillon
   au sommet de la page. */
.legal-doc mark {
  background: transparent;
  color: var(--danger);
  font-style: italic;
}

.legal-doc__back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13.5px;
  color: var(--text-muted);
  text-decoration: none;
  margin-bottom: 16px;
}
.legal-doc__back:hover {
  color: var(--accent);
}

/* Second exemplaire, en bas de page (voir cgu.php/confidentialite.php, demandé
   explicitement) : espacement au-dessus (le premier n'en a pas besoin, .idiaa-page-wrap
   s'en charge déjà côté topbar) plutôt qu'en dessous. */
.legal-doc__back--bottom {
  margin-top: 20px;
  margin-bottom: 0;
}

/* ==========================================================================
   Événements repas ("Menus planifiés", meal-events.php / meal-event-view.php)
   Voir CLAUDE.md pour le cahier des charges. Réutilise autant que possible les
   composants déjà en place (.data-table, .people-row, .btn--pill, .idiaa-field,
   .friend-add-overlay/.list-create-overlay pour les popups, .zone-liste-ingr/
   .ingredient-item pour le picker de recette) — seuls les éléments propres à cette
   fonctionnalité (lignes de tableau glissables, en-tête, planning, créneaux/entrées,
   onglets de la popup d'ajout) ont un CSS dédié ci-dessous.
   ========================================================================== */

/* Lignes de la liste des événements (meal-events.php/meal-events.js) — même famille de
   technique que .shopping-list-row-swipe (glissement tactile/menu contextuel, voir son
   commentaire détaillé, style.css) : reprise à l'identique, seul le nom de classe change. */
.meal-event-row-swipe__link {
  transition: transform 0.2s ease;
}
.meal-event-row-swipe.is-dragging .meal-event-row-swipe__link {
  transition: none;
}
td.meal-event-row-swipe__menu {
  text-align: right;
}
@media (hover: hover) and (pointer: fine) {
  .meal-event-row-swipe:hover .row-menu-trigger,
  .meal-event-row-swipe:focus-within .row-menu-trigger,
  .meal-event-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }
  .meal-event-row-swipe__actions {
    display: none;
  }
}
.meal-event-row-swipe__action {
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0;
}
.meal-event-row-swipe__actions {
  position: relative;
}
@media (hover: none), (pointer: coarse) {
  .meal-event-row-swipe.is-swiped .meal-event-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 64px)));
  }
  .meal-event-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    inset: -1px 0 -1px auto;
    /* Fond rouge, icône blanche (rowMenuDeleteIconSvg) — demandé explicitement. */
    background: var(--danger);
    border-radius: 15px 0 0 15px;
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }
  .meal-event-row-swipe.is-dragging .meal-event-row-swipe__actions-inner {
    transition: none;
  }
  .meal-event-row-swipe.is-swiped .meal-event-row-swipe__actions-inner {
    transform: translateX(0);
  }
  .meal-event-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    color: var(--accent-contrast);
  }
  .meal-event-row-swipe__action svg {
    width: 24px;
    height: 24px;
  }
}

/* Badge "non lu" (meal-events.js, hasUnread) — icône SVG bleue à droite du nom,
   demandé explicitement (remplace l'ancienne simple pastille ronde). Conteneur
   .meal-event-name-wrap en inline-flex : garantit que l'icône reste bien À CÔTÉ du nom,
   sur la même ligne, quel que soit le mode d'affichage du <a> qu'il contient (un <a>
   seul, en display:block par défaut dans certains contextes de tableau, aurait fait
   passer un élément inline suivant à la ligne d'en dessous). */
.meal-event-name-wrap {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.meal-event-unread-icon {
  display: flex;
  color: var(--unread-badge);
  flex-shrink: 0;
}

.meal-event-unread-icon svg {
  /* +80% (16px -> 28.8px), demandé explicitement. */
  width: 28.8px;
  height: 28.8px;
}

/* Popup "Nouvel événement" (meal-events.php, #mealEventCreateOverlay) : ex-boutons
   pleine largeur "Ajouter"/"Fermer" en bas de popup, RETIRÉS — remplacés par un
   en-tête icône Fermer/titre centré/icône Enregistrer, même design que
   #collectionCreateOverlay (index.php)/#newArticleOverlay (shopping-list-view.php),
   demandé explicitement ("même format de popup... respecter les mêmes couleurs que
   les popups déjà retravaillées"). #mealEventCreateCancel/#mealEventCreateSubmit
   sont désormais de simples .icon-btn dans .list-header--icon — plus de règle CSS
   dédiée nécessaire (ancien correctif d'espacement/largeur ci-dessous supprimé avec
   les boutons qu'il ciblait). */

/* Popup "Modifier l'événement" (#mealEventEditOverlay) — pas concernée par le
   passage en en-tête icônes ci-dessus (demande limitée à "Nouvel événement") : garde
   son bouton "Fermer" pleine largeur en bas de popup. "Fermer" à la même largeur que
   "Enregistrer" (100%, comme .btn par défaut), 20px d'écart entre les deux
   (margin-top), et 20px entre la bordure basse de "Fermer" et celle de la popup —
   .list-create-panel a déjà 10px de padding propre, margin-bottom:10px s'y ajoute pour
   les 20px voulus (10+10). */
#mealEventEditCancel {
  margin-top: 20px;
  margin-bottom: 10px;
  width: 100%;
}

/* Popup "Ajouter un repas" (#mealEventSlotOverlay, meal-event-view.php) — même
   correctif que #mealEventEditCancel ci-dessus, demandé explicitement : 20px d'écart
   entre "Ajouter" (#mealEventSlotSubmit, `.btn`, pleine
   largeur par défaut) et "Fermer" (#mealEventSlotClose, `.btn--neu`, sinon collé
   dessous — pas de marge entre les deux avant ce correctif), et 20px entre la bordure
   basse de "Fermer" et celle de la popup — .friend-add-panel a déjà 10px de padding
   propre (voir sa définition plus haut), margin-bottom:10px s'y ajoute pour les 20px
   voulus (10+10), plutôt que 20px qui en ferait 30 au total. */
#mealEventSlotClose {
  margin-top: 20px;
  margin-bottom: 10px;
}

/* Popup "Ajouter au créneau" (#mealEventEntryOverlay, meal-event-view.php) — même
   correctif que #mealEventSlotClose ci-dessus, demandé explicitement : 20px entre la
   bordure basse de "Fermer" (#mealEventEntryClose) et celle de la popup —
   .ingredient-list a déjà 10px de padding propre (voir sa définition plus haut),
   margin-bottom:10px s'y ajoute pour les 20px voulus (10+10). */
#mealEventEntryClose {
  margin-bottom: 10px;
}

/* ---------- Calendrier personnalisé (assets/js/date-picker.js) ---------- */
/* Remplace <input type="date"> natif (#mealEventStartDateInput/EndDateInput, devenus
   type="hidden") — voir date-picker.js pour pourquoi (format/premier jour de semaine
   dépendant de la région système, pas de lang="fr" ni de la langue de l'app). Le champ
   affiché (readonly) reste un .idiaa-field/.input normal — seul le curseur change, pour
   indiquer qu'il s'ouvre au clic plutôt que de se taper au clavier. */
.date-field .input[readonly] {
  cursor: pointer;
}

/* Même famille visuelle que .row-menu (position:fixed, calculée par getBoundingClientRect()
   en JS puis posée en style inline — voir position(), date-picker.js) — ajouté à <body>
   plutôt qu'imbriqué dans le champ : évite le rognage par l'overflow:hidden des popups
   qui l'utilisent (#mealEventCreatePanel, .list-create-panel), même raison déjà
   rencontrée pour #pickerOverlay/les popups de shopping-list-view.php (voir CLAUDE.md). */
.date-picker-panel {
  position: fixed;
  z-index: 300;
  width: 280px;
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  padding: 12px;
}

/* Même fond vitré que .row-menu/.list-create-panel en mode Îdiää (voir leurs règles). */
:root[data-theme="idiaa"] .date-picker-panel {
  background-color: var(--surface-alt);
  -webkit-backdrop-filter: blur(20px);
  backdrop-filter: blur(20px);
}

.date-picker-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

.date-picker-month {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  text-transform: capitalize;
}

.date-picker-nav {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  border: none;
  background: none;
  border-radius: 50%;
  color: var(--text);
  cursor: pointer;
  transition: background-color 0.15s ease;
}

@media (hover: hover) and (pointer: fine) {
  .date-picker-nav:hover {
    background-color: var(--accent-tint);
  }
}

.date-picker-nav svg {
  width: 12px;
  height: 12px;
}

/* Même tracé que le chevron "précédent" (idiaa_icon('back')), tourné à 180° plutôt
   qu'un second SVG inventé pour "suivant" — voir date-picker.js. */
.date-picker-nav--next svg {
  transform: rotate(180deg);
}

.date-picker-weekdays,
.date-picker-days {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
}

.date-picker-weekdays span {
  text-align: center;
  font-size: 11px;
  color: var(--text-muted);
  text-transform: uppercase;
  padding-bottom: 6px;
}

.date-picker-day {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  border: none;
  background: none;
  border-radius: 50%;
  font-size: 13px;
  color: var(--text);
  cursor: pointer;
  transition: background-color 0.15s ease;
}

@media (hover: hover) and (pointer: fine) {
  .date-picker-day:hover {
    background-color: var(--accent-tint);
  }
}

.date-picker-day--empty {
  cursor: default;
}

@media (hover: hover) and (pointer: fine) {
  .date-picker-day--empty:hover {
    background: none;
  }
}

.date-picker-day.is-today {
  font-weight: 600;
  color: var(--accent);
}

.date-picker-day.is-selected {
  background-color: var(--accent);
  color: var(--accent-contrast);
}

/* "Effacer" (retire la date déjà choisie) — n'existe que si une valeur est posée, voir
   render(), date-picker.js. Simple lien texte sous la grille, pas un bouton pleine
   largeur : action secondaire, rare (la plupart du temps on choisit un jour, pas
   l'inverse). */
.date-picker-clear {
  display: block;
  width: 100%;
  margin-top: 8px;
  padding: 6px 0 0;
  border: none;
  border-top: 1px solid var(--border);
  background: none;
  text-align: center;
  font-size: 12px;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .date-picker-clear:hover {
    color: var(--danger);
  }
}

/* ---------- En-tête (meal-event-view.php) ---------- */
.meal-event-header {
  display: flex;
  gap: 20px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 24px;
}
.meal-event-header__media {
  width: 96px;
  height: 96px;
  flex-shrink: 0;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface-alt);
  display: flex;
  align-items: center;
  justify-content: center;
}
.meal-event-header__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.meal-event-header__icon {
  width: 40px;
  height: 40px;
  color: var(--text-muted);
}
.meal-event-header__info {
  flex: 1;
  min-width: 200px;
}
.meal-event-header__info h1 {
  margin: 0 0 10px;
  font-size: 26px;
}
.meal-event-header__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
/* .btn--pill (style.css, plus haut) est déjà inline-flex mais sans gap ni contrainte de
   taille sur un <svg> enfant (pas nécessaire ailleurs : ses seuls autres usages,
   friends.php, sont du texte seul) — les deux ajoutés ici, scopés à ces boutons
   précis pour ne rien changer aux autres .btn--pill de l'app. Sans la taille explicite,
   un <svg> sans width/height propre se rend à ~300×150px (taille de remplacement par
   défaut du navigateur), même piège que .icon-plus/.th-sort déjà résolu ailleurs. */
.meal-event-header__actions .btn--pill {
  gap: 6px;
}
.meal-event-header__actions svg {
  width: 16px;
  height: 16px;
}

.meal-event-section-title {
  font-size: 18px;
  margin: 24px 0 10px;
}
.meal-event-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 24px;
  /* Même espace que .list-header (shopping-lists.php) jusqu'à la liste en dessous,
     demandé explicitement — .meal-event-header-row n'avait jusqu'ici aucune marge
     basse propre, rien ne séparait "Planning" du tableau juste en dessous. */
  margin-bottom: 16px;
}
.meal-event-header-row .meal-event-section-title {
  margin: 0;
}

/* ---------- Participants ---------- */
.meal-event-participants {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.meal-event-participant {
  width: auto;
  padding: 6px 12px 6px 6px;
  background: var(--surface-alt);
  border-radius: 999px;
}
.meal-event-participant__creator {
  font-size: 11px;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 1px 8px;
}

/* ---------- Planning (jour > repas > plats), même forme que la liste des courses ----------
   Un seul <table> plutôt qu'une pile de cards par jour (ancien .meal-event-day/
   .meal-event-slot/.meal-event-entry, retirés) — demandé explicitement, "même forme que
   la liste des courses avec les catégories". Trois niveaux de ligne dans le même
   <tbody> (voir renderPlanning()/slotHtml()/entryHtml(), meal-event-view.js) :
   1. .meal-event-day-row : le jour, <th colspan="4"> pleine largeur — même traitement
      que .shopping-category-row (fond var(--surface-alt), 600/15px).
   2. .meal-event-moment-row-swipe : un repas (créneau), <th colspan="2"> (thumb+nom
      fusionnées) + 2 vraies cellules menu/actions pour SES propres actions (supprimer
      CE repas) — un cran moins large que le jour, effet d'escalier visuel voulu.
   3. .meal-event-entry-row-swipe : une SOUS-LIGNE par plat proposé sur ce repas (autant
      que nécessaire, demandé explicitement) — 4 vraies cellules, même mécanique que
      .shopping-item-row-swipe (menu "⋮"/glissement tactile, une seule action
      "Supprimer"). */
.meal-event-planning-table {
  width: 100%;
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--surface);
  border-radius: 8px;
  border: 1px solid var(--text-muted);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  margin-bottom: 16px;
}

/* Refonte couleurs sombre en cours (voir variables.css / .data-table) — même traitement
   que .data-table pour cette liste. */
:root[data-theme="dark"] .meal-event-planning-table {
  background: rgb(43, 43, 43);
}

.meal-event-planning-table th,
.meal-event-planning-table td {
  padding: 12px;
  text-align: left;
  vertical-align: middle;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}

.meal-event-planning-table tbody tr:last-child td,
.meal-event-planning-table tbody tr:last-child th {
  border-bottom: none;
}

/* 112px = 62px (12px d'espacement de départ standard + 50px de décalage supplémentaire,
   demandé explicitement) + 50px de vignette — voir .meal-event-entry-row__thumb plus
   bas pour le détail complet du calcul. */
.meal-event-planning-table__col--thumb {
  width: 112px;
}

/* width:100%, pas un <col> sans largeur — même correctif que .data-table__col--name/
   .shopping-items-table__col--name (bug iPhone potentiel, table-layout:fixed). */
.meal-event-planning-table__col--name {
  width: 100%;
}

/* Même raisonnement que .data-table__col--menu (style.css) : réservée dans TOUS les cas
   désormais, le "⋮" étant visible en tactile aussi. */
.meal-event-planning-table__col--menu {
  width: 58px;
}

/* Colonne dédiée pour ...-actions-inner (toujours 0, mais DÉCLARÉE) — même piège déjà
   rencontré et corrigé sur shopping-list-view.php (voir son commentaire) : sans elle,
   une ligne d'en-tête à colspan juste au-dessus fausse le calcul de largeur des lignes
   suivantes dans Chromium. */
.meal-event-planning-table__col--actions {
  width: 0;
}

/* En-tête de jour : même apparence que .shopping-category-row th. */
/* Refonte couleurs sombre en cours (voir variables.css / .data-table th) — même
   traitement que .shopping-category-row th pour ce header "de groupe" (une ligne
   .meal-event-day-row par jour). */
:root[data-theme="dark"] .meal-event-day-row th {
  background: rgb(95, 95, 95);
}

.meal-event-day-row th {
  color: var(--text);
  font-weight: 600;
  font-size: 15px;
  text-transform: capitalize;
  background: var(--surface-alt);
}

/* "+" par jour (remplace le "+" générique de la ligne "Planning", retiré — demandé
   explicitement) — même principe que .meal-event-moment-row : porté par un <span>
   interne au <th>, pas le <th> lui-même (display:flex dessus lui ferait perdre son
   rattachement au calcul de largeur de colonne du tableau, voir le commentaire de
   .meal-event-moment-row plus haut pour le détail du bug que ça évite). */
.meal-event-day-row__inner {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.meal-event-day-row__add {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  color: var(--accent);
  cursor: pointer;
  transition: color 0.2s ease;
}

@media (hover: hover) and (pointer: fine) {
  .meal-event-day-row__add:hover {
    color: var(--svg-hover);
  }
}

.meal-event-day-row__add svg {
  width: 100%;
  height: 100%;
}

/* En-tête de repas — un cran moins marqué que le jour (pas de fond teinté), demandé
   implicitement par l'effet d'escalier jour > repas > plat : reste en gras pour se
   distinguer des sous-lignes de plats, sans reprendre le fond var(--surface-alt) du
   jour (qui écraserait la hiérarchie visuelle si les deux avaient le même poids).
   Texte seul directement sur le <th> — l'icône "+" qui vivait ici a déménagé dans le
   menu contextuel/glissement (.meal-event-moment-row-swipe__actions-inner, voir
   slotHtml()), demandé explicitement, plus besoin du <span> flex interne qui la
   plaçait à côté du label. */
.meal-event-moment-row-swipe__link {
  font-weight: 600;
  font-size: 14px;
  color: var(--text);
  /* Nom du repas souligné, demandé explicitement. */
  text-decoration: underline;
  transition: transform 0.2s ease;
}

.meal-event-moment-row-swipe.is-dragging .meal-event-moment-row-swipe__link {
  transition: none;
}

/* Pas de ligne de séparation entre le nom du repas et ses plats, demandé explicitement
   — seulement quand il EN A (.has-entries, posée en JS, voir slotHtml()) : c'est alors
   la dernière sous-ligne de plat qui porte la frontière avec le repas suivant (voir
   .meal-event-entry-row-swipe.is-last-in-slot plus bas). Un repas SANS plat garde sa
   propre bordure (rien d'autre ne la porterait à sa place). */
.meal-event-moment-row-swipe.has-entries td,
.meal-event-moment-row-swipe.has-entries th {
  border-bottom: none;
}

td.meal-event-moment-row-swipe__menu {
  text-align: right;
}

@media (hover: hover) and (pointer: fine) {
  .meal-event-moment-row-swipe:hover .row-menu-trigger,
  .meal-event-moment-row-swipe:focus-within .row-menu-trigger,
  .meal-event-moment-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  .meal-event-moment-row-swipe__actions {
    display: none;
  }
}

.meal-event-moment-row-swipe__action {
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0;
}

.meal-event-moment-row-swipe__actions {
  position: relative;
}

@media (hover: none), (pointer: coarse) {
  .meal-event-moment-row-swipe.is-swiped .meal-event-moment-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 64px)));
  }

  .meal-event-moment-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    inset: -1px 0 -1px auto;
    /* Fond rouge, icône blanche (rowMenuDeleteIconSvg) — demandé explicitement. */
    background: var(--danger);
    border-radius: 15px 0 0 15px;
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .meal-event-moment-row-swipe.is-dragging .meal-event-moment-row-swipe__actions-inner {
    transition: none;
  }

  .meal-event-moment-row-swipe.is-swiped .meal-event-moment-row-swipe__actions-inner {
    transform: translateX(0);
  }

  .meal-event-moment-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    color: var(--accent-contrast);
  }

  .meal-event-moment-row-swipe__action svg {
    width: 28px;
    height: 28px;
  }
}

/* ---------- Sous-ligne "plat" (recette ou texte libre) ---------- */
.meal-event-entry-row-swipe__link {
  transition: transform 0.2s ease;
}

.meal-event-entry-row-swipe.is-dragging .meal-event-entry-row-swipe__link {
  transition: none;
}

/* Pas de ligne de séparation entre deux plats du MÊME repas, demandé explicitement —
   seule la dernière sous-ligne (.is-last-in-slot, posée en JS, voir entryHtml()) garde
   son border-bottom : c'est elle qui sépare visuellement ce repas du suivant (ou du
   prochain jour). th/td explicitement les deux : la cellule vignette est un <th>... non,
   ce sont bien des <td> ici (seules .meal-event-day-row/.meal-event-moment-row-swipe
   utilisent un <th>) — gardé générique par cohérence avec les règles voisines. */
.meal-event-entry-row-swipe td,
.meal-event-entry-row-swipe th {
  border-bottom: none;
}

/* Bordure restaurée sur la DERNIÈRE sous-ligne d'un repas uniquement — sur les 4
   cellules explicitement (pas juste .link) pour que la ligne coure sur toute la
   largeur du tableau, pas seulement sous vignette+nom. Signalé sur capture : "la ligne
   de séparation avec le repas suivant n'est pas toute largeur". */
.meal-event-entry-row-swipe.is-last-in-slot td,
.meal-event-entry-row-swipe.is-last-in-slot th {
  border-bottom: 1px solid var(--border);
}

/* Cellule vignette : padding neutralisé (voir .meal-event-entry-row__thumb ci-dessous
   pour pourquoi — bug "l'image ne prend pas tout le carré", corrigé) — td.xxx (élément +
   classe) pour égaler la spécificité de la règle de base (.meal-event-planning-table td,
   même schéma), déclarée après elle dans ce fichier pour gagner à spécificité égale
   (même technique que td.shopping-item-row-swipe__menu, voir plus haut). */
td.meal-event-entry-row__thumb-cell {
  padding: 0;
}

/* <span> interne dédié à la vignette, plus la classe posée directement sur le <td>
   (voir entryHtml(), meal-event-view.js, pour le détail du bug que ce découplage
   corrige) — même principe que .shopping-item-row__thumb. margin-left: 62px = 12px
   (retrouve l'espacement de départ standard, perdu avec padding:0 sur le <td> parent)
   + 50px de décalage supplémentaire vers la droite, demandé explicitement ("décaler la
   ligne de recette vers la droite de 50px") — largeur de colonne ajustée en
   conséquence, voir .meal-event-planning-table__col--thumb plus haut (112px = 62 +
   50 de vignette, la colonne "nom" qui suit hérite déjà de son propre padding-left de
   12px, pas besoin d'une marge droite ici). */
.meal-event-entry-row__thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 50px;
  height: 50px;
  margin-left: 62px;
  border-radius: var(--radius-sm);
  background: var(--surface-alt);
  color: var(--text-muted);
  overflow: hidden;
}

.meal-event-entry-row__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.meal-event-entry-row__thumb svg {
  width: 24px;
  height: 24px;
}

.meal-event-entry-row__name-link {
  display: block;
  color: var(--text);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.meal-event-entry-row__name-link:hover {
  color: var(--accent);
}

.meal-event-entry-row__text {
  display: block;
  font-style: italic;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.meal-event-entry-row__by {
  display: block;
  font-size: 11px;
  color: var(--text-muted);
  white-space: nowrap;
}

td.meal-event-entry-row-swipe__menu {
  text-align: right;
}

@media (hover: hover) and (pointer: fine) {
  .meal-event-entry-row-swipe:hover .row-menu-trigger,
  .meal-event-entry-row-swipe:focus-within .row-menu-trigger,
  .meal-event-entry-row-swipe.row-menu-open .row-menu-trigger {
    opacity: 1;
    pointer-events: auto;
  }

  .meal-event-entry-row-swipe__actions {
    display: none;
  }
}

.meal-event-entry-row-swipe__action {
  border: none;
  background: transparent;
  cursor: pointer;
  padding: 0;
}

.meal-event-entry-row-swipe__actions {
  position: relative;
}

@media (hover: none), (pointer: coarse) {
  .meal-event-entry-row-swipe.is-swiped .meal-event-entry-row-swipe__link {
    transform: translateX(calc(-1 * var(--reveal, 64px)));
  }

  .meal-event-entry-row-swipe__actions-inner {
    display: flex;
    position: absolute;
    inset: -1px 0 -1px auto;
    /* Fond rouge, icône blanche (rowMenuDeleteIconSvg) — demandé explicitement. */
    background: var(--danger);
    border-radius: 15px 0 0 15px;
    transform: translateX(100%);
    transition: transform 0.2s ease;
  }

  .meal-event-entry-row-swipe.is-dragging .meal-event-entry-row-swipe__actions-inner {
    transition: none;
  }

  .meal-event-entry-row-swipe.is-swiped .meal-event-entry-row-swipe__actions-inner {
    transform: translateX(0);
  }

  .meal-event-entry-row-swipe__action {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 64px;
    color: var(--accent-contrast);
  }

  .meal-event-entry-row-swipe__action svg {
    width: 28px;
    height: 28px;
  }
}

/* Icône de repli (pas de photo de recette) dans la popup "Ajouter une entrée" (onglet
   "Recette existante", meal-event-view.js / renderRecipePicker()) — SANS rapport avec
   le planning ci-dessus (classe différente, .meal-event-entry-row__recipe-icon, sa
   taille suit .meal-event-entry-row__thumb svg plus haut, sélecteur descendant — pas de
   règle dédiée nécessaire pour elle) : celle-ci reste utilisée par ce picker précis. */
.meal-event-entry__recipe-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  background: var(--surface);
  flex-shrink: 0;
}

/* ---------- Popup "Ajouter une entrée" (onglets) ---------- */
.meal-event-entry-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.meal-event-entry-tab {
  flex: 1;
  border: none;
  background: transparent;
  padding: 8px 4px;
  font-size: 13px;
  color: var(--text-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
}
.meal-event-entry-tab.is-active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.meal-event-entry-empty {
  color: var(--text-muted);
  font-size: 13px;
  text-align: center;
  margin: 12px 0;
}
.meal-event-recipe-pick-row {
  cursor: pointer;
}

/* ---------- Invitation ---------- */
.meal-event-invite__not-pro {
  font-size: 12px;
  color: var(--text-muted);
}

