
@layer tokens, base, glass, rack, layout, adapt;

/* ═══ TOKENS ══════════════════════════════════════════════
   --harden : 0 machine ouverte, 1 machine blindée. Le scroll
   l'écrit ; couleur, netteté du verre et voyants en dépendent.
   ════════════════════════════════════════════════════════ */
@layer tokens {
  :root {
    --cyan: #1ac0ff;
    --harden: 0;

    --alert:     #d63b1c;
    --cyan-text: #06617f;
    /* Mesuré : blanc sur cette teinte donne 5,0:1. À la valeur
       précédente (#0a86b4) le bouton plein tombait à 4,13:1,
       sous le seuil AA — visuellement identique, mais hors norme. */
    --cyan-fill: #0076ac;
    --ink:       #08222e;
    --ink-soft:  #45606e;
    --ink-faint: #5f7a8c;
    --ground:    #f2f7fb;

    @supports (color: rgb(from white r g b)) {
      --cyan-text: oklch(from var(--cyan) .42 c h);
      --cyan-fill: oklch(from var(--cyan) .53 c h);
      --ink:       oklch(from var(--cyan) .22 .045 h);
      --ink-soft:  oklch(from var(--cyan) .46 .03 h);
      --ink-faint: oklch(from var(--cyan) .52 .025 h);
      --ground:    oklch(from var(--cyan) .97 .012 h);
    }

    /* `longer hue` : la teinte descend par l'orange puis le vert au
       lieu de remonter par le magenta — le trajet d'un feu tricolore,
       pas celui qui passe par la couleur des pages générées. */
    --state: color-mix(in oklch longer hue, var(--alert), var(--cyan) calc(var(--harden) * 100%));

    /* ── Surfaces ────────────────────────────────────────────
       Le verre a deux couleurs distinctes : le corps du panneau
       (--pane) et l'arête que la lumière accroche (--edge). En
       clair les deux sont blanches ; en sombre le corps s'assombrit
       alors que l'arête reste claire, sinon le relief disparaît.
       Composantes séparées de l'alpha : les opacités du verre
       dépendent de --harden et doivent rester calculables. */
    --pane: 255 255 255;
    --edge: 255 255 255;
    --shade: #08222e;          /* couleur des ombres portées */
    --on-cyan: #ffffff;        /* texte posé sur un aplat cyan */
    --field: #ffffff;          /* fond des champs de saisie */
    --code-bg: #08222e;
    --code-fg: #e6f4fb;
    --code-link: #9fd8f2;
    --code-link-hi: #d6efff;

    /* Baie : tôle claire, montant plus froid, unités laquées. */
    --rack-a: rgb(255 255 255 / .62);
    --rack-b: rgb(226 238 246 / .5);
    --rail-a: rgb(196 216 230 / .9);
    --rail-b: rgb(168 194 212 / .9);
    --u-a: #fdfefe;
    --u-b: #e8f0f6;

    /* Opacités du voile : plus discrètes en sombre, où le même
       réglage noierait les halos au lieu de les tempérer. */
    --veil-1: .84;
    --veil-2: .4;

    --display: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
    --mono: "IBM Plex Mono", ui-monospace, monospace;
    --step: .5rem;
    --r-card: 16px;
    --r-ctl: 9px;

    color-scheme: light;
  }

  /* ═══ PALETTE SOMBRE ══════════════════════════════════════
     Les valeurs vivent ici une seule fois, sous le préfixe --d-,
     et les deux sélecteurs plus bas se contentent de les poser.
     Deux listes de valeurs finiraient par diverger ; deux listes
     de renvois, non.
     ════════════════════════════════════════════════════════ */
  :root {
    --d-alert:     #ff7a5c;
    --d-cyan-text: #6ed3ff;
    --d-cyan-fill: #4ec5f5;
    --d-ink:       #e9f3f9;
    --d-ink-soft:  #b5c9d6;
    --d-ink-faint: #93acbd;
    --d-ground:    #0d1a23;

    @supports (color: rgb(from white r g b)) {
      --d-cyan-text: oklch(from var(--cyan) .84 c h);
      --d-cyan-fill: oklch(from var(--cyan) .78 c h);
      --d-ink:       oklch(from var(--cyan) .96 .012 h);
      --d-ink-soft:  oklch(from var(--cyan) .82 .022 h);
      --d-ink-faint: oklch(from var(--cyan) .71 .026 h);
      --d-ground:    oklch(from var(--cyan) .18 .028 h);
    }

    /* Le corps du verre s'assombrit, l'arête reste claire :
       c'est elle qui donne le relief, l'inverser l'effacerait. */
    --d-pane: 36 62 78;
    --d-edge: 150 192 216;
    --d-shade: #000508;
    --d-on-cyan: #05202c;
    --d-field: #16242f;
    --d-code-bg: #06131b;

    --d-rack-a: rgb(54 84 104 / .62);
    --d-rack-b: rgb(32 54 70 / .5);
    --d-rail-a: rgb(58 88 108 / .9);
    --d-rail-b: rgb(40 64 82 / .9);
    --d-u-a: #1d3140;
    --d-u-b: #16262f;

    --d-veil-1: .52;
    --d-veil-2: .24;
  }

  /* Réglage système, sauf si la personne a explicitement choisi le clair. */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
      --alert: var(--d-alert);
      --cyan-text: var(--d-cyan-text);
      --cyan-fill: var(--d-cyan-fill);
      --ink: var(--d-ink);
      --ink-soft: var(--d-ink-soft);
      --ink-faint: var(--d-ink-faint);
      --ground: var(--d-ground);
      --pane: var(--d-pane);
      --edge: var(--d-edge);
      --shade: var(--d-shade);
      --on-cyan: var(--d-on-cyan);
      --field: var(--d-field);
      --code-bg: var(--d-code-bg);
      --rack-a: var(--d-rack-a);
      --rack-b: var(--d-rack-b);
      --rail-a: var(--d-rail-a);
      --rail-b: var(--d-rail-b);
      --u-a: var(--d-u-a);
      --u-b: var(--d-u-b);
      --veil-1: var(--d-veil-1);
      --veil-2: var(--d-veil-2);
      color-scheme: dark;
    }
  }

  /* Choix explicite : il l'emporte sur le réglage système. */
  :root[data-theme="dark"] {
    --alert: var(--d-alert);
    --cyan-text: var(--d-cyan-text);
    --cyan-fill: var(--d-cyan-fill);
    --ink: var(--d-ink);
    --ink-soft: var(--d-ink-soft);
    --ink-faint: var(--d-ink-faint);
    --ground: var(--d-ground);
    --pane: var(--d-pane);
    --edge: var(--d-edge);
    --shade: var(--d-shade);
    --on-cyan: var(--d-on-cyan);
    --field: var(--d-field);
    --code-bg: var(--d-code-bg);
    --rack-a: var(--d-rack-a);
    --rack-b: var(--d-rack-b);
    --rail-a: var(--d-rail-a);
    --rail-b: var(--d-rail-b);
    --u-a: var(--d-u-a);
    --u-b: var(--d-u-b);
    --veil-1: var(--d-veil-1);
    --veil-2: var(--d-veil-2);
    color-scheme: dark;
  }
}

@layer base {
  *, *::before, *::after { box-sizing: border-box; }
  /* Les cartes sont en `display: block` : sans cette règle, masquer une
     carte filtrée par la propriété `hidden` resterait sans effet. */
  [hidden] { display: none !important; }
  body {
    margin: 0;
    background: var(--ground);
    color: var(--ink);
    font-family: var(--display);
    line-height: 1.6;
    -webkit-font-smoothing: antialiased;
  }
  a { color: inherit; text-decoration: none; }
  h1, h2, h3 { margin-block: 0; text-wrap: balance; }
  p, ul { margin-block: 0; }
  :focus-visible { outline: 2px solid var(--cyan-text); outline-offset: 3px; }
  .num { font-variant-numeric: tabular-nums; }
  .mono { font-family: var(--mono); }
}

@layer glass {
  .glass {
    position: relative;
    border-radius: var(--r-card);
    background: rgb(var(--pane) / calc(.34 + var(--harden) * .16));
    backdrop-filter: blur(calc(10px + var(--harden) * 18px)) saturate(calc(130% + var(--harden) * 60%));
    -webkit-backdrop-filter: blur(calc(10px + var(--harden) * 18px)) saturate(calc(130% + var(--harden) * 60%));
    border: 1px solid rgb(var(--edge) / calc(.38 + var(--harden) * .34));
    box-shadow:
      inset 0 1px 0 rgb(var(--edge) / calc(.45 + var(--harden) * .5)),
      0 14px 40px -20px oklch(from var(--shade) l c h / .35);
  }
}

/* ═══ RACK ════════════════════════════════════════════════
   Une baie en CSS 3D — pas de WebGL, pas de modèle à charger.
   Chaque U est une plaque posée à sa hauteur ; les voyants
   virent du rouge au cyan à mesure que la machine se ferme.
   ════════════════════════════════════════════════════════ */
