/*
 * Kours Admin — thème visuel
 * Inspiré de Material Design 3 : épuré, coins arrondis, une seule couleur
 * d'accent, mode clair / sombre (auto via prefers-color-scheme + bouton de
 * bascule manuelle mémorisé en localStorage).
 */

:root {
  /* Couleur d'accent : bleu #0B5DA9 — c'est **exactement** le bleu foncé du logo
     Kours, pas une approximation (décision du 02/09/2026 : le logo décide, le
     vert #207F10 n'était qu'un test). Les tons associés sont dérivés autour de
     cette teinte, puis vérifiés au calcul de contraste WCAG : tous les couples
     texte/fond utilisés atteignent AA, le plus serré étant le lien sur carte
     (5,88:1).
     Pour changer d'accent : ces quatre variables, PLUS leurs équivalents dans
     les deux blocs sombres ci-dessous. Les couleurs du Nutri-Score, elles, ne
     suivent jamais l'accent — ce sont celles de l'échelle officielle. */
  --md-primary: #0B5DA9;
  --md-on-primary: #FFFFFF;
  --md-primary-container: #D3E4FF;
  --md-on-primary-container: #002F5D;

  --md-warning-container: #FFE7A3;
  --md-on-warning-container: #4A3600;

  --md-surface: #FBFAFF;
  --md-surface-container: #F1F0F7;
  --md-surface-container-high: #E7E6EE;
  --md-on-surface: #1B1B1F;
  --md-on-surface-variant: #45464F;
  --md-outline: #C9C7D0;

  --md-error: #BA1A1A;
  --md-error-container: #FFDAD6;
  --md-on-error-container: #410002;

  --md-radius-sm: 8px;
  --md-radius-md: 16px;
  --md-radius-lg: 28px;
  --md-elevation-1: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.08);
  --md-elevation-2: 0 3px 6px rgba(0, 0, 0, 0.14), 0 1px 3px rgba(0, 0, 0, 0.10);

  /* Logo Kours. La variante est choisie ICI, avec la palette, et non dans le
     `src` d'une balise <img> : le thème bascule à l'exécution (bouton + valeur
     mémorisée, et `prefers-color-scheme` quand rien n'est mémorisé), donc une
     URL écrite en PHP serait figée au rendu de la page et le logo resterait
     celui du thème initial. Contrastes mesurés le 02/09/2026 (WCAG) :
       - logo couleur sur --md-surface-container clair (#F1F0F7) : 5,80 ✔
       - logo couleur sur --md-surface-container sombre (#1E1E24) : 2,52 ✘
       - logo blanc  sur --md-surface-container sombre (#1E1E24) : 16,58 ✔ */
  --kours-logo-32: url("../img/logo-64.png");
  --kours-logo-64: url("../img/logo-96.png");

  /* Reflet de la barre d'activité d'un import. Jeton dédié plutôt que
     `color-mix()` sur `--md-primary` : la fonction n'est pas disponible partout,
     et une couleur définie dans un seul bloc de thème serait invisible dans
     l'autre. */
  --md-primary-sweep: rgba(11, 93, 169, 0.30);
}

:root[data-theme="dark"] {
  --kours-logo-32: url("../img/logo-64-mono.png");
  --kours-logo-64: url("../img/logo-96-mono.png");
  --md-primary-sweep: rgba(163, 201, 248, 0.26);

  --md-primary: #A3C9F8;
  --md-on-primary: #002F5D;
  --md-primary-container: #22486F;
  --md-on-primary-container: #D3E4FF;

  --md-warning-container: #5C4300;
  --md-on-warning-container: #FFE7A3;

  --md-surface: #131318;
  --md-surface-container: #1E1E24;
  --md-surface-container-high: #29292F;
  --md-on-surface: #E4E2E9;
  --md-on-surface-variant: #C7C5D0;
  --md-outline: #46464F;

  --md-error: #FFB4AB;
  --md-error-container: #93000A;
  --md-on-error-container: #FFDAD6;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --kours-logo-32: url("../img/logo-64-mono.png");
    --kours-logo-64: url("../img/logo-96-mono.png");
    --md-primary-sweep: rgba(163, 201, 248, 0.26);

    --md-primary: #A3C9F8;
    --md-on-primary: #002F5D;
    --md-primary-container: #22486F;
    --md-on-primary-container: #D3E4FF;

    --md-warning-container: #5C4300;
    --md-on-warning-container: #FFE7A3;

    --md-surface: #131318;
    --md-surface-container: #1E1E24;
    --md-surface-container-high: #29292F;
    --md-on-surface: #E4E2E9;
    --md-on-surface-variant: #C7C5D0;
    --md-outline: #46464F;

    --md-error: #FFB4AB;
    --md-error-container: #93000A;
    --md-on-error-container: #FFDAD6;
  }
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: "Roboto", "Segoe UI", system-ui, -apple-system, sans-serif;
  background: var(--md-surface);
  color: var(--md-on-surface);
  transition: background-color .2s ease, color .2s ease;
}

