@charset "UTF-8";
/* ==========================================================================
   WilyStore — feuille de style unique du store web (§6.3)
   --------------------------------------------------------------------------
   Charte Wily (D7) : rouge #b11226, noir #0f172a, blanc. Aucune dépendance,
   aucun framework CSS, aucun fichier de police externe : le site doit rester
   utilisable sur un téléphone d'entrée de gamme et sur un réseau à faible
   débit. C'est la raison d'être du store (« applications légères »).

   Contraintes à ne pas enfreindre en modifiant ce fichier :
     * La CSP (ST-11 / SecurityHeaders::build) déclare style-src 'self' :
       AUCUN attribut style="..." dans les templates, et le JavaScript ne doit
       pas écrire dans el.style.* — on agit par classes, par [hidden] et par
       attributs ARIA.
     * Les couleurs sémantiques (succès/avertissement/erreur/info) complètent
       la charte, elles ne la remplacent pas : le rouge de marque reste
       --wily-red et signale aussi les erreurs.
     * Tout texte affiché vient du dictionnaire (§4.1.1). Donc jamais de texte
       dans une propriété content: ; les pictogrammes sont des formes CSS.

   Sommaire
     1. Jetons (couleurs, espacement, typographie, ombres, z-index)
     2. Préférence de thème sombre
     3. Réinitialisation & base
     4. Typographie
     5. Mise en page
     6. Accessibilité (lien d'évitement, focus, masquage visuel)
     7. Boutons
     8. Formulaires
     9. En-tête & navigation
    10. Cartes, fiches d'application, fil d'Ariane et pagination
        (10.1 fil d'Ariane, 10.2 pagination, 10.3 fiche détaillée, 10.4 intro)
    11. Messages serveur (.flash)
    12. Notifications (.wily-toast)
    13. Confirmation (.wily-confirm)
    14. État de chargement
    15. Pied de page
    16. Console éditeur : onglets de langue et compteurs (D34, D35)
        16.1 Coquille de la console et parcours d'étapes (D36, D37)
    17. Vitrine du store : bandeau et rangées (D39)
    18. Préférences du système et impression
    19. Console développeur : espace de travail, bandeaux, fiches (D40)
    20. Promotion de publication — accueil de la vitrine
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Jetons
   -------------------------------------------------------------------------- */
:root {
  /* Couleurs de marque : Bleu Marine pour la vitrine publique du store */
  --wily-red: #0f2b5c;
  --wily-red-dark: #091c3d;
  --wily-red-soft: #eaf1fb;
  --wily-red-on-dark: #7bb0ff;
  --wily-black: #0f172a;
  --wily-white: #ffffff;

  /* Couleurs sémantiques */
  --wily-success: #0f7a45;
  --wily-success-soft: #e8f5ee;
  --wily-warning: #8a5a00;
  --wily-warning-soft: #fdf3e0;
  --wily-danger: #b11226;
  --wily-danger-soft: #fdecee;
  --wily-info: #0f2b5c;
  --wily-info-soft: #eaf1fb;

  /* Surfaces et texte — Fond bleuté élégant */
  --wily-bg: #f0f4f9;
  --wily-surface: #ffffff;
  --wily-surface-2: #e6edf7;
  --wily-surface-3: #dbe5f2;
  --wily-text: #0f172a;
  --wily-text-muted: #475569;
  --wily-border: #d8e2ee;
  --wily-border-strong: #c4d3e5;
  --wily-focus: #0f2b5c;
  --wily-overlay: rgba(15, 35, 70, 0.6);

  /* Typographie : pile système uniquement, pour ne dépendre d'aucun service
     tiers (font-src 'self' dans la CSP) et éviter un saut de mise en page. */
  --wily-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", sans-serif;
  --wily-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
    "Liberation Mono", monospace;

  --wily-text-xs: 0.75rem;
  --wily-text-sm: 0.875rem;
  --wily-text-base: 1rem;
  --wily-text-lg: 1.125rem;
  --wily-text-xl: 1.375rem;
  --wily-text-2xl: clamp(1.5rem, 1.2rem + 1.4vw, 1.875rem);
  --wily-text-3xl: clamp(1.875rem, 1.4rem + 2.4vw, 2.75rem);

  --wily-leading-tight: 1.25;
  --wily-leading-normal: 1.55;

  /* Espacement : échelle de 0.25rem, alignée sur les multiples de 4 px. */
  --wily-space-1: 0.25rem;
  --wily-space-2: 0.5rem;
  --wily-space-3: 0.75rem;
  --wily-space-4: 1rem;
  --wily-space-5: 1.5rem;
  --wily-space-6: 2rem;
  --wily-space-7: 3rem;
  --wily-space-8: 4rem;

  --wily-radius-sm: 6px;
  --wily-radius: 10px;
  --wily-radius-lg: 16px;
  --wily-radius-pill: 999px;

  /* Barre latérale de la console (D40). En `rem` et non en pixels : la largeur
     suit la taille de police choisie par l'utilisateur, comme tout le reste. */
  --wily-sidebar-width: 15.625rem;

  --wily-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
  --wily-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
  --wily-shadow-lg: 0 18px 40px rgba(15, 23, 42, 0.18);

  /* La confirmation passe au-dessus des notifications : un dialogue modal est
     bloquant, une notification ne l'est jamais. */
  --wily-z-header: 20;
  --wily-z-toast: 70;
  --wily-z-modal: 80;

  --wily-transition: 140ms ease-out;

  /* Surfaces sombres (barre latérale de la console, D36). Le noir de marque
     --wily-black sert de fond ; ces jetons donnent le texte, les séparateurs et
     les remplissages lisibles dessus, DANS LES DEUX THÈMES : un bloc toujours
     sombre ne peut pas réutiliser --wily-text, qui s'inverse avec le réglage
     système. Aucune couleur de marque n'est introduite (invariant n°4). */
  --wily-on-dark: #e6ebf4;
  --wily-on-dark-muted: #a8b4c8;
  --wily-on-dark-line: rgba(255, 255, 255, 0.16);
  --wily-on-dark-fill: rgba(255, 255, 255, 0.08);

  color-scheme: light;
}

/* --------------------------------------------------------------------------
   2. Préférence de thème sombre
   --------------------------------------------------------------------------
   Aucun bouton de bascule pour l'instant : on suit le réglage du système.
   Utile sur OLED et pour la lecture de nuit, sans doubler les templates.
   -------------------------------------------------------------------------- */
@media (prefers-color-scheme: dark) {
  :root {
    --wily-bg: #0a1120;
    --wily-surface: #101a30;
    --wily-surface-2: #162340;
    --wily-surface-3: #1e2f54;
    --wily-text: #e6ebf4;
    --wily-text-muted: #94a3b8;
    --wily-border: #1e3157;
    --wily-border-strong: #2b4478;
    --wily-focus: #7bb0ff;
    --wily-overlay: rgba(2, 6, 16, 0.75);

    --wily-red-soft: #17284d;
    --wily-success: #4cd18c;
    --wily-success-soft: #10291f;
    --wily-warning: #f0b429;
    --wily-warning-soft: #2c2210;
    --wily-danger: #ff8296;
    --wily-danger-soft: #35141c;
    --wily-info: #7bb0ff;
    --wily-info-soft: #14203a;

    color-scheme: dark;
  }
}

/* Préservation de l'identité originale pour la console développeur et le staff */
.console,
.staff-shell,
.admin {
  --wily-red: #b11226;
  --wily-red-dark: #8e0e1e;
  --wily-red-soft: #fdecee;
  --wily-red-on-dark: #ff6b7d;
  --wily-bg: #f6f7f9;
  --wily-surface: #ffffff;
  --wily-surface-2: #eef1f5;
  --wily-surface-3: #e3e8ef;
  --wily-border: #e2e8f0;
  --wily-border-strong: #cbd5e1;
}

/* --------------------------------------------------------------------------
   3. Réinitialisation & base
   -------------------------------------------------------------------------- */
*,
*::before,
*::after {
  box-sizing: border-box;
}

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

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background-color: var(--wily-bg);
  color: var(--wily-text);
  font-family: var(--wily-font-sans);
  font-size: var(--wily-text-base);
  line-height: var(--wily-leading-normal);
}

img,
svg,
video {
  max-width: 100%;
  height: auto;
  display: block;
}

[hidden] {
  display: none !important;
}

/* Un corps de page bloqué pendant un dialogue : le contenu derrière ne doit
   pas défiler, sinon le focus visuel et le focus clavier divergent. */
html.wily-modal-open,
body.wily-modal-open {
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   4. Typographie
   -------------------------------------------------------------------------- */
h1,
h2,
h3,
h4 {
  margin: 0 0 var(--wily-space-3);
  line-height: var(--wily-leading-tight);
  font-weight: 700;
  letter-spacing: -0.01em;
}

h1 {
  font-size: var(--wily-text-3xl);
}

h2 {
  font-size: var(--wily-text-2xl);
}

h3 {
  font-size: var(--wily-text-xl);
}

h4 {
  font-size: var(--wily-text-lg);
}

p {
  margin: 0 0 var(--wily-space-4);
}

a {
  color: var(--wily-red);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

a:hover {
  color: var(--wily-red-dark);
}

@media (prefers-color-scheme: dark) {
  a {
    color: var(--wily-red-on-dark);
  }

  a:hover {
    color: var(--wily-text);
  }
}

ul,
ol {
  margin: 0 0 var(--wily-space-4);
  padding-left: var(--wily-space-5);
}

code,
kbd,
samp {
  font-family: var(--wily-font-mono);
  font-size: 0.9em;
}

hr {
  border: 0;
  border-top: 1px solid var(--wily-border);
  margin: var(--wily-space-5) 0;
}

/* --------------------------------------------------------------------------
   5. Mise en page
   -------------------------------------------------------------------------- */
.container {
  width: 100%;
  max-width: 68rem;
  margin-inline: auto;
  padding-inline: var(--wily-space-4);
}

.container--narrow {
  max-width: 30rem;
}

/* Écran de TRAVAIL de la console (liste des fiches, D40) : la liste doit
   occuper TOUTE la largeur de la zone de droite. Le plafond de 68 rem laissait
   un vide à droite et décalait la recherche par rapport au tableau. Le
   `box-sizing` garde les marges internes DANS cette largeur : aucun débordement
   horizontal, même quand la barre latérale est ouverte. */
.container--wide {
  box-sizing: border-box;
  max-width: none;
  padding-inline: 0;
}

.section {
  padding-block: var(--wily-space-6);
}

.section__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--wily-space-3);
  margin-bottom: var(--wily-space-4);
}

.section__title {
  margin: 0;
  font-size: var(--wily-text-xl);
}

.grid {
  display: grid;
  gap: var(--wily-space-4);
  grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
}

.stack>*+* {
  margin-top: var(--wily-space-4);
}

.main {
  flex: 1 0 auto;
  padding-block: var(--wily-space-5) var(--wily-space-7);
}

/* --------------------------------------------------------------------------
   6. Accessibilité
   -------------------------------------------------------------------------- */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: var(--wily-space-2);
  top: -4rem;
  z-index: calc(var(--wily-z-header) + 1);
  padding: var(--wily-space-2) var(--wily-space-4);
  background-color: var(--wily-surface);
  color: var(--wily-text);
  border-radius: var(--wily-radius);
  box-shadow: var(--wily-shadow);
  transition: top var(--wily-transition);
}

.skip-link:focus {
  top: var(--wily-space-2);
}

/* Anneau de focus unique sur tout le site. On utilise :focus-visible pour ne
   pas afficher d'anneau sur un clic souris, mais l'anneau est indispensable
   au clavier : c'est la seule indication de position. */
:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
  border-radius: var(--wily-radius-sm);
}

/* Un titre de section qui sert d'ancre ne doit pas être masqué par l'en-tête
   collant lors d'un saut par ancre. */
:target {
  scroll-margin-top: 5rem;
}

/* --------------------------------------------------------------------------
   7. Boutons
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--wily-space-2);
  min-height: 2.75rem;
  /* cible tactile >= 44 px */
  padding: var(--wily-space-2) var(--wily-space-5);
  border: 1px solid transparent;
  border-radius: var(--wily-radius);
  background-color: var(--wily-surface-2);
  color: var(--wily-text);
  font: inherit;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  transition: background-color var(--wily-transition),
    border-color var(--wily-transition), color var(--wily-transition);
}

.btn:hover {
  background-color: var(--wily-surface-3);
}

.btn:active {
  transform: translateY(1px);
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
}

.btn--primary {
  background-color: var(--wily-red);
  border-color: var(--wily-red);
  color: var(--wily-white);
}

.btn--primary:hover {
  background-color: var(--wily-red-dark);
  border-color: var(--wily-red-dark);
  color: var(--wily-white);
}

.btn--ghost {
  background-color: transparent;
  border-color: var(--wily-border-strong);
  color: var(--wily-text);
}

.btn--ghost:hover {
  background-color: var(--wily-surface-2);
}

.btn--danger {
  background-color: transparent;
  border-color: var(--wily-red);
  color: var(--wily-red);
}

.btn--danger:hover {
  background-color: var(--wily-red-soft);
}

.btn--block {
  width: 100%;
}

.btn--link {
  min-height: auto;
  padding: 0;
  border: 0;
  background: none;
  color: var(--wily-red);
  font-weight: 500;
  text-decoration: underline;
}

.btn--link:hover {
  background: none;
  color: var(--wily-red-dark);
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-3);
}

/* --------------------------------------------------------------------------
   8. Formulaires
   -------------------------------------------------------------------------- */
.form {
  display: block;
}

.field {
  margin-bottom: var(--wily-space-4);
}

.field__label {
  display: block;
  margin-bottom: var(--wily-space-1);
  font-size: var(--wily-text-sm);
  font-weight: 600;
}

.field__hint {
  margin: var(--wily-space-1) 0 0;
  font-size: var(--wily-text-xs);
  color: var(--wily-text-muted);
}

.field__error {
  margin: var(--wily-space-1) 0 0;
  font-size: var(--wily-text-sm);
  font-weight: 600;
  color: var(--wily-danger);
}

.field__required {
  color: var(--wily-red);
}

.input,
.select,
.textarea {
  display: block;
  width: 100%;
  /* 16 px minimum : en dessous, Safari iOS zoome automatiquement au focus et
     l'utilisateur perd le contexte du formulaire. */
  font-size: max(1rem, var(--wily-text-base));
  font-family: inherit;
  line-height: 1.4;
  color: var(--wily-text);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border-strong);
  border-radius: var(--wily-radius);
  padding: 0.65rem 0.75rem;
  transition: border-color var(--wily-transition), box-shadow var(--wily-transition);
}

.textarea {
  min-height: 7rem;
  resize: vertical;
}

.input:hover,
.select:hover,
.textarea:hover {
  border-color: var(--wily-text-muted);
}

.input:focus-visible,
.select:focus-visible,
.textarea:focus-visible {
  border-color: var(--wily-red);
  outline-offset: 0;
}

.input[aria-invalid="true"],
.textarea[aria-invalid="true"],
.select[aria-invalid="true"] {
  border-color: var(--wily-danger);
  background-color: var(--wily-danger-soft);
}

.checkbox {
  display: flex;
  align-items: flex-start;
  gap: var(--wily-space-2);
  font-size: var(--wily-text-sm);
}

.checkbox input {
  width: 1.15rem;
  height: 1.15rem;
  margin-top: 0.15rem;
  accent-color: var(--wily-red);
}

/* --------------------------------------------------------------------------
   9. En-tête & navigation
   -------------------------------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--wily-z-header);
  background-color: var(--wily-surface);
  border-bottom: 1px solid var(--wily-border);
}

.site-header__inner {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--wily-space-2);
  padding-block: var(--wily-space-2);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--wily-space-2);
  color: var(--wily-text);
  font-weight: 700;
  font-size: var(--wily-text-lg);
  text-decoration: none;
  letter-spacing: -0.02em;
}

.brand:hover {
  color: var(--wily-text);
}

.brand__logo {
  height: 2rem;
  width: auto;
}

.brand__mark {
  color: var(--wily-red);
}

/* -- En-tête compact : trois commandes (D39) -------------------------------
   La barre complète — marque, trois liens, recherche, sélecteur de langue,
   deux boutons — occupait plusieurs lignes avant le premier contenu. Elle est
   remplacée, à TOUTES les tailles d'écran, par une seule ligne :

       [☰]        WILYSTORE           [loupe]  [compte]

   - `[☰]` ouvre le tiroir de gauche (navigation + catégories) ;
   - `[loupe]` déplie le champ de recherche sous la barre (section 17) ;
   - `[compte]` ouvre le tiroir de droite (langue + compte).

   Le bureau ne fait plus exception : garder une barre large d'un côté et des
   tiroirs de l'autre obligeait à maintenir deux navigations qui finissaient
   par diverger. Seul le centrage de la marque reste propre au téléphone.

   Un `<details>` est un composant NATIF : il s'ouvre sans JavaScript. Le script
   n'ajoute que l'exclusivité, la fermeture par Échap, le clic au-dehors pour la
   recherche, le focus et le verrou de défilement (wily-ui.js,
   `bindHeaderDrawers`). Le dessin des tiroirs vit en section 18.1. */
.header-drawer {
  display: block;
  flex: 0 0 auto;
}

/* `order` plutôt que l'ordre du document : le balisage se lit dans l'ordre
   logique (menu, marque, recherche, compte) et le CSS n'a rien à réordonner
   pour le lecteur d'écran. */
.header-drawer--start {
  order: 1;
}

.site-header__inner .brand {
  order: 2;
  margin-inline-end: auto;
}

.header-drawer--search {
  order: 3;
}

.header-drawer--end {
  order: 4;
}

.site-nav__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-nav__link {
  display: inline-block;
  padding: var(--wily-space-2) var(--wily-space-3);
  border-radius: var(--wily-radius);
  color: var(--wily-text);
  font-size: var(--wily-text-sm);
  font-weight: 600;
  text-decoration: none;
}

.site-nav__link:hover {
  background-color: var(--wily-surface-2);
  color: var(--wily-text);
}

.site-nav__link[aria-current="page"] {
  color: var(--wily-red);
  background-color: var(--wily-red-soft);
}

.lang-switcher {
  display: flex;
  align-items: center;
  gap: var(--wily-space-2);
  margin: 0;
}

/* Variante LIENS (A-02). Une page publique anonyme peut être servie SANS
   session — c'est ce qui la rend cachable par un CDN — donc sans jeton CSRF :
   le <select> POST ne pourrait pas aboutir. Le changeur devient une suite de
   liens préfixés (/en/categories), qui portent déjà la langue dans l'URL (D16).
   Même encombrement et même cible tactile que le sélecteur, aucun formulaire
   mort, aucune bordure ni ombre ajoutée (QUALITÉ #12). */
.lang-switcher__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.5rem;
  min-height: 2.5rem;
  padding: 0 var(--wily-space-2);
  border-radius: var(--wily-radius-sm);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  font-weight: 600;
  text-decoration: none;
}

.lang-switcher__link:hover,
.lang-switcher__link:focus-visible {
  color: var(--wily-text);
}

.lang-switcher__link[aria-current="true"] {
  color: var(--wily-red);
  background-color: var(--wily-red-soft);
}

.lang-switcher__select {
  width: auto;
  min-height: 2.5rem;
  padding: var(--wily-space-1) var(--wily-space-2);
  font-size: var(--wily-text-sm);
}

/* Le bouton reste toujours visible : sans JavaScript, changer de langue doit
   rester possible (c'est un critère de sortie du lot L0). */
.lang-switcher__submit {
  min-height: 2.5rem;
  padding: var(--wily-space-1) var(--wily-space-3);
  font-size: var(--wily-text-sm);
}

@media (min-width: 48rem) {
  .lang-switcher__submit {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   10. Cartes et fiches d'application
   -------------------------------------------------------------------------- */
.card {
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  padding: var(--wily-space-5);
  box-shadow: var(--wily-shadow-sm);
}

.card--auth {
  max-width: 30rem;
  margin-inline: auto;
}

.card__title {
  margin-bottom: var(--wily-space-2);
}

.card__subtitle {
  margin-bottom: var(--wily-space-5);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

/* --------------------------------------------------------------------------
   Connexion Google (page de connexion)
   -------------------------------------------------------------------------- */
.auth__divider {
  display: flex;
  align-items: center;
  gap: var(--wily-space-3);
  margin-block: var(--wily-space-4);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
}

.auth__divider::before,
.auth__divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background-color: var(--wily-border);
}

.auth__google {
  display: block;
}

.auth__google #google-signin-btn {
  display: flex;
  justify-content: center;
  min-height: 44px;
}

/* --------------------------------------------------------------------------
   QR Code d'enrôlement du second facteur

   Le conteneur est vide dans le gabarit : c'est `assets/js/wily-twofactor-qr.js`
   qui y trace le QR (canvas + reflet PNG `data:`), à partir de `data-qr-uri`.

   POURQUOI LES COULEURS NE SUIVENT PAS LES JETONS DE THÈME
   --------------------------------------------------------
   Un QR se lit par contraste : modules NOIRS sur fond BLANC. Le bureau du DG
   est sombre en permanence (`data-wily-theme="dark"`) ; un QR dessiné avec les
   jetons de thème y deviendrait noir sur noir. Le blanc et le noir sont donc
   écrits ici en dur, comme une contrainte de lisibilité et non comme un choix
   graphique.

   Le rembourrage tient lieu de « zone calme » (quiet zone) exigée par la
   norme : sans elle, un lecteur bute sur le bord du bloc et le scan échoue —
   d'où le fond blanc qui DÉBORDE autour du dessin.
   -------------------------------------------------------------------------- */
.twofactor-qr {
  display: block;
  width: fit-content;
  margin-block: var(--wily-space-2);
  padding: var(--wily-space-3);
  background-color: #ffffff;
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius);
  line-height: 0;
}

