/* ===================================================================
   DOM'S MARKET — enhance.css
   Couche visuelle additive, chargée APRÈS le <style> de index.html.

   Règle du jeu : on n'ajoute que des propriétés de présentation.
   Aucun `display`, aucune position ni taille susceptible de déplacer ou
   masquer un élément existant, aucun sélecteur qui dépende du texte
   généré par le JS.
   =================================================================== */

/* ── JETONS ─────────────────────────────────────────────────────────
   Une seule source pour les ombres, rayons, durées et couleurs
   d'accent. Les variables de index.html restent la référence pour les
   couleurs de base : on ne fait que les compléter. */
:root {
  --surface-1: rgba(255, 255, 255, .035);
  --surface-2: rgba(255, 255, 255, .06);
  --hairline:  rgba(255, 255, 255, .09);

  --glow-purple: rgba(124, 58, 237, .35);
  --glow-orange: rgba(245, 166, 35, .35);
  --ring: 0 0 0 3px rgba(124, 58, 237, .4);

  --elev-1: 0 1px 2px rgba(0,0,0,.3), 0 2px 8px rgba(0,0,0,.22);
  --elev-2: 0 4px 12px rgba(0,0,0,.34), 0 12px 32px rgba(0,0,0,.38);
  --elev-3: 0 8px 24px rgba(0,0,0,.42), 0 24px 64px rgba(0,0,0,.5);

  --r-sm: 10px;
  --r-md: 16px;
  --r-lg: 22px;

  --ease-out: cubic-bezier(.22, 1, .36, 1);
  --ease-spring: cubic-bezier(.34, 1.4, .64, 1);
}

/* ── TYPOGRAPHIE ────────────────────────────────────────────────────
   La pile d'origine ('Segoe UI') tombe sur une police générique sur
   Android, où se trouve l'essentiel du trafic. On passe sur la police
   système, plus nette et déjà présente sur l'appareil. */