a {
  color: var(--md-primary);
}

.material-symbols-rounded {
  font-variation-settings: "OPSZ" 24, "GRAD" 0, "FILL" 0, "wght" 400;
  vertical-align: middle;
  line-height: 1;
}

/* ---------- Bouton de bascule de thème (coin haut-droit, sur toutes les pages) ---------- */
.theme-toggle {
  position: fixed;
  top: 16px;
  right: 16px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.theme-toggle:hover {
  background: var(--md-surface-container);
}

/* ---------- Écran de connexion ---------- */
.login-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.login-card {
  width: 100%;
  max-width: 380px;
  background: var(--md-surface-container);
  border-radius: var(--md-radius-lg);
  padding: 40px 32px;
  box-shadow: var(--md-elevation-1);
  text-align: center;
}

/* Logo Kours (fourni le 02/09/2026). Le disque coloré et la lettre « K » qui
   tenaient lieu de logo ont disparu : l'image a son propre fond transparent, et
   la poser sur un disque de couleur d'accent créerait un heurt de teintes.
   Élément vide plutôt que <img> : la variante claire/sombre vient de
   `--kours-logo-64`, donc du thème effectif, cf. le bloc :root plus haut. Le
   logo est décoratif (le nom de l'application est écrit juste dessous), il est
   donc masqué aux lecteurs d'écran. */
.login-logo {
  display: block;
  width: 64px;
  height: 64px;
  margin: 0 auto 16px;
  background-image: var(--kours-logo-64);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}

.login-card h1 {
  font-size: 20px;
  font-weight: 500;
  margin: 0 0 4px;
}

.login-card p.subtitle {
  margin: 0 0 24px;
  color: var(--md-on-surface-variant);
  font-size: 14px;
}

.field {
  position: relative;
  margin-bottom: 20px;
  text-align: left;
}

/* Sélecteur d'enfant direct (>) volontaire : ne doit cibler QUE l'icône de
   tête (person/lock), pas l'icône œil qui est imbriquée dans le bouton
   .toggle-visibility et se positionne toute seule via son display:flex. */
.field > .material-symbols-rounded {
  position: absolute;
  left: 14px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--md-on-surface-variant);
  font-size: 20px;
}

.field input {
  width: 100%;
  padding: 14px 44px;
  border-radius: var(--md-radius-sm);
  border: 1px solid var(--md-outline);
  background: var(--md-surface);
  color: var(--md-on-surface);
  font-size: 15px;
}

.field input:focus {
  outline: 2px solid var(--md-primary);
  outline-offset: -1px;
}

.field .toggle-visibility {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  cursor: pointer;
  padding: 6px;
  display: flex;
  color: var(--md-on-surface-variant);
}

.btn-primary {
  width: 100%;
  padding: 14px;
  border-radius: var(--md-radius-sm);
  border: none;
  background: var(--md-primary);
  color: var(--md-on-primary);
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}
.btn-primary:hover {
  filter: brightness(1.05);
}

/* Un bouton désactivé qui porte un fond explicite n'est PAS grisé par le
   navigateur : il garde son bleu franc et invite au clic. Constaté sur capture
   le 03/09/2026, en écho à la remarque du client — « on peut continuer à cliquer
   partout ». Il faut donc le dire soi-même, et couper le survol. */
.btn-primary:disabled,
.btn-secondary:disabled {
  opacity: 0.55;
  cursor: not-allowed;
  filter: none;
}

.btn-primary:disabled:hover,
.btn-secondary:disabled:hover {
  filter: none;
}

.alert {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border-radius: var(--md-radius-sm);
  background: var(--md-error-container);
  color: var(--md-on-error-container);
  font-size: 14px;
  margin-bottom: 20px;
  text-align: left;
}

.alert.success {
  background: var(--md-primary-container);
  color: var(--md-on-primary-container);
}

.alert.warning {
  background: var(--md-warning-container);
  color: var(--md-on-warning-container);
}

.alert.warning a {
  color: inherit;
  font-weight: 500;
}

/* Information neutre — ni erreur, ni avertissement, ni réussite. Sert à la
   proposition de reprise de sélection de colonnes : c'est un constat, pas un
   problème, et le rouge de `.alert` comme l'ambre de `.warning` diraient tous
   deux le contraire. */
.alert.info {
  background: var(--md-surface-container-high);
  color: var(--md-on-surface);
}

/* ---------- Signal d'activité d'un lot d'imports ---------- */
/* Demande du client du 03/09/2026 : « on n'a pas vraiment de truc qui montre
   que ça tourne ». Le mouvement ne s'affiche que si une base travaille
   RÉELLEMENT — sinon il mentirait, ce qui est pire que pas de signal. */
.fill-bar.is-working {
  position: relative;
  overflow: hidden;
}

.fill-bar.is-working::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    115deg,
    transparent 0 35%,
    var(--md-primary-sweep) 45% 55%,
    transparent 65% 100%
  );
  background-size: 220% 100%;
  animation: fill-bar-sweep 1.6s linear infinite;
}

