/* ==========================================================================
   OMNIA — Socle : remise à zéro, typographie, primitives de mise en page
   ========================================================================== */

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

* { margin: 0; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Compense la barre de navigation collante lors d'un saut d'ancre. */
  scroll-padding-top: 6rem;
  overflow-x: hidden;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  min-height: 100svh;
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  letter-spacing: var(--track-normal);
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  font-variant-numeric: tabular-nums;
  /* Ce `hidden` porte plus qu'il n'en a l'air, dans les deux sens.
   *
   * Il coupe pour de bon : `html` seul ne suffit pas. Sa valeur remonte bien
   * au viewport, mais le viewport reste alors *défilable* en largeur —
   * mesuré sur trois écrans (accueil, boutique, révisions), la page se
   * laissait tirer de 7 à 71 pixels sur le côté dès que cette ligne
   * disparaissait. C'est le corps qui referme cette porte.
   *
   * En retour, il fait du corps une boîte de défilement au sens du moteur,
   * avec un `overflow-y: auto` implicite. Et `position: sticky` se cale sur
   * la plus proche boîte de défilement : tout élément collant d'Omnia se
   * cale donc sur un corps qui ne défile jamais, et ne colle jamais. Un
   * `position: sticky` posé ici ne fera rien — c'est `fixed` qu'il faut, et
   * `.defis__envoi` en donne l'exemple. */
  overflow-x: hidden;
  /* L'app vit aussi en plein écran installé : on respecte les encoches. */
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* Grain : une trame imperceptible qui retire au produit son aspect « écran ».
   Sans elle, les grandes surfaces plates paraissent numériques et bon marché. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: var(--grain-opacity);
  mix-blend-mode: var(--grain-blend);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

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

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

button { background: none; border: 0; padding: 0; cursor: pointer; }

a {
  color: inherit;
  text-decoration: none;
  text-underline-offset: 0.18em;
}

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

hr {
  border: 0;
  height: 1px;
  background: var(--hairline);
}

::selection {
  background: var(--accent-wash);
  color: var(--ink);
}

/* Anneau de focus : visible, calme, jamais bleu système. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--r-xs);
}

:focus:not(:focus-visible) { outline: none; }

/* — Barres de défilement discrètes ————————————————————————————— */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--hairline-strong) transparent;
}

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: var(--hairline-strong);
  border: 3px solid transparent;
  border-radius: var(--r-full);
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background-color: var(--ink-4); background-clip: content-box; }

/* ==========================================================================
   Typographie
   ========================================================================== */

h1, h2, h3, h4, h5 {
  font-weight: 500;
  line-height: var(--leading-snug);
  letter-spacing: var(--track-tight);
  text-wrap: balance;
}

.display {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: var(--leading-tight);
  letter-spacing: var(--track-display);
  text-wrap: balance;
}

.display--1 { font-size: var(--display-1); }
.display--2 { font-size: var(--display-2); }
.display--3 { font-size: var(--display-3); }
.display--4 { font-size: var(--display-4); }

/* Étiquette en petites capitales : structure les sections sans titre criard. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--text-2xs);
  font-weight: 600;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--ink-3);
}

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

.lede {
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
  color: var(--ink-2);
  max-width: var(--w-prose);
  text-wrap: pretty;
}

.muted { color: var(--ink-2); }
.faint { color: var(--ink-3); }

.numeral {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.prose {
  max-width: var(--w-prose);
  font-size: var(--text-lg);
  line-height: var(--leading-loose);
  color: var(--ink-2);
  text-wrap: pretty;
}

.prose > * + * { margin-top: var(--s5); }
.prose strong { color: var(--ink); font-weight: 600; }
.prose em { font-family: var(--font-display); font-style: italic; }

.prose a {
  color: var(--accent-ink);
  text-decoration: underline;
  text-decoration-color: var(--accent-line);
  text-decoration-thickness: 1px;
  transition: text-decoration-color var(--dur-2) var(--ease);
}

.prose a:hover { text-decoration-color: var(--accent); }

/* ==========================================================================
   Primitives de mise en page
   ========================================================================== */