/* qrcodejs empile parfois un canvas et une image ; seul l'un des deux est
   visible, et aucun ne doit dépasser de la carte (30rem au plus). */
.twofactor-qr canvas,
.twofactor-qr img {
  display: block;
  max-width: 100%;
  height: auto;
}

.app-card {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-2);
  padding: var(--wily-space-3);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius);
  color: inherit;
  text-decoration: none;
}

.app-card:hover {
  border-color: var(--wily-border-strong);
  box-shadow: var(--wily-shadow-sm);
  color: inherit;
}

/* Aucune règle `.app-card__icon` ici : la vignette d'une carte est rendue par
   `partials/app-icon.php` (`.app-icon--sm|md|lg`). L'ancienne règle survécut à
   la refonte D39 et n'habillait plus aucun élément — une règle orpheline fait
   croire à un composant encore disponible, exactement ce que la suppression de
   `section--chips` a évité. */

.app-card__name {
  margin: 0;
  font-size: var(--wily-text-base);
  font-weight: 700;
  /* Un nom d'application long ne doit pas casser la grille. */
  overflow-wrap: anywhere;
}

.app-card__meta {
  margin: 0;
  font-size: var(--wily-text-xs);
  color: var(--wily-text-muted);
}

.badge {
  display: inline-block;
  padding: 0.1rem var(--wily-space-2);
  border-radius: var(--wily-radius-pill);
  background-color: var(--wily-surface-2);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  font-weight: 600;
}

.badge--free {
  background-color: var(--wily-success-soft);
  color: var(--wily-success);
}

.badge--paid {
  background-color: var(--wily-red-soft);
  color: var(--wily-danger);
}

/*
 * Badge de livraison (« Cloud » pour un SaaS, « Instantané » pour une PWA).
 * Il occupe la place de la taille de fichier sur une fiche web : ce n'est pas
 * une information de poids, c'est un mode de service. Le rouge de marque le
 * distingue des badges déclaratifs (prix, PEGI) sans introduire de couleur
 * nouvelle — la charte n'en autorise qu'une.
 */
.badge--delivery {
  background-color: var(--wily-red-soft);
  color: var(--wily-red-dark);
}

/*
 * Badges de PLATEFORME (console, colonne « plate-forme ») : la valeur vient de
 * `apps.delivery` — `apk`, `pwa` ou `saas`. Trois teintes déjà présentes dans
 * la charte, aucune couleur nouvelle ajoutée :
 *   - PWA : aplat rouge de marque (le mode que la maison met en avant) ;
 *   - SaaS : ambre doux, la teinte des services en ligne ;
 *   - APK : bleu d'information, celui des binaires à télécharger.
 * Le contraste texte/fond reste au-dessus de 4.5:1 dans les deux thèmes, car
 * chaque paire existe déjà ailleurs dans la feuille.
 */
.badge--pwa {
  background-color: var(--wily-red);
  color: var(--wily-white);
}

.badge--saas {
  background-color: var(--wily-warning-soft);
  color: var(--wily-warning);
}

.badge--android {
  background-color: var(--wily-info-soft);
  color: var(--wily-info);
}

/*
 * Ligne d'informations d'une carte : badge de type, prix, note, installations.
 * Le flex remplace une suite de séparateurs typographiques (« • » ou « | »),
 * qui seraient du texte affiché hors dictionnaire et donc interdits (§4.1.1).
 */
.app-card__facts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-2) var(--wily-space-3);
  margin: 0;
  font-size: var(--wily-text-xs);
  color: var(--wily-text-muted);
}

/* Bouton neutralisé (par exemple un téléchargement pas encore ouvert) : il doit
   rester lisible et non cliquable, sans disparaître — l'information « cette
   action existe mais n'est pas ouverte » est justement ce qu'il porte. */
.btn:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

.btn:disabled:hover {
  background-color: inherit;
  border-color: inherit;
  color: inherit;
}

/* --------------------------------------------------------------------------
   10.1 Fil d'Ariane (.breadcrumb)
   --------------------------------------------------------------------------
   Le séparateur entre deux entrées est une BORDURE, jamais un caractère : un
   « / » écrit en CSS contournerait le dictionnaire (§4.1.1). Le fil n'est rendu
   que s'il compte au moins deux entrées (voir templates/partials/breadcrumb.php).
   -------------------------------------------------------------------------- */
.breadcrumb {
  margin-bottom: var(--wily-space-4);
  font-size: var(--wily-text-sm);
}

.breadcrumb__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.breadcrumb__item {
  display: flex;
  align-items: center;
  gap: var(--wily-space-2);
}

.breadcrumb__item+.breadcrumb__item {
  padding-inline-start: var(--wily-space-2);
  border-inline-start: 1px solid var(--wily-border-strong);
}

.breadcrumb__link {
  color: var(--wily-text-muted);
}

.breadcrumb__link:hover {
  color: var(--wily-red);
}

.breadcrumb__current {
  color: var(--wily-text);
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   10.2 Pagination (.pagination)
   --------------------------------------------------------------------------
   Deux liens au plus (précédent / suivant) et l'indicateur « Page n sur N » :
   les numéros de page ne sont pas affichés, faute de libellé correspondant dans
   le dictionnaire — une plage de numéros sans texte de remplacement accessible
   serait muette au lecteur d'écran.
   -------------------------------------------------------------------------- */
.pagination {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--wily-space-3);
  margin-top: var(--wily-space-5);
}

.pagination__status {
  margin: 0;
  font-size: var(--wily-text-sm);
  color: var(--wily-text-muted);
}

.pagination__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.pagination__link {
  display: inline-flex;
  align-items: center;
  min-height: 2.5rem;
  padding-inline: var(--wily-space-3);
  border: 1px solid var(--wily-border-strong);
  border-radius: var(--wily-radius-pill);
  color: inherit;
  font-size: var(--wily-text-sm);
  font-weight: 600;
  text-decoration: none;
}

.pagination__link:hover {
  border-color: var(--wily-red);
  color: var(--wily-red);
}

/* --------------------------------------------------------------------------
   10.3 Fiche d'application (.app-detail)
   --------------------------------------------------------------------------
   La fiche tient sur un écran : un bandeau d'identité (vignette, titre, résumé,
   badges) avec l'action principale à sa droite, une rangée de chiffres, puis
   deux panneaux. Aucune décoration textuelle n'est écrite ici : les espacements
   et les séparateurs sont des bordures, jamais des caractères (§4.1.1).
   -------------------------------------------------------------------------- */
.app-detail {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-4);
}

.app-detail__hero {
  display: grid;
  gap: var(--wily-space-4);
  padding: var(--wily-space-5);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  box-shadow: var(--wily-shadow-sm);
}

.app-detail__identity {
  display: flex;
  align-items: flex-start;
  gap: var(--wily-space-4);
  min-width: 0;
}

.app-detail__intro {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-2);
  min-width: 0;
}

.app-detail__title {
  margin: 0;
  font-size: var(--wily-text-2xl);
}

.app-detail__lede {
  margin: 0;
  color: var(--wily-text-muted);
}

.app-detail__badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-2);
  margin: 0;
}

.app-detail__aside {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-2);
}

.app-detail__note {
  margin: 0;
  font-size: var(--wily-text-sm);
  color: var(--wily-text-muted);
}

/* ÉDITEUR DE PLANS (onglet « Accès ») -------------------------------------
   Une case par type de plan, deux contrôles par ligne : « je le propose » et
   « c'est le plus élevé ». Aucun champ texte, donc rien à aligner sur des
   colonnes : deux lignes souples suffisent, et la cible reste à 32 px. */
.plans-editor {
  margin: var(--wily-space-4) 0 0;
  padding: var(--wily-space-3) var(--wily-space-4);
  background-color: var(--wily-surface-2);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
}

.plans-editor .field__hint {
  margin: 0 0 var(--wily-space-2);
}

.plans-editor__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-4);
  padding: var(--wily-space-1) 0;
}

.plans-editor__pick {
  display: flex;
  align-items: center;
  gap: var(--wily-space-2);
  min-height: 32px;
  font-size: var(--wily-text-sm);
  color: var(--wily-text);
  cursor: pointer;
}

/* Le second contrôle est en retrait : même ligne, mais secondaire. C'est un
   qualificatif (« le plus élevé »), pas un second choix. */
.plans-editor__pick+.plans-editor__pick {
  color: var(--wily-text-muted);
}

.plans-editor input[type='checkbox'],
.plans-editor input[type='radio'] {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--wily-red);
}

.plans-snippets {
  margin-top: var(--wily-space-3);
}

.plans-snippets>summary {
  cursor: pointer;
}

/* Trois colonnes de chiffres, comme la vitrine : la VALEUR en grand, son
   intitulé en dessous, un filet entre les colonnes. Une grille de trois parts
   égales garde les trois chiffres alignés quel que soit leur nombre de
   caractères ; `minmax(0, 1fr)` autorise l'ellipse plutôt qu'un débordement. */
.app-detail__stats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--wily-border);
  border-bottom: 1px solid var(--wily-border);
}

.app-detail__stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--wily-space-3) var(--wily-space-2);
  text-align: center;
  border-left: 1px solid var(--wily-border);
}

/* Le premier chiffre n'a pas de filet à sa gauche : il touche le bord du bloc. */
.app-detail__stat:first-child {
  border-left: 0;
}

.app-detail__stat-value {
  max-width: 100%;
  overflow: hidden;
  font-size: var(--wily-text-lg);
  font-weight: 700;
  color: var(--wily-text);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-detail__stat-label {
  font-size: var(--wily-text-xs);
  color: var(--wily-text-muted);
}

.app-detail__panels {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-5);
  margin-top: var(--wily-space-5);
}

.app-detail__panel {
  padding: var(--wily-space-5);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  box-shadow: 0 1px 3px rgba(15, 43, 92, 0.04);
}

.app-detail__panel-title {
  margin: 0 0 var(--wily-space-3);
  font-size: var(--wily-text-lg);
}

/* Le texte saisi par l'éditeur conserve ses retours à la ligne : `pre-line`
   plie les lignes longues mais respecte les paragraphes écrits. */
.app-detail__text {
  margin: 0;
  line-height: var(--wily-leading-normal);
  white-space: pre-line;
}

.app-detail__list {
  margin: 0;
}

.app-detail__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--wily-space-1) var(--wily-space-4);
  padding: var(--wily-space-2) 0;
  border-top: 1px solid var(--wily-border);
  font-size: var(--wily-text-sm);
}

.app-detail__row:first-child {
  padding-top: 0;
  border-top: 0;
}

.app-detail__row:last-child {
  padding-bottom: 0;
}

.app-detail__row dt {
  color: var(--wily-text-muted);
}

.app-detail__row dd {
  margin: 0;
  text-align: right;
}

/* Une valeur longue (empreinte SHA-256) passe sous son intitule et prend toute
   la largeur : alignee a droite, elle serait coupee au premier retour a la ligne
   sans qu'on voie ou commence la valeur a comparer. */
.app-detail__row--value dd {
  flex: 1 1 100%;
  text-align: left;
}

/* Langues declarees du produit : la langue principale, puis le raccourci
   « et N autres langues ». La ligne tient sur UNE hauteur : la fiche ne
   s'allonge pas avec quarante langues (QUALITE #12). L'espacement vient du
   gap : aucun separateur typographique (§4.1.1). */
.app-detail__langs-line {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-2);
  align-items: center;
  justify-content: flex-end;
}

.app-detail__lang-main {
  padding: 1px var(--wily-space-2);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-sm);
  background: var(--wily-surface-2);
  color: var(--wily-text);
  font-size: var(--wily-text-xs);
  line-height: var(--wily-leading-normal);
  white-space: nowrap;
}

/* Raccourci : un vrai bouton, libelle explicite et cible tactile de 44 px.
   Aucun ornement — un seul soulignement, et pas de bordure (QUALITE #12). */
.app-detail__langs-more {
  min-height: 44px;
  padding: 0 var(--wily-space-1);
  border: 0;
  background: none;
  color: var(--wily-text);
  font: inherit;
  font-size: var(--wily-text-xs);
  text-align: end;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  cursor: pointer;
}

.app-detail__langs-more:hover {
  text-decoration-thickness: 2px;
}

.app-detail__langs-more:focus-visible {
  outline: 2px solid var(--wily-focus);
  outline-offset: 2px;
}

/* Chasse fixe obligatoire pour une empreinte, et coupure autorisee : 64
   caracteres hexadecimaux ne doivent ni deborder du panneau ni provoquer un
   defilement horizontal de la page. */
.app-detail__code {
  font-family: var(--wily-font-mono);
  font-size: var(--wily-text-xs);
  line-height: var(--wily-leading-normal);
  overflow-wrap: anywhere;
}

/* Note d'integrite : ce que les empreintes prouvent, et ce qu'elles ne
   prouvent pas. Separee des valeurs par un filet, jamais par un titre de plus. */
.app-detail__integrity-note {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-2);
  margin-top: var(--wily-space-3);
  padding-top: var(--wily-space-3);
  border-top: 1px solid var(--wily-border);
}

.app-detail__integrity-note strong {
  display: block;
  margin-bottom: var(--wily-space-1);
  color: var(--wily-text);
}

.app-detail__integrity-link {
  font-size: var(--wily-text-sm);
  font-weight: 600;
}

/*
 * Documents légaux d'un service tiers (mentions légales, confidentialité,
 * conditions). Ce sont des liens sortants : ils sont donc listés, pas glissés
 * dans une phrase — le visiteur doit pouvoir viser le bon document. Aucun
 * séparateur typographique n'est écrit ici (§4.1.1) : l'espacement vient du
 * `gap`, et une adresse de document très longue se plie au lieu de déborder
 * (`overflow-wrap`), sans hauteur fixe — un texte ne doit jamais être rogné.
 */
.app-detail__links {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-1);
  margin: var(--wily-space-3) 0 0;
  padding: 0;
  list-style: none;
}

.app-detail__links-item {
  font-size: var(--wily-text-sm);
  overflow-wrap: anywhere;
}

.app-detail__links-item a {
  color: var(--wily-red);
  font-weight: 600;
  text-decoration: none;
}

.app-detail__links-item a:hover,
.app-detail__links-item a:focus-visible {
  text-decoration: underline;
}

/* =============================================================================
   Spécifications & Métadonnées d'application (Style Uptodown)
   ============================================================================= */
.app-specs-group {
  margin-top: var(--wily-space-4);
  padding-top: var(--wily-space-4);
  border-top: 1px solid var(--wily-border);
}

.app-specs-group:first-of-type {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.app-specs-subtitle {
  margin: 0 0 var(--wily-space-3);
  font-size: var(--wily-text-sm);
  font-weight: 700;
  color: var(--wily-text);
  letter-spacing: -0.01em;
}

.app-specs-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--wily-space-4);
}

.app-spec-card {
  display: flex;
  align-items: center;
  gap: var(--wily-space-3);
  min-width: 0;
}

.app-spec-card--full {
  grid-column: 1 / -1;
  align-items: flex-start;
}

.app-spec-card--full .app-spec-icon-box {
  margin-top: 2px;
}

.app-spec-icon-box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  border-radius: var(--wily-radius-md);
  background-color: var(--wily-red-soft);
  border: 1px solid rgba(15, 43, 92, 0.14);
  color: var(--wily-red);
  box-shadow: 0 1px 2px rgba(15, 43, 92, 0.04);
  transition: transform 0.15s ease;
}

.app-spec-card:hover .app-spec-icon-box {
  transform: translateY(-1px);
}

.app-spec-icon-box .wily-icon {
  width: 18px;
  height: 18px;
}

.app-spec-content {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.app-spec-label {
  font-size: var(--wily-text-xs);
  font-weight: 500;
  color: var(--wily-text-muted);
  line-height: 1.25;
  margin-bottom: 2px;
}

.app-spec-value {
  font-size: var(--wily-text-sm);
  font-weight: 600;
  color: var(--wily-text);
  line-height: var(--wily-leading-tight);
  word-break: break-word;
}

.app-spec-value a {
  color: var(--wily-red);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.app-spec-value a:hover {
  text-decoration-thickness: 2px;
}

.app-spec-value--verified {
  color: #0d6e40;
}

/* Sur un téléphone, le rembourrage est resserré : la vignette et le texte
   partagent la même ligne, et chaque rem gagné revient au titre. */
@media (max-width: 47.99rem) {

  .app-detail__hero,
  .app-detail__panel {
    padding: var(--wily-space-4);
  }
}

@media (min-width: 48rem) {
  .app-detail__hero {
    grid-template-columns: minmax(0, 1fr) minmax(12rem, 16rem);
    align-items: start;
  }
}


/* --------------------------------------------------------------------------
   10.4 Texte d'introduction d'une page de contenu (.section__intro)
   --------------------------------------------------------------------------
   Le paragraphe qui suit un titre de page : plus large qu'une carte, plus court
   qu'un article. Il sert aussi bien à la description d'une catégorie qu'au
   résumé d'une fiche.
   -------------------------------------------------------------------------- */
.section__intro {
  max-width: 48rem;
  margin: 0 0 var(--wily-space-5);
  color: var(--wily-text-muted);
}

/* --------------------------------------------------------------------------
   10.4b Texte légal structuré (.legal-doc)
   --------------------------------------------------------------------------
   Les cinq pages /legal/* partagent une charpente unique
   (templates/legal/_document.php) : un titre, un chapeau, la date de mise à
   jour, puis des clauses. La numérotation est portée par <ol> et son marqueur
   natif : elle reste juste si une clause est ajoutée ou retirée, et aucun
   chiffre n'entre dans un dictionnaire (§4.1.1). La densité ne se paie pas en
   lisibilité : les corps conservent un interligne large et une mesure bornée
   (QUALITÉ #12).
   -------------------------------------------------------------------------- */
.legal-doc {
  display: grid;
  gap: var(--wily-space-4);
  margin: 0 0 var(--wily-space-5);
  padding-left: var(--wily-space-5);
  list-style: decimal;
}

.legal-doc__item {
  min-width: 0;
  padding-left: var(--wily-space-2);
}

.legal-doc__item::marker {
  color: var(--wily-text-muted);
  font-weight: 600;
}

.legal-doc__title {
  margin: 0 0 var(--wily-space-2);
  font-size: var(--wily-text-lg);
  line-height: 1.3;
}

.legal-doc__body {
  max-width: 48rem;
  margin: 0;
  color: var(--wily-text-muted);
  line-height: 1.65;
}

.legal-doc__meta {
  margin: 0 0 var(--wily-space-4);
  font-size: var(--wily-text-sm);
  color: var(--wily-text-muted);
}

/* --------------------------------------------------------------------------
   10.5 Hiérarchie des catégories (.category-tree)
   --------------------------------------------------------------------------
   L'arbre est rendu par un partiel récursif (templates/partials/category-tree.php).
   Chaque niveau est une grille de cartes ; les enfants s'inscrivent sous la carte
   du parent, décalés par une BORDURE et non par un caractère (une puce écrite en
   CSS échapperait au dictionnaire, §4.1.1).
   -------------------------------------------------------------------------- */
.category-tree {
  display: grid;
  gap: var(--wily-space-4);
  grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}

.category-tree__item {
  display: grid;
  gap: var(--wily-space-3);
  min-width: 0;
}

.category-tree .category-tree {
  grid-column: 1 / -1;
  padding-inline-start: var(--wily-space-4);
  border-inline-start: 2px solid var(--wily-border-strong);
}

/* Une carte de catégorie n'est qu'un titre et, éventuellement, une description et
   un compte : elle s'aligne en haut plutôt que de s'étirer sur la ligne. */
.app-card--category {
  justify-content: flex-start;
}

.section__subtitle {
  margin: 0 0 var(--wily-space-4);
  font-size: var(--wily-text-lg);
}

.category-children {
  margin-bottom: var(--wily-space-5);
}

/* --------------------------------------------------------------------------
   10.6 Centre d'aide et guide de première visite
   --------------------------------------------------------------------------
   Le guide (.guide) est rendu par le serveur avec l'attribut hidden : il
   n'apparaît qu'une fois, après révélation par wily-ui.js. Sans JavaScript, il
   reste invisible et la page n'est jamais bloquée ; le centre d'aide (/aide)
   tient alors lieu de documentation permanente. Aucun style en ligne : la CSP
   n'autorise que style-src 'self'.
   -------------------------------------------------------------------------- */
.guide {
  padding-block: var(--wily-space-5);
  border-bottom: 1px solid var(--wily-border);
  background-color: var(--wily-surface-2);
}

.guide__title {
  margin: 0 0 var(--wily-space-2);
  font-size: var(--wily-text-lg);
}

.guide__intro {
  max-width: 48rem;
  margin: 0 0 var(--wily-space-4);
  color: var(--wily-text-muted);
}

.guide__steps,
.help-steps {
  margin: 0 0 var(--wily-space-5);
  padding-left: var(--wily-space-5);
}

.guide__step,
.help-step {
  margin-bottom: var(--wily-space-4);
}

.guide__step:last-child,
.help-step:last-child {
  margin-bottom: 0;
}

.guide__step-title,
.help-step__title {
  margin: 0 0 var(--wily-space-1);
  font-size: var(--wily-text-base);
}

.guide__step p,
.help-step p {
  margin: 0;
  color: var(--wily-text-muted);
}

.help-cta {
  margin-top: var(--wily-space-6);
  padding: var(--wily-space-5);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius);
  background-color: var(--wily-surface-2);
}

.help-cta p {
  margin: 0 0 var(--wily-space-4);
  color: var(--wily-text-muted);
}

/* --------------------------------------------------------------------------
   10.7 Recherche — formulaire d'en-tête (.site-search) et de page (.search-form)
   --------------------------------------------------------------------------
   Deux formulaires, un seul geste : saisir un terme, valider, arriver sur
   /recherche. Celui de l'en-tête tient sur une ligne (champ + bouton) ; celui
   de la page porte une étiquette visible et respire davantage. Tous deux
   reprennent l'apparence des champs du §8 pour qu'un formulaire Wily se
   reconnaisse d'une page à l'autre. Aucun style en ligne (CSP style-src 'self').
   -------------------------------------------------------------------------- */
.site-search {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-1);
  margin: 0;
}

