/* Apogée — feuille de style unique, sans dépendance ni build.

   Palette « vert bouteille & craie » : la couleur de la cave plutôt que celle
   du vin. Le fond est une craie très pâle, l'encre un vert presque noir, et
   l'accent le vert profond du flacon. Ce choix a une conséquence à connaître
   avant de toucher aux jetons : l'accent occupe la case « vert », donc la
   confiance haute ne peut plus l'être. Elle est rendue en encre avec une
   pastille d'accent, et seuls le doute et l'échec prennent une couleur.
   « Tout va bien » est l'absence d'alerte.

   Typographie système, l'application devant fonctionner sans accès réseau. Le
   serif des titres est un substitut : pour la voix exacte des maquettes, déposer
   un .woff2 dans static/fonts/ et l'ajouter en tête de --police-titre. Un
   fichier servi par l'application n'est pas une ressource externe ; un CDN si.

   La même palette existe en sombre, plus bas, sous [data-theme='sombre']. Le
   tampon est posé par static/js/theme.js avant le premier rendu : la feuille
   n'interroge donc pas `prefers-color-scheme`, et les jetons sombres ne sont
   écrits qu'une fois. Suivre le système en CSS *et* accepter un choix explicite
   demanderait trois sélecteurs et deux copies de la palette.

   Ces deux blocs-ci sont les palettes **par défaut**, celles d'un compte qui n'a
   rien choisi. Quatre autres suivent, sous [data-palette-claire] et
   [data-palette-sombre] : voir leur en-tête, plus bas, pour ce qui les sépare et
   pourquoi aucun accent n'y est rouge.

   Règle à ne pas défaire : AUCUNE
   couleur ne se déclare ailleurs que dans un jeton. Une couleur écrite en dur
   dans un composant ne bascule pas, et le défaut ne se voit que dans l'autre
   thème — le pied de fiche restait blanc sur fond sombre, illisible, alors que
   tout le reste avait basculé. Les six blocs de jetons se répondent ligne à
   ligne, et un test le vérifie. */

:root {
  /* Les contrôles natifs — sélecteurs, molettes, curseur de champ — suivent ce
     mot-clé et rien d'autre. Il est déclaré avec les jetons, dans chaque thème :
     posé une fois en « light dark », il rendrait les listes déroulantes sombres
     alors que l'utilisateur a forcé le clair. */
  color-scheme: light;

  /* Fonds et surfaces */
  --craie: #f1f4ef;
  --carte: #ffffff;
  --champ: #f6f8f4;

  /* Encres */
  --encre: #131c18;
  --doux: #4f5d56;
  --faible: #87938c;

  /* Filets */
  --trait: #dfe5dd;
  --trait-fort: #c6d0c5;

  /* Accent. « appui » est l'état pressé ou survolé : plus sombre en thème
     clair, plus clair en thème sombre — d'où un nom qui ne parle pas de
     luminosité. */
  --accent: #24614a;
  --accent-appui: #1b4b39;
  --accent-doux: #e3ede7;
  --sur-accent: #ffffff;

  /* Sémantique. Le vert manque volontairement : voir l'en-tête. */
  --ambre: #b5761a;
  --ambre-doux: #faf1e2;
  --alerte: #b0442f;
  --alerte-doux: #f9ebe7;
  --alerte-trait: rgba(176, 68, 47, 0.4);

  /* Fond posé sous une photo. Sombre dans les deux thèmes : une étiquette est
     claire, et c'est ce qui lui donne ses bords. */
  --fond-photo: #1d2622;
  /* Pied de fiche collant, translucide sur ce qui défile dessous. */
  --quai: rgba(255, 255, 255, 0.94);

  /* Les trois jetons qui ne rebasculent **pas** en thème sombre, et c'est leur
     raison d'être — ils décrivent des objets qui n'appartiennent pas à
     l'interface. Déclarés ici pour ne pas être des couleurs en dur, absents du
     bloc sombre pour ne pas mentir, et déclarés comme invariants dans
     `test_les_jetons_basculent_en_sombre`.

     Un QR est un objet **physique** : sa lisibilité vient du contraste entre un
     fond clair et un motif sombre, et l'inverser le rendrait illisible pour la
     moitié des lecteurs. Il n'a donc pas de mode sombre, sur l'écran comme sur le
     papier — et c'est sur le papier qu'il finit.

     Le voile, lui, assombrit ce qu'il y a derrière : un flux de caméra, pas une
     surface de l'application. Le noir vaut dans les deux thèmes. */
  --qr-fond: #ffffff;
  --qr-encre: #000000;
  --voile: rgba(0, 0, 0, 0.6);

  --rayon: 14px;
  --rayon-petit: 9px;
  --police: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --police-titre: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia,
    "Times New Roman", serif;
  --police-fixe: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

/* Le même vin dans un autre verre : la craie devient la pierre de la cave, le
   vert bouteille s'éclaircit assez pour tenir sur un fond sombre, et le noir du
   bouton passe sur son texte. Les rôles ne bougent pas — seules les valeurs.

   Le sélecteur est le tampon posé par theme.js, jamais une requête média : le
   script a déjà arbitré entre le réglage du système et le choix explicite de
   l'utilisateur, et c'est son verdict qui s'applique ici. */
:root[data-theme='sombre'] {
  color-scheme: dark;

  /* Fonds et surfaces */
  --craie: #0f1613;
  --carte: #16201c;
  --champ: #111a16;

  /* Encres */
  --encre: #e8efea;
  --doux: #a3b2aa;
  --faible: #8b9a92;

  /* Filets */
  --trait: #25322c;
  --trait-fort: #35453d;

  /* Accent. Le vert profond du clair serait illisible ici : il s'éclaircit,
     et le texte posé dessus devient sombre. */
  --accent: #5fbf95;
  --accent-appui: #7ed3ab;
  --accent-doux: #1b2f27;
  --sur-accent: #0f1613;

  /* Sémantique. Le vert manque toujours : l'accent occupe la case. */
  --ambre: #e0a44a;
  --ambre-doux: #2e2415;
  --alerte: #f08a72;
  --alerte-doux: #331c17;
  --alerte-trait: rgba(240, 138, 114, 0.35);

  /* Le fond de photo s'assombrit un peu : sur une carte déjà sombre, il doit
     rester distinct d'elle, sans quoi la vignette perd ses bords. */
  --fond-photo: #0b100e;
  --quai: rgba(22, 32, 28, 0.94);
}

/* ------------------------------------------------------------------------- *
   Les palettes.

   Le thème dit la *luminosité* — clair ou sombre, et c'est le bouton de
   l'en-tête qui en décide, appareil par appareil. La palette dit la *couleur*,
   elle est réglée sur le compte, et les deux sont orthogonales : il en faut donc
   une par luminosité, sans quoi un compte réglé en « système » verrait sa
   couleur disparaître au coucher du soleil.

   D'où deux attributs et non un — `data-palette-claire` et `data-palette-sombre`
   — posés ensemble sur le `<html>` par le gabarit, et combinés ici avec le
   tampon de `theme.js`. La sélection se fait donc **en CSS et non en
   JavaScript** : le script continue de ne connaître que la luminosité, et il n'a
   pas à savoir laquelle des deux palettes s'applique.

   Chaque bloc redéclare **tous** les jetons de couleur, et ce n'est pas de la
   redite : `:root` est la palette claire par défaut et s'applique toujours, donc
   un jeton oublié dans un bloc sombre retomberait sur une valeur claire — de
   l'encre presque noire sur un fond noir, c'est-à-dire le bug que
   `test_les_palettes_declarent_les_memes_jetons` existe pour empêcher, en pire :
   il ne se verrait que sur la palette qu'on n'a pas sous les yeux.

   `color-scheme` n'y est pas répété : il est posé par le bloc de luminosité
   au-dessus, que la palette ne fait que recolorer. Les trois jetons invariants
   — `--qr-fond`, `--qr-encre`, `--voile` — n'y sont pas non plus, et pour la
   raison écrite en tête : ils décrivent des objets qui n'appartiennent pas à
   l'interface. Un QR reste noir sur blanc quelle que soit la palette, sur
   l'écran comme sur le papier.

   L'accent, lui, obéit à une contrainte mesurée : **il ne peut pas être rouge.**
   L'en-tête l'a déjà dit pour le vert — l'accent occupant cette case, la
   confiance haute a dû s'en passer — et la même règle mord plus fort ici, parce
   que l'échec, lui, ne peut pas renoncer à sa couleur. Un accent grenat, le plus
   tentant pour une cave, mettrait le bouton principal et le bandeau d'erreur
   dans la même famille. Les deux palettes ajoutées prennent donc le bleu et le
   prune, qui laissent l'ambre et l'alerte lisibles.
 * ------------------------------------------------------------------------- */

/* « Ardoise & givre » — la pierre froide plutôt que le bois. Un bleu de
   lavis pour l'accent, un fond légèrement bleuté : c'est la palette la plus
   neutre des trois, celle qui ne raconte rien. */
:root[data-theme='clair'][data-palette-claire='ardoise'] {
  --craie: #eef1f5;
  --carte: #ffffff;
  --champ: #f4f7fa;

  --encre: #131a21;
  --doux: #4d5a67;
  --faible: #85919d;

  --trait: #dde4ea;
  --trait-fort: #c3ccd6;

  --accent: #2b5d80;
  --accent-appui: #204a67;
  --accent-doux: #e2ecf3;
  --sur-accent: #ffffff;

  --ambre: #b5761a;
  --ambre-doux: #faf1e2;
  --alerte: #b0442f;
  --alerte-doux: #f9ebe7;
  --alerte-trait: rgba(176, 68, 47, 0.4);

  --fond-photo: #1b232b;
  --quai: rgba(255, 255, 255, 0.94);
}

/* « Prune & parchemin » — la couleur du vin jeune, sur un papier chaud. Le
   prune est le seul détour possible vers le rouge : il en garde la chaleur sans
   entrer dans la case de l'alerte. */
:root[data-theme='clair'][data-palette-claire='prune'] {
  --craie: #f5f1ea;
  --carte: #fffdf9;
  --champ: #f8f4ed;

  --encre: #1c161c;
  --doux: #5b4f5b;
  --faible: #928693;

  --trait: #e6ded4;
  --trait-fort: #cfc4b7;

  --accent: #6b3a6e;
  --accent-appui: #552d58;
  --accent-doux: #efe6f0;
  --sur-accent: #fffdf9;

  --ambre: #a9711d;
  --ambre-doux: #f8f0df;
  --alerte: #b0442f;
  --alerte-doux: #f9ebe7;
  --alerte-trait: rgba(176, 68, 47, 0.4);

  --fond-photo: #221b22;
  --quai: rgba(255, 253, 249, 0.94);
}

/* « Ardoise & nuit » — la même pierre, la lampe éteinte. L'accent s'éclaircit
   comme le vert du thème sombre par défaut, et pour la même raison : un bleu de
   lavis serait illisible sur ce fond. */
:root[data-theme='sombre'][data-palette-sombre='ardoise'] {
  --craie: #0e131a;
  --carte: #161e27;
  --champ: #111922;

  --encre: #e6edf4;
  --doux: #a0adba;
  --faible: #8794a1;

  --trait: #253141;
  --trait-fort: #35455a;

  --accent: #6fb3e0;
  --accent-appui: #91c8ec;
  --accent-doux: #1a2c3b;
  --sur-accent: #0e131a;

  --ambre: #e0a44a;
  --ambre-doux: #2e2415;
  --alerte: #f08a72;
  --alerte-doux: #331c17;
  --alerte-trait: rgba(240, 138, 114, 0.35);

  --fond-photo: #090d12;
  --quai: rgba(22, 30, 39, 0.94);
}

/* « Prune & suie » — le pendant sombre du parchemin. Le fond garde la chaleur
   du prune plutôt que de retomber sur un gris neutre, sans quoi les deux
   palettes sombres se ressembleraient. */
:root[data-theme='sombre'][data-palette-sombre='prune'] {
  --craie: #141014;
  --carte: #1d181e;
  --champ: #171218;

  --encre: #efe8ef;
  --doux: #b0a3b0;
  --faible: #97899a;

  --trait: #2e2530;
  --trait-fort: #403542;

  --accent: #c391cd;
  --accent-appui: #d7aade;
  --accent-doux: #2b1f2e;
  --sur-accent: #141014;

  --ambre: #e0a44a;
  --ambre-doux: #2e2415;
  --alerte: #f08a72;
  --alerte-doux: #331c17;
  --alerte-trait: rgba(240, 138, 114, 0.35);

  --fond-photo: #0d090d;
  --quai: rgba(29, 24, 30, 0.94);
}

* {
  box-sizing: border-box;
}

/* L'attribut `hidden` n'est qu'une règle `display: none` de la feuille du
   navigateur : n'importe quelle règle d'auteur posant un `display` la bat, et
   l'élément reste visible. C'est arrivé ici sur `.bandeau` et `.puces`, qui
   s'affichaient au chargement de la page alors qu'ils portaient `hidden`.
   Le `!important` est la seule façon de rendre l'attribut fiable, puisqu'il doit
   l'emporter sur toute règle de mise en page présente ou future. */
[hidden] {
  display: none !important;
}

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

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--craie);
  color: var(--encre);
  font-family: var(--police);
  font-size: 16px;
  line-height: 1.5;
  -webkit-tap-highlight-color: transparent;
}

