/* ==========================================================================
   Aurora — media.css : DEFILEMENT STABLE DANS LE FIL
   Chargee EN DERNIER (apres style, themes, anim, polish, header, call,
   attach) : plusieurs regles ci-dessous doivent surcharger `style.css`
   l.301-302 (`.msg img.media`, `.msg video.media`) a specificite egale, et
   c'est alors la DERNIERE feuille qui gagne.

   ---------------------------------------------------------------- le probleme
   `renderAttachment()` posait un `<img class="media" loading="lazy">` SANS
   largeur ni hauteur. Tant que l'image n'est pas decodee, un `<img>` sans
   dimension mesure 0 px de haut : la bulle est plate, puis l'image arrive et
   prend d'un coup ses 180 px. Tout ce qui est en dessous descend d'autant.
   En REMONTANT le fil c'est pire encore : `loading="lazy"` ne declenche la
   requete qu'au moment ou l'image approche du champ de vision, donc chaque
   image qu'on croise en remontant pousse le contenu au moment precis ou on
   essaie de le lire. C'est le saut decrit par le proprietaire.

   ---------------------------------------------------------------- le principe
   On ne laisse plus l'image dicter la hauteur : on RESERVE la place avant
   qu'elle arrive. Une boite `.msta` porte la reservation, l'image se contente
   de la remplir. La boite connait le rapport largeur/hauteur du media par la
   variable `--ratio` (un NOMBRE : largeur / hauteur), posee par
   `js/mediastable.js` a partir de trois sources, dans cet ordre :
     1. les champs `w`/`h` de la piece jointe (albums, et envois recents) ;
     2. le registre local des tailles deja vues (localStorage) ;
     3. a defaut, `--msta-ratio-defaut` — la boite garde une forme plausible
        et ne se corrige qu'une seule fois, a la premiere ouverture.

   La largeur reservee reproduit EXACTEMENT ce que le navigateur aurait fait
   d'un `<img>` contraint par `max-width` / `max-height` :
       largeur = min( taille naturelle, largeur max, hauteur max x ratio, 100% )
   - « taille naturelle » evite d'agrandir un petit autocollant ;
   - « hauteur max x ratio » gere les portraits, brides par la hauteur ;
   - « 100% » gere les bulles etroites (mobile), ou la largeur disponible est
     inferieure aux 240 px du gabarit.
   La hauteur, elle, decoule d'`aspect-ratio` : aucune valeur a maintenir.
   ========================================================================== */

:root {
  /* Rapport largeur/hauteur suppose quand on ne sait RIEN du media. 4/3 est le
     format le plus frequent des photos partagees ; un carre reserverait trop de
     hauteur pour les paysages, un 16/9 pas assez pour les portraits. */
  --msta-ratio-defaut: 1.3333;

  /* Gabarit des medias du fil. Reprend a l'identique les bornes historiques de
     `style.css` (240x280 pour les images, 260x300 pour les videos) : la mise en
     page ne bouge pas d'un pixel par rapport a avant. */
  --msta-max-w: 240px;
  --msta-max-h: 280px;
  --msta-arrondi: 12px;
}

/* --------------------------------------------------------------- 1. LA BOITE */

