/* ============================================================================
   PROJECT151 — SURCHARGES DU THÈME MATERIALIZE (back-office)
   ----------------------------------------------------------------------------
   Ce fichier remplace `public/css/coreui-custom.css`, qui accompagnait le thème
   Sash. Il ne contient QUE deux choses :

     1. la couleur d'accent Project151 posée sur les variables Bootstrap ;
     2. les composants réellement PROPRES à Project151 (catalogue, opportunités
        du jour, effet 3D des cartes, moniteur de scraping, bottom bar mobile,
        tableaux empilés…), repris tels quels de l'ancien fichier.

   ⛔ CE QUI A ÉTÉ VOLONTAIREMENT ABANDONNÉ, ET QU'IL NE FAUT PAS RÉINTRODUIRE :
   les ~140 règles de `coreui-custom.css` qui repeignaient en sombre des éléments
   Bootstrap génériques — `.card`, `.form-control`, `.btn-*`, `.btn-outline-*`,
   `.dropdown-*`, `.modal-*`, `.table`, `.pagination`, `.form-check-input`,
   `.input-group-text`, `.d-none`… Elles n'existaient que parce que le thème Sash
   tournait en `data-theme-mode="light"` et qu'il fallait rattraper chaque surface
   à la main. Materialize tourne ici en `data-bs-theme="dark"` NATIF : ces surfaces
   sont déjà sombres, et les recouvrir se voit tout de suite (contrastes cassés,
   états `:disabled` / `:checked` qui ne suivent plus le thème).

   🔴 Deux pièges de l'ancien fichier disparaissent avec lui, ne pas les recréer :
     - il redéfinissait `.d-none` APRÈS Bootstrap, ce qui neutralisait toutes les
       variantes responsives (`d-lg-block` & co) ;
     - il rendait `.btn-outline-*` quasi invisible au repos.

   ⚠️ Après toute modification de ce fichier : incrémenter `asset_version`
   (`config/app.php`), sinon le navigateur sert l'ancienne version.
   ============================================================================ */

/* ----------------------------------------------------------------------------
   1. IDENTITÉ VISUELLE PROJECT151
   -------------------------------------------------------------------------- */

:root {
    /* Palette de marque. `--p151-gradient-primary` est la signature du produit :
       elle est utilisée par les composants plus bas et par l'extension Chrome. */
    --p151-primary-color: #ffffff;
    --p151-secondary-color: #f747f2;
    --p151-tertiary-color: #a12c9d;
    --p151-gradient-primary: linear-gradient(135deg, #f747f2 0%, #a855f7 100%);
    --p151-gradient-secondary: linear-gradient(135deg, #a12c9d 0%, #7a1f75 100%);
    --p151-gradient-info: linear-gradient(135deg, #f747f2 0%, #a855f7 100%);
    --p151-gradient-warning: linear-gradient(135deg, #f75600 0%, #e04400 100%);
    --p151-gradient-success: linear-gradient(135deg, #4caf50 0%, #388e3c 100%);
}

/* ----------------------------------------------------------------------------
   1bis. SURFACES ET TEXTES — ALIAS DES JETONS DU THÈME
   ----------------------------------------------------------------------------
   ⛔ C'EST LA RÈGLE LA PLUS IMPORTANTE DE CE FICHIER.

   Aucune surface, aucune bordure, aucun gris de texte ne s'écrit en dur dans ce
   projet. Tout passe par ces sept variables, qui ne font que POINTER sur les
   jetons de Materialize. Conséquence : un composant maison est peint avec
   exactement la même couleur qu'une `.card` du thème, sans que personne n'ait à
   recopier un code hexadécimal — et si le thème change (autre skin, mode clair),
   tout suit du même coup.

   🔴 POURQUOI CETTE RÈGLE EXISTE. Le thème Sash tournait en mode CLAIR : il
   fallait repeindre chaque surface à la main, d'où une palette sombre écrite en
   dur un peu partout (`#1c1c1c`, `#262626`, `#303030`, bordures `#3a3a3a`…),
   recopiée dans 28 vues et ~500 déclarations. Materialize est nativement sombre
   avec SA palette (carte `#30334e`, page `#282a42`). Les deux ne coïncident pas :
   chaque bloc gardant l'ancienne teinte ressortait comme une pièce rapportée —
   le cas qui a déclenché cette passe était le panneau « Veille marché » du
   tableau de bord, en `#262626` à côté de cartes en `#30334e`.

   ⛔ NE JAMAIS réintroduire une couleur de surface en dur, ni ici ni dans une
   vue. La consigne du `.cursorrules` qui liste « surfaces #1c1c1c / #262626 /
   #2e2e2e / #303030, bordures #3a3a3a / #424242 » a été écrite POUR SASH : elle
   est caduque depuis la bascule.

   ⚠️ Ces variables ne sont déclarées QUE dans ce fichier, chargé par le seul
   layout Materialize. Une vue servie par `layouts.theme`, `layouts.auth` ou un
   e-mail n'y a pas accès : n'y écris pas `var(--p151-surface)`, la déclaration
   serait invalide et l'élément perdrait sa couleur au lieu d'en changer.
   -------------------------------------------------------------------------- */

:root {
    /* Surface d'un panneau : exactement celle des `.card` du thème. */
    --p151-surface: var(--bs-paper-bg);

    /* Fond en creux, un cran plus sombre que la carte : celui de la page. Sert
       aux zones encastrées (encadrés d'aide, blocs de code, listes internes). */
    --p151-surface-sunken: var(--bs-body-bg);

    /* Surface légèrement détachée POSÉE SUR une carte (badge, pastille, entête
       de sous-bloc). Calculée à partir de la carte pour rester juste quel que
       soit le skin, plutôt que devinée. */
    --p151-surface-raised: color-mix(in sRGB, #eaeaff 6%, var(--bs-paper-bg));

    --p151-border: var(--bs-border-color);
    --p151-text: var(--bs-body-color);
    --p151-text-strong: var(--bs-heading-color);
    --p151-text-muted: var(--bs-secondary-color);
}

/* Couleur primaire du thème. Le violet `#a855f7` plutôt que le rose `#f747f2` :
   c'est l'extrémité du dégradé qui tient le contraste sur fond sombre (les deux
   restent disponibles via les variables ci-dessus).
   Les trois sélecteurs sont ceux qu'utilise Materialize pour ses jeux de
   variables : viser `:root` seul ne suffirait pas en thème sombre. */
:root,
[data-bs-theme='light'],
[data-bs-theme='dark'] {
    --bs-primary: #a855f7;
    --bs-primary-rgb: 168, 85, 247;
    --bs-primary-bg-subtle: rgba(168, 85, 247, 0.1);
    --bs-primary-border-subtle: rgba(168, 85, 247, 0.3);
    --bs-primary-contrast: #fff;
}

/* Logo dans l'en-tête de la sidebar. Hauteur fixe : le logo est un PNG large,
   sans contrainte il pousserait la hauteur de `.app-brand` et décalerait tout le
   menu d'un cran. */
.p151-brand-logo img,
img.p151-brand-logo {
    height: 34px;
    width: auto;
    max-width: 100%;
    display: block;
}

/* Pastille d'avatar de la navbar et de son menu déroulant. */
.p151-avatar {
    background: var(--p151-gradient-primary);
    color: #fff;
}

/* Compteur de crédits de la navbar. */
.p151-header-credits {
    color: var(--bs-body-color);
    white-space: nowrap;
}

.p151-header-credits:hover {
    color: var(--bs-primary);
}

/* ----------------------------------------------------------------------------
   2. NOTIFICATIONS DE SESSION
   ----------------------------------------------------------------------------
   Ces alertes étaient positionnées par des `style=""` en dur dans le layout Sash.
   Le style vit désormais ici (le projet interdit le CSS en ligne).
   Elles sont refermées automatiquement au bout de 5 s par materialize-p151.js.
   -------------------------------------------------------------------------- */

.session-notification {
    position: fixed;
    top: 1.25rem;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1090; /* au-dessus des modales Bootstrap (1055) */
    min-width: 320px;
    max-width: min(600px, calc(100vw - 2rem));
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
}

/* ----------------------------------------------------------------------------
   3. RETOUR EN HAUT DE PAGE
   ----------------------------------------------------------------------------
   `.scrollToTop` était un composant du thème Sash : il n'existe pas dans
   Materialize. Il est donc réimplémenté ici (styles) et dans
   public/js/materialize-p151.js (apparition au scroll + remontée). Sur mobile,
   il est décalé au-dessus de la bottom bar par la règle
   `body.p151-has-bottombar .scrollToTop`, plus bas dans ce fichier.
   -------------------------------------------------------------------------- */

.scrollToTop {
    position: fixed;
    inset-block-end: 1.5rem;
    inset-inline-end: 1.5rem;
    z-index: 1030;
    display: none;
    align-items: center;
    justify-content: center;
    inline-size: 42px;
    block-size: 42px;
    border-radius: 50%;
    background: var(--p151-gradient-primary);
    color: #fff;
    cursor: pointer;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
    transition: transform 0.15s ease;
}

.scrollToTop.p151-visible {
    display: flex;
}

.scrollToTop:hover {
    transform: translateY(-2px);
}

/* ----------------------------------------------------------------------------
   4. ANIMATION PARTAGÉE
   -------------------------------------------------------------------------- */

@keyframes pulse {
    0% {
        opacity: 1;
    }

    50% {
        opacity: 0.5;
    }

    100% {
        opacity: 1;
    }
}

/* ============================================================================
   5. COMPOSANTS PROJECT151
   ----------------------------------------------------------------------------
   Tout ce qui suit est repris de `coreui-custom.css` sans modification de fond :
   ce sont des composants maison, ils ne dépendaient pas du thème Sash.
   ============================================================================ */
/* ========================================
   BADGE V1 (DORÉ)
   ======================================== */
.badge-v1 {
    background: linear-gradient(135deg, #ffd700 0%, #ffed4e 50%, #ffd700 100%);
    color: #000;
    font-weight: 600;
    font-size: 0.7rem;
    padding: 0.25em 0.5em;
    border-radius: 0.25rem;
    box-shadow: 0 2px 4px rgba(255, 215, 0, 0.3);
    text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5);
}

/* ========================================
   FOND VIDÉO CATALOGUE
   ======================================== */
.catalog-body-with-video {
    position: relative;
    overflow: hidden;
    min-height: 500px;
}

.catalog-bg-video {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 1;
    pointer-events: none;
}

.catalog-bg-video video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0.3;
}

/* Tout le contenu du card-body doit être au-dessus de la vidéo */
.catalog-body-with-video > *:not(.catalog-bg-video) {
    position: relative;
    z-index: 2;
}

.notification-pulse {
    animation: pulse 1.5s infinite;
}

/* Styles pour le forum */
.forum_categories td {
    max-width: 300px;
}

.forum_categories td.align-middle {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Permettre à la colonne de description de s'étendre */
.forum_categories td:nth-child(2) {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 400px;
}

/* Styles pour les guides */
.guide-category-img {
    height: 325px;
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    transition: all 0.3s ease;
}

.guide-links .list-group-item {
    border-left: none;
    border-right: none;
    border-radius: 0;
    transition: all 0.2s ease;
}

.guide-links .list-group-item:first-child {
    border-top-left-radius: 0.25rem;
    border-top-right-radius: 0.25rem;
}

.guide-links .list-group-item:last-child {
    border-bottom-left-radius: 0.25rem;
    border-bottom-right-radius: 0.25rem;
}

.guide-links .list-group-item:hover {
    background-color: var(--p151-surface-raised);
    transform: translateX(5px);
}

.card:hover .guide-category-img {
    transform: scale(1.05);
}

.guide-content img {
    max-width: 100%;
    height: auto;
    border-radius: 5px;
    margin: 10px 0;
}

.guide-content table {
    width: 100%;
    border-collapse: collapse;
    margin: 15px 0;
}

.guide-content table,
.guide-content th,
.guide-content td {
    border: 1px solid #ddd;
}

.guide-content th,
.guide-content td {
    padding: 8px;
    text-align: left;
    vertical-align: middle;
}

.guide-content th {
    background-color: #f2f2f2;
}

.sortable-categories tr,
.sortable-guides tr {
    cursor: move;
}

.ui-sortable-helper {
    display: table;
    background-color: var(--p151-surface-raised);
    box-shadow: 0 0 10px rgba(0,0,0,0.1);
}

.ui-state-highlight {
    height: 3em;
    line-height: 3em;
    background-color: #fffde7;
    border: 1px dashed #ffd54f;
}

/* Styles pour la wishlist */
.wishlist-card {
    transition: all 0.3s ease;
}

.wishlist-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

/* `btn-block` n'existe plus en Bootstrap 5 : les vues portent `w-100`. */

.wishlist-card .card-body {
    display: flex;
    flex-direction: column;
}

/* Commandes posées SUR la vignette d'un article suivi. Elles portaient leur
   apparence en `style=` — `padding: .25rem .5rem` et `border-radius: 50%` — :
   deux pastilles rondes de 25 px au milieu d'un back-office où tous les boutons
   font 33 px et 6 px de rayon. On ne déclare plus que le PLACEMENT. */
.wishlist-card__overlay {
    position: absolute;
    right: 5px;
    z-index: 10;
    opacity: 0.85;
    transition: opacity 0.2s ease;
}

.wishlist-card__overlay:hover {
    opacity: 1;
}

.wishlist-card__overlay--1 {
    top: 5px;
}

/* La seconde se pose sous la première : hauteur du bouton (33 px) + 5 px de marge. */
.wishlist-card__overlay--2 {
    top: 43px;
}

/* Styles pour les cases à cocher */
.wishlist-checkbox {
    width: 20px;
    height: 20px;
    cursor: pointer;
}

/* Style pour le bouton de suppression multiple */
#delete-selected-btn,
#delete-selected-bottom-btn {
    transition: all 0.3s ease;
}

/* Styles pour le formulaire de filtrage */
.filter-form {
    background-color: var(--p151-surface-raised);
    padding: 15px;
    border-radius: 5px;
    margin-bottom: 20px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

/* ⛔ Trois restes de THÈME CLAIR retirés ici le 2026-08-24 :
     - `label { color: #495057 }` : un gris foncé, illisible sur fond sombre ;
     - `:focus { border-color:#80bdff; box-shadow: rgba(0,123,255,…) }` : l'anneau
       de focus BLEU de Bootstrap, alors que tous les autres champs du back-office
       prennent le violet primaire ;
     - `.input-group-text { background-color:#e9ecef }` : un bloc GRIS CLAIR posé
       contre un champ sombre.
   Le thème gère les trois. Ne rien remettre : c'est exactement le genre de
   surcharge qui faisait qu'un formulaire ne ressemblait pas au suivant. */
.filter-form label {
    font-weight: 500;
    font-size: 0.9rem;
}

@media (max-width: 767.98px) {
  .filter-form .btn {
          width: 100%;
          margin-top: 10px;
      }
  
  .filter-form .d-flex {
          flex-direction: column;
      }
  
  .filter-form .me-2 {
          margin-right: 0 !important;
          margin-bottom: 10px;
      }
}

/* Styles pour les fonctionnalités */
.row-equal-height {
    display: flex;
    flex-wrap: wrap;
}

.row-equal-height > [class*='col-'] {
    display: flex;
    flex-direction: column;
}

.row-equal-height .card {
    height: 100%;
}

.feature-image-container {
    height: 200px;
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: var(--p151-surface-raised);
    position: relative;
}

.feature-image-link {
    display: block;
    width: 100%;
    height: 100%;
    position: relative;
    text-decoration: none;
}

.feature-image-background {
    width: 100%;
    height: 100%;
    background-position: center;
    background-repeat: no-repeat;
    background-size: contain;
    transition: transform 0.3s ease;
}

.feature-image-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.feature-image-overlay-text {
    color: white;
    font-size: 1.2rem;
    text-align: center;
    padding: 10px;
    background-color: rgba(0, 0, 0, 0.7);
    border-radius: 5px;
}

.feature-image-link:hover .feature-image-background {
    transform: scale(1.05);
}

.feature-image-link:hover .feature-image-overlay {
    opacity: 1;
}

.no-image {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    width: 100%;
    font-size: 3rem;
    color: var(--p151-border);
}

.feature-description-preview {
    overflow: hidden;
    text-overflow: ellipsis;
}

.feature-detail-image {
    max-height: 400px;
    object-fit: contain;
}

.admin-feature-thumbnail {
    max-width: 100px;
    max-height: 60px;
    object-fit: cover;
}

.admin-feature-image-preview {
    max-width: 300px;
    max-height: 200px;
    object-fit: contain;
    border: 1px solid #dee2e6;
    border-radius: 0.25rem;
    padding: 0.25rem;
}

.invalid-feedback {
    display: none;
    width: 100%;
    margin-top: 0.25rem;
    font-size: 80%;
    color: #e55353;
}

.is-invalid ~ .invalid-feedback {
    display: block;
}

/* Augmenter la hauteur du textarea de description dans les pages de transactions */
#description {
    min-height: 150px;
    height: auto;
}

.feature-detail-image {
    height: 300px;
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    margin: 0 auto;
    max-width: 100%;
}

.titre_site_appname {
    text-decoration: none;
    color: #c171f7;
}

.feature-image-background {
    height: 190px;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}

ul.header-nav.ms-auto {
    margin-bottom: unset;
    vertical-align: middle;
    list-style: none;
    position: relative;
    top: 17px;
}

.filter-form.mb-4 {
    background: var(--p151-surface-sunken);
}

.news-content > p > a,
.news-content > a {
    text-decoration: underline;
}

/* ========================================
   CATALOGUE DES CARTES CARDMARKET
   ======================================== */

/* Filtres - Colonne de gauche */
.catalog-filters {
    background: var(--p151-surface);
    border: 1px solid var(--p151-border);
}

.catalog-filters .card-header {
    background: var(--p151-surface);
    border-bottom: 1px solid var(--p151-border);
}

.catalog-filters .card-header h5 {
    color: #fff;
}

.catalog-filters .card-body {
    background: var(--p151-surface);
}

.catalog-filters .search-section {
    border-bottom: 1px solid var(--p151-border);
}

.catalog-filters .search-label {
    color: #fff;
    font-size: 0.85rem;
}

.catalog-filters #search {
    background: var(--p151-surface);
    border-color: var(--p151-border);
    color: #fff;
    font-size: 0.8rem;
}

.catalog-filters #search:focus {
    background: var(--p151-surface) !important;
    border-color: #667eea !important;
    color: #fff !important;
    box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}

/* Accordéon des expansions */
.catalog-accordion-item {
    background: var(--p151-surface);
    border-color: var(--p151-border);
}

.catalog-accordion-button {
    background: var(--p151-surface) !important;
    color: #fff;
    border-color: var(--p151-border);
    font-size: 0.85rem;
    padding: 0.5rem 0.75rem;
}

.catalog-accordion-button:not(.collapsed) {
    background: var(--p151-surface) !important;
}

.catalog-accordion-button:hover:not(.collapsed) {
    background: var(--p151-surface) !important;
}

.catalog-accordion-button.collapsed:hover {
    background: var(--p151-border) !important;
}

.catalog-accordion-button.collapsed::after {
    filter: invert(1);
}

.catalog-accordion-badge {
    font-size: 0.7rem;
}

/* Liste des expansions */
.catalog-expansion-list .list-group-item {
    background: var(--p151-surface);
    color: #fff;
    border-color: var(--p151-border);
    font-size: 0.75rem;
    padding: 0.5rem 0.75rem;
    word-wrap: break-word;
    white-space: normal;
}

.catalog-expansion-list .list-group-item.active {
    background: #667eea;
}

.catalog-expansion-list .list-group-item:hover:not(.active) {
    background: var(--p151-border) !important;
}

.catalog-expansion-badge {
    font-size: 0.65rem;
}

/* Colonne de droite - Cartes */
.catalog-main {
    background: var(--p151-surface);
    border: 1px solid var(--p151-border);
}

.catalog-main .card-header {
    background: var(--p151-surface);
    border-bottom: 1px solid var(--p151-border);
}

.catalog-main .card-header h4 {
    color: #fff;
}

.catalog-main .card-header small {
    color: var(--p151-text);
}

/* ========================================
   /cards — ACCORDEONS MOBILE (« Filtres » + « Recherche & tri »)
   Fermes par defaut sous 768px, toujours ouverts a partir de 768px.
   ======================================== */
.catalog-collapse-chevron {
    color: var(--p151-secondary-color);
    transition: transform 0.25s ease;
    font-size: 1rem;
}

.catalog-collapse-header[aria-expanded="true"] .catalog-collapse-chevron {
    transform: rotate(180deg);
}

/* Barre « Recherche & tri » : uniquement sur mobile (le formulaire est libre sur desktop) */
.catalog-search-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.7rem 1rem;
    background: var(--p151-surface);
    border-bottom: 1px solid var(--p151-border);
    color: var(--p151-text-strong);
    font-weight: 600;
    cursor: pointer;
    user-select: none;
}

@media (min-width: 768px) {
  /* Desktop : les blocs restent visibles meme sans la classe .show de Bootstrap */
  .catalog-collapse-mobile {
          display: block !important;
          height: auto !important;
          overflow: visible !important;
          visibility: visible !important;
      }
  
  .catalog-search-toggle {
          display: none;
      }
  
  .catalog-filters .card-header.catalog-collapse-header {
          pointer-events: none;
      }
  
  .catalog-filters .catalog-collapse-chevron {
          display: none;
      }
}

@media (max-width: 767px) {
  .catalog-filters .card-header.catalog-collapse-header {
          cursor: pointer;
          user-select: none;
      }
}

/* Formulaire de filtres de recherche et prix */
#cardSearchForm {
    background: var(--p151-surface);
}

#cardSearchForm .form-label {
    color: #fff;
    font-size: 0.85rem;
    margin-bottom: 0.3rem;
}

