/* ====================== 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;
}
/* FOND PAR DEFAUT — ajoute le 09/10/2026 (« les boutons blanc et bleu
   sont affreux », Arnaud, capture iPhone). .mbtn seul ne posait que la
   GEOMETRIE : ni background ni color. Un bouton sans variante tombait
   donc sur le rendu NATIF d'iOS, fond blanc et texte bleu, au milieu
   d'une interface sombre. 20 boutons du produit étaient dans ce cas
   (comptés) : « Fermer ce régime », « + Ajouter un rappel », les sept
   « Revenir au défaut » des réglages, etc.
   Ce n'était pas un choix : aucun de ces vingt n'a de raison d'être
   natif. Le défaut devient donc la variante SECONDAIRE, déjà définie
   plus bas et déjà éprouvée ailleurs. Les variantes explicites
   (primary/outline/danger) suivent dans la feuille et gagnent par
   l'ordre de source : aucun bouton déjà style ne bouge.
   agenda-mobile.css ne touche pas .mbtn (vérifié, zéro occurrence) :
   le rendu de référence n'est pas concerné. */
.mbtn {
  background: var(--bg-lift);
  border-color: var(--hair);
  color: var(--text-primary);
}
.mbtn:active { background: var(--bg-field); }

.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);
  /* LOT 14 — CORRECTIF : ce repli (tablette/sans barre du bas, la
     variante téléphone ci-dessous à hauteur réelle de barre reste
     inchangée) additionnait 16px décoratifs à la zone sûre — aucun
     élément à dégager ici, même famille que .app-header (socle.css). */
  bottom: max(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);
  /* LOT 14 — CORRECTIF : 20px de clôture basse additionnés à la zone
     sûre, aucun élément à dégager sous cette feuille (z-index au-dessus
     de tout, y compris la barre du bas) — même famille que .app-header
     (socle.css).
     LOT 15 T3 — depuis Chrome 135, Android bord-à-bord rend
     env(safe-area-inset-bottom) DYNAMIQUE (recalculé pendant l'animation
     de la barre système au défilement) ; l'utiliser en padding-bottom
     provoque un recalcul de mise en page continu (mise en garde
     explicite de Chrome for Developers, guide de migration edge-to-edge).
     safe-area-max-inset-bottom est la variante STATIQUE (borne haute,
     jamais animée) — c'est elle qu'il faut lire pour un espace RÉSERVÉ ;
     repli sur l'inset classique là où safe-area-max-inset-* n'existe pas
     encore (iOS, où l'inset est déjà statique).
     COCKPIT:SAFE-AREA-GOUTTIERE raison="20px = clôture décorative VOULUE
     de cette feuille (voir ci-dessus), jamais réduite à 12px : l'arbitrage
     du 28/09/2026 ne vaut que pour une valeur ADDITIONNÉE à un inset pour
     dégager une barre système, pas pour ce max() (LOT 15 T1/T2)" */
  padding: 24px 20px max(20px, env(safe-area-max-inset-bottom, 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); }
/* LA BORNE DE LARGEUR, SUR TOUT GRAND ÉCRAN (12/10/2026).
   ====================================================================
   CONSTAT D'ARNAUD, répété plusieurs fois avant que je le comprenne :
   « sur pc, toutes les pages, sauf la grille, ont une largeur de
   900px ». La règle existait déjà — .mcontent { max-width: 900px } —
   mais elle vivait dans le bloc @media (width >= 768px) and
   (pointer: coarse). `coarse` veut dire DOIGT. Un PC a une souris,
   donc `pointer: fine` : il ne recevait jamais cette borne et
   retombait dans le régime téléphone, sans plafond, étalé sur 2560 px.
   Mesuré sur sa page Écrans : des cartes de 2 500 px de large pour
   trois lignes de texte.

   C'est la même racine que « à chaque version il y a des problèmes
   d'écrans » : le produit classait les appareils par la façon de les
   TOUCHER, pas par leur TAILLE. Une tablette tactile et un PC à souris
   de même largeur recevaient deux mises en page différentes, et la
   seconde n'était testée nulle part.

   Posé ici, HORS du bloc tactile, donc valable pour les deux : un
   grand écran est un grand écran, qu'on le touche ou qu'on le clique.
   Le bloc tactile garde tout le reste (cibles de 44 px, deux colonnes,
   grille de l'agenda) — ce lot ne déplace QUE la largeur, qui est ce
   qu'Arnaud a demandé.

   CENTRÉ, et pas collé à gauche : 900 px alignés au bord d'un écran de
   2560 laissent 1660 px de vide d'un seul côté. `align-self: center`
   plutôt qu'une marge automatique — la marge auto sur l'axe transverse
   d'un enfant flex court-circuite le `stretch` et fait retomber la
   largeur sur celle du CONTENU (CSS Flexbox §8.1), défaut déjà mesuré
   et corrigé au LOT 19 BIS, qu'on ne réintroduit pas ici. */
@media (width >= 768px) {
  .mcontent {
    width: 100%;
    max-width: var(--app-largeur);
    align-self: center;
  }
  /* L'en-tête suit la même borne, sinon le titre flotte seul à gauche
     au-dessus d'un contenu centré. */
  .mheader { width: 100%; max-width: var(--app-largeur); align-self: center; }
  /* LA BARRE DU BAS SUIT LA MÊME BORNE (12/10/2026, demande d'Arnaud :
     « je veux qu'elle soit à 900px comme le reste »).
     Elle était restée pleine largeur parce qu'elle est `position: fixed`
     avec `left: 0; right: 0` — le `align-self` des deux règles ci-dessus
     ne l'atteint pas, un élément fixé n'étant plus un enfant de flux.
     `margin-inline: auto` est le centrage propre à ce cas : sur un
     élément fixé dont `left` ET `right` valent 0, une largeur contrainte
     laisse un reste que les marges automatiques répartissent à parts
     égales (CSS Positioned Layout, résolution des sur-contraintes).
     Jamais `transform: translateX(-50%)` ici : un transform crée un
     bloc conteneur pour les descendants fixés, et le bouton vocal
     central dépasse justement de cette barre par son propre transform. */
  .mnav { max-width: var(--app-largeur); margin-inline: auto; }

  /* ================================================================
     LA RÈGLE, ET PLUS UNE CORRECTION PAR MORCEAU (12/10/2026)
     ================================================================
     Arnaud, après avoir dû signaler l'en-tête, puis la barre du bas,
     puis la feuille de légende : « 900px partout quand on affiche
     l'app sur pc ».
     Il a raison, et mon tort était de borner les éléments un par un :
     chaque conteneur oublié rouvrait exactement le même défaut, et
     c'est lui qui les trouvait, un par un, à chaque capture.
     Ce qui suit borne TOUT CE QUE L'APP OUVRE PAR-DESSUS elle-même.
     Ces conteneurs échappaient à la borne pour la même raison que la
     barre du bas : ils sont `position: fixed`, donc hors du flux, donc
     insensibles au `align-self` qui borne .mcontent et .mheader.
     Un conteneur de premier niveau ajouté plus tard DOIT être ajouté
     ici — c'est la seule liste, il n'y en a pas d'autre ailleurs. */

  /* La feuille du bas (légende, fiche d'événement, saisie, choix…).
     Le VOILE reste plein écran, c'est son travail d'assombrir tout
     l'écran ; c'est la FEUILLE qui se borne, et l'overlay la centre. */
  .msheet-overlay { justify-content: center; }
  .msheet { max-width: var(--app-largeur); }

  /* Le bouton d'action fixe. Son `width: calc(100% - 40px)` est
     volontaire (voir sa règle de base : un `right` serait ignoré sur un
     élément sur-contraint) — on borne donc la largeur et on recentre
     par la marge, sans toucher à ce calcul. */
  .mcta-fixed {
    max-width: calc(var(--app-largeur) - 40px);
    left: 0; right: 0;
    margin-inline: auto;
  }

  /* La visionneuse de photo, `inset: 0`. */
  .mphoto-detail { max-width: var(--app-largeur); margin-inline: auto; }
}

