/* =========================================================================
   HARDBASS.FR — feuille de style unique
   -------------------------------------------------------------------------
   Aucune dépendance externe : ni framework, ni CDN, ni police distante.
   Tout est servi depuis l'origine, ce qui permet une politique de sécurité
   du contenu stricte et supprime toute fuite d'adresse IP vers un tiers.

   Cascade maîtrisée par @layer : un composant ne peut plus être écrasé par
   accident depuis une règle de base, et aucun !important n'est nécessaire.
   ========================================================================= */

@layer reset, tokens, base, layout, components, utilities;

/* ------------------------------------------------------------------ reset */
@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  * {
    margin: 0;
  }

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

  img,
  svg,
  canvas,
  video {
    display: block;
    max-width: 100%;
  }

  input,
  button,
  select,
  textarea {
    font: inherit;
    color: inherit;
  }

  ul,
  ol {
    list-style: none;
    padding: 0;
  }

  a {
    color: inherit;
    text-decoration: none;
  }

  :focus-visible {
    outline: 2px solid var(--ice);
    outline-offset: 3px;
  }

  /*
    Sauf sur ce qu'aucun clavier ne peut atteindre.

    « FocusOnNavigate » pose le focus sur le titre apres une navigation, pour
    qu'un lecteur d'ecran sache que la page a change. Il lui ajoute pour cela
    un « tabindex="-1" » — un cran qui n'entre pas dans l'ordre de tabulation
    et ne se rejoint que par script.

    L'anneau y encadrait donc le titre a l'arrivee, sans rien apprendre a
    personne : celui qui navigue au clavier n'y arrivera jamais par lui-meme,
    et celui qui ecoute la page a deja l'annonce. Le retirer ne coute aucune
    accessibilite ; le laisser faisait croire a un defaut d'affichage.

    Le focus, lui, reste : c'est de lui que vient l'annonce.
  */
  [tabindex="-1"]:focus {
    outline: none;
  }
}

/* ----------------------------------------------------------------- tokens */
@layer tokens {
  /*
     Pour auto-héberger Oswald (l'ancienne police du site), déposer le fichier
     dans wwwroot/fonts puis décommenter. Rien n'est chargé depuis Google :
     l'appel distant exposerait l'IP de chaque visiteur à un tiers.

     @font-face {
       font-family: "Oswald";
       src: url("/fonts/oswald.woff2") format("woff2");
       font-weight: 400 700;
       font-display: swap;
     }
  */

  :root {
    color-scheme: dark;

    /* Couleurs en oklch : luminosité perceptuellement linéaire, donc des
       variantes cohérentes entre elles sans réglage à l'œil. */
    --ink-950: oklch(11% 0.012 260);
    --ink-900: oklch(15% 0.014 260);
    --ink-850: oklch(18% 0.015 260);
    --ink-800: oklch(21% 0.016 260);
    --ink-700: oklch(27% 0.018 260);
    --ink-600: oklch(36% 0.018 260);

    --chalk: oklch(97% 0.004 260);
    --smoke: oklch(72% 0.012 260);
    --smoke-dim: oklch(58% 0.014 260);

    /* Rouge de signalisation : l'accent d'action, jamais décoratif. */
    --red: oklch(57% 0.213 26);
    --red-hot: oklch(66% 0.227 28);
    --red-dim: oklch(38% 0.13 26);

    /* Cyan froid : information, direct, focus. */
    --ice: oklch(81% 0.121 214);
    --ice-dim: oklch(52% 0.09 214);

    --surface: var(--ink-900);
    --surface-raised: var(--ink-850);
    --line: color-mix(in oklab, var(--ink-600) 70%, transparent);
    --line-strong: var(--ink-600);

    /* Échelle typographique fluide : plus de points de rupture à maintenir. */
    --step--1: clamp(0.78rem, 0.74rem + 0.2vw, 0.86rem);
    --step-0: clamp(0.94rem, 0.9rem + 0.2vw, 1rem);
    --step-1: clamp(1.1rem, 1rem + 0.5vw, 1.3rem);
    --step-2: clamp(1.4rem, 1.2rem + 1vw, 1.9rem);
    --step-3: clamp(1.9rem, 1.5rem + 2vw, 3rem);
    --step-4: clamp(2.6rem, 1.8rem + 4.5vw, 5.5rem);

    --gap: clamp(1rem, 0.7rem + 1.2vw, 1.75rem);
    --shell: 78rem;
    --radius: 4px;
    --dock-height: 6rem;

    --font-display: "Oswald", "Archivo Narrow", "Roboto Condensed",
      "Helvetica Neue", "Arial Narrow", system-ui, sans-serif;
    --font-body: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
      "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;

    --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
    --shadow-panel: 0 1px 0 color-mix(in oklab, var(--chalk) 6%, transparent);
  }
}

/* ------------------------------------------------------------------- base */
@layer base {
  /*
    Ascenseur accorde au site.

    « color-scheme: dark » suffit a le noircir, mais laisse chaque navigateur
    choisir sa teinte : le resultat va du gris clair au gris moyen selon le
    systeme, et tranche sur une page noire. Les deux proprietes standard le
    fixent ; la forme WebKit prend le relais la ou elles manquent encore.

    Les deux cohabitent sans conflit : un navigateur qui comprend
    « scrollbar-color » ignore les pseudo-elements WebKit.
  */
  :root {
    scrollbar-color: var(--ink-600) var(--ink-950);
    scrollbar-width: thin;
  }

  ::-webkit-scrollbar {
    width: 10px;
    height: 10px;
  }

  ::-webkit-scrollbar-track {
    background: var(--ink-950);
  }

  ::-webkit-scrollbar-thumb {
    background: var(--ink-600);
    border-radius: 999px;
  }

  ::-webkit-scrollbar-thumb:hover {
    background: var(--smoke-dim);
  }

  /*
    Pas de fleches aux extremites.

    Definir les pseudo-elements WebKit fait basculer le navigateur sur son
    ascenseur historique, qui dessine deux boutons que « scrollbar-width » aurait
    evites. On les retire donc explicitement.
  */
  ::-webkit-scrollbar-button {
    display: none;
    block-size: 0;
    inline-size: 0;
  }

  ::-webkit-scrollbar-corner {
    background: transparent;
  }

  body {
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    background-color: var(--ink-950);
    color: var(--chalk);
    font-family: var(--font-body);
    font-size: var(--step-0);
    line-height: 1.55;
    /* Compense le lecteur fixe en pied de page, encoche iOS comprise.
       La marge dépasse volontairement la hauteur du lecteur : sans elle, la
       dernière ligne de contenu passe sous la barre. */
    padding-block-end: calc(var(--dock-height) + 3rem + env(safe-area-inset-bottom));
  }

  /* Grain diagonal très léger : casse l'aplat noir sans image de fond. */
  body::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: -2;
    pointer-events: none;
    background-image: repeating-linear-gradient(
      135deg,
      color-mix(in oklab, var(--chalk) 2%, transparent) 0 1px,
      transparent 1px 7px
    );
    opacity: 0.5;
  }

  h1,
  h2,
  h3 {
    font-family: var(--font-display);
    font-weight: 700;
    line-height: 1.02;
    letter-spacing: 0.01em;
    text-transform: uppercase;
    text-wrap: balance;
  }

  p {
    text-wrap: pretty;
  }

  a:hover {
    color: var(--red-hot);
  }

  time {
    font-family: var(--font-mono);
    font-size: var(--step--1);
    color: var(--smoke-dim);
    font-variant-numeric: tabular-nums;
  }
}