@keyframes fill-bar-sweep {
  from { background-position: 120% 0; }
  to   { background-position: -120% 0; }
}

/* Un utilisateur qui a demandé moins d'animation garde le signal, sous une
   forme fixe : le compteur de secondes suffit à prouver l'activité. */
@media (prefers-reduced-motion: reduce) {
  .fill-bar.is-working::after {
    animation: none;
    background-image: none;
  }
}

.heartbeat {
  font-family: "Roboto Mono", ui-monospace, monospace;
  font-size: 12px;
  color: var(--md-on-surface-variant);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Au-delà d'une minute et demie de silence, le compteur cesse d'être rassurant
   et devient l'information importante. */
.heartbeat.is-late {
  color: var(--md-error);
  font-weight: 500;
}

/* ---------- Proposition de reprise d'une sélection de colonnes ---------- */
/* `align-items: flex-start` remplace le `center` de `.alert` : ce bloc contient
   plusieurs paragraphes et un bouton, l'icône doit rester en haut. */
.alert.proposal {
  align-items: flex-start;
}

.alert.proposal > span:last-child {
  min-width: 0;
  flex: 1 1 auto;
}

.proposal-detail {
  margin-top: 8px;
}

.proposal-detail > summary {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-weight: 500;
  list-style: none;
}

.proposal-detail > summary::-webkit-details-marker,
.proposal-detail > summary::marker {
  display: none;
}

/* Le chevron partagé porte `margin-left: auto` pour la barre des sources ; ici
   il suit immédiatement le libellé. */
.proposal-detail > summary .summary-chevron {
  margin-left: 0;
  color: inherit;
  font-size: 20px;
}

.proposal-detail[open] > summary .summary-chevron {
  transform: rotate(180deg);
}

.proposal-detail > summary:focus-visible {
  outline: 2px solid var(--md-primary);
  outline-offset: 2px;
  border-radius: var(--md-radius-sm);
}

.proposal-list,
.proposal-absent {
  margin: 8px 0 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 6px;
  line-height: 1.7;
}

.proposal-list code,
.proposal-absent code {
  background: var(--md-surface);
  border: 1px solid var(--md-outline);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 12.5px;
  overflow-wrap: anywhere;
}

.proposal-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
}

/* `.btn-primary` est en `width: 100%` — dimension pensée pour le bouton unique
   d'un formulaire de connexion. Dans une bannière, un bouton pleine largeur
   ressemble à une barre de validation de page et écrase le reste du bloc. Ici il
   se dimensionne sur son texte. */
/* ⚠️ `flex: 0 1 auto` et non `0 0 auto` : mesuré le 03/09/2026, un bouton qui ne
   peut pas se réduire fait déborder la page à 400 px de large. `min-width: 0`
   est nécessaire pour qu'un élément flex descende sous la largeur de son
   contenu, et `flex-wrap` laisse le libellé passer sous l'icône plutôt que de
   pousser le bord. */
.proposal-actions .btn-primary {
  width: auto;
  max-width: 100%;
  min-width: 0;
  flex: 0 1 auto;
  flex-wrap: wrap;
  padding: 11px 18px;
  font-size: 14px;
}

.copy-hint {
  font-size: 12.5px;
  color: var(--md-on-surface-variant);
  flex: 1 1 220px;
  min-width: 0;
}

.copy-selection label {
  font-size: 13px;
  color: var(--md-on-surface-variant);
}

/* ---------- App bar + contenu (dashboard, install) ---------- */
/* Le padding droit (72px) réserve la place du bouton de bascule de thème, qui
   est en position: fixed à 16px du bord droit (40px de large) : sans cette
   marge, il se superposait au dernier bouton de la barre (déconnexion). */
.app-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding: 16px 72px 16px 24px;
  background: var(--md-surface-container);
  box-shadow: var(--md-elevation-1);
}

/* Sur petit écran la barre peut passer sur deux lignes plutôt que de déborder :
   le bouton de bascule de thème reste en haut à droite, donc on ne réserve la
   marge que sur la première ligne. */
@media (max-width: 700px) {
  .app-bar {
    padding: 12px 20px;
  }
  .app-bar .title {
    padding-right: 56px;
  }
  /* Sur mobile la place va à la navigation : l'identifiant et le niveau de
     droits sont de toute façon rappelés sur le tableau de bord et dans
     « Mon compte ». */
  .app-bar-user,
  .app-bar .role-pill {
    display: none;
  }
  .icon-btn {
    width: 36px;
    height: 36px;
  }
}

/* Le titre doit pouvoir se comprimer, sinon c'est lui qui pousse la
   navigation hors de l'écran. */
.app-bar .title {
  min-width: 0;
  /* Peut rétrécir (le libellé de page s'ellipse), mais ne s'étire pas : sinon
     il pousserait la navigation contre le bord. */
  flex: 0 1 auto;
}

