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

   L'état vide d'une bibliothèque : une invitation à agir, pas une
   illustration « Rien à afficher ». Deux usages dans l'app
   (frontend/index.html) :
     - recherche : showEmpty (l.607–610) — « Aucune recherche ne correspond
       à « … » » ;
     - historique : l.774 — « Aucun envoi pour l'instant… ».
   Le template JS assemble exactement :
     <div class="empty" style="grid-column:1/-1">
       <div class="big"><svg class="ic" style="width:46px;height:46px;
                            stroke-width:1.2"><use href="#i-…"/></svg></div>
       TEXTE-NU
     </div>
   CONTRAT : les classes .empty et .empty .big doivent survivre. Le texte
   est un NOEUD NU (pas de balise) : la typographie du texte nu est posée
   sur .empty elle-même, et .empty__title / .empty__text (markup riche,
   specimen) re-déclarent les mêmes valeurs sans conflit.

   Règles (02-ANTI-SLOP.md + skill primitive) :
   - Aucune valeur en dur : uniquement les tokens de tokens.css et de la
     clé de variation. Aucune variable de statut n'est nécessaire —
     l'échelle red est importée et mappée par themes/kindle-shelf.css
     (--danger-9/10/11 existent), comme pour chip.css / panel.css.
     Littéraux tolérés comme dans button.css / chip.css : les durées du
     spin (0.75 s / 2 s) et les pourcentages/angles du masque (structure
     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 se combine avec le focus. Le
     `border: 0` est un reset du bord de l'app (l.157), pas une bordure.
   - ÉTAT DE LA PAGE — le plafond « 3 ombres distinctes » porte sur la
     page specimen ENTIÈRE et est SATURÉ par la primitive Button :
        1. --borders-base                    (repos)
        2. --borders-base + --borders-focus  (focus clavier)
        3. --borders-error                   (erreur)
     Empty ne PEUT PAS introduire d'ombre nouvelle : le cadre réutilise
     --borders-base (repos / vide / chargement / désactivé) et
     --borders-error (erreur) — exactement comme Panel (primitives/
     panel.css). Pas d'élévation card-rest/card-hover (4e/5e ombre).
   - Rayons : --radius-panel sur le cadre (c'est un regroupement réel —
     « la zone de la bibliothèque »), --radius-field sur l'action
     (famille du Button). Jamais le rayon unique appliqué à tout.
   - Icône .big : slot carré 48 px (sur la grille ; l'app impose 46 px en
     inline — l'inline l'emporte, la primitive ne le conteste pas),
     trait fin, couleur --fg-muted (l'app : --line-2, l'étape atténuée :
     l'icône d'un vide est discrète, le signal est le texte). Pas d'emoji,
     pas de glyphe décoratif : le domaine (bibliothèque, liseuse,
     recherche) vient du texte, pas de l'illustration.
   - Survol : le cadre n'est PAS interactif — son survol n'existe pas
     (décision documentée, comme Panel : le survol de la skill ne
     s'applique qu'aux éléments interactifs ; l'interaction vit dans
     l'action, et elle seule). L'action : fond --accent-10, pas
     d'agrandissement.
   - Focus clavier : :focus-visible uniquement, sur l'action, via la
     chaîne --borders-base + --borders-focus (jamais --borders-focus
     seul = 4e ombre). Le cadre n'est jamais focusable.
   - Désactivé : l'action est estompée + cursor: not-allowed +
     pointer-events: none (aria-disabled posé par l'écran). C'est l'état
     « rien à faire ici » — le bouton de la zone est coupé.
   - Chargement : c'est l'ÉTAT DU CONTENEUR, pas de l'action — comme
     l'app le signale sur une liste qui se charge (#hist-status l.765 :
     spinner + libellé, pas de bouton en chargement). Le slot .big (place
     figée 48 px) reçoit le spinner (inset:0 + margin:auto, centré sans
     déplacement), le texte nu reste visible : aucun saut de mise en
     page. Le conteneur porte seul l'aria-label (CONTRAT : aria-label
     UNIQUEMENT en chargement). L'action, elle, n'a pas d'état de
     chargement : ses états sont repos / survol / focus / désactivé.
   - Erreur : --borders-error sur le cadre (liseré 1px + halo 15 %) +
     titre en --danger-11 (l'échelle red est mappée par la clé de
     variation — repli var(--fg-default) si une clé future ne le fait
     plus) + phrase qui dit ce qui s'est passé ET quoi faire. Le
     correctif est dans l'action « Réessayer » (accent).
   - Transitions : background-color + box-shadow, --duration-fast (2
     propriétés, 1 durée — mêmes que Button, plafond 2/3 respecté). Le
     spin est une animation de transform, hors transitions.
   - Graisses : --weight-strong (titre), --weight-medium (action),
     --weight-regular (texte) — 3 graisses max 600, déjà sur la page.
   ========================================================================= */

/* --- Cadre ----------------------------------------------------------------
   Remplace .empty de l'app (l.157) : le padding 60 px de l'app n'est pas
   sur l'échelle fermée → var(--space-10) (48 px), var(--space-6) (20 px)
   conservé. Le texte nu (contrat JS) lit la typographie posée ici :
   14 px / --lh-base / --weight-regular / --fg-muted — la phrase du vide. */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;

  padding: var(--space-10) var(--space-6);
  gap: var(--space-3); /* icône → texte : 8 px (l'app : 10 px inline, off-grid) */

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

  background: var(--bg-subtle);
  border: 0;
  border-radius: var(--radius-panel);
  box-shadow: var(--borders-base);

  min-width: 0;
}