/* ⛔ Le fond, la bordure et l'état :focus des champs du catalogue ne sont PLUS
   redéfinis ici (2026-08-24). Ils remplissaient les champs alors que ceux du
   thème sont transparents et bordés — /cards était le dernier écran où un champ
   ne ressemblait pas aux autres. L'anneau de focus était en prime BLEU
   (#667eea), quand tous les autres champs du back-office prennent le violet
   primaire. Seule la taille de police, qui est un choix de densité propre à
   cette barre de filtres, est conservée. */
#cardSearchForm .form-control {
    font-size: 0.9rem;
}

/* ⛔ Les boutons de cette barre ne se repeignent PAS. Ils portaient un dégradé
   rose→violet (`btn-primary`) et un gris `--p151-border` (`btn-secondary`) : deux
   apparences que l'on ne retrouvait sur aucun autre écran. Section 10.10. */

/* Cartes individuelles */
.catalog-card {
    background: var(--p151-surface);
    border: 1px solid var(--p151-border);
}

.catalog-card-img {
    height: 320px;
    object-fit: contain;
    padding: 10px;
}

.catalog-card-img-placeholder {
    height: 320px;
    background: #000;
}

.catalog-card-img-placeholder i {
    font-size: 3rem;
    color: var(--p151-border);
}

.catalog-card-title {
    font-size: 0.8rem;
    font-weight: 600;
    color: #fff;
}

.catalog-card-title-truncate {
    /* Force le texte sur une seule ligne avec ellipsis */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

/* Photos d'expansion Pokecardex */
.pokecardex-expansion-photo {
    height: 120px;
    width: auto;
    object-fit: contain;
    border-radius: 8px;
    display: block;
    margin: 0 auto;
    background: transparent;
}

.pokecardex-expansion-photo-header {
    height: 60px;
    width: auto;
    object-fit: contain;
    margin-right: 15px;
    border-radius: 6px;
    background: transparent;
}

.pokecardex-photo-container {
    background: transparent;
    padding: 0;
    margin-bottom: 1rem;
    text-align: center;
}

.catalog-card-expansion {
    font-size: 0.7rem;
    color: var(--p151-text-muted);
}

.catalog-card-price {
    font-size: 0.8rem;
}

.catalog-card-price-na {
    font-size: 0.7rem;
}

/* Tableau des prix par marketplace */
.catalog-price-table {
    margin-bottom: 0;
    background: transparent;
}

.catalog-price-table td {
    border: none;
    padding: 0.25rem 0.3rem;
    color: #fff;
    font-size: 0.75rem;
}

.catalog-price-table tr {
    background: rgba(255, 255, 255, 0.05);
}

.catalog-price-table tr:hover {
    background: rgba(255, 255, 255, 0.1);
}

.catalog-price-icon {
    width: 30px;
    text-align: left;
}

.catalog-price-value {
    font-weight: 600;
    color: #4caf50;
    text-align: center;
}

.catalog-price-link {
    color: #4caf50;
    text-decoration: none;
}

.catalog-price-link:hover {
    color: #66bb6a;
    text-decoration: underline;
}

.catalog-price-trend {
    width: 60px;
    text-align: right;
}

.catalog-price-trend-content {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

.catalog-price-trend-icon {
    font-size: 1.2rem;
}

.catalog-price-trend-percentage {
    font-size: 0.75rem;
    font-weight: 600;
}

/* Icône des plateformes pour les cartes */
.icon-platform-for-card {
    width: 20px;
    height: 20px;
    object-fit: contain;
    transition: transform 0.2s;
}

.icon-platform-for-card:hover {
    transform: scale(1.2);
}

span.badge.bg-secondary.float-end.catalog-expansion-badge,
span.badge.bg-primary.ms-2.catalog-accordion-badge {
    background: #000000 !important;
}

span.badge.bg-secondary.catalog-expansion-badge {
    background: black !important;
}

/* ========================================
   BOUTONS DE TRI DES EXPANSIONS
   ======================================== */
.sort-btn {
    transition: transform 0.2s;
}

.sort-btn:hover {
    transform: scale(1.05);
}

/* ========================================
   MODAL RÉGÉNÉRATION EN MASSE
   ======================================== */
#bulkRefreshModal .modal-content {
    background-color: var(--p151-surface);
    color: #fff;
    border: 1px solid var(--p151-border);
}

#bulkRefreshModal .modal-header {
    background-color: var(--p151-surface);
    border-bottom: 1px solid var(--p151-border);
}

#bulkRefreshModal .modal-body {
    background-color: var(--p151-surface);
}

#bulkRefreshModal .modal-footer {
    background-color: var(--p151-surface);
    border-top: 1px solid var(--p151-border);
}

#bulkRefreshModal .list-group-item {
    background-color: var(--p151-surface);
    color: #fff;
    border-color: var(--p151-border);
}

#bulkRefreshModal .list-group-item:hover {
    background-color: var(--p151-surface-raised);
}

#bulkRefreshModal .badge {
    background-color: #667eea !important;
}

#bulkRefreshModal .text-muted {
    color: var(--p151-text) !important;
}

#bulkRefreshModal .alert-warning {
    background-color: rgba(255, 193, 7, 0.1);
    border-color: rgba(255, 193, 7, 0.3);
    color: #ffc107;
}

/* ========================================
   RESPONSIVE DESIGN - CATALOGUE
   ======================================== */
/* Pour les écrans jusqu'à 1366px : afficher 4 cartes par ligne au lieu de 6 */
@media (min-width: 992px) and (max-width: 2000px) {
  #cards-container .col-lg-2 {
          flex: 0 0 25%;
          max-width: 25%;
      }
}

/* Pour les très petits écrans (mobile) : afficher 2 cartes par ligne */
@media (max-width: 576px) {
  #cards-container .col-6 {
          flex: 0 0 50%;
          max-width: 50%;
      }
}

/* Pour les tablettes (768px - 991px) : garder 4 cartes par ligne */
@media (min-width: 768px) and (max-width: 991px) {
  #cards-container .col-md-3 {
          flex: 0 0 25%;
          max-width: 25%;
      }
}

input.form-check-input.expansion-toggle {
    position: relative;
}

/* ⛔ HAUTEUR DES SELECTS : PLUS AUCUNE VALEUR FORCEE (2026-08-24).
   Ces regles imposaient 36px a trois listes deroulantes, alors que celles du
   theme font 47px : cote a cote dans la meme barre de filtres, deux selects
   n'avaient donc pas la meme hauteur et le libelle du plus court se retrouvait
   rogne. La hauteur d'un controle appartient au theme. */

input.form-check-input.expansion-checkbox {
    position: relative;
}

button#tab-months,
button#tab-years,
button#tab-days {
    background: var(--p151-surface-raised);
}

/* ========================================
   EFFET 3D HOLOGRAPHIQUE SUR LES CARTES
   ======================================== */
.card-3d-wrapper {
    position: relative;
    display: inline-block;
    cursor: pointer;
}

