/* Charte définitive, reprise du design validé dans Claude Design (voir
   "Crea News — design de site", fichier Crea News.dc.html) — remplace la
   charte provisoire calquée sur polycreatifs.fr. Thème clair, oklch().
   Polices : Space Grotesk (titres), IBM Plex Sans (texte), Inter 900 (logo).

   Passe design du 15/08/2026 (skill tasteskill v2, audit Section 11) :
   polices auto-hébergées (voir public/fonts/), échelle de rayons unifiée en
   3 paliers (voir --rayon-* ci-dessous), légère activation d'un mouvement
   restreint (voir plus bas dans ce fichier). Toujours zéro dépendance JS
   ajoutée — cohérent avec le choix déjà pris pour ce projet.

   font-display: optional (20/08/2026, retour de Thomas - le flash visible
   au changement de vue/thème "ne fait pas pro") : avec "swap" (valeur
   d'avant), le navigateur affiche toujours une police de secours puis
   bascule sur la vraie police dès qu'elle arrive, même si elle est déjà en
   cache - d'où le léger redimensionnement visible à CHAQUE rechargement,
   pas seulement au tout premier chargement du site. "optional" laisse le
   navigateur choisir une seule fois, au moment du rendu : police déjà en
   cache -> utilisée immédiatement, sans bascule ni flash ; pas encore en
   cache -> le navigateur attend ~100ms max puis affiche la police de
   secours et NE reviendra plus jamais sur la vraie police pour CE rendu
   (elle sera en cache et correcte dès la page suivante). Contrepartie
   assumée : sur une toute première visite avec une connexion très lente,
   la police de secours peut rester affichée sur cette page-là - jugé
   largement préférable au flash systématique que ça remplace. */