body, input, select, textarea, button {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
               'Helvetica Neue', system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body { text-rendering: optimizeLegibility; }

.section-title { letter-spacing: -.3px; }
.detail-name, .wallet-amount, .modal-title, .topbar-title { letter-spacing: -.5px; }

/* Chiffres alignés : les montants ne « dansent » plus quand ils changent. */
.wallet-amount, .wallet-chip, .history-amount, .product-price,
.detail-price, .stat-value, .d-amount {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* ═══════════════════════════════════════════════════════════════════
   FOND VIVANT

   Le fond n'est plus un pseudo-élément de `body` mais une couche
   injectée par enhance.js (`.dm-bg`). Trois raisons :

   1. Un pseudo-élément ne donne que deux plans ; il en faut cinq pour
      obtenir de la profondeur (nappes, faisceau, trame, grain, vignette).
   2. L'ancienne version appliquait `filter: blur(30px)` à une surface
      plein écran *animée*. Un flou est recalculé à chaque image : c'était
      le poste le plus cher de la page sur mobile, pour un rendu que des
      dégradés radiaux donnent gratuitement. Il n'y a plus aucun `filter`
      ici.
   3. Une vraie couche peut être mise en pause quand l'onglet passe en
      arrière-plan, et réagir à l'état de l'application.

   Règle de coût : après la première peinture, seules `transform` et
   `opacity` changent — le compositeur s'en occupe, le processeur
   principal reste libre pour le rendu des listes.
   ═══════════════════════════════════════════════════════════════════ */

.dm-bg {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  /* Isole totalement la couche : rien de ce qui s'y passe ne peut
     provoquer un recalcul de mise en page ailleurs dans la page. */
  contain: strict;
}

/* ── Photo de fond ─────────────────────────────────────────────────
   Plan le plus profond : les nappes, la trame et le grain passent
   par-dessus et l'intègrent au reste au lieu de la laisser en autocollant.

   Le chemin du fichier vient de enhance.js (`--dm-photo`) : c'est lui qui
   vérifie d'abord que l'image existe et se charge, et il n'y a ainsi
   qu'un seul endroit à modifier pour changer de visuel.

   L'opacité reste à 0 tant que `body.dm-photo` n'est pas posée. Si le
   fichier manque, est trop lourd, ou si l'utilisateur a demandé
   l'économie de données, rien n'apparaît et le fond d'origine reste
   intact — aucun trou, aucun carré gris. */
.dm-bg-photo {
  position: absolute;
  inset: -3%;
  background-image: var(--dm-photo, none);
  background-size: cover;
  /* 28% et non `center`. Sur un écran large, `cover` cale sur la largeur
     et rogne fortement en hauteur : centrer couperait le visage, qui se
     trouve à ~34 % de la hauteur de l'illustration. Sur un téléphone en
     portrait, l'image tient entière en hauteur et cette valeur n'a aucun
     effet — elle ne coûte donc rien là où elle ne sert pas. */
  background-position: center 28%;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity .9s var(--ease-out);
}

/* Écran de connexion : la photo est le sujet, à pleine intensité.
   L'illustration ne fait que 16 % de luminance moyenne — elle porte son
   propre contraste et n'a besoin d'aucune atténuation supplémentaire. */
body.dm-photo .dm-bg-photo {
  opacity: 1;
  animation: dmPhotoDrift 44s ease-in-out infinite alternate;
}

/* Dans l'application : elle recule, sans disparaître. Le contenu y est
   posé sur des cartes opaques — la photo ne se voit que dans les espaces
   entre elles, où elle joue le rôle d'une texture. C'est le sens de
   « en commençant par la page de connexion » : c'est là qu'elle a de la
   place, ici elle accompagne. */
body.dm-photo.dm-in-app .dm-bg-photo { opacity: .45; }

/* Dérive lente, façon travelling. Une seule propriété animée, sur le
   compositeur : le coût est le même que pour une image fixe. */
@keyframes dmPhotoDrift {
  0%   { transform: scale(1)    translate3d(0, 0, 0); }
  100% { transform: scale(1.07) translate3d(-1.5%, -1%, 0); }
}

/* ── Voile de lisibilité ───────────────────────────────────────────
   Garantit le contraste du texte blanc quelle que soit l'image posée.

   Ces valeurs sont calibrées sur l'illustration en place, mesurée bande
   par bande : 11 % de luminance en haut, 23-25 % dans la moitié basse,
   9 % tout en bas. Un voile générique, prévu pour une photo claire,
   l'écrasait en noir et annulait l'intérêt de la mettre. Le bas reste le
   plus chargé — c'est là que passe la barre de navigation.

   Si tu remplaces l'image par une plus claire, ce sont ces trois arrêts
   qu'il faut remonter. */
.dm-bg-scrim {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity .9s var(--ease-out);
  background:
    linear-gradient(180deg, rgba(9, 9, 16, .2) 0%, rgba(9, 9, 16, .32) 52%, rgba(9, 9, 16, .66) 100%),
    radial-gradient(120% 80% at 50% 32%, transparent 42%, rgba(6, 6, 12, .38) 100%);
}
body.dm-photo .dm-bg-scrim { opacity: 1; }

/* Dans l'application, le voile se referme : la photo n'est plus qu'une
   texture perceptible dans les espaces entre les cartes. */
body.dm-photo.dm-in-app .dm-bg-scrim {
  background: linear-gradient(180deg, rgba(11, 11, 18, .6), rgba(9, 9, 15, .74));
}

/* La vignette de l'horizon ferait doublon avec celle du voile : cumulées,
   elles noircissent les angles, là où passent justement les bras du
   personnage. Avec une photo, on ne garde que la lueur chaude du bas. */
body.dm-photo .dm-bg-horizon {
  background: radial-gradient(80% 34% at 50% 106%, rgba(245, 166, 35, .12), transparent 70%);
}

/* Les nappes de couleur s'effacent devant la photo sur l'écran de
   connexion : à pleine intensité elles la délavent. Dans l'application,
   la photo ayant reculé, elles reprennent leur rôle habituel. */
body.dm-photo:not(.dm-in-app) {
  --dm-o-p: .34;
  --dm-o-o: .26;
  --dm-o-c: .3;
}

/* ── Place faite au sujet ──────────────────────────────────────────
   #auth-screen centre sa carte verticalement. Sur un téléphone, `cover`
   cale l'illustration sur la hauteur : le visage se retrouve vers 34 %
   de l'écran, c'est-à-dire précisément derrière le haut de la carte. On
   voyait les yeux, un bras, du feu — et rien du personnage.

   La carte descend donc, uniquement quand une photo est active. Sans
   elle, le centrage d'origine reste strictement inchangé.

   `flex-start` et non `flex-end` : sur un petit écran, la carte dépasse
   du bas et la page défile normalement. Centrée ou collée en bas, le
   haut du formulaire serait rogné et inatteignable. */
body.dm-photo #auth-screen {
  align-items: flex-start;
  padding-top: clamp(110px, 40vh, 420px);
  padding-bottom: 40px;
}

/* ── Nappes de couleur ─────────────────────────────────────────────
   Trois taches lumineuses, chacune avec sa propre dérive. Les durées
   sont volontairement longues et premières entre elles : les nappes ne
   se recroisent jamais au même endroit, le fond ne « boucle » pas
   visiblement.

   L'opacité passe par une variable héritée de `body[data-page]` : c'est
   ce qui rend le fond réactif à la navigation sans repeindre quoi que ce
   soit — une transition d'opacité reste sur le compositeur. */
/* Pas de `will-change` ici : une animation `transform` en cours suffit à
   faire promouvoir l'élément par le navigateur. L'ajouter réserverait de
   la mémoire vidéo en permanence pour trois surfaces de la taille de
   l'écran — sur un téléphone d'entrée de gamme, c'est autant de moins
   pour le défilement des listes. */
.dm-bg-blob {
  position: absolute;
  border-radius: 50%;
  transition: opacity 1.2s var(--ease-out);
}

.dm-bg-blob.p {
  width: 92vmax; height: 92vmax;
  top: -32vmax; left: -28vmax;
  background: radial-gradient(closest-side, rgba(124, 58, 237, .55), rgba(124, 58, 237, 0));
  opacity: var(--dm-o-p, .85);
  animation: dmDriftA 47s ease-in-out infinite alternate;
}

.dm-bg-blob.o {
  width: 62vmax; height: 62vmax;
  top: -14vmax; right: -24vmax;
  background: radial-gradient(closest-side, rgba(245, 166, 35, .36), rgba(245, 166, 35, 0));
  opacity: var(--dm-o-o, .7);
  animation: dmDriftB 61s ease-in-out infinite alternate;
}

.dm-bg-blob.c {
  width: 100vmax; height: 100vmax;
  bottom: -52vmax; left: 8vmax;
  background: radial-gradient(closest-side, rgba(76, 60, 220, .42), rgba(76, 60, 220, 0));
  opacity: var(--dm-o-c, .8);
  animation: dmDriftC 73s ease-in-out infinite alternate;
}

@keyframes dmDriftA {
  0%   { transform: translate3d(0, 0, 0)            scale(1); }
  50%  { transform: translate3d(9vw, 5vh, 0)        scale(1.13); }
  100% { transform: translate3d(-5vw, 11vh, 0)      scale(1.05); }
}

@keyframes dmDriftB {
  0%   { transform: translate3d(0, 0, 0)            scale(1.06); }
  50%  { transform: translate3d(-11vw, 9vh, 0)      scale(.92); }
  100% { transform: translate3d(4vw, -4vh, 0)       scale(1.1); }
}

@keyframes dmDriftC {
  0%   { transform: translate3d(0, 0, 0)            scale(1); }
  50%  { transform: translate3d(-8vw, -7vh, 0)      scale(1.12); }
  100% { transform: translate3d(7vw, -3vh, 0)       scale(.96); }
}

/* ── Faisceau ──────────────────────────────────────────────────────
   Une bande de lumière traverse l'écran en diagonale toutes les ~26 s.
   C'est le seul élément du fond dont on remarque consciemment le
   mouvement : il ne doit donc pas être fréquent. À .06 d'opacité il se
   devine plus qu'il ne se voit, ce qui est exactement le but — l'écran
   respire au lieu d'être un aplat mort. */
.dm-bg-beam {
  position: absolute;
  top: -60%;
  left: -30%;
  width: 42%;
  height: 220%;
  transform-origin: center;
  background: linear-gradient(90deg,
    rgba(255, 255, 255, 0),
    rgba(190, 160, 255, .085),
    rgba(255, 255, 255, 0));
  animation: dmBeam 26s linear infinite;
}

@keyframes dmBeam {
  0%       { transform: translateX(0)    rotate(18deg); opacity: 0; }
  12%      { opacity: 1; }
  70%      { opacity: 1; }
  85%, 100%{ transform: translateX(260%) rotate(18deg); opacity: 0; }
}

/* ── Trame ─────────────────────────────────────────────────────────
   Un quadrillage très pâle, atténué vers le bas par un masque. Il donne
   au fond une structure — sans lui, les nappes flottent dans le vide et
   l'écran paraît flou. Entièrement statique : peint une fois, jamais
   recalculé. */
.dm-bg-grid {
  position: absolute;
  inset: 0;
  opacity: .5;
  background-image:
    linear-gradient(rgba(255, 255, 255, .028) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, .028) 1px, transparent 1px);
  background-size: 54px 54px;
  -webkit-mask-image: radial-gradient(120% 78% at 50% 0%, #000 0%, transparent 72%);
  mask-image: radial-gradient(120% 78% at 50% 0%, #000 0%, transparent 72%);
}

/* ── Horizon + vignette ────────────────────────────────────────────
   Lueur chaude au ras du bas de l'écran : elle prolonge visuellement la
   barre de navigation et rattache le fond à l'orange de la marque. La
   vignette assombrit les angles pour que le contenu ressorte. */
.dm-bg-horizon {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(80% 34% at 50% 106%, rgba(245, 166, 35, .16), transparent 70%),
    radial-gradient(130% 92% at 50% 42%, transparent 46%, rgba(6, 6, 12, .62) 100%);
}

/* ── Grain ─────────────────────────────────────────────────────────
   Casse l'aplat des grandes zones sombres, qui « bandent » sur les
   écrans OLED bon marché. Une tuile de 180 px suffit. */
.dm-bg-grain {
  position: absolute;
  inset: 0;
  opacity: .03;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3'/></filter><rect width='180' height='180' filter='url(%23n)'/></svg>");
}

/* ── Parallaxe ─────────────────────────────────────────────────────
   `--dm-par` est mis à jour par enhance.js dans un requestAnimationFrame.
   Le fond suit le défilement en retard, ce qui crée une profondeur que
   `background-attachment: fixed` ne donne pas (et qui, elle, force un
   repaint à chaque image sur mobile). */
/* La marge verticale doit dépasser le décalage maximal appliqué par
   enhance.js (90 px), sinon le bas de la couche se décolle du bas de
   l'écran en fin de défilement. */
.dm-bg-layer {
  position: absolute;
  inset: -14% -4%;
  transform: translate3d(0, calc(var(--dm-par, 0) * -1px), 0);
  will-change: transform;
}

/* ── Réaction à l'application ──────────────────────────────────────
   Le fond s'éclaircit brièvement quand le solde change. L'utilisateur
   perçoit l'événement du coin de l'œil, même s'il regarde ailleurs. */
@keyframes dmBgPulse {
  0%   { opacity: 0; }
  30%  { opacity: .55; }
  100% { opacity: 0; }
}

.dm-bg-flash {
  position: absolute;
  inset: 0;
  opacity: 0;
  background: radial-gradient(70% 46% at 50% 34%, rgba(245, 166, 35, .3), transparent 68%);
}
.dm-bg.dm-flash .dm-bg-flash { animation: dmBgPulse 1.1s var(--ease-out); }

/* ── Économie ──────────────────────────────────────────────────────
   Onglet en arrière-plan : plus rien ne tourne. Une animation invisible
   consomme quand même de la batterie, et c'est le genre de détail qui
   fait désinstaller une application sur un téléphone d'entrée de gamme. */
.dm-bg.is-idle *,
.dm-bg.is-idle { animation-play-state: paused !important; }

/* ── Teintes par page ──────────────────────────────────────────────
   Chaque section a sa dominante. Le changement se fait sur l'opacité de
   nappes déjà peintes : la transition est gratuite, et l'application
   donne l'impression de changer d'ambiance en même temps que de page.
   `data-page` est posé sur `body` par enhance.js. */
body {
  --dm-o-p: .85;
  --dm-o-o: .7;
  --dm-o-c: .8;
}

/* Accueil : violet et orange à parts égales, l'identité de la marque. */
body[data-page="home"]     { --dm-o-p: .9;  --dm-o-o: .85; --dm-o-c: .7; }
/* Argent : l'orange domine, comme la carte de solde. */
body[data-page="profile"],
body[data-page="history"]  { --dm-o-p: .6;  --dm-o-o: 1;   --dm-o-c: .55; }
/* Espaces d'écriture : plus sombre et plus froid, pour la concentration. */
body[data-page="sell"],
body[data-page="vendor"],
body[data-page="messages"] { --dm-o-p: .5;  --dm-o-o: .28; --dm-o-c: .95; }
/* Administration : volontairement plus sourd, on n'y vient pas flâner. */
body[data-page="admin"],
body[data-page="stats"]    { --dm-o-p: .42; --dm-o-o: .2;  --dm-o-c: .6; }

/* Le contenu passe au-dessus du fond. On ne touche pas à `display`,
   piloté en inline par app-api.js. */
#app, #auth-screen { position: relative; z-index: 1; }

/* Repli : si enhance.js n'a pas pu s'exécuter, `body` garde un dégradé
   statique plutôt qu'un aplat noir. */
body:not(.dm-bg-on) {
  background-image:
    radial-gradient(58% 44% at 12% 0%, rgba(124, 58, 237, .3), transparent 68%),
    radial-gradient(46% 36% at 92% 8%, rgba(245, 166, 35, .14), transparent 68%);
}

/* ── ÉCRAN DE DÉMARRAGE ─────────────────────────────────────────── */
/* Affiché uniquement quand un jeton est présent au chargement (classe
   posée par le script d'amorce d'index.html), et retiré par app-api.js
   dès que la session est tranchée. Il remplace le clignotement de
   l'écran de connexion par une attente qui a l'air voulue. */
#dm-splash { display: none; }

html.dm-resume #dm-splash {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 900;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  background: var(--bg);
}

html.dm-resume #auth-screen { display: none !important; }

.dm-splash-mark {
  width: 74px;
  height: 74px;
  border-radius: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 34px;
  background: linear-gradient(135deg, var(--purple), var(--purple2));
  box-shadow: 0 14px 40px var(--glow-purple);
  animation: dmFloat 2.6s ease-in-out infinite;
}

.dm-splash-name {
  font-size: 18px;
  font-weight: 800;
  letter-spacing: -.4px;
  color: var(--orange);
}

/* Barre indéterminée : elle ne ment pas sur une progression qu'on ne
   connaît pas, elle montre seulement que quelque chose travaille. */
.dm-splash-bar {
  width: 128px;
  height: 3px;
  border-radius: 3px;
  overflow: hidden;
  background: var(--card2);
}
.dm-splash-bar i {
  display: block;
  width: 40%;
  height: 100%;
  border-radius: 3px;
  background: linear-gradient(90deg, var(--purple), var(--orange));
  animation: dmSplashRun 1.1s ease-in-out infinite;
}

@keyframes dmSplashRun {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(350%); }
}

