﻿/* ==========================================================================
   Piscologik — feuille de styles principale

   Parti pris : l'interface reste neutre (noir, blanc, gris) parce que les
   œuvres sont très colorées. Toute couleur d'interface leur ferait concurrence.
   Thème clair uniquement, par choix explicite du client.
   ========================================================================== */

/* --------------------------------------------------------------- Tokens */
:root {
  /* Couleurs */
  --ink: #101010;
  --ink-soft: #4a4a4a;
  --ink-faint: #767676; /* contraste 4.6:1 sur --paper : conforme AA */
  --paper: #ffffff;
  --paper-warm: #f6f5f3;
  --paper-sunk: #ecebe8;
  --line: #e0dfdb;
  --line-strong: #c9c7c2;
  --accent: #d6261f;
  --accent-ink: #a61c17;

  /* Couleur du logo. Le SVG étant appliqué en masque, il suffit de changer
     cette valeur ici (ou de la redéfinir localement) pour le repeindre. */
  --logo-color: var(--ink);

  /* ---------------------------------------------------------------- Polices
     POINT UNIQUE DE RÉGLAGE de la typographie du site.

     Les @font-face sont dans assets/css/fonts.css, généré par `npm run fonts`.
     Pour changer de police :
       1. modifier FONT dans tools/fetch-fonts.mjs
       2. npm run fonts
       3. remplacer le nom ci-dessous
     Rien d'autre n'est à toucher : tout le site passe par ces deux variables. */
  --font-display: "Inter", system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
  --font-body: "Inter", system-ui, -apple-system, "Segoe UI", Arial, sans-serif;

  --step--1: clamp(0.82rem, 0.79rem + 0.15vw, 0.9rem);
  --step-0: clamp(1rem, 0.96rem + 0.2vw, 1.09rem);
  --step-1: clamp(1.2rem, 1.1rem + 0.5vw, 1.45rem);
  --step-2: clamp(1.5rem, 1.3rem + 1vw, 2.1rem);
  --step-3: clamp(1.9rem, 1.5rem + 2vw, 3.1rem);
  --step-4: clamp(2.4rem, 1.7rem + 3.4vw, 4.6rem);

  /* Rythme */
  --gutter: clamp(1rem, 0.6rem + 2vw, 2.5rem);
  /* Espacements des en-têtes de page interne. Un seul réglage pour les fiches
     projet, les articles et les pages éditoriales : sans cela, chaque gabarit
     dérivait et les titres ne tombaient pas à la même hauteur d'une page à
     l'autre. */
  --head-space-top: clamp(2.25rem, 1.5rem + 2.6vw, 3.75rem);
  --head-space-bottom: clamp(1.5rem, 1.1rem + 1.6vw, 2.25rem);
  /* Espace SOUS le trait de séparation. Il dépendait jusque-là du bloc qui suit,
     dont le padding varie d'un gabarit à l'autre : de 1,25rem sur une page
     éditoriale à 5,5rem sur une page de catégorie. */
  --head-space-after: clamp(1.75rem, 1.2rem + 2vw, 3rem);
  /* Espace entre deux blocs de contenu, quels qu'ils soient : deux paragraphes,
     une image et un texte, deux images. Une seule valeur pour tout le site, afin
     que le rythme vertical reste identique d'une page à l'autre. */
  --block-gap: clamp(0.85rem, 0.78rem + 0.3vw, 1.1rem);
  /* Écart après un visuel. Plus large que celui entre deux paragraphes : une
     image est un bloc dense, qui a besoin de respirer avant le texte qui suit. */
  --media-gap: clamp(1.75rem, 1.4rem + 1.4vw, 2.75rem);
  --radius: 2px;
  --max-width: 82rem;
  /* Largeur des colonnes de texte. Au-delà d'environ 50rem, l'œil peine à
     retrouver le début de la ligne suivante : c'est la limite haute utile. */
  --max-text: 48rem;

  --transition: 200ms cubic-bezier(0.2, 0, 0.2, 1);
}

/* ----------------------------------------------------------------- Base */
*,
*::before,
*::after {
  box-sizing: border-box;
}

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

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.6;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.02em;
  margin: 0;
  text-wrap: balance;
}

h1 {
  font-size: var(--step-4);
}
h2 {
  font-size: var(--step-2);
}
h3 {
  font-size: var(--step-1);
}

p,
ul,
ol {
  margin: 0;
}

