/* =========================================================================
   Field — primitive du design system (kindle-shelf)

   Le champ de formulaire : libellé + contrôle + message d'erreur.
   kindle-shelf n'a pas de framework — le contrat de la primitive est le
   contrat du markup que l'app produit (frontend/index.html) :

   <div class="field [--error] [--disabled] [--loading]">
     <label for="…">Adresse Kindle</label>
     <input …>                                  (contrôle, seul enfant form)
     <span class="field__spinner" aria-hidden="true"></span>   (uniquement en --loading)
     <span class="field__error" id="…">…</span>                (uniquement en --error)
   </div>

   Deux usages dans l'app : le profil (email Kindle, l.371-374) et le login
   (mot de passe, l.427-429). La bordure colorée de l'app (1px solid
   --line / --accent) devient box-shadow: var(--borders-base) — l'approche
   du design system. Le focus de l'input passe par var(--borders-focus).

   États (classes is-*, convention identique à button.css / badge.css ;
   l'app ne les pose pas encore) :
     <div class="field">…</div>                     (repos, défaut)
     <input class="is-hover">                        (survol, specimen)
     <input class="is-focus">                        (focus clavier, specimen)
     <input value="…">                               (rempli — pas de classe)
     <div class="field field--error">…</div>         (erreur)
     <div class="field field--disabled">…</div>      (désactivé, aria-disabled sur l'input)
     <div class="field field--loading">…</div>       (chargement)
   Contrat : aria-label SEULEMENT en chargement (le libellé <label> est le
   nom accessible au repos ; en chargement il est masqué et l'input le
   porte seul). Jamais d'aria-busy ici.

   Règles (02-ANTI-SLOP.md + skill primitive) :
   - Aucune valeur en dur, ZÉRO exception (contrairement à button.css qui
     documente --success-9) : l'échelle red est importée et mappée par la
     clé de variation (themes/kindle-shelf.css) — --danger-9/10/11
     existent, l'état d'erreur ne demande aucune valeur littérale.
     Uniquement tolérés : la durée du spin (pas de token pour une
     animation continue dans tokens.css — décision à demander, pas à
     prendre, comme dans button.css) et les pourcentages/angles
     structurels du masque (forme de l'arc, pas des valeurs d'échelle).
   - Bordures = box-shadow à spread 1px (jamais de `border` dessinée) : la
     boîte ne bouge pas et l'anneau de focus se combine à la bordure. Le
     `border: 0` initial est un reset du bord UA de <input>, pas une
     bordure dessinée.
   - Focus clavier : :focus-visible uniquement, jamais :focus seul, jamais
     d'anneau au clic souris. Le liseré --bg-app + anneau accent 60 %
     viennent de --borders-focus (redéclaré dans le bloc
     [data-theme="dark"] de tokens.css — le liseré suit le panneau).
   - Survol : fond --bg-hover SEUL, pas d'agrandissement, pas d'ombre
     nouvelle (voir budget ci-dessous).
   - Désactivé : opacité posée sur le wrapper (libellé + contrôle
     s'estompent ensemble) + cursor: not-allowed + clic coupé sur le
     contrôle + aria-disabled posé par l'app sur le contrôle.
   - Chargement : largeur figée — le spinner vit dans le padding droit
     EXISTANT (aucun changement de box, aucun saut de mise en page).
     Le libellé est masqué (visibility:hidden conserve la hauteur) et
     l'input porte seul le nom accessible (aria-label, posé par l'écran —
     contrat ci-dessus).
   - Erreur : --borders-error (liseré danger + halo) + message qui dit ce
     qui s'est passé et quoi faire. --danger-11 (texte, pas fond) :
     contraste correct sur le thème sombre fixe.
   - Rayon : --radius-field sur le contrôle — la famille champ/bouton,
     jamais un rayon unique appliqué à tout.
   - Transitions : background-color + box-shadow, --duration-fast (les 2
     propriétés et la 1 durée déjà comptées par la page via Button —
     rien de neuf). Le spin est une animation de transform, hors des
     plafonds de transition.
   - Graisses : --weight-medium (libellé, message d'erreur) et
     --weight-regular (contrôle) — deux graisses, plafond 600 respecté.
   - Accessibilité : <label for> ↔ <input id> (association native) ;
     l'app pose aria-invalid (erreur) et aria-disabled (désactivé) sur le
     contrôle, et aria-describedby pointant le message en erreur.
   - Spécificité : .field > input (0,1,1) — la primitive possède le look
     du contrôle. Les états posent sur .field (wrapper), pas sur des
     sélecteurs de plus en plus lourds.

   -------------------------------------------------------------------------
   DECISIONS CONTRAIGNANTES SPECIFIQUES A kindle-shelf (page specimen
   entière) — le budget OMBRES est PARTAGÉ par la page, pas par primitive
   -------------------------------------------------------------------------
   L'audit (audit-slop-json.js) mesure la PAGE ENTIERE (un seul passage)
   et le plafond « 3 ombres distinctes » est SATURÉ par la primitive
   Button :
       1. --borders-base                    (repos)
       2. --borders-base + --borders-focus  (focus clavier)
       3. --borders-error                   (erreur)
   Le champ NE PEUT PAS introduire d'ombre nouvelle (pas d'élévation,
   pas d'anneau ad hoc). Il réutilise EXACTEMENT ces trois :
     - repos / survol / rempli / désactivé / chargement : --borders-base
     - focus clavier                                    : ombre 2
     - erreur                                           : ombre 3
   Le SURVOL ne peut pas changer l'ombre (budget saturé) : il passe par
   le FOND SEUL (--bg-hover) — la règle de survol de la skill («
   changement de fond via --bg-hover, pas d'agrandissement »).
   ========================================================================= */

