/* ─── Viewer Beamcord — habillage ──────────────────────────────────────────
 *
 * Identité reprise du site vitrine (beamcord-site/src/index.css) :
 * « lueur d'écran dans une pièce sombre ». Sombre assumé, UN seul accent (la
 * lueur pêche), et le rouge réservé au direct — comme sur du matériel de régie.
 *
 * Grammaire visuelle, identique au site :
 *   bordures      rgba(255,255,255,.10)
 *   séparateurs   rgba(255,255,255,.06)
 *   surfaces      rgba(255,255,255,.05) → .07
 *   rayons        14 px fenêtre · 12 px barre de contrôles · 16 px cartes · 999 px pastilles
 *   ombre         0 40px 120px -20px rgba(0,0,0,.75)
 *   titres        dégradé lueur en `background-clip: text`
 *   télémétrie    mono 10-11 px, surface .07, texte clair à 65 %
 *
 * Deux contraintes qui viennent de viewer.js, pas du design :
 *   · les `id` sont un contrat (`getElementById`) — jamais renommés ;
 *   · plusieurs éléments voient leur `textContent` réécrit (#status, #tierBadge,
 *     #latencyBadge, #soundBtn, #fsBtn, #joinBtn, #err, #sdkStatus) : aucun
 *     enfant ne survit dans ceux-là, d'où les pseudo-éléments pour tout ce qui
 *     doit y rester (point de tally, icônes).
 *
 * La page tourne dans une iframe Discord : aucune dépendance à `window.top`,
 * utilisable de 320 px à 4 K, et rien d'atteignable au seul survol.
 */

/* ── Polices auto-hébergées ────────────────────────────────────────────────
 * Variables (un fichier couvre 100→900), donc `font-weight: 100 900` : sans
 * cette plage le navigateur synthétise un faux gras au lieu d'instancier l'axe.
 * `swap` : le texte s'affiche tout de suite dans la police système, ce qui vaut
 * mieux qu'un écran d'accueil vide pendant le chargement.
 */
