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

   La silhouette de contenu en attente : une grille de lignes (.skeleton >
   .sk) qui préserve la place AVANT que le contenu n'arrive. Deux usages
   dans l'app (frontend/index.html) :
     - historique (loadHistory l.766) : 3 lignes de 72 px en ligne unique
       (grid-template-columns:1fr inline) pendant la charge de #hist ;
     - couvertures de cartes en attente : la silhouette remplit la place
       de la couverture (ratio 2/3) pendant le chargement de l'image.
   Le template JS assemble exactement
     <div class="skeleton" style="grid-template-columns:1fr">
       <div class="sk" style="height:72px"></div> ×3
     </div>
   CONTRAT : les classes .skeleton et .sk et la keyframe sh survivent —
   la primitive les remplace tels quels (l.268–270 de l'app).

   Règles (02-ANTI-SLOP.md + skill primitive) :
   - Aucune valeur en dur : uniquement les tokens de tokens.css et de la
     clé de variation (échelles sand/bronze sombres). Littéraux tolérés
     (comme card.css / panel.css) : la durée du balayage (1.4 s — tokens.css
     ne définit que des durées de transition ; ajouter un token se demande,
     ne se décide pas), les pourcentages de position/du fond et l'angle
     100deg (géométrie de la bande, pas des échelles).
   - Bordure de l'app (1px solid var(--line)) → box-shadow --borders-base :
     l'approche du design system (la boîte ne bouge pas, l'ombre se combine
     avec le focus si un jour un squelette devient focusable).
   - É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)
     Skeleton ne PEUT PAS introduire d'ombre nouvelle : la ligne réutilise
     --borders-base — exactement comme Panel (.panel__sk) et Card
     (repos / survol / chargement / désactivé). Pas d'élévation
     card-rest/card-hover (4e/5e ombre).
   - Rayon : --radius-panel sur la ligne (c'est la silhouette d'un
     regroupement, famille de la Card et du Panel — jamais le rayon unique
     appliqué à tout ; badge/field/panel/modal gardent chacun le leur).
   - Fond : NEUTRE — background-image (PAS background-color → hors du
     compteur « fonds non neutres ») dérivé des tokens : bande
     --bg-component → --bg-hover → --bg-component, 200 % de large, balayée
     par la keyframe sh (background-position — ANIMATION, hors transitions,
     hors plafonds de transition). Le signal d'attente est le mouvement lui-
     même ; aucune couleur d'accent.

   --- Les six états, décision par décision --------------------------------
   Skeleton est un INDICATEUR, pas un contrôle. C'est lui l'état de
   chargement d'une zone, pas une primitive qui SE CHARGE :
   - repos        = le squelette tel qu'il est vu dans l'app en continu
     (le « repos » d'un indicateur, c'est son balayage — il n'existe pas
     sans charger ; l'app ne le rend jamais statique).
   - chargement   = l'état UNIQUE réel : lignes de l'historique (3) et
     couverture de carte en attente. Le conteneur porte l'aria-label
     (CONTRAT : aria-label SEULEMENT en chargement — retiré quand le
     contenu remplace le squelette, comme sur .card.is-loading).
   - transition   = le passage vers le contenu : le squelette est retiré
     du DOM (l'app remplace #hist par les .hrow) — la place est déjà figée
     par la grille, donc le contenu prend la suite sans saut.
   - survol       N'EXISTE PAS (décision documentée, comme Panel/Empty) :
     le squelette n'est pas interactif — rien ne survole une silhouette
     vide. Ne pas « compléter » avec un .sk:hover : état hors contrat.
   - focus clavier N'EXISTE PAS : pas de tabindex, pas de rôle, pas de
     contenu — un indicateur muet n'est pas au clavier (il n'y a rien à
     activer). L'accessibilité passe par l'aria-label du CONTENEUR en
     chargement, pas par un anneau sur la ligne.
   - désactivé    N'EXISTE PAS : couper le chargement n'est pas un état du
     squelette — on retire le squelette (transition vers le contenu ou
     vers l'état vide Empty). Un squelette figé serait un bug, pas un état.
   - erreur       N'EXISTE PAS (explicitement hors contrat de la tâche) :
     « c'est un indicateur ». L'échec de la charge n'est pas signalé par
     le squelette mais par l'état qui le remplace — Empty (--borders-error
     + message « quoi faire ») ou le message de ligne (l'app : t("hist.error")
     + e sur #hist-status). Le rouge reste réservé à ces états.

   -------------------------------------------------------------------------
   DÉCISIONS CONTRAIGNANTES — page specimen kindle-shelf (audit-slop-json.js,
   un SEUL passage sur la PAGE ENTIÈRE)
   -------------------------------------------------------------------------
   - Ombre : --borders-base UNIQUEMENT (ombre 1 de la page, déjà comptée
     par Button/Panel/Card) — zéro ombre nouvelle.
   - Fonds : background-image neutre (sand-3 → sand-4 en sombre, delta
     r/g/b < 12 → le script les compte comme NEUTRES) — zéro teinte
     non neutre ajoutée (le plafond de 3 est saturé : bronze-9 / bronze-10
     du bouton + red-9).
   - Graisses/tailles : la section specimen réutilise .kv / .kv b / .kv .v
     (kv-row.css) pour la transition vers le contenu — aucune nouvelle
     valeur de police introduite ; le squelette lui-même ne porte aucun
     texte (silhouette).
   - Espacements : gap --space-4, largeurs de ligne en pourcentages
     (structure, pas des valeurs d'échelle) ; le 72 px inline de l'app
     n'est pas sur l'échelle fermée --space-* : la silhouette de la
     primitive fait --space-9 (40 px) — documenté, comme .panel__sk.
   ========================================================================= */

/* --- Grille de squelettes ----------------------------------------------------
   Remplace .skeleton de l'app (l.268) : gap 18 px (hors grille 4px) →
   var(--space-4) (12 px). La grille auto-fill reprend le comportement
   de l'app pour les usages multi-colonnes ; l'usage historique pose
   grid-template-columns:1fr en inline (l.766) — l'inline l'emporte, la
   primitive ne le conteste pas. La place est figée par la grille : le
   contenu qui arrive (transition) ne fait pas sauter la vue. */

.skeleton {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--space-12), 1fr)); /* 80 px — grille 4 (l'app : 196 px inline) */
  gap: var(--space-4);            /* 12 px (l'app : 18 px, hors grille) */
  min-width: 0;
}