/* ── ÉCRAN DE CONNEXION ─────────────────────────────────────────── */
/* Le dégradé d'origine de #auth-screen est opaque : il masquerait
   entièrement le fond vivant. On le rend transparent — la profondeur
   vient désormais de la couche .dm-bg, commune à tout le site, ce qui
   supprime aussi la rupture visuelle au moment de la connexion. */
#auth-screen { background: transparent; }

.auth-box {
  box-shadow: var(--elev-3), 0 0 80px rgba(124, 58, 237, .14);
  backdrop-filter: blur(14px);
  background: linear-gradient(160deg, rgba(30, 30, 46, .92), rgba(22, 22, 31, .94));
  border-color: var(--hairline);
  animation: dmRise .6s var(--ease-out) both;
}

.auth-logo .logo-icon {
  box-shadow: 0 10px 30px var(--glow-purple);
  animation: dmFloat 4s ease-in-out infinite;
}

@keyframes dmFloat {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-6px); }
}

.auth-tab { transition: color .2s ease, border-color .2s ease; }

/* ── APPARITIONS ────────────────────────────────────────────────── */
@keyframes dmRise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

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

.page.active { animation: dmRise .34s var(--ease-out) both; }

/* Entrée en cascade des listes. Les 8 premiers éléments sont décalés,
   les suivants partagent le dernier délai — les grilles sont re-rendues
   intégralement à chaque filtre, l'animation doit rester courte. */