/* ---------- Structure ---------- */

/* L'en-tête est collant : sur un téléphone, la fiche est longue et savoir où
   l'on est coûte moins cher qu'un aller-retour vers le haut de page. */
.entete {
  position: sticky;
  top: 0;
  z-index: 5;
  border-bottom: 1px solid var(--trait);
  background: var(--carte);
  padding: 0.75rem 1.25rem;
  padding-top: max(0.75rem, env(safe-area-inset-top));
}

.entete .contenu-large {
  margin: 0 auto;
  max-width: 44rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.entete h1 {
  margin: 0;
  font-family: var(--police-titre);
  font-size: 1.45rem;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: 0.01em;
}

.entete nav {
  display: flex;
  gap: 0.25rem;
  font-size: 0.8rem;
}

.entete nav a {
  padding: 0.28rem 0.6rem;
  border-radius: 999px;
  color: var(--faible);
  text-decoration: none;
}

.entete nav a:hover {
  background: var(--champ);
  color: var(--doux);
}

.entete nav a.actif {
  background: var(--accent-doux);
  color: var(--accent);
  font-weight: 500;
}

/* Bascule de thème. Glyphe seul : à 380 px, l'en-tête porte déjà le titre et
   trois onglets, et un libellé de plus les ferait passer à la ligne. Le nom du
   thème courant est porté par `aria-label` et par l'infobulle, tous deux écrits
   par theme.js. */
.bascule-theme {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.85rem;
  height: 1.85rem;
  padding: 0;
  border: 1px solid var(--trait);
  border-radius: 50%;
  background: var(--carte);
  color: var(--doux);
  font: inherit;
  font-size: 0.85rem;
  line-height: 1;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.bascule-theme:hover {
  border-color: var(--trait-fort);
  background: var(--champ);
  color: var(--encre);
}

.contenu {
  margin: 0 auto;
  max-width: 44rem;
  padding: 1.25rem 1.25rem calc(2.5rem + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Titre de section, en serif : c'est la seule voix de la marque dans une
   interface qui est par ailleurs un outil. */
.titre-section {
  margin: 0 0 0.75rem;
  font-family: var(--police-titre);
  font-size: 1.1rem;
  font-weight: 600;
  letter-spacing: 0.01em;
}

/* ---------- Cartes ---------- */

/* Les grandes zones de la page sont des cartes blanches sur la craie. La règle
   est posée sur les sections existantes plutôt que par une classe ajoutée au
   gabarit : ce sont les mêmes blocs qu'avant, seul l'habillage change. */
#capture,
#pellicule,
.entete-ecran,
#liste,
#fiche-vin,
.fiche,
.reglages {
  padding: 1rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon);
  background: var(--carte);
}

/* ---------- Zone de capture ---------- */

.zone-capture {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 2.5rem 1.5rem;
  border: 1.5px dashed var(--trait-fort);
  border-radius: 12px;
  background: var(--champ);
  color: inherit;
  font: inherit;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, transform 0.1s;
}

.zone-capture:hover,
.zone-capture.survol {
  border-color: var(--accent);
  background: var(--accent-doux);
}

.zone-capture:active {
  transform: scale(0.99);
}

.zone-capture .icone {
  font-size: 2.25rem;
  line-height: 1;
}

.zone-capture .titre {
  font-family: var(--police-titre);
  font-size: 1.15rem;
  font-weight: 600;
}

.zone-capture .aide,
.zone-capture .aide-desktop {
  font-size: 0.8rem;
  color: var(--faible);
}

/* Le glisser-déposer n'existe pas au doigt : on ne le mentionne qu'au pointeur. */
.zone-capture .aide-desktop {
  display: none;
}

@media (hover: hover) and (pointer: fine) {
  .zone-capture .aide-desktop {
    display: block;
  }
}

/* ---------- Aperçu ---------- */

.apercu {
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--trait);
  border-radius: 11px;
  background: var(--champ);
}

.apercu img {
  display: block;
  width: 100%;
  max-height: 18rem;
  object-fit: contain;
  background: var(--fond-photo);
}

.apercu figcaption {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0.65rem;
  border-top: 1px solid var(--trait);
  background: var(--champ);
  font-family: var(--police-fixe);
  font-size: 0.68rem;
  color: var(--faible);
}

/* ---------- Pellicule : les photos d'un même scan ---------- */

.pellicule {
  display: flex;
  gap: 0.5rem;
  margin: 0 0 0.85rem;
  padding: 0;
  list-style: none;
  /* Le plafond est de quelques photos, mais un écran étroit doit pouvoir en
     montrer quatre : on défile plutôt qu'on rétrécit les vignettes. */
  overflow-x: auto;
}

.vignette {
  position: relative;
  flex: 0 0 auto;
  border-radius: var(--rayon-petit);
  overflow: hidden;
  background: var(--fond-photo);
}

.vignette img {
  display: block;
  width: 5.25rem;
  height: 7rem;
  object-fit: cover;
}

/* Rang et bouton « retirer » sont posés SUR la photo, pas sur une surface de
   l'interface : ils ne suivent aucun des deux thèmes, ils suivent l'image. Ce
   sont les seules couleurs en dur de la feuille, et c'est pour cette raison. */
.vignette .rang {
  position: absolute;
  bottom: 4px;
  left: 4px;
  padding: 0 0.3rem;
  border-radius: 3px;
  background: rgba(19, 28, 24, 0.72);
  font-family: var(--police-fixe);
  font-size: 0.6rem;
  color: #fff;
}

/* La photo dont l'étiquette a été recadrée. Avec plusieurs vues, c'est
   l'information qui explique un recadrage inattendu. */
.vignette.lue {
  box-shadow: inset 0 0 0 2px var(--accent);
}

.vignette .retirer {
  position: absolute;
  top: 0.25rem;
  right: 0.25rem;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(19, 28, 24, 0.72);
  color: #fff;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.vignette .retirer:hover {
  background: var(--alerte);
}

.actions-pellicule {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.55rem;
}

#pellicule .aide {
  margin: 0 0 0.85rem;
  font-size: 0.8rem;
  color: var(--faible);
}

/* Recadrage renvoyé avec l'extraction : l'étiquette seule, redressée. Il vit
   dans la fiche, sous les notes de lecture, d'où la marge basse. */
.recadre {
  margin: 0 0 1rem;
}

.recadre img {
  max-height: 15rem;
}

.actions-recadrage {
  display: flex;
  gap: 0.8rem;
}

/* ---------- Bandeaux d'état ---------- */

.bandeau {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon);
  background: var(--carte);
  font-size: 0.92rem;
  color: var(--doux);
}