.site-search__input {
  flex: 1 1 12rem;
  min-width: 0;
  /* Même plancher de 16 px que .input : Safari iOS ne zoome pas au focus. */
  font-size: max(1rem, var(--wily-text-base));
  font-family: inherit;
  line-height: 1.4;
  color: var(--wily-text);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border-strong);
  border-radius: var(--wily-radius-pill);
  padding: var(--wily-space-1) var(--wily-space-3);
  transition: border-color var(--wily-transition), box-shadow var(--wily-transition);
}

.site-search__input:hover {
  border-color: var(--wily-text-muted);
}

.site-search__input:focus-visible {
  border-color: var(--wily-red);
  outline-offset: 0;
}

.site-search__submit {
  flex: 0 0 auto;
  border-radius: var(--wily-radius-pill);
}

.search-form {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--wily-space-3);
  margin: 0 0 var(--wily-space-5);
}

.search-form__label {
  flex-basis: 100%;
  font-size: var(--wily-text-sm);
  font-weight: 600;
}

.search-form__input {
  flex: 1 1 16rem;
  min-width: 0;
  /* Cf. .input : 16 px minimum contre le zoom automatique d'iOS. */
  font-size: max(1rem, var(--wily-text-base));
  font-family: inherit;
  line-height: 1.4;
  color: var(--wily-text);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border-strong);
  border-radius: var(--wily-radius);
  padding: 0.65rem 0.75rem;
  transition: border-color var(--wily-transition), box-shadow var(--wily-transition);
}

.search-form__input:hover {
  border-color: var(--wily-text-muted);
}

.search-form__input:focus-visible {
  border-color: var(--wily-red);
  outline-offset: 0;
}

/* Le champ de recherche de l'en-tête vit désormais dans un dépliant plein
   largeur (section 18.1) : il n'a plus besoin d'un point de rupture, la
   largeur du panneau fait déjà tout le travail. */

.hero {
  padding-block: var(--wily-space-6);
  background-color: var(--wily-surface);
  border-bottom: 1px solid var(--wily-border);
}

.hero__title {
  max-width: 32rem;
}

.hero__subtitle {
  max-width: 34rem;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-lg);
}

.empty-state {
  padding: var(--wily-space-6);
  border: 1px dashed var(--wily-border-strong);
  border-radius: var(--wily-radius);
  color: var(--wily-text-muted);
  text-align: center;
}

/* --------------------------------------------------------------------------
   11. Messages serveur (.flash)
   --------------------------------------------------------------------------
   Rendu par le layout après une redirection (Flash::consume). Sans JavaScript
   c'est le seul retour visible ; avec JavaScript, wily-ui.js retire l'élément
   et affiche une notification : le HTML ne change pas pour autant.
   -------------------------------------------------------------------------- */
.flash {
  display: flex;
  align-items: flex-start;
  gap: var(--wily-space-2);
  margin-bottom: var(--wily-space-4);
  padding: var(--wily-space-3) var(--wily-space-4);
  border: 1px solid transparent;
  border-inline-start-width: 4px;
  border-radius: var(--wily-radius);
  font-size: var(--wily-text-sm);
}

.flash--success {
  background-color: var(--wily-success-soft);
  border-inline-start-color: var(--wily-success);
}

.flash--error {
  background-color: var(--wily-danger-soft);
  border-inline-start-color: var(--wily-danger);
}

.flash--warning {
  background-color: var(--wily-warning-soft);
  border-inline-start-color: var(--wily-warning);
}

.flash--info {
  background-color: var(--wily-info-soft);
  border-inline-start-color: var(--wily-info);
}

.flash__message {
  margin: 0;
  color: var(--wily-text);
}

/* --------------------------------------------------------------------------
   12. Notifications (.wily-toast)
   --------------------------------------------------------------------------
   Empilées en bas sur mobile (zone du pouce), en haut à droite sur grand
   écran. Chaque notification porte son propre rôle live : success/info/
   warning annoncent poliment, error interrompt (role="alert").
   -------------------------------------------------------------------------- */
.wily-toast-region {
  position: fixed;
  inset-inline: var(--wily-space-4);
  bottom: var(--wily-space-4);
  z-index: var(--wily-z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-2);
  /* Le conteneur ne doit jamais intercepter un clic destiné à la page. */
  pointer-events: none;
}

@media (min-width: 48rem) {
  .wily-toast-region {
    inset-inline: auto var(--wily-space-5);
    top: var(--wily-space-5);
    bottom: auto;
    width: min(24rem, calc(100vw - 2 * var(--wily-space-5)));
  }
}

.wily-toast {
  display: flex;
  align-items: flex-start;
  gap: var(--wily-space-2);
  padding: var(--wily-space-3) var(--wily-space-4);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-inline-start: 4px solid var(--wily-info);
  border-radius: var(--wily-radius);
  box-shadow: var(--wily-shadow);
  pointer-events: auto;
  animation: wily-toast-in 180ms ease-out;
}

.wily-toast--success {
  border-inline-start-color: var(--wily-success);
  background-color: var(--wily-success-soft);
}

.wily-toast--error {
  border-inline-start-color: var(--wily-danger);
  background-color: var(--wily-danger-soft);
}

.wily-toast--warning {
  border-inline-start-color: var(--wily-warning);
  background-color: var(--wily-warning-soft);
}

.wily-toast--info {
  border-inline-start-color: var(--wily-info);
  background-color: var(--wily-info-soft);
}

/* Forme CSS (pas de glyphe, pas de texte dans content:) : la couleur seule ne
   doit pas porter l'information — le niveau est aussi lu par le lecteur
   d'écran via le rôle live. */
.wily-toast__dot {
  flex: 0 0 auto;
  width: 0.6rem;
  height: 0.6rem;
  margin-top: 0.45rem;
  border-radius: 50%;
  background-color: var(--wily-info);
}

.wily-toast--success .wily-toast__dot {
  background-color: var(--wily-success);
}

.wily-toast--error .wily-toast__dot {
  background-color: var(--wily-danger);
}

.wily-toast--warning .wily-toast__dot {
  background-color: var(--wily-warning);
}

.wily-toast__message {
  flex: 1 1 auto;
  margin: 0;
  font-size: var(--wily-text-sm);
  color: var(--wily-text);
  overflow-wrap: anywhere;
}

.wily-toast__close {
  flex: 0 0 auto;
  width: 1.75rem;
  height: 1.75rem;
  padding: 0;
  border: 0;
  border-radius: var(--wily-radius-sm);
  background: none;
  cursor: pointer;
  position: relative;
}

.wily-toast__close:hover {
  background-color: var(--wily-surface-2);
}

/* Croix dessinée par deux barres : aucun caractère, donc aucune chaîne
   affichable à traduire ni à échapper. */
.wily-toast__close::before,
.wily-toast__close::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0.75rem;
  height: 2px;
  border-radius: 2px;
  background-color: var(--wily-text-muted);
}

.wily-toast__close::before {
  transform: translate(-50%, -50%) rotate(45deg);
}

.wily-toast__close::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

.wily-toast--leaving {
  animation: wily-toast-out 200ms ease-in forwards;
}

@keyframes wily-toast-in {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@keyframes wily-toast-out {
  from {
    opacity: 1;
    transform: translateY(0);
  }

  to {
    opacity: 0;
    transform: translateY(0.5rem);
  }
}

/* --------------------------------------------------------------------------
   13. Confirmation (.wily-confirm)
   --------------------------------------------------------------------------
   Le même balisage sert les deux modes : <dialog> natif quand showModal est
   disponible (piégeage du focus et inertie gérés par le navigateur), sinon
   une superposition maison avec role="dialog" aria-modal="true".
   -------------------------------------------------------------------------- */
.wily-confirm {
  border: 0;
  padding: 0;
  width: min(28rem, calc(100vw - 2 * var(--wily-space-4)));
  max-width: none;
  background: transparent;
  color: var(--wily-text);
  border-radius: var(--wily-radius-lg);
}

.wily-confirm::backdrop {
  background: var(--wily-overlay);
}

.wily-confirm__panel {
  padding: var(--wily-space-5);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  box-shadow: var(--wily-shadow-lg);
}

.wily-confirm__title {
  margin-bottom: var(--wily-space-2);
  font-size: var(--wily-text-xl);
}

.wily-confirm__message {
  margin-bottom: var(--wily-space-5);
  color: var(--wily-text-muted);
}

.wily-confirm__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--wily-space-2);
}

/* Sur un écran étroit, les boutons passent en pleine largeur, l'action
   principale en bas (la plus proche du pouce). */
@media (max-width: 30rem) {
  .wily-confirm__actions {
    flex-direction: column;
  }

  .wily-confirm__actions .btn {
    width: 100%;
  }
}

.wily-confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--wily-z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--wily-space-4);
  background: var(--wily-overlay);
  overflow-y: auto;
}

/* --------------------------------------------------------------------------
   14. État de chargement
   --------------------------------------------------------------------------
   Posé par le JavaScript pendant une action asynchrone (envoi de formulaire,
   confirmation) : on ne désactive jamais un formulaire sans le dire à l'écran.
   -------------------------------------------------------------------------- */
.is-loading {
  cursor: progress;
}

.is-loading .btn[type="submit"],
.is-loading button[type="submit"] {
  cursor: progress;
  opacity: 0.7;
}

.spinner {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  border: 2px solid var(--wily-border-strong);
  border-top-color: var(--wily-red);
  border-radius: 50%;
  animation: wily-spin 700ms linear infinite;
}

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

/* --------------------------------------------------------------------------
   15. Pied de page
   -------------------------------------------------------------------------- */
.site-footer {
  flex: 0 0 auto;
  margin-top: var(--wily-space-7);
  padding-block: var(--wily-space-5);
  background-color: var(--wily-surface);
  border-top: 1px solid var(--wily-border);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

.site-footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--wily-space-3);
}

.site-footer__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-footer__link {
  color: var(--wily-text-muted);
}

/* --------------------------------------------------------------------------
   16. Console éditeur : onglets de langue et compteurs (D34, D35)
   --------------------------------------------------------------------------
   Les onglets sont une amélioration progressive : sans JavaScript, tous les
   panneaux restent visibles et empilés. Aucune règle de style ne masque un
   contenu — c'est le script qui pose l'attribut `hidden`, dont le `display:
   none` natif reste prioritaire.
   -------------------------------------------------------------------------- */
.lang-tabs__list {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--wily-space-1);
  margin-bottom: var(--wily-space-4);
  padding: 0;
  overflow-x: auto;
  list-style: none;
  border-bottom: 1px solid var(--wily-border);
  -webkit-overflow-scrolling: touch;
}

.lang-tabs__tab {
  display: inline-flex;
  align-items: center;
  gap: var(--wily-space-2);
  padding: var(--wily-space-2) var(--wily-space-3);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  color: var(--wily-text-muted);
  font: inherit;
  font-size: var(--wily-text-sm);
  white-space: nowrap;
  cursor: pointer;
}

.lang-tabs__tab:hover {
  color: var(--wily-text);
}

.lang-tabs__tab[aria-selected='true'] {
  color: var(--wily-text);
  border-bottom-color: var(--wily-red);
  font-weight: 600;
}

.lang-tabs__tab:focus-visible {
  outline: 2px solid var(--wily-focus);
  outline-offset: 2px;
}

.lang-tabs__name {
  line-height: 1.2;
}

.lang-tabs__mark {
  padding: 0 var(--wily-space-2);
  background-color: var(--wily-surface-2);
  border-radius: var(--wily-radius-pill);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  font-weight: 400;
}

.lang-tabs__panel+.lang-tabs__panel {
  margin-top: var(--wily-space-6);
  padding-top: var(--wily-space-6);
  border-top: 1px dashed var(--wily-border);
}

.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--wily-space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.stat {
  padding: var(--wily-space-4);
  background-color: var(--wily-surface-2);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius);
}

.stat__value {
  display: block;
  color: var(--wily-text);
  font-size: var(--wily-text-2xl);
  font-weight: 700;
  line-height: 1.1;
}

.stat__label {
  display: block;
  margin-top: var(--wily-space-1);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

/* --------------------------------------------------------------------------
   16.1 Coquille de la console et parcours d'étapes (D36, D37)
   --------------------------------------------------------------------------
   La console n'est plus une vitrine : c'est un OUTIL. Sur grand écran, une
   barre latérale claire de 250 px et une zone de travail sur le gris de fond du
   produit (D40) ; sous 60rem, la barre redevient un tiroir que la bascule
   (rendue `hidden` par le serveur) ouvre.

   Rien de tout ceci ne dépend de JavaScript pour rester UTILISABLE :
     * la barre latérale est un bloc NORMAL, toujours visible, et le tiroir
       hors-écran n'est armé que sous `.is-nav-enhanced` — classe que le script
       pose lui-même. Sans script, la barre reste empilée au-dessus du contenu ;
     * les étapes (D37) sont TOUTES dans le document ; c'est le script qui pose
       `hidden` sur les inactives, et le rail est déjà à jour côté serveur.

   La barre était SOMBRE (D36) ; elle est BLANCHE depuis D40, séparée de la zone
   de travail par un filet, sur une maquette de poste de développeur. Aucune
   couleur nouvelle n'est introduite pour autant : blanc de surface, gris de
   bordure et rouge de marque viennent des jetons (invariant n°4). Les
   pictogrammes de navigation sont des FORMES CSS (content: vide + box-shadow) :
   jamais de texte dans `content:`, le libellé reste dans le DOM (§4.1.1).
   -------------------------------------------------------------------------- */

/* -- Coquille ------------------------------------------------------------- */
.console {
  min-height: 100vh;
}

.shell {
  display: flex;
  align-items: stretch;
  flex: 1 1 auto;
  min-height: 100vh;
}

.shell__main {
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--wily-space-5) var(--wily-space-4);
}

@media (min-width: 60rem) {
  .shell__main {
    padding: var(--wily-space-6) var(--wily-space-7);
  }
}

/* -- Barre latérale ------------------------------------------------------- */
.sidebar {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-4);
  /* La largeur de la barre est une CONSTANTE du produit (D40) : elle n'est pas
     recopiée dans chaque requête média. */
  flex: 0 0 var(--wily-sidebar-width);
  width: var(--wily-sidebar-width);
  padding: var(--wily-space-5) var(--wily-space-4);
  border-right: 1px solid var(--wily-border);
  background-color: var(--wily-surface);
  color: var(--wily-text);
}

.sidebar__brand {
  display: flex;
  align-items: center;
  gap: var(--wily-space-3);
  padding-inline: var(--wily-space-2);
}

.sidebar__logo {
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--wily-radius-sm);
  background-color: var(--wily-white);
  object-fit: contain;
}

.sidebar__brand-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.sidebar__brand-name {
  color: var(--wily-text);
  font-size: var(--wily-text-lg);
  font-weight: 700;
  letter-spacing: -0.02em;
}

.sidebar__brand-sub {
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
}

.sidebar__user {
  display: flex;
  align-items: center;
  gap: var(--wily-space-3);
  padding: var(--wily-space-3);
  background-color: var(--wily-surface-2);
  border-radius: var(--wily-radius);
}

.sidebar__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2.25rem;
  height: 2.25rem;
  background-color: var(--wily-red);
  border-radius: var(--wily-radius-pill);
  color: var(--wily-white);
  font-weight: 700;
  text-transform: uppercase;
}

.sidebar__user-text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.sidebar__user-name {
  overflow: hidden;
  color: var(--wily-text);
  font-size: var(--wily-text-sm);
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar__user-role {
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
}

/* Le courriel du compte, sous son nom d'affichage (D40) : une information en
   plus, jamais une répétition — la vue ne l'affiche que s'il diffère du nom. */
.sidebar__user-email {
  overflow: hidden;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar__nav {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-1);
}

.sidebar__section {
  margin: var(--wily-space-4) 0 var(--wily-space-1);
  padding-inline: var(--wily-space-3);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.sidebar__section:first-child {
  margin-top: 0;
}

.sidebar__link {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--wily-space-3);
  min-height: 2.75rem;
  padding: var(--wily-space-2) var(--wily-space-3);
  border-radius: var(--wily-radius);
  color: var(--wily-text);
  font-size: var(--wily-text-sm);
  font-weight: 600;
  text-decoration: none;
  transition: background-color var(--wily-transition), color var(--wily-transition);
}

.sidebar__link:hover {
  background-color: var(--wily-surface-2);
  color: var(--wily-text);
}

/* Entrée ACTIVE (D40) : fond très clair + BARREAU vertical rouge collé au bord,
   au lieu de l'aplat rouge de D36. Dans une barre claire, un aplat écraserait
   les libellés voisins ; et le repère ne repose pas sur la seule couleur — le
   poids de la police et `aria-current="page"` le portent aussi. */
.sidebar__link.is-active {
  background-color: var(--wily-red-soft);
  color: var(--wily-text);
  font-weight: 700;
}

.sidebar__link.is-active::before {
  content: '';
  position: absolute;
  top: 0.375rem;
  bottom: 0.375rem;
  left: 0;
  width: 0.1875rem;
  background-color: var(--wily-red);
  border-radius: var(--wily-radius-pill);
}

.sidebar__link:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
}

/* Entrée annoncée mais pas encore servie (D40) : elle a l'allure d'une entrée
   et aucun lien. Le curseur reste celui d'un texte, la couleur est en retrait —
   « bientôt » est écrit à droite, l'information ne dépend jamais de la couleur. */
.sidebar__link--soon {
  color: var(--wily-text-muted);
  cursor: default;
}

.sidebar__link--soon:hover {
  background-color: transparent;
}

.sidebar__soon {
  margin-inline-start: auto;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  font-weight: 600;
  white-space: nowrap;
}

.sidebar__label {
  min-width: 0;
}

/* Pictogrammes : des formes, jamais des glyphes. */
.sidebar__icon {
  position: relative;
  flex: 0 0 auto;
  width: 1.125rem;
  height: 1.125rem;
  color: currentColor;
}

.sidebar__icon::before,
.sidebar__icon::after {
  content: '';
  position: absolute;
  background-color: currentColor;
  border-radius: 2px;
}

.sidebar__icon[data-icon='grid']::before {
  top: 0;
  left: 0;
  width: 0.5rem;
  height: 0.5rem;
  box-shadow: 0.625rem 0 0 0 currentColor, 0 0.625rem 0 0 currentColor,
    0.625rem 0.625rem 0 0 currentColor;
}

.sidebar__icon[data-icon='stack']::before {
  top: 0.125rem;
  left: 0;
  width: 1.125rem;
  height: 0.1875rem;
  box-shadow: 0 0.375rem 0 0 currentColor, 0 0.75rem 0 0 currentColor;
}

.sidebar__icon[data-icon='plus']::before {
  top: 0.5rem;
  left: 0;
  width: 1.125rem;
  height: 0.1875rem;
}

.sidebar__icon[data-icon='plus']::after {
  top: 0;
  left: 0.5rem;
  width: 0.1875rem;
  height: 1.125rem;
}

/* Pictogrammes des entrées annoncées (D40) : cloche, immeuble, clé. Comme les
   autres, ce sont des FORMES — rien à traduire, rien à charger. */
.sidebar__icon[data-icon='bell']::before {
  top: 0.1875rem;
  left: 0.125rem;
  width: 0.875rem;
  height: 0.6875rem;
  border-radius: 0.375rem 0.375rem 0 0;
}

.sidebar__icon[data-icon='bell']::after {
  top: 0.9375rem;
  left: 0.4375rem;
  width: 0.25rem;
  height: 0.1875rem;
  border-radius: 0 0 2px 2px;
}

.sidebar__icon[data-icon='building']::before {
  top: 0;
  left: 0.25rem;
  width: 0.625rem;
  height: 1.125rem;
  border-radius: 2px;
}

/* La porte est « creusée » avec la couleur de la SURFACE de la barre, et non
   avec `transparent` : le pictogramme doit rester lisible sur les deux fonds. */
.sidebar__icon[data-icon='building']::after {
  top: 0.75rem;
  left: 0.5rem;
  width: 0.25rem;
  height: 0.375rem;
  background-color: var(--wily-surface);
}

.sidebar__icon[data-icon='key']::before {
  top: 0.28125rem;
  left: 0;
  width: 0.5625rem;
  height: 0.5625rem;
  border-radius: var(--wily-radius-pill);
}

.sidebar__icon[data-icon='key']::after {
  top: 0.5rem;
  left: 0.5rem;
  width: 0.625rem;
  height: 0.125rem;
  box-shadow: 0.375rem 0.25rem 0 0 currentColor;
}

/* Pictogramme de la vérification (Lot B) : une coche, en deux segments. Comme
   les autres, c'est une FORME — rien à traduire, rien à charger. Les deux
   segments partagent leur origine à gauche pour former l'angle. */