/* ----------------------------------------------------------------- layout */
@layer layout {
  .shell {
    width: min(100% - 2 * var(--gap), var(--shell));
    margin-inline: auto;
  }

  .site-main {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: var(--gap);
    padding-block: var(--gap);
  }

  /* Les trois barres du fond — signature visuelle héritée du site d'origine.
     Dégradé NON répétitif : `repeating-linear-gradient` en produisait une
     dizaine sur toute la largeur, ce qui donnait des pavés gris au lieu des
     trois bandes obliques attendues. L'angle de 100deg reprend l'inclinaison
     du logo. */
  .stripes {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 50%;
    translate: -50% 0;
    width: min(32rem, 84vw);
    z-index: -1;
    pointer-events: none;
    opacity: 0.085;
    background: linear-gradient(
      100deg,
      transparent 0 14%,
      var(--chalk) 14% 26%,
      transparent 26% 38%,
      var(--chalk) 38% 50%,
      transparent 50% 62%,
      var(--chalk) 62% 74%,
      transparent 74%
    );
    /* Fondu haut et bas : les barres ne viennent pas buter sur l'en-tête
       ni sur le lecteur. */
    mask-image: linear-gradient(180deg, transparent, #000 18%, #000 58%, transparent 94%);
    transition: opacity 120ms linear;
  }

  .grid {
    display: grid;
    gap: var(--gap);

    /* Sans cela, les colonnes d'une même ligne s'étirent toutes à la hauteur
       de la plus haute : le panneau « à l'antenne » se retrouvait avec un
       grand vide sous la pochette pour rattraper la longueur du classement.
       Chaque panneau prend maintenant la hauteur de son contenu. */
    align-items: start;
  }

  /*
    Le profil deroge au « align-items: start » de la grille, et seul lui.

    Ce reglage general existe pour l'accueil, ou un classement de vingt titres
    voisine une pochette : les egaliser creuserait un vide enorme. Le profil ne
    presente jamais que deux panneaux jumeaux — identite et mot de passe, amis
    et favoris, acheminement et presence — dont les hauteurs se tiennent a
    quelques dizaines de pixels. Mesure sur l'onglet identite : 365 contre 412.
    Deux cartes presque egales se lisent comme un defaut d'alignement ; egales,
    comme une intention.

    Ce qui rend la chose sure est que plus aucun panneau du profil ne grandit
    sans borne : voir « .console__corps », qui etait le seul.
  */
  .grid--profile {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
    align-items: stretch;
  }

  .site-header {
    position: sticky;
    top: 0;
    z-index: 30;
    background: color-mix(in oklab, var(--ink-950) 88%, transparent);
    backdrop-filter: blur(10px);
    border-block-end: 1px solid var(--line);
  }

  .site-header__inner {
    display: flex;
    align-items: center;
    gap: var(--gap);
    min-height: 3.75rem;
    flex-wrap: wrap;
  }

  .site-footer {
    border-block-start: 1px solid var(--line);
    padding-block: var(--gap);
    font-size: var(--step--1);
    color: var(--smoke-dim);
  }
}

/* ------------------------------------------------------------- components */
@layer components {
  /* ---- marque ---- */
  .brand {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-family: var(--font-display);
    font-size: var(--step-1);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    margin-inline-end: auto;
  }

  .brand:hover {
    color: var(--chalk);
  }

  /* Trois barres, exactement trois.
     `repeating-linear-gradient` en produisait une dizaine de très fines : ça
     donnait des hachures, pas un logo. Dégradé non répétitif, incliné une
     seule fois par `skewX`. */
  .brand__mark {
    width: 1.4rem;
    height: 1.45rem;
    background: linear-gradient(
      90deg,
      currentColor 0 24%,
      transparent 24% 38%,
      currentColor 38% 62%,
      transparent 62% 76%,
      currentColor 76% 100%
    );
    color: var(--chalk);
    rotate: 0deg;
    transform: skewX(-14deg);
    transition: color 160ms var(--ease-out);
  }

  .brand:hover .brand__mark {
    color: var(--red-hot);
  }

  .brand__tld {
    color: var(--red);
  }

  /* ---- navigation ---- */
  .nav {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex-wrap: wrap;
  }

  .nav a {
    position: relative;
    padding: 0.35rem 0.65rem;
    font-family: var(--font-display);
    font-size: var(--step--1);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--smoke);
  }

  .nav a::after {
    content: "";
    position: absolute;
    inset-inline: 0.65rem;
    bottom: 0.1rem;
    height: 2px;
    background: var(--red);
    scale: 0 1;
    transform-origin: left;
    transition: scale 180ms var(--ease-out);
  }

  .nav a:hover,
  .nav a[aria-current="page"] {
    color: var(--chalk);
  }

  .nav a:hover::after {
    scale: 1 1;
  }

  .nav--account {
    gap: 0.5rem;
  }

  .pseudo {
    font-family: var(--font-display);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    font-size: var(--step--1);
    color: var(--ice);
  }

  /* ---- boutons ---- */
  .btn {
    --btn-bg: transparent;
    --btn-fg: var(--chalk);
    --btn-line: var(--line-strong);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    padding: 0.7rem 1.3rem;
    border: 1px solid var(--btn-line);
    border-radius: var(--radius);
    background: var(--btn-bg);
    color: var(--btn-fg);
    font-family: var(--font-display);
    font-size: var(--step-0);
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    cursor: pointer;
    transition: background 140ms var(--ease-out), border-color 140ms var(--ease-out),
      translate 140ms var(--ease-out);
  }

  .btn:hover {
    translate: 0 -1px;
    border-color: var(--chalk);
    color: var(--chalk);
  }

  .btn:active {
    translate: 0 0;
  }

  .btn--accent {
    --btn-bg: var(--red);
    --btn-line: var(--red);
    --btn-fg: var(--chalk);
  }

  .btn--accent:hover {
    --btn-bg: var(--red-hot);
    --btn-line: var(--red-hot);
  }

  .btn--danger {
    --btn-line: var(--red-dim);
    --btn-fg: var(--red-hot);
  }

  .btn--ghost {
    --btn-bg: transparent;
  }

  .btn--sm {
    padding: 0.35rem 0.7rem;
    font-size: var(--step--1);
  }

  .btn--block {
    width: 100%;
  }

  .btn-icon {
    display: inline-grid;
    place-items: center;
    width: 2.4rem;
    height: 2.4rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    background: transparent;
    color: var(--smoke);
    font-size: 1.1rem;
    cursor: pointer;
    transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out);
  }

  .btn-icon:hover {
    color: var(--chalk);
    border-color: var(--chalk);
  }

  .btn-icon[aria-pressed="true"] {
    color: var(--red-hot);
    border-color: var(--red);
  }

  .inline-form {
    display: inline;
  }

  /* ---- héros ---- */
  .hero {
    padding-block: clamp(1.5rem, 1rem + 4vw, 4rem);
    text-align: center;
  }

  .hero__kicker {
    font-family: var(--font-display);
    font-size: var(--step--1);
    letter-spacing: 0.4em;
    text-transform: uppercase;
    color: var(--red);
  }

  .hero__title {
    display: flex;
    justify-content: center;
    align-items: baseline;
    gap: 0.1em;
    font-size: var(--step-4);
    letter-spacing: -0.02em;
  }

  .hero__title-accent {
    color: var(--red);
  }

  .hero__tagline {
    margin-block-start: 0.75rem;
    color: var(--smoke);
    font-size: var(--step-1);
  }

  .hero__stats {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: clamp(1rem, 4vw, 3.5rem);
    margin-block-start: clamp(1.5rem, 3vw, 2.5rem);
  }

  .hero__stats div {
    text-align: center;
  }

  .hero__stats dt {
    font-family: var(--font-display);
    font-size: var(--step--1);
    letter-spacing: 0.24em;
    text-transform: uppercase;
    color: var(--smoke-dim);
  }

  .hero__stats dd {
    font-family: var(--font-display);
    font-size: var(--step-2);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
  }

  .hero__stats--compact {
    justify-content: flex-start;
    margin-block: 0 var(--gap);
  }

  /* ---- panneaux ---- */
  .panel {
    position: relative;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: clamp(1rem, 0.8rem + 1vw, 1.6rem);
    box-shadow: var(--shadow-panel);
    /* Requêtes de conteneur : les cartes s'adaptent à leur colonne, pas à
       la largeur de la fenêtre. */
    container-type: inline-size;
  }

  /* Liseré rouge sur le panneau de l'antenne uniquement. */
  .panel--now::before {
    content: "";
    position: absolute;
    inset-block: -1px;
    inset-inline-start: -1px;
    width: 3px;
    background: linear-gradient(180deg, var(--red), var(--red-dim));
  }

  .panel--form {
    max-width: 30rem;
    margin-inline: auto;
    width: 100%;
  }

  .panel__title {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    font-size: var(--step-1);
    letter-spacing: 0.14em;
    padding-block-end: 0.6rem;
    margin-block-end: 1rem;
    border-block-end: 1px solid var(--line);
  }

  .panel__hint {
    font-family: var(--font-body);
    font-size: var(--step--1);
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
    color: var(--smoke-dim);
  }

  .panel__next,
  .panel__more {
    margin-block-start: 1rem;
    padding-block-start: 0.8rem;
    border-block-start: 1px solid var(--line);
    font-size: var(--step--1);
    color: var(--smoke);
  }

  .page-title {
    font-size: var(--step-3);
    letter-spacing: 0.02em;
  }

  /* ---- morceau ---- */
  .track {
    display: flex;
    align-items: center;
    gap: 1rem;
  }

  .track__art {
    width: 5.5rem;
    height: 5.5rem;
    object-fit: cover;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    background: var(--ink-800);
  }

  /*
    Pile de pochettes : la sortante dessous, la nouvelle par-dessus. Les deux
    occupent la meme case, ce qui permet le fondu enchaine sans que la carte ne
    change de taille au passage.
  */
  .track__pochette {
    position: relative;
    display: block;
    flex: none;
    width: 5.5rem;
    height: 5.5rem;
  }

  .track__pochette .track__art {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
  }

  .track__art--entrante {
    animation: pochette-entrante 520ms var(--ease-out) both;
  }

  /*
    La sortante recule pendant que la nouvelle monte. Meme duree, pour que les
    deux mouvements n'en fassent qu'un.
  */
  .track__art--sortante {
    animation: pochette-sortante 520ms var(--ease-out) both;
    transform-origin: center;
  }

  .track__art--empty {
    background: repeating-linear-gradient(
      45deg,
      var(--ink-800) 0 8px,
      var(--ink-850) 8px 16px
    );
  }

  .track__artist {
    font-size: var(--step--1);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--red);
  }

  .track__title {
    font-family: var(--font-display);
    font-size: var(--step-2);
    line-height: 1.1;
    text-wrap: balance;
  }

  @container (min-width: 30rem) {
    .track--big .track__pochette {
      width: 7.5rem;
      height: 7.5rem;
    }

    .track--big .track__art {
      width: 7.5rem;
      height: 7.5rem;
    }
  }

  /* ---- listes ---- */
  .playlist__row {
    display: flex;
    align-items: baseline;
    gap: 0.9rem;
    padding-block: 0.55rem;
    border-block-end: 1px solid color-mix(in oklab, var(--line) 60%, transparent);
  }

  .playlist__row:last-child {
    border-block-end: none;
  }

  .playlist__song {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem;
    min-width: 0;
  }

  .playlist__song strong {
    font-weight: 600;
  }

  .playlist--dense .playlist__row {
    padding-block: 0.4rem;
    font-size: var(--step--1);
  }

  .chart__row {
    display: flex;
    /* Centre et non ligne de base : le classement porte des pochettes, et une
       image n'a pas de ligne de base — s'y aligner la ferait flotter. */
    align-items: center;
    gap: 0.9rem;
    padding-block: 0.5rem;
    border-block-end: 1px solid color-mix(in oklab, var(--line) 60%, transparent);
    counter-increment: chart;
  }

  .chart__row:last-child {
    border-block-end: none;
  }

  .chart__count {
    min-width: 2.2rem;
    font-family: var(--font-display);
    font-size: var(--step-1);
    font-variant-numeric: tabular-nums;
    color: var(--red);
    text-align: right;
  }

  .chart__song {
    display: flex;
    flex-direction: column;
    min-width: 0;
  }

  /* ---- étiquettes ---- */
  .tag,
  .badge {
    display: inline-block;
    padding: 0.15rem 0.5rem;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    font-size: var(--step--1);
    letter-spacing: 0.06em;
    color: var(--smoke);
  }

  .tag--role {
    border-color: var(--ice-dim);
    color: var(--ice);
  }

  .badge--live {
    border-color: var(--red);
    color: var(--red-hot);
  }

  .notice {
    padding: 0.7rem 1rem;
    border: 1px solid var(--ice-dim);
    border-radius: var(--radius);
    background: color-mix(in oklab, var(--ice) 8%, transparent);
    color: var(--ice);
    font-size: var(--step--1);
  }

  .empty {
    padding-block: 1.2rem;
    color: var(--smoke-dim);
    font-style: italic;
  }

  .error-code {
    font-family: var(--font-display);
    font-size: var(--step-4);
    line-height: 1;
    color: var(--red);
  }

  /* ---- formulaires ---- */
  .field {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin-block-end: 1.1rem;
  }

  .field label,
  form label {
    font-family: var(--font-display);
    font-size: var(--step--1);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--smoke);
  }

  .field input {
    padding: 0.7rem 0.85rem;
    background: var(--ink-950);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    color: var(--chalk);
    transition: border-color 140ms var(--ease-out);
  }

  .field input:hover {
    border-color: var(--smoke-dim);
  }

  .field input:focus-visible {
    border-color: var(--ice);
  }

  /* Le champ passe en rouge dès qu'il porte un message d'erreur : la règle
     suit l'état réel du DOM, sans classe à poser en JavaScript. */
  .field:has(.field__error:not(:empty)) input {
    border-color: var(--red);
  }

  .field__hint {
    font-size: var(--step--1);
    color: var(--smoke-dim);
  }

  .field__error:not(:empty) {
    font-size: var(--step--1);
    color: var(--red-hot);
  }

  .form-errors:not(:empty) {
    margin-block-end: 1rem;
    padding: 0.7rem 1rem;
    border: 1px solid var(--red-dim);
    border-radius: var(--radius);
    background: color-mix(in oklab, var(--red) 10%, transparent);
    color: var(--red-hot);
    font-size: var(--step--1);
  }

  .form-errors ul {
    list-style: none;
  }

  .checkbox {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-block-end: 1.2rem;
    text-transform: none;
    letter-spacing: 0;
    font-family: var(--font-body);
    color: var(--smoke);
  }

  .checkbox input {
    width: 1rem;
    height: 1rem;
    accent-color: var(--red);
  }

  /* ---- tableaux ---- */
  .table-scroll {
    overflow-x: auto;

    /*
      Sans cela, arriver au bout du defilement lateral fait remonter le geste au
      navigateur, qui l'interprete comme « page precedente ». On perd la page en
      voulant lire une colonne.
    */
    overscroll-behavior-x: contain;
  }

  .table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--step--1);
  }

  .table th {
    text-align: left;
    font-family: var(--font-display);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--smoke-dim);
    padding-block: 0.5rem;

    /* Alignee sur les cellules : sans elle, « Inscrit le » et « Roles » se
       touchaient dans l'en-tete alors que leurs colonnes etaient separees. */
    padding-inline-end: 0.8rem;
    border-block-end: 1px solid var(--line-strong);
  }

  .table td {
    padding-block: 0.6rem;
    padding-inline-end: 0.8rem;
    border-block-end: 1px solid color-mix(in oklab, var(--line) 60%, transparent);
    vertical-align: middle;
  }

  /*
    A poser sur un div a l'interieur de la cellule, jamais sur le <td> lui-meme.
    « display: flex » retire la cellule de la mise en page du tableau : elle
    cesse d'etre une table-cell, sa hauteur ne suit plus celle de la ligne, et sa
    bordure basse se retrouve six pixels au-dessus des autres. Mesure : 41 px de
    haut contre 48 pour les cellules voisines.
  */
  .table__actions {
    display: flex;
    gap: 0.35rem;
    flex-wrap: wrap;
    align-items: center;
  }

  .datalist {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.5rem 1.2rem;
    font-size: var(--step--1);
  }

  .datalist dt {
    font-family: var(--font-display);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--smoke-dim);
  }

  /*
    Une valeur suivie d'une pastille d'etat. Le conteneur porte l'ecart, parce
    que le balisage ne peut pas s'en charger : un commentaire Razor separe les
    deux morceaux, et Razor n'emet alors aucune espace entre eux — le point se
    retrouve colle a la derniere lettre de l'adresse.

    « wrap » plutot qu'une ligne unique : sur telephone, une adresse longue et
    sa mention deborderaient de la colonne.
  */
  .datalist__valeur {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.2rem 0.6rem;
  }

  /* L'aide et le bouton de renvoi valent une ligne chacun, sous l'adresse. */
  .datalist__valeur > .etat__note,
  .datalist__valeur > .inline-form {
    flex-basis: 100%;
  }

  /* ---- lecteur persistant ---- */
  .dock {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 40;
    background: color-mix(in oklab, var(--ink-950) 92%, transparent);
    backdrop-filter: blur(14px);
    border-block-start: 1px solid var(--line-strong);
    padding-block-end: env(safe-area-inset-bottom);
  }

  .dock__inner {
    display: flex;
    align-items: center;
    gap: clamp(0.6rem, 2vw, 1.25rem);
    min-height: var(--dock-height);
    padding-block: 0.6rem;
  }

  .dock__play {
    flex: none;
    display: grid;
    place-items: center;
    width: 3.25rem;
    height: 3.25rem;
    border: 1px solid var(--red);
    border-radius: 50%;
    background: color-mix(in oklab, var(--red) 16%, transparent);
    cursor: pointer;
    transition: background 160ms var(--ease-out), scale 160ms var(--ease-out);
  }

  .dock__play:hover {
    background: color-mix(in oklab, var(--red) 30%, transparent);
    scale: 1.04;
  }

  /* Ce qui remplace le bouton quand l'ecoute est fermee a ce visiteur.
     Volontairement rien d'un bouton de lecture : le dessin d'un cercle et
     d'un triangle promet du son, et une promesse qu'on ne tient pas se lit
     comme une panne. */
  .dock__connexion {
    flex: none;
    display: grid;
    place-items: center;
    min-height: 3.25rem;
    padding-inline: 0.9rem;
    border: 1px solid var(--red);
    border-radius: 6px;
    background: color-mix(in oklab, var(--red) 16%, transparent);
    color: var(--chalk);
    font-size: 0.85rem;
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    text-decoration: none;
    text-wrap: balance;
    transition: background 160ms var(--ease-out);
  }

  .dock__connexion:hover,
  .dock__connexion:focus-visible {
    background: color-mix(in oklab, var(--red) 30%, transparent);
  }

  /* Triangle de lecture dessiné en CSS : aucune image à charger. */
  .dock__play-icon {
    width: 0;
    height: 0;
    margin-inline-start: 3px;
    border-block: 0.55rem solid transparent;
    border-inline-start: 0.9rem solid var(--chalk);
    transition: border 120ms var(--ease-out);
  }

  /* En lecture, le même élément devient deux barres de pause. */
  .dock[data-state="playing"] .dock__play-icon {
    width: 0.85rem;
    height: 1.05rem;
    margin-inline-start: 0;
    border: none;
    background: linear-gradient(
      90deg,
      var(--chalk) 0 34%,
      transparent 34% 66%,
      var(--chalk) 66% 100%
    );
  }

  .dock[data-state="loading"] .dock__play {
    animation: pulse 1.1s ease-in-out infinite;
  }

  .dock__meta {
    flex: 1;
    min-width: 0;
  }

  .dock__label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-family: var(--font-display);
    font-size: var(--step--1);
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--smoke-dim);
  }

  .live-dot {
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: var(--smoke-dim);
  }

  .dock[data-state="playing"] .live-dot {
    background: var(--red);
    animation: pulse 1.6s ease-in-out infinite;
  }

  .dock__track {
    font-family: var(--font-display);
    font-size: var(--step-1);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /*
    L'avis de radio pleine. Vide le plus souvent, et sans marge : il ne prend
    alors aucune place, mais reste dans la page pour qu'un lecteur d'écran
    annonce son texte quand il arrive.
  */
  .dock__avis {
    margin: 0;
    font-size: var(--step--1);
    color: var(--chalk);
  }

  .dock__progress {
    margin-block-start: 0.4rem;
    height: 2px;
    background: var(--ink-700);
    overflow: hidden;
  }

  /*
    Toujours pleine, raccourcie par une échelle. Une largeur animée refaisait
    le style et la mise en page de la page entière à chaque image, neuf
    dixièmes du temps, lecteur à l'arrêt compris — mesuré : 62 ms de fil
    principal par seconde en classique, 146 sous Панелька. Une échelle se
    compose sans rien recalculer. Le dégradé n'y perd rien : il couvre la
    barre entière avant réduction, comme il couvrait sa largeur.
  */
  .dock__progress-bar {
    display: block;
    height: 100%;
    width: 100%;
    background: linear-gradient(90deg, var(--red), var(--red-hot));
    transform: scaleX(0);
    transform-origin: 0 50%;
    transition: transform 900ms linear;
  }

  .dock__spectrum {
    flex: none;
    width: clamp(6rem, 18vw, 20rem);
    height: 3rem;
    opacity: 0.85;
  }

  .dock__controls {
    flex: none;
    display: flex;
    align-items: center;
    gap: 0.75rem;
  }

  /*
    Conteneur souple, et l'ecart vient de lui. Les morceaux de cette ligne sont
    separes dans le balisage par un « @if » et un commentaire Razor, autour
    desquels Razor n'emet aucune espace : « 0 slavdont ». Rattraper chaque
    jointure a la main revenait a en oublier une a la retouche suivante — c'est
    arrive deux fois.
  */
  .dock__listeners {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    font-family: var(--font-display);
    font-size: var(--step-0);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  .volume input {
    width: 5.5rem;
    accent-color: var(--red);
  }

  @keyframes pulse {
    0%,
    100% {
      opacity: 1;
    }
    50% {
      opacity: 0.45;
    }
  }

  /* Sous 60rem, le lecteur perd le superflu plutôt que de se tasser. */
  @media (max-width: 60rem) {
    .dock__spectrum,
    .volume {
      display: none;
    }
  }

  @media (max-width: 40rem) {
    .site-header__inner {
      justify-content: center;
    }

    .brand {
      margin-inline-end: 0;
      width: 100%;
      justify-content: center;
    }

    .nav {
      justify-content: center;
    }
  }

  /* ------------------------------------------- journal des modifications */

  /* ---- entrees en scene ---- */

  /*
    Deux mouvements, pas un de plus.

    L'un accueille le contenu d'une page, l'autre signale un changement de
    morceau. Le lecteur, lui, ne bouge jamais — il vit hors du contenu de page,
    et le voir tressauter a chaque navigation trahirait la continuite du son.

    Aucun des deux ne part de l'invisible, et c'est le point important. Une
    entree en fondu depuis zero se lit comme une page qui charge : plus la page
    est dense, plus l'effet trompe. Mesure sur le journal — quatre-vingt-dix
    kilo-octets, deux cent cinquante elements — le serveur repond en douze
    millisecondes, mais le fondu tenait l'ecran presque vide pendant deux cent
    quarante. C'etait l'animation qu'on prenait pour une attente.

    Le bloc « prefers-reduced-motion » en fin de feuille les neutralise tous les
    deux : il vise l'ensemble des elements, sans avoir a les nommer.
  */

  @keyframes apparition {
    from {
      opacity: 0.35;
      translate: 0 5px;
    }
  }

  @keyframes changement-de-piste {
    from {
      opacity: 0.35;
      translate: 0 4px;
    }
  }

  .site-main > * {
    animation: apparition 140ms var(--ease-out) both;
  }

  /*
    Rejoue a chaque morceau grace a une cle de rendu posee sur l'element : Blazor
    remplace alors le noeud, ce qui relance l'animation. Sans cette cle, il
    reutiliserait le meme noeud en changeant seulement son texte, et rien ne
    bougerait.
  */
  /*
    Fondu enchaine de la pochette. Elle ne bouge pas : seule son opacite monte,
    par-dessus la precedente restee dessous. Un deplacement en plus donnerait
    l'impression que la carte tremble a chaque morceau.
  */
  @keyframes pochette-entrante {
    from {
      opacity: 0;
    }
  }

  /*
    La sortante s'eloigne : elle retrecit et se floute, ce qui se lit comme une
    profondeur plutot que comme un effacement.

    Son opacite ne bouge PAS avant 60 %, et c'est tout l'interet de cette
    courbe. Les deux images occupent la meme case : les faire varier ensemble
    laisserait passer le fond entre elles au milieu du fondu — chacune a moitie
    transparente — et l'on verrait un creux de luminosite au lieu d'un
    enchainement. La sortante ne s'efface donc qu'une fois recouverte.
  */
  @keyframes pochette-sortante {
    from {
      scale: 1;
      filter: blur(0);
    }

    60% {
      opacity: 1;
    }

    to {
      scale: 0.94;
      filter: blur(3px);
      opacity: 0;
    }
  }

  /*
    Le texte, lui, monte. Artiste puis titre, decales de quelques dizaines de
    millisecondes : l'oeil lit le changement comme un mouvement, et non comme un
    remplacement brutal. Le decalage est ce qui distingue une transition d'un
    simple clignotement.
  */
  .track__body > .track__artist {
    animation: changement-de-piste 320ms var(--ease-out) both;
  }

  .track__body > .track__title {
    animation: changement-de-piste 320ms 60ms var(--ease-out) both;
  }

  .track__body > .badge {
    animation: changement-de-piste 320ms 120ms var(--ease-out) both;
  }

  .dock__track {
    animation: changement-de-piste 320ms var(--ease-out) both;
  }

  /*
    Les listes se remplissent en cascade.

    Une liste qui apparait d'un bloc se lit comme un rechargement ; la meme,
    posee ligne apres ligne sur cent cinquante millisecondes, se lit comme un
    remplissage. Le decalage s'arrete a la cinquieme ligne : au-dela l'oeil ne
    distingue plus les retards, et les faire durer ne ferait qu'allonger
    l'attente.

    Le decalage passe par « nth-child » et non par une variable posee sur
    chaque ligne : la CSP interdit tout style en ligne, y compris pour porter
    un simple index.
  */
  .playlist__row {
    animation: apparition 200ms var(--ease-out) both;
  }

  .playlist__row:nth-child(2) {
    animation-delay: 30ms;
  }

  .playlist__row:nth-child(3) {
    animation-delay: 60ms;
  }

  .playlist__row:nth-child(4) {
    animation-delay: 90ms;
  }

  .playlist__row:nth-child(5) {
    animation-delay: 120ms;
  }

  .playlist__row:nth-child(n+6) {
    animation-delay: 150ms;
  }

  .empty {
    animation: apparition 200ms var(--ease-out) both;
  }

  /* ---- recherche ---- */

  .recherche {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    margin-block-end: var(--gap);
  }

  .recherche__champ {
    flex: 1 1 16rem;
    min-width: 0;
    padding: 0.45rem 0.7rem;
    background: var(--surface-raised);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    color: var(--chalk);
    font: inherit;
    font-size: var(--step--1);
  }

  .recherche__champ::placeholder {
    color: var(--smoke-dim);
  }

  .recherche__champ:focus-visible {
    border-color: var(--ice-dim);
    outline: none;
  }

  /*
    Le bouton d'effacement ne surgit pas : il se pose.

    Rendu en permanence et seulement masque, il peut s'animer dans les deux
    sens — un noeud retire du document n'a aucun moyen de partir en fondu.
    « visibility » le sort de l'arbre d'accessibilite et du parcours au
    clavier ; son basculement est retarde jusqu'a la fin du fondu, sinon il
    disparaitrait avant d'avoir fini de s'effacer.
  */
  .recherche__effacer {
    /* Le libelle ne se coupe pas pendant que la largeur s'anime. */
    overflow: hidden;
    white-space: nowrap;
    max-width: 12rem;
    transition: max-width 160ms var(--ease-out), opacity 160ms var(--ease-out),
      padding-inline 160ms var(--ease-out), margin-inline-start 160ms var(--ease-out),
      border-width 160ms var(--ease-out), visibility 0s;
  }

  /*
    Masque ET replie.

    « visibility » seul sortait le bouton de la vue mais lui gardait sa place :
    le champ restait court d'une largeur de bouton, et la ligne finissait sur un
    vide. La largeur s'anime donc jusqu'a zero, et la marge negative annule
    l'ecart que le conteneur flex reserve encore a un enfant sans largeur.
  */
  .recherche__effacer--masque {
    max-width: 0;
    opacity: 0;
    padding-inline: 0;
    border-width: 0;
    margin-inline-start: -0.5rem;
    visibility: hidden;
    transition: max-width 160ms var(--ease-out), opacity 160ms var(--ease-out),
      padding-inline 160ms var(--ease-out), margin-inline-start 160ms var(--ease-out),
      border-width 160ms var(--ease-out), visibility 0s 160ms;
  }

  /* ---- sous-onglets ---- */

  /*
    Des liens, pas des boutons : chaque onglet a sa propre adresse. Le lecteur
    d'ecran annonce donc une navigation, ce qu'elle est, et « aria-current » dit
    lequel est ouvert sans dependre de la couleur.
  */
  .onglets {
    display: flex;
    flex-wrap: wrap;
    gap: 1.2rem;
    margin-block-end: var(--gap);
    border-block-end: 1px solid var(--line);
  }

  .onglets__lien {
    padding-block: 0.5rem;
    margin-block-end: -1px;
    border-block-end: 2px solid transparent;
    font-family: var(--font-display);
    font-size: var(--step--1);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--smoke-dim);
    text-decoration: none;
    transition: color 140ms var(--ease-out), border-color 140ms var(--ease-out);
  }

  .onglets__lien:hover {
    color: var(--chalk);
  }

  .onglets__lien--actif {
    color: var(--chalk);
    border-block-end-color: var(--red);
  }

  /* ---- selecteur de langue ---- */

  .langues {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    margin-block-start: 0.75rem;
    font-size: 0.8rem;
  }

  .langues__courante {
    color: var(--smoke);
  }

  .langues__choix {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: var(--smoke-dim);
    text-decoration: underline;
    text-underline-offset: 0.2em;
    cursor: pointer;
  }

  .langues__choix:hover {
    color: var(--chalk);
  }

  /* ---- choix d'allure ---- */

  /*
    Même grammaire que le choix de langue, juste en dessous : des mots, pas des
    boutons. Le choix en cours reste un bouton — le script du site déplace la
    marque d'un choix à l'autre sans recharger —, il se distingue donc par sa
    marque « choix en cours » et non par sa nature.
  */
  .affichage {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.4rem 1.2rem;
    margin-block-start: 0.5rem;
    font-size: 0.8rem;
  }

  .affichage__groupe {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
  }

  /* L'éclairage ne concerne que Панелька : l'ancienne allure n'en a qu'un. */
  .affichage__groupe--theme {
    display: none;
  }

  .affichage__titre {
    color: var(--smoke-dim);
  }

  .affichage__choix {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: var(--smoke-dim);
    text-decoration: underline;
    text-underline-offset: 0.2em;
    cursor: pointer;
  }

  .affichage__choix:hover {
    color: var(--chalk);
  }

  .affichage__choix[aria-current="true"] {
    color: var(--smoke);
    text-decoration: none;
    cursor: default;
  }

  /* Dans le profil : une carte par choix, le nom puis ce qu'il donne. */
  .affichage--complet {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
    gap: 0.75rem;
    margin: 0;
    font-size: var(--step-0);
  }

  .affichage__aide {
    margin-block-end: 1rem;
  }

  .affichage__carte {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3rem;
    padding: 0.8rem 0.9rem;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    background: var(--surface-raised);
    color: var(--chalk);
    font: inherit;
    text-align: start;
    cursor: pointer;
    transition: border-color 140ms var(--ease-out), background 140ms var(--ease-out);
  }

  .affichage__carte:hover {
    border-color: var(--chalk);
  }

  .affichage__carte[aria-current="true"] {
    border-color: var(--red);
    background: color-mix(in oklab, var(--red) 12%, var(--surface-raised));
  }

  .affichage__nom {
    font-family: var(--font-display);
    font-size: var(--step-1);
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }

  .affichage__description {
    color: var(--smoke);
    font-size: var(--step--1);
  }

  .journal {
    display: flex;
    flex-direction: column;
    gap: var(--gap);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .journal__item {
    padding-block-start: var(--gap);
    border-block-start: 1px solid var(--line);
  }

  .journal__item:first-child {
    padding-block-start: 0;
    border-block-start: 0;
  }

  .journal__entete {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    margin-block-end: 0.4rem;
    font-size: 0.85rem;
    color: var(--smoke-dim);
  }

  .journal__titre {
    margin: 0 0 0.5rem;
    font-size: 1.05rem;
    line-height: 1.3;
  }

  /*
    Les entrees comportent des paragraphes separes par des sauts de ligne. La
    politique de securite du contenu interdit tout style en ligne : la mise en
    forme du texte doit donc venir d'ici, jamais d'un attribut style=.
  */
  .journal__texte {
    margin: 0;
    white-space: pre-wrap;
    color: var(--smoke);
    max-width: 68ch;
  }

  .journal__technique {
    margin-block-start: 0.75rem;
    padding-inline-start: 0.75rem;
    border-inline-start: 2px solid var(--line);
  }

  .journal__technique > summary {
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: 0.8rem;
    letter-spacing: 0.04em;
    color: var(--ice-dim);
  }

  .journal__technique > summary:hover {
    color: var(--ice);
  }

  .journal__technique .journal__texte {
    margin-block-start: 0.5rem;
    font-size: 0.9rem;
    color: var(--smoke-dim);
  }

  .journal__fichiers-titre {
    margin-block: 0.75rem 0.25rem;
    font-size: 0.8rem;
    color: var(--smoke-dim);
  }

  .journal__fichiers {
    margin: 0;
    padding-inline-start: 1.1rem;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    line-height: 1.6;
    color: var(--smoke-dim);
    overflow-wrap: anywhere;
  }

  /*
    L'edition d'une entree se rejoint par un lien : elle a sa propre adresse,
    donc elle se partage et le bouton precedent y ramene. C'est aussi ce qui
    garde la liste legere — le formulaire n'existe que sur la page ou on
    l'utilise.
  */
  /*
    Liste des favoris.

    Alignement sur le centre et non sur la ligne de base : une pochette n'a pas
    de ligne de base, et l'aligner sur celle du texte la fait flotter au-dessus
    de la ligne.
  */
  .favoris {
    display: flex;
    flex-direction: column;
  }

  .favori {
    display: flex;
    align-items: center;
    gap: 0.8rem;
    padding-block: 0.6rem;
    border-block-end: 1px solid color-mix(in oklab, var(--line) 60%, transparent);
  }

  /* Le dernier trait doublerait le bord du panneau. */
  .favori:last-child {
    border-block-end: none;
  }

  /*
    Vignette commune aux favoris, a l'historique et au classement.

    Les deux dimensions sont posees ici et nulle part ailleurs : cela suffit a
    reserver la place avant que l'image n'arrive, donc la ligne ne saute pas.
    Les repeter en attribut dans le balisage n'ajouterait qu'un endroit ou la
    valeur peut diverger.

    La taille se regle par le contexte, pas par un attribut : la CSP interdit
    tout style en ligne.
  */
  .vignette {
    flex: 0 0 auto;
    inline-size: var(--vignette, 40px);
    block-size: var(--vignette, 40px);
    border-radius: 3px;
    object-fit: cover;
    background: var(--ink-800);
  }

  .vignette--vide {
    display: inline-block;
    border: 1px solid var(--line);
  }

  .favori {
    --vignette: 48px;
  }

  /*
    Une liste qui porte des pochettes s'aligne sur le centre et non sur la ligne
    de base du texte : une image n'a pas de ligne de base, et s'y aligner la
    fait flotter au-dessus de la ligne.
  */
  .playlist--pochettes .playlist__row {
    align-items: center;
  }

  /*
    Titre au-dessus de l'artiste : c'est le titre qu'on cherche dans une liste
    de favoris. « min-width: 0 » autorise la troncature — sans lui, un long
    titre pousserait la date et le bouton hors de la ligne.
  */
  .favori__morceau {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    min-width: 0;
    line-height: 1.3;
  }

  .favori__morceau > strong {
    overflow-wrap: anywhere;
  }

  /*
    Troisieme ligne, sur les favoris d'amis seulement : qui a aime le morceau.
    Plus petite que l'artiste, pour qu'on lise le morceau d'abord — c'est lui
    qu'on cherche dans une liste de favoris, pas le nom du membre.
  */
  .favori__par {
    color: var(--smoke-dim);
    font-size: var(--step--1);
  }

  /*
    La marge automatique pousse la date ET le bouton au bout, ensemble : ils
    forment le coin des actions, quel que soit la longueur du titre.
  */
  .favori__date {
    margin-inline-start: auto;
    flex: 0 0 auto;
    color: var(--smoke-dim);
    font-size: var(--step--1);
    font-variant-numeric: tabular-nums;
  }

  .favori__retrait {
    flex: 0 0 auto;
  }

  /*
    Page de direct : la scene et le chat cote a cote.

    Le seuil est en « rem » et non en pixels : il suit la taille de police du
    visiteur, de sorte qu'un reglage a 150 % bascule en une colonne au lieu
    d'ecraser deux colonnes illisibles.
  */
  .direct {
    display: grid;
    gap: var(--gap);
    grid-template-columns: minmax(0, 1fr);
  }

  /*
    Les deux colonnes du direct espacent leurs panneaux de la meme facon.

    La colonne de droite ne portait aucune mise en page : tant qu'elle n'a
    contenu que le chat, cela ne se voyait pas. Le blind test s'est ajoute
    au-dessus et s'est retrouve colle a lui, sans le moindre pixel entre les
    deux — alors que la colonne de gauche separait deja les siens.
  */
  .direct__scene,
  .direct__chat {
    display: flex;
    flex-direction: column;
    gap: var(--gap);
    min-width: 0;
  }

  @media (min-width: 64rem) {
    .direct {
      grid-template-columns: minmax(0, 1fr) minmax(20rem, 26rem);
      align-items: start;
    }

    /*
      Le chat suit le defilement : sur une page longue, on garde la
      conversation sous les yeux sans la chercher. Le decalage laisse passer
      l'en-tete colle en haut.
    */
    .direct__chat {
      position: sticky;
      top: calc(var(--dock-height) * 0.5);
    }

    .direct__chat .chat__fil {
      block-size: 30rem;
    }
  }

  /*
    Fil du chat.

    Hauteur bornée et defilement propre : le fil vit sous le reste de la page,
    il ne doit pas la pousser indefiniment vers le bas a mesure que la
    conversation avance.
  */
  /*
    Hauteur FIXE et non plafonnee : avec un simple maximum, la boite grandissait
    a chaque message jusqu'a l'atteindre, et tout ce qui suit sur la page
    descendait d'autant. On ne pousse pas la mise en page d'un visiteur parce
    qu'un autre a parle.
  */
  .chat__fil {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    block-size: 26rem;
    overflow-y: auto;
    padding-inline-end: 0.4rem;
    margin-block-end: 1rem;

    /*
      Ascenseur discret. Les regles WebKit globales du site ne s'appliquent plus
      des que « scrollbar-width » est present : Chrome retient alors la propriete
      standard et ignore les pseudo-elements. On declare donc les deux, la
      standard d'abord.
    */
    scrollbar-width: thin;
    scrollbar-color: color-mix(in oklab, var(--ink-600) 85%, transparent) transparent;
  }

  .chat__fil::-webkit-scrollbar {
    inline-size: 6px;
  }

  .chat__fil::-webkit-scrollbar-track {
    background: transparent;
  }

  .chat__fil::-webkit-scrollbar-thumb {
    background: color-mix(in oklab, var(--ink-600) 85%, transparent);
    border-radius: 999px;
  }

  /* Les deux fleches, que l'ascenseur historique redessine. */
  .chat__fil::-webkit-scrollbar-button {
    display: none;
    block-size: 0;
    inline-size: 0;
  }

  /*
    L'archive n'est pas le direct : elle se lit d'une traite, et rien ne s'y
    ajoute sous les yeux. Elle reprend donc un simple plafond.
  */
  .chat__fil--archive {
    block-size: auto;
    max-block-size: 40rem;
  }

  /*
    Arrivee d'un message.

    Celle-ci part bien de l'invisible, a la difference des entrees de page — et
    la nuance a son importance. Une page qui se devoile en fondu se lit comme un
    chargement ; une ligne qui se pose dans un fil deja la se lit comme ce
    qu'elle est, une arrivee. Elle glisse depuis la gauche, dans le sens de la
    lecture.

    Reservee au direct : dans l'archive, cinquante lignes qui glissent a chaque
    page seraient un tic, pas une information.
  */
  @keyframes chat-entrant {
    from {
      opacity: 0;
      translate: -0.6rem 0;
    }
  }

  .chat__fil:not(.chat__fil--archive) .chat__message {
    animation: chat-entrant 220ms var(--ease-out) both;
  }

  /*
    Un message coule d'un trait, la vignette posee en marge.

    Il etait en flex, le texte en « flex: 1 1 12rem ». Or le fil ne fait que
    363 pixels : l'heure, le pseudo et la vignette en mangent cent soixante, il
    en reste moins que la base demandee, et le texte passait donc a la ligne
    presque a chaque message. Il y retombait cale a gauche, sous la vignette,
    sans plus rien qui le rattache a son auteur — c'est ce qu'on voyait.

    En flux normal, il suit le pseudo sur la meme ligne quand il tient, et se
    replie sous l'heure quand il deborde. La vignette sort du flux et le retrait
    lui reserve sa place : le repli se fait donc en retrait pendant, jamais sous
    l'image.
  */
  .chat__message {
    position: relative;
    padding-inline-start: calc(28px + 0.5rem);
    min-block-size: 28px;
    font-size: var(--step--1);
    line-height: 1.45;
  }

  /*
    Legerement descendue : calee en haut, elle flotte au-dessus de la premiere
    ligne de texte, dont la hauteur de ligne depasse la casse.
  */
  .chat__avatar {
    position: absolute;
    inset-inline-start: 0;
    inset-block-start: 0.1rem;
  }

  /*
    L'espace vient d'une marge et non plus d'un « gap » : entre elements en
    flux, il n'y a pas de gap. Elle est portee par ce qui precede, de sorte
    qu'un texte replie ne herite d'aucun decalage.
  */
  .chat__heure {
    margin-inline-end: 0.4rem;
    color: var(--smoke-dim);
    font-variant-numeric: tabular-nums;
  }

  .chat__auteur {
    margin-inline-end: 0.4rem;
    font-weight: 600;
    color: var(--ice);
  }

  /*
    Se coupe ou il faut : un mot de trois cents caracteres ne doit pas elargir
    la page.
  */
  .chat__texte {
    overflow-wrap: anywhere;
  }

  /*
    Corps du chat : hauteur posee des le depart.

    Sans elle, le panneau naissait de la taille d'une phrase puis se depliait a
    chaque message, poussant la page a chaque arrivee.
  */
  .chat__corps {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    min-block-size: 22rem;
    margin-block-end: 1rem;
  }

  /*
    Le pseudo est cliquable pour la moderation, mais discretement : le
    souligne pointille d'origine posait un chapelet de points sous chaque
    pseudo, plus salissant qu'informatif. La couleur suffit a dire que c'est
    actionnable, et le curseur le confirme.
  */
  .chat__auteur--cliquable {
    padding: 0;
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    font-weight: 600;
    cursor: pointer;
    transition: color 120ms var(--ease-out);
  }

  .chat__auteur--cliquable:hover,
  .chat__auteur--cliquable:focus-visible {
    color: var(--chalk);
  }

  /*
    Pastille de moderation, dans la couche superieure.

    Le fil defile, donc il decoupe tout enfant en position absolue : aucun
    « z-index » n'en sort. L'attribut « popover » place le panneau hors du flux
    et hors de tout decoupage, et le navigateur se charge de la fermeture par
    Echap comme par clic exterieur.
  */
  .chat__pastille[popover] {
    inline-size: min(20rem, calc(100vw - 2rem));
    padding: 0.5rem;
    background: var(--ink-850);
    color: var(--chalk);
    border: 1px solid var(--line-strong);
    border-radius: var(--radius);
    box-shadow: 0 12px 32px oklch(0% 0 0 / 0.55);
  }

  .chat__pastille[popover]:popover-open {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
  }

  .chat__pastille[popover]::backdrop {
    background: oklch(0% 0 0 / 0.35);
  }

  /*
    Curseur de peine. « accent-color » suffit a l'accorder au site : styler un
    « range » de bout en bout demande un jeu de pseudo-elements par moteur, pour
    un gain nul ici.
  */
  .chat__curseur {
    inline-size: 100%;
    margin-block: 0.5rem 0.2rem;
    accent-color: var(--red);
    cursor: pointer;
  }

  /*
    Une phrase, pas un chiffre. Les notes empruntaient le style du compteur —
    police d'affichage, centrage, interlettrage large — ce qui donnait a une
    simple explication l'allure d'un titre.
  */
  .chat__note {
    margin-block: 0.2rem 0.5rem;
    color: var(--smoke);
    font-size: var(--step--1);
    line-height: 1.45;
  }

  /*
    Le compteur de duree : un chiffre qu'on lit d'un coup d'oeil pendant qu'on
    fait glisser le curseur. D'ou la police d'affichage et le centrage.
  */
  .chat__peine {
    margin-block-end: 0.5rem;
    color: var(--chalk);
    font-family: var(--font-display);
    font-size: var(--step-0);
    letter-spacing: 0.08em;
    text-align: center;
  }

  /*
    Le titre porte le pseudo et le grade. Sans « flex » ni ecart, l'etiquette du
    grade venait toucher le texte et debordait de la ligne des qu'un pseudo
    s'allongeait.
  */
  .chat__pastille-titre {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-block-end: 0.5rem;
    padding-block-end: 0.5rem;
    border-block-end: 1px solid var(--line);
    color: var(--smoke-dim);
    font-size: var(--step--1);
  }

  .chat__action {
    padding: 0.35rem 0.5rem;
    background: none;
    border: 0;
    border-radius: 2px;
    color: var(--chalk);
    font: inherit;
    font-size: var(--step--1);
    text-align: start;
    cursor: pointer;
    transition: background 120ms var(--ease-out);
  }

  .chat__action:hover,
  .chat__action:focus-visible {
    background: var(--ink-700);
  }

  .chat__action:disabled {
    color: var(--smoke-dim);
    cursor: default;
  }

  /* Desactiver un compte ne se defait pas d'un clic : la couleur le dit. */
  .chat__action--grave {
    color: var(--red-hot);
  }

  /* Un message retire de la vue : la mention se lit comme une absence. */
  .chat__texte--masque {
    color: var(--smoke-dim);
    font-style: italic;
  }

  .tag--masque {
    color: var(--red-hot);
    border-color: var(--red-dim);
  }

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

  .chat__erreur {
    margin-block-start: 0.6rem;
    color: var(--red-hot);
    font-size: var(--step--1);
  }

  .pagination {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    align-items: center;
    justify-content: center;
    margin-block-start: 1.25rem;
    padding-block-start: 1rem;
    border-block-start: 1px solid var(--line);
  }

  .journal__lien-edition {
    display: inline-block;
    margin-block-start: 0.75rem;
    font-size: 0.85rem;
    color: var(--smoke-dim);
    text-decoration-color: var(--line-strong);
    transition: color 140ms var(--ease-out);
  }

  .journal__lien-edition:hover,
  .journal__lien-edition:focus-visible {
    color: var(--ice);
  }

  .journal__formulaire {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-block: 0.75rem;
    max-width: 60ch;
  }
}

/* -------------------------------------------------------------- utilities */
@layer utilities {
  .muted {
    color: var(--smoke-dim);
  }

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

  .skip-link {
    position: absolute;
    inset-inline-start: -100vw;
    top: 0.5rem;
    z-index: 100;
    padding: 0.6rem 1rem;
    background: var(--red);
    color: var(--chalk);
  }

  .skip-link:focus-visible {
    inset-inline-start: 0.5rem;
  }
}

/*
  Respecte le réglage système : plus aucune animation ni transition.

  Les retards aussi. Abrégé sans eux, un mouvement attendait encore son tour
  dans son état de départ : un panneau restait décalé et pâli jusqu'à un
  cinquième de seconde, une fenêtre du bloc s'allumait près d'une seconde
  après sa voisine — un mouvement, justement.
*/
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
}