.card-3d-image {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 0.375rem;
}

.card-3d-shine {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border-radius: 0.375rem;
    pointer-events: none;
    z-index: 3;
    opacity: 0;
    transform-style: preserve-3d;
    background: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.15) 15%, rgba(255, 255, 255, 0.05) 30%, rgba(255, 255, 255, 0) 50%);
    transition: 0.3s ease-out;
}

img.card-3d-image.img-fluid.rounded {
    max-height: 380px;
}

avec deux differences :
   - l'image n'ouvre rien au clic -> pas de `cursor: pointer` trompeur ;
   - `width: auto` est INDISPENSABLE. Le defaut `.card-3d-image { width: 100% }`
     combine au plafond `max-height: 380px` ci-dessus ecraserait la carte
     (largeur de la colonne conservee, hauteur rognee = image deformee).
     Avec width auto + max-width/max-height, le navigateur respecte le ratio. */
.card-3d-wrapper.cards-detail-3d {
    cursor: default;
    max-width: 100%;
}

.card-3d-wrapper.cards-detail-3d .card-3d-image {
    width: auto;
    max-width: 100%;
}

.catalog-card-img {
    background: var(--p151-surface-sunken);
}



/* ========================================
   BOUTON YOUTUBE TUTO
   ----------------------------------------
   ⛔ NE PAS y remettre `font-size` / `padding` : ce bouton vit dans une barre de
   titre, à côté de boutons ordinaires du thème. Toute taille propre le fait
   ressortir et casse l'alignement de la ligne (il portait `btn-lg` + 0.95rem
   jusqu'au 2026-08-24 : il était visiblement plus haut que ses voisins).
   ======================================== */
.btn-youtube-tuto {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ========================================
   MODALE TCGDEX + HISTORIQUE DE PRIX
   ======================================== */
.cards-nav-btn {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bs-primary);
    border: none;
}

.cards-nav-prev {
    left: 0;
}

.cards-nav-next {
    right: 0;
}

/* ⛔ L'état actif du sélecteur de période se dit par la VARIANTE (`btn-primary`,
   posée par cards-catalog.js), plus par un dégradé maison. */

@media (max-width: 991px) {
  .cards-nav-btn {
          display: none !important;
      }
}

/* ========================================
   OPPORTUNITÉS DU JOUR
   ======================================== */
.daily-opportunities {
    border: 1px solid var(--p151-border);
    border-radius: 8px;
    padding: 1rem;
    background: var(--p151-surface);
}

.daily-opportunities-filter {
    max-width: 100%;
}

.daily-opportunities-price-input {
    width: 120px;
}

.daily-opportunities-languages {
    min-width: 320px;
}

.daily-opportunities-languages .accordion-item {
    background: var(--p151-surface-sunken);
    border: 1px solid var(--p151-border);
    border-radius: 6px;
    overflow: hidden;
}

.daily-opportunities-languages .accordion-button {
    min-height: 34px;
    padding: 0.35rem 0.75rem;
    background: var(--p151-surface-sunken);
    color: var(--p151-text-strong);
    box-shadow: none;
}

.daily-opportunities-languages .accordion-button:not(.collapsed) {
    background: rgba(247, 71, 242, 0.12);
    color: #fff;
}

.daily-opportunities-languages .accordion-button::after {
    filter: invert(1);
}

.daily-opportunities-languages .accordion-body {
    padding: 0.75rem;
    border-top: 1px solid var(--p151-border);
}

.daily-opportunities-language-check {
    min-height: 32px;
    padding: 0.35rem 0.5rem 0.35rem 2rem;
    border: 1px solid var(--p151-border);
    border-radius: 6px;
    background: var(--p151-surface);
}

/* ⛔ Ces cases cochaient en ROSE quand toutes les autres du back-office cochent
   en violet primaire. La couleur d'etat coche appartient au theme. */

.daily-opportunities-table {
    margin-bottom: 0;
}

.daily-opportunities-table th {
    color: var(--p151-text-strong);
    border-color: var(--p151-border);
    white-space: nowrap;
}

.daily-opportunities-table td {
    border-color: var(--p151-border);
}

.tcgdex-opportunity-row {
    cursor: pointer;
    border-left: 3px solid transparent;
    transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
}

.tcgdex-opportunity-row:hover {
    background-color: rgba(247, 71, 242, 0.08) !important;
    border-left-color: var(--p151-secondary-color);
    transform: translateX(2px);
}

.daily-opportunities-card-img,
.daily-opportunities-card-placeholder {
    width: 42px;
    height: 58px;
    flex: 0 0 42px;
    border-radius: 4px;
}

.daily-opportunities-card-img {
    object-fit: contain;
    background: var(--p151-surface-sunken);
}

.daily-opportunities-card-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--p151-surface-sunken);
    color: var(--p151-text-muted);
}

.daily-opportunities-card-title {
    min-width: 0;
}

.daily-opportunities-card-title strong {
    display: block;
    max-width: 220px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.daily-opportunities-platform-icon {
    width: 18px;
    height: 18px;
    display: inline-block;
    object-fit: contain;
    vertical-align: middle;
}

.daily-opportunities-trend-badge {
    background: rgba(229, 83, 83, 0.18) !important;
    color: #ff7a90;
    border: 1px solid rgba(229, 83, 83, 0.35);
}

/* Variante haussière (page /trends — Tendances haussières) */
.daily-opportunities-trend-badge-up {
    background: rgba(25, 135, 84, 0.18) !important;
    color: #4ade80;
    border: 1px solid rgba(25, 135, 84, 0.35);
}

.ebay-trend-badge {
    line-height: 1.25;
}

.ebay-trend-badge .daily-opportunities-trend-badge {
    font-size: 0.82rem;
    font-weight: 800;
    box-shadow: 0 6px 14px rgba(229, 83, 83, 0.16);
}

.ebay-trend-drop-amount {
    display: inline-flex;
    align-items: center;
    margin-left: 0.35rem;
    color: #ff7a90;
    font-size: 0.78rem;
    font-weight: 800;
}

.ebay-trend-price-line {
    font-size: 0.78rem;
}

/* Colonnes optionnelles du tableau (remplacent d-none d-lg-table-cell / d-md-table-cell :
   coreui-custom.css redefinit .d-none apres Bootstrap, ce qui neutralisait les variantes
   responsive et masquait ces colonnes MEME sur desktop). */
.daily-opportunities-table .dop-col-set,
.daily-opportunities-table .dop-col-change {
    display: none;
}

@media (min-width: 768px) {
  .daily-opportunities-table .dop-col-change {
          display: table-cell;
      }
}

@media (min-width: 992px) {
  .daily-opportunities-table .dop-col-set {
          display: table-cell;
      }
}

@media (max-width: 767px) {
  .daily-opportunities {
          padding: 0.75rem;
      }
  
  /* --- Tableau -> cartes empilees (illisible en mode tableau sur un ecran de telephone) --- */
  .daily-opportunities .table-responsive {
          overflow-x: visible;
      }
  
  /* ⚠️ ANNULE le `zoom: 0.5` + `font-size: 0.75rem` applique plus haut a TOUS les
         `.table-responsive table` sur mobile : combine aux tailles deja petites du
         tableau, le contenu tombait a ~6-9px (illisible). En mode empile on n'a plus
         besoin de compresser, on repasse en zoom 1 avec des tailles confortables. */
  .daily-opportunities .table-responsive table.daily-opportunities-table {
          zoom: 1;
          font-size: 0.95rem;
      }
  
  .daily-opportunities-table,
  .daily-opportunities-table tbody,
  .daily-opportunities-table tr {
          display: block;
          width: 100%;
      }
  
  .daily-opportunities-table thead {
          display: none;
      }
  
  .daily-opportunities-table tr {
          background: var(--p151-surface);
          border: 1px solid var(--p151-border);
          border-left-width: 3px;
          border-radius: 10px;
          padding: 0.6rem 0.75rem;
          margin-bottom: 0.6rem;
          box-shadow: none !important;
      }
  
  .tcgdex-opportunity-row:hover {
          transform: none;
      }
  
  /* ⚠️ Bootstrap pose background-color: var(--bs-table-bg) = #fff sur chaque cellule ;
         en mode tableau c'est masque par l'inset box-shadow (--bs-table-accent-bg sombre).
         En mode empile on neutralise les deux et on laisse voir le fond sombre du <tr>. */
  .daily-opportunities-table td {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 0.75rem;
          width: 100%;
          padding: 0.28rem 0;
          border: 0 !important;
          background-color: transparent !important;
          box-shadow: none !important;
          text-align: right !important;
      }
  
  .daily-opportunities-table td[data-label]::before {
          content: attr(data-label);
          flex: 0 0 auto;
          color: var(--p151-text);
          font-size: 0.82rem;
          font-weight: 600;
          text-transform: uppercase;
          letter-spacing: 0.02em;
      }
  
  /* Valeurs (prix, variation) bien lisibles */
  .daily-opportunities-table td strong,
  .daily-opportunities-table td > span {
          font-size: 1rem;
      }
  
  /* 1re cellule = entete de la carte (image + nom), sans libelle */
  .daily-opportunities-table td:first-child {
          justify-content: flex-start;
          text-align: left !important;
          padding-bottom: 0.55rem;
          margin-bottom: 0.35rem;
          border-bottom: 1px solid var(--p151-border) !important;
      }
  
  /* Le montant de la variation reste utile en mode empile */
  .daily-opportunities-table .dop-col-change {
          display: flex;
      }
  
  .daily-opportunities-price-input {
          width: 100%;
      }
  
  .daily-opportunities-languages {
          min-width: 0;
          width: 100%;
      }
  
  .daily-opportunities-filter,
  .daily-opportunities-filter .d-flex,
  .daily-opportunities-filter .d-flex > div {
          width: 100%;
      }
  
  .daily-opportunities-filter .btn {
          flex: 1 1 auto;
      }
  
  .daily-opportunities-card-title strong {
          max-width: none;
          white-space: normal;
          overflow: visible;
          font-size: 1.05rem;
          line-height: 1.25;
      }
  
  .daily-opportunities-card-title .text-muted {
          font-size: 0.82rem;
      }
  
  /* Vignette de carte : lisible (elle tombait a 21px avec le zoom 0.5) */
  .daily-opportunities-card-img,
  .daily-opportunities-card-placeholder {
          width: 52px;
          height: 72px;
          flex: 0 0 52px;
      }
  
  .daily-opportunities-platform-icon {
          width: 22px;
          height: 22px;
      }
  
  .daily-opportunities-table .badge {
          font-size: 0.88rem;
          padding: 0.4em 0.65em;
      }
  
  /* ⛔ Pas de padding propre : un `btn-sm` a la même taille partout (§10.10). */
}

/* ========================================
   TABLEAUX EMPILES SUR MOBILE (.p151-stack-table)
   Generalisation du rendu mobile du tableau « Opportunites du jour » de
   /dashboard (.daily-opportunities-table) : sous 768px chaque <tr> devient
   une carte et chaque <td> une ligne « LIBELLE -> valeur », le libelle etant
   lu dans l'attribut data-label de la cellule. Plus aucun scroll horizontal.

   Utilisation dans une vue :
     <div class="table-responsive p151-stack-wrap">
       <table class="table table-hover p151-stack-table">
         <thead>...</thead>                      <- masque sous 768px
         <tbody>
           <tr>
             <td class="p151-stack-thumb">       <- vignette, 1re ligne, sans libelle
             <td class="p151-stack-head">        <- titre, 1re ligne, sans libelle
             <td data-label="Prix">              <- ligne « PRIX        12,00 € »
             <td class="p151-stack-actions">     <- boutons, alignes a droite

   ⚠️ Pour masquer une colonne selon la largeur, ne PAS utiliser
   d-none d-lg-table-cell : coreui-custom.css redefinit .d-none APRES Bootstrap
   et neutralise les variantes responsive (la colonne disparaitrait aussi sur
   desktop). Utiliser une classe maison + media query (cf. .dop-col-*).
   ======================================== */
@media (max-width: 767px) {
  /* Le tableau ne deborde plus : on rend le scroll horizontal inutile. */
  .p151-stack-wrap {
          overflow-x: visible;
      }
  
  /* Annule le zoom/font-size applique aux tables de .table-responsive :
         en mode empile il n'y a plus rien a compresser. */
  .table-responsive table.p151-stack-table,
  table.p151-stack-table {
          zoom: 1;
          font-size: 0.95rem;
          width: 100%;
      }
  
  .p151-stack-table,
  .p151-stack-table tbody {
          display: block;
          width: 100%;
      }
  
  .p151-stack-table thead {
          display: none;
      }
  
  /* flex + wrap (et non display:block) : permet de garder vignette + titre
         cote a cote sur la premiere ligne de la carte via `order`. */
  .p151-stack-table tr {
          display: flex;
          flex-wrap: wrap;
          width: 100%;
          background: var(--p151-surface);
          border: 1px solid var(--p151-border);
          border-left-width: 3px;
          border-radius: 10px;
          padding: 0.6rem 0.75rem;
          margin-bottom: 0.6rem;
          box-shadow: none !important;
      }
  
  /* ⚠️ Bootstrap pose background-color: var(--bs-table-bg) sur chaque cellule ;
         en mode tableau c'est masque par l'inset box-shadow (--bs-table-accent-bg
         sombre). En mode empile on neutralise les deux pour laisser voir le fond
         sombre du <tr>. */
  .p151-stack-table td {
          display: flex;
          align-items: center;
          justify-content: space-between;
          gap: 0.75rem;
          flex: 1 1 100%;
          width: 100%;
          padding: 0.28rem 0;
          border: 0 !important;
          background-color: transparent !important;
          box-shadow: none !important;
          text-align: right !important;
      }
  
  .p151-stack-table td[data-label]::before {
          content: attr(data-label);
          flex: 0 0 auto;
          color: var(--p151-text);
          font-size: 0.82rem;
          font-weight: 600;
          text-transform: uppercase;
          letter-spacing: 0.02em;
      }
  
  /* Valeurs (prix, variation, statut) bien lisibles */
  .p151-stack-table td strong,
  .p151-stack-table td > span {
          font-size: 1rem;
      }
  
  /* --- Premiere ligne de la carte : vignette + titre, sans libelle --- */
  .p151-stack-table td.p151-stack-thumb,
  .p151-stack-table td.p151-stack-head {
          order: -1;
          width: auto;
          justify-content: flex-start;
          text-align: left !important;
          padding-bottom: 0.55rem;
          margin-bottom: 0.35rem;
          border-bottom: 1px solid var(--p151-border) !important;
      }
  
  .p151-stack-table td.p151-stack-thumb {
          order: -2;
          flex: 0 0 auto;
          padding-right: 0.7rem;
      }
  
  .p151-stack-table td.p151-stack-head {
          flex: 1 1 auto;
          min-width: 0;
          font-size: 1.05rem;
          line-height: 1.3;
      }
  
  .p151-stack-table td.p151-stack-head strong {
          font-size: 1.05rem;
          white-space: normal;
          overflow-wrap: anywhere;
      }
  
  /* Cellule « actions » : boutons a taille confortable, alignes a droite. */
  .p151-stack-table td.p151-stack-actions {
          justify-content: flex-end;
          gap: 0.5rem;
          padding-top: 0.5rem;
      }
  
  /* ⛔ Pas de taille propre : un `btn-sm` a la même taille partout (§10.10). */
  
  .p151-stack-table .badge {
          font-size: 0.88rem;
          padding: 0.4em 0.65em;
      }
  
  /* Liens longs (URL de sniper) : coupe plutot que debordement lateral. */
  .p151-stack-table td a {
          overflow-wrap: anywhere;
      }
}

/* ========================================
   /cards — VUE « LISTE » (tableau du catalogue)
   Les classes .catalog-table* n'avaient aucun style : la vignette prenait sa
   taille naturelle (~600px) et faisait exploser la largeur du tableau.
   ======================================== */
.catalog-table th {
    color: var(--p151-text-strong);
    border-color: var(--p151-border);
    white-space: nowrap;
}

.catalog-table td {
    border-color: var(--p151-border);
    vertical-align: middle;
}

.catalog-table-col-image {
    width: 78px;
}

.catalog-table-img {
    width: 46px;
    height: 64px;
    object-fit: contain;
    background: var(--p151-surface-sunken);
    border-radius: 4px;
    cursor: pointer;
}

@media (max-width: 767px) {
  /* Vignette plus grande en mode empile (alignee sur /dashboard) */
  .catalog-table-img {
          width: 52px;
          height: 72px;
      }
}

/* ============================================================================
   MONITEUR DE SCRAPING EBAY — panneau en tete de la sidebar
   ----------------------------------------------------------------------------
   🔴 ÉTAT AU 2026-08-24 : LE BALISAGE EST MIS EN COMMENTAIRE, CE PANNEAU NE
   S'AFFICHE PLUS. Les règles ci-dessous ne s'appliquent donc à rien pour le
   moment. Elles sont CONSERVÉES volontairement : la réactivation se fait en
   décommentant le seul bloc de `layouts/materialize/menu.blade.php` (voir son
   en-tête, qui donne la marche à suivre). Ne pas les supprimer sans supprimer
   aussi ce bloc.

   CE QUE C'EST. Le seul écran de supervision du scraping eBay du catalogue
   TCGdex. Il est rendu par `layouts/materialize/menu.blade.php`, juste au-dessus
   des entrées de menu, et **uniquement pour un administrateur sur /dashboard** :
   c'est `DashboardController` qui pose `$ebayScrapingStats` (null hors admin,
   absente sur toutes les autres pages), depuis `EbayScrapingStatsService`.

   CE QU'IL MONTRE, DE HAUT EN BAS :
     `.psm-today`     — combien de cartes ont été scrapées AUJOURD'HUI, par marché
                        (les « chips » FR / JAP) ;
     `.psm-accordion` — un `.psm-item` repliable par marché, dont l'en-tête donne
                        « à jour / total » et le corps la barre d'avancement, le
                        pourcentage, la cadence de rafraîchissement et le domaine.

   ⚠️ UN MARCHÉ QUOTIDIEN REPART DE 0 % À MINUIT, c'est voulu : le service ancre
   sa période de fraîcheur sur le jour civil (cf. l'en-tête de
   `EbayScrapingStatsService`). Une barre à 12 % le matin n'est donc pas une
   panne — c'est la journée qui commence.

   🔴 L'ANCIEN COMMENTAIRE DE CETTE SECTION EST CADUC. Il disait « Theme sombre
   P151 explicite (le layout est data-theme-mode="light") » : c'était vrai sous
   Sash, qui tournait en mode CLAIR et qu'il fallait repeindre à la main. Depuis
   la bascule du 2026-08-24 le layout est en `data-bs-theme="dark"` NATIF, et ce
   bloc suit les jetons `--p151-*` de la section 1bis comme le reste du
   back-office. Les onze couleurs qu'il écrivait en dur (`#f2eafb`, `#c9a8f0`,
   `#ffffff`, `#7d7d7d`, `#8a8a8a`, `#c2c2c2`, `#8f8f8f`) étaient les derniers
   restes de la palette Sash dans ce fichier.
   ========================================================================= */
.p151-scrape-monitor {
    margin: 6px 12px 14px;
    padding: 12px;
    border-radius: 12px;
    background: var(--p151-surface);
    border: 1px solid var(--p151-border);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
    color: var(--p151-text-strong);
    font-size: 0.78rem;
    line-height: 1.3;
}

.p151-scrape-monitor .psm-title {
    display: flex;
    align-items: center;
    gap: 6px;
    font-weight: 700;
    font-size: 0.8rem;
    letter-spacing: 0.02em;
    color: var(--p151-text-strong);
    margin-bottom: 10px;
}

.p151-scrape-monitor .psm-title i {
    color: var(--bs-primary);
    font-size: 0.95rem;
}

/* ----------------------------------------------------------------------------
   Bandeau « scrapées aujourd'hui » : une pastille par marché QUOTIDIEN.
   Les marchés hebdomadaires (EN, refresh_days = 7) n'y figurent pas — un
   compteur du jour n'y voudrait rien dire. C'est le service qui fait le tri,
   dans `daily_markets`.
   -------------------------------------------------------------------------- */
.p151-scrape-monitor .psm-today-label {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--p151-text-muted);
    margin-bottom: 6px;
}

