/* Les jetons, et rien d'autre.
 *
 * Quatre blocs disent la même chose et doivent le dire à l'identique : le
 * défaut, la préférence du système, et les deux réglages explicites du
 * bascule. Un jeton ajouté ici sans l'être dans les trois autres se voit à
 * l'œil dans un seul thème, ce qui est la pire façon de le découvrir.
 *
 * --surface est la surface ordinaire, posée sur --bg. --panel est la seconde
 * élévation, pour ce qui encadre plutôt que ce qui contient : barres
 * latérales, panneaux, chrome. Elle monte dans le sombre et descend dans le
 * clair — « distincte de la surface », pas « plus claire ».
 *
 * --accent-soft est *dérivée* et non écrite : layout.html laisse un site
 * remplacer --accent par sa propre couleur, et une teinte douce codée en dur
 * cessait alors de lui correspondre. --accent-contrast ne peut pas l'être :
 * choisir entre texte clair et texte sombre demande de comparer des
 * luminances, ce que CSS ne sait pas faire (color-contrast() n'est pas
 * déployé). Elle reste donc écrite par thème, en accord avec la clarté de
 * l'accent de ce thème — un site dont l'accent s'écarte beaucoup du nôtre
 * devra la revoir, et c'est la limite honnête de ce mécanisme.
 */
:root {
  color-scheme: light dark;

  --bg: #f5f5f7;
  --surface: #ffffff;
  --panel: #edeef1;
  --fg: #15161a;
  --muted: #6e7079;
  --border: #e3e4e9;
  --border-strong: #d0d1d9;

  --accent: #b4530b;
  --accent-hover: #8f4109;
  --accent-soft: color-mix(in srgb, var(--accent) 10%, var(--surface));
  --accent-contrast: #ffffff;

  --error-bg: #fdecec;
  --error-fg: #b3261e;
  --error-border: #f6c9c7;
  --ok-fg: #1a7f4b;

  --code-bg: #f0f0f4;
  --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.05);
  --shadow-md: 0 6px 20px rgba(20, 20, 30, 0.08);
  --radius: 10px;
  --radius-sm: 6px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #101114;
    --surface: #191b1f;
    --panel: #212328;
    --fg: #f3f4f6;
    --muted: #a2a4ad;
    --border: #2c2e35;
    --border-strong: #3a3d45;

    --accent: #f08a3c;
    --accent-hover: #f7a663;
    --accent-soft: color-mix(in srgb, var(--accent) 16%, var(--surface));
    --accent-contrast: #150d05;

    --error-bg: #2a1618;
    --error-fg: #f2a3a1;
    --error-border: #4a2224;
    --ok-fg: #74d3a0;

    --code-bg: #1d1f25;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 10px 30px rgba(0, 0, 0, 0.45);
  }
}

/* Explicit overrides driven by the theme toggle (data-theme attribute on
   <html>). These use a higher-specificity attribute selector so they always
   win over the prefers-color-scheme defaults above, in either direction. */
:root[data-theme="light"] {
  /* Le bascule doit emporter les contrôles natifs avec lui. Sans cette ligne,
     color-scheme reste « light dark » et le navigateur habille boutons, listes
     et champs selon la préférence du système — si bien qu'un thème clair choisi
     sur une machine réglée en sombre donnait des boutons gris foncé portant du
     texte gris clair, illisibles. La page obéissait au réglage, les contrôles
     non. */
  color-scheme: light;

  --bg: #f5f5f7;
  --surface: #ffffff;
  --panel: #edeef1;
  --fg: #15161a;
  --muted: #6e7079;
  --border: #e3e4e9;
  --border-strong: #d0d1d9;

  --accent: #b4530b;
  --accent-hover: #8f4109;
  --accent-soft: color-mix(in srgb, var(--accent) 10%, var(--surface));
  --accent-contrast: #ffffff;

  --error-bg: #fdecec;
  --error-fg: #b3261e;
  --error-border: #f6c9c7;
  --ok-fg: #1a7f4b;

  --code-bg: #f0f0f4;
  --shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.05);
  --shadow-md: 0 6px 20px rgba(20, 20, 30, 0.08);
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: #101114;
  --surface: #191b1f;
  --panel: #212328;
  --fg: #f3f4f6;
  --muted: #a2a4ad;
  --border: #2c2e35;
  --border-strong: #3a3d45;

  --accent: #f08a3c;
  --accent-hover: #f7a663;
  --accent-soft: color-mix(in srgb, var(--accent) 16%, var(--surface));
  --accent-contrast: #150d05;

  --error-bg: #2a1618;
  --error-fg: #f2a3a1;
  --error-border: #4a2224;
  --ok-fg: #74d3a0;

  --code-bg: #1d1f25;
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  --shadow-md: 0 10px 30px rgba(0, 0, 0, 0.45);
}

* { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", Roboto,
    Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--fg);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--accent);
  text-decoration: none;
  text-underline-offset: 3px;
}
a:hover { text-decoration: underline; }

/* Un titre de page se lit d'un coup d'œil : plus grand, plus serré, posé sur
   sa ligne plutôt qu'aéré. Le resserrement (letter-spacing négatif) est ce qui
   fait qu'une grande taille reste un titre et ne devient pas une bannière. */
h1 {
  font-size: 1.65rem;
  font-weight: 700;
  letter-spacing: -0.022em;
  line-height: 1.15;
  margin: 0 0 0.25rem;
}

.subtitle {
  color: var(--muted);
  font-size: 0.92rem;
  margin: 0;
  max-width: 68ch;
  text-wrap: pretty;
}

/* L'étiquette de section : petite, capitales, très espacée. C'est le seul
   endroit où espacer les lettres aide à lire — un mot court en capitales a
   besoin d'air pour ne pas se tasser — et c'est ce qui donne le rythme d'une
   page sans y ajouter une seule couleur. */
.eyebrow {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Une étiquette de section suivie d'un filet qui va jusqu'au bout, et
   éventuellement d'un compte à droite. */
.section-rule {
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
  margin: 0 0 0.9rem;
}
.section-rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}
.section-rule > .count {
  order: 1;
  font-size: 0.75rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.subtitle-inline {
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 400;
  margin-left: 0.5rem;
}

/* ---------- Topbar ---------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: 10;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: saturate(180%) blur(10px);
  border-bottom: 1px solid var(--border);
}

.topbar-inner {
  max-width: 860px;
  margin: 0 auto;
  padding: 0.7rem 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-weight: 700;
  font-size: 1.02rem;
  letter-spacing: -0.01em;
  color: var(--fg);
}
.brand:hover { text-decoration: none; }

/* Le dégradé se déduit de l'accent au lieu de nommer une seconde couleur :
   il tenait un violet clair, choisi pour l'indigo d'alors, qui jurait dès que
   l'accent changeait — et l'accent change, chaque site pouvant poser le sien. */
.brand-mark {
  width: 20px;
  height: 20px;
  border-radius: 6px;
  background: linear-gradient(
    135deg,
    var(--accent),
    color-mix(in srgb, var(--accent) 55%, #ffffff)
  );
  box-shadow: var(--shadow-sm);
  flex-shrink: 0;
}

.topbar-nav,
.topbar-anon {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.topbar-nav { margin-right: auto; margin-left: 1.5rem; }

.topbar-nav > a,
.topbar-anon > a {
  color: var(--muted);
  font-size: 0.9rem;
  font-weight: 500;
  padding: 0.4rem 0.7rem;
  border-radius: var(--radius-sm);
}
.topbar-nav > a:hover,
.topbar-anon > a:hover {
  color: var(--fg);
  background: var(--accent-soft);
  text-decoration: none;
}

/* Everything that isn't a destination — administration, preferences, signing
   out — behind one control, so the bar stays three items wide whatever the
   account can do. Built on <details> so it opens without JavaScript. */
.menu { position: relative; }
.menu > summary {
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::after {
  content: "";
  display: inline-block;
  margin-left: 0.4rem;
  border: 4px solid transparent;
  border-top-color: currentColor;
  vertical-align: middle;
  transform: translateY(2px);
}
.menu[open] > summary { color: var(--fg); border-color: var(--border-strong); }

.menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 0.5rem);
  z-index: 20;
  min-width: 15rem;
  padding: 0.4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}

.menu-panel a,
.menu-logout button {
  display: block;
  width: 100%;
  padding: 0.4rem 0.6rem;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--fg);
  font: inherit;
  font-size: 0.9rem;
  text-align: left;
  cursor: pointer;
}
.menu-panel a:hover,
.menu-logout button:hover { background: var(--accent-soft); color: var(--accent); text-decoration: none; }

.menu-group {
  margin-top: 0.3rem;
  padding-top: 0.3rem;
  border-top: 1px solid var(--border);
}

.menu-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.3rem 0.6rem;
  color: var(--muted);
  font-size: 0.85rem;
}
.menu-row select { font-size: 0.85rem; }
.menu-logout { margin-top: 0.3rem; padding-top: 0.3rem; border-top: 1px solid var(--border); }

.user-chip {
  color: var(--muted);
  font-size: 0.85rem;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
}

form.inline { display: inline; }

.topbar-right {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.prefs { display: flex; align-items: center; }

.font-controls {
  display: flex;
  align-items: center;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.font-controls button {
  background: var(--surface);
  color: var(--muted);
  border: none;
  padding: 0.4rem 0.6rem;
  font-size: 0.78rem;
  font-weight: 700;
  cursor: pointer;
  line-height: 1;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.font-controls button:not(:last-child) { border-right: 1px solid var(--border); }
.font-controls button:hover { color: var(--fg); background: var(--accent-soft); }

.btn-icon {
  width: 34px;
  height: 34px;
  padding: 0;
  justify-content: center;
}

.icon-moon { display: none; }
:root[data-theme="dark"] .icon-sun { display: none; }
:root[data-theme="dark"] .icon-moon { display: inline-block; }

/* ---------- Layout ---------- */

.container {
  max-width: 860px;
  margin: 0 auto;
  /* La marge basse dégage la barre de lecture, qui est fixée par-dessus la
     page. Sa hauteur est mesurée par queue.js et publiée ici : elle vaut zéro
     quand la file est vide, et davantage quand la barre se replie ou que le
     texte est agrandi. Écrite en rem, elle était juste sur un écran et fausse
     sur le suivant — les derniers résultats passaient sous la barre. */
  padding: 2.5rem 1.5rem 1.5rem;
}

/* 860 px est une largeur de lecture : c'est ce qu'il faut pour une colonne de
   texte, et c'est pour cela que tout le site s'y tient. Une page qui montre
   une image n'a pas cette contrainte — au contraire, elle l'étouffe : trois
   colonnes dans 860 px laissaient 260 px à la vidéo. */
.container-wide {
  max-width: 1500px;
  /* Chaque rem repris en haut est une ligne de résultats de plus. La marge
     basse reste : c'est elle qui dégage la barre de lecture. */
  padding-top: 1.25rem;
}

.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
  margin-bottom: 1.75rem;
}

.page-header .actions {
  display: flex;
  gap: 0.5rem;
  margin-left: auto;
}

/* ---------- Buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font: inherit;
  font-weight: 600;
  font-size: 0.9rem;
  padding: 0.55rem 1.1rem;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease, background-color 0.12s ease, border-color 0.12s ease;
}
.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(1px); }

.btn-primary {
  background: var(--accent);
  color: var(--accent-contrast);
  box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
  background: var(--accent-hover);
  box-shadow: var(--shadow-md);
}

.btn-secondary {
  background: var(--surface);
  color: var(--fg);
  border-color: var(--border-strong);
}
.btn-secondary:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.btn-ghost {
  background: transparent;
  color: var(--muted);
  border-color: var(--border);
}
.btn-ghost:hover {
  color: var(--fg);
  border-color: var(--border-strong);
}

.btn-sm { padding: 0.4rem 0.8rem; font-size: 0.85rem; }
.btn-block { width: 100%; justify-content: center; }

.icon {
  width: 1em;
  height: 1em;
  font-size: 1.1em;
  flex-shrink: 0;
}

/* ---------- Forms ---------- */

.form {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  max-width: 420px;
}

.form.form-wide { max-width: 720px; }

.form label {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--muted);
}

input, textarea, button { font: inherit; }

input[type="text"], input[type="email"], input[type="password"],
input[type="search"], textarea {
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--fg);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}

input:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

textarea {
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.88rem;
  resize: vertical;
  line-height: 1.6;
}

.form button[type="submit"] { align-self: flex-start; }

/* ---------- Markdown editor toolbar ---------- */

.editor-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.25rem;
  padding: 0.4rem;
  background: var(--code-bg);
  border: 1px solid var(--border-strong);
  border-bottom: none;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

#editor {
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}

.toolbar-sep {
  width: 1px;
  align-self: stretch;
  margin: 0.2rem 0.15rem;
  background: var(--border-strong);
}

.ed-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--fg);
  font-size: 0.85rem;
  cursor: pointer;
  line-height: 1;
}
.ed-btn:hover {
  background: var(--surface);
  border-color: var(--border-strong);
}
.ed-btn-wide { font-size: 0.8rem; font-weight: 600; }

.ed-select {
  padding: 0.35rem 0.5rem;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--fg);
  font-size: 0.82rem;
}
.ed-select:hover { border-color: var(--border-strong); }

/* ---------- Auth screens ---------- */

.auth-screen {
  display: flex;
  justify-content: center;
  padding-top: 1.5rem;
}

.auth-card {
  width: 100%;
  max-width: 380px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2rem;
  box-shadow: var(--shadow-sm);
}

.auth-card .form { max-width: none; margin-top: 1.5rem; }

.auth-switch {
  margin: 1.5rem 0 0;
  font-size: 0.88rem;
  color: var(--muted);
  text-align: center;
}

/* ---------- Search ---------- */

.search {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 0 0.75rem;
  height: 40px;
}
.search .icon { color: var(--muted); }
.search input {
  border: none;
  padding: 0;
  height: 100%;
  background: transparent;
  box-shadow: none !important;
}

/* ---------- Page list ---------- */

.page-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.page-card {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.9rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--fg);
  transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease;
}
.page-card:hover {
  text-decoration: none;
  border-color: var(--border-strong);
  box-shadow: var(--shadow-md);
  transform: translateY(-1px);
}

