:root {
  /* SURFACES — verre sur nuit (redesign premium 2026-07-13) : la page est un
     dégradé profond, les surfaces sont des VOILES translucides (blur), plus
     aucune « boîte grise » plate. */
  --bg:        #09090b;   /* page / body — zinc-950, un cran plus profond */
  --bg-elev:   #101014;
  --paper:     rgb(255 255 255 / 4%);   /* verre : voile clair sur la nuit */
  --card:      rgb(255 255 255 / 4%);
  --field-bg:  rgb(255 255 255 / 6%);
  /* TEXT */
  --fg:        #fafafa;
  --muted:     #a1a1aa;   /* zinc-400 */
  --muted-2:   #71717a;   /* zinc-500 */
  /* BORDERS — liserés lumineux, pas des traits gris */
  --border:       1px solid rgb(255 255 255 / 9%);
  --border-soft:  1px solid rgb(255 255 255 / 6%);
  --hairline:     rgb(255 255 255 / 9%);
  /* ACCENT — le vert signature (l'Onde, le live) */
  --accent:        #22c55e;
  --accent-bright: #4ade80;
  --accent-dim:    #16a34a;
  --accent-soft:   rgb(34 197 94 / 16%);
  /* PILL primaire : blanc pur sur nuit — le CTA premium */
  --pill:     #fafafa;
  --pill-fg:  #09090b;
  /* SEMANTIC STATE */
  --ok:    #4ade80;
  --warn:  #fbbf24;
  --err:   #f87171;
  --red:   #7f1d1d;
  /* CONSTANTES restantes (purge §2 spec UI épurée) : chaque littéral hors
     :root a rejoint la source. Alpha d'un token = color-mix, pas un rgba. */
  --ink:        #000;      /* noir des voiles/ombres (color-mix) */
  --qr-bg:      #ffffff;   /* quiet-zone QR : blanc scanner, non thémable */
  --bg-vault:   #14141a;   /* voûte du dégradé body */
  --bg-sheet:   #131317;   /* fond des dialogs / menus */
  --disabled-bg: #262626;
  --disabled-fg: #737373;
  --danger-fg:  #fecaca;   /* encre des boutons danger (sur --red) */
  --track-bg:   #262626;   /* piste de progression */
  --dot-idle:   #3f3f46;
  --hairline-strong: #3a3a3a;
  /* Palette des chips pseudo (identity.ts la consomme via var(--nick-N)) */
  --nick-0: #ec4899;
  --nick-1: #a5f3fc;
  --nick-2: #86efac;
  --nick-3: #fdba74;
  /* RADII — généreux (le premium est rond) */
  --radius:      18px;
  --radius-sm:   13px;
  --radius-pill: 999px;
  /* SHADOWS + GLOW */
  --shadow:         0 24px 60px -18px rgb(0 0 0 / 70%), 0 6px 18px -6px rgb(0 0 0 / 50%);
  --shadow-sm:      0 6px 20px -8px rgb(0 0 0 / 55%);
  --shadow-pressed: 0 1px 4px rgb(0 0 0 / 60%);
  --glow:           0 10px 32px -6px rgb(250 250 250 / 22%);
  --glow-accent:    0 0 0 5px rgb(34 197 94 / 30%);
  /* RESSORT tactile (transform des contrôles) */
  --spring: cubic-bezier(.34, 1.4, .64, 1);
  /* FONTS — self-hébergées (fontsource, importées par Layout.astro) */
  --font-sans: 'Inter Variable', 'Inter', -apple-system, system-ui, sans-serif;
  --font-mono: 'Geist Mono', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;

  /* Échelle d'espacement (rythme vertical régulier) */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;

  /* Échelle de typo — montée d'un cran (lisibilité mobile, présence) */
  --text-xs: 12px;
  --text-sm: 14px;
  --text-md: 16px;
  --text-lg: 19px;
  --text-xl: 26px;
  --text-2xl: 34px;
  --text-code: 40px;
}

* { box-sizing: border-box; }
html, body {
  margin: 0; padding: 0;
  font-family: var(--font-sans);
  font-weight: 400;
  /* Profondeur : halo neutre + murmure d'accent en voûte — jamais un fond plat. */
  background:
    radial-gradient(90% 55% at 50% -12%, color-mix(in srgb, var(--accent) 7%, transparent), transparent 60%),
    radial-gradient(130% 90% at 50% -10%, var(--bg-vault) 0%, var(--bg) 58%) no-repeat fixed;
  background-color: var(--bg);
  color: var(--fg);
  min-height: 100vh;
  min-height: 100dvh;             /* mobile : la vraie hauteur, barre comprise */
  color-scheme: dark;   /* native widgets (range, reveal) render dark */
  -webkit-font-smoothing: antialiased;
  font-optical-sizing: auto;
}
/* Défilement « infini » (terrain 2026-07-14) : la fenêtre défile mais sans
   barre visible : la console qui déborde ne montre jamais d'ascenseur. */
html { scrollbar-width: none; }
html::-webkit-scrollbar, body::-webkit-scrollbar { display: none; }