.p151-scrape-monitor .psm-today-chips {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
}

.p151-scrape-monitor .psm-chip {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 7px 6px;
    border-radius: 9px;
    background: var(--p151-surface-raised);
    border: 1px solid var(--p151-border);
}

/* Pastille : le code langue au-dessus, le compteur en dessous. `flex: 1 1 0` +
   `min-width: 0` sur `.psm-chip` : les pastilles se partagent la largeur à
   parts égales et un grand nombre ne pousse pas la sidebar. */
.p151-scrape-monitor .psm-chip-lang {
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    color: var(--bs-primary);
}

/* ⚠️ `tabular-nums` n'est pas cosmétique : sans chiffres à chasse fixe, un
   compteur qui s'incrémente fait vibrer la largeur de la pastille. */
.p151-scrape-monitor .psm-chip-value {
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--p151-text-strong);
    line-height: 1.1;
    font-variant-numeric: tabular-nums;
}

/* ----------------------------------------------------------------------------
   Accordéon : un `.psm-item` par marché actif de config/ebay.php.
   Le repli est celui de Bootstrap (`data-bs-toggle="collapse"` +
   `data-bs-parent`), donc un seul marché ouvert à la fois.
   -------------------------------------------------------------------------- */
.p151-scrape-monitor .psm-item {
    border-radius: 9px;
    overflow: hidden;
    background: var(--p151-surface);
    border: 1px solid var(--p151-border);
    margin-bottom: 6px;
}

.p151-scrape-monitor .psm-item:last-child {
    margin-bottom: 0;
}

.p151-scrape-monitor .psm-item-head {
    width: 100%;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    background: transparent;
    border: 0;
    color: var(--p151-text-strong);
    cursor: pointer;
    text-align: left;
}

/* 🔴 CETTE RÈGLE NE FAISAIT RIEN. Elle posait au survol `var(--p151-surface)`,
   soit EXACTEMENT le fond que `.psm-item` porte déjà : l'en-tête ne réagissait
   pas, alors que c'est un `<button>` qui plie et déplie le marché. La surface
   légèrement détachée du projet est `--p151-surface-raised`. */
.p151-scrape-monitor .psm-item-head:hover {
    background: var(--p151-surface-raised);
}

.p151-scrape-monitor .psm-item-label {
    font-weight: 700;
    font-size: 0.74rem;
    letter-spacing: 0.04em;
    color: var(--bs-primary);
    min-width: 30px;
}

/* « à jour / total ». `margin-left: auto` le pousse à droite du libellé, avant
   le chevron ; `nowrap` empêche le compte de se couper en deux lignes. */
.p151-scrape-monitor .psm-item-count {
    margin-left: auto;
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    color: var(--p151-text-strong);
    white-space: nowrap;
}

.p151-scrape-monitor .psm-item-count .psm-sep {
    color: var(--p151-text-muted);
    margin: 0 3px;
}

/* Le chevron pivote quand le marché est déplié. Bootstrap pose `.collapsed` sur
   le bouton tant que le panneau est FERMÉ : la rotation se déclenche donc sur
   `:not(.collapsed)`, jamais l'inverse. */
.p151-scrape-monitor .psm-chevron {
    color: var(--p151-text-muted);
    font-size: 0.9rem;
    transition: transform 0.2s ease;
}

.p151-scrape-monitor .psm-item-head:not(.collapsed) .psm-chevron {
    transform: rotate(180deg);
}

.p151-scrape-monitor .psm-item-body {
    padding: 2px 10px 10px;
}

/* Barre d'avancement « % de cartes à jour ». La largeur est écrite en ligne par
   la vue (`style="width: X%"`) : c'est une DONNÉE, pas une couleur — c'est le
   seul `style=` admis ici. */
.p151-scrape-monitor .psm-progress {
    height: 6px;
    border-radius: 6px;
    background: var(--p151-surface-sunken);
    overflow: hidden;
    margin: 4px 0 7px;
}

.p151-scrape-monitor .psm-progress-bar {
    height: 100%;
    border-radius: 6px;
    background: var(--p151-gradient-primary);
    transition: width 0.3s ease;
}

/* Deux lignes de méta sous la barre : pourcentage + cadence, puis compteur du
   jour + domaine eBay. La seconde est en `--muted`, elle est secondaire. */
.p151-scrape-monitor .psm-meta {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    font-size: 0.68rem;
    color: var(--p151-text);
}

.p151-scrape-monitor .psm-meta + .psm-meta {
    margin-top: 3px;
}

.p151-scrape-monitor .psm-meta-muted {
    color: var(--p151-text-muted);
}

/* ----------------------------------------------------------------------------
   SIDEBAR REPLIÉE EN ICÔNES : ON MASQUE LE MONITEUR
   ----------------------------------------------------------------------------
   🔴 CE SÉLECTEUR ÉTAIT MORT DEPUIS LA BASCULE VERS MATERIALIZE, EN SILENCE.
   Il visait `html[data-toggled="icon-overlay-close" | "icon-text" |
   "detached-close"] `, les valeurs du thème **Sash**. Materialize ne pose JAMAIS
   `data-toggled` (vérifié à l'écran le 2026-08-24 : l'attribut vaut `null` sur
   le back-office) — il ajoute la classe `layout-menu-collapsed` sur `<html>`.

   Conséquence constatée : sidebar repliée, le panneau restait affiché et se
   retrouvait écrasé sur **46 px** — titre coupé (« Scrapi eBay »), compteurs
   empilés à la verticale, et trois accordéons réduits à « FI / JI / EI ». Soit
   exactement le rendu cassé que cette règle existe pour éviter.

   ⚠️ MÊME PIÈGE QUE LA BOTTOM BAR MOBILE, plus bas dans ce fichier : Sash
   marquait l'ouverture du menu par `html[data-toggled="open"]`, Materialize par
   `html.layout-menu-expanded`. Celui-là avait été migré, pas celui-ci. Devant
   tout sélecteur en `data-toggled` dans ce projet : il ne correspond à rien.
   -------------------------------------------------------------------------- */
html.layout-menu-collapsed .p151-scrape-monitor {
    display: none;
}

/* ========================================
   MOBILE (< 992px) — HEADER + BOTTOM BAR
   Breakpoint aligne sur le theme Sash (.horizontal-logo masquee >= 992px).
   ======================================== */

/* --- Hamburger tout a droite du header sur mobile ---
   .header-content-left passe en display:contents : ses deux .header-element
   deviennent des flex-items directs de .main-header-container, ce qui permet
   de reordonner le toggle APRES le bloc de droite (credits + profil). */
@media (max-width: 991.98px) {
  .app-header .header-content-left > .header-element:not(.p151-header-toggle) {
          margin-inline-end: auto;
      }
  
  .app-header .header-element.p151-header-toggle {
          order: 3;
          margin-inline-start: 0.25rem;
      }
  
  .app-header .p151-header-credits {
          white-space: nowrap;
          font-size: 0.8rem;
      }
  
  /* Icone carte bancaire du compteur de credits : masquee sur mobile uniquement */
  .app-header .p151-header-credits-icon {
          display: none !important;
      }
}

/* --- Bottom bar mobile : 5 raccourcis de la sidebar --- */
.p151-mobile-bottombar {
    display: none;
}

