/* Mise en page de PingHub. Aucune couleur en dur ici : tout vient des variables de themes.css
   (docs/design/ui-mockup.md), à la seule exception de la palette fixe des catégories (plus bas) et des
   NEUTRES — c'est-à-dire des valeurs qui ne sont la couleur de personne : les voiles (noir à 50 %), les
   ombres et le reflet du verre (noir et blanc translucides, dans :root), le bandeau du pied des tuiles
   (noir à 4 %) et son alerte (orange à 18 %), le jaune du bandeau de staging, le noir et blanc de
   l'impression. Un neutre doit rendre le même service sur les quatre thèmes ; une couleur, non.
   Bootstrap reste chargé pour la grille et les espacements ; ses classes de couleur sont réécrites ici
   avec les variables, pour ne pas réécrire chaque page (ui-mockup § 8).
   Les DIMENSIONS partagées — rail, hauteurs d'action, rayons, ombres, durée du retour tactile — sont
   toutes dans le :root qui suit, et nulle part ailleurs : les quatre thèmes ont le même cadre, seule la
   palette leur appartient (vague 16, point 7). */

/* ----- Dimensions (23/09/2026, densité iPad A16) -----
   Les tailles de cadre qui se répètent d'une règle à l'autre sont nommées ici, et nulle part ailleurs : le rail
   est écrit dans cinq règles (sa largeur, le décalage des pages, celui de la coque de caisse, celui du voile des
   modales), la hauteur des boutons de barre dans quatre. Elles vivent dans app.css et non dans themes.css, qui
   est la feuille des couleurs (ui-mockup § 1) : une dimension n'appartient à aucun thème, les quatre thèmes ont
   exactement le même cadre. Elles sont dans :root pour être lisibles partout, médias compris.
   Valeurs arbitrées par l'utilisateur le 23/09/2026 sur le matériel du club (iPad A16, 1180 × 820 en paysage) :
   le rail passe de 72 à 56, ses icônes restent à 24, les actions de caisse tombent de 44 à 36 de haut.
   --touch-target garde les 44 px d'origine : ils restent la règle partout où un toucher écrit de l'argent
   (vendre, ouvrir un fût, retirer une ligne) — voir docs/design/ui-mockup.md § 1.7. */
:root {
    /* Rail de navigation : largeur du bandeau, cible d'une entrée, taille des icônes dessinées à la main. */
    --rail-width: 56px;
    --rail-item-size: 48px;
    --rail-icon-size: 24px;
    /* Barres de la caisse : hauteur d'un bouton « icône + libellé court » et de son icône.
       36 ET PAS 40, bien que la maquette de la vague 16 dessine ses boutons de barre à 40 px. Deux raisons,
       dans cet ordre : la valeur a été ARBITRÉE PAR L'UTILISATEUR le 23/09/2026 sur le matériel du club (elle
       tombait alors de 44 à 36), et surtout le compte ne passe pas. --action-height tient DEUX lignes de la
       caisse (l'opérateur et ses actions, puis les catégories) à 45 px chacune : 36 → 40 en coûte 4 par
       ligne, soit 8 px pris aux tuiles. Il tient aussi, depuis le 06/10/2026, la hauteur de la pastille de
       l'opérateur posée dans la première ligne (.operator-badge-inline) : à 40 px, elle grandirait d'autant.
       LE CAS LE PLUS SERRÉ A CHANGÉ CE JOUR-LÀ, l'en-tête ayant rendu aux tuiles les 42 px de la barre du
       badge : ce n'est plus la quatrième rangée de tuiles AU FÛT à 1024 × 768 (592 px pour 649,2 disponibles
       en prod, 57,2 px de marge), c'est la SIXIÈME rangée ordinaire sur l'instance de test au même écran —
       616 px pour 625,2, soit 9,2 px. À 40 px il manquerait 7 px et la zone des tuiles défilerait là où elle
       ne défile pas aujourd'hui.
       Qui veut les 40 px de la maquette doit d'abord refaire ce budget, mesure en main sur l'iPad. */
    --action-height: 36px;
    --action-icon-size: 20px;
    /* Tuile de vente : hauteur minimale de la zone qui vend, et seule valeur que le curseur de la caisse change
       (23/09/2026). La page la réécrit en pixels sur .caisse-tiles (style en ligne, aucun JavaScript) ; 6 rem =
       96 px est la borne haute du curseur, c'est-à-dire la taille d'origine. En dessous, tout ce qui en découle
       suit (largeur de colonne, rembourrage, nom, prix) ; ce qui engage une écriture ne suit jamais —
       « Nouveau fût », l'indicateur des verres et les flèches de rangement gardent --touch-target. */
    --tile-min-height: 6rem;
    /* Plancher tactile de ce qui engage une écriture : inchangé depuis l'origine.
       44 ET PAS 34, bien que la maquette de la vague 16 dessine ses boutons de ligne à 34 px. Ce n'est pas
       un goût : 44 px est le plancher d'accessibilité d'une cible tactile, et la règle est écrite quatre
       lignes plus haut dans ce même commentaire — « partout où un toucher écrit de l'argent (vendre,
       ouvrir un fût, RETIRER UNE LIGNE) », ui-mockup § 1.7. Les « − » et « + » d'une ligne de vente sont
       nommés dans cette liste. Descendre à 34 px rognerait de 23 % la cible du seul geste qui RETIRE un
       article déjà vendu, et le ferait sur la surface la plus dense de l'écran, où deux boutons aux effets
       opposés se touchent déjà par erreur (voir .caisse-line-remove). Non négociable sans l'accord de
       l'utilisateur, qui l'a refusé le 05/10/2026 après mesure, puis le 06/10/2026 une troisième fois.
       CE QUI A ÉTÉ ACCORDÉ LE 06/10/2026, et c'est la sortie de trois refus d'affilée : l'APPARENCE de la
       maquette sans sa cible. Voir --touch-inner juste en dessous. */
    --touch-target: 44px;
    /* Le carré PEINT à l'intérieur de la cible (06/10/2026, vague 17, point 5). La maquette dessine les
       « − » et « + » d'une ligne de vente en carrés de 34 px ; composants-jetons.html l'écrit lui-même en
       titre de section — « 34 ligne — touche ≥ 44 avec sa marge ». C'est exactement ce que fait ce jeton :
       la cible garde ses 44 px (--touch-target, le plancher de ce qui retire un article déjà vendu) et le
       fond, le trait et le rayon ne peignent que les 34 px du milieu. Les 5 px de part et d'autre sont une
       bordure TRANSPARENTE — le doigt les touche, l'œil ne les voit pas. L'écart entre deux lignes y gagne
       même en lisibilité : deux carrés de 34 px séparés de 18 px apparents, là où deux pleins de 44 px
       n'en laissaient que 8.
       Qui veut remonter le carré à 44 px n'a qu'une valeur à changer ici ; qui veut descendre la CIBLE
       doit convaincre l'utilisateur, qui a dit non trois fois. */
    --touch-inner: 34px;
    /* Même mécanique, deuxième valeur : la PASTILLE de 24 px que la maquette pose dans le pied d'une tuile
       au verre (« Nouveau fût »). Elle non plus ne descend pas la cible — « ouvrir un fût » est dans la
       liste de --touch-target —, elle ne descend que ce qui est peint. Deux valeurs et pas une seule :
       34 est un carré de ligne de vente, 24 une pastille de bandeau, et la maquette les dessine
       différentes parce qu'elles ne pèsent pas le même geste. */
    --pill-inner: 24px;
    /* Air autour du panneau de vente (06/10/2026, vague 17, point 5) : les 14 px de la maquette, qui
       DÉTACHENT la carte des quatre bords au lieu de la coller à droite. Nommé parce qu'il se paie en
       largeur de tuiles et que le chiffre doit être trouvable : 2 × 14 = 28 px pris à la zone des tuiles
       (voir le budget recompté sur .caisse-tiles), ce qui ne coûte AUCUNE colonne ni à 1180 ni à 1024. */
    --panel-gap: 0.875rem;
    /* Largeur utile d'une feuille de caisse (clôture, comptage) : écrite une fois, portée par deux règles. */
    --sheet-width: min(46rem, 100%);
    /* Hauteur du bandeau de l'instance de test. NULLE EN PROD, et c'est l'essentiel : les cinq règles qui la
       déduisent (rail, page, coque de caisse, voile des modales, voile du verrou) calculent alors « - 0px » et
       rendent exactement les mêmes pixels qu'avant l'ajout du bandeau. Le staging seul la relève, plus bas. */
    --staging-banner-h: 0px;

    /* ----- Rayons (05/10/2026, vague 16 ; six valeurs depuis le 06/10/2026) -----
       Ils vivaient dans themes.css, un par thème, et valaient 16 px dans les quatre : un rayon n'appartient
       à aucun thème, il monte donc ici avec les autres dimensions. Six valeurs, pas une de plus, et aucune
       règle n'écrit un rayon en pixels ailleurs.
       --radius reste le rayon des SURFACES ORDINAIRES (cartes, messages, listes) et garde ses 16 px : les
       vingt-neuf règles qui le lisent rendent exactement les mêmes pixels qu'avant la vague. */
    --radius: 16px;
    --radius-button: 12px;   /* boutons et champs (la maquette dit 11 à 14 : 12 est le cran du milieu) */
    /* SIXIÈME VALEUR (06/10/2026, vague 17, point 5), et elle n'habille qu'une cible : « Payer ». La
       maquette de la modale (caisse-modale-payer.html, ligne 222) l'écrit « border-radius: 14px » là où
       tous ses autres boutons sont à 11 ou 12 — c'est le seul bouton de l'application à qui elle donne un
       rayon à lui, et c'est aussi le plus grand (72 px de haut, voir .caisse-pay) : à cette taille, 12 px
       le fait paraître plus carré que les boutons de 48 qui l'entourent. Une valeur nommée plutôt qu'un
       « 14px » écrit dans la règle, pour que la famille des rayons reste lisible d'un coup d'œil. */
    --radius-pay: 14px;
    --radius-tile: 18px;     /* tuile de vente, tuile de stock */
    --radius-panel: 24px;    /* panneau de vente, la plus grande surface qui se détache du fond */
    --radius-modal: 20px;    /* modales : feuilles de caisse et confirmations */
    --radius-pill: 999px;    /* pastilles, badges, toast */

    /* ----- Relief (05/10/2026, vague 16) -----
       Les quatre thèmes reçoivent les mêmes ombres ; seule la palette leur appartient. Sur « encre » un noir
       à 10 % sur un fond presque noir ne se voit pas, et c'est voulu : un thème de nuit se passe d'ombres,
       il n'a pas besoin qu'on en invente pour lui.
       Le REFLET est ce qui fait le verre : une ligne blanche d'un pixel sur l'arête haute. Il est en
       box-shadow intérieur et non en bordure, pour ne pas prendre la place du trait de --border, qui lui
       doit rester un vrai trait (3:1, WCAG 1.4.11). */
    --shadow-tile: 0 1px 2px rgba(23, 24, 28, 0.05), 0 10px 30px rgba(23, 24, 28, 0.10);
    --shadow-panel: 0 1px 2px rgba(23, 24, 28, 0.05), 0 20px 50px rgba(23, 24, 28, 0.14);
    --shadow-modal: 0 24px 64px rgba(23, 24, 28, 0.28);
    --glass-sheen: 0 1px 0 rgba(255, 255, 255, 0.95) inset;

    /* Durée de l'état touché (point 5 de la vague 16). Un seul chiffre, lu par toutes les cibles. */
    --touch-feedback: 80ms;
}

/* ----- Bandeau de l'instance de test (staging) -----
   Affiché seulement si PingHub:Environment vaut « Staging » (App.razor pose data-staging sur <html>, et c'est le
   composant racine qui écrit le bandeau : un seul exemplaire pour la zone interactive comme pour /Account et
   /Error). Aucune interaction : il ne se ferme pas, il n'y a rien à toucher.
   24 px, pas plus : chaque pixel est pris à la caisse (voir le budget vertical sur .caisse-tiles). Le texte est en
   13 px sur une seule ligne, tronqué plutôt que replié — une deuxième ligne changerait la hauteur réservée.
   Le jaune est en dur, comme le noir et blanc de l'impression : une instance de test n'est pas un thème
   (ui-mockup § 8). Il doit crier pareil sur les quatre thèmes, clairs comme sombre. */
html[data-staging] {
    --staging-banner-h: 24px;
}

.staging-banner {
    /* sticky : le bandeau reste en haut quand une page de lecture défile, mais il occupe sa place dans le flux —
       un « fixed » ne prendrait aucune hauteur et recouvrirait le badge de l'opérateur. */
    position: sticky;
    top: 0;
    z-index: 1095;          /* au-dessus du rail (1090) ; ils ne se croisent pas, le rail commence sous lui */
    height: var(--staging-banner-h);
    line-height: var(--staging-banner-h);
    padding: 0 0.5rem;
    background-color: #ffd400;
    color: #1b1b1b;
    font-size: 0.8125rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ----- Manrope (05/10/2026, vague 16) -----
   LE FICHIER EST DANS LE DÉPÔT (wwwroot/fonts/), et c'est la raison exigée par CLAUDE.md § 10 pour tout
   ajout : la tablette du comptoir peut être sur le réseau du club SANS INTERNET. L'application répond
   alors — le serveur est au club — mais tout appel vers l'extérieur part dans le vide. Deux conséquences,
   et ce sont les deux qui ont décidé :
     1. HORS LIGNE, LA POLICE EST QUAND MÊME LÀ. Elle est servie depuis wwwroot, comme app.css. Pas
        d'empreinte « ?v= » sur cette URL-ci, et ce n'est pas un oubli : AssetVersion n'empreinte que ce
        qu'une page Razor écrit, et celle-ci est écrite dans une feuille statique. Elle n'en a pas besoin,
        le fichier étant immuable — une autre version de Manrope serait un autre nom de fichier.
        Ce qui compte n'est pas le dessin des lettres, ce sont les CHIFFRES À LARGEUR FIXE : c'est tout
        l'intérêt du changement de police, et il serait perdu la moitié du temps si la police venait du
        réseau.
     2. AUCUN APPEL SORTANT. Plus de requête vers fonts.gstatic.com, et plus de preconnect dans App.razor
        (retiré le 05/10/2026 : il n'avait plus d'objet). Une application qui ne sort nulle part se défend
        plus facilement qu'une application qui garde un appel pour rien.
   Ce n'est PAS une librairie : un fichier de police, 24 Ko, sans code, sans API, sans mise à jour à suivre.
   Provenance : la feuille officielle « fonts.googleapis.com/css2?family=Manrope:wght@500..800&display=swap »
   demandée avec un navigateur moderne, dont on a repris l'URL woff2 du sous-ensemble « latin » et le
   fichier tel quel (v20, police variable, axe wght 200-800, 280 glyphes). Licence SIL OFL 1.1, copiée à
   côté dans fonts/OFL.txt — l'OFL demande que la licence accompagne le fichier.
   POURQUOI LE SOUS-ENSEMBLE « LATIN » SUFFIT au français : il porte U+0000-00FF (é, û, ×, ·), l'euro, le
   signe moins U+2212 et U+2000-206F (le chevron « › » des tuiles groupées, les guillemets, les points de
   suspension). Les trois signes que Manrope n'a pas — « ◀ », « ▶ » (rangement des tuiles) et « ✓ »
   (personne choisie) — tombent glyphe par glyphe sur system-ui, ce qui est le comportement voulu : ils
   sont dessinés par le système, pas par la police de texte. Vérifié sur le fichier livré.
   font-display: swap EST GARDÉ même en local, et ce n'est pas superflu : sa période de blocage est NULLE,
   donc aucun texte invisible pendant que le fichier se charge — la caisse peint en system-ui à la première
   image et échange une fraction de seconde plus tard. L'agencement tient dans les deux cas (voir le budget
   de .caisse-tiles) ; Manrope est la plus large des deux, c'est elle qui sert de pire cas aux calculs de
   largeur de .caisse-line-name et de .caisse-action-label. */
@font-face {
    font-family: 'Manrope';
    font-style: normal;
    font-weight: 500 800;
    font-display: swap;
    src: url(fonts/manrope-latin-500-800.woff2) format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ----- Base ----- */
/* La police et les CHIFFRES TABULAIRES valent pour les quatre thèmes : ce n'est pas un habillage, c'est la
   lecture des montants. Manrope a des chiffres proportionnels par défaut — son « 1 » mesure 6,75 px là où
   son « 0 » en mesure 10,27 (à 16 px, graisse 600) — et sans tabular-nums une colonne de prix danserait de
   trois pixels et demi d'une ligne à l'autre. Avec, chaque chiffre vaut 9,92 px et les montants s'alignent.
   Posé sur html ET body : les deux portent déjà la famille, et font-variant-numeric s'hérite. */
html, body {
    font-family: 'Manrope', system-ui, sans-serif;
    font-variant-numeric: tabular-nums;
}

html {
    /* Safari grossit de lui-même certains textes en paysage : les tailles posées ici sont les bonnes. */
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
}

body {
    background-color: var(--bg);
    color: var(--fg);
    /* Halos du thème verre. Les autres thèmes ne déclarent pas --bg-image : ils lisent « none » et gardent
       leur fond uni, au pixel près comme avant la vague. */
    background-image: var(--bg-image, none);
    background-repeat: no-repeat;
}

/* Zoom au double toucher de Safari (iPad) : deux touchers rapprochés — le geste d'une vente de deux articles —
   agrandissaient la page. « touch-action: manipulation » supprime la détection du double toucher, et le délai
   de 300 ms avec elle, sans rien retirer au pincement : agrandir à deux doigts reste possible partout.
   Posé sur les cibles tactiles, jamais sur le document entier. */
button, a, label, summary, input, select, textarea,
.btn, .btn-back, .rail-item, .rail-theme, .home-card, .stock-tab, .category-chip,
.caisse-action, .caisse-strip-link, .caisse-toast, .tab-tile, .journal-summary,
.pad-key, .pad-validate, .caisse-pay, .member-button, .letter-button, .open-tab-button,
.tile-sell, .tile-action, .tile-container, .tile-glasses, .tile-arrow,
.stock-tile-action, .stock-tile-wide, .operator-badge-change {
    touch-action: manipulation;
}

/* ----- État TOUCHÉ (05/10/2026, vague 16, point 5) -----
   La même liste de cibles que touch-action ci-dessus, et pour la même raison : c'est l'inventaire de ce qui
   se touche. Le doigt couvre le bouton qu'il presse ; le seul retour qu'on perçoit est celui qui déborde du
   doigt — l'assombrissement du bord — et celui qu'on sent à travers le mouvement de la surface. D'où les
   deux : 6 % plus sombre (--touch-filter, le thème sombre éclaircit) et un pixel vers le bas, en 80 ms.
   AUCUN EFFET AU SURVOL, et ce n'est pas un oubli : il n'y a pas de souris au bar, et un survol reste collé
   après un toucher sur iPad — le bouton garderait l'air d'être resté enfoncé. Les règles de survol qui
   existaient (le rail, les actions des tuiles de stock, le trait des boutons discrets) ont été retirées ;
   celles qui ANNULENT un survol de Bootstrap (.btn:hover, a:hover) restent, elles font le contraire.
   :focus-visible garde sa place : c'est le clavier, pas la souris. */
button:active, a:active, .btn:active, .btn-back:active, .rail-item:active, .rail-theme:active,
.home-card:active, .stock-tab:active, .category-chip:active, .caisse-action:active,
.caisse-strip-link:active, .caisse-toast:active, .tab-tile:active, .journal-summary:active,
.pad-key:active, .pad-validate:active, .caisse-pay:active, .member-button:active, .letter-button:active,
.open-tab-button:active, .tile-sell:active, .tile-action:active, .tile-container:active, .tile-glasses:active, .tile-arrow:active,
.stock-tile-action:active, .stock-tile-wide:active, .operator-badge-change:active {
    filter: var(--touch-filter);
    transform: translateY(1px);
}

/* La transition ne porte QUE sur ces deux propriétés : rien d'autre ne doit s'animer dans une caisse.
   Elle est posée sur la même liste au repos, pour que le retour à l'état normal dure aussi 80 ms.
   prefers-reduced-motion est respecté plus bas. */
button, a.btn, .btn-back, .rail-item, .rail-theme, .home-card, .stock-tab, .category-chip,
.caisse-strip-link, .caisse-toast, .tab-tile, .journal-summary {
    transition: filter var(--touch-feedback) linear, transform var(--touch-feedback) linear;
}

/* Un réglage système qui demande moins de mouvement garde l'assombrissement et perd la translation :
   le retour tactile reste, le déplacement part. */
@media (prefers-reduced-motion: reduce) {
    button, a.btn, .btn-back, .rail-item, .rail-theme, .home-card, .stock-tab, .category-chip,
    .caisse-strip-link, .caisse-toast, .tab-tile, .journal-summary {
        transition: none;
    }

    button:active, a:active, .btn:active, .btn-back:active, .rail-item:active, .rail-theme:active,
    .home-card:active, .stock-tab:active, .category-chip:active, .caisse-action:active,
    .caisse-strip-link:active, .caisse-toast:active, .tab-tile:active, .journal-summary:active,
    .pad-key:active, .pad-validate:active, .caisse-pay:active, .member-button:active, .letter-button:active,
    .open-tab-button:active, .tile-sell:active, .tile-action:active, .tile-container:active, .tile-glasses:active, .tile-arrow:active,
    .stock-tile-action:active, .stock-tile-wide:active, .operator-badge-change:active {
        transform: none;
    }
}

a {
    color: var(--fg);
}

/* Survol et focus d'un lien : Bootstrap y impose son bleu (#0a58ca) avec une spécificité plus forte
   que .card ou .category-* ; un lien garde donc la couleur de la surface qui le porte.
   Les cas particuliers (rail, retour, pastilles de catégorie) reprennent la main plus bas. */
a:hover, a:focus {
    color: inherit;
}

code {
    color: var(--fg);
}

h1:focus {
    outline: none;
}

/* Focus clavier visible partout ; sur le rail, la couleur de focus d'un thème peut manquer de
   contraste sur le fond du rail : on y reprend la couleur des entrées au repos. */
:focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: 2px;
}

/* Bootstrap remet un halo bleu sur .btn:focus (spécificité plus forte que :focus-visible). */
.btn:focus, .btn:focus-visible, .btn:active:focus, .btn-check:focus + .btn, .form-check-input:focus {
    box-shadow: none;
}

.btn:focus-visible, .btn-back:focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: 2px;
}

.rail :focus-visible {
    outline-color: var(--rail-fg);
}

/* Transition douce au changement de thème (200 ms), jamais sur les tailles ni les positions. */
body, .page, .rail, .rail-item, .rail-theme, .btn, .btn-back, .card, .home-card, .list-group-item,
.form-control, .form-select, .alert, .caisse-shell, .caisse-header, .caisse-bar, .caisse-action, .caisse-action-icon,
.caisse-panel, .caisse-strip,
.caisse-dialog, .caisse-sheet, .pad-display, .tile-warning, .stock-tile, .stock-tile-action, .stock-tile-wide,
.stock-tab, .tab-tile, #blazor-error-ui {
    transition: background-color 200ms, color 200ms, border-color 200ms;
}

@media (prefers-reduced-motion: reduce) {
    body, .page, .rail, .rail-item, .rail-theme, .btn, .btn-back, .card, .home-card, .list-group-item,
    .form-control, .form-select, .alert, .caisse-shell, .caisse-header, .caisse-bar, .caisse-action, .caisse-action-icon,
    .caisse-panel, .caisse-strip,
    .caisse-dialog, .caisse-sheet, .pad-display, .tile-warning, .stock-tile, .stock-tile-action, .stock-tile-wide,
    .stock-tab, .tab-tile, #blazor-error-ui {
        transition: none;
    }
}

/* ----- Rail fixe de 56 px (ui-mockup 4.1, resserré le 23/09/2026) ----- */
.rail {
    position: fixed;
    /* Le haut cède la hauteur du bandeau de staging (0 en prod) : le rail est « fixed », il ignore le flux et
       passerait sinon sous le bandeau. */
    inset: var(--staging-banner-h) auto 0 0;
    width: var(--rail-width);
    z-index: 1090;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    padding: 0.5rem 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    background-color: var(--rail-bg);
    color: var(--rail-fg);
    /* 1/5 des surfaces qui floutent (vague 16, point 3). Les thèmes qui ne déclarent pas --glass-blur
       lisent « none » et ne paient rien : aucune couche de composition en plus, aucun pixel différent.
       Le rail est le meilleur candidat du lot — il est fixe, étroit, et ne change jamais de taille. */
    backdrop-filter: var(--glass-blur, none);
    -webkit-backdrop-filter: var(--glass-blur, none);
    box-shadow: var(--glass-sheen), inset -1px 0 0 rgba(23, 24, 28, 0.04);
}

/* Zone sûre (viewport-fit=cover) : le rail reste collé au bord, mais ses 56 px utiles commencent après
   l'encoche. Déclaration séparée, après la précédente : un navigateur sans env() garde simplement 56 px. */
.rail {
    width: calc(var(--rail-width) + env(safe-area-inset-left));
    padding: calc(0.5rem + env(safe-area-inset-top)) 0 calc(0.5rem + env(safe-area-inset-bottom)) env(safe-area-inset-left);
}

.rail-logo {
    display: block;
    line-height: 0;
}

    /* Le logo tient dans le rail resserré : il prend la cible d'une entrée, jamais la largeur du bandeau. */
    .rail-logo img {
        width: var(--rail-item-size);
        height: var(--rail-item-size);
        object-fit: contain;
    }

.rail-user {
    max-width: calc(var(--rail-width) - 0.5rem);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 0.7rem;
    /* --muted n'est pas lisible sur le rail du thème « club » : on reprend la couleur des entrées. */
    color: var(--rail-fg);
}

.rail-items {
    /* Occupe l'espace libre : « Quitter » reste en bas même quand les pastilles ne sont pas rendues (pages Account). */
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    margin-top: 0.25rem;
}

/* Entrée du rail : elle occupe la largeur du bandeau moins un filet d'air, et sa hauteur de cible reste bien
   au-dessus du pouce (48 px). Le libellé de 11 px passe encore à cette largeur ; s'il tient sur deux lignes
   (« Connexion »), l'entrée grandit, elle ne rogne pas le texte. */
.rail-item {
    width: calc(var(--rail-width) - 0.25rem);
    min-height: var(--rail-item-size);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 4px 2px;
    border: 0;
    border-radius: 12px;
    background-color: transparent;
    color: var(--rail-fg);
    text-align: center;
    text-decoration: none;
}

    /* Pas de .rail-item:hover (vague 16, point 5) : il peignait l'entrée survolée comme l'entrée ACTIVE, et
       sur iPad le survol reste collé après un toucher — deux entrées semblaient actives en même temps. */
    .rail-item.active {
        color: var(--rail-active-fg);
        background-color: var(--rail-active-bg);
    }

.rail-icon {
    width: var(--rail-icon-size);
    height: var(--rail-icon-size);
    flex: 0 0 auto;
}

.rail-label {
    font-size: 11px;
    line-height: 1.1;
}

/* Pastilles de thème : une colonne, cible de 44 px (elles tiennent dans le rail de 56), disque de 28 px
   aux couleurs du thème. */
.rail-themes {
    margin-top: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.25rem;
    padding: 0.5rem 0;
}

.rail-theme {
    width: var(--touch-target);
    height: var(--touch-target);
    color: var(--rail-fg);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 1px solid var(--rail-fg);
    border-radius: 999px;
    cursor: pointer;
}

    .rail-theme.selected {
        outline: 3px solid var(--ok);
        outline-offset: 2px;
    }

.rail-theme-dot {
    width: 28px;
    height: 28px;
    border-radius: 999px;
}

.rail-error {
    max-width: calc(var(--rail-width) - 0.5rem);
    margin: 0 0 0.25rem;
    font-size: 0.65rem;
    line-height: 1.1;
    text-align: center;
    color: var(--danger);
}

.rail-logout {
    margin: 0;
}

/* ----- Contenu hors caisse ----- */
/* Pages de lecture (Stock, Ardoises, Compta, Admin) : elles défilent normalement, seule la hauteur minimale
   suit la fenêtre. 100dvh après 100vh : sur iPad, 100vh compte la barre d'adresse et ajoute un défilement
   d'une page pourtant vide. */
.page {
    /* « - var(--staging-banner-h) » (0 en prod) : sinon le bandeau, qui est dans le flux au-dessus, ajouterait
       sa hauteur et ferait défiler d'autant une page pourtant vide. */
    min-height: calc(100vh - var(--staging-banner-h));
    min-height: calc(100dvh - var(--staging-banner-h));
    margin-left: var(--rail-width);
    margin-left: calc(var(--rail-width) + env(safe-area-inset-left));
    padding-right: env(safe-area-inset-right);
    background-color: var(--bg);
}