.bandeau.erreur {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  border-color: var(--alerte-trait);
  background: var(--alerte-doux);
}

.bandeau.erreur p {
  margin: 0;
}

.erreur-titre {
  font-weight: 600;
  color: var(--alerte);
}

.rouet {
  width: 1rem;
  height: 1rem;
  flex: none;
  border: 2px solid var(--accent);
  border-top-color: transparent;
  border-radius: 50%;
  animation: tourne 0.7s linear infinite;
}

@keyframes tourne {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .rouet {
    animation-duration: 2s;
  }
}

/* ---------- Fiche de validation ---------- */

.fiche {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.fiche-entete {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.fiche-entete h2 {
  margin: 0;
  font-family: var(--police-titre);
  font-size: 1.2rem;
  font-weight: 600;
}

/* La confiance haute est en encre, pas en vert : l'accent a pris la couleur.
   Seuls le doute et l'échec sont colorés — voir l'en-tête de la feuille. */
.etiquette-confiance {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  background: var(--champ);
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--encre);
}

.etiquette-confiance::before {
  content: '';
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
}

.etiquette-confiance.moyenne {
  background: var(--ambre-doux);
  color: var(--ambre);
}

.etiquette-confiance.moyenne::before {
  background: var(--ambre);
}

.etiquette-confiance.faible {
  background: var(--alerte-doux);
  color: var(--alerte);
}

.etiquette-confiance.faible::before {
  background: var(--alerte);
}

.notes {
  margin: 0;
  padding: 0.65rem 0.85rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--champ);
  font-size: 0.88rem;
  color: var(--doux);
}

.grille {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
}

/* Deux colonnes dès qu'il y a la place. Le seuil est bas exprès : à 380 px, les
   champs courts — millésime, degré, contenance — tiennent côte à côte, et la
   fiche passe de treize écrans de haut à sept. */
@media (min-width: 22rem) {
  .grille {
    grid-template-columns: 1fr 1fr;
  }

  .grille .pleine-largeur {
    grid-column: 1 / -1;
  }
}

.grille label {
  display: flex;
  flex-direction: column;
  gap: 0.28rem;
}

/* L'intitulé d'un champ de la grille, qu'il coiffe un contrôle unique — le
   `span` d'un `label` — ou une liste de puces suivie de son champ d'ajout, où
   c'est le `label` lui-même qui porte le texte. Même voix pour les deux : ce sont
   les intitulés d'un même formulaire. */
.grille label > span,
.grille .champ-liste > label {
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faible);
}

.grille .champ-liste {
  display: flex;
  flex-direction: column;
  gap: 0.28rem;
  min-width: 0;
}

/* L'aspect d'un contrôle de saisie, écrit une seule fois pour les trois endroits
   qui en posent : la grille d'un formulaire, la ligne de réglage — pastilles,
   jeton, rattachement d'un contact — et le filtre de l'historique.

   Il l'était en deux copies, et deux contrôles n'en avaient aucune : le filtre de
   l'historique et la recherche de l'annuaire, qui est un `type=search`, un type
   que la liste ci-dessous ne nommait pas. Tous deux sortaient donc en natif,
   gris et bombés au milieu d'un écran plat. C'est un oubli qui ne se voit que sur
   l'écran concerné : rien ne casse, la commande fonctionne, elle vient simplement
   d'une autre application — donc énumérer les types est un piège, et un contrôle
   ajouté ici se regarde plutôt qu'il ne se suppose habillé.

   Le champ de la page « Recherche » (`.champ-recherche input`) garde exprès sa
   propre règle : il est plus grand que les autres parce que c'est lui qu'on
   utilise, et le replier ici lui ferait perdre ce rang. */