.page-card-icon {
  width: 34px;
  height: 34px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent);
}
.page-card-icon svg { width: 17px; height: 17px; }

.page-card-body {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.page-card-title {
  font-weight: 600;
  font-size: 0.98rem;
}

.meta { color: var(--muted); font-size: 0.82rem; }

/* ---------- Empty state ---------- */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  text-align: center;
  padding: 3.5rem 1.5rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  color: var(--muted);
}

/* ---------- Timeline (revision history) ---------- */

.timeline {
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  display: flex;
  flex-direction: column;
}

.timeline-item {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.7rem 0.5rem;
  border-radius: var(--radius-sm);
  color: var(--fg);
}
.timeline-item:hover {
  background: var(--accent-soft);
  text-decoration: none;
}

.timeline-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
}

.timeline-body {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  min-width: 0;
}

.timeline-title { font-weight: 500; }

.back-link {
  color: var(--muted);
  font-size: 0.9rem;
}

.history-link {
  margin-top: 3rem;
  padding-top: 1rem;
  border-top: 1px solid var(--border);
}
.history-link a {
  color: var(--muted);
  font-size: 0.8rem;
}

/* ---------- Flash messages ---------- */

.flash {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 1rem;
  border-radius: var(--radius-sm);
  margin-bottom: 1.25rem;
  font-size: 0.9rem;
}

.flash-error {
  background: var(--error-bg);
  color: var(--error-fg);
  border: 1px solid var(--error-border);
}

/* ---------- Markdown content ---------- */

.markdown-body {
  font-size: 1rem;
}

.markdown-body :is(h1, h2, h3, h4) {
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-top: 1.8em;
  margin-bottom: 0.6em;
}
.markdown-body h1 { font-size: 1.5rem; }
.markdown-body h2 { font-size: 1.25rem; }
.markdown-body h3 { font-size: 1.05rem; }

.markdown-body p { margin: 0.9em 0; }

.markdown-body a { text-decoration: underline; }

.markdown-body ul, .markdown-body ol {
  padding-left: 1.4em;
}

.markdown-body blockquote {
  margin: 1em 0;
  padding: 0.3rem 1rem;
  border-left: 3px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  color: var(--muted);
}

.markdown-body pre {
  background: var(--code-bg);
  padding: 0.9rem 1.1rem;
  border-radius: var(--radius-sm);
  overflow-x: auto;
  border: 1px solid var(--border);
}

.markdown-body code {
  background: var(--code-bg);
  padding: 0.15em 0.4em;
  border-radius: 5px;
  font-size: 0.88em;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}
.markdown-body pre code { background: none; padding: 0; }

.markdown-body table {
  border-collapse: collapse;
  width: 100%;
  margin: 1em 0;
  font-size: 0.92rem;
}
.markdown-body th, .markdown-body td {
  border: 1px solid var(--border);
  padding: 0.5rem 0.75rem;
  text-align: left;
}
.markdown-body th { background: var(--code-bg); font-weight: 600; }

.markdown-body hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 2em 0;
}

.markdown-body img { max-width: 100%; border-radius: var(--radius-sm); }
.markdown-body video, .markdown-body audio { max-width: 100%; border-radius: var(--radius-sm); }
.markdown-body audio { width: 100%; }

/* ---------- Media library ---------- */

.upload-form {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem;
  margin-bottom: 1.75rem;
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
}

.upload-form input[type="file"] {
  flex: 1;
  min-width: 0;
  font-size: 0.85rem;
  color: var(--muted);
}

.media-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 1rem;
}

.media-card {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: box-shadow 0.12s ease, border-color 0.12s ease;
}
.media-card:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-md);
}

.media-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 140px;
  background: var(--code-bg);
  overflow: hidden;
}
.media-preview img, .media-preview video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.media-preview-audio {
  padding: 0 1rem;
}
.media-preview-audio audio { width: 100%; }

.media-info {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.85rem 1rem;
}

.media-name {
  font-weight: 600;
  font-size: 0.9rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.media-delete {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0 1rem 0.85rem;
}

.form-hint {
  font-size: 0.85rem;
  color: var(--muted);
  max-width: 720px;
  margin: -0.75rem 0 1.5rem;
}
.form-hint code {
  background: var(--code-bg);
  padding: 0.1em 0.4em;
  border-radius: 5px;
  font-size: 0.9em;
}

/* ---------- Super-admin "switched site" banner ---------- */

.switched-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.5rem 1rem;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 0.85rem;
  text-align: center;
}

.link-button {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

/* ---------- Forms: select ---------- */

select {
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--fg);
}
select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* ---------- Permissions (pages & media) ---------- */

.badge {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--muted);
  background: var(--code-bg);
}
.badge-group {
  color: var(--accent);
  background: var(--accent-soft);
}

.permission-list {
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.permission-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.permission-principal {
  font-weight: 600;
  flex: 1;
}

.permission-level {
  color: var(--muted);
  font-size: 0.85rem;
}

.permission-remove { margin: 0; }

/* The "grant access" form lays its fields out in a row on wide screens and
   stacks them on narrow ones, the submit button aligning with the inputs
   rather than with the labels above them. */
.permission-add {
  max-width: none;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem 1rem;
  padding: 1rem;
  margin-bottom: 1.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.permission-add label:first-of-type { flex: 1 1 18rem; }
.permission-add button[type="submit"] { align-self: flex-end; }

/* Text field + "Parcourir…" button sharing one line. */
.principal-field {
  display: flex;
  gap: 0.4rem;
}
.principal-field input[type="text"] {
  flex: 1;
  min-width: 0;
}
.principal-field button { white-space: nowrap; }

.delete-form {
  margin: 1.5rem 0;
}

h2 {
  font-size: 1.1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 2rem 0 1rem;
}

/* ---------- Link/media picker modal ---------- */

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 10vh 1rem;
  z-index: 100;
}
.modal-overlay[hidden] { display: none; }

.modal {
  width: 100%;
  max-width: 480px;
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
}
.modal-header h2 { margin: 0; font-size: 1rem; }

.modal-close {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.2rem 0.4rem;
}
.modal-close:hover { color: var(--fg); }

.modal-controls {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem 1.25rem;
  border-bottom: 1px solid var(--border);
}
.modal-controls input[type="search"] { flex: 1; }
.modal-controls select { flex-shrink: 0; }

.picker-results {
  list-style: none;
  margin: 0;
  padding: 0.5rem;
  overflow-y: auto;
}

.picker-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  padding: 0.55rem 0.75rem;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--fg);
  text-align: left;
  cursor: pointer;
  font: inherit;
}
.picker-item:hover { background: var(--accent-soft); }

.picker-item-body {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

.picker-item-title {
  font-weight: 600;
  font-size: 0.9rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.picker-item-meta {
  font-size: 0.78rem;
  color: var(--muted);
}

#picker-empty { padding: 1rem 1.25rem; }

/* ---------- Media library: collections, imports, transcoding ---------- */

/* Same role as .page-header .actions, under the name the newer templates use. */
.page-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-left: auto;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.quality-form select {
  padding: 0.3rem 0.5rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--fg);
  font-size: 0.8rem;
}

.checkbox-label {
  flex-direction: row !important;
  align-items: center;
  gap: 0.5rem !important;
}

/* Collections list: a poster wall that reflows from 2 columns on a phone to
   as many as fit on a wide screen. */
/* ---------- Médiathèque ---------- */

/* One grid for the whole library: a film, an album and a lone file are the
   same kind of thing to whoever is browsing, so they get the same card. */
.library-grid {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 1.25rem;
}

.library-card {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
}

.library-cover {
  position: relative;
  display: block;
  aspect-ratio: 2 / 3;
  overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--code-bg);
}
.library-cover img { width: 100%; height: 100%; object-fit: cover; }

/* A file that isn't a picture gets its play button filling the same frame,
   so the grid keeps one rhythm whatever the entry is. */
.library-cover-empty,
.library-cover-audio,
.library-cover-video,
.library-cover-subtitle,
.library-cover-playlist {
  display: flex;
  align-items: center;
  justify-content: center;
}
.library-cover-empty { width: 100%; height: 100%; font-size: 2.5rem; opacity: 0.4; }

.library-body {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 0;
}

.library-title {
  font-weight: 600;
  color: inherit;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
a.library-title:hover { color: var(--accent); }

.library-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.2rem;
}

/* Le cadre appartient au groupe et non au champ : la loupe est ainsi dedans
   plutôt qu'à côté, et les deux ne forment qu'une commande. Le champ perd son
   bord et son fond au profit du groupe, qui reprend aussi son état de focus —
   sans quoi taper dans le champ n'allumerait que la moitié de ce qu'on voit. */
.library-search {
  display: flex;
  align-items: center;
  margin-bottom: 1rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.library-search:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.library-search input[type="search"] {
  flex: 1;
  min-width: 0;
  border: 0;
  background: none;
  border-radius: inherit;
}
/* Le halo du champ ferait double emploi avec celui du groupe, et se
   dessinerait à l'intérieur du cadre. */
.library-search input[type="search"]:focus { box-shadow: none; }
.library-search .icon-button {
  flex: none;
  margin-right: 0.3rem;
}

/* While a search is in flight the list stays readable but says it is stale. */
#library-results[aria-busy="true"] { opacity: 0.55; transition: opacity 0.15s ease; }


.player video {
  width: 100%;
  max-height: 75vh;
  background: #000;
  border-radius: var(--radius);
}

/* Sits under a player fed by a live conversion, where the native timeline
   only covers what has already been received. */
.live-seek {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.5rem;
}
.live-seek input[type="range"] { flex: 1; accent-color: var(--accent); }
.live-seek-time { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* La liste des titres occupe sa section entière : la pochette est montée dans
   l'en-tête commun, et l'album n'a plus deux colonnes à composer. */
.track-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.track {
  display: flex;
  gap: 0.75rem;
  padding: 0.6rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.track-no {
  min-width: 1.5rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.track-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}

.track-title { font-weight: 600; color: inherit; }
a.track-title:hover { color: var(--accent); }
.track audio { width: 100%; }

/* Une sortie. Les séquences se lisent comme des pistes — même ligne, même
   bouton — mais avec l'image en tête : quarante clips nommés GX010399,
   GX010400, GX010401 ne se distinguent que par ce qu'on y voit. */
.clip-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.clip { align-items: center; }

.clip-thumb {
  flex: 0 0 auto;
  width: 96px;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--panel2, var(--surface));
}

.clip-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

.photo-grid {
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 0.5rem;
}

.photo-grid img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}

.trace-list { margin: 0 0 1.5rem 1.2rem; }

/* Import roots. */
.import-list {
  list-style: none;
  padding: 0;
  margin: 0 0 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.import-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.import-main {
  flex: 1 1 260px;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
}

.import-path { word-break: break-all; }

.import-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.scan-result {
  padding: 1rem 1.25rem;
  margin-bottom: 1.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.scan-list {
  margin: 0.5rem 0 0;
  padding-left: 1.25rem;
  max-height: 18rem;
  overflow-y: auto;
  font-size: 0.85rem;
}

/* Transcode status. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1rem;
  margin: 0 0 1.5rem;
}

.stat-grid > div {
  padding: 0.75rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.stat-grid dt {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.stat-grid dd {
  margin: 0.25rem 0 0;
  font-size: 1.05rem;
}

/* Une proposition de série : un titre modifiable, un bouton, et la liste des
   fichiers repliée en dessous — on l'ouvre pour vérifier avant d'accepter. */
/* Un formulaire replié derrière son bouton. La classe is-foldable est posée
   par le script : sans lui rien n'est caché, et la page reste utilisable. */
.is-foldable > .editable-form { display: none; }
.is-foldable.is-editing > .editable-form { display: block; }
.is-foldable.is-editing > [data-edit-toggle] { display: none; }

.cell-actions { white-space: nowrap; }

.cell-forms {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
}

/* Le titre de la section et son bouton sur une seule ligne. */
.identify-head {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}

.identify-head h2 { margin-bottom: 0.4rem; }

.identify-block { margin-bottom: 1.5rem; }

/* Les liens vers les fiches externes, séparés d'eux-mêmes. */
.identify-links a + a::before {
  content: " · ";
  color: var(--muted);
}

.proposal {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.9rem 1rem;
  margin-bottom: 0.75rem;
}

.proposal-head {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

.proposal-head input[type="text"] { flex: 1 1 16rem; }

.proposal-files {
  margin: 0.5rem 0 0;
  padding-left: 1.2rem;
  max-height: 18rem;
  overflow-y: auto;
}

.proposal-files li { margin-bottom: 0.2rem; }

.identify-list {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
}

.transcode-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

/* Wide tables scroll inside their own box rather than making the page
   scroll sideways. */
.table-scroll {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}

.data-table th,
.data-table td {
  padding: 0.55rem 0.8rem;
  text-align: left;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

.data-table th {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
  background: var(--code-bg);
}

.data-table tr:last-child td { border-bottom: none; }

/* Figures line up on the right and share a digit width: a column of numbers is
   read by comparing them, which needs the digits to sit above one another. */
.data-table th.num,
.data-table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.cell-error {
  max-width: 28rem;
  color: var(--muted);
  word-break: break-word;
}

/* ---------- Pagination ---------- */

.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin: 1.25rem 0;
}

.pager-count {
  color: var(--muted);
  font-size: 0.85rem;
}

.pager-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.pager-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  padding: 0.3rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}
.pager-link:hover { color: var(--fg); border-color: var(--border-strong); text-decoration: none; }
.pager-link.is-current {
  color: var(--accent-contrast);
  background: var(--accent);
  border-color: var(--accent);
  font-weight: 600;
}
.pager-link.is-disabled { opacity: 0.35; }
.pager-gap { padding: 0.3rem 0.2rem; color: var(--muted); }

/* ---------- Media library rows ---------- */

/* The stand-in for a player that hasn't been created yet. */
.media-play {
  width: 100%;
  height: 100%;
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 1.4rem;
  cursor: pointer;
  transition: background-color 0.12s ease, color 0.12s ease;
}
.media-play:hover { background: var(--accent-soft); color: var(--accent); }

.media-player-slot:empty { display: none; }
.media-player-slot audio,
.media-player-slot video { width: 100%; margin-top: 0.4rem; }
.media-player-slot video { max-height: 60vh; background: #000; border-radius: var(--radius-sm); }

/* ---------- Task manager ---------- */

.progress-card {
  padding: 1rem 1.25rem;
  margin-bottom: 1.25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.progress-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 0.5rem;
}

.progress-track {
  height: 8px;
  border-radius: 999px;
  background: var(--code-bg);
  overflow: hidden;
}

.progress-fill {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width 0.3s ease;
}

.progress-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.1rem;
  margin-top: 0.6rem;
  color: var(--muted);
  font-size: 0.85rem;
}
.progress-legend b { color: var(--fg); font-variant-numeric: tabular-nums; }

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}

/* Fond déclaré et non hérité : ces puces sont des <button>, et un bouton sans
   fond prend celui du navigateur, qui n'est pas le nôtre. */
.filter-chip {
  padding: 0.3rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-size: 0.85rem;
}
.filter-chip:hover { color: var(--fg); border-color: var(--border-strong); text-decoration: none; }
.filter-chip.is-current {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent);
  font-weight: 600;
}

.autorefresh {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--muted);
  font-size: 0.85rem;
}