.container {
  max-width: 30rem;
  margin: 0 auto;
  /* Touch-first : respiration + zones sûres iOS (encoche, home bar). */
  padding: var(--space-4)
           max(var(--space-4), env(safe-area-inset-right))
           calc(var(--space-5) + env(safe-area-inset-bottom))
           max(var(--space-4), env(safe-area-inset-left));
}

/* Rythme vertical : enfants directs espacés régulièrement */
.stack > * + * { margin-top: var(--space-4); }
.stack-sm > * + * { margin-top: var(--space-2); }
.stack-lg > * + * { margin-top: var(--space-6); }
.center { text-align: center; }
.row { display: flex; gap: var(--space-2); align-items: center; }
.row-between { display: flex; justify-content: space-between; align-items: center; }
.grow { flex: 1; }
.hidden { display: none !important; }
.mono { font-family: var(--font-mono); }

/* Utilitaires (§2) : les motifs des ~85 style= inline, une seule recette.
   Un style= résiduel = cas unique, justifié par un commentaire à côté. */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.hint { font-size: var(--text-xs); color: var(--muted); }
.faint { color: var(--muted-2); }
.lead { font-size: var(--text-md); font-weight: 600; }
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }
.mt-5 { margin-top: var(--space-5); }
.mt-6 { margin-top: var(--space-6); }
.mb-1 { margin-bottom: var(--space-1); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-3 { margin-bottom: var(--space-3); }
.mb-4 { margin-bottom: var(--space-4); }
.w-auto { width: auto; }
.btn-compact { width: auto; min-height: 44px; }
.sep { border-top: var(--border); margin: var(--space-4) 0; }
.ellipsis { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }

h1 {
  font-size: var(--text-2xl);
  font-weight: 760;              /* Inter Variable : un poids display exact */
  margin: 0 0 0.5rem 0;
  letter-spacing: -0.03em;
}
/* Label : titre de section discret (ghost), pas un bloc criard. */
.label {
  display: inline-block;
  background: color-mix(in srgb, var(--fg) 6%, transparent);
  color: var(--muted);
  padding: 0.25rem 0.6rem;
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  border-radius: var(--radius-pill);
}

.room-code { font-family: var(--font-mono); letter-spacing: 0.18em; font-weight: 600; }

.qr-wrap {
  /* QR must stay light: scanners need a bright quiet-zone. The one light surface. */
  background: var(--qr-bg);
  border: var(--border);
  box-shadow: var(--shadow-sm);
  padding: 12px;
  display: inline-block;
  border-radius: var(--radius-sm);
}
/* Partage compact : miniature par défaut (le QR géant mangeait l'écran host),
   tap pour agrandir. Le canvas garde sa résolution native (CSS scale = net). */
.qr-wrap.compact { cursor: pointer; padding: 8px; flex: none; }
.qr-wrap.compact canvas { display: block; width: 96px; height: 96px; }
.qr-wrap.compact.expanded canvas { width: min(70vw, 280px); height: auto; }

/* Mini-QR du RoomBadge : vignette dans la pilule (maquette : timbre discret) ;
   tap = overlay plein écran. */
.qr-wrap.mini { padding: 2px; cursor: pointer; flex: none; background: var(--qr-bg); border-radius: 6px; }
.qr-wrap.mini img { display: block; width: 30px; height: 30px; }

/* Overlay QR : l'agrandissement in-flux déformait la carte — voile plein écran,
   QR centré en grand, tap n'importe où pour fermer. */
.qr-overlay {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--ink) 88%, transparent);
  cursor: pointer;
}
.qr-overlay img {
  width: min(80vw, 80vh, 420px); height: auto;
  background: var(--qr-bg); padding: 14px; border-radius: var(--radius);
}

/* Tuiles-sources host : 4 façons de diffuser, lisibles SANS texte (épure).
   La tuile verte = celle qui émet ; 🎙️/🖥️ actives deviennent ■ (re-tap = stop) ;
   🔗 .open = son panneau est déplié. */
.tile {
  flex: 1; width: auto; min-height: 52px;
  font-size: 20px; line-height: 1; padding: var(--space-2);
  background: var(--field-bg); color: var(--fg);
  border: var(--border); border-radius: 14px;
  box-shadow: none;
}
.tile.open { border-color: color-mix(in srgb, var(--fg) 25%, transparent); }
/* Tuile choisie (maquette .on) : voile accent, pas juste un liseré. */
.tile.active { background: var(--accent-soft); border-color: var(--accent); box-shadow: none; }
.tile:disabled { opacity: .35; }

/* Logo-santé du RoomBadge : le diapason de la marque, vert qui pulse = room
   saine, rouge fixe = room bancale (remplace l'ancien dot « live »). */
.badge-logo { color: var(--accent); flex: none; animation: badge-pulse 2.2s ease-in-out infinite; }
.badge-logo.bad { color: var(--err); animation: none; }
/* Le logo-santé EST celui du header : ces règles battent `.brand svg`
   (spécificité) — hors room, aucune classe posée, la marque reste statique. */
.brand svg.badge-logo { color: var(--accent); }
.brand svg.badge-logo.bad { color: var(--err); }
@keyframes badge-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .45; }
}