.grille input[type='text'],
.grille input[type='search'],
.grille input[type='number'],
.grille input[type='password'],
.grille select,
.ligne-reglage input,
.ligne-reglage select,
.filtre-nature select {
  width: 100%;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background-color: var(--champ);
  color: var(--encre);
  font: inherit;
  font-size: 0.92rem;
  transition: border-color 0.15s, background-color 0.15s;
}

/* `background-color` et non `background` au-dessus : le chevron d'une liste
   déroulante est un `background-image`, et la forme courte l'effacerait.

   Le chevron est dessiné plutôt qu'importé, pour les deux raisons habituelles de
   cette feuille — aucune ressource externe, et aucune couleur hors d'un jeton,
   qu'un fichier SVG emporterait avec lui. Deux dégradés en biseau font la pointe,
   et `appearance: none` retire la flèche du système : c'est elle qui fait qu'une
   liste déroulante ne ressemble à rien d'autre sur l'écran, et elle ne se laisse
   habiller nulle part. */
.grille select,
.ligne-reglage select,
.filtre-nature select {
  appearance: none;
  padding-right: 1.9rem;
  background-image: linear-gradient(45deg, transparent 43%, var(--doux) 43%, var(--doux) 57%, transparent 57%),
    linear-gradient(-45deg, transparent 43%, var(--doux) 43%, var(--doux) 57%, transparent 57%);
  background-position: right 1.05rem center, right 0.65rem center;
  background-size: 0.4rem 0.4rem;
  background-repeat: no-repeat;
}

/* Safari habille un `input[type=search]` de sa propre forme — un ovale, sa
   croix, son ombre interne — et laisse le bord et le fond de la feuille sans
   effet tant qu'on ne la lui retire pas. C'est ce qui laissait le champ de
   l'annuaire seul de son espèce sur l'écran. */
.grille input[type='search'] {
  appearance: none;
}

.grille input::placeholder {
  color: var(--faible);
}

.grille input:focus,
.grille select:focus,
.ligne-reglage input:focus,
.ligne-reglage select:focus,
.filtre-nature select:focus {
  outline: none;
  border-color: var(--accent);
  background-color: var(--carte);
}

.grille label.case {
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
  align-self: end;
  padding-bottom: 0.55rem;
}

/* `align-self: end` et le talon ci-dessus existent pour poser la case sur la
   même ligne de base que le champ voisin de sa rangée. Une case en pleine
   largeur n'a pas de voisin : les garder n'alignerait rien et ouvrirait un blanc
   sous chaque case des réglages, qui en empilent trois d'affilée. */
.grille label.case.pleine-largeur {
  align-self: auto;
  padding-bottom: 0;
}

.grille label.case span {
  font-family: var(--police);
  font-size: 0.9rem;
  letter-spacing: normal;
  text-transform: none;
  color: var(--doux);
}

.grille input[type='checkbox'] {
  width: 1.05rem;
  height: 1.05rem;
  accent-color: var(--accent);
}

/* Une liste de mots courts — mentions, cépages — en bulles séparées. Sur le
   formulaire de scan, ces puces *sont* la liste : `ficheASoumettre()` les relit au
   moment d'enregistrer, donc la balise n'est pas décorative.

   Enfant direct exprès : une puce saisie contient son bouton de retrait, et un
   `.puces span` non contraint habillerait aussi ce qu'elle porte — une bulle dans
   la bulle. */
.puces {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.puces > span {
  padding: 0.18rem 0.5rem;
  border: 1px solid var(--trait);
  border-radius: 999px;
  background: var(--champ);
  font-size: 0.72rem;
  color: var(--doux);
}

/* Une puce saisie — cépage ou mention : la même bulle, plus le bouton qui la
   retire. Le bord droit se resserre pour que la croix ne flotte pas dans la
   gouttière. */
.puces > span.retirable {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  padding-right: 0.2rem;
}

.retirer-puce {
  width: 1.1rem;
  height: 1.1rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--faible);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.retirer-puce:hover {
  background: var(--alerte-doux);
  color: var(--alerte);
}

/* Cépages déduits de l'appellation. Volontairement plus discrets qu'une puce et
   dessinés en pointillé : ce ne sont pas des valeurs de la fiche mais des
   propositions, et un cépage supposé qui aurait l'air d'un cépage lu serait
   indiscernable une fois en base. Le survol les rapproche de l'accent — elles
   deviennent alors ce qu'elles sont, un bouton. */
.suggestions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.1rem;
}

.suggestions-intro {
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.06em;
  color: var(--faible);
}

.puce-suggeree {
  padding: 0.18rem 0.5rem;
  border: 1px dashed var(--trait-fort);
  border-radius: 999px;
  background: transparent;
  color: var(--doux);
  font: inherit;
  font-size: 0.72rem;
  cursor: pointer;
}

.puce-suggeree::before {
  content: '+ ';
  color: var(--faible);
}

.puce-suggeree:hover {
  border-style: solid;
  border-color: var(--accent);
  background: var(--accent-doux);
  color: var(--accent);
}

.brut summary {
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--faible);
}

.brut summary:hover {
  color: var(--doux);
}

.brut pre {
  margin: 0.6rem 0 0;
  padding: 0.75rem;
  overflow-x: auto;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--champ);
  font-family: var(--police-fixe);
  font-size: 0.76rem;
  white-space: pre-wrap;
  color: var(--doux);
}

/* Le rangement — combien d'exemplaires, et où. Séparé visuellement de la fiche
   parce qu'il ne décrit pas le vin mais les bouteilles : c'est la séparation
   Wine / Bottle du modèle de données, rendue lisible à l'écran. */
.rangement {
  margin: 0;
  padding: 0.85rem;
  border: 1px solid var(--trait);
  border-radius: 12px;
  background: var(--champ);
}

.rangement legend {
  padding: 0 0.3rem;
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faible);
}

.rangement .grille {
  margin-top: 0.5rem;
}

.rangement .grille input {
  background: var(--carte);
}

/* Confirmation d'enregistrement. Le variant « erreur » du bandeau existe déjà ;
   celui-ci est son pendant, pour que les deux réponses d'un même bouton se
   lisent au même endroit plutôt que dans une alerte du navigateur. */
.bandeau.succes {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  margin-top: 1rem;
  border-color: var(--accent);
  background: var(--accent-doux);
}

.bandeau.succes p {
  margin: 0;
  color: var(--encre);
}

/* Le pied de fiche est collant : sur un téléphone la fiche fait plusieurs
   écrans de haut, et « Ajouter à la cave » se trouvait tout en bas. Il reste
   dans le flux du formulaire, donc il ne masque rien une fois la fiche passée. */
.fiche-pied {
  position: sticky;
  bottom: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin: 0 -1rem -1rem;
  padding: 0.7rem 1rem;
  padding-bottom: max(0.7rem, env(safe-area-inset-bottom));
  border-top: 1px solid var(--trait);
  border-radius: 0 0 var(--rayon) var(--rayon);
  background: var(--quai);
  backdrop-filter: blur(8px);
}

.fiche-pied .bouton.principal {
  flex: 1;
}

.provenance {
  font-family: var(--police-fixe);
  font-size: 0.64rem;
  color: var(--faible);
}

/* ---------- Contrôles ---------- */

.bouton {
  padding: 0.52rem 1rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--carte);
  color: var(--encre);
  font: inherit;
  font-size: 0.88rem;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.bouton:hover:not(:disabled) {
  border-color: var(--trait-fort);
  background: var(--champ);
}

.bouton.principal {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--sur-accent);
}

