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

   Le Panel est le regroupement-réel d'une section d'écran (titre + sous-titre
   + corps) : brique des vues Historique et Profil. Il remplace les
   `.panel` / `.panel h2` / `.panel .sub` de frontend/index.html (l.200–202)
   : 4 usages — historique (l.358), profil (l.367, l.380, l.391).

   Contrat de la primitive (selon la tâche) :
     .panel     l'enveloppe (section réelle, jamais un conteneur générique —
                02-ANTI-SLOP.md « Surfaces »)
     .panel h2  le titre (l'app écrit <h2 data-i18n="…"></h2>)
     .panel .sub  la sous-ligne (l'app écrit <p class="sub">…</p>)
   Le contenu (hist, krow, steps, health) est du DOM de vue : la primitive
   fournit la grille du corps (flex column, gap --space-4) et le style des
   états ; elle ne réécrit pas les enfants des vues.

   Règles (02-ANTI-SLOP.md + skill primitive) :
   - Aucune valeur en dur : uniquement les tokens de tokens.css et de la clé
     de variation (themes/kindle-shelf.css, échelles sand/bronze sombres).
     Seuls littéraux tolérés : les durées du spin (0.75 s / 2 s) et les
     pourcentages/angles structurels du masque — tokens.css ne définit
     aucune durée d'animation continue ; ajouter un token là-bas se
     demande, ne se décide pas.
   - Bordures = box-shadow (jamais de `border` dessinée) : la boîte ne
     bouge pas et les ombres se combinent.
   - Focus clavier : le Panel n'est jamais focusable — le focus visible est
     celui d'un contrôle À L'INTÉRIEUR (:focus-visible descendant, anneau
     du contrôle lui-même) + la chaîne --borders-base, --borders-focus sur
     l'enveloppe en :focus-within (couvre aussi le focus programmé du
     login, focus() après 50 ms).
   - Désactivé : opacité + clic coupé, drivé par l'attribut ARIA
     (aria-disabled posé par l'appelant), comme la Card.
   - Chargement : la place est conservée (visibility:hidden sur le titre,
     squelettes .panel__sk visibles, min-height figée) → zéro saut de mise
     en page. Le libellé de statut (spinner + texte) vit dans la
     sous-ligne, comme #hist-status dans l'app : c'est lui le nom
     accessible. Contrat : le Panel porte aria-busy + aria-label
     (le libellé de chargement) UNIQUEMENT dans cet état — retiré au
     retour au repos.
   - Erreur : l'anneau --borders-error sur l'enveloppe + un message
     .panel__error (role="alert") qui dit ce qui s'est passé et quoi
     faire, jamais une excuse (02-ANTI-SLOP.md « Contenu »).
   - Espacement : paddings, gap et min-height sur la grille 4px
     (--space-*). L'espacement ENTRE Panels est posé par le parent
     (flex column + gap) — jamais de margin-top sur le Panel lui-même.
     (Les .sk de l'app font 72 px en inline : 72 n'existe pas dans
     l'échelle fermée --space-* — la primitive ne peut pas le consommer.
     Ses squelettes .panel__sk font --space-9 = 40 px ; c'est la
     silhouette de la primitive, pas une réécriture de .sk.)

   -------------------------------------------------------------------------
   DÉCISIONS CONTRAIGNANTES — page specimen kindle-shelf (audit-slop-json.js,
   un SEUL passage sur la PAGE ENTIÈRE)
   -------------------------------------------------------------------------
   OMBRES (plafond 3, 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)
   Le Panel n'en introduit AUCUNE :
     - repos / désactivé / chargement / vide : --borders-base
     - focus clavier (.panel:focus-within) : --borders-base,
       --borders-focus  (chaîne — jamais --borders-focus SEUL, ce serait
       une 4e ombre)
     - erreur : --borders-error
   Conséquence documentée : --elevation-card-rest / --elevation-card-hover
   sont ILLICITES dans la page specimen (valeurs distinctes → ok:false) —
   c'est la même décision que primitives/card.css. Le Panel n'a donc PAS de
   SURVOL (il est un regroupement passif, pas un contrôle : rien n'est
   survolé dans l'app ; le survol des lignes et des boutons est porté par
   leurs propres primitives KVRow/Badge/Button). Le « lift » n'existe pas —
   le signal de section vient du fond + du titre, pas d'une ombre de survol.
   FONDS NON NEUTRES (plafond 3, SATURÉ : bronze-9 + bronze-10 du bouton
   primaire + red-9 du point .dot.no) : le Panel n'ajoute RIEN — son fond
   est --bg-subtle (sand-2, neutre), l'anneau de focus est une OMBRE (hors
   du compteur de fonds). Aucune exception --success-9 n'est nécessaire :
   kindle-shelf mappe bien --danger-* (échelle Radix red importée par la clé
   de variation — voir themes/kindle-shelf.css), donc var(--danger-9) /
   var(--danger-11) se résolvent sans fallback hex.
   ========================================================================= */

/* --- Enveloppe ------------------------------------------------------------- */
/* Remplace l.200 de l'app : fond --panel + border 1px + rayon 12 + padding
   20/22 → fond --bg-subtle + box-shadow --borders-base + rayon --radius-panel
   + padding --space-6. */
.panel {
  display: block; /* la structure de l'app est block (.sk grid, .krow flex…) */
  min-width: 0;
  position: relative; /* ancre du libellé de chargement */

  background: var(--bg-subtle);
  color: var(--fg-default);
  border-radius: var(--radius-panel);
  box-shadow: var(--borders-base);
  padding: var(--space-6);

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

/* --- En-tête ---------------------------------------------------------------
   Remplace l.201 de l'app (serif 19 / 600). La police du titre est
   --font-ui (Instrument Sans) : la clé de variation kindle-shelf ne
   définit pas de --font-serif ; la hiérarchie vient de la taille +
   la graisse (600, plafond) + l'espace, pas de la famille (02-ANTI-SLOP
   « hiérarchie »). */
.panel h2 {
  font-family: var(--font-ui);
  font-size: var(--text-md);       /* 16 px */
  line-height: var(--lh-md);
  font-weight: var(--weight-strong); /* 600 — plafond */
  letter-spacing: var(--tracking-normal);
  color: var(--fg-default);
  margin: 0 0 var(--space-2);      /* 4 px */
}
/* Remplace l.202 de l'app (--text-3 13px, margin-bottom 16) : même taille,
   --fg-muted, espace sur la grille. */
.panel .sub {
  font-size: var(--text-sm);       /* 13 px */
  line-height: var(--lh-compact);
  letter-spacing: var(--tracking-wide);
  color: var(--fg-muted);
  max-width: 72ch;
  margin: 0 0 var(--space-4);      /* 16 px */
}

/* --- Corps -----------------------------------------------------------------
   Grille du corps : stack, l'espacement entre blocs internes est un gap,
   pas des margins. Les enfants directs gardent leurs margins de vue
   (l'app pose margin-top inline entre ses panneaux : l.380, l.391) — la
   primitive n'écrase pas la mise en page existante. */
.panel__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);             /* 16 px */
  min-width: 0;
}