/* ROUTAGE VOCAL (11/10/2026) — LA QUESTION EN COURS. Elle se lit avant
   de répondre, donc elle est au-dessus du champ et plus grosse que lui :
   un dialogue où la question a la taille d'une légende n'est pas un
   dialogue, c'est un formulaire avec une étiquette. Posée ICI, dans le
   socle, jamais dans agenda.html (règle « CSS agenda intouchable »). */
.msheet-guide-q {
  margin: 0 0 10px;
  font-size: 19px;
  line-height: 1.3;
  color: var(--text-primary);
}
.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);
  /* LOT 15 T3 — espace RÉSERVÉ (padding), pas un élément positionné :
     variante statique d'Android bord-à-bord (safe-area-max-inset-bottom),
     repli sur l'inset classique sur iOS. Voir .msheet ci-dessus pour la
     justification complète. */
  padding-bottom: env(safe-area-max-inset-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(49px - 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à. */
    /* COCKPIT:SAFE-AREA-ADDITIF raison="56px = hauteur réelle de la barre de
       navigation fixe (contenu à dégager), jamais une compensation
       d'encoche — voir commentaire ci-dessus (LOT 14)".
       LOT 15 T1/T2 — RECLASSÉ, pas corrigé : la règle double (WebKit
       max() pour un bord latéral, MDN calc(gouttière + inset) pour un
       DÉGAGEMENT réel sous une barre système) classe ceci en ADDITION,
       confirmé — .mnav est une vraie barre, pas une compensation
       d'encoche. Arbitrage du 28/09 (12px de gouttière) NON appliqué
       ICI : 56px n'est pas une gouttière tâtonnée, c'est la hauteur de
       contenu MESURÉE (grid-template-rows ci-dessus, mnav-item
       min-height:48px, CDP Emulation.setSafeAreaInsetsOverride, S99) —
       la ramener à 12px couperait les icônes/libellés réels et
       découvrirait le contenu de .mcontent sous la barre (S58, "la barre
       ne doit rien cacher"). Écart mesuré par Arnaud sur iPhone (92pt de
       vide sous les libellés contre 34pt d'inset) signalé en BLOQUANT
       dans logs/rapport-lot15.txt — à confirmer avant tout changement de
       la valeur elle-même, pas décidé ici (§2.8, "ce qui fonctionne est
       figé"). */
    /* COCKPIT:SAFE-AREA-ADDITIF raison="56px = hauteur de contenu mesurée
       (voir juste au-dessus), pas réduite à 12px (LOT 15 T1/T2)" */
    height: calc(49px + var(--mnav-gap-bas));
    padding-bottom: var(--mnav-gap-bas);
    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. */
  /* COCKPIT:SAFE-AREA-ADDITIF raison="56px = hauteur réelle de la barre de
     navigation fixe à dégager, jamais une compensation d'encoche (LOT 14)" —
     LOT 15 T1/T2 : reclassé ADDITION (confirmé), valeur 56px non réduite
     à 12px, voir justification complète au bloc .mnav ci-dessus. */
  .mcontent { margin-bottom: calc(49px + var(--mnav-gap-bas)); }
  /* 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. */
  /* COCKPIT:SAFE-AREA-ADDITIF raison="56px barre + 40px protrusion du bouton
     vocal = clairances d'éléments réels à dégager, jamais une compensation
     d'encoche (LOT 14)" — LOT 15 T1/T2/T3 : reclassé ADDITION (confirmé),
     valeurs fixes non réduites (voir bloc .mnav), variante Android
     bord-à-bord appliquée. */
  .mpage:has(.mnav-voice-btn) .mcontent { margin-bottom: calc(49px + var(--mnav-gap-bas) + 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. */
  /* COCKPIT:SAFE-AREA-ADDITIF raison="56px barre + 64px batchBar = clairances
     d'éléments réels à dégager, jamais une compensation d'encoche (LOT 14)" —
     LOT 15 T1/T2/T3 : reclassé ADDITION (confirmé), valeurs fixes non
     réduites (voir bloc .mnav), variante Android bord-à-bord appliquée. */
  .mpage:has(.mcta-fixed:not(.hidden)) .mcontent { margin-bottom: calc(49px + 64px + var(--mnav-gap-bas)); }
  /* COCKPIT:SAFE-AREA-ADDITIF raison="56px = hauteur réelle de la barre de
     navigation fixe à dégager (variante téléphone), jamais une compensation
     d'encoche (LOT 14)" — LOT 15 T1/T2 : reclassé ADDITION (confirmé),
     valeur non réduite (voir bloc .mnav). LOT 15 T3 : élément POSITIONNÉ
     (propriété bottom, pas un espace réservé) — reste sur la variable
     DYNAMIQUE (env(safe-area-inset-bottom) nu), c'est elle qui doit faire
     suivre le bouton, jamais la variante statique max-inset. */
  .mcta-fixed { bottom: calc(49px + var(--mnav-gap-bas-dyn)); }
  /* 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(-34px); }
  /* 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) {
  /* LOT 15 T3 — espace RÉSERVÉ (margin-bottom), variante statique
     Android bord-à-bord, repli inset classique sur iOS. */
  :root.mnav-hidden .mcontent { margin-bottom: env(safe-area-max-inset-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. */
/* LOT 19 BIS — DÉFAUT A (source unique, §5.8 BONNES-PRATIQUES.md) : la
   largeur de la barre latérale tablette (96px) était écrite en dur à
   QUATRE endroits (largeur de .mnav, margin-left des deux rangs sur
   .mheader/.mcontent, .mcta-fixed) et le débordement horizontal du
   bouton vocal (30px) à DEUX (son propre translateX, et le margin-left
   compensatoire qui dégage .mheader/.mcontent sous lui) — deux mesures
   qui ne se parlaient pas : si l'une bougeait, l'autre mentait en
   silence. Ne change AUCUNE valeur, remplace les six littéraux par ces
   deux variables. */
:root { --mnav-sidebar-width: 96px; --mnav-voice-overflow-x: 30px; }

/* LA LARGEUR DE L'APP, écrite UNE fois (12/10/2026). Elle valait 900 en
   littéral dans .mcontent et .mheader ; la demande d'Arnaud l'étend à
   tout ce que l'app ouvre, et six littéraux qui ne se parlent pas sont
   six occasions qu'un seul bouge un jour (même défaut, même remède que
   --mnav-sidebar-width juste au-dessus). */
:root { --app-largeur: 900px; }

/* « RETOUR À L'ÉCRAN » — 12/10/2026, demande d'Arnaud : « il n'y a pas
   de bouton de retour vers le kiosque ».
   Le bouton est monté sur les cinq pages racines par le composant unique
   de mobile-nav.js, mais il n'a de SENS que là où l'écran mural en a un.
   Masqué par défaut, révélé sur grand écran seulement : sur un téléphone
   de 375 px il mènerait à une mise en page conçue pour un mur, c'est-à-
   dire à une commande qui déçoit ce qu'elle promet (§5.14).
   C'est le SPAN qui se masque, pas le bouton : un conteneur vide reste
   un élément flex et laisserait un `gap` de 12 px fantôme dans
   l'en-tête, à côté d'un bouton qui n'y est plus. */
/* Le bouton « retour à l'écran » n'a PAS de règle d'affichage ici, et
   c'est délibéré : la peau premium pose `display: inline-flex !important`
   sur les mounts d'en-tête avec deux ID de spécificité, qu'aucune règle
   d'ici ne peut annuler sans surenchérir — une course que la feuille
   chargée après regagnerait de toute façon. C'est le composant qui
   décide de le monter ou non (mobile-nav.js::mountKiosqueButton). */

@media (width >= 768px) and (pointer: coarse) {
  /* La borne de 900 px posée juste au-dessus vaut pour la barre du BAS.
     Ici la barre est LATÉRALE (top/left/bottom, 96 px de large) : lui
     laisser `margin-inline: auto` la centrerait horizontalement dans
     l'écran, au milieu du contenu. Annulé explicitement plutôt que
     laissé au hasard de l'ordre des règles. */
  .mnav { max-width: none; margin-inline: 0; }
  /* 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: var(--mnav-sidebar-width);
    /* LOT 14 — CORRECTIF : additionnait 24px à la zone sûre haute ; le
       contenu est centré (align-content:center) donc l'effet visuel
       était déjà discret, mais même famille que .app-header
       (socle.css) — aucun élément à dégager au-dessus.
       COCKPIT:SAFE-AREA-GOUTTIERE raison="24px = gouttière décorative
       VOULUE au-dessus d'un contenu centré, jamais réduite à 12px :
       l'arbitrage du 28/09/2026 ne vaut que pour une valeur ADDITIONNÉE
       à un inset pour dégager une barre système, pas pour ce max()
       (LOT 15 T1/T2)" */
    padding-top: max(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(var(--mnav-voice-overflow-x)); }
  .mheader, .mcontent { margin-left: var(--mnav-sidebar-width); }
  /* LOT 19 — le régime téléphone compense déjà la protrusion du bouton
     vocal par :has(.mnav-voice-btn) (voir .mpage:has(.mnav-voice-btn)
     .mcontent plus haut dans ce fichier, axe vertical, +40px) ; ce
     régime tablette ne l'avait jamais eue sur SON axe (horizontal,
     translateX(30px) ci-dessus) — mesuré : le bouton flotte au-dessus
     du contenu, recouvre la grille. Même mécanisme conditionnel,
     transposé : ne s'applique que sur les pages qui rendent réellement
     le bouton (DOM réel, jamais une page sans objet dictable). Plus
     spécifique que la règle non conditionnelle ci-dessus (deux classes
     contre une) : gagne par spécificité, pas par ordre de source. */
  .mpage:has(.mnav-voice-btn) .mheader,
  .mpage:has(.mnav-voice-btn) .mcontent { margin-left: calc(var(--mnav-sidebar-width) + var(--mnav-voice-overflow-x)); }
  /* 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.
     LOT 19 BIS — DÉFAUT B, `margin-right: auto` RETIRÉ. Mesuré par
     scripts/test-tablet-ipad-lot19.js (révélé par le LOT 19, pas
     introduit par lui — avant, la grille masquait le défaut) : en
     portrait, .mcontent restait figé à 409,77px quelle que soit la
     largeur de la fenêtre (834 ou 768px), laissant plus de 290px vides
     à droite. Cause : .mpage (ligne ~87) n'a pas d'align-items propre
     donc `stretch` (défaut) s'applique à ses enfants flex — mais une
     marge automatique sur l'axe transverse d'un enfant flex COURT-
     CIRCUITE ce stretch et le fait retomber sur sa largeur de CONTENU
     (CSS Flexbox §8.1) : le commentaire ci-dessus l'appelait "jamais un
     étirement" en le présentant comme voulu, mais 410px de large sur
     un iPad n'a jamais été l'intention. Un enfant flex sans margin
     auto s'étire déjà par défaut jusqu'à max-width, PUIS s'arrête —
     aucun étirement au-delà de 900px, donc rien à retenir avec une
     marge. En paysage (agenda-grid, contenu intrinsèquement large)
     rien ne change : le contenu shrink-to-fit atteignait déjà le
     plafond de 900px de lui-même, stretch+max-width y arrive pareil. */
  /* LOT 12/10/2026 — `max-width: 900px` RETIRÉ D'ICI : la borne est
     désormais posée pour TOUT grand écran (bloc « LA BORNE DE LARGEUR »
     plus haut), tactile ou non. La garder ici en double aurait gagné
     par ordre de source sur la règle générale, et le contenu serait
     resté non centré sur tablette pendant qu'il l'est sur PC — deux
     mises en page pour une seule intention, exactement ce que ce lot
     supprime. Le dégagement bas, lui, reste propre au tactile. */
  .mcontent { 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).
     CORRECTIF (LOT 23, défaut K) — flex-direction: row EXPLICITE,
     obligatoire : le même élément porte aussi .mcontent (ligne ~243),
     qui pose flex-direction: column pour son propre défilement vertical.
     .mtablet-columns ne redéclarait que display/gap/align-items, jamais
     la direction — display:flex seul ne réinitialise PAS flex-direction
     (CSS Flexbox §8.3), donc la colonne héritée de .mcontent survivait
     et les deux colonnes s'empilaient au lieu de se juxtaposer. Mesuré
     avant correctif : .mtablet-col-list et .mtablet-col-detail au MÊME
     x (116px), largeur de la liste à 404px au lieu des 340px déclarés
     ligne 1132 (flex: 0 0 340px jamais appliqué sur un axe colonne). */
  .mtablet-columns { display: flex; flex-direction: row; 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(var(--mnav-sidebar-width) + 24px); width: min(420px, calc(100% - var(--mnav-sidebar-width) - 48px)); }

  /* LOT "agenda mobile" §5 — grille mensuelle : téléphone = liste,
     tablette/bureau = grille, MÊME page, MÊME seuil que ci-dessus.
     LOT 19 — bascule désormais PAR ORIENTATION (paysage = grille,
     portrait = liste, défaut 3 ci-dessous) : le `display` se décide
     dans les deux blocs orientation qui suivent celui-ci, jamais ici.
     Ces propriétés de mise en page restent posées à ce niveau (tablette
     générale, indépendant de l'orientation) — inertes tant que le
     display de .agenda-grid n'est pas activé par le bloc paysage. */
  .agenda-grid {
    grid-template-columns: repeat(7, 1fr);
    /* CORRECTIF (LOT 19 TER, défaut C) — .agenda-grid-weekday (7 cases,
       renderGrid) est un SIBLING des cases de dates dans la même grille,
       donc sans piste explicite pour lui, `grid-auto-rows` s'appliquait
       AUSSI à sa ligne : l'en-tête de jours héritait des mêmes 96px
       qu'une semaine de dates, laissant une bande vide d'une semaine
       entière avant le 1er du mois (mesuré sur capture réelle, iPad
       paysage — leadingPad/renderGrid restent corrects, AUCUNE case
       n'est décalée, seule la hauteur de la ligne d'en-tête était en
       cause). `grid-template-rows: auto` ne couvre QUE la première
       ligne explicite (l'en-tête, hauteur de son contenu + padding déjà
       posé sur .agenda-grid-weekday) ; `grid-auto-rows` continue de
       régir toutes les lignes implicites suivantes (les semaines de
       dates), strictement inchangées. */
    grid-template-rows: auto;
    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; }
  /* LOT 19 — paysage : grille mensuelle (7 colonnes tiennent l'espace
     disponible, mesuré S135/LOT19 sur iPad réel). DOIT rester APRÈS le
     bloc tablette de base ci-dessus (même sélecteur .agenda-grid, même
     spécificité — une seule classe des deux côtés — donc c'est l'ORDRE
     DE SOURCE qui tranche, exactement le piège consigné au commentaire
     S135 plus bas dans ce fichier ; vérifié, pas supposé). */
  .agenda-grid { display: grid; }
  .agenda-rows { display: none; }
  /* LOT 20 — la grille mensuelle (7 colonnes) ne doit pas subir le
     plafond de largeur de lecture de .mcontent (900px, ligne ~1126,
     pensé pour une LISTE/un paragraphe, jamais pour une grille).
     Mesuré sur iPad réel (Arnaud) : à 1194×834, 1068px sont
     disponibles mais le plafond en bloque 900, laissant 168px vides à
     droite et rétrécissant les colonnes au lieu de les élargir — le
     plafond produisait l'effet inverse de celui recherché.
     :has(.agenda-grid) cible spécifiquement agenda.html (seule page du
     produit qui porte cet élément, vérifié) et seulement dans CE bloc
     paysage : les 15 autres pages tablette, et agenda.html lui-même en
     portrait (liste, .agenda-rows), gardent le plafond de la ligne
     ~1126 inchangé. Spécificité : deux classes (.mcontent + .agenda-grid
     via :has()) contre une (.mcontent seul) — gagne par spécificité,
     même patron que :has(.mnav-voice-btn) plus haut dans ce fichier. */
  .mcontent:has(.agenda-grid) { max-width: none; }
}
@media (width >= 768px) and (pointer: coarse) and (orientation: portrait) {
  .maccueil-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 24px; }
  /* LOT 19 — portrait : liste (grille à 7 colonnes tombe vers 95-105px/
     colonne à cette largeur et tronque les libellés, mesure déjà
     consignée plus bas dans ce fichier). Rien à écrire ici : .agenda-grid
     retombe sur son display:none de base (ligne ~1012, jamais réécrit
     par ce bloc) et .agenda-rows n'est masquée par AUCUNE règle dans ce
     régime, donc reste à son display natif (visible). */
}

/* 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. */
/* COCKPIT:SAFE-AREA-ADDITIF raison="16px = dégagement de cible tactile de
   bord (brief), pas une compensation d'encoche : s'ajoute à l'inset une
   fois le plus grand des deux retenu par le max() extérieur (LOT 14)" */
.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); }
/* LOT 21, défaut F — même gabarit que .agenda-month-nav-arrow ci-dessus
   (44×44px, même repos --text-secondary, même pressé --gold) : un
   contrôle de plus dans la même famille visuelle, jamais un style
   inventé pour lui seul. Placé AVANT la flèche précédente (même ordre
   que l'app Calendrier d'Apple : "Aujourd'hui" à gauche du couple
   précédent/suivant) — pas ajouté APRÈS la flèche suivante, ce qui
   aurait serré le libellé central déjà flex:1. */
.agenda-month-nav-today { 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-today:active { color: var(--gold); }
/* Sur le mois courant, le retour au mois ne sert plus (on y est déjà) —
   seul le retour au DÉFILEMENT (liste scrollée loin d'aujourd'hui)
   reste utile. Teinte atténuée pour ne pas crier un geste partiellement
   redondant, jamais disabled/caché : voir renderMonthNav(), agenda.html,
   le bouton reste actionnable en toute circonstance. */
.agenda-month-nav-today.is-current-month { color: var(--text-muted); }
.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; }
/* RENFORCÉ (LOT 19 TER, défaut E) — même lavis --gold-faint que
   .agenda-row.today ci-dessus, même raison, même sobriété. La case
   garde son fond propre (var(--bg), posé sur .agenda-grid-cell) :
   cette règle, plus spécifique (deux classes), le remplace SEULEMENT
   sur la case d'aujourd'hui. */
/* LOT 21, défaut G — box-shadow ajoutée à la même règle (filet --gold
   sur le bord d'attaque), PLUS SOBREMENT que .agenda-row.today plus bas
   dans ce fichier : pas d'élévation (--shadow) ici — une grille de sept
   colonnes serrées réagirait mal à une ombre portée répétée sept fois
   par semaine (bruit visuel), contrairement à une liste à une ligne par
   jour. */
.agenda-grid-cell.today { background: var(--gold-faint); box-shadow: inset 3px 0 0 var(--gold); }
.agenda-grid-cell.today .agenda-grid-cell-num { color: var(--gold); font-weight: 500; }
/* LOT 24 — pictogramme de garde, REMPLACE la teinte de fond du LOT 22
   (.garde-ici/.garde-ailleurs, retirées : à l'œil les deux teintes à 9%
   d'opacité étaient indistinguables malgré des valeurs CALCULÉES
   différentes — arbitrage d'Arnaud du 02/10, "je préfère un logo bien
   visible"). .agenda-grid-cell-head regroupe le numéro du jour et le(s)
   badge(s) sur une même ligne, en tête de case — remplace l'ancien enfant
   direct unique (.agenda-grid-cell-num) sans changer son style, flex-
   shrink hérité pour ne jamais céder de place aux événements en dessous. */
.agenda-grid-cell-head { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
/* Badges de garde en case — colonnes mesurées entre 128,6 et 151,4px
   (LOT 20) : taille réduite par rapport au 28px par défaut de .badge-icon
   (mobile.css plus bas) pour que DEUX badges (jour de transition) tiennent
   sans réduire la place du contenu ni déborder — même patron de
   réduction contextuelle que cockpit.css::.grid-day-badges .badge-icon
   pour l'écran mural, jamais un second composant.
   LOT 24 BIS, défaut 2 — 16px jugé "trop petit" (constat d'Arnaud sur
   capture réelle). Remonté à 24px, le haut de la fourchette proposée
   (22-24px) : calcul sur la colonne la plus ÉTROITE mesurée (128,6px,
   padding 6px de chaque côté soustrait, .agenda-grid-cell plus haut dans
   ce fichier → 116,6px utiles). Un jour de TRANSITION (le cas le plus
   large) loge le numéro du jour (~14px), un gap de 4px
   (.agenda-grid-cell-head), puis DEUX badges de 24px séparés d'un gap de
   3px (24+3+24=51px) : 14+4+51 = 69px, très en-deçà des 116,6px
   disponibles — aucun débordement, aucune éviction du contenu, preuve
   par scripts/test-garde-rythme.js (assertions de non-débordement déjà
   en place, complétées par une assertion de taille minimale ce lot). */
.agenda-grid-cell-garde { display: flex; gap: 3px; flex-shrink: 0; }
.agenda-grid-cell-garde .badge-icon {
  width: 24px;
  height: 24px;
  font-size: 13px;
  line-height: 24px;
}
/* LOT 28 — marqueur de congé scolaire, vue grille : propre ligne SOUS
   .agenda-grid-cell-head, AVANT la disposition de transition —
   PROPRIÉTÉ DU JOUR, jamais une pastille de plus à côté des badges de
   garde (brief explicite, même raison exacte que .agenda-grid-cell-
   disposition ci-dessous : colonnes mesurées 128,6 à 151,4px, trop
   étroit pour un troisième élément sur la ligne de tête). Icône +
   texte ensemble, jamais une teinte seule (S99/LOT 24 BIS) : var(--text-
   primary) en gras, pas var(--text-muted) comme la disposition — se
   distingue d'elle à l'œil par le poids, pas seulement par la position.
   Tronqué à l'ellipsis plutôt que débordé, même technique que
   .agenda-grid-cell-disposition juste en dessous. */
.agenda-grid-cell-vacances {
  display: flex;
  align-items: center;
  gap: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-primary);
  flex-shrink: 0;
}
.agenda-grid-cell-vacances .vacances-icon { flex-shrink: 0; display: flex; }
/* LOT 25 — disposition de transition, vue grille : propre ligne SOUS
   .agenda-grid-cell-head (jamais à côté des badges sur la même ligne,
   trop étroit — colonnes mesurées 128,6 à 151,4px, § commentaire
   .agenda-grid-cell-garde plus haut). Étirée pleine largeur par le
   défaut align-items:stretch de .agenda-grid-cell (flex-direction:
   column, aucun align-items explicite dessus), tronquée plutôt que
   débordée : "affiche l'essentiel (heure et lieu abrégé)" (brief),
   l'ellipsis EST l'abréviation, même technique que .agenda-grid-cell-evt
   plus haut, jamais réinventée. */
.agenda-grid-cell-disposition {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 11px;
  color: var(--text-muted);
  cursor: pointer;
  flex-shrink: 0;
  -webkit-appearance: none;
  appearance: none;
}
.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;
}
/* LOT 19 — affordance d'ajout : la case est déjà un <button> cliquable
   (agenda.html::renderGrid, aria-label "Ajouter un événement le ..."),
   mais rien ne le signalait visuellement — constat d'Arnaud, abandon
   devant une case crue inerte. Signe permanent, UNIQUEMENT sur une case
   SANS événement (posé par renderGrid) : une case déjà occupée n'a pas
   besoin du rappel, et le signe y encombrerait. Même glyphe SVG "+" que
   le reste du produit (.mheader-add : agenda.html, activites.html,
   ecrans.html, lexique.html, photos.html, connexions.html) — un seul
   socle pour ce geste, jamais un second dessiné pour ce seul écran
   (§5.8). Couleur var(--text-muted), même famille que
   .agenda-grid-cell-num juste au-dessus : discret par construction,
   jamais une couleur inventée pour ce rôle.
   Taille du glyphe (16px) : AUCUN token de taille n'existe dans
   socle.css pour une icône (PIPELINE.md §C le constate déjà : ni
   --font-size-*, ni --space-*, ni --icon-size-* n'existe encore) —
   valeur littérale signalée au rapport plutôt qu'un rôle inventé ici,
   choisie dans l'échelle déjà posée localement par .agenda-grid-cell-num
   (13px) et .agenda-grid-cell-evt (12px) ci-dessus, sans s'y confondre
   au point de devenir illisible en trait fin. */
.agenda-grid-cell-add {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
}

.agenda-row {
  display: flex;
  gap: 12px;
  padding: 4px 20px;
  border-bottom: 1px solid var(--hair);
}
/* RENFORCÉ (LOT 19 TER, défaut E, constat Arnaud sur capture réelle :
   la teinte --gold posée sur les deux seuls libellés de date restait à
   peine perceptible dans une liste dense). Même patron qu'un rôle déjà
   établi ailleurs dans le produit pour "cette ligne se distingue des
   autres" — .mrow-card.is-selected (plus haut, mobile.css) : un lavis
   --gold-faint sur TOUTE la ligne, jamais une couleur inventée pour ce
   rôle. Sobre par construction (alpha .16, même token que l'existant,
   affichage d'ambiance) : un lavis, pas un aplat plein --gold. */
/* RENFORCÉ (LOT 21, défaut G, constat Arnaud : "pas jolie", "quelque
   chose de très beau et de surprenant", sur un aplat --gold-faint jugé
   fade). CONTRAINTE qui prime (brief explicite) : affichage d'ambiance
   sombre, consulté en passant — se voir en une demi-seconde, ne jamais
   crier. Choix du plus discret des deux quand hésitation (brief), donc :
   jamais d'animation, jamais une couleur hors palette, jamais une
   bordure épaisse. Deux renforts ajoutés au lavis déjà en place, tous
   deux pris au socle existant, aucune valeur inventée :
   - un filet de 3px sur le bord d'attaque (inset, donc dans le padding
     existant — ne déplace rien), seule couleur utilisée ailleurs pour
     "ceci est le jour courant" (--gold, même token que plus haut dans
     ce fichier, .agenda-grid-cell.today, .is-current-month) ;
   - une élévation légère au même token --shadow déjà réservé aux
     éléments flottants (socle.css, bouton vocal) : la ligne se détache
     du flux par la profondeur, pas par un à-plat de plus.
   Le texte "Aujourd'hui" (agenda.html::renderList) fait le reste du
   travail : savoir OÙ on est sans avoir à décoder une couleur. */
.agenda-row.today {
  /* LOT ergonomie agenda mobile, défaut 3 — var(--gold-faint) (or à 16 %)
     posé sur le fond bleu nuit (body, var(--bg) #060c18) se compose en
     rgb(37,37,38) : un gris SANS composante bleue, calculé sur les tokens
     du socle, pas une impression. Remplacé par le fond bleu nuit éclairci
     du même socle (--bg-lift #0a1120). Le filet --gold et l'ombre ci-
     dessous restent le signal de « aujourd'hui ». */
  /* LOT 21 TER, défaut I — flex-wrap:wrap vivait sur .agenda-row (toutes
     les lignes) pour loger .agenda-today-label en bandeau pleine largeur ;
     .agenda-empty-btn (width:100%, jour sans événement) en profitait pour
     réclamer sa propre ligne au lieu de rétrécir à côté de la colonne de
     date (capture réelle, 834x1194 : jours 5/7/10 désalignés, ligne
     doublée). Seule la ligne du jour porte .agenda-today-label, donc seule
     elle a besoin du retour à la ligne. */
  flex-wrap: wrap;
  background: var(--bg-lift);
  box-shadow: inset 3px 0 0 var(--gold), 0 1px 5px var(--shadow);
}
.agenda-row.today .agenda-datecol-day,
.agenda-row.today .agenda-datecol-num { color: var(--gold); font-weight: 500; }
/* LOT 21 BIS, défaut G — la colonne de date reste à largeur fixe (2.75em,
   plus bas dans ce fichier), IDENTIQUE pour toutes les lignes y compris
   aujourd'hui (agenda.html::renderList ne lui donne plus jamais
   "Aujourd'hui", seulement l'abréviation à 3 lettres) : plus aucun
   contenu spécial à faire tenir dedans, donc plus de renvoi à la ligne à
   y forcer. Le renvoi forcé tenté ici au LOT 21 (white-space:normal +
   overflow-wrap + font-size:9px) rendait "Aujourd'hui" en TROIS
   fragments empilés et illisibles sur capture réelle — règle retirée. */
/* Bandeau pleine largeur, AU-DESSUS de la colonne de date et du contenu
   (agenda.html::renderList, un seul enfant .agenda-today-label ajouté en
   tête du DOM, SEULEMENT sur la ligne d'aujourd'hui) : flex-basis:100%
   le force sur sa propre ligne grâce à flex-wrap:wrap, posé UNIQUEMENT
   sur .agenda-row.today (LOT 21 TER, défaut I — ci-dessus), jamais sur
   .agenda-row générique : une ligne ordinaire wrapperait aussi sinon, et
   .agenda-empty-btn (width:100%, jour sans événement) réclamerait sa
   propre ligne au lieu de rétrécir à côté de la colonne de date. Même
   couleur --gold que le reste du renforcement LOT 21 (filet, teinte des
   libellés de la colonne) : un seul signal visuel cohérent, pas une
   teinte inventée pour ce bandeau. nowrap + ellipsis en garde-fou
   seulement (largeur disponible ici très supérieure au mot, jamais
   atteinte en usage réel aux formats testés) — jamais une troncature
   voulue. */
.agenda-today-label {
  flex: 1 0 100%;
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  font-weight: 500;
  color: var(--gold);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 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);
}

/* LOT « balises de disponibilité » — la vignette sous la date. Brief
   d'Arnaud du 08/10, mot pour mot : « une petite vignette sous la date
   exemple "8-18" ou encore "6 - 21" ». DANS la colonne de date, jamais
   dans le contenu de la ligne : propriété du JOUR, même raison que le
   pictogramme de garde du LOT 24.
   CONTRAINTE DURE, mesurée : .agenda-datecol fait 2.75em / 44px minimum
   et cette largeur ne change PAS (le LOT 21 BIS a déjà payé ce défaut —
   élargir la colonne désaligne verticalement toute la liste). La
   vignette s'inscrit donc dans cette largeur, et le texte lui-même est
   déjà compact côté serveur (api/dispo-moteur.js::libelleVignette rend
   "8-18" quand les heures sont pleines, "8h45-16h30" seulement quand il
   y a des minutes). Le cas long passe à la ligne plutôt que de déborder
   ou d'être tronqué : une balise coupée mentirait sur l'heure, un
   retour à la ligne non. */
.agenda-dispo-vignette {
  margin-top: 2px;
  /* Place RÉSERVÉE même sans texte — mesuré au laboratoire : sans ça,
     une ligne de jour vide passe de 62 à 68 px selon qu'elle porte une
     balise ou non, et la liste ondule. Vide, le div garde sa hauteur. */
  min-height: 10px;
  max-width: 100%;
  font-size: 9px;
  font-weight: 600;
  line-height: 10px;
  letter-spacing: -.01em;
  color: var(--text-muted);
  overflow-wrap: break-word;
}

/* LOT 24 — pictogramme de garde, vue liste : à côté de la colonne de
   date, jamais mélangé au contenu de la ligne (.agenda-rowcontent
   ci-dessous, réservé aux événements) — propriété du JOUR, pas de
   l'événement (brief explicite). Sibling flex, largeur nulle quand absent
   (agenda.html ne l'ajoute qu'en présence d'un régime actif ce jour-là) :
   n'élargit jamais les lignes sans régime. Taille par défaut de
   .badge-icon (28px, mobile.css plus bas) — aucune réduction ici,
   contrairement à la grille, la ligne a la largeur disponible. */
.agenda-garde-badges { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
/* EMPILES A GAUCHE DU BLOC DATE (09/10/2026, dessin d'Arnaud : deux
   ronds l'un sous l'autre, dans la marge, a hauteur du quantieme).
   Premiere version du meme jour : en tete de colonne, en rangee. Elle
   ne correspondait pas au dessin et coutait 28px de hauteur par ligne.

   POURQUOI EN ABSOLU ET PAS EN RANGEE [pile][date], qui aurait ete la
   facon naturelle — deux murs, tous deux verifies avant d'ecrire :
     1. agenda-mobile.css force flex-direction:column sur
        .agenda-datecol avec une specificite superieure. Ce fichier est
        INTOUCHABLE (regle d'Arnaud du 08/10 : les vues Liste/Jour sont
        le rendu de reference dont les autres pages s'inspirent).
     2. la largeur de .agenda-datecol ne change PAS — contrainte dure
        deja documentee et deja PAYEE juste au-dessus : "le LOT 21 BIS a
        deja paye ce defaut, elargir la colonne desaligne verticalement
        toute la liste".
   Un positionnement absolu ne touche ni l'une ni l'autre : la colonne
   garde sa largeur, son flux et son alignement.

   MESURE (iPhone 390, regime semaine alternee, jour de passage inclus) :
   badge de x=7 a x=27, texte de date a x=31 — 4px d'ecart, aucun
   chevauchement, jamais hors ecran (la marge est de 15px, le reste est
   pris sur le blanc a gauche du quantieme centre). Hauteur de ligne
   revenue a 68px : les badges ne coutent plus rien en hauteur. */
.agenda-datecol { position: relative; }
.agenda-datecol .agenda-garde-badges {
  position: absolute;
  right: calc(100% - 12px);
  /* ANCRÉ EN HAUT, jamais centré — défaut constaté par Arnaud sur son
     appareil le 09/10 : avec top:50% la pile se centrait sur la hauteur
     de .agenda-datecol, qui s'étire à la hauteur de TOUTE la ligne.
     Sur un jour à trois cartes, le badge se retrouvait des dizaines de
     pixels sous sa date, parfois en face de la ligne suivante.
     Mon laboratoire ne l'avait pas montré : mes journées d'essai
     portaient au plus un événement, donc des lignes courtes où centre
     et haut se confondent. Le jeu d'essai a été corrigé en même temps
     que la règle.
     4px, pas 2 : cale la première pastille sur le haut du jour de la
     semaine plutôt que sur le bord de la colonne. */
  top: 4px;
  flex-direction: column;
  width: auto;
  gap: 3px;
  margin: 0;
}
.agenda-datecol .garde-badge { width: 20px; height: 20px; font-size: 11px; line-height: 20px; }
/* HAUTE VISIBILITE : socle.css passe .garde-badge a 44px, qui ne tient
   plus dans la marge — mesure : le badge sortait de l'ecran a x=-17.
   Il retourne donc EN FLUX, au-dessus de la date, ou il tient (x=15).
   La haute visibilite choisit la taille, pas la compacite ; c'est le
   meme arbitrage qu'elle fait deja partout ailleurs. L'ordre DOM place
   deja les badges en premier (agenda.html), ce repli n'a donc rien a
   reordonner. */
:root[data-contrast="haute-visibilite"] .agenda-datecol .agenda-garde-badges {
  position: static;
  flex-direction: row;
  justify-content: center;
  width: 100%;
  margin-bottom: 2px;
}

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

/* LOT 28 — marqueur de congé scolaire, vue liste : PREMIER enfant de
   .agenda-rowcontent (flex-column, min-width:0 déjà posé ci-dessus),
   jamais à côté de .agenda-garde-badges (brief explicite : propriété du
   JOUR, pas une pastille de plus à côté de celles de la garde). Icône +
   texte ensemble (S99 : une couleur seule ne porte jamais l'information),
   var(--text-primary) en gras — se distingue de la disposition
   (var(--text-muted), ci-dessous) par le poids autant que par la
   position, jamais par la seule teinte. Tronqué à l'ellipsis si le
   libellé traduit dépasse la largeur disponible (locale la plus longue),
   même technique que .agenda-garde-disposition juste en dessous. */
.agenda-vacances-marque {
  display: flex;
  align-items: center;
  gap: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
}
.agenda-vacances-marque .vacances-icon { flex-shrink: 0; display: flex; }

/* LOT 25 — disposition de transition (heure/lieu), vue liste : rangée
   dans .agenda-rowcontent (flex-column, min-width:0 DÉJÀ posé ci-dessus
   pour les événements) plutôt qu'à côté de .agenda-garde-badges (flex-
   shrink:0, largeur de contenu jamais contrainte) — une adresse longue
   aurait sinon poussé .agenda-rowcontent hors de la ligne au lieu de se
   tronquer. Bouton (cible tactile, ouvre la feuille de disposition),
   réinitialisé à plat : même sobriété que .agenda-empty-btn plus bas,
   jamais un style de bouton par défaut du navigateur. */
.agenda-garde-disposition {
  display: block;
  width: 100%;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 13px;
  color: var(--text-muted);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* 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); }
/* LOT ergonomie agenda mobile, défaut B — plancher de la ligne d'événement,
   LOCAL à l'agenda : .swipe-front reçoit 44px (cible tactile) au lieu des
   48px du token partagé --list-row-min-height (socle.css), qui reste intact
   pour les autres listes. Un titre (17px, ligne de 20px) tient dans 44px ;
   une ligne à badges ou à titre long reste dictée par son contenu. */
.agenda-row .swipe-front { min-height: 44px; }

/* 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); }
/* LOT ergonomie agenda mobile, défaut A — jour vide. La hauteur d'une ligne
   vide est fixée par SES DEUX cibles tactiles (colonne de date
   .agenda-datecol et « + Ajouter un événement » .agenda-empty-btn, chacune
   min-height 44px), pas par le padding : 4px haut + 4px bas ajoutaient 8px
   sans rien porter. Retiré sur les seules lignes vides ; la cible à 44px
   reste intouchable. */
.agenda-row:has(> .agenda-empty-btn) { padding-top: 0; padding-bottom: 0; }

.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;
}
/* LOT 24 — pictogramme de garde : une INITIALE plutôt qu'une icône SVG,
   seul ajout sur .badge-icon (texte au lieu d'un <svg>, même boîte, même
   fond neutre — S99 : "une couleur sans texte n'est un signal que pour
   celui qui l'a définie", donc jamais de fond distinct par côté ici non
   plus). line-height calée sur la hauteur de la boîte (28px par défaut)
   pour centrer verticalement un caractère de texte, ce que .badge-icon
   seul n'a jamais eu besoin de faire pour une icône SVG déjà centrée par
   son propre viewBox. */