a {
  color: inherit;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

a:hover {
  color: var(--accent-ink);
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

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

/* ------------------------------------------------------------ Structure */
.wrap {
  width: 100%;
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  padding-block: clamp(2.5rem, 1.5rem + 4vw, 5.5rem);
}

.section--tight {
  padding-block: clamp(1.75rem, 1rem + 2.5vw, 3rem);
}

/**
 * Dernière section avant le pied de page.
 *
 * Son padding inférieur s'ajoute à celui du pied de page : au maximum de la
 * plage, cela faisait près de neuf rem de blanc d'un seul tenant. On ne réduit
 * que le bas · l'espacement du haut, lui, sépare la section de la précédente
 * et reste nécessaire.
 */
.section--flush-end {
  padding-block-end: clamp(1.5rem, 1rem + 1.5vw, 2.75rem);
}

.section--sunk {
  background: var(--paper-warm);
}

/**
 * Trait entre deux sections qui partagent le même fond.
 *
 * Il est tracé par un pseudo-élément placé DANS le conteneur, et non par une
 * bordure sur la section : celle-ci irait d'un bord à l'autre de l'écran, alors
 * que tous les autres traits du site s'arrêtent à la largeur du contenu.
 *
 * Le padding supérieur est annulé, l'espacement venant du padding inférieur de
 * la section précédente puis de la marge du trait.
 */
.section--divided {
  padding-block-start: 0;
}

/* Même nuance que les autres filets du site, pour ne pas créer deux gris, et
   même espacement sous le trait que sur les pages internes. */
.section--divided > .wrap::before {
  content: "";
  display: block;
  border-top: 1px solid var(--line);
  margin-block-end: var(--head-space-after);
}

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

.skip-link {
  position: absolute;
  left: 0.5rem;
  top: -4rem;
  z-index: 100;
  background: var(--ink);
  color: var(--paper);
  padding: 0.7rem 1.1rem;
  border-radius: var(--radius);
  transition: top var(--transition);
}

.skip-link:focus {
  top: 0.5rem;
  color: var(--paper);
}

/* --------------------------------------------------------------- Header */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: 4.25rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  white-space: nowrap;
}

/**
 * Logo appliqué en MASQUE : le SVG découpe un aplat de couleur.
 *
 * Conséquence pratique : changer la couleur du logo ne demande qu'une
 * déclaration `--logo-color`, sans second fichier ni filtre. Le SVG reste un
 * fichier externe, donc chargé une seule fois et mis en cache, contrairement à
 * un SVG recopié dans chaque page.
 */
.brand__logo {
  display: block;
  height: 2.4rem;
  /* Ratio exact du viewBox (1926 × 847) : aucun décalage au chargement. */
  aspect-ratio: 1926 / 847;
  background-color: var(--logo-color, var(--ink));
  -webkit-mask: url("/images/ui/logo.svg") no-repeat center / contain;
  mask: url("/images/ui/logo.svg") no-repeat center / contain;
  transition: background-color var(--transition);
}

@media (max-width: 46rem) {
  .brand__logo {
    height: 2rem;
  }
}

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

/* Sur fond sombre, une seule déclaration suffit à repeindre le logo. */
.site-footer .brand__logo {
  --logo-color: #fff;
  height: 3rem;
}

/**
 * Les rubriques occupent toute la hauteur du bandeau (`align-items: stretch`),
 * et leur trait descend d'un pixel sous celui-ci (`margin-block-end: -1px`).
 *
 * Le trait rouge de la rubrique active vient ainsi exactement sur la ligne grise
 * de séparation du bandeau, dont il prend la place sur sa largeur, à la manière
 * d'un onglet. Sans cela, il flottait quelques pixels plus haut.
 */
/**
 * `margin-inline-start: auto` plutôt que de compter sur le `space-between` du
 * conteneur : depuis que le groupe Instagram suit le menu, celui-ci se serait
 * retrouvé centré entre le logo et lui. La marge automatique absorbe tout
 * l'espace libre à gauche et maintient les deux blocs groupés à droite.
 */
.nav {
  display: flex;
  align-items: stretch;
  gap: clamp(0.75rem, 0.2rem + 1.4vw, 1.9rem);
  margin-inline-start: auto;
}

.nav > a {
  display: flex;
  align-items: center;
  font-size: var(--step--1);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-soft);
  border-bottom: 2px solid transparent;
  margin-block-end: -1px;
  transition: color var(--transition), border-color var(--transition);
}

.nav > a:hover,
.nav > a[aria-current="page"] {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

/**
 * Ce qui reste visible quand le menu est replié : Instagram aujourd'hui, le
 * sélecteur de langue demain.
 *
 * Le groupe vit HORS du <nav>. Sur grand écran il le prolonge, la marge
 * automatique de `.nav` repoussant l'ensemble à droite ; sur mobile, où le menu
 * quitte le flux, il reste à côté du bouton « Menu ».
 */
.header__extras {
  display: flex;
  align-items: center;
  gap: 0.9rem;
}

.nav__social {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  color: var(--ink-soft);
  transition: color var(--transition);
}

.nav__social:hover {
  color: var(--accent-ink);
}

.nav__social .social__icon {
  width: 1.25rem;
  height: 1.25rem;
}

/**
 * Bouton d'ouverture du menu, visible en dessous de 46rem.
 *
 * `color` doit être posé explicitement : `font: inherit` ne transmet que la
 * police, pas la couleur, et un <button> sans couleur déclarée prend celle du
 * navigateur · le bleu système sur iOS, seule tache bleue de tout le site.
 *
 * `--accent-ink` plutôt que `--accent` : c'est le rouge foncé que le site
 * emploie partout pour du texte, et il garde un contraste suffisant sur fond
 * clair, là où le rouge vif est réservé aux traits et aux aplats.
 */
.nav__toggle {
  display: none;
  /*
   * `inline-flex` avec `line-height: 1` : la hauteur de la boîte colle alors
   * au mot, et le centrage vertical du conteneur tombe juste. En `block`, le
   * bouton héritait de l'interligne du corps de texte, qui ajoutait un blanc
   * inégal au-dessus et en dessous et décalait le mot vers le bas.
   */
  align-items: center;
  line-height: 1;
  background: none;
  color: var(--ink);
  border: 0;
  /*
   * Le mot seul, sans cadre. Le padding reste généreux malgré la disparition
   * du contour : il ne se voit plus, mais il fait la surface tactile, et une
   * cible de moins de 44 px se rate une fois sur trois au pouce.
   */
  padding: 0.6rem 0.25rem;
  font: inherit;
  font-size: var(--step-0);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  cursor: pointer;
  /* Sans cela, un appui pose un voile bleuté sur Android et iOS. */
  -webkit-tap-highlight-color: transparent;
}

/*
 * Pas de changement de couleur à l'ouverture : le menu qui se déplie est un
 * retour bien plus clair, et repasser le mot en rouge le ferait lire comme une
 * alerte. Le survol ne concerne de toute façon que les rares pointeurs sur un
 * écran étroit.
 */
.nav__toggle:hover {
  color: var(--accent-ink);
}

/* ------------------------------------------------- Sous-menu Portfolio
   Construit sur <details> : l'ouverture au clic est native, donc le sous-menu
   reste utilisable même sans JavaScript. */
.nav__group {
  position: relative;
  display: flex;
}

/* Même alignement que les autres rubriques : hauteur pleine et trait posé sur
   la ligne du bandeau. */
.nav__group > summary {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--step--1);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-soft);
  border-bottom: 2px solid transparent;
  margin-block-end: -1px;
  cursor: pointer;
  list-style: none; /* retire le triangle par défaut */
  transition: color var(--transition), border-color var(--transition);
}

