/* ====================== Cockpit — mobile.css (S58) ======================
   Feuille PROPRE à l'interface mobile dédiée (agenda.html, ecrans.html —
   pas panel.html, resté inchangé). Réutilise les tokens de socle.css
   (couleurs, police) — aucun token redéfini ici (§5.8) — mais construit
   des composants NEUFS : cette interface n'est pas panel.html retouché au
   format 375px, c'est un parcours pensé pour le pouce.

   Repères mesurés (brief S58, révisé) :
   - Téléphone jusqu'à 767px : barre de navigation en bas, masquage
     DIRECTIONNEL au défilement (S123 T1 — voir mobile-nav.js). S114
     avait retiré un mécanisme à SEUIL DE DISTANCE (S113, 2px,
     tremblement de doigt basculant deux fois par glissé) : celui-ci
     décide par la DIRECTION du geste, jamais une distance, demande
     explicite d'Arnaud (2026-09-10) — pas un retour en arrière sur S114.
   - Tablette dès 768px ET pointeur grossier : barre latérale permanente
     (jamais masquée — on tient une tablette par les côtés).
   - Cibles 48px (44px sur tablette, précision meilleure à deux mains),
     écart minimal 8px, 16px de dégagement pour toute cible à moins de
     80px d'un bord (conflit avec les gestes de bord iOS sinon).
   - Corps de texte 16px minimum partout, y compris les champs.

   S63 point 2 — la consigne S52 lot 6 ("le rendu de liste existe, ne le
   réécris pas") est explicitement levée par Arnaud pour ce lot : refonte
   de l'affichage agenda commandée en détail (colonne de date à gauche,
   hiérarchie titre/heure, jours vides cliquables, en-tête de mois
   collant). agenda.html ne charge plus cockpit.css — plus aucune
   dépendance au kiosque, alignée sur ecrans.html/photos.html (S62
   point 10). .dot/.badge-pill/.badge-rappel reportés tels quels
   (mêmes valeurs), la structure de liste est neuve par consigne. */

html, body { height: 100%; overflow: hidden; }

/* S62 points 8/9/10 — agenda.html charge cockpit.css UNIQUEMENT pour
   réutiliser .agenda-list / .agenda-day-* / .evt-* (S52 lot 6) — jamais
   pour son fond décoratif (body::before/::after, vague-cockpit.svg),
   pensé pour un grand écran ambiant vu de loin, jamais pour la lecture
   rapprochée d'un téléphone. Mesuré (valeurs CSS réelles, pas une
   estimation) : au point le plus dense du masque, text-secondary tombe
   à 3.03:1 contre le fond composité (7.63:1 sur fond nu) — SOUS le
   seuil 4.5:1 déjà établi (S61) pour du texte porteur d'information.
   Pas une simple gêne esthétique : une régression de contraste
   mesurée. Supprimé sur toute page mobile (.mpage), jamais touché sur
   le kiosque (index.html, qui n'a pas .mpage) — atténuer aurait exigé
   de redescendre l'opacité bien plus bas que la valeur réglée pour un
   écran mural, pour un gain incertain ; supprimer est plus simple et
   restaure exactement le fond plat déjà utilisé par ecrans.html et
   photos.html (alignement des trois pages, point 10). */
body:has(.mpage)::before, body:has(.mpage)::after { display: none; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text-primary);
  font-family: var(--font-main);
  font-size: 16px;
  /* S61 volet C, C1 — mesuré (getComputedStyle, navigateur réel) : tout
     texte secondaire de l'interface mobile (libellés de champs, "N
     écran(s)", "Dernier contact...", "Interface complète (ordinateur)",
     les pas de la feuille d'ajout) hérite du poids 300 posé sur body
     par socle.css (§5.8, jamais redéfini ici jusqu'à ce lot) — alors
     que ses couleurs (--text-muted, --text-secondary) mesurent déjà
     5.4:1 à 7.6:1 sur les trois fonds de l'interface (bg/bg-lift/
     bg-field), au-delà du seuil 4.5:1 demandé. Le défaut rapporté par
     Arnaud n'est donc pas une couleur sous le seuil — aucun jeton de
     socle.css n'est en cause ici, rien à corriger côté charte — c'est
     le poids le plus fin (300, "Inter Light") appliqué à du texte
     porteur d'information, à une taille aussi réduite que 12-13px :
     WCAG suppose un texte "normal", pas le poids le plus clair de la
     police. Correctif scopé à CETTE feuille (mobile.css) uniquement —
     ne touche ni socle.css ni le poids du kiosque/bureau (panel.html,
     index.html, superadmin.html, menu.html ne chargent pas ce fichier,
     "ce qui fonctionne est figé"). Les libellés déjà à 500 (titres,
     boutons) restent inchangés, plus spécifiques donc prioritaires en
     cascade. */
  font-weight: 400;
  -webkit-tap-highlight-color: transparent;
}

.hidden { display: none !important; }

/* ===== Page : zones sûres (encoche, barre d'accueil) =====
   height FIXE, jamais min-height : un enfant flex (.mcontent) n'obtient
   une base de hauteur fiable pour son propre défilement interne QUE si
   cet ancêtre a une hauteur définie — mesuré : avec min-height, le
   contenu débordait .mpage tout entier au lieu de défiler dans
   .mcontent/.agenda-list, et la barre ne se masquait jamais (elle
   écoute le défilement du mauvais élément, qui ne défilait pas). */
.mpage {
  height: 100vh;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  padding-top: env(safe-area-inset-top, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
  overflow: hidden;
}

.mheader {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 18px 20px 14px;
}
.mheader-title {
  font-size: 20px;
  font-weight: 500;
  color: var(--text-primary);
  margin: 0;
  flex-grow: 1;
}
/* Rare, destructeur ou secondaire (brief : "le haut est réservé à ce qui
   est rare") — Administration, Déconnexion. Jamais dans la zone de
   pouce du bas, jamais confondu avec une action de contenu. */
/* S65 T1 — 44x44 réels (Apple HIG, plancher non négociable — mesuré
   40x40 en S64, sous le seuil). Apparence inchangée : seule la zone
   touchable grandit, l'icône à l'intérieur garde sa taille propre. */
/* PATCH "en-tête de l'agenda et retour de modale" (S86) — carré gris/
   blanc hors charte trouvé sur TROIS boutons (Rappels, Réglages de
   l'agenda, Réglages global) ET sur le retour de mountBackButton
   (même classe partout) : `.mheader-link` ne réinitialisait JAMAIS le
   chrome natif du <button> (mesuré : appearance:auto, fond
   rgb(240,240,240), bordure 2px outset — le glyphe SVG se dessinait
   PAR-DESSUS ce fond par défaut du navigateur). `.mheader-add`, juste
   en dessous, avait déjà ce retrait depuis S62 — jamais reporté ici.
   C'est la RÉAPPARITION du même défaut que le "×" de la visionneuse
   photo (PIPELINE.md, dette "trois conventions de retour coexistantes") :
   un bouton d'en-tête qui ne repart pas du même socle réintroduit le
   même carré à chaque fois. Retrait aligné mot pour mot sur
   `.mheader-add` ci-dessous — un seul socle, jamais deux. */
.mheader-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: none;
  border: none;
  font: inherit;
  color: var(--text-muted);
  cursor: pointer;
  flex-shrink: 0;
  -webkit-appearance: none;
  appearance: none;
}
.mheader-link:active { color: var(--gold); }
/* S62 point 3 — "+" d'ajout, patron natif iOS (Contacts, Notes,
   Rappels) : un ajout occasionnel au-dessus d'une liste qui EST le
   contenu n'est pas une action de validation en fin de parcours — la
   consigne "actions primaires en bas d'écran" visait ce second cas,
   pas celui-ci. Taille visuelle réduite (24px) mais cible tactile
   réelle ≥44px (min-width/min-height, jamais seulement le glyphe). */
.mheader-add {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  margin: -4px -10px -4px 0;
  background: none;
  border: none;
  font: inherit;
  color: var(--gold);
  cursor: pointer;
  flex-shrink: 0;
  -webkit-appearance: none;
  appearance: none;
}
.mheader-add:active { color: var(--gold-dim); }
/* S59 — un icône seul, sans mot, a été lu comme "une commande sans
   fonction" (§5.14) : le libellé rend la fonction évidente au premier
   regard, jamais à deviner. */
.mheader-link-labeled {
  width: auto;
  gap: 8px;
  padding: 0 14px 0 10px;
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
}
.mheader-link-labeled span { font-size: 14px; }

/* ===== Lien discret vers l'interface complète — la SEULE porte entre
   les deux interfaces (brief S59, décision d'Arnaud). Discret mais
   jamais caché : nommé en toutes lettres, jamais une icône seule qui
   inviterait à un tap distrait. ===== */
.mlink-complete {
  display: block;
  width: 100%;
  margin: 28px 0 20px;
  padding: 14px 0;
  background: none;
  border: none;
  border-top: 1px solid var(--hair);
  color: var(--text-muted);
  font: inherit;
  font-size: 14px;
  text-align: center;
  cursor: pointer;
}
.mlink-complete:active { color: var(--text-secondary); }

