/* ==================================================================== */
/* Fiona Lebre — OVERVIEW                                               */
/* La mosaïque (/overview/) et les gestes qui y mènent.                 */
/* ==================================================================== */
/*
 * Ce fichier est sorti de theme.css POUR POUVOIR ITÉRER DESSUS. Le
 * réglage de cette transition se fait à l'œil, par petites retouches
 * successives, et republier 60 Ko de CSS pour changer trois lignes
 * rendait chaque essai coûteux. Tout ce qui concerne l'Overview vit donc
 * ici, et rien d'autre.
 *
 * Chargé APRÈS theme.css (cf. fl_enqueue_assets dans functions.php), sur
 * toutes les pages : la mosaïque est rendue par archive-project.php en
 * accès direct, et injectée dans la home par js/overview.js.
 *
 * CE QUI EST RESTÉ DANS theme.css, et pourquoi :
 * - .home-beige (position dans la grille) : c'est la home au repos, la
 *   règle n'a rien à voir avec l'Overview. Seule son animation est ici ;
 * - .fl-home-frame et .fl-home-frame::before : le montage et son plan
 *   blanc servent au carrousel, pas à la transition ;
 * - les tokens de grille (--grid-*) : partagés par tout le site.
 */

:root {
	/* ==============================================================
	   LA HAUTEUR : UNE LIGNE DE GRILLE, ET 20% DE PLUS.

	   La ligne de grille PLACE, l'échelle DIMENSIONNE. Les rangées restent
	   inscrites dans les lignes 2, 4 et 6 — c'est la grille qui décide OÙ.
	   Mais la hauteur des images est un multiple de la ligne, pas la ligne
	   elle-même : à 1,0 elles paraissaient petites.

	   À 1,2 : 231 x 154 pour une 3:2, 103 x 154 pour une 2:3. Elles
	   dépassent alors leur ligne de 13px en haut et en bas (elles sont
	   centrées), ce qui est sans conséquence — le texte du header est à
	   y=38, le cartel commence 22px sous l'image.

	   IL NE SE RÈGLE PAS SEUL : il commande les gouttières, et
	   FL_OVERVIEW_MAX_LANDSCAPE doit suivre (cf. inc/cpt-projects.php).
	   Mesuré à 1512, avec le plafond à 3 couchées :

	       x1,00  ->  gouttières 108 et 91   (amplitude 20%)
	       x1,20  ->  gouttières  81 et 60   (amplitude 36%)
	       x1,25  ->  gouttières  75 et 52   (amplitude 42%)  */
	--mosaic-row-scale: 1.2;

	/* ==============================================================
	   LA PLANCHE TIENT TOUTE LA LARGEUR : 8 COLONNES SUR 8.

	   --mosaic-span compte en colonnes de la grille du site. À 8 les
	   lignes sont justifiées d'une marge à l'autre, comme la maquette, et
	   le retrait latéral retombe exactement sur --grid-margin.

	   UN RESSERREMENT À 6 A ÉTÉ ESSAYÉ, ET ÉCARTÉ — par Madeo ET par
	   Fiona, qui ont préféré celle-ci. Ce qu'il donnait, mesuré à 1512 :

	                        8 colonnes    6 colonnes
	       2 couchées           81px         48px
	       3 couchées           60px         30px
	       amplitude            36%          60%

	   Plus dense, et le trou s'y lisait mieux (cf. plus bas). Mais à 30px
	   deux verticales de 103px se touchent presque, et le bloc flotte au
	   milieu d'un grand écran sans plus rien toucher. */
	--mosaic-span: 8;
	--mosaic-col: calc( ( 100vw - 2 * var(--grid-margin) ) / var(--grid-columns) );
	--mosaic-inset: calc(
		var(--grid-margin)
		+ ( var(--grid-columns) - var(--mosaic-span) ) / 2 * var(--mosaic-col)
	);

	/* ==============================================================
	   LA GOUTTIÈRE EST ÉLASTIQUE — c'est ce qui rend la ligne justifiée.

	   La ligne s'étire d'une marge à l'autre et les écarts reçoivent ce
	   qui reste, divisé par six. Ils varient donc d'une rangée à l'autre
	   selon le mélange de formats : 81px sur une ligne à 2 couchées, 60px
	   sur une ligne à 3, mesuré à 1512. C'est le plafond
	   FL_OVERVIEW_MAX_LANDSCAPE qui garde ces deux nombres comparables.

	   CE TOKEN N'EST DONC PAS LA GOUTTIÈRE : c'est le plancher qu'on lui
	   garantit, et il ne sert qu'au garde-fou de largeur plus bas — « à
	   partir de quelle hauteur d'image la ligne devient-elle trop serrée
	   pour rester présentable ».

	   UNE VERSION À GOUTTIÈRE FIXE A ÉTÉ ESSAYÉE (20px, tirages
	   jointifs, lignes de largeurs inégales et centrées). Elle rendait le
	   trou trivialement lisible — 115px contre 20 — et rendait caduc le
	   plafond de couchées. Écartée au profit de la justification aux
	   marges. Pour y revenir : retirer width/justify-content ci-dessous
	   et poser gap: 20px. */
	--mosaic-gutter-min: 40px;

	/* ==============================================================
	   LE RATIO PLANCHER — À 0,667 IL NE RECADRE RIEN, ET C'EST VOULU.

	   0,667 est le format le plus étroit du catalogue (2:3, 26 images sur
	   55). Le plancher est donc AU NIVEAU du plus étroit : aucune photo
	   n'est au-dessous, donc aucune n'est élargie, donc aucune n'est
	   rognée. Chaque image garde son format.

	   IL A ÉTÉ MONTÉ À 0,75 PUIS REDESCENDU. À 0,75 les verticales
	   passaient de 103 à 115px — le cartel y respirait mieux — mais la
	   photo était alors rognée de 11% de sa hauteur, 10px en haut et 10px
	   en bas. C'était le seul endroit du site où une image de Fiona était
	   recadrée sans qu'elle l'ait décidé. Le respect du 2:3 l'a emporté.

	   LE MÉCANISME RESTE EN PLACE, et c'est pour ça qu'on garde un token
	   plutôt que d'écrire le ratio en dur : remonter à 0,70 ou 0,75
	   suffit à rouvrir l'essai. Ne pas oublier le coefficient du garde-fou
	   plus bas, qui en dépend.

	   IL NE CONCERNE QUE CETTE PAGE. Le cadre de la home et la planche
	   des pages projet montrent les photos à leur format, sans exception. */
	--mosaic-ratio-min: 0.667;
	--mosaic-row-h: min(
		calc(
			( 100svh - var(--grid-margin-top) - var(--grid-margin) )
			/ var(--grid-rows) * var(--mosaic-row-scale)
		),
		/* LE GARDE-FOU DE LARGEUR, et sans lui la page déborde sur une
		   fenêtre haute et étroite. La ligne la plus large que le plan
		   autorise vaut 3 couchées + 3 debout + le trou, soit
		   3 x 1,5h + 4 x 0,667h = 7,168 h ; il faut qu'il reste de quoi
		   payer six gouttières décentes.

		   LE COEFFICIENT SUIT --mosaic-ratio-min : il valait 7,5 le temps
		   de l'essai où les verticales étaient élargies à 0,75. Si l'un
		   bouge, l'autre doit bouger, sinon le garde-fou autorise une
		   hauteur qui déborde.

		   Vérifié à 1000x1200 : la première branche demanderait 197px de
		   hauteur, et la ligne dense ferait alors 1416px pour 950
		   disponibles — un débord de 49%. Cette seconde branche la ramène
		   à 99px.

		   Quand c'est la largeur qui commande, les images sont plus basses
		   qu'une ligne et restent centrées dedans (align-items: center sur
		   .fl-mosaic-row). Les rangées ne collent alors plus exactement aux
		   lignes 2/4/6 mais y restent inscrites : dégradation visible et
		   sans casse.

		   Elle compte sur la largeur RÉELLEMENT disponible — span
		   colonnes, pas la page entière. */
		calc(
			( var(--mosaic-span) * var(--mosaic-col) - 6 * var(--mosaic-gutter-min) )
			/ 7.168
		)
	);
	/* ==============================================================
	   LE SURVOL NE GROSSIT PLUS L'IMAGE — 1 = désactivé.

	   Il était à 1,15 et c'était juste : dans l'ancienne mosaïque, avancer
	   le tirage était le SEUL retour au survol, et il restait dans sa case
	   grâce aux 18px de gouttière.

	   Deux choses ont changé.

	   D'abord le survol dit maintenant quelque chose : il écrit le cartel.
	   Un agrandissement par-dessus, c'est deux signaux pour une seule
	   intention — et le plus faible des deux masque le plus fort, parce
	   qu'une image qui bouge attire l'œil pendant qu'on essaie de lire un
	   nom juste en dessous.

	   Ensuite les images ont grandi de 20%, donc les gouttières ont
	   fondu. Mesuré : une couchée à 1,15 gagne 17px de chaque côté ; il
	   reste 43px sur une ligne dense au lieu de 64 sur une ligne aérée.
	   Ça tient encore, mais la marge de sécurité a été divisée par trois.

	   POUR LE RÉTABLIR : remettre 1.15 ici, rien d'autre. Toute la
	   mécanique est intacte plus bas — la courbe, la durée, et le pavé qui
	   explique pourquoi le rebond avait été jeté. C'est un doute de Madeo,
	   pas une décision fermée, et ce token est la façon de le rouvrir en
	   un caractère. */
	--mosaic-hover-scale: 1;
	--mosaic-hover-duration: 0.55s;
	--mosaic-hover-ease: cubic-bezier(0.22, 1, 0.36, 1);
	/* LE CARTEL AU SURVOL. --mosaic-caption-gap est la distance entre le
	   bas de l'image et la première ligne de texte. Relevé à 22px sur la
	   maquette, ramené à 20 à l'œil.

	   LA VALEUR DÉCLARÉE EST ICI LA VALEUR VUE, ce qui n'allait pas de
	   soi : le blanc réel entre la photo et le premier trait d'encre vaut
	   20,5px pour 20 déclarés. C'est l'interlignage serré qui le permet —
	   à 12px pour une fonte de 10, le demi-interligne ne fait qu'1px, et
	   l'ascendante d'encre du Louize (7,48) le rattrape presque
	   entièrement. Au temps de l'interlignage à 1,5, le même token aurait
	   donné 3,8px de blanc en plus sans qu'on l'ait demandé.

	   Aucune dépendance, contrairement à l'interlignage : ce nombre ne
	   touche ni au plancher des jambages ni à la géométrie de la ligne.

	   Les deux durées, elles, sont celles de body.fl-quick-swap et pas
	   celles du cartel au repos — cf. la règle .fl-mosaic-caption. */
	--mosaic-caption-gap: 20px;
	--mosaic-caption-in: 0.3s;
	--mosaic-caption-out: 0.15s;
	/* ==============================================================
	   LE CARTEL A SES PROPRES CORPS, ET C'EST INDISPENSABLE.

	   Il empruntait --font-size-caption-title (11px) et
	   --font-size-caption (10px) — les tokens du cartel de la home, que
	   la page projet utilise aussi. Les réduire là-bas aurait rapetissé
	   le cartel des trois vues d'un coup. D'où ces deux tokens-ci.

	   10px, un corps de moins que le cartel de la home, parce que les
	   images de l'Overview sont petites : 231x154 pour une couchée, 103x154 pour
	   une debout. Un cartel au corps du site pesait autant que la photo
	   qu'il nomme.

	   L'information n'est de toute façon pas QUE là : le cartel est en
	   aria-hidden, le nom du projet étant déjà porté par le alt de
	   l'image (cf. archive-project.php). Rien ne se perd pour qui n'y a
	   pas accès. */
	--mosaic-caption-size-1: 10px;

	/* ==============================================================
	   L'INTERLIGNAGE, EN PIXELS ET PAS EN RAPPORT — et les deux points
	   comptent.

	   EN PIXELS parce que les deux lignes n'ont pas le même corps. Un
	   rapport de 1,5 donnait 16,5px puis 15px, soit un rythme inégal dans
	   un bloc de deux lignes. Une valeur absolue les aligne sur la même
	   respiration.

	   IL SUIT LE CORPS, ET IL LE DOIT. Le plancher est la somme des
	   jambages de la ligne 1 et des capitales de la ligne 2, donc il
	   descend quand le texte rapetisse. Mesuré sur les vraies fontes :

	       corps 11/10  ->  3,03 + 7,15  =  plancher 10,18px
	       corps 10/9   ->  2,75 + 6,43  =  plancher  9,18px

	   Le blanc réel entre les deux encres vaut interlignage - plancher.
	   Aux corps actuels :

	       13px  ->  3,82px de blanc
	       12px  ->  2,82px   <- retenu
	       11px  ->  1,82px
	       10px  ->  0,82px   (ça se frôle)

	   12 a été choisi pour garder EXACTEMENT la densité d'avant : à 11/10
	   avec 13px d'interlignage le blanc valait 2,84px ; à 10/9 avec 12px
	   il vaut 2,82. Le cartel rapetisse sans se desserrer. Le bloc passe
	   de 26px de haut à 24.

	   SI LES CORPS CHANGENT ENCORE, remesurer : actualBoundingBoxDescent
	   de la ligne 1, actualBoundingBoxAscent de la ligne 2, et garder au
	   moins 2px de blanc au-dessus de leur somme. */
	--mosaic-caption-leading: 12px;
	/* Durée du geste home <-> overview. TOUT part à l'instant zéro et se
	   termine ensemble : le déplacement de la photo, la fermeture du
	   passe-partout blanc autour d'elle, l'expansion du beige. Rien
	   n'attend rien.
	   Une première version enchaînait les temps (le cadre se refermait,
	   PUIS la photo partait) : ça se lisait comme deux animations mises
	   bout à bout, et surtout la photo ne bougeait qu'une fois le beige à
	   moitié étendu. En parallèle, le geste est unique — et comme plus
	   rien n'est en file d'attente, on peut se permettre de le ralentir :
	   0,85s en séquentiel donnaient 1,275s perçues, 1,1s en parallèle sont
	   à la fois plus longues à l'œil et plus courtes en tout.
	   La courbe démarre en douceur et s'installe très longuement à
	   l'arrivée : la photo se pose, elle ne s'arrête pas net. */
	--overview-morph-duration: 1.1s;
	--overview-morph-ease: cubic-bezier(0.42, 0.02, 0.18, 1);
	/* Apparition des tuiles. --overview-tile-stagger est l'écart entre
	   deux tuiles ; le décalage de départ, lui, est en JS (TILE_START_MS).
	   À 13 tuiles, 45ms donnent ~0,6s d'égrenage, qui se termine à peu près
	   quand la photo se pose. Plus court, les tuiles arrivent en bloc ;
	   plus long, la page a l'air de charger. */
	--overview-tile-duration: 0.55s;
	--overview-tile-stagger: 45ms;
	/* Le texte de la home — index, légende, message d'attente. Court : ces
	   éléments doivent avoir libéré la place avant que les tirages ne
	   commencent à se poser. C'est aussi ce qui donne, par soustraction, le
	   moment où ils reviennent au retour (cf. plus bas). */
	--overview-text-duration: 0.35s;
}