/* Safari conserve son propre marqueur sans cette règle. */
.nav__group > summary::-webkit-details-marker {
  display: none;
}

/* Chevron maison, pivoté à l'ouverture. */
.nav__group > summary::after {
  content: "";
  width: 0.42em;
  height: 0.42em;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-0.1em, -0.1em);
  transition: transform var(--transition);
}

.nav__group[open] > summary::after {
  transform: rotate(-135deg) translate(-0.15em, -0.15em);
}

.nav__group > summary:hover,
.nav__group[open] > summary,
.nav__group.is-current > summary {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

.nav__group > summary:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.nav__submenu {
  list-style: none;
  margin: 0;
  padding: 0.4rem;
}

/* `text-decoration: none` est indispensable ici : la règle de la navigation ne
   vise que les enfants directs (`.nav > a`), pour l'alignement du trait actif,
   et ne couvre donc pas les entrées du sous-menu. */
.nav__submenu a {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 0.6rem 0.8rem;
  text-decoration: none;
  color: var(--ink-soft);
  border-bottom: 0;
  border-radius: var(--radius);
  white-space: nowrap;
  transition: background-color var(--transition), color var(--transition);
}

.nav__submenu a:hover,
.nav__submenu a[aria-current="page"] {
  background: var(--paper-warm);
  color: var(--ink);
  border-bottom: 0;
}

.nav__count {
  font-size: 0.75em;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* Au-dessus de la rupture mobile, le sous-menu se superpose à la page. */
@media (min-width: 46.001rem) {
  .nav__submenu {
    position: absolute;
    top: calc(100% + 0.5rem);
    left: 50%;
    transform: translateX(-50%);
    z-index: 60;
    min-width: 15rem;
    background: var(--paper);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: 0 12px 28px rgb(0 0 0 / 0.1);
  }
}

@media (max-width: 46rem) {
  .nav__toggle {
    display: inline-flex;
    /*
     * Logo, puis « Menu », puis Instagram. Le menu ayant quitté le flux, la
     * marge automatique portée par le premier des deux absorbe l'espace libre
     * et maintient le couple groupé à droite.
     */
    order: 1;
    margin-inline-start: auto;
  }

  .header__extras {
    order: 2;
  }

  .nav {
    display: none;
    position: absolute;
    inset-inline: 0;
    top: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--paper);
    border-bottom: 1px solid var(--line);
    padding: 0.5rem var(--gutter) 1.25rem;
  }

  .nav[data-open="true"] {
    display: flex;
  }

  /* Le menu déplié est une colonne : chaque rubrique redevient une ligne, et le
     décalage d'un pixel qui alignait le trait sur le bandeau n'a plus lieu d'être. */
  .nav > a {
    padding-block: 0.85rem;
    border-bottom: 1px solid var(--line);
    margin-block-end: 0;
  }

  .nav > a:hover,
  .nav > a[aria-current="page"] {
    border-bottom-color: var(--line);
    color: var(--accent-ink);
  }

  /* Sur mobile, le sous-menu se déplie dans le flux plutôt que de se superposer :
     un panneau flottant serait vite plus haut que l'écran. */
  .nav__group {
    display: block;
  }

  .nav__group > summary {
    padding-block: 0.85rem;
    border-bottom: 1px solid var(--line);
    margin-block-end: 0;
  }

  /* Aucun trait sous les sous-rubriques : seule la rubrique parente en porte un,
     ce qui suffit à séparer les blocs sans alourdir le menu. */
  .nav__submenu {
    padding: 0.25rem 0 0.5rem 0.75rem;
  }

  .nav__submenu a {
    padding: 0.6rem 0.4rem;
    border-bottom: 0;
  }
}

/* ----------------------------------------------------------------- Hero */
.hero {
  position: relative;
  display: grid;
  align-items: end;
  min-height: clamp(24rem, 62vh, 40rem);
  padding-block: clamp(2rem, 1rem + 5vw, 5rem);
  background: var(--paper-sunk);
  overflow: hidden;
  isolation: isolate;
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -2;
}

.hero__media img,
.hero__media picture {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/**
 * Parallaxe du bandeau d'accueil.
 *
 * Pilotée par le défilement en CSS pur (`animation-timeline: scroll()`), donc
 * sans écouteur JavaScript : l'animation tourne sur le fil de composition du
 * navigateur et ne provoque aucun recalcul de mise en page.
 *
 * Trois garde-fous :
 *  - `@supports` : les navigateurs qui ignorent ces animations affichent une
 *    image fixe, ce qui reste parfaitement correct ;
 *  - `prefers-reduced-motion` : aucun mouvement pour qui en a fait la demande ;
 *  - un léger agrandissement, sans lequel le déplacement découvrirait un bord
 *    vide en haut ou en bas de l'image.
 */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .hero__media img {
      will-change: transform;
      animation: hero-parallax linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 100vh;
    }

    @keyframes hero-parallax {
      from {
        transform: translateY(0) scale(1.14);
      }
      to {
        transform: translateY(12%) scale(1.14);
      }
    }
  }
}

.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(to top, rgb(0 0 0 / 0.78) 0%, rgb(0 0 0 / 0.35) 45%, rgb(0 0 0 / 0.1) 100%);
}

.hero__content {
  color: #fff;
  max-width: 56rem;
}

.hero__eyebrow {
  font-size: var(--step--1);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  opacity: 0.9;
  margin-bottom: 0.8rem;
}

.hero__title {
  color: #fff;
  text-shadow: 0 2px 24px rgb(0 0 0 / 0.35);
}

/* Élargi pour que la phrase d'accroche tienne sur trois lignes plutôt que
   quatre. `text-wrap: pretty` évite qu'un mot isolé finisse seul en dernière
   ligne. */