/* ===== Entrée de navigation inactive (brief S59, pt.2) — visiblement
   distincte d'une destination réelle : jamais cliquable, jamais un
   renvoi silencieux vers une autre interface.

   S61 volet C, C1 — cas particulier signalé par Arnaud : "atténué" ne
   veut pas dire "invisible". Mesuré (contraste composité, opacité
   comprise) : --text-faint sur --bg-lift ne dépasse JAMAIS 1.82:1, même
   à opacité pleine — ce jeton lui-même est trop sombre pour porter du
   texte ici, à N'IMPORTE QUELLE opacité (opacity ne fait qu'interpoler
   vers cette limite, jamais au-delà). Corrigé en changeant le jeton
   RÉFÉRENCÉ par CETTE règle (text-muted, déjà 5.45:1 plein), pas en
   modifiant --text-faint lui-même — ce jeton est partagé avec le
   bureau (admin.css, socle.css, cockpit.css : placeholders, états
   désactivés) et une correction de charte commune n'est pas décidée
   ici sans mesurer d'abord son impact là-bas (hors périmètre de ce
   lot, signalé au récap). Opacité remontée de .5 à .75 (icône+libellé
   ET badge "Bientôt", qui hérite de cette opacité) : mesuré à 3.57:1
   composité — au-dessus du seuil décoratif 3:1, toujours visiblement
   plus terne que les trois onglets actifs (5.45:1 plein). */
.mnav-item.is-soon {
  color: var(--text-muted);
  opacity: .75;
  cursor: default;
  pointer-events: none;
  position: relative;
}
.mnav-item-soon-badge {
  position: absolute;
  top: 2px;
  font-size: 8px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.mcontent {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  /* CORRECTIF (S117 P1, "la page Activités déborde et bouge") — même
     défaut que .msheet, corrigé S116, jamais reporté ici : overflow-y
     seul, sans overflow-x posé à côté, fait calculer overflow-x à
     `auto` (jamais `visible`, CSS Overflow §3.1 — confirmé ici par
     getComputedStyle sur .mcontent réel). N'importe quel débordement
     horizontal, même d'un pixel, dessine alors une VRAIE barre de
     défilement. .mcontent est LE conteneur défilant de neuf pages sur
     neuf (activites.html compris — elle n'utilise PAS .msheet, sa
     fiche est une page réelle, jamais une feuille : c'est pour ça que
     le correctif S116 ne l'a jamais atteinte). Posé ICI, au même titre
     que .msheet, jamais sur une seule page. */
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  padding: 0 20px;
  /* CORRECTIF (S124 T3, "l'écran clignote quand on force vers le haut")
     — le rechargement par tirage de Safari (pull-to-refresh) se
     déclenche quand le geste dépasse le haut du CONTENU ; le masquage
     directionnel de S123 n'en est pas la cause mais rend le geste plus
     fréquent (la barre libère de la place, incitant à continuer de
     tirer une fois en haut). Posé sur le CONTENEUR défilant lui-même,
     jamais sur `html`/`body` (brief explicite, "jamais sur le document
     entier sans mesure") — un rebond élastique en dehors de ce
     conteneur (S65, écran mural) n'est jamais concerné. */
  overscroll-behavior-y: contain;
}
/* .agenda-rows (S63 point 2, remplace .agenda-list de cockpit.css) porte
   son propre défilement — jamais deux défileurs imbriqués : .mcontent
   lui cède la main quand elle est présente (agenda.html uniquement).
   CORRECTIF (S99, enquête troncature agenda) — :has() seul (Safari
   15.4+, mars 2022) est un point de défaillance UNIQUE pour une règle
   aussi critique à la mise en page : si jamais il ne matche pas (pour
   quelque raison que ce soit), .mcontent retombe sur overflow-y:auto
   générique, DEUX défileurs imbriqués (.mcontent ET .agenda-rows) —
   source connue de défilement qui semble s'arrêter tôt sur iOS Safari
   (chaînage de défilement entre conteneurs imbriqués). Classe explicite
   posée par le HTML en renfort, jamais remplacée : ne dépend d'aucune
   fonctionnalité CSS récente. */
.mcontent:has(.agenda-rows), .mcontent.mcontent-agenda-rows { overflow-y: hidden; padding: 0; }

/* ===== Boutons — brief explicite : "sur mobile, une action porte un
   fond ou un contour, toujours. Aucune action ne partage le traitement
   typographique d'un titre." Jamais .act (lien souligné, réservé au
   bureau). ===== */
.mbtn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  min-height: 48px;
  padding: 12px 20px;
  border-radius: var(--radius-md);
  font: inherit;
  font-size: 16px;
  font-weight: 500;
  cursor: pointer;
  box-sizing: border-box;
  border: 1px solid transparent;
  -webkit-appearance: none;
  appearance: none;
}
.mbtn-primary {
  background: var(--gold);
  color: var(--bg);
}
.mbtn-primary:active { background: var(--gold-dim); }
.mbtn-outline {
  background: transparent;
  border-color: var(--gold-dim);
  color: var(--gold);
}
.mbtn-outline:active { background: var(--gold-faint); }
.mbtn-secondary {
  background: var(--bg-lift);
  border-color: var(--hair);
  color: var(--text-primary);
}
.mbtn-secondary:active { background: var(--bg-field); }
.mbtn-danger {
  background: transparent;
  border-color: var(--red);
  color: var(--red);
}
.mbtn-danger:active { background: var(--red-faint); }

/* S67 T1 — garde anti double-appui (mobile-nav.js::guardedAction). Un
   bouton occupé reste lisible (jamais un texte qui disparaît sous un
   spinner) : juste visiblement estompé, curseur "attente", et non
   interactif nativement via l'attribut disabled — cette règle ne fait
   que rendre l'état visible, `disabled` fait déjà tout le travail de
   blocage. */
button:disabled, .mrow-card:disabled, .msheet-choice.is-busy { opacity: .55; cursor: wait; }

/* ===== Liste de rangées (écrans, connexions...) — rangée entière
   cliquable, cible large, jamais des liens .act minuscules empilés. ===== */
.mrows { display: flex; flex-direction: column; gap: 8px; margin: 16px 0; }
.mrow-card {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 64px;
  padding: 12px 16px;
  border-radius: var(--radius-md);
  background: var(--bg-lift);
  border: 1px solid var(--hair);
  cursor: pointer;
  text-align: left;
  color: inherit;
  font: inherit;
  width: 100%;
  box-sizing: border-box;
  -webkit-appearance: none;
  appearance: none;
}
.mrow-card:active { background: var(--bg-field); }
.mrow-card-main { flex-grow: 1; min-width: 0; }
.mrow-card-title { font-size: 16px; color: var(--text-primary); }
.mrow-card-sub { font-size: 13px; color: var(--text-muted); margin-top: 2px; }
.mrow-card-chevron { color: var(--text-muted); flex-shrink: 0; }
/* S80 — valeur d'état à droite d'une ligne de forage ("Agendas
   connectés   3"), avant le chevron. Absente si la ligne n'a pas
   d'état à montrer (§ cadrage réglages : "quand il y en a un"). */
/* CORRECTIF "version et éditeur" (S99), vérification systématique
   demandée : trouvé en mesurant sur le rendu réel (pas la donnée de
   test la plus courte) — "Personne en particulier" (valeur réelle,
   ligne "Cet appareil") mesurait 164.6px, flex-shrink:0 lui laissant
   toute la place et poussant le libellé ("Cet appareil", 90.4px
   nécessaires) sur deux lignes dans une ligne de 335px. Une VALEUR est
   une donnée de longueur imprévisible (nom de membre inclus) — jamais
   au libellé, fixe et court, d'en payer le prix. Tronquée avec
   ellipse plutôt que d'écraser le libellé. */