@media (max-width: 991.98px) {
  .p151-mobile-bottombar {
          position: fixed;
          left: 0;
          right: 0;
          z-index: 1035;
          display: flex;
          align-items: stretch;
          /* --- ANCRAGE : `bottom: 0` ET RIEN D'AUTRE ---
             /!\ NE PAS reintroduire `bottom: max(0px, calc(100% - 100dvh))` (essaye le
             2026-07-29, retire le jour meme). L'idee etait de compenser l'UI du navigateur
             mobile (100% = layout viewport, 100dvh = hauteur visible). Probleme : c'est un
             decalage AVEUGLE. Sur les navigateurs qui placent DEJA la barre correctement,
             il la remonte pour rien -> barre decollee du bas de l'ecran, et le rendu
             variait d'une page a l'autre selon l'etat de la barre d'URL (page courte non
             scrollable = barre d'URL jamais repliee = decalage permanent).
             La compensation est desormais faite UNIQUEMENT par public/js/mobile-bottombar.js,
             qui MESURE la position reellement rendue : il ne bouge la barre que s'il constate
             un vrai depassement, donc rendu identique sur toutes les pages. */
          bottom: 0;
          /* HAUTEUR CONSTANTE : elle ne depend jamais de la longueur des libelles, du
             chargement des polices d'icones ni de la position de scroll. Aucun mode compact. */
          height: calc(64px + env(safe-area-inset-bottom, 0px));
          overflow: hidden;
          /* Legerement plus clair que le fond de page (var(--p151-surface-sunken)) + liseré : la barre reste
             identifiable meme en haut d'une page sombre. */
          background: var(--p151-surface);
          border-top: 1px solid var(--p151-border);
          box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.6);
          padding-bottom: env(safe-area-inset-bottom, 0px);
      }
  
  .p151-mobile-bottombar .p151-bb-item {
          position: relative;
          flex: 1 1 0;
          min-width: 0;
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          gap: 4px;
          padding: 4px 2px;
          color: var(--p151-text);
          text-decoration: none;
          font-size: 0.6rem;
          line-height: 1.15;
          text-align: center;
          transition: color 0.15s ease, background-color 0.15s ease;
      }
  
  .p151-mobile-bottombar .p151-bb-item i {
          font-size: 1.2rem;
          line-height: 1;
      }
  
  .p151-mobile-bottombar .p151-bb-item span {
          display: -webkit-box;
          -webkit-line-clamp: 2;
          -webkit-box-orient: vertical;
          overflow: hidden;
          width: 100%;
          height: 2.3em;
          word-break: break-word;
      }
  
  .p151-mobile-bottombar .p151-bb-item:hover,
  .p151-mobile-bottombar .p151-bb-item:focus {
          color: #ffffff;
      }
  
  .p151-mobile-bottombar .p151-bb-item.active {
          color: #ffffff;
          background: rgba(247, 71, 242, 0.12);
      }
  
  .p151-mobile-bottombar .p151-bb-item.active i {
          color: var(--p151-secondary-color);
      }
  
  .p151-mobile-bottombar .p151-bb-item.active::before {
          content: "";
          position: absolute;
          top: 0;
          left: 15%;
          right: 15%;
          height: 2px;
          border-radius: 0 0 3px 3px;
          background: var(--p151-gradient-primary);
      }
  
  /* Sidebar mobile ouverte (hamburger) : on masque la barre pour ne pas rogner le menu.
     ⚠️ CLASSE CHANGÉE AVEC LE THÈME : Sash marquait l'ouverture par
     `html[data-toggled="open"]`, Materialize pose `html.layout-menu-expanded`
     (cf. `Helpers` du thème). L'ancien sélecteur ne correspondrait plus à rien, en
     silence — la barre resterait affichée par-dessus le menu déplié. */
  html.layout-menu-expanded .p151-mobile-bottombar {
          display: none;
      }

  /* Reserve la hauteur PLEINE de la barre (64px) pour ne jamais masquer le bas du contenu.
     ⚠️ CONTENEUR CHANGÉ AVEC LE THÈME : `.page` était le wrapper Sash ; sous Materialize
     c'est `.content-wrapper`. Les deux sont listés pour rester juste si une vue isolée
     porte encore l'ancien conteneur. */
  body.p151-has-bottombar .content-wrapper,
  body.p151-has-bottombar .page {
          padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px));
      }

  body.p151-has-bottombar .scrollToTop {
          bottom: calc(80px + env(safe-area-inset-bottom, 0px));
      }
}

/* =============================================================================
   BOUTONS D'ACTION SUR LES CARTES TCGDEX
   « Ajouter a ma collection » (.add-to-collection-btn), « Ajouter une alerte »
   (.add-alert-btn) et « Prompts IA » (.ai-prompts-btn) : MEME DESIGN (bouton plein,
   jamais .btn-outline-*). Les 2 premiers en violet P151 uni, le 3e en degrade
   identitaire P151 pour le distinguer (c'est une action d'une autre nature : on
   n'ecrit rien dans le compte, on copie un prompt).

   /!\ Ces regles sont GLOBALES et NON dans un partial de modal
   (cards/_card-alert-modal, cards/_ai-prompts-modal) : les boutons sont rendus sur
   /cards, /dashboard, /trends, /scan et la page detail collection, alors que la modal
   alerte n'etait incluse que sur 2 de ces pages -> ailleurs le bouton « Ajouter une
   alerte » restait sans couleur (INVISIBLE).
   ============================================================================= */
.add-to-collection-btn,
.add-alert-btn {
    font-weight: 600;
}

.add-to-collection-btn:hover,
.add-to-collection-btn:focus,
.add-alert-btn:hover,
.add-alert-btn:focus {
    filter: brightness(1.08);
    color: #ffffff !important;
}

/* « Prompts IA » : gradient identitaire P151 (rose -> violet). */
.ai-prompts-btn {
    font-weight: 600;
}

.ai-prompts-btn:hover,
.ai-prompts-btn:focus {
    filter: brightness(1.08);
    color: #ffffff !important;
}

.ai-prompts-btn i {
    color: inherit !important;
}

/* Etat actif : carte deja en collection / alerte deja posee -> vert */
.add-to-collection-btn.in-collection,
.add-alert-btn.is-active {
    background: var(--bs-secondary) !important;
    border-color: var(--bs-secondary) !important;
    color: #ffffff !important;
}

.add-to-collection-btn.in-collection:hover,
.add-alert-btn.is-active:hover {
    filter: brightness(1.08);
    color: #ffffff !important;
}

.add-to-collection-btn i,
.add-alert-btn i {
    color: inherit !important;
}

/* =========================================================================
   Interrupteur du moteur de reconnaissance des cartes (/admin/settings)

   Permet de couper Ximilar (abonnement payant) et de revenir a l'analyse
   vision seule, sans redeploiement ni suppression de la cle API.
   ========================================================================= */
.p151-engine-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    margin-bottom: 14px;
}

.p151-engine-switch .form-check-label {
    color: var(--p151-text-strong);
    font-size: 14.5px;
    cursor: pointer;
}

/* ⛔ La couleur de la piste n'est plus forcee (2026-08-24). Elle l'etait parce que
   SASH, en mode clair, rendait le switch en BLANC sur fond sombre. Materialize
   peint deja ses interrupteurs, coches comme decoches, avec la couleur primaire :
   les repeindre ici donnait un violet legerement different de celui de tous les
   autres interrupteurs du back-office. Seule la TAILLE reste, c'est un choix de
   densite propre a ce banc. */
.p151-engine-switch .form-check-input {
    width: 2.6em;
    height: 1.35em;
    cursor: pointer;
}

.p151-engine-switch .form-check-input:disabled {
    opacity: .45;
    cursor: not-allowed;
}

.p151-engine-help {
    font-size: 13px;
    color: var(--p151-text);
    line-height: 1.55;
}

.p151-engine-help strong { color: var(--p151-text-strong); }

.p151-engine-note { color: var(--p151-text-muted); }

.p151-engine-warn {
    background-color: #2b2213;
    color: #e0a338;
    border-radius: 8px;
    padding: 9px 12px;
    margin-bottom: 12px;
    font-size: 13px;
}

.p151-engine-warn code {
    background-color: var(--p151-surface-sunken);
    color: #e0a338;
    padding: 1px 5px;
    border-radius: 4px;
}

.p151-engine-bench {
    margin-top: 12px;
    padding: 11px 13px;
    background-color: var(--p151-surface);
    border-radius: 8px;
    font-size: 12.5px;
    color: var(--p151-text-muted);
}

.p151-engine-bench table {
    margin-top: 7px;
    border-collapse: collapse;
}

.p151-engine-bench th,
.p151-engine-bench td {
    padding: 3px 16px 3px 0;
    text-align: left;
    font-weight: 400;
    color: var(--p151-text);
}

.p151-engine-bench th { color: var(--p151-text-muted); font-size: 11.5px; text-transform: uppercase; }

.p151-engine-bench b { color: #4cd08a; }

/* ============================================================================
   6. COMPATIBILITÉ — CLASSES QUE LE THÈME SASH FOURNISSAIT ET QUE MATERIALIZE N'A PAS
   ----------------------------------------------------------------------------
   Relevé mécanique : toutes les classes employées par les 214 vues migrées ont
   été comparées aux feuilles chargées AVANT (Sash + Bootstrap + libs) et APRÈS
   (Materialize + assets_icons + ce fichier). 17 classes étaient définies avant
   et ne l'étaient plus. Celles qui portent vraiment un rendu sont réimplantées
   ici, à l'identique de leur définition Sash.

   Ne sont volontairement PAS reprises :
     - `.icon`, `.header`, `.warning`, `.success`, `.primary`, `.secondary`,
       `.grid` : ces vues (mcp/authorize, test/*, billing/invoice-pdf,
       card-alerts/disable-result) embarquent leur propre <style>, ces noms y
       sont déjà définis localement ;
     - `.page-header-breadcrumb` : chez Sash elle ne faisait que remplacer le
       séparateur de fil d'Ariane par un glyphe « tabler-icons ». Materialize a
       son propre séparateur, et forcer l'ancien exigerait de charger une fonte
       entière pour un chevron ;
     - `.hidden`, `.w-5`, `.h-5` : uniquement dans `vendor/pagination/tailwind`
       et `welcome.blade.php`, deux vues Laravel par défaut que le projet
       n'utilise pas (la pagination du projet est celle de Bootstrap).
   ========================================================================= */

/* Tailles de police en pixels du thème Sash. Bootstrap ne fournit que
   `.fs-1` à `.fs-6` (présentes dans Materialize) : seules ces quatre-là
   manquaient. Valeurs reprises telles quelles de `assets_sash/css/styles.css`. */
.fs-11 {
    font-size: 0.6875rem;
}

.fs-12 {
    font-size: 0.75rem;
}

.fs-18 {
    font-size: 1.125rem;
}

.fs-20 {
    font-size: 1.25rem;
}

/* En-tête de page + son titre (écrans d'administration). */
.page-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    justify-content: space-between;
    margin: 1.5rem 0;
    padding: 0;
}

.page-title {
    margin: 0 0 0.2rem;
    font-size: 20px;
    font-weight: 500;
    position: relative;
}

/* `.custom-card` était la variante de carte du thème Sash. Sous Materialize,
   `.card` porte déjà la surface, l'ombre et le rayon : il ne reste que la
   largeur et la marge basse, que Sash posait sur cette classe. */
.card.custom-card {
    width: 100%;
    margin-block-end: 1.5rem;
}

/* Retirée par Bootstrap 5.3 au profit des utilitaires d'opacité, mais encore
   employée dans affiliate/dashboard. */
.text-white-50 {
    color: rgba(255, 255, 255, 0.5) !important;
}

/* ----------------------------------------------------------------------------
   7. `.text-black` SUR LES FONDS D'ACCENT — LISIBILITÉ
   ----------------------------------------------------------------------------
   54 en-têtes de carte du back-office sont écrits `card-header bg-primary
   text-black`, plus 14 sur `bg-info` / `bg-success`. C'est le bon choix : la
   palette sombre de Materialize est VIVE, et le texte noir y contraste mieux
   que le blanc (mesuré : sur `bg-primary` #a855f7, noir = 5,31:1 contre 3,96:1
   pour le blanc ; sur `bg-success` #72e128, noir = 12,52:1). Les passer en
   blanc DÉGRADERAIT la lisibilité.

   Le problème est ailleurs : Materialize redéfinit `--bs-black-rgb` en
   `38, 43, 67` (un bleu nuit, pas du noir), et `.text-black` de Bootstrap est
   construit dessus. Le texte sortait donc en #262B43 sur violet, soit 3,53:1 —
   sous le seuil AA de 4,5:1, et visiblement délavé à l'écran.

   On rend donc `.text-black` au noir réel, uniquement là où une vue le demande
   explicitement. Vérifié avant d'écrire cette règle : dans tout le projet,
   `.text-black` n'est jamais posé sur une surface sombre — toujours sur
   `bg-primary`, `bg-info`, `bg-success` ou `btn-info`.
   -------------------------------------------------------------------------- */

.text-black {
    color: #000 !important;
}

/* ----------------------------------------------------------------------------
   8. CARTE DE SIGNAL — VEILLE MARCHÉ
   ----------------------------------------------------------------------------
   Le composant `<x-market-news.signal-card>` construisait sa surface dans un
   style en ligne, avec la palette Sash. Les deux variantes gardent leur rapport
   d'origine, qui est intentionnel :
     - `--compact` est posée DANS une carte (sidebar du tableau de bord, modale
       détail carte) : elle prend le fond « en creux », un cran plus sombre que
       la carte qui la porte, pour se détacher sans introduire de troisième teinte ;
     - la variante pleine est posée directement sur la page (/market-news) :
       elle prend la teinte d'une carte, comme tout ce qui flotte sur le fond.
   -------------------------------------------------------------------------- */

.p151-signal-card {
    background: var(--p151-surface);
    border: 1px solid var(--p151-border);
    border-radius: 0.5rem;
    padding: 1rem;
    margin-block-end: 0.75rem;
}

.p151-signal-card--compact {
    background: var(--p151-surface-sunken);
    border-radius: 0.4rem;
    padding: 0.6rem 0.75rem;
    margin-block-end: 0.5rem;
}

/* ----------------------------------------------------------------------------
   9. `.p151-zone` — L'UNIQUE SURFACE LÉGÈREMENT DÉTACHÉE
   ----------------------------------------------------------------------------
   Un encart posé DANS une carte et qu'on veut distinguer d'elle : bloc d'aide,
   résumé, zone de code. Une seule classe pour tout le back-office, afin qu'un
   encart ressemble au suivant.

   ⛔ ELLE REMPLACE `bg-light`, QU'IL NE FAUT PLUS UTILISER ICI. `bg-light` est un
   utilitaire de thème CLAIR : sur la carte blanche de Sash il donnait un gris
   très pâle, discret. Sous Materialize il rend `rgb(70,68,91)`, soit PLUS CLAIR
   que la carte qui le porte (`rgb(48,51,78)`) — l'encart ressortait comme un
   bloc rapporté. C'est le défaut relevé sur la barre de filtres de /cards le
   2026-08-24, et il touchait 51 emplacements.

   ⚠️ Une bande qui porte DÉJÀ une bordure (`border-bottom`, `border`…) n'a pas
   besoin de cette classe : le filet suffit à la séparer, et c'est la façon dont
   le reste du back-office délimite ses zones.
   -------------------------------------------------------------------------- */

.p151-zone {
    background-color: var(--p151-surface-raised);
    border-radius: 0.5rem;
}