.content {
    padding: 1rem 1.25rem;
}

/* Retour en haut de page (design 04-habillage 2.4) : un bouton secondaire, jamais un lien bleu souligné. */
.btn-back {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    padding: 0 1rem;
    margin-bottom: 0.5rem;
    border: 1px solid var(--btn-border);
    border-radius: var(--radius-button);
    background-color: var(--btn-bg);
    color: var(--fg);
    text-decoration: none;
}

    /* Le trait ne se renforce plus au survol (vague 16, point 5) ; la couleur, elle, reste écrite : elle
       n'ajoute rien, elle empêche le bleu de lien que Bootstrap pose sur a:hover. */
    .btn-back:hover {
        color: var(--fg);
    }

/* ----- Accueil (ui-mockup 5) : logo, phrase et cartes d'accès ----- */
.home-intro {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
    text-align: center;
}

.home-logo {
    width: 96px;
    height: 96px;
    object-fit: contain;
}

/* Titre long : on l'équilibre sur deux lignes au plus plutôt que de laisser un mot seul en bas. */
.home-title {
    margin: 0;
    max-width: 46rem;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.3;
    text-wrap: balance;
}

/* Cartes réduites (plus que le titre à lire) : elles tiennent toutes sur la hauteur de l'écran de référence.
   Rangée centrée et largeur plafonnée à celle du titre : le bloc des cartes tombe sur l'axe du logo, et la
   dernière rangée incomplète (cinq cartes = 3 + 2) reste centrée elle aussi — ce qu'une grille ne sait pas
   faire, ses colonnes étant posées d'avance. Les cartes gardent donc une largeur fixe, identiques d'une
   rangée à l'autre. */
.home-grid {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.75rem;
    max-width: 47rem;
    margin-inline: auto;
}

/* Carte = lien plein bloc : toute la surface est cliquable, aucun chiffre lu ici.
   Filet supérieur à la couleur d'action du thème, titre centré : la page se lit d'un coup d'œil. */
.home-card {
    flex: 0 1 14rem;
    min-height: 88px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.75rem 1rem;
    border: 1px solid var(--border);
    border-top: 4px solid var(--primary-bg);
    border-radius: var(--radius-tile);
    background-color: var(--panel-bg);
    color: var(--fg);
    text-align: center;
    text-decoration: none;
}

    /* FOCUS CLAVIER seul : la carte se remplit de la couleur d'action, comme un bouton principal. Le survol
       a été retiré le 05/10/2026 (vague 16, point 5) — sur l'iPad il restait collé après le toucher, et
       l'accueil gardait une carte rouge pendant la navigation suivante. */
    a.home-card:focus-visible {
        background-color: var(--primary-bg);
        border-color: var(--primary-bg);
        color: var(--primary-fg);
    }

.home-card-title {
    font-size: 1.35rem;
    font-weight: 700;
}

/* Cartes des réglages : sous le sujet, en plus petit, ce qui s'y règle. La couleur est celle de la carte
   (héritée), atténuée : elle reste lisible au survol, quand la carte passe en couleur d'action. */
.home-card-note {
    display: block;
    margin-top: 0.25rem;
    font-size: 0.95rem;
    font-weight: 400;
    line-height: 1.3;
    opacity: 0.85;
}

/* ----- Boutons : les variantes Bootstrap reprennent les variables ----- */
/* --radius-button (12 px) depuis la vague 16 : un bouton a un rayon plus serré qu'une carte, sinon une
   rangée de boutons de 48 px de haut finit par ressembler à une rangée de pastilles. */
.btn {
    border-radius: var(--radius-button);
}

.btn, .btn:hover, .btn:focus {
    color: var(--fg);
}

.btn-lg {
    border-radius: var(--radius-button);
}

/* Action principale : « Payer », « Valider », « Enregistrer », « Ouvrir la caisse ».
   LE DÉGRADÉ DE LA MAQUETTE, AVEC SON HAUT REMONTÉ (06/10/2026, vague 17, point 5). La maquette l'écrit
   « linear-gradient(180deg, #e8312f, #c91717) », et le 05/10/2026 il avait été REFUSÉ, mesure en main : du
   blanc sur #e8312f ne vaut que 4,28:1, sous le 4,5:1 exigé d'un texte courant. L'utilisateur l'a redemandé
   le 06/10/2026 en demandant explicitement qu'on remonte le haut : c'est fait, et voici les chiffres.
     haut  #e02c2a : blanc à 4,55:1 (contre 4,28:1 pour #e8312f — six points de rouge, deux de vert, cinq
                     de bleu en moins, soit un écart invisible à l'œil et le seuil franchi) ;
     bas   #c91717 : blanc à 5,80:1, la valeur de la maquette, inchangée — elle passait déjà.
   Le dégradé est un JETON DE THÈME, --primary-gradient, et il n'est DÉCLARÉ QUE PAR « glass » : les trois
   autres thèmes ne le déclarent pas, lisent le repli var(--primary-bg) et rendent leur rouge (ou leur teal)
   plein, au pixel près comme avant. C'est l'idiome de --glass-blur, point 7 de la vague 16 : le structurel
   ici, la palette au thème.
   POURQUOI --primary-bg N'EST PAS DEVENU LE DÉGRADÉ, ce qui aurait été plus court : treize règles de ce
   fichier le lisent en « background-color » ou en « border-color » (l'indicateur des verres bas, la tuile de
   stock sous son seuil, l'ardoise en attente, l'écart du journal, le filet des cartes d'accueil…), et un
   dégradé y serait une valeur invalide — donc aucun fond du tout. Le trait du bouton garde d'ailleurs
   --primary-bg, qui est plein : une bordure ne peut pas être un dégradé.
   « background » et non « background-color » : même raison qu'à .btn-secondary, juste en dessous. */
.btn-primary, .btn-primary:hover, .btn-primary:focus, .btn-primary:active,
.btn-success, .btn-success:hover, .btn-success:focus, .btn-success:active {
    background: var(--primary-gradient, var(--primary-bg));
    border-color: var(--primary-bg);
    color: var(--primary-fg);
}

/* Action secondaire de même poids : « ESPÈCES » (et « Dépôt en banque » en compta).
   PLUS « CARTE », et c'est la correction du 05/10/2026 : --secondary-bg porte le JAUNE sur les thèmes qui
   en ont un, la demande de l'utilisateur dit « espèces jaune, carte sombre », et cette classe peignait la
   carte — les deux moyens étaient inversés par rapport à ce qui avait été demandé. La carte prend
   .btn-dark, juste en dessous.
   « background » et non « background-color » (05/10/2026) : c'est la seule différence, et elle sert à ce
   qu'un thème puisse poser un DÉGRADÉ dans --secondary-bg (le thème verre y met celui des espèces,
   #ffd34a → #f1b707). Une couleur unie s'y écrit exactement comme avant ; le raccourci ne remet à zéro que
   des propriétés d'arrière-plan qu'aucune règle ne pose ici.
   LE TRAIT PASSE À TRANSPARENT, et cela ne change rien à l'œil : il valait --secondary-bg, c'est-à-dire
   exactement la couleur du fond qu'il entourait. Un dégradé, lui, ne peut pas être une bordure. La bordure
   garde son pixel (Bootstrap pose « 1px solid transparent » sur .btn) : le bouton a la même taille qu'avant. */
.btn-secondary, .btn-secondary:hover, .btn-secondary:focus, .btn-secondary:active {
    background: var(--secondary-bg);
    border-color: transparent;
    color: var(--secondary-fg);
}

/* Action de même poids encore, le SOMBRE D'ACTION : « Carte » (vague 16, point 1). Troisième poids et dernier
   — le projet n'a que trois moyens de payer, et une quatrième couleur d'action n'aurait rien à peindre.
   Variante de Bootstrap qui n'était employée nulle part, réécrite avec les variables comme ses voisines ;
   « background » et trait transparent pour la même raison que .btn-secondary : le thème verre y pose le
   second dégradé de la maquette (#2a2c33 → #17181c).
   Le texte NE PEUT PAS être celui du jaune des espèces : --tertiary-fg est clair là où --secondary-fg est de
   l'encre (1,00:1 sur le sombre, 1,82:1 à l'inverse — les deux chiffres sont dans themes.css). */
.btn-dark, .btn-dark:hover, .btn-dark:focus, .btn-dark:active {
    background: var(--tertiary-bg);
    border-color: transparent;
    color: var(--tertiary-fg);
}

/* Boutons discrets (retour, actualiser, fermer un message, bascules non choisies). */
.btn-outline-primary, .btn-outline-primary:hover, .btn-outline-primary:focus, .btn-outline-primary:active,
.btn-outline-secondary, .btn-outline-secondary:hover, .btn-outline-secondary:focus, .btn-outline-secondary:active,
.btn-outline-success, .btn-outline-success:hover, .btn-outline-success:focus, .btn-outline-success:active,
.btn-outline-light, .btn-outline-light:hover, .btn-outline-light:focus, .btn-outline-light:active,
.btn-outline-dark, .btn-outline-dark:hover, .btn-outline-dark:focus, .btn-outline-dark:active,
.btn-outline-warning, .btn-outline-warning:hover, .btn-outline-warning:focus, .btn-outline-warning:active {
    background-color: var(--btn-bg);
    border-color: var(--btn-border);
    color: var(--fg);
}

    /* Le trait ne se renforce plus au survol (vague 16, point 5). L'état touché est le même pour tous,
       6 % plus sombre et un pixel plus bas ; le focus clavier garde son anneau. */

/* Action destructrice : « Terminer le contenant », « Annuler la vente ». */
.btn-danger, .btn-danger:hover, .btn-danger:focus, .btn-danger:active {
    background-color: var(--danger);
    border-color: var(--danger);
    color: var(--bg);
}

.btn-outline-danger, .btn-outline-danger:hover, .btn-outline-danger:focus, .btn-outline-danger:active {
    background-color: var(--btn-bg);
    border-color: var(--danger);
    color: var(--danger);
}

.btn-link, .btn-link:hover, .btn-link:focus, .btn-link:disabled {
    color: var(--fg);
}

/* Boutons désactivés : Bootstrap leur redonne ses propres couleurs avec une spécificité plus forte. */
.btn-primary:disabled, .btn-primary.disabled, .btn-success:disabled, .btn-success.disabled {
    background: var(--primary-gradient, var(--primary-bg));
    border-color: var(--primary-bg);
    color: var(--primary-fg);
}

.btn-secondary:disabled, .btn-secondary.disabled {
    background: var(--secondary-bg);
    border-color: transparent;
    color: var(--secondary-fg);
}

.btn-dark:disabled, .btn-dark.disabled {
    background: var(--tertiary-bg);
    border-color: transparent;
    color: var(--tertiary-fg);
}

.btn-outline-primary:disabled, .btn-outline-primary.disabled,
.btn-outline-secondary:disabled, .btn-outline-secondary.disabled,
.btn-outline-success:disabled, .btn-outline-success.disabled,
.btn-outline-light:disabled, .btn-outline-light.disabled,
.btn-outline-dark:disabled, .btn-outline-dark.disabled,
.btn-outline-warning:disabled, .btn-outline-warning.disabled {
    background-color: var(--btn-bg);
    border-color: var(--btn-border);
    color: var(--fg);
}

.btn-danger:disabled, .btn-danger.disabled {
    background-color: var(--danger);
    border-color: var(--danger);
    color: var(--bg);
}

.btn-outline-danger:disabled, .btn-outline-danger.disabled {
    background-color: var(--btn-bg);
    border-color: var(--danger);
    color: var(--danger);
}

/* ----- Surfaces ----- */
.card {
    background-color: var(--panel-bg);
    border-color: var(--border);
    border-radius: var(--radius);
    color: var(--fg);
}

.list-group-item {
    background-color: var(--panel-bg);
    border-color: var(--border);
    color: var(--fg);
}

    .list-group-item:first-child {
        border-top-left-radius: var(--radius);
        border-top-right-radius: var(--radius);
    }

    .list-group-item:last-child {
        border-bottom-left-radius: var(--radius);
        border-bottom-right-radius: var(--radius);
    }

.list-group-item-secondary {
    background-color: var(--bg);
    color: var(--fg);
}

/* Messages de retour : fond de panneau, filet de couleur à gauche, texte toujours en --fg
   (le vert --ok du comité n'atteint pas 4,5:1 en texte sur les thèmes clairs). */
.alert {
    border-radius: var(--radius);
    background-color: var(--panel-bg);
    border: 1px solid var(--border);
    border-left: 6px solid var(--border);
    color: var(--fg);
}

.alert-success {
    border-color: var(--ok);
}

.alert-danger {
    border-color: var(--danger);
}

.alert-warning {
    border-color: var(--warn);
}

.alert-secondary {
    border-color: var(--border);
}

/* ----- Formulaires ----- */
.form-control, .form-select {
    background-color: var(--panel-bg);
    border-color: var(--btn-border);
    border-radius: var(--radius-button);
    color: var(--fg);
}

    .form-control:focus, .form-select:focus {
        background-color: var(--panel-bg);
        border-color: var(--focus);
        color: var(--fg);
        outline: 3px solid var(--focus);
        outline-offset: 0;
        box-shadow: none;
    }

    .form-control::placeholder {
        color: var(--muted);
    }

.form-floating > label, .form-text {
    color: var(--muted);
}

/* Champ refusé : bordure et anneau de focus en --danger. L'icône de fond de Bootstrap est retirée
   (elle est en dur, hors thème, et masque la fin d'une saisie sur tablette) : le trait porte le sens,
   le message d'erreur reste sous le champ. */
.form-control.is-invalid, .form-select.is-invalid, .form-control.invalid, .form-control.modified.invalid {
    border-color: var(--danger);
    background-image: none;
    padding-right: 0.75rem;
    box-shadow: none;
}

    .form-control.is-invalid:focus, .form-select.is-invalid:focus,
    .form-control.invalid:focus, .form-control.modified.invalid:focus {
        border-color: var(--danger);
        outline: 3px solid var(--danger);
        outline-offset: 0;
        box-shadow: none;
    }

.invalid-feedback, .validation-message {
    color: var(--danger);
}

.form-control:disabled, .form-control[readonly], .form-select:disabled {
    background-color: var(--bg);
    color: var(--muted);
}

.form-check-input {
    background-color: var(--panel-bg);
    border-color: var(--btn-border);
    color: var(--fg);
}

    .form-check-input:checked {
        background-color: var(--primary-bg);
        border-color: var(--primary-bg);
    }

    .form-check-input:focus {
        border-color: var(--focus);
        box-shadow: none;
    }

.darker-border-checkbox.form-check-input {
    border-color: var(--btn-border);
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid var(--ok);
}

.invalid {
    outline: 1px solid var(--danger);
}

.validation-message, .invalid-feedback {
    color: var(--danger);
}

/* ----- Afficher / masquer un mot de passe (S53) -----
   Le bouton se pose dans le champ, à droite, et le champ lui réserve sa place : il ne recouvre jamais le texte
   saisi. Il prend toute la hauteur du champ (jamais moins de 44 px) pour rester une cible sûre au comptoir.
   L'état vit dans aria-pressed, posé par app.js ; le choix de l'icône en découle, par CSS seul. */
.password-field {
    position: relative;
}

    .password-field .form-control {
        padding-right: 3.25rem;
    }

.password-toggle {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    /* Au-dessus du libellé flottant, qui couvre tout le champ (il ne reçoit aucun toucher : pointer-events: none). */
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    border: 0;
    border-radius: var(--radius);
    background-color: transparent;
    color: var(--muted);
    touch-action: manipulation;
}

    /* Pas de survol (vague 16, point 5) ; le focus clavier, lui, garde son anneau, juste en dessous. */

    .password-toggle:focus-visible {
        outline: 3px solid var(--focus);
        outline-offset: -3px;
        color: var(--fg);
    }

    .password-toggle svg {
        width: 24px;
        height: 24px;
    }

    /* Mot de passe masqué : œil ouvert. Visible : œil barré. */
    .password-toggle .password-eye-off {
        display: none;
    }

    .password-toggle[aria-pressed="true"] .password-eye {
        display: none;
    }

    .password-toggle[aria-pressed="true"] .password-eye-off {
        display: block;
    }

/* ----- Utilitaires Bootstrap réécrits (ils portent !important) ----- */
.text-muted {
    color: var(--muted) !important;
}

.text-danger {
    color: var(--danger) !important;
}

/* Entrée de stock du journal : --ok est une couleur de remplissage (le vert du comité ne tient pas
   4,5:1 en texte sur les thèmes clairs), le texte reste donc en --fg ; le signe porte le sens. */
.text-success {
    color: var(--fg) !important;
}

.text-dark {
    color: var(--bg) !important;
}

.bg-light {
    background-color: var(--bg) !important;
}

.bg-secondary {
    background-color: var(--muted) !important;
    color: var(--panel-bg) !important;
}

.bg-warning {
    background-color: var(--warn) !important;
}

.border, .border-bottom, .border-top {
    border-color: var(--border) !important;
}

.badge {
    border-radius: 999px;
}

/* ----- Bandeau d'erreur de Blazor (hors layout, commun aux deux layouts) ----- */
#blazor-error-ui {
    background-color: var(--panel-bg);
    color: var(--fg);
    border-top: 3px solid var(--danger);
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1100;
}

    #blazor-error-ui a {
        color: var(--fg);
    }

    #blazor-error-ui .dismiss {
        cursor: pointer;
        position: absolute;
        right: 0.75rem;
        top: 0.5rem;
    }

.blazor-error-boundary {
    background-color: var(--danger);
    color: var(--bg);
    padding: 1rem;
    border-radius: var(--radius);
}

    .blazor-error-boundary::after {
        content: "Une erreur s'est produite.";
    }

/* ----- Tablette : zones tactiles d'au moins 44 px (page Stock et formulaires) ----- */
.btn-touch {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.form-switch-touch {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    gap: 0.75rem;
    padding-left: 0;
    margin-bottom: 0;
}

    .form-switch-touch .form-check-input {
        width: 3.25rem;
        height: 1.75rem;
        margin: 0;
        float: none;
    }

.stock-figure {
    font-size: 1.25rem;
    font-weight: 600;
}

/* Volumes en cl sous les verres (page Stock, journal) : l'information reste lisible mais passe au second plan. */
.stock-volume {
    font-size: 0.875rem;
    color: var(--muted);
}

.stock-negative {
    color: var(--danger);
    font-weight: 700;
    padding: 0 0.4rem;
}

/* ----- Page Stock en tuiles (design 02-stock S2.2) : onglets de catégorie, grille de tuiles, actions de 44 px.
   Même vocabulaire que la caisse : bandeau .caisse-strip en haut, modales .caisse-overlay / .caisse-sheet. ----- */
/* Onglet de catégorie : un bouton qui filtre, là où la caisse pose une ancre (.caisse-strip-link). */
.stock-tab {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    padding: 0 1rem;
    border: 3px solid transparent;
    border-radius: var(--radius-pill);
    background-color: var(--cat-tint);
    color: var(--cat-ink);
    font-weight: 600;
}

    /* Onglet choisi : double anneau (texte du thème doublé du fond des panneaux), lisible sur les huit couleurs. */
    .stock-tab.selected {
        border-color: var(--fg);
        box-shadow: 0 0 0 2px var(--panel-bg) inset;
    }

/* Une colonne par défaut, deux à partir de 1024 px (écran de référence), trois au-delà de 1280 px.
   La page Ardoises pose ses tuiles dans la même grille (.tab-grid) : une seule règle pour les deux. */
.stock-grid, .tab-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 0.75rem;
}

@media (min-width: 1024px) {
    .stock-grid, .tab-grid {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (min-width: 1280px) {
    .stock-grid, .tab-grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

.stock-tile {
    min-height: 170px;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-tile);
    background-color: var(--panel-bg);
    color: var(--fg);
    box-shadow: var(--glass-sheen), var(--shadow-tile);
}

/* Produit SERVI DEPUIS le fût d'un autre (S15.4) : sa tuile se lit EN RETRAIT sous celle de son fût, et le filet
   épais du côté du retrait dit qu'elle en dépend. C'est ce décalage — avec la phrase « servi depuis le fût de… » à
   la place du chiffre — qui dit qu'elle n'a rien à elle : ni stock, ni réserve, ni verre, ni alerte.
   Retrait et filet en rem : ils suivent la taille du texte, et aucun pixel n'est écrit en dur. Le service range
   déjà les produits d'un même groupe côte à côte (OrderInBlocks), donc le dérivé tombe sous son fût sans que la
   page trie quoi que ce soit. */
.stock-tile-derived {
    margin-left: 1.5rem;
    border-left: 0.25rem solid var(--muted);
}

/* Bande supérieure à la couleur de la catégorie : elle la nomme, ce qui compte quand une recherche
   mêle les catégories. Teinte 10 % et encre 700 depuis la vague 16 (point 4, « plus de bande pleine ») :
   elle nomme toujours la catégorie, elle ne la crie plus. */
.stock-tile-band {
    padding: 0.15rem 0.75rem;
    background-color: var(--cat-tint);
    color: var(--cat-ink);
    font-size: 0.85rem;
    font-weight: 600;
}

.stock-tile-body {
    flex: 1 1 auto;
    padding: 0.5rem 0.75rem;
}

.stock-tile-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}

.stock-tile-name {
    font-size: 1.15rem;
    font-weight: 600;
    overflow-wrap: anywhere;
}

.stock-tile-price {
    font-size: 1.5rem;
    font-weight: 700;
    white-space: nowrap;
}

/* Le stock en gros : c'est la réponse qu'on vient chercher d'un coup d'œil. */
.stock-tile-figure {
    display: inline-block;
    margin-top: 0.25rem;
    padding: 0 0.35rem;
    border-radius: 0.35rem;
    font-size: 1.25rem;
    font-weight: 600;
}

/* Sous le seuil (moins d'un contenant, ou moins de dix pièces) : couleur de l'action principale du thème,
   comme l'indicateur des verres de la caisse (.tile-glasses-low). */
.stock-tile-low {
    background-color: var(--primary-bg);
    color: var(--primary-fg);
}

/* Quatre actions de 44 px à parts égales ; trois seulement pour un produit inactif (aucune livraison). */
.stock-tile-actions {
    display: flex;
    border-top: 1px solid var(--border);
}

.stock-tile-action {
    flex: 1 1 0;
    min-width: 0;
    min-height: 48px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    padding: 0.35rem 0.2rem;
    border: 0;
    border-left: 1px solid var(--border);
    background-color: var(--btn-bg);
    color: var(--fg);
    font-size: 0.8rem;
    font-weight: 600;
    line-height: 1.1;
}

    .stock-tile-action:first-child {
        border-left: 0;
    }

    /* Focus clavier et APPUI : la couleur d'action du thème remplit le bouton (l'icône suit par
       currentColor). Le survol a été retiré le 05/10/2026 (vague 16, point 5) : sur iPad il restait collé
       après le toucher, et une livraison enregistrée gardait son bouton rouge jusqu'au toucher suivant. */
    .stock-tile-action:focus-visible, .stock-tile-action:active,
    .stock-tile-wide:focus-visible, .stock-tile-wide:active {
        background-color: var(--primary-bg);
        color: var(--primary-fg);
    }

.stock-tile-icon {
    width: 22px;
    height: 22px;
}

/* Cinquième action, pleine largeur : le contenant d'un produit au verre. */
.stock-tile-wide {
    min-height: 48px;
    padding: 0.35rem 0.75rem;
    border: 0;
    border-top: 1px solid var(--border);
    background-color: var(--btn-bg);
    color: var(--fg);
    font-size: 0.95rem;
    font-weight: 600;
    letter-spacing: 0.01em;
}

.stock-tab:focus-visible, .stock-tile-action:focus-visible, .stock-tile-wide:focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: 2px;
}

/* ----- Palette fixe des catégories (design 03-caisse, 6.6) : donnée du club, identique dans tous les
   thèmes, seule exception à la règle « aucune couleur en dur hors themes.css ».

   CE QUI A CHANGÉ LE 05/10/2026 (vague 16, point 4) : la MISE EN ŒUVRE, pas les couleurs. Les huit teintes
   du club sont les mêmes à l'octet près (--cat-solid) ; elles ne sont plus posées en bande pleine avec du
   texte blanc dessus. Chaque classe porte désormais QUATRE jetons et aucune déclaration : c'est la surface
   qui reçoit la couleur qui décide ce qu'elle en fait.
     --cat-solid : la couleur du club, pleine. Le POINT d'un titre de groupe, et lui seul.
     --cat-tint  : la même, à 10 %. Le fond d'une tuile, d'une pastille, d'une bande de stock. Écrite en
                   rgba et non en hexadécimal cuit : à 10 % elle se compose avec ce qui est dessous, donc
                   elle marche aussi bien sur le verre que sur le blanc ou sur le bleu-noir d'« encre ».
     --cat-ink   : la couleur du TEXTE sur cette teinte, dans le registre 700/800. Trois des huit ne sont
                   pas leur propre couleur, parce qu'elles ne tiendraient pas 4,5:1 sur leur propre
                   teinte : vert #15803D → #166534 (3,88:1 → 5,52:1), teal #0F766E → #115E59
                   (4,22:1 → 5,85:1), orange #FB923C → #9A3412 (2,0:1 → 6,00:1 ; l'orange vif en texte
                   était hors de question). Brun descend à #78350F pour ne pas se confondre avec l'orange,
                   qui s'en est rapproché. Les cinq autres gardent leur valeur : bleu 5,08 · rouge 4,84 ·
                   violet 5,22 · gris 5,74. Pire cas mesuré sur les quatre fonds clairs : 4,84:1.
     --cat-edge  : l'arête d'une tuile — la couleur à 25 % composée SUR LE TRAIT, précalculée et mesurée
                   (3,63 à 4,14:1 contre le fond). Voir le long commentaire de .tile : composée sur du
                   blanc, elle tombait à 1,1:1 et l'arête disparaîtrait.
     --cat-fg    : le texte sur la couleur PLEINE. Il ne reste qu'un usage (le thème sombre, plus bas),
                   mais il reste juste, et c'est lui qui portait l'ancienne mise en œuvre. ----- */
