/* =========================================================
   LE MUR CHAT-MAN — feuille de style
   Univers Chat-Man (coloré), distinct du noir & blanc Epoque.
   La couleur vient des post-it (photos) ; l'interface reste sobre.
   ========================================================= */

:root {
  /* Le chargement d'un post-it (postit.js, attendLImage) : il ne se montre
     que si l'image n'est pas la apres `--chargement-seuil`, et une fois
     montre il reste au moins `--chargement-min`. Le JS les lit ici. */
  --chargement-seuil: .1s;
  --chargement-min: .5s;
  /* Declarer les deux schemas coupe l'assombrissement automatique de Chrome
     Android et de Samsung Internet : ces navigateurs ne recalculent les
     couleurs eux-memes que sur une page qui se dit claire uniquement. Le mur
     annonce donc qu'il gere son propre mode sombre — c'est le bloc
     @media (prefers-color-scheme: dark) plus bas qui le fait, avec une tile
     dessinee, pas un filtre. */
  color-scheme: light dark;

  /* LE FOND N'EST PLUS ICI. Le brun (--wall-brown), la tuile (--tile, en
     `data:`), sa version de nuit et sa taille (--tuile) viennent de la peau
     Chat-Man du site : <html data-peau="chat-man">, comme la page du rayon.
     C'est le code de l'onglet Chat-Man et du panneau, a la lettre — une
     seule planche, une seule maille pour tout l'univers (peau-chat-man.css,
     epoque.css `[data-peau]`). */

  --ink:             #1a1410;   /* texte sombre */
  --paper:           #f4efe3;   /* papier crème (fond de la modale) */
  --wall-text:       #ffffff;   /* texte posé sur le monogramme : blanc pur */
  --paper-edge:      #e2d9c6;
  --sticky:          #ffe14d;   /* jaune post-it — seul accent, lié au sujet */
  --sticky-edge:     #eacf2e;

  /* Direction de l'ombre a l'ecran, en vecteur unitaire (y vers le bas).
     Source lumineuse fixee a 2h sur un cadran : l'ombre tombe a l'oppose,
     vers 8h, soit en bas a gauche. C'est la reference du mur entier, elle
     ne bouge jamais. */
  --light-x: -0.866;
  --light-y:  0.5;

  /* Rayon de flou admissible, en fraction de la distance de l'ombre. Un flou
     de rayon r deborde d'environ 0.75 r autour du bord decale : passe
     (--light-y / 0.75) x distance, l'ombre remonte au-dessus du bord haut de
     l'objet, la ou il touche son support — une ombre ne peut pas s'y trouver.
     Le rayon suit donc la distance au lieu d'etre fixe. Les valeurs en px
     ci-dessous restent la distance voulue, jamais le flou.
     La limite stricte serait --light-y / 0.75, soit x1.33 ; on garde 1.15 pour
     que le 0.75 — une approximation du flou gaussien, pas une garantie — ne
     laisse pas passer un liseré. */
  --sh-blur: calc(var(--light-y) * 1.15);

  /* Vecteur reellement utilise pour dessiner, exprime dans le repere de
     l'element. Pour tout ce qui n'est pas incline, les deux coincident. */
  --lx: var(--light-x);
  --ly: var(--light-y);

  /* Ligne de colle : hauteur de la bande plaquee au mur, en % de la note.
     C'est l'origine de tout ce qui bascule ou projette. Repli de la constante
     GLUE_BAND de postit.js, qui la repose ici au demarrage. */
  --glue: 15%;

  /* Ombres du bouton d'ajout, qui n'est pas incline aleatoirement et peut
     donc garder un vecteur fige. Les post-it, eux, composent la leur en
     calc() a partir de --lx / --ly : une custom property declaree ici
     resoudrait var(--lx) sur :root et perdrait la valeur par note. */
  --shadow-note:
      calc(var(--lx) *  2px) calc(var(--ly) *  2px) calc(var(--sh-blur) *  2px) -1px rgba(0,0,0,.45),
      calc(var(--lx) *  7px) calc(var(--ly) *  7px) calc(var(--sh-blur) *  7px) -4px rgba(0,0,0,.65);
  --shadow-note-hover:
      calc(var(--lx) *  5px) calc(var(--ly) *  5px) calc(var(--sh-blur) *  5px) -1px rgba(0,0,0,.45),
      calc(var(--lx) * 20px) calc(var(--ly) * 20px) calc(var(--sh-blur) * 20px) -6px rgba(0,0,0,.7);

  /* Contour de lettre, en deux couches. De l'interieur vers l'exterieur :
     la lettre blanche, un lisere noir mince, puis un contour brun plus large
     (#533C2E, le brun fonce de la tile monogramme). Le brun detache le mot du
     motif ; le noir, lui, ne sert qu'a poser une arete sous le blanc, qui
     sinon se dilue dans le brun a petite taille.

     Deux traits, alors qu'un element n'a qu'un seul -webkit-text-stroke : le
     noir est celui de l'element, le brun est un SECOND TRACE de la meme
     chaine, dessine par ::before a partir de data-contour et glisse derriere
     (voir « Le second trace » plus bas). Les deux sont des contours de glyphe
     vrais — nets, et rasterises avec la lettre.

     Deux techniques ecartees, dans l'ordre ou elles ont ete essayees :
     la couronne de text-shadow (seize copies empilees du texte) bavait aux
     angles et se redessinait a chaque animation ; le drop-shadow repete, lui,
     etait net de loin mais reste un FLOU — liseré mou de pres, deux passes de
     blur par element, et un filtre pose une surface de rendu et un contexte
     d'empilement sur chaque texte. Sur un mur qui charge toutes ses notes d'un
     coup, ca fait deux elements filtres par post-it. Le second trace ne coute
     qu'une rasterisation de glyphe de plus, sans couche.

     Trois calibres, parce que la lettre change de taille et de graisse d'un
     endroit a l'autre : sur du serif de 11px, le trait large fermerait les
     delies. Un -webkit-text-stroke se trace a cheval sur le contour de la
     glyphe : seule la MOITIE de la valeur deborde, des deux cotes. */
  --outline-ink: #533C2E;

  /* Le lisere noir, sur l'element lui-meme. */
  --lisere-fort:  1.4px;  /* titre, indice — grosses capitales de mono */
  --lisere-moyen: 1px;    /* intro, signature, etats du mur */
  --lisere-fin:   .7px;   /* commentaire en serif, mention de retrait */

  /* Le contour brun, sur la copie. Toujours plus large que le lisere qu'il
     borde, sinon le noir le mange. A poser en --brun sur l'element : le
     ::before en herite (et cette fois le var() est lu dans une propriete
     normale, sur le pseudo — pas dans une custom property de :root, ou il
     n'aurait rien a resoudre et rendrait la declaration invalide). */
  --brun-fort:  3.2px;
  --brun-moyen: 2.4px;
  --brun-fin:   1.6px;

  --font-mono:  "Space Mono", ui-monospace, monospace;
  --font-body:  "Space Grotesk", system-ui, sans-serif;

  --max: 1180px;
}

/* ---------- Mode sombre ----------
   Une seule chose change : le mur. La tile sombre remplace la claire, et la
   couleur de repli reprend le fond de CETTE tile-la (pas le brun clair), pour
   la meme raison qu'en clair — au-dela du document, en fin de defilement, il
   ne reste qu'elle, et un ecart se verrait comme une bande.
   Les post-it gardent leurs couleurs : ce sont des photos, elles n'ont pas de
   version nuit. La modale aussi reste creme (voir .modal-card).

   La tile sombre n'assombrit pas le motif : elle echange ses deux bruns, le
   fonce passe au fond et le clair au monogramme. Ca compte pour le contour du
   texte (--outline-ink, #533C2E, le brun fonce de la tile), qui lui ne change
   pas la nuit : sur le fond, devenu ce meme brun, le trait ne se voit plus du
   tout — c'est le monogramme, reste a #ac8a67, qui le fait lire. Le trait mord
   la ou le motif passe, et l'epaissir ne rachete rien : il lui faut un support
   clair. Une tile sombre uniformement assombrie (fond ET motif) le ferait
   disparaitre : ne pas revenir a ca.

   `:not([data-theme="clair"])` est la porte de sortie : la bascule de la
   maison (epoque.js, `bascule`) pose cet attribut sur <html> et le mur
   repasse en clair sans que rien d'autre ne change. Pas de
   [data-theme="sombre"] symetrique — la bascule n'existe que sur un telephone
   deja en sombre, donc retirer l'attribut suffit toujours a revenir a la nuit. */
/* La bascule elle-meme vit dans la peau, sur le meme attribut
   (peau-chat-man.css, `[data-peau="chat-man"]:not([data-theme="clair"])`). */

* { box-sizing: border-box; }
[hidden] { display: none !important; }

/* Pas de surlignage au toucher. Le mur se manipule au doigt : chaque appui sur
   une note, une punaise ou un bouton declenchait le voile bleu des navigateurs
   mobiles, et un appui maintenu selectionnait la photo comme un texte — deux
   marques d'interface de navigateur posees sur des objets qui doivent avoir
   l'air d'etre punaises a un mur. On les coupe partout, puis on rend la
   selection la ou elle sert vraiment : les champs et le texte de la modale. */
html {
  -webkit-tap-highlight-color: transparent;
}
body {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}
img {
  -webkit-user-drag: none;
  user-drag: none;
}
input, textarea, .modal-card {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}
/* ...sauf les commandes de la modale, qui restent des boutons. */
.modal-card button, .modal-card label {
  -webkit-user-select: none;
  user-select: none;
}

html {
  -webkit-text-size-adjust: 100%;

  /* Pas de scrollbar-gutter: stable ici. La gouttiere reservee reste vide sur
     les navigateurs a barres en surimpression : la boite de <html> perd une
     quinzaine de pixels, le motif s'y arrete net et il ne reste que la couleur
     de fond — une bande marron permanente le long du bord droit. Sans
     gouttiere, la boite occupe toute la largeur et le monogramme va jusqu'au
     bord ; quand la barre prend vraiment de la place, c'est elle qui peint la
     zone, pas un vide.
     Le risque que la gouttiere evitait (une page qui tient tout juste dans la
     fenetre se recompose quand la barre apparait) ne se pose pas avec des
     barres en surimpression, et le mur de post-it deborde de toute facon. */

  /* L'elasticite de fin de defilement decouvre le canevas au-dela du document,
     ou le motif ne s'etend pas : meme bande marron, en bas cette fois. */
  overscroll-behavior-y: none;
}

body {
  margin: 0;
  min-height: 100vh;

  /* Repere du calque des reactions : sans ancetre positionne, celui-ci se
     calerait sur la boite initiale — de la hauteur de la fenetre, et hors de
     portee de sa propre coupe. Le body etant sans marge, l'origine ne bouge
     pas d'un pixel. */
  position: relative;
  color: var(--wall-text);
  font-family: var(--font-body);

  /* LE FOND N'EST PLUS AU BODY, ni l'espace sous la derniere note : les deux
     sont au panneau (`<main class="page">`), comme sur la page du rayon —
     voir « L'ETAGE DE LA MAISON » en fin de feuille. */
}

/* ---------- Bandeau démo ---------- */
.demo-banner {
  font-family: var(--font-mono);
  font-size: 12px;
  text-align: center;
  padding: 8px 16px;
  background: var(--sticky);
  color: var(--ink);
  border-bottom: 1px solid var(--sticky-edge);
}

/* ---------- En-tête ----------
   Le titre ne coiffe plus le mur : il tient la meme bande que le monogramme EE
   et la bascule de theme, entre les deux, a leur hauteur. Fixe comme eux —
   c'est une barre, et une barre qui defile n'en est plus une : le mur est un
   long deroule, et le titre disparaissait au premier ecran en laissant les
   deux coins seuls de part et d'autre du vide.
   `pointer-events: none` parce que la bande couvre toute la largeur alors
   qu'elle ne porte qu'un mot : sans ca elle avalerait les clics des post-it
   qui passent dessous. */
.wall-head {
  position: fixed;
  top: calc(14px + env(safe-area-inset-top));
  left: 0;
  right: 0;
  /* Meme etage que le monogramme et la bascule, ses voisins de bande. */
  z-index: 40;

  /* Les deux marges laissent la place aux coins : le monogramme a gauche
     (44px au plus, plus son retrait), la bascule a droite (38px). */
  padding: 0 clamp(58px, 12vw, 96px);
  /* La hauteur de la bascule : le titre se centre sur elle plutot que sur sa
     propre ligne, et les trois objets tiennent le meme axe. */
  min-height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  pointer-events: none;
}
/* Le bandeau de demo occupe la bande : la barre descend dessous, comme le
   monogramme et la bascule. */
body:has(#demo-banner:not([hidden])) .wall-head {
  top: calc(48px + env(safe-area-inset-top));
}
/* Le mur commence sous la barre — fixe, elle ne prend pas de place dans le
   flux. 52px : les 14px de retrait plus les 38px de la bande — le mur ajoute
   ensuite son propre creux, et la premiere note ne frole pas le titre. */
main { padding-top: calc(52px + env(safe-area-inset-top)); }
/* Titre composé : "epoque"  POST-IT !  "chat-man™".
   Le flex en baseline pose les deux mentions latérales sur la ligne de
   base du grand titre plutôt que centrées sur sa hauteur. */
.wall-title {
  display: flex;
  align-items: baseline;
  justify-content: center;
  flex-wrap: wrap;
  gap: clamp(8px, 2vw, 20px);

  font-family: var(--font-mono);
  margin: 0 0 14px;
  color: var(--wall-text);
  -webkit-text-stroke: var(--lisere-fort) #000;
  paint-order: stroke fill;
  --brun: var(--brun-fort);
}
.wall-title-main {
  font-weight: 700;
  letter-spacing: .14em;
  font-size: clamp(28px, 5vw, 52px);
  line-height: 1.05;
}
.wall-title-side {
  font-weight: 400;
  font-size: clamp(10px, 1.5vw, 14px);
  letter-spacing: .22em;
  text-transform: lowercase;
  white-space: nowrap;
  /* Dix pixels a cote d'un titre qui en fait cinquante : les deux mentions
     reprennent le calibre moyen, et le sup le fin. Le calibre du titre
     fermerait ces lettres-la. */
  -webkit-text-stroke: var(--lisere-moyen) #000;
  --brun: var(--brun-moyen);
}
.wall-title-tm {
  font-size: .6em;
  letter-spacing: .06em;
  line-height: 0;   /* évite que le sup ne creuse l'interligne */
  -webkit-text-stroke: var(--lisere-fin) #000;
  --brun: var(--brun-fin);
}
/* ---------- Le second trace ----------
   Le contour brun des textes du mur (voir « Contour de lettre » en tete). Un
   element n'a qu'un seul -webkit-text-stroke, et il sert au lisere noir : le
   brun est donc une COPIE de la lettre, tracee en brun, glissee derriere.

   La copie se lit dans data-contour, que le JS repose a chaque ecriture
   (poseTexte, postit.js) — le CSS ne sait pas relire le contenu d'un noeud.
   Un element qui contient du balisage n'en met donc pas : le titre porte ses
   attributs sur ses trois mentions, pas sur le <h1> qui les range.

   La copie se cale sur la boite de rappel de son element (inset: 0 sur un
   parent positionne = sa boite de padding), reprend son padding et herite de
   tout le reste — police, corps, alignement, cesure. Elle se replie donc au
   meme endroit qu'elle, ligne pour ligne.

   z-index: -1 la passe derriere la lettre : sans lui, un ::before absolu peint
   APRES le contenu de son element et couvrirait le blanc. Et `isolation` avec,
   sinon ce -1 remonte chercher le premier contexte d'empilement au-dessus et
   la copie va se ranger derriere le mur lui-meme. */
[data-contour] {
  position: relative;
  isolation: isolate;
}
[data-contour]::before {
  content: attr(data-contour);
  position: absolute;
  inset: 0;
  padding: inherit;
  z-index: -1;

  -webkit-text-stroke: var(--brun) var(--outline-ink);
  paint-order: stroke fill;
  /* Le remplissage de la copie est brun lui aussi : la vraie lettre le
     recouvre entierement, et un blanc reste dessous ferait un halo au
     moindre decalage de rendu. */
  color: var(--outline-ink);

  /* Elle double le texte a l'ecran, pas dans l'arbre : un ::before n'est pas
     annonce, mais il resterait selectionnable a la souris. */
  pointer-events: none;
  user-select: none;
}

/* Écran serré (smartphone) : plutôt que de laisser le flex casser la ligne
   n'importe où, on répartit à la main — le grand titre seul en haut, les deux
   mentions latérales rangées dessous, toujours de part et d'autre du centre. */
@media (max-width: 480px) {
  .wall-title {
    display: grid;
    grid-template-columns: auto auto;
    grid-template-areas:
      "main main"
      "left right";
    justify-content: center;
    row-gap: 8px;
  }
  .wall-title-main {
    grid-area: main;
  }
  .wall-title-side:first-child {
    grid-area: left;
    justify-self: end;
  }
  .wall-title-side:last-child {
    grid-area: right;
    justify-self: start;
  }
}
/* Dans la barre, le titre se mesure a ses voisins de bande et non a la page :
   les valeurs plus haut sont celles du titre seul, qui peut monter. */
