/* workbench.css v0.14.0 — copie, ne pas modifier ici.
   Toute correction remonte à ~/projects/perso/workbench. */
/* ==========================================================================
   WORKBENCH — design system des apps perso
   Copié dans chaque app, jamais modifié sur place : une correction remonte au
   dépôt ~/projects/perso/workbench puis redescend par `scripts/install_css.py`.

   Pas de numéro de version ici : `install_css.py` l'estampille lui-même en
   tête du fichier copié, en la lisant dans `workbench/__init__.py` — c'est ce
   qui permet à `--check` de dire à une app qu'elle est en retard. En écrire
   une seconde ici créerait deux mentions que rien ne compare, et qui
   divergeraient silencieusement au premier oubli.

   Workbench ne définit AUCUNE teinte d'accent : c'est ce qui distingue une app des
   autres. Chaque app pose sa paire --accent / --accent-tx / --accent-soft /
   --accent-line / --ring, pour les DEUX thèmes, vérifiée au contraste :
   `python3 css/check_contrasts.py "#8c2f39" "#ffffff"`.

   Cette vérification n'est pas une formalité : l'ancien accent de recipes
   (#FEF991) tenait sur fond noir et tombait à 1,2:1 sur fond blanc.
   ========================================================================== */

/* --- Jetons : thème clair (défaut) --------------------------------------- */
:root {
  --bg: #f7f7f6;
  --surface: #ffffff;
  --surface-2: #f2f2f0;   /* zone en retrait, ligne survolée */
  --surface-hi: #ffffff;  /* élément surélevé : menu, popover */

  /* `--line` et `--line-strong` sont décoratifs (séparateurs, bord de carte).
     `--line-controle` est le seul qui identifie un contrôle : il tient 3:1
     comme l'exige WCAG 1.4.11, les deux autres n'y sont pas soumis. */
  --line: #e4e3df;
  --line-strong: #d3d2cd;
  --line-controle: #8a8880;

  --tx: #16150f;
  --tx-2: #5f5d55;
  --tx-3: #6f6d64;        /* gris de service : libellés, unités, inactifs */

  --ok-bg: #e7f4e7;   --ok-tx: #1e5320;   --ok-line: #bfe0c0;
  --warn-bg: #fdf1dd; --warn-tx: #6b4200; --warn-line: #f0dcb0;
  --err-bg: #fdecec;  --err-tx: #8c1d1d;  --err-line: #f3c9c9;
  --info-bg: #e8f0fd; --info-tx: #1a4a8c; --info-line: #c5d9f5;

  --shadow-1: 0 1px 2px rgba(20,18,10,.05), 0 1px 1px rgba(20,18,10,.04);
  --shadow-2: 0 6px 16px -6px rgba(20,18,10,.14), 0 2px 5px -2px rgba(20,18,10,.08);
  --shadow-3: 0 1px 2px rgba(20,18,10,.04), 0 12px 32px -12px rgba(20,18,10,.16);

  color-scheme: light;
}

/* --- Jetons : thème sombre ----------------------------------------------- */
/* Le sombre n'est pas une inversion : les surfaces s'ÉCLAIRCISSENT à mesure
   qu'elles s'élèvent, et l'accent se désature.
   `:root:not([data-theme="light"])` est ce qui permet à un choix « clair »
   explicite de gagner contre un système en sombre. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #0d0d0e;
    --surface: #161618;
    --surface-2: #1d1d20;
    --surface-hi: #1f1f23;

    --line: #26262a;
    --line-strong: #35353b;
    --line-controle: #66666f;

    --tx: #f2f1ee;
    --tx-2: #a3a29c;
    --tx-3: #8a8982;

    --ok-bg: #16290f;   --ok-tx: #9bd79b;   --ok-line: #274a20;
    --warn-bg: #2a1e10; --warn-tx: #f0c07a; --warn-line: #4a3616;
    --err-bg: #2c1414;  --err-tx: #f5a9a9;  --err-line: #4d2222;
    --info-bg: #101f33; --info-tx: #9dc0f0; --info-line: #1e3a5c;

    --shadow-1: 0 1px 2px rgba(0,0,0,.5);
    --shadow-2: 0 10px 26px -10px rgba(0,0,0,.75), 0 2px 6px -2px rgba(0,0,0,.5);
    --shadow-3: 0 1px 2px rgba(0,0,0,.5), 0 14px 36px -14px rgba(0,0,0,.8);

    color-scheme: dark;
  }
}

/* Choix explicite. Duplique le bloc ci-dessus : c'est le prix à payer pour
   qu'une préférence stockée l'emporte sur le réglage du système. */
:root[data-theme="dark"] {
  --bg: #0d0d0e;
  --surface: #161618;
  --surface-2: #1d1d20;
  --surface-hi: #1f1f23;

  --line: #26262a;
  --line-strong: #35353b;
  --line-controle: #66666f;

  --tx: #f2f1ee;
  --tx-2: #a3a29c;
  --tx-3: #8a8982;

  --ok-bg: #16290f;   --ok-tx: #9bd79b;   --ok-line: #274a20;
  --warn-bg: #2a1e10; --warn-tx: #f0c07a; --warn-line: #4a3616;
  --err-bg: #2c1414;  --err-tx: #f5a9a9;  --err-line: #4d2222;
  --info-bg: #101f33; --info-tx: #9dc0f0; --info-line: #1e3a5c;

  --shadow-1: 0 1px 2px rgba(0,0,0,.5);
  --shadow-2: 0 10px 26px -10px rgba(0,0,0,.75), 0 2px 6px -2px rgba(0,0,0,.5);
  --shadow-3: 0 1px 2px rgba(0,0,0,.5), 0 14px 36px -14px rgba(0,0,0,.8);

  color-scheme: dark;
}
:root[data-theme="light"] { color-scheme: light; }

/* --- Formes, espacement, mouvement, typographie --------------------------- */
:root {
  --r-s: 6px; --r-m: 10px; --r-l: 14px; --r-xl: 18px;

  /* Base de 4. Une valeur d'espacement écrite en dur est un jeton manquant. */
  --e-1: 4px; --e-2: 8px; --e-3: 12px; --e-4: 16px;
  --e-5: 24px; --e-6: 32px; --e-7: 48px;

  /* Le rythme vertical d'une PAGE, distinct des espacements de composant.
     `--e-section` sépare deux sections, `--e-bloc` sépare un titre de section
     de son contenu. Ce sont eux que la densité resserre — une app qui pose
     ses propres marges continue de marcher, mais ne suivra pas le compact. */
  --e-section: 32px;
  --e-bloc: 12px;

  /* La taille de BASE du corps de texte, distincte des 16px d'un champ de
     saisie (`--e-4` ni l'un ni l'autre : `font-size: 16px` reste écrit en
     dur sur les champs, jamais en dessous, jamais paramétré par la densité —
     c'est un plancher d'usage, pas un jeton de style). Le corps, lui, PEUT
     descendre : c'est ce que redéfinit la densité compacte. */
  --taille-corps: 15px;

  --lg-lecture: 720px;  /* contenu linéaire */
  --lg-large: 1240px;   /* grilles et tableaux */
  --lg-formulaire: 30rem; /* saisies linéaires : nom, email, mot de passe */
  /* Entre les deux : une page dont le contenu principal est un tableau avec
     une action par ligne (l'administration, au premier chef). `--lg-lecture`
     (720px) coupait les boutons « Réinitialiser » du tableau des comptes ;
     `--lg-large` (1240px) est pensé pour des grilles denses et écarte les
     colonnes au point qu'on ne relie plus un nom à son rôle. 880px laisse
     respirer quatre colonnes (Nom, Email, Rôle + bouton, Mot de passe +
     bouton) plus une colonne d'action étroite, sans dilater le reste de la
     page. */
  --lg-tableau: 880px;

  --ease: cubic-bezier(.2,.7,.3,1);
  --t: 140ms var(--ease);

  --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text",
          "Segoe UI Variable Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --serif: Georgia, "Iowan Old Style", "Times New Roman", serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono",
          "Cascadia Mono", Menlo, Consolas, monospace;

  /* --face-titre, --graisse-titre et --lettrage-titre n'ont VOLONTAIREMENT
     aucun défaut ici. `.titre-affiche` et `.bandeau .nom-app` n'ont pas la
     même allure aujourd'hui (serif/500/-.01em contre sans/640/-.02em) : une
     seule valeur par défaut ne peut pas reproduire les deux. Chacun des deux
     lit donc `var(--x, SON-PROPRE-REPLI)` — le même motif que
     `outline: var(--accent, var(--tx))` plus bas — et une ambiance qui pose
     l'une de ces variables l'impose aux deux d'un coup, sans rien dupliquer.
     Une ambiance qui ne la pose PAS (ex. `moderne` et `--face-titre`) doit le
     faire explicitement : sans déclaration nulle part, c'est le repli de
     CHAQUE élément qui s'applique, pas une valeur partagée. */
}

