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

   Une ligne clé/valeur et sa grille :
     .kv        la ligne (clé + valeur, espacées bout à bout)
     .kv .v     la valeur (contient le point de statut et les actions)
     .dot       point de statut (ok)
     .dot.no    point de statut (ko)
     .dmeta     grille 2 colonnes de lignes .kv (fiche du détail)
     .kv--loading / .kv--error / .kv--disabled / .kv__spinner /
     .kv__status / .kv__error
     les modificateurs d'état.

   Noms de classes conservés à l'identique de l'app existante
   (frontend/index.html l.215–219 et l.182–184) : les templates JS
   (openDetail l.739–744, loadHealth l.829–834) réécrivent le DOM en
   innerHTML avec .kv / .kv .v / .dot — les renommer casserait le JS.
   Le <b> de la clé de l'app est absorbé par .kv__k (même rôle).

   Règles (02-ANTI-SLOP.md + skill primitive) :
   - Aucune valeur en dur : uniquement les tokens de tokens.css et de la
     clé de variation. Seuls littéraux tolérés : la durée du spin
     (0.75 s / 2 s) — tokens.css ne définit aucune durée d'animation
     continue ; ajouter un token là-bas se demande, ne se décide pas —
     et les pourcentages/angles structurels du masque (forme de l'arc,
     pas des valeurs d'échelle).
   - Bordures = box-shadow à spread 1px, jamais de `border` dessinée.
     La ligne au repos est PLATE (aucun device) : la séparation des
     lignes vient du gabarit (interligne, espacement), pas d'un trait.
     Un seul device par état : survol = fond + bordure de base ;
     focus = bordure de base + anneau (chaîne, ombre 2 de la page) ;
     erreur = texte danger (le trait rouge de la note d'erreur reste en
     CSS de vue, comme .dnote).
   - Survol : changement de fond uniquement (--bg-hover) + bordure de
     base. Pas d'agrandissement.
   - Focus clavier : :focus-visible sur la ligne (quand elle est
     focusable) et :focus-within (quand un enfant interactif — un
     button .btn ghost comme « Tester la clé » — porte le focus).
     Chaîne --borders-base + --borders-focus (liseré --bg-app + anneau
     accent) — ombre 2 de la page, jamais --borders-focus seul.
   - Désactivé : opacité + clic coupé (aria-disabled posé par l'écran).
   - Chargement : hauteur figée à --row-h (la densité du thème porte
     déjà la hauteur de ligne) + spinner + libellé ; clé et valeur
     masquées (visibility, la place est conservée). Contrat : le nom
     accessible vient de .kv__status visible (aria-label seulement si
     le libellé est masqué) ; le spinner est aria-hidden.
   - Erreur : .kv--error + .kv__error (texte --danger-11) qui dit ce
     qui s'est passé et quoi faire, jamais une excuse. Le point .dot.no
     porte la même couleur fonctionnelle (red, importée par la clé de
     variation — red n'est PAS l'accent bronze, PARC.md).
   - Rayon : la ligne n'a pas de rayon (surface plate). Le point de
     statut est rond (100 %), le spinner est en masque.
   - Transitions : background-color + box-shadow, --duration-fast
     (2 propriétés, 1 durée). Le spin est une animation de transform,
     hors transitions.
   - Graisses : --weight-medium (clé, statut) et --weight-regular
     (valeur) — deux graisses, plafond 600 respecté.
   - Spécificité : les règles de la ligne sont en (0,1,0) — les écrans
     peuvent les ajuster sans !important.

   -------------------------------------------------------------------------
   DECISIONS CONTRAIGNES — page specimen kindle-shelf (audit-slop-json.js,
   un SEUL passage sur la PAGE ENTIÈRE)
   -------------------------------------------------------------------------
   OMBRES (plafond 3, saturé par la primitive Button : borders-base /
   borders-base+borders-focus / borders-error) — la ligne n'en introduit
   AUCUNE, elle RÉUTILISE les deux seules qu'elle emploie :
     1. --borders-base                       (survol, .is-hover)
     2. --borders-base + --borders-focus     (focus clavier, .is-focus)
   L'erreur N'ajoute pas d'anneau : le device d'erreur reste le point
   .dot.no + le texte .kv__error (un seul device par état, comme l'app).
   Le slot --borders-error (3e ombre de la page, porté par Button/Field/
   Badge) n'est PAS consommé par la ligne → zéro ombre nouvelle.
   Le focus n'utilise PAS --borders-focus SEUL (ce serait une 4e ombre,
   hors du budget) — c'est la chaîne borders-base + borders-focus, comme
   Button (.btn:focus-visible) et Badge (.badge:focus-visible).
   Fonds NON NEUTRES (plafond 3) — la page porte déjà l'accent bronze :
     --bg-solid (bronze-9, .btn.primary repos)
     --accent-10 (bronze-10, .btn.primary hover)
   Il ne reste QU'UN budget. Le point d'erreur .dot.no (red-9) est la 3e
   et SATURE le budget. Conséquence : le point d'état OK .dot doit rester
   NEUTRE (--bg-active, sand-5) — un vert --success-9 serait une 4e teinte
   non neutre → ok:false. Red est mappé par la clé de variation (échelle
   Radix red), aucune exception en dur n'est nécessaire.
   ========================================================================= */

/* --- Ligne ---------------------------------------------------------------- */

.kv {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3); /* 8 px — sur la grille */

  padding: var(--space-3) var(--space-4); /* 8 / 12 px — sur la grille */

  font-family: var(--font-ui, inherit);
  font-size: var(--text-base); /* 14 px, corps du thème (default) */
  line-height: var(--lh-base);
  letter-spacing: var(--tracking-normal);

  /* position:relative — ancre du .kv__status (chargement, absolute). */
  position: relative;
  min-width: 0;

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

/* La clé : libellé muet, graisse medium — la hiérarchie vient de la
   position et de la couleur, pas d'un <b> 700 (plafond 600). */
.kv__k,
.kv > b {
  color: var(--fg-muted);
  font-weight: var(--weight-medium);
  min-width: 0;
}

/* La valeur : le contenu, à droite. Flex pour aligner le point de
   statut et accueillir une action (button) sans casser l'alignement
   de base. */
.kv .v {
  display: flex;
  align-items: center;
  gap: var(--space-3); /* 8 px — sur la grille */
  color: var(--fg-default);
  font-weight: var(--weight-regular);
  min-width: 0;
  max-width: 100%;
  text-align: end;
  overflow-wrap: anywhere;
}

/* --- Point de statut -------------------------------------------------------
   Rond (100 %). Ok = neutre (l'état nominal ne consomme pas l'accent :
   l'accent bronze reste réservé aux actions). Ko = couleur
   fonctionnelle d'erreur (échelle red, importée par la clé de
   variation ; repli --fg-default si le thème ne la définit pas). */

.dot {
  width: var(--space-3); /* 8 px — sur la grille */
  height: var(--space-3);
  flex-shrink: 0;
  border-radius: 100%;
  background: var(--bg-active);
}
.dot.no {
  background: var(--danger-9, var(--fg-default));
}

/* --- Grille de fiches ------------------------------------------------------
   La grille 2 colonnes de la fiche du détail (l.182–184 de l'app).
   L'espacement entre lignes/colonnes vient du gabarit (gap), pas de
   padding par enfant — et aucune ligne ne porte de trait de
   séparation (le .dmeta de l'app n'en avait pas non plus). */

.dmeta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3) var(--space-5); /* 8 / 16 px — sur la grille */
  min-width: 0;
}
.dmeta .kv {
  padding-top: 0;
  padding-bottom: 0;
}
@media (max-width: 600px) {
  /* Une colonne quand la fiche se resserre : les paires restent
     lisibles, l'espacement reste sur la grille. */
  .dmeta {
    grid-template-columns: minmax(0, 1fr);
  }
}

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