/* ------------------------------------------------------------------ */
/* La vue /overview/                                                   */
/* ------------------------------------------------------------------ */

/* Écran verrouillé, comme la home : la mosaïque tient dans le viewport,
   il n'y a rien à faire défiler. C'est un parti pris et pas une
   commodité — une planche de tirages se regarde d'un coup d'œil, dès
   qu'elle défile elle redevient une galerie.
   CONSÉQUENCE À GARDER EN TÊTE : la vue est plafonnée aux 24 cases de la
   grille. Le jour où le catalogue dépassera largement ce nombre, il
   faudra trancher entre plusieurs planches (pagination) et une grille
   qui défile — pas juste ajouter des lignes. */
html:has(body.fl-overview) {
	height: 100%;
	overflow: hidden;
}

/* LE BEIGE À FOND PERDU. Sur la home c'est un rectangle posé dans les
   marges (un objet sur la page) ; ici il couvre tout (la table sur
   laquelle les tirages sont étalés). C'est le même aplat qui change de
   statut, et c'est précisément ce que la transition met en scène. */
body.fl-overview {
	height: 100vh;
	overflow: hidden;
	position: relative;
	background: var(--color-bg);
}

body.fl-overview.admin-bar {
	height: calc(100vh - 32px);
}

@media screen and (max-width: 782px) {
	body.fl-overview.admin-bar {
		height: calc(100vh - 46px);
	}
}

