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

   Le regroupement d'un livre dans la bibliothèque : couverture + titre +
   auteur + méta (format, langue, taille) + action « Envoyer à ma liseuse ».
   Une carte signale un regroupement REAL (un livre), pas un conteneur
   générique (02-ANTI-SLOP.md « Surfaces »).

   Markup de la primitive — c'est le contrat du markup que l'app produit
   (frontend/index.html, cardHtml l.617-641) ; la primitive le style tel quel :

     <div class="card" data-md5="…">                      ← cliquable, data-md5
       <div class="coverwrap">
         <img class="ok" alt="…" data-urls="…" data-ph="…">
       </div>                                             ← data-urls / data-ph
       <div class="cbody">
         <div class="ctitle">…</div>
         <div class="cauthor">… · 1844</div>
         <div class="badges">
           <span class="badge fmt">EPUB</span>
           <span class="badge">fr</span>
           <span class="badge">2,4 Mo</span>
         </div>
       </div>
       <div class="sendcol">
         <button class="btn" data-md5="…" data-title="…">Envoyer à ma liseuse</button>
       </div>                                             ← data-md5 sur le bouton aussi
     </div>

   Le clic « hors bouton » ouvre le détail : comportement JS de l'app,
   préservé par la STRUCTURE (la carte est cliquable/focusable, le bouton
   est un <button> séparé qui stoppe la propagation via closest('.btn')).
   tabindex="0" est posé par l'appelant pour la focus clavier.

   Règles (02-ANTI-SLOP.md + skill primitive) :
   - Aucune valeur en dur : uniquement les tokens de tokens.css et de la clé
     de variation (couleurs, espacements, rayons, ombres, durées). Les
     pourcentages du shimmer (largeur/position de la bande) sont structurels
     (géométrie), pas des échelles — même usage toléré que badge.css.
   - 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.
   - Rayon hiérarchique : --radius-panel sur l'enveloppe, --radius-badge sur
     les badges. La couverture est pleine largeur (flush) : les coins hauts
     sont taillés par l'overflow:hidden de la carte, elle n'a pas de rayon
     propre (un rayon unique appliqué à tout est du slop).
   - Graisses : --weight-regular (auteur, méta, message) et --weight-strong
     (titre) + --weight-medium (badges) — trois graisses, plafond 600.
   - Transitions : box-shadow + background-color, --duration-fast (2
     propriétés, 1 durée). C'est le SEUL jeu de transitions autorisé : le
     plafond « 2 propriétés animées » porte sur la page entière et le bouton
     occupe déjà background-color + box-shadow.

   -------------------------------------------------------------------------
   DECISIONS CONTRAIGNES SPECIFIQUES A kindle-shelf (page specimen entière)
   -------------------------------------------------------------------------
   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 qui
   occupe déjà exactement :
       1. --borders-base                    (repos)
       2. --borders-base + --borders-focus  (focus clavier)
       3. --borders-error                   (erreur)
   La carte NE PEUT DONC PAS introduire d'ombre nouvelle — en particulier pas
   --elevation-card-rest / --elevation-card-hover (ce seraient des valeurs
   distinctes → 5 ombres → ok:false). Elle réutilise EXACTEMENT ces trois :
     - repos / survol / chargement / désactivé : --borders-base
     - focus clavier : --borders-base, --borders-focus
     - erreur        : --borders-error
   C'est conforme à la skill (« repos : --elevation-* OU --borders-base »).
   Conséquences, documentées :
   - SURVOL = changement de FOND (--bg-hover), pas d'élévation distincte.
     C'est la règle de survol de la skill (« changement de fond via
     --bg-hover, pas d'agrandissement »). L'ancien translateY(-3px) de l'app
     est du slop (agrandissement/décalage) → retiré. Le survol se lit par le
     fond, pas par une 4e ombre.
   - COUVERTURE : pas de transition d'opacité. L'app avait
     `img { opacity:0; transition:opacity }` — mais une 3e propriété animée
     (opacity) ferait ok:false (plafond « 2 propriétés animées »). La
     couverture est donc invisible (opacity:0) jusqu'à .ok (ajouté par
     coverLoaded), l'apparition est instantanée. Le contrat JS
     (data-urls / data-ph / .ok / coverNext / coverLoaded) est préservé.
   - POLICE du titre : var(--font-ui) (Instrument Sans), PAS un serif. La clé
     de variation kindle-shelf ne définit pas de --font-serif ; inventer un
     token est interdit. La hiérarchie titre vient de la taille + la graisse
     (600) + l'espace, pas de la famille (02-ANTI-SLOP « la hiérarchie vient
     de la taille, de l'espace et de la position »). Une seule famille sur
     la page (Instrument Sans) — le plafond de familles n'est pas un risque.
   - BADGES : la carte contient .badges (conteneur) + .badge. La primitive
     Badge (badge.css) est une LIVRAISON SEPARÉE ; pour que la carte soit
     autonome dans le specimen (sans dépendre du <link> badge.css), card.css
     style .badges + .badges .badge + .badges .badge.fmt AVEC LES MEMES
     tokens que badge.css (text-xs, weight-medium, radius-badge,
     borders-base, bg-component / accent sur bg-subtle) → aucun conflit si
     les deux feuilles sont chargées, la carte reste lisible seule.
   - ETATS (classes, convention is-* identique à button.css) :
       .card.is-hover     simulation du survol (specimen)
       .card.is-focus     simulation du focus clavier (specimen)
       .card[aria-disabled="true"]  désactivé (drivé par l'attribut ARIA)
       .card.is-loading   chargement (couverture en attente) — aria-label
       .card.is-error     erreur (couverture indisponible → placeholder)
     Contrat : aria-label SEULEMENT en chargement (pas dans les autres
     états — le titre/auteur portent déjà le nom accessible).
   - KEYFRAME rise : la primitive la possède (remplace l.124 de l'app).
     C'est une ANIMATION (hors transitions, hors plafonds de transition).
     translate sur var(--space-3) (8 px, sur la grille) — pas le 10 px hors
     échelle de l'app. L'app garde son stagger inline (animation-delay).
   ========================================================================= */

/* --- Enveloppe ------------------------------------------------------------- */
.card {
  display: flex;
  flex-direction: column;
  min-width: 0;

  font-family: var(--font-ui, inherit);

  background: var(--bg-component);
  color: var(--fg-default);

  border-radius: var(--radius-panel);
  overflow: hidden;               /* taille les coins hauts de la couverture */
  cursor: pointer;                /* le clic « hors bouton » ouvre le détail */

  /* repos — RÉUTILISE l'ombre du bouton (borders-base) : le plafond « 3
     ombres » de la page entière est déjà saturé (voir en-tête). */
  box-shadow: var(--borders-base);

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

  /* Entrée (remplace la keyframe rise de l'app). Animation, pas transition. */
  animation: rise var(--duration-normal) var(--ease) backwards;
}

/* --- Survol ---------------------------------------------------------------- */
/* Fond --bg-hover uniquement (pas d'ombre nouvelle, pas d'agrandissement —
   voir en-tête). Le survol est coupé sur la carte désactivée et en chargement. */
.card:hover:not([aria-disabled="true"]):not(.is-loading),
.card.is-hover {
  background: var(--bg-hover);
}

/* --- Focus clavier ---------------------------------------------------------- */
/* :focus-visible seulement — jamais d'anneau au clic souris. tabindex posé
   par l'appelant. Réutilise l'ombre 2 (borders-base + borders-focus). */
.card:focus-visible,
.card.is-focus {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

/* --- Désactivé -------------------------------------------------------------- */
/* Livré non envoyable (quota, pas de permission). Estompé, clic coupé.
   Drivé par l'attribut ARIA (aria-disabled posé par l'appelant). */
.card[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}
.card[aria-disabled="true"],
.card[aria-disabled="true"] * {
  pointer-events: none;
}

/* --- Couverture ------------------------------------------------------------- */
/* Pleine largeur, ratio 2/3 (fixe → AUCUN saut de mise en page : la zone
   occupe son espace avant le chargement de l'image). */
.coverwrap {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 2 / 3;
  flex-shrink: 0;
  overflow: hidden;
  background: var(--bg-subtle);   /* couleur placeholder quand pas d'image */
}
.coverwrap img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  /* invisible jusqu'à .ok (coverLoaded) — PAS de transition d'opacité :
     le plafond « 2 propriétés animées » de la page est déjà atteint
     (background-color + box-shadow). Apparition instantanée. */
  opacity: 0;
}
.coverwrap img.ok {
  opacity: 1;
}

/* --- Corps ----------------------------------------------------------------- */
.cbody {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);            /* 4 px */
  flex: 1;
  min-height: 0;
  padding: var(--space-4) var(--space-4) var(--space-3);  /* 12 12 8 — grille 4 */
}

.ctitle {
  font-size: var(--text-base);    /* 14 px */
  line-height: var(--lh-compact);
  font-weight: var(--weight-strong);  /* 600 — le titre porte la hiérarchie */
  letter-spacing: var(--tracking-normal);
  color: var(--fg-default);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.cauthor {
  font-size: var(--text-sm);      /* 13 px */
  line-height: var(--lh-compact);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-wide);
  color: var(--fg-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- Métadonnées (badges) ---------------------------------------------------- */
/* Conteneur. Les .badge sont stylées ci-dessous (voir note en-tête : la
   primitive badge.css est une livraison séparée, mêmes tokens). */
.badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);            /* 4 px */
  margin-top: var(--space-2);     /* 4 px */
}
.badges .badge {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--space-3);      /* 8 px horizontal */
  font-size: var(--text-xs);      /* 12 px */
  line-height: var(--lh-compact);
  font-weight: var(--weight-medium);   /* 500 */
  letter-spacing: var(--tracking-wide);
  color: var(--fg-default);
  background: var(--bg-component);
  border-radius: var(--radius-badge);  /* 2 px */
  box-shadow: var(--borders-base);     /* = l'ombre 1 (déjà comptée) */
  white-space: nowrap;
}
/* le format (EPUB / AZW3 / MOBI) est l'unique badge qui porte l'accent :
   texte --fg-accent sur fond NEUTRE (--bg-subtle) — pas de fond teinté. */