.product-card,
.history-item,
.sub-card,
.chat-item,
.stat-card {
  animation: dmRise .4s var(--ease-out) both;
}

.product-card:nth-child(1),  .history-item:nth-child(1),  .sub-card:nth-child(1)  { animation-delay: .02s; }
.product-card:nth-child(2),  .history-item:nth-child(2),  .sub-card:nth-child(2)  { animation-delay: .06s; }
.product-card:nth-child(3),  .history-item:nth-child(3),  .sub-card:nth-child(3)  { animation-delay: .10s; }
.product-card:nth-child(4),  .history-item:nth-child(4),  .sub-card:nth-child(4)  { animation-delay: .14s; }
.product-card:nth-child(5),  .history-item:nth-child(5),  .sub-card:nth-child(5)  { animation-delay: .18s; }
.product-card:nth-child(6),  .history-item:nth-child(6),  .sub-card:nth-child(6)  { animation-delay: .22s; }
.product-card:nth-child(7),  .history-item:nth-child(7),  .sub-card:nth-child(7)  { animation-delay: .26s; }
.product-card:nth-child(n+8), .history-item:nth-child(n+8), .sub-card:nth-child(n+8) { animation-delay: .30s; }

/* ── SQUELETTES DE CHARGEMENT ───────────────────────────────────── */
.sk {
  background: linear-gradient(90deg, var(--card2) 25%, rgba(255,255,255,.07) 37%, var(--card2) 63%);
  background-size: 400% 100%;
  animation: dmShimmer 1.3s ease-in-out infinite;
  border-radius: 8px;
}