/* Bouton primaire = pilule blanche beatsync (bg blanc / texte quasi-noir). */
button, .btn {
  display: inline-block;
  width: 100%;
  padding: var(--space-3) var(--space-4);
  border: none;
  border-radius: var(--radius-pill);
  background: var(--pill);
  color: var(--pill-fg);
  font-family: inherit;
  font-weight: 600;
  font-size: var(--text-md);
  letter-spacing: 0.01em;
  box-shadow: none;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
  transition: transform 150ms ease, box-shadow 150ms ease, background 150ms ease, color 150ms ease;
  min-height: 48px;
}
/* Touch-first : l'appui SE SENT (ressort), le hover n'est qu'un bonus desktop. */
button, .btn { transition: transform 260ms var(--spring), box-shadow 200ms ease, background 200ms ease, color 200ms ease; }
button:active, .btn:active { transform: scale(.96); box-shadow: var(--shadow-pressed); }
button:disabled, .btn:disabled, .btn[aria-disabled="true"] {
  background: var(--disabled-bg);
  color: var(--disabled-fg);
  box-shadow: none;
  cursor: not-allowed;
  transform: none;
}
/* Secondary = bouton fantôme (transparent, bordure fine) — un cran plus léger. */
button.secondary, .btn.secondary {
  background: transparent;
  color: var(--muted);
  border: var(--border);
  box-shadow: none;
}
button.danger, .btn.danger { background: var(--red); color: var(--danger-fg); }
.btn.primary { background: var(--pill); color: var(--pill-fg); }
/* Héros : un cran plus gros pour le CTA principal (toujours pilule blanche). */
button.hero, .btn.hero { font-size: var(--text-lg); padding: var(--space-4) var(--space-5); min-height: 56px; }

input[type="text"], input[type="url"], input[type="search"], input[type="password"], .field {
  width: 100%;
  padding: var(--space-3);
  border: var(--border);
  border-radius: var(--radius-sm);
  background: var(--field-bg);
  color: var(--fg);
  font-family: inherit;
  font-weight: 400;
  font-size: var(--text-md);
  box-shadow: none;
}
input::placeholder, .field::placeholder { color: var(--muted-2); }
input:focus, .field:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent-soft);
}
/* Sliders (seek / volume) : piste sombre + remplissage vert natif. */
input[type="range"] { accent-color: var(--accent); }

/* Code de room segmenté (maquette) : 6 cases monospace PLEINE LARGEUR — la
   rangée remplit la carte, la fusion donne donc une vraie pilule large. */
.otp { display: flex; gap: var(--space-2); justify-content: center; height: 58px; }
.otp-slot {
  flex: 1 1 0; min-width: 0; width: auto; height: 100%; padding: 0; text-align: center;
  font-size: var(--text-xl); font-weight: 400; text-transform: uppercase;
  border: var(--border); border-radius: var(--radius-sm);
  background: var(--field-bg); color: var(--fg);
  transition: border-color .2s, box-shadow .2s, background .2s;
}
.otp-slot:focus {
  outline: none; border-color: var(--accent);
  background: var(--card); box-shadow: 0 0 0 1px var(--accent-soft);
}
/* Ligne repliée (§1 règle 3) : le secondaire n'existe qu'au tap. Maquette :
   un fin séparateur au-dessus + chevron ▸ qui pivote à l'ouverture. */
.fold-line {
  width: 100%; min-height: 48px; padding: var(--space-2) var(--space-1);
  display: flex; align-items: center; gap: var(--space-2);
  background: none; border: none; border-top: var(--border-soft); border-radius: 0;
  box-shadow: none;
  color: var(--muted); font-size: var(--text-sm); text-align: left;
}
.fold-line:hover { color: var(--fg); transform: none; box-shadow: none; }
.fold-line .chev { margin-left: auto; color: var(--muted-2); font-size: 11px; transition: transform .25s; }
.fold-line[aria-expanded="true"] .chev { transform: rotate(90deg); }

/* Pill skip : transparent, l'état voté = accent plein. */
.skip-pill {
  width: auto; min-height: 44px; padding: var(--space-1) var(--space-3);
  background: none; color: var(--muted); border: 1px solid var(--hairline);
  border-radius: var(--radius-pill); font-size: var(--text-xs); font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.skip-pill.voted { color: var(--accent-bright); border-color: var(--accent); background: var(--accent-soft); }

/* Étiquette de section de file (maquette) : mono, uppercase, très discrète. */
.q-label {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--muted-2);
}

/* Pied écouteur (§7) : pseudo tapable (✎) + compteur, centrés. */
.me-edit {
  width: auto; min-height: 44px; padding: var(--space-1) var(--space-2);
  display: inline-flex; align-items: center; gap: var(--space-1);
  background: none; border: none; box-shadow: none;
  color: var(--muted); font-size: var(--text-sm);
}
.me-edit:hover { color: var(--fg); transform: none; box-shadow: none; }
.me-input { width: 12ch; padding: var(--space-1) var(--space-2); font-size: var(--text-sm); }

/* Arrivée QR/lien direct (§5) : aucun geste préalable, UN ▶ plein écran.
   Terrain 2026-07-14 : le vert du site sur fond transparent (la pilule
   blanche tranchait trop sur la page sombre). */