.wall-head .wall-title-main {
  font-size: clamp(17px, 2.6vw, 26px);
}
.wall-head .wall-title-side {
  font-size: clamp(8px, 1.1vw, 11px);
  letter-spacing: .18em;
}
.wall-head .wall-title {
  margin: 0;
  gap: clamp(6px, 1.4vw, 14px);
}

.wall-intro {
  max-width: 44ch;
  margin: 0 auto;
  font-size: clamp(15px, 2vw, 18px);
  line-height: 1.5;
  color: var(--wall-text);
  -webkit-text-stroke: var(--lisere-moyen) #000;
  paint-order: stroke fill;
  --brun: var(--brun-moyen);
}

/* ---------- Le mur (colonnes reelles, posees par le JS) ----------
   Ce mur a d'abord ete un `column-width` : une seule declaration, le navigateur
   fragmentait et repartissait. C'etait le bon outil, et il a fallu l'abandonner.

   Le multi-colonnes est le seul morceau de mise en page de cette page dont les
   moteurs divergent franchement, et WebKit y rend une geometrie fausse pour ce
   qui vit dedans — `offsetTop` et `getBoundingClientRect()` compris. Sur iPhone,
   la legende descendait de 25 px sous le papier puis reprenait sa place, et
   l'estampe d'une reaction se posait au milieu de la fenetre au lieu du post-it.
   Ce dernier chiffrait le defaut sans appel : le mur fait 347 px de large, une
   colonne 167 ; l'estampe, posee au centre du rectangle rendu pour la note,
   atterrissait a 16 + 347/2 = 189 — le milieu de la fenetre. WebKit rendait donc
   a la note la boite NON fragmentee, celle qu'elle aurait sans colonnes.

   Rien ne se corrige la-dedans : c'est le moteur qui repond a cote, et tout ce
   qui mesure une note en dependait. Des colonnes reelles — un `div` par colonne,
   des notes en flux normal dedans — rendent la geometrie exacte partout, au prix
   d'une repartition a faire soi-meme (postit.js, poserColonnes).

   `--col` est la largeur visee d'une colonne, `--gouttiere` l'ecart : le JS les
   lit ici pour appliquer la meme regle que `column-width`, et il n'y a toujours
   qu'un seul endroit ou les changer.

   `--col` est la largeur AU REPOS : les boutons + / - la multiplient par
   `--zoom` (postit.js, LE ZOOM DU MUR). Le facteur n'est volontairement pas
   applique ici en `calc()` — une propriete personnalisee non declaree garde sa
   chaine de jetons pour valeur calculee, et le `parseFloat` de `nbColonnes` n'y
   lirait plus rien. C'est donc le JS qui multiplie, apres lecture. */
.wall {
  --col: 230px;
  --gouttiere: 22px;
  max-width: var(--max);
  margin: 0 auto;
  padding: 24px 20px 40px;
  display: flex;
  /* Sans quoi les colonnes s'etirent toutes a la hauteur de la plus haute, et
     la derniere note de chaque colonne se retrouve espacee du bas. */
  align-items: flex-start;
  gap: var(--gouttiere);
}
/* `flex: 1 1 0` et non `1 1 auto` : les colonnes se partagent la largeur a
   parts egales quel que soit leur contenu. `min-width: 0` leve le plancher de
   taille minimale du flex, qui sinon empeche une image de se reduire sous sa
   largeur intrinseque et fait deborder le mur. */
.wall-col {
  flex: 1 1 0;
  min-width: 0;
}

.note {
  --rot: 0deg;
  /* La typo du pied de note se mesure sur le papier, pas sur l'ecran. Une
     taille fixe tenait tant que le mur avait des colonnes larges ; a trois
     colonnes sur un telephone, les memes 12px font une signature plus haute
     que le nom ecrit sur le post-it lui-meme. --note-w est posee par le JS
     (postit.js, fitNote) et vaut la largeur reelle du papier : le texte suit
     donc la note, quel que soit le nombre de colonnes, sans point de rupture
     a maintenir.
     Les deux bornes sont des garde-fous, pas le reglage : en bas la limite de
     lisibilite d'une chasse fixe sur un motif charge, en haut la taille qu'a
     toujours eue la legende sur grand ecran — elle ne grossit pas, elle cesse
     seulement de retrecir. */
  --pied: clamp(8.5px, calc(var(--note-w, 250px) * .058), 12px);
  /* --fit est pose par le JS : une note tournee est plus large que sa boite
     droite, et deborderait sur ses voisines. Repli a 1 — sans JS, la note
     n'est de toute facon pas tournee. */
  --fit: 1;
  /* --lx / --ly sont poses par le JS, contre-tournes de --rot : l'ombre est
     peinte dans le repere tourne de la note, donc un vecteur brut tournerait
     avec le papier et la lumiere changerait de direction d'une note a
     l'autre. En contre-tournant, elle retombe toujours a 2h a l'ecran. */
  /* Plus de `break-inside: avoid` : il empechait une note d'etre coupee en deux
     par une fracture de colonne, ce qui n'a plus de sens depuis que les colonnes
     sont des boites a part et les notes en flux normal dedans. */
  /* Repli : la colonne entiere. Un post-it detoure recoit du JS une largeur en
     px, tiree de la taille de papier a laquelle la console publie — c'est elle
     qui rend toutes les notes de la meme taille, quel que soit le cadrage de la
     photo (postit.js, fitNote et PAPIER_CIBLE). Une photo pleine — note en
     attente — n'a pas ce fichier normalise et garde la colonne.
     `margin: auto` sur les cotes : plus etroite que sa colonne, la note s'y
     centre au lieu de se coller a gauche. */
  width: 100%;
  margin: 0 auto 22px;
  /* z-index au survol n'a d'effet que sur un element positionne : sans ca la
     note qui se souleve passerait sous ses voisines. */
  position: relative;
  /* Pivot sur la ligne de colle, en haut : c'est ce point qui est fixe au
     mur. Le redressement au survol fait bouger le bas de la feuille, pas
     le haut. Sur un post-it detoure, ce haut est le vrai bord colle : la
     console publie le PNG d'aplomb et range son inclinaison en base, que le
     JS rend a la note via --rot. Avant ca, le pivot tombait sur le haut de
     la boite englobante, donc a cote du papier des que la photo penchait. */
  transform-origin: 50% 0;
  transform: rotate(var(--rot)) scale(var(--fit));
  transition: transform .25s ease;
  /* La note s'ouvre au toucher partout ou il y a un doigt : elle est donc
     cliquable, et `touch-action` lui evite le delai de double-tap. Pose
     inconditionnelle — la gater sur `(hover: none)` la privait des Android qui
     annoncent un survol (mode « site pour ordinateur », stylet, tablette) et
     des portables tactiles, justement ceux ou le doigt trainait. Sans effet la
     ou il n'y a pas de doigt. */
  cursor: pointer;
  touch-action: manipulation;
}
.note-inner {
  position: relative;
  /* Au-dessus du calque d'ombre, qui le precede dans le DOM. */
  z-index: 1;
  /* Meme pivot que la note : la ligne de colle. Voir .note.is-open. */
  transform-origin: 50% 0;
  transition: transform .25s ease;
  border-radius: 3px;
  overflow: hidden;
  background: #fff;
  line-height: 0;
}

/* ---------- Ombre portee du bord libre ----------
   Un post-it colle en haut ne projette pas une ombre uniforme tout autour :
   sa bande de colle touche le mur — la, aucune ombre n'est possible — et le
   reste s'en ecarte de plus en plus jusqu'au bord libre. L'ombre naît donc de
   rien sur la ligne de colle et s'elargit vers le bas, meme au repos.

   Geometrie : pour une source lointaine, la silhouette est deplacee de
   (--lx, --ly) x d, ou d est la distance au mur. d valant zero au depart et
   croissant lineairement jusqu'au bord libre, ce deplacement est une
   application affine — donc exprimable en une seule transformation, un
   cisaillement horizontal plus un etirement vertical, tous deux pris depuis un
   meme point. Aucune approximation : l'ombre y tombe exactement sous le papier
   et reste invisible, par construction.

   Ce point de depart n'est pas la ligne de colle mais --sh-root, plus bas. Une
   feuille qui s'enroule ne quitte pas le mur des la colle : elle reste plaquee
   un moment puis s'ecarte, et cette sortie est quadratique, pas lineaire — sur
   le premier tiers l'ecart est negligeable. Le cisaillement, lui, ne sait etre
   que lineaire : le faire partir de la colle epaississait l'ombre bien trop
   haut. La faire naitre plus bas rend le haut du biseau aussi fin qu'il doit
   l'etre, sans avoir a trancher la silhouette en segments comme .note-curl.

   --f est le decollement du bord libre, en fraction de la hauteur *sous
   --sh-root* : un nombre pur, donc l'ombre suit la taille de la note. Il est la
   seule chose que le survol change ; direction, flou et etalement en
   decoulent. */

/* Et c'est --f lui-meme qu'on anime, pas ses consequences. Une transition sur
   `filter` ou `opacity` fait promouvoir le calque d'ombre en couche composee le
   temps du decollement ; la legende, qui passe devant lui et le chevauche, est
   alors promue a son tour, et son texte part dans une texture rasterisee a
   plat, avant le rotate() de la note. Elle devenait floue au depart du
   decollement puis redevenait nette a l'arrivee — le clignotement. C'est le
   meme piege que les tranches de .note-curl, gardees en 2D pour cette raison
   (postit.js, buildCurl).
   Une propriete personnalisee, elle, s'interpole sur le thread principal :
   flou, cisaillement et etirement sont recalcules a chaque frame, rien n'est
   promu, et la legende reste dessinee avec la note — nette du debut a la fin.
   Le prix est un repeint de la silhouette floutee pendant les .25s, sur la
   seule note survolee.
   `inherits: true` : c'est la silhouette, fille du calque, qui lit --f.
   L'opacite passe par --sh-op pour la meme raison — une transition sur
   `opacity` suffirait a promouvoir le calque. Sans @property (Firefox < 128),
   rien ne s'interpole : l'ombre saute d'un etat a l'autre, la note se decolle
   normalement. */
@property --f {
  syntax: "<number>";
  inherits: true;
  initial-value: 0.078;
}
@property --sh-op {
  syntax: "<number>";
  inherits: false;
  initial-value: 0.52;
}

.note-shadow {
  --sh-root: calc(var(--glue) + 20%);
  --f: .078;
  --sh-op: .52;
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  z-index: 0;
  pointer-events: none;
  opacity: var(--sh-op);
  /* Le flou suit le decollement : plus la feuille s'ecarte du mur, plus son
     ombre est molle.
     Il est volontairement pris sous la penombre du bord libre — a peu pres sa
     moitie. Une penombre croit avec la distance au mur et vaut donc zero a la
     racine, ou le papier touche ; un flou unique, lui, s'applique partout
     pareil. Regle sur le bord libre, il debordait la-haut de son rayon entier
     tout autour de la note, du cote eclaire compris : une bave a droite, ou
     aucune ombre ne peut se trouver. En le reglant sur le milieu de l'ombre, ce
     qui s'echappe a la racine reste sous le seuil de perception, et le bord
     libre n'est qu'a peine trop net.
     Il vit ici et non sur la silhouette, pour passer *apres* la coupe — floute
     d'abord, eteint ensuite, et le flou ramenerait de l'ombre au-dessus de la
     racine. */
  filter: blur(calc(var(--f) * 34px));
  /* Le cisaillement sort de la boite, et un element en absolu fait remonter ce
     debordement jusqu'au viewport : la page grandirait, une barre de scroll
     apparaitrait (meme raison que .note-curl). Une marge de clip laisse peindre
     sans rendre scrollable ; le flou, lui, est un debordement d'encre, qui n'a
     jamais fait defiler quoi que ce soit — il passe outre le clip sans risque. */
  overflow: clip;
  overflow-clip-margin: 64px;
  /* Vers le bas, la coupe est a part. `clip-path` et non
     `overflow-clip-margin`, qui vaut pour les quatre cotes ; il passe apres le
     filtre, donc il coupe aussi le flou.

     Elle etait posee au bord bas de la boite, c'est-a-dire au bord bas du
     *fichier*. Ce qui restait alors a l'ombre pour deborder n'etait rien
     d'autre que la marge transparente laissee par le detourage (MARGE_CROP,
     3 % de l'objet — detourage.py) : une grandeur de l'image, sans rapport avec
     le mur, et la meme pour une note de 140px que pour une de 510. Trop courte,
     surtout : la silhouette etiree descend de
     (100% - --sh-root) x --ly x --f sous la boite, soit ~2.5 % de la note au
     repos et ~4.8 % note ouverte, le flou en plus — 12px et 24px sur une note
     de 510. Les 3 % suffisaient tout juste au repos et pas du tout a
     l'ouverture. L'ombre s'y faisait trancher net, a l'horizontale, au moment
     precis ou elle devait s'etaler, et le trait ne suivait pas le contour du
     papier mais le cadrage du PNG. Une photo pleine cadre, elle, n'avait aucune
     marge et perdait tout son debord.

     La coupe est donc calculee, pas posee : exactement la portee de l'ombre,
     avec les memes nombres que ceux qui la dessinent, plus 2.6 fois le rayon de
     flou pour la trainee d'encre qui bave au-dela. Elle suit --f, donc elle
     s'ouvre avec la note. Ce qu'elle retient n'est plus de l'ombre, seulement
     du vide : le bord bas visible est desormais celui de la silhouette, donc
     celui du papier.
     Repli de --ly : sans le JS, aucune note n'est tournee et le vecteur d'ombre
     vaut sa valeur nue (voir --light-y).

     Le prix est celui que la coupe payait : l'ombre atteint de nouveau la
     legende, ecrite sur le mur au pied du papier, et sa limite balaie quelques
     pixels pendant le decollement — le contour noir des lettres semble
     s'epaissir puis se reprendre. Le `z-index` de la legende n'y peut rien, il
     decide qui peint devant, pas ou l'encre s'arrete. Rendre ces pixels a la
     legende ne se peut pas non plus : la portee suit la hauteur de la note, un
     retrait fixe ne la rattrape jamais, et un retrait proportionnel decollerait
     le mot du papier. Entre une ombre guillotinee sur toutes les notes et un
     contour qui fremit sur la premiere ligne, c'est la legende qui cede — elle
     passe deja devant l'ombre, et pour cette raison-la (voir .note-sign). */
  clip-path: inset(-64px -64px calc(
      -1 * (100% - var(--sh-root)) * var(--ly, .5) * var(--f)
      - var(--f) * 88px));
  transition: --f .25s ease, --sh-op .25s ease;
}
/* La silhouette : l'image elle-meme passee au noir. Sur une photo pleine c'est
   un rectangle, sur un PNG detoure c'est le contour decoupe — un seul chemin de
   code pour les deux, et la hauteur du calque vient de l'image sans mesure. */
