/* app.css — mood sombre & dense (signature Phil), clair par découpe.
   Phone-first mesuré à 390 px : le dock devient bottom sheet. */
:root {
  --bg: #0b0d10; --bg2: #12151a; --panel: #171b21; --line: #242a33;
  --ink: #e8e6e1; --ink-dim: #8a8f98; --accent: #c94f7c; --accent2: #4f9cc9;
  --ok: #4fc98a; --warn: #c9a44f; --danger: #c95555;
  --radius: 12px;
}
@font-face {
  font-family: "GenPix Display";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(../fonts/chakrapetch-700-latin.woff2) format("woff2");
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body, #app { height: 100%; }
body {
  background: var(--bg); color: var(--ink);
  font: 15px/1.5 -apple-system, "Segoe UI", Roboto, sans-serif;
  overflow: hidden;
}

/* ── L'ÉCHELLE DE LA PAGE, SUR TÉLÉPHONE (Phil, 15/09) ───────────────────────
   « désactive le zoom à deux doigts, la page sort du cadre de l'iPhone,
   surtout après avoir envoyé un message ». Trois portes, trois serrures :
   · le double-tap → `touch-action: manipulation` (ici) ;
   · le pincement → refusé en JS (app.js, garde `gesturestart`) ; iOS ignore
     `user-scalable=no` depuis iOS 10, la balise seule ne retient rien ;
   · le zoom AUTOMATIQUE à la mise au point d'un champ — la vraie cause du
     « après avoir envoyé un message » : Safari agrandit toute la page dès
     qu'un champ a une police < 16 px, et ne revient jamais à 1. `#chat-input`
     héritait les 15 px du corps : chaque appui sur la barre d'écriture
     agrandissait la page de 7 %, et elle y restait. */
html { touch-action: manipulation; -webkit-text-size-adjust: 100%; }
html, body { overflow-x: clip; }   /* on ne tire plus la page sur le côté */
body { overscroll-behavior: none; }
@media (pointer: coarse) {
  input[type=text], input[type=email], input[type=password], input[type=number],
  input[type=search], input[type=tel], input[type=url],
  input:not([type]), textarea, select, #chat-input { font-size: 16px !important; }
}

.hidden { display: none !important; }
.view { height: 100%; display: flex; align-items: center; justify-content: center; }

/* ── Cartes porte / auth ── */
.gate-card {
  width: min(440px, 92vw); background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 36px 32px; text-align: center;
  box-shadow: 0 30px 80px rgba(0,0,0,.55);
  animation: card-in .5s cubic-bezier(.2,.9,.3,1) both;
}
@keyframes card-in { from { opacity: 0; transform: translateY(14px) scale(.985); } }
.brand { font-size: 26px; letter-spacing: .35em; font-weight: 700; }
.brand-dot { color: var(--accent); }
.gate-sub { color: var(--ink-dim); margin: 12px 0 26px; }
.gate-q { font-size: 18px; margin-bottom: 18px; }
.gate-btns { display: flex; flex-direction: column; gap: 10px; }
.gate-teen { margin-top: 18px; color: var(--ink-dim); background: var(--bg2);
  border: 1px solid var(--line); border-radius: 8px; padding: 12px; font-size: 13px; }
.gate-legal { margin-top: 22px; font-size: 11.5px; color: var(--ink-dim); }
.gate-legal a, .check-row a { color: var(--accent2); }

.btn { border: 0; border-radius: 9px; padding: 12px 18px; font-size: 15px;
  cursor: pointer; transition: transform .12s, filter .12s; }
.btn:active { transform: scale(.97); }
.btn-main { background: var(--accent); color: #fff; }
.btn-main:hover { filter: brightness(1.12); }
.btn-ghost { background: transparent; color: var(--ink-dim); border: 1px solid var(--line); }
.btn-ghost:hover { color: var(--ink); }

/* La porte GenPix est un lien habillé en bouton : il lui faut le bloc et
   la ligne de texte que <button> a d'office. */
.auth-genpix { display: block; text-align: center; text-decoration: none; margin: 18px 0 6px; }

.tabs { display: flex; gap: 6px; margin: 20px 0 16px; }
.tab { flex: 1; background: var(--bg2); border: 1px solid var(--line); color: var(--ink-dim);
  padding: 9px; border-radius: 8px; cursor: pointer; }
.tab.active { color: var(--ink); border-color: var(--accent); }
#auth-form { display: flex; flex-direction: column; gap: 10px; text-align: left; }
#auth-form input[type=email], #auth-form input[type=password] {
  background: var(--bg2); border: 1px solid var(--line); border-radius: 8px;
  color: var(--ink); padding: 11px 13px; font-size: 15px; }
#auth-form input:focus { outline: none; border-color: var(--accent2); }
.check-row { display: flex; gap: 8px; align-items: flex-start; font-size: 13px; color: var(--ink-dim); }
.form-err { color: var(--danger); font-size: 13px; min-height: 18px; }

/* ── Builder ── */
/* Le builder est plus haut que l'écran dès qu'on ouvre le vestiaire et la voix.
   Un enfant centré par `align-items:center` dans une boîte qui ne défile pas
   déborde par les DEUX bouts : le haut passe sous le bord (inatteignable, on ne
   peut pas remonter) et le bas sort de la page. La vue devient donc le
   défileur, et le centrage passe par `margin:auto` — qui centre quand il y a de
   la place et ne rogne jamais quand il n'y en a plus.
   La même règle vaut pour la porte et l'auth (cartes hautes sur un écran
   court) — #view-main garde sa mise en page propre. */
#view-builder, #view-auth {
  align-items: flex-start; overflow-y: auto; overscroll-behavior: contain;
  padding: 24px 0; scrollbar-width: thin; }
.gate-card { margin: auto; flex-shrink: 0; }
/* ── LA FENETRE EST LA MESURE, PAS UNE VALEUR ECRITE D'AVANCE (Phil, 15/09) ──
   « in PC version better to use the whole page or at least make it dynamic
   based on the browser window ». Mesure AVANT correction, fenetre 1440x789 :
   le builder occupait 860 px (60 % de la largeur), la fiche etait tassee dans
   320 px — « Her voice » et la note du rendu passaient a la ligne — et le
   portrait restait un cadre fige de 280x340 alors que 789 px de hauteur
   etaient libres. 580 px de noir mort, par construction.
   Tout depend maintenant de la fenetre : la largeur du cadre, la colonne des
   reglages, et la taille du portrait — celle-la en JS (`builderStage()`),
   parce que sa proportion est celle de la BANDE servie par le serveur, pas un
   2:3 decrete (meme raison que stageResize sur le dock). */
.builder-wrap { display: flex; gap: clamp(22px, 4vw, 72px); align-items: center;
  width: min(1520px, 94vw); margin: auto; flex-shrink: 0; }
.builder-preview { flex: 1 1 auto; min-width: 0; display: flex; justify-content: center; }
.builder-form { flex: 0 0 auto; width: clamp(320px, 26vw, 440px);
  display: flex; flex-direction: column; gap: 12px; }
.builder-form h2 { font-size: 19px; margin-bottom: 6px; }
/* Le libelle garde sa ligne, le controle se pose au bord droit. `space-between`
   ecartelait libelle / note / champ en trois colonnes et ecrasait le premier :
   c'est ce qui coupait « Her voice » en deux. */
.builder-form label { display: flex; align-items: center;
  gap: 10px; color: var(--ink-dim); font-size: 14px; white-space: nowrap; }
.builder-form label > .hint { white-space: normal; min-width: 0; }
/* Une note de six mots ne se glisse pas ENTRE le libelle et le champ : elle
   les ecrase en trois lignes de deux mots (mesure : « you can hear her / before
   you / decide » sur une colonne de 374 px). Marquee `under`, elle prend sa
   propre ligne SOUS la rangee — le libelle et le controle gardent la leur. */
.builder-form label { flex-wrap: wrap; }
.builder-form label > .hint.under { order: 9; flex: 1 0 100%; margin-top: -3px; }
/* Sur un ecran large la fiche est plus haute que la fenetre : le portrait la
   suit au lieu de defiler hors de vue — c'est LUI qu'on regarde en reglant. */
@media (min-width: 721px) {
  .builder-wrap { align-items: flex-start; }
  .builder-preview { position: sticky; top: 0; align-self: flex-start; }
}
.builder-form label > input[type=text], .builder-form label > input[type=number],
.builder-form label > input[type=color], .builder-form label > input[type=file],
.builder-form label > select, .builder-form label > .voice-row { margin-left: auto; }
/* Une case a cocher lit de gauche a droite : elle ne part pas au bord droit,
   et sa phrase a le droit de passer a la ligne. */
.builder-form label.check-row { white-space: normal; align-items: flex-start; }
.builder-form input[type=text], .builder-form input[type=number], .builder-form select {
  background: var(--bg2); border: 1px solid var(--line); border-radius: 8px;
  color: var(--ink); padding: 8px 10px; width: clamp(150px, 48%, 230px); font-size: 14px; }
.builder-form input[type=color] { width: 42px; height: 30px; border: 1px solid var(--line);
  border-radius: 6px; background: var(--bg2); padding: 2px; cursor: pointer; }
.hint { font-size: 12px; color: var(--ink-dim); }
.upload-row input[type=file] { width: clamp(150px, 48%, 230px); font-size: 12px; color: var(--ink-dim); }
.upload-row input[type=file]::file-selector-button {
  background: var(--bg2); border: 1px solid var(--line); border-radius: 6px;
  color: var(--ink); padding: 6px 10px; margin-right: 8px; cursor: pointer; }
.extract-note { color: var(--accent2); }

/* ── Scène avatar (2D SVG vivant) ── */
.avatar-stage { width: 100%; height: 100%; display: flex; align-items: flex-end; justify-content: center; }
/* Portrait réel par-dessus la maquette SVG. Le SVG passe en fantôme (pas
   display:none : sa boîte donne la taille de la pile).
   La TAILLE de la pile vit dans --stage-w/--stage-h : posées par stageResize()
   (JS) sur le dock — le cadre s'ouvre avec la révélation, jusqu'à ×3 de large
   en 2:3 (le format de l'image : corps entier). Le builder garde les défauts. */
.stage-stack {
  position: relative; perspective: 700px;
  /* Deux tailles se multiplient ici, et il ne faut pas les confondre :
     --stage-w/h = ce que le NIVEAU a ouvert (la révélation, écrite par
     stageResize) ; --pscale = ce que le DOIGT a demandé (un appui court fait
     le tour des trois tailles). Le geste ne touche jamais la révélation. */
  width: calc(var(--stage-w, 280px) * var(--pscale, 1));
  height: calc(var(--stage-h, 340px) * var(--pscale, 1));
  transition: width .5s cubic-bezier(.2,.9,.3,1), height .5s cubic-bezier(.2,.9,.3,1);
  -webkit-touch-callout: none; user-select: none; -webkit-user-select: none;
  cursor: pointer;
}
/* Le rebond : c'est la RÉPONSE au doigt, pas une décoration — sans lui un
   appui qui ne change presque rien (grand → moyen) passe pour un raté.
   PortraitLife écrit sur .portrait-wrap, jamais ici : deux écrivains du même
   transform, c'est le bug. */
.stage-stack.bounce { animation: pbounce .42s cubic-bezier(.2,.9,.3,1); }
@keyframes pbounce {
  0% { transform: scale(1); } 34% { transform: scale(.93); }
  68% { transform: scale(1.035); } 100% { transform: scale(1); }
}
.stage-stack .avatar-stage.ghost { visibility: hidden; }
/* La vie du cadre (respiration, pivot 3D, appui) est écrite par PortraitLife
   en JS — UN seul écrivain du transform, jamais une animation CSS en plus. */
.portrait-wrap {
  position: absolute; inset: 0; overflow: hidden; border-radius: 14px;
  border: 1px solid var(--line);
  will-change: transform;
}
.portrait-wrap img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transform: scale(1.045);          /* marge de parallaxe : jamais de bord nu */
  will-change: transform;
}
.builder-preview { flex-direction: column; align-items: center; gap: 12px; }
/* Le clip prend la place de l'image quand il existe : même cadre, même
   respiration — le portrait ne SAUTE pas, il se met à bouger. */
.portrait-wrap video { width: 100%; height: 100%; object-fit: cover; display: block; }
.portrait-wrap img.hidden, .portrait-wrap video.hidden { display: none; }
.btn-mini {
  margin-top: 8px; background: transparent; color: var(--accent);
  border: 1px solid var(--accent); border-radius: 7px;
  padding: 5px 10px; font-size: 12px; cursor: pointer;
}
.btn-mini:hover:not(:disabled) { background: var(--accent); color: #fff; }
.btn-mini:disabled { opacity: .5; cursor: default; }
.dock-actions { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; margin-top: 8px; }
.pose-row { display: flex; gap: 5px; flex-wrap: wrap; justify-content: center; margin-top: 8px; }
.pose-row .btn-mini { font-size: 11px; padding: 4px 8px; }
/* ── LES DEUX VOIES DE LA FEUILLE (Phil, 15/09 : « re organising or fusion …
   now we dont have only 2d but 3d + animation ») ───────────────────────────
   Un intertitre ne doit pas peser autant qu'un bouton : petite capitale,
   filet fin au-dessus, et la voie entière se centre comme le reste de la
   feuille. Sur un téléphone c'est ce filet, pas le texte, qui fait comprendre
   que « Standing » et « Her body » ne parlent pas de la même image. */
.dock-lane { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--line); text-align: center; }
.dock-lane:first-of-type { border-top: 0; padding-top: 0; }
.dock-lane > .btn-mini { margin-top: 8px; }
.lane-cap {
  display: block; font-size: 9.5px; letter-spacing: .09em; text-transform: uppercase;
  opacity: .62; font-weight: 700;
}
.lane-cap i { font-style: normal; text-transform: none; letter-spacing: 0; font-weight: 400; opacity: .8; }
@media (max-width: 520px) {
  /* Le téléphone est le poste de contrôle : la glose passe à la ligne au lieu
     d'étirer la feuille au-delà du cadre. */
  .lane-cap i { display: block; font-size: 10px; margin-top: 1px; }
}