.bouton.principal:hover:not(:disabled) {
  border-color: var(--accent-appui);
  background: var(--accent-appui);
}

.bouton:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.lien,
.bouteille-mouvement,
.bouteille-ranger {
  border: 0;
  background: none;
  padding: 0;
  color: var(--accent);
  font: inherit;
  font-size: 0.82rem;
  font-weight: 500;
  cursor: pointer;
}

.lien:hover,
.bouteille-mouvement:hover,
.bouteille-ranger:hover {
  color: var(--accent-appui);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.lien:disabled,
.bouteille-mouvement:disabled,
.bouteille-ranger:disabled {
  opacity: 0.4;
  cursor: default;
  text-decoration: none;
}

/* Le focus clavier doit rester visible partout : sur fond clair, la bordure
   d'accent des champs ne suffit pas à signaler une cible atteinte au clavier. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- Réglages ---------- */

.reglages {
  font-size: 0.9rem;
}

.reglages summary {
  cursor: pointer;
  color: var(--doux);
}

.reglages .aide {
  color: var(--faible);
  font-size: 0.8rem;
}

/* `align-items: flex-end` : sur la page des pastilles, le contrôle est coiffé
   d'un intitulé et le bouton ne l'est pas. Étirés par défaut, ils se retrouvent
   décalés d'une hauteur de ligne ; alignés par le bas, ils reposent sur la même
   assise. */
/* `wrap` est arrivé avec le second intitulé de la planche — nombre et premier
   numéro côte à côte, suivis de trois boutons. Sans lui, la ligne ne peut que
   comprimer : `min-width: 0` plus bas empêche le bouton d'être écrasé, il
   n'empêche pas les champs de devenir illisibles sur un téléphone, qui est
   justement l'écran devant la cave. */
.ligne-reglage {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

/* L'aspect est celui de tous les contrôles de la feuille, plus haut ; ce qui
   appartient à cette ligne, c'est la répartition de la place entre le contrôle
   et le bouton qui le suit.

   `min-width: 0` n'est pas décoratif : un contrôle est un élément remplacé, donc
   sa largeur minimale automatique est sa taille intrinsèque, et sans cette ligne
   c'est le bouton qui se fait écraser sur un écran étroit. */
.ligne-reglage input,
.ligne-reglage select,
.ligne-reglage label {
  flex: 1;
  min-width: 0;
}

/* La ligne des pastilles coiffe son contrôle d'un intitulé, là où la fiche d'un
   vin et le bloc du jeton n'en ont pas : le `label` empile alors les deux, comme
   dans une grille de formulaire, et sa voix est celle des autres intitulés. */
.ligne-reglage label {
  display: flex;
  flex-direction: column;
  gap: 0.28rem;
}

.ligne-reglage label > span {
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faible);
}


/* ---------- Inventaire : liste de la cave et résultats de recherche ---------- */

/* Rappel de l'en-tête de cette feuille : aucune couleur ne se déclare ici, rien
   que des jetons. Une couleur écrite en dur ne basculerait pas de thème, et le
   défaut ne se verrait que dans l'autre. */

.entete-ecran .aide {
  margin: 0;
  font-size: 0.8rem;
  color: var(--faible);
}

.entete-ecran .titre-section {
  margin-bottom: 0.35rem;
}

/* Le formulaire de recherche : un champ pleine largeur, deux filtres, une
   action. Le champ passe avant les filtres parce que c'est lui qu'on utilise. */
#recherche {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.champ-recherche {
  display: flex;
  flex-direction: column;
  gap: 0.28rem;
}

.etiquette-champ {
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faible);
}

.champ-recherche input {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--champ);
  color: var(--encre);
  font: inherit;
  font-size: 1rem;
  transition: border-color 0.15s, background 0.15s;
}

.champ-recherche input::placeholder {
  color: var(--faible);
}

.champ-recherche input:focus {
  outline: none;
  border-color: var(--accent);
  background: var(--carte);
}

.actions-recherche {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* Compte des références et des bouteilles, au-dessus de la liste. En chasse fixe
   comme les autres chiffres de l'interface : c'est une mesure, pas une phrase. */
.resume {
  margin: 0 0 0.75rem;
  font-family: var(--police-fixe);
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  color: var(--faible);
}

.liste-vins,
.liste-contacts {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* Chaque ligne est un bouton pleine largeur : la cible tactile est la ligne
   entière, et non le seul titre. C'est la différence entre « ça s'ouvre » et
   « ça s'ouvre parfois ».

   La ligne d'annuaire partage la règle : ce sont deux listes du même écran-type —
   un titre, un détail, une pastille à droite — et les séparer aurait fait dériver
   l'une des deux au premier ajustement. */
.ligne-vin .ouvrir,
.ligne-contact .ouvrir {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 0.5rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--carte);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}

.ligne-vin .ouvrir:hover,
.ligne-contact .ouvrir:hover {
  border-color: var(--accent);
  background: var(--accent-doux);
}

/* Le fond tient la place de l'image avant qu'elle arrive, et la garde si elle
   n'arrive pas : sans lui, une ligne sans recadrage n'aurait pas la même
   indentation que ses voisines. */
.vignette-etiquette {
  display: block;
  flex: 0 0 auto;
  width: 3rem;
  height: 4rem;
  overflow: hidden;
  border-radius: 6px;
  background: var(--fond-photo);
}

.vignette-etiquette img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.ligne-texte {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
  flex: 1;
}

/* Les trois lignes de texte se coupent plutôt que de pousser la largeur : un
   producteur à rallonge ne doit pas faire défiler l'écran horizontalement. */
.ligne-titre,
.ligne-detail,
.ligne-emplacements {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ligne-titre {
  font-family: var(--police-titre);
  font-size: 1.02rem;
  font-weight: 600;
}

.ligne-detail {
  font-size: 0.82rem;
  color: var(--doux);
}

.ligne-emplacements {
  font-family: var(--police-fixe);
  font-size: 0.66rem;
  color: var(--faible);
}

.stock {
  flex: 0 0 auto;
  align-self: flex-start;
  padding: 0.18rem 0.5rem;
  border-radius: 999px;
  background: var(--champ);
  font-family: var(--police-fixe);
  font-size: 0.64rem;
  color: var(--doux);
}

#suite {
  margin-top: 0.85rem;
  width: 100%;
}

/* L'état vide est un bandeau, comme le chargement et l'échec : les trois se
   lisent au même endroit et se remplacent l'un l'autre. */
#vide {
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
}

#vide p {
  margin: 0;
}

/* ---------- Fiche d'un vin en consultation ---------- */

#fiche-vin {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

#retour {
  align-self: flex-start;
}