@font-face {
  font-family: 'Bricolage Grotesque';
  src: url('fonts/bricolage-grotesque.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Instrument Sans';
  src: url('fonts/instrument-sans.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'JetBrains Mono';
  src: url('fonts/jetbrains-mono.woff2') format('woff2');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Palette — identique au @theme du site vitrine. */
  --encre: #0b0d12;      /* fond, graphite froid */
  --panneau: #12151d;    /* cartes et panneaux */
  --clair: #eef0f5;      /* texte principal */
  --brume: #99a1b3;      /* texte secondaire, gris bleuté */
  --lueur: #ff9460;      /* accent chaud — le SEUL accent */
  --lueur-2: #ffd9b8;    /* accent chaud, clair */
  --live: #ff4d59;       /* rouge broadcast — indicateurs LIVE uniquement */
  --live-clair: #ff8089; /* texte des pastilles LIVE */

  /* Matières */
  --bord: rgba(255, 255, 255, .10);
  --separateur: rgba(255, 255, 255, .06);
  --surface: rgba(255, 255, 255, .05);
  --surface-2: rgba(255, 255, 255, .07);
  --ombre: 0 40px 120px -20px rgba(0, 0, 0, .75);
  --degrade-lueur: linear-gradient(115deg, #ffd9b8 0%, #ff9460 62%, #ff7a59 100%);

  --police-titre: 'Bricolage Grotesque', 'Instrument Sans', system-ui, sans-serif;
  --police-texte: 'Instrument Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
  --police-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* Marges de sécurité : sur iPhone en plein écran, et dans une Activity
     affichée en bas d'un écran à encoche, les coins ne sont pas cliquables.
     Les `env()` sont doublés d'un repli `0px` : sans lui, un moteur qui ignore
     la fonction rend tout le `max()` invalide, et la variable devient inutile
     au moment du calcul — les contrôles partiraient dans le coin haut gauche. */
  --marge-h: max(12px, env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px));
  --marge-b: max(12px, env(safe-area-inset-bottom, 0px));
  --marge-t: max(12px, env(safe-area-inset-top, 0px));
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--encre);
  color: var(--clair);
  font-family: var(--police-texte);
  color-scheme: dark;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

::selection { background: rgba(255, 148, 96, .3); }

/* Anneau de focus unique pour tout ce qui est actionnable. Au clavier
   uniquement : un anneau après chaque clic souris ferait du bruit visuel. */
:focus-visible {
  outline: 2px solid var(--lueur);
  outline-offset: 2px;
}

/*
 * Les deux écrans sont fixés à la fenêtre plutôt que dimensionnés en
 * `100vw/100vh` : `100vw` compte la barre de défilement (débordement d'un
 * cheveu) et `100vh` est faux sur Safari mobile, où la barre d'URL se rétracte.
 * `inset: 0` ne se trompe ni sur l'un ni sur l'autre.
 * viewer.js pilote leur `display` (`none` / `flex`) en style en dur.
 */
.screen {
  position: fixed;
  inset: 0;
  overflow: hidden;
}

/* ═══ Écran d'accueil ══════════════════════════════════════════════════════ */

.join {
  display: flex;
  padding: 24px;
  /* `margin: auto` sur la carte plutôt que `align-items: center` : sur une
     fenêtre plus courte que la carte, le centrage par alignement rend le haut
     inatteignable (le débordement part vers le négatif). */
  align-items: flex-start;
  justify-content: center;
  overflow-x: hidden;
  overflow-y: auto;
}

/* Grain photographique, par-dessus l'écran d'accueil — jamais par-dessus la
   vidéo, où il combattrait le travail de l'encodeur. */
.join::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: .04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

/* Les halos débordent volontairement du cadre : leur conteneur les rogne, ce
   qui évite d'avoir à rendre tout l'écran non défilable pour eux seuls. */
.halos {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.halo {
  position: absolute;
  border-radius: 9999px;
  filter: blur(95px);
  will-change: transform;
  animation: derive 18s ease-in-out infinite alternate;
}
.halo-chaud {
  right: -140px;
  top: 40px;
  width: 560px;
  height: 480px;
  background: rgba(255, 148, 96, .13);
}
/* Pénombre froide : le contrepoint bleuté qui fait lire la lueur comme chaude. */
.halo-froid {
  left: -220px;
  top: -160px;
  width: 540px;
  height: 440px;
  background: rgba(61, 74, 107, .25);
  animation-delay: -7s;
}
@keyframes derive {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(28px, -34px, 0) scale(1.07); }
}

.card {
  position: relative;
  z-index: 2;
  margin: auto;
  width: min(384px, 100%);
  padding: 30px 28px 26px;
  text-align: center;
  background: rgba(18, 21, 29, .82);
  border: 1px solid var(--bord);
  border-radius: 16px;
  box-shadow: var(--ombre);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

/* Pastille LIVE — la seule apparition du rouge sur cet écran, reprise du héro
   du site. Elle annonce ce que fait le produit, comme sur la page d'accueil. */
.pastille-live {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 20px;
  padding: 5px 12px 5px 10px;
  border-radius: 999px;
  background: rgba(255, 77, 89, .15);
  color: var(--live-clair);
  font-family: var(--police-mono);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .06em;
  white-space: nowrap;
}
.point-live {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: #ff5560;
  animation: pulse-live 1.8s ease-out infinite;
}
/* Onde qui s'échappe du point, en box-shadow : pas d'élément supplémentaire à
   maintenir, et rien à repeindre en dehors du point. */
@keyframes pulse-live {
  0%   { box-shadow: 0 0 0 0 rgba(255, 85, 96, .55); }
  70%  { box-shadow: 0 0 0 7px rgba(255, 85, 96, 0); }
  100% { box-shadow: 0 0 0 0 rgba(255, 85, 96, 0); }
}

.card h1 {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin: 0 0 6px;
  font-family: var(--police-titre);
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.05;
}
.logo {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  color: var(--clair);
}
/* Le seul dégradé de texte de la page : c'est la marque, pas une décoration
   qu'on répète. `color: transparent` reste lisible si la police tombe en
   repli — c'est le dégradé qui peint les glyphes, pas la police. */
.mot-marque {
  background: var(--degrade-lueur);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  /* Safari a longtemps ignoré `color: transparent` sur du texte rogné : sans
     cette ligne, le mot ressort en plein clair, dégradé compris mais invisible. */
  -webkit-text-fill-color: transparent;
}

.accroche {
  margin: 0 0 24px;
  color: var(--brume);
  font-size: 13.5px;
  line-height: 1.5;
}

/* Étiquette de champ dans le registre « eyebrow » du site : mono, capitales,
   très espacée, dans l'accent à 90 %. */
.champ-label {
  display: block;
  margin-bottom: 9px;
  color: rgba(255, 148, 96, .9);
  font-family: var(--police-mono);
  font-size: 10px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .2em;
  text-align: left;
}

/*
 * Le code de room est ce qu'on se lit à voix haute : mono (un 0 barré ne se
 * confond pas avec un O), large, très espacé.
 * `text-indent` compense l'`letter-spacing` : la lettre finale traîne son
 * espacement derrière elle, ce qui décale tout le contenu vers la gauche d'un
 * demi-espace — visible sur cinq caractères de 34 px.
 */
#codeInput {
  width: 100%;
  padding: 14px 12px;
  border: 1px solid var(--bord);
  border-radius: 12px;
  background: var(--surface);
  color: var(--clair);
  font-family: var(--police-mono);
  font-size: clamp(26px, 7.5vw, 34px);
  font-weight: 600;
  letter-spacing: .22em;
  /* Moitié de l'espacement : la dernière lettre traîne son `letter-spacing`
     derrière elle, ce qui décale le bloc centré d'un demi-espace vers la
     gauche. Compenser d'un espacement entier surcorrigerait. */
  text-indent: .11em;
  text-align: center;
  text-transform: uppercase;
  transition: border-color .18s ease, background .18s ease, box-shadow .18s ease;
}
#codeInput::placeholder {
  color: rgba(153, 161, 179, .38);
  font-weight: 400;
  letter-spacing: .22em;
}
#codeInput:focus {
  outline: none;
  border-color: rgba(255, 148, 96, .6);
  background: var(--surface-2);
  /* Halo tiède : c'est « l'écran qui s'allume » du thème, appliqué au champ. */
  box-shadow: 0 0 0 3px rgba(255, 148, 96, .14),
              0 12px 40px -14px rgba(255, 148, 96, .45);
}