/* L'OUVERTURE du cadre. La bande neuve arrive du serveur : ce qu'on anime,
   c'est la hauteur qui se déplie et une lueur qui passe — le geste dit
   « il y a plus à voir », il ne fait pas semblant de dévoiler. */
.portrait-wrap.opening { animation: frame-open 1.1s cubic-bezier(.2,.9,.3,1); }
@keyframes frame-open {
  0%   { clip-path: inset(0 0 42% 0); filter: brightness(1.35); }
  60%  { clip-path: inset(0 0 0 0);   filter: brightness(1.12); }
  100% { clip-path: inset(0 0 0 0);   filter: none; }
}

/* La boutique — posée au-dessus du dock, jamais une page à part : on offre en
   la regardant, pas dans un tunnel de paiement. */
/* EN FLUX, pas par-dessus (Phil, 15/09 : « when I click on Gift button the
   frame doesn't expand, there's plenty of stuff hidden behind it »). Posée en
   `absolute` dans un panneau de 320 px, la boutique se peignait SUR les
   réglages sans jamais pousser leur boîte : six cadeaux dans un cadre calé sur
   deux boutons. Dans le flux, le panneau grandit avec elle — c'est le contenu
   qui décide de la taille, plus une valeur écrite d'avance. */
.gift-box {
  position: relative; z-index: 5; margin-top: 10px;
  background: var(--bg2); border: 1px solid var(--accent);
  border-radius: var(--radius); padding: 11px; text-align: left;
  animation: card-in .28s cubic-bezier(.2,.9,.3,1) both;
}
.gift-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.gift {
  display: flex; align-items: center; gap: 10px; width: 100%; margin-bottom: 6px;
  background: var(--bg2); border: 1px solid var(--line); border-radius: 9px;
  padding: 8px 10px; color: var(--ink); cursor: pointer; text-align: left;
  transition: border-color .15s, transform .15s;
}
.gift:hover:not(:disabled) { border-color: var(--accent); transform: translateX(2px); }
.gift:disabled { opacity: .45; cursor: default; }
.gift-emoji { font-size: 20px; }
.gift-main { flex: 1; display: flex; flex-direction: column; font-size: 13px; }
.gift-price { color: var(--accent); font-weight: 600; font-size: 13px; }
@media (prefers-reduced-motion: reduce) {
  .portrait-wrap.opening { animation: none; }
  }
.avatar-stage svg { width: 100%; height: 100%; overflow: visible; }

/* ── LE CHAT PREND TOUTE LA PAGE, ELLE FLOTTE DESSUS (15/09) ───────────────
   Phil : « better to have a chat session frame the whole page and the companion
   profile image is just a popup on the left — tap once to reduce the size, tap
   another time to rebounce it, tap+hold to see all the options instead of
   putting every option or setting on the page ».
   Donc le dock SORT du flux : il ne prend plus de colonne (bureau) ni de moitié
   d'écran (téléphone), il se pose au-dessus, en bas à gauche, au-dessus du
   champ de saisie. Le réglage n'est plus AFFICHÉ, il est TENU : appui long. */
#view-main { display: flex; align-items: stretch; justify-content: stretch; position: relative; }
#dock {
  position: absolute; left: 12px; bottom: var(--dock-bottom, 92px); z-index: 20;
  width: auto; min-width: 0; background: none; border-right: 0;
  display: flex; flex-direction: column; align-items: flex-start; gap: 7px;
  transition: bottom .25s ease;
}
/* Le portrait porte sa propre ombre maintenant qu'il n'a plus de colonne
   derrière lui : sans elle il flotte SUR le texte sans s'en détacher. */
.stage-stack .portrait-wrap { box-shadow: 0 18px 44px rgba(0,0,0,.6); }
/* La poignée « < » n'a plus de rôle : réduire, c'est un appui court. */
#dock-toggle { display: none; }

/* Les réglages : un panneau tenu par l'appui long, ancré au portrait, jamais
   posé dans la page. Fermé, il ne coûte rien ; ouvert, il ne pousse rien. */
.dock-meta {
  /* Ancré au BAS du portrait, pas au-dessus : le haut du dock monte avec la
     taille du cadre, donc un panneau posé au-dessus finirait hors de l'écran à
     la plus grande taille. En bas, il est toujours visible — la base du dock
     l'est toujours, elle est calée au-dessus du champ de saisie. */
  position: absolute; left: calc(100% + 10px); bottom: 0; z-index: 2;
  display: none; text-align: center;
  /* La taille suit le CONTENU : ces bornes sont un garde-fou d'écran, pas une
     mise en page. Le plafond est mesuré contre la fenêtre (72 vh) parce qu'un
     panneau plus haut que l'écran n'a jamais de bas atteignable. */
  min-width: 212px; width: max-content;
  max-width: min(370px, 92vw); max-height: min(72vh, 620px);
  overflow-y: auto; overscroll-behavior: contain;
  padding: 12px 13px 13px; background: var(--panel);
  border: 1px solid var(--line); border-radius: 14px;
  box-shadow: 0 26px 70px rgba(0,0,0,.66);
  animation: meta-in .22s cubic-bezier(.2,.9,.3,1) both;
}
.dock-meta.open { display: block; }
@keyframes meta-in { from { opacity: 0; transform: translateY(8px) scale(.97); } }
.meta-close {
  position: absolute; top: 6px; right: 7px; width: 22px; height: 22px; padding: 0;
  background: transparent; border: 1px solid var(--line); border-radius: 7px;
  color: var(--ink-dim); font-size: 13px; line-height: 1; cursor: pointer;
}
/* Le pli du téléphone n'a plus lieu d'être : le panneau EST le pli. */
.dock-more { display: none; }
.dock-fold { display: block; }
/* L'indice du geste : montré une fois, à la première ouverture de la salle. */
.press-hint {
  position: absolute; left: 0; bottom: calc(100% + 9px); z-index: 1;
  background: var(--panel); border: 1px solid var(--line); border-radius: 9px;
  padding: 6px 9px; font-size: 11px; color: var(--ink-dim); white-space: nowrap;
  animation: hint-fade 4.6s ease both; pointer-events: none;
}
@keyframes hint-fade { 0% { opacity: 0; } 12%, 74% { opacity: 1; } 100% { opacity: 0; } }

/* La gouttière gauche de la conversation (la place du portrait flottant) est
   posée avec le reste du padding de #messages, plus bas : un seul propriétaire. */

/* `min-height: 0` n'est PAS décoratif : sans lui un enfant flex refuse de
   descendre sous la taille de son contenu (min-height:auto). Avec un long
   historique, #messages gardait ses 3 602 px, #chat-pane passait à 3 723, et en
   pile (téléphone) tout le haut — LE HEADER — partait au-dessus de l'écran :
   mesuré à -3 651 px. Maintenant la liste défile et le header reste posé. */
#chat-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
#messages { min-height: 0; }
/* ── LE HEADER TIENT SUR UNE LIGNE, QUELLE QUE SOIT LA LARGEUR (15/09) ─────
   Phil : « if you see the header it should do whatever to keep only 1 row ».
   Mesuré à 496 px avant correction : contenu 526 px pour 496 px de place —
   « Lv. » passait à la ligne, « IMAGES LEFT » aussi, le quota tenait sur trois
   lignes et le dernier bouton sortait du cadre. Deux décisions :
   1. Plus rien ne se replie : `nowrap` partout — un débordement se VOIT au
      lieu de fabriquer une deuxième ligne (et rien n'est rogné ici : le menu
      profil est ancré dans ce header, un `overflow:hidden` l'effacerait).
   2. L'échelle de densité (.d1 → .d6, posée par `fitHud()` d'après une MESURE,
      pas d'après une media query) retire le superflu palier par palier :
      texte d'xp → quota abrégé → étiquette du budget → crayon → salle → jauges.
   Rien de ce qui disparaît n'est perdu : crayon et salle vivent aussi dans le
   menu profil, le budget et le quota gardent leur infobulle. */
#hud {
  display: flex; justify-content: space-between; align-items: center;
  flex-wrap: nowrap; gap: 10px;
  padding: 9px 12px; border-bottom: 1px solid var(--line); background: var(--bg2);
}
.hud-left { display: flex; align-items: center; gap: 9px; flex-wrap: nowrap; min-width: 0; }
/* Les deux blocs ne rétrécissent pas non plus : sinon ils se laissent comprimer
   DANS la gouttière du header (mesuré à 390 px : le bouton profil finissait à
   386 px pour un bord à 378) et `scrollWidth` n'y voit rien — il ne compte que
   ce qui dépasse la boîte de remplissage. Rigides, le débordement redevient
   mesurable, donc l'échelle de densité le voit. */
.hud-left, .hud-right { flex: 0 0 auto; }
.hud-left > *, .hud-right > * { flex: 0 0 auto; }
.hud-level, .hud-xp { white-space: nowrap; }
.hud-level b { color: var(--accent); }
.xp-bar { width: 140px; height: 6px; background: var(--line); border-radius: 3px; overflow: hidden; }
#xp-fill { height: 100%; width: 0; background: linear-gradient(90deg, var(--accent), var(--accent2));
  transition: width .6s cubic-bezier(.2,.9,.3,1); }
.hud-xp { font-size: 12px; color: var(--ink-dim); }
.lv-word { font-style: normal; }
.hud-right { display: flex; align-items: center; gap: 6px; flex-wrap: nowrap; }

/* Les paliers. Cumulatifs : au palier 3 l'élément porte d1, d2 ET d3. */
#hud.d1 .hud-xp { display: none; }
/* Palier 2 : les deux mots tombent d'un coup — l'icône DIT déjà lequel est
   lequel (🖼 / 💬), et l'infobulle porte la phrase entière. Il monte d'un cran
   par rapport à avant : le header porte deux réservoirs maintenant, l'échelle
   doit gagner de la place plus tôt.
   (Les anciennes règles .q-long/.q-short sont parties avec la ligne de texte
   qu'elles habillaient : le compteur de messages est une jauge maintenant.) */
#hud.d2 .energy-head b { display: none; }
#hud.d2 .energy-head { gap: 0; }
/* Le budget passe en une ligne : l'icône reste (elle DIT « images »),
   l'étiquette part, la jauge et le nombre restent — la bougie ne disparaît
   jamais avant le palier 6, Phil l'a voulue aussi lisible que l'xp. */
#hud.d3 .gauge-wrap { display: flex; align-items: center; gap: 6px; min-width: 0;
  padding: 4px 6px 4px 8px; }
#hud.d3 .energy-meter { width: 64px; flex: none; }
#hud.d4 .xp-bar { width: 84px; }
#hud.d4 #btn-edit { display: none; }
/* Palier 5 réglé sur l'iPhone : à 390 px la boîte de contenu fait 368 et le
   palier demandait 409 — 21 px de trop, et l'écran retombait au palier 6 (sans
   « Lv. », sans jauge d'images). Barres resserrées : 362 px, l'iPhone garde le
   header complet. */
#hud.d5 .xp-bar { width: 46px; }
#hud.d5 #btn-roster { display: none; }
/* Palier 5 — RE-MESURÉ le 15/09, le jour où le header a gagné un deuxième
   réservoir. À 390 px le palier 5 demandait 471 px pour 366 disponibles :
   l'iPhone retombait au palier 6, donc SANS les deux jauges — exactement ce
   que Phil avait fait corriger (« make it like the lv. bar »). En une ligne
   plate, deux boîtes coûtent 292 px à elles seules.
   Alors on remonte la boîte : elle redevient la grille à deux rangs du
   bureau (nombre au-dessus, barre en dessous, ＋ à droite sur les deux rangs),
   sa largeur devient celle du nombre au lieu de la somme icône + barre +
   nombre. Mesuré après : 330 px pour 366 — l'iPhone tient sur UNE ligne et
   garde ses deux jauges. La hauteur ne bouge pas (51 px, deux rangs internes,
   comme sur grand écran). */
#hud.d5 .gauge-wrap { display: grid; grid-template-columns: auto 24px;
  grid-template-rows: auto auto; column-gap: 5px; row-gap: 2px;
  padding: 3px 4px 3px 6px; }
#hud.d5 .energy-head { display: none; }
#hud.d5 .energy-meter { grid-column: 1; grid-row: 2; width: 100%; height: 5px; }
#hud.d5 .hud-energy { grid-column: 1; grid-row: 1; font-size: 11px; text-align: left; }
#hud.d5 .gauge-topup { grid-column: 2; grid-row: 1 / 3; width: 24px; height: 24px;
  flex: 0 0 24px; font-size: 13px; }
/* Dernier palier : il ne reste que des CHIFFRES — le niveau, les images, les
   messages — et leurs infobulles portent les phrases entières. */
#hud.d6 .lv-word { display: none; }
#hud.d6 .gauge-wrap { padding: 4px 5px 4px 6px; }
#hud.d6 .energy-meter { display: none; }
#hud.d6 .xp-bar { width: 44px; }
/* Un <button> ne centre PAS un enfant en display:block : l'icône partait à
   gauche (et le glyphe ✎ montait sur sa ligne de base). Même grille centrée
   que .btn-mic — la boîte ne dépend plus du contenu, glyphe ou SVG. */
.btn-icon { background: transparent; border: 1px solid var(--line); color: var(--ink-dim);
  border-radius: 8px; width: 28px; height: 28px; flex: 0 0 28px; cursor: pointer;
  display: grid; place-items: center; padding: 0; line-height: 1;
  font-size: 15px; overflow: hidden; }
/* Le compteur déborde du bouton : celui-là ne rogne pas ses enfants. */
#btn-roster { overflow: visible; }
.btn-icon:hover { color: var(--ink); border-color: var(--ink-dim); }
/* ── STUDIO | COMPANION — le bouton soudé, côté companion (15/09) ──────────
   Même pièce que .gp-duo de GenPix (theme.css), redéclarée ici parce que les
   deux apps ne partagent pas leur CSS. Le header est saturé de jauges, donc
   Phil l'a voulu « en haut à gauche du cadre image » : il est ancré au dock
   (position:relative), et part avec lui quand on replie le cadre — replier,
   c'est demander l'écran nu. Le jaune est la MARQUE, identique aux deux thèmes. */