.note-shadow img {
  display: block;
  width: 100%;
  height: auto;
  filter: brightness(0);
  /* Coupe franche, pas un degrade : l'ombre a la meme densite partout, c'est sa
     *forme* qui s'affine vers le haut — le papier est davantage decolle en bas,
     donc il masque davantage de mur en bas. Un degrade d'opacite par-dessus
     ferait le travail une seconde fois et delaverait le bas.
     La coupe tombe sur --sh-root et nulle part ailleurs : c'est la que le
     cisaillement vaut zero, donc que l'ombre passe exactement sous le papier et
     s'y trouve deja cachee. Couper plus haut laisserait sortir la moitie du
     cisaillement qui repart de l'autre cote au-dessus du pivot ; couper plus bas
     trancherait de l'ombre visible. Le masque est applique avant la
     transformation, ses pourcentages sont donc ceux de la note. */
  -webkit-mask-image: linear-gradient(to bottom, transparent var(--sh-root), #000 var(--sh-root));
          mask-image: linear-gradient(to bottom, transparent var(--sh-root), #000 var(--sh-root));
  transform-origin: 50% var(--sh-root);
  /* skewX veut un angle : tan(a) = lx x f, et a ces valeurs (moins d'un demi
     degre) l'angle vaut sa tangente. 57.2958 convertit radians en degres. */
  transform:
      skewX(calc(var(--lx) * var(--f) * 57.2958deg))
      scaleY(calc(1 + var(--ly) * var(--f)));
  /* Pas de transition ici : --f est deja interpole sur le calque parent, la
     silhouette suit frame par frame. En ajouter une la ferait courir apres
     lui, et remettrait une couche composee la ou on vient de l'enlever. */
}
.note img {
  width: 100%;
  height: auto;
  display: block;
}
/* petite épingle discrète pour vendre le mur */
.note-inner::before {
  content: "";
  position: absolute;
  top: 7px; left: 50%;
  width: 9px; height: 9px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: rgba(0,0,0,.35);
  box-shadow: 0 1px 2px rgba(0,0,0,.4);
  z-index: 2;
}
/* La note ne se redresse pas : elle est collee, son inclinaison lui
   appartient. L'ouverture la fait basculer en 3D autour de la ligne de colle
   (transform-origin en haut), donc le bas de la feuille vient vers l'oeil et
   grandit legerement, comme un coin qui se decolle. L'axe de rotateX est
   l'horizontale de la note apres rotate() : sur un post-it redresse, c'est
   le bord colle lui-meme, et la feuille bascule autour de sa colle plutot
   que de travers.
   Garder rotate(var(--rot)) est aussi ce qui rend --lx / --ly valables
   pendant toute l'ouverture : l'inclinaison ne change pas, la contre-rotation
   reste juste. L'ordre compte, perspective() doit venir en premier.

   `.is-open` et non `:hover` : au doigt il n'y a pas de survol, c'est le clic
   qui ouvre, et les deux gestes doivent donner exactement le meme etat. Le JS
   pose la classe des `pointerenter` la ou le survol existe (postit.js,
   armNote) — c'est le meme evenement, donc la meme frame. En prime, plus de
   :hover collant sur iOS, qui gardait une note levee apres le toucher.

   La bascule porte sur .note-inner et non sur la note entiere : la signature
   est une fille de la note, elle suivrait la rotation et, etant tout en bas —
   donc loin du pivot — c'est elle qui se deplacerait le plus. Le texte est
   ecrit sur le mur, pas sur le papier : il ne bouge pas quand la feuille se
   decolle. Pas de rotate(var(--rot)) ici, la note le pose deja : l'axe de
   rotateX est donc deja l'horizontale du papier, comme avant. */
.note.is-open {
  z-index: 3;
}
/* --oeil est pose par le JS (postit.js, buildCurl) et vaut une part constante
   de la largeur de la note : sans ca, une focale en px fait fuir une petite
   note bien moins qu'une grande, et le decollement s'ecrase sur mobile. Le
   repli couvre la note dont l'image n'est pas encore chargee — le calque de
   courbure manque alors de toute facon. */
.note.is-open .note-inner {
  transform: perspective(var(--oeil, 700px)) rotateX(9deg);
}
/* Le bord libre s'ecarte du mur : l'ombre s'etale et mollit d'autant. Une
   seule valeur a changer, tout le reste en decoule. */
.note.is-open .note-shadow {
  /* La valeur physique serait .22 : pour un arc de 20 degres (CURL_ANGLE) sur
     toute la hauteur libre, le bord libre finit a L/theta x (1 - cos theta) du
     mur, soit 0.17 x cette hauteur libre, ou 0.22 ramene a la hauteur sous
     --sh-root. On reste volontairement en dessous. A .22 l'ombre bondit d'un
     facteur trois au survol et attire l'oeil plus que le papier qu'elle sert ;
     et comme le flou suit --f, elle deborde alors aussi du cote eclaire. Le
     mouvement du calque de courbure porte deja le decollement — l'ombre n'a
     qu'a le confirmer. */
  --f: .15;
  --sh-op: .46;
}

/* Post-it detoure (PNG transparent publie depuis la console de moderation).
   Le cadre blanc et le rognage sont faits pour une photo pleine : ici ils
   reboucheraient la transparence. L'ombre, elle, n'a rien de special a faire —
   le calque .note-shadow porte deja le contour decoupe, puisqu'il est fait de
   l'image elle-meme. */
.note--cut .note-inner {
  background: transparent;
  border-radius: 0;
  overflow: visible;
}
/* L'epingle flotterait dans le vide au-dessus d'un contour irregulier. */
.note--cut .note-inner::before { display: none; }

/* ---------- Les photos : ce qui les tient au mur ----------
   Tout envoi n'est pas un post-it. Une capture d'ecran, une photo imprimee,
   une image trouvee : elles n'ont pas de colle au dos, donc elles sont
   PUNAISEES ou SCOTCHEES. L'attache n'est pas un ornement pose dessus — c'est
   elle qui dit pourquoi le haut de l'image reste plaque au mur pendant que le
   bas se souleve, exactement comme la bande de colle d'un post-it. Le
   decollement lui-meme ne change pas d'une ligne : meme --glue, meme calque
   de courbure, meme ombre portee.

   Le genre se lit dans l'attache et nulle part ailleurs : pas d'attache = un
   post-it colle, comme avant. C'est la console de moderation qui la pose, note
   par note (DOCUMENTATION §A.11) ; rien ne se devine ici.

   TOUT EST EN % DE LA LARGEUR DE LA NOTE, jamais en px. Une colonne fait 230px
   sur un bureau et ~150 sur un telephone : un scotch fige en px y grossirait a
   vue d'oeil par rapport au papier qu'il tient. C'est aussi ce qui permet a la
   console d'en dessiner l'apercu sans connaitre l'echelle
   (tools/console.py, _presenter).

   Il n'y a plus qu'une attache dessinee, le scotch. La punaise l'etait aussi —
   une tete de plastique rouge bombee — et elle a ete retiree le jour ou les
   attaches en image sont arrivees (§A.12) : un objet dessine en degrades ne
   tient pas la comparaison avec un PNG, et une punaise se remplace, la ou un
   morceau de scotch translucide se dessine tres bien. La valeur 'punaise' ne
   sort plus de la base ; une ligne qui la porterait encore retomberait sur le
   post-it colle, comme n'importe quelle valeur inconnue (postit.js, ATTACHES). */
.note-attache {
  position: absolute;
  top: 0;
  left: 50%;
  z-index: 5;   /* au-dessus du calque de courbure (1) et des reactions (4) */
  pointer-events: none;
  transform: translateX(-50%);
}

/* Le scotch : une bande translucide, arrachee a la main. Les bouts en dents de
   scie sont le seul detail qui distingue un morceau de scotch d'un rectangle
   gris — d'ou le clip-path, qui les taille des deux cotes.
   Pas de box-shadow ici : clip-path coupe aussi les ombres. drop-shadow, lui,
   suit la silhouette decoupee, dents comprises. */
.note-attache--scotch {
  --scotch: 31%;
  width: var(--scotch);
  aspect-ratio: 3.2;
  transform: translateX(-50%) rotate(var(--scotch-rot, -4deg));
  /* Deux couches : les bords longs, qui accrochent la lumiere et font tout
     le poids du scotch a l'oeil, et une trainee en biais qui casse l'aplat.
     Le scotch est translucide par nature — sur un cadre blanc il ne reste
     presque que son ombre pour le dessiner, et c'est juste : un vrai morceau
     de scotch sur du papier blanc ne se voit pas autrement. */
  background:
    linear-gradient(to bottom,
      rgba(255, 255, 255, .46) 0 12%,
      rgba(255, 255, 255, .22) 12% 88%,
      rgba(255, 255, 255, .40) 88% 100%),
    linear-gradient(102deg,
      rgba(255, 255, 255, .22), rgba(255, 255, 255, .06) 44%,
      rgba(255, 255, 255, .18) 72%, rgba(255, 255, 255, .10));
  clip-path: polygon(
    3% 0%, 97% 0%, 100% 21%, 96.5% 42%, 100% 63%, 97% 84%, 99% 100%,
    2% 100%, 0% 82%, 3.5% 61%, 0% 40%, 3% 19%);
  filter: drop-shadow(calc(var(--lx, -.866) * 2px) calc(var(--ly, .5) * 2px)
                      2px rgba(0, 0, 0, .34));
}

/* ---------- Les attaches en image ----------
   Le scotch ci-dessus est dessine : un objet ferme, ecrit une fois, le meme
   sur tout le mur et toujours en haut au centre. Une attache en image ouvre la
   serie — punaise, gaffer, epingle a linge, aimant — et surtout la POSE :
   chacune a sa place, sa taille et son angle sur SA note (postit.js,
   poseAttaches ; DOCUMENTATION §A.12).

   Le calque est pose sur la note et non dans .note-inner, qui clippe
   (`overflow: hidden`) et qui se souleve : une attache doit rester plantee
   pendant que le papier s'enroule sous elle, exactement comme le scotch
   dessine. Son z-index le met au-dessus du calque de courbure (1) et des
   reactions (4).

   Pas d'ombre portee ici : elle est peinte dans le PNG. Une attache en image
   porte deja son jour — celui de la photo ou du dessin dont elle vient — et
   deux ombres valent moins qu'une. C'est la seule chose qu'une attache
   dessinee fait mieux : la sienne suit l'inclinaison de chaque note. */
.note-attaches {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 5;
  pointer-events: none;
}
/* Le fantome : la meme image, invisible, qui donne au calque exactement la
   boite du papier — cadre blanc compris, puisqu'il recoit le meme padding.
   Sans lui un calque en absolu n'a pas de hauteur, et le `top` en % des
   attaches tomberait dans le vide. Meme procede que l'ombre portee
   (.note-shadow img) et meme src : rien n'est charge deux fois. */
/* Les deux selecteurs portent le calque : `.note img` (postit.css, plus haut)
   pose deja une largeur sur toute image de la note et l'emporterait sur une
   simple classe. Meme raison que `.note-shadow img`. */
.note-attaches .note-attaches-boite {
  display: block;
  width: 100%;
  height: auto;
  visibility: hidden;
}
/* --ax / --ay sont le CENTRE de l'attache en % de la boite du papier, --aech
   sa largeur en % de la largeur de la note — comme tout le reste du mur, rien
   en pixels (§A.11). Les replis sont ceux de config.json (attaches.defaut) :
   une attache sans pose enregistree se pose en haut au centre. */
.note-attaches .note-attache-img {
  --ax: 50;
  --ay: 2;
  --aech: 20;
  --arot: 0deg;
  position: absolute;
  left: calc(var(--ax) * 1%);
  top: calc(var(--ay) * 1%);
  width: calc(var(--aech) * 1%);
  height: auto;
  transform: translate(-50%, -50%) rotate(var(--arot));
}

/* L'epingle d'origine — un point sombre dessine sur l'image — etait le
   raccourci qui tenait lieu d'attache a tout le mur. La ou une vraie attache
   est posee, elle n'a plus rien a faire. */
.note--attachee .note-inner::before { display: none; }

/* ---------- La photo punaisee : elle pivote au lieu de se lever ----------
   Une bande de colle ou un morceau de scotch tiennent tout un BORD : le papier
   n'a alors qu'un mouvement possible, s'enrouler vers l'avant, et c'est le
   decollement du mur d'origine. Une punaise ne tient qu'un POINT. Une feuille
   plantee d'un seul point ne se souleve pas — elle reste plaquee au mur et
   JOUE autour de sa punaise, comme un tirage qu'on effleure et qui se remet
   de travers.

   Deux choses, donc, et c'est tout le mode punaise :

   1. le decollement tombe au sixieme (postit.js, CURL_ANGLE_PUNAISE), et l'ombre
      portee avec lui — elle nait de l'ecart au mur, elle ne peut pas s'ouvrir
      plus que le papier ne s'ecarte ;
   2. la feuille pivote de --swing, pose par note (postit.js) et suivant le sens
      de son inclinaison : ce qui penche continue de tomber.

   Le pivot est le CENTRE DE LA PUNAISE — --pivx / --pivy, les memes reperes que
   la pose de l'attache elle-meme (--ax / --ay), sans quoi le papier tournerait
   a cote de la tete qui le tient. Les replis sont ceux de config.json
   (attaches.defaut) : haut, au centre.

   Le calque des attaches ne pivote pas : la punaise est plantee dans le MUR,
   c'est le papier qui bouge autour d'elle. C'est la toute la lecture de la
   scene, et elle ne tient qu'a ce calque laisse tranquille.

   L'origine est posee au repos comme a l'ouverture : elle ne s'interpole pas,
   et la changer en meme temps que la transformation ferait revenir la feuille
   par un autre chemin que celui de l'aller. */
/* L'INCLINAISON de la note, elle aussi, tourne autour de la punaise. Elle est
   posee sur .note (transform: rotate(var(--rot))), dont le pivot par defaut est
   la ligne de colle — le milieu du bord haut. Sur une note punaisee ce point
   n'est fixe a rien : le seul point que le mur retient est la tete de la
   punaise, et une feuille qui tournerait autour d'un autre point ferait
   glisser sa punaise sur le mur.

   Le pivot arrive en PIXELS, pose par le JS (postit.js, fitNote), la ou celui
   des calques du papier est en % : les pourcentages d'un transform-origin
   parlent de la boite de l'element, et .note est plus haute que le papier —
   elle porte aussi la legende. 2 % de la note ne sont pas 2 % du papier. Le JS
   fait la conversion, qui demande les deux hauteurs mesurees.
   Le repli couvre la frame ou la note est posee mais pas encore mesuree : le
   pivot d'un papier colle, c'est-a-dire ce que faisait le mur avant. */
.note--punaisee {
  transform-origin: var(--pivo-x, 50%) var(--pivo-y, 0);
}
.note--punaisee .note-inner,
.note--punaisee .note-shadow {
  transform-origin: calc(var(--pivx, 50) * 1%) calc(var(--pivy, 2) * 1%);
}
/* La transition du pivot est ecrite sur l'etat de REPOS, pas sur l'ouverture :
   c'est l'etat vers lequel on va qui decide de la duree, et sans elle ici le
   papier reviendrait d'un coup a la fermeture. Le calque d'ombre n'avait pas de
   transformation a interpoler jusqu'ici, d'ou la liste redite en entier. */
.note--punaisee .note-shadow {
  transition: --f .25s ease, --sh-op .25s ease, transform .25s ease;
}
/* Le calque de courbure tourne d'un bloc autour de la punaise, bande de colle
   comprise : le pliage est peint dans le canvas, dans le repere du papier, et
   il suit donc le pivot sans rien savoir de lui — la charniere du pli se
   deplace avec le reste de la feuille, exactement comme sur un tirage qu'on
   fait jouer autour de son point d'accroche.

   Encore faut-il que ce soit le MEME point que celui du papier, et sa boite de
   coupe n'est celle d'aucun des deux : elle deborde de --bleed a gauche et a
   droite (voir .note-curl) et, en hauteur, elle vaut toute la note — legende
   comprise, puisqu'elle est fille de .note et non du papier. Ses pourcentages
   ne sont donc les bons dans aucune des deux directions.

   La largeur se rattrape en pourcentages, les deux boites ayant le meme centre.
   La hauteur, non : le rapport entre le papier et la note depend de la longueur
   de la legende, que le CSS ne peut pas lire. Le pivot vertical arrive donc en
   pixels, mesure par le JS (postit.js, pivotNote) — la meme valeur que pour
   .note, les deux boites partant du meme haut. Le repli, lui, ramene au bord
   haut plutot qu'a un pourcentage de la mauvaise boite. */
.note--punaisee .note-curl {
  transform-origin:
    calc(var(--bleed) + (100% - 2 * var(--bleed)) * var(--pivx, 50) / 100)
    var(--pivo-y, 0px);
  transition: transform .25s ease;
}
/* Ecrit apres `.note.is-open .note-inner`, meme specificite : c'est l'ordre qui
   tranche. La bascule reste, tres reduite — elle est ce qui tient quand il n'y
   a pas de calque de courbure (mouvement reduit, image pas encore chargee). */
.note--punaisee.is-open .note-inner {
  transform: rotate(var(--swing, 0deg))
             perspective(var(--oeil, 700px)) rotateX(1.5deg);
}
/* Note pliee : le calque de courbure a pris la place de l'image, qui ne porte
   plus que le pivot — les deux doivent tourner ensemble, sinon l'image
   depasserait du calque qui la recouvre. Plus specifique que
   `.note--curled.is-open .note-inner`, qui remet la transformation a zero. */
.note--punaisee.note--curled.is-open .note-inner { transform: rotate(var(--swing, 0deg)); }
.note--punaisee.is-open .note-curl { transform: rotate(var(--swing, 0deg)); }
/* L'ombre suit le papier : elle pivote avec lui, et elle s'ouvre a peine —
   la feuille ne s'ecarte presque pas du mur. Valeurs prises tout pres du repos
   (.078 / .52), tres loin de l'ouverture d'un post-it colle (.15 / .46) :
   l'ombre ne peut pas s'ouvrir plus que le papier ne s'ecarte. */
.note--punaisee.is-open .note-shadow {
  --f: .086;
  --sh-op: .51;
  transform: rotate(var(--swing, 0deg));
}

/* ---------- A plat : l'image qui EST une surface collee au mur ----------
   La troisieme attache, et la seule qui ne se dessine pas. Une image scotchee
   sur tout son pourtour — un personnage entierement scotche — n'a plus de bord
   libre : rien ne s'ecarte du mur. Tout ce que le mur fait d'un papier colle
   par le haut devient alors un mensonge, et on le retire en bloc :

   - pas d'ombre portee. Elle nait du decollement (voir .note-shadow) ; sans
     ecart, il n'y a rien a projeter. C'est ce qui la pose vraiment SUR le mur
     plutot que devant lui ;
   - pas de bascule ni de calque de courbure au survol comme au clic. Le JS ne
     monte meme pas le calque (postit.js, armNote) ; la regle ci-dessous est ce
     qui tient si la classe arrive sans lui.

   Ce qui reste : `is-open` est pose comme sur toute autre note — les reactions
   se deploient, la note passe devant ses voisines. Le papier, lui, ne quitte
   plus le mur : ce qu'il lui reste pour repondre au regard tient dans sa
   surface — le grain et le tremblement, plus bas, qui ne sortent ni l'un ni
   l'autre du plan du mur. */
.note--plat .note-shadow { display: none; }
/* Ecrit apres `.note.is-open .note-inner`, et aussi specifique : c'est l'ordre
   qui tranche. La transition part avec la bascule — sans elle, il n'y a rien a
   interpoler, et une transition qui ne finit jamais garde une couche composee
   pour rien. */
.note--plat .note-inner,
.note--plat.is-open .note-inner {
  transform: none;
  transition: none;
}

/* ---------- PROTOTYPE : le grain, reponse d'une note qui ne bouge pas -------
   Tout le mur repond au regard en bougeant : le papier se leve, l'ombre
   s'ouvre. La note a plat n'a rien de tout ca — c'est le prix de sa pose, et
   c'est juste. Restait qu'elle etait la seule a ne rien dire du tout quand on
   la touchait.

   Ce qui repond ici, c'est la SURFACE : un grain qui monte sur l'image et se
   remet a bouger, comme une emulsion qu'on reveille. Le papier, lui, ne bouge
   toujours pas d'un pixel — c'est la seule regle a ne pas franchir.

   `soft-light` plutot qu'une opacite : le grain se melange a l'image au lieu
   de se poser dessus en voile gris. Une zone claire prend un grain clair, une
   zone sombre un grain sombre, et la photo garde sa densite.

   Le deplacement se fait par sauts (`steps`) et non en continu : un grain qui
   glisse est une texture qui derive, un grain qui saute est du grain. Quatre
   positions suffisent — au-dela, personne ne compte.

   A REGLER A L'OEIL : --grain-op (la force), la duree du battement, et le
   choix de `soft-light` contre `overlay` (plus contraste) ou `luminosity`. */
.note--plat .note-grain {
  --grain-op: .5;
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 120px 120px;
  mix-blend-mode: soft-light;
  /* Le masque tire de l'image : sur un PNG detoure le grain s'arrete au bord du
     papier. Meme procede que le jour du calque de courbure (.curl-light) ;
     l'image est posee par le JS, elle seule connait le fichier. */
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  opacity: 0;
  transition: opacity .25s ease;
}
.note--plat.is-open .note-grain {
  opacity: var(--grain-op);
  animation: grain-bouge .32s steps(1, end) infinite;
}
@keyframes grain-bouge {
  0%   { background-position:   0    0; }
  25%  { background-position: -37px 19px; }
  50%  { background-position:  23px -31px; }
  75%  { background-position: -17px -13px; }
  100% { background-position:   0    0; }
}
/* Un grain qui grouille est du mouvement, et il n'a rien a faire chez qui n'en
   veut pas. Le voile, lui, reste : il monte et redescend, c'est la selection
   qu'il dit. */
@media (prefers-reduced-motion: reduce) {
  .note--plat.is-open .note-grain { animation: none; }
}

/* ---------- PROTOTYPE : le tremblement ----------
   Le grain dit que la note repond ; le tremblement dit que c'est CETTE note.
   Un va-et-vient tres court et tres rapide, de l'ordre d'un demi-pixel, comme
   une surface qui vibre sans se decoller.

   Il ne contredit pas la pose a plat, et c'est toute la condition pour qu'il
   ait le droit d'exister : le mouvement reste DANS LE PLAN DU MUR. Pas de
   translation verticale qui ferait croire a un saut, pas de rotation, pas
   d'echelle — rien qui suggere que le papier prenne du champ. Une note collee
   sur tout son pourtour peut vibrer ; elle ne peut pas se lever. L'ombre reste
   absente, et c'est elle qui garantit la lecture : sans ombre, aucun
   deplacement ne se lit comme un decollement.

   Les sauts sont francs (`steps`) et les positions tournent autour de
   l'immobile sans jamais y passer : interpolees, elles donneraient un
   balancement, or ce qu'on veut est un grelottement.

   C'est une SALVE, pas un tremblement continu : elle dure une demi-seconde
   et s'eteint, meme si le regard reste sur la note et meme si on a clique. Un
   objet qu'on touche tressaille et se remet en place ; il ne vibre pas tant
   qu'on le regarde. Une vibration qui dure devient un etat — donc un signal
   d'alerte — la ou celle-ci n'est qu'un accuse de reception. C'est le grain,
   lui, qui tient toute la duree : la salve dit le moment ou on choisit, le
   grain dit qu'on a choisi.

   L'amortissement est ecrit dans les images-cles, chacune une fraction de
   --tremble, de 1 a rien : sans lui la salve se coupe net au milieu d'un
   soubresaut, et une vibration qui s'arrete net s'entend comme une panne. La
   derniere image ramene exactement a zero — c'est elle qui rend la note a sa
   place, sans avoir a garder l'etat final de l'animation.

   L'amplitude est en PIXELS et non en % de la note, pour une fois : un
   tremblement est une vibration de la surface, pas une propriete de sa taille.
   Un demi-pixel sur une note de 150 comme sur une de 510 — au-dela, ca cesse
   d'etre une vibration et ca devient un objet qui bouge.

   La salve repart a chaque ouverture : la classe est retiree a la fermeture,
   donc l'animation est reposee au survol suivant. Rester sur la note ne la
   relance pas — il faut la quitter et y revenir, ce qui est exactement le
   geste qu'elle accuse.

   A REGLER A L'OEIL : --tremble (l'amplitude) et la duree de la salve.

   Ecrit apres la regle qui remet la transformation a zero : une animation
   l'emporte de toute facon sur une transformation declaree, mais l'ordre dit
   ce qui est la regle et ce qui est l'exception. */
.note--plat .note-inner { --tremble: .5px; }
.note--plat.is-open .note-inner {
  animation: plat-tremble .5s steps(1, end) 1;
}
@keyframes plat-tremble {
  0%   { transform: translate(calc(-1    * var(--tremble)), 0); }
  12%  { transform: translate(0, calc( .82 * var(--tremble))); }
  24%  { transform: translate(calc(  .66 * var(--tremble)), 0); }
  36%  { transform: translate(0, calc(-.52 * var(--tremble))); }
  48%  { transform: translate(calc( -.40 * var(--tremble)), 0); }
  60%  { transform: translate(0, calc( .29 * var(--tremble))); }
  72%  { transform: translate(calc(  .19 * var(--tremble)), 0); }
  84%  { transform: translate(0, calc(-.10 * var(--tremble))); }
  100% { transform: translate(0, 0); }
}
/* Meme raison que pour le grain : ce qui grelotte n'a rien a faire chez qui a
   demande moins de mouvement. Le voile de grain, lui, reste. */
@media (prefers-reduced-motion: reduce) {
  .note--plat.is-open .note-inner { animation: none; }
}

/* ---------- Le cadre blanc : le look photo imprimee ----------
   Le blanc est pose sur l'IMAGE elle-meme — un fond et un padding en % — et
   non sur la boite qui la porte. C'est ce qui le fait suivre gratuitement tout
   ce que le mur fait de l'image :
   - l'ombre portee est une copie de l'image passee au noir, donc la silhouette
     devient le rectangle encadre, sans une ligne de plus ;
   - les tranches du calque de courbure sont des copies de la meme image, donc
     le cadre s'enroule avec le papier au lieu de rester plat dessous.
   Le padding en % se resout sur la largeur du bloc contenant, la meme pour les
   trois copies : elles restent superposables au pixel pres.

   Les selecteurs sont ecrits un par un plutot qu'un `img` global : la note
   porte aussi les dessins des reactions et les attaches en image, qui n'ont
   rien a faire dans un cadre blanc.

   Le blanc n'est pas un aplat. Deux choses le sortent de l'a-plat de synthese,
   toutes les deux tres faibles — a la limite du visible, et c'est le but : on
   ne doit pas les REMARQUER, on doit trouver le papier moins mort.

   1. Un degrade vertical, plus sombre EN HAUT. Le tirage est tenu par le
      haut : c'est la que le papier reste plaque au mur, dans sa propre ombre,
      pendant que le bas s'ecarte et prend le jour. Le meme raisonnement que
      l'ombre portee, qui part de la ligne de colle et s'elargit vers le bord
      libre (.note-shadow) — ici en beaucoup plus discret, puisqu'il s'agit du
      liseré et non du mur derriere.
   2. Un grain, en SVG inline (feTurbulence) plutot qu'un fichier : quelques
      centaines d'octets dans la feuille de style, aucune requete, et il se
      repete tout seul. Sa tuile est en PIXELS et non en % — un grain est une
      propriete de la SURFACE du papier, pas de la taille a laquelle on le
      regarde : il doit rester le meme sur une note de 150 px et sur une de 510.

   --cadre-grain redit l'opacite ecrite dans le SVG, qui ne peut pas lire une
   variable CSS depuis une data-URI. C'est la seule valeur en double du
   fichier ; elle existe pour que la console lise le grain comme elle lit le
   reste (tools/console.py, mesures_mur) — les deux doivent bouger ensemble.

   Sur la copie d'ombre, tout ceci disparait sous `filter: brightness(0)` : le
   degrade et le grain n'ont pas a se voir dans une silhouette. */
.note--cadre .note-inner img,
.note--cadre .note-shadow img,
.note--cadre .note-attaches-boite,
.note--cadre .curl-seg > img {
  --cadre: 4.5%;
  --cadre-haut: #ece7de;
  --cadre-bas: #ffffff;
  --cadre-grain: .13;
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='96' height='96' filter='url(%23g)' opacity='.13'/%3E%3C/svg%3E"),
    linear-gradient(to bottom, var(--cadre-haut), var(--cadre-bas) 78%);
  background-size: 96px 96px, 100% 100%;
  padding: var(--cadre);
}

/* ---------- Decollement en courbure ----------
   Un post-it ne se souleve pas d'un bloc : sa bande de colle reste plaquee au
   mur et le reste s'enroule. Un seul element ne peut pas plier — il n'a qu'un
   plan. On superpose donc a la note un calque de tranches horizontales, chacune
   un peu plus inclinee que la precedente et posee au bout d'elle : la suite des
   plis fait la courbure. Geometrie et nombre de tranches : postit.js,
   constantes CURL_*.

   Le calque n'existe que le temps du survol. Au repos il redessinerait la note
   exactement par-dessus elle-meme, et sur un PNG detoure les bords
   antialiases, composites deux fois, durciraient.

   Toutes les hauteurs sont en % : chaque tranche fait height:100% du plan, donc
   la hauteur de la note, et un pourcentage signifie la meme chose pour toutes.
   C'est ce qui permet de placer l'image entiere dans chaque tranche et de la
   decouper en coordonnees d'image — et c'est aussi ce qui rend les
   pourcentages de translate3d lisibles comme des positions sur la note, leur
   reference etant la boite de l'element. L'aspect-ratio pose par le JS donne
   au plan cette hauteur.

   Le prix de cette mise a plat : chaque tranche fait la hauteur entiere de la
   note alors qu'elle n'en montre qu'une bande, et celle du bas est descendue
   de ~91%, donc son bord bas est a ~1.91 fois la hauteur de la note. Ce vide
   ne peint rien — les images sont decoupees au clip-path — mais il compte
   comme debordement, et un calque en absolu le fait remonter jusqu'au
   viewport : la page grandissait au survol, une barre de defilement
   apparaissait, la largeur perdue recomposait les colonnes. D'ou la boite de
   coupe ci-dessous.

   `overflow: hidden` et non `clip` : `clip` a l'avantage d'accepter une marge,
   mais mesure sur cette page, il ne retient pas le debordement de tranches
   transformees en 3D — Chromium continue d'en tenir compte dans la hauteur du
   document tant qu'un recalcul force ne le detrompe pas. `hidden`, lui, fait
   du calque un conteneur de defilement : le debordement lui appartient, par
   definition il ne remonte plus.

   Reste que `hidden` coupe pile sur sa boite, alors que le bord libre vient
   vers l'oeil et en sort (~13px sur les cotes, ~23px dessous pour une note de
   342px). La boite de coupe est donc elargie — mais lateralement seulement.
   Vers le bas elle s'arrete a la note : une boite de coupe plus haute que la
   note depasse sous elle et fait grandir la page a son tour, ce qui reviendrait
   a deplacer le probleme au lieu de le resoudre. La place pour le bord libre
   vient de la legende, qui appartient a la note et vit sous l'image.
   Lateralement, elle sort dans la gouttiere entre colonnes ou dans les 20px
   de marge du mur, jamais au-dela de la page. */
.note-curl {
  --bleed: 20px;
  position: absolute;
  top: 0;
  left: calc(-1 * var(--bleed));
  width: calc(100% + 2 * var(--bleed));
  height: 100%;
  padding: 0 var(--bleed);
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
}
/* Le plan de reference : la boite exacte de la note, dont tout le reste tire
   ses pourcentages. Separe de la boite de coupe, qui elle est plus grande. */
.curl-plane {
  position: relative;
  width: 100%;
  /* Ni perspective ni preserve-3d : la projection est calculee en JS et posee
     en 2D (postit.js, buildCurl). Toute transformation 3D ferait promouvoir les
     tranches en couches composees, rendues dans une texture dont la resolution,
     sous un ancetre tourne, n'est pas celle de l'ecran — net a 100%, flou des
     que l'affichage est mis a 125% ou 150%. Le point de fuite d'alors est
     devenu CURL_OEIL, et il est toujours sur la ligne de colle : la feuille
     s'ouvre vers le bas et vers l'oeil, elle ne derive pas lateralement. */
}
/* Les tranches sont soeurs, chacune posee a sa bande par `top` et portant son
   inclinaison absolue — rien ne s'additionne par la hierarchie, donc aucune
   imprecision de rendu ne se propage du haut de la feuille vers le bas.
   Position et angle sont calcules en JS (postit.js, buildCurl) ; ne restent
   ici que les deux etats.

   L'origine sur le bord haut de la tranche : c'est sa charniere. Et les deux
   transformations s'ecrivent avec les memes fonctions dans le meme ordre —
   sans ca le navigateur interpolerait des matrices au lieu des composantes, et
   la feuille se deplierait de travers.

   Au repos la transformation est l'identite, et c'est voulu : une couche posee
   entre deux pixels est reechantillonnee, donc molle, meme immobile. */
.curl-seg {
  position: absolute;
  left: 0; right: 0;
  height: 100%;
  transform-origin: 50% 0;
  transform: translateY(0) scale(1, 1);
  transition: transform .25s ease;
}
.note.is-lifted .curl-seg {
  transform: translateY(var(--ty)) scale(var(--sx), var(--sy));
}
/* > img : ne pas atteindre les images des tranches imbriquees, qui portent
   chacune leur propre decoupe. top et clip-path sont poses par le JS. */
.curl-seg > img {
  position: absolute;
  left: 0;
  width: 100%;
  /* 100% et non `auto` : la tranche fait deja la hauteur exacte de la note (le
     plan porte le rapport de la boite), donc les deux valent la meme chose —
     mais `auto` la fait deduire du rapport intrinseque du FICHIER, et sur une
     note encadree ce rapport n'est plus celui de la boite (le cadre est un
     padding pose sur l'image). Le calcul tombait alors juste une frame trop
     tard : le calque naissait aplati le temps d'un rendu, pile celui ou la
     bascule demarre. */
  height: 100%;
}
/* Le jour sur la feuille qui s'enroule. Un seul degrade, pose sur la note
   entiere : il part de rien sur la ligne de colle — la, le papier touche encore
   le mur et garde son eclairage — et monte jusqu'au bord libre, le plus proche
   de la source. Chaque tranche en montre sa portion, decoupee au meme
   clip-path que son image ; la rampe, elle, ne connait pas le decoupage, donc
   les raccords sont invisibles et le nombre de tranches ne se lit plus dans la
   lumiere. Un eclairci par tranche donnait autant d'aplats que de plis.
   --glow-rgb et --glow-a viennent du JS : blanc quand la source est en haut,
   noir quand elle vient d'en bas, amplitude tiree de --ly. */
.curl-light {
  position: absolute;
  left: 0;
  width: 100%;
  pointer-events: none;
  background: linear-gradient(to bottom,
      rgb(var(--glow-rgb) / 0) var(--glue),
      rgb(var(--glow-rgb) / var(--glow-a)) 100%);
  /* Masque par l'image : sur un PNG detoure, le jour s'arrete au bord du
     papier au lieu de peindre le rectangle entier. */
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  /* Le voile monte avec la bascule. Applique d'emblee, il ferait clignoter la
     note au montage du calque, alors qu'elle est encore a plat. */
  opacity: 0;
  transition: opacity .25s ease;
}
.note.is-lifted .curl-light { opacity: 1; }
/* Le calque bascule a la place de l'image : sans ca les deux rotations
   s'additionneraient. Plus specifique que .note.is-open .note-inner, et place
   apres. */
.note--curled.is-open .note-inner {
  transform: none;
}
/* ---------- Reactions ----------
   Cinq pastilles a cheval sur le bord bas de l'image — mais seulement note
   ouverte. Au repos, aucune ne se voit : ce qu'un post-it a recu, il le dit en
   le laissant s'echapper (l'emanation, plus bas), pas en gardant cinq epingles
   plantees dans son bord bas. Un post-it reste un post-it, pas un panneau de
   commandes. La note ouverte les revele toutes, avec leurs decomptes : c'est
   la qu'on lit et qu'on vote.

   La barre est dans le flux, entre l'image et la legende, mais de hauteur
   nulle : elle ne pousse donc rien, et `align-items: center` centre les
   pastilles sur cette ligne de hauteur zero — soit pile sur le bord bas de
   l'image. Les mettre en absolu aurait demande de connaitre la hauteur de la
   legende, que la police fait bouger jusqu'a son chargement.

   Elle a ete accusee a tort du saut de legende sur iPhone, et deux correctifs
   sont passes ici avant d'etre retires : un `contain: layout size`, puis un
   passage en absolu. Ni l'un ni l'autre ne tenait. La mesure prise sur
   l'appareil les a defaits tous les deux — la barre gardait un `offsetHeight`
   de 0 d'un bout a l'autre pendant que la legende descendait de 25 px, donc
   elle n'a jamais rien pousse ; et l'absolu, loin de remettre la legende en
   place, la figeait en position basse. La cause etait le multi-colonnes du mur
   (voir `.wall`), et couper la barre ne faisait que masquer un symptome.

   z-index 4 : au-dessus du calque de courbure (1), qui redessine l'image par
   dessus pendant le decollement. */
.note-actions {
  position: relative;
  z-index: 4;
  height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Seules les pastilles captent : la barre traverse toute la largeur de la
     note et intercepterait sinon le clic d'ouverture au doigt. */
  pointer-events: none;
}
/* La barre mord sur le haut de la legende : on lui rend la place. */
.note--react .note-sign { padding-top: 12px; }

.note-react {
  /* 5 x (32 + 2 x 2) = 180px, dans une colonne de 230px. */
  --d: 32px;   /* diametre dessine */
  --g: 2px;    /* demi-gouttiere */
  /* Zone de clic : un rectangle, pas le cercle. La gouttiere est avalee dans
     la largeur du bouton (--w) au lieu de vivre en marge, et la hauteur (--h)
     deborde franchement du dessin — cinq zones jointives qui couvrent toute la
     ligne des pastilles, sans interstice mort entre deux emojis ni coin de
     cercle qui laisse passer le doigt. La largeur totale ne bouge pas :
     5 x 36 = 180px, comme avant. */
  --w: calc(var(--d) + 2 * var(--g));
  --h: 40px;
  /* Rognage des deux bouts, cote exterieur (voir plus bas) : la mante rend
     plus que le coeur, qui reste la plus large des deux. --rogne s'arrete au
     bord de l'image dessinee. */
  --rogne: 0px;
  /* Decalage du decompte par rapport au coin du cercle dessine, negatif vers
     l'exterieur. Les deux axes sont separes : en largeur il n'a que la
     gouttiere pour deborder avant de mordre l'emoji voisin, en hauteur il n'a
     personne sous lui et peut descendre franchement. */
  --cnt-x: -1px;
  --cnt-y: -7px;
  --rogne-coeur: 3px;
  --rogne-mante: 5px;
  --h-coeur: 36px;
  --h-mante: 32px;
  pointer-events: auto;
  position: relative;
  flex: 0 0 auto;
  width: var(--w);
  height: var(--h);
  /* Pas de `gap` ni de marge : une pastille repliee garderait sa gouttiere et
     laisserait des trous, et une gouttiere en marge serait un trou dans la
     zone de clic. L'ecart vit dans la largeur, il disparait avec elle. */
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  /* Surtout pas d'arrondi ici : `border-radius` decoupe aussi la zone
     cliquable. Le cercle de la sienne est dessine par ::before. */
  border-radius: 0;
  background: transparent;
  color: var(--ink);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  /* Evite le delai de 300 ms du double-tap sur les navigateurs mobiles. */
  touch-action: manipulation;
  transition: opacity .2s ease, transform .2s ease, width .2s ease,
              margin .2s ease, background-color .15s ease;
}
/* Au repos, TOUTES sont repliees — largeur et marge a zero, la barre se
   resserre exactement comme si les pastilles n'existaient pas. Une reaction
   deja recue n'est plus une epingle plantee en bas du papier : elle se dit en
   s'echappant du post-it (voir l'emanation au repos, plus bas). La note
   ouverte, elle, les rend toutes cliquables — c'est la qu'on vote, et la
   qu'on lit les decomptes. */
.note-react {
  width: 0;
  opacity: 0;
  transform: scale(.5);
  overflow: hidden;
  pointer-events: none;
}
.note.is-open .note-react {
  width: var(--w);
  opacity: 1;
  transform: none;
  pointer-events: auto;
  /* Le compteur deborde du cercle (voir .react-count) : `overflow: hidden` sert
     au repliage, il rognerait le decompte une fois la pastille deployee. */
  overflow: visible;
}
/* Aux deux bouts, la zone n'a pas de voisine a rejoindre : elle deborde dans le
   vide, sur le bord de la note. Le coeur et la mante ramassaient donc des clics
   qui ne visaient rien. On leur reprend du terrain, cote exterieur seulement —
   la mante plus que le coeur, qui reste la plus large des deux.

   Le rognage s'arrete au bord de l'image (26px dans une zone de 36) : tout ce
   qui se voit reste cliquable, on ne rend que le halo. La place reprise est
   rendue en marge, donc la barre ne bouge pas d'un pixel ; le dessin, lui, est
   recale du demi-rognage pour rester exactement ou il etait. La hauteur se
   reprend sans rien recaler : la zone reste centree sur la meme ligne. */
.note-react:first-child { --rogne: var(--rogne-coeur); --h: var(--h-coeur); }
.note-react:last-child  { --rogne: var(--rogne-mante); --h: var(--h-mante); }

.note.is-open .note-react:first-child {
  width: calc(var(--w) - var(--rogne));
  margin-left: var(--rogne);
}
.note-react:first-child::before {
  margin-left: calc(var(--d) / -2 - var(--rogne) / 2);
}
.note-react:first-child .react-face { left: calc(var(--rogne) / -2); }
/* Bord droit inchange : le decompte, qui s'y accroche, ne bouge pas. */

.note.is-open .note-react:last-child {
  width: calc(var(--w) - var(--rogne));
  margin-right: var(--rogne);
}
.note-react:last-child::before {
  margin-left: calc(var(--d) / -2 + var(--rogne) / 2);
}
.note-react:last-child .react-face { left: calc(var(--rogne) / 2); }
/* Le bord droit s'est deplace vers l'interieur : le decompte le suit. */
.note-react:last-child .react-count {
  right: calc(var(--cnt-x) + (var(--w) - var(--d)) / 2 - var(--rogne));
}

/* La sienne, en jaune post-it : la seule qui reprend la pastille, et c'est
   justement ce qui la distingue — elle dit « c'est reste ». Meme lumiere que
   le reste du mur : --lx / --ly sont contre-tournes par le JS sur la note, et
   la pastille tourne avec elle, l'ombre retombe donc a la meme heure.

   Le cercle est dessine par ::before, centre dans la zone de clic : le bouton,
   lui, reste un rectangle sans arrondi pour ne rien rogner au doigt. */
.note-react::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--d);
  height: var(--d);
  margin: calc(var(--d) / -2) 0 0 calc(var(--d) / -2);
  border-radius: 50%;
  /* Tout au fond de la pastille : le decompte se glisse entre ce cercle et
     l'emoji (voir .react-count). */
  z-index: 0;
}
.note-react.is-mine::before {
  background: var(--sticky);
}

