/* =========================================================
   STUDIO FMR — style.css
   xp.css est chargé avant ce fichier et gère seul la structure
   et le rendu réel des boutons, du bouton close et de la barre
   de progression. Ce fichier ne surcharge QUE des couleurs
   (thème rose au lieu du bleu XP) : jamais display, position,
   flex-direction ou les tailles internes des composants xp.css.
   ========================================================= */

/* font-display: swap — sans lui, les barres de titre restent
   invisibles tant que la police n'est pas chargée.
   La déclaration @font-face "Tahoma" a été retirée : elle
   téléchargeait 266 Ko pour une police qu'aucune règle
   n'utilisait. */
@font-face {
  font-family: "MS1";
  src: url("fonts/MS1.otf");
  font-display: swap;
}

* { box-sizing: border-box; }

html,
body {
  width: 100%;
  height: 100%;
  margin: 0;
  overflow: hidden;
  background: lightskyblue;
  font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
  cursor: url("img/cursor2.cur"), url("img/cursor.png") 2 2, auto;
}

button,
a { cursor: url("img/cursor2.cur"), url("img/cursor.png") 2 2, pointer; }

/* h1/p globaux retirés : ils s'appliquaient à TOUT le site
   (fenêtres, listes, console...), pas seulement au contenu des
   projets. Les titres de projet utilisent déjà leurs propres
   règles dans .project-unified-body. */

/* =========================================================
   Loading et BIOS
   xp.css stylise déjà <progress> (barre verte striée) : on ne
   touche qu'à la taille via une classe, pas à son rendu interne.
   ========================================================= */
#loading-screen,
#bios-screen {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  padding: 24px;
  color: #fff;
  background: #000;
}

#loading-screen.hidden {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
}

/* Police du mode texte VGA, fournie par xp.css : c'est elle qui donne
   l'écran de démarrage plutôt que « du monospace vert ». Les replis
   servent si xp.css ne répond pas. */
#loading-ascii {
  max-width: 96vw;
  margin: 0;
  overflow: hidden;
  color: #39ff14;
  font-family: "Perfect DOS VGA 437 Win", Consolas, "Lucida Console", monospace;
  font-size: clamp(1rem, 5vw, 3rem);
  line-height: 1.1;
  text-align: center;
  text-shadow: 0 0 6px #39ff14;
  white-space: pre;
}

.loading-progress {
  width: min(300px, 72vw);
  height: 24px;
}

/* Le BIOS est masqué par défaut et affiché en plein écran
   via .active, ajouté/retiré par actions.js (playBiosSequence). */
#bios-screen {
  display: none;
  align-items: flex-start;
  justify-content: flex-start;
}
#bios-screen.active { display: flex; }
#bios-output {
  margin: 0;
  font-family: "Perfect DOS VGA 437 Win", Consolas, "Lucida Console", monospace;
  font-size: 16px;
  /* Le mode texte VGA a des cellules de 16px de haut, sans interligne :
     1.35 espaçait les lignes comme un terminal moderne. */
  line-height: 1;
  white-space: pre-wrap;
}

/* =========================================================
   Bureau desktop
   ========================================================= */
.desktop-bg {
  position: relative;
  width: 100vw;
  height: 100vh;
  padding-bottom: 32px;
  overflow: hidden;
}

#grid-menu {
  position: absolute;
  top: 20px;
  left: 20px;
  z-index: 2;
  display: flex;
  flex-direction: column;
}

.icon-row-spacer { height: 82px; }
.icon-row { display: flex; gap: 20px; }

.icon-menu {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 84px;
  min-height: 82px;
  padding: 4px;
  color: #fff;
  background: transparent;
  border: 1px dashed transparent;
  box-shadow: none;
  font: inherit;
  text-align: center;
  text-shadow: 1px 1px #000;
}

/* `:not(:disabled)` n'est pas décoratif : il porte le sélecteur à
   trois classes, une de plus que le `button:not(:disabled):hover`
   d'xp.css qu'il doit surclasser. Sans lui, xp.css l'emportait et
   ces éléments héritaient du liseré orange de XP au survol, et de
   son dégradé enfoncé au clic — alors qu'ils ne ressemblent en rien
   à des boutons XP. Ce sont des <button> pour le clavier, pas pour
   le rendu. */
.icon-menu:not(:disabled):hover,
.icon-menu:not(:disabled):focus,
.icon-menu:not(:disabled):focus-visible,
.icon-menu:not(:disabled):active {
  outline: none;
  border-color: transparent;
  box-shadow: none;
  background: rgba(255, 255, 255, 0.08);
}

.icon-menu.selected {
  border-color: #fff;
  background: rgba(255, 255, 255, 0.12);
}

.icon-menu img {
  display: block;
  width: 48px;
  height: 48px;
  margin-bottom: 4px;
  object-fit: contain;
  border: 0;
}

.icon-menu span {
  font-size: 14px;
  text-shadow: none;
  margin-top:5px;
  overflow-wrap: anywhere;
}

/* =========================================================
   Zone des fenêtres desktop : ne doit imposer aucune largeur.
   ========================================================= */
#desktop-window-group {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 0;
}

/* =========================================================
   Thème rose des fenêtres — uniquement des couleurs.
   xp.css gère déjà display/flex/padding de .window, .title-bar,
   .window-body et .title-bar-controls button : on ne touche
   ici qu'au fond, aux bordures et aux couleurs de texte/ombre.
   ========================================================= */
.window {
  background: #fff;
  box-shadow:
    inset -1px -1px #8a008c,
    inset 1px 1px #df75c6,
    inset -2px -2px #fa89ed,
    inset 2px 2px #f88fe3,
    inset -3px -3px #da00a0,
    inset 3px 3px #e02ab8;
}

