/* ═══════════════════════════════════════════════════════════════════════════
   Tatua · premium.css — couche de finition, 21/08/2026
   Chargée APRÈS styles.css et motion.css. N'écrase rien : elle n'ajoute que
   de la profondeur, de la matière et des micro-interactions.

   CONTRAT, identique à motion.css : aucun contenu n'est masqué durablement,
   la page reste complète sans JavaScript, et tout est désactivé sous
   prefers-reduced-motion. Un moteur de recherche ou un robot IA voit
   exactement la même page.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  --pm-ease-out: cubic-bezier(0.16, 1, 0.3, 1);      /* expo.out, le ressort « premium » */
  --pm-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* léger dépassement, pour les pressions */
  --pm-accent: #10b981;
  --pm-glow: 16, 185, 129;
}

/* ───────────────────────────────────────────────────────────────────────────
   1 · GRAIN. Le défaut qui trahit une page générée est l'aplat parfaitement
   lisse. Un grain très faible en superposition casse cette planéité et donne
   la matière d'un tirage. Coût : un seul pseudo-élément, aucune image.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-grain::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  opacity: 0.035;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ───────────────────────────────────────────────────────────────────────────
   2 · ARÊTE DE LUMIÈRE. Une surface premium attrape la lumière sur son bord
   supérieur. C'est ce détail, plus que l'ombre, qui donne l'impression de
   verre plutôt que de rectangle gris.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-edge {
  position: relative;
  isolation: isolate;
}
.pm-edge::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(160deg,
    rgba(255, 255, 255, 0.14) 0%,
    rgba(255, 255, 255, 0.03) 28%,
    rgba(255, 255, 255, 0) 55%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}

/* ───────────────────────────────────────────────────────────────────────────
   3 · HALO QUI SUIT LE CURSEUR. Les deux variables sont posées par premium.js.
   Sans JS, --pm-x/--pm-y valent 50% : le halo reste centré et discret, donc
   l'effet dégrade proprement au lieu de disparaître.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-spot {
  position: relative;
}
.pm-spot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 420ms var(--pm-ease-out);
  background: radial-gradient(
    360px circle at var(--pm-x, 50%) var(--pm-y, 50%),
    rgba(var(--pm-glow), 0.10),
    transparent 62%);
}
.pm-spot:hover::after { opacity: 1; }

/* ───────────────────────────────────────────────────────────────────────────
   4 · BOUTON. Pression physique plutôt que changement de couleur, plus une
   traînée de lumière qui traverse au survol. La traînée est en transform,
   donc composée par le GPU, jamais en background-position.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-btn {
  position: relative;
  overflow: hidden;
  /* overflow non visible réaligne un inline-block sur son bord de marge : sans ce
     vertical-align, les conteneurs de CTA grandissaient de 7 px. */
  vertical-align: middle;
  transform: translateZ(0);
  transition: transform 220ms var(--pm-ease-spring),
              box-shadow 260ms var(--pm-ease-out),
              filter 200ms linear;
}
.pm-btn::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 45%;
  height: 100%;
  transform: translateX(-160%) skewX(-18deg);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.22), transparent);
  pointer-events: none;
}
.pm-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 30px -10px rgba(var(--pm-glow), 0.55);
  filter: saturate(1.06);
}
.pm-btn:hover::after { animation: pm-sheen 780ms var(--pm-ease-out) forwards; }
.pm-btn:active { transform: translateY(0) scale(0.985); }
@keyframes pm-sheen {
  to { transform: translateX(260%) skewX(-18deg); }
}

/* ───────────────────────────────────────────────────────────────────────────
   5 · CHAMP DE SAISIE. C'est l'objet le plus important de la page : c'est là
   que le visiteur écrit son idée. Il mérite de réagir.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-field {
  transition: box-shadow 320ms var(--pm-ease-out),
              border-color 320ms var(--pm-ease-out),
              transform 320ms var(--pm-ease-out);
}
.pm-field:focus-within {
  border-color: rgba(var(--pm-glow), 0.55);
  box-shadow: 0 0 0 4px rgba(var(--pm-glow), 0.10),
              0 18px 44px -22px rgba(var(--pm-glow), 0.5);
  transform: translateY(-1px);
}

/* ───────────────────────────────────────────────────────────────────────────
   6 · CARTES. Lévitation courte, jamais de bond. 3px suffisent à signaler
   qu'un élément est vivant.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-lift {
  transition: transform 300ms var(--pm-ease-out),
              border-color 300ms var(--pm-ease-out),
              box-shadow 300ms var(--pm-ease-out);
}
.pm-lift:hover {
  transform: translateY(-3px);
  box-shadow: 0 18px 40px -24px rgba(0, 0, 0, 0.85);
}
/* La carte du hero cumule pm-lift et pm-tilt : sans cette règle, le survol (0,2,0)
   écrasait la rotation 3D de .pm-tilt (0,1,0) et l'inclinaison était du code mort. */