.category-blue { --cat-solid: #1D4ED8; --cat-tint: rgba(29, 78, 216, 0.10); --cat-ink: #1D4ED8; --cat-fg: #FFFFFF; --cat-edge: #6776a1; }
.category-green { --cat-solid: #15803D; --cat-tint: rgba(21, 128, 61, 0.10); --cat-ink: #166534; --cat-fg: #FFFFFF; --cat-edge: #65837a; }
.category-red { --cat-solid: #B91C1C; --cat-tint: rgba(185, 28, 28, 0.10); --cat-ink: #B91C1C; --cat-fg: #FFFFFF; --cat-edge: #8e6a72; }
.category-orange { --cat-solid: #FB923C; --cat-tint: rgba(251, 146, 60, 0.10); --cat-ink: #9A3412; --cat-fg: #111827; --cat-edge: #867070; }
.category-purple { --cat-solid: #7E22CE; --cat-tint: rgba(126, 34, 206, 0.10); --cat-ink: #7E22CE; --cat-fg: #FFFFFF; --cat-edge: #806c9f; }
.category-teal { --cat-solid: #0F766E; --cat-tint: rgba(15, 118, 110, 0.10); --cat-ink: #115E59; --cat-fg: #FFFFFF; --cat-edge: #648087; }
.category-brown { --cat-solid: #92400E; --cat-tint: rgba(146, 64, 14, 0.10); --cat-ink: #78350F; --cat-fg: #FFFFFF; --cat-edge: #84736f; }
.category-gray { --cat-solid: #4B5563; --cat-tint: rgba(75, 85, 99, 0.10); --cat-ink: #4B5563; --cat-fg: #FFFFFF; --cat-edge: #737884; }

/* THÈME SOMBRE : la teinte à 10 % tient très bien sur le bleu-noir (le texte du thème y garde 12 à 14:1),
   mais une encre 700 n'y est pas lisible — de 2,09:1 (gris) à 3,08:1 (vert) sur sa propre teinte, mesuré.
   Les cinq surfaces teintées repassent donc au texte du thème, et c'est le POINT coloré du titre de groupe
   qui porte la catégorie. Une seule règle, et le seul endroit du fichier où un thème est nommé : « encre »
   est le seul thème sombre, et une négation (:not) se serait trompée au prochain thème clair. */
[data-theme="encre"] .tile,
[data-theme="encre"] .caisse-strip-link,
[data-theme="encre"] .stock-tab,
[data-theme="encre"] .category-chip,
[data-theme="encre"] .category-tile-preview,
[data-theme="encre"] .stock-tile-band {
    color: var(--fg);
    /* --cat-edge est précalculé pour un fond CLAIR (teinte composée sur le trait du thème verre) : sur le
       bleu-noir il éclaircirait l'arête au lieu de la teinter. Le thème sombre garde donc son trait, et
       c'est la teinte à 10 % plus le point coloré qui portent la catégorie — elle y tient très bien. */
    --cat-edge: var(--border);
}

/* Une pastille de catégorie est souvent un lien (bandeau de la caisse) : Bootstrap lui impose son bleu
   de survol, plus spécifique que la classe de couleur. Elle garde son propre texte — l'encre de la
   catégorie depuis la vague 16. */
.category-blue:hover, .category-blue:focus, .category-green:hover, .category-green:focus,
.category-red:hover, .category-red:focus, .category-orange:hover, .category-orange:focus,
.category-purple:hover, .category-purple:focus, .category-teal:hover, .category-teal:focus,
.category-brown:hover, .category-brown:focus, .category-gray:hover, .category-gray:focus {
    color: inherit;
}

/* Pastille de couleur devant un titre de groupe (page Stock, produits offerts, titres de la caisse).
   LE SEUL ENDROIT QUI GARDE LA COULEUR PLEINE, et c'est sa raison d'être depuis la vague 16 : quand les
   fonds ne sont plus que des teintes à 10 %, c'est ce disque qui dit la catégorie d'un coup d'œil. */
.category-dot {
    display: inline-block;
    width: 1rem;
    height: 1rem;
    border-radius: 50%;
    vertical-align: -0.1rem;
    margin-right: 0.5rem;
    background-color: var(--cat-solid);
}

/* Pastille de choix de la catégorie (fiche produit) : zone tactile de 44 px, sélection bien visible.
   Teinte 10 % et encre 700 depuis la vague 16, comme toutes les surfaces de catégorie. */
.category-chip {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    padding: 0.375rem 1rem;
    border: 3px solid transparent;
    border-radius: var(--radius-pill);
    background-color: var(--cat-tint);
    color: var(--cat-ink);
    font-size: 1.125rem;
    font-weight: 600;
}

    /* Sélection : double anneau (texte du thème doublé du fond des panneaux), lisible sur les huit couleurs. */
    .category-chip.selected {
        border-color: var(--fg);
        box-shadow: 0 0 0 2px var(--panel-bg) inset;
    }

/* Aperçu de tuile (page Catégories) : taille minimale d'une tuile de caisse, 96 × 72 px.
   IL NE SUIT PLUS .tile DEPUIS LE 06/10/2026, et il ne peut pas : la tuile de caisse n'a plus de fond de
   couleur de catégorie du tout (vague 17, point 2), alors que cet aperçu-ci est là pour montrer la COULEUR
   qu'on est en train de choisir — sans teinte, il n'y aurait rien à voir. Il montre donc la teinte de la
   catégorie, ce qu'elle est : la couleur qui apparaîtra sur le point du titre de groupe et sur la pastille
   d'ancre du bandeau, pas un fond de tuile. Il garde le rayon et le texte du thème. */
.category-tile-preview {
    min-width: 96px;
    min-height: 72px;
    max-width: 12rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.5rem 0.75rem;
    border-radius: var(--radius-tile);
    background-color: var(--cat-tint);
    color: var(--fg);
    font-size: 1.125rem;
    font-weight: 600;
    text-align: center;
    overflow-wrap: anywhere;
}

    /* Aucune couleur choisie : cadre en pointillés, sans fond. Le fond transparent est ÉCRIT depuis la
       vague 16 : la règle du dessus lit var(--cat-tint), et cet aperçu-là ne porte aucune classe de
       catégorie — on ne laisse pas une variable absente décider à notre place. */
    .category-tile-preview.empty {
        border: 2px dashed var(--btn-border);
        background-color: transparent;
        color: var(--muted);
        font-weight: 400;
    }

/* ----- Écran caisse (design 03-caisse 6.3, ui-mockup 4.3) : rail, tuiles, panneau d'ardoise adaptable ----- */
/* Coque de la caisse : elle remplit exactement l'écran et ne défile jamais. Badge, en-tête et barre des
   catégories gardent leur hauteur ; seules la zone des tuiles et le panneau d'ardoise défilent dedans.
   100dvh après 100vh : sur iPad, 100vh compte la barre d'adresse, la coque dépassait donc du bas et c'est la
   page entière qui défilait. Le repli sert aux navigateurs qui ignorent encore l'unité dynamique.
   touch-action ici couvre aussi les espaces entre les tuiles : un doigt qui manque la tuile de quelques
   pixels ne zoome pas davantage. Le pincement, lui, reste possible. */
.caisse-shell {
    display: flex;
    flex-direction: column;
    /* La coque remplit ce qui reste sous le bandeau de staging (0 en prod : « 100dvh - 0px », pixel pour pixel la
       hauteur d'avant). Sans cette soustraction, la coque dépasserait du bas de 24 px sur le staging et la page
       entière défilerait — le défaut que 100dvh était venu corriger. */
    height: calc(100vh - var(--staging-banner-h));
    height: calc(100dvh - var(--staging-banner-h));
    margin-left: var(--rail-width);
    margin-left: calc(var(--rail-width) + env(safe-area-inset-left));
    padding-right: env(safe-area-inset-right);
    padding-bottom: env(safe-area-inset-bottom);
    overflow: hidden;
    overscroll-behavior: none;
    touch-action: manipulation;
    background-color: var(--bg);
    /* La coque recouvre le fond du document : c'est donc elle qui porte les halos du thème verre, sans quoi
       les surfaces de verre n'auraient rien à laisser passer. « none » pour les trois autres thèmes. */
    background-image: var(--bg-image, none);
    background-repeat: no-repeat;
}

/* Première ligne (CaisseHeader) : l'OPÉRATEUR à gauche, les actions de la journée à droite. Sa hauteur est celle
   des boutons (36 px depuis le 23/09/2026) ; le rembourrage vertical est réduit au minimum, chaque pixel étant
   pris aux tuiles. Total : 4 + 36 + 4 + 1 de filet = 45 px, contre 53 avant.
   UNE SEULE LIGNE DEPUIS LE 06/10/2026, et c'est l'objet de la retouche : l'écran caisse en portait DEUX — la
   barre du badge d'opérateur (42 px) posée par CaisseLayout au-dessus du corps, puis cette ligne-ci avec l'état
   de la journée sur deux rangs. L'état de la journée est parti en tête de la modale de clôture (.closing-opened,
   mot pour mot) et le badge est descendu ici : 42 px rendus aux tuiles, et la ligne garde ses 45 px puisque
   c'est son bouton le plus haut qui la tient, pas son texte. Le compte refait est sur .caisse-tiles.
   CE QUI CÈDE QUAND LA PLACE MANQUE, dans cet ordre et jamais autrement : l'espace entre les deux groupes
   (margin-left: auto sur les actions), puis le NOM de l'opérateur, qui se tronque (c'est une information). Les
   libellés des actions ne cèdent jamais — règle du 06/10/2026, voir .caisse-action-label. */
.caisse-header {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.25rem 0.75rem;
    background-color: var(--panel-bg);
    border-bottom: 1px solid var(--border);
    color: var(--fg);
}

/* TITRE 17/800 (06/10/2026, vague 17, point 4). IL N'EN RESTE QU'UN SEUL LECTEUR DEPUIS LE 06/10/2026 AU
   SOIR : la ligne « Encaissement seul » de l'écran caisse FERMÉE avec une ardoise reprise (CaissePage). La
   journée ouverte, elle, ne porte plus de titre du tout — son état est parti en tête de la modale de clôture
   (.closing-opened), où la sous-ligne grise l'a suivi.
   Ce que ça coûte à la hauteur de la ligne : RIEN, et c'est mesuré. .caisse-header est tenue par ses
   boutons de 36 px (--action-height) et son rembourrage de 2 × 4 px, soit 45 px avec le filet ; une ligne
   de 17 × 1,2 = 20,4 px passe largement dessous. C'est la seule contrainte de ce bloc, et elle vaut pour
   qui voudrait grossir ce titre : au-delà de 36 px, la barre passe à 47 et deux pixels partent des tuiles.
   La colonne flex reste écrite bien qu'il n'y ait plus qu'une ligne : c'est elle qui porte le span, et le
   span est ce qui porte la troncature.
   min-width: 0 : sans lui, un élément de flex ne descend pas sous la largeur de son texte et rien ne se
   tronque. */
.caisse-header-title {
    flex: 0 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    font-size: 1.0625rem;
    font-weight: 800;
    letter-spacing: -0.01em;
    line-height: 1.2;
    margin: 0;
}

    .caisse-header-title > span {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }

/* Actions de la journée : collées à droite, largeur intouchable (flex: 0 0 auto) — un opérateur au nom à
   rallonge ne pousse jamais « Clôturer la caisse » hors de l'écran. */
.caisse-header-actions {
    flex: 0 0 auto;
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

/* Deuxième ligne (CaisseBar) : les catégories seules, sur toute la largeur, hors de la zone qui défile — donc
   toujours visibles sans position: sticky. Deux lignes plutôt qu'une : mêlées aux actions de la journée, les
   catégories ne se distinguaient plus de ce qui clôture ou actualise. */
.caisse-bar {
    display: flex;
    align-items: center;
    padding: 0.25rem 0.75rem;
    background-color: var(--panel-bg);
    border-bottom: 1px solid var(--border);
}

    /* Le bandeau de catégories partagé avec la page Stock reprend ici ses habitudes de barre d'outils. */
    .caisse-bar .caisse-strip {
        position: static;
        flex: 1 1 auto;
        min-width: 0;
        flex-wrap: nowrap;
        overflow-x: auto;
        overscroll-behavior-x: contain;
        scrollbar-width: thin;
        padding: 0;
        margin: 0;
        background-color: transparent;
        border-bottom: 0;
    }

    .caisse-bar .caisse-strip-link {
        flex: 0 0 auto;
    }

/* Curseur de taille des tuiles (23/09/2026, 03-caisse 6.3) : au bout de la ligne des catégories, jamais dans celle
   des actions — c'est la seule ligne où rien ne s'écrit, et il n'y coûte pas un pixel de hauteur, la ligne étant
   déjà tenue par les pastilles de 36 px. flex: 0 0 auto : ce sont les catégories qui défilent, jamais lui.
   Le mot « Taille » reste écrit : un curseur nu ne dit pas ce qu'il règle. */
.tile-size {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin-left: 0.75rem;
    cursor: pointer;
}

.tile-size-label {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--muted);
}

/* Le rail du curseur fait la hauteur d'une action (36 px) : la poignée peut y être grande sans faire grandir la
   ligne. Largeur 8 rem — assez pour que les quatorze crans se distinguent au doigt, assez peu pour laisser les
   catégories défiler sur le reste. */
.tile-size-range {
    -webkit-appearance: none;
    appearance: none;
    width: 8rem;
    height: var(--action-height);
    margin: 0;
    background: transparent;
    cursor: pointer;
}

/* Poignée : 44 px de large (--touch-target, le plancher tactile) sur toute la hauteur de la ligne. Elle est plus
   large que haute parce qu'on la tire horizontalement, et parce que la hauteur appartient aux tuiles.
   WebKit et Firefox nomment ces parties différemment, et un sélecteur inconnu annulerait toute la règle : les
   deux moteurs ont donc chacun la leur, écrites à l'identique. */
.tile-size-range::-webkit-slider-runnable-track {
    height: 4px;
    border-radius: 999px;
    background-color: var(--border);
}

.tile-size-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: var(--touch-target);
    height: var(--action-height);
    /* Poignée centrée sur le rail : la moitié de ce qui dépasse, de part et d'autre. */
    margin-top: calc((4px - var(--action-height)) / 2);
    border: 1px solid var(--btn-border);
    border-radius: 999px;
    background-color: var(--btn-bg);
}

.tile-size-range::-moz-range-track {
    height: 4px;
    border-radius: 999px;
    background-color: var(--border);
}

.tile-size-range::-moz-range-thumb {
    width: var(--touch-target);
    height: var(--action-height);
    border: 1px solid var(--btn-border);
    border-radius: 999px;
    background-color: var(--btn-bg);
}

/* Bouton des actions de la journée, 36 px de haut (23/09/2026, arbitrage densité iPad A16) — la plupart n'ont
   plus que leur icône, « Comptoir » et « Pause » gardent un mot à côté.
   Cette hauteur passe sous les 44 px qui étaient la règle jusqu'ici ; le plancher de 44 reste entier partout où
   un toucher écrit (tuiles, « Nouveau fût », « − », paiements) — voir ui-mockup § 1.7. Elle ne bouge pas avec
   le retrait des libellés : c'est elle qui tient la ligne, et la zone tactile ne se rabote pas.
   Rembourrage et gabarit resserrés dans le même mouvement : 0,6 rem de part et d'autre, 0,35 rem entre l'icône
   et le mot quand il y en a un, libellé en 0,9 rem. min-width : un carré au minimum — c'est ce qui donne leur
   gabarit aux boutons sans mot, et à « Terminé ». Le rayon reste celui d'une pastille : pas des cartes. */
.caisse-action {
    min-height: var(--action-height);
    min-width: var(--action-height);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    padding: 0 0.6rem;
    border: 1px solid var(--btn-border);
    border-radius: 999px;
    background-color: var(--btn-bg);
    color: var(--fg);
    font-family: inherit;
    font-size: 0.9rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    /* VERRE SOULEVÉ (06/10/2026, vague 17, point 4) : la maquette dessine ses boutons de barre en blanc
       translucide AVEC un reflet sur l'arête haute et une ombre diffuse en dessous — c'est ce relief, et
       pas la couleur du fond, qui les fait lire comme des plaques posées sur le halo. Les trois autres
       thèmes reçoivent le même relief avec leur palette : une ombre est une dimension, pas une couleur
       (:root), et sur « encre » un noir à 8 % sur un bleu-noir ne se voit pas — ce qui est voulu.
       La hauteur, elle, reste à --action-height : 36 px et pas les 40 de la maquette, voir --action-height. */
    box-shadow: var(--glass-sheen), 0 6px 20px rgba(23, 24, 28, 0.08);
}

    /* Pas de survol sur les actions de la journée (vague 16, point 5) : leur trait ne se renforçait que
       sous une souris, et restait renforcé après un toucher sur iPad. La règle qui l'annulait pour
       .caisse-action-warn est partie avec lui : elle ne servait qu'à ça. */

    /* Validation en cours : le bouton garde ses couleurs, seul le curseur change. */
    .caisse-action:disabled {
        opacity: 1;
        cursor: default;
    }

    /* Ventes comptoir en cours : le seul état qui appelle une décision porte la couleur d'avertissement,
       sur le trait seulement — le texte reste en --fg, et le survol ne la lui prend pas. */
    .caisse-action-warn {
        border-color: var(--warn);
        border-width: 2px;
    }

    /* « Clôturer la caisse » : UNE PASTILLE CLAIRE À TRAIT RENFORCÉ, comme ses voisines et comme avant la
       vague 17. Elle avait passé une journée en bouton sombre dégradé (la maquette le dessine ainsi), et
       l'utilisateur l'a ramenée au clair le 06/10/2026 pour une raison qui tient en un chiffre : il voulait
       son cadenas ROUGE, et un rouge d'alerte sur ce fond sombre ne vaut que 2,75:1 au plus clair du
       dégradé — illisible. Sur la pastille claire, le même rouge donne 6,78:1 sur « glass », 6,47 sur
       « spontin », 7,56 sur « sauge » et 6,26 sur « encre ». Le fond ne porte donc plus le « cette action
       ne se reprend pas » : c'est le cadenas rouge qui le dit, et le trait renforcé qui le détache de ses
       voisines. Le jeton --tertiary-* reste employé ailleurs (« Carte », « Dépôt en banque ») : il n'est
       pas mort, il n'est plus ici.
       Le cadenas y est seul depuis le retrait des libellés (23/09/2026) ; c'est sa confirmation qui dit ce
       qu'on ferme, en toutes lettres. « Terminé » (mode Réorganiser) garde la même classe : il n'a pas
       d'icône, et il est seul sur sa ligne. Le cadenas suit à .caisse-action-icon-closing. */
    .caisse-action-closing {
        border-color: var(--fg);
    }

/* Icônes des actions de journée (19/09/2026, ui-mockup 4.3) : la teinte double le dessin et sépare les sept
   boutons d'un coup d'œil. Elle ne prend que des variables de thème — jamais la palette des catégories, qui
   appartient aux produits — et ne porte jamais le sens à elle seule : chaque bouton garde son aria-label et son
   title écrits en toutes lettres (les libellés visibles, eux, sont repartis le 23/09/2026, « Comptoir » et
   « Pause » exceptés, « Ardoises » les rejoignant le 06/10/2026 — voir .caisse-action-label), et les dessins
   restent différents (feuille,
   panier, cadeau, boucle, flèches, cadenas, silhouette), donc lisibles par quelqu'un qui ne distingue pas le
   rouge du vert. Les trois teintes vives s'étagent aussi en clarté (--ok 3,3:1, --warn ~5:1, --danger ~6,5:1
   sur --btn-bg), elles se distinguent donc même en niveaux de gris.
   20 px au lieu de 22 : le bouton descend à 36 px de haut et le dessin doit garder son air autour de lui. */
.caisse-action-icon {
    width: var(--action-icon-size);
    height: var(--action-icon-size);
    flex: none;
    /* Par défaut la couleur du texte : « Ardoises ouvertes », l'action la plus courante de la ligne. */
    color: var(--fg);
}

    /* Ventes comptoir en cours : la même couleur d'avertissement que le trait du bouton. */
    .caisse-action-icon-warn {
        color: var(--warn);
    }

    /* « Offrir » : une faveur accordée, pas une alerte — la couleur de l'état sain. --ok ne porte jamais de
       texte (ui-mockup 2) ; un trait d'icône est un élément graphique, seuil 3:1, mesuré à 3,30:1. */
    .caisse-action-icon-gift {
        color: var(--ok);
    }

    /* « Actualiser », « Ranger les tuiles » et « Pause » : des gestes neutres et réversibles, sans
       conséquence sur la journée ni sur la vente en cours (--muted, de 5,17:1 à 5,55:1 selon le thème). */
    .caisse-action-icon-neutral {
        color: var(--muted);
    }

    /* « Clôturer la caisse » : la seule action qui termine et ne se reprend pas, donc le ROUGE D'ALERTE du
       thème — et c'est lui, pas un rouge figé. L'utilisateur a demandé « le rouge d'action (#d81f1f) comme
       avant » le 06/10/2026 ; --danger est ce qui était là avant, il est mesuré sur les quatre thèmes
       (6,78:1 sur « glass », 6,47 « spontin », 7,56 « sauge », 6,26 « encre »), et #d81f1f figé tomberait
       à 2,2:1 sur le thème de nuit. Son bouton a repris sa pastille claire le même jour, sans quoi aucun
       rouge ne tenait dessus (2,75:1). */
    .caisse-action-icon-closing {
        color: var(--danger);
    }

    /* Indisponible (validation en cours) : l'icône perd sa teinte et redescend en --muted — un bouton
       qu'on ne peut pas toucher ne doit pas se présenter comme un bouton actif. Le bouton, lui, garde
       son fond et son trait : c'est un état de quelques centaines de millisecondes, pas un clignotement. */
    .caisse-action:disabled .caisse-action-icon {
        color: var(--muted);
    }

/* Libellé des actions de la journée. LA RÈGLE, DEPUIS LE 06/10/2026 (vague 17 bis, point 4a) : « Pause »,
   « Ardoises » et « Comptoir » affichent leur libellé EN ENTIER — jamais coupé, jamais d'ellipse, la pastille
   s'élargit —, et TOUTES les autres n'affichent que leur picto (« Offrir », « Actualiser », « Ranger »,
   « Clôturer »), leur nom entier restant dans l'aria-label et le title, une icône sans nom étant un bouton muet.
   « Ardoises » est le libellé AJOUTÉ par cette règle : les deux autres étaient déjà écrits depuis le
   23/09/2026, et son bouton était le seul des trois à n'avoir qu'un picto et sa pastille de comptage.
   CE QUI COUPAIT EST PARTI : cette règle portait « overflow: hidden; white-space: nowrap », c'est-à-dire le
   mécanisme même de la troncature que l'utilisateur voyait. Le « nowrap » reste, mais sur .caisse-action — là
   il empêche un libellé de se replier en deux lignes dans une barre de 36 px de haut, ce qui n'est pas la même
   chose que de le rogner. « flex: none » dit le reste : un libellé n'est jamais comprimé par son bouton.
   MESURÉ EN MANROPE 14,4 px / 600, les trois libellés et les sept boutons, à partir du fichier de police du
   dépôt : « Pause » 42,4 px, « Ardoises » 60,4, « Comptoir » 65,0. Un bouton coûte 2 (traits) + 19,2
   (rembourrage) + 20 (icône) + 5,6 (écart) par élément de plus, et une pastille de comptage 24 px.
   Donc, dans le cas le plus chargé (sept boutons, un compte qui range les tuiles) :
   Pause 89,2 + Ardoises 136,8 + Comptoir 141,4 + quatre pictos seuls 4 × 41,2 + six écarts de 6,4 = 570,6 px,
   contre 504,6 px avant l'ajout (+66,0 px, le mot « Ardoises » et son écart).
   LA PASTILLE « COMPTOIR » EST PERMANENTE DEPUIS LE 06/10/2026 (elle ouvre sa propre modale, et un bouton qui
   va et vient n'est pas un endroit où aller voir) : « une vente comptoir en cours » DISPARAÎT donc des
   conditions ci-dessus, et ce cas le plus chargé devient le cas NORMAL d'un compte qui tient le stock.
   LE MAXIMUM NE BOUGE PAS D'UN PIXEL pour autant — il comptait déjà ce bouton et son libellé —, et la pastille
   de comptage garde ses 24 px à « 0 » comme à « 9 » (min-width 1,5 rem, un chiffre de Manrope 11,9/600 en
   mesurant 9,4). Ce qui change est le PLANCHER : la ligne mesure 570,6 px en permanence au lieu de 429,2
   (570,6 − 141,4) quand aucune vente n'était en cours, et 523,0 px pour un compte sans « Tenir le stock »
   (six boutons, cinq écarts). Les sept boutons gardent donc la même place d'une vente à l'autre.
   CE QU'IL RESTE À GAUCHE DES ACTIONS, c'est-à-dire à l'OPÉRATEUR depuis le 06/10/2026 (l'état de la journée
   qui occupait cette place est parti en tête de la modale de clôture) :
     1180 × 820 : 1 124 − 24 (rembourrage) − 12 (écart) − 570,6 = 517,4 px ;
     1024 × 768 :   968 − 24 − 12 − 570,6 = 361,4 px.
   La pastille de l'opérateur demande 60 px + son nom (voir .operator-badge-inline), soit 220,4 px avec
   « Sarah Vandenberghe » (160,4 px en Manrope 16/600) : il reste 297 px d'espace central à 1180 et 141 px à
   1024, et le nom ne se tronque qu'au-delà de 301,4 px, soit une trentaine de caractères.
   POUR MÉMOIRE, CE QUE DEMANDAIT LE TITRE PARTI : 234,9 px (« Caisse ouverte depuis 18:42 », 17 px/800),
   325 px dans sa forme longue (« … depuis le 14/09 à 18:42 ») et au plus 147,4 px pour sa sous-ligne grise
   (« par Sarah Vandenberghe », 12,5 px/600). Il tenait, mais sur une DEUXIÈME ligne de 42 px (la barre du
   badge d'opérateur) : c'est cette ligne-là que la retouche a supprimée, pas de la largeur.
   LA BARRE NE PEUT PAS PASSER À DEUX LIGNES, et ce n'est pas une mesure mais une structure : .caisse-header
   n'a aucun flex-wrap et .caisse-header-actions est en « flex: 0 0 auto ». 570,6 px d'actions dans 932 px de
   ligne utile à 1024 : la barre garde ses 45 px, et le budget vertical de .caisse-tiles en gagne 42. */
.caisse-action-label {
    flex: none;
}

.caisse-action .caisse-badge {
    min-width: 1.5rem;
    font-size: 0.85rem;
}

.caisse-body {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
}

/* Seule zone qui défile de l'écran caisse. overscroll-behavior : le défilement s'arrête à ses bornes, sans
   rebond élastique ni entraînement de la page — c'est lui qui donnait l'impression que l'écran bougeait.
   ----- BUDGET VERTICAL DE L'ÉCRAN CAISSE, REFAIT LE 06/10/2026 (en-tête ramené à UNE ligne) -----
   CE QUI A CHANGÉ DANS LE COMPTE, ET IL N'Y A QU'UNE LIGNE : les 42 px de la BARRE DU BADGE D'OPÉRATEUR
   (4 de marge + 36 de bouton + 2 de filets) NE SONT PLUS DÉDUITS quand une journée est ouverte. Cette barre
   était posée par CaisseLayout au-dessus de tout le corps ; la pastille est descendue DANS la ligne de
   l'en-tête (.operator-badge-inline), qui garde ses 45 px — c'est son bouton le plus haut qui la tient, pas
   son texte. L'état de la journée qui occupait cette ligne est parti en tête de la modale de clôture
   (.closing-opened) : il ne coûte plus un pixel ici. RIEN D'AUTRE N'A BOUGÉ : deux barres de 45 px (l'en-tête
   et les catégories), rembourrage bas 8 px, titre de groupe 20,8 px (capitales 12/800 sans trait, voir
   .tile-group-title), tuile ordinaire 96 px au réglage le plus gros, tuile au verre 142 px (96 + 44 de
   « Nouveau fût » + 2 de filet), 105 px à la borne basse du curseur.
   COMMENT SE COMPTE UNE RANGÉE : sa hauteur, plus un écart de 8 px avant chaque rangée SAUF la première
   (c'est un « gap » de grille, il n'y en a pas après la dernière). n rangées ordinaires demandent donc
   104 n − 8 px, n rangées au verre 150 n − 8. Le « 59,2 px de marge » écrit ici le 05/10/2026 pour 1180
   comptait un écart final qui n'existe pas : la marge valait 67,2.
   HAUTEUR DISPONIBLE POUR LES RANGÉES = écran − 24 (bandeau de staging, 0 EN PROD) − 45 − 45 − 8 − 20,8 (un
   seul titre de groupe affiché) :
     1180 × 820 : 701,2 px (659,2 avant, soit les 42 px rendus) ;
     1024 × 768 : 649,2 px (607,2 avant).
   CE QUE ÇA CHANGE EN RANGÉES, au réglage le plus gros du curseur (5 colonnes à 1180, 4 à 1024) :
     1180 × 820 : 6 rangées ordinaires (616 px, marge 85,2 contre 43,2) ⇒ 5 × 6 = 30 produits, et 4 rangées
                  au verre (592 px, marge 109,2 contre 67,2) ⇒ 5 × 4 = 20. AUCUNE RANGÉE DE PLUS : la
                  septième ordinaire demande 720 px, il manque 18,8 ; la cinquième au verre 742, il manque
                  40,8. Les 42 px rendus passent donc tous en marge sur cet écran.
     1024 × 768 : 6 rangées ordinaires AU LIEU DE 5 (616 px pour 649,2 disponibles, marge 33,2) ⇒ 4 × 6 = 24
                  produits visibles d'un coup au lieu de 20 : c'est LA rangée gagnée, et elle est gagnée sur
                  l'écran plancher. Au verre, 4 rangées comme avant (592 px), mais la marge passe de 15,2 à
                  57,2 px — c'était le chiffre le plus serré de tout le budget.
   AU RÉGLAGE LE PLUS SERRÉ DU CURSEUR (tuile ordinaire 44 px, tuile au verre 105 px) :
     1180 × 820 : ordinaires 7 × 13 = 91 produits (7 × 12 = 84 avant) ; au verre 7 × 6 = 42 (7 × 5 = 35).
     1024 × 768 : ordinaires 6 × 12 = 72 produits (6 × 11 = 66) ; au verre 6 × 5 = 30, inchangé.
   SUR L'INSTANCE DE TEST (bandeau de 24 px, --staging-banner-h). EN PROD la variable vaut 0 et les chiffres
   ci-dessus sont ceux-là ; sur le staging il reste 677,2 px à 1180 et 625,2 px à 1024.
     1180 × 820 : 6 rangées ordinaires (marge 61,2) et 4 au verre (marge 85,2), comme en prod.
     1024 × 768 : 6 rangées ordinaires (616 px, marge 9,2) et — C'EST LE SEUL ÉCART ASSUMÉ ENTRE LES DEUX
                  INSTANCES, ET IL DISPARAÎT — les 4 rangées de tuiles AU VERRE au réglage le plus gros, qui
                  demandaient 592 px pour 583,2 disponibles et faisaient défiler la zone des tuiles, tiennent
                  maintenant avec 33,2 px de marge. Plus rien ne défile sur le staging là où rien ne défile
                  en prod, et le curseur n'a plus de rattrapage à faire.
   EN LARGEUR, rien n'a changé le 06/10/2026 au soir : la zone des tuiles fait 771,3 px à 1180 et 642 px à
   1024 (le détail est sur .caisse-panel), colonnes de 144 px à la borne haute du curseur et de 96 à la borne
   basse. La police ne touche que des largeurs, et les deux budgets qu'elle décide sont recomptés sur
   .caisse-line-name et sur .caisse-action-label.
   À QUI VOUDRAIT REPRENDRE DES PIXELS ICI : les deux barres sont tenues par --action-height (36) et les
   tuiles par --tile-min-height ; le plus serré de tous les chiffres ci-dessus est désormais la marge de
   9,2 px des six rangées ordinaires à 1024 × 768 SUR LE STAGING. En prod, le plus serré est 33,2 px. */
.caisse-tiles {
    flex: 1 1 auto;
    min-width: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0 0.75rem 0.5rem;
}

/* Largeur adaptable mais bornée (19/09/2026) : 28 % de la zone de travail, jamais moins de 18 rem (288 px),
   jamais plus de 26 rem (416 px). Le pourcentage nu étalait le panneau sur les grands écrans et l'écrasait sur
   les petits ; les bornes tiennent les deux bouts — 288 px laissent « Reste à payer » et son montant sur la
   même ligne (1,625 rem depuis le 06/10/2026 : 221 px pour 254 de ligne utile, voir .caisse-balance) et
   « Payer 9,70 € » sur toute la rangée, 416 px suffisent au plus long nom d'ardoise. À 1024 px (iPad de référence,
   rail déduit) le panneau se pose sur sa borne basse, 288 px : les 48 px repris aux 336 px d'avant passent aux
   tuiles, qui gagnent une colonne (quatre par rangée au lieu de trois). flex-shrink 0 : les tuiles cèdent,
   jamais le panneau.
   Hauteur fixe : le panneau ne défile pas en entier — c'est la liste des articles qui défile dedans, pour que
   « Espèces » et « Carte » restent à l'écran même avec vingt lignes d'ardoise (leur place était sous la ligne
   de flottaison dès qu'une ardoise s'allongeait). min-height: 0 : sans lui, un élément de flex ne descend
   jamais sous la hauteur de son contenu et la colonne dépasserait de la coque. */
.caisse-panel {
    flex: 0 0 auto;
    width: clamp(18rem, 28%, 26rem);
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow: hidden;
    background-color: var(--panel-bg);
    /* UNE CARTE DÉTACHÉE DES QUATRE BORDS (06/10/2026, vague 17, point 5), là où le panneau était collé au
       bord droit avec deux coins arrondis seulement. Le 05/10/2026 la marge avait été refusée sur un calcul :
       « la marge se prendrait sur la zone des tuiles, et une colonne de tuiles tomberait à 1024 px ».
       RECOMPTÉ LE 06/10/2026, ET LE CALCUL ÉTAIT FAUX — il n'avait jamais été fait, seulement craint. La
       marge de GAUCHE est nulle, exactement comme dans la maquette (« margin: 14px 14px 14px 0 ») : de ce
       côté-là, c'est le rembourrage de la zone des tuiles qui tient déjà l'écart, et le doubler serait de
       l'air payé deux fois. Le panneau ne coûte donc que 14 px de largeur, pas 28 :
         1024 px : zone de travail 968, panneau à sa borne basse 288 + 14 de marge = 302, tuiles 666 moins
                   24 de rembourrage = 642 px. Colonnes de 144 px au réglage le plus gros, écarts de 8 :
                   4 × 144 + 3 × 8 = 600 ≤ 642. QUATRE COLONNES, comme avant la marge (642 → 154,5 px par
                   colonne au lieu de 164).
         1180 px : zone de travail 1124, panneau 28 % = 314,7 + 14 = 328,7, tuiles 795,3 moins 24 = 771,3 px.
                   5 × 144 + 4 × 8 = 752 ≤ 771,3. CINQ COLONNES, comme avant (771,3 → 147,9 px par colonne
                   au lieu de 150,6). C'est cette colonne de 147,9 px qui fixe la largeur du pied d'une
                   tuile au verre (voir .tile-action).
       Aucune colonne perdue, donc, sur aucun des deux écrans. Ce que la marge coûte vraiment est VERTICAL :
       28 px de hauteur de panneau, soit une demi-ligne d'article (voir .caisse-line, 44 px).
       2/5 des surfaces qui floutent (vague 16, point 3). C'est la plus grande des cinq, et la seule qui
       mérite la dépense : le panneau de vente est posé sur les tuiles pendant toute la soirée. Détaché, son
       flou a enfin quelque chose à flouter sur ses quatre côtés.
       Le trait fait le tour, maintenant qu'il y a un tour : un bord gauche seul sur une carte flottante se
       lirait comme un défaut d'affichage. */
    margin: var(--panel-gap) var(--panel-gap) var(--panel-gap) 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-panel);
    backdrop-filter: var(--glass-blur, none);
    -webkit-backdrop-filter: var(--glass-blur, none);
    box-shadow: var(--glass-sheen), var(--shadow-panel);
}

/* Bandeau des catégories, un bouton par catégorie à sa couleur : collé en haut de la page Stock, et repris
   dans la barre de la caisse (.caisse-bar), qui lui donne là-bas ses réglages de barre d'outils. */
.caisse-strip {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    padding: 0.5rem 0;
    background-color: var(--bg);
    border-bottom: 1px solid var(--border);
    margin-bottom: 0.75rem;
}

/* Pastille de catégorie de la caisse : elle suit la hauteur des actions (36 px, 23/09/2026) — c'est une ancre
   qui déplace l'écran, pas un geste qui écrit. La page Stock, elle, garde ses onglets de 44 px (.stock-tab). */
.caisse-strip-link {
    min-height: var(--action-height);
    display: inline-flex;
    align-items: center;
    padding: 0 1rem;
    border-radius: var(--radius-pill);
    /* Teinte 10 % et encre 700 (vague 16, point 4). OPAQUE À 90 % ET SANS FLOU, avec le même trait et le
       même reflet que les tuiles : c'est la décision de performance iPad du point 3, écrite une deuxième
       fois ici parce que c'est l'autre endroit qu'on serait tenté de flouter. Voir .tile pour le pourquoi.
       Deux couches comme sur une tuile : la teinte en image, la surface opaque en couleur de fond. */
    background-image: linear-gradient(var(--cat-tint), var(--cat-tint));
    background-color: var(--tile-bg, var(--panel-bg));
    color: var(--cat-ink);
    box-shadow: 0 0 0 1px var(--border) inset, var(--glass-sheen);
    font-weight: 600;
    text-decoration: none;
}

.tile-group {
    /* Écran de référence 768 px de haut : l'air entre deux catégories est ce qui coûte le plus de rangées. */
    margin-bottom: 0.75rem;
    /* Le bandeau des catégories est désormais hors de la zone qui défile : une ancre amène le titre du groupe
       tout en haut de cette zone, où rien ne le recouvre. Reste un peu d'air sous le trait du bandeau. */
    scroll-margin-top: 0.5rem;
}

/* EN-TÊTE DE SECTION EN CAPITALES 12/800, SANS TRAIT DE SÉPARATION (06/10/2026, vague 17, point 3). La
   maquette en fait une étiquette et non un titre : 12 px, graisse 800, capitales, interlettrage ouvert, un
   point coloré devant, et rien en dessous — le trait d'avant doublait un blanc qui séparait déjà les
   groupes, et il barrait la grille d'un bout à l'autre toutes les quatre rangées.
   CE QUE ÇA REND À LA VERTICALE, et il faut le recompter parce que le budget de .caisse-tiles comptait
   « 30 px de titre de groupe » : 16 px × 1,2 + 3,2 (rembourrage bas) + 6,4 (marge) + 1 (trait) = 29,8 px
   AVANT ; 12 px × 1,2 + 6,4 (marge) = 20,8 px APRÈS, soit 9 px rendus par catégorie affichée.
   La hauteur d'interligne reste écrite plutôt que laissée à « normal », qui vaut 1,2 sur l'iPad et 1,33 sur
   le poste de développement — quatre pixels d'écart par catégorie affichée, et c'est une rangée de tuiles qui
   passe ou ne passe pas sur l'iPad de 768 px, où le budget de .caisse-tiles tient à 9,2 px près.
   LE TEXTE RESTE EN --muted ET NON À L'ENCRE DE LA CATÉGORIE, bien que la maquette le colore : l'encre 700
   d'une catégorie est faite pour un fond clair et ne se lit pas sur le thème « encre », où le fond est un
   bleu-noir — il faudrait une table de huit couleurs par thème sombre pour la seule couleur d'un titre.
   C'est le POINT qui porte la catégorie, et l'utilisateur ne demande que lui (« avec point coloré ») ;
   --muted vaut 4,85:1 sur le fond du verre et de 5,2 à 5,5:1 sur les trois autres. */
.tile-group-title {
    display: flex;
    align-items: center;
    font-size: 0.75rem;
    font-weight: 800;
    line-height: 1.2;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--muted);
    margin-bottom: 0.4rem;
}

    /* Le point de la caisse est plus petit que celui des pages de lecture : 8 px, la valeur de la maquette,
       devant une étiquette de 12 px. Le disque de 1rem de .category-dot était réglé pour un titre de 1rem,
       et il écraserait des capitales de 12 px. vertical-align n'a plus d'objet : le titre est un flex. */
    .tile-group-title .category-dot {
        width: 0.5rem;
        height: 0.5rem;
        margin-right: 0.5rem;
        vertical-align: 0;
        flex: 0 0 auto;
    }

/* Largeur de colonne : elle suit le curseur, sinon des tuiles plus basses ne feraient gagner que des rangées et
   laisseraient la moitié de l'écran en blanc. Une tuile et demie de haut (144 px à la borne haute, soit les 9 rem
   d'origine au pixel près), jamais moins que les 96 px de .tile — en dessous, le prix ne tient plus à côté de
   l'indicateur des verres. Le gap, lui, ne bouge pas : c'est ce qui sépare deux cibles. */
.tile-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(max(96px, calc(var(--tile-min-height) * 1.5)), 1fr));
    gap: 0.5rem;
}

/* GRILLE DE CHOIX D'UNE MODALE À TUILES (vague 17 bis, point 1) : la même tuile, la même largeur de colonne et
   le même écart qu'à la caisse — mais les tuiles NE S'ÉTIRENT PAS pour remplir la ligne, elles gardent leur
   largeur et se centrent. C'est ce qui permet à la modale de prendre la largeur de son contenu (voir
   .caisse-sheet-choices) au lieu d'une largeur fixe : deux choix ne doivent pas s'étaler sur 736 px, six ne
   doivent pas s'y entasser.
   POURQUOI UN FLEX ET PLUS UNE GRILLE, et c'est le cœur de la mécanique : « repeat(auto-fill, …) » compte UNE
   seule colonne quand on lui demande sa largeur maximale (espace disponible indéfini, CSS Grid § 7.2.3.2), donc
   un « width: fit-content » posé au-dessus d'une grille auto-fill rendrait une modale d'une colonne de large.
   Un conteneur flex qui se replie, lui, demande la somme de ses éléments : la modale fait la largeur de tous les
   choix sur une ligne, et dès que le plafond la rattrape, la ligne SE REPLIE d'elle-même. Aucun comptage de
   tuiles nulle part, aucune colonne à tenir à jour.
   Largeurs, aux deux écrans de référence et au réglage par défaut du curseur (colonne de 144 px, écart de 8) :
   deux choix 296 px, trois 448, quatre 600, cinq 752, six 904. Le plafond (92 vw, moins le rail et le
   rembourrage) laisse donc SIX choix sur une ligne à 1180 × 820 et CINQ à 1024 × 768, le sixième passant à la
   ligne. Au réglage le plus serré (colonne de 96 px), six choix tiennent sur une ligne partout (616 px). */
.tile-grid-choice {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}

    /* La tuile garde la largeur d'une colonne de la caisse (la même expression qu'au-dessus, celle de
       .tile/min-width) et ne s'étire pas : flex-grow à 0, sinon la dernière ligne d'un repli étalerait ses
       tuiles sur toute la largeur et deux lignes n'auraient plus la même tuile. */
    .tile-grid-choice > .tile {
        flex: 0 0 auto;
        width: max(96px, calc(var(--tile-min-height) * 1.5));
    }

/* --tile-min-height : hauteur minimale de la tuile (03-caisse 6.3). Jamais rabotée d'office pour gagner de la
   place — les pixels se prennent sur les barres et les marges, pas sur la cible qui vend ; depuis le 23/09/2026
   c'est l'utilisateur qui la règle au curseur, entre 44 px (le plancher tactile) et 96 px (la taille d'origine).
   Un minimum, pas une hauteur fixe : un nom sur deux lignes rend toujours sa tuile un peu plus haute. */
.tile {
    min-height: var(--tile-min-height);
    min-width: max(96px, calc(var(--tile-min-height) * 1.5));
    display: flex;
    flex-direction: column;
    border-radius: var(--radius-tile);
    overflow: hidden;
    /* LA TEINTE DE CATÉGORIE EST REVENUE LE 06/10/2026, le jour même où elle avait été retirée, et il faut
       lire les deux décisions pour comprendre qu'elles ne disent pas la même chose. Le matin : « surface
       blanche translucide, AUCUN fond de couleur de catégorie » — parce que la maquette ne peint ses tuiles
       qu'en blanc et que la vague 16 y avait laissé un aplat pastel. L'après-midi : « fond = teinte à 10 % de
       la couleur de catégorie SUR la surface blanche translucide (dérivée, comme les pastilles), bord à 25 %,
       reflet et ombre inchangés. Pas d'aplat pastel. » Ce qui est refusé n'est donc pas la couleur, c'est
       l'APLAT : la teinte est posée PAR-DESSUS la surface du thème et la laisse translucide, au lieu de la
       remplacer par un pastel opaque. Le reflet et l'ombre ne changent pas d'un pixel.
       Mise en œuvre : --cat-tint (la couleur à 10 %) en background-image, la surface en background-color.
       Deux couches, pas une couleur calculée à la main — c'est ce qui la rend « dérivée comme les
       pastilles », qui font exactement pareil. Mesuré : le texte du thème garde 14,78 à 16,22:1 sur les huit
       teintes (le pire étant le rouge), donc rien à corriger côté lisibilité.
       La catégorie se lit donc à QUATRE endroits : cette teinte, le bord, le point coloré du titre de groupe
       et la pastille d'ancre du bandeau.
       --tile-bg n'est déclaré que par le thème verre (90 %) ; les trois autres lisent --panel-bg et rendent
       une tuile opaque, exactement comme avant.

       AUCUN FLOU ICI, ET C'EST UNE DÉCISION, PAS UN OUBLI (vague 16, point 3). Une grille de caisse porte
       de vingt à quatre-vingt-quatre tuiles (voir le budget de .caisse-tiles) : autant de backdrop-filter,
       autant de couches à recomposer à chaque défilement, à chaque vente, à chaque rendu du circuit. Le
       flou est réservé aux CINQ surfaces qui sont uniques à l'écran — le rail, le panneau de vente, les
       modales, le voile du verrou, le toast. Si l'envie revient d'en ajouter un ici, ou sur
       .caisse-strip-link, c'est cette ligne qu'il faut contredire d'abord, mesure en main sur l'iPad.
       C'est aussi pourquoi la tuile reste à 90 % d'opacité là où la maquette descend à 55-78 % : sans flou
       derrière elle, une tuile plus transparente laisserait lire le halo jaune À TRAVERS le nom du produit.
       Ce qui tient le rôle du verre sans rien coûter : le reflet (une ligne blanche sur l'arête haute),
       le trait, et l'ombre portée. */
    background-color: var(--tile-bg, var(--panel-bg));
    /* La teinte par-dessus la surface, et non à sa place : deux couches, la tuile reste translucide. */
    background-image: linear-gradient(var(--cat-tint, transparent), var(--cat-tint, transparent));
    color: var(--fg);
    /* LE TRAIT EST UN BOX-SHADOW INTÉRIEUR, PAS UNE BORDURE, et c'est une mesure, pas une préférence : la
       tuile porte min-height: var(--tile-min-height) et sa zone de vente aussi (.tile-sell). Une bordure
       de 1 px la ferait grandir de 2 px — 98 au lieu de 96, 144 au lieu de 142 pour une tuile au verre —
       et le budget vertical de .caisse-tiles, qui décide combien de rangées passent à 768 px, deviendrait
       faux. Un anneau intérieur ne prend aucune place et se voit pareil en plein soleil. */
    /* LE BORD À 25 % EST POSÉ SUR LE TRAIT, PAS SUR DU BLANC, et c'est une mesure. La demande dit « bord à
       25 % » ; la couleur de catégorie à 25 % composée sur la surface blanche donne un bord à 1,08 à 1,37:1
       contre le fond de page — invisible, exactement « la bordure invisible » corrigée le 23/09/2026, et
       pire que le trait neutre qu'elle remplacerait (3,28:1). Composée sur le TRAIT, la même teinte à 25 %
       donne 3,63 à 4,14:1 : la tuile prend la couleur de sa catégorie ET garde une arête plus nette qu'avant.
       Les huit valeurs sont précalculées dans --cat-edge, comme --cat-ink l'est déjà pour le texte ; l'orange
       y part de son encre et non de son solide, trop clair pour franchir 3:1 (2,94:1 mesuré). */
    box-shadow: 0 0 0 1px var(--cat-edge, var(--border)) inset, var(--glass-sheen), var(--shadow-tile);

}

/* Haut de la tuile : la zone qui vend. L'indicateur des verres n'y est PLUS posé depuis le 06/10/2026 —
   il est descendu dans le pied, avec son nombre (voir .tile-foot). position: relative reste : c'est lui
   qui ancre la pastille de quantité du panneau d'offert (.tile-gift-count), posée dans le coin de la zone
   qui ajoute sans lui prendre de place. */
.tile-top {
    position: relative;
    flex: 1 1 auto;
    display: flex;
}

/* Zone qui vend : tout le haut de la tuile.
   NOM EN HAUT À GAUCHE, PRIX EN BAS À GAUCHE (06/10/2026, vague 17, point 2). La maquette ne centre rien :
   son bouton de tuile est un « flex-direction: column; justify-content: space-between; text-align: left »,
   et c'est ce qui donne à la grille son alignement de colonne — tous les noms sur une même ligne du haut,
   tous les prix sur une même ligne du bas, quel que soit le nombre de lignes du nom. Centré, chaque tuile
   plaçait son prix à une hauteur différente de sa voisine et l'œil devait chercher. */
.tile-sell {
    flex: 1 1 auto;
    min-width: 0;
    min-height: var(--tile-min-height);
    display: flex;
    flex-direction: column;
    align-items: stretch;
    justify-content: space-between;
    gap: 0.15rem;
    /* Le rembourrage suit le curseur : 0,5 rem à la borne haute (l'air d'origine), 0,25 rem à la borne basse,
       où les 8 px rendus sont justement ce qui permet au nom et au prix de tenir dans 44 px. */
    padding: clamp(0.25rem, calc(var(--tile-min-height) * 0.0834), 0.5rem);
    border: 0;
    background: transparent;
    color: inherit;
    /* CENTRÉ (06/10/2026) : nom, badge et prix sur des lignes centrées. L'alignement à gauche du matin
       venait de la maquette ; au comptoir, une grille de noms centrés se balaie mieux du regard, et c'est
       l'arbitrage de l'utilisateur. Les lignes restent celles d'avant — seul leur alignement change.
       EN HAUTEUR (vague 17 bis, point 2) : le « space-between » ci-dessus tient toujours le prix sur la
       ligne du bas, et le NOM se centre dans ce qui reste (voir .tile-head). Les deux ne se contredisent
       pas : l'un place les blocs, l'autre place le texte dans son bloc. */
    align-items: center;
    text-align: center;
}

    /* TUILE GROUPÉE : TROIS LIGNES CENTRÉES, EN LARGEUR ET EN HAUTEUR (vague 17 bis, point 2). Le nom, le
       badge « N choix » et le prix forment un bloc centré dans la tuile, là où le « space-between » des
       tuiles directes aurait posé le nom en haut et le prix en bas d'une tuile qui n'a, elle, aucune ligne
       de contenance à aligner avec ses voisines.
       Pourquoi la règle diffère de celle des tuiles directes, alors que les deux grilles doivent se
       ressembler : une tuile groupée ne vend rien au toucher, elle OUVRE un choix. Elle n'a donc pas à
       aligner son prix sur celui des tuiles qui vendent — ce prix est un « à partir de », pas le prix de
       l'article qui va partir —, et un bloc centré se distingue d'un coup d'œil d'une tuile qui vend. */
    .tile-sell-group {
        justify-content: center;
    }

/* Ligne du bas de la tuile : le prix à gauche, la contenance en gris à sa droite (point 2). Alignées sur la
   LIGNE DE BASE et non au centre : « 2,00 € » en 1,5 rem et « 25 cl » en 0,9 rem posent alors leurs chiffres
   sur le même trait, ce qu'un centrage vertical casserait. La contenance était une troisième ligne jusqu'ici,
   ce qui faisait une tuile au verre systématiquement plus haute qu'une tuile ordinaire ; remontée à côté du
   prix, elle ne coûte plus une seule rangée — c'est ce que la maquette dessine et c'est aussi 20 px gagnés
   par tuile au verre. */
/* Le prix, puis la contenance SOUS lui en petit gris (06/10/2026) : elle était à sa droite depuis le matin,
   ce qui désalignait les prix d'une tuile au verre et d'une tuile à la pièce dans la même rangée. */
.tile-figures {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0;
    min-width: 0;
}

/* Haut de la zone qui vend : le nom, et le badge « N choix » SOUS lui sur une tuile groupée (06/10/2026) —
   jamais à côté : à côté, il mangeait la largeur du nom, qui se replie déjà sur deux lignes dans une
   colonne de 148 px. */
.tile-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    min-width: 0;
}

    /* LE NOM EST CENTRÉ EN HAUTEUR (vague 17 bis, point 2), et c'est le seul changement des tuiles DIRECTES :
       le nom prend toute la place laissée par le prix et se centre dedans, au lieu d'être collé à l'arête
       haute par le « space-between » de .tile-sell. Tailles inchangées.
       CE QUE ÇA NE CASSE PAS, et c'était la condition du 06/10/2026 : le prix reste sur la LIGNE DU BAS,
       donc tous les prix d'une rangée restent alignés entre eux — c'est .tile-head qui absorbe la hauteur
       libre, pas le bloc entier. Un nom d'une ligne et un nom de deux lignes se centrent chacun dans la
       même boîte, et leurs prix sont à la même hauteur au pixel.
       :not(.tile-sell-group) : sur une tuile groupée, les TROIS lignes se centrent ensemble (voir
       .tile-sell-group), donc le nom ne doit surtout pas y pousser le prix vers le bas. */
    .tile-sell:not(.tile-sell-group) > .tile-head {
        flex: 1 1 auto;
        justify-content: center;
    }

/* Badge d'une tuile GROUPÉE (S15.3, vague 17 point 2) : il REMPLACE le chevron « › », que l'utilisateur a
   demandé de retirer le 06/10/2026. Il dit la même chose en mieux — « un choix suit » — et il dit en plus
   COMBIEN, ce que le chevron ne pouvait pas.
   IL EST DÉSORMAIS SEUL À LE DIRE (vague 17 bis, point 2) : l'absence de prix était le second signe, et le
   prix est revenu sur la tuile groupée le même jour. D'où le soin apporté à ce badge — c'est lui, et le nom
   accessible du bouton (CaisseFormat.VariantTileLabel), qui empêchent de croire qu'un toucher vend.
   SA TAILLE NE SUIT PAS LE CURSEUR, seule des trois lignes de la tuile groupée : ui-mockup § 2 fixe un
   plancher à 11 px pour un texte secondaire, et 0,7 rem EST ce plancher — un clamp qui descendrait avec la
   tuile écrirait « 3 choix » en 9 px au réglage le plus serré, c'est-à-dire ne l'écrirait plus. Ce que ça
   coûte, mesuré : aux deux plus petits réglages (44 et 48 px), les trois lignes demandent 56,8 px et la
   tuile groupée grandit d'autant — elle ne rogne ni ne coupe rien. Même arbitrage que .tile-serving, pour
   la même raison et aux deux mêmes réglages.
   Aucune couleur de catégorie : un gris de thème, comme la maquette (gris 700 sur gris clair).
   LE FOND EST --bg ET NON UN NOIR TRANSLUCIDE, bien que la maquette écrive « #f0f1f5 » : un noir à 6 %
   posé sur une tuile ne se verrait pas sur le thème « encre », dont la tuile est déjà sombre. --bg est le
   fond de la page — en creux sur une tuile plus claire que lui sur les trois thèmes clairs, plus sombre
   que lui sur le thème de nuit : un creux dans les deux sens de la lumière. --muted y est lisible par
   construction, c'est la paire que chaque thème a réglée (4,85:1 sur « glass », 5,5:1 sur « spontin »).
   11,2 px et non les 10,5 de la maquette : ui-mockup § 2 fixe un plancher à 11 px pour un texte
   secondaire, et un badge que personne ne lit ne dit pas combien il y a de choix. */
.tile-variants {
    flex: 0 0 auto;
    padding: 0.1rem 0.3rem;
    border-radius: 0.375rem;
    background-color: var(--bg);
    color: var(--muted);
    font-size: 0.7rem;
    font-weight: 800;
    line-height: 1.3;
    white-space: nowrap;
}

/* PIED DE LA TUILE AU VERRE (06/10/2026, vague 17, point 2) : un BANDEAU qui porte deux choses côte à
   côte — le compteur de verres à gauche, « Nouveau fût » en PASTILLE à droite. C'est la structure de la
   maquette, et c'est un changement de balisage, pas d'habillage : jusqu'ici le pied ÉTAIT le bouton
   « Nouveau fût », pleine largeur, et le compteur était posé en absolu dans le coin de la zone qui vend.
   Le bandeau discret (noir à 4 %) et son filet blanc viennent de la vague 16 et ne changent pas : le voile
   clair d'avant était fait pour se poser sur une couleur de catégorie saturée.
   LES PARCOURS NE BOUGENT PAS D'UN TOUCHER : le compteur ouvre toujours le détail des verres, la pastille
   ouvre ou change toujours le contenant. Seule leur place à l'écran change.
   DEUX PIXELS DE FILET, PAS UN (la maquette en montre un) : la hauteur d'une tuile au verre est écrite
   dans le budget vertical de .caisse-tiles — 96 + 44 + 2 = 142 px — et ce budget décide combien de rangées
   passent sur l'iPad de 768 px. Un pixel gagné ici ne servirait à rien et rendrait le calcul faux.
   44 PX ET NON LES 31 DE LA MAQUETTE, pour la même raison qu'aux « − » et « + » : « ouvrir un fût » est
   nommément dans la liste des touchers qui écrivent (voir --touch-target). La maquette est suivie pour
   l'APPARENCE — pastille de 24 px, compteur discret, bandeau ambré — dans une bande qui garde ses 44 px.
   Les deux cibles sont donc hautes de 44 px, et c'est aussi ce qui laisse les 142 px intacts. */
.tile-foot {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    /* 2 px de part et d'autre, et pas les 12 de la maquette : c'est le bandeau le plus serré de l'écran, et
       le compte est au pixel (voir .tile-container). Les deux cibles qu'il porte ont leur air à elles, en
       rembourrage interne — de l'air qui se touche vaut mieux que de l'air qui ne se touche pas. */
    padding: 0 0.125rem;
    min-height: var(--touch-target);
    border-top: 2px solid rgba(255, 255, 255, 0.60);
    background-color: rgba(23, 24, 28, 0.04);
}

    /* FÛT BAS : le bandeau passe à l'ambre dilué (point 2 : « bandeau ambré quand le fût est bas »).
       Il n'y a pas de classe pour le dire — le signal vit sur le compteur, qui est dans ce bandeau — et il
       n'en faut pas : :has lit l'état là où il est déjà écrit, sans une ligne de Blazor ni un attribut de
       plus. La règle est plus simple qu'avant la vague 17 : le compteur étant descendu DANS le pied, il
       n'y a plus de sélecteur de frère à écrire.
       Repli : un navigateur sans :has ignore la règle et garde le bandeau neutre. L'alerte ne disparaît
       pas pour autant, elle reste sur le compteur lui-même (.tile-glasses-low). */
    .tile-foot:has(.tile-glasses-low) {
        background-color: rgba(245, 177, 75, 0.18);
    }

/* « Nouveau fût » / « Ouvrir un fût » : la PASTILLE du pied (point 2). Collée à droite (margin-left: auto)
   que le compteur soit là ou non — sans contenant ouvert, le pied ne porte qu'elle, et elle reste à sa
   place apprise. La cible fait toute la hauteur du bandeau (44 px) ; ce qui est PEINT est la pastille de
   la maquette, d'où le bord transparent et le rayon de pastille.
   LE COMPTE LE PLUS SERRÉ DE LA VAGUE, et il faut l'écrire parce qu'il décide de trois valeurs de ce
   bandeau. Au réglage le plus gros, la colonne la plus étroite des deux écrans de référence est celle de
   1180 px : 147,9 px (voir .caisse-panel). Le bandeau y demande, Manrope mesurée :
     2 + 2 (son rembourrage) + 45,5 (le compteur : icône 16 + écart 3,2 + trois chiffres 22,3 + 4 de
     rembourrage) + 4 (l'écart du bandeau) + 90,1 (la pastille : « Nouveau fût » 78,9 + 11,2 de
     rembourrage) = 143,6 px. Il reste 4,3 px ; 11,7 avec un compteur à deux chiffres, qui est le cas
     courant. C'est pour tenir dans ces 4,3 px que le bandeau n'a que 2 px de rembourrage, le compteur
     2 px et la pastille 5,6 — la maquette en met 12, 12 et 9.
   AUX RÉGLAGES PLUS PETITS, LE MOT DE LA PASTILLE SE REPLIE SUR DEUX LIGNES et le bandeau grandit avec
   lui. Ce n'est pas une régression : c'est exactement ce que faisait le bouton pleine largeur d'avant la
   vague, dont le libellé en 0,95 rem (15,2 px, soit ~104 px) ne tenait déjà pas dans une colonne de
   103 px au cran le plus bas. En 0,72 rem il tient jusqu'à des colonnes de 102 px SANS le compteur, et
   le repli ne concerne donc que les produits au fût affichés très petit. Pas de nowrap pour cette raison :
   un mot rogné (« Nouveau fû… ») serait pire qu'un mot sur deux lignes.
   CLASSE DISTINCTE DE .tile-action, et ce n'est pas un doublon : .tile-action est la zone basse PLEINE
   LARGEUR d'une tuile, et elle sert toujours — c'est le « − » qui retire un article au panneau d'offert et
   dans la grille de choix (GiftPanel, VariantGrid). Un « − » en pastille de 11,5 px serait une cible
   ridicule pour un geste qui retire. Deux rôles, deux classes. */
.tile-container {
    flex: 0 1 auto;
    min-width: 0;
    min-height: var(--touch-target);
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    /* LE TOUR DE MAIN DE TOUTE LA VAGUE : un bord TRANSPARENT en haut et en bas, large de ce qui sépare
       la cible (44) de la pastille peinte (--pill-inner, 24). Le doigt touche 44 px, l'œil voit 24. Rien
       en largeur — un bord latéral, lui, élargirait la pastille de 20 px et « Nouveau fût » ne tiendrait
       plus dans la colonne la plus étroite ; c'est le rembourrage qui donne l'air autour du mot, et le
       fond le peint (background-clip reste à son défaut, la boîte de rembourrage). */
    padding: 0 0.35rem;
    border-style: solid;
    border-color: transparent;
    border-width: calc((var(--touch-target) - var(--pill-inner)) / 2) 0;
    border-radius: var(--radius-pill);
    background-color: rgba(23, 24, 28, 0.08);
    background-clip: padding-box;
    color: inherit;
    font-size: 0.72rem;
    font-weight: 800;
    line-height: 1.2;
    text-align: center;
}

/* Zone basse PLEINE LARGEUR d'une tuile : le « − » qui retire un article retenu, au panneau d'offert et
   dans la grille de choix (GiftPanel, VariantGrid). Séparée de 44 px de la zone qui ajoute, pour ne jamais
   retirer d'un doigt qui glisse (S3.8). Le bandeau discret (noir à 4 %) et son filet blanc de 2 px sont
   ceux de .tile-foot : c'est le même pied, avec un seul bouton qui le remplit.
   ELLE NE PORTE PLUS « NOUVEAU FÛT » depuis le 06/10/2026 : la maquette en fait une pastille dans un
   bandeau qui porte aussi le compteur de verres, et c'est .tile-container, juste au-dessus. */
.tile-action {
    min-height: var(--touch-target);
    border: 0;
    border-top: 2px solid rgba(255, 255, 255, 0.60);
    background-color: rgba(23, 24, 28, 0.04);
    color: inherit;
    font-size: 0.95rem;
    font-weight: 600;
}

/* NOM 15/800 ET PRIX 15/700 (06/10/2026, vague 17, point 2), c'est-à-dire la MÊME taille pour les deux, la
   graisse seule les distinguant. C'est ce que la maquette dessine, et c'est un vrai renversement : le prix
   valait 1,5 rem (24 px) contre 1,05 rem (16,8) au nom — il était le gros chiffre de la tuile. Ce qu'on y
   gagne, et c'est ce qui a décidé : à 24 px un prix à quatre chiffres (« 1 234,50 € » n'arrive pas au bar,
   mais « 12,50 € » si) poussait la contenance sous lui, donc une ligne de plus ; à 15 px le prix et la
   contenance tiennent sur UNE rangée, celle de .tile-figures, et toutes les tuiles de la grille alignent
   leur prix sur la même ligne. Le gros chiffre de l'écran reste le reste à payer du panneau (.caisse-total).
   Les deux suivent toujours le curseur, sans quoi la tuile ne descendrait jamais sous ~66 px : à 44 px, le
   texte d'origine dépasse à lui seul la hauteur demandée. Un seul clamp pour les deux, maintenant qu'ils
   ont la même taille — plancher 0,8 rem (12,8 px), borne haute 0,9375 rem (15 px, la maquette).
   Budget à la borne basse du curseur (44 px) : deux rangées de 14,7 px (interligne 1,15) + 2,4 d'écart
   + 8 de rembourrage = 39,8 px pour 44 disponibles. Il reste 4,2 px, contre 0 avant la vague. */
.tile-name {
    font-size: clamp(0.8rem, calc(var(--tile-min-height) * 0.175), 0.9375rem);
    font-weight: 800;
    line-height: 1.15;
    overflow-wrap: anywhere;
    min-width: 0;
    /* Deux lignes au plus (6.3). */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Contenance d'un verre (« 25 cl ») : À DROITE DU PRIX ET EN GRIS (point 2), plus en-dessous. Elle ne suit
   plus le curseur — elle n'est plus sur une ligne à elle, donc elle ne décide plus de la hauteur de la
   tuile, et une taille constante de 11,5 px (celle de la maquette) est plus simple et plus lisible qu'un
   clamp qui ne servait qu'à ça. CONSÉQUENCE VOULUE : une tuile au verre n'est plus plus haute qu'une tuile
   ordinaire à réglage égal — c'est une rangée de texte rendue à la grille. */
.tile-serving {
    flex: 0 0 auto;
    /* Elle suit le curseur comme le nom et le prix depuis le 06/10/2026, et c'est ce qui rend la
       rangée qu'elle occupe désormais payable : sous le prix, elle coûte une TROISIÈME rangée de texte
       dans une zone qui vend dimensionnée pour DEUX. Mesuré : à 0,72 rem fixe, l'empilement débordait
       dès 60 px de tuile ; à ce clamp, il tient dès 52 px, soit à DOUZE des quatorze réglages, le défaut
       compris. Il déborde encore aux deux plus petits — 44 px (de 6,8) et 48 px (de 2,8) —, où la tuile
       grandit d'autant : elle ne rogne ni ne coupe rien, mais une rangée de tuiles de moins peut tenir
       à l'écran. Ces deux réglages existent pour EMPILER des tuiles, donc c'est le mauvais endroit pour
       grandir : si quelqu'un s'en sert vraiment, la contenance doit retourner à côté du prix en dessous
       de 52 px, et cela demande une requête de conteneur que le projet n'emploie nulle part encore. */
    font-size: clamp(0.62rem, calc(var(--tile-min-height) * 0.12), 0.72rem);
    font-weight: 700;
    line-height: 1.1;
    color: var(--muted);
    white-space: nowrap;
}

/* La hauteur d'interligne est écrite plutôt que laissée à « normal », qui vaut 1,2 sur l'iPad et 1,33 sur le poste
   de développement : à la borne basse du curseur, ces quelques pixels décident si la tuile tient dans 44 px ou non
   (même raison qu'au titre de groupe).
   Le nom d'une tuile GROUPÉE a eu ce corps-là pendant une journée, et ce n'était pas ce qui était demandé :
   « la taille du prix d'une tuile simple » laissait le nom au pixel où il était, seule la graisse changeant.
   Il prend donc 1,3 fois ce corps depuis le 06/10/2026, et il a sa déclaration à lui, juste en dessous. */
.tile-price {
    font-size: clamp(0.8rem, calc(var(--tile-min-height) * 0.175), 0.9375rem);
    line-height: 1.15;
    min-width: 0;
}

.tile-price {
    font-weight: 700;
}

    /* LE NOM D'UNE TUILE GROUPÉE EST 1,3 FOIS LE CORPS DU PRIX D'UNE TUILE SIMPLE (06/10/2026), au réglage de
       curseur courant : même facteur et mêmes bornes que .tile-price, multipliés par 1,3 — 0,175 × 1,3 =
       0,2275, et les bornes 0,8 et 0,9375 rem deviennent 1,04 et 1,21875 rem. Le curseur s'applique donc
       à lui comme aux autres lignes.
       PAS D'ELLIPSE, et c'est explicitement demandé : le clamp à deux lignes de .tile-name est annulé ici
       (line-clamp à l'infini remet l'affichage en bloc normal), donc un nom trop long se replie au lieu de
       se faire couper. Il garde overflow-wrap: anywhere, hérité de .tile-name.
       CE QUE ÇA COÛTE, mesuré, et il faut le lire avant d'y toucher. Les trois lignes d'une tuile groupée
       (nom + badge + prix, écarts et rembourrage compris) demandent :
         • sur UNE ligne de nom : 75,0 px à 96 px de tuile, 69,7 à 80, 63,9 à 64 — ça tient ; mais 61,2 px
           aux réglages de 44 à 52, où la tuile grandit de 9 à 17 px. Elle grandissait déjà là (56,8 px
           avant ce changement) : le corps plus grand ajoute 4,4 px à un débordement qui existait.
         • sur DEUX lignes de nom : 97,5 px au réglage par défaut, soit 1,5 px de plus que la tuile — elle
           grandit d'un cheveu, et d'autant plus aux réglages serrés.
       Rien n'est jamais rogné ni coupé : la tuile porte min-height, pas height. Une rangée de tuiles de
       moins peut tenir à l'écran quand une tuile groupée a un nom long et que le curseur est serré.
       Graisse 700 et non 800 : le nom d'un groupe est un titre de trois lignes, pas l'étiquette d'un article
       qui part au toucher. */
    .tile-sell-group .tile-name {
        font-size: clamp(1.04rem, calc(var(--tile-min-height) * 0.2275), 1.21875rem);
        font-weight: 700;
        -webkit-line-clamp: none;
    }

/* .tile-chevron a été retirée le 06/10/2026 : « badge "N choix" à côté du nom, ET PLUS DE CHEVRON »
   (vague 17, point 2). Elle habillait le « › » qui, avec l'absence de prix, était le seul signe qu'un choix
   suivait sur une tuile groupée. Le badge .tile-variants le remplace et en dit plus : il compte les choix.
   L'absence de prix, elle, reste le second signe, et le nom accessible n'a pas changé d'un mot
   (CaisseFormat.VariantTileLabel). Vérifié sur tout le dépôt avant de la retirer : aucun usage. */

/* Verres restants (S3.4) : À GAUCHE DU PIED depuis le 06/10/2026, et plus en absolu dans le coin de la
   zone qui vend. Une icône de verre et SON NOMBRE, sur une rangée, comme la maquette.
   LE NOMBRE EST UN RETOUR DEMANDÉ LE 06/10/2026 (vague 17, point 2 : « compteur de verres » nommé par
   l'utilisateur), et il faut écrire ici POURQUOI il revient, sinon quelqu'un le retirera une troisième
   fois en citant l'ancienne décision :
     · 03/10/2026 — l'utilisateur fait RETIRER le nombre de la tuile (« c'est inutile » au comptoir),
       après l'avoir essayé en staging. Le bouton garde alors son icône, sa couleur d'alerte et son accès
       au détail ;
     · 05/10/2026 — la vague 16 habille le pied des tuiles et garde le nombre ABSENT, explicitement : la
       maquette le montrait déjà, c'était le seul endroit où elle n'était pas suivie ;
     · 06/10/2026 — l'utilisateur le REDEMANDE, en le nommant, pour la deuxième fois, et dans le même
       mouvement il descend le compteur dans le bandeau du pied, où le nombre a enfin la place de se lire
       à côté de son icône au lieu d'être tassé sous elle dans un carré de 44 px.
   Ce qui a changé entre les deux décisions n'est pas l'avis sur le nombre, c'est sa PLACE : le nombre
   retiré était celui des verres SERVIS, en 0,8 rem sous l'icône, dans le coin de la zone qui vend — un
   chiffre qui ne répondait à aucune question du comptoir. Celui-ci est celui des verres RESTANTS (le
   même que le nom accessible annonce depuis l'origine, GlassesDetailLabel), sur la ligne du pied, à côté
   de « Nouveau fût » : il répond exactement à la question qui précède ce geste-là — « est-ce qu'il faut
   en ouvrir un ? ».
   Au repos, aucune couleur propre : l'encre du thème, déjà en place sur la tuile. */
.tile-glasses {
    flex: 0 0 auto;
    /* 44 px de haut : c'est la cible, et c'est la hauteur du bandeau. La LARGEUR suit son contenu, avec
       --action-height (36 px) pour plancher : ouvrir le détail des verres ne REND rien et n'écrit rien —
       c'est la seule cible de la tuile dont --touch-target ne gouverne que la hauteur, et ces 8 px rendus
       sont ce qui laisse « Nouveau fût » entier dans la colonne la plus étroite. */
    min-width: var(--action-height);
    min-height: var(--touch-target);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.2rem;
    padding: 0 0.125rem;
    border: 0;
    border-radius: var(--radius-button);
    background-color: transparent;
    color: inherit;
    line-height: 1;
}

/* Moins d'un contenant plein : le NOMBRE passe à la couleur d'alerte du thème, et le bandeau qui le porte
   à l'ambre (voir .tile-foot:has). PLUS DE PASTILLE ROUGE PLEINE, et c'est une demande explicite du
   06/10/2026 : « plus de bouton rouge dans la tuile ». Le fond rouge de --primary-bg faisait du compteur
   la chose la plus voyante de la grille, pour une information qui n'est qu'un avertissement.
   POURQUOI --danger ET NON --warn, alors que le bandeau est ambré : mesuré sur les quatre thèmes, --warn
   ne passe pas partout sur ce bandeau-là. Le fond composé (ambre 18 % sur la tuile) vaut #fcf0de sur
   « glass » et « sauge », #403828 sur « encre » ; --warn y donne 5,36:1 (glass), 5,27:1 (sauge), 5,96:1
   (encre) mais seulement 4,38:1 sur « spontin » (#a16207), sous le 4,5:1 d'un texte de 12 px.
   --danger passe sur les quatre : 6,25:1 (glass #b01818), 5,76:1 (spontin #b91c1c), 4,78:1 (encre
   #f08a80), 6,73:1 (sauge #a32017). L'ambre dit l'alerte par le bandeau, l'encre d'alerte par le chiffre :
   deux signaux, aucun qui tienne à la seule couleur — le nombre lui-même dit tout. */
.tile-glasses-low {
    color: var(--danger);
}

.tile-glasses-icon {
    width: 16px;
    height: 16px;
    flex: 0 0 auto;
}

/* Le nombre de verres restants, 12 px gras comme la maquette. Chiffres tabulaires (html, body) : trois
   chiffres mesurent 21,4 px et ne font pas danser la pastille voisine d'une tuile à l'autre. */
.tile-glasses-count {
    font-size: 0.75rem;
    font-weight: 800;
}

/* Mode « Réorganiser » (D16) : la tuile ne vend pas. Le haut ne porte plus de bouton, seulement le nom et le
   prix — la zone qui vend n'est pas rendue du tout. */
.tile-top-reorder {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.15rem;
    padding: 0.5rem;
}

/* Les deux flèches, en bas de la tuile, à la place de « Nouveau fût » : même bandeau discret, mêmes
   44 px, même filet de 2 px — elles occupent exactement la place du pied qu'elles remplacent. */
.tile-arrows {
    display: flex;
    border-top: 2px solid rgba(255, 255, 255, 0.60);
    background-color: rgba(23, 24, 28, 0.04);
}

.tile-arrow {
    flex: 1 1 0;
    min-height: var(--touch-target);
    border: 0;
    background-color: transparent;
    color: inherit;
    font-size: 1.25rem;
    font-weight: 700;
}

/* Aux bornes : case inerte, sans gestionnaire, pour que « ▶ » reste toujours à droite. */
.tile-arrow-empty {
    opacity: 0.35;
}

/* Bandeau du mode : il dit pourquoi les tuiles ne répondent plus. Le filet porte la couleur d'avertissement. */
.caisse-reorder-banner {
    margin: 0.5rem 0.75rem 0;
    padding: 0.4rem 0.75rem;
    border: 2px solid var(--warn);
    border-radius: var(--radius);
    font-size: 1.05rem;
    font-weight: 600;
}

/* Produit au verre sans contenant ouvert : mention posée sur la couleur de la catégorie, aux couleurs du thème. */
.tile-warning {
    background-color: var(--panel-bg);
    color: var(--warn);
    border-radius: 0.25rem;
    padding: 0 0.35rem;
    font-size: 0.85rem;
    font-weight: 600;
}

/* ----- Panneau de la vente ----- */
/* Corps du panneau : il prend toute la hauteur et ne la dépasse jamais. Le défilement posé ici est le repli des
   panneaux qui n'ont pas de zone défilante propre (espèces, carte, attente du terminal) ; le panneau d'ardoise,
   lui, découpe sa colonne en tête / liste qui défile / pied, et ce repli ne s'y déclenche pas. */
.caisse-panel-body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding: 0.75rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* Tête du panneau d'ardoise : le nom de l'ardoise et les refus. Jamais rabotée par la liste. */
.caisse-panel-head {
    flex: 0 0 auto;
}

/* Seule partie du panneau qui défile : les articles. */
.caisse-panel-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* Pied du panneau, collé en bas : totaux, « Annuler la vente », « Mettre sur une ardoise » / « Retour à la vente
   comptoir », puis « Payer 9,70 € » (S16.1). Ce sont les boutons touchés à chaque vente : ils ne descendent
   jamais sous l'écran, quelle que soit la longueur de l'ardoise. */
.caisse-panel-foot {
    flex: 0 0 auto;
    padding-top: 0.5rem;
    border-top: 1px solid var(--border);
}

.caisse-panel-title {
    font-size: 1.25rem;
    font-weight: 700;
    margin-bottom: 0.5rem;
}

/* En-tête d'un panneau de paiement : le titre et « Retour » sur la même ligne (28/09/2026).
   Le retour était en bas de la pile des boutons : avec « Compte juste », quatre montants rapides, « Autre
   montant » et « Payer une partie », la pile fait huit boutons de 72 px et le retour tombait sous la ligne de
   flottaison — il fallait deviner qu'il fallait faire défiler pour revenir en arrière. Ici il est toujours
   visible, quel que soit le nombre de montants proposés. */
.caisse-panel-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}

    .caisse-panel-head .caisse-panel-title {
        margin-bottom: 0.5rem;
    }

/* Cible tactile pleine (--touch-target) sans le poids d'un bouton d'action : revenir en arrière ne doit
   jamais peser autant qu'encaisser. */
.caisse-panel-back {
    flex: 0 0 auto;
    min-height: var(--touch-target);
    padding-inline: 0.5rem;
    text-decoration: none;
}

/* Même précaution que .caisse-totals : sur un panneau à sa borne basse, le montant en 2,5 rem passe à la
   ligne au lieu de déborder. */
.caisse-panel-balance {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
}

.caisse-lines {
    list-style: none;
    padding: 0;
    margin: 0 0 0.75rem;
}

/* UNE LIGNE SANS TRAIT, AVEC UN RAYON (06/10/2026, vague 17, point 5) : la maquette ne sépare pas ses
   lignes d'articles, elle les pose comme des rangées à coins arrondis. Le trait d'avant barrait la colonne
   sous chaque article, en-têtes de jour compris, et c'est justement l'en-tête de jour en capitales qui
   sépare maintenant ce qui doit l'être.
   CE QUE ÇA REND, et c'est ce qui compte le plus dans ce panneau : la ligne mesurait 53 px (44 de cible
   + 8 de rembourrage + 1 de trait) ; elle en mesure 44, soit exactement la hauteur de ses deux cibles.
   Neuf pixels par ligne, c'est une ligne d'article de plus toutes les cinq. */
.caisse-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    /* L'ÉCART ET LE REMBOURRAGE TOMBENT À 0,25 rem, et ce n'est pas un resserrement : les deux carrés de
       34 px portent chacun 5 px de bord transparent (--touch-inner), qui sont de l'air pour l'œil. Entre
       le « − » et le « + » on voit donc 4 + 5 + 5 = 14 px, contre 8 px avant la vague, et la maquette en
       montre 10. Les 16 px rendus par l'écart et les 8 par le rembourrage vont au NOM de l'article, qui en
       a besoin — le montant prend maintenant une colonne fixe de 4 rem. Le compte est sur
       .caisse-line-name. */
    gap: 0.25rem;
    padding: 0 0.25rem;
    border-radius: var(--radius-button);
}

/* Signature de la ligne dans le détail d'une ardoise (S8.6) : « ajoutée par Julie, retirée par Tom à 21:12 »,
   seule sous l'article, en petit et jamais en couleur d'alerte. */
.caisse-line-author {
    flex: 1 0 100%;
    font-size: 0.8rem;
    color: var(--muted);
}

/* Nom de l'article. flex-basis 0 et min-width 0 (B1) : avec « − » ET « + », le nom demandait plus de place qu'il
   n'en reste sur un panneau à sa borne basse, et un élément de flex qui dépasse fait passer le dernier bouton à la
   ligne — le « + » se serait retrouvé seul sous la ligne. À base nulle, le nom prend ce qui reste et se replie
   dedans (overflow-wrap), les deux boutons restent à sa droite.
   Compté à 1024 × 768, panneau sur sa borne basse de 18 rem : 288 − 24 (rembourrage) = 264 px de ligne, dont 44 + 44
   (les deux cibles) + 32 (quatre écarts de 0,5 rem) de cadre, et 31 (« × 12 ») + 58 (« 12,50 € ») de texte qui
   ne se coupe pas — 209 px tenus, soit ≈ 55 px pour le nom.
   RECOMPTÉ LE 05/10/2026 AVEC MANROPE (vague 16, point 2), chiffres tabulaires compris. Les deux largeurs de
   texte étaient de 31 et 53 px en system-ui : « × 12 » ne bouge pas d'un pixel (31,0 mesuré), « 12,50 € »
   passe de 53 à 58,3 — les chiffres tabulaires de Manrope mesurent 9,92 px à 16 px, contre 8,88 pour ceux de
   la police système. Le nom perd donc 5 px, de ≈ 60 à ≈ 55.
   RECOMPTÉ LE 06/10/2026 (vague 17, point 5), quatre choses ayant bougé sur cette ligne : le panneau a pris
   un trait sur ses quatre côtés (2 px de largeur), le montant est passé à une COLONNE FIXE de 4 rem (64 px
   au lieu de 58,3 de texte), le rembourrage de la ligne est tombé à 0,25 rem et son écart aussi.
     288 − 2 (traits) − 24 (rembourrage du corps) = 262 px de colonne
     262 − 8 (rembourrage de la ligne) = 254 px de ligne utile
     254 − 44 − 44 (les deux cibles, intactes) − 16 (quatre écarts de 0,25 rem) − 31 (« × 12 ») − 64 (la
     colonne du montant) = 55 px POUR LE NOM.
   Exactement les 55 px d'avant la vague, au pixel : ce que la colonne fixe et le trait prennent (7,7 px),
   l'écart resserré et le rembourrage le rendent (24 px), et les 16 px de trop vont à la marge. « Coca Zéro »
   s'y replie toujours en deux lignes, « Coca » (42 px) tenant avec 13 px de marge.
   « Coca Zéro » s'y replie toujours en deux lignes, la plus large étant « Coca » à 42 px (36 px avant la
   vague) : il reste 13 px de marge. C'est pour cela qu'il n'y a PAS d'ellipse ici : à 55 px elle couperait
   après CINQ caractères (« Coca… », 53 px — « Coca Z » en demande déjà 56), et le bar lit le nom de
   l'article, pas son début.
   L'interligne est écrit plutôt que laissé à Bootstrap (1,5, soit 25,2 px par ligne) : à 1,2 les deux lignes du nom
   font 40,4 px et passent sous les 44 px que les deux boutons imposent déjà, donc la ligne de vente garde la
   hauteur qu'elle a aujourd'hui. Un nom qui demanderait trois lignes la fait grandir, comme avant lui. */
/* « flex: 0 1 auto » depuis le 06/10/2026 (vague 17, point 5), et non plus « 1 1 0 » : la maquette écrit le
   nom et la quantité D'UN SEUL TENANT (« Jupiler × 3 »), suivis du vide. À base nulle, le nom prenait
   toute la place libre et poussait son « × 3 » loin de lui, au milieu de la ligne. À base automatique, il
   prend la largeur de son texte, « × 3 » le suit aussitôt, et c'est le compteur qui porte le vide
   (margin-right: auto). Le budget du pire cas ne change pas d'un pixel : un nom trop long est le SEUL
   élément qui puisse rétrécir (min-width: 0 et overflow-wrap), il reçoit donc exactement les 55 px
   calculés ci-dessus — la base automatique ne change que le cas des noms courts, qui sont la majorité. */
.caisse-line-name {
    flex: 0 1 auto;
    min-width: 0;
    font-size: 1.05rem;
    line-height: 1.2;
    overflow-wrap: anywhere;
}

.caisse-line-count {
    flex: 0 0 auto;
    /* Le vide de la ligne est ici, entre la quantité et les boutons : c'est ce qui colle « × 3 » au nom. */
    margin-right: auto;
    font-weight: 600;
    white-space: nowrap;
}

/* MONTANT EN COLONNE, ALIGNÉ À DROITE (06/10/2026, vague 17, point 5) : la maquette lui donne une largeur
   fixe de 64 px et l'aligne à droite, au bout de la ligne, APRÈS les deux boutons. C'est ce qui met tous
   les montants de l'ardoise sur une même colonne — avec les chiffres tabulaires de Manrope, les virgules
   tombent les unes sous les autres et la somme se vérifie d'un coup d'œil.
   4 rem = les 64 px de la maquette, et ils suffisent : « 12,50 € » mesure 58,3 px en Manrope (le chiffre
   est sur .caisse-line-name). Un montant à trois chiffres déborderait de sa boîte sans la rogner — flex
   ne coupe pas un nowrap —, et le nom cède alors de la place, comme il le fait déjà. */
.caisse-line-amount {
    flex: 0 0 auto;
    width: 4rem;
    text-align: right;
    font-weight: 600;
    white-space: nowrap;
}

/* Prix figé de la ligne, affiché dans le détail d'une ardoise (page Ardoises) à côté de la quantité. */
.caisse-line-unit {
    color: var(--muted);
    white-space: nowrap;
}

/* « − » et « + » d'une ligne de vente (B1) : deux cibles de 44 px, le plancher de tout toucher qui écrit, séparées
   par l'écart de la ligne (0,5 rem) — deux boutons collés dont les effets s'opposent se touchent par erreur.
   CE SONT LES DEUX BOUTONS QUE LA MAQUETTE DESSINE À 34 PX, et elle les a redemandés trois fois. Depuis le
   06/10/2026, ILS ONT LES DEUX : la CIBLE reste à 44 px (--touch-target, et la raison est écrite une fois
   pour toutes à sa déclaration), le CARRÉ PEINT descend aux 34 px de la maquette (--touch-inner).
   Comment : un bord transparent de 5 px fait le tour, le fond ne peint que la boîte de rembourrage
   (background-clip) et le rayon de la maquette (10 px) est posé sur cette boîte-là — d'où le « + 5 » sur le
   rayon du bord extérieur, qui rend 10 px à l'intérieur. Le trait du bouton ne peut pas être une bordure,
   la bordure étant devenue transparente : c'est un anneau en ombre intérieure, qui se dessine lui aussi
   sur la boîte de rembourrage, donc sur le carré de 34 px.
   Ce que ça donne à l'œil par rapport à la maquette : exactement ses deux carrés de 34 px, et 18 px
   apparents entre eux au lieu de 8 — l'écart de la ligne (8 px) plus les deux bords transparents (2 × 5).
   La maquette en montre 10. On est plus près d'elle qu'avant, pas moins. */
.caisse-line-remove, .caisse-line-add,
.caisse-line-remove:focus, .caisse-line-add:focus,
.caisse-line-remove:focus-visible, .caisse-line-add:focus-visible {
    flex: 0 0 auto;
    min-width: var(--touch-target);
    min-height: var(--touch-target);
    padding: 0;
    border-style: solid;
    border-color: transparent;
    border-width: calc((var(--touch-target) - var(--touch-inner)) / 2);
    /* Le rayon du carré PEINT est --radius-button : il se mesure sur la boîte de rembourrage, donc il faut
       y ajouter la largeur du bord pour l'écrire sur la boîte extérieure. La maquette dit 10 px, la famille
       des rayons dit 12 (le cran du milieu, voir :root) — on garde la famille, l'écart est de deux pixels
       sur un carré de 34. */
    border-radius: calc(var(--radius-button) + (var(--touch-target) - var(--touch-inner)) / 2);
    background-clip: padding-box;
    /* L'anneau est répété pour :focus parce que « .btn:focus » d'app.css met box-shadow à none (il annule
       le halo bleu de Bootstrap) : sans cette reprise, le carré perdrait son trait après chaque toucher —
       sur iPad le focus reste collé au dernier bouton pressé. L'anneau de focus CLAVIER, lui, est un
       outline et ne passe pas par là. */
    box-shadow: 0 0 0 1px var(--btn-border) inset;
    font-size: 1.25rem;
    line-height: 1;
}

    /* Le « − » garde le trait d'alerte que .btn-outline-danger lui donnait en bordure : la bordure est
       transparente maintenant, l'anneau prend le relais. Son texte reste en --danger (règle de
       .btn-outline-danger, inchangée) et le carré garde le fond des boutons du thème. */
    .caisse-line-remove,
    .caisse-line-remove:focus,
    .caisse-line-remove:focus-visible {
        box-shadow: 0 0 0 1px var(--danger) inset;
    }

/* Ligne retirée (S3.6) : gardée à l'écran, barrée et grisée, sans « − ». Elle ne compte ni au total ni au reste. */
.caisse-line-removed {
    color: var(--muted);
}

    /* Barré sur l'article et son montant seulement : la mention « retiré » reste lisible
       (text-decoration se propage aux descendants et ne s'y annule pas). */
    .caisse-line-removed .caisse-line-name,
    .caisse-line-removed .caisse-line-count,
    .caisse-line-removed .caisse-line-unit,
    .caisse-line-removed .caisse-line-amount {
        text-decoration: line-through;
    }

.caisse-line-removed-tag {
    font-size: 0.8rem;
    white-space: nowrap;
}

/* Heure du premier ajout d'un groupe (S15.10), dans le DÉTAIL d'une ardoise seulement : on lit « 20:15 »,
   puis ce qui a été servi, comme sur une ligne de journal. Le panneau de vente de la caisse n'affiche
   AUCUNE heure (arbitrage du 04/10/2026 : elle y coûtait deux des six lignes lisibles d'un coup à
   1024 × 768) — il garde l'en-tête de jour, qui répond à « quel soir ». Elle n'entre PAS dans la liste
   des éléments barrés de .caisse-line-removed : l'heure n'est pas l'article, elle ne se barre pas. */
.caisse-line-time {
    color: var(--muted);
    white-space: nowrap;
}

/* En-tête d'un bloc de jour de bar (S15.10) : « lun. 29/09 », toujours rendu, même sur une ardoise d'un seul
   jour. Mêmes déclarations que le sous-titre « Journée du … » qu'il remplace : c'est un renommage. */
/* EN-TÊTE DE JOUR EN CAPITALES 11/800 (06/10/2026, vague 17, point 5). La maquette en fait une étiquette,
   comme les en-têtes de section de la grille : petite, grasse, en capitales, interlettrage ouvert. C'est
   ce qui lui permet de séparer deux soirées maintenant que les lignes d'articles n'ont plus de trait sous
   elles — un 13,6 px demi-gras se lisait comme une ligne d'article un peu pâle.
   Le rembourrage latéral est celui des lignes (0,5 rem), pour que « LUN. 29/09 » s'aligne sur les noms
   d'articles. 11 px est le plancher de ui-mockup § 2 : on y est, pas en dessous. */
.caisse-line-day {
    margin-top: 0.5rem;
    padding: 0.25rem 0.25rem 0;
    font-size: 0.6875rem;
    font-weight: 800;
    letter-spacing: 0.05em;
    color: var(--muted);
    text-transform: uppercase;
}

/* Le PREMIER en-tête ne sépare de rien : sa marge du haut ne sert qu'à détacher un bloc du précédent.
   La rendre à la liste n'est donc pas une économie de pixels déguisée, c'est la règle juste — et elle vaut
   exactement la sixième ligne du panneau de vente à 1024 × 768 : l'en-tête y coûte 28,4 px (8 de marge
   + 20,4 de texte) sur les 341 disponibles, et 6 lignes de 53 px en demandent 318 — 346,4 avec la marge,
   338,4 sans. */
.caisse-lines .caisse-line-day:first-child {
    margin-top: 0;
}

.caisse-pending-card {
    border: 2px solid var(--warn);
    border-radius: var(--radius);
    padding: 0.5rem;
    margin-bottom: 0.75rem;
    background-color: var(--panel-bg);
}

.caisse-totals {
    margin: 0 0 0.75rem;
}

    /* flex-wrap : le panneau pouvant descendre à 18 rem, « Reste à payer » et un montant à trois chiffres en
       2,5 rem ne tiennent plus toujours sur la même ligne. Le montant passe alors dessous, entier et en gros
       chiffres, plutôt que de déborder du panneau ou d'être rogné. */
    .caisse-totals > div {
        display: flex;
        flex-wrap: wrap;
        align-items: baseline;
        justify-content: space-between;
        gap: 0.5rem;
    }

    /* TOTAUX GRIS DES DEUX CÔTÉS (06/10/2026, vague 17, point 5) : la maquette met « Total » ET son montant
       en 13 px gris — ce sont des informations de contrôle, pas la réponse qu'on vient chercher. Le montant
       était en --fg demi-gras, du même poids visuel que le reste à payer, et les trois chiffres du pied se
       disputaient l'œil. Un seul gagne maintenant, et c'est le bon. */
    .caisse-totals dt,
    .caisse-totals dd {
        font-size: 0.8125rem;
        font-weight: 700;
        color: var(--muted);
    }

    .caisse-totals dd {
        margin: 0;
    }

/* La rangée du reste à payer : le libellé reprend l'encre du thème en 14/800 (il n'est plus une info parmi
   les autres, c'est la question du pied), le montant garde le gros chiffre. */
.caisse-balance dt {
    font-size: 0.875rem;
    font-weight: 800;
    color: var(--fg);
}

/* LE RESTE À PAYER À 26/800 (06/10/2026, vague 17, point 5), contre 40 px demi-gras avant.
   SEULEMENT DANS LA RANGÉE DU PIED (.caisse-balance), et c'est délibéré : .caisse-total habille aussi le
   grand chiffre des panneaux d'encaissement, de la modale « Payer », du panneau d'offert et du détail des
   verres, où il est le CONTENU de l'écran et non une ligne d'un pied. Ceux-là gardent leurs 2,5 rem, et
   les hauteurs de modale qui les comptent (voir .caisse-pay-choices) ne changent pas d'un pixel.
   C'est la valeur de la maquette, et elle règle trois choses d'un coup :
     1. « Reste à payer » (102 px en Manrope) et « 212,30 € » tiennent ENFIN sur la même ligne à 1024 px.
        En 2,5 rem le montant mesurait 171 px, soit 281 px avec l'écart pour 264 px de ligne utile : il
        passait sous son libellé. En 1,625 rem il mesure 111 px, soit 221 px — il reste 43 px de marge.
        Le repli de .caisse-totals existe toujours, il ne se déclenche plus sur ce cas-là.
     2. Le pied du panneau d'une ardoise d'équipe rend les 46 px que ce repli lui coûtait (voir le budget
        de .caisse-shares, recompté là-bas).
     3. La rangée elle-même passe de 46 à 32 px (26 × 1,1 + 3 de marge de dd), soit 14 px de plus pour les
        articles.
   Reste-t-il « un gros chiffre » ? Oui, et c'est le plus gros de l'écran : 26 px gras, contre 17 px pour le
   titre de la journée et 15 px pour un prix de tuile. Ce qu'on perd, ce sont des pixels dont personne
   n'avait besoin pour lire quatre caractères à 50 cm. */
.caisse-balance .caisse-total {
    font-size: 1.625rem;
    font-weight: 800;
    letter-spacing: -0.02em;
    line-height: 1.1;
    color: var(--fg);
}

/* Le grand chiffre des panneaux d'encaissement, de la modale « Payer », du panneau d'offert et du détail
   des verres : inchangé depuis l'origine, 2,5 rem. Là, le montant EST l'écran. */
.caisse-total {
    font-size: 2.5rem;
    font-weight: 700;
    line-height: 1.1;
}

/* BOUTON SECONDAIRE DISCRET DU PANNEAU (06/10/2026, vague 17, point 5) : « Ardoises » et « Retour à la
   vente comptoir ». La maquette les dessine en texte gris sur rien — ni fond, ni trait —, et c'est juste :
   ils partagent le pied avec « Payer », la cible la plus lourde de l'écran, et deux boutons encadrés
   au-dessus d'elle lui disputaient l'œil à chaque vente. Ce ne sont pas des gestes rares pour autant :
   ils gardent la cible pleine (--touch-target) et toute la largeur du panneau.
   --muted sur la surface du panneau : 5,39:1 sur « glass », de 5,2 à 5,5:1 sur les trois autres. Une
   classe à part plutôt qu'une retouche de .btn-outline-secondary, qui habille une centaine de boutons
   ailleurs — sur une page de réglages, un bouton sans trait ne se verrait pas. */
.caisse-quiet {
    min-height: var(--touch-target);
    border-color: transparent;
    background-color: transparent;
    color: var(--muted);
    font-weight: 600;
}

    /* Bootstrap redonne ses couleurs à un bouton désactivé avec une spécificité plus forte. */
    .caisse-quiet:disabled, .caisse-quiet.disabled {
        border-color: transparent;
        background-color: transparent;
        color: var(--muted);
    }

.caisse-cancel {
    align-self: flex-start;
    margin-bottom: 0.75rem;
}

/* ----- Panneau « Parts » d'une ardoise d'équipe (S14.5) -----
   Il vit dans le PIED du panneau, avec les totaux et les moyens de paiement : ce sont les boutons qu'on touche
   toute la soirée, et ils ne descendent jamais sous l'écran — seule la liste des articles défile, comme partout
   ailleurs sur ce panneau.
   Budget vertical, panneau à sa borne basse (288 px de large, iPad de 1024), interlignes écrits plus bas pour que
   le calcul ne dépende pas du « normal » du navigateur :
     tête 32 (titre 24 + 8) · rembourrages du corps 24 · filet et rembourrage du pied 9
     totaux 82 (« Total » 24 + reste à payer 46 + 12) — 106 dès qu'une part est payée (« Déjà payé »)
     parts : filet 1 + 8 + titre 20 + grille + lignes payées (19 chacune) + 44 de « Répartir » + écarts de 4 + 12
     « Retour à la vente comptoir » 52 · rangée de « Payer » 84 (12 + 72), inchangée depuis S16.1
   À SIX parts non payées la grille fait trois rangées de 56 (deux colonnes de 8 rem dans 264 px utiles) plus deux
   écarts, soit 176 : le fixe vaut 536 px, et il reste 152 px de liste d'articles à 1180 × 820 (688 disponibles),
   100 px à 1024 × 768 (636 disponibles).
   Le pire cas n'est pas six parts mais UNE payée sur six : la grille garde ses trois rangées (cinq parts), et le
   pied gagne la ligne « Déjà payé » et la part payée — fixe 599 px, donc 89 px de liste à 820 et 37 px à 768.
   Rien ne déborde dans aucun des deux cas, et rien d'autre que les articles ne défile.
   DEPUIS MANROPE (05/10/2026), un cas de plus à compter : « Reste à payer » mesure 102 px et un montant à
   TROIS CHIFFRES en 2,5 rem (« 212,30 € ») 171 px, soit 281 px avec l'écart, pour 264 px de ligne utile à
   1024 px. Le montant passe donc sous son libellé, là où il tenait à côté en system-ui (238 px). Ce n'est
   pas un débordement — .caisse-totals prévoit ce repli depuis l'origine, et il reste entier et en gros
   chiffres — mais il coûte 46 px de plus au pied : le fixe du pire cas monte de 599 à 645 px, et la liste
   des articles tombe de 89 à 43 px à 820 et de 37 à 0 px à 768. À 1024 × 768, une ardoise d'équipe à plus
   de 100 € avec une part déjà payée n'affiche donc plus de ligne d'article sans défiler ; elle défile, et
   seuls les articles défilent. Aucun autre écran n'est concerné : partout ailleurs le panneau est plus
   large que sa borne basse, ou le pied plus court.
   CE CAS N'EXISTE PLUS DEPUIS LE 06/10/2026 (vague 17, point 5), et c'est le reste à payer passé à
   1,625 rem qui l'a effacé : « 212,30 € » y mesure 111 px, soit 221 px avec son libellé et son écart, pour
   254 px de ligne utile — il reste 33 px de marge et le repli ne se déclenche plus. Les 46 px reviennent
   au pied, et 23 de plus : la rangée du reste à payer tombe de 46 à 32 px, et « Total » comme « Déjà payé »
   de 24 à 19,5 px chacune (13/700 gris). Le fixe du pire cas repasse donc de 645 à 576 px.
   À quoi la vague reprend 30 px de hauteur DISPONIBLE, et il faut les compter aussi : le panneau est
   détaché de ses bords (2 × 14 de marge) et a pris un trait (2 × 1), voir .caisse-panel. Le compte net,
   pire cas — une part payée sur six, ardoise à plus de 100 € :
     1180 × 820 : 688 − 30 = 658 disponibles, 576 de fixe → 82 px de liste (43 avant). UNE ligne d'article
                  de 44 px y tient de nouveau, là où il n'en tenait aucune.
     1024 × 768 : 636 − 30 = 606 disponibles, 576 de fixe → 30 px de liste (0 avant). Toujours moins qu'une
                  ligne : sur l'iPad ancien, ce cas-là défile encore, et seuls les articles défilent.
   C'est le seul écran du panneau où la vague ne suffit pas, et c'est le même qu'avant elle — six parts, une
   payée, plus de 100 € : la rencontre des trois pires cas à la fois.
   Aucune hauteur nouvelle en dur : les boutons de part sont à --touch-target, le plancher de ce qui écrit de
   l'argent, et les colonnes sont en rem comme celles de .member-grid. */
.caisse-shares {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    margin-bottom: 0.75rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--border);
}