.hero__text {
  margin-top: 1rem;
  font-size: var(--step-1);
  max-width: 50rem;
  text-wrap: pretty;
  opacity: 0.94;
}

/* Crédit de l'œuvre affichée en bandeau, discret et en retrait. */
.hero__credit {
  position: absolute;
  right: var(--gutter);
  bottom: 0.75rem;
  z-index: 1;
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  color: #fff;
  opacity: 0.72;
  transition: opacity var(--transition);
}

.hero__credit:hover {
  opacity: 1;
}

.hero__credit a {
  color: #fff;
  text-decoration: none;
  border-bottom: 1px solid rgb(255 255 255 / 0.45);
  padding-bottom: 1px;
}

.hero__credit a:hover {
  color: #fff;
  border-bottom-color: #fff;
}

@media (max-width: 46rem) {
  /* Sous le texte d'accroche, le crédit gênerait la lecture sur petit écran. */
  .hero__credit {
    display: none;
  }
}

/* Variante sobre, sans image de fond (pages éditoriales). */
/**
 * Le trait est tracé par un pseudo-élément, et non par la bordure du bloc.
 *
 * Posée sur `.page-head`, qui est un `.wrap`, la bordure courait jusqu'au bord
 * des gouttières. Le pseudo-élément, lui, est un enfant : il s'arrête à la
 * largeur du contenu, exactement comme le trait des fiches projet, pris comme
 * référence pour tout le site.
 */
.page-head {
  padding-block: var(--head-space-top) 0;
}

.page-head::after {
  content: "";
  display: block;
  margin-block-start: var(--head-space-bottom);
  border-bottom: 1px solid var(--line);
}

/**
 * Espace uniforme sous le trait de séparation.
 *
 * Le bloc qui suit l'en-tête n'est pas le même selon le gabarit (`.section`,
 * `.section--tight`…) et chacun apportait son propre padding : le titre ne
 * tombait donc pas à la même distance du trait d'une page à l'autre. On impose
 * ici la même valeur à tous.
 *
 * `:has()` permet de viser aussi le bloc qui suit la fiche technique d'un
 * projet, dont le trait bas joue le même rôle.
 */
.page-head + *,
.wrap:has(> .specs) + * {
  padding-block-start: var(--head-space-after);
}

.page-head__lead {
  margin-top: 1rem;
  font-size: var(--step-1);
  color: var(--ink-soft);
  max-width: var(--max-text);
}

/* ------------------------------------------------------ Skeleton d'attente
   Un dégradé animé occupe l'emplacement de l'image pendant son chargement.

   Il est posé en FOND du conteneur, jamais en masquant l'image : une fois
   chargée, celle-ci le recouvre simplement. Conséquence importante, si le
   JavaScript ne s'exécute pas, les images restent visibles — l'inverse
   (image en opacity 0 révélée par JS) rendrait le site vide en cas d'échec.

   Le JS ne sert qu'à couper l'animation une fois l'image arrivée, pour ne pas
   laisser tourner un calcul inutile. */
@keyframes skeleton-shimmer {
  from {
    background-position: 200% 0;
  }
  to {
    background-position: -200% 0;
  }
}

.card__media,
.post-item__media,
.gallery__item,
.hero__media {
  background-color: var(--paper-sunk);
  background-image: linear-gradient(
    100deg,
    var(--paper-sunk) 20%,
    var(--paper-warm) 40%,
    var(--paper-sunk) 60%
  );
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.6s linear infinite;
}

/* Image arrivée : plus rien à animer. */
.is-loaded {
  animation: none;
  background-image: none;
}

@media (prefers-reduced-motion: reduce) {
  .card__media,
  .post-item__media,
  .gallery__item,
  .hero__media {
    animation: none;
  }
}

/* --------------------------------------------------------------- Grille */
.grid {
  display: grid;
  gap: clamp(0.75rem, 0.4rem + 1.2vw, 1.5rem);
  grid-template-columns: repeat(auto-fill, minmax(min(19rem, 100%), 1fr));
}

/**
 * Grille à 3 colonnes fixes.
 *
 * Utilisée par le bloc « Explorer par catégorie » : avec six entrées, on obtient
 * deux lignes de trois, plutôt qu'une répartition variable selon la largeur.
 * En dessous de 46rem, on repasse à deux colonnes puis à une seule.
 */
.grid--3 {
  grid-template-columns: repeat(3, 1fr);
}