.pm-tilt.pm-lift:hover {
  transform: rotateX(var(--pm-rx, 0deg)) rotateY(var(--pm-ry, 0deg)) translateY(-3px) translateZ(0);
  box-shadow: 0 42px 92px -30px rgba(0, 0, 0, 0.85), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

/* ───────────────────────────────────────────────────────────────────────────
   7 · PARALLAXE. Décoratif uniquement, jamais sur du texte ni sur un contrôle.
   La variable est posée par premium.js ; sans JS elle vaut 0, donc rien ne
   bouge et rien ne se décale.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-par {
  transform: translate3d(0, var(--pm-par, 0px), 0);
  will-change: transform;
}

/* ───────────────────────────────────────────────────────────────────────────
   8 · TITRE. Révélation par mots, montés depuis leur ligne de base. Les mots
   sont enveloppés par premium.js ; sans JS le h1 est du texte normal, donc
   parfaitement lisible par un moteur de recherche.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-word {
  display: inline-block;
  will-change: transform, opacity;
}
.pm-word-in {
  animation: pm-rise 760ms var(--pm-ease-out) both;
}
@keyframes pm-rise {
  from { opacity: 0; transform: translateY(0.5em) rotate(1.2deg); }
  to   { opacity: 1; transform: translateY(0) rotate(0); }
}

/* ───────────────────────────────────────────────────────────────────────────
   9 · SOULIGNÉ QUI SE TRACE. Sur les liens de contenu, un trait qui se
   dessine au survol au lieu d'apparaître d'un bloc.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-underline {
  background-image: linear-gradient(var(--pm-accent), var(--pm-accent));
  background-size: 0% 1px;
  background-repeat: no-repeat;
  background-position: 0 100%;
  transition: background-size 320ms var(--pm-ease-out);
  text-decoration: none;
}
.pm-underline:hover,
.pm-underline:focus-visible { background-size: 100% 1px; }

/* ───────────────────────────────────────────────────────────────────────────
   10 · RESPECT DU RÉGLAGE SYSTÈME. Tout ce qui bouge s'arrête, tout ce qui
   éclaire reste. Une personne sensible au mouvement garde une belle page.
   ─────────────────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .pm-btn,
  .pm-lift,
  .pm-field,
  .pm-par,
  .pm-word,
  .pm-underline {
    transition: none !important;
    animation: none !important;
    transform: none !important;
  }
  .pm-btn::after { display: none; }
  .pm-word-in { opacity: 1; }
  .pm-spot::after { display: none; }
}

/* Écrans fins : on allège, la surface est trop petite pour de la profondeur. */
@media (max-width: 640px) {
  .pm-grain::after { opacity: 0.025; }
  .pm-spot::after { display: none; }
}

/* ───────────────────────────────────────────────────────────────────────────
   11 · INCLINAISON 3D DE LA CARTE DU HERO. Le geste signature de la page :
   la carte suit le curseur comme un objet posé sur une table. Amplitude
   volontairement faible (6 degrés) : au delà l'effet devient un gadget et
   l'image se déforme. Les variables sont posées par premium.js ; sans JS
   elles valent 0, la carte est parfaitement droite.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-scene { perspective: 1100px; }
.pm-tilt {
  transform: rotateX(var(--pm-rx, 0deg)) rotateY(var(--pm-ry, 0deg)) translateZ(0);
  transform-style: preserve-3d;
  transition: transform 220ms var(--pm-ease-out);
  will-change: transform;
}
.pm-tilt.pm-tilt-live { transition: transform 90ms linear; }
/* La lumière se déplace à l'inverse de l'inclinaison : c'est ce contre-mouvement
   qui vend le volume, plus que la rotation elle-même. */
/* ⚠️ ::after et non ::before : .pm-edge::before occupe déjà ce pseudo sur la même carte.
   En visant le même, le reflet 3D héritait du masque « xor » de l'arête, donc les DEUX
   effets étaient muets sur l'objet central de la page. */
.pm-tilt::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  opacity: var(--pm-shine, 0);
  transition: opacity 300ms var(--pm-ease-out);
  background: linear-gradient(
    var(--pm-shine-a, 120deg),
    rgba(255,255,255,0.16) 0%,
    rgba(255,255,255,0) 42%);
}