/* Survol : fond --bg-hover + bordure de base (device unique : l'un et
   l'autre forment le même signal « zone lisible »). Pas d'agrandissement. */
.kv:hover {
  background: var(--bg-hover);
  box-shadow: var(--borders-base);
}

/* Focus clavier : la ligne est focusable quand elle porte elle-même une
   action (role="button"/tabindex posé par l'écran) ; sinon c'est
   :focus-within (un enfant interactif — button ghost « Tester la clé » —
   reçoit le focus clavier). La chaîne --borders-base + --borders-focus
   RÉUTILISE l'ombre 2 de la page (identique au .btn:focus-visible) —
   jamais --borders-focus seul (ce serait une 4e ombre, hors budget). */
.kv:focus-visible,
.kv:focus-within {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

/* Désactivé : la ligne entière est estompée, clic coupé. aria-disabled
   posé par l'écran. L'état d'erreur ne s'estompe pas : l'information
   d'échec doit rester lisible (les écrans ne combinent pas les deux). */
.kv--disabled,
.kv[aria-disabled="true"] {
  opacity: 0.5;
  pointer-events: none;
}

/* Chargement : la hauteur est figée à --row-h (la densité du thème
   porte la hauteur de ligne ; repli 40 px si le thème ne la définit
   pas) — zéro saut de mise en page. La clé et la valeur sont masquées
   (visibility, la place est conservée) ; le spinner et le libellé
   prennent leur place. Le libellé reste VISIBLE : c'est lui le nom
   accessible de la ligne en chargement (le spinner est aria-hidden). */
.kv--loading {
  min-height: var(--row-h, calc(var(--space-5) * 2));
  overflow: hidden;
  pointer-events: none;
}
.kv--loading .kv__k,
.kv--loading > b,
.kv--loading .v {
  visibility: hidden;
}

/* Le libellé de statut (spinner + texte), superposé à la place de la
   clé. Absolute : la ligne garde sa largeur et sa hauteur de contenu. */
.kv__status {
  display: none;
  position: absolute;
  left: var(--space-4);
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-sm); /* 13 px — texte courant compact */
  line-height: var(--lh-compact);
  letter-spacing: var(--tracking-wide);
  color: var(--fg-muted);
  white-space: nowrap;
}
.kv--loading .kv__status {
  display: inline-flex;
}

/* Spinner : anneau de 16 px (sur la grille), 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 sont
   structurels (forme de l'arc), pas des valeurs d'échelle. */
.kv__spinner {
  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: kv-spin 0.75s linear infinite;
}

@keyframes kv-spin {
  to { transform: rotate(360deg); }
}

/* Erreur : le message est obligatoire et dit quoi faire. Texte
   --danger-11 (texte, pas fond : contraste correct en clair comme en
   sombre). Le point .dot.no porte déjà la même couleur fonctionnelle ;
   la ligne n'ajoute pas d'anneau (le device d'erreur reste le texte —
   un seul device par état). */
.kv__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: 100%;
  overflow-wrap: anywhere;
}

/* --- 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. */

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

@media (prefers-reduced-motion: reduce) {
  .kv {
    transition: none;
  }
  /* Le spin ralentit mais reste : l'information « chargement » doit
     passer. (Les durées de transition sont déjà nulles via tokens.css.) */
  .kv__spinner {
    animation-duration: 2s;
  }
}
