/* shop.css — thème de la boutique.
   Volontairement autonome : la boutique ne dépend d'aucune feuille de l'ERP, pour
   pouvoir évoluer sans risquer de casser le back-office.

   ⭐⭐⭐ RTL : LA RÈGLE DE CETTE FEUILLE.
   La boutique s'affiche en français (LTR) ou en arabe (RTL), et `dir="rtl"` est posé
   sur `<html>` par le serveur (lib/i18n.js). Aucun côté n'est donc écrit en dur :
   - `text-align: start / end` remplace `left / right` ;
   - `margin-inline-start`, `padding-inline-start`, `border-inline-end`… remplacent
     `margin-left`, `padding-left`, `border-right`… ;
   - `inset-inline-start / end` remplacent `left / right` sur les éléments positionnés.
   La MÊME règle place alors correctement les deux langues, et un écran ajouté plus
   tard n'a rien à prévoir : ce qui est logique est déjà juste.
   ⚠ Exceptions assumées, marquées `NON LOGIQUE` : ce qui doit rester physiquement à
   gauche ou à droite quel que soit le sens de lecture (centrage du toast). */

/* ⓘ LA POLICE DU SITE N'EST PAS DÉCLARÉE ICI : elle vient du `<link>` posé dans
   `views/layouts/main.hbs` (CDN Google Fonts, demandé par le NAVIGATEUR du visiteur — le
   serveur de la boutique n'a aucun accès internet sortant), et la famille est appliquée par
   la variable `--shop-font` ci-dessous. ⚠ AUCUNE RÈGLE DE CETTE FEUILLE NE FIXE DE
   `font-family` : ce qui doit changer de corps change de TAILLE ou de GRAISSE, et **hérite**
   de la police du site. Une règle qui reposerait une famille ferait diverger une brique de la
   page des autres, et la police demandée par le client ne s'appliquerait plus partout. */

:root {
  --shop-accent: #e8590c;      /* orange : nom produit, boutons */
  --shop-accent-dark: #c2410c;
  --shop-navy: #1e293b;
  --shop-border: #e2e8f0;
  --shop-muted: #64748b;
  --shop-bg: #f8fafc;
  /* ⭐⭐⭐ LES ARRONDIS, EN UN SEUL ENDROIT (demande du client : « unify edge roundness for all
     elements »). La feuille portait SEPT valeurs écrites à la main — 2, 4, 6, 8, 9, 10, 14 px —
     posées au fil des demandes : deux cartes voisines n'avaient donc pas le même coin, et un
     composant ajouté plus tard héritait de la valeur du jour. Trois jetons suffisent, et chacun
     dit À QUOI il sert :
       · `sm`   — ce qui est IMBRIQUÉ dans autre chose (champ de saisie, miniature, petite pastille) ;
       · `base` — le défaut : cartes, boutons, encadrés, fenêtres ;
       · `pill` — ce qui doit être entièrement rond (badge de compteur, puce, barre de progression).
     ⚠ `--shop-radius` GARDE SON NOM ET SA VALEUR (6 px) : elle est utilisée 31 fois, et la
     renommer ferait un diff de 31 lignes pour aucun gain visuel.
     ⚠⚠ `soft` — LA SEULE EXCEPTION ASSUMÉE À L'UNIFICATION (9 px) : les PILULES de quantité et les
     deux boutons d'unité qui les remplacent. Le client les a demandées AVANT (« make the pill less
     round, soft corners, slim pills ») et « 9 px » est la valeur de la maquette : tout ramener à
     6 px aurait donc défait une demande antérieure SANS LE DIRE. Deux contrôleurs mesurent cette
     valeur — `npm run check:shop` (la feuille) et `npm run check:qty` (le rayon CALCULÉ dans Chrome)
     — et c'est leur échec qui a fait ressortir la régression. Ce jeton garde la valeur demandée, et
     son nom dit pourquoi il existe. */
  --shop-radius-sm: 4px;
  --shop-radius: 6px;
  --shop-radius-soft: 9px;
  --shop-radius-pill: 999px;
  /* ⭐ « NOTO KUFI ARABIC » PARTOUT (demande du client), POUR LES DEUX LANGUES.
     ⚠ POURQUOI PAR LE CDN DE GOOGLE ET NON PAR UN FICHIER MAISON. Le serveur de la boutique
     n'a pas d'accès internet sortant : télécharger les `.woff2` pour les héberger ici est
     impossible depuis cette machine (`https://fonts.googleapis.com` est injoignable). La
     feuille est donc demandée par le NAVIGATEUR du visiteur, qui, lui, a internet — c'est le
     seul chemin possible. Le `@font-face` reste déclaré par Google, et la famille est posée
     ici, en un seul endroit (`--shop-font`).
     ⚠ CE QUE CELA COÛTE, ET QUI DOIT ÊTRE DIT : une requête vers un domaine tiers (Google voit
     l'adresse IP du visiteur), et une police qui n'arrive pas si le visiteur est hors ligne —
     d'où la PILE DE REPLI, qui rend la page correctement dans tous les cas (aucun texte
     invisible : `font-display: swap` est demandé dans l'URL). */
  --shop-font: 'Noto Kufi Arabic', Arial, Helvetica, sans-serif;
}

* { box-sizing: border-box; }
body.shop {
  margin: 0;
  font: 14px/1.5 var(--shop-font);
  color: var(--shop-navy);
  background: #fff;
}
/* ⚠ L'ARABE GARDE LA MÊME POLICE (c'est justement celle qui lui convient), mais un peu plus
   d'air entre les lignes : les ascendantes et descendantes arabes se touchent à 1.5. */
html[dir="rtl"] body.shop {
  font-family: var(--shop-font);
  line-height: 1.7;
}
a { color: inherit; text-decoration: none; }
a:hover { text-decoration: underline; }
img { max-width: 100%; display: block; }

/* ⭐ Montants, références, dates, téléphones : des ÎLOTS LATINS dans une page arabe.
   Les isoler en LTR (attribut posé par l'aide de mise en forme des montants et par les
   gabarits) évite que l'algorithme bidirectionnel ne sépare « 1 234,50 » de « DH ».
   ⓘ Les accolades sont évitées dans les commentaires de cette feuille : plusieurs contrôles
   du dépôt découpent les règles dessus, et une accolade citée dans une phrase les fait
   associer un faux sélecteur à un vrai corps de règle (voir aussi plus bas). */
.num { unicode-bidi: isolate; }

/* ⭐ LES PICTOGRAMMES (partials/icon.hbs). En ligne, donc ils suivent `currentColor` : une
   icône s'allume et s'éteint avec le texte de son bouton, dans les deux sens de lecture et
   sans requête réseau. `display: block` : un `<svg>` inline est un élément en ligne, posé sur
   la ligne de base — sans ce réglage il « flotte » et décale le mot qu'il accompagne. */
.shop-icon { display: block; flex: 0 0 auto; }