.btn {
  width: 100%;
  margin-top: 14px;
  padding: 13px;
  border: none;
  border-radius: 12px;
  font-family: var(--police-texte);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: transform .25s ease, box-shadow .25s ease, filter .25s ease;
}
/* Le dégradé des titres réemployé comme surface : l'accent est chaud et clair,
   donc le texte du bouton est sombre — l'inverse serait illisible. */
.btn.primary {
  background: var(--degrade-lueur);
  color: #14100c;
}
.btn.primary:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 16px 50px -12px rgba(255, 148, 96, .45);
}
.btn.primary:active:not(:disabled) { transform: translateY(0); }
.btn.primary:disabled {
  cursor: not-allowed;
  filter: saturate(.35) brightness(.8);
}

/*
 * Erreurs. Le rouge vif est réservé au direct : un message d'erreur ne prend
 * donc ni pastille ni fond rouge, seulement le ton clair de la famille LIVE,
 * qui reste le seul rouge lisible du thème. Un accent chaud (la lueur) ne
 * ferait pas l'affaire : il sert déjà à tout ce qui va bien.
 */
.err {
  min-height: 18px;
  margin-top: 12px;
  color: var(--live-clair);
  font-size: 13px;
  line-height: 1.45;
}

/* État du SDK Discord : information de diagnostic, donc registre télémétrie. */
.sdk-status {
  margin-top: 10px;
  color: var(--brume);
  font-family: var(--police-mono);
  font-size: 10.5px;
  line-height: 1.5;
  word-break: break-word;
}
.sdk-status:empty { margin-top: 0; }