/* Aucune ombre dans la barre : ni sous l'emoji, ni sous les deux cercles.
   Une ombre y decoupait chaque element comme une gommette posee sur le
   papier, et surtout elle passait par-dessus le cercle jaune de la sienne —
   le decompte et la pastille redevenaient deux jetons empiles au lieu d'une
   seule tache. La barre vit a plat sur la note ; l'ombre reste au vol (voir
   .fx-emoji), la ou la reaction quitte vraiment le papier. */
.react-face {
  display: block;
  /* Positionne pour passer devant le cercle de ::before et le decompte, tous
     deux absolus. L'emoji reste toujours le dessus de la pile. */
  position: relative;
  z-index: 2;
  line-height: 1;
}
.react-face img {
  display: block;
  width: 26px;
  height: 26px;
  object-fit: contain;
}
/* Hors du flux : le compteur peut passer de 9 a 10 sans elargir la pastille,
   donc sans recomposer la barre sous le doigt.

   Il se cale sur le cercle dessine, pas sur le bouton : celui-ci est plus large
   et plus haut que le dessin (zone de clic), on rattrape donc la moitie de
   l'ecart pour que le decompte reste ou il etait. */
.react-count {
  position: absolute;
  /* Le decompte passe SOUS l'emoji (z-index 1 contre 2) : range trop loin
     dans le cercle, il disparaissait purement et simplement dessous. Il se
     pose donc bas, sous le ventre du dessin, ou l'emoji ne le couvre plus —
     franchement en hauteur, d'un cheveu seulement en largeur, ou la gouttiere
     est tout ce qui le separe de la pastille voisine. */
  right: calc(var(--cnt-x) + (var(--w) - var(--d)) / 2);
  bottom: calc(var(--cnt-y) + (var(--h) - var(--d)) / 2);
  /* Taille calee sur le cercle (12 sur 32) : assez pour deux chiffres en mono
     a 8px, assez petit pour se glisser sous le bord du dessin. */
  min-width: 12px;
  height: 12px;
  padding: 0 2px;
  border-radius: 6px;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 8px;
  line-height: 12px;
  text-align: center;
  /* Entre le cercle (0) et l'emoji (2) : la vignette du decompte se glisse
     SOUS la reaction, mais reste au-dessus du cercle jaune de la sienne. Le
     coin de l'emoji la mord donc legerement, et les deux pastilles se lisent
     comme un seul objet au lieu de deux jetons empiles. */
  z-index: 1;
}
.react-count:empty { display: none; }

