/* MSG-02 (#742) — la messagerie 1-à-1 : « Messages » (P21-01, la liste des fils), « La conversation »
   (P21-02, le fil et le composer) et « le mot envoyé dans l'app » (P21-03). Écrans du catalogue /dev/ecrans
   qui SONT les maquettes du paquet P21 ; MSG-06 / MSG-07 / MSG-09 les déplacent au branchement et gardent
   cette feuille. Couleurs, rayons et ombres par les jetons seulement — jamais une valeur en dur. */

/* ------------------------------------------------------------------ */
/* 1. La liste des fils (components/conversation_row)                   */
/* ------------------------------------------------------------------ */
.msg-thread { display: flex; align-items: center; gap: .7rem; padding: .75rem 0; border-bottom: 1px dashed var(--brand-line-soft); color: inherit; }
.msg-thread:last-child { border-bottom: 0; }
.msg-thread .msg-text { flex: 1; min-width: 0; }
/* Le prénom ne se tronque jamais : il se replie ; la relation passe à la ligne si la place manque. */
.msg-thread .msg-name { display: flex; flex-wrap: wrap; align-items: baseline; gap: .1rem .45rem; font-size: .88rem; color: var(--brand-secondary); }
.msg-thread .msg-name b { overflow-wrap: anywhere; }
.msg-rel { font-size: .66rem; font-weight: 700; color: var(--brand-muted); }
/* Le dernier message, lui, tient sur UNE ligne. */
.msg-thread .msg-last { display: block; margin-top: .15rem; font-size: .76rem; color: var(--brand-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.msg-thread .msg-side { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: .3rem; }
.msg-thread .msg-when { font-size: .62rem; color: var(--brand-muted); white-space: nowrap; }
.msg-thread.is-unread .msg-last { color: var(--brand-text); font-weight: 700; }
.msg-thread.is-unread .msg-when { color: var(--brand-accent-deep); font-weight: 800; }
.msg-dot { min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px; background: var(--brand-accent); color: var(--brand-secondary); font-size: .62rem; font-weight: 800; line-height: 20px; text-align: center; }
/* L'état d'un fil (MSG-11) : « en silence » chez celle qui a bloqué, « fermé » pour une relation cessée — ou bloquée par l'autre, sans le dire */
.msg-state { font-size: .6rem; font-weight: 800; color: var(--brand-muted); white-space: nowrap; }
/* Les fils en silence de « Mes données » (MSG-11) : une ligne, l'avatar, le prénom, « Rouvrir » */
.msg-silenced { display: flex; align-items: center; gap: .6rem; padding: .5rem 0; border-bottom: 1px dashed var(--brand-line-soft); }
.msg-silenced:last-child { border-bottom: 0; }
.msg-silenced .msg-text { flex: 1; min-width: 0; display: flex; flex-direction: column; font-size: .84rem; }
.msg-silenced .msg-text span { font-size: .66rem; font-weight: 700; color: var(--brand-muted); }
/* La demande de contact en tête du fil (MSG-13, porte 2) : qui, en une ligne avec sa photo, puis les deux gestes */
.msg-request { margin-bottom: 1rem; }
.msg-request-who { display: flex; align-items: flex-start; gap: .7rem; margin-bottom: .75rem; font-size: .9rem; color: var(--brand-secondary); }
.msg-request-who b { overflow-wrap: anywhere; }
.msg-request-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
/* La phrase qui dit ce que le message va faire (demande de contact, acceptation), juste au-dessus de la zone de saisie */
.msg-notice { margin: 0 0 .5rem; }
/* Un lien dans une phrase d'information grise se lit comme un lien, pas comme un mot de plus
   (retour d'Axel, 29/09 — recette #934 : « je n'ai aucun bouton » devant « En savoir plus »). */
.msg-notice a { text-decoration: underline; text-underline-offset: .15em; }

/* ------------------------------------------------------------------ */
/* 2. La feuille « À qui ? » (groupes : mes filleules / ma marraine / mes coureuses) */
/* ------------------------------------------------------------------ */
.msg-group { font-size: .62rem; letter-spacing: .12em; text-transform: uppercase; font-weight: 800; color: var(--brand-muted); margin: .9rem 0 .25rem; }
.msg-group:first-child { margin-top: 0; }
/* La note d'un groupe (MSG-13) : ce qu'une première demande de contact veut dire, ou le plafond atteint */
.msg-group-note { margin: -.1rem 0 .35rem; line-height: 1.35; }
.msg-pick { display: flex; align-items: center; gap: .6rem; padding: .5rem 0; color: inherit; border-bottom: 1px dashed var(--brand-line-soft); }
.msg-pick:last-child { border-bottom: 0; }
.msg-pick .msg-text { flex: 1; min-width: 0; }
.msg-pick b { display: block; font-size: .84rem; color: var(--brand-secondary); overflow-wrap: anywhere; }
.msg-pick span { font-size: .68rem; color: var(--brand-muted); }
.msg-pick .msg-go { color: var(--brand-muted); font-size: 1.1rem; }
/* #931 : un groupe long se replie (huit personnes, puis « Voir les … ») ; une grande branche se cherche. */
.msg-group-summary { padding: .35rem 0 .5rem; line-height: 1.35; }
/* Le déplié glisse comme les autres de l'app (data-slide, race.js). La feuille est une colonne flex de hauteur bornée :
   pendant le glissement, overflow: hidden laisserait le <details> rétrécir à la hauteur de son résumé — la mesure
   d'arrivée tomberait à 35 px et rien ne glisserait. Il ne rétrécit donc jamais. */
.msg-more { flex-shrink: 0; }
.msg-more > summary { list-style: none; cursor: pointer; padding: .55rem 0; font-size: .8rem; font-weight: 700; color: var(--brand-accent-deep); }
.msg-more > summary::-webkit-details-marker { display: none; }
.msg-more > summary::before { content: '▾ '; }
.msg-more[open] > summary { display: none; }
/* « Chercher une membre » (#931) : le champ en tête de la feuille, les résultats dessous — le pseudo à côté du nom,
   la relation dessous, et pour une membre sans relation ce que le premier message vaut. */
.msg-search { margin-bottom: 1rem; }
.msg-search label.msg-group { display: block; }
.msg-search-hint { margin-top: .35rem; }
.msg-search-results:not(:empty) { margin-top: .35rem; }
.msg-search[aria-busy="true"] .msg-search-results { opacity: .6; }
.msg-search-empty, .msg-search-more, .msg-search-short, .msg-search-tip { padding: .5rem 0; }
.msg-search-empty + .msg-search-tip { padding-top: 0; }
.msg-pick .msg-handle { font-style: normal; font-weight: 600; color: var(--brand-muted); }
.msg-pick .msg-pick-note { display: block; margin-top: .1rem; line-height: 1.3; }

/* ------------------------------------------------------------------ */
/* 3. La conversation : en-tête, fil, bulles                            */
/* ------------------------------------------------------------------ */
/* L'en-tête : avatar, prénom, relation, menu ⋯ — le menu déborde du hero, donc pas d'overflow caché ici ;
   le halo décoratif du hero (::after) est repris dans le fond lui-même. */
.msg-hero { display: flex; align-items: center; gap: .8rem; overflow: visible; background: radial-gradient(circle at calc(100% + 60px) -60px, rgba(var(--brand-accent-rgb), .16) 0, transparent 170px), linear-gradient(168deg, var(--brand-secondary) 0%, var(--brand-secondary-2) 58%, var(--brand-secondary-3)); }
.msg-hero::after { content: none; }
.msg-hero .msg-hero-text { flex: 1; min-width: 0; }
.msg-hero h1 { font-size: 1.6rem; margin: 0; overflow-wrap: anywhere; }
.msg-hero .hero-sub { margin: 0; }
.msg-hero .avatar-initials { width: 46px; height: 46px; font-size: 1rem; }
.msg-hero .msg-more { color: var(--brand-on-dark); border: 1px solid rgba(255, 255, 255, .28); background: rgba(255, 255, 255, .1); border-radius: 50%; width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center; font-size: 1.15rem; line-height: 1; }
/* Le menu ⋯ du fil (MSG-11) : mettre en silence — le bouton reste à droite du prénom, le menu s'aligne dessous */
.msg-hero .msg-hero-more { flex: 0 0 auto; }
.msg-hero .msg-hero-more .msg-more[aria-expanded="true"] { background: rgba(255, 255, 255, .22); }

.msg-older.btn { display: block; width: 100%; text-align: center; font-size: .72rem; font-weight: 700; color: var(--brand-muted); padding: .35rem 0 .6rem; text-decoration: none; }
.msg-fil { display: flex; flex-direction: column; gap: .55rem; }
.msg-day { align-self: center; font-size: .62rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--brand-muted); background: var(--brand-line-soft); padding: .2rem .7rem; border-radius: var(--brand-radius-pill); margin: .5rem 0 .15rem; }

/* Une bulle : elle à gauche (avatar), moi à droite (teinte accent). */
.msg-bubble { display: flex; align-items: flex-end; gap: .45rem; max-width: 86%; }
.msg-bubble.is-me { align-self: flex-end; flex-direction: row-reverse; }
.msg-bubble > .avatar-initials { width: 28px; height: 28px; font-size: .7rem; }
/* Le ⋯ se cale en haut de la bulle, sur sa première ligne (retour Axel 18/09 : centré sur le bloc entier,
   il flottait « n'importe comment » dès que la bulle portait des réactions ou une méta). */
.msg-bubble > .dropdown { align-self: flex-start; margin-top: .3rem; }
.msg-body { min-width: 0; }
.msg-box { position: relative; padding: .6rem .8rem; border-radius: 18px; background: var(--brand-surface); border: 1px solid var(--brand-line); font-size: .84rem; line-height: 1.5; color: var(--brand-text); overflow-wrap: break-word; }
.msg-bubble:not(.is-me) .msg-box { border-bottom-left-radius: 6px; }
.msg-bubble.is-me .msg-box { border-bottom-right-radius: 6px; background: color-mix(in srgb, var(--brand-accent) 18%, var(--brand-surface)); border-color: color-mix(in srgb, var(--brand-accent) 40%, var(--brand-line)); }
.msg-box.is-retired { color: var(--brand-muted); font-style: italic; background: var(--brand-line-soft); border-style: dashed; }
.msg-box.is-pending { border-style: dashed; }
.msg-meta { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; font-size: .6rem; color: var(--brand-muted); margin-top: .25rem; padding: 0 .3rem; }
.msg-bubble.is-me .msg-meta { justify-content: flex-end; }
/* La citation (réponse à un message ou à une sélection de texte) */
.msg-quote { border-left: 3px solid var(--brand-accent); background: rgba(var(--brand-secondary-rgb), .05); padding: .3rem .55rem; border-radius: 8px; font-size: .74rem; color: var(--brand-muted); margin-bottom: .4rem; }
.msg-quote b { display: block; font-size: .62rem; color: var(--brand-secondary); }
/* La mention @ — limitée aux participantes du fil */
.msg-mention { display: inline-block; padding: 0 .35rem; border-radius: 6px; background: rgba(var(--brand-accent-rgb), .25); color: var(--brand-secondary); font-weight: 800; }
/* Les réactions (jeu fermé de six), à cheval sur le bas de la bulle */
.msg-reacts { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: -.5rem; padding: 0 .5rem; position: relative; }
.msg-bubble.is-me .msg-reacts { justify-content: flex-end; }
.msg-reacts .react { padding: .05rem .45rem; font-size: .7rem; box-shadow: var(--brand-shadow-card); }
/* Les pièces jointes d'une bulle (MSG-08 bis) : image en aperçu, fichier en carte.
   ⚠ UNE LARGEUR FIXE, comme le GIF : la galerie d'un message sans texte n'a rien pour lui donner sa
   largeur, et des colonnes en fractions d'une boîte qui se mesure sur son contenu valent 0 avant
   que les images arrivent — le fil sauterait (leçon du #748). 320 px donne la largeur, `max-width`
   la plafonne sur un petit écran ; `aspect-ratio` réserve la hauteur de chaque aperçu. */
.msg-files { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .4rem; margin: .35rem 0 .1rem; width: 320px; max-width: 100%; }
.msg-img { display: block; aspect-ratio: 4 / 3; border-radius: 12px; overflow: hidden; background: linear-gradient(135deg, var(--brand-secondary-3), var(--brand-surface-soft)); }
.msg-img img { display: block; width: 100%; height: 100%; object-fit: cover; }
.msg-img:hover img, .msg-img:focus-visible img { outline: 2px solid var(--brand-accent); outline-offset: -2px; }
/* Une vignette qui s'ouvre dans la visionneuse (RM-224) : le curseur le dit. */
.msg-img[data-viewer-url] { cursor: zoom-in; }
/* Une pièce = sa vignette ou sa carte + son ⤓ (retour d'Axel du 23/09, comme la puce de Poulpit) :
   le ⤓ est un lien à part, posé SUR le coin de la vignette ou À CÔTÉ de la carte — jamais dans le
   lien de la vignette. */
.msg-att { position: relative; min-width: 0; }
.msg-att-file { grid-column: 1 / -1; display: flex; align-items: center; gap: .35rem; }
.msg-att-file .msg-file { flex: 1; min-width: 0; }
.msg-dl { flex: none; display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; background: var(--brand-surface); border: 1px solid var(--brand-line); color: var(--brand-secondary); font-size: .95rem; line-height: 1; text-decoration: none; }
.msg-dl:hover, .msg-dl:focus-visible { background: var(--brand-accent-light); color: var(--brand-secondary); }
.msg-att > .msg-img + .msg-dl { position: absolute; right: .35rem; bottom: .35rem; background: color-mix(in srgb, var(--brand-surface) 88%, transparent); box-shadow: var(--brand-shadow-card); }
.msg-file { display: flex; align-items: center; gap: .55rem; padding: .45rem .6rem; border-radius: 12px; background: var(--brand-surface-soft); border: 1px solid var(--brand-line); color: inherit; text-decoration: none; }
.msg-file .msg-file-ico { font-size: 1.2rem; }
.msg-file .msg-text { flex: 1; min-width: 0; }
.msg-file b { display: block; font-size: .76rem; color: var(--brand-secondary); overflow-wrap: anywhere; }
.msg-file small { font-size: .62rem; color: var(--brand-muted); }
/* Des pièces existent, mais le réseau les a désactivées depuis : on le dit, on ne montre pas une image cassée. */
.msg-files-off { margin-top: .35rem; font-size: .7rem; color: var(--brand-muted); font-style: italic; }
/* Le GIF (MSG-08) — servi par l'app depuis son propre stockage, jamais depuis le tiers.
   `height: auto` avec les attributs width/height du serveur : la place est réservée avant que
   l'image charge (le fil ne saute pas), et l'image reste à ses proportions.
   ⚠ UNE LARGEUR FIXE, PAS UN POURCENTAGE : la boîte d'un GIF envoyé seul n'a aucun texte pour lui
   donner sa largeur — elle se mesure sur son contenu. Une image pas encore chargée dont la largeur
   est « 100 % de la boîte » est alors cyclique, vaut 0, et la bulle fait 12 px jusqu'à l'arrivée
   des octets : le fil sautait de 228 px sous les yeux de celle qui venait d'envoyer (mesuré le
   22/09). 240 px donne la largeur, `max-width` la plafonne sur un petit écran une fois la boîte
   posée. */
.msg-gif { display: block; width: 240px; max-width: 100%; height: auto; border-radius: 12px; margin-top: .35rem; background: var(--brand-surface-soft); }
/* Le panneau de recherche de GIF du composer (MSG-08) : replié tant qu'on n'a pas touché « GIF ».
   Les vignettes sont servies par l'app — aucune adresse du tiers n'atteint le navigateur. */
.msg-gifs { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .4rem; padding: .5rem; border-radius: 14px; background: var(--brand-surface-soft); }
/* La mosaïque de Poulpit (retour d'Axel, 22/09 : « sur Poulpit c'est propre ») : des colonnes CSS,
   chaque vignette pleine largeur à sa hauteur naturelle — ni bande, ni rognage, ni case vide —,
   deux colonnes sur téléphone, trois à partir de 576 px (la carte retrouve la taille de celle de
   Poulpit). Le défilement et les colonnes sont deux éléments : un multi-colonnes borné en hauteur
   empilerait les colonnes en trop sur la droite au lieu de déborder vers le bas. Les attributs
   width / height posés par le script réservent la place de chaque vignette avant qu'elle arrive :
   la mosaïque ne se réordonne pas au chargement. */
.msg-gifs-scroll { max-height: min(45vh, 420px); overflow-y: auto; overflow-x: hidden; }
.msg-gifs-grid { column-count: 2; column-gap: 6px; }
.msg-gifs-grid button { display: block; width: 100%; margin-bottom: 6px; padding: 0; border: 0; background: transparent; border-radius: 10px; overflow: hidden; break-inside: avoid; line-height: 0; }
.msg-gifs-grid img { display: block; width: 100%; height: auto; border-radius: 10px; }
.msg-gifs-grid button:hover img, .msg-gifs-grid button:focus-visible img { outline: 2px solid var(--brand-accent); outline-offset: -2px; }
@media (min-width: 576px) { .msg-gifs-grid { column-count: 3; } }
.msg-gifs-note { margin: 0; font-size: .7rem; color: var(--brand-muted); }
/* Le GIF choisi, au-dessus de la zone : on voit ce qui partira, et on peut le retirer */
.msg-gif-chosen { position: relative; width: min(100%, 160px); margin-bottom: .4rem; }
.msg-gif-chosen img { width: 100%; height: auto; border-radius: 12px; display: block; }
.msg-gif-chosen .btn-close { position: absolute; top: .25rem; right: .25rem; padding: .3rem; background-color: var(--brand-surface); border-radius: 50%; opacity: .9; }
/* Le message retenu par les heures calmes : il partira le matin — modifiable, retirable d'ici là */
.msg-pending { display: flex; flex-wrap: wrap; align-items: center; gap: .3rem .6rem; margin-top: .35rem; padding: .45rem .7rem; border-radius: 12px; background: rgba(var(--brand-secondary-rgb), .07); font-size: .7rem; font-weight: 700; color: var(--brand-secondary); }
.msg-pending button { border: 0; background: none; padding: 0; font: inherit; font-weight: 800; color: var(--brand-accent-deep); }
/* Signalé (MSG-11) : le message se fige en attendant la responsable — dit dans la ligne d'heure */
.msg-reported { color: var(--brand-accent-deep); font-weight: 800; }
/* Les heures calmes dites AVANT d'écrire (MSG-11, V6), dans le composer */
.msg-hold { margin: .35rem 0 .2rem; padding: .4rem .7rem; border-radius: 12px; background: rgba(var(--brand-secondary-rgb), .07); font-size: .7rem; font-weight: 700; color: var(--brand-secondary); }
/* Le menu ⋯ d'une bulle (dropdown Bootstrap) : les six réactions, répondre, modifier / retirer ou signaler */
.msg-more-btn { border: 0; background: transparent; color: var(--brand-muted); font-size: 1.1rem; line-height: 1; padding: .1rem .3rem; border-radius: 8px; }
.msg-more-btn:hover, .msg-more-btn[aria-expanded="true"] { background: var(--brand-line-soft); color: var(--brand-secondary); }
.msg-menu { font-size: .84rem; border-radius: 14px; box-shadow: var(--brand-shadow-float); border: 1px solid var(--brand-line); }
/* Un menu rendu ouvert par le serveur (maquette) n'a pas été placé par Popper : l'alignement à droite se force. */
.msg-menu.dropdown-menu-end.show:not([data-bs-popper]) { right: 0; left: auto; }
.msg-menu .msg-emojis { display: flex; gap: .2rem; padding: .3rem .6rem .45rem; border-bottom: 1px solid var(--brand-line-soft); margin-bottom: .25rem; }
.msg-menu .msg-emojis button { border: 0; background: var(--brand-surface-soft); border-radius: 50%; width: 32px; height: 32px; font-size: 1rem; line-height: 1; }
.msg-menu .msg-emojis button.is-on { background: rgba(var(--brand-accent-rgb), .3); }
/* Le fil fermé (course terminée, relation qui a cessé) : lisible, plus d'écriture */
.msg-closed { text-align: center; font-size: .74rem; color: var(--brand-muted); padding: .8rem 1rem; }

/* ------------------------------------------------------------------ */
/* 4. Le composer, collé en bas au-dessus de la tab bar (components/conversation_composer) */
/* ------------------------------------------------------------------ */
.msg-composer { position: sticky; bottom: calc(84px + var(--safe-lift)); z-index: 20; margin-top: 1rem; padding: .55rem .6rem .5rem; border-radius: var(--brand-radius-card); background: var(--brand-surface); border: 1px solid var(--brand-line); box-shadow: var(--brand-shadow-float); }
/* PENDANT QU'ELLE CORRIGE UN MESSAGE, la zone du bas RESTE VISIBLE mais devient inactive
   (arbitrage d'Axel, 22/09 : « la garder visible mais grisée ou inactive, justement pour éviter
   d'écrire des deux côtés en même temps »). On voit qu'elle est là, et son invite dit pourquoi
   elle attend — plutôt qu'un vide qu'on prend pour un bug.
   Elle NE QUITTE JAMAIS LE DOCUMENT : `display: none` lui ferait perdre ses ~145 px, et quand on
   est collée tout en bas du fil le navigateur ÉCRÊTE alors le défilement — la page glisse (mesuré
   à 118 px le 22/09).
   `pointer-events` double l'attribut `inert` du gabarit, qui fait le vrai travail (clavier et
   lecteurs d'écran compris) : ici c'est le filet pour un navigateur qui ne connaîtrait pas `inert`,
   sans quoi le grisé ne serait qu'un décor.
   ⚠ `position: static` N'EST PAS COSMÉTIQUE. Flotter est le privilège de la zone ACTIVE — elle doit
   rester atteignable où qu'on soit dans le fil. Restée collante, la zone grisée passait PAR-DESSUS
   « Enregistrer » et « Annuler » quand on corrige le dernier message (retour d'Axel, 22/09). Une
   zone inactive n'a aucune raison de flotter : remise dans le flux, elle ne peut plus rien
   recouvrir — et la hauteur du document ne bouge pas d'un pixel, `sticky` occupant déjà sa place
   normale. Un `z-index` plus fort sur la bulle en édition n'aurait fait que superposer deux
   cadres. */
.msg-composer.is-locked { opacity: .45; pointer-events: none; position: static; }
/* LA ZONE DE SAISIE TOUJOURS EN BAS DE L'ÉCRAN (retour d'Axel en recette du #754, 24/09 — RM-231) :
   « comme sur Poulpit / WhatsApp / Teams, le pouce sur mobile va auto en bas pour démarrer une saisie ».
   `sticky` seul ne suffit pas : sur un fil court (une demande de contact, un premier message), la place
   naturelle de la zone est juste sous le dernier message, et elle y reste. Ici, SCOPÉ À LA PAGE DU FIL
   (`screen_body_class` posé par member/message/show — rendre .app-main flex sur toutes les pages en
   changerait la mise en page) : la colonne remplit .app-body (déjà `flex: 1` dans .app-screen), et la
   zone du bas — ou ce qui la remplace : la phrase 🤝 qui la précède, « Ce fil est fermé » — prend la
   marge haute automatique. `sticky` garde son rôle : sur un fil long elle colle au bas de l'écran
   pendant qu'on relit plus haut, et sur mobile, quand la barre d'adresse rogne les 100vh, il la
   ramène dans la vue. La pastille « ↓ Nouveau message » y gagne son centrage (`align-self: center`,
   inerte hors flex jusqu'ici). */
.app-body.msg-thread-body { display: flex; flex-direction: column; }
.msg-thread-body > .app-main { flex: 1; display: flex; flex-direction: column; }
.msg-thread-body .msg-composer, .msg-thread-body .msg-notice, .msg-thread-body .msg-closed { margin-top: auto; }
.msg-thread-body .msg-notice + .msg-composer { margin-top: 1rem; }
/* La marge automatique vaut 0 dès que le fil remplit l'écran : l'écart d'un rem avec la dernière bulle
   passe donc sur le fil lui-même (mesuré à 390 px sur le fil 6 : 573 = 573 sans cette ligne). */
.msg-thread-body .msg-fil { margin-bottom: 1rem; }
/* La pastille « Un souci ? » (RETOURS-01, #835 — fixe en bas à droite au-dessus de la tab bar) et la zone de
   saisie collante se disputent le même coin en mobile : mesuré à 390 px sur le fil 6, remontée dans le fil,
   la pastille (714 → 752) recouvre ENTIÈREMENT « Envoyer » (718 → 751). Sur la page du fil, en mobile, elle
   vit DANS la barre d'app, à la place des stats (`screen_feedback_in_bar`, rendue par _partials/app_bar avec
   la variante `in_bar` du composant — feedback.css) : le flex la range, elle ne peut rien chevaucher. Une
   pilule flottante posée par-dessus la barre mordait sur la marque centrée (mesuré : 6 px de marge à 390 px,
   −21 px à 320 px). Ici on éteint donc la flottante, en mobile seulement : sur bureau la tab bar disparaît,
   la pastille est hors de la colonne 720 px, et la barre d'app n'existe pas. */
@media (max-width: 991.98px) {
    .msg-thread-body ~ .feedback-badge { display: none; }
}
/* PENDANT LA SAISIE, LA TAB BAR S'EFFACE ET LA ZONE SE COLLE AU CLAVIER (#881, retour #871) : sur iPhone, le clavier
   remontait la tab bar au-dessus de lui, et la zone de saisie — collée 84 px plus haut pour lui laisser la place —
   se retrouvait au milieu de l'écran. Tant qu'un champ de la zone a le focus (`:focus-within`, Safari 15.4+), la
   tab bar est retirée et la zone descend à 0 : le message occupe l'espace au-dessus du clavier. Au blur, tout revient.
   Mobile seulement : sur bureau la tab bar n'est pas au bas de l'écran. */
@media (max-width: 991.98px) {
    .app-screen:has(.msg-composer:focus-within) .app-tabbar { display: none; }
    .msg-composer:focus-within { bottom: 0; }
}
/* LE FIL D'UNE CLIENTE, DANS SA COQUILLE (MSG-15 / #934, H1) : la coquille publique n'a pas de tab bar — la zone de
   saisie colle au bas de l'écran, pas 84 px au-dessus, et « ↓ Nouveau message » descend d'autant. Scopé par
   `.app-body-flush`, la classe du corps de la coquille publique (le corps de la coquille membre ne la porte pas). */
.app-body-flush .msg-composer { bottom: var(--safe-lift); }
.app-body-flush .msg-new { bottom: calc(166px + var(--safe-lift)); }
/* Lumi et le duo (MSG-15, H10) : la question à la conseillère avant son premier message à une cliente — une carte
   dans le flux, juste au-dessus de la zone de saisie (les jetons de la carte, rien d'autre). */
.msg-lumi { margin-bottom: .75rem; }
.msg-lumi .card-body { padding: .8rem .9rem; }
/* « ↓ Nouveau message » (MSG-08) : colle au-dessus de la zone de saisie (84 px de tab bar + ~150 px
   de composer) quand un message arrive pendant qu'on relit plus haut ; un tap emmène en bas. */
.msg-new { position: sticky; bottom: calc(250px + var(--safe-lift)); z-index: 19; align-self: center; margin-top: .5rem; padding: .35rem .9rem; border: 0; border-radius: 999px; background: var(--brand-accent); color: var(--brand-on-dark); font-size: .78rem; font-weight: 800; box-shadow: var(--brand-shadow-float); }
.msg-new[hidden] { display: none; }
/* La réponse citée au-dessus de la zone */
.msg-reply { display: flex; align-items: center; gap: .5rem; padding: .35rem .55rem; margin-bottom: .4rem; border-left: 3px solid var(--brand-accent); border-radius: 8px; background: rgba(var(--brand-secondary-rgb), .05); font-size: .72rem; color: var(--brand-muted); }
.msg-reply .msg-text { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.msg-reply b { color: var(--brand-secondary); }
/* Les pièces jointes en brouillon (MSG-08 bis) : nom, taille, ✕ — et la vignette d'une image */
.msg-chips { display: flex; flex-wrap: wrap; gap: .35rem; margin-bottom: .4rem; }
.msg-chips:empty { display: none; }
.msg-chip { display: inline-flex; align-items: center; gap: .35rem; max-width: 100%; padding: .2rem .55rem; border-radius: var(--brand-radius-pill); background: var(--brand-surface-soft); border: 1px solid var(--brand-line); font-size: .68rem; font-weight: 700; color: var(--brand-secondary); }
.msg-chip .msg-chip-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-chip img { width: 22px; height: 22px; object-fit: cover; border-radius: 6px; }
.msg-chip small { font-weight: 400; color: var(--brand-muted); white-space: nowrap; }
.msg-chip .btn-close, .msg-reply .btn-close { font-size: .55rem; }
/* Le refus d'un dépôt (type, poids, quota) et l'envoi en cours, sous les puces */
.msg-chips-note { margin: 0 0 .4rem; font-size: .7rem; color: var(--bs-danger); }
.msg-chips-note[hidden] { display: none; }
/* Le champ fichier ordinaire (sans JavaScript) — caché par le script, remplacé par le 📎 */
.msg-files-pick { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin: .1rem 0 .3rem; font-size: .72rem; font-weight: 700; color: var(--brand-muted); }
.msg-files-pick input { font-size: .72rem; font-weight: 400; max-width: 100%; }
.msg-files-pick[hidden] { display: none; }
/* Un fichier survole la zone : on le dit */
.msg-composer.is-dragging { outline: 2px dashed var(--brand-accent); outline-offset: -2px; }
/* La barre d'outils du texte riche (Quill la remplace au branchement, même dessin) */
.msg-tools { display: flex; flex-wrap: wrap; gap: .15rem; margin-bottom: .3rem; }
.msg-tools .msg-tool { border: 0; background: transparent; color: var(--brand-muted); font-size: .8rem; font-weight: 800; padding: .25rem .5rem; border-radius: 8px; line-height: 1.2; }
.msg-tools .msg-tool:hover { background: var(--brand-line-soft); color: var(--brand-secondary); }
/* Le bouton allumé dit que le curseur est DANS ce format : on sait qu'on écrit en gras avant
   d'avoir tapé la première lettre (retour d'Axel, 21/09). Posé par `messagerie.js`. */
.msg-tools .msg-tool[aria-pressed="true"], .msg-tools .msg-tool[aria-pressed="true"]:hover { background: var(--brand-accent); color: var(--brand-on-dark); }
.msg-editor { display: block; width: 100%; border: 0; background: transparent; resize: none; font-size: .9rem; line-height: 1.5; padding: .2rem .3rem; min-height: 2.6rem; }
/* SUR TÉLÉPHONE, JAMAIS UN CHAMP SOUS 16 PX (#881, retour #871) : Safari iOS zoome sur tout champ éditable écrit plus
   petit, et recadre la page sur le curseur — c'est le « texte coupé à gauche » de la capture de Laureen. Vaut pour la
   zone de saisie (textarea et éditeur riche) et la recherche de GIF. Sur bureau, la taille reste celle du fil. */
@media (max-width: 991.98px) {
    .msg-editor, .msg-editor-rich .ql-editor, .msg-gifs .form-control { font-size: 16px; }
}
.msg-editor:focus { outline: none; box-shadow: none; }
.msg-send { display: flex; align-items: center; justify-content: space-between; gap: .6rem; margin-top: .25rem; }
.msg-send .msg-note { flex: 1; min-width: 0; font-size: .64rem; color: var(--brand-muted); line-height: 1.4; }
@media (min-width: 992px) {
    .msg-composer { bottom: 1rem; }
}

/* ------------------------------------------------------------------ MSG-07 : écrire
   Le composer branché (P21-02) : l'éditeur riche de Quill prend la place du <textarea>, qui
   reste le champ posté (masqué). Sans JavaScript, c'est le <textarea> qu'on voit et qui sert —
   ces règles ne le concernent pas. */
.msg-editor-rich {
    min-height: 2.6rem;
    max-height: 9rem;
    overflow-y: auto;
    padding: .45rem .7rem;
    background: var(--brand-surface);
    border: 1px solid var(--bs-border-color);
    border-radius: .7rem;
}

.msg-editor-rich:focus-within {
    border-color: var(--brand-accent);
    outline: 2px solid var(--brand-accent-light);
    outline-offset: -1px;
}

/* Quill pose son propre placeholder en ::before sur un éditeur vide. */
.msg-editor-rich .ql-editor.ql-blank::before {
    color: var(--bs-secondary-color);
    font-style: normal;
}

.msg-editor-rich .ql-editor {
    padding: 0;
    line-height: 1.5;
}

/* L'ÉCART ENTRE DEUX PARAGRAPHES, identique dans la bulle et dans l'éditeur — ce qu'on écrit
   doit ressembler à ce qui s'affiche. Bootstrap pose 1 rem (16 px) sous chaque <p>, soit
   presque une ligne vide pour un texte de 13 px : un simple retour à la ligne ouvrait un trou
   au milieu du message (retour d'Axel, 21/09 : « le saut de ligne est trop large »). */
.msg-box > *,
.msg-editor-rich .ql-editor > * {
    margin: 0;
}

.msg-box > * + *,
.msg-editor-rich .ql-editor > * + * {
    margin-top: .35rem;
}

/* La liste déroulante des mentions (quill-mention) ramenée aux jetons de l'app — jamais son
   bleu par défaut. Un seul nom dans un fil 1-à-1 ; plusieurs dans les groupes (MSG-14). */
.ql-mention-list-container {
    z-index: 1080;
    overflow: hidden;
    background: var(--brand-surface);
    border: 1px solid var(--bs-border-color);
    border-radius: .7rem;
    box-shadow: 0 .5rem 1.2rem rgb(0 0 0 / .12);
}

.ql-mention-list {
    margin: 0;
    padding: .2rem;
    list-style: none;
}

.ql-mention-list-item {
    padding: .35rem .7rem;
    border-radius: .5rem;
    cursor: pointer;
}

.ql-mention-list-item.selected {
    background: var(--brand-accent-light);
}

.msg-bubble .mention {
    font-weight: 600;
    color: var(--brand-accent-deep);
}

/* Le compteur de caractères : sa propre ligne, collée sous la zone de saisie et alignée à
   droite comme la borne qu'il annonce. Posé à la suite de la phrase du filtre, il s'y noyait
   (retour d'Axel, 21/09). */
.msg-count {
    margin: .2rem .15rem 0 0;
    text-align: right;
    white-space: nowrap;
    font-size: .64rem;
    color: var(--brand-muted);
    opacity: .75;
}

/* Modifier en place : le formulaire remplace la bulle (V5 — librement, n'importe quand). */
/* CORRIGER DEMANDE DE LA PLACE. Une bulle de lecture s'arrête à 86 % du fil et se rétrécit à son
   texte ; le formulaire, lui, prend TOUTE la largeur du fil et s'ouvre sur plusieurs lignes —
   corriger dans un timbre-poste est pénible (retour d'Axel, 21/09). */
.msg-bubble.is-editing {
    max-width: 100%;
    width: 100%;
}

.msg-bubble.is-editing .msg-body {
    width: 100%;
}

.msg-edit {
    width: 100%;
}

.msg-edit .msg-editor-rich {
    min-height: 6rem;
    max-height: 60vh;
}

/* `.msg-editor` est volontairement sans bordure dans le composer (la carte fait le cadre) —
   posée seule à la place d'une bulle, elle a besoin du sien, sinon on dirait du texte flottant
   (vu en recette le 21/09). */
.msg-edit .msg-editor {
    width: 100%;
    padding: .45rem .7rem;
    background: var(--brand-surface);
    border: 1px solid var(--bs-border-color);
    border-radius: .7rem;
}

.msg-edit .msg-editor:focus {
    border-color: var(--brand-accent);
    outline: 2px solid var(--brand-accent-light);
    outline-offset: -1px;
}

.msg-edit-actions {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin-top: .35rem;
}

/* Une bulle coupe un mot seulement s'il ne tient pas à lui seul sur la ligne (`break-word`) —
   `anywhere`, posé au dessin des maquettes (#742) et jamais éprouvé sur un vrai message, pèse
   aussi sur la largeur minimale de la bulle et coupait « composer » en « comp / oser »
   (retour d'Axel, 21/09). */

/* Plancher de lisibilité sur téléphone (#879) — mêmes principes qu'app.css : aucun texte sous 12 px */
@media (max-width: 991.98px) {
    .msg-rel, .msg-thread .msg-when, .msg-silenced .msg-text span, .msg-pick span, .msg-pick .msg-handle, .msg-quote b, .msg-file small,
    .msg-send .msg-note, .msg-state, .msg-reply { font-size: .8rem; }
    .msg-group, .msg-day { font-size: .75rem; letter-spacing: .08em; }
    .msg-thread .msg-last, .msg-thread .msg-name { font-size: .95rem; }
    .msg-box { font-size: .95rem; }
    /* AJUST-43 (#2466) : les réactions sous une bulle (11,2 → 12,8 px), comme celles du cercle */
    .msg-reacts .react { font-size: .8rem; }
}