/* ROND, pas un carre arrondi — demande d'Arnaud du 09/10/2026. Seule
   propriete ajoutee a .badge-icon ici : border-radius. Le reste des
   badges (collecte, rappel) garde var(--radius-sm) du socle, c'est
   justement ce qui distingue le signal de GARDE des autres d'un coup
   d'oeil. S'applique aussi a la vue grille (24px) et a la haute
   visibilite (44px) : meme classe, un seul endroit a tenir. */
.garde-badge { font-size: 13px; font-weight: 700; line-height: 28px; border-radius: 50%; }
/* LOT 24 BIS, défaut 2 — constat d'Arnaud sur capture réelle : un petit
   rond gris (--hair, le fond neutre PAR DÉFAUT de .badge-icon) sur fond
   sombre ne "saute pas aux yeux", contrairement à la demande explicite
   ("je préfère un logo bien visible"). Le fond porte désormais la couleur
   du FOYER (--garde-ici/--garde-ailleurs, socle.css — contraste et
   doctrine S99 documentés à cet endroit), la lettre reste le porteur
   PRIMAIRE du sens (jamais l'inverse) : --white dessus, pas --text-
   primary (rôle déjà nommé au socle, "texte/icône plein blanc sur fond
   saturé"). Sélecteur d'ATTRIBUT, jamais une seconde classe pour ne pas
   dupliquer data-garde-cote déjà posé par agenda.html::gardeBadgeHtml. */