/* La sienne : le decompte prend le jaune du cercle et l'encre pour chiffre.
   Les deux ronds ne font plus qu'une seule tache — c'est la fusion qui dit
   « c'est reste », pas une gommette noire posee dessus. Et rien ne les
   separe : pas d'ombre, elle retracerait la couture qu'on vient d'effacer. */
.note-react.is-mine .react-count {
  background: var(--sticky);
  color: var(--ink);
}

/* --------- Emanation ---------
   Voter ne remplit pas seulement un compteur : la reaction s'echappe du
   post-it. Les particules vivent dans un calque a part, et non dans la note —
   sinon elles heriteraient de son inclinaison, de son `overflow` et de son
   empilement, et seraient coupees au bord du papier.

   Ce calque est cale sur le document, pas sur la fenetre : une reaction part
   d'un post-it precis, elle doit rester avec lui quand le mur defile. Un
   calque fixe faisait voler les particules a cote de la note qui les avait
   emises. `position: absolute` a l'origine du document, coordonnees en
   document (le JS ajoute le defilement au getBoundingClientRect) : le
   navigateur les fait defiler lui-meme, sans ecouteur de scroll et donc sans
   retard d'une frame.

   Il couvre le document entier et coupe a ses bords : une particule lancee
   pres du bord droit derive au-dela du papier peint, et sans cette coupe le
   mur deviendrait defilable en largeur le temps de son vol. La coupe posee
   plus haut (body ou <html>) ne servait a rien : l'overflow du body remonte au
   viewport, et celui de la racine ne retient pas un absolu qui deborde.

   z-index 60 : au-dessus du mur et du bouton d'epinglage (40), sous la modale
   (100) — une reaction lancee juste avant l'ouverture du formulaire ne doit
   pas passer par dessus.

   Le calque ne capte rien : ses particules survolent le mur, elles avaleraient
   les clics. */
.fx-layer {
  position: absolute;
  inset: 0;
  z-index: 60;
  pointer-events: none;
  /* `clip` et non `hidden` : un calque qui ne defile pas et n'attrape pas le
     focus. */
  overflow: clip;
}

/* Chaque particule porte son propre tirage, en variables : derive laterale,
   hauteur, rotation, taille, retard. Une seule animation les sert toutes —
   c'est ce qui permet d'en lancer quinze sans y penser.

   `position: absolute` : les coordonnees sont celles du document, mesurees
   depuis l'origine du calque — c'est ce qui les fait defiler avec le mur. */
.fx-emoji {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--taille, 26px);
  height: var(--taille, 26px);
  /* La particule est posee par son centre : le JS donne le centre de la
     pastille, pas son coin. */
  margin: calc(var(--taille, 26px) / -2) 0 0 calc(var(--taille, 26px) / -2);
  display: grid;
  place-items: center;
  font-size: calc(var(--taille, 26px) * .9);
  line-height: 1;
  will-change: transform, opacity;
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, .3));
  animation: emane var(--duree, 1200ms) cubic-bezier(.22, .61, .36, 1)
             var(--retard, 0ms) both;
}
.fx-emoji img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* Trois temps : la particule jaillit de la pastille (petite, opaque d'un
   coup), monte en derivant, puis s'efface avant d'arriver. La derive est
   coupee en deux stops pour donner un arc plutot qu'une ligne droite — une
   fumee, pas une fusee. */
@keyframes emane {
  0% {
    transform: translate(var(--x), var(--y)) scale(.3) rotate(0deg);
    opacity: 0;
  }
  12% {
    transform: translate(calc(var(--x) + var(--dx) * .1),
                         calc(var(--y) - var(--dy) * .14)) scale(1.15)
               rotate(calc(var(--rot) * .15));
    opacity: 1;
  }
  55% {
    transform: translate(calc(var(--x) + var(--dx) * .78),
                         calc(var(--y) - var(--dy) * .62)) scale(1)
               rotate(calc(var(--rot) * .6));
    opacity: 1;
  }
  100% {
    transform: translate(calc(var(--x) + var(--dx)),
                         calc(var(--y) - var(--dy))) scale(.72)
               rotate(var(--rot));
    opacity: 0;
  }
}

/* L'emanation au repos, elle, ne jaillit pas : elle sourd. Pas de pop de
   depart, pas de plein contraste, une montee longue et une derive molle — un
   post-it beaucoup aime fume doucement, il ne clignote pas. C'est le seul
   signe, au repos, qu'il a recu quelque chose : les pastilles, elles, ne se
   montrent que note ouverte.

   --pic est le palier d'opacite tenu pendant le vol : plein (1) par defaut —
   un emoji a demi efface n'a pas l'air discret, il a l'air sale. Seules la
   sortie et la disparition sont progressives. Le reglage reste ouvert dans
   config.json pour qui voudrait un nuage en retrait. */
.fx-emoji--calme {
  animation-name: fume;
  animation-timing-function: cubic-bezier(.3, .5, .4, 1);
}
@keyframes fume {
  0% {
    transform: translate(var(--x), var(--y)) scale(.55) rotate(0deg);
    opacity: 0;
  }
  14% {
    transform: translate(calc(var(--x) + var(--dx) * .12),
                         calc(var(--y) - var(--dy) * .14)) scale(1)
               rotate(calc(var(--rot) * .14));
    opacity: var(--pic, 1);
  }
  78% {
    transform: translate(calc(var(--x) + var(--dx) * .8),
                         calc(var(--y) - var(--dy) * .8)) scale(1)
               rotate(calc(var(--rot) * .78));
    opacity: var(--pic, 1);
    /* La courbe de l'animation vaut par intervalle : sur le dernier, elle
       souffle l'emoji d'un coup. Ce segment-la se fait en lineaire, l'emoji
       s'eteint au lieu de disparaitre. */
    animation-timing-function: linear;
  }
  100% {
    transform: translate(calc(var(--x) + var(--dx)),
                         calc(var(--y) - var(--dy))) scale(.85)
               rotate(var(--rot));
    opacity: 0;
  }
}

/* L'estampe : l'emoji pose en grand par-dessus le post-it, le temps qu'on
   lise ce qu'on vient de laisser. Meme calque que les particules, donc memes
   coordonnees de document — elle reste sur sa note quand le mur defile — et
   comme elles, elle ne capte aucun clic : on peut recliquer pendant qu'elle
   s'efface.

   La taille vient du JS, prise sur le post-it lui-meme. */
.fx-stamp {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--taille, 160px);
  height: var(--taille, 160px);
  margin: calc(var(--taille, 160px) / -2) 0 0 calc(var(--taille, 160px) / -2);
  display: grid;
  place-items: center;
  font-size: calc(var(--taille, 160px) * .82);
  line-height: 1;
  will-change: transform, opacity;
  /* Plus appuyee que celle des particules : a cette taille, une ombre legere
     laisse l'emoji flotter sans poids au-dessus de la photo. */
  filter: drop-shadow(0 8px 18px rgba(0, 0, 0, .38));
  animation: estampe var(--duree, 900ms) cubic-bezier(.2, .8, .3, 1) both;
}
.fx-stamp img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* Elle tombe : elle arrive un peu trop grande, se pose en depassant legerement,
   tient le temps qu'on la lise, puis s'ouvre et s'efface — c'est ce dernier
   temps que les particules viennent recouvrir. */
@keyframes estampe {
  0% {
    transform: translate(var(--x), var(--y)) scale(.35) rotate(calc(var(--tilt, 0deg) * -2));
    opacity: 0;
  }
  22% {
    transform: translate(var(--x), var(--y)) scale(1.08) rotate(var(--tilt, 0deg));
    opacity: 1;
  }
  34% {
    transform: translate(var(--x), var(--y)) scale(1) rotate(var(--tilt, 0deg));
    opacity: 1;
  }
  58% {
    transform: translate(var(--x), var(--y)) scale(1.02) rotate(var(--tilt, 0deg));
    opacity: 1;
    /* Le fondu se fait en lineaire : l'estampe s'eteint au lieu de claquer. */
    animation-timing-function: linear;
  }
  100% {
    transform: translate(var(--x), var(--y)) scale(1.3) rotate(var(--tilt, 0deg));
    opacity: 0;
  }
}