.caisse-shares-title {
    font-size: 1rem;
    line-height: 1.25;
    font-weight: 600;
    color: var(--muted);
    margin: 0;
}

/* Deux parts par rangée sur un panneau étroit, trois quand il est à sa borne haute (416 px) : six boutons de
   44 px en colonne mangeraient toute la place laissée aux articles sur l'iPad de 768 px. */
.caisse-share-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
    gap: 0.25rem;
}

/* Bouton d'une part à encaisser : le nom et le montant, alignés à gauche comme une liste. Le libellé vient d'une
   seule fonction (CaisseFormat.ShareButtonLabel) et passe à la ligne au tiret quand la colonne est étroite ;
   overflow-wrap : un nom à rallonge grandit le bouton, il ne déborde jamais du panneau. */
.caisse-share {
    min-height: var(--touch-target);
    display: inline-flex;
    align-items: center;
    justify-content: flex-start;
    text-align: left;
    font-size: 1rem;
    line-height: 1.25;
    font-weight: 600;
    overflow-wrap: anywhere;
}

/* Part déjà payée : sous les parts à encaisser, en retrait — c'est de l'histoire, pas un geste. Le montant reste
   lisible (il est celui réellement réglé) mais n'appelle plus rien, donc ni cadre ni cible tactile. */
