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

   La racine de l'app : la barre supérieure (header.top, .logo, #nav) et la
   barre mobile dupliquée (.bnav, < 760 px), plus la transition des vues
   (keyframe fade). Le contrat du markup (frontend/index.html) :
     <header class="top">
       <div class="logo"><svg><use href="#i-ray"/></svg>kindle-<em>shelf</em></div>
       <nav id="nav">
         <button data-view="search" class="on"><svg class="ic"><use href="#i-search"/></svg><span>Recherche</span></button>
         <button data-view="history">…</button>
         <button data-view="profile">…</button>
       </nav>
     </header>
     <nav class="bnav" id="bnav"> …mêmes 3 boutons dupliqués… </nav>
   (header l.313–320, .bnav l.400–404, CSS l.49–68 et l.272–293, keyframe
   fade l.71–73). switchView (l.441–447) bascule .on sur les boutons ET les
   .view par data-view — les noms .on, data-view, #nav, .bnav et fade
   doivent survivre. Le markup est inchangé.

   HOOK DES BOUTONS : [data-view], pas #nav button. Les 6 boutons de nav
   (3 × #nav + 3 × .bnav) sont les SEULES occurrences de data-view dans
   l'app (vérifié) — c'est l'attribut du contrat, posé et lu par switchView
   (l.443, l.448). Styler [data-view] couvre les DEUX navs par une seule
   règle et reste ancré au markup de l'app ; le spécimen peut montrer les
   états hors du header de démo (les boutons portent data-view, comme
   l'app). Décision documentée : le hook est l'attribut, pas l'id.

   États :
     .on            bouton de la vue active (l'app le pose, l.443)
     :hover         survol
     :focus-visible focus clavier — VISIBLE (correction de l'app : l.61–68 et
                    l.285–288 n'avaient AUCUN focus visible — border
                    transparent, rien d'autre)
     :disabled      bouton coupé (vue indisponible)
     .nav-btn--loading  libellé en cours de résolution (chargement i18n /
                    vues) : spinner dans le slot d'icône, largeur figée
     .nav-btn--error    vue indisponible : anneau --borders-error + libellé
                    --danger-11 qui dit ce qu'il s'est passé
   Contrat : aria-label SEULEMENT en chargement (les autres états ont déjà
   un nom accessible : le libellé visible). Les classes is-* / --loading /
   --error sont livrées comme les primitives précédentes (Button, Chip,
   Tag) ; l'app ne les pose pas encore.

   Règles (02-ANTI-SLOP.md + skill primitive) :
   - Aucune valeur en dur, ZÉRO exception : la clé de variation
     (themes/kindle-shelf.css) importe et mappe l'échelle red
     (--danger-9/10/11 existent) — l'état d'erreur ne demande aucune valeur
     littérale. Zéro hex en dur dans ce fichier.
   - Bordures = box-shadow à spread 1px (jamais de `border` dessinée) : la
     ligne 1px solid de l'app (l.55, l.282) devient --borders-base ; la
     bordure 1px solid line-2 du bouton .on (l.68) devient le fond plein
     --bg-solid (un seul endroit audacieux, comme .chip.on).
   - Rayons : --radius-field (4px) sur les boutons (l'app : 9px, hors
     échelle fermée — 4px est la valeur la plus proche, « la plus
     contrainte ») ; zéro rayon ailleurs.
   - Le focus clavier de l'app est invisible (l.61–68, l.285–288) :
     CORRIGÉ ici par :focus-visible — la chaîne
     --borders-base + --borders-focus (liseré --bg-app + anneau accent
     60 %), jamais :focus seul, jamais d'anneau au clic souris.
   - Typo : --text-base (14px) sur les boutons #nav (l'app : 14px, sur
     échelle) ; --text-xs (12px) sur .bnav (l'app : 11px, hors échelle).
     Le logo : le 21px serif de l'app (l.57) est doublement hors contrat
     (hors échelle fermée ; la clé kindle-shelf n'a pas de --font-serif) →
     --text-md (16px) + --weight-strong : la hiérarchie vient de la taille
     et de la graisse, pas d'une famille inventée.
   - Graisse : --weight-medium (500) sur les boutons, --weight-strong
     (600) sur le logo — dans le budget de 3 de la page (400/500/600).
   - Transitions : background-color + box-shadow, --duration-fast (le même
     jeu que Button/Chip — rien de neuf sur la page). La keyframe fade est
     une ANIMATION (hors des plafonds de transition) : --duration-normal
     (300ms ; l'app : 250ms hors de l'échelle 75/150/300 — la valeur la
     plus proche) ; elle tombe à 0 sous prefers-reduced-motion
     (tokens.css).
   - prefers-reduced-motion : transitions coupées, fade annulée, spin
     ralenti (l'information « chargement » doit passer).
   - Ordre de chargement : nav.css SE charge APRÈS le bloc <style> de
     l'app — `svg.ic` de l'app (l.46, 18px, spécificité 0,1,1) et
     `[data-view] .ic` ici (16px, 0,1,1) sont à égalité : la feuille la
     plus tardive gagne.

   -------------------------------------------------------------------------
   DÉCISIONS CONTRAIGNANTES SPÉCIFIQUES À kindle-shelf (page specimen
   entière, audit-slop-json.js, un SEUL passage)
   -------------------------------------------------------------------------
   Le plafond « 3 ombres distinctes » est SATURÉ par la primitive Button :
       1. --borders-base                    (repos)
       2. --borders-base + --borders-focus  (focus clavier)
       3. --borders-error                   (erreur)
   La nav NE PEUT PAS introduire d'ombre nouvelle (pas d'élévation, pas
   d'ombre littérale pour la ligne du header). Elle réutilise EXACTEMENT
   ces trois :
     - header.top / .bnav / bouton au repos / .on / survol / désactivé /
       chargement : --borders-base (ombre 1) — la ligne 1px de l'app et la
       bordure du bouton .on en deviennent la box-shadow de spread 1px
     - focus clavier : ombre 2 (chaîne — jamais --borders-focus SEUL, qui
       serait une 4e ombre distincte)
     - erreur : --borders-error (ombre 3)
   Le SURVOL ne peut pas changer l'ombre (budget saturé) : il passe par le
   FOND SEUL (--bg-hover / --accent-10), pas d'agrandissement.
   FONDS NON NEUTRES (plafond 3, SATURÉ : bronze-9 + bronze-10 du bouton
   primaire + red-9) : le bouton .on consomme --bg-solid (bronze-9) et son
   survol --accent-10 (bronze-10) — les DEUX teintes déjà comptées par
   .btn.primary / .chip.on, aucune teinte nouvelle. Le header et .bnav sont
   NEUTRES (--bg-app, --bg-component). Le spinner de chargement est
   currentColor neutre. Le <em> du logo et l'icône .logo svg sont des
   COULEURS DE TEXTE/TRAIT (--fg-accent), hors du compteur de fonds.
   ========================================================================= */

/* --- Header (barre supérieure) --------------------------------------------
   Remplace header.top de l'app (l.49–56). sticky (l.50) ; padding sur la
   grille (l'app : 14/26 px, hors grille → 16/24 px). La ligne 1px solid de
   l'app devient --borders-base (l'ombre 1 de la page, déjà comptée —
   l'approche du design system, pas une ombre nouvelle). Fond NEUTRE
   --bg-app + blur (l'app : rgba(27,25,22,.92) + backdrop-filter — le fond
   teinté de l'app est absorbé par le token neutre ; le blur est un
   traitement de surface, pas une couleur). */
header.top {
  position: sticky;
  top: 0;
  z-index: 20;

  display: flex;
  align-items: center;
  gap: var(--space-5);          /* logo ↔ nav : 16 px (l'app : 18px, hors grille) */

  padding: var(--space-4) var(--space-6); /* 16/24 px, grille (l'app : 14/26) */

  background: var(--bg-app);
  backdrop-filter: blur(var(--space-2));
  -webkit-backdrop-filter: blur(var(--space-2));
  box-shadow: var(--borders-base); /* = l'ombre 1 de la page (déjà comptée) */
}

/* --- Logo -------------------------------------------------------------------
   Remplace .logo de l'app (l.57–59). L'accent reste porté par le TEXTE du
   <em> et le TRAIT de l'icône (--fg-accent), jamais par un fond (budget de
   fonds non neutres saturé, voir en-tête). */
.logo {
  display: flex;
  align-items: center;
  gap: var(--space-3);          /* icône ↔ texte : 8 px (l'app : 10px) */

  font-family: var(--font-ui, inherit);
  font-size: var(--text-md);    /* 16 px (l'app : 21px serif, hors échelle) */
  line-height: var(--lh-md);
  font-weight: var(--weight-strong);
  letter-spacing: var(--tracking-normal);

  color: var(--fg-default);
  white-space: nowrap;
}
.logo svg {
  width: var(--space-5);        /* 20 px — sur la grille (l'app : 26px) */
  height: var(--space-5);
  flex: none;
  fill: none;
  stroke: var(--fg-accent);     /* l'accent du logo est porté par le trait */
  stroke-width: 1.5;            /* trait fin, comme l'app (l.58) */
  stroke-linecap: round;
  stroke-linejoin: round;
}
.logo em {
  font-style: normal;
  color: var(--fg-accent);
}

/* --- Conteneur de nav (#nav) ---------------------------------------------------
   L'app (l.60) : flex, gap 4px, poussé à droite. Sur la grille. */
#nav {
  display: flex;
  align-items: center;
  gap: var(--space-2);          /* boutons : 4 px (l'app : 4px) */
  margin-left: auto;
}

/* --- Boutons de navigation ([data-view]) ----------------------------------------
   Remplace #nav button de l'app (l.61–68) ET .bnav button (l.285–288) par
   une seule règle — les 6 boutons de l'app sont les seuls porteurs de
   data-view (voir en-tête). La bordure 1px transparent de l'app (et 1px
   solid line-2 en .on) est RETIRÉE : remplacée par une box-shadow de
   spread 1px (l'approche du design system : la boîte ne bouge pas et
   l'anneau de focus se combine). Padding sur la grille (l'app : 8/14 px →
   8/16 px). Texte --fg-muted : la rangée n'est pas une action, la vue
   active porte l'accent. */
[data-view] {
  display: flex;
  align-items: center;
  gap: var(--space-3);          /* icône ↔ libellé : 8 px (l'app : 8px) */

  padding: var(--space-3) var(--space-4); /* 8/16 px, grille (l'app : 8/14) */

  font-family: var(--font-ui, inherit);
  font-size: var(--text-base);  /* 14 px (l'app #nav : 14px, sur échelle) */
  line-height: var(--lh-base);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-wide);

  color: var(--fg-muted);
  background: transparent;      /* l'app : none — le fond ne change qu'en .on */

  border: 0;
  border-radius: var(--radius-field); /* 4 px (l'app : 9px, hors échelle) */
  box-shadow: var(--borders-base);    /* = l'ombre 1 (la 1px de l'app) */
  cursor: pointer;
  white-space: nowrap;

  transition:
    background-color var(--duration-fast) var(--ease),
    box-shadow var(--duration-fast) var(--ease);
}
[data-view] .ic {
  width: var(--space-4);        /* 16 px — sur la grille (l'app : 18px) */
  height: var(--space-4);
  flex: none;
  fill: none;
  stroke: currentColor;         /* suit l'état du texte (muted / default / on) */
  stroke-width: 1.5;            /* trait fin, comme l'app (l.46) */
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- Bouton actif (.on) ---------------------------------------------------------
   La vue en cours : fond plein --bg-solid, texte sur solide. L'app avait
   contour line-2 + fond panel + texte accent-2 (l.68) — remplacé par un
   fond plein (un seul endroit audacieux, comme .chip.on) : consomme les
   deux teintes bronze DÉJÀ comptées par .btn.primary / .chip.on. */
[data-view].on {
  color: var(--fg-on-solid);
  background: var(--bg-solid);  /* bronze-9 — teinte déjà comptée sur la page */
}

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

/* Survol : fond --bg-hover SEUL pour le bouton inactif (pas d'ombre nouvelle
   — budget saturé, voir en-tête), pas d'agrandissement. Le bouton actif
   monte d'une teinte bronze (bronze-10), comme .btn.primary:hover. Coupé sur
   le bouton désactivé. */
[data-view]:hover:not(:disabled) {
  color: var(--fg-default);
  background: var(--bg-hover);
}
[data-view].on:hover:not(:disabled) {
  background: var(--accent-10); /* bronze-10 — teinte déjà comptée */
}

/* Focus clavier : :focus-visible uniquement — jamais d'anneau au clic souris.
   C'est la CORRECTION du focus invisible de l'app (l.61–68, l.285–288) :
   chaîne --borders-base + --borders-focus (liseré --bg-app + anneau accent
   60 %) — RÉUTILISE l'ombre 2 de la page (jamais --borders-focus seul : ce
   serait une 4e ombre distincte). --borders-focus est redéclaré dans le
   bloc [data-theme="dark"] de tokens.css → le liseré suit le panneau sombre
   du specimen. Couvre les DEUX navs (#nav et .bnav) par la même chaîne. */
[data-view]:focus-visible {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

/* --- Désactivé -------------------------------------------------------------------
   Une vue indisponible (service hors ligne) : estompée, curseur de refus.
   :disabled natif (l'app le posera ; le specimen l'utilise tel quel). Pas de
   pointer-events (precedent Button/Chip) : la souris pointe l'état. */
[data-view]:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* --- Chargement ---------------------------------------------------------------------
   Le libellé se résout (chargement i18n / vues) : un spinner occupe le slot
   d'icône (place figée 16px ; le spinner 12px y est centré, (16−12)/2 = 2px
   sur la grille) et le libellé reste visible : zéro saut de mise en page.
   Clic coupé (rien à cliquer tant que le libellé n'existe pas).
   aria-label posé par l'appelant, UNIQUEMENT dans cet état (contrat). */
[data-view].nav-btn--loading {
  pointer-events: none;
}
[data-view].nav-btn--loading > .ic {
  display: none;
}

/* Spinner de chargement : anneau en MASQUE (radial + conic), fond
   currentColor neutre (un spinner d'état n'est pas un accent), 3 couches en
   intersection — même structure que .chip__spinner / .toast__spinner.
   12 px (--space-4) dans le slot d'icône de 16 px ; le slot est figé →
   zéro saut. borderRadius: 0 (le masque fait le cercle) → zéro rayon
   ajouté. Le masque ne lit que le canal alpha : `black` y est structurel,
   pas une couleur. Les angles sont structurels (forme de l'arc). */
.nav-btn__spinner {
  display: none;
  width: var(--space-4);        /* 12 px — sur la grille (l'app : 13px, .spin) */
  height: var(--space-4);
  flex: none;
  align-self: center;
  background: currentColor;
  -webkit-mask-image:
    radial-gradient(farthest-side, transparent calc(100% - var(--space-1)), 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-1)), black 0),
    radial-gradient(closest-side, black 99%, transparent 100%),
    conic-gradient(black 270deg, transparent 0deg);
  mask-composite: intersect, intersect;
}
[data-view].nav-btn--loading > .nav-btn__spinner {
  display: block;
  animation: nav-spin 0.75s linear infinite;
}
@keyframes nav-spin {
  to { transform: rotate(360deg); }
}

/* --- Erreur --------------------------------------------------------------------------
   Une vue n'a pas pu être chargée (ex. profil indisponible) : anneau
   --borders-error (RÉUTILISE l'ombre 3 de la page, déjà comptée) + libellé
   --danger-11 (l'étape lisible en sombre FIXE, comme chip.css / panel.css).
   Non cliquable — le correctif est dans le message voisin (le bouton dit ce
   qui s'est passé, le message dit quoi faire). */
[data-view].nav-btn--error {
  color: var(--danger-11, var(--fg-default));
  box-shadow: var(--borders-error);
  pointer-events: none;
}

/* --- Barre mobile (.bnav) ---------------------------------------------------------------
   Remplace .bnav de l'app (l.272–293) : dupliquée en bas d'écran sous
   760 px. Le 760 px de l'app n'est pas sur une échelle fermée de points de
   rupture — c'est le point de rupture EXISTANT de l'app, conservé tel quel
   (structurel, pas une valeur d'échelle). Fond NEUTRE --bg-component (l'app
   : rgba(27,25,22,.97) — absorbé par le token). La ligne 1px de l'app
   (border-top) devient --borders-base (l'ombre 1, déjà comptée).
   safe-area-inset-bottom : inset système (l.283), jamais mesuré. */
.bnav {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 30;

  display: none; /* l'app : display none, flex sous 760 px (l.273/281) */

  background: var(--bg-component);
  box-shadow: var(--borders-base);
  padding: var(--space-2) var(--space-1) calc(var(--space-2) + env(safe-area-inset-bottom));
}

/* Les boutons de la barre mobile : colonne (icône au-dessus du libellé),
   12px, pleine largeur — override du bouton de base (spécificité 0,2,0 >
   0,1,0). Le 11px de l'app est hors échelle → --text-xs. */
.bnav [data-view] {
  flex: 1;
  flex-direction: column;
  gap: var(--space-2);          /* icône ↔ libellé : 4 px (l'app : 3px) */
  padding: var(--space-3) 0;    /* 8px vertical, sur la grille (l'app : 6px) */
  font-size: var(--text-xs);    /* 12 px (l'app : 11px, hors échelle) */
  line-height: var(--lh-xs);
}

/* --- Point de rupture (l'app : 760 px, conservé tel quel) ---------------------------
   #nav passe, .bnav prend le relais — les 3 boutons de chacune sont
   dupliqués dans le markup et switchView bascule .on sur les deux en même
   temps (l.443). */
@media (max-width: 760px) {
  header.top {
    padding: var(--space-3) var(--space-4); /* 12/16 px, grille (l'app : 12/16) */
  }
  #nav {
    display: none;
  }
  .bnav {
    display: flex;
  }
}

/* --- Transition des vues (keyframe fade) ----------------------------------------------
   Contrat de l'app (l.71–73) : .view { display:none } / .view.on {
   display:block; animation: fade .25s ease } — le nom fade et le sélecteur
   .view.on doivent survivre (switchView bascule .on sur les .view).
   Durée --duration-normal (300ms ; l'app : 250ms hors de l'échelle
   75/150/300 — la valeur la plus proche) : ANIMATION, hors des plafonds de
   transition, et elle tombe à 0 sous prefers-reduced-motion (tokens.css). */
.view {
  display: none;
}
.view.on {
  display: block;
  animation: fade var(--duration-normal) var(--ease);
}
@keyframes fade {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 1; transform: none; }
}

/* --- 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.
   Spécificité identique au hover réel (0,2,1 / 0,4,1) : .on.is-hover gagne
   sur :hover, comme dans l'app. */
[data-view].is-hover {
  color: var(--fg-default);
  background: var(--bg-hover);
}
[data-view].on.is-hover {
  background: var(--accent-10);
}
[data-view].is-focus {
  outline: none;
  box-shadow: var(--borders-base), var(--borders-focus);
}

/* --- prefers-reduced-motion -----------------------------------------------------------------
   Transitions coupées, fade annulée (--duration-* tombe aussi à 0 via
   tokens.css), spin ralenti (l'information « chargement » doit passer). */
@media (prefers-reduced-motion: reduce) {
  [data-view] {
    transition: none;
  }
  .view.on {
    animation: none;
  }
  [data-view].nav-btn--loading > .nav-btn__spinner {
    animation-duration: 2s;
  }
}