@layer rack {
  .stage { display: grid; place-items: center; min-block-size: 26rem; padding-block: calc(var(--step) * 2); }

  /* Baie inclinée : assez de perspective pour donner du volume,
     assez peu pour que les étiquettes restent lisibles. */
  .rack {
    position: relative;
    inline-size: 19rem;
    padding: .75rem .8rem 1.1rem;
    border-radius: 12px;
    transform: perspective(1400px) rotateY(-21deg) rotateX(5deg);
    transform-style: preserve-3d;
    background: linear-gradient(160deg, var(--rack-a), var(--rack-b));
    border: 1px solid rgb(var(--edge) / .85);
    box-shadow:
      inset 0 1px 0 rgb(var(--edge) / .95),
      -18px 26px 50px -26px oklch(from var(--shade) l c h / .6);
    animation: sway 9s ease-in-out infinite alternate;
  }

  /* 6° sur 16 s font 0,4°/s : techniquement animé, humainement invisible.
     13° sur 9 s se perçoit sans distraire. */
  @keyframes sway {
    from { transform: perspective(1400px) rotateY(-25deg) rotateX(6deg); }
    to   { transform: perspective(1400px) rotateY(-12deg) rotateX(2deg); }
  }

  /* Les unités se posent une à une au chargement : le premier signe
     de vie, avant même que la personne ait défilé. */
  .u { animation: pose .5s cubic-bezier(.2,.8,.3,1) backwards; }
  .u:nth-child(1) { animation-delay: .05s; }
  .u:nth-child(2) { animation-delay: .13s; }
  .u:nth-child(3) { animation-delay: .21s; }
  .u:nth-child(4) { animation-delay: .29s; }
  .u:nth-child(5) { animation-delay: .37s; }
  .u:nth-child(6) { animation-delay: .45s; }

  @keyframes pose {
    from { opacity: 0; transform: translateX(2.5rem); }
    to   { opacity: 1; transform: none; }
  }

  /* Le montant latéral : c'est lui qui fait lire « baie ». */
  .rack::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: -10px;
    inline-size: 10px;
    border-radius: 12px 0 0 12px;
    background: linear-gradient(180deg, var(--rail-a), var(--rail-b));
    transform: rotateY(-62deg);
    transform-origin: right;
  }

  .rack .floor {
    position: absolute;
    inset-inline: -14%;
    inset-block-end: -2.5rem;
    block-size: 3rem;
    border-radius: 50%;
    background: radial-gradient(closest-side,
      oklch(from var(--state) l c h / .34) 0%, oklch(from var(--state) l c h / 0) 100%);
  }

  .units { display: grid; gap: 5px; }

  .u {
    display: flex;
    align-items: center;
    gap: .55rem;
    block-size: 2.3rem;
    padding-inline: .7rem;
    border-radius: 4px;
    background: linear-gradient(178deg, var(--u-a), var(--u-b));
    border: 1px solid rgb(var(--edge) / .9);
    box-shadow: 0 1px 0 rgb(var(--edge) / .9) inset, 0 3px 7px -4px oklch(from var(--shade) l c h / .45);
    font-family: var(--mono);
    font-size: .5625rem;
    letter-spacing: .03em;
    color: var(--ink-soft);
  }

  .led {
    flex: none;
    inline-size: 6px; block-size: 6px; border-radius: 50%;
    background: var(--state);
    box-shadow: 0 0 8px 1px oklch(from var(--state) l c h / .8);
    animation: blink 2.4s steps(1, end) infinite;
    animation-delay: calc(var(--d, 0) * 1s);
  }
  /* Un vrai clignotement d'équipement : visible, irrégulier, pas un pouls. */
  @keyframes blink {
    0%, 44%  { opacity: 1; }
    46%, 52% { opacity: .12; }
    54%, 78% { opacity: 1; }
    80%, 84% { opacity: .12; }
    86%      { opacity: 1; }
  }

  .u .bays { margin-inline-start: auto; display: flex; gap: 3px; }
  .u .bays i {
    inline-size: 13px; block-size: 8px; border-radius: 2px;
    background: oklch(from var(--ink) l c h / .12);
    box-shadow: inset 0 1px 1px oklch(from var(--ink) l c h / .18);
  }

  .rack-label {
    display: block;
    margin-block-start: .7rem;
    text-align: center;
    font-family: var(--mono);
    font-size: .625rem;
    color: var(--ink-faint);
  }
}