.shell {
  width: 100%;
  max-width: var(--w-content);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.shell--wide { max-width: var(--w-wide); }
.shell--narrow { max-width: var(--w-narrow); }
.shell--prose { max-width: calc(var(--w-prose) + 6rem); }

.stack { display: flex; flex-direction: column; }
.stack--1 { gap: var(--s1); }
.stack--2 { gap: var(--s2); }
.stack--3 { gap: var(--s3); }
.stack--4 { gap: var(--s4); }
.stack--5 { gap: var(--s5); }
.stack--6 { gap: var(--s6); }
.stack--7 { gap: var(--s7); }
.stack--8 { gap: var(--s8); }

.row {
  display: flex;
  align-items: center;
  gap: var(--s3);
}

.row--between { justify-content: space-between; }
.row--end { justify-content: flex-end; }
.row--center { justify-content: center; }
.row--top { align-items: flex-start; }
.row--baseline { align-items: baseline; }
.row--wrap { flex-wrap: wrap; }
.row--tight { gap: var(--s2); }
.row--loose { gap: var(--s5); }

.grow { flex: 1 1 auto; min-width: 0; }
.shrink-0 { flex: 0 0 auto; }

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

/* Grille automatique : la carte impose sa largeur minimale, la grille suit. */
.grid--auto {
  grid-template-columns: repeat(auto-fill, minmax(min(100%, var(--min, 17rem)), 1fr));
}

.section {
  padding-block: var(--section-gap);
}

.section + .section { padding-block-start: 0; }

.section__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s5);
  margin-block-end: var(--s6);
  flex-wrap: wrap;
}

.section__title {
  font-family: var(--font-display);
  font-size: var(--display-4);
  font-weight: 400;
  letter-spacing: var(--track-tight);
}

/* Deux usages sous un même nom, et un seul convient à la mise en boîte
   flexible : le lien de section (« Boutique › ») aligne une icône et un
   libellé, le paragraphe de section porte de la prose.

   En flex, chaque élément en ligne devient un élément flexible : un
   `<strong>` au milieu d'une phrase sortait de la colonne au lieu de passer
   à la ligne avec elle, et « Cette action est définitive. » se faisait couper
   à 320 pixels. La prose reprend donc un affichage de bloc, où le texte se
   compose comme du texte. */
.section__aside {
  display: block;
  text-wrap: pretty;
}

a.section__aside,
span.section__aside {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  font-size: var(--text-sm);
  color: var(--ink-3);
  text-wrap: pretty;
  transition: color var(--dur-2) var(--ease);
}

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

/* Le chevron d'un lien de section ne se détache jamais de son libellé. */
.section__aside .icon { flex: none; }

/* — Utilitaires ——————————————————————————————————————————————— */

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

.hairline { height: 1px; background: var(--hairline); border: 0; }

.text-center { text-align: center; }
.text-right { text-align: right; }
.nowrap { white-space: nowrap; }

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.clamp-2,
.clamp-3 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.clamp-2 { -webkit-line-clamp: 2; line-clamp: 2; }
.clamp-3 { -webkit-line-clamp: 3; line-clamp: 3; }

.hide { display: none !important; }

/* `hidden` doit gagner contre tout `display` déclaré par un composant :
   sans cela, un élément masqué en JavaScript resterait visible. */
[hidden] { display: none !important; }

@media (max-width: 47.99rem) {
  .hide-sm { display: none !important; }
}

@media (min-width: 48rem) {
  .only-sm { display: none !important; }
}

/* — Apparition à l'entrée ————————————————————————————————————————
   Les éléments montent de quelques pixels au chargement, décalés en cascade.
   Le décalage est piloté par --i pour rester déclaratif dans le HTML.

   Huit pixels et non quatorze : la cascade dure trois fois moins longtemps
   qu'avant, et sur une durée courte une même distance se lit comme un
   sursaut plutôt que comme une arrivée. */
@keyframes rise {
  from { opacity: 0; transform: translate3d(0, 8px, 0); }
  to   { opacity: 1; transform: none; }
}

@keyframes fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* 240 ms et 28 de décalage, au lieu de 520 et 55.
 *
 * C'est l'animation d'arrivée de presque tout le produit, et elle se rejoue
 * à chaque page. Depuis que la navigation se fait sans rechargement, on la
 * traverse en changeant d'onglet : le cinquième bloc d'une page finissait de
 * se poser huit cents millisecondes après le clic, bien après que l'écran
 * soit lisible. Une arrivée qu'on attend n'est plus une arrivée, c'est un
 * chargement. */