/* --- Halo ---------------------------------------------------------------- */
/* Ombre teintée de l'accent, posée sur ce qui doit accrocher l'œil. Dérivée de
   `--accent` : une app qui pose sa teinte reçoit son halo sans rien déclarer.

   Dosé plus bas que sur le site vitrine, et volontairement : une app consultée
   dix fois par jour n'est pas une page d'accueil. En clair le halo descend
   (une ombre portée), en sombre il rayonne (il n'y a pas de lumière au-dessus
   d'un fond noir). */
:root {
  --halo: 0 2px 12px color-mix(in srgb, var(--accent, transparent) 20%, transparent);
  --halo-2: 0 2px 12px color-mix(in srgb, var(--accent-2, transparent) 20%, transparent);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --halo: 0 0 16px color-mix(in srgb, var(--accent, transparent) 30%, transparent);
    --halo-2: 0 0 16px color-mix(in srgb, var(--accent-2, transparent) 30%, transparent);
  }
}
:root[data-theme="dark"] {
  --halo: 0 0 16px color-mix(in srgb, var(--accent, transparent) 30%, transparent);
  --halo-2: 0 0 16px color-mix(in srgb, var(--accent-2, transparent) 30%, transparent);
}

/* --- L'ACCENT est fourni par l'application ------------------------------- */
/* Socle ne définit aucune teinte : c'est ce qui distingue une app des autres.
   Chaque app pose, dans sa propre feuille et POUR LES DEUX THÈMES :

     --accent        la teinte
     --accent-tx     le texte qui se pose dessus  (≥ 4,5:1 sur --accent)
     --accent-soft   fond discret teinté
     --accent-line   filet teinté
     --ring          l'accent à ~30 % d'opacité, pour l'anneau de focus

   L'accent est une PAIRE, jamais une couleur seule. Un jaune pâle qui tient
   sur fond noir tombe à 1,2:1 dès qu'on pose un fond blanc — c'est la leçon
   qui a coûté le plus cher dans recipes. Vérifier avant de commiter :
   `python3 check_contrasts.py`.

   UN SECOND ACCENT, facultatif :

     --accent-2       la teinte secondaire
     --accent-2-tx    le texte qui se pose dessus
     --accent-2-soft  fond discret teinté
     --accent-2-line  filet teinté

   À quoi il sert : distinguer « c'est ici que vous êtes » de « ceci mérite
   votre attention ». Avec une seule teinte, l'onglet actif et le badge urgent
   ont la même couleur, et chaque app finit par bricoler la seconde dans son
   coin. Non déclaré, il retombe sur l'accent principal — une app qui n'en veut
   pas n'a rien à faire.

   Des paires prêtes et vérifiées vivent dans `css/accents/`. */
:root {
  --accent-2: var(--accent);
  --accent-2-tx: var(--accent-tx);
  --accent-2-soft: var(--accent-soft);
  --accent-2-line: var(--accent-line);
}

/* ==========================================================================
   LES AMBIANCES
   La personnalité visuelle, choisie par l'utilisateur et POSÉE PAR LE SERVEUR
   sur `:root` — jamais par un script après coup, sinon la page part dans une
   ambiance et saute dans l'autre.

   `countryside` n'a PAS de bloc ici : c'est l'existant, il vit dans `:root`.
   L'absence de l'attribut rend donc exactement le CSS d'avant, et c'est ce
   qui permet à chaque app de prendre cette version sans changer d'allure.

   AUCUNE ambiance ne redéfinit l'accent ni les couleurs d'état. L'accent
   appartient à l'app — c'est lui qui dit dans laquelle on se trouve — et un
   message d'erreur reste rouge partout, sous peine de ne plus se lire comme
   une erreur. Verrouillé par tests/test_css.py.
   ========================================================================== */

/* --- terminal : chasse fixe, angles vifs, filets francs, zéro ombre ------- */
:root[data-ambiance="terminal"] {
  /* BLANC PUR, ENCRE NOIRE. Pas de papier crème, pas de gris de surface : un
     terminal n'a qu'un fond et des caractères dessus. Les quatre surfaces
     valent donc la même chose, et TOUTE la structure passe par les filets —
     c'est ce qui rend l'ambiance radicale au lieu de décorative. */
  --bg: #ffffff; --surface: #ffffff; --surface-2: #ffffff; --surface-hi: #ffffff;
  --line: #000000; --line-strong: #000000; --line-controle: #000000;
  /* Le secondaire et le tertiaire restent des gris : du noir pur partout
     supprimerait toute hiérarchie de lecture. 10,4:1 et 7,0:1 sur blanc. */
  --tx: #000000; --tx-2: #3a3a3a; --tx-3: #565656;
  --r-s: 0; --r-m: 0; --r-l: 0; --r-xl: 0;
  --shadow-1: none; --shadow-2: none; --shadow-3: none;
  /* Redéfinit --sans, pas une règle sur `body` : les CINQ apps écrivent
     `font-family: var(--sans)` dans leur PROPRE feuille, en dehors de celle-
     ci. Une règle posée ici sur `body` ne les atteindrait jamais — le
     terminal se serait arrêté au bord du CSS de l'app. --mono garde sa
     valeur : le monospace doit rester du monospace là où il est demandé
     explicitement (`.num`, `.raccourci`…). */
  --sans: var(--mono);
  --face-titre: var(--mono);
  --graisse-titre: 700; --lettrage-titre: .01em;
}
/* Le sombre tire les gris vers le vert — l'esprit phosphore — sans toucher
   l'accent, qui reste celui de l'app. */
:root[data-ambiance="terminal"][data-theme="dark"] {
  /* NOIR PUR, symétrique du blanc pur en clair. Aucune surface surélevée :
     sur un terminal, une « carte » est un cadre, pas un aplat plus clair. */
  --bg: #000000; --surface: #000000; --surface-2: #000000; --surface-hi: #000000;
  --line: #4a4a4a; --line-strong: #6e6e6e; --line-controle: #8a8a8a;
  /* L'encre reste NEUTRE, pas verte : le vert phosphore serait un accent, et
     l'accent appartient à l'app — c'est lui qui dit dans laquelle on est.
     Un terminal ambré pour recipes, un vert pour fitness, chacun sur ce noir. */
  --tx: #ededed; --tx-2: #a6a6a6; --tx-3: #8a8a8a;
}
@media (prefers-color-scheme: dark) {
  :root[data-ambiance="terminal"]:not([data-theme="light"]) {
    --bg: #000000; --surface: #000000; --surface-2: #000000; --surface-hi: #000000;
    --line: #4a4a4a; --line-strong: #6e6e6e; --line-controle: #8a8a8a;
    --tx: #ededed; --tx-2: #a6a6a6; --tx-3: #8a8a8a;
  }
}

/* --- moderne : rayons larges, ombres portées, gris froids ---------------- */
:root[data-ambiance="moderne"] {
  --bg: #f6f8fb; --surface: #ffffff; --surface-2: #eef2f8; --surface-hi: #ffffff;
  --line: #e2e8f1; --line-strong: #cbd4e2; --line-controle: #8d97a8;
  --tx: #0f131b; --tx-2: #596375; --tx-3: #6d7688;
  --r-s: 10px; --r-m: 16px; --r-l: 22px; --r-xl: 28px;
  --shadow-1: 0 1px 3px rgba(16,24,40,.06);
  --shadow-2: 0 4px 14px -4px rgba(16,24,40,.10), 0 1px 3px rgba(16,24,40,.06);
  --shadow-3: 0 12px 32px -12px rgba(16,24,40,.18);
  /* Explicite, et pas un oubli : sans valeur par défaut au :root pour
     --face-titre, ne pas la poser laisserait `.titre-affiche` retomber sur
     SON repli à lui (`--serif`) — `moderne` se retrouverait avec des titres
     en serif, ce que rien dans sa description ne demande. */
  --face-titre: var(--sans);
  --graisse-titre: 700; --lettrage-titre: -.035em;
}
:root[data-ambiance="moderne"][data-theme="dark"] {
  --bg: #0a0d13; --surface: #141922; --surface-2: #1b212c; --surface-hi: #1f2733;
  --line: #232b38; --line-strong: #333d4e; --line-controle: #667389;
  --tx: #edf1f7; --tx-2: #9aa5b8; --tx-3: #7e8899;
  --shadow-1: 0 1px 3px rgba(0,0,0,.4);
  --shadow-2: 0 6px 20px -6px rgba(0,0,0,.6), 0 1px 3px rgba(0,0,0,.4);
  --shadow-3: 0 14px 36px -14px rgba(0,0,0,.8);
}
@media (prefers-color-scheme: dark) {
  :root[data-ambiance="moderne"]:not([data-theme="light"]) {
    --bg: #0a0d13; --surface: #141922; --surface-2: #1b212c; --surface-hi: #1f2733;
    --line: #232b38; --line-strong: #333d4e; --line-controle: #667389;
    --tx: #edf1f7; --tx-2: #9aa5b8; --tx-3: #7e8899;
    --shadow-1: 0 1px 3px rgba(0,0,0,.4);
    --shadow-2: 0 6px 20px -6px rgba(0,0,0,.6), 0 1px 3px rgba(0,0,0,.4);
    --shadow-3: 0 14px 36px -14px rgba(0,0,0,.8);
  }
}

