/* =========================================================================
   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;
  }
}

/* ----------------------------------------------------------------- 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;
  }

  .grid--profile {
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  }

  .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;
  }

  .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;
  }

  .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;
    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;
  }

  /* 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;
  }

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

  .dock__progress-bar {
    display: block;
    height: 100%;
    width: 0%;
    background: linear-gradient(90deg, var(--red), var(--red-hot));
    transition: width 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;
  }

  .dock__listeners {
    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;
    }
  }

  /*
    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);
  }

  .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;
  }

  /*
    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);
  }

  .direct__scene {
    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 {
      max-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.
  */
  .chat__fil {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    max-block-size: 26rem;
    overflow-y: auto;
    padding-inline-end: 0.4rem;
    margin-block-end: 1rem;
  }

  .chat__fil--archive {
    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;
  }

  .chat__message {
    display: flex;
    flex-wrap: wrap;
    /*
      Centre et non ligne de base. La ligne de base convenait quand la rangee
      n'etait que du texte ; depuis qu'une vignette l'ouvre, elle laissait le
      texte trois pixels au-dessus du centre de l'image. Tous les elements
      partagent la meme taille de police, les centrer ne les desaligne donc pas
      entre eux.
    */
    align-items: center;
    gap: 0.5rem;
    font-size: var(--step--1);
    line-height: 1.45;
  }

  .chat__heure {
    flex: 0 0 auto;
    color: var(--smoke-dim);
    font-variant-numeric: tabular-nums;
  }

  .chat__auteur {
    flex: 0 0 auto;
    font-weight: 600;
    color: var(--ice);
  }

  /*
    Le texte prend la place restante et se coupe ou il faut : un mot de trois
    cents caracteres ne doit pas elargir la page.
  */
  .chat__texte {
    flex: 1 1 12rem;
    min-width: 0;
    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. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !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;
}

/* 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.
*/
.chat__avatar {
  align-self: center;
}

/* Liste d'amis et de demandes. */
.amis {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

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

.amis__ligne .muted {
  margin-left: auto;
}

.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);
}

.etat__note {
  display: block;
  margin-top: 0.2rem;
  font-size: 0.78rem;
  color: var(--smoke-dim);
  white-space: nowrap;
}

/* 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;
}

.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;
  }
}