.rise {
  animation: rise 240ms var(--ease) both;
  animation-delay: calc(var(--i, 0) * 28ms);
}

.fade {
  animation: fade var(--dur-3) var(--ease) both;
  animation-delay: calc(var(--i, 0) * 45ms);
}

/* Révélation au défilement : appliquée par motion.js, neutralisée sans JS. */
.reveal {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
  transition:
    opacity var(--dur-4) var(--ease),
    transform var(--dur-4) var(--ease);
  transition-delay: calc(var(--i, 0) * 50ms);
}

.reveal.is-in { opacity: 1; transform: none; }

html:not(.js) .reveal { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .rise, .fade { animation: fade var(--dur-3) linear both; }
  .reveal { transform: none; }
}

/* — Transitions entre documents ————————————————————————————————
   Le navigateur anime lui-même le passage d'une page à l'autre : on obtient la
   continuité d'une application native sans construire de SPA, et les
   navigateurs qui ne savent pas faire naviguent normalement. */
@view-transition { navigation: auto; }

::view-transition-old(root) {
  animation: vt-out var(--dur-3) var(--ease-soft) both;
}

::view-transition-new(root) {
  animation: vt-in var(--dur-4) var(--ease) both;
}

@keyframes vt-out {
  to { opacity: 0; transform: scale(0.995); }
}

@keyframes vt-in {
  from { opacity: 0; transform: translate3d(0, 8px, 0); }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: 1ms; }
}

/* ==========================================================================
   Budget de composition
   --------------------------------------------------------------------------
   Omnia pose deux surfaces floutées en permanence : la barre du haut et, sur
   mobile, la barre d'onglets. Un `backdrop-filter` n'est pas un effet peint
   une fois : le navigateur refloute ce qui passe dessous à chaque image, donc
   à chaque pixel de défilement. Deux bandes pleine largeur, c'est le coût le
   plus élevé de toute la feuille de style, et c'est celui qui se paie sur les
   appareils qui ont le moins à donner.

   On ne renonce pas au verre dépoli : sur un téléphone récent il tient les
   soixante images par seconde sans effort, et il fait beaucoup pour l'aspect
   du produit. On le retire là où il coûte plus qu'il ne rapporte, selon trois
   signaux, du plus explicite au plus approximatif.

   1. `prefers-reduced-transparency`, réglage d'accessibilité posé par le
      système : la personne a demandé moins de transparence, on obéit.
   2. `.perf-eco`, posé par core.js quand l'appareil déclare peu de cœurs ou
      peu de mémoire. C'est une approximation, mais elle vise juste : ce sont
      exactement les appareils où le flou fait tomber le défilement.
   3. L'absence de prise en charge, où la couleur de repli doit rester lisible
      sans le flou qui la densifiait.

   Dans les trois cas le remplacement est la même surface, rendue opaque. Rien
   ne bouge de place : seule la texture change.
   ========================================================================== */

@media (prefers-reduced-transparency: reduce) {
  :root .topbar, :root .tabbar, :root .study__bar {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: var(--paper);
  }
}

.perf-eco .topbar,
.perf-eco .tabbar,
.perf-eco .study__bar {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: var(--paper);
}

/* Les voiles plein écran suivent la même règle, et ils comptent davantage :
   une barre floute une bande de soixante pixels, un voile floute l'écran
   entier — et il le refloute à chaque image tant qu'une carte s'anime
   au-dessus. C'est le seul flou du produit qui puisse faire tomber une
   ouverture de panneau sous les soixante images par seconde. */
.perf-eco .sheet,
.perf-eco .celebration,
.perf-eco .search-overlay {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  background: color-mix(in oklab, var(--paper-deep) 88%, transparent);
}

@media (prefers-reduced-transparency: reduce) {
  :root .sheet, :root .celebration, :root .search-overlay {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: color-mix(in oklab, var(--paper-deep) 88%, transparent);
  }
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  :root .topbar, :root .tabbar, :root .study__bar { background: var(--paper); }
}