.badges .badge.fmt {
  color: var(--fg-accent);
  background: var(--bg-subtle);
}

/* --- Action (pied de carte) -------------------------------------------------- */
.sendcol {
  padding: var(--space-3) var(--space-4) var(--space-4);  /* 8 12 12 — grille 4 */
}
.sendcol .btn {
  width: 100%;                    /* le bouton d'envoi remplit la carte */
}

/* --- État chargement : couverture en attente --------------------------------- */
/* La carte garde sa taille exacte (couverture ratio fixe) → aucun saut de
   mise en page. Le contenu (titre/auteur/méta/action) reste présent et
   lisible : seul le CHARGEMENT de la couverture est en attente.
   Contrat : aria-label (SEULEMENT dans cet état) posé par l'appelant. */
.card.is-loading {
  /* fond/ombre de repos (borders-base) — pas d'ombre nouvelle */
}
/* Shimmer : une bande discrète balaye la zone couverture. background-image
   (PAS backgroundColor → hors du compteur « fonds non neutres »), dérivée
   des tokens (color-mix sur --fg-muted). C'est une ANIMATION de
   background-position (hors transitions, hors plafonds de transition). */
.card.is-loading .coverwrap {
  background-image: linear-gradient(
    90deg,
    transparent 30%,
    color-mix(in srgb, var(--fg-muted) 12%, transparent) 50%,
    transparent 70%
  );
  background-size: 200% 100%;
  background-repeat: no-repeat;
  background-position: 200% 0;
  animation: card-cover-shimmer 1.4s linear infinite;
}
@keyframes card-cover-shimmer {
  to { background-position: -200% 0; }
}