/* ── Badge de contexte Discord ─────────────────────────────────────────────
   Vit hors des deux écrans (il sert sur les deux) : il est donc placé en haut
   à droite, seul coin qu'aucun contrôle n'occupe. */
.ctx-badge {
  position: fixed;
  top: var(--marge-t);
  right: var(--marge-h);
  z-index: 20;
  padding: 5px 11px;
  border: 1px solid var(--bord);
  border-radius: 999px;
  background: rgba(11, 13, 18, .6);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  color: var(--brume);
  font-family: var(--police-mono);
  font-size: 10px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: .14em;
  transition: opacity .25s ease;
}

/* ═══ Écran de visionnage ══════════════════════════════════════════════════ */

/*
 * Noir franc, et pas l'encre du thème : les bandes de `object-fit: contain`
 * doivent disparaître, pas se signaler comme un cadre.
 *
 * L'ancien parti pris remplissait ces bandes d'une copie très floutée du flux
 * — rien de perdu, rien de noir. Jugé plus gênant que les bandes elles-mêmes
 * (deux larges barres floues qui rétrécissent visuellement l'image), il est
 * retiré : plus de `.ambient`, plus de vignettage `.watch::after`.
 * `overflow: hidden` sur `.screen` reste utile pour d'autres raisons (les
 * halos de l'accueil), mais ce n'est plus le flou qu'il contient.
 */
.watch { background: #000; }

/*
 * Les deux surfaces d'image partagent EXACTEMENT la même boîte : le lecteur
 * choisi (MSE → <video>, WebCodecs → <canvas>) ne doit rien changer au cadrage,
 * à la place ni aux bandes. Une liste de sélecteurs plutôt qu'un doublon de
 * valeurs : la boîte est une seule décision, elle s'écrit une seule fois.
 * `object-fit` s'applique bien à un canvas — c'est un élément remplacé, dont la
 * taille intrinsèque est celle de ses attributs `width`/`height` (que
 * wc-player.js règle sur chaque trame ; laissés au défaut ils vaudraient
 * 300×150, et l'image serait encadrée en 2:1).
 * viewer.js pilote leur `display` : un seul des deux est affiché à la fois.
 */
#video,
#frameCanvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}

/* ── Spinner de mise en mémoire tampon ────────────────────────────────────
   C'est un signal d'anomalie : il n'est JAMAIS escamoté avec l'habillage.
   Le voile assombrit l'image figée pour qu'on ne prenne pas un gel pour une
   image fixe. */
.spinner {
  position: absolute;
  inset: 0;
  /* Sous l'habillage (6) et non au-dessus : son voile assombrit toute l'image,
     et il n'y a aucune raison d'assombrir aussi les contrôles. L'anneau est
     centré, la barre est en bas — ils ne se recouvrent pas. */
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  background: rgba(11, 13, 18, .35);
}
.spinner-ring {
  width: 52px;
  height: 52px;
  border: 3px solid rgba(255, 255, 255, .12);
  border-top-color: var(--lueur);
  border-radius: 50%;
  animation: beamcord-spin .8s linear infinite;
}
@keyframes beamcord-spin {
  to { transform: rotate(360deg); }
}

/* ── Voyant d'état ────────────────────────────────────────────────────────
 *
 * Verre neutre, pas pastille rouge : le texte est réécrit par viewer.js et va
 * de « En direct » à « Le host a arrêté le live. » — un fond rouge mentirait
 * une fois sur deux, et CSS ne peut pas lire un `textContent`.
 * Le point, lui, est un tally de régie : il dit que le canal est engagé.
 * En pseudo-élément, donc insensible aux réécritures de texte.
 */
.status-pill {
  position: absolute;
  top: var(--marge-t);
  left: var(--marge-h);
  z-index: 6;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  max-width: min(62vw, 420px);
  padding: 7px 14px 7px 12px;
  border: 1px solid var(--bord);
  border-radius: 999px;
  background: rgba(11, 13, 18, .55);
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  color: var(--clair);
  font-size: 12.5px;
  line-height: 1.35;
  transition: opacity .25s ease;
}
.status-pill::before {
  content: '';
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: #ff5560;
  animation: pulse-live 1.8s ease-out infinite;
}

/* ── Barre de contrôles ───────────────────────────────────────────────────
 *
 * Calquée sur la maquette du site (AppWindow.jsx) : un seul bandeau de verre,
 * télémétrie à gauche, contrôles à droite. Largeur bornée à 1200 px — le
 * conteneur du site — pour qu'en 4 K la barre ne s'étire pas d'un bord à
 * l'autre.
 *
 * `pointer-events: none` sur le bandeau, réarmé sur ses groupes : sans cela, un
 * bandeau large de 1200 px intercepterait le double-clic sur la vidéo (qui
 * bascule en plein écran) sur toute sa surface, y compris ses vides.
 */
.watch-bar {
  position: absolute;
  z-index: 6;
  left: var(--marge-h);
  right: var(--marge-h);
  bottom: var(--marge-b);
  max-width: 1200px;
  margin-inline: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--bord);
  border-radius: 12px;
  background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(16px) saturate(150%);
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  pointer-events: none;
  transition: opacity .25s ease;
}
.watch-bar > * { pointer-events: auto; }