/* Header superposé, même logique que la home : sa hauteur ne doit pas
   s'ajouter au-dessus de la mosaïque, sinon celle-ci est poussée vers le
   bas et déborde du viewport verrouillé. */
body.fl-overview .site-header {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	z-index: 3;
}

body.fl-overview .site-footer {
	display: none;
}

body.fl-overview .site-main.overview-page {
	height: 100%;
	min-height: 0;
	overflow: hidden;
}

/* ------------------------------------------------------------------ */
/* La mosaïque                                                         */
/* ------------------------------------------------------------------ */

/* En position:fixed et non en flux : c'est ce qui permet au JS de
   l'injecter dans la HOME (par-dessus le montage, sans toucher à
   .home-layout) et d'obtenir exactement la même géométrie que sur
   /overview/ en accès direct. Un seul jeu de règles pour les deux
   rendus, donc aucune dérive possible entre les deux.

   Le padding-top réserve la ligne du header : marge haute + la hauteur de
   ligne de la nav, plus une respiration. */
.fl-mosaic {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 2;
	display: grid;
	/* LES 7 LIGNES DU SITE, pas une trame à nous. C'est tout le
	   changement : la mosaïque n'a plus de grille propre, elle emprunte
	   celle de .home-layout — mêmes lignes, mêmes marges, même padding
	   haut. Une image fait donc exactement la hauteur d'une ligne, et le
	   cartel tombe dans la ligne vide du dessous. */
	grid-template-rows: repeat(var(--grid-rows), 1fr);
	/* Le retrait latéral vaut les marges du site PLUS les colonnes qu'on
	   abandonne (cf. --mosaic-inset). À span = 8 il retombe exactement
	   sur --grid-margin, donc la pleine largeur. */
	padding: var(--grid-margin-top) var(--mosaic-inset) var(--grid-margin);
	box-sizing: border-box;
	pointer-events: none;
}

/* ------------------------------------------------------------------ */
/* Une ligne : justifiée à ses deux marges                             */
/* ------------------------------------------------------------------ */

/* LES IMAGES OCCUPENT LES LIGNES 2, 4 ET 6. La 1 est le bandeau du
   header, les 3, 5 et 7 reçoivent les cartels. Ce n'est pas un rythme
   décoratif : la ligne vide sous chaque rangée EST le logement de son
   cartel, et c'est ce qui justifie la septième.

   space-between, et c'est le cœur du dessin. Les largeurs varient avec
   le format des photos, donc les gouttières ne peuvent pas être fixes :
   elles reçoivent ce qui reste, à parts égales. Mesuré à 1512, à la
   hauteur actuelle — 81px pour une ligne à 2 couchées, 60px pour une
   ligne à 3. C'est le plafond FL_OVERVIEW_MAX_LANDSCAPE qui garde ces
   deux nombres comparables (cf. fl_get_mosaic_layout dans
   inc/cpt-projects.php) : en autorisant 4 couchées on tomberait à 39px,
   soit 110% d'amplitude au lieu de 36%.

   align-items: center et pas stretch : quand la largeur devient le
   facteur limitant (cf. --mosaic-row-h plus bas), les images sont plus
   basses qu'une ligne et doivent rester centrées dedans. */