.caisse-share-paid {
    font-size: 0.9rem;
    line-height: 1.3;
    color: var(--muted);
}

/* La rangée de paiement en bas du panneau, toujours visible : « Payer 9,70 € » seul depuis S16.1, « Valider »
   à 0 €, et ailleurs (modales, panneaux d'encaissement) les boutons d'action qui portent .caisse-pay.
   72 PX ET PAS 48, bien que la maquette de la vague 16 dessine ses boutons d'action à 48 px. C'est la cible
   la plus touchée de l'application — une fois par vente, toute la soirée — et sa hauteur est RÉFÉRENCÉE
   AILLEURS, deux fois : .caisse-toast se pose à 6,5 rem du bas « assez haut pour ne jamais recouvrir le
   bouton « Payer » (72 px) », et le budget vertical de .caisse-shares compte « rangée de « Payer » 84
   (12 + 72) ». Passer à 48 rendrait 24 px au panneau (une demi-ligne d'article), au prix de 33 % de la
   cible : l'utilisateur a tranché le 05/10/2026 de garder les 72. Qui y revient doit corriger les deux
   références ci-dessus en même temps.
   LES 72 PX N'ONT PAS BOUGÉ EN PASSANT DE TROIS BOUTONS À UN (S16.1), et c'était la condition du point 9 :
   la rangée garde sa hauteur, le pied garde son agencement, et aucun des deux budgets verticaux ci-dessus
   ne change d'un pixel. Ce que la rangée gagne est en LARGEUR : un seul bouton de 264 px au lieu de trois
   de 88, 67 et 94 px sur le panneau à sa borne basse (288 px, iPad de 1024), soit trois fois la cible pour
   le premier toucher — payé par un toucher de plus par encaissement, mesuré et assumé. */