/* --- Ligne / cellule ----------------------------------------------------------
   Remplace .sk de l'app (l.269) : la bordure 1px solid var(--line) devient
   box-shadow --borders-base ; le rayon 12 px inline de l'app n'est pas la
   hiérarchie de la primitive → --radius-panel (8 px, famille Card/Panel).
   Hauteur : l'app impose 72 px en inline (l.766) — 72 n'existe pas dans
   l'échelle fermée --space-* : la silhouette de la primitive fait
   --space-9 (40 px), comme .panel__sk. L'inline de l'app l'emporte quand
   l'app le pose ; le specimen montre la valeur de la primitive. */

.sk {
  height: var(--space-9);          /* 40 px — sur la grille (l'app : 72 px inline) */
  border-radius: var(--radius-panel);
  box-shadow: var(--borders-base); /* = l'ombre 1 de la page (déjà comptée) */

  /* Balayage : bande NEUTRE dérivée des tokens (background-IMAGE — hors
     du compteur « fonds non neutres »). 100deg / 40-50-60 / 200 % :
     géométrie de la bande (même structure que .panel__sk / .sk de l'app). */
  background-image: linear-gradient(
    100deg,
    var(--bg-component) 40%,
    var(--bg-hover) 50%,
    var(--bg-component) 60%
  );
  background-size: 200% 100%;
  background-position: 100% 0;

  animation: sh 1.4s linear infinite;
}

@keyframes sh {
  to { background-position: -200% 0; }
}

/* --- Variantes de silhouette ---------------------------------------------------
   La même ligne .sk sert de couverture (ratio figé 2/3 = ratio de la
   Card : la place est identique à l'arrivée de l'image) et de ligne de
   texte (largeurs décroissantes — la hiérarchie vient de la place, pas
   d'un texte qui ne peut pas exister ici). Pourcentages = structure. */

/* Couverture de carte en attente : ratio 2/3 figé (identique au
   .coverwrap de la Card) → la couverture qui arrive ne décale rien.
   Largeur = 100 % de la colonne, HAUTEUR dérivée du ratio (comme la
   coverwrap de la Card : c'est la largeur qui est connue, pas la
   hauteur) — un height:--space-9 figé laisserait l'aspect-ratio rétrécir
   la largeur (vérifié CDP : 26,7 px au lieu de la colonne). */
.sk--cover {
  width: 100%;
  height: auto;
  aspect-ratio: 2 / 3;
}

/* Lignes de texte : la première porte la place du titre (pleine),
   la seconde celle de la méta (partiellement occupée). */
.sk--title { width: 100%; }
.sk--meta  { width: 60%; }

/* --- Mouvement réduit -----------------------------------------------------------
   Le balayage s'arrête : la silhouette reste visible (la place est
   préservée, l'information « chargement » reste portée par l'aria-label
   du conteneur). On ne coupe pas l'état, on retire le mouvement. */

@media (prefers-reduced-motion: reduce) {
  .sk {
    animation: none;
  }
}