/*
  Photos de profil. La vignette est toujours carree et rognee par le serveur :
  object-fit ne fait que garantir le rendu si une image ancienne, produite avant
  cette regle, ne l'etait pas.
*/
.avatar {
  border-radius: 50%;
  object-fit: cover;
  background: var(--surface-raised);
  flex: none;
}

.avatar--grand {
  width: 6rem;
  height: 6rem;
}

.avatar--menu {
  width: 1.75rem;
  height: 1.75rem;
}

.avatar-edition {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1rem;
  margin-top: 1rem;
}

.avatar-edition__formulaire {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 14rem;
  flex: 1 1 14rem;
}

.avatar-edition__boutons {
  display: flex;
  gap: 0.5rem;
}

/*
  Choix de la couleur du pseudo.

  Le selecteur natif porte la pipette : le navigateur en sait plus long que
  nous sur la fenetre de choix, et il l'ouvre deja au bon endroit sur chaque
  systeme. Reste a lui donner une taille tenable — sans regle il s'affiche en
  petit rectangle gris de 3 centimetres, different d'un moteur a l'autre.
*/
.couleur {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 1rem;
  margin-top: 1rem;
}

.couleur > .field {
  margin-block-end: 0;
}

.couleur input[type="color"] {
  inline-size: 4rem;
  block-size: 2.4rem;
  padding: 0.15rem;
  cursor: pointer;
}