.gp-duo {
  position: relative; isolation: isolate;
  display: inline-flex; align-items: stretch;
  border-radius: 999px; overflow: hidden;
  /* Anneau sombre : détache la pastille même sur un fond de la même couleur. */
  box-shadow: 0 0 0 1.5px rgba(0, 0, 0, .34), 0 2px 12px rgba(0, 0, 0, .28);
  /* Propriétés séparées, pas le raccourci `font:` : côté GenPix le même
     raccourci portait un var() absent et s'annulait en entier (theme.css). */
  font-size: 12px; font-weight: 700; line-height: 1; letter-spacing: .02em;
}
.gp-duo-h { display: grid; place-items: center; padding: 8px 13px; position: relative;
  text-decoration: none; white-space: nowrap; transition: filter .18s ease; }
.gp-duo-h.gp-duo-s { background: #fcee0a; color: #000; padding-right: 31px; z-index: 0;
  font-family: "GenPix Display", "Arial Narrow", sans-serif; }
.gp-duo-h.gp-duo-c { background: #c94f7c; color: #fff; z-index: 1; }

/* ── La jointure liquide (15/09) ───────────────────────────────────────────
   Même pièce que GenPix (theme.css) : la moitié rose passe SOUS la jaune et se
   découpe sur un bord ondulant, plus une gouttelette détachée. Phil voulait de
   l'eau entre les deux couleurs, pas un trait. Classes préfixées `gp-duo-` :
   `.studio` / `.companion` nus percutaient des règles de page. */
.gp-duo-h.gp-duo-c {
  margin-left: -16px; padding-left: 28px;
  /* Colonne de disques qui se répète et déborde dans le jaune, plus un
     rectangle pour le reste : un polygone donnait des dents de scie, un disque
     donne une courbe. Le masque défile d'une tuile exactement, en linéaire. */
  --lobe: radial-gradient(circle 8px at 8px 50%, #000 96%, rgba(0, 0, 0, 0) 100%);
  -webkit-mask-image: var(--lobe), linear-gradient(#000 0 0);
          mask-image: var(--lobe), linear-gradient(#000 0 0);
  -webkit-mask-size: 16px 14px, calc(100% - 8px) 100%;
          mask-size: 16px 14px, calc(100% - 8px) 100%;
  -webkit-mask-repeat: repeat-y, no-repeat;
          mask-repeat: repeat-y, no-repeat;
  -webkit-mask-position: 0 0, 100% 0;
          mask-position: 0 0, 100% 0;
  animation: gp-duo-flow 4.6s linear infinite;
}
@keyframes gp-duo-flow {
  to { -webkit-mask-position: 0 14px, 100% 0; mask-position: 0 14px, 100% 0; }
}
.gp-duo-h.gp-duo-s::after {
  content: ''; position: absolute; right: 21px; top: 50%;
  width: 6px; height: 6px; background: #c94f7c; opacity: .9;
  border-radius: 60% 40% 55% 45% / 50% 60% 40% 50%;
  transform: translateY(-50%); pointer-events: none;
  animation: gp-duo-drop 5.3s ease-in-out infinite;
}
@keyframes gp-duo-drop {
  0%, 100% { transform: translate(0, -50%) scale(1);
             border-radius: 60% 40% 55% 45% / 50% 60% 40% 50%; }
  30%      { transform: translate(-3px, -78%) scale(.78);
             border-radius: 45% 55% 40% 60% / 60% 45% 55% 40%; }
  62%      { transform: translate(-1px, -24%) scale(1.15);
             border-radius: 55% 45% 60% 40% / 40% 55% 45% 60%; }
}
.gp-duo:hover .gp-duo-h.gp-duo-c { animation-duration: 1.9s; }
.gp-duo:hover .gp-duo-h.gp-duo-s::after { animation-duration: 2.1s; }
@media (prefers-reduced-motion: reduce) {
  .gp-duo-h.gp-duo-c, .gp-duo-h.gp-duo-s::after { animation: none; }
}
.gp-duo-h:hover { filter: brightness(1.1); }
/* Éteinte sans être boueuse : on baisse la lumière, on garde la couleur. */
.gp-duo-h.here { filter: saturate(.8) brightness(.84); pointer-events: none; cursor: default; }
/* La pastille soudée est une VIGNETTE sur le cadre tant qu'il y a du cadre ;
   à la plus petite taille elle serait plus large que lui, donc elle repasse
   dans le flux, au-dessus. Elle ne disparaît jamais : c'est la passerelle. */
.dock-duo { position: absolute; top: 9px; left: 9px; z-index: 5; }
#dock[data-size="s"] .dock-duo { position: static; }

#crisis-banner {
  background: #2a1f27; border-bottom: 1px solid var(--accent);
  padding: 12px 18px; font-size: 13.5px; line-height: 1.6;
}
#crisis-banner a { color: var(--accent2); font-weight: 600; }

#messages { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 12px;
  padding: 22px 22px 10px;
  transition: padding .5s cubic-bezier(.2,.9,.3,1); }
/* ── La place du portrait flottant : deux réponses, une par écran ──────────
   Sur un écran large il y a de la marge à gauche : on creuse une gouttière et
   rien ne passe jamais sous elle. Sur un téléphone, une gouttière de 150 px
   sur 390 ampute la conversation d'un tiers pour une pastille posée EN BAS —
   alors c'est le BAS qu'on dégage : la conversation s'arrête au-dessus d'elle,
   bornée à 38 % de la hauteur (au-delà, c'est qu'on l'a agrandie exprès pour
   la regarder, et un appui la réduit). */
@media (min-width: 721px) {
  #messages { padding-left: calc(var(--float-w, 0px) + 22px); }
}
@media (max-width: 720px) {
  #messages { padding-bottom: min(calc(var(--float-h, 0px) + 14px), 38vh); }
}
.msg { max-width: 72%; padding: 10px 14px; border-radius: 14px; white-space: pre-wrap; word-wrap: break-word;
  animation: msg-in .3s cubic-bezier(.2,.9,.3,1) both; }
@keyframes msg-in { from { opacity: 0; transform: translateY(8px); } }
.msg.user { align-self: flex-end; background: var(--accent); color: #fff; border-bottom-right-radius: 4px; }
.msg.ai { align-self: flex-start; background: var(--panel); border: 1px solid var(--line); border-bottom-left-radius: 4px; }
.msg.sys { align-self: center; background: transparent; color: var(--ink-dim); font-size: 12.5px; }
.msg.guard { align-self: flex-start; background: #241a1a; border: 1px solid var(--danger); }
.level-up { align-self: center; color: var(--ok); font-size: 13px; letter-spacing: .12em;
  animation: msg-in .5s both; }

/* ── L'attente : trois points qui sautent en vague + le mot (Phil, 15/09 :
   « i dont want to see a dumb "..." »). Le « … » statique ne dit rien — il
   ressemble à un message vide. Ici l'attente est un GESTE : la vague avance
   toujours dans le même sens (gauche → droite), donc l'œil lit « ça avance »,
   et le mot respire à un autre rythme que les points pour qu'aucun des deux
   ne devienne un clignotant régulier qu'on cesse de voir. */
.msg.thinking { display: inline-flex; align-items: center; gap: 9px;
  padding: 11px 15px; color: var(--ink-dim); font-style: italic; }
.tdots { display: inline-flex; align-items: center; gap: 4px; height: 12px; }
.tdot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
  animation: tdot-wave 1.15s cubic-bezier(.35,0,.3,1) infinite; }
.tdot:nth-child(2) { animation-delay: .13s; }
.tdot:nth-child(3) { animation-delay: .26s; }
@keyframes tdot-wave {
  0%, 62%, 100% { transform: translateY(0); opacity: .38; }
  31% { transform: translateY(-6px); opacity: 1; }
}
.tword { font-size: 13px; letter-spacing: .02em;
  animation: tword-breathe 2.6s ease-in-out infinite; }
@keyframes tword-breathe { 0%, 100% { opacity: .55; } 50% { opacity: .95; } }

/* ── La bougie du budget images (HUD, à côté du niveau) ── */
/* À droite, avec les quotas du jour — séparée du niveau (Phil, 14/09 : « the
   lvl and the budget should be clearly separate ») ; un filet la détache des
   icônes de compte. */
.gauge-wrap { display: grid; grid-template-columns: auto 96px auto 28px;
  grid-template-rows: auto auto; align-items: center; column-gap: 7px; row-gap: 1px;
  min-width: 0; padding: 4px 7px 4px 9px; border: 1px solid var(--line);
  border-radius: 9px; background: var(--panel); }
.energy-head { grid-column: 1 / 3; display: flex; align-items: center; gap: 5px;
  color: var(--ink-dim); font-size: 10px; letter-spacing: .06em; text-transform: uppercase; }
.energy-ico { font-size: 12px; opacity: .9; }
.energy-meter { grid-column: 1 / 3; width: 100%; height: 6px; background: var(--line);
  border-radius: 3px; overflow: hidden; }
#energy-fill { display: block; height: 100%; width: 100%;
  background: linear-gradient(90deg, var(--accent2), var(--accent));
  transition: width .6s cubic-bezier(.2,.9,.3,1), background .3s; }
.hud-energy { grid-column: 3; grid-row: 1 / 3; color: var(--ink); font-size: 12px;
  font-variant-numeric: tabular-nums; text-align: right; }
#energy-fill.low { background: var(--warn); }
#energy-fill.out { background: var(--danger); }
.gauge-topup { grid-column: 4; grid-row: 1 / 3; display: inline-flex; align-items: center;
  justify-content: center; width: 26px; height: 26px; border-radius: 8px; flex: 0 0 26px;
  background: var(--accent); color: #fff; font-size: 15px; line-height: 1;
  text-decoration: none; font-weight: 700; }
.gauge-topup:hover { filter: brightness(1.15); transform: translateY(-1px); }
/* Le réservoir MESSAGES — même pièce, couleur à lui. La barre d'images tire
   vers l'accent rose, celle des messages vers le bleu de l'encre vive : deux
   jauges identiques côte à côte se confondent, et c'est précisément ce que
   Phil reprochait à la première (« user will confusing »). */
#quota-fill { display: block; height: 100%; width: 100%;
  background: linear-gradient(90deg, var(--accent), var(--accent2));
  transition: width .6s cubic-bezier(.2,.9,.3,1), background .3s; }
#quota-fill.low { background: var(--warn); }
#quota-fill.out { background: var(--danger); }
/* Le ＋ des messages ne crie pas moins fort que celui des images : c'est le
   même geste, sur la même caisse. */
#quota-topup { background: var(--accent2); }
.hint a { color: var(--accent2); }

/* ── Le selfie dans le chat : une photo d'elle, clic = zoom ── */
.msg.selfie { padding: 6px; background: var(--panel); }
.chat-selfie { display: block; width: min(240px, 58vw); border-radius: 11px;
  cursor: zoom-in; transition: width .35s cubic-bezier(.2,.9,.3,1); }
.chat-selfie.zoom { width: min(560px, 86vw); cursor: zoom-out; }
.msg.selfie-wait { color: var(--ink-dim); font-style: italic; }

/* ── Le profil (menu sous le HUD) ── */
#hud { position: relative; }
.profile-menu {
  position: absolute; top: calc(100% + 6px); right: 12px; z-index: 30; min-width: 240px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 8px; box-shadow: 0 18px 50px rgba(0,0,0,.55); text-align: left;
  animation: card-in .22s cubic-bezier(.2,.9,.3,1) both;
}
.profile-menu .pm-id { font-size: 13px; padding: 6px 8px 10px; border-bottom: 1px solid var(--line);
  margin-bottom: 6px; word-break: break-all; }
.profile-menu .pm-sub { display: block; color: var(--ink-dim); font-size: 11.5px; margin-top: 2px; }
.profile-menu button, .profile-menu a {
  display: flex; gap: 8px; align-items: center; width: 100%;
  background: transparent; border: 0; color: var(--ink); padding: 8px;
  border-radius: 8px; font: inherit; font-size: 13px; cursor: pointer; text-decoration: none;
}
.profile-menu button:hover, .profile-menu a:hover { background: var(--bg2); }

/* ── LES QUATRE COMPTEURS (15/09) ────────────────────────────────────────────
   Quatre unités, quatre caisses, un seul endroit où les lire. La barre reprend
   la grammaire des jauges du header (remplissage + low/out) pour qu'un client
   n'ait pas à réapprendre un deuxième langage : ce qui est vert va bien, ce
   qui est rouge demande une recharge. Le ＋ doit ÉCHAPPER au sélecteur
   .profile-menu a (rangée pleine largeur) — sinon il pousse le nombre dehors. */
.pm-meters { padding: 2px 2px 6px; margin-bottom: 6px; border-bottom: 1px solid var(--line); }
.pm-meter { padding: 6px 8px 7px; border-radius: 8px; }
.pm-meter + .pm-meter { margin-top: 2px; }
.pm-meter:hover { background: var(--bg2); }
.pm-meter-head { display: flex; align-items: baseline; gap: 6px; }
.pm-meter-name { font-size: 12.5px; }
.pm-meter-num { margin-left: auto; font-size: 11.5px; color: var(--ink-dim); white-space: nowrap; }
.profile-menu a.pm-meter-plus {
  display: inline-flex; width: auto; padding: 0 5px; margin: 0; border-radius: 6px;
  border: 1px solid var(--line); line-height: 16px; font-size: 12px; color: var(--ink-dim);
  align-items: center; justify-content: center; flex: 0 0 auto;
}
.profile-menu a.pm-meter-plus:hover { color: var(--ink); border-color: var(--accent); background: transparent; }
.pm-meter-bar { height: 4px; border-radius: 3px; background: var(--bg2); overflow: hidden; margin: 5px 0 4px; }
.pm-meter-bar i { display: block; height: 100%; border-radius: 3px; background: var(--accent);
  transition: width .3s cubic-bezier(.2,.9,.3,1); }
.pm-meter.low .pm-meter-bar i { background: #e0a23a; }
.pm-meter.out .pm-meter-bar i { background: var(--danger); }
.pm-meter.out .pm-meter-num { color: var(--danger); }
.pm-meter-sub { font-size: 10.5px; color: var(--ink-dim); line-height: 1.35; }
.profile-menu .pm-danger { color: var(--danger); }
/* Tenir pour agir : la jauge se remplit derrière le libellé pendant les 3 s.
   --hold ∈ [0,1] est posé par le JS à chaque frame ; relâcher remet à 0. */
/* Le geste TENU, partout où il est demandé — plus seulement dans le menu du
   profil : la suppression d'une compagne le réclame aussi, et un bouton qui se
   tient sans montrer sa jauge ne dit pas qu'il faut tenir (15/09). */
.pm-hold { position: relative; overflow: hidden; touch-action: none; }
.pm-hold::before {
  content: ''; position: absolute; inset: 0; border-radius: 8px;
  background: color-mix(in srgb, var(--danger) 28%, transparent);
  transform: scaleX(var(--hold, 0)); transform-origin: left center;
  pointer-events: none;
}
.pm-hold > * { position: relative; }
.pm-hold.holding { color: var(--danger); }
.pm-hold::before { border-radius: inherit; }
.btn-icon svg { display: block; }

#chat-form { display: flex; gap: 10px; padding: 14px 18px; border-top: 1px solid var(--line); background: var(--bg2); }
#chat-input { flex: 1; background: var(--bg); border: 1px solid var(--line); border-radius: 10px;
  color: var(--ink); padding: 11px 14px; font: inherit; resize: none; max-height: 120px; }
#chat-input:focus { outline: none; border-color: var(--accent2); }
#chat-send { width: 52px; }

/* ── Phone (390 px mesuré) : elle flotte, elle ne prend plus la moitié basse ──
   Toute la mécanique « bottom sheet » est partie avec le dock flottant : plus
   de pile inversée, plus de poignée, plus de pli. Il ne reste que l'échelle :
   sur un iPhone la pastille soudée et le panneau doivent être plus petits. */
@media (max-width: 720px) {
  #dock { left: 10px; gap: 6px; }
  /* Sur un téléphone il n'y a pas de place À CÔTÉ : le panneau se pose sur le
     bas du portrait, jamais hors cadre. */
  .dock-meta { left: 0; bottom: 0; width: max-content;
    max-width: calc(100vw - 24px); max-height: min(68vh, 560px); }
  .press-hint { max-width: calc(100vw - 30px); white-space: normal; }
  /* La pastille soudée prenait 156 px de large (40 % d'un iPhone) pour deux
     mots. Elle garde ses deux moitiés et sa jointure liquide — c'est la
     passerelle entre les deux apps, elle ne disparaît pas — mais à l'échelle du
     téléphone. Phil, 15/09 : « in mobile phone format the half button part
     takes too much space ». */
  .gp-duo { font-size: 10px; letter-spacing: 0; }
  .gp-duo-h { padding: 5px 8px; }
  .gp-duo-h.gp-duo-s { padding-right: 22px; }
  .gp-duo-h.gp-duo-c { margin-left: -13px; padding-left: 19px; }
  /* La gouttelette suit la couture resserrée, sinon elle flotte dans le jaune. */
  .gp-duo-h.gp-duo-s::after { right: 15px; width: 5px; height: 5px; }
      /* Plus de second défileur imbriqué ici : #view-builder défile pour tout le
     monde, et un `max-height:100vh` sur la pile rognait encore le bas. */
  .builder-wrap { flex-direction: column; gap: 16px; padding: 20px 0; }
  /* En pile, la fiche prend la largeur qu'on lui donne : une colonne de 320 px
     fixes laissait une gouttiere morte sur un ecran de 360. */
  .builder-form { width: 100%; }
  .msg { max-width: 88%; }
}
@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}
/* Ce qui reste ici est de la RESPIRATION (marges), pas du tri : le tri est
   l'échelle de densité, mesurée. Deux propriétaires du même comportement,
   c'est le bug — pas leur désaccord. */