.sidebar__icon[data-icon='check']::before {
  top: 0.4375rem;
  left: 0.09375rem;
  width: 0.5rem;
  height: 0.1875rem;
  transform: rotate(45deg);
  transform-origin: left center;
}

.sidebar__icon[data-icon='check']::after {
  top: 0.78125rem;
  left: 0.40625rem;
  width: 0.75rem;
  height: 0.1875rem;
  transform: rotate(-45deg);
  transform-origin: left center;
}

/* Pictogrammes du Lot C. Le sélecteur de structure : deux blocs superposés,
   dont l'un est atténué — le dessin dit « plusieurs, une seule choisie ».
   L'équipe : deux bustes de même forme. Comme les autres, ce sont des FORMES
   — rien à traduire, rien à charger, et rien qui dépende d'une police. */
.sidebar__icon[data-icon='switch']::before {
  top: 0.0625rem;
  left: 0.0625rem;
  width: 1rem;
  height: 0.375rem;
  opacity: 0.45;
}

.sidebar__icon[data-icon='switch']::after {
  top: 0.625rem;
  left: 0.0625rem;
  width: 1rem;
  height: 0.375rem;
}

.sidebar__icon[data-icon='team']::before {
  top: 0;
  left: 0.25rem;
  width: 0.4375rem;
  height: 0.4375rem;
  border-radius: var(--wily-radius-pill);
}

.sidebar__icon[data-icon='team']::after {
  top: 0.5625rem;
  left: 0.0625rem;
  width: 0.5rem;
  height: 0.4375rem;
  border-radius: 0.375rem 0.375rem 0 0;
  box-shadow: 0.5rem 0 0 0 currentColor;
}

/* Pictogramme de la sécurité du compte (AUTH-3) : un bouclier, c'est-à-dire un
   corps qui se rétrécit vers le bas. Deux formes suffisent — un haut arrondi et
   une pointe — parce que les autres pictogrammes de la barre suivent la même
   règle : des FORMES, rien à traduire, rien à charger. */
.sidebar__icon[data-icon='shield']::before {
  top: 0.0625rem;
  left: 0.1875rem;
  width: 0.75rem;
  height: 0.4375rem;
  border-radius: 0.25rem 0.25rem 0 0;
}

.sidebar__icon[data-icon='shield']::after {
  top: 0.4375rem;
  left: 0.3125rem;
  width: 0.5rem;
  height: 0.5625rem;
  clip-path: polygon(0 0, 100% 0, 50% 100%);
}

.sidebar__footer {
  margin-top: auto;
  padding-top: var(--wily-space-4);
  border-top: 1px solid var(--wily-border);
}

/* Liens de service (D40) : ils mènent au site public, pas à la console. Ils sont
   volontairement discrets — ce ne sont pas des écrans de travail. */
.sidebar__services {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-1);
  margin-bottom: var(--wily-space-3);
}

.sidebar__service {
  overflow: hidden;
  padding: var(--wily-space-1) var(--wily-space-3);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar__service:hover {
  color: var(--wily-red);
  text-decoration: underline;
}

.sidebar__logout {
  margin: 0;
}

.sidebar__logout-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 2.75rem;
  padding: var(--wily-space-2) var(--wily-space-4);
  background-color: transparent;
  border: 1px solid var(--wily-border-strong);
  border-radius: var(--wily-radius);
  color: var(--wily-text);
  font: inherit;
  font-size: var(--wily-text-sm);
  font-weight: 600;
  cursor: pointer;
  transition: background-color var(--wily-transition),
    border-color var(--wily-transition), color var(--wily-transition);
}

.sidebar__logout-btn:hover {
  background-color: var(--wily-red);
  border-color: var(--wily-red);
  color: var(--wily-white);
}

/* -- Bascule et voile (petits écrans, JavaScript) -------------------------
   Le bouton est `hidden` dans le balisage et `[hidden]` (section 3) le masque
   avec `!important` : ces règles ne le font donc apparaître qu'après que le
   script a RETIRÉ l'attribut. Aucune bascule ne subsiste sans JavaScript. */
.shell__toggle {
  position: fixed;
  top: var(--wily-space-3);
  left: var(--wily-space-3);
  z-index: calc(var(--wily-z-header) + 3);
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  background-color: var(--wily-black);
  border: 1px solid var(--wily-on-dark-line);
  border-radius: var(--wily-radius);
  box-shadow: var(--wily-shadow);
  cursor: pointer;
}

.shell__toggle-mark,
.shell__toggle-mark::before,
.shell__toggle-mark::after {
  display: block;
  width: 1.25rem;
  height: 0.125rem;
  background-color: var(--wily-on-dark);
  border-radius: 2px;
}

.shell__toggle-mark {
  position: relative;
}

.shell__toggle-mark::before,
.shell__toggle-mark::after {
  content: '';
  position: absolute;
  left: 0;
}

.shell__toggle-mark::before {
  top: -0.375rem;
}

.shell__toggle-mark::after {
  top: 0.375rem;
}

.shell__scrim {
  position: fixed;
  inset: 0;
  z-index: calc(var(--wily-z-header) + 2);
  background-color: var(--wily-overlay);
}

/* -- En-tête minimal des écrans anonymes (connexion, second facteur) ------ */
.console-topbar {
  background-color: var(--wily-surface);
  border-bottom: 1px solid var(--wily-border);
}

.console-topbar__inner {
  display: flex;
  align-items: center;
  padding-block: var(--wily-space-4);
}

/* -- Petit écran : barre empilée ; tiroir seulement si le script est là ---- */
@media (max-width: 59.99rem) {
  .shell {
    flex-direction: column;
  }

  .sidebar {
    flex: 0 0 auto;
    width: 100%;
  }

  /* Le tiroir n'est armé QUE sous `.is-nav-enhanced` : sans JavaScript, la
     barre latérale reste un bloc normal, visible au-dessus du contenu. */
  .shell.is-nav-enhanced .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: calc(var(--wily-z-header) + 2);
    width: min(18rem, 84vw);
    overflow-y: auto;
    transform: translateX(-100%);
    transition: transform var(--wily-transition);
  }

  .shell.is-nav-enhanced.is-nav-open .sidebar {
    transform: translateX(0);
  }

  .shell.is-nav-enhanced .shell__main {
    padding-top: calc(var(--wily-space-8) + var(--wily-space-2));
  }
}

/* -- Parcours d'étapes (D37) ----------------------------------------------
   Le rail est rendu par le serveur : les numéros, l'état de chaque étape et
   le repère « étape courante » sont justes SANS JavaScript. Le script ne fait
   que replier les panneaux inactifs et gérer Suivant/Précédent. */
.steps {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-5);
}

.steps__rail {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.steps__item {
  flex: 1 1 8rem;
  min-width: 0;
}

.steps__step {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--wily-space-1);
  width: 100%;
  height: 100%;
  padding: var(--wily-space-3);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-top: 3px solid var(--wily-border-strong);
  border-radius: var(--wily-radius);
  color: var(--wily-text-muted);
  font: inherit;
  text-align: start;
  cursor: pointer;
  transition: border-color var(--wily-transition), color var(--wily-transition),
    background-color var(--wily-transition);
}

.steps__step:hover {
  border-top-color: var(--wily-red);
  color: var(--wily-text);
}

.steps__step:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
}

.steps__step[data-wily-step-state='current'] {
  border-top-color: var(--wily-red);
  background-color: var(--wily-red-soft);
  color: var(--wily-text);
}

.steps__step[data-wily-step-state='done'] {
  border-top-color: var(--wily-success);
  color: var(--wily-text);
}

.steps__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  background-color: var(--wily-surface-2);
  border-radius: var(--wily-radius-pill);
  color: var(--wily-text);
  font-size: var(--wily-text-sm);
  font-weight: 700;
}

.steps__step[data-wily-step-state='current'] .steps__num {
  background-color: var(--wily-red);
  color: var(--wily-white);
}

.steps__step[data-wily-step-state='done'] .steps__num {
  background-color: var(--wily-success);
  color: var(--wily-white);
}

.steps__label {
  font-size: var(--wily-text-sm);
  font-weight: 600;
}

/* Un seul état de mot affiché à la fois, en correspondance exacte de l'état de
   l'étape : le rail reste vrai sans script, et aucun texte ne passe par
   `content:` (§4.1.1). */
.steps__state {
  display: none;
}

.steps__step[data-wily-step-state='todo'] .steps__state--todo,
.steps__step[data-wily-step-state='current'] .steps__state--current,
.steps__step[data-wily-step-state='done'] .steps__state--done {
  display: block;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
}

.steps__hint {
  margin: 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

.steps__progress {
  margin: 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  font-weight: 600;
}

.steps__panel {
  display: block;
}

.steps__nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-3);
  justify-content: space-between;
}

/* Récapitulatif de l'étape 4 : une liste de définitions, pas un tableau —
   chaque paire libellé/valeur se replie proprement sur petit écran. */
.summary {
  margin: 0;
  border-top: 1px solid var(--wily-border);
}

.summary__row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-2) var(--wily-space-4);
  padding-block: var(--wily-space-3);
  border-bottom: 1px solid var(--wily-border);
}

.summary__term {
  flex: 0 0 12rem;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  font-weight: 600;
}

.summary__value {
  flex: 1 1 12rem;
  min-width: 0;
  margin: 0;
  color: var(--wily-text);
}

/* --------------------------------------------------------------------------
   16.2 Dépôt des médias de fiche — icône et captures (D38)
   --------------------------------------------------------------------------
   Le widget est une AMÉLIORATION : sans JavaScript il se réduit à un bloc
   inerte, et l'enregistrement passe par les champs de transport repliés
   (`.upload__manual`) — aucune règle d'ici ne masque un contenu nécessaire.

   `.progress` et `.status` ne sont pas propres à cet écran : la page des
   versions (L2.6) les emploie depuis le début sans qu'ils fussent habillés.
   Les deux règles génériques sont donc posées une seule fois, ici.
   -------------------------------------------------------------------------- */

/* -- Barre de progression et ligne d'état -------------------------------- */
.progress {
  display: block;
  width: 100%;
  height: 0.5rem;
  margin: var(--wily-space-3) 0 0;
  border: 0;
  border-radius: var(--wily-radius-pill);
  background-color: var(--wily-surface-3);
  overflow: hidden;
  appearance: none;
  -webkit-appearance: none;
}

.progress::-webkit-progress-bar {
  background-color: var(--wily-surface-3);
}

.progress::-webkit-progress-value {
  background-color: var(--wily-red);
}

.progress::-moz-progress-bar {
  background-color: var(--wily-red);
}

.status {
  min-height: 1.25rem;
  margin: var(--wily-space-2) 0 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

/* L'état est porté par l'attribut, jamais écrit dans `el.style` (ST-11). */
.upload[data-upload-state='error'] .status {
  color: var(--wily-danger);
  font-weight: 600;
}

.upload[data-upload-state='done'] .status {
  color: var(--wily-success);
  font-weight: 600;
}

/* -- Zone de dépôt ------------------------------------------------------- */
.upload {
  margin-block: var(--wily-space-5);
  padding: var(--wily-space-4);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius);
  background-color: var(--wily-surface);
}

/* Le contrôle natif reste affiché : c'est lui qui ouvre le sélecteur du
   système. On ne le remplace pas par un bouton qui le simulerait — un
   `<label for>` suffit à faire du cadre entier une cible. */
.upload__drop {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-3);
  padding: var(--wily-space-4);
  border: 2px dashed var(--wily-border-strong);
  border-radius: var(--wily-radius);
  background-color: var(--wily-surface-2);
  cursor: pointer;
  transition: border-color var(--wily-transition), background-color var(--wily-transition);
}

.upload__drop:hover,
.upload__drop:focus-within {
  border-color: var(--wily-red);
  background-color: var(--wily-red-soft);
}

.upload__title {
  font-weight: 600;
}

.upload__input {
  flex: 1 1 14rem;
  min-width: 0;
  max-width: 100%;
  font: inherit;
  color: inherit;
}

.upload__input:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

.upload__input:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
}

/* -- Aperçu de l'icône --------------------------------------------------- */
.upload__preview {
  display: flex;
  align-items: center;
  gap: var(--wily-space-3);
  margin: var(--wily-space-3) 0 0;
}

.upload__image {
  flex: 0 0 auto;
  width: 5rem;
  height: 5rem;
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-sm);
  background-color: var(--wily-surface-2);
  object-fit: contain;
}

.upload__caption {
  min-width: 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  overflow-wrap: anywhere;
}

/* -- Captures déjà envoyées ---------------------------------------------- */
.upload__list {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-2);
  margin: var(--wily-space-3) 0 0;
  padding: 0;
  list-style: none;
}

.upload__item {
  display: flex;
  align-items: center;
  gap: var(--wily-space-3);
  padding: var(--wily-space-2) var(--wily-space-3);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-sm);
  background-color: var(--wily-surface-2);
}

.upload__thumb {
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--wily-radius-sm);
  background-color: var(--wily-surface-3);
  object-fit: cover;
}

.upload__item-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--wily-text-sm);
  overflow-wrap: anywhere;
}

.upload__remove {
  flex: 0 0 auto;
  font-size: var(--wily-text-sm);
}

/* -- Affiche de couverture (PWA / SaaS) ---------------------------------- */
/* Le bloc est le MÊME que pour un APK ; seule la forme de la vignette change.
   Une affiche n'est pas un logo : elle se cadre en 3:4, exactement comme la
   carte du catalogue, pour que l'éditeur voie ce qu'il publie (F-48). */
.media-block--cover .upload__thumb {
  width: 4.5rem;
  height: 6rem;
  border-radius: var(--wily-radius-md);
}

.media-block--cover .upload__item {
  align-items: flex-start;
}

/* Aperçu de l'affiche ENREGISTRÉE : le même cadre 3:4 que les vignettes de
   dépôt, pour que l'éditeur voie exactement ce que le catalogue affichera. */
.media-block__saved {
  margin: 0 0 var(--wily-space-3);
}

.media-block__saved-image {
  display: block;
  width: 4.5rem;
  height: 6rem;
  border-radius: var(--wily-radius-md);
  background-color: var(--wily-surface-3);
  object-fit: cover;
}

/* -- Repli sans JavaScript -----------------------------------------------
   `<details>`/`<summary>` sont natifs : le bloc s'ouvre et se ferme seul, et
   il reste la seule voie d'enregistrement quand le script n'est pas là. */
.upload__manual {
  margin-block: var(--wily-space-5);
  padding: var(--wily-space-3) var(--wily-space-4);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius);
  background-color: var(--wily-surface-2);
}

.upload__summary {
  cursor: pointer;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  font-weight: 600;
}

.upload__summary:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
}

.upload__manual .field {
  margin-top: var(--wily-space-4);
}

/* --------------------------------------------------------------------------
   17. Vitrine du store : bandeau et rangées (D39)
   --------------------------------------------------------------------------
   L'accueil est une VITRINE : un bandeau en tête, les campagnes de
   staff/bannieres dans un carrousel « TV » en demi-largeur, puis des rangées
   d'applications qui défilent latéralement. Rien de tout ceci ne dépend du
   JavaScript — chaque bande est une grille défilante avec arrêt sur carte, pas
   un carrousel piloté par un minuteur.

   Les catégories n'ont plus de bande ici : elles vivent dans le tiroir de
   gauche de l'en-tête (section 9), qui les rend sur tous les écrans étroits et
   laisse le premier écran au contenu éditorial.

   Contraintes tenues ici :
     * aucune image de remplissage : la seule illustration est la vignette de la
       fiche (icône publiée, ou monogramme) ;
     * `style-src 'self'` : aucune valeur de style n'est écrite dans un gabarit,
       tout passe par les classes ci-dessous ;
     * aucune couleur d'identité nouvelle : les teintes de vignette réutilisent
       les paires sémantiques de la charte (fond `-soft`, texte sémantique), qui
       basculent ensemble en thème sombre — le contraste suit donc le thème ;
     * aucun texte dans `content:` : la loupe du bandeau et le chevron d'un lien
       de section sont des FORMES.
   -------------------------------------------------------------------------- */

/* -- Bandeau « à la une » --------------------------------------------------
   Dégradé dérivé du rouge de marque (--wily-red-soft) et non une image : le
   bandeau reste léger, lisible en plein soleil et identique dans les 5 langues. */
.store-hero {
  padding-block: var(--wily-space-5);
  border-bottom: 1px solid var(--wily-border);
  background-image: linear-gradient(135deg, var(--wily-red-soft) 0%, var(--wily-surface) 58%);
}

.store-hero__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-4);
}

.store-hero__text {
  flex: 1 1 22rem;
  min-width: 0;
}

.store-hero__label {
  margin: 0 0 var(--wily-space-2);
}

/* Le titre du bandeau est un `<p>` depuis D49 : le `<h1>` de la page vit dans
   `home/index.php`, où il ne dépend ni des données ni de la largeur d'écran.
   Ce paragraphe reprend donc EXPLICITEMENT l'échelle d'un titre de niveau 1
   (taille, graisse, interligne, approche) : le rendu du bandeau au bureau est
   identique au pixel, sans dépendre de la balise employée. */
.store-hero__title {
  max-width: 34rem;
  margin: 0 0 var(--wily-space-2);
  font-size: var(--wily-text-3xl);
  font-weight: 700;
  line-height: var(--wily-leading-tight);
  letter-spacing: -0.01em;
}

.store-hero__tagline {
  max-width: 34rem;
  margin: 0 0 var(--wily-space-3);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-base);
}

.store-hero__art {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Repli sans fiche publiée : la marque du store, encadrée comme une vignette. */
.store-hero__art--mark {
  padding: var(--wily-space-4);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  background-color: var(--wily-surface);
}

.store-hero__mark {
  width: 6rem;
  height: 6rem;
  border-radius: var(--wily-radius);
}

/* Pastille accentuée : le badge de type reste neutre, celui-ci porte l'ACTION
   (retour vers la fiche à la une) — donc le rouge de marque, en fond plein. */
.badge--accent {
  background-color: var(--wily-red);
  color: var(--wily-white);
  text-decoration: none;
}

.badge--accent:hover {
  background-color: var(--wily-red-dark);
  color: var(--wily-white);
}

/* -- Carrousel « TV » des campagnes, pleine largeur sur téléphone ---------
   Les campagnes de l'emplacement « home » (staff/bannieres) occupent leur
   propre bande. Sous 48rem, le bandeau « à la une » est retiré (D49) : le
   carrousel devient le premier visuel de la page et prend TOUTE la largeur
   utile, une campagne à la fois et cadrée au centre — comme le slider de
   `marketplace.php` de WILY. À partir de 48rem, le bandeau reprend sa place et
   une campagne fait la MOITIÉ de la largeur : deux visuels se lisent côte à
   côte, la bande redevient une introduction.

   Comme les rangées, c'est une grille défilante : aucun script, aucun
   minuteur, aucun bouton de pagination. Le conteneur est atteignable au clavier
   (`tabindex="0"` posé par le gabarit), sinon une zone défilante serait
   inutilisable sans souris.

   Le visuel a un ratio fixe (`aspect-ratio: 16 / 9`) : la bande ne saute pas
   pendant le chargement de l'image. Une campagne sans visuel n'arrive jamais
   ici — le service l'écarte en amont. */
.section--tv {
  padding-block: var(--wily-space-5);
  border-bottom: 1px solid var(--wily-border);
}

.store-tv {
  display: grid;
  grid-auto-flow: column;
  /* Une campagne par écran : la bande est le visuel de tête sur téléphone. */
  grid-auto-columns: 100%;
  gap: var(--wily-space-3);
  margin: 0;
  /* La bande de défilement reste fine et ne recouvre pas les visuels. */
  padding: 0 0 var(--wily-space-3);
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
}

.store-tv__item {
  min-width: 0;
  /* Cadrée au centre : après un défilement partiel, la campagne revient
     toujours d'aplomb, jamais coupée en deux. */
  scroll-snap-align: center;
  scroll-snap-stop: always;
}

.store-tv__card {
  display: block;
  height: 100%;
  overflow: hidden;
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  background-color: var(--wily-surface);
  color: var(--wily-text);
  text-decoration: none;
  transition: border-color var(--wily-transition), box-shadow var(--wily-transition), transform var(--wily-transition);
}

/* Seules les campagnes qui MÈNENT quelque part sont des liens : la campagne
   sans destination est une figure (un `div`), elle n'a donc ni survol ni
   curseur de lien. */
a.store-tv__card:hover {
  border-color: var(--wily-border-strong);
  box-shadow: 0 4px 14px rgba(15, 43, 92, 0.08);
  transform: translateY(-2px);
}

a.store-tv__card:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
}