/* `margin-right: auto` plutôt que `justify-content: space-between` : la
   télémétrie est masquée jusqu'à ce que le flux démarre, et les contrôles
   sauteraient alors à gauche puis à droite. */
.bar-tele {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  margin-right: auto;
}

/* ── Chips de télémétrie ──────────────────────────────────────────────────
 * Mono, petites, sur surface .07 : le registre « mesure » du site. Les deux
 * reçoivent un `display: block` en dur de viewer.js — parfaitement compatible
 * avec leur rôle d'élément flex, dont le display est de toute façon blocifié.
 */
.tier-badge,
.latency-badge {
  padding: 5px 8px;
  border-radius: 8px;
  background: var(--surface-2);
  color: rgba(238, 240, 245, .65);
  font-family: var(--police-mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: .02em;
  white-space: nowrap;
  transition: opacity .25s ease, background .16s ease, color .16s ease;
}

/* viewer.js réécrit `className` en entier (`'tier-badge ' + 'free'|'pro'`) :
   aucune autre classe ne peut être posée sur cet élément en HTML. */
.tier-badge {
  border: 1px solid transparent;
  text-transform: uppercase;
  letter-spacing: .08em;
}
.tier-badge.pro {
  /* Mise en valeur par l'accent, pas par une deuxième couleur : le site
     souligne toujours avec `bg-lueur/10 + border-lueur/40 + text-lueur-2`. */
  background: rgba(255, 148, 96, .12);
  border-color: rgba(255, 148, 96, .38);
  color: var(--lueur-2);
}

/* Bouton déguisé en chip : on remet à zéro l'apparence native avant de peindre.
   Cliquer revient au direct, d'où le curseur et la réaction au survol — le
   chiffre affiché EST le retard à rattraper. */
.latency-badge {
  appearance: none;
  -webkit-appearance: none;
  border: 1px solid transparent;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.latency-badge:hover {
  background: rgba(255, 148, 96, .12);
  border-color: rgba(255, 148, 96, .38);
  color: var(--lueur-2);
}
.latency-badge:active { transform: translateY(1px); }

/* ── Contrôle du son ──────────────────────────────────────────────────────
 * Plus de pilule de verre autonome : le bloc vit maintenant dans la barre,
 * qui porte la matière. viewer.js lui pose `display: flex` en dur.
 */
.sound-ctl {
  align-items: center;
  gap: 8px;
  transition: opacity .25s ease;
}
/* Le curseur n'a aucun effet là où `video.volume` est en lecture seule
   (iPhone) : viewer.js pose alors cette classe plutôt que d'afficher un
   réglage qui ne fait rien. */
.sound-ctl.no-volume .volume { display: none; }

/* Curseur de volume : on repart de zéro (`appearance: none`) car les styles
   natifs diffèrent trop d'un navigateur à l'autre pour cohabiter avec le
   reste. La piste est peinte par un dégradé dont l'arrêt est piloté depuis
   viewer.js via --fill : c'est ce qui donne la portion « remplie ». */
.volume {
  -webkit-appearance: none;
  appearance: none;
  width: 92px;
  height: 4px;
  border-radius: 999px;
  background: linear-gradient(
    to right,
    var(--lueur-2) 0%, var(--lueur) var(--fill, 100%),
    rgba(255, 255, 255, .16) var(--fill, 100%), rgba(255, 255, 255, .16) 100%);
  cursor: pointer;
  outline: none;
}
/* `outline: none` ci-dessus l'emporte sur l'anneau global (même spécificité,
   déclaré plus loin) : on le rétablit explicitement, sinon le curseur de volume
   serait le seul contrôle invisible au clavier. */
.volume:focus-visible {
  outline: 2px solid var(--lueur);
  outline-offset: 4px;
}
.volume::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--clair);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .55);
  cursor: pointer;
}
.volume::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border: 0;
  border-radius: 50%;
  background: var(--clair);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .55);
  cursor: pointer;
}
/* Firefox peint sa propre piste par-dessus le background de l'input. */
.volume::-moz-range-track { background: transparent; }