/* --- classique : serif de titre, filets plutôt qu'ombres, gris chauds ----- */
/* La serif ne touche QUE les titres : sur un paragraphe d'app, elle fatigue. */
:root[data-ambiance="classique"] {
  --bg: #faf7f1; --surface: #fffdf8; --surface-2: #f2ede3; --surface-hi: #fffdf8;
  --line: #ddd6c7; --line-strong: #c4bbaa; --line-controle: #8b8471;
  --tx: #1d1a13; --tx-2: #5c5545; --tx-3: #6d6555;
  --r-s: 2px; --r-m: 3px; --r-l: 4px; --r-xl: 6px;
  --shadow-1: none; --shadow-2: none; --shadow-3: none;
  --face-titre: ui-serif, Georgia, "Iowan Old Style", "Times New Roman", serif;
  --graisse-titre: 600; --lettrage-titre: 0;
}
:root[data-ambiance="classique"][data-theme="dark"] {
  --bg: #14120d; --surface: #1c1913; --surface-2: #24201a; --surface-hi: #24201a;
  --line: #35301f; --line-strong: #4b4430; --line-controle: #7d745c;
  --tx: #f2ece0; --tx-2: #b3aa96; --tx-3: #968d7a;
}
@media (prefers-color-scheme: dark) {
  :root[data-ambiance="classique"]:not([data-theme="light"]) {
    --bg: #14120d; --surface: #1c1913; --surface-2: #24201a; --surface-hi: #24201a;
    --line: #35301f; --line-strong: #4b4430; --line-controle: #7d745c;
    --tx: #f2ece0; --tx-2: #b3aa96; --tx-3: #968d7a;
  }
}

/* ==========================================================================
   TERMINAL — LES CADRES EN ASCII
   Le seul endroit du fichier où une ambiance touche à autre chose qu'un jeton.
   C'est assumé : « chasse fixe et angles vifs » ne fait pas un terminal, les
   cadres en font un.

   LE HTML NE CHANGE PAS, et c'est la contrainte qui a dicté tout le dessin.
   Les cinq apps rendent déjà leurs cartes, leurs boutons et leurs badges ; une
   ambiance qui exigerait un balisage particulier ne serait plus une ambiance,
   ce serait un framework — et l'adopter demanderait de modifier cinq apps.
   Tout passe donc par `content` sur des pseudo-éléments.

   DEUX COINS, PAS QUATRE. Un élément n'a que deux pseudo-éléments ; en poser
   quatre exigerait un enfant de plus, donc du HTML. Les deux coins opposés
   suffisent à faire lire le cadre : la bordure dessine déjà les côtés, l'œil
   complète les angles.

   L'ACCENT N'EST PAS TOUCHÉ. `.bouton.principal` garde son fond d'accent : il
   appartient à l'app, c'est lui qui dit dans laquelle on se trouve. Les
   crochets s'ajoutent autour du libellé, ils ne le remplacent pas.
   ========================================================================== */

:root[data-ambiance="terminal"] .carte,
:root[data-ambiance="terminal"] .carte-flottante,
:root[data-ambiance="terminal"] .panneau { position: relative; }

:root[data-ambiance="terminal"] .carte::before,
:root[data-ambiance="terminal"] .carte::after,
:root[data-ambiance="terminal"] .carte-flottante::before,
:root[data-ambiance="terminal"] .carte-flottante::after,
:root[data-ambiance="terminal"] .panneau::before,
:root[data-ambiance="terminal"] .panneau::after {
  position: absolute; color: var(--tx); background: var(--bg);
  line-height: 1; font-size: 1.05em; pointer-events: none;
}
:root[data-ambiance="terminal"] .carte::before,
:root[data-ambiance="terminal"] .carte-flottante::before,
:root[data-ambiance="terminal"] .panneau::before {
  content: "\250C"; top: -1px; left: -1px; padding: 0 2px 2px 0;
}
:root[data-ambiance="terminal"] .carte::after,
:root[data-ambiance="terminal"] .carte-flottante::after,
:root[data-ambiance="terminal"] .panneau::after {
  content: "\2518"; bottom: -1px; right: -1px; padding: 2px 0 0 2px;
}

/* Un bouton de terminal se lit entre crochets. Le `gap` du bouton fournit
   déjà l'espace : les crochets n'en portent pas, sinon il compte double. */
:root[data-ambiance="terminal"] .bouton { text-transform: uppercase; letter-spacing: .06em; }
:root[data-ambiance="terminal"] .bouton::before { content: "["; }
:root[data-ambiance="terminal"] .bouton::after { content: "]"; }

/* Un badge devient un jeton entre chevrons — la convention des méta-variables
   dans une page de manuel. */
:root[data-ambiance="terminal"] .badge {
  text-transform: uppercase; background: transparent; border-color: currentColor;
}
:root[data-ambiance="terminal"] .badge::before { content: "<"; }
:root[data-ambiance="terminal"] .badge::after { content: ">"; }

/* Le repli troque son chevron dessiné contre un marqueur de nœud d'arbre, la
   convention de tout explorateur en console. Le `transform` du chevron est
   annulé : il ferait pivoter le texte. */
:root[data-ambiance="terminal"] .repli > summary::before {
  content: "[+]"; width: auto; height: auto; border: 0;
  transform: none; transition: none; font-family: var(--mono);
}
:root[data-ambiance="terminal"] .repli[open] > summary::before {
  content: "[-]"; transform: none;
}

/* Le message d'erreur GARDE SA COULEUR — un rouge devenu monochrome cesserait
   de se lire comme une erreur. Il gagne seulement le préfixe des sorties
   d'erreur d'un terminal. */
:root[data-ambiance="terminal"] .alerte::before { content: "!! "; font-weight: 700; }

/* ==========================================================================
   LA DENSITÉ
   Axe INDÉPENDANT de l'ambiance : un terminal aéré et un countryside
   resserré sont deux demandes légitimes.

   Elle ne touche AUCUNE couleur — c'est ce qui garde la vérification de
   contraste à 8 combinaisons (4 ambiances × 2 luminosités) au lieu de 16.

   Et elle ne descend sous AUCUN des deux planchers : 44px de cible tactile,
   16px sur un champ de saisie. Ce sont des règles d'usage, pas de style — en
   dessous de 16px Safari iOS zoome au focus et ne dézoome jamais, que la vue
   soit compacte ou non. Les deux sont verrouillés par tests/test_css.py.
   ========================================================================== */
:root[data-densite="compact"] {
  --e-1: 3px; --e-2: 6px; --e-3: 8px; --e-4: 11px;
  --e-5: 16px; --e-6: 22px; --e-7: 32px;
  --e-section: 18px;
  --e-bloc: 8px;
  /* Le spec demande les espacements ET la taille de base du texte — cette
     seconde partie manquait. --taille-corps, jamais `font-size` en dur ici :
     un champ de saisie reste à 16px littéral, gravé dans sa propre règle,
     quelle que soit la densité. Rien dans ce bloc ne descend sous ce
     plancher, seul le corps de texte (`body`) le peut. */
  --taille-corps: 14px;
}

/* --- Réinitialisation ---------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }

/* `hidden` doit l'emporter sur les `display:flex` posés plus bas, sinon un
   élément masqué par le serveur reste visible. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  background: var(--bg);
  color: var(--tx);
  font-family: var(--sans);
  font-size: var(--taille-corps);
  line-height: 1.5;
  min-height: 100dvh;   /* jamais 100vh : sur iOS la barre d'adresse rétracte la hauteur */
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