@keyframes dmShimmer {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

.sk-card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 16px 14px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.sk-logo   { width: 62px; height: 62px; border-radius: 14px; }
.sk-line   { height: 11px; width: 100%; }
.sk-sm     { height: 8px; margin-top: 7px; }
.sk-btn    { height: 32px; width: 100%; border-radius: 10px; margin-top: 4px; }
.sk-block  { height: 54px; width: 100%; margin-bottom: 10px; border-radius: 14px; }

.sk-row {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 14px 16px;
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  gap: 12px;
}
.sk-avatar { width: 40px; height: 40px; border-radius: 12px; flex-shrink: 0; }

/* ── ÉTATS VIDES ────────────────────────────────────────────────── */
.empty-state {
  grid-column: 1 / -1;
  text-align: center;
  padding: 54px 24px;
  animation: dmRise .4s var(--ease-out) both;
}

.empty-icon {
  font-size: 52px;
  line-height: 1;
  margin-bottom: 16px;
  display: inline-block;
  animation: dmFloat 3.5s ease-in-out infinite;
  filter: drop-shadow(0 8px 20px rgba(0, 0, 0, .4));
}

.empty-title {
  font-size: 17px;
  font-weight: 800;
  margin-bottom: 7px;
  color: var(--text);
}

.empty-text {
  font-size: 13.5px;
  color: var(--muted);
  line-height: 1.6;
  max-width: 280px;
  margin: 0 auto;
}

.empty-action {
  margin: 20px auto 0;
  display: inline-flex;
}

/* ── CARTE SOLDE ────────────────────────────────────────────────── */
.wallet-card {
  background: linear-gradient(115deg, #f5a623 0%, #ff8c00 45%, #e67e00 70%, #ffb347 100%);
  background-size: 200% 200%;
  animation: dmGradient 12s ease infinite;
  box-shadow: 0 16px 44px rgba(245, 166, 35, .3), inset 0 1px 0 rgba(255, 255, 255, .38);
  border-radius: var(--r-lg);
}

@keyframes dmGradient {
  0%, 100% { background-position: 0% 50%; }
  50%      { background-position: 100% 50%; }
}

/* Reflet qui balaie la carte toutes les 6 s.
   Les pseudo-éléments de .wallet-card portent déjà les deux cercles
   décoratifs : on accroche donc le reflet au premier bloc interne. Comme
   ce bloc n'est pas positionné, l'absolu se cale sur .wallet-card
   (position:relative) et couvre bien toute la carte. Étant absolu, il ne
   devient pas un item du flex parent et ne déplace rien. */
.wallet-card > div:first-child::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -60%;
  width: 40%;
  pointer-events: none;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .45), transparent);
  animation: dmSheen 6s ease-in-out infinite;
}

@keyframes dmSheen {
  0%, 65%  { left: -60%; }
  100%     { left: 130%; }
}

.wallet-amount { text-shadow: 0 2px 14px rgba(0, 0, 0, .28); }
.wallet-label  { text-transform: uppercase; letter-spacing: 1.2px; font-size: 11px; opacity: .85; }

.wallet-btn {
  transition: transform .18s var(--ease-spring), box-shadow .18s ease, background .18s ease;
}
.wallet-btn:hover  { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0, 0, 0, .24); }
.wallet-btn:active { transform: scale(.96); }

.wallet-chip {
  transition: transform .2s var(--ease-spring), box-shadow .2s ease, border-color .2s ease;
  cursor: pointer;
}
.wallet-chip:hover  { transform: translateY(-1px); border-color: var(--orange); box-shadow: 0 6px 18px var(--glow-orange); }
.wallet-chip:active { transform: scale(.96); }

/* ── CARTES PRODUIT ─────────────────────────────────────────────── */
.product-card {
  background:
    linear-gradient(180deg, var(--surface-1), transparent 42%),
    var(--card);
  border-color: var(--hairline);
  box-shadow: var(--elev-1);
  transition:
    transform .28s var(--ease-out),
    box-shadow .28s ease,
    border-color .28s ease;
}

.product-card:hover {
  transform: translateY(-5px);
  box-shadow: var(--elev-2), 0 0 0 1px rgba(245, 166, 35, .35), 0 0 28px rgba(245, 166, 35, .12);
}

.product-card:active { transform: translateY(-1px) scale(.99); }