/* --- Champ ---------------------------------------------------------------- */
/* Le wrapper est la colonne : libellé → contrôle → erreur. min-width:0 — le
   champ peut rétrécir sous sa largeur intrinsèque dans une ligne flex / une
   cellule de grille (empêche le débordement mobile) ; le contrôle
   (width:100%) suit le champ. */
.field {
  position: relative; /* ancre absolue du spinner (frère de l'input) */
  display: flex;
  flex-direction: column;
  gap: var(--space-2); /* libellé → contrôle → erreur : 4 px, sur la grille */
  min-width: 0;
}

/* --- Libellé ----------------------------------------------------------------
   Remplace le .field label de l'app (l.223) : 13 px, texte par défaut. */
.field > label {
  font-family: var(--font-ui, inherit);
  font-size: var(--text-sm);
  line-height: var(--lh-compact);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);
  color: var(--fg-default);
}

/* --- Contrôle ----------------------------------------------------------------
   Remplace le .field input de l'app (l.224-226) : fond de composant +
   bordure de base (l'app avait `border: 1px solid var(--line)` — remplacée
   par la box-shadow de spread 1px). Elements de substitution :
   width:100% + box-sizing:border-box pour remplir le champ. */
.field > input {
  display: block;
  width: 100%;
  box-sizing: border-box;

  padding: var(--space-3) var(--space-4); /* 8 / 12 px — sur la grille */

  font-family: inherit;
  font-size: var(--text-base);
  line-height: var(--lh-base);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-wide);

  border: 0;
  border-radius: var(--radius-field);
  background: var(--bg-component);
  color: var(--fg-default);

  box-shadow: var(--borders-base); /* = l'ombre 1 de la page (déjà comptée) */

  transition:
    background-color var(--duration-fast) var(--ease),
    box-shadow var(--duration-fast) var(--ease);
}

.field > input::placeholder {
  color: var(--fg-muted);
  opacity: 0.7;
}

/* --- États du contrôle --------------------------------------------------------- */

/* Survol : fond --bg-hover SEUL (pas d'ombre nouvelle — budget saturé, voir
   en-tête), pas d'agrandissement. Même ombre que le repos. Coupé sur le
   contrôle désactivé. */
.field:not(.field--disabled) > input:hover {
  background: var(--bg-hover);
}

/* Focus clavier : :focus-visible uniquement — jamais :focus seul, jamais
   d'anneau au clic souris. Réutilise l'ombre 2 (bordure de base + anneau
   --borders-focus : liseré --bg-app + anneau accent 60 %). */