a { color: inherit; text-decoration: none; }
img, svg { max-width: 100%; }
img { display: block; }

:focus-visible {
  outline: 2px solid var(--accent, var(--tx));
  outline-offset: 2px;
  border-radius: 4px;
}

/* Lien de sortie d'une page qui ne partage pas le bandeau de l'app — la page
   d'administration au premier chef. Sans lui on y entre sans pouvoir en
   sortir. Cible tactile de 44 px comme partout ailleurs. */
.retour {
  display: inline-flex; align-items: center; gap: var(--e-1);
  min-height: 44px; color: var(--tx-2); text-decoration: none;
}
.retour:hover { color: var(--tx); }

/* En-tête d'une page qui ne partage pas le bandeau de l'app (l'admin, au
   premier chef — voir README, « La page d'administration a toujours une
   sortie »). Sans elle, le titre et le lien de sortie sont deux éléments
   empilés au hasard : ici le lien se lit comme une VRAIE sortie, posée à
   distance du titre plutôt que perdue au-dessus dans son propre `<p>`. */
.entete-page {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--e-3); flex-wrap: wrap;
}
.entete-page h1 { margin: 0; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
  }
}

/* --- Typographie --------------------------------------------------------- */
/* `--face-titre` sans repli concurrent plus bas dans le fichier (vérifié :
   aucune autre règle ne cible `h1, h2, h3` sur `font-family`) : la variable
   peut donc se poser ici sans risquer de perdre la cascade. Le repli
   `var(--sans)` reproduit l'existant — aucune ambiance ne change le corps de
   la page tant qu'elle ne pose pas --face-titre. */
h1, h2, h3 { font-family: var(--face-titre, var(--sans)); line-height: 1.25; letter-spacing: -.015em; font-weight: 640; }
h1 { font-size: 1.5rem; }
h2 { font-size: 1.1rem; }
h3 { font-size: 1rem; }

/* Le serif est réservé aux grands titres et écrans d'accueil. Toute
   l'interface courante reste en --sans.

   C'EST ICI, et nulle part avant, que `.titre-affiche` lit les trois
   variables — pas dans une règle antérieure à spécificité égale qu'une
   règle plus bas pourrait ensuite écraser en silence. Le repli de chaque
   `var()` reproduit exactement l'apparence d'avant la branche : aucune
   ambiance active ne change ce rendu. */
.titre-affiche {
  font-family: var(--face-titre, var(--serif));
  font-weight: var(--graisse-titre, 500);
  letter-spacing: var(--lettrage-titre, -.01em);
}

/* Chiffres en chasse fixe tabulaire : durées, quantités, montants, compteurs.
   Sans elle, une colonne « 5 min / 45 min / 120 min » danse d'une ligne à
   l'autre. */
.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: .92em;
  letter-spacing: -.01em;
}

.doux { color: var(--tx-2); }
.service { color: var(--tx-3); font-size: .85rem; }

.contenu { max-width: var(--lg-lecture); margin: 0 auto; padding: var(--e-4); }
.contenu-large { max-width: var(--lg-large); margin: 0 auto; padding: var(--e-4); }
/* Voir `--lg-tableau` : une page-tableau avec actions par ligne. */
.contenu-tableau { max-width: var(--lg-tableau); margin: 0 auto; padding: var(--e-4); }

/* ==========================================================================
   COMPOSANTS
   ========================================================================== */

/* --- Bandeau collant ----------------------------------------------------- */
.bandeau {
  position: sticky; top: 0; z-index: 50;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
  padding: var(--e-2) var(--e-4);
  padding-top: calc(var(--e-2) + env(safe-area-inset-top, 0px));
}
@supports (backdrop-filter: blur(1px)) {
  .bandeau {
    background: color-mix(in srgb, var(--bg) 88%, transparent);
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
  }
}
.bandeau .dedans {
  display: flex; align-items: center; gap: var(--e-3);
  max-width: var(--lg-large); margin: 0 auto;
  /* Nowrap PAR DÉFAUT, explicitement : au-dessus du seuil mobile (640px,
     ci-dessous), la ligne ne casse jamais toute seule. Avant ce lot,
     l'absence de valeur ici (le `nowrap` implicite du flexbox) cohabitait
     mal avec le `flex-wrap: wrap` du bloc 640px plus bas — selon l'ordre du
     fichier, l'un écrasait l'autre à spécificité égale, et chaque enfant de
     `.dedans` (marque, recherche, bouton, thème, menu) pouvait finir sur sa
     propre ligne. C'est le bug constaté : le bouton de thème au-dessus du
     menu de compte. Un seul point de rupture, déclaré une seule fois : voir
     `.bandeau-actions` et le bloc `@media (max-width: 640px)` plus bas. */
  flex-wrap: nowrap;
}
/* La marque : une icône optionnelle, puis le nom. L'icône n'est PAS posée ici
   en `<img>` — elle vient en SVG inline, rendu par l'app, pour prendre
   `currentColor` et donc `--accent` ; une image ne se colore pas en CSS.
   Cible tactile de 44px comme ses voisins, sans que le dessin lui-même
   grossisse : la taille est fixée sur le SVG (`.mark-svg`), pas sur le lien.

   PIÈGE DÉJÀ PAYÉ : cette règle remplace une ancienne version qui traitait
   `.marque` comme un lien simple (`font-weight` et `letter-spacing` posés
   directement dessus). Une app qui n'a pas encore adopté le contrat à deux
   enfants (`.mark` + `.nom-app`) perd ce style au prochain `installer_css.py`
   — c'est le prix assumé de ne PAS doubler le sélecteur : deux déclarations
   de `.bandeau .marque` à quelques lignes d'écart, c'est le bug déjà vu sur
   `.avis-form`, où celle qui gagnait était invisible à qui lisait l'autre. */
/* `--e-2` (8px) lisait comme deux éléments côte à côte plutôt qu'une seule
   marque — l'icône et le nom sont UNE chose, pas deux : resserré à `--e-1`. */
.bandeau .marque { display: flex; align-items: center; gap: var(--e-1); margin-right: auto; }
.bandeau .marque .mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; margin-left: -10px;
  color: var(--accent); flex: none;
}
.bandeau .marque .mark-svg { width: 22px; height: 22px; }
.bandeau .marque .mark:hover { filter: brightness(1.1); }
/* Même trois variables que `.titre-affiche`, avec le repli PROPRE au nom
   d'app (640 / -.02em / --sans hérité) : une ambiance qui pose --face-titre,
   --graisse-titre ou --lettrage-titre les impose aux deux à la fois — c'est
   voulu, un terminal a des titres ET un nom d'app en chasse fixe — mais tant
   que rien ne les pose, chacun garde SA propre allure d'aujourd'hui. */
.bandeau .nom-app {
  font-family: var(--face-titre, var(--sans));
  font-weight: var(--graisse-titre, 640);
  letter-spacing: var(--lettrage-titre, -.02em);
}

/* --- Bloc de droite : thème + compte, jamais séparés ----------------------
   Avant ce lot, `.bascule-theme` et `<details class="menu">` étaient deux
   enfants directs de `.dedans`, au même niveau que la marque et la
   recherche : quand la ligne manquait de place, `flex-wrap` les répartissait
   CHACUN où il pouvait — le bouton de thème pouvait finir seul au-dessus du
   menu de compte, séparé de son voisin. Ce conteneur les regroupe en UN
   SEUL item flex de `.dedans`. `flex: none` : il ne grandit ni ne rétrécit,
   donc ne cède jamais sa place — c'est le repère de navigation, il reste sur
   la ligne de la marque à toute largeur. Contrat HTML : voir README,
   section « Bandeau ». */
.bandeau-actions { display: flex; align-items: center; gap: var(--e-2); flex: none; }

/* --- Bouton d'ajout : perd son libellé avant de disparaître ---------------
   `.bouton-ajouter` porte DEUX enfants : un signe `+` toujours visible, et un
   `<span class="libelle">` masqué sous le seuil mobile. `aria-label` est posé
   sur `.bouton-ajouter` lui-même (contrat README) — il fixe le nom
   accessible dans les DEUX cas, donc masquer `.libelle` en CSS ne retire
   rien pour un lecteur d'écran. Un bouton d'action ne doit pas s'évaporer en
   silence : sous le seuil il reste une cible de 44px avec un `+` lisible,
   jamais un élément qui disparaît sans repli. */