.gate-play {
  width: 100%; min-height: 44vh; border-radius: var(--radius);
  font-size: 72px; line-height: 1;
  background: transparent; color: var(--accent-bright);
  border: none; box-shadow: none;
}
.gate-play:hover:not(:disabled) { box-shadow: none; }

/* Fusion OTP (§4) : au 6e caractère les cases fusionnent en pilule blanche,
   puis la pilule-bouton « Rejoindre CODE → » les remplace (champ et bouton
   ne coexistent jamais). */
.otp { transition: gap .22s var(--spring), box-shadow .3s; }
/* La pilule blanche se soulève (glow, maquette) pendant la vérification. */
.otp.fused { gap: 0; border-radius: var(--radius-pill); box-shadow: var(--glow); }
.otp.fused .otp-slot {
  background: var(--pill); color: var(--pill-fg); border-color: var(--pill);
  border-radius: 0;
}
.otp.fused .otp-slot:first-child { border-radius: var(--radius-pill) 0 0 var(--radius-pill); }
.otp.fused .otp-slot:last-child { border-radius: 0 var(--radius-pill) var(--radius-pill) 0; }

/* Morph création (§4, maquette + terrain 2026-07-14) : « crée ta station »
   mute la MÊME forme en pilule VERTE ; le bouton « Lancer ma station → » la
   recouvre (champ et bouton ne coexistent jamais). */
.otp { position: relative; }
.otp-act {
  position: absolute; inset: 0; width: 100%; min-height: 0; padding: 0;
  border: none; border-radius: var(--radius-pill);
  background: none; box-shadow: none;
  font-weight: 600; font-size: var(--text-sm); color: var(--accent-bright);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .25s .1s, visibility .25s .1s;
}
.otp-act:hover { transform: none; box-shadow: none; }
.otp.create { gap: 0; border-radius: var(--radius-pill); box-shadow: 0 0 0 1px var(--accent); }
.otp.create .otp-slot {
  background: var(--accent-soft); color: transparent; border-color: transparent;
  border-radius: 0; pointer-events: none;
}
.otp.create .otp-slot:first-child { border-radius: var(--radius-pill) 0 0 var(--radius-pill); }
.otp.create .otp-slot:last-child { border-radius: 0 var(--radius-pill) var(--radius-pill) 0; }
.otp.create .otp-act { opacity: 1; visibility: visible; pointer-events: auto; }

/* Liens sociaux dans la carte d'accueil (Discord | GitHub). */
.social { display: flex; align-items: center; justify-content: center; gap: var(--space-4); margin-top: var(--space-4); }
.social a { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--muted); font-size: var(--text-xs); text-decoration: none; transition: color .2s; }
.social a:hover { color: var(--fg); }

/* Header global EN FLUX : marque à gauche, menu ⋯ à droite. (Un fixed
   recouvrait la topbar de la page écouteur.) */
.app-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-2) 0 var(--space-4);
  position: relative; /* ancre du menu ⋯ */
}

/* Menu ⋯ (§3) : la seule porte méta du header. */
.menu-btn {
  width: 40px; min-height: 40px; padding: 0; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--card); border: var(--border); border-radius: 50%;
  color: var(--muted); font-weight: 700; font-size: var(--text-lg); line-height: 1;
  box-shadow: var(--shadow-sm);
}
.menu-btn:hover { color: var(--fg); transform: none; box-shadow: var(--shadow-sm); }
.menu {
  position: absolute; top: calc(100% - var(--space-2)); right: 0; z-index: 70;
  min-width: 14rem; padding: var(--space-1);
  background: var(--bg-sheet); border: var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow); display: grid; gap: 2px;
  animation: enter .18s ease both;
}
.menu-item {
  width: 100%; min-height: 48px; padding: var(--space-2) var(--space-3);
  background: none; border: none; box-shadow: none; border-radius: 10px;
  color: var(--fg); font-size: var(--text-sm); font-weight: 500; text-align: left;
}
.menu-item:hover { background: color-mix(in srgb, var(--fg) 7%, transparent); transform: none; box-shadow: none; }
.menu-item.danger { color: var(--err); }
.menu-item.sub { padding-left: var(--space-6); color: var(--muted); }
/* Sous-choix gouvernance (maquette) : un SEGMENT de 3 emojis sous l'item,
   pas trois rangées — l'actif porte le liseré paper. */
.menu-seg { display: flex; gap: 4px; justify-content: center; padding: 2px 0 var(--space-1); }
.menu-seg .menu-item {
  flex: 0 1 auto; width: auto; min-height: 44px; padding: var(--space-1) var(--space-3);
  border: 1px solid transparent; border-radius: var(--radius-pill);
  text-align: center; font-size: var(--text-md); color: var(--muted);
}
.menu-seg .menu-item.active { border-color: var(--hairline); background: var(--paper); color: var(--fg); }
/* Voile du menu ⋯ (maquette) : la page recule quand le méta est ouvert. */
.veil { position: fixed; inset: 0; z-index: 65; background: color-mix(in srgb, var(--ink) 55%, transparent); }
.brand {
  display: inline-flex; align-items: center; gap: var(--space-2);
  color: var(--fg); font-weight: 700; font-size: var(--text-md); letter-spacing: -0.02em;
  text-decoration: none;
}
.brand svg { color: var(--accent-bright); flex: none; }
.brand:hover .brand-name { color: var(--accent-bright); transition: color .2s; }