/* Per-state colouring, so a wall of rows reads at a glance. */
.state { font-weight: 600; }
.state-running { color: var(--accent); }
.state-failed { color: var(--error-fg); }
.state-skipped,
.state-done { color: var(--muted); }

/* ---------- Journal ---------- */

.log-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.6rem 0.9rem;
  padding: 0.9rem 1rem;
  margin-bottom: 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.log-filters label { display: flex; flex-direction: column; gap: 0.25rem; }
.log-filters label.grow { flex: 1; min-width: 12rem; }
.log-filters label span { color: var(--muted); font-size: 0.8rem; }
.log-filter-actions { display: flex; gap: 0.4rem; }

/* One column per hour or per day, tallest at 100%. */
.histogram {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 72px;
  margin-top: 0.9rem;
}
.histogram-col { flex: 1; height: 100%; display: flex; align-items: flex-end; }
.histogram-bar {
  position: relative;
  width: 100%;
  background: var(--accent);
  border-radius: 2px 2px 0 0;
  min-height: 2px;
}
/* The failures sit at the top of their own column, so the eye finds them. */
.histogram-bar-errors {
  position: absolute;
  inset: 0 0 auto 0;
  background: var(--error-fg);
  border-radius: 2px 2px 0 0;
}

.breakdowns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 1rem;
  margin-bottom: 1.25rem;
}
.breakdown {
  padding: 0.8rem 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.breakdown h2 { margin: 0 0 0.5rem; font-size: 0.85rem; color: var(--muted); font-weight: 600; }
.breakdown-list { list-style: none; margin: 0; padding: 0; }
.breakdown-list li {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.15rem 0;
  font-size: 0.9rem;
}
.breakdown-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.breakdown-count { font-variant-numeric: tabular-nums; color: var(--muted); }

.log-table { font-size: 0.9rem; }
.log-time { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--muted); }
.log-row .meta { margin-left: 0.4rem; }
.log-ERROR .badge-ERROR { color: var(--error-fg); border-color: var(--error-fg); }
.log-WARN .badge-WARN { color: var(--accent); border-color: var(--accent); }

/* ---------- Jetons d'API ---------- */

.tokens { margin-top: 2.5rem; }
.tokens h2 { font-size: 1.1rem; margin-bottom: 0.25rem; }

.token-create {
  display: flex;
  gap: 0.5rem;
  margin: 1rem 0;
}
.token-create input { flex: 1; max-width: 22rem; }

/* Shown once, so it has to be easy to copy and hard to miss. */
.token-reveal { display: block; }
.token-secret {
  width: 100%;
  margin-top: 0.5rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9rem;
}

.token-revoked { opacity: 0.55; }

/* ---------- Traces GPS ---------- */

.gps-map {
  position: relative;
  height: 320px;
  margin: 1rem 0;
  overflow: hidden;
  background: var(--code-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.gps-tiles { position: absolute; inset: 0; }
.gps-tiles img {
  position: absolute;
  width: 256px;
  height: 256px;
  /* A basemap is drawn for daylight; dimming it in the dark theme keeps the
     trace on top of it readable rather than lost in glare. */
  filter: none;
}
:root[data-theme="dark"] .gps-tiles img { filter: brightness(0.75) saturate(0.85); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .gps-tiles img { filter: brightness(0.75) saturate(0.85); }
}

.gps-line { position: absolute; inset: 0; width: 100%; height: 100%; }
.gps-path-halo {
  fill: none;
  stroke: #fff;
  stroke-width: 6;
  stroke-linejoin: round;
  stroke-linecap: round;
  opacity: 0.8;
}
.gps-path {
  fill: none;
  stroke: var(--accent);
  stroke-width: 3;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.gps-start { fill: #16a34a; stroke: #fff; stroke-width: 2; }
.gps-end { fill: var(--error-fg); stroke: #fff; stroke-width: 2; }

.gps-caption {
  position: absolute;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: 0.15rem 0.5rem;
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  border-top-left-radius: var(--radius-sm);
  font-size: 0.78rem;
}

/* Inside a médiathèque card the frame is smaller, and the caption would
   cover a third of it. */
.media-player-slot .gps-map { height: 220px; }

/* ---------- File de lecture ---------- */

/* At the bottom rather than in the header: it needs width, and it only
   exists while something is playing — the navigation is three items and
   stays that way. */
/* La barre est la dernière rangée de la coquille, et non plus une bande
   collée au bas de la fenêtre. C'est ce qui permettra à la vidéo de descendre
   jusqu'à elle sans quitter le document — un <video> qu'on sort du DOM se met
   en pause, et c'est l'invariant que toute cette grille sert à tenir.

   position: relative parce que la progression se pose sur son bord haut. */
.queue-bar {
  position: relative;
  grid-column: 1 / -1;
  grid-row: -2 / -1;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1rem 0.5rem;
  background: color-mix(in srgb, var(--surface) 94%, transparent);
  backdrop-filter: saturate(180%) blur(10px);
  border-top: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
}

/* La progression, sur le bord haut de la barre et sur toute sa largeur.
   Trois pixels de trait — mais dix de prise : un curseur qu'on ne peut pas
   viser n'est pas un curseur, et c'est la moitié qu'on oublie en amincissant.
   Il s'épaissit au survol, où l'on est justement en train de viser. */
.queue-seek {
  position: absolute;
  left: 0;
  right: 0;
  top: -4px;
  width: 100%;
  height: 10px;
  margin: 0;
  padding: 0;
  background: none;
  appearance: none;
  cursor: pointer;
}
.queue-seek::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 2px;
  background: var(--border-strong);
}
.queue-seek::-moz-range-track {
  height: 3px;
  border-radius: 2px;
  background: var(--border-strong);
}
.queue-seek::-moz-range-progress { height: 3px; border-radius: 2px; background: var(--accent); }
.queue-seek::-webkit-slider-thumb {
  appearance: none;
  width: 11px;
  height: 11px;
  margin-top: -4px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0;
  transition: opacity 90ms ease-out;
}
.queue-seek::-moz-range-thumb {
  width: 11px;
  height: 11px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0;
  transition: opacity 90ms ease-out;
}
.queue-bar:hover .queue-seek::-webkit-slider-thumb,
.queue-seek:focus-visible::-webkit-slider-thumb { opacity: 1; }
.queue-bar:hover .queue-seek::-moz-range-thumb,
.queue-seek:focus-visible::-moz-range-thumb { opacity: 1; }
.queue-bar[hidden] { display: none; }

/* L'élément qui fait le son. Il est dans la barre parce qu'il doit être sur
   toutes les pages, pas parce qu'on veut le voir : les commandes sont
   celles de la barre, pas celles du navigateur. */
.queue-audio { display: none; }

/* ---------- La vignette d'un fichier, et son choix dans le film ---------- */

/* Le champ de fichier natif est illisible et impossible à styler : on le cache
   dans un label, qui devient le bouton. Cliquer le label ouvre le sélecteur,
   sans script. */
.preview-upload { cursor: pointer; }
.preview-note { margin-top: 0.5rem; }
.preview-note.is-bad { color: var(--error-fg); }

.frame-picker {
  width: min(52rem, 92vw);
  padding: 0.9rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
}
.frame-picker::backdrop { background: rgba(0, 0, 0, 0.6); }
.frame-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.6rem; }
.frame-picker video {
  width: 100%;
  max-height: 60vh;
  background: #000;
  border-radius: var(--radius-sm);
  display: block;
}
.frame-picker input[type="range"] { width: 100%; margin: 0.6rem 0 0.4rem; accent-color: var(--accent); }
.frame-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; }
/* Le temps pousse le bouton de validation à droite : ce qu'on lit et ce qu'on
   décide ne se confondent pas. */
.frame-controls .meta { flex: 1; font-variant-numeric: tabular-nums; }

.playlist-save[hidden] { display: none; }

/* Le retour d'une touche, posé dans la barre. Sur l'accent pour qu'il se
   distingue du reste, qui est en gris et permanent. */
.queue-flash {
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--fg);
  font-size: 0.82rem;
  white-space: nowrap;
}
.queue-flash[hidden] { display: none; }

.queue-cover { width: 40px; height: 40px; object-fit: cover; border-radius: var(--radius-sm); }
.queue-now { display: flex; flex-direction: column; min-width: 0; flex: 0 1 16rem; }
.queue-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.queue-controls { display: flex; gap: 0.15rem; }
.queue-controls button {
  /* Une taille fixe, et le symbole centré dedans. Sans cela le bouton central
     change de largeur en même temps que son symbole — ▶ et ⏸ n'ont pas la
     même chasse, 26,3 px contre 21,6 — et le bouton « suivant » sautait de
     cinq pixels vers la droite à chaque pause. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: none;
  background: none;
  color: var(--fg);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  border-radius: var(--radius-sm);
}
.queue-controls button:hover:not(:disabled) { background: var(--accent-soft); color: var(--accent); }
.queue-controls button:disabled { opacity: 0.3; cursor: default; }

/* La barre montre trois choses à la fois : la position (le curseur natif), ce
   qui est prêt à être lu, et ce qui ne l'est pas encore. Le fond du champ
   porte les deux dernières sous forme de dégradé à arrêt net, dont le point de
   rupture est écrit par le script dans --queue-ready.
   
   Un dégradé plutôt qu'un second élément posé derrière : un <input type=range>
   ne peut rien contenir, et superposer une barre à un curseur natif oblige à
   reproduire à la main l'endroit exact où il commence et finit — donc à le
   refaire pour chaque navigateur. Ici le champ reste natif, avec ses flèches,
   son clavier et son accessibilité. */
.queue-seek {
  flex: 1;
  min-width: 4rem;
  /* La hauteur du rail, maintenant qu'il n'y a plus de rond pour la donner. */
  height: 10px;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  /* Trois zones : lu, prêt, pas encore encodé. Les arrêts sont nets — une
     barre de lecture n'est pas un décor, chaque frontière veut dire quelque
     chose. Sur un fichier entier, la troisième zone est vide et l'on retrouve
     la barre ordinaire : rempli jusqu'au curseur, rail après. */
  background: linear-gradient(
    to right,
    var(--accent) 0 var(--queue-played, 0%),
    var(--border-strong) var(--queue-played, 0%) var(--queue-ready, 100%),
    var(--border) var(--queue-ready, 100%) 100%
  );
  /* Arrondi aux deux bouts : la barre se lit comme un objet, pas comme une
     tranche découpée dans la page. */
  border-radius: 999px;
  overflow: hidden;
}
/* Les pistes natives sont effacées pour laisser voir ce fond ; le curseur,
   lui, reste celui du navigateur. */
.queue-seek::-webkit-slider-runnable-track { background: transparent; }
.queue-seek::-moz-range-track { background: transparent; }

/* Pas de curseur : la frontière entre le vert et le gris dit déjà où l'on en
   est, et le rond ne faisait que la redire en la masquant. La barre reste un
   champ de saisie — on clique dedans, on la parcourt aux flèches — mais elle
   se présente comme une barre.

   Une largeur d'un pixel plutôt que zéro : à zéro, Firefox et Chrome décident
   chacun quoi faire d'un curseur inexistant, et l'un des deux cesse de
   répondre au clic. Un pixel transparent reste un curseur pour eux et ne se
   voit pour personne. */
.queue-seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 1px;
  height: 10px;
  background: transparent;
  border: 0;
}
.queue-seek::-moz-range-thumb {
  width: 1px;
  height: 10px;
  background: transparent;
  border: 0;
}
/* Firefox colore lui-même la portion parcourue, ce qui masquerait le dégradé
   jusqu'au curseur et cacherait où s'arrête l'encodage. On la lui retire —
   c'est le dégradé qui la dessine maintenant, dans la même couleur d'accent,
   et lui seul sait aussi montrer la troisième zone. */
.queue-seek::-moz-range-progress { background: transparent; }
.queue-time { white-space: nowrap; font-variant-numeric: tabular-nums; }

/* The queue itself opens upward, over the page rather than pushing it. */
.queue-list {
  position: absolute;
  right: 1rem;
  bottom: calc(100% + 0.5rem);
  max-height: 50vh;
  overflow-y: auto;
  min-width: 18rem;
  margin: 0;
  padding: 0.4rem;
  list-style: none;
  counter-reset: queue;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}
.queue-list li {
  padding: 0.35rem 0.6rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.9rem;
}
.queue-list li:hover { background: var(--accent-soft); }
.queue-list .queue-current { color: var(--accent); font-weight: 600; }

/* The bar sits over the end of the page; the page ends higher up. */
/* La marge suit la hauteur mesurée de la barre, et non un 6rem écrit
   d'avance : celui-ci suffisait à une barre d'une ligne et laissait les
   derniers résultats passer dessous dès qu'elle se repliait ou que le texte
   était agrandi. Le sélecteur reste, parce que la marge ne vaut que lorsque
   la barre est là — sans elle, .container garde son repli de 1,5rem. */
body:has(.queue-bar:not([hidden])) .container:not(.container-wide) {
  padding-bottom: calc(var(--queue-bar, 3.5rem) + 1.5rem);
}

@media (max-width: 640px) {
  .queue-seek, .queue-time { display: none; }
}

/* The explanation for a resume the browser refused: it sits above the bar,
   says what happened and what to do about it once and for all. */
.queue-notice {
  position: absolute;
  left: 1rem;
  right: 1rem;
  bottom: calc(100% + 0.5rem);
  margin: 0;
  padding: 0.6rem 0.9rem;
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  font-size: 0.85rem;
  color: var(--muted);
}
.queue-notice[hidden] { display: none; }

/* ---------- Écran de lecture ---------- */

/* Trois régions : où l'on navigue à gauche, ce qu'on regarde au milieu, ce
   qui va jouer à droite. La colonne du centre est la plus large parce que
   c'est la seule qu'on regarde vraiment ; les deux autres se consultent. */
/* La coquille : trois colonnes, deux rangées, et quatre pièces qui changent
   de case selon ce qu'on fait.

   La scène est une pièce de la grille et non un enfant de la colonne du
   milieu, et c'est tout l'enjeu : on ne déplace pas un <video> qui joue. Le
   sortir du DOM pour le remettre ailleurs le met en pause — c'est la
   spécification HTML, pas un caprice de navigateur. Elle reste donc à sa
   place dans le document pour toujours, et seule sa case change. */
.player-page {
  display: grid;
  grid-template-columns: minmax(0, 11rem) minmax(0, 1fr) minmax(0, 21rem);
  /* Trois rangées : le contenu, la miniature, puis la barre tout en bas. */
  grid-template-rows: minmax(0, 1fr) auto auto;
  gap: 1.25rem;
  align-items: start;
}
.player-nav { grid-column: 1; grid-row: 1 / span 2; }

/* Hors lecture — qu'on explore ou qu'on soit à l'accueil : le centre prend
   les deux rangées, la liste tient le haut de la colonne de droite, et la
   scène se range dessous en miniature. Le lecteur continue de jouer dans un
   coin pendant qu'on cherche.

   Écrit en « pas le mode lecture » plutôt qu'en énumérant les autres : un
   quatrième centre viendra — une fiche — et il n'aura rien à ajouter ici. */
.player-page:not([data-mode="player"]) > .player-main { grid-column: 2; grid-row: 1 / span 2; }
.player-page:not([data-mode="player"]) > .playlist-panel { grid-column: 3; grid-row: 1; }
.player-page:not([data-mode="player"]) > .player-stage { grid-column: 3; grid-row: 2; }

/* En lecture : la scène prend le centre, la liste garde toute la hauteur à
   droite, et les résultats s'effacent — on regarde. */
.player-page[data-mode="player"] > .player-stage { grid-column: 2; grid-row: 1 / span 2; }
.player-page[data-mode="player"] > .playlist-panel { grid-column: 3; grid-row: 1 / span 2; }
.player-page[data-mode="player"] > .player-main { display: none; }

/* La miniature. Sa hauteur est bornée pour qu'elle reste une vignette, et
   surtout : flex:none, parce qu'elle est dans une colonne qui défile et que
   tout ce qui s'y trouve devient compressible par défaut. C'est ce qui avait
   écrasé la grande scène — 328 px pour une image qui en demandait 404, le
   débordement coupé — et le petit format n'y échappe pas. */
.player-page:not([data-mode="player"]) > .player-stage {
  flex: none;
  min-height: 0;
}
/* Les tailles ci-dessous valent pour la scène telle qu'elle est posée dans la
   page. Agrandie, elle n'y est plus : le :not(:fullscreen) le dit, faute de
   quoi ces règles — plus précises que celles du plein écran — gardaient le
   film à onze rem au milieu d'un écran noir (rapporté par Fred, 2026-08-04). */
.player-page:not([data-mode="player"]) > .player-stage .video-stage:not(:fullscreen) video {
  height: auto;
  max-height: 11rem;
}
.player-page:not([data-mode="player"]) > .player-stage .player-art:not(:fullscreen) img {
  height: min(11rem, 30vh);
}
/* Ce qui ne tient pas dans une vignette : la fiche et les suggestions se
   lisent au centre, pas dans un coin de 21rem. */
.player-page:not([data-mode="player"]) > .player-stage .player-related { display: none; }


.player-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  --stage-gap: 0.5rem;
  gap: var(--stage-gap);
}
/* Sur cette page la recherche n'est pas suivie d'un texte mais d'un panneau
   qui vient s'y accrocher : toute marge sous elle fait un trou. Il y en avait
   deux, celle du champ et celle du formulaire qui le porte. */