/* ═══ LAYOUT ══════════════════════════════════════════════ */
@layer layout {
  /* L'ambiance de la page reste dans la charte.

     Les halos suivaient `--state`, qui part du rouge d'alerte et ne
     rejoint le cyan qu'en bas de page : le portfolio s'ouvrait donc en
     rose saumon et passait par le vert à mi-parcours. Cinq des six
     états traversés étaient hors charte, et la première impression
     était celle d'une alerte.

     `--state` garde son rôle là où il veut dire quelque chose — le
     point de la jauge, les voyants de la baie, l'ombre au sol. Les
     halos, eux, sont du décor : ils restent cyan, et c'est leur
     intensité qui suit le durcissement. */
  .aura {
    position: fixed; inset: -14%; z-index: 0;
    filter: blur(58px); pointer-events: none;
    opacity: calc(.72 + var(--harden) * .28);
    transition: opacity .4s linear;
    & b { position: absolute; display: block; border-radius: 50%; background: var(--cyan); }
    & b:nth-child(1) { inline-size: 34%; block-size: 52%; inset-block-start: -10%; inset-inline-start: -4%; opacity: .42; }
    & b:nth-child(2) { inline-size: 30%; block-size: 46%; inset-block-start: 34%; inset-inline-start: 24%; opacity: .22; }
    & b:nth-child(3) { inline-size: 34%; block-size: 52%; inset-block-start: 0; inset-inline-end: -2%; opacity: .36; }
    & b:nth-child(4) { inline-size: 24%; block-size: 36%; inset-block-start: 18%; inset-inline-end: 30%; opacity: .26; }
  }

  .veil {
    position: fixed; inset: 0; z-index: 0; pointer-events: none;
    background:
      radial-gradient(78% 62% at 28% 42%, rgb(var(--pane) / var(--veil-1)) 0%, rgb(var(--pane) / var(--veil-2)) 46%, rgb(var(--pane) / 0) 74%),
      linear-gradient(180deg, oklch(from var(--ground) l c h / 0) 58%, oklch(from var(--ground) l c h / .8) 100%);
  }

  .shell {
    position: relative; z-index: 1;
    max-inline-size: 72rem;
    margin-inline: auto;
    padding-inline: calc(var(--step) * 3);
    container-type: inline-size;
  }

  /* ── barre de navigation ────────────────────────────────
     Les pages de contenu font plusieurs milliers de pixels : sans
     barre collante, revenir au menu demande de remonter toute la
     page.

     Le verre ne porte pas sur la barre mais sur les entrées : les
     dix se touchent et forment une bande continue, sans le moindre
     trait pour les séparer. C'est le fond partagé qui les tient
     ensemble, et la pastille pleine qui désigne la page. Aucun
     conteneur ne les entoure dans le HTML — l'intervalle à zéro et
     les coins portés par la première et la dernière suffisent, ce
     qui permet de changer d'allure sans toucher aux pages. */
  .bar {
    position: sticky;
    inset-block-start: 0;
    z-index: 6;
    display: flex; align-items: center;
    gap: 0;
    padding-block: calc(var(--step) * 2.5);
    font-size: .8125rem; color: var(--ink-soft);

    & .id {
      font-weight: 700; font-size: 1rem; letter-spacing: -.02em;
      color: var(--ink); margin-inline-end: auto;
      padding-inline: 0;
    }

    /* Une entrée ne se coupe jamais. Sans cette règle, mesuré :
       « Morgann Riu », « À propos » et « Accompagnement IA »
       passaient sur deux lignes dès 940 px et la barre grandissait
       de 86 à 93 px sans que rien ne le signale. */
    & a { white-space: nowrap; }

    & a:not(.id) {
      padding-inline: calc(var(--step) * 1.5);
      background: rgb(var(--pane) / .5);
      backdrop-filter: blur(14px) saturate(140%);
      -webkit-backdrop-filter: blur(14px) saturate(140%);
      transition: background-color .18s ease, color .18s ease;
    }
    /* La bande ne s'arrondit qu'à ses deux extrémités. */
    & a.id + a {
      border-start-start-radius: 999px; border-end-start-radius: 999px;
      padding-inline-start: calc(var(--step) * 2.25);
    }
    & a:last-of-type {
      border-start-end-radius: 999px; border-end-end-radius: 999px;
      padding-inline-end: calc(var(--step) * 2.25);
    }
    /* On lit un tutoriel, donc on est « dans » Tutoriels sans être
       sur la page qui les liste : un fond plus dense que le repos,
       loin de la pastille pleine que la page exacte garde pour
       elle. La nuance est visuelle seulement — annoncer cette
       entrée comme la page courante à un lecteur d'écran désignerait
       une page où l'on n'est pas. */
    & a.est-section {
      background: rgb(var(--pane) / .92);
      color: var(--ink);
      font-weight: 600;
    }

    & a:not(.id):hover { background: rgb(var(--pane) / .85); color: var(--ink); }
    /* Le survol doit ajouter, jamais retrancher : à .85 il rendait
       l'entrée de section plus pâle qu'au repos. */
    & a.est-section:hover { background: rgb(var(--pane)); }

    /* `aria-current="page"` était posé dans le HTML sans aucun
       rendu : rien n'indiquait la page où l'on se trouve. */
    & a[aria-current="page"] {
      background: var(--cyan-fill); color: var(--on-cyan); font-weight: 700;
    }
    & a[aria-current="page"]:hover { background: var(--cyan-text); color: var(--on-cyan); }
    /* L'anneau de focus du thème est cyan. Sur une pastille
       elle-même remplie de cyan il tombe à 1,2:1 — sous les 3:1 de
       WCAG 1.4.11, et invisible précisément sur l'entrée où l'on
       se trouve. Il prend la couleur du texte de la pastille et
       rentre dedans : dehors, il retomberait sur le fond clair de
       la page et le problème changerait de côté. */
    & a[aria-current="page"]:focus-visible {
      outline-color: var(--on-cyan); outline-offset: -4px;
    }

    /* L'intervalle tombé à zéro, les deux boutons se colleraient
       à la bande. */
    & .theme-btn { margin-inline-start: calc(var(--step) * 2); }
    & .burger { margin-inline-start: calc(var(--step) * 1); }
  }
  @media (prefers-reduced-motion: reduce) {
    .bar a:not(.id) { transition: none; }
  }

  .gauge {
    position: sticky;
    /* Sous la barre, pas derrière : --h-bar est mesurée par le script.
       Le repli couvre le cas où le script n'a pas encore tourné. */
    inset-block-start: calc(var(--h-bar, 5.5rem) + var(--step));
    z-index: 5;
    display: flex; align-items: center; gap: calc(var(--step) * 2);
    padding-block: calc(var(--step) * 1.5);
    padding-inline: calc(var(--step) * 2.5);
    border-radius: 999px;
    font-family: var(--mono); font-size: .6875rem;

    & .dot { flex: none; inline-size: 8px; block-size: 8px; border-radius: 50%; background: var(--state); }
    & .lab { color: var(--ink); font-weight: 600; }
    /* À 0 %, une piste de 900 px de large et 4 px de haut se lisait comme
       un filet de séparation. Elle reste une jauge, mais bornée : c'est
       un indicateur, pas une règle horizontale. */
    & .meter { flex: 0 1 14rem; block-size: 4px; border-radius: 2px; background: oklch(from var(--ink) l c h / .1); overflow: hidden; }
    & .lab { margin-inline-end: auto; }
    & .meter i { display: block; block-size: 100%; inline-size: calc(var(--harden) * 100%); background: var(--state); }
    & .pct { color: var(--ink-soft); font-variant-numeric: tabular-nums; }
  }

  /* ── hero : texte à gauche, baie à droite ───────────────── */
  .hero {
    display: grid;
    gap: calc(var(--step) * 4);
    align-items: center;
    padding-block: calc(var(--step) * 6) calc(var(--step) * 4);
  }

  h1 {
    font-size: clamp(2.75rem, 7cqi, 4.75rem);
    font-weight: 800; line-height: .94; letter-spacing: -.045em;
    margin-block-end: calc(var(--step) * 2.5);
  }
  .role { font-size: 1.125rem; font-weight: 600; color: var(--ink-soft); margin-block-end: calc(var(--step) * 3.5); }

  .actions { display: flex; flex-wrap: wrap; gap: calc(var(--step) * 1.5); margin-block-end: calc(var(--step) * 4); }
  .btn {
    padding-block: calc(var(--step) * 1.75);
    padding-inline: calc(var(--step) * 3.5);
    border-radius: var(--r-ctl);
    font-size: .9375rem; font-weight: 700;
    &.solid { background: var(--cyan-fill); color: var(--on-cyan); border: 1px solid transparent; }
    &.solid:hover { background: var(--cyan-text); }
    &.quiet { color: var(--cyan-text); background: rgb(var(--pane) / .62); border: 1px solid oklch(from var(--cyan) l c h / .42); }
    &.quiet:hover { background: rgb(var(--pane) / .95); }
  }

  .signup {
    padding: calc(var(--step) * 2.5);
    margin-block-end: calc(var(--step) * 3);
    & p { font-size: .9375rem; color: var(--ink-soft); margin-block-end: calc(var(--step) * 1.5); }
    & .row { display: flex; gap: calc(var(--step) * 1.25); flex-wrap: wrap; }
    & input[type=email] {
      flex: 1 1 12rem;
      padding-block: calc(var(--step) * 1.5); padding-inline: calc(var(--step) * 2);
      border-radius: var(--r-ctl);
      border: 1px solid oklch(from var(--ink) l c h / .18);
      background: var(--field); color: var(--ink); font: inherit; font-size: .875rem;
    }
    & button {
      padding-block: calc(var(--step) * 1.5); padding-inline: calc(var(--step) * 3);
      border-radius: var(--r-ctl); border: 1px solid transparent;
      background: var(--cyan-fill); color: var(--on-cyan); font: inherit; font-weight: 700; font-size: .875rem; cursor: pointer;
    }
    & label { display: flex; gap: .625rem; align-items: center; font-size: .8125rem; color: var(--ink-soft); margin-block-start: calc(var(--step) * 1.5); }
    /* La case native mesurait 13×13 px : sous le seuil des 24 px, et
       difficile à viser au doigt comme à la souris. */
    & input[type=checkbox] {
      flex: none;
      /* 24 px : le minimum que WCAG 2.5.8 demande pour une commande. */
      inline-size: 24px; block-size: 24px;
      margin: 0 2px;
      accent-color: var(--cyan-fill);
    }
  }

  .quicklinks {
    display: grid; gap: calc(var(--step) * 1.25);
    /* 13 px pour une rangée de liens qu'on lit vraiment : trop petit.
       Le corps du site est à 16 px, ces raccourcis restent secondaires
       sans descendre sous le seuil de confort. */
    font-size: .9375rem; color: var(--ink-soft);
    & b { font-weight: 600; color: var(--ink); }
    & a { color: var(--cyan-text); font-weight: 600; }
    & a:hover { text-decoration: underline; }
  }

  /* ── bandeau de chiffres ────────────────────────────────── */
  .stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
    gap: calc(var(--step) * 2);
    padding: calc(var(--step) * 3);
    margin-block-end: calc(var(--step) * 8);
    text-align: center;
    & b { display: block; font-size: 1.75rem; font-weight: 800; letter-spacing: -.035em; color: var(--cyan-text); }
    & span { font-size: .75rem; color: var(--ink-soft); }
  }

  /* ── sections courantes ─────────────────────────────────── */
  .sec { padding-block: calc(var(--step) * 5); }
  .sec > h2 { font-size: clamp(1.5rem, 3cqi, 2rem); font-weight: 800; letter-spacing: -.03em; }
  .sec > .sub { color: var(--ink-soft); margin-block: calc(var(--step) * 1) calc(var(--step) * 3.5); max-inline-size: 56ch; }

  .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr)); gap: calc(var(--step) * 2.5); }
  .card {
    padding: calc(var(--step) * 3);
    display: flex; flex-direction: column; gap: calc(var(--step) * 1.25);
    & h3 { font-size: 1.0625rem; font-weight: 700; letter-spacing: -.02em; }
    /* 14 px pour la description d'une carte qu'on lit vraiment avant de
       cliquer : un cran au-dessus se lit sans effort et ne déforme pas
       la grille. */
    & p { font-size: .9375rem; color: var(--ink-soft); line-height: 1.6; }
    & ul { list-style: none; padding-inline-start: 0; display: grid; gap: calc(var(--step) * .75); font-size: .875rem; color: var(--ink-soft); }
    & li::before { content: "▪"; color: var(--cyan-text); margin-inline-end: .5rem; }
    & .tags { margin-block-start: auto; padding-block-start: calc(var(--step) * 1.5); border-block-start: 1px solid oklch(from var(--ink) l c h / .1); font-family: var(--mono); font-size: .625rem; color: var(--ink-faint); }
    & .meta { font-family: var(--mono); font-size: .625rem; color: var(--ink-faint); }
    /* « Voir le projet → » ne mesurait que 13 px de haut : c'est l'appel
       à l'action de la carte, il doit se viser. Le lien n'a pas de classe
       dans le contenu repris, on le prend par sa place. */
    & .more, & .more > a, & > a:last-child {
      font-size: .875rem; font-weight: 700; color: var(--cyan-text);
      display: inline-flex; align-items: center; min-block-size: 24px;
      align-self: flex-start;
    }
  }

  .tutos { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: calc(var(--step) * 2); }
  .tuto {
    padding: calc(var(--step) * 2.5);
    & b { display: block; font-size: 1rem; font-weight: 700; margin-block-end: calc(var(--step) * .5); }
    & span { font-size: .8125rem; color: var(--ink-soft); }
  }

  .about-figs { display: flex; flex-wrap: wrap; gap: calc(var(--step) * 4); margin-block-start: calc(var(--step) * 2.5); }
  .about-figs b { display: block; font-size: 1.5rem; font-weight: 800; letter-spacing: -.03em; color: var(--cyan-text); }
  .about-figs span { font-size: .75rem; color: var(--ink-soft); }

  .contact { padding: calc(var(--step) * 4); text-align: center; }
  .contact h2 { font-size: 1.5rem; font-weight: 800; letter-spacing: -.03em; margin-block-end: calc(var(--step) * 1.5); }
  .contact p { color: var(--ink-soft); max-inline-size: 44ch; margin-inline: auto; margin-block-end: calc(var(--step) * 3); }

  .nl {
    padding: calc(var(--step) * 4);
    margin-block: calc(var(--step) * 5) calc(var(--step) * 8);
    & h2 { font-size: 1.25rem; font-weight: 800; letter-spacing: -.025em; margin-block-end: calc(var(--step) * 1); }
    & p { color: var(--ink-soft); font-size: .9375rem; margin-block-end: calc(var(--step) * 2.5); max-inline-size: 50ch; }
    /* 11 px pour une mention qui engage juridiquement : trop petit pour
       être lu, donc pour être consenti. 14 px est le plancher du texte
       secondaire sur le reste du site. */
    & .fine { font-size: .875rem; color: var(--ink-soft); margin-block-start: calc(var(--step) * 1.5); }
  }

  .note {
    border-block-start: 1px solid oklch(from var(--ink) l c h / .16);
    padding-block: calc(var(--step) * 4) calc(var(--step) * 10);
    & .label { font-family: var(--mono); font-size: .6875rem; color: var(--ink-faint); }
    & h2 { font-size: 1.25rem; font-weight: 800; letter-spacing: -.025em; margin-block: calc(var(--step) * 1.25) calc(var(--step) * 2.5); }
    & dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr)); gap: calc(var(--step) * 2.75) calc(var(--step) * 4); margin-block: 0; align-items: start; }
    & dt { font-weight: 700; color: var(--cyan-text); margin-block-end: calc(var(--step) * .5); font-size: .9375rem; }
    & dd { margin-inline-start: 0; font-size: .875rem; line-height: 1.55; color: var(--ink-soft); }
    /* 12,5 px était la seule valeur hors de l'échelle, coincée entre 12
       et 13 sans raison : un demi-pixel que personne ne perçoit mais qui
       trahit un réglage au coup par coup. */
    & code { font-family: var(--mono); font-size: .8125rem; color: var(--ink); }
  }
}

@layer adapt {
  @container (min-width: 58rem) {
    .hero { grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); }
  }
  /* Dix entrées, une marque et deux boutons réclament 1 060 px. En
     dessous, les libellés insécables s'écrasaient les uns dans les
     autres : mesuré, « Morgann Riu », « À propos » et « Accompagnement
     IA » passaient sur deux lignes dès 940 px, et la barre grandissait
     de 86 à 93 px sans que rien ne le signale. Le bouton prend donc le
     relais bien avant ce point, pas à la largeur d'un téléphone.

     La baie 3D, elle, garde son propre seuil : elle rétrécit pour une
     autre raison et n'a rien à voir avec le nombre d'entrées. */
  @container (max-width: 66rem) {
    .bar a { display: none; }
  }
  @container (max-width: 44rem) {
    .stage { min-block-size: 20rem; }
    .rack { inline-size: 15rem; }
  }

  /* Mouvement réduit = supprimer le MOUVEMENT, pas l'information.
     Le balancement de la baie et le clignotement des voyants sautent ;
     le durcissement, lui, reste : il suit le défilement de la personne,
     ce n'est pas une animation autonome mais une réponse à son geste. */
  @media (prefers-reduced-motion: reduce) {
    .rack, .led, .u { animation: none; }
  }
}

/* ═══ CHROME COMMUN ═══════════════════════════════════════
   Pied de page, lien d'évitement, retour en haut : présents sur
   le site d'origine, donc présents ici.
   ════════════════════════════════════════════════════════ */