@media (max-width: 60rem) {
  .grid--3 {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 34rem) {
  .grid--3 {
    grid-template-columns: 1fr;
  }
}

/**
 * Quatre colonnes égales.
 *
 * Quatre rubriques tiennent confortablement sur une ligne dans la largeur du
 * conteneur, là où les six d'avant la refonte de septembre 2026 imposaient une
 * bande pleine largeur et un palier intermédiaire.
 *
 * Sous 60rem on passe à deux colonnes, et on s'y tient jusqu'au mobile : une
 * carte par ligne étirerait la section sur quatre écrans.
 */
.grid--4 {
  grid-template-columns: repeat(4, 1fr);
}

@media (max-width: 60rem) {
  .grid--4 {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* --------------------------------------------------------------- Cartes */
/**
 * Les deux couches de la carte se superposent en GRILLE, et non par
 * positionnement absolu.
 *
 * C'est ce qui rend la vignette entière cliquable : le lien s'étire sur son
 * plus proche ancêtre positionné. Tant que `.card__body` était en
 * `position: absolute`, c'est lui qui servait de référent, et seule la zone de
 * texte réagissait au clic.
 */
.card {
  position: relative;
  display: grid;
  grid-template-areas: "stack";
  background: var(--paper-sunk);
  overflow: hidden;
  border-radius: var(--radius);
}

.card__media {
  grid-area: stack;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--paper-sunk);
}

.card__media img,
.card__media picture {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 450ms cubic-bezier(0.2, 0, 0.2, 1);
}

.card:hover .card__media img {
  transform: scale(1.045);
}

/**
 * `position: relative` + `z-index` sont indispensables ici.
 *
 * Au survol, l'image reçoit un `transform`, ce qui crée un contexte
 * d'empilement et la ferait passer devant un bloc resté en flux normal : le
 * titre blanc disparaîtrait derrière la photo. Le z-index le maintient au-dessus.
 *
 * Ce bloc peut être positionné sans risque, car le lien de couverture n'est plus
 * imbriqué dedans mais placé en enfant direct de la carte.
 */
.card__body {
  grid-area: stack;
  align-self: end;
  position: relative;
  z-index: 2;
  padding: 1.5rem 1.1rem 1rem;
  color: #fff;
  background: linear-gradient(to top, rgb(0 0 0 / 0.82), rgb(0 0 0 / 0));
}

.card__title {
  font-size: var(--step-1);
  line-height: 1.15;
  color: #fff;
}

.card__meta {
  font-size: var(--step--1);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  opacity: 0.85;
  margin-top: 0.3rem;
}

/* Le lien recouvre toute la vignette, image comprise. Il vient en dernier dans
   le DOM et passe au-dessus de tout : c'est lui qui reçoit le clic, où qu'on
   pointe dans la carte. */
.card__link {
  grid-area: stack;
  position: relative;
  z-index: 3;
  text-decoration: none;
  color: inherit;
}

.card__link:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: -3px;
}

/* --------------------------------------------------------------- Boutique
   Trois articles en grille plutôt qu'un carrousel : tout est visible d'un coup
   d'œil, sans script, et le contenu reste accessible au clavier comme aux
   moteurs de recherche. La grille se réorganise seule sur petit écran. */
.shop-grid {
  margin: 1.5rem 0 0;
  padding: 0;
}

/* `position: relative` sert d'ancrage au lien qui recouvre la vignette : aucun
   descendant n'étant positionné, c'est bien la carte entière qui est cliquable. */
.shop-card {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color var(--transition), transform var(--transition);
}

.shop-card__link {
  position: absolute;
  inset: 0;
  z-index: 2;
}

.shop-card__link:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: -3px;
}

.shop-card:hover {
  cursor: pointer;
}

.shop-card:hover {
  border-color: var(--line-strong);
  transform: translateY(-2px);
}

.shop-card__media {
  aspect-ratio: 4 / 3;
  background: var(--paper-sunk);
  overflow: hidden;
}

.shop-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.shop-card__body {
  padding: 1rem 1.1rem 1.2rem;
}

.shop-card__title {
  font-size: var(--step-1);
  line-height: 1.2;
  transition: color var(--transition);
}

/* Le titre n'est plus un lien — c'est la carte entière qui l'est — mais il en
   garde le retour visuel : il passe au rouge au survol comme à la tabulation. */
.shop-card:hover .shop-card__title,
.shop-card:focus-within .shop-card__title {
  color: var(--accent-ink);
}

.shop-card__price {
  margin-top: 0.4rem;
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  color: var(--ink-faint);
}

.section__intro {
  max-width: var(--max-text);
  margin-top: 0.75rem;
  color: var(--ink-soft);
}

/* ----------------------------------------------------- Cartes catégories */
.cat-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 13rem;
  padding: 1.25rem;
  background: var(--paper-warm);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  text-decoration: none;
  transition: background var(--transition), border-color var(--transition), transform var(--transition);
}

.cat-card:hover {
  background: var(--paper-sunk);
  border-color: var(--line-strong);
  transform: translateY(-2px);
  color: var(--ink);
}

/**
 * Variante à six de front.
 *
 * La hauteur minimale est levée : les cartes s'alignent sur la plus haute de la
 * rangée, la grille s'en chargeant. Le texte descriptif tient grâce à la pleine
 * largeur de la bande, qui laisse environ 290px par carte sur un grand écran.
 */
.cat-card--compact {
  min-height: 0;
  padding: 1.1rem 1rem 1.2rem;
}

.cat-card--compact .cat-card__title {
  font-size: var(--step-1);
  text-wrap: balance;
}

.cat-card--compact .cat-card__text {
  font-size: var(--step--1);
  line-height: 1.45;
}

/* Sur mobile, les cartes se réduisent à deux colonnes : la description y
   occuperait plus de place que le nom qu'elle accompagne. */
@media (max-width: 46rem) {
  .cat-card--compact .cat-card__text {
    display: none;
  }
}

.cat-card__count {
  font-size: var(--step--1);
  color: var(--ink-faint);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.cat-card__title {
  font-size: var(--step-2);
  margin-top: 0.35rem;
}

.cat-card__text {
  margin-top: 0.5rem;
  font-size: var(--step--1);
  color: var(--ink-soft);
}

/* ------------------------------------------------------- Fiche projet */
.project-head {
  padding-block: var(--head-space-top) var(--head-space-bottom);
}

.project-head__eyebrow {
  font-size: var(--step--1);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 0.75rem;
}

.project-head__eyebrow a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid var(--line-strong);
}

.project-head__eyebrow a:hover {
  color: var(--accent-ink);
  border-bottom-color: currentColor;
}

.project-lead {
  margin-top: 1.25rem;
  font-size: var(--step-1);
  color: var(--ink-soft);
  max-width: var(--max-text);
}

/**
 * Les traits de séparation s'arrêtent à la largeur du CONTENU, gouttières
 * exclues : c'est la largeur de référence retenue pour tout le site.
 *
 * Ils sont donc portés par la liste elle-même, et non par le conteneur `.wrap`
 * dont la bordure courrait jusqu'au bord des gouttières. Les en-têtes de page
 * obtiennent le même résultat via un pseudo-élément (voir `.page-head::after`).
 */
.specs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: 1.25rem 2rem;
  margin: 0;
  padding-block: var(--head-space-bottom);
  border-block: 1px solid var(--line);
}

.specs div {
  margin: 0;
}