/* ── Actions du spectateur ────────────────────────────────────────────── */
.watch-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  transition: opacity .25s ease;
}

/* ── Icônes ───────────────────────────────────────────────────────────────
 *
 * Dessinées en CSS (masque SVG teinté par `currentColor`), pas en SVG inline.
 * Raison technique, pas esthétique : viewer.js réécrit le `textContent` de
 * #fsBtn (⛶ / ⤢) et de #soundBtn (🔇 / 🔉 / 🔊) pour refléter leur état, ce qui
 * effacerait n'importe quel enfant du bouton — un `<svg>` compris, dès le
 * premier passage en plein écran. Un pseudo-élément, lui, y survit.
 * `font-size: 0` sur le bouton avale le glyphe que le JS y réinjecte ; les
 * libellés qui doivent rester visibles reprennent leur taille (.act-lbl).
 * Le nom accessible vient des `aria-label` / `title`, que viewer.js tient à
 * jour — rien n'est perdu pour les lecteurs d'écran.
 *
 * Tracés repris de src/components/icons.jsx du site (trait 1,8, bouts ronds).
 * Dans un masque seule l'opacité compte : le noir n'est qu'un « opaque ».
 */
:root {
  --ico-switch: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 8.5h13.5l-3.6-3.6'/%3E%3Cpath d='M20 15.5H6.5l3.6 3.6'/%3E%3C/svg%3E");
  --ico-pip: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.9' y='4.6' width='18.2' height='14.8' rx='2.4'/%3E%3Crect x='11.8' y='11.5' width='7.3' height='6' rx='1.2' fill='%23000'/%3E%3C/svg%3E");
  --ico-expand: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 4H4v5'/%3E%3Cpath d='M15 4h5v5'/%3E%3Cpath d='M9 20H4v-5'/%3E%3Cpath d='M15 20h5v-5'/%3E%3C/svg%3E");
  --ico-collapse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9h5V4'/%3E%3Cpath d='M20 9h-5V4'/%3E%3Cpath d='M4 15h5v5'/%3E%3Cpath d='M20 15h-5v5'/%3E%3C/svg%3E");
  --ico-volume: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9.5v5h3.5L12 19V5L7.5 9.5H4z'/%3E%3Cpath d='M15.5 8.5a5 5 0 0 1 0 7'/%3E%3Cpath d='M18 6a8.5 8.5 0 0 1 0 12'/%3E%3C/svg%3E");
  --ico-mute: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 9.5v5h3.5L12 19V5L7.5 9.5H4z'/%3E%3Cpath d='M16 10l5 4'/%3E%3Cpath d='M21 10l-5 4'/%3E%3C/svg%3E");
  /* Repères de recadrage : un cadre en pointillés d'angles, avec l'image
     dedans. Se lit « cadrage » sans emprunter la métaphore du plein écran. */
  --ico-cadrage: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 3v14a1 1 0 0 0 1 1h13'/%3E%3Cpath d='M3 7h14a1 1 0 0 1 1 1v13'/%3E%3C/svg%3E");
  /* Masque vide, servi par défaut : un `--ico` oublié donnerait sinon un
     `mask-image` invalide, donc un carré plein de 18 px à la place de l'icône.
     Vide, l'icône est simplement absente — un défaut qui se voit sans salir. */
  --ico-vide: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3C/svg%3E");
}