/* MIRE (V14→) : une VRAIE mire TV (SMPTE) en pur CSS — trois bandes :
   barres de couleur, barres inversées, pluge — désaturée pour le thème dark.
   L'écouteur VOIT que la room attend son DJ (le son, lui, est très doux/absent). */
.mire-visual {
  height: 140px; border-radius: var(--radius); margin-bottom: var(--space-4);
  overflow: hidden;
  background:
    /* bande basse (25 %) : pluge — bleu-I, blanc, violet-Q, noirs étagés */
    linear-gradient(to right,
      #0d2a63 0 17%, #f2f2f2 17% 34%, #2a0e57 34% 51%,
      #0a0a0a 51% 68%, #000 68% 79%, #1a1a1a 79% 89%, #0a0a0a 89% 100%
    ) left bottom / 100% 25% no-repeat,
    /* bande médiane (8 %) : barres inversées */
    linear-gradient(to right,
      #0000b8 0 14.3%, #131313 14.3% 28.6%, #b800b8 28.6% 42.9%,
      #131313 42.9% 57.2%, #00b8b8 57.2% 71.5%, #131313 71.5% 85.8%, #b8b8b8 85.8% 100%
    ) left 67% / 100% 8% no-repeat,
    /* barres principales SMPTE (67 %) */
    linear-gradient(to right,
      #b8b8b8 0 14.3%, #b8b800 14.3% 28.6%, #00b8b8 28.6% 42.9%,
      #00b800 42.9% 57.2%, #b800b8 57.2% 71.5%, #b80000 71.5% 85.8%, #0000b8 85.8% 100%
    ) left top / 100% 67% no-repeat;
  filter: saturate(.7) brightness(.85);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06);
}

.mire-mini { height: 72px; margin: var(--space-3) 0 0; }

/* Segments (maquette .seg) : transparents, l'actif = voile paper + liseré.
   Fini le remplissage blanc plein : le blanc reste réservé au CTA. */
.vis-switch { display: flex; gap: 4px; justify-content: center; }
.vis-switch button {
  flex: 0 1 auto; width: auto; min-height: 44px; padding: var(--space-2) var(--space-4);
  font-size: var(--text-sm); font-weight: 600;
  background: none; color: var(--muted-2); border: 1px solid transparent; box-shadow: none;
  border-radius: var(--radius-pill);
}
.vis-switch button:hover { color: var(--fg); transform: none; box-shadow: none; }
.vis-switch button.active { border-color: var(--hairline); background: var(--paper); color: var(--fg); }

/* Création (§4) : un lien texte qui MUTE en pilule verte au dépliage. */
.link-create {
  width: auto; min-height: 44px; margin: 0 auto; display: block;
  background: none; border: none; box-shadow: none;
  color: var(--muted); font-size: var(--text-sm);
  text-decoration: underline 2px; text-decoration-color: var(--accent); text-underline-offset: 4px;
}
.link-create:hover { color: var(--fg); transform: none; box-shadow: none; }

dialog {
  background: var(--bg-sheet); color: var(--fg); border: var(--border);
  border-radius: var(--radius); padding: var(--space-5);
  max-width: 28rem; width: calc(100% - 2 * var(--space-5));
  box-shadow: var(--shadow);
  animation: enter .3s var(--spring) both;
}
dialog::backdrop { background: color-mix(in srgb, var(--ink) 62%, transparent); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
/* Mobile : les dialogs deviennent des BOTTOM SHEETS (le pouce, pas le centre). */
@media (max-width: 540px) {
  dialog {
    margin: auto 0 0; width: 100%; max-width: none;
    border-radius: 24px 24px 0 0; border-bottom: none;
    padding-bottom: calc(var(--space-5) + env(safe-area-inset-bottom));
    animation: sheet-up .34s var(--spring) both;
  }
}
@keyframes sheet-up { from { opacity: .6; transform: translateY(40px); } to { opacity: 1; transform: none; } }
.help-body h3 { font-size: var(--text-sm); margin: var(--space-4) 0 var(--space-1); }
.help-body p { font-size: var(--text-sm); margin: 0; }

.card {
  background: var(--card);
  border: var(--border);
  /* VERRE : voile translucide + blur — la profondeur du fond transparaît. */
  -webkit-backdrop-filter: blur(22px) saturate(1.15);
  backdrop-filter: blur(22px) saturate(1.15);
  /* Ombre profonde + fin liseré clair en haut = relief. */
  box-shadow: var(--shadow), inset 0 1px 0 color-mix(in srgb, var(--fg) 6%, transparent);
  padding: var(--space-4);
  border-radius: var(--radius);
}

/* Liste générique (room écouteurs, résultats remote) */
.list { list-style: none; padding: 0; margin: 0; }

/* Chip : pastille de pseudo (couleur auto via --chip-accent inline) */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 600;
  color: var(--fg);
}

/* Dot d'état : pastille ronde colorée (vert/orange/rouge) */
.dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border: none;
  border-radius: 50%;
  background: var(--dot-idle);
  flex: none;
}
.dot.ready { background: var(--accent); }
.dot.calibrating { background: var(--warn); }
.dot.error { background: var(--err); }