@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 500;
  font-display: optional;
  src: url('/fonts/space-grotesk-500.woff2') format('woff2');
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 600;
  font-display: optional;
  src: url('/fonts/space-grotesk-600.woff2') format('woff2');
}
@font-face {
  font-family: 'Space Grotesk';
  font-style: normal;
  font-weight: 700;
  font-display: optional;
  src: url('/fonts/space-grotesk-700.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 400;
  font-display: optional;
  src: url('/fonts/ibm-plex-sans-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 500;
  font-display: optional;
  src: url('/fonts/ibm-plex-sans-500.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Sans';
  font-style: normal;
  font-weight: 600;
  font-display: optional;
  src: url('/fonts/ibm-plex-sans-600.woff2') format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 900;
  font-display: optional;
  src: url('/fonts/inter-900.woff2') format('woff2');
}

:root {
  --fond: oklch(0.98 0.004 60);          /* fond de page */
  --fond-panneau: oklch(0.995 0.002 60); /* cartes, champs de formulaire */
  --fond-alt: oklch(0.955 0.005 55);     /* header, footer, pastilles diverses */
  --texte: oklch(0.22 0.008 55);
  --texte-article: oklch(0.28 0.008 55);
  --discret: oklch(0.48 0.012 60);
  --discret-fort: oklch(0.4 0.012 60);
  --bordure: oklch(0.89 0.006 55);
  --bordure-forte: oklch(0.85 0.006 55);
  /* Orange repris directement de la flamme du logo (public/assets/
     flamme-polyworkshop.svg, fill="#F7841E") plutôt que du orange plus pâle
     défini dans les specs Claude Design d'origine (#F2933D) — Thomas a
     tranché le 15/08/2026 pour une seule teinte cohérente entre l'icône et
     le reste du site, quitte à ne pas toujours atteindre le contraste WCAG
     AA en usage "texte sur fond clair" (logo, libellés) : choix assumé,
     préférence pour un orange vif plutôt qu'une variante plus sombre jugée
     "moche" (marron/terne). Les boutons à fond plein restent conformes
     (texte sombre dessus, voir plus bas) — seul ce compromis esthétique
     s'applique au texte orange sur fond clair. */
  --accent: #f7841e;
  /* Couleur de texte pour tout ce qui est posé SUR un fond --accent (boutons
     pleins, onglet actif...) — volontairement fixe, jamais redéfinie dans le
     thème sombre plus bas. --accent ne change pas de valeur selon le thème,
     donc le texte posé dessus ne doit pas en changer non plus : au 15/08/2026
     ce token utilisait --texte (qui, lui, s'inverse en sombre), ce qui
     cassait le contraste des boutons une fois le thème sombre ajouté
     (2.19:1 mesuré) - bug trouvé et corrigé le jour même. */
  --sur-accent: oklch(0.2 0.008 55);
  --ok: #16a34a;
  --ok-fond: oklch(0.93 0.03 145);
  --ok-texte: oklch(0.35 0.08 145);
  --ko: #dc2626;
  --largeur-page: 80rem;         /* 1280px — listing */
  --largeur-page-etroite: 57.5rem; /* 920px — article/about/contact */

  /* Échelle de rayons unifiée (3 paliers documentés, au lieu de 7 valeurs
     proches mais différentes auparavant) : petits contrôles interactifs,
     grandes surfaces/panneaux, formes pilule (déjà en 999px, inchangées). */
  --rayon-controle: .5rem;   /* boutons, champs, onglets, badges non-pilule */
  --rayon-panneau: .875rem;  /* cartes, images, panneaux, blocs admin */

  /* Easing partagé pour toutes les transitions du site (mouvement restreint,
     cohérent avec MOTION_INTENSITY ~3 retenu pour cette passe). */
  --ease-doux: cubic-bezier(0.16, 1, 0.3, 1);
}

/* --- Thème sombre (15/08/2026) -------------------------------------------
   Activé via l'attribut data-theme="sombre" posé sur <html> par le serveur
   (voir layout.ejs + le cookie "theme" lu dans app.js) — jamais par
   @media (prefers-color-scheme), Thomas voulant un choix mémorisé
   explicitement plutôt qu'une détection automatique du système. Mêmes noms
   de variables que le thème clair : tout le reste du fichier n'a besoin
   d'aucune modification, il consomme déjà ces tokens partout. --accent reste
   inchangé (même orange vif dans les deux thèmes, décision du 15/08/2026 -
   voir plus haut). Pas de noir pur ni de blanc pur (cf. bonnes pratiques
   habituelles) : --fond et --texte restent légèrement teintés. */
:root[data-theme="sombre"] {
  --fond: oklch(0.17 0.006 55);
  --fond-panneau: oklch(0.215 0.007 55);
  --fond-alt: oklch(0.245 0.008 55);
  --texte: oklch(0.95 0.004 60);
  --texte-article: oklch(0.88 0.006 55);
  --discret: oklch(0.64 0.012 58);
  --discret-fort: oklch(0.72 0.012 58);
  --bordure: oklch(0.32 0.008 55);
  --bordure-forte: oklch(0.4 0.008 55);
  --ok-fond: oklch(0.28 0.05 145);
  --ok-texte: oklch(0.78 0.12 145);
  --ko: #f37272;
}

/* Le repère de catégorie (pastille + libellé, teinté par catégorie via
   --teinte-categorie posé en style inline sur chaque élément — voir
   lib/categories-visuel.js) et le motif rayé des vignettes vides ne passent
   PAS par les tokens ci-dessus : leur luminosité est calculée directement
   pour un fond clair. Sans ces overrides, le libellé de catégorie serait
   trop terne sur fond sombre, et le motif rayé des news sans image
   resterait une plaque claire éblouissante au milieu d'une page sombre. */
:root[data-theme="sombre"] .etiquette-categorie,
:root[data-theme="sombre"] .carte-news-meta-categorie {
  color: oklch(0.78 0.14 var(--teinte-categorie));
}

:root[data-theme="sombre"] .carte-news-image-vide,
:root[data-theme="sombre"] .article-image-vide {
  background: repeating-linear-gradient(
    135deg,
    oklch(0.3 0.03 var(--teinte-categorie)) 0 14px,
    oklch(0.25 0.006 55) 14px 28px
  );
}

:root[data-theme="sombre"] .carte-news-image-vide span,
:root[data-theme="sombre"] .article-image-vide span {
  color: oklch(0.72 0.01 60);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--fond);
  color: var(--texte);
  font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  line-height: 1.6;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.conteneur {
  max-width: var(--largeur-page);
  width: 100%;
  margin: 0 auto;
  padding: 1.5rem 2rem 4.5rem;
  flex: 1;
}

/* Pages simples (article/about/contact) : le conteneur principal lui-même
   se resserre (au lieu d'ajouter un conteneur étroit À L'INTÉRIEUR de
   .conteneur) — sinon son padding s'ajoute à celui du conteneur étroit
   imbriqué, et le texte se retrouve décalé par rapport au header/footer qui
   n'ont qu'un seul niveau de padding, eux (piège rencontré en corrigeant un
   premier désalignement le 15/08/2026 : le correctif précédent avait
   remplacé un désalignement par un autre, dans l'autre sens, pour cette
   raison). Un seul niveau de padding partout = un seul bord gauche partout,
   quelle que soit la page. */
.conteneur.page-etroite {
  max-width: var(--largeur-page-etroite);
}

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

h1, h2 { font-family: 'Space Grotesk', sans-serif; font-weight: 600; letter-spacing: -0.01em; }

.note { color: var(--discret); font-size: .875rem; }

/* --- Header et footer publics -------------------------------------------- */

.entete-public {
  border-bottom: 1px solid var(--bordure);
  background: var(--fond-alt);
}

.entete-public-interieur {
  max-width: var(--largeur-page);
  margin: 0 auto;
  padding: 1.5rem 2rem;
  display: flex;
  /* flex-start (pas center) : aligne le haut de l'icône sur le haut du bloc
     titre+tagline juste à côté, plutôt que de centrer une icône de taille
     fixe sur toute la ligne (rendu "bancal" signalé par Thomas le
     15/08/2026). Voir .entete-logo-icone pour la hauteur. Note technique :
     un align-items:stretch + height:100% sur l'icône avait été tenté
     d'abord (dimensionnement dynamique "parfait"), mais un <img> stretché
     dans une ligne flex utilise sa taille intrinsèque (365px) pour calculer
     la hauteur de ligne AVANT le stretch → l'icône explosait à pleine
     hauteur d'écran. Hauteur fixe = plus robuste, préféré ici. */
  align-items: flex-start;
  gap: .75rem;
  flex-wrap: wrap;
}

/* Sur les pages étroites (article/à propos/contact — voir .page-etroite),
   le header ET le footer se resserrent eux aussi à 920px au lieu de 1280px,
   pour garder le même bord gauche que le contenu (comme dans le design :
   containerMaxWidth y vaut 920px, pas seulement le contenu). Sélecteur à 2
   classes pour gagner sur l'ordre des règles sans y penser. */
.entete-public-interieur.page-etroite,
.pied-public-interieur.page-etroite {
  max-width: var(--largeur-page-etroite);
}

.entete-logo-icone {
  /* Hauteur mesurée pour correspondre exactement au bloc titre + tagline
     (article/à-propos/contact, sans champ de recherche à côté) —
     approximation fixe et robuste plutôt qu'un étirement dynamique fragile
     (voir la note sur .entete-public-interieur). Le listing (avec champ de
     recherche, tagline sur une largeur différente) a sa propre valeur juste
     en dessous. À ajuster à la main si le texte de la tagline change
     significativement. */
  height: 4.5rem;
  width: auto;
  flex-shrink: 0;
}

/* Sur le listing (recherche à côté, .entete-recherche-form présente), le
   bloc titre+tagline mesure ~5.77rem au lieu de ~4.5rem — même texte, mais
   largeur disponible différente. Mesuré directement plutôt que deviné. */
.entete-public-interieur:not(.page-etroite) .entete-logo-icone {
  height: 5.8rem;
}

.entete-logo-bloc {
  flex: 1;
  min-width: 0;
}

.entete-logo,
.entete-logo-ligne .entete-logo {
  display: inline-block;
  /* Pile de secours proche d'Inter (au lieu d'un simple "sans-serif"
     générique) : réduit le saut de largeur/forme si jamais la police de
     secours reste affichée un instant (voir app.js pour le vrai correctif,
     le cache long sur /fonts). */
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-weight: 900;
  font-size: 1.875rem;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--accent);
  text-decoration: none;
}

.entete-tagline {
  margin: .25rem 0 0;
  color: var(--discret);
  font-size: .84rem;
  line-height: 1.5;
}

.entete-recherche-form {
  position: relative;
  width: 16.25rem;
  max-width: 100%;
  flex-shrink: 0;
  /* La ligne est en align-items:flex-start (voir .entete-public-interieur) :
     sans ça, le champ de recherche s'alignerait sur le haut au lieu du
     centre vertical du bloc titre+tagline. */
  align-self: center;
}

.entete-recherche {
  width: 100%;
  padding: .69rem 2.5rem .69rem 1rem;
  /* Coins moins arrondis (retour de Thomas, 20/08/2026) : même rayon que
     les onglets de catégorie juste en dessous, pas la pilule complète
     d'avant (999px). */
  border-radius: var(--rayon-controle);
  border: 1px solid var(--bordure-forte);
  background: var(--fond-panneau);
  color: inherit;
  font: inherit;
  font-size: .875rem;
  outline: none;
}

.entete-recherche:focus {
  border-color: var(--accent);
}

.entete-recherche-bouton {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 2.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  color: var(--discret);
  cursor: pointer;
}

.entete-recherche-bouton:hover { color: var(--accent); }

.pied-public {
  border-top: 1px solid var(--bordure);
  background: var(--fond-alt);
  margin-top: auto;
}

.pied-public-interieur {
  max-width: var(--largeur-page);
  margin: 0 auto;
  padding: 1.375rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
  font-size: .78rem;
  color: var(--discret);
}

/* Le texte occupe la place restante et se replie sur plusieurs lignes plutôt
   que de repousser les liens à la ligne suivante : sans ce flex-basis, un
   texte un peu long ferait sauter la mise en page « texte à gauche / liens à
   droite » dès que Thomas rallonge le texte depuis /admin/textes. */
.pied-description {
  margin: 0;
  flex: 1 1 20rem;
  max-width: 46rem;
}

.pied-liens {
  display: flex;
  gap: 1.125rem;
  flex-wrap: wrap;
  flex-shrink: 0;
}

.pied-lien {
  color: var(--discret-fort);
  font-weight: 500;
  text-decoration: none;
}

.pied-lien:hover { color: var(--texte); }

/* Mobile : les liens passent AU-DESSUS du texte (demande de Thomas,
   23/08/2026 - c'était l'inverse jusque-là). L'ordre du DOM reste l'ordre
   desktop ; on ne réordonne qu'ici, avec `order` sur les liens plutôt qu'un
   `column-reverse` sur le conteneur : si un 3e élément est ajouté au footer un
   jour, il se placera naturellement à la suite du texte au lieu de se
   retrouver en tête sans qu'on comprenne pourquoi.
   Ce bloc est volontairement placé juste après les règles du footer plutôt
   qu'avec les autres @media en fin de fichier : plus rien après ne cible
   .pied-*, la proximité l'emporte donc sur la convention. */
@media (max-width: 40rem) {
  .pied-public-interieur {
    flex-direction: column;
    align-items: flex-start;
    gap: .875rem;
  }

  .pied-liens { order: -1; }
}

/* --- Onglets de catégories ---------------------------------------------- */

.onglets {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-bottom: 1.25rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid var(--bordure);
}

.onglet {
  padding: .44rem .94rem;
  border-radius: var(--rayon-controle);
  font-size: .84rem;
  font-weight: 500;
  text-decoration: none;
  color: var(--discret-fort);
  border: 1px solid var(--bordure-forte);
  background: transparent;
}

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

.onglet-actif,
.onglet-actif:hover {
  /* Texte sombre plutôt que blanc : le orange de la marque n'offre pas assez
     de contraste avec du blanc (2.26:1 mesuré, sous le seuil WCAG AA de
     4.5:1) — voir l'audit du 15/08/2026. --sur-accent (fixe) sur --accent
     atteint 7.4:1 dans les deux thèmes. */
  color: var(--sur-accent);
  background: var(--accent);
  border-color: var(--accent);
}

/* --- Barre de résultats + bascule liste/grille --------------------------- */

.barre-resultats {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: .75rem;
  margin: 0 0 1.125rem;
}

.compteur-resultats {
  margin: 0;
  color: var(--discret);
  font-size: .84rem;
}

.recherche-active a {
  color: var(--accent);
  text-decoration: underline;
}

/* Regroupe la bascule liste/grille et la bascule clair/sombre — la
   "section préférences d'affichage" demandée par Thomas le 15/08/2026, sans
   étiquette de titre superflue : les deux groupes de boutons côte à côte se
   lisent déjà comme un seul bloc de réglages. */
.preferences-affichage {
  display: flex;
  align-items: center;
  gap: .75rem;
  flex-wrap: wrap;
}

.bascule-vue-bouton {
  padding: .375rem .8rem;
  border-radius: var(--rayon-controle);
  font-size: .8rem;
  font-weight: 500;
  border: 1px solid var(--bordure-forte);
  background: transparent;
  color: var(--discret-fort);
  text-decoration: none;
}

/* Variante icône (bascule Liste/Grille, bascule Clair/Sombre) : un seul
   bouton par bascule, carré, centré sur son icône plutôt que le padding
   horizontal pensé pour du texte. L'icône affichée reflète l'état ACTUEL
   (voir views/listing.ejs) ; le libellé (éditable dans /admin/textes)
   décrit l'action du clic et sert d'info-bulle (title) + label accessible
   (aria-label). */
.bascule-vue-bouton--icone {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.125rem;
  height: 2.125rem;
  padding: 0;
}

/* --- Liste de news -------------------------------------------------------- */
/* Vue « Liste » (par défaut) : une news par ligne, miniature à gauche.
   Vue « Grille » (.liste-news--grille) : cartes empilées en grille. */

.liste-news {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1.125rem;
}

.liste-news--grille {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: 1.25rem;
}

.carte-news {
  background: var(--fond-panneau);
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-panneau);
  overflow: hidden;
}

.carte-news-image-conteneur {
  position: relative;
  grid-area: image;
}

.carte-news-image {
  display: block;
  width: 100%;
  height: 100%;
  aspect-ratio: 4 / 3;
  background: var(--bordure);
}

.liste-news--grille .carte-news-image {
  /* 4:3 au lieu de 16:9 (demande de Thomas, 17/08/2026) : le 16:9 rognait
     trop le haut/bas des images, en particulier celles avec du texte
     incrusté (packaging produit, visuels promo). Cette règle l'emporte
     aussi sur mobile (voir @media plus bas dans le fichier) grâce à sa
     spécificité plus forte (2 classes) : pas besoin de la répéter dans le
     bloc mobile pour que "desktop et mobile" soient couverts. Le mode
     Liste garde son 16:9 mobile inchangé, non concerné par cette demande. */
  aspect-ratio: 4 / 3;
}

.carte-news-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.carte-news-image-vide {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: .5rem;
  text-align: center;
  background: repeating-linear-gradient(
    135deg,
    oklch(0.9 0.03 var(--teinte-categorie)) 0 10px,
    oklch(0.94 0.006 55) 10px 20px
  );
}

.carte-news-image-vide span,
.article-image-vide span {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .7rem;
  color: oklch(0.4 0.01 60);
  line-height: 1.4;
}

/* Bouton d'édition rapide, visible seulement en étant connecté (voir
   listing.ejs) et seulement au survol de la miniature. */
.carte-news-editer {
  position: absolute;
  top: .5rem;
  right: .5rem;
  padding: .25rem .56rem;
  border-radius: var(--rayon-controle);
  background: rgba(0, 0, 0, .72);
  color: #fff;
  font-size: .72rem;
  font-weight: 600;
  text-decoration: none;
  opacity: 0;
  transform: translateY(-.25rem);
  transition: opacity .15s ease, transform .15s ease;
}

.carte-news-image-conteneur:hover .carte-news-editer,
.carte-news-editer:focus-visible {
  opacity: 1;
  transform: translateY(0);
}

/* .carte-news-corps regroupe TOUT le contenu de la carte (catégorie, titre,
   image, extrait, méta, actions) - image comprise, voir listing.ejs : elle
   n'est plus un élément séparé à côté de corps, mais un enfant de corps
   parmi les autres, placé dans le DOM entre le titre et l'extrait. Ce choix
   (plutôt qu'une image en frère séparé comme avant) sert le mode Liste
   mobile plus bas : un élément flottant (float) n'affecte jamais ce qui le
   précède dans le flux, donc placer catégorie+titre AVANT l'image dans le
   DOM garantit qu'ils restent en pleine largeur quel que soit le mode
   d'affichage, sans dépendre d'un correctif CSS par mode.
   Par défaut (règles ci-dessous, avant tout média-query) : mise en page du
   mode Liste desktop - grille CSS 2 colonnes (image à gauche pleine
   hauteur, texte à droite) via des grid-area nommées, qui replacent
   l'image où on veut visuellement SANS dépendre de sa position dans le DOM
   (contrairement à un flottant). Le mode Grille et le mode Liste mobile
   redéfinissent .carte-news-corps plus bas pour leurs propres mises en
   page (flex empilé, puis bloc + flottant). */
.carte-news-corps {
  display: grid;
  grid-template-columns: 17rem 1fr;
  grid-template-areas:
    "image categorie"
    "image titre"
    "image extrait"
    "image meta"
    "image actions";
  column-gap: 1.375rem;
  padding: 1.125rem 1.375rem 1.125rem 0;
  /* Centre le bloc de texte empilé (5 lignes) verticalement face à l'image
     quand celle-ci est plus haute que le texte, comme le faisait avant
     .carte-news-corps en display:flex + justify-content:center - avec des
     lignes de grille en "auto" (hauteur au contenu), l'espace excédentaire
     dû à l'image (qui s'étend sur les 5 lignes) devient de l'espace libre
     que align-content répartit, ici en le centrant au milieu du groupe de
     lignes plutôt qu'en étirant chaque ligne individuellement. */
  align-content: center;
}

.carte-news-corps > .carte-news-categorie { grid-area: categorie; }
.carte-news-corps > .carte-news-titre { grid-area: titre; min-width: 0; }
.carte-news-corps > .carte-news-extrait { grid-area: extrait; min-width: 0; }
.carte-news-corps > .carte-news-meta { grid-area: meta; }
.carte-news-corps > .carte-news-actions { grid-area: actions; }

.carte-news-image-conteneur {
  position: relative;
  grid-area: image;
  /* .carte-news-corps a un padding (voir plus haut) pour que le texte ne
     touche pas les bords de la carte - mais l'image, elle, doit continuer
     à mordre jusqu'aux bords haut/bas comme avant (le padding gauche est
     déjà à 0 sur le conteneur, donc rien à compenser de ce côté). Cette
     marge négative annule le padding haut/bas uniquement pour la cellule
     image, sans toucher au padding des autres cellules. */
  margin: -1.125rem 0;
}

.liste-news--grille .carte-news-corps {
  display: flex;
  flex-direction: column;
  gap: .375rem;
  padding: 1.125rem 1.375rem;
}

.liste-news--grille .carte-news-image-conteneur {
  /* order:-1 replace l'image visuellement en premier (en haut) malgré sa
     position dans le DOM (entre le titre et l'extrait, voir plus haut) -
     flex, comme grid, ignore l'ordre du DOM dès qu'on lui dit où placer
     les choses. Marge négative même principe qu'en desktop (voir plus
     haut), adaptée au padding uniforme de ce mode (mord aussi sur les
     côtés ici, car l'image est en haut d'une colonne plutôt qu'à gauche
     d'une ligne). */
  order: -1;
  margin: -1.125rem -1.375rem 0;
}

/* --- Repère de catégorie (pastille + libellé coloré) ---------------------- */

.carte-news-categorie {
  display: flex;
  align-items: center;
  gap: .44rem;
}

.pastille-categorie {
  display: inline-block;
  flex-shrink: 0;
  width: .625rem;
  height: .625rem;
  background: oklch(0.72 0.15 var(--teinte-categorie));
}

.pastille-rond { border-radius: 50%; }
.pastille-carre { border-radius: .125rem; }
.pastille-losange { border-radius: .125rem; transform: rotate(45deg); }

.etiquette-categorie {
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .01em;
  color: oklch(0.5 0.14 var(--teinte-categorie));
}

.carte-news-titre {
  margin: .125rem 0 0;
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 600;
  font-size: 1.0625rem;
  line-height: 1.35;
}

.carte-news-titre a {
  color: var(--texte);
  text-decoration: none;
}

.carte-news-titre a:hover {
  text-decoration: underline;
}

.carte-news-extrait {
  margin: 0;
  color: var(--discret);
  font-size: .875rem;
  line-height: 1.5;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

.carte-news-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin: .125rem 0 0;
  font-size: .8125rem;
  color: var(--discret);
}

.point-separateur { color: var(--bordure-forte); }

/* Catégorie en double dans .carte-news-meta (voir listing.ejs) : masquée
   par défaut en Liste/Grille (identique au comportement d'avant), visible
   seulement en mode Compact (voir plus bas) où elle remplace la date. */
.carte-news-meta-categorie,
.point-separateur--apres-categorie {
  display: none;
}

/* Marque cliquable (carte du listing + en-tête d'article) : même couleur
   discrète que le texte environnant (voir `a { color: inherit }` global),
   juste un soulignement au survol pour signaler que c'est cliquable. */
.lien-marque:hover { text-decoration: underline; }

.carte-news-actions {
  display: flex;
  align-items: center;
  gap: .875rem;
  margin: .25rem 0 0;
}

.bouton-lire {
  font-size: .84rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

.bouton-lire:hover { text-decoration: underline; }

.bouton-source {
  font-size: .84rem;
  font-weight: 500;
  color: var(--discret);
  text-decoration: none;
}

.bouton-source:hover { text-decoration: underline; }

/* Vue « Compact » (.liste-news--compact) : backlog du 20/08/2026, une news
   par ligne dense (pas de miniature, pas d'extrait, pas de boutons) pour
   qu'une semaine chargée ne pousse plus rien en page 2 sans avoir besoin
   d'un vrai scroll infini (voir docs/plan-mvp.md, "pagination classique
   retenue" reste vrai ici - juste beaucoup plus d'items par page, voir
   PAR_PAGE_COMPACT dans lib/news.js). Réutilise EXACTEMENT le même balisage
   que les vues Liste/Grille (voir listing.ejs) : seules ces règles
   changent, aucune logique de gabarit en plus. */
.liste-news--compact {
  gap: 0;
}

.liste-news--compact .carte-news {
  background: transparent;
  border: none;
  border-radius: 0;
  border-bottom: 1px solid var(--bordure);
}

.liste-news--compact .carte-news:last-child {
  border-bottom: none;
}

.liste-news--compact .carte-news-corps {
  display: flex;
  align-items: baseline;
  gap: .625rem;
  padding: .5rem .125rem;
}

/* Miniature, extrait, boutons d'action : masqués, c'est tout l'objectif de
   la densité ici. Le titre reste le seul lien vers l'article/la source. */
.liste-news--compact .carte-news-image-conteneur,
.liste-news--compact .carte-news-extrait,
.liste-news--compact .carte-news-actions {
  display: none;
}

/* Juste la pastille de couleur, pas le nom de catégorie en toutes lettres
   (prendrait trop de place sur une ligne dense). */
.liste-news--compact .carte-news-categorie {
  flex-shrink: 0;
}

.liste-news--compact .etiquette-categorie {
  display: none;
}

.liste-news--compact .carte-news-titre {
  margin: 0;
  font-size: .875rem;
  font-weight: 500;
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Colonne de largeur FIXE (retour de Thomas, 20/08/2026, 3e itération) :
   sans ça, chaque ligne a une largeur de texte différente ("Logiciels ·
   xTool" vs "Impression 3D Résine · Autre marque"), donc rien ne
   s'aligne d'une ligne à l'autre. Une largeur fixe + alignement à droite
   fait que la colonne démarre toujours au même endroit, comme une vraie
   colonne de tableau - display:block (pas flex, contrairement à la
   règle de base) pour que text-overflow:ellipsis fonctionne sur
   l'ensemble de la ligne (catégorie + séparateur + marque) d'un coup,
   plutôt que sur un seul enfant isolé. */
/* Catégorie + marque à droite plutôt que la date (retour de Thomas,
   20/08/2026, 2e itération du mode Compact) : plus utile pour se repérer
   d'un coup d'œil sur une ligne où il n'y a que le titre à côté. La date
   disparaît complètement dans ce mode - à ramener facilement si Thomas
   change d'avis (une seule règle à retirer ci-dessous).

   3e itération (toujours le 20/08/2026, retour de Thomas - "très strict
   sur les alignements") : catégorie et marque deviennent deux VRAIES
   colonnes à largeur fixe indépendantes, plutôt qu'un seul bloc où la
   marque démarrait à une position différente selon la longueur du nom de
   catégorie ("zig-zag"). Possible seulement parce que .carte-news-meta-
   marque est désormais TOUJOURS présent dans le DOM (voir listing.ejs),
   même vide : une colonne qui disparaîtrait sur les news sans marque
   décalerait la largeur totale du conteneur d'une ligne à l'autre, et
   casserait l'alignement de la colonne catégorie aussi. */
.liste-news--compact .carte-news-meta {
  display: flex;
  margin: 0;
  flex: 0 0 auto;
  gap: .375rem;
  /* Réduit d'environ 1,5pt par rapport aux autres tailles du mode Compact
     (retour de Thomas) : laisse la place à la colonne catégorie élargie
     ci-dessous sans pousser le titre - on est déjà en mode dense, un texte
     de tag n'a pas besoin d'être grand pour rester lisible. */
  font-size: .625rem;
}

/* Style "tag" (retour de Thomas, 20/08/2026, 4e itération du mode Compact) :
   fond coloré + coins arrondis façon pilule (--rayon 999px, même
   convention que le reste du site), largeur fixe = largeur de la colonne
   entière (pas juste la largeur du texte) pour que tous les tags d'une
   même colonne soient VISUELLEMENT identiques en taille, quel que soit le
   nom en dessous - c'est ça qui donne l'alignement en vraies colonnes. */
.liste-news--compact .carte-news-meta-categorie,
.liste-news--compact .carte-news-meta-marque {
  display: block;
  text-align: center;
  padding: .15rem .4rem;
  /* Coins moins arrondis (retour de Thomas, 20/08/2026) : même rayon que
     les onglets de catégorie, pas la pilule complète (999px) du premier
     essai. */
  border-radius: var(--rayon-controle);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Catégorie élargie à 8.5rem (retour de Thomas) : assez pour "Impression
   3D Résine" au complet, la plus longue catégorie actuelle - à charge de
   Thomas de garder les futurs noms de catégorie sous cette longueur,
   sinon l'ellipsis prend le relais proprement plutôt que de casser la
   mise en page. */
.liste-news--compact .carte-news-meta-categorie {
  flex: 0 0 8.5rem;
  font-weight: 600;
  text-decoration: none;
  background: oklch(0.93 0.045 var(--teinte-categorie));
  color: oklch(0.42 0.14 var(--teinte-categorie));
}

.liste-news--compact .carte-news-meta-categorie:hover {
  text-decoration: underline;
}

:root[data-theme="sombre"] .liste-news--compact .carte-news-meta-categorie {
  background: oklch(0.32 0.06 var(--teinte-categorie));
  color: oklch(0.82 0.12 var(--teinte-categorie));
}

/* Marque : même style "tag", mais gris neutre (pas de couleur par marque).
   --fond-alt (essai précédent) était trop proche de la couleur de page
   (0.955 vs 0.98 de luminosité) et se fondait dedans - jugé "trop pastel"
   par Thomas. --bordure-forte tranche davantage tout en restant un gris
   neutre déjà theme-aware, sans inventer de nouvelle couleur. */
.liste-news--compact .carte-news-meta-marque {
  flex: 0 0 6.5rem;
  background: var(--bordure-forte);
  color: var(--discret-fort);
}

:root[data-theme="sombre"] .liste-news--compact .carte-news-meta-marque {
  color: var(--texte);
}

/* Les séparateurs "•" n'ont plus lieu d'être entre deux colonnes
   distinctes (retour de Thomas) - masqués uniquement en mode Compact,
   toujours utilisés tels quels en Liste/Grille. */
.liste-news--compact .point-separateur--apres-categorie,
.liste-news--compact .point-separateur--apres-marques,
.liste-news--compact .carte-news-meta time {
  display: none;
}

@media (max-width: 40rem) {
  /* Mode Liste sur mobile (retour de Thomas, 17/08/2026, 3e itération) :
     titre en pleine largeur, extrait en dessous en pleine largeur (plus de
     miniature du tout dans ce mode - la 2e itération la gardait, en petit,
     avec l'extrait qui s'enroulait autour via un float ; Thomas a préféré
     la retirer complètement). Sans image, plus besoin du float, mais la
     règle de base (desktop) utilise une grille 2 colonnes "17rem 1fr" avec
     des grid-area nommées - sans ce display:block, cette grille resterait
     active ici (masquer l'image avec display:none ne suffit pas à faire
     disparaître la colonne qu'elle occupait) et laisserait un grand vide à
     gauche de chaque carte. En bloc normal, les enfants s'empilent tout
     simplement dans leur ordre du DOM. */
  .liste-news:not(.liste-news--grille):not(.liste-news--compact) .carte-news-corps {
    display: block;
    padding: .875rem 1rem;
  }

  .liste-news:not(.liste-news--grille):not(.liste-news--compact) .carte-news-image-conteneur {
    display: none;
  }

  .liste-news:not(.liste-news--grille):not(.liste-news--compact) .carte-news-titre {
    font-size: .9375rem;
  }

  /* Pas de troncature : Thomas veut le texte complet, pas coupé. */
  .liste-news:not(.liste-news--grille):not(.liste-news--compact) .carte-news-extrait {
    -webkit-line-clamp: unset;
  }

  /* Date/marque jugées peu utiles dans ce mode compact (question de
     Thomas, 17/08/2026) : masquées ici uniquement (toujours visibles en
     desktop, en Grille, et sur la page article). Simple à ramener si
     besoin - une seule ligne à retirer. */
  .liste-news:not(.liste-news--grille):not(.liste-news--compact) .carte-news-meta {
    display: none;
  }

  /* "Lire sur Crea News" et "Voir la source" sur la même ligne (demande de
     Thomas) - c'est déjà le comportement par défaut de .carte-news-actions
     (display:flex, pas de flex-direction:column ici contrairement à la 2e
     itération), donc rien à forcer. Seul ajout : flex-wrap, pour que les
     deux textes puissent redescendre proprement l'un sous l'autre s'ils
     ne tiennent pas côte à côte sur un écran étroit, plutôt que de
     déborder de la carte. */
  .liste-news:not(.liste-news--grille):not(.liste-news--compact) .carte-news-actions {
    flex-wrap: wrap;
  }

  /* Mode Compact sur mobile : les deux colonnes fixes (8.5rem + 6.5rem en
     desktop, voir plus haut) sont trop larges à deux sur un écran étroit -
     laisseraient trop peu de place au titre. Réduites ici (même
     proportion, catégorie un peu plus large que marque), l'ellipsis
     (déjà en place) absorbe le reste. */
  .liste-news--compact .carte-news-meta-categorie {
    flex-basis: 5.5rem;
  }

  .liste-news--compact .carte-news-meta-marque {
    flex-basis: 4.25rem;
  }

  .liste-news--compact .carte-news-meta {
    font-size: .58rem;
    gap: .25rem;
  }
}

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

.pagination {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 2rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--bordure);
  font-size: .84rem;
}

.pagination a {
  text-decoration: none;
  font-weight: 600;
  color: var(--accent);
}

.pagination a:hover {
  text-decoration: underline;
}

.pagination-inactif {
  color: var(--bordure-forte);
}

.pagination-etat {
  color: var(--discret);
}

/* --- Article ---------------------------------------------------------------- */

.fil-ariane {
  margin: 1.5rem 0 1.5rem;
  font-size: .875rem;
  display: flex;
  align-items: center;
  gap: .5rem;
  color: var(--discret);
}

.fil-ariane a {
  text-decoration: none;
  color: var(--discret);
}

.fil-ariane a:hover {
  text-decoration: underline;
}

.fil-ariane-separateur {
  color: var(--discret);
}

.article-entete-meta { margin-bottom: .875rem; }

.article-titre {
  margin: 0 0 .875rem;
  font-size: 1.875rem;
  line-height: 1.3;
}

.article-sous-titre {
  margin: 0 0 1.375rem;
  color: var(--discret);
  font-size: .875rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .875rem;
}

.bouton-partager {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
}

.article-intro {
  margin: 0 0 1.375rem;
  font-size: 1.125rem;
  line-height: 1.6;
  font-weight: 500;
  color: var(--texte);
}

.article-image {
  /* Hauteur naturelle (pas de aspect-ratio/object-fit ici) : demande de
     Thomas le 15/08/2026, l'image de l'article doit s'afficher en entier,
     sans recadrage — contrairement aux vignettes du listing (.carte-news-
     image), qui gardent un format fixe pour l'alignement de la grille. */
  width: 100%;
  border-radius: var(--rayon-panneau);
  margin-bottom: 1.125rem;
  display: block;
}

.article-image-vide {
  /* Contrairement au <img> réel ci-dessus, cette vignette n'a pas de contenu
     à "montrer en entier" — juste un bloc décoratif qui a besoin d'une
     hauteur définie, donc garde son format fixe. */
  aspect-ratio: 16 / 9;
  display: flex;
  align-items: center;
  justify-content: center;
  background: repeating-linear-gradient(
    135deg,
    oklch(0.9 0.03 var(--teinte-categorie)) 0 14px,
    oklch(0.94 0.006 55) 14px 28px
  );
}

/* Vidéo YouTube liée à l'article (backlog groupe A #5, 20/08/2026) : une
   miniature cliquable, pas de lecteur intégré - voir article.ejs pour le
   raisonnement complet. */
.article-youtube {
  position: relative;
  display: block;
  margin-bottom: 1.125rem;
}

.article-youtube img {
  width: 100%;
  border-radius: var(--rayon-panneau);
  display: block;
  /* hqdefault.jpg est en 4:3 (480x360) alors que la miniature réelle d'une
     vidéo est quasi toujours en 16:9 - contient donc des bandes noires
     haut/bas. object-fit + un ratio forcé recadre ces bandes plutôt que de
     les laisser visibles. */
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.article-youtube-lecture {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 3.5rem;
  height: 3.5rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--accent);
  color: var(--sur-accent);
  /* Le triangle "lecture" d'une police d'icônes standard n'est jamais
     parfaitement centré visuellement (son centre de masse penche à
     gauche) - ce petit décalage vers la droite compense l'effet. */
  padding-left: .2rem;
}

.article-youtube:hover .article-youtube-lecture {
  background: var(--sur-accent);
  color: var(--accent);
}

.article-cible-ligne {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 1.625rem;
}

.bouton-cible {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .625rem 1.25rem;
  border-radius: var(--rayon-controle);
  background: var(--accent);
  /* Texte sombre plutôt que blanc : voir la note sur .onglet-actif plus haut
     dans ce fichier (7.4:1 de contraste au lieu de 2.26:1). Le fond orange
     ne change JAMAIS au survol (ni assombri, ni ombre portée) - Thomas ne
     voulait ni l'un ni l'autre ; seule la flèche bouge, voir plus bas. --sur-
     accent (fixe, pas --texte) : voir la note sur ce token plus haut. */
  color: var(--sur-accent);
  text-decoration: none;
  font-weight: 600;
  font-size: .875rem;
}

.bouton-cible-fleche {
  display: inline-block;
}

.bouton-cible:hover .bouton-cible-fleche {
  transform: translate(2px, -2px);
}

.article-corps {
  font-size: 1rem;
  line-height: 1.75;
  color: var(--texte-article);
}

.article-corps :first-child {
  margin-top: 0;
}

/* --- Navigation actu précédente/suivante ---------------------------------- */

.article-nav-voisins {
  display: flex;
  gap: 1rem;
  margin: 2.25rem auto 0;
  padding-top: 1.5rem;
  border-top: 1px solid var(--bordure);
}

.article-nav-carte { flex: 1; min-width: 0; }
.article-nav-carte-droite { display: flex; justify-content: flex-end; }

.article-nav-lien {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: 1rem 1.125rem;
  border-radius: var(--rayon-panneau);
  background: var(--fond-alt);
  text-decoration: none;
  min-width: 0;
}

.article-nav-lien-droite {
  justify-content: flex-end;
}

.article-nav-fleche {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--accent);
  flex-shrink: 0;
}

.article-nav-texte {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  min-width: 0;
}

.article-nav-lien-droite .article-nav-texte {
  align-items: flex-end;
  text-align: right;
}

.article-nav-etiquette-ligne {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .75rem;
  width: 100%;
  min-width: 0;
}

.article-nav-etiquette {
  font-size: .69rem;
  font-weight: 600;
  /* Même orange que la flèche juste à côté (.article-nav-fleche) — c'était
     --accent-fonce (marron terne) avant le 15/08/2026, incohérent avec la
     flèche qui, elle, était déjà en --accent. */
  color: var(--accent);
  white-space: nowrap;
}

.article-nav-etiquette-categorie {
  font-size: .69rem;
  font-weight: 600;
  color: var(--discret);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.article-nav-titre {
  font-size: .875rem;
  font-weight: 600;
  color: var(--texte);
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0; /* sans ça, un span dans une colonne flex refuse de se
    réduire sous la largeur de son contenu : l'ellipsis ne s'applique
    jamais et le texte déborde de sa carte (voir lib/news.js pour le
    vrai correctif — le titre est déjà tronqué avant d'arriver ici). */
}

/* --- Pages simples (à propos, contact) ------------------------------------ */

.page-titre {
  margin: 1.5rem 0 .625rem;
  font-size: 1.875rem;
  line-height: 1.3;
}

.page-intro {
  margin: 0 0 1.625rem;
  color: var(--discret);
  font-size: .9375rem;
}

.page-texte {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  font-size: 1rem;
  line-height: 1.75;
  color: var(--texte-article);
}

.page-texte p { margin: 0; }

/* --- Admin ------------------------------------------------------------- */

.admin-entete {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1.5rem;
  border-bottom: 1px solid var(--bordure);
  background: var(--fond-alt);
  flex-wrap: wrap;
}

.admin-entete-gauche {
  display: flex;
  align-items: center;
  gap: 1.75rem;
  flex-wrap: wrap;
}

.entete-logo-ligne {
  display: inline-flex;
  align-items: center;
  gap: .625rem;
  text-decoration: none;
}

.entete-logo-icone-admin {
  height: 1.75rem;
  width: auto;
}

/* .bouton-lien de base a un margin-bottom pensé pour un bouton seul en haut
   de page (voir plus bas) - annulé ici, dans le contexte en ligne de
   l'en-tête admin, et la taille resserrée pour rester à l'échelle du logo
   et de la nav à côté (retour de Thomas, 25/08/2026).
   Sélecteur qualifié par .admin-entete-gauche (pas juste .bouton-lien-entete
   seul) : .bouton-lien est définie PLUS BAS dans ce fichier, donc à
   spécificité égale (une classe) c'est elle qui gagnait en cascade et le
   margin-bottom:0 ci-dessous ne s'appliquait jamais - piège trouvé en
   mesurant les positions réelles (le bouton restait collé au ancien
   margin-bottom:1.5rem, ce qui le décalait visuellement vers le haut dans
   le centrage flex de l'en-tête). Qualifier avec le parent règle le conflit
   sans dépendre de l'ordre des règles dans le fichier. */
.admin-entete-gauche .bouton-lien-entete {
  margin-bottom: 0;
  padding: .4rem 1rem;
  font-size: .9rem;
}

.admin-nav {
  display: flex;
  gap: 1.25rem;
  font-size: .9rem;
  /* flex-wrap: wrap (25/08/2026) - sans ça, la nav (7 liens) refusait de
     rétrécir sous sa largeur totale non coupée, ce qui empêchait
     .admin-entete-gauche de passer à la ligne malgré son propre
     flex-wrap:wrap - le bouton "+ Nouvelle news" nouvellement ajouté
     débordait hors de l'écran sur mobile plutôt que de passer dessous.
     Repéré en testant à 375px de large, pas visible en desktop. */
  flex-wrap: wrap;
}

.admin-nav a {
  text-decoration: none;
  color: var(--discret);
}

.admin-nav a:hover {
  color: var(--texte);
}

.admin-deconnexion {
  display: flex;
  align-items: center;
  gap: .75rem;
  font-size: .85rem;
  color: var(--discret);
}

.lien-bouton {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  text-decoration: underline;
  cursor: pointer;
}

/* Messages flash */

.flash {
  margin: 0 0 1.5rem;
  padding: .75rem 1rem;
  border-radius: var(--rayon-panneau);
  font-size: .9rem;
}

.flash-succes {
  background: var(--ok-fond);
  color: var(--ok-texte);
}

.flash-erreur {
  background: color-mix(in srgb, var(--ko) 12%, transparent);
  color: var(--ko);
}

/* Formulaires */

.formulaire {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  max-width: 36rem;
  margin: 0;
}

.formulaire-etroit {
  max-width: 24rem;
}

/* Formulaire public (contact) : mêmes champs/boutons que l'admin, mais sans
   le plafond de largeur pensé pour l'admin — il doit remplir sa colonne
   (.page-etroite, 920px), comme dans le design. */
.formulaire-pleine-largeur {
  max-width: none;
}

/* Formulaire d'édition news (admin/news-form.ejs) : 2 colonnes internes
   (voir .editeur-news-colonnes plus bas) — le plafond de 36rem pensé pour un
   formulaire à une seule colonne serait bien trop étroit ici. */
.formulaire-large {
  max-width: none;
}

.formulaire label {
  margin-top: .75rem;
  font-size: .85rem;
  font-weight: 600;
  color: var(--discret-fort);
}

.formulaire input[type="text"],
.formulaire input[type="email"],
.formulaire input[type="password"],
.formulaire input[type="url"],
.formulaire input[type="number"],
.formulaire input[type="datetime-local"],
.formulaire input[type="file"],
.formulaire select,
.formulaire textarea {
  padding: .55rem .7rem;
  border: 1px solid var(--bordure-forte);
  border-radius: var(--rayon-controle);
  background: var(--fond-panneau);
  color: var(--texte);
  font: inherit;
  font-size: .9375rem;
  width: 100%;
  outline: none;
}

.formulaire textarea {
  resize: vertical;
}

#body,
#prompt {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85rem;
}

.formulaire button[type="submit"],
.formulaire > button {
  align-self: flex-start;
  margin-top: 1.25rem;
  padding: .6rem 1.375rem;
  border: none;
  border-radius: var(--rayon-controle);
  background: var(--accent);
  /* Texte sombre plutôt que blanc — même correctif de contraste que
     .onglet-actif plus haut. Le fond ne s'assombrit plus au survol (voir
     .bouton-cible pour le raisonnement complet) ; le retour au clic vient du
     léger tassement déjà défini plus bas (.formulaire button[type='submit']:active).
     --sur-accent (fixe, pas --texte) : voir la note sur ce token plus haut. */
  color: var(--sur-accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.champ-marques {
  margin-top: .75rem;
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-controle);
  padding: .75rem;
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.25rem;
}

.champ-marques legend {
  font-size: .85rem;
  font-weight: 600;
  padding: 0 .35rem;
}

.generation-ia {
  margin-bottom: 1.5rem;
  border: 1px dashed var(--bordure-forte);
  border-radius: var(--rayon-panneau);
  padding: .75rem;
}

.generation-ia legend {
  font-size: .85rem;
  font-weight: 600;
  padding: 0 .35rem;
}

.generation-ia-ligne {
  display: flex;
  gap: .5rem;
}

.generation-ia-ligne input {
  flex: 1;
  padding: .55rem .7rem;
  border: 1px solid var(--bordure-forte);
  border-radius: var(--rayon-controle);
  background: var(--fond-panneau);
  color: var(--texte);
  font: inherit;
}

.case-marque {
  display: flex;
  align-items: center;
  gap: .35rem;
  font-size: .9rem;
  font-weight: 400;
}

.apercu-image {
  max-width: 12rem;
  border-radius: var(--rayon-controle);
  margin: .5rem 0;
  display: block;
}

/* Vignettes dérivées (Liste/Grille, voir plus bas) : plus petites que
   l'image principale - ce sont des miniatures de contrôle, pas l'image de
   travail. */
.apercu-image--derivee {
  max-width: 8rem;
}

/* Classe partagée par les 3 conteneurs d'aperçu (image principale +
   2 vignettes dérivées) - id="apercu-image-conteneur" reste sur le premier
   pour le JS (getElementById), mais la mise en page elle-même doit
   s'appliquer aux 3. */
.apercu-image-conteneur {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .35rem;
}

/* 2 vignettes dérivées de l'image de base (retour de Thomas, 20/08/2026) :
   côte à côte sur desktop, empilées si la place manque. */
.vignettes-derivees {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  margin: .5rem 0 1.25rem;
}

.vignette-derivee {
  flex: 1 1 12rem;
}

.vignette-derivee-titre {
  margin: 0 0 .35rem;
  font-size: .85rem;
  font-weight: 600;
}

.vignette-derivee-vide {
  margin: 0;
}

/* Listes admin (catégories, marques, news, messages) — grille CSS plutôt
   qu'un vrai <table>, pour pouvoir faire de chaque ligne un unique <form>
   (impossible avec <tr>/<td>, qui n'acceptent pas <form> comme enfant
   direct). */

.grille-admin {
  display: grid;
  gap: 0;
  margin-bottom: 2rem;
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-panneau);
  overflow: hidden;
  background: var(--fond-panneau);
}

/* Colonne « News » (nombre de news dans la catégorie) ajoutée le 23/08/2026,
   entre l'URL et l'ordre d'affichage. */
.grille-admin-categories { grid-template-columns: 2fr 2fr 4rem 5rem auto; }
.grille-admin-marques { grid-template-columns: 2fr 2fr auto; }
.grille-admin-news { grid-template-columns: 6rem 3fr 1fr 7rem auto; }
.grille-admin-messages { grid-template-columns: 6rem 14rem 2fr 6rem 8rem; }
.grille-admin-textes { grid-template-columns: 16rem 1fr 8rem; }

.texte-cle {
  display: flex;
  flex-direction: column;
  gap: .2rem;
  font-size: .85rem;
  word-break: break-word;
}

.ligne-admin textarea {
  padding: .35rem .5rem;
  border: 1px solid var(--bordure-forte);
  border-radius: var(--rayon-controle);
  background: var(--fond-panneau);
  color: var(--texte);
  font: inherit;
  font-size: .9rem;
  width: 100%;
  resize: vertical;
}

.ligne-admin {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / -1;
  align-items: center;
  gap: .75rem;
  padding: .6rem 1rem;
  border-top: 1px solid var(--bordure);
}

.ligne-admin:first-child { border-top: none; }

.ligne-admin-entete {
  font-size: .8rem;
  font-weight: 600;
  color: var(--discret);
  background: var(--fond-alt);
}

.ligne-admin input {
  padding: .35rem .5rem;
  border: 1px solid var(--bordure-forte);
  border-radius: var(--rayon-controle);
  background: var(--fond-panneau);
  color: var(--texte);
  font: inherit;
  font-size: .9rem;
  width: 100%;
}

.champ-court { max-width: 5rem; }

.cellule-discrete {
  color: var(--discret);
  font-size: .85rem;
}

.message-contenu {
  color: var(--discret-fort);
  font-size: .85rem;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.actions-ligne {
  display: flex;
  gap: .5rem;
  justify-self: end;
}

.actions-ligne button,
.ligne-admin > button {
  padding: .35rem .75rem;
  border: 1px solid var(--bordure-forte);
  border-radius: var(--rayon-controle);
  background: var(--fond-panneau);
  color: var(--texte);
  font: inherit;
  font-size: .85rem;
  cursor: pointer;
}

.bouton-danger {
  color: var(--ko);
  border-color: var(--ko);
}

.badge {
  padding: .15rem .55rem;
  border-radius: 999px;
  background: var(--fond-alt);
  color: var(--discret-fort);
  font-size: .75rem;
  white-space: nowrap;
}

.badge-publie {
  background: color-mix(in srgb, var(--ok) 16%, transparent);
  color: var(--ok);
}

.badge-brouillon {
  background: var(--bordure);
  color: var(--discret-fort);
}

/* News proposée par un visiteur (voir routes/soumission.js) : couleur
   accent plutôt que neutre comme les 2 badges ci-dessus - volontairement
   plus visible, pour attirer l'œil sur ce qui attend une relecture. */
.badge-visiteur {
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  color: var(--accent);
}

/* Regroupe le titre + son éventuel badge "Proposée" dans une seule cellule
   de la grille admin (voir .grille-admin-news, subgrid à colonnes fixes) -
   sans ce conteneur, le badge deviendrait un 6e item de grille et décalerait
   toutes les colonnes suivantes. */
.cellule-titre-news {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
}

.bouton-lien {
  display: inline-block;
  margin-bottom: 1.5rem;
  padding: .6rem 1.375rem;
  border-radius: var(--rayon-controle);
  background: var(--accent);
  /* Même correctif de contraste que .bouton-cible / .formulaire button
     (--sur-accent fixe, pas --texte). */
  color: var(--sur-accent);
  text-decoration: none;
  font-weight: 600;
}

/* Éditeur de news (admin/news-form.ejs) : voir .formulaire-large et
   .editeur-news-colonnes ci-dessous. Jusqu'au 21/08/2026, l'aperçu
   (.panneau-apercu) vivait ici en 3e colonne flex à côté du formulaire —
   déplacé DANS la colonne de contenu, sous l'article (retour de Thomas :
   3 colonnes aurait fini par écraser la colonne réglages ou pousser l'aperçu
   hors champ sur un portable 16"). .panneau-apercu reste stylé plus bas,
   simplement comme un bloc de plus dans .colonne-contenu désormais. */

.editeur-news-colonnes {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 21rem;
  gap: 2.5rem;
  align-items: start;
}

/* En dessous de 60rem (fenêtre réduite, pas plein écran) : une seule
   colonne plutôt que de continuer à écraser la colonne de contenu. Choisi en
   dur plutôt qu'avec un auto-fit/minmax comme .liste-news (voir plus haut) :
   les 2 colonnes n'ont pas la même largeur cible ici (contenu large, réglages
   étroite et fixe), un auto-fit s'y prêterait mal. */
@media (max-width: 60rem) {
  .editeur-news-colonnes {
    grid-template-columns: 1fr;
  }
}

.colonne-contenu,
.colonne-parametres {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  /* Évite qu'un enfant de grid (ex. le <textarea> Article rempli d'un long
     mot/URL sans espace) ne pousse toute la grille plus large que son
     conteneur — comportement par défaut d'un enfant de grid (min-width:auto),
     à écraser explicitement. */
  min-width: 0;
}

/* Bloc "Publication" (Publier / Enregistrer en brouillon / Prévisualiser),
   en haut de la colonne de droite — retour explicite de Thomas, 21/08/2026 :
   remplace l'ancien select Statut + le bouton unique "Enregistrer" du bas de
   page. Position collante : reste visible en scrollant un article long dans
   la colonne de gauche, ce qui répond directement à la plainte de départ
   ("trop de scroll avant d'arriver aux actions"). */
.bloc-publication {
  position: sticky;
  top: 1rem;
  display: flex;
  flex-direction: column;
  gap: .6rem;
  padding: 1rem 1.1rem 1.2rem;
  border: 1px solid var(--bordure-forte);
  border-radius: var(--rayon-panneau);
  background: var(--fond-panneau);
}

.bloc-publication-titre {
  margin: 0;
  font-size: 1rem;
}

.bloc-publication-statut {
  margin: 0;
  font-size: .85rem;
  color: var(--discret-fort);
}

/* Les boutons de ce bloc ne sont pas des enfants directs de .formulaire (ils
   sont nichés dans .colonne-parametres > .bloc-publication) — même piège déjà
   documenté sur .bouton-secondaire/#bouton-valider-recadrage plus bas : les
   règles ".formulaire > button" ne les concernent pas. Le bouton submit
   (type="submit") est lui bien couvert par ".formulaire button[type='submit']"
   (sélecteur descendant, sans ">"), il ne lui manque que la largeur pleine. */
.bloc-publication button[type="submit"] {
  align-self: stretch;
  margin-top: 0;
  width: 100%;
}

.bloc-publication-secondaires {
  display: flex;
  gap: .5rem;
}

.bloc-publication-secondaires .bouton-secondaire {
  flex: 1;
  text-align: center;
}

.bloc-publication label {
  margin-top: .4rem;
}

.actions-formulaire {
  display: flex;
  gap: .75rem;
  align-items: center;
}

.actions-formulaire button {
  margin-top: 1.25rem;
}

.bouton-secondaire {
  /* Bug préexistant trouvé le 15/08/2026 (signalé par Thomas sur le bouton
     "Prévisualiser") : cette classe ne définissait QUE la couleur, en
     supposant hériter du padding/rayon/police de ".formulaire > button" —
     qui ne matche que les boutons ENFANTS DIRECTS de .formulaire. Les deux
     boutons qui utilisent .bouton-secondaire (#bouton-apercu, #bouton-generer)
     sont nichés plus profond (dans .actions-formulaire / .generation-ia-ligne),
     donc ne recevaient jamais ce style de base — d'où un bouton "nu". Rendu
     autonome ici plutôt que dépendant d'un sélecteur qu'il ne matche pas. */
  padding: .6rem 1.375rem;
  border-radius: var(--rayon-controle);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  background: var(--fond-panneau) !important;
  color: var(--texte) !important;
  border: 1px solid var(--bordure-forte) !important;
}

.bouton-secondaire:hover {
  border-color: var(--accent) !important;
}

.apercu-image-actions {
  display: flex;
  align-items: center;
  gap: .75rem;
  margin-top: .5rem;
}

/* --- Modale de recadrage (Cropper.js) --------------------------------------
   Ajoutée le 20/08/2026 (backlog groupe A #4) - voir views/admin/
   _modale-recadrage.ejs pour le balisage, public/js/admin.js pour la
   logique. Overlay plein écran plutôt qu'un panneau latéral comme
   .panneau-apercu : le recadrage a besoin de toute la place possible pour
   rester précis. */
.modale-recadrage {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: rgba(0, 0, 0, .6);
}

/* [hidden] doit gagner sur ".modale-recadrage" ci-dessus : même spécificité
   (0,1,0 chacun), donc c'est l'ordre dans la feuille qui déciderait sans
   cette règle - et le "display:none" par défaut du navigateur pour [hidden]
   vit dans SA feuille de style (UA, la plus faible priorité), pas dans
   celle-ci. Sans cette règle explicite, la modale s'affichait dès le
   chargement de la page (bug trouvé en testant, 20/08/2026). */
.modale-recadrage[hidden] {
  display: none;
}

.modale-recadrage-panneau {
  width: 100%;
  /* 1400px demandé explicitement par Thomas (20/08/2026), en px plutôt
     qu'en rem pour matcher exactement ce chiffre plutôt qu'une conversion
     approximative. */
  max-width: 1400px;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--fond-panneau);
  border-radius: var(--rayon-panneau);
  border: 1px solid var(--bordure);
  overflow: hidden;
}

.modale-recadrage-entete {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .75rem;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--bordure);
}

.modale-recadrage-entete h2 {
  margin: 0;
  font-size: 1.05rem;
}

.modale-recadrage-ratios {
  display: flex;
  gap: .5rem;
}

/* Ratio actuellement sélectionné (voir admin.js) - même traitement visuel
   que .onglet-actif ailleurs sur le site : fond plein, pas juste une
   bordure, pour qu'il soit repérable d'un coup d'œil. */
.modale-recadrage-ratios button.ratio-actif {
  background: var(--accent) !important;
  color: var(--sur-accent) !important;
  border-color: var(--accent) !important;
}

.modale-recadrage-zone {
  flex: 1;
  min-height: 20rem;
  /* Agrandi en cohérence avec la largeur max (retour de Thomas) : un
     panneau bien plus large avec la même hauteur qu'avant aurait laissé
     une zone d'édition disproportionnée. */
  max-height: 44rem;
  background: var(--fond-alt);
  /* Cropper.js recalcule sa mise en page sur l'image dès qu'elle est visible
     et dimensionnée - cette zone doit donc avoir une hauteur fixe (pas
     "auto") avant même que Cropper.js s'initialise. */
}

.modale-recadrage-zone img {
  display: block;
  max-width: 100%;
}

.modale-recadrage-pied {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .75rem;
  padding: 1rem 1.25rem;
  border-top: 1px solid var(--bordure);
}

/* Taille du recadrage en pixels réels (voir admin.js, événement "crop" de
   Cropper.js) - retour de Thomas : savoir si un recadrage trop serré sur
   une petite image produirait un fichier final trop petit. */
#taille-recadrage {
  margin: 0;
  font-size: .8rem;
  color: var(--discret);
}

.modale-recadrage-pied-boutons {
  display: flex;
  gap: .75rem;
}

/* Bouton principal autonome (même leçon que .bouton-secondaire plus haut :
   ne pas supposer hériter de ".formulaire > button", ce bouton est niché
   trop profond pour le matcher). */
#bouton-valider-recadrage {
  padding: .6rem 1.375rem;
  border: none;
  border-radius: var(--rayon-controle);
  background: var(--accent);
  color: var(--sur-accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

/* Depuis le 21/08/2026 : bloc normal DANS .colonne-contenu (voir plus haut),
   plus un flex sibling à côté du formulaire — donc plus de flex-basis ici,
   juste un espacement au-dessus pour bien le détacher visuellement du champ
   Article juste au-dessus (le gap de .35rem de .colonne-contenu, pensé pour
   des champs de formulaire, serait trop serré pour un panneau entier). */
.panneau-apercu {
  margin-top: 1rem;
  border: 1px solid var(--bordure);
  border-radius: var(--rayon-panneau);
  padding: 1.5rem;
  max-height: 90vh;
  overflow-y: auto;
  background: var(--fond-panneau);
}

.panneau-apercu h2 {
  margin-top: 0;
  font-size: 1.1rem;
}

/* --- Mouvement restreint (MOTION_INTENSITY ~3, passe design du 15/08/2026) -
   Uniquement des transitions CSS sur des états déjà existants (hover/focus/
   active), zéro JS ajouté, zéro boucle continue. Toujours désactivé sous
   prefers-reduced-motion. --------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  a,
  button,
  input,
  select,
  textarea,
  .onglet,
  .bascule-vue-bouton,
  .carte-news,
  .carte-news-editer,
  .bouton-cible-fleche {
    transition: color .16s var(--ease-doux), background-color .16s var(--ease-doux),
      border-color .16s var(--ease-doux), box-shadow .16s var(--ease-doux),
      transform .16s var(--ease-doux), opacity .16s var(--ease-doux);
  }

  /* Retour tactile : un léger tassement au clic sur les boutons d'action,
     plutôt qu'un simple changement de couleur instantané. */
  .bouton-cible:active,
  .bouton-lien:active,
  .formulaire button[type='submit']:active,
  .formulaire > button:active,
  .onglet:active,
  .bascule-vue-bouton:active {
    transform: scale(.97);
  }

  /* Léger relief au survol des cartes de news (liste et grille) — remplace
     l'absence totale d'état hover par un signal discret que la carte est
     cliquable. Pas d'ombre portée : le reste du site n'en a aucune (avis de
     Thomas le 15/08/2026) - juste la bordure qui fonce et un léger décalage. */
  .carte-news:hover {
    border-color: var(--bordure-forte);
    transform: translateY(-2px);
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* Apparition douce des cartes au scroll — CSS natif (animation-timeline),
   pas de librairie JS. Protégé par @supports : dans les navigateurs qui ne
   le prennent pas en charge, la règle entière est ignorée et les cartes
   restent visibles normalement (aucun risque de contenu invisible). */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .carte-news {
      animation: apparition-carte linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 15%;
    }
  }
}

@keyframes apparition-carte {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: translateY(0); }
}