.player-main .browser-form,
.player-main .library-search { margin-bottom: 0; }

/* Le titre, en tête de la colonne de navigation, et son point d'explication. */
.player-title-row {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding-left: 0.5rem;
  margin-bottom: 0.2rem;
}
.player-title { font-size: 1.25rem; margin: 0; }

.info-hint {
  display: inline-flex;
  color: var(--muted);
  cursor: help;
  border-radius: 50%;
}
.info-hint:hover { color: var(--fg); }
.info-hint .icon { width: 15px; height: 15px; }

/* La pile : le lecteur au fond, les résultats par-dessus. Une hauteur
   minimale pour que fermer les résultats ne fasse pas sauter la page d'un
   demi-écran. */
.player-stage {
  position: relative;
  min-height: 22rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
/* Tant que les résultats sont ouverts, la scène prend toute la hauteur qui
   reste : une liste de vingt titres dans une fenêtre de 22rem se lit à la
   paille.

   Par chaînage et non par soustraction. Un calc(100vh - Nrem) demanderait de
   connaître d'avance ce qui surmonte la scène — or la carte « Reprendre » est
   là ou non selon qu'on a commencé quelque chose, et la barre du site change
   de hauteur avec la taille du texte. Le seul N juste est celui que le
   navigateur mesure : on lui fait donc étirer le corps, puis le contenu, puis
   la colonne, puis la scène. Chaque maillon ne connaît que son parent.

   Conditionné à l'ouverture, parce qu'une vidéo refermant les résultats doit
   retrouver une scène à sa taille et non un grand noir sous l'image. Les
   navigateurs sans :has() ignorent tout cela et gardent les 22rem — c'est
   l'état d'avant, pas une casse. */
body:has(> main.container-wide) {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
body:has(> main.container-wide) > main.container-wide {
  flex: 1;
  display: flex;
  flex-direction: column;
  /* Les deux lignes qui manquaient. Le « margin: 0 auto » de .container
     centre — et en flex, une marge auto dans l'axe transversal annule
     l'étirement : l'élément se dimensionne alors sur son contenu. La page
     tombait à 884 px dans une fenêtre de 1280 et tout se ratatinait. Le
     min-width parce qu'un élément flex ne descend pas sous la largeur
     minimale de son contenu sans qu'on le lui dise. */
  width: 100%;
  min-width: 0;
}
/* /player ne défile pas : la page tient l'écran, et ce sont les listes qui
   défilent dedans. Sans cela rien ne borne la hauteur — le corps était en
   min-height: 100vh, donc libre de grandir — et « s'étirer jusqu'au lecteur »
   ne voulait rien dire : la liste s'étirait à son propre contenu et passait
   mille deux cents pixels sous la barre.

   Seulement là où la page se lit en colonnes. Empilée sur un écran étroit,
   elle doit défiler comme n'importe quelle page, et la borner y couperait le
   bas. */
@media (min-width: 681px) {
  body:has(> main.container-wide):has(.player-page) {
    height: 100vh;
    min-height: 0;
    overflow: hidden;
  }
  /* Et le conteneur doit accepter de rétrécir. min-height: auto est la valeur
     par défaut d'un enfant flex : elle lui interdit de descendre sous la
     hauteur de son contenu, si bien qu'il faisait 2142 px dans un corps de
     900 et débordait sans que rien ne l'arrête. C'est la ligne qui manquait —
     borner le corps ne sert à rien tant qu'un enfant refuse la contrainte. */
  body:has(.player-page) > main.container-wide { min-height: 0; }
}

.container-wide > .player-page {
  flex: 1;
  min-height: 0;
  /* Trois rangées, et il en faut bien trois : le contenu, la miniature, puis
     la barre. Avec deux, « grid-row: -2 / -1 » désignait la deuxième — celle
     de la miniature — et la barre se posait au milieu de la page, en travers
     de la fiche. Le sélecteur de la miniature écrit « grid-row: 2 » en toutes
     lettres ; la barre doit donc avoir la sienne, la troisième.

     minmax(0, 1fr) sur la première est ce qui l'empêche de grandir au-delà de
     la place disponible, là où 1fr seul se laisse pousser par son contenu. */
  grid-template-rows: minmax(0, 1fr) auto auto;
}
/* La colonne du milieu et celle de la liste s'étirent jusqu'au lecteur. La
   liste, parce qu'elle est faite pour être longue : plafonnée à la hauteur de
   son contenu, elle laissait un vide sous elle et se donnait un ascenseur
   alors qu'il restait de la place. La colonne de gauche, elle, se consulte de
   haut en bas et n'a rien à gagner à faire la hauteur de l'écran. */
.container-wide > .player-page > .player-main,
.container-wide > .player-page > .playlist-panel { align-self: stretch; }

/* Et la colonne du milieu défile pour son propre compte. La page, elle, ne
   défile pas — c'est ce qui permet à la liste de descendre jusqu'au lecteur —
   si bien que tout ce qui dépassait était simplement coupé : l'image se
   trouvait plafonnée par la fenêtre plutôt que par sa propre forme, et un 4/3
   ne pouvait pas prendre la hauteur qu'il demande. Fred l'a vu autrement : en
   réduisant la police, tout rentrait, donc c'était bon.

   min-height: 0 est la moitié qui manque toujours : un enfant de grille refuse
   de descendre sous la hauteur de son contenu tant qu'on ne le lui dit pas, et
   overflow n'a alors rien à faire. */
.container-wide > .player-page > .player-main {
  min-height: 0;
  overflow-y: auto;
}

/* Étirée, la colonne doit encore dire ce qui prend la place : le titre garde
   sa hauteur, la liste prend le reste. Sans min-height: 0, un enfant flex
   défilant refuse de descendre sous la hauteur de son contenu et déborde du
   panneau au lieu de s'y ranger. */
.playlist-panel { display: flex; flex-direction: column; min-height: 0; }
.playlist-panel > #playlist-pane {
  flex: 1;
  min-height: 0;
  /* .scroll-pane est le composant générique, plafonné à 62vh : très bien
     dans une page qui défile, faux ici où le panneau connaît sa propre
     hauteur et où le plafond arrêtait la liste avant le bas. */
  max-height: none;
}
.player-main:has(.browser-overlay:not([hidden])) > .player-stage { flex: 1; }

.player-idle {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 18rem;
  padding: 2rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  text-align: center;
}
.player-idle[hidden] { display: none; }

/* La pochette, quand c'est tout ce qu'il y a à voir. Carrée et centrée : un
   disque est carré, et l'étirer pour remplir la colonne le déforme. */
.player-art {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Le même remède que la scène vidéo : un enfant de flex se rétrécit par
     défaut, et la boîte se faisait comprimer à ses seuls rembourrages (32 px
     mesurés) quand la légende et les suggestions réclamaient la place — son
     image, elle, gardait ses min(52vh, 420px) et débordait, centrée, jusque
     par-dessus la barre de recherche. */
  flex: none;
  padding: 1rem 0;
}
.player-art[hidden] { display: none; }
/* En plein écran, la pochette occupe l'écran comme le ferait une vidéo : fond
   noir, image centrée et entière. Sans ces deux règles, l'élément agrandi
   garde la marge et la hauteur maximale qu'il avait dans la colonne, et
   l'image reste petite au milieu du blanc. */
.player-art:fullscreen {
  padding: 0;
  background: #000;
  position: relative;
  width: 100%;
  height: 100%;
}
.player-art:fullscreen img {
  /* Même règle en plein écran : on remplit ce qu'on a, sans déformer. */
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: contain;
  border-radius: 0;
  box-shadow: none;
}
.player-art img {
  width: auto;
  max-width: 100%;
  /* Le cadre est rempli, dans un sens comme dans l'autre : une pochette de
     2830 px est réduite, une de 200 px est agrandie. object-fit tient les
     proportions, donc rien n'est jamais déformé — seulement plus ou moins
     net. Une vignette de 200 px perdue au milieu d'une colonne de 680 se lit
     comme un défaut d'affichage ; un peu de flou se lit comme une petite
     image, ce qu'elle est. */
  height: min(52vh, 420px);
  object-fit: contain;
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
  background: var(--panel);
}

/* Ce qu'on regarde, écrit sous l'image. */
.player-caption[hidden] { display: none; }

/* Le titre mène quelque part : il le montre au survol, et pas avant — un
   soulignement permanent sous un titre de film le déguise en note de bas de
   page. */
a.player-caption-title { color: inherit; text-decoration: none; }
a.player-caption-title:hover { text-decoration: underline; }

/* À écouter après. Deux groupes côte à côte quand la place le permet, l'un
   sous l'autre sinon — ils se lisent indépendamment. */
.player-related { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 0.9rem; }
.player-related[hidden] { display: none; }
.related-group { flex: 1 1 16rem; min-width: 0; }
.related-group[hidden] { display: none; }
/* Pas d'ascenseur ici : la fiche est déjà dans une colonne qui défile, et un
   cadre qui défile dans un cadre qui défile fait qu'on ne sait plus lequel on
   fait bouger. La longueur est bornée à la source — huit suggestions par
   groupe — plutôt qu'après coup. */
.related-list {
  list-style: none;
  margin: 0.35rem 0 0;
  padding: 0;
}
.related-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.15rem 0.25rem;
  border-radius: var(--radius-sm);
  cursor: grab;
}
.related-row:active { cursor: grabbing; }
.related-row:hover { background: var(--accent-soft); }
.related-art {
  flex: none;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--code-bg);
}
.related-art img { width: 100%; height: 100%; object-fit: cover; }
.related-name {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  padding: 0.1rem 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.related-name > * , .related-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Même règle que partout ailleurs sur cette page : le bouton rare attend le
   survol, mais garde sa place pour que la ligne ne saute pas. */
.related-end { flex: none; width: 24px; height: 24px; opacity: 0; transition: opacity 90ms ease-out; }
.related-row:hover .related-end,
.related-row:focus-within .related-end { opacity: 1; }
@media (hover: none) { .related-end { opacity: 1; } }
.player-caption-title {
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: -0.012em;
}
.player-caption-sub { margin-top: 0.1rem; }
.player-caption-note {
  margin: 0.5rem 0 0;
  max-width: 68ch;
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1.5;
  text-wrap: pretty;
}
.player-caption-note:empty { display: none; }

/* Les résultats, posés par-dessus le lecteur. Ils le couvrent entièrement —
   d'où le fond opaque : à travers, l'image et le texte se mêleraient. */
.browser-overlay {
  /* Dans le flux, et non plus par-dessus la scène : les résultats sont le
     centre de l'exploration, la scène a quitté cette colonne. Le nom reste —
     le renommer toucherait le script, le gabarit et la feuille pour ne rien
     changer à ce qu'on voit ; la grande passe s'en chargera.

     Collés sous le champ de recherche, sans l'intervalle qui les faisait
     flotter à côté : ce panneau est la réponse à ce qu'on vient de taper, et
     il doit se lire comme le prolongement du champ. D'où aussi les coins
     hauts droits — un coin arrondi contre un bord droit dessine une couture
     là où l'on veut une continuité. */
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* Sans cadre ni marge intérieure sur les côtés : le panneau occupe déjà
     toute la colonne, un trait autour ne délimite rien qu'on ne voie et
     coûte de la largeur là où les titres en manquent. */
  padding: 0;
  /* Une surface, pas le fond de la page : posé sur le lecteur, il doit se
     tenir devant lui et non se confondre avec lui. */
  background: var(--surface);
}
.browser-overlay[hidden] { display: none; }
.browser-overlay .scroll-pane {
  flex: 1;
  min-height: 0;
  /* .scroll-pane est le composant générique : il se plafonne à 62vh et se
     dessine un cadre, ce qui va très bien dans une page qui défile. Ici les
     deux nuisent — le plafond arrêtait la liste bien avant le bas du
     panneau, et le trait redoublait une limite que le panneau marque déjà. */
  max-height: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  background: transparent;
}

.browser-overlay-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border);
}
/* L'indication pousse la croix à droite et se retire la première quand la
   place manque : c'est un rappel, pas une commande. */