.fiche-cave {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.fiche-cave-entete {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.fiche-cave-entete h2 {
  margin: 0;
  font-family: var(--police-titre);
  font-size: 1.35rem;
  font-weight: 600;
  line-height: 1.2;
}

.fiche-cave-entete p {
  margin: 0;
  font-size: 0.9rem;
  color: var(--doux);
}

.stock-fiche {
  margin-top: 0.2rem !important;
  font-family: var(--police-fixe);
  font-size: 0.68rem;
  color: var(--faible) !important;
}

/* Deux colonnes d'étiquette et de valeur. Le même seuil que la grille du
   formulaire de scan : à 380 px, deux propriétés courtes tiennent côte à côte. */
.proprietes {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.6rem;
  margin: 0;
}

@media (min-width: 22rem) {
  .proprietes {
    grid-template-columns: 1fr 1fr;
  }
}

.propriete {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.propriete dt {
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faible);
}

.propriete dd {
  margin: 0;
  font-size: 0.94rem;
}

/* ---------- Exemplaires ---------- */

.exemplaires .titre-section,
.photos-vin .titre-section {
  margin-bottom: 0.5rem;
  font-size: 1rem;
}

.liste-bouteilles {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.bouteille {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--champ);
  font-size: 0.86rem;
}

.bouteille-emplacement {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* L'achat s'efface devant l'emplacement : c'est l'emplacement qui répond à la
   question de l'écran de recherche — « où est-elle ? ». Le bloc ne prend pas de
   place quand il est vide, donc un exemplaire sans date ni prix garde la ligne
   d'avant. */
.bouteille-achat {
  font-size: 0.74rem;
  color: var(--faible);
  white-space: nowrap;
}

.bouteille-statut {
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: var(--accent-doux);
  font-size: 0.68rem;
  color: var(--accent);
}

.bouteille-numero {
  font-family: var(--police-fixe);
  font-size: 0.64rem;
  color: var(--faible);
}

.photos-vin .aide {
  margin: 0.6rem 0 0;
  font-size: 0.78rem;
  color: var(--faible);
}

/* Refaire le recadrage : la commande, et l'image qu'elle propose.

   La case « sans déroulage » n'est pas dans une `.ligne-reglage` : là, un `label`
   s'étire et empile un intitulé en capitales au-dessus de son champ, ce qui est
   juste pour une saisie coiffée d'un titre et faux pour une case à cocher suivie
   de son mot. */
/* Un seul bouton, donc rien à aligner : seule la respiration au-dessus de la
   pellicule est à poser. La classe est distincte d'`.actions-retraitement` juste
   en dessous parce que ce sont deux gestes, et que l'un s'affiche sans l'autre. */
.actions-photos {
  margin-top: 0.75rem;
}

.actions-retraitement {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem;
  margin-top: 0.75rem;
}

.actions-retraitement label {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.85rem;
  color: var(--doux);
}

.actions-retraitement input[type='checkbox'] {
  width: 1.05rem;
  height: 1.05rem;
  accent-color: var(--accent);
}

/* Pointillé comme les cépages suggérés et la zone de capture : ce qui n'est pas
   encore entré dans la fiche ne doit pas avoir l'air d'y être. Sans cette
   distinction, la proposition et l'étiquette enregistrée se ressembleraient
   assez pour qu'on croie le remplacement déjà fait. */
.propose {
  margin-top: 0.8rem;
  border-style: dashed;
  border-color: var(--trait-fort);
}

.photos-vin .pellicule {
  margin-bottom: 0;
}

/* La suppression est destructive et sans retour : elle se distingue du bouton
   ordinaire sans pour autant prendre la place du bouton principal, qui n'existe
   pas sur cet écran. */
.bouton.danger {
  border-color: var(--alerte-trait);
  color: var(--alerte);
}

.bouton.danger:hover:not(:disabled) {
  border-color: var(--alerte);
  background: var(--alerte-doux);
}

/* Le pied de la fiche de consultation est dans une carte qui a son propre
   padding : il reprend la marge négative du pied de fiche du scan. */
#fiche-vin .fiche-pied {
  margin: 0 -1rem -1rem;
}

/* Les deux suites possibles après un enregistrement réussi : scanner encore, ou
   aller voir la fiche qui vient d'être écrite. */
.actions-enregistre {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.actions-enregistre .bouton {
  text-decoration: none;
}

/* ---------- Mouvements ---------- */

/* La même liste sur deux écrans : la section « Mouvements » d'une fiche et
   l'écran « Historique ». Le gabarit de ligne est partagé, donc son habillage
   aussi — deux blocs de règles auraient divergé au premier ajustement, et l'une
   des deux vues aurait alors montré autre chose sans raison. */
.mouvements .titre-section {
  margin-bottom: 0.5rem;
  font-size: 1rem;
}

.mouvements .resume {
  margin-bottom: 0.5rem;
}

.liste-mouvements {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.mouvement-ouvrir {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--champ);
  color: inherit;
  text-decoration: none;
  font-size: 0.86rem;
}

/* L'attribut, pas la classe : la même ligne mène à une fiche sur l'écran
   d'historique et ne mène nulle part sur la fiche de ce vin — on y est déjà.
   Sans `href`, elle ne doit donc ni s'éclairer au survol ni prendre la main. */
.mouvement-ouvrir[href] {
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}

.mouvement-ouvrir[href]:hover {
  border-color: var(--accent);
  background: var(--accent-doux);
}

/* Deux tiers de la vignette d'inventaire : sur une ligne de mouvement,
   l'étiquette sert à reconnaître le vin d'un coup d'œil, pas à être regardée. */
.vignette-mouvement {
  display: block;
  flex: 0 0 auto;
  width: 2rem;
  height: 2.7rem;
  overflow: hidden;
  border-radius: 5px;
  background: var(--fond-photo);
}

.vignette-mouvement img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.mouvement-texte {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
  flex: 1;
}

/* Les trois lignes se coupent plutôt que de pousser la largeur, comme sur la
   ligne d'inventaire : une note de dégustation à rallonge ne doit pas faire
   défiler l'écran horizontalement. */
.mouvement-geste,
.mouvement-vin,
.mouvement-detail {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mouvement-geste {
  font-weight: 500;
}

.mouvement-vin {
  font-size: 0.82rem;
  color: var(--doux);
}

.mouvement-detail {
  font-family: var(--police-fixe);
  font-size: 0.66rem;
  color: var(--faible);
}

/* La date s'aligne en haut et ne se coupe jamais : c'est la colonne qu'on
   parcourt du regard pour situer un geste. */
.mouvement-date {
  flex: 0 0 auto;
  align-self: flex-start;
  font-family: var(--police-fixe);
  font-size: 0.64rem;
  color: var(--doux);
  white-space: nowrap;
}

/* Le filtre de nature tient sur une ligne sous l'intitulé de l'écran, comme les
   champs du formulaire de recherche. */
.filtre-nature {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  margin-top: 0.75rem;
  max-width: 16rem;
}

.filtre-nature span {
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--faible);
}

/* --------------------------------------------------------------------------
   Pastilles RFID

   Aucune couleur en dur : tout passe par les jetons, donc les deux thèmes
   suivent sans règle supplémentaire — c'est ce que vérifie
   `test_aucune_couleur_nest_ecrite_hors_dun_jeton`.
   -------------------------------------------------------------------------- */

/* Le bloc « Scanner un casier » est masqué par le script quand le navigateur ne
   sait pas lire de NFC ; il n'est donc pas dans le flux la plupart du temps. */
.scan-casier {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.scan-casier .aide {
  margin: 0;
}

/* Sur la fiche d'un vin : l'état de la pastille et les deux commandes. */
.pastille {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin: 0.75rem 0;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--champ);
}

.pastille-etat {
  margin: 0;
  color: var(--doux);
  font-size: 0.9rem;
}

/* L'écart entre le casier annoncé par la pastille et celui des bouteilles. Il
   est *signalé* et non corrigé : le logiciel ne sait pas si la pastille a suivi
   la caisse. Même voix qu'une aide, pas celle d'une erreur — rien n'est cassé,
   il y a seulement une décision à prendre. */
.pastille-ecart {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
  margin: 0.4rem 0 0;
  color: var(--doux);
  font-size: 0.85rem;
}

/* ---------- Correction d'une fiche ---------- */

/* Le formulaire remplace ce qu'il édite. Les blocs sont masqués par cette classe
   et non par leur attribut `hidden` : « Mouvements » et « Photos » se masquent
   déjà elles-mêmes quand elles sont vides, et les rallumer en refermant le
   formulaire ferait apparaître une section vide. Deux logiques sur le même
   attribut, dont une seule sait ce qu'elle fait.

   Ce qui reste visible est délibéré : l'en-tête, l'étiquette recadrée et le
   texte brut. Ce sont les trois choses qu'on regarde en corrigeant. */
.en-correction #proprietes,
.en-correction #vin-mentions,
.en-correction #vin-notes,
.en-correction #vin-pastille,
.en-correction #contacts-vin,
.en-correction #exemplaires,
.en-correction #mouvements,
.en-correction #photos-vin,
.en-correction #fiche-actions {
  display: none;
}

.actions-pastille {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Sur l'écran de scan, dans le bloc « À ranger en cave ». */
.pastille-scan {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 0.75rem;
}

/* Le résumé de la liste et l'oubli groupé du stock, sur une ligne. Le bouton est
   à l'opposé du compte parce que c'est ce compte qu'il supprime : les mettre côte
   à côte inviterait à cliquer en croyant filtrer. */
.ligne-resume {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}

/* La liste du parc. Même construction que `.liste-vins` : une colonne de lignes
   encadrées, parce que c'est le même geste de lecture au doigt. */
.liste-tags {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.ligne-tag {
  display: grid;
  /* Le casier d'abord et à largeur fixe : c'est la colonne qu'on suit du regard
     en parcourant la cave, et des largeurs qui dansent d'une ligne à l'autre la
     rendraient illisible. */
  grid-template-columns: 6rem 1fr auto auto;
  align-items: center;
  gap: 0.5rem 0.75rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--carte);
}

/* Le QR d'une ligne : la même figure que la planche, à la taille près. Assez
   grand pour être reconnu d'un coup d'œil en parcourant le parc, pas assez pour
   être scanné à l'écran — ce n'est pas le geste, on scanne le papier collé sur le
   casier. L'impression se fait depuis la fiche du vin, où le format réel est
   servi. En millimètres comme le reste du QR : un `rem` ferait dépendre la
   vignette du réglage de police.

   **La colonne n'est réservée que si la page en a au moins un**, par une classe
   que le script pose après avoir peint. Sans cette condition, une gouttière vide
   s'ouvrirait sur un parc tout en puces, et sur toute installation où le QR est
   éteint — et la réserver *par ligne* serait pire : les casiers ne
   s'aligneraient plus d'une ligne à l'autre, or c'est la colonne qu'on suit du
   regard. */
.liste-tags.avec-qr .ligne-tag {
  grid-template-columns: 16mm 6rem 1fr auto auto;
}

.liste-tags.avec-qr .tag-casier {
  grid-column: 2;
}

.liste-tags.avec-qr .tag-vin {
  grid-column: 3;
}

/* Les deux commandes sont placées explicitement dès que la colonne du QR
   s'ouvre : le placement automatique compte les cellules libres, et la figure du
   code en occupe une sur deux rangées — donc les boutons glisseraient d'un cran
   selon que la ligne porte un QR ou non. */
.liste-tags.avec-qr .ligne-tag .tag-stl {
  grid-column: 4;
}

.liste-tags.avec-qr .ligne-tag .oublier {
  grid-column: 5;
}

.liste-tags.avec-qr .tag-uid {
  grid-column: 2 / -1;
}

.ligne-tag .tag-qr {
  grid-row: 1 / span 2;
  grid-column: 1;
  padding: 1mm;
  gap: 0;
}

.ligne-tag .pastille-qr-dessin {
  width: 14mm;
  height: 14mm;
}

.tag-casier {
  font-family: var(--police-fixe);
  font-size: 0.85rem;
  color: var(--encre);
}

.tag-vin {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.tag-lien {
  color: inherit;
  font-weight: 600;
}

.tag-stock {
  font-size: 0.85rem;
  color: var(--doux);
}

/* L'UID est ce qu'on tape pour réparer quelque chose : lisible, mais discret —
   il n'est pas ce qu'on cherche en parcourant la liste. */
.tag-uid {
  grid-column: 1 / -1;
  font-family: var(--police-fixe);
  font-size: 0.7rem;
  color: var(--faible);
  overflow-wrap: anywhere;
}

.ligne-tag .oublier {
  grid-row: 1;
  grid-column: 3;
}

/* Le support d'une pastille, en petites capitales à côté de son identifiant.
   Affiché parce qu'il décide du geste de réparation — une puce se reprogramme,
   un autocollant se réimprime — et non pour ranger le parc en deux listes. */
.tag-support {
  margin-right: 0.4rem;
  padding: 0.05rem 0.3rem;
  border: 1px solid var(--trait-fort);
  border-radius: var(--rayon-petit);
  font-family: var(--police);
  font-size: 0.65rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--doux);
}

/* ---------- Pastilles QR : la variante imprimée ---------- *

   Un QR est un objet **physique**, et l'habillage en tient compte de bout en
   bout : fond blanc et trait noir dans les deux thèmes, et une taille donnée en
   millimètres plutôt qu'en rem. Un autocollant inversé en thème sombre serait
   illisible pour la moitié des lecteurs, et un module dont la taille suit celle
   du texte de l'utilisateur ne scannerait plus une fois imprimé. */

.pastille-qr {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  margin: 0;
  padding: 2mm;
  /* Le blanc est le fond du QR, pas celui du thème : il fait partie du dessin,
     puisque c'est lui qui donne le contraste que le lecteur cherche. */
  background: var(--qr-fond);
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  /* Chaque pastille est une unité de découpe : la couper en deux entre deux
     pages la rendrait inutilisable. */
  break-inside: avoid;
}

/* Le SVG sort du serveur sans dimensions, seulement avec sa `viewBox` : c'est
   ici que l'autocollant prend sa taille, et en millimètres — 30 mm de côté font
   des modules de 0,7 mm sur un symbole de version 4, ce que lit n'importe quel
   téléphone. Un `rem` aurait fait dépendre la taille imprimée du réglage de
   police du navigateur. */
.pastille-qr-dessin {
  width: 30mm;
  height: 30mm;
}

.pastille-qr-dessin svg {
  display: block;
  width: 100%;
  height: 100%;
}

/* L'identifiant en clair, et ce n'est pas une légende : c'est le seul chemin qui
   reste quand le code est abîmé ou quand le téléphone ne sait pas scanner. D'où
   la police à pas fixe — on le recopie caractère par caractère. */
.pastille-qr-uid {
  font-family: var(--police-fixe);
  font-size: 0.6rem;
  color: var(--qr-encre);
  overflow-wrap: anywhere;
}

/* La planche est une section d'entretien posée sous la liste du parc, et le filet
   dit qu'elle ne parle plus des mêmes pastilles : celles du dessus sont posées,
   celles du dessous n'existent pas encore. */
.planche {
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--trait);
}

/* La planche à découper. Une grille qui se remplit d'elle-même : le nombre de
   pastilles est choisi par l'utilisateur, et fixer des colonnes ferait dépendre
   la mise en page du format de papier — que seul le navigateur connaît. */
.planche-grille {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(34mm, max-content));
  gap: 3mm;
  margin-top: 0.75rem;
}

/* Une carte de la planche : l'autocollant, et la commande qui en tire la version
   en volume.

   Le bouton est **hors de la figure** — c'est la figure seule qui se découpe et
   se colle — et **dans la carte**, parce que le fichier STL est celui d'une
   pastille : un clic, un fichier. La règle de découpe monte donc de la figure à
   la carte, sinon la coupure de page tomberait entre un code et son bouton. */
.pastille-planche {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  break-inside: avoid;
}

/* La ligne de pose d'un QR sur la fiche d'un vin : taper, scanner, imprimer. */
.pastille-pose {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  width: 100%;
  margin-top: 0.6rem;
}

.pastille-pose input {
  flex: 1 1 12rem;
  min-width: 0;
  font-family: var(--police-fixe);
}

/* L'autocollant qu'on vient de forger, sur la fiche comme sur l'écran de scan.
   Bordé en pointillé comme les cépages suggérés et la proposition de
   retraitement : ce n'est pas une donnée de la fiche, c'est quelque chose à
   faire — imprimer, découper, coller. */
.pastille-a-coller {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  width: 100%;
  margin-top: 0.6rem;
  padding: 0.6rem;
  border: 1px dashed var(--trait-fort);
  border-radius: var(--rayon-petit);
}

/* Une pastille **déjà posée** n'est pas une proposition : le pointillé dit
   « quelque chose à faire » — comme les cépages suggérés et la proposition de
   retraitement — et une pastille collée sur son casier est un état de la fiche.
   Le trait plein le dit, et c'est la seule différence entre les deux emplois de
   cette figure. */
.pastille-a-coller.posee {
  border-style: solid;
}

.pastille-a-coller .aide {
  flex: 1 1 12rem;
  margin: 0;
}

/* La visionneuse de QR, dans `base.html` parce que trois écrans s'en servent. */
.scanner-qr {
  max-width: min(92vw, 30rem);
  padding: 0.75rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon);
  background: var(--carte);
  color: var(--encre);
}