/* Vitrine « Live now » (maquette) : UNE ligne par room — dot dont le halo
   respire, code mono, compteur à droite. Partagée accueil + overlay explore. */
.live-row {
  display: flex; align-items: center; gap: var(--space-3); width: 100%;
  background: var(--paper); border: var(--border); border-radius: var(--radius-sm);
  padding: 13px 15px; color: var(--fg); font-size: var(--text-sm);
  text-decoration: none; transition: border-color .15s, transform .15s;
}
.live-row:hover, .live-row:focus-visible {
  border-color: var(--hairline-strong); transform: translateY(-1px); outline: none;
}
.live-dot {
  width: 7px; height: 7px; border-radius: var(--radius-pill); flex: none;
  background: var(--accent-bright); box-shadow: 0 0 0 4px var(--accent-soft);
  animation: live-halo 2.4s ease-in-out infinite;
}
@keyframes live-halo { 50% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--accent) 7%, transparent); } }
.live-row .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.live-row .count { color: var(--muted-2); font-size: var(--text-xs); flex: none; font-variant-numeric: tabular-nums; }

/* Badge de room (maquette) : une PILULE centrée — QR + code espacé + slot,
   partagée console/écouteur. Le plein-écran topbar est mort avec elle. */
.room-badge {
  display: flex; align-items: center; justify-content: center; gap: var(--space-3);
  width: fit-content; max-width: 100%; margin: 0 auto;
  padding: var(--space-2) var(--space-4);
  background: var(--paper); border: var(--border); border-radius: var(--radius-pill);
  -webkit-backdrop-filter: blur(18px) saturate(1.15);
  backdrop-filter: blur(18px) saturate(1.15);
}
.room-badge .code { font-size: var(--text-md); letter-spacing: .25em; font-weight: 600; }

/* Connexion WS perdue : feedback animé (host + listener via WsClient onClose). */
.loading { position: relative; opacity: 0.7; }
.loading::after {
  content: '';
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  margin-left: var(--space-2);
  vertical-align: -0.1em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

.muted { color: var(--muted); font-weight: 400; }
/* La ligne de statut console : « à l'antenne » s'allume en accent (maquette). */
.live-word { color: var(--accent-bright); }

/* Transport console (maquette .h-ctrl) : boutons RONDS 44px, icône seule. */
.ctl-round {
  width: 44px; min-width: 44px; min-height: 44px; padding: 0; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--radius-pill); font-size: var(--text-md);
  background: var(--field-bg); color: var(--fg); border: var(--border); box-shadow: none;
}
.ctl-round:hover:not(:disabled) { border-color: var(--hairline-strong); transform: none; box-shadow: none; }

/* Chip de mode (maquette) : le libellé dit l'état, les points la position. */
.mode-chip {
  display: inline-flex; align-items: center; gap: var(--space-2);
  width: auto; min-height: 44px; padding: 0 var(--space-4); flex: none;
  border-radius: var(--radius-pill); font-size: var(--text-xs);
  background: var(--field-bg); color: var(--fg); border: var(--border); box-shadow: none;
}
.mode-chip:hover:not(:disabled) { border-color: var(--hairline-strong); transform: none; box-shadow: none; }
.mode-chip .dots { display: flex; gap: 3px; }
.mode-chip .dots i { width: 4px; height: 4px; border-radius: 2px; background: color-mix(in srgb, var(--fg) 18%, transparent); }
.mode-chip .dots i.on { background: var(--accent-bright); }