.browser-overlay-head .browser-hint {
  flex: 1;
  margin: 0;
  /* Elle s'efface plutôt que de s'empiler sur cinq lignes : c'est un rappel,
     il ne vaut pas la hauteur de cinq titres. */
  white-space: nowrap;
  overflow: hidden;
}
@media (max-width: 900px) {
  .browser-overlay-head .browser-hint { display: none; }
}

.player-nav {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
}
.player-nav-group { display: flex; flex-direction: column; gap: 0.5rem; }
.player-nav .nav-links { display: flex; flex-direction: column; gap: 0.15rem; }

.nav-link {
  display: block;
  width: 100%;
  padding: 0.28rem 0.5rem;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted);
  font-size: 0.92rem;
  text-align: left;
  cursor: pointer;
}
/* Un bouton n'hérite pas de la police de la page : sans cela « Accueil » et
   « Lecteur » s'écriraient dans la police du système, à côté de catégories
   qui sont des liens. */
.nav-link { font-size: 0.88rem; font-family: inherit; }
.nav-link:hover { background: var(--surface); color: var(--fg); text-decoration: none; }
.nav-link.is-current {
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}
/* Les trois affichages, côte à côte : le choix se lit d'un coup d'œil, ce que
   trois mots empilés dans une colonne étroite ne permettaient pas. */
/* Le titre à gauche, le réglage qu'il concerne à droite. */
.nav-group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
}
.nav-group-head .eyebrow { margin: 0; }

/* Une seule icône visible : celle de l'affichage en cours. Les trois sont
   dans le gabarit, ce qui évite de redessiner les mêmes formes en
   JavaScript — et de les corriger deux fois. */
.layout-cycle { flex: none; }
.layout-cycle svg { display: none; }
.layout-cycle[data-layout="grid"] [data-layout-icon="grid"],
.layout-cycle[data-layout="list"] [data-layout-icon="list"],
.layout-cycle[data-layout="editorial"] [data-layout-icon="editorial"] { display: block; }

/* Même mécanique pour séparée/unifiée : une icône par état, une seule
   visible. */
.unify-toggle { flex: none; }
.unify-toggle svg { display: none; }
.unify-toggle[data-unify="1"] [data-unify-icon="1"],
.unify-toggle[data-unify="0"] [data-unify-icon="0"] { display: block; }

/* La ligne « Tout » porte son réglage à son extrémité : le lien prend la
   place, le bouton ce qui reste. */
.nav-row { display: flex; align-items: center; gap: 0.2rem; }
.nav-row .nav-link { flex: 1; min-width: 0; }

/* Reprendre : une seule carte, large, avant tout le reste — c'est la seule
   chose de cette page qu'on veut parfois sans rien chercher. */


/* Trois paliers, et à chacun on cède ce qui coûte le moins.

   D'abord la largeur : les colonnes se resserrent avant de se rompre. Passer
   d'un coup à deux colonnes laissait de la place inutilisée. */
@media (max-width: 1240px) {
  .player-page { grid-template-columns: minmax(0, 9.5rem) minmax(0, 1fr) minmax(0, 16rem); gap: 0.9rem; }
}

/* Ensuite la colonne de navigation : elle passe au-dessus, sur une ligne.
   C'est elle qui coûte le plus cher pour ce qu'elle porte — une colonne
   entière pour six mots — et la sacrifier laisse le lecteur et la file
   chacun sur la leur. La file reste donc à droite bien plus bas en largeur
   qu'en la renvoyant sous le lecteur, où elle passait sous la ligne de
   flottaison. */
@media (max-width: 1080px) {
  .player-page { grid-template-columns: minmax(0, 1fr) minmax(0, 14rem); }
  /* Le menu passé au-dessus prend sa propre rangée : elle garde sa hauteur,
     et c'est la seconde — le lecteur et la liste — qui prend le reste. La
     troisième est celle de la barre, ici comme ailleurs. */
  .container-wide > .player-page { grid-template-rows: auto minmax(0, 1fr) auto; }
  .player-nav {
    grid-column: 1 / -1;
    display: flex;
    /* En toutes lettres : la règle de base met cette colonne en flex column,
       et une déclaration omise ici n'est pas une déclaration neutre — elle
       laisse l'autre en place. */
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem 1rem;
    padding: 0;
    background: none;
    border: 0;
  }
  .player-title-row { margin: 0; padding: 0; }
  .player-nav-group { display: flex; align-items: center; gap: 0.5rem; flex: 1; min-width: 0; }
  /* « Bibliothèque » au-dessus d'une colonne annonce ce qui suit ; sur une
     ligne, devant les mots eux-mêmes, il ne fait que les répéter. */
  .nav-group-head .eyebrow { display: none; }
  .player-nav .nav-links { flex-direction: row; flex-wrap: wrap; gap: 0.3rem; }
  /* width: 100 % vaut pour un lien empilé dans une colonne étroite ; en ligne
     il le rendait aussi large que le bandeau, si bien que les six mots
     restaient l'un sous l'autre malgré le flex-direction. */
  .player-nav .nav-link { width: auto; }
  .player-nav .nav-row { flex: none; }
  /* La reprise se replie sur un rang, puisqu'elle a la largeur de la page. */
  /* La reprise cède la place aux six mots plutôt que l'inverse : elle est le
     rappel d'un jour sur deux, ils sont la navigation. */
}

/* Enfin la file elle-même, quand deux colonnes ne tiennent plus. Elle devient
   un tiroir plutôt que de s'empiler sous le lecteur, où l'on ne savait plus
   qu'elle existait : le bouton « Liste » de la barre la fait apparaître et
   disparaître, et c'est le même bouton qui, ailleurs sur le site, mène ici. */
/* La file se resserre encore avant de partir : à 11rem un titre tronqué reste
   plus utile qu'un tiroir qu'il faut ouvrir. */
@media (max-width: 900px) {
  .player-page { grid-template-columns: minmax(0, 1fr) minmax(0, 11rem); gap: 0.7rem; }
}

@media (max-width: 680px) {
  .player-page {
    grid-template-columns: minmax(0, 1fr);
    /* Empilées, les trois colonnes deviennent trois rangées : les contraindre
       à la hauteur de l'écran les écraserait au lieu de laisser la page
       défiler, qui est le bon comportement sur un écran étroit. */
    grid-template-rows: auto;
  }
  .playlist-panel {
    position: fixed;
    top: 0;
    right: 0;
    bottom: var(--queue-bar, 3.5rem);
    z-index: 40;
    width: min(22rem, 88vw);
    padding: 1rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--surface);
    border-left: 1px solid var(--border-strong);
    box-shadow: var(--shadow-md);
    transform: translateX(101%);
    transition: transform 0.16s ease-out;
  }
  .playlist-panel.is-open { transform: none; }
}

/* Ce qu'un dossier est en train de faire. Le point tourne pendant, et rien ne
   tourne quand c'est fini : une animation qui continue après coup laisse
   croire que le travail continue aussi. */
.import-run { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.3rem; font-size: 0.88rem; color: var(--muted); }
.import-run.is-running { color: var(--accent); }
.spinner {
  width: 12px;
  height: 12px;
  border: 2px solid color-mix(in srgb, currentColor 30%, transparent);
  border-top-color: currentColor;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 3s; } }
@media (max-width: 900px) {
}

/* Où l'on en est, posé sur la vignette. */
.browser-thumb { position: relative; }
.thumb-progress {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 3px;
  background: rgba(0, 0, 0, 0.45);
}
.thumb-progress > span { display: block; height: 100%; background: var(--accent); }

.stars { color: var(--accent); letter-spacing: 0.04em; }

/* Le synopsis n'existe que pour l'affichage éditorial ; il est écrit dans
   tous les cas, et caché ailleurs. */
.browser-note { display: none; }

/* ---------- Les trois façons de regarder la même liste ---------- */

/* Liste : la disposition d'origine, une ligne par élément. */

/* Grille : la vignette prend le dessus, le texte passe dessous. */
.browser-list.layout-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 1.1rem 0.9rem;
}
.browser-list.layout-grid .browser-row {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  /* La règle de base pose justify-content: space-between, ce qui va bien en
     ligne — la vignette à gauche, les boutons à droite. Retournée en colonne
     elle répartit le vide *entre* la vignette, le texte et les boutons : une
     carte dont le texte tient sur une ligne de moins recevait dix pixels de
     vide au-dessus de son titre, et le titre descendait. Les cartes voisines
     ne s'alignaient donc plus dès que l'une avait un titre plus long. */
  justify-content: flex-start;
  /* Le repère du bouton posé sur la vignette. */
  position: relative;
  gap: 0.5rem;
  border: none;
  padding: 0;
}
/* Deux lignes au plus pour les métadonnées : au-delà, c'est la carte entière
   qui grandit et la rangée qui se désaligne. */
/* Une ligne chacune, coupée si elle déborde : les deux moitiés ayant leur
   propre rang, un plafond commun de deux lignes laisserait la première en
   manger deux et la seconde disparaître. */