.app-bar .title {
  font-size: 18px;
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 12px;
  /* C'est un <a> (clic sur le logo → tableau de bord, demande client du
     15/09/2026) : sans ça, la règle générale `a { color: var(--md-primary) }`
     teinterait le nom de l'application en bleu et lui poserait un
     soulignement au survol, ce que ce titre n'a jamais eu. */
  color: inherit;
  text-decoration: none;
}

.app-bar .badge {
  /* `display: block` est indispensable : ce n'est plus une <img> mais un
     <span>, et un élément en ligne ignore `width` / `height` (mesuré au test
     du 02/09/2026 : boîte 0 × 19 px, badge invisible). */
  display: block;
  width: 32px;
  height: 32px;
  flex-shrink: 0;
  background-image: var(--kours-logo-32);
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
}

/* Libellé de l'onglet courant, à droite du nom de l'application (demande client
   du 23/08/2026 : « indiquer dans quel onglet on se trouve »). Séparé par un
   filet vertical plutôt que par un tiret : plus lisible, et le nom de
   l'application reste l'élément dominant. */
.app-bar .page-label {
  font-size: 16px;
  font-weight: 400;
  color: var(--md-on-surface-variant);
  padding-left: 12px;
  border-left: 1px solid var(--md-outline);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 700px) {
  .app-bar .page-label {
    font-size: 14px;
  }
}

.app-bar-user {
  font-size: 14px;
  color: var(--md-on-surface-variant);
  white-space: nowrap;
}

.app-bar .actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  /* La barre porte maintenant la navigation complète sur tous les écrans : sans
     ce retour à la ligne, elle débordait à 480 px (mesuré). */
  flex-wrap: wrap;
  row-gap: 4px;
  min-width: 0;
}

.icon-btn {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--md-on-surface);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  text-decoration: none;
}
.icon-btn:hover {
  background: var(--md-surface-container-high);
}

/* Onglet courant : le libellé de page dit où l'on est, l'icône allumée le
   confirme dans la barre de navigation. */
.icon-btn.active {
  background: var(--md-primary-container);
  color: var(--md-on-primary-container);
}

/* Règle de mise en page (demande client du 23/08/2026) : exploiter toute la
   largeur disponible avec une marge raisonnable, plutôt que de laisser des
   « pavés isolés au milieu ». La page occupe donc la largeur de la fenêtre
   jusqu'à une limite haute confortable, et ce sont les GRILLES ci-dessous
   (.card-grid / .layout-split) qui répartissent le contenu — pas des largeurs
   maximales posées au cas par cas sur chaque bloc. */
.page-content {
  max-width: 1760px;
  margin: 0 auto;
  padding: 32px 40px;
}

/* Écrans de données (visualiseur produits, journaux) : pas de plafond du tout,
   la fenêtre entière sert, marges comprises. Demande client du 23/08/2026 :
   « l'affichage n'utilise pas la largeur de la page ». Réservé aux pages faites
   de tableaux — un texte suivi sur 2500 px serait illisible. */
.page-content.full-width {
  max-width: none;
}

@media (max-width: 700px) {
  .page-content {
    padding: 24px 20px;
  }
}

/* Cartes d'information réparties automatiquement : autant de colonnes que la
   largeur le permet, jamais moins de 340px par carte, et une seule colonne sur
   mobile. Aucun point de rupture à maintenir à la main. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  gap: 20px;
  align-items: start;
}

/* Indispensable : par défaut une cellule de grille refuse de descendre sous la
   largeur mini de son contenu, donc un tableau large la ferait déborder au lieu
   de défiler dans son .table-wrap. min-width: 0 lève ce plancher. */
.card-grid > *,
.layout-split > * {
  min-width: 0;
}

/* Contenu principal large (tableau) + colonne latérale (formulaire, actions).
   Repasse en une seule colonne quand la place manque. */
.layout-split {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr);
  gap: 20px;
  align-items: start;
}

/* Variante pour les écrans où la colonne latérale n'est qu'un panneau de
   filtres : à 2fr/1fr elle prenait près de 500 px sur grand écran, au détriment
   du tableau (« les filtres à droite prennent trop de place », 23/08/2026). On
   lui donne une largeur fixe et tout le reste va au contenu. */
.layout-split.side-fixed {
  grid-template-columns: minmax(0, 1fr) 300px;
}