.specs dt {
  font-size: var(--step--1);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.specs dd {
  margin: 0.25rem 0 0;
  font-weight: 600;
}

/* ------------------------------------------------------------- Galerie */
/* Même écart entre deux images qu'entre deux blocs de texte. */
.gallery {
  display: grid;
  gap: var(--block-gap);
}

.gallery__item {
  margin: 0;
  background: var(--paper-warm);
}

.gallery__item img {
  width: 100%;
  height: auto;
}

.gallery__item figcaption {
  margin-top: 0.6rem;
  font-size: var(--step--1);
  color: var(--ink-faint);
}

/* Au-delà de deux images, on passe en deux colonnes sur grand écran. */
@media (min-width: 60rem) {
  .gallery--multi {
    grid-template-columns: 1fr 1fr;
  }
  .gallery--multi .gallery__item:first-child {
    grid-column: 1 / -1;
  }
}

/* --------------------------------------------------------------- Vidéos
   Façade de lecteur : rien n'est chargé tant que le visiteur n'a pas cliqué,
   donc aucun cookie tiers à l'arrivée sur la page. */
.videos {
  display: grid;
  gap: 1.5rem;
  margin-top: 1.5rem;
}

.video {
  margin: 0;
}

.video__facade,
.video__frame {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  aspect-ratio: 16 / 9;
  width: 100%;
  background: var(--ink);
  color: #fff;
  border-radius: var(--radius);
  text-decoration: none;
  overflow: hidden;
}

.video__facade:hover {
  color: #fff;
}

.video__play {
  width: 4rem;
  height: 4rem;
  border-radius: 50%;
  background: var(--accent);
  display: grid;
  place-items: center;
  transition: transform var(--transition);
  flex: none;
}

.video__play::after {
  content: "";
  border-style: solid;
  border-width: 0.7rem 0 0.7rem 1.15rem;
  border-color: transparent transparent transparent #fff;
  margin-left: 0.25rem;
}

.video__facade:hover .video__play {
  transform: scale(1.08);
}

.video__label {
  font-size: var(--step--1);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  max-width: 60%;
}

.video__frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* ----------------------------------------------------------- Pagination */
/**
 * Navigation d'une fiche à l'autre.
 *
 * Trois zones : précédent, retour au listing, suivant. Les extrémités gardent
 * leur place même lorsqu'il n'y a pas de voisin (un <span> vide tient la
 * colonne), afin que le lien central ne se décale pas d'une page à l'autre.
 */
.pager {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: start;
  gap: 1rem 1.5rem;
  padding-block: 2rem;
  border-top: 1px solid var(--line);
}

.pager a {
  text-decoration: none;
  color: var(--ink-soft);
}

.pager a:hover {
  color: var(--accent-ink);
}

.pager__link {
  display: grid;
  gap: 0.25rem;
}

.pager__link--next {
  text-align: right;
}

.pager__dir {
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.pager__title {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.2;
}

.pager__all {
  align-self: center;
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--line-strong);
  padding-bottom: 2px;
}

.pager__all:hover {
  border-bottom-color: var(--accent);
}

@media (max-width: 46rem) {
  /**
   * Sur mobile, les trois zones s'empilent : le titre du voisin resterait
   * illisible sur une colonne du tiers de l'écran.
   *
   * L'ordre est rétabli avec `order` : « précédent » puis « suivant » se
   * suivent, et le retour au listing passe en dernier. Dans le flux du
   * document, il est placé entre les deux — position logique sur trois colonnes,
   * mais déroutante une fois empilée, le lien coupant la paire qu'il sépare.
   *
   * Chaque voisin devient une ligne pleine largeur soulignée d'un filet, plus
   * facile à viser au doigt qu'un texte centré.
   */
  .pager {
    grid-template-columns: 1fr;
    gap: 0;
    text-align: left;
    padding-block: 1.25rem 0;
  }

  .pager span:empty {
    display: none;
  }

  .pager__link {
    padding-block: 0.95rem;
    border-bottom: 1px solid var(--line);
  }

  .pager__link--prev {
    order: 1;
  }

  .pager__link--next {
    order: 2;
    text-align: left;
  }

  .pager__all {
    order: 3;
    align-self: start;
    justify-self: start;
    margin-block-start: 1.25rem;
  }
}

/* -------------------------------------------------------------- Boutons */
.button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.8rem 1.5rem;
  background: var(--ink);
  color: var(--paper);
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  font: inherit;
  font-size: var(--step--1);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}

.button:hover {
  background: transparent;
  color: var(--ink);
}

.button--ghost {
  background: transparent;
  color: var(--ink);
}

.button--ghost:hover {
  background: var(--ink);
  color: var(--paper);
}

/* ----------------------------------------------------------- Fil d'Ariane */
.breadcrumb {
  padding-block: 1rem 0;
  font-size: var(--step--1);
  color: var(--ink-faint);
}

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

.breadcrumb li + li::before {
  content: "›";
  margin-right: 0.4rem;
  color: var(--line-strong);
}

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

.breadcrumb a:hover {
  color: var(--accent-ink);
  text-decoration: underline;
}

/* ----------------------------------------------------------------- Blog */
.post-list {
  display: grid;
  gap: clamp(1.5rem, 1rem + 2vw, 3rem);
}

.post-item {
  display: grid;
  gap: 1.25rem;
  padding-bottom: clamp(1.5rem, 1rem + 2vw, 3rem);
  border-bottom: 1px solid var(--line);
}

/* Le trait sépare deux articles : sous le dernier, il n'annonce rien et donne
   l'impression d'une liste tronquée. */
.post-item:last-child {
  padding-bottom: 0;
  border-bottom: 0;
}

@media (min-width: 48rem) {
  .post-item {
    grid-template-columns: 22rem 1fr;
    gap: 2rem;
    align-items: start;
  }

  /**
   * Article sans visuel : le texte occupe toute la largeur.
   *
   * Sans cette règle, la colonne de 22rem réservée à l'image reste en place et
   * le seul enfant présent, le bloc de texte, s'y trouve enfermé : le titre se
   * brisait alors sur plusieurs lignes sans raison.
   */
  .post-item:not(:has(.post-item__media)) {
    grid-template-columns: 1fr;
  }
}