@media (max-width: 640px) {
  #hud { padding: 8px 10px; gap: 8px; }
  .hud-left { gap: 7px; }
  .hud-right { gap: 5px; }
}

/* Repeinte d'historique : les bulles arrivent deja en place, sans l'animation
   d'entree — 60 messages qui glissent un par un donnent un sapin de Noel. */
#messages.no-anim .msg { animation: none; }

/* Le vestiaire des importations : une bande horizontale, pas une grille —
   c'est une pile de tentatives qu'on parcourt, et la hauteur du builder est
   déjà comptée. L'active porte l'accent, pas une étiquette de plus. */
.ref-vault { margin: 2px 0 4px; }
.ref-strip { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
.ref-card {
  position: relative; flex: 0 0 auto; width: 74px; border-radius: 9px; overflow: hidden;
  border: 1px solid var(--line); background: var(--panel); cursor: pointer;
  transition: border-color .18s, transform .18s;
}
.ref-card:hover { transform: translateY(-2px); }
.ref-card.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.ref-card img { display: block; width: 100%; height: 92px; object-fit: cover; }
.ref-card.gone img { opacity: .2; }
.ref-card .ref-tag {
  display: block; font-size: 9px; letter-spacing: .06em; text-transform: uppercase;
  text-align: center; padding: 3px 2px; color: var(--ink-dim);
}
.ref-card.on .ref-tag { color: var(--accent); font-weight: 700; }
/* La croix est toujours là : « anytime » veut dire sans chercher le geste. */
.ref-del {
  position: absolute; top: 3px; right: 3px; width: 19px; height: 19px; border-radius: 50%;
  border: none; background: rgba(8,9,11,.72); color: #fff; font-size: 12px; line-height: 19px;
  padding: 0; cursor: pointer; opacity: .75;
}
.ref-del:hover { opacity: 1; background: var(--danger); }

/* ═══ LA VOIX ═══════════════════════════════════════════════════════════
   Le haut-parleur, le micro, et le petit bouton d'écoute sur ses bulles.
   Règle de la maison : l'état se VOIT (rouge qui bat = ça enregistre, onde
   qui vit = elle parle) — jamais un libellé à lire. */

/* Haut-parleur du HUD : les ondes disparaissent et la barre oblique arrive
   quand elle est muette. Une icône qui change de FORME se lit de loin ;
   une icône qui change juste de couleur, non. */
.voice-toggle .voice-off-slash { opacity: 0; transition: opacity .18s ease; }
.voice-toggle.off { color: var(--ink-dim); opacity: .75; }
.voice-toggle.off .wave-1, .voice-toggle.off .wave-2 { opacity: 0; }
.voice-toggle.off .voice-off-slash { opacity: 1; }
.voice-toggle .wave-1, .voice-toggle .wave-2 { transition: opacity .18s ease; }
.voice-toggle:not(.off) { color: var(--accent); border-color: rgba(201,79,124,.45); }
.voice-toggle:not(.off) .wave-2 { animation: voice-wave 2.4s ease-in-out infinite; }
@keyframes voice-wave { 0%,100% { opacity: .35 } 50% { opacity: 1 } }

/* L'écoute d'une bulle : posée dans le coin, elle ne mange pas le texte. */
.msg.ai { position: relative; }
.msg-voice { position: absolute; right: 6px; bottom: -9px; width: 22px; height: 22px;
  display: grid; place-items: center; border-radius: 50%; cursor: pointer;
  background: var(--bg2); border: 1px solid var(--line); color: var(--ink-dim);
  opacity: 0; transition: opacity .18s ease, color .18s ease, transform .18s ease; }
.msg.ai:hover .msg-voice, .msg-voice.playing, .msg-voice:focus-visible { opacity: 1; }
.msg-voice:hover { color: var(--ink); transform: scale(1.08); }
.msg-voice.playing { color: var(--accent); border-color: var(--accent);
  animation: voice-pulse 1.4s ease-in-out infinite; }
.msg-voice.loading { opacity: 1; color: var(--ink-dim); animation: voice-pulse .9s linear infinite; }
@keyframes voice-pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(201,79,124,.35) }
                         50% { box-shadow: 0 0 0 5px rgba(201,79,124,0) } }

/* Le micro : au repos c'est un bouton fantôme ; en enregistrement il bat, et
   il COMPTE (savoir depuis quand on parle évite le message de 3 minutes). */
.btn-mic { position: relative; width: 46px; flex: 0 0 46px; background: var(--bg);
  border: 1px solid var(--line); border-radius: 10px; color: var(--ink-dim);
  cursor: pointer; display: grid; place-items: center;
  transition: color .18s ease, border-color .18s ease, background .18s ease; }
.btn-mic:hover { color: var(--ink); border-color: var(--ink-dim); }
.btn-mic.recording { color: #fff; background: var(--danger); border-color: var(--danger);
  animation: mic-beat 1.1s ease-in-out infinite; }
.btn-mic svg { display: block; }
.mic-time { position: absolute; bottom: 1px; right: 4px; font-size: 9.5px;
  font-variant-numeric: tabular-nums; }
@keyframes mic-beat { 0%,100% { box-shadow: 0 0 0 0 rgba(201,85,85,.45) }
                      50% { box-shadow: 0 0 0 7px rgba(201,85,85,0) } }

/* Le choix de la voix dans le builder : la liste et l'essai sur la même ligne
   — écouter doit être à un doigt du choix, sinon personne n'essaie. */
.voice-row { display: flex; gap: 8px; align-items: center; }
.voice-row select { width: clamp(110px, 10vw, 148px); }
.voice-row .btn-mini { white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  .voice-toggle:not(.off) .wave-2, .msg-voice.playing, .msg-voice.loading,
  .btn-mic.recording { animation: none; }
}

/* Phone : le micro reste large au doigt, et l'écoute d'une bulle ne se cache
   plus derrière un survol qui n'existe pas (Phil : le téléphone est le poste
   de contrôle, pas une variante). */
@media (max-width: 720px) {
  #chat-form { gap: 8px; padding: 10px 12px; }
  .btn-mic { width: 44px; flex: 0 0 44px; }
  .msg-voice { opacity: .85; right: 4px; bottom: -10px; }
  .voice-row select { width: 96px; }
}

/* ── LA VITRINE avant/après (Phil, 14/09 soir : « that's a good pub ») ──────
   Le seul geste de la page d'accueil, et il porte une INFORMATION : la
   position de la poignée EST l'avancement de la transformation. Pas de survol
   décoratif — on tire, le dessin devient une personne, et le pourcentage
   parcouru se lit dans l'image elle-même.
   Construit à la main faute de composant ORBITE équivalent (cherché le 14/09,
   la bibliothèque n'a pas de comparateur) : à verser dans ORBITE. */
/* ATTENTION : ces regles portaient l'ancien id de la porte 18+, supprime du DOM
   le 15/09 quand elle est devenue un dialogue. La vitrine a suivi la carte sur
   l'ecran de CONNEXION, la mise en page est restee derriere : plus aucune regle
   ne s'appliquait, donc carte et vitrine se rangeaient en ligne sans retour,
   chacune a moitie hors ecran sur telephone. Elles sont recollees sur #view-auth.
   overflow-x: hidden est la ceinture : meme si un enfant deborde un jour, la
   page ne part plus en defilement horizontal. */
#view-auth { flex-direction: row; gap: 42px; flex-wrap: wrap; justify-content: center;
             padding: 24px max(24px, env(safe-area-inset-right)) 24px max(24px, env(safe-area-inset-left));
             overflow-x: hidden; }
/* .gate-card porte `margin:auto` pour le builder. Dans une RANGEE, cet auto
   horizontal avale tout l'espace libre : la carte partait au milieu-gauche et la
   vitrine se collait au bord droit, les deux ne se lisaient plus comme un seul
   ecran. On rend l'axe horizontal a justify-content et on gorde l'auto VERTICAL,
   qui centre quand il y a de la place et ne rogne jamais quand il n'y en a plus
   (align-items:center rognerait le haut d'une carte plus haute que la fenetre,
   meme piege que le builder plus haut). */
#view-auth > .gate-card, #view-auth > .showcase { margin: auto 0; }
.showcase {
  width: min(320px, 86vw);
  animation: card-in .6s .12s cubic-bezier(.2,.9,.3,1) both;
}
.showcase-cap {
  color: var(--ink-dim); font-size: 12.5px; line-height: 1.55;
  margin-bottom: 10px; text-align: left;
}
.showcase-cap strong { color: var(--ink); }
.showcase-kicker {
  display: block; color: var(--accent); font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase; font-size: 10.5px; margin-bottom: 3px;
}
.showcase-frame {
  position: relative; aspect-ratio: 3 / 4; border-radius: var(--radius);
  overflow: hidden; border: 1px solid var(--line); cursor: ew-resize;
  box-shadow: 0 30px 80px rgba(0,0,0,.55);
  touch-action: pan-y;               /* le doigt garde le scroll vertical */
  --split: 50%;
}
.showcase-frame img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block; user-select: none; -webkit-user-drag: none;
}
/* Le volet « avant » est DÉCOUPÉ, pas superposé en opacité : un fondu
   laisserait voir les deux à la fois et la comparaison deviendrait une bouillie.
   La découpe se fait en clip-path et NON en largeur de conteneur : une boîte
   rétrécie rétrécirait aussi l'image qu'elle contient (100 % de la boîte), et
   les deux visages ne seraient plus à la même échelle — la comparaison
   mentirait. clip-path coupe la vue sans toucher à la géométrie. */
.showcase-clip {
  position: absolute; inset: 0;
  clip-path: inset(0 calc(100% - var(--split)) 0 0);
}
.showcase-tag {
  position: absolute; bottom: 10px; font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; padding: 4px 8px; border-radius: 99px;
  background: rgba(11,13,16,.72); border: 1px solid var(--line);
  color: var(--ink-dim); backdrop-filter: blur(6px); pointer-events: none;
  transition: opacity .2s ease;
}
.showcase-tag-a { left: 10px; }
.showcase-tag-b { right: 10px; color: var(--accent); border-color: var(--accent); }
.showcase-handle {
  position: absolute; top: 0; bottom: 0; left: var(--split);
  width: 2px; margin-left: -1px; background: var(--ink);
  box-shadow: 0 0 0 1px rgba(0,0,0,.5); cursor: ew-resize;
}
.showcase-handle span {
  position: absolute; top: 50%; left: 50%; width: 34px; height: 34px;
  transform: translate(-50%, -50%); border-radius: 50%;
  background: var(--ink); border: 2px solid var(--bg);
  box-shadow: 0 6px 18px rgba(0,0,0,.5);
}
/* Les deux chevrons : ils disent DANS QUEL SENS tirer, sans texte. */
.showcase-handle span::before, .showcase-handle span::after {
  content: ''; position: absolute; top: 50%; width: 5px; height: 5px;
  border-top: 2px solid var(--bg); border-right: 2px solid var(--bg);
}
.showcase-handle span::before { left: 9px; transform: translateY(-50%) rotate(-135deg); }
.showcase-handle span::after  { right: 9px; transform: translateY(-50%) rotate(45deg); }
.showcase-handle:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
/* Pendant le glissé, les étiquettes s'effacent : on regarde le visage. */
.showcase-frame.dragging .showcase-tag { opacity: 0; }