@media (max-width: 1000px) {
  .layout-split,
  .layout-split.side-fixed {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Un formulaire reste lisible : on limite la largeur du FORMULAIRE, pas celle
   de la carte qui le contient — la carte, elle, remplit sa colonne. C'est ce qui
   évite l'effet « petit bloc perdu au centre ». */
.form-narrow {
  max-width: 520px;
}

.card {
  background: var(--md-surface-container);
  border-radius: var(--md-radius-md);
  padding: 24px;
  box-shadow: var(--md-elevation-1);
  margin-bottom: 20px;
}

.card h2 {
  margin: 0 0 12px;
  font-size: 16px;
  font-weight: 500;
  display: flex;
  align-items: center;
  gap: 8px;
  /* Le tableau de bord pose un compteur en fin de titre (`margin-left:auto`) :
     sans repli, un grand nombre déborderait sur un écran étroit plutôt que de
     passer à la ligne suivante. */
  flex-wrap: wrap;
}

.migration-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.migration-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--md-outline);
  font-size: 14px;
  /* La ligne passe à la ligne plutôt que de déborder : un nom de migration en
     police à largeur fixe plus la date d'application ne tiennent pas dans
     480 px. Défaut mesuré le 02/09/2026 sur install.php (8 px de débordement
     horizontal) — invisible à l'œil, trouvé par la mesure. */
  flex-wrap: wrap;
}
.migration-list li:last-child {
  border-bottom: none;
}

/* "Appliquée" reprend la couleur d'accent (bleu #0B5DA9 depuis la 1.12.0) :
   aucune couleur codée en dur qui jurerait avec la palette, et l'icône
   distingue déjà les trois états. */
.migration-list .status-applied { color: var(--md-primary); }
.migration-list .status-pending { color: var(--md-on-surface-variant); }
.migration-list .status-error { color: var(--md-error); }

.migration-list .filename {
  flex: 1 1 auto;
  font-family: "Roboto Mono", monospace;
  font-size: 13px;
  /* Sans `min-width: 0`, un élément flex refuse de descendre sous la largeur
     de son contenu : c'est ce qui poussait la ligne hors de l'écran. Même
     cause que pour les cellules de grille (voir .card-grid > *). */
  min-width: 0;
  overflow-wrap: anywhere;
}

.btn-secondary {
  padding: 10px 20px;
  border-radius: var(--md-radius-sm);
  border: 1px solid var(--md-outline);
  background: transparent;
  color: var(--md-primary);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
}
.btn-secondary:hover {
  background: var(--md-surface-container-high);
}

/* ---------- Bouton d'action destructrice (suppression) ---------- */
.btn-danger {
  padding: 10px 20px;
  border-radius: var(--md-radius-sm);
  border: 1px solid var(--md-error);
  background: transparent;
  color: var(--md-error);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  font-family: inherit;
}
.btn-danger:hover {
  background: var(--md-error-container);
  color: var(--md-on-error-container);
}

/* ---------- Tableau de données (liste des utilisateurs) ---------- */
/* Conteneur scrollable horizontalement : sur mobile, le tableau défile dans
   sa propre zone plutôt que de faire déborder toute la page. */
.table-wrap {
  overflow-x: auto;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}

.data-table th {
  text-align: left;
  font-weight: 500;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--md-on-surface-variant);
  padding: 8px 12px 8px 0;
  border-bottom: 1px solid var(--md-outline);
  white-space: nowrap;
}

.data-table td {
  padding: 12px 12px 12px 0;
  border-bottom: 1px solid var(--md-outline);
  vertical-align: middle;
}

.data-table tr:last-child td {
  border-bottom: none;
}

.table-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}

.table-actions .btn-secondary,
.table-actions .btn-danger {
  padding: 6px 12px;
  font-size: 13px;
}

/* ---------- Pastille de rôle (droits complets / lecture seule) ---------- */
.role-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
}

.role-pill.full {
  background: var(--md-primary-container);
  color: var(--md-on-primary-container);
}

.role-pill.readonly {
  background: var(--md-surface-container-high);
  color: var(--md-on-surface-variant);
}

.role-pill.pending {
  background: var(--md-warning-container);
  color: var(--md-on-warning-container);
}

/* ---------- Bloc "version installée" (update.php) ---------- */
.version-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.version-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 0;
  border-bottom: 1px solid var(--md-outline);
  font-size: 14px;
  flex-wrap: wrap;
}
.version-list li:last-child {
  border-bottom: none;
}

.version-list .material-symbols-rounded {
  font-size: 20px;
  color: var(--md-on-surface-variant);
}

.version-list .version-label {
  color: var(--md-on-surface-variant);
  min-width: 150px;
}

.version-list .version-value {
  font-weight: 500;
}

/* ---------- Barre de remplissage (profilage des colonnes) ---------- */
.fill-bar {
  display: inline-block;
  width: 90px;
  height: 8px;
  border-radius: 999px;
  background: var(--md-surface-container-high);
  overflow: hidden;
  vertical-align: middle;
  margin-right: 6px;
}

.fill-bar-value {
  display: block;
  height: 100%;
  background: var(--md-primary);
}

/* ---------- Barre de filtre + pagination (journal) ---------- */
.filter-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.filter-bar select {
  padding: 10px 12px;
  border-radius: var(--md-radius-sm);
  border: 1px solid var(--md-outline);
  background: var(--md-surface);
  color: var(--md-on-surface);
  font-size: 14px;
  font-family: inherit;
  max-width: 100%;
}

.filter-bar select:focus {
  outline: 2px solid var(--md-primary);
  outline-offset: -1px;
}

.pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-top: 20px;
  flex-wrap: wrap;
}

/* ---------- Version affichée en pied de la carte de connexion ---------- */
.version-footer {
  margin: 24px 0 0;
  font-size: 12px;
  color: var(--md-on-surface-variant);
}

/* ---------- Case à cocher avec libellé + explication ---------- */
.checkbox-field {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 20px;
  text-align: left;
}

.checkbox-field input[type="checkbox"] {
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  accent-color: var(--md-primary);
  flex-shrink: 0;
}

.checkbox-field label {
  font-size: 14px;
  line-height: 1.4;
  cursor: pointer;
}

.checkbox-field .hint {
  display: block;
  font-size: 12px;
  color: var(--md-on-surface-variant);
  margin-top: 2px;
}

/* ---------- Pastilles de filtre actif / bascule (visualiseur produits) ----- */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--md-outline);
  background: var(--md-surface);
  color: var(--md-on-surface);
  font-size: 13px;
  font-weight: 500;
  text-decoration: none;
  max-width: 100%;
  /* Une marque ou une catégorie peut être très longue : on tronque plutôt que
     de laisser la pastille déborder de la carte. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: background 0.15s, border-color 0.15s;
}

.chip:hover {
  background: var(--md-surface-container-high);
  border-color: var(--md-primary);
}

.chip.chip-on {
  background: var(--md-primary);
  border-color: var(--md-primary);
  color: var(--md-on-primary);
}

/* ---------- Détail d'une base, replié (data_sources.php) ------------------
   Avec quatre bases, garder tous les blocs dépliés donnait une page de
   plusieurs mètres où l'information utile — quelle base est à jour ? — était
   introuvable. Le tableau du haut répond à cette question, et chaque base
   garde son détail à un clic. <details> plutôt que du JavaScript : l'ouverture
   fonctionne sans script et reste accessible au clavier. */
.source-details > summary {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  font-size: 16px;
  font-weight: 500;
  /* Le marqueur par défaut disparaît dès que summary passe en flex : on fournit
     donc notre propre chevron (.summary-chevron). Sans lui, le bloc replié
     ressemblait à une carte inerte — rien n'indiquait qu'on pouvait l'ouvrir,
     ce qui est pire que pas de repli du tout. */
  list-style: none;
}

.source-details > summary::-webkit-details-marker,
.source-details > summary::marker {
  display: none;
}

.summary-chevron {
  margin-left: auto;
  color: var(--md-on-surface-variant);
  transition: transform 0.15s;
}

.source-details[open] > summary .summary-chevron {
  transform: rotate(180deg);
}

.source-details > summary:hover .source-summary-label,
.source-details > summary:hover .summary-chevron {
  color: var(--md-primary);
}

.source-details > summary:focus-visible {
  outline: 2px solid var(--md-primary);
  outline-offset: 4px;
  border-radius: var(--md-radius-sm);
}

.source-details[open] > summary {
  margin-bottom: 20px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--md-outline);
}

.source-summary-label {
  flex: 1 1 auto;
  min-width: 0;
}

/* ---------- Sélecteur de base consultée (products.php) -------------------
   Une base par type de produit : il faut pouvoir passer de l'une à l'autre
   sans repasser par l'écran d'administration. */