.title-bar {
  position: relative;
  height:35px;
  background: linear-gradient(#ee97ec, #f6d3f2);
  text-shadow: 1px 1px #50004e;
  box-shadow: none;
  border-top: 1px solid #f88fe3;
  border-left: 1px solid #f88fe3;
  border-right: 1px solid #8a008c;
  font-family: "MS1";
}

.title-bar-text { color: #fff; }

/* =========================================================
   Remplissage des fenêtres desktop
   ---------------------------------------------------------
   Nos wrappers sont en flex column avec une hauteur fixe, mais
   le .window de xp.css est un bloc en flex: 0 1 auto. Deux
   conséquences qui se cumulaient :

   1. il se dimensionnait sur son contenu et débordait du
      wrapper (une liste de 11 projets donnait 536px dans un
      cadre de 340px) ;
   2. étant en display:block, le flex:1 posé sur .window-body
      était inerte : le corps prenait sa hauteur naturelle et
      le scroll interne ne s'activait jamais.

   D'où les deux réglages : flex + min-height:0 pour que le
   .window prenne exactement la hauteur du wrapper, et flex
   column pour qu'il la transmette au corps, qui peut alors
   scroller. La barre de titre garde sa taille (flex-shrink:0).

   Limité à nos propres conteneurs : le .window de xp.css n'est
   pas touché ailleurs.
   ========================================================= */
.trash-board-window > .window,
.project-text-window > .window,
.project-list-window > .window,
.project-unified-window > .window,
.site-window > .window {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  min-height: 0;
}

.trash-board-window > .window > .title-bar,
.project-text-window > .window > .title-bar,
.project-list-window > .window > .title-bar,
.project-unified-window > .window > .title-bar,
.site-window > .window > .title-bar { flex-shrink: 0; }

.trash-board-window .title-bar,
.project-text-window .title-bar,
.project-list-window .title-bar,
.project-unified-window .title-bar,
.site-window .title-bar { cursor: move; }

/* =========================================================
   En-tête de retour — explorateur, fiche projet, fenêtre site
   ---------------------------------------------------------
   Même gabarit que .site-nav : fond beige, marge
   latérale de 3px pour rester dans le biseau de .window, et
   flex-shrink:0 pour ne jamais se faire écraser par le corps.

   Le bouton garde le rendu xp.css : ni fond, ni bordure, ni ombre
   ne sont redéfinis ici — seulement la mise en ligne de l'icône et
   du mot, que xp.css ne prévoit pas.
   ========================================================= */
.window-back-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 3px;
  padding: 4px 6px;
  background: #ece9d8;
  border-bottom: 1px solid #c8c2b0;
  flex-shrink: 0;
}

.window-bar-button {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-width: 72px;
  padding: 3px 9px;
  font: 11px Arial, sans-serif;
}

.window-bar-button img {
  display: block;
  width: 16px;
  height: 16px;
  object-fit: contain;
}

/* Racine de l'explorateur : le bouton est là mais inerte, comme
   dans l'explorateur de Windows quand l'historique est vide. */
.window-back[disabled] img { opacity: 0.4; }

/* =========================================================
   Explorateur Portfolio — grille des trois dossiers
   ---------------------------------------------------------
   Le corps de .project-list-window est un flex colonne en
   overflow:hidden : c'est donc la grille qui doit scroller,
   d'où flex:1 + min-height:0 comme sur .project-list-rows.
   ========================================================= */
.explorer-grid {
  display: grid;
  flex: 1 1 auto;
  min-height: 0;
  grid-template-columns: repeat(auto-fill, minmax(116px, 1fr));
  gap: 12px;
  align-content: start;
  padding: 12px;
  overflow-y: auto;
  background: #fff;
}

.explorer-folder {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: 0;
  padding: 6px 4px;
  background: transparent;
  border: 1px dashed transparent;
  box-shadow: none;
  outline: none;
  font: inherit;
}

/* Même raison que .icon-menu : surclasser le survol d'xp.css. */
.explorer-folder:not(:disabled):hover, .explorer-folder:not(:disabled):active, .explorer-folder:not(:disabled):focus, .explorer-folder:not(:disabled):focus-visible {
  outline: none;
  border-color: transparent;
  box-shadow: none;
  background: #dbeafe;
}

.explorer-folder img { width: 46px; height: 46px; margin-bottom: 6px; object-fit: contain; }
.explorer-folder-name { width: 100%; overflow: hidden; color: #000; font: 12px Arial, sans-serif; text-align: center; text-overflow: ellipsis; white-space: nowrap; }

/* =========================================================
   Fenêtre About — une fenêtre simple comme les autres.
   L'ancien bloc #about-default-window fixait right/bottom en
   CSS pendant que le JS écrivait left/top : les quatre étaient
   définies en même temps.
   ========================================================= */
.about-window .window-body {
  padding: 12px;
  color: #000;
  background: #fff;
  font-size: 13px;
  line-height: 1.5;
}

/* =========================================================
   Projet
   ---------------------------------------------------------
   Le scroll manquant venait de deux causes combinées :
   1. .window-body (règle globale de xp.css) peut appliquer
      un overflow par défaut sans min-height:0, ce qui empêche
      un enfant flex de rétrécir et donc de scroller.
   2. max-height sans min-height:0 sur le flex parent bloque
      le calcul de la hauteur réelle disponible.
   La combinaison flex:1 + min-height:0 + overflow-y:auto ici
   est nécessaire dans cet ordre pour que le scroll fonctionne.
   ========================================================= */
.project-unified-window {
  position: absolute;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: 60vw;
  height: 75vh;
  min-width: 320px;
  min-height: 300px;
  max-width: calc(100vw - 40px);
  max-height: calc(100vh - 80px);
}

/* overflow-x: hidden explicite. Déclarer le seul overflow-y:auto
   laissait l'axe X à `visible`, que CSS transforme alors en `auto` :
   le moindre contenu trop large ouvrait une barre horizontale au
   lieu de rester dans le cadre. Le débord est corrigé à la source
   (.project-title), ceci empêche qu'un futur contenu le rouvre.

   Le rembourrage haut est revenu : il était à 0 pour ne pas décaler
   le point d'accroche de l'en-tête collant, qui vivait alors dans
   le corps. Ce bloc est parti en pied de fenêtre, la respiration
   redevient celle du corps. */
.project-unified-body {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  padding: 16px;
  overflow-x: hidden;
  overflow-y: auto;
  background: #fff;
}

/* Le titre de projet portait la taille par défaut de xp.css —
   h1 { font-size: 5rem }, soit 80px — faute de règle propre : le
   commentaire en tête de ce fichier annonçait des règles « dans
   .project-unified-body » qui n'ont jamais existé.

   Mesuré dans Firefox sur une fenêtre mobile de 312px : le titre
   « Affiches Conférences ESAC 25 » occupait 510px, soit 210px de
   débord horizontal. Comme le corps est en overflow-y:auto, son
   overflow-x passe implicitement à auto : le contenu sortait du
   cadre au lieu de se limiter au scroll vertical. Le desktop
   débordait pareillement, en moins visible.

   La classe évite de rattacher la règle à un conteneur : le corps
   s'appelle .project-unified-body sur desktop et
   #mobile-window-content sur mobile.

   Le rendu visé est celui d'un <h2> non stylé sur un site de
   l'époque XP : serif par défaut, gras, 1.5x la taille de base.

   1.5rem et non 1.5em : em se calculerait sur le parent, or xp.css
   pose .window { font-size: 11px } — le titre serait tombé à 16px
   au lieu des 24px d'un h2 de navigateur. rem part bien de la
   racine à 16px.

   overflow-wrap est la ceinture : aucun mot, à aucune taille, ne
   peut plus dépasser la largeur du corps. */
.project-title {
  margin: 0 0 12px;
  font-family: "Times New Roman", Times, serif;
  font-size: 1.5rem;
  font-weight: bold;
  overflow-wrap: anywhere;
}

/* Pied de fenêtre : projet précédent / suivant, et le titre en
   relais. Il était un en-tête collant DANS le corps ; il est
   maintenant frère du corps, comme .site-nav.

   Ce qui disparaît avec ce déplacement : position:sticky et son
   z-index n'ont plus d'objet — un frère du conteneur qui défile ne
   défile pas — et le fond opaque n'a plus rien à masquer.

   Ce qui reste indispensable : position:relative, qui sert de
   repère au titre en absolu (voir .project-nav-title), et que
   sticky assurait jusqu'ici. */
.project-nav-links {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0 3px;
  padding: 6px 8px;
  background: #ece9d8;
  border-top: 1px solid #c8c2b0;
  font-family: Arial, sans-serif;
  font-size: 13px;
  flex-shrink: 0;
}

/* Le titre prend le relais au centre de l'en-tête dès qu'il a
   défilé hors de vue (bascule par .shows-title, posée par
   watchProjectTitle).

   Il est HORS DU FLUX, ancré à 50% de l'en-tête. En élément flex il
   se partageait la largeur avec les deux liens : son centre suivait
   leurs longueurs, toujours inégales, et ne tombait donc jamais au
   milieu. En absolu il y tombe, quelles que soient ces longueurs —
   l'en-tête étant déjà positionné (sticky), il sert lui-même de
   repère.

   Aucun `top` : sans lui, l'élément garde sa position statique,
   c'est-à-dire la ligne où il serait tombé dans le flux. Il reste
   donc aligné sur les liens sans rien savoir du rembourrage de
   l'en-tête, qui peut changer sans le désaligner.

   Il est masqué en opacité et non en display : rien ne bouge au
   moment où il apparaît. */
.project-nav-title {
  position: absolute;
  left: 50%;
  max-width: 50%;
  overflow: hidden;
  opacity: 0;
  color: #000;
  font-family: "Times New Roman", Times, serif;
  font-weight: bold;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
  transform: translateX(-50%);
  transition: opacity 0.18s ease;
}
.project-nav-links.shows-title .project-nav-title { opacity: 1; }

/* Les voisins sont maintenant des boutons xp.css (.window-bar-button).
   flex:0 1 auto les laisse se réduire quand le titre prend le relais,
   sans jamais s'étirer pour occuper la ligne. Le titre passant en
   absolu par-dessus, un bouton trop long le masquerait : max-width
   plafonne le libellé, l'ellipse fait le reste. */
.project-nav-link {
  flex: 0 1 auto;
  max-width: 38%;
  overflow: hidden;
}

.project-nav-link .project-nav-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* La flèche ne se réduit jamais : c'est elle qui reste seule sur
   mobile quand le libellé s'efface. */
.project-nav-arrow { flex: 0 0 auto; }

.project-info-bar { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-bottom: 16px; padding: 6px 0; color: #555; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; font-family: Arial, sans-serif; font-size: 12px; }
.project-images-flow { display: flex; flex-wrap: wrap; gap: 16px; }
.project-image-item { width: 100%; margin: 0; }
.project-image-item.portrait { width: calc(50% - 8px); }
.project-image-item img { display: block; width: 100%; height: auto; }
.project-image-item figcaption { margin-top: 4px; color: #555; font: 12px Arial, sans-serif; text-align: right; }

/* =========================================================
   Fenêtres simples (About, Liens, Console…)
   ========================================================= */
.project-text-window {
  position: absolute;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: 340px;
  height: 280px;
  min-width: 200px;
  min-height: 120px;
}
.project-text-window .window-body {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  overflow-y: auto;
  background: #fff;
}

/* Une fenêtre qui ne montre qu'une image : elle s'ajuste au cadre
   sans se déformer ni déborder.

   max-width ET max-height, avec width/height en auto : c'est le
   couple qui laisse l'image choisir sa taille en gardant ses
   proportions. object-fit ne servirait à rien ici — il ne s'applique
   qu'à une image dont on a IMPOSÉ les deux dimensions. */
.fenetre-image {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  margin: 0 auto;
}

/* Le corps centre l'image au lieu de la coller en haut à gauche.
   Il est déjà en flex:1 par .project-text-window ; il ne manquait
   que la direction et les deux centrages.

   Une classe sur la fenêtre, et non un `:has()` sur le corps : le
   site est écrit pour tourner sur de vieux navigateurs, et `:has()`
   n'y existe pas — l'image y serait restée collée dans l'angle sans
   que rien ne le signale. */
.image-window { width: 380px !important; height: 420px !important; }

.image-window .window-body {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 8px;
}

.links-page { padding: 16px; color: #000; background: #fff; font-family: Arial, sans-serif; }
.links-page h3 { 
  margin: 20px 0px; 
  text-align: center;
}
.links-page h3:first-child { margin-top: 0; }
.links-page ul { margin: 0; padding-left: 24px; }
.links-page li { margin: 5px 0; }

/* Console */

/* Calé sur un écran de mode texte : 80 colonnes sur 25 lignes affichées
   à 10px dans la police VGA, soit 450 x 250px, plus la ligne tapée et
   les marges. Les 340x280 des autres fenêtres simples obligeraient à
   descendre à 6px. */
.window-console {
  width: 500px;
  height: 390px;
}

.window-console .window-body {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  padding: 10px;
  overflow-y: auto;
  color: #d4d4d4;
  background: #000;
  font-family: Consolas, "Lucida Console", monospace;
  font-size: 13px;
  line-height: 1.6;
}
.console-line { white-space: pre-wrap; word-break: break-word; }

/* L'animation fait 80 colonnes de large. La taille suit la fenêtre
   pour rester entière sur mobile, et le défilement horizontal reste
   possible plutôt que de rogner le logo. line-height: 1 est
   indispensable : l'interligne 1.6 de la console disloquerait le
   dessin verticalement. */
.console-ascii {
  /* Le dessin est fait de demi-blocs (▀ ▄ █). Dans les polices
     système, leur glyphe est très légèrement plus étroit que la
     cellule : une rainure noire traverse alors chaque aplat, à
     chaque caractère. Cette ombre d'un demi-pixel comble le jeu.
     Une police CP437 (Web437) le ferait proprement, au prix d'un
     fichier de police à charger. */
  margin: 6px 0;
  overflow: hidden;
  /* xp.css charge "Perfect DOS VGA 437 Win", la police du mode texte
     VGA, et l'applique a tous les <pre>. On la laisse s'appliquer :
     ses blocs remplissent exactement la cellule, ce qui supprime les
     rainures entre caracteres — et le text-shadow qui les masquait. */
  line-height: 1;
  white-space: pre;
}
.console-prompt-line::before { content: "> "; color: #7fbf7f; }
.console-line:last-child::after { content: "█"; animation: console-blink 1s step-end infinite; }
@keyframes console-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }

/* =========================================================
   Listes projets, sites et publications
   Même correction de scroll que les fenêtres projet : flex
   column sur le conteneur, flex:1 + min-height:0 sur le corps.
   ========================================================= */
.project-list-window {
  position: absolute;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: 360px;
  height: 340px;
  min-width: 250px;
  min-height: 180px;
  max-height: calc(100vh - 80px);
}
.project-list-window .window-body {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  flex-direction: column;
  margin: 0px 3px;
  padding: 0px;
  overflow: hidden;
  background: #fff;
}

.project-list-header,
.project-list-row { display: grid; margin: 0; background-color: #ece9d8; grid-template-columns: 36px minmax(0, 1fr) 64px 64px; gap: 0; align-items: center; }

.project-list-header {
  padding: 6px 8px;
  color: #444;
  border-bottom: 1px solid #999;
  font-family: "MS1", Arial, sans-serif;
  font-size: 11px;
  font-weight: bold;
  text-transform: uppercase;
  flex-shrink: 0;
}

.project-list-rows {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  background: #fff;
}

.project-list-row {
  width: 100%;
  padding: 8px;
  color: inherit;
  background: transparent;
  border: 0;
  border-bottom: 1px solid #ddd;
  box-shadow: none;
  outline: none;
  text-align: left;
}

/* Même raison que .icon-menu : surclasser le survol d'xp.css. */
.project-list-row:not(:disabled):hover, .project-list-row:not(:disabled):active, .project-list-row:not(:disabled):focus, .project-list-row:not(:disabled):focus-visible {
  outline: none;
  border-color: transparent;
  border-bottom-color: #ddd;
  box-shadow: none;
  background: #dbeafe;
}

.project-list-row img { display: block; width: 26px; height: 26px; object-fit: contain; }
.project-list-name { overflow: hidden; color: #000; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
.project-list-date, .project-list-size { color: #555; font-size: 11px; text-align: left; }

/* =========================================================
   Contact — carte de visite à deux faces
   ---------------------------------------------------------
   Plus une fenêtre de bureau mais une carte au centre d'un fond
   flouté. Les anciennes règles .contact-window ont disparu avec
   elle : plus rien ne porte cette classe.

   La scène floute ce qu'il y a DERRIÈRE elle ; la carte, qui est
   son enfant, reste nette.
   ========================================================= */
.card-scene {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 24px;
  background: rgba(30, 20, 40, 0.24);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  /* Le geste tourne la carte : il ne doit pas faire défiler la page
     sous elle, ni déclencher le zoom à deux doigts du navigateur. */
  touch-action: none;
  cursor: grab;
}

/* Rien ne se surligne pendant le geste : ni le texte de la carte, ni
   les icônes du bureau vers lesquelles le glissé s'étend. La
   sélection commence au point d'appui, et celui-ci est toujours dans
   la scène — la couper ici suffit.

   Déclaré aussi sur la descendance, et pas seulement hérité : la
   valeur `auto` d'un enfant est censée reprendre celle du parent,
   mais elle se lit `auto` dans les outils et le comportement dépend
   du moteur. Autant ne rien laisser à l'interprétation. */
.card-scene,
.card-scene * {
  -webkit-user-select: none;
  user-select: none;
}
.card-scene.is-dragging { cursor: grabbing; }

/* La perspective est portée par le parent, jamais par l'élément qui
   tourne : posée sur la carte elle-même, elle serait tournée avec
   elle et le point de fuite suivrait la rotation. */
/* La carte est GRANDE, elle n'est pas agrandie. La nuance fait tout
   le rendu.

   Un transform:scale ne redessine rien : le navigateur compose la
   carte à sa taille de mise en page, en fait une image, puis étire
   cette image. Sur une couche 3D — et tout ici en est une — le
   résultat est une photo d'écran zoomée : texte flou, cadre baveux.

   Donc pas d'échelle nulle part. Les dimensions sont écrites en
   dur, le texte est composé à sa taille définitive, et chaque pixel
   est un vrai pixel. C'est aussi ce qui rendra un futur visuel de
   carte net : l'image sera affichée à sa résolution, jamais étirée.

   Deux niveaux suffisent alors : .card-3d porte la perspective, qui
   doit être sur le PARENT DIRECT de ce qui tourne, et
   .card-3d-inner tourne — son transform est écrit par le moteur à
   chaque image, tout ce qu'on y ajouterait serait effacé. */
.card-scene {
  /* 85 x 55 mm, le format d'une carte de visite : une image exportée
     à ces proportions se pose sans recadrage. */
  --card-w: 520px;
  --card-h: 337px;
}

.card-3d { perspective: 1900px; }

/* Sur un écran qui ne peut plus la contenir, la carte rapetisse
   pour de bon — elle n'est pas réduite après coup. Les paliers se
   recouvrent, le dernier applicable l'emporte. */
@media (max-width: 600px) { .card-scene { --card-w: 420px; --card-h: 272px; } }
@media (max-width: 460px) { .card-scene { --card-w: 330px; --card-h: 214px; } }
@media (max-width: 360px) { .card-scene { --card-w: 280px; --card-h: 181px; } }

/* Les deux faces sont superposées en absolu : c'est donc ce cadre
   qui donne sa taille à la carte, et lui seul.

   L'épaisseur est celle d'une carte tenue en main : les deux faces
   s'écartent d'un demi-millimètre de chaque côté du plan, et quatre
   tranches ferment le volume entre elles. */
.card-3d-inner {
  --card-thickness: 6px;
  position: relative;
  width: var(--card-w);
  height: var(--card-h);
  transform-style: preserve-3d;
}

.card-face {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Sans cela les deux faces se traversent : on lirait le verso en
     transparence par-dessus le recto. */
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}

/* Une image bord à bord, jamais déformée : cover remplit la face en
   rognant l'excédent plutôt qu'en étirant. À proportions égales —
   celles d'une carte de visite — il n'y a rien à rogner. */
.card-face img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Chaque face s'écarte du plan médian d'une demi-épaisseur. Pour le
   verso, translateZ vient APRÈS le demi-tour : dans son repère
   retourné, avancer c'est reculer — les deux faces s'écartent donc
   dos à dos au lieu de se croiser. */
.card-face-front { transform: translateZ(calc(var(--card-thickness) / 2)); }
.card-face-back { transform: rotateY(180deg) translateZ(calc(var(--card-thickness) / 2)); }

/* Les quatre tranches. Chacune est une bande large (ou haute) comme
   l'épaisseur, redressée d'un quart de tour pour se mettre de chant :
   sa largeur se déploie alors en profondeur. Le translate qui précède
   la ramène pile sur le bord de la carte, son centre y tombant à une
   demi-épaisseur près.

   Le chant est une tranche peinte, comme une tranche argentée ou
   dorée d'imprimeur. Deux dégradés superposés, et c'est leur
   ORIENTATION qui fait tout :

   — en travers de l'épaisseur, le reflet spéculaire : sombre aux
     deux arêtes, éclatant au milieu. C'est ce que fait la lumière
     sur un chant lisse, et c'est ce qui distingue le métal d'une
     bande de couleur ;
   — le long de la tranche, une brillance irrégulière, sans quoi le
     reflet serait le même sur toute la longueur et se lirait comme
     une réglette de plastique.

   Les dégradés sont en blanc et noir TRANSPARENTS : la teinte vient
   de --card-edge (content.js). Un gris donne l'argent, un ocre la
   dorure, un vert fluo une tranche peinte — la matière ne change
   pas, seule sa couleur.

   Le blanc reste mesuré, et c'est délibéré : porté à fond il lave
   la teinte au centre du reflet, et une couleur saturée y perdrait
   justement ce qui fait sa saturation. Ce sont les arêtes sombres,
   pas le blanc, qui donnent son volume au chant. */
.card-edge {
  position: absolute;
  background-color: var(--card-edge, #b9bcc2);
  pointer-events: none;
}

.card-edge-left,
.card-edge-right {
  background-image:
    linear-gradient(to bottom,
      rgba(255, 255, 255, 0.42), rgba(255, 255, 255, 0) 16%,
      rgba(0, 0, 0, 0.12) 38%,
      rgba(255, 255, 255, 0.4) 56%, rgba(255, 255, 255, 0) 76%,
      rgba(255, 255, 255, 0.45)),
    linear-gradient(to right,
      rgba(0, 0, 0, 0.45),
      rgba(255, 255, 255, 0.34) 28%,
      rgba(255, 255, 255, 0.52) 46%,
      rgba(255, 255, 255, 0.08) 64%,
      rgba(0, 0, 0, 0.4));
}

.card-edge-top,
.card-edge-bottom {
  background-image:
    linear-gradient(to right,
      rgba(255, 255, 255, 0.38), rgba(255, 255, 255, 0) 18%,
      rgba(0, 0, 0, 0.12) 40%,
      rgba(255, 255, 255, 0.42) 58%, rgba(255, 255, 255, 0) 78%,
      rgba(255, 255, 255, 0.45)),
    linear-gradient(to bottom,
      rgba(0, 0, 0, 0.45),
      rgba(255, 255, 255, 0.34) 28%,
      rgba(255, 255, 255, 0.52) 46%,
      rgba(255, 255, 255, 0.08) 64%,
      rgba(0, 0, 0, 0.4));
}

/* L'éclairage de XP, venu d'en haut à gauche : les deux chants
   opposés restent en retrait. Léger — le métal garde son éclat. */
.card-edge-right,
.card-edge-bottom { filter: brightness(0.9); }
.card-edge-left,
.card-edge-right {
  top: 0;
  width: var(--card-thickness);
  height: 100%;
}
.card-edge-top,
.card-edge-bottom {
  left: 0;
  width: 100%;
  height: var(--card-thickness);
}
.card-edge-left {
  left: 0;
  transform: translateX(calc(var(--card-thickness) / -2)) rotateY(90deg);
}
.card-edge-right {
  right: 0;
  transform: translateX(calc(var(--card-thickness) / 2)) rotateY(90deg);
}
.card-edge-top {
  top: 0;
  transform: translateY(calc(var(--card-thickness) / -2)) rotateX(90deg);
}
.card-edge-bottom {
  bottom: 0;
  transform: translateY(calc(var(--card-thickness) / 2)) rotateX(90deg);
}

/* Le rendu est masqué, pas la surface : une face tournée continue
   d'intercepter les clics destinés à l'autre. La classe est posée
   par render() dans startContactCard. */
.card-3d-inner:not(.shows-back) .card-face-back,
.card-3d-inner.shows-back .card-face-front { pointer-events: none; }

/* Verso par défaut, en attendant le vrai visuel de carte. */
/* Les boutons, à plat sous la carte. Ils ne tournent pas avec elle :
   sur une face ils seraient illisibles de biais et absents de dos,
   et le geste qui retourne la carte se confondrait avec le clic.

   Ils restent des boutons xp.css — c'est la balise qui porte le
   rendu, il n'y a rien à redéclarer ici que l'espacement. */
.card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
}

.card-action {
  min-width: 88px;
  /* Hauteur imposée, et pas seulement du rembourrage : le bouton de
     fermeture n'a pas de texte, donc pas de ligne pour le porter. Il
     s'écraserait à la seule hauteur de ses marges pendant que les
     deux autres gardent la leur. */
  min-height: 24px;
  padding: 4px 14px;
  font: 12px Arial, sans-serif;
}

/* Le bouton de fermeture : même gabarit que ses voisins, mais la
   croix de XP à la place du mot.

   La croix est reprise du bouton close d'xp.css, au pixel près —
   c'est un dessin en escalier, pas une police : d'où
   shape-rendering:crispEdges, qui interdit au navigateur de
   l'adoucir, et le viewBox décalé d'un demi-pixel, qui fait tomber
   des traits d'un pixel sur la grille de l'écran.

   Le rouge est celui du vrai bouton : #e03802, relevé dans le
   dessin d'xp.css lui-même, avec le dégradé clair-vers-sombre de XP.

   Le fond est déclaré en deux couches, la croix PUIS le dégradé :
   dans background, la première couche est celle du dessus.

   Le contour reste celui d'xp.css — rien n'est redéclaré ici — pour
   que les trois boutons de la rangée aient exactement le même
   cadre. */
.card-close {
  background:
    url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -0.5 21 21' shape-rendering='crispEdges'%3E%3Cpath stroke='%23fff' d='M6 5h1M14 5h1M5 6h3M13 6h3M6 7h3M12 7h3M7 8h3M11 8h3M8 9h5M9 10h3M8 11h5M7 12h3M11 12h3M6 13h3M12 13h3M5 14h3M13 14h3M6 15h1M14 15h1'/%3E%3C/svg%3E") center / 21px 21px no-repeat,
    linear-gradient(#ee7a55, #e03802 46%, #c62f02);
  /* Le texte est absent, mais un mot par accident (une extension, une
     traduction automatique) ne doit pas déformer le bouton. */
  font-size: 0;
}

/* xp.css habille button:hover et button:active avec son propre
   dégradé : sans ces deux règles, survoler le bouton le repeindrait
   en gris. */
.card-close:hover {
  background:
    url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -0.5 21 21' shape-rendering='crispEdges'%3E%3Cpath stroke='%23fff' d='M6 5h1M14 5h1M5 6h3M13 6h3M6 7h3M12 7h3M7 8h3M11 8h3M8 9h5M9 10h3M8 11h5M7 12h3M11 12h3M6 13h3M12 13h3M5 14h3M13 14h3M6 15h1M14 15h1'/%3E%3C/svg%3E") center / 21px 21px no-repeat,
    linear-gradient(#f2926f, #e8501c 46%, #cf3703);
}

.card-close:active {
  background:
    url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -0.5 21 21' shape-rendering='crispEdges'%3E%3Cpath stroke='%23fff' d='M6 5h1M14 5h1M5 6h3M13 6h3M6 7h3M12 7h3M7 8h3M11 8h3M8 9h5M9 10h3M8 11h5M7 12h3M11 12h3M6 13h3M12 13h3M5 14h3M13 14h3M6 15h1M14 15h1'/%3E%3C/svg%3E") center / 21px 21px no-repeat,
    linear-gradient(#c62f02, #e03802 54%, #ee7a55);
}

/* La rotation n'a aucun signe visible : sans cette ligne, rien ne
   dit qu'il y a un dos. */
.card-hint {
  margin: 0;
  color: rgba(255, 255, 255, 0.85);
  font-family: Arial, sans-serif;
  font-size: 11px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  user-select: none;
  pointer-events: none;
}

/* =========================================================
   Sécurité / Captcha — débord beige + panneau blanc.
   Les boutons Reset/Entrer restent gérés par xp.css.
   ========================================================= */
.captcha-window { width: 360px !important; height: auto !important; min-height: 0 !important; }
.captcha-window .window-body {
  height: auto;
  margin: 0px 3px;
  padding: 10px;
  overflow: visible;
  background: #ece9d8;
}
.captcha-box { font: 13px Arial, sans-serif; }

/* Les deux moitiés du code sont empilées, chacune dans son cadre et
   séparées par un filet d'espace : on lit deux fragments d'un même
   code, pas une image coupée en deux. */
.captcha-image-wrap {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 16px;
}

/* Hauteur automatique, sans rapport imposé : c'est l'image qui donne
   sa forme au cadre, pas l'inverse.

   Avec aspect-ratio, le rapport porte sur la boîte EXTÉRIEURE, dont
   la bordure de 1px retire deux pixels de large. La zone de contenu
   n'était donc plus tout à fait au format des images, et object-fit:
   contain rendait la différence en bandes vides sur les côtés. */
.captcha-image {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid #999;
  background: #eee;
}

/* Le display:none de l'attribut hidden vient de la feuille du
   navigateur, que la règle ci-dessus surclasse : sans cette ligne,
   une moitié sans image resterait un cadre vide. */
.captcha-image[hidden] { display: none; }

/* Réponse au code saisi (Critical / Favorites). Elle occupe la
   hauteur des deux moitiés réunies — même rapport, donc aucun saut
   de mise en page au moment où elle les remplace. */
.captcha-answer {
  display: none;
  width: 100%;
  aspect-ratio: 3 / 2;
  padding: 12px;
  object-fit: contain;
  border: 1px solid #999;
  background: #fff;
}

.captcha-image-wrap.is-answering .captcha-image { display: none; }
.captcha-image-wrap.is-answering .captcha-answer { display: block; }

/* Respiration entre la saisie et les boutons : collés, on visait
   Entrer en croyant cliquer dans le champ. */
.captcha-input { width: 100%; margin-bottom: 24px; }
.captcha-actions { display: flex; justify-content: center; gap: 8px; margin-top:15px }
.success { color: green; }
.error { color: red; }

/* =========================================================
   Sites web — le site s'affiche dans une fenêtre
   ---------------------------------------------------------
   Dimensions demandées : toute la page moins une marge, calquée
   sur les insets du panneau mobile (36px sur les côtés, la barre
   du bas en plus). placeWindow centre ensuite la fenêtre, ce qui
   répartit la marge de lui-même.
   ========================================================= */
.site-window {
  position: absolute;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: calc(100vw - 72px);
  height: calc(100vh - 104px);
  min-width: 280px;
  min-height: 240px;
}

/* Même pied que la fiche projet : frère du corps, donc il ne défile
   jamais. Le retour vers l'explorateur, lui, est en en-tête
   (.window-back-bar). */
.site-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: 0 3px;
  padding: 6px 8px;
  background: #ece9d8;
  border-top: 1px solid #c8c2b0;
  font: 12px Arial, sans-serif;
  flex-shrink: 0;
}
.site-nav a { color: #00f; }

/* Publication seule dans sa liste : pas de voisin à nommer, donc pas
   de pied. Il reste dans le DOM (voir buildSiteNavHtml), mais une
   barre beige vide n'a rien à montrer. */
.site-nav.is-empty { display: none; }

.site-body {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  margin: 0 3px;
  padding: 0;
  overflow: hidden;
  background: #fff;
}

/* Chargeur et message d'échec sont deux états exclusifs du corps,
   posés par startSiteLoading() : .is-loading pendant l'attente,
   .is-blocked une fois le délai écoulé. Aucun des deux n'est visible
   quand le site a fini par s'afficher. */
.site-loader,
.site-blocked { display: none; }
.site-body.is-loading .site-loader,
.window-body.is-loading .site-loader { display: flex; }
.site-body.is-blocked .site-blocked,
.window-body.is-blocked .site-blocked { display: flex; }

.site-loader {
  position: absolute;
  inset: 0;
  z-index: 2;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 24px;
  color: #fff;
  background: #000;
  font-family: Consolas, "Lucida Console", monospace;
  text-align: center;
}

.site-loader-text { margin: 0; font-size: 13px; }

/* Le rendu de la barre appartient entièrement à xp.css : sans
   attribut value, elle prend la forme indéterminée qui défile en
   boucle. On ne pose ici que sa largeur — sa hauteur est celle du
   composant XP, et l'en-tête de ce fichier interdit de toucher aux
   tailles internes de xp.css. Une hauteur déclarée ici serait de
   toute façon perdue : progress:not([value]) est plus spécifique
   qu'une simple classe. */
.site-loader-progress { width: min(300px, 70%); }

/* L'iframe couvre le message : celui-ci n'apparaît que si le site
   refuse d'être encadré et ne peint rien. */
.site-frame {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
.site-blocked {
  position: absolute;
  inset: 0;
  z-index: 0;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 0;
  padding: 24px;
  color: #555;
  text-align: center;
  font: 13px Arial, sans-serif;
}

/* =========================================================
   Corbeille — le plan et son hublot
   ---------------------------------------------------------
   Le corps de la fenêtre est le hublot : il masque tout ce qui
   dépasse et ne défile pas, c'est le plan qui bouge dessous.

   touch-action: none coupe le défilement et le zoom que le
   navigateur déclencherait sur un glissé du doigt — sans lui, le
   geste qui déplace la vue tire aussi la page.

   overscroll-behavior: contain empêche le même geste, arrivé au
   bord, de remonter au document et d'y déclencher le rebond ou le
   « tirer pour rafraîchir » du mobile.
   ========================================================= */
.trash-board-window {
  position: absolute;
  z-index: 1;
  display: flex;
  flex-direction: column;
  width: 70vw;
  height: 78vh;
  min-width: 320px;
  min-height: 260px;
  max-width: calc(100vw - 40px);
  max-height: calc(100vh - 80px);
}

.trash-board-body,
.mobile-window.trash-board .window-body {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  margin: 0 3px;
  padding: 0;
  overflow: hidden;
  background: #fff;
  cursor: grab;
  touch-action: none;
  overscroll-behavior: contain;
}

.trash-board-body.is-dragging,
.mobile-window.trash-board .window-body.is-dragging { cursor: grabbing; }

/* Le curseur de survol est posé par le JS, pas par une règle sur les
   images : le navigateur ne sait pas dire ce qui est sous le curseur
   dans une scène en preserve-3d (voir trashPieceAt), et la main
   restait ouverte au-dessus d'une image parfaitement visible. */
.trash-board-body.is-over-piece,
.mobile-window.trash-board .window-body.is-over-piece { cursor: pointer; }

/* L'aplat coloré du mode démo. Sa taille est posée en ligne : elle
   remplace les proportions d'une image, qu'il n'a pas. */
.trash-fill { display: block; }

/* Fenêtre agrandie. Position et dimensions viennent du JS, qui seul
   connaît la place disponible ; la règle n'est là que pour couper le
   redimensionnement au curseur. */
.is-maximized .resize-handle { display: none; }

/* Le monde 3D. Il remplit le hublot pour que le centre de la sphère
   — l'origine de sa rotation, et la place des supports — tombe au
   milieu du cadre.

   preserve-3d est la clause décisive : sans elle le navigateur
   aplatit les enfants dans le plan du parent, et la sphère se
   réduirait à un tas d'images superposées. Elle doit être posée sur
   CHAQUE niveau qui doit rester en volume, d'où sa présence aussi
   sur les supports.

   La perspective, elle, est écrite en pixels sur le hublot par
   layoutTrashSphere : elle vaut le rayon de la sphère, que le CSS
   ne peut pas connaître. */
.trash-world {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  will-change: transform;
}

/* Un carreau du décor. Sa taille et son orientation viennent du JS,
   qui seul connaît le rayon ; ne restent ici que les deux couleurs
   du damier et le placement au centre, dont les carreaux s'écartent
   par leur marge négative.

   Un damier laisse voir ses arêtes en escalier quand deux plans
   sécants se rejoignent : le liseré de la même couleur que le
   carreau les efface, en débordant très légèrement. */
.trash-tile {
  position: absolute;
  top: 50%;
  left: 50%;
  background: #fff;
  box-shadow: 0 0 0 1px #fff;
}

.trash-tile.is-dark {
  background: #111;
  box-shadow: 0 0 0 1px #111;
}

/* Un support est un point sans dimension, placé sur la sphère et
   tourné vers son centre. Sa taille nulle fait de son origine de
   transformation le centre exact de la sphère : c'est ce qui permet
   d'écrire le placement en « angle, angle, rayon » sans jamais se
   soucier de la taille de l'image qu'il porte. */
.trash-slot {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 0;
  height: 0;
  transform-style: preserve-3d;
}

/* La fenêtre est suspendue au support et centrée dessus. Elle ne
   connaît que le plat : son inclinaison, et son agrandissement au
   premier plan.

   Sa largeur n'est pas donnée : c'est l'image qui la fixe, par ses
   max-width / max-height posées à la composition. Le cadre s'ajuste
   donc à chaque proportion — d'où le min-width remis à zéro, que
   xp.css pose pour de vraies fenêtres.

   --tilt vient de la composition : la garder en variable permet de
   la retirer d'une image recyclée sans recopier le reste de la
   transformation.

   Le rembourrage est indispensable, et sur les quatre côtés :
   xp.css n'en pose qu'en bas (mesuré : 0/0/3px/0), et le biseau du
   cadre est dessiné par des ombres INTERNES. Sans marge, l'image
   occupe toute la boîte et recouvre le biseau — il ne resterait de
   cadre qu'en bas. */
.trash-piece {
  position: absolute;
  top: 0;
  left: 0;
  width: auto;
  min-width: 0;
  height: auto;
  padding: 3px;
  /* xp.css arrondit les deux coins hauts de .window (8px) : c'est la
     place de la barre de titre. Ici il n'y en a plus, donc le cadre
     est droit sur les quatre côtés. */
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  transform: translate(-50%, -50%) rotate(var(--tilt));
  transition: filter 0.25s ease;
  user-select: none;
}

.trash-piece img {
  display: block;
  width: auto;
  height: auto;
}

/* L'ombre passe par filter et non par box-shadow : sur .window,
   box-shadow n'est pas décoratif, c'est lui qui dessine tout le
   biseau rose du cadre. Une ombre portée écrite là l'effacerait. */
.trash-piece:hover { filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.35)); }

/* La visionneuse. Elle couvre le hublot, pas la page : c'est la
   sphère qu'on met en retrait, pas le bureau.

   Le blanc monte du bas et se dissipe vers le haut, le flou porte
   sur toute la surface. Les deux se cumulent là où backdrop-filter
   existe, et le dégradé tient seul le rôle ailleurs — un fond flouté
   qui disparaîtrait faute de support laisserait la sphère en pleine
   lumière derrière l'image. */
.trash-view {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: linear-gradient(to top, #fff 0%, rgba(255, 255, 255, 0.92) 32%, rgba(255, 255, 255, 0) 88%);
  backdrop-filter: blur(6px);
  cursor: default;
}

/* Indispensable : le display:none de l'attribut hidden vient de la
   feuille du navigateur, que n'importe quelle règle d'auteur
   surclasse. Sans cette ligne, le display:flex ci-dessus l'emporte
   et la visionneuse reste ouverte en permanence — vide, et
   impossible à refermer puisqu'elle masque ce qu'on voudrait
   cliquer. */
.trash-view[hidden] { display: none; }

/* Assez grande pour qu'on voie l'image, assez petite pour qu'on
   sente le décor derrière — c'est le propos de la visionneuse. */
.trash-view-figure {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: min(58%, 480px);
  max-height: 100%;
  min-height: 0;
  margin: 0;
}

/* flex:1 + min-height:0 + overflow:hidden, dans cet ordre : c'est ce
   qui donne au média une hauteur DÉFINIE, celle qui reste une fois
   la légende servie. Sans elle, le max-height de l'image ne se
   réfère à rien, l'image garde sa taille naturelle et déborde par
   le bas — elle passait par-dessus sa propre légende. */
.trash-view-media {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  justify-content: center;
  min-height: 0;
  overflow: hidden;
}

.trash-view-media img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.28);
}

.trash-view-fill { width: 260px; height: 200px; }

/* La légende porte deux lignes : l'auteur, puis le nom du fichier.
   Elles sont empilées par le flux — display:block sur des span — et
   non par un flex : le bloc doit pouvoir rétrécir à une seule ligne
   quand le nom de fichier manque, sans laisser de vide. */
.trash-legend {
  flex-shrink: 0;
  margin: 12px 0 0;
  color: #222;
  font: 11px Arial, sans-serif;
  text-align: center;
}

.trash-author {
  display: block;
  color: #000;
  font-size: 13px;
  font-weight: bold;
}

/* Le nom de fichier est une donnée, pas une phrase : en monospace il
   se lit caractère par caractère, comme dans un explorateur. Il peut
   être long et sans espaces — d'où la césure, sans quoi il
   élargirait la visionneuse au lieu de passer à la ligne. */
.trash-filename {
  display: block;
  margin-top: 3px;
  max-width: 34em;
  color: #555;
  font-family: "Courier New", monospace;
  font-size: 11px;
  overflow-wrap: anywhere;
}

.trash-legend:empty { display: none; }

.trash-empty { padding: 20px; color: #555; text-align: center; font-family: Arial, sans-serif; }

/* =========================================================
   Resize desktop
   ========================================================= */
.resize-handle { position: absolute; z-index: 6; }
.resize-handle.n { top: -3px; right: 8px; left: 8px; height: 6px; cursor: n-resize; }
.resize-handle.s { right: 8px; bottom: -3px; left: 8px; height: 6px; cursor: s-resize; }
.resize-handle.e { top: 8px; right: -3px; bottom: 8px; width: 6px; cursor: e-resize; }
.resize-handle.w { top: 8px; bottom: 8px; left: -3px; width: 6px; cursor: w-resize; }
.resize-handle.ne { top: -3px; right: -3px; width: 12px; height: 12px; cursor: ne-resize; }
.resize-handle.nw { top: -3px; left: -3px; width: 12px; height: 12px; cursor: nw-resize; }
.resize-handle.se { right: -3px; bottom: -3px; width: 12px; height: 12px; cursor: se-resize; }
.resize-handle.sw { bottom: -3px; left: -3px; width: 12px; height: 12px; cursor: sw-resize; }

/* =========================================================
   Barre basse
   ---------------------------------------------------------
   Elle passe au-dessus de tout le reste : fenêtres desktop
   (z-index attribué par focusWindow, plafonné à 900), fenêtres
   mobiles (999) et flou d'arrière-plan mobile (998).
   Seuls le loading screen et le BIOS (2000) restent devant.
   ========================================================= */
#bottom-bar {
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 1500;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 32px;
  padding: 3px 12px;
  color: #fff;
  background: linear-gradient(#ee97ec, #f6d3f2);
  border-top: 1px solid #ee97ec;
  font-family: "MS1", Arial, sans-serif;
  font-size: 1rem;
  text-shadow: 1px 1px #50004e;
}

/* =========================================================
   Mobile
   ========================================================= */
.mobile-window,
.mobile-small-window,
#mobile-blur-overlay { display: none; }

@media (max-width: 768px), (pointer: coarse) {
  html, body { cursor: auto; }
  .resize-handle { display: none !important; }

  /* 5 colonnes x 3 lignes, dont une vide (la 2) qui sépare les deux
     groupes : Portfolio / About / Contact d'un côté, tout le reste
     de l'autre.

     Cinq colonnes parce que le second groupe compte cinq icônes et
     tient sur une seule rangée, comme sur le bureau. C'est serré sur
     un petit écran — d'où les libellés qui peuvent passer à la
     ligne, et la hauteur minimale qui garde les rangées alignées.

     La ligne vide n'a pas d'icône, donc pas de hauteur propre : les
     28px sont la seule valeur à retoucher pour resserrer ou élargir
     la respiration (les 18px de gap s'ajoutent de part et d'autre). */
  #grid-menu {
    position: absolute;
    top: 22px;
    right: 10px;
    left: 10px;
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    grid-template-rows: auto 28px auto;
    gap: 18px 4px;
  }
  .icon-row { display: contents; }
  .icon-row-spacer { display: none; }

  .icon-menu { width: auto; min-width: 0; min-height: 76px; padding: 3px 1px; cursor: pointer; }
  .icon-menu img { width: 44px; height: 44px; }
  /* À cinq colonnes, « website.info » ne tient plus sur une ligne en
     dessous de ~360px de large. Il passe à la ligne plutôt que de
     déborder — d'où la césure autorisée au milieu d'un mot, ces
     libellés n'ayant pas toujours d'espace où couper. */
  .icon-menu span { font-size: 11px; line-height: 1.15; overflow-wrap: anywhere; }

  /* Chaque icône est placée explicitement par sa clé, en ligne /
     colonne. La grille se lit donc ici, telle qu'elle s'affiche, au
     lieu de se déduire de l'ordre du HTML.

     Deux autres pistes écartées : réordonner le HTML déplacerait
     aussi les icônes du bureau, et `order` désynchroniserait
     l'ordre de tabulation sans rendre la grille plus lisible. */
  .icon-menu[data-key="portfolio"]    { grid-area: 1 / 1; }
  .icon-menu[data-key="about"]        { grid-area: 1 / 2; }
  .icon-menu[data-key="contact"]      { grid-area: 1 / 3; }

  .icon-menu[data-key="liens"]        { grid-area: 3 / 1; }
  .icon-menu[data-key="console"]      { grid-area: 3 / 2; }
  .icon-menu[data-key="captcha"]      { grid-area: 3 / 3; }
  .icon-menu[data-key="corbeille"]    { grid-area: 3 / 4; }
  .icon-menu[data-key="restart"]      { grid-area: 3 / 5; }
  .mobile-window.open {
    position: fixed;
    top: 36px;
    right: 36px;
    bottom: 68px;
    left: 36px;
    z-index: 999;
    display: flex;
  }
  /* .window de xp.css est un bloc : le flex:1 posé plus bas sur le
     corps y était inerte, le corps prenait sa hauteur de contenu et
     débordait du panneau au lieu de scroller. En colonne flex, il
     remplit la place laissée par la barre de titre et scrolle.

     position:relative en fait aussi le repère des flèches de la
     corbeille, positionnées en absolu par-dessus le média. */
  /* Sélecteur d'enfant direct, et non de descendant : la corbeille
     pose de vraies .window à l'intérieur du panneau, une par image,
     et un descendant les étirait toutes à 100 % de leur support —
     qui est un point sans dimension. Elles disparaissaient. */
  .mobile-window.open > .window {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
  }
  .mobile-window.open > .window > .title-bar { flex-shrink: 0; }
  /* Le corps mobile a exactement le retrait du desktop : 3px de
     chaque côté, la largeur du biseau de .window, et rien de plus.

     L'ancienne marge de 8px (--body-inset) obligeait chaque bloc
     qui doit filer bord à bord — en-tête de liste, barre de la
     corbeille — à la reprendre en négatif. Or une marge négative
     dans un conteneur en overflow-y:auto élargit l'enfant au-delà
     de la boîte de contenu : overflow-x passe implicitement à auto,
     le contenu se décale et déborde. D'où la marge « bizarre ».

     La respiration des contenus texte est désormais posée par le
     contenu lui-même (.links-page, règles About et
     fiche projet ci-dessous), là où elle est visible. */
  .mobile-window .window-body {
    flex: 1;
    min-height: 0;
    margin: 0 3px;
    padding: 0px;
    overflow-x: hidden;
    overflow-y: auto;
    background: #fff;
  }

  /* Les seuls contenus sans padding propre. */
  .mobile-window.about-window .window-body { padding: 12px; }
  /* Le rembourrage haut est revenu en même temps que .project-nav-links
     est passé en pied : plus rien ne se colle en haut du corps. */
  .mobile-window.project-page .window-body { padding: 12px; }

  /* Le site occupe tout le corps : c'est lui qui défile, dans son
     iframe, pas le corps. position:relative sert de repère au
     message de repli placé derrière. */
  .mobile-window.site-page .window-body {
    position: relative;
    overflow: hidden;
  }

  /* 312px ne suffisent pas pour deux libellés ET le titre : quand le
     titre prend le relais, les voisins se réduisent à leur flèche. */
  .mobile-window .project-nav-links.shows-title .project-nav-label { display: none; }
  /* Réduits à leur flèche, les boutons n'ont plus à tenir la largeur
     minimale d'un bouton XP (72px) : elle repousserait le titre. */
  .mobile-window .project-nav-links.shows-title .project-nav-link { min-width: 0; }
  /* Les flèches seules laissent au titre presque toute la largeur. */
  .mobile-window .project-nav-title { max-width: 72%; }

  .mobile-window .project-list-row { padding-right: 8px; padding-left: 8px; }
  .mobile-window .project-list-row img { width: 28px; height: 28px; }
  .mobile-window .project-list-name { font-size: 12px; }

  .captcha-window.mobile-small-window .window-body { margin: 0 3px; padding: 10px; background: #ece9d8; }

  .card-scene { padding: 16px; }

  #mobile-blur-overlay.active {
    position: fixed;
    inset: 0;
    z-index: 998;
    display: block;
    background: rgba(0, 0, 0, 0.2);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
  }

  .mobile-small-window {
    position: fixed;
    top: 50%;
    left: 50%;
    z-index: 999;
    display: block;
    width: min(88vw, 340px);
    max-height: calc(100vh - 90px);
    transform: translate(-50%, -50%);
  }
  .mobile-small-window .window { width: 100%; height: auto; max-height: calc(100vh - 90px); }
  .mobile-small-window .window-body { max-height: calc(100vh - 150px); padding: 10px; overflow-y: auto; }

  .project-image-item.portrait { width: calc(50% - 6px); }
}

@media (max-width: 390px) {
  .project-image-item.portrait { width: 100%; }
}
