/* ══════════════════════════════════════════════════════════════════════════
   Vinyles · feuille unique (accueil public, application, page de partage)
   --------------------------------------------------------------------------
   Direction (v2, septembre 2026) : « papier et encre, et les pochettes ».
   La v1 avait son propre papier chaud et un orange d'étiquette ; l'app
   devient publique sur le portfolio, elle prend donc LA charte du site, celle
   que Sudoku et Le Cahier portent déjà : le papier du site, son encre, ses
   filets d'un pixel, et son unique accent, l'azur. Les pochettes restent les
   vedettes : l'interface autour d'elles est de l'encre et du papier.

   Cinq règles qui expliquent tout le reste :

   1. les JETONS sont ceux de static/style.css (le site), recopiés à la
      lettre : `--vy-bg` est `--bg`, `--vy-card` est `--card`, `--vy-muted-3`
      est `--muted-3` (le jeton AA des petits libellés). Deux feuilles qui
      partagent leurs valeurs se ressemblent sans qu'on ait à le vouloir ;
   2. UNE seule couleur d'accent, l'azur, et réservée à peu de choses :
      l'encoche de l'onglet actif, l'anneau de focus,
      le soulignement des liens, l'étiquette du disque du placeholder. Les
      étoiles de notation sont à l'ENCRE (pleines ou creuses), le bouton
      principal est NOIR sur papier (papier sur encre en sombre), avec la
      recette d'ombre exacte de `.btn-primary` du site ;
   3. aucune profondeur molle : une surface REMPLIE plutôt qu'un filet (le
      gris translucide `--vy-fill` d'une pastille ou d'un bouton, le fond
      `--vy-groupe` d'une carte), un voile. Seule la POCHETTE a une ombre,
      parce que c'est un objet posé ;
   4. deux rôles typographiques : Space Grotesk serrée pour les titres,
      Inter Tight pour TOUT le reste, y compris les petits libellés système
      (compteurs, étiquettes, années), en casse normale, à l'encre discrète,
      chiffres tabulaires partout où il y a un nombre. JetBrains Mono ne
      sert plus qu'à ce qui se recopie (un code, un secret, un import) ;
   5. des rayons continus, ceux d'une app d'Apple : 6px pour une pochette,
      12px pour un champ, 16px pour une carte, 22px pour une grande carte,
      la capsule pour un bouton, une pastille ou un groupe d'outils.

   Octobre 2026 (« un look vraiment app Apple 2027, très sobre, toutes les
   fonctionnalités disponibles ») : les règles 3 à 5 ont changé. Avant, les
   libellés étaient en mono capitales espacées, chaque pastille et chaque
   carte avait son filet d'un pixel et les rayons s'arrêtaient à 10px : trois
   voix pour une seule interface. Rien n'a été retiré : les mêmes boutons,
   aux mêmes places, parlent maintenant une seule langue.

   Polices : auto-hébergées par /static/fonts.css, chargé avant cette feuille.
   AUCUNE nouvelle famille, aucune requête tierce.

   La section « Paramètres » (septembre 2026) laisse le membre changer trois de
   ces règles pour lui-même, et seulement celles-là : la PAIRE de polices des
   rôles 4 (sept paires, les binaires de /static/vinyles-fonts.css), la
   COULEUR de l'accent de la règle 2 (sept teintes mesurées, l'azur restant
   celle d'usine) et la TAILLE du texte (`--vy-k`, qui multiplie toute taille
   de police sous 22 px ; les titres, déjà grands, et les planchers de 16 px
   des champs ne bougent pas). Tout vit dans la section 16, en fin de
   fichier, accroché à des attributs de <html> que `theme-init.js` pose avant
   le premier rendu.

   Clair/sombre : piloté par `data-theme` sur <html>, que `theme-init.js` pose
   AVANT le premier rendu, système résolu. D'où UN SEUL bloc sombre ici,
   accroché à l'attribut.

   Plan du fichier :
     1 · Jetons               9 · Feuilles (fiche, ajout, formulaire, filtres)
     2 · Socle               10 · Les 200 et le tirage au sort
     3 · Accueil public      11 · Modales
     4 · Coque               12 · Page de partage · 12 bis · Fil, membres, messages
     5 · Listes de pochettes 13 · Animations
     6 · Pour vous           14 · Téléphone et grands écrans
     7 · Plus                15 · Survol (toujours en fin de fichier)
     8 · Panneaux de « Plus »  16 · Paramètres (en fin de fichier)
   ══════════════════════════════════════════════════════════════════════════ */


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 1 · JETONS
   │ Les valeurs de static/style.css, recopiées. Les anciens noms de la v1
   │ (`--vy-surface`, `--vy-ink-3`, `--vy-line-fort`) restent des ALIAS : ils
   │ pointent sur les nouveaux jetons, aucune règle ne les redéfinit.
   └────────────────────────────────────────────────────────────────────────── */

:root {
  /* Papier et surfaces (= --bg, --paper, --paper-2, --card du site) */
  --vy-bg: #fbfaf7;
  --vy-paper: #fafaf9;
  --vy-paper-2: #f5f5f4;
  --vy-card: #ffffff;
  --vy-surface: var(--vy-card);
  --vy-surface-2: var(--vy-paper-2);
  --vy-surface-3: #ebebe9;

  /* Encres (= --fg, --fg-soft, --muted, --muted-2, --muted-3 du site).
     Mesuré sur le papier : ink-2 9,9:1, muted 7,5:1, muted-3 5,1:1 (c'est le
     jeton AA des petits libellés et des placeholders), muted-2 4,5:1 tout
     juste : RÉSERVÉ aux tracés décoratifs et au texte large. */
  --vy-ink: #0a0a0a;
  --vy-ink-2: #404040;
  --vy-muted: #525252;
  --vy-muted-2: #737373;
  --vy-muted-3: #6f6a63;
  --vy-ink-3: var(--vy-muted-2);

  --vy-line: rgba(0, 0, 0, .08);
  --vy-line-strong: rgba(0, 0, 0, .12);
  --vy-line-fort: var(--vy-ink);

  /* L'accent : l'azur du site, oklch(0.62 0.18 210). Trois rôles :
     `--vy-accent` pour ce qui est tracé, point ou encoche (3,0:1 sur le
     papier : du graphique, jamais du texte) ;
     `--vy-accent-texte` quand il porte des lettres (5,0:1 sur le papier) ;
     `--vy-accent-doux` pour un fond à peine teinté. */
  --vy-accent: oklch(0.62 0.18 210);
  --vy-accent-texte: oklch(0.50 0.15 210);
  --vy-accent-doux: oklch(0.955 0.025 210);
  /* Un APLAT d'accent qui porte des lettres (pastille « Prochain », rayon,
     puce de rayon) et l'encre de ces lettres. En clair c'est l'accent de
     texte sous du papier ; en sombre, l'accent clair sous de l'encre : le
     papier sur l'accent clair ne tenait que 2,4:1 (mesuré avant la section
     Paramètres), une vraie faute du mode sombre. */
  --vy-accent-plein: var(--vy-accent-texte);
  --vy-on-accent: #fbfaf7;

  /* La taille du texte des Paramètres : le multiplicateur de toute taille de
     police sous 22 px. 1 = l'app telle qu'elle a été dessinée. */
  --vy-k: 1;

  /* Le bouton principal du site : noir sur papier, et sa recette d'ombre. */
  --vy-btn-bg: #0a0a0a;
  --vy-btn-fg: #fafaf9;
  --vy-btn-ombre: 0 1px 2px rgba(0, 0, 0, .10), inset 0 1px 0 rgba(255, 255, 255, .08);

  /* Couleurs de sens : jamais du texte de contenu, seulement un tracé, un
     aplat, ou un libellé court sur sa teinte. */
  --vy-vert: #2f8f66;
  --vy-rouge: #c33b3b;
  --vy-rouge-texte: #a52f2f;
  --vy-tint-rouge: #fae6e6;
  --vy-tint-vert: #e6f2ec;
  --vy-tint-or: #f8efdc;

  /* Rayons : pochette, bouton ou champ, conteneur, grande carte. Octobre
     2026 : les rayons continus d'une app d'Apple (avant : 4, 8, 10, 10). */
  --vy-r-0: 6px;
  --vy-r-1: 12px;
  --vy-r-2: 16px;
  --vy-r-3: 22px;

  /* Les REMPLISSAGES d'iOS (octobre 2026) : un gris translucide qui se pose
     sur n'importe quel fond, à la place du filet d'un pixel autour d'une
     pastille, d'un bouton secondaire ou de la piste d'un contrôle segmenté.
     `--vy-seg-on` : le segment choisi, un cran au-dessus de la piste. */
  --vy-fill: rgba(118, 118, 128, .12);
  --vy-fill-2: rgba(118, 118, 128, .2);
  --vy-seg-on: #ffffff;
  /* Le fond d'une CARTE posée sur la page (liste groupée, bandeau, tuile de
     statistique, carte de « Pour vous ») : sans filet autour d'elle, une carte
     blanche disparaissait sur le papier presque blanc ; elle prend donc un
     gris chaud, d'un cran sous le papier (le texte discret y tient 4,8:1).
     En sombre, c'est la carte, un cran au-dessus du papier. */
  --vy-groupe: #f2f1ee;
  /* Le filet d'un lien discret (un artiste, un genre : cf. section 13 bis). */
  --vy-lien-filet: rgba(10, 10, 10, .28);

  /* L'ombre d'une pochette : la seule ombre de la feuille, d'un pixel. */
  --vy-ombre-pochette: 0 1px 2px rgba(0, 0, 0, .14), 0 0 0 1px rgba(0, 0, 0, .06);
  --vy-ombre-hero: 0 1px 3px rgba(0, 0, 0, .18), 0 0 0 1px rgba(0, 0, 0, .06);
  /* La seule ombre PORTÉE de la feuille : une pochette posée sur une autre
     dans le bac (cf. 5b). Un filet d'un pixel la borde, pour une pochette
     blanche sur le papier. */
  --vy-ombre-bac: 0 30px 60px -22px rgba(0, 0, 0, .42), 0 10px 20px -10px rgba(0, 0, 0, .2), 0 0 0 1px rgba(0, 0, 0, .06);
  --vy-ombre-bac-haut: 0 48px 90px -26px rgba(0, 0, 0, .5), 0 16px 32px -14px rgba(0, 0, 0, .26), 0 0 0 1px rgba(0, 0, 0, .06);

  --vy-voile: rgba(251, 250, 247, .86);
  --vy-voile-fort: rgba(10, 10, 10, .48);

  --vy-titre: 'Space Grotesk', 'Inter Tight', system-ui, sans-serif;
  --vy-ui: 'Inter Tight', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --vy-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  --vy-top-h: 56px;
  --vy-nav: 0px;
  --vy-t: 140ms;
  --vy-t-2: 180ms;

  color-scheme: light;
}

/* ── Le SEUL bloc sombre du fichier (= [data-theme="dark"] du site) ───────── */
:root[data-theme="dark"] {
  --vy-bg: #111110;
  --vy-paper: #171716;
  --vy-paper-2: #222220;
  --vy-card: #1c1c1a;
  --vy-surface-3: #2a2a27;

  --vy-ink: #f5f4f1;
  --vy-ink-2: #c8c6c0;
  --vy-muted: #9a9896;
  --vy-muted-2: #7a7876;
  --vy-muted-3: #8a8884;

  --vy-line: rgba(255, 255, 255, .07);
  --vy-line-strong: rgba(255, 255, 255, .12);

  /* En sombre l'accent est assez clair pour porter du texte (8,3:1 sur le
     papier) : les deux jetons se rejoignent. */
  --vy-accent: oklch(0.72 0.15 210);
  --vy-accent-texte: oklch(0.72 0.15 210);
  --vy-accent-doux: oklch(0.26 0.04 210);
  --vy-accent-plein: var(--vy-accent);
  --vy-on-accent: #111110;

  --vy-btn-bg: #f5f4f1;
  --vy-btn-fg: #111110;
  --vy-btn-ombre: 0 1px 2px rgba(0, 0, 0, .3), inset 0 1px 0 rgba(255, 255, 255, .15);

  --vy-fill: rgba(118, 118, 128, .24);
  --vy-fill-2: rgba(118, 118, 128, .34);
  --vy-seg-on: #5a5a58;
  --vy-groupe: var(--vy-card);
  --vy-lien-filet: rgba(255, 255, 255, .3);

  --vy-vert: #4bbd8b;
  --vy-rouge: #ff7a7a;
  --vy-rouge-texte: #ff9a9a;
  --vy-tint-rouge: #2a1414;
  --vy-tint-vert: #12271f;
  --vy-tint-or: #2b2313;

  --vy-ombre-pochette: 0 1px 2px rgba(0, 0, 0, .5), 0 0 0 1px rgba(255, 255, 255, .06);
  --vy-ombre-hero: 0 1px 3px rgba(0, 0, 0, .6), 0 0 0 1px rgba(255, 255, 255, .07);
  --vy-ombre-bac: 0 30px 60px -22px rgba(0, 0, 0, .85), 0 10px 20px -10px rgba(0, 0, 0, .6), 0 0 0 1px rgba(255, 255, 255, .08);
  --vy-ombre-bac-haut: 0 48px 90px -26px rgba(0, 0, 0, .95), 0 16px 32px -14px rgba(0, 0, 0, .7), 0 0 0 1px rgba(255, 255, 255, .1);

  --vy-voile: rgba(17, 17, 16, .86);
  --vy-voile-fort: rgba(0, 0, 0, .62);

  color-scheme: dark;
}


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 2 · SOCLE
   └────────────────────────────────────────────────────────────────────────── */

*,
*::before,
*::after { box-sizing: border-box; }

/* `[hidden]` doit gagner sur TOUT `display` posé par une classe (un bouton
   en `inline-flex`, une grille, un flex). C'est la règle qui crée le piège
   qui doit le refermer : une seule fois, ici, pour toute la feuille. */
[hidden] { display: none !important; }

/* Le rectangle gris qu'iOS pose sur l'élément touché est la signature d'une
   page web : chaque bouton et chaque tuile a son propre retour à l'appui
   (`:active`, bloc « Les gestes : l'appui »), qui le remplace. Mesuré : il restait posé sur la
   tuile de départ PAR-DESSUS la pochette en vol. */
html { -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: transparent; }

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--vy-bg);
  color: var(--vy-ink);
  font: 400 calc(15px * var(--vy-k))/1.5 var(--vy-ui);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overscroll-behavior-y: none;
}

/* Une feuille ou un panneau ouvert : la page derrière ne défile plus, et la
   pastille « Admin » du Portfolio (z-index maximal, en bas à gauche, PILE
   sur les boutons d'un pied de feuille sur téléphone) s'efface le temps de
   l'ouverture. Le marqueur est posé par UN SEUL helper côté JS. */
body.has-modal { overflow: hidden; }
body.has-modal #mn-admin-pill { display: none; }

/* La même pastille, application ouverte : elle mesurait 206×52px posée
   PILE sur les onglets Collection et Souhaits (mesuré à 390×844 avec la
   session admin du site ouverte : `elementFromPoint` au centre de
   `#tabCollection` rendait la pastille, et le tap n'atteignait jamais
   l'onglet). On la remonte au-dessus de la barre d'onglets tant que la
   barre est en bas d'écran ; à partir de 900px la barre est une colonne et
   la pastille peut rester où elle est. La spécificité (2,0,1) bat le
   `#mn-admin-pill` (1,0,0) du <style> que le site injecte après nous. */
@media (max-width: 899px) {
  body:has(#scrApp:not([hidden])) #mn-admin-pill {
    bottom: calc(72px + env(safe-area-inset-bottom, 0px));
  }
}

h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--vy-titre);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

p { margin: 0; }
ul, ol, dl { margin: 0; padding: 0; list-style: none; }
b, strong { font-weight: 600; }
img { display: block; max-width: 100%; }
button, input, select, textarea { font-family: inherit; }

/* Des chiffres de même largeur partout où il y a un nombre (un compteur qui
   change ne fait plus bouger la ligne). Octobre 2026 : dans la police de
   l'interface, plus en mono (cf. la règle 4 en tête de fichier). */
.mono, .count, .kv dd, .tile-year, .stat-val, .bar-n, .track-n, .track-dur,
.result-hint, .share-views, .badge-num, .ess-rank, .tile-rank, .play-when {
  font-family: var(--vy-ui);
  font-variant-numeric: tabular-nums;
}

/* Le petit libellé système : la police de l'interface, 12px, en casse
   normale, à l'encre discrète (octobre 2026 ; avant : mono, 11px, capitales
   espacées). */
.lab {
  font-family: var(--vy-ui);
  font-size: calc(12px * var(--vy-k));
  font-weight: 500;
  color: var(--vy-muted-3);
}

::selection { background: var(--vy-accent-doux); color: var(--vy-ink); }

:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 2px; }
:focus:not(:focus-visible) { outline: none; }

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.icon-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* ── Icônes ─────────────────────────────────────────────────────────────── */
.ico {
  width: 20px;
  height: 20px;
  flex: none;
  fill: none;
  stroke: currentColor;
  /* 1,6 (octobre 2026, avant 1,7) : le trait régulier des symboles d'Apple. */
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: middle;
}

.link {
  color: var(--vy-ink);
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
  text-decoration-color: var(--vy-accent);
}

/* Un lien-bouton dans le texte (« Ouvrir » du doublon, « Annuler » d'un toast). */
.link-btn {
  display: inline;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  font: inherit;
  font-weight: 600;
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
  text-decoration-color: var(--vy-accent);
  cursor: pointer;
}

/* ── Logo ─────────────────────────────────────────────────────────────────
   Le nom de l'app est son logo : le bac de disques et « Vinyles », dessinés
   ensemble (septembre 2026). Posé en FOND et non en <img> : seule la
   variante du thème affiché est téléchargée, et la bascule de thème la
   remplace sans script. Le nom accessible est porté par role="img" et
   aria-label dans les gabarits. Il peut rétrécir (min-width: 0, contain)
   plutôt que pousser un bouton hors de la barre. */
.wordmark {
  display: inline-block;
  flex: 0 1 auto;
  min-width: 0;
  height: 40px;
  aspect-ratio: 889 / 256;
  background: url("brand/logo.png") left center / contain no-repeat;
}
:root[data-theme="dark"] .wordmark { background-image: url("brand/logo-dark.png"); }
.wordmark-sm { height: 28px; }

/* ── Boutons ──────────────────────────────────────────────────────────────── */
/* Octobre 2026 : des capsules, comme les boutons d'iOS. Le bouton courant
   est REMPLI du gris translucide (`--vy-fill`), sans filet ; le principal
   reste l'aplat d'encre ; le discret est un mot à la couleur de l'accent. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  padding: 9px 18px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--vy-fill);
  color: var(--vy-ink);
  font: 600 calc(14px * var(--vy-k))/1.2 var(--vy-ui);
  letter-spacing: -0.01em;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--vy-t) ease, color var(--vy-t) ease,
              border-color var(--vy-t) ease, transform var(--vy-t) ease,
              box-shadow var(--vy-t) ease;
}
.btn:active { transform: translateY(1px); }
.btn .ico { width: 17px; height: 17px; }

.btn-main {
  background: var(--vy-btn-bg);
  border-color: var(--vy-btn-bg);
  color: var(--vy-btn-fg);
  box-shadow: none;
}

.btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--vy-accent-texte);
}

.btn-danger {
  border-color: transparent;
  color: var(--vy-rouge-texte);
}

/* « Petit » par sa typographie et ses marges, jamais par sa hauteur : la cible
   reste au plancher tactile de 44px, sur téléphone comme à la souris. */
.btn-sm { min-height: 44px; padding: 8px 14px; font-size: calc(13.5px * var(--vy-k)); }
.btn[disabled] { opacity: .5; cursor: default; }
.btn-block { width: 100%; }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.btn-row .btn { flex: 1 1 auto; min-width: 0; }

.icon-btn {
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex: none;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--vy-ink);
  cursor: pointer;
  transition: color var(--vy-t) ease, background var(--vy-t) ease;
}
.icon-btn .ico { width: 22px; height: 22px; }
.icon-btn[aria-pressed="true"] .ico { fill: currentColor; }

/* ── Champs ───────────────────────────────────────────────────────────────── */
.field {
  display: grid;
  gap: 5px;
  min-width: 0;
}

.field-label {
  font-size: calc(12.5px * var(--vy-k));
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--vy-ink-2);
}

.field-help {
  font-size: calc(12.5px * var(--vy-k));
  line-height: 1.45;
  color: var(--vy-muted-3);
}

.field input,
.field select,
.field .field-menu-btn,
.field textarea,
.searchbar input {
  width: 100%;
  min-height: 44px;
  padding: 9px 12px;
  border: 1px solid var(--vy-line-strong);
  border-radius: var(--vy-r-1);
  background: var(--vy-card);
  color: var(--vy-ink);
  font: 400 16px/1.35 var(--vy-ui);
  appearance: none;
  -webkit-appearance: none;
  transition: border-color var(--vy-t) ease, box-shadow var(--vy-t) ease;
}
.field textarea { min-height: 96px; resize: vertical; line-height: 1.45; }
.field input:focus,
.field select:focus,
.field .field-menu-btn:focus-visible,
.field textarea:focus,
.searchbar input:focus { outline: none; border-color: var(--vy-ink); box-shadow: inset 0 0 0 1px var(--vy-ink); }

/* Placeholders : `--vy-muted-3`, le jeton AA du dépôt (5,1:1 sur le papier,
   4,8:1 en sombre). L'ancien `ink-3` tombait à 4,4:1 en clair, sous le seuil
   tenu partout ailleurs. L'italique les distingue du texte saisi. */
.field input::placeholder,
.field textarea::placeholder,
.searchbar input::placeholder { color: var(--vy-muted-3); font-style: italic; opacity: 1; }

/* Une seule croix d'effacement : la maison. Chromium et Safari en dessinent
   une native dans un `type="search"`, à côté de la nôtre (vu au navigateur :
   deux croix côte à côte). */
#searchInput::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; display: none; }

.field select,
.field .field-menu-btn {
  padding-right: 28px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236f6a63' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  background-size: 16px;
}

.field input[type="file"] {
  padding: 8px 10px;
  font-size: calc(14px * var(--vy-k));
}
.field input[type="file"]::file-selector-button {
  min-height: 28px;
  margin-right: 10px;
  padding: 4px 10px;
  border: 1px solid var(--vy-line-strong);
  border-radius: var(--vy-r-0);
  background: var(--vy-card);
  color: var(--vy-ink);
  font: 500 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
}

.field input[type="date"] { min-width: 0; }
.field input[type="number"] { -moz-appearance: textfield; }

.field-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
.field-row-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.stack-form { display: grid; gap: 12px; }

.form-error,
.form-ok {
  padding: 9px 12px;
  border-radius: var(--vy-r-1);
  font-size: calc(14px * var(--vy-k));
  line-height: 1.4;
}
.form-error { background: var(--vy-tint-rouge); color: var(--vy-rouge-texte); }
.form-ok { background: var(--vy-tint-vert); color: var(--vy-ink); }

.state-line { font-size: calc(14px * var(--vy-k)); color: var(--vy-muted); }
.state-line b { color: var(--vy-ink); }

/* Le doublon annoncé dans le formulaire : une ligne, jamais un blocage. */
.dup-notice {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 12px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-1);
  background: var(--vy-paper-2);
  font-size: calc(13.5px * var(--vy-k));
  line-height: 1.4;
}
.dup-notice > .ico { flex: none; color: var(--vy-muted); }
.dup-notice p { flex: 1 1 auto; min-width: 0; }

/* ── Contrôle segmenté : celui d'iOS (octobre 2026). Une piste remplie, et
   le choix courant posé dessus, un cran plus clair, avec une ombre douce.
   Avant : une bande sous un filet, le choix souligné d'accent. ───────────── */
.seg {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2px;
  padding: 2px;
  border: 0;
  border-radius: var(--vy-r-1);
  background: var(--vy-fill);
  overflow: hidden;
}
.seg-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }

.seg-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 40px;
  padding: 8px 6px;
  border: 0;
  border-radius: calc(var(--vy-r-1) - 2px);
  background: transparent;
  color: var(--vy-ink-2);
  font: 500 calc(13.5px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
  transition: color var(--vy-t) ease, background var(--vy-t) ease, box-shadow var(--vy-t) ease;
}
.seg-btn .ico { width: 17px; height: 17px; }
.seg-btn:focus-visible { outline-offset: -3px; }

.seg-btn.is-on,
.seg-btn[aria-checked="true"] {
  background: var(--vy-seg-on);
  color: var(--vy-ink);
  font-weight: 600;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 0 0 .5px rgba(0, 0, 0, .04);
}

/* ── Interrupteur ─────────────────────────────────────────────────────────── */
.switch-list { display: grid; gap: 2px; }

.switch {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr);
  gap: 12px;
  align-items: center;
  min-height: 46px;
  padding: 4px 0;
  cursor: pointer;
}

.switch input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.switch-box {
  position: relative;
  width: 46px;
  height: 28px;
  border-radius: 999px;
  background: var(--vy-fill-2);
  border: 0;
  transition: background var(--vy-t) ease, border-color var(--vy-t) ease;
}
/* Le bouton de l'interrupteur d'iOS : blanc dans les deux thèmes, posé
   d'une ombre, sur une piste grise (éteint) ou verte (allumé). */
.switch-box::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 24px;
  height: 24px;
  border-radius: 999px;
  background: #ffffff;
  box-shadow: 0 2px 5px rgba(0, 0, 0, .18), 0 0 0 .5px rgba(0, 0, 0, .04);
  transition: transform var(--vy-t) ease, background var(--vy-t) ease;
}
.switch input:checked + .switch-box { background: var(--vy-vert); border-color: var(--vy-vert); }
.switch input:checked + .switch-box::after { transform: translateX(18px); background: #ffffff; }
.switch input:focus-visible + .switch-box { outline: 2px solid var(--vy-accent); outline-offset: 2px; }

.switch-lab { font-size: calc(14.5px * var(--vy-k)); line-height: 1.4; color: var(--vy-ink); min-width: 0; }

/* ── Pastilles, badges : des capsules REMPLIES du gris translucide, en casse
   normale (octobre 2026 ; avant : les puces du site, mono, capitales, filet) ─ */
.chips-row {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  scrollbar-width: none;
  padding-bottom: 2px;
  -webkit-overflow-scrolling: touch;
}
.chips-row::-webkit-scrollbar { display: none; }
.chips-wrap { flex-wrap: wrap; overflow: visible; }

/* Un conteneur qui défile doit le dire (règle de la maison) : un voile en
   dégradé du côté où il reste du contenu, et seulement de ce côté. Les
   classes viennent d'app.js (`cueDefilement`), d'après scrollLeft et
   scrollWidth : un dégradé permanent effacerait la dernière puce d'une
   rangée qui tient entière. */
.has-more-right {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 36px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 36px), transparent);
}
.has-more-left {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 36px);
  mask-image: linear-gradient(to right, transparent, #000 36px);
}
.has-more-left.has-more-right {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 36px, #000 calc(100% - 36px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 36px, #000 calc(100% - 36px), transparent);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
  min-height: 32px;
  padding: 6px 12px;
  border: 0;
  border-radius: 999px;
  background: var(--vy-fill);
  color: var(--vy-ink-2);
  font: 500 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  white-space: nowrap;
}
.chip .ico { width: 14px; height: 14px; }

button.chip { cursor: pointer; min-height: 44px; }
button.chip.is-on,
.chip.is-on {
  background: var(--vy-ink);
  color: var(--vy-bg);
}
.chip-x { display: inline-grid; place-items: center; width: 18px; height: 18px; margin-right: -5px; border-radius: 999px; background: rgba(255, 255, 255, .18); }
.chip-x .ico { width: 12px; height: 12px; }

.badges { display: flex; flex-wrap: wrap; gap: 6px; }

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 24px;
  padding: 2px 9px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--vy-fill);
  color: var(--vy-ink-2);
  font: 500 calc(12px * var(--vy-k))/1.2 var(--vy-ui);
}
.badge .ico { width: 14px; height: 14px; }
.badge-accent { background: var(--vy-ink); color: var(--vy-bg); font-weight: 600; }
.badge-ok { background: var(--vy-tint-vert); border-color: transparent; }

/* Un badge-bouton : celui de l'essentiel dans la fiche. */
button.badge { cursor: pointer; min-height: 32px; font: inherit; font-size: calc(12.5px * var(--vy-k)); font-weight: 500; }

/* ── Étoiles : à l'ENCRE, pleines ou creuses ─────────────────────────────── */
.stars { display: flex; gap: 0; margin-left: -8px; }

.star {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--vy-muted-2);
  cursor: pointer;
  transition: color var(--vy-t) ease, transform var(--vy-t) ease;
}
.star .ico { width: 24px; height: 24px; }
.star.is-on { color: var(--vy-ink); }
.star.is-on .ico { fill: currentColor; }
.star:active { transform: scale(.9); }

.stars-static { pointer-events: none; }
.stars-static .star { width: 28px; height: 28px; }
.stars-static .star .ico { width: 20px; height: 20px; }

/* ── Définitions, blocs ───────────────────────────────────────────────────── */
.kv {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px 16px;
}
.kv > div { min-width: 0; }
.kv dt {
  font-family: var(--vy-ui);
  font-size: calc(12px * var(--vy-k));
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--vy-muted-3);
}
.kv dd {
  margin: 2px 0 0;
  font-size: calc(15px * var(--vy-k));
  overflow-wrap: anywhere;
}

.block { display: grid; gap: 10px; }

.block-title {
  font-family: var(--vy-ui);
  font-size: calc(12px * var(--vy-k));
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--vy-muted-3);
}

.block-help { font-size: calc(13.5px * var(--vy-k)); line-height: 1.5; color: var(--vy-muted); }
.empty-line { font-size: calc(14px * var(--vy-k)); color: var(--vy-muted); }
.loading { font-size: calc(14px * var(--vy-k)); color: var(--vy-muted); }

/* Un bloc dangereux (supprimer son compte) : un filet rouge, pas un aplat. */
.block-danger {
  padding: 14px;
  border: 1px solid var(--vy-rouge);
  border-radius: var(--vy-r-2);
}

/* ── État vide ────────────────────────────────────────────────────────────── */
.empty {
  display: grid;
  justify-items: center;
  gap: 8px;
  padding: 32px 16px 24px;
  text-align: center;
}
.empty-ico {
  width: 44px;
  height: 44px;
  fill: none;
  stroke: var(--vy-muted-2);
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.empty-title { font-family: var(--vy-titre); font-size: calc(19px * var(--vy-k)); font-weight: 600; }
.empty-text { max-width: 36ch; font-size: calc(14.5px * var(--vy-k)); line-height: 1.5; color: var(--vy-muted); }
.empty .btn { margin-top: 8px; }

/* Squelettes de chargement : des blocs de papier, immobiles (aucune
   animation continue dans cette feuille). */
.skel { display: grid; gap: 10px; }
.skel-row {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: 12px;
  align-items: center;
  min-height: 72px;
}
.skel-box, .skel-line {
  background: var(--vy-paper-2);
  border-radius: var(--vy-r-0);
}
.skel-box { width: 64px; aspect-ratio: 1; }
.skel-line { height: 12px; margin: 6px 0; }
.skel-line-short { width: 55%; }
.skel-rail { display: flex; gap: 12px; overflow: hidden; }
.skel-rail .skel-box { width: 156px; flex: none; }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 3 · ACCUEIL PUBLIC : LA VITRINE
   │ Un objet, pas une page de vente (la leçon du Cahier) : le bac de
   │ démonstration en haut, à feuilleter, la fiche de connexion en
   │ INTERCALAIRE (ses onglets sont les languettes d'un séparateur de bac),
   │ puis le dos de la pochette : les pistes, ce que fait l'app, et les
   │ crédits. Rien de la silhouette générée d'avant : ni sur-titre, ni
   │ slogan sur trois lignes, ni grille de fonctionnalités numérotées, ni
   │ carte flottante. Sur téléphone tout s'empile ; à partir de 900px le
   │ titre et la fiche se posent dans les deux coins vides de la scène.
   └────────────────────────────────────────────────────────────────────────── */

.landing {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

.landing-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 14px 20px;
  padding-top: max(14px, env(safe-area-inset-top));
}

.theme-btn {
  width: 40px;
  height: 40px;
  border: 1px solid transparent;
  background: var(--vy-fill);
  color: var(--vy-muted);
}
.theme-btn .ico { width: 20px; height: 20px; }

/* ── La vitrine : le titre, le bac de démonstration, la fiche ──────────── */
.vitrine {
  position: relative;
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
}

.vitrine-titre { display: grid; gap: 8px; padding: 4px 20px 14px; }
.vitrine-h1 {
  font-size: clamp(26px, 6vw, 40px);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.06;
  max-width: 18ch;
}
.vitrine-sub { font-size: calc(15px * var(--vy-k)); line-height: 1.5; color: var(--vy-muted); max-width: 40ch; }

/* Le bac de démonstration : la même matière que celui de la collection
   (section 5b). Sa hauteur est fixée ICI : le script ne la cale que dans
   l'app, où le bac prend tout l'écran. Deux classes pour battre `.pile`
   et `.pile-foot`, déclarés plus bas : sans elles, la marge négative du
   bac (pleine largeur dans l'app) faisait déborder l'accueil de 16px de
   chaque côté (mesuré à 390 : une page de 406). */
.vitrine .pile { margin: 0; height: auto; min-height: 0; }
/* La hauteur fixée est celle de la SCÈNE, pas du bac entier : ce que le pied
   porte en plus (le bouton lecture, la proposition « moins d'animations »)
   l'allonge par en dessous au lieu de rétrécir les pochettes (mesuré à 320 :
   la proposition ramenait la pochette de devant de 93 à 47 px). 140 px est
   le pied de base (curseur, légende, note). */
.vitrine .pile-stage { flex: none; height: calc(max(340px, min(46vh, 520px)) - 140px); }
.vitrine .pile-foot {
  grid-template-areas: "range range" "caption count" "note note";
  border-top: 0;
}
.vitrine-note {
  grid-area: note;
  margin: 4px 0 0;
  font: 500 calc(12px * var(--vy-k))/1.45 var(--vy-ui);
  color: var(--vy-muted-3);
}

/* Les pochettes inventées : un SVG par carte, dessiné par app.js, dont les
   textes prennent les polices de l'app. */
.sleeve { display: block; width: 100%; height: 100%; }
.sleeve-artist { font: 600 6.5px var(--vy-titre); letter-spacing: -0.02em; }
.sleeve-title { font: 400 5px var(--vy-ui); }
.sleeve-huge { font: 600 46px var(--vy-titre); letter-spacing: -0.06em; }

/* ── La fiche de connexion en intercalaire ─────────────────────────────── */
/* Les onglets sont des languettes découpées dans le même carton que la
   fiche ; l'actif se soude à elle (il couvre le filet du haut). */
.auth-panel { position: relative; margin: 6px 16px 0; }

.auth-tabs {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  padding: 0 0 0 10px;
}
.auth-tabs.is-single .auth-tab { cursor: default; }

.auth-tab {
  position: relative;
  min-height: 44px;
  padding: 10px 16px 11px;
  border: 1px solid var(--vy-line-strong);
  border-bottom: 0;
  border-radius: 8px 8px 0 0;
  background: var(--vy-paper-2);
  color: var(--vy-muted);
  font: 500 calc(14px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
  transition: color var(--vy-t) ease, background var(--vy-t) ease;
}
.auth-tab.is-on {
  z-index: 1;
  margin-bottom: -1px;
  padding-bottom: 12px;
  background: var(--vy-card);
  color: var(--vy-ink);
  font-weight: 600;
}

.auth-card {
  display: grid;
  gap: 16px;
  padding: 18px;
  background: var(--vy-groupe);
  border: 1px solid transparent;
  border-radius: var(--vy-r-1);
}

.auth-form { display: grid; gap: 12px; }
.auth-form .btn-main { margin-top: 4px; }
.auth-note { text-align: center; }

/* ── Le dos de la pochette : les pistes et les crédits ─────────────────── */
.dos {
  display: grid;
  gap: 30px;
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 44px 20px 20px;
}
.dos-head { display: grid; gap: 6px; }

.dos-etiquette, .face-titre, .piste-n, .piste-d, .credits dt {
  font-family: var(--vy-ui);
  font-weight: 500;
}
.dos-etiquette { font-size: calc(11px * var(--vy-k)); color: var(--vy-muted-3); }
.dos-titre { font-size: 26px; letter-spacing: -0.025em; }

.faces { display: grid; gap: 28px; }
.face-titre {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--vy-ink);
  font-size: calc(12px * var(--vy-k));
  color: var(--vy-ink);
}

.pistes { display: grid; }
.piste {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr);
  gap: 3px 12px;
  align-items: baseline;
  padding: 12px 0;
  border-bottom: 1px solid var(--vy-line);
}
.piste-n { font-size: calc(11px * var(--vy-k)); color: var(--vy-muted-3); }
.piste-t { font-size: calc(15.5px * var(--vy-k)); line-height: 1.45; }
.piste-d { grid-column: 2; font-size: calc(11px * var(--vy-k)); color: var(--vy-muted-3); }

.notes {
  display: grid;
  gap: 12px;
  max-width: 64ch;
  font-size: calc(15px * var(--vy-k));
  line-height: 1.6;
  color: var(--vy-muted);
}

.credits {
  display: grid;
  gap: 10px;
  padding-top: 16px;
  border-top: 1px solid var(--vy-line-strong);
}
.credits > div {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  gap: 12px;
  align-items: baseline;
}
.credits dt { font-size: calc(11px * var(--vy-k)); color: var(--vy-ink-2); }
.credits dd { margin: 0; font-size: calc(13px * var(--vy-k)); line-height: 1.5; color: var(--vy-muted); }

.landing-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px 18px;
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 16px 20px;
  padding-bottom: calc(16px + env(safe-area-inset-bottom));
  font-size: calc(13px * var(--vy-k));
  text-align: center;
  color: var(--vy-muted-3);
}
.landing-foot nav { display: flex; gap: 16px; }
.landing-foot a { color: var(--vy-muted); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--vy-line-strong); }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 4 · COQUE
   │ Barre du haut collée, contenu, barre de navigation en bas (le pouce).
   │ À partir de 900px la barre devient une colonne à gauche : `--vy-nav`
   │ porte sa largeur, lue par la barre, le contenu et le bandeau.
   └────────────────────────────────────────────────────────────────────────── */

.shell {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: var(--vy-top-h);
  padding: 6px 12px;
  padding-top: max(6px, env(safe-area-inset-top));
  background: var(--vy-bg);
  border-bottom: 1px solid var(--vy-line);
}

.top-right {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  min-width: 0;
}

/* Le logo au MILIEU de la barre, dès qu'il y a la place pour lui.
   Il vivait dans le coin gauche, au-dessus d'une colonne de navigation qui
   commence 220 px plus loin : il y était échoué plutôt que posé. La barre
   passe donc en trois colonnes (vide · le logo · les outils), ce qui le
   centre sur la FENÊTRE et non sur ce qui reste à côté des boutons.

   Le seuil est mesuré, pas choisi : le groupe de droite fait 194 px plus
   12 px de marge, donc un logo centré de L px ne tient qu'à partir de
   412 + L px de fenêtre. Le logo de septembre 2026 fait 97 px (28 px de
   haut), soit 509 px ; à 560 il reste 25 px entre lui et le premier bouton
   (mesuré : 231 à 329, le groupe commence à 354). En dessous il reste à
   gauche, en flux, comme avant. */
@media (min-width: 560px) {
  .topbar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  }
  .topbar .wordmark { grid-column: 2; justify-self: center; }
  .topbar .top-right { grid-column: 3; justify-self: end; margin-left: 0; }
}

.top-btn { color: var(--vy-ink); }
.top-btn[aria-expanded="true"] { background: var(--vy-paper-2); }

/* Le bouton d'ajout est le seul aplat d'encre de la barre : c'est l'action
   de l'app, celle qu'on fait chez le disquaire, le disque encore en main. */
.top-btn-main {
  background: var(--vy-btn-bg);
  color: var(--vy-btn-fg);
  box-shadow: var(--vy-btn-ombre);
}

/* Collée sous la barre du haut. `top` = la hauteur RÉELLE de la barre,
   mesurée par app.js (`--vy-top-real`, ResizeObserver sur #topbar) : sous
   une encoche la barre grandit de (inset - 6px), et une valeur fixe de 56px
   laissait la recherche passer SOUS la barre pendant le défilement (mesuré
   avec un inset simulé de 47px : le champ était inatteignable). Le calcul
   CSS reste en repli tant que la mesure n'est pas posée. */
.searchbar {
  position: sticky;
  top: var(--vy-top-real, calc(var(--vy-top-h) + max(0px, env(safe-area-inset-top, 0px) - 6px)));
  z-index: 29;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: var(--vy-bg);
  border-bottom: 1px solid var(--vy-line);
}
.searchbar > .ico { color: var(--vy-muted); }
.searchbar input { flex: 1 1 auto; min-width: 0; }
/* `flex: none` : comprimé par le champ, le bouton tombait à 35px (mesuré). */
.searchbar .icon-btn { flex: none; }
.searchbar-inline { position: static; padding: 0; border: 0; background: transparent; }

/* ── Bandeaux ─────────────────────────────────────────────────────────────── */
.banner {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 12px 16px 0;
  margin-left: calc(16px + var(--vy-nav));
  padding: 12px 14px;
  border: 0;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
  font-size: calc(14px * var(--vy-k));
  line-height: 1.45;
}
.banner p { min-width: 0; flex: 1 1 auto; }
.banner .btn { flex: none; }
.banner > .ico { flex: none; color: var(--vy-muted); }
.banner-warn { background: var(--vy-tint-or); border-color: transparent; }
.banner-warn > .ico { color: var(--vy-ink-2); }
.banner-offline { background: var(--vy-paper-2); border-color: transparent; color: var(--vy-ink-2); }

/* ── Contenu ──────────────────────────────────────────────────────────────── */
.main {
  flex: 1 1 auto;
  width: 100%;
  min-width: 0;
  padding: 14px 16px calc(86px + env(safe-area-inset-bottom));
  padding-left: calc(16px + var(--vy-nav));
}

/* `minmax(0, 1fr)` et non la colonne implicite `auto` : une piste `auto`
   grandit jusqu'à la largeur minimale de son contenu, et une seule ligne
   insécable (un `.count` en `nowrap`) élargissait alors TOUTE la vue au-delà
   de l'écran. Mesuré à 320 px, texte « Très grande » : « Pour vous » à
   367 px pendant son chargement. */
.view { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; align-content: start; }

/* Le grand titre d'iOS (octobre 2026) : gras, serré. 28 px et pas 34 : la
   barre du haut porte déjà le logo, et le bac a besoin de la hauteur. */
.view-title { font-size: 28px; font-weight: 700; letter-spacing: -0.03em; line-height: 1.12; }
.view-sub { font-size: calc(13.5px * var(--vy-k)); line-height: 1.45; color: var(--vy-muted); }

/* L'en-tête commun des pages (octobre 2026) : le titre, et à sa droite ce
   qui dit d'où vient la page (les sources des 200) ; la barre (le compteur,
   puis les outils) suit, serrée sous le titre pour qu'ils se lisent comme un
   seul bloc. Le même partout : on sait où l'on est et où chercher le tri,
   la vue et le filtre, quelle que soit la page. */
.view-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
  min-height: 32px;
  margin-bottom: -6px;
}
.view-head .view-title { min-width: 0; overflow-wrap: anywhere; }
.view-head-act { flex: none; margin: -6px -10px -6px 0; }

/* ── Barre d'onglets : cinq entrées ──────────────────────────────────────── */
.tabbar {
  position: fixed;
  z-index: 40;
  left: 0;
  right: 0;
  bottom: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 2px;
  padding: 6px 6px;
  padding-bottom: calc(6px + env(safe-area-inset-bottom));
  background: var(--vy-bg);
  border-top: 1px solid var(--vy-line);
}

.tab {
  position: relative;
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 3px;
  min-height: 48px;
  padding: 5px 2px;
  border: 0;
  border-radius: var(--vy-r-1);
  background: transparent;
  color: var(--vy-muted);
  /* La barre d'onglets est du mobilier, pas du texte de lecture : elle ne
     suit ni la « Taille du texte » ni la police « Machine à écrire » des
     Paramètres (comme la barre d'onglets d'iOS, qui ne suit pas la taille
     du texte du système). Mesuré à 320 px, « Machine à écrire » en très
     grand : « Collection » demandait 76 px dans un onglet de 56. */
  font: 500 11px/1.2 var(--vy-ui-nav, var(--vy-ui));
  cursor: pointer;
  transition: color var(--vy-t) ease;
}
.tab span {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tab.is-on { color: var(--vy-ink); font-weight: 600; }

/* L'onglet courant est marqué SUR LE FILET de la barre par une encoche
   d'accent, pas par un fond coloré derrière le mot. */
.tab.is-on::after {
  content: "";
  position: absolute;
  top: -7px;
  left: 50%;
  width: 24px;
  height: 2px;
  margin-left: -12px;
  background: var(--vy-accent);
}

/* ── Le message éphémère ──────────────────────────────────────────────────── */
.toast {
  position: fixed;
  z-index: 95;
  left: 50%;
  bottom: calc(84px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: min(92vw, 460px);
  padding: 11px 16px;
  border-radius: 999px;
  background: var(--vy-ink);
  color: var(--vy-bg);
  font-size: calc(14px * var(--vy-k));
  font-weight: 500;
  text-align: center;
}
.toast .link-btn {
  flex: none;
  min-height: 32px;
  padding: 0 4px;
  color: var(--vy-bg);
  text-decoration-color: var(--vy-accent);
}


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 5 · LISTES DE POCHETTES
   │ Le bac : des carrés bord à bord, gap 8px, coins 4px. Le placeholder est
   │ un disque monoligne avec les initiales de l'artiste.
   └────────────────────────────────────────────────────────────────────────── */

.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 44px;
}

/* Sur une ligne, quoi qu'il arrive : « 12 » seul au-dessus d'« albums » à
   320px, mesuré, et c'est une donnée. */
.count {
  font-size: calc(14px * var(--vy-k));
  color: var(--vy-muted-3);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.toolbar-tools { display: flex; align-items: center; gap: 2px; flex: none; }
/* Les outils d'une page forment UN groupe (octobre 2026), une capsule
   remplie comme les groupes de boutons d'une barre d'iOS : trier, la vue,
   filtrer, au hasard se lisent comme un seul objet, à écarts égaux. Chaque
   bouton garde sa cible de 44 px : la capsule fait 44 px de haut. */
.toolbar .toolbar-tools {
  gap: 0;
  padding: 0;
  border-radius: 999px;
  background: var(--vy-fill);
}
.toolbar .toolbar-tools:empty { display: none; }
/* Le bloc du titre est un enfant flex, donc `min-width: auto` : sans ce
   zéro, il refusait de descendre sous la largeur de sa ligne `.count`, dont
   l'ellipse ne se déclenchait jamais. Mesuré à 320 px en texte « Très
   grande » : « Calculé d'après votre collection » poussait « Pour vous » à
   367 px de large pendant le chargement. */
.toolbar > :not(.toolbar-tools) { min-width: 0; }

.tool-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 44px;
  padding: 6px 10px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--vy-ink-2);
  font: 500 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
  transition: color var(--vy-t) ease, background var(--vy-t) ease;
}
.tool-btn .ico { width: 20px; height: 20px; }
.tool-ico { width: 44px; padding: 0; justify-content: center; }
.tool-btn.is-on { color: var(--vy-ink); }
.tool-btn.is-on .ico { stroke-width: 2.2; }
.tool-lab { white-space: nowrap; }

.tiles {
  display: grid;
  grid-template-columns: repeat(var(--vy-cols, 3), minmax(0, 1fr));
  gap: 14px 8px;
}

.tile {
  display: grid;
  gap: 5px;
  align-content: start;
  min-width: 0;
  padding: 0;
  border: 0;
  border-radius: var(--vy-r-0);
  background: transparent;
  color: var(--vy-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  /* Une grille de cinq mille disques : le navigateur ne peint que ce qui
     approche de l'écran, et réserve la place du reste. */
  content-visibility: auto;
  contain-intrinsic-size: auto 160px;
}
.tile:focus-visible { outline-offset: 3px; }

.tile-cover {
  position: relative;
  aspect-ratio: 1;
  border-radius: var(--vy-r-0);
  overflow: hidden;
  background: var(--vy-paper-2);
  box-shadow: var(--vy-ombre-pochette);
}
.tile-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Le placeholder : un disque qui dépasse du bord droit de la pochette, et
   les initiales de l'artiste à gauche. Construit par createElementNS. Les
   initiales portent un HALO de la couleur du fond (`paint-order: stroke`) :
   à deux lettres, la seconde passait SOUS le disque et se dessinait dans la
   même encre que lui (mesuré : « FM » lu « FI »). */
.ph {
  width: 100%;
  height: 100%;
  display: block;
  background: #c4a77c url("casier/pochette.webp") center / cover no-repeat;
}
/* Le disque de la vignette est le même vinyle que celui de la fiche et du
   tirage : le corps et les sillons de la platine (section 18.3), noirs dans
   les deux thèmes (à l'encre, il devenait crème en sombre), et le liseré du
   bord qui le détache du papier sombre. */
.ph-disc { fill: var(--vy-platine-corps); stroke: var(--vy-platine-rim); stroke-width: 1.2; }
.ph-groove { fill: none; stroke: var(--vy-platine-sillon); stroke-width: 1; }
.ph-label { fill: var(--vy-accent); }
.ph-hole { fill: #c4a77c; }
.ph-init {
  fill: #2b2013;
  font-family: var(--vy-titre);
  font-weight: 600;
  font-size: 23px;
  letter-spacing: -0.03em;
  paint-order: stroke;
  stroke: #c4a77c;
  stroke-width: 6px;
  stroke-linejoin: round;
}
.ph-big .ph-init { font-size: calc(20px * var(--vy-k)); }

.tile-badge {
  position: absolute;
  top: 6px;
  right: 6px;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-height: 22px;
  padding: 2px 6px;
  border-radius: 999px;
  background: rgba(10, 10, 10, .84);
  color: #fbfaf7;
  font: 600 calc(12px * var(--vy-k))/1 var(--vy-ui);
}
.tile-badge .ico { width: 12px; height: 12px; }
.tile-badge-fav { color: #fbfaf7; }
.tile-badge-fav .ico { fill: currentColor; }

/* La pastille de rang d'un essentiel : encre sur papier, discrète, en haut
   à gauche (en bas elle recouvrait les initiales du disque de remplacement).
   La médaille de l'onglet « Les 200 » + le numéro : « n° 12 » en toutes
   lettres ne tenait plus à côté de la note sur une tuile de 89px (trois
   colonnes à 320px, mesuré : les deux pastilles se chevauchaient). */
.tile-rank {
  position: absolute;
  left: 6px;
  top: 6px;
  display: inline-flex;
  align-items: center;
  gap: 3px;
  min-height: 20px;
  padding: 1px 6px;
  border: 1px solid rgba(10, 10, 10, .25);
  border-radius: 999px;
  background: #fbfaf7;
  color: #0a0a0a;
  font: 600 calc(12px * var(--vy-k))/1 var(--vy-ui);
}
.tile-rank .ico { width: 11px; height: 11px; stroke-width: 2; }

/* Le numéro d'un souhait dans la file d'achat, EN BAS À GAUCHE : le
   haut-gauche est à la médaille des 200, le haut-droit à la note. Le n° 1 se
   dit en toutes lettres (« Prochain ») et prend l'accent : c'est la réponse
   que la liste des souhaits apporte, pas un numéro de plus. */
.tile-file {
  position: absolute;
  left: 6px;
  bottom: 6px;
  display: inline-flex;
  align-items: center;
  min-height: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(10, 10, 10, .84);
  color: #fbfaf7;
  font: 600 calc(12px * var(--vy-k))/1 var(--vy-ui);
}
.tile-file.is-next { background: var(--vy-accent-plein); color: var(--vy-on-accent); }
/* Une tuile des 200 dit en bas à gauche si le disque est À MOI (l'accent :
   c'est ce qui ouvre ma fiche) ou dans mes souhaits, à la place du numéro de
   la file des souhaits ; « écouté » est une coche en haut à droite, la place
   de la note. Ce n'est pas une pastille décorative : elle reste quand on
   masque les pastilles (cf. 16.6), comme le numéro de la file. */
.tile-etat {
  position: absolute;
  left: 6px;
  bottom: 6px;
  display: inline-flex;
  align-items: center;
  min-height: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(10, 10, 10, .84);
  color: #fbfaf7;
  font: 600 calc(12px * var(--vy-k))/1 var(--vy-ui);
}
.tile-etat.is-mine { background: var(--vy-accent-plein); color: var(--vy-on-accent); }
.tile-artist {
  font-size: calc(12px * var(--vy-k));
  line-height: 1.3;
  color: var(--vy-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tile-title {
  font-size: calc(13px * var(--vy-k));
  font-weight: 600;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tile-year { font-size: calc(11px * var(--vy-k)); color: var(--vy-muted-3); }

/* Vue liste : une ligne par disque, pochette de 56px. */
.tiles.is-list { grid-template-columns: minmax(0, 1fr); gap: 0; }
.tiles.is-list .tile {
  grid-template-columns: 56px minmax(0, 1fr);
  grid-template-areas: "cover artist" "cover title";
  column-gap: 12px;
  row-gap: 2px;
  align-items: center;
  align-content: center;
  min-height: 72px;
  padding: 8px 0;
  border-radius: 0;
  border-bottom: 1px solid var(--vy-line);
  contain-intrinsic-size: auto 72px;
}
.tiles.is-list .tile-cover { grid-area: cover; }
.tiles.is-list .tile-artist { grid-area: artist; }
.tiles.is-list .tile-title { grid-area: title; font-size: calc(15px * var(--vy-k)); white-space: normal; }
/* 11px et pas 10,5 : la note est une donnée, plancher de 11px. */
.tiles.is-list .tile-badge { top: 3px; right: 3px; min-height: 18px; padding: 1px 4px; font-size: calc(11px * var(--vy-k)); }
.tiles.is-list .tile-rank { left: 3px; top: 3px; padding: 0 4px; min-height: 16px; }
.tiles.is-list .tile-year { display: inline; margin-left: 6px; }

.tiles-mini { grid-template-columns: repeat(3, minmax(0, 1fr)); }

/* Une liste filtrée qui ne rend rien le dit sur toute la largeur de la grille. */
.tiles-none { grid-column: 1 / -1; font-size: calc(14px * var(--vy-k)); color: var(--vy-muted); padding: 12px 0; }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 5c · RANGER LA FILE D'ACHAT
   │ Le troisième mode de la liste des souhaits. Des LIGNES, pas des tuiles :
   │ on range une file, on ne la contemple pas. La hauteur d'une ligne est
   │ FIXE, et c'est ce qui rend le calcul du glissé exact (cf. `ORD_H` dans
   │ app.js, et `indexCible` dans file.js) : une ligne traversée décale d'une
   │ hauteur, ni plus ni moins.
   └────────────────────────────────────────────────────────────────────────── */

.ord { display: grid; gap: 8px; }

.ord-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.ord-sum { font: 600 calc(12px * var(--vy-k))/1.2 var(--vy-ui); color: var(--vy-muted); }
.ord-help { font-size: calc(12.5px * var(--vy-k)); line-height: 1.45; color: var(--vy-muted-3); }

.ord-list { list-style: none; margin: 0; padding: 0; }

.ord-row {
  position: relative;
  display: grid;
  grid-template-columns: 42px 20px 40px minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
  height: 64px;
  padding: 0;
  border-bottom: 1px solid var(--vy-line);
  background: var(--vy-bg);
}
/* Les lignes que la ligne glissée traverse s'écartent : elles seules
   s'animent, la glissée suit le doigt sans retard. */
.ord-list.is-dragging .ord-row { transition: transform var(--vy-t-2) ease; }
.ord-list.is-dragging .ord-row.is-drag {
  transition: none;
  z-index: 2;
  border-radius: var(--vy-r-2);
  border-bottom-color: transparent;
  background: var(--vy-card);
  box-shadow: var(--vy-ombre-hero);
}

.ord-grip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 100%;
  min-height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--vy-r-1);
  background: transparent;
  color: var(--vy-muted-2);
  cursor: grab;
  /* Sans cela, le navigateur prend le glissé pour un défilement et la ligne
     ne suit pas le doigt. C'est `pasDeDefilement` (app.js) qui fait alors
     défiler la page près des bords. */
  touch-action: none;
}
.ord-grip:active { cursor: grabbing; }
.ord-grip .ico { width: 18px; height: 18px; }

.ord-n {
  font: 600 calc(13px * var(--vy-k))/1 var(--vy-ui);
  color: var(--vy-muted);
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.ord-row:first-child .ord-n { color: var(--vy-accent-texte); }

.ord-row .tile-cover { width: 40px; border-radius: var(--vy-r-0); }

.ord-main {
  display: grid;
  gap: 1px;
  min-width: 0;
  padding: 0;
  border: 0;
  border-radius: var(--vy-r-0);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ord-artist,
.ord-title,
.ord-money { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ord-artist { font-size: calc(12px * var(--vy-k)); color: var(--vy-muted); }
.ord-title { font-size: calc(14px * var(--vy-k)); font-weight: 600; }
.ord-money { font: 400 calc(12px * var(--vy-k))/1.3 var(--vy-ui); color: var(--vy-muted-3); }
/* Le prix unitaire n'apparaît QUE s'il y a la place : sur un téléphone la
   colonne du milieu fait 146px (mesuré à 390px) et le cumul seul y tient.
   Le point médian est posé par la règle, jamais par le script : un
   séparateur ne doit pas survivre à la disparition de ce qu'il sépare. */
.ord-prix { display: none; }
.ord-prix:not(:empty)::after { content: " · "; }

.ord-tools { display: inline-flex; align-items: center; gap: 2px; }
.ord-move {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--vy-r-1);
  background: transparent;
  color: var(--vy-muted);
}
.ord-move .ico { width: 18px; height: 18px; }
.ord-move:disabled { color: var(--vy-line-strong); }

@media (prefers-reduced-motion: reduce) {
  .ord-list.is-dragging .ord-row { transition: none; }
}

/* Sous 380px, le cumul passe à la trappe avant l'artiste : c'est le nom du
   disque qu'on cherche des yeux en rangeant, le montant se relit dans la
   fiche. Mesuré à 320px : la colonne du milieu tombait à 84px. */
@media (max-width: 379px) {
  .ord-row { grid-template-columns: 38px 18px 36px minmax(0, 1fr) auto; gap: 6px; }
  .ord-money { display: none; }
}


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 5b · LE BAC
   │ La collection en pile de pochettes, en perspective (cf. bac.js) : la
   │ première devant en bas à gauche, les suivantes qui s'enfoncent vers le
   │ coin haut-droit. Tout le PLACEMENT est calculé par le script ; ici ne
   │ vivent que la matière (papier, ombre, voile) et le pied (curseur,
   │ légende, compteur). La scène déborde du gabarit jusqu'aux bords de
   │ l'écran : une pile qui s'arrête à 16px du bord est un cadre, pas un bac.
   └────────────────────────────────────────────────────────────────────────── */

.pile {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 320px;
  margin: 0 -16px;
  overflow: hidden;
}

/* `touch-action: none` : c'est le doigt qui feuillette, pas la page. La
   hauteur du bac est calée par le script pour que la page n'ait de toute
   façon rien à faire défiler. */
.pile-stage {
  position: relative;
  z-index: 0;
  isolation: isolate;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  cursor: grab;
  outline-offset: -3px;
}
.pile-stage.is-drag { cursor: grabbing; }
.pile-stage:focus-visible { outline: 2px solid var(--vy-accent); }

.pile-scene {
  position: absolute;
  inset: 0;
  transform-style: flat;
}

/* Le halo : la lumière de la vitrine. Un disque flou derrière la pochette de
   devant, de sa couleur dominante (posée par le script dans `--vy-aura`), qui
   change de teinte en fondu quand on feuillette ou qu'on survole. C'est une
   lumière, pas une ombre : l'exception assumée du bac à la règle « aucune
   profondeur molle », parce que c'est ici que l'app fait la démonstration. */
.pile-aura {
  --vy-aura: transparent;
  position: absolute;
  left: 0;
  top: 0;
  width: 320px;
  height: 320px;
  border-radius: 50%;
  background: var(--vy-aura);
  filter: blur(64px);
  opacity: .45;
  pointer-events: none;
  /* 0,5 s et non 0,8 : bac.js donne au halo la couleur de la DESTINATION dès
     le lâcher, il a donc tout le vol de la carte pour y arriver. À 0,8 s il
     n'y était que 0,4 s après elle (« la lumière vient après »). */
  transition: background-color .5s var(--vy-ease-out, cubic-bezier(.2, .7, .2, 1));
}
:root[data-theme="dark"] .pile-aura { opacity: .5; }

/* Une carte : la pochette, son ombre portée, et un voile de papier dont
   l'opacité (`--vy-fade`, posée par le script) suit la distance à la carte
   de devant : les pochettes du fond se fondent dans le papier. */
.pile-card {
  --vy-fade: 0;
  position: absolute;
  left: 0;
  top: 0;
  padding: 0;
  border: 0;
  border-radius: 3px;
  overflow: hidden;
  background: var(--vy-paper-2);
  color: inherit;
  font: inherit;
  box-shadow: var(--vy-ombre-bac);
  transform-origin: 50% 50%;
  will-change: transform, opacity;
  backface-visibility: hidden;
  cursor: pointer;
  -webkit-touch-callout: none;
  transition: box-shadow .25s ease;
}
/* Survolée, la carte s'avance et penche vers le pointeur (le script), son
   ombre s'allonge (ici) et un REFLET suit la souris : une lumière posée à
   `--vy-mx`/`--vy-my`, que le script déplace. Ouverte, elle n'est plus un
   bouton à presser. */
.pile-card { --vy-mx: 50%; --vy-my: 50%; }
.pile-card.is-hover { box-shadow: var(--vy-ombre-bac-haut); }
.pile-card.is-open { cursor: default; }
.pile-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  background: radial-gradient(circle at var(--vy-mx) var(--vy-my),
    rgba(255, 255, 255, .34), rgba(255, 255, 255, .06) 38%, transparent 62%);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s ease;
}
.pile-card.is-hover::before { opacity: 1; }

/* Le disque qui sort de la pochette au clic : noir, ses sillons, une
   étiquette de la couleur de la pochette (`--vy-disc-label`, posée par le
   script) et le trou au centre, de la couleur du papier. */
.pile-disc {
  --vy-disc-label: #5b5b58;
  position: absolute;
  left: 0;
  top: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle closest-side, var(--vy-bg) 0 3%, var(--vy-disc-label) 3.2% 33%,
      #0e0e0e 33.3% 34.5%, transparent 34.7%),
    repeating-radial-gradient(circle closest-side, #151515 0 1%, #242423 1% 2%);
  box-shadow: 0 12px 34px -10px rgba(0, 0, 0, .7), 0 0 0 1px rgba(255, 255, 255, .05);
  transform-origin: 50% 50%;
  pointer-events: none;
  will-change: transform;
}
.pile-card .tile-cover {
  position: absolute;
  inset: 0;
  aspect-ratio: auto;
  border-radius: inherit;
  box-shadow: none;
}
/* Le voile n'a PAS de transition : bac.js écrit `--vy-fade` à chaque image,
   déjà lissé par le mouvement de la carte. Une transition de 0,3 s par-dessus
   le faisait courir 0,3 s derrière la carte, qui arrivait voilée au milieu
   puis s'éclaircissait après coup, reflet compris. Seul le survol garde son
   fondu : la carte qu'on touche perd son voile en douceur. */
.pile-card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--vy-bg);
  opacity: var(--vy-fade);
  pointer-events: none;
}
.pile-card.is-hover::after { transition: opacity .25s ease; }

/* Le panneau d'infos (grand écran seulement, cf. section 14) : posé dans le
   coin libre à droite de la scène, sous la diagonale des cartes, au-dessus
   d'elles (la scène isole son empilement, ses cartes ne peuvent pas passer
   par-dessus). Il dit la pochette de devant, ou celle que la souris survole. */
.pile-info {
  position: absolute;
  right: 3%;
  bottom: 12%;
  z-index: 5;
  display: grid;
  gap: 14px;
  width: min(32%, 380px);
}
.pile-info-body { display: grid; gap: 8px; }
.pile-info-sur {
  font: 500 calc(12px * var(--vy-k))/1.4 var(--vy-ui);
  color: var(--vy-muted-3);
}
.pile-info-artist {
  font-family: var(--vy-titre);
  font-size: clamp(24px, 2.6vw, 36px);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.05;
}
.pile-info-title { font-size: calc(17px * var(--vy-k)); line-height: 1.3; color: var(--vy-ink-2); }
.pile-info-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.pile-info-chips:empty { display: none; }
.pile-info-line { display: flex; align-items: center; gap: 10px; font-size: calc(13.5px * var(--vy-k)); color: var(--vy-muted); }
.pile-info-stars { display: inline-flex; gap: 2px; color: var(--vy-ink); }
.pile-info-stars:empty { display: none; }
.pile-info-stars .ico { width: 15px; height: 15px; }
.pile-info-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; margin-top: 6px; }
.pile-info .btn { justify-self: start; }
.pile-card:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 3px; }

.pile-none {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 24px;
  text-align: center;
  font-size: calc(14px * var(--vy-k));
  color: var(--vy-muted);
}

/* Le pied : le curseur (un trait, comme le repère d'un bac), la légende de
   la pochette de devant, le compteur. */
.pile-foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas: "range range" "caption count";
  align-items: end;
  gap: 2px 14px;
  padding: 2px 16px 6px;
  border-top: 1px solid var(--vy-line);
}

.pile-range {
  grid-area: range;
  width: 100%;
  height: 28px;
  margin: 0;
  padding: 0;
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  cursor: pointer;
}
.pile-range:disabled { opacity: .4; cursor: default; }
.pile-range::-webkit-slider-runnable-track { height: 2px; border-radius: 1px; background: var(--vy-line-strong); }
.pile-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  margin-top: -6px;
  border: 2px solid var(--vy-bg);
  border-radius: 50%;
  background: var(--vy-ink);
  box-shadow: 0 0 0 1px var(--vy-line-strong);
}
.pile-range::-moz-range-track { height: 2px; border-radius: 1px; background: var(--vy-line-strong); }
.pile-range::-moz-range-thumb {
  width: 10px;
  height: 10px;
  border: 2px solid var(--vy-bg);
  border-radius: 50%;
  background: var(--vy-ink);
  box-shadow: 0 0 0 1px var(--vy-line-strong);
}
.pile-range:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 2px; }

.pile-caption { grid-area: caption; display: grid; gap: 1px; min-width: 0; }
.pile-artist {
  font-family: var(--vy-titre);
  font-size: calc(20px * var(--vy-k));
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.15;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pile-title {
  font-size: calc(14.5px * var(--vy-k));
  line-height: 1.3;
  color: var(--vy-ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pile-meta, .pile-count {
  font: 500 calc(12px * var(--vy-k))/1.4 var(--vy-ui);
  color: var(--vy-muted-3);
  white-space: nowrap;
}
.pile-count { grid-area: count; margin: 0; }

/* ┌──────────────────────────────────────────────────────────────────────────
   │ 5b-bis · LE COVER FLOW ET LE PLEIN ÉCRAN (octobre 2026)
   │ La collection se feuillette comme sur l'iPod : la pochette au milieu, de
   │ face, les autres de part et d'autre tournées vers elle (cf. bac.js,
   │ disposition « flow »). La vitrine de l'accueil garde la pile en
   │ diagonale : ces règles ne visent que `.pile-flow`.
   │
   │ Le plein écran (cf. plein.js) : le bac couvre l'app, sur un fond sombre
   │ quel que soit le thème (une tablette allumée la nuit dans un salon ne
   │ doit pas éclairer la pièce), ses commandes s'effacent au calme.
   └────────────────────────────────────────────────────────────────────────── */

/* Le reflet sur le sol, comme sur l'iPod : une COPIE de la pochette, posée
   sous la carte, retournée et fondue. Elle vit DANS la carte (bac.js,
   `fabriquerReflet`) : même couche, même transform, même peinture, et la
   carte ne se montre qu'une fois sa pochette décodée, reflet compris. Elle
   tourne donc avec elle, comme avant, mais elle ne peut plus arriver
   après.

   Le `-webkit-box-reflect` d'avant dépendait du moteur : une couche
   « réplique » et son masque, créés et peints à part de la carte. Le reflet
   d'une carte neuve arrivait après elle, « quelques micro secondes » plus
   tard (et un reflet en retard se voit tout de suite), chaque carte coûtait
   trois couches au lieu d'une, et Firefox n'en avait pas du tout. Le dégradé
   reproduit l'ancien : 22 % d'opacité au contact, plus rien au tiers de la
   hauteur de la pochette, avec une traîne plus douce.
   La carte n'écrête plus (`overflow: visible`) : sa pochette (`.tile-cover`)
   s'écrête elle-même, et le reflet sort sous la carte. */
.pile-flow .pile-card {
  overflow: visible;
  border-radius: 2px;
}
.pile-reflet {
  position: absolute;
  left: 0;
  top: calc(100% + 2px);
  width: 100%;
  height: 34%;
  overflow: hidden;
  pointer-events: none;
  /* Une carte lointaine fond son reflet avec elle (le voile de profondeur). */
  opacity: calc(1 - var(--vy-fade, 0) * 1.8);
  -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, .22), rgba(0, 0, 0, .1) 40%, rgba(0, 0, 0, .025) 75%, rgba(0, 0, 0, 0));
  mask-image: linear-gradient(to bottom, rgba(0, 0, 0, .22), rgba(0, 0, 0, .1) 40%, rgba(0, 0, 0, .025) 75%, rgba(0, 0, 0, 0));
}
.pile-reflet[hidden] { display: none; }
.pile-reflet-face {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  aspect-ratio: 1;
  transform: scaleY(-1);
  background: var(--vy-paper-2);
}
.pile-reflet-face img,
.pile-reflet-face .ph {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* La pochette ouverte (le disque en sort) n'a plus de reflet : il s'efface
   avec le soulèvement, et revient d'un coup quand elle est rangée. */
.pile-flow .pile-card.is-open > .pile-reflet { opacity: 0; transition: opacity .18s ease; }

/* Le pied : la légende au milieu, sous la pochette ; le curseur et le
   compteur dessous. Plus de filet : la scène et le pied sont un seul objet. */
.pile-flow .pile-foot {
  grid-template-areas: "caption caption" "range count";
  align-items: center;
  gap: 0 14px;
  padding-top: 0;
  border-top: 0;
}
.pile-flow .pile-caption { justify-items: center; text-align: center; padding-bottom: 2px; }
.pile-flow .pile-artist,
.pile-flow .pile-title { max-width: 100%; }

/* Le panneau d'infos (grand écran) : sous la pochette du milieu, centré,
   dans la place que la géométrie lui garde. bac.js pose `--vy-flow-info`
   (le haut du panneau, juste sous la partie vive du reflet) et centre le
   bloc « pochette, reflet, panneau » dans la scène : le panneau ne peut plus
   recouvrir la pochette, quelle que soit la hauteur de l'écran. Compact :
   la note, les écoutes et le bouton partagent une ligne. Pas de fond : il
   se pose sur la traîne du reflet, déjà presque effacée. */
.pile-flow .pile-info {
  top: var(--vy-flow-info, auto);
  left: 50%;
  right: auto;
  bottom: auto;
  width: min(680px, 80%);
  gap: 0;
  justify-items: center;
  text-align: center;
  transform: translateX(-50%);
}
.pile-flow .pile-info-body { gap: 3px; justify-items: center; justify-self: stretch; }
.pile-flow .pile-info-artist { font-size: clamp(22px, 2.1vw, 30px); }
.pile-flow .pile-info-title { font-size: calc(15px * var(--vy-k)); }
.pile-flow .pile-info-chips { margin-top: 6px; }
.pile-flow .pile-info-chips,
.pile-flow .pile-info-meta,
.pile-flow .pile-info-line { justify-content: center; }

/* Le bouton du plein écran : en haut à droite de la scène, comme sur un
   lecteur vidéo. Discret, il ne cache aucune pochette. */
.pile-plein-btn {
  position: absolute;
  top: 10px;
  right: 12px;
  z-index: 6;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 44px;
  min-height: 44px;
  padding: 0 12px;
  justify-content: center;
  border: 1px solid var(--vy-line);
  border-radius: 999px;
  background: var(--vy-card);
  color: var(--vy-ink-2);
  font: 500 calc(13px * var(--vy-k))/1 var(--vy-ui);
  cursor: pointer;
}
.pile-plein-btn .ico { width: 18px; height: 18px; }
.pile-plein-btn:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 2px; }
/* Au téléphone, l'icône suffit : le libellé reste pour le lecteur d'écran. */
@media (max-width: 699px) {
  .pile-plein-btn { padding: 0; width: 44px; }
  .pile-plein-btn .pile-plein-lib { display: none; }
}

/* ── Le plein écran ───────────────────────────────────────────────────────
   Les jetons de couleur sont redéfinis SUR le bac : cartes, voile, légende
   et commandes prennent le sombre sans rien savoir du mode. */
.pile.is-plein {
  --vy-bg: #0b0b0b;
  --vy-ink: #f3f1ec;
  --vy-ink-2: #d9d5cd;
  --vy-muted: #a9a49b;
  --vy-muted-3: #a9a49b;
  --vy-line: rgba(255, 255, 255, .14);
  --vy-line-strong: rgba(255, 255, 255, .3);
  --vy-paper-2: #1b1b1a;
  --vy-ombre-bac: 0 34px 70px -26px rgba(0, 0, 0, .95), 0 0 0 1px rgba(255, 255, 255, .07);
  --vy-ombre-bac-haut: 0 48px 90px -26px rgba(0, 0, 0, .95), 0 0 0 1px rgba(255, 255, 255, .1);
  position: fixed;
  inset: 0;
  z-index: 45;
  margin: 0;
  min-height: 0;
  background: var(--vy-bg);
  color: var(--vy-ink);
  color-scheme: dark;
  padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
  /* Rien ne défile ni ne zoome ici : un double appui est un geste de l'app
     (il montre les commandes), pas celui du navigateur (il zoomerait). Et
     un doigt qui reste posé ne sélectionne rien et n'ouvre aucun menu. */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
/* La dérive anti-marquage (plein.js) : quelques pixels, toutes les deux
   minutes, en douceur. La scène et la légende dérivent ensemble ; le pied
   (curseur, compteur) s'efface au calme et n'a pas à dériver. */
.pile.is-plein .pile-stage,
.pile.is-plein .pile-foot .pile-caption {
  transform: translate(var(--vy-derive-x, 0px), var(--vy-derive-y, 0px));
  transition: transform 4s ease;
}
.pile.is-plein .pile-aura { opacity: .6; filter: blur(90px); }
.pile.is-plein .pile-info { display: none; }
/* Le pied ne tient plus de place : la scène a tout l'écran utile et les
   pochettes en profitent (sur un iPhone en paysage dans Safari, 852 × 333,
   la pochette tombait à 110 px parce que la géométrie gardait la place d'un
   pied invisible au calme). Le curseur et le compteur se posent par-dessus,
   et ne sont là qu'après un double appui. Le pied n'est pas positionné : la
   légende, absolue, garde le bac pour repère. */
.pile.is-plein .pile-foot {
  display: block;
  height: 0;
  padding: 0;
  border: 0;
  overflow: visible;
}
.pile.is-plein .pile-range {
  position: absolute;
  z-index: 6;
  left: calc(env(safe-area-inset-left) + 16px);
  right: calc(env(safe-area-inset-right) + 96px);
  bottom: calc(env(safe-area-inset-bottom) + 4px);
  width: auto;
}
.pile.is-plein .pile-count {
  position: absolute;
  z-index: 6;
  right: calc(env(safe-area-inset-right) + 16px);
  bottom: calc(env(safe-area-inset-bottom) + 8px);
}
.pile.is-plein .pile-artist { font-size: clamp(22px, 3.4vmin, 40px); }
.pile.is-plein .pile-title { font-size: clamp(15px, 2.2vmin, 24px); }
/* La légende peut se poser sur la queue du reflet : une ombre diffuse garde
   le texte lisible sur une pochette claire. */
.pile.is-plein .pile-artist,
.pile.is-plein .pile-title,
.pile.is-plein .pile-meta { text-shadow: 0 1px 10px rgba(0, 0, 0, .7); }
/* La légende, juste sous le reflet de la pochette du milieu (bac.js pose
   `--vy-flow-legende`). Sur grand écran, elle est d'habitude cachée au
   profit du panneau : en plein écran, c'est elle qui parle (le second
   sélecteur bat la règle qui la cache, quel que soit l'ordre). */
.pile.is-plein .pile-foot .pile-caption,
.pile.is-plein .pile-info ~ .pile-foot .pile-caption {
  position: absolute;
  top: var(--vy-flow-legende, auto);
  bottom: auto;
  left: calc(env(safe-area-inset-left) + 24px);
  right: calc(env(safe-area-inset-right) + 24px);
  width: auto;
  height: auto;
  overflow: visible;
  clip: auto;
  white-space: normal;
}
.pile.is-plein .pile-plein-btn { display: none; }

/* La barre des commandes, en haut à droite. */
.pile-plein-bar {
  position: absolute;
  top: calc(env(safe-area-inset-top) + 12px);
  right: calc(env(safe-area-inset-right) + 12px);
  left: calc(env(safe-area-inset-left) + 12px);
  z-index: 6;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
  transition: opacity .5s ease;
}
.pile-plein-bar[hidden] { display: none; }
.pile-plein-ctl {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  min-width: 44px;
  padding: 0 14px;
  justify-content: center;
  border: 1px solid var(--vy-line);
  border-radius: 999px;
  background: rgba(20, 20, 20, .72);
  color: var(--vy-ink);
  font: 500 calc(13.5px * var(--vy-k))/1 var(--vy-ui);
  cursor: pointer;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}
.pile-plein-ctl .ico { width: 18px; height: 18px; flex: none; }
.pile-plein-ctl[aria-pressed="true"] { border-color: rgba(255, 255, 255, .5); background: rgba(255, 255, 255, .16); }
.pile-plein-ctl:focus-visible,
.pile-plein-sel:focus-within { outline: 2px solid var(--vy-accent); outline-offset: 2px; }
.pile-plein-sel select {
  min-height: 42px;
  padding: 0 4px;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  font: 500 16px/1 var(--vy-ui);
  cursor: pointer;
}
.pile-plein-sel select:focus { outline: none; }
.pile-plein-sel select option { color: #111; }
.pile-plein-sel select:disabled { opacity: .5; cursor: default; }
@media (max-width: 699px) {
  .pile-plein-ctl:not(.pile-plein-sel) { padding: 0; width: 44px; }
  .pile-plein-ctl .pile-plein-lib { display: none; }
}
/* Le calme : sans geste depuis quelques secondes, les commandes, le curseur
   et le compteur s'effacent ; la pochette et sa légende restent. */
.pile.is-plein.is-calme .pile-plein-bar,
.pile.is-plein.is-calme .pile-range,
.pile.is-plein.is-calme .pile-count {
  opacity: 0;
  pointer-events: none;
}
.pile.is-plein .pile-range,
.pile.is-plein .pile-count { transition: opacity .5s ease; }
.pile.is-plein.is-calme,
.pile.is-plein.is-calme .pile-stage { cursor: none; }

/* Le reste de l'app ne défile pas sous le bac, et la pastille « Admin » du
   portfolio ne se pose pas sur une tablette allumée jour et nuit. */
body.vy-plein { overflow: hidden; }
body.vy-plein #mn-admin-pill { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  .pile.is-plein .pile-stage,
  .pile.is-plein .pile-foot .pile-caption,
  .pile-plein-bar { transition: none; }
}
:root[data-mouvement="reduit"] .pile.is-plein .pile-stage,
:root[data-mouvement="reduit"] .pile.is-plein .pile-foot .pile-caption { transition: none; }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 6 · POUR VOUS
   │ Le hub (raccourcis en cartes, profil et « Explorer » en puces cliquables),
   │ puis des rails horizontaux de cartes, avec accroche au défilement.
   └────────────────────────────────────────────────────────────────────────── */

/* Les raccourcis : un rail de cartes au téléphone (même géométrie que les
   rails de suggestions, à ras des bords), une grille qui s'enroule sur
   ordinateur. La première carte est l'action principale, en aplat d'encre. */
.hub-rail {
  display: flex;
  gap: 8px;
  margin: 0 -16px;
  padding: 2px 16px 8px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-left: 16px;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.hub-rail::-webkit-scrollbar { display: none; }

.hub-card {
  flex: 0 0 auto;
  scroll-snap-align: start;
  display: grid;
  gap: 3px;
  align-content: start;
  width: 148px;
  min-height: 96px;
  padding: 14px 14px 12px;
  border: 0;
  border-radius: var(--vy-r-3);
  background: var(--vy-groupe);
  color: var(--vy-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color var(--vy-t) ease, background var(--vy-t) ease;
}
.hub-card .ico { width: 20px; height: 20px; margin-bottom: 4px; color: var(--vy-muted); }
.hub-card-lab { font: 600 calc(14px * var(--vy-k))/1.25 var(--vy-ui); overflow-wrap: anywhere; }
.hub-card-sub {
  font: 500 calc(12.5px * var(--vy-k))/1.3 var(--vy-ui);
  color: var(--vy-muted-3);
}
.hub-card-main { background: var(--vy-ink); color: var(--vy-bg); }
.hub-card-main .ico, .hub-card-main .hub-card-sub { color: var(--vy-bg); }

/* Les puces du profil et d'« Explorer » sont des boutons : le filet léger
   des puces d'information, la hauteur tactile des boutons. */
.reco-profile button.chip, .reco-explore button.chip { color: var(--vy-ink-2); }

.reco-section { display: grid; gap: 6px; margin-bottom: 10px; }
.reco-title { font-size: calc(18px * var(--vy-k)); letter-spacing: -0.02em; }
.reco-sub { font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); }
/* Le titre d'un rail et son lien « voir » sur la même ligne : le lien garde
   sa cible tactile sans faire grandir la ligne. */
.reco-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-width: 0; }
.reco-head .reco-title { min-width: 0; }
.reco-more {
  display: inline-flex;
  align-items: center;
  flex: none;
  min-height: 44px;
  font-size: calc(13px * var(--vy-k));
  white-space: nowrap;
}

.rail {
  display: flex;
  gap: 12px;
  margin: 0 -16px;
  padding: 6px 16px 10px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: 16px;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.rail::-webkit-scrollbar { display: none; }

.reco-card {
  flex: 0 0 156px;
  scroll-snap-align: start;
  display: grid;
  gap: 5px;
  align-content: start;
  min-width: 0;
}
.reco-card .tile { content-visibility: visible; contain-intrinsic-size: none; }
.reco-card .tile-cover { width: 156px; }
/* Deux lignes réservées à la raison, qu'elle en prenne une ou deux : sinon
   la rangée d'actions monte et descend d'une carte à l'autre du même rail
   (mesuré : 32px d'écart entre voisines). */
.reco-reason {
  min-height: calc(2 * 1.35 * 12px);
  font-size: calc(12px * var(--vy-k));
  line-height: 1.35;
  color: var(--vy-muted);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.reco-acts { display: flex; gap: 4px; margin-top: 2px; }
.reco-act {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  flex: 1 1 auto;
  min-height: 44px;
  padding: 4px 6px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-1);
  background: var(--vy-fill);
  color: var(--vy-ink);
  font: 500 calc(12px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
.reco-act .ico { width: 16px; height: 16px; }
.reco-act-ico { flex: 0 0 44px; padding: 0; }
/* Trois boutons, pas cinq : le souhait prend toute la largeur de la carte
   (156 px, il en avait 60 coincé entre deux icônes), les deux avis se
   partagent la rangée du dessous. */
.reco-acts-fb .reco-act-fb { flex: 1 1 0; }

.reco-hidden { display: grid; gap: 8px; padding-top: 6px; }
.reco-hidden-list { display: grid; }
.reco-hidden-list > li {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 4px 0;
  border-bottom: 1px solid var(--vy-line);
  font-size: calc(14px * var(--vy-k));
}
.reco-hidden-list > li > span { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* « Vos avis » (octobre 2026) : le même panneau que les masquées, avec une
   phrase par écart. Elle peut faire deux lignes (« Country : écarté après
   2 « pas mon genre » »), elle ne se coupe donc pas en ellipse. */
.reco-avis-corps { display: grid; gap: 8px; }
.reco-avis-corps[hidden] { display: none; }
.reco-avis .reco-hidden-list > li > span:first-child { white-space: normal; }
.reco-avis .reco-hidden-list > li > .reco-avis-note { flex: 0 1 auto; white-space: normal; font-size: calc(12px * var(--vy-k)); color: var(--vy-muted); text-align: right; }
.reco-avis-aimes { font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); }
.reco-avis-aimes[hidden] { display: none; }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 7 · PLUS
   └────────────────────────────────────────────────────────────────────────── */

.more-who { font-size: calc(14px * var(--vy-k)); color: var(--vy-muted); margin-top: -6px; }

/* La liste groupée d'iOS (octobre 2026) : une carte aux grands rayons, des
   lignes de 52 px, et un filet entre deux lignes qui commence au TEXTE, pas
   au bord (l'icône dépasse à gauche, comme dans Réglages). */
.more-list {
  display: grid;
  overflow: hidden;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
}
.more-list > li { position: relative; }
.more-list > li + li::before {
  content: "";
  position: absolute;
  top: 0;
  left: 52px;
  right: 0;
  border-top: 1px solid var(--vy-line);
  pointer-events: none;
}

.more-btn {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  min-height: 52px;
  padding: 10px 14px 10px 16px;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  font: 500 calc(15px * var(--vy-k))/1.3 var(--vy-ui);
  text-align: left;
  cursor: pointer;
}
.more-btn > span { flex: 1 1 auto; min-width: 0; }
.more-btn > .ico:first-child { color: var(--vy-muted); }
.more-chev { width: 16px; height: 16px; color: var(--vy-muted-2); }
.more-count {
  min-width: 24px;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--vy-ink);
  color: var(--vy-bg);
  font: 600 calc(12px * var(--vy-k))/1.4 var(--vy-ui);
  text-align: center;
}
/* « Se déconnecter » : en rouge, comme la sortie d'un compte dans Réglages. */
.more-btn-out { color: var(--vy-rouge-texte); }

.more-foot { font-size: calc(13px * var(--vy-k)); color: var(--vy-muted-3); text-align: center; padding-top: 10px; }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 8 · PANNEAUX DE « PLUS » (plein écran sur téléphone)
   └────────────────────────────────────────────────────────────────────────── */

.panel,
.sheet {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  background: var(--vy-bg);
}
.sheet { z-index: 60; }

.panel-head,
.sheet-head {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: none;
  min-height: var(--vy-top-h);
  padding: 6px 10px;
  padding-top: max(6px, env(safe-area-inset-top));
  border-bottom: 1px solid var(--vy-line);
  background: var(--vy-bg);
}
.panel-head h2,
.sheet-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: calc(18px * var(--vy-k));
  letter-spacing: -0.02em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sheet-head-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: calc(14px * var(--vy-k));
  font-weight: 600;
  color: var(--vy-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Colonne flexible et non grille, et chaque enfant en `flex: none` : le corps
   a une hauteur DÉFINIE (il prend le reste de la feuille et défile), donc une
   grille y sizerait ses pistes `auto` sur la contribution minimale des
   enfants, et celle d'un enfant qui défile (une rangée de pastilles en
   `overflow-x: auto`) est NULLE. Mesuré : la rangée des genres de la fiche
   tombait à 2px de haut, ses pastilles coupées. */
.panel-body,
.sheet-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 16px 16px calc(28px + env(safe-area-inset-bottom));
}
.panel-body > *,
.sheet-body > * { flex: none; min-width: 0; }
.sheet-body { gap: 14px; }

/* Un pied collé : le bouton principal d'une feuille courte (« Voir les
   résultats ») était sous la ligne de flottaison à 320×568, mesuré. */
.sheet-foot {
  flex: none;
  display: flex;
  gap: 10px;
  padding: 10px 16px;
  padding-bottom: calc(10px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--vy-line);
  background: var(--vy-bg);
}
.sheet-foot .btn { flex: 1 1 auto; min-width: 0; }

.sheet-form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

.account-name {
  font-family: var(--vy-titre);
  font-size: 22px;
  font-weight: 600;
  letter-spacing: -0.02em;
}

/* ── Statistiques ─────────────────────────────────────────────────────────── */
.stat-tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.stat-tile {
  display: grid;
  gap: 2px;
  padding: 12px 14px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
}
.stat-val { font-size: 24px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.1; }
.stat-lab { font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }

.bars { display: grid; gap: 10px; }
.bar {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 4px 10px;
  font-size: calc(13.5px * var(--vy-k));
}
.bar-lab { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-n { color: var(--vy-muted-3); font-size: calc(12px * var(--vy-k)); }
.bar-track {
  grid-column: 1 / -1;
  height: 6px;
  border-radius: 3px;
  background: var(--vy-paper-2);
  overflow: hidden;
}
.bar-fill { height: 100%; border-radius: 3px; background: var(--vy-ink); }

/* Douze mois côte à côte : des colonnes, le mois en mono dessous. */
.cols {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 4px;
  align-items: end;
  height: 96px;
  padding-top: 6px;
}
.col {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 4px;
  height: 100%;
  min-width: 0;
}
.col-track { display: flex; align-items: end; min-height: 0; }
.col-fill { width: 100%; min-height: 2px; border-radius: 2px 2px 0 0; background: var(--vy-ink); }
.col-lab { font: 500 calc(10.5px * var(--vy-k))/1 var(--vy-ui); text-align: center; color: var(--vy-muted-3); letter-spacing: 0; }
.col-n { font: 500 calc(11px * var(--vy-k))/1 var(--vy-ui); text-align: center; color: var(--vy-muted-3); }

.rank-list { display: grid; }
.rank-list > li {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  min-height: 40px;
  padding: 4px 0;
  border-bottom: 1px solid var(--vy-line);
  font-size: calc(14.5px * var(--vy-k));
}
/* Le message d'une liste vide n'est pas une ligne de classement : rendu en
   grille, il tombait un mot par ligne (mesuré : cinq lignes). */
.rank-list > li.empty-line { display: block; border-bottom: 0; }
.rank-n { font-family: var(--vy-ui); font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }
.rank-c { font-family: var(--vy-ui); font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }

/* ── Partage, amis, comptes : des lignes, pas des cartes ─────────────────── */
.share-list,
.user-list,
.owner-list { display: grid; }

.share-list > li,
.user-list > li {
  display: grid;
  gap: 8px;
  padding: 12px 0;
  border-bottom: 1px solid var(--vy-line);
}
.share-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.share-lab { font-weight: 600; }
.share-scope { font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); }
.share-views { font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); margin-left: auto; }
.share-url {
  font: calc(12.5px * var(--vy-k))/1.3 var(--vy-ui);
  color: var(--vy-muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.share-acts { display: flex; flex-wrap: wrap; gap: 6px; }
.share-acts .btn { flex: 1 1 auto; }

.user-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.user-name { font-weight: 600; }
.user-meta { font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); }

.owner-list > li { border-bottom: 1px solid var(--vy-line); }
.owner-btn {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 56px;
  padding: 8px 4px;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  font: 500 calc(15px * var(--vy-k))/1.3 var(--vy-ui);
  text-align: left;
  cursor: pointer;
}
.owner-btn > span { flex: 1 1 auto; min-width: 0; }
.owner-btn small { display: block; font-size: calc(13px * var(--vy-k)); font-weight: 400; color: var(--vy-muted); }
.owner-btn > .ico:first-child { color: var(--vy-muted); }

/* La zone d'import Discogs : un textarea, un bouton. */
.import-zone textarea { font-family: var(--vy-mono); font-size: calc(13px * var(--vy-k)); }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 9 · FEUILLES
   └────────────────────────────────────────────────────────────────────────── */

/* ── La fiche d'un album ──────────────────────────────────────────────────── */
.album-hero {
  display: grid;
  justify-items: center;
  gap: 12px;
  padding-top: 4px;
}

.album-cover-btn {
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: var(--vy-r-0);
  cursor: zoom-in;
}
.album-cover-btn:disabled { cursor: default; }
.album-cover-btn:focus-visible { outline-offset: 4px; }

.album-cover {
  width: min(66vw, 300px);
  aspect-ratio: 1;
  border-radius: var(--vy-r-0);
  overflow: hidden;
  background: var(--vy-paper-2);
  box-shadow: var(--vy-ombre-hero);
}
.album-cover img { width: 100%; height: 100%; object-fit: cover; }

.album-photo-wrap { position: relative; }

/* Sans pochette, le bouton photo est l'action PRINCIPALE de la fiche. */
.album-photo-wrap.is-main .btn { background: var(--vy-btn-bg); border-color: var(--vy-btn-bg); color: var(--vy-btn-fg); box-shadow: var(--vy-btn-ombre); }

.menu {
  position: absolute;
  top: calc(100% + 6px);
  left: 50%;
  z-index: 5;
  transform: translateX(-50%);
  display: grid;
  min-width: 260px;
  padding: 6px;
  border: 1px solid var(--vy-line-strong);
  border-radius: var(--vy-r-2);
  background: var(--vy-card);
  box-shadow: var(--vy-ombre-pochette);
}
.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  padding: 8px 12px;
  border: 0;
  border-radius: var(--vy-r-1);
  background: transparent;
  color: var(--vy-ink);
  font: 500 calc(14.5px * var(--vy-k))/1.3 var(--vy-ui);
  text-align: left;
  cursor: pointer;
}
.menu-item .ico { width: 18px; height: 18px; color: var(--vy-muted); }
.menu-item-danger { color: var(--vy-rouge-texte); }
.menu-item-danger .ico { color: inherit; }

.album-artist { font-size: calc(15px * var(--vy-k)); color: var(--vy-muted); margin-top: 6px; }
.album-title { font-size: 24px; letter-spacing: -0.025em; overflow-wrap: anywhere; }
.album-meta { font-family: var(--vy-ui); font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }
.album-notes { white-space: pre-wrap; font-size: calc(14.5px * var(--vy-k)); line-height: 1.55; overflow-wrap: anywhere; }

/* Le badge de l'essentiel dans la fiche : un bouton-badge à l'encre. */
.ess-badge-line { display: flex; }

/* La place dans la file d'achat, sur la fiche d'un souhait : une phrase et
   un geste. Sur téléphone les deux s'empilent, le bouton pleine largeur
   (une cible de 44px de haut, jamais un lien au fil du texte). */
.queue-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  flex-wrap: wrap;
  padding: 10px 12px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
}
.queue-text { font-size: calc(13px * var(--vy-k)); line-height: 1.4; color: var(--vy-ink-2); min-width: 0; }
@media (max-width: 419px) {
  .queue-line { align-items: stretch; }
  .queue-line .btn { width: 100%; justify-content: center; }
}

/* Les écoutes : le compte, le bouton, puis l'historique (dix dernières). */
.plays {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px 10px;
  align-items: center;
  padding: 10px 12px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
}
.plays-text { font-size: calc(13.5px * var(--vy-k)); color: var(--vy-muted); min-width: 0; }
.plays .btn { flex: none; }
.plays-list { grid-column: 1 / -1; display: grid; border-top: 1px solid var(--vy-line); }
.plays-list > li {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 4px 0;
  border-bottom: 1px solid var(--vy-line);
  font-size: calc(13.5px * var(--vy-k));
}
.plays-list > li:last-child { border-bottom: 0; }
.play-when { flex: none; font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }
.play-note { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--vy-ink-2); }
.plays-undo { grid-column: 1 / -1; justify-self: start; }

.tracks { display: grid; }
.tracks > li {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) auto;
  gap: 8px;
  align-items: baseline;
  padding: 7px 0;
  border-bottom: 1px solid var(--vy-line);
  font-size: calc(14.5px * var(--vy-k));
}
.track-n { font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }
.track-title { min-width: 0; overflow-wrap: anywhere; }
.track-dur { font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }

.album-actions {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  padding-top: 6px;
}

/* La pochette en grand : un voile, l'image, un tap referme. */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  place-items: center;
  padding: 16px;
  background: rgba(10, 10, 10, .94);
  cursor: zoom-out;
}
.viewer img {
  max-width: min(100%, 92vh);
  max-height: 92dvh;
  width: auto;
  height: auto;
  border-radius: var(--vy-r-0);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, .12);
}

/* ── Où l'acheter / Cote et vente ─────────────────────────────────────────
   La cote Discogs sur une ligne avec son bouton discret, puis les liens
   marchands en puces : des liens SORTANTS, donc l'encre et le filet des
   puces de la maison, pas la couleur des boutons de l'app ; 44px de cible
   (règle mobile), l'icône devant le nom, la note en retrait. Le même style
   sert « Chercher ailleurs » sous les résultats. */
.album-ext { display: flex; flex-wrap: wrap; gap: 8px; }
.market { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin-bottom: 10px; }
.market-text { min-width: 0; font-size: calc(14px * var(--vy-k)); color: var(--vy-ink-2); }
.buy-links { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }
.buy-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 44px;
  padding: 8px 14px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--vy-fill);
  color: var(--vy-ink);
  font: 500 calc(14px * var(--vy-k))/1.2 var(--vy-ui);
  text-decoration: none;
}
.buy-link .ico { width: 16px; height: 16px; color: var(--vy-muted); }
/* Sur un remplissage, l'encre discrète est `--vy-muted` : `--vy-muted-3` y
   tombait à 4,4:1 (le gris translucide l'assombrit). Même règle plus bas. */
.buy-link small { font-size: calc(11.5px * var(--vy-k)); color: var(--vy-muted); }

/* ── Ajouter : la recherche et ses résultats ──────────────────────────────── */
.results { display: grid; }
/* La ligne de résultat porte DEUX gestes : ouvrir la fiche (la ligne entière)
   et l'ajout rapide (le bouton du bout). Un bouton ne peut pas en contenir un
   autre (le HTML l'interdit, et le navigateur défait l'imbrication) : la ligne
   est donc une rangée qui contient les deux, et c'est elle qui porte le filet.
   `align-items: center` sur la rangée, pas `stretch` : le bouton d'ajout garde
   sa hauteur de cible sans s'étirer sur trois lignes de texte. */
/* Audit fonctionnel (septembre 2026) : l'ARTISTE d'un résultat se touche
   (ma liste sur lui, ou sa discographie en ligne). Un bouton ne pouvant pas
   en contenir un autre, la rangée porte QUATRE boutons dans une seule
   grille : la pochette et le titre (`.result`) ouvrent la fiche préremplie,
   l'artiste filtre, le bout ajoute. Une seule `.result` par rangée, toujours
   (les tests la comptent). */
.result-row {
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr) auto auto;
  grid-template-areas: "cover artist listen add" "cover main listen add";
  gap: 2px 12px;
  align-items: center;
  min-height: 80px;
  padding: 8px 0;
  border-bottom: 1px solid var(--vy-line);
}
.result-cover {
  grid-area: cover;
  align-self: center;
  width: 64px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}
.result-row > .result-artist { grid-area: artist; align-self: end; }
.result {
  grid-area: main;
  display: grid;
  gap: 3px;
  align-self: start;
  width: 100%;
  min-width: 0;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* L'ajout rapide : 44 px de cible, l'icône seule (le libellé accessible vit
   dans `aria-label`, le titre le donne au survol). Le mot « Ajouter » à côté
   de trois lignes de texte volait la largeur du titre sur un téléphone.
   « Écouter », à côté, a la même forme (`.result-act`) mais PAS la classe
   de l'ajout : avec elle, tout ce qui visait « le bouton d'ajout de la
   ligne » attrapait d'abord l'écoute. */
.result-act,
.result-add {
  grid-area: add;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex: none;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--vy-fill);
  color: var(--vy-ink);
  cursor: pointer;
}
.result-add:disabled { opacity: .45; cursor: default; }
.result-listen { grid-area: listen; color: var(--vy-accent-texte); text-decoration: none; }
.result-act .ico,
.result-add .ico { width: 20px; height: 20px; }
.result-cover .tile-cover { width: 64px; }
.result-artist { font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.result-title { font-size: calc(15.5px * var(--vy-k)); font-weight: 600; line-height: 1.25; overflow-wrap: anywhere; }
.result-hint { font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }
.result-src { display: inline; font-family: var(--vy-ui); color: var(--vy-muted-3); }

.barcode-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: end; }

/* ── Chercher ailleurs ─────────────────────────────────────────────────────
   Sous les résultats, une fois qu'une recherche par le nom a été faite : les
   catalogues que l'app n'interroge pas, la requête déjà posée dans le lien
   (cf. liens.js). Masqué tant qu'il n'y a rien à chercher. */
.elsewhere { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--vy-line); }
.elsewhere-title { margin-bottom: 8px; font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); }

/* ── Ajouter par photo ────────────────────────────────────────────────────
   Les deux boutons d'entrée sont en TÊTE de la feuille d'ajout : le disque
   est dans la main, la recherche par le nom vient après. Ils partagent la
   largeur pour que la cible reste large au pouce. */
.add-photo { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.add-photo-btn { justify-content: center; min-height: 46px; cursor: pointer; }
.add-photo-help { margin: 6px 2px 0; }

/* Le séparateur « ou » : un filet coupé par un mot, pour dire que les deux
   chemins mènent au même endroit sans hiérarchiser l'un au-dessus de
   l'autre. */
.add-sep {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 16px 0 2px;
  font-size: calc(12.5px * var(--vy-k));
  color: var(--vy-muted-3);
}
.add-sep::before,
.add-sep::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--vy-line);
}

/* ── Ce que la photo a donné ───────────────────────────────────────────── */
.photo-state {
  margin: 12px 0 4px;
  font-size: calc(13.5px * var(--vy-k));
  color: var(--vy-muted);
  line-height: 1.4;
}
.photo-list { display: grid; gap: 10px; margin: 8px 0 0; padding: 0; list-style: none; }
.photo-card {
  display: grid;
  grid-template-columns: 110px minmax(0, 1fr);
  gap: 12px;
  padding: 12px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
}
/* La carte du code-barres n'a pas de vignette (on a lu des chiffres, pas
   regardé une image) : elle prend toute la largeur au lieu de laisser une
   colonne vide de cent dix pixels. */
.photo-card-large { grid-template-columns: minmax(0, 1fr); }
.photo-vignette {
  width: 110px;
  height: 110px;
  border-radius: var(--vy-r-1);
  object-fit: cover;
  background: var(--vy-surface-2);
}
/* `min-width: 0` : sans lui, un titre long refuse de descendre sous sa
   largeur intrinsèque (un enfant de grille vaut `min-width: auto`) et pousse
   la carte hors de l'écran. Le piège est déjà documenté ailleurs dans le
   dépôt, il se referme ici aussi. */
.photo-corps { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.photo-verdict {
  margin: 0;
  font: 600 calc(12px * var(--vy-k))/1 var(--vy-ui);
  color: var(--vy-muted-3);
}
.photo-nom { margin: 0; font-size: calc(15px * var(--vy-k)); font-weight: 600; line-height: 1.25; overflow-wrap: anywhere; }
.photo-sous { margin: 0; font-size: calc(12.5px * var(--vy-k)); color: var(--vy-muted); line-height: 1.35; }
.photo-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.photo-actions .btn { min-height: 38px; }
.photo-pistes { display: grid; gap: 4px; margin: 2px 0 0; padding: 0; list-style: none; }
.photo-piste {
  display: block;
  width: 100%;
  min-height: 38px;
  padding: 8px 10px;
  border: 1px solid var(--vy-line-strong);
  border-radius: var(--vy-r-1);
  background: var(--vy-surface-2);
  color: var(--vy-ink);
  font: inherit;
  font-size: calc(13.5px * var(--vy-k));
  text-align: left;
  cursor: pointer;
  overflow-wrap: anywhere;
}
.photo-piste:hover { border-color: var(--vy-ink); }
.photo-foot { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
.photo-foot .btn { min-height: 44px; cursor: pointer; }

/* Sur un écran étroit, la vignette passe au-dessus : à 110 px de colonne, le
   verdict et deux boutons se battaient pour 190 px et le nom du disque
   partait sur quatre lignes. */
@media (max-width: 420px) {
  .photo-card { grid-template-columns: 84px minmax(0, 1fr); gap: 10px; }
  /* Redite VOULUE : la règle d'au-dessus a la même spécificité et vient
     après, donc elle emportait la carte sans vignette et lui laissait une
     colonne vide de 84 px sur TOUS les téléphones. Le piège de la cascade
     que ce dépôt paie régulièrement : ce n'est pas la spécificité qui
     tranche entre deux règles égales, c'est l'ordre. */
  .photo-card-large { grid-template-columns: minmax(0, 1fr); }
  .photo-vignette { width: 84px; height: 84px; }
  .photo-actions .btn { flex: 1 1 auto; justify-content: center; }
}

/* ── A2 · L'identification d'une pochette (l'IA, ChatGPT) ────────────────
   La carte d'identification vit DANS la trouvaille (ou sous la pochette du
   formulaire) : ce qui a été lu, le degré d'assurance en mots, les
   résultats en ligne avec les mêmes lignes que la recherche, et la zone où
   coller la réponse de ChatGPT. Un filet à gauche la distingue du verdict
   de la reconnaissance par empreinte, qui est une autre chose. */
.photo-ident { display: grid; gap: 8px; margin-top: 8px; min-width: 0; }
.photo-ident[hidden] { display: none; }
.photo-ident:empty { display: none; }
.ident-card {
  display: grid;
  gap: 4px;
  padding: 10px 12px;
  border-left: 3px solid var(--vy-accent);
  background: var(--vy-surface-2);
  border-radius: 0 var(--vy-r-1) var(--vy-r-1) 0;
}
.ident-conf { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 2px 0 0; }
.ident-conf-sur .badge { border-color: var(--vy-ink); color: var(--vy-ink); font-weight: 600; }
.ident-conf-incertain .badge { color: var(--vy-muted); border-style: dashed; border-color: var(--vy-line-strong); background: transparent; }
.ident-notes { font-size: calc(12.5px * var(--vy-k)); color: var(--vy-muted); line-height: 1.35; }
.photo-ident .results { margin: 0; padding: 0; list-style: none; }
.photo-ident .field { margin: 0; }
.ident-paste { min-height: 84px; font-family: var(--vy-mono); font-size: calc(13px * var(--vy-k)); }
.photo-ident .loading { margin: 0; }
/* Sous la pochette du formulaire : deux liens discrets, en ligne. */
.ident-tools { display: flex; flex-wrap: wrap; gap: 4px 14px; margin-top: 2px; }
.ident-tools .link-btn { display: inline-flex; align-items: center; gap: 5px; min-height: 38px; }
.ident-tools .ico { width: 15px; height: 15px; }

/* ── Le formulaire ────────────────────────────────────────────────────────── */
.form-cover {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  gap: 12px;
  align-items: center;
}
.form-cover-preview {
  width: 96px;
  aspect-ratio: 1;
  border-radius: var(--vy-r-0);
  overflow: hidden;
  background: var(--vy-paper-2);
  box-shadow: var(--vy-ombre-pochette);
}
.form-cover-preview img { width: 100%; height: 100%; object-fit: cover; }
.form-cover-tools { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

.wish-only { display: contents; }
.wish-only[data-off] { display: none; }

.fold {
  border: 1px solid transparent;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
}
.fold summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 48px;
  padding: 10px 14px;
  font: 600 calc(14.5px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
  list-style: none;
}
.fold summary::-webkit-details-marker { display: none; }
.fold summary .ico { color: var(--vy-muted); transition: transform var(--vy-t) ease; }
.fold[open] summary .ico { transform: rotate(180deg); }
.fold-body {
  display: grid;
  gap: 12px;
  padding: 4px 14px 14px;
}

/* ── Filtres ──────────────────────────────────────────────────────────────── */
.sheet-short .sheet-body { gap: 16px; }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 10 · LES 200 ET LE TIRAGE AU SORT
   └────────────────────────────────────────────────────────────────────────── */


/* La jauge : deux nombres, une barre d'encre. */
.ess-progress { display: grid; gap: 6px; }
.ess-progress-text { display: flex; justify-content: space-between; gap: 10px; font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); }
.ess-progress-text b { color: var(--vy-ink); font-family: var(--vy-ui); font-variant-numeric: tabular-nums; }
.ess-progress-track { height: 6px; border-radius: 3px; background: var(--vy-paper-2); overflow: hidden; }
.ess-progress-fill { height: 100%; border-radius: 3px; background: var(--vy-ink); transition: width var(--vy-t-2) ease; }
.ess-progress-fill.is-owned { background: var(--vy-accent); }


.ess-list { display: grid; }
.ess-row {
  display: grid;
  grid-template-columns: 34px 52px minmax(0, 1fr);
  grid-template-areas: "rank cover text" "rank cover acts";
  gap: 4px 10px;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid var(--vy-line);
  content-visibility: auto;
  contain-intrinsic-size: auto 96px;
}
.ess-row.is-owned { background: transparent; }
.ess-rank {
  grid-area: rank;
  align-self: start;
  padding-top: 2px;
  font-size: calc(12px * var(--vy-k));
  font-weight: 600;
  color: var(--vy-muted-3);
}
.ess-cover { grid-area: cover; width: 52px; align-self: start; }
.ess-cover .tile-cover { width: 52px; }
.ess-text { grid-area: text; min-width: 0; display: grid; gap: 2px; }
.ess-open {
  display: grid;
  gap: 2px;
  width: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
  min-width: 0;
}
.ess-artist { font-size: calc(12.5px * var(--vy-k)); color: var(--vy-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ess-title { font-size: calc(15px * var(--vy-k)); font-weight: 600; line-height: 1.25; overflow-wrap: anywhere; }
.ess-meta { font-family: var(--vy-ui); font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }
.ess-sources { display: flex; flex-wrap: wrap; gap: 4px; }
.ess-src {
  display: inline-flex;
  align-items: center;
  min-height: 18px;
  padding: 1px 7px;
  border: 0;
  border-radius: 999px;
  background: var(--vy-fill);
  font: 500 calc(11.5px * var(--vy-k))/1.2 var(--vy-ui);
  color: var(--vy-muted);
  white-space: nowrap;
}
.ess-state { font-size: calc(12.5px * var(--vy-k)); color: var(--vy-ink-2); }
.ess-state .link-btn { font-size: calc(12.5px * var(--vy-k)); }
.ess-acts { display: flex; gap: 6px; flex-wrap: nowrap; }
.ess-acts .btn { min-height: 38px; padding: 6px 10px; font-size: calc(12.5px * var(--vy-k)); }
.ess-acts .btn.is-on { background: var(--vy-ink); border-color: var(--vy-ink); color: var(--vy-bg); }

/* Le pied d'une ligne (octobre 2026) : les classements repliés en un bouton,
   puis les gestes en icônes, sur une seule rangée sous le texte. Au-delà de
   700 px, il passe à droite du texte, les classements dépliés dans le texte
   et les gestes avec leur libellé. */
.ess-row { grid-template-areas: "rank cover text" "rank cover bas"; }
.ess-bas {
  grid-area: bas;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 6px 10px;
  min-width: 0;
}
.ess-src-sum {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 38px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--vy-muted);
  font: 500 calc(12px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
.ess-src-sum .ico { width: 14px; height: 14px; transition: transform var(--vy-t) ease; }
.ess-src-sum[aria-expanded="true"] .ico { transform: rotate(180deg); }
.ess-src-sum:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 2px; }
.ess-row:not(.is-src) .ess-sources { display: none; }
.ess-state .link-btn { white-space: nowrap; }
.ess-row { grid-template-columns: 28px 52px minmax(0, 1fr); column-gap: 8px; }
.ess-acts { gap: 4px; }
.ess-acts .ess-act { position: relative; width: 44px; min-height: 44px; padding: 0; justify-content: center; }
.ess-acts .ess-act .ico { margin: 0; }
/* « Écouté » non coché : la coche reste grise. En encre dans un cadre, elle
   se lisait comme une case DÉJÀ cochée. */
.ess-acts .ess-act[data-act="ecoute"]:not(.is-on) { color: var(--vy-muted-3); }
.ess-act-lib {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* « Dans votre collection » est un état, pas une action principale : en
   texte d'interface, pas en capitales de méta, souligné comme un lien. */
.ess-meta .ess-state .link-btn {
  font: 500 calc(12px * var(--vy-k))/1.3 var(--vy-ui);
  letter-spacing: 0;
  color: var(--vy-ink-2);
  text-decoration-thickness: 1px;
}
/* L'en-tête des 200 au téléphone : la phrase sur les sources s'efface (le
   bouton « Les sources » juste à côté du titre les montre toutes), et les
   quatre filtres deviennent un seul contrôle segmenté au lieu de quatre
   pastilles de 44 px. */
@media (max-width: 699px) {
  #essSub { display: none; }
  #essFilters {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 2px;
    padding: 2px;
    border: 0;
    border-radius: var(--vy-r-1);
    background: var(--vy-fill);
    overflow: visible;
  }
  /* Le contrôle segmenté d'iOS (cf. `.seg`) : les pastilles perdent leur
     remplissage, le choix courant est posé sur la piste. */
  #essFilters .chip {
    justify-content: center;
    min-height: 40px;
    padding: 0 4px;
    border: 0;
    border-radius: calc(var(--vy-r-1) - 2px);
    background: transparent;
  }
  #essFilters .chip.is-on {
    background: var(--vy-seg-on);
    color: var(--vy-ink);
    font-weight: 600;
    box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 0 0 .5px rgba(0, 0, 0, .04);
  }
}
@media (max-width: 359px) {
  #essFilters .chip { font-size: calc(11.5px * var(--vy-k)); letter-spacing: 0; }
}
@media (min-width: 700px) {
  .ess-row { grid-template-columns: 40px 56px minmax(0, 1fr) auto; grid-template-areas: "rank cover text bas"; }
  .ess-bas { justify-content: flex-end; flex-wrap: nowrap; }
  .ess-src-sum { display: none; }
  .ess-row:not(.is-src) .ess-sources { display: flex; }
  .ess-acts .ess-act { width: auto; padding: 6px 10px; }
  .ess-act-lib { position: static; width: auto; height: auto; margin: 0; overflow: visible; clip-path: none; }
}

/* Les sources, dans leur modale : nom, lien, nombre lus. */
.src-list { display: grid; }
.src-list > li {
  display: grid;
  gap: 2px;
  padding: 10px 0;
  border-bottom: 1px solid var(--vy-line);
}
.src-name { font-weight: 600; }
.src-meta { font-size: calc(12.5px * var(--vy-k)); color: var(--vy-muted); }
/* Le lien dit le site (« Consulter sur en.wikipedia.org »), l'adresse
   entière vit dans le survol : elle prenait trois lignes (mesuré à 390px).
   Un lien seul sur sa ligne est une cible, pas un mot au fil d'une phrase. */
.src-list a { display: inline-flex; align-items: center; width: fit-content; min-height: 38px; color: var(--vy-ink); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--vy-accent); overflow-wrap: anywhere; font-size: calc(13px * var(--vy-k)); }

/* ── Le tirage au sort ────────────────────────────────────────────────────── */
.random-stage {
  display: grid;
  justify-items: center;
  gap: 12px;
  padding: 8px 0 4px;
}
/* La platine : le disque et la pochette dans la MÊME case, l'un par-dessus
   l'autre. Rien ne bouge autour pendant que le tirage se fait, et la
   pochette n'a plus besoin de « prendre la place » du disque. */
.random-platine {
  display: grid;
  width: min(52vw, 220px);
  aspect-ratio: 1;
}
.random-platine > * { grid-area: 1 / 1; }
.random-disc {
  width: 100%;
  aspect-ratio: 1;
  color: var(--vy-platine-corps);
  transform-origin: 50% 50%;
}
.random-disc .rd-label { fill: var(--vy-accent); }
.random-disc .rd-paper { fill: var(--vy-bg); }
/* Le disque tourne VRAIMENT : une rotation linéaire sans fin tant que le
   tirage court (l'ancienne courbe l'arrêtait au tiers de ses 700 ms, d'où
   « il n'apparaît pas assez longtemps »), puis il ralentit et s'efface
   pendant que la pochette monte. */
.random-disc.is-spinning { animation: vy-tourne-platine 900ms linear infinite; }
/* Rien de tenu à la fin : l'état d'arrivée (effacé) est porté par la RÈGLE,
   pas par un remplissage d'animation, le temps qu'app.js retire la classe et
   cache le disque. */
.random-disc.is-stopping {
  opacity: 0;
  animation: vy-tourne-fin 620ms cubic-bezier(.16, .84, .3, 1);
}
/* La pochette du tirage réutilise `.album-cover`, qui porte SA largeur
   (min(66vw, 300px), soit 257 px sur un téléphone de 390) : dans la platine
   elle la débordait et faisait grandir la case, c'est-à-dire exactement le
   saut qu'on venait de supprimer. Elle remplit la platine, point. */
.random-cover, .random-platine .album-cover { width: 100%; }
#randomResult.is-in { animation: vy-pochette-monte 480ms cubic-bezier(.16, .84, .3, 1); }
@keyframes vy-tourne-platine { to { transform: rotate(360deg); } }
@keyframes vy-tourne-fin {
  from { transform: rotate(0); opacity: 1; }
  to   { transform: rotate(150deg) scale(.86); opacity: 0; }
}
@keyframes vy-pochette-monte {
  from { opacity: 0; transform: translateY(14px) scale(.94); }
  to   { opacity: 1; transform: none; }
}
.random-lines { display: grid; gap: 2px; text-align: center; }
.random-artist { font-size: calc(14px * var(--vy-k)); color: var(--vy-muted); }
.random-title { font-size: calc(20px * var(--vy-k)); letter-spacing: -0.02em; overflow-wrap: anywhere; }
.random-ctx { font-family: var(--vy-ui); font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }
.random-acts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.random-acts .btn-main { grid-column: 1 / -1; }

/* Le LOT (octobre 2026) : plusieurs albums d'un coup. La platine et le disque
   ne servent qu'au tirage d'UN album ; un lot s'affiche tout de suite, en
   lignes. */
.seg-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
/* Une seule rangée de 44 px (le libellé à gauche, le contrôle à droite) : une
   feuille qui grandit de 80 px fait défiler son corps quand le tirage donne le
   focus à « J'écoute », et la platine saute (test_vinyles_ux_2026_09). */
.random-nombre { display: flex; align-items: center; gap: 12px; }
.random-nombre .field-label { flex: none; margin: 0; }
.random-nombre .seg { flex: 1 1 auto; min-width: 0; }
#randomSheet.is-lot .random-platine,
#randomSheet.is-lot .random-lines { display: none; }
#randomSheet.is-lot .random-stage { padding: 0; gap: 0; }
.random-lot-note { margin: 0; text-align: center; color: var(--vy-muted-3); font: 500 calc(12px * var(--vy-k))/1.4 var(--vy-ui); }
.random-list { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
.random-list[hidden], .random-lot-note[hidden] { display: none; }
.random-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 2px;
  min-width: 0;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
}
.random-row.is-new { animation: vy-pochette-monte 320ms cubic-bezier(.16, .84, .3, 1) backwards; animation-delay: calc(var(--i, 0) * 45ms); }
.random-row-main {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  min-height: 64px;
  min-width: 0;
  padding: 4px 4px 4px 4px;
  border: 0;
  border-radius: var(--vy-r-2);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.random-row-main:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: -2px; }
.random-row-cover { width: 56px; }
.random-row-cover .tile-cover { width: 100%; }
.random-row-txt { display: grid; gap: 1px; min-width: 0; }
.random-row-artist,
.random-row-title,
.random-row-ctx { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.random-row-artist { font-size: calc(12.5px * var(--vy-k)); color: var(--vy-muted); }
.random-row-title { font-size: calc(15px * var(--vy-k)); font-weight: 600; letter-spacing: -0.01em; }
.random-row-ctx { font: 500 calc(11.5px * var(--vy-k))/1.3 var(--vy-ui); color: var(--vy-muted-3); }
.random-row-swap { margin-right: 2px; }
:root[data-mouvement="reduit"] .random-row.is-new { animation: none; }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 11 · MODALES
   │ Ancrées en bas d'écran sur téléphone (là où vit le pouce), centrées
   │ au-delà. Le pied porte les gestes, il réserve la zone gestuelle.
   └────────────────────────────────────────────────────────────────────────── */

.modal {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  align-items: end;
  justify-items: center;
  padding: 0;
  background: var(--vy-voile-fort);
  overscroll-behavior: contain;
}

.modal-card {
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: 100%;
  max-width: 520px;
  max-height: 92dvh;
  padding: 18px 18px 0;
  background: var(--vy-card);
  border: 1px solid var(--vy-line-strong);
  border-bottom: 0;
  border-radius: var(--vy-r-3) var(--vy-r-3) 0 0;
  overflow: hidden;
}
.modal-card > * { flex: none; min-width: 0; }
.modal-wide { max-width: 640px; }

.modal-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 44px;
  gap: 10px;
  align-items: center;
}
.modal-head h2 { font-size: calc(19px * var(--vy-k)); letter-spacing: -0.02em; min-width: 0; }

.modal-sub {
  margin-top: -6px;
  font-size: calc(13.5px * var(--vy-k));
  line-height: 1.45;
  color: var(--vy-muted);
}

.modal-body {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  min-height: 0;
  padding-bottom: 4px;
}

.modal-foot {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: auto;
  padding: 14px 0;
  padding-bottom: calc(14px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--vy-line);
  background: var(--vy-card);
}
.modal-foot .btn { flex: 1 1 auto; min-width: 0; }

.confirm-text { font-size: 16px; line-height: 1.5; padding: 6px 0; }

.kv-secret dd { font-family: var(--vy-mono); font-size: calc(17px * var(--vy-k)); user-select: all; }

/* ── Les pochettes trouvées en ligne ───────────────────────────────────────── */
.cover-choices {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px;
}
.cover-choice {
  display: grid;
  gap: 4px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.cover-choice small { font-size: calc(12px * var(--vy-k)); color: var(--vy-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ── Recadrage : un outil photo, donc sombre quel que soit le thème ─────── */
/* Le recadrage a son PROPRE plan : il s'ouvre depuis la fiche, depuis le
   formulaire et depuis la modale « Je l'ai acheté », qui sont toutes des
   `.modal` au meme z-index. Pose plus haut dans le document, il passait alors
   DESSOUS celle qui venait de l'ouvrir : mesure au navigateur,
   `elementFromPoint` au centre de « Utiliser cette photo » rendait le bouton
   de la modale d'achat, et le geste souhait vers collection avec photo etait
   impossible au telephone. */
.modal-crop { align-items: stretch; background: #0a0a0a; z-index: 80; }

.crop-card {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100dvh;
  background: #0a0a0a;
  color: #f5f4f1;
}
.crop-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}
.crop-stage canvas {
  display: block;
  width: 100%;
  height: 100%;
  cursor: grab;
}
.crop-stage canvas:active { cursor: grabbing; }

.crop-status {
  padding: 10px 16px 0;
  font-size: calc(13px * var(--vy-k));
  font-weight: 500;
  line-height: 1.4;
  text-align: center;
  color: #f5f4f1;
}
.crop-status[hidden] { display: none; }
.crop-help {
  padding: 10px 16px 0;
  font-size: calc(13px * var(--vy-k));
  line-height: 1.4;
  text-align: center;
  color: #c8c6c0;
}

.crop-zoom { display: block; padding: 6px 16px; }
.crop-zoom input[type="range"] {
  display: block;
  width: 100%;
  height: 44px;
  margin: 0;
  background: transparent;
  accent-color: #f5f4f1;
  cursor: pointer;
}

.crop-tools {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 6px;
  padding: 0 16px;
}
/* Quatre boutons sur une ligne ne tiennent qu'au-delà de 640px : en dessous,
   « Détecter la pochette » s'écrivait sur trois lignes (mesuré à 390px). */
@media (max-width: 640px) {
  .crop-tools { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.crop-tools .btn { min-width: 0; padding: 8px 6px; background: transparent; color: #f5f4f1; border-color: rgba(255, 255, 255, .22); font-size: calc(12.5px * var(--vy-k)); }
.crop-tools .btn .ico { width: 16px; height: 16px; }

.crop-card .modal-foot {
  margin-top: 0;
  padding: 12px 16px;
  padding-bottom: calc(12px + env(safe-area-inset-bottom));
  border-top-color: rgba(255, 255, 255, .12);
  background: #0a0a0a;
}
.crop-card .btn-ghost { color: #c8c6c0; border-color: rgba(255, 255, 255, .22); }
.crop-card .btn-main { background: #f5f4f1; border-color: #f5f4f1; color: #111110; box-shadow: none; }

/* ── Scanner ──────────────────────────────────────────────────────────────── */
.modal-scan { align-items: stretch; background: #0a0a0a; }

.scan-card {
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto auto;
  width: 100%;
  height: 100dvh;
  background: #0a0a0a;
  color: #f5f4f1;
}
.scan-card video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #000;
}
.scan-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(78vw, 360px);
  height: 34vw;
  max-height: 160px;
  transform: translate(-50%, -80%);
  border: 2px solid #f5f4f1;
  border-radius: var(--vy-r-1);
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .35);
  pointer-events: none;
}
.scan-help { padding: 12px 16px 4px; font-size: calc(14px * var(--vy-k)); text-align: center; color: #c8c6c0; }
.scan-card .btn {
  margin: 8px 16px;
  margin-bottom: calc(14px + env(safe-area-inset-bottom));
  background: transparent;
  color: #f5f4f1;
  border-color: rgba(255, 255, 255, .22);
}

/* ── A2 · Écouter le disque ───────────────────────────────────────────────
   Une modale ordinaire (pas l'écran noir du scanner : il n'y a rien à
   cadrer), une jauge fine qui se remplit pendant les dix secondes, puis le
   verdict et les lignes de résultats. Sans fournisseur, le texte honnête. */
.listen-help { margin: 0 0 12px; font-size: calc(15px * var(--vy-k)); line-height: 1.5; }
.listen-gauge { height: 6px; border-radius: 3px; background: var(--vy-paper-2); overflow: hidden; }
.listen-fill { height: 100%; width: 0; border-radius: 3px; background: var(--vy-accent); transition: width .2s linear; }
.listen-count { margin: 8px 0 0; font: 500 calc(12px * var(--vy-k))/1.4 var(--vy-ui); color: var(--vy-muted-3); min-height: 17px; }
.listen-result { display: grid; gap: 8px; margin-top: 14px; }
.listen-verdict { margin: 0; font-size: calc(15.5px * var(--vy-k)); font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; }
.listen-result .results { margin: 0; padding: 0; list-style: none; }
.listen-honest { margin: 0 0 10px; font-size: calc(15px * var(--vy-k)); line-height: 1.55; }
.listen-honest-sub { font-size: calc(13.5px * var(--vy-k)); color: var(--vy-muted); }
.listen-yes[hidden], .listen-no[hidden], .listen-result[hidden] { display: none; }
#listenFoot .btn[hidden] { display: none; }

/* ── A2 · Le menu du + (appui long) et l'ajout express ───────────────────
   Le menu s'ancre sous le bouton, aligné à DROITE (le + est au bord droit
   de la barre : centré sur lui, un menu de 260 px sortait de l'écran). Le
   bouton refuse la sélection et le rappel de lien : un appui long sur
   téléphone ne doit ni surligner ni proposer « copier ». */
.add-wrap { position: relative; }
#btnAdd { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; touch-action: manipulation; }
/* Menu ouvert, le + reste le seul aplat d'encre de la barre : la règle
   `.top-btn[aria-expanded="true"]` des autres boutons à menu le peignait en
   papier avec son « + » blanc dessus (mesuré à 390 px ; sur ordinateur le
   survol le remettait en encre et cachait le défaut). C'est le menu qui
   annonce l'ouverture, pas le bouton. */
#btnAdd[aria-expanded="true"], #btnAdd[aria-expanded="true"]:hover { background: var(--vy-btn-bg); color: var(--vy-btn-fg); }
.add-menu { left: auto; right: 0; transform: none; }
.add-menu[hidden] { display: none; }
/* Le mode série : l'interrupteur et, dessous, le compteur. */
.serie-line { display: grid; gap: 4px; }
.serie-count { margin: 0 2px; font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); }
.serie-count .link-btn { margin-left: 4px; min-height: 38px; padding: 8px 4px; }
/* La dictée : un micro DANS le champ, à droite, comme une loupe. */
.field-wrap { position: relative; display: block; min-width: 0; }
.field-wrap > input { padding-right: 44px; }
.field-dictate { position: absolute; top: 50%; right: 4px; width: 38px; height: 38px; transform: translateY(-50%); color: var(--vy-muted); }
.field-dictate[hidden] { display: none; }
.field-dictate.is-on { color: var(--vy-accent); background: var(--vy-paper-2); }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 12 · PAGE DE PARTAGE
   └────────────────────────────────────────────────────────────────────────── */

.share-shell { display: flex; flex-direction: column; min-height: 100dvh; }
.share-main { padding-bottom: calc(28px + env(safe-area-inset-bottom)); display: grid; gap: 12px; align-content: start; }
.share-flag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 30px;
  padding: 3px 10px;
  border: 1px solid transparent;
  border-radius: 999px;
  color: var(--vy-muted);
  font: 500 calc(12px * var(--vy-k))/1 var(--vy-ui);
  background: var(--vy-fill);
}
.share-flag .ico { width: 14px; height: 14px; }
.share-foot { padding-top: 16px; font-size: calc(13px * var(--vy-k)); color: var(--vy-muted-3); text-align: center; line-height: 1.5; }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 12 bis · LE FIL, LES MEMBRES, LES MESSAGES (le lot social, septembre 2026)
   │ Même matière que le reste : des lignes sous un filet, jamais des cartes
   │ flottantes ; la seule couleur est l'encre, l'accent reste réservé à la
   │ pastille « du nouveau » et au soulignement des noms. Les bulles d'une
   │ conversation sont la seule exception assumée (les miennes en aplat
   │ d'encre douce, les siennes sous un filet) : c'est la convention de
   │ tout téléphone, pas un tic de gabarit.
   └────────────────────────────────────────────────────────────────────────── */

/* ── La barre du haut : pastille et compteur sur un bouton d'icône ─────── */
.top-btn { position: relative; }
.top-dot {
  position: absolute;
  top: 9px;
  right: 9px;
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--vy-accent);
  box-shadow: 0 0 0 2px var(--vy-bg);
}
.top-count {
  position: absolute;
  top: 5px;
  right: 3px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--vy-ink);
  color: var(--vy-bg);
  font: 600 calc(11.5px * var(--vy-k))/18px var(--vy-ui);
  text-align: center;
  box-shadow: 0 0 0 2px var(--vy-bg);
}
.top-dot[hidden], .top-count[hidden] { display: none; }
/* La pastille de « Plus » : un point d'accent à côté du libellé. */
.more-dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--vy-accent);
  flex: none;
}
.more-dot[hidden] { display: none; }

/* ── L'accueil : le bandeau d'invitation ───────────────────────────────── */
.invite-banner {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin: 0 0 8px;
  padding: 10px 12px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-1);
  background: var(--vy-groupe);
  font-size: calc(14px * var(--vy-k));
  line-height: 1.45;
  color: var(--vy-ink-2);
}
.invite-banner .ico { flex: none; width: 20px; height: 20px; color: var(--vy-ink); margin-top: 1px; }
.invite-banner p { margin: 0; }

/* ── Le fil ───────────────────────────────────────────────────────────── */
.feed-ask {
  display: grid;
  gap: 6px;
  padding: 12px 14px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
}
.feed-ask-title { font-weight: 600; }
.feed-ask-text { font-size: calc(13.5px * var(--vy-k)); line-height: 1.5; color: var(--vy-muted); }
.feed-ask .btn-row { margin-top: 4px; }

/* La pilule « 3 nouveaux » : collée sous la barre du haut, au-dessus de la
   liste, pour ramener en tête sans rien bouger sous le doigt. */
.feed-new {
  position: sticky;
  top: calc(var(--vy-top-real, var(--vy-top-h)) + 8px);
  z-index: 5;
  justify-self: center;
  margin: 0 auto;
  padding: 7px 14px;
  border: 0;
  border-radius: 999px;
  background: var(--vy-btn-bg);
  color: var(--vy-btn-fg);
  font: 600 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  box-shadow: var(--vy-btn-ombre);
  cursor: pointer;
  min-height: 44px;
}
.feed-new[hidden] { display: none; }

.feed-list { display: grid; border-top: 1px solid var(--vy-line-strong); }
.feed-card {
  display: grid;
  grid-template-columns: 84px minmax(0, 1fr);
  gap: 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--vy-line);
}
.feed-open {
  display: block;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  border-radius: 4px;
}
.feed-cover { width: 84px; }
.feed-body { display: grid; gap: 2px; min-width: 0; align-content: start; }
.feed-who {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: calc(12.5px * var(--vy-k));
  color: var(--vy-muted-3);
}
.feed-name { font-weight: 600; color: var(--vy-ink); }
/* Le nom du membre est la SEULE porte vers son profil depuis une carte du
   fil : une cible de 22×19px ne se vise pas au pouce. Ce que la zone tapable
   gagne en garniture est reprise en marge négative, pour que la ligne de
   l'en-tête de carte ne s'écarte pas d'autant. */
button.feed-name {
  font-size: calc(12.5px * var(--vy-k));
  min-height: 38px;
  padding: 8px 6px;
  margin: -8px -6px;
}
.feed-badge { min-height: 20px; padding: 0 7px; font-size: calc(10.5px * var(--vy-k)); }
.feed-artist { font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.feed-title { font-weight: 600; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.feed-meta { font: 500 calc(12px * var(--vy-k))/1.4 var(--vy-ui); color: var(--vy-muted-3); }
.feed-acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
/* La pastille « Admin » du Portfolio est remontée au-dessus de la barre
   d'onglets (cf. le bloc du socle) : elle occupe donc la bande juste
   au-dessus, là où « Charger la suite » se trouve en fin de liste. Le bouton
   garde sa place libre. */
.feed-more {
  justify-self: center;
  margin: 4px auto calc(64px + env(safe-area-inset-bottom, 0px));
}
@media (min-width: 900px) {
  .feed-more { margin-bottom: 0; }
}

/* Un nouveau venu glisse d'un cran : court, sur transform et opacité. */
@keyframes vy-feed-new {
  from { transform: translateY(-8px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.feed-card.is-new { animation: vy-feed-new var(--vy-t-2) ease-out; }

/* ── La fiche d'un album : la ligne du propriétaire, l'interrupteur ──── */
.owner-line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 10px;
  padding: 10px 12px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-1);
  background: var(--vy-groupe);
  font-size: calc(14px * var(--vy-k));
}
.owner-line > .ico { flex: none; color: var(--vy-muted); }
.owner-line > span { flex: 1 1 auto; min-width: 0; }
.public-row { display: grid; gap: 0; }
.public-row .field-help { margin-top: -2px; }

/* ── Un membre ────────────────────────────────────────────────────────── */
.member-head { display: grid; gap: 6px; }
.member-meta { font-size: calc(13.5px * var(--vy-k)); color: var(--vy-muted); }
.member-acts { margin-top: 6px; }

/* ── Les fils de messages ──────────────────────────────────────────────── */
.thread-list { display: grid; border-top: 1px solid var(--vy-line-strong); }
.thread-list > li { border-bottom: 1px solid var(--vy-line); }
.thread-btn {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 64px;
  padding: 10px 4px;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  font: 400 calc(14.5px * var(--vy-k))/1.35 var(--vy-ui);
  text-align: left;
  cursor: pointer;
}
.thread-avatar {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--vy-line-strong);
  background: var(--vy-card);
  font: 600 calc(15px * var(--vy-k))/1 var(--vy-titre);
  color: var(--vy-ink);
}
.thread-body-line { flex: 1 1 auto; min-width: 0; display: grid; gap: 2px; }
.thread-head { display: flex; align-items: baseline; gap: 8px; }
.thread-name { font-weight: 600; flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thread-when { flex: none; font: 500 calc(12px * var(--vy-k))/1.4 var(--vy-ui); color: var(--vy-muted-3); }
.thread-last { font-size: calc(13.5px * var(--vy-k)); color: var(--vy-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.thread-btn.is-unread .thread-last { color: var(--vy-ink); font-weight: 500; }

/* ── Une conversation ─────────────────────────────────────────────────── */
.panel-thread .panel-body { gap: 8px; padding-bottom: 12px; }
.msg-list { display: grid; gap: 8px; align-content: end; }
.msg {
  display: grid;
  gap: 4px;
  justify-self: start;
  max-width: min(78%, 520px);
  padding: 8px 12px;
  border: 1px solid var(--vy-line-strong);
  border-radius: 14px 14px 14px 4px;
  background: var(--vy-card);
}
.msg-mine {
  justify-self: end;
  border-color: transparent;
  border-radius: 14px 14px 4px 14px;
  background: var(--vy-paper-2);
}
.msg-body { white-space: pre-wrap; overflow-wrap: anywhere; font-size: calc(14.5px * var(--vy-k)); line-height: 1.45; }
.msg-when { font: 500 calc(11.5px * var(--vy-k))/1.4 var(--vy-ui); color: var(--vy-muted-3); justify-self: end; }
.msg-album {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 6px;
  border: 1px solid var(--vy-line);
  border-radius: var(--vy-r-1);
  background: var(--vy-bg);
  color: var(--vy-ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.msg-album-cover { width: 48px; flex: none; }
.msg-album-text { display: grid; gap: 1px; min-width: 0; }
.msg-album-artist { font-size: calc(12px * var(--vy-k)); color: var(--vy-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-album-title { font-size: calc(13.5px * var(--vy-k)); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Le composeur, ancré en bas du panneau (donc de l'écran au téléphone) :
   la zone sûre est comptée, sans quoi le bouton d'envoi passe sous la barre
   gestuelle des iPhone. */
.msg-compose {
  flex: none;
  display: grid;
  gap: 6px;
  padding: 8px 12px;
  padding-bottom: calc(8px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--vy-line);
  background: var(--vy-bg);
}
.msg-closed { font-size: calc(13.5px * var(--vy-k)); color: var(--vy-muted); text-align: center; padding: 6px 0; }
.msg-compose-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) 44px;
  gap: 8px;
  align-items: end;
}
.msg-swap { align-self: end; min-height: 44px; }
.msg-field { display: block; min-width: 0; }
.msg-field textarea {
  display: block;
  width: 100%;
  min-height: 44px;
  max-height: 160px;
  padding: 10px 12px;
  border: 1px solid var(--vy-line-strong);
  border-radius: 22px;
  background: var(--vy-card);
  color: var(--vy-ink);
  font: 400 16px/1.35 var(--vy-ui);
  resize: none;
  overflow-y: auto;
}
.msg-send {
  width: 44px;
  height: 44px;
  background: var(--vy-btn-bg);
  color: var(--vy-btn-fg);
  box-shadow: var(--vy-btn-ombre);
}
.msg-send:disabled { opacity: .5; cursor: default; }

/* Sur téléphone, le bouton « Proposer un échange » et le champ de saisie ne
   tiennent pas sur la même ligne : le libellé prend sa largeur (`auto`) et il
   ne restait que 158px pour écrire, mesuré à 390px. Le bouton passe DESSOUS,
   le champ prend toute la ligne. Le repli ne valait que sous 380px, c'est-à-dire
   nulle part sur les téléphones courants. */
@media (max-width: 760px) {
  .msg-compose-row { grid-template-columns: minmax(0, 1fr) 44px; }
  .msg-swap { grid-column: 1 / -1; justify-self: start; }
}


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 13 · ANIMATIONS
   │ Courtes (180ms au plus), sur `transform` et `opacity` seulement, chacune
   │ accrochée à un geste réel. Aucune animation continue. Le seul mouvement
   │ plus long est le disque du tirage au sort (700ms, une fois).
   └────────────────────────────────────────────────────────────────────────── */

@keyframes vy-glisse {
  from { transform: translateY(14px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.sheet, .panel { animation: vy-glisse var(--vy-t-2) ease-out; }

@keyframes vy-monte {
  from { transform: translateY(12px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.modal-card { animation: vy-monte var(--vy-t-2) ease-out; }

@keyframes vy-toast {
  from { transform: translate(-50%, 10px); opacity: 0; }
  to   { transform: translate(-50%, 0);    opacity: 1; }
}
.toast { animation: vy-toast var(--vy-t) ease-out; }

@keyframes vy-fondu {
  from { opacity: 0; transform: scale(.98); }
  to   { opacity: 1; transform: scale(1); }
}
.album-cover.is-tire { animation: vy-fondu var(--vy-t-2) ease-out; }

@keyframes vy-tourne {
  from { transform: rotate(0); }
  to   { transform: rotate(720deg); }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
    transition-duration: .01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 13 bis · TOUT CE QUI NOMME SE TOUCHE (audit fonctionnel, septembre 2026)
   │ Un artiste, un genre, une année, un label sont des boutons « lien
   │ discret » : le mot garde sa place dans la phrase, un filet fin dit qu'il
   │ mène quelque part, et au doigt sa cible monte à 44 px par le padding
   │ (vertical, sur un inline : la ligne ne bouge pas). Octobre 2026 : le
   │ pointillé d'accent est devenu un filet plein, gris translucide
   │ (`--vy-lien-filet`) ; il se voit, il ne crie pas, et il tient avec
   │ l'accent « Encre » (un pointillé gris clair sur un mot gris clair ne se
   │ distinguait plus). Au survol, il prend la couleur du mot.
   └────────────────────────────────────────────────────────────────────────── */
.link-nav {
  display: inline;
  padding: 0;
  margin: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: var(--vy-lien-filet);
  cursor: pointer;
  border-radius: 2px;
}
.link-nav:hover,
.link-nav:focus-visible { text-decoration-color: currentColor; color: var(--vy-ink); }
.link-nav:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 2px; }
/* Dans une ligne tronquée (résultat, ligne des 200), le bouton prend la ligne. */
.result-artist .link-nav,
.ess-artist .link-nav,
.feed-artist .link-nav { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
/* Une pastille qui mène quelque part (un genre de la fiche, une suggestion) :
   l'encre pleine, là où une pastille d'information reste à l'encre seconde.
   (Le filet en tirets d'avant n'a plus de filet sur quoi se poser.) */
button.chip.chip-nav { color: var(--vy-ink); }
.chip-kind { font-size: calc(11px * var(--vy-k)); color: var(--vy-muted); }
/* Les suggestions, sous la barre de recherche, collantes comme elle. */
.search-suggest {
  position: sticky;
  top: calc(var(--vy-top-real, calc(var(--vy-top-h) + max(0px, env(safe-area-inset-top, 0px) - 6px))) + 61px);
  z-index: 28;
  padding: 6px 12px 8px;
  background: var(--vy-bg);
  border-bottom: 1px solid var(--vy-line);
}
.reco-links { font-size: calc(12px * var(--vy-k)); color: var(--vy-muted); }
.listen-link { display: inline-flex; align-items: center; gap: 4px; padding: 0; border: 0; background: transparent; font: inherit; cursor: pointer; text-decoration: none; color: var(--vy-accent-texte); }
.listen-link:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 2px; }
@media (pointer: coarse) { .listen-link { padding: 12px 0; } }
.listen-link .ico { width: 12px; height: 12px; }
.listen-btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; }
.stream-bar { margin: 0 0 14px; }
.ess-meta .link-nav { font: inherit; }
button.ess-src { cursor: pointer; background: transparent; }
button.ess-src:hover,
button.ess-src:focus-visible { border-color: var(--vy-ink); color: var(--vy-ink); }
button.bar-lab { display: block; min-width: 0; text-align: left; }
/* À partir de 900 px la navigation est une colonne fixe à gauche, et la
   barre de recherche (pleine largeur, collante) passait DESSOUS : l'icône et
   les 220 premiers pixels du champ étaient recouverts (mesuré à 1280 px :
   `elementFromPoint` sur l'icône rendait l'onglet Collection). Défaut
   préexistant trouvé par l'audit fonctionnel ; la barre et ses suggestions
   commencent après la colonne, comme le contenu. */
@media (min-width: 900px) {
  .searchbar,
  .search-suggest { padding-left: calc(28px + var(--vy-nav, 220px)); padding-right: 28px; }
}
@media (pointer: coarse) {
  .link-nav { padding: 12px 0; }
  .result-artist .link-nav,
  .ess-artist .link-nav,
  .feed-artist .link-nav { padding: 11px 0; }
  button.ess-src { min-height: 38px; padding: 6px 8px; }
}

/* ┌──────────────────────────────────────────────────────────────────────────
   │ 14 · TÉLÉPHONE ET GRANDS ÉCRANS
   └────────────────────────────────────────────────────────────────────────── */

@media (max-width: 760px) {
  /* 1 · AUCUN CHAMP SOUS 16px. En dessous, iOS zoome tout seul au premier
     tap. La règle NOMME LES PORTEUSES : un sélecteur sur `input` nu se
     ferait battre par la moindre classe qui pose une taille. */
  .field input,
  .field select,
  .field .field-menu-btn,
  .field textarea,
  .searchbar input,
  .auth-form input,
  .stack-form input,
  .stack-form select,
  .modal-body input,
  .modal-body select,
  .fold-body input,
  .fold-body select,
  .fold-body textarea,
  .import-zone textarea,
  .msg-field textarea,
  .field input[type="file"] {
    font-size: 16px;
  }
  /* Une porteuse déclarée APRÈS ce bloc ne peut pas y être corrigée : une
     media query n'ajoute aucune priorité, seul l'ordre tranche. Elle porte
     donc son propre `@media (max-width: 760px)` à côté de sa règle de base
     (cf. `.biblio-find-input`, plus bas). */

  /* 2 · CIBLES TACTILES : 44px partout. Les boutons, les pastilles et les
     actions d'une carte de reco portent déjà ce plancher dans leur règle de
     base : le redire ici ne ferait que RABAISSER une valeur plus généreuse. */
  .icon-btn, .top-btn { width: 44px; height: 44px; }
  .theme-btn { width: 44px; height: 44px;
  background: var(--vy-fill);
}
  .seg-btn { min-height: 44px; }
  .tool-btn { min-height: 44px; }

  /* 3 · Les zones défilantes retiennent leur défilement. */
  .sheet-body, .panel-body, .modal-body, .rail, .chips-row { overscroll-behavior: contain; }

  /* 4 · Trois colonnes de pochettes par défaut (réglable 2 à 4). */
  .tiles { grid-template-columns: repeat(var(--vy-cols, 3), minmax(0, 1fr)); }
}

/* Le panneau d'infos du bac n'existe qu'à partir de 900px : en dessous, la
   légende du pied fait le travail, et un panneau n'aurait pas de coin libre. */
@media (max-width: 899px) {
  .pile-info { display: none; }
}

/* Les trois sélecteurs du pressage : « 12 pouces » ne tenait dans aucune
   colonne de tiers sous 400px (mesuré : 55px disponibles à 390, 31 à 320).
   Deux colonnes, et des libellés courts posés par le JS. */
@media (max-width: 400px) {
  .field-row-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* La boîte des écoutes : à 320px le texte n'avait que 69px à côté du bouton
   (cinq lignes, mesuré). Le bouton passe dessous, pleine largeur. */
@media (max-width: 360px) {
  .plays { grid-template-columns: minmax(0, 1fr); }
  .plays .btn { width: 100%; }
  .field-row-cond { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 380px) {
  /* Cinq onglets : 10,5px à 320px, jamais en dessous. */
  .tab { font-size: calc(10.5px * var(--vy-k)); }
  /* Quatre boutons de 44px dans la barre du haut (fil, messages, loupe, +) :
     à 360px il reste 160px pour le titre, à 320px 120 ; le logo se réduit
     et la barre serre ses écarts plutôt que de couper un bouton. */
  .topbar { gap: 6px; padding-left: 10px; padding-right: 8px; }
  .top-right { gap: 2px; }
  .topbar .wordmark-sm { height: 24px; }
  .tool-btn { padding: 6px 7px; }
  /* Le libellé du tri (« Artiste ») coupait le compteur en deux lignes. */
  .toolbar .tool-lab { display: none; }
}

/* Un bandeau étroit : le texte s'écrivait sur six lignes dans la moitié
   gauche, le bouton seul dans la moitié droite (mesuré à 320px). Le bouton
   passe sous le texte, aligné à droite. */
@media (max-width: 400px) {
  .banner { flex-wrap: wrap; }
  .banner p { flex-basis: calc(100% - 32px); }
  .banner .btn { margin-left: auto; }
  /* Deux boutons (le bandeau des animations) : le premier pousse la paire à
     droite, le second la suit, sinon chacun prenait la moitié de l'espace. */
  .banner .btn + .btn { margin-left: 0; }
}

/* Tuiles étroites (trois par ligne à 360px et moins) : les deux pastilles
   se resserrent pour tenir côte à côte sur 89px. */
@media (max-width: 360px) {
  .tile-rank { left: 4px; top: 4px; padding: 1px 4px; }
  .tile-badge { right: 4px; top: 4px; padding: 2px 5px; }
}

/* Un écran court (iPhone SE) : l'état vide poussait sa seule action sous la
   barre d'onglets (mesuré : à moitié sous la barre à 320×568). */
@media (max-height: 600px) {
  .empty { padding-top: 0; gap: 6px; }
  .empty .btn { margin-top: 2px; }
  .empty-ico { display: none; }
  /* Le titre commun des pages (octobre 2026) a pris sa ligne au-dessus de la
     barre : sur un écran court, il la rend en serrant sa hauteur, et le
     grand titre redescend d'un cran (le casier y gagne sa pochette). */
  .view-head { min-height: 0; }
  .view-title { font-size: 24px; }
}

/* ── À partir de 480px : quatre pochettes par ligne, le réglage ne joue plus ─ */
@media (min-width: 480px) {
  .tiles { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 10px; }
  .tiles-mini { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .stat-tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ess-row { grid-template-columns: 40px 56px minmax(0, 1fr); }
}

/* À partir de 520px, la colonne du milieu d'une ligne de rangement passe les
   deux cents pixels : le prix du disque revient à côté du cumul. */
@media (min-width: 520px) {
  .ord-prix { display: inline; }
}

/* ── À partir de 560px : la modale se décolle du bord et se centre ────────── */
@media (min-width: 560px) {
  .modal { align-items: center; padding: 24px; }
  .modal-card {
    border-bottom: 1px solid var(--vy-line-strong);
    border-radius: var(--vy-r-3);
    padding-bottom: 0;
    max-height: 86dvh;
  }
  .modal-foot { padding-bottom: 16px; }
  .modal-crop, .modal-scan { padding: 0; }
  .crop-card, .scan-card { height: 100dvh; }
}

/* ── À partir de 720px : les feuilles et les panneaux deviennent des fenêtres
   centrées, bornées à 720px, posées sur un voile. Le voile est une ombre
   étalée sur toute la fenêtre : un élément fixe ET transformé ne peut pas
   recevoir un voile en pseudo-élément. ───────────────────────────────────── */
@media (min-width: 720px) {
  .panel, .sheet {
    inset: auto;
    top: 50%;
    left: 50%;
    width: min(720px, calc(100vw - 48px));
    height: min(92dvh, 900px);
    transform: translate(-50%, -50%);
    border: 1px solid var(--vy-line-strong);
    border-radius: var(--vy-r-3);
    overflow: hidden;
    box-shadow: 0 0 0 100vmax var(--vy-voile-fort);
  }
  .sheet-short { height: auto; max-height: 92dvh; }
  .panel-head, .sheet-head { padding-top: 6px; }
  .panel-body, .sheet-body { padding: 20px 22px 28px; }
  .tiles { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .album-cover { width: 300px; }
  /* Une piste : le numéro, le titre, et le détail à droite, comme une durée. */
  .piste { grid-template-columns: 34px minmax(0, 1fr) auto; }
  .piste-d { grid-column: auto; text-align: right; }
  .album-actions { grid-template-columns: repeat(2, minmax(0, 220px)); }
  @keyframes vy-glisse {
    from { transform: translate(-50%, calc(-50% + 14px)); opacity: 0; }
    to   { transform: translate(-50%, -50%); opacity: 1; }
  }
}

/* ── À partir de 900px : la navigation devient une colonne à gauche, et
   l'accueil pose le discours à gauche, le cahier à droite ────────────────── */
@media (min-width: 900px) {
  .shell { --vy-nav: 220px; }

  .tabbar {
    top: var(--vy-top-real, var(--vy-top-h));
    right: auto;
    width: var(--vy-nav);
    grid-template-columns: minmax(0, 1fr);
    align-content: start;
    gap: 4px;
    padding: 16px 14px;
    border-top: 0;
    border-right: 1px solid var(--vy-line);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  .tab {
    grid-template-columns: 22px minmax(0, 1fr);
    justify-items: start;
    align-items: center;
    gap: 12px;
    min-height: 44px;
    padding: 8px 12px;
    font-size: calc(14.5px * var(--vy-k));
  }
  .tab.is-on::after {
    top: 50%;
    left: -14px;
    width: 3px;
    height: 24px;
    margin: -12px 0 0;
  }
  .main { padding-top: 22px; padding-right: 28px; padding-left: calc(28px + var(--vy-nav)); max-width: calc(1400px + var(--vy-nav)); }
  /* Le bac affiché, le plafond de largeur saute : la pile va jusqu'au bord
     droit de l'écran (mesuré à 1907 px : 290 px de papier vide à droite). */
  .main.is-bac, .main.is-nuage { max-width: none; }
  .pile { margin: 0 -28px; }
  .pile-foot { padding-left: 28px; padding-right: 28px; }
  .pile-artist { font-size: 24px; }
  .tiles { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 18px 12px; }
  .toast { bottom: 28px; }
  .rail { margin: 0; padding: 6px 0 10px; }
  .hub-rail { margin: 0; padding: 2px 0 8px; flex-wrap: wrap; overflow: visible; }
  .hub-card { width: 176px; }

  /* La vitrine : le bac prend toute la scène, le titre et la fiche se
     posent dans ses deux coins vides (au-dessus de la pochette de devant,
     sous la diagonale des cartes). Le pied garde la moitié gauche : à
     droite, la fiche. */
  .vitrine { min-height: min(78vh, 760px); }
  .vitrine .pile { position: relative; }
  .vitrine .pile-stage { height: calc(min(78vh, 760px) - 140px); }
  .vitrine .pile-foot { width: min(56%, 620px); }
  .vitrine-titre { position: absolute; top: 24px; left: 28px; z-index: 2; padding: 0; pointer-events: none; }
  .auth-panel { position: absolute; right: 28px; bottom: 40px; z-index: 2; width: 360px; margin: 0; }
  .dos { padding-top: 56px; }
  .faces { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 40px; }
  /* Le bac de la collection : le panneau d'infos remplace la légende du
     pied, qui reste dans l'arbre d'accessibilité (elle est `aria-live`). */
  .pile-info ~ .pile-foot .pile-caption {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}

@media (min-width: 1300px) {
  .tiles { grid-template-columns: repeat(7, minmax(0, 1fr)); }
}


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 15 · SURVOL
   │ Ce qui suit n'existe QUE pour un appareil qui a un pointeur, et vient en
   │ dernier : un bloc `hover: none` posé plus haut perdrait contre tout
   │ `:hover` déclaré après lui, et l'état resterait COLLÉ après un tap.
   └────────────────────────────────────────────────────────────────────────── */

@media (hover: hover) and (pointer: fine) {
  .btn:hover { border-color: transparent; background: var(--vy-fill-2); }
  .btn-main:hover { background: var(--vy-btn-bg); border-color: var(--vy-btn-bg); color: var(--vy-btn-fg); transform: none; box-shadow: none; opacity: .88; }
  .btn-ghost:hover { border-color: transparent; color: var(--vy-accent-texte); background: var(--vy-fill); }
  .icon-btn:hover { background: var(--vy-paper-2); }
  .top-btn-main:hover { background: var(--vy-btn-bg); color: var(--vy-btn-fg); }
  .tool-btn:hover { color: var(--vy-ink); background: var(--vy-fill); }
  .tab:hover { color: var(--vy-ink); }
  .tile:hover .tile-title { text-decoration: underline; text-decoration-color: var(--vy-accent); text-underline-offset: 3px; }
  .more-btn:hover, .owner-btn:hover { background: var(--vy-paper-2); }
  .menu-item:hover { background: var(--vy-paper-2); }
  .result:hover { background: var(--vy-paper-2); }
  .reco-act:hover { border-color: var(--vy-ink); }
  .seg-btn:hover { color: var(--vy-ink); }
  button.chip:hover { background: var(--vy-fill-2); color: var(--vy-ink); }
  button.chip.is-on:hover { background: var(--vy-ink); color: var(--vy-bg); }
  .buy-link:hover { border-color: var(--vy-ink); }
  .star:hover { color: var(--vy-ink); }
  .link:hover, .link-btn:hover { text-decoration-thickness: 2.5px; }
  .ess-open:hover .ess-title { text-decoration: underline; text-decoration-color: var(--vy-accent); text-underline-offset: 3px; }
  .auth-tab:hover { color: var(--vy-ink); }
  .crop-tools .btn:hover, .scan-card .btn:hover { background: #1c1c1a; border-color: rgba(255, 255, 255, .4); }
  .thread-btn:hover, .msg-album:hover { background: var(--vy-paper-2); }
  .feed-open:hover .tile-cover { outline: 2px solid var(--vy-accent); outline-offset: 2px; }
  .msg-send:hover { background: var(--vy-btn-bg); color: var(--vy-btn-fg); }
  .ord-grip:hover, .ord-move:hover:not(:disabled) { background: var(--vy-paper-2); color: var(--vy-ink); }
  .ord-main:hover .ord-title { text-decoration: underline; text-decoration-color: var(--vy-accent); text-underline-offset: 3px; }
}

/* Ceinture : sur un appareil sans pointeur fin, aucun état de survol ne peut
   se figer, même si une règle plus haut venait à en réintroduire un. */
@media (hover: none) {
  .btn:hover { border-color: transparent; background: var(--vy-fill); }
  .btn-main:hover { background: var(--vy-btn-bg); border-color: var(--vy-btn-bg); transform: none; box-shadow: none; opacity: 1; }
  .btn-ghost:hover { border-color: transparent; color: var(--vy-accent-texte); background: transparent; }
  .btn-danger:hover { border-color: transparent; }
  .icon-btn:hover { background: transparent; }
  .top-btn[aria-expanded="true"]:hover { background: var(--vy-paper-2); }
  .top-btn-main:hover { background: var(--vy-btn-bg); color: var(--vy-btn-fg); }
  .tool-btn:hover { color: var(--vy-ink-2); background: transparent; }
  .tool-btn.is-on:hover { color: var(--vy-ink); }
  .tab:hover { color: var(--vy-muted); }
  .tab.is-on:hover { color: var(--vy-ink); }
  .tile:hover .tile-title { text-decoration: none; }
  .more-btn:hover, .owner-btn:hover, .menu-item:hover, .result:hover { background: transparent; }
  .reco-act:hover { border-color: var(--vy-line-strong); }
  .seg-btn:hover { color: var(--vy-ink-2); }
  .seg-btn.is-on:hover, .seg-btn[aria-checked="true"]:hover { color: var(--vy-ink); }
  button.chip:hover { background: var(--vy-fill); color: var(--vy-ink-2); }
  button.chip.is-on:hover { background: var(--vy-ink); color: var(--vy-bg); }
  .buy-link:hover { border-color: var(--vy-line-strong); }
  .star:hover { color: var(--vy-muted-2); }
  .star.is-on:hover { color: var(--vy-ink); }
  .link:hover, .link-btn:hover { text-decoration-thickness: 1.5px; }
  .ess-open:hover .ess-title { text-decoration: none; }
  .auth-tab:hover { color: var(--vy-muted); }
  .auth-tab.is-on:hover { color: var(--vy-ink); }
  .crop-tools .btn:hover, .scan-card .btn:hover { background: transparent; border-color: rgba(255, 255, 255, .22); }
  .photo-piste:hover { border-color: var(--vy-line-strong); background: var(--vy-surface-2); }
  /* A2 */
  .field-dictate:hover { background: transparent; }
  .field-dictate.is-on:hover { background: var(--vy-paper-2); }
  .thread-btn:hover { background: transparent; }
  .msg-album:hover { background: var(--vy-bg); }
  .feed-open:hover .tile-cover { outline: none; }
  .msg-send:hover { background: var(--vy-btn-bg); color: var(--vy-btn-fg); }
  .ord-grip:hover, .ord-move:hover { background: transparent; color: var(--vy-muted); }
  .ord-main:hover .ord-title { text-decoration: none; }
}

/* ── 5d · Le nuage de pochettes ────────────────── */
/* La collection en nid d'abeilles façon écran d'accueil de montre (cf.
   nuage.js) : des disques ronds, les plus proches du centre en grand, le
   glissé, le zoom, l'accroche. Le script pose les disques ; la feuille ne
   fait que dessiner un vinyle vu de dessus (sillons, trou) et poser les
   outils. Trois jetons propres, déclarés dans les deux thèmes : la lueur
   sous le disque central, son liseré, et le papier des sillons. */
:root {
  --vy-nuage-lueur: 0 24px 60px -18px rgba(0, 0, 0, .45);
  --vy-nuage-bord: 0 0 0 1px rgba(0, 0, 0, .06);
  --vy-nuage-sillon: rgba(0, 0, 0, .18);
}
:root[data-theme="dark"] {
  --vy-nuage-lueur: 0 24px 60px -18px rgba(0, 0, 0, .9);
  --vy-nuage-bord: 0 0 0 1px rgba(255, 255, 255, .1);
  --vy-nuage-sillon: rgba(255, 255, 255, .14);
}
.nuage {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 320px;
  margin: 0 -16px;
  overflow: hidden;
  /* Le nuage ISOLE son empilement : les disques portent des z-index
     jusqu'à 2000 (le central) et, sans ce contexte, ils battaient la fiche
     d'album (`.sheet`, 60) et toute modale du site. Mesuré sur la capture
     d'Antoine : les pochettes peintes PAR-DESSUS la fiche ouverte. */
  isolation: isolate;
  z-index: 0;
}
.nuage[hidden] { display: none; }
.nuage-stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  touch-action: none;
  overscroll-behavior: contain;
  cursor: grab;
  outline: none;
  -webkit-user-select: none;
  user-select: none;
}
.nuage-stage:focus-visible { box-shadow: inset 0 0 0 2px var(--vy-accent); }
.nuage-stage.is-drag { cursor: grabbing; }
.nuage-scene { position: absolute; inset: 0; }
/* Un léger vignettage : le nuage se fond dans le papier vers les bords. */
.nuage-halo {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 55%, var(--vy-bg) 100%);
  opacity: .9;
}
.nuage-none {
  position: absolute;
  left: 16px;
  right: 16px;
  top: 50%;
  transform: translateY(-50%);
  text-align: center;
  font-size: calc(14px * var(--vy-k));
  color: var(--vy-muted);
}
.nuage-none[hidden] { display: none; }

/* Un disque : la pochette dans un cercle, un liseré de sillons, le trou. */
.nuage-disc {
  position: absolute;
  left: 0;
  top: 0;
  border-radius: 50%;
  overflow: hidden;
  background: var(--vy-paper-2);
  box-shadow: 0 0 0 1px var(--vy-line-strong);
  transform-origin: 50% 50%;
  will-change: transform, opacity;
  backface-visibility: hidden;
  cursor: pointer;
  -webkit-touch-callout: none;
}
.nuage-disc .nuage-cover {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  box-shadow: none;
}
.nuage-disc .nuage-cover img { width: 100%; height: 100%; object-fit: cover; }
/* Les sillons : un anneau de fines lignes concentriques sur le pourtour. */
.nuage-disc::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  background: repeating-radial-gradient(circle at 50% 50%, transparent 0 3px, var(--vy-nuage-sillon) 3px 4px);
  -webkit-mask: radial-gradient(circle at 50% 50%, transparent 0 74%, #000 76% 100%);
  mask: radial-gradient(circle at 50% 50%, transparent 0 74%, #000 76% 100%);
  opacity: .55;
}
.nuage-hole {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 9%;
  height: 9%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: var(--vy-bg);
  box-shadow: 0 0 0 2px rgba(0, 0, 0, .28), inset 0 0 0 1px rgba(255, 255, 255, .5);
  pointer-events: none;
}
/* L'anneau du disque central : un calque dont seule l'opacité s'anime (une
   ombre animée repeint à chaque image, leçon du dépôt). Il apparaît TOUT DE
   SUITE sur le nouveau central et ne s'efface que sur l'ancien, en 0,16 s :
   avec un fondu de 0,3 s dans les deux sens, le sélecteur arrivait 0,3 s
   après le disque et traînait des anneaux fantômes derrière un glissé
   (mesuré : trois anneaux à 0,2, 0,35 et 0,1 d'opacité à la fois, aucun
   disque n'en portant un plein pendant que les flèches répétaient). La
   durée est posée sur l'état de départ (la transition qui joue est celle
   de l'état d'ARRIVÉE) : `is-center` arrive sans délai, repartir est doux. */
.nuage-disc::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  box-shadow: inset 0 0 0 2px var(--vy-accent);
  opacity: 0;
  transition: opacity .16s ease-out;
}
.nuage-disc.is-center::after { opacity: 1; transition: none; }
/* Au pointeur fin, un disque survolé annonce qu'il se touche : le même
   anneau que le central, à demi. Rien au doigt, qui ne survole pas. */
@media (hover: hover) and (pointer: fine) {
  .nuage-disc:hover::after { opacity: .55; }
}
/* Le disque central passe devant les autres. Sa LUEUR n'est plus son
   `box-shadow` (posé à la classe, donc repeint avec un changement de taille de
   couche à chaque changement de centre) : c'est `.nuage-lueur`, un calque
   unique que nuage.js déplace sous lui. Il ne garde que son liseré. */
.nuage-disc.is-center { box-shadow: var(--vy-nuage-bord); z-index: 2000 !important; }
.nuage-lueur {
  position: absolute;
  left: 0;
  top: 0;
  border-radius: 50%;
  box-shadow: var(--vy-nuage-lueur);
  transform-origin: 50% 50%;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
  z-index: 1999;
}

/* Le pied : la légende du disque central et les outils. */
.nuage-foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px 12px;
  padding: 8px 16px calc(6px + env(safe-area-inset-bottom));
  border-top: 1px solid var(--vy-line);
  background: var(--vy-bg);
}
.nuage-caption { display: grid; min-width: 0; }
.nuage-artist, .nuage-title {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nuage-artist { font-family: var(--vy-titre); font-size: calc(18px * var(--vy-k)); letter-spacing: -0.02em; }
.nuage-title { font-size: calc(13.5px * var(--vy-k)); color: var(--vy-muted); }
.nuage-count { font-family: var(--vy-ui); font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }
.nuage-tools { display: flex; gap: 2px; }
.nuage-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--vy-r-1);
  background: transparent;
  color: var(--vy-ink-2);
  cursor: pointer;
}
.nuage-btn .ico { width: 20px; height: 20px; }
.nuage-btn:focus-visible { box-shadow: 0 0 0 2px var(--vy-accent); outline: none; }
@media (hover: hover) and (pointer: fine) {
  .nuage-btn:hover { background: var(--vy-paper-2); color: var(--vy-ink); }
}
@media (min-width: 900px) {
  .nuage { margin: 0 -28px; }
  .nuage-foot { padding-left: 28px; padding-right: 28px; }
  .nuage-artist { font-size: 22px; }
}
@media (prefers-reduced-motion: reduce) {
  .nuage-disc::after { transition: none; }
}

/* ── 5c · La bibliothèque par genre ────────────────── */
/* Les étagères d'une bibliothèque : une planche par rayon (un genre du
   canon), les pochettes posées dessus en légère perspective, la tranche
   visible. Le regroupement vit dans bibliotheque.js ; ici, le bois est un
   dégradé (aucune image binaire), l'ombre est portée sur la planche, et les
   disques glissent sur l'étagère à l'arrivée, sauf sous « moins de
   mouvement ». Sur téléphone chaque rayon se fait glisser à l'horizontale
   (scroll-snap) ; à partir de 900px il s'enroule. */
.biblio {
  --bib-bois-1: #d9b98c;
  --bib-bois-2: #b8925f;
  --bib-bois-3: #8a6a42;
  --bib-tranche: rgba(0, 0, 0, .34);
  --bib-ombre: rgba(0, 0, 0, .28);
  --i: 0;
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 4px 0 24px;
  /* Enfant d'une grille (`.view`), donc `min-width: auto` : sans ce zéro le
     conteneur prenait la largeur de la rangée entière (624 px mesurés sur
     un écran de 390) au lieu de la laisser défiler. Le piège de
     `.ec-link-box code` et des « Pages libres » du Cahier, une fois de plus. */
  min-width: 0;
  max-width: 100%;
}
:root[data-theme="dark"] .biblio {
  --bib-bois-1: #5a4631;
  --bib-bois-2: #3f3123;
  --bib-bois-3: #241b13;
  --bib-tranche: rgba(0, 0, 0, .6);
  --bib-ombre: rgba(0, 0, 0, .7);
}
.biblio[hidden] { display: none; }

/* ── « Ranger un disque » : le chercheur, en tête de la bibliothèque ─────
   Même carte que le panneau des réglages, mais posée AVANT les étagères et
   repliée : c'est le geste de tous les jours (trouver un disque et lui
   donner un rayon), là où les réglages se posent une fois. Replié, il coûte
   une ligne ; un outil qu'il faut aller chercher trois mille pixels plus
   bas n'est pas un outil. */
.biblio-find {
  margin: 0 0 18px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
  overflow: hidden;
}
.biblio-find[hidden] { display: none; }
.biblio-find-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 48px;
  padding: 10px 14px;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  text-align: left;
  cursor: pointer;
}
.biblio-find-btn .ico { flex: none; width: 18px; height: 18px; color: var(--vy-muted); }
.biblio-find-lab { flex: none; font: 600 calc(14px * var(--vy-k))/1.2 var(--vy-ui); }
.biblio-find-hint {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font: calc(12.5px * var(--vy-k))/1.3 var(--vy-ui);
  color: var(--vy-muted);
}
.biblio-find-chev { transition: transform var(--vy-t) ease; }
.biblio-find.is-open .biblio-find-chev { transform: rotate(180deg); }
.biblio-find-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 4px 14px 14px;
  border-top: 1px solid var(--vy-line);
}
.biblio-find-body[hidden] { display: none; }
.biblio-find-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
  border: 1px solid var(--vy-line-strong);
  border-radius: 999px;
  background: var(--vy-bg);
}
.biblio-find-bar .ico { flex: none; width: 17px; height: 17px; color: var(--vy-muted); }
.biblio-find-bar:focus-within { border-color: var(--vy-accent); }
.biblio-find-input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 44px;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  font: calc(15px * var(--vy-k))/1.2 var(--vy-ui);
}
.biblio-find-input:focus { outline: none; }
.biblio-find-list { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
.biblio-found {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px;
  border-radius: var(--vy-r-1);
  background: var(--vy-paper-2);
}
.biblio-found-cover { flex: none; width: 40px; }
.biblio-found-cover .tile-cover { box-shadow: none; border-radius: 4px; }
.biblio-found-txt { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.biblio-found-title {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 600 calc(13.5px * var(--vy-k))/1.3 var(--vy-ui); color: var(--vy-ink);
}
.biblio-found-artist {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: calc(12px * var(--vy-k))/1.3 var(--vy-ui); color: var(--vy-muted-3);
}
.biblio-found-sel {
  flex: none;
  max-width: 44%;
  min-height: 40px;
  padding: 6px 10px;
  border: 1px solid var(--vy-line-strong);
  border-radius: var(--vy-r-1);
  background: var(--vy-surface);
  color: var(--vy-ink);
  font: 500 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
.biblio-find-none { margin: 0; font: calc(13px * var(--vy-k))/1.5 var(--vy-ui); color: var(--vy-muted-3); }
.biblio-find-none[hidden] { display: none; }
/* Aucun champ sous 16 px au téléphone (iOS zoome au premier tap). Le bloc
   est ICI et pas dans la section « téléphone » de la feuille : celle-ci est
   déclarée plus haut, et à spécificité égale c'est la dernière règle qui
   gagne, donc l'override y restait inerte (mesuré : 15 px et 13 px). */
@media (max-width: 760px) {
  .biblio-find-input, .biblio-found-sel { font-size: 16px; }
  /* Le choix reprend toute la largeur sous la ligne : à 16 px, « Rangement
     par genre » ne tient pas dans les 44 % qui restent à côté du titre. */
  .biblio-found { flex-wrap: wrap; }
  .biblio-found-sel { flex: 1 0 100%; max-width: none; min-height: 44px; }
}

/* Le panneau des réglages : SOUS les étagères, replié par défaut. Il
   ouvrait la vue et prenait 382 px sur un écran de 844 : on entrait dans
   une bibliothèque sans y voir un disque. */
.biblio-panel {
  margin-top: 26px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
  overflow: hidden;
}
.biblio-panel-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 48px;
  padding: 10px 14px;
  border: 0;
  background: transparent;
  color: var(--vy-ink);
  text-align: left;
  cursor: pointer;
}
.biblio-panel-btn .ico { flex: none; width: 18px; height: 18px; color: var(--vy-muted); }
.biblio-panel-lab { flex: none; font: 600 calc(14px * var(--vy-k))/1.2 var(--vy-ui); }
.biblio-panel-state {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font: calc(12.5px * var(--vy-k))/1.3 var(--vy-ui);
  color: var(--vy-muted);
}
.biblio-panel-chev { transition: transform var(--vy-t) ease; }
.biblio-panel.is-open .biblio-panel-chev { transform: rotate(180deg); }
.biblio-panel-body {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 4px 14px 16px;
  border-top: 1px solid var(--vy-line);
}
.biblio-panel-body[hidden] { display: none; }
/* Comment ranger : trois réponses écrites, jamais un nombre à décoder. */
.biblio-modes { display: flex; flex-wrap: wrap; gap: 6px; padding-top: 12px; }
.biblio-mode {
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--vy-fill);
  color: var(--vy-ink);
  font: 500 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
/* Le choix retenu prend l'encre, comme une puce active : les deux jetons
   d'un aplat de la maison, jamais une couleur d'accent qui porterait du
   texte (elle change de clarté avec le thème). */
.biblio-mode.is-on {
  border-color: var(--vy-ink);
  background: var(--vy-ink);
  color: var(--vy-bg);
}
.biblio-step { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.biblio-step[hidden] { display: none; }
.biblio-step-lab { font: 600 calc(14px * var(--vy-k))/1.2 var(--vy-ui); color: var(--vy-ink); }
.biblio-step-grp {
  display: flex;
  align-items: center;
  gap: 2px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--vy-fill);
}
.biblio-step-btn {
  width: 44px;
  min-height: 44px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--vy-ink);
  font: 600 calc(18px * var(--vy-k))/1 var(--vy-ui);
  cursor: pointer;
}
.biblio-step-btn:disabled { color: var(--vy-muted-2); cursor: default; }
.biblio-step-out { min-width: 32px; text-align: center; font: 600 16px/1 var(--vy-ui); color: var(--vy-ink); }
.biblio-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.biblio-chips[hidden] { display: none; }
/* Mes rayons : les renommer et les supprimer là où on les lit. */
.biblio-persos { display: flex; flex-direction: column; gap: 8px; }
.biblio-persos-title { margin: 0; font: 600 calc(13px * var(--vy-k))/1.2 var(--vy-ui); color: var(--vy-ink); }
.biblio-persos-list { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
.biblio-persos-none { margin: 0; font: calc(12.5px * var(--vy-k))/1.45 var(--vy-ui); color: var(--vy-muted); }
.biblio-perso {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 4px 4px 4px 10px;
  border: 1px solid var(--vy-line);
  border-radius: var(--vy-r-1);
}
.biblio-perso-nom { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 calc(14px * var(--vy-k))/1.3 var(--vy-ui); color: var(--vy-ink); }
.biblio-perso-n { flex: none; font: calc(12px * var(--vy-k))/1.2 var(--vy-ui); color: var(--vy-muted); }
.biblio-perso-act {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  min-height: 40px;
  border: 0;
  border-radius: var(--vy-r-1);
  background: transparent;
  color: var(--vy-muted);
  cursor: pointer;
}
.biblio-perso-act .ico { width: 16px; height: 16px; }
.biblio-perso-edit { display: flex; flex-wrap: wrap; gap: 6px; width: 100%; margin: 0; }
.biblio-perso-input { flex: 1 1 140px; }
/* « Nouveau rayon » : un bouton, un champ qui se déplie, l'aide du glissé. */
.biblio-new { display: flex; flex-direction: column; gap: 6px; margin: 0; }
.biblio-new-btn {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  align-self: flex-start;
  padding: 0 14px;
  border: 1px dashed var(--vy-line-strong);
  border-radius: 999px;
  background: transparent;
  color: var(--vy-ink);
  font: 600 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
.biblio-new-btn .ico { width: 16px; height: 16px; }
.biblio-new-btn:hover { border-color: var(--vy-ink); }
.biblio-new-btn[hidden] { display: none; }
.biblio-new-row { display: flex; flex-wrap: wrap; gap: 6px; }
.biblio-new-row[hidden] { display: none; }
.biblio-new-input {
  flex: 1 1 200px;
  min-width: 0;
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid var(--vy-line-strong);
  border-radius: var(--vy-r-1);
  background: var(--vy-bg);
  color: var(--vy-ink);
  font: 16px/1.3 var(--vy-ui);
}
.biblio-new-input:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: -1px; }
.biblio-new-ok, .biblio-new-cancel {
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid var(--vy-line-strong);
  border-radius: var(--vy-r-1);
  background: var(--vy-btn-bg);
  color: var(--vy-btn-fg);
  font: 600 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
.biblio-new-cancel { background: transparent; color: var(--vy-ink); }
.biblio-new-err { margin: 0; font: calc(13px * var(--vy-k))/1.4 var(--vy-ui); color: var(--vy-rouge-texte); }
.biblio-new-err[hidden] { display: none; }
.biblio-help { margin: 0; font: calc(12.5px * var(--vy-k))/1.45 var(--vy-ui); color: var(--vy-muted); }
.biblio-chip { display: inline-flex; align-items: center; gap: 6px; }
.biblio-chip-n {
  font: 500 calc(12px * var(--vy-k))/1 var(--vy-ui);
  padding: 2px 6px;
  border-radius: 999px;
  background: var(--vy-surface-3);
  color: var(--vy-muted);
}
.biblio-chip.is-on .biblio-chip-n { background: rgba(255, 255, 255, .18); color: inherit; }
.biblio-chip-auto { font-style: normal; }

.biblio-shelves { display: flex; flex-direction: column; gap: 22px; }
.shelf-empty { margin: 0; font: calc(14px * var(--vy-k))/1.5 var(--vy-ui); color: var(--vy-muted); }

/* Une étagère : le titre du rayon (un bouton quand il pose le filtre), le
   compte, la raison du rayon, la rangée des disques, la planche. */
.shelf { display: flex; flex-direction: column; min-width: 0; }
.shelf-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  margin: 0 0 8px;
}
.shelf-title {
  margin: 0;
  font: 600 calc(17px * var(--vy-k))/1.2 var(--vy-titre);
  color: var(--vy-ink);
  letter-spacing: -.01em;
}
.shelf-title-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 4px 0 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: var(--vy-line-strong);
  text-underline-offset: 4px;
}
.shelf-title-btn .ico { width: 15px; height: 15px; color: var(--vy-muted-2); }
.shelf-count { font: 500 calc(12px * var(--vy-k))/1.2 var(--vy-ui); color: var(--vy-muted); }
/* Un rayon créé à la main : son titre porte l'accent, sa tête la corbeille ;
   son étagère est toujours là, même vide, avec l'invitation à déposer. */
.shelf.is-perso .shelf-title { color: var(--vy-accent-texte); }
/* Les deux actions d'un rayon à soi, groupées : `margin-left: auto` sur
   l'enveloppe, jamais sur chaque bouton. Posée sur deux frères, la marge
   automatique se PARTAGE l'espace libre, et le crayon se retrouvait au
   milieu de la tête (mesuré à 870 px), la corbeille tout au bout. */
.shelf-acts { display: flex; align-items: center; gap: 2px; margin-left: auto; }
.shelf-del {
  display: inline-grid;
  place-items: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--vy-muted);
  cursor: pointer;
}
.shelf-del .ico { width: 16px; height: 16px; }
.shelf-del:hover, .shelf-del:focus-visible { color: var(--vy-rouge-texte); background: var(--vy-paper-2); outline: none; }
.shelf-drop {
  align-self: center;
  margin: 0;
  padding: 26px 16px;
  border: 1px dashed var(--vy-line-strong);
  border-radius: var(--vy-r-1);
  font: calc(13px * var(--vy-k))/1.4 var(--vy-ui);
  color: var(--vy-muted);
}
/* Le glissé : l'étagère visée s'éclaire, la pochette tenue s'efface à
   moitié, son fantôme suit le pointeur au-dessus de tout (hors de la
   scène, dans le corps : le rail rogne ce qui dépasse de lui). */
.shelf.is-drop-target { outline: 2px solid var(--vy-accent); outline-offset: 4px; border-radius: var(--vy-r-1); }
.shelf.is-drop-target .shelf-board { background: var(--vy-accent); opacity: .35; }
.shelf-disc.is-dragging { opacity: .35; }
.biblio.is-dragging .shelf-row { scroll-snap-type: none; }
.biblio.is-dragging .shelf-disc { animation: none; }
.shelf-ghost {
  position: fixed;
  left: 0;
  top: 0;
  width: 104px;
  z-index: 90;
  pointer-events: none;
  filter: drop-shadow(0 18px 24px rgba(0, 0, 0, .35));
  will-change: transform;
}
.shelf-ghost .tile-cover { box-shadow: none; }

/* La barre de dépôt : elle n'existe que pendant un glissé, et elle est ce qui
   rend le geste possible (cf. `bibliotheque.js`). Posée au-dessus de la barre
   d'onglets (40) et sous le fantôme (90), elle amène tous les rayons sous le
   pointeur quelle que soit la hauteur de la page. */
.shelf-dropbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 80;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
  background: var(--vy-bg);
  border-top: 1px solid var(--vy-line-strong);
  box-shadow: 0 -10px 30px -18px rgba(0, 0, 0, .55);
}
.shelf-dropbar-lab {
  font: 600 calc(12px * var(--vy-k))/1 var(--vy-ui);
  color: var(--vy-muted);
}
/* Les pastilles s'ENROULENT au lieu de défiler : une rangée qui défile
   cacherait les derniers rayons, et on ne peut pas la faire défiler tout en
   tenant une pochette. C'est le défaut même que la barre corrige, il ne doit
   pas revenir à l'intérieur d'elle. Le plafond de hauteur n'est qu'une
   ceinture pour une collection à quarante rayons. */
.shelf-dropbar-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  max-height: 40vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-bottom: 2px;
}
.dropchip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 16px;
  border: 1px dashed var(--vy-line-strong);
  border-radius: 999px;
  background: var(--vy-paper);
  color: var(--vy-ink);
  font: 600 calc(14px * var(--vy-k))/1.2 var(--vy-ui);
  white-space: nowrap;
}
.dropchip.is-perso { border-style: solid; border-color: var(--vy-accent-texte); color: var(--vy-accent-texte); }
.dropchip.is-drop-target {
  background: var(--vy-accent-plein);
  border-color: var(--vy-accent-plein);
  border-style: solid;
  color: var(--vy-on-accent);
}
/* La pastille « Admin » du portfolio flotte en bas à gauche au z-index
   maximal : sans cela elle se pose sur la première pastille de la barre,
   c'est-à-dire sur la cible la plus facile à viser (même parade que
   `has-modal` et `has-opt-bar`). */
body.has-dropbar #mn-admin-pill { display: none; }
.shelf-badge {
  position: absolute;
  right: 4px;
  bottom: 4px;
  max-width: calc(100% - 8px);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--vy-accent-plein);
  color: var(--vy-on-accent);
  font: 600 calc(11.5px * var(--vy-k))/16px var(--vy-ui);
}
/* Dans la fiche : le badge et le choix du rayon, à côté des genres. */
.album-rayon { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 10px 0 0; }
.album-rayon[hidden] { display: none; }
.album-rayon-lab { display: inline-flex; align-items: center; gap: 5px; font: 600 calc(12px * var(--vy-k))/1.2 var(--vy-ui); color: var(--vy-muted); }
.album-rayon-lab .ico { width: 14px; height: 14px; }
.album-rayon-sel {
  min-height: 40px;
  padding: 6px 10px;
  border: 1px solid var(--vy-line-strong);
  border-radius: var(--vy-r-1);
  background: var(--vy-bg);
  color: var(--vy-ink);
  font: 500 calc(14px * var(--vy-k))/1.2 var(--vy-ui);
}
.chip.chip-rayon { background: var(--vy-accent-plein); color: var(--vy-on-accent); border-color: transparent; }
.chip.chip-rayon[hidden] { display: none; }
/* `--vy-muted-3` et pas `--vy-muted-2` : mesuré sur le fond d'étagère, le
   second tombe à 4,3:1 en thème sombre (4,54 en clair, à la limite). Ces
   deux lignes portent l'explication du rangement, elles doivent se lire. */
.shelf-why { font: calc(12px * var(--vy-k))/1.3 var(--vy-ui); color: var(--vy-muted-3); }

/* La rangée : sur téléphone un rail qui se fait glisser, cran par cran ;
   le padding bas laisse la place à l'ombre portée sur la planche. */
.shelf-row {
  display: flex;
  gap: 14px;
  align-items: flex-end;
  padding: 10px 4px 12px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: thin;
  perspective: 900px;
  -webkit-overflow-scrolling: touch;
}
.shelf-row:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 2px; border-radius: var(--vy-r-1); }
/* L'enveloppe d'une pochette : elle porte la place dans la rangée, la
   pochette porte le dessin. Elle existe parce que `.shelf-disc` est un
   `<button>` et qu'un bouton ne s'imbrique pas dans un bouton : le bandeau
   « Ranger » doit être son FRÈRE. */
.shelf-item {
  position: relative;
  flex: 0 0 auto;
  scroll-snap-align: start;
}
.shelf-disc {
  position: relative;
  flex: 0 0 auto;
  width: 124px;
  /* Un appui long sur une image ouvre la loupe de sélection d'iOS et le menu
     contextuel d'Android : le navigateur annule alors le pointeur, et le
     glissé au doigt mourait avant de commencer. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  border-radius: var(--vy-r-0);
  animation: shelf-glisse 420ms cubic-bezier(.2, .8, .2, 1) both;
  animation-delay: calc(var(--i) * 28ms);
}
.shelf-disc:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 3px; }
/* La pochette est tournée d'un rien vers le lecteur, sa tranche à gauche ;
   l'ombre tombe sur la planche, décalée du côté opposé à la lumière. */
.shelf-disc-3d {
  display: block;
  transform-style: preserve-3d;
  transform: rotateY(-14deg);
  transition: transform var(--vy-t-2) ease;
}
.shelf-disc-3d .tile-cover {
  box-shadow: 8px 10px 14px -6px var(--bib-ombre), var(--vy-ombre-pochette);
  backface-visibility: hidden;
}
.shelf-disc-3d::before {
  content: "";
  position: absolute;
  top: 1px;
  bottom: 1px;
  left: 0;
  width: 7px;
  background: linear-gradient(90deg, var(--bib-tranche), rgba(0, 0, 0, .06));
  transform: rotateY(-90deg) translateZ(3px) translateX(-3px);
  transform-origin: left center;
  border-radius: 2px 0 0 2px;
}
.shelf-note {
  position: absolute;
  right: -4px;
  top: -6px;
  display: inline-grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border-radius: 999px;
  color: var(--vy-btn-fg);
  background: var(--vy-btn-bg);
  box-shadow: var(--vy-ombre-pochette);
}
.shelf-note .ico { width: 13px; height: 13px; }
/* Le bandeau « Ranger » : le chemin d'une souris vers le rayon d'un disque,
   sans rien glisser. Transparent au repos, il ne paraît qu'au survol ou au
   focus et seulement sur un écran à souris (cf. le bloc « survol » en fin de
   feuille) : au doigt, l'appui long soulève déjà le disque. Transparent et
   non `display: none` : le clavier l'atteint, et un assistant de navigateur
   le lit. */
.shelf-move {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 2;
  display: none;
  align-items: center;
  justify-content: center;
  gap: 6px;
  /* 38 px : le plancher des micro-actions du dépôt (tests/test_vinyles_ux_2026_09.py
     le mesure à 1 280 px) ; à 30 px le bandeau passait sous la barre. */
  min-height: 38px;
  padding: 0 8px;
  border: 0;
  border-radius: 0 0 var(--vy-r-0) var(--vy-r-0);
  background: var(--vy-btn-bg);
  color: var(--vy-btn-fg);
  font: 600 calc(12px * var(--vy-k))/1 var(--vy-ui);
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--vy-t) ease;
}
.shelf-move .ico { width: 14px; height: 14px; }
.shelf-move-lab { font-weight: 600; }

/* Le menu « Ranger dans », posé sur le corps du document en position fixe :
   la rangée d'une étagère défile à l'horizontale sous 900 px, un menu posé
   dedans y serait coupé net. */
.rmenu {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 60;
  min-width: 232px;
  max-height: min(60vh, 420px);
  overflow-y: auto;
  overscroll-behavior: contain;
  transform: none;
}
.rmenu-item { gap: 12px; }
.rmenu-lab { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rmenu-check { flex: none; opacity: 0; }
.rmenu-item.is-on { color: var(--vy-accent-texte); font-weight: 600; }
.rmenu-item.is-on .ico { color: inherit; }
.rmenu-item.is-on .rmenu-check { opacity: 1; }
.rmenu-item:focus-visible { background: var(--vy-paper-2); outline-offset: -2px; }
.rmenu-none { color: var(--vy-muted); }

.shelf-more {
  flex: 0 0 auto;
  align-self: center;
  min-height: 44px;
  padding: 0 14px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--vy-fill);
  color: var(--vy-ink);
  font: 500 calc(14px * var(--vy-k))/1 var(--vy-ui);
  cursor: pointer;
  scroll-snap-align: end;
}
/* La planche : trois tons de bois en dégradé, un chant plus sombre dessous. */
.shelf-board {
  height: 12px;
  margin: -6px 0 0;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--bib-bois-1) 0%, var(--bib-bois-2) 55%, var(--bib-bois-3) 100%);
  box-shadow: 0 6px 10px -4px var(--bib-ombre), inset 0 1px 0 rgba(255, 255, 255, .25);
}

@keyframes shelf-glisse {
  from { opacity: 0; transform: translateX(46px); }
  to { opacity: 1; transform: none; }
}
/* « Moins de mouvement » (la préférence de l'appareil, ou la classe que le
   script pose quand il la lit) : les disques sont déjà posés. */
@media (prefers-reduced-motion: reduce) {
  .shelf-disc { animation: none; }
  .shelf-disc-3d { transition: none; }
}
.biblio.is-still .shelf-disc { animation: none; }
.biblio.is-still .shelf-disc-3d { transition: none; }
.biblio.is-live { min-height: 120px; }

@media (min-width: 900px) {
  .shelf-row { flex-wrap: wrap; overflow: visible; scroll-snap-type: none; row-gap: 26px; }
  .shelf-disc { width: 140px; }
}
@media (max-width: 560px) {
  .shelf-disc { width: 112px; }
}
@media (hover: hover) and (pointer: fine) {
  /* `display` ici, `opacity` au survol : posé en `display: none`, le bandeau
     « Ranger » sortirait du parcours clavier et de ce qu'un assistant de
     navigateur sait lire ; posé visible, ce serait une pastille permanente
     sur chacune des cent trente pochettes d'une page d'étagères. */
  .shelf-move { display: flex; }
  .shelf-item:hover .shelf-move,
  .shelf-item:focus-within .shelf-move { opacity: 1; }
  .shelf-move:hover { background: var(--vy-accent-plein); color: var(--vy-on-accent); }
  .shelf-disc:hover .shelf-disc-3d { transform: rotateY(-4deg) translateY(-6px); }
  .shelf-title-btn:hover { text-decoration-color: var(--vy-ink); }
  .shelf-more:hover { border-color: var(--vy-ink); }
  .biblio.is-still .shelf-disc:hover .shelf-disc-3d { transform: rotateY(-14deg); }
}
@media (hover: none) {
  .shelf-title-btn:hover { text-decoration-color: var(--vy-line-strong); }
  .shelf-more:hover { border-color: var(--vy-line-strong); }
}

/* ┌──────────────────────────────────────────────────────────────────────────
   │ 6 · ANIMATIONS ET MATIÈRE (septembre 2026, anim.js)
   │ Le vocabulaire commun (durées, courbes) que anim.js relit, les
   │ animations que le script déclenche par une classe (le squelette d'une
   │ pochette, le reflet du badge des 200, le « + » qui respire, le fond de
   │ l'accueil), le disque qui sort de la pochette, et les micro-interactions
   │ au pointeur fin. Tout sur `transform` et `opacity` (jamais une ombre
   │ animée : c'est l'opacité d'un calque qui bouge) ; tout coupé sous
   │ « moins de mouvement » (le bloc de la section 13 met déjà toute
   │ animation CSS à 0,01 ms, et anim.js n'en lance aucune en WAAPI).
   └────────────────────────────────────────────────────────────────────────── */
/* ── 6 · Animations et matière ────────────────── */

:root {
  --vy-dur-xs: 160ms;
  --vy-dur-s: 260ms;
  --vy-dur-m: 420ms;
  --vy-dur-l: 700ms;
  --vy-ease-out: cubic-bezier(.2, .7, .2, 1);
  --vy-ease-in-out: cubic-bezier(.65, 0, .35, 1);
  /* La matière : le reflet du squelette et du badge, l'encre des sillons
     du fond de l'accueil, la teinte d'étiquette du disque par défaut. */
  --vy-reflet: rgba(255, 255, 255, .55);
  --vy-fond-encre: rgba(10, 10, 10, .09);
  --vy-disc-teinte: var(--vy-accent);
  --vy-disc-corps: #141413;
  --vy-disc-sillon: rgba(255, 255, 255, .07);
}
:root[data-theme="dark"] {
  --vy-reflet: rgba(255, 255, 255, .10);
  --vy-fond-encre: rgba(250, 250, 249, .07);
  --vy-disc-teinte: var(--vy-accent);
  --vy-disc-corps: #0a0a0a;
  --vy-disc-sillon: rgba(255, 255, 255, .09);
}

/* anim.js pose `vy-anim` sur <html> au démarrage : les entrées CSS de la
   section 13 (feuille, modale, toast) cèdent la place au ressort WAAPI,
   sinon les deux se composeraient. Sans le script, la section 13 reste. */
html.vy-anim .sheet,
html.vy-anim .panel,
html.vy-anim .modal-card,
html.vy-anim .toast { animation: none; }

/* Le voile d'une modale floute ce qu'il couvre : la carte se lit sur un
   fond calme, et la profondeur se voit. Pas sur les deux outils plein
   écran (recadrage, scanner), qui sont opaques. */
.modal:not(.modal-crop):not(.modal-scan) {
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

/* ── Le squelette d'une pochette qui charge ──────────────────────────── */
/* `.is-loading` : l'image n'est pas encore là, un miroitement passe sur
   le papier de la boîte. `.is-loaded` : l'image arrive en fondu. Posées
   par anim.js ; sans lui, une pochette s'affiche comme avant. */
.tile-cover.is-loading img,
.album-cover.is-loading img { opacity: 0; }
.tile-cover.is-loading::after,
.album-cover.is-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 30%, var(--vy-reflet) 50%, transparent 70%);
  background-size: 220% 100%;
  animation: vy-miroite 1.3s var(--vy-ease-in-out) infinite;
}
@keyframes vy-miroite {
  from { background-position: 120% 0; }
  to   { background-position: -120% 0; }
}
.tile-cover.is-loaded img,
.album-cover.is-loaded img { animation: vy-fondu-img var(--vy-dur-m) var(--vy-ease-out); }
@keyframes vy-fondu-img {
  from { opacity: 0; transform: scale(1.03); }
  to   { opacity: 1; transform: scale(1); }
}

/* ── Le reflet du badge des 200 ───────────────────────────────────────── */
.badge { position: relative; overflow: hidden; }
.badge.vy-shine::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 35%, var(--vy-reflet) 50%, transparent 65%);
  background-size: 260% 100%;
  animation: vy-miroite var(--vy-dur-l) var(--vy-ease-in-out) 1;
  pointer-events: none;
}

/* ── Le « + » qui respire tant que la collection est vide ────────────── */
/* Un anneau (pseudo-élément) qui s'écarte et s'efface, six fois puis se
   tait : une invitation, pas un signal d'alarme. `position: relative` est
   déjà posé par `.top-btn`. */
.top-btn-main.vy-breathe::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid var(--vy-ink);
  opacity: 0;
  animation: vy-respire 2.2s var(--vy-ease-out) 6;
  pointer-events: none;
}
@keyframes vy-respire {
  0%   { transform: scale(1); opacity: .55; }
  70%  { transform: scale(1.55); opacity: 0; }
  100% { transform: scale(1.55); opacity: 0; }
}

/* ── Le disque qui sort de la pochette (la fiche, à l'écoute) ────────── */
/* Posé par anim.js DANS le bouton de la pochette, derrière elle. Les
   sillons sont un dégradé radial répété, l'étiquette la teinte du disque,
   le trou la couleur du papier. Il ne vit que le temps du toast. */
.album-cover-btn { position: relative; }
.album-cover-btn .album-cover { position: relative; z-index: 1; }
.vy-disc {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.vy-disc-rot {
  position: absolute;
  inset: 1.5%;
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 50%,
      var(--vy-bg) 0 4.5%,
      var(--vy-disc-teinte) 4.6% 22%,
      var(--vy-disc-corps) 22.5% 24%,
      transparent 24.1%),
    repeating-radial-gradient(circle at 50% 50%,
      var(--vy-disc-corps) 0 1.6%,
      var(--vy-disc-sillon) 1.6% 2%);
  box-shadow: var(--vy-ombre-hero);
  will-change: transform;
}
/* Un reflet fixe sur le disque, qui ne tourne pas avec lui : c'est la
   lumière de la pièce, pas un dessin sur le vinyle. */
.vy-disc::before {
  content: "";
  position: absolute;
  inset: 1.5%;
  border-radius: 50%;
  background: linear-gradient(150deg, rgba(255, 255, 255, .16) 0 30%, transparent 30.5% 100%);
  pointer-events: none;
}
.vy-tonearm {
  position: absolute;
  top: -3%;
  right: 3%;
  width: 58%;
  height: 58%;
  color: var(--vy-ink);
  fill: none;
  stroke: currentColor;
  stroke-width: .8;
  stroke-linecap: round;
  stroke-linejoin: round;
  transform-origin: 77% 21%;
  transform: rotate(-34deg);
  filter: drop-shadow(0 2px 2px rgba(0, 0, 0, .25));
}

/* ── Le fond vivant de l'accueil ──────────────────────────────────────── */
/* Trois disques à l'encre des sillons, derrière la vitrine. `.landing`
   isole son empilement : `z-index: -1` les met sous le contenu et non
   sous la page. La parallaxe est écrite par anim.js dans `--vy-px` /
   `--vy-py` (de -1 à 1), chaque disque la suit selon sa profondeur. */
.landing { position: relative; isolation: isolate; }
.vy-fond {
  --vy-px: 0;
  --vy-py: 0;
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  color: var(--vy-fond-encre);
}
.vy-fond-boite {
  position: absolute;
  display: block;
  transform: translate(calc(var(--vy-px) * var(--vy-prof, 1) * -8px), calc(var(--vy-py) * var(--vy-prof, 1) * -8px));
  transition: transform var(--vy-dur-l) var(--vy-ease-out);
}
.vy-fond-a { top: -140px; right: -160px; }
.vy-fond-b { top: 46%; left: -120px; }
.vy-fond-c { bottom: 8%; right: 12%; }
.vy-fond-disc { display: block; animation: vy-tourne-lent 70s linear infinite; }
.vy-fond-b .vy-fond-disc { animation-duration: 95s; animation-direction: reverse; }
.vy-fond-c .vy-fond-disc { animation-duration: 55s; }
@keyframes vy-tourne-lent {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* ── Les gestes : l'appui ─────────────────────────────────────────────── */
/* Un appui se sent sous le doigt : la surface se resserre d'un rien. Les
   transitions de transform sont posées ici pour les porteuses qui n'en
   avaient pas (`.tile-cover`, `.icon-btn`, `.chip`, `.tab .ico`). */
.tile-cover,
.icon-btn,
.chip,
.hub-card,
.tab .ico { transition-property: transform, color, background, border-color; transition-duration: var(--vy-dur-xs); transition-timing-function: var(--vy-ease-out); }
.tile-cover img { transition: transform var(--vy-dur-m) var(--vy-ease-out); }
.btn:active,
.icon-btn:active,
button.chip:active,
.hub-card:active,
.tile:active .tile-cover { transform: scale(.97); }
.tab:active .ico { transform: scale(.88); }

/* ── Les gestes : le survol, au pointeur fin seulement ────────────────── */
@media (hover: hover) and (pointer: fine) {
  .tile:hover .tile-cover { transform: translateY(-3px); box-shadow: var(--vy-ombre-hero); }
  .tile:hover .tile-cover img { transform: scale(1.035); }
  .btn:hover:not([disabled]) { transform: translateY(-1px); }
  .btn-main:hover:not([disabled]) { transform: translateY(-1px); }
  .icon-btn:hover { transform: translateY(-1px); }
  .top-btn-main:hover { transform: translateY(-1px) scale(1.04); }
  .hub-card:hover { transform: translateY(-2px); }
  button.chip:hover { transform: translateY(-1px); }
  .tab:hover .ico { transform: translateY(-1px); }
  /* L'appui garde le dernier mot sur le survol. */
  .btn:active, .icon-btn:active, button.chip:active, .hub-card:active { transform: scale(.97); }
  .tile:active .tile-cover { transform: translateY(-1px) scale(.98); }
}

@media (prefers-reduced-motion: reduce) {
  .vy-fond-disc { animation: none; }
  .vy-fond-boite { transition: none; transform: none; }
  .top-btn-main.vy-breathe::after { display: none; }
  .tile-cover.is-loading::after { animation: none; opacity: .6; }
  .tile:hover .tile-cover, .tile:hover .tile-cover img, .btn:hover, .icon-btn:hover,
  .hub-card:hover, button.chip:hover, .tab:hover .ico { transform: none; }
}

/* ── 5e · Le menu déroulant de la vue ────────────────── */
/* Un <select> natif : la feuille de choix du téléphone est la meilleure
   liste déroulante qui soit, et elle ne zoome pas (16 px au doigt). Le
   chevron est le nôtre (l'apparence native est retirée), posé DANS l'étiquette
   pour que le tap le traverse jusqu'au champ. */
.tool-select {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  border-radius: 999px;
  color: var(--vy-ink-2);
}
.tool-select select {
  appearance: none;
  -webkit-appearance: none;
  min-height: 44px;
  padding: 6px 26px 6px 10px;
  border: 0;
  border-radius: var(--vy-r-1);
  background: transparent;
  color: inherit;
  font: 500 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
/* Le contrôle de vue : l'icône DIT la vue affichée, le menu natif est posé
   transparent par-dessus. Un seul objet de 44 px là où il y en avait trois
   (le nom de la vue, un chevron, un bouton de cycle). Le `select` garde sa
   taille de police pour qu'iOS n'ait aucune raison de zoomer. */
.tool-view { width: 44px; justify-content: center; }
.tool-view .tool-view-ico,
.tool-select.tool-view > .tool-view-ico { position: static; width: 20px; height: 20px; color: var(--vy-ink-2); transform: none; }
.tool-view select {
  position: absolute;
  inset: 0;
  width: 100%;
  padding: 0;
  font-size: 16px;
  opacity: 0;
}
.tool-view:has(select:focus-visible) { outline: 2px solid var(--vy-accent); outline-offset: -2px; }
/* Le menu maison qui remplace la liste du navigateur sur un écran à souris
   (cf. `ouvrirMenuVue` dans app.js). Il reprend le vocabulaire de `.menu` :
   même carte, même rayon, mêmes lignes de 44 px. Ancré à DROITE du contrôle,
   qui vit en bout de barre d'outils : aligné à gauche, une liste de 210 px
   sortirait de l'écran. */
.vmenu {
  top: calc(100% + 8px);
  right: 0;
  left: auto;
  z-index: 40;
  min-width: 212px;
  transform: none;
}
.vmenu-item { gap: 12px; }
.vmenu-lab { flex: 1 1 auto; }
.vmenu-check { flex: none; opacity: 0; }
.vmenu-item.is-on { color: var(--vy-accent-texte); font-weight: 600; }
.vmenu-item.is-on .ico { color: inherit; }
.vmenu-item.is-on .vmenu-check { opacity: 1; }
.vmenu-item:focus-visible { background: var(--vy-paper-2); outline-offset: -2px; }
/* Les listes des Paramètres (cf. `armerChampMenu` dans app.js) : le `select`
   reste dans le champ, sans occuper d'espace ni recevoir un geste ; le bouton
   qui dit la valeur prend son aspect de champ (mêmes règles que `.field
   select`, nommées plus haut) et la carte s'ouvre à pleine largeur dessous. */
.field-menu { position: relative; display: block; }
.field-menu select { position: absolute; inset: 0; opacity: 0; pointer-events: none; }
.field .field-menu-btn { display: block; text-align: left; cursor: pointer; }
.field-menu-val { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fmenu { left: 0; right: 0; min-width: 0; max-width: none; }
.tool-select > .ico {
  position: absolute; right: 6px; top: 50%;
  width: 16px; height: 16px;
  transform: translateY(-50%);
  pointer-events: none;
}
.tool-select select:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: -2px; }
@media (hover: hover) and (pointer: fine) {
  .tool-select:hover { color: var(--vy-ink); }
}
@media (max-width: 480px) {
  .album-rayon-sel { font-size: 16px; }
  /* 16 px (iOS ne zoome pas), et une largeur bornée : « Bibliothèque » à
     côté de quatre boutons et du compteur débordait de 39 px à 390 (mesuré).
     Le libellé du tri (« Artiste ») se replie dès ici et plus seulement à
     380 px : le menu de la vue prend sa place, et il dit plus que lui. */
  .tool-select select {
    font-size: 16px; padding: 4px 20px 4px 6px; max-width: 96px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .tool-select > .ico { right: 2px; width: 14px; height: 14px; }
  .toolbar .tool-lab { display: none; }
  .toolbar .tool-btn { padding: 6px 6px; }
}
@media (max-width: 360px) {
  /* 320 px (iPhone SE de première génération) : la barre faisait 352 px,
     mesuré. Le menu se resserre (« Grille » entier, « Bibliothèque » en
     ellipse) et les boutons d'icône perdent deux pixels de chaque côté. */
  .tool-select select { max-width: 54px; padding: 4px 4px; }
  /* Le CHEVRON s'efface, pas l'icône qui DIT la vue affichée. Elles
     partagent la classe `.ico` et le même parent : la règle attrapait donc
     les deux, et sur un iPhone SE le contrôle de vue n'était plus qu'une
     zone de 44 px sans rien dedans (défaut préexistant, vu sur une capture
     à 320 px). */
  .tool-select > .ico:not(.tool-view-ico) { display: none; }
  .toolbar { gap: 4px; }
  .toolbar .tool-btn { padding: 6px 4px; }
}

/* ── 5f · Le menu du tri ────────────────── */
/* Le bouton de tri ouvre la même carte que le choix de vue (`.vmenu`), au
   doigt comme à la souris (cf. `ouvrirMenuTri` dans app.js). Son enveloppe
   porte la carte : un bouton ne peut pas contenir les boutons du menu.
   Elle pose un `display`, donc elle porte sa parade `[hidden]` (le piège du
   dépôt : une règle d'auteur bat l'attribut du navigateur). */
.tool-sort { position: relative; display: inline-flex; }
.tool-sort[hidden] { display: none; }
/* Libellé replié (sous 480 px), le bouton n'est plus qu'une icône : il garde
   la cible tactile de 44 px au lieu de fondre à 32. */
.tool-sort .tool-btn { min-width: 44px; justify-content: center; }
.tool-sort .tool-btn[aria-expanded="true"] { color: var(--vy-ink); background: var(--vy-fill-2); }
/* Deux lignes par tri : le nom, puis le sens du rangement, plus discret.
   La carte ne dépasse jamais l'écran en largeur ; en hauteur, le script la
   borne à la place qui reste sous le bouton et elle défile en elle-même
   (un défilement de la PAGE la refermerait). */
.vmenu { overflow-y: auto; overscroll-behavior: contain; }
.tmenu { min-width: 272px; max-width: calc(100vw - 16px); }
.vmenu-nom, .vmenu-aide { display: block; }
.vmenu-aide {
  margin-top: 1px;
  color: var(--vy-muted);
  font: 400 calc(12px * var(--vy-k))/1.3 var(--vy-ui);
}

/* ── 5f bis · Deux boutons, comme dans Photos (octobre 2026) ──────────────
   Demande d'Antoine, capture de la Photothèque d'iOS à l'appui : la capsule
   de quatre outils devient DEUX boutons, « Filtrer » et « ••• ». Le « ••• »
   est l'ancien bouton du tri (même identifiant, même enveloppe) : son menu
   porte la vue en tuiles, « Au hasard », puis le tri. Rien n'est retiré du
   DOM : le `select` de la vue reste la source (une tuile lui pose sa valeur
   et lui envoie `change`), réduit à un point invisible, hors de la
   tabulation ; le bouton du tirage reste celui que « Au hasard » presse. */
.toolbar .toolbar-tools { position: relative; }
.toolbar [id$="Filter"] { order: 1; }
.toolbar .tool-sort { order: 2; }
.toolbar .tool-lab { display: none; }
.toolbar .tool-view {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 1px;
  height: 1px;
  min-height: 0;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}
.toolbar .toolbar-tools > [id$="Random"] { display: none; }
/* Le menu : une carte de verre aux grands rayons, comme les menus d'iOS. */
.tmenu {
  border-color: transparent;
  border-radius: var(--vy-r-3);
  background-color: var(--vy-verre-fond, var(--vy-card));
  -webkit-backdrop-filter: blur(24px) saturate(1.8);
  backdrop-filter: blur(24px) saturate(1.8);
  box-shadow: var(--vy-verre-ombre, 0 12px 32px rgba(0, 0, 0, .2));
  padding: 8px;
}
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .tmenu { background-color: var(--vy-card); }
}
.vmenu-vues {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
  padding: 2px 0 8px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--vy-line);
}
.vmenu-vue {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: 4px;
  min-height: 60px;
  padding: 8px 4px;
  border: 0;
  border-radius: 14px;
  background: var(--vy-fill);
  color: var(--vy-ink);
  font: 500 calc(12px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
.vmenu-vue .ico { width: 22px; height: 22px; }
.vmenu-vue-lab { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vmenu-vue.is-on { background: var(--vy-ink); color: var(--vy-bg); }
.vmenu-vue:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 2px; }
.vmenu-hasard, .vmenu-filtrer { margin-bottom: 2px; }
.vmenu-titre {
  margin: 8px 12px 2px;
  color: var(--vy-muted);
  font: 600 calc(12px * var(--vy-k))/1.3 var(--vy-ui);
}
@media (hover: hover) and (pointer: fine) {
  .vmenu-vue:hover { background: var(--vy-fill-2); }
  .vmenu-vue.is-on:hover { background: var(--vy-ink); }
}

/* ── 7 · Pour vous, le moteur ────────────────── */
/* Les parts d'un score (« 63 % soul · 37 % années 1970 ») : une rangée de
   petites pastilles sous la raison, la plus forte teintée d'accent doux.
   Les deux retours d'une carte (« Pas mon genre », « Plus comme ça ») sont
   des icônes à côté de « Masquer », 44 px chacune. « Encore » est un bouton
   plein, centré, sous les rails. Aucun jeton neuf : tout lit --vy-*. */
.reco-parts { display: flex; flex-wrap: wrap; gap: 4px; min-height: 18px; }
.reco-part {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--vy-surface-2);
  color: var(--vy-muted);
  font: 500 calc(11px * var(--vy-k))/1.4 var(--vy-ui);
  white-space: nowrap;
}
.reco-part-main { background: var(--vy-accent-doux); color: var(--vy-accent-texte); }
.reco-part b { font-weight: 700; }
.reco-act[aria-pressed="true"] { border-color: var(--vy-accent); color: var(--vy-accent-texte); }

/* La carte épurée (octobre 2026) : le titre, puis « artiste · année » (la
   ligne de liens), puis la raison sur UNE ligne de parts quand le moteur en
   donne (la phrase qui disait la même chose reste dans le document), et une
   seule rangée de trois boutons-icônes, sans cadre. Avant : le nom de
   l'artiste trois fois, la raison deux fois, trois boutons encadrés sur
   deux rangées (mesuré : 290 px de carte sous la pochette). */
.reco-card .tile-title { margin-top: 2px; }
/* Au doigt, les liens portent 12 px de marge tactile au-dessus et
   au-dessous (règle des liens de navigation) : la ligne remonte d'autant,
   sinon elle flotte à 30 px sous le titre. */
.reco-card .reco-links { margin-top: -3px; }
@media (pointer: coarse) {
  .reco-card .reco-links { margin-top: -12px; margin-bottom: -8px; }
}
.reco-card.has-parts .reco-reason { display: none; }
.reco-card.has-parts .reco-parts {
  flex-wrap: nowrap;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, #000 82%, transparent);
  mask-image: linear-gradient(to right, #000 82%, transparent);
}
.reco-acts.is-trio {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 6px;
  margin-top: 4px;
}
/* `position: relative` : le libellé masqué (absolu) reste DANS son bouton.
   Sans ancre, il prenait la page pour bloc conteneur, sortait du rail qui
   défile et élargissait la page à 1 553 px (mesuré : au téléphone, la
   barre d'onglets partait au bas d'une page dézoomée). */
.reco-acts.is-trio .reco-act {
  position: relative;
  min-width: 0;
  padding: 0;
  border: 0;
  background: var(--vy-paper-2);
  color: var(--vy-ink-2);
}
.reco-acts.is-trio .reco-act .ico { width: 18px; height: 18px; }
.reco-acts.is-trio .reco-act[aria-pressed="true"] { background: var(--vy-accent-doux); color: var(--vy-accent-texte); }
.reco-acts.is-trio .reco-act-lib {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.reco-acts.is-trio .reco-act-wish { color: var(--vy-ink); }
.reco-card.is-fil { position: relative; }
.reco-maillon {
  position: absolute; top: 6px; left: 6px; z-index: 2;
  min-width: 22px; height: 22px; padding: 0 6px;
  display: inline-grid; place-items: center;
  border-radius: 999px;
  background: var(--vy-ink); color: var(--vy-bg);
  font: 700 calc(12px * var(--vy-k))/1 var(--vy-ui);
  pointer-events: none;
}
.reco-encore {
  display: grid; justify-items: center; gap: 8px;
  padding: 14px 0 6px;
  text-align: center;
}
.reco-encore-text { max-width: 44ch; font-size: calc(13px * var(--vy-k)); color: var(--vy-muted); }
.reco-encore .btn { min-height: 44px; }
.reco-encore .btn .ico { width: 16px; height: 16px; margin-right: 6px; }
.reco-encore[data-fin="1"] .btn { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .reco-card.is-nouvelle { animation: vy-reco-in var(--vy-dur-m, .28s) var(--vy-ease-out, ease-out) both; }
  @keyframes vy-reco-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
}

/* ── 8 · Les anecdotes ────────────────── */
/* « Le saviez-vous ? » : jusqu'à trois cartes de fait, chacune posée sur le
   papier de la fiche avec sa section et sa source (un lien sortant vers
   Wikipédia), une note qui dit d'où ça vient (et « résumé par l'IA » quand
   c'est le cas), un squelette le temps de la lecture, et le bouton
   « D'autres anecdotes » à 44 px. Aucun jeton neuf : tout lit --vy-*. Les
   deux animations (sortie, arrivée) ne vivent que sous « moins de mouvement :
   aucune préférence » : le remplacement est sinon immédiat. */
.facts-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.fact {
  display: grid; gap: 6px;
  padding: 12px 14px;
  border: 1px solid transparent;
  border-left: 3px solid var(--vy-accent);
  border-radius: 12px;
  background: var(--vy-groupe);
  min-width: 0;
}
.fact-text { margin: 0; font-size: calc(14.5px * var(--vy-k)); line-height: 1.55; color: var(--vy-ink); overflow-wrap: anywhere; }
.fact-meta {
  margin: 0;
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
  font: 500 calc(12px * var(--vy-k))/1.4 var(--vy-ui);
  color: var(--vy-muted);
}
.fact-section { font-family: var(--vy-ui); font-size: calc(12px * var(--vy-k)); color: var(--vy-muted-3); }
.fact-src {
  display: inline-flex; align-items: center; gap: 3px;
  min-height: 24px;
  color: var(--vy-accent-texte);
  text-decoration: underline; text-underline-offset: 2px;
}
.fact-src .ico { width: 13px; height: 13px; }
.facts-note { margin: 0; font-size: calc(13px * var(--vy-k)); line-height: 1.5; color: var(--vy-muted); }
.facts-note.is-fin { color: var(--vy-accent-texte); }
/* La note d'un bloc VIDE (pas d'article, réseau absent) : en italique, pour
   qu'elle se lise comme une explication et non comme une anecdote. */
.facts-note.is-empty { font-style: italic; }
.facts-more { min-height: 44px; justify-self: start; }
.facts-more .ico { width: 16px; height: 16px; margin-right: 6px; }
/* Le squelette : trois cartes de barres, un miroitement dessus. */
.fact.is-skeleton { border-left-color: var(--vy-line); gap: 8px; }
.fact-bone { display: block; height: 11px; border-radius: 6px; background: var(--vy-surface-3); }
.fact-bone-l { width: 92%; } .fact-bone-m { width: 74%; } .fact-bone-s { width: 38%; }
@media (prefers-reduced-motion: no-preference) {
  .fact.is-skeleton .fact-bone {
    background: linear-gradient(100deg, var(--vy-surface-3) 30%, var(--vy-reflet) 50%, var(--vy-surface-3) 70%);
    background-size: 220% 100%;
    animation: vy-miroite 1.3s var(--vy-ease-in-out) infinite;
  }
  .facts-list.is-out .fact { animation: vy-fact-out var(--vy-dur-s) var(--vy-ease-in-out) both; }
  .facts-list.is-in .fact { animation: vy-fact-in var(--vy-dur-m) var(--vy-ease-out) both; }
  .facts-list.is-in .fact:nth-child(2) { animation-delay: 60ms; }
  .facts-list.is-in .fact:nth-child(3) { animation-delay: 120ms; }
  @keyframes vy-fact-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateX(-10px); } }
  @keyframes vy-fact-in { from { opacity: 0; transform: translateX(12px); } to { opacity: 1; transform: none; } }
}

/* ── 9 · La page d'un artiste ──────────────────────────────────────────
   Deux onglets marqués juste sous l'en-tête, comme le demandait Antoine :
   « En ligne » d'abord (c'est ce qu'on n'a pas), « Ma collection » ensuite.
   La barre est collante : sur une discographie de trente disques, changer
   d'onglet ne doit pas demander de remonter. */
/* Les onglets vivent ENTRE l'en-tête et le corps, pas dedans : c'est le
   corps qui défile, eux restent. `flex: none` parce que le panneau est une
   colonne flex et qu'un enfant sans consigne se laisse comprimer. */
.art-tabs {
  flex: none;
  display: flex;
  gap: 2px;
  padding: 0 14px;
  border-bottom: 1px solid var(--vy-line);
  background: var(--vy-bg);
}
.art-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 46px;
  padding: 10px 12px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--vy-muted);
  font: 500 calc(14px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
.art-tab .ico { width: 16px; height: 16px; }
.art-tab.is-on { border-bottom-color: var(--vy-accent); color: var(--vy-ink); font-weight: 600; }
.art-tab-n { font: calc(12px * var(--vy-k))/1 var(--vy-ui); color: var(--vy-muted); }
.art-tab.is-on .art-tab-n { color: var(--vy-accent-texte); }
.art-note { margin: 0 0 10px; font: calc(13px * var(--vy-k))/1.5 var(--vy-ui); color: var(--vy-muted); }
/* Ce qu'on possède déjà, marqué sur la ligne du résultat : c'est cette
   pastille qu'on parcourt pour voir ce qui manque. */
.result-owned {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 3px;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--vy-paper-2);
  color: var(--vy-ink);
  font: 500 calc(11.5px * var(--vy-k))/1.5 var(--vy-ui);
}
.result-owned .ico { width: 13px; height: 13px; color: var(--vy-vert); }
.result-owned.is-wish .ico { color: var(--vy-accent-texte); }
/* Les puces des deux premières sections de « Pour vous » : une icône, un
   nom, un compte en pastille. Une rangée de pilules grises toutes pareilles
   se lisait mot à mot ; celle-ci se lit d'un coup d'œil. */
.chip-hub { gap: 6px; padding-right: 6px; }
.chip-hub .ico { width: 15px; height: 15px; color: var(--vy-muted); }
.chip-hub.is-on .ico { color: inherit; }
.chip-hub-lab { font-weight: 500; }
.chip-hub-n {
  display: inline-grid;
  place-items: center;
  min-width: 22px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--vy-fill);
  font: 600 calc(12px * var(--vy-k))/1.5 var(--vy-ui);
  color: var(--vy-muted);
}
/* La discographie arrive en cascade, une ligne après l'autre. */
@media (prefers-reduced-motion: no-preference) {
  #artResults.is-in > li {
    animation: vy-art-ligne 320ms cubic-bezier(.16, .84, .3, 1) both;
    animation-delay: calc(var(--i, 0) * 26ms);
  }
  @keyframes vy-art-ligne {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
  }
}


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 16 · PARAMÈTRES (septembre 2026) : police, accent, taille du texte,
   │      noir profond, animations, légendes et pastilles des pochettes
   │ Tout est accroché à un attribut de <html> que `theme-init.js` pose avant
   │ le premier rendu, et SEULEMENT quand la valeur n'est pas celle d'usine :
   │ sans réglage, aucune règle de cette section ne s'applique.
   └────────────────────────────────────────────────────────────────────────── */

/* ── 16.1 · Les paires de polices ────────────────────────────────────────────
   Chaque paire se déclare pour l'app (`:root[data-police]`) ET pour sa carte
   d'aperçu dans le panneau (`[data-apercu-police]`) : un jeton posé sur un
   élément l'emporte sur celui qu'il hérite, donc la carte « Éditorial »
   s'écrit en Playfair même quand l'app est en « Livre ». Une seule liste de
   familles par paire, jamais recopiée. Les binaires : /static/fonts.css
   (Azur Tech, Machine à écrire) et /static/vinyles-fonts.css (les autres). */
[data-apercu-police="azur"] {
  --vy-titre: 'Space Grotesk', 'Inter Tight', system-ui, sans-serif;
  --vy-ui: 'Inter Tight', system-ui, -apple-system, 'Segoe UI', sans-serif;
}
:root[data-police="systeme"],
[data-apercu-police="systeme"] {
  --vy-titre: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --vy-ui: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
:root[data-police="editorial"],
[data-apercu-police="editorial"] {
  --vy-titre: 'Playfair Display', Georgia, 'Times New Roman', serif;
  --vy-ui: 'Source Sans 3', 'Inter Tight', system-ui, sans-serif;
}
:root[data-police="livre"],
[data-apercu-police="livre"] {
  --vy-titre: 'Petrona', Georgia, 'Times New Roman', serif;
  --vy-ui: 'Petrona', Georgia, 'Times New Roman', serif;
}
:root[data-police="arrondie"],
[data-apercu-police="arrondie"] {
  --vy-titre: 'Nunito', 'Inter Tight', system-ui, sans-serif;
  --vy-ui: 'Nunito', 'Inter Tight', system-ui, sans-serif;
}
:root[data-police="geometrique"],
[data-apercu-police="geometrique"] {
  --vy-titre: 'Poppins', 'Inter Tight', system-ui, sans-serif;
  --vy-ui: 'Lato', 'Inter Tight', system-ui, sans-serif;
}
:root[data-police="machine"],
[data-apercu-police="machine"] {
  --vy-titre: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  --vy-ui: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
}
/* Une chasse fixe est trop large pour les cinq onglets du bas (cf. `.tab`). */
:root[data-police="machine"] { --vy-ui-nav: 'Inter Tight', system-ui, -apple-system, 'Segoe UI', sans-serif; }

/* ── 16.2 · Les couleurs d'accent ────────────────────────────────────────────
   Trois rôles comme l'azur (section 1) : `--vy-accent` pour un tracé (3:1 au
   moins sur le papier), `--vy-accent-texte` quand il porte des lettres
   (4,5:1 au moins sur le papier ET sur le fond teinté `--vy-accent-doux`),
   `--vy-accent-doux` pour un fond. Mesuré sur les valeurs AFFICHÉES (hors du
   gamut sRGB, une couleur est rognée par le navigateur : les hexadécimaux
   ci-dessous sont déjà rognés, pour que la mesure et l'écran disent la même
   chose). Clair, sur le papier : texte vert 5,2 · ambre 5,9 · rouge 6,2 ·
   violet 6,2 · rose 6,3 · encre 9,9. Sombre, l'accent sous de l'encre
   (`--vy-on-accent`) : 7 à 8:1. tests/test_vinyles_parametres.py le recalcule.

   Même règle qu'en 16.1 : la pastille d'une couleur (`.swatch[data-accent]`)
   porte ses propres jetons, et la version sombre gagne par spécificité
   (0,3,0 contre 0,2,0), pas par l'ordre. */
.swatch[data-accent="azur"] {
  --vy-accent: oklch(0.62 0.18 210);
  --vy-accent-texte: oklch(0.50 0.15 210);
  --vy-accent-doux: oklch(0.955 0.025 210);
}
:root[data-theme="dark"] .swatch[data-accent="azur"] {
  --vy-accent: oklch(0.72 0.15 210);
  --vy-accent-texte: oklch(0.72 0.15 210);
  --vy-accent-doux: oklch(0.26 0.04 210);
}
:root[data-accent="vert"], .swatch[data-accent="vert"] {
  --vy-accent: #00a452; --vy-accent-texte: #007a3a; --vy-accent-doux: #e4f5e9;
}
:root[data-theme="dark"][data-accent="vert"], :root[data-theme="dark"] .swatch[data-accent="vert"] {
  --vy-accent: #43c07a; --vy-accent-texte: #43c07a; --vy-accent-doux: #132a1b;
}
:root[data-accent="ambre"], .swatch[data-accent="ambre"] {
  --vy-accent: #c86c00; --vy-accent-texte: #974e00; --vy-accent-doux: #fbeedf;
}
:root[data-theme="dark"][data-accent="ambre"], :root[data-theme="dark"] .swatch[data-accent="ambre"] {
  --vy-accent: #df911a; --vy-accent-texte: #df911a; --vy-accent-doux: #30200d;
}
:root[data-accent="rouge"], .swatch[data-accent="rouge"] {
  --vy-accent: #de4e4b; --vy-accent-texte: #a83634; --vy-accent-doux: #ffeae8;
}
:root[data-theme="dark"][data-accent="rouge"], :root[data-theme="dark"] .swatch[data-accent="rouge"] {
  --vy-accent: #f47b74; --vy-accent-texte: #f47b74; --vy-accent-doux: #351c1a;
}
:root[data-accent="violet"], .swatch[data-accent="violet"] {
  --vy-accent: #9867e1; --vy-accent-texte: #724aab; --vy-accent-doux: #f2edff;
}
:root[data-theme="dark"][data-accent="violet"], :root[data-theme="dark"] .swatch[data-accent="violet"] {
  --vy-accent: #b48df4; --vy-accent-texte: #b48df4; --vy-accent-doux: #271f35;
}
:root[data-accent="rose"], .swatch[data-accent="rose"] {
  --vy-accent: #d14e95; --vy-accent-texte: #9e366f; --vy-accent-doux: #feeaf2;
}
:root[data-theme="dark"][data-accent="rose"], :root[data-theme="dark"] .swatch[data-accent="rose"] {
  --vy-accent: #e97ab2; --vy-accent-texte: #e97ab2; --vy-accent-doux: #331c27;
}
/* L'encre : aucune couleur, pour qui veut les pochettes seules en couleur. */
:root[data-accent="encre"], .swatch[data-accent="encre"] {
  --vy-accent: #737373; --vy-accent-texte: #404040; --vy-accent-doux: #efeeea;
}
:root[data-theme="dark"][data-accent="encre"], :root[data-theme="dark"] .swatch[data-accent="encre"] {
  --vy-accent: #c8c6c0; --vy-accent-texte: #c8c6c0; --vy-accent-doux: #2a2a27;
}

/* ── 16.3 · La taille du texte ───────────────────────────────────────────────
   Pas de taille « petite » : le plancher de 16 px des champs (iOS zoome sous
   ce seuil) et celui de 10 px des libellés ne se négocient pas. */
:root[data-texte="grand"] { --vy-k: 1.1; }
:root[data-texte="tres-grand"] { --vy-k: 1.2; }

/* ── 16.4 · Le noir profond (écrans OLED) ────────────────────────────────────
   En sombre seulement : le papier devient un noir pur, les surfaces restent
   un cran au-dessus pour qu'une carte se détache encore. Mesuré : muted-3 sur
   la carte 5,4:1, l'encre 17:1. La barre système suit (theme-init.js). */
:root[data-theme="dark"][data-noir="oui"] {
  --vy-bg: #000000;
  --vy-paper: #0a0a0a;
  --vy-paper-2: #171716;
  --vy-card: #0f0f0e;
  --vy-surface-3: #222220;
  --vy-line: rgba(255, 255, 255, .09);
  --vy-voile: rgba(0, 0, 0, .86);
}

/* ── 16.5 · Animations réduites ──────────────────────────────────────────────
   Le réglage vaut `prefers-reduced-motion: reduce` pour la feuille entière,
   même quand l'appareil ne le demande pas. Une durée quasi nulle plutôt que
   `none` : les écouteurs de `transitionend` / `animationend` reçoivent
   toujours leur évènement, et un état final tenu par `both` est bien atteint.
   Le JavaScript (ressorts, bac, nuage, disque) consulte le même réglage par
   `VinylPrefs.moinsDeMouvement()`. */
:root[data-mouvement="reduit"] { scroll-behavior: auto !important; }
:root[data-mouvement="reduit"] *,
:root[data-mouvement="reduit"] *::before,
:root[data-mouvement="reduit"] *::after {
  animation-duration: .01ms !important;
  animation-delay: 0s !important;
  animation-iteration-count: 1 !important;
  transition-duration: .01ms !important;
  transition-delay: 0s !important;
  scroll-behavior: auto !important;
}

/* ── 16.6 · Le mur de pochettes : légendes et pastilles ──────────────────────
   Sans légende, la grille n'est plus que des pochettes ; le nom reste lu par
   le lecteur d'écran (l'`aria-label` de la tuile). La liste garde les siennes :
   sans elles, une ligne ne dirait plus rien. Le numéro de la file d'achat
   n'est pas une pastille décorative, c'est l'ordre de la liste : il reste. */
:root[data-legendes="non"] .tiles:not(.is-list) .tile-artist,
:root[data-legendes="non"] .tiles:not(.is-list) .tile-title { display: none; }
:root[data-pastilles="non"] .tile-badge,
:root[data-pastilles="non"] .tile-rank,
:root[data-pastilles="non"] .shelf-badge { display: none; }

/* ── 16.7 · Le panneau Paramètres ─────────────────────────────────────────── */

/* L'aperçu en tête : il lit les jetons de l'app, donc il change à chaque
   geste, sous les yeux. Collé en haut du panneau sur téléphone : le panneau
   couvre l'app, c'est lui qui montre ce qu'on vient de régler. */
.set-apercu {
  position: sticky;
  top: -16px;
  z-index: 2;
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: 12px;
  align-items: center;
  margin: -16px -16px 0;
  padding: 14px 16px;
  border-bottom: 1px solid var(--vy-line);
  background: var(--vy-bg);
}
/* À partir de 720 px le panneau est une boîte centrée au corps plus large
   (20 px 22 px, cf. section 14) : l'aperçu suit, sinon son filet s'arrête
   avant les bords. */
@media (min-width: 720px) {
  .set-apercu { top: -20px; margin: -20px -22px 0; padding: 14px 22px; }
}
.set-apercu-cover {
  position: relative;
  width: 64px;
  height: 64px;
  border-radius: var(--vy-r-0);
  background:
    radial-gradient(circle at 50% 50%, var(--vy-bg) 0 5%, var(--vy-accent) 5.5% 18%, transparent 18.5%),
    repeating-radial-gradient(circle at 50% 50%, #151514 0 2px, #222220 2px 3px);
  box-shadow: var(--vy-ombre-pochette);
}
.set-apercu-txt { display: grid; gap: 3px; min-width: 0; }
.set-apercu-titre {
  font-family: var(--vy-titre);
  font-size: calc(18px * var(--vy-k));
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
  overflow-wrap: anywhere;
}
.set-apercu-corps { font-size: calc(14px * var(--vy-k)); color: var(--vy-muted); line-height: 1.4; }
.set-apercu-bas { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 2px; }
.set-apercu-lab {
  font-family: var(--vy-ui);
  font-size: calc(12px * var(--vy-k));
  color: var(--vy-muted-3);
}
.set-apercu-chip {
  display: inline-flex;
  align-items: center;
  min-height: 20px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--vy-accent-plein);
  color: var(--vy-on-accent);
  font: 600 calc(12px * var(--vy-k))/1 var(--vy-ui);
}

/* Les couleurs : une rangée de pastilles. La cible entière (pastille + nom)
   fait 64 px de haut. */
.swatches {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 6px;
}
/* Sept sur une ligne dès qu'une case garde 44 px de large (mesuré : 46 px à
   390 px de fenêtre). En dessous, quatre puis trois. */
@media (min-width: 380px) {
  .swatches { grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
}
.swatch {
  display: grid;
  justify-items: center;
  gap: 6px;
  min-height: 64px;
  padding: 8px 2px 6px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-1);
  background: transparent;
  color: var(--vy-muted);
  font: 500 calc(12px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
.swatch-lab { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.swatch-dot {
  width: 30px;
  height: 30px;
  border-radius: 999px;
  background: var(--vy-accent);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .12);
}
.swatch[aria-checked="true"] { border-color: var(--vy-line-strong); background: var(--vy-card); color: var(--vy-ink); font-weight: 600; }
.swatch[aria-checked="true"] .swatch-dot { box-shadow: 0 0 0 2px var(--vy-card), 0 0 0 4px var(--vy-accent); }
.swatch:focus-visible { outline-offset: 0; }

/* Les polices : deux cartes par ligne sur téléphone, chacune écrite dans sa
   propre paire (cf. 16.1). */
.polices {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.police-opt {
  display: grid;
  gap: 2px;
  align-content: start;
  min-height: 92px;
  padding: 10px 12px;
  border: 1px solid transparent;
  border-radius: var(--vy-r-1);
  background: var(--vy-groupe);
  color: var(--vy-ink);
  text-align: left;
  font-family: var(--vy-ui);
  cursor: pointer;
}
.police-aa {
  font-family: var(--vy-titre);
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
.police-nom { font-size: calc(14px * var(--vy-k)); font-weight: 600; line-height: 1.25; }
.police-ex { font-size: calc(12.5px * var(--vy-k)); color: var(--vy-muted); line-height: 1.3; overflow-wrap: anywhere; }
/* Sept paires sur deux colonnes : la dernière prend toute la ligne plutôt
   que de rester seule à gauche. */
.police-opt:last-child:nth-child(odd) { grid-column: 1 / -1; }
.police-opt[aria-checked="true"] {
  border-color: var(--vy-ink);
  box-shadow: inset 0 -3px 0 var(--vy-accent);
}

/* La taille : trois « A » de taille croissante, pour qu'on voie ce qu'on
   choisit avant de lire le mot. */
.seg-a { font-family: var(--vy-titre); font-weight: 600; line-height: 1; }
.seg-a-1 { font-size: calc(14px * var(--vy-k)); }
.seg-a-2 { font-size: calc(17px * var(--vy-k)); }
.seg-a-3 { font-size: calc(20px * var(--vy-k)); }

.set-reset { justify-self: start; }

@media (hover: hover) and (pointer: fine) {
  .swatch:hover { background: var(--vy-paper-2); color: var(--vy-ink); }
  .police-opt:hover { border-color: var(--vy-muted-2); }
}

/* ┌──────────────────────────────────────────────────────────────────────────
   │ 17 · LES EFFETS (septembre 2026) : vol.js, lumiere.js, vivant.js, flux.js
   │ Quatre modules posés par-dessus l'app, chacun avec SON bloc ci-dessous.
   │ Toujours dans la section 6 (qui court jusqu'à la fin du fichier) : ses
   │ verrous valent ici (aucune ombre animée, aucun :hover hors du bloc
   │ « pointeur fin », tout coupé sous « moins de mouvement »).
   └────────────────────────────────────────────────────────────────────────── */

/* ── 17.1 · La continuité : vol.js ──────────────────────────────────────── */
/* (bloc de vol.js) */
/* Les clones qui voyagent. Posés dans <body> par vol.js, en position fixe
   depuis le coin de l'écran : c'est le transform qui les place et les
   rétrécit (origine au coin haut gauche, la taille dessinée est celle du
   plus grand des deux rectangles). Aucun pointeur, aucun lecteur d'écran
   (aria-hidden posé par le script), et ils quittent le DOM à l'arrivée.
   Le plan (z-index) est posé par le script : au-dessus de la feuille, sous
   les modales ; au-dessus de la visionneuse pour la visionneuse. */
.vy-vol {
  position: fixed;
  top: 0;
  left: 0;
  margin: 0;
  max-width: none;
  display: block;
  object-fit: cover;
  transform-origin: 0 0;
  border-radius: var(--vy-r-0);
  background: var(--vy-paper-2);
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  will-change: transform, opacity;
  backface-visibility: hidden;
}
/* L'ombre est STATIQUE : elle voyage avec le clone, elle ne s'anime pas
   (section 6). Celle de la grande pochette, qu'il va remplacer. */
.vy-vol-fiche { box-shadow: var(--vy-ombre-hero); }
.vy-vol-vision { box-shadow: 0 0 0 1px rgba(255, 255, 255, .12); }

/* Le voile qui s'efface à la place de la visionneuse déjà fermée : même
   teinte qu'elle, pour que la fermeture se lise comme un fondu et non
   comme une coupure. */
.vy-vol-voile {
  position: fixed;
  inset: 0;
  background: rgba(10, 10, 10, .94);
  pointer-events: none;
  will-change: opacity;
}

/* Le souhait qui plonge vers son onglet : deux calques (l'horizontale sur
   l'un, la verticale en cloche sur l'autre), c'est ce qui dessine l'arc.
   La miniature est une copie de la boîte de la pochette, dont la classe a
   été remplacée : elle reprend ici ce que `.tile-cover` lui donnait. */
.vy-vol-arc {
  position: fixed;
  top: 0;
  left: 0;
  pointer-events: none;
  will-change: transform;
}
.vy-vol-mini {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border-radius: var(--vy-r-0);
  background: var(--vy-paper-2);
  box-shadow: var(--vy-ombre-pochette);
  transform-origin: 50% 50%;
  will-change: transform, opacity;
}
.vy-vol-mini img,
.vy-vol-mini .ph {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Ce que le vol cache le temps du trajet. `visibility` et non `display` :
   la place reste, le texte accessible aussi. La grande pochette (et l'image
   de la visionneuse) pendant que le clone s'y pose ; l'IMAGE de la pochette
   d'où la fiche est partie, tant que la fiche est ouverte : sa boîte garde
   son papier et son ombre, une pochette sortie de son bac. */
.album-cover.is-en-vol,
#coverViewerImg.is-en-vol { visibility: hidden; }
.album-cover.is-en-vol-vision { visibility: hidden; }
.is-source-vol > img,
.is-source-vol > .ph { visibility: hidden; }


/* ── 17.2 · La lumière : lumiere.js ─────────────────────────────────────── */
/* (bloc de lumiere.js) */
/* La matière : l'intensité de la lumière d'ambiance (plus forte sur le
   sombre, où une lumière colorée se lit comme une lueur ; plus retenue sur
   le papier, où elle se lirait comme une tache), la part du lavis de tête,
   et le reflet qui suit le pointeur sur la grande pochette. */
:root {
  --vy-lum-fort: .5;
  --vy-lum-lavis: .34;
  --vy-lum-reflet: rgba(255, 255, 255, .34);
}
:root[data-theme="dark"] {
  --vy-lum-fort: .56;
  --vy-lum-lavis: .42;
  --vy-lum-reflet: rgba(255, 255, 255, .18);
}

/* Le corps de la fiche isole son empilement : la lumière (z-index -1) passe
   DERRIÈRE tout ce qu'il porte (texte, boutons, pochette) sans descendre
   sous le papier de la feuille. Le héros est son repère. Écrit sur des
   CLASSES et jamais sur un identifiant : feuille.js fait descendre un clone
   de la fiche sans aucun `id`, et ce clone doit garder la mise en page de la
   fiche qu'on voyait (la carte qui part est celle qu'on regardait). La
   pochette de la fiche est la seule posée dans un `.album-cover-btn` : c'est
   ce qui la distingue de celle de la page d'un lien de partage. */
.sheet:has(.album-cover-btn) .sheet-body { isolation: isolate; }
.album-hero:has(> .album-cover-btn) { position: relative; }

/* ── La lumière d'ambiance ─────────────────────────────────────────────── */
/* Posée par lumiere.js d'après la pochette réelle (`--vy-lum-cw` : sa
   largeur, `--vy-lum-cy` : son centre dans le héros, `--vy-lum-haut` : le
   haut du corps qui défile, `--vy-lum-ext` : jusqu'aux bords du corps, pas
   au-delà). Les replis valent une pochette de 300 px, le temps de la
   première mesure. Le flou est PEINT dans le dégradé (une décroissance
   continue depuis le centre, amenée par un point d'inflexion, sans palier
   plein : une pochette plus petite ou décalée n'en découvre jamais le bord
   net) : jamais un `filter`. L'opacité du conteneur porte
   l'intensité et s'estompe quand le héros se replie. */
.vy-lum {
  position: absolute;
  top: calc(-1 * var(--vy-lum-haut, 16px));
  left: calc(-1 * var(--vy-lum-ext, 0px));
  right: calc(-1 * var(--vy-lum-ext, 0px));
  height: calc(var(--vy-lum-haut, 16px) + var(--vy-lum-cy, 154px) + var(--vy-lum-cw, 300px) * .92);
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  opacity: calc(var(--vy-lum-fort) * (1 - var(--vy-hero-p, 0) * .85));
  /* Le haut du corps est sous l'en-tête opaque de la feuille : la couleur y
     arrive en rampe sur 28 px, jamais en marche d'un pixel à l'autre. Un
     masque STATIQUE, qui vaut pour les deux calques et le lavis d'un coup. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 28px);
  mask-image: linear-gradient(to bottom, transparent 0, #000 28px);
}
/* Pendant le vol d'ouverture, la grande pochette est cachée (son clone vole
   ailleurs) : la lumière attend qu'elle soit posée, sans quoi son cœur se
   lirait comme une boule de couleur au milieu de la feuille. À l'atterrissage
   elle se fond, par une animation qui part de 0 et finit sur la valeur de la
   règle (aucune image clé `to`, donc rien de tenu à la fin, et le défilement
   continue d'écrire l'opacité sans retard : une `transition` l'aurait
   suivi avec 260 ms de retard). Le clone de la carte qui part n'arrive pas :
   il garde la lumière qu'on voyait. */
.album-hero:has(> .album-cover-btn > .album-cover.is-en-vol) .vy-lum {
  opacity: 0;
  animation: none;
}
.album-hero:has(> .album-cover-btn) .vy-lum { animation: vy-lum-arrive var(--vy-dur-s) var(--vy-ease-out); }
.vy-carte-clone .album-hero .vy-lum { animation: none; }
@keyframes vy-lum-arrive {
  from { opacity: 0; }
}
.vy-lum-calque {
  --vy-lum-c: var(--vy-accent);
  position: absolute;
  inset: 0;
  opacity: 0;
  background: radial-gradient(
    calc(var(--vy-lum-cw, 300px) * .98) calc(var(--vy-lum-cw, 300px) * .86)
      at 50% calc(var(--vy-lum-haut, 16px) + var(--vy-lum-cy, 154px)),
    var(--vy-lum-c) 0%, 38%, transparent 100%);
}
/* Le lavis de tête : la même couleur, en travers de toute la fiche, qui se
   perd à mi-hauteur de la pochette. */
.vy-lum-calque::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: var(--vy-lum-lavis);
  background: linear-gradient(to bottom,
    var(--vy-lum-c) 0,
    calc(var(--vy-lum-haut, 16px) + var(--vy-lum-cy, 154px) * .35),
    transparent calc(var(--vy-lum-haut, 16px) + var(--vy-lum-cy, 154px) * 1.05));
}
.vy-lum-calque.is-on { opacity: 1; }
.vy-lum-calque.is-dessus { z-index: 1; }

/* ── La pochette physique ─────────────────────────────────────────────── */
/* L'inclinaison est écrite par le script (`transform` en ligne, sur la
   pochette seulement : jamais sur son bouton, qui porte le disque d'écoute).
   Le reflet suit `--vy-mx` / `--vy-my` ; il vit AU-DESSUS de l'image
   (`::after` est au squelette d'anim.js). `is-appui` : sous le doigt ;
   `is-lueur` : l'orientation de l'appareil, un reflet plus discret. Le
   survol à la souris (`is-penche`) ne vit que sous un pointeur fin. */
.album-cover-btn > .album-cover::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  background: radial-gradient(circle at var(--vy-mx, 50%) var(--vy-my, 50%),
    var(--vy-lum-reflet), transparent 60%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--vy-dur-s) var(--vy-ease-out);
}
.album-cover-btn > .album-cover.is-appui::before { opacity: 1; }
.album-cover-btn > .album-cover.is-lueur::before { opacity: .55; }
@media (hover: hover) and (pointer: fine) {
  .album-cover-btn > .album-cover.is-penche::before { opacity: 1; }
}

/* ── Le héros se replie au défilement ──────────────────────────────────── */
/* `--vy-hero-p` (0 en haut de la fiche, 1 quand le héros est passé) :
   la pochette rétrécit autour de son centre et descend de la moitié de ce
   qu'elle perd, donc elle reste posée sur sa base (jamais sur le bouton
   « Pochette » ni sur le titre) et monte moins vite que le texte. Les
   propriétés individuelles `scale` / `translate` se composent avec le
   `transform` de l'inclinaison et avec la sortie du disque, sans les
   écraser. Posées seulement quand la fiche a défilé (`is-replie`). */
.album-hero.is-replie .album-cover-btn > .album-cover,
.album-hero.is-replie .vy-disc {
  scale: calc(1 - var(--vy-hero-p, 0) * .14);
  translate: 0 calc(var(--vy-hero-p, 0) * 7%);
}

@media (prefers-reduced-motion: reduce) {
  .album-cover-btn > .album-cover::before { transition: none; }
  .album-hero:has(> .album-cover-btn) .vy-lum { animation: none; }
}
:root[data-mouvement="reduit"] .album-hero:has(> .album-cover-btn) .vy-lum { animation: none; }


/* ── 17.3 · Le vivant : vivant.js ───────────────────────────────────────── */
/* (bloc de vivant.js) */
/* Le reflet qui balaie la jauge des 200 une fois remplie : une lueur claire
   sur la barre d'encre en thème clair ; en sombre, l'encre est claire et une
   lueur blanche ne se verrait pas, c'est l'accent qui passe. */
:root { --vy-jauge-reflet: rgba(255, 255, 255, .6); }
:root[data-theme="dark"] { --vy-jauge-reflet: color-mix(in srgb, var(--vy-accent) 62%, transparent); }

/* Ce qui se construit s'étire depuis sa base : la largeur et la hauteur
   inline restent la donnée, seule l'échelle bouge (vivant.js). */
.bar-fill { transform-origin: 0 50%; }
.col-fill { transform-origin: 50% 100%; }

/* Le compte d'un chiffre : le texte final reste dans le DOM (lecteur
   d'écran, textContent, innerText), son DESSIN est effacé, et le
   pseudo-élément dessine la valeur qui monte, par-dessus, sans texte
   alternatif (`/ ""`). `--vy-n` est un entier enregistré par vivant.js et
   animé par WAAPI. Posée sur un `b` au fil d'une phrase, la classe en fait
   un bloc en ligne : même ligne de base, même largeur (le compte ne dépasse
   jamais la valeur finale, chiffres tabulaires).
   Sous `@supports` : un moteur qui ne lit pas le texte alternatif de
   `content` (Safari avant 17.4) ne génère pas le pseudo-élément, et le
   chiffre effacé resterait invisible le temps du compte. */
@supports (content: "x" / "") {
  .vy-compte {
    display: inline-block;
    position: relative;
    counter-reset: vy-n var(--vy-n, 0);
    -webkit-text-fill-color: transparent;
  }
  .vy-compte::after {
    content: counter(vy-n) var(--vy-apres, "") / "";
    position: absolute;
    inset: 0;
    -webkit-text-fill-color: currentColor;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    text-align: left;
    pointer-events: none;
  }
  #essProgressPct.vy-compte::after { text-align: right; }
}

/* La jauge des 200 : remplie en ressort depuis la gauche. Le temps du
   ressort, la transition de largeur de la feuille est suspendue : la
   largeur arrivée avec les données ne court pas une seconde fois. */
.ess-progress-fill { position: relative; overflow: hidden; transform-origin: 0 50%; }
.ess-progress-fill.is-remplit { transition: none; }
.ess-progress-fill.is-reflet::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 18%, var(--vy-jauge-reflet) 50%, transparent 82%);
  transform: translateX(-100%);
  animation: vy-jauge-reflet 860ms var(--vy-ease-in-out) 360ms;
  pointer-events: none;
}
@keyframes vy-jauge-reflet {
  from { transform: translateX(-100%); }
  to   { transform: translateX(100%); }
}

/* Les éclats du favori et de la note pleine : un conteneur fixe de taille
   nulle, posé au centre du bouton, au plan 65 (au-dessus des feuilles, sous
   les modales). Il ne reçoit jamais le pointeur. */
.vy-eclats {
  position: fixed;
  z-index: 65;
  width: 0;
  height: 0;
  pointer-events: none;
  color: var(--vy-accent);
}
/* Cinq petits cœurs à l'accent, comme les étincelles de la note : l'encre
   pleine en confettis se lisait comme une image collée, et les cœurs de
   16 px mordaient sur les boutons voisins. */
.vy-eclat {
  position: absolute;
  left: -6px;
  top: -6px;
  width: 12px;
  height: 12px;
  overflow: visible;
  fill: currentColor;
  stroke: none;
  pointer-events: none;
}
.vy-eclats-etincelles .vy-eclat { left: -9px; top: -9px; width: 18px; height: 18px; }

/* L'onde du tirage : DANS la scène, derrière la platine (la scène isole son
   empilement), rognée par elle et effacée avant les lignes de texte : la
   lumière ne passe jamais sous un mot. L'anneau est pré-flouté dans son
   dégradé ; seules son échelle et son opacité bougent. */
.random-stage { position: relative; isolation: isolate; }
.vy-onde-calque {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(var(--vy-onde-bas, 100%) - 30px), transparent var(--vy-onde-bas, 100%));
  mask-image: linear-gradient(to bottom, #000 calc(var(--vy-onde-bas, 100%) - 30px), transparent var(--vy-onde-bas, 100%));
}
.vy-onde {
  position: absolute;
  border-radius: 50%;
  opacity: 0;
  background: radial-gradient(closest-side, transparent 44%, var(--vy-onde, var(--vy-accent)) 72%, transparent 100%);
}

@media (prefers-reduced-motion: reduce) {
  .vy-eclats, .vy-onde-calque { display: none; }
  .ess-progress-fill.is-reflet::after { animation: none; content: none; }
}
:root[data-mouvement="reduit"] .vy-eclats,
:root[data-mouvement="reduit"] .vy-onde-calque { display: none; }
:root[data-mouvement="reduit"] .ess-progress-fill.is-reflet::after { animation: none; content: none; }


/* ── 17.4 · Le flux : flux.js ───────────────────────────────────────────── */
/* (bloc de flux.js) */
/* Une tuile en vol quitte le confinement de peinture de `content-visibility:
   auto` le temps du glissé : sa boîte d'arrivée peut être hors de l'écran
   pendant qu'elle part d'une place visible, et la pochette qui voyage de la
   grille à la liste déborde de sa tuile. Les tuiles proches de l'écran la
   portent aussi le temps du geste, sans bouger : une tuile jamais peinte se
   mesure à sa taille intrinsèque, et la mesure d'arrivée serait fausse.
   Retirée à la fin du vol. */
.tile.is-flux-vol { content-visibility: visible; }

/* Les calques du flux (la tuile qui s'en va, la carte retirée de « Pour
   vous ») : des images, jamais des cibles ni des éléments du parcours
   (aria-hidden et inert sont posés par le script). Ce qui PART passe
   SOUS la page (`z-index: -1`, au-dessus du fond du document, que le body
   porte) : les tuiles qui restent glissent par-dessus, le texte de la
   barre d'outils reste lisible, un bandeau opaque le cache, et rien ne
   monte jamais au-dessus des barres collantes. */
.tiles.vy-flux-sortie,
.vy-flux-retrait {
  position: fixed;
  z-index: -1;
  margin: 0;
  pointer-events: none;
}
/* Le fantôme d'une tuile reprend la classe de sa grille (les règles de la
   vue liste vivent sous `.tiles.is-list`) sur une seule colonne, à la
   largeur de la tuile qu'il remplace. */
.tiles.vy-flux-sortie { grid-template-columns: minmax(0, 1fr); }
/* La carte retirée s'efface DANS la fenêtre de son rail, qui la rognait. */
.vy-flux-retrait { overflow: hidden; }
.vy-flux-retrait > .reco-card { position: absolute; margin: 0; }
/* Le temps que les voisines referment le trou, le rail ne s'accroche plus :
   Chromium compte la translation d'une carte dans sa zone d'accroche, et
   recalait le rail d'un cran vers la voisine repartie de son ancienne place.
   Posée et retirée par le script, avec la dernière animation du rail. */
.rail.is-flux-serre { scroll-snap-type: none; }


/* ┌──────────────────────────────────────────────────────────────────────────
   │ 18 · LA VAGUE 2 (septembre 2026) : verre.js, feuille.js, platine.js,
   │      scene.js. L'app au niveau d'une app iOS dernier cri : verre
   │      liquide, cartes qui se tirent au doigt, disques qui tournent,
   │      apparitions en ressort. Toujours dans la section 6 (ses verrous
   │      valent ici). Un bloc par module.
   └────────────────────────────────────────────────────────────────────────── */

/* ── 18.1 · Le verre liquide : verre.js ─────────────────────────────────── */
/* (bloc de verre.js) */
/* Tout est accroché à `html.vy-verre`, que verre.js pose en s'exécutant
   (avant qu'app.js ne montre l'application : aucun saut au premier rendu).
   Sans le script, la barre plate d'avant. Le flou est POSÉ, jamais animé :
   ce qui change est l'opacité d'un calque ou le transform d'un bloc. */

/* La matière. Le fond du verre est assez couvrant pour que tout texte posé
   dessus tienne 4,5:1 quel que soit ce qui passe dessous (calculé au pire :
   un fond noir sous le verre clair, un fond blanc sous le verre sombre :
   aucun canal sous 180 en clair, aucun au-dessus de 77 en sombre, donc
   l'encre seconde tient 5,0:1 dans les deux thèmes). Le flou et la saturation ne font
   qu'adoucir ce pire cas (tests/test_vinyles_fx2_verre.py le recalcule). */
:root {
  --vy-verre-fond: rgba(251, 250, 247, .72);
  --vy-verre-speculaire: rgba(255, 255, 255, .62);
  --vy-verre-lisere: rgba(255, 255, 255, .95);
  --vy-verre-bord: rgba(10, 10, 10, .09);
  --vy-verre-rim: rgba(255, 255, 255, .9);
  --vy-verre-rim-bas: rgba(255, 255, 255, .45);
  --vy-verre-ombre: 0 18px 38px -14px rgba(10, 10, 10, .34), 0 4px 12px -4px rgba(10, 10, 10, .14);
  --vy-lentille-fond: color-mix(in srgb, var(--vy-accent) 11%, rgba(255, 255, 255, .9));
  --vy-lentille-lisere: rgba(255, 255, 255, 1);
  --vy-lentille-bord: rgba(10, 10, 10, .06);
  --vy-lentille-ombre: 0 1px 1px rgba(10, 10, 10, .06), 0 8px 18px -8px rgba(10, 10, 10, .28);
  --vy-lentille-reflet: rgba(255, 255, 255, .75);
  --vy-bouton-verre: rgba(10, 10, 10, .075);
  --vy-bouton-reflet: rgba(255, 255, 255, .95);
}
:root[data-theme="dark"] {
  --vy-verre-fond: rgba(26, 26, 24, .78);
  --vy-verre-speculaire: rgba(255, 255, 255, .08);
  --vy-verre-lisere: rgba(255, 255, 255, .16);
  --vy-verre-bord: rgba(255, 255, 255, .09);
  --vy-verre-rim: rgba(255, 255, 255, .34);
  --vy-verre-rim-bas: rgba(255, 255, 255, .1);
  --vy-verre-ombre: 0 20px 44px -12px rgba(0, 0, 0, .78), 0 4px 12px -3px rgba(0, 0, 0, .5);
  /* En sombre, la lentille ASSOMBRIT le verre au lieu de l'éclaircir : elle
     porte une icône d'accent clair, qu'un voile blanc ramenait à 2:1 quand
     une pochette blanche passait dessous. Son relief vient du liseré, du
     bord et de l'ombre. Au pire, l'icône tient 3,4:1 et l'encre 7:1. */
  --vy-lentille-fond: color-mix(in srgb, var(--vy-accent) 16%, rgba(0, 0, 0, .3));
  --vy-lentille-lisere: rgba(255, 255, 255, .24);
  --vy-lentille-bord: rgba(255, 255, 255, .14);
  --vy-lentille-ombre: 0 1px 1px rgba(0, 0, 0, .3), 0 8px 18px -8px rgba(0, 0, 0, .7);
  --vy-lentille-reflet: rgba(255, 255, 255, .08);
  --vy-bouton-verre: rgba(255, 255, 255, .1);
  --vy-bouton-reflet: rgba(255, 255, 255, .5);
}
/* Le noir profond : le verre fumé devient un verre noir, pour qu'il ne
   paraisse pas gris sur un fond #000. */
:root[data-theme="dark"][data-noir="oui"] {
  --vy-verre-fond: rgba(0, 0, 0, .74);
  --vy-verre-speculaire: rgba(255, 255, 255, .07);
}
/* ── G1 · La barre d'onglets : une capsule de verre qui flotte ─────────── */
@media (max-width: 899px) {
  html.vy-verre .tabbar {
    left: 12px;
    right: 12px;
    bottom: max(10px, calc(env(safe-area-inset-bottom, 0px) - 6px));
    max-width: 460px;
    margin: 0 auto;
    gap: 0;
    padding: 5px;
    border: 0;
    border-radius: 999px;
    background-color: var(--vy-verre-fond);
    background-image: linear-gradient(180deg, var(--vy-verre-speculaire) 0, transparent 48%);
    -webkit-backdrop-filter: blur(22px) saturate(1.7);
    backdrop-filter: blur(22px) saturate(1.7);
    box-shadow: var(--vy-verre-ombre), 0 0 0 .5px var(--vy-verre-bord), inset 0 1px 0 var(--vy-verre-lisere);
    /* Le doigt glisse le long de la barre (G2) : c'est elle qui reçoit le
       geste, pas la page. */
    touch-action: none;
    transform-origin: 50% 100%;
    transition: transform var(--vy-dur-m) cubic-bezier(.3, 1.42, .5, 1);
  }
  /* Le liseré du bord, lumineux en haut à gauche et en bas à droite : la
     réfraction d'un verre épais. Un dégradé en bordure (masque composé). */
  html.vy-verre .tabbar::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 2;
    padding: 1px;
    border-radius: inherit;
    background: linear-gradient(135deg, var(--vy-verre-rim) 0, transparent 34%, transparent 66%, var(--vy-verre-rim-bas) 100%);
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
    pointer-events: none;
  }
  html.vy-verre .tabbar .tab { border-radius: 999px; }
  /* G3 · défilement vers le bas : la capsule se contracte, les libellés
     s'effacent, les icônes descendent au centre. En ressort, dans les deux
     sens. Les libellés restent dans le DOM (noms accessibles). */
  html.vy-verre .tabbar .tab span { transition: opacity var(--vy-dur-s) var(--vy-ease-out); }
  html.vy-verre .tabbar .tab .ico {
    transition-property: transform, color, background, border-color, translate;
    transition-duration: var(--vy-dur-xs), var(--vy-dur-xs), var(--vy-dur-xs), var(--vy-dur-xs), var(--vy-dur-m);
    transition-timing-function: var(--vy-ease-out), var(--vy-ease-out), var(--vy-ease-out), var(--vy-ease-out), cubic-bezier(.3, 1.42, .5, 1);
  }
  /* .92 et pas moins : l'onglet de 48 px passe à 44,2 px, le plancher
     tactile (à .88, 42,2 px). */
  html.vy-verre .tabbar.is-reduite { transform: translateY(5px) scale(.92); }
  html.vy-verre .tabbar.is-reduite .tab span { opacity: 0; }
  html.vy-verre .tabbar.is-reduite .tab .ico { translate: 0 8px; }
}
@media (max-width: 360px) {
  html.vy-verre .tabbar { left: 8px; right: 8px; padding: 4px; }
}
/* Sans flou (navigateur ancien), un verre qu'on ne peut pas lire à travers
   serait une tache : le papier opaque. */
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  html.vy-verre .tabbar,
  html.vy-verre .topbar::after { background-color: var(--vy-bg); }
}
/* Safari avant 16.2 ne connaît pas `color-mix()`. Posé sur une propriété
   personnalisée, il n'est pas refusé à l'analyse : c'est la déclaration qui
   la LIT qui devient invalide au calcul, et la lentille perdait son fond
   (une seconde déclaration devant le `var()` ne repêche rien). Le repli se
   pose donc sur la propriété elle-même, pour ces navigateurs seulement :
   la part « autre » du mélange, neutre, sans la teinte de l'accent (elle
   vaut pour les sept couleurs). */
@supports not (color: color-mix(in srgb, red, blue)) {
  :root { --vy-lentille-fond: rgba(255, 255, 255, .9); }
  :root[data-theme="dark"] {
    --vy-lentille-fond: rgba(0, 0, 0, .34);
    --vy-jauge-reflet: rgba(17, 17, 16, .35);
  }
}

/* Dans le verre, un libellé d'onglet est à l'encre seconde (et non grise) :
   c'est ce qui tient 4,5:1 au pire. L'onglet courant, à l'encre ; son icône
   prend l'accent, comme la teinte de l'onglet choisi d'iOS. */
html.vy-verre .tabbar .tab { z-index: 1; color: var(--vy-ink-2); -webkit-tap-highlight-color: transparent; }
html.vy-verre .tabbar .tab.is-on,
html.vy-verre .tabbar .tab.is-sous-lentille { color: var(--vy-ink); }
html.vy-verre .tabbar .tab.is-on .ico,
html.vy-verre .tabbar .tab.is-sous-lentille .ico { color: var(--vy-accent-texte); }
@media (hover: hover) and (pointer: fine) {
  html.vy-verre .tabbar .tab:hover { color: var(--vy-ink); }
}
/* L'encoche d'accent de l'onglet devient celle de la lentille (qui glisse
   avec elle, en ressort) : l'ancienne ne s'efface qu'une fois la lentille
   posée, jamais sans elle. */
html.vy-verre .tabbar.has-lentille .tab.is-on::after { content: none; }

/* ── G2 · La lentille ─────────────────────────────────────────────────── */
/* Deux calques : `.vy-lentille` porte la position (translate, posé en ligne
   par le script, animé en ressort), `.vy-lentille-verre` l'étirement
   (scale, WAAPI) et l'appui (la propriété individuelle `scale`, en
   transition). Taille posée par le script, celle d'un onglet. */
.vy-lentille {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  border-radius: 999px;
  opacity: 0;
  pointer-events: none;
  will-change: transform;
}
.vy-lentille.is-posee { opacity: 1; }
.vy-lentille-verre {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background-color: var(--vy-lentille-fond);
  background-image: linear-gradient(180deg, var(--vy-lentille-reflet) 0, transparent 58%);
  box-shadow: var(--vy-lentille-ombre), 0 0 0 .5px var(--vy-lentille-bord), inset 0 1px 0 var(--vy-lentille-lisere);
  transform-origin: 50% 50%;
  transition: scale var(--vy-dur-s) cubic-bezier(.3, 1.6, .5, 1);
}
/* La lentille SUFFIT à marquer l'onglet courant (octobre 2026) : le trait
   d'accent posé sur son bord haut, comme l'encoche de la barre d'avant
   l'était sur son filet, faisait un repère de trop. La barre d'iOS n'a que
   la capsule et la teinte de l'icône. */
.vy-lentille.is-appui .vy-lentille-verre { scale: 1.1; transition-duration: var(--vy-dur-xs); }
.vy-lentille.is-glisse .vy-lentille-verre { scale: 1.14; }
/* À partir de 900 px, la colonne : la lentille est un rectangle doux sous la
   ligne de l'onglet, l'encoche passe sur son bord gauche. */
@media (min-width: 900px) {
  .vy-lentille { border-radius: var(--vy-r-2); }
  .vy-lentille.is-appui .vy-lentille-verre { scale: 1.03; }
  .vy-lentille.is-glisse .vy-lentille-verre { scale: 1.05; }
}

/* ── G4 · La barre du haut devient du verre quand le contenu passe dessous ─ */
/* Deux calques derrière le contenu de la barre : le papier (au repos) et le
   verre (flou posé une fois pour toutes). Seules leurs opacités changent. */
html.vy-verre .topbar { background: transparent; }
html.vy-verre .topbar::before,
html.vy-verre .topbar::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  transition: opacity var(--vy-dur-s) var(--vy-ease-out);
}
html.vy-verre .topbar::before { background: var(--vy-bg); }
html.vy-verre .topbar::after {
  opacity: 0;
  background-color: var(--vy-verre-fond);
  background-image: linear-gradient(180deg, var(--vy-verre-speculaire) 0, transparent 80%);
  -webkit-backdrop-filter: blur(20px) saturate(1.7);
  backdrop-filter: blur(20px) saturate(1.7);
}
html.vy-verre .topbar.is-verre::before { opacity: 0; }
html.vy-verre .topbar.is-verre::after { opacity: 1; }
/* Rien ne déborde de la barre à l'horizontale : l'anneau du « + » qui
   respire (anim.js, collection vide) s'écarte à 1,55 fois le bouton, et
   sous 360 px la barre n'a que 8 px de marge à droite ; l'anneau poussait
   la page de 4 px (mesuré à 320). `clip` sur le SEUL axe horizontal : le
   menu du « + », qui descend sous la barre, reste entier. */
html.vy-verre .topbar { overflow-x: clip; }
html.vy-verre #topTitle {
  transform-origin: 0 50%;
  transition: scale var(--vy-dur-m) cubic-bezier(.3, 1.4, .5, 1);
}
html.vy-verre .topbar.is-verre #topTitle { scale: .94; }
@media (min-width: 560px) {
  html.vy-verre #topTitle { transform-origin: 50% 50%; }
}

/* ── G5 · Les boutons de verre répondent au doigt ────────────────────── */
/* À l'appui : le bouton s'écrase (la propriété `scale`, en transition
   courte ; le rebond du relâché est un ressort WAAPI, sans transition sur
   la même propriété), une pastille de verre apparaît sous l'icône et un
   reflet part du point touché (`--vy-bx`, `--vy-by`, posés par le script).
   L'icône et les pastilles restent au-dessus du reflet. */
html.vy-verre .top-btn .ico { position: relative; z-index: 1; }
html.vy-verre .top-dot,
html.vy-verre .top-count { z-index: 2; }
html.vy-verre .top-btn::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    radial-gradient(circle at var(--vy-bx, 50%) var(--vy-by, 50%), var(--vy-bouton-reflet) 0, transparent 62%),
    var(--vy-bouton-verre);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--vy-dur-s) var(--vy-ease-out);
}
html.vy-verre .top-btn-main::before {
  background: radial-gradient(circle at var(--vy-bx, 50%) var(--vy-by, 50%), rgba(255, 255, 255, .42) 0, transparent 64%);
}
html.vy-verre .top-btn.is-presse::before { opacity: 1; transition-duration: 60ms; }
html.vy-verre .top-btn.is-presse { scale: .9; transition: scale 110ms var(--vy-ease-out); }
html.vy-verre .top-btn:active { transform: none; }
/* Le « + » tourne d'un huitième de tour quand son menu s'ouvre : l'état est
   celui qu'app.js pose (`aria-expanded`), le ressort est joué par verre.js. */
html.vy-verre #btnAdd[aria-expanded="true"] .ico { rotate: 45deg; }
/* « Moins de mouvement » : verre.js ne pose ni `is-reduite`, ni `is-presse`,
   ni `is-appui` (l'arbitre est relu à chaque geste, et le réglage
   « Animations : toujours » doit pouvoir l'emporter sur l'appareil, ce
   qu'une media query ne saurait pas) ; les transitions qui restent sont
   déjà ramenées à 0,01 ms par les sections 13 et 16.5. La matière, elle,
   reste : la capsule, le verre, la lentille posée sans glisser. */


/* ── 18.2 · Les feuilles d'iOS : feuille.js ─────────────────────────────── */
/* (bloc de feuille.js) */
/* Le vocabulaire. `--vy-recul-s`, `--vy-recul-t` (le haut de la carte moins
   10 px : `ECART`), `--vy-empile-s` et `--vy-empile-y` sont recopiés dans
   feuille.js (`RECUL`, `ECART`, `EMPILE_S`, `EMPILE_Y`) : un test relit les
   deux. La carte descend sous la zone sûre du haut ; l'app reculée laisse
   voir 10 px d'elle au-dessus de la carte, comme sur iOS. Le voile qui
   descend sur l'app est plus fort en sombre : sans lui, la carte (#111110)
   et l'app reculée se confondraient. L'île est une capsule d'encre en clair,
   de noir pur bordé d'un filet en sombre (celle d'iOS). */
:root {
  --vy-carte-haut: calc(env(safe-area-inset-top, 0px) + 12px);
  --vy-carte-rayon: 14px;
  --vy-carte-lisere: rgba(0, 0, 0, .06);
  --vy-carte-voile: .18;
  --vy-recul-s: .93;
  --vy-recul-t: calc(var(--vy-carte-haut) - 10px);
  --vy-recul-rayon: 14px;
  --vy-empile-s: .94;
  --vy-empile-y: 10px;
  --vy-poignee: rgba(0, 0, 0, .22);
  --vy-ile-fond: var(--vy-ink);
  --vy-ile-texte: #fbfaf7;
  --vy-ile-lisere: rgba(255, 255, 255, 0);
}
:root[data-theme="dark"] {
  --vy-carte-lisere: rgba(255, 255, 255, .1);
  --vy-carte-voile: .46;
  --vy-poignee: rgba(255, 255, 255, .28);
  --vy-ile-fond: #000000;
  --vy-ile-texte: #f5f4f1;
  --vy-ile-lisere: rgba(255, 255, 255, .16);
}

/* La poignée : un trait en tête de chaque feuille, rien qu'un repère (le
   geste se prend sur tout l'en-tête). Elle n'existe qu'en carte. */
.vy-poignee { display: none; }

/* Les calques posés dans <body> par feuille.js : la coulisse noire derrière
   l'app reculée (sous tout : le fond du document ne passe plus), le voile
   qui descend sur l'app et les panneaux (sous les feuilles, 60), celui de
   l'écran poussé (au plan des panneaux, posé juste avant celui qui arrive).
   Aucun ne prend le pointeur. Ils n'existent que le temps du geste. */
.vy-carte-coulisse,
.vy-carte-voile,
.vy-pousse-voile {
  position: fixed;
  inset: 0;
  background: #000;
  pointer-events: none;
}
.vy-carte-coulisse { z-index: -1; display: none; }
.vy-carte-voile { z-index: 55; display: none; opacity: 0; }
.vy-pousse-voile { z-index: 50; opacity: 0; }
html.vy-recul .vy-carte-coulisse,
html.vy-recul-retour .vy-carte-coulisse,
html.vy-recul .vy-carte-voile,
html.vy-recul-retour .vy-carte-voile { display: block; }
html.vy-recul .vy-carte-voile { opacity: var(--vy-carte-voile); }

/* Pendant un glissé au doigt, rien ne se sélectionne. */
html.vy-glisse body { -webkit-user-select: none; user-select: none; }

@media (max-width: 719.98px) {
  /* ── La carte : une mise en page STATIQUE (vol.js lit le `top` calculé
     pour poser la grande pochette) ── */
  html.vy-feuille .sheet {
    top: var(--vy-carte-haut);
    border-radius: var(--vy-carte-rayon) var(--vy-carte-rayon) 0 0;
    overflow: hidden;
    box-shadow: 0 0 0 1px var(--vy-carte-lisere);
  }
  /* L'en-tête ne réserve plus la zone sûre (elle est au-dessus de la carte)
     mais la place de la poignée ; c'est lui qu'on tire au doigt, d'où le
     `touch-action` (ses boutons se touchent toujours). */
  html.vy-feuille .sheet-head {
    padding-top: 14px;
    touch-action: none;
  }
  html.vy-feuille .vy-poignee {
    display: block;
    position: absolute;
    z-index: 2;
    top: 6px;
    left: 50%;
    width: 36px;
    height: 5px;
    margin-left: -18px;
    border-radius: 3px;
    background: var(--vy-poignee);
    pointer-events: none;
  }
  /* Une carte ouverte par-dessus une autre : celle du dessous recule d'un
     cran (elle monte et rétrécit, son haut dépasse au-dessus de l'autre)
     et s'assombrit un peu. */
  html.vy-feuille .sheet.is-empilee {
    transform: translateY(calc(-1 * var(--vy-empile-y))) scale(var(--vy-empile-s));
    transform-origin: 50% 0;
  }
  html.vy-feuille .sheet.is-empilee::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    background: #000;
    opacity: .08;
    pointer-events: none;
  }
  html[data-theme="dark"].vy-feuille .sheet.is-empilee::after { opacity: .22; }

  /* ── L'app qui recule derrière la carte ──
     L'origine (le haut de l'écran, au milieu) et la découpe à l'écran sont
     mesurées au geste et posées en ligne sur #scrApp. La découpe arrondit
     les coins ; le fond de la coque est posé pour que la coulisse noire ne
     passe pas au travers d'une vue sans fond. */
  html.vy-recul #scrApp,
  html.vy-recul-retour #scrApp {
    transform-origin: var(--vy-recul-ox, 50%) var(--vy-recul-oy, 0px);
    clip-path: inset(var(--vy-recul-haut, 0px) 0 var(--vy-recul-bas, 0px) 0 round var(--vy-recul-rayon));
    background: var(--vy-bg);
  }
  html.vy-recul #scrApp {
    transform: translateY(var(--vy-recul-t)) scale(var(--vy-recul-s));
    pointer-events: none;
  }
  /* Un panneau ouvert sous la carte recule avec l'app (il est plein écran :
   son origine est le haut de l'écran). */
  html.vy-feuille .panel.vy-recul-panneau {
    transform-origin: 50% 0;
    border-radius: var(--vy-recul-rayon);
    overflow: hidden;
  }
  html.vy-recul .panel.vy-recul-panneau {
    transform: translateY(var(--vy-recul-t)) scale(var(--vy-recul-s));
    pointer-events: none;
  }
  /* Le bord gauche d'un panneau est le retour d'iOS. Sans ce `touch-action`
     le navigateur prend le glissé horizontal pour lui (le retour dans
     l'historique de Chrome sur téléphone : mesuré, la page partait) et le
     doigt n'arrive jamais au script. Posé sur le panneau ET sur son corps,
     parce que c'est le conteneur qui défile le plus proche qui décide ; une
     rangée qui défile à l'horizontale dans le corps est un conteneur à part,
     qui garde son propre geste. */
  html.vy-feuille .panel,
  html.vy-feuille .panel-body { touch-action: pan-y pinch-zoom; }
}

/* La barre d'onglets est fixe DANS la coque : une coque transformée devient
   son bloc conteneur, et elle sauterait au bas du document. On la recale
   d'autant, le temps du recul ou de l'écran poussé (`--vy-recul-barre`,
   mesuré une fois au geste). La propriété `translate` s'ajoute au reste. */
html.vy-recul #scrApp .tabbar,
html.vy-recul-retour #scrApp .tabbar,
html.vy-pousse #scrApp .tabbar { translate: 0 var(--vy-recul-barre, 0px); }

/* Les clones qui partent : jamais le pointeur. */
.vy-carte-clone,
.vy-ile-clone { pointer-events: none; }

/* ── L'île : le message éphémère posé en haut, capsule d'encre ── */
/* `left: 50%` ne laisse à une boîte en position fixe que la MOITIÉ de
   l'écran pour se dimensionner : « Ajouté à la collection. » y passait sur
   deux lignes. La largeur est donc celle du contenu, bornée par la
   `max-width` du toast.
   L'île se pose SOUS ce qu'elle recouvrirait, jamais par-dessus : une page
   web n'atteint pas la barre d'état d'iOS, et posée à 10 px du haut elle
   masquait le « + », la recherche et les messages de la barre du haut, puis
   le partage et le titre de la fiche à chaque favori. Sous la barre du haut
   (même jeton que la vue, `--vy-top-real` mesuré par app.js), sous l'en-tête
   de la carte quand une fiche est ouverte au téléphone, sous l'en-tête de la
   feuille centrée à l'ordinateur. */
html.vy-feuille .toast {
  top: calc(var(--vy-top-real, calc(var(--vy-top-h) + max(0px, env(safe-area-inset-top, 0px) - 6px))) + 8px);
  bottom: auto;
  width: max-content;
  gap: 10px;
  min-height: 42px;
  padding: 10px 18px 10px 14px;
  border: 1px solid var(--vy-ile-lisere);
  border-radius: 24px;
  background: var(--vy-ile-fond);
  color: var(--vy-ile-texte);
  text-align: left;
  pointer-events: none;
}
/* La carte (téléphone) : son haut, puis son en-tête (14 px de poignée, le
   bouton de 44 px, 6 px de bas et le filet : 65 px), puis 8 px. */
@media (max-width: 719.98px) {
  html.vy-feuille.vy-recul .toast { top: calc(var(--vy-carte-haut) + 65px + 8px); }
}
/* La feuille centrée (ordinateur) : son haut vaut 50 % moins la moitié de
   `min(92dvh, 900px)` (section 8), puis son filet, son en-tête de 57 px et
   8 px. Une feuille courte, plus basse, laisse l'île au-dessus d'elle. */
@media (min-width: 720px) {
  html.vy-feuille body.has-modal .toast { top: calc(max(4dvh, 50dvh - 450px) + 1px + 57px + 8px); }
}
/* Le point d'accent, à gauche : il pulse une fois quand l'île naît. */
html.vy-feuille .toast::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--vy-accent);
}
html.vy-feuille .toast.is-pulse::before { animation: vy-ile-pulse 620ms cubic-bezier(.3, 0, .2, 1) 60ms; }
@keyframes vy-ile-pulse {
  0% { transform: scale(1); opacity: 1; }
  35% { transform: scale(2.1); opacity: .45; }
  100% { transform: scale(1); opacity: 1; }
}
/* Seul le bouton d'action prend le pointeur ; au doigt, une cible de 44 px
   qui ne grandit pas l'île (marges négatives). */
html.vy-feuille .toast .link-btn {
  pointer-events: auto;
  color: var(--vy-ile-texte);
  text-decoration-color: var(--vy-accent);
}
@media (pointer: coarse) {
  html.vy-feuille .toast .link-btn { min-height: 44px; margin: -6px -6px -6px 0; padding: 0 6px; }
}
/* La naissance : le fond du message s'efface, c'est la capsule décorative
   posée dessous qui se déploie. */
html.vy-feuille .toast.is-nait {
  background: transparent;
  border-color: transparent;
}
.vy-ile-capsule {
  position: fixed;
  z-index: 94;
  box-sizing: border-box;
  border: 1px solid var(--vy-ile-lisere);
  border-radius: 24px;
  background: var(--vy-ile-fond);
  transform-origin: 50% 50%;
  pointer-events: none;
}


/* ── 18.3 · Le disque : platine.js ──────────────────────────────────────── */
/* (bloc de platine.js) */
/* Le vrai disque, partout. Rien ici n'est une image : les sillons sont un
   dégradé radial répété, l'étiquette un SVG construit par le script, le
   reflet un `conic-gradient` posé sur un calque qui ne tourne pas.

   Les jetons du disque, dans les deux thèmes : le corps (le noir du vinyle,
   plus noir que le papier sombre pour qu'il s'en détache), les sillons, la
   bande lisse (fin de face, espace entre deux plages), le liseré du bord,
   deux lueurs (le reflet de la pièce), le bras et l'ombre portée. */
:root {
  --vy-platine-corps: #121211;
  --vy-platine-sillon: rgba(255, 255, 255, .075);
  --vy-platine-lisse: #1d1d1b;
  --vy-platine-moire: rgba(255, 255, 255, .04);
  --vy-platine-rim: rgba(255, 255, 255, .16);
  --vy-platine-lueur: rgba(255, 255, 255, .2);
  --vy-platine-lueur-2: rgba(255, 255, 255, .12);
  /* Un gris métal et non un blanc : sur le papier, la lumière (≈ 206, 222,
     241 sous une pochette bleue) portait un bras clair à 1,04:1. Celui-ci
     tient 3,3:1 contre elle, 4,4:1 contre le papier et 4,1:1 contre le
     disque. */
  --vy-platine-bras: #777671;
  --vy-platine-onde: rgba(255, 255, 255, .7);
  --vy-platine-ombre: 0 14px 28px -10px rgba(0, 0, 0, .45), 0 2px 6px rgba(0, 0, 0, .2);
}
:root[data-theme="dark"] {
  --vy-platine-corps: #0b0b0a;
  --vy-platine-sillon: rgba(255, 255, 255, .06);
  --vy-platine-lisse: #171715;
  --vy-platine-moire: rgba(255, 255, 255, .035);
  --vy-platine-rim: rgba(255, 255, 255, .15);
  --vy-platine-lueur: rgba(255, 255, 255, .17);
  --vy-platine-lueur-2: rgba(255, 255, 255, .1);
  --vy-platine-bras: #cfcec9;
  --vy-platine-onde: rgba(255, 255, 255, .6);
  --vy-platine-ombre: 0 14px 28px -10px rgba(0, 0, 0, .8), 0 0 0 1px rgba(255, 255, 255, .06);
}

/* ── 1 · Le disque de la fiche ─────────────────────────────────────────── */
/* La largeur de la pochette devient un jeton du bouton : la pochette ET le
   disque la lisent, et la place du disque (40 % de la pochette, le
   dépassement du ressort compris) est réservée ICI, dès le rendu, par une
   `margin-right` : vol.js mesure le rectangle de repos de la pochette à
   l'ouverture, une place prise par une animation l'aurait trompé. Une
   MARGE et non un `padding` : le bouton garde la largeur de la pochette,
   que lumiere.js lit (`offsetWidth`) pour dimensionner la lumière ; un
   bouton élargi de 40 % faisait descendre la lumière sous le nom de
   l'artiste (contraste mesuré à 4,1:1). La grille du héros centre la boîte
   de marge : le couple pochette et disque est centré, et il tient dans
   l'écran à toute largeur (1,4 fois la pochette, plus les marges du corps
   de la fiche). Sans photo, le disque est là aussi : la vignette de
   remplacement EST une pochette (sans image), et « J'écoute » y pose le
   bras comme ailleurs. L'effet ne dépend pas d'avoir pris une photo.
   Tout est écrit sur des CLASSES (`.album-cover-btn`, `vy-a-platine` posée
   par platine.js) : le clone de feuille.js n'a plus d'identifiant, et sur
   `#albumCoverBtn` il perdait le jeton, la largeur et la marge (le disque
   passait de 243 à 300 px et sortait de l'écran à chaque fermeture). */
.album-cover-btn.vy-a-platine {
  --vy-pochette: min(66vw, 300px, calc((100vw - 36px) / 1.4));
  margin-right: calc(var(--vy-pochette) * .4);
}
@media (min-width: 720px) {
  .album-cover-btn.vy-a-platine { --vy-pochette: 300px; }
}
.album-cover-btn.vy-a-platine > .album-cover { width: var(--vy-pochette); }

.vy-platine {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--vy-pochette, 300px);
  aspect-ratio: 1;
  z-index: 0;
  pointer-events: none;
}
/* Le glissé : la place de repos, 38 % de la pochette vers la droite (la
   même valeur que `SORTIE` de platine.js : le test les compare). */
.vy-platine-glisse {
  position: absolute;
  inset: 0;
  transform: translateX(38%);
}
/* En attendant que la pochette de vol.js soit posée : rangé et caché. */
.vy-platine.is-attente .vy-platine-glisse { transform: translateX(0); visibility: hidden; }
/* Le disque se cache pendant le vol de l'ALLER seulement (`is-en-vol`) :
   pendant le vol de la visionneuse (`is-en-vol-vision`), la pochette part
   seule et le disque reste où il est. */
.album-cover-btn:has(> .album-cover.is-en-vol) > .vy-platine { visibility: hidden; }

.vy-platine-ombre,
.vy-platine-plateau,
.vy-platine-reflet {
  position: absolute;
  inset: 2%;
  border-radius: 50%;
}
.vy-platine-ombre { box-shadow: var(--vy-platine-ombre); }
/* Le plateau ne tourne pas et coupe au rond : la boîte d'un carré qui
   tourne s'élargit de 41 % à 45 degrés, et cette boîte faisait défiler la
   fiche de côté. Le disque tourne dedans, à la même taille. */
.vy-platine-plateau { overflow: hidden; }
.vy-platine-disque {
  position: absolute;
  inset: 0;
  border-radius: 50%;
}
/* Le vinyle : de l'extérieur vers le centre, le liseré du bord, la bande
   lisse d'entrée, les sillons coupés par deux blancs entre plages, puis la
   bande lisse de fin de face contre l'étiquette. Une moire à peine visible
   (un `conic-gradient` qui tourne AVEC le disque) : c'est elle, avec les
   lignes de l'étiquette, qui rend la rotation lisible. */
.vy-platine-disque {
  background:
    radial-gradient(circle closest-side,
      transparent 0 35%,
      var(--vy-platine-lisse) 35.5% 41%,
      transparent 41.5% 62%,
      var(--vy-platine-lisse) 62.5% 64%,
      transparent 64.5% 79%,
      var(--vy-platine-lisse) 79.5% 81%,
      transparent 81.5% 95.5%,
      var(--vy-platine-lisse) 96% 98%,
      var(--vy-platine-rim) 98.5% 100%,
      transparent 100%),
    conic-gradient(from 20deg,
      var(--vy-platine-moire) 0 7%, transparent 13% 43%,
      var(--vy-platine-moire) 50% 57%, transparent 63% 100%),
    url("casier/sillons.webp") center / 100% 100% no-repeat,
    repeating-radial-gradient(circle closest-side,
      var(--vy-platine-corps) 0 1.7%,
      var(--vy-platine-sillon) 1.9% 2.4%),
    var(--vy-platine-corps);
  will-change: transform;
}
.vy-platine-etiquette {
  position: absolute;
  left: 32.5%;
  top: 32.5%;
  width: 35%;
  height: 35%;
  overflow: visible;
}
.vy-etq-fond { fill: var(--vy-halo, var(--vy-accent)); }
.vy-etq-filet { fill: none; stroke: rgba(255, 255, 255, .45); stroke-width: 1.2; }
.vy-etq-ligne { fill: none; stroke: rgba(255, 255, 255, .78); stroke-width: 3.4; stroke-linecap: round; }
.vy-etq-ligne-2 { stroke-width: 2.4; opacity: .7; }
.vy-etq-marque { fill: rgba(255, 255, 255, .8); }
.vy-etq-trou { fill: var(--vy-bg); }
/* Le papier de l'étiquette (octobre 2026) : le grain, le liseré et l'usure
   d'une vraie étiquette, en gris centré sur le neutre et fondu en
   « overlay » sur l'étiquette dessinée, qui garde la couleur de la pochette.
   Posé dans le disque : il tourne avec lui. */
.vy-platine-disque::after {
  content: "";
  position: absolute;
  left: 32.5%;
  top: 32.5%;
  width: 35%;
  height: 35%;
  border-radius: 50%;
  background: url("casier/etiquette.webp") center / cover no-repeat;
  mix-blend-mode: overlay;
  opacity: .75;
  pointer-events: none;
}
/* La lumière de la pièce : deux coins de reflet en vis-à-vis sur les
   sillons, qui ne tournent pas. Le masque l'arrête à l'étiquette et au
   bord. */
.vy-platine-reflet {
  background: conic-gradient(from 300deg,
    transparent 0 4%, var(--vy-platine-lueur) 6%, transparent 9% 52%,
    var(--vy-platine-lueur-2) 56%, transparent 60% 100%);
  -webkit-mask: radial-gradient(circle closest-side, transparent 0 36%, #000 38% 97%, transparent 98%);
  mask: radial-gradient(circle closest-side, transparent 0 36%, #000 38% 97%, transparent 98%);
}
/* Le bras, et l'onde de sa pointe : coupés au carré du disque, jamais
   au-delà (le couple doit tenir dans l'écran). Pivot dans le coin haut
   droit ; les angles valent ceux de platine.js (`BRAS_REPOS`, `BRAS_POSE`).
   Au repos, invisible ; `is-lecture` tient le bras posé. */
.vy-platine-cadre {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}
.vy-platine-bras {
  position: absolute;
  left: 29.34%;
  top: -6.67%;
  width: 80%;
  height: 80%;
  overflow: visible;
  transform-origin: 77.08% 20.83%;
  transform: rotate(-38deg);
  opacity: 0;
  color: var(--vy-platine-bras);
  fill: none;
  stroke: currentColor;
  stroke-width: .62;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter: drop-shadow(0 2px 2px rgba(0, 0, 0, .45));
}
.vy-platine.is-lecture .vy-platine-bras { opacity: 1; transform: rotate(-22.9deg); }
/* Le bras photographié remplace le dessin une fois chargé (cf. platine.js). */
.vy-platine-bras.a-photo > :not(.vy-bras-photo) { display: none; }
.vy-platine-onde {
  position: absolute;
  left: 80%;
  top: 67%;
  width: 18%;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 2px solid var(--vy-platine-onde);
  transform: translate(-50%, -50%) scale(.2);
  opacity: 0;
}
/* Le héros qui se replie (lumiere.js) : le disque suit la pochette. */
.album-hero.is-replie .vy-platine {
  scale: calc(1 - var(--vy-hero-p, 0) * .14);
  translate: 0 calc(var(--vy-hero-p, 0) * 7%);
}

/* ── 3 · Les tuiles : la pochette glisse et découvre son disque ─────────── */
/* Le disque est un pseudo-élément de la boîte (aucun nœud de plus par
   tuile), posé DERRIÈRE l'image : la boîte isole son empilement pour que le
   `z-index: -1` reste au-dessus de son propre fond. Il est décalé vers la
   droite, là où la pochette le découvre. Le retour de la pochette est un
   ressort (une courbe `linear()` échantillonnée ; une courbe de Bézier qui
   dépasse à défaut). */
.tiles:not(.is-list) .tile-cover { isolation: isolate; }
.tiles:not(.is-list) .tile-cover::before {
  content: "";
  position: absolute;
  top: 3%;
  bottom: 3%;
  left: 16%;
  right: -10%;
  z-index: -1;
  border-radius: 50%;
  background:
    radial-gradient(circle closest-side,
      var(--vy-bg) 0 4%, var(--vy-accent) 4.5% 34%,
      var(--vy-platine-lisse) 34.5% 40%, transparent 40.5% 96%,
      var(--vy-platine-rim) 97% 100%, transparent 100%),
    conic-gradient(from 30deg,
      var(--vy-platine-lueur) 0 5%, transparent 11% 50%,
      var(--vy-platine-lueur-2) 55%, transparent 61% 100%),
    repeating-radial-gradient(circle closest-side,
      var(--vy-platine-corps) 0 2.4%, var(--vy-platine-sillon) 2.7% 3.3%),
    var(--vy-platine-corps);
  opacity: 0;
  transform: rotate(0deg);
  transition: opacity 160ms var(--vy-ease-out), transform 700ms cubic-bezier(.2, .7, .2, 1);
  pointer-events: none;
}
/* L'appui long sur une pochette est à l'effet, jamais au menu d'image de
   Safari (même patron que le bac, le nuage et la bibliothèque). */
.tiles:not(.is-list) .tile-cover img {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.tiles:not(.is-list) .tile-cover > img {
  transform-origin: 20% 80%;
  box-shadow: 4px 0 10px -4px rgba(0, 0, 0, .45);
  transition: transform 660ms cubic-bezier(.34, 1.56, .64, 1);
}
@supports (transition-timing-function: linear(0, 1)) {
  .tiles:not(.is-list) .tile-cover > img {
    transition-timing-function: linear(0, .058, .197, .373, .554, .719, .855, .958, 1.029, 1.072,
      1.091, 1.094, 1.086, 1.071, 1.054, 1.037, 1.022, 1.01, 1.002, .996, .992, .991, 1);
  }
}
.tiles:not(.is-list) .tile-cover.is-devoile > img {
  transform: translateX(-16%) rotate(-2.5deg);
  transition: transform 420ms var(--vy-ease-out);
}
.tiles:not(.is-list) .tile-cover.is-devoile::before { opacity: 1; transform: rotate(90deg); }
/* La fiche s'ouvre depuis cette tuile : tout revient sans transition. */
.tiles:not(.is-list) .tile-cover.is-net > img,
.tiles:not(.is-list) .tile-cover.is-net::before { transition: none; }
.tiles:not(.is-list) .tile-cover.is-source-vol::before { visibility: hidden; }

/* ── 4 · Les attentes tournent sur une platine ───────────────────────── */
.vy-a-mini,
.vy-mini-rang {
  display: flex;
  align-items: center;
  gap: 10px;
}
.vy-mini {
  position: relative;
  flex: none;
  display: inline-block;
  width: 22px;
  height: 22px;
  /* Ne tourne pas, coupe au rond : la boîte du disque qui tourne ne
     déborde jamais de la rangée (même règle que le plateau de la fiche). */
  border-radius: 50%;
  overflow: hidden;
}
.vy-mini-disque,
.vy-mini-reflet {
  position: absolute;
  inset: 0;
  border-radius: 50%;
}
/* Un coin clair qui tourne avec lui : à 22 px, c'est ce qui se voit
   tourner. */
.vy-mini-disque {
  background:
    radial-gradient(circle closest-side,
      transparent 0 44%, var(--vy-platine-sillon) 46% 50%, transparent 52% 68%,
      var(--vy-platine-sillon) 70% 74%, transparent 76% 94%,
      var(--vy-platine-rim) 95% 100%, transparent 100%),
    conic-gradient(from 0deg, var(--vy-platine-lueur) 0 8%, transparent 14% 100%),
    var(--vy-platine-corps);
  will-change: transform;
}
.vy-mini-etiquette {
  position: absolute;
  left: 30%;
  top: 30%;
  width: 40%;
  height: 40%;
  overflow: visible;
}
.vy-mini-reflet {
  background: conic-gradient(from 300deg,
    transparent 0 4%, var(--vy-platine-lueur-2) 7%, transparent 11% 100%);
  pointer-events: none;
}

/* ── 5 · La collection vide ────────────────────────────────────────────── */
/* L'illustration remplace l'icône (le script la pose à côté, la classe
   efface l'icône) ; l'écran court la retire comme il retirait l'icône.
   Une boîte aux proportions de la photo (1353 × 820, du bord gauche du
   carton au bord droit du disque), deux images posées en pourcentages de
   cette boîte : les positions sortent de `tools/vinyles_images.py vide`, qui
   les imprime. Le disque est dessous, la pochette dessus. L'ombre que le
   carton portait sur le disque est ici, FIXE avec lui : cuite dans l'image
   du disque, elle glisserait avec lui quand il sort. */
#colEmpty.vy-illustre > .empty-ico { display: none; }
.vy-vide {
  position: relative;
  display: block;
  width: 200px;
  aspect-ratio: 1353 / 820;
}
.vy-vide-sortie {
  position: absolute;
  left: 39.64%;
  top: -0.24%;
  width: 60.5%;
  aspect-ratio: 1;
}
.vy-vide-pochette,
.vy-vide-disque {
  display: block;
  width: 100%;
  height: 100%;
  -webkit-user-drag: none;
  user-select: none;
}
.vy-vide-disque { filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .18)); }
.vy-vide-pochette {
  position: absolute;
  left: 0;
  top: 0;
  width: 63.69%;
  border-radius: 1px;
  filter: drop-shadow(1px 2px 3px rgba(0, 0, 0, .26));
}
/* Sur le fond sombre, un disque noir n'a plus de bord : un filet de lumière
   le lui rend, comme le liseré de la platine (`--vy-platine-rim`). */
:root[data-theme="dark"] .vy-vide-disque {
  filter: drop-shadow(0 0 1px rgba(255, 255, 255, .28)) drop-shadow(0 2px 3px rgba(0, 0, 0, .5));
}
@media (max-height: 600px) {
  .vy-vide { display: none; }
}

/* ── Moins de mouvement ────────────────────────────────────────────────── */
/* Le disque de la fiche dépasse, immobile (le script ne le fait ni sortir
   ni tourner) ; les tuiles ne découvrent rien. */
@media (prefers-reduced-motion: reduce) {
  .tiles:not(.is-list) .tile-cover.is-devoile > img { transform: none; }
  .tiles:not(.is-list) .tile-cover.is-devoile::before { opacity: 0; transform: none; }
}
:root[data-mouvement="reduit"] .tiles:not(.is-list) .tile-cover.is-devoile > img { transform: none; }
:root[data-mouvement="reduit"] .tiles:not(.is-list) .tile-cover.is-devoile::before { opacity: 0; transform: none; }

/* Sans photo, la vignette de remplacement dessine déjà son disque (app.js,
   placeholder) : dans la fiche, c'est le VRAI qui dépasse de la pochette, on
   n'en garde qu'un. Les initiales restent : elles tiennent lieu de pochette. */
.album-cover-btn.vy-a-platine .ph-big .ph-disc,
.album-cover-btn.vy-a-platine .ph-big .ph-groove,
.album-cover-btn.vy-a-platine .ph-big .ph-label,
.album-cover-btn.vy-a-platine .ph-big .ph-hole { display: none; }

/* ── 6 · Le tirage : le même vinyle que la fiche ─────────────────────────── */
/* platine.js pose la fabrique commune dans la platine du tirage et la
   marque `vy-a-platine` : l'ancien disque plat (le SVG de la page) se
   cache, le vinyle prend la case de la platine (la même que la pochette
   qui monte, `grid-area` de `.random-platine > *`). `is-range` tient l'état
   final de son effacement : l'animation de platine.js ne fait que l'y
   amener, rien n'est tenu à la fin. */
.random-platine.vy-a-platine > .random-disc { display: none; }
.random-platine.vy-a-platine > .vy-platine-tirage {
  position: relative;
  left: auto;
  top: auto;
  width: 100%;
}
.vy-platine-tirage.is-range { opacity: 0; transform: scale(.86); }


/* ── 18.4 · La scène : scene.js ─────────────────────────────────────────── */
/* scene.js pose trois choses, et presque toutes vivent dans des animations
   WAAPI qu'il lance et efface lui-même (l'arrivée de l'app, la révélation au
   défilement) : rien à déclarer ici pour elles, et rien ne tourne au repos.
   Ce bloc ne porte que les deux éléments qu'il ajoute au document. */

/* L'interrupteur caché qui donne le « tic » haptique de Safari (iOS 17.4 et
   plus : un `<input type="checkbox" switch>` basculé par son étiquette vibre
   sous le doigt). Jamais `display: none` : Safari ne donne alors aucune
   vibration. Hors écran, invisible, sans pointeur, et en haut de la page (un
   élément fixé en bas devrait sinon réserver la zone sûre). */
.vy-haptique {
  position: fixed;
  top: 0;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}

/* La note des Paramètres, montrée quand le téléphone demande de réduire les
   animations et que le réglage suit l'appareil : c'est elle qui dit
   pourquoi rien ne bouge, et où le changer. */
.set-mvt-note {
  color: var(--vy-accent-texte);
}

.set-mvt-note[hidden] {
  display: none;
}

/* ── 18.5 · L'accueil : accueil.js ──────────────────────────────────────── */
/* La porte d'entrée : ce que le module ajoute à l'accueil, et les états
   qu'il pose. Les arrivées (titre, pochettes, fiche, pistes) sont des
   animations WAAPI qu'il lance et efface lui-même : rien à déclarer ici pour
   elles. */

/* Le ruban de la tête de lecture : un soupçon d'accent. `color-mix()` vit
   sur la propriété et son repli aussi (Safari avant 16.2) : posé en seconde
   déclaration de `background`, il serait accepté à l'analyse puis invalide
   au calcul, et le ruban perdrait son fond. */
:root {
  --acc-tete-fond: color-mix(in srgb, var(--vy-accent) 7%, transparent);
}
@supports not (color: color-mix(in srgb, red, blue)) {
  :root { --acc-tete-fond: rgba(0, 0, 0, .035); }
  :root[data-theme="dark"] { --acc-tete-fond: rgba(255, 255, 255, .05); }
}

/* Le titre découpé en mots : une boîte par mot, pour qu'un transform s'y
   applique. Visible, une boîte en ligne garde la ligne de base de son
   texte : rien ne bouge à l'écran. */
.acc-mot { display: inline-block; }

/* Le disque de la pochette de devant. Il vit DANS la carte : `z-index: -1`
   dans le contexte d'empilement qu'elle ouvre (son `will-change`) le peint
   au-dessus de son fond et sous sa pochette et son voile. La carte cesse de
   rogner ce qui dépasse (elle rognait pour ses coins, à trois pixels
   près). Il sort par une transition, en léger dépassement, et rentre plus
   vite qu'il n'est sorti. */
#vitrineScene .pile-card.acc-carte { overflow: visible; }
.pile-card > .acc-disque {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  z-index: -1;
  transform: translateX(0);
  transition: transform .3s cubic-bezier(.4, 0, .7, 1);
}
.pile-card.acc-sorti > .acc-disque {
  transform: translateX(40%);
  transition: transform .64s cubic-bezier(.3, 1.35, .5, 1);
}

/* La teinte de la pochette de devant, derrière toute la vitrine : deux
   calques dont l'opacité se croise. La couleur se pose sur le calque
   éteint, elle ne s'anime jamais. */
.acc-teinte {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}
/* Sur téléphone, la pochette de devant est en haut à gauche de la vitrine
   (le titre au-dessus, la fiche en dessous) ; à partir de 900px, à gauche au
   milieu de la scène. La force est posée par le script selon la clarté de
   la couleur (`--acc-force-clair`, `--acc-force-sombre`). */
.acc-teinte-calque {
  --acc-ton: transparent;
  --acc-force-clair: .12;
  --acc-force-sombre: .2;
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 80% 30% at 28% 30%, var(--acc-ton), transparent 72%);
  opacity: 0;
  transition: opacity 1.1s var(--vy-ease-out);
}
@media (min-width: 900px) {
  .acc-teinte-calque { background: radial-gradient(ellipse 60% 62% at 26% 52%, var(--acc-ton), transparent 72%); }
}
/* Au-delà de 1180px la vitrine ne touche plus le bord de l'écran : sa
   teinte, encore colorée à son bord gauche, y traçait un trait vertical net
   (le défaut du halo du bac, masqué plus bas). Elle s'y fond. */
@media (min-width: 1181px) {
  .acc-teinte {
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 16%);
    mask-image: linear-gradient(90deg, transparent, #000 16%);
  }
}
.acc-teinte-calque.is-on { opacity: var(--acc-force-clair); }
:root[data-theme="dark"] .acc-teinte-calque.is-on { opacity: var(--acc-force-sombre); }
/* Dans la vitrine, la teinte fait la lumière : le halo du bac, rogné par la
   scène, y dessinait un bord net sous les pochettes. */
.vitrine .pile-aura { display: none; }

/* Le bouton lecture/pause de la vitrine, au bout du curseur : un rond, un
   arc d'accent qui tourne tant que les pochettes défilent. */
.vitrine .pile-foot { grid-template-areas: "range play" "caption count" "note note" "mvt mvt"; }
.vitrine .pile-range { align-self: center; }
.vitrine-lecture {
  grid-area: play;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  width: 44px;
  height: 44px;
  margin: -8px 0;   /* la cible de 44px, sans grandir la ligne du curseur (28px) */
  padding: 0;
  border: 1px solid var(--vy-line-strong);
  border-radius: 50%;
  background: var(--vy-card);
  color: var(--vy-ink);
  cursor: pointer;
}
.vitrine-lecture .ico { width: 18px; height: 18px; }
.vitrine-lecture .ico-play { fill: currentColor; stroke: none; }
.vitrine-lecture[data-etat="lecture"] .ico-play,
.vitrine-lecture:not([data-etat="lecture"]) .ico-pause { display: none; }
.vitrine-lecture::before {
  content: "";
  position: absolute;
  inset: -3px;
  border: 2px solid transparent;
  border-top-color: var(--vy-accent);
  border-radius: 50%;
  opacity: 0;
  transition: opacity .3s ease;
}
.vitrine-lecture[data-etat="lecture"]::before {
  opacity: 1;
  animation: acc-tourne 3.6s linear infinite;
}
.vitrine-lecture:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 3px; }
@keyframes acc-tourne { to { transform: rotate(360deg); } }

/* En pause (voulue, ou après un geste) : tout ce qui bouge seul sur
   l'accueil s'arrête. Hors de l'écran, l'arc du bouton aussi. */
.landing.acc-immobile .vy-fond-disc { animation-play-state: paused; }
.landing.acc-hors-champ .vitrine-lecture::before { animation-play-state: paused; }

/* La proposition, quand le téléphone réduit les animations. */
.acc-mvt {
  grid-area: mvt;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  margin: 8px 0 0;
  font-size: calc(13px * var(--vy-k));
  line-height: 1.4;
  color: var(--vy-muted);
}
.acc-mvt-actions { display: inline-flex; flex-wrap: wrap; gap: 6px 8px; }
.acc-mvt-voir {
  min-height: 44px;
  padding: 0 16px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: var(--vy-fill);
  color: var(--vy-accent-texte);
  font: 600 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  cursor: pointer;
}
.acc-mvt-discret { border-color: transparent; background: transparent; color: var(--vy-muted); }

/* Les champs de mot de passe : l'œil dans le champ, une cible de 44px. */
.mdp { position: relative; display: block; min-width: 0; }
.field .mdp input { padding-right: 50px; }
.mdp-oeil {
  position: absolute;
  top: 0;
  right: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 100%;
  min-height: 44px;
  padding: 0;
  border: 0;
  border-radius: 0 var(--vy-r-1) var(--vy-r-1) 0;
  background: transparent;
  color: var(--vy-muted);
  cursor: pointer;
}
.mdp-oeil .ico { width: 20px; height: 20px; }
.mdp-oeil[aria-pressed="true"] { color: var(--vy-ink); }
.mdp-oeil[aria-pressed="true"] .ico-oeil,
.mdp-oeil:not([aria-pressed="true"]) .ico-oeil-barre { display: none; }
.mdp-oeil:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: -3px; }

/* La jauge des six caractères, et la coche de la seconde saisie. */
.mdp-aide { display: flex; align-items: center; gap: 8px; }
.mdp-aide-texte { min-width: 0; }
.mdp-jauge { display: inline-flex; flex: none; gap: 3px; }
.mdp-jauge i {
  display: block;
  width: 7px;
  height: 11px;
  border-radius: 2px;
  background: var(--vy-line-strong);
  transform-origin: 50% 100%;
  /* Pas de fondu de couleur : c'est le petit saut en hauteur (scaleY, posé
     par accueil.js) qui marque le trait qui s'allume. La doctrine du bloc :
     transform et opacité seulement. */
}
.mdp-jauge i.is-on { background: var(--vy-ink-2); }
.mdp-aide.is-ok .mdp-jauge i { background: var(--vy-accent); }
.mdp-aide.is-ok,
.mdp-accord.is-ok { color: var(--vy-accent-texte); }
.mdp-accord.is-non { color: var(--vy-rouge-texte); }
/* Sa ligne est réservée, même vide : la coche qui paraît à la frappe ne
   pousse ni le bouton d'envoi ni, sur ordinateur (fiche ancrée par le bas),
   le champ où l'on tape. */
.mdp-accord { display: flex; align-items: center; gap: 6px; min-height: 1.45em; }
.mdp-coche { flex: none; width: 16px; height: 16px; }

/* Le bouton qui attend la réponse : le texte garde sa place (transparent),
   un sillon tourne au milieu. */
.btn-main.is-occupe { position: relative; color: transparent; }
.btn-main.is-occupe::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border-radius: 50%;
  background: conic-gradient(from 0deg, transparent 0 35%, var(--vy-btn-fg) 100%);
  -webkit-mask: radial-gradient(circle closest-side, transparent 0 46%, #000 50% 100%);
  mask: radial-gradient(circle closest-side, transparent 0 46%, #000 50% 100%);
  animation: acc-tourne .8s linear infinite;
}

/* L'envol : la pochette photographiée à l'envoi, qui grandit vers le
   visiteur pendant que l'app arrive. Sans animation, elle est invisible. */
.acc-envol {
  position: fixed;
  z-index: 90;
  overflow: hidden;
  border-radius: 3px;
  box-shadow: var(--vy-ombre-bac);
  opacity: 0;
  pointer-events: none;
  transform-origin: 50% 50%;
}
.acc-envol > .tile-cover { position: absolute; inset: 0; width: 100%; height: 100%; }

/* La tête de lecture du dos : un ruban sous la piste la plus proche du
   milieu de l'écran, qui glisse d'une piste à l'autre. */
.pistes { position: relative; }
.piste { position: relative; z-index: 1; }
.acc-tete {
  position: absolute;
  top: 0;
  left: -10px;
  right: -10px;
  height: 100px;
  list-style: none;
  border-left: 2px solid var(--vy-accent);
  background: var(--acc-tete-fond);
  opacity: 0;
  pointer-events: none;
  transform-origin: 0 0;
  transition: transform .5s cubic-bezier(.3, 1.2, .5, 1), opacity .3s ease;
}
.acc-tete.is-on { opacity: 1; }
.piste.is-lue .piste-n { color: var(--vy-accent-texte); }

@media (prefers-reduced-motion: reduce) {
  .pile-card > .acc-disque,
  .pile-card.acc-sorti > .acc-disque,
  .acc-teinte-calque,
  .acc-tete { transition: none; }
  .vitrine-lecture[data-etat="lecture"]::before,
  .btn-main.is-occupe::after { animation: none; }
}


/* L'aperçu d'un disque proposé (panelPreview) */
.preview-head { display: grid; gap: 16px; justify-items: center; text-align: center; margin-bottom: 16px; }
.preview-head .album-cover { width: min(66vw, 300px); }
.preview-title { margin: 0 0 4px; font-family: var(--vy-titre); font-size: calc(22px * var(--vy-k)); }
.preview-meta, .preview-reason { margin: 0 0 6px; color: var(--vy-muted); font-size: calc(14px * var(--vy-k)); }
.preview-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.preview-actions .btn { flex: 1 1 140px; justify-content: center; }
.preview-listen { flex-basis: 100%; }
.preview-search { margin-bottom: 18px; }


/* ── 18.6 · La visite : visite.js ───────────────────────────────────────── */
/* Cinq scènes sous la vitrine. La feuille dessine chaque démonstration dans
   son état FINAL ; `.is-avant` (posé une image par visite.js, sans
   transition) est l'état de départ, et les transitions ci-dessous, avec
   leurs délais, sont la chorégraphie. Rien n'est donc caché tant que rien
   ne joue, et rien n'est tenu par une animation. */
.visite {
  display: grid;
  gap: 64px;
  width: 100%;
  max-width: 1180px;
  margin: 0 auto;
  padding: 64px 20px 12px;
}
.visite-head { display: grid; gap: 10px; max-width: 46ch; }
.visite-titre {
  margin: 0;
  font-family: var(--vy-titre);
  font-size: clamp(30px, 6.4vw, 46px);
  font-weight: 600;
  line-height: 1.04;
  letter-spacing: -0.035em;
}
.visite-chapo { margin: 0; font-size: calc(15px * var(--vy-k)); line-height: 1.55; color: var(--vy-muted); }

.scene-v { display: grid; gap: 24px; align-items: center; }
.scene-texte { display: grid; gap: 12px; max-width: 46ch; }
.scene-n {
  margin: 0;
  font-family: var(--vy-ui);
  font-size: calc(12px * var(--vy-k));
  font-weight: 500;
  color: var(--vy-accent-texte);
}
.scene-titre {
  margin: 0;
  font-family: var(--vy-titre);
  font-size: clamp(25px, 4.8vw, 34px);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.025em;
}
.scene-p { margin: 0; font-size: calc(15.5px * var(--vy-k)); line-height: 1.6; color: var(--vy-ink-2); }
.scene-points { display: grid; margin: 4px 0 0; padding: 0; list-style: none; border-top: 1px solid var(--vy-line); }
.scene-points li {
  position: relative;
  padding: 9px 0 9px 22px;
  border-bottom: 1px solid var(--vy-line);
  font-size: calc(13.5px * var(--vy-k));
  color: var(--vy-muted);
}
.scene-points li::before {
  content: "";
  position: absolute;
  left: 1px;
  top: calc(50% - 4.5px);
  width: 9px;
  height: 9px;
  border: 1.5px solid var(--vy-accent);
  border-radius: 50%;
  box-sizing: border-box;
}

.scene-demo {
  position: relative;
  isolation: isolate;
  display: grid;
  place-items: center;
  aspect-ratio: 5 / 4;
  min-width: 0;
  overflow: hidden;
  border: 1px solid var(--vy-line);
  border-radius: var(--vy-r-2);
  background: var(--vy-paper-2);
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
}
.scene-demo img { display: block; width: 100%; height: 100%; object-fit: cover; }
.scene-v.is-avant,
.scene-v.is-avant *,
.scene-v.is-avant *::after { transition: none !important; }

@media (min-width: 900px) {
  .visite { gap: 96px; padding-top: 88px; }
  .scene-v { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 64px; }
  .scene-v:nth-of-type(even) .scene-texte { order: 2; }
}

/* 01 · Le bac : le vrai composant, qui ne prend pas le doigt (la page
   défile par-dessus). La scène et la légende se partagent la boîte. */
.vf-demo {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  background: radial-gradient(ellipse at 50% 34%, var(--vy-paper), var(--vy-paper-2) 72%);
}
.vf-pile { flex: 1 1 auto; min-height: 0; margin: 0; }
.vf-stage { height: 100%; cursor: default; touch-action: auto; }
.vf-legende {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 0 16px 16px;
  text-align: center;
}
.vf-legende b {
  font-family: var(--vy-titre);
  font-size: calc(15px * var(--vy-k));
  font-weight: 600;
}
.vf-legende span { font-size: calc(12.5px * var(--vy-k)); color: var(--vy-muted); }

/* 02 · Ajouter : un viseur, toujours sombre comme celui d'un appareil. Les
   coins se resserrent sur la pochette, la photo se redresse, un éclair, et
   la pastille dit ce qui a été reconnu. */
.va-demo { border-color: transparent; background: radial-gradient(circle at 28% 18%, #2c2a27, #121211 72%); }
.va-viseur { position: absolute; inset: 0; display: grid; place-items: center; }
.va-cadre { position: relative; width: 50%; aspect-ratio: 1; margin-bottom: 9%; }
.va-photo {
  border-radius: 2px;
  box-shadow: 0 22px 44px -20px rgba(0, 0, 0, .8);
  transition: transform .85s cubic-bezier(.2, .8, .2, 1) .9s;
}
.is-avant .va-photo { transform: translate(7%, 5%) rotate(-9deg) scale(.82); }
.va-coin {
  position: absolute;
  width: 20%;
  height: 20%;
  border: 3px solid var(--vy-accent);
  transition: transform .6s cubic-bezier(.2, .8, .2, 1) .25s;
}
.va-hg { left: -7px; top: -7px; border-right: 0; border-bottom: 0; border-top-left-radius: 6px; }
.va-hd { right: -7px; top: -7px; border-left: 0; border-bottom: 0; border-top-right-radius: 6px; }
.va-bg { left: -7px; bottom: -7px; border-right: 0; border-top: 0; border-bottom-left-radius: 6px; }
.va-bd { right: -7px; bottom: -7px; border-left: 0; border-top: 0; border-bottom-right-radius: 6px; }
.is-avant .va-hg { transform: translate(-46%, -40%); }
.is-avant .va-hd { transform: translate(46%, -40%); }
.is-avant .va-bg { transform: translate(-46%, 40%); }
.is-avant .va-bd { transform: translate(46%, 40%); }
.va-flash { position: absolute; inset: 0; background: #fff; opacity: 0; }
.va-chip {
  position: absolute;
  left: 50%;
  bottom: 8%;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  max-width: 92%;
  margin: 0;
  padding: 9px 15px 9px 11px;
  border-radius: 999px;
  background: #fbfaf7;
  color: #0a0a0a;
  font-size: calc(13px * var(--vy-k));
  font-weight: 600;
  white-space: nowrap;
  transform: translateX(-50%);
  transition: transform .55s cubic-bezier(.2, 1.3, .4, 1) 2.05s, opacity .3s ease 2.05s;
}
.va-chip .ico { flex: none; width: 17px; height: 17px; color: #2f8f66; }
.is-avant .va-chip { opacity: 0; transform: translate(-50%, 16px) scale(.94); }

/* 03 · Ranger : deux étagères ; le disque quitte le jazz pour le rayon créé
   à la main (visite.js le fait voler), puis son badge paraît. */
.vr-demo { align-content: center; gap: 22px; padding: 7% 9%; place-items: stretch; }
.vr-rayon { display: grid; gap: 9px; }
.vr-nom {
  display: flex;
  justify-content: space-between;
  margin: 0;
  font-family: var(--vy-ui);
  font-size: calc(12px * var(--vy-k));
  font-weight: 500;
  color: var(--vy-muted-3);
}
.vr-perso .vr-nom { color: var(--vy-accent-texte); }
.vr-rang {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 7%;
  padding: 0 5% 11px;
}
.vr-rang::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 7px;
  border-radius: 2px;
  background: linear-gradient(var(--vy-surface-3), var(--vy-line-strong));
}
.vr-p { position: relative; aspect-ratio: 1; }
.vr-p img { box-shadow: var(--vy-ombre-pochette); transform: perspective(520px) rotateY(-16deg); transform-origin: left center; }
.vr-trou { border-radius: 2px; outline: 1.5px dashed var(--vy-line-strong); outline-offset: -1px; }
.vr-volant { z-index: 2; }
.vr-badge {
  position: absolute;
  left: 4%;
  bottom: 6%;
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--vy-accent-plein);
  color: var(--vy-on-accent);
  font-size: calc(10px * var(--vy-k));
  font-weight: 600;
  white-space: nowrap;
  transition: transform .45s cubic-bezier(.2, 1.4, .4, 1) 2.25s, opacity .25s ease 2.25s;
}
.is-avant .vr-badge { opacity: 0; transform: scale(.6); }

/* 04 · Écouter : la pochette, le vinyle de platine.js qui en sort et
   tourne, le bras qui se pose, puis la proposition d'écoute. */
.ve-demo {
  align-content: center;
  gap: 9%;
  padding: 7% 0 6%;
  background: radial-gradient(ellipse at 42% 30%, var(--vy-accent-doux), transparent 68%), var(--vy-paper-2);
}
.ve-platine { position: relative; width: 40%; aspect-ratio: 1; margin-right: 17%; --vy-pochette: 100%; --vy-halo: #5cc8ff; }
.ve-platine .vy-platine { transform: translateX(40%); transition: transform .9s cubic-bezier(.2, .8, .2, 1) .15s; }
.is-avant .ve-platine .vy-platine { transform: none; }
.ve-pochette { position: relative; z-index: 1; width: 100%; height: 100%; box-shadow: var(--vy-ombre-hero); }
.ve-bras {
  position: absolute;
  z-index: 2;
  left: 86%;
  top: -14%;
  width: 66%;
  height: 66%;
  color: var(--vy-ink-2);
  transform-origin: 82% 14%;
  transition: transform .9s cubic-bezier(.3, .9, .3, 1) 1.15s;
}
.is-avant .ve-bras { transform: rotate(-32deg); }
.ve-bras-tube,
.ve-bras-contre { fill: none; stroke: currentColor; stroke-linecap: round; }
.ve-bras-tube { stroke-width: 3.4; }
.ve-bras-contre { stroke-width: 7; }
.ve-bras-tete,
.ve-bras-socle { fill: currentColor; }
.ve-bras-palier { fill: var(--vy-paper-2); }
.ve-notif {
  display: flex;
  align-items: center;
  gap: 12px;
  width: min(84%, 340px);
  padding: 7px;
  border-radius: 999px;
  background: #161614;
  color: #f5f4f1;
  box-shadow: 0 18px 40px -20px rgba(0, 0, 0, .6), 0 0 0 1px rgba(255, 255, 255, .06);
  transition: transform .6s cubic-bezier(.2, 1.25, .4, 1) 1.85s, opacity .3s ease 1.85s;
}
.is-avant .ve-notif { opacity: 0; transform: translateY(24px) scale(.92); }
.ve-notif-p { flex: none; width: 42px !important; height: 42px !important; border-radius: 50%; }
.ve-notif-t { display: grid; flex: 1 1 auto; gap: 1px; min-width: 0; margin: 0; }
.ve-notif-t span { font-size: calc(11px * var(--vy-k)); color: rgba(245, 244, 241, .74); }
.ve-notif-t b { overflow: hidden; font-size: calc(14px * var(--vy-k)); font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.ve-notif-b {
  position: relative;
  display: grid;
  flex: none;
  place-items: center;
  width: 42px;
  height: 42px;
  margin-right: 3px;
  border-radius: 50%;
  background: #f5f4f1;
  color: #161614;
}
.ve-notif-b .ico { width: 18px; height: 18px; }
.ve-anneau { position: absolute; inset: -5px; width: calc(100% + 10px); height: calc(100% + 10px); transform: rotate(-90deg); }
.ve-anneau circle { fill: none; stroke: #4ac3da; stroke-width: 2.4; stroke-linecap: round; stroke-dasharray: 113.1; stroke-dashoffset: 0; }

/* 05 · Découvrir : une proposition expliquée en chiffres, la jauge des
   200 qui se remplit, et un ajout qui passe dans le fil. */
.vd-demo { align-content: center; gap: 12px; padding: 6% 7%; place-items: stretch; }
.vd-carte,
.vd-200,
.vd-fil {
  margin: 0;
  border: 1px solid transparent;
  border-radius: var(--vy-r-2);
  background: var(--vy-groupe);
}
.vd-carte {
  display: grid;
  grid-template-columns: 38% minmax(0, 1fr);
  gap: 16px;
  align-items: center;
  padding: 14px;
  transition: transform .6s cubic-bezier(.2, .8, .2, 1) .15s, opacity .4s ease .15s;
}
.is-avant .vd-carte { opacity: 0; transform: translateY(18px); }
.vd-p { aspect-ratio: 1; height: auto !important; box-shadow: var(--vy-ombre-pochette); }
.vd-corps { display: grid; gap: 3px; min-width: 0; }
.vd-corps p { margin: 0; }
.vd-sur {
  font-family: var(--vy-ui);
  font-size: calc(11px * var(--vy-k));
  font-weight: 500;
  color: var(--vy-accent-texte);
}
.vd-titre { font-family: var(--vy-titre); font-size: clamp(17px, 2.2vw, 21px); font-weight: 600; letter-spacing: -0.02em; }
.vd-artiste { font-size: calc(12.5px * var(--vy-k)); color: var(--vy-muted); }
.vd-part { display: grid; gap: 4px; padding-top: 7px; font-size: calc(12px * var(--vy-k)); color: var(--vy-ink-2); }
.vd-part i { position: relative; display: block; height: 6px; overflow: hidden; border-radius: 3px; background: var(--vy-surface-3); }
.vd-part i::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 3px;
  background: var(--vy-accent);
  transform: scaleX(var(--p));
  transform-origin: left center;
  transition: transform .95s cubic-bezier(.2, .8, .2, 1) .75s;
}
.vd-i1 { --p: .62; }
.vd-i2 { --p: .38; }
.vd-i2::after { transition-delay: .95s !important; }
.is-avant .vd-part i::after { transform: scaleX(0); }
.vd-bas { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 12px; }
.vd-200 { display: flex; align-items: center; gap: 10px; padding: 10px 14px 10px 10px; font-size: calc(12.5px * var(--vy-k)); color: var(--vy-muted); }
.vd-200 b { font-family: var(--vy-titre); font-size: calc(17px * var(--vy-k)); color: var(--vy-ink); }
.vd-anneau { flex: none; width: 40px; height: 40px; transform: rotate(-90deg); }
.vd-anneau circle { fill: none; stroke-width: 4; }
.vd-anneau-fond { stroke: var(--vy-surface-3); }
.vd-anneau-plein {
  stroke: var(--vy-accent);
  stroke-linecap: round;
  stroke-dasharray: 119.4;
  stroke-dashoffset: 97.3;
}
.vd-fil {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 8px 12px 8px 8px;
  font-size: calc(13px * var(--vy-k));
  color: var(--vy-ink-2);
  transition: transform .55s cubic-bezier(.2, 1.2, .4, 1) 1.7s, opacity .3s ease 1.7s;
}
.vd-fil img { flex: none; width: 36px !important; height: 36px !important; }
.vd-fil span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vd-fil b { color: var(--vy-ink); }
.is-avant .vd-fil { opacity: 0; transform: translateX(22px); }

/* Au téléphone, deux démonstrations prennent un carré : le bac y garde une
   pochette lisible, et la carte de « Pour vous » ses deux lignes du bas.
   Posé APRÈS les règles qu'il corrige : une media query n'ajoute aucune
   priorité, seul l'ordre tranche. */
@media (max-width: 899px) {
  .vf-demo,
  .vd-demo { aspect-ratio: 1; }
  .vd-bas { grid-template-columns: minmax(0, 1fr); }
}

/* Les titres bonus du dos : ce que les deux faces n'ont pas la place de dire. */
.bonus { display: grid; gap: 2px; }
.bonus-liste { display: grid; gap: 0 32px; margin: 0; padding: 0; list-style: none; }
.bonus-liste li {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr);
  gap: 12px;
  align-items: start;
  padding: 12px 0;
  border-bottom: 1px solid var(--vy-line);
  font-size: calc(14.5px * var(--vy-k));
  line-height: 1.5;
  color: var(--vy-ink-2);
}
.bonus-liste .ico { width: 20px; height: 20px; margin-top: 1px; color: var(--vy-accent-texte); }
.bonus-liste b { font-weight: 600; color: var(--vy-ink); }
@media (min-width: 900px) {
  .bonus-liste { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* L'appel : un vinyle, une phrase, deux boutons. */
.appel {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 14px;
  width: 100%;
  max-width: 1180px;
  margin: 28px auto 0;
  padding: 60px 20px 56px;
  border-top: 1px solid var(--vy-line);
  background: radial-gradient(ellipse at 50% 0%, var(--vy-accent-doux), transparent 62%);
  text-align: center;
}
.appel-disque { position: relative; width: 128px; aspect-ratio: 1; margin-bottom: 8px; --vy-pochette: 100%; --vy-halo: var(--vy-accent); }
.appel-titre {
  margin: 0;
  font-family: var(--vy-titre);
  font-size: clamp(30px, 7vw, 50px);
  font-weight: 600;
  line-height: 1.04;
  letter-spacing: -0.035em;
}
.appel-p { max-width: 44ch; margin: 0; font-size: calc(15.5px * var(--vy-k)); line-height: 1.55; color: var(--vy-muted); }
.appel-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 8px; }
.appel-actions .btn { min-height: 44px; padding-inline: 22px; }
.appel-actions .btn[hidden] { display: none; }

/* ┌──────────────────────────────────────────────────────────────────────────
   │ 19 · LA PROPOSITION D'ÉCOUTE (octobre 2026) : ecoute.js
   │ En bas de l'écran, à la place de la barre d'onglets sur un téléphone :
   │ la capsule de la barre se pince puis se déploie en notification
   │ (pochette en disque, état, album, bouton rond cerclé du compte à
   │ rebours), puis redevient la barre. Sur un grand écran, la barre est une
   │ colonne : la notification monte du bas, au milieu du contenu.
   └────────────────────────────────────────────────────────────────────────── */
.vy-ecoute {
  position: fixed;
  z-index: 88;
  left: 12px;
  right: 12px;
  bottom: max(10px, calc(env(safe-area-inset-bottom, 0px) - 6px));
  max-width: 460px;
  min-height: 58px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr) 44px 44px;
  align-items: center;
  gap: 8px;
  padding: 6px;
  border-radius: 999px;
  background-color: var(--vy-verre-fond);
  background-image: linear-gradient(180deg, var(--vy-verre-speculaire) 0, transparent 48%);
  -webkit-backdrop-filter: blur(22px) saturate(1.7);
  backdrop-filter: blur(22px) saturate(1.7);
  box-shadow: var(--vy-verre-ombre), 0 0 0 .5px var(--vy-verre-bord), inset 0 1px 0 var(--vy-verre-lisere);
  color: var(--vy-ink);
  /* Glisser vers le bas la renvoie : c'est elle qui reçoit le geste. */
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
}
.vy-ecoute[hidden] { display: none; }
/* Pendant la métamorphose, c'est la capsule qui porte le verre, et la
   notification ne reçoit pas le doigt (aucun clic sur un bouton qui
   apparaît ou s'efface). */
.vy-ecoute.is-morph {
  pointer-events: none;
  background: transparent;
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.vy-ecoute-capsule {
  position: fixed;
  z-index: 87;
  border-radius: 999px;
  background-color: var(--vy-verre-fond);
  background-image: linear-gradient(180deg, var(--vy-verre-speculaire) 0, transparent 48%);
  -webkit-backdrop-filter: blur(22px) saturate(1.7);
  backdrop-filter: blur(22px) saturate(1.7);
  box-shadow: var(--vy-verre-ombre), 0 0 0 .5px var(--vy-verre-bord), inset 0 1px 0 var(--vy-verre-lisere);
  transform-origin: 50% 50%;
  pointer-events: none;
  will-change: transform, opacity;
}
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .vy-ecoute,
  .vy-ecoute-capsule { background-color: var(--vy-bg); }
}

/* La pochette en disque (le bord arrondi de la capsule rognerait un carré),
   percée en son centre comme un vinyle. */
.vy-ecoute-pochette {
  position: relative;
  display: grid;
  place-items: center;
  width: 46px;
  height: 46px;
  overflow: hidden;
  border-radius: 50%;
  background: var(--vy-line);
  color: var(--vy-ink-2);
}
.vy-ecoute-pochette img { width: 100%; height: 100%; object-fit: cover; }
.vy-ecoute-pochette .ico { width: 22px; height: 22px; }
.vy-ecoute-pochette::after {
  content: "";
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--vy-bg);
  box-shadow: 0 0 0 1.5px rgba(10, 10, 10, .18);
}

/* Trois lignes dans la hauteur de la pochette (46 px) : la notification a
   exactement la hauteur de la barre qu'elle remplace. */
.vy-ecoute-texte { display: grid; gap: 0; min-width: 0; }
.vy-ecoute-texte p {
  margin: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.vy-ecoute-statut { color: var(--vy-accent-texte); font-size: calc(11.5px * var(--vy-k)); font-weight: 600; line-height: 1.2; }
.vy-ecoute-titre { color: var(--vy-ink); font-size: calc(14.5px * var(--vy-k)); font-weight: 600; line-height: 1.15; }
.vy-ecoute-artiste { color: var(--vy-ink-2); font-size: calc(12.5px * var(--vy-k)); line-height: 1.2; }

/* Le bouton rond, cerclé de l'anneau qui se vide. */
.vy-ecoute-go,
.vy-ecoute-x {
  position: relative;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--vy-ink-2);
  cursor: pointer;
}
.vy-ecoute-go-ico {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--vy-accent-plein);
  color: var(--vy-on-accent);
  transition: opacity var(--vy-t) ease, transform var(--vy-dur-xs) var(--vy-ease-out);
}
.vy-ecoute-go-ico .ico { width: 16px; height: 16px; }
.vy-ecoute-go:active .vy-ecoute-go-ico { transform: scale(.92); }
.vy-ecoute-anneau {
  position: absolute;
  inset: 0;
  width: 44px;
  height: 44px;
  transform: rotate(-90deg);
  pointer-events: none;
}
.vy-ecoute-anneau circle { fill: none; stroke-width: 2.5; }
.vy-ecoute-piste { stroke: var(--vy-line); }
.vy-ecoute-reste {
  stroke: var(--vy-accent);
  stroke-linecap: round;
  stroke-dasharray: 100;
  stroke-dashoffset: 0;
}
/* Pendant la recherche : un arc qui tourne, et le bouton attend. */
.vy-ecoute.is-cherche .vy-ecoute-reste { stroke-dasharray: 26 74; }
.vy-ecoute.is-cherche .vy-ecoute-anneau { animation: vy-ecoute-tourne .9s linear infinite; }
.vy-ecoute.is-cherche .vy-ecoute-go { cursor: default; }
.vy-ecoute.is-cherche .vy-ecoute-go-ico { opacity: .45; }
@keyframes vy-ecoute-tourne {
  from { transform: rotate(-90deg); }
  to { transform: rotate(270deg); }
}
/* Le temps arrêté (sous le pointeur, au clavier) se voit : l'anneau pâlit. */
.vy-ecoute.is-compte.is-pause .vy-ecoute-reste { stroke: var(--vy-ink-2); }
.vy-ecoute-x .ico { width: 18px; height: 18px; }
@media (hover: hover) and (pointer: fine) {
  .vy-ecoute-x:hover { color: var(--vy-ink); }
}

/* Sur un téléphone, la notification EST la barre le temps de la proposition :
   la barre s'efface (en fondu, piloté par le script) et ne reçoit plus le
   doigt. */
@media (max-width: 899px) {
  html.vy-ecoute-cede .tabbar { opacity: 0; pointer-events: none; }
}
@media (max-width: 360px) {
  .vy-ecoute { left: 8px; right: 8px; }
}
/* Sur un grand écran, la barre est une colonne à gauche : la notification
   se pose en bas, au milieu du contenu. */
@media (min-width: 900px) {
  .vy-ecoute {
    left: 220px;
    right: 0;
    bottom: 22px;
    width: min(440px, calc(100% - 252px));
    max-width: none;
  }
}

/* ── 5g · Les panneaux du bac et le casier ────────────────── */
/* Les panneaux : quand la liste est triée par une chose qui range en
   catégories (lettre, décennie, genre…), bac.js glisse un panneau entre deux
   catégories, comme les séparateurs d'un vrai bac. Plein à l'accent : c'est
   le seul aplat de la scène, il se voit de loin. `--sep-fs` est posé par le
   script (la taille du nom suit celle de la pochette). */
.pile-sep {
  position: absolute;
  left: 0;
  top: 0;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  gap: 8px;
  padding: 12px;
  overflow: hidden;
  border-radius: 3px;
  background: var(--vy-accent-plein);
  color: var(--vy-on-accent);
  box-shadow: var(--vy-ombre-bac);
  transform-origin: 50% 50%;
  will-change: transform, opacity;
  backface-visibility: hidden;
  pointer-events: none;
}
.pile-sep::after {
  content: "";
  position: absolute;
  inset: 6px;
  border: 1px solid currentColor;
  border-radius: 2px;
  opacity: .35;
  pointer-events: none;
}
.pile-sep-lib {
  font-family: var(--vy-titre);
  font-weight: 700;
  font-size: var(--sep-fs, 20px);
  line-height: 1.05;
  letter-spacing: -.01em;
  overflow-wrap: anywhere;
}
.pile-sep-nb {
  font-family: var(--vy-ui);
  font-size: calc(12px * var(--vy-k));
}
/* Dans le casier le panneau est un INTERCALAIRE (octobre 2026, demande
   d'Antoine : « les panneaux de catégorie avec encoche pour marquer la
   catégorie ») : la taille d'une pochette, dont on ne voit que la tranche du
   haut, et un ONGLET qui dépasse au-dessus des tranches et porte le nom,
   comme les intercalaires de plastique d'un disquaire. Corps et onglet sont
   une seule pièce (`--sep-fond`) ; deux congés au pied de l'onglet font
   l'encoche. Le corps se voile avec la profondeur (`--vy-fade`, posé par
   bac.js), l'onglet reste net : c'est lui que l'oeil cherche. Les onglets
   alternent à gauche, au milieu et à droite (`data-onglet`, posé par
   bac.js) : deux intercalaires proches ne se cachent pas l'un l'autre. */
.pile-casier .pile-sep {
  --sep-fond: var(--vy-accent-plein);
  justify-content: flex-start;
  padding: 12px 14px;
  gap: 4px;
  overflow: visible;
  border-radius: 4px;
  background: var(--sep-fond);
}
.pile-casier .pile-sep::after { content: none; }
.pile-casier .pile-sep::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  background: var(--vy-bg);
  opacity: var(--vy-fade, 0);
  pointer-events: none;
}
.pile-sep-onglet {
  position: absolute;
  bottom: calc(100% - 1px);
  left: 7%;
  display: flex;
  align-items: center;
  max-width: 56%;
  height: var(--sep-onglet, 24px);
  padding: 0 calc(var(--sep-onglet, 24px) * .55);
  border-radius: calc(var(--sep-onglet, 24px) * .34) calc(var(--sep-onglet, 24px) * .34) 0 0;
  background: var(--sep-fond);
  color: var(--vy-on-accent);
}
.pile-sep-onglet::before,
.pile-sep-onglet::after {
  content: "";
  position: absolute;
  bottom: 0;
  width: 8px;
  height: 8px;
  pointer-events: none;
}
.pile-sep-onglet::before { right: 100%; background: radial-gradient(circle at 0 0, transparent 7.5px, var(--sep-fond) 8px); }
.pile-sep-onglet::after { left: 100%; background: radial-gradient(circle at 100% 0, transparent 7.5px, var(--sep-fond) 8px); }
.pile-casier .pile-sep[data-onglet="1"] .pile-sep-onglet { left: 50%; transform: translateX(-50%); }
.pile-casier .pile-sep[data-onglet="2"] .pile-sep-onglet { left: auto; right: 7%; }
.pile-casier .pile-sep-lib {
  min-width: 0;
  overflow: hidden;
  font-family: var(--vy-ui);
  font-weight: 650;
  line-height: 1;
  letter-spacing: -.01em;
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow-wrap: normal;
}
.pile-casier .pile-sep-nb { opacity: .8; }
/* Dans le Cover Flow le panneau est une lame étroite, tournée comme ses
   voisines, dont une bande seule dépasse : le nom s'y lit DANS LE SENS DE LA
   HAUTEUR, comme sur la tranche d'un disque rangé, et reste lisible tourné. */
.pile-flow:not(.pile-casier) .pile-sep {
  flex-direction: row;
  justify-content: center;
  align-items: center;
  gap: 8px;
  padding: 10px 6px;
}
.pile-flow:not(.pile-casier) .pile-sep-lib,
.pile-flow:not(.pile-casier) .pile-sep-nb {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  white-space: nowrap;
}

/* La catégorie du disque de devant, dans la légende. */
.pile-cat {
  justify-self: center;
  margin-bottom: 4px;
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--vy-accent-plein);
  color: var(--vy-on-accent);
  font-family: var(--vy-ui);
  font-size: calc(11px * var(--vy-k));
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: .02em;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  will-change: transform, opacity;
}
.pile-cat[hidden] { display: none; }

/* La barre des catégories (octobre 2026, cf. `rendreCats` de bac.js) :
   toutes les catégories du tri en cours, en pastilles, sous la scène du bac
   et du casier, celle du disque de devant marquée et ramenée au milieu. Elle
   remplace la pastille seule de la légende (qui reste dans le DOM pour le
   lecteur d'écran et les tests). Les pastilles font 32 px à l'oeil et 44 px
   au doigt (le `::after` étend la cible). La barre déborde jusqu'aux bords de
   l'écran et s'y fond. */
.pile-flow .pile-foot { grid-template-areas: "cats cats" "caption caption" "range count"; }
.pile-cats {
  grid-area: cats;
  display: flex;
  gap: 6px;
  min-width: 0;
  margin: 0 -16px;
  padding: 8px 16px 6px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 14px, #000 calc(100% - 14px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 14px, #000 calc(100% - 14px), transparent);
}
.pile-cats::-webkit-scrollbar { display: none; }
.pile-cats-b {
  position: relative;
  flex: none;
  min-height: 32px;
  padding: 6px 12px;
  border: 0;
  border-radius: 999px;
  background: var(--vy-fill);
  color: var(--vy-ink-2);
  font: 500 calc(13px * var(--vy-k))/1.2 var(--vy-ui);
  white-space: nowrap;
  cursor: pointer;
}
.pile-cats-b::after { content: ""; position: absolute; inset: -6px -3px; }
.pile-cats-b.is-on { background: var(--vy-accent-plein); color: var(--vy-on-accent); font-weight: 600; }
.pile-cats-b:focus-visible { outline: 2px solid var(--vy-accent); outline-offset: 2px; }
.pile.has-cats .pile-cat { display: none; }
.pile.is-plein .pile-cats { display: none; }
@media (hover: hover) and (pointer: fine) {
  .pile-cats-b:hover { background: var(--vy-fill-2); color: var(--vy-ink); }
  .pile-cats-b.is-on:hover { background: var(--vy-accent-plein); color: var(--vy-on-accent); }
}

/* Le casier : la scène laisse de l'air en haut, où les tranches s'étagent. */
.pile-casier .pile-card { border-radius: 3px; }
/* Sans perspective partagée (cf. `projeter` de bac.js), la scène du casier
   reste un contexte d'empilement. */
.pile-casier .pile-scene { z-index: 1; }
.pile-casier .pile-card.is-open { z-index: 1200 !important; }

/* ── 5h · La pochette en carton, et le verre liquide (octobre 2026) ─────────
   Le carton : jetons posés par bac.js (`habillerCarton`) : `--vy-age` (0 neuf,
   1 soixante-dix ans), `--vy-carton` (la couleur du carton, du blanc cassé au
   kraft jauni), `--vy-bord-carton` (le filet des deux flancs), `--vy-ep`
   (l'épaisseur du haut). Rien ne capte le pointeur : un tap sur la bande ou
   la face usée tombe sur la pochette, comme avant. */
.pile-casier .pile-card {
  border-radius: 3px;
  box-shadow: var(--vy-bord-carton, 0 0 0 transparent), var(--vy-ombre-bac);
}
.pile-casier .pile-card.is-hover { box-shadow: var(--vy-bord-carton, 0 0 0 transparent), var(--vy-ombre-bac-haut); }
.pile-carton {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(100% - 1px);
  height: var(--vy-ep, 6px);
  border-radius: 3px 3px 0 0;
  background:
    linear-gradient(to bottom, rgba(255, 255, 255, .42), rgba(255, 255, 255, 0) 55%),
    linear-gradient(to right, rgba(0, 0, 0, .16), rgba(0, 0, 0, 0) 12%, rgba(0, 0, 0, 0) 88%, rgba(0, 0, 0, .16)),
    var(--vy-carton, #e8e0cf);
  clip-path: polygon(1.1% 0, 98.9% 0, 100% 100%, 0 100%);
  pointer-events: none;
}
/* L'ombre de contact (octobre 2026) : la pochette de devant, posée sur le bas
   de la bande visible de sa voisine, y jette son ombre (`--vy-vu`, posée par
   bac.js : la hauteur de cette bande). Seuls les 18 derniers pixels de la bande
   s'assombrissent, vers la pochette qui les recouvre : quelle que soit la
   hauteur de la bande, le contact se voit. Rien ne capte le pointeur. */
.pile-occl {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: var(--vy-vu, 0px);
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom, rgba(48, 30, 6, 0) calc(100% - 18px), rgba(48, 30, 6, .34));
}
.pile-usure {
  --a: var(--vy-age, .4);
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  pointer-events: none;
  background:
    radial-gradient(circle at 0 0, rgba(176, 134, 58, calc(var(--a) * .55)), transparent 20%),
    radial-gradient(circle at 100% 0, rgba(176, 134, 58, calc(var(--a) * .5)), transparent 18%),
    radial-gradient(circle at 0 100%, rgba(176, 134, 58, calc(var(--a) * .6)), transparent 22%),
    radial-gradient(circle at 100% 100%, rgba(176, 134, 58, calc(var(--a) * .6)), transparent 20%),
    radial-gradient(circle at 50% 50%, transparent 0 45.5%, rgba(255, 255, 255, calc(var(--a) * .15)) 46.5% 48%, transparent 49%);
  box-shadow: inset 0 0 calc(3px + var(--a) * 12px) calc(var(--a) * 1px) rgba(192, 146, 64, calc(var(--a) * .62));
}

/* Le verre liquide : la barre d'outils, les pastilles de catégorie (la
   catégorie du disque de devant est la LENTILLE, comme l'onglet de la barre
   du bas), le bouton du plein écran. Même matière que la barre d'onglets
   (jetons de la section 18.1), accrochée à `html.vy-verre` : sans le script,
   les capsules remplies d'avant. La barre des catégories flotte sur le bas de
   la scène : les pochettes passent SOUS le verre, c'est ce qui le fait voir. */
html.vy-verre .toolbar .toolbar-tools,
html.vy-verre .pile-cats-b,
html.vy-verre .pile-plein-btn {
  background-color: var(--vy-verre-fond);
  background-image: linear-gradient(180deg, var(--vy-verre-speculaire) 0, transparent 62%);
  -webkit-backdrop-filter: blur(18px) saturate(1.7);
  backdrop-filter: blur(18px) saturate(1.7);
  box-shadow: var(--vy-lentille-ombre), 0 0 0 .5px var(--vy-verre-bord), inset 0 1px 0 var(--vy-verre-lisere);
}
/* Le verre fait de la capsule un contexte d'empilement : son menu (le « ••• »)
   doit rester au-dessus de la scène qui la suit dans la page. */
html.vy-verre .toolbar { position: relative; z-index: 6; }
html.vy-verre .pile-cats-b.is-on {
  background-color: var(--vy-lentille-fond);
  background-image: linear-gradient(180deg, var(--vy-lentille-reflet) 0, transparent 58%);
  color: var(--vy-ink);
  box-shadow: var(--vy-lentille-ombre), 0 0 0 .5px var(--vy-lentille-bord), inset 0 1px 0 var(--vy-lentille-lisere);
}
html.vy-verre .pile-cats-b.is-on:hover { background-color: var(--vy-lentille-fond); color: var(--vy-ink); }
html.vy-verre .pile-flow .pile-cats {
  position: relative;
  z-index: 4;
  margin-top: -38px;
  -webkit-mask-image: none;
  mask-image: none;
}
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  html.vy-verre .toolbar .toolbar-tools,
  html.vy-verre .pile-cats-b,
  html.vy-verre .pile-plein-btn { background-color: var(--vy-card); }
}

/* ── 5i · Épurer : l'en-tête sur une ligne, le pied du bac et du casier ─────
   Demande d'Antoine, capture annotée à l'appui : « simplifier l'UI : supprime
   ce que j'ai entouré en rouge, et le nombre de vinyles, mets-le à côté de
   Ma collection ». Rien n'est retiré du DOM (le rendu commun lit ces
   éléments par leur identifiant) : ce qui disparaît est caché pour l'oeil,
   et reste lisible pour un lecteur d'écran quand l'information en vaut la peine.
   · L'en-tête : le titre, le compteur à côté, « ••• » à droite, sur UNE ligne
     (avant : le titre, puis une barre à part). Le bouton « Filtrer » n'est plus
     dans la barre : « Filtrer » est une ligne du menu « ••• ».
   · Le pied du bac et du casier : la catégorie et l'artiste. Plus de curseur,
     de compteur, de titre ni de ligne d'édition sous la pochette ; le plein
     écran (`.is-plein`), qui a son propre curseur, n'est pas touché. */
.view-head > .toolbar {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
}
.view-head > .toolbar .count { align-self: flex-end; margin-bottom: 7px; }
.toolbar .toolbar-tools > [id$="Filter"] { display: none; }
.vmenu-badge {
  flex: none;
  min-width: 20px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--vy-accent-plein);
  color: var(--vy-on-accent);
  font: 600 calc(12px * var(--vy-k))/1.5 var(--vy-ui);
  text-align: center;
}
@media (max-width: 360px) {
  .view-head .view-title { font-size: 24px; }
}

.pile-flow:not(.is-plein) .pile-range { display: none; }
.pile-flow:not(.is-plein) .pile-title,
.pile-flow:not(.is-plein) .pile-meta,
.pile-flow:not(.is-plein) .pile-count {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}
.pile-flow:not(.is-plein) .pile-foot { grid-template-areas: "cats cats" "caption caption"; }

/* ── 5j · Les panneaux de la grille (octobre 2026) ──────────────────────────
   Demande d'Antoine, capture de la grille triée par genre : « au lieu de mettre
   les genres par-dessus la pochette de chaque vinyle, mets des panneaux pour
   indiquer le changement de genre, et enlève ce qu'il y a sur la pochette pour
   bien la voir ». Un panneau = l'onglet de l'intercalaire du casier (même
   pièce, mêmes congés au pied) posé sur un filet, le nombre de disques du
   groupe à l'autre bout. Il occupe toute la largeur de la grille, quel que
   soit le nombre de colonnes, et aussi la vue en liste. */
.tiles-sep {
  --sep-fond: var(--vy-accent-plein);
  grid-column: 1 / -1;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px;
  min-width: 0;
  margin-top: 10px;
  border-bottom: 2px solid var(--sep-fond);
}
.tiles-sep:first-child { margin-top: 0; }
.tiles-sep-onglet {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-width: 0;
  max-width: 72%;
  height: 28px;
  margin-left: 10px;
  padding: 0 14px;
  border-radius: 10px 10px 0 0;
  background: var(--sep-fond);
  color: var(--vy-on-accent);
}
.tiles-sep-onglet::before,
.tiles-sep-onglet::after {
  content: "";
  position: absolute;
  bottom: 0;
  width: 8px;
  height: 8px;
  pointer-events: none;
}
.tiles-sep-onglet::before { right: 100%; background: radial-gradient(circle at 0 0, transparent 7.5px, var(--sep-fond) 8px); }
.tiles-sep-onglet::after { left: 100%; background: radial-gradient(circle at 100% 0, transparent 7.5px, var(--sep-fond) 8px); }
.tiles-sep-lib {
  min-width: 0;
  overflow: hidden;
  font: 650 calc(14px * var(--vy-k))/1 var(--vy-ui);
  letter-spacing: -.01em;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tiles-sep-nb {
  flex: none;
  padding-bottom: 6px;
  color: var(--vy-muted);
  font: 500 calc(12px * var(--vy-k))/1 var(--vy-ui);
  white-space: nowrap;
}

/* La ligne « titre · année » du bac et du casier : petite, sous l'artiste, pour
   ne pas prendre de place. Le plein écran montre déjà le titre en grand. */
.pile-sous {
  font: 500 calc(12px * var(--vy-k))/1.3 var(--vy-ui);
  color: var(--vy-muted-3);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pile-sous:empty, .pile.is-plein .pile-sous { display: none; }

/* ── Le casier en vrais matériaux (octobre 2026) ─────────────────────────────
   Images générées par Antoine, encodées à la taille d'affichage × 2 (cf.
   `tests/test_vinyles_casier_textures.py`) : les cartons usés des intercalaires, la boutique floue derrière le bac en sombre.
   Les congés du pied de l'onglet portent la même matière, découpée par un
   masque (un dégradé radial ne peut pas porter une texture). */
.pile-casier .pile-sep,
.pile-casier .pile-sep-onglet,
.pile-casier .pile-sep-onglet::before,
.pile-casier .pile-sep-onglet::after {
  background-color: var(--sep-fond);
  background-image: var(--sep-img, none);
  background-size: cover;
  background-position: center;
}
.pile-casier .pile-sep-onglet { color: var(--sep-encre, var(--vy-on-accent)); }
/* Cinq cartons, un par rang (`data-teinte`, posé par bac.js) : deux
   intercalaires voisins ne sont jamais de la même couleur, on retrouve sa
   catégorie à la couleur autant qu'au nom. La couleur plate (`--sep-fond`)
   est la moyenne du carton : elle sert de repli et aux congés du pied de l'onglet. */
.pile-casier .pile-sep[data-teinte="0"] { --sep-fond: #aa2c1d; --sep-img: url("casier/inter-rouge.webp"); --sep-encre: #fbf3e2; }
.pile-casier .pile-sep[data-teinte="1"] { --sep-fond: #707645; --sep-img: url("casier/inter-vert.webp"); --sep-encre: #fbf3e2; }
.pile-casier .pile-sep[data-teinte="2"] { --sep-fond: #1d4e5b; --sep-img: url("casier/inter-bleu.webp"); --sep-encre: #fbf3e2; }
.pile-casier .pile-sep[data-teinte="3"] { --sep-fond: #d58f19; --sep-img: url("casier/inter-moutarde.webp"); --sep-encre: #2b1d05; }
.pile-casier .pile-sep[data-teinte="4"] { --sep-fond: #d15c13; --sep-img: url("casier/inter-orange.webp"); --sep-encre: #fbf3e2; }
.pile-casier .pile-sep-onglet::before {
  -webkit-mask: radial-gradient(circle at 0 0, transparent 7.5px, #000 8px);
  mask: radial-gradient(circle at 0 0, transparent 7.5px, #000 8px);
}
.pile-casier .pile-sep-onglet::after {
  -webkit-mask: radial-gradient(circle at 100% 0, transparent 7.5px, #000 8px);
  mask: radial-gradient(circle at 100% 0, transparent 7.5px, #000 8px);
}



/* Le vinyle rouge (champ « Couleur » de la fiche, cf. `couleurVinyle` d'app.js) :
   un disque translucide, et sous lui la feutrine du plateau qu'on devine. */
.album-cover-btn[data-vinyle="rouge"] .vy-platine-plateau {
  background: #2b2b2b url("casier/feutrine.webp") center / 100% 100% no-repeat;
}
.album-cover-btn[data-vinyle="rouge"] .vy-platine-disque {
  background: url("casier/disque-rouge.webp") center / 100% 100% no-repeat;
}

/* Les souhaits vides : une pochette ouverte qui attend son disque, à la place
   de l'icône. Retirée sur un écran court, comme l'icône. */
#wishEmpty > .empty-ico { display: none; }
#wishEmpty::before {
  content: "";
  display: block;
  width: 220px;
  max-width: 70vw;
  aspect-ratio: 480 / 258;
  background: url("casier/pochette-ouverte.webp") center / contain no-repeat;
  filter: drop-shadow(1px 3px 4px rgba(0, 0, 0, .28));
}
@media (max-height: 600px) {
  #wishEmpty::before { display: none; }
}