.post-item__media {
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background: var(--paper-sunk);
  border-radius: var(--radius);
}

.post-item__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.post-item__date,
.post-meta {
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.post-item__title {
  margin-top: 0.4rem;
  font-size: var(--step-2);
}

.post-item__title a {
  text-decoration: none;
}

.post-item__excerpt {
  margin-top: 0.75rem;
  color: var(--ink-soft);
}

/* Corps rédactionnel */
.prose {
  max-width: var(--max-text);
  font-size: var(--step-0);
}

.prose > * + * {
  margin-block-start: var(--block-gap);
}

/**
 * Corps d'une page interne : galerie, texte, rattachements, étiquettes.
 *
 * Tous les blocs sont séparés par la même valeur. Auparavant, chacun vivait dans
 * sa propre section et l'écart résultait de l'addition de deux paddings, ce qui
 * creusait jusqu'à 6rem entre une image et le texte qui la suit, contre 1rem
 * entre deux paragraphes.
 */
/* Seul le bas est défini ici : l'espace supérieur vient de la règle qui suit le
   trait de séparation (`.page-head + *`), déjà harmonisée pour tout le site. */
.page-body {
  padding-block-end: clamp(2rem, 1.4rem + 2.4vw, 3.5rem);
}

.page-body > * + * {
  margin-block-start: var(--block-gap);
}

/* Après une galerie ou une vidéo, l'écart est élargi : ce sont les deux seuls
   blocs où le contenu touche le bord, et un texte collé dessous paraît happé. */
.page-body > .gallery + *,
.page-body > .videos + * {
  margin-block-start: var(--media-gap);
}

/**
 * Un titre appartient au texte qu'il annonce, pas à celui qui le précède.
 *
 * L'espace AVANT doit donc être nettement plus grand que celui APRÈS : ici,
 * environ quatre fois. Avec la seule règle `* + *`, les deux valaient
 * pratiquement la même chose et le titre semblait flotter entre deux blocs.
 */
.prose h2 {
  margin-block-start: 1.7em;
  font-size: var(--step-2);
}

.prose h3 {
  margin-block-start: 1.35em;
  font-size: var(--step-1);
}

.prose h2 + *,
.prose h3 + *,
.prose h4 + * {
  margin-block-start: 0.45em;
}

/* Un titre en tête de contenu n'a rien au-dessus dont il faudrait le détacher. */
.prose > :first-child {
  margin-block-start: 0;
}

.prose ul,
.prose ol {
  padding-inline-start: 1.3em;
}

.prose li + li {
  margin-top: 0.25em;
}

/* Le bloc de texte d'une page interne n'a pas besoin de la respiration d'une
   section d'accueil : le titre et son trait marquent déjà l'entrée en matière. */
.page-head + .section,`n.page-head + .page-body {
  padding-block-end: clamp(2rem, 1.4rem + 2.4vw, 3.5rem);
}

.prose a {
  color: var(--accent-ink);
}

.prose blockquote {
  margin-inline: 0;
  padding-inline-start: 1.25rem;
  border-inline-start: 3px solid var(--accent);
  color: var(--ink-soft);
  font-style: italic;
}

/* ------------------------------------------------------------ Formulaire */
.form {
  display: grid;
  gap: 1.1rem;
  max-width: 36rem;
}

.field {
  display: grid;
  gap: 0.35rem;
}

.field label {
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.field input,
.field textarea {
  font: inherit;
  padding: 0.7rem 0.85rem;
  background: var(--paper);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  color: var(--ink);
}

/**
 * Champ actif : liseré noir, jamais rouge.
 *
 * Le rouge se lit comme un signal d'erreur. L'employer pour marquer le simple
 * fait qu'un champ est sélectionné donnait l'impression d'une saisie fautive
 * dès le premier clic. Il est donc réservé à la validation, plus bas.
 *
 * Le contraste reste suffisant pour l'accessibilité : deux pixels d'encre noire
 * sur fond clair, doublés d'un épaississement de la bordure.
 */
.field input:focus,
.field textarea:focus {
  border-color: var(--ink);
  outline: 2px solid var(--ink);
  outline-offset: 1px;
}

/**
 * Erreur de saisie · le seul endroit où le rouge apparaît sur le formulaire.
 *
 * `:user-invalid` ne se déclenche qu'après une interaction réelle, à la
 * différence de `:invalid` qui colorerait en rouge un champ obligatoire encore
 * vide, avant même que le visiteur ait commencé à écrire.
 */
.field input:user-invalid,
.field textarea:user-invalid {
  border-color: var(--accent);
}

.field input:user-invalid:focus,
.field textarea:user-invalid:focus {
  outline-color: var(--accent);
}

.field textarea {
  min-height: 10rem;
  resize: vertical;
}

/* Piège à robots : invisible pour l'humain, rempli par les robots. */
.field--trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__status {
  padding: 0.85rem 1rem;
  border-radius: var(--radius);
  font-size: var(--step--1);
}

.form__status[data-state="ok"] {
  background: #e8f4ea;
  border: 1px solid #a8d3b2;
  color: #1c5b2b;
}

.form__status[data-state="error"] {
  background: #fdeceb;
  border: 1px solid #f0b4b1;
  color: #8c1a15;
}

/* --------------------------------------------------------------- Footer */
.site-footer {
  margin-top: clamp(3rem, 2rem + 4vw, 6rem);
  padding-block: clamp(2rem, 1.5rem + 2vw, 3.5rem);
  background: var(--ink);
  color: #d8d6d2;
}

.site-footer a {
  color: #fff;
  text-decoration: none;
}

.site-footer a:hover {
  color: #fff;
  text-decoration: underline;
}

.site-footer__inner {
  display: grid;
  /* Gouttière horizontale plus large que la verticale : la colonne de gauche et
     les liens du portfolio se touchaient presque sur écran large. */
  gap: 2rem clamp(2.5rem, 1rem + 5vw, 6rem);
}

@media (min-width: 48rem) {
  .site-footer__inner {
    grid-template-columns: 1.4fr 1fr 1fr;
  }
}

.site-footer h2 {
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #fff;
  margin-bottom: 0.85rem;
}

.site-footer ul {
  list-style: none;
  padding: 0;
  display: grid;
  gap: 0.5rem;
  font-size: var(--step--1);
}

/* ------------------------------------------------------ Réseaux sociaux */
.social {
  display: flex !important;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.social__link {
  display: grid;
  place-items: center;
  width: 2.6rem;
  height: 2.6rem;
  border: 1px solid currentColor;
  border-radius: 50%;
  /* Le tracé est en currentColor : la couleur suit celle du texte environnant. */
  color: inherit;
  opacity: 0.75;
  transition: opacity var(--transition), background-color var(--transition), color var(--transition);
}

/**
 * La règle est préfixée par `.site-footer` pour passer devant
 * `.site-footer a:hover { color: #fff }`, plus spécifique que `.social__link`
 * seul. Sans cela, le tracé restait blanc sur le fond blanc du survol : les
 * icônes disparaissaient au passage de la souris.
 */
.site-footer .social__link:hover,
.site-footer .social__link:focus-visible,
.social__link:hover,
.social__link:focus-visible {
  opacity: 1;
  background: #fff;
  color: var(--ink);
  border-color: #fff;
}

.social__icon {
  width: 1.15rem;
  height: 1.15rem;
  display: block;
}

.site-footer__bottom {
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid #2e2e2e;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.5rem;
  justify-content: space-between;
  font-size: var(--step--1);
  color: #9b9892;
}

/* Retour en haut de page. */
.to-top {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.to-top__icon {
  transition: transform var(--transition);
}

.to-top:hover .to-top__icon,
.to-top:focus-visible .to-top__icon {
  transform: translateY(-2px);
}

/* ------------------------------------------------------------- Lightbox */

/* --------------------------------------------------------------- Divers */
.stack-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.stack-head h2 {
  font-size: var(--step-2);
}

/* Titre centré et plus discret : il introduit une bande pleine largeur, où un
   titre aligné à gauche paraîtrait décroché du contenu qu'il annonce. */
.section-title--centered {
  text-align: center;
  font-size: var(--step-1);
  letter-spacing: 0.02em;
}

.link-more {
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  border-bottom: 1px solid var(--line-strong);
  padding-bottom: 2px;
}

.link-more:hover {
  border-bottom-color: var(--accent);
}

/**
 * Barre de filtres de l'archive.
 *
 * Son écart avec la première grille était la somme de deux paddings de section,
 * soit près de 6rem : les filtres semblaient détachés du contenu qu'ils pilotent.
 * Ils lui sont désormais rattachés, et c'est la section suivante qui voit son
 * padding supérieur annulé.
 */
.filters {
  padding-block-end: 0;
}

.filters + .section {
  padding-block-start: var(--head-space-after);
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  list-style: none;
  padding: 0;
  margin-top: 1.5rem;
}

/**
 * Respiration en fin de fiche projet.
 *
 * Le dernier bloc (étiquettes, rattachements secondaires ou texte) se retrouvait
 * collé au bandeau « Dans la même catégorie » qui suit.
 */
.project-article {
  padding-block-end: clamp(1.5rem, 1rem + 2vw, 2.75rem);
}

.project-article .tag-list {
  margin-block-end: 0.5rem;
}

.tag-list li {
  font-size: var(--step--1);
  padding: 0.3rem 0.7rem;
  background: var(--paper-warm);
  border: 1px solid var(--line);
  border-radius: 100px;
  color: var(--ink-soft);
}

.empty-state {
  padding: 3rem 0;
  color: var(--ink-faint);
}

/* ------------------------------------------------------ Bloc « À propos »
   Deux colonnes sur grand écran, portrait à gauche, texte à droite.

   Les colonnes sont légèrement inégales : le texte a besoin de plus de place
   que l'image pour garder une longueur de ligne confortable. `minmax(0, …)`
   est indispensable, sans quoi une image large refuserait de se réduire et
   ferait déborder la grille.
   ========================================================================== */
.about-teaser {
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(1.75rem, 1rem + 3vw, 3.5rem);
  align-items: center;
}

@media (max-width: 52rem) {
  .about-teaser {
    grid-template-columns: 1fr;
  }
}

.about-teaser__media img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
}

.about-teaser__body > * + * {
  margin-top: var(--block-gap);
}

.about-teaser__lead {
  font-size: var(--step-1);
  line-height: 1.45;
  color: var(--ink);
}

.about-teaser__body p:not(.about-teaser__lead) {
  color: var(--ink-soft);
  max-width: var(--max-text);
}

/* =============================================================================
   Utilitaires
   =============================================================================
   Ces quelques classes remplacent les attributs `style` qui parsemaient les
   gabarits. Sans elles, la politique de sécurité du contenu devrait autoriser
   les styles en ligne (`style-src 'unsafe-inline'`), ce qui revient à laisser
   ouverte la porte qu'elle est censée fermer.
   ========================================================================== */

.mt-sm {
  margin-top: 1rem;
}

.mt-md {
  margin-top: 1.5rem;
}

.mt-lg {
  margin-top: 2rem;
}

.mt-0 {
  margin-top: 0;
}

.ml-xs {
  margin-left: 0.5rem;
}

/* Liste dépouillée de ses puces et de ses retraits. */
.list-bare {
  list-style: none;
  padding: 0;
  margin: 0;
}

.link-bare {
  text-decoration: none;
}

/* Deux colonnes qui se replient d'elles-mêmes sur une seule si la place manque. */
.split {
  display: grid;
  gap: clamp(2rem, 1rem + 4vw, 4rem);
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
}