.mrow-card-value { font-size: 15px; color: var(--text-muted); flex-shrink: 0; max-width: 45%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.mempty { padding: 32px 0; text-align: center; color: var(--text-muted); font-size: 16px; }

/* ===== Action principale fixe — brief : "actions principales dans les
   40% inférieurs de l'écran", toujours atteignable au pouce sans
   défiler, jamais perdue en bas d'une longue liste. ===== */
.mcta-fixed {
  /* .mbtn fixe width:100% — sur un élément position:fixed, un width
     explicite rend "right" sans effet (système sur-contraint, CSS
     2.1 §10.3.7 : right est ignoré, pas recalculé). Mesuré : le bouton
     débordait de 20px à droite. width explicite en calc(), jamais
     left+width-auto+right sur le même élément. */
  position: fixed;
  left: 20px;
  width: calc(100% - 40px);
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  z-index: 140;
}

/* ===== Modale bas d'écran — "un titre, un contenu, les actions en bas,
   atteignables au pouce. Une modale par tâche." ===== */
.msheet-overlay {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  display: flex;
  align-items: flex-end;
  z-index: 200;
}
.msheet {
  width: 100%;
  /* S61 volet C, C2 — mesuré : 88vh laisse la feuille s'étendre au-delà
     de la zone RÉELLEMENT visible sur Safari iOS (barres comprises),
     sans jamais dépasser son propre scrollHeight — overflow-y:auto n'a
     alors rien à faire défiler (contenu qui "tient" dans une boîte
     elle-même trop haute), et html/body étant overflow:hidden (aucun
     repli), le geste de défilement produit un rebond sans déplacement.
     Même défaut, même correctif que .mpage (S58) : dvh suit la zone
     visible DYNAMIQUE, jamais la zone nominale barres exclues. */
  max-height: 88vh;
  max-height: 88dvh;
  overflow-y: auto;
  /* CORRECTIF (S116, "la fiche de rappel perd la saisie") — mesuré :
     `overflow-y: auto` seul, sans `overflow-x` posé à côté, fait
     calculer overflow-x à `auto` (jamais `visible`, règle CSS Overflow
     §3.1, confirmé ici par getComputedStyle sur .msheet réel). N'importe
     quel débordement horizontal, même d'un pixel, dessine alors une
     VRAIE barre de défilement — jamais reproduit dans chrome-headless-
     shell (limite d'outillage déjà rencontrée S113/S114, WebKit
     seul en cause probable ici, ex. input[type=number] : seul contrôle
     natif de cette fiche jamais couvert par le correctif date/heure de
     S103 — hypothèse posée, non mesurée sur Safari réel, hors de
     portée de cet outillage). Une feuille est par construction une
     colonne pleine largeur : rien n'a jamais eu besoin de défiler
     latéralement, quelle qu'en soit la cause précise. Composant
     PARTAGÉ (neuf pages) : posé ICI, jamais sur la fiche École seule
     (§2.8 — le défaut de spécification touche les neuf, vérifié). */
  overflow-x: hidden;
  background: var(--bg);
  border-radius: var(--radius-md) var(--radius-md) var(--radius-none) var(--radius-none);
  border-top: 1px solid var(--hair);
  padding: 24px 20px calc(20px + env(safe-area-inset-bottom, 0px));
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.msheet-title { font-size: 18px; font-weight: 500; margin: 0; }
/* Retour unique (mobile-nav.js::mountBackButton) — en haut à gauche de
   la feuille, visible sans défilement, jamais une sortie en pied en
   plus (brief explicite). */
.msheet-head { display: flex; align-items: center; gap: 10px; margin: -8px 0 0 -10px; }
.msheet-head .msheet-title { flex: 1; }
.msheet-steps { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 10px; font-size: 16px; color: var(--text-secondary); }
.msheet-field label {
  display: block;
  font-size: 13px;
  color: var(--text-muted);
  margin-bottom: 6px;
}
.msheet-field input, .msheet-field select {
  width: 100%;
  min-height: 48px;
  font-size: 16px;
  font-family: inherit;
  color: var(--text-primary);
  background: var(--bg-field);
  border: 1px solid var(--hair);
  border-radius: var(--radius-md);
  padding: 0 14px;
  box-sizing: border-box;
}
/* PATCH "contrôles de formulaire hors charte" (S88), point 4 — même
   raison que socle.css::input.field : un texte qui dépasse un champ
   sur une ligne doit le signaler (ellipse), jamais se couper en
   silence. `select` inclus sans effet néfaste (contenu toujours court). */
.msheet-field input {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.msheet-field input:focus, .msheet-field select:focus {
  outline: none;
  border-color: var(--gold-dim);
}
/* Safari iOS rend input[type=date/time] en display:inline-flex et ignore
   width:100% tant que display:block n'est pas posé — débordement
   horizontal constaté sur le formulaire d'ajout d'événement (05/09).
   La mise en block réintroduit un remplissage bas parasite (line-height
   du contrôle interne non alignée à min-height) : neutralisé ici.
   Remplissages du shadow DOM (::-webkit-datetime-edit et enfants)
   remis à zéro — ils s'ajoutaient au padding déjà posé sur .msheet-field
   input, doublant l'espace interne et poussant le contrôle hors de sa
   largeur réservée. */
.msheet-field input[type="date"],
.msheet-field input[type="time"] {
  display: block;
  line-height: normal;
}
.msheet-field input[type="date"]::-webkit-datetime-edit,
.msheet-field input[type="time"]::-webkit-datetime-edit,
.msheet-field input[type="date"]::-webkit-datetime-edit-fields-wrapper,
.msheet-field input[type="time"]::-webkit-datetime-edit-fields-wrapper {
  padding: 0;
  margin: 0;
}
.msheet-field input[type="date"]::-webkit-date-and-time-value,
.msheet-field input[type="time"]::-webkit-date-and-time-value {
  text-align: left;
  margin: 0;
}
.msheet-choices { display: flex; gap: 8px; flex-wrap: wrap; }
.msheet-choice {
  min-height: 48px;
  padding: 0 18px;
  border-radius: var(--radius-md);
  border: 1px solid var(--hair);
  background: var(--bg-field);
  color: var(--text-secondary);
  font-size: 16px;
  display: flex;
  align-items: center;
  cursor: pointer;
}
.msheet-choice.is-on { border-color: var(--gold); color: var(--gold); background: var(--gold-faint); }
.msheet-actions { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
.msheet .msg { font-size: 14px; min-height: 18px; }
.msheet .msg.ok { color: var(--green); }
.msheet .msg.ko { color: var(--red); }

/* Résumé de repli natif <details> — réutilisable (S99, correctif
   "réglages d'écran introuvables") : même patron qu'Atlas "détails/
   résumé" déjà utilisé par courses.html ("Déjà pris"), généralisé ici
   pour toute section secondaire qui ne doit pas s'imposer par défaut.
   Cible tactile ≥44px comme partout ailleurs dans ce fichier, jamais
   une exception pour un contrôle "juste un repli". */
.mdetails-summary {
  font-size: 12px; color: var(--text-muted); letter-spacing: .02em;
  cursor: pointer; list-style: none;
  display: flex; align-items: center; gap: 6px;
  min-height: 44px; box-sizing: border-box;
}
.mdetails-summary::-webkit-details-marker { display: none; }
.mdetails-summary::before { content: "▸"; display: inline-block; transition: transform .15s; }
details[open] > .mdetails-summary::before { transform: rotate(90deg); }

/* ===== Aperçu (à conserver, même en petit — brief explicite) ===== */
.mpreview {
  position: relative;
  width: 100%;
  aspect-ratio: 16/9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-lift);
  border: 1px solid var(--hair);
  margin: 16px 0;
  /* S75 — bug réel mesuré en portant l'aperçu sur ecran-detail.html :
     un enfant direct d'un conteneur flex en colonne (#detailContent
     .mcontent) dont le SEUL contenu est un enfant en position absolute
     (l'iframe, sans emprise sur la taille intrinsèque) ne dérive jamais
     sa hauteur de aspect-ratio par l'algorithme flex — mesuré à
     1.33px de haut au lieu de 687px attendus (16:9 d'une largeur de
     1222px). flex-shrink:0 le sort de ce calcul, sans effet hors
     contexte flex (ecrans.html, .msheet, jamais flex — jamais affecté). */
  flex-shrink: 0;
}
.mpreview iframe {
  position: absolute;
  top: 0; left: 0;
  width: 1920px;
  height: 1080px;
  border: 0;
  transform-origin: top left;
  pointer-events: none;
}

/* S73 point 4/5 — profil Téléphone : mesuré (docs/JOURNAL-S73.md), un
   cadre 1920×1080 systématique produisait un texte déjà dimensionné en
   px fixes pour un vrai téléphone (16px, seuil iOS S50) réduit à moins
   de 3px effectifs une fois écrasé dans la carte ~343px de cette
   feuille — ET empêchait la bascule de mise en page réelle d'un
   téléphone (@media max-width:1024px et portrait, cockpit.css) de se
   déclencher, montrant une mise en page qu'aucun téléphone réel
   n'affiche. Dimensions reprises telles que mesurées par Arnaud sur son
   propre appareil : 375×610 CSS utiles, jamais 390×844 (résolution
   physique, pas la taille CSS après barres du navigateur). */
.mpreview.is-portrait {
  aspect-ratio: 375 / 610;
}
.mpreview.is-portrait iframe {
  width: 375px;
  height: 610px;
}

/* ===== Message générique (upload, connexion...) — généralisé depuis
   .msheet .msg (mêmes couleurs, jamais une troisième paire de classes
   pour la même distinction) pour tout message hors feuille modale. ===== */
.msg { font-size: 14px; min-height: 18px; margin-top: 8px; color: var(--text-secondary); }
.msg.ok { color: var(--green); }
.msg.ko { color: var(--red); }

/* ===== Connexion (login-gate partagé, agenda.html / ecrans.html) ===== */
.mlogin { flex: 1; display: flex; flex-direction: column; justify-content: center; padding: 0 28px; gap: 4px; }
.mlogin-title { font-size: 22px; font-weight: 500; margin: 0 0 22px; }

/* ===== S62 point 5 — Photos : grille de vignettes + vue unique =====
   Patron natif Photos iOS. 78 photos listées une par écran (S61 volet B,
   .mphoto-card pleine largeur) demandaient 78 écrans de défilement pour
   atteindre la dernière — densité injouable, mesurée sur capture réelle.
   La grille ne fait plus que lister et ouvrir ; TOUTES les actions
   (réordonner, pivoter, point focal, supprimer) vivent dans la vue
   unique plein écran (.mphoto-detail), jamais dans la grille — décision
   qui règle d'un coup la densité, la place des actions, et l'endroit où
   loger le point focal sans l'encombrer. 3 colonnes à 375px (gap 2px,
   comme l'app Photos), recadrage carré — la grille sert à repérer une
   photo, pas à l'examiner : le cadrage exact se voit dans la vue
   unique, jamais tronqué au clic. */
.mphoto-thumbgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2px; margin: 16px 0; }
.mphoto-thumb-btn {
  position: relative;
  aspect-ratio: 1;
  background: var(--bg-field);
  border: none;
  padding: 0;
  overflow: hidden;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.mphoto-thumb-btn img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mphoto-thumb-btn:active img { opacity: .7; }
/* S65 T10 — sélection multiple : même valeurs que .photo-item.is-sel
   du bureau (admin.css:132), reportées à l'identique. */
.mphoto-thumb-btn.is-sel { outline: 3px solid var(--gold); outline-offset: 3px; }

/* Vue unique — plein écran, au-dessus de la navigation (150) mais sous
   les feuilles de confirmation/point focal (200) qui peuvent s'ouvrir
   par-dessus elle (msheet-overlay). */
.mphoto-detail {
  position: fixed;
  inset: 0;
  z-index: 180;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.mphoto-detail-position { flex-grow: 1; text-align: center; color: var(--text-muted); font-size: 13px; padding-right: 40px; }
.mphoto-detail-imgwrap {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-field);
  overflow: hidden;
}
.mphoto-detail-imgwrap img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }

/* S63 point 1 — RÉTABLI : icônes discrètes posées sur l'image, telles
   qu'elles existent dans panel.html (admin.css::.pi-actions, variante
   tactile @media(hover:none),(pointer:coarse) — déjà pensée pour ce
   pointeur, jamais retouchée ici, seulement reportée). Les quatre gros
   boutons libellés en rangée (S62) n'avaient été demandés par personne
   — la consigne "cible ≥44px" est une contrainte de dimension, jamais
   une autorisation de redessiner (doctrine actée, CLAUDE.md). Mêmes
   glyphes, même dégradé de fond, mêmes tailles de police par glyphe
   (mesurées S39 pour une encre visuelle égale malgré des glyphes
   différents) — copiés valeur pour valeur depuis admin.css, pas
   réinventés. */
.pi-actions {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 6px;
  background: linear-gradient(to top,
    color-mix(in srgb, var(--bg) 75%, transparent) 0%,
    color-mix(in srgb, var(--bg) 75%, transparent) 65%,
    transparent 100%);
}
.pi-actions button {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  font-size: 22px;
  font-family: inherit;
  color: var(--text-primary);
  background: none;
  border: none;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.pi-actions button[disabled] { opacity: .35; pointer-events: none; }
.pi-actions [data-up], .pi-actions [data-down] { font-size: 26px; }
.pi-actions .del { font-size: 24px; }
.pi-actions .del:active { color: var(--red); }

/* Liste de messages d'échec/doublon (brief "Retour visuel de l'envoi de
   photos", précédent panel.js) — même distinction : rouge pour un
   échec réel, neutre pour un doublon (pas une erreur). */
.mphoto-issues { margin: 8px 0 0; padding-left: 20px; font-size: 13px; line-height: 1.6; }
.mphoto-issues-fail { color: var(--red); }
.mphoto-issues-dup { color: var(--text-muted); }

/* S62 point 1 — point focal (régression comblée : présent dans
   panel.html/admin.css depuis S41, absent du mobile). Même patron,
   mêmes valeurs (admin.css::.focal-img-wrap/.focal-marker) : une seule
   interaction, le tap POSE et ENREGISTRE le point, jamais de bouton
   "valider" séparé ni de poignées de cadrage — déjà pensé au doigt à
   l'origine ("touchez ou cliquez le point à garder visible"), reporté
   ici sans réinvention. */
.mfocal-img-wrap { position: relative; max-width: 100%; line-height: 0; touch-action: none; }
.mfocal-img-wrap img { max-width: 100%; display: block; -webkit-user-select: none; user-select: none; -webkit-user-drag: none; }
.mfocal-marker {
  position: absolute;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border: 2px solid var(--gold);
  border-radius: var(--radius-full);
  background: var(--gold-faint);
  pointer-events: none;
}
.mfocal-marker::after { content: ''; position: absolute; inset: 8px; border-radius: var(--radius-full); background: var(--gold); }

/* ===========================================================
   Navigation — même balisage pour les deux formats, la mise en
   page bascule entièrement par media query (brief explicite :
   pas le format téléphone agrandi sur tablette).
   =========================================================== */
/* CORRECTIF ESPACEMENT (constat Arnaud, mesuré en iframe 375px avant
   tout changement — patron déjà établi PIPELINE.md §F) : le bouton
   vocal en position:absolute, centré sur TOUTE la largeur/hauteur de
   la barre (left:50%/top:50%), tombait exactement sur la frontière
   entre les deux items centraux et CHEVAUCHAIT leur cible tactile de
   30px de chaque côté (mesuré : voix.x=155.7, Courses.right=185.7,
   Photos.x=185.7, voix.right=215.7 — chevauchement, pas un manque de
   marge). Corrigé : grille CSS EXPLICITE avec une piste RÉSERVÉE pour
   le bouton (jamais une répartition automatique qui écrase), --mnav-
   voice-track = largeur/hauteur du bouton (60px) + 2×14px de dégagement
   symétrique (marge au-dessus du minimum WCAG 2.2 SC 2.5.8 de 12px). */
:root { --mnav-voice-track: 88px; }

.mnav {
  position: fixed;
  z-index: 150;
  display: flex;
  background: var(--bg-lift);
  border: 1px solid var(--hair);
}
.mnav-item {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--text-muted);
  text-decoration: none;
}
.mnav-item.is-on { color: var(--gold); }
.mnav-item svg { flex-shrink: 0; }
.mnav-item-label { font-size: 11px; }

/* S81 — bouton vocal central, PAS un onglet (aucun .mnav-item). Anneau
   "détaché" : même langage visuel que .badge-pill.unusual/
   .mcolor-swatch.is-on (double box-shadow bg/gold), jamais réinventé.
   Placement (grid-column/-row + transform de protrusion) décidé par
   breakpoint ci-dessous — jamais position:absolute (c'était la cause
   du chevauchement : un positionnement hors grille ignore forcément
   la largeur réelle de ses voisins). */
.mnav-voice-btn {
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 60px;
  height: 60px;
  border-radius: var(--radius-full);
  background: var(--gold);
  color: var(--bg);
  border: none;
  box-shadow: 0 0 0 4px var(--bg), 0 4px 14px var(--shadow);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.mnav-voice-btn:active { background: var(--gold-dim); }

/* Téléphone : barre du bas, pleine largeur, FIXE (S114 — voir plus
   haut, "la barre de navigation ne se masque plus"). Grille à 5
   colonnes EXPLICITES : les 4 destinations à 1fr chacune, la 3e piste
   RÉSERVÉE au bouton vocal (jamais une des quatre items, jamais une
   répartition qui bouge si leur contenu change). */
@media (max-width: 767px), (width >= 768px) and (pointer: fine) {
  .mnav {
    left: 0; right: 0; bottom: 0;
    display: grid;
    /* S141 T6 — Photos quitte la barre (arbitrage d'Arnaud du 12/09,
       vers Réglages : "de la configuration d'affichage, pas une
       consultation quotidienne") : retour à QUATRE destinations, la
       piste ajoutée par S139 T1 pour "À faire" est retirée — la barre
       repasse sous le plafond de cinq posé par A3bis (5 destinations +
       micro l'avait saturée sans que personne ne le relève). Colonnes
       EXACTES d'avant S139 (1/2/4/5), jamais renumérotées. */
    grid-template-columns: 1fr 1fr var(--mnav-voice-track) 1fr 1fr;
    /* grid-template-rows EXPLICITE (56px, jamais auto) — sans ça, la
       ligne unique s'agrandit pour loger le bouton vocal (60px), et les
       QUATRE destinations (stretch par défaut) grandissent avec elle,
       débordant à leur tour de la zone sûre (mesuré : 783px, encore
       5px au-delà de la limite). Le bouton continue de dépasser au-delà
       de cette ligne fixe via son propre transform ci-dessous — c'est
       voulu (protrusion), jamais subi par les quatre items normaux. */
    grid-template-rows: calc(56px - 1px); /* -1px : border-top:1px solid reste posé (border-left/-right/-bottom seuls retirés ci-dessous), et box-sizing:border-box le prélève sur ce même budget de 56px — mesuré (1px d'écart exact, 779px au lieu de 778px, avant ce correctif) */
    /* CORRECTIF "barre et libellé" (S99) — height:56px FIXE avec
       padding-bottom:env(...) faisait manger la zone sûre DANS ce
       budget de 56px (mesuré : sur 34px de zone sûre, il ne restait que
       22px de contenu utile pour des items de 48px de haut — ils
       débordaient déjà tous dans la zone sûre avant même le bouton
       vocal, jamais vu par le test précédent qui n'émulait aucune zone
       sûre). La barre grandit maintenant AVEC la zone sûre au lieu de
       rétrécir son contenu — même patron additif que .mcontent/
       .mcta-fixed/.agenda-rows ci-dessous, qui l'appliquaient déjà. */
    height: calc(56px + env(safe-area-inset-bottom, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
    border-left: none; border-right: none; border-bottom: none;
  }
  .mnav-item { flex-direction: column; min-height: 48px; }
  /* Ordre DOM réel : les 4 <a class="mnav-item"> puis le bouton vocal
     en dernier (web/assets/js/mobile-nav.js) — placement EXPLICITE
     plutôt que de dépendre de l'ordre naturel, pour que le bouton
     s'intercale visuellement au milieu sans dépendre du markup. */
  .mnav-item:nth-child(1) { grid-column: 1; }
  .mnav-item:nth-child(2) { grid-column: 2; }
  .mnav-item:nth-child(3) { grid-column: 4; }
  .mnav-item:nth-child(4) { grid-column: 5; }
  /* Contenu au-dessus de la barre — jamais recouvert (S58 : "le bas est
     réservé aux actions principales", la barre ne doit rien cacher).
     S81 — la marge de dégagement passe de 16 à 40px : le bouton vocal
     protrude de 30px au-dessus de la barre (centré sur son bord haut),
     le défaut déjà corrigé sur Photos (S62) ne devait pas revenir ici. */
  /* CORRECTIF "version et éditeur" (S99), point 1 — padding-bottom ICI
     était un piège : .mcontent défile (flex:1, overflow-y:auto), et un
     padding-bottom fait PARTIE de la zone défilante (comptabilisé dans
     scrollHeight) — il ne protège donc que la position TOUT EN BAS du
     défilement, jamais la position NATURELLE (scrollTop=0, ce que
     l'utilisateur voit à l'ouverture). Mesuré, reproduit avec un compte
     réaliste (ligne "Diagnostic superadmin" visible, comme celui
     d'Arnaud) : le pied de page finissait à 746px quand le bouton
     vocal commençait à 690.5px — 55.5px de recouvrement RÉEL, alors
     même que le padding-bottom (130px) semblait largement suffisant
     sur le papier. margin-bottom, à l'inverse, retire réellement cette
     hauteur de la boîte défilante elle-même (.mcontent devient plus
     courte) : le contenu déborde donc plus tôt, mais ne peut plus
     JAMAIS atteindre visuellement la zone réservée, scrollé ou non —
     c'est la garantie que padding-bottom ne donnait pas. */
  .mcontent { margin-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
  /* CORRECTIF (S114, "la réserve de place, maintenant qu'elle est
     permanente") — le +40px ci-dessus (S81, clairance du bouton vocal)
     s'appliquait à TOUTES les pages, y compris Photos et Activités qui
     ne rendent JAMAIS ce bouton (S99/S112, "aucun objet dictable") :
     mesuré, un vide allant jusqu'à 40px de trop sur ces deux pages,
     sans aucun élément à y protéger. `:has(.mnav-voice-btn)` matche
     uniquement les pages où le bouton existe réellement dans le DOM —
     même patron que `:has(.mcta-fixed)` ci-dessous, déjà en usage ici. */
  .mpage:has(.mnav-voice-btn) .mcontent { margin-bottom: calc(56px + env(safe-area-inset-bottom, 0px) + 40px); }
  /* CORRECTIF (S99, "réserve comptée deux fois" — même famille que le
     défilement de l'agenda, signalé par Arnaud) — :has(.mcta-fixed)
     matche la PRÉSENCE dans le DOM, jamais sa visibilité : #batchBar
     (photos.html) est POSÉ en permanence dans le HTML, seulement masqué
     par la classe "hidden" hors du mode sélection. Cette règle
     s'appliquait donc TOUJOURS (mesuré : margin-bottom à 160px hors
     sélection, batchBar bel et bien caché), volant 64px en PERMANENCE à
     la grille de photos pour une barre qui n'est, la plupart du temps,
     jamais affichée. :not(.hidden) restreint la règle au SEUL moment où
     la barre occupe réellement cet espace. +40px retiré (S114, ci-
     dessus) : photos.html ne rend jamais le bouton vocal. */
  .mpage:has(.mcta-fixed:not(.hidden)) .mcontent { margin-bottom: calc(56px + 64px + env(safe-area-inset-bottom, 0px)); }
  .mcta-fixed { bottom: calc(56px + env(safe-area-inset-bottom, 0px)); }
  /* CORRECTIF "barre et libellé" (S99) — bouton coupé par le bord bas
     en app épinglée, constaté sur iPhone. Mesuré (Puppeteer + CDP
     Emulation.setSafeAreaInsetsOverride, seul moyen de reproduire une
     vraie zone sûre hors d'un appareil réel) : grid-row n'était JAMAIS
     posé ici — seul grid-column l'était. Le placement automatique de
     grille (sparse packing, curseur qui n'avance que vers l'avant) a
     donc placé ce bouton dans une ligne IMPLICITE 2, après que les
     quatre items (colonnes 1/2/4/5) aient fait avancer le curseur au-
     delà de la colonne 3 sur la ligne 1 — computed grid-template-rows
     mesuré "48px 60px" (DEUX lignes, jamais voulu), bouton mesuré à
     835px de haut sur un écran de 812px : littéralement hors écran,
     pas seulement hors zone sûre. grid-row:1 explicite force le
     partage de la MÊME ligne que les quatre destinations. */
  .mnav-voice-btn { grid-column: 3; grid-row: 1; justify-self: center; align-self: center; transform: translateY(-30px); }
  /* CORRECTIF (S99, constat Arnaud : "la liste n'occupe que 42% de
     l'écran") — .agenda-rows portait ICI sa PROPRE réserve de 130px,
     EN PLUS de celle déjà posée sur .mcontent (règle générique
     ci-dessus, jamais retirée par .mcontent:has(.agenda-rows) — cette
     dernière ne touche que overflow-y/padding, jamais margin-bottom).
     Mesuré : .mcontent réserve déjà, À ELLE SEULE, la bonne hauteur
     (son propre bas s'arrête à 8-9px du bouton vocal, clôturé par
     overflow-y:hidden — rien ne peut jamais dessiner plus bas). La
     réserve ICI était donc une SECONDE réservation de la MÊME zone,
     jamais additive dans les faits (.agenda-rows est elle-même bornée
     par le bas déjà clos de .mcontent) mais RETRANCHÉE de la hauteur
     utile de la liste (474px mesurés après retrait, contre 344px
     avant — la liste passe de 42% à 58% de l'écran). Retirée.
     Vérifié : aucun recouvrement avec le bouton vocal (scripts/
     test-elements-critiques-presents.js, point 9). */
  /* S123 T1 — masquage directionnel (mobile-nav.js pose/retire les deux
     classes ensemble). +40px : même clairance que la protrusion du
     bouton vocal (30px, .mnav-voice-btn ci-dessus) — sans cette marge,
     translateY(100%) seul laissait le bouton à moitié visible (mesuré
     S114, "il déborde de trente pixels au-dessus d'elle et ne suit pas
     une translation de 100%" — brief explicite, traité ici). */
  /* Bascule INSTANTANÉE, jamais animée — brief explicite : "réapparaît
     IMMÉDIATEMENT, sans seuil, sans attente". Une transition CSS aurait
     laissé une fenêtre où la barre est mesurée (et vue) à mi-course. */
  .mnav.mnav--hidden { transform: translateY(calc(100% + 40px)); }
}
/* S123 T1 — rétraction CONJOINTE de la réserve de contenu quand la barre
   est masquée : sans elle, le vide qu'elle laissait restait réservé
   pour rien ("on déplace le vide", brief explicite). scrollTop du
   conteneur ne change pas quand sa hauteur augmente (margin-bottom plus
   petit) : la ligne EN COURS DE LECTURE (haut de la zone visible) ne
   bouge jamais, seul le bas (hors du champ de vision de qui lit) gagne
   de la place — mesuré, retenu plutôt que la réserve fixe (l'autre
   option essayée, brief explicite de comparer les deux).
   Spécificité (0,3,0), À ÉGALITÉ avec les trois règles margin-bottom du
   bloc téléphone ci-dessus (.mcontent seul, :has(.mnav-voice-btn),
   :has(.mcta-fixed:not(.hidden))) — placée APRÈS elles dans la feuille,
   l'ordre de source tranche l'égalité en sa faveur, sur les trois. */
@media (max-width: 767px), (width >= 768px) and (pointer: fine) {
  :root.mnav-hidden .mcontent { margin-bottom: env(safe-area-inset-bottom, 0px); }
}

/* S135 T1 — repli TÉLÉPHONE de la grille mensuelle (masquée par
   défaut, la tablette la réactive plus bas) — DÉPLACÉ ICI depuis sa
   position d'origine (après sa propre définition visuelle, plus bas
   dans ce fichier), qui le plaçait APRÈS l'override tablette
   ci-dessous : même spécificité, l'ordre de source aurait fait gagner
   ce repli MÊME sur tablette (bogue réel, voir le commentaire à
   l'ancienne position de cette règle pour la mesure complète). Cette
   ligne doit TOUJOURS précéder le bloc tablette qui la lève. */
.agenda-grid { display: none; }

/* Tablette (pointeur grossier, ≥768px) : barre latérale permanente,
   jamais masquée — "elle ne coûte pas de hauteur". Même principe que
   le téléphone, transposé à l'axe vertical : grid-template-rows à 5
   pistes, la 3e réservée au bouton — mesuré (Puppeteer, hasTouch
   émule réellement pointer:coarse, scripts/test-mobile-nav-spacing.js) :
   22px d'espace libre identique de chaque côté, jamais lu dans le CSS. */
@media (width >= 768px) and (pointer: coarse) {
  /* S135 T1 — constat (iPad réel, 11/09) : `.mnav` est déjà `position:
     fixed` (règle de base, ligne ~747) — jamais dépendant du sens de
     .mpage pour sortir du flux. Ce `flex-direction: row` restructurait
     donc à tort .mheader ET .mcontent (les DEUX AUTRES enfants directs
     de .mpage) en RANGÉE horizontale au lieu de la COLONNE verticale
     du téléphone (base .mpage, ligne ~91) — mesuré : .mheader étiré à
     la hauteur PLEINE de l'écran (1194px sur iPad portrait) au lieu de
     sa hauteur propre, son contenu centré verticalement AU MILIEU de
     l'écran (align-items:center hérité, inoffensif tant que la boîte
     restait compacte) plutôt qu'ancré en haut. Retiré : .mpage reste
     en colonne ici comme sur téléphone, .mheader/.mcontent gardent
     leur hauteur propre — seul margin-left (ci-dessous) les décale
     pour dégager la barre latérale fixe. */
  .mnav {
    top: 0; left: 0; bottom: 0;
    display: grid;
    /* S135 T1 — `1fr 1fr var(--track) 1fr 1fr` distribuait les QUATRE
       destinations sur la HAUTEUR DISPONIBLE ENTIÈRE (top:0/bottom:0 —
       jusqu'à 1194px sur iPad portrait) : mesuré, des vides de 260px+
       entre deux destinations consécutives, jamais voulu ("elle ne
       coûte pas de hauteur" ne dit jamais "elle s'étire dans le
       vide"). `auto` : chaque piste ne prend que la hauteur de son
       contenu (44px + padding) ; `align-content: center` (plus bas)
       regroupe les cinq pistes en un seul bloc compact, centré dans la
       hauteur disponible plutôt qu'étalé à travers elle. */
    /* S139 T1 — cinquième destination ("À faire") : une piste "auto" de
       plus après la dernière, jamais insérée avant le bouton vocal
       (les quatre premières gardent leurs lignes EXACTES). */
    grid-template-rows: auto auto var(--mnav-voice-track) auto auto auto;
    align-content: center;
    width: 96px;
    padding-top: calc(24px + env(safe-area-inset-top, 0px));
    border-top: none; border-bottom: none; border-left: none;
    gap: 24px;
  }
  .mnav-item:nth-child(1) { grid-row: 1; }
  .mnav-item:nth-child(2) { grid-row: 2; }
  .mnav-item:nth-child(3) { grid-row: 4; }
  .mnav-item:nth-child(4) { grid-row: 5; }
  .mnav-item:nth-child(5) { grid-row: 6; }
  .mnav-item { flex-direction: column; min-height: 44px; padding: 10px 0; }
  /* S81 — protrusion latérale (bord droit de la colonne) plutôt que
     verticale, même bouton, orientation adaptée à la barre latérale. */
  /* grid-column:1 explicite — même précaution que le téléphone
     ci-dessus (S99), même si une seule colonne existe ici : jamais
     laisser le placement automatique deviner. */
  .mnav-voice-btn { grid-row: 3; grid-column: 1; justify-self: end; align-self: center; transform: translateX(30px); }
  .mheader, .mcontent { margin-left: 96px; }
  /* S135 T1 — borne haute de largeur (constat Arnaud, iPad réel : étiré
     jusqu'à 1194px, "pas plus utile, il devient illisible" — mesuré,
     l'agenda-grid à 7 colonnes passait de 105px/colonne en portrait
     (738px dispo) à 156px/colonne en paysage, sans qu'aucune ligne de
     contenu supplémentaire n'apparaisse). 900px reprend le plafond de
     lisibilité DÉJÀ posé ailleurs dans ce produit pour du contenu à
     lire (cockpit.css, .pairing-warning/.pairing-url) — jamais un
     second seuil arbitraire inventé pour ce seul écran. margin-right
     auto plutôt qu'un centrage : le contenu reste au contact de la
     barre latérale (sens de lecture), l'excédent devient une marge
     silencieuse à droite, jamais un étirement. */
  .mcontent { max-width: 900px; margin-right: auto; padding-bottom: 24px; }
  /* Cibles 44px suffisent à deux mains (brief explicite) — boutons/rangées
     un peu plus compacts qu'au format téléphone. */
  .mbtn, .mrow-card, .msheet-field input, .msheet-field select, .msheet-choice { min-height: 44px; }
  /* Deux colonnes là où c'est pertinent (liste + détail simultanés). */
  .mtablet-columns { display: flex; gap: 24px; align-items: flex-start; }
  .mtablet-columns > .mtablet-col-list { flex: 0 0 340px; }
  .mtablet-columns > .mtablet-col-detail { flex: 1; min-width: 0; }
  /* S67 T5 — câblée pour la première fois (ecrans.html) : le détail est
     l'iframe RÉELLE d'ecran-detail.html, jamais une réimplémentation —
     même page, deux façons de l'atteindre (navigation directe en
     largeur compacte, incluse ici en largeur régulière). */
  .mtablet-col-detail { position: sticky; top: 0; height: calc(100vh - 48px); background: var(--bg-lift); border: 1px solid var(--hair); border-radius: var(--radius-md); overflow: hidden; display: flex; align-items: center; justify-content: center; }
  .mtablet-col-detail iframe { width: 100%; height: 100%; border: 0; }
  .mtablet-col-detail #screensDetailEmpty { margin: 0; }

  /* Pas de barre du bas ici (la latérale ne coûte pas de hauteur) — la
     dégager suffit, pas besoin de lui faire de place en plus. Largeur
     explicite, jamais left+right sur un élément déjà width:100% (voir
     commentaire .mcta-fixed plus haut — même piège, même remède). */
  .mcta-fixed { left: calc(96px + 24px); width: min(420px, calc(100% - 96px - 48px)); }

  /* LOT "agenda mobile" §5 — grille mensuelle : téléphone = liste,
     tablette/bureau = grille, MÊME page, MÊME seuil que ci-dessus. */
  .agenda-rows { display: none; }
  .agenda-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    grid-auto-rows: minmax(96px, 1fr);
    gap: 1px;
    background: var(--hair);
    border: 1px solid var(--hair);
    flex: 1;
    min-height: 0;
    overflow-y: auto;
  }
}

/* S135 T2 — mini-display tablette (web/accueil.html), point d'entrée
   PWA sur tablette : trois colonnes en paysage, deux en portrait,
   "en contenu sélectionnable" (Arnaud, 10/09) — même patron que la
   grille du kiosque (S125/S130, index.html #tabletGrid), jamais un
   second seuil ou une largeur en pixels devinée pour l'équivalent PWA :
   `orientation`, jamais une mesure de largeur en JavaScript (brief
   explicite). Une grille CSS à colonnes, pas "3 cartes vs 2 cartes" —
   les TROIS cartes (agenda, courses, citation+météo) restent toujours
   les mêmes, seul le nombre de colonnes où elles se répartissent change.
   Téléphone : aucune règle ici, empilement vertical par défaut du flux
   normal — "rien ne change" (brief explicite). */
@media (width >= 768px) and (pointer: coarse) and (orientation: landscape) {
  .maccueil-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
}
@media (width >= 768px) and (pointer: coarse) and (orientation: portrait) {
  .maccueil-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; }
}

/* Ligne de liste marquée sélectionnée (rendu deux colonnes) — même
   langage visuel que .msheet-choice.is-on, jamais un second style de
   sélection inventé pour cette seule page. */
.mrow-card.is-selected {
  border-color: var(--gold);
  background: var(--gold-faint);
}
.mrow-card.is-selected .mrow-card-title { color: var(--gold); }

/* Dégagement de bord (brief : cibles à moins de 80px d'un bord → 16px) —
   appliqué aux boutons de tête de page, proches du bord supérieur. */
.mheader { padding-left: max(20px, env(safe-area-inset-left, 0px) + 16px); padding-right: max(20px, env(safe-area-inset-right, 0px) + 16px); }

/* ===========================================================
   S63 point 2 — Agenda mobile, refonte de l'affichage
   ===========================================================
   Colonne de date à GAUCHE, pas d'en-tête de jour pleine largeur : avec
   les jours vides affichés, un mois entier d'en-têtes pleine largeur
   consommerait plus d'un demi-écran en hauteur pure (mesuré, S63) — la
   colonne coûte de la largeur, jamais la hauteur, et la hauteur est le
   budget contraint ici. Groupement visuel d'un jour rétabli par un
   filet (border-bottom), jamais par de l'espace vertical. Aucune
   hauteur de ligne figée en dur nulle part ci-dessous — tout
   dimensionné par le contenu + padding, jamais un height/line-height
   en px. */
/* LOT "agenda mobile" §1 — flex:1 (jamais height:100%) : .agenda-month-nav
   ci-dessous devient un SIBLING au même niveau flex (.mcontent, déjà
   display:flex;flex-direction:column) — height:100% aurait fait
   déborder le total (nav + liste) au-delà de .mcontent, jamais partagé
   l'espace restant correctement. */
/* margin-bottom, jamais padding-bottom (S99, balayage des cibles) — un
   padding-bottom sur un conteneur défilant n'ajoute de l'espace qu'APRÈS
   le dernier élément, révélé seulement en défilant tout en bas ; il ne
   change rien à ce qui est visible au chargement (constaté ici en
   commitant d'abord ce correctif, sans effet mesuré). Un margin-bottom,
   lui, réduit la boîte elle-même (hors flex:1, avant répartition) : la
   liste s'arrête 10px plus tôt, dès l'ouverture. Le bouton vocal central
   flotte au-dessus de la barre de nav (transform: translateY négatif,
   voir .mnav-voice-btn) et empiète volontairement sur la zone de
   contenu ; sans cette réserve, la dernière ligne visible du mois (ex.
   "+ Ajouter un événement" d'un jour vide) ne laissait que ~8px avant
   lui. */
/* overflow-x: hidden — même correctif que .mcontent/.msheet ci-dessus
   (S117 P1) : c'est ELLE le vrai défileur d'agenda.html, jamais
   .mcontent (voir la règle .mcontent-agenda-rows). */
.agenda-rows { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; margin-bottom: 10px; overscroll-behavior-y: contain; /* S124 T3 — même correctif que .mcontent, ce conteneur-ci lui cède la main sur agenda.html */ }

/* LOT "agenda mobile" §1 — navigateur mensuel : la liste affiche
   désormais un mois ENTIER à la fois (remplace le défilement à
   fenêtre glissante de 14 jours, S63) — cette barre nomme le mois et
   reste visible pendant le défilement (SIBLING de #calList, jamais
   à l'intérieur : #calList porte son propre défilement interne,
   .mcontent ne défile plus du tout depuis S63 — la barre est donc
   fixe par construction, sans `position: sticky`). Le libellé du mois
   EST le geste "revenir au mois courant" (brief explicite, "en un
   geste depuis n'importe quel mois") — jamais un bouton séparé de
   plus. */
.agenda-month-nav {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* CORRECTIF (S99, "le contenu prime, le chrome se réduit") —
     10px de haut/bas ne servait qu'à centrer visuellement les flèches
     de 44×44px (cible tactile, non négociable — mesurée ailleurs,
     jamais réduite ici) dans une rangée déjà plus haute qu'elles :
     réduit à 4px, la rangée se resserre SANS jamais toucher la taille
     de la cible tactile elle-même (padding autour, jamais la cible). */
  padding: 4px 20px;
  border-bottom: 1px solid var(--hair);
}
/* CORRECTIF (S99, signalé quatre fois) — color:var(--text-muted) sur
   fond --bg passe déjà, seul, le contraste WCAG 1.4.11 (5.66:1, calculé
   — pas une supposition), donc jamais la vraie cause : c'est la
   COMBINAISON avec un trait fin (2px) et un petit chevron (20px) qui
   rend le signal faible, le geste le plus fréquent de l'agenda restant
   pourtant le plus discret de l'écran. --text-secondary (7.63:1, plus
   net à l'œil que 5.66:1 même si les deux "passent" au sens strict)
   combiné à un trait et un chevron agrandis ensemble — jamais un seul
   des trois isolément, aucun ne suffisait seul. --text-primary/--gold
   restent réservés à l'état pressé/actif ailleurs dans ce fichier, un
   chevron au repos n'est ni l'un ni l'autre. */
.agenda-month-nav-arrow {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: none;
  border: none;
  color: var(--text-secondary);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.agenda-month-nav-arrow:active { color: var(--gold); }
.agenda-month-nav-label {
  flex: 1;
  min-width: 0;
  text-align: center;
  background: none;
  border: none;
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  color: var(--text-primary);
  text-transform: capitalize;
  cursor: pointer;
  min-height: 44px;
  -webkit-appearance: none;
  appearance: none;
}
.agenda-month-nav-label:active { color: var(--gold); }
.agenda-month-nav-label.is-current-month { color: var(--gold); }

/* LOT "affichage immédiat et fonctionnement hors ligne" (S99) —
   indicateur de fraîcheur discret et PERMANENT (jamais masqué). Placé
   sous le navigateur mensuel, au-dessus de la liste. */
.agenda-staleness {
  margin: 0 0 6px;
  padding: 0 2px;
  font-size: 11px;
  font-weight: 400;
  opacity: .5;
  color: var(--text-stale);
}
.agenda-staleness.is-stale { opacity: .75; color: var(--gold); }

/* LOT "agenda mobile" §5 — grille mensuelle : rendu TABLETTE/bureau de
   la MÊME page (jamais une page séparée), masquée sur téléphone. Le
   seuil de bascule reprend TEL QUEL celui déjà établi pour le patron
   liste-détail (mobile.css, "Tablette (pointeur grossier, ≥768px)"),
   jamais un second seuil inventé pour l'agenda. #calList et #calGrid
   coexistent toujours dans le DOM (renderList/renderGrid appelés
   ensemble à chaque chargement) — seul le CSS décide lequel s'affiche,
   jamais une bascule pilotée en JS (résiste nativement à la rotation
   d'écran/au redimensionnement, sans écouteur resize).
   S135 T0/T1 — bogue réel trouvé en reproduisant le défaut 4 d'Arnaud
   (iPad réel, agenda vide) : la règle `.agenda-grid { display: none; }`
   (repli téléphone) vivait ICI, DONC APRÈS la règle tablette qui la
   passe à `display: grid` (ligne ~984, dans le bloc "Tablette
   (pointeur grossier, ≥768px)" plus haut) — même spécificité (une
   seule classe), l'ORDRE DE SOURCE tranche l'égalité, et ce repli,
   plus bas dans le fichier, gagnait aussi SUR tablette. Résultat
   mesuré : matchMedia confirmait bien le contexte tablette, mais
   `getComputedStyle(.agenda-grid).display` rendait quand même "none"
   — la grille ET la liste (masquée à raison, elle, par la même règle
   tablette) invisibles SIMULTANÉMENT, un agenda pourtant rempli
   (31 lignes réelles dans le DOM) rendu comme totalement vide. Ce
   n'était donc PAS un problème de données — jamais reproduit ni
   supposé, mesuré. Corrigé en repositionnant ce repli AVANT son
   override tablette (juste avant ce bloc, avec .agenda-rows) : jamais
   une histoire de spécificité à artificiellement gonfler, seul l'ordre
   de source comptait ici. */
.agenda-grid-weekday {
  padding: 8px 6px;
  text-align: center;
  font-size: 12px;
  color: var(--text-muted);
  background: var(--bg-lift);
  position: sticky;
  top: 0;
  z-index: 1;
}
.agenda-grid-cell {
  min-height: 96px;
  padding: 6px;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  gap: 4px;
  cursor: pointer;
  border: none;
  text-align: left;
  font: inherit;
  color: inherit;
  -webkit-appearance: none;
  appearance: none;
}
.agenda-grid-cell.is-pad { cursor: default; }
.agenda-grid-cell-num { font-size: 13px; color: var(--text-muted); flex-shrink: 0; }
.agenda-grid-cell.today .agenda-grid-cell-num { color: var(--gold); font-weight: 500; }
.agenda-grid-cell-evt {
  font-size: 12px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  background: var(--gold-dim);
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex-shrink: 0;
}

.agenda-row {
  display: flex;
  gap: 12px;
  padding: 10px 20px;
  border-bottom: 1px solid var(--hair);
}
.agenda-row.today .agenda-datecol-day,
.agenda-row.today .agenda-datecol-num { color: var(--gold); font-weight: 500; }

/* Largeur en unités de texte, jamais en pixels fixes (brief explicite :
   Apple signale la troncature en contexte horizontal contraint quand la
   taille de police augmente — Dynamic Type, AX3 à AX5). S81 — devenue un
   <button> (ouvre l'ajout, préremplie de sa date, sur tout jour) : reset
   des styles natifs de bouton, min-width/min-height 44px en PLUS de la
   largeur en em (le texte a beau devenir plus large avec Dynamic Type,
   la cible tactile ne descend jamais sous le plancher). */
.agenda-datecol {
  flex-shrink: 0;
  width: 2.75em;
  min-width: 44px;
  min-height: 44px;
  padding-top: 2px;
  text-align: left;
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.agenda-datecol:active { background: var(--bg-field); border-radius: var(--radius-sm); }
.agenda-datecol-day {
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.agenda-datecol-num {
  font-size: 15px;
  color: var(--text-secondary);
}

.agenda-rowcontent { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }

/* LOT "icônes à la place des pastilles" (S99) — gap et marge élargis :
   brief explicite, "augmente la hauteur d'une ligne pour que l'icône
   respire à côté du titre" (icônes de 28px, contre des pastilles de
   14px avant ce lot). */
.agenda-badges { display: flex; gap: 10px; margin-bottom: 6px; }

/* Ligne d'événement — titre dominant, heure secondaire APRÈS le titre
   (brief explicite : la majorité des événements de cet agenda sont des
   journées entières sans heure — une heure en tête laisserait un trou
   sur la plupart des lignes). 17px/400 = variante 2 retenue sur /diag,
   correspond au corps de texte iOS (Body, 17pt). Heure 13px = Footnote
   iOS. Aucune graisse sous 400 nulle part ici (Apple HIG : "avoid
   Ultralight, Thin, and Light font weights"). */
.agenda-evt { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; }
.agenda-evt-title { font-size: 17px; font-weight: 400; color: var(--text-primary); }
.agenda-evt-time { font-size: 13px; font-weight: 400; color: var(--text-muted); display: inline-flex; align-items: center; gap: 6px; }
.agenda-allday { font-size: 17px; font-weight: 400; color: var(--text-primary); }
/* S66 — un événement local (créé sur Cockpit) est désormais modifiable/
   supprimable au tap ; un événement d'une source ICS externe reste en
   lecture seule (jamais éditable, hors périmètre du produit). Cible
   pleine largeur, padding vertical suffisant pour ≥44px au doigt. */
/* LOT "agenda mobile" §2 — renommée (était .agenda-evt-editable) :
   TOUTE ligne d'événement ouvre désormais sa fiche au tap (local ET
   externe, en lecture seule pour ce dernier) — "éditable" ne décrivait
   plus correctement ce que le tap déclenche. */
.agenda-evt-clickable { cursor: pointer; margin: -6px -4px; padding: 6px 4px; border-radius: var(--radius-sm); min-height: 44px; box-sizing: border-box; }
.agenda-evt-clickable:active { background: var(--bg-field); }

/* Balayage horizontal (modifier/supprimer, ou information en lecture
   seule — S124 T6) — S123 T2/T3, web/assets/js/swipe-actions.js, UN
   SEUL composant partagé (agenda ET activités, §5.8).
   STRUCTURE (S124 T4, corrigée — "corriger la STRUCTURE, pas les
   marges") : `.swipe-shell` (un `<div>` NEUTRE, jamais `row` — un `<a
   href>` en display:flex, cas des activités, n'est pas un bloc de
   positionnement fiable d'un moteur à l'autre pour ses propres enfants
   absolus, constat non reproductible sous chrome-headless-shell mais
   jamais assumé résolu sans ce découplage) porte SEULE le masquage
   (`overflow:hidden`) et le rôle de bloc conteneur (`position:
   relative`) — `border-radius:inherit` copie AUTOMATIQUEMENT
   l'arrondi de la carte, jamais une valeur dupliquée à la main (une
   carte qui changerait de rayon ailleurs ne pourrait plus désynchroniser
   ce masquage). `.swipe-actions` reste DERRIÈRE `.swipe-front`
   (empilement naturel, jamais un z-index compliqué) tant que
   `.swipe-front` ne coulisse pas ; `touch-action: pan-y` laisse le
   NAVIGATEUR trancher verticale (défilement natif, S123 T1 continue de
   le voir) contre horizontale (ce composant) — jamais une heuristique
   JS pour ce qui est déjà un contrat de plateforme standard. */
.swipe-shell { position: relative; overflow: hidden; border-radius: inherit; width: 100%; touch-action: pan-y; }
/* min-height (S127 T1) — jamais un littéral ici : --list-row-min-height
   (socle.css, commentaire complet sur le token) garantit que .swipe-
   actions, dimensionné sur front.offsetHeight (S124 addendum, swipe-
   actions.js), ne tranche plus jamais le bouton de 44px déjà imposé
   par la règle universelle "cible tactile" plus bas dans ce fichier —
   le bouton n'a jamais été trop petit, c'est CE conteneur qui pouvait
   être trop court. Posé sur .swipe-front lui-même (le contenu MESURÉ),
   jamais sur .swipe-shell ni sur `row` — ce sont deux boîtes
   différentes (S125 T3). */
.swipe-front { position: relative; z-index: 1; background: var(--bg); min-height: var(--list-row-min-height); box-sizing: border-box; }
/* .mrow-card (planificateur d'activités) déplace ses enfants DANS
   .swipe-front sans conteneur de mise en page intermédiaire (contenu
   HTML non touché, §2.8, portée additive) — .swipe-front reprend donc
   ICI la mise en page flex de .mrow-card elle-même, jamais dupliquée
   sous un autre nom, seulement portée au bon niveau. */
.mrow-card.swipe-row .swipe-front { display: flex; align-items: center; gap: 14px; width: 100%; background: var(--bg-lift); }
.swipe-actions { position: absolute; top: 0; right: 0; bottom: 0; display: flex; }
/* Largeur au CONTENU, jamais un min-width deviné (S124 T4, défaut
   "Supprimer coupé à droite" : 76px ne correspondait plus au texte
   réel une fois police+padding appliqués) — swipe-actions.js mesure
   `panel.offsetWidth` APRÈS ce rendu, jamais une estimation figée. */
.swipe-action { border: none; color: var(--text-primary); font-size: 14px; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0 20px; white-space: nowrap; }
.swipe-action-edit { background: var(--gold-dim); }
.swipe-action-delete { background: var(--red); }
/* S139 T1 — "Terminer" (to-do) : positif, jamais confondu avec la
   modification (gold) ou la suppression (rouge), un item complété
   n'étant ni modifié ni détruit — il est simplement retiré. */
.swipe-action-complete { background: var(--green); }
/* S124 T6 — événement en lecture seule : information, jamais une
   action (§5.12, rien à toucher qui échouerait). Neutre (ni or ni
   rouge, jamais confondue avec une action). */
.swipe-info { align-items: center; gap: 10px; padding: 0 16px; background: var(--bg-field); color: var(--text-secondary); white-space: nowrap; }
.swipe-info-dot { width: 10px; height: 10px; border-radius: var(--radius-full); flex-shrink: 0; }
.swipe-info-label { font-size: 13px; white-space: nowrap; }

/* Jour vide — affiché, en ligne de hauteur réduite (padding seul, pas de
   hauteur figée), cliquable : ouvre l'ajout d'événement avec la date de
   CETTE ligne préremplie. Un jour vide devient une cible, pas une perte
   de surface. */
.agenda-empty-btn {
  display: block;
  width: 100%;
  text-align: left;
  padding: 4px 0;
  min-height: 44px;
  background: none;
  border: none;
  font: inherit;
  font-size: 14px;
  color: var(--text-muted);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.agenda-empty-btn:active { color: var(--text-secondary); }

.dot { display: inline-block; width: 4px; height: 4px; border-radius: var(--radius-full); flex-shrink: 0; }
.badge-pill { width: 14px; height: 14px; border-radius: var(--radius-full); flex-shrink: 0; position: relative; }

/* CORRECTIF (S99, constat direct d'Arnaud à l'usage sur iPhone : "les
   pastilles de couleur ne fonctionnent pas, il faut aller chercher la
   légende à chaque fois — une couleur sans texte n'est un signal que
   pour celui qui l'a définie") — la collecte porte désormais une ICÔNE
   reconnaissable (event-icons.js, catalogue type -> icône). Taille 28px
   (icône 20px + marge) : la ligne d'événement grandit pour qu'elle
   respire (brief explicite), jamais serrée comme l'ancienne pastille
   de 14px.
   CORRECTIF (S99, "précision au lot en cours" — "les fonds de badge par
   type deviennent une teinte neutre unique, les jetons --waste-* ne
   servent plus au fond des badges") — un fond qui variait PAR TYPE
   (résiduel/papier/pmc) codait encore la NATURE par la couleur, en
   contradiction avec la règle posée juste avant dans la même session
   ("les couleurs ne servent plus qu'aux agendas [sources]") : un même
   fond neutre pour TOUS les badges désormais (collecte ET rappel,
   même traitement — plus de distinction .badge-rappel-icon), la
   silhouette de l'icône seule distingue la nature. --waste-residuel/
   papier/pmc n'ont plus aucun lecteur après ce correctif (vérifié,
   retirés de socle.css). */
.badge-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
  background: var(--hair);
  color: var(--text-primary);
  flex-shrink: 0;
  position: relative;
}
/* CORRECTIF (S99, "les trois signaux", repris ici pour la pastille
   devenue icône) — un halo doré confondait ce signal ("passage
   inhabituel", le plus utile des trois et le plus facile à rater) avec
   le langage visuel déjà utilisé PARTOUT ailleurs pour "sélectionné/
   actif" (.mcolor-swatch.is-on ci-dessous, même halo doré, sens
   totalement différent). Pastille "!" distincte, --red (jamais utilisé
   pour un état neutre dans ce produit), en coin de l'icône —
   reconnaissable même sans lire la légende. */
.badge-icon.unusual::after {
  content: "!";
  position: absolute;
  top: -5px; right: -5px;
  width: 14px; height: 14px;
  border-radius: var(--radius-full);
  background: var(--red);
  color: var(--white);
  font-size: 10px;
  font-weight: 700;
  line-height: 14px;
  text-align: center;
  box-shadow: 0 0 0 1.5px var(--bg);
}
/* LOT "événements matinaux" (S99) — anticipation de la veille : même
   icône, opacité réduite (ce n'est pas encore le jour), jamais
   cliquable comme un événement à part (brief explicite) — juste un
   avant-goût de ce qui arrive. */
.agenda-anticipation {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 6px;
  padding: 8px 4px;
  min-height: 44px;
  box-sizing: border-box;
  opacity: 0.6;
  cursor: pointer;
}
.agenda-anticipation:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.agenda-anticipation-label {
  font-size: 13px;
  color: var(--text-secondary);
}
.agenda-anticipation-tomorrow {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--gold);
  background: var(--gold-faint);
  border-radius: var(--radius-sm);
  padding: 2px 6px;
  flex-shrink: 0;
}

/* LOT "membres du foyer et discrétion d'affichage" — un membre n'est
   pas un compte : un nom, une couleur, choisie dans une palette fixe
   (jamais un sélecteur libre, §1 du brief). */
/* Pastille de choix de couleur (feuille d'édition d'un membre) — cible
   tactile ≥44px (padding compris), anneau doré "sélectionné" (même
   halo que .mcta-fixed/.mnav-item actifs ailleurs dans ce fichier —
   .badge-pill.unusual n'en fait PLUS partie depuis S99, voir plus
   haut : ce halo dit "sélectionné", jamais "exceptionnel"). */
.mcolor-swatch { width: 36px; height: 36px; border-radius: var(--radius-full); border: none; padding: 4px; cursor: pointer; background-clip: content-box; }
.mcolor-swatch.is-on { box-shadow: 0 0 0 3px var(--bg), 0 0 0 5px var(--gold); }

/* Ligne de pastilles NOMMÉES (formulaire d'événement, §2 : "on tape
   dessus pour sélectionner" — jamais un menu déroulant ni une liste à
   cocher dans un sous-écran). Même patron visuel que .msheet-choice
   (déjà établi pour la fréquence de récurrence), avec un point de
   couleur du membre en plus. */
.mmember-pill { display: inline-flex; align-items: center; gap: 8px; padding: 10px 16px; min-height: 44px; box-sizing: border-box;
  border-radius: var(--radius-pill); background: var(--bg-field); border: 1px solid var(--hair); color: var(--text-secondary); font-size: 15px; cursor: pointer; }
.mmember-pill.is-on { border-color: var(--gold-dim); color: var(--text-primary); background: var(--gold-faint); }
.mmember-pill-dot { width: 10px; height: 10px; border-radius: var(--radius-full); flex-shrink: 0; }

/* Légende des signaux (S99) — une ligne = un échantillon RÉEL (mêmes
   classes/mêmes fonctions de rendu que le vrai badge — collecteBadgeHtml/
   rappelBadgeHtml, agenda.html) + son explication en texte, jamais un
   dessin séparé qui pourrait diverger du vrai signal. */
.mlegend-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; font-size: 14px; color: var(--text-secondary); }
.mlegend-row .badge-pill, .mlegend-row .dot { margin: 0 1px; }

/* PATCH "l'écran est chargé — le retour visible se resserre" —
   #quickAddPreview : deux lignes au plus (titre en évidence, reste
   discret dessous), jamais plus. */
.nlp-preview-title { font-size: 15px; color: var(--text-primary); font-weight: 500; }
.nlp-preview-line2 { font-size: 13px; color: var(--text-muted); margin-top: 2px; }

/* LOT "pied de page éditeur" (S99) — discret : petit, atténué, jamais
   en gras, jamais tronqué (une ligne casse en deux plutôt que de
   perdre du texte par ellipse). Placement : DERNIER enfant de
   .mcontent (jamais entre lui et <nav>, même précaution que le bloc
   Version de Réglages) — bénéficie du même correctif de marge que le
   bouton vocal (margin-bottom, pas padding-bottom) : ne passe jamais
   sous la barre ni sous le bouton. */
.cockpit-footer {
  text-align: center;
  font-size: 12px;
  font-weight: 300;
  opacity: .55;
  color: var(--text-muted);
  white-space: normal;
  line-height: 1.4;
  margin: 22px 0 8px;
}