/* Carte now-playing console (maquette) : pochette 52 px + titre + progression. */
.now-card { display: flex; gap: var(--space-3); align-items: center; padding: var(--space-2) 0 var(--space-3); }
.nc-art {
  width: 52px; height: 52px; flex: none; border-radius: var(--radius-sm);
  border: var(--border); object-fit: cover;
  display: flex; align-items: center; justify-content: center;
  color: var(--muted-2); font-size: var(--text-lg);
  background: radial-gradient(circle at 30% 25%, color-mix(in srgb, var(--fg) 12%, transparent), transparent);
}
.nc-meta { flex: 1; min-width: 0; }
.nc-title { font-size: var(--text-sm); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nc-track { height: 3px; border-radius: 2px; background: color-mix(in srgb, var(--fg) 10%, transparent); margin-top: var(--space-2); overflow: hidden; }
.nc-fill { height: 100%; width: 0; border-radius: 2px; background: var(--accent); }
.nc-times {
  display: flex; justify-content: space-between; font-family: var(--font-mono);
  font-size: 10px; color: var(--muted-2); margin-top: 5px; font-variant-numeric: tabular-nums;
}
/* En-tête de section (Programme · n titres) — partagé console/écouteur. */
.q-head {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 11px; color: var(--muted-2); text-transform: uppercase;
  letter-spacing: .1em; padding: 0 var(--space-1);
}

/* Le RUBAN (spec 2026-07-14) : programme horizontal, aimanté, ZÉRO scrollbar
   (mobile et desktop), fondu latéral — remplace toute liste verticale de pistes. */
.ribbon {
  display: flex; gap: 10px; overflow-x: auto; scrollbar-width: none;
  scroll-snap-type: x mandatory; padding: var(--space-2) 2px var(--space-1);
  -webkit-mask-image: linear-gradient(90deg, transparent, var(--ink) 7%, var(--ink) 93%, transparent);
  mask-image: linear-gradient(90deg, transparent, var(--ink) 7%, var(--ink) 93%, transparent);
}
.ribbon::-webkit-scrollbar { display: none; }
.ribbon-card {
  flex: none; width: 118px; scroll-snap-align: center; position: relative;
  border: var(--border); background: var(--paper); border-radius: var(--radius-sm);
  padding: 10px; color: var(--fg); text-align: left; box-shadow: none;
}
button.ribbon-card { cursor: pointer; }
button.ribbon-card:hover { border-color: var(--hairline-strong); transform: none; box-shadow: none; }
.rc-art {
  width: 100%; aspect-ratio: 1; border-radius: 9px; margin-bottom: var(--space-2);
  object-fit: cover; display: flex; align-items: center; justify-content: center;
  color: var(--muted-2); font-size: 20px;
  background: radial-gradient(circle at 30% 25%, color-mix(in srgb, var(--fg) 12%, transparent), transparent), var(--field-bg);
}
.rc-title {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; font-size: var(--text-xs); line-height: 1.35; min-height: 2.7em;
}
.rc-sub {
  display: block; font-family: var(--font-mono); font-size: 10px;
  color: var(--muted-2); margin-top: 5px; font-variant-numeric: tabular-nums;
}
.rc-actions { display: flex; gap: var(--space-1); margin-top: var(--space-1); }
.ribbon-card.on-air { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent-soft); }
.ribbon-card.on-air .rc-title { color: var(--accent-bright); font-weight: 600; }
/* Jump armé (2ᵉ tap sous 4 s) : la carte s'arme en accent. */
.ribbon-card.confirm { box-shadow: inset 0 0 0 1px var(--accent); }
.ribbon-card.confirm .rc-sub { color: var(--accent-bright); }
/* Démocratie : compte de votes sur la carte, MON vote en accent. */
.vote-badge {
  position: absolute; top: 14px; right: 14px; font-size: 10px;
  font-family: var(--font-mono);
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  color: var(--fg); border: var(--border); border-radius: var(--radius-pill);
  padding: 2px 7px;
}
.ribbon-card.voted .vote-badge {
  background: var(--accent-soft); color: var(--accent-bright); border-color: var(--accent);
}

/* Pochette du player écouteur (maquette ruban) : LE hero de la page — 132 px,
   ♪ en repli : toujours une pochette, jamais un trou dans la carte. */
.artwork {
  width: 132px; height: 132px; object-fit: cover;
  border-radius: var(--radius-sm); border: var(--border);
  margin: 0 auto var(--space-3); display: block;
}
.art-fallback {
  width: 132px; height: 132px; border-radius: var(--radius-sm); border: var(--border);
  margin: 0 auto var(--space-3);
  background: radial-gradient(circle at 30% 25%, color-mix(in srgb, var(--fg) 10%, transparent), transparent);
  display: flex; align-items: center; justify-content: center;
  color: var(--muted-2); font-size: var(--text-2xl);
}
/* La tuile ♪ s'efface quand une vraie pochette (ou la mire) prend la place. */
.card:has(#artwork:not(.hidden)) .art-fallback,
.card:has(#mire:not(.hidden)) .art-fallback { display: none; }
.ok   { color: var(--ok);   font-weight: 600; }
.warn { color: var(--warn); font-weight: 600; }
.err  { color: var(--err);  font-weight: 600; }

.progress-track {
  background: var(--track-bg);
  border: none;
  height: 4px; /* maquette : un fil, pas une barre */
  box-shadow: none;
  border-radius: var(--radius-pill);
  position: relative;
  overflow: hidden;
}
.progress-fill {
  background: var(--accent);
  height: 100%;
  width: 0;
  transition: width 0.3s;
}

a { color: inherit; text-decoration: underline 2px; text-decoration-color: var(--accent); text-underline-offset: 4px; }

/* Hover-lift on interactive surfaces. Primary pills get a white glow; ghost
   (.secondary) buttons only lighten — no glow. */
.card { transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease; }
button:hover:not(:disabled), .btn:hover:not(.disabled) { transform: scale(1.02); box-shadow: var(--glow); }
button.secondary:hover:not(:disabled), .btn.secondary:hover:not(.disabled) {
  box-shadow: none;
  color: var(--fg);
  background: color-mix(in srgb, var(--fg) 5%, transparent);
}

/* Micro-boutons (votes, ✓/✗ suggestions) : le vote est un micro-geste, pas un
   gros pill — un seul endroit pour la recette (avant : styles inline copiés ×3). */
button.btn-micro { width: auto; padding: 4px 12px; font-size: var(--text-xs); min-height: 32px; }

/* Live "now-playing" dot : doux pouls + anneau qui se propage (beatsync animate-ping). */
@keyframes pulse-dot { 0%,100% { box-shadow: 0 0 0 0 transparent; } 50% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 30%, transparent); } }
.dot.live { position: relative; animation: pulse-dot 1.8s ease-in-out infinite; }
.dot.live::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  background: var(--accent); opacity: 0;
  animation: ping 1.8s cubic-bezier(0,0,.2,1) infinite;
}
@keyframes ping { 0% { transform: scale(1); opacity: .4; } 70%,100% { transform: scale(2); opacity: 0; } }