.base-picker {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.base-tab {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 16px;
  border-radius: var(--md-radius-sm);
  border: 1px solid var(--md-outline);
  background: var(--md-surface);
  color: var(--md-on-surface);
  text-decoration: none;
  transition: background 0.15s, border-color 0.15s;
}

.base-tab:hover {
  background: var(--md-surface-container-high);
  border-color: var(--md-primary);
}

.base-tab-on,
.base-tab-on:hover {
  background: var(--md-primary);
  border-color: var(--md-primary);
  color: var(--md-on-primary);
}

.base-tab-title {
  font-size: 14px;
  font-weight: 500;
}

.base-tab-note {
  font-size: 12px;
  color: var(--md-on-surface-variant);
}

/* Sur l'onglet actif, la note doit rester lisible sur le fond coloré : la
   couleur « variant » y tomberait à un contraste insuffisant. */
.base-tab-on .base-tab-note {
  color: var(--md-on-primary);
  opacity: 0.85;
}

/* ---------- Listes de valeurs de filtre (marques, catégories) ------------- */
.facet-list {
  list-style: none;
  margin: 0;
  padding: 0;
  /* Les listes sont tronquées côté SQL, mais 30 lignes suffisent à allonger la
     page : on borne la hauteur et on laisse défiler dans la carte. */
  max-height: 340px;
  overflow-y: auto;
}

.facet-list li + li {
  border-top: 1px solid var(--md-outline);
}

.facet-list a {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 4px;
  color: var(--md-on-surface);
  text-decoration: none;
  font-size: 13px;
  border-radius: var(--md-radius-sm);
}

.facet-list a:hover {
  background: var(--md-surface-container-high);
}

.facet-list a.selected {
  color: var(--md-primary);
  font-weight: 500;
}

.facet-value {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.facet-count {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--md-on-surface-variant);
  font-variant-numeric: tabular-nums;
}

/* ---------- Pastille Nutri-Score aux couleurs officielles ----------------- */
/* Les cinq couleurs sont celles du logo officiel : une pastille verte pour un
   E serait trompeuse. Le C, jaune, prend un texte foncé pour rester lisible. */
.ns {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  padding: 2px 7px;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  line-height: 1.5;
}

/* Fonds = couleurs officielles de l'échelle, intouchables. La couleur du TEXTE,
   elle, est choisie par le contraste mesuré et non par habitude : le blanc,
   appliqué partout au départ, tombait à 2,30:1 sur le B et 2,70:1 sur le D —
   très en dessous du seuil AA de 4,5:1. Corrigé le 02/09/2026.
   Rapports obtenus : A 4,98 · B 9,15 · C 13,76 · D 7,78 · E 5,06. */
.ns-a { background: #038141; color: #FFFFFF; }
.ns-b { background: #85BB2F; color: #000000; }
.ns-c { background: #FECB02; color: #000000; }
.ns-d { background: #EE8100; color: #000000; }
.ns-e { background: #E63E11; color: #000000; }

/* Version cliquable dans les filtres : contour tant que non sélectionné, pour
   qu'on distingue une case cochée d'une simple légende de couleurs. */
a.ns {
  text-decoration: none;
  opacity: 0.45;
  border: 2px solid transparent;
  min-width: 34px;
  padding: 5px 10px;
  font-size: 13px;
}

a.ns:hover {
  opacity: 0.8;
}

a.ns.ns-on {
  opacity: 1;
  border-color: var(--md-on-surface);
}

/* ---------- Cellules du visualiseur produits ------------------------------ */
/* Deux régimes seulement : les valeurs courtes (code, quantité, date, score)
   restent sur une ligne, le texte se replie. Auparavant toutes les cellules
   étaient tronquées à 280 px, ce qui gaspillait la largeur disponible. */
.cell-tight {
  font-size: 13px;
  white-space: nowrap;
}

.cell-text {
  font-size: 13px;
  max-width: 460px;
  /* Un nom de produit sans espace ne doit pas élargir la colonne indéfiniment. */
  overflow-wrap: anywhere;
}

/* Marqueur des colonnes recommandées dans le profilage : sans lui, rien ne
   distingue une colonne décorative d'une colonne dont dépendent les écrans de
   consultation. */
.reco-tag {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  font-family: "Roboto", sans-serif;
  font-size: 11px;
  font-weight: 500;
  background: var(--md-primary-container);
  color: var(--md-on-primary-container);
  vertical-align: middle;
  cursor: help;
}

/* Boutons de pagination : doivent pouvoir passer à la ligne sur mobile, sinon
   ils débordaient de la page (mesuré à 480 px). */
.pager-nav {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* La case à cocher d'une valeur de filtre ne doit jamais manger la place du
   libellé : si la police d'icônes ne se charge pas (hors ligne), le mot
   « check_box_outline_blank » s'affiche à la place du glyphe et écrasait
   entièrement le nom de la marque. Boîte de taille fixe, contenu rogné. */
.facet-list a > .material-symbols-rounded {
  flex: 0 0 18px;
  width: 18px;
  overflow: hidden;
}

/* --------------------------------------------------------------------------
   Enseignes de magasins (brands.php, brand_edit.php) — migration 015.
   -------------------------------------------------------------------------- */

/* Pastille de logo.

   ⚠️ Fond BLANC dans les deux thèmes, et ce n'est pas une inattention. Un logo
   d'enseigne est dessiné pour du papier et des vitrines : il est presque toujours
   sombre sur fond clair. Posé sur --md-surface-container-high (#29292F en thème
   sombre), un logo noir disparaîtrait purement et simplement — le même piège que
   le logo Kours couleur sur l'aplat d'accent, mesuré à 1,01:1 (contrat §4.11).
   La bordure délimite la pastille en thème clair, où le blanc se fondrait sinon
   dans la carte. */
.brand-logo,
.brand-logo-empty {
  width: 48px;
  height: 48px;
  border-radius: 8px;
  background: #FFFFFF;
  border: 1px solid var(--md-outline);
  flex: 0 0 auto;
}

.brand-logo {
  object-fit: contain;
  padding: 4px;
  box-sizing: border-box;
}

/* ⚠️ display: block obligatoire. Cet élément est un <span>, donc inline par
   défaut : sa boîte rendrait 0 × 19 px et la case resterait vide, sans erreur ni
   message. Piège déjà payé sur ce projet. */
.brand-logo-empty {
  display: block;
}

/* Format de la fiche, où la pastille est regardée de près. */
.brand-logo.lg,
.brand-logo-empty.lg {
  width: 96px;
  height: 96px;
  border-radius: 12px;
}

/* Ligne « pastille + explication » de la fiche. */
.brand-logo-row {
  display: flex;
  gap: 16px;
  align-items: flex-start;
  margin-bottom: 20px;
}

/* Pourquoi ce logo-là, ou pourquoi il n'y en a pas. Jamais une case vide :
   « pas encore saisi » et « fichier perdu » doivent se distinguer. */
.brand-why {
  margin: 0;
  font-size: 13px;
  color: var(--md-on-surface-variant);
  min-width: 0;
}

/* Colonne d'explication du tableau : bornée pour ne pas manger la largeur des
   actions, mais assez large pour une phrase entière. */
.brand-why-cell {
  max-width: 320px;
  font-size: 13px;
  color: var(--md-on-surface-variant);
}

/* Une enseigne désactivée reste lisible, mais se distingue au premier coup d'œil. */
.brand-row-off {
  opacity: .55;
}

/* Ajout d'un alias : champ et bouton sur une ligne, qui se replie si la place
   manque.

   ⚠️ Le bouton est un .btn-primary, donc en width: 100%. Sans flex: 0 1 auto et
   min-width: 0, il déborde horizontalement — mesuré à 400 px sur la bannière du
   tableau de bord. */
.brand-alias-form {
  display: flex;
  gap: 12px;
  align-items: center;
  margin-top: 16px;
  flex-wrap: wrap;
}

.brand-alias-form .field {
  margin: 0;
  flex: 1 1 240px;
  min-width: 0;
}

.brand-alias-form .btn-primary {
  flex: 0 1 auto;
  min-width: 0;
}

/* Texte d'aide sous un champ : collé au champ qu'il explique, pas flottant
   entre les deux. */
.field-hint {
  font-size: 13px;
  color: var(--md-on-surface-variant);
  margin: -8px 0 16px;
}

.field-hint.last {
  margin-bottom: 20px;
}

/* Libellé au-dessus d'un champ.

   ⚠️ Indispensable sur tout écran d'ÉDITION, et c'est un défaut constaté le
   14/09/2026 : le motif .field ne porte qu'une icône et un `placeholder`. Sur un
   formulaire de création, champs vides, l'invite suffit. Sur un formulaire
   d'édition, elle disparaît dès que le champ a une valeur — il ne reste qu'une
   icône, et plus rien ne dit ce qu'on est en train de modifier. Le client a
   cherché en vain le champ « Origine et licence du logo », dont le texte n'était
   affiché nulle part.

   Double bénéfice : un <input> sans <label> associé est aussi un défaut
   d'accessibilité — un lecteur d'écran n'annonce rien. */
.field-label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: var(--md-on-surface-variant);
  margin-bottom: 6px;
}

/* Un libellé qui suit un texte d'aide a besoin de respirer. */
.field-hint + .field-label {
  margin-top: 4px;
}

/* Libellé réservé aux lecteurs d'écran.

   Pour les champs où l'invite visible suffit à l'œil — un champ vide dans un
   formulaire d'ajout — mais où un <input> sans <label> associé ne serait annoncé
   par rien. `display: none` ne conviendrait pas : les lecteurs d'écran l'ignorent
   aussi. D'où le rognage à 1 px, la technique habituelle. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* État Wikidata d'une enseigne (migration 016).

   Trois codes, et c'est le cœur de l'information : « Wikidata publie le logo »
   veut dire qu'il n'y a RIEN à faire, « absent » veut dire que personne ne le
   trouvera. L'écran confondait les deux dans un « aucun logo enregistré ici ».

   Couleurs : jamais la couleur seule ne porte le sens — le libellé est écrit en
   toutes lettres à côté, et le détail complet est dans la colonne suivante. Un
   daltonien lit la même chose. */
.wd-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
}

/* Rien à faire : l'application trouve le logo toute seule. */
.wd-pill.yes {
  background: var(--md-primary-container);
  color: var(--md-on-primary-container);
}

/* À traiter ici : aucun logo publié, ou aucune entité identifiée. */
.wd-pill.no {
  background: #FFD9D6;
  color: #7A1F1A;
}

:root[data-theme="dark"] .wd-pill.no {
  background: #5C1A16;
  color: #FFDAD5;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .wd-pill.no {
    background: #5C1A16;
    color: #FFDAD5;
  }
}

/* Jamais vérifié — ni bon ni mauvais, juste inconnu. */
.wd-pill.unknown {
  background: var(--md-surface-container-high);
  color: var(--md-on-surface-variant);
}

/* Bandeau d'action au-dessus du tableau : ce qu'il reste à faire, et le bouton
   qui le fait. `flex-wrap` obligatoire, sinon débordement horizontal sur mobile. */
.wd-actions {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 16px;
}

.wd-actions form {
  margin: 0;
}

/* .btn-secondary est plus sobre que .btn-primary pour une action de
   rafraîchissement, et n'est pas en width:100% — pas de min-width à corriger. */
.wd-actions .btn-secondary {
  flex: 0 0 auto;
}