.fl-mosaic-row {
	display: flex;
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
	min-width: 0;

	/* LES DEUX DÉCLARATIONS VONT ENSEMBLE, et l'oubli de width: 100% a
	   déjà coûté une séance : une marge auto en ligne retire à un enfant
	   de grille son étirement par défaut (elle l'emporte sur
	   justify-self: stretch). La ligne devient alors large comme son
	   CONTENU, space-between n'a plus rien à distribuer, et les images se
	   touchent — gouttière mesurée à 0. */
	width: 100%;

	/* ==============================================================
	   GOUTTIÈRE ZÉRO : LES TIRAGES SE TOUCHENT.

	   La ligne n'est plus étirée d'une marge à l'autre. Elle se
	   dimensionne sur son CONTENU et se centre, donc les images sont
	   jointives et les trois rangées n'ont pas la même largeur — 991,
	   1100 et 989px sur un tirage mesuré à 1512. C'est voulu : des bandes
	   posées, pas des lignes alignées.

	   CE QUE ÇA FAIT AU TROU, et c'est son état idéal. Il devient le SEUL
	   blanc à l'intérieur d'une ligne. Plus de rapport à calculer, plus
	   de plafond de gouttière à régler pour qu'il reste distinguable d'un
	   écart ordinaire : il n'y a plus d'écart ordinaire. La case manquante
	   ne peut pas être lue autrement.

	   ET AU CARTEL : les chiffres romains deviennent la seule chose qui
	   sépare une image de la suivante, comme les numéros de vue en bord de
	   pellicule. Ce qui était un repère devient une ponctuation.

	   CE QU'IL FAUT SURVEILLER : deux photos voisines de tonalité proche
	   fusionnent en une seule image. Laisser les tirages se toucher est
	   une position éditoriale — ça dit « ceux-là vont ensemble » — et
	   c'est à juger sur le vrai catalogue, pas en principe.

	   AUCUN max-width ICI, et ce n'est pas un oubli : le garde-fou de
	   --mosaic-row-h garantit déjà que la ligne la plus large (7,5 h)
	   tient dans la largeur disponible avec de la marge. Un plafond
	   supplémentaire ne pourrait que produire un débordement.

	   ------------------------------------------------------------------
	   POUR REVENIR À LA VERSION JUSTIFIÉE AUX MARGES, il faut ces trois
	   déclarations, et les trois ensemble :

	       width: 100%;
	       max-width: calc( 6 * 70px + 6.335 * var(--mosaic-row-h) );
	       margin: 0 auto;

	   width: 100% N'EST PAS FACULTATIF — c'est ce qui a produit ce
	   dessin-ci par accident. Une marge auto en ligne l'emporte sur le
	   justify-self: stretch d'un enfant de grille : sans width, la ligne
	   redevient large comme son contenu et space-between n'a plus rien à
	   distribuer. Le max-width borne l'étirement sur les grands écrans,
	   où les gouttières gonflaient jusqu'à avaler le trou (rapport 0,8 à
	   2000px de large, contre 1,5 à 2,5 une fois plafonné).

	   ------------------------------------------------------------------
	   UNE CONSÉQUENCE EN CASCADE, si cette version est retenue :
	   FL_OVERVIEW_MAX_LANDSCAPE (3) existait pour garder les gouttières
	   comparables d'une ligne à l'autre. À gouttière nulle il n'a plus
	   d'objet — ce sont les largeurs de lignes qui varient. On pourrait
	   rouvrir à 4 ou 5 couchées et gagner de la variété. */
	margin: 0 auto;
}

.fl-mosaic-row:nth-of-type(1) { grid-row: 2; }
.fl-mosaic-row:nth-of-type(2) { grid-row: 4; }
.fl-mosaic-row:nth-of-type(3) { grid-row: 6; }

/* ------------------------------------------------------------------ */
/* Une case : sa HAUTEUR est donnée, sa LARGEUR est déduite du format   */
/* ------------------------------------------------------------------ */

/* L'INVERSION PAR RAPPORT À LA VERSION PRÉCÉDENTE, en une phrase :
   avant, la case imposait une largeur et l'image s'y logeait ;
   maintenant l'image impose sa largeur et la case la suit. D'où
   aspect-ratio, alimenté par --tile-ratio posé en style inline par le
   template — c'est une donnée par photo, pas un réglage.

   aspect-ratio ET PAS une largeur calculée en JS : le navigateur connaît
   ainsi la largeur AVANT de décoder l'image, donc la ligne ne reflue pas
   au chargement et les gouttières ne sautent pas. C'est la même raison
   qui fait poser width/height sur un <img> ordinaire.

   0.667 en repli — une 2:3 debout, le format majoritaire (26 sur 55) et
   le moins coûteux en largeur. Une URL héritée d'un ancien bloc saisi à
   la main n'est pas dans la médiathèque, donc sans ratio connu ; mieux
   vaut la supposer étroite que large, sinon elle déborde la ligne. */
/* ==================================================================
   max() PLUTÔT QU'UN RATIO IMPOSÉ : la règle ne peut QU'ÉLARGIR.

   La tuile prend le format de sa photo, sauf s'il est plus étroit que
   --mosaic-ratio-min. À la valeur actuelle (0,667, le format le plus
   étroit du catalogue) le plancher n'attrape rien : chaque image garde
   exactement son format, aucune n'est recadrée.

   Le mécanisme est là pour pouvoir rouvrir l'essai d'élargissement des
   verticales sans rien réécrire — cf. le pavé du token, qui porte les
   mesures et le coût en recadrage.

   CE QUI FAIT LA LARGEUR : aspect-ratio, alimenté par --tile-ratio posé
   en style inline par le template. C'est une donnée par photo, pas un
   réglage. Et ça donne au navigateur la largeur AVANT le décodage de
   l'image, donc la ligne ne reflue pas au chargement et les six
   gouttières ne sautent pas.

   0,667 en repli du var() — une 2:3, le format majoritaire (26 sur 55) et
   le moins coûteux en largeur. Une URL héritée d'un ancien bloc saisi à
   la main n'est pas dans la médiathèque, donc sans ratio connu ; mieux
   vaut la supposer étroite que large, sinon elle déborde la ligne.
   ================================================================== */
.fl-mosaic-cell {
	flex: 0 0 auto;
	height: var(--mosaic-row-h);
	aspect-ratio: max( var(--tile-ratio, 0.667), var(--mosaic-ratio-min) );
	min-width: 0;
	box-sizing: border-box;
}

/* LE TROU EST UN ÉLÉMENT, ET IL FAUT QU'IL EN SOIT UN. Avec
   space-between, une case simplement absente ne produirait qu'une
   gouttière double — un écart un peu plus grand, que l'œil lit comme une
   irrégularité de rythme, pas comme un vide. En réservant la largeur
   d'une verticale, il crée une ABSENCE À LA BONNE TAILLE : on voit qu'il
   manque un tirage.

   Mesuré sur la maquette de Madeo : l'écart au trou valait 380px, soit
   130 + 120 + 130 — une gouttière, une verticale, une gouttière. C'est
   cette mesure qui a révélé la règle. */
.fl-mosaic-hole {
	/* IL SUIT LE PLANCHER, et il le doit : le trou réserve « la largeur
	   d'une verticale ». Les verticales ayant été élargies à 0,75, un
	   trou resté à 0,667 serait plus étroit qu'elles et ne se lirait plus
	   comme une case manquante mais comme un intervalle. */
	aspect-ratio: var(--mosaic-ratio-min);
	pointer-events: none;
}

.fl-mosaic-tile {
	position: relative;
	pointer-events: auto;
	display: block;
}

/* La photo remplit sa case, et le recadrage est SANS EFFET : la case a
   exactement le ratio de l'image. object-fit: cover plutôt que contain
   pour la même raison — à ratio égal les deux sont identiques, et cover
   ne laisse pas de bande d'un demi-pixel au sous-pixel. */
.fl-mosaic-tile img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: cover;
}

/* ------------------------------------------------------------------ */
/* Le cartel : sous l'image, au survol                                 */
/* ------------------------------------------------------------------ */