/* ============================================================================
   10. GRAMMAIRE VISUELLE COMMUNE DES ÉCRANS UTILISATEUR (2026-08-24)
   ----------------------------------------------------------------------------
   🔴 POURQUOI CETTE SECTION EXISTE. La bascule Sash → Materialize n'avait touché
   que la ligne `@extends` des vues : elles ont hérité d'un thème homogène, mais
   ont gardé leur mise en page d'origine, écrite écran par écran sur plusieurs
   années. Relevé mécanique sur les 48 écrans accessibles depuis la sidebar :

     - 9 façons différentes d'annoncer le titre de la page (`h1`, `h1.h3`,
       `h3.card-title`, `h4`, `h4.card-title.mb-0`, `h4.mb-0`, un `div.card-title`
       sans balise de titre, un `<i>` + texte nu dans un `card-header bg-primary`,
       et 4 écrans sans titre du tout) ;
     - 4 façons d'écrire un sous-titre, dont deux en `style=` ;
     - des tailles de police de titre allant de 15 px (`div.card-title`) à 57 px
       (`h1.display-4` de /subscribe) pour la MÊME fonction ;
     - des boutons de barre de titre en trois tailles mélangées sur la même ligne
       (`btn-lg` du bouton YouTube à côté d'un `btn` et d'un `btn-sm`).

   La règle qui en sort, et qu'il faut tenir :

     ① LE TITRE DE LA PAGE VIT AU-DESSUS DES CARTES, via `<x-page-header>`.
       Un `<h1>` par page, toujours à la même taille, toujours au même endroit.
     ② L'INTITULÉ D'UNE CARTE EST UN `h5.card-title.mb-0` dans son `.card-header`.
       C'est un intitulé de BLOC, pas un titre de page : il ne doit jamais être
       plus gros que le `<h1>` au-dessus de lui.
     ③ LES BOUTONS D'UNE MÊME BARRE ONT LA MÊME TAILLE. On ne mélange pas
       `btn-lg`, `btn` et `btn-sm` sur une même ligne.
     ④ TROIS VARIANTES, ET TROIS SEULEMENT, DANS UNE BARRE DE TITRE :
         action principale ....... `btn-primary`
         action secondaire/retour . `btn-secondary`
         action destructrice ..... `btn-danger`

   ⛔ NE PAS UTILISER `btn-label-*` NI `btn-outline-*` POUR UN BOUTON D'ACTION.
   Contrastes mesurés au navigateur sur ce back-office, texte de bouton (seuil
   AA = 4,5:1) :

     | variante                | texte / fond            | contraste |
     |-------------------------|-------------------------|-----------|
     | `btn-label-secondary`   | #6D788D sur #3A3E58     | **2,35**  |
     | `btn-label-primary`     | #A855F7 sur #433869     | **2,64**  |
     | `btn-outline-secondary` | #6D788D sur #282A42     | **3,15**  |
     | `btn-outline-primary`   | #A855F7 sur #282A42     | **3,54**  |
     | `btn-secondary`         | #FFFFFF sur #6D788D     | 14,00     |

   Les quatre premières ratent le seuil, et à l'écran un bouton d'action y
   ressemble à un bouton DÉSACTIVÉ — c'est exactement ce que donnaient
   « Marquer tous comme lus » et « Recevoir par mail » sur /guide. La seule
   variante non-primaire lisible du thème est `btn-secondary`.

   ⚠️ Après toute modification de ce fichier : incrémenter `asset_version`
      (`config/app.php`).
   ========================================================================= */

/* ----------------------------------------------------------------------------
   10.1 EN-TÊTE DE PAGE — `<x-page-header>`
   -------------------------------------------------------------------------- */

.p151-page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem 1rem;
    margin-block-end: 1.5rem;
}

.p151-page-header-text {
    min-width: 0; /* sinon un titre long empêche le flex de se replier */
}

/* Vignette d'identité de la page (slot `media`) : image d'une collection, d'une
   carte PCA… Taille fixe, sinon un visuel portrait fait grandir l'en-tête. */
.p151-page-media {
    flex-shrink: 0;
    inline-size: 48px;
    block-size: 48px;
    border-radius: 0.5rem;
    overflow: hidden;
    background: var(--p151-surface-sunken);
    border: 1px solid var(--p151-border);
}

.p151-page-media img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
    display: block;
}

/* Variante « pictogramme » du slot media : une catégorie de forum n'a pas
   d'image, elle a une icône. Même gabarit que la vignette, pour que les deux
   écrans s'alignent sur la même hauteur d'en-tête. */
.p151-page-media-glyph {
    display: flex;
    align-items: center;
    justify-content: center;
    inline-size: 100%;
    block-size: 100%;
    font-size: 1.25rem;
    color: var(--bs-primary);
}

/* Taille UNIQUE du titre de page pour tout le back-office. 1.375rem = 22 px :
   au-dessus du `h5` des cartes (1.125rem) sans écraser la page comme le faisait
   le `display-4` de /subscribe (57 px). */
.p151-page-title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin: 0;
    font-size: 1.375rem;
    font-weight: 600;
    line-height: 1.3;
    color: var(--p151-text-strong);
}

.p151-page-title i {
    color: var(--bs-primary);
    font-size: 1.25rem;
    line-height: 1;
    flex-shrink: 0;
}

.p151-page-subtitle {
    margin: 0.35rem 0 0;
    font-size: 0.875rem;
    line-height: 1.5;
    color: var(--p151-text-muted);
    max-width: 78ch; /* une explication reste lisible, elle ne traverse pas l'écran */
}

.p151-page-subtitle a {
    color: var(--bs-primary);
}

/* Barre d'actions du titre. `align-items: center` + hauteur homogène : c'est ce
   qui met réellement les boutons sur la même ligne de base, quel que soit leur
   contenu (icône seule, icône + texte, texte long). */
.p151-page-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-inline-start: auto;
}

@media (max-width: 575.98px) {
    .p151-page-header {
        margin-block-end: 1.25rem;
    }

    .p151-page-title {
        font-size: 1.1875rem;
    }

    /* Sur mobile les actions passent sous le titre et prennent toute la largeur :
       deux demi-boutons côte à côte sont illisibles sur 360 px. */
    .p151-page-actions {
        margin-inline-start: 0;
        width: 100%;
    }

    .p151-page-actions > .btn,
    .p151-page-actions > * > .btn {
        flex: 1 1 auto;
        justify-content: center;
    }
}

/* ----------------------------------------------------------------------------
   10.2 INTITULÉ DE CARTE — TAILLE UNIQUE
   ----------------------------------------------------------------------------
   Materialize donne à `.card-title` la taille du `<h*>` qui la porte : un
   `h3.card-title` sortait à 24 px et un `div.card-title` à 15 px, pour la même
   fonction. On fixe la taille sur la CLASSE, la balise ne décide plus.

   ⚠️ Volontairement limité à `.card-header` : `.card-title` sert aussi de titre
   de vignette dans les grilles (guides, fonctionnalités), où il doit garder la
   taille de son contexte.
   -------------------------------------------------------------------------- */

.card-header > .card-title,
.card-header .card-title {
    font-size: 1.125rem;
    font-weight: 600;
    line-height: 1.4;
    color: var(--p151-text-strong);
}

/* Icône d'un intitulé de carte : même traitement que celle du titre de page. */
.card-header .card-title > i,
.card-header .card-title > .bi,
.card-header .card-title > .fe {
    color: var(--bs-primary);
}

/* Un en-tête de carte coloré (`bg-primary`, `bg-info`…) porte son propre texte :
   l'icône y suit la couleur du texte, pas l'accent — sinon violet sur violet. */
.card-header[class*='bg-'] .card-title,
.card-header[class*='bg-'] .card-title > i {
    color: inherit;
}

/* ----------------------------------------------------------------------------
   10.3 BARRE D'ACTIONS D'UN EN-TÊTE DE CARTE
   ----------------------------------------------------------------------------
   `.card-header-actions` venait de CoreUI : elle n'existe ni dans Sash ni dans
   Materialize, et 6 écrans s'en servaient encore pour pousser leurs boutons à
   droite — sans effet, les boutons tombaient donc SOUS le titre. On lui rend le
   comportement attendu plutôt que de la retirer de chaque vue.
   -------------------------------------------------------------------------- */

.card-header-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-inline-start: auto;
}

/* ----------------------------------------------------------------------------
   10.4 BOUTONS — ALIGNEMENT
   ----------------------------------------------------------------------------
   Un `.btn` de Bootstrap est un `inline-block` : une icône et un texte de
   hauteurs différentes y décalent la ligne de base, et deux boutons voisins ne
   s'alignent plus au pixel. En flex centré, ils s'alignent toujours.

   ⛔ On ne touche NI aux couleurs NI aux fonds : ce sont ceux du thème.
   -------------------------------------------------------------------------- */

.p151-page-actions .btn,
.card-header-actions .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
}

/* ----------------------------------------------------------------------------
   10.5 BOUTON-TUILE — `.p151-tile-btn`
   ----------------------------------------------------------------------------
   Un bouton carré « icône au-dessus, libellé en dessous », utilisé par la grille
   de raccourcis de /download. Les six tuiles portaient chacune trois `style=`
   (`height: 80px`, `font-size: 24px`, `font-size: 12px`) recopiés à la main :
   une seule faute de frappe et une tuile sortait d'un demi-centimètre.
   -------------------------------------------------------------------------- */

.p151-tile-btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    inline-size: 100%;
    block-size: 80px;
    gap: 0.25rem;
    text-align: center;
}

.p151-tile-btn i {
    font-size: 24px;
    line-height: 1;
}

.p151-tile-btn span {
    font-size: 0.75rem;
    font-weight: 600;
    line-height: 1.2;
}

/* ----------------------------------------------------------------------------
   10.6 GUIDES
   ----------------------------------------------------------------------------
   Ces règles vivaient en trois exemplaires, dans les `<style>` de guides/index,
   guides/category et guides/show — avec des valeurs DIFFÉRENTES pour la même
   classe (`.guide-category-img` : 350 px sur l'index, 250 px + largeur fixe de
   575 px sur la catégorie), et des couleurs de thème CLAIR en dur (`#ddd`,
   `#f2f2f2`) qui sortaient un tableau gris pâle au milieu d'une carte sombre.
   Une seule définition, avec les jetons du thème.
   -------------------------------------------------------------------------- */

.guide-category-img {
    block-size: 250px;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    border-radius: 0.5rem;
}

/* Bascule « lu / non lu » d'une ligne de guide.

   ⛔ ON NE REPEINT PAS L'ICÔNE. La règle précédente la forçait à
   `var(--p151-text-muted)` (`#7b7d95`) à l'intérieur d'un `btn-secondary`
   (`#6d788d`) : **1,1:1**, mesuré au navigateur — un rectangle gris vide, rien
   d'autre. Le bouton porte déjà sa couleur de texte (`#fff`, 4,45:1) : l'icône
   n'a qu'à en hériter.

   L'état se dit donc par la VARIANTE du bouton, conformément à la doctrine des
   boutons (section 10.10), et la glyphe le redit — lisible même sans couleur :

     non lu → `btn-secondary`     + `bi-check-circle`      — blanc sur gris,  4,45:1
     lu     → `btn-label-success` + `bi-check-circle-fill` — vert sur vert, 12,49:1

   ⚠️ `btn-label-success` est l'exception documentée au refus des variantes
   `btn-label-*` : le tableau de la section 10.10 mesure `btn-label-secondary` à
   2,35:1 et `btn-label-primary` à 2,64:1, mais `btn-label-success` sort à
   **12,49:1** (`#72e128` sur `#3a4f48`). Ne pas lui substituer `btn-success`, qui
   met du blanc sur le vert vif du thème : **1,68:1**.

   ⚠️ Pas de `font-size` ici : `.btn > .bi` (section 10.10) impose
   `font-size: inherit` pour que le bouton garde la hauteur du thème. Le `1.2rem`
   qui figurait ici n'était déjà plus appliqué. */
.guide-read-icon {
    color: inherit;
    display: inline-block;
}

/* Intitulé du guide. Sans cette règle, une ligne LUE perd la couleur posée par
   `.guide-unread > a` et retombe sur la couleur de lien de Bootstrap : le violet.
   Résultat à l'écran, exactement à l'envers de l'intention — les guides déjà lus
   ressortaient en violet, les non lus en blanc. Seule la GRAISSE distingue les
   deux états. */
.guide-links .list-group-item > a {
    color: var(--p151-text);
}

/* Guide NON LU. L'état de lecture se signale comme dans une boîte mail : un
   liséré d'accent et un titre affirmé — JAMAIS un aplat de couleur sur la ligne.
   ⛔ L'ancienne règle peignait la ligne entière en `var(--p151-secondary-color)`
   (#f747f2, le rose vif de la marque) et en `!important` : criard, illisible, et
   hors de la palette du back-office (cf. § 1bis). Ne pas la réintroduire.
   ⚠️ Le liséré passe par `box-shadow: inset` et NON par `border-left` :
   `.guide-links .list-group-item` remet justement `border-left` à `none`, et une
   bordure décalerait le texte de 3 px entre une ligne lue et une ligne non lue.
   ⚠️ Pas de `!important` sur le fond : la teinte doit céder au survol, dont la
   règle `.guide-links .list-group-item:hover` est plus spécifique. */
.guide-unread {
    background-color: var(--bs-primary-bg-subtle);
    box-shadow: inset 3px 0 0 var(--bs-primary);
}

/* ⚠️ Sélecteur volontairement aussi spécifique que la règle de couleur ci-dessus
   (`.guide-links .list-group-item > a`, 0-2-1) : écrit `.guide-unread > a` (0-1-1),
   il perdait, et un guide non lu ressortait dans la même couleur qu'un guide lu. */
.guide-links .guide-unread > a {
    color: var(--p151-text-strong);
    font-weight: 600;
}

/* Corps d'un guide : du HTML libre saisi en back-office. Les tableaux qu'il
   contient doivent prendre l'habillage du thème, pas un gris de thème clair. */
.guide-content {
    line-height: 1.6;
}

.guide-content img {
    max-inline-size: 100%;
    block-size: auto;
    margin: 1rem 0;
}

.guide-content table {
    inline-size: 100%;
    border-collapse: collapse;
    margin: 1rem 0;
}

.guide-content table,
.guide-content th,
.guide-content td {
    border: 1px solid var(--p151-border);
}

.guide-content th,
.guide-content td {
    padding: 0.5rem;
    text-align: start;
}