/* RELIEF (09/10/2026, « essaie de faire des badges un peu plus 3D, je
   voudrais soigner cette UI »). Trois couches, et pas une de plus —
   c'est ce qui fait un relief credible sans tomber dans le skeuomorphe :
     1. un DÉGRADÉ vertical clair -> foncé : la lumière vient du haut,
        comme partout ailleurs dans cette interface ;
     2. un LISERÉ CLAIR en haut, en inset — l'arête que la lumière
        accroche. Il fait double emploi, et c'est voulu : les teintes
        franches ne font que 2,9:1 contre le fond sombre de la liste,
        sous les 3:1 d'un composant ; ce liseré porte le bord, mesuré à
        6,29:1 (rose) et 7,00:1 (bleu) contre #0b1b36 ;
     3. une OMBRE PORTÉE courte et basse, qui decolle la pastille.
   Le texte garde une ombre d'un pixel : sans elle, une lettre pleine
   blanche sur un degrade sature flotte au lieu de s'y poser. */
.garde-badge {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .45),
    inset 0 -1px 0 rgba(0, 0, 0, .25),
    0 1px 3px rgba(0, 0, 0, .38);
  text-shadow: 0 1px 1px rgba(0, 0, 0, .35);
}
.garde-badge[data-garde-cote="ici"] {
  background: linear-gradient(180deg, var(--garde-ici) 0%, var(--garde-ici-bas) 100%);
  color: var(--white);
}
.garde-badge[data-garde-cote="ailleurs"] {
  background: linear-gradient(180deg, var(--garde-ailleurs) 0%, var(--garde-ailleurs-bas) 100%);
  color: var(--white);
}
/* HAUTE VISIBILITÉ : aucun relief. Le mode retire déjà toute teinte
   (socle.css : blanc et gris clair, initiale noire) et le contraste y
   vient de la forme, pas de la lumière. Un liseré translucide et une
   ombre ne feraient qu'abaisser le contraste qu'on vient d'y mettre. */