.bandeau .bouton-ajouter { flex: none; }

/* Sous 640px (breakpoint mobile unifié, déjà en place ailleurs dans ce
   fichier — voir la densité et `.nav-basse`) : LA RECHERCHE EST CE QUI CÈDE,
   rien d'autre. `order` + `flex-basis: 100%` la poussent sur sa PROPRE
   ligne, en dernier — un choix délibéré (elle ne tient plus dans la largeur
   disponible entre la marque et le bloc de droite), pas un débordement qui
   dépendrait de la largeur exacte du contenu ce jour-là. La marque et
   `.bandeau-actions` restent `flex: none` : ils ne participent pas au calcul
   qui pousserait la recherche à la ligne, ils sont simplement toujours là. */
@media (max-width: 640px) {
  .bandeau .dedans { flex-wrap: wrap; row-gap: var(--e-2); }
  .bandeau .recherche { order: 5; flex-basis: 100%; }
  .bandeau .bouton-ajouter .libelle { display: none; }
}
.bandeau nav { display: flex; gap: var(--e-4); font-size: .9rem; }
.bandeau nav a { color: var(--tx-2); }
.bandeau nav a:hover, .bandeau nav a[aria-current] { color: var(--tx); }

/* --- Carte --------------------------------------------------------------- */
.carte {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-l);
  padding: var(--e-4);
  box-shadow: var(--shadow-1);
}
.carte-flottante {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: var(--e-6) var(--e-5) var(--e-5);
  box-shadow: var(--shadow-3);
}

/* --- Boutons ------------------------------------------------------------- */
/* 44px de haut : cible tactile minimale. Un bouton de 32px se rate au doigt. */
.bouton {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--e-2);
  min-height: 44px; padding: 0 var(--e-4);
  font: inherit; font-weight: 600;
  border: 1px solid var(--line-controle);
  border-radius: var(--r-m);
  background: var(--surface); color: var(--tx);
  cursor: pointer;
  transition: background var(--t), border-color var(--t), color var(--t);
}
.bouton:hover { background: var(--surface-2); }
.bouton:active { transform: translateY(1px); }
.bouton[disabled] { opacity: .5; cursor: not-allowed; }

.bouton.principal {
  background: var(--accent); color: var(--accent-tx);
  border-color: transparent;
}
.bouton.principal:hover { filter: brightness(.94); background: var(--accent); }

.bouton.discret {
  background: transparent; border-color: transparent; color: var(--tx-2);
  min-height: 36px; padding: 0 var(--e-2); font-weight: 500; font-size: .9rem;
}
.bouton.discret:hover { background: var(--surface-2); color: var(--tx); }

.bouton.pleine-largeur { width: 100%; }

/* --- Champs -------------------------------------------------------------- */
.champ { display: flex; flex-direction: column; gap: var(--e-1); }
.champ > span { font-size: .85rem; color: var(--tx-2); }

/* `input:not([type])` n'est pas une coquetterie : un <input name="x"> sans
   attribut type est un champ texte parfaitement valide, et c'est la forme la
   plus courante dans un gabarit. Sans cette ligne il ressort au style natif du
   navigateur au milieu de champs stylés — l'app a l'air cassée. */
.champ input, .champ select, .champ textarea,
input:not([type]), input[type=text], input[type=email], input[type=password],
input[type=search], input[type=number], input[type=date], input[type=url],
input[type=tel], select, textarea {
  width: 100%;
  padding: var(--e-2) var(--e-3);
  /* 16px, et pas moins : en dessous, Safari zoome la page au focus. */
  font: inherit; font-size: 16px;
  color: var(--tx);
  background: var(--surface);
  border: 1px solid var(--line-controle);
  border-radius: var(--r-m);
  transition: border-color var(--t), box-shadow var(--t);
}
.champ input:focus, .champ select:focus, .champ textarea:focus,
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--ring);
}
textarea { min-height: 5rem; resize: vertical; }

/* Un champ occupe 100% de son conteneur — ce qui est juste dans une carte
   étroite, et faux dans `.contenu-large`, prévu pour les grilles et les
   tableaux. Un champ « Nom » de 1200px ne dit plus rien de ce qu'on attend :
   la largeur d'une saisie est une information. Les formulaires linéaires se
   bornent donc explicitement, sans toucher au 100% dont tout le reste dépend. */
.formulaire-etroit { max-width: var(--lg-formulaire); }

/* Même largeur de champ que `.formulaire-etroit` colonne les longues
   saisies (nom, email) — mais un mot de passe provisoire ou un rôle de
   quinze caractères n'a rien à dire à 480px : une longueur qui ne reflète
   pas ce qu'on y tape est plus dure à remplir, pas plus facile. Posée sur
   `.champ` lui-même (pas sur son `<input>`) pour que le libellé suive la
   même largeur que le contrôle. */
.champ.champ-etroit { max-width: 12rem; }

/* Rythme vertical d'un gabarit autonome. Une page qui empile un titre, un
   tableau et une carte les met au contact sans elle : la réinitialisation
   annule les marges par défaut du navigateur, et rien ne les remplace. Sur
   `* + *` plutôt que sur chaque bloc, pour qu'un premier élément ne décolle
   pas du haut de la page. */
.rythme > * + * { margin-top: var(--e-5); }

/* Une pile de champs. Un `form` n'espace rien tout seul, et chaque app
   redéfinissait la même règle dans sa propre feuille — c'est le genre de
   contrat qui appartient au socle. Plus serré que `.rythme` : à l'intérieur
   d'un formulaire, les champs sont un groupe, pas des sections. */
.pile { display: flex; flex-direction: column; gap: var(--e-4); }

/* Un bouton dans une pile s'étirerait à la largeur des champs — `stretch` est
   l'alignement par défaut d'un flex en colonne. Sa largeur doit dire ce qu'il
   fait, pas la taille de la saisie au-dessus. Volontairement portée par
   `.formulaire-etroit` et non par `.pile` seule : les apps utilisent déjà
   `.pile` avec leurs propres boutons, et une règle globale changerait leur
   rendu sans qu'elles aient rien demandé. */
.formulaire-etroit .pile > .bouton { align-self: flex-start; }

/* Un champ et le bouton qui l'applique vont sur une ligne, pas l'un sous
   l'autre : dessous, le bouton se lit comme la légende du champ. `flex-end`
   les aligne par le bas, seul alignement correct quand le champ porte un
   libellé au-dessus et pas le bouton. */
.champ-action { display: flex; align-items: flex-end; gap: var(--e-2); }
.champ-action .champ { flex: 1; min-width: 0; }

/* --- Badges d'état ------------------------------------------------------- */
.badge {
  display: inline-flex; align-items: center; gap: var(--e-1);
  padding: 2px var(--e-2);
  font-size: .8rem; font-weight: 600;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface-2); color: var(--tx-2);
}
.badge.ok   { background: var(--ok-bg);   color: var(--ok-tx);   border-color: var(--ok-line); }
.badge.warn { background: var(--warn-bg); color: var(--warn-tx); border-color: var(--warn-line); }
.badge.err  { background: var(--err-bg);  color: var(--err-tx);  border-color: var(--err-line); }
.badge.info { background: var(--info-bg); color: var(--info-tx); border-color: var(--info-line); }

/* --- Alertes ------------------------------------------------------------- */
.alerte {
  padding: var(--e-3);
  border: 1px solid var(--line);
  border-radius: var(--r-m);
  background: var(--surface-2); color: var(--tx-2);
  font-size: .9rem;
}
.alerte.ok   { background: var(--ok-bg);   color: var(--ok-tx);   border-color: var(--ok-line); }
.alerte.warn { background: var(--warn-bg); color: var(--warn-tx); border-color: var(--warn-line); }
.alerte.err  { background: var(--err-bg);  color: var(--err-tx);  border-color: var(--err-line); }
.alerte.info { background: var(--info-bg); color: var(--info-tx); border-color: var(--info-line); }

/* --- Listes -------------------------------------------------------------- */
.liste { list-style: none; }
.liste > li {
  padding: var(--e-3) 0;
  border-bottom: 1px solid var(--line);
}
.liste > li:last-child { border-bottom: 0; }
.liste .titre-ligne { font-weight: 600; }
.liste .meta { display: block; color: var(--tx-2); font-size: .85rem; }

/* --- Tableaux ------------------------------------------------------------ */
/* Le conteneur défile, pas la page : sur téléphone un tableau large ne doit
   jamais provoquer un défilement horizontal du document entier. */