/* IL VIT DANS LA TUILE, et c'est ce qui supprime tout JS de
   positionnement. En absolu sous son image, il est ferré sur elle par
   construction — quelle que soit la gouttière, quelle que soit la
   largeur de la photo. Un cartel unique par ligne, repositionné au
   survol, aurait demandé de lire la position de la tuile à chaque
   entrée de souris ; celui-ci ne demande rien.

   top: 100% le fait déborder dans la ligne vide du dessous. C'est
   exactement ce que cette ligne est là pour recevoir (cf. le pavé de
   .fl-mosaic-row), et pour la dernière rangée c'est la septième ligne
   qui sert — celle qu'on avait prise pour du vide en trop.

   nowrap : un titre long déborde dans la gouttière plutôt que de passer
   à la ligne. Un retour à la ligne changerait la hauteur du cartel d'une
   photo à l'autre, et le cartel cesserait d'être un objet stable.

   aria-hidden côté template : le nom du projet est déjà porté par le alt
   de l'image, qui est le nom accessible du lien. Répéter ne servirait
   qu'à faire lire deux fois la même chose. */
.fl-mosaic-caption {
	position: absolute;
	top: 100%;
	left: 0;
	display: block;
	padding-top: var(--mosaic-caption-gap);
	white-space: nowrap;
	pointer-events: none;
	/* LE BLOC EST TOUJOURS LÀ. Ce n'est plus lui qui fond : ce sont le
	   nom et la ligne 2, séparément. La numérotation, elle, reste
	   visible au repos — c'est ce qui donne à l'Overview une couche de
	   texte permanente, seule chose qu'elle n'avait pas. */
	opacity: 1;
	/* LA COURBE DU CARTEL, LA DURÉE DU SURVOL. « Même apparition que sur
	   la home, adaptée » : on garde --fl-cap-ease-in, la courbe douce du
	   cartel (cf. :root dans css/theme.css), et on raccourcit la durée.

	   Les 0,8s de la home sont justes pour un carrousel qui change de
	   lui-même toutes les 9 secondes — on a le temps. Au survol elles
	   seraient une latence : le cartel arriverait après qu'on a déjà
	   regardé ailleurs. C'est le même arbitrage que
	   body.fl-quick-swap dans css/transition-project.css, qui ramène le
	   cartel de la home à 0,3s quand on parcourt l'index à la souris. On
	   reprend donc ses valeurs, pas d'autres. */
}

/* ==================================================================
   CE QUI FOND, ET CE QUI RESTE.

   Le nom du projet et la ligne 2 sont à zéro au repos, la numérotation
   non. Au survol, les deux premiers arrivent.

   OPACITY ET PAS DISPLAY, ET C'EST CE QUI FAIT MARCHER TOUT LE RESTE.
   Un display:none aurait rétréci la ligne 1 au repos à la seule largeur
   du numéro ; le bloc, ancré à droite sur la dernière image de chaque
   ligne, aurait alors replacé le numéro à chaque survol, et on aurait
   vu un chiffre sauter de 100px. Avec opacity la largeur est celle du
   cartel COMPLET en permanence, invisible mais réservée : la
   numérotation ne bouge jamais, ni à gauche ni à droite.

   Le coût est nul — un bloc transparent de 136px de large sous chaque
   photo, sans pointer-events, dans une ligne de grille vide.
   ================================================================== */
.fl-mosaic-caption-name {
	opacity: 0;
	transition: opacity var(--mosaic-caption-out) var(--fl-cap-ease-out);
}

/* ------------------------------------------------------------------ */
/* Le cartel de la dernière image ne sort pas de la fenêtre            */
/* ------------------------------------------------------------------ */

/* MESURÉ, PARCE QUE « ça dépasse » ne dit pas de combien. Le cartel le
   plus large du catalogue fait 151px — « VII. Les fleurs studio /
   COMMERCIAL, 2024-2025 ». La dernière image d'une ligne finit à 25px du
   bord ; si c'est une DEBOUT (103px de large), son bord gauche est à
   1384 et le cartel finirait à 1535 pour une fenêtre de 1512 : 23px
   coupés. Sur une couchée (231px) il reste 105px de marge, donc le
   défaut ne touche que les verticales — mais elles sont majoritaires.

   FERRAGE À DROITE, et c'est un choix de Madeo contre mon avis — que je
   note parce qu'il a eu raison de trancher à l'œil.

   `right: 0` ancre la boîte sur le bord DROIT de l'image. J'avais
   d'abord laissé les deux lignes ferrées à gauche l'une par rapport à
   l'autre : le bloc se déplaçait, sa typographie ne changeait pas, et le
   cartel de la dernière image restait identique aux autres. Argument
   défendable, mais théorique.

   À l'écran, un bloc aligné sur un bord droit et dont le texte part à
   gauche donne un fer ragged qui ne répond à rien. Le ferrage à droite,
   lui, fait coïncider le bord du texte avec le bord de la photo — le
   même rapport que les autres cartels entretiennent avec leur image, vu
   en miroir. C'est plus cohérent que l'uniformité que je défendais.

   CE QUE ÇA DÉPLACE, et il faut le savoir : la numérotation romaine
   n'ouvre plus le bloc au même endroit. « VII. » se cale désormais selon
   la longueur du titre, alors qu'ailleurs elle démarre sur le bord de
   l'image. Sur une seule tuile par ligne, et à cette taille, ça ne se
   lit pas comme une incohérence.

   :last-child ET PAS :last-of-type, et c'est sans risque : le trou ne
   tombe jamais en bout de ligne (fentes 2 à 6 seulement, cf.
   fl_get_mosaic_layout), donc le dernier enfant est toujours une tuile.

   CE QUI RESTE À SURVEILLER. L'avant-dernière image a 259px de budget
   avant de déborder à son tour, pour 151 utilisés : il faudrait un titre
   d'environ 25 caractères de plus. Confortable aujourd'hui, mais ce n'est
   pas infini — si un projet reçoit un nom très long, c'est ici qu'il
   faudra revenir, et la règle devra alors viser les deux dernières. */
.fl-mosaic-row > .fl-mosaic-cell:last-child .fl-mosaic-caption {
	left: auto;
	right: 0;
	text-align: right;
}

.fl-mosaic-tile:hover .fl-mosaic-caption-name,
.fl-mosaic-tile:focus-visible .fl-mosaic-caption-name {
	opacity: 1;
	/* La transition vit sur l'état d'ARRIVÉE — piège récurrent de ce
	   thème, cf. METHODE/03. Sans cette seconde déclaration, l'entrée
	   emprunterait la durée de sortie. */
	transition: opacity var(--mosaic-caption-in) var(--fl-cap-ease-in);
}