/* --- État vide ---------------------------------------------------------------
   « Un écran vide est une invitation à agir, pas une illustration avec
   Rien à afficher » (02-ANTI-SLOP.md) : le message dit quoi faire.
   min-height = 2 × --space-9 = 80 px (grille 4) : une place de corps
   raisonnable, le passage vide ↔ contenu ne saute pas. */
.panel__empty {
  font-size: var(--text-base);     /* 14 px */
  line-height: var(--lh-compact);
  color: var(--fg-muted);
  max-width: 72ch;
  min-height: calc(var(--space-9) * 2); /* 80 px — sur la grille */
  margin: 0;
}

/* --- État chargement -----------------------------------------------------------
   La place est conservée : le titre est masqué en visibility (pas
   display:none) ; la sous-ligne porte le libellé de statut (spinner +
   texte, comme #hist-status dans l'app) et les squelettes .panel__sk
   restent visibles — silhouette du corps en attente. aria-busy +
   aria-label (le libellé de chargement) sont posés par l'appelant — le
   SEUL moment où le Panel porte un aria-label, retiré au retour au repos.
   min-height = 2 × 40 (squelettes) + 4 (gap) = 84 px, sur la grille :
   le passage chargement ↔ contenu ne fait pas sauter la vue. */
.panel--loading {
  min-height: calc(var(--space-9) * 2 + var(--space-2)); /* 84 px */
}
.panel--loading h2 {
  visibility: hidden;
}
/* Libellé de statut (spinner + texte) : il VIT dans la sous-ligne, comme
   #hist-status dans l'app (« <spinner> chargement… ») — aucune règle
   d'absolue, le layout du Panel ne change pas. */