.msta {
  /* Valeurs de repli : elles s'appliquent tant que le JS n'a pas pose les
     vraies. Une boite dont le JS aurait echoue reste donc dimensionnee. */
  --ratio: var(--msta-ratio-defaut);
  /* Taille naturelle inconnue -> une borne assez grande pour ne jamais gagner
     dans le `min()`. Le JS la remplace par la vraie largeur en pixels des que
     l'image est mesuree, et c'est elle qui empeche un autocollant de 100 px
     d'etre etire a 240. */
  --msta-nat-w: 9999px;

  display: block;
  position: relative;
  box-sizing: border-box;
  width: min(
    var(--msta-nat-w),
    var(--msta-max-w),
    calc(var(--msta-max-h) * var(--ratio))
  );
  /* La borne « largeur disponible » est posee SEPAREMENT, et surtout pas comme
     un quatrieme terme `100%` dans le `min()` ci-dessus. VERIFIE au banc :
     `.bubble` est un element flexible aligne sur `flex-start` (style.css
     l.292-294), sa largeur est donc calculee d'apres son contenu — et un
     pourcentage dont le conteneur depend lui-meme du contenu se resout, pendant
     la passe de dimensionnement intrinseque, sur la largeur du TEXTE de la
     bulle. Une image de 240 px se retrouvait large de 107 px, la largeur du
     libelle qui l'accompagnait.
     `max-width` n'a pas ce defaut : `width` annonce une largeur definie a la
     passe intrinseque (la bulle s'ouvre donc bien a 240 px), et le pourcentage
     n'intervient qu'ensuite, pour rogner sur un ecran vraiment etroit — le
     comportement exact d'un `<img>` en `max-width: 100%`. La hauteur suit
     d'elle-meme, `aspect-ratio` s'appliquant a la largeur UTILISEE. */
  max-width: 100%;
  aspect-ratio: var(--ratio);
  border-radius: var(--msta-arrondi);
  overflow: hidden;
  /* Surface creusee : la place reservee se lit comme un emplacement en attente,
     pas comme un trou dans la bulle. */
  background: var(--sunken);

  /* La hauteur de CETTE boite est justement celle qui change quand l'image
     arrive : en faire l'ancre de defilement du navigateur reviendrait a ancrer
     la lecture sur l'element mouvant. On l'exclut des candidats — le moteur
     choisira la bulle ou le separateur de date qui l'entoure, dont la position
     est stable. */
  overflow-anchor: none;
}

/* Le filet interieur ne sert qu'a materialiser l'emplacement pendant l'attente.
   Une fois l'image posee il disparait : elle a son propre contour. */
.msta:not(.pret) {
  box-shadow: inset 0 0 0 1px var(--border);
}

.msta[data-cliquable="1"] { cursor: pointer; }

/* Les videos gardent leur gabarit historique, plus large et plus haut. */
.msta[data-msta="video"] {
  --msta-max-w: 260px;
  --msta-max-h: 300px;
}

/* ------------------------------------------------- 2. LE BALAYAGE D'ATTENTE */
/* Un reflet qui traverse lentement la surface creusee. Il ne deplace RIEN : le
   pseudo-element est en position absolue dans une boite `overflow: hidden`, et
   seul `transform` est anime — donc composition pure, zero recalcul de mise en
   page a chaque image en vol. */

.msta::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    100deg,
    transparent 18%,
    var(--glass-2) 50%,
    transparent 82%
  );
  opacity: .3;
  transform: translateX(-100%);
  animation: msta-balayage 1.4s var(--an-ease, cubic-bezier(.22, 1, .36, 1)) infinite;
}

/* `content: none` ne masque pas le pseudo-element : il ne le CREE pas. Le
   balayage cesse donc reellement d'exister — pas une animation invisible qui
   continuerait de tourner sur cent bulles. */
.msta.pret::before,
.msta.direct::before,
.msta.err::before { content: none; }

