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

   L'action d'un livre dans la bibliothèque : envoyer vers la liseuse,
   réessayer, ré-envoyer depuis l'historique, enregistrer le profil.
   kindle-shelf n'a pas de framework — le contrat de cette primitive est le
   contrat du markup que l'app produit (frontend/index.html) :

   <button class="btn [primary|ghost] [ok]" data-md5="…" [data-busy] [disabled]>
     <svg …>…</svg> Envoi            (contenu LIBRE : l'app réécrit innerHTML)
   </button>

   L'app réécrit le bouton 5 fois pendant l'envoi (sendToReader) : icône +
   libellé de progression (« Téléchargement… », « Conversion… », « Envoi… »),
   puis « Envoyé » (ok) ou « Réessayer » (erreur, via toast). La primitive
   ne suppose donc AUCUNE structure interne fixe : elle style la boîte, pas
   ses enfants.

   Règles (02-ANTI-SLOP.md + skill primitive) :
   - Aucune valeur en dur : uniquement les tokens de tokens.css et de la clé
     de variation. Exception UNIQUE tolérée, documentée ici comme le demande
     la carte : --success-9 (#30a46c, valeur vendor Radix green) —
     l'échelle green n'est pas importée par la clé de variation kindle-shelf
     (qui n'importe que sand/bronze/red) et tokens.css ne la définit pas.
     --danger-9 N'a pas besoin d'exception : il est mappé par
     themes/kindle-shelf.css (échelle Radix red) ; il garde quand même un
     repli var(--fg-default) au cas où la clé change (pattern chip.css /
     panel.css). Aucune autre valeur littérale, hors la durée du spin.
   - Bordures = box-shadow à spread 1px (jamais de `border` dessinée) : la
     boîte ne bouge pas et les ombres se combinent avec l'anneau de focus.
     Le `border: 0` initial est un reset du bord UA du <button>, pas une
     bordure dessinée.
   - Élévation au repos via --borders-base. Survol : changement de fond
     seulement, pas d'agrandissement.
   - Rayon : --radius-field (famille champ/bouton — tokens.css le documente).
   - Focus clavier : :focus-visible uniquement, jamais :focus seul. Chaîne
     --borders-base + --borders-focus (liseré --bg-app + anneau accent 60 %).
   - Désactivé : :disabled natif (l'app pose btn.disabled) — opacité +
     curseur. pointer-events: none volontairement OMIS : le quota de
     l'app coupe déjà les clics en JS (l.551-556), et la souris doit pouvoir
     pointer le bouton désactivé pour voir l'état.
   - Chargement : l'app pose data-busy + aria-label SEUL dans cet état
     (CONTRAT — le libellé visible est le nom accessible au repos). Le
     bouton reste cliquable-coupé via pointer-events. Le contenu (icône
     spinner + libellé) est injecté par l'app : la primitive ne cache rien
     et ne fige aucune largeur — les libellés de progression partagent le
     corps 14 px de la grille, le saut est contenu dans le bouton, jamais
     dans la page.
   - Succès (.ok) : fond NEUTRE (hérité) + texte --success-9. Pas de fond
     teinté — le budget de fonds non neutres de la page est serré (plafond
     3) et l'accent bronze est déjà porté par .btn.primary.
   - Erreur (.is-error) : anneau --borders-error (robuste : fallback
     interne à tokens.css) + texte danger. L'app ne pose PAS encore de
     classe d'erreur (elle passe par toast + réécriture du libellé) ; la
     classe est livrée pour que le prochain écran puisse l'utiliser.
     pointer-events: none — le correctif est le toast / le bouton «
     Réessayer », pas ce bouton.
   - Transitions : background-color + box-shadow, --duration-fast (2
     propriétés, 1 durée — plafonds respectés). Le spin est une animation
     de transform, hors transitions.
   - Graisse : --weight-medium (500) seule — la hiérarchie vient de la
     couleur et du fond, pas du gras.
   - Note spin : 0.75 s / 2 s (reduced-motion) sont les seuls littéraux du
     fichier, comme chip.css / field.css / empty.css. tokens.css ne définit
     aucune durée d'animation continue ; ajouter un token là-bas se
     demande, ne se décide pas.
   ========================================================================= */

/* --success-9 : UNIQUE exception en dur, tolérée par la carte (valeur
   vendor Radix green, identique à green.css). Déclaré au niveau du
   composant pour ne PAS polluer tokens.css (canonique, partagé par 5
   apps) ni la clé de variation kindle-shelf. */
.btn {
  --success-9: #30a46c;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3); /* icône ↔ libellé : 8 px, sur la grille */

  /* 8 px verticaux, 12 px horizontaux — sur la grille. Hauteur de repos
     36 px = --row-h de la clé de variation. */
  padding: var(--space-3) var(--space-4);

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

  border: 0;
  border-radius: var(--radius-field);
  cursor: pointer;
  white-space: nowrap;

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

/* --- Variantes ----------------------------------------------------------- */

/* Repos (défaut) — remplace le .btn de l'app : fond de composant + bordure
   de base. C'est l'état des 3 boutons d'envoi de la grille. */
.btn {
  background: var(--bg-component);
  color: var(--fg-default);
  box-shadow: var(--borders-base);
}
.btn:hover:not(:disabled):not([data-busy]):not([disabled]) {
  background: var(--bg-hover);
}

/* primary — l'action principale de la vue (connexion, l.431). Accent plein :
   l'unique endroit audacieux d'une rangée. */
.btn.primary {
  background: var(--bg-solid);
  color: var(--fg-on-solid);
}
.btn.primary:hover:not(:disabled):not([data-busy]):not([disabled]) {
  background: var(--accent-10);
}

/* ghost — action tertiaire (ré-envoyer l'historique, tester le compte,
   déconnexion) : ni fond, ni bordure. */
.btn.ghost {
  background: transparent;
  box-shadow: none;
  color: var(--fg-muted);
  padding: var(--space-2) var(--space-3); /* 4 / 8 px — variante compacte */
}
.btn.ghost:hover:not(:disabled):not([data-busy]):not([disabled]) {
  background: var(--bg-hover);
  color: var(--fg-default);
}

/* --- États ---------------------------------------------------------------- */

/* Focus clavier : :focus-visible uniquement — jamais d'anneau au clic
   souris. La chaîne (bordure de base + anneau) est identique pour les
   trois variantes : le ghost y gagne un contour, ce qui renforce la
   séparation sur fond transparent. Le liseré --bg-app suit le panneau
   (clair/sombre) car --borders-focus est redéclaré dans le bloc
   [data-theme="dark"] de tokens.css. */
.btn:focus-visible {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

/* Désactivé : quota épuisé ou envoi en cours (l'app pose .disabled, et le
   specimen montre aussi la forme attribut). */
.btn:disabled,
.btn[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Chargement : data-busy posé par l'app (sendToReader l.677) + aria-label
   SEUL dans cet état (contrat). Clic coupé ; le contenu (spinner +
   libellé de progression) est injecté par l'app. Le bouton reste une
   surface neutre — le spinner passe en --fg-muted (currentColor) : un
   spinner d'action en cours n'est pas un accent. */
.btn[data-busy] {
  pointer-events: none;
  color: var(--fg-muted);
}

/* Succès : l'envoi est passé (l'app ajoute .ok, l.686). Texte vert vendor
   sur fond neutre hérité — pas de fond teinté (budget de fonds non
   neutres). */
.btn.ok {
  color: var(--success-9);
}

/* Erreur : l'envoi a échoué. Anneau danger (robuste) + texte danger.
   Non cliquable — le correctif est le toast et le libellé « Réessayer ». */
.btn.is-error {
  color: var(--danger-11, var(--fg-default));
  box-shadow: var(--borders-error);
  pointer-events: none;
}

/* --- Spinner de chargement -------------------------------------------------
   L'app injecte <span class="spin"></span> devant le libellé de
   progression (sendToReader l.679). La primitive le dessine : anneau de
   16 px (sur la grille), trait de 4 px, en MASQUE (pas de `border`) —
   le fond est currentColor, donc il suit la couleur de la variante.
   Trois couches en intersection : donut (trou central, épaisseur sur la
   grille), 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, pas une
   couleur. Les angles sont structurels (forme de l'arc). */
.btn .spin {
  display: inline-block;
  width: var(--space-5);
  height: var(--space-5);
  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: btn-spin 0.75s linear infinite;
}

@keyframes btn-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. */

.btn.is-hover {
  background: var(--bg-hover);
}
.btn.primary.is-hover {
  background: var(--accent-10);
}
.btn.ghost.is-hover {
  background: var(--bg-hover);
  color: var(--fg-default);
}
.btn.is-focus {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

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