/* ---------------------------------------------------------------- en-tête */
.shop-header { border-bottom: 1px solid var(--shop-border); background: #fff; position: sticky; top: 0; z-index: 20; }
.shop-header-inner { max-width: 1280px; margin: 0 auto; padding: 10px 16px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
/* ⭐ LA MARQUE ET LA LANGUE, CÔTE À CÔTE DANS LE DOM (voir le gabarit) — mais pas forcément
   côte à côte À L'ÉCRAN : sur grand écran, cette boîte s'efface (`display: contents`) et ses
   deux enfants redeviennent des éléments de l'en-tête, dans l'ordre habituel
   marque · recherche · liens · langue. Sur téléphone, elle reprend son rôle de ligne (voir le
   bloc mobile) : c'est le seul moyen d'avoir la langue TOUJOURS VISIBLE quand l'en-tête
   s'empile, sans dupliquer le sélecteur ni changer la disposition du bureau. */
.shop-header-top { display: contents; }
/* ⭐ MÊME PRINCIPE POUR LA LIGNE DE RECHERCHE : cette boîte n'existe QUE sur téléphone, où
   elle forme la LIGNE 2 de l'en-tête (recherche + langue de dictée). Sur grand écran elle
   s'efface, et `.shop-search-wrap` redevient l'enfant direct de l'en-tête — donc
   `flex: 1 1 260px`, la disposition d'origine, inchangée. Sans cette boîte, on ne pouvait pas
   dire « ces deux contrôles sont une ligne » sans déplacer le DOM ni dupliquer le partiel. */
.shop-header-search { display: contents; }
/* ⭐⭐ LA MARQUE EST LE LOGO (demande du client) : plus aucun texte dans l'en-tête.
   ⚠ LA HAUTEUR EST IMPOSÉE ICI, LA LARGEUR SUIT (`width: auto`) : le logo fourni mesure
   1000 × 807, donc presque carré — une largeur fixe l'écraserait ou le déformerait, alors que
   `height` + `width: auto` conserve le rapport d'aspect quel que soit le fichier fourni demain.
   ⚠ `height: 40px` SUR GRAND ÉCRAN, ET C'EST UNE TAILLE D'EN-TÊTE, PAS DE LOGO : au-delà, la
   ligne de 44 px des champs de recherche serait poussée vers le bas. Sur téléphone il descend à
   30 px (voir le bloc mobile) : la place y est comptée, et le logo n'a pas à dominer un écran
   de 375 px. */
.shop-brand { display: inline-flex; align-items: center; }
.shop-brand-logo { height: 40px; width: auto; display: block; }
/* ---------------------------------------------------------------- recherche
   ⭐ L'EN-TÊTE PORTE LA RECHERCHE, DONC LE PANNEAU DE SUGGESTIONS AUSSI.
   `.shop-search-wrap` est le seul élément POSITIONNÉ de ce bloc : le panneau s'y accroche
   (`inset-inline: 0` + `top: 100%`), il prend donc exactement la largeur du champ, où que
   la barre se trouve — en-tête sur une ligne, ou pleine largeur sur un téléphone. */
.shop-search-wrap { position: relative; flex: 1 1 260px; min-width: 200px; }
.shop-search { display: flex; gap: 0; }

/* ⭐ 44 px DE HAUT : c'est la cible du doigt, et la barre de recherche est l'élément le
   plus touché d'une boutique sur téléphone. La largeur, elle, reste SOUPLE (`flex`, pas
   de largeur en dur) : la même règle sert un téléphone de 375 px et un écran de bureau. */
.shop-search-input {
  flex: 1 1 auto; min-width: 0; min-height: 44px;
  padding: 7px 10px; border: 1px solid var(--shop-border); border-inline-end: 0;
  border-radius: var(--shop-radius);
  /* Le champ et ce qui le SUIT forment un seul bloc visuel : les coins du milieu restent
     droits, que le bouton micro soit affiché ou non. Les coins LOGIQUES suivent le sens
     de lecture (à droite en LTR, à gauche en RTL). */
  border-start-end-radius: 0; border-end-end-radius: 0;
  font-size: 14px;
}
.shop-search-submit {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 7px 14px; min-height: 44px; border: 1px solid var(--shop-navy);
  background: var(--shop-navy); color: #fff; white-space: nowrap;
  border-radius: var(--shop-radius);
  border-start-start-radius: 0; border-end-start-radius: 0;
  cursor: pointer;
}
/* ⭐ UNE LOUPE SUR TÉLÉPHONE, LE MOT « RECHERCHER » SUR GRAND ÉCRAN.
   POURQUOI CE DÉDOUBLEMENT : sur 375 px, « champ + micro + Rechercher » fait 347 px à lui
   seul ; la langue de dictée demandée sur LA MÊME LIGNE (voir le bloc mobile) ne tenait donc
   pas. Le pictogramme rend la place du libellé, et le nom accessible reste entier : le
   partiel pose un `aria-label` sur le bouton — un lecteur d'écran annonce « Rechercher »,
   jamais « image ». Sur grand écran, rien ne change : le mot est là, la loupe non. */
.shop-search-submit-icon { display: none; }

/* ⭐ LE MICRO EST MASQUÉ TANT QUE LE NAVIGATEUR NE SAIT PAS RECONNAÎTRE LA PAROLE : le
   serveur le rend avec `hidden`, et shop.js le révèle seulement s'il trouve
   `SpeechRecognition`. Un bouton qui ne peut rien faire ne doit jamais apparaître.
   Il se place au BOUT du champ — à droite en français, à gauche en arabe : `border-inline`
   suit le sens de lecture, comme tout le reste de cette feuille. */
.shop-mic {
  display: flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px; padding: 0 10px;
  border: 1px solid var(--shop-border); border-inline-start: 0; border-inline-end: 0;
  background: #fff; color: var(--shop-muted); cursor: pointer;
}
.shop-mic[hidden] { display: none; }
.shop-mic svg { display: block; }
.shop-mic:hover { color: var(--shop-accent); }
.shop-mic:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: -2px; }
/* ⭐ « J'ÉCOUTE » SE VOIT : un micro actif qui ne change pas d'aspect laisse croire que
   l'appui n'a pas été pris en compte, et le client recommence — ou parle trop tard. */
.shop-mic.is-listening { background: var(--shop-accent); border-color: var(--shop-accent); color: #fff; }

/* Langue de la dictée + état du micro : révélés en même temps que le bouton (donc jamais
   visibles sans API de reconnaissance). Sur grand écran, le sélecteur reste une ligne
   discrète sous le champ ; sur téléphone il monte À CÔTÉ du champ (voir le bloc mobile) —
   c'est la « ligne 2 » demandée : barre de recherche + `|FR|AR|`. */
.shop-voice { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 4px; font-size: 12px; color: var(--shop-muted); }
.shop-voice[hidden] { display: none; }

/* ⭐⭐ `|FR|AR|` — UN SÉLECTEUR SEGMENTÉ, ET DE VRAIS BOUTONS.
   Deux cibles, une seule allumée (`aria-pressed="true"` → fond plein) : la langue de dictée
   se lit d'un coup d'œil, et il n'y a rien à dérouler — un geste au lieu de deux. Les deux
   segments partagent une même bordure (le séparateur du milieu n'est posé qu'une fois) pour
   se lire comme UN contrôle, et non comme deux boutons voisins. */
.shop-voice-lang {
  display: inline-flex; align-items: stretch;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius); overflow: hidden; background: #fff;
}
.shop-voice-lang-btn {
  min-width: 38px; min-height: 30px; padding: 0 8px;
  border: 0; background: #fff; cursor: pointer;
  font: inherit; font-size: 11px; font-weight: 700; letter-spacing: .4px;
  color: var(--shop-muted);
  /* ⚠ `unicode-bidi: isolate` : « FR » et « AR » sont des ÎLOTS LATINS dans une page arabe.
     Sans isolation, l'algorithme bidirectionnel peut les recoller dans le mauvais ordre. */
  unicode-bidi: isolate;
}
.shop-voice-lang-btn + .shop-voice-lang-btn { border-inline-start: 1px solid var(--shop-border); }
.shop-voice-lang-btn:hover { color: var(--shop-navy); }
.shop-voice-lang-btn:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: -2px; }
.shop-voice-lang-btn[aria-pressed="true"] { background: var(--shop-navy); color: #fff; }
.shop-voice-status { margin: 0; flex: 1 1 100%; }
.shop-voice-status[hidden] { display: none; }
.shop-voice-status[data-state="error"] { color: #b91c1c; }

/* ---------------------------------------------------------------- suggestions
   ⭐ LE PANNEAU NE DOIT JAMAIS DÉPASSER L'ÉCRAN. En 375 × 667 (le plus petit téléphone
   courant), il occupe la largeur du champ et se limite à 60 % de la hauteur de fenêtre :
   au-delà, il défile. `inset-inline: 0` le place des deux côtés de la même façon en
   français et en arabe, sans jamais écrire `left` ni `right`. */
.shop-suggest {
  position: absolute; inset-inline: 0; top: 100%; z-index: 30;
  margin: 4px 0 0; padding: 0; list-style: none;
  background: #fff; border: 1px solid var(--shop-border); border-radius: var(--shop-radius);
  box-shadow: 0 8px 24px rgba(15, 23, 42, .12);
  max-height: min(60vh, 420px); overflow-y: auto; overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
}
.shop-suggest[hidden] { display: none; }

/* ⭐ LA LIGNE EST UNE RANGÉE : le lien prend la place disponible, le bouton d'AJOUT RAPIDE
   garde sa largeur au bout. C'est ce qui permet d'ajouter un article au panier sans
   ouvrir sa fiche — le geste le plus fréquent d'une boutique, et celui qui coûtait le
   plus d'allers-retours.
   ⚠ `align-items: stretch` : le bouton fait la HAUTEUR de la ligne (44 px), pas 20 px. */
.shop-suggest-row { min-height: 44px; border-bottom: 1px solid var(--shop-border); display: flex; align-items: stretch; }
.shop-suggest-row:last-child { border-bottom: 0; }
.shop-suggest-link {
  display: flex; align-items: center; gap: 10px;
  min-height: 44px; padding: 6px 10px;
  /* ⚠ `overflow: hidden` ICI, ET `min-width: 0` SURTOUT ENFANT FLEXIBLE : sans eux, une
     longue désignation refuse de se réduire et pousse la ligne — puis la page — hors de
     l'écran. C'est LE défaut classique du débordement horizontal sur mobile.
     ⚠ `flex: 1 1 auto` EST LA CONTREPARTIE DU BOUTON À CÔTÉ : sans lui, le lien se
     réduirait au profit du bouton, et la désignation perdrait la place qu'elle occupe
     aujourd'hui. */
  flex: 1 1 auto; min-width: 0;
  overflow: hidden;
}
.shop-suggest-link:hover, .shop-suggest-row.is-active .shop-suggest-link { background: var(--shop-bg); text-decoration: none; }
/* La ligne surlignée au clavier (↑ ↓) doit se VOIR : le fond seul est trop discret. */
.shop-suggest-row.is-active .shop-suggest-link { outline: 2px solid var(--shop-accent); outline-offset: -2px; }

/* ⚠ 44 px : c'est la CIBLE DU DOIGT, comme le champ de recherche et le bouton micro. Un
   « + » de 20 px de large se rate une fois sur deux, et l'ajout rapide devient plus lent
   que d'ouvrir la fiche — l'inverse du but. */
.shop-suggest-add {
  flex: 0 0 auto; width: 44px; min-height: 44px;
  border: 0; border-inline-start: 1px solid var(--shop-border);
  background: #fff; color: var(--shop-accent);
  font-size: 20px; font-weight: 700; line-height: 1; cursor: pointer;
}
.shop-suggest-add:hover { background: #fff7ed; }
.shop-suggest-add:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: -2px; }
/* ⚠ L'ÉTAT « EN COURS » SE VOIT : un second clic pendant la requête ferait un second
   ajout. Le bouton est désactivé par le script, encore faut-il que ça se voie. */
.shop-suggest-add[disabled] { color: #cbd5e1; cursor: progress; }

/* Vignette : 48 × 48 comme la vignette d'un résultat de recherche, et c'est la seule
   dimension fixe de ce bloc — la taille d'une image d'aperçu, pas une mise en page. */
.shop-suggest-thumb {
  flex: 0 0 auto; width: 48px; height: 48px;
  display: flex; align-items: center; justify-content: center;
  background: var(--shop-bg); border: 1px solid var(--shop-border); border-radius: var(--shop-radius-sm);
  overflow: hidden;
}
.shop-suggest-thumb img { width: 48px; height: 48px; object-fit: contain; }
/* Repli DISCRET quand l'article n'a pas de photo : l'initiale de la désignation. */
.shop-suggest-thumb.is-empty { color: #94a3b8; font-size: 18px; font-weight: 700; text-transform: uppercase; }

.shop-suggest-text { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* `text-align: start` : le texte commence du bon côté dans les deux langues. Et le
   débordement se règle par des POINTS DE SUSPENSION, jamais par un décalage de la ligne. */
.shop-suggest-name {
  font-size: 13px; font-weight: 600; color: var(--shop-accent); text-align: start;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* ⭐⭐ LES TITRES TROP LONGS DÉFILENT (demande du client).
   ⚠ LA FENÊTRE ET LE TEXTE SONT DEUX ÉLÉMENTS. `.shop-suggest-name` est la fenêtre : elle garde sa
   largeur, tronque, et ne bouge JAMAIS. `.shop-suggest-name-inner` porte le texte et c'est LUI qui
   glisse. Animer directement la fenêtre déplacerait la boîte (donc la ligne entière) au lieu de
   faire défiler le texte à l'intérieur — c'est la raison d'être de l'enveloppe.
   ⚠ LA DISTANCE ET LA DURÉE VIENNENT DU SCRIPT (`--suggest-glissement`, `--suggest-duree`), qui les
   a MESURÉES : la distance est exactement ce qui manque pour lire la fin, et la durée suit la
   distance pour que la vitesse reste la même quel que soit le titre.
   ⚠ `alternate` : le texte revient au début au lieu de sauter d'un coup — un saut donnerait
   l'impression que le titre a changé. Et l'animation se MET EN PAUSE au survol : on ne fait pas
   défiler un texte que le client est en train de lire. */
.shop-suggest-name.is-defilant { text-overflow: clip; }
.shop-suggest-name-inner { display: inline-block; white-space: nowrap; }
.shop-suggest-name.is-defilant .shop-suggest-name-inner {
  animation: shop-suggest-defile var(--suggest-duree, 6s) ease-in-out infinite alternate;
}
.shop-suggest-row:hover .shop-suggest-name.is-defilant .shop-suggest-name-inner { animation-play-state: paused; }
@keyframes shop-suggest-defile {
  from { transform: translateX(0); }
  to { transform: translateX(calc(-1 * var(--suggest-glissement, 0px))); }
}
/* ⚠ EN ARABE, LE TEXTE DÉBORDE DE L'AUTRE CÔTÉ : il faut donc glisser dans l'AUTRE SENS (+X), sans
   quoi l'animation emmènerait le titre plus loin au lieu de révéler sa fin. */
[dir="rtl"] .shop-suggest-name.is-defilant .shop-suggest-name-inner { animation-name: shop-suggest-defile-rtl; }
@keyframes shop-suggest-defile-rtl {
  from { transform: translateX(0); }
  to { transform: translateX(var(--suggest-glissement, 0px)); }
}
/* ⚠ QUI A DEMANDÉ MOINS DE MOUVEMENT N'EN A PAS. Le script ne pose déjà pas la classe dans ce cas
   (voir `animerTitresTronques`) ; cette règle est la seconde barrière, pour que la feuille de style
   dise la même chose que le script — et pour couvrir le cas où le réglage change EN COURS de
   session, sans rechargement. Le titre reste lisible en entier par son infobulle. */
@media (prefers-reduced-motion: reduce) {
  .shop-suggest-name.is-defilant .shop-suggest-name-inner { animation: none; transform: none; }
}
.shop-suggest-meta {
  font-size: 11px; color: var(--shop-muted); text-align: start;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.shop-suggest-code { letter-spacing: .3px; }
.shop-suggest-sep { opacity: .6; }
.shop-suggest-stock.is-in { color: #15803d; }
.shop-suggest-stock.is-out { color: #b91c1c; }
/* Le prix est un ÎLOT LATIN dans une page arabe : `[dir="ltr"]` est posé par le script,
   et `unicode-bidi: isolate` empêche l'algorithme bidirectionnel de le déplacer. */
.shop-suggest-price { flex: 0 0 auto; font-size: 13px; font-weight: 700; color: var(--shop-navy); unicode-bidi: isolate; }
.shop-suggest-empty { padding: 10px; font-size: 13px; color: var(--shop-muted); text-align: center; }
/* ⭐⭐ LA LIGNE « RÉSULTATS APPROCHÉS » (voir la passe partielle de search.js). Elle est plus
   discrète qu'un résultat — c'est une EXPLICATION, pas une proposition — mais elle reste lisible
   et alignée à gauche, dans le sens de lecture de la page (`text-align: start`, donc à droite en
   arabe). ⚠ Aucune largeur en dur : la ligne suit la largeur du panneau. */
.shop-suggest-hint { padding: 8px 10px; font-size: 12px; color: var(--shop-muted); text-align: start; border-bottom: 1px solid var(--shop-border); }
.shop-suggest-all .shop-suggest-link { justify-content: center; font-size: 13px; font-weight: 600; color: var(--shop-navy); }

/* Sur téléphone, la barre prend toute la largeur de l'en-tête : elle n'a plus à cohabiter
   avec les liens du menu, et le panneau a ainsi l'écran entier à sa disposition. */
@media (max-width: 600px) {
  .shop-search-wrap { flex: 1 1 100%; min-width: 0; }
  .shop-suggest { max-height: 60vh; }
}
/* ═══════════════════════════════════════════════════════════════════════════════════
   LES STORIES DE L'ACCUEIL — la vitrine des articles « mis en avant » dans la gestion
   ═══════════════════════════════════════════════════════════════════════════════════

   ⭐ DEUX ÉTAGES, ET ILS SONT INDÉPENDANTS DU SCRIPT :
     · `.stories-track` — LA BANDE, dans le flux de la page : UN CERCLE PAR CATÉGORIE, et chaque
       cercle est un LIEN vers les articles de cette catégorie (le catalogue filtré sur elle). Sans
       JavaScript, c'est tout ce qu'il y a, et c'est utile tel quel.
     · `.story-viewer` — LA VISIONNEUSE plein écran, `hidden` dans le gabarit. Elle n'existe à
       l'écran que si le script l'ouvre : aucun bouton mort, aucun contenu invisible.

   ⚠ `hidden` EST RESPECTÉ PAR LA FEUILLE : la règle qui affiche la visionneuse est portée par la
   CLASSE `is-open` (posée par le script), jamais par `display` sur l'élément lui-même. Écrire
   `display: flex` ici la montrerait en permanence — l'erreur classique, et elle rendrait la page
   inutilisable (un calque plein écran qui capte tous les clics). */
.stories { margin-bottom: 18px; }
.stories-title { font-size: 16px; margin: 0 0 8px; }
/* ⭐ LA DURÉE DE VIE, DITE AU VISITEUR (48 h par défaut, voir SHOP_STORY_HOURS). Une histoire qui
   disparaît sans explication ressemble à un bug ; une histoire annoncée « 48 h » se comprend. */
.stories-hint { margin: 0 0 10px; font-size: 12px; color: var(--shop-muted); }
/* ⭐⭐⭐ LA BANDE EST UNE RANGÉE DE CERCLES DE CATÉGORIE (demande du client : « make each category a
   cercle with a title below, each article has an indicator around the circle »). Un cercle par
   CATÉGORIE, la photo d'un de ses articles dedans, LE NOM DE LA CATÉGORIE SOUS LE CERCLE, le nombre
   d'articles POSÉ SUR le cercle, et — ce qui est l'objet de la dernière demande du client — UN TIRET
   ORANGE PAR HISTOIRE AUTOUR du cercle (« each story is an orange dash, seen = dash disapear », voir
   `.story-chip-ring` et `.story-chip-dash` plus bas).
   ⚠ LA RANGÉE RESTE UNE LISTE (`role="list"` dans le gabarit) : un lecteur d'écran annonce « 3 sur
   10 » — dix cercles se comptent, cent pastilles non, et c'est tout l'objet de la refonte.
   ⓘ `.stories-groups` n'est plus qu'un CONTENEUR (une seule rangée désormais) : il porte le point
   d'écoute du script ([data-stories-groups], voir public/shop/shop.js), donc il reste. */
.stories-groups { display: flex; flex-direction: column; gap: 12px; }
.stories-track {
  display: flex; gap: 10px; margin: 0; padding: 2px 2px 6px; list-style: none;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
}
.story-chip { flex: 0 0 auto; scroll-snap-align: start; }
.story-chip-link {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  width: 84px; text-decoration: none; color: var(--shop-navy); font-size: 11px; text-align: center;
}
.story-chip-link:hover { text-decoration: none; }
/* Le cercle : c'est le code visuel des histoires (un anneau autour d'une vignette ronde).
   ⚠ `position: relative` N'EST PAS DÉCORATIF, ET IL L'EST DÉSORMAIS DEUX FOIS : le nombre d'articles
   est posé SUR le cercle (voir `.story-chip-count`) et les TIRETS de l'anneau sont des calques
   absolus (voir `.story-chip-dash`), tous deux repérés par rapport à lui.
   ⭐⭐⭐ L'ANNEAU EST FAIT DE TIRETS, UN PAR HISTOIRE (demande du client : « each story is an orange
   dash, seen = dash disapear »). Le nombre de tirets EST le nombre d'articles de la catégorie, et
   chacun part quand SON histoire a été ouverte (voir public/shop/shop.js et routes/shop.js, qui
   calcule les angles).
   ⚠⚠ IL N'Y A DONC PLUS AUCUN MOTIF DE FOND ICI : l'ancien `repeating-conic-gradient` a disparu, et
   c'est tout l'objet de la demande — un fond est UN SEUL dessin, il ne sait pas perdre un de ses
   tirets. Un anneau peint ne pouvait pas non plus dire COMBIEN d'histoires il restait à voir.
   ⓘ `padding: 6px` rentre la photo dans le cercle et réserve la place de l'anneau. ⚠⚠ IL EST LE MÊME
   DANS TOUS LES ÉTATS : un cercle vu ne se rembourre plus (l'ancienne règle « vue » passait de 4 px à
   3 px), sinon la photo d'un cercle vu n'aurait pas la même taille que celle de ses voisins, et la
   rangée tressauterait au moment où l'indicateur change — un défaut visuel pour rien, puisque c'est
   la DISPARITION des tirets, et non l'épaisseur du rembourrage, qui porte l'information. */
.story-chip-ring {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 72px; height: 72px; border-radius: 50%; padding: 6px;
}
/* ⭐⭐ UN TIRET DE L'ANNEAU. C'est un CALQUE qui recouvre tout le cercle (`inset: 0`) et que l'on fait
   PIVOTER de `--a` degrés — l'angle que le SERVEUR a calculé pour cette histoire (`i * 360 / n`, voir
   `categoriesDeStories`). La barre elle-même est posée par `::before`, EN HAUT du calque : la rotation
   l'amène donc à sa place sur la circonférence, et elle y reste TANGENTE (perpendiculaire au rayon)
   quel que soit l'angle — c'est ce qui donne l'anneau régulier, et non des barres qui s'inclinent.
   ⚠⚠ AUCUNE PROPRIÉTÉ PHYSIQUE ICI (`left`, `margin-left`…), ET C'EST LA RÈGLE DE CETTE FEUILLE (voir
   l'en-tête) : la barre se centre avec `inset-inline-start: 50%` et une marge LOGIQUE négative. En
   arabe, le calque est retourné en miroir ; la rotation étant symétrique, l'anneau est identique et la
   barre reste centrée sur son angle — le tiret ne se décale donc pas de quelques pixels en RTL.
   ⓘ LA LARGEUR DE LA BARRE EST FIXE (9 px), ET C'EST ASSUMÉ : plus une catégorie a d'histoires, plus
   les tirets se resserrent. Au-delà d'une vingtaine ils se touchent presque et l'anneau paraît DENSE —
   ce qui reste vrai et lisible (« il y a beaucoup à voir »), et une vitrine de boutique se compte en
   unités par famille (le total de la page est borné par STORY_LIMIT, voir routes/shop.js). Rétrécir la
   barre pour en caser davantage la rendrait invisible à trois mètres, c'est-à-dire inutile. */
.story-chip-dash {
  position: absolute; inset: 0;
  transform: rotate(var(--a));
}
.story-chip-dash::before {
  content: '';
  position: absolute; inset-block-start: 0; inset-inline-start: 50%;
  margin-inline-start: -4.5px;
  width: 9px; height: 4px; border-radius: var(--shop-radius-pill);
  background: var(--shop-accent);
}
/* ⛔ UN TIRET VU DISPARAÎT — c'est la demande mot pour mot : « seen = dash disapear ». ⚠⚠ SURTOUT PAS
   UN TIRET PÂLI : un tiret délavé se lit encore comme « il est là », or c'est justement son ABSENCE
   qui porte l'information (« cette histoire est vue », et, quand il n'en reste aucun, « il n'y a plus
   rien à voir ici »). `display: none` le retire aussi de la mise en page : un cercle dont TOUS les
   tirets sont partis ne porte donc plus aucun anneau — il ne reste que la photo et la pastille du
   nombre, INTACTES toutes les deux : le cercle garde exactement l'apparence d'un cercle neuf (voir
   l'explication « ne pas ternir » plus bas). */
.story-chip-dash.is-seen { display: none; }
/* ⭐ LA PHOTO EST RONDE ET REMPLIT LE CERCLE : elle occupe tout l'intérieur du rembourrage, et c'est
   donc le tour du cercle — la bande où vivent les tirets — qui reste visible. C'est la seule façon de
   lire l'indicateur : un carré dans un cercle ne laisserait voir qu'un liseré aux quatre coins.
   ⓘ Les tirets restent DANS cette bande de rembourrage (une barre posée au bord d'un calque de 72 px
   occupe un rayon d'environ 32 à 36 px, là où la photo s'arrête à 30 px) : ils ne mordent donc jamais
   sur l'image, même quand ils se croisent. */
.story-chip-img { display: block; width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
/* Pas de photo dans TOUTE la catégorie : un cercle vide, propre — jamais une image cassée. */
.story-chip-img.is-empty { display: block; background: var(--shop-bg); }
/* ⭐⭐ LE NOMBRE D'ARTICLES DE LA CATÉGORIE, POSÉ SUR LE CERCLE (demande du client : un indicateur
   visible). ⚠ Il chevauche le bord du cercle : c'est ce qui le fait lire comme un compteur de la
   catégorie, et non comme un nombre perdu dans la rangée. */
.story-chip-count {
  position: absolute; inset-block-end: -2px; inset-inline-end: -2px;
  min-width: 20px; padding: 0 5px; border-radius: var(--shop-radius-pill);
  background: var(--shop-accent); color: #fff;
  font-size: 11px; font-weight: 700; line-height: 20px; text-align: center;
}
/* ⭐⭐⭐ « NE PAS TERNIR UNE HISTOIRE DÉJÀ VUE » (demande du client : « dont grey out story after being
   seen »). Les QUATRE signaux de grisaille qui vivaient ici — désaturation de la photo, baisse de son
   opacité, nom dégrisé et allégé, pastille du nombre éteinte — sont SUPPRIMÉS : une catégorie vue
   garde exactement la même photo, le même nom et la même pastille qu'une catégorie à voir.
   ⚠⚠ CE QUI RESTE, ET C'EST VOULU : LA DISPARITION DES TIRETS (`display: none`, plus haut). Le client
   l'a demandée mot pour mot (« seen = dash disapear »), et c'est un COMPTE, pas une couleur : le
   nombre de tirets dit combien d'histoires restent à voir. Ternir en plus le cercle revenait à le dire
   deux fois — et à faire croire qu'une histoire vue ne valait plus la peine d'être regardée.
   ⓘ L'état du cercle (`is-seen`) n'est donc plus qu'une CONSÉQUENCE de la disparition du dernier
   tiret (voir `rafraichirCercle` dans public/shop/shop.js) : il ne colore plus rien lui-même. */
/* Le nom sous le cercle, sur DEUX lignes au maximum, puis des points de suspension : la rangée garde
   une hauteur régulière, sinon un long nom de catégorie décale tous les cercles voisins.
   ⓘ Il reste EN GRAS pour tout le monde : depuis que la grisaille a été retirée (voir plus haut),
   l'état « vu » ne se lit plus que sur les tirets du cercle. */
.story-chip-name {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; line-height: 1.15; max-width: 84px; font-weight: 700;
}

/* ------- LE LIEN VERS L'ADRESSE DES HISTOIRES (sur l'accueil seulement) -------
   Demande du client : « make stories apearin ther own url , /shop/stories ». Un lien discret, sous la
   bande : la vitrine reste ce qu'on regarde, le lien ne fait que dire qu'il y en a plus. */
.stories-more { margin: 12px 0 0; }

/* ------- LA PAGE DES HISTOIRES (`/shop/stories`) -------
   ⭐⭐ LES CARTES SONT CELLES DE LA LISTE, PAS CELLES DU CATALOGUE. La vignette du catalogue
   (`partials/productCard.hbs`) porte un cœur de devis et un bouton d'ajout : deux actions qui ont
   leur place dans une page où l'on CHERCHE un article. Ici on est venu REGARDER la vitrine — la
   carte mène donc à la fiche et propose « voir en grand », et rien d'autre. ⓘ Une seconde grille
   écrite à la main serait à maintenir ; un `productCard` détourné de son sens serait pire. */
.stories-intro { margin: 0 0 14px; font-size: 13px; color: var(--shop-muted); }
.stories-back { margin: 18px 0 0; }

/* Un groupe par catégorie, comme les cercles de la bande (même ordre, même nom). */
.stories-cat { margin: 0 0 18px; }
.stories-cat-title {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin: 0 0 8px; font-size: 16px;
}
.stories-cat-title a { color: inherit; text-decoration: none; }
.stories-cat-title a:hover { color: var(--shop-accent); }
/* Le compte, à CÔTÉ du nom et non dans le titre : il change de forme selon la langue (l'arabe a
   cinq pluriels), et il est donc rendu par la traduction, pas par une concaténation. */
.stories-cat-count { font-size: 12px; font-weight: 400; color: var(--shop-muted); }

/* ⭐ LA GRILLE SE REPLIE TOUTE SEULE (`auto-fill`) : deux colonnes sur un téléphone, quatre ou cinq
   sur un écran large, sans point de rupture à tenir. ⓘ `minmax(150px, 1fr)` et non une largeur
   fixe : la carte s'étire au lieu de laisser un vide à droite. */
.stories-grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px;
}
.story-card {
  display: flex; flex-direction: column;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius);
  background: #fff; overflow: hidden;
}
/* ⛔⛔ `.story-card.is-seen` NE PEINT RIEN, ET C'EST VOLONTAIRE. Le client a demandé qu'une histoire
   vue ne soit PLUS ternie (« dont grey out story after being seen ») : la classe est posée par le
   script (voir `marquerElement` dans public/shop/shop.js) et sert à SAVOIR, pas à griser — exactement
   comme pour les vignettes de visuels. ⚠ Ne pas ajouter de règle ici sans relire cette demande : le
   retour de la grisaille serait un défaut signalé deux fois. */
.story-card-media { display: block; }
.story-card-img { display: block; width: 100%; aspect-ratio: 1 / 1; object-fit: cover; }
.story-card-img.is-empty {
  display: flex; align-items: center; justify-content: center;
  background: var(--shop-bg); color: var(--shop-muted); font-size: 12px;
}
.story-card-body { display: flex; flex-direction: column; gap: 6px; padding: 10px; flex: 1 1 auto; }
.story-card-name { margin: 0; font-size: 14px; line-height: 1.25; }
.story-card-name a { color: var(--shop-navy); text-decoration: none; }
.story-card-name a:hover { color: var(--shop-accent); text-decoration: underline; }
.story-card-price { margin: 0; font-size: 15px; font-weight: 700; color: var(--shop-accent); }
.story-card-out { margin: 0; font-size: 12px; color: var(--shop-muted); }
/* ⚠ `margin-top: auto` COLLE LE BOUTON AU BAS DE LA CARTE : les noms de longueurs différentes
   laissent ainsi la même ligne d'action sur toute la rangée, au lieu de boutons en escalier. */
.story-card-open {
  margin-top: auto; align-self: flex-start;
  padding: 6px 10px;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius-pill);
  font-size: 12px; text-decoration: none; color: var(--shop-navy);
}
.story-card-open:hover { border-color: var(--shop-accent); color: var(--shop-accent); }
.story-card-open:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: 2px; }

/* ------- la visionneuse plein écran ------- */
.story-viewer {
  position: fixed; inset: 0; z-index: 96;
  display: none; flex-direction: column;
  background: #0b1220; color: #fff;
  /* ⚠ `touch-action: pan-y` REND LE BALAYAGE POSSIBLE. Sans elle, le navigateur garde le geste
     horizontal pour lui (défilement latéral, geste « retour » du système) et le doigt ne produit
     jamais les événements pointeur attendus : le balayage d'une histoire à l'autre — le geste que
     tout le monde fait — ne partirait pas. On LAISSE donc le vertical (`pan-y`) à la VISIONNEUSE
     ELLE-MÊME, qui n'a rien à faire défiler : ce qui se trouve derrière reste accessible au clavier
     et au lecteur d'écran, et le bloc de texte garde son propre défilement (voir `.story-info`).
     ⭐⭐⭐ MAIS LES GESTES DE LECTURE VIVENT SUR LES ZONES DE FRAPPE, ET ELLES, ELLES REPRENNENT LES
     DEUX AXES (`touch-action: none`, voir `.story-tap`) : le balayage VERS LE HAUT qui ouvre le
     groupe suivant (demande du client : « when i swipe up in story open next group ») ne partirait
     jamais si le navigateur s'emparait du geste vertical pour faire défiler la page. */
  touch-action: pan-y;
}
.story-viewer.is-open { display: flex; }
/* La barre de progression : une barre par histoire, toutes dans une seule boîte (le script y met
   les segments qu'il faut, une fois qu'il connaît le nombre d'histoires). */
.story-progress { display: flex; gap: 4px; padding: 10px 12px 4px; }
.story-progress-seg { flex: 1 1 0; height: 3px; border-radius: var(--shop-radius-pill); background: rgba(255, 255, 255, .3); overflow: hidden; }
.story-progress-seg i { display: block; height: 100%; width: 0; background: #fff; }
.story-progress-seg.is-done i { width: 100%; }
/* ⚠ LA DURÉE EST UNE VARIABLE POSÉE PAR LE SCRIPT (`--story-duree`) : la barre et le minuteur
   avancent donc ENSEMBLE, et changer la durée ne demande pas de toucher à deux endroits. */
.story-progress-seg.is-live i { animation: story-avance var(--story-duree, 5s) linear forwards; }
@keyframes story-avance { from { width: 0; } to { width: 100%; } }
/* ⭐⭐⭐ LA BARRE SE FIGE PENDANT UN APPUI LONG (demande du client : « when i press long on story pause
   it »). ⚠ `animation-play-state: paused` ARRÊTE l'animation LÀ OÙ ELLE EN EST — elle ne la rembobine
   pas et ne la relance pas : au relâchement, la barre repart du même point, exactement comme le
   minuteur du script reprend avec le temps qui reste (voir public/shop/shop.js). C'est ce qui garantit
   que la barre et la lecture ne se désynchronisent jamais.
   ⓘ La classe est portée par la VISIONNEUSE, et non par le segment : c'est l'ÉTAT DE LA LECTURE, pas
   l'état d'une barre — et c'est aussi ce qui permettra de l'annoncer un jour aux lecteurs d'écran. */
.story-viewer.is-paused .story-progress-seg.is-live i { animation-play-state: paused; }

.story-stage { position: relative; flex: 1 1 auto; min-height: 0; }
.story-slide { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: flex-end; }
/* ⚠⚠⚠ CETTE LIGNE N'EST PAS UNE REDONDANCE : SANS ELLE, TOUTES LES HISTOIRES S'AFFICHENT EN MÊME
   TEMPS, EMPILÉES. C'est le défaut signalé par le client — « images don't change in stories » : le
   script posait bien l'attribut `hidden` sur les diapositives inactives, mais `hidden` n'est qu'une
   règle de la feuille DU NAVIGATEUR, qui perd TOUJOURS contre une règle d'auteur. Le `display: flex`
   de la règle ci-dessus l'emportait donc, chaque diapositive restait affichée, et comme elles sont
   toutes en `position: absolute; inset: 0`, c'était la DERNIÈRE du DOM qu'on voyait — en
   permanence. L'histoire avançait, la barre de progression avançait, la photo et le titre non.
   ⓘ C'est le même piège que `.shop-top[hidden]`, `.shop-mic[hidden]` et `.shop-voice[hidden]` plus
   haut : dès qu'une règle d'auteur pose un `display` sur un élément que le gabarit masque avec
   `hidden`, il faut la règle `[hidden]` qui va avec. Vérifié au navigateur : `affichees` doit valoir
   1 (voir scripts/browser-check-shop.js). */
.story-slide[hidden] { display: none; }
.story-media { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.story-img { width: 100%; height: 100%; object-fit: contain; }
/* ⚠ LES VISUELS D'UN MÊME ARTICLE SONT EMPILÉS AU MÊME ENDROIT : c'est cette CLASSE qui décide
   lequel se voit — jamais l'attribut `hidden`, que la règle ci-dessus battrait (elle appartient à
   l'auteur, `hidden` au navigateur — voir l'avertissement sur `.story-slide`). */
.story-img.is-hidden { display: none; }
.story-img.is-empty { display: flex; align-items: center; justify-content: center; color: rgba(255, 255, 255, .6); font-size: 14px; }
/* ⭐⭐ LA VIDÉO D'UNE HISTOIRE (demande du client : « add possibility to add videos to product ,
   (video also shows in stories ) »). Elle prend EXACTEMENT la place d'une photo — `object-fit:
   contain`, comme `.story-img` — et elle porte le même `data-story-image` : la rotation ne fait
   aucune différence entre une image et une vidéo, et c'est voulu. `contain` plutôt que `cover` :
   une vidéo verticale ou carrée ne doit pas être rognée, c'est le seul réglage qui montre tout.
   ⚠ AUCUNE BARRE DE CONTRÔLE ICI, ET CE N'EST PAS UN OUBLI : les zones de frappe de la visionneuse
   (`.story-tap`) couvrent la moitié de la largeur sur toute la hauteur, donc une barre de contrôle
   serait visible et INERTE. La vidéo est jouée en sourdine (voir partials/storyViewer.hbs), le son
   s'obtient par le bouton `.story-sound`, et le lecteur complet vit sur la fiche produit. */
.story-video { background: #000; }
/* ⭐ LE REPÈRE DE LECTURE SUR LA VIGNETTE D'UNE VIDÉO : sans lui, la vignette d'un film est
   indiscernable de celle d'une photo — le client croit que sa vidéo ne s'est pas ajoutée. */
.story-thumb-video { position: relative; }
.story-thumb-video::after {
  content: '▶'; position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: 14px; text-shadow: 0 1px 3px rgba(0, 0, 0, .85);
  background: rgba(6, 12, 24, .35);
}
/* ⭐ LE BOUTON DE SON D'UNE VIDÉO (voir `data-story-sound`) : il CHANGE DE LIBELLÉ quand on
   l'actionne, donc ses deux phrases viennent de la langue de la page (voir services/i18n.js).
   ⚠ Il est dans `.story-info`, donc au-dessus des zones de frappe : l'appuyer rend le son, il ne
   fait pas avancer l'histoire. */
.story-sound {
  align-self: flex-start; padding: 6px 12px; cursor: pointer;
  background: rgba(255, 255, 255, .12); border: 1px solid rgba(255, 255, 255, .35);
  border-radius: var(--shop-radius-pill); color: #fff; font: inherit; font-size: 13px;
}
.story-sound:hover { background: rgba(255, 255, 255, .26); }
.story-sound:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* ⚠⚠ LE BOUTON EST RENDU `hidden` PAR LE GABARIT (il n'apparaît que sur une diapositive vidéo) : LA
   RÈGLE `[hidden]` EST DONC OBLIGATOIRE, comme pour `.shop-top`, `.shop-mic` et `.story-slide`. Une
   règle d'AUTEUR l'emporte TOUJOURS sur la feuille du navigateur : sans celle-ci, le bouton
   resterait visible sur les photos, et il agirait sur une vidéo qui n'est pas à l'écran. */
.story-sound[hidden] { display: none; }
/* ⭐⭐ LES VIGNETTES DES AUTRES VISUELS (les variantes de l'article, demande du client). Elles vivent
   DANS le bloc de texte et d'actions, donc au-dessus des zones de frappe : un doigt sur une vignette
   change l'image, il ne passe pas à l'histoire suivante. ⚠ 44 px de côté, encoche comprise : c'est
   une cible de doigt, pas une décoration. */
.story-thumbs { display: flex; gap: 6px; flex-wrap: wrap; }
.story-thumb {
  width: 44px; height: 44px; padding: 0; cursor: pointer;
  border: 2px solid transparent; border-radius: var(--shop-radius); overflow: hidden;
  background: rgba(255, 255, 255, .12);
}
.story-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* ⭐⭐ L'INDICATEUR FIN D'UNE HISTOIRE : la vignette du visuel AFFICHÉ est entourée d'un accent — et
   c'est désormais le SEUL état que porte la bande de vignettes.
   ⚠⚠ LE TERNISSEMENT DES VISUELS DÉJÀ VUS A ÉTÉ SUPPRIMÉ, comme celui des cercles (demande du client :
   « dont grey out story after being seen ») : une histoire vue se regarde aussi bien qu'une autre.
   ⚠ La couleur n'est jamais le seul signal : la vignette active porte aussi `aria-pressed`. */
.story-thumb.is-active { border-color: var(--shop-accent); }
.story-thumb:hover { border-color: rgba(255, 255, 255, .6); }
.story-thumb:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* ⚠ LE TEXTE SE POSE SUR UN DÉGRADÉ SOMBRE : les photos de catalogue sont souvent claires, et un
   titre blanc sur fond blanc serait illisible. Le dégradé n'est PAS décoratif. */
.story-info {
  position: relative; padding: 16px 16px 24px;
  display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
  background: linear-gradient(to top, rgba(6, 12, 24, .95) 0%, rgba(6, 12, 24, .75) 60%, rgba(6, 12, 24, 0) 100%);
  /* ⚠⚠ `z-index: 1` N'EST PAS DÉCORATIF : C'EST CE QUI REND LES BOUTONS DE LA STORY CLIQUABLES.
     Les zones de frappe (`story-tap`, plus bas) font 35 % de large sur TOUTE LA HAUTEUR de la
     visionneuse, et elles sont peintes APRÈS l'histoire dans le DOM — donc PAR-DESSUS le bloc de
     texte et d'actions. Résultat, constaté par le client : « Ajouter au panier » et « Voir la fiche »
     ne faisaient pas ce qu'ils annonçaient — le doigt tombait sur la zone de frappe, et la story
     AVANÇAIT. Le bloc de texte passe donc au-dessus (z-index 1) et reprend les clics sur la bande
     du bas ; les zones de frappe gardent tout l'espace au-dessus, là où il n'y a rien à viser.
     ⚠ À NE PAS « SIMPLIFIER » EN RÉDUISANT LA HAUTEUR DES ZONES DE FRAPPE À UNE VALEUR EN PIXELS :
     la hauteur du bloc de texte dépend du titre (une ou deux lignes), du prix et de l'état de stock —
     un nombre fixe serait faux dès qu'un titre passe sur deux lignes, et le défaut reviendrait
     seulement pour les articles à nom long. */
  z-index: 1;
  /* ⭐⭐⭐ ET IL GARDE SON PROPRE DÉFILEMENT, MAINTENANT QUE LES ZONES DE FRAPPE ONT PRIS LES DEUX AXES
     (voir .story-tap : touch-action: none). Sans ces deux lignes, une histoire dont le titre tient sur
     deux lignes, dont les vignettes sont nombreuses et dont l'article est en rupture pousserait ses
     boutons HORS de l'écran — et plus rien ne permettrait d'aller les chercher, puisque le geste
     vertical n'appartient plus qu'au script. Le bloc se limite donc à la hauteur de la diapositive et
     défile pour lui-même ; `touch-action: pan-y` lui rend le geste vertical DANS SA SURFACE, où il n'y
     a rien à balayer. */
  max-height: 100%; overflow-y: auto; touch-action: pan-y;
}
.story-name { margin: 0; font-size: 18px; line-height: 1.25; }
.story-price { margin: 0; font-size: 17px; font-weight: 700; }
.story-price-none { font-size: 13px; font-weight: 400; color: rgba(255, 255, 255, .75); }
.story-out { margin: 0; font-size: 13px; color: #fbbf24; }
.story-actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.story-actions .btn { background: rgba(255, 255, 255, .12); border-color: rgba(255, 255, 255, .35); color: #fff; }
.story-actions .btn-primary { border-color: var(--shop-accent); }
/* ⭐⭐⭐ LE BOUTON DE PARTAGE D'UNE HISTOIRE EST VERT WHATSAPP, ET IL FAUT L'ÉCRIRE ICI (demande du
   client : « add botton to share story in whatssap status … »). Le bouton porte .btn-wa (le vert de la
   maison, voir plus bas) ET .btn, or la règle ci-dessus — .story-actions .btn — est PLUS SPÉCIFIQUE que
   .btn-wa : sans cette ligne, le bouton de partage serait peint comme les autres (fond translucide
   blanc) et perdrait la seule chose qui dit « WhatsApp » avant même de lire le libellé.
   ⚠ IL EST PLEINEMENT CLIQUABLE, comme les autres : il vit dans .story-info, donc AU-DESSUS des zones
   de frappe (voir l'avertissement de .story-info) — un doigt sur « Partager » partage, il ne fait pas
   avancer l'histoire. */
.story-actions .btn-wa { background: #25d366; border-color: #1da851; color: #fff; }
.story-actions .btn-wa:hover { background: #1da851; }
/* ⭐⭐ IL EST EN ICÔNE SEULE, COMME CELUI DE LA FICHE PRODUIT (demande du client : « share button in
   stories just a whatsapp icon »). Le libellé « Partager » a donc disparu, et c'est la classe .btn-icon
   (plus bas dans cette feuille) qui fait la forme : un carré de la taille du dessin, centré. ⚠ Le mot
   ne disparaît pas de la page pour autant : il vit dans title ET aria-label, donc la souris affiche
   l'infobulle et un lecteur d'écran annonce « Partager cette histoire sur WhatsApp ». Un dessin seul,
   sans nom accessible, serait un bouton muet — c'est la règle de la maison, et elle est vérifiée. */
.story-share { flex: 0 0 auto; }
/* Les zones de frappe : transparentes, mais de VRAIS boutons (tabulation, nom accessible).
   ⚠ `z-index: 0` LES MET SOUS LE BLOC DE TEXTE ET D'ACTIONS (voir `.story-info` ci-dessus) : elles
   couvrent la photo, pas les boutons. Sans cette précision, elles repasseraient au-dessus au gré de
   l'ordre du DOM — c'est exactement le défaut qui a été signalé.
   ⚠⚠ ET ELLES COUVRENT LA MOITIÉ DE LA LARGEUR CHACUNE, PAS 35 %. À 35 %, il restait une bande
   MORTE de 30 % au milieu de la photo : le client qui tapait le centre de l'image — le geste le plus
   naturel — ne déclenchait RIEN, et la story ne passait pas (constaté : « the story don't skip on
   tap »). Deux moitiés se partagent toute la photo, comme dans les visionneuses que le client
   utilise déjà : à droite on avance, à gauche on revient. */
/* ⭐⭐⭐ ET ELLES APPARTIENNENT AU SCRIPT SUR LES DEUX AXES (demande du client : « when i swipe up in
   story open next group »). C'est ICI que le doigt se pose pour lire une histoire — donc ici que se
   font TOUS les gestes, y compris le balayage vers le haut qui change de groupe. Or la visionneuse
   laisse VOLONTAIREMENT le geste vertical au navigateur (touch-action: pan-y, voir plus haut) : sur un
   téléphone, celui-ci commencerait à faire défiler la page derrière, nous enverrait un « geste annulé »
   et le balayage vers le haut ne partirait jamais. Les zones de frappe reprennent donc la main
   entièrement (touch-action: none) : le doigt qui touche la photo ne fait plus défiler quoi que ce
   soit, et le script décide — tap, balayage latéral, balayage vers le haut, appui long.
   ⚠ LE BLOC DE TEXTE, LUI, GARDE SON DÉFILEMENT (voir .story-info) : sur un écran serré, l'histoire
   dont le titre, le prix et les vignettes débordent doit rester lisible jusqu'aux boutons. */
.story-tap { position: absolute; top: 0; bottom: 0; width: 50%; background: none; border: 0; cursor: pointer; z-index: 0; touch-action: none; }
.story-tap-prev { inset-inline-start: 0; }
.story-tap-next { inset-inline-end: 0; }
.story-tap:focus-visible { outline: 2px solid #fff; outline-offset: -4px; }
.story-close {
  position: absolute; top: 8px; inset-inline-end: 8px; z-index: 2;
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0; cursor: pointer;
  background: rgba(255, 255, 255, .14); border: 0; border-radius: 50%; color: #fff;
}
.story-close:hover { background: rgba(255, 255, 255, .26); }
/* ⚠ QUI A DEMANDÉ MOINS DE MOUVEMENT N'A PAS DE BARRE QUI COURT : le script ne lance alors aucune
   avance automatique (voir public/shop/shop.js), et cette règle est la seconde barrière — elle
   fige l'animation même si le réglage change en cours de session. */
@media (prefers-reduced-motion: reduce) {
  .story-progress-seg.is-live i { animation: none; width: 0; }
}

/* Le titre de l'accueil et le lien « voir le catalogue » sous les nouveautés. */
.home-section { margin-top: 18px; }
.home-section-title { font-size: 16px; margin: 0 0 10px; }
.home-more { margin: 14px 0 0; text-align: center; }
/* ⭐⭐⭐ LE BOUTON « VOIR LE CATALOGUE » ÉPINGLÉ EN BAS DE L'ACCUEIL (demande du client : « stiky or pin
   button voire catalouge in home page mobile /web »).

   ⚠⚠ COMMENT IL EST ÉPINGLÉ, ET POURQUOI AINSI. C'est un élément COLLANT (position: sticky), posé en
   DERNIER dans la page : il descend donc avec le contenu, mais s'arrête au bord bas de l'écran tant
   qu'il reste de la page à lire, et reprend sa place à la fin. C'est ce qui fait qu'il ne recouvre
   jamais le pied de page — un élément collant ne peut pas sortir de son conteneur, or son conteneur
   s'arrête avec le contenu.
   ⚠⚠ ET IL NE CAPTE PAS LES CLICS QU'IL NE DOIT PAS : le conteneur est en pointer-events: none, seul
   le bouton reprend les clics. Sans cela, la bande (large de tout le contenu) recouvrirait les
   vignettes qu'elle survole, et un doigt sur un article ouvrirait le catalogue — le défaut classique
   d'un calque qui n'a rien à faire là.
   ⚠⚠ IL EST CENTRÉ (demande du client : « centre button voir cataloge »). Il était aligné sur la marge
   du contenu, du côté où commence la lecture ; le client l'a vu à l'écran et a demandé qu'il soit au
   milieu — c'est un appel à l'action, et un appel se lit au centre.
   ⚠ LE CENTRAGE NE LE FAIT PAS EMPIÉTER SUR LES DEUX AUTRES BOUTONS : WhatsApp et « haut de page »
   occupent le coin de FIN de ligne (44 px de large, à 16 px du bord — voir .shop-wa), et la bande du
   bouton centré reste au milieu : sur un écran de 360 px, le bouton le plus long (« Voir le
   catalogue » et son pictogramme) s'arrête autour du tiers droit, et sur un écran de 320 px l'écart se
   réduit sans jamais se recouvrir. C'est mesuré, pas espéré : deux pastilles superposées au même
   endroit seraient exactement le défaut qu'on vient de corriger.
   ⚠ IL EST AU-DESSUS DE LA BARRE D'ONGLETS SUR TÉLÉPHONE (voir la requête média plus bas) : sous elle,
   il serait invisible et intouchable, comme tout ce que la barre fixe recouvre. */
.home-pin {
  position: sticky; z-index: 40; bottom: 16px;
  display: flex; justify-content: center;
  margin-top: 18px; pointer-events: none;
}
.home-pin-btn {
  pointer-events: auto;
  display: inline-flex; align-items: center; gap: 8px;
  box-shadow: 0 8px 22px rgba(15, 23, 42, .22);
}
/* ⚠ `.sr-only` EXISTE POUR LE TITRE DE PAGE DE L'ACCUEIL : la page s'annonce (« Accueil ») aux
   lecteurs d'écran sans qu'un gros titre répète ce que les stories disent déjà. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

.shop-nav { display: flex; align-items: center; gap: 12px; font-size: 13px; }
.shop-nav-link { white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; }
/* ⭐⭐ LES ENTRÉES DE COMPTE SONT DES PICTOGRAMMES (demande du client) : l'en-tête du bureau tient
   sur une seule ligne, et les mêmes dessins servent la barre d'onglets du téléphone.
   ⚠ LE LIBELLÉ EST MASQUÉ, PAS SUPPRIMÉ : il reste dans le document (traduit, à sa place), et
   `aria-label` porte le nom accessible — sans lui, « display: none » retirerait le texte de
   l'arbre d'accessibilité et il ne resterait qu'un dessin muet. Voir layouts/main.hbs.
   ⓘ La bande entière est masquée sous 700 px (voir le bloc mobile) : cette règle ne concerne donc
   QUE le bureau, où les mots sont remplacés par les icônes. */
.shop-nav-label { display: none; }
/* ⚠ 40 px DE CÔTÉ POUR UNE ICÔNE SEULE : c'est la cible de la souris, et l'écart entre deux
   icônes reste celui de la bande (12 px). Sans cela, trois dessins de 20 px collés l'un à l'autre
   seraient difficiles à viser. */
.shop-nav-link { min-height: 40px; min-width: 34px; justify-content: center; }
.shop-link-btn.shop-nav-link { min-width: 40px; }
/* Le libellé masqué laisse la place au dessin : l'infobulle (`title`) dit le mot, dans la langue
   de la page, et le nom accessible reste entier. */
.shop-nav-link .shop-icon { opacity: .85; }
.shop-nav-link:hover .shop-icon { opacity: 1; }
.shop-nav-cta { border: 1px solid var(--shop-accent); color: var(--shop-accent); padding: 4px 10px; border-radius: var(--shop-radius); }
.shop-badge { background: var(--shop-accent); color: #fff; border-radius: var(--shop-radius-pill); padding: 0 6px; font-size: 11px; font-weight: 700; }
/* ⚠ UN BADGE À ZÉRO SE TAIT. « Panier 0 » à côté d'un mot attire l'œil pour ne rien dire, et
   dans une barre d'onglets il encombre quatre cibles de 56 px. La classe est posée au premier
   rendu par le gabarit, puis tenue à jour par `setBadge` (public/shop/shop.js) — vider son
   panier à la dernière ligne doit faire disparaître le badge, pas afficher « 0 ». */
.shop-badge.is-empty { display: none; }
.shop-inline-form { display: inline; }
.shop-link-btn { background: none; border: 0; color: var(--shop-muted); cursor: pointer; font-size: 13px; padding: 0; }

/* ⭐ LA DÉCONNEXION DE L'EN-TÊTE — TÉLÉPHONE UNIQUEMENT (voir layouts/main.hbs).
   ⚠ MASQUÉE SUR GRAND ÉCRAN (`display: none`) ET PAS SEULEMENT « déplacée » : la bande de
   liens du haut porte déjà la déconnexion, et l'en-tête du bureau doit rester exactement
   `marque · recherche · liens · langues`. Un second bouton dans cette ligne changerait la
   disposition demandée (et ferait doublon). Elle est révélée sous 700 px, où la bande de
   liens est masquée : c'est là qu'elle devient la SEULE déconnexion accessible. */
.shop-header-logout { display: none; }
/* ⚠ `font: inherit` EST VOLONTAIRE, ET C'EST LE SEUL ENDROIT DE CETTE FEUILLE OÙ IL EST
   ÉCRIT POUR MOI : un `<button>` n'hérite PAS de la police de la page (le navigateur lui
   applique la police d'interface du système). Sans cette ligne, « Déconnexion » s'afficherait
   dans une autre police que le reste de l'en-tête — c'est-à-dire exactement le défaut que la
   demande du client cherche à supprimer. ⓘ Il n'y a donc TOUJOURS aucune famille de police
   écrite en dur ici : on demande l'héritage, on ne choisit pas. */
.shop-logout-btn {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 44px; padding: 0 10px; cursor: pointer;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius);
  background: #fff; color: var(--shop-muted); font: inherit; font-size: 12px;
}
.shop-logout-btn:hover { color: var(--shop-navy); border-color: var(--shop-muted); }
.shop-logout-btn:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: -2px; }

/* Sélecteur de langue : deux PASTILLES-ICÔNES (`FR` / `AR`), la langue courante allumée.
   ⚠ `order: 4` CONSERVE LA DISPOSITION DU BUREAU. Sur grand écran, la boîte `.shop-header-top`
   s'efface (`display: contents`) : la marque et la langue redeviennent des éléments de
   l'en-tête, et sans cet `order` la langue s'insérerait AVANT la recherche (ordre du DOM) —
   l'en-tête de bureau changerait d'aspect. Avec `order: 4`, elle repasse en dernier, comme
   avant. Rien de tout cela ne joue sur téléphone, où la boîte est une vraie ligne.
   ⭐ UN GLOBE + LE CODE, PAS UN DRAPEAU : une langue n'est pas un pays, et le nom complet
   (« Français », « العربية ») reste dans `aria-label`/`title`, prononcé dans la bonne langue. */
.shop-lang { display: flex; align-items: center; gap: 2px; border: 1px solid var(--shop-border); border-radius: var(--shop-radius); padding: 2px; order: 4; }
.shop-lang-link {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; font-weight: 700; letter-spacing: .3px; padding: 3px 7px;
  border-radius: var(--shop-radius-sm); color: var(--shop-muted); white-space: nowrap;
}
.shop-lang-link .shop-icon { opacity: .7; }
/* Le code de langue est un ÎLOT LATIN dans une page arabe : même précaution que les montants
   (voir `.num`), sinon « FR » peut se retrouver recomposé à l'envers. */
.shop-lang-code { unicode-bidi: isolate; }
.shop-lang-link:hover { background: var(--shop-bg); text-decoration: none; }
.shop-lang-link.is-active { background: var(--shop-navy); color: #fff; }
.shop-lang-link.is-active .shop-icon { opacity: 1; }

/* Bandeau « pas de prix » : explique la règle au lieu de laisser croire à un bug. */
.shop-notice { background: #fff7ed; border-top: 1px solid #fed7aa; color: #7c2d12; padding: 8px 16px; font-size: 13px; text-align: center; }
.shop-notice a { text-decoration: underline; }

/* ════════════════════════════════════════════ BARRE D'ONGLETS (TÉLÉPHONE UNIQUEMENT)
   ⭐ ELLE N'EXISTE PAS SUR GRAND ÉCRAN (`display: none` par défaut, révélée sous 700 px).
   Sur un bureau, la bande de liens de l'en-tête montre déjà tout, en clair, sans rien
   apprendre au visiteur ; une barre fixe en bas d'un écran de 1400 px ne ferait que voler de
   la hauteur utile et imiter une application là où un site suffit.
   ⭐ ET ELLE EST EN BAS PARCE QUE LE POUCE Y ARRIVE SANS CHANGER DE PRISE. Les liens de compte
   en haut de page demandaient de déplacer la main, ou de faire défiler pour les retrouver.
   ⚠ `padding-bottom: env(safe-area-inset-*)` N'EST PAS COSMÉTIQUE : sur un iPhone à encoche,
   la barre d'accueil du système recouvre le bas de l'écran ; sans cette réserve, le libellé
   des onglets passe dessous et devient intouchable. Le `body` reçoit la même réserve en
   `padding-bottom` (voir le bloc mobile), sinon le pied de page resterait caché sous la barre. */
.shop-tabbar { display: none; }
.shop-tab {
  position: relative; flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  min-height: 56px; padding: 4px 2px;
  color: var(--shop-muted); text-decoration: none;
  font-size: 11px; line-height: 1.1; text-align: center;
}
.shop-tab:hover { text-decoration: none; color: var(--shop-navy); }
.shop-tab.is-active { color: var(--shop-navy); }
/* L'onglet actif est aussi SOULIGNÉ : la couleur seule ne suffit pas à dire où l'on est, et
   un lecteur d'écran, lui, lit `aria-current="page"` (posé par le gabarit). */
/* ⚠ PAS D'ARRONDI PHYSIQUE ICI (`0 0 … …`). Le soulignement se pose sur le bord INFÉRIEUR de la
   barre d'onglets, donc sur ses deux coins de FIN de ligne : les propriétés LOGIQUES le placent
   correctement dans les deux sens de lecture — en arabe, « 0 0 » aurait arrondi les mauvais. */
.shop-tab.is-active::after {
  content: ""; position: absolute; inset-inline: 22%; top: 0; height: 2px;
  background: var(--shop-accent);
  border-end-start-radius: var(--shop-radius-pill);
  border-end-end-radius: var(--shop-radius-pill);
}
.shop-tab-label { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Le badge se pose À CÔTÉ de l'icône, pas au-dessus : la barre fait 56 px de haut, et un
   badge superposé mangerait le pictogramme. `inset-inline-start` le place du côté de la FIN
   de l'icône centrée — donc à droite en français, à gauche en arabe, sans règle dédiée. */
.shop-tab-badge { position: absolute; top: 5px; inset-inline-start: calc(50% + 5px); font-size: 10px; padding: 0 4px; }

.shop-main { max-width: 1280px; margin: 0 auto; padding: 16px; }
/* ---- Pied de page ----------------------------------------------------------------
   ⚠⚠ TROIS SECTIONS, ET UNE GRILLE PLUTÔT QU'UNE RANGÉE FLEXIBLE (demande du client :
   « add footer 3 section : localisation de magasin place holder, details / contact, useful
   links »). En `flex` avec `space-between`, les trois blocs se partageaient la largeur au
   PRORATA DE LEUR CONTENU : la colonne « liens utiles » se réduisait à la largeur de ses mots, et
   deux pieds de page différents (une adresse longue, une adresse courte) ne se ressemblaient plus.
   Trois colonnes ÉGALES ne dépendent pas du texte qu'on y met — c'est ce qu'on veut d'un pied de
   page qui doit rester reconnaissable d'une page à l'autre.
   ⓘ `minmax(0, 1fr)` ET NON `1fr` : sans le minimum à zéro, une adresse sans espace (une URL, un
   mot arabe long) élargirait sa colonne au-delà du tiers et ferait DÉBORDER la page entière. */
.shop-footer { border-top: 1px solid var(--shop-border); background: var(--shop-bg); margin-top: 32px; }
.shop-footer-inner {
  max-width: 1280px; margin: 0 auto; padding: 20px 16px;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px;
  font-size: 12px; color: var(--shop-muted);
}
/* ⚠ PAS DE MAJUSCULES FORCÉES COMME SEUL REPÈRE : `text-transform` est IGNORÉ par l'arabe (cette
   écriture n'a pas de casse), donc si le titre ne se distinguait QUE par les capitales, la version
   arabe n'aurait plus aucun titre de section. Il se distingue par la graisse ET la couleur, qui
   fonctionnent dans les deux langues ; les capitales ne sont qu'un supplément pour le français. */
.shop-footer-title {
  font-size: 12px; font-weight: 700; color: var(--shop-text);
  margin: 0 0 8px; text-transform: uppercase; letter-spacing: 0.04em;
}
.shop-footer-text { margin: 0 0 6px; line-height: 1.5; }
.shop-footer-strong { font-weight: 600; color: var(--shop-text); }
/* L'emplacement vide de la localisation : présent, lisible, et visiblement PAS renseigné. */
.shop-footer-placeholder { margin: 0; font-style: italic; }
.shop-footer-map { display: inline-block; margin-top: 2px; }
/* ⭐⭐⭐ LA CARTE INTÉGRÉE DU PIED DE PAGE (demande du client : « add emebed for map localiastion »).
   ⚠ LA BORDER-RADIUS EST SUR LE CONTENEUR, PAS SUR L'IFRAME : un cadre venu d'ailleurs (Google) peint
   ses propres coins, et un `border-radius` posé sur lui est ignoré par les navigateurs qui le rendent
   dans son propre document — c'est le conteneur qui coupe. `overflow: hidden` va avec.
   ⚠ LA HAUTEUR EST EN PIXELS ET NON EN POURCENTAGE : le conteneur n'a pas de hauteur propre, et une
   iframe en `height: 100%` se réduirait à zéro (le classique « la carte ne s'affiche pas »). 180 px
   montrent un carrefour et deux rues, ce qui suffit à situer le magasin au doigt ; 450 px (la valeur
   du code d'intégration) mangeraient la moitié de la page sur un téléphone.
   ⚠ `display: block` SUR L'IFRAME : un cadre est un élément EN LIGNE, donc posé sur la ligne de base
   du texte — il laisse sous lui un blanc d'environ 5 px qui ressemble à un défaut de marge. */
.shop-footer-map-embed {
  margin-top: 10px; overflow: hidden;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius);
}
.shop-footer-map-embed iframe { display: block; width: 100%; height: 180px; border: 0; }
.shop-footer-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }

/* ---------------------------------------------------------------- catalogue */
.catalog { display: grid; grid-template-columns: 240px 1fr; gap: 20px; align-items: start; }
@media (max-width: 900px) { .catalog { grid-template-columns: 1fr; } }

.catalog-sidebar { border: 1px solid var(--shop-border); border-radius: var(--shop-radius); padding: 12px; background: #fff; }
/* ⭐⭐⭐ SUR GRAND ÉCRAN, LA COLONNE DES FILTRES RESTE EN PLACE ET SE DÉFILE TOUTE SEULE
   (demandes du client : « fix la table catégories et le filtre en mode PC » — la colonne s'étirait
   sur toute la hauteur de la page —, puis « fix in place the category and filters menu — it still
   scrolls » : bordée en hauteur, elle quittait quand même l'écran dès qu'on descendait dans les
   produits).
   ⚠⚠ LE DÉFAUT, EN DEUX TEMPS. La grille du catalogue est en `align-items: start` : la colonne
   prenait la hauteur de SON CONTENU — les catégories, leurs sous-familles, les marques et la
   fourchette de prix, soit bien plus qu'un écran sur un vrai catalogue. La page devenait donc haute
   comme la liste, et il fallait descendre sous des centaines de lignes de catégories pour voir autre
   chose que des filtres. La borner en hauteur a réglé la page, mais pas l'usage : la colonne
   disparaissait vers le haut dès les premiers produits, et il fallait remonter tout en haut pour
   changer de catégorie.
   ⭐ LES DEUX RÈGLES VONT DONC ENSEMBLE : elle est COLLANTE (`position: sticky`, elle reste sous
   l'en-tête pendant qu'on parcourt les produits) ET BORNÉE (`max-height`, c'est elle qui défile à
   l'intérieur — les catégories, les marques et les boutons restent tous atteignables).
   ⚠⚠ LE DÉCALAGE DU HAUT EST MESURÉ, PAS DEVINÉ (`--shop-header-h`, posée par public/shop/shop.js).
   L'en-tête du site est lui-même collant (`top: 0`) et sa hauteur N'EST PAS FIXE : une ligne en
   français, deux quand la recherche passe dessous, une de plus quand les langues s'empilent — et
   il est OPAQUE et AU-DESSUS (`z-index: 20`). Avec une valeur écrite en dur, la colonne glisserait
   SOUS lui et ses premiers filtres seraient recouverts. La variable est le seul moyen de coller une
   colonne sous un en-tête dont la hauteur n'est connue qu'à l'exécution.
   ⓘ Sans JavaScript, la variable vaut `0px` : la colonne reste collante au bord haut de la fenêtre
   et garde sa hauteur bornée — elle reste utilisable, simplement elle peut passer sous l'en-tête.
   Sur téléphone (≤ 900 px), RIEN DE TOUT CELA : les filtres sont un TIROIR, qui a déjà son propre
   défilement et sa propre barre (voir plus bas). */
@media (min-width: 901px) {
  .catalog-sidebar {
    position: sticky;
    top: calc(var(--shop-header-h, 0px) + 12px);
    /* ⚠⚠ `max(220px, …)` N'EST PAS DÉCORATIF : sur une fenêtre TRÈS plate (un portable de 600 px
       de haut avec un en-tête de 200 px), le calcul donnerait une hauteur nulle ou négative, et la
       colonne des filtres disparaîtrait complètement — le client n'aurait plus aucun filtre. On
       garde donc un plancher utilisable : la colonne peut alors dépasser légèrement le bas de
       l'écran, ce qui vaut mieux qu'une colonne vide. */
    max-height: max(220px, calc(100vh - var(--shop-header-h, 0px) - 24px));
    overflow-y: auto;
    /* ⚠ Le défilement de la colonne ne se « propage » pas à la page quand elle arrive en bas :
       sans cela, la molette bloquerait en fin de liste, comme si la page était figée. */
    overscroll-behavior: contain;
  }
}
.sidebar-title { margin: 0 0 8px; font-size: 14px; }
/* ⭐ L'EN-TÊTE DU TIROIR ET LE BOUTON DU TIROIR N'EXISTENT PAS SUR GRAND ÉCRAN.
   Sur grand écran, les filtres SONT une colonne visible : il n'y a rien à ouvrir ni à fermer,
   et un bouton « Fermer » y serait un contrôle sans objet. Ils ne sont révélés que par le
   mode tiroir (`body.is-drawer-ready`, posé par le script sous 900 px) — voir plus bas. */
.sidebar-head { display: none; }
.sidebar-close { display: none; }
.catalog-filters-btn { display: none; }
/* Le voile du tiroir : masqué par défaut, il n'apparaît qu'avec le mode tiroir. */
.catalog-backdrop { display: none; }

/* ⭐⭐ LE MODE TIROIR DES FILTRES — `body.is-drawer-ready`, POSÉ PAR LE SCRIPT SOUS 900 px.
   ⚠⚠ POURQUOI UNE CLASSE, ET POURQUOI ELLE VIENT DU SCRIPT. Sans JavaScript, le tiroir ne
   peut pas s'ouvrir : si la feuille de style avait mis les filtres hors écran d'emblée, le
   catalogue serait devenu INFILTRABLE — le client verrait une liste de produits sans moyen de
   filtrer ni de choisir une catégorie, ce qui est bien pire que le bloc empilé d'avant. La
   mise hors écran est donc conditionnée à une classe que SEUL le script sait poser : sans lui,
   tout reste dans le flux, exactement comme avant.
   ⚠ 900 px EST LA MÊME BORNE QUE `.catalog` (une colonne) : au-dessus, les filtres sont une
   colonne à côté de la grille et doivent rester visibles ; en dessous, ils s'empilaient
   AU-DESSUS des produits — c'est ce que le tiroir corrige. Une borne différente des deux côtés
   (CSS d'un côté, `matchMedia` de l'autre) produirait un état où le bouton ouvre un tiroir
   par-dessus une colonne visible, ou l'inverse. */
@media (max-width: 900px) {
  /* ⭐⭐ LE VOILE N'EXISTE QUE TIROIR OUVERT — `is-drawer-open`, ET RIEN D'AUTRE.
     ⚠⚠ LA RÈGLE PRÉCÉDENTE ÉTAIT `body.is-drawer-ready`, ET ELLE A RENDU TOUTE LA BOUTIQUE
     INTAPPABLE SUR TÉLÉPHONE. `is-drawer-ready` est posée dès qu'on est sous 900 px, donc sur
     n'importe quel téléphone, en permanence : le voile — plein écran, `z-index: 50` — restait
     posé sur la page, et chaque tap partait dedans. Aucun message, aucun indice visuel si la
     teinte se confond avec le fond : juste une boutique qui ne répond plus.
     ⓘ La visibilité a désormais UN SEUL propriétaire : la classe `is-drawer-open`. Le script
     n'y touche plus (il ne fait qu'ajouter ou retirer la classe), et le gabarit rend le voile
     `hidden` — donc rien sans JavaScript. */
  body.is-drawer-ready .catalog-backdrop { display: none; }
  body.is-drawer-ready.is-drawer-open .catalog-backdrop {
    display: block; position: fixed; inset: 0; z-index: 50;
    width: 100%; height: 100%; padding: 0; margin: 0; border: 0;
    background: rgba(15, 23, 42, .45); cursor: pointer;
  }

  /* Le bouton qui ouvre le tiroir, dans la barre d'outils du catalogue (donc au-dessus de la
     grille, sous les yeux). Il n'apparaît QUE dans le mode tiroir : sans tiroir, il n'ouvrirait
     rien (le gabarit le rend `hidden`, et cette règle est la seule à l'afficher). */
  body.is-drawer-ready .catalog-filters-btn {
    display: inline-flex; align-items: center; gap: 6px;
    min-height: 44px; padding: 6px 12px; cursor: pointer;
    border: 1px solid var(--shop-border); border-radius: var(--shop-radius);
    background: #fff; color: var(--shop-navy); font-size: 13px; font-weight: 600;
  }

  /* LE PANNEAU : hors écran par défaut, il glisse depuis le bord d'ATTACHE quand le tiroir
     s'ouvre.

     ⚠⚠ LE HORS-ÉCRAN EST EXPRIMÉ AVEC LA PROPRIÉTÉ **LOGIQUE** `inset-inline-start`, ET C'EST
     LA CORRECTION D'UN DÉFAUT QUI N'APPARAISSAIT QU'EN ARABE : « la catégorie n'affiche qu'un
     voile ».
     Ce qui se passait : le hors-écran était fait par une translation `translateX(-100%)`, avec
     une SECONDE règle pour l'arabe — `html[dir="rtl"] body.is-drawer-ready .catalog-sidebar`,
     qui translatait de `+100%`. Or cette seconde règle est PLUS SPÉCIFIQUE (3 classes ou
     attributs + 2 éléments) que la règle d'ouverture — `body.is-drawer-open .catalog-sidebar`,
     2 + 1 : en arabe, elle continuait donc de gagner TIROIR OUVERT, et le panneau restait
     translaté d'une largeur d'écran vers la droite, hors du champ. Le voile, lui, s'affichait —
     d'où l'impression d'un tiroir vide.
     `inset-inline-start` est LOGIQUE : il vaut `left` en français et `right` en arabe, tout seul.
     Il n'y a donc plus AUCUNE règle propre à une direction, donc plus rien qui puisse battre
     l'état ouvert — la cause est supprimée, pas compensée par une règle de plus.
     ⓘ Le pourcentage d'un `inset` se calcule sur la largeur du bloc conteneur (l'écran) : le
     panneau part donc un peu plus loin que nécessaire, ce qui ne se voit pas — il n'entre en
     scène que pendant la fin de la glissade.
     ⚠⚠ AUCUNE ACCOLADE DANS CE COMMENTAIRE, ET CE N'EST PAS UN HASARD : plusieurs contrôles du
     dépôt lisent cette feuille par expression régulière (découpage des règles sur les accolades),
     et des accolades dans un commentaire les font déraper — le contrôle examine alors un faux
     sélecteur et peut aussi bien rater un défaut qu'en inventer un. On décrit donc les
     sélecteurs en toutes lettres plutôt qu'en les citant. */
  body.is-drawer-ready .catalog-sidebar {
    position: fixed; z-index: 60; top: 0; bottom: 0;
    /* Hors écran du côté d'ATTACHE — à gauche en français, à droite en arabe. */
    inset-inline-start: -100%;
    width: min(88vw, 360px); max-width: none;
    margin: 0; padding: 12px;
    /* Les bords qui touchent l'écran n'ont plus de cadre : c'est un panneau, pas une carte. */
    border: 0; border-inline-end: 1px solid var(--shop-border); border-radius: 0;
    background: #fff; overflow-y: auto; overscroll-behavior: contain;
    box-shadow: 0 0 24px rgba(15, 23, 42, .18);
    /* ⚠ `visibility` EST DANS LA TRANSITION, AVEC UN DÉLAI DANS LE SENS DE LA FERMETURE.
       Sans elle, le panneau resterait « présent » hors écran : un utilisateur au clavier
       tabulerait dans des champs invisibles, et un lecteur d'écran les annoncerait. Le délai
       (0,22 s) laisse la glissade se terminer avant de masquer — sans lui, la fermeture
       serait un clignotement. À l'ouverture, le délai est remis à zéro : le panneau devient
       visible immédiatement, sinon on le verrait arriver de nulle part. */
    visibility: hidden;
    transition: inset-inline-start .22s ease, visibility 0s linear .22s;
  }

  /* ⚠ `is-drawer-ready` EST RAPPELÉE ICI, ET PAS SEULEMENT `is-drawer-open`. Deux raisons :
     la règle d'ouverture doit être STRICTEMENT plus spécifique que celle du repos (c'est ce qui
     garantit qu'aucun état ne puisse la battre), et elle ne veut rien dire hors du mode tiroir. */
  body.is-drawer-ready.is-drawer-open .catalog-sidebar {
    inset-inline-start: 0; visibility: visible;
    transition: inset-inline-start .22s ease, visibility 0s;
  }

  /* L'en-tête du tiroir : le titre et le bouton de fermeture. Visibles seulement ici. */
  body.is-drawer-ready .sidebar-head {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    margin-bottom: 8px;
  }
  body.is-drawer-ready .sidebar-head .sidebar-title { margin: 0; }
  body.is-drawer-ready .sidebar-close {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 44px; min-height: 44px; padding: 0; cursor: pointer;
    border: 1px solid var(--shop-border); border-radius: var(--shop-radius);
    background: #fff; color: var(--shop-muted);
  }
  body.is-drawer-ready .sidebar-close:hover { color: var(--shop-navy); }
  /* Le tiroir ne rétrécit plus le catalogue : les filtres n'y sont plus une colonne de la
     grille, et laisser `240px` de piste laisserait une bande vide à côté des produits. */
  body.is-drawer-ready .catalog { grid-template-columns: 1fr; }
}

.sidebar-group { margin-top: 12px; }
.sidebar-group-title { font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--shop-muted); margin-bottom: 4px; }
.sidebar-check { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 13px; cursor: pointer; }
.sidebar-tree, .sidebar-subtree { list-style: none; margin: 0; padding: 0; }
/* Indentation logique : les sous-familles se décalent vers l'INTÉRIEUR de la page, donc
   à droite en LTR et à gauche en RTL (c'est `padding-inline-start` qui le sait). */
.sidebar-subtree { padding-inline-start: 16px; }
.sidebar-price { display: flex; align-items: center; gap: 6px; }
.sidebar-price input { width: 72px; padding: 4px 6px; border: 1px solid var(--shop-border); border-radius: var(--shop-radius-sm); }
.sidebar-actions { display: flex; gap: 6px; margin-top: 14px; }

.catalog-toolbar { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 12px; }
.catalog-count { font-size: 13px; color: var(--shop-muted); }
.catalog-sort { font-size: 13px; color: var(--shop-muted); display: flex; align-items: center; gap: 6px; }
.catalog-sort select { padding: 4px 6px; border: 1px solid var(--shop-border); border-radius: var(--shop-radius-sm); }
/* Pousse les boutons 2/3 colonnes à l'extrémité de la ligne : à droite en LTR, à gauche
   en RTL — c'est bien la FIN de la ligne dans les deux cas. */
.catalog-cols { margin-inline-start: auto; display: flex; gap: 4px; }
.catalog-cols button { border: 1px solid var(--shop-border); background: #fff; padding: 3px 8px; cursor: pointer; border-radius: var(--shop-radius-sm); font-size: 12px; }
.catalog-cols button.is-active { border-color: var(--shop-accent); color: var(--shop-accent); }

/* ⭐⭐ LE CHOIX « 1 OU 2 PRODUITS PAR LIGNE » NE CONCERNE QUE LE TÉLÉPHONE.
   ⚠ IL EST MASQUÉ ICI (`display: none`) ET RÉVÉLÉ SOUS 600 px, VOIR PLUS BAS. Deux
   réglages de grille ne doivent JAMAIS être visibles en même temps : le client qui
   choisirait « 3 colonnes » sur un écran où c'est « 2 par ligne » qui décide ne verrait
   rien changer, et croirait le bouton cassé. Sur grand écran, ce groupe n'existe donc pas
   — la disposition du bureau est strictement celle d'avant. */
.catalog-mcols { display: none; margin-inline-start: auto; gap: 4px; }
.catalog-mcols button { border: 1px solid var(--shop-border); background: #fff; padding: 3px 8px; cursor: pointer; border-radius: var(--shop-radius-sm); font-size: 12px; }
.catalog-mcols button.is-active { border-color: var(--shop-accent); color: var(--shop-accent); }

.grid { display: grid; gap: 16px; }
.grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
.grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
.grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1100px) { .grid.cols-3, .grid.cols-4 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .grid.cols-2, .grid.cols-3, .grid.cols-4 { grid-template-columns: 1fr; } }

/* ⭐⭐ MOBILE : 1 OU 2 VIGNETTES PAR LIGNE, AU CHOIX DU CLIENT.
   ⚠⚠ CES RÈGLES DOIVENT SUIVRE CELLES DE `cols-*`, ET C'EST TOUT LE MÉCANISME. Les deux
   sélecteurs ont la MÊME spécificité (`.grid.cols-3` et `.grid.mcols-2` : deux classes) :
   à spécificité égale, c'est la DERNIÈRE déclarée dans la feuille qui gagne. Écrites plus
   haut, elles laisseraient la grille du bureau décider, et le bouton ne ferait rien.
   ⚠ LE DÉFAUT NE CHANGE PAS : `mcols-1` reproduit exactement ce que la règle ci-dessus
   faisait déjà (une colonne par ligne). Le client qui ne touche à rien ne voit aucune
   différence ; il gagne seulement le droit de passer à deux.
   ⓘ `repeat(2, 1fr)` ET NON `repeat(auto-fill, minmax(...))` : sur un écran de 375 px,
   l'automatisme donnerait UNE colonne (une vignette ne descend pas sous 200 px sans que
   le nom et le bouton se coupent), donc le choix « 2 par ligne » ne s'appliquerait jamais.
   ⚠ LA GRILLE NE RÉTRÉCIT RIEN ELLE-MÊME : ce sont les vignettes qui s'adaptent (voir le
   bloc mobile plus bas, qui garde des cibles de 44 px). */
@media (max-width: 600px) {
  .grid.mcols-1 { grid-template-columns: 1fr; }
  .grid.mcols-2 { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  /* Deux vignettes côte à côte : un peu moins de rembourrage, sinon le nom et le bouton
     se coupent, et la vignette perd exactement ce qu'on venait y lire.
     ⚠ ON NE TOUCHE PAS À LA HAUTEUR DES CIBLES (44 px, voir le bloc mobile plus bas) :
     la vignette s'allonge d'une ligne, c'est tout — un titre de 34 px de haut se rate au
     doigt, et le client a déjà accepté des vignettes plus petites en choisissant 2. */
  .grid.mcols-2 .card { padding: 6px; }
  /* Les deux réglages s'échangent : le bureau (2/3 colonnes) disparaît, le mobile (1/2 par
     ligne) apparaît. Un seul contrôle à la fois, et celui qui agit vraiment. */
  .catalog-cols { display: none; }
  .catalog-mcols { display: flex; }
}

/* ⚠⚠ ÉCRANS TRÈS ÉTROITS (320 px, iPhone SE) : LA GRILLE À DEUX COLONNES N'Y TIENT PAS
   ENTIÈREMENT. À 320 px, chaque vignette dispose d'environ 143 px, dont 12 px de
   rembourrage : le stepper de quantité (44 + 52 + 44 = 140 px, cibles de 44 px du bloc
   mobile) déborderait de sa propre grille de 9 px — et c'est la VIGNETTE qui déborde, pas
   la page, ce qui se voit immédiatement.
   On réduit donc la LARGEUR des deux boutons, et seulement dans le mode « 2 par ligne »
   que le client a explicitement demandé : leur HAUTEUR reste de 44 px, donc la cible du
   doigt reste haute comme partout ailleurs. Au-delà de 360 px, rien ne change. */
@media (max-width: 360px) {
  /* ⚠ `height` AUSSI, ET PAS SEULEMENT `width` : les boutons d'unité sont RONDS (`border-radius:
     50%`), donc une largeur réduite sans hauteur réduite en ferait des ovales. */
  .grid.mcols-2 .qty-btn { width: 34px; height: 34px; }
  .grid.mcols-2 .qty-input { width: 38px; }
}
.catalog-empty { color: var(--shop-muted); padding: 24px; text-align: center; }

/* ⚠ LA PAGINATION N'EST PLUS QU'UN REPLI, ET ELLE VIT DANS UN `<noscript>` : elle ne
   s'affiche que si le script ne tourne pas (voir le gabarit du catalogue). Le catalogue se
   charge désormais au défilement, 10 articles par 10. */
.catalog-pager { display: flex; align-items: center; justify-content: center; gap: 16px; margin-top: 20px; }
.pager-link { border: 1px solid var(--shop-border); padding: 5px 12px; border-radius: var(--shop-radius); }
.pager-info { font-size: 13px; color: var(--shop-muted); }

/* ---- Chargement au défilement (voir public/shop/shop.js) ---- */
/* ⚠ LA SENTINELLE EST UN REPÈRE, PAS UN ÉLÉMENT VISIBLE : une hauteur d'un pixel suffit à
   l'observateur, et un bloc plus haut laisserait un vide en bas de page. */
.catalog-sentinel { height: 1px; }
.catalog-more { display: flex; flex-direction: column; align-items: center; gap: 8px; margin-top: 20px; }
/* La place du message est RÉSERVÉE (`min-height`) : sans cela, chaque changement de texte
   (Chargement… → 30 sur 6 452 articles) décalerait le bas de page d'une ligne, et le doigt
   qui visait le bouton tomberait à côté. */
.catalog-more-status { margin: 0; min-height: 18px; font-size: 13px; color: var(--shop-muted); text-align: center; }

/* ---------------------------------------------------------------- carte produit */
.card { border: 1px solid var(--shop-border); border-radius: var(--shop-radius); padding: 10px; background: #fff; display: flex; flex-direction: column; gap: 8px; }
.card-ref { font-size: 11px; color: var(--shop-muted); letter-spacing: .3px; }
.card-image { position: relative; aspect-ratio: 1 / 1; display: flex; align-items: center; justify-content: center; background: var(--shop-bg); border-radius: var(--shop-radius-sm); overflow: hidden; }
.card-image img { max-height: 100%; max-width: 100%; object-fit: contain; }
.card-image-empty { color: #94a3b8; font-size: 12px; }
/* Pastilles posées sur l'image : rupture d'un côté, « vedette » de l'autre. Les deux
   restent aux DEUX coins opposés en RTL (elles permutent, ce qui est le comportement
   attendu : chaque information garde son coin « début » ou « fin » de lecture). */
.card-out { position: absolute; top: 6px; inset-inline-start: 6px; background: #b91c1c; color: #fff; font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: var(--shop-radius); }
.card-featured { position: absolute; top: 6px; inset-inline-end: 6px; background: var(--shop-accent); color: #fff; font-size: 10px; font-weight: 700; padding: 1px 6px; border-radius: var(--shop-radius); }
/* ⭐⭐⭐ LE NOM DU PRODUIT EST CENTRÉ, PLUS GRAND ET EN GRAS (demande du client).
   POURQUOI. C'est le seul texte que le client LIT vraiment dans une vignette : il doit
   arrêter l'œil avant le prix et le bouton, et se centrer au-dessus d'eux comme une
   étiquette. ⚠ `text-align: center` EST ICI UNE EXCEPTION ASSUMÉE à la règle « propriétés
   logiques » de cette feuille : centrer n'a pas de sens de lecture (le texte se centre dans
   les deux langues), donc aucune forme logique n'existe pour cette intention.
   ⚠ `min-height` est passée de 38 px à 40 px et le texte peut tenir sur DEUX lignes : une
   vignette dont le nom passe à trois lignes (ou dont le nom est plus long) ne doit pas
   décaler les voisines — d'où la hauteur plancher, qui garde les vignettes d'une grille
   alignées entre elles quelle que soit la longueur des désignations. */
.card-name {
  color: var(--shop-accent); font-weight: 700; font-size: 15px; line-height: 1.3;
  text-align: center; min-height: 40px;
}
.card-name:hover { color: var(--shop-accent-dark); }

/* ⭐⭐ LA « QUANTITÉ / BOÎTE », SOUS L'IMAGE DE LA VIGNETTE (contrat ERP : `qty_per_box`).
   Elle se lit à sa place, juste sous la photo qu'elle commente, et reste discrète : c'est une
   information de CONDITIONNEMENT, pas une accroche. ⚠ Le gabarit ne l'écrit que lorsqu'elle
   existe : la règle ne prévoit donc aucun état vide à masquer. */
.card-box-qty { font-size: 12px; color: var(--shop-muted); text-align: center; }

/* ⭐⭐ PRIX ET QUANTITÉ SUR LA MÊME LIGNE. Le prix était au-dessus du stepper, sur une ligne
   à lui : le client lisait un montant puis devait descendre d'une ligne pour dire COMBIEN il
   en voulait — deux informations qui n'ont de sens qu'ensemble.
   ⚠ `flex-wrap: wrap` N'EST PAS UNE CONTRADICTION : les deux restent sur une seule ligne
   partout où la place existe (c'est le cas normal, y compris à 375 px). Le repli n'intervient
   qu'à 320 px en mode « deux produits par ligne », où la vignette fait 143 px : plutôt qu'un
   débordement horizontal (qui déplacerait la PAGE entière), le prix passe au-dessus du
   stepper. Le défaut est ainsi borné au seul cas qui ne peut pas les tenir côte à côte. */
.card-buy { display: flex; align-items: center; justify-content: space-between; gap: 6px; flex-wrap: wrap; }

/* Zone prix : elle disparaît complètement si le visiteur n'y a pas droit. */
.card-price { font-size: 16px; font-weight: 800; }
.card-price-value { color: var(--shop-navy); }
.card-price-cur { font-size: 11px; color: var(--shop-muted); font-weight: 400; }

/* ⭐⭐⭐ LE COMPTEUR DE QUANTITÉ — DEUX COLONNES DE PILULES EMPILÉES, LE CHAMP AU MILIEU
   (voir partials/qtyControl.hbs). Sur l'écran, article qui déclare ses deux conditionnements :

       − PAQUET 12        + PAQUET 12
       − CARTON 120       + CARTON 120
           [  1  ]
           2 paquets + 1

   ⭐ LE DESSIN VIENT DE LA MAQUETTE DU CLIENT (« stacked quantity pills » : pilules, vert pour
   l'ajout, rouge pour le retrait), CORRIGÉE par ses trois retours suivants : « values inside the
   pill », « less round, soft corners, slim pills », « indicator of number of paquet/carton ».
   ⚠⚠ LA MAQUETTE EST EN TAILWIND + INTER CHARGÉS PAR CDN : la boutique ne charge RIEN d'extérieur
   (elle doit fonctionner hors ligne, et une police distante changerait l'affichage d'un jour à
   l'autre), donc le dessin est TRADUIT ici, en CSS maison, avec les couleurs et les états de la
   maquette.

   ⚠⚠ `flex-wrap: wrap` N'EST PAS DÉCORATIF : le contrôle demande ~290 px. Sur une fiche produit
   étroite ou une colonne de panier serrée, il passe donc à la ligne SUIVANTE au lieu de déborder de
   la page (le défaut que le client a signalé : « make responsive for large numbers in cart and in
   product detail »). */
.card-qty {
  display: flex; flex-direction: column; gap: 3px;
  width: fit-content; font-size: 15px;
}
.qty-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.qty-col { display: flex; flex-direction: column; gap: 5px; }

/* ⭐⭐ LA PILULE : le signe, le mot, ET sa quantité. ⚠ La valeur est revenue dedans (demande du
   client : « add qty/carton and qty/paquet values inside the pill ») — c'est elle qui dit si l'on
   ajoute 6 ou 144, et le mot seul laissait la question ouverte.
   ⚠⚠ « LESS ROUND, SOFT CORNERS, SLIM PILLS » : plus de pilule en gélule (`border-radius: 999px`),
   mais un rectangle à coins doux (9 px), et surtout plus FIN — 30 px de haut au lieu de 40, moins
   d'air à l'intérieur : deux pilules empilées plus le champ doivent tenir dans une colonne de
   panier. ⚠ `min-height: 40px` (44 px sur téléphone) n'est PAS conservé ici : une pilule de 44 px
   ferait 96 px de haut à deux, dans une ligne de tableau de 60 px. La cible tactile du téléphone
   est traitée plus bas (`@media`), pas en permanence.
   ⚠ `touch-action: manipulation` évite le délai de 300 ms du double-tap. */
.qty-pill {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  min-width: 104px; min-height: 30px; padding: 3px 10px;
  border-radius: var(--shop-radius-soft); cursor: pointer; white-space: nowrap;
  user-select: none; touch-action: manipulation;
  transition: background-color .15s cubic-bezier(.4, 0, .2, 1),
              transform .15s cubic-bezier(.4, 0, .2, 1),
              box-shadow .15s cubic-bezier(.4, 0, .2, 1);
}
/* ⭐ VERT POUR L'AJOUT, ROUGE POUR LE RETRAIT (couleurs de la maquette) : le sens du geste se lit
   avant le mot, ce qui compte sur un écran de téléphone. */
.qty-pill-plus { background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; }
.qty-pill-plus:hover { background: #d1fae5; }
.qty-pill-moins { background: #fff1f2; color: #be123c; border: 1px solid #fecdd3; }
.qty-pill-moins:hover { background: #ffe4e6; }
.qty-pill:hover { transform: translateY(-1px); box-shadow: 0 4px 12px -2px rgba(0, 0, 0, .05); }
.qty-pill:active { transform: translateY(1px) scale(.97); }
.qty-pill:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
.qty-pill-sign { font-size: 14px; font-weight: 700; line-height: 1; }
.qty-pill-word { font-size: 10.5px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; }
/* ⚠ LE NOMBRE EST PLUS GROS QUE LE MOT : c'est lui qu'on lit d'un coup d'œil (« 12 » ou « 120 »), le
   mot ne sert qu'à lever l'ambiguïté entre les deux conditionnements. */
.qty-pill-num { font-size: 12.5px; font-weight: 800; font-variant-numeric: tabular-nums; }

/* ⭐⭐⭐ L'INDICATEUR (« 2 paquets + 1 ») : ce que la quantité représente en conditionnements.
   ⚠ IL EST DISCRET MAIS TOUJOURS LÀ, sous le compteur : c'est une LECTURE, pas un réglage — il ne
   doit donc pas prendre la place du geste. ⓘ `tabular-nums` fige la largeur des chiffres : sans lui,
   la ligne « saute » à chaque changement de valeur. */
.qty-split {
  font-size: 11.5px; color: var(--shop-muted); font-variant-numeric: tabular-nums;
  padding-inline-start: 2px; white-space: nowrap;
}

/* ⭐⭐ LES BOUTONS D'UNITÉ `−` / `+` — LE COMPTEUR PAR DÉFAUT (voir partials/qtyControl.hbs).
   Ils apparaissent dans DEUX cas, et c'est le même dessin dans les deux :
     · l'article ne déclare AUCUN conditionnement → « revert to default +|qty|- » (demande du client) ;
     · la vignette du catalogue (`compact`), dont la tuile est trop étroite pour des pilules.
   ⚠⚠ ILS SONT NEUTRES, ALORS QUE LES PILULES SONT COLORÉES (vert / rouge) : la couleur dit « voici le
   paquet, voici le carton », pas « +1 ». Un stepper d'unité en vert et rouge crierait aussi fort
   qu'un conditionnement, et l'opérateur ne saurait plus ce qui est un lot et ce qui est une unité.
   ⚠⚠ ET ILS SUIVENT LA MÊME FORME QUE LES PILULES (coins doux, pas de rond) : depuis « less round,
   soft corners, slim pills », un cercle au milieu de rectangles à coins doux jurerait. ⓘ Ils
   remplacent le cadre gris de l'ancien compteur groupé : chaque bouton est une cible à lui, 34 px
   (44 px sur téléphone, plus bas). */
.qty-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; flex: 0 0 auto;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius-soft);
  background: #fff; color: var(--shop-text);
  font-size: 18px; font-weight: 700; line-height: 1; cursor: pointer;
  user-select: none; touch-action: manipulation;
  transition: background-color .15s cubic-bezier(.4, 0, .2, 1),
              transform .15s cubic-bezier(.4, 0, .2, 1);
}
.qty-btn:hover { background: #eef2f7; }
.qty-btn:active { transform: translateY(1px) scale(.97); }
.qty-btn:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: 2px; }

/* ⭐⭐⭐ LE CHAMP GRANDIT AVEC LE NOMBRE (demande du client : « make responsive for large numbers in
   cart and in product detail »). ⚠⚠ IL AVAIT UNE LARGEUR FIXE DE 52 px, calibrée pour deux ou trois
   chiffres : depuis que le plafond de 9 999 est parti, une quantité de 12 500 y était COUPÉE — on
   ne voyait plus ce qu'on avait tapé, alors que la valeur enregistrée, elle, était juste.
   ⭐ LA LARGEUR SUIT LE NOMBRE DE CHIFFRES, publié par le script dans `--qty-ch`
   (`ajusterLargeurChamp`, public/shop/shop.js) : la largeur est calculée en `ch` — l'unité de la
   POLICE —, donc elle reste juste quand la police, le zoom ou la langue changent. `min-width` garde
   le même air pour « 1 » et pour « 12 500 ».
   ⭐ LA VALEUR EST ÉCRITE EN GROS ET EN GRAS (maquette : « the neutral / central counter ») : c'est
   elle qu'on vient lire et corriger. ⓘ `font-variant-numeric: tabular-nums` fige la largeur des
   chiffres : sans elle, la boîte « saute » à chaque changement de valeur (1 → 12 → 144).
   ⚠⚠ ON N'UTILISE PAS L'ATTRIBUT `size`, QUI SERAIT LE RÉFLEXE : il ne s'applique PAS à un champ de
   type `number` (la spécification l'exclut), donc la largeur n'aurait pas bougé dans les
   navigateurs — un correctif qui ne corrige rien, et invisible en test. */
.qty-input {
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius-sm); background: #f1f5f9;
  text-align: center; font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums;
  padding: 9px 8px; color: var(--shop-text);
  width: calc(var(--qty-ch, 3) * 1ch + 34px);
  min-width: 68px;
}
/* ⭐ LA VALEUR « POUSSE » QUAND ELLE CHANGE (maquette : `animate-pop`) : l'œil suit le clic même
   quand la boîte est petite. ⚠ ANIMATION RETIRÉE SI LE SYSTÈME DEMANDE MOINS DE MOUVEMENT
   (`prefers-reduced-motion`) — c'est une politesse, pas un détail : certains clients la règlent
   exprès, et une secousse à chaque clic devient vite désagréable. */
@keyframes qty-pop { 0% { transform: scale(1); } 50% { transform: scale(1.12); } 100% { transform: scale(1); } }
.qty-input.animate-pop { animation: qty-pop .22s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .qty-pill, .qty-input { transition: none; }
  .qty-pill:hover, .qty-pill:active { transform: none; box-shadow: none; }
  .qty-input.animate-pop { animation: none; }
}

.card-actions { display: flex; gap: 6px; align-items: stretch; margin-top: auto; }
.btn { border: 1px solid var(--shop-border); background: #fff; padding: 7px 12px; border-radius: var(--shop-radius); cursor: pointer; font-size: 13px; }
.btn:hover { background: var(--shop-bg); }
.btn-primary { background: var(--shop-accent); border-color: var(--shop-accent); color: #fff; font-weight: 600; }
.btn-primary:hover { background: var(--shop-accent-dark); }
.btn-primary[disabled] { background: #cbd5e1; border-color: #cbd5e1; cursor: not-allowed; }
.btn-quote { background: var(--shop-navy); border-color: var(--shop-navy); color: #fff; font-weight: 600; }
.btn-quote:hover { background: #0f172a; }
.card-add { flex: 1 1 auto; }
.btn-heart { border: 1px solid var(--shop-border); background: #fff; width: 36px; border-radius: var(--shop-radius); cursor: pointer; font-size: 15px; color: var(--shop-accent); }
.btn-heart:hover { background: #fff7ed; }
/* ⭐ LE CŒUR PLEIN DIT « CET ARTICLE EST DANS MA LISTE » (voir partials/productCard.hbs).
   Il sert surtout sur la liste d'envies elle-même, où le même geste RETIRE l'article : sans
   cette différence visible, le client lirait « ajouter » sous un article déjà ajouté.
   ⚠ DÉCLARÉ APRÈS `:hover` : à spécificité égale, c'est le dernier qui gagne, et l'état
   « déjà dans la liste » doit survivre au survol. */
.btn-heart.is-on { background: var(--shop-accent); border-color: var(--shop-accent); color: #fff; }
.btn-heart[disabled] { cursor: progress; opacity: .7; }

/* ---------------------------------------------------------------- fiche produit */
.breadcrumb { font-size: 12px; color: var(--shop-muted); margin-bottom: 12px; display: flex; gap: 6px; flex-wrap: wrap; }
.product { display: grid; grid-template-columns: minmax(280px, 420px) 1fr; gap: 28px; align-items: start; }
@media (max-width: 800px) { .product { grid-template-columns: 1fr; } }
.product-main-image { position: relative; aspect-ratio: 1/1; border: 1px solid var(--shop-border); border-radius: var(--shop-radius); display: flex; align-items: center; justify-content: center; background: var(--shop-bg); }
.product-main-image img { max-height: 100%; object-fit: contain; }
/* ⭐ LA PHOTO EST ENTOURÉE D'UN LIEN (`a.product-zoom`, voir views/shop/product.hbs) : il
   occupe toute la boîte, et l'image se cale dedans exactement comme avant (elle était fille
   directe de `.product-main-image`, elle est maintenant petite-fille — sans ces règles, une
   photo carrée déborderait du cadre car `max-height: 100%` se rapporterait à la hauteur du
   lien, qui est indéfinie).
   ⚠ `cursor: zoom-in` : le curseur annonce ce que fait le clic, et c'est le même que sur les
   vignettes du panier (voir `.qsel-thumb`). */
.product-zoom { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; cursor: zoom-in; }
.product-zoom img { max-width: 100%; max-height: 100%; }
.product-zoom:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: -2px; }
.product-thumbs { display: flex; gap: 8px; margin-top: 8px; }
.thumb { width: 64px; height: 64px; object-fit: contain; border: 1px solid var(--shop-border); border-radius: var(--shop-radius-sm); cursor: pointer; background: #fff; }
.thumb:hover { border-color: var(--shop-accent); }
/* ⭐⭐ LA VIGNETTE DE LA PHOTO AFFICHÉE (demande du client : « nav arrow for next variant picture
   when zoomed »). Elle est marquée à DEUX endroits : au chargement par le gabarit (la première),
   puis par le script dès qu'on change de vue — au doigt ou à la flèche de l'agrandissement.
   ⚠ LA BORDURE CHANGE AUSSI D'ÉPAISSEUR, ET ELLE EST COMPENSÉE PAR UNE MARGE NÉGATIVE : sans
   cela, la vignette marquée grandirait de 2 px et DÉCALERAIT toute la rangée à chaque changement
   de vue — un tremblement à chaque flèche. */
.thumb.is-active { border: 2px solid var(--shop-accent); margin: -1px; }
.product-ref { font-size: 12px; color: var(--shop-muted); }
.product-title { font-size: 22px; margin: 4px 0 8px; color: var(--shop-accent); }
.product-brand { font-size: 13px; color: var(--shop-muted); }
/* ⭐ Catégories SUPPLÉMENTAIRES (un article rangé aussi ailleurs, voir migrations/013) :
   volontairement discrètes — c'est une aide au repérage, pas une accroche commerciale —
   mais cliquables, car chaque libellé mène au filtre qui retrouve l'article. */
.product-extra-cats { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 6px 0; font-size: 12px; color: var(--shop-muted); }
.extra-cat { color: var(--shop-muted); text-decoration: none; border: 1px solid var(--shop-border); border-radius: var(--shop-radius-pill); padding: 1px 8px; }
.extra-cat:hover { border-color: var(--shop-accent); color: var(--shop-accent); }
.extra-cat-sep { margin: 0 2px; }
/* ⭐⭐⭐ LES TAGS DE LA FICHE PRODUIT (demande du client : « use tags in shop to improve search and
   visibility »). Ils viennent de la variable « D.C.I. » de l'ERP, devenue une liste de mots de
   classement. ⚠⚠ CHAQUE PASTILLE EST UN LIEN vers le catalogue filtré sur ce tag : c'est ce qui les
   rend utiles (le client découvre les autres articles du même usage) — le style dit donc « cliquable »
   (bordure, survol) au lieu de les habiller en étiquettes décoratives.
   ⚠ Propriétés LOGIQUES uniquement (`margin-inline`, `padding-inline`) : la même règle sert la page
   arabe retournée, sans un seul côté codé en dur. */
.product-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 8px 0; font-size: 12px; color: var(--shop-muted); }
.product-tags-label { color: var(--shop-muted); }
.product-tag {
  display: inline-flex;
  align-items: center;
  background: var(--shop-bg);
  color: var(--shop-navy);
  text-decoration: none;
  border: 1px solid var(--shop-border);
  border-radius: var(--shop-radius-pill);
  padding: 2px 10px;
}
.product-tag:hover { border-color: var(--shop-accent); color: var(--shop-accent); }
.product-price { margin: 12px 0; font-size: 26px; font-weight: 800; }
/* ⓘ Plus de `.product-tariff` : le niveau de tarif appliqué n'est plus affiché. */
.product-no-price { margin: 12px 0; padding: 10px; background: #fff7ed; border: 1px solid #fed7aa; border-radius: var(--shop-radius); color: #7c2d12; font-size: 13px; }
.product-stock { margin: 8px 0; font-size: 13px; }
.stock-in { color: #15803d; font-weight: 600; }
.stock-out { color: #b91c1c; font-weight: 600; }
/* ⭐⭐⭐ QUANTITÉ : LE MOT AU-DESSUS, LE CHAMP AU MILIEU, LE CONDITIONNEMENT EN DESSOUS (demande du
   client : « "quantity" word above input and x carton under it »). La rangée était en ligne
   (`align-items: center`), donc le mot « Quantité » se lisait À CÔTÉ du champ — l'œil devait
   chercher à quoi le champ appartenait. On empile : l'étiquette, le compteur, puis la taille du
   conditionnement.
   ⭐⭐⭐ TOUT EST CENTRÉ (« make the title "quantity" and the "number of box/packet" indicator centered,
   not aligned to left in fr / right in arabic »). Le bloc suivait le SENS DE LECTURE de la page :
   à gauche en français, à droite en arabe — donc le même compteur ne se lisait pas au même endroit
   selon la langue, alors que sa valeur, elle, est déjà centrée dans son champ. On aligne donc TOUT
   sur un axe unique : l'étiquette, le compteur (qui reste entier, `width: fit-content`), la ligne
   « × 120 par carton » et l'indicateur « 2 paquets + 1 carton ». ⓘ C'est plus juste qu'un
   `text-align: center` posé sur deux lignes seulement : centrer l'étiquette au milieu de la colonne
   et laisser le compteur au bord aurait décalé l'un par rapport à l'autre. */
.product-qty { margin: 12px 0; display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px; font-size: 13px; }
.product-qty-label { font-weight: 600; color: var(--shop-text); }
/* Le conditionnement (« × 120 par carton ») : discret, sous le champ, jamais confondu avec un prix.
   ⚠ Les chiffres restent lisibles dans une page arabe : le texte du serveur porte déjà le sens. */
.product-qty-pack { color: var(--shop-muted); font-size: 12px; }
/* ⚠ L'indicateur du compteur porte un `padding-inline-start: 2px` (voir `.qty-split`) qui, sur un
   texte centré, décalerait la ligne de 1 px selon la langue : on l'annule ici, pour que l'axe soit
   exactement le même en français et en arabe. */
.product-qty .qty-split { padding-inline-start: 0; }
.product-actions { display: flex; gap: 8px; margin: 14px 0; }
.product-actions .btn { padding: 10px 18px; }
.product-description { margin-top: 20px; border-top: 1px solid var(--shop-border); padding-top: 12px; }
.product-description h2 { font-size: 15px; }
.product-description p { margin: 8px 0; line-height: 1.55; white-space: pre-line; }

/* ---- Description riche (HTML de l'éditeur, assaini côté serveur) ----
   On lui redonne une mise en forme LISIBLE sans hériter des styles du site : le
   contenu vient d'un éditeur, donc ses balises (h2, ul, blockquote, img, iframe)
   doivent s'afficher correctement dans la colonne produit. */
.rt-rendered .rt-body { font-size: 14px; line-height: 1.6; }
.rt-rendered .rt-body > *:first-child { margin-top: 0; }
.rt-rendered .rt-body > *:last-child { margin-bottom: 0; }
.rt-rendered .rt-body h1, .rt-rendered .rt-body h2,
.rt-rendered .rt-body h3, .rt-rendered .rt-body h4 { margin: 14px 0 6px; line-height: 1.3; font-size: 15px; }
.rt-rendered .rt-body p { margin: 8px 0; white-space: normal; }
.rt-rendered .rt-body ul, .rt-rendered .rt-body ol { margin: 8px 0; padding-inline-start: 22px; }
.rt-rendered .rt-body li { margin: 3px 0; }
.rt-rendered .rt-body a { color: var(--shop-accent, #c8672a); }
.rt-rendered .rt-body blockquote {
  margin: 10px 0; padding: 4px 12px;
  border-inline-start: 3px solid var(--shop-border); color: var(--shop-muted, #6b7280);
}
.rt-rendered .rt-body img { max-width: 100%; height: auto; border-radius: var(--shop-radius); }
.rt-rendered .rt-body iframe {
  display: block; width: 100%; max-width: 640px; aspect-ratio: 16 / 9; height: auto;
  border: 0; border-radius: var(--shop-radius); background: #000;
}
.rt-rendered .rt-body pre, .rt-rendered .rt-body code {
  font-family: ui-monospace, Consolas, monospace; font-size: 13px; background: #f6f7f9;
}
.rt-rendered .rt-body pre { padding: 10px 12px; border-radius: var(--shop-radius); overflow-x: auto; }
.rt-rendered .rt-body table { border-collapse: collapse; width: 100%; margin: 10px 0; }
.rt-rendered .rt-body td, .rt-rendered .rt-body th { border: 1px solid var(--shop-border); padding: 6px 8px; }

/* ---- Médias intégrés dans la description (services/description.js) ----
   Une vidéo ou une image collée dans la description s'affiche ici, en grand.
   Le lecteur garde un rapport 16/9 quel que soit l'écran : sans cela, un iframe
   sans hauteur explicite s'affiche à 150 px et la vidéo devient inutilisable. */
.product-media { margin-top: 20px; border-top: 1px solid var(--shop-border); padding-top: 12px; }
.product-media h2 { font-size: 15px; margin-bottom: 10px; }
.media-item { margin: 0 0 16px; }
.media-video .media-embed {
  display: block; width: 100%; aspect-ratio: 16 / 9; height: auto;
  border: 0; border-radius: var(--shop-radius); background: #000;
}
.media-image img {
  display: block; max-width: 100%; height: auto;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius);
}
.media-item figcaption { margin-top: 6px; font-size: 13px; }
.media-item figcaption a { color: var(--shop-accent, #c8672a); text-decoration: none; }
.media-item figcaption a:hover { text-decoration: underline; }

/* ⭐⭐ VIDÉOS DU PRODUIT (fiche article → onglet « Vidéos », voir services/videos.js).
   ⚠ MÊME RÈGLE QUE LE LECTEUR DE LA DESCRIPTION : un <video> sans hauteur explicite s'affiche
   minuscule, et un lecteur minuscule ne sert à rien. On impose donc le rapport 16/9.
   ⚠ `height: auto` est indispensable en plus de `aspect-ratio` : certains navigateurs conservent la
   hauteur intrinsèque du fichier et dépassent le cadre. */
.product-videos { margin-top: 20px; border-top: 1px solid var(--shop-border); padding-top: 12px; }
.product-videos h2 { font-size: 15px; margin-bottom: 10px; }
.media-video-file { margin: 0 0 16px; }
.product-video {
  display: block; width: 100%; aspect-ratio: 16 / 9; height: auto;
  border-radius: var(--shop-radius); background: #000;
}
/* Le repli affiché DANS le lecteur quand le navigateur ne sait pas lire le fichier (le gabarit y
   met aussi un lien direct) : sans cette règle il hérite du blanc et devient illisible sur le noir. */
.product-video .hint { color: #fff; font-size: 13px; padding: 8px 10px; margin: 0; }
.product-video .hint a { color: var(--shop-accent, #c8672a); }
/* Forme « link » : on ne l'intègre pas, on l'annonce comme un lien. */
.product-video-link { margin: 0 0 10px; }
.product-video-link a { color: var(--shop-accent, #c8672a); text-decoration: none; }
.product-video-link a:hover { text-decoration: underline; }
.similar { margin-top: 36px; }
.similar h2 { font-size: 16px; }

/* ---------------------------------------------------------------- retour utilisateur */
/* ⚠ NON LOGIQUE, ET VOLONTAIRE : le toast est CENTRÉ horizontalement. `left: 50%` +
   `translateX(-50%)` centre dans les deux sens de lecture (un `inset-inline-start: 50%`
   donnerait un décalage inverse en RTL).
   ⚠⚠ `pointer-events: none` : UN MESSAGE D'INFORMATION NE DOIT JAMAIS INTERCEPTER UN TAP.
   Posé en bas de l'écran, le toast recouvrait la barre d'onglets du téléphone pendant ses
   3,5 secondes de vie : les taps sur « Compte », « Panier » ou « Envies » partaient dans le
   message au lieu du bouton. Même cause que le voile du tiroir : un élément décoratif qui
   reçoit les clics. Ici la garantie est STRUCTURELLE — il ne peut plus rien avaler, quelle que
   soit sa position ou sa durée. */
.shop-toast {
  position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
  background: var(--shop-navy); color: #fff; padding: 10px 18px; border-radius: var(--shop-radius);
  font-size: 13px; z-index: 100; box-shadow: 0 4px 14px rgba(0,0,0,.2);
  max-width: min(90vw, 560px); text-align: center;
  pointer-events: none;
}
.shop-toast.is-error { background: #b91c1c; }

/* ⭐⭐ LA SURCOUCHE D'AGRANDISSEMENT D'UNE VIGNETTE (voir public/shop/shop.js).
   Elle est créée par le script au clic sur `.qsel-thumb`, et retirée à la fermeture — donc
   elle n'existe PAS dans le HTML servi, et rien de tout cela ne s'affiche sans JavaScript
   (la vignette du panier reste alors une simple image, ce qui est exactement le repli voulu).

   ⚠ `inset: 0` ET NON `bottom: 0` : la surcouche couvre l'écran entier, et l'écrire ainsi
   évite de la confondre avec la barre de navigation fixe (le contrôle qui compte les
   éléments collés en bas de page s'en sert comme repère, voir test/shopChrome.test.js).
   ⚠ `z-index` AU-DESSUS de la barre d'onglets (45) et de l'en-tête (20), et AU-DESSOUS du
   toast (100) : un message d'information doit rester lisible par-dessus l'image agrandie. */
.shop-lightbox {
  position: fixed; inset: 0; z-index: 95;
  display: flex; align-items: center; justify-content: center;
  padding: 16px; background: rgba(15, 23, 42, .85);
  cursor: zoom-out;
}
/* ⚠ `max-height` EN `vh` : sans elle, une photo carrée de 2000 px remplirait l'écran et
   pousserait le reste hors du champ — on ne verrait qu'un coin de l'article. */
.shop-lightbox-img { max-width: 100%; max-height: 90vh; width: auto; height: auto; object-fit: contain; }

/* ⭐⭐⭐ LES FLÈCHES DE L'AGRANDISSEMENT (demande du client : « nav arrow for next variant picture
   when zoomed »). Elles n'existent QUE sur une fiche à plusieurs photos (le script ne les crée pas
   ailleurs) : une flèche sans autre photo à montrer serait un contrôle mort.
   ⚠ ELLES SONT POSÉES SUR LES BORDS LATÉRAUX, PAS AU-DESSUS DE L'IMAGE : au centre, elles
   masqueraient le détail qu'on est justement venu regarder.
   ⚠ 44 px DE CÔTÉ : c'est la taille d'une cible au doigt (le pouce ne vise pas un chevron de
   20 px). Le pictogramme, lui, reste petit.
   ⚠ LE CURSEUR `zoom-out` DU FOND EST ANNULÉ ICI : sur un bouton, la flèche du pointeur annonce
   « on clique pour changer de photo », pas « on clique pour fermer ». */
.shop-lightbox-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--shop-radius-pill);
  background: rgba(255, 255, 255, .18); color: #fff; font-size: 26px; line-height: 1;
  cursor: pointer;
}
.shop-lightbox-nav:hover, .shop-lightbox-nav:focus-visible { background: rgba(255, 255, 255, .34); }
.shop-lightbox-nav.is-prev { inset-inline-start: 8px; }
.shop-lightbox-nav.is-next { inset-inline-end: 8px; }
/* ⚠ LE CHEVRON SE RETOURNE AVEC LA LANGUE : en arabe, « la photo suivante » est à GAUCHE. Le
   script envoie le même geste (voir `sensLecture`) — les deux doivent dire la même chose. */
html[dir="rtl"] .shop-lightbox-arrow { display: inline-block; transform: scaleX(-1); }

/* ⭐⭐ LA FENÊTRE DE CONNEXION DU TÉLÉPHONE (`<dialog id="login-modal">`, voir layouts/main.hbs).
   Elle s'ouvre depuis l'onglet « Connexion » de la barre du bas, pour que le visiteur n'ait pas
   à quitter la page qu'il lisait (voir public/shop/shop.js).

   ⚠⚠ AUCUNE RÈGLE ICI NE POSE `display` SUR `.shop-modal` ELLE-MÊME, ET C'EST VOLONTAIRE.
   Une `<dialog>` fermée est masquée par la règle du navigateur `dialog:not([open])`. Or une
   déclaration d'AUTEUR l'emporte sur la feuille du navigateur : écrire `display: flex` ici
   afficherait la fenêtre EN PERMANENCE, ouverte ou non — c'est l'erreur classique de ce
   composant. On ne touche donc qu'à ce qui se voit une fois la fenêtre ouverte.
   ⚠ `z-index` : inutile. Une `<dialog>` ouverte par `showModal()` est placée dans la couche
   supérieure du navigateur (top layer), au-dessus de TOUT — y compris du toast (100) et de la
   surcouche d'image (95), qui ne peuvent donc pas passer devant. C'est une des raisons de
   préférer l'élément natif à une fenêtre fabriquée. */
.shop-modal {
  border: 0; padding: 0; border-radius: var(--shop-radius);
  /* Jamais plus large que l'écran : la marge de 12 px évite que la fenêtre touche les bords. */
  width: 100%; max-width: min(420px, calc(100vw - 24px));
  background: #fff; box-shadow: 0 18px 44px rgba(15, 23, 42, .3);
}
/* Le fond assombri. Le navigateur fournit `::backdrop` (donc le clic sur le fond est capté par
   la fenêtre, voir shop.js) ; il ne reste qu'à lui donner la couleur de la maison. */
.shop-modal::backdrop { background: rgba(15, 23, 42, .55); }
.shop-modal-inner { padding: 16px; }
.shop-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.shop-modal-title { margin: 0; font-size: 18px; }
/* ⚠ LA CIBLE DE FERMETURE FAIT 44 px, COMME TOUT LE RESTE SUR TÉLÉPHONE (voir le bloc mobile) :
   c'est le seul chemin de fermeture visible, et 36 px se rate au pouce. */
.shop-modal-close {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0; cursor: pointer;
  border: 1px solid var(--shop-border); border-radius: 50%; background: #fff; color: var(--shop-muted);
}
.shop-modal-close:hover { border-color: var(--shop-accent); color: var(--shop-accent); }
.shop-modal-lead { margin: 4px 0 0; color: var(--shop-muted); font-size: 13px; }

/* ⭐ LE BOUTON « HAUT DE PAGE » (voir layouts/main.hbs et public/shop/shop.js).
   ⚠ IL EST CACHÉ PAR DÉFAUT, À DEUX TITRES : l'attribut `hidden` du gabarit (donc caché même
   si le script ne tourne pas) et la règle `.shop-top[hidden]` ci-dessous, qui doit gagner
   contre le `display: inline-flex` de la règle suivante — d'où le sélecteur à deux classes.
   ⚠ `inset-inline-end` ET NON `right` : en arabe, le bouton se place du côté où le pouce arrive,
   c'est-à-dire à gauche — comme le reste des contrôles (voir la règle RTL en tête de fichier). */
.shop-top[hidden] { display: none; }
.shop-top {
  position: fixed; z-index: 50;
  inset-inline-end: 16px; bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 44px; padding: 8px 14px;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius-pill);
  background: #fff; color: var(--shop-navy); font: inherit; font-size: 13px; font-weight: 600;
  box-shadow: 0 6px 18px rgba(15, 23, 42, .18); cursor: pointer;
}
.shop-top:hover { border-color: var(--shop-accent); color: var(--shop-accent); }
/* ⚠⚠ QUAND LE BOUTON WHATSAPP EXISTE, « HAUT DE PAGE » MONTE AU-DESSUS DE LUI (voir .shop-has-wa,
   posée par le gabarit sur le corps de page). Les deux vivent dans le même coin bas, côté fin de
   ligne : empilés, ils se recouvriraient, et c'est le bouton WhatsApp — le plus utilisé — qui garde
   la place du pouce. ⚠ LA CLASSE VIENT DU GABARIT, ELLE N'EST PAS UNE SECONDE VÉRITÉ : le corps de
   page n'annonce shop-has-wa que si le serveur a trouvé un numéro, donc la règle suit exactement
   l'existence du bouton. */
/* ⚠ 72 px = 16 px de marge basse + 44 px de bouton WhatsApp + 12 px d'air entre les deux. Le calcul se
   refait dès que l'un des deux change de taille (voir .shop-wa, qui porte la même). */
.shop-has-wa .shop-top { bottom: calc(72px + env(safe-area-inset-bottom, 0px)); }

/* ⭐⭐⭐ LE BOUTON WHATSAPP FLOTTANT (demande du client : « add flaoting button whatssap for chat with
   us »). Il vit sur TOUTES les pages (voir layouts/main.hbs) : une question se pose aussi bien sur une
   fiche que dans le panier.

   ⚠⚠ POURQUOI LE COIN BAS, CÔTÉ FIN DE LIGNE. C'est là que le pouce arrive sans changer la prise du
   téléphone, et c'est là que le client voit déjà ce bouton dans les boutiques qu'il fréquente — donc
   rien à apprendre. ⚠ inset-inline-end ET NON right : en arabe, le bouton passe à gauche, comme tout
   le reste de cette feuille (propriétés LOGIQUES, voir l'en-tête du fichier).
   ⚠ env(safe-area-inset-bottom) : sur un iPhone, la barre d'accueil du système recouvrirait le bouton
   sans cette réserve.
   ⭐⭐ C'EST UNE ICÔNE SEULE, SUR TOUTES LES TAILLES D'ÉCRAN (demande du client : « also the floating ws
   assistant ») : un rond au combiné, sans mot. ⚠ Le nom accessible (title et aria-label, traduits)
   porte le sens : un rond anonyme ne dirait ni à qui l'on écrit, ni sur quel canal.
   ⭐⭐⭐ ET IL A EXACTEMENT LA TAILLE DU BOUTON « HAUT DE PAGE » (demande du client : « button chat
   whatssap make it size lik up button »). Les deux vivent dans le MÊME coin, empilés l'un au-dessus de
   l'autre : deux cibles de tailles différentes dans un même coin se lisent comme un défaut de
   finition — et le client l'a vu à l'écran avant nous.
   ⚠ 44 px EST LA CIBLE MINIMALE DU POUCE, et c'est déjà la hauteur de « haut de page » (.shop-top)
   comme de tout bouton de la boutique sur téléphone (voir la requête média plus bas). Descendre plus
   bas rendrait le bouton le plus utilisé de la page difficile à viser : la taille demandée est donc
   aussi la plus petite raisonnable, et elle ne rétrécit sur aucun écran.
   ⚠ ET IL EST AU-DESSUS DE LA BARRE D'ONGLETS SUR TÉLÉPHONE (requête média plus bas) : sous elle, il
   serait invisible et intouchable.
   ⚠⚠ IL N'EST PAS CLIQUABLE QUAND LA VISIONNEUSE DES HISTOIRES EST OUVERTE, ET CE N'EST PAS UN OUBLI :
   la visionneuse est un calque plein écran plus élevé (z-index 96), donc elle le recouvre entièrement
   — rien ne dépasse, et le visiteur qui regarde une histoire ne peut pas cliquer un bouton caché
   derrière. */
.shop-wa {
  position: fixed; z-index: 52;
  inset-inline-end: 16px; bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  width: 44px; height: 44px; padding: 0;
  border-radius: var(--shop-radius-pill);
  background: #25d366; color: #fff;
  font-size: 14px; font-weight: 700; text-decoration: none;
  box-shadow: 0 10px 24px rgba(15, 23, 42, .28);
}
.shop-wa:hover { background: #1da851; color: #fff; }
.shop-wa:focus-visible { outline: 2px solid var(--shop-navy); outline-offset: 3px; }

/* ---------------------------------------------------------------- pages simples */
.page-title { font-size: 22px; margin: 4px 0 16px; }
.shop-alert { padding: 10px 12px; border-radius: var(--shop-radius); margin-bottom: 14px; font-size: 13px; }
.shop-alert.is-error { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
/* ⭐ LA CONFIRMATION D'UNE DEMANDE ENREGISTRÉE (mot de passe oublié, voir views/shop/forgot.hbs) :
   le vert des états favorables de la boutique (`.account-status.is-ok`). Une demande reçue est une
   réussite du point de vue du client — l'afficher en orange ou en rouge lui ferait croire à un
   refus, et il recommencerait. */
.shop-alert.is-ok { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }
.empty-state { border: 1px dashed var(--shop-border); border-radius: var(--shop-radius); padding: 28px; text-align: center; color: var(--shop-muted); }
.empty-state .btn { display: inline-block; margin-top: 10px; }
.btn-mini { padding: 4px 9px; font-size: 12px; }
.btn-danger { color: #b91c1c; border-color: #fecaca; }
.btn-danger:hover { background: #fef2f2; }
/* ⭐ LE BOUTON WHATSAPP de la confirmation : c'est l'action principale de la page (la
   demande est déjà enregistrée, il reste à l'envoyer). Vert WhatsApp, texte blanc, et
   assez large pour être visé au pouce. */
.btn-wa {
  background: #25d366; color: #fff; border-color: #1da851; font-weight: 700;
  padding: 12px 22px; font-size: 15px; display: inline-block; text-decoration: none;
}
.btn-wa:hover { background: #1da851; color: #fff; }
.sent-wa { margin: 18px 0; padding: 16px; background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: var(--shop-radius); }
.sent-wa h2 { font-size: 16px; margin: 0 0 8px; }
.sent-wa .qsel-hint { margin: 8px 0 12px; }
.shop-footer-wa a { color: #25d366; font-weight: 600; }

/* ---------------------------------------------------------------- sélection de devis */
.qsel { display: flex; flex-direction: column; gap: 16px; }
.qsel-hint { color: var(--shop-muted); font-size: 13px; margin: 0; }
.qsel-table { width: 100%; border-collapse: collapse; font-size: 13px; }
/* ⚠ `start` / `end` ET NON `left` / `right` : dans un tableau, la première colonne est
   aussi celle des références d'articles — un code se lit de gauche à droite, mais sa
   COLONNE doit rester au début de la ligne, donc à droite en arabe. */
.qsel-table th, .qsel-table td { border-bottom: 1px solid var(--shop-border); padding: 10px 8px; text-align: start; vertical-align: middle; }
.qsel-table thead th { font-size: 12px; text-transform: uppercase; color: var(--shop-muted); }
.qsel-table tbody tr.is-unavailable { background: #fffbeb; }
.qsel-col-ref { width: 110px; font-size: 12px; color: var(--shop-muted); }
/* ⚠ LA COLONNE DE QUANTITÉ EST PLUS LARGE QUE LES AUTRES, ET C'EST UN CHOIX : elle porte le
   compteur COMPLET — deux colonnes de pilules et le champ, soit ~300 px (voir `.qty-pill` et
   partials/qtyControl.hbs). ⚠⚠ LA LARGEUR N'EST PAS COSMÉTIQUE : à 290 px, les deux colonnes ne
   tenaient pas sur une ligne et le contrôle SE REPLIAIT dans le panier — les `+` passaient sous les
   `−`, alors que « minus on the left, plus on the right » est la demande. Au pire (écran serré), le
   repli reste possible : c'est le `flex-wrap` de `.card-qty` qui s'en charge, et la page ne déborde
   jamais. */
.qsel-col-qty { width: 310px; }
/* ⭐⭐⭐ DANS UN TABLEAU, LE CHAMP DU COMPTEUR EST PLUS GRAND ET À ANGLES DROITS (demande du client :
   « change the style with bigger font size and hard edge corner for table »).
   ⓘ POURQUOI LES PILULES GARDENT LEURS COINS DOUX ALORS QUE LE CHAMP N'EN A AUCUN : une pilule est
   une ACTION (elle porte un mot et un signe), le champ est une CELLULE DE SAISIE posée dans la grille
   du tableau — ses angles droits le rangent dans la grille et le distinguent des gestes. Les pilules,
   elles, suivent la maquette (« soft corners »), pas la grille.
   ⚠⚠ CETTE RÈGLE NE TOUCHE PAS LA VIGNETTE (`.is-compact`, dans une carte, pas un tableau) : c'est
   la portée `.qsel-table` qui le garantit. */
.qsel-table .qty-input { border-radius: 0; font-size: 17px; min-width: 64px; }
/* ⚠⚠ LA PILULE EST PLUS FINE DANS UN TABLEAU, ET CE N'EST PAS UN DÉTAIL DE GOUT : la cellule du
   panier fait ~294 px (le navigateur répartit les colonnes), et le contrôle — deux colonnes de
   pilules plus le champ — doit y tenir, sinon il SE REPLIE et les `+` passent sous les `−`, alors que
   « minus on the left, plus on the right » est la demande. Depuis que la pilule porte sa quantité
   (demande du client), elle est plus large : on reprend donc la place dans l'habillage — mot et
   nombre un peu plus petits qu'ailleurs, moins d'air à l'intérieur.
   ⓘ C'EST LA MESURE QUI A TRANCHÉ, à chaque fois (voir `npm run check:qty`, qui lit les positions
   réelles des pilules dans le panier) : une estimation à l'œil s'était déjà trompée deux fois. */
.qsel-table .card-qty { gap: 3px; }
.qsel-table .qty-row { gap: 5px; flex-wrap: nowrap; }
.qsel-table .qty-col { gap: 3px; }
.qsel-table .qty-pill { min-width: 84px; padding-inline: 6px; gap: 3px; }
/* ⚠ LE MOT RESTE LISIBLE (12 px en CAPITALES espacées : il se lit plus grand qu'un 12 px normal), et
   le NOMBRE ne descend jamais sous la taille du texte de la ligne. */
.qsel-table .qty-pill-word { font-size: 11px; }
.qsel-table .qty-pill-num { font-size: 12.5px; }
.qsel-table .qty-pill-sign { font-size: 13px; }
/* Les colonnes de montants s'alignent sur le BORD DE FIN de ligne (à droite en LTR,
   à gauche en RTL) : c'est là qu'on lit un nombre dans les deux langues. */
.qsel-col-price, .qsel-col-total { width: 120px; text-align: end; white-space: nowrap; }
.qsel-col-act { width: 90px; text-align: end; }
.qsel-table tfoot td { border-bottom: 0; font-weight: 700; }
.qsel-total-label { text-align: end !important; color: var(--shop-muted); font-weight: 400 !important; }
.qsel-total-value { font-size: 16px; }
.qsel-name { color: var(--shop-accent); font-weight: 600; }
/* ⭐⭐ LA LIGNE DU PANIER : VIGNETTE + TEXTE, CÔTE À CÔTE.
   ⚠ `min-width: 0` SUR LE BLOC DE TEXTE, ET `flex: 0 0 auto` SUR LA VIGNETTE : c'est ce qui
   permet à une longue désignation de se réduire (et de finir en points de suspension) au lieu
   d'élargir le tableau — le défaut classique du débordement horizontal sur téléphone. */
.qsel-line { display: flex; align-items: center; gap: 8px; }
.qsel-line-text { min-width: 0; }
/* La vignette est un BOUTON (elle agrandit l'image au clic, voir public/shop/shop.js), mais
   elle se dessine comme une image : le style de bouton ne doit pas apparaître. */
.qsel-thumb {
  flex: 0 0 auto; width: 56px; height: 56px; padding: 0; cursor: zoom-in;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius-sm); background: var(--shop-bg);
  overflow: hidden;
}
.qsel-thumb img { width: 100%; height: 100%; object-fit: contain; }
.qsel-thumb:hover { border-color: var(--shop-accent); }
.qsel-thumb:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: -2px; }
/* Le bouton-corbeille : une icône seule, sans mot — le libellé vit dans `aria-label` et
   `title` (voir views/shop/cart.hbs). ⚠ `display: inline-flex` : sans lui, le `<svg>` se pose
   sur la ligne de base et décale la hauteur du bouton. */
.btn-icon { display: inline-flex; align-items: center; justify-content: center; padding: 4px 8px; }
.qsel-warn { color: #b45309; font-size: 12px; margin-top: 2px; }
.qsel-variant { color: var(--shop-muted); font-size: 12px; }
.qsel-qty-form { display: flex; align-items: center; gap: 6px; }
.qsel-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.qsel-count { font-size: 13px; color: var(--shop-muted); }
.qsel-send { border: 1px solid var(--shop-border); border-radius: var(--shop-radius); padding: 16px; background: var(--shop-bg); }
.qsel-send h2 { font-size: 16px; margin: 0 0 6px; }

/* ---------------------------------------------------------------- formulaires */
.form-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 12px; }
@media (max-width: 800px) { .form-grid { grid-template-columns: 1fr; } }
.field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; }
.field > span { color: var(--shop-muted); }
.field em { color: var(--shop-accent); font-style: normal; }
.field input, .field textarea {
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius-sm); padding: 7px 9px;
  font: inherit; background: #fff; color: inherit;
}
.field input:focus, .field textarea:focus { outline: 2px solid var(--shop-accent); outline-offset: -1px; }
.field-error { color: #b91c1c; font-size: 12px; }
/* ⭐⭐ L'ŒIL « VOIR LE MOT DE PASSE » (demande du client, voir partials/loginForm.hbs).
   ⚠ LE CHAMP ET LE BOUTON SONT DANS UN MÊME BLOC POSITIONNÉ : le bouton se pose DANS le champ, à la
   fin de la ligne de lecture (`inset-inline-end`), donc à droite en français et à gauche en arabe —
   comme les autres contrôles de la boutique. `padding-inline-end` sur le champ réserve sa place :
   sans cela, le texte saisi passerait SOUS le bouton sur un mot de passe long.
   ⚠ LE BOUTON FAIT 44 px : c'est une cible de doigt, et il est collé au champ que le client vise
   déjà — une cible plus petite serait manquée une fois sur trois. */
.field-eye-wrap { position: relative; display: block; }
.field-eye-wrap input { padding-inline-end: 44px; }
.field-eye {
  position: absolute; inset-inline-end: 0; top: 50%; transform: translateY(-50%);
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; padding: 0; cursor: pointer;
  border: 0; background: none; color: var(--shop-muted);
}
.field-eye:hover { color: var(--shop-navy); }
/* ⚠⚠ ET LE BOUTON EST RENDU `hidden` PAR LE GABARIT — LA RÈGLE `[hidden]` EST DONC OBLIGATOIRE.
   Sans elle, le `display: inline-flex` ci-dessus l'emporterait (une règle d'auteur bat toujours
   `[hidden]`, qui vient de la feuille du navigateur) et un ŒIL INERTE resterait affiché tant que le
   script n'a pas tourné — c'est-à-dire pour toujours s'il échoue. Même piège que `.story-slide` et
   `.shop-top` : il est désormais détecté automatiquement par le contrôle générique de
   `test/shopChrome.test.js`, qui a justement refusé cette version. */
.field-eye[hidden] { display: none; }
.field-eye:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: -2px; border-radius: var(--shop-radius-sm); }
/* ⚠ L'ÉTAT DÉCIDE DU DESSIN, ET C'EST LA FEUILLE QUI LE FAIT : les deux icônes sont rendues par le
   gabarit, `aria-pressed` choisit celle qui se voit. Un œil barré se lit d'un coup d'œil, là où un
   simple changement de couleur ne dit rien — et la couleur n'est jamais le seul signal (même règle
   que le solde du compte). */
.field-eye .shop-icon-eyeOff { display: none; }
.field-eye[aria-pressed="true"] .shop-icon-eye { display: none; }
.field-eye[aria-pressed="true"] .shop-icon-eyeOff { display: block; }
.field-wide { grid-column: 1 / -1; }
.form-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.form-actions .btn { display: inline-block; }

/* ---------------------------------------------------------------- confirmation */
.sent { max-width: 720px; }
.sent-check {
  width: 46px; height: 46px; border-radius: 50%; background: #dcfce7; color: #15803d;
  display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 700;
  margin-bottom: 12px;
}
.sent-ref { font-size: 16px; }
.sent-ref strong { color: var(--shop-accent); letter-spacing: .5px; }
.sent-steps { margin: 20px 0; }
.sent-steps h2 { font-size: 15px; }
.sent-steps ol { margin: 0; padding-inline-start: 20px; color: var(--shop-muted); font-size: 13px; }
.sent-steps li { margin-bottom: 4px; }

/* ---------------------------------------------------------------- panier / commande */
.cart, .checkout { display: flex; flex-direction: column; gap: 18px; }
.checkout { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
@media (max-width: 900px) { .checkout { grid-template-columns: 1fr; } }
.checkout-recap, .checkout-form { border: 1px solid var(--shop-border); border-radius: var(--shop-radius); padding: 16px; }
.checkout-recap h2, .checkout-form h2 { font-size: 16px; margin: 0 0 10px; }
.checkout-form { background: var(--shop-bg); }
/* ⭐⭐ LE BLOC « QUI REÇOIT LA MARCHANDISE » EST UNE INFORMATION, PAS UN PARAGRAPHE.
   ⚠⚠ LA VALEUR EST UN `<p>`, DONC ELLE PORTAIT LES MARGES PAR DÉFAUT D'UN PARAGRAPHE (1em en haut
   et en bas, soit ~32 px) À L'INTÉRIEUR D'UN CHAMP DÉJÀ ESPACÉ : le bloc mesurait 112 px dans une
   colonne de téléphone pour deux lignes de texte, et ces 32 px étaient pris sur la hauteur utile du
   formulaire — donc sur le défilement que le client doit faire pour valider. On remet la marge à
   zéro et l'on met le nom et le téléphone CÔTE À CÔTE (ils tiennent sur une ligne dès 320 px) : le
   bloc passe sous les 60 px. ⓘ C'est la même correction que le reste de cette page : compacter sans
   rien retirer — les deux informations, leur libellé et leur explication restent tous affichés. */
.checkout-recipient { gap: 2px; }
.checkout-recipient-value {
  margin: 0; display: flex; flex-wrap: wrap; gap: 2px 10px; align-items: baseline;
}
.checkout-recipient .field-hint { font-size: 12px; }
/* ⚠⚠ `width: 100%` ET `min-width: 0` SUR TOUS LES CHAMPS DE FORMULAIRE, ET CE N'EST PAS
   COSMÉTIQUE. Un `<input>` sans largeur explicite prend sa largeur INTRINSÈQUE, celle de son
   attribut `size` (20 caractères par défaut) — et avec la police du site à 16 px sur téléphone,
   cela fait ~351 px, soit PLUS que la colonne qui l'accueille. Or un élément de grille ou de flex
   a `min-width: auto` : il REFUSE de devenir plus étroit que son contenu, donc un champ trop large
   ÉLARGIT sa grille et fait déborder la PAGE ENTIÈRE — le client peut alors la faire glisser
   latéralement, et en arabe le décalage se voit au premier coup d'œil.
   ⓘ Constaté et mesuré sur la page de demande de devis : 5 px de débordement, champ à 351 px dans
   une colonne de 317 (chaîne des parents relevée dans le navigateur). Le défaut existait sur TOUS
   les formulaires (inscription, commande, coordonnées) : c'est la même règle qui les corrige. */
.field input, .field textarea { width: 100%; min-width: 0; }
.field select {
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius-sm); padding: 7px 9px;
  font: inherit; background: #fff; color: inherit;
  width: 100%; min-width: 0;
}
.sent-summary { margin: 16px 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
.sent-summary > div { display: flex; gap: 10px; justify-content: space-between; border-bottom: 1px dashed var(--shop-border); padding-bottom: 4px; }
.sent-summary span { color: var(--shop-muted); }

/* ---------------------------------------------------------------- compte client */
.auth-card { max-width: 560px; }
.auth-note {
  background: #eef2ff; border: 1px solid #c7d2fe; color: #3730a3;
  border-radius: var(--shop-radius); padding: 10px 12px; font-size: 13px; margin-bottom: 14px;
}
.form-single { grid-template-columns: 1fr; }

/* ⭐⭐ « MOT DE PASSE OUBLIÉ » (views/shop/forgot.hbs). La page de confirmation décrit une
   DÉMARCHE en trois temps : une liste numérotée se lit dans l'ordre, là où trois phrases à la
   suite se sautent. ⚠ Aucune largeur en dur, et le retrait de la liste suit le sens de lecture
   (`padding-inline-start`), donc la page arabe n'a pas de règle supplémentaire. */
.forgot-steps {
  margin: 12px 0; padding-inline-start: 22px; font-size: 13px;
  display: flex; flex-direction: column; gap: 8px; color: var(--shop-navy);
}
/* Les deux issues de la confirmation (WhatsApp, téléphone) sur une ligne quand il y a la place,
   l'une sous l'autre sur un téléphone : ce sont des boutons, donc des cibles de pouce. */
.forgot-actions { flex-wrap: wrap; gap: 8px; }
/* Le lien « Mot de passe oublié ? » sous le formulaire de connexion : un recours, pas l'action
   attendue — il reste discret, mais il est une VRAIE cible tactile (44 px de haut). */
.form-hint { margin: 2px 0 0; font-size: 13px; }
.form-link { display: inline-flex; align-items: center; min-height: 44px; color: var(--shop-accent-dark); text-decoration: underline; }
.account-status {
  border-radius: var(--shop-radius); padding: 10px 12px; font-size: 13px; margin-bottom: 16px;
}
.account-status.is-pending { background: #fff7ed; border: 1px solid #fed7aa; color: #7c2d12; }
.account-status.is-ok { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }
.account-block { border-top: 1px solid var(--shop-border); padding-top: 14px; margin-bottom: 22px; }
.account-block h2 { font-size: 16px; margin: 0 0 10px; }
.account-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px 18px; font-size: 13px; }
.account-grid > div { display: flex; flex-direction: column; }
.account-grid span { color: var(--shop-muted); font-size: 12px; }
.account-list { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.account-list li {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  border: 1px solid var(--shop-border); border-radius: var(--shop-radius); padding: 8px 10px; font-size: 13px;
}
.account-tag {
  background: #dcfce7; color: #166534; border-radius: var(--shop-radius); font-size: 10.5px;
  font-weight: 700; padding: 0 6px; margin-inline-start: 6px;
}
.account-ok { color: #15803d; font-weight: 600; }
/* État « transmission en attente » : ce n'est ni une réussite ni une faute du client —
   un opérateur est prévenu (voir services/outbox.js, qui lève une alerte). */
.account-warn { color: #b45309; font-weight: 600; }

/* ⭐ L'INDICATEUR « LIVRAISON VALIDÉE », SOUS L'ÉTAT DE LA COMMANDE (voir
   views/partials/deliveryState.hbs). Il vient de l'ERP (`delivery_validated`,
   `delivery_date`) et répond à LA question du client : ma marchandise est-elle partie ?
   ⚠ IL REPREND LE VOCABULAIRE VISUEL DÉJÀ LÀ — le vert des états favorables
   (`.account-ok`), le gris des mentions neutres — plutôt que d'inventer une pastille de
   plus. `display: block` parce qu'il se pose SOUS la pastille d'état, dans la même case. */
.delivery-state { display: block; margin-top: 2px; font-size: 12px; color: var(--shop-muted); }
.delivery-state.is-ok { color: #15803d; font-weight: 600; }

/* ⭐⭐ L'ÉTAT DE PRÉPARATION PUBLIÉ PAR L'ERP (`prep_status`, migration 016), affiché SOUS
   l'état de la commande, dans la même case. Il reprend le vocabulaire visuel de l'indicateur
   de livraison juste au-dessus — c'est la même sorte d'information, et le client les lit
   ensemble.
   ⚠ CLASSE DISTINCTE (`.prep-state`, et non `.delivery-state`) : les deux notions sont
   VOISINES MAIS DIFFÉRENTES — l'une vient du bon de livraison (`delivery_validated`), l'autre
   de l'ERP sur la commande elle-même — et un contrôle qui veut parler de l'une ne doit pas
   lire l'autre. Cela permet aussi de vérifier, dans les tests, qu'un état absent ne produit
   AUCUNE ligne (le partiel de livraison, lui, écrit « En préparation » quand il ne sait rien :
   c'est un autre cas, et il est testé séparément). */
.prep-state { display: block; margin-top: 2px; font-size: 12px; color: var(--shop-muted); }
.prep-state.is-ok { color: #15803d; font-weight: 600; }

/* Un titre de sous-section de l'espace client (liste d'envies : articles retirés). Plus
   discret qu'un `<h2>` de section : c'est une précision, pas une section de plus. */
.account-subtitle { font-size: 14px; margin: 18px 0 8px; }

/* ---------------------------------------------------------------- onglets de l'espace client
   ⚠⚠ LE MASQUAGE DES PANNEAUX DÉPEND DE `.is-enhanced`, ET RIEN D'AUTRE. Cette classe est
   posée par le JavaScript (public/shop/shop.js), jamais par le gabarit : sans script, les
   quatre sections restent VISIBLES l'une sous l'autre. Une page client plus longue est
   acceptable ; une page client vide parce qu'un script a échoué ne l'est pas. */
.account-tabs.is-enhanced > .account-panel { display: none; }
.account-tabs.is-enhanced > .account-panel.is-active { display: block; }
.account-panel { margin-bottom: 8px; }

.account-tablist {
  display: flex; flex-wrap: wrap; gap: 4px;
  border-bottom: 1px solid var(--shop-border); margin-bottom: 16px;
}
.account-tab {
  border: 1px solid transparent; border-bottom: 0; background: none;
  padding: 8px 14px; font: inherit; font-size: 13px; color: var(--shop-muted);
  cursor: pointer; margin-bottom: -1px;
  /* Coins arrondis du HAUT seulement : la barre d'onglets est sous eux. Ce sont les coins
     LOGIQUES de début, pour que l'onglet se dessine pareil en arabe. */
  border-start-start-radius: var(--shop-radius); border-start-end-radius: var(--shop-radius);
}
.account-tab:hover { color: var(--shop-navy); }
.account-tab:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: -2px; }
/* ⭐ L'ONGLET ACTIF DOIT SE VOIR SANS ÊTRE SURVOLÉ : c'est ce qui dit au client où il est.
   La classe est doublée par `aria-selected` (voir shop.js), pour les lecteurs d'écran. */
.account-tab.is-active {
  color: var(--shop-navy); font-weight: 700; background: #fff;
  border-color: var(--shop-border); border-bottom: 2px solid var(--shop-accent);
}

/* ---- historique & relevé
   Les deux tableaux reprennent la base des tableaux de la boutique (`.qsel-table`) : c'est
   la même grille, les mêmes filets, et donc la même lecture. Ce qui suit ne règle que ce
   qui leur est propre : la largeur des colonnes et l'alignement des montants.
   ⚠ LES COLONNES SONT CELLES DES ÉCRANS DE L'ERP, MOINS CELLES QUE LE CLIENT A DEMANDÉ DE
   RETIRER — et elles ne se comptent plus de la même façon, justement parce qu'une colonne est
   partie (voir `ledger-col-act`, dernière colonne d'action) :
     · Historique  : REF BL · Date mvt · Type · Etat · Montant Net TTC · Regl (+ action)
       — ⚠ la colonne « Échéance » de l'ERP est RETIRÉE : jamais renseignée sur ces pièces,
         une date vide n'apprend rien au client et lui coûte une colonne de plus à traverser ;
     · Bons de livraison du mois : REF BL · Date mvt · Etat · Montant Net TTC · Regl (+ action)
       — mêmes colonnes, sans « Type » : toutes les lignes sont des B.L. ;
     · Relevé      : ID · Date · Libellé · Débit · Crédit · Réf mvt · Type (inchangé).
   ⓘ « Réf mvt » est devenu « REF BL » dans les deux tableaux de pièces (demande du client) :
   le mot « mvt » (mouvement) est le vocabulaire du back-office, et c'est « BL » que le client
   prononce au téléphone. Le RELEVÉ garde « Réf mvt » : là, la colonne n'est renseignée que
   sur un RÈGLEMENT et désigne la pièce que ce règlement solde — « REF BL » y serait faux. */

/* ⚠ Sept colonnes sur un téléphone ne tiennent pas : on préfère un DÉFILEMENT horizontal du
   tableau à un tableau écrasé dont les montants se coupent. */
.ledger-scroll { overflow-x: auto; }
/* ⭐⭐ LA LIGNE D'ACTIONS D'UN TABLEAU DE L'ESPACE CLIENT (l'aperçu PDF du relevé) : elle se place
   AU-DESSUS du tableau, alignée sur son bord d'attache — et jamais au milieu, contrairement à la
   barre d'actions de la PAGE d'un document (voir `.doc-actions`), qui est le document lui-même. */
.ledger-actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 10px; }
.ledger-table { min-width: 720px; }
.ledger-col-id { width: 84px; }
.ledger-col-date { width: 92px; }
.ledger-col-ref { width: 104px; }
/* « Retour/Avoir » est le plus long type de l'ERP : la colonne doit le tenir d'un seul
   tenant, sinon elle se coupe en deux et le tableau devient illisible. */
.ledger-col-type { width: 124px; }
.ledger-col-state { width: 108px; }
.ledger-col-regl { width: 116px; }
.ledger-col-type-letter { width: 52px; }
/* Colonne d'ACTION (« Aperçu PDF ») des tableaux de pièces : l'en-tête reste VIDE (voir le
   gabarit) — c'est la convention déjà utilisée par le panier, et une colonne d'action n'a
   rien à annoncer. ⚠ La largeur est fixée pour que le bouton ne se coupe pas, et le contenu
   s'aligne sur le bord de FIN de ligne comme les autres colonnes d'action. */
.ledger-table th.ledger-col-act, .ledger-table td.ledger-col-act { width: 116px; text-align: end; white-space: nowrap; }
/* La colonne du libellé prend ce qui reste : c'est la plus longue, et c'est celle qu'on lit. */
.ledger-col-label { width: auto; }
/* ⚠ SÉLECTEUR DOUBLÉ (`th.` ET `td.`) À DESSEIN : `.qsel-table th`/`.qsel-table td` pose
   `text-align: start` avec une spécificité PLUS FORTE qu'une simple classe, et une règle
   `.ledger-col-amount` posant un autre alignement n'aurait donc aucun effet.
   ⭐⭐ LES MONTANTS SONT AU CENTRE (demande du client : « prices and dates in the middle »).
   ⚠⚠ ILS ÉTAIENT POSÉS SUR `end` — « le bord de fin de ligne, là où on lit un nombre ». En
   FRANÇAIS c'est à droite, donc tout allait bien ; en ARABE, `end` vaut GAUCHE, et les montants
   partaient seuls à l'autre bout de la ligne, détachés de leur colonne. Un montant centré se lit
   en colonne — c'est ainsi qu'on compare un relevé, d'une ligne à l'autre — et il ne danse plus
   d'une ligne à l'autre selon le nombre de chiffres. `center` se lit pareil dans les deux sens. */
.ledger-table th.ledger-col-amount, .ledger-table td.ledger-col-amount {
  width: 112px; text-align: center; white-space: nowrap;
}

/* ⭐⭐⭐ LES TABLEAUX DE L'ESPACE CLIENT : BORDÉS, ALIGNÉS SUR LE BORD D'ATTACHE — SAUF LES MONTANTS
   ET LES DATES, AU CENTRE (demande du client, dans les trois onglets « طلباتي · السجل · الكشف » :
   « make table with border — align element to the right in arabic in each column — prices and dates
   in the middle »).

   ⚠⚠ CE QUI N'ALLAIT PAS, ET QUI NE SE VOYAIT QU'EN ARABE. `.qsel-table th/td` pose
   `text-align: start` — donc à DROITE en arabe, à gauche en français —, mais les MONTANTS étaient
   posés sur `end` par une règle plus spécifique (`td.ledger-col-amount`, justement écrite pour les
   coller au bord de fin de ligne) : en arabe, ce bord est à GAUCHE, et les chiffres partaient donc
   seuls à l'autre bout de la ligne, détachés de leur colonne. Toute règle de côté PHYSIQUE se paie
   dans la seconde langue ; `start` et `center` se lisent dans les deux.

   ⭐ LE CONTENU S'ALIGNE DONC SUR LE BORD D'ATTACHE (à droite en arabe, à gauche en français) —
   les références, les types, les états, les modes de règlement — SAUF :
     · LES MONTANTS (`ledger-col-amount`, `qsel-col-total`, `qsel-col-price`) et
     · LES DATES (`cell-center`),
   qui sont AU CENTRE. Ce sont les deux colonnes qu'on lit EN COLONNE d'une ligne à l'autre : un
   montant collé à un bord danse d'une ligne à l'autre selon le nombre de chiffres, alors qu'un
   montant centré se compare d'un coup d'œil — c'est exactement ce qu'on fait sur un relevé.

   ⚠⚠ LES BORDURES SONT POSÉES ICI, ET PAS SUR `.qsel-table` TOUT ENTIER : la même classe sert au
   PANIER et à la demande de devis, où les cellules sont des blocs de saisie (vignette, compteur,
   corbeille) — un cadre autour de chacun alourdirait des écrans qu'on manipule. La grille complète
   est ce qu'on veut d'un TABLEAU DE CONSULTATION : trois onglets de l'espace client, plus le relevé
   imprimé (`.doc-table`, le même tableau sur le papier).
   ⚠ ET LA BORDURE EXISTANTE (`border-bottom`, sur toutes les cellules) N'EST PAS RETIRÉE : elle est
   simplement complétée — `border-collapse: collapse` fait que les quatre côtés ne se doublent pas. */
.account-panel .qsel-table th, .account-panel .qsel-table td,
.doc-table th, .doc-table td { border: 1px solid var(--shop-border); }
/* ⭐ `cell-center` EST POSÉE PAR LES GABARITS, COLONNE PAR COLONNE (les dates) : une classe sur la
   cellule, jamais sur un rang — un tableau dont l'ordre des colonnes change ne doit pas déplacer
   les alignements par accident. */
.qsel-table th.cell-center, .qsel-table td.cell-center { text-align: center; }
/* ⚠ LES SÉLECTEURS SONT DOUBLÉS (`th.` ET `td.`) ET PRÉFIXÉS, À DESSEIN : `.qsel-table th/td` pose
   `start` avec une spécificité plus forte qu'une simple classe, et une classe seule ne suffirait
   pas à la corriger (même piège que celui documenté plus haut pour les montants). */
.account-panel .qsel-table th.qsel-col-total, .account-panel .qsel-table td.qsel-col-total,
.account-panel .qsel-table th.qsel-col-price, .account-panel .qsel-table td.qsel-col-price,
.doc-table th.qsel-col-price, .doc-table td.qsel-col-price { text-align: center; }
/* ⚠ LA COLONNE D'ACTION AUSSI, ET C'EST UNE CORRECTION D'ARABE : en écriture de droite à gauche
   elle est la DERNIÈRE COLONNE, donc à gauche du tableau — `end` (son bord gauche) y collait le
   bouton au bord de la page, à l'écart de la ligne qu'il concerne. `start` le ramène du côté du
   contenu, dans les deux sens de lecture. */
.account-panel .qsel-table th.ledger-col-act, .account-panel .qsel-table td.ledger-col-act,
.account-panel .qsel-table td.qsel-col-act { text-align: start; }

/* ⭐⭐ LE SOLDE DU COMPTE, SOUS LE TABLEAU — comme le récapitulatif de l'ERP, et non comme une
   colonne : c'est la question que le client vient poser (« combien je dois ? »), et l'ERP ne
   l'affiche pas ligne à ligne.

   ⭐ IL EST SUR FOND VERDÂTRE (demande du client). Le vert est déjà, dans toute la boutique,
   la couleur des états favorables (`.account-status.is-ok`, `.account-ok`, `.delivery-state.is-ok`) :
   le solde — le chiffre qu'on vient chercher — se lit donc comme une information, et non
   comme un tableau de plus. Le couple vert foncé sur vert très clair est le même que celui
   des encadrés voisins, dont le contraste est largement suffisant (≈ 12:1).
   ⚠ LA TEINTE N'EST PAS LE SEUL SIGNAL : voir `is-credit` ci-dessous. */
.ledger-balance {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin: 12px 0 0; padding: 8px 12px; font-size: 14px;
  border: 1px solid #bbf7d0; border-radius: var(--shop-radius); background: #f0fdf4;
  color: #166534;
}
.ledger-balance > span { color: #166534; font-size: 12px; }
.ledger-balance-tag {
  border: 1px solid #bbf7d0; background: #fff; border-radius: var(--shop-radius-pill);
  font-size: 12px; font-weight: 700; padding: 0 8px; white-space: nowrap;
}

/* ⚠⚠ UN SOLDE NÉGATIF EST UN CRÉDIT EN FAVEUR DU CLIENT — ET CELA SE DIT, PAS SEULEMENT SE
   COLORER. Le montant porte déjà son signe (c'est l'aide de mise en forme des montants qui
   l'ajoute), la phrase « en votre faveur » est écrite dans le gabarit, et cette classe change
   la TEINTE : un jaune ambré (la couleur des mentions à lire, déjà utilisée par
   `.ledger-unpaid`) plutôt que le vert, pour qu'un solde créditeur ne se confonde pas avec un
   solde nul ou dû au premier coup d'œil. Une couleur seule ne dit rien à un lecteur d'écran, ni
   à un daltonien : c'est pourquoi elle vient EN PLUS des mots, jamais à leur place.
   ⓘ Les accolades sont évitées dans les commentaires de cette feuille : plusieurs contrôles du
   dépôt découpent les règles dessus, et une moustache citée en exemple les ferait déraper. */
.ledger-balance.is-credit { background: #fffbeb; border-color: #fde68a; color: #92400e; }
.ledger-balance.is-credit > span { color: #92400e; }
.ledger-balance.is-credit .ledger-balance-tag { border-color: #fde68a; }

/* ⚠ « IMPAYÉ » EST UNE MENTION, PAS UN MONTANT. L'ERP renvoie ces règlements à 0 des deux
   côtés (voir server/services/clientLedger.js) : sans ce badge, la ligne se lirait comme un
   règlement ordinaire de zéro — un chiffre muet. */
.ledger-unpaid {
  background: #fef3c7; color: #92400e; border-radius: var(--shop-radius); font-size: 10.5px;
  font-weight: 700; padding: 0 6px; white-space: nowrap;
}
/* La ligne entière est teintée : le regard doit s'y arrêter, c'est souvent la raison de
   l'appel. */
.ledger-table tbody tr.is-unpaid { background: #fffbeb; }
/* Les pièces réglées, dans le libellé composé d'une page arabe (voir ledgerType.hbs). */
.ledger-refs { color: var(--shop-muted); font-size: 12px; }

/* ⭐⭐ L'ÉTAT DE RÈGLEMENT D'UNE PIÈCE (demande du client : « réglé / partiel / non réglé » dans
   l'historique). Trois teintes, LES MÊMES que partout ailleurs dans la boutique : le vert des
   états favorables (`#15803d` sur `#f0fdf4`), l'ambre des mentions à lire (`#92400e` sur
   `#fef3c7`) et le gris des informations neutres. Aucune couleur n'est inventée ici.
   ⚠⚠ LA COULEUR N'EST JAMAIS LE SEUL SIGNAL : les trois mots (« Réglé », « Partiellement réglé »,
   « Non réglé ») sont écrits dans la cellule, en toutes lettres et dans la langue de la page. Un
   daltonien, un lecteur d'écran ou une impression en noir et blanc lisent donc la même chose.
   ⚠ Et l'état reste DISCRET : c'est une colonne d'information, pas une alerte. Seul l'ambre de
   « non réglé » attire l'œil — c'est la ligne qui fait téléphoner. */
.ledger-settled { border-radius: var(--shop-radius); font-size: 10.5px; font-weight: 700; padding: 0 6px; white-space: nowrap; }
.ledger-settled.is-ok { background: #f0fdf4; color: #15803d; }
.ledger-settled.is-part { background: #fef3c7; color: #92400e; }
.ledger-settled.is-due { background: #fff7ed; color: #7c2d12; }

/* ⚠ LE MESSAGE D'INDISPONIBILITÉ N'EST PAS UNE LISTE VIDE. Il dit qu'on ne SAIT PAS —
   panne de l'ERP, client inconnu, compte non rattaché — au lieu d'affirmer « aucun
   document ». Voir views/partials/ledgerNotice.hbs. */
.ledger-notice {
  margin: 0; padding: 10px 12px; font-size: 13px;
  border: 1px solid #fed7aa; background: #fff7ed; color: #7c2d12;
  border-radius: var(--shop-radius);
}

/* Colonne laissée vide (un débit nul n'est pas « 0,00 », il n'existe pas) : le tiret doit
   se lire comme une absence, donc en gris. */
.muted { color: var(--shop-muted); }

/* ═══════════════════════════════════════════════════════════════════════════════════
   ⭐⭐ GRAND ÉCRAN (au-dessus de 700 px) : L'EN-TÊTE SUR UNE SEULE LIGNE
   ═══════════════════════════════════════════════════════════════════════════════════

   ⚠⚠ CE BLOC EST LE COMPLÉMENT EXACT DU BLOC MOBILE QUI SUIT (`max-width: 700px`) : les deux
   bornes se partagent l'écran, et rien n'est décrit deux fois. Il vit ICI, à la fin de la
   feuille, pour une raison précise : il doit pouvoir ÉCRASER les règles générales posées plus
   haut (`.shop-voice`, `.shop-search-wrap`, `order`), qui décrivent la mise en page de repli.

   ⭐ CE QUE LA DEMANDE CHANGE, ET POURQUOI CHAQUE RÈGLE EST LÀ :
     1. « aligner les éléments de l'en-tête sur UNE RANGÉE » : plus de retour à la ligne
        (`flex-wrap: nowrap`) et un ORDRE explicite — logo · recherche · icônes · langues —,
        parce que l'ordre du DOM, lui, commence par la marque puis les langues (la boîte
        `.shop-header-top` s'efface en `display: contents`) ;
     2. « des icônes au lieu du texte » : les libellés de `.shop-nav` sont masqués (règle
        générale, plus haut) — les entrées sont donc des dessins de 20 px avec une cible de
        34-40 px, et l'infobulle porte le mot ;
     3. « la langue de dictée À CÔTÉ de la barre de recherche » : le sélecteur `|FR|AR|` était
        SOUS le champ sur grand écran (`margin-top: 4px`). `.shop-voice` passe en
        `display: contents` — comme sur téléphone — et ses enfants deviennent des éléments de la
        ligne de recherche : le champ prend la place qui reste, le sélecteur garde la sienne, et
        le message d'état (« J'écoute… ») s'étale dessous (`flex: 1 1 100%`) sans pousser la
        ligne. ⚠ AUCUN NŒUD N'EST DÉPLACÉ POUR AUTANT : le même partiel sert les deux
        dispositions.
   ⚠ `min-width: 0` SUR LA RECHERCHE, SINON LA LIGNE DÉBORDE. Un élément flex refuse de devenir
   plus étroit que son contenu (`min-width: auto`) : sans cette borne, le champ — qui porte un
   texte de désignation parfois long — élargirait l'en-tête au lieu de se comprimer, et la page
   entière défilerait latéralement (c'est exactement le défaut de 68 px corrigé sur téléphone). */
@media (min-width: 701px) {
  .shop-header-inner { flex-wrap: nowrap; }
  .shop-brand { order: 1; }
  .shop-header-search { display: block; order: 2; flex: 1 1 auto; min-width: 0; }
  .shop-header-search .shop-search-wrap { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; min-width: 0; width: 100%; }
  .shop-search { flex: 1 1 240px; min-width: 0; }
  .shop-voice { display: contents; }
  .shop-voice-lang { order: 1; flex: 0 0 auto; }
  .shop-voice-status { order: 2; margin-top: 0; }
  .shop-nav { order: 3; flex: 0 0 auto; }
  .shop-lang { order: 4; flex: 0 0 auto; }
}

/* ═══════════════════════════════════════════════════════════════════════════════════
   MOBILE — MESURÉ SUR UN ÉCRAN DE 375 px, PAS DEVINÉ.
   ═══════════════════════════════════════════════════════════════════════════════════
   ⚠⚠ CE QUI A ÉTÉ TROUVÉ, ET QUI A DICTÉ TOUT CE BLOC. L'audit en 375×667 (Chrome, émulation
   tactile) donnait, sur CHAQUE page : `nav.shop-nav` mesurait 427 px dans une fenêtre de
   375 → 68 px de débordement, donc la page ENTIÈRE se déplaçait latéralement (titre coupé,
   contenu qui glisse sous le doigt, et en arabe la page se décalait de 43 px). Le reste du
   CSS était sain : aucune image sans dimensions, aucun tableau débordant hors de sa boîte.

   ⓘ La mesure utilise `document.documentElement.clientWidth`, PAS `window.innerWidth` :
   en émulation mobile, `innerWidth` s'ÉLARGIT avec le contenu débordant (443 pour un écran de
   375) et fait donc disparaître le défaut qu'on cherche — c'est ce qui a masqué le problème
   au premier passage. Voir le script d'audit dans le rapport.

   Ce bloc ne « réduit » rien : il laisse la page tenir dans la largeur de l'écran, et rend
   les cibles assez grandes pour un doigt (44 px, recommandation Apple/Google). */
@media (max-width: 700px) {
  /* ═══════════════════════════════════════════════════════════════════════════════
     L'EN-TÊTE DU TÉLÉPHONE TIENT EN DEUX LIGNES, ET RIEN DE PLUS.
     LIGNE 1 : la marque, puis les langues du site en pastilles-icônes (`FR` / `AR`).
     LIGNE 2 : la barre de recherche, et la langue de DICTÉE juste à côté (`|FR|AR|`).
     Les quatre destinations du quotidien (compte, commandes, panier, envies) sont descendues
     dans la BARRE D'ONGLETS, en bas de l'écran — c'est là que le pouce les atteint.
     ⚠ POURQUOI DEUX LIGNES ET NON TROIS, COMME AVANT. La troisième ligne était une bande de
     liens qui DÉFILAIT horizontalement : il fallait la faire glisser pour découvrir « Créer un
     compte », et le sélecteur de langue s'y perdait hors écran. Une barre d'onglets fixe montre
     tout, tout le temps, et ne pousse jamais le contenu.
     ⚠ `min-width: 0` SUR CHAQUE LIGNE N'EST PAS COSMÉTIQUE. Un élément flex a `min-width: auto` :
     il REFUSE de devenir plus étroit que son contenu. Sans ces bornes, une ligne dont le contenu
     ne tient pas ÉLARGIT l'en-tête au lieu de le comprimer, et la PAGE déborde horizontalement
     (mesuré : 39 px de débordement sur un iPhone SE lors d'un essai précédent). */
  .shop-header-inner {
    flex-direction: column; align-items: stretch; gap: 8px;
    padding: 8px 12px;
    /* Encoche et coins arrondis : le contenu ne doit pas passer dessous. */
    padding-inline: calc(12px + env(safe-area-inset-left)) calc(12px + env(safe-area-inset-right));
    min-width: 0;
  }
  .shop-brand { flex-direction: row; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; flex: 1 1 auto; }
  /* ⭐ LE LOGO RÉTRÉCIT SUR TÉLÉPHONE (demande du client) : 30 px de haut au lieu de 40. La place
     est comptée sur la ligne 1 (marque · langues · déconnexion), et un logo de 40 px y prenait la
     hauteur d'une cible tactile pour un élément qui, lui, n'a pas besoin d'être gros. */
  .shop-brand-logo { height: 30px; }
  /* ⭐ LIGNE 1 : « MARQUE + LANGUES ». `display: contents` est ANNULÉ ici — la boîte redevient
     une vraie ligne, la marque à un bout et les langues à l'autre. */
  .shop-header-top {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; min-width: 0;
  }
  /* ⭐⭐ LA DÉCONNEXION EST ICI, SUR LA PREMIÈRE LIGNE, ET ELLE EST VISIBLE SANS DÉFILER
     (demande du client) : la bande de liens du haut est masquée sous 700 px, et elle portait
     la seule déconnexion de la page.
     ⚠ `order: 5` : elle passe APRÈS les pastilles de langue (qui portent `order: 4`), donc en
     fin de ligne — la place habituelle d'une action de compte, et la même en arabe (c'est
     l'ordre du DOM dans une ligne `flex` qui suit le sens de lecture). */
  .shop-header-logout { display: block; order: 5; flex: 0 0 auto; }
  /* ⚠ 44 px DE CÔTÉ COMME DE HAUT : c'est la cible du doigt, au même titre qu'un onglet. */
  .shop-logout-btn { min-width: 44px; min-height: 44px; }
  .shop-lang { margin-inline-start: 0; padding: 2px; }
  /* 44 px : la langue est une cible comme les autres, au même titre qu'un onglet. */
  .shop-lang-link { min-height: 44px; min-width: 44px; justify-content: center; padding-inline: 8px; }

  /* ⭐ LIGNE 2 : LA RECHERCHE, ET LA LANGUE DE DICTÉE À CÔTÉ.
     ⚠ C'EST UNE GRILLE, PAS UN FLEX, ET C'EST CE QUI FAIT TENIR LA LIGNE. Le sélecteur
     `|FR|AR|` doit occuper une COLONNE À LUI (`auto` = sa largeur naturelle ≈ 86 px), et le
     champ se partager le reste — un flex laisserait le champ se comprimer jusqu'à devenir
     inutilisable, ou pousserait le sélecteur hors écran. La colonne `1fr` du champ a
     `min-width: 0`, donc elle RÉTRÉCIT au lieu de déborder.
     ⓘ `.shop-voice` passe en `display: contents` : ses enfants (le sélecteur et le message
     d'état) deviennent des cases de CETTE grille. C'est ce qui permet au `|FR|AR|` de monter à
     côté du champ sans déplacer un seul nœud du DOM, et au message d'état de s'étaler dessous
     quand la dictée a quelque chose à dire.
     ⓘ QUAND LA DICTÉE N'EST PAS DISPONIBLE, `.shop-voice` est `hidden` : la règle qui vise cet
     attribut (elle pose `display: none`) est PLUS SPÉCIFIQUE que `display: contents`, donc
     elle gagne — la ligne 2 redevient simplement la barre de recherche. */
  .shop-header-search { display: block; width: 100%; min-width: 0; }
  .shop-search-wrap {
    display: grid; grid-template-columns: minmax(0, 1fr) auto;
    align-items: center; column-gap: 6px; row-gap: 4px;
    min-width: 0; width: 100%;
  }
  .shop-search { grid-column: 1; min-width: 0; }
  .shop-voice { display: contents; }
  .shop-voice-lang { grid-column: 2; }
  /* ⚠ 44 px PAR SEGMENT, ET IL FAUT L'ÉCRIRE ICI. La règle générique des boutons de ce bloc
     (44 px de côté) ne suffit PAS : `.shop-voice-lang-btn`, à 30 px, est plus SPÉCIFIQUE qu'un
     sélecteur de balise, et gagnait donc — les deux segments restaient des cibles de 30 px de
     haut, sous la recommandation tactile. On repose la taille ici, où elle vaut pour le doigt. */
  .shop-voice-lang-btn { min-width: 44px; min-height: 44px; }
  .shop-voice-status { grid-column: 1 / -1; }
  .shop-suggest { grid-column: 1 / -1; }
  /* Le bouton d'envoi devient une LOUPE (voir la règle générale) : le libellé libère la place
     qu'occupe maintenant la langue de dictée. */
  .shop-search-submit-icon { display: block; }
  .shop-search-submit-text { display: none; }
  .shop-search-submit { padding-inline: 12px; }

  /* ---- LA BANDE DE LIENS DE L'EN-TÊTE DISPARAÎT SUR TÉLÉPHONE --------------------
     ⚠ ELLE N'EST PAS SUPPRIMÉE DU DOM, ELLE EST MASQUÉE. C'est ce qui garde, sous 700 px, un
     repli complet pour qui n'a pas de JavaScript (la barre d'onglets, elle, n'a pas besoin de
     script : elle est rendue par le serveur — mais elle ne porte ni « Créer un compte » pour un
     visiteur qui a déjà un compte en attente, ni la déconnexion, ni le compteur de sélection).
     ⚠ `display: none` EMPÊCHE AUSSI LE DÉBORDEMENT : c'était cette bande (496 px de contenu)
     qui élargissait l'en-tête quand elle défilait horizontalement. Sans elle, plus rien à
     contenir — et les règles de défilement `overflow-x` qui vivaient ici ont disparu avec elle. */
  .shop-nav { display: none; }

  /* ---- LA BARRE D'ONGLETS, EN BAS, SOUS LE POUCE ---------------------------------
     ⚠ `position: fixed` + `inset-block-end: 0` : elle ne défile pas avec la page. C'est tout
     son intérêt — un raccourci qui s'éloigne quand on descend ne sert à rien. Le
     `padding-bottom: env(safe-area-inset-bottom)` réserve la place de la barre d'accueil du
     système sur les écrans à encoche, sans quoi les libellés passent dessous. */
  .shop-tabbar {
    display: flex; align-items: stretch;
    position: fixed; inset-inline: 0; bottom: 0; z-index: 45;
    background: #fff; border-top: 1px solid var(--shop-border);
    padding-bottom: env(safe-area-inset-bottom);
  }
  /* ⚠⚠ LA PAGE RÉSERVE LA HAUTEUR DE LA BARRE, SINON ELLE LA RECOUVRE. Le pied de page
     (adresse, téléphone, WhatsApp) et le bouton « afficher la suite » du catalogue se
     retrouveraient dessous, invisibles et intouchables. La réserve est sur le `body` : elle
     s'applique à toutes les pages, y compris celles qui n'ont pas de pied de page. */
  body { padding-bottom: calc(58px + env(safe-area-inset-bottom)); }
  /* ⚠ LE TOAST SE POSE AU-DESSUS DE LA BARRE, PAS DESSUS. À `bottom: 20px`, il recouvrait les
     quatre onglets pendant ses 3,5 secondes de vie. `pointer-events: none` (règle générale) le
     rend inoffensif de toute façon, mais un message qui masque la navigation reste illisible et
     désagréable : on le lève donc au-dessus de la barre. */
  .shop-toast { bottom: calc(58px + env(safe-area-inset-bottom) + 12px); }
  /* ⭐⭐⭐ LES TROIS BOUTONS FLOTTANTS SE PARTAGENT LE BAS DE L'ÉCRAN, SANS SE MARCHER DESSUS.
     Le calcul est le même pour tous : 58 px de barre d'onglets + la réserve d'encoche + 12 px d'air.
       · WhatsApp                    → coin de fin de ligne (voir .shop-wa) ;
       · « haut de page »            → au-dessus de lui, quand il existe (voir .shop-has-wa) ;
       · « voir le catalogue » (accueil) → côté début de ligne, à la même hauteur.
     ⚠ SANS CETTE REQUÊTE MÉDIA, LES TROIS SERAIENT SOUS LA BARRE D'ONGLETS : invisibles et
     intouchables — exactement le défaut que la barre fixe a déjà causé au pied de page (voir plus
     haut). */
  .shop-wa { bottom: calc(58px + env(safe-area-inset-bottom) + 12px); }
  /* ⚠ 56 px ET NON 68 : le bouton WhatsApp ne fait plus 56 px de côté mais 44 (la taille de « haut de
     page », voir .shop-wa), donc l'empilement se resserre d'autant — 12 px d'air + 44 px de bouton. */
  .shop-has-wa .shop-top { bottom: calc(58px + env(safe-area-inset-bottom) + 12px + 56px); }
  .home-pin { bottom: calc(58px + env(safe-area-inset-bottom) + 12px); }
  /* 56 px de haut + la réserve : c'est la cible du pouce, et les quatre onglets se partagent
     la largeur (`flex: 1 1 0`) — donc quatre cibles égales, à la même distance. */
  .shop-tab { min-height: 56px; font-size: 12px; }
  /* ⚠ CINQ ENTRÉES AU LIEU DE QUATRE : chaque cible perd de la largeur (375 / 5 = 75 px), donc
     on récupère la place sur le REMBOURRAGE LATÉRAL. ⓘ La HAUTEUR (56 px) et la taille du
     texte (12 px) ne bougent pas : ce sont elles qui rendent la cible attrapable au doigt et
     le libellé lisible — les réduire aurait échangé un défaut d'espace contre un défaut
     d'usage. Un libellé trop long pour sa colonne finit en points de suspension
     (`.shop-tab-label`), jamais en débordement. */
  .shop-tab { padding-inline: 1px; }
  .shop-tab:active { background: var(--shop-bg); }
  /* Le badge des onglets est plus petit que celui de la bande du haut : la barre fait 56 px,
     et un badge de 18 px y mangerait le pictogramme. */
  .shop-tab-badge { font-size: 10px; padding: 1px 5px; }
  /* Les badges de la bande du haut restent à 12 px (lisibilité sur téléphone, voir l'audit). */
  .shop-badge { font-size: 12px; padding: 1px 7px; }

  /* ---- Cibles tactiles ---------------------------------------------------------- */
  .btn, .btn-primary, .btn-quote, button, input[type=submit] { min-height: 44px; min-width: 44px; }
  .btn-mini { min-height: 40px; }        /* action secondaire, dans un tableau dense */
  .qty-btn { width: 44px; height: 44px; }
  /* ⭐ LES PILULES AUSSI SE PRENNENT AU POUCE : 44 px de haut, comme tout le reste sur téléphone.
     ⚠ `min-width: 44px` de la règle générique ci-dessus ne les gêne pas : elles sont plus larges
     que ça (`.qty-pill` impose 104 px), et le mot reste sur une ligne (`white-space: nowrap`). */
  .qty-pill { min-height: 44px; }
  .qty-input { min-height: 44px; }
  .btn-heart { width: 44px; min-height: 44px; }
  .card-cta, .product-actions .btn { width: 100%; }
  /* Le bouton « afficher la suite » occupe toute la largeur : c'est la cible la plus
     évidente du bas de page, et elle doit se prendre au pouce sans viser. */
  .catalog-more .btn { width: 100%; }
  /* Le titre du produit EST la cible principale d'une vignette : 44 px de haut, comme un
     bouton (la vignette elle-même reste cliquable).
     ⚠ `justify-content: center` EST OBLIGATOIRE ICI, ET PAS SEULEMENT `text-align: center` :
     une fois la cible passée en `display: flex`, c'est le flex qui place le texte, et
     `text-align` ne le recentre plus. Sans cette ligne, le nom repasserait à gauche sur
     téléphone — l'exact contraire de la demande du client. */
  .card-name { min-height: 44px; display: flex; align-items: center; justify-content: center; }
  /* La marque est un lien vers le catalogue, la pagination et le fil d'Ariane aussi : ce sont
     des cibles, pas des mots dans une phrase. */
  .shop-brand, .pager-link, .breadcrumb a { display: inline-flex; align-items: center; min-height: 44px; }
  /* Les liens isolés (bandeau, pied de page, encart « pas de prix ») sont aussi des cibles :
     un lien de 14 px de haut se rate au doigt. Les liens DANS UNE PHRASE restent des liens. */
  .shop-notice a, .shop-footer a, .product-no-price a {
    display: inline-flex; align-items: center; min-height: 44px;
  }
  /* Le délai de 300 ms du double-tap (zoom) disparaît : le tap répond tout de suite. */
  a, button, input, select, textarea, label { touch-action: manipulation; }

  /* ---- Champs : 16 px MINIMUM -----------------------------------------------
     ⚠ EN DESSOUS DE 16 px, iOS ZOOME LA PAGE dès qu'un champ reçoit le focus : le client
     se retrouve avec une page agrandie et décalée, et doit dézoomer à la main. La taille
     du texte est donc une contrainte de MISE EN PAGE, pas une question de goût.
     ⚠⚠ IL FAUT NOMMER LES CLASSES : une requête média N'AJOUTE AUCUNE SPÉCIFICITÉ, donc
     la règle générique des champs (16 px) perdait contre `.qty-input` (13 px) — les
     quantités du panier restaient à 13 px et iOS zoomait toujours. On reprend les sélecteurs
     d'origine, plus bas dans la feuille : à spécificité égale, c'est le dernier qui gagne. */
  input, select, textarea,
  .shop-search-input, .qty-input, .catalog-sort select, .sidebar-price input,
  .field input, .field textarea { font-size: 16px; }
  /* ⚠ LE SÉLECTEUR DE DICTÉE N'EST PAS DANS CETTE LISTE, ET C'EST VOLONTAIRE : la règle des
     16 px existe pour les CHAMPS DE SAISIE (iOS agrandit la page dès qu'un `<input>` ou un
     `<select>` de moins de 16 px reçoit le focus). `|FR|AR|` est fait de `<button>` — un bouton
     ne déclenche pas ce zoom, et lui donner 16 px élargirait le contrôle à ~110 px, soit la
     moitié de la ligne 2, au détriment du champ de recherche. */

  /* ---- Tableaux de données ------------------------------------------------------
     Panier, sélection, devis, commandes, relevé : 4 à 7 colonnes ne tiennent pas dans
     375 px. ⚠ On ne ROGNE PAS les colonnes (un montant tronqué est un montant faux, et
     c'est exactement ce qu'un client lit) : le tableau défile DANS sa boîte, la page non. */
  .qsel-table {
    display: block; width: 100%;
    overflow-x: auto; -webkit-overflow-scrolling: touch;
    white-space: nowrap;
  }
  .ledger-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

  /* ---- PANIER **ET** DEMANDE DE DEVIS : DES LISTES DE FICHES, PAS DES TABLEAUX QUI DÉFILENT
     (demande du client : « panier compact », puis « la liste de devis compacte comme le panier »).
     ⚠⚠ POURQUOI ON NE SE CONTENTE PAS DU DÉFILEMENT LATÉRAL. La règle ci-dessus laisse un
     tableau large défiler DANS sa boîte, ce qui est le bon compromis pour un historique, un relevé
     ou une liste de commandes (des colonnes de chiffres qu'on veut comparer). Dans le panier et
     dans la demande de devis, non : il n'y a qu'UNE ligne par article, et il fallait faire glisser
     la fiche de gauche à droite pour voir le total, puis revenir pour lire le nom — sur les deux
     écrans où le client CONSTITUE sa commande. Chaque ligne devient donc une FICHE :
     article en haut, quantité et corbeille dessous, montants en bas. Tout tient dans la largeur.
     ⚠ LES DEUX PAGES PARTAGENT LA MÊME RÈGLE (sélecteurs jumelés) : elles affichent le même
     tableau (`.qsel-table`) pour les mêmes lignes, et deux copies auraient divergé au premier
     ajustement — c'est exactement ce qui rendrait le panier compact et le devis illisible.

     ⓘ AUCUN TEXTE N'EST ÉCRIT ICI. Les étiquettes des montants (« Prix unitaire », « Total »)
     sont dans `data-label` sur les cellules, écrites par les gabarits DANS LA LANGUE DE LA PAGE,
     et posées par `content: attr(data-label)`. C'est la seule façon d'avoir des étiquettes
     traduites (et en arabe) sans texte en dur dans une feuille de style.

     ⭐ LA FICHE EST UN FLEX DONT L'ORDRE VISUEL EST PILOTÉ (`order`), ET NON UNE GRILLE À
     PLACEMENT AUTOMATIQUE. C'est ce qui met la corbeille SUR LA LIGNE DE LA QUANTITÉ, à l'autre
     bout : en grille, l'ordre du DOM décidait (article, quantité, prix, total, corbeille), et la
     corbeille se retrouvait seule sur une ligne à elle — mesuré dans le navigateur avant
     correction. ⚠ `flex-wrap` garantit qu'aucune fiche ne déborde : ce qui ne tient pas passe à
     la ligne au lieu d'élargir la page.
     ⚠ `display: block` SUR LE TABLEAU ANNULE LA RÈGLE DE DÉFILEMENT : c'est voulu, et c'est pour
     ça que cette règle vient APRÈS (à spécificité supérieure, avec DEUX classes). */
  .cart .qsel-table, .qsel .qsel-table, .checkout-recap .qsel-table { display: block; overflow: visible; white-space: normal; }
  .cart .qsel-table thead, .qsel .qsel-table thead, .checkout-recap .qsel-table thead { display: none; }
  .cart .qsel-table tbody, .cart .qsel-table tfoot,
  .qsel .qsel-table tbody, .qsel .qsel-table tfoot,
  .checkout-recap .qsel-table tbody, .checkout-recap .qsel-table tfoot { display: block; }
  .cart .qsel-table tbody tr, .qsel .qsel-table tbody tr, .checkout-recap .qsel-table tbody tr {
    display: flex; flex-wrap: wrap; align-items: center;
    gap: 8px 10px; padding: 10px; margin-bottom: 10px;
    border: 1px solid var(--shop-border); border-radius: var(--shop-radius); background: #fff;
  }
  /* ⚠ LA LIGNE « ARTICLE RETIRÉ DE LA VENTE » GARDE SON FOND D'ALERTE. La règle générale
     (`.qsel-table tbody tr.is-unavailable`) a la MÊME spécificité que la fiche ci-dessus : à
     spécificité égale, c'est la dernière écrite qui gagne — et la fiche est plus bas dans la
     feuille. Sans cette ligne, le fond jaune d'avertissement disparaîtrait sur téléphone,
     c'est-à-dire précisément là où le client remplit sa commande. */
  .cart .qsel-table tbody tr.is-unavailable, .qsel .qsel-table tbody tr.is-unavailable { background: #fffbeb; }
  /* ⚠ LES CELLULES PERDENT LEUR RÔLE DE COLONNE : bordures et rembourrages du tableau
     disparaissent, la fiche les remplace. `width: auto` est indispensable : les colonnes du
     tableau portent des largeurs FIXES (120 px pour les montants, 170 px pour la quantité) qui,
     dans une fiche, fixeraient la largeur de l'élément et feraient déborder la ligne. */
  .cart .qsel-table tbody td, .qsel .qsel-table tbody td, .checkout-recap .qsel-table tbody td {
    display: block; width: auto; padding: 0; border: 0;
    white-space: normal; text-align: start;
  }
  /* L'ordre visuel de la fiche. ⚠ `flex: 1 0 100%` = « toute la ligne » pour l'article (et pour
     la référence, quand la société affiche ses codes). */
  .cart .qsel-table td.qsel-col-ref, .qsel .qsel-table td.qsel-col-ref, .checkout-recap .qsel-table td.qsel-col-ref { order: 1; flex: 1 0 100%; font-size: 12px; }
  .cart .qsel-table td.qsel-col-article, .qsel .qsel-table td.qsel-col-article, .checkout-recap .qsel-table td.qsel-col-article { order: 2; flex: 1 0 100%; min-width: 0; }
  .cart .qsel-table td.qsel-col-qty, .qsel .qsel-table td.qsel-col-qty, .checkout-recap .qsel-table td.qsel-col-qty { order: 3; }
  /* La corbeille vient APRÈS la quantité et se pousse au bord de fin de ligne (`margin-inline-start`
     est logique : à droite en français, à gauche en arabe). */
  .cart .qsel-table td.qsel-col-act, .qsel .qsel-table td.qsel-col-act { order: 4; margin-inline-start: auto; }
  .cart .qsel-table td.qsel-col-price, .qsel .qsel-table td.qsel-col-price { order: 5; }
  /* ⚠ LE TOTAL EST LE CHIFFRE QU'ON CHERCHE : il se pose au bord de fin de ligne et il est un peu
     plus grand. */
  .cart .qsel-table td.qsel-col-total, .qsel .qsel-table td.qsel-col-total, .checkout-recap .qsel-table td.qsel-col-total {
    order: 6; margin-inline-start: auto; text-align: end; font-size: 15px; font-weight: 700;
  }
  /* L'étiquette du montant, devant le montant. `attr()` lit le `data-label` du gabarit. */
  .cart .qsel-table td.qsel-col-price::before, .cart .qsel-table td.qsel-col-total::before,
  .qsel .qsel-table td.qsel-col-price::before, .qsel .qsel-table td.qsel-col-total::before,
  .checkout-recap .qsel-table td.qsel-col-total::before {
    content: attr(data-label) " "; color: var(--shop-muted); font-size: 12px;
  }
  .cart .qsel-table td.qsel-col-total::before, .qsel .qsel-table td.qsel-col-total::before,
  .checkout-recap .qsel-table td.qsel-col-total::before { font-weight: 400; }
  /* La ligne de quantité : son étiquette au-dessus du stepper (le stepper est une boîte, une
     étiquette en ligne se collerait à ses boutons). ⓘ Dans le récapitulatif de commande il n'y a
     PAS de stepper — juste le nombre — mais l'étiquette au-dessus reste la bonne lecture : elle
     dit ce qu'est ce nombre, et elle est traduite. */
  .cart .qsel-table td.qsel-col-qty, .qsel .qsel-table td.qsel-col-qty, .checkout-recap .qsel-table td.qsel-col-qty { display: flex; flex-direction: column; gap: 4px; }
  .cart .qsel-table td.qsel-col-qty::before, .qsel .qsel-table td.qsel-col-qty::before,
  .checkout-recap .qsel-table td.qsel-col-qty::before {
    content: attr(data-label); color: var(--shop-muted); font-size: 12px;
  }
  /* ⚠ SUR LA DEMANDE DE DEVIS, LA QUANTITÉ PORTE AUSSI SON BOUTON « Mettre à jour » (le panier,
     lui, ne l'a plus). Il reste donc SUR LA MÊME LIGNE que le stepper, et il se resserre : une
     fiche de 351 px ne peut pas aligner un stepper de 132 px ET un bouton pleine largeur. */
  .qsel .qsel-qty-form { flex-wrap: nowrap; }
  .qsel .qsel-qty-form .btn-mini { min-width: 0; padding-inline: 8px; }

  /* ⚠ LE PIED DU TABLEAU DEVIENT UNE FICHE À PART, comme les lignes : c'est le récapitulatif
     (sous-total, livraison, total à payer), et il doit se lire d'un bloc. */
  .cart .qsel-table tfoot, .qsel .qsel-table tfoot, .checkout-recap .qsel-table tfoot {
    padding: 10px 12px; border: 1px solid var(--shop-border); border-radius: var(--shop-radius);
    background: var(--shop-bg);
  }
  .cart .qsel-table tfoot tr, .qsel .qsel-table tfoot tr, .checkout-recap .qsel-table tfoot tr {
    display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
    padding: 3px 0;
  }
  .cart .qsel-table tfoot td, .qsel .qsel-table tfoot td, .checkout-recap .qsel-table tfoot td { display: block; width: auto; padding: 0; border: 0; }
  /* ⚠ `!important` EST NÉCESSAIRE ICI, ET SEULEMENT ICI : la règle générale du récapitulatif
     aligne son libellé au bord de fin (`text-align: end !important` sur `.qsel-total-label`,
     écrite pour un tableau à deux colonnes). Dans une fiche, le libellé commence la ligne et le
     montant la termine — il faut donc défaire l'`!important` par un autre `!important`, à
     spécificité supérieure.
     ⓘ Les accolades sont évitées dans ce commentaire : un contrôle du dépôt vérifie qu'aucun
     commentaire de cette feuille n'en contient, sinon le découpage des règles sur les accolades
     dérape (voir test/shopChrome.test.js). */
  .cart .qsel-table tfoot .qsel-total-label, .qsel .qsel-table tfoot .qsel-total-label,
  .checkout-recap .qsel-table tfoot .qsel-total-label { text-align: start !important; }
  .cart .qsel-table tfoot .qsel-total-value, .qsel .qsel-table tfoot .qsel-total-value,
  .checkout-recap .qsel-table tfoot .qsel-total-value { text-align: end; }
  .cart .qsel-table tfoot tr:last-child td, .qsel .qsel-table tfoot tr:last-child td,
  .checkout-recap .qsel-table tfoot tr:last-child td { font-size: 16px; font-weight: 700; }

  /* ---- COMMANDE : LA MÊME COMPACITÉ, ET DES ACTIONS PLEINE LARGEUR -------------------
     ⚠ LES DEUX CARTES SE RESSERRENT (16 px → 12 px) et s'écartent moins : sur 375 px, chaque pixel
     de rembourrage est pris sur le contenu, et les deux encadrés l'un sous l'autre laissaient un
     vide inutile entre le récapitulatif et le formulaire.
     ⚠ LES BOUTONS D'ACTION PRENNENT TOUTE LA LARGEUR : « Valider ma commande » est l'action de la
     page, et une cible de 44 px de haut sur toute la largeur se vise sans regarder — c'est la même
     règle que le bouton « afficher la suite » du catalogue. Les deux boutons restent l'un SOUS
     l'autre (`flex-basis: 100%`) : côte à côte, ils seraient deux demi-cibles. */
  .checkout { gap: 12px; }
  .checkout-recap, .checkout-form { padding: 12px; }
  .checkout-recap h2, .checkout-form h2 { margin-bottom: 6px; }
  .checkout-form .form-actions { display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
  .checkout-form .form-actions .btn { width: 100%; text-align: center; }
  /* ⭐⭐ DEUX CHAMPS COURTS PAR LIGNE, LES AUTRES EN PLEINE LARGEUR.
     ⚠⚠ LA RÈGLE GÉNÉRALE DES FORMULAIRES PASSE TOUT EN UNE COLONNE SOUS 800 px (celle de
     `.form-grid`, plus haut dans ce fichier), et c'est JUSTE pour l'inscription ou les
     coordonnées, où chaque champ mérite sa ligne. Sur la commande, deux des six champs demandent
     quelques caractères (« Nom de l'adresse », « Ville ») : les empiler coûtait deux rangées de
     ~68 px, soit un dixième de la hauteur de la page, pour deux champs qui tiennent côte à côte même
     sur un écran de 320 px.
     ⭐ LA RÈGLE SE LIT « TOUT EST PLEIN, SAUF CE QUI EST DÉCLARÉ PARTAGEABLE » : c'est le sens de
     l'erreur qui compte. Un champ ajouté demain sans classe prend toute la largeur (lisible) au lieu
     d'une demi-colonne où son libellé serait tronqué ; et `field-half` doit donc venir APRÈS, à
     spécificité égale.
     ⚠ CE QUI RESTE EN PLEINE LARGEUR, ET POURQUOI : l'adresse de livraison et la note (des textes
     longs, tronqués à l'œil si on les rétrécit), et le mode de règlement (il n'a pas de voisin : à
     moitié de largeur, il laisserait un trou à côté de lui).
     ⓘ La grille est décidée par la FEUILLE DE STYLE, pas par le gabarit : le champ ne porte que
     l'intention (`field-half`), et un écran large continue de les ranger sur trois colonnes. */
  .checkout-form .form-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
  .checkout-form .form-grid > .field, .checkout-form .form-grid > .form-actions { grid-column: 1 / -1; }
  .checkout-form .form-grid > .field-half { grid-column: span 1; }

  /* ---- Textes sous 12 px --------------------------------------------------------
     L'audit en listait 20 par page (badges, références, ruptures de stock). À 10-11 px, un
     libellé se lit sur un écran de bureau, pas sur un téléphone tenu à bout de bras. */
  .card-ref, .card-price-cur, .card-out, .card-featured, small, small.muted { font-size: 12px; }
  .ledger-unpaid { font-size: 12px; }

  /* ---- Pied de page --------------------------------------------------------------
     ⚠ SUR TÉLÉPHONE, LES DEUX SECTIONS UTILES S'EMPILENT (une colonne) : à 360 px de large, deux
     colonnes de 170 px couperaient l'adresse du magasin tous les deux mots.
     ⭐⭐⭐ ET LES « LIENS UTILES » SONT MASQUÉS (demande du client : « hide liens utile in mobile »).
     ⚠ CE N'EST PAS UNE PERTE, C'EST UN DOUBLON RETIRÉ : cette colonne répète les destinations que le
     pouce a DÉJÀ sous la main, en bas de chaque page (accueil, catalogue, panier, compte — la barre
     d'onglets, voir .shop-tabbar). Sur un téléphone, elle n'ajoutait donc qu'un écran de défilement
     entre le contact du magasin et la fin de la page. ⚠ SUR GRAND ÉCRAN ELLE RESTE : la barre
     d'onglets n'existe pas au-delà de 700 px, et le pied de page y est la SEULE façon de rejoindre
     « Demande de devis » ou « Connexion ». Masquer la colonne partout aurait supprimé des liens, pas
     un doublon. */
  .shop-footer-inner {
    grid-template-columns: 1fr; gap: 16px;
    padding-bottom: calc(16px + env(safe-area-inset-bottom));
  }
  .shop-footer-links { display: none; }
  .shop-main { padding: 12px; }

  /* ---- Fenêtre de connexion et retour en haut ------------------------------------
     ⚠ LA FENÊTRE SE POSE VERS LE HAUT, PAS AU CENTRE. Le clavier logiciel monte du bas de
     l'écran dès que le champ « Téléphone » reçoit le focus (c'est justement ce que fait
     l'ouverture, voir shop.js) : une fenêtre centrée verticalement se retrouverait DERRIÈRE le
     clavier, avec ses deux champs et son bouton invisibles. `margin-block-end: auto` annule le
     centrage du navigateur, `margin-block-start` la place à 8 % du haut. */
  .shop-modal { margin-block-start: 8vh; margin-block-end: auto; }
  /* ⚠ LE BOUTON SE POSE AU-DESSUS DE LA BARRE D'ONGLETS, jamais dessus : à `bottom: 16px`, il
     recouvrirait les onglets — les deux contrôles se disputeraient le même coin de l'écran. */
  .shop-top {
    inset-inline-end: 12px;
    bottom: calc(58px + env(safe-area-inset-bottom) + 12px);
    width: 44px; padding: 0; justify-content: center;
  }
  /* Le libellé disparaît : sur 375 px, une pastille large recouvrirait une bonne partie de la
     ligne de texte qu'on est en train de lire. La CIBLE reste de 44 px et le nom accessible
     (aria-label + title) porte « Haut de page » — la flèche seule se comprend. */
  .shop-top-label { display: none; }
}

/* ⚠ SUR UN TRÈS PETIT ÉCRAN (320 px), LE MOT « DÉCONNEXION » NE TIENT PLUS à côté de la
   marque et des deux pastilles de langue : le libellé s'efface, la CIBLE RESTE (44 px), et le
   nom accessible subsiste — `aria-label` ET `title` portent « Déconnexion », dans la langue de
   la page. Un bouton de 44 px avec sa maison-qui-sort reste compréhensible ; une ligne qui
   déborde horizontalement, non.
   ⓘ Volontairement une requête média À PART, et non imbriquée dans celle de 700 px : des
   `@media` imbriquées compliquent la lecture des règles, et plusieurs contrôles du dépôt
   découpent cette feuille sur les accolades. */
@media (max-width: 400px) {
  .shop-logout-label { display: none; }
}

/* Écrans TRÈS étroits (320 px : iPhone SE, petits Android) : la recherche et le bouton
   d'envoi restent sur une ligne, quitte à raccourcir le libellé du bouton. */
@media (max-width: 360px) {
  .shop-search-submit { padding-inline: 10px; }
  .shop-search-input { padding-inline: 8px; }
  /* ⚠ LA VIGNETTE EN DEUX COLONNES À 320 px : c'est le SEUL cas où le prix et le stepper ne
     tiennent pas côte à côte (la vignette fait alors ~143 px). Ils se replient alors l'un
     au-dessus de l'autre (`card-buy`, `flex-wrap`) — c'est prévu et commenté là-bas — et les
     deux boutons du stepper se resserrent, exactement comme la règle des grilles juste
     au-dessus. La HAUTEUR des cibles reste de 44 px : le doigt ne perd rien. */
  .grid.mcols-2 .card-price { font-size: 15px; }
  .grid.mcols-2 .card-box-qty { font-size: 12px; }
}

/* ⓘ Correctif GLOBAL (toutes tailles) : iOS agrandit les textes en paysage quand la page
   utilise `text-size-adjust: auto` (défaut). On fige la taille demandée par la feuille :
   un tableau de bord à 15 px qui devient 18 px casse les hauteurs calculées. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }

/* ═══════════════════════════════════════════════════════════════════════════════════
   LE DOCUMENT IMPRIMABLE — `@media print` (voir views/shop/document.hbs)
   ═══════════════════════════════════════════════════════════════════════════════════

   ⭐⭐⭐ POURQUOI CETTE PAGE EST LA SEULE SOLUTION SANS DÉPENDANCE, ET CE QUE CE BLOC FAIT.
   Le client demande « un aperçu PDF, avec téléchargement ». Écrire un PDF binaire à la main
   est possible… sauf pour l'ARABE : les polices de base d'un PDF (Helvetica, Times) n'ont pas
   les glyphes arabes, et sans police embarquée en sous-ensemble les désignations et les noms
   de clients sortiraient en CARRÉS. La boutique rend donc une page HTML, et c'est le
   NAVIGATEUR qui l'enregistre en PDF (`window.print()`) : il dispose des polices, de la mise
   en page et des coupures de page. L'aperçu et le fichier viennent du même document.

   ⚠ CE BLOC EST LA FEUILLE D'IMPRESSION, PAS UNE SECONDE MISE EN PAGE. À l'écran, la page du
   document garde l'en-tête, la navigation et le bouton — c'est ce qui en fait un APERÇU
   utilisable (on voit ce qu'on va imprimer, et on peut revenir en arrière). À l'impression,
   tout cela DISPARAÎT : il ne doit rester que le document, sur du blanc, sans ombre ni
   couleur de fond (une économie d'encre, et un PDF lisible tel quel).
   ⚠ `print-color-adjust: exact` N'EST PAS POSÉ : dans un document commercial, un fond teinté
   qui disparaît à l'impression ne doit PAS emporter d'information — ici, aucun montant ni
   état ne dépend d'une couleur (la règle du solde le dit déjà : couleur EN PLUS des mots). */

/* Le document, à l'écran : la largeur d'une page A4, centrée, pour que l'aperçu ressemble à
   ce qui sortira de l'imprimante. */
.doc { max-width: 820px; margin: 0 auto; padding: 20px; border: 1px solid var(--shop-border); border-radius: var(--shop-radius); background: #fff; }
.doc-actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin: 0 0 8px; max-width: 820px; margin-inline: auto; }
.doc-hint { max-width: 820px; margin: 0 auto 16px; }

/* ⭐⭐ LA PAGE DE LA GESTION, AFFICHÉE TELLE QUELLE — dans une IFRAME (voir views/shop/document.hbs).
   Le modèle d'impression (bandes, format, sens de lecture) vit dans l'ERP, et la boutique reçoit la
   page DÉJÀ RENDUE : c'est ce qui garantit que le PDF du client est exactement le papier de l'équipe.
   ⚠ ON NE LA RECOLLE PAS DANS LA PAGE : elle est autonome (doctype, `<style>`, `@page`) et ses
   règles écraseraient celles de la boutique — le document sortirait déformé. L'iframe l'isole.

   ⚠⚠ ELLE EST EN MILLIMÈTRES, DONC PLUS LARGE QU'UN TÉLÉPHONE. Une feuille A4 fait 210 mm, soit
   ~794 px : posée telle quelle dans une colonne de 340 px, elle déborderait et la page entière
   défilerait de travers. Le script (public/shop/shop.js) MESURE donc la page reçue et pose
   l'échelle et la hauteur ci-dessous ; sans script, le cadre garde une hauteur de repli, le
   document défile DANS le cadre et reste lisible et imprimable — c'est le comportement d'un
   aperçu PDF ordinaire, et il n'y a donc jamais de cadre vide. */
.doc-frame-wrap { max-width: 820px; margin: 0 auto 20px; overflow: hidden; height: var(--doc-hauteur, 78vh); }
.doc-frame {
  display: block;
  width: var(--doc-largeur, 100%);
  height: var(--doc-hf, 1123px);
  border: 1px solid var(--shop-border);
  border-radius: var(--shop-radius);
  background: #fff;
  transform: scale(var(--doc-echelle, 1));
  transform-origin: top left;
}
/* ⭐⭐ EN ARABE, LE CADRE SE RÉDUIT DEPUIS SON COIN DROIT (demande du client : « fix pdf display
   frame in arabic language »).
   ⚠⚠ CE QUI SE PASSAIT. Le cadre garde la LARGEUR RÉELLE de la feuille (≈ 794 px pour un A4) et
   n'est réduit que par une transformation, ancrée sur `top left`. Or en écriture de droite à
   gauche, un bloc plus large que sa colonne DÉBORDE VERS LA GAUCHE : sa partie visible est son
   bord DROIT. Le cadre était donc réduit autour de son coin gauche — un point situé hors de la
   colonne, masqué par `overflow: hidden` — et le client arabe voyait un aperçu décalé, voire un
   cadre vide, alors que le document était bien chargé.
   ⚠⚠ `transform-origin` N'ACCEPTE PAS DE MOT-CLÉ LOGIQUE (`inline-start` n'existe pas pour cette
   propriété) : c'est le seul endroit de la feuille où il faut nommer un côté physique, et la règle
   est donc écrite explicitement, avec son motif. Toute autre solution (retourner le cadre, changer
   son `direction`) toucherait au document lui-même, qui doit rester tel que la gestion le rend. */
html[dir="rtl"] .doc-frame { transform-origin: top right; }
.doc-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; border-bottom: 2px solid var(--shop-navy); padding-bottom: 12px; }
.doc-company { font-size: 13px; color: var(--shop-navy); }
.doc-company strong { font-size: 17px; }
.doc-ident { text-align: end; font-size: 13px; }
.doc-ident h2 { margin: 0 0 6px; font-size: 18px; color: var(--shop-accent); }
.doc-ident span { color: var(--shop-muted); }
.doc-parties { display: flex; gap: 24px; flex-wrap: wrap; margin: 16px 0; }
.doc-parties > div { flex: 1 1 240px; }
.doc-parties h3, .doc-meta { font-size: 13px; }
.doc-parties h3 { margin: 0 0 4px; color: var(--shop-muted); font-weight: 600; }
.doc-meta { display: flex; flex-direction: column; gap: 4px; margin: 12px 0; }
.doc-meta > div { display: flex; gap: 8px; }
.doc-meta span { color: var(--shop-muted); }
.doc-table { margin-top: 12px; }
.doc-totals { margin-top: 14px; display: flex; flex-direction: column; gap: 4px; align-items: flex-end; font-size: 13px; }
.doc-totals > div { display: flex; gap: 12px; }
.doc-totals span { color: var(--shop-muted); }
.doc-total-strong { font-size: 16px; font-weight: 700; border-top: 1px solid var(--shop-border); padding-top: 4px; }

@media print {
  /* ⚠ TOUT CE QUI N'EST PAS LE DOCUMENT DISPARAÎT : en-tête du site, barre de navigation du
     bas, pied de page, bandeau d'information, boutons, surcouche d'image et messages. Ce qui
     n'est pas listé ici sortirait sur le PDF — un en-tête de site au milieu d'une facture est
     le genre de détail qui fait douter du document. */
  .shop-header, .shop-tabbar, .shop-footer, .shop-notice, .shop-toast, .shop-lightbox,
  .shop-top, .shop-modal,
  .doc-actions, .doc-hint, .page-title, .account-tabs, .account-tablist { display: none !important; }

  /* Le papier est blanc, et le document n'a plus ni cadre ni ombre. */
  html, body.shop { background: #fff; }
  body { padding-bottom: 0 !important; }
  .shop-main { max-width: none; margin: 0; padding: 0; }
  .doc { max-width: none; margin: 0; padding: 0; border: 0; border-radius: 0; }

  /* ⭐ LE DOCUMENT DE LA GESTION, S'IL EST AFFICHÉ. On annule l'échelle posée par le script (une
     page réduite pour tenir à l'écran s'imprimerait en timbre-poste) et l'on donne au cadre la
     hauteur d'une feuille : `Ctrl+P` sur CETTE page imprime alors le document à sa taille.
     ⚠ LE BOUTON « Télécharger le PDF », LUI, N'EMPRUNTE PAS CE CHEMIN : il appelle
     `contentWindow.print()` et fait imprimer la page de la gestion ELLE-MÊME — celle qui porte les
     vrais `@page` du modèle, donc le format, les marges et les coupures de page de la gestion, y
     compris sur un document de plusieurs pages (voir public/shop/shop.js). */
  .doc-frame-wrap { height: auto; max-width: none; overflow: visible; }
  .doc-frame { width: 100%; height: 260mm; transform: none; border: 0; border-radius: 0; }

  /* ⚠ LES TABLEAUX NE DOIVENT PAS DÉFILER SUR UNE FEUILLE : à l'écran, un tableau large
     défile dans sa boîte (`.qsel-table` en `display: block` sur téléphone) ; sur papier, un
     défilement veut dire « colonnes coupées ». On rétablit donc un vrai tableau, et l'on
     évite qu'une ligne soit coupée en deux par un saut de page. */
  .qsel-table, .ledger-scroll { display: table; width: 100%; overflow: visible; white-space: normal; }
  .doc-table tr, .doc-totals { page-break-inside: avoid; break-inside: avoid; }
  .doc-totals { align-items: flex-end; }
}