@layer layout {
  .skip {
    position: absolute;
    inset-inline-start: -9999px;
    inset-block-start: 0;
    z-index: 20;
    padding: calc(var(--step) * 1.5) calc(var(--step) * 2.5);
    background: var(--cyan-text);
    color: var(--on-cyan);
    font-weight: 700;
    border-radius: 0 0 var(--r-ctl) 0;
  }
  .skip:focus { inset-inline-start: 0; }

  .pied-site {
    margin-block-start: calc(var(--step) * 8);
    padding-block: calc(var(--step) * 5) calc(var(--step) * 4);
    border-block-start: 1px solid oklch(from var(--ink) l c h / .16);
  }
  .pied-grille {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
    gap: calc(var(--step) * 4);
  }
  .pied-marque b { display: block; font-size: 1.0625rem; font-weight: 800; letter-spacing: -.02em; }
  .pied-marque span { display: block; font-size: .8125rem; color: var(--ink-soft); margin-block-end: calc(var(--step) * 1.5); }
  .pied-social { display: flex; gap: calc(var(--step) * 2); font-size: .8125rem; font-weight: 600; }
  .pied-social a { color: var(--cyan-text); }
  .pied-social a:hover { text-decoration: underline; }

  .pied-site h2 {
    font-family: var(--mono);
    font-size: .625rem;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--ink-faint);
    font-weight: 600;
    margin-block-end: calc(var(--step) * 1.5);
  }
  .pied-site ul { list-style: none; margin: 0; padding-inline-start: 0; display: grid; gap: calc(var(--step) * 1); }
  .pied-site li a { font-size: .8125rem; color: var(--ink-soft); }
  .pied-site li a:hover { color: var(--cyan-text); }

  .pied-bas {
    margin-block-start: calc(var(--step) * 4);
    padding-block-start: calc(var(--step) * 2.5);
    border-block-start: 1px solid oklch(from var(--ink) l c h / .1);
    font-size: .75rem;
    color: var(--ink-faint);
  }

  .haut {
    position: fixed;
    inset-block-end: calc(var(--step) * 3);
    inset-inline-end: calc(var(--step) * 3);
    z-index: 10;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    display: grid;
    place-items: center;
    border-radius: 50%;
    background: rgb(var(--pane) / .82);
    border: 1px solid oklch(from var(--ink) l c h / .14);
    box-shadow: 0 6px 18px -8px oklch(from var(--shade) l c h / .5);
    color: var(--ink);
    font-size: 1.125rem;
    opacity: 0;
    pointer-events: none;
    transition: opacity .25s ease;
  }
  .haut.visible { opacity: 1; pointer-events: auto; }

  /* Zones tactiles : 44 px au minimum, seuil d'accessibilité. */
  .bar a, .btn, .pied-site li a {
    min-block-size: 44px;
    display: inline-flex;
    align-items: center;
  }
  /* `.tuto` était dans cette liste : le `inline-flex` la faisait passer
     en rangée, mettant le titre et sa description côte à côte au lieu de
     les empiler. La hauteur minimale se garde sans changer l'axe. */
  .tuto { display: flex; flex-direction: column; justify-content: center; min-block-size: 44px; }
  .bar a { min-block-size: 40px; }
  .pied-site li a { min-block-size: 32px; }

  /* Un lien court reste étroit même haut de 44 px : « EN » ne mesurait
     que 18 px de large. WCAG 2.5.8 demande 24 px dans les deux sens. */
  .bar a, .pied-site li a, .pied-social a { min-inline-size: 24px; justify-content: center; }
  .pied-site li a, .pied-social a { justify-content: flex-start; }

  /* Liens autonomes du contenu repris — listes de pages légales, boutons
     de partage, appels à l'action de fin de carte. Ils ne sont pas au fil
     d'une phrase : la règle des 24 px s'y applique, et 19 px de haut ne
     suffisaient pas.

     Deux familles de cartes coexistent : `.card` sur l'accueil, écrit à
     la main, et `.carte` posée par le balisage sur le contenu repris. */
  .prose li > a:only-child,
  .prose div > a:not(.carte),
  .prose p > a[title],
  .prose .carte > a,
  .prose .carte > span > a {
    display: inline-flex;
    align-items: center;
    min-block-size: 24px;
  }
  /* Un bouton de partage tient parfois en une lettre : « X » ne faisait
     que 11 px de large. La règle des 24 px vaut dans les deux sens. */
  .prose a[title] { min-inline-size: 24px; justify-content: center; }
  /* Rangées d'icônes de partage : l'espacement compte autant que la
     taille, deux cibles collées se manquent l'une pour l'autre. */
  .prose div:has(> a[title]) { display: flex; flex-wrap: wrap; gap: calc(var(--step) * 1.5); }
}

/* ═══ LECTURE ═════════════════════════════════════════════
   Pages de contenu : le texte prime, le décor s'efface.
   ════════════════════════════════════════════════════════ */
@layer layout {
  .page { padding-block: calc(var(--step) * 5) calc(var(--step) * 6); }

  .fil { font-family: var(--mono); font-size: .6875rem; color: var(--ink-faint); margin-block-end: calc(var(--step) * 2); }
  .fil a:hover { color: var(--cyan-text); }

  .page > h1 {
    font-size: clamp(1.875rem, 4.4cqi, 2.75rem);
    font-weight: 800; line-height: 1.04; letter-spacing: -.038em;
    max-inline-size: 22ch; margin-block-end: calc(var(--step) * 4);
  }

  .prose {
    max-inline-size: 68ch; font-size: 1rem; line-height: 1.7; color: var(--ink-soft);

    & h2 { font-size: 1.5rem; font-weight: 800; letter-spacing: -.028em; color: var(--ink);
           margin-block: calc(var(--step) * 5) calc(var(--step) * 1.5); max-inline-size: 28ch; }
    & h3 { font-size: 1.1875rem; font-weight: 700; letter-spacing: -.02em; color: var(--ink);
           margin-block: calc(var(--step) * 3.5) calc(var(--step) * 1.25); }
    & h4 { font-size: 1rem; font-weight: 700; color: var(--ink); margin-block: calc(var(--step) * 3) calc(var(--step) * 1); }
    & p { margin-block: 0 calc(var(--step) * 2.5); }
    & ul, & ol { margin-block: 0 calc(var(--step) * 2.5); padding-inline-start: calc(var(--step) * 3); }
    & li { margin-block-end: calc(var(--step) * .75); }
    & li::marker { color: var(--cyan-text); }
    & a { color: var(--cyan-text); font-weight: 600; text-decoration: underline; text-underline-offset: 2px; }
    & a:hover { color: var(--ink); }
    & strong { color: var(--ink); font-weight: 700; }

    & code { font-family: var(--mono); font-size: .8125rem; background: rgb(var(--pane) / .7);
             border: 1px solid oklch(from var(--ink) l c h / .12); border-radius: 4px; padding: .1em .35em; color: var(--ink); }
    /* Le bloc de code reste sombre dans les deux thèmes : c'est un
       terminal, il ne s'inverse pas avec le reste de la page. */
    & pre { background: var(--code-bg); border-radius: var(--r-card);
            padding: calc(var(--step) * 2.5); overflow-x: auto; margin-block: 0 calc(var(--step) * 3);
            font-size: .8125rem; line-height: 1.6; color: var(--code-fg); }
    & pre code { background: none; border: 0; padding: 0; color: var(--code-fg); }

    & blockquote { margin-inline: 0; padding-inline-start: calc(var(--step) * 2.5);
                   border-inline-start: 3px solid oklch(from var(--cyan) l c h / .5); font-style: italic; }
    & img { max-inline-size: 100%; block-size: auto; border-radius: var(--r-card); margin-block: calc(var(--step) * 2); }
    & table { inline-size: 100%; border-collapse: collapse; font-size: .9375rem; margin-block: 0 calc(var(--step) * 3); }
    & th, & td { text-align: start; padding: calc(var(--step) * 1.25); border-block-end: 1px solid oklch(from var(--ink) l c h / .12); }
    & th { color: var(--ink); font-weight: 700; }
    & hr { border: 0; border-block-start: 1px solid oklch(from var(--ink) l c h / .14); margin-block: calc(var(--step) * 4); }
  }

  .page .pied {
    margin-block-start: calc(var(--step) * 6);
    padding-block-start: calc(var(--step) * 3);
    border-block-start: 1px solid oklch(from var(--ink) l c h / .16);
    display: flex; flex-wrap: wrap; gap: calc(var(--step) * 3); font-size: .875rem;
  }
  .page .pied a {
    color: var(--cyan-text); font-weight: 700;
    /* 22 px : deux pixels sous le seuil des commandes. */
    display: inline-flex; align-items: center; min-block-size: 24px;
  }
  .page .pied a:hover { text-decoration: underline; }
}

/* ═══════════════════════════════════════════════════════════
   CORRECTIFS — défauts relevés par l'audit des pages intérieures
   ═══════════════════════════════════════════════════════════ */