.store-tv__visual {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.store-tv__caption {
  display: block;
  padding: var(--wily-space-2) var(--wily-space-3);
  font-size: var(--wily-text-sm);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (min-width: 48rem) {

  /* Demi-largeur : deux cartes plus l'écart qui les sépare remplissent
     exactement la largeur utile. L'alignement repasse au début, sinon chaque
     vignette se centrerait dans le rail et la première ne toucherait plus le
     bord gauche. */
  .store-tv {
    grid-auto-columns: calc(50% - var(--wily-space-3) / 2);
  }

  .store-tv__item {
    scroll-snap-align: start;
  }
}

/* -- Section de rangée (accueil) ------------------------------------------
   Une rangée de l'accueil n'est plus un rail qui défile latéralement, mais une
   GRILLE de cartes en ligne (`.app-list`, section 17.1). Le défilement
   horizontal cachait la moitié du catalogue : sur téléphone on ne voyait que
   deux vignettes et demie, et rien n'indiquait qu'il en restait d'autres. Le
   rythme vertical de la section est conservé tel quel. */
.section--row {
  padding-block: var(--wily-space-4) var(--wily-space-5);
}

/* -- Vignette d'application (icône publiée, ou monogramme) ----------------- */
.app-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  /* Angles fortement arrondis : c'est la signature visuelle d'une vignette
     d'application, et cela la distingue de la carte qui la porte.

     Le rayon est PROPORTIONNEL (28 %), pas une valeur en pixels. Avec un rayon
     fixe de 16 px, la même forme donnait trois silhouettes différentes :
     `sm` (40 px) presque ronde, `md` (56 px) correcte, `lg` (88 px, bandeau)
     à peine arrondie. Une proportion garde la même famille de forme d'un écran
     à l'autre — le téléphone et le bureau ne voient pas deux styles d'icône. */
  border-radius: 28%;
  background-color: var(--wily-surface-2);
  color: var(--wily-text);
  font-weight: 700;
  overflow: hidden;
  box-shadow: 0 2px 6px rgba(15, 43, 92, 0.08);
  border: 1px solid rgba(15, 43, 92, 0.06);
}

.app-icon--sm {
  width: 2.5rem;
  height: 2.5rem;
  font-size: var(--wily-text-sm);
}

.app-icon--md {
  width: 3.5rem;
  height: 3.5rem;
  font-size: var(--wily-text-lg);
}

.app-icon--lg {
  width: 5.5rem;
  height: 5.5rem;
  font-size: var(--wily-text-2xl);
}

/* Le monogramme est écrit SOUS l'image : si l'objet storage a disparu, le
   navigateur échoue à charger l'image et le repère reste lisible (D39). */
.app-icon__monogram {
  line-height: 1;
  letter-spacing: 0.02em;
}

.app-icon__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Six teintes, toutes issues des paires sémantiques de la charte (fond `-soft`,
   texte sémantique) : aucune couleur d'identité n'est inventée, et les deux
   valeurs basculent ENSEMBLE en thème sombre — le contraste est conservé. */
.app-icon--tint-0 {
  background-color: var(--wily-red-soft);
  color: var(--wily-danger);
}

.app-icon--tint-1 {
  background-color: var(--wily-info-soft);
  color: var(--wily-info);
}

.app-icon--tint-2 {
  background-color: var(--wily-success-soft);
  color: var(--wily-success);
}

.app-icon--tint-3 {
  background-color: var(--wily-warning-soft);
  color: var(--wily-warning);
}

.app-icon--tint-4 {
  background-color: var(--wily-surface-3);
  color: var(--wily-text);
}

.app-icon--tint-5 {
  background-color: var(--wily-surface-2);
  color: var(--wily-text-muted);
}

/* Repli sans image (W2) : carre plein aux couleurs de la marque et lettre
   blanche. Pose seulement quand l'application n'a AUCUNE icone publiee, donc
   jamais par-dessus une image transparente. Regle volontairement apres les
   teintes pour l'emporter sans `!important`. */
.app-icon--fallback {
  background-color: var(--wily-red);
  color: var(--wily-white);
  box-shadow: var(--wily-shadow-sm);
}

.app-icon--fallback .app-icon__monogram {
  font-size: 1.1em;
  font-weight: 800;
}

/* -- Lien « tout voir » d'un en-tête de section ---------------------------
   Le chevron n'est pas un caractère : deux bordures tournées. Un « > » écrit en
   CSS serait du texte affiché hors dictionnaire (§4.1.1). */
.section__more {
  display: inline-flex;
  align-items: center;
  gap: var(--wily-space-1);
  min-height: 2.5rem;
  font-size: var(--wily-text-sm);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

.section__more::after {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-top: 2px solid currentColor;
  border-right: 2px solid currentColor;
  transform: rotate(45deg);
}

.section__more:hover {
  color: var(--wily-red-dark);
}

/* -- Aucune variante de `.app-card` ici -----------------------------------
   Les deux formats du catalogue ne sont plus des variantes de la carte
   générique : ce sont deux composants distincts, `.app-line` (format liste de
   l'accueil) et `.app-grid-card` (format grille d'une catégorie), décrits en
   section 17.1. `.app-card` ne survit que pour la vignette de catégorie
   (`.app-card--category`), qui n'est pas une application.

   Les anciennes variantes `--tile` et `--wide` ont été supprimées avec le
   rail défilant : une règle orpheline fait croire à un composant encore
   disponible. */

/* -- Loupe du formulaire de recherche ------------------------------------
   La même forme que `.icon--search` du bouton, mais statique et discrète :
   elle rappelle qu'on est dans un champ de recherche, même quand la valeur
   est vide. */
.site-search__icon {
  position: relative;
  flex: 0 0 auto;
  width: 1.05rem;
  height: 1.05rem;
  border: 2px solid var(--wily-text-muted);
  border-radius: 50%;
}

.site-search__icon::after {
  content: "";
  position: absolute;
  right: -0.3rem;
  bottom: -0.25rem;
  width: 0.5rem;
  height: 2px;
  border-radius: 2px;
  background-color: var(--wily-text-muted);
  transform: rotate(45deg);
}

/* -- Dépliant de recherche : un panneau sous la barre, sans voile ----------
   La loupe n'ouvre pas un tiroir plein écran : le champ apparaît juste sous la
   barre et la page continue de défiler. Le panneau se cale sur `.site-header`
   (`sticky`, donc positionné) et non sur le `<details>` : `left`/`right` à 0 le
   font épouser toute la largeur de l'en-tête, du téléphone au grand écran.
   Les valeurs de tiroir plein écran (position fixe, 20rem de large) sont donc
   explicitement annulées ici. */
.header-drawer--search .header-drawer__panel {
  position: absolute;
  top: 100%;
  right: 0;
  bottom: auto;
  left: 0;
  z-index: 2;
  flex-direction: row;
  justify-content: center;
  width: auto;
  padding: var(--wily-space-3) var(--wily-space-4);
  background-color: var(--wily-surface);
  border-bottom: 1px solid var(--wily-border);
  box-shadow: var(--wily-shadow-lg);
}

/* Le champ garde une largeur de lecture confortable au bureau, sans jamais
   dépasser l'écran : le reste du panneau est du vide. `nowrap` garde le
   bouton sur la même ligne que le champ, même sur 320 px. */
.header-drawer--search .site-search {
  flex: 1 1 auto;
  flex-wrap: nowrap;
  max-width: 44rem;
}

.site-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-2);
}

.site-actions form {
  margin: 0;
}

@media (max-width: 47.99rem) {

  /* Le bandeau « à la une » ne tient pas sur un écran étroit : grande surface,
     texte puis visuel empilés, il repousse le catalogue de deux écrans. Il est
     donc retiré sous 48rem (D49) et la page commence par le carrousel « TV »,
     pleine largeur. La page ne perd pas son titre pour autant : le `<h1>` du
     store est rendu par `home/index.php`, hors de ce bloc. */
  .store-hero {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   17.1 Catalogue : les deux formats d'affichage
   --------------------------------------------------------------------------
   L'accueil et la page de catégorie ne racontent pas la même chose, donc ils
   n'ont pas la même forme de carte.

   ACCUEIL — format LISTE (`.app-list` / `.app-line`). On compare des
   applications, on ne les contemple pas : vignette à gauche sur 64 px, nom sur
   UNE ligne, description sur DEUX, note et étiquettes dessous. La grille passe
   d'une colonne pleine largeur (téléphone) à deux ou trois colonnes dès 48rem,
   sans palier à maintenir : `auto-fit` + `minmax(18.75rem, 1fr)` calcule seul
   le nombre de colonnes possibles.

   CATÉGORIE — format GRILLE (`.app-grid` / `.app-grid-card`). On parcourt un
   rayon : la vignette domine (85 px), le nom est centré dessous sur une ligne,
   la note rouge en dernier. Trois colonnes sur téléphone, quatre à partir de
   48rem, six à partir de 60rem.

   RÈGLE DE DÉBORDEMENT (charte d'affichage)
   -----------------------------------------
   Aucun conteneur de texte ne reçoit de hauteur fixe. Le nom est tronqué sur
   UNE ligne (`nowrap` + `text-overflow: ellipsis`), la description sur DEUX
   (`-webkit-line-clamp`), et la carte grandit librement : deux cartes voisines
   ne peuvent pas se chevaucher, quelle que soit la longueur des traductions.
   Le `min-width: 0` posé sur les cellules et sur les colonnes de texte est
   indispensable : sans lui, un enfant de grille refuse de rétrécir sous la
   largeur de son contenu et l'ellipse ne se déclenche jamais.

   L'étoile de note est une FORME (`.app-star`, `clip-path`), jamais un
   caractère : une étoile écrite en CSS serait du texte affiché hors
   dictionnaire (§4.1.1). Elle prend la couleur du texte qui la porte
   (`currentColor`), donc elle suit le rouge de la charte — et bascule avec le
   thème sombre.
   -------------------------------------------------------------------------- */

/* -- Format LISTE : carte en ligne (accueil) ------------------------------ */
.app-list {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--wily-space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.app-list__item {
  /* Sans cela, une carte dont le titre est long élargit sa colonne et déborde
     de la grille. */
  min-width: 0;
}

@media (min-width: 48rem) {

  .app-list {
    grid-template-columns: repeat(auto-fit, minmax(18.75rem, 1fr));
    gap: var(--wily-space-4);
  }
}

.app-line {
  display: flex;
  align-items: center;
  gap: var(--wily-space-3);
  /* Les cartes d'une même rangée s'alignent en bas même si l'une n'a pas de
     note : la hauteur vient du contenu, jamais d'une valeur imposée. */
  height: 100%;
  padding: var(--wily-space-3) var(--wily-space-4);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  box-shadow: 0 2px 8px rgba(15, 43, 92, 0.05);
  color: inherit;
  text-decoration: none;
  transition: transform var(--wily-transition), box-shadow var(--wily-transition), border-color var(--wily-transition);
}

.app-line:hover {
  transform: translateY(-2px);
  border-color: var(--wily-border-strong);
  box-shadow: 0 6px 18px rgba(15, 43, 92, 0.09);
  color: inherit;
}

/* Anneau de focus bleu marine : la charte a une couleur d'accent nette. */
.app-line:focus-visible,
.app-grid-card:focus-visible {
  outline: 2px solid var(--wily-red);
  outline-offset: 2px;
}

.app-line__body {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-1);
  flex: 1 1 auto;
  min-width: 0;
}

.app-line__name {
  margin: 0;
  font-size: var(--wily-text-base);
  font-weight: 700;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-line__desc {
  display: -webkit-box;
  overflow: hidden;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  line-height: 1.4;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.app-line__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-1) var(--wily-space-2);
  min-width: 0;
  margin-top: var(--wily-space-1);
}

.app-line__rating {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  color: #d97706;
  font-size: var(--wily-text-sm);
  font-weight: 700;
}

/* Chevron de la carte : deux bordures tournées, pas un « > » écrit en CSS
   (texte hors dictionnaire, §4.1.1). Il indique que la carte mène à la fiche. */
.app-line__arrow {
  flex: 0 0 auto;
  width: 0.45rem;
  height: 0.45rem;
  border-top: 2px solid var(--wily-red);
  border-right: 2px solid var(--wily-red);
  transform: rotate(45deg);
  transition: transform var(--wily-transition);
}

.app-line:hover .app-line__arrow {
  transform: translateX(2px) rotate(45deg);
}

/* -- Étoile de note (forme CSS, aucune police d'icônes) ------------------- */
.app-star {
  flex: 0 0 auto;
  width: 0.875rem;
  height: 0.875rem;
  background-color: #d97706;
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%,
      32% 57%, 2% 35%, 39% 35%);
}

/* -- Format GRILLE : carte verticale (page de catégorie) ------------------ */
.app-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--wily-space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.app-grid__item {
  min-width: 0;
}

@media (min-width: 48rem) {

  .app-grid {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--wily-space-4);
  }
}

@media (min-width: 60rem) {

  .app-grid {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
}

.app-grid-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--wily-space-2);
  height: 100%;
  padding: var(--wily-space-3) var(--wily-space-2);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  box-shadow: 0 2px 6px rgba(15, 43, 92, 0.04);
  color: inherit;
  text-align: center;
  text-decoration: none;
  transition: transform var(--wily-transition), box-shadow var(--wily-transition), border-color var(--wily-transition);
}

.app-grid-card:hover {
  transform: translateY(-2px);
  border-color: var(--wily-border-strong);
  box-shadow: 0 6px 14px rgba(15, 43, 92, 0.08);
  color: inherit;
}

.app-grid-card__name {
  width: 100%;
  margin: 0;
  overflow: hidden;
  font-size: var(--wily-text-sm);
  font-weight: 700;
  line-height: 1.3;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* `margin-top: auto` colle la note au bas de la carte : les étoiles d'une même
   rangée sont alignées, qu'il y ait une note ou non. */
.app-grid-card__rating {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  margin-top: auto;
  color: #d97706;
  font-size: var(--wily-text-xs);
  font-weight: 600;
}

/* -- Carte AFFICHE (livraisons web : PWA / SaaS) --------------------------- */
/* Une carte de service se lit comme une couverture : l'image occupe la largeur,
   le texte passe dessous, aligné à gauche. La carte cesse d'être centrée et
   n'ajoute aucun ornement — un seul visuel, un titre, une note.
   `aspect-ratio` fixe le cadre : sans lui, deux affiches voisines décalent la
   rangée. `object-fit: cover` recadre au lieu d'écraser. */
.app-grid-card--cover {
  align-items: stretch;
  overflow: hidden;
  padding: 0;
  text-align: left;
}

.app-grid-card__cover {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
}

.app-grid-card--cover .app-grid-card__name {
  display: -webkit-box;
  overflow: hidden;
  margin: var(--wily-space-2) 0 0;
  padding: 0 var(--wily-space-3);
  white-space: normal;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}

.app-grid-card--cover .app-grid-card__rating {
  margin: 0;
  padding: var(--wily-space-1) var(--wily-space-3) var(--wily-space-3);
}

/* -- Tailles de vignette propres au catalogue ----------------------------- */
/* 64 px de côté : la carte en ligne (accueil, recherche). 85 px : la carte en
   grille d'une catégorie. Ces deux côtés sont écrits en dur PARCE QUE c'est la
   charte d'affichage d'un catalogue — une taille de composant, pas un palier
   responsive.

   Le RAYON, lui, n'est pas réécrit : il reste la proportion de 28 % posée par
   `.app-icon`. Sur 64 px elle vaut 17,9 px et sur 85 px 23,8 px — les 16 px et
   20 px de la charte à quelques pixels près. Une valeur en pixels ici aurait
   ramené les silhouettes divergentes que D39 a supprimées (une petite icône
   presque ronde, une grande à peine arrondie). */
.app-icon--row {
  width: 4rem;
  height: 4rem;
  font-size: var(--wily-text-lg);
}

.app-icon--grid {
  width: 5.3125rem;
  height: 5.3125rem;
  font-size: var(--wily-text-2xl);
}

/* --------------------------------------------------------------------------
   18. Préférences du système et impression
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* Le défilement de la page est bloqué tant qu'un tiroir d'en-tête est ouvert :
   sinon le contenu glisse derrière le panneau au moindre geste. La classe est
   posée par wily-ui.js ; sans script, elle n'existe pas et rien n'est bloqué. */
html.is-drawer-open {
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   18.1 En-tête compact : tiroirs et dépliant (D39)
   --------------------------------------------------------------------------
   Une seule barre, à TOUTES les tailles d'écran :

       [☰]                WILYSTORE                [loupe] [compte]

   POURQUOI PAS DEUX MISES EN PAGE
   -------------------------------
   Le bureau n'a pas une version « riche » et le téléphone une version
   « pauvre » : deux rendus, ce sont deux navigations à maintenir et deux
   occasions de divergence. La barre complète du bureau (liens à plat, champ
   de recherche, sélecteur de langue, boutons de compte) disparaît donc
   entièrement au profit des trois mêmes commandes partout. Le contenu ne
   change pas : il est simplement rangé, une fois pour toutes, derrière les
   boutons qui le nomment.

   La recherche est la SEULE chose gardée à portée d'un clic : sa loupe
   ouvre un dépliant juste sous la barre, sans voile, sans quitter la page.
   Le champ n'existe qu'une fois dans le document — un seul `name="q"`, un
   seul `id` — donc aucun doublon à l'écran ni au clavier.

   L'ordre des commandes, le `nowrap` et la marque sont décrits dans la
   section 9 ; les pictogrammes et les DEUX TIROIRS sont ici, une seule fois.
   Le dépliant de recherche, lui, est décrit avec son formulaire (section 17) :
   c'est un panneau ordinaire sous la barre, pas un tiroir plein écran. */

/* -- Les trois commandes : pictogrammes en FORMES CSS --------------------
   Aucun glyphe n'est écrit dans `content:` (§4.1.1) : rien à traduire, rien
   à charger, et la CSP `font-src`/`img-src 'self'` reste intacte. Chaque
   forme est dessinée avec `before`/`after`, donc aucun élément décoratif
   n'est ajouté au document. */
.icon {
  position: relative;
  display: inline-block;
  flex: 0 0 auto;
  width: 1.25rem;
  height: 1.25rem;
}

/* Menu : trois barres, dont deux décalées par une ombre (aucun élément). */
.icon--menu::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 0.05rem;
  right: 0.05rem;
  height: 2px;
  margin-top: -1px;
  border-radius: 2px;
  background-color: currentColor;
  box-shadow: 0 -0.375rem 0 currentColor, 0 0.375rem 0 currentColor;
}

/* Compte : une tête et des épaules — le buste est un arc ouvert. */
.icon--user::before {
  content: '';
  position: absolute;
  top: 0.05rem;
  left: 50%;
  width: 0.5rem;
  height: 0.5rem;
  margin-left: -0.25rem;
  border: 2px solid currentColor;
  border-radius: 50%;
}

.icon--user::after {
  content: '';
  position: absolute;
  bottom: 0.1rem;
  left: 50%;
  width: 1rem;
  height: 0.5rem;
  margin-left: -0.5rem;
  border: 2px solid currentColor;
  border-bottom: 0;
  border-radius: 0.5rem 0.5rem 0 0;
}

/* Recherche : un anneau et un manche inclinés — la loupe universelle, donc
   aucune image à charger ni à traduire. */
.icon--search::before {
  content: '';
  position: absolute;
  top: 0.05rem;
  left: 0.05rem;
  width: 0.7rem;
  height: 0.7rem;
  border: 2px solid currentColor;
  border-radius: 50%;
}

.icon--search::after {
  content: '';
  position: absolute;
  right: 0.05rem;
  bottom: 0.1rem;
  width: 0.45rem;
  height: 2px;
  border-radius: 2px;
  background-color: currentColor;
  transform: rotate(45deg);
  transform-origin: right center;
}

/* Fermer : deux barres croisées. */
.icon--close::before,
.icon--close::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 0.1rem;
  right: 0.1rem;
  height: 2px;
  margin-top: -1px;
  border-radius: 2px;
  background-color: currentColor;
}

.icon--close::before {
  transform: rotate(45deg);
}

.icon--close::after {
  transform: rotate(-45deg);
}

/* -- Le `<summary>` est le bouton ---------------------------------------
   Cible de 44 px, sans bordure ni marqueur : c'est le pictogramme seul qui
   parle. Le `<details>` fournit l'état ouvert/fermé au navigateur, donc le
   clavier et les lecteurs d'écran fonctionnent même sans script. */
.header-drawer__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  list-style: none;
  border: 0;
  border-radius: var(--wily-radius-pill);
  background-color: var(--wily-surface-2);
  color: var(--wily-text);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color var(--wily-transition);
}

.header-drawer__toggle:hover {
  background-color: var(--wily-surface-3);
}

.header-drawer__toggle:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
}

/* Le marqueur natif du `<summary>` est retiré dans les deux moteurs : ici le
   bouton est un pictogramme, pas une ligne de texte dépliable. */
.header-drawer__toggle::-webkit-details-marker {
  display: none;
}

.header-drawer__toggle::marker {
  content: '';
}

/* La commande ouverte se signale : on sait d'où vient le panneau et où le
   refermer. Le même signal sert au tiroir et au dépliant de recherche. */
.header-drawer[open] .header-drawer__toggle {
  background-color: var(--wily-red-soft);
  color: var(--wily-red);
}

/* -- Voile et panneau ---------------------------------------------------
   Le voile et le bouton « fermer » sont rendus `hidden` par le serveur :
   `[hidden]` (section 3) les masque tant que le script ne les a pas révélés.
   Sans JavaScript, on referme en touchant de nouveau le bouton.

   Le panneau est FIXE et plein cadre en hauteur : il s'ouvre par-dessus la
   page, sans la pousser. Le dépliant de recherche, lui, n'est pas un tiroir :
   sa position est décrite dans la section 17, avec le formulaire qu'il
   contient (`.header-drawer--search .header-drawer__panel`), parce que sa
   spécificité doit y gagner sur les règles ci-dessous. */
.header-drawer__scrim {
  position: fixed;
  inset: 0;
  z-index: 1;
  background-color: var(--wily-overlay);
}

.header-drawer__panel {
  position: fixed;
  top: 0;
  bottom: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-3);
  width: min(20rem, 86vw);
  padding: var(--wily-space-4);
  overflow-y: auto;
  overscroll-behavior: contain;
  background-color: var(--wily-surface);
  box-shadow: var(--wily-shadow-lg);
}

.header-drawer--start .header-drawer__panel {
  left: 0;
  border-inline-end: 1px solid var(--wily-border);
}

.header-drawer--end .header-drawer__panel {
  right: 0;
  border-inline-start: 1px solid var(--wily-border);
}