/* ==================================================================
   LES COULEURS SONT INVERSÉES PAR RAPPORT À LA HOME, et c'est voulu.

   Sur la home : titre en doré, seconde ligne en noir. Ici l'inverse —
   titre en noir, catégorie et année en doré.

   La raison tient à ce que chaque cartel désigne. Sur la home il n'y a
   qu'une photo, et le doré sert à faire ressortir son NOM au-dessus du
   reste. Sur l'Overview il y a dix-huit photos de neuf projets
   différents : ce qui se cherche du regard, ce n'est plus le titre — on
   l'a sous les yeux — c'est à quel projet cette image appartient. Le
   doré passe donc sur la ligne qui classe.

   ON NE TOUCHE PAS AU CARTEL DE LA HOME. Les deux vues n'ont pas la même
   question à répondre ; les aligner par symétrie serait confondre
   cohérence et uniformité.
   ================================================================== */

/* Ligne 1 — le titre, en Louize italique NOIR. La fonte et l'italique ne
   changent pas : c'est ce qui fait reconnaître l'objet d'une vue à
   l'autre. Ne PAS le passer en Garcon — la ligne 1 du cartel n'a jamais
   été en sans-serif. */
.fl-mosaic-caption-title {
	display: block;
	font-family: var(--font-louize);
	font-style: italic;
	font-size: var(--mosaic-caption-size-1);
	line-height: var(--mosaic-caption-leading);
	letter-spacing: var(--letter-spacing-wide);
	color: var(--color-text);
}

/* ==================================================================
   LE CHIFFRE EST DORÉ, LE NOM EST NOIR — le contraste des deux couleurs
   a simplement changé d'échelle.

   Il séparait les deux lignes du cartel. La seconde ayant été retirée
   (cf. archive-project.php), il passe à l'intérieur de la ligne qui
   reste : rien n'est perdu de ce qui plaisait.

   ET IL VA DANS CE SENS-LÀ POUR UNE RAISON MESURÉE. Le doré tient 2,7
   de contraste sur le beige, contre 17,1 pour le noir. Les seuils WCAG
   sont à 4,5 pour du texte courant et 3,0 même pour du gros texte : le
   doré est sous les deux. Il ne peut donc pas porter seul une
   information.

   Le chiffre, lui, le peut : il est permanent, décoratif, et son faible
   contraste est une QUALITÉ — dix-huit repères qui s'effacent derrière
   les photos plutôt que de les ponctuer. Le nom du projet, seule chose
   que cette page écrive vraiment, reste en noir.

   EN ROMAIN NON ITALIQUE, comme l'index de la home depuis qu'on l'a
   redressé : le chiffre est un repère, pas un titre. L'italique le
   ferait lire comme la suite de la phrase.
   ================================================================== */
.fl-mosaic-caption-num {
	font-style: normal;
	color: var(--color-accent);
}

/* ------------------------------------------------------------------ */
/* Le survol : le tirage qu'on vise avance, et se repose                */
/* ------------------------------------------------------------------ */

/* ON N'ESTOMPE PAS LES AUTRES, ON AVANCE CELUI-LÀ — et la nuance est
   tout le sujet.

   Une version précédente estompait toutes les tuiles sauf celle visée.
   Retirée, et à raison : sur une grille aérée ça grisait la moitié de
   l'écran au moindre passage de souris, et la planche réagissait en
   permanence alors qu'on ne fait que la parcourir des yeux. Le défaut
   n'était pas d'avoir un survol, c'était d'en faire un événement qui
   touche TOUT.

   Un agrandissement ne touche qu'un élément et ne dit qu'une chose :
   celui-ci est sous le curseur. Les autres tirages restent pleins et
   immobiles.

   ------------------------------------------------------------------
   PAS DE REBOND. C'est un essai raté, et il vaut d'être consigné.

   Une version intermédiaire faisait dépasser le tirage : il montait à
   17 %, redescendait à 14,8 %, se posait sur 15 %. Un ressort amorti,
   calculé et échantillonné en 27 points dans un linear(). Techniquement
   juste, et à l'écran pas élégant du tout — un tirage photo qui rebondit
   se lit comme un jouet, pas comme du papier. La leçon : l'inertie d'un
   objet lourd ne se rend pas par une oscillation, elle se rend par une
   DÉCÉLÉRATION LONGUE. Une masse qui rebondit est une masse légère au
   bout d'un ressort ; une masse lourde arrive lentement et s'arrête.

   D'où cubic-bezier(0.22, 1, 0.36, 1) : le mouvement part vite, puis
   passe l'essentiel de ses 0,55s à ralentir, et n'atteint sa taille
   qu'à la toute fin. Il n'y a aucun dépassement. C'est plus long qu'une
   transition de survol ordinaire (0,3s ailleurs sur le site), et c'est
   là que se trouve le poids.

   Une seule courbe, une seule durée, déclarée sur l'état de REPOS :
   elle joue donc à l'identique à l'aller et au retour. Comme il n'y a
   plus d'asymétrie à ménager (le rebond en avait besoin), il n'y a plus
   de raison de dédoubler.

   AU PASSAGE, DEUX PIÈGES DE linear() PAYÉS EN PRODUCTION, au cas où
   l'idée revienne un jour : ses valeurs sont des NOMBRES et non des
   pourcentages (un pourcentage y désigne une position sur l'axe du
   temps, donc linear(0%, 112.91%, 100%) est purement rejeté) ; et
   doubler la déclaration avec une courbe ordinaire en repli ne protège
   de rien dès qu'il y a un var() dans la valeur — une déclaration avec
   var() n'est validée qu'après substitution, et si elle échoue là elle
   ne rend pas la main à la déclaration précédente, elle remet la
   propriété à sa valeur initiale (transition-duration: 0s).

   ------------------------------------------------------------------
   C'EST L'IMAGE QU'ON TRANSFORME, PAS LA TUILE, et c'est indispensable.

   .fl-mosaic-tile porte DÉJÀ un transform : scale(0.94) au repos,
   none quand la planche se révèle, avec une transition qui embarque
   --tile-delay — jusqu'à 0,6s d'égrenage. Poser le survol dessus
   produirait deux dégâts : un conflit de spécificité avec
   .fl-mosaic.is-revealed .fl-mosaic-tile (même poids, donc l'ordre du
   fichier tranche, ce qui est fragile), et surtout un survol qui
   attendrait le délai d'égrenage de sa tuile avant de répondre. Une
   tuile en fin de série mettrait plus d'une demi-seconde à réagir.

   L'image, elle, n'a aucun transform. Les deux mouvements vivent donc
   sur deux éléments distincts et ne peuvent pas se marcher dessus : la
   tuile s'occupe de l'apparition, l'image du survol.

   ------------------------------------------------------------------
   SEULEMENT LÀ OÙ LE SURVOL EXISTE.

   (hover: hover) and (pointer: fine) — sans ça, un appareil tactile
   applique l'état au premier contact et le GARDE : la tuile reste
   agrandie après le tap, sans rien pour la reposer. L'Overview est de
   toute façon une vue desktop (son bouton est masqué de la navigation
   du téléphone), mais une tablette avec souris existe, et c'est le
   pointeur qu'on interroge, pas la taille de l'écran.

   Le tempo, en revanche, est PROPRE À CET EFFET : le surlignage de
   l'index arrive et s'arrête, il n'a rien à faire ralentir. On ne peut
   donc pas lui emprunter sa durée. */