@layer layout {

  /* ── Menu mobile ────────────────────────────────────────
     Le balisage et le script existaient ; la feuille de style
     était muette. Sous 44rem la barre n'affichait donc AUCUN
     lien et le bouton retombait en bouton natif 16x6 px. */
  .burger { display: none; }

  /* ── Bascule de thème ───────────────────────────────────
     Le site d'origine laisse choisir clair ou sombre ; la
     préversion doit en faire autant. Le disque mi-plein est
     le symbole du contraste — il dit ce que fait le bouton
     sans dépendre d'une icône chargée. */
  .theme-btn {
    flex: none;
    inline-size: 44px;
    block-size: 44px;
    display: grid;
    place-content: center;
    padding: 0;
    border: 0;
    border-radius: var(--r-ctl);
    background: none;
    color: var(--ink-soft);
    font-size: 1.0625rem;
    line-height: 1;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
  }
  .theme-btn:hover { color: var(--ink); }
  /* Le glyphe pivote pour signaler le thème actif : plein à
     gauche en clair, à droite en sombre. */
  .theme-btn[aria-pressed="true"] { transform: rotate(180deg); }

  /* ── Blocs dépliants (FAQ) ──────────────────────────────
     37 blocs <details> arrivaient sans style, collés les uns
     aux autres au rendu par défaut du navigateur. */
  .prose details {
    margin-block-end: calc(var(--step) * 2);
    padding: calc(var(--step) * 2) calc(var(--step) * 2.5);
    border: 1px solid oklch(from var(--ink) l c h / .14);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .55);
  }
  .prose details[open] { background: rgb(var(--pane) / .75); }
  .prose summary {
    cursor: pointer;
    font-weight: 700;
    color: var(--ink);
    list-style: none;
    display: flex;
    align-items: baseline;
    gap: calc(var(--step) * 1.25);
    min-block-size: 44px;
  }
  .prose summary::-webkit-details-marker { display: none; }
  .prose summary::before {
    content: "+";
    flex: none;
    font-family: var(--mono);
    color: var(--cyan-text);
    font-size: 1rem;
  }
  .prose details[open] summary::before { content: "−"; }
  .prose details > :not(summary) { margin-block-start: calc(var(--step) * 1.5); }

  /* ── Tableaux ───────────────────────────────────────────
     Sans conteneur défilant, un tableau de plus de trois
     colonnes fait défiler la PAGE entière en largeur. */
  .prose .table-wrap {
    overflow-x: auto;
    margin-block-end: calc(var(--step) * 3);
    border: 1px solid oklch(from var(--ink) l c h / .12);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .5);
  }
  .prose .table-wrap table { margin-block: 0; min-inline-size: 34rem; }
  .prose caption {
    caption-side: bottom;
    padding-block-start: calc(var(--step) * 1.25);
    font-size: .8125rem;
    color: var(--ink-faint);
    text-align: start;
  }

  /* ── Blocs de code ──────────────────────────────────────
     Un lien à l'intérieur d'un pre sombre héritait du cyan
     foncé du texte courant : 2,02:1, illisible. */
  .prose pre a { color: var(--code-link); text-decoration-color: rgb(159 216 242 / .5); }
  .prose pre a:hover { color: var(--code-link-hi); }
  .prose pre { position: relative; overscroll-behavior-x: contain; }
  /* Repère de défilement : sans lui, rien n'indique qu'un bloc
     déborde de 5 fois la largeur de l'écran.
     Posé uniquement sur les blocs qui débordent VRAIMENT — le script
     mesure et pose `.deborde`. Sans cette condition, chaque bloc, même
     tenant en une ligne, annonçait un débordement inexistant : une
     affordance qui ment sur tous les cas use la confiance. */
  .prose pre.deborde::after {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-end: 0;
    inline-size: 2.5rem;
    pointer-events: none;
    background: linear-gradient(90deg, transparent, oklch(from var(--code-bg) l c h / .55));
    border-radius: 0 var(--r-card) var(--r-card) 0;
  }

  /* ── Hiérarchie ─────────────────────────────────────────
     h4 et strong étaient typographiquement identiques. */
  /* Un titre plus petit que le texte qu'il annonce inverse la
     hiérarchie : h4 était à 13 px sous un corps à 16. Il garde sa
     couleur et son interlettrage pour se distinguer d'un `strong`,
     mais reprend une taille de titre. */
  .prose h4 {
    font-family: var(--mono);
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: .02em;
    color: var(--cyan-text);
  }

  /* ── Encadrés du contenu d'origine ──────────────────────
     Les blockquote et les paragraphes d'avertissement
     arrivaient sans fond ni marge. */
  .prose blockquote {
    margin-block-end: calc(var(--step) * 3);
    padding-block: calc(var(--step) * 1.5);
    padding-inline-end: calc(var(--step) * 2);
    background: rgb(var(--pane) / .45);
    border-radius: 0 var(--r-card) var(--r-card) 0;
  }

  /* ── Cartes importées ───────────────────────────────────
     Les pages de liste contiennent des ancres qui englobent
     un titre et un paragraphe entiers : sans règle, tout le
     bloc passait en cyan souligné. */
  .prose a:has(> h2, > h3, > h4, > p, > div) {
    display: block;
    text-decoration: none;
    font-weight: inherit;
    color: inherit;
    margin-block-end: calc(var(--step) * 2);
    padding: calc(var(--step) * 2.5);
    border: 1px solid oklch(from var(--ink) l c h / .12);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .55);
    transition: background .18s ease, transform .18s ease;
  }
  .prose a:has(> h2, > h3, > h4, > p, > div):hover {
    background: rgb(var(--pane) / .85);
    transform: translateY(-2px);
  }
  .prose a:has(> h2, > h3, > h4, > p, > div) :is(h2, h3, h4) {
    margin-block: 0 calc(var(--step) * 1);
    color: var(--ink);
  }
  .prose a:has(> h2, > h3, > h4, > p, > div) p { margin-block-end: 0; }

  /* ── Quiz ───────────────────────────────────────────────
     Les 10 questions vivaient dans un script en ligne de la
     production : l'extraction les emportait avec le reste des
     scripts, et la page n'annonçait plus qu'un questionnaire
     qu'on ne pouvait pas faire. */
  .quiz {
    margin-block-start: calc(var(--step) * 4);
    padding: calc(var(--step) * 3);
    border: 1px solid oklch(from var(--ink) l c h / .14);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .55);
  }
  .quiz-rang {
    margin-block: 0 calc(var(--step) * 1.5);
    font-family: var(--mono); font-size: .625rem;
    letter-spacing: .09em; text-transform: uppercase;
    color: var(--ink-faint);
  }
  .quiz-question {
    margin-block: 0 calc(var(--step) * 2.5);
    font-size: 1.25rem; font-weight: 800; letter-spacing: -.025em;
    color: var(--ink);
    /* `balance`, hérité des titres, coupait l'énoncé juste avant la
       commande pour égaliser ses deux lignes : « Que fait la commande »
       d'un côté, `chmod 755 fichier` de l'autre. Ici la phrase doit
       rester d'un seul tenant tant qu'elle tient. */
    text-wrap: pretty;
  }
  .quiz-question code {
    font-family: var(--mono); font-size: .9em;
    background: rgb(var(--pane) / .7);
    border: 1px solid oklch(from var(--ink) l c h / .12);
    border-radius: 4px; padding: .1em .35em;
    /* Une commande coupée en deux lignes perd les bords de sa boîte aux
       points de coupure : le lecteur voit deux moitiés d'encadré. Elle
       reste donc d'un seul tenant, et si l'écran est vraiment trop
       étroit, chaque morceau garde son cadre complet. */
    white-space: nowrap;
    -webkit-box-decoration-break: clone;
    box-decoration-break: clone;
  }

  .quiz-options { display: grid; gap: calc(var(--step) * 1.25); }
  .quiz-option {
    inline-size: 100%;
    min-block-size: 44px;
    padding: calc(var(--step) * 1.5) calc(var(--step) * 2);
    border: 1px solid oklch(from var(--ink) l c h / .16);
    border-radius: var(--r-ctl);
    background: rgb(var(--pane) / .6);
    color: var(--ink);
    font: inherit; font-size: .9375rem; text-align: start;
    cursor: pointer;
    -webkit-appearance: none; appearance: none;
  }
  .quiz-option:hover:not(:disabled) { border-color: oklch(from var(--cyan) l c h / .55); }
  .quiz-option:disabled { cursor: default; }
  /* Juste ou faux ne se dit pas qu'en couleur : le signe en tête porte
     l'information pour qui ne distingue pas le vert du rouge. */
  .quiz-option.est-juste, .quiz-option.est-faux { font-weight: 700; }
  .quiz-option.est-juste {
    border-color: var(--cyan-fill);
    background: oklch(from var(--cyan) l c h / .16);
  }
  .quiz-option.est-juste::before { content: "✓ "; color: var(--cyan-text); }
  .quiz-option.est-faux {
    border-color: var(--alert);
    background: oklch(from var(--alert) l c h / .12);
  }
  .quiz-option.est-faux::before { content: "✕ "; color: var(--alert); }

  .quiz-verdict {
    margin-block: calc(var(--step) * 2) 0;
    font-size: .875rem; font-weight: 600;
    min-block-size: 1.4em;
  }
  .quiz-verdict.est-juste { color: var(--cyan-text); }
  .quiz-verdict.est-faux { color: var(--ink-soft); }
  .quiz-bilan { margin-block: 0 calc(var(--step) * 2.5); color: var(--ink-soft); }
  .quiz > .btn { margin-block-start: calc(var(--step) * 2.5); }
  .quiz-question:focus-visible { outline: 2px solid var(--cyan-text); outline-offset: 4px; }

  /* ── Page « à propos » ──────────────────────────────────
     Les blocs y arrivent sans classes : une série de compteurs
     s'empilait en huit lignes, quatre liens de section se
     touchaient, et un parcours professionnel se lisait comme
     une suite de paragraphes sans repère de date. */

  /* Compteurs alignés, le nombre porte l'information. */
  /* Une série de compteurs se lit d'un balayage : elle tient sur une
     ligne tant que la place le permet, et se range en colonnes égales
     plutôt que de laisser un chiffre seul en bas. */
  .prose .chiffres {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: calc(var(--step) * 2.5);
    margin-block: calc(var(--step) * 3) calc(var(--step) * 4);
    max-inline-size: 46rem;
  }
  .prose .chiffre { display: flex; flex-direction: column; margin-block-end: 0; }
  .prose .chiffre > :first-child {
    font-size: 1.75rem; font-weight: 800; letter-spacing: -.035em;
    color: var(--cyan-text); line-height: 1.1;
    font-variant-numeric: tabular-nums;
  }
  .prose .chiffre > :last-child { font-size: .875rem; color: var(--ink-soft); }

  /* Quatre liens de section qui se touchaient : ils deviennent des
     boutons distincts, chacun visable. */
  .prose .ancres {
    display: flex;
    flex-wrap: wrap;
    gap: calc(var(--step) * 1.5);
    margin-block: calc(var(--step) * 2) calc(var(--step) * 5);
  }
  .prose .ancres > a {
    display: inline-flex; align-items: center;
    min-block-size: 40px;
    padding-inline: calc(var(--step) * 2.25);
    border: 1px solid oklch(from var(--ink) l c h / .16);
    border-radius: 999px;
    background: rgb(var(--pane) / .5);
    color: var(--cyan-text);
    font-size: .875rem; font-weight: 700;
    text-decoration: none;
  }
  .prose .ancres > a:hover {
    border-color: oklch(from var(--cyan) l c h / .5);
    background: rgb(var(--pane) / .8);
  }

  /* Parcours : la date devient un repère en marge, pas une ligne de
     texte perdue au-dessus du poste. */
  .prose .jalon {
    position: relative;
    margin-block-end: calc(var(--step) * 4);
    padding-inline-start: calc(var(--step) * 3);
    border-inline-start: 2px solid oklch(from var(--cyan) l c h / .35);
  }
  .prose .jalon::before {
    content: "";
    position: absolute;
    inset-inline-start: -5px;
    inset-block-start: .55em;
    inline-size: 8px; block-size: 8px;
    border-radius: 50%;
    background: var(--cyan-fill);
  }
  .prose .jalon-date {
    font-family: var(--mono);
    font-size: .6875rem;
    font-weight: 600;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--ink-faint);
    margin-block-end: calc(var(--step) * .75);
  }
  .prose .jalon :is(h2, h3, h4) { margin-block-start: 0; }
  .prose .jalon h4 { margin-block: calc(var(--step) * .5) calc(var(--step) * 1); }

  /* Compétence chiffrée : une donnée qui se compare doit se voir.
     « Configuration réseau · 90% » arrivait en ligne de monospace grise
     de 10 px — la valeur était là, l'information ne passait pas. */
  .prose .mesures { display: grid; gap: calc(var(--step) * 1.75); margin-block: calc(var(--step) * 2) 0; }
  .prose .mesure {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: baseline;
    gap: calc(var(--step) * .5) calc(var(--step) * 2);
    margin-block-end: 0;
    font-family: inherit;
    font-size: .9375rem;
    letter-spacing: normal;
    text-transform: none;
    color: var(--ink-soft);
  }
  .prose .mesure-nom { font-weight: 600; color: var(--ink); }
  .prose .mesure-val {
    font-family: var(--mono); font-size: .8125rem;
    color: var(--ink-faint); font-variant-numeric: tabular-nums;
  }
  /* La barre occupe toute la largeur, sous les deux libellés. */
  .prose .mesure::after {
    content: "";
    grid-column: 1 / -1;
    block-size: 6px;
    border-radius: 3px;
    background:
      linear-gradient(to right, var(--cyan-fill) 0 calc(var(--val, 0) * 1%),
                      transparent calc(var(--val, 0) * 1%) 100%),
      oklch(from var(--ink) l c h / .1);
  }
  /* Le point médian des étiquettes n'a pas lieu d'être dans une mesure. */
  .prose .mesure > :not(:last-child)::after { content: none; }

  /* Groupes de compétences côte à côte, au lieu de 1 800 px empilés. */
  .prose .groupes {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
    gap: calc(var(--step) * 3);
    margin-block: calc(var(--step) * 3);
  }
  .prose .groupe {
    padding: calc(var(--step) * 3);
    border: 1px solid oklch(from var(--ink) l c h / .12);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .5);
  }
  .prose .groupe-tete {
    display: flex; align-items: center; gap: calc(var(--step) * 1.5);
    margin-block-end: calc(var(--step) * 1);
  }
  .prose .groupe-tete img { inline-size: 32px; block-size: 32px; object-fit: contain; margin: 0; }
  .prose .groupe-tete :is(h2, h3, h4) { margin-block: 0; font-size: 1.0625rem; }

  /* Certifications : chacune est un fait daté, pas un paragraphe. */
  .prose .certifs {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
    gap: calc(var(--step) * 2.5);
    margin-block: calc(var(--step) * 3);
  }
  .prose .certif {
    padding: calc(var(--step) * 3);
    border: 1px solid oklch(from var(--ink) l c h / .12);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .5);
    margin-block-end: 0;
  }
  .prose .certif :is(h2, h3, h4) { margin-block: 0 calc(var(--step) * 1); font-size: 1rem; }
  .prose .certif p { font-size: .9375rem; margin-block-end: calc(var(--step) * 1.5); }
  .prose .certif-annee {
    display: inline-block;
    font-family: var(--mono); font-size: .6875rem; font-weight: 600;
    letter-spacing: .09em;
    color: var(--cyan-text);
  }

  /* Une icône accompagnant une étiquette arrivait avec ses attributs
     `width="200" height="200"` : un bouclier de 200 px posé à côté d'un
     pingouin de 20, dans une ligne de texte de 10 px. La contrainte
     porte sur l'usage — une icône d'étiquette — pas sur le format du
     fichier. */
  .prose .meta-ligne img,
  .prose .chiffre img,
  .prose .ancres img {
    /* Taille fixe, pas relative : ces icônes ont des proportions très
       différentes (un bouclier haut, un pingouin large). Une contrainte
       sur la seule hauteur les laissait à 55, 20 et 15 px côte à côte.
       Une boîte carrée avec `contain` les met à la même échelle
       optique, quel que soit leur format. */
    inline-size: 24px;
    block-size: 24px;
    object-fit: contain;
    margin-block: 0;
    vertical-align: -.35em;
  }
  .prose .meta-ligne { align-items: center; }
  /* Une étiquette illustrée se lit comme un tout : icône et mot ensemble. */
  .prose .meta-ligne > span:has(img) {
    display: inline-flex;
    align-items: center;
    gap: .45em;
  }

  /* Les illustrations d'ouverture de section gardent une taille de
     vignette : elles décorent, elles ne portent pas le propos. */
  .prose > section img[src*="/illustrations/"] {
    max-block-size: 5rem;
    inline-size: auto;
  }

  /* ── Listes : une grille, pas une colonne ───────────────
     `.prose` borne le texte à 68 caractères, ce qui est la bonne mesure
     pour lire un paragraphe. Appliquée à un catalogue, elle empile 66
     tutoriels dans 712 px de large : la page mesurait 32 556 px de haut
     et laissait 440 px de vide à sa droite.

     Un catalogue se balaie, il ne se lit pas ligne à ligne : il reprend
     toute la largeur disponible et se range en colonnes. */
  .prose .liste {
    max-inline-size: none;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
    gap: calc(var(--step) * 2.5);
    align-items: start;
  }
  .prose .liste > * { margin-block-end: 0; }

  /* ── Fiches : trois sections courtes valent une rangée ───
     La page de contact alignait « À propos », « Services »,
     « Disponibilité » et « Me retrouver » les unes sous les autres, sur
     toute la hauteur, pour quatre paragraphes de trois lignes. Quatre
     blocs courts qui se répondent se lisent d'un coup d'œil quand ils
     sont côte à côte. */
  .prose .fiches {
    max-inline-size: none;
    display: grid;
    /* 18 rem et non 15 : à quatre colonnes de 260 px, la fiche
       « Services » et ses huit lignes montait à 460 px pendant que ses
       voisines en faisaient 180. Trois colonnes plus larges rendent la
       rangée lisible d'un bloc. */
    grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
    gap: calc(var(--step) * 2.5);
    align-items: start;
    margin-block: calc(var(--step) * 3);
  }
  /* ── Formulaire de contact ───────────────────────────────
     La page est servie par PHP — jeton de session, tampon
     anti-spam — mais son habillage vit ici, comme celui de
     toute page v2. */
  .prose .formulaire {
    display: grid;
    gap: calc(var(--step) * 2);
    max-inline-size: 40rem;
    padding: calc(var(--step) * 3);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .62);
    border: 1px solid oklch(from var(--ink) l c h / .1);
    margin-block: calc(var(--step) * 3);
  }
  .prose .champ { display: grid; gap: calc(var(--step) * .75); }
  .prose .champ > label { font-size: .875rem; font-weight: 600; }
  .prose .champ abbr[title] { color: var(--cyan-text); text-decoration: none; }
  .prose .formulaire :is(input[type="text"], input[type="email"], input[type="password"], input[type="tel"], input[type="url"], input[type="search"], select, textarea) {
    inline-size: 100%;
    padding-block: calc(var(--step) * 1.5);
    padding-inline: calc(var(--step) * 2);
    border: 1px solid oklch(from var(--ink) l c h / .18);
    border-radius: var(--r-ctl);
    background: var(--field);
    color: var(--ink);
    font: inherit; font-size: .9375rem;
  }
  .prose .formulaire textarea { resize: vertical; min-block-size: 9rem; }
  .prose .case {
    display: flex; gap: .625rem; align-items: center;
    font-size: .875rem; color: var(--ink-soft);
    & input[type="checkbox"] {
      flex: none; inline-size: 24px; block-size: 24px;
      margin: 0; accent-color: var(--cyan-fill);
    }
  }
  .prose .formulaire button { justify-self: start; cursor: pointer; }
  .prose .formulaire button:disabled { opacity: .6; cursor: wait; }

  /* Le pot de miel s'écarte de l'écran sans `display: none` : les
     robots savent reconnaître un champ que le CSS supprime. */
  .prose .hp { position: absolute; inset-inline-start: -9999px; }

  /* Contexte pré-rempli depuis une offre (« Demander ce pack »). */
  .prose .contexte {
    padding: calc(var(--step) * 2) calc(var(--step) * 2.5);
    border-radius: var(--r-ctl);
    background: oklch(from var(--cyan) l c h / .1);
    border: 1px solid oklch(from var(--cyan) l c h / .28);
    font-size: .875rem;
    & p, & li { margin: 0; color: var(--ink-soft); }
    & ul { margin: calc(var(--step)) 0 0; padding-inline-start: 1rem; }
  }

  .prose .form-ok, .prose .form-ko {
    max-inline-size: 40rem;
    padding: calc(var(--step) * 2.5) calc(var(--step) * 3);
    border-radius: var(--r-card);
    margin-block: calc(var(--step) * 3);
  }
  .prose .form-ok {
    background: oklch(from var(--cyan) l c h / .14);
    border: 1px solid oklch(from var(--cyan) l c h / .4);
  }
  .prose .form-ko {
    background: oklch(from var(--alert) l c h / .1);
    border: 1px solid oklch(from var(--alert) l c h / .4);
    & a { color: var(--cyan-text); font-weight: 600; }
  }

  /* ── Vignettes : une illustration, un titre, une phrase ──
     Six d'entre elles empilées sous « Ce que je fais » occupaient
     1 800 px pour six phrases, chacune avec son image de 200 px. */
  .prose .vignettes {
    max-inline-size: none;
    display: grid;
    /* 16 rem donnaient quatre colonnes de 256 px, soit 211 px de texte :
       la description tombait à 28 caractères par ligne sur cinq lignes.
       19 rem ramènent à trois colonnes et une mesure lisible. */
    grid-template-columns: repeat(auto-fit, minmax(min(19rem, 100%), 1fr));
    gap: calc(var(--step) * 2.5);
    align-items: start;
    margin-block: calc(var(--step) * 3);
  }
  .prose .vignette {
    padding: calc(var(--step) * 3);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .55);
    border: 1px solid oklch(from var(--ink) l c h / .1);

    /* L'illustration accompagne, elle ne porte pas le propos : à 200 px
       elle valait deux fois le texte qu'elle illustre. */
    & img { max-block-size: 3.5rem; inline-size: auto; }
    & h3 { font-size: 1rem; font-weight: 700; letter-spacing: -.015em;
           margin-block: calc(var(--step) * 1.5) calc(var(--step) * .75); }
    & p { font-size: .9375rem; color: var(--ink-soft); line-height: 1.6; }
    & > :last-child { margin-block-end: 0; }
  }

  /* ── Offres : deux packs se comparent, ils ne se lisent pas ──
     Les deux packs de la page des formations tenaient sur 700 px de
     texte courant, prix compris. Côte à côte, l'écart se voit. */
  .prose .offres {
    max-inline-size: none;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(21rem, 100%), 1fr));
    gap: calc(var(--step) * 2.5);
    align-items: start;
    margin-block: calc(var(--step) * 3);
  }
  .prose .offre {
    padding: calc(var(--step) * 3);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .62);
    border: 1px solid oklch(from var(--cyan) l c h / .28);

    & > :first-child { margin-block-start: 0; }
    & > :last-child { margin-block-end: 0; }
    & h3 { font-size: 1.125rem; font-weight: 800; letter-spacing: -.02em;
           margin-block: calc(var(--step) * 1.5) calc(var(--step)); }
    & p { font-size: .9375rem; color: var(--ink-soft); }
    & ul { list-style: none; padding-inline-start: 0; display: grid;
           gap: calc(var(--step) * .75); font-size: .875rem; }
    & li { display: flex; gap: .5rem; justify-content: space-between; }
    /* L'appel à l'action d'une offre se vise : c'est un bouton, pas un
       lien au fil du texte. */
    & > a {
      display: inline-flex; align-items: center; justify-content: center;
      min-block-size: 44px;
      padding-inline: calc(var(--step) * 3);
      margin-block-start: calc(var(--step) * 2);
      border-radius: var(--r-ctl);
      background: var(--cyan-fill); color: var(--on-cyan);
      font-weight: 700; font-size: .9375rem;
    }
    & > a:hover { background: var(--cyan-text); }
  }

  /* Les trois montants d'un prix remisé. Sans distinction, « 178,00 € »
     et « 149,00 € » se lisent comme deux prix pour la même chose ; la
     production barre le premier, on fait de même. */
  .prose .prix-ligne {
    display: flex; flex-wrap: wrap; align-items: baseline;
    gap: calc(var(--step) * 1.5);
    margin-block-start: calc(var(--step) * 1.5);
  }
  .prose .prix-avant {
    text-decoration: line-through;
    color: var(--ink-faint);
    font-size: .9375rem;
  }
  .prose .prix {
    font-size: 1.5rem; font-weight: 800; letter-spacing: -.03em;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
  }
  .prose .remise {
    padding: .15rem .5rem;
    border-radius: 999px;
    background: oklch(from var(--cyan) l c h / .18);
    color: var(--cyan-text);
    font-family: var(--mono); font-size: .6875rem; font-weight: 700;
  }

  /* Un frère qui n'est pas une fiche occupait une cellule sans rien y
     mettre : la page de contact gardait, parmi ses quatre fiches, le
     bloc des messages du formulaire dont tous les enfants sont masqués.
     Il décalait la grille d'un cran et ouvrait un trou en haut à gauche.
     La règle de balisage garantit que ces frères-là sont sans contenu
     visible. */
  .prose .fiches > :not(.fiche) { display: none; }

  .prose .fiche {
    padding: calc(var(--step) * 3);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .55);
    border: 1px solid oklch(from var(--ink) l c h / .1);

    & > :first-child {
      margin-block-start: 0;
      font-size: 1rem;
      font-weight: 700;
      letter-spacing: -.015em;
    }
    & > :last-child { margin-block-end: 0; }
    & p { font-size: .9375rem; color: var(--ink-soft); }
    & ul {
      list-style: none;
      padding-inline-start: 0;
      display: grid;
      gap: calc(var(--step) * .75);
      font-size: .875rem;
      color: var(--ink-soft);
    }
    /* La puce en `::before` inline laissait la deuxième ligne repasser
       SOUS elle : « Administration Linux/Windows » revenait à la marge
       au lieu de s'aligner sur son texte. La puce devient une colonne. */
    & li { display: flex; gap: .5rem; }
    & li::before { content: "▪"; color: var(--cyan-text); flex: none; }
    /* Un lien seul dans une fiche est son appel à l'action. */
    & > a:last-child {
      display: inline-flex; align-items: center; min-block-size: 24px;
      color: var(--cyan-text); font-weight: 700; font-size: .875rem;
    }
  }
  /* La barre de filtres commande toute la grille : elle en prend la largeur.
     Elle reste aussi accessible pendant le défilement — sur une page de
     15 000 px, le champ de recherche disparaissait dès la première carte
     et il fallait remonter au sommet pour changer de filtre. */
  .prose .filtres {
    max-inline-size: none;
    position: sticky;
    inset-block-start: calc(var(--h-bar, 5.5rem) + var(--step));
    z-index: 4;
    padding-block: calc(var(--step) * 2);
    /* Le fond déborde de la grille, le contenu reste dessus : la marge
       négative et la marge interne se compensent exactement. Sans elles,
       donner un rayon au bandeau décalait le champ de recherche par
       rapport aux cartes qu'il commande. */
    margin-inline: calc(var(--step) * -2.5);
    padding-inline: calc(var(--step) * 2.5);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .72);
    backdrop-filter: blur(14px) saturate(150%);
    -webkit-backdrop-filter: blur(14px) saturate(150%);
    border: 1px solid oklch(from var(--ink) l c h / .1);
  }

  /* La grille ne peut pas dépasser son parent : c'est `.prose` lui-même
     qui doit lever sa contrainte sur ces pages. Le texte suivi la
     reprend individuellement — un chapô se lit, un catalogue se balaie. */
  .prose:has(.liste), .prose:has(.fiches), .prose:has(.offres),
  .prose:has(.vignettes) { max-inline-size: none; }
  /* Le contenu importé imbrique ses paragraphes dans des sections : on ne
     peut pas viser les seuls enfants directs. On borne donc tout texte
     suivi, sauf celui des cartes — dans une colonne de 19rem, la mesure
     est déjà la bonne. */
  .prose:has(.liste) :is(p, ul, ol, h2, h3, .compteur):not(.liste *),
  .prose:has(.fiches) :is(p, ul, ol, h2, h3, .compteur):not(.fiches *),
  .prose:has(.offres) :is(p, ul, ol, h2, h3, .compteur):not(.offres *),
  .prose:has(.vignettes) :is(p, ul, ol, h2, h3, .compteur):not(.vignettes *) {
    max-inline-size: 68ch;
  }

  /* ── Recherche et filtres des listes ────────────────────
     66 tutoriels et 93 articles s'affichaient en bloc : la
     production les filtre, l'extraction avait perdu le
     dispositif. Reconstruit ici sur les catégories réellement
     présentes dans la page. */
  /* Trois lignes empilées — champ, pastilles, compteur — laissaient le
     champ seul sur 1 104 px et le compteur ouvrait un blanc en bas. Le
     compteur remonte à côté du champ : deux lignes au lieu de trois, et
     le nombre de résultats se lit là où on le cherche, près de la
     commande qui le fait varier. */
  .filtres {
    display: grid;
    grid-template-columns: minmax(0, 28rem) auto;
    align-items: center;
    gap: calc(var(--step) * 1.5) calc(var(--step) * 2);
    margin-block-end: calc(var(--step) * 4);
  }
  .filtres-tags { grid-column: 1 / -1; }

  /* Sous 34 rem, la colonne du compteur écrasait le champ à 100 px de
     large. Chacun reprend alors toute la largeur.

     Préfixé par `.prose` : à spécificité égale, la déclaration de
     `.filtres-compte` qui suit plus bas dans le fichier l'emportait, et
     le compteur restait collé en deuxième colonne. */
  @container (max-width: 34rem) {
    .prose .filtres {
      grid-template-columns: 1fr;
      /* Onze catégories tiennent sur six rangées au téléphone : le
         bandeau fait 314 px, soit 40 % d'un écran de 800. Collé en
         haut, il ne laisse presque rien à lire. Il défile donc avec la
         page — on le retrouve en remontant, ce qui coûte moins que de
         perdre l'écran en permanence. */
      position: static;
    }
    .prose .filtres-compte { grid-column: 1; grid-row: auto; justify-self: start; }
    /* Des pastilles plus serrées font gagner deux rangées. */
    .prose .filtre-tag {
      padding-inline: calc(var(--step) * 1.5);
      font-size: .6875rem;
    }
  }

  .filtres-champ { display: block; }
  .filtres-champ input {
    inline-size: 100%;
    max-inline-size: 28rem;
    min-block-size: 44px;
    padding-block: calc(var(--step) * 1.25);
    padding-inline: calc(var(--step) * 2);
    border: 1px solid oklch(from var(--ink) l c h / .18);
    border-radius: var(--r-ctl);
    background: var(--field);
    color: var(--ink);
    font: inherit;
    font-size: .9375rem;
  }
  .filtres-champ input::placeholder { color: var(--ink-faint); }

  .filtres-tags { display: flex; flex-wrap: wrap; gap: calc(var(--step) * 1); }
  .filtre-tag {
    min-block-size: 36px;
    padding-block: calc(var(--step) * .75);
    padding-inline: calc(var(--step) * 1.75);
    border: 1px solid oklch(from var(--ink) l c h / .16);
    border-radius: 999px;
    background: rgb(var(--pane) / .5);
    color: var(--ink-soft);
    font-family: var(--mono);
    font-size: .625rem;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
  }
  .filtre-tag:hover { color: var(--ink); border-color: oklch(from var(--cyan) l c h / .5); }
  /* L'état actif ne doit pas reposer sur la seule couleur (WCAG 1.4.1).
     Le commentaire l'annonçait, la règle ne le faisait pas : elle ne
     changeait que des couleurs. Le trait épaissi et la coche le disent
     désormais sans dépendre de la perception des teintes. Le padding
     compense la bordure pour que la pastille ne bouge pas. */
  .filtre-tag.est-actif {
    background: var(--cyan-fill);
    border-color: var(--cyan-fill);
    border-width: 2px;
    padding-block: calc(var(--step) * .75 - 1px);
    padding-inline: calc(var(--step) * 1.75 - 1px);
    color: var(--on-cyan);
  }
  .filtre-tag.est-actif::before { content: "✓ "; }

  .filtres-compte {
    margin: 0;
    /* Sans placement explicite, il partait à la troisième ligne : le
       flux de la grille le posait après les pastilles, qui occupent
       toute la largeur. */
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    font-family: var(--mono);
    /* 11 px pour un chiffre qu'on cherche du regard, c'était trop peu :
       il est monté d'un cran maintenant qu'il tient sa propre place. */
    font-size: .75rem;
    color: var(--ink-soft);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  /* Une recherche sans résultat vidait la page en ne laissant qu'une
     ligne de 11 px : un cul-de-sac. Le bloc n'apparaît qu'à ce
     moment-là, et propose la sortie. */
  .filtres-vide {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: calc(var(--step) * 2);
    margin-block: calc(var(--step) * 3) calc(var(--step) * 6);
    padding: calc(var(--step) * 3);
    border: 1px dashed oklch(from var(--ink) l c h / .2);
    border-radius: var(--r-card);
    color: var(--ink-soft);
    font-size: .9375rem;
  }
  .filtres-vide .btn { padding-block: calc(var(--step) * 1.25); }

  /* Une carte qui mène quelque part doit le montrer. Sans cet état,
     deux cartes visuellement jumelles — l'une cliquable, l'autre non —
     laissent croire que la page est à moitié finie. */
  .prose .carte, .card, .tuto {
    transition: background .18s ease, border-color .18s ease, transform .18s ease;
  }
  .prose .carte:hover, .card:hover, .tuto:hover {
    border-color: oklch(from var(--cyan) l c h / .45);
    transform: translateY(-2px);
  }
  .prose .carte:has(a):hover { background: rgb(var(--pane) / .78); }
  @media (prefers-reduced-motion: reduce) {
    .prose .carte, .card, .tuto { transition: none; }
    .prose .carte:hover, .card:hover, .tuto:hover { transform: none; }
  }

  /* Étiquette réservée aux lecteurs d'écran. */
  .visuellement-cache {
    position: absolute;
    inline-size: 1px; block-size: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden; white-space: nowrap;
    clip-path: inset(50%);
  }

  /* ── Chaînes insécables ─────────────────────────────────
     Un chemin (`/etc/systemd/system/nginx.service.d/override.conf`)
     ou une requête (`node_cpu_seconds_total{instance=~"$instance"}`)
     dans du code EN LIGNE poussait la page : mesuré jusqu'à 91 px de
     défilement horizontal sur 5 tutoriels à 375 px.

     La règle s'arrête au code en ligne. Dans un bloc, la ligne doit
     rester entière et défiler : une commande coupée serait copiée
     fausse, ce qui est pire qu'un défilement. */
  .prose code:not(pre code) { overflow-wrap: anywhere; }
  .prose :is(h1, h2, h3, h4) { overflow-wrap: break-word; }

  /* ── Métadonnées des cartes importées ───────────────────
     Le gabarit d'origine les distingue par des classes que
     l'extraction retire ; baliser.py les repose au moment de
     la génération. Sans elles, « Populaire », la catégorie et
     la durée s'empilaient en trois lignes de texte courant,
     indistinctes du corps de la carte. */
  .prose .carte-badge {
    display: inline-block;
    margin-block-end: calc(var(--step) * 1.5);
    padding: .25em .75em;
    border-radius: 999px;
    background: oklch(from var(--cyan) l c h / .16);
    color: var(--cyan-text);
    font-family: var(--mono);
    font-size: .625rem;
    font-weight: 600;
    letter-spacing: .09em;
    text-transform: uppercase;
  }

  .prose .carte-cat {
    display: block;
    margin-block-end: calc(var(--step) * .75);
    font-family: var(--mono);
    font-size: .625rem;
    font-weight: 600;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--ink-faint);
  }

  /* Appel à l'action d'un côté, durée de l'autre : la ligne se
     lit d'un coup d'œil au lieu de s'empiler. */
  .prose .carte-pied {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: calc(var(--step) * 2);
    margin-block-start: calc(var(--step) * 2);
    padding-block-start: calc(var(--step) * 1.5);
    border-block-start: 1px solid oklch(from var(--ink) l c h / .1);
  }
  .prose .carte-pied > span:first-child { color: var(--cyan-text); font-weight: 700; font-size: .8125rem; }
  .prose .carte-pied > span:last-child {
    flex: none;
    font-family: var(--mono); font-size: .6875rem; color: var(--ink-faint);
    font-variant-numeric: tabular-nums;
  }

  /* Compteur : le nombre porte l'information, le mot l'explique. */
  .prose .chiffre {
    display: inline-flex;
    flex-direction: column;
    margin-inline-end: calc(var(--step) * 5);
    margin-block-end: calc(var(--step) * 2);
  }
  .prose .chiffre > span:first-child {
    font-size: 1.75rem; font-weight: 800; letter-spacing: -.035em;
    color: var(--cyan-text); line-height: 1.1;
    font-variant-numeric: tabular-nums;
  }
  .prose .chiffre > span:last-child { font-size: .75rem; color: var(--ink-soft); }

  /* Étiquettes de même rang — catégorie, état, date, durée — sur une
     ligne au lieu d'une par ligne. Les points médians les séparent
     sans ajouter de balise au contenu importé. */
  .prose .meta-ligne {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: calc(var(--step) * 1.25);
    margin-block-end: calc(var(--step) * 1.25);
    font-family: var(--mono);
    font-size: .625rem;
    font-weight: 600;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--ink-faint);
  }
  .prose .meta-ligne > :not(:last-child)::after { content: " ·"; opacity: .55; }

  /* « 93 articles » : le nombre porte l'information. */
  .prose .compteur {
    margin-block-end: calc(var(--step) * 3);
    color: var(--ink-soft);
    font-size: .875rem;
  }
  .prose .compteur > span:first-child,
  .prose .compteur-val {
    font-size: 1.375rem; font-weight: 800; letter-spacing: -.03em;
    color: var(--cyan-text); font-variant-numeric: tabular-nums;
    margin-inline-end: .3em;
  }

  /* Trois arguments chiffrés alignés valent une rangée : empilés en bas
     de la page du quiz, ils se lisaient comme trois phrases de plus. */
  .prose .compteurs {
    display: flex;
    flex-wrap: wrap;
    gap: calc(var(--step) * 2) calc(var(--step) * 5);
    align-items: baseline;
    margin-block: calc(var(--step) * 3);
    padding-block: calc(var(--step) * 2.5);
    border-block: 1px solid oklch(from var(--ink) l c h / .1);
  }
  .prose .compteurs .compteur { margin-block-end: 0; }
  /* Chaque argument devient une colonne : le nombre au-dessus du mot. */
  .prose .compteurs .compteur-val { display: block; margin-inline-end: 0; }

  /* Le blog encadre chaque entrée dans un <article> plutôt qu'une
     ancre : même carte, autre balise. */
  .prose article.carte {
    display: block;
    margin-block-end: calc(var(--step) * 2);
    padding: calc(var(--step) * 3);
    border: 1px solid oklch(from var(--ink) l c h / .12);
    border-radius: var(--r-card);
    background: rgb(var(--pane) / .55);
  }
  .prose article.carte :is(h2, h3, h4) { margin-block: 0 calc(var(--step) * 1); color: var(--ink); }
  .prose article.carte > :last-child { margin-block-end: 0; }

  /* Pied ne contenant qu'un lien : il fait office d'appel à l'action. */
  .prose .carte-pied > a:only-child { color: var(--cyan-text); font-weight: 700; font-size: .8125rem; }

  /* ── Fil d'Ariane ───────────────────────────────────────
     11 px de monospace sur le halo cyan : 4,2:1, sous AA. */
  .fil { color: var(--ink-soft); }

  /* ── Retour en haut ─────────────────────────────────────
     Il restait focalisable au clavier alors qu'il est
     invisible et inerte à la souris. */
  .haut:not(.visible) { visibility: hidden; }
  .prose { scroll-margin-block-start: 5rem; }
}