.tableau-conteneur { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.tableau { width: 100%; border-collapse: collapse; font-size: .9rem; }
.tableau th, .tableau td {
  padding: var(--e-2) var(--e-3);
  text-align: left;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.tableau th { color: var(--tx-3); font-weight: 600; font-size: .8rem; }
.tableau tbody tr:hover { background: var(--surface-2); }
/* La ligne de création (admin, « Créer un compte ») porte des champs, pas du
   texte : `white-space: nowrap` hérité de `.tableau td` ne les gêne pas, mais
   sans cette teinte de fond elle se lirait comme un compte de plus plutôt
   que comme une action. */
.tableau tbody tr.ligne-creation { background: var(--surface-2); }
.tableau tbody tr.ligne-creation td { white-space: normal; }

/* --- Navigation basse (téléphone) ---------------------------------------- */
.nav-basse { display: none; }
@media (max-width: 640px) {
  .nav-basse {
    display: flex;
    position: fixed; inset: auto 0 0 0; z-index: 30;
    background: var(--surface);
    border-top: 1px solid var(--line);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .nav-basse a {
    flex: 1;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; min-height: 56px;
    color: var(--tx-3); font-size: .7rem; font-weight: 600;
  }
  .nav-basse a[aria-current] { color: var(--accent); }
  .nav-basse .ico { font-size: 1.15rem; line-height: 1; }
}

/* ==========================================================================
   COMPOSANTS DE PARITÉ — grille/liste, recherche, palette, panneau
   Ajoutés le 2026-08-04 pour ce que recipes sait faire et qu'aucune autre app
   ne savait reproduire sans tout réécrire.
   ========================================================================== */

/* --- Accessible mais invisible ------------------------------------------- */
/* Un libellé de champ que le dessin ne montre pas doit exister quand même : un
   `<select>` sans <label> n'est annoncé par rien. `display:none` le retirerait
   aussi du lecteur d'écran — c'est tout l'intérêt de cette recette. */
.visuellement-cache {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Le premier lien de la page, visible seulement au clavier : sans lui, tabuler
   sur une app à bandeau oblige à traverser toute la navigation à chaque page. */
.saut-contenu {
  position: absolute; left: var(--e-3); top: -100%; z-index: 100;
  padding: var(--e-2) var(--e-3);
  background: var(--surface-hi); border: 1px solid var(--line-strong);
  border-radius: var(--r-m); box-shadow: var(--shadow-2);
}
.saut-contenu:focus { top: var(--e-3); }

/* --- Grille ou liste ------------------------------------------------------ */
/* Les deux dispositions du MÊME balisage : le serveur rend les mêmes éléments
   et pose `data-view` sur le conteneur. Rien à re-rendre pour basculer, et la
   bascule marche donc aussi sans JavaScript, au rechargement. */
.collection[data-view="grid"] {
  display: grid;
  gap: var(--e-4);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
}
.collection[data-view="list"] {
  display: flex; flex-direction: column;
  gap: 0;
}
.collection[data-view="list"] > * {
  display: flex; align-items: center; gap: var(--e-3);
  padding: var(--e-3) 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0; box-shadow: none; background: none;
}
.collection[data-view="list"] > :last-child { border-bottom: 0; }
/* En liste, l'image se réduit à une vignette — sinon la « liste » est une
   grille à une colonne, ce qui ne rend service à personne. */
.collection[data-view="list"] img {
  width: 56px; height: 56px; object-fit: cover;
  border-radius: var(--r-m); flex: none;
}
.collection[data-view="grid"] img {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover;
  border-radius: var(--r-m) var(--r-m) 0 0;
}

/* --- Bascule segmentée (thème, vue) --------------------------------------- */
/* Un groupe de boutons dont un seul est actif. Chacun est un <button> dans un
   <form>, donc la bascule fonctionne sans JavaScript. */
.segmente {
  display: inline-flex; align-items: center; flex: none;
  padding: 3px; gap: 2px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-m);
}
.segmente button {
  /* `inline-flex` et non le défaut : sans lui, une icône posée à côté du
     libellé se colle au texte et pend sous la ligne de base. */
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--e-2);
  min-height: 38px; padding: 0 var(--e-3);
  font: inherit; font-size: .85rem; font-weight: 600;
  color: var(--tx-2); background: none; border: 0;
  border-radius: calc(var(--r-m) - 3px);
  cursor: pointer;
  transition: background var(--t), color var(--t);
}
.segmente button:hover { color: var(--tx); }
.segmente button[aria-pressed="true"] {
  background: var(--surface-hi); color: var(--tx);
  box-shadow: var(--shadow-1);
}
/* Sur téléphone la cible remonte à 44px : le segmenté est petit par nature,
   c'est justement là qu'on rate le bouton. */
@media (max-width: 640px) {
  .segmente button { min-height: 44px; }
}

/* --- Bouton de thème cyclant ---------------------------------------------
   Le bouton porte les TROIS icônes, rendues par le serveur ; on n'en montre
   qu'une. C'est ce qui permet à `workbench.js` de se contenter de changer
   `data-etat` — il ne fabrique aucun élément, conformément à la règle du
   dépôt, et le rendu sans JavaScript est déjà le bon.                      */
.bascule-theme {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 38px; min-height: 38px; padding: 0;
  color: var(--tx-2); background: var(--surface-2);
  border: 1px solid var(--line); border-radius: var(--r-m);
  cursor: pointer;
  transition: color var(--t), background var(--t);
}
.bascule-theme:hover { color: var(--tx); background: var(--surface-hi); }
/* Ni taille ni couleur ici : `icons.icon()` pose déjà `stroke="currentColor"`
   et les dimensions. Le seul rôle de ce bloc est de n'en montrer qu'une. */
.bascule-theme svg { display: none; }
.bascule-theme[data-etat="auto"]  .ico-auto,
.bascule-theme[data-etat="light"] .ico-light,
.bascule-theme[data-etat="dark"]  .ico-dark { display: block; }
/* Même bouton, même mécanique, pour la densité (deux états au lieu de
   trois) : `.bascule-theme` n'a jamais été spécifique au thème, seul son nom
   l'est resté — voir `js/workbench.js`, « Thème, et tout autre réglage
   cyclant sur le même patron ». */
.bascule-theme[data-etat="normal"]  .ico-normal,
.bascule-theme[data-etat="compact"] .ico-compact { display: block; }

@media (max-width: 640px) {
  .bascule-theme { min-width: 44px; min-height: 44px; }
}

/* --- Barre de recherche --------------------------------------------------- */
/* `flex` et `min-width` n'ont d'effet QUE si `.recherche` est elle-même un
   enfant flex (c'est le cas dans `.bandeau .dedans`) : ailleurs, une
   propriété flex sur un élément qui n'est pas un item flex ne fait rien —
   donc rien ne change pour un usage existant hors bandeau. C'est ce qui
   permet à la recherche de bandeau d'être « large » sans lui donner une
   largeur fixe qui déborderait sur petit écran. */
.recherche { position: relative; display: flex; align-items: center; flex: 1 1 240px; min-width: 0; }
.recherche input[type=search] {
  padding-left: calc(var(--e-4) + 1.1rem);
  padding-right: calc(var(--e-4) + 1.5rem);
  /* Le rond natif de Safari efface le champ sans prévenir le formulaire. */
  -webkit-appearance: none; appearance: none;
}
.recherche .ico {
  position: absolute; left: var(--e-3);
  color: var(--tx-3); pointer-events: none; font-size: 1rem;
}
/* Le badge `/` qui rappelle le raccourci clavier : même allure que
   `.palette .raccourci` (c'est le même signifiant — « voici la touche qui
   déclenche ceci »), mais collé au bord DROIT du champ plutôt qu'à la fin
   d'une ligne de résultat, donc `position: absolute` et non `margin-left:
   auto`. Optionnel : une app sans raccourci clavier n'a simplement pas ce
   `<span>` dans son balisage, le champ garde son `padding-right` de réserve. */
.recherche .raccourci {
  position: absolute; right: var(--e-3);
  font-family: var(--mono); font-size: .75rem;
  color: var(--tx-3); pointer-events: none;
  padding: 2px var(--e-2);
  border: 1px solid var(--line); border-radius: var(--r-s);
}
/* L'indicateur d'attente ne remplace pas la liste : la remplacer par un
   chargeur fait clignoter la page à chaque frappe. On l'affiche à côté. */
.recherche[data-busy] .ico { opacity: .4; }

/* --- Pictogrammes --------------------------------------------------------- */
/* Rendus par `workbench.icons.icon()`. Ils héritent de la couleur du texte —
   donc ils suivent le thème et l'état du bouton sans une règle de plus. */
.ico { flex: 0 0 auto; vertical-align: -.15em; }
/* Dans un bouton, l'icône se cale sur le libellé sans l'écarter : `.bouton`
   pose déjà le `gap`. Sans ce `margin: 0`, une icône ajoutée à un bouton
   existant décale tous les autres de la rangée. */
.bouton .ico { margin: 0; }

/* --- Note en étoiles ------------------------------------------------------ */
/* Rendue par `workbench.icons.stars()`. Le tracé est le même pour les trois
   états : seul le remplissage change. */
.etoiles { display: inline-flex; align-items: center; gap: var(--e-2); }
.etoiles .glyphes { display: inline-flex; gap: 1px; }
.etoiles .etoile {
  position: relative; display: inline-flex;
  width: 16px; height: 16px;
  color: var(--tx-3);
}
.etoiles .etoile svg { position: absolute; inset: 0; }
.etoiles .etoile.pleine { color: var(--accent); }
.etoiles .etoile.pleine svg { fill: currentColor; }
/* La demie porte deux tracés : le premier reste vide, le second est peint puis
   rogné à la moitié gauche. `inset()` coupe à droite, pas au milieu du trait —
   la pointe de l'étoile reste nette. */
.etoiles .etoile.demie svg:last-child {
  color: var(--accent); fill: currentColor;
  clip-path: inset(0 50% 0 0);
}
.etoiles .valeur {
  font-size: .85rem; font-weight: 600; color: var(--tx-2);
  font-variant-numeric: tabular-nums;
}

/* --- Pagination ----------------------------------------------------------- */
.pagination {
  display: flex; align-items: center; justify-content: center;
  gap: var(--e-3); padding: var(--e-5) 0;
}
.pagination .position { color: var(--tx-3); font-size: .85rem; }

/* --- État vide ------------------------------------------------------------ */
/* Une liste vide sans explication ressemble à une panne. */
.vide {
  text-align: center; padding: var(--e-7) var(--e-4);
  color: var(--tx-2);
}
.vide .ico { font-size: 2rem; opacity: .5; margin-bottom: var(--e-3); }
.vide p { margin-bottom: var(--e-4); }

/* --- Palette de commandes ------------------------------------------------- */
/* Le même composant que les raccourcis clavier, pas un second : la palette
   liste ce que les raccourcis déclenchent, et c'est la seule façon qu'ont les
   raccourcis d'être découvrables. */
.palette {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: flex-start; justify-content: center;
  padding: max(10vh, var(--e-6)) var(--e-4) var(--e-4);
  background: color-mix(in srgb, var(--bg) 60%, transparent);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
.palette .boite {
  width: min(100%, 560px);
  background: var(--surface-hi);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-l);
  box-shadow: var(--shadow-3);
  overflow: hidden;
}
.palette input {
  border: 0; border-radius: 0;
  border-bottom: 1px solid var(--line);
  background: none; padding: var(--e-4);
}
.palette input:focus { box-shadow: none; border-color: var(--line); }
.palette .resultats { max-height: min(50vh, 360px); overflow-y: auto; }
.palette .resultats button {
  display: flex; align-items: center; gap: var(--e-3);
  width: 100%; min-height: 44px; padding: var(--e-2) var(--e-4);
  font: inherit; text-align: left;
  color: var(--tx); background: none; border: 0; cursor: pointer;
}
.palette .resultats button:hover,
.palette .resultats button[aria-selected="true"] { background: var(--surface-2); }
.palette .raccourci {
  margin-left: auto;
  font-family: var(--mono); font-size: .75rem;
  color: var(--tx-3);
  padding: 2px var(--e-2);
  border: 1px solid var(--line); border-radius: var(--r-s);
}

/* --- Panneau latéral (assistant) ------------------------------------------ */
/* Deux tailles, et la préférence vit dans un cookie lu par le serveur : sans
   lui, le panneau s'ouvre en coin puis saute en plein écran une fois le script
   passé. */
.panneau {
  position: fixed; z-index: 150;
  display: flex; flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  box-shadow: var(--shadow-3);
}
.panneau[data-size="corner"] {
  right: var(--e-4); bottom: var(--e-4);
  width: 400px; max-height: min(70vh, 640px);
  border-radius: var(--r-l);
}
.panneau[data-size="full"] {
  inset: 0; border-radius: 0; border: 0;
}
.panneau .entete {
  display: flex; align-items: center; gap: var(--e-2);
  padding: var(--e-3) var(--e-4);
  border-bottom: 1px solid var(--line);
}
.panneau .entete .titre { font-weight: 640; margin-right: auto; }
.panneau .fil { flex: 1; overflow-y: auto; padding: var(--e-4); }
/* Le pied porte une saisie et son bouton : ils sont EN LIGNE. Sans ce flex,
   chaque app le redécouvre en voyant son champ, son trombone et son bouton
   « Envoyer » empilés les uns sous les autres — constaté en ouvrant le panneau
   de `commandes`, la première app à l'exercer dans un navigateur.
   Le champ prend la place restante ; les boutons gardent la leur. */
.panneau .pied {
  display: flex;
  align-items: center;
  gap: var(--e-2);
  border-top: 1px solid var(--line);
  padding: var(--e-3) var(--e-4);
  padding-bottom: calc(var(--e-3) + env(safe-area-inset-bottom, 0px));
}
.panneau .pied input[type="text"],
.panneau .pied textarea { flex: 1; min-width: 0; }
/* Sous 640px il est DÉJÀ plein écran : un panneau de 400px déborde d'un
   téléphone de 390. La commande de taille n'y est pas rendue — elle ne
   voudrait rien dire. */
@media (max-width: 640px) {
  .panneau, .panneau[data-size="corner"] {
    inset: 0; width: auto; max-height: none;
    border-radius: 0; border: 0;
  }
  .panneau .taille { display: none; }
}

/* --- Bulles de conversation ----------------------------------------------- */
.bulle { padding: var(--e-3); border-radius: var(--r-m); margin-bottom: var(--e-3); }
.bulle.moi {
  background: var(--accent-soft); border: 1px solid var(--accent-line);
  margin-left: var(--e-6);
}
.bulle.lui { background: var(--surface-2); border: 1px solid var(--line); }
/* L'outil en cours d'exécution : discret, il n'est pas la réponse. */
.bulle.outil {
  font-family: var(--mono); font-size: .8rem;
  color: var(--tx-3); background: none; border: 0; padding: var(--e-1) 0;
}
/* Le tour qui s'est mal terminé. La famille était incomplète : un flux SSE
   finit par `done` OU par `error`, et sans bulle pour le second cas chaque app
   se fabrique la sienne — c'est-à-dire quatre rouges différents.
   Relevé en branchant `commandes`, première app à exercer le panneau. */
.bulle.erreur {
  background: var(--err-bg); color: var(--err-tx);
  border: 1px solid var(--err-line);
}

/* --- Onboarding ----------------------------------------------------------- */
/* Une seule carte, au centre, la première fois. Pas un carrousel de cinq
   écrans : ce qui n'est pas lu à la première carte ne le sera pas à la
   cinquième. */
.accueil-premiere-fois {
  display: flex; align-items: center; justify-content: center;
  min-height: 100dvh; padding: var(--e-4);
}
.accueil-premiere-fois .carte-flottante { width: min(100%, 480px); }
.accueil-premiere-fois h1 { margin-bottom: var(--e-3); }
.accueil-premiere-fois .etapes { margin: var(--e-4) 0; }
.accueil-premiere-fois .etapes li {
  display: flex; gap: var(--e-3); padding: var(--e-2) 0;
  color: var(--tx-2); list-style: none;
}
.accueil-premiere-fois .etapes .ico { color: var(--accent); flex: none; }

/* --- Menu déroulant -------------------------------------------------------
   `<details>/<summary>` natif, pas un composant JavaScript : il s'ouvre et se
   ferme sans script, au clavier comme au doigt. `workbench.js` n'ajoute que le
   confort — fermeture au clic extérieur et à Échap.

   Sert d'abord au bandeau : au-delà d'une poignée de liens, une barre étalée
   sur toute la largeur n'est plus une navigation, c'est une liste.

   LE SEUIL EST À 1024px, et il est choisi sur des appareils réels, pas sur un
   nombre rond. Largeurs CSS des iPad en portrait :

       mini 8.3      744        Air / 10.9    820
       Pro 11        834        Pro 12.9     1024

   Un seuil à 900px laissait donc l'iPad Pro 12.9 en portrait avec la barre à
   plat — précisément le symptôme qu'on corrige. En paysage (1133 à 1366px) la
   barre à plat tient sans se serrer, et c'est bien ce qu'on veut : le repli
   n'est pas une punition pour les grands écrans. */
.menu { position: relative; }

.menu > summary {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--e-2);
  min-height: 44px; min-width: 44px; padding: 0 var(--e-3);
  border: 1px solid var(--line-controle);
  border-radius: var(--r-m);
  background: var(--surface); color: var(--tx);
  font-weight: 600; cursor: pointer;
  list-style: none;   /* retire le triangle Firefox */
  transition: background var(--t);
}
/* Safari a besoin du pseudo-élément, `list-style` ne lui suffit pas. */
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::marker { content: ""; }

/* PIÈGE VÉRIFIÉ — une app qui style tous ses `<details>` (chevron dessiné,
   couleur atténuée pour les replis) touche AUSSI ce menu, qui en est un. Le
   symptôme : un chevron en trop à côté du ☰, et un bouton grisé. La règle de
   l'app doit exclure `.menu` — le socle ne se bat pas en spécificité contre
   les feuilles qu'il ne connaît pas. Vu sur wishlist, sur iPad. */
.menu > summary:hover { background: var(--surface-2); }

/* Un menu qui porte un libellé (« Yann ») a besoin de dire qu'il s'ouvre — un
   ☰ le dit déjà par lui-même, pas un prénom. Le chevron pivote à l'ouverture :
   c'est le seul retour visuel quand le panneau sort sous le doigt. */
.menu.avec-fleche > summary::after {
  content: "";
  width: .4rem; height: .4rem; flex: none;
  margin-left: var(--e-1);
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  opacity: .6;
  transition: transform var(--t);
}
.menu.avec-fleche[open] > summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}
.menu[open] > summary { background: var(--surface-2); }