@media (hover: hover) and (pointer: fine) {
	.fl-mosaic-tile img {
		transition: transform var(--mosaic-hover-duration) var(--mosaic-hover-ease);
	}

	.fl-mosaic-tile:hover img,
	.fl-mosaic-tile:focus-visible img {
		transform: scale(var(--mosaic-hover-scale));
	}
}

/* ÉTAT DE REPOS DES TUILES : invisibles et très légèrement enfoncées.
   Le JS ajoute .is-revealed au conteneur pour les faire apparaître, la
   retire pour les reprendre — après avoir re-tiré au sort les délais dans
   les deux cas. En accès direct à /overview/ la classe est déjà dans le
   HTML servi, donc rien ne s'anime et rien ne clignote au chargement.
   L'échelle de départ est à 0,94 et pas plus bas : au-delà on voit les
   tirages GRANDIR, ce qui ajoute un mouvement là où on veut seulement une
   apparition.

   LA TRANSITION EST DÉCLARÉE ICI, sur l'état de REPOS, et surtout pas
   dans la règle .is-revealed. C'est exactement le piège qui avait déjà
   fait échouer la réouverture du cadre : une transition qui ne vit que
   dans la règle de l'état d'arrivée disparaît EN MÊME TEMPS que la classe.
   Les tirages apparaissaient donc bien un par un, mais s'effaçaient tous
   d'un coup au retour — il n'y avait plus aucune transition à jouer.
   Déclarée sur le repos, elle joue dans les deux sens ; --tile-delay est
   simplement re-tiré au sort avant chaque trajet (cf. shuffleDelays()). */
.fl-mosaic-tile {
	opacity: 0;
	transform: scale(0.94);
	transition: opacity var(--overview-tile-duration) var(--overview-morph-ease) var(--tile-delay, 0ms),
	            transform var(--overview-tile-duration) var(--overview-morph-ease) var(--tile-delay, 0ms);
}

.fl-mosaic.is-revealed .fl-mosaic-tile {
	opacity: 1;
	transform: none;
}

/* La case d'ancrage pendant le trajet : sa vraie tuile reste masquée tant
   que le clone en vol n'est pas arrivé, sinon on verrait deux fois la
   même photo, l'une posée, l'autre en mouvement. */
.fl-mosaic-tile.is-anchor-pending {
	visibility: hidden;
}

.overview-empty {
	grid-column: 1 / -1;
	padding: 120px var(--grid-margin);
	text-align: center;
	font-family: var(--font-mono);
	font-size: 12px;
}

/* ------------------------------------------------------------------ */
/* Home <-> Overview : le geste                                        */
/* ------------------------------------------------------------------ */

/* LE BEIGE EN MOUVEMENT. Le JS le sort du flux de la grille
   (position:fixed + rect mesuré en pixels) puis le pousse à inset:0 —
   ou le ramène à son rectangle au retour. La transition est déclarée ICI
   et pas en JS pour que prefers-reduced-motion puisse la neutraliser plus
   bas d'une seule règle.

   Les valeurs d'arrivée sont posées en JS, en ligne : les coordonnées de
   DÉPART le sont aussi (rect mesuré), et un style en ligne ne peut pas
   être battu par une classe. Les deux extrémités doivent donc vivre au
   même endroit. */
.home-beige.is-expanding {
	position: fixed;
	z-index: 0;

	/* ATTENTION — CETTE DÉCLARATION REPREND AUSSI LE PLI D'ABOUT, et ce
	   n'est pas une commodité : c'est une obligation.

	   "transition" est un RACCOURCI. Il ne s'ajoute pas à la déclaration
	   de css/about.css sur .home-beige, il la REMPLACE — et cette règle-ci
	   est plus spécifique, donc c'est elle qui gagne. Sans les deux
	   dernières lignes, le beige perd la transition de son bord de coupe à
	   l'instant précis où il passe en plein écran.
	   Constaté sur le trajet About -> Overview : le beige devait s'y
	   dérouler, et le bord passait de 100 % à 0 % d'un seul coup. Rien
	   dans le JS ne pouvait le rattraper — la transition n'existait plus.

	   Les valeurs sont lues dans les tokens d'About : si leur RÉGLAGE
	   change là-bas, il n'y a rien à répercuter ici. En revanche, si on
	   ajoute une propriété animée sur .home-beige dans about.css, il faut
	   l'ajouter ici aussi, sinon elle disparaîtra dès que le beige
	   s'étend. C'est le même piège que sur .home-index a, où about.css
	   doit reprendre la transition de couleur de theme.css. */
	transition: top var(--overview-morph-duration) var(--overview-morph-ease),
	            left var(--overview-morph-duration) var(--overview-morph-ease),
	            right var(--overview-morph-duration) var(--overview-morph-ease),
	            bottom var(--overview-morph-duration) var(--overview-morph-ease),
	            --about-wipe var(--about-fold-duration) var(--about-fold-ease),
	            opacity calc( var(--about-fold-duration) * var(--about-fade-share) ) var(--about-fold-ease);
}

/* ------------------------------------------------------------------ */
/* L'index et la légende                                               */
/* ------------------------------------------------------------------ */

/* LE RETOUR EST DÉCLARÉ ICI, SUR L'ÉTAT DE REPOS — et c'est une
   correction, pas une élégance.

   La transition ne vivait QUE dans la règle body.fl-to-overview plus bas.
   À l'aller ça marche : c'est l'état d'arrivée, donc elle s'applique. Au
   retour, la classe s'en va EN EMPORTANT LA DÉCLARATION, et l'index
   réapparaissait d'un bloc, sans fondu. Exactement le piège documenté
   vingt lignes plus haut à propos des tuiles — il avait survécu ici parce
   que personne n'avait regardé le sens retour d'assez près.

   LE DÉLAI EST CELUI DU RETOUR, et il est calculé : l'index revient à la
   toute fin du geste, une fois les tirages repris. Sans lui, il
   réapparaissait à l'instant zéro, c'est-à-dire du texte noir par-dessus
   une planche encore pleine — précisément ce que la version aller évite
   en s'effaçant vite. Le miroir de "partir tout de suite" n'est pas
   "revenir tout de suite", c'est "revenir en dernier".

   La LÉGENDE n'est pas dans cette règle : son fondu de repos est déjà
   déclaré par css/about.css, qui est chargé après celui-ci et gagnerait
   de toute façon. Une seule déclaration pour elle, là-bas. */
.home-index,
.home-work-placeholder {
	transition: opacity var(--overview-text-duration) var(--overview-morph-ease)
	            calc( var(--overview-morph-duration) - var(--overview-text-duration) );
}

/* L'ALLER : l'index et la légende s'effacent VITE et sans délai. Ce sont
   des éléments de la home, ils doivent avoir disparu avant que la
   mosaïque ne commence à se poser, sous peine de se retrouver du texte
   noir par-dessus les tirages. */