.guide-content th {
    background-color: var(--p151-surface-raised);
    color: var(--p151-text-strong);
}

/* ----------------------------------------------------------------------------
   10.7 GRILLE DE PRIX — /subscribe et /buy-credits
   ----------------------------------------------------------------------------
   🔴 Ces règles vivaient dans un `@section('styles')` de subscriptions/plans.
   Le layout du back-office expose `@stack('styles')`, PAS `@yield('styles')` :
   ce bloc n'était rendu NULLE PART. Le prix sortait donc à la taille par défaut
   d'un `<h1>` et le bouton d'abonnement n'avait pas son `position: relative`
   (son spinner se posait au coin de la page). Trois autres vues étaient dans le
   même cas — cf. la note de la Partie IX de ARCHITECTURE-PROJECT151.md.

   ⛔ Dans une vue du back-office : `@push('styles')` / `@push('scripts')`,
   jamais `@section`.
   -------------------------------------------------------------------------- */

.p151-pricing-card .card-title {
    font-size: 2.25rem;
    font-weight: 600;
    line-height: 1.2;
    color: var(--p151-text-strong);
}

.p151-pricing-card .card-header .card-title {
    font-size: 1.25rem;
    font-weight: 500;
}

/* Le bouton porte un spinner en position absolue pendant l'appel à Stripe. */
.subscription-button {
    position: relative;
}

.subscription-button .spinner-border {
    position: absolute;
    inset-inline-end: 10px;
    inset-block-start: 50%;
    transform: translateY(-50%);
}

.p151-pricing-card {
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.p151-pricing-card:hover {
    transform: translateY(-4px);
}

/* ----------------------------------------------------------------------------
   10.8 CHOIX DE MODE — `.btn-mode` (saisie d'une transaction)
   ----------------------------------------------------------------------------
   Quatre boutons qui expriment UN choix : même taille, même variante au repos
   (`btn-secondary`), et c'est l'état `.active` — posé par le JS de la vue — qui
   dit lequel est retenu. C'est la règle du projet : un état actif se dit par la
   variante, jamais par un fond écrit à la main.
   -------------------------------------------------------------------------- */

.btn-mode.active {
    background-color: var(--bs-primary);
    border-color: var(--bs-primary);
    color: var(--bs-primary-contrast, #fff);
}

/* ----------------------------------------------------------------------------
   10.9 TUILE DE STATISTIQUE — `<x-stat-tile>`
   ----------------------------------------------------------------------------
   Un chiffre-clé se lit toujours à la même taille, quel que soit l'écran. Voir
   l'en-tête du composant pour le relevé des quatre gabarits qu'elle remplace.
   -------------------------------------------------------------------------- */

.p151-stat {
    block-size: 100%;
    margin-block-end: 1rem;
}

.p151-stat-body {
    display: flex;
    align-items: center;
    gap: 0.875rem;
    padding: 1rem;
}

.p151-stat-icon {
    font-size: 1.75rem;
    line-height: 1;
    flex-shrink: 0;
    opacity: 0.85;
}

.p151-stat-text {
    min-width: 0;
}

/* Taille UNIQUE du chiffre : 1.75rem = 28 px. Assez présent pour se détacher,
   sans le `display-5` (48 px) qui écrasait le reste de l'écran /earnings. */
.p151-stat-value {
    font-size: 1.75rem;
    font-weight: 600;
    line-height: 1.15;
    word-break: break-word;
}

.p151-stat-label {
    font-size: 0.8125rem;
    line-height: 1.35;
    opacity: 0.85;
}

/* Sur une tuile sans fond d'accent, le libellé prend le gris de texte du thème
   (sur fond d'accent il hérite du noir, et `--p151-text-muted` y serait illisible). */
.p151-stat:not([class*='bg-']) .p151-stat-label {
    color: var(--p151-text-muted);
    opacity: 1;
}

.p151-stat:not([class*='bg-']) .p151-stat-icon {
    color: var(--bs-primary);
}

/* ----------------------------------------------------------------------------
   10.10 BOUTONS — UNE SEULE TAILLE, DEUX ACCENTS AU-DESSUS DU NEUTRE (2026-08-24)
   ----------------------------------------------------------------------------
   🔴 CETTE SECTION ÉTEND LA RÈGLE ④ DE LA SECTION 10 À *TOUS* LES BOUTONS D'UN
   ÉCRAN, et plus seulement à ceux de la barre de titre.

   Relevé du 2026-08-24 sur /accounting, /boutiques-tcg, /market-news, /trends,
   /collections et /scan : onze apparences de bouton pour six écrans — `btn-info`,
   `btn-warning`, `btn-success`, `btn-outline-secondary`, `btn-outline-primary`,
   `btn-outline-danger`, trois variantes maison à dégradé (`.scan-btn-primary`,
   `.scan-btn-analyze`, `.scan-batch-confirm`) et cinq pastilles à couleur écrite en
   dur, une par site marchand (#b02c30, #7a3033, #0a2540…). Sur /scan, la colonne
   « Liens » sortait en carrés de 30 px juste à côté d'un `btn-sm` de 31 px de haut
   dans la colonne « Actions » : deux tailles voisines mais différentes, le pire des
   cas — l'œil voit le décalage sans pouvoir le nommer.

   LA RÈGLE, EN DEUX POINTS :

     ① UNE SEULE TAILLE : `btn-sm`. Partout — en-tête de page, barre de filtres,
       ligne de tableau, vignette, modale. Un `.btn` sans `btn-sm` dépasse ses
       voisins d'environ 7 px de haut : c'est assez pour casser une ligne.

     ② DEUX ACCENTS AU-DESSUS DU NEUTRE, ET DEUX SEULEMENT :
         `btn-secondary` ... la BASE. Tout ce qui n'est ni création ni suppression :
                             voir, modifier, retour, réinitialiser un filtre, lien
                             externe, annuler.
         `btn-primary` ..... l'action PRINCIPALE de l'écran (créer, filtrer, analyser,
                             valider). Une par barre, pas trois.
         `btn-danger` ...... la suppression, et rien d'autre.

   ⛔ INTERDITS SUR UN BOUTON D'ACTION : `btn-info`, `btn-warning`, `btn-success`
   — ils codent une couleur, pas une intention : « Modifier » n'est pas un
   avertissement ; `btn-outline-*` et `btn-label-*` — contrastes de 2,3 à 3,5:1,
   mesurés en tête de section 10, ils ressemblent à des boutons DÉSACTIVÉS ; et
   toute variante maison à fond ou dégradé écrit en dur.

   ⛔ UN LIEN EXTERNE SE NOMME. Dans une colonne « Liens », une icône seule — pire,
   une pastille de couleur — n'apprend rien : on écrit le NOM du site (« eBay »,
   « Cardmarket », « Vinted »), l'icône restant décorative. Quand le site n'est pas
   connu à l'avance : `\App\Helpers\TextHelper::siteName($url)`.

   ✅ CONTRÔLE AVANT DE DIRE QUE C'EST FINI. Sur l'écran : relever la hauteur de
   chaque `.btn` visible — une seule valeur attendue — puis compter les couleurs de
   fond : au plus trois (le gris de `btn-secondary`, le violet de `btn-primary`, le
   rouge de `btn-danger`).

   ⚠️ Après toute modification de ce fichier : incrémenter `asset_version`
      (`config/app.php`).
   -------------------------------------------------------------------------- */

/* 🔴 RANGÉE D'ACTIONS — `.p151-actions`, JAMAIS `.btn-group`.
   `btn-group` SOUDE les boutons : coins internes carrés, écart nul. Une colonne
   « Actions » ainsi bâtie ne ressemble à aucune autre liste du SaaS, où les
   commandes sont des boutons SÉPARÉS gardant chacun leur rayon. 12 groupes étaient
   dans ce cas au 2026-08-24 (dont /accounting, /snipers, /pca-cards et le tableau
   des transactions). `.p151-actions` les remplace partout — y compris pour un
   filtre segmenté ou une bascule `btn-check`, dont l'état actif se dit par la
   variante `btn-primary`. */
.p151-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.25rem;
}

/* Un bouton d'action est un conteneur flex centré : icône et libellé s'alignent
   quel que soit leur poids, y compris dans une cellule de tableau. La section 10.4
   le faisait déjà pour les deux barres de titre ; les colonnes d'actions et les
   grilles de liens en avaient autant besoin. */
.p151-actions .btn,
.scan-links .btn,
.collection-card__actions .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
}

/* Bascule `btn-check` (radio déguisé en bouton) : coché = variante principale.
   Les vues portaient `btn-outline-success` / `btn-outline-info`, deux couleurs
   hors palette et illisibles au repos. */
.btn-check:checked + .btn {
    background-color: var(--bs-primary);
    border-color: var(--bs-primary);
    color: var(--bs-primary-contrast, #fff);
}

/* 🔴 CE QUE CONTIENT UN BOUTON NE DOIT PAS CHANGER SA HAUTEUR. Trois pièges,
   tous mesurés au navigateur le 2026-08-24, tous invisibles à la lecture du code :

     ① ICÔNE + LIBELLÉ → 36,3 px au lieu de 33,3 (`/subscribe`). Un `.btn` est un
       `inline-block` ; le `vertical-align: -.125em` des Bootstrap Icons descend la
       boîte de l'icône SOUS la ligne de base et gonfle la boîte de ligne. En flex
       centré, l'icône ne peut plus pousser la hauteur.
     ② ICÔNE SEULE → 29,8 px (`/collections`). Sans nœud de texte, la boîte se
       replie sur la hauteur de la GLYPHE (14,3 px) au lieu de la hauteur de LIGNE
       du thème (17,9 px).
     ③ ICÔNE SEULE PLUS GROSSE QUE LA LIGNE → 34,7 px (`/guide` : `.guide-read-icon`
       en `1.2rem`, soit une glyphe de 19,2 px). ⚠️ `line-height: inherit` ne suffit
       PAS : la valeur héritée est un NOMBRE (1.375), multiplié par la taille de
       police de l'ENFANT — il fallait aussi hériter `font-size`.

   `.p151-tile-btn` est exclue : c'est une tuile carrée de 80 px à icône de 24 px
   (section 10.5), sa grande icône est voulue. */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Plancher de hauteur calculé sur les jetons du thème : un bouton ne peut plus
       se replier sous la hauteur qu’il aurait avec du texte, quel que soit son
       contenu (icône seule, pastille, image). */
    min-height: calc(var(--bs-btn-line-height) * var(--bs-btn-font-size)
                   + 2 * var(--bs-btn-padding-y)
                   + 2 * var(--bs-btn-border-width));
}

.btn:not(.p151-tile-btn) > i,
.btn:not(.p151-tile-btn) > .bi,
.btn:not(.p151-tile-btn) > .fe {
    line-height: inherit;
    font-size: inherit;
}

/* Même cause, autre contenu : une PASTILLE posée dans un bouton (compteur,
   mention « -20 % ») fait 20,8 px avec son padding vertical, contre 17,9 px de
   hauteur de ligne : le bouton sortait à 36,3 px sur /subscribe. */
.btn > .badge {
    padding-block: 0;
    line-height: var(--bs-btn-line-height);
}

/* ----------------------------------------------------------------------------
   10.11 VIGNETTE DE COLLECTION — /collections
   ----------------------------------------------------------------------------
   Ces règles vivaient dans un `<style>` à l'intérieur de la vue — interdit, les
   styles vont dans un fichier CSS. Trois d'entre elles repeignaient les boutons
   d'action de la vignette (fond `--p151-surface-sunken`, texte `#e6e6e6` pour
   `btn-outline-secondary`, `#ff6b6b` pour `btn-outline-danger`) parce que les
   variantes `outline` du thème sont illisibles. La bonne réponse n'était pas de les
   repeindre mais de ne pas les employer : ces boutons sont désormais des
   `btn btn-sm btn-secondary` / `btn btn-sm btn-danger` (section 10.10), et il n'y a
   plus rien à corriger ici.
   -------------------------------------------------------------------------- */

.collection-card {
    border: 1px solid rgba(168, 85, 247, .20);
    border-radius: 16px;
    /* Teinte identitaire P151 (violet/rose) : clairement thémée, pas blanche. */
    background: linear-gradient(135deg, rgba(247, 71, 242, .07) 0%, rgba(168, 85, 247, .07) 100%);
    padding: 16px 12px 12px;
    text-align: center;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    transition: box-shadow .15s ease, transform .15s ease, border-color .15s ease;
}

.collection-card:hover {
    border-color: rgba(168, 85, 247, .45);
    box-shadow: 0 8px 24px rgba(168, 85, 247, .16);
    transform: translateY(-2px);
}

.collection-card__img {
    width: 88px;
    height: 88px;
    border-radius: 14px;
    background: var(--p151-surface-sunken);
    border: 1px solid rgba(168, 85, 247, .15);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 12px;
    margin-bottom: 12px;
}

.collection-card__img img {
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
}

.collection-card__name {
    font-weight: 600;
    font-size: .9rem;
    line-height: 1.25;
    margin-bottom: 12px;
    color: var(--p151-text-strong);
    width: 100%;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
    min-height: 2.4em;
}

.collection-card__count {
    font-size: .78rem;
    color: var(--p151-text);
    margin-bottom: 10px;
}

/* Collection « série à compléter » : badge + mini barre de progression. */
.collection-card.is-series {
    border-color: rgba(247, 71, 242, .35);
}

.collection-card__badge {
    font-size: .6rem;
    font-weight: 700;
    letter-spacing: .06em;
    color: #fff;
    background: var(--p151-gradient-primary);
    border-radius: 8px;
    padding: 2px 8px;
    margin-bottom: 8px;
}

.collection-card__progress {
    width: 100%;
    height: 6px;
    border-radius: 6px;
    background: var(--p151-surface);
    overflow: hidden;
    margin-bottom: 10px;
}

.collection-card__progress span {
    display: block;
    height: 100%;
    border-radius: 6px;
    background: linear-gradient(90deg, var(--bs-primary) 0%, var(--p151-secondary-color) 100%);
}

/* Barre d'actions de la vignette : on ne déclare QUE la mise en page. Les trois
   boutons sont des variantes du thème, elles se peignent toutes seules. */
.collection-card__actions {
    display: flex;
    gap: 8px;
    margin-top: auto;
}

.collection-empty-icon {
    width: 72px;
    height: 72px;
    border-radius: 50%;
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--p151-gradient-primary);
    color: #fff;
    font-size: 1.8rem;
}