.menu > .panneau-menu {
  position: absolute; right: 0; top: calc(100% + var(--e-2)); z-index: 60;
  min-width: max(200px, 100%);
  padding: var(--e-2);
  background: var(--surface-hi);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-l);
  box-shadow: var(--shadow-2);
  display: flex; flex-direction: column; gap: var(--e-1);
}
/* Ancré à gauche quand le menu est à gauche de son conteneur. */
.menu.a-gauche > .panneau-menu { right: auto; left: 0; }

.menu > .panneau-menu a,
.menu > .panneau-menu button {
  display: flex; align-items: center; gap: var(--e-2);
  min-height: 44px; padding: 0 var(--e-3);
  border: 0; border-radius: var(--r-m);
  background: none; color: var(--tx);
  font: inherit; font-weight: 500; text-align: left;
  cursor: pointer; white-space: nowrap;
}
.menu > .panneau-menu a:hover,
.menu > .panneau-menu button:hover { background: var(--surface-2); }
.menu > .panneau-menu a[aria-current] { font-weight: 700; color: var(--accent); }
.menu > .panneau-menu hr {
  border: 0; border-top: 1px solid var(--line); margin: var(--e-1) 0;
}
/* Un intertitre dans le menu : « Thème », « Affichage ». */
.menu > .panneau-menu .titre-groupe {
  padding: var(--e-1) var(--e-3) 0;
  font-size: .75rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--tx-3);
}
/* Un segmenté tient dans le menu, mais sur toute la largeur. */
.menu > .panneau-menu .segmente { display: flex; }
.menu > .panneau-menu .segmente button { flex: 1; justify-content: center; }