/* --- État erreur : couverture indisponible → placeholder --------------------- */
/* L'anneau danger RÉUTILISE l'ombre 3 (--borders-error, déjà comptée).
   La couverture montre le placeholder (data-ph, via coverNext) et le
   message dit ce qui s'est passé ET quoi faire (02-ANTI-SLOP). Non
   coupée : le livre reste consultable, seul l'envoi de couverture a échoué. */
.card.is-error {
  box-shadow: var(--borders-error);
}
.cerror {
  font-size: var(--text-xs);      /* 12 px */
  line-height: var(--lh-compact);
  font-weight: var(--weight-regular);
  letter-spacing: var(--tracking-normal);
  color: var(--danger-11, var(--fg-default));
  margin: var(--space-2) 0 0;     /* 4 px */
}

/* --- Keyframe d'entrée (remplace l.124 de l'app) ----------------------------- */
@keyframes rise {
  from { opacity: 0; transform: translateY(var(--space-3)); }  /* 8 px, grille 4 */
  to   { opacity: 1; transform: none; }
}

/* --- Simulation d'états (page specimen uniquement) ---------------------------- */
/* Déjà déclarés plus haut comme pseudo-classes ; les classes is-* ci-dessus
   posent exactement les mêmes valeurs token par token. */

@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
    animation: none;              /* pas de montée ; la carte apparaît telle quelle */
  }
  /* Le shimmer s'arrête mais la zone couverture reste (l'état passe par
     l'aria-label + la zone neutre). */
  .card.is-loading .coverwrap {
    animation: none;
    background-image: none;
  }
}