/* Liseré lumineux en haut de la carte au survol. */
.product-card::after {
  content: '';
  position: absolute;
  top: 0;
  left: 12%;
  right: 12%;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, var(--orange), transparent);
  opacity: 0;
  transition: opacity .28s ease;
}
.product-card:hover::after { opacity: .9; }

.product-name  { letter-spacing: -.2px; }
.product-price { font-weight: 800; }

.product-buy-btn { transition: filter .18s ease, transform .18s var(--ease-spring); }
.product-card:hover .product-buy-btn { filter: brightness(1.1); }

.fav-btn { transition: transform .22s var(--ease-spring); }
.fav-btn:active { transform: scale(1.35); }

/* ── BADGES « VIVANTS » ─────────────────────────────────────────── */
.badge-hot {
  box-shadow: 0 0 0 0 rgba(239, 68, 68, .55);
  animation: dmPulseRed 2.4s ease-out infinite;
}

@keyframes dmPulseRed {
  0%   { box-shadow: 0 0 0 0 rgba(239, 68, 68, .5); }
  70%  { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
  100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
}

.badge-new {
  box-shadow: 0 0 0 0 rgba(34, 197, 94, .5);
  animation: dmPulseGreen 2.4s ease-out infinite .6s;
}

@keyframes dmPulseGreen {
  0%   { box-shadow: 0 0 0 0 rgba(34, 197, 94, .5); }
  70%  { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
  100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); }
}

/* ── BOUTONS ────────────────────────────────────────────────────── */
.btn {
  position: relative;
  overflow: hidden;
  transition:
    transform .18s var(--ease-spring),
    box-shadow .22s ease,
    filter .18s ease,
    border-color .18s ease,
    background .18s ease;
}

.btn:active { transform: scale(.97); }

/* Onde au clic, injectée par enhance.js. */
.dm-ripple {
  position: absolute;
  border-radius: 50%;
  transform: scale(0);
  background: rgba(255, 255, 255, .38);
  pointer-events: none;
  animation: dmRipple .55s var(--ease-out);
}

@keyframes dmRipple {
  to { transform: scale(2.6); opacity: 0; }
}

.btn-orange  { box-shadow: 0 6px 18px rgba(245, 166, 35, .3); }
.btn-orange:hover {
  opacity: 1;
  filter: brightness(1.08);
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(245, 166, 35, .42);
}

.btn-primary { box-shadow: 0 6px 18px var(--glow-purple); }
.btn-primary:hover {
  opacity: 1;
  filter: brightness(1.1);
  transform: translateY(-2px);
  box-shadow: 0 10px 26px rgba(124, 58, 237, .5);
}

.btn-green:hover { filter: brightness(1.1); transform: translateY(-2px); box-shadow: 0 10px 24px rgba(34, 197, 94, .35); }
.btn-red:hover   { filter: brightness(1.1); transform: translateY(-2px); box-shadow: 0 10px 24px rgba(239, 68, 68, .35); }

.btn-outline:hover {
  border-color: var(--purple);
  background: rgba(124, 58, 237, .1);
  transform: translateY(-1px);
}

/* Bouton occupé : évite les doubles envois perçus comme « rien ne se passe ». */
.btn[disabled], .btn.is-busy {
  opacity: .6;
  pointer-events: none;
  filter: grayscale(.3);
}

/* Accessibilité : le focus clavier redevient visible. */
.btn:focus-visible,
.cat-tab:focus-visible,
.wallet-btn:focus-visible,
.bottom-nav-item:focus-visible,
.nav-item:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: none;
  box-shadow: var(--ring);
}

/* Cibles tactiles : sous 44 px, un doigt rate la cible une fois sur cinq. */
.btn-sm, .cat-tab, .fav-btn, .cred-copy { min-height: 36px; }
.bottom-nav-item { min-height: 52px; }

/* ── ONGLETS CATÉGORIES ─────────────────────────────────────────── */
.cat-tab {
  transition: transform .2s var(--ease-spring), background .2s ease, color .2s ease, box-shadow .2s ease;
}
.cat-tab:hover  { transform: translateY(-2px); border-color: var(--orange); color: var(--text); }
.cat-tab:active { transform: scale(.95); }
.cat-tab.active {
  background: linear-gradient(135deg, var(--orange), var(--orange2));
  box-shadow: 0 6px 18px var(--glow-orange);
}

/* Dégradé de bord : indique qu'il reste des catégories à faire défiler. */
.cat-tabs {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 14px, #000 calc(100% - 24px), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 14px, #000 calc(100% - 24px), transparent 100%);
}

/* ── NAVIGATION ─────────────────────────────────────────────────── */
.bottom-nav {
  background: rgba(22, 22, 31, .84);
  backdrop-filter: blur(18px);
  border-top-color: var(--hairline);
  box-shadow: 0 -8px 28px rgba(0, 0, 0, .35);
}

.bottom-nav-item { position: relative; }

.bottom-nav-item .nav-icon { transition: transform .28s var(--ease-spring); }
.bottom-nav-item:active .nav-icon  { transform: scale(.88); }
.bottom-nav-item.active .nav-icon  { transform: translateY(-2px) scale(1.12); }

/* Pastille lumineuse sous l'onglet actif. */
.bottom-nav-item.active::after {
  content: '';
  position: absolute;
  top: 4px;
  left: 50%;
  width: 26px;
  height: 26px;
  margin-left: -13px;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(circle, var(--glow-orange), transparent 70%);
  animation: dmFade .3s ease both;
}