.header-drawer__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--wily-space-3);
}

.header-drawer__title {
  font-size: var(--wily-text-lg);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.header-drawer__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  padding: 0;
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-pill);
  background-color: var(--wily-surface);
  color: var(--wily-text);
  cursor: pointer;
}

.header-drawer__close:hover {
  background-color: var(--wily-surface-2);
}

.header-drawer__subtitle {
  margin: var(--wily-space-2) 0 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* -- Contenu du tiroir de gauche ----------------------------------------
   La navigation et les catégories sont des listes de LIENS : une ligne = une
   cible de 44 px, atteignable au pouce sans viser. Aucun accordéon : sur
   320 px, des niveaux imbriqués seraient illisibles. */
.site-nav--drawer {
  width: 100%;
  margin: 0;
}

.site-nav--drawer .site-nav__list {
  flex-direction: column;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 2px;
  overflow-x: visible;
}

.site-nav--drawer .site-nav__link {
  display: block;
  min-height: 2.75rem;
  padding: var(--wily-space-2) var(--wily-space-3);
}

.drawer-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.drawer-list__link {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--wily-space-3);
  min-height: 2.75rem;
  padding: var(--wily-space-2) var(--wily-space-3);
  border-radius: var(--wily-radius);
  color: var(--wily-text);
  font-size: var(--wily-text-sm);
  font-weight: 600;
  text-decoration: none;
}

.drawer-list__link:hover {
  background-color: var(--wily-surface-2);
  color: var(--wily-text);
}

.drawer-list__name {
  min-width: 0;
}

.drawer-list__count {
  flex: 0 0 auto;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  font-weight: 500;
}

/* Famille active dans le tiroir : même repère que le lien de navigation
   courant (rouge de marque + fond rouge très clair). Deux « vous êtes ici »
   de même nature se lisent donc de la même façon. */
.drawer-list__link[aria-current='page'] {
  background-color: var(--wily-red-soft);
  color: var(--wily-red);
}

.drawer-list__link[aria-current='page'] .drawer-list__count {
  color: var(--wily-red);
}

/* -- Pastilles de bascule de famille (page boutique) ---------------------
   « Toutes » puis Android / PWA / SaaS. Les pastilles passent à la ligne :
   quatre libellés courts tiennent sur une rangée au bureau et se replient
   proprement sur téléphone, sans jamais rogner un mot. Hauteur minimale de
   2,75rem : la cible tactile reste confortable au pouce. */
.delivery-filter {
  margin-bottom: var(--wily-space-4);
}

.delivery-filter__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.delivery-filter__item {
  min-width: 0;
  max-width: 100%;
}

.delivery-filter__link {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: var(--wily-space-2) var(--wily-space-4);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-pill);
  background-color: var(--wily-surface);
  color: var(--wily-text);
  font-size: var(--wily-text-sm);
  font-weight: 600;
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
}

.delivery-filter__link:hover {
  border-color: var(--wily-red);
  color: var(--wily-red);
}

.delivery-filter__link:focus-visible {
  outline: 2px solid var(--wily-red);
  outline-offset: 2px;
}

.delivery-filter__link[aria-current='page'] {
  border-color: var(--wily-red);
  background-color: var(--wily-red);
  color: var(--wily-white);
}

.delivery-filter__link[aria-current='page']:hover {
  background-color: var(--wily-red-dark);
  border-color: var(--wily-red-dark);
  color: var(--wily-white);
}

.delivery-filter__name {
  min-width: 0;
}

/* -- Contenu du tiroir de droite ---------------------------------------- */
.header-drawer__panel .lang-switcher {
  flex-wrap: wrap;
}

.header-drawer__panel .lang-switcher__select {
  flex: 1 1 auto;
}

/* Les actions collent au bas du panneau : le pouce les trouve sans faire
   défiler, et le bloc reste lisible si la liste de catégories est longue. */
.site-header__inner .site-actions--drawer {
  flex-direction: column;
  align-items: stretch;
  gap: var(--wily-space-2);
  margin-top: auto;
}

/* -- Le SEUL écart entre téléphone et bureau : la marque -----------------
   Sur un écran large, la marque reste collée au bouton de menu (alignement à
   gauche classique) et la loupe rejoint le compte à droite. Sous 48rem, les
   deux boutons de droite sont rapprochés du bord : la marque est recentrée
   entre les trois commandes, ce qui donne la barre équilibrée du téléphone.
   Les règles de base étant plus spécifiques (`.site-header__inner .brand`),
   cette surcharge doit venir APRÈS elles. */
@media (max-width: 47.99rem) {

  .site-header__inner .brand {
    margin-inline: auto;
    font-size: var(--wily-text-base);
  }

  .brand__logo {
    height: 1.75rem;
  }
}

@media print {

  .site-header,
  .site-footer,
  .wily-toast-region,
  .lang-switcher {
    display: none;
  }
}

/* --------------------------------------------------------------------------
   19. Console développeur : espace de travail, bandeaux, fiches (D40)
   --------------------------------------------------------------------------
   Le poste de travail d'un éditeur : identité en tête, deux bandeaux
   d'information, une ligne de titre avec ses actions, la recherche, puis UNE
   CARTE PAR FICHE.

   POURQUOI DES CARTES ET NON UN TABLEAU
   -------------------------------------
   Six colonnes tiennent sur un écran de bureau, pas sur un téléphone : un
   `<table>` obligerait à tasser les colonnes ou à faire défiler la page
   latéralement. Chaque ligne est donc une grille qui se replie en colonne. Les
   étiquettes de colonne (`.app-row__label`) sont présentes dans le DOM et ne
   s'AFFICHENT que repliées, là où il n'y a plus d'en-tête pour les porter : la
   ligne reste comprise dans les deux dispositions.

   Aucune couleur en dur : fonds, bordures et statuts viennent des jetons de la
   section 1 (invariant n°4). Les pictogrammes restent des FORMES (`content`
   vide + box-shadow) — jamais de glyphe dans `content:` (§4.1.1), donc rien à
   traduire.
   -------------------------------------------------------------------------- */

/* -- Identité de l'espace de travail (haut droite) ------------------------ */
.workspace {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--wily-space-3);
  margin-bottom: var(--wily-space-4);
}

.workspace__text {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  min-width: 0;
}

.workspace__name {
  font-weight: 700;
  line-height: var(--wily-leading-tight);
}

.workspace__email {
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
}

.workspace__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 2.5rem;
  height: 2.5rem;
  background-color: var(--wily-red);
  border-radius: var(--wily-radius-pill);
  color: var(--wily-white);
  font-weight: 700;
  text-transform: uppercase;
}

.workspace__body {
  padding-top: var(--wily-space-2);
}

/* -- Bandeaux d'information ----------------------------------------------- */
.promo-row {
  display: grid;
  gap: var(--wily-space-4);
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  margin-bottom: var(--wily-space-5);
}

/* Bandeau COMPACT : titre et texte à gauche, bouton à droite, sur la même
   rangée. En colonne avec 1.5 rem de padding, deux bandeaux occupaient près de
   12 rem de haut et repoussaient la liste sous la ligne de flottaison. */
.promo {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--wily-space-1) var(--wily-space-4);
  padding: var(--wily-space-3) var(--wily-space-4);
  border-radius: var(--wily-radius);
  box-shadow: var(--wily-shadow-sm);
  color: var(--wily-white);
}

/* Deux fonds, deux intentions : le premier appelle à l'action (rouge de marque),
   le second informe (bleu d'information). Les deux existent déjà comme jetons. */
.promo--warm {
  background-color: var(--wily-red);
}

.promo--cool {
  background-color: var(--wily-info);
}

.promo__title {
  grid-column: 1;
  margin: 0;
  font-size: var(--wily-text-base);
  line-height: var(--wily-leading-tight);
}

.promo__text {
  grid-column: 1;
  margin: 0;
  color: var(--wily-white);
  font-size: var(--wily-text-sm);
}

/* Le bouton d'un bandeau garde son propre fond : sur un aplat coloré, un bouton
   transparent disparaîtrait. */
.btn--on-promo {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  margin-top: 0;
  background-color: var(--wily-white);
  border-color: var(--wily-white);
  color: var(--wily-text);
}

.btn--on-promo:hover {
  background-color: var(--wily-surface-2);
  border-color: var(--wily-surface-2);
}

/* -- Ligne de titre : compteur, revendication, ajout ---------------------- */
/* Titre et actions sur UNE rangée, centrés verticalement, marge minimale : la
   liste doit commencer tout de suite sous la ligne de titre. */
.list-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--wily-space-2) var(--wily-space-4);
  margin-bottom: var(--wily-space-3);
}

.list-head__text {
  min-width: 0;
}

.list-head__title {
  display: flex;
  align-items: center;
  gap: var(--wily-space-2);
  margin: 0;
  font-size: var(--wily-text-xl);
  line-height: var(--wily-leading-tight);
}

.list-head__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.75rem;
  padding: 0 var(--wily-space-2);
  background-color: var(--wily-surface-2);
  border-radius: var(--wily-radius-pill);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  font-weight: 700;
}

/* Sous-titre et ligne de contexte : la liste des applications les a déplacés
   dans l'infobulle, mais SEPT autres écrans de la console (équipe, structures,
   profil, organisation, notifications, vérification, revendication) affichent
   encore ces deux paragraphes sous leur titre. Les règles restent donc livrées :
   une page qui ne les utilise pas ne paie rien, une page qui les utilise garde
   sa hiérarchie. */
.list-head__subtitle {
  margin: var(--wily-space-1) 0 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
}

.list-head__count-text {
  margin: var(--wily-space-1) 0 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
}

/* -- Infobulle d'information (elle remplace le bloc de texte du titre) ----- */
/* Le déclencheur est un BOUTON : il reçoit le focus au clavier, et c'est
   `:focus-within` qui ouvre la bulle. Un `title=` natif serait invisible au
   clavier et non stylable ; une bulle en JavaScript serait interdite (CSP).
   La bulle est HORS FLUX : elle ne pousse jamais la liste vers le bas. */
.info-tip {
  position: relative;
  display: inline-flex;
  align-items: center;
}

.info-tip__trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  background-color: var(--wily-surface-2);
  border: 1px solid var(--wily-border-strong);
  border-radius: var(--wily-radius-pill);
  color: var(--wily-text-muted);
  cursor: help;
  transition: border-color var(--wily-transition), color var(--wily-transition);
}

.info-tip__trigger:hover {
  border-color: var(--wily-red);
  color: var(--wily-red);
}

.info-tip__trigger:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
}

/* Le « i » est DESSINÉ : un point et une barre, deux formes `content: ''`.
   Aucun glyphe dans `content:` (§4.1.1) — donc rien à traduire ni à charger. */
.info-tip__trigger::before,
.info-tip__trigger::after {
  content: '';
  position: absolute;
  left: 50%;
  width: 0.1875rem;
  background-color: currentColor;
  border-radius: 1px;
  transform: translateX(-50%);
}

.info-tip__trigger::before {
  top: 0.3125rem;
  height: 0.1875rem;
}

.info-tip__trigger::after {
  top: 0.625rem;
  height: 0.5rem;
}

/* La BULLE : hors flux, donc aucune hauteur ajoutée à la page. Le couple
   fond/texte vient des jetons INVERSÉS (`--wily-text` sur `--wily-bg`) : il
   reste lisible dans les deux thèmes sans introduire de couleur.
   Les propriétés typographiques du titre sont RÉTABLIES : une infobulle dans un
   `h1` hériterait sinon de son échelle et de sa graisse. */
.info-tip__bubble {
  position: absolute;
  top: calc(100% + var(--wily-space-2));
  left: 0;
  z-index: var(--wily-z-header);
  display: none;
  flex-direction: column;
  gap: var(--wily-space-1);
  width: max-content;
  max-width: min(22rem, 70vw);
  padding: var(--wily-space-2) var(--wily-space-3);
  background-color: var(--wily-text);
  border-radius: var(--wily-radius-sm);
  box-shadow: var(--wily-shadow);
  color: var(--wily-bg);
  font-size: var(--wily-text-xs);
  font-weight: 400;
  letter-spacing: normal;
  line-height: var(--wily-leading-normal);
  text-align: left;
  text-transform: none;
}

/* Deux déclencheurs, un seul état visible : le survol à la souris, le focus au
   clavier (`:focus-within` couvre le bouton). Aucun script n'est nécessaire. */
.info-tip:hover .info-tip__bubble,
.info-tip:focus-within .info-tip__bubble {
  display: flex;
}

/* Deux lignes : la promesse de l'écran, puis le compte. La seconde est plus
   discrète — c'est un compteur, pas un argument de vente. */
.info-tip__text {
  font-weight: 600;
}

.info-tip__meta {
  opacity: 0.85;
}

.list-head__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-3);
}

/* Ambre d'avertissement plutôt que rouge : ce n'est pas l'action principale de
   l'écran, et deux boutons de même couleur côte à côte ne hiérarchiseraient
   plus rien. */
.btn--claim {
  background-color: var(--wily-warning);
  border-color: var(--wily-warning);
  color: var(--wily-white);
}

.btn--claim:hover {
  background-color: var(--wily-text);
  border-color: var(--wily-text);
  color: var(--wily-white);
}

.btn--lg {
  min-height: 3rem;
  padding-inline: var(--wily-space-6);
}

/* Bouton compact d'une ligne de fiche : plus étroit, mais il conserve la cible
   tactile de 2,75 rem (44 px) — c'est la largeur qui change, pas la hauteur. */
.btn--compact {
  min-height: 2.75rem;
  padding-inline: var(--wily-space-3);
  font-size: var(--wily-text-sm);
}

/* -- Recherche ------------------------------------------------------------ */
/* La barre occupe TOUTE la largeur disponible : son bord gauche est donc celui
   du tableau, et son bord droit celui des boutons de titre. La marge basse est
   réduite : la liste commence juste dessous. */
.searchbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-2);
  margin-bottom: var(--wily-space-3);
}

.searchbar__input {
  flex: 1 1 16rem;
  min-width: 0;
  min-height: 2.75rem;
  padding: var(--wily-space-2) var(--wily-space-4);
  border: 1px solid var(--wily-border-strong);
  border-radius: var(--wily-radius-pill);
  background-color: var(--wily-surface);
  color: var(--wily-text);
  font: inherit;
  font-size: var(--wily-text-sm);
}

.searchbar__input:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 1px;
}

.searchbar__clear {
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

.searchbar__clear:hover {
  color: var(--wily-red);
}

/* -- Liste des fiches ----------------------------------------------------- */
/* Une seule surface, des séparateurs : la liste se lit comme un TABLEAU, pas
   comme une pile de cartes. Le cadre et le rayon vivent sur le conteneur, les
   lignes ne portent plus qu'un filet bas — plus d'ombre par ligne, donc plus de
   vide entre deux fiches. Largeur pleine, `box-sizing` inclus : la liste prend
   exactement la place disponible à droite. */
.app-rows {
  display: flex;
  flex-direction: column;
  width: 100%;
  box-sizing: border-box;
  gap: 0;
  overflow: hidden;
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius);
}

/* Une ligne = une rangée de TABLEAU à sept pistes figées, dans cet ordre :
   [vignette] [nom] [plateforme] [publication] [mise à jour] [statut] [actions].
   La vignette a SA propre piste (`2.75rem`) au lieu d'être noyée dans le bloc
   texte : c'est ce qui garantit qu'aucune colonne ne peut écraser sa voisine,
   le nom gardant `minmax(0, 1.5fr)` pour l'ellipse. Les dates et les badges
   sont exprimés en `rem` (largeur du contenu réel), les actions en
   `max-content` pour ne jamais être tronquées.
   `display: contents` sur `.app-row__identity` fait remonter la vignette et le
   bloc texte au niveau de la grille sans dupliquer le balisage.
   La cible tactile reste de 44 px (`.btn--compact`, `.icon-btn`) : c'est la
   police et la taille de la vignette qui diminuent, pas la zone cliquable. */
.app-row {
  display: grid;
  grid-template-columns:
    2.75rem minmax(0, 1.5fr) 5.5rem 8.5rem 8.5rem minmax(5.5rem, 1fr) max-content;
  column-gap: var(--wily-space-3);
  row-gap: var(--wily-space-1);
  align-items: center;
  font-size: 0.8125rem;
  padding: var(--wily-space-2) var(--wily-space-4);
  background-color: var(--wily-surface);
  border: 0;
  border-bottom: 1px solid var(--wily-border);
  border-radius: 0;
  box-shadow: none;
}

.app-row:last-child {
  border-bottom: 0;
}

.app-row:hover {
  background-color: var(--wily-surface-2);
}

/* Coquille historique : elle ne sert plus qu'à regrouper les deux noeuds, sa
   boîte disparaît pour que la grille du parent les place directement. */
.app-row__identity {
  display: contents;
}

/* Vignette de 36 px : la ligne reste dense, et le NOM demeure l'élément
   dominant — c'est lui qu'un éditeur cherche en balayant la liste. Elle occupe
   désormais sa propre piste de grille (`2.75rem`), d'où `justify-self`. */
.app-row__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  justify-self: start;
  width: 2.25rem;
  height: 2.25rem;
  overflow: hidden;
  background-color: var(--wily-surface-2);
  border-radius: var(--wily-radius-sm);
}

.app-row__monogram {
  color: var(--wily-red);
  font-size: var(--wily-text-base);
  font-weight: 700;
  line-height: 1;
  text-transform: uppercase;
}

.app-row__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.app-row__name {
  overflow: hidden;
  color: var(--wily-text);
  font-weight: 700;
  text-decoration: none;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-row__name:hover {
  color: var(--wily-red);
  text-decoration: underline;
}

.app-row__name:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
}

.app-row__package {
  overflow: hidden;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.app-row__cell {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-1);
  margin: 0;
  min-width: 0;
}

/* L'étiquette de colonne n'est utile QUE repliée : en ligne, la disposition
   horizontale la rend redondante. Elle est donc masquée par défaut et révélée
   par la requête média du bas — jamais retirée du DOM, sinon une carte empilée
   ne dirait plus ce que chaque valeur représente. */
.app-row__label {
  display: none;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  font-weight: 600;
}

/* Une valeur tient sur UNE ligne : un horodatage brut est long, et une colonne
   qui s'élargit casserait l'alignement des six colonnes. Les chiffres sont
   tabulaires pour que les dates se superposent d'une ligne à l'autre. */
.app-row__value {
  overflow: hidden;
  font-size: inherit;
  font-variant-numeric: tabular-nums;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Un badge ne s'étire pas sur toute la colonne : une pastille pleine largeur ne
   se lit plus comme un badge. */
.app-row__cell>.badge {
  align-self: flex-start;
}

/* Les explications longues (révision en cours, fiche rejetée) ne sont plus des
   noeuds du flux : elles vivent dans l'attribut `title` du badge. Ces badges
   reçoivent `has-tooltip` et un curseur d'aide, seul indice visuel nécessaire.
   Plus aucune largeur n'est consommée, donc plus aucun chevauchement. */
.app-row .has-tooltip {
  cursor: help;
}

/* -- Badges d'état -------------------------------------------------------- */
.status-badge {
  align-self: flex-start;
  padding: 0.15rem var(--wily-space-2);
  border-radius: var(--wily-radius-sm);
  font-size: var(--wily-text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.status-badge.is-draft {
  background-color: var(--wily-surface-2);
  color: var(--wily-text-muted);
}

.status-badge.is-in-review {
  background-color: var(--wily-warning-soft);
  color: var(--wily-warning);
}

.status-badge.is-published {
  background-color: var(--wily-success-soft);
  color: var(--wily-success);
}

.status-badge.is-suspended {
  background-color: var(--wily-danger-soft);
  color: var(--wily-danger);
}

.status-badge.is-removed {
  background-color: var(--wily-surface-3);
  color: var(--wily-text-muted);
}

/* « Révision en cours » n'est pas un état de la FICHE : c'est une proposition
   ouverte à côté d'elle. Elle se pose donc à côté du badge d'état, en version
   douce et dans la teinte d'information — deux aplats pleins côte à côte
   crieraient aussi fort l'un que l'autre, alors que la fiche, elle, est
   toujours publiée. */
.status-badge.is-revision {
  background-color: var(--wily-info-soft);
  color: var(--wily-info);
}

/* « Rejetée » est le seul état qui demande une ACTION de l'éditeur : il prend
   l'aplat plein et son texte passe en blanc. Les autres restent en version
   douce — l'aplat rouge de marque reste rare, donc lisible. */
.status-badge.is-rejected {
  background-color: var(--wily-danger);
  color: var(--wily-white);
}

/* -- Actions d'une ligne -------------------------------------------------- */
.row-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--wily-space-2);
}

.row-actions__form {
  display: flex;
  margin: 0;
}

/* Pictogramme-bouton : 44 px de côté (cible tactile conservée). Les formes sont
   dessinées en CSS — aucune police d'icônes, aucun SVG à charger, et rien à
   traduire : le nom accessible est un texte masqué dans le bouton. */
.icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  background-color: var(--wily-surface-2);
  border: 0;
  border-radius: var(--wily-radius-sm);
  color: var(--wily-text-muted);
  cursor: pointer;
  transition: background-color var(--wily-transition), color var(--wily-transition);
}

.icon-btn:hover {
  background-color: var(--wily-surface-3);
}

.icon-btn:focus-visible {
  outline: 3px solid var(--wily-focus);
  outline-offset: 2px;
}

.icon-btn::before,
.icon-btn::after {
  content: '';
  position: absolute;
  background-color: currentColor;
  border-radius: 2px;
}