.field > input:focus-visible {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

/* Erreur : --borders-error (RÉUTILISE l'ombre 3, déjà comptée). Le champ
   corrigé au clavier garde en plus l'anneau de focus (chaîne rendue qu'au
   clavier). */
.field--error > input {
  box-shadow: var(--borders-error);
}
.field--error > input:focus-visible {
  box-shadow: var(--borders-error), var(--borders-focus);
}

/* Désactivé : le wrapper est estompé (libellé + contrôle ensemble), le
   curseur est coupé et les interactions sont coupées sur le contrôle.
   Posé APRÈS survol/focus pour l'emporter. */
.field--disabled {
  opacity: 0.5;
}
.field--disabled > input {
  cursor: not-allowed;
  pointer-events: none;
}

/* Chargement : le spinner s'ajoute à droite, DANS le padding droit
   existant (12 → 12 + 16 px) — la largeur du contrôle ne change pas,
   aucun saut de mise en page. Le libellé est masqué en VISIBILITÉ
   (visibility:hidden conserve la hauteur) et l'input porte seul le nom
   accessible (aria-label posé par l'écran — contrat). */
.field--loading > input {
  padding-right: calc(var(--space-4) + var(--space-5)); /* 12 + 16 px */
  pointer-events: none;
}
.field--loading > label {
  visibility: hidden;
}

/* --- Message d'erreur -----------------------------------------------------------
   Une seule phrase, pas d'excuse : ce qui s'est passé, quoi faire.
   --danger-11 (texte, pas fond) — red est mappé par la clé de variation. */
.field__error {
  font-family: var(--font-ui, inherit);
  font-size: var(--text-xs);
  line-height: var(--lh-compact);
  letter-spacing: var(--tracking-wide);
  font-weight: var(--weight-medium);
  color: var(--danger-11);
}

/* --- Spinner de chargement --------------------------------------------------------
   Ancre à droite, centré verticalement, dans le padding droit du contrôle.
   16 px (sur la grille), trait de 4 px, en MASQUE (pas de `border`) — le
   fond est currentColor (l'input hérite --fg-default ; le spinner est un
   état de processus, pas un accent). Trois couches en intersection : donut
   (trou central), disque plein (coupe les coins), arc de 270° (sans le
   tiers manquant, la rotation d'un anneau complet ne serait pas visible).
   Le masque ne lit que le canal alpha : `black` y est structurel.

   Alignement : le spinner est FRÈRE de l'input (un <input> est un élément
   vide — il ne peut pas l'enfermer), et l'input ne peut pas être l'ancre
   absolue d'un frère — l'ancre est donc le .field (position:relative),
   commun aux deux. Le top est le centre vertical du CONTRÔLE, calculé en
   tokens : libellé (--lh-compact) + écart (--space-2) + la moitié de
   l'input ((--lh-base + 2×--space-3)/2 : interligne + padding vertical).
   Sombre : 22 + 4 + 18 = 44 px dans un champ de 62 px (libellé 22 +
   écart 4 + input 36) — le spinner est centré sur le contrôle (44 px), pas
   sur le champ (31 px). En chargement le libellé est
   masqué en VISIBILITÉ (la hauteur du champ reste) — le contrôle ne bouge
   pas, donc le spinner non plus. right: var(--space-4) = le padding droit
   du contrôle : le spinner vit dans la zone de padding, jamais sur le
   texte. */
.field__spinner {
  position: absolute;
  top: calc(var(--lh-compact) + var(--space-2) + (var(--lh-base) + 2 * var(--space-3)) / 2);
  right: var(--space-4);
  width: var(--space-5);
  height: var(--space-5);
  margin-top: calc(var(--space-5) / -2);
  flex-shrink: 0;
  background: currentColor;
  -webkit-mask-image:
    radial-gradient(farthest-side, transparent calc(100% - var(--space-2)), black 0),
    radial-gradient(closest-side, black 99%, transparent 100%),
    conic-gradient(black 270deg, transparent 0deg);
  -webkit-mask-composite: source-in, source-in;
  mask-image:
    radial-gradient(farthest-side, transparent calc(100% - var(--space-2)), black 0),
    radial-gradient(closest-side, black 99%, transparent 100%),
    conic-gradient(black 270deg, transparent 0deg);
  mask-composite: intersect, intersect;
  animation: field-spin 0.75s linear infinite;
}

@keyframes field-spin {
  to { transform: rotate(360deg); }
}

/* --- Simulation d'états (page specimen uniquement) ---------------------------------
   Les vrais états viennent de la souris et du clavier ; ces classes posent
   exactement les mêmes valeurs token par token que les pseudo-classes. */
.field > input.is-hover {
  background: var(--bg-hover);
}
.field > input.is-focus {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

@media (prefers-reduced-motion: reduce) {
  .field > input {
    transition: none;
  }
  /* Le spin ralentit mais reste : l'information « chargement » doit passer. */
  .field__spinner {
    animation-duration: 2s;
  }
}