/*
  L'apercu montre le pseudo lui-meme, pas une pastille : ce qu'on regle est la
  lisibilite d'un nom sur ce fond-la, et une pastille de couleur n'en dit rien.
*/
.couleur__apercu {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  margin: 0;
  font-size: 1.05rem;
}

/* Pseudo precede de sa vignette, dans les tableaux et les listes. */
.membre {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

/*
  La ligne d'un message est un conteneur flex aligne sur la ligne de base. Une
  image y pose son bas sur cette ligne, et parait donc flotter au-dessus du
  texte : il lui faut son propre alignement. « vertical-align » et une marge
  droite y trainaient, tous deux sans effet ici — l'un ne vaut que pour un flux
  en ligne, l'autre doublait l'ecart deja pose par le conteneur.
*/
/* Liste d'amis et de demandes. */
/*
  Les colonnes sont portees par la liste, pas par la ligne.

  Une marge automatique sur la date la collait au bouton : sa position dependait
  alors de sa propre longueur, et « le 1 septembre » ne commencait pas ou
  commencait « le 30 aout ». Une grille par ligne ne repare rien — chaque ligne
  dimensionnerait ses colonnes pour elle seule, et le desalignement resterait.
  C'est la liste qui doit tenir les colonnes ; les lignes s'y raccrochent par
  « subgrid ».

  « minmax(0, 1fr) » sur la premiere colonne : sans le zero, un pseudo tres long
  refuserait de retrecir et pousserait la date hors du panneau.
*/
.amis {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: 0.5rem 0.75rem;
}

.amis__ligne {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
}

/*
  En dessous, la date et le bouton ne tiennent plus a cote du pseudo : le pseudo
  prend la ligne entiere, et les deux autres passent dessous.
*/
@media (max-width: 40rem) {
  .amis {
    grid-template-columns: minmax(0, 1fr) auto;

    /*
      Une fois la ligne repliee sur deux rangs, l'ecart entre amis doit depasser
      celui qui separe le pseudo de sa date : a ecart egal, on ne sait plus
      quelle date appartient a qui.
    */
    row-gap: 1rem;
  }

  .amis__ligne {
    row-gap: 0.15rem;
  }

  .amis__ligne .membre {
    grid-column: 1 / -1;
  }
}

.panel__soustitre {
  margin: 1rem 0 0;
  font-size: 0.95rem;
}

/*
  Pastille de presence. La couleur ne porte pas seule le sens : chaque pastille
  est doublee d'un texte pour lecteur d'ecran, et d'un titre au survol.
*/
.presence {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  display: inline-block;
  flex: none;
}

.presence--en-ligne {
  background: var(--ice);
  box-shadow: 0 0 0 0.2rem color-mix(in oklab, var(--ice) 25%, transparent);
}

.presence--absent {
  background: var(--ink-700);
}

/*
  Tableau des membres. Le probleme n'etait pas la couleur mais le groupement :
  cinq boutons de nature differente se suivaient dans une seule cellule, et le
  rouge pleine largeur ecrasait tout le reste.
*/
.table th {
  /* « Inscrit le » se coupait en deux lignes et decalait tout l'en-tete. */
  white-space: nowrap;
}

.roles {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
}

/*
  La pastille de role est la commande elle-meme, allumee ou eteinte. Elle a
  l'aspect d'une etiquette et non d'un bouton, parce qu'elle designe un etat
  avant de designer une action.
*/
.pastille-role {
  font: inherit;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--smoke-dim);
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.pastille-role:hover,
.pastille-role:focus-visible {
  color: var(--smoke);
  border-color: var(--line-strong);
}

.pastille-role--porte {
  border-color: var(--ice-dim);
  color: var(--ice);
}

.etat {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  white-space: nowrap;
}

.etat__point {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  flex: none;
}

.etat__point--actif {
  background: var(--ice-dim);
}

.etat__point--ferme {
  background: var(--red);
}

/*
  Des etats independants, empiles. Le div porte la mise en page, jamais la
  cellule : rendre un <td> flex le retire de la mise en page du tableau, et sa
  bordure basse remonte de quelques pixels par rapport a ses voisines.
*/
/* Des champs empiles dans un panneau, sans la grille d'un formulaire complet. */
/*
  Pile de champs d'un panneau.

  Sans « align-items », donc etires : c'est la convention du reste du site, ou
  un champ occupe la largeur de son formulaire. Cette pile portait
  « align-items: flex-start », qui faisait retrecir chaque champ sur son
  contenu — et le contenu le plus large etait le LIBELLE. Mesure sur le
  changement de mot de passe : trois champs de 210 pixels dans un formulaire de
  557, tailles par « Confirme le mot de passe » au pixel pres.

  Deux defauts pour le prix d'un. Trois boites etroites flottant dans un
  panneau large se lisaient comme un desalignement, alors qu'elles etaient
  rigoureusement egales. Et la largeur dependait d'une chaine traduite :
  basculer la page en anglais aurait change la taille des champs sans que rien
  ne l'explique.
*/
.pile-de-champs {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

/*
  L'ecart vient de la pile seule. « .field » porte une marge basse pour les
  formulaires ordinaires ; ici elle s'ajoutait au « gap », et les deux
  mecanismes cumules aeraient le panneau bien au-dela du necessaire.
*/
.pile-de-champs > .field {
  margin-block-end: 0;
}

/*
  Le bouton garde sa taille propre : c'est lui, et lui seul, que
  « align-items: flex-start » protegeait. Le dire sur le bouton plutot que sur
  la pile evite d'en faire payer le prix aux champs.
*/
.pile-de-champs > .btn {
  align-self: flex-start;
  margin-block-start: 0.3rem;
}

/*
  La conversation avec le routeur, telle qu'elle s'est deroulee. Un « details »
  natif porte le pliage : la politique de securite du contenu interdit le script
  en ligne, et le navigateur sait deja faire cela seul.
*/
/*
  Les presents, a la suite du compte d'auditeurs. Une pile de visages plutot
  qu'une enumeration : elle se lit d'un coup d'oeil et ne s'allonge pas quand le
  monde arrive.

  L'ecart entre les morceaux de la ligne vient du conteneur, jamais du balisage :
  voir « .dock__listeners ».
*/
.dock__presents {
  display: inline-flex;
  align-items: center;

  /*
    Un filet plutot qu'un mot. « dont » expliquait le lien entre le compte et
    les visages, mais des visages n'ont pas besoin d'etre annonces : le trait
    dit la meme chose sans prendre la place d'un mot dans un bandeau qui n'en a
    pas.
  */
  padding-inline-start: 0.55rem;
  border-inline-start: 1px solid var(--line);
  margin-inline-start: 0.2rem;
}

.dock__present {
  display: block;
  border-radius: 50%;
  line-height: 0;

  /*
    Le chevauchement fait la pile. La bordure prend la couleur du fond du
    bandeau : sans elle, les tetes se fondent les unes dans les autres et
    l'ensemble n'est plus qu'une tache.
  */
  margin-inline-start: -0.5rem;
  border: 2px solid var(--ink-950);
  transition: translate 120ms ease;
}

.dock__present:first-child {
  margin-inline-start: 0;
}

/*
  Le survol sort la tete de la pile. « position: relative » n'est pas
  decoratif : sans lui, l'element suivant recouvre celui qu'on souleve, et le
  geste ne montre rien.
*/
.dock__present:hover,
.dock__present:focus-visible {
  position: relative;
  z-index: 1;
  translate: 0 -2px;
}

.dock__visage {
  width: 1.5rem;
  height: 1.5rem;
  display: block;
}

/*
  Retiree sur les petits ecrans. Mesure sur un telephone de 375 px : la pile
  prend 104 px dans un bandeau qui n'en a pas, et le titre du morceau tombait a
  « XS ... ». Savoir qui est la ne vaut pas de ne plus savoir ce qui passe.
*/
@media (max-width: 40rem) {
  .dock__presents {
    display: none;
  }
}

.dock__present--reste {
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  background: var(--surface-raised);
  color: var(--smoke-dim);
  font-size: 0.68rem;

  /* Le nombre ne doit pas faire respirer la pastille en changeant. */
  font-variant-numeric: tabular-nums;
}

/* ---- recherche dans une liste ---- */
.recherche {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-block-end: 1rem;
}

.recherche__champ {
  flex: 1 1 14rem;
  min-width: 0;
}

/* ---- parrainage ---- */

/*
  Un code se recopie a la main. Chasse fixe et lettres espacees : c'est ce qui
  permet de lire « MNPQ » sans compter les jambages, et de ne pas confondre une
  suite de caracteres avec un mot.
*/
.codes__valeur {
  font-family: ui-monospace, "Cascadia Code", "Fira Code", monospace;
  font-size: var(--step--1);
  letter-spacing: 0.12em;
  color: var(--ice);
  background: var(--ink-950);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.15rem 0.45rem;
  white-space: nowrap;
}

/*
  Un lien complet, et non un code de seize caracteres : il ne tient pas sur une
  ligne et ne doit pas pousser la page en largeur. La coupure est autorisee
  n'importe ou, une adresse n'offrant pas de point de coupure naturel une fois
  le jeton atteint.
*/
.codes__valeur--long {
  display: block;
  letter-spacing: normal;
  padding: 0.5rem 0.6rem;
  margin-block-start: 0.5rem;

  /*
    Les deux, et il en faut deux : « .codes__valeur » interdit le retour a la
    ligne — c'est ce qu'il faut pour un code de seize caracteres — et
    « overflow-wrap » seul n'a alors rien a couper. Mesure : sans la premiere
    ligne, l'adresse mesurait 4611 pixels dans un panneau de 1163, et la page
    entiere debordait en largeur.

    Et cette regle vient APRES celle qu'elle corrige : a specificite egale —
    une classe de chaque cote — c'est l'ordre de declaration qui tranche.
    Posee avant, elle etait ecrasee et ne servait a rien.
  */
  white-space: normal;
  overflow-wrap: anywhere;
}

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

.codes__ligne {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  font-size: var(--step--1);
}

/*
  Un code consomme ou perime reste lisible mais s'efface : il raconte une
  histoire, il n'appelle plus un geste. Sans cette nuance, une liste de vingt
  codes dont deux sont utilisables se lit comme vingt codes utilisables.
*/
.codes__ligne--eteinte .codes__valeur {
  color: var(--smoke-dim);
  text-decoration: line-through;
}

.tag--disponible {
  border-color: var(--line-strong);
  color: var(--ice);
}

.tag--utilise {
  border-color: var(--line);
  color: var(--smoke-dim);
}

.tag--expire {
  border-color: var(--red-dim);
  color: var(--red-hot);
}

/*
  Le champ du code, replie sur un site ouvert. Un « details » natif : la
  politique de securite interdit le script en ligne, le navigateur sait deja
  faire cela, et le champ reste atteignable sans JavaScript.
*/
.repli {
  margin-block-end: 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.6rem 0.9rem;
}

.repli__resume {
  cursor: pointer;
  font-size: var(--step--1);
  color: var(--smoke-dim);
  list-style: none;
}

/*
  Le marqueur est refait a la main : le triangle par defaut differe d'un
  navigateur a l'autre, et Safari le rend sur une ligne a part.
*/
.repli__resume::before {
  content: "+";
  display: inline-block;
  width: 1rem;
  color: var(--red-hot);
  font-weight: 700;
}

/*
  Le signe moins en toutes lettres, et non son echappement CSS. Ecrit
  « 2 » depuis un script Python, «  » se lisait comme un code
  octal : la feuille recevait un caractere de controle suivi d'un « 2 », et
  le marqueur affichait « 02 ». La feuille est en UTF-8, le caractere s'y
  ecrit tel quel.
*/
.repli[open] .repli__resume::before {
  content: "−";
}

.repli__resume::-webkit-details-marker {
  display: none;
}

.repli[open] .repli__resume {
  margin-block-end: 0.8rem;
  color: var(--smoke);
}

.repli .field {
  margin-block-end: 0;
}

/* Un sous-titre dans un panneau, sous la barre du titre principal. */
.panel__sous-titre {
  margin-block: 1.6rem 0.4rem;
  font-family: var(--font-display);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--smoke-dim);
}

/* ---- gabarit d'attente ---- */

/*
  Accompagne le rendu en flux : la page part tout de suite, ces barres tiennent
  la place, le contenu les remplace.

  Leur hauteur n'est pas decorative. Sans elles, la section naitrait vide puis
  se deplierait d'un coup en poussant tout ce qui suit — la secousse qu'on a
  supprimee sur la pagination reviendrait par la porte du chargement.
*/
.squelette {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding-block: 0.4rem;
}

.squelette__ligne {
  height: 1.15rem;
  border-radius: 4px;

  /*
    Le degrade fait trois fois la largeur de la barre et se deplace : c'est ce
    qui donne le reflet. Un fond uni qui clignote se lit comme une panne, un
    reflet qui balaie se lit comme un travail en cours.
  */
  background: linear-gradient(
    90deg,
    var(--surface-raised) 0%,
    color-mix(in oklab, var(--surface-raised) 60%, var(--smoke-dim)) 50%,
    var(--surface-raised) 100%
  );
  background-size: 300% 100%;
  animation: squelette-reflet 1.4s var(--ease-out) infinite;
}

/*
  Barre haute comme une rangee de tableau.

  Mesure : une rangee du tableau des membres fait 78 pixels — un visage, deux
  lignes de texte, une pile d'etats, un bouton. Une barre ordinaire en fait 30.
  Vingt rangees, et le panneau passait de 600 a 1553 pixels a l'arrivee des
  donnees : le gabarit annoncait le tiers de ce qu'il remplacait.

  4rem de barre plus 0.75rem d'ecart donnent 76 pixels. Approcher vaut mieux
  qu'egaler : la hauteur reelle depend des donnees — un pseudo qui passe a la
  ligne, deux roles au lieu d'un — et aucune valeur figee ne la suivra.
*/
.squelette__ligne--rangee {
  height: 4rem;
}

/*
  Largeurs inegales, et decroissantes. Des barres toutes identiques se lisent
  comme un motif ; inegales, elles se lisent comme du texte pas encore arrive.
  La derniere est la plus courte, comme l'est une derniere ligne de paragraphe.
*/
.squelette__ligne:nth-child(3n + 2) {
  width: 92%;
}

.squelette__ligne:nth-child(3n) {
  width: 78%;
}

.squelette__ligne:last-child {
  width: 60%;
}

@keyframes squelette-reflet {
  from {
    background-position: 150% 0;
  }

  to {
    background-position: -150% 0;
  }
}

/* ---- filtre d'etat de l'archive ---- */

/*
  Des liens, pas des boutons : ils changent l'adresse. Ils en ont donc l'aspect
  d'un jeu de choix, mais gardent le comportement d'un lien — ouvrir dans un
  onglet, mettre en favori, revenir en arriere.
*/
.filtres {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-block-end: 1rem;
}

.filtres__choix {
  padding: 0.25rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--smoke-dim);
  font-size: var(--step--1);
  text-decoration: none;
  transition:
    color 140ms var(--ease-out),
    border-color 140ms var(--ease-out),
    background-color 140ms var(--ease-out);
}