.browser-list.layout-grid .meta-who,
.browser-list.layout-grid .meta-what {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.browser-list.layout-grid .browser-thumb {
  width: 100%;
  height: auto;
  aspect-ratio: 1;
  font-size: 2rem;
}
.browser-list.layout-grid .browser-thumb img {
  width: 100%; height: 100%; object-fit: cover;
}
/* En grille, le bouton se pose sur la vignette, en haut à droite. Sous le
   texte il ajoutait une rangée à chaque carte pour un geste rare ; là il ne
   coûte rien tant qu'on ne survole pas. Le fond sombre parce qu'il repose sur
   une pochette dont on ne sait rien. */
.browser-list.layout-grid .browser-actions {
  position: absolute;
  top: 0.3rem;
  right: 0.3rem;
  z-index: 2;
  justify-content: flex-end;
}
.browser-list.layout-grid .browser-actions .icon-button {
  width: 26px;
  height: 26px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, #000 60%, transparent);
  color: #fff;
  backdrop-filter: blur(4px);
}
.browser-list.layout-grid .browser-actions .icon-button:hover {
  background: var(--accent);
  color: var(--accent-contrast);
}

/* Éditorial : plus large, avec le synopsis — pour parcourir en lisant plutôt
   qu'en reconnaissant. */
.browser-list.layout-editorial {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
  gap: 0.9rem;
}
.browser-list.layout-editorial .browser-row {
  align-items: flex-start;
  padding: 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.browser-list.layout-editorial .browser-thumb {
  width: 84px; height: 84px; flex: none; font-size: 1.6rem;
}
.browser-list.layout-editorial .browser-note {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
  margin-top: 0.35rem;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.45;
  text-wrap: pretty;
}

/* La scène : présente seulement quand la file arrive sur une vidéo. Fond noir
   et non --surface, parce que ce qui l'entoure doit disparaître devant
   l'image — c'est le seul endroit du site où le contenu prime sur le
   contenant. */
.video-stage {
  /* Ne pas se laisser comprimer. Mesuré dans le navigateur : la scène faisait
     328 px de haut pour une vidéo qui en demandait 404, et son overflow:hidden
     coupait la différence — d'où l'image écrasée, et non un mauvais rapport.
     C'est .player-main qui l'y forçait : un enfant de flex se rétrécit par
     défaut, et la colonne venait de recevoir min-height: 0 pour pouvoir
     défiler. Elle défile maintenant autour d'une scène qui garde sa taille. */
  flex: none;
  /* La scène épouse la vidéo au lieu d'occuper la colonne : son fond noir
     ne doit exister qu'en plein écran, jamais en bandes autour du film. */
  width: fit-content;
  max-width: 100%;
  margin: 0 auto 1.5rem;
  background: #000;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow-md);
}
.video-stage[hidden] { display: none; }

/* En plein écran, la scène occupe l'écran et loge la barre de lecture. Le
   fond noir vient d'elle et non de la vidéo, qui garde ses proportions au
   milieu. */
.video-stage:fullscreen {
  display: flex;
  align-items: center;
  justify-content: center;
  /* La case où la barre vient se poser : sans elle, une barre en absolu
     chercherait son ancrage plus haut dans la page. */
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  border-radius: 0;
  background: #000;
}
.video-stage:fullscreen video { height: 100%; width: 100%; }

/* La barre déplacée dans l'élément agrandi. Cachée par défaut — on est là pour
   l'image — et montrée par le script quand la souris bouge, puis remasquée au
   bout de trois secondes d'immobilité.
   
   Par une classe et non par :hover : en plein écran la souris est presque
   toujours au-dessus de l'image, donc :hover serait vrai en permanence et la
   barre ne partirait jamais. Ce qui compte n'est pas où est la souris mais si
   elle vient de bouger, et cela, seul le script le sait.
   
   Le focus garde la barre visible quoi qu'il arrive : quelqu'un qui l'atteint
   au clavier ne bouge pas la souris, et n'a pas à la voir disparaître sous
   ses doigts. */
.queue-bar.is-fullscreen {
  /* En bas, sur toute la largeur — là où on la cherche.
  
     Posée en absolu et non laissée dans le flux : la scène agrandie est une
     boîte flex qui centre l'image, et la barre y devenait un second élément
     de la rangée. Elle se tenait debout à droite du film, à mi-hauteur, et
     lui prenait le tiers de l'écran (rapporté par Fred, 2026-08-04). */
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 0;
  opacity: 0;
  transition: opacity 0.25s ease-out;
  /* Invisible *et* intouchable : une barre transparente qui intercepte encore
     les clics est pire qu'une barre visible. */
  pointer-events: none;
}
.queue-bar.is-fullscreen.is-awake,
.queue-bar.is-fullscreen:focus-within {
  opacity: 1;
  pointer-events: auto;
}

/* Le pointeur s'efface avec elle : garder une flèche au milieu d'un film est
   la même gêne, en plus petit. */
.video-stage:fullscreen,
.player-art:fullscreen { cursor: none; }
.video-stage:fullscreen.is-awake,
.player-art:fullscreen.is-awake { cursor: default; }
.video-stage video {
  display: block;
  /* Le cadre a la forme du film, quatrième essai — les trois premiers,
     faits sans mesurer, sont racontés dans l'historique. Ce qui change :
     la largeur est bornée DES DEUX CÔTÉS par le même calcul, donc la
     hauteur ne dépasse jamais min(72vh, 640px) et il n'y a jamais rien à
     couper ni à combler — pas de bande possible, le letterbox d'object-fit
     ne sert qu'entre la mise en page et les métadonnées.
     --video-ratio (« 704 / 524 ») et --video-ar (le même, en nombre) sont
     posés par queue.js : depuis la base via /tracks d'abord, puis depuis
     videoWidth/videoHeight qui savent l'anamorphose et ont le dernier mot. */
  margin-inline: auto;
  width: min(100%, calc(min(72vh, 640px) * var(--video-ar, 1.7778)));
  aspect-ratio: var(--video-ratio, 16 / 9);
  object-fit: contain;
  background: #000;
}

.playlist-panel, .browser-panel { min-width: 0; }
.playlist-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.playlist-head h2 { font-size: 1rem; margin: 0 0 0.5rem; }
.playlist-head-actions { display: flex; gap: 0.3rem; }

.playlist { list-style: none; margin: 0; padding: 0; }
.playlist-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius-sm);
}
/* Survolé par un dépôt, le titre s'inverse : le texte prend le fond et le
   fond prend le texte. Franc et sans transition — on doit savoir sans
   hésiter si on est sur la cible ou à côté. */
.playlist-title {
  border-radius: var(--radius-sm);
  padding: 0.1rem 0.35rem;
  margin-left: -0.35rem;
}
.playlist-title.is-drop-target {
  background: var(--fg);
  color: var(--bg);
}
.playlist-title.is-drop-target .meta { color: var(--bg); }

.playlist-row:hover { background: var(--accent-soft); }

/* Monter, descendre, retirer : trois icônes par ligne, sur une liste qui en
   compte trente, c'est quatre-vingt-dix icônes pour des gestes qu'on fait une
   fois de temps en temps. Elles gardent leur place — les faire apparaître en
   poussant le titre ferait sauter la ligne sous la souris qui vient s'y
   poser — mais elles ne se montrent qu'au survol, ou au focus, sans quoi
   elles n'existeraient pas pour le clavier. */
.playlist-actions { opacity: 0; transition: opacity 90ms ease-out; }
.playlist-row:hover .playlist-actions,
.playlist-row:focus-within .playlist-actions { opacity: 1; }
@media (hover: none) {
  .playlist-actions { opacity: 1; }
}
.playlist-current { color: var(--accent); font-weight: 600; }

/* La vignette, en tête de ligne. Elle ne paraît que si la colonne est assez
   large : sous 900 px elle tombe à 11rem, où trente-quatre pixels d'image se
   prennent sur le titre, qui en a plus besoin. */
.playlist-art {
  flex: none;
  width: 34px;
  height: 34px;
  display: none;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--code-bg);
}
.playlist-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
@media (min-width: 901px) {
  .playlist-art { display: flex; }
}

.playlist-label {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  text-align: left;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}
/* Chaque ligne se coupe pour elle-même : un titre long ne doit pas pousser le
   nom de l'artiste hors du cadre, ni l'inverse. */
.playlist-title,
.playlist-artist {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* La seconde ligne occupe sa hauteur même vide : une ligne creuse coûte moins
   qu'une colonne au rythme irrégulier. */
.playlist-artist { min-height: 1.15em; }
/* Les trois commandes flottent au-dessus du titre au lieu de lui prendre sa
   place. Le titre va donc jusqu'au bord — et dans une colonne de 16rem, les
   quatre-vingts pixels qu'elles réservaient étaient la différence entre
   « Cello Sonata, Op.36 - II Andant… » et le mouvement qu'on cherchait.
   Le fond sombre translucide sépare les icônes du texte qui passe dessous. */
.playlist-actions {
  position: absolute;
  right: 0.2rem;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  gap: 0.05rem;
  padding: 0.1rem 0.15rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, #000 58%, transparent);
  backdrop-filter: blur(3px);
}
.playlist-actions button {
  border: none;
  background: none;
  color: #d8d8dd;
  cursor: pointer;
  padding: 0.1rem 0.3rem;
  border-radius: var(--radius-sm);
}
.playlist-actions button:hover { color: #fff; background: color-mix(in srgb, #fff 20%, transparent); }

.playlist-save { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 1rem; }
.playlist-save input[type="text"] { flex: 1; min-width: 8rem; }

.browser-list { list-style: none; margin: 0; padding: 0; }
.browser-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--border);
}
.browser-row:hover { background: var(--accent-soft); }

/* Le bouton « à la fin » ne se montre qu'au survol, ou quand il a le focus —
   sans quoi il n'existerait pas pour le clavier. Il garde sa place en tout
   temps : le faire apparaître en poussant le texte ferait sauter la ligne
   sous la souris qui vient s'y poser. */
.browser-actions { opacity: 0; transition: opacity 90ms ease-out; }
.browser-row:hover .browser-actions,
.browser-row:focus-within .browser-actions,
/* Choisie, la ligne garde ses gestes visibles : c'est ce que le clic vient
   d'ouvrir, et les faire disparaître dès que la souris s'écarte demanderait
   de viser sans jamais quitter la ligne. */
.browser-row.is-selected .browser-actions { opacity: 1; }
@media (hover: none) {
  /* Sans souris il n'y a pas de survol : le bouton reste visible. */
  .browser-actions { opacity: 1; }
}

/* En vue liste, « qui » prend toute la largeur : « quoi » passe donc à la
   ligne. Ailleurs les deux moitiés restent côte à côte. */
.meta-who, .meta-what { display: inline; }
.browser-list.layout-list .meta-who { display: block; }
.browser-list.layout-list .meta-what { display: block; }

/* L'artiste sur une ligne, coupé s'il déborde, entier dans l'infobulle. Un
   nom de compilation tient sur trois lignes et repoussait tout le reste. */
/* Un mot des métadonnées qui refait la recherche. Il ne se signale qu'au
   survol : quarante lignes où chaque nom crie qu'il est cliquable ne se lit
   plus comme une liste. */
.meta-filter {
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  border-radius: 2px;
}
.meta-filter:hover { color: var(--fg); text-decoration: underline; }

/* Le candidat qui concorde le mieux. Un liseré et non un fond : on met en
   avant sans décider à la place de qui regarde — c'est une identification, et
   le dernier mot lui revient. */
.identify-row.is-best {
  border-left: 3px solid var(--accent);
  padding-left: 0.6rem;
  background: var(--accent-soft);
  border-radius: var(--radius-sm);
}
.identify-why { margin: 0.25rem 0 0; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.badge-best { background: var(--accent); color: var(--accent-contrast); }

/* Aucune mise en forme sur l'artiste lui-même : il ne porte que l'infobulle.
   En inline-block avec overflow caché, sa ligne de base devient son bord
   inférieur, et le nom descendait de deux pixels sous la date à côté de
   laquelle il est écrit. La coupure est donc faite un cran plus haut, sur la
   ligne entière, où c'est du texte simple qui s'aligne tout seul. */
.browser-list.layout-list .meta-who,
.browser-list.layout-list .meta-what {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Le type, posé en bas à gauche de la vignette — en grille seulement, où la
   ligne de métadonnées passe sous l'image et l'y noie. */
.thumb-kind { display: none; }
.browser-list.layout-grid .thumb-kind {
  display: block;
  position: absolute;
  left: 0.35rem;
  bottom: 0.35rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, #000 62%, transparent);
  color: #fff;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.browser-list.layout-grid .browser-thumb { position: relative; }

/* La durée, face au type. Même traitement, même hauteur : deux repères qui se
   lisent d'un coup d'œil sur toute la grille. */
.thumb-time { display: none; }
.browser-list.layout-grid .thumb-time {
  display: block;
  position: absolute;
  right: 0.35rem;
  bottom: 0.35rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, #000 62%, transparent);
  color: #fff;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
}
/* Dit deux fois, ce serait dit une fois de trop. */
.browser-list.layout-grid .badge-kind { display: none; }

/* L'exemplaire posé devant le titre ne sert qu'en éditorial. */
.kind-ahead { display: none; }
.browser-list.layout-editorial .kind-ahead { display: inline-block; flex: none; }
.browser-list.layout-editorial .meta-who > .badge-kind { display: none; }
/* Et le retour à la ligne après l'artiste, comme en vue liste — et comme en
   grille, où la carte est étroite et où « 1968 · Jimi Hendrix 16 pistes ·
   1:15:26 » d'un seul tenant se coupait n'importe où. */
.browser-list.layout-editorial .meta-who,
.browser-list.layout-editorial .meta-what,
.browser-list.layout-grid .meta-who,
.browser-list.layout-grid .meta-what { display: block; }
/* flex: 1 — sans quoi le libellé, enfant du milieu d'une ligne en
   space-between, se dimensionne sur son texte et flotte au centre. Les quatre
   boutons d'avant le poussaient à gauche par leur seule largeur ; il n'en
   reste qu'un, et invisible au repos. */
.browser-label { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.browser-label > span:first-child, .browser-label a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Le titre cède la place au numéro de disque, jamais l'inverse : « CD 2 »
   tronqué en « CD… » ne dirait plus rien, alors qu'un titre coupé se devine. */
.browser-title-row { display: flex; align-items: baseline; gap: 0.4rem; min-width: 0; overflow: visible; }
.browser-title-row .browser-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.browser-title-row .badge-group { flex: none; }
.browser-actions { display: flex; gap: 0.2rem; flex-shrink: 0; }

/* Each panel scrolls in its own frame: a queue of two hundred tracks must
   not push the library off the screen, nor the reverse. */
.scroll-pane {
  max-height: min(62vh, 40rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.35rem;
  background: var(--surface);
  /* L'ascenseur reste visible au lieu de n'apparaître qu'au survol. Il n'est
     pas là pour être manœuvré — on parcourt à la molette — mais pour dire
     deux choses qu'aucun autre élément ne dit : qu'il y a plus à voir, et où
     l'on se trouve dedans. Un ascenseur qui se cache retire cette information
     précisément quand on ne touche à rien, c'est-à-dire quand on regarde.

     scrollbar-color et scrollbar-width sont la voie normalisée, comprise par
     Firefox ; les règles ::-webkit- qui suivent disent la même chose aux
     navigateurs qui ne l'implémentent pas encore. */
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.scroll-pane::-webkit-scrollbar { width: 10px; }
.scroll-pane::-webkit-scrollbar-track { background: transparent; }
.scroll-pane::-webkit-scrollbar-thumb {
  background: var(--border-strong);
  border-radius: 999px;
  /* La bordure transparente amincit le curseur sans réduire la zone qu'on
     peut attraper : ce qui se voit fait six pixels, ce qui se clique en fait
     dix. */
  border: 2px solid transparent;
  background-clip: padding-box;
}
.scroll-pane::-webkit-scrollbar-thumb:hover { background: var(--muted); }

.browser-sort select { font-size: 0.85rem; }
.browser-hint { margin: 0.35rem 0; }
.browser-hint kbd {
  padding: 0 0.25rem;
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  font-size: 0.8em;
}

/* A thumbnail in every result: recognising a cover is faster than reading
   its title. */
.browser-thumb {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--code-bg);
  color: var(--muted);
}
.browser-thumb img { width: 100%; height: 100%; object-fit: cover; }

.browser-row { cursor: grab; user-select: none; }
.browser-row:active { cursor: grabbing; }
.browser-row.is-selected { background: var(--accent-soft); box-shadow: inset 2px 0 0 var(--accent); }
a.browser-name { color: inherit; text-decoration: none; }
a.browser-name:hover { text-decoration: underline; }
.browser-name { font-weight: 500; }

.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.icon-button:hover { color: var(--accent); background: var(--surface); border-color: var(--border); }

.playlist-row { cursor: grab; }
.playlist-row:active { cursor: grabbing; }
/* Where a drop would land, drawn on the row itself rather than as a floating
   line: the list is short enough that a border says it clearly. */
.drop-before { box-shadow: inset 0 2px 0 var(--accent); }
.drop-after { box-shadow: inset 0 -2px 0 var(--accent); }

/* ---------- Fiche d'un média ---------- */

.file-panel {
  padding: 1rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-width: 0;
}

.file-panel h2 {
  margin: 0 0 0.75rem;
  font-size: 1rem;
}

/* Label on the left, value on the right, wrapping to two lines on a phone
   rather than squeezing a path into a column two words wide. */
.file-facts {
  display: grid;
  grid-template-columns: minmax(8rem, auto) 1fr;
  gap: 0.35rem 1rem;
  margin: 0;
  font-size: 0.9rem;
}

.file-facts dt {
  color: var(--muted);
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  align-self: center;
}

.file-facts dd { margin: 0; }

.file-path {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 0.82rem;
  word-break: break-all;
}

.file-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
  font-size: 0.9rem;
}

.file-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
}

.file-preview {
  margin: 1rem 0 0;
  text-align: center;
}

.file-preview img {
  max-width: 100%;
  max-height: 60vh;
  border-radius: var(--radius);
}

@media (max-width: 560px) {
  .file-facts { grid-template-columns: 1fr; gap: 0.15rem; }
  .file-facts dt { margin-top: 0.5rem; }
}

/* ---------- Identification d'un film ---------- */

.identify-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.75rem 0;
}

.identify-form input[type="search"] { flex: 1 1 14rem; min-width: 0; }
.identify-form input[type="number"] { width: 6rem; }

.identify-current { margin: 0 0 0.25rem; font-size: 0.9rem; }
.identify-note { margin: 0.25rem 0 0; }
.identify-error { color: var(--error-fg); font-size: 0.9rem; }

.identify-list {
  list-style: none;
  margin: 0.5rem 0 0;
  padding: 0;
  display: grid;
  gap: 0.6rem;
  /* Long enough to be worth scrolling on its own rather than pushing the
     rest of the page down by two screens. */
  max-height: 26rem;
  overflow-y: auto;
}

/* Texte puis bouton : le cas commun, servi par le serveur pour les albums et
   les séries, qui n'ont pas d'affiche à montrer. */
.identify-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.75rem;
  align-items: start;
  padding: 0.55rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
}

/* Affiche, texte, bouton : le cas des films, construit par identify.js, qui
   a toujours une vignette (une image ou 🎞 à défaut). */
.identify-row.has-poster { grid-template-columns: 46px 1fr auto; }

.identify-poster {
  width: 46px;
  height: 68px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--code-bg);
  color: var(--muted);
}
.identify-poster img { width: 100%; height: 100%; object-fit: cover; }