.caisse-pay-row {
    display: flex;
    gap: 0.5rem;
    margin-top: auto;
    padding-top: 0.75rem;
}

.caisse-pay {
    flex: 1 1 0;
    min-height: 72px;
    font-size: 1.25rem;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

/* « Payer 9,70 € » : L'ENSEMBLE CENTRÉ DANS LE BOUTON (06/10/2026, vague 17 bis, point 3), là où le mot
   était collé au bord gauche et le montant au bord droit.
   DEUX DEMANDES SE CONTREDISENT ICI, ET IL FAUT LIRE LES DEUX. Le 05/10/2026 : « montant aligné à droite »,
   d'où le « justify-content: space-between; padding: 0 18px » de la maquette (caisse-modale-payer.html,
   ligne 222), lu comme un ticket — le geste d'un côté, la somme de l'autre. Le 06/10/2026 : « le mot n'est
   pas centré dans le bouton ». Les deux ne peuvent pas être vraies en même temps sur un bouton de 264 px
   de large : ou les deux textes sont poussés aux extrémités, ou ils forment un groupe centré.
   CE QUI A ÉTÉ CHOISI : le groupe centré, avec son écart interne — « Payer 24,50 € » lu d'un seul tenant,
   au milieu du bouton. C'est la demande la plus récente, c'est celle qui parle du SYMPTÔME vu à l'écran, et
   l'alignement à droite du montant n'était pas une fin en soi : il venait d'une maquette dont le bouton de
   modale est bien plus large. Le montant garde tout ce qui le rend lisible — sa graisse 800
   (.caisse-pay-amount) et les chiffres à largeur fixe de Manrope. Qui voudrait revenir au ticket doit
   d'abord relire ces deux demandes, pas seulement la première.
   MESURÉ, et rien ne se replie : sur le panneau à sa borne basse (288 px, iPad de 1024) le bouton offre
   238 px de place pour 136,4 px de texte — « Payer » 54,6 + l'écart 8 + « 24,50 € » 73,8 —, soit 50 px
   d'air de chaque côté ; 161,3 px pour « Payer 1 234,50 € », et il reste 38 px de chaque côté.
   L'écart de 0,5 rem est celui de la rangée des moyens (.caisse-pay-choices) : c'est l'écart de la caisse.
   Le rembourrage reste écrit, en PLANCHER : il garantit l'air aux bords si un montant devait un jour
   remplir le bouton. Le rayon de 14 px (--radius-pay) et le dégradé (--primary-gradient, voir .btn-primary)
   viennent de la maquette. La HAUTEUR, elle, reste à 72 px et pas aux 54 de la maquette : c'est la cible la
   plus touchée de l'application et elle est référencée deux fois ailleurs (voir .caisse-pay-row).
   Une classe à part et pas un sélecteur de position : « Valider » occupe exactement la même rangée quand
   le reste à payer est nul (S16.1) et n'a aucun montant à écarter. La rangée à trois boutons (le pied du
   détail d'une ardoise) non plus. Le centrage VERTICAL, lui, n'a pas changé d'une ligne : il vient de
   .caisse-pay (align-items: center sur un inline-flex), et il valait déjà pour les deux spans. */
.caisse-pay-split {
    justify-content: center;
    gap: 0.5rem;
    padding-inline: 1.125rem;
    border-radius: var(--radius-pay);
}

/* Le montant du bouton « Payer » : même corps que le mot, graisse 800 comme la maquette — c'est le chiffre
   qu'on vérifie avant de poser le doigt. Il n'est pas rendu quand le bouton est grisé (le reste à payer est
   alors nul) : le libellé perd son montant, décision du 05/10/2026, et c'est la même fonction qui décide. */
.caisse-pay-amount {
    font-weight: 800;
}

/* Trois boutons sur la même rangée : elle rend aux boutons la largeur de leur texte (flex-basis auto, le reste
   de la place étant partagé) et passe au corps courant, parce qu'en 1,25 rem gras le plus long replierait son
   bouton sur deux lignes. La hauteur de cible ne bouge pas : ces 72 px sont ce qu'on touche, et ils viennent
   de .caisse-pay.
   UN SEUL EMPLOI DEPUIS S16.1 (05/10/2026) : le pied du détail d'une ardoise (ArdoisesPage) — « Fermer »,
   « Fusionner dans une autre ardoise » et « Reprendre dans la caisse ». La feuille fait 46 rem (736 px) ;
   rembourrage (2 × 1 rem) et écarts (2 × 0,5 rem) déduits, il reste 688 px, et les trois textes en 1 rem
   demandent 54 + 254 + 190 px, soit 546 px rembourrage compris (518 px en system-ui avant la vague 16).
   SON PREMIER EMPLOI N'EXISTE PLUS, et les chiffres qui le comptaient sont morts avec lui : la rangée
   « Espèces », « Carte » et « Virement » du pied du panneau de vente (S13.1), 64 + 43 + 70 px de texte pour
   248 px disponibles, 22 px de marge. S16.1 l'a remplacée par un seul bouton « Payer », qui reprend toute la
   rangée et le corps de 1,25 rem — le modificateur n'y a plus d'objet. La classe reste parce que le second
   emploi, lui, a toujours le même problème dans une feuille large. */
.caisse-pay-row-three .caisse-pay {
    flex: 1 1 auto;
    min-width: 0;
    padding-inline: 0.5rem;
    font-size: 1rem;
}

/* Les moyens de la modale « Payer » (S16.1) : deux ou trois GRANDES TUILES côte à côte, à largeurs égales.
   « flex: 1 1 0 » vient de .caisse-pay et fait tout le travail — un moyen absent (« Virement » sur une vente
   comptoir) ne laisse aucun trou, c'est la rangée qui se partage autrement, sans qu'aucune règle ne compte les
   tuiles et sans grid-template-columns à tenir à jour.
   La modale est une .caisse-dialog (26 rem = 416 px) : rembourrage (2 × 1 rem) et traits (2 × 1 px) déduits, il
   reste 382 px de contenu, et les deux écarts (2 × 0,5 rem) laissent 122 px par tuile à trois, 187 px à deux.
   Une tuile garde le rembourrage d'un bouton (2 × 0,75 rem) et son trait : 96 px de texte. En 1,25 rem gras —
   le corps des cibles de paiement, hérité de .caisse-pay —, « Virement » demande 88 px et c'est le plus long
   des trois (« Espèces » 80, « Carte » 54) : il tient, avec 8 px de marge. Qui agrandit ce corps ou allonge un
   libellé doit remesurer celui-là.
   Hauteur totale de la modale, identique à deux et à trois tuiles : 295 px (rembourrage 32 + titre 40,8 +
   reste à payer 56 + tuiles 108 + « Retour » 56 + traits 2), pour 736 px de voile utile à 1024 × 768 et 788 px
   à 1180 × 820. Elle ne défile dans aucun des deux cas, et c'est le but d'une modale qu'on touche debout. */
.caisse-pay-choices {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
}

/* 6 rem = les 96 px de la maquette, écrits en rem comme toute taille qui n'est pas un plancher tactile. Seule
   déclaration de la tuile : le rayon, le relief et les couleurs viennent du bouton (--radius-button, les
   variantes Bootstrap réécrites plus haut), et son corps comme sa largeur de .caisse-pay. */
.caisse-pay-choice {
    min-height: 6rem;
}

/* Rappel du tiroir après un règlement espèces hors journée (S9.1) : sous la monnaie à rendre, lisible de
   loin mais plus discret qu'elle — c'est un rappel, pas un chiffre à compter. */
.caisse-drawer {
    font-size: 1.15rem;
    font-weight: 600;
    margin-bottom: 1rem;
}

.change-due {
    font-size: 2.75rem;
    font-weight: 700;
    line-height: 1.1;
    margin-bottom: 1rem;
}

/* ----- Caisse fermée (S3.3) ----- */
.caisse-closed {
    flex: 1 1 auto;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.25rem;
    padding: 1.5rem;
    text-align: center;
}

/* Caisse fermée avec une ardoise à encaisser (S9.1) : la zone des tuiles porte le rappel de ce qui reste
   permis, centré comme l'écran d'ouverture. Aucune tuile n'y est rendue — vendre n'est pas désactivé,
   il n'existe pas. height: 100 % : .caisse-tiles est un bloc, le centrage a besoin d'une hauteur. */
.caisse-payment-only {
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 1.5rem;
    text-align: center;
}

.caisse-payment-only-lead {
    font-size: 1.5rem;
    font-weight: 700;
    margin: 0;
}

/* « Ouvrir la caisse » et « Offrir » (S9.4) : des boutons ordinaires, à leur largeur de texte, sur une rangée
   centrée par .caisse-closed. La rangée revient à la ligne si la place manque — à 1024 px comme à 1180 px, les
   deux tiennent côte à côte. Chacun garde sa hauteur de cible tactile par .btn-touch. */
.caisse-closed-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.75rem;
    margin-top: 0.75rem;
}

.caisse-closed-actions .btn {
    padding-inline: 1.75rem;
}

.caisse-closed-title {
    font-size: 2.5rem;
    font-weight: 700;
}

/* ----- Pavé numérique (D8) : touches d'au moins 64 px, « Valider » pleine largeur ----- */
.numeric-pad {
    width: min(22rem, 100%);
    margin: 0 auto;
    text-align: left;
}

.numeric-pad-title {
    font-weight: 600;
    margin-bottom: 0.25rem;
}

.pad-display {
    display: flex;
    align-items: baseline;
    justify-content: flex-end;
    gap: 0.35rem;
    padding: 0.25rem 0.75rem;
    border: 1px solid var(--btn-border);
    border-radius: var(--radius-button);
    background-color: var(--panel-bg);
    color: var(--fg);
    font-size: 2.25rem;
    font-weight: 700;
}

/* Code masqué (S8.1, S8.4) : des points centrés, jamais un chiffre, et une hauteur stable même à vide. */
.pad-display-masked {
    justify-content: center;
    min-height: 3.5rem;
    letter-spacing: 0.5rem;
}

.pad-unit {
    font-size: 1.25rem;
    color: var(--muted);
}

.pad-keys {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.5rem;
    margin-top: 0.5rem;
}

.pad-key {
    min-height: 64px;
    font-size: 1.5rem;
    font-weight: 600;
}

.pad-validate {
    min-height: 64px;
    font-size: 1.25rem;
    font-weight: 600;
}

/* ----- Confirmation d'une action destructrice ----- */
/* Voile neutre (pas une couleur de thème) ; il commence après le rail, qui reste visible et utilisable — et, sur
   le staging, sous le bandeau (0 en prod), pour la même raison : ce qui dit où l'on est reste lisible. */