.filtres__choix:hover,
.filtres__choix:focus-visible {
  color: var(--ice);
  border-color: var(--line-strong);
}

/*
  Le choix actif est plein, non simplement colore. Une simple nuance de gris ne
  se distingue pas sur un ecran mal regle, et l'on ne sait plus quel filtre on
  regarde — la liste parait alors incomplete sans raison.
*/
.filtres__choix--actif {
  background: var(--surface-raised);
  border-color: var(--line-strong);
  color: var(--ice);
  font-weight: 600;
}

/*
  Effacement du chat, remonte en tete de l'onglet.

  Le lisere rouge fait tout le travail : place au-dessus de l'archive, ce
  panneau est la premiere chose qu'on voit, et rien ne doit laisser croire
  qu'il s'agit d'une commande ordinaire.
*/
.panel--grave {
  border-color: var(--red-dim);
}

.panel--grave .panel__title {
  color: var(--red-hot);
}

.purge {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}

/*
  Le contenu remplace apparait en fondu. Un remplacement instantane se lit comme
  un accroc — l'oeil voit que quelque chose a bouge sans savoir quoi. Deux
  dixiemes de seconde suffisent a dire « ceci vient de changer ».

  Le bloc « mouvement reduit » de fin de feuille le neutralise, comme le reste.
*/
@keyframes changement-de-contenu {
  from {
    opacity: 0.3;
  }
}