.identify-body { min-width: 0; display: grid; gap: 0.15rem; }
.identify-title { font-weight: 600; }

.identify-overview {
  margin: 0.25rem 0 0;
  font-size: 0.85rem;
  color: var(--muted);
  /* Three lines is enough to recognise a film without turning the list into
     an encyclopedia. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.identify-saved {
  margin: 0.5rem 0 0;
  font-size: 0.9rem;
  color: var(--accent);
}

.file-panel h3 {
  margin: 1rem 0 0.4rem;
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}
.file-panel h3:first-of-type { margin-top: 0.75rem; }

.file-belongs { margin: 0; font-size: 0.95rem; }

.identify-links {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.82rem;
}

/* Le renvoi entre les disques d'une même édition. */
.disc-nav {
  margin: 0.3rem 0 0;
  font-size: 0.9rem;
  color: var(--muted);
}
.disc-nav a,
.disc-nav strong { margin: 0 0.2rem; }
.disc-nav strong { color: var(--fg); }

/* ---------- Distribution ---------- */

.credit-list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.9rem;
}

.credit-heading {
  margin: 0.75rem 0 0.3rem;
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
}
.credit-heading:first-child { margin-top: 0; }

.credit {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.15rem 0;
}

.credit-films li { padding: 0.2rem 0; }

/* ---------- Outils d'une carte ---------- */

/* Lying over the bottom of the thumbnail, hidden until the card is hovered
   or something inside it has focus: on a listing of a hundred files these
   were four lines of chrome each, for something one does to one file at a
   time. Never hidden from a keyboard, and never on a touch screen, where
   there is no hover to speak of. */
.cover-tools {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.1rem;
  padding: 0.3rem 0.2rem 0.25rem;
  /* Its own dark backdrop rather than the theme's: the icons sit on whatever
     the poster happens to be, and white on a gradient reads over both a
     night scene and a snowfield. */
  background: linear-gradient(to top, rgba(0, 0, 0, 0.78), rgba(0, 0, 0, 0));
  opacity: 0;
  transition: opacity 0.12s ease;
}

.library-card:hover .cover-tools,
.library-card:focus-within .cover-tools { opacity: 1; }

@media (hover: none) {
  .cover-tools { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .cover-tools { transition: none; }
}

.cover-tools .icon-button {
  width: 28px;
  height: 28px;
  color: #fff;
}
.cover-tools .icon-button:hover {
  color: var(--accent);
  background: rgba(0, 0, 0, 0.45);
  border-color: transparent;
}
.cover-tools .icon-danger:hover { color: #ff8a8a; border-color: transparent; }


/* What the copy button says once it has done it, without moving anything. */
.icon-button.is-copied { color: var(--accent); border-color: var(--accent); }
.icon-button.is-failed { color: var(--error-fg); border-color: var(--error-fg); }

/* ---------- Barre du navigateur de médias ---------- */

.browser-form { display: grid; gap: 0.5rem; margin-bottom: 0.5rem; }

.browser-order {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
}

.browser-order select { min-width: 12rem; }

/* Pressed reads as "the order is turned round", so it stays lit rather than
   returning to the muted default when the pointer leaves. */
.icon-button[aria-pressed="true"] {
  color: var(--accent);
  border-color: var(--accent);
}

.identify-refresh { margin: 0 0 0.4rem; }

/* ---------- Accès : comptes, groupes, restrictions ---------- */

/* An explanation one can open when needed and ignore otherwise: the rules of
   the permission model are short, but nobody wants to read them on every
   visit to the screen. */
.explainer,
.danger-zone {
  margin: 0 0 1.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.explainer > summary,
.danger-zone > summary {
  padding: 0.7rem 1rem;
  cursor: pointer;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--muted);
}
.explainer > summary:hover,
.danger-zone > summary:hover { color: var(--fg); }
.danger-zone > summary { color: var(--error-fg); }

.explainer-body {
  padding: 0 1rem 1rem;
  font-size: 0.9rem;
  line-height: 1.6;
}
.explainer-body p { margin: 0 0 0.75rem; }
.explainer-body p:last-child { margin-bottom: 0; }
.explainer-body .delete-form { margin: 0; }

.role-legend { margin: 0 0 1rem; display: grid; gap: 0.5rem; }
.role-legend > div {
  display: grid;
  grid-template-columns: 12rem 1fr;
  gap: 0.75rem;
  align-items: baseline;
}
.role-legend dt { margin: 0; }
.role-legend dd { margin: 0; color: var(--muted); }

@media (max-width: 640px) {
  .role-legend > div { grid-template-columns: 1fr; gap: 0.2rem; }
}

/* The one line that answers "what can this account actually do", which the
   stored role alone doesn't. */
.effective-role {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 1rem;
  margin-bottom: 1.5rem;
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: 0.9rem;
  color: var(--muted);
}
.effective-role strong { color: var(--fg); }

/* Group names shown inline in a table cell: small, round, and a link when
   there is somewhere to go. */
.chip-row { display: inline-flex; flex-wrap: wrap; gap: 0.3rem; }
.chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.8rem;
  color: var(--muted);
  background: var(--surface);
}
a.chip:hover { color: var(--accent); border-color: var(--accent); text-decoration: none; }
.chip-group { color: var(--accent); background: var(--accent-soft); border-color: transparent; }
.chip-note { font-size: 0.7rem; opacity: 0.8; }

/* Membership as a set of checkboxes rather than one add/remove at a time:
   the whole of what a user belongs to, changed in a single save. */
.checkbox-set {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.75rem 1rem 1rem;
  margin: 0;
  display: grid;
  gap: 0.4rem;
}
.checkbox-set legend { padding: 0 0.4rem; font-size: 0.85rem; color: var(--muted); }
.checkbox-row {
  flex-direction: row !important;
  align-items: flex-start;
  gap: 0.6rem !important;
  font-weight: 400 !important;
}
.checkbox-row input { margin-top: 0.25rem; }
.checkbox-row .meta { display: block; }

.list-tools { margin-bottom: 0.75rem; }
.list-filter input { width: 100%; max-width: 24rem; }

.btn-danger {
  background: var(--error-bg);
  color: var(--error-fg);
  border-color: var(--error-border);
}
.btn-danger:hover { border-color: var(--error-fg); }

/* The level select sits in the row rather than under a label of its own:
   there is one per grant, and the column header is the row itself. */
.permission-level-form { margin: 0; }
.permission-level-form select {
  padding: 0.3rem 0.5rem;
  font-size: 0.85rem;
}

/* ---------- Sécurité du compte ---------- */

.security-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.85rem 1rem;
  margin-bottom: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.security-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.security-actions form { margin: 0; }

/* Codes shown once and never again: big enough to copy without squinting,
   and laid out so a missed line is obvious. */
.recovery-panel {
  padding: 1rem 1.25rem 1.25rem;
  margin-bottom: 1.5rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent-soft);
}
.recovery-panel h2 { margin-top: 0; }

.recovery-codes {
  list-style: none;
  padding: 0;
  margin: 1rem 0 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 0.5rem;
}
.recovery-codes code {
  display: block;
  padding: 0.4rem 0.6rem;
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: 1rem;
  letter-spacing: 0.05em;
  text-align: center;
}

.totp-setup {
  padding: 1rem 1.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  margin-bottom: 1.5rem;
}
.totp-setup h3 { margin-top: 0; }
.totp-setup ol { margin: 0 0 1rem; padding-left: 1.2rem; line-height: 1.8; }

.totp-secret {
  display: inline-block;
  padding: 0.25rem 0.6rem;
  font-size: 1.05rem;
  letter-spacing: 0.08em;
  user-select: all;
}

/* The QR code beside the steps on a wide screen, above them on a narrow one.
   It keeps its white surround whatever the theme: a camera looks for dark on
   light, and an inverted symbol is markedly harder to read. */
.totp-columns {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  align-items: flex-start;
}
.totp-columns ol { flex: 1 1 18rem; margin: 0 0 1rem; }

.totp-qr {
  flex: 0 0 auto;
  padding: 0.5rem;
  border-radius: var(--radius-sm);
  background: #ffffff;
  line-height: 0;
}
.totp-qr svg { display: block; width: 100%; max-width: 13rem; height: auto; }

/* The "add someone" panel: the same row-of-fields shape as the permission
   forms, so the section keeps one way of asking for things. */
.new-user-panel { margin-bottom: 1.5rem; }
.new-user-panel .subtitle { margin-top: -0.75rem; }

/* The number of registrations waiting, carried by the tab itself so a queue
   is not something one has to remember to go and look at. */
.chip-count {
  display: inline-block;
  min-width: 1.4em;
  padding: 0 0.35em;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-contrast);
  font-size: 0.75em;
  font-weight: 700;
  text-align: center;
}

.policy-form .checkbox-row input { margin-top: 0.35rem; }

/* The subscription form dropped into the middle of a page: a row on a wide
   screen, stacked on a narrow one, and never as wide as the text it
   interrupts. */