.caisse-overlay {
    position: fixed;
    inset: var(--staging-banner-h) 0 0 var(--rail-width);
    inset: var(--staging-banner-h) 0 0 calc(var(--rail-width) + env(safe-area-inset-left));
    z-index: 1080;
    overscroll-behavior: contain;
    touch-action: manipulation;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    /* Voile neutre par défaut (aucune couleur de thème). Le thème verre l'allège à 28 % et le floute de
       6 px : derrière une modale de verre, un voile opaque à moitié ne laisserait rien à flouter.
       Compté avec les modales dans le 3/5 des surfaces qui floutent (vague 16, point 3) : le voile et la
       feuille qu'il porte sont la même chose à l'œil. */
    background-color: var(--veil-bg, rgba(0, 0, 0, 0.5));
    backdrop-filter: var(--veil-blur, none);
    -webkit-backdrop-filter: var(--veil-blur, none);
}

.caisse-dialog {
    width: min(26rem, 100%);
    max-height: 100%;
    overflow-y: auto;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-modal);
    background-color: var(--panel-bg);
    color: var(--fg);
    /* 3/5 des surfaces qui floutent (vague 16, point 3) : les modales. Avec .caisse-sheet, ce sont les
       deux mêmes déclarations, écrites deux fois parce que les deux n'ont rien d'autre en commun. */
    backdrop-filter: var(--glass-blur, none);
    -webkit-backdrop-filter: var(--glass-blur, none);
    box-shadow: var(--glass-sheen), var(--shadow-modal);
}

.caisse-dialog-question {
    font-size: 1.25rem;
    font-weight: 600;
}

.caisse-dialog-button {
    min-height: 56px;
}

/* Toast de confirmation de la caisse : un seul à la fois, en bas à droite, au-dessus du panneau d'ardoise
   et des panneaux pleins écran (1080), sous le rail (1090). Il s'efface seul après quatre secondes ou au toucher.
   Les refus, eux, restent des bandeaux rouges dans le panneau : on ne fait pas disparaître une erreur. */
.caisse-toast {
    position: fixed;
    right: 1rem;
    right: calc(1rem + env(safe-area-inset-right));
    /* Assez haut pour ne jamais recouvrir le bouton « Payer » (72 px), qu'on doit pouvoir toucher aussitôt.
       Un seul bouton au lieu de trois depuis S16.1 : la rangée garde ses 72 px, ce calcul ne change pas. */
    bottom: 6.5rem;
    bottom: calc(6.5rem + env(safe-area-inset-bottom));
    z-index: 1085;
    max-width: 22rem;
    min-height: 44px;
    display: flex;
    align-items: center;
    padding: 0.6rem 0.9rem;
    background-color: var(--panel-bg);
    color: var(--fg);
    border: 1px solid var(--border);
    border-left: 6px solid var(--ok);
    /* Pastille (vague 16) : le toast est un message qui passe, pas une carte. Son filet vert devient l'arc
       coloré de son bord gauche, ce qui tient le rôle du point vert de la maquette sans un signe de plus
       dans le balisage. Le texte reste à l'écart de la courbe : il est centré en hauteur, là où l'arc est
       le plus plat, et à 0,9 rem du bord. */
    border-radius: var(--radius-pill);
    /* 5/5 des surfaces qui floutent (vague 16, point 3). */
    backdrop-filter: var(--glass-blur, none);
    -webkit-backdrop-filter: var(--glass-blur, none);
    box-shadow: var(--glass-sheen), var(--shadow-panel);
    cursor: pointer;
}

/* ----- Pastille des actions de la journée (« Ardoises ouvertes (3) ») ----- */
.caisse-badge {
    display: inline-block;
    min-width: 1.75rem;
    padding: 0 0.4rem;
    border-radius: 999px;
    background-color: var(--fg);
    color: var(--panel-bg);
    font-weight: 700;
}

/* ----- Panneaux plein écran de la caisse (personne, ardoises ouvertes, contenant, clôture) ----- */
.caisse-sheet {
    display: flex;
    flex-direction: column;
    width: var(--sheet-width);
    max-height: 100%;
    overflow-y: auto;
    padding: 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius-modal);
    background-color: var(--panel-bg);
    color: var(--fg);
    /* 3/5 des surfaces qui floutent (vague 16, point 3), avec .caisse-dialog. */
    backdrop-filter: var(--glass-blur, none);
    -webkit-backdrop-filter: var(--glass-blur, none);
    box-shadow: var(--glass-sheen), var(--shadow-modal);
}

/* MODALE À TUILES : LARGEUR = CONTENU (vague 17 bis, point 1), là où toute feuille de caisse mesurait les
   46 rem de --sheet-width. Le symptôme : « largeur fixe, vide avec 2 choix, entassée avec 6+ » — 736 px pour
   deux tuiles de 144, et quatre colonnes imposées dès le cinquième choix.
   UNE SEULE RÈGLE, PARTAGÉE, et c'est la condition du point 1 : elle habille le choix d'une variante
   (VariantPanel) ET le changement ou l'ouverture de contenant (ContainerChangePanel, « Quel fût ouvrir ? »).
   Deux copies auraient fini par se contredire ; la liste des porteurs est écrite ici, et une troisième modale
   à choix s'y ajoute en posant la classe, jamais en recopiant ces quatre lignes.
   PLANCHER 360 PX : en dessous, une modale de deux choix n'a plus l'air d'une modale, et son titre comme son
   bouton de sortie se replieraient. PLAFOND 92 VW : au-delà, la ligne de tuiles SE REPLIE (voir
   .tile-grid-choice) ; « min(92vw, 100%) » parce que 92 vw mesure le viewport ENTIER, rail compris — à
   1024 px le voile n'offre que 936 px utiles (1024 − 56 de rail − 2 × 16 de rembourrage) pour 942 de 92 vw,
   et le dernier mot doit rester à la place réelle.
   LE TITRE ET LE BOUTON DE SORTIE PRENNENT TOUTE LA LARGEUR : rien à écrire pour cela, .caisse-sheet est une
   colonne flex et ses enfants s'étirent déjà ; c'est la largeur de la feuille qui a changé, pas la leur.
   LES PARAGRAPHES SONT BRIDÉS À 46 REM, et c'est ce qui empêche la règle de faire l'inverse de ce qu'on lui
   demande : « fit-content » prend le plus large de ses enfants, et une phrase du changement de contenant
   (« Le fût ouvert il y a 2 h sera terminé… ») mesure plus de 1 000 px sur une seule ligne — sans cette
   bride, ces modales passeraient de 736 px à 1 085. Bridés à la largeur d'avant, ils ne peuvent que rendre
   de la place. Mesuré : choix de contenance 2 contenances ⇒ 360 px (736 avant), choix d'une variante à 2
   ⇒ 360, à 3 ⇒ 482, à 4 ⇒ 634, à 6 ⇒ 938 à 1180 × 820 et 936 avec repli à 1024 × 768. */
.caisse-sheet-choices {
    width: fit-content;
    min-width: min(360px, 100%);
    max-width: min(92vw, 100%);
}

    .caisse-sheet-choices > p,
    .caisse-sheet-choices > .alert {
        max-width: 46rem;
    }

.caisse-sheet-title {
    font-size: 1.5rem;
    font-weight: 700;
    margin-bottom: 0.75rem;
}

.caisse-sheet-subtitle {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--muted);
    margin-bottom: 0.5rem;
}

.caisse-sheet-question {
    font-size: 1.35rem;
    font-weight: 600;
}

/* Tête d'une feuille de caisse : le titre, et à droite l'interrupteur qui change le geste (« Ardoise d'équipe »,
   S14.4). Sur la même ligne, elle ne coûte pas un pixel de hauteur de plus que le titre seul, l'interrupteur
   étant à --touch-target ; la ligne passe en deux si la place manque. */
.caisse-sheet-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}

/* Choix d'une personne (S3.10) et reprise d'une ardoise (S3.11) : gros boutons, aucune cible sous 44 px. */
.member-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
    gap: 0.5rem;
}

.member-button {
    min-height: 64px;
    font-size: 1.15rem;
    font-weight: 600;
}

/* Coche d'une personne choisie pour une équipe (S14.4) : la couleur du bouton dit déjà l'état, le « ✓ » le dit
   sans elle — quatre thèmes, et on ne fait jamais porter un sens à la seule couleur. */
.member-picked {
    margin-right: 0.35rem;
    font-weight: 700;
}

/* Nom de l'équipe et compteur, sous les tuiles à cocher (S14.4) : UNE seule rangée — le libellé, le champ et le
   compteur côte à côte. Trois lignes empilées auraient coûté 40 px de plus, et la feuille doit tenir sans défiler
   à six personnes choisies sur l'iPad de 768 px. Le champ prend la place qui reste et passe à la ligne si elle
   manque ; sa hauteur de cible vient de .form-control-lg. */
.team-compose {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    text-align: left;
}

.team-compose-label {
    font-weight: 600;
    white-space: nowrap;
}

.team-compose-name {
    flex: 1 1 14rem;
    min-width: 0;
}

.team-compose-count {
    font-size: 0.9rem;
    color: var(--muted);
}

.letter-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.letter-button {
    min-width: 56px;
    min-height: 56px;
    font-size: 1.25rem;
    font-weight: 700;
}

.open-tab-button {
    min-height: 72px;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.1rem;
}

/* Tuile de la page Ardoises (ui-mockup 6) : bouton plein bloc qui ouvre le détail, posé dans la grille .tab-grid
   partagée avec la page Stock. Toute la tuile est la cible, bien au-delà de 44 px. */
.tab-tile {
    min-height: 120px;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.1rem;
    padding: 0.6rem 0.75rem;
    text-align: left;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--panel-bg);
    color: var(--fg);
}

    /* Pas de survol (vague 16, point 5) : le focus clavier suffit, et c'est le toucher qui donne le retour. */

    .tab-tile:focus-visible {
        outline: 3px solid var(--focus);
        outline-offset: 2px;
    }

.tab-tile-name {
    font-size: 1.3rem;
    font-weight: 700;
    overflow-wrap: anywhere;
}

.tab-tile-since {
    font-size: 0.9rem;
    color: var(--muted);
}

/* Le reste à payer en gros : c'est la réponse qu'on vient chercher d'un coup d'œil. */
/* Total d'un groupe d'ardoises, posé à la suite de son titre : il se lit comme une somme, pas comme un titre,
   d'où le poids ordinaire et la teinte atténuée. Il porte sur les tuiles affichées, recherche comprise.
   Deuxième emploi depuis S15.8, pour la même raison : le reste à payer posé à la suite d'un nom dans le choix de
   la cible d'une fusion. Là aussi le montant accompagne un libellé au lieu de le remplacer — c'est le nom qu'on
   touche, le montant n'est qu'un repère. */
.tab-group-total {
    margin-left: 0.5rem;
    color: var(--muted);
    font-weight: 400;
}

.tab-tile-amount {
    margin-top: 0.15rem;
    padding: 0 0.35rem;
    border-radius: 0.35rem;
    font-size: 1.75rem;
    font-weight: 700;
}

/* Carte en attente : couleur de l'action principale du thème, comme .stock-tile-low et .tile-glasses-low.
   La mention « carte en attente » reste écrite sous le montant : la couleur ne porte jamais seule le sens. */
.tab-tile-waiting {
    background-color: var(--primary-bg);
    color: var(--primary-fg);
}

.open-tab-label {
    font-size: 1.3rem;
    font-weight: 700;
}

.open-tab-detail {
    font-size: 0.9rem;
    color: var(--muted);
}

.open-tab-pending {
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--warn);
}

/* ----- Journal de caisse (ui-mockup 7) : une ligne de résumé par journée, dépliable au toucher ----- */
.journal-days {
    list-style: none;
    padding: 0;
    margin: 0;
}

.journal-day {
    margin-bottom: 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--panel-bg);
    overflow: hidden;
}

/* Toute la ligne est la cible du dépliement : bien au-delà des 44 px exigés. */
.journal-summary {
    width: 100%;
    min-height: 64px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.9rem;
    border: 0;
    background-color: transparent;
    color: var(--fg);
    text-align: left;
}

    /* Pas de survol (vague 16, point 5). */

.journal-day-head {
    flex: 1 1 13rem;
    min-width: 11rem;
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.4rem;
}

.journal-day-date {
    font-size: 1.25rem;
    font-weight: 700;
}

/* Journée en cours : une pastille, pas une couleur de texte. */
.journal-day-badge {
    padding: 0.1rem 0.6rem;
    border-radius: 999px;
    background-color: var(--warn);
    color: var(--bg);
    font-size: 0.8rem;
    font-weight: 700;
}

.journal-day-moments {
    flex: 1 1 100%;
    font-size: 0.85rem;
    color: var(--muted);
}

/* Les chiffres de la journée : libellé discret au-dessus, montant en gros. */
.journal-figures {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1.1rem;
}

.journal-figure {
    display: flex;
    flex-direction: column;
    min-width: 5.5rem;
}

.journal-figure-label {
    font-size: 0.75rem;
    color: var(--muted);
}

.journal-figure-value {
    font-size: 1.1rem;
    font-weight: 700;
    white-space: nowrap;
}

/* Écart : la couleur remplit une pastille, jamais le texte (--ok est interdit en texte, ui-mockup § 2,
   et --primary-bg tombe sous 4,5:1 sur le fond du thème « club »). Le signe porte le sens. */
.journal-gap-dot {
    display: inline-block;
    width: 0.7rem;
    height: 0.7rem;
    margin-right: 0.35rem;
    border-radius: 999px;
}

.journal-gap-ok {
    background-color: var(--ok);
}

.journal-gap-off {
    background-color: var(--primary-bg);
}

.journal-chevron {
    margin-left: auto;
    font-size: 1.25rem;
    color: var(--muted);
}

/* ----- Détail chronologique d'une journée ----- */
.journal-detail {
    padding: 0.25rem 0.9rem 0.75rem;
    border-top: 1px solid var(--border);
}

.journal-entries {
    list-style: none;
    padding: 0;
    margin: 0;
}

.journal-entry {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.5rem 0;
    border-bottom: 1px solid var(--border);
}

    .journal-entry:last-child {
        border-bottom: 0;
    }

.journal-entry-time {
    flex: 0 0 3.5rem;
    font-weight: 700;
    white-space: nowrap;
}

/* Section « Hors journée » du journal (S9.3) : la même carte qu'une journée, mais rien à déplier — elle
   n'est pas un bouton, et ses lignes sont là d'emblée. */
.journal-offsession-head {
    padding: 0.6rem 0.9rem;
}

/* Ces règlements ne vivent sous le jour d'aucune journée : leur ligne porte la date entière, pas la seule heure. */
.journal-entry-moment {
    flex: 0 0 9.5rem;
}

.journal-entry-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.journal-entry-title {
    font-weight: 600;
}

.journal-entry-user {
    font-size: 0.85rem;
    color: var(--muted);
}

/* Paiement carte en attente (--warn) ou refusé (--danger) : mention encadrée, à côté du moyen de paiement.
   La couleur est portée par le filet, le texte reste en --fg. */
.journal-entry-status {
    margin-left: 0.4rem;
    padding: 0 0.4rem;
    border: 2px solid var(--warn);
    border-radius: 999px;
    font-size: 0.8rem;
    font-weight: 700;
    white-space: nowrap;
}

.journal-entry-failed {
    border-color: var(--danger);
}

.journal-entry-amount {
    font-size: 1.1rem;
    font-weight: 700;
    white-space: nowrap;
}

.journal-items {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    font-size: 0.9rem;
}

.journal-item {
    display: flex;
    gap: 0.5rem;
}

.journal-item-amount {
    font-weight: 600;
}

/* Article retiré (S3.6) : gardé à l'écran, barré et grisé, comme dans le panneau d'ardoise.
   Le barré ne porte que sur l'article et son montant : la mention « retiré » reste lisible. */
.journal-item-removed {
    color: var(--muted);
}

    .journal-item-removed .journal-item-label,
    .journal-item-removed .journal-item-amount {
        text-decoration: line-through;
    }

.journal-item-tag {
    font-size: 0.8rem;
}

/* ----- Clôture (S3.12) ----- */
/* L'ÉTAT DE LA JOURNÉE, ARRIVÉ ICI LE 06/10/2026 depuis la ligne de l'en-tête de la caisse : « Caisse ouverte
   depuis 18:42 » puis « par Sarah » en dessous. MÊME TEXTE (les deux mêmes formateurs, CaisseFormat.OpenedSince
   et OpenedByLine) et MÊME RENDU qu'avant — 17 px/800 et sa sous-ligne 12,5 px/600 en --muted (4,85:1 sur le
   fond du verre, 5,2 à 5,5:1 ailleurs) : c'est un déménagement, pas une réécriture, et il doit se reconnaître.
   Les 17 px ne concurrencent pas le titre de la modale, qui est à 24 px/700 (.caisse-sheet-title).
   Pourquoi dans cette modale et pas dans une autre : l'heure d'ouverture et le nom de celui qui a ouvert ne
   servent à aucune vente, ils servent à fermer — « depuis quand compte-t-on ? qui a mis le fond ? ». */
.closing-opened {
    margin: 0 0 0.75rem;
    font-size: 1.0625rem;
    font-weight: 800;
    letter-spacing: -0.01em;
    line-height: 1.2;
}

    .closing-opened-by {
        display: block;
        font-size: 0.78125rem;
        font-weight: 600;
        letter-spacing: 0;
        line-height: 1.2;
        color: var(--muted);
    }

/* Récapitulatif à gauche, pavé à droite : tout tient sur une tablette de 10 pouces en paysage. */
.closing-body {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

.closing-recap {
    flex: 1 1 18rem;
    min-width: 16rem;
}

.closing-pad {
    flex: 0 1 22rem;
}

.closing-summary {
    margin-bottom: 0.75rem;
}

    .closing-summary > div {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 0.5rem;
    }

    .closing-summary dt {
        font-size: 1.1rem;
        color: var(--muted);
    }

    .closing-summary dd {
        margin: 0;
        font-size: 1.25rem;
        font-weight: 700;
    }

/* Solde théorique : le chiffre que le bénévole compare à sa caisse. */
.closing-summary dd.closing-expected {
    font-size: 2rem;
    line-height: 1.1;
}

.closing-details {
    list-style: none;
    padding: 0;
    margin-bottom: 0.75rem;
    color: var(--muted);
}

.closing-warning {
    font-weight: 700;
    color: var(--warn);
    background-color: var(--panel-bg);
    border-radius: 0.25rem;
    padding: 0.15rem 0.35rem;
}

.closing-difference {
    margin: 0.5rem 0 0;
    font-size: 1.25rem;
    font-weight: 600;
}

.closing-difference-strong {
    font-size: 1.75rem;
    font-weight: 700;
    color: var(--danger);
}

.numeric-pad-extra {
    margin-top: 0.25rem;
}

/* ----- Comptage du tiroir par coupures (story 12 § 6), ouverture et clôture ----- */
/* Contrainte de la vague, vérifiée à 1180 × 820 et à 1024 × 768 : rien ne défile. D'où deux blocs côte à côte —
   Billets (5 lignes) et Pièces (6 lignes) —, soit six rangées au plus et jamais onze, et une seule rangée sous eux
   pour le total, la bascule et la validation. Aucune hauteur en dur : les cibles viennent de --touch-target. */
.cash-count-modes {
    /* Largeur utile d'une feuille de caisse : les deux blocs s'y posent côte à côte à 1024 px comme à 1180 px. */
    width: var(--sheet-width);
    /* L'écran « Caisse fermée » centre son texte ; les onze lignes, elles, se lisent alignées à gauche,
       comme le pavé numérique qui s'échappe déjà du centrage. */
    text-align: left;
}

/* « Retour » efface onze lignes : il se tient à l'écart de la bascule et ne porte pas son trait. */
.cash-count-cancel {
    margin-left: 0.75rem;
    text-decoration: none;
}

.cash-count-blocks {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1rem;
}

.cash-count-block {
    flex: 1 1 14rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.cash-count-block-title {
    font-size: 1rem;
    font-weight: 600;
    color: var(--muted);
    margin: 0 0 0.25rem;
}

/* Une ligne : le libellé de la coupure à gauche, le champ à droite. Le champ compte de l'argent, il garde donc le
   plancher tactile entier ; c'est lui qui fixe la hauteur de la rangée. */
.cash-count-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    min-height: var(--touch-target);
    margin: 0;
}

.cash-count-coin {
    flex: 1 1 auto;
    font-size: 1.15rem;
    font-weight: 600;
}

.cash-count-field {
    flex: 0 0 auto;
    width: 5rem;
    min-height: var(--touch-target);
    text-align: right;
    font-size: 1.25rem;
    font-weight: 700;
}

/* Collage illisible : la ligne le dit, le total passe à « — » et la validation s'éteint. Rare, et c'est la seule
   chose qui rallonge une rangée — la marge de hauteur des deux écrans l'absorbe. */
.cash-count-invalid {
    flex: 1 1 100%;
    font-size: 0.8rem;
    color: var(--danger);
}

.cash-count-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.5rem;
}

.cash-count-total {
    flex: 1 1 auto;
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    margin: 0;
}

.cash-count-total-label {
    color: var(--muted);
}

/* Le chiffre qu'on relit pendant qu'on empile : de la taille du solde théorique, en face. */
.cash-count-total-value {
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.1;
}

/* La validation reste l'action de la rangée : même hauteur que les deux boutons en trait fin, plus large. */
.cash-count-validate {
    padding-inline: 1.5rem;
}

/* Clôture en mode coupures : le récapitulatif passe en une rangée au-dessus, les deux blocs prennent la largeur. */
.closing-body-coins {
    gap: 0.5rem;
}

.closing-body-coins .closing-recap,
.closing-coins {
    flex: 1 1 100%;
}

.closing-body-coins .closing-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 1.5rem;
    margin-bottom: 0.5rem;
}

    .closing-body-coins .closing-summary > div {
        justify-content: flex-start;
    }

/* Les détails de la journée se lisent en ligne : chaque rangée prise ici est une rangée de coupures perdue. */
.closing-body-coins .closing-details {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1.5rem;
    margin-bottom: 0.5rem;
}

.closing-body-coins .caisse-sheet-subtitle {
    margin-bottom: 0.25rem;
}

/* ----- Offert « Verre du Club » (design 08-offert 6.2, 6.3) ----- */
/* Quantité déjà retenue sur la tuile du panneau d'offert : posée dans le coin de la zone qui ajoute, au-dessus
   d'elle, elle ne lui prend aucune place. Aux couleurs du thème, sur la couleur de la catégorie. */
.tile-gift-count {
    position: absolute;
    top: 0.25rem;
    right: 0.25rem;
    z-index: 1;
    min-width: 2.25rem;
    padding: 0 0.4rem;
    border-radius: 999px;
    background-color: var(--panel-bg);
    color: var(--fg);
    font-size: 0.95rem;
    font-weight: 700;
    line-height: 1.6;
}

/* Période libre du journal « Verres du club » : deux jours, côte à côte, sur une seule ligne à 1024 px. */
.gift-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
    margin-bottom: 1rem;
}

.gift-date {
    max-width: 12rem;
    min-height: 44px;
}

/* Le journal des offerts porte la date entière, là où le journal de caisse n'a que l'heure. */
.gift-entry-time {
    flex: 0 0 9.5rem;
}

/* Pied du journal : « 12 articles offerts — valeur 123,50 € ». Une information, jamais une écriture. */
.gift-journal-total {
    margin: 0.75rem 0 0.25rem;
    font-size: 1.5rem;
    font-weight: 700;
}

/* Un offert porte plusieurs produits : une ligne chacun, sous la même heure et le même motif. On annule
   l'offert entier (G11), jamais une ligne : le bouton est au niveau de l'offert, à droite. */
.gift-line {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.gift-line-label, .gift-line-amount {
    font-weight: 600;
}

.gift-line-unit {
    color: var(--muted);
}

/* Offert annulé (invariant 41) : gardé à l'écran, barré et grisé, comme un article retiré (précédent M9).
   Le barré ne porte que sur les produits et leurs montants : « annulé par Julie à 20:31 » reste lisible. */
.gift-entry-cancelled {
    color: var(--muted);
}

    .gift-entry-cancelled .gift-line-label,
    .gift-entry-cancelled .gift-line-unit,
    .gift-entry-cancelled .gift-line-amount {
        text-decoration: line-through;
    }

/* « Annuler l'offert » : au niveau de l'offert, à droite, centré sur ses lignes. */
.gift-entry-action {
    flex: 0 0 auto;
    align-self: center;
}

.gift-cancelled-tag {
    padding: 0 0.4rem;
    border: 2px solid var(--border);
    border-radius: 999px;
    font-size: 0.8rem;
    font-weight: 700;
    white-space: nowrap;
}

/* ----- Compta (S7.2 à S7.11, design 07-compta 8) : soldes en tête, liste du mois, rapport imprimable ----- */
/* Les deux soldes côte à côte : c'est la réponse qu'on vient chercher avant d'encoder quoi que ce soit. */
.compta-balances {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    margin-bottom: 1rem;
}

.compta-balance {
    display: flex;
    flex-direction: column;
    flex: 1 1 15rem;
    max-width: 24rem;
    padding: 0.6rem 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--panel-bg);
}

.compta-balance-label {
    font-size: 0.85rem;
    color: var(--muted);
}

.compta-balance-value {
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.1;
}

.compta-balance-note {
    font-size: 0.85rem;
    color: var(--muted);
}

.compta-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
    margin-bottom: 1rem;
}

/* « Mois clôturé », « Clôturé » : une pastille, jamais une couleur de texte. */
.compta-badge {
    display: inline-block;
    padding: 0.1rem 0.6rem;
    border-radius: 999px;
    background-color: var(--warn);
    color: var(--bg);
    font-size: 0.8rem;
    font-weight: 700;
    vertical-align: middle;
}

.compta-day {
    margin-bottom: 1.25rem;
}

.compta-day-title {
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--muted);
    margin-bottom: 0.35rem;
}

.compta-lines {
    list-style: none;
    padding: 0;
    margin: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background-color: var(--panel-bg);
}

.compta-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem 0.75rem;
    min-height: 56px;
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--border);
}

    .compta-line:last-child {
        border-bottom: 0;
    }

.compta-line-date {
    min-width: 3.2rem;
    font-size: 0.9rem;
    color: var(--muted);
}

.compta-line-main {
    display: flex;
    flex-direction: column;
    flex: 1 1 16rem;
    min-width: 12rem;
}

.compta-line-title {
    font-weight: 600;
}

.compta-line-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
    font-size: 0.85rem;
    color: var(--muted);
}

.compta-line-author, .compta-line-note {
    font-size: 0.85rem;
    color: var(--muted);
}

.compta-line-amounts {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    min-width: 9rem;
    margin-left: auto;
}

.compta-line-account {
    font-size: 0.85rem;
    color: var(--muted);
}

.compta-line-amount {
    font-size: 1.35rem;
    font-weight: 700;
    white-space: nowrap;
}

/* Les deux comptes d'un dépôt, l'un sous l'autre : une seule opération, jamais deux lignes de liste. */
.compta-line-leg {
    font-size: 1.05rem;
    font-weight: 600;
    white-space: nowrap;
}

.compta-line-action {
    flex: 0 0 auto;
}

/* Virements à valider (S13.2) : la ligne reprend celle d'une écriture, à deux détails près.
   La date y porte le jour ET l'heure d'encodage (« 29/09/2026 19:40 ») au lieu du seul jour d'une écriture :
   elle demande 8 rem plutôt que 3,2, faute de quoi elle se casse en deux lignes et la ligne grandit de 20 px. */
.compta-transfer-date {
    min-width: 8rem;
}

/* Les deux boutons de la ligne, côte à côte et à hauteur de cible tactile (btn-touch les porte déjà à 44 px) :
   ils restent groupés à droite même quand la ligne se replie sur un écran étroit. */
.compta-transfer-actions {
    display: flex;
    flex: 0 0 auto;
    gap: 0.5rem;
}

/* Écriture annulée et son annulation : gardées à l'écran, atténuées, hors de tous les totaux. */
.compta-line-off {
    opacity: 0.8;
}

.compta-line-cancelled .compta-line-title {
    text-decoration: line-through;
}

.compta-tag {
    padding: 0.05rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: 0.8rem;
    white-space: nowrap;
}

.compta-tag-off {
    border-color: var(--danger);
    color: var(--danger);
}

/* Modales de saisie : les champs à gauche, le pavé à droite, comme la clôture de caisse. */
.compta-form {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

.compta-form-fields {
    flex: 1 1 18rem;
    min-width: 16rem;
}

.compta-form-pad {
    flex: 0 1 22rem;
}

/* ----- Rapport mensuel (S7.8, S7.9) ----- */
.compta-report {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 1rem;
}

.compta-report-block {
    flex: 1 1 22rem;
    min-width: 19rem;
    padding: 0.75rem 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--panel-bg);
}

.compta-report-title {
    font-size: 1.25rem;
    font-weight: 700;
    margin-bottom: 0.25rem;
}

.compta-report-subtitle {
    font-size: 1rem;
    font-weight: 700;
    color: var(--muted);
    margin: 0.75rem 0 0.25rem;
}