/* La pastille elle-meme accuse le coup : un petit rebond au depart, qui donne
   son origine au nuage. */
.note-react.is-bump .react-face { animation: react-bump 420ms ease-out; }
@keyframes react-bump {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.45); }
  60%  { transform: scale(.9); }
  100% { transform: scale(1); }
}

/* Plus specifique que la regle de repliement ci-dessus, et place apres :
   sans ca `transform: none` gagnerait sur une pastille revelee. */
@media (hover: hover) {
  .note.is-open .note-react:hover { transform: translateY(-2px) scale(1.12); }
}
.note.is-open .note-react:active { transform: scale(.92); }

.note-sign {
  font-family: var(--font-mono);
  font-size: var(--pied);
  line-height: 1.3;
  color: var(--wall-text);
  /* Cale a gauche et remontee sous le papier : le texte est ecrit sur le mur,
     au pied du post-it, pas centre sous une etiquette. Il ne bouge plus au
     decollement (voir .note.is-open), donc l'ombre qui s'etale lui passe
     dessus — elle est positionnee, il ne l'est pas, elle peint apres lui.
     La marge negative la fait mordre sur le bord bas du papier : la legende
     touche le post-it au lieu de flotter dessous. Elle passe devant l'image
     sans risque — .note-inner coupe a sa boite, la legende est en dehors.
     Et pas calee pile sur le bord gauche du papier : un alignement parfait
     ferait etiquette. Une lettre de decalage (1ch, la police est a chasse
     fixe) suffit a rendre la legende posee a la main.

     Elle passe DEVANT l'ombre portee, et c'est un correctif. Elle etait posee
     derriere, ce qui se defendait — l'ombre du papier tombe bien sur le mur
     ou la legende est ecrite. Mais cette ombre est une image floue dont le
     rayon et l'opacite changent pendant les .25s du decollement, et son encre
     deborde de 64px (overflow-clip-margin), donc pile sur ces 12px de mono :
     le texte se delavait puis se reprenait a chaque ouverture. Un realisme qui
     coute la lisibilite du seul texte de la note n'en vaut pas le prix.
     Positionnee, elle peint apres l'ombre au lieu d'avant. Elle passe du meme
     coup devant le bord libre du calque de courbure, qui sort sous l'image et
     serait venu la couvrir.

     Passer devant a un revers, et c'est ce qui a fait clignoter la legende
     ensuite : ce qui peint apres une couche composee et la chevauche est promu
     avec elle. Le remede est chez l'ombre — elle n'anime plus de propriete
     composable (voir .note-shadow) — et non ici : la remettre derriere
     ramenerait le delavage. */
  position: relative;
  z-index: 2;
  margin-top: -4px;
  padding: 2px 4px 0 calc(2px + 1ch);
  text-align: left;
  word-break: break-word;
  -webkit-text-stroke: var(--lisere-moyen) #000;
  paint-order: stroke fill;
  --brun: var(--brun-moyen);
}

/* apparition */
.note { opacity: 0; }
.note.in { opacity: 1; transition: opacity .5s ease, transform .25s ease; }

/* ---------- Le post-it d'ajout ----------
   La premiere note du mur, et la seule qui ne soit pas une photo : un papier
   vierge, du jaune post-it, qui attend qu'on ecrive dessus. Elle emprunte tout
   le reste aux vraies notes — l'inclinaison, le pivot sur la ligne de colle, le
   decollement au survol, la legende ecrite sur le mur a son pied. Ce qu'elle
   n'a pas, elle ne l'a pas du tout : ni calque de courbure (rien a trancher
   sans image), ni reactions, ni ombre portee en calque.

   L'ombre est donc un `box-shadow`, la seule du mur a l'etre. C'est defendable
   ici et nulle part ailleurs : l'ombre en calque existe parce qu'un post-it
   colle en haut projette de plus en plus large vers son bord libre, et parce
   qu'elle doit epouser le contour d'un PNG detoure. Ce papier-ci est un
   rectangle plein, et il ne se decolle que de la bascule — un box-shadow dit
   tout ce qu'il y a a dire, sans un second calque ni un second fichier. */
.note--ajout .ajout-papier {
  /* Aucune image ne vient donner sa hauteur a la boite : sans ce rapport, le
     papier serait plat et fitNote n'aurait rien a mesurer. La valeur vient du
     JS (AJOUT_RATIO), qui s'en sert aussi pour calculer la largeur — les deux
     doivent parler du meme papier. */
  aspect-ratio: 1 / var(--ajout-ratio, 1.06);
  background: var(--sticky);
  box-shadow: var(--shadow-note);
  transition: transform .25s ease, box-shadow .25s ease;
}
.note--ajout.is-open .ajout-papier { box-shadow: var(--shadow-note-hover); }

/* ---------- Quand le logo tient la place du papier ----------
   `ajout.relaisLogo` declare, le logo ne se POSE pas sur le post-it jaune : il
   EST le post-it (postit.js, buildAjout). Le papier n'a donc plus ni couleur
   ni ombre de boite — l'aplat jaune reparaitrait sous le dessin, et le
   box-shadow dessinerait un rectangle autour d'une forme qui n'en est pas un.

   AUCUNE LIGNE DE FUSION ICI, et pourtant le logo est bien en produit : le
   melange est cuit dans le FICHIER (tools/logo_produit.py). `mix-blend-mode`
   n'aurait rien donne — `.note` porte une rotation, une rotation ouvre un
   contexte d'empilement, et le produit n'y voit que le fond transparent de la
   note, jamais le mur qui est dehors. Retirer les rotations aurait coute le
   mur entier. Un pixel noir dont l'opacite vaut l'inverse de la luminance rend
   exactement ce que le produit rendrait, sur n'importe quel fond et sans mode
   de fusion : c'est ce que l'outil ecrit dans le PNG. Le monogramme passe donc
   au travers du papier, et le post-it se lit comme un contour vide qui attend
   qu'on ecrive dedans. */
.note--ajout-logo .ajout-papier,
.note--ajout-logo.is-open .ajout-papier { background: none; box-shadow: none; }
/* Et le meme mouvement que la pastille du bas, plutot que celui des notes.
   Une vraie note se DECOLLE : sa bande de colle reste plaquee, le bas s'enroule
   vers l'oeil (perspective + rotateX). Ce mouvement raconte l'epaisseur d'une
   feuille, et le logo n'en a pas — c'est un trace, le mur se voit au travers.
   Il prend donc le geste de la pastille, qui est le meme objet : le papier se
   redresse et se souleve un peu, rien de plus. Meme duree qu'elle (.18s la ou
   les notes sont a .25s) : deux fois le meme dessin, deux fois le meme temps.

   Le pivot est le CENTRE du dessin, comme sur la pastille, et non la ligne de
   colle dont `.note` herite (50% 0). Une note collee tourne par le haut parce
   que c'est par la qu'elle tient ; le logo ne tient a rien, il est pose au
   milieu de sa boite, et un redressement par le haut lui faisait balayer sept
   pixels du bas — un essuie-glace, la ou la pastille se contente d'un quart de
   tour sur elle-meme.
   Deplacer ce pivot ne se fait pas qu'ici : fitNote mesure les marges de la
   note DEPUIS lui (postit.js, pivotNote), et le CSS seul aurait fait mentir
   les mesures — la note aurait mordu sur ses voisines. Le JS rend donc le
   meme centre. Il tombe pile sur le centre de l'image, la note d'ajout etant
   la seule sans legende sous son papier. */
.note--ajout-logo { transform-origin: 50% 50%; transition: transform .18s ease; }
/* `.note.in` repose la transition a l'apparition (opacity .5s, transform .25s)
   et vaut deux classes : sans cette seconde regle, aussi specifique et posee
   plus bas, la note d'ajout retombait a .25s des qu'elle etait revelee. */
.note--ajout-logo.in { transition: opacity .5s ease, transform .18s ease; }
.note--ajout-logo.is-open {
  transform: rotate(0deg) scale(var(--fit)) translateY(-2px);
}
.note--ajout-logo:active {
  transform: rotate(0deg) scale(var(--fit));
}
/* Pas de bascule 3D sur le papier : elle s'ajouterait au redressement. */
.note--ajout-logo.is-open .note-inner { transform: none; }

/* Et pas d'epingle. Meme raison que sur une photo detouree (.note--cut) : elle
   flotterait dans le vide au-dessus d'un contour qui n'est pas un rectangle —
   ici par-dessus le trait du dessin, ou pire, dans le papier devenu
   transparent. Le logo est un dessin pose sur le mur, pas une feuille punaisee
   dessus. */
.note--ajout-logo .note-inner::before { display: none; }

.ajout-logo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* AUCUNE OMBRE. Les autres notes en ont une parce qu'elles sont des objets
     poses sur le mur, avec une epaisseur et un bord qui se souleve. Le logo
     n'en est pas un : c'est un trace, et le mur se voit au travers. Une ombre
     portee suivrait le seul trait — un liftage de contour, pas une feuille qui
     decolle — et un box-shadow cernerait le coin replie dans le vide. Ce qui
     signale qu'on peut la toucher, c'est la bascule de `.note-inner`, qui
     suffit. */
}

/* Le bouton couvre le papier entier : toute la note ouvre le formulaire, et le
   signe n'est pas une cible plus petite que ce qu'on voit. */
.ajout-bouton {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  color: var(--ink);
  font-family: var(--font-mono);
  font-weight: 700;
  /* Le signe se mesure sur le papier, comme la legende (--pied) : a trois
     colonnes sur un telephone, une taille fixe ferait un plus gros que la
     note. --note-w est posee par le JS (fitNote). */
  font-size: calc(var(--note-w, 230px) * .34);
  /* `.note-inner` est a `line-height: 0` — il n'y contient d'ordinaire que des
     images. Le signe, lui, est du texte et a besoin de sa ligne. */
  line-height: 1;
  cursor: pointer;
  /* Le papier tourne deja avec la note : le contour de focus doit suivre le
     papier, pas la boite droite. */
  border-radius: 3px;
}
.ajout-bouton:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -4px;
}

/* ---------- Post-it en attente de validation ----------
   Visible de son seul auteur, le temps de la moderation (postit.js, PENDING_KEY).
   La note est deja la, mais elle n'est pas encore tout a fait du mur : un peu
   de couleur en moins, un peu de jour au travers. Rien de plus — pas de cadre,
   pas de hachures : le mur n'a pas de boites, et le mot au pied du papier dit
   deja ce qu'il y a a dire.
   L'opacite est posee sur la note entiere, et apres .note.in qui la remet a 1 :
   ainsi le papier, son ombre portee et le calque de courbure s'estompent
   ensemble, dans tous les etats. Les regler separement les ferait diverger au
   survol, quand le calque prend le relais de l'image. */
.note--pending { filter: grayscale(.25); }
.note--pending.in { opacity: .85; }

/* Meme calage que .note-sign : ecrit sur le mur au pied du post-it, mordant sur
   son bord bas, decale d'une lettre pour ne pas faire etiquette. La signature
   suit dessous et perd donc sa propre remontee. */
.note-pending-tag {
  /* Devant l'ombre portee, meme raison que .note-sign. */
  position: relative;
  z-index: 2;
  margin: -4px 0 0;
  padding: 2px 4px 0 calc(2px + 1ch);
  font-family: var(--font-mono);
  /* Un cran sous la signature, comme avant — mais en part d'elle, pour que le
     rapport tienne a toutes les largeurs. */
  font-size: calc(var(--pied) * .92);
  line-height: 1.3;
  letter-spacing: .04em;
  color: var(--wall-text);
  opacity: .72;
  text-align: left;
  word-break: break-word;
}
.note--pending .note-sign { margin-top: 0; }

/* Le commentaire laisse par l'auteur, ecrit sur le mur SOUS la signature.
   C'est la signature qui mord sur le bord bas du papier et qui tient la place
   fixe (postit.js) : elle ne doit pas monter d'une ligne parce qu'un mot a ete
   laisse, sinon le post-it qui se decolle vient mordre dessus. Le mot suit
   donc, cale sur la meme lettre de decalage, serre contre la signature — la
   voix qui prolonge le nom, pas un second bloc.
   Devant l'ombre portee pour la meme raison que .note-sign. */
.note-mot {
  position: relative;
  z-index: 2;
  margin: 1px 0 0;
  padding: 0 4px 0 calc(2px + 1ch);
  /* A empattement, et plus petit que la signature : deux textes au pied du
     papier, une seule police les ferait lire comme une meme ligne. Le serif
     du systeme plutot qu'une fonte de plus a charger — le mur n'attend pas
     une requete supplementaire pour une phrase. */
  font-family: Georgia, "Times New Roman", serif;
  /* En italique : c'est une voix, pas une etiquette. */
  font-style: italic;
  font-size: calc(var(--pied) * .92);
  line-height: 1.35;
  /* Blanc pur et plein, comme la signature. Il a porte un opacity: .88 le
     temps d'un commit : l'opacite ne voile pas que la lettre, elle voile
     aussi le contour brun qui la detache du monogramme, et a cette taille
     c'est lui qui portait toute la lisibilite. Le mot s'efface derriere le
     nom par sa taille et son italique, pas en perdant du blanc. */
  color: var(--wall-text);
  text-align: left;
  word-break: break-word;
  white-space: pre-wrap;
  /* Trait plus fin qu'ailleurs : sur du serif de 11px, un contour de 2px
     engraisse les delies jusqu'a les fermer. 1px suffit a detacher le mot
     du motif. */
  -webkit-text-stroke: var(--lisere-fin) #000;
  paint-order: stroke fill;
  --brun: var(--brun-fin);
}

/* Le retrait ne s'offre pas : il parait au clic sur la note, et pas au survol
   (postit.js, armRemove). Au repos, rien — le mur n'est pas une console.

   Le calque couvre exactement la photo : sa hauteur vient du ratio de l'image,
   pose par le JS (repli carre ici, le temps du chargement). Au-dessus du calque
   de courbure, qui redessine l'image note ouverte : le voile porte donc sur
   l'image *et* sur ses copies, sans avoir a les atteindre une par une.
   Et sous le mot, pas de plaque : c'est le papier entier qui s'assombrit, le
   texte reste pose dessus. */
.note-remove-zone {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  aspect-ratio: 1;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0);
  pointer-events: none;
  transition: background .2s ease;
}
.note--pending.is-armed .note-remove-zone { background: rgba(0, 0, 0, .45); }

/* Le papier se remet a plat le temps de la decision. Le voile est un rectangle
   et le restera : il ne saurait pas epouser une feuille enroulee, dont la forme
   est refaite tranche par tranche a chaque ouverture — l'ombre debordait donc
   du papier souleve, et ca se voyait.
   Plutot que de courir apres la geometrie du decollement, on l'annule : on
   demande a retirer ce post-it, il se pose. Le calque de courbure s'efface en
   opacite et non en `display`, sinon sa transition ne finirait jamais et
   personne ne viendrait le demonter (voir armNote, demonter). */
.note--pending.is-armed .note-curl   { opacity: 0; }
.note--pending.is-armed .note-inner  { transform: none; }
.note--pending.is-armed .note-shadow { --f: .078; --sh-op: .52; }
.note-curl { transition: opacity .2s ease; }

.note-remove {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .06em;
  color: #fff;
  background: none;
  border: none;
  padding: 8px 12px;
  margin: 0;
  cursor: pointer;
  text-align: center;
  text-decoration: underline;
  text-underline-offset: 3px;
  /* Le contour porte le mot sur une photo claire, ou le voile seul ne
     suffirait pas a le detacher. Fin : le mot est petit et deja souligne. */
  -webkit-text-stroke: var(--lisere-fin) #000;
  paint-order: stroke fill;
  --brun: var(--brun-fin);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease;
}
/* Au clavier, le mot se montre des qu'il prend le focus : le masquer par
   `display` sortirait la seule sortie de secours de l'ordre de tabulation. */
.note--pending.is-armed .note-remove,
.note-remove:focus-visible {
  opacity: 1;
  pointer-events: auto;
}

/* La note en attente se clique — c'est ce clic qui offre le retrait. Les
   autres ne repondent au clic que faute de survol (voir plus bas). */
.note--pending { cursor: pointer; }

/* ---------- Etats vide / chargement ---------- */
.wall-empty, .wall-loading {
  text-align: center;
  font-family: var(--font-mono);
  font-size: 14px;
  color: var(--wall-text);
  padding: 20px 24px 60px;
  -webkit-text-stroke: var(--lisere-moyen) #000;
  paint-order: stroke fill;
  --brun: var(--brun-moyen);
}
/* ---------- Les post-it arrivent dans l'ordre ----------
   postit.js, revelerDansLOrdre. Une note dont le tour n'est pas venu
   (`data-attend`) tient sa place sans rien montrer : sa case est carree (le
   papier publie l'est, TAILLE_PUBLIEE) — sans elle, une image en route ne
   donnait a la note aucune hauteur, et le mur sautait a son arrivee. La
   tete de la file, si elle tarde, montre son anneau (`data-charge`) et rien
   d'autre : la note apparait entiere, d'un coup. */