/* ============================================================================
   FREEMIUM — /admin/settings, onglets et tableau des features (2026-08-26)
   ----------------------------------------------------------------------------
   ⛔ AUCUNE COULEUR DE SURFACE, DE BORDURE NI DE TEXTE N'EST ÉCRITE ICI. Les
   panneaux sont des `.card` (peintes par le thème), les encarts des `.p151-zone`
   (l'unique surface détachée du projet), les pastilles des `bg-label-*`. Tout ce
   qui suit est de la MISE EN PAGE — et le seul jeton employé est `--p151-border`,
   pour un filet de séparation.

   ⚠️ Après toute modification de ce fichier : incrémenter `ASSET_VERSION` dans
   `config/app.php`, sinon le navigateur sert la feuille en cache et la
   modification est invisible (piège déjà payé sur ce projet).
   ========================================================================== */

/* Barre d'onglets : un filet la sépare du contenu, rien d'autre. */
.p151-settings-tabs {
    margin-bottom: 1.25rem;
    flex-wrap: wrap;
}

.p151-settings-panes {
    /* Le contenu d'un onglet ne doit pas coller à la barre. */
    padding-top: 0.25rem;
}

/* Une feature = un encart détaché, séparé du suivant. */
.p151-freemium-feature {
    padding: 1rem;
    margin-bottom: 0.75rem;
}

.p151-freemium-feature:last-child {
    margin-bottom: 0;
}

/* En-tête : identité à gauche, interrupteur collé à droite.
   `gap` plutôt que des marges : la ligne se replie proprement sur mobile. */
.p151-freemium-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.p151-freemium-ident {
    display: flex;
    align-items: center;
    flex: 1 1 260px;
    min-width: 0;
}

.p151-freemium-desc {
    margin: 0.5rem 0 0.25rem;
}

.p151-freemium-routes {
    margin-bottom: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

/* Bloc des actions facturées, en retrait sous la feature qui les déclenche. */
.p151-freemium-actions {
    margin-top: 0.85rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--p151-border);
}

.p151-freemium-actions-title {
    font-weight: 600;
    margin-bottom: 0.6rem;
}

.p151-freemium-action {
    display: flex;
    align-items: flex-start;
    flex-wrap: wrap;
    gap: 0.75rem;
    padding: 0.5rem 0;
}

.p151-freemium-action + .p151-freemium-action {
    border-top: 1px solid var(--p151-border);
}

.p151-freemium-action-ident {
    flex: 1 1 280px;
    min-width: 0;
}

.p151-freemium-cost {
    flex: 0 0 auto;
}

/* Un champ numérique de réglage n'a aucune raison de faire toute la largeur.
   ⚠️ `width` et `max-width` sont les SEULES déclarations admises sur un champ :
   le thème seul décide de son fond, de sa bordure et de sa couleur. */
.p151-freemium-number {
    max-width: 160px;
}

/* ----------------------------------------------------------------------------
   Freemium — usage réel et coût réel (2026-08-26)
   ----------------------------------------------------------------------------
   Mise en page seule. Les pastilles sont des `bg-label-*` du thème, la surface
   est une `.p151-zone` : aucune couleur n'est écrite ici.
   -------------------------------------------------------------------------- */

/* Bandeau « ce que l'IA nous coûte » : les chiffres se répartissent et se
   replient proprement sur mobile. */
.p151-freemium-cout-total {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    padding: 0.85rem 1rem;
}

/* Les mesures d'une action facturée, sous sa description. */
.p151-freemium-mesure {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-top: 0.5rem;
}

/* Le total historique, discret à côté du chiffre de la fenêtre : c'est la
   fenêtre glissante qui porte la décision, le total n'est qu'un repère. */
.p151-freemium-total {
    opacity: 0.7;
    font-weight: 400;
}

/* ============================================================================
   `.p151-choice` — LE CHOIX À DEUX OPTIONS DE L'ÉCRAN FREEMIUM (2026-08-26)
   ----------------------------------------------------------------------------
   🔴 POURQUOI CE COMPOSANT EXISTE, ET POURQUOI CE N'EST PLUS UN INTERRUPTEUR.

   L'écran portait deux familles d'interrupteurs qui se ressemblaient et dont le
   sens était INVERSE :
     - sur une feature,  allumé = « Gratuite »
     - sur une action,   allumé = « Payante »
   Un même geste voulait donc dire une chose et son contraire selon la ligne, et
   l'écran affichait côte à côte des curseurs allumés annonçant l'un « Gratuite »,
   l'autre « Payante ». Signalé par l'humain le 26/08/2026 : « c'est trop bizarre
   pour un humain ».

   La correction n'est pas de renommer les libellés : c'est de supprimer l'état
   implicite. Ici les DEUX options sont écrites, une seule est allumée, et sa
   COULEUR porte le sens — jamais la position d'un curseur.

   ⛔ UNE SEULE GRAMMAIRE DE COULEUR DANS TOUT L'ÉCRAN :
        VERT   = l'utilisateur ne paie rien
        VIOLET = il faut payer (abonnement pour l'accès, crédits pour l'action)

   ⚠️ CONTRASTES MESURÉS AU NAVIGATEUR le 26/08/2026 (seuil AA texte = 4,5:1) —
   c'est ce qui dicte la couleur du TEXTE, pas le goût :
     | fond                       | blanc | `--bs-body-bg` (#282a42) | noir (#000) |
     | `--bs-success` (`#72e128`) |  1,68 |          10,4            | **12,52**   |
     | `--bs-primary` (`#a855f7`) |  3,96 |       3,54 ❌            | **5,31**    |
   Le vert du thème est VIF : un libellé blanc dessus est illisible (1,68).
   ⚠️ ET `--bs-body-bg` NE SUFFIT PAS SUR LE VIOLET : mesuré à **3,54** au navigateur
   le 26/08/2026 — sous le seuil. C'était le premier essai, il a été corrigé.
   On écrit donc en NOIR RÉEL (`#000`) sur les deux fonds pleins : c'est la seule
   valeur qui passe AA des deux côtés.

   ⛔ NE PAS ÉCRIRE `var(--bs-black)` ICI. Materialize redéfinit `--bs-black-rgb`
   en `38, 43, 67` — un bleu nuit, PAS du noir. C'est très exactement le piège déjà
   documenté en section 7 de ce fichier (`.text-black`), qui donnait 3,53:1 sur
   `bg-primary`. Le `#000` en dur est ici la MÊME décision, prise pour la MÊME raison
   mesurée, et c'est la seule couleur écrite en dur de ce composant.

   ⚠️ POURQUOI PAS UN `btn-group` DE `btn-outline-*` : mesuré à 3,15–3,54:1 sous ce
   thème, un `btn-outline-*` PORTANT UN LIBELLÉ se lit comme un bouton DÉSACTIVÉ.
   L'option non retenue doit rester lisible — on peut vouloir cliquer dessus.
   Elle garde donc `--p151-text` (10,18:1), et se distingue par l'ABSENCE de fond.

   ⚠️ LES RADIOS NE SONT PAS EN `display: none` : ça les sortirait de l'ordre de
   tabulation. Ils sont masqués en `opacity: 0` + `position: absolute`, et l'anneau
   de focus est reporté sur le libellé (`:focus-visible + label`).
   ========================================================================== */

/* ⚠️ `flex: 0 0 auto` : le composant est calé à droite de la ligne d'une feature
   et ne doit ni s'étirer ni se comprimer. Il remplace `.p151-freemium-switch`,
   supprimée le 26/08/2026 avec les interrupteurs. */
.p151-choice {
    display: inline-flex;
    align-items: stretch;
    flex: 0 0 auto;
    background-color: var(--p151-surface-sunken);
    border: 1px solid var(--p151-border);
    border-radius: 0.5rem;
    padding: 2px;
    gap: 2px;
}

/* Masquage accessible : le radio reste focusable et lisible par un lecteur d'écran. */
.p151-choice input[type="radio"] {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

.p151-choice__opt {
    margin: 0;
    padding: 0.28rem 0.8rem;
    border-radius: 0.35rem;
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.4;
    color: var(--p151-text);
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
    transition: background-color 0.12s ease, color 0.12s ease;
}

.p151-choice__opt:hover {
    background-color: var(--p151-surface-raised);
    color: var(--p151-text-strong);
}

/* Option retenue : fond plein, texte sombre — cf. le tableau de contrastes. */
.p151-choice input[type="radio"]:checked + .p151-choice__opt--libre {
    background-color: var(--bs-success);
    color: #000;
}

.p151-choice input[type="radio"]:checked + .p151-choice__opt--paye {
    background-color: var(--bs-primary);
    color: #000;
}

/* L'anneau de focus suit le clavier : sans lui, un radio masqué serait
   focusable mais invisible. */
.p151-choice input[type="radio"]:focus-visible + .p151-choice__opt {
    outline: 2px solid var(--bs-primary);
    outline-offset: 2px;
}

/* Légende de l'écran : la grammaire de couleur, écrite une fois, en haut. */
.p151-legende {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    margin-top: 0.75rem;
}

.p151-legende__puce {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.p151-legende__pastille {
    width: 0.85rem;
    height: 0.85rem;
    border-radius: 0.25rem;
    display: inline-block;
}

.p151-legende__pastille--libre { background-color: var(--bs-success); }
.p151-legende__pastille--paye { background-color: var(--bs-primary); }

/* Le champ de coût s'efface quand l'action est offerte : il ne commande rien.
   ⚠️ `readonly` et non `disabled` — un champ désactivé n'est pas envoyé par le
   navigateur, et le montant saisi serait perdu à chaque enregistrement fait en
   mode « Offerte », alors qu'on le garde exprès sous la main. */
.p151-freemium-cost.is-inactive {
    opacity: 0.45;
}

/* ----------------------------------------------------------------------------
   `.p151-menu-verrou` — le cadenas des features réservées, dans le menu
   ----------------------------------------------------------------------------
   ⛔ L'ENTRÉE N'EST PAS MASQUÉE, ELLE EST MARQUÉE. C'est la doctrine du projet
   pour les colonnes « Actions » (une commande qui ne s'applique pas reste
   affichée, inerte, avec son motif en infobulle) — et en freemium, montrer ce
   qu'on rate EST le levier de conversion. Le lien reste cliquable : il mène à
   /trial ou /subscribe, c'est-à-dire au parcours qu'on veut.

   Discret par construction : c'est un indicateur, pas une alarme. Il ne doit pas
   attirer l'œil plus que le libellé qu'il annote.
   -------------------------------------------------------------------------- */

.p151-menu-verrou {
    font-size: 0.9rem;
    opacity: 0.55;
    flex: 0 0 auto;
}

/* Sur l'entrée survolée ou active, le cadenas suit et se réveille un peu. */
.menu-link:hover .p151-menu-verrou,
.menu-item.active > .menu-link .p151-menu-verrou {
    opacity: 0.85;
}

/* ⚠️ Sidebar repliée en icônes : le libellé disparaît, le cadenas n'a plus rien
   à annoter et se retrouverait seul à côté de l'icône. On le retire. */
.layout-menu-collapsed:not(:hover) .p151-menu-verrou {
    display: none;
}

/* ============================================================================
   `.p151-paywall` — LA PAGE D'ACCROCHE D'UNE FEATURE RÉSERVÉE (2026-08-26)
   ----------------------------------------------------------------------------
   Rendue par `FeatureMiddleware::refuser()` À L'URL DEMANDÉE, à la place d'une
   redirection sèche vers /trial. L'utilisateur garde son contexte et voit ce
   qu'il rate — c'est là que se joue la conversion, pas sur la page de prix.

   ⛔ AUCUNE COULEUR DE SURFACE ÉCRITE ICI : la carte est peinte par le thème,
   l'encart par `.p151-zone`, la pastille par `bg-label-primary`. Tout ce qui
   suit est de la mise en page.
   ========================================================================== */

.p151-paywall {
    display: flex;
    justify-content: center;
    padding: 2rem 1rem;
}

/* ⚠️ `max-width` en ch et non en px : la mesure qui compte pour un texte de
   conversion, c'est le nombre de caractères par ligne, pas la largeur d'écran. */
.p151-paywall__carte {
    background-color: var(--p151-surface);
    border: 1px solid var(--p151-border);
    border-radius: 0.75rem;
    padding: 2.5rem 2rem;
    max-width: 68ch;
    width: 100%;
    text-align: center;
}

.p151-paywall__icone {
    width: 4rem;
    height: 4rem;
    margin: 0 auto 1.25rem;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--p151-gradient-primary);
    color: #fff;
    font-size: 1.75rem;
}

.p151-paywall__titre {
    font-size: 1.6rem;
    font-weight: 600;
    color: var(--p151-text-strong);
    margin-bottom: 0.75rem;
}

.p151-paywall__texte {
    color: var(--p151-text);
    margin-bottom: 1.5rem;
}

/* L'argumentaire est aligné à GAUCHE même dans une carte centrée : une liste
   centrée se lit mal, l'œil perd le début de chaque ligne. */
.p151-paywall__bonus {
    background-color: var(--p151-surface-raised);
    border-radius: 0.5rem;
    padding: 1.1rem 1.25rem;
    margin-bottom: 1.75rem;
    text-align: left;
}

.p151-paywall__bonus-titre {
    font-weight: 600;
    color: var(--p151-text-strong);
    margin-bottom: 0.6rem;
}

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

.p151-paywall__liste li {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.3rem 0;
    color: var(--p151-text);
}

.p151-paywall__liste i {
    color: var(--bs-primary);
    margin-top: 0.15rem;
    flex: 0 0 auto;
}

.p151-paywall__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.75rem;
}

.p151-paywall__mention {
    color: var(--p151-text-muted);
    font-size: 0.8125rem;
    margin: 1rem 0 0;
}

.p151-paywall__retour {
    margin-top: 1.75rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--p151-border);
}

.p151-paywall__retour a {
    color: var(--p151-text-muted);
    text-decoration: none;
}

.p151-paywall__retour a:hover {
    color: var(--p151-text-strong);
}

/* ----------------------------------------------------------------------------
   Compteur de crédits de la barre supérieure — à zéro, il propose de recharger
   ----------------------------------------------------------------------------
   « 0 crédits » ne disait rien de ce qu'il fallait faire. Le compteur était déjà
   un lien vers /buy-credits, mais rien ne l'annonçait.
   -------------------------------------------------------------------------- */

.p151-header-credits-recharger {
    margin-left: 0.4rem;
}