.compta-report-lines {
    margin: 0;
}

    .compta-report-lines > div {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 0.75rem;
        padding: 0.15rem 0;
    }

    .compta-report-lines dt {
        font-weight: 400;
    }

    .compta-report-lines dd {
        margin: 0;
        font-weight: 600;
        white-space: nowrap;
    }

.compta-report-total {
    margin-top: 0.25rem;
    padding-top: 0.35rem;
    border-top: 1px solid var(--border);
}

    .compta-report-total dt, .compta-report-total dd {
        font-weight: 700;
    }

.compta-report-closing {
    margin-top: 0.75rem;
}

/* Écart de réconciliation : la couleur remplit une pastille, jamais le texte (ui-mockup § 2, comme le journal).
   Le montant signé et la phrase portent le sens : l'écart reste lisible imprimé en noir et blanc. */
.compta-gap {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    margin: 0.75rem 0 0.25rem;
    font-size: 1.25rem;
    font-weight: 700;
}

.compta-gap-dot {
    align-self: center;
    width: 0.7rem;
    height: 0.7rem;
    border-radius: 999px;
}

.compta-gap-ok {
    background-color: var(--ok);
}

.compta-gap-off {
    background-color: var(--primary-bg);
}

.compta-gap-value {
    margin-left: auto;
    white-space: nowrap;
}

.compta-gap-message {
    margin: 0;
}

/* ----- Verrou de poste et badge d'opérateur (S8.3, S8.4, design 07-verrou 4) ----- */
/* Voile opaque au fond du thème, au-dessus de tout — rail compris, contrairement aux modales de la caisse
   qui commencent après lui. L'écran reste dessous, intact : il est recouvert, jamais détruit.
   Seule exception à « au-dessus de tout » : le bandeau de staging (0 en prod). Un poste verrouillé est ce qu'on
   trouve en arrivant devant la tablette ; c'est le moment où il faut le plus savoir laquelle des deux on a. */
.operator-lock {
    position: fixed;
    inset: var(--staging-banner-h) 0 0 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
    overflow-y: auto;
    overscroll-behavior: contain;
    touch-action: manipulation;
    /* Voile « Qui es-tu ? ». Opaque par défaut (le fond du thème) ; le thème verre le descend à 92 % et le
       floute — 4/5 des surfaces qui floutent (vague 16, point 3).
       ÉCART ASSUMÉ AVEC LA MAQUETTE DES JETONS, qui écrit « fond uni à 96 %, plus de flou coûteux sur
       iPad » : elle décrit la variante « Moderne », pas celle-ci, et le point 3 de la vague liste ce voile
       parmi les cinq surfaces qui floutent. 92 % plutôt que 96 : à 96 % il n'y aurait plus rien à flouter,
       et on paierait un flou invisible. Si l'iPad accroche, c'est la première ligne à retirer. */
    background-color: var(--lock-bg, var(--bg));
    backdrop-filter: var(--veil-blur, none);
    -webkit-backdrop-filter: var(--veil-blur, none);
}

.operator-lock-panel {
    width: min(24rem, 100%);
    text-align: center;
}

.operator-lock-logo {
    display: block;
    margin: 0 auto 0.5rem;
}

.operator-lock-title {
    margin: 0;
    font-size: 2rem;
    font-weight: 700;
}

.operator-lock-hint {
    margin: 0.25rem 0 1rem;
    color: var(--muted);
}

/* Barre du badge : en haut à droite de l'écran, dans le flux — sur la caisse elle est le premier élément de
   .caisse-shell, donc elle prend sa place sans jamais recouvrir « Clôturer la caisse ». */
/* Mise en service : aucun code défini, le poste ne se verrouille pas et le dit. */
.operator-warning {
    padding: 0.25rem 0.6rem;
    border-radius: var(--radius);
    background-color: var(--warn);
    color: var(--bg);
    font-weight: 600;
}

/* La barre reste exactement ce qu'elle était sur TOUTES les pages de MainLayout et sur l'écran caisse FERMÉE.
   Elle ne se pose plus au-dessus de la ligne de la caisse quand une journée est ouverte : là, c'est la pastille
   seule qui descend DANS cette ligne (.operator-badge-inline), et les 42 px de cette barre — 4 de marge + 36 de
   bouton + 2 de filets — passent aux tuiles. */
.operator-bar {
    flex: 0 0 auto;   /* dans la coquille de la caisse (colonne flex) : sa hauteur est prise sur le corps */
    display: flex;
    justify-content: flex-end;
    /* Sur la caisse, chaque pixel de cette ligne est pris aux tuiles : on la serre au plus court. */
    padding: 0.25rem 0.75rem 0;
}

/* Badge discret : une information, pas un bouton de première importance. */
.operator-badge {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    max-width: 100%;
    /* Rembourrage de la variante par DÉFAUT, celle qui garde son bouton « Changer d'opérateur » : c'est lui
       qui donne la hauteur, et les 0,25 rem de droite sont ce qui reste entre son bord et celui de la
       pastille. La variante EN LIGNE n'a plus ce bouton depuis le 06/10/2026 et rééquilibre donc sa droite
       (voir .operator-badge-inline) : sans ça, la pastille montrerait 12 px à gauche et 4 à droite. */
    padding: 0 0.25rem 0 0.75rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    background-color: var(--panel-bg);
    color: var(--fg);
}

.operator-badge-name {
    overflow: hidden;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Hauteur des boutons de barre (36 px depuis le 23/09/2026) : sur la caisse, cette ligne est prise sur les
   tuiles, et « Changer d'opérateur » est un relais, pas une écriture. Il double « Pause », à portée de pouce
   dans la ligne de travail. */
.operator-badge-change {
    display: inline-flex;
    align-items: center;
    min-height: var(--action-height);
    padding: 0 0.9rem;
    border: 1px solid var(--btn-border);
    border-radius: 999px;
    background-color: var(--btn-bg);
    color: var(--fg);
    font-size: 0.875rem;
    white-space: nowrap;
}

/* ----- VARIANTE EN LIGNE (06/10/2026) : la pastille de l'opérateur POSÉE DANS LA LIGNE DE L'EN-TÊTE de la
   caisse, à la place de l'état de la journée. Elle n'existe que là ; la barre d'au-dessus garde partout
   ailleurs son rendu au pixel près.
   TROIS CONTRAINTES, ET TOUTES LES TROIS SONT DES CHIFFRES :
   1. 36 PX DE HAUT, FILETS COMPRIS. La ligne vaut 4 + 36 + 4 + 1 = 45 px et c'est son bouton le plus haut qui
      la tient : une pastille de 38 px (34 de bouton + 2 de filets, ce que donnerait la hauteur libre) ferait
      passer la ligne à 47 et prendrait deux pixels aux tuiles. D'où une hauteur ÉCRITE : 34 de bouton peint
      + 2 de filets = --action-height, exactement.
   2. CIBLE TACTILE DE 44 PX sur un carré peint de 34, par la mécanique déjà en place sur les « − » et « + »
      d'une ligne de vente (--touch-inner / --touch-target, 06/10/2026) — à ceci près qu'ici c'est le
      pseudo-élément qui DÉBORDE au lieu que la bordure rentre : il n'y a que 36 px de ligne, et la cible va
      donc chercher ses 4 px manquants dans le rembourrage de l'en-tête, au-dessus et en dessous. Aucun pixel
      en dur : l'inset vaut (34 − 44) / 2 = −5 px, calculé des deux jetons.
   3. ELLE CÈDE AVANT LES LIBELLÉS. flex: 0 1 auto et min-width: 0 : c'est le NOM qui se tronque (il a déjà son
      ellipse), après que l'espace central s'est refermé — jamais « Pause », « Ardoises » ou « Comptoir », qui
      sont en flex: none dans un groupe en flex: 0 0 auto.
   LARGEUR, au cas le plus chargé — et c'est le cas NORMAL depuis que la pastille « Comptoir » est permanente,
   06/10/2026 — (sept actions, 570,6 px, inchangé : voir .caisse-action-label) : 2 (filets) + 12
   (rembourrage gauche) + le nom + 8 (écart) + 34 (bouton) + 4 (rembourrage droit) = 60 px + le nom. Avec
   « Sarah Vandenberghe » (160,4 px en Manrope 16/600, mesuré sur le fichier de police du dépôt), la pastille
   fait 220,4 px et la ligne entière 24 + 220,4 + 12 + 570,6 = 827 px. Il reste 141 px d'espace central à
   1024 px de large (968 moins le rail) et 297 px à 1180 : la ligne tient aux deux résolutions sans rien
   tronquer. Le nom ne commence à se tronquer qu'au-delà de 301,4 px à 1024 px, soit une trentaine de
   caractères (« Jean-Christophe Vanderhaeghen » en demande 254,3). ----- */
/* La pastille de l'opérateur dans la ligne de l'en-tête. DEPUIS LE 06/10/2026 ELLE NE CONTIENT PLUS AUCUN
   BOUTON : son « Changer d'opérateur » faisait doublon avec « Pause », deux pastilles plus loin dans la
   même ligne et menant au même voile. Ce qui reste est un libellé — silhouette, nom — donc :
   aucun état de survol, aucune ombre d'appui, aucun retour au toucher, et le CURSEUR PAR DÉFAUT, parce
   qu'un texte qui se montre cliquable sans l'être est pire qu'un texte muet. Il n'y a plus de cible de
   44 px à ménager ici : la règle de la cible et son débordement de 5 px sont partis avec le bouton.
   Sa hauteur reste celle des boutons de la barre (36 px, filets compris) : deux pixels de plus et la ligne
   passerait de 45 à 47, c'est-à-dire deux pixels pris aux tuiles. */
.operator-badge-inline {
    flex: 0 1 auto;
    min-width: 0;
    height: var(--action-height);
    cursor: default;
    /* La droite retrouve la gauche : le bouton qui occupait ce bord est parti, et 4 px y laisseraient le
       nom collé au trait. 8 px de plus sur la ligne, pour 141 px de libre à 1024 × 768. */
    padding-right: 0.75rem;
}

/* ----- Journal technique (design 09-journal-technique § 4.1) : liste dense, lecture seule, une entrée par ligne
   et son exception en retrait dessous. Aucune couleur en dur : le niveau prend --warn ou --danger sur son filet. */
.applog-entries {
    list-style: none;
    padding: 0;
    margin: 0;
    border-top: 1px solid var(--border);
}

.applog-entry {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.5rem 0.25rem;
    border-bottom: 1px solid var(--border);
}

.applog-time {
    flex: 0 0 auto;
    /* La date s'ajoute à l'heure dès que l'entrée n'est pas du jour : la colonne est dimensionnée pour les deux. */
    min-width: 4.75rem;
    font-weight: 700;
    white-space: nowrap;
}

/* Étiquette de niveau : la couleur du thème remplit le filet, le mot reste en --fg — même parade que l'écart
   du journal de caisse, où --danger et --warn en texte tombent sous 4,5:1 sur certains thèmes. Le mot écrit
   (INFO, AVERT., ERREUR, CRITIQUE) suffit à distinguer les niveaux sans voir la couleur. */
.applog-level {
    flex: 0 0 auto;
    min-width: 5.5rem;
    padding: 0 0.4rem;
    border: 2px solid var(--border);
    border-radius: 999px;
    font-size: 0.8rem;
    font-weight: 700;
    text-align: center;
    white-space: nowrap;
}

.applog-level-info {
    border-color: var(--border);
}

.applog-level-warning {
    border-color: var(--warn);
}

.applog-level-error {
    border-color: var(--danger);
}

.applog-category {
    flex: 0 0 9rem;
    min-width: 0;
    color: var(--muted);
    font-weight: 600;
    overflow-wrap: anywhere;
}

/* Un message peut faire 2 000 caractères, sans la moindre espace (une URL, une réponse JSON) : « min-width: 0 »
   lève la largeur minimale d'une colonne flex et « overflow-wrap » coupe le mot, donc la page ne part jamais
   sur le côté. */
.applog-body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    overflow-wrap: anywhere;
}

.applog-exception {
    padding-left: 1rem;
    font-size: 0.85rem;
    color: var(--muted);
}

/* ----- Personnes : membre du club et import des affiliés (S14.1, S14.3) ----- */
/* Badge « Membre » : sur la ligne de la page Personnes et sur la tuile du choix d'une personne à la caisse.
   Il porte la couleur d'action du thème — c'est le badge du club, pas un avertissement — et il est la réponse
   au « pourquoi je ne peux pas renommer Julie » quand les boutons réservés sont absents. Même forme que
   .compta-badge : aucune dimension nouvelle, rien qu'en rem. */
.member-badge {
    display: inline-block;
    padding: 0.1rem 0.6rem;
    border-radius: 999px;
    background-color: var(--primary-bg);
    color: var(--primary-fg);
    font-size: 0.8rem;
    font-weight: 700;
    vertical-align: middle;
}

/* Bascule club ⇄ extérieur sur la fiche : un bloc à part, parce qu'elle s'enregistre seule, sans
   « Enregistrer ». Sa cible tactile vient de .form-switch-touch (--touch-target). */
.member-club-switch {
    padding: 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* Compteurs du rapport d'import : les quatre chiffres restent au-dessus du rapport et toujours à l'écran. */
.import-counters {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 0.5rem;
}

.import-counter {
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    text-align: center;
}

.import-counter-value {
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.1;
}

/* Le rapport d'un fichier de 57 lignes est long : il défile DANS son cadre et ne pousse jamais la page. Les
   compteurs, « Analyser le fichier » et « Importer » restent donc visibles ensemble sur 1180 × 820 comme sur
   1024 × 768. La hauteur est une fraction de la fenêtre (aucune valeur en pixels) : elle suit les deux
   résolutions et les quatre thèmes sans être recalculée. */
.import-report {
    max-height: 38vh;
    overflow-y: auto;
    overscroll-behavior: contain;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* ----- Section « Réapprovisionnement » de la fiche produit (S17.1) -----
   Les quatre réglages du bon de commande sur UNE rangée. Le compte, parce qu'il a décidé de la forme : empilés en
   quatre blocs avec une aide chacun, ils pesaient 558 px — sur l'écran que le comité ouvre le plus souvent pour y
   changer un PRIX, et pour des champs qu'on remplit une fois par produit. En rangée, avec une seule aide, la
   section tombe à 212 px. La fiche d'un produit à la pièce passe ainsi de ~1 359 à ~1 013 px, dans une modale
   haute de ~756 px à 1180 × 820 et de ~704 px à 1024 × 768 : elle défile de ~257 px au lieu de ~603.
   Elle défilait déjà avant cette vague (~801 px, soit ~45 px de défilement à 820) : la rangée ne lui rend pas le
   confort qu'elle n'avait pas, elle lui évite d'en perdre un demi-écran.
   Une GRILLE et non un flex, pour que les quatre champs restent alignés même si un libellé passe sur deux lignes
   (align-items: end) : sans elle, « Taille du casier » replié décalerait son seul champ de 24 px.
   Les trois premières colonnes sont fixes (5,5 + 5,5 + 8,5 rem) et le fournisseur prend ce qui reste des 36 rem du
   formulaire, soit 228 px — cette largeur ne dépend ni de l'écran ni de la modale, les deux résolutions cibles
   donnent donc le même nombre. */
.reorder-fields {
    display: grid;
    grid-template-columns: 5.5rem 5.5rem 8.5rem minmax(0, 1fr);
    gap: 0.75rem;
    align-items: end;
}

/* ----- Bon de commande brasseur (S17.1, décision S125) -----
   Une liste à cinq colonnes alignées, plus la colonne du bouton « Retirer » qui ne s'imprime pas. Aucune couleur
   neuve : le trait, le texte, le gris secondaire et le rouge du reste négatif sont ceux de la page Stock, déjà
   mesurés dans themes.css. Aucun pixel en dur : les hauteurs de cible viennent de --touch-target, les largeurs
   sont en rem.
   Le gabarit des colonnes est écrit UNE fois, dans --order-cols, et lu par l'en-tête comme par les lignes : deux
   déclarations qui divergent décaleraient l'en-tête d'une liste d'un demi-caractère sans que personne ne voie
   pourquoi. Le nom prend ce qui reste (minmax(0, 1fr)) et se replie sur deux lignes plutôt que de rogner les
   chiffres : à 1024 px de large, un nom long y passe en deux lignes, la ligne grandit, les colonnes ne bougent
   pas. */
.order-list {
    /* Budget de largeur, mesuré à 1180 × 820 CSS (rail 56 + marges 40 ⇒ 1084 px utiles, 1022,8 px de colonnes
       une fois le cadre et les cinq écarts retirés) : 4 + 4 + 20 + 9 + 5 = 42 rem = 672 px fixes, donc 350,8 px
       pour le nom. À 1024 × 768 les mêmes 672 px laissent 194,8 px au nom, et un nom long y passe sur deux
       lignes. La quantité est la plus large des cinq parce que c'est la plus longue phrase de la page —
       « 3 casiers de 24 (72 bouteilles) » — et qu'elle partage sa cellule avec le champ de saisie. */
    --order-cols: minmax(0, 1fr) 4rem 4rem 20rem 9rem 5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--panel-bg);
    overflow: hidden;
}

.order-head, .order-row {
    display: grid;
    grid-template-columns: var(--order-cols);
    align-items: center;
    gap: 0.5rem;
    padding: 0.35rem 0.6rem;
}

/* Légende des colonnes : au second plan, elle nomme et ne pèse pas. */
.order-head {
    border-bottom: 1px solid var(--border);
    color: var(--muted);
    font-size: 0.85rem;
    font-weight: 600;
}

.order-row {
    border-bottom: 1px solid var(--border);
}

    .order-row:last-child {
        border-bottom: 0;
    }

.order-name {
    font-weight: 600;
    /* min-width: 0 est déjà donné par minmax(0, 1fr) ; le mot trop long casse plutôt qu'il ne déborde. */
    overflow-wrap: anywhere;
}

/* Reste et cible : les deux chiffres qu'on compare d'un coup d'œil, donc gros, comme sur la tuile de stock. */
.order-figure {
    font-size: 1.25rem;
    font-weight: 700;
}

.order-quantity {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
}

/* Le champ qui retouche la quantité : cible tactile pleine, et la largeur d'un nombre à trois chiffres — pas un
   pixel de plus, les 250 px qui restent dans la cellule sont pour la phrase. */
.order-quantity-input {
    width: 4rem;
    min-height: var(--touch-target);
}

.order-quantity-text {
    font-weight: 600;
}

.order-supplier {
    overflow-wrap: anywhere;
}

/* Ligne sans fournisseur : au second plan, mais jamais cachée — c'est le produit qu'on oublie. */
.order-no-supplier {
    color: var(--muted);
}

/* Filtre : la largeur d'un nom de fournisseur, pas celle de la page. */
.order-filter {
    max-width: 22rem;
}

/* Repli du presse-papiers : un cadre assez haut pour qu'on voie ce qu'on sélectionne à la main. */
.order-copy-fallback {
    min-height: 10rem;
}

/* Réservé au papier (voir le bloc d'impression) : l'écran sait où il est, la feuille non. */
.order-print-only {
    display: none;
}

/* ----- Administration > Utilisateurs (S129) : UNE LIGNE par compte, le détail dans une modale -----
   Le symptôme : un bloc complet par compte (champs, droits, mot de passe, code), illisible dès dix comptes.
   La liste devient un TABLEAU — quatre colonnes aux mêmes places sur toutes les lignes, d'où des largeurs
   fixes et non « auto » : avec « auto », la colonne des pastilles mesurerait son contenu ligne par ligne, les
   quatre pastilles se décaleraient d'un compte à l'autre et l'œil devrait relire chaque ligne au lieu de
   balayer une colonne. C'est le même parti que .order-list, et le même vocabulaire.
   BUDGET DE LARGEUR, mesuré à 1024 x 768 CSS, la plus petite des deux tablettes : 1024 - 56 de rail - 40 de
   marges = 928 px utiles ; la liste est bridée à 56rem = 896 px, dont 2 de cadre et 28,8 de rembourrage
   (2 x 0,9rem) = 865,2 px de colonnes. Les trois colonnes fixes valent 10 + 20,25 + 7 = 37,25rem = 596 px, les
   trois écarts 36 px : il reste 233,2 px au nom affiché, soit une trentaine de caractères avant que les points
   de suspension ne prennent la suite. AUCUN RETOUR À LA LIGNE, AUCUN ASCENSEUR : ce sont les deux noms qui se
   tronquent, jamais les pastilles ni l'état, parce qu'une pastille à moitié lue ne dit plus rien.
   DU JEU DANS LES DEUX PISTES FIXES, et c'est volontaire : les quatre pastilles demandent 19,75rem (4 x 4,75 +
   3 x 0,25) et l'état 6,75 (5,5 + 0,25 + 1 de clé), donc les pistes de 20,25 et 7 leur laissent 0,5 et 0,25rem
   de marge. Sans ce jeu, elles seraient justes AU PIXEL : une police qui rend « Réglages » quelques pixels plus
   large qu'estimé ferait grandir les pastilles — « flex: 0 0 auto », donc elles ne se replient pas — et elles
   DÉBORDERAIENT sur la pastille d'état, sans ascenseur mais en chevauchement. Les 12 px ainsi dépensés sont pris
   au nom affiché, qui sait se tronquer ; et la liste garde 32 px de reste sur les 928 utiles. */
.user-list {
    --user-cols: 10rem minmax(0, 1fr) 20.25rem 7rem;
    max-width: 56rem;
    list-style: none;
    padding: 0;
    margin: 0 0 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background-color: var(--panel-bg);
    overflow: hidden;
}

/* La ligne est un BOUTON : tout le rectangle ouvre la modale, et non un lien ou une zone à viser. 56 px de
   haut (au-dessus de --touch-target) parce qu'on la touche parfois d'un pouce, en tenant la tablette. */
.user-row {
    display: grid;
    grid-template-columns: var(--user-cols);
    align-items: center;
    gap: 0.75rem;
    width: 100%;
    min-height: 56px;
    padding: 0.5rem 0.9rem;
    border: 0;
    border-bottom: 1px solid var(--border);
    background-color: transparent;
    color: var(--fg);
    text-align: left;
}

    /* Le trait tombe sur la DERNIÈRE LIGNE DE LA LISTE, et le sélecteur doit donc viser le <li> : le bouton est
       fils unique du sien, donc « .user-row:last-child » matcherait toutes les lignes et la liste n'aurait plus
       aucun séparateur. Défaut trouvé en relecture, le 07/10/2026. */
    .user-list li:last-child .user-row {
        border-bottom: 0;
    }

    /* Le halo de focus se dessine À L'INTÉRIEUR de la ligne (décalage négatif) : la liste masque ce qui dépasse
       d'elle (« overflow: hidden », pour que ses coins arrondis coupent la première et la dernière ligne), et un
       halo posé 2 px en dehors serait rogné sur les quatre bords. Le clavier doit voir où il est. */
    .user-row:focus-visible {
        outline: 3px solid var(--focus);
        outline-offset: -3px;
    }

/* Les deux noms : ils se tronquent, et c'est eux qui rendent la place quand l'écran se resserre. Le nom
   d'utilisateur est celui qu'on tape pour se connecter, le nom affiché celui qu'on cherche du regard. */
.user-row-login, .user-row-name {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.user-row-login {
    font-weight: 600;
}

.user-row-name {
    color: var(--muted);
}

.user-pills, .user-state {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

/* Les quatre droits, toujours les quatre, toujours dans le même ordre : pleine quand le compte l'a, en creux
   quand il ne l'a pas. MÊME LARGEUR pour les quatre (4,75rem tient « Réglages », la plus longue), donc les
   colonnes s'alignent d'une ligne à l'autre sans tableau HTML. Le TRAIT TRANSPARENT de la pastille pleine est
   ce qui garde les deux états exactement à la même taille : sans lui, allumer un droit déplacerait les trois
   suivantes de deux pixels. Même forme que .compta-badge et .member-badge : rien qu'en rem. */
.user-pill {
    flex: 0 0 auto;
    min-width: 4.75rem;
    padding: 0.1rem 0.4rem;
    border: 1px solid transparent;
    border-radius: 999px;
    background-color: var(--fg);
    color: var(--panel-bg);
    font-size: 0.8rem;
    font-weight: 700;
    text-align: center;
    white-space: nowrap;
}

/* En creux : le mot reste LISIBLE (--muted, 4,85:1 au moins sur les quatre thèmes) au lieu d'être effacé.
   Un droit absent n'est pas une information de second plan — c'est la moitié de ce que la ligne raconte. */
.user-pill-off {
    border-color: var(--border);
    background-color: transparent;
    color: var(--muted);
}

.user-state-pill {
    flex: 0 0 auto;
    min-width: 5.5rem;
    padding: 0.1rem 0.4rem;
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: 0.8rem;
    font-weight: 700;
    text-align: center;
    white-space: nowrap;
    color: var(--muted);
}

/* Désactivé : la pastille se remplit, parce que c'est l'état qui doit sauter aux yeux dans une liste où il est
   rare. Fond --muted et texte --panel-bg, comme .caisse-badge inverse le contraste pour compter les ardoises. */
.user-state-off {
    background-color: var(--muted);
    color: var(--panel-bg);
}

/* Mot de passe encore temporaire : une clé de 16 px en couleur d'avertissement, à côté de l'état. Un
   pictogramme et non un mot, parce que la ligne n'a plus un caractère à donner ; ce qu'il veut dire est dans
   son title, dans le récapitulatif vocal de la ligne (UserFormat.RowLabel) et en clair dans la modale. */
.user-flag {
    flex: 0 0 auto;
    width: 1rem;
    height: 1rem;
    color: var(--warn);
}

/* ----- Impression du rapport mensuel (S7.8) et du bon de commande (S17.1) -----
   Le papier n'est pas un thème : le blanc et le noir ne viennent donc pas de themes.css, exactement comme les
   voiles des modales (ui-mockup § 8). Sans eux, le thème « ardoise » imprimerait un texte clair sur une page
   blanche. Ni rail, ni boutons, ni filtres : il reste le titre, les deux blocs, les totaux et l'écart. */
@media print {
    /* Le bandeau de staging ne s'imprime pas : sur le papier, il n'aurait plus rien à signaler. */
    .rail, .btn, .btn-back, .compta-no-print, .order-no-print, .caisse-overlay, .operator-bar, .operator-lock,
    #blazor-error-ui, .staging-banner {
        display: none !important;
    }

    body, .page, .content {
        background-color: #ffffff;
        color: #000000;
    }

    .page {
        margin-left: 0;
        min-height: 0;
    }

    .content {
        padding: 0;
    }

    /* Tout sur une page : les deux blocs se suivent, chacun d'un seul tenant. */
    .compta-report {
        display: block;
    }

    .compta-report-block {
        min-width: 0;
        margin-bottom: 0.5rem;
        padding: 0.4rem 0.6rem;
        border-color: #000000;
        background-color: #ffffff;
        break-inside: avoid;
    }

    /* « !important » parce que Bootstrap en met un sur .text-muted (réécrit plus haut). */
    .compta-report-title, .compta-report-subtitle, .compta-report-lines dt, .compta-report-lines dd,
    .compta-gap, .compta-gap-message, .compta-badge, .text-muted {
        color: #000000 !important;
    }

    .compta-badge {
        background-color: #ffffff;
        border: 1px solid #000000;
    }

    .compta-gap-dot {
        border: 1px solid #000000;
    }

    /* Bon de commande sur papier : il ne reste que le titre et la liste affichée. Le filtre, les quatre boutons,
       la mention de la dernière commande, le cadre de repli du presse-papiers et le champ qui retouche la
       quantité portent .order-no-print et tombent avec le rail — le nombre imprimé est alors celui du texte de
       StockFormat, qui suit la valeur tapée.
       Le gabarit perd la colonne du bouton (il n'en reste aucun) et resserre la quantité : une feuille A4 en
       portrait est plus étroite que l'iPad. */
    .order-list {
        --order-cols: minmax(0, 1fr) 3rem 3rem 15rem 7rem;
        border-color: #000000;
        background-color: #ffffff;
    }

    .order-head, .order-row {
        border-color: #000000;
        /* Une ligne de commande ne se coupe jamais entre deux pages : on la lit, ou on ne la lit pas. */
        break-inside: avoid;
    }

    /* L'en-tête du bon (« Commande TT Spontin — 06/10/2026 ») n'apparaît que là, et jamais à l'écran. */
    .order-print-only {
        display: block;
        margin-bottom: 0.4rem;
        font-weight: 700;
    }

    /* « !important » pour les mêmes raisons que plus haut : Bootstrap et les thèmes posent ces couleurs. */
    .order-name, .order-figure, .order-quantity-text, .order-supplier, .order-no-supplier, .order-head,
    .order-print-only {
        color: #000000 !important;
    }
}