:root[data-contrast="haute-visibilite"] .garde-badge {
  box-shadow: none;
  text-shadow: none;
}
/* 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, refondue le 16/10/2026) — une ligne = un
   échantillon RÉEL (mêmes classes, mêmes fonctions de rendu que le vrai
   badge : collecteBadgeHtml / rappelBadgeHtml / gardeBadgeHtml,
   agenda.html) + son explication, jamais un dessin séparé qui pourrait
   diverger du vrai signal.

   DEUX LIGNES PAR SIGNAL (arbitrage du 16/10) : le mot d'abord, au plus
   simple, et la limite en dessous, plus discrète. D'où align-items en
   flex-start — centré, l'échantillon flottait entre les deux lignes au
   lieu de se poser en face du mot. */
.mlegend-row { display: flex; align-items: flex-start; gap: 12px; padding: 10px 0; font-size: 14px; color: var(--text-secondary); }
.mlegend-row .badge-pill, .mlegend-row .dot { margin: 0 1px; }
/* Deux badges qui se lisent comme UNE paire (jour de passage de la
   garde) : sans cette enveloppe, le gap de 12px de .mlegend-row les
   ecartait comme deux signaux sans rapport. */
.mlegend-paire { display: inline-flex; align-items: center; gap: 4px; }

/* Gouttière FIXE pour l'échantillon : sans elle, les mots commençaient
   à une abscisse différente selon la largeur du badge (une carte
   miniature est plus large qu'une pastille de garde) et la colonne de
   texte ondulait d'une ligne à l'autre. */