@layer adapt {
  /* Le menu replié suit le seuil du menu — 66 rem, la largeur en
     dessous de laquelle dix entrées ne tiennent plus sur une ligne. */
  @container (max-width: 66rem) {
    /* La marque n'est pas une entrée de menu : elle reste visible. */
    .bar .id { display: inline-flex; }
    /* `order` garde le bouton sur la première ligne : sans lui,
       étant dernier dans le DOM, il s'enveloppe sous les 10 liens. */
    .bar a:not(.id) { display: none; order: 1; }
    .bar { flex-wrap: wrap; row-gap: 0; }

    .burger {
      display: grid;
      place-content: center;
      gap: 4px;
      inline-size: 44px;
      block-size: 44px;
      padding: 0;
      border: 0;
      border-radius: var(--r-ctl);
      background: none;
      cursor: pointer;
      -webkit-appearance: none;   /* sinon WebKit garde le bouton natif */
      appearance: none;
    }
    .burger span {
      display: block;
      inline-size: 20px;
      block-size: 2px;
      border-radius: 2px;
      background: var(--ink);
    }

    /* La bande n'existe qu'en une ligne. Repliée, ce sont dix
       rangées : le verre repasse sur la barre, que les entrées
       lui rendent. Sans ce fond, le titre de la page défilerait
       à travers la marque et les deux boutons. */
    .bar {
      border-radius: var(--r-card);
      background: rgb(var(--pane) / .62);
      backdrop-filter: blur(18px) saturate(150%);
      -webkit-backdrop-filter: blur(18px) saturate(150%);
      padding-inline: calc(var(--step) * 2);
    }
    .bar a:not(.id) {
      background: none;
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
    }

    /* La règle qui manquait : app.js posait `is-open` dans le vide. */
    .bar.is-open { padding-block-end: calc(var(--step) * 1.5); }
    .bar.is-open {
      background: rgb(var(--pane) / .92);
      backdrop-filter: blur(20px) saturate(160%);
      -webkit-backdrop-filter: blur(20px) saturate(160%);
    }
    .bar.is-open a:not(.id) {
      display: flex;
      flex-basis: 100%;
      min-block-size: 44px;
      align-items: center;
      border-block-start: 1px solid oklch(from var(--ink) l c h / .1);
      border-radius: 0;
      padding-inline: calc(var(--step) * 1.5);
    }
    /* Les seuls repères une fois la pile déroulée : sans eux, la
       règle qui remet les entrées à plat efface aussi les deux
       états, et dix lignes identiques ne disent plus où l'on est.
       La section garde un cran d'écart avec la page exacte. */
    .bar.is-open a[aria-current="page"] {
      background: var(--cyan-fill); color: var(--on-cyan);
    }
    .bar.is-open a.est-section {
      background: oklch(from var(--cyan) l c h / .16);
      color: var(--ink); font-weight: 600;
    }

  }

  /* Ces deux-là tiennent à l'étroitesse de l'écran, pas au nombre
     d'entrées du menu : elles gardent le seuil du téléphone. */
  @container (max-width: 44rem) {
    /* Le bouton flottant recouvrait des liens du contenu. */
    .haut { inset-block-end: calc(var(--step) * 2); inset-inline-end: calc(var(--step) * 2); }
    .page { padding-block-end: calc(var(--step) * 12); }
  }
}