.note[data-attend] .note-inner { aspect-ratio: 1; }
.note[data-attend]:not([data-charge]) { visibility: hidden; }
.note[data-charge] .note-inner > *,
.note[data-charge] > :not(.note-inner) { visibility: hidden; }
.note[data-charge] .note-inner::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 22%;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 3px solid rgba(0, 0, 0, .15);
  border-top-color: #000;
  animation: tourne-charge .8s linear infinite;
}
@keyframes tourne-charge { to { transform: rotate(1turn); } }
@media (prefers-reduced-motion: reduce) {
  .note[data-charge] .note-inner::after { animation: none; }
}

/* ---------- Indice « clique sur un post-it » ----------
   Le mur ressemble a une galerie : rien n'y annonce que les notes se decollent
   au doigt, et une partie des visiteurs repart sans en avoir ouvert une seule.
   La phrase se pose donc au milieu de l'ecran au bout de quelques secondes, et
   elle bat — du transparent complet a 70 %, lentement. Le battement n'est pas un
   ornement : c'est lui qui la donne comme un calque pose sur le mur et non comme
   un objet epingle dessus, la ou une opacite fixe passerait pour une legende du
   mur. Il fait aussi office d'arrivee : partant de zero, sa premiere montee EST
   le fondu d'entree, et il n'y a rien a raccorder.

   `pointer-events: none` n'est pas un detail : la phrase couvre le centre de la
   fenetre, donc les post-it les plus en vue. Sans elle, l'indice bloquerait
   exactement le geste qu'il reclame.

   z-index 50 : au-dessus du mur et du bouton d'epinglage (40), sous les
   reactions en vol (60) et la modale (100) — une phrase d'accueil ne se met pas
   en travers de ce qui se passe. Voir armerIndice() dans postit.js. */
.hint {
  position: fixed;
  /* Tendue d'un bord a l'autre plutot que posee a `left: 50%` : un element fixe
     sans bord droit ne dispose, pour se dimensionner, que de ce qui reste a la
     droite de son bord gauche — soit la moitie de l'ecran, et la phrase se
     brisait en trois lignes sur telephone quelle que soit la largeur qu'on lui
     accordait. Etendue, elle recoit toute la fenetre ; c'est `margin-inline`
     qui la recentre, et le max-width plus bas qui la retient. */
  /* Le milieu de la VUE, pas celui du cadre. Un element fixe se cale sur le
     viewport de mise en page, dont la hauteur change quand la barre d'adresse
     du telephone se replie au defilement : la phrase sautait d'un demi-repli a
     chaque fois. --vv-mid est le centre du rectangle reellement visible, tenu a
     jour par suivreLaVue() dans postit.js ; le repli 50svh sert aux navigateurs
     sans visualViewport, et le -50% recentre la phrase sur elle-meme. */
  /* Le pincement decale et retrecit la vue sans rien changer au cadre : la
     phrase, calee sur le cadre, sortait de l'ecran des qu'on zoomait dans un
     coin du mur — elle disparaissait sans s'etre effacee. --vv-dx la ramene sur
     l'axe horizontal comme --vv-mid le fait sur l'axe vertical, et --vv-zoom
     annule l'agrandissement pour qu'elle garde sa taille a l'ecran au lieu de
     deborder de la vue reduite. Les trois viennent de suivreLaVue(). */
  inset: 0 0 auto;
  transform:
    translate(var(--vv-dx, 0px), calc(var(--vv-mid, 50svh) - 50%))
    scale(var(--vv-zoom, 1));
  z-index: 50;
  pointer-events: none;

  margin: 0 auto;
  padding: 0 24px;
  /* La phrase tient sur une ligne, telephone compris — c'est ce qui en fait une
     indication et pas un paragraphe. La largeur la laisse donc entiere, et ne
     sert que de garde-fou a un texte plus long ecrit dans config.json : il
     reviendra a la ligne plutot que de courir d'un bord a l'autre de l'ecran.
     Large pour une vingtaine de signes, et pas vingt et un `ch` : l'unite mesure
     la chasse de la police, pas l'interlettrage genereux pose plus bas, qui
     ajoute ici un bon sixieme a chaque caractere. */
  max-width: 30ch;
  text-align: center;
  text-wrap: balance;

  /* Space Mono, comme le titre et le bouton d'epinglage : c'est la voix du mur
     qui parle, pas une bulle d'aide venue d'ailleurs. */
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: clamp(18px, 3.4vw, 30px);
  letter-spacing: .1em;
  line-height: 1.25;
  color: var(--wall-text);
  -webkit-text-stroke: var(--lisere-fort) #000;
  paint-order: stroke fill;
  --brun: var(--brun-fort);

  /* Un seul temps. Le battement part du transparent complet : la phrase naît
     donc de rien a la seconde ou le compte a rebours tombe, et sa montee tient
     lieu de fondu d'entree — pas de seconde animation, pas de raccord a regler
     entre les deux. Elle s'efface pareil au creux de chaque cycle : c'est ce qui
     la garde legere sur le mur. */
  animation: hint-beat 2.6s ease-in-out infinite alternate;
}
@keyframes hint-beat { from { opacity: 0; } to { opacity: .7; } }

/* La sortie part de l'opacite du moment, posee en --o par le JS : le battement
   peut etre n'importe ou dans sa course, et une sortie qui partirait de 70 %
   alors que la phrase en est a un souffle du transparent se verrait sauter.
   --hint-fade vient de postit.js, qui compte la meme duree avant de reposer
   l'attribut `hidden`. */
.hint.is-out {
  animation: hint-out var(--hint-fade, .4s) ease forwards;
}
@keyframes hint-out { from { opacity: var(--o, .5); } to { opacity: 0; } }

.zoom-moins { grid-column: 1; grid-row: 1; }
.zoom-plus  { grid-column: 2; grid-row: 1; }


/* ---------- Le cadre des deux commandes du bas ----------
   Il ne dessine rien. Il se pose sur le rectangle REELLEMENT visible, de sorte
   que ses deux enfants — le relais d'ajout et les deux signes de zoom —
   tiennent les coins de ce qu'on voit, et non ceux du viewport de mise en page.
   Sans lui, un pincement les emportait hors de l'ecran : un element fixe se
   cale sur le cadre, que le pincement ne bouge pas, alors que la vue, elle,
   s'y deplace et s'y retrecit (postit.js, suivreLesCoins).

   Il porte aussi la contre-echelle, et c'est pour ca qu'il existe comme boite
   plutot que comme deux corrections separees : sa taille est celle de la vue
   REMISE A L'ECHELLE DU CADRE, et `scale(--vue-s)` la redivise du meme facteur.
   Le compte est nul pour le cadre, qui retombe sur le rectangle visible, et pas
   pour ses enfants — ils vivent dans un repere ou 16px valent 16px d'ecran,
   pincement ou non. Le relais garde ainsi son inclinaison de papier et ses
   transformations de survol, intactes.

   `transform-origin: 0 0` n'est pas cosmetique : l'echelle doit pivoter sur le
   coin haut-gauche du cadre, celui que la translation vient de poser sur la
   vue. Au centre, la boite se serait ouverte de part et d'autre et les quatre
   bords auraient rate la vue.

   Les cinq replis rendent exactement le viewport de mise en page : sans
   `visualViewport`, ou avant la premiere mesure, le cadre est le cadre et les
   deux commandes se comportent comme avant.

   `pointer-events: none` : le cadre couvre tout l'ecran. Sans ca, il prendrait
   chaque doigt pose sur le mur — les deux commandes le reprennent pour elles
   seules. */
.vue {
  position: fixed;
  top: 0;
  left: 0;
  width: var(--vue-w, 100%);
  height: var(--vue-h, 100%);
  transform-origin: 0 0;
  transform: translate(var(--vue-x, 0px), var(--vue-y, 0px))
             scale(var(--vue-s, 1));
  /* Meme etage que les commandes qu'il porte : au-dessus du mur, sous la
     modale (100) et sous les reactions en vol (60). */
  z-index: 40;
  pointer-events: none;
  /* Le retour, une fois la vue reposee : un fondu franc mais pose. */
  transition: opacity 220ms ease;
}
.vue > * { pointer-events: auto; }

/* Efface le temps du geste. Un element fixe ne peut pas etre colle a la vue
   visuelle — le pincement se fait dans le compositeur et `visualViewport` ne
   previent qu'apres coup, si bien que les deux commandes couraient derriere le
   mur par a-coups (postit.js, suivreLesCoins). Elles s'en vont donc pendant le
   geste et reviennent en place quand il est fini : le retard reste, mais il se
   passe hors de vue. Le depart est plus rapide que le retour — ce qui doit
   disparaitre n'a pas a s'attarder, ce qui revient a droit a son fondu.
   `pointer-events` tombe avec l'opacite : une commande qu'on ne voit pas ne
   doit pas prendre le doigt qui pince. */
.vue.is-effacee {
  opacity: 0;
  transition: opacity 90ms linear;
}
.vue.is-effacee > * { pointer-events: none; }

/* ---------- Le relais du post-it d'ajout ----------
   Ce qui invite a poster est une note du mur (postit.css, .note--ajout ;
   postit.js, buildAjout). Elle defile avec lui : cette pastille reste, du
   premier au dernier ecran. Elle n'attend plus d'avoir depasse la note pour
   paraitre — les deux portent le meme dessin, ce n'est donc pas une invitation
   en double mais la meme, une fois sur le mur et une fois sous le pouce.
   Une pastille et non l'ancien bandeau : le post-it porte deja l'invitation,
   ce qui reste ici n'est qu'un rappel, et il n'a pas a occuper le bas de
   l'ecran comme s'il etait la seule porte. */
.cta-relais {
  /* `absolute` et non `fixed` : c'est le cadre .vue qui est fixe, et lui seul
     sait ou est la vue. Les valeurs ci-dessous sont inchangees — elles se
     comptent maintenant depuis les bords de ce qu'on voit. */
  position: absolute;
  right: max(16px, env(safe-area-inset-right));
  /* Au-dessus de la barre gestuelle : la page etant en `viewport-fit=cover`,
     22px du bas, c'est 22px sous le doigt du systeme. */
  bottom: calc(22px + env(safe-area-inset-bottom));

  width: 52px;
  height: 52px;
  display: grid;
  place-items: center;
  padding: 0;

  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 26px;
  line-height: 1;
  color: var(--ink);
  background: var(--sticky);
  border: none;
  /* Un post-it, pas un bouton rond : le coin a peine casse, et la meme
     inclinaison de papier que les notes du mur. */
  border-radius: 4px;
  transform: rotate(-3deg);
  cursor: pointer;
  box-shadow: var(--shadow-note);
  transition: transform .18s ease, box-shadow .18s ease;
}
.cta-relais:hover {
  transform: rotate(0deg) translateY(-2px);
  box-shadow: var(--shadow-note-hover);
}
.cta-relais:active { transform: rotate(0deg); }
/* Le logo, quand config.json en declare un (ajout.relaisLogo). Il ne se pose
   pas SUR la pastille jaune : il la remplace — c'est lui le post-it, comme sur
   la note du mur. Le papier perd donc sa couleur et son ombre de boite, et le
   logo prend toute la place ; le signe, lui, garde le jaune (JS, cta-relais--logo).

   Ce n'est PAS le meme fichier que sur le mur, et c'est tout le point. La note
   du mur est un trace en produit, que le monogramme traverse ; la pastille, qui
   flotte sur ce qui defile, ne peut pas se laisser traverser sans devenir
   illisible d'un post-it a l'autre. Son fichier est donc PLEIN — papier colore,
   trait noir — et se pose en fusion classique, sans rien a melanger. Elle
   n'aurait de toute facon pas eu le choix : `position: fixed` avec un
   `z-index`, c'est un contexte d'empilement a elle seule, ou un
   `mix-blend-mode` n'aurait meme pas vu le mur qui defile dessous.
   Pas d'ombre, pour la meme raison que sur le mur (.ajout-logo) : un trace
   n'est pas un objet pose. Le mouvement au survol reste, lui — c'est le
   seul retour dont la pastille a besoin. */
.cta-relais--logo {
  background: none;
  box-shadow: none;
}
.cta-relais--logo:hover { box-shadow: none; }