.mlegend-exemple { flex: 0 0 72px; display: inline-flex; align-items: center; justify-content: flex-start; min-height: 28px; }
.mlegend-texte { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.mlegend-mot { color: var(--text-primary); font-size: 14px; line-height: 1.35; }
.mlegend-limite { color: var(--text-muted); font-size: 12px; line-height: 1.35; }
/* Le geste du bloc 2 : doré et discret, il dit qu'on peut appuyer sans
   imiter un bouton (la ligne ENTIÈRE est la cible, pas ce texte). */
.mlegend-geste { color: var(--gold); font-size: 12px; margin-top: 2px; }

/* Titres : le titre de BLOC (les deux grandes parties) se distingue du
   titre de SECTION (une famille de signaux dedans), sinon la feuille se
   lit comme une seule liste plate. */
.mlegend-bloc-titre { margin: 22px 0 2px; color: var(--text-primary); font-size: 13px; letter-spacing: .02em; text-transform: uppercase; }
.mlegend-bloc-titre:first-of-type { margin-top: 10px; }
.mlegend-section { margin: 14px 0 0; }
.mlegend-eteint-note { margin: 0 0 4px 84px; font-size: 12px; }

/* Les lignes du bloc 2 sont de vrais <button> (arbitrage : cliquables).
   Remise à plat de l'apparence native, cible tactile de 44 px tenue —
   le balayage des cibles (scripts/test-touch-targets-sweep.js) les
   compte comme des boutons, et il a raison de le faire. */
.mlegend-row-action { width: 100%; text-align: left; background: none; border: 0; min-height: 44px; box-sizing: border-box; cursor: pointer; font: inherit; color: inherit; }
.mlegend-row-action:active { opacity: .7; }

/* Le troisième état des balises : déclaré par le foyer mais affichage
   éteint. Grisé, PAS masqué — c'est son réglage, et le voir grisé le lui
   rappelle (arbitrage du 16/10). */
.mlegend-row.est-eteint { opacity: .45; }

/* LA CARTE MINIATURE (arbitrage : « la carte, pas la pastille »).
   Le dégradé et le liseré reprennent la recette de la vraie carte
   (#mainView .agenda-evt : linear-gradient 105deg + liseré de 2px à
   gauche). Les COULEURS, elles, ne sont pas recopiées : elles sont lues
   à l'exécution sur la palette réelle et posées en style inline
   (legPaletteDeFamille, agenda.html) — agenda-mobile.css reste la seule
   source des six familles. */
/* La recette (dégradé 105deg + liseré de 2px) vit ICI, les VALEURS
   arrivent en variables depuis legCarteMiniature. Deux raisons : le
   ledger de dette i18n interdit d'ajouter une chaîne à agenda.html (une
   déclaration CSS assemblée en JS y est comptée comme du texte), et une
   recette dans une feuille de style se relit mieux qu'une concaténation. */
.mlegend-carte { position: relative; display: inline-flex; align-items: center; width: 44px; height: 28px; border-radius: var(--radius-sm); border: 1px solid var(--mlegend-bord, var(--hair)); background: linear-gradient(105deg, var(--mlegend-a, rgba(36,48,65,.60)), var(--mlegend-b, rgba(18,28,43,.55))); overflow: hidden; }
.mlegend-carte::before { content: ""; position: absolute; left: 0; top: 5px; bottom: 5px; width: 2px; border-radius: 2px; background: var(--mlegend-liseret, var(--gold)); opacity: .9; }
.mlegend-carte-bulle { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin-left: 9px; border-radius: var(--radius-full); background: rgba(255,255,255,.07); color: var(--mlegend-icone, var(--text-primary)); }
.mlegend-carte-bulle svg { width: 14px; height: 14px; fill: none; stroke: currentColor; }

/* Échantillons qui ne sont pas des badges : une icône nue (nature d'un
   rendez-vous, congé scolaire, rappel d'école) et trois échantillons de
   texte (la balise d'heures, la puce « Demain », la pastille de
   disposition). Chacun reprend l'aspect de son vrai rendu, jamais une
   invention pour la légende. */
.mlegend-nature { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; color: var(--text-primary); }
.mlegend-nature svg { width: 18px; height: 18px; fill: none; stroke: currentColor; }
/* En SÉRIE dans la gouttière (aperçu des natures de rendez-vous) : plus petites et resserrées, sinon la série déborde et décale tout le texte de la ligne. */
.mlegend-exemple .mlegend-paire .mlegend-nature { width: 16px; height: 16px; }
.mlegend-exemple .mlegend-paire .mlegend-nature svg { width: 16px; height: 16px; }
.mlegend-balise { display: inline-flex; align-items: center; font-size: 11px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.mlegend-demain { display: inline-flex; align-items: center; padding: 2px 7px; border-radius: var(--radius-pill); background: var(--hair); font-size: 11px; color: var(--text-secondary); }
.mlegend-disposition { display: inline-flex; align-items: center; font-size: 11px; color: var(--text-secondary); white-space: nowrap; }

/* Le pied de légende : séparé par un trait, parce que ce n'est PAS un
   signal (voir le motif écrit dans le gabarit de #agendaLegendSheet). */
.mlegend-pied { margin-top: 20px; padding-top: 12px; border-top: 1px solid var(--hair); }

/* L'étiquette du premier lancement, à côté du bouton information de
   l'en-tête. Retirée du DOM dès que la légende a été ouverte une fois,
   donc aucune règle d'extinction ici. */


/* 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;
}

/* DEGAGEMENT BAS DE LA BARRE DE NAVIGATION - decision Arnaud 07/10/2026.
   Demande litterale : "quand l'app est epinglee, le menu de bas de page doit
   se trouver le plus bas possible. dans l'app ouverte dans safari il faut
   conserver un espace pour les commandes de ios".
   env() seul fait EXACTEMENT L'INVERSE : il vaut 34px en standalone (zone de
   l'indicateur d'accueil) et 0 dans Safari (ou la barre d'outils du navigateur
   occupe deja le bas). Le degagement est donc pilote par le MODE D'AFFICHAGE,
   jamais par le seul inset.
   Variante -dyn : .mcta-fixed suit la variante DYNAMIQUE de l'inset (voir son
   propre commentaire), elle garde donc sa propre expression.
   ECART ASSUME avec les regles Apple, qui reservent la zone entiere : en
   epingle les cibles descendent a 4px du bord, donc dans la zone du geste de
   retour a l'accueil. A remonter si un appui bas se fait intercepter. */
:root {
  /* MESURE DU 07/10/2026 (Chromium, CDP setSafeAreaInsetsOverride,
     quatre configurations : Safari inset 0, Safari inset 34, PWA
     inset 34, PWA inset 0) — la formule precedente etait un PIEGE,
     et c'est elle qui produisait des chiffres differents d'une
     iteration a l'autre depuis le 03/10 :

       env(safe-area-max-inset-bottom, env(safe-area-inset-bottom, 0px))

     Un repli env() ne sert QUE si le navigateur ignore la variable.
     Chrome >= 135 CONNAIT safe-area-max-inset-bottom et la donne a
     0px hors mode bord-a-bord : le repli n'est alors JAMAIS lu, le
     degagement tombe a 16px. Safari ne la connait pas : le repli
     s'applique, l'inset vaut 34pt, le degagement tombe a 0px. Un
     seul CSS, deux geometries opposees — mesure : 65px de barre sur
     l'un, 49px sur l'autre, libelle a 21px du bas ici et 5px la.

     Le degagement n'a aucune raison de suivre l'inset. L'inset dit
     ou commence la zone sure ; .mnav est posee en bottom:0 et sa
     hauteur est MESUREE (49px de contenu). Ce qu'il faut sous les
     libelles, c'est de quoi passer au-dessus de l'indicateur
     d'accueil iOS (5px de haut, 8px au-dessus du bord) : une
     distance ABSOLUE, identique partout. 12px = l'arbitrage
     d'Arnaud du 28/09, pose puis jamais applique — c'est de la que
     venait le tatonnement. Mesure apres : 61px de barre et libelle
     a 17px du bas, a l'identique en Safari, en PWA epinglee et sur
     Android. Une seule geometrie, plus rien a reconcilier. */
  --mnav-gap-bas: 12px;
  --mnav-gap-bas-dyn: 12px;
}
/* Plus aucune distinction navigateur / app epinglee : la valeur ne
   depend plus de l'environnement, donc les deux rendus convergent.
   Le bloc @media (display-mode: standalone) qui descendait a 4px est
   retire — il ne reglait qu'une moitie du probleme et masquait
   l'autre. */