.vient-de-changer {
  animation: changement-de-contenu 200ms var(--ease-out);
}

/* ---- fenetre de moderation ---- */

/*
  « dialog » natif plutot qu'un panneau maison : le navigateur le porte dans sa
  couche superieure, rend le reste inerte, piege le focus et ferme sur Echap.
  Autant de choses qu'il faudrait sinon reecrire, moins bien.
*/
.fenetre {
  /*
    Le centrage est retabli a la main. Le navigateur l'applique d'ordinaire aux
    fenetres modales — « inset: 0; margin: auto; height: fit-content » — mais la
    reinitialisation globale de la feuille remet toutes les marges a zero, et la
    fenetre se collait alors en haut a gauche, par-dessus la barre du site.
  */
  inset: 0;
  margin: auto;
  height: fit-content;

  /* Une liste d'actions ne doit pas deborder l'ecran sur un portable couche. */
  max-height: calc(100dvh - 3rem);
  overflow: auto;

  width: min(34rem, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--smoke);
  box-shadow: var(--shadow-panel);

  /*
    L'entree et la sortie s'animent toutes les deux. « allow-discrete » est ce
    qui rend la sortie possible : sans lui, « display: none » s'applique
    immediatement et la fenetre disparait d'un coup, sans transition — on ne
    voit que l'ouverture.
  */
  opacity: 0;
  translate: 0 10px;
  transition:
    opacity 200ms var(--ease-out),
    translate 200ms var(--ease-out),
    overlay 200ms allow-discrete,
    display 200ms allow-discrete;
}

.fenetre[open] {
  opacity: 1;
  translate: 0 0;
}

/* L'etat de depart de l'entree. Sans lui, la fenetre apparait deja en place. */
@starting-style {
  .fenetre[open] {
    opacity: 0;
    translate: 0 10px;
  }
}

.fenetre::backdrop {
  background: color-mix(in oklab, var(--ink-950) 72%, transparent);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition:
    opacity 200ms var(--ease-out),
    overlay 200ms allow-discrete,
    display 200ms allow-discrete;
}

.fenetre[open]::backdrop {
  opacity: 1;
}

@starting-style {
  .fenetre[open]::backdrop {
    opacity: 0;
  }
}

.fenetre__entete {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.7rem;
  padding: 0.9rem 1.1rem;
  border-block-end: 1px solid var(--line);
}

/* Le visage, puis le nom et l'adresse, puis la fermeture. */
.fenetre__qui {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.fenetre__titre {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-0);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Une adresse longue ne doit pas pousser la croix hors de la fenetre. */
.fenetre__adresse {
  font-size: var(--step--1);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/*
  Les actions sont regroupees et nommees. En file indistincte, cinq boutons se
  valaient a l'oeil : on lisait chaque libelle pour retrouver celui qu'on
  cherchait.
*/
.fenetre__section {
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.fenetre__section-titre {
  margin: 0;
  font-family: var(--font-display);
  font-size: 0.7rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--smoke-dim);
}

/*
  Fermer un compte est mis a part par un filet. Range parmi les autres, le
  bouton se cliquait au passage.
*/
.fenetre__section--grave {
  padding-block-start: 0.9rem;
  border-block-start: 1px solid var(--line);
}

.fenetre__fermer {
  flex: none;
  border: 0;
  background: none;
  color: var(--smoke-dim);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.25rem;
  transition: color 140ms var(--ease-out);
}

.fenetre__fermer:hover,
.fenetre__fermer:focus-visible {
  color: var(--smoke);
}

/*
  Les groupes s'empilent ici, alors qu'ils s'alignaient dans la cellule : la
  fenetre a la hauteur que le tableau n'avait pas, et une action par ligne se
  vise sans hesiter.
*/
.fenetre__corps {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
  padding: 1.1rem;
}

.fenetre__corps .actions__groupe {
  flex-wrap: wrap;
}

.console {
  margin-block-start: 0.8rem;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--ink-950);
}

.console__titre {
  padding: 0.5rem 0.8rem;
  cursor: pointer;
  font-family: var(--font-display);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: var(--step--1);
  color: var(--smoke-dim);
}

/*
  « overflow-x » sur le bloc et non sur la page : une ligne de protocole depasse
  souvent la largeur, et sans cela c'est le document entier qui defilerait
  lateralement.

  Borne aussi en hauteur, et cette fois pour deux raisons. Un dialogue SMTP
  rate fait quarante lignes : il repoussait tout le reste de la page vers le
  bas. Et c'etait le seul contenu du profil capable de grandir sans limite,
  donc le seul qui rendait risque d'egaliser la hauteur des panneaux — voir
  « .grid--profile ».
*/
.console__corps {
  margin: 0;
  padding: 0 0 0.8rem;
  max-block-size: 22rem;
  overflow-y: auto;
  overflow-x: auto;
  list-style: none;
  font-family: ui-monospace, "Cascadia Code", "Fira Code", monospace;
  font-size: var(--step--1);
  line-height: 1.5;
  color: var(--smoke);
}

/*
  Le remplissage passe sur la ligne et non sur le bloc : posé sur le bloc, il
  ne suit pas le défilement horizontal, et le fond d'un refus s'arrêtait net
  au bord visible au lieu d'accompagner la ligne jusqu'à son bout.
*/
.console__ligne {
  padding-inline: 0.8rem;
  border-inline-start: 2px solid transparent;
  white-space: pre;
}

/*
  Deux voix, deux couleurs : ce que le site dit, ce que le routeur répond. Le
  préfixe « C: » ou « S: » reste à sa place — la couleur double l'information,
  elle ne la remplace pas, et une console lue à voix haute doit rester juste.
*/
.console__ligne--client {
  color: var(--ice);
}

.console__ligne--serveur {
  color: var(--smoke);
}

/*
  Le refus, seul, porte un fond. C'est la ligne qu'on cherche quand rien n'est
  parti, et elle se noyait au milieu de quarante autres du même gris.
*/
.console__ligne--refus {
  color: var(--red-hot);
  border-inline-start-color: var(--red);
  background: color-mix(in oklab, var(--red) 12%, transparent);
}

/* Ce qui n'est pas du protocole : un abandon, une absence d'acheminement. */
.console__ligne--note {
  color: var(--smoke-dim);
  font-style: italic;
}

.etat__pile {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
}

/*
  Le blind test.

  Il s'insere au-dessus du chat et ne parait que pendant une partie : hors jeu,
  il volerait la place a la conversation, qui en a besoin.
*/
/*
  Le panneau du jeu.

  Il se lit dans la colonne du chat, sous les cent-quatre-vingts secondes d'un
  morceau : tout y va au plus court. La consigne d'abord, le champ, puis les
  propositions ; le classement ferme, separe par un trait, parce qu'il se
  consulte apres coup et non pendant qu'on cherche.
*/
.blindtest__consigne {
  margin: 0;
  color: var(--smoke-dim);
  font-size: var(--step--1);
}

/*
  La revelation est le seul moment ou le panneau parle fort. Le liseré porte
  l'annonce a lui seul : un fond plein, dans une colonne deja dense, ferait un
  bloc de couleur de plus.
*/
.blindtest__annonce {
  margin: 0;
  padding: 0.6rem 0.8rem;
  border-inline-start: 3px solid var(--ice);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: color-mix(in oklab, var(--ice) 10%, transparent);
  color: var(--chalk);
  font-size: var(--step--1);
}

.blindtest__avis {
  margin: 0.5rem 0 0;
  color: var(--smoke-dim);
  font-size: var(--step--1);
}

/*
  Le vote du prochain morceau.

  Meme grammaire que le blind test — consigne, choix, annonce — parce que les
  deux se lisent au meme endroit et se succedent dans la meme colonne. Des
  styles distincts pour des panneaux jumeaux se seraient mis a diverger.
*/
.vote__consigne {
  margin: 0;
  color: var(--smoke-dim);
  font-size: var(--step--1);
}

.vote__annonce {
  margin: 0 0 0.7rem;
  padding: 0.6rem 0.8rem;
  border-inline-start: 3px solid var(--red);
  border-radius: 0 var(--radius) var(--radius) 0;
  background: color-mix(in oklab, var(--red) 12%, transparent);
  color: var(--chalk);
  font-size: var(--step--1);
}

.vote__candidats {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin: 0.8rem 0 0;
  padding: 0;
  list-style: none;
}

/*
  « min-width: 0 » sur le libelle autorise la troncature : sans lui, un titre a
  rallonge elargirait le bouton et ferait deborder la colonne.
*/
.vote__candidat {
  --vignette: 36px;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  inline-size: 100%;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--ink-850);
  color: var(--chalk);
  font: inherit;
  font-size: var(--step--1);
  text-align: start;
  cursor: pointer;

  /*
    Le soulevement est vertical, jamais horizontal. Un deplacement lateral
    ferait glisser le libelle sous la pochette voisine a chaque survol, et la
    liste entiere semblerait bouger.
  */
  transition:
    border-color 140ms var(--ease-out),
    background 140ms var(--ease-out),
    box-shadow 140ms var(--ease-out),
    translate 140ms var(--ease-out);
}

/*
  La pochette respire avec le bouton. Elle porte sa propre transition : la
  regrouper sur le parent ne l'atteindrait pas, une transition ne descendant
  pas aux enfants.
*/
.vote__candidat .vignette {
  transition: scale 140ms var(--ease-out);
}

.vote__candidat:hover:not(:disabled):not(.vote__candidat--inerte) .vignette {
  scale: 1.06;
}

/*
  Le compte de voix s'eclaire au survol : c'est lui qu'on regarde en comparant
  les propositions.
*/
.vote__voix {
  transition: color 140ms var(--ease-out);
}

.vote__libelle {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-inline-size: 0;
  flex: 1 1 auto;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

/*
  Le compte de voix ne bouge pas quand il passe de 9 a 10 : chiffres tabulaires
  et largeur reservee, sinon les trois lignes se decalent a chaque bulletin.
*/
.vote__voix {
  flex: 0 0 auto;
  min-inline-size: 1.6em;
  text-align: end;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--ice);
}

.vote__candidat:hover:not(:disabled):not(.vote__candidat--inerte),
.vote__candidat:focus-visible {
  border-color: var(--red);
  background: var(--ink-800);
  translate: 0 -2px;
  box-shadow: 0 6px 16px -10px color-mix(in oklab, var(--red) 70%, transparent);
}

/*
  L'enfoncement : le bouton revient a sa place, plus vite qu'il n'en est parti.
  Sans lui, un clic ne rend rien et l'on doute d'avoir touche la cible.
*/
.vote__candidat:active:not(:disabled):not(.vote__candidat--inerte) {
  translate: 0 0;
  transition-duration: 60ms;
}

/*
  Son propre bulletin se reconnait sans avoir a s'en souvenir : liseré plein a
  gauche plutot qu'une bordure entiere, pour que le survol garde son propre
  langage et qu'on distingue « choisi » de « survole ».
*/
.vote__candidat--choisi {
  border-color: color-mix(in oklab, var(--red) 55%, var(--line));
  background: color-mix(in oklab, var(--red) 12%, var(--ink-850));
  box-shadow: inset 3px 0 0 0 var(--red);
  animation: vote-choisi 240ms var(--ease-out);
}

.vote__candidat--choisi .vote__voix {
  color: var(--red);
  font-weight: 600;
}

/*
  Une pulsation breve au moment du choix. Elle part d'un etat legerement
  retreci plutot que de grandir : un bouton qui deborde puis revient bouscule
  ses voisins le temps de l'animation.
*/
@keyframes vote-choisi {
  from {
    scale: 0.97;
    box-shadow: inset 0 0 0 0 var(--red);
  }
}

.vote__candidat:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Sans compte : on voit ce qui se joue, on n'y touche pas. */
.vote__candidat--inerte {
  cursor: default;
}

.blindtest__champ {
  inline-size: 100%;
  margin-block: 0.6rem 0.4rem;
}

/*
  Les propositions sont une liste de boutons, pas une liste deroulante : le
  choix se fait d'un seul geste, la ou un « select » demande d'ouvrir, viser,
  puis valider. Trois secondes perdues quand le morceau en dure cent-quatre-
  vingts, et quelqu'un d'autre a repondu entre-temps.
*/
.blindtest__propositions {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
  max-block-size: 14rem;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in oklab, var(--ink-600) 85%, transparent) transparent;
}

/*
  La pochette accompagne le libelle : sur huit cents titres aux noms proches,
  l'image tranche plus vite que la lecture. Elle ne trahit rien — ce sont les
  morceaux deja passes, et rien ne dit lequel tourne.
*/
.blindtest__proposition {
  --vignette: 32px;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  inline-size: 100%;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--ink-850);
  color: var(--chalk);
  font: inherit;
  font-size: var(--step--1);
  text-align: start;
  cursor: pointer;
  transition: border-color 120ms var(--ease-out), background 120ms var(--ease-out);
}