@keyframes msta-balayage {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

/* -------------------------------------------------------------- 3. LE MEDIA */
/* Specificite : `.msg .msta > .msta-media` compte trois classes, contre deux
   classes + un element pour `.msg img.media` (style.css l.301). Trois classes
   l'emportent : les `max-width` / `max-height` historiques sont bien neutralises
   ici, et restent en vigueur partout ailleurs. */

.msta > .msta-media,
.msg .msta > .msta-media {
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  /* `contain` et non `cover` : tant que le ratio n'est pas encore verifie (tout
     premier affichage d'une image inconnue), `cover` ROGNERAIT le sujet. Avec
     `contain` le pire cas est un liseré de surface creusee sur deux cotes,
     corrige des la fin du chargement — jamais une image amputee. */
  object-fit: contain;
  border-radius: inherit;
  background: transparent;

  /* Apparition en fondu : l'image ne surgit pas, elle se pose dans la place qui
     l'attendait deja. Seuls `opacity` et `transform` sont animes. */
  opacity: 0;
  transform: scale(.985);
  transition:
    opacity var(--an-base, 220ms) var(--an-ease, cubic-bezier(.22, 1, .36, 1)),
    transform var(--an-base, 220ms) var(--an-ease, cubic-bezier(.22, 1, .36, 1));
}

.msta.pret > .msta-media {
  opacity: 1;
  transform: none;
}

/* Image deja dans le cache du navigateur : elle est prete AVANT le premier
   rendu de la boite. Lui jouer un fondu ferait clignoter tout l'historique a
   chaque retour dans la conversation. */
.msta.direct > .msta-media {
  transition: none;
  opacity: 1;
  transform: none;
}

/* ------------------------------------------------------- 4. BLOC D'ERREUR */
/* Meme boite, meme place : une image cassee ne doit surtout pas rendre sa
   hauteur au fil — ce serait a nouveau un saut, juste dans l'autre sens. */

.msta.err {
  background: var(--sunken-2);
  box-shadow: inset 0 0 0 1px var(--border);
}

/* Le media en echec doit DISPARAITRE, pas seulement etre recouvert. La classe
   `.pret` est posee aussi en cas d'erreur (il n'y a plus rien a attendre), et
   elle remet l'opacite a 1 : sans cette regle, l'icone native de « image
   cassee » du navigateur reapparaissait dans le coin superieur gauche, par
   dessus notre propre bloc. VU au banc. `display: none` ne change rien a la
   place occupee — c'est la BOITE qui la tient. */
.msta.err > .msta-media { display: none; }

.msta > .msta-err {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px;
  color: var(--muted);
  font-size: .72rem;
  line-height: 1.25;
  text-align: center;
}

.msta > .msta-err svg {
  width: 26px;
  height: 26px;
  opacity: .75;
  flex: none;
}

/* Boite tres basse (panorama, bandeau) : le libelle ne tiendrait pas. Plutot
   qu'une requete de conteneur — qui obligerait a poser `container-type: size`
   sur `.msta` et donc a lui appliquer un confinement de taille incompatible
   avec `aspect-ratio` — c'est le JS qui pose `data-court="1"` quand la hauteur
   reservee descend sous 76 px. */
.msta[data-court="1"] > .msta-err .msta-err-txt { display: none; }
.msta[data-court="1"] > .msta-err svg { width: 20px; height: 20px; }

/* ------------------------------------------- 5. ANCRAGE DU DEFILEMENT (CSS) */
/* L'ancrage de defilement du navigateur (`overflow-anchor`) est la premiere
   ligne de defense : quand la hauteur d'un element SITUE AU-DESSUS du champ de
   vision change, le moteur corrige `scrollTop` tout seul pour que ce qu'on
   regarde ne bouge pas.
   Il est actif par defaut — mais il suffit d'un `overflow-anchor: none` sur la
   boite defilante pour l'eteindre, et `anim.css` l.465-468 en pose justement un
   sur `.sidebar-scroll`. VERIFIE : `.sidebar-scroll` n'est PAS un ancetre de
   `.messages` (l'un est dans la barre laterale, l'autre dans `#chat-active`),
   donc le fil n'a jamais ete concerne. On l'ecrit tout de meme explicitement :
   la valeur devient intentionnelle, et une future regle `*  { overflow-anchor:
   none }` ne pourra plus l'eteindre par accident.
   Reste que ce mecanisme a deux angles morts — Safari ne l'implemente pas, et
   tous les moteurs le suspendent quand `scrollTop` vaut 0 (le tout debut de la
   liste, exactement la ou l'on remonte). D'ou le filet de securite en JS :
   `ancrerDefilement()` dans js/mediastable.js. */

.messages { overflow-anchor: auto; }

/* `:not(.msta)` n'est PAS decoratif. Cette regle est plus bas dans le fichier
   que `.msta { overflow-anchor: none }` (§1) et a la meme specificite : sans
   l'exclusion, une boite de media posee un jour directement dans `.messages`
   (bulle sans conteneur, mosaique a plat) redeviendrait candidate a l'ancrage
   — et le moteur calerait la lecture sur l'element dont la hauteur est
   justement celle qui change. Exactement le bug qu'on repare. */
.messages > *:not(.msta) { overflow-anchor: auto; }

/* --------------------------------------------- 6. MOUVEMENT REDUIT RESPECTE */

@media (prefers-reduced-motion: reduce) {
  .msta::before { animation: none; }

  .msta > .msta-media,
  .msg .msta > .msta-media {
    transition: none;
    transform: none;
    /* Sans transition, l'opacite doit repasser a 1 des que la classe `.pret`
       arrive — mais tant qu'elle n'est pas la, l'image reste masquee pour ne
       pas apparaitre a moitie decodee. C'est `.pret` qui la revele. */
  }

  .msta.pret > .msta-media { opacity: 1; }
}