.ico-btn {
  --ico: var(--ico-vide);
  appearance: none;
  -webkit-appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 34px;
  min-width: 34px;
  padding: 0 8px;
  border: 1px solid transparent;
  border-radius: 9px;
  background: transparent;
  color: var(--brume);
  font-size: 0;   /* avale le glyphe que viewer.js réinjecte — cf. bloc ci-dessus */
  cursor: pointer;
  transition: background .16s ease, color .16s ease, border-color .16s ease, opacity .25s ease;
}
.ico-btn::before {
  content: '';
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  background-color: currentColor;
  -webkit-mask-image: var(--ico);
  mask-image: var(--ico);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}
/* Les icônes de la maquette sont en `brume` au repos et s'éclairent au
   contact ; elles restent toujours visibles, donc rien n'est atteignable au
   seul survol — ce qui compte sur écran tactile. */
.ico-btn:hover {
  background: var(--surface-2);
  color: var(--clair);
}
.ico-btn:active { transform: translateY(1px); }

/* Libellé du bouton « Changer » : la seule action dont l'icône ne suffit pas à
   dire l'effet. Il reprend sa taille malgré le `font-size: 0` du bouton. */
.act-lbl {
  font-family: var(--police-texte);
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
}

#switchBtn { --ico: var(--ico-switch); padding-right: 12px; }
#pipBtn    { --ico: var(--ico-pip); }
#fsBtn     { --ico: var(--ico-expand); }
/* Deux règles séparées, pas une liste : un sélecteur préfixé inconnu
   invaliderait la règle entière chez l'autre moteur. */
#watch:fullscreen #fsBtn { --ico: var(--ico-collapse); }
#watch:-webkit-full-screen #fsBtn { --ico: var(--ico-collapse); }

.sound-btn { --ico: var(--ico-mute); }
/* viewer.js pose `.on` dès que le son est actif. */
.sound-btn.on { --ico: var(--ico-volume); color: var(--lueur-2); }
.sound-btn.on:hover { color: var(--lueur-2); }

/* ── Cadrage ───────────────────────────────────────────────────────────────
   Une seule icône (repères de recadrage) pour les deux états, et c'est
   volontaire : la paire « flèches vers l'extérieur / vers l'intérieur » est
   déjà celle du plein écran, la réutiliser ici ferait deux boutons qui se
   ressemblent pour deux fonctions différentes. L'état se lit à la teinte
   d'accent, comme le bouton de son, et le `title` dit ce que le clic FERA. */
#fitBtn { --ico: var(--ico-cadrage); }
/* viewer.js pose `.on` quand l'image remplit le cadre (donc rognée). */
#fitBtn.on { color: var(--lueur-2); }
#fitBtn.on:hover { color: var(--lueur-2); }

/* Remplir : l'image couvre tout le cadre, le débord est rogné. `contain` reste
   la valeur par défaut posée plus haut — on ne fait qu'y surcharger. Les deux
   surfaces sont visées : le basculement (touche R) doit marcher à l'identique,
   que le flux passe par MSE ou par WebCodecs. */
#video.fill,
#frameCanvas.fill { object-fit: cover; }