@media (max-width: 860px) {
  /* ⚠ flex-wrap: nowrap est OBLIGATOIRE ici, ce n'est pas du ménage. Vu à
     l'œil à 390 px : avec `wrap` hérité de la règle large, une colonne dont
     les enfants dépassent la hauteur ne les empile pas — elle ouvre une
     DEUXIÈME COLONNE à droite, hors écran. La carte s'affichait seule et la
     vitrine partait derrière le bord droit, accessible seulement par une
     barre de défilement horizontale. `column` seul ne suffit jamais : il faut
     retirer le wrap ET laisser le défilement vertical faire son travail. */
  #view-auth { flex-direction: column; flex-wrap: nowrap; gap: 20px;
               overflow-y: auto; height: 100%; align-items: center;
               justify-content: flex-start;
               padding-top: 28px; padding-bottom: 28px; }
  /* En COLONNE, l'axe bloc devient l'axe principal : un margin auto vertical y
     repousserait la carte et la vitrine aux deux bouts de l'ecran. On le coupe. */
  #view-auth > * { flex: 0 0 auto; margin: 0; }
  .gate-card { padding: 26px 22px; }
  .showcase { width: min(300px, 82vw); }
}
@media (prefers-reduced-motion: reduce) {
  .showcase { animation: none; }
}

/* ── La porte 18+, là où elle décide quelque chose (15/09) ─────────────────
   Elle ne barre plus l'entrée du site : elle se lève quand on demande un rendu
   réaliste. Donc elle est un dialogue, pas une page — on revient d'où on
   vient en un clic, et refuser ne coûte pas la visite. */