/*
  « min-width: 0 » autorise la troncature : sans lui, un titre a rallonge
  elargirait le bouton et ferait deborder la colonne entiere.
*/
.blindtest__libelle {
  min-inline-size: 0;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.blindtest__proposition:hover:not(:disabled),
.blindtest__proposition:focus-visible {
  border-color: var(--red);
  background: var(--ink-800);
}

.blindtest__proposition:disabled {
  opacity: 0.5;
  cursor: default;
}

/* Un trait avant le classement : il se consulte apres coup, pas en cherchant. */
.blindtest__sous-titre {
  margin-block: 1.1rem 0.5rem;
  padding-block-start: 0.9rem;
  border-block-start: 1px solid var(--line);
  font-family: var(--font-display);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: var(--step--1);
  color: var(--smoke-dim);
}

.blindtest__classement {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: rang;
}

.blindtest__rang {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  padding-block: 0.35rem;
  border-block-end: 1px solid color-mix(in oklab, var(--line) 60%, transparent);
  font-size: var(--step--1);
  counter-increment: rang;
}

/* Le dernier trait doublerait le bord du panneau. */
.blindtest__rang:last-child {
  border-block-end: none;
}

/*
  Le rang se compte en CSS : le serveur envoie un ordre, pas des numeros.

  Chiffres tabulaires et largeur fixe : sans elles, le passage de « 9. » a
  « 10. » decalerait tous les pseudos d'un cran.
*/
.blindtest__joueur::before {
  content: counter(rang) ".";
  display: inline-block;
  inline-size: 1.6em;
  color: var(--smoke-dim);
  font-variant-numeric: tabular-nums;
}

.blindtest__points {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  color: var(--ice);
  font-variant-numeric: tabular-nums;
}

/* La premiere place se distingue, sans quoi un classement n'en est pas un. */
.blindtest__rang:first-child .blindtest__joueur,
.blindtest__rang:first-child .blindtest__points {
  color: var(--red);
  font-weight: 600;
}

/*
  Le panneau de prise d'antenne.

  Il n'emprunte plus « etat__pile », qui empile en colonne et mettait les deux
  boutons l'un sous l'autre. Les notes, elles, reprennent « etat__note » : cette
  classe tronquait a l'epoque, elle ne tronque plus.
*/
.diffusion__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-block: 1.1rem 0.5rem;
}

/*
  Le mot de passe se recopie a la main dans un logiciel tiers. Il merite donc
  d'etre lisible sans plisser les yeux, et selectionnable d'un seul geste :
  « user-select: all » prend tout le bloc au premier clic, ce qui evite de
  rater un caractere au bord.
*/
.diffusion__secret {
  display: block;
  margin: 0.6rem 0 0;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--ink-950);
  font-family: var(--font-mono);
  font-size: var(--step-0);
  letter-spacing: 0.04em;
  color: var(--ice);
  overflow-wrap: anywhere;
  user-select: all;
}

/* Les parametres respirent avant les notes qui les commentent. */
.diffusion__reglages {
  margin-block-end: 0.9rem;
}

/* L'ecart vient de la pile ; le garder ici cumulerait les deux. */
.etat__pile > .etat__note {
  margin-top: 0;
}

/*
  Note discrete sous un etat.

  Elle portait « white-space: nowrap », pose pour les deux seuls usages courts
  qui existaient alors — « sans terme », une date de fin de silence. Toute
  phrase un peu longue s'en trouvait tronquee net au bord de son conteneur, sans
  point de suspension ni barre de defilement : le texte disparaissait, tout
  simplement. Le piege a mordu deux fois en une journee.

  Une date coupee en deux serait moins grave qu'une phrase amputee ; le jour ou
  cela genera, la reponse est une espace insecable dans la traduction, pas un
  interdit global de retour a la ligne.
*/
.etat__note {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--smoke-dim);
}

/* Ce qui touche a la parole, puis ce qui touche au compte. */
.actions__groupe {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.actions__groupe + .actions__groupe {
  margin-inline-start: 0.25rem;
  padding-inline-start: 0.5rem;
  border-inline-start: 1px solid var(--line);
}

.inline-form--groupee {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

.champ-compact {
  font: inherit;
  font-size: 0.8rem;
  padding: 0.28rem 0.25rem;
  border-radius: 0.35rem;
  border: 1px solid var(--line);
  background: var(--surface-raised);
  color: var(--chalk);
}

/*
  Dans une liste de lignes semblables, un aplat rouge par ligne ne signale plus
  rien : il devient le fond. La fermeture d'un compte reste distincte, mais ne
  s'affirme qu'au moment ou l'on va la cliquer.
*/
.table__actions .btn--danger {
  border-color: var(--line);
  color: var(--smoke-dim);
}

.table__actions .btn--danger:hover,
.table__actions .btn--danger:focus-visible {
  border-color: var(--red);
  color: var(--red-hot);
}

/*
  Champ d'envoi de fichier. La classe des champs texte lui donnait une hauteur
  fixe : un navigateur ne rend pas un « input file » comme une zone de saisie,
  et le cadre restait vide sur presque toute sa surface.
*/
.champ-fichier {
  font: inherit;
  font-size: 0.85rem;
  color: var(--smoke-dim);
  max-width: 100%;
}

.champ-fichier::file-selector-button {
  font: inherit;
  font-size: 0.8rem;
  margin-inline-end: 0.6rem;
  padding: 0.3rem 0.7rem;
  border-radius: 0.35rem;
  border: 1px solid var(--line);
  background: var(--surface-raised);
  color: var(--chalk);
  cursor: pointer;
}

.champ-fichier::file-selector-button:hover {
  border-color: var(--line-strong);
}

/* Un ami se clique pour ouvrir son profil. */
.membre__lien {
  color: inherit;
  text-decoration: none;
  border-block-end: 1px solid transparent;
}

.membre__lien:hover,
.membre__lien:focus-visible {
  border-block-end-color: var(--ice-dim);
}

.membre-entete {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1rem;
}

.membre-entete__texte {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  min-width: 12rem;
  flex: 1 1 12rem;
}

.membre-entete__pseudo {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  font-size: 1.15rem;
}

/*
  Le pseudo reste visible pendant le defilement lateral. Sans cela, on lit une
  ligne de boutons de moderation sans savoir sur qui ils agissent.

  A partir de soixante rem seulement : mesure sur un telephone de 375 px, la
  colonne figee occupait cinquante-sept pour cent de la largeur visible. Ce qui
  aide sur un ecran large enferme sur un petit.
*/
@media (min-width: 60rem) {
  .table__pseudo {
    position: sticky;
    inset-inline-start: 0;
    z-index: 1;

    /* Opaque, sinon les colonnes defilent visiblement dessous. */
    background: var(--surface);
  }

  /*
    La bordure d'une cellule collante disparait sous « border-collapse:
    collapse » : elle est peinte par le tableau et non par la cellule, si bien
    que le contenu defile par-dessus. Des ombres internes la remplacent, et la
    seconde marque le bord de la zone figee.
  */
  .table td.table__pseudo {
    box-shadow:
      inset 0 -1px 0 color-mix(in oklab, var(--line) 60%, transparent),
      1px 0 0 var(--line);
  }

  .table th.table__pseudo {
    box-shadow:
      inset 0 -1px 0 var(--line-strong),
      1px 0 0 var(--line);
  }

  .table tbody tr:hover td.table__pseudo {
    background: color-mix(in oklab, var(--smoke) 5%, var(--surface));
  }
}

/*
  Une ligne fait plus de mille deux cents pixels de large : sans repere, l'oeil
  saute d'une ligne a l'autre en revenant du bord droit.
*/
.table tbody tr:hover td {
  background: color-mix(in oklab, var(--smoke) 5%, transparent);
}

.table__email {
  max-width: 10rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Un libelle coupe en deux au milieu d'un bouton ne se lit plus. */
.table__actions .btn,
.roles .pastille-role {
  white-space: nowrap;
}

/*
  Panneau des listes deroulantes. Il est dessine par le systeme et non par la
  page : « color-scheme » est ce qui lui dit de le peindre sombre, et il est
  deja declare sur la racine. Encore faut-il que le champ ait un fond valide —
  une variable inexistante fait tomber la declaration en silence, et le panneau
  repart alors sur le blanc par defaut. C'est ce qui arrivait ici.

  Les lignes sont peintes en plus pour Firefox, qui les laisse a l'auteur.
*/
select option,
select optgroup {
  background: var(--surface-raised);
  color: var(--chalk);
}

select:disabled {
  opacity: 0.6;
}

/*
  Liste deroulante animee.

  Le panneau d'un « select » est dessine par le systeme, hors de la page :
  aucune regle CSS ne l'atteint, et il ne peut donc rien animer. « appearance:
  base-select » demande au navigateur de nous en confier le dessin — le panneau
  devient un element du document, stylable et transitionnable.

  C'est une amelioration progressive, sans repli a ecrire : un navigateur qui
  ignore la valeur garde son panneau natif, deja sombre puisque la racine
  declare son theme. Rien ne se degrade, l'animation manque simplement.
*/
select,
select::picker(select) {
  appearance: base-select;
}

select {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.7rem;
  background: var(--surface-raised);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  color: var(--chalk);
  cursor: pointer;
  transition: border-color 140ms var(--ease-out);
}

/* Dans un formulaire, la liste s'aligne sur les champs qui l'entourent. */
.field select {
  padding: 0.7rem 0.85rem;
  background: var(--ink-950);
}

select:hover {
  border-color: var(--smoke-dim);
}

select:focus-visible {
  border-color: var(--ice);
}

/* Le chevron suit l'ouverture : c'est ce qui relie le panneau a son champ. */
select::picker-icon {
  color: var(--smoke-dim);
  transition: rotate 160ms var(--ease-out);
}

select:open::picker-icon {
  rotate: 180deg;
}

select::picker(select) {
  /*
    Toujours sous le champ. Par defaut le navigateur retourne le panneau des
    qu'il manque de place en bas — c'est raisonnable dans l'absolu, mais dans un
    tableau ou toutes les lignes proposent la meme liste, la derniere s'ouvrait
    a l'envers des autres et le geste changeait de sens sans prevenir.
    « position-try-fallbacks: none » retire ce retournement.

    Le panneau se reduit alors au lieu de se retourner : sa hauteur est bornee a
    l'espace disponible sous le champ, et il defile s'il n'y tient pas.
  */
  position-area: block-end span-inline-end;
  position-try-fallbacks: none;
  max-block-size: 100%;
  overflow-y: auto;

  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-raised);
  padding: 0.25rem;
  margin-block-start: 0.3rem;
  box-shadow: 0 1rem 2rem oklch(0% 0 0 / 0.45);

  /*
    « display » et « overlay » sont des proprietes discretes : sans
    « allow-discrete », le panneau disparait d'un coup et la transition de
    sortie ne se joue jamais.
  */
  opacity: 0;
  translate: 0 -0.4rem;
  transition:
    opacity 150ms var(--ease-out),
    translate 150ms var(--ease-out),
    display 150ms allow-discrete,
    overlay 150ms allow-discrete;
}

select:open::picker(select) {
  opacity: 1;
  translate: 0 0;
}

/* Sans cette regle, l'ouverture n'a pas d'etat de depart et ne s'anime pas. */
@starting-style {
  select:open::picker(select) {
    opacity: 0;
    translate: 0 -0.4rem;
  }
}

select option {
  padding: 0.35rem 0.6rem;
  border-radius: 0.3rem;
  transition: background-color 120ms var(--ease-out);
}

select option:hover {
  background: var(--ink-800);
}

select option:checked {
  color: var(--ice);
}

/*
  Le bloc general de mouvement reduit vise « * », « ::before » et « ::after » :
  il ne connait pas ces pseudo-elements-la.
*/
@media (prefers-reduced-motion: reduce) {
  select::picker(select),
  select::picker-icon,
  select option {
    transition-duration: 0.01ms !important;
  }
}

/* ================================================== programme des directs */

/*
  La grille et sa colonne, sur le modèle du direct : à gauche la journée, à
  droite ce qui ne regarde que le visiteur — ses rappels — et ce qui se passe
  hors créneau. Une seule colonne sur mobile, la journée d'abord.
*/
.programme {
  display: grid;
  gap: var(--gap);
  grid-template-columns: minmax(0, 1fr);
}

.programme__grille,
.programme__cote {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  min-width: 0;
}

@media (min-width: 64rem) {
  .programme {
    grid-template-columns: minmax(0, 1fr) minmax(18rem, 22rem);
    align-items: start;
  }
}

/*
  Sept onglets : ils passent à la ligne plutôt que de déborder sur un
  téléphone. La marge basse des onglets ordinaires s'ajouterait à l'écart de
  la colonne, qui espace déjà ses enfants.
*/
.programme__jours {
  gap: 0.4rem 1rem;
  margin-block-end: 0;
}

.programme__jour {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  min-width: 3.4rem;
}

.programme__date {
  font-size: var(--step-1);
  line-height: 1;
}

.programme__compte {
  font-family: var(--font-body);
  font-size: 0.7rem;
  letter-spacing: 0;
  text-transform: none;
  color: var(--smoke-dim);
}

/* Un jour sans direct s'efface sans disparaître : on peut toujours y aller. */
.programme__jour--vide .programme__date {
  color: var(--smoke-dim);
}

.programme__liste {
  display: flex;
  flex-direction: column;
  list-style: none;
}

.direct-prevu {
  display: grid;
  grid-template-columns: 7.5rem minmax(0, 1fr) auto;
  gap: 0.4rem 1rem;
  align-items: start;
  padding-block: 0.9rem;
  border-block-end: 1px solid var(--line);
}

.direct-prevu:last-child {
  border-block-end: 0;
}

.direct-prevu__horaire {
  padding-block-start: 0.2rem;
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--ice);
  white-space: nowrap;
}

.direct-prevu__corps {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}

/* La suite d'un direct de la veille : une mention discrète, au-dessus du titre. */
.direct-prevu__suite {
  margin: 0 0 0.15rem;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--smoke);
}

.direct-prevu__titre {
  font-family: var(--font-display);
  font-size: var(--step-1);
  letter-spacing: 0.04em;
  color: var(--chalk);
  overflow-wrap: anywhere;
}

.direct-prevu__dj {
  font-size: var(--step--1);
  color: var(--smoke);
}

.direct-prevu__etat {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.5rem;
}

@media (max-width: 40rem) {
  .direct-prevu {
    grid-template-columns: minmax(0, 1fr);
  }

  .direct-prevu__etat {
    align-items: flex-start;
  }
}

/* Un direct terminé reste lisible : il dit ce qui s'est joué. */
.direct-prevu--passe .direct-prevu__titre,
.direct-prevu--passe .direct-prevu__horaire {
  color: var(--smoke-dim);
}

/* Le liseré de l'antenne, sur le direct qui l'occupe. */
.direct-prevu--en-cours {
  padding-inline-start: 0.8rem;
  border-inline-start: 3px solid var(--red);
}

.echeance {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  color: var(--smoke);
  white-space: nowrap;
}

.echeance--passe {
  border-color: var(--line);
  color: var(--smoke-dim);
}

.echeance--direct {
  border-color: var(--red);
  color: var(--red-hot);
}