/* ═══ Habillage escamotable ════════════════════════════════════════════════
 *
 * viewer.js pose / retire la classe `chrome-hidden` sur #watch. Le contrat est
 * exactement celui-ci — ne pas l'altérer.
 *
 * ⚠️ Aucune règle `:hover` / `:focus` ne doit rendre ces éléments à nouveau
 * visibles : c'est le JS qui décide de ne pas armer sa minuterie quand le
 * curseur repose sur un contrôle. Une exception CSS ferait réapparaître
 * l'habillage sous le curseur alors que le JS le croit escamoté.
 *
 * #spinner n'y figure pas, et n'y figurera pas : c'est un signal d'anomalie.
 */
#watch.chrome-hidden { cursor: none; }
#watch.chrome-hidden #status,
#watch.chrome-hidden #soundCtl,
#watch.chrome-hidden .watch-actions,
#watch.chrome-hidden #tierBadge,
#watch.chrome-hidden #latencyBadge { opacity: 0; pointer-events: none; }

/* Ajouts au contrat, dans le même sens — pas contre lui :
   · le bandeau porte la matière de verre ; sans lui, ses enfants s'effaceraient
     en laissant une barre vide flotter sur l'image ;
   · le badge « Discord Activity » vit hors de #watch (il sert aussi sur
     l'écran d'accueil), donc aucune descendance ne peut l'atteindre : `:has()`
     le fait partir avec le reste. Là où `:has()` manque, il reste simplement
     affiché — dégradation sans casse. */
#watch.chrome-hidden .watch-bar { opacity: 0; pointer-events: none; }
body:has(#watch.chrome-hidden) .ctx-badge { opacity: 0; pointer-events: none; }

/* ═══ Adaptations ══════════════════════════════════════════════════════════ */

/* Écrans étroits (jusqu'à 320 px) : le libellé « Changer » cède la place, son
   `title` et son icône suffisent, et la barre se resserre. */
@media (max-width: 560px) {
  .act-lbl { display: none; }
  #switchBtn { padding-right: 8px; }
  .status-pill { max-width: min(72vw, 320px); font-size: 12px; }
  .watch-bar { gap: 6px; padding: 7px 8px; }
  .volume { width: 68px; }
  .ctx-badge { font-size: 9px; padding: 4px 9px; }
}

/* Très étroit : la télémétrie passe après les contrôles dans l'ordre des
   priorités — on tronque le tier plutôt que de replier la barre sur deux
   lignes, qui mangerait le bas de l'image. */
@media (max-width: 380px) {
  .bar-tele { overflow: hidden; }
  .tier-badge, .latency-badge { font-size: 10px; padding: 4px 7px; }
  .card { padding: 26px 20px 22px; }
  /* La pastille du héro tient tout juste sur 320 px : on la laisse se replier
     plutôt que déborder de la carte. */
  .pastille-live { white-space: normal; text-align: left; }
}

/* Doigt : 34 px se pointent à la souris, pas au doigt, et une piste de 4 px est
   intouchable. Recommandation d'accessibilité : 44 px. */
@media (pointer: coarse) {
  .ico-btn { height: 44px; min-width: 44px; padding: 0 12px; }
  #switchBtn { padding-right: 14px; }
  .ico-btn::before { width: 20px; height: 20px; }
  .volume { height: 6px; }
  .volume::-webkit-slider-thumb { width: 18px; height: 18px; }
  .volume::-moz-range-thumb { width: 18px; height: 18px; }
  .sound-ctl { gap: 10px; }
  .tier-badge, .latency-badge { padding: 7px 9px; }
}

/* Mouvement réduit : on éteint le décoratif (halos qui dérivent, onde du point
   de tally) et on garde le spinner, qui n'est pas une décoration mais le seul
   signe qu'une lecture est bloquée. */
@media (prefers-reduced-motion: reduce) {
  .halo,
  .point-live,
  .status-pill::before { animation: none; }
  .btn, .ico-btn, .latency-badge { transition: none; }
}