@media (prefers-reduced-motion: reduce) {
  .pm-tilt { transform: none !important; transition: none !important; }
  .pm-tilt::after { display: none; }
}
@media (max-width: 900px) {
  .pm-tilt { transform: none !important; }
  .pm-tilt::after { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   12 · AURORE. Le signal « futuriste » le plus fort et le moins coûteux : un
   fond qui respire au lieu d'un dégradé figé. Deux voiles de couleur en
   rotation lente, floutés à l'extrême, donc aucune arête visible et aucune
   texture à charger. Purement décoratif, sous le contenu, sans interception
   de clic.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-aurora {
  position: absolute;
  /* ⚠️ Ancrage EN PIXELS, pas en pourcentage. Avec inset:-35%, la moindre
     variation de hauteur du hero (arrivée de la police, image tardive)
     déplaçait ce bloc de 2 400 px de large : mesuré à lui seul 0,25 de CLS
     sur mobile. Ancré au bord haut avec une hauteur fixe, il ne bouge plus,
     quoi qu'il arrive au contenu. Le hero est en overflow:hidden, donc le
     surplus est rogné. */
  top: -240px;
  left: -30%;
  right: -30%;
  height: 1240px;
  z-index: 0;
  pointer-events: none;
  opacity: 0.5;
  filter: blur(84px);
  background:
    radial-gradient(42% 38% at 22% 28%, rgba(16, 185, 129, 0.30), transparent 68%),
    radial-gradient(36% 34% at 78% 22%, rgba(45, 212, 191, 0.20), transparent 66%),
    radial-gradient(46% 40% at 62% 76%, rgba(5, 150, 105, 0.22), transparent 70%);
  animation: pm-aurora-tourne 34s linear infinite;
  will-change: transform;
}
@keyframes pm-aurora-tourne {
  0%   { transform: rotate(0deg)   scale(1);    }
  50%  { transform: rotate(180deg) scale(1.14); }
  100% { transform: rotate(360deg) scale(1);    }
}

/* ───────────────────────────────────────────────────────────────────────────
   13 · GRILLE RÉACTIVE. Un maillage fin révélé uniquement autour du curseur,
   par un masque radial. Au repos il est invisible : la page reste sobre, et
   le motif n'apparaît que sous la main. Zéro élément ajouté au DOM.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-grid {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 700ms var(--pm-ease-out);
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.055) 1px, transparent 1px);
  background-size: 46px 46px;
  -webkit-mask-image: radial-gradient(220px circle at var(--pm-gx, -999px) var(--pm-gy, -999px), #000 12%, transparent 72%);
          mask-image: radial-gradient(220px circle at var(--pm-gx, -999px) var(--pm-gy, -999px), #000 12%, transparent 72%);
}
.pm-grid.pm-grid-on { opacity: 1; }

/* ───────────────────────────────────────────────────────────────────────────
   14 · TYPOGRAPHIE QUI RAYONNE. Uniquement sur les mots déjà en accent : le
   halo suit la couleur du texte, donc aucun risque de contraste. On n'ajoute
   pas de lueur sur du texte courant, qui deviendrait moins lisible.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-glow {
  text-shadow:
    0 0 18px rgba(var(--pm-glow), 0.34),
    0 0 46px rgba(var(--pm-glow), 0.16);
}

/* ───────────────────────────────────────────────────────────────────────────
   15 · BARRE DE NAVIGATION FLOTTANTE. Elle se détache du haut au défilement
   et devient une pastille de verre. C'est le code visuel qui date le plus une
   interface : une barre collée au bord fait 2019, une pastille flottante fait
   aujourd'hui.
   ─────────────────────────────────────────────────────────────────────────── */
.pm-nav {
  transition: transform 420ms var(--pm-ease-out),
              background-color 420ms var(--pm-ease-out),
              border-radius 420ms var(--pm-ease-out),
              box-shadow 420ms var(--pm-ease-out),
              width 420ms var(--pm-ease-out);
}
.pm-nav-flottante {
  transform: translateY(10px) scale(0.975);
  border-radius: 999px;
  background-color: rgba(11, 11, 12, 0.72);
  box-shadow: 0 18px 44px -26px rgba(0, 0, 0, 0.9),
              inset 0 1px 0 rgba(255, 255, 255, 0.07);
}

@media (prefers-reduced-motion: reduce) {
  .pm-aurora { animation: none !important; }
  .pm-grid { display: none; }
  .pm-nav, .pm-nav-flottante { transition: none !important; transform: none !important; }
}
@media (max-width: 900px) {
  .pm-grid { display: none; }
  .pm-aurora { filter: blur(60px); opacity: 0.38; }
  .pm-nav-flottante { transform: none; border-radius: 0; }
}
/* Ceinture : l'en-tête de l'app ne flotte jamais (premium.js l'exclut déjà). */
body.app-premium .pm-nav-flottante { transform: none; border-radius: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   16 · GALERIE « VU SUR DE LA VRAIE PEAU » — TRIABLE ET REPLIÉE
   ---------------------------------------------------------------------------
   La grille de 24 vignettes faisait 1 662 px d'un seul tenant : un mur qu'on
   fait défiler sans le lire. Le visiteur, lui, a UNE question précise —
   « ça donne quoi sur MON avant-bras ». On lui donne donc le tri par zone,
   et on ne montre que 8 rendus tant qu'il n'en demande pas plus.

   Tout est posé par JavaScript : le HTML servi aux robots garde ses 24 liens
   visibles, donc aucune ligne de contenu n'est retirée de l'index.
   ═══════════════════════════════════════════════════════════════════════════ */

.pm-tri {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
  margin: 0 0 1.75rem;
}
.pm-tri-btn {
  --pm-tri-bord: rgba(255, 255, 255, 0.09);
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  padding: 0.5rem 1rem;
  border-radius: 999px;
  border: 1px solid var(--pm-tri-bord);
  background: rgba(15, 14, 13, 0.72);
  color: #a8a29e;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  -webkit-appearance: none;
          appearance: none;
  transition: color 220ms ease, border-color 220ms ease,
              background-color 220ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.pm-tri-btn:hover { color: #f5f5f4; --pm-tri-bord: rgba(16, 185, 129, 0.45); transform: translateY(-1px); }
.pm-tri-n {
  font-size: 0.6875rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  opacity: 0.82;
}
/* L'état actif se lit sans la couleur seule : le fond change aussi de valeur,
   ce qui reste perceptible en vision daltonienne comme en noir et blanc. */
.pm-tri-btn[aria-pressed="true"] {
  color: #0b0b0c;
  background: #10b981;
  --pm-tri-bord: #10b981;
  box-shadow: 0 6px 20px -8px rgba(16, 185, 129, 0.7);
}
.pm-tri-btn[aria-pressed="true"] .pm-tri-n { opacity: 0.72; }

/* Les vignettes écartées par le tri, ou repliées, sortent du flux.
   [hidden] seul ne suffit pas : la grille impose display:grid aux enfants. */
.pm-gal-off { display: none !important; }

/* Entrée décalée des vignettes révélées : l'œil suit le remplissage au lieu
   de subir un saut. Le délai est plafonné pour que la 9ᵉ n'attende pas. */
@keyframes pm-gal-in {
  from { opacity: 0; transform: translateY(10px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}
.pm-gal-in { animation: pm-gal-in 420ms cubic-bezier(0.16, 1, 0.3, 1) both; }



@media (prefers-reduced-motion: reduce) {
  .pm-tri-btn { transition: none !important; }
  .pm-tri-btn:hover { transform: none !important; }
  .pm-gal-in { animation: none !important; }
}

/* Annonce réservée aux lecteurs d'écran : le nombre de rendus affichés change
   sans rechargement, il faut le dire à qui ne voit pas la grille. */
.pm-sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   17 · ÉQUILIBRE DES GRANDS TITRES
   Sans cela, « Un tatouage raté coûte des centaines d'euros à effacer. »
   laissait le mot « effacer. » seul sur sa ligne. balance répartit le titre
   sur des lignes de longueur voisine. Appliqué en CSS dès le premier rendu,
   donc identique avec la police de repli et avec la police finale : aucun
   décalage ajouté.
   ═══════════════════════════════════════════════════════════════════════════ */
h1.font-display,
h2.font-display,
h3.font-display,
.font-display.text-4xl,
.font-display.text-5xl { text-wrap: balance; }

/* ═══════════════════════════════════════════════════════════════════════════
   18 · TITRE DE L'APPLICATION : HAUTEUR RÉSERVÉE
   ---------------------------------------------------------------------------
   MESURÉ sur /en/app/ à 375 px : « Describe your dream tattoo » tient sur une
   ligne avec la police de repli et passe à deux avec Bricolage Grotesque.
   Ce basculement d'une ligne poussait tout le formulaire vers le bas —
   0,116 de CLS, au-dessus du seuil de Google, alors que la version française
   du même écran mesurait 0,022.

   On réserve donc les deux lignes que le titre occupe de toute façon une fois
   la police arrivée. min-height, pas height : un titre plus long reste libre
   de s'étendre. leading-tight vaut 1,25, d'où 2,5em.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 639px) {
  body.app-premium h1.font-display { min-height: 2.5em; }
}

/* ─── Galerie « Vu sur de la vraie peau » · RAIL défilant (21/08/2026) ─────
   Une seule rangée à aimantation : on choisit sa zone, on fait glisser.
   Remplace la grille « 8 + voir plus » qui restait un mur (4 rangées sur mobile).
   Le HTML (24 liens) ne change pas : le rail est purement une mise en page. */
.pm-rail {
  display: flex !important;
  grid-template-columns: none !important;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: 0.25rem;
  padding: 0.25rem 0.25rem 1rem;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 2.5%, #000 97.5%, transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 2.5%, #000 97.5%, transparent 100%);
}
.pm-rail::-webkit-scrollbar { display: none; }
.pm-rail > * {
  flex: 0 0 auto;
  width: clamp(180px, 23.2%, 280px);
  scroll-snap-align: start;
}
/* 641-900 px : 3 vignettes visibles, pour qu'on lise la rangée comme un rail */
@media (max-width: 900px) {
  .pm-rail > * { width: clamp(190px, 31%, 260px); }
}
/* ≤640 px : une vignette + l'aperçu de la suivante (le dépassement signale le glissement) */
@media (max-width: 640px) {
  .pm-rail > * { width: 60vw; }
}
/* Sur pointeur souris : une barre fine visible, pour que le défilement se voie */
@media (hover: hover) and (pointer: fine) {
  .pm-rail { scrollbar-width: thin; scrollbar-color: rgba(16, 185, 129, 0.35) transparent; }
  .pm-rail::-webkit-scrollbar { display: block; height: 6px; }
  .pm-rail::-webkit-scrollbar-thumb { background: rgba(16, 185, 129, 0.35); border-radius: 999px; }
  .pm-rail::-webkit-scrollbar-track { background: transparent; }
}
.pm-rail-wrap { position: relative; }
.pm-rail-nav {
  position: absolute; top: 50%; transform: translateY(-60%);
  width: 44px; height: 44px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(11, 11, 12, 0.78); color: #fafafa;
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  cursor: pointer; z-index: 3;
  transition: background 160ms ease, border-color 160ms ease, opacity 160ms ease, transform 160ms ease;
}
.pm-rail-nav:hover { background: rgba(16, 185, 129, 0.22); border-color: rgba(16, 185, 129, 0.55); }
.pm-rail-nav[disabled] { opacity: 0.28; cursor: default; }
.pm-rail-nav svg { width: 18px; height: 18px; }
.pm-rail-nav.prev { left: -14px; }
.pm-rail-nav.next { right: -14px; }
@media (hover: none) { .pm-rail-nav { display: none; } }
@media (max-width: 900px) { .pm-rail-nav.prev { left: -4px; } .pm-rail-nav.next { right: -4px; } }
@media (prefers-reduced-motion: reduce) { .pm-rail { scroll-behavior: auto; } }

/* ═══════════════════════════════════════════════════════════════════════════
   19 · LA BULLE DE CHAT NE DOIT RIEN RECOUVRIR
   ---------------------------------------------------------------------------
   MESURÉ à 375 px sur /app/ : la bulle (z-index 9998) passait devant le mur de
   paiement (z-index 100). elementsFromPoint au centre de la bulle renvoyait un
   bouton d'achat juste dessous, recouvert sur 39x60 px. Un visiteur décidé à
   payer tapait le chat.
   Hors mur, la même bulle recouvrait 2 348 px² du bouton « Générer 4 designs ».

   Deux réponses distinctes :
   · pendant une modale, la bulle disparaît (règle :has ci-dessous) ;
   · hors modale, elle se soulève au-dessus du bouton principal quand les deux
     se croisent (classe posée par js/premium.js, qui mesure les rectangles).
   ═══════════════════════════════════════════════════════════════════════════ */
body:has(#paywallModal:not(.hidden)) #tatua-chat-bubble,
body:has(#paywallModal:not(.hidden)) #tatua-chat-panel,
body:has(#anonGate) #tatua-chat-bubble,
body:has(#onboarding-modal) #tatua-chat-bubble,
body:has(#remix-editor) #tatua-chat-bubble,
body:has(#moodboard-modal) #tatua-chat-bubble,
body:has(#compare-placements-modal) #tatua-chat-bubble {
  display: none !important;
}

#tatua-chat-bubble.pm-bulle-haute {
  transform: translateY(var(--pm-bulle-decalage, -72px));
  transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
@media (prefers-reduced-motion: reduce) {
  #tatua-chat-bubble.pm-bulle-haute { transition: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   20 · LA CROIX DU MUR DE PAIEMENT DOIT RESTER ATTEIGNABLE
   ---------------------------------------------------------------------------
   MESURÉ à 375 px : la carte du mur fait 1 280 px de haut pour 745 px visibles.
   Il faut donc défiler 535 px pour atteindre les packs, et la croix, en
   position:absolute dans ce conteneur qui défile, part alors à y = -486 px,
   hors écran. À 1 440 px elle part à -95 px. Restait la gouttière de fond
   cliquable : 16 px de chaque côté à 375 px.

   #paywallModal porte backdrop-filter, ce qui en fait le bloc conteneur des
   descendants en position:fixed. La croix se cale donc sur la modale, qui est
   elle-même en inset:0 : elle reste visible quel que soit le défilement.
   Cible portée à 44x44, le minimum recommandé pour le doigt.
   ═══════════════════════════════════════════════════════════════════════════ */
#paywallModal #paywallClose {
  position: fixed;
  top: max(0.75rem, env(safe-area-inset-top, 0px));
  right: max(0.75rem, env(safe-area-inset-right, 0px));
  z-index: 3;
  width: 44px;
  height: 44px;
  border-radius: 999px;
  background: rgba(11, 11, 12, 0.74);
  border: 1px solid rgba(255, 255, 255, 0.14);
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
  color: #e7e5e4;
  transition: background-color 200ms ease, border-color 200ms ease, color 200ms ease;
}
#paywallModal #paywallClose:hover {
  background: rgba(28, 25, 23, 0.9);
  border-color: rgba(255, 255, 255, 0.28);
  color: #fafafa;
}
@media (prefers-reduced-motion: reduce) {
  #paywallModal #paywallClose { transition: none !important; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   21 · BARRE DE NAVIGATION SOUS 400 px
   ---------------------------------------------------------------------------
   MESURÉ sur la landing FR : l'écart entre le mot « Tatua » et le bouton
   « Se connecter » tombe à 2 px à 375 px, et à 320 px la barre déborde de 8 px
   hors du viewport. La version EN n'a pas le problème, ses libellés étant plus
   courts (62 px d'écart).

   On masque le mot, pas la marque : le monogramme reste, et il est le vrai
   repère visuel. Rétabli dès 400 px, où l'écart repasse à 41 px.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 399px) {
  nav a[href="/"] > span.font-display,
  header a[href="/"] > span.font-display { display: none; }
}

/* ─────────────────────────────────────────────────────────────────────────────
   22 · ONGLETS DE MODE : rendre le défilement VISIBLE (≤640 px)

   Le problème, mesuré : styles.css transforme la grille en une rangée défilante
   sous 640 px — décision correcte, la grille 3 colonnes rognait « Texte →
   Tatouage » en français — mais elle masque aussi la barre de défilement
   (scrollbar-width:none). Résultat sur un écran de 375 px : la bande déborde de
   27 px, « Sur ma photo » est coupé en plein mot, et rien n'indique qu'il y a
   la suite. Quatre modes sur sept sont hors champ, dont Coverup et Stencil qui
   font partie de ce que le client paie. Une fonctionnalité qu'on ne trouve pas
   est une fonctionnalité qu'on n'a pas achetée.

   La parade : la technique des ombres de défilement en CSS pur. Deux dégradés
   ancrés au CONTENU (background-attachment: local) glissent avec lui ; deux
   ancrés au CADRE (scroll) restent en place. Aux extrémités, les premiers
   recouvrent les seconds, l'indice disparaît donc tout seul quand il n'y a plus
   rien à voir de ce côté. Aucun JavaScript, aucun écouteur de défilement.

   #0b0b0c est la couleur de fond réelle du corps (relevée dans le navigateur,
   pas devinée) : c'est elle qui doit masquer, sinon le dégradé se voit.
   ───────────────────────────────────────────────────────────────────────────── */
@media (max-width: 640px) {
  body.app-premium nav.mode-tabs {
    background:
      linear-gradient(to right, #0b0b0c 35%, rgba(11, 12, 13, 0)) left center / 26px 100% no-repeat local,
      linear-gradient(to left,  #0b0b0c 35%, rgba(11, 12, 13, 0)) right center / 26px 100% no-repeat local,
      linear-gradient(to right, rgba(16, 185, 129, .30), rgba(16, 185, 129, 0)) left center / 20px 100% no-repeat scroll,
      linear-gradient(to left,  rgba(16, 185, 129, .30), rgba(16, 185, 129, 0)) right center / 20px 100% no-repeat scroll;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   23 · L'ANIMATION DES MOTS NE DOIT PAS MORDRE SUR LE TEXTE SUIVANT

   Mesuré sur /app/ à 375 px : le titre « Décris ton tatouage de rêve » tient sur
   deux lignes, son bas est à 236 px, le paragraphe commence à 240 px — 4 px
   d'écart. Or pm-rise part de translateY(0.5em), soit 12 px à 24 px de corps.
   Pendant les 760 ms de l'animation, la seconde ligne du titre passe donc
   PAR-DESSUS le paragraphe. À chaque chargement de page, sur l'écran principal
   du produit.

   Le réflexe serait de réduire la distance, mais pour ne jamais mordre il
   faudrait descendre sous 4 px (0.17em) : l'effet ne se verrait plus. On
   réserve donc la place à la place — juste sous les titres réellement animés,
   pas sur tous les titres du site.

   0.55em > 0.5em de course : la marge absorbe le déplacement quelle que soit la
   taille de police, sans valeur en dur à re-régler quand le titre change.
   ───────────────────────────────────────────────────────────────────────────── */
/* Variante SANS mouvement vertical, choisie par premium.js quand le titre n'a pas
   la place de monter sans mordre sur le texte suivant. Même durée, même courbe :
   la page garde son rythme, seul le déplacement disparaît. On ne touche PAS à la
   marge — la modifier déplacerait la mise en page et referait du décalage cumulé,
   qui a déjà coûté cher sur ce projet (0,428 mesuré en août). */
.pm-word-fade {
  animation: pm-fade-in 620ms var(--pm-ease-out) both;
}
@keyframes pm-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ─────────────────────────────────────────────────────────────────────────────
   24 · CACHE FLOUTÉ DU PAYWALL

   Le tunnel est en « paiement d'abord » : rien n'est généré avant l'encaissement.
   Il n'existe donc AUCUNE image du tatouage du client à montrer, et en fabriquer
   une reviendrait à lui vendre autre chose que ce qu'il a vu.

   Ce cache ne prétend pas être sa création. Le flou est poussé à 26 px, au-delà de
   toute reconnaissance : ce qui reste est une texture d'encre, un signal de contenu
   verrouillé. Le texte au-dessus dit la vérité — « Ton idée est prête », « le
   paiement lance ta création » — et c'est lui qui porte la promesse, pas l'image.

   Les visuels viennent de assets/showcase, donc de créations maison : aucune
   question de droits, et rien qui ressemble au travail d'un tatoueur identifiable.
   ───────────────────────────────────────────────────────────────────────────── */
.pw-teaser {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: .5rem;
  margin: 0 0 1.25rem;
  position: relative;
  border-radius: 1rem;
  overflow: hidden;
}
.pw-teaser__tuile {
  position: relative;
  aspect-ratio: 1;
  border-radius: .75rem;
  overflow: hidden;
  background: #141416;
}
.pw-teaser__tuile img {
  width: 100%; height: 100%; object-fit: cover;
  /* scale(1.15) : le flou déborde du cadre, sans lui les bords révèlent une bordure nette */
  filter: blur(26px) saturate(.35) contrast(1.1);
  transform: scale(1.15);
  opacity: .55;
}
.pw-teaser__voile {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at center, rgba(11,11,12,.15), rgba(11,11,12,.72));
  pointer-events: none;
}
.pw-teaser__cadenas {
  width: 2.5rem; height: 2.5rem;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: rgba(11,11,12,.82);
  border: 1px solid rgba(16,185,129,.45);
  color: #10b981;
}
.pw-teaser__cadenas svg { width: 1.15rem; height: 1.15rem; }
.pw-teaser__legende {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: .6rem .75rem;
  font-size: .72rem; letter-spacing: .06em; text-transform: uppercase;
  text-align: center; color: #ECECEF;
  background: linear-gradient(to top, rgba(11,11,12,.92), rgba(11,11,12,0));
}
@media (prefers-reduced-motion: reduce) { .pw-teaser__tuile img { transform: none; } }

/* ───────────────────────────────────────────────────────────────────────────
   25 · PAYWALL 3 OFFRES (01/09/2026). Offres empilées sélectionnables (radio) :
   annuel (meilleure offre, présélectionné) / hebdo / déblocage 0,99 € discret,
   puis un seul bouton « Continuer ». Tailwind est COMPILÉ (tw-e.css) : ces classes
   bespoke évitent d'injecter des utilitaires arbitraires absents du build. Palette
   alignée sur ink-*/accent de l'app (accent #10b981, ok #34d399, ink chauds).
   ─────────────────────────────────────────────────────────────────────────── */
#paywallModal .pw-design { margin: 0 0 16px; border-radius: 16px; overflow: hidden;
  border: 1px solid rgb(41 37 36); background: rgb(11 11 12); max-height: 200px; }
#paywallModal .pw-design img { width: 100%; height: 100%; max-height: 200px; object-fit: contain; display: block; }
#paywallModal .pw-design.pw-design--locked img { filter: blur(18px) saturate(.8); transform: scale(1.12); }

#paywallModal .pw-benefits { list-style: none; margin: 0 0 18px; padding: 0; display: grid; gap: 9px; }
#paywallModal .pw-benefits li { display: flex; align-items: center; gap: 10px; font-size: .92rem; color: rgb(231 229 228); }
#paywallModal .pw-check { flex: none; width: 20px; height: 20px; border-radius: 50%;
  background: rgba(16,185,129,.16); color: #10b981; display: inline-flex; align-items: center; justify-content: center; }
#paywallModal .pw-check svg { width: 12px; height: 12px; }

#paywallModal .pw-offers { display: grid; gap: 10px; margin: 0 0 14px; }
#paywallModal .pw-offer { position: relative; display: flex; align-items: center; gap: 13px; width: 100%; text-align: left;
  background: rgb(15 14 13); border: 1.5px solid rgb(41 37 36); border-radius: 16px; padding: 16px 18px; cursor: pointer;
  color: rgb(245 245 244); transition: border-color .18s, background .18s; }
#paywallModal .pw-offer:hover { border-color: rgba(16,185,129,.5); }
#paywallModal .pw-offer.sel { border-color: #10b981; background: linear-gradient(180deg, rgba(16,185,129,.10), rgb(15 14 13)); }
#paywallModal .pw-radio { flex: none; width: 22px; height: 22px; border-radius: 50%;
  border: 2px solid rgb(87 83 78); position: relative; transition: border-color .18s; }
#paywallModal .pw-offer.sel .pw-radio { border-color: #10b981; }
#paywallModal .pw-offer.sel .pw-radio::after { content: ""; position: absolute; inset: 4px; border-radius: 50%; background: #10b981; }
#paywallModal .pw-lead { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
#paywallModal .pw-lead .pw-t { font-weight: 700; font-size: 1.02rem; line-height: 1.1; }
#paywallModal .pw-lead .pw-s { font-size: .78rem; color: rgb(168 162 158); }
#paywallModal .pw-price { flex: none; text-align: right; display: flex; flex-direction: column; align-items: flex-end; line-height: 1.15; }
#paywallModal .pw-price .pw-was { font-size: .72rem; color: rgb(120 113 108); text-decoration: line-through; text-decoration-color: #e2564d; }
#paywallModal .pw-price .pw-big { font-weight: 800; font-size: 1.25rem; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
#paywallModal .pw-price .pw-per { font-size: .74rem; color: rgb(168 162 158); }
#paywallModal .pw-best { position: absolute; top: -9px; left: 16px; background: #10b981; color: rgb(11 11 12);
  font-size: .62rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; padding: 2px 8px; border-radius: 6px; }
/* 3e option (déblocage 0,99 €) : présente mais volontairement discrète */
#paywallModal .pw-offer.mini { padding: 12px 18px; opacity: .62; }
#paywallModal .pw-offer.mini:hover { opacity: .85; }
#paywallModal .pw-offer.mini .pw-radio { width: 18px; height: 18px; }
#paywallModal .pw-offer.mini .pw-lead .pw-t { font-size: .9rem; font-weight: 600; }
#paywallModal .pw-offer.mini .pw-price .pw-big { font-size: 1rem; }

#paywallModal .pw-reassure { display: flex; align-items: center; justify-content: center; gap: 8px;
  font-size: .8rem; color: rgb(168 162 158); text-align: center; margin: 0 0 14px; }
#paywallModal .pw-reassure svg { width: 16px; height: 16px; flex: none; color: #34d399; }

#paywallModal .pw-cta { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%;
  background: #10b981; color: rgb(11 11 12); font-weight: 800; font-size: 1.02rem; border: none; border-radius: 14px;
  padding: 15px; cursor: pointer; transition: filter .18s, transform .06s; }
#paywallModal .pw-cta:hover { filter: brightness(1.06); }
#paywallModal .pw-cta:active { transform: translateY(1px); }
#paywallModal .pw-cta svg { width: 18px; height: 18px; }
#paywallModal .pw-offer:focus-visible, #paywallModal .pw-cta:focus-visible {
  outline: 2px solid #34d399; outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  #paywallModal .pw-offer, #paywallModal .pw-cta, #paywallModal .pw-radio { transition: none; }
}

/* Deux utilitaires Tailwind employés par app-v22.js mais ABSENTS du CSS compilé (css/tw-e.css),
   parce que le générateur ne scanne pas ce fichier JS. Ils étaient donc écrits dans le HTML et
   sans aucun effet.
   `max-w-xs` porte la liste d'arguments de la bannière post-essai : sans elle, les trois lignes
   s'étalaient sur toute la largeur de la carte au lieu d'être centrées en colonne étroite, juste
   là où l'on demande au visiteur de payer.
   Ajoutés ICI plutôt que par une recompilation de Tailwind : cette couche est additive, quand un
   rebuild du CSS toucherait les 3600 pages du site pour deux règles. Mêmes valeurs que Tailwind. */
.max-w-xs { max-width: 20rem; }
.underline-offset-2 { text-underline-offset: 2px; }

/* ============================================================================
   MUR DE PAIEMENT SUR MOBILE : rendre le bouton atteignable (02/09)

   97,2 % des sessions sont mobiles (243 sur 250). Or, dans l'état RÉEL du mur,
   celui qu'un client voit juste après avoir reçu son design (aperçu affiché +
   idée + trois offres + bénéfices), le contenu mesure ~1185 px alors que le
   bouton « Continuer » commence à ~1054 px. Mesuré INVISIBLE à l'ouverture sur
   375×667, 393×745 et 375×812, et à 375×667 aucune des trois offres n'apparaît :
   la découpe tombe au milieu de la liste des bénéfices. La carte s'ouvre à
   scrollTop 0 et rien ne rattrapait : le bouton n'était pas collant.

   Or ce bouton est le SEUL chemin vers le paiement pour un compte sans crédit.
   Un client qui ne peut pas atteindre le bouton ne peut pas acheter, quelle que
   soit la qualité de l'offre au-dessus.

   Deux gestes, tous deux additifs et réversibles :
   1. le bouton se colle en bas de la carte défilante, donc il est visible dès
      l'ouverture, quel que soit l'écran ;
   2. l'aperçu du design est plafonné plus bas sur mobile, ce qui remonte les
      offres dans le premier écran au lieu de les pousser hors champ.
   ============================================================================ */
@media (max-width: 640px) {
  #paywallModal .pw-cta {
    position: sticky;
    bottom: 0;
    z-index: 3;
    /* ⚠️ Cette ombre a d'abord été écrite `rgb(41 37 36)`, SANS canal alpha, donc opaque, et
       elle remontait de 14 px au-dessus du bouton : elle recouvrait la dernière ligne du
       paragraphe placé juste avant, c'est-à-dire la mention légale de reconduction, sur
       l'écran même où le client sort sa carte. Une ombre est une ombre, pas un masque : elle
       doit être translucide et rester sous le bouton. */
    box-shadow: 0 -10px 16px -14px rgba(11, 11, 12, .85);
    /* Espace franc avec le texte du dessus, plutôt que de compter sur l'ombre pour séparer. */
    margin-top: 10px;
  }
  /* La mention de reconduction et la garantie restent lisibles au-dessus du
     bouton collant plutôt que d'être recouvertes. */
  #paywallModal .pw-reassure { margin-bottom: 6px; }
  /* 200 px d'aperçu sur un écran de 667 px, c'est 30 % de la hauteur consommée
     par une image que le client vient déjà de voir en grand dans l'application. */
  #paywallModal .pw-design img { max-height: 120px; }
}