/* Sur mobile, le rayon descend même quand le flou reste : le coût d'un
   `blur()` croît avec son rayon *et avec la surface floutée*, et sur une
   bande pleine largeur l'œil ne fait pas la différence entre vingt et douze
   pixels.
 *
 * La barre d'onglets fait exception, et le second facteur dit pourquoi :
 * elle mesure trois cent cinquante pixels sur soixante-douze, marges
 * comprises, soit un dixième de la surface d'une barre pleine largeur. Un
 * flou plus large y coûte donc dix fois moins qu'ailleurs, alors que c'est
 * l'élément qu'on regarde le plus et le seul posé *au-dessus* du contenu :
 * c'est là que la matière du verre se voit. */
@media (max-width: 47.99rem) {
  :root .topbar, :root .study__bar {
    backdrop-filter: saturate(160%) blur(12px);
    -webkit-backdrop-filter: saturate(160%) blur(12px);
  }

  :root .tabbar {
    backdrop-filter: saturate(190%) blur(22px);
    -webkit-backdrop-filter: saturate(190%) blur(22px);
  }
}

/* ==========================================================================
   Zones vivantes
   --------------------------------------------------------------------------
   Une zone qui échange son contenu ne doit pas clignoter. Elle s'estompe très
   légèrement le temps de l'aller-retour — assez pour dire « je travaille »,
   trop peu pour qu'on parle d'un chargement. Aucun sablier, aucune barre :
   l'échange dure quelques dizaines de millisecondes en local, et le moindre
   chargeur ferait paraître l'attente plus longue qu'elle ne l'est.
   ========================================================================== */

/* La zone ne s'estompe plus, et c'était le défaut central de tout l'écran.
 *
 * L'estompage se justifiait pour une zone de la taille d'un classement. Or
 * sur le Cercle, `data-live="cercle"` enveloppe *toute la page* : accepter un
 * ami faisait tomber l'écran entier à 55 % d'opacité puis le remplaçait. Ce
 * n'est pas « je travaille », c'est un rechargement — et c'est exactement ce
 * qu'on lit quand toute la page pâlit d'un coup pour un clic sur un bouton.
 *
 * Le signal d'attente a une meilleure place, et elle existe déjà : le
 * formulaire qu'on vient d'envoyer (`form.is-busy`). C'est le bon endroit —
 * le retour appartient au geste, pas à la page.
 *
 * `pointer-events` reste : pendant l'échange, la zone contient du contenu qui
 * va disparaître, et un second clic dessus enverrait une action sur un nœud
 * déjà remplacé. */
[data-live].is-swapping { pointer-events: none; }

/* Un rafraîchissement n'est pas une arrivée.
 *
 * Une zone échangée après une action est *mise à jour* : son contenu était
 * déjà là, on vient d'en changer une partie. Les animations d'entrée —
 * `.reveal` qui monte de dix-huit pixels, `.rise` en cascade — se rejouaient
 * pourtant sur tout ce que la zone contient, parce que les nœuds viennent
 * d'être créés et que le navigateur ne sait pas qu'ils remplacent des nœuds
 * identiques. Sur le Cercle, cela faisait rentrer le titre, la recherche, le
 * classement et le fil d'activité en cascade pour un ami accepté.
 *
 * On a d'abord essayé de les faire taire ici, par un `animation: none` posé le
 * temps de deux images. Cela ne marche pas, et c'est instructif : une
 * animation CSS démarre quand son nom cesse d'être `none`. La retirer puis la
 * rendre ne l'annulait pas — elle partait deux images plus tard, entière.
 *
 * La correction est en JavaScript et tient en un appel : `poserSansEntree()`
 * termine les animations d'entrée au lieu de les museler. Rien à déclarer
 * ici, et surtout pas d'`!important` générique qui empêchait justement de les
 * trouver pour les terminer.
 *
 * Reste la coupure des transitions, qui, elle, opère bien : un nœud fraîchement
 * inséré n'a pas d'ancienne valeur, mais son voisinage en a — une largeur de
 * colonne, une couleur héritée — et le recalcul provoquait des glissements
 * qui n'appartenaient à personne. Les *descendants* seulement : l'hôte garde
 * les siennes, c'est par elles que passe la réponse au geste. */
.is-sans-entree * {
  transition: none !important;
}