.echeance--bientot {
  border-color: var(--ice-dim);
  color: var(--ice);
}

.etiquettes {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  list-style: none;
}

.etiquette {
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  color: var(--smoke);
}

.cloche {
  white-space: nowrap;
}

/* La cloche posée se reconnaît sans lire le bouton. */
.cloche--posee {
  border-color: var(--ice-dim);
  color: var(--ice);
}

.rappels {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-block-end: 0.8rem;
  list-style: none;
}

.rappels__ligne {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.6rem;
}

.rappels__quand {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--ice);
  white-space: nowrap;
}

.rappels__titre {
  color: var(--chalk);
  overflow-wrap: anywhere;
}

/* ---- le prochain direct, sur l'accueil ---- */

.prochain__quand {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.8rem;
  margin-block-end: 0.7rem;
}

.prochain__horaire {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--smoke);
}

.prochain__titre {
  font-family: var(--font-display);
  font-size: var(--step-2);
  line-height: 1.1;
  letter-spacing: 0.03em;
  color: var(--chalk);
  overflow-wrap: anywhere;
}

.prochain__dj {
  margin-block: 0.35rem 0.6rem;
  color: var(--smoke);
}

/* ---- le Studio : proposer, relire ses créneaux ---- */

/* Le jour, l'heure et la durée tiennent sur une ligne dès que la place le permet. */
.proposition {
  max-width: 40rem;
}

.proposition__horaire {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
  gap: 0.9rem;
}

.proposition__horaire > .field {
  margin-block-end: 0;
}

.creneaux {
  display: flex;
  flex-direction: column;
  list-style: none;
}

.creneaux__ligne {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.8rem;
  padding-block: 0.7rem;
  border-block-end: 1px solid var(--line);
}

.creneaux__ligne:last-child {
  border-block-end: 0;
}

.creneaux__quand {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  color: var(--ice);
  white-space: nowrap;
}

.creneaux__titre {
  font-weight: 600;
  color: var(--chalk);
  overflow-wrap: anywhere;
}

.creneaux__dj {
  font-size: var(--step--1);
  color: var(--smoke);
}

/* Les décisions se rangent au bout de la ligne, à l'écart de ce qu'elles jugent. */
.creneaux__actions {
  display: inline-flex;
  gap: 0.4rem;
  margin-inline-start: auto;
}

.tag--attente {
  border-style: dashed;
  color: var(--smoke);
}

.tag--valide {
  border-color: var(--ice-dim);
  color: var(--ice);
}

.tag--refuse {
  border-color: var(--red-dim);
  color: var(--red-hot);
}

.tag--retire,
.tag--annule,
.tag--echu {
  border-color: var(--line);
  color: var(--smoke-dim);
}

/* ===================================== réactions : ДАВАЙ et КОЛБАСИТ */

/*
  Sous le morceau, une seule ligne : le bouton et son compte, puis la jauge.
  Elle passe à la ligne dans une colonne étroite plutôt que de tasser la jauge
  jusqu'à l'illisible.
*/
.reactions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem 1.5rem;
  margin-block-start: 1rem;
  padding-block-start: 0.9rem;
  border-block-start: 1px solid var(--line);
}

/*
  Une grille plutôt qu'un flux : l'avis occupe sa propre rangée sans hauteur
  tant qu'il est vide. Il doit rester dans la page même muet — une région
  annoncée qui n'apparaît qu'avec son texte n'est pas lue partout.
*/
.davai {
  display: grid;
  grid-template-columns: auto auto;
  justify-content: start;
  align-items: center;
  column-gap: 0.9rem;
}

.davai__bouton {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  padding: 0.5rem 1rem;
  border: 1px solid var(--red);
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--red) 16%, transparent);
  color: var(--chalk);
  font: inherit;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
  transition: background 140ms var(--ease-out), translate 140ms var(--ease-out);
}

.davai__bouton:hover {
  background: color-mix(in oklab, var(--red) 30%, transparent);
  translate: 0 -1px;
}

.davai__bouton:active {
  translate: 0 0;
}

/* Tout donné sur ce morceau, ou pas encore interactif : le bouton reste là,
   lisible, mais ne promet plus rien. */
.davai__bouton:disabled,
.davai__bouton[aria-disabled="true"],
.davai__bouton[aria-disabled="true"]:hover {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--smoke-dim);
  translate: 0 0;
  cursor: not-allowed;
}

/* L'invitation à se connecter : même forme, sans l'accent — on n'agit pas
   encore, on est invité à le faire. */
.davai__bouton--invite {
  border-color: var(--line-strong);
  background: transparent;
}

.davai__bouton--invite:hover {
  border-color: var(--chalk);
  background: transparent;
}

.davai__cri {
  font-family: var(--font-display);
  font-size: var(--step-1);
  font-weight: 700;
  letter-spacing: 0.08em;
  line-height: 1.1;
}

.davai__sous-titre {
  font-size: var(--step--1);
  color: var(--smoke);
}

/*
  Le « +1 » s'envole du bouton. Une sortie, pas une entrée : il part visible et
  s'efface — l'inverse de ce que la feuille interdit. Le réglage « mouvement
  réduit » le ramène à rien, sans le nommer.
*/
.davai__envol {
  position: absolute;
  inset-block-start: -0.2rem;
  inset-inline-end: 0.5rem;
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--red-hot);
  pointer-events: none;
  animation: davai-envol 900ms ease-out forwards;
}

/*
  Opaque sur l'essentiel du vol, effacé à la fin seulement. Avec la courbe
  « --ease-out » du site, l'opacité tombait à un dixième au tiers du trajet :
  mesuré, le « +1 » ne se voyait presque pas.
*/
@keyframes davai-envol {
  from {
    opacity: 1;
    translate: 0 0;
  }

  60% {
    opacity: 1;
  }

  to {
    opacity: 0;
    translate: 0.3rem -2.4rem;
  }
}

.davai__compte {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: var(--step--1);
}

.davai__total {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--chalk);
}

.davai__legende {
  color: var(--smoke);
}

.davai__avis {
  grid-column: 1 / -1;
  font-size: var(--step--1);
  color: var(--smoke);
}

.davai__avis:not(:empty) {
  margin-block-start: 0.5rem;
}

.kolbasit {
  flex: 1 1 12rem;
  display: grid;
  gap: 0.35rem;
  min-width: 10rem;
}

.kolbasit__titre {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--step--1);
}

.kolbasit__nom {
  font-family: var(--font-display);
  letter-spacing: 0.14em;
  color: var(--chalk);
}

.kolbasit__sous-titre {
  color: var(--smoke-dim);
}

.kolbasit__valeur {
  margin-inline-start: auto;
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  color: var(--smoke);
}

/*
  La jauge est un <meter> : sa valeur est un attribut, que la politique de
  sécurité laisse passer là où une largeur en style serait refusée. Chaque
  moteur nomme autrement la partie remplie — d'où une règle par moteur, et
  jamais une liste commune : un sélecteur inconnu fait tomber toute la liste.
*/
.kolbasit__jauge {
  appearance: none;
  display: block;
  width: 100%;
  height: 0.6rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--ink-800);
  overflow: hidden;
}

.kolbasit__jauge::-webkit-meter-bar {
  height: 100%;
  border: 0;
  border-radius: 0;
  background: var(--ink-800);
}

.kolbasit__jauge::-webkit-meter-optimum-value,
.kolbasit__jauge::-webkit-meter-suboptimum-value,
.kolbasit__jauge::-webkit-meter-even-less-good-value {
  background: linear-gradient(90deg, var(--red-dim), var(--red-hot));
  transition: inline-size 600ms var(--ease-out);
}

.kolbasit__jauge::-moz-meter-bar {
  background: linear-gradient(90deg, var(--red-dim), var(--red-hot));
}

/* ======================================== minuterie de veille du lecteur */

/*
  Un bouton d'icône qui porte aussi du texte : « Veille », puis le temps
  restant. Sa largeur ne bouge pas d'un état à l'autre, pour que le volume et
  le compte des auditeurs ne sautent pas à chaque minute.
*/
.dock__veille {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  width: auto;
  padding-inline: 0.6rem;
  font-family: var(--font-display);
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  white-space: nowrap;
}

.dock__veille-temps {
  min-width: 3.6em;
  font-variant-numeric: tabular-nums;
  text-align: start;
}

.dock__veille:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/*
  Armée, elle prend la teinte d'un bouton enfoncé — celle qu'elle tenait de
  « aria-pressed », retiré parce que son nom dit déjà l'état. L'aspect reste
  celui d'avant.
*/
.dock__veille--armee {
  color: var(--red-hot);
  border-color: var(--red);
}

/* Comme le volume : sur les écrans étroits, le lecteur garde l'essentiel. */
@media (max-width: 60rem) {
  .dock__veille {
    display: none;
  }
}

/* ================================================================ insignes */

/*
  En tête du profil : les trois premiers insignes obtenus, en pastilles, et le
  lien vers l'onglet. Sobre, comme le reste de l'ancienne allure : un liseré
  rouge, pas d'émail.
*/
.insignes-entete {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  margin-block: -0.4rem 1.2rem;
}

.insignes-entete__liste {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pastille-insigne {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.65rem;
  border: 1px solid var(--red);
  border-radius: 999px;
  background: color-mix(in oklab, var(--red) 14%, transparent);
  font-family: var(--font-display);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  color: var(--chalk);
}

.pastille-insigne__etoile {
  color: var(--red-hot);
}

.insignes-entete__lien {
  font-size: var(--step--1);
  color: var(--smoke);
}

.insignes-entete__lien:hover {
  color: var(--chalk);
}

/* Le panneau des insignes prend toute la largeur : la grille a besoin de place
   pour huit épinglettes, et il est seul de son onglet. */
.panneau-insignes {
  grid-column: 1 / -1;
}

.panneau-insignes__collecte {
  margin-block-start: 1.5rem;
  font-size: var(--step--1);
}

/*
  Le nombre au-dessus, son nom dessous. Le balisage garde l'ordre d'une liste
  de définitions — le nom d'abord — et c'est la disposition qui les inverse :
  un lecteur d'écran entend « heures d'écoute, 12 », pas l'inverse.
*/
.chiffres-insignes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr));
  gap: 0.75rem;
  margin: 0 0 1.5rem;
}

.chiffres-insignes__case {
  display: flex;
  flex-direction: column-reverse;
  gap: 0.2rem;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-raised);
}

.chiffres-insignes__case dt {
  font-size: var(--step--1);
  color: var(--smoke);
}

.chiffres-insignes__case dd {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--chalk);
}

/*
  Quatre colonnes au plus, deux au moins sur un téléphone : huit épinglettes
  font alors deux rangées pleines, au lieu d'une rangée de sept et d'une
  orpheline. Le quart se calcule gouttières déduites.
*/
.insignes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(max(min(100%, 8rem), calc((100% - 3rem) / 4)), 1fr));
  gap: 1.5rem 1rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.insigne {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  text-align: center;
}

/*
  L'épinglette : un rond, deux anneaux, une étoile et l'étiquette russe. Le
  second anneau est une ombre portée sans flou, qui ne déplace rien autour.
*/
.insigne__epinglette {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 0.15rem;
  width: 5.75rem;
  aspect-ratio: 1;
  margin-block-end: 0.4rem;
  border: 2px solid var(--chalk);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, var(--red-hot), var(--red) 55%, var(--red-dim));
  box-shadow:
    inset 0 0 0 3px color-mix(in oklab, var(--ink-950) 55%, transparent),
    0 0 0 3px var(--ink-900),
    0 0 0 4px var(--red-dim);
}

.insigne__etoile {
  font-size: 1.35rem;
  line-height: 1;
  color: var(--chalk);
}

.insigne__ru {
  font-family: var(--font-display);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--chalk);
}

.insigne__nom {
  font-family: var(--font-display);
  letter-spacing: 0.04em;
  color: var(--chalk);
}

.insigne__condition {
  max-width: 14rem;
  font-size: var(--step--1);
  color: var(--smoke);
}

.insigne__etat {
  font-size: var(--step--1);
  color: var(--smoke-dim);
}

.insigne--obtenu .insigne__etat {
  color: var(--red-hot);
}

/* À venir : la même épinglette, éteinte. Pas d'opacité sur l'ensemble — le
   texte dessous doit rester lisible. */
.insigne--a-venir .insigne__epinglette {
  border-style: dashed;
  border-color: var(--line-strong);
  background: var(--ink-800);
  box-shadow: none;
}

.insigne--a-venir .insigne__etoile,
.insigne--a-venir .insigne__ru {
  color: var(--smoke-dim);
}

.insigne--a-venir .insigne__nom {
  color: var(--smoke);
}

/*
  La progression est un <progress> : sa valeur est un attribut, que la
  politique de sécurité laisse passer là où une largeur en style serait
  refusée. Une règle par moteur, jamais une liste commune : un sélecteur
  inconnu ferait tomber toute la liste.
*/
.insigne__progression {
  appearance: none;
  display: block;
  width: 100%;
  max-width: 9rem;
  height: 0.5rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--ink-800);
  overflow: hidden;
}

.insigne__progression::-webkit-progress-bar {
  background: var(--ink-800);
}

.insigne__progression::-webkit-progress-value {
  background: linear-gradient(90deg, var(--red-dim), var(--red-hot));
}

.insigne__progression::-moz-progress-bar {
  background: linear-gradient(90deg, var(--red-dim), var(--red-hot));
}

.insigne__chiffres {
  font-size: var(--step--1);
  font-variant-numeric: tabular-nums;
  color: var(--smoke);
}

/* ===================================== ce qui n'appartient qu'à Панелька */

/*
  Rendus pour tout le monde, montrés par la seule feuille Панелька : la page
  d'accueil est interactive et ne lit pas le cookie d'allure, et changer
  d'allure sans recharger doit pouvoir les faire paraître. L'ancienne allure
  les reçoit donc, et les tait ici — rien ne change pour elle.

  Ceux de la mise en page — le bloc, la bascule d'éclairage, les onglets du
  téléphone — ne lui sont jamais envoyés, pas plus que ceux des pages — la
  fiche d'un membre, l'entrée des comptes, l'argot des erreurs. Ils figurent
  ici quand même : quand on quitte Панелька, sa feuille part avant que la page
  ne soit refaite, et ils s'étalaient un instant sans style.
*/
.une,
.presents,
.vote__rang,
.vote__part,
.vote__jauge,
.immeuble,
.bascule,
.langues-courtes,
.pseudo__vignette,
.onglets-bas,
.feuille,
.fiche,
.acces,
.acces__bascule,
.surtitre,
.error-code[aria-hidden="true"] {
  display: none;
}