/* --- Icône grande (.big) ----------------------------------------------------
   Remplace .empty .big de l'app (l.158–159). Slot carré 48 px (grille
   4px), centrage en flex : la place est figée, donc le spinner de
   chargement peut s'y substituer sans déplacement. L'app impose 46 px sur
   le <svg> en inline (l.609/l.774) — l'inline l'emporte sur cette règle
   (46 < 48 : le glyphe tient dans le slot) : documenté, hors budget du
   gate (qui mesure padding/margin/gap, pas width). */

.empty .big {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--space-10);  /* 48 px, sur la grille */
  height: var(--space-10);
  color: var(--fg-muted);
}
.empty .big svg {
  display: block;
  width: 100%;
  height: 100%;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.2; /* trait fin, comme l'app (l.609) */
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- Texte (markup riche, specimen) -----------------------------------------
   Le template de l'app injecte un texte nu ; le markup riche (titre +
   phrase + action) est livré pour les écrans qui veulent appeler à agir —
   c'est l'esprit de l'état vide d'ANTI-SLOP (« une invitation à agir »).
   Mêmes valeurs token par token que le texte nu de .empty, sans conflit. */

.empty__title {
  font-size: var(--text-md);
  line-height: var(--lh-md);
  font-weight: var(--weight-strong);
  letter-spacing: var(--tracking-normal);
  color: var(--fg-default);
  margin: 0;
}

.empty__text {
  margin: 0;
  max-width: 44ch; /* invitation, pas de lecture */
}

/* --- Action (CTA) ----------------------------------------------------------
   L'action du vide : « Réessayer » en erreur, « Envoyer mon premier
   livre » dans une bibliothèque vide. Auto-porteur : sa propre
   apparence, pas de dépendance à button.css (mêmes tokens → rendu
   identique si la vue le remplace par .btn). L'unique endroit du cadre
   qui a le droit d'être audacieux (fond accent). */

.empty__action {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);

  margin-top: var(--space-3); /* un souffle avant l'action : 8 px */
  padding: var(--space-3) var(--space-5); /* 8 / 16 px — sur la grille */

  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;

  background: var(--bg-solid);
  color: var(--fg-on-solid);
  box-shadow: var(--borders-base);

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

/* Le libellé passe dans un span (structure du markup riche, alignement
   futur d'un éventuel glyphe). */
.empty__action__label {
  display: inline-flex;
  align-items: center;
}

/* --- États de l'action ------------------------------------------------------- */

/* Survol : fond accent 10 (pas d'agrandissement, pas d'anneau). */
.empty__action:hover:not(:disabled) {
  background: var(--accent-10);
}

/* Focus clavier : bordure de base + anneau accent (chaîne — jamais
   --borders-focus seul). Liseré --bg-app : --borders-focus est
   redéclaré dans le bloc [data-theme="dark"] de tokens.css. */
.empty__action:focus-visible {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

/* Désactivé : opacité + curseur + clic coupé (aria-disabled : l'écran). */
.empty__action:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* --- Chargement : état du conteneur -----------------------------------------
   Le slot .big (place figée) reçoit le spinner (inset:0 + margin:auto,
   centré sans déplacement) ; le texte nu reste visible : aucun saut de
   mise en page. L'aria-label du conteneur est posé par l'écran — CONTRAT :
   aria-label UNIQUEMENT en chargement. */

.empty--loading .empty__spinner {
  position: absolute;
  inset: 0;
  margin: auto;
  color: var(--fg-muted);
}

/* --- Erreur -------------------------------------------------------------------
   Le cadre passe à l'anneau danger (ombre 3 de la page, déjà portée par
   Button/Field/Badge). Titre en --danger-11 + phrase qui dit ce qui s'est
   passé ET quoi faire (ANTI-SLOP : jamais d'erreur vague). Le fond reste
   neutre — jamais de fond teinté + ombre + bordure sur la même surface. */

.empty--error {
  box-shadow: var(--borders-error);
  /* Le texte nu de l'erreur (si le template l'utilise sans titre riche)
     prend la teinte danger : l'erreur se lit dans les deux marqueups. */
  color: var(--danger-11, var(--fg-default));
}
.empty--error .empty__title {
  color: var(--danger-11, var(--fg-default));
}

/* --- Spinner de chargement -----------------------------------------------------
   Anneau en masque, fond currentColor, trois couches en intersection
   (donut + disque + arc de 270°) — même structure que .btn .spin /
   .chip__spinner. 16 px (sur la grille), trait 4 px. */

.empty__spinner {
  display: block;
  width: var(--space-5);
  height: var(--space-5);
  flex-shrink: 0;
  /* margin: 0 — garde-fou explicite : le centrage du spinner vient des
     règles de chargement (inset:0 + margin:auto), jamais d'un margin
     résiduel. Le margin:auto résolu du spinner du slot .big vaut
     (48−16)/2 = 16 px, sur la grille 4px. */
  margin: 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: empty-spin 0.75s linear infinite;
}

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

.empty__action.is-hover {
  background: var(--accent-10);
}
.empty__action.is-focus {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

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