/* Soft entrance for list items / cards revealed dynamically. */
@keyframes fade-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.fade-rise { animation: fade-rise 260ms ease both; }

/* Entrée façon beatsync : fondu + léger translate/scale, courbe douce. Sur une
   carte (.enter) ou en cascade sur ses enfants (.enter-stagger). */
@keyframes enter { from { opacity: 0; transform: translateY(10px) scale(.985); } to { opacity: 1; transform: none; } }
.enter { animation: enter .5s cubic-bezier(.22,1,.36,1) both; }
.enter-stagger > * { animation: enter .5s cubic-bezier(.22,1,.36,1) both; }
.enter-stagger > *:nth-child(1) { animation-delay: .04s; }
.enter-stagger > *:nth-child(2) { animation-delay: .10s; }
.enter-stagger > *:nth-child(3) { animation-delay: .16s; }
.enter-stagger > *:nth-child(4) { animation-delay: .22s; }
.enter-stagger > *:nth-child(5) { animation-delay: .28s; }
.enter-stagger > *:nth-child(6) { animation-delay: .34s; }
.enter-stagger > *:nth-child(7) { animation-delay: .40s; }
.enter-stagger > *:nth-child(8) { animation-delay: .46s; }

/* l'Onde — 8 barres VERTICALES dont la HAUTEUR raconte l'état (maquette
   validée « UI épurée » 2026-07-13, retour terrain 2026-07-14). Plate =
   arrêt ; balayage G→D = synchro (« les horloges s'accordent ») ;
   respiration du centre vers les extérieurs = à l'antenne. Le contrat de
   state-bar.ts est inchangé : UNE classe d'état posée par le writer unique
   (playing / paused / seeking / syncing / lost), défaut sans classe = plate. */
.sync-bar { display: flex; gap: 5px; justify-content: center; align-items: center; height: 24px; }
.sync-bar i {
  width: 4px; height: 12%; border-radius: 2px;
  background: var(--accent-bright);
  transition: height .4s, background .4s;
}
/* à l'antenne : respiration, allumage du centre vers les extérieurs. */
@keyframes onde-breathe { 0%, 100% { height: 18%; } 50% { height: 92%; } }
.sync-bar.playing i, .sync-bar.seeking i { animation: onde-breathe 1.15s ease-in-out infinite; }
/* avance rapide / re-ancrage : la même respiration, deux fois plus vite. */
.sync-bar.seeking i { animation-duration: .55s; }
.sync-bar.playing i:nth-child(4), .sync-bar.playing i:nth-child(5),
.sync-bar.seeking i:nth-child(4), .sync-bar.seeking i:nth-child(5) { animation-delay: 0s; }
.sync-bar.playing i:nth-child(3), .sync-bar.playing i:nth-child(6),
.sync-bar.seeking i:nth-child(3), .sync-bar.seeking i:nth-child(6) { animation-delay: .2s; }
.sync-bar.playing i:nth-child(2), .sync-bar.playing i:nth-child(7),
.sync-bar.seeking i:nth-child(2), .sync-bar.seeking i:nth-child(7) { animation-delay: .4s; }
.sync-bar.playing i:nth-child(1), .sync-bar.playing i:nth-child(8),
.sync-bar.seeking i:nth-child(1), .sync-bar.seeking i:nth-child(8) { animation-delay: .6s; }
/* jonction/synchro : balayage gauche→droite, l'opacité suit la crête. */
@keyframes onde-sweep { 0%, 30%, 100% { height: 14%; opacity: .45; } 12% { height: 85%; opacity: 1; } }
.sync-bar.syncing i { animation: onde-sweep 1.1s linear infinite; }
.sync-bar.syncing i:nth-child(1) { animation-delay: 0s; }
.sync-bar.syncing i:nth-child(2) { animation-delay: .11s; }
.sync-bar.syncing i:nth-child(3) { animation-delay: .22s; }
.sync-bar.syncing i:nth-child(4) { animation-delay: .33s; }
.sync-bar.syncing i:nth-child(5) { animation-delay: .44s; }
.sync-bar.syncing i:nth-child(6) { animation-delay: .55s; }
.sync-bar.syncing i:nth-child(7) { animation-delay: .66s; }
.sync-bar.syncing i:nth-child(8) { animation-delay: .77s; }
/* pause (utilisateur) : vert plein à mi-hauteur, immobile. */
.sync-bar.paused i { animation: none; height: 45%; background: var(--accent); }
/* flux mort : plate et rouge. */
.sync-bar.lost i { animation: none; background: var(--err); }
/* Héros : variante large de l'Onde. */
.sync-bar.lg { height: 38px; }

@media (max-width: 480px) {
  body { font-size: 16px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition: none !important; animation: none !important; }
  /* Exception: the .loading spinner is a functional status indicator, not
     decoration. Killing it under reduced-motion makes "connecting…" look
     frozen/broken — keep it spinning. */
  .loading::after { animation: spin 0.7s linear infinite !important; }
}