/* --- Le repli ------------------------------------------------------------
   Un `<details>` ordinaire garde le marqueur ▶ du navigateur : le socle ne
   stylisait `summary` que sous `.menu`, donc chaque app se débrouillait —
   ou ne se débrouillait pas.

   UNE CLASSE, PAS `details` NU. Styliser tout `<details>` du document
   imposerait une allure à un élément dont les usages diffèrent d'une app à
   l'autre. Une classe se demande ; un sélecteur d'élément s'impose.

   Le repli reste NATIF : le navigateur tient `aria-expanded` sur le
   `<summary>` tout seul, et le bloc s'ouvre sans script. Le dupliquer à la
   main créerait une seconde source de vérité, celle qui se désynchronise
   toujours en premier. */
.repli { border: 1px solid var(--line); border-radius: var(--r-m); background: var(--surface); }
.repli > summary {
  display: flex; align-items: center; gap: var(--e-2);
  min-height: 44px; padding: 0 var(--e-4);
  font-size: .9rem; color: var(--tx-2);
  cursor: pointer; list-style: none; user-select: none;
}
.repli > summary::-webkit-details-marker { display: none; }
.repli > summary::marker { content: ""; }
.repli > summary:hover { color: var(--tx); }
.repli > summary:focus-visible { outline: 2px solid var(--accent, var(--tx)); outline-offset: -2px; }
/* Le chevron est dessiné ici et tourne en CSS : rien à synchroniser en
   JavaScript, donc rien à désynchroniser. */
.repli > summary::before {
  content: ""; width: 7px; height: 7px; flex: 0 0 auto;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--t, .15s);
}
.repli[open] > summary::before { transform: rotate(-135deg) translate(-2px, -2px); }
.repli[open] > summary { border-bottom: 1px solid var(--line); }
.repli-corps { padding: var(--e-4); }

/* --- L'action principale -------------------------------------------------
   Une page a UNE action mise en avant. Cette classe est un EMPLACEMENT, pas
   un bouton : elle se pose sur le conteneur, et le bouton garde `.bouton`.

   Elle existe parce que « Démarrer la séance » de fitness touchait la carte
   suivante : sans rythme déclaré, l'espacement d'une action dépend de qui
   l'a écrite. */
.action-principale { margin: var(--e-section) 0; }
.action-principale > .bouton { width: 100%; }
@media (min-width: 641px) {
  .action-principale > .bouton { width: auto; min-width: 220px; }
}

/* --- Bandeau : à plat, ou replié ------------------------------------------
   Les deux formes coexistent dans le HTML rendu ; seule l'une est visible.
   C'est ce qui permet au repli de fonctionner sans JavaScript et sans que le
   serveur ait à connaître la largeur de l'écran. */
/* `.si-replie` — dans un menu, ce qui ne sert QUE lorsque la barre est
   repliée : les liens de navigation, qui sont déjà visibles à plat au-dessus
   du seuil. Les répéter dans le menu à toute largeur donnerait deux chemins
   vers la même page à trois centimètres l'un de l'autre. */
/* Sélecteur aligné sur `.menu > .panneau-menu a` (0,2,1) : une règle moins
   spécifique perdrait la cascade et les liens resteraient visibles à toute
   largeur. Vu à l'écran — deux chemins vers la même page à trois centimètres
   l'un de l'autre. */
.menu > .panneau-menu .si-replie { display: none; }

/* --- Un réglage dans le menu de compte ------------------------------------
   L'ambiance a quatre valeurs : un bouton cyclant demanderait jusqu'à trois
   clics pour revenir à celle qu'on voulait. C'est donc un <select> dans son
   propre formulaire — soumis sans JavaScript, comme les sélecteurs de rôle de
   la page d'administration. La densité, elle, n'a que deux valeurs : elle
   garde le bouton cyclant. */
.menu-reglage { display: flex; flex-direction: column; gap: var(--e-1); padding: var(--e-2) var(--e-3); }
.menu-reglage > label { font-size: .78rem; color: var(--tx-3); }
.menu-reglage select { width: 100%; min-height: 44px; font-size: 1rem; }
/* Un `.bouton` en colonne flex s'étire à la largeur du parent par défaut
   (`align-items: stretch`) — le même piège que `.pile`, corrigé là par
   `.formulaire-etroit .pile > .bouton`. Ici la carte « Apparence » de
   l'admin réutilise `.menu-reglage` hors `.pile` : sans cette règle, son
   bouton « Appliquer » prend toute la largeur de la carte alors que son
   jumeau du tableau de rôles (dans `.champ-action`) reste à la largeur de
   son texte — les deux font la même chose, ils doivent se ressembler. */
.menu-reglage .bouton { align-self: flex-start; }

@media (max-width: 1024px) {
  .bandeau nav.a-plat { display: none; }
  .bandeau .segmente.a-plat { display: none; }
  /* `flex` et non `block` : le séparateur et les liens s'empilent. */
  .menu > .panneau-menu .si-replie { display: flex; }
  /* Le bandeau redevient une seule ligne : la marque, puis le menu. */
  .bandeau .dedans { flex-wrap: nowrap; }
}