/* --- Header mobile : icône revenue à une taille fixe simple -------------- */
/* Les hauteurs mesurées de .entete-logo-icone (4.5rem / 5.8rem) sont
   calibrées pour le desktop, où la tagline tient sur 1-2 lignes. En dessous
   de 40rem (même seuil que .carte-news plus haut), la tagline peut passer à
   3-4 lignes selon la largeur de l'écran : essayer de faire correspondre la
   hauteur de l'icône à ce texte qui s'étire de façon continue produirait une
   icône énorme et disproportionnée. Plus simple et plus robuste : revenir à
   une icône de taille fixe et modeste, centrée verticalement à côté du
   texte - le même principe que l'ancien design, juste rapproché du texte
   (voir le gap réduit sur .entete-public-interieur). */
@media (max-width: 40rem) {
  .entete-public-interieur {
    align-items: center;
  }

  .entete-logo-icone,
  .entete-public-interieur:not(.page-etroite) .entete-logo-icone {
    height: 3.25rem;
  }

  /* Bug corrigé le 17/08/2026 (signalé par Thomas, capture d'écran mobile) :
     .entete-recherche-form a une largeur fixe (16.25rem) et ne rétrécit pas
     (flex-shrink:0), alors que .entete-logo-bloc juste à côté est en
     flex:1 + min-width:0 (rétrécissement illimité). Sur une largeur d'écran
     courante comme 414px (iPhone), le champ de recherche restait accroché
     sur la même ligne que le logo+tagline au lieu de passer dessous : c'est
     le bloc texte qui encaissait tout le manque de place en s'écrasant à
     une largeur minuscule (tagline à un mot par ligne), avec le champ de
     recherche qui semblait se superposer par-dessus. Fix : forcer le champ
     de recherche à occuper toute la largeur de la ligne suivante en dessous
     du logo, comme un bloc à part - flex-basis:100% force le
     retour à la ligne (le form ne peut plus tenir à côté du reste), plus
     robuste qu'un ajustement de largeurs au pixel près. */
  .entete-recherche-form {
    flex-basis: 100%;
    width: 100%;
    align-self: stretch;
  }
}