.scanner-qr::backdrop {
  background: var(--voile);
}

.scanner-qr video {
  display: block;
  width: 100%;
  max-height: 60vh;
  border-radius: var(--rayon-petit);
  /* Le fond sombre est celui d'une photo : il donne ses bords à l'image le temps
     que la caméra démarre, plutôt qu'un rectangle blanc qui clignote. */
  background: var(--fond-photo);
  object-fit: cover;
}

.scanner-qr .aide {
  margin: 0.5rem 0;
}

/* ---------- Impression d'un fragment de page ---------- *

   `Apogee.imprimer` pose `.a-imprimer` sur le fragment et `.impression-ciblee`
   sur le corps, puis appelle `window.print()`. Sans ça, imprimer un carré de
   trois centimètres emporterait la fiche entière et sa barre de navigation.

   `visibility` et non `display` : masquer un ancêtre en `display: none`
   emporterait ses descendants, donc le fragment lui-même. `visibility: hidden`
   se laisse au contraire annuler par un descendant, ce qui est exactement ce
   qu'on veut ici — c'est le seul moyen en CSS de dire « tout sauf la branche qui
   mène à cet élément », puisqu'on ne peut pas sélectionner un ancêtre.

   Les classes ne vivent que dans ce bloc : une classe oubliée par un navigateur
   qui n'émet pas `afterprint` ne se voit donc pas à l'écran. */