.newsletter-embed {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem;
  max-width: none;
  padding: 1rem;
  margin: 1.5rem 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.newsletter-embed label { flex: 1 1 12rem; }
.newsletter-embed-link { margin: 1.5rem 0; }

/* Le bouton de conversion, dans la barre. Deux flèches qui font un tour et
   s'arrêtent : l'animation dure une seconde sur un cycle de six, si bien que
   le mouvement se remarque sans jamais tourner en continu devant les yeux —
   une roue qui tourne sans fin ne dit plus rien au bout de dix secondes.

   La rotation tient dans les vingt premiers pour cent des images-clés ; les
   quatre-vingts autres sont l'attente, immobile. */
.queue-encode {
  flex-shrink: 0;
  color: var(--muted);
}
.queue-encode:hover:not(:disabled) { color: var(--accent); }
.queue-encode[hidden] { display: none; }
.queue-encode .icon { animation: queue-turn 6s ease-in-out infinite; }

/* Désactivé pendant que ça travaille : la conversion entière est déjà en route,
   il n'y a rien à demander. Il garde son mouvement — c'est lui qui dit que le
   travail avance — mais ne se propose plus, et le curseur le confirme. */
.queue-encode:disabled { cursor: default; }

/* Fait. Le vert ne dure que le temps du morceau : il salue un travail qu'on a
   vu se faire, il ne décore pas les fichiers déjà convertis. */
.queue-encode.is-done { color: var(--ok-fg); }
.queue-encode.is-done .icon { animation: none; }
.queue-encode.is-failed { color: var(--error-fg); }
.queue-encode.is-failed .icon { animation: none; }

@keyframes queue-turn {
  0%   { transform: rotate(0deg); }
  17%  { transform: rotate(360deg); }
  100% { transform: rotate(360deg); }
}

/* Le mouvement est un agrément, pas une information : qui l'a désactivé n'a
   rien à y perdre — la bulle et le pourcentage disent la même chose. */
@media (prefers-reduced-motion: reduce) {
  .queue-encode .icon { animation: none; }
}

/* Le cœur des favoris.
   Vide par défaut, plein une fois posé : la forme change, pas seulement la
   couleur — qui ne distingue pas le rouge du gris doit pouvoir lire l'état.
   Discret tant qu'on ne le survole pas, pour ne pas disputer la ligne au
   titre : c'est une marque qu'on pose, pas une commande qu'on cherche. */
.heart {
  background: none;
  border: 0;
  padding: 0 0 0 0.35rem;
  cursor: pointer;
  color: var(--muted);
  line-height: 0;
  vertical-align: -0.15em;
}
.heart svg { width: 1.05em; height: 1.05em; }
.heart:hover { color: var(--error-fg); }
.heart.is-on { color: var(--error-fg); }
.heart.is-on svg path { fill: currentColor; }

/* Les étiquettes. Une liste de mots, chacun cliquable — il cherche ce qu'il
   rassemble — et retirable par la croix qui le suit. */
.tag-list {
  list-style: none;
  margin: 0 0 0.6rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.1rem 0.2rem 0.1rem 0.55rem;
}
.tag .meta-filter { border: none; background: none; padding: 0; }
.tag-remove {
  border: none;
  background: none;
  color: var(--muted);
  cursor: pointer;
  padding: 0 0.35rem;
  line-height: 1;
}
.tag-remove:hover { color: var(--fg); }
.tag-add { display: flex; gap: 0.4rem; }
.tag-add input { flex: 1; min-width: 0; }

.stars { display: inline-flex; gap: 0.05rem; }
/* Une cible qu'on attrape : mesurée à 17 px de côté sans rembourrage, ce qui
   est petit pour une souris et hors de question au doigt. */
.star {
  border: none;
  background: none;
  cursor: pointer;
  padding: 0.2rem 0.15rem;
  font-size: 1.25rem;
  line-height: 1;
  color: var(--border-strong);
}
.star.is-on { color: var(--accent); }

.comment-list, .comment-replies { list-style: none; margin: 0.6rem 0 0; padding: 0; }
/* Les réponses décalées d'un cran, avec un filet qui les rattache à leur
   message : sans lui, un décalage seul se lit comme une marge. */
.comment-replies {
  margin-left: 0.9rem;
  padding-left: 0.9rem;
  border-left: 2px solid var(--border);
}
.comment + .comment, .comment-replies .comment + .comment { margin-top: 0.7rem; }
.comment-head { display: flex; align-items: baseline; gap: 0.5rem; }
.comment-author { font-weight: 600; font-size: 0.92rem; }
/* pre-wrap : ce qui a été écrit sur deux lignes se lit sur deux lignes. */
.comment-body { margin: 0.15rem 0 0.2rem; white-space: pre-wrap; overflow-wrap: anywhere; }
.comment-actions { display: flex; gap: 0.75rem; }
.link-button {
  border: none;
  background: none;
  padding: 0;
  color: var(--muted);
  font-size: 0.82rem;
  cursor: pointer;
}
.link-button:hover { color: var(--fg); text-decoration: underline; }
.comment-add { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.8rem; }
.comment-add textarea { width: 100%; resize: vertical; font-family: inherit; }
.comment-add button { align-self: flex-start; }


/* ---------- La trame commune des fiches ---------- */

/* Une fiche est un en-tête, puis ce qui dépend du type. L'en-tête est le même
   pour un film, un album, une sortie, une série, une saga ou un fichier seul :
   un visuel, une identité, une grille de faits, et une barre de gestes. Une
   seule barre — c'est de n'en avoir eu aucune que sont venus les panneaux
   empilés, chaque nouveauté se posant à côté faute d'un endroit où elle
   appartienne. */

.fiche-head {
  display: grid;
  /* La colonne du visuel se resserre avec la place : la fiche s'affiche aussi
     dans le centre de /player, qui est plus étroit qu'une page. */
  grid-template-columns: minmax(0, clamp(7rem, 24%, 13rem)) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
  padding: 1.25rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 1.5rem;
}

/* Le bloc entier se saisit et va dans la liste de lecture : la cible est
   large, on n'a pas à viser un bouton. Le prix en est que le texte de
   l'en-tête ne se sélectionne plus à la souris — c'est ce que draggable fait
   d'un conteneur, et c'est le geste qui a été demandé. */
.fiche-head[draggable="true"] { cursor: grab; }
.fiche-head[draggable="true"]:active { cursor: grabbing; }

/* Le visuel, au rapport de l'œuvre : 2:3 pour un film ou une série, carré pour
   un album, 16:9 pour une sortie, une photo ou une vidéo isolée. Les gestes de
   la vignette sont posés dessous, contre l'image qu'ils changent — c'était une
   section « Vignette » ailleurs dans la page. */
.fiche-art {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
}
.fiche-art > img {
  display: block;
  width: 100%;
  border-radius: var(--radius);
  background: var(--bg);
  object-fit: cover;
}
.fiche-art > img[hidden] { display: none; }
.fiche-art-poster > img { aspect-ratio: 2 / 3; }
.fiche-art-square > img { aspect-ratio: 1 / 1; }
.fiche-art-wide   > img { aspect-ratio: 16 / 9; }
.fiche-art-empty {
  margin: 0;
  padding: 1.5rem 0.5rem;
  text-align: center;
  border: 1px dashed var(--border);
  border-radius: var(--radius);
}
.fiche-art-actions { display: flex; flex-wrap: wrap; gap: 0.3rem; }

.fiche-ident {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.fiche-badge { margin: 0; }
.fiche-title {
  margin: 0;
  line-height: 1.15;
  font-size: clamp(1.35rem, 1.05rem + 1.3vw, 2rem);
  overflow-wrap: anywhere;
}
.fiche-line { margin: 0; color: var(--muted); }

/* La grille de faits : ce que l'identification a rapporté, serré. Des colonnes
   qui se posent d'elles-mêmes plutôt qu'un nombre fixe — la fiche est large
   sur sa page, étroite dans le centre du lecteur. */
.fiche-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 0.5rem 1rem;
  margin: 0.15rem 0 0;
}
.fiche-fact { min-width: 0; }
.fiche-fact dt {
  color: var(--muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.fiche-fact dd {
  margin: 0;
  font-size: 0.92rem;
  overflow-wrap: anywhere;
}
.fiche-fact .meta-filter { font-size: inherit; }

/* La barre. Lire et mettre en file d'abord, ce qu'on ressent ensuite, et
   poussés à droite les deux gestes d'un autre ordre : emporter le fichier,
   dire qui peut le voir. */
.fiche-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
}
.fiche-bar-end {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
}

/* Le cœur et les étoiles sont ici des cibles qu'on attrape, pas des marques
   posées en marge d'une ligne de texte : dans la barre ils ont la place. */
.fiche-bar .heart {
  font-size: 1.2rem;
  line-height: 1;
  padding: 0.15rem 0.4rem;
}

/* Deux compteurs plutôt que deux panneaux : ils disent qu'il y a quelque
   chose, et mènent à l'endroit où c'est. */
.fiche-count {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border: 1px solid var(--border);
  background: none;
  color: var(--muted);
  border-radius: 999px;
  padding: 0.22rem 0.6rem;
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}
.fiche-count:hover { color: var(--fg); border-color: var(--border-strong); }

.fiche-tags .tag-list { margin-bottom: 0.4rem; }
/* Le champ d'ajout ne prend pas toute la largeur : c'est un mot qu'on tape,
   pas un paragraphe, et pleine largeur il se lisait comme le sujet du bloc. */
.fiche-tags .tag-add { max-width: 22rem; }
.fiche-tags .tag-list:empty { margin-bottom: 0; }

/* Ce qui dépend du type, en dessous et seulement là. */
.fiche-parts {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.fiche-part {
  padding: 1rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  min-width: 0;
}
.fiche-part > h2:first-child { margin-top: 0; }
.fiche-part h2 { margin: 0 0 0.75rem; font-size: 1rem; }
.fiche-part h3 { margin: 1rem 0 0.5rem; font-size: 0.92rem; }
.fiche-part > .track-list, .fiche-part > .clip-list { margin-bottom: 0; }

/* Renommer, retirer : ce qu'on ne fait qu'une fois, tout en bas. */
.fiche-admin-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.fiche-admin-row form { margin: 0; }

/* La fiche se replie sur la largeur qu'on lui donne, et non sur celle de la
   fenêtre : c'est la même fiche dans une page large et dans le centre de
   /player, qui est étroit. Mesurée à la fenêtre, elle gardait ses deux
   colonnes dans la coquille et y écrivait le titre lettre par lettre. */
.fiche { container-type: inline-size; }

@container (max-width: 34rem) {
  .fiche-head { grid-template-columns: minmax(0, 1fr); }
  .fiche-art { max-width: 11rem; }
  .fiche-facts { grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr)); }
}

/* Le même repli pour qui n'a pas les requêtes de conteneur : sur un téléphone
   la fenêtre et la fiche font la même largeur, donc la mesure se vaut. */
@media (max-width: 46rem) {
  .fiche-head { grid-template-columns: minmax(0, 1fr); }
  .fiche-art { max-width: 13rem; }
}

/* Les liens vers l'extérieur, en rangée : ce sont des recherches offertes, pas
   des faits sur l'œuvre — les identifiants que nous possédons, eux, sont dans
   la grille de l'en-tête. */
.link-row {
  list-style: none;
  margin: 0 0 0.6rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* Une entrée de liste dont le fichier ne se retrouve plus. Montrée quand même :
   une ligne muette dit qu'il manque quelque chose, une ligne absente ne dit
   rien — et c'est précisément ce qu'on veut savoir d'une liste ancienne. */
.track.is-missing { opacity: 0.6; }
.track.is-missing .track-title { text-decoration: line-through; }

/* ---------- La scène comme une fenêtre ---------- */

/* Les trois commandes, au survol de l'image. Absentes le reste du temps :
   posées en permanence sur un film, elles se lisent comme une décoration
   qu'on finit par ne plus voir — et elles mangent le coin de l'image. */
.stage-window {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  z-index: 4;
  display: flex;
  gap: 0.25rem;
  opacity: 0;
  transition: opacity 120ms ease-out;
}
.player-stage:hover .stage-window,
.stage-window:focus-within { opacity: 1; }
/* Sans survol — au doigt — elles restent visibles : il n'y a pas de « avant
   le clic » sur un écran tactile. */
@media (hover: none) { .stage-window { opacity: 1; } }

.stage-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(6px);
  color: var(--fg);
  cursor: pointer;
}
.stage-button:hover { border-color: var(--accent); color: var(--accent); }
.stage-button .icon { width: 15px; height: 15px; }

/* Le troisième cran : l'image descend dans la barre, à la place de la
   vignette. Elle reste un enfant direct de la grille — c'est la règle qui
   permet de la déplacer sans la mettre en pause — et ne fait que changer de
   case. La barre occupe la même rangée, sur toute la largeur : les deux se
   superposent, et le z-index dit lequel est devant. */
.player-page[data-stage="barre"] > .player-stage {
  grid-column: 1;
  grid-row: -2 / -1;
  z-index: 6;
  align-self: center;
  justify-self: start;
  width: 5.5rem;
  /* La hauteur est posée sur la scène et non sur ce qu'elle contient : selon
     ce qui joue, c'est une vidéo, une pochette ou un carré de repli, chacun
     avec sa forme et ses marges. Borner le conteneur les borne tous — et
     c'est ce qui manquait, la pochette d'un morceau dépassant de la barre
     alors que la vidéo y tenait. */
  height: 2.5rem;
  min-height: 0;
  overflow: hidden;
  margin: 0 0 0 1rem;
  cursor: pointer;
}
/* La marge basse du cadre vidéo — 24 px, qui séparent l'image de sa légende —
   n'a plus rien à séparer ici : elle dépassait de la barre par le bas, et
   c'est ce qui empêchait la vignette d'y tenir. */
.player-page[data-stage="barre"] > .player-stage .video-stage:not(:fullscreen),
.player-page[data-stage="barre"] > .player-stage .player-art:not(:fullscreen) {
  margin: 0;
  height: 100%;
  min-height: 0;
}
.player-page[data-stage="barre"] > .player-stage .video-stage:not(:fullscreen) video,
.player-page[data-stage="barre"] > .player-stage .player-art:not(:fullscreen) img {
  height: 100%;
  max-height: 100%;
  width: auto;
  border-radius: var(--radius);
}
/* Tout ce qui accompagne l'image reste en haut : dans trois centimètres de
   barre, un titre, une légende et des suggestions ne se lisent pas. */
.player-page[data-stage="barre"] > .player-stage .player-caption,
.player-page[data-stage="barre"] > .player-stage .player-related,
.player-page[data-stage="barre"] > .player-stage .stage-window,
.player-page[data-stage="barre"] > .player-stage .player-idle { display: none; }

/* Et la barre lui fait place, plutôt que de la laisser passer par-dessus le
   titre de ce qui joue. */
.player-page[data-stage="barre"] > .queue-bar { padding-left: 7.5rem; }

/* ---------- L'en-tête de fiche : gestes, liens, replis ---------- */

/* La colonne de gauche porte les gestes puis le visuel. Les gestes en haut :
   ce sont eux qu'on cherche en arrivant, et une image de trente centimètres
   les repoussait sous la ligne de flottaison sur un écran court. */
.fiche-side {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-width: 0;
}

.fiche-acts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

/* Des carrés plutôt que des mots. « Télécharger » et « Permissions » prenaient
   à eux seuls la moitié de la barre, pour des gestes qu'on fait une fois par
   œuvre — là où le cœur et la note se posent en passant. */
.fiche-act {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--fg);
  cursor: pointer;
}
.fiche-act:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }
.fiche-act .icon { width: 17px; height: 17px; }
/* Lire est le geste principal, et se voit comme tel : c'est le seul de la
   rangée qu'on cherche des yeux plutôt que du souvenir. */
.fiche-act.is-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}
.fiche-act.is-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

/* Les liens du dehors, en une ligne au pied de l'en-tête. En pastilles et non
   en phrase : ce sont six portes équivalentes, et une énumération séparée par
   des points laisse chercher où finit l'une et où commence l'autre. */
.fiche-links {
  list-style: none;
  margin: 0.9rem 0 0;
  padding: 0.7rem 0 0;
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.fiche-links a {
  display: inline-block;
  padding: 0.18rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.82rem;
  color: var(--muted);
}
.fiche-links a:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; }

/* Le champ d'ajout d'étiquette, révélé par le compteur de la barre. */
.fiche-tags .tag-add[hidden] { display: none; }