.panel__status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);             /* 8 px */
  font-size: inherit;
  letter-spacing: inherit;
  color: inherit;
}
.panel__status .panel__spinner {
  color: currentColor;
}

/* Squelette de chargement : 40 px de haut (--space-9, grille 4), fond
   NEUTRE (--bg-component) bandé par un linear-gradient (background-IMAGE —
   hors du compteur « fonds non neutres »), dérivé des tokens. Statique :
   le signal d'attente est déjà porté par le libellé de statut — pas
   d'animation en plus. */
.panel__sk {
  height: var(--space-9);          /* 40 px — sur la grille */
  border-radius: var(--radius-panel);
  background-image: linear-gradient(
    100deg,
    var(--bg-component) 40%,
    var(--bg-hover) 50%,
    var(--bg-component) 60%
  );
  background-size: 200% 100%;
  background-position: 70% 0;
}
/* Squelettes empilés : l'écart vient de la primitive (4 px, grille), pas
   d'un margin par écran. */
.panel__sk + .panel__sk {
  margin-top: var(--space-2); /* 4 px */
}

/* Spinner : anneau de 16 px (grille 4), trait de 4 px, dessiné par masque
   (pas de `border`), fond currentColor. Trois couches en intersection :
   donut (trou central), disque plein (coupe les coins), arc de 270° (conic)
   — 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/centres sont structurels (forme de l'arc),
   pas des valeurs d'échelle. */
.panel__spinner {
  width: var(--space-5);           /* 16 px — sur la grille */
  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: panel-spin 0.75s linear infinite;
}
@keyframes panel-spin {
  to { transform: rotate(360deg); }
}

/* --- État désactivé -------------------------------------------------------------
   Estompé, clic coupé. Drivé par l'attribut ARIA (aria-disabled posé par
   l'appelant), comme la Card. L'information reste lisible : on estompé,
   on ne masque pas. */
.panel[aria-disabled="true"] {
  opacity: 0.5;
  pointer-events: none;
}

/* --- État erreur ------------------------------------------------------------------
   L'anneau RÉUTILISE l'ombre 3 (--borders-error, déjà comptée par
   Button/Field/Badge/Card). Le message est OBLIGATOIRE : il dit ce qui s'est
   passé et quoi faire — jamais une excuse, jamais un vague « une erreur
   s'est produite ». */
.panel--error {
  box-shadow: var(--borders-error);
}
.panel__error {
  font-size: var(--text-xs);       /* 12 px */
  line-height: var(--lh-compact);
  letter-spacing: var(--tracking-wide);
  color: var(--danger-11, var(--fg-default));
  max-width: 72ch;
  overflow-wrap: anywhere;
  margin: 0;
}
.panel__body + .panel__error,
.sub + .panel__error {
  margin-top: var(--space-4);      /* 16 px */
}

/* --- Focus clavier (enveloppe) -------------------------------------------------------
   Le Panel n'est jamais focusable : le liseré d'ACCESSIBILITÉ est celui du
   contrôle interne (:focus-visible descendant, anneau porté par sa propre
   primitive — Field : .field > input:focus-visible). L'enveloppe, elle,
   répond à :focus-within — même pour le focus clavier du contrôle interne
   ET pour le focus programmé (login : focus() après 50 ms). Chaîne
   --borders-base + --borders-focus = ombre 2 de la page (identique au
   .btn:focus-visible et au .kv:focus-within de kv-row.css) — jamais
   --borders-focus SEUL (ce serait une 4e ombre, hors budget). Pas de
   :focus-visible sur .panel : le Panel n'a pas de tabindex, il ne peut
   pas matcher. */
.panel:focus-within {
  box-shadow: var(--borders-base), var(--borders-focus);
}

/* --- Mouvement réduit --------------------------------------------------------------
   Les durées de transition sont déjà nulles via tokens.css ; on coupe en
   plus la transition locale et on ralentit le spin (l'information
   « chargement » doit passer — on ne l'éteint pas). */
@media (prefers-reduced-motion: reduce) {
  .panel {
    transition: none;
  }
  .panel__spinner {
    animation-duration: 2s;
  }
}