.adult-gate {
  position: fixed; inset: 0; z-index: 900; display: grid; place-items: center;
  background: rgba(10, 8, 12, .78); backdrop-filter: blur(4px);
  animation: adult-in .18s ease both;
}
@keyframes adult-in { from { opacity: 0 } to { opacity: 1 } }
.adult-card {
  width: min(400px, calc(100vw - 36px)); padding: 26px 24px 20px;
  border-radius: 16px; text-align: center;
  background: var(--panel, #191219); border: 1px solid var(--line, #33283a);
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
}
.adult-q { margin: 0 0 6px; font-size: 19px; font-weight: 700; }
.adult-why { margin: 0 0 16px; font-size: 13.5px; opacity: .72; line-height: 1.5; }
.adult-btns { display: grid; gap: 9px; }
.adult-legal { margin: 14px 0 0; font-size: 11.5px; opacity: .55; line-height: 1.5; }

/* ═══ LA SALLE DES COMPAGNES (15/09) ═══════════════════════════════════════
   Plusieurs personnages par compte, chacun SON niveau. La carte montre la
   jauge de CETTE compagne : c'est ce qui rend la séparation des niveaux
   visible sans lire un chiffre. */
.roster {
  position: fixed; inset: 0; z-index: 60;
  display: grid; place-items: center;
  background: rgba(8, 4, 12, .72); backdrop-filter: blur(6px);
  padding: 16px;
  animation: roster-in .18s ease-out;
}
@keyframes roster-in { from { opacity: 0 } to { opacity: 1 } }
.roster-card {
  width: min(560px, 100%); max-height: 86vh; overflow-y: auto;
  background: var(--panel, #170f1d); border: 1px solid rgba(255,255,255,.1);
  border-radius: 16px; padding: 16px 16px 12px;
  box-shadow: 0 24px 60px rgba(0,0,0,.5);
}
.roster-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px }
.roster-head b { font-size: 1.05rem }
.roster-head .hint { margin: 0; flex: 1 }
.roster-list { display: grid; gap: 10px }

.rc {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 10px; border-radius: 12px;
  border: 1px solid rgba(255,255,255,.09);
  background: rgba(255,255,255,.03);
  transition: border-color .18s, background .18s;
}
.rc.on { border-color: var(--accent, #e26aa5); background: rgba(226,106,165,.08) }
.rc-face {
  width: 58px; height: 74px; flex: 0 0 58px; border-radius: 9px;
  object-fit: cover; object-position: 50% 0;
  background: #241a2c;
}
.rc-noface { display: grid; place-items: center; font-weight: 700; font-size: 1.4rem; color: #1b121f }
.rc-body { flex: 1; min-width: 0 }
.rc-top { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap }
.rc-name { font-size: 1rem }
.rc-age { opacity: .55; font-size: .8rem }
.rc-here {
  font-size: .68rem; letter-spacing: .04em; text-transform: uppercase;
  color: var(--accent, #e26aa5); border: 1px solid currentColor;
  border-radius: 99px; padding: 1px 7px;
}
.rc-lvl { display: flex; align-items: center; gap: 8px; margin: 5px 0 2px; font-size: .82rem }
.rc-bar {
  flex: 1; height: 6px; border-radius: 99px; overflow: hidden;
  background: rgba(255,255,255,.1); min-width: 60px;
}
.rc-bar i { display: block; height: 100%; background: var(--accent, #e26aa5); border-radius: 99px }
.rc-xp { opacity: .5; font-size: .72rem; white-space: nowrap }
.rc-sub { opacity: .5; font-size: .74rem; display: block }
.rc-acts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px }
.rc-acts .btn-mini { font-size: .72rem; padding: 3px 8px }
.btn-mini.danger { border-color: rgba(255,120,120,.4); color: #ff9c9c }

/* La place suivante : libre, ou son prix. Jamais un bouton qui mène à un refus. */
.rc-add {
  width: 100%; text-align: left; cursor: pointer; align-items: center;
  border-style: dashed; color: inherit; font: inherit;
}
.rc-add:hover { border-color: var(--accent, #e26aa5) }
.rc-max { cursor: default; opacity: .5 }
.rc-plus { font-size: 1.5rem; width: 34px; text-align: center; opacity: .6 }
.rc-buy { border-color: rgba(226,106,165,.45) }

/* La case « reset her level too » sous le bouton de wipe du profil. */
.pm-opt {
  display: flex; align-items: flex-start; gap: 7px;
  padding: 2px 10px 8px; font-size: .74rem; opacity: .72; cursor: pointer;
}
.pm-opt input { margin: 2px 0 0 }
.pm-opt-sub { opacity: .6 }

/* Le compteur sur l'icône du header : un compte à trois compagnes ne doit pas
   ressembler à un compte à une seule. */
#btn-roster { position: relative }
#btn-roster.has-count::after {
  content: attr(data-count);
  position: absolute; top: -3px; right: -3px;
  min-width: 13px; height: 13px; padding: 0 2px;
  border-radius: 99px; background: var(--accent, #e26aa5); color: #18101c;
  font-size: .58rem; font-weight: 700; line-height: 13px; text-align: center;
}

@media (max-width: 560px) {
  .roster-card { padding: 12px 12px 10px; max-height: 92vh }
  .rc-face { width: 46px; height: 60px; flex-basis: 46px }
  .rc-acts .btn-mini { font-size: .68rem }
}

/* La sortie du builder (15/09) : discrète, mais toujours là dès qu'une
   compagne existe — créer la suivante ne doit jamais être un cul-de-sac. */
.btn-back {
  background: none; border: 0; color: var(--ink-dim); cursor: pointer;
  font: inherit; font-size: .8rem; padding: 0 0 6px; text-align: left;
}
.btn-back:hover { color: var(--ink); }

/* Les trois gestes de la carte, en symboles (Phil, 15/09). Poussés à droite :
   « Talk to her » reste l'action qu'on lit, les trois autres se reconnaissent. */
.rc-acts .btn-icon { width: 28px; height: 28px; flex-basis: 28px; }
/* « Talk to her » pousse les symboles à droite ; sans lui (carte active) ils
   restent alignés à gauche plutôt que de flotter dans le vide. */
.rc-acts .btn-mini { margin-right: auto; }
.rc-acts .btn-icon.danger { border-color: rgba(201,85,85,.35); color: #d98d8d; }
.rc-acts .btn-icon.danger:hover { color: var(--danger); border-color: var(--danger); }

/* Renommer DANS le nom (Phil, 15/09) : le champ prend la place du mot, à la
   même taille et au même endroit — on corrige un prénom, on ne remplit pas un
   formulaire. */
.rc-name { cursor: text; }
.rc-name-input {
  font: inherit; font-weight: 700; color: var(--ink);
  background: var(--bg); border: 1px solid var(--accent);
  border-radius: 6px; padding: 1px 6px; width: 11ch; max-width: 100%;
}
.rc-name-input:focus { outline: none; box-shadow: 0 0 0 2px rgba(201,79,124,.25); }

/* ═══════════════ LA FIGURE 3D (15/09) ═══════════════
   Le corps prend EXACTEMENT la place du portrait : même cadre, même bord,
   même arrondi. Passer de la photo au corps n'est donc pas un changement
   d'écran, c'est le même cadre qui change de matière. */
.figure-stage {
  position: absolute; inset: 0; border-radius: 14px; overflow: hidden;
  border: 1px solid var(--line); background: radial-gradient(120% 90% at 50% 10%, #2a1b26 0%, #140d14 70%);
}
.figure-stage model-viewer {
  width: 100%; height: 100%; display: block;
  --poster-color: transparent;
  background: transparent;
}
/* ── LE RAIL DES VUES ────────────────────────────────────────────────────────
   Deux flèches sur les bords du cadre, une étiquette qui passe. Elles vivent
   DANS la pile (z-index au-dessus du portrait et du viewer) et interceptent
   leur propre geste : la pile, elle, écoute l'appui pour changer de taille. */
.stage-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 4; width: 30px; height: 46px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(255, 255, 255, .26); border-radius: 11px;
  background: rgba(10, 7, 12, .58); backdrop-filter: blur(3px);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .45);
  color: #fff; font-size: 21px; line-height: 1; cursor: pointer;
  /* Une flèche doit rester lisible SUR l'image — un portrait de plein jour
     avalait un bouton à .5 d'opacité. */
  opacity: .72; transition: opacity .18s ease, background .18s ease, transform .18s ease;
}
.stage-nav:hover { opacity: 1; background: rgba(201, 79, 124, .62); }
.stage-nav:active { transform: translateY(-50%) scale(.92); }
.stage-nav.prev { left: 6px; }
.stage-nav.next { right: 6px; }
@media (max-width: 720px) { .stage-nav { opacity: .85; } }
/* L'étiquette ne RESTE pas : elle nomme la vue une seconde, puis s'en va.
   Une légende permanente sous le cadre, c'est ce que Phil a fait retirer. */
.stage-tag {
  position: absolute; left: 50%; bottom: 10px; z-index: 4;
  transform: translateX(-50%); pointer-events: none;
  padding: 3px 10px; border-radius: 999px; border: 1px solid var(--line);
  background: rgba(12, 8, 14, .6); color: #fff; font-size: 11px; letter-spacing: .04em;
  animation: tagfade 1.5s ease forwards;
}
@keyframes tagfade {
  0% { opacity: 0; transform: translateX(-50%) translateY(6px); }
  18% { opacity: 1; transform: translateX(-50%) translateY(0); }
  72% { opacity: 1; }
  100% { opacity: 0; }
}
/* Le changement de vue ARRIVE par le côté d'où on l'a appelée — le geste dit
   dans quel sens on a tourné, pas seulement qu'il s'est passé quelque chose.
   Le glissement ne s'applique QU'AU viewer : le portrait a déjà un écrivain de
   transform (PortraitLife), et deux écrivains du même transform, c'est le bug.
   Lui, il arrive en fondu — même durée, même courbe. */
@keyframes viewin-l { from { opacity: 0; transform: translateX(-9%); } to { opacity: 1; transform: none; } }
@keyframes viewin-r { from { opacity: 0; transform: translateX(9%); } to { opacity: 1; transform: none; } }
@keyframes viewfade { from { opacity: 0; } to { opacity: 1; } }
.stage-stack .figure-stage.viewin-l { animation: viewin-l .34s cubic-bezier(.2,.9,.3,1); }
.stage-stack .figure-stage.viewin-r { animation: viewin-r .34s cubic-bezier(.2,.9,.3,1); }
.stage-stack .portrait-wrap.viewin-l,
.stage-stack .portrait-wrap.viewin-r { animation: viewfade .34s ease; }
@media (prefers-reduced-motion: reduce) {
  .stage-stack .figure-stage.viewin-l, .stage-stack .figure-stage.viewin-r,
  .stage-stack .portrait-wrap.viewin-l, .stage-stack .portrait-wrap.viewin-r { animation: none; }
}
/* Le diagnostic reste LISIBLE sur n'importe quel fond : il arrive quand la
   vue est ratée, c'est le pire moment pour un texte pâle. */
.stage-warn {
  position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 5; margin: 0;
  padding: 7px 9px; border-radius: 10px; border: 1px solid rgba(255, 190, 90, .5);
  background: rgba(28, 16, 8, .88); color: #ffd9a0; font-size: 11px; line-height: 1.35;
}
/* L'offre offerte se voit : c'est la seule carte que le gratuit peut presser. */
.figure-offer.gifted { border-color: rgba(110, 220, 160, .7); }
.figure-offer.gifted em { color: #8fe7b8; }
.figure-box {
  margin-top: 10px; padding: 10px; border: 1px solid var(--line);
  border-radius: 12px; background: rgba(255,255,255,.03);
}
.figure-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.figure-head .hint { font-size: 11px; }
/* ── LE CHOIX, AVANT LE PRIX (Phil, 15/09) ─────────────────────────────────
   Deux lignes au-dessus des cartes. Elles doivent se lire comme un réglage,
   pas comme deux boutons de plus : la légende porte le sens, les options ne
   portent que le mot. */
.fp-row { display: flex; flex-direction: column; gap: 5px; }
/* Un réglage binaire tient sur sa ligne : la légende ne mérite pas la sienne. */
.fp-row.fp-inline { flex-direction: row; align-items: center; gap: 8px; }
.fp-row.fp-inline .fp-seg { margin-left: auto; }
/* La barre des gestes : « Moves 6/6 » à gauche, la porte du catalogue à droite. */
.fp-head { display: flex; align-items: baseline; gap: 8px; }
.fp-head .fp-more { margin-left: auto; margin-top: 0; }
.fp-cap { font-size: 11px; letter-spacing: .04em; text-transform: uppercase; opacity: .72; }
.fp-cap i { font-style: normal; text-transform: none; letter-spacing: 0; opacity: .75; }
.fp-head .fp-cap i { opacity: 1; color: var(--accent); font-weight: 700; }
.fp-hint { margin: 0; font-size: 11px; opacity: .6; }
.fp-seg {
  display: inline-flex; align-self: flex-start; padding: 2px; gap: 2px;
  border: 1px solid var(--line); border-radius: 999px; background: rgba(255,255,255,.04);
}
.fp-opt {
  border: 0; background: transparent; color: var(--fg); cursor: pointer;
  padding: 4px 12px; border-radius: 999px; font-size: 12px;
  transition: background .18s, color .18s;
}
.fp-opt:hover:not(.on) { background: rgba(255,255,255,.07); }
.fp-opt.on { background: var(--accent); color: #fff; font-weight: 600; }
.fp-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.fp-chip {
  border: 1px solid var(--line); background: transparent; color: var(--fg);
  border-radius: 999px; padding: 4px 9px; font-size: 11px; cursor: pointer;
  opacity: .55; transition: opacity .18s, border-color .18s, background .18s;
}
.fp-chip:hover { opacity: .85; }
/* Cochée = pleine. L'œil doit compter les poses retenues d'un coup, sans lire. */
.fp-chip.on {
  opacity: 1; border-color: var(--accent); background: rgba(233,90,150,.18); font-weight: 600;
}
/* La raison d'un verrou, sous les cartes : une carte grise sans phrase se lit
   comme une panne, jamais comme une règle. */
.fp-note { flex: 1 1 100%; margin: 3px 0 0; font-size: 11px; opacity: .72; text-align: center; }
/* Un geste encore en fabrication : présent, nommé, mais pas encore choisissable. */
.fp-chip.wait { opacity: .38; cursor: progress; border-style: dashed; }
/* La porte du catalogue. Discrète tant qu'on ne la cherche pas, mais elle dit
   qu'il y a autre chose derrière les six mots. */
.fp-more {
  align-self: flex-start; margin-top: 2px; border: 0; background: transparent;
  color: var(--accent); font-size: 11px; cursor: pointer; padding: 2px 0;
  letter-spacing: .02em;
}
.fp-more:hover { text-decoration: underline; }

/* ── LE CATALOGUE DE GESTES (Phil, 15/09) ───────────────────────────────────
   678 mouvements avec leur aperçu animé. Une page à la fois — on tourne, on ne
   déroule pas — et trois colonnes sur un téléphone. */
.figure-motion {
  margin-top: 10px; padding: 10px; border-radius: 12px;
  border: 1px solid var(--line); background: rgba(255,255,255,.025);
  display: flex; flex-direction: column; gap: 8px;
}
.mb-search {
  width: 100%; box-sizing: border-box; padding: 7px 10px; font-size: 12px;
  border-radius: 999px; border: 1px solid var(--line);
  background: rgba(0,0,0,.25); color: var(--fg);
}
.mb-search:focus { outline: none; border-color: var(--accent); }
.mb-fams { display: flex; gap: 5px; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
.mb-fams::-webkit-scrollbar { display: none; }
.mb-fam {
  flex: 0 0 auto; border: 1px solid var(--line); background: transparent; color: var(--fg);
  border-radius: 999px; padding: 3px 9px; font-size: 10.5px; cursor: pointer; opacity: .6;
}
.mb-fam.on { opacity: 1; border-color: var(--accent); background: rgba(233,90,150,.16); font-weight: 600; }
/* LES DEUX CRANS D'ÉNERGIE, en tête de rang (Phil, 15/09 : « a walk or sit but
   not too much action »). Ils ne sont pas des familles : ils tranchent la
   question qu'on se pose vraiment — tranquille, ou qui bouge. D'où le trait
   plein plutôt que le contour, et le séparateur après le second. */
.mb-fam.mb-en { opacity: .8; border-color: transparent; background: rgba(255,255,255,.07); font-weight: 600; letter-spacing: .02em; }
.mb-fam.mb-en.on { opacity: 1; border-color: var(--accent); background: rgba(233,90,150,.22); }
.mb-fam.mb-en + .mb-fam:not(.mb-en) { margin-left: 8px; position: relative; }
.mb-fam.mb-en + .mb-fam:not(.mb-en)::before {
  content: ''; position: absolute; left: -5px; top: 3px; bottom: 3px; width: 1px; background: var(--line);
}
.mb-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); gap: 6px;
}
.mb-card {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 5px 3px 6px; border-radius: 10px; cursor: pointer;
  border: 1px solid var(--line); background: rgba(0,0,0,.2); color: var(--fg);
  transition: border-color .18s, background .18s;
}
.mb-card img {
  width: 100%; height: auto; aspect-ratio: 1; object-fit: contain;
  border-radius: 7px; background: rgba(255,255,255,.04);
}
.mb-card span {
  font-size: 9.5px; line-height: 1.15; text-align: center; opacity: .8;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
/* Le liseré des gestes de la maison : une teinte froide, jamais l'accent —
   l'accent dit « choisi », et un mouvement possédé peut ne pas l'être. */
.mb-card.own { border-color: rgba(120,200,190,.42); }
.mb-card.own:hover { border-color: rgba(120,200,190,.75); }
.mb-own {
  font-size: 8px; letter-spacing: .06em; text-transform: uppercase;
  font-style: normal; color: rgba(120,200,190,.9); opacity: .85;
}
.mb-none {
  width: 100%; aspect-ratio: 1; border-radius: 7px; font-style: normal;
  background: rgba(255,255,255,.04); display: flex; align-items: center;
  justify-content: center; font-size: 20px; opacity: .45;
}
.mb-card:hover { border-color: rgba(233,90,150,.5); }
.mb-card.on { border-color: var(--accent); background: rgba(233,90,150,.18); }
.mb-card.on span { opacity: 1; font-weight: 600; }
.mb-empty { grid-column: 1 / -1; margin: 10px 0; font-size: 11.5px; opacity: .7; text-align: center; }
.mb-pager { display: flex; align-items: center; justify-content: center; gap: 10px; }
.mb-page {
  width: 26px; height: 26px; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--line); background: transparent; color: var(--fg); font-size: 13px;
  display: flex; align-items: center; justify-content: center;
}
.mb-page:disabled { opacity: .3; cursor: default; }
.mb-count { font-size: 11px; opacity: .75; font-variant-numeric: tabular-nums; }
.mb-make { display: flex; flex-direction: column; gap: 6px; border-top: 1px solid var(--line); padding-top: 9px; }
.mb-make-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.mb-prompt {
  flex: 1 1 100%; box-sizing: border-box; padding: 7px 10px; font-size: 12px;
  border-radius: 9px; border: 1px solid var(--line); background: rgba(0,0,0,.25); color: var(--fg);
}
.mb-prompt:focus { outline: none; border-color: var(--accent); }
.mb-dur { display: flex; align-items: center; gap: 6px; font-size: 11px; opacity: .8; }
.mb-dur input { width: 90px; accent-color: var(--accent); }
.mb-dur b { font-variant-numeric: tabular-nums; min-width: 28px; }
.mb-go { align-self: flex-start; }
.mb-say { margin: 0; font-size: 11px; opacity: .75; min-height: 13px; }
.mb-mine { display: flex; flex-direction: column; gap: 4px; }
/* ── LES DEUX VOIES (Phil, 16/09 : « why it mixed up on each other ? ») ─────
   Chaque voie est une BOÎTE : son numéro, son titre, ses réglages, sa carte.
   Le bloc plat d'avant laissait la posture et les gestes se toucher, donc se
   confondre. La voie 2 garde le même cadre quand elle est fermée — on voit
   qu'elle existe et ce qui l'ouvre, au lieu de la découvrir par un refus. */
.figure-pick, .fig-lane {
  margin-top: 9px; padding: 8px 9px 9px; display: flex; flex-direction: column; gap: 8px;
  border: 1px solid var(--line); border-radius: 12px; background: rgba(255,255,255,.02);
  text-align: left;
}
.fl-head { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.fl-n {
  flex: 0 0 auto; width: 17px; height: 17px; border-radius: 999px;
  background: var(--accent); color: #fff; font-size: 11px; font-weight: 700;
  line-height: 17px; text-align: center; align-self: center;
}
.fl-t { font-size: 12.5px; font-weight: 600; }
.fl-h { font-style: normal; font-size: 10.5px; color: var(--ink-dim); }
.fl-wait { margin: 0; font-size: 11px; color: var(--ink-dim); }
.fl-buy { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; }
/* ── LES RÉGLAGES DE MAILLE (16/09) ─────────────────────────────────────────
   Repliés : ce sont des détails de fabrication, et le défaut est le bon choix
   pour presque tout le monde. Le résumé dit l'état SANS ouvrir — sinon un
   panneau fermé cache un 8K facturé. */
.fp-adv { border-top: 1px dashed var(--line); padding-top: 7px; }
.fp-adv > summary {
  display: flex; align-items: baseline; gap: 7px; cursor: pointer; list-style: none;
}
.fp-adv > summary::-webkit-details-marker { display: none; }
.fp-adv > summary::after { content: '▾'; margin-left: auto; font-size: 10px; opacity: .6; }
.fp-adv[open] > summary::after { content: '▴'; }
.fp-adv > summary i { font-style: normal; font-size: 10.5px; color: var(--ink-dim); }
.fp-opt-row {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 7px;
}
.fp-opt-row.off { opacity: .4; }
.fp-opt-lab { font-size: 11.5px; display: flex; flex-direction: column; }
.fp-opt-lab i { font-style: normal; font-size: 10px; color: var(--ink-dim); }
.fp-opt-row .fp-seg { margin-left: auto; }
.fp-opt-price { font-style: normal; font-size: 10.5px; color: var(--accent); }
/* Les trois sources d'un geste, nommées (16/09). La troisième reste visible et
   éteinte : une case absente se lit « on ne sait pas faire », une case éteinte
   dit « pas encore, et ça viendra ». */
.fp-src { display: flex; gap: 5px; flex-wrap: wrap; }
.fp-srcb {
  flex: 1 1 96px; padding: 5px 7px; text-align: left; cursor: pointer;
  display: flex; flex-direction: column; gap: 1px;
  border: 1px solid var(--line); border-radius: 9px; background: var(--bg2);
  transition: border-color .18s, background .18s;
}
.fp-srcb:hover:not(:disabled) { border-color: var(--accent); background: rgba(233,90,150,.10); }
.fp-srcb b { font-size: 11.5px; }
.fp-srcb i { font-style: normal; font-size: 10px; color: var(--ink-dim); }
.fp-srcb.off { opacity: .45; cursor: default; border-style: dashed; }
/* Un bouton désactivé hérite d'une couleur SOMBRE de la feuille de base : le
   titre « Video to Motion » sortait en rgba(16,16,16,.3), donc invisible sur
   fond sombre (mesuré dans le navigateur, 16/09). La carte est déjà atténuée
   par son opacité — le texte, lui, garde sa couleur. */
.fp-srcb:disabled b { color: var(--ink); }
.fp-srcb:disabled i { color: var(--ink-dim); }
.fl-buy:empty { display: none; }
/* La voie 2 fermée s'efface d'un ton — présente, pas offerte. */
.fig-lane:has(.fl-wait) { opacity: .72; border-style: dashed; }
.figure-offers { display: flex; flex-direction: column; gap: 0; margin-top: 0; }
/* Une offre n'est pas un bouton de plus : elle dit le nom, ce qu'on reçoit et
   le prix, sur trois lignes, parce que c'est dans cet ordre qu'on décide. */
.figure-offer {
  flex: 1 1 130px; min-width: 120px; display: flex; flex-direction: column; gap: 3px;
  padding: 9px 10px; border-radius: 10px; cursor: pointer; text-align: left;
  border: 1px solid var(--accent); background: transparent; color: var(--fg);
  transition: background .18s, transform .18s;
}
.figure-offer:hover:not(:disabled) { background: rgba(233,90,150,.14); transform: translateY(-1px); }
.figure-offer.locked, .figure-offer:disabled { opacity: .45; cursor: default; }
.figure-offer b { font-size: 13px; color: var(--accent); }
.figure-offer span { font-size: 11px; opacity: .8; line-height: 1.3; }
.figure-offer em { font-style: normal; font-size: 13px; font-weight: 700; }
/* La barre d'avancement : une figure demande des minutes, et une barre muette
   pendant six minutes se lit comme une panne. */
.figure-bar { height: 6px; border-radius: 4px; background: rgba(255,255,255,.1); margin-top: 9px; overflow: hidden; }
.figure-bar i {
  display: block; height: 100%; width: 4%; border-radius: 4px;
  background: linear-gradient(90deg, var(--accent), #ffb3d1);
  transition: width .6s cubic-bezier(.2,.9,.3,1);
}
@media (max-width: 560px) { .figure-offer { flex: 1 1 100%; } }

/* ═══════════════ LE PLATEAU DES RÉFÉRENCES (Phil, 16/09) ═══════════════
   « we should be able to see/control/edit/add/remove the image/3d/2d
   reference for any generation … and please remove the guide 1 2 3 ».
   Le guide en cartes est parti avec son bestiaire de pictogrammes animés
   (quatre SVG en boucle infinie dans un panneau toujours ouvert — passe
   anti-surchauffe : rien ici ne tourne en permanence, seules les transitions
   au survol coûtent, et elles ne durent pas). À sa place, la seule chose qui
   décidait vraiment et qui manquait : ce que la génération va manger. */
.fig-refs {
  display: flex; flex-direction: column; gap: 8px; margin: 8px 0 8px;
  padding: 8px; border: 1px solid var(--line); border-radius: 12px;
  background: rgba(255,255,255,.02);
  /* Le dock centre son texte : sans ça, les deux légendes du plateau
     dérivaient vers le milieu alors que les vignettes restaient à gauche
     (visible seulement en prod, le banc local n'a pas ce parent). */
  text-align: left;
}
.fig-refs .fr-cap { display: flex; flex-wrap: wrap; gap: 5px; align-items: baseline; }
.fig-refs .fr-cap b { font-size: 11.5px; }
.fig-refs .fr-cap i { font-style: normal; font-size: 10.5px; color: var(--ink-dim); }
.fig-refs .fr-row { display: flex; flex-direction: column; gap: 5px; }
.fig-refs .fr-lab { font-size: 10.5px; color: var(--ink-dim); }
.fig-refs .fr-tiles { display: flex; flex-wrap: wrap; gap: 6px; }

/* Une vignette : l'image, son rang d'envoi, ce qu'elle est, et sa croix. */
.fig-refs .fr-tile {
  position: relative; width: 58px; display: flex; flex-direction: column;
  gap: 2px; align-items: center;
}
.fig-refs .fr-hit {
  position: relative; width: 58px; height: 58px; padding: 0; cursor: pointer;
  border: 1px solid var(--line); border-radius: 9px; background: var(--bg2);
  overflow: hidden; opacity: .48; transition: opacity .18s, border-color .18s;
}
.fig-refs .fr-hit img { width: 100%; height: 100%; object-fit: cover; display: block; }
.fig-refs .fr-hit:hover { opacity: .85; }
.fig-refs .fr-tile.on .fr-hit { opacity: 1; border-color: var(--accent); }
.fig-refs .fr-n {
  position: absolute; top: 2px; left: 2px; min-width: 16px; height: 16px;
  border-radius: 999px; background: var(--accent); color: #fff;
  font-size: 10px; font-style: normal; line-height: 16px; text-align: center;
}
.fig-refs .fr-kind {
  font-size: 9.5px; font-style: normal; color: var(--ink-dim);
  max-width: 58px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fig-refs .fr-x {
  position: absolute; top: -4px; right: -4px; width: 18px; height: 18px;
  padding: 0; line-height: 16px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: var(--bg); color: var(--ink-dim);
  font-size: 12px; z-index: 2; opacity: .5; transition: opacity .15s, color .15s;
}
/* Elle ne se CACHE plus (16/09) : `:hover` n'existe pas sur un téléphone —
   une croix visible à la souris seule était une croix absente sur l'appareil
   où Phil travaille. Discrète au repos, franche au survol. */
.fig-refs .fr-tile:hover .fr-x, .fig-refs .fr-bcell:hover .fr-x,
.fig-refs .fr-x:focus-visible { opacity: 1; }
.fig-refs .fr-x:hover { color: #ff8a8a; border-color: #ff8a8a; }
.fig-refs .fr-x:disabled { opacity: .18; cursor: default; }
/* La jauge de maintien sur un rond de 18 px : le balayage de la barre plate
   (.pm-hold) n'y voudrait rien dire, l'anneau si — il se ferme en 2 s. */
.fig-refs .fr-x.pm-hold { overflow: visible; }
.fig-refs .fr-x.pm-hold::before {
  inset: -2px; border-radius: 999px; background: transparent; transform: none;
  border: 2px solid transparent;
  background-image: conic-gradient(var(--danger) calc(var(--hold, 0) * 360deg), transparent 0);
  -webkit-mask: radial-gradient(circle, transparent 60%, #000 62%);
          mask: radial-gradient(circle, transparent 60%, #000 62%);
}
.fig-refs .fr-x.holding { color: var(--danger); border-color: var(--danger); opacity: 1; }
.fig-refs .fr-add {
  height: 58px; cursor: pointer; border: 1px dashed var(--line); border-radius: 9px;
  background: transparent; color: var(--ink-dim); justify-content: center;
  transition: border-color .18s, color .18s;
}
.fig-refs .fr-add:hover { border-color: var(--accent); color: var(--accent); }
.fig-refs .fr-add span { font-size: 17px; line-height: 1; }

/* ── Le voile des deux portes (bibliothèque studio / vues dessinées, 17/09).
   Plein écran, la feuille au centre, et tout se lit à 390 px : la grille est
   fluide, la feuille défile, le fond se referme d'un tap à côté. */
.fl-veil {
  position: fixed; inset: 0; z-index: 90; display: flex;
  align-items: center; justify-content: center; padding: 14px;
  background: rgba(8, 6, 10, .72); backdrop-filter: blur(3px);
}
.fl-sheet {
  width: min(560px, 100%); max-height: min(78vh, 640px); display: flex;
  flex-direction: column; border: 1px solid var(--line); border-radius: 14px;
  background: var(--bg); box-shadow: 0 18px 50px rgba(0, 0, 0, .5);
}
.fl-head {
  display: flex; gap: 8px; align-items: baseline; padding: 12px 14px 8px;
  border-bottom: 1px solid var(--line);
}
.fl-head b { font-size: 13px; }
.fl-head i { flex: 1; font-style: normal; font-size: 10.5px; color: var(--ink-dim); }
.fl-close {
  width: 24px; height: 24px; padding: 0; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: var(--bg2); color: var(--ink-dim);
  font-size: 14px; line-height: 22px;
}
.fl-close:hover { color: var(--accent); border-color: var(--accent); }
.fl-body { padding: 12px 14px; overflow: auto; display: flex; flex-direction: column; gap: 10px; }
.fl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 8px; }
.fl-card {
  position: relative; padding: 0; aspect-ratio: 2/3; overflow: hidden; cursor: pointer;
  border: 1px solid var(--line); border-radius: 10px; background: var(--bg2);
  transition: border-color .18s;
}
.fl-card img { width: 100%; height: 100%; object-fit: cover; display: block; }
.fl-card em {
  position: absolute; left: 4px; right: 4px; bottom: 4px; padding: 2px 0;
  border-radius: 7px; background: rgba(8, 6, 10, .72); color: var(--ink);
  font-size: 10px; font-style: normal; text-align: center;
  opacity: 0; transition: opacity .15s;
}
.fl-card:hover { border-color: var(--accent); }
.fl-card:hover em, .fl-card.done em { opacity: 1; }
.fl-card.done { border-color: var(--accent); }
.fl-note { margin: 0; font-size: 11px; color: var(--ink-dim); line-height: 1.5; }
.fl-state { margin: 0; font-size: 11px; color: var(--accent); min-height: 14px; }
.fl-foot { display: flex; gap: 8px; justify-content: flex-end; }
.fl-views { display: flex; flex-direction: column; gap: 10px; }
.fl-src { display: flex; gap: 10px; align-items: center; }
.fl-src img {
  width: 58px; height: 58px; object-fit: cover; border-radius: 9px;
  border: 1px solid var(--accent);
}
.fl-src span { font-size: 11px; }
.fl-src i { font-style: normal; font-size: 10px; color: var(--ink-dim); }
.fl-opts { display: flex; flex-wrap: wrap; gap: 8px; }
.fl-opt {
  display: flex; gap: 6px; align-items: center; padding: 7px 10px; cursor: pointer;
  border: 1px solid var(--line); border-radius: 9px; background: var(--bg2);
  font-size: 11px;
}
.fl-opt:has(input:checked) { border-color: var(--accent); }
.fl-opt.had { opacity: .6; }
.fl-opt i { font-style: normal; font-size: 9.5px; color: var(--ink-dim); }

/* Les corps : une carte chacun, l'anneau sur celui qu'Animate va toucher. */
.fig-refs .fr-bodies { gap: 5px; align-items: stretch; }
/* La cellule porte la taille ET la croix ; la carte, elle, remplit sa cellule. */
.fig-refs .fr-bcell {
  position: relative; flex: 1 1 96px; min-width: 96px; max-width: 150px; display: flex;
}
.fig-refs .fr-bcell .fr-body { flex: 1 1 auto; min-width: 0; max-width: none; }
.fig-refs .fr-body {
  flex: 1 1 96px; min-width: 96px; max-width: 150px; padding: 6px 8px;
  display: flex; flex-direction: column; gap: 1px; text-align: left; cursor: pointer;
  border: 1px solid var(--line); border-radius: 9px; background: var(--bg2);
  transition: border-color .18s, background .18s;
}
.fig-refs .fr-body:hover:not(:disabled) { border-color: var(--accent); }
.fig-refs .fr-body.on {
  border-color: var(--accent); background: rgba(233,90,150,.14);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.fig-refs .fr-body b { font-size: 11.5px; }
/* La marque « venu du plan de travail » : une flèche, pas un mot qui tronque. */
.fig-refs .fr-imp { font-style: normal; font-size: 9px; color: var(--accent2); margin-left: 3px; }
/* LA POSE DE REPOS SUR LA CARTE (16/09) : T ou A en vert, la croix en rouge.
   Une carte refusée garde sa bordure rouge même non sélectionnée — c'est un
   ÉTAT du corps, pas un survol. */
.fig-refs .fr-pos { font-style: normal; font-size: 9px; color: #6ee7a8; margin-left: 3px; letter-spacing: .04em; }
.fig-refs .fr-nop { font-style: normal; font-size: 9px; color: #ff6b6b; margin-left: 3px; }
.fig-refs .fr-body.nopose { border-color: rgba(255, 107, 107, .55); }
.fig-refs .fr-body span {
  font-size: 10px; color: var(--ink-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fig-refs .fr-body em { font-style: normal; font-size: 9.5px; color: var(--ink-dim); }
.fig-refs .fr-body.dead { opacity: .45; }
.fig-refs .fr-body.dead em { color: #ffb38a; }
.fig-refs .fr-body:disabled { cursor: default; opacity: .5; }

/* ═══════════════ LE VERDICT — la planche avant la caisse ═══════════════ */
/* L'arrivée de la planche. Elle vivait dans le bloc du guide ; le guide parti,
   elle revient ici, chez son seul utilisateur — une animation de 0,22 s qui
   joue UNE fois, jamais en boucle. */
@keyframes fgIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.fig-review {
  display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 8px; padding: 10px;
  border: 1px solid var(--accent); border-radius: 12px;
  background: rgba(233,90,150,.08); animation: fgIn .22s ease-out;
}
.fig-review .fr-sheet { flex: 0 0 132px; }
.fig-review .fr-sheet img {
  width: 132px; border-radius: 8px; display: block; background: var(--bg2);
  border: 1px solid var(--line);
}
.fig-review .fr-side { flex: 1 1 180px; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.fig-review b { font-size: 12.5px; }
.fig-review p { font-size: 11.5px; color: var(--ink-dim); line-height: 1.4; margin: 0; }
.fig-review .fr-seg { display: flex; gap: 4px; }
.fig-review .fr-acts { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.fig-review .fr-yes { padding: 8px 14px; font-size: 13px; }
.fig-review .fr-drop { opacity: .6; }
.fig-review .fr-note { font-size: 10.5px; color: #8fe7b8; }

/* La planche de scan dans le cadre : elle remplit comme le portrait, mais on
   la voit ENTIÈRE — un corps coupé aux genoux ne prouve rien. */
.scan-wrap { position: absolute; inset: 0; display: grid; place-items: center; }
.scan-wrap img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: inherit; display: block; }

@media (max-width: 560px) {
  .fig-review .fr-sheet { flex: 0 0 auto; align-self: center; }
}
@media (prefers-reduced-motion: reduce) {
  .fig-review { animation: none; }
}

/* Les VUES envoyées au moteur (15/09) : une grille de vignettes, et le rang de
   chacune écrit dessus — la première EST la vue de face chez le fournisseur,
   donc l'ordre doit se voir, pas se deviner. */
.fr-views { display: flex; flex-direction: column; gap: 4px; }
.fr-grid { display: flex; flex-wrap: wrap; gap: 5px; }
.fr-view {
  position: relative; width: 52px; height: 52px; padding: 0; cursor: pointer;
  border: 1px solid var(--line); border-radius: 8px; background: var(--bg2);
  overflow: hidden; opacity: .5; transition: opacity .18s, border-color .18s;
}
.fr-view img { width: 100%; height: 100%; object-fit: cover; display: block; }
.fr-view:hover { opacity: .8; }
.fr-view.on { opacity: 1; border-color: var(--accent); }
.fr-view em {
  position: absolute; top: 2px; left: 2px; min-width: 15px; height: 15px;
  border-radius: 999px; background: var(--accent); color: #fff;
  font-style: normal; font-size: 10px; font-weight: 700; line-height: 15px;
}

/* ── L'IMPORT D'UN CORPS DÉJÀ SCULPTÉ (15/09) ───────────────────────────────
   Une ligne discrète sous les cartes : ce n'est pas une formule à vendre,
   c'est un droit de la maison. Même géométrie de contrôle que le reste du
   panneau (hauteur 30, rayon 8) pour que rien ne dépasse d'une rangée. */
.figure-import { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line, rgba(255,255,255,.12)); }
.figure-import.hidden { display: none; }
.figure-import .fi-lab {
  display: block; width: 100%; text-align: left; padding: 0; margin: 0;
  border: 0; background: transparent; cursor: pointer;
  font-size: 11px; color: var(--ink-dim); font-family: inherit;
}
.figure-import .fi-lab:hover { color: var(--accent); }
.figure-import .fi-body { margin-top: 7px; display: flex; flex-direction: column; gap: 7px; }
.figure-import .fi-body.hidden { display: none; }
.figure-import .fi-row { display: flex; gap: 6px; align-items: center; }
.figure-import input {
  flex: 1 1 auto; min-width: 0; height: 30px; padding: 0 8px; border-radius: 8px;
  border: 1px solid var(--line, rgba(255,255,255,.18));
  background: rgba(255,255,255,.04); color: inherit; font-size: 12px;
}
.figure-import input:focus { outline: none; border-color: var(--accent); }
.figure-import .btn-mini { white-space: nowrap; }
.figure-import .fp-note { text-align: left; }
/* Le plan de travail, listé. Un geste déjà fabriqué n'a pas de lien qui le
   nomme : cette liste EST sa seule porte, donc elle doit se lire d'un coup —
   la sorte, le moment, et un seul bouton. */
.figure-import .fi-ws { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; font-size: 11px; }
.figure-import .fi-wsline {
  display: flex; align-items: center; gap: 6px;
  padding: 4px 6px; border-radius: 8px; background: rgba(255, 255, 255, .04);
}
.figure-import .fi-wskind {
  flex: 0 0 auto; padding: 1px 6px; border-radius: 999px; font-size: 9px;
  letter-spacing: .05em; text-transform: uppercase;
  border: 1px solid var(--line, rgba(255,255,255,.18)); opacity: .9;
}
.figure-import .fi-wskind.move { border-color: var(--pink, #c94f7c); color: var(--pink, #c94f7c); }
.figure-import .fi-wsname { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; opacity: .8; }
/* À qui appartient la ligne : la sienne se lit, celles des autres se voient. */
.figure-import .fi-wsowner { display: inline-block; margin-left: .35rem; padding: 0 .38rem; border-radius: 999px;
  font-size: .68rem; letter-spacing: .02em; background: rgba(255,255,255,.08); color: rgba(255,255,255,.62); }
.figure-import .fi-wsowner.mine { background: rgba(236,72,153,.18); color: #f9a8d4; }
/* « pas encore classé » : fabriqué à la main chez le fournisseur, provenance
   inconnue tant que le fichier n'est pas ouvert. L'ambre dit « à mesurer »,
   jamais « à toi » — le tri inventé est exactement ce qu'on répare. */
.figure-import .fi-wsowner.new { background: rgba(245,158,11,.16); color: #fcd34d; }
/* La ligne qui avoue ce qui est caché. Une liste filtrée en silence se lit
   comme une panne. */
.figure-import .fi-wsnote { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap;
  padding: .3rem .1rem .45rem; font-size: .74rem; color: rgba(255,255,255,.5); }
.figure-import .fi-ws .btn-mini:disabled { opacity: .45; cursor: default; }

/* ═══════════════ LA ROUE D'ACTIONS ═══════════════
   Phil, 15/09 : « at the left bottom of the profil … a buttom circle that we
   can type and it will expan into many circle of action run stand sit walk
   every action (animate) of that character we have ».
   Un rond dans le coin, et l'éventail s'ouvre VERS l'espace libre (vers le haut
   et vers la droite). Chaque pastille porte l'aperçu réel du geste, pas une
   icône générique : on reconnaît « marcher » sans lire le mot. La position de
   chaque rond est écrite en JS (--ax/--ay), parce qu'elle dépend de la TAILLE
   du cadre, qui change avec le niveau et avec l'appui. */
.acts { position: absolute; left: 10px; bottom: 10px; z-index: 5; width: 0; height: 0; }
.acts-hub {
  position: absolute; left: 0; bottom: 0; width: 34px; height: 34px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(255, 255, 255, .26); border-radius: 50%;
  background: rgba(10, 7, 12, .62); backdrop-filter: blur(3px);
  box-shadow: 0 2px 10px rgba(0, 0, 0, .45);
  color: #fff; cursor: pointer; opacity: .82;
  transition: opacity .18s ease, background .2s ease, transform .3s cubic-bezier(.2,.9,.3,1);
}
.acts-hub:hover { opacity: 1; background: rgba(201, 79, 124, .6); }
.acts-hub-dot {
  width: 9px; height: 9px; border-radius: 50%; background: var(--pink, #c94f7c);
  box-shadow: 0 0 0 0 rgba(201, 79, 124, .55);
  animation: actspulse 3.4s ease-out infinite;
}
/* Le compte vit SUR le rond : trois gestes ou quinze, ça ne s'ouvre pas pareil. */
.acts-hub-n {
  position: absolute; top: -3px; right: -3px; min-width: 15px; height: 15px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px; background: var(--pink, #c94f7c); color: #fff;
  font-size: 9px; font-weight: 700; padding: 0 3px;
}
@keyframes actspulse {
  0%, 72% { box-shadow: 0 0 0 0 rgba(201, 79, 124, .5); }
  86% { box-shadow: 0 0 0 7px rgba(201, 79, 124, 0); }
  100% { box-shadow: 0 0 0 0 rgba(201, 79, 124, 0); }
}
.acts.open .acts-hub { transform: rotate(135deg); background: rgba(201, 79, 124, .72); opacity: 1; }
.acts.open .acts-hub-dot { animation: none; border-radius: 2px; width: 12px; height: 2px; }
.acts.open .acts-hub-n { opacity: 0; }

/* L'origine de l'éventail, c'est le CENTRE du hub — pas le coin du cadre.
   Mesuré à l'écran : posée sur le coin, la couronne partait 17 px trop à
   gauche et 17 px trop bas, et l'arc semblait tourné. (Le hub fait 34 px.) */
.acts-ring { position: absolute; left: 17px; bottom: 17px; width: 0; height: 0; }
.act {
  position: absolute; left: 0; bottom: 0; padding: 0;
  width: var(--asz, 46px); height: var(--asz, 46px); border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .28); background: rgba(12, 8, 14, .72);
  box-shadow: 0 3px 12px rgba(0, 0, 0, .5);
  color: #fff; cursor: pointer; overflow: visible;
  /* Fermé : tout est replié SOUS le hub. L'ouverture est donc un vrai
     déploiement, pas un fondu — le geste dit d'où ça vient. */
  transform: translate(-50%, 50%) scale(.2);
  opacity: 0; pointer-events: none;
  transition: transform .34s cubic-bezier(.22,1.2,.36,1), opacity .2s ease;
  /* Le repli se fait dans l'ORDRE INVERSE du déploiement : l'éventail se
     referme comme une main, il ne s'éteint pas d'un bloc. */
  transition-delay: var(--adc, 0ms);
}
.acts.open .act {
  transform: translate(calc(var(--ax) - 50%), calc(var(--ay) + 50%)) scale(1);
  opacity: 1; pointer-events: auto;
  transition-delay: var(--ad, 0ms);
}
.act img, .act .act-mark {
  width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block;
  background: rgba(255, 255, 255, .06);
}
.act .act-mark {
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; letter-spacing: .02em; text-transform: uppercase; opacity: .9;
}
/* LA BANDE DE NOM — une seule, à droite du hub. Six légendes posées sous six
   pastilles se chevauchaient (vu à l'écran) ; celle-ci dit le geste survolé,
   et à défaut celui qui joue. */
.acts-name {
  position: absolute; left: 42px; bottom: 2px; z-index: 6;
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, .22); background: rgba(10, 7, 12, .72);
  backdrop-filter: blur(3px);
  font-size: 10px; letter-spacing: .03em; color: #fff; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; pointer-events: none;
  opacity: 0; transition: opacity .16s ease;
}
.acts.open .acts-name { opacity: 1; }
.acts-name.live { color: var(--pink, #c94f7c); border-color: rgba(201, 79, 124, .5); }
.act:hover { border-color: rgba(255, 255, 255, .6); }
.act.on { border-color: var(--pink, #c94f7c); box-shadow: 0 0 0 2px rgba(201, 79, 124, .45), 0 3px 12px rgba(0, 0, 0, .5); }
/* Le voile : les pastilles se lisent sur un portrait de plein jour sans
   masquer ce qu'on est en train de regarder. */
.acts.open::before {
  content: ''; position: absolute; left: -10px; bottom: -10px;
  width: var(--aw, 280px); height: var(--ah, 340px);
  background: radial-gradient(120% 120% at 0% 100%, rgba(8, 5, 10, .62), rgba(8, 5, 10, 0) 72%);
  border-radius: 14px; pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
  .act, .acts-hub { transition-duration: .01ms; }
  .acts-hub-dot { animation: none; }
}

/* ═══════════════ LE JOYAU DE LA VOIE 3D ═══════════════
   Phil, 16/09 : « "🧍 Her body & moves" should be more lightful and more blink
   blink … this part is the hardest complex part that we almost got near the
   videogame production lvl ».
   Trois règles tenues ici :
   · l'intensité ENCODE le rang (pas de décor gratuit) — `data-rank` sur le
     bouton pilote le halo : `new` = plein, `body` = moyen, `full` = braise,
     `open` = éteint, le bouton ayant déjà fait son travail ;
   · rien ne tourne en boucle — le scintillement est une rafale d'une seconde
     que le JS déclenche — jamais une boucle sans fin (anti-surchauffe) ;
   · jamais deux fois pareil — sens du balayage, place et retard des étincelles
     sont tirés au sort à chaque rafale (variables CSS posées par le JS). */
#lane-3d .lane-cap {
  opacity: .92;
  background: linear-gradient(94deg, #f3c07a 0%, #ff92b6 42%, #8fd2ff 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.lane-jewel {
  position: relative; isolation: isolate;
  padding: 7px 15px; font-size: 12.5px; font-weight: 700; letter-spacing: .015em;
  color: #ffeaf2; border: 1px solid transparent; border-radius: 9px;
  /* Le liseré est un dégradé, pas une couleur : deux calques, l'un pour le
     fond (padding-box), l'autre pour le bord (border-box). */
  background:
    linear-gradient(var(--panel), var(--panel)) padding-box,
    linear-gradient(118deg, #c94f7c, #f0b45a 38%, #4f9cc9 72%, #c94f7c) border-box;
  box-shadow: 0 6px 18px -10px rgba(201, 79, 124, .75);
  transition: transform .18s cubic-bezier(.2,.9,.3,1), box-shadow .18s ease;
}
.lane-jewel .jw-text { position: relative; z-index: 2; }
.lane-jewel:hover:not(:disabled) {
  background:
    linear-gradient(var(--bg2), var(--bg2)) padding-box,
    linear-gradient(118deg, #ff7aa6, #ffd08a 38%, #8fd2ff 72%, #ff7aa6) border-box;
  color: #fff; transform: translateY(-1px);
  box-shadow: 0 10px 22px -10px rgba(201, 79, 124, .9);
}
.lane-jewel:active:not(:disabled) { transform: translateY(0); }
/* LE HALO — statique par construction : une couche composée qu'on ne repeint
   jamais. Seule son opacité change, et seulement quand le rang change. */
.lane-jewel::before {
  content: ''; position: absolute; inset: -7px; z-index: -1; border-radius: 14px;
  background:
    radial-gradient(62% 130% at 26% 0%, rgba(201, 79, 124, .85), rgba(201, 79, 124, 0) 70%),
    radial-gradient(62% 130% at 82% 100%, rgba(79, 156, 201, .62), rgba(79, 156, 201, 0) 70%),
    radial-gradient(50% 120% at 54% 50%, rgba(240, 180, 90, .40), rgba(240, 180, 90, 0) 72%);
  filter: blur(7px); opacity: var(--jw-halo, .55);
  transition: opacity .5s ease;
}
/* LA RAFALE — un éclat qui traverse. Le fond voyage DANS la boîte : il est
   découpé par le rayon du bouton sans `overflow`, donc sans couper le halo. */
.lane-jewel::after {
  content: ''; position: absolute; inset: 0; z-index: 1; border-radius: 9px;
  pointer-events: none; opacity: 0;
  background: linear-gradient(100deg,
    rgba(255, 255, 255, 0) 42%, rgba(255, 240, 248, 1) 50%, rgba(255, 255, 255, 0) 58%);
  background-size: 300% 100%;
  background-position: var(--jw-from, 190%) 0;
  mix-blend-mode: screen;
}
.lane-jewel.gleam::after { animation: jw-sheen 1.05s cubic-bezier(.35,.7,.3,1) 1; }
@keyframes jw-sheen {
  0%   { opacity: 0;  background-position: var(--jw-from, 190%) 0; }
  16%  { opacity: .78; }
  100% { opacity: 0;  background-position: var(--jw-to, -90%) 0; }
}
/* LES ÉTINCELLES — trois éclats à quatre branches, placés au hasard autour du
   bouton à chaque rafale. Hors rafale elles sont invisibles ET immobiles. */
.jw-spark {
  position: absolute; z-index: 3; pointer-events: none; opacity: 0;
  left: var(--x, 20%); top: var(--y, -20%);
  width: var(--s, 8px); height: var(--s, 8px); margin: calc(var(--s, 8px) / -2);
  background: #fff;
  clip-path: polygon(50% 0%, 58% 42%, 100% 50%, 58% 58%, 50% 100%, 42% 58%, 0% 50%, 42% 42%);
  filter: drop-shadow(0 0 4px rgba(255, 190, 220, .95));
}
.lane-jewel.gleam .jw-spark { animation: jw-spark .92s ease-out var(--d, 0s) 1; }
@keyframes jw-spark {
  0%   { opacity: 0; transform: scale(.15) rotate(0deg); }
  30%  { opacity: 1; transform: scale(1) rotate(32deg); }
  100% { opacity: 0; transform: scale(.3) rotate(74deg); }
}
/* LE RANG — le seul endroit où l'intensité est décidée. */
.lane-jewel[data-rank="new"]  { --jw-halo: .95; }
.lane-jewel[data-rank="body"] { --jw-halo: .76; }
.lane-jewel[data-rank="full"] { --jw-halo: .58; }
.lane-jewel[data-rank="open"] { --jw-halo: .18; box-shadow: none; }
/* Le halo REPREND SON SOUFFLE pendant la rafale — c'est ce battement qu'on
   voit du coin de l'œil, plus que l'éclat lui-même. Animation bornée : elle
   meurt avec la rafale, elle ne tourne jamais en boucle. */
.lane-jewel.gleam::before { animation: jw-halo 1.25s cubic-bezier(.3,.8,.35,1) 1; }
@keyframes jw-halo {
  0%, 100% { opacity: var(--jw-halo, .55); }
  28%      { opacity: calc(var(--jw-halo, .55) + .5); }
}
@media (prefers-reduced-motion: reduce) {
  /* Le liseré et le halo restent — ils ne bougent pas. Tout ce qui scintille
     s'éteint, et le JS ne programme plus aucune rafale. */
  .lane-jewel { transition: none; }
  .lane-jewel.gleam::after, .lane-jewel.gleam .jw-spark,
  .lane-jewel.gleam::before { animation: none; }
}