.cta-relais-logo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* ---------- Les deux signes de zoom ----------
   La piste ouverte a la place du tap qui zoome (DOCUMENTATION, « Ce qui a ete
   ecarte ») : pour qui ne veut pas pincer, deux signes qui echelonnent le mur
   lui-meme au lieu de la page. Rien n'est mis a l'echelle par une
   transformation — c'est la colonne qui s'elargit et le motif qui suit — donc
   le texte reste net et aucun calque n'est promu en couche composee.

   Chacun dans un cercle brun — le brun du papier de la pastille d'ajout, pris
   dans son fichier. Ni post-it jaune (le papier est le langage de ce qui se
   pose SUR le mur, un reglage de vue n'en est pas) ni pastille blanche (un
   jeton d'application, etranger a tout le reste) : le meme materiau que la
   commande d'a cote, en plus petit.
   Le signe garde l'encre sombre, sans contour ni ombre : le blanc et le contour
   brun des textes sont la voix du mur — le titre, les legendes, ce qui se LIT.
   Ces deux-la ne se lisent pas, ils se pressent.

   Ils se posent dans le coin bas GAUCHE, cote a cote : le − a gauche, le + a
   droite, l'ordre de lecture de l'echelle. Le coin oppose au relais, et la meme
   marge basse que lui : les deux commandes du bas se tiennent sur une ligne,
   chacune dans son coin, et le reglage ne se met plus en travers de
   l'invitation.
   Chaque signe a le diametre ecrit dans --zoom-signe, et donne une cible au
   doigt bien plus large que son trace. */
.zoom {
  /* #AC8A67, qui est exactement --wall-brown : le brun du mur, et celui du
     papier dans le fichier de la pastille d'ajout. Pris a la variable et non
     reecrit en dur — une teinte ecrite deux fois finit par ne plus l'etre.
     Le rond ne pose donc pas une couleur SUR le mur : il y perce un disque net,
     ou le monogramme s'arrete et ou le signe se lit sans rien derriere lui. */
  --zoom-rond: var(--wall-brown);

  /* Le trait noir des deux cercles. Il ne s'invente pas : c'est celui,
     deja cuit dans le fichier, du logo de la pastille d'ajout (53px de trace
     sur 863px de large, rendus dans 52px de pastille — un peu plus de 3px a
     l'ecran). Les trois commandes du bas partagent donc la meme epaisseur
     APPARENTE, ce qui est la seule qui se compare : ecrite en pixels et non en
     part du cercle, elle ne maigrit pas parce que le rond est deux fois plus
     petit que la pastille. */
  --zoom-trait: 3px;

  /* `absolute` dans le cadre .vue, pour la meme raison que le relais. */
  /* Le diametre d'un signe, ecrit en clair : la boite se mesure sur ses
     enfants. */
  --zoom-signe: 24px;
  /* L'ecart du − au + : celui d'une meme echelle, serre. */
  --zoom-ecart: 4px;

  /* `absolute` dans le cadre .vue, pour la meme raison que le relais. */
  position: absolute;
  left: max(16px, env(safe-area-inset-left));
  /* La marge basse du relais, a l'identique : au-dessus de la barre gestuelle,
     la page etant en `viewport-fit=cover`. */
  bottom: calc(22px + env(safe-area-inset-bottom));

  /* Deux colonnes larges comme un signe.
     `justify-content: start` : la grille ne s'etale pas, elle fait la largeur
     de ses deux colonnes, et le coin bas gauche reste le coin bas gauche. */
  display: grid;
  grid-template-columns: repeat(2, var(--zoom-signe));
  justify-content: start;
  column-gap: var(--zoom-ecart);
}

.zoom-btn {
  /* Chacun la moitie de la boite, moins l'ecart : le cercle fait 24px la ou le
     signe n'en dessine qu'une dizaine, et la cible au doigt vaut le cercle
     entier. `aspect-ratio` plutot qu'une hauteur ecrite : un rond dont le
     diametre se deduit de la largeur ne peut pas devenir un oeuf au point de
     rupture du telephone. */
  flex: none;
  width: var(--zoom-signe);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  padding: 0;

  font-family: var(--font-mono);
  font-weight: 700;
  /* Le signe occupe le cercle sans le toucher : environ sept dixiemes du
     diametre. Plus petit, il flottait au milieu d'un jeton. */
  font-size: 17px;
  line-height: 1;
  color: var(--ink);
  /* Voir --zoom-rond plus haut : le brun du mur lui-meme. Les trois commandes
     du bas sont ainsi du meme materiau, et le cercle se lit comme un morceau de
     mur degage plutot que comme un jeton d'application. */
  background: var(--zoom-rond);
  /* `border-box` (postit.css, la regle universelle du haut) : le trait se prend
     SUR les 24px du cercle au lieu de les gonfler. La cible au doigt et l'ecart
     entre les deux signes sont donc inchanges — c'est le disque brun qui cede
     la place au noir, pas le rond qui grossit. */
  border: var(--zoom-trait) solid #000;
  border-radius: 50%;
  cursor: pointer;
  transition: transform .18s ease, opacity .18s ease;
}
/* Le signe se centre sur son TRACE, pas sur sa ligne de texte.
   `place-items: center` cale la ligne, et le centre d'une ligne de Space Mono
   se trouve a 0.379em au-dessus de la ligne de pied, la ou le trace du + est a
   0.352em et celui du − a 0.344em. Le signe tombait donc d'un tiers de dixieme
   d'em sous le centre du rond : un demi-pixel, invisible sur un texte, mais
   pas au milieu d'un cercle de 20px que le trait noir vient de cerner — l'oeil
   compare le blanc du dessus a celui du dessous et voit le signe couler.
   Ecrit en em et non en pixels : la correction est une part du dessin de la
   fonte, elle doit suivre le corps du signe aux deux tailles.
   Une moyenne pour les deux signes : les 0.008em qui les separent valent un
   dixieme de pixel, que personne ne verra. */
.zoom-btn > span {
  display: block;
  transform: translateY(-.032em);
}

.zoom-btn:hover:not(:disabled) { transform: translateY(-2px); }
.zoom-btn:active:not(:disabled) { transform: none; }
/* Au bout de la course, le signe ne disparait pas : il palit. Un bouton qui
   s'en va laisserait l'autre seul et deplacerait la paire sous le doigt. */
.zoom-btn:disabled {
  opacity: .3;
  cursor: default;
}

/* ---------- Modale ---------- */
/* La page est en `viewport-fit=cover` : elle passe *sous* les barres du
   telephone. Rien ici ne s'en soustrayait, et le bas de la carte tombait
   derriere la barre d'outils et la barre gestuelle — la ou le doigt commande
   le navigateur et non la page. On ne pouvait donc ni lire ni atteindre la fin
   du formulaire, et le defilement paraissait bloque : il ne l'etait pas, c'est
   la carte qui etait plus haute que ce qu'on voyait d'elle.
   Les marges de securite vivent sur le cadre et non sur la carte : celle-ci
   n'a plus qu'a tenir dans ce qui reste (max-height: 100%), sans avoir a
   deviner la hauteur du telephone. */
.modal {
  position: fixed; inset: 0; z-index: 100; display: flex;
  padding:
    max(16px, env(safe-area-inset-top))
    max(12px, env(safe-area-inset-right))
    max(16px, env(safe-area-inset-bottom))
    max(12px, env(safe-area-inset-left));
}
.modal-backdrop {
  position: absolute; inset: 0;
  background: rgba(15,10,6,.66);
  backdrop-filter: blur(2px);
}
.modal-card {
  position: relative;
  margin: auto;
  width: min(480px, 100%);
  /* 100% du cadre, pas 92vh. `vh` est la hauteur *grande* du viewport sur iOS
     — celle qu'on aurait si les barres se repliaient — donc une carte reglee
     dessus depasse toujours de ce qu'on voit, et sa fin reste hors d'atteinte.
     Le cadre, lui, est en `position: fixed` : iOS le cale sur la zone
     reellement visible, barres comprises. Un pourcentage de cette boite suit
     donc la fenetre a tout moment, y compris quand le clavier monte. */
  max-height: 100%;
  overflow-y: auto;
  /* Arrive en bout de course, le geste passait au mur derriere : la page
     defilait a la place du formulaire, et l'elastique d'iOS decollait la
     modale entiere. */
  overscroll-behavior: contain;
  /* Le document annonce les deux schemas ; sans cette ligne, les widgets
     dessines par le navigateur (case a cocher, remplissage automatique,
     barre de defilement de la modale) passeraient en nuit sur un panneau
     creme. Le formulaire, lui, ne change pas avec le mur. */
  color-scheme: light;
  background: var(--paper);
  color: var(--ink);
  border-radius: 6px;
  padding: 28px 26px 26px;
  box-shadow: 0 30px 80px rgba(0,0,0,.5);
  -webkit-overflow-scrolling: touch;
}
/* la carte reçoit le focus à l'ouverture : pas de liseré autour du panneau */
.modal-card:focus { outline: none; }
.modal-x {
  position: absolute; top: 12px; right: 12px;
  width: 34px; height: 34px;
  border: none; background: transparent;
  font-size: 18px; color: var(--ink);
  cursor: pointer; border-radius: 50%;
}
.modal-x:hover { background: rgba(0,0,0,.06); }

.form-title {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 22px;
  margin: 0 0 6px;
}
.form-subtitle {
  margin: 0 0 20px;
  font-size: 14px;
  color: color-mix(in srgb, var(--ink) 72%, transparent);
}

/* zone photo */
.photo-row {
  margin-bottom: 20px;
}
.photo-row .photo-drop { margin-bottom: 0; }

.photo-drop {
  display: block;
  border: 1.5px dashed var(--paper-edge);
  border-radius: 6px;
  background: #fbf8f0;
  padding: 0;
  margin-bottom: 20px;
  cursor: pointer;
  overflow: hidden;
}
.photo-drop:hover { border-color: var(--sticky-edge); }
.photo-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; padding: 34px 16px; text-align: center;
}
.photo-plus { font-size: 30px; line-height: 1; color: var(--sticky-edge); }
.photo-label { font-family: var(--font-mono); font-size: 14px; }
.photo-hint  { font-size: 12px; color: color-mix(in srgb, var(--ink) 55%, transparent); }
.photo-preview { width: 100%; display: block; }

/* champs */
.field { margin-bottom: 16px; }
.field-label {
  display: block;
  font-family: var(--font-mono);
  font-size: 12.5px;
  margin-bottom: 6px;
}
.field-label .req { color: #b23b2e; font-style: normal; font-size: 11px; }
.field-label .opt { color: color-mix(in srgb, var(--ink) 50%, transparent); font-style: normal; font-size: 11px; }
.field input[type="text"],
.field input[type="email"],
.field textarea {
  width: 100%;
  font-family: var(--font-body);
  font-size: 15px;
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--paper-edge);
  border-radius: 5px;
  padding: 11px 12px;
  line-height: 1.45;
}
/* Une ligne au repos, a la hauteur exacte de la signature, et qui s'etire a la
   main : le mot laisse est court par nature (140 signes), la boite ne doit pas
   annoncer une redaction. */
.field textarea { resize: vertical; }
.field input:focus,
.field textarea:focus { outline: 2px solid var(--sticky-edge); outline-offset: 1px; border-color: var(--sticky-edge); }
.field-note {
  font-size: 12px;
  line-height: 1.4;
  color: color-mix(in srgb, var(--ink) 62%, transparent);
  margin: 8px 0 0;
}

/* honeypot */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }

/* cases à cocher */
.check {
  display: flex; gap: 10px; align-items: flex-start;
  font-size: 12.5px; line-height: 1.45;
  margin-bottom: 12px;
  color: color-mix(in srgb, var(--ink) 82%, transparent);
}
.check input { margin-top: 2px; flex: 0 0 auto; width: 16px; height: 16px; accent-color: var(--ink); }
.check-optin { font-family: var(--font-mono); font-size: 13px; color: var(--ink); }

/* bloc concours révélé sous l'opt-in */
.concours-block {
  margin: -2px 0 14px 26px;
  padding-left: 12px;
  border-left: 2px solid var(--paper-edge);
}
.concours-block .field { margin-bottom: 8px; }

.form-error {
  color: #b23b2e;
  font-size: 13px;
  font-family: var(--font-mono);
  margin: 4px 0 12px;
}

.form-actions { display: flex; gap: 10px; margin-top: 14px; }
.btn-solid {
  flex: 1;
  font-family: var(--font-mono); font-weight: 700; font-size: 15px;
  color: var(--paper); background: var(--ink);
  border: none; border-radius: 5px; padding: 13px 18px; cursor: pointer;
}
.btn-solid:hover { background: #000; }
.btn-solid:disabled { opacity: .55; cursor: default; }
.btn-ghost {
  font-family: var(--font-mono); font-size: 14px;
  color: var(--ink); background: transparent;
  border: 1px solid var(--paper-edge); border-radius: 5px;
  padding: 13px 18px; cursor: pointer;
}
.btn-ghost:hover { background: rgba(0,0,0,.04); }

/* succès */
.success-view { text-align: center; padding: 12px 0 6px; }
.success-mark {
  width: 54px; height: 54px; margin: 0 auto 14px;
  border-radius: 50%; background: var(--sticky); color: var(--ink);
  font-size: 26px; display: flex; align-items: center; justify-content: center;
}

/* focus visible clavier */
:focus-visible { outline: 2px solid var(--sticky-edge); outline-offset: 2px; }

/* Le curseur ne parle qu'a la souris : la ou il y a survol, c'est le survol qui
   ouvre, pas le clic. */
@media (hover: hover) {
  .note { cursor: default; }
}

/* mobile */
@media (max-width: 560px) {
  /* Colonnes a 100px : cinq emojis a 32px + gouttieres n'y tiendraient pas.
     5 x 20 = 100px, il reste juste ce qu'il faut. La demi-gouttiere tombe a
     zero : les zones restent jointives, et la barre cesse de deborder du papier. */
  /* Le dessin retrecit, la zone de clic beaucoup moins : c'est ici qu'on vise
     au doigt. */
  .note-react {
    --d: 20px; --g: 0px; --h: 26px;
    /* Zone de 21px pour une image de 16px : il y a moins de halo a rendre. */
    --rogne-coeur: 1.5px; --rogne-mante: 3px;
    --h-coeur: 23px; --h-mante: 21px;
    font-size: 14px;
  }
  .react-face img { width: 16px; height: 16px; }
  /* Le decompte est une pastille posee sur le coin du dessin : il en couvre
     toujours un quart, c'est le parti pris. Reste qu'il doit RETRECIR avec lui
     — laisse a 15px sur un emoji de 20, il ne mordait plus le coin, il mangeait
     l'image entiere. Meme rapport qu'en grand (11/20 contre 13/32), donc meme
     morsure, en plus petit.

     Ici la demi-gouttiere est nulle : les cercles se touchent. Le pixel de
     debordement en largeur passe quand meme — a la hauteur du decompte, le
     cercle voisin est rentre de 4px dans sa propre boite, il reste 1px de
     jour. C'est en hauteur que le decompte se degage de l'emoji. */
  .react-count {
    --cnt-x: -1px;
    --cnt-y: -6px;
    min-width: 10px; height: 10px; padding: 0 2px;
    border-radius: 5px; font-size: 7px; line-height: 10px;
  }
  /* Trois colonnes plutot que deux : sur un telephone de 375 px de large il
     reste 347 px de mur, et 3 x 100 + 2 x 10 = 320 y tiennent. Le papier fait
     alors 80 px de large la ou il en faisait 120 — on teste si le mur se lit
     encore a cette taille. */
  .wall { --col: 100px; --gouttiere: 10px; padding: 16px 14px 30px; }
  .note { margin-bottom: 14px; }
  /* La barre garde sa ligne unique sur telephone : le titre y est deja
     petit, et le pliage en deux lignes est une affaire de rideau. */
  .wall-head { padding: 0 58px; }
  .wall-head .wall-title {
    display: flex;
    row-gap: 0;
  }
  .cta-relais {
    width: 44px; height: 44px; font-size: 22px;
    right: max(12px, env(safe-area-inset-right));
    bottom: calc(16px + env(safe-area-inset-bottom));
  }
  /* Les pastilles suivent le relais, qui a retreci a 44px et remonte de 6px :
     elles restent dans son axe et juste au-dessus de lui. */
  /* Les deux signes descendent avec le relais, dans le coin oppose : les deux
     coins bas du telephone se tiennent a la meme hauteur. */
  .zoom {
    left: max(12px, env(safe-area-inset-left));
    bottom: calc(16px + env(safe-area-inset-bottom));
    /* Les memes 20px qu'avant, quand la boite de 44px se coupait en deux. */
    --zoom-signe: 20px;
  }
  /* Meme rapport qu'en grand, sur un cercle de 20px au lieu de 24. */
  .zoom-btn { font-size: 14px; }
  /* Le relais retrecit a 44px : son trait cuit tombe a 2.5px d'ecran, et le
     trait des cercles le suit. Le laisser a 3px aurait mange un cercle devenu
     plus petit, et rompu l'egalite qu'on cherche justement a tenir. */
  .zoom { --zoom-trait: 2.5px; }
  /* Le logo remplit la pastille : c'est elle qui retrecit, pas lui. Une
     taille fixe le laisserait grand dans une boite devenue petite. */
  .modal-card { padding: 24px 18px 20px; }
}

/* mouvement réduit */
@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
  .note { opacity: 1; transform: none; }
  /* Le battement est tout le propos de l'indice, mais il reste une animation
     qui tourne sans fin : la phrase se pose a mi-course et ne bouge plus. Sa
     sortie devient franche — le JS repose `hidden` a la meme seconde, elle ne
     traine pas. */
  .hint { animation: none; opacity: .55; }
  .hint.is-out { animation: none; opacity: 0; }
}

/* =========================================================
   L'ETAGE DE LA MAISON
   ---------------------------------------------------------
   Le mur vit sous le rayon Chat-Man du site, et il est la piece de l'onglet
   Post-it ! : la maison pose au-dessus de lui son nom et ses onglets
   (index.html, Epoque.chrome), avec epoque.css et la peau Chat-Man pour les
   habiller. Ces feuilles passent AVANT celle-ci ; ce qui suit tranche les
   deux endroits ou elles parlent de la meme chose.

   LE BODY EST AU MUR. epoque.css y pose la voix de la maison — sa taille de
   texte, son interligne, le clair force. Le mur, lui, s'est toujours compose
   sur les valeurs du navigateur : on les lui rend, et la voix de la maison
   passe a son etage, qui est le seul a la parler ici. Ce sont les valeurs du
   `body` d'epoque.css, sur le seul element qui les porte encore.
   ========================================================= */
body {
  font-size: medium;
  line-height: normal;
  color-scheme: inherit;
}
/* Meme remarque qu'en tete de feuille : pas de gouttiere reservee au mur. */
html { scrollbar-gutter: auto; }

/* LA MAISON EST NOIR ET BLANC EN TOUTES CIRCONSTANCES, meme au-dessus d'un
   mur de nuit : c'est elle qui tient les univers, elle n'en porte aucun. Son
   blanc s'arrete sous les onglets, dont l'encoche descend d'un pixel dans le
   mur — c'est le mur qui fait le panneau. */
.maison {
  background: var(--blanc);
  color: var(--noir);
  font-family: var(--grotesk);
  font-size: clamp(15px, 1.05vw, 17px);
  line-height: 1.55;
  color-scheme: light;
}

/* LE MUR NE DIT PAS SON NOM DEUX FOIS. L'onglet Post-it ! le porte, ouvert,
   juste au-dessus : la barre du titre quitte la vue et reste au document —
   ce qu'on retire de l'oeil, on le rend a ce qui lit la page a voix haute
   (meme geste qu'epoque.css pour le titre d'un rayon). Le mur commence donc
   sous les onglets, et `main` n'a plus de barre a qui garder la place. */
.wall-head {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
main { padding-top: 0; }

/* LE MUR EST LE PANNEAU DE L'ONGLET. `<main class="page">` : c'est le `.page`
   d'epoque.css et de la peau Chat-Man, le meme que sur la page du rayon, et
   il en prend tout — le brun, la tuile a la maille `--tuile` (sans le zoom du
   mur), la phase posee sur l'axe du panneau et arrondie au pixel d'ecran,
   l'ombre, le trait du bord et son trou sous l'encoche. `Epoque.sertit`
   trouve enfin un panneau : il cale les onglets sur son reseau, comme
   ailleurs. Le mur ne declare plus aucun fond.

   Ce que le mur garde a lui, c'est sa mise en page : ses colonnes ont leurs
   propres marges, donc le panneau ne rajoute pas les siennes, et il descend
   jusqu'au bas de la fenetre meme quand le mur est court (le body est une
   colonne, le panneau prend ce qui reste). */
body {
  display: flex;
  flex-direction: column;
}
main.page {
  /* LE MONOGRAMME A SA TAILLE VRAIE A COTE DES POST-IT, par la loi du rayon.
     La peau grossit deja la tuile du panneau d'un facteur quand on s'approche
     d'un objet (`--zoom-livre`, peau-chat-man.css) : le mur, c'est le mur
     Chat-Man vu de pres, et il donne le sien — la tuile de 59 mm a l'echelle
     d'un post-it de 76 mm (postit.js, mesurerEchelle, qui le publie en
     `--echelle`). Le zoom le suit de lui-meme. Foyer par defaut : l'axe du
     panneau, donc la phase reste celle du rayon. L'onglet, lui, garde la
     tuile du rayon : il appartient a l'etage de la maison. */
  --zoom-livre: var(--echelle, 1);
  flex: 1 0 auto;
  min-height: 0;
  /* En haut, UNE DEMI-TUILE d'air avant les premiers post-it — la tuile telle
     qu'on la voit, zoom compris : la respiration se compte dans l'unite du
     mur, et le motif ne bouge pas (il part du bord du panneau,
     `background-origin: border-box`). Une tuile entiere faisait 167 px sur
     un ecran large depuis que le monogramme a sa taille vraie. Et cet air se
     compte SOUS la bande de l'onglet (`--bande-ep`, epoque.css). */
  padding: calc(var(--tuile) * var(--zoom-livre) / 2 + var(--bande-ep, 0) * 1px) 0
           calc(48px + env(safe-area-inset-bottom));
}