/* Les pictogrammes ne sont plus DESSINÉS en CSS (deux barres pouvaient se lire
   comme deux objets identiques à 18 px). Chaque bouton porte un SVG inline
   `currentColor` : trait net, forme sans ambiguïté, et zéro attribut `style=`
   (CSP, ST-11). */
.icon-btn__glyph {
  display: block;
  width: 1.125rem;
  height: 1.125rem;
  flex: 0 0 auto;
}

/* Crayon de marque : rouge Wily — c'est l'action principale de la ligne. */
.icon-btn--edit {
  color: var(--wily-red);
}

.icon-btn--edit:hover {
  background-color: var(--wily-red-soft);
  color: var(--wily-red-dark);
}

/* -- F-46 : l'action destructrice porte son LIBELLÉ ------------------------ */
/* Une corbeille seule, à 8 px du crayon et dans la même teinte grise, ne se
   distingue pas de lui : l'éditeur visait la suppression et ouvrait la fiche.
   Le bouton dit donc ce qu'il fait (« Retirer », clé `console.apps.delete`),
   et la corbeille reste en tête pour la lecture rapide. L'espacement interne
   est serré (`space-1`) pour que le libellé coûte le moins de largeur
   possible : la ligne reste dense (F-45), et `btn--compact` conserve la cible
   tactile de 44 px. La teinte de danger vient de `btn--danger`. */
.row-actions__delete {
  gap: var(--wily-space-1);
}

/* Le pictogramme du bouton libellé est plus petit que celui d'un bouton-icône
   (16 px au lieu de 18) : le texte porte le sens, l'icône l'accompagne. Il ne
   se comprime jamais dans un bouton étroit. */
.row-actions__delete-glyph {
  display: block;
  flex: 0 0 auto;
  width: 1rem;
  height: 1rem;
}

/* -- F-51 : la piste d'actions ne peut plus pousser une cible hors carte --- */
/* `.app-row` se termine par une piste `max-content`, dans une carte
   `overflow:hidden`. Une cible de plus n'était donc pas une action de plus :
   c'était une action de MOINS — le bouton « Soumettre » sortait du cadre et
   devenait inatteignable. Les avis ne vivent plus sur la ligne (ils sont dans
   la fiche éditeur et dans la boîte notifications) ; cette règle est le filet
   de sécurité : le groupe d'actions se replie au lieu de déborder, et la
   piste peut enfin rétrécir. */
.row-actions {
  flex-wrap: wrap;
  min-width: 0;
}

/* Filet vertical : la suppression est physiquement SÉPARÉE des autres actions.
   Le clic ne peut plus glisser de la corbeille vers la fiche, et l'œil ne lit
   plus les deux pictogrammes comme un couple interchangeable. */
.row-actions__danger {
  padding-left: var(--wily-space-3);
  border-left: 1px solid var(--wily-border);
}

/* -- Écrans étroits : la promo passe sur une colonne ----------------------- */
@media (max-width: 40rem) {
  .promo {
    grid-template-columns: 1fr;
  }

  .btn--on-promo {
    grid-column: 1;
    grid-row: auto;
    justify-self: start;
  }
}

/* -- Petits écrans : la ligne se replie, les étiquettes apparaissent -------- */
@media (max-width: 59.99rem) {

  /* Repli : la vignette et le nom tiennent la première rangée, tout le reste
     occupe la pleine largeur. `grid-column: 1 / -1` remplace l'ancien
     `grid-template-columns: 1fr` : sans cela, les cellules resteraient dans la
     colonne du nom et la carte se tasserait au lieu de s'empiler. */
  .app-row {
    grid-template-columns: 2.75rem minmax(0, 1fr);
    column-gap: var(--wily-space-3);
    row-gap: var(--wily-space-2);
    padding: var(--wily-space-3) var(--wily-space-4);
  }

  .app-row__icon {
    grid-column: 1;
    grid-row: 1;
  }

  .app-row__text {
    grid-column: 2;
    grid-row: 1;
  }

  .app-row__cell,
  .row-actions {
    grid-column: 1 / -1;
  }

  .app-row__label {
    display: block;
  }

  .row-actions {
    flex-wrap: wrap;
    justify-content: flex-start;
  }

  .list-head {
    align-items: flex-start;
  }

  .workspace {
    justify-content: flex-start;
  }
}

/* -- Impression : le châssis sort de la page, les fiches restent ----------- */
@media print {

  .sidebar,
  .shell__toggle,
  .searchbar,
  .promo-row,
  .list-head__actions {
    display: none;
  }

  .app-row {
    border-color: var(--wily-border-strong);
    box-shadow: none;
  }
}

/* =============================================================================
   20. Profil éditeur et vignettes d'image (D41)

   Deux surfaces, une même contrainte : une image vient d'un stockage PRIVÉ et
   peut manquer. Elle est donc toujours posée PAR-DESSUS un repli déjà écrit
   dans le DOM (monogramme ou initiale) : si l'objet a disparu, le navigateur
   n'affiche rien et le repli reste lisible. Aucun JavaScript, aucun `onerror`
   (CSP, ST-11), aucune case vide.
   ========================================================================== */

/* -- Vignette d'une fiche : l'icône recouvre le monogramme ------------------ */
.app-row__icon {
  position: relative;
}

.app-row__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* -- Photo de profil dans la coquille -------------------------------------- */
.sidebar__avatar,
.workspace__avatar {
  position: relative;
  overflow: hidden;
}

.sidebar__avatar-image,
.workspace__avatar-image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* -- Page de profil -------------------------------------------------------- */
/* Deux cartes côte à côte dès qu'il y a la place ; empilées sur téléphone.
   `minmax(18rem, 1fr)` évite une carte trop étroite pour le champ de nom. */
.profile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
  gap: var(--wily-space-4);
  align-items: start;
}

.profile-avatar {
  margin: 0 0 var(--wily-space-3);
}

.profile-avatar__image {
  display: block;
  width: 6rem;
  height: 6rem;
  object-fit: cover;
  background-color: var(--wily-surface-2);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-pill);
}

/* Le retrait est un choix EXPLICITE : une case, jamais un bouton qui efface
   au premier clic. Le texte reste lisible sans la case (lecteur d'écran). */
.profile-check {
  display: flex;
  align-items: center;
  gap: var(--wily-space-2);
  margin-top: var(--wily-space-3);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

/* Édition : un crayon diagonal — la pointe est découpée dans la forme, donc
   aucun glyphe n'est écrit dans `content:` et rien n'est à charger. */
.icon--edit::before {
  content: '';
  position: absolute;
  inset: 0;
  width: 0.55rem;
  height: 1rem;
  margin: auto;
  background-color: currentColor;
  clip-path: polygon(50% 0, 100% 26%, 100% 100%, 0 100%, 0 26%);
  transform: rotate(225deg);
}

/* Identité du compte : une ligne dense — avatar, textes tronqués, puis le
   crayon qui porte son libellé (QUALITÉ #12) et mène à l'édition réelle. */
.account-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-3);
}

.account-head__avatar {
  flex: 0 0 auto;
  width: 3rem;
  height: 3rem;
  object-fit: cover;
  background-color: var(--wily-surface-2);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-pill);
}

.account-head__avatar--empty {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--wily-text-muted);
  font-size: 1.125rem;
  font-weight: 600;
}

.account-head__texts {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}

.account-head__name,
.account-head__email {
  overflow: hidden;
  margin: 0;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.account-head__name {
  font-weight: 600;
}

.account-head__email {
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

.account-head__edit {
  margin-left: auto;
}

/* =============================================================================
   21. Notifications de l'éditeur (D42)

   Une notification est une LIGNE DE JOURNAL rendue lisible : aucune boîte de
   réception séparée, aucun compteur décoratif. Trois tons seulement — info
   (soumission), succès (publication), alerte (rejet motivé) — et un état « non
   lu » porté par une bordure franche ET par le mot « non lue » écrit dans la
   carte : l'information survit au daltonisme comme à l'impression noir et blanc.
   Aucune couleur n'est écrite en dur ici, seulement des jetons de la charte.
   ========================================================================== */

.notice-list {
  display: flex;
  flex-direction: column;
  gap: var(--wily-space-3);
}

/* Deux colonnes : le récit de l'événement, puis ses actions. La bordure de
   départ reste neutre tant que l'événement est lu — un rejet déjà traité ne
   doit plus crier. */
.notice {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--wily-space-4);
  align-items: start;
  padding: var(--wily-space-4) var(--wily-space-5);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-inline-start: 4px solid var(--wily-border-strong);
  border-radius: var(--wily-radius);
  box-shadow: var(--wily-shadow-sm);
}

/* Non lu : la bordure s'épaissit et prend le ton de l'événement. */
.notice.is-unread {
  border-inline-start-width: 6px;
}

.notice--info.is-unread {
  border-inline-start-color: var(--wily-info);
}

.notice--success.is-unread {
  border-inline-start-color: var(--wily-success);
}

.notice--warning.is-unread {
  border-inline-start-color: var(--wily-warning);
}

.notice__body {
  min-width: 0;
}

.notice__title {
  margin: 0;
  font-size: var(--wily-text-lg);
  font-weight: 700;
  overflow-wrap: anywhere;
}

.notice__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--wily-space-2);
  margin: var(--wily-space-1) 0 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
}

/* Horodatage BRUT (QUALITÉ #10) : chiffres de largeur fixe, pas de mise en
   forme manuelle — c'est le même texte qu'en base, donc le même instant. */
.notice__time {
  font-variant-numeric: tabular-nums;
}

/* Pastille « non lue » : rouge de marque, comme le compteur du menu. */
.notice__chip {
  padding: 0.05rem var(--wily-space-2);
  border-radius: var(--wily-radius-pill);
  background-color: var(--wily-red);
  color: var(--wily-white);
  font-weight: 700;
}

/* Motif de rejet : du texte libre de modérateur, donc traité comme une citation
   et non comme une consigne d'interface. */
.notice__reason {
  margin-top: var(--wily-space-3);
  padding: var(--wily-space-3) var(--wily-space-4);
  background-color: var(--wily-surface-2);
  border-inline-start: 3px solid var(--wily-warning);
  border-radius: var(--wily-radius-sm);
}

.notice__reason-label {
  margin: 0 0 var(--wily-space-1);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.notice__reason p {
  margin: 0;
  overflow-wrap: anywhere;
}

.notice__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--wily-space-2);
}

.notice__actions form,
.list-head__form {
  display: flex;
  margin: 0;
}

/* Repli sur téléphone : une seule colonne, actions à gauche comme les lignes de
   fiches. */
@media (max-width: 40rem) {
  .notice {
    grid-template-columns: 1fr;
  }

  .notice__actions {
    justify-content: flex-start;
  }
}

@media print {
  .notice {
    box-shadow: none;
  }
}

/* --------------------------------------------------------------------------
   22. Aide au téléchargement de l'APK (.wily-apk-help)
   --------------------------------------------------------------------------
   Le balisage est un <dialog> natif rendu par `catalog/app.php` : le
   navigateur fournit le piège à focus, la touche Échap et l'inertie du fond,
   le script se contente d'ouvrir et de fermer. Sans JavaScript, la modale
   reste fermée et le lien de téléchargement garde son comportement normal.

   La numérotation des étapes vient des compteurs CSS, jamais d'un chiffre
   recopié dans le balisage : traduire une étape ne peut pas casser l'ordre.
   -------------------------------------------------------------------------- */
.wily-apk-help {
  width: min(30rem, calc(100vw - 2 * var(--wily-space-4)));
  max-width: none;
  max-height: calc(100vh - 2 * var(--wily-space-4));
  max-height: calc(100dvh - 2 * var(--wily-space-4));
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--wily-text);
}

.wily-apk-help::backdrop {
  background: var(--wily-overlay);
}

.wily-apk-help__panel {
  display: grid;
  gap: var(--wily-space-4);
  max-height: calc(100vh - 2 * var(--wily-space-4));
  max-height: calc(100dvh - 2 * var(--wily-space-4));
  padding: var(--wily-space-5);
  overflow-y: auto;
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-block-start: 4px solid var(--wily-red);
  border-radius: var(--wily-radius-lg);
  box-shadow: var(--wily-shadow-lg);
  animation: wily-apk-help-in 180ms ease-out;
}

.wily-apk-help__title {
  margin: 0;
  font-size: var(--wily-text-xl);
  line-height: var(--wily-leading-tight);
}

.wily-apk-help__intro {
  margin: 0;
  color: var(--wily-text-muted);
}

/* Liste ordonnée sans puces : le rang est porté par la pastille rouge, pas par
   un marqueur de liste dont la position change avec la longueur du texte. */
.wily-apk-help__steps {
  display: grid;
  gap: var(--wily-space-3);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: wily-apk-step;
}

.wily-apk-help__step {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: var(--wily-space-3);
  counter-increment: wily-apk-step;
}

.wily-apk-help__step::before {
  content: counter(wily-apk-step);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--wily-radius-pill);
  background-color: var(--wily-red);
  color: var(--wily-white);
  font-size: var(--wily-text-sm);
  font-weight: 700;
  line-height: 1;
}

.wily-apk-help__text {
  margin: 0;
  line-height: var(--wily-leading-normal);
  overflow-wrap: anywhere;
}

.wily-apk-help__close {
  margin: 0;
}

@keyframes wily-apk-help-in {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .wily-apk-help__panel {
    animation: none;
  }
}

/* Téléphone : la modale prend toute la largeur utile et le bouton de fermeture
   reste le dernier élément atteint, sous le pouce. */
@media (max-width: 30rem) {
  .wily-apk-help {
    width: calc(100vw - 2 * var(--wily-space-3));
  }

  .wily-apk-help__panel {
    padding: var(--wily-space-4);
  }
}

/* --------------------------------------------------------------------------
   23. Captures de la fiche (.app-shots)
   --------------------------------------------------------------------------
   DEUX pistes, jamais une seule. Une capture de telephone est en portrait
   (9:16), une capture de bureau en paysage (16:10) : alignees dans la meme
   bande, elles donneraient des vignettes de hauteurs inegales. Chaque groupe
   garde donc SON format et SON defilement horizontal.
   -------------------------------------------------------------------------- */
.app-shots {
  margin-block-start: var(--wily-space-6);
}

.app-shots__title {
  margin: 0 0 var(--wily-space-3);
  font-size: var(--wily-text-lg);
  line-height: var(--wily-leading-tight);
}

.app-shots__group+.app-shots__group {
  margin-block-start: var(--wily-space-4);
}