/* --- Page 404 (views/erreur.ejs) -----------------------------------------
   Demande explicite de Thomas (20/08/2026) : une page ludique plutôt qu'un
   message générique - faite à la main, sans passer par la skill /design,
   dans la charte existante (mêmes tokens, même classe .bouton-lien que le
   reste du site). Reprend le thème "atelier créatif" (gravure laser,
   impression 3D, CNC) : le visuel joue sur "cette page a raté sa cible"
   (bullseye + tir parti à côté). Seul le 404 a ce traitement, voir le
   commentaire dans erreur.ejs pour le cas 500. --accent ne changeant jamais
   entre les deux thèmes (voir plus haut), le SVG en currentColor n'a besoin
   d'aucun réglage sombre séparé. */
.page-404 {
  max-width: 32rem;
  margin: 3.5rem auto;
  text-align: center;
}

.page-404-visuel {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  margin-bottom: 1.5rem;
  color: var(--accent);
}

.page-404-cible {
  flex-shrink: 0;
}

.page-404-chiffre {
  font-family: 'Space Grotesk', sans-serif;
  font-weight: 600;
  font-size: 4.5rem;
  line-height: 1;
  color: var(--texte);
}

.page-404-titre {
  margin: 0 0 .75rem;
}

.page-404-texte {
  color: var(--discret-fort);
  line-height: 1.6;
  margin: 0 0 1.75rem;
}

/* Un seul passage à l'arrivée sur la page, jamais de boucle continue - même
   politique de mouvement que le reste du site (voir la section "Mouvement
   restreint" plus haut). */
@media (prefers-reduced-motion: no-preference) {
  .page-404-visuel {
    animation: page-404-apparition .5s var(--ease-doux) both;
  }
}

@keyframes page-404-apparition {
  from { opacity: 0; transform: translateY(10px) scale(.92); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

@media (max-width: 40rem) {
  .page-404-chiffre {
    font-size: 3.25rem;
  }

  .page-404-cible {
    width: 56px;
    height: 56px;
  }
}