body.fl-to-overview .home-index,
body.fl-to-overview .home-caption,
body.fl-to-overview .home-work-placeholder {
	opacity: 0;
	transition: opacity var(--overview-text-duration) var(--overview-morph-ease);
	pointer-events: none;
}

/* LA PASSATION. Le montage est coupé NET à l'instant zéro, au moment
   précis où le clone prend le relais — clone qui démarre exactement sur
   le rectangle du montage, photo comprise. Il n'y a donc rien à voir : à
   cette frame, l'écran est identique avant et après.

   visibility et non display, et posé sur .home-work SEULEMENT : l'index
   et la légende sont d'autres cases de la grille, ils doivent rester dans
   le flux le temps de s'effacer. C'est la grille ENTIÈRE qui passe en
   display:none, mais seulement à la fin du geste.

   Attention à ne pas confondre .is-handed-off avec .is-about-done, celle
   du geste About : les deux valent display:none, mais chaque module ne
   retire QUE la sienne au retour. */
.home-work.is-handed-off {
	visibility: hidden;
}

.home-layout.is-handed-off {
	display: none;
}

/* LE CLONE EN VOL — c'est lui qui porte tout le geste.
   Ce n'est pas une simple image : c'est le MONTAGE en réduction. La boîte
   part à la taille du cadre et arrive à la taille du tirage dans la
   planche ; la photo, elle, part à sa taille réelle affichée (calculée en
   JS depuis le object-fit:contain, jamais la taille de la boîte, sinon le
   départ serait décalé et on verrait un saut) et arrive en remplissant
   toute la boîte.
   Les deux mouvements se jouent ensemble, donc le passe-partout blanc se
   REFERME sur la photo pendant le trajet, jusqu'à disparaître derrière
   elle à l'arrivée. C'est le geste demandé, mais transporté — ce qui le
   rend parallèle au reste, et symétrique : au retour, les mêmes règles
   rouvrent le passe-partout autour de la photo qui remonte. */
.fl-morph-clone {
	position: fixed;
	z-index: 4;
	margin: 0;
	pointer-events: none;
	overflow: hidden;
	background: var( --color-paper );
	transition: top var(--overview-morph-duration) var(--overview-morph-ease),
	            left var(--overview-morph-duration) var(--overview-morph-ease),
	            width var(--overview-morph-duration) var(--overview-morph-ease),
	            height var(--overview-morph-duration) var(--overview-morph-ease);
}

.fl-morph-clone img {
	position: absolute;
	display: block;
	max-width: none;
	transition: top var(--overview-morph-duration) var(--overview-morph-ease),
	            left var(--overview-morph-duration) var(--overview-morph-ease),
	            width var(--overview-morph-duration) var(--overview-morph-ease),
	            height var(--overview-morph-duration) var(--overview-morph-ease);
}

/* Réglage système "réduire les animations" : on ne raccourcit pas la
   transition, on la SUPPRIME. Un mouvement rapide serait pire qu'un
   changement net pour les personnes concernées. Le JS n'intercepte alors
   plus le clic du tout : "Overview" redevient un lien ordinaire vers
   /overview/, servi par archive-project.php. */
@media (prefers-reduced-motion: reduce) {
	.home-beige.is-expanding,
	.fl-morph-clone,
	.fl-morph-clone img,
	.fl-mosaic-tile,
	.fl-mosaic-tile img,
	/* Le cartel aussi, et par cohérence plus que par nécessité : un fondu
	   d'opacité n'est pas du mouvement au sens strict. Mais ce thème coupe
	   TOUT sous ce réglage, et une exception non documentée finirait par
	   passer pour un oubli. Le cartel s'affiche donc d'un coup, ce qui est
	   de toute façon plus net au survol. */
	.fl-mosaic-caption-name,
	.home-index,
	.home-work-placeholder,
	body.fl-to-overview .home-index,
	body.fl-to-overview .home-caption {
		transition: none;
	}

	/* LE SURVOL EST SUPPRIMÉ, PAS RACCOURCI. Neutraliser la seule
	   transition laisserait l'agrandissement se produire d'un coup — un
	   saut de 15 % à chaque passage de souris, soit précisément ce que ce
	   réglage cherche à éviter.
	   La transition est reprise ici parce que la règle de survol en
	   déclare une DE SON CÔTÉ, et que .fl-mosaic-tile img tout court ne
	   peut pas la battre (pseudo-classe = spécificité supérieure). */
	.fl-mosaic-tile:hover img,
	.fl-mosaic-tile:focus-visible img {
		transform: none;
		transition: none;
	}
}

/* ------------------------------------------------------------------ */
/* Petit écran                                                         */
/* ------------------------------------------------------------------ */

@media (max-width: 900px) {
	/* ON ABANDONNE LES SEPT FENTES, et il faut le dire franchement.
	   Sept images justifiées d'une marge à l'autre demandent de la
	   largeur : à 375px, le garde-fou de --mosaic-row-h ramènerait la
	   hauteur de ligne à 34px. Ce ne sont plus des tirages, ce sont des
	   confettis. La règle de composition du bureau n'est donc pas
	   rétrécie ici, elle est RETIRÉE.

	   L'OVERVIEW N'EST PAS UNE VUE MOBILE, et ce repli n'est pas une
	   composition : c'est un filet. Le bouton est masqué de la navigation
	   du téléphone (cf. css/mobile.css) parce que la planche se regarde
	   et se vise à la souris. Mais l'URL reste publique et indexable : on
	   peut y atterrir depuis une recherche. Ce bloc est ce qui rend ce cas
	   présentable, rien de plus. */
	.fl-mosaic {
		display: flex;
		flex-direction: column;
		justify-content: center;
		gap: 24px;
		padding: calc( var(--grid-margin-top) + 42px ) var(--grid-margin) var(--grid-margin);
		overflow-y: auto;
	}

	/* Les lignes débordent et se replient. On ne cherche plus à les
	   justifier : à cette largeur, space-between produirait deux images
	   collées aux bords et un trou au milieu. */
	.fl-mosaic-row {
		display: flex;
		flex-wrap: wrap;
		justify-content: flex-start;
		align-items: center;
		gap: 12px;
	}

	.fl-mosaic-row:nth-of-type(1),
	.fl-mosaic-row:nth-of-type(2),
	.fl-mosaic-row:nth-of-type(3) {
		grid-row: auto;
	}

	.fl-mosaic-cell {
		height: 110px;
	}

	/* LE TROU N'A PLUS DE SENS une fois les lignes repliées : il ne
	   désigne plus une case manquante dans un rythme, il ajoute juste un
	   blanc arbitraire au milieu du flux. On le retire. */
	.fl-mosaic-hole {
		display: none;
	}

	/* PAS DE CARTEL SANS SURVOL. Au doigt il n'y a pas d'état
	   intermédiaire entre « je regarde » et « j'ouvre » : le toucher
	   navigue. Un cartel qui n'apparaît jamais vaut mieux qu'un cartel
	   qui apparaît au moment où la page se remplace. */
	.fl-mosaic-caption {
		display: none;
	}
}