.app-shots__subtitle {
  margin: 0 0 var(--wily-space-2);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* Piste horizontale native : le debordement suffit, aucun script. Le
   `scroll-snap` ne fait qu arreter le geste sur une vignette entiere. */
.app-shots__strip {
  display: flex;
  gap: var(--wily-space-3);
  margin: 0;
  padding: 0 0 var(--wily-space-2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  list-style: none;
  scroll-snap-type: x proximity;
}

.app-shots__item {
  flex: 0 0 auto;
  scroll-snap-align: start;
}

/* Les deux pistes n'ont pas la meme respiration : le portrait se parcourt
   carte par carte (aimantation centree), le paysage defile en bandeau. */
.app-shots__strip--phone {
  gap: var(--wily-space-2);
}

.app-shots__strip--phone .app-shots__item {
  scroll-snap-align: center;
}

.app-shots__image {
  display: block;
  background-color: var(--wily-surface-2);
  border: 1px solid var(--wily-border);
  border-radius: 8px;
  box-shadow: var(--wily-shadow-sm);
}

/* Chaque format garde SES dimensions : c'est ce qui rend les deux pistes
   lisibles separement. Le portrait remplit son cadre, le paysage s'y inscrit
   entier (`contain`) pour ne jamais rogner l'interface d'un service. */
.app-shots__image--phone {
  width: 270px;
  height: 480px;
  object-fit: cover;
}

.app-shots__image--desktop {
  width: 280px;
  height: auto;
  object-fit: contain;
}

@media (max-width: 30rem) {
  .app-shots__image--phone {
    width: 216px;
    height: 384px;
  }

  .app-shots__image--desktop {
    width: 224px;
  }
}

/* --------------------------------------------------------------------------
   23 bis. Vidéos de la fiche (.app-videos)
   --------------------------------------------------------------------------
   Carte COMPACTE des vidéos qui ne sont pas le héros : une fiche APK, ou les
   vidéos suivantes d'une fiche web. Elle dit ce qu'elle fait — ouvrir
   l'adresse dans un onglet neuf — au lieu d'imiter un lecteur. Une seule
   bordure, aucun ornement (QUALITÉ #12).
   -------------------------------------------------------------------------- */
.app-videos {
  margin-block-start: var(--wily-space-6);
}

.app-videos__title {
  margin: 0 0 var(--wily-space-3);
  font-size: var(--wily-text-lg);
  line-height: var(--wily-leading-tight);
}

.app-videos__list {
  display: grid;
  gap: var(--wily-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.app-videos__link {
  display: flex;
  align-items: center;
  gap: var(--wily-space-3);
  min-height: 56px;
  padding: var(--wily-space-2) var(--wily-space-3);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-sm);
  background-color: var(--wily-surface-2);
  color: inherit;
  text-decoration: none;
}

.app-videos__link:hover,
.app-videos__link:focus-visible {
  border-color: var(--wily-red);
}

/* Forme CSS, jamais un glyphe : le triangle est dessiné, aucune police
   d'icônes n'est chargée pour la boutique publique. */
.app-videos__play {
  flex: 0 0 auto;
  width: 0;
  height: 0;
  border-left: 14px solid var(--wily-red);
  border-top: 8px solid transparent;
  border-bottom: 8px solid transparent;
}

.app-videos__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.app-videos__label {
  font-weight: 700;
  line-height: var(--wily-leading-tight);
}

.app-videos__host {
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  overflow-wrap: anywhere;
}

/* --------------------------------------------------------------------------
   23 ter. Vidéo héros de la fiche (.app-video-hero)
   --------------------------------------------------------------------------
   SaaS et PWA : la démonstration pleine largeur passe AVANT l'entête
   d'identité. Un seul cadre 16:9, une bordure, aucune ombre (QUALITÉ #12).
   La hauteur vient de `aspect-ratio`, jamais d'une valeur fixe : la page ne
   saute pas quand la vidéo arrive.
   -------------------------------------------------------------------------- */
.app-video-hero {
  /* Aucune marge basse : `.app-detail` est une colonne flex dont le `gap`
     porte déjà l'espacement, l'ajouter ici créerait un trou (QUALITÉ #12). */
  margin: 0;
}

.app-video-hero__frame {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  overflow: hidden;
  background-color: #000;
}

.app-video-hero__media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background-color: #000;
}

.app-video-hero__badge {
  position: absolute;
  inset-block-start: var(--wily-space-3);
  inset-inline-start: var(--wily-space-3);
  padding: 4px var(--wily-space-2);
  border-radius: var(--wily-radius-pill);
  background-color: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: var(--wily-text-sm);
  font-weight: 700;
  line-height: var(--wily-leading-tight);
  pointer-events: none;
}

/* Bascule centrale. Le glyphe est DESSINÉ : triangle en pause, deux barres en
   lecture — aucune police d'icônes n'est chargée par la boutique publique. Le
   bouton reste dans le flux de frappe ; en lecture il s'efface, mais le survol
   et le focus le ramènent à plein contraste. */
.app-video-hero__toggle {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background-color: rgba(0, 0, 0, 0.55);
  color: #fff;
  cursor: pointer;
  transform: translate(-50%, -50%);
  transition: opacity 160ms ease, background-color 160ms ease;
}

.app-video-hero__toggle[hidden],
.app-video-hero__sound[hidden] {
  display: none;
}

.app-video-hero__toggle[data-state="playing"] {
  opacity: 0.35;
}

.app-video-hero__toggle:hover,
.app-video-hero__toggle:focus-visible {
  background-color: rgba(0, 0, 0, 0.78);
  opacity: 1;
}

.app-video-hero__toggle:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

.app-video-hero__glyph {
  display: block;
  width: 0;
  height: 0;
  border-left: 16px solid currentColor;
  border-top: 10px solid transparent;
  border-bottom: 10px solid transparent;
}

.app-video-hero__toggle[data-state="playing"] .app-video-hero__glyph {
  width: 4px;
  height: 18px;
  border: 0;
  background-color: currentColor;
  box-shadow: 9px 0 0 currentColor;
}

/* Son : un libellé traduit, jamais un pictogramme seul — l'action change de
   sens d'un clic à l'autre. Cible tactile de 44 px (QUALITÉ #12). */
.app-video-hero__sound {
  position: absolute;
  inset-block-end: var(--wily-space-3);
  inset-inline-end: var(--wily-space-3);
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--wily-space-3);
  border: 0;
  border-radius: var(--wily-radius-pill);
  background-color: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: var(--wily-text-sm);
  font-weight: 700;
  cursor: pointer;
  transition: background-color 160ms ease;
}

.app-video-hero__sound:hover,
.app-video-hero__sound:focus-visible {
  background-color: rgba(0, 0, 0, 0.82);
}

.app-video-hero__sound:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* Anneau d'attente : centré par `inset: 0` + `margin: auto`, et non par une
   translation — `wily-spin` anime `transform`, une translation serait écrasée. */
.app-video-hero__spinner {
  position: absolute;
  inset: 0;
  display: none;
  width: 32px;
  height: 32px;
  margin: auto;
  border: 3px solid rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: wily-spin 700ms linear infinite;
  pointer-events: none;
}

.app-video-hero__frame.is-buffering .app-video-hero__spinner {
  display: block;
}

.app-video-hero__caption {
  margin-block-start: var(--wily-space-2);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

/* --------------------------------------------------------------------------
   24. Notes et avis de la fiche (.app-reviews)
   Aucune barre n'est dessinee en `style=` : l'echelle passe par <progress>,
   que les navigateurs stylent nativement (voir __progress ci-dessous).
   -------------------------------------------------------------------------- */
.app-reviews {
  margin-block-start: var(--wily-space-7);
  padding-block-start: var(--wily-space-5);
  border-block-start: 1px solid var(--wily-border);
}

.app-reviews__title {
  margin: 0 0 var(--wily-space-4);
  font-size: var(--wily-text-lg);
  line-height: var(--wily-leading-tight);
}

.app-reviews__empty {
  margin: 0 0 var(--wily-space-4);
  padding: var(--wily-space-4);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  background-color: var(--wily-surface-2);
  border-radius: var(--wily-radius);
}

.app-reviews__summary {
  display: grid;
  gap: var(--wily-space-4);
  margin-block-end: var(--wily-space-5);
}

.app-reviews__score {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-2);
  align-items: baseline;
  margin: 0;
}

.app-reviews__score-value {
  color: var(--wily-text);
  font-size: var(--wily-text-3xl);
  font-weight: 800;
  line-height: var(--wily-leading-tight);
  letter-spacing: -0.02em;
}

.app-reviews__score-total {
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

.app-reviews__bars {
  display: grid;
  gap: var(--wily-space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.app-reviews__bar {
  display: grid;
  grid-template-columns: 1.5rem 1fr 2.5rem;
  gap: var(--wily-space-3);
  align-items: center;
}

.app-reviews__bar-label,
.app-reviews__bar-count {
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  font-variant-numeric: tabular-nums;
}

.app-reviews__bar-count {
  text-align: end;
}

/* <progress> natif : on neutralise l'apparence du systeme puis on peint la
   piste et la valeur. Aucun attribut `style=` n'est ecrit dans le HTML. */
.app-reviews__progress {
  width: 100%;
  height: 0.5rem;
  overflow: hidden;
  appearance: none;
  background-color: var(--wily-surface-2);
  border: 0;
  border-radius: var(--wily-radius-pill);
}

.app-reviews__progress::-webkit-progress-bar {
  background-color: var(--wily-surface-2);
  border-radius: var(--wily-radius-pill);
}

.app-reviews__progress::-webkit-progress-value {
  background-color: var(--wily-red);
  border-radius: var(--wily-radius-pill);
}

.app-reviews__progress::-moz-progress-bar {
  background-color: var(--wily-red);
  border-radius: var(--wily-radius-pill);
}

.app-reviews__list {
  display: grid;
  gap: var(--wily-space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.app-reviews__item {
  display: grid;
  gap: var(--wily-space-2);
  padding: var(--wily-space-4);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius);
}

.app-reviews__item-head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-2);
  align-items: center;
  margin: 0;
}

/* Pastille d'initiale : elle donne un repere visuel a l'auteur sans photo ni
   gravatar (aucune requete tierce, aucune donnee personnelle envoyee). */
.app-reviews__item-avatar {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background-color: var(--wily-surface-2, var(--wily-border));
  color: var(--wily-text);
  font-size: var(--wily-text-xs, 0.75rem);
  font-weight: 700;
  text-transform: uppercase;
}

.app-reviews__item-author {
  color: var(--wily-text);
  font-size: var(--wily-text-sm);
  font-weight: 700;
}

.app-reviews__item-body {
  margin: 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
  overflow-wrap: anywhere;
}

/* Etoiles dessinees en CSS (clip-path) : aucune glyphe et aucun emoji dans
   le HTML, donc rien a traduire et rien a charger. `.star--on` = pleine. */
.app-reviews__stars {
  display: inline-flex;
  gap: 0.15rem;
  align-items: center;
}

.app-reviews__star {
  display: block;
  width: 0.85rem;
  height: 0.85rem;
  background-color: var(--wily-border-strong);
  clip-path: polygon(50% 0%,
      61% 35%,
      98% 35%,
      68% 57%,
      79% 91%,
      50% 70%,
      21% 91%,
      32% 57%,
      2% 35%,
      39% 35%);
}

.app-reviews__star--on {
  background-color: var(--wily-red);
}

.app-reviews__reply {
  display: grid;
  gap: var(--wily-space-1);
  margin-block-start: var(--wily-space-2);
  padding-block-start: var(--wily-space-2);
  padding-inline-start: var(--wily-space-3);
  border-inline-start: 3px solid var(--wily-border);
}

.app-reviews__reply-title {
  margin: 0;
  color: var(--wily-text);
  font-size: var(--wily-text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.app-reviews__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-3);
  align-items: center;
  margin-block-start: var(--wily-space-5);
}

.app-reviews__mine {
  margin: 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
}

/* Signalement : volontairement discret (texte rouge, pas de bouton plein)
   pour ne jamais concurrencer le bouton "Laisser un avis". */
.app-reviews__report {
  margin: var(--wily-space-5) 0 0;
  padding-block-start: var(--wily-space-4);
  border-block-start: 1px solid var(--wily-border);
}

.app-reviews__report-link {
  padding: 0;
  color: var(--wily-danger);
  font: inherit;
  font-size: var(--wily-text-sm);
  text-align: start;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  background: none;
  border: 0;
  cursor: pointer;
}

.app-reviews__report-link:hover {
  text-decoration-thickness: 2px;
}

.app-reviews__report-link:focus-visible {
  outline: 2px solid var(--wily-focus);
  outline-offset: 2px;
}

@media (min-width: 40rem) {
  .app-reviews__summary {
    grid-template-columns: 12rem 1fr;
    align-items: center;
  }
}

/* --------------------------------------------------------------------------
   25. Boites de dialogue de la fiche (.wily-dialog)
   Deux <dialog> natives (avis + signalement) pilotent leur propre fermeture
   via wily-dialog-open.js ; le fond est cliquable et Echap fonctionne seul.
   -------------------------------------------------------------------------- */
.wily-dialog {
  width: min(32rem, calc(100vw - 2 * var(--wily-space-4)));
  max-height: calc(100dvh - 2 * var(--wily-space-4));
  padding: 0;
  color: var(--wily-text);
  background-color: transparent;
  border: 0;
}

.wily-dialog::backdrop {
  background: var(--wily-overlay);
}

.wily-dialog__panel {
  display: grid;
  gap: var(--wily-space-4);
  padding: var(--wily-space-5);
  overflow-y: auto;
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border);
  border-block-start: 4px solid var(--wily-red);
  border-radius: var(--wily-radius-lg);
  box-shadow: var(--wily-shadow-lg);
  animation: wily-dialog-in 180ms ease-out;
}

@keyframes wily-dialog-in {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .wily-dialog__panel {
    animation: none;
  }
}

.wily-dialog__title {
  margin: 0;
  font-size: var(--wily-text-lg);
  font-weight: 800;
  line-height: var(--wily-leading-tight);
}

.wily-dialog__intro,
.wily-dialog__note {
  margin: 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
}

.wily-dialog__field {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.wily-dialog__label {
  display: block;
  margin-block-end: var(--wily-space-2);
  padding: 0;
  color: var(--wily-text);
  font-size: var(--wily-text-sm);
  font-weight: 700;
}

.wily-dialog__input {
  display: block;
  width: 100%;
  padding: var(--wily-space-3);
  color: var(--wily-text);
  font: inherit;
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border-strong);
  border-radius: var(--wily-radius-sm);
  resize: vertical;
}

.wily-dialog__input:focus-visible {
  outline: 2px solid var(--wily-focus);
  outline-offset: 1px;
}

/* Le balisage emet les cases de 5 a 1 (la premiere case au clavier est la note
   la plus haute), mais l'oeil doit lire 1 -> 5 de gauche a droite. `row-reverse`
   renverse l'axe principal : la CINQUIEME etoile visible porte donc bien la
   valeur 5 — cliquer dessus soumet 5, pas 1. `flex-end` recale le groupe a
   gauche, puisque l'axe est inverse. */
.wily-dialog__rating {
  display: flex;
  flex-direction: row-reverse;
  justify-content: flex-end;
  gap: var(--wily-space-2);
  align-items: center;
}

.wily-dialog__star {
  display: inline-flex;
  cursor: pointer;
}

.wily-dialog__star-mark {
  display: block;
  width: 2rem;
  height: 2rem;
  background-color: var(--wily-border-strong);
  clip-path: polygon(50% 0%,
      61% 35%,
      98% 35%,
      68% 57%,
      79% 91%,
      50% 70%,
      21% 91%,
      32% 57%,
      2% 35%,
      39% 35%);
  transition: background-color var(--wily-transition);
}

.wily-dialog__star input:checked+.wily-dialog__star-mark {
  background-color: var(--wily-red);
}

.wily-dialog__star:hover .wily-dialog__star-mark {
  background-color: var(--wily-red);
}

.wily-dialog__star input:focus-visible+.wily-dialog__star-mark {
  filter: drop-shadow(0 0 0.3rem var(--wily-focus));
}

/* Remplissage cumulatif. Les regles ci-dessus n'allument qu'une etoile (repli
   pour les navigateurs sans :has()). Avec :has(), la note choisie allume la
   sienne ET toutes les suivantes dans le DOM — or le DOM va de 5 a 1 : les
   suivantes sont donc les plus petites, exactement celles qu'il faut allumer.
   Le survol prime sur l'enregistrement : on apercoit la note visee avant de
   cliquer, et le groupe repart du gris des que la souris quitte les etoiles. */
@supports selector(:has(*)) {

  .wily-dialog__rating:not(:hover) .wily-dialog__star:has(input:checked) .wily-dialog__star-mark,
  .wily-dialog__rating:not(:hover) .wily-dialog__star:has(input:checked)~.wily-dialog__star .wily-dialog__star-mark {
    background-color: var(--wily-red);
  }

  .wily-dialog__rating:hover .wily-dialog__star .wily-dialog__star-mark {
    background-color: var(--wily-border-strong);
  }

  .wily-dialog__rating:hover .wily-dialog__star:hover .wily-dialog__star-mark,
  .wily-dialog__rating:hover .wily-dialog__star:hover~.wily-dialog__star .wily-dialog__star-mark {
    background-color: var(--wily-red);
  }
}

/* Precision sous un champ : explique une regle sans alourdir le libelle. */
.wily-dialog__hint {
  display: block;
  margin-block-start: var(--wily-space-1);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs, 0.75rem);
  line-height: var(--wily-leading-normal);
}

/* Liste des langues dans le dialogue de la fiche : autant de colonnes que la
   largeur le permet, une seule ligne par langue, la principale signalee par
   une pastille. Le panneau fournit la seule bordure (QUALITE #12). */
.wily-dialog__langs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: var(--wily-space-1) var(--wily-space-4);
  max-height: min(60vh, 26rem);
  margin: var(--wily-space-4) 0 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

.wily-dialog__lang {
  display: flex;
  gap: var(--wily-space-2);
  align-items: center;
  justify-content: space-between;
  padding-block: var(--wily-space-1);
  border-block-end: 1px solid var(--wily-border);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
}

.wily-dialog__lang-name {
  min-width: 0;
  overflow-wrap: anywhere;
}

.wily-dialog__lang-tag {
  flex: 0 0 auto;
  padding: 0 var(--wily-space-2);
  border-radius: var(--wily-radius-sm);
  background: var(--wily-surface-2);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
  white-space: nowrap;
}

.wily-dialog__choice {
  display: flex;
  gap: var(--wily-space-3);
  align-items: flex-start;
  padding: var(--wily-space-3);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-sm);
  cursor: pointer;
}

.wily-dialog__choice+.wily-dialog__choice {
  margin-block-start: var(--wily-space-2);
}

.wily-dialog__choice:focus-within {
  outline: 2px solid var(--wily-focus);
  outline-offset: 1px;
}

.wily-dialog__choice input {
  flex: 0 0 auto;
  margin-block-start: 0.15rem;
}

.wily-dialog__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--wily-space-3);
  align-items: center;
  margin-block-start: var(--wily-space-2);
}

@media (max-width: 30rem) {
  .wily-dialog__panel {
    gap: var(--wily-space-3);
    padding: var(--wily-space-4);
  }

  .wily-dialog__actions .btn {
    flex: 1 1 auto;
    justify-content: center;
  }
}

/* --------------------------------------------------------------------------
   26. Sélecteur de pays autorisés (.territory-picker)
   Le bouton se lit comme un champ : cette classe remplace le champ libre de
   secours, qui reste dans le DOM pour un navigateur sans JavaScript.
   Aucun ornement : une bordure, un chevron dessiné en CSS, rien de plus.
   -------------------------------------------------------------------------- */
.territory-picker {
  display: grid;
  gap: var(--wily-space-2);
  min-width: 0;
}

.territory-picker__trigger {
  display: flex;
  gap: var(--wily-space-3);
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: 44px;
  padding: var(--wily-space-3);
  color: var(--wily-text);
  font: inherit;
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
  text-align: start;
  background-color: var(--wily-surface);
  border: 1px solid var(--wily-border-strong);
  border-radius: var(--wily-radius-sm);
  cursor: pointer;
}

/* Chevron purement décoratif : aucune phrase, donc aucune traduction. */
.territory-picker__trigger::after {
  flex: 0 0 auto;
  width: 0.45rem;
  height: 0.45rem;
  content: "";
  border-inline-end: 2px solid currentcolor;
  border-block-end: 2px solid currentcolor;
  transform: rotate(45deg);
}

.territory-picker__trigger:hover {
  border-color: var(--wily-text-muted);
}

.territory-picker__trigger:focus-visible {
  outline: 2px solid var(--wily-focus);
  outline-offset: 1px;
}

.territory-picker__trigger[aria-invalid="true"] {
  border-color: var(--wily-red);
}

/* Plus large que les modales de lecture : huit pays par ligne, sans zoom. */
.territory-picker__dialog {
  width: min(46rem, calc(100vw - 2 * var(--wily-space-4)));
}

/* Deux colonnes dès qu'il y a la place : 57 pays tiennent alors en un écran. */
.territory-picker__groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
  gap: var(--wily-space-4) var(--wily-space-5);
  align-items: start;
}

.territory-picker__group {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.territory-picker__legend {
  width: 100%;
  padding: 0;
  margin-block-end: var(--wily-space-2);
}

/* Le libellé du groupe EST la commande de groupe : cocher ici coche les pays. */
.territory-picker__group-toggle {
  display: flex;
  gap: var(--wily-space-2);
  align-items: center;
  min-height: 44px;
  padding: 0 var(--wily-space-1);
  color: var(--wily-text);
  font-size: var(--wily-text-sm);
  font-weight: 700;
  cursor: pointer;
}

.territory-picker__group-toggle:focus-within {
  outline: 2px solid var(--wily-focus);
  outline-offset: 1px;
  border-radius: var(--wily-radius-sm);
}

.territory-picker__codes,
.locale-picker__codes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: var(--wily-space-1) var(--wily-space-2);
}

.territory-picker__code,
.locale-picker__code {
  display: flex;
  gap: var(--wily-space-2);
  align-items: center;
  min-height: 44px;
  padding: 0 var(--wily-space-1);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
  cursor: pointer;
}

.territory-picker__code:focus-within,
.locale-picker__code:focus-within {
  outline: 2px solid var(--wily-focus);
  outline-offset: 1px;
  border-radius: var(--wily-radius-sm);
}

@media (max-width: 30rem) {
  .territory-picker__groups {
    grid-template-columns: 1fr;
    gap: var(--wily-space-3);
  }

  .territory-picker__codes,
  .locale-picker__codes {
    grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  }
}

/* --------------------------------------------------------------------------
   20. Promotion de publication — accueil de la vitrine
   --------------------------------------------------------------------------
   Modale centrale révélée par `bindPromoModal` (wily-ui.js) après le délai
   porté par data-wily-promo-delay. Sans script, l'attribut [hidden] posé par
   le serveur la garde fermée : aucune couche fantôme, aucun focus déplacé.
   Compacité (F-45, F-46) : une seule bordure, une seule ombre, aucune
   décoration ; les cibles tactiles font au moins 44 px et chaque action
   destructrice ou ambiguë porte son libellé traduit. Le verrou de défilement
   est celui des autres modales (html.wily-modal-open, §3) : il n'est pas
   redéfini ici. Sur téléphone, la carte remonte en bas d'écran pour rester
   dans le pouce, sans changer le balisage.
   -------------------------------------------------------------------------- */
.wily-promo {
  position: fixed;
  inset: 0;
  z-index: var(--wily-z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--wily-space-4);
  overflow-y: auto;
}

.wily-promo[hidden] {
  display: none;
}

.wily-promo__scrim {
  position: fixed;
  inset: 0;
  background: var(--wily-overlay);
}

.wily-promo__card {
  position: relative;
  width: 100%;
  max-width: 26rem;
  padding: var(--wily-space-5);
  border-radius: var(--wily-radius-lg);
  background: var(--wily-surface);
  color: var(--wily-text);
  box-shadow: var(--wily-shadow-lg);
  animation: wily-promo-in 220ms ease-out;
}

@keyframes wily-promo-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.wily-promo__close {
  position: absolute;
  top: var(--wily-space-2);
  right: var(--wily-space-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--wily-radius-pill);
  background: transparent;
  color: var(--wily-text-muted);
  cursor: pointer;
  transition: background-color var(--wily-transition), color var(--wily-transition);
}

.wily-promo__close:hover,
.wily-promo__close:focus-visible {
  background: var(--wily-surface-2);
  color: var(--wily-text);
}

.wily-promo__close:focus-visible,
.wily-promo__later:focus-visible {
  outline: 2px solid var(--wily-focus);
  outline-offset: 2px;
}

.wily-promo__badge {
  display: inline-block;
  margin: 0 0 var(--wily-space-3);
  padding: var(--wily-space-1) var(--wily-space-2);
  border-radius: var(--wily-radius-pill);
  background: var(--wily-red-soft);
  color: var(--wily-danger);
  font-size: var(--wily-text-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.wily-promo__title {
  margin: 0 0 var(--wily-space-2);
  font-size: var(--wily-text-xl);
  line-height: var(--wily-leading-tight);
  letter-spacing: -0.01em;
}

.wily-promo__text {
  margin: 0 0 var(--wily-space-4);
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
}

.wily-promo__perks {
  display: grid;
  gap: var(--wily-space-2);
  margin: 0 0 var(--wily-space-5);
  padding: 0;
  list-style: none;
}

.wily-promo__perk {
  position: relative;
  padding-left: var(--wily-space-5);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
}

.wily-promo__perk::before {
  content: "";
  position: absolute;
  top: 0.45rem;
  left: var(--wily-space-1);
  width: 0.5rem;
  height: 0.3rem;
  border-bottom: 2px solid var(--wily-success);
  border-left: 2px solid var(--wily-success);
  transform: rotate(-45deg);
}

.wily-promo__actions {
  display: grid;
  gap: var(--wily-space-2);
  margin-bottom: var(--wily-space-2);
}

.wily-promo__later {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 0 var(--wily-space-3);
  border: 0;
  border-radius: var(--wily-radius);
  background: transparent;
  color: var(--wily-text-muted);
  font: inherit;
  font-size: var(--wily-text-sm);
  cursor: pointer;
  transition: color var(--wily-transition);
}

.wily-promo__later:hover {
  color: var(--wily-text);
}

@media (max-width: 30rem) {
  .wily-promo {
    align-items: flex-end;
    padding: var(--wily-space-2);
  }

  .wily-promo__card {
    padding: var(--wily-space-4);
  }
}

@media (prefers-reduced-motion: reduce) {
  .wily-promo__card {
    animation: none;
  }
}

/* --------------------------------------------------------------------------
   Bandeau d'installation de l'application mobile
   (templates/partials/app-install-banner.php, inclus par layout.php)

   Dernière chose lue avant le pied de page : une seule carte, une seule
   commande, une seule bordure. Aucun pictogramme isolé — le bouton porte son
   libellé traduit (QUALITÉ #12). Le partiel se tait seul sur /telecharger et
   sur les fiches /app/* : ici, rien n'est conditionnel.
   -------------------------------------------------------------------------- */
.app-banner {
  padding-block: var(--wily-space-6) 0;
}

.app-banner__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--wily-space-4);
  padding: var(--wily-space-5);
  border: 1px solid var(--wily-border);
  border-radius: var(--wily-radius-lg);
  background-color: var(--wily-surface-2);
}

.app-banner__copy {
  min-width: 15rem;
  flex: 1 1 22rem;
}

/* Pastille de canal : un point plein, décoratif, jamais un texte. */
.app-banner__badge {
  display: inline-flex;
  align-items: center;
  gap: var(--wily-space-2);
  margin: 0 0 var(--wily-space-2);
  font-size: var(--wily-text-xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--wily-text-muted);
}

.app-banner__dot {
  width: 0.5rem;
  height: 0.5rem;
  flex: none;
  border-radius: 50%;
  background-color: var(--wily-success);
}

.app-banner__title {
  margin: 0 0 var(--wily-space-1);
  font-size: var(--wily-text-lg);
  line-height: var(--wily-leading-tight);
  letter-spacing: -0.01em;
}

.app-banner__text {
  margin: 0;
  max-width: 46rem;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-sm);
  line-height: var(--wily-leading-normal);
}

.app-banner__side {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--wily-space-1);
}

.app-banner__cta {
  min-height: 44px;
  padding-inline: var(--wily-space-5);
}

.app-banner__meta {
  margin: 0;
  color: var(--wily-text-muted);
  font-size: var(--wily-text-xs);
}

@media (min-width: 48rem) {
  .app-banner__side {
    align-items: flex-end;
    text-align: right;
  }
}