@media print {
  .impression-ciblee,
  .impression-ciblee * {
    visibility: hidden;
  }

  .impression-ciblee .a-imprimer,
  .impression-ciblee .a-imprimer * {
    visibility: visible;
  }

  /* Le bouton « STL » d'une carte de planche ne part pas à l'imprimante : le
     ciblage rend visible *tout* ce que porte le fragment, et une planche
     constellée de boutons ne se colle pas. `display` et non `visibility` — celle-ci
     garderait la place du bouton entre deux pastilles, donc décalerait la
     découpe. C'est aussi la seule des deux qu'aucune des règles ci-dessus ne
     peut annuler depuis un descendant. */
  .pastille-stl {
    display: none;
  }

  /* Sorti du flux et remonté en haut de page : sans ça, le fragment s'imprimerait
     à la place qu'il occupe dans l'écran, donc précédé de plusieurs pages
     blanches — les blocs masqués gardent leur hauteur, c'est le prix de
     `visibility`. */
  .impression-ciblee .a-imprimer {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    margin: 0;
    padding: 0;
    border: 0;
  }
}

/* ---------- Annuaire : le bloc de contact ---------- *

   Un seul rendu pour trois endroits — la fiche de l'annuaire, le vigneron d'un
   vin, le vendeur de ses exemplaires — parce que c'est `Apogee.peindreContact`
   qui les écrit tous les trois. Trois habillages auraient fait lire le même
   contact de trois façons selon l'écran. */

.contact-bloc {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--carte);
}

.contact-nom {
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  margin: 0;
  font-family: var(--police-titre);
  font-size: 1rem;
  font-weight: 600;
}

/* Le type est une étiquette et non une phrase : il range la fiche, il ne la
   décrit pas. D'où la casse fixe et la taille en retrait. */
.contact-type {
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faible);
}

/* `pre-line` : l'adresse est saisie sur plusieurs lignes et rangée telle quelle,
   donc ses retours sont porteurs. Les rendre par des `<br>` fabriqués en
   JavaScript ferait décider au script d'une mise en forme qui appartient au
   texte. */
.contact-adresse {
  margin: 0;
  white-space: pre-line;
  font-size: 0.85rem;
  color: var(--doux);
}

.contact-liens {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0;
  font-size: 0.85rem;
}

.contact-liens a {
  color: var(--accent);
  overflow-wrap: anywhere;
}

/* ---------- Annuaire : la fiche et le formulaire ---------- */

.modes-commande {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.9rem;
  margin: 0.6rem 0 0;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
}

.modes-commande legend {
  padding: 0 0.3rem;
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faible);
}

.actions-contact {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0.9rem 0 0;
}

/* Le bouton d'ajout est dans la grille des filtres, où les autres cases portent
   un intitulé au-dessus de leur champ : sans ce recalage, il flotterait à la
   hauteur des étiquettes. */
.aligne-bas {
  align-self: end;
}

/* ---------- Les deux rattachements d'un vin ---------- */

.contacts-vin {
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
  margin-top: 1.1rem;
}

.contact-role {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.contact-role-titre {
  margin: 0;
  font-family: var(--police-fixe);
  font-size: 0.63rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--faible);
}

/* ---------- Le critère venu de l'annuaire ---------- *

   Il dit de quoi la liste est filtrée, et propose d'en sortir. Posé hors de la
   liste, parce qu'il doit rester lisible quand elle est vide — c'est justement là
   qu'il explique le plus. */

.critere-actif {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.7rem;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--accent-doux);
  font-size: 0.85rem;
}

/* ---------- Comptes : connexion, installation, réglages ---------- *

   Trois écrans nouveaux, et volontairement peu de règles neuves : ce sont des
   formulaires, et le dépôt en a déjà — `.grille`, `.bandeau`, `.bouton`,
   `.titre-section`. Ce qui suit ne couvre que ce qu'aucun écran n'avait :
   une colonne étroite pour les deux pages sans barre de navigation, une case à
   cocher avec son intitulé sur la même ligne, et la liste des comptes. */

/* Les écrans de connexion et d'installation sont seuls sur leur page : une
   colonne pleine largeur y étirerait deux champs sur 1200 px. */
.contenu-etroit {
  max-width: 26rem;
}

.titre-accueil {
  margin: 0 0 1.25rem;
  font-family: var(--police-titre);
  font-size: 1.6rem;
  text-align: center;
}

.formulaire-compte {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
  margin-bottom: 1.5rem;
  padding: 1rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon);
  background: var(--carte);
}

/* Qui est connecté, et par où partir. En pied de page plutôt qu'en en-tête, où
   la barre compte déjà sept liens sur un téléphone. */
.pied-compte {
  margin: 2rem 0 0;
  padding-top: 0.9rem;
  border-top: 1px solid var(--trait);
  font-size: 0.82rem;
  color: var(--faible);
}

/* La liste des comptes. Elle ne réutilise pas `.liste-vins` : ses lignes ne
   s'ouvrent pas — il n'y a pas de fiche de compte — donc elles ne sont pas des
   boutons pleine largeur, et leur donner l'apparence d'une ligne cliquable
   promettrait un écran qui n'existe pas. */
.liste {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.ligne-compte {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--trait);
  border-radius: var(--rayon-petit);
  background: var(--carte);
}

.ligne-principale {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.ligne-compte .aide {
  margin: 0;
  font-family: var(--police-fixe);
  font-size: 0.68rem;
  letter-spacing: 0.03em;
}

/* Les boutons passent à la ligne plutôt que de comprimer : trois actions ne
   tiennent pas côte à côte sur un téléphone, et un bouton écrasé à trois
   caractères ne se lit plus. */
.ligne-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.badge {
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: var(--accent-doux);
  font-family: var(--police-fixe);
  font-size: 0.62rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--accent);
}