.topbar {
  background: rgba(14, 14, 20, .8);
  backdrop-filter: blur(16px);
  border-bottom-color: var(--hairline);
}

.topbar-title span {
  background: linear-gradient(135deg, var(--orange), #ffd479, var(--orange2));
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: dmGradient 6s ease infinite;
}

.menu-btn { transition: transform .2s var(--ease-spring), border-color .2s ease; }
.menu-btn:hover  { transform: rotate(-6deg) scale(1.06); border-color: var(--purple); }
.menu-btn:active { transform: scale(.92); }

.sidebar { box-shadow: var(--elev-3); }
.nav-item { transition: background .18s ease, padding-left .18s var(--ease-out); }
.nav-item:hover { background: var(--surface-1); padding-left: 26px; }

/* ── LISTES ─────────────────────────────────────────────────────── */
.history-item,
.chat-item,
.sub-card,
.stat-card,
.admin-card,
.dispute-card,
.support-ticket,
.deposit-row {
  border-color: var(--hairline);
  transition: transform .22s var(--ease-out), box-shadow .22s ease, border-color .22s ease;
}

.history-item:hover,
.chat-item:hover,
.sub-card:hover,
.dispute-card:hover,
.support-ticket:hover {
  transform: translateX(3px);
  border-color: rgba(124, 58, 237, .55);
  box-shadow: var(--elev-1);
}

.stat-card {
  background: linear-gradient(160deg, var(--surface-1), transparent 60%), var(--card);
}
.stat-card:hover {
  transform: translateY(-3px);
  border-color: rgba(124, 58, 237, .5);
  box-shadow: var(--elev-1);
}

/* ── MODALES ────────────────────────────────────────────────────── */
.modal {
  animation: dmModalUp .42s var(--ease-out);
  box-shadow: 0 -12px 48px rgba(0, 0, 0, .55);
  border-top: 1px solid var(--hairline);
}

@keyframes dmModalUp {
  from { transform: translateY(100%); opacity: .6; }
  to   { transform: translateY(0); opacity: 1; }
}

.modal-overlay { backdrop-filter: blur(10px); }
.modal-handle { transition: background .2s ease, width .2s ease; }
.modal:hover .modal-handle { background: var(--muted); width: 52px; }

/* ── TOASTS ─────────────────────────────────────────────────────── */
.toast {
  backdrop-filter: blur(16px);
  background: rgba(30, 30, 46, .95);
  border-color: var(--hairline);
  box-shadow: var(--elev-3);
  transition: transform .38s var(--ease-spring), opacity .25s ease;
}
.toast.success { box-shadow: var(--elev-2), 0 0 30px rgba(34, 197, 94, .2); }
.toast.error   { box-shadow: var(--elev-2), 0 0 30px rgba(239, 68, 68, .2); }
.toast.info    { box-shadow: var(--elev-2), 0 0 30px rgba(245, 166, 35, .2); }

/* ── FORMULAIRES ────────────────────────────────────────────────── */
input, select, textarea {
  transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
}
input:focus, select:focus, textarea:focus {
  border-color: var(--purple) !important;
}

/* Sur iOS, une police de moins de 16 px déclenche un zoom automatique
   à la mise au point du champ. */
input, select, textarea { font-size: max(16px, 1em); }

/* ── IDENTIFIANTS LIVRÉS ────────────────────────────────────────── */
.creds-box {
  box-shadow: 0 0 0 1px rgba(34, 197, 94, .25), 0 12px 34px rgba(34, 197, 94, .12);
  animation: dmRise .45s var(--ease-out) both;
}

.cred-row { align-items: center; gap: 8px; }

.cred-val {
  user-select: all;
  -webkit-user-select: all;
  word-break: break-all;
}

.cred-copy {
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  border-radius: 9px;
  color: var(--text);
  cursor: pointer;
  font-size: 14px;
  padding: 6px 9px;
  flex-shrink: 0;
  transition: transform .18s var(--ease-spring), background .18s ease, border-color .18s ease;
}
.cred-copy:hover  { background: rgba(124, 58, 237, .25); border-color: var(--purple); }
.cred-copy:active { transform: scale(.9); }
.cred-copy.copied { background: rgba(34, 197, 94, .22); border-color: var(--green); }

.creds-copy-all { width: 100%; margin-top: 14px; }
.creds-copy-all.copied { border-color: var(--green); color: var(--green); }

/* ── BANDEAU HORS LIGNE (injecté par enhance.js) ────────────────── */
.dm-offline {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  z-index: 998;
  padding: 9px 16px;
  text-align: center;
  font-size: 13px;
  font-weight: 700;
  color: #fff;
  background: linear-gradient(135deg, #b91c1c, #ef4444);
  transform: translateY(-100%);
  transition: transform .35s var(--ease-out);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .35);
}
.dm-offline.show { transform: translateY(0); }

/* ── INVITATION À INSTALLER (injectée par enhance.js) ───────────── */
.dm-install {
  position: fixed;
  left: 14px;
  right: 14px;
  bottom: calc(76px + env(safe-area-inset-bottom));
  z-index: 190;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  border-radius: var(--r-md);
  background: linear-gradient(150deg, rgba(40, 34, 62, .97), rgba(24, 22, 34, .97));
  border: 1px solid var(--hairline);
  backdrop-filter: blur(16px);
  box-shadow: var(--elev-3);
  transform: translateY(140%);
  transition: transform .45s var(--ease-out);
}
.dm-install.show { transform: translateY(0); }
.dm-install-icon { font-size: 30px; line-height: 1; }
.dm-install-txt  { flex: 1; min-width: 0; }
.dm-install-t    { font-weight: 800; font-size: 14px; }
.dm-install-s    { font-size: 12px; color: var(--muted); margin-top: 2px; }
.dm-install-x {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 20px;
  cursor: pointer;
  padding: 4px 6px;
  line-height: 1;
}

/* ── BARRES DE PROGRESSION ──────────────────────────────────────── */
.vip-progress, .sub-progress-bar {
  transition: width .8s var(--ease-out);
  box-shadow: 0 0 12px var(--glow-orange);
}

/* ── ICÔNES SVG ─────────────────────────────────────────────────────
   Les icônes héritent de la couleur du texte parent (`currentColor`) :
   c'est tout l'intérêt par rapport à un emoji, qui garde ses propres
   couleurs quoi qu'il arrive. L'onglet actif devient réellement orange. */
.ico {
  display: inline-block;
  vertical-align: -.18em;
  flex-shrink: 0;
  color: inherit;
}

/* Repli quand l'administrateur ajoute une catégorie inconnue : l'emoji
   qu'il a saisi reste affiché, à la bonne taille. */
.ico-emoji {
  display: inline-block;
  font-size: 1.05em;
  line-height: 1;
  vertical-align: -.1em;
}

.nav-icon .ico { vertical-align: middle; }

.bottom-nav-item .ico {
  transition: transform .28s var(--ease-spring), stroke-width .2s ease;
}
.bottom-nav-item.active .ico { stroke-width: 2.2; }

.nav-item .ico { margin-right: 2px; opacity: .9; }
.nav-item:hover .ico { opacity: 1; }

.wallet-btn .ico { vertical-align: -.2em; }

.history-icon .ico,
.pay-icon .ico,
.pay-icon-sm .ico { vertical-align: middle; }

.pay-icon-sm {
  width: 42px; height: 42px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 12px;
  background: var(--surface-2);
  border: 1px solid var(--hairline);
  flex-shrink: 0;
}

.product-badge .ico { vertical-align: -.15em; margin-right: 1px; }

.cat-tab .ico { vertical-align: -.22em; margin-right: 3px; }

/* États vides : l'icône reprend la teinte de la marque au lieu d'un
   emoji multicolore qui jure avec le reste. */
.empty-icon .ico {
  color: var(--purple);
  opacity: .85;
  filter: drop-shadow(0 6px 18px rgba(124, 58, 237, .35));
}
.empty-icon.error .ico {
  color: var(--red);
  filter: drop-shadow(0 6px 18px rgba(239, 68, 68, .3));
}

.support-fab .ico { vertical-align: middle; }

/* ── SUPPORT ────────────────────────────────────────────────────── */
/* Les réponses automatiques sont visuellement distinctes des réponses
   humaines : l'utilisateur doit savoir à qui il parle. */
.support-msg.auto {
  border-left: 3px solid var(--purple);
  background: linear-gradient(100deg, rgba(124, 58, 237, .14), var(--card2));
}

.s-auto {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .6px;
  text-transform: uppercase;
  color: var(--purple);
  margin-bottom: 6px;
  opacity: .95;
}

.support-msg { line-height: 1.55; }

.quick-replies { animation: dmRise .35s var(--ease-out) both; }

.quick-chip {
  transition: transform .18s var(--ease-spring), background .18s ease, border-color .18s ease;
}
.quick-chip:hover  { transform: translateY(-2px); }
.quick-chip:active { transform: scale(.95); }

.support-fab {
  box-shadow: 0 10px 30px var(--glow-purple);
  transition: transform .24s var(--ease-spring), box-shadow .24s ease;
}
.support-fab:hover  { transform: scale(1.08) rotate(-6deg); box-shadow: 0 14px 38px rgba(124, 58, 237, .55); }
.support-fab:active { transform: scale(.94); }

/* ── ASCENSEURS & SÉLECTION ─────────────────────────────────────── */
::selection { background: rgba(245, 166, 35, .35); color: #fff; }

* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: linear-gradient(var(--purple), var(--purple2));
  border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover { background: var(--orange); }

/* Rien ne doit jamais déborder horizontalement sur mobile. */
html, body { overflow-x: hidden; max-width: 100%; }

/* ── SOLDE ANIMÉ (piloté par enhance.js) ────────────────────────── */
.dm-count-flash { animation: dmCountFlash .7s ease-out; }

@keyframes dmCountFlash {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.07); text-shadow: 0 0 22px rgba(255, 255, 255, .55); }
  100% { transform: scale(1); }
}

/* ── ÉCRANS LARGES ──────────────────────────────────────────────── */
@media (min-width: 900px) {
  .products-grid { grid-template-columns: repeat(4, 1fr); }
  #app > .page { max-width: 1100px; margin: 0 auto; }
  .topbar { padding-left: 6vw; padding-right: 6vw; }
  .dm-install { left: auto; right: 20px; width: 380px; bottom: 20px; }
}

@media (min-width: 600px) and (max-width: 899px) {
  .products-grid { grid-template-columns: repeat(3, 1fr); }
}

/* ── ACCESSIBILITÉ : MOUVEMENT RÉDUIT ───────────────────────────── */
/* Si l'utilisateur a désactivé les animations dans son système,
   tout redevient statique — y compris les animations préexistantes. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}
