/* ==========================================================================
   QUICK SWING — VITRINE LABORATOIRE v2 (page d'accueil)
   Bandeaux plein écran, taxonomie couleur par catégorie, animations signature.
   Toutes les valeurs d'espacement sont en clamp()/rem fixes : on n'y référence
   jamais --wp--preset--spacing--2xl (absent des styles globaux générés côté
   live -> tout un padding shorthand qui le contient devient invalide et
   s'effondre à 0, d'où le premier titre "collé" au hero avant ce correctif).
   ========================================================================== */

/* --- Bandeaux plein écran --- */
.qs-band,
.wp-block-group.qs-band--contact {
    /* WordPress ajoute automatiquement un margin-block-start entre blocs
       frères via :where(.is-layout-flow) > * (24px par défaut). Chaque
       bandeau gère déjà son propre espacement avec son padding interne :
       on neutralise ce gap pour qu'ils restent bien accolés, sans double
       espace ni liseré de la couleur de fond du dessous qui transparaît. */
    margin-block: 0;
}
.qs-band {
    position: relative;
    box-sizing: border-box;
    width: 100%;
    padding-block: clamp(3.5rem, 8vw, 6.5rem);
    padding-inline: clamp(1.25rem, 5vw, 3rem);
    overflow: clip;
}
/* La bande qui porte le carrousel « laboratoire » doit laisser l'ombre des
   cartes au survol déborder verticalement (sinon coupée net au ras de la
   vague décorative). overflow-x:clip + overflow-y:visible est une
   combinaison valide qui NE force PAS l'axe visible en auto (contrairement à
   hidden + visible) : le débordement latéral des bandes alignfull reste
   maîtrisé, seul le vertical est libéré. */
.qs-band:has(.qs-carousel) {
    overflow-x: clip;
    overflow-y: visible;
}
.qs-band > .qs-band__inner {
    /* !important nécessaire : depuis que "Explorez le laboratoire" est un
       vrai wp:group (pour nicher un wp:query dynamique) et non plus du
       wp:html brut, Gutenberg lui applique par défaut son propre layout
       "constrained" (~800px, thème.json) sur les enfants directs — plus
       étroit que cette largeur de bande et invisible à l'édition, seulement
       au rendu. On force donc explicitement notre propre largeur ici plutôt
       que de dépendre d'un attribut "layout" JSON dont le bon type n'a pas
       pu être vérifié sans instance WordPress live. */
    max-width: var(--qs-band-max, 1280px) !important;
    margin-inline: auto;
}
.qs-band--tight > .qs-band__inner { max-width: 760px; }
.qs-band--base   { background: var(--wp--preset--color--base); }
.qs-band--surface{ background: var(--wp--preset--color--surface); }

/* Trait "circuit" en haut de bandeau — reprend .is-style-laser du thème */
.qs-band__rule {
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--qs-neon-a50), transparent);
    box-shadow: 0 0 12px var(--qs-neon-a40);
}

/* --- Halo derrière le H1 (soudure visuelle avec le hero) --- */
.qs-intro { text-align: center; }
.qs-intro::before {
    content: '';
    position: absolute;
    top: -10%;
    left: 50%;
    width: min(900px, 100vw);
    height: 420px;
    transform: translateX(-50%);
    background: radial-gradient(closest-side, var(--qs-neon-a15), transparent 70%);
    pointer-events: none;
    z-index: 0;
}
.qs-intro > .qs-band__inner { position: relative; z-index: 1; }

/* Indicateur "signal actif" à côté de l'eyebrow */
.qs-eyebrow-row {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
    margin: 0 0 1rem;
}
.qs-live-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--qs-neon);
    box-shadow: 0 0 8px var(--qs-neon), 0 0 16px var(--qs-neon-a50);
    animation: qs-pulse 1.8s ease-in-out infinite;
    flex-shrink: 0;
}
.qs-eyebrow {
    font-family: var(--wp--preset--font-family--jetbrains-mono, monospace);
    font-size: var(--wp--preset--font-size--sm);
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--wp--preset--color--accent);
    margin: 0;
}
@keyframes qs-pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50%      { opacity: .4; transform: scale(0.7); }
}

.qs-intro__title {
    margin: 0;
    font-weight: 900;
    line-height: 1.08;
    font-size: var(--wp--preset--font-size--display);
    text-shadow: 2px 0 var(--wp--preset--color--accent), -2px 0 var(--wp--preset--color--accent-alt);
}
.qs-intro__lede {
    max-width: 700px;
    margin: clamp(1.25rem, 3vw, 1.75rem) auto 0;
    font-size: var(--wp--preset--font-size--lg);
    color: var(--wp--preset--color--gray-300);
    line-height: 1.7;
}

.qs-skill-pills {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.6rem;
    margin: clamp(1.75rem, 4vw, 2.5rem) 0 0;
    padding: 0;
    list-style: none;
    font-family: var(--wp--preset--font-family--jetbrains-mono, monospace);
}
.qs-skill-pills li {
    border: 1px solid var(--qs-neon-a30);
    border-radius: 999px;
    padding: 0.45rem 1.05rem;
    font-size: 0.78rem;
    letter-spacing: .02em;
    color: var(--wp--preset--color--gray-300);
    background: var(--qs-neon-a04);
    white-space: nowrap;
    transition: border-color .25s ease, color .25s ease, background .25s ease;
}
.qs-skill-pills li:hover {
    border-color: var(--qs-neon-a50);
    color: var(--wp--preset--color--contrast);
    background: var(--qs-neon-a08);
}

/* --- Stats --- */
.qs-stats-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: clamp(1.5rem, 4vw, 2.5rem);
    text-align: center;
}
@media (max-width: 768px) {
    .qs-stats-grid { grid-template-columns: repeat(2, 1fr); }
}
.qs-stat-number {
    color: var(--wp--preset--color--accent);
    font-weight: 900;
    font-size: var(--wp--preset--font-size--display);
    margin: 0 0 0.4rem;
    text-shadow: 0 0 24px var(--qs-neon-a30);
}
.qs-stat-label {
    color: var(--wp--preset--color--gray-300);
    font-size: var(--wp--preset--font-size--sm);
    font-family: var(--wp--preset--font-family--jetbrains-mono, monospace);
    text-transform: uppercase;
    letter-spacing: .04em;
    margin: 0;
}

/* --- En-tête de section --- */
.qs-section-head {
    text-align: center;
    max-width: 640px;
    margin: 0 auto clamp(2rem, 5vw, 2.75rem);
}
.qs-section-head .wp-block-heading {
    color: var(--wp--preset--color--accent);
    margin: 0 0 0.75rem;
}
.qs-section-head p {
    color: var(--wp--preset--color--gray-300);
    margin: 0;
    line-height: 1.6;
}

/* --- Filter bar --- */
.qs-filter-bar {
    display: flex;
    gap: 0.6rem;
    margin: 0 0 0.5rem;
    padding: 0 0 0.5rem;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    -ms-overflow-style: none;
    mask-image: linear-gradient(90deg, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
}
.qs-filter-bar::-webkit-scrollbar { display: none; }
@media (min-width: 768px) {
    .qs-filter-bar {
        flex-wrap: wrap;
        justify-content: center;
        /* Largeur plafonnée volontairement : sur les 7 boutons, ça force un
           retour propre à 4 + 3 plutôt qu'un dernier bouton isolé tout seul
           sur sa ligne quand ça déborde pile d'un item. */
        max-width: 42rem;
        margin-inline: auto;
        overflow: visible;
        mask-image: none;
    }
}
.qs-filter-btn {
    scroll-snap-align: start;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font: inherit;
    font-family: var(--wp--preset--font-family--jetbrains-mono, monospace);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    color: var(--wp--preset--color--gray-300);
    background: transparent;
    border: 1px solid var(--qs-neon-a20);
    border-radius: 999px;
    padding: 0.6rem 1.2rem;
    cursor: pointer;
    transition: color .25s ease, border-color .25s ease, background .25s ease, box-shadow .25s ease, transform .2s ease;
}
.qs-filter-btn .qs-cat-dot {
    width: 7px; height: 7px; border-radius: 50%;
    background: var(--cat, var(--qs-neon));
    box-shadow: 0 0 6px var(--cat, var(--qs-neon));
    flex-shrink: 0;
}
.qs-filter-btn:hover {
    border-color: color-mix(in srgb, var(--cat, var(--qs-neon)) 55%, transparent);
    color: var(--wp--preset--color--contrast);
}
.qs-filter-btn.is-active {
    color: #000;
    background: var(--cat, var(--qs-neon));
    border-color: var(--cat, var(--qs-neon));
    box-shadow: 0 0 22px color-mix(in srgb, var(--cat, var(--qs-neon)) 45%, transparent);
}
.qs-filter-btn.is-active .qs-cat-dot { background: #000; box-shadow: none; }
.qs-filter-btn:focus-visible,
.qs-projet-card:focus-visible {
    outline: 2px solid var(--wp--preset--color--accent);
    outline-offset: 3px;
}

.qs-showcase-count {
    text-align: center;
    font-family: var(--wp--preset--font-family--jetbrains-mono, monospace);
    font-size: 0.78rem;
    letter-spacing: .04em;
    color: var(--wp--preset--color--gray-500);
    margin: 0 0 clamp(1.75rem, 4vw, 2.5rem);
}

/* --- Contrôles archive : recherche + tri --------------------------------
   Émis par [qs_projet_filters search="1" sort="1"] (plugin qs-core), au-dessus
   de la barre de boutons .qs-filter-bar. Lus par js/showcase-filter.js. */
.qs-showcase-controls {
    display: flex;
    flex-wrap: wrap;
    gap: 0.7rem;
    justify-content: center;
    align-items: center;
    max-width: 42rem;
    margin: 0 auto clamp(1rem, 3vw, 1.5rem);
}
.qs-showcase-search {
    position: relative;
    flex: 1 1 18rem;
    min-width: 0;
}
.qs-showcase-search::before {
    content: "";
    position: absolute;
    left: 1rem;
    top: 50%;
    width: 15px;
    height: 15px;
    margin-top: -7.5px;
    background-color: var(--wp--preset--color--gray-300);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='7' cy='7' r='4.5' stroke='black' stroke-width='1.6' fill='none'/%3E%3Cpath d='M11 11l3.5 3.5' stroke='black' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat center / contain;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='7' cy='7' r='4.5' stroke='black' stroke-width='1.6' fill='none'/%3E%3Cpath d='M11 11l3.5 3.5' stroke='black' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat center / contain;
    pointer-events: none;
}
.qs-showcase-search input {
    width: 100%;
    box-sizing: border-box;
    font-family: var(--wp--preset--font-family--jetbrains-mono, monospace);
    font-size: 0.82rem;
    color: var(--wp--preset--color--contrast);
    background: var(--qs-glass-bg, rgba(255, 255, 255, 0.04));
    border: 1px solid var(--qs-neon-a20);
    border-radius: 999px;
    padding: 0.7rem 1.1rem 0.7rem 2.5rem;
    -webkit-appearance: none;
    appearance: none;
    transition: border-color .25s ease, box-shadow .25s ease;
}
.qs-showcase-search input::placeholder { color: var(--wp--preset--color--gray-500); }
.qs-showcase-search input:focus-visible {
    outline: none;
    border-color: var(--qs-neon);
    box-shadow: 0 0 0 3px var(--qs-neon-a20);
}
.qs-showcase-search input::-webkit-search-cancel-button {
    -webkit-appearance: none;
    width: 12px;
    height: 12px;
    background-color: var(--wp--preset--color--gray-300);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 2l8 8M10 2l-8 8' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat center / contain;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 2l8 8M10 2l-8 8' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat center / contain;
    cursor: pointer;
}
.qs-showcase-sort select {
    font-family: var(--wp--preset--font-family--jetbrains-mono, monospace);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--wp--preset--color--gray-300);
    background-color: transparent;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%23999' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.9rem center;
    background-size: 14px;
    border: 1px solid var(--qs-neon-a20);
    border-radius: 999px;
    padding: 0.65rem 2.3rem 0.65rem 1.2rem;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
    transition: border-color .25s ease, color .25s ease;
}
.qs-showcase-sort select:hover {
    border-color: var(--qs-neon-a40);
    color: var(--wp--preset--color--contrast);
}
.qs-showcase-sort select:focus-visible {
    outline: 2px solid var(--wp--preset--color--accent);
    outline-offset: 2px;
}
.qs-showcase-sort option {
    background: var(--wp--preset--color--surface, #12121a);
    color: var(--wp--preset--color--contrast);
}
.qs-showcase-empty {
    text-align: center;
    font-family: var(--wp--preset--font-family--jetbrains-mono, monospace);
    font-size: 0.9rem;
    color: var(--wp--preset--color--gray-300);
    margin: clamp(2rem, 6vw, 4rem) auto;
}
.qs-showcase-empty[hidden] { display: none; }
@media (max-width: 600px) {
    .qs-showcase-controls { flex-direction: column; align-items: stretch; }
    /* En colonne, le `flex: 1 1 18rem` de .qs-showcase-search s'applique à
       l'axe vertical : le champ ferait 18rem de haut (icône loupe flottant
       dans le vide, gros trou avant le tri). On repasse en hauteur de
       contenu. */
    .qs-showcase-search { flex: 0 0 auto; }
    .qs-showcase-sort select { width: 100%; }
}

/* Archives en grille (/projets/ ET /experiences/) : le <main> est
   `is-layout-constrained`, donc WordPress remet à zéro le `margin-block-end` de
   son dernier enfant (la grille), qui se retrouvait collée au footer. On ajoute
   l'air par le bas via padding (non concerné par ce reset). Même respiration que
   `.qs-featured-projects` sur l'accueil. */
.post-type-archive-projet .site-main,
.category-experiences .site-main {
    padding-bottom: clamp(4rem, 9vw, 6.5rem);
}

/* Pagination client de la vitrine (générée par showcase-filter.js, /experiences/).
   Réutilise la classe .pagination (css/articles.css) ; il ne reste qu'à
   neutraliser le chrome par défaut des <button>. */
.qs-showcase-pagination { margin-top: clamp(2.5rem, 6vw, 4rem); }
.qs-showcase-pagination button {
    -webkit-appearance: none;
    appearance: none;
    background: none;
    cursor: pointer;
    /* pas de font ici : on laisse .pagination .page-numbers / .wp-block-query-
       pagination-* (css/articles.css) fixer la police, comme pour la pagination WP. */
}
.qs-showcase-pagination button[disabled] {
    opacity: 0.3;
    cursor: default;
    pointer-events: none;
}

/* --- Grid --- */
.qs-showcase-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: clamp(1.1rem, 2.5vw, 1.75rem);
    /* !important : même raison qu'au-dessus sur .qs-band__inner — le
       wp:query dynamique est lui-même un enfant direct constraint par le
       layout Gutenberg par défaut de son wp:group parent (.qs-band__inner),
       à une largeur bien plus étroite que la bande. */
    max-width: none !important;
    width: 100% !important;
}
@media (max-width: 991px) {
    .qs-showcase-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 768px) {
    .qs-showcase-grid { grid-template-columns: 1fr; }
}

/* --- Grille maçonnée (masonry) -----------------------------------------------
   Les cartes projet n'ont pas toutes la même hauteur : l'extrait tombe sur 2 ou
   3 lignes, les tags sur 1 ou 2 rangs. En grille CSS classique, toute une rangée
   s'aligne sur la carte la plus haute -> un vide sous les cartes courtes et des
   espacements inégaux avant la rangée suivante.

   La maçonnerie est calculée par js/showcase-filter.js (createMasonry) : il
   répartit les cartes visibles dans 3 colonnes (2 puis 1 en responsive), chaque
   carte allant dans la colonne la plus courte, et positionne les <li> en absolu.
   Contrairement au multi-colonnes CSS (`column-count`), ça garde toujours le bon
   nombre de colonnes même quand un filtre réduit fortement le nombre de cartes.

   Repli sans JS : multi-colonnes CSS (ci-dessous). Il peut retomber à 2 colonnes
   quand peu de cartes sont visibles, mais le script prend le relais dès qu'il
   tourne (classe .is-js-masonry).

   Cible : le <ul> wp:post-template dès qu'il contient des .qs-projet-card
   (vitrine accueil, « Projets récents », archive /projets/), quel que soit le
   layout Gutenberg d'origine (grid ou flow), + la grille statique historique. */
:is(.wp-block-post-template.is-layout-grid,
    .wp-block-post-template.is-layout-flow,
    .qs-showcase-grid):has(.qs-projet-card, .qs-projet-card__title) {
    display: block;
    grid-template-columns: none;
    gap: 0;
    column-count: 3;
    /* column-gap sert AUSSI de source pour l'entre-colonnes lu par le script
       (getComputedStyle(...).columnGap) — on le garde même en .is-js-masonry. */
    column-gap: clamp(1.1rem, 2.5vw, 1.75rem);
}

/* Enfant de colonne = le <li> (grille dynamique) ou la carte elle-même
   (grille statique). Repli sans JS : pas de coupure entre colonnes + marge
   basse pour l'espacement vertical. */
:is(.wp-block-post-template, .qs-showcase-grid):has(.qs-projet-card, .qs-projet-card__title) > li,
.qs-showcase-grid:has(> .qs-projet-card) > .qs-projet-card {
    display: block;
    width: 100%;
    margin: 0 0 clamp(1.1rem, 2.5vw, 1.75rem);
    -webkit-column-break-inside: avoid;
    break-inside: avoid;
}

@media (max-width: 991px) {
    :is(.wp-block-post-template.is-layout-grid,
        .wp-block-post-template.is-layout-flow,
        .qs-showcase-grid):has(.qs-projet-card, .qs-projet-card__title) { column-count: 2; }
}
@media (max-width: 768px) {
    :is(.wp-block-post-template.is-layout-grid,
        .wp-block-post-template.is-layout-flow,
        .qs-showcase-grid):has(.qs-projet-card, .qs-projet-card__title) { column-count: 1; }
}

/* --- Maçonnerie pilotée par le script ------------------------------------- */
.is-js-masonry {
    /* le multi-colonnes CSS est neutralisé : le script positionne tout en
       absolu et fixe la hauteur du conteneur en inline. */
    column-count: auto !important;
    column-width: auto !important;
    position: relative;
}
.is-js-masonry > li,
.is-js-masonry > .qs-projet-card {
    /* largeur / left / top / margin posés en inline par le script */
    break-inside: auto;
}
/* Cartes masquées par un filtre : hors flux ET hors calcul de maçonnerie. */
:is(.wp-block-post-template, .qs-showcase-grid) > li.is-hidden,
.qs-projet-card.is-hidden {
    display: none;
}

.qs-projet-card {
    /* .is-style-card-hover apporte position/overflow/le halo ::before via
       une règle CSS classique (donc déjà appliquée), mais son fond/bordure/
       rayon proviennent normalement de styles/sections/card-hover.json,
       généré par WordPress uniquement pour de vrais blocs core/group passés
       par le rendu Gutenberg. Ces cartes sont du HTML brut dans un bloc
       wp:html : elles ne reçoivent jamais cette règle générée, d'où des
       coins droits sur la boîte alors que le ::before (rayon déclaré en dur
       dans block-styles.css) restait bien arrondi. On réplique donc ici les
       mêmes valeurs que card-hover.json, en dur. */
    border-radius: 16px;
    border: 1px solid rgba(255, 255, 255, 0.06);
    background: rgba(255, 255, 255, 0.02);
    display: flex;
    flex-direction: column;
    text-decoration: none !important;
    color: inherit;
    transition: box-shadow .45s cubic-bezier(.23,1,.32,1),
                transform .45s cubic-bezier(.23,1,.32,1);
}

/* Apparition en cascade au chargement/filtrage : gérée par
   js/showcase-filter.js, qui ne cherche des .qs-projet-card que dans
   [data-qs-showcase-grid] et leur ajoute .is-cascade-in. .qs-projet-card
   est aussi réutilisé (hors de cette grille) par la boucle de requête CPT
   "projet" (patterns galerie-projets / carte-projet, single-projet.html) :
   sans ce scope, ces cartes héritaient de l'opacity:0 initiale sans jamais
   recevoir .is-cascade-in et restaient donc invisibles en permanence. */
.qs-showcase-grid .qs-projet-card {
    opacity: 0;
    transform: translateY(22px);
    transition: opacity .55s ease, transform .55s ease,
                box-shadow .45s cubic-bezier(.23,1,.32,1),
                transform .45s cubic-bezier(.23,1,.32,1);
}
.qs-showcase-grid .qs-projet-card.is-hidden { display: none; }
/* Grille dynamique (wp:post-template) : chaque carte est enveloppée dans un
   <li> qui est le vrai enfant grid — le masquer aussi évite les cellules
   fantômes vides quand un filtre est actif (voir showcase-filter.js). */
.qs-showcase-grid li.is-hidden { display: none; }
.qs-showcase-grid .qs-projet-card.is-cascade-in { opacity: 1; transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
    .qs-showcase-grid .qs-projet-card { opacity: 1; transform: none; transition: none; }
}

/* =========================================================================
   CARROUSEL — section « Explorez le laboratoire » de l'accueil
   -------------------------------------------------------------------------
   Le <ul.qs-showcase-grid> devient une piste horizontale : js/showcase-filter.js
   (createCarousel) l'enveloppe dans un .qs-carousel__viewport à débordement
   masqué et injecte deux flèches .qs-carousel__nav. Défilement carte par carte,
   en boucle infinie (rotation du DOM), + glisser tactile.

   Tant que le JS n'a pas tourné, .qs-carousel__track / __viewport n'existent
   pas : la grille reste en repli multi-colonnes comme les autres. Les !important
   ci-dessous neutralisent ce repli (sélecteurs :has() plus spécifiques), même
   principe que .is-js-masonry plus haut.

   --qs-carousel-visible : nombre de cartes visibles, lu aussi par le JS pour
   calculer le pas de défilement. */
.qs-carousel {
    position: relative;
    --qs-carousel-visible: 3;
    --qs-carousel-gap: clamp(1.1rem, 2.5vw, 1.75rem);
    /* Amorce (peek) : largeur visible d'un bout de la carte précédente /
       suivante de chaque côté. Donne de l'air pour que la lueur des cartes
       au survol diffuse latéralement au lieu d'être coupée net au bord. */
    --qs-carousel-peek: clamp(22px, 5vw, 52px);
    /* Largeur d'une carte pleine, en réservant la place des deux amorces et
       du gap qui les précède (visible + 1 gouttières au total). */
    --qs-carousel-card: calc(
        (100% - var(--qs-carousel-gap) * (var(--qs-carousel-visible) + 1)
              - var(--qs-carousel-peek) * 2)
        / var(--qs-carousel-visible)
    );
    /* Largeur du dégradé de bord (amorce + sa gouttière). */
    --qs-carousel-fade: calc(var(--qs-carousel-peek) + var(--qs-carousel-gap));
}
@media (max-width: 991px) { .qs-carousel { --qs-carousel-visible: 2; } }
@media (max-width: 768px) { .qs-carousel { --qs-carousel-visible: 1; } }

.qs-carousel__viewport {
    /* Débordement infini : les cartes hors-champ à gauche/droite doivent
       disparaître, MAIS l'ombre projetée des cartes au survol (box-shadow
       0 24px 60px, ~85px) doit pouvoir déborder verticalement sans être
       rognée net. overflow-x:clip + overflow-y:visible est une combinaison
       valide qui NE force PAS l'axe visible en auto (contrairement à
       hidden) : coupe latérale nette, débordement vertical libre. La coupe
       latérale est ensuite adoucie par le dégradé .qs-carousel::before/after,
       et la bande .qs-band:has(.qs-carousel) est aussi en overflow-y:visible
       pour laisser l'ombre sortir jusque sur la vague décorative. */
    overflow-x: clip;
    overflow-y: visible;
}

/* Fondu des bords : voile de la couleur de fond de bande (suit les
   variations clair/sombre via le token sémantique) qui estompe l'amorce
   de carte et laisse la lueur des cartes au survol se diffuser
   progressivement au lieu d'être tranchée. Sous les flèches (z-index 3),
   au-dessus des cartes. */
.qs-carousel:has(.qs-carousel__track)::before,
.qs-carousel:has(.qs-carousel__track)::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: var(--qs-carousel-fade);
    z-index: 2;
    pointer-events: none;
}
.qs-carousel:has(.qs-carousel__track)::before {
    left: 0;
    background: linear-gradient(90deg,
        var(--wp--preset--color--base) 0%, transparent 100%);
}
.qs-carousel:has(.qs-carousel__track)::after {
    right: 0;
    background: linear-gradient(270deg,
        var(--wp--preset--color--base) 0%, transparent 100%);
}

.qs-carousel__track {
    display: flex !important;
    flex-wrap: nowrap !important;
    column-count: auto !important;
    column-width: auto !important;
    gap: var(--qs-carousel-gap);
    align-items: stretch;          /* -> toutes les cartes à la même hauteur */
    margin: 0 !important;
    padding: 0;
    list-style: none;
    transform: translateX(0);
    will-change: transform;
    touch-action: pan-y;
    /* Décalage constant vers la gauche (via `left`, pas `transform` qui est
       piloté par le JS) : la 1re carte du DOM devient l'amorce de gauche,
       il reste donc une amorce symétrique de chaque côté au repos. `left`
       n'affecte ni la taille de la piste ni la résolution des % des cartes. */
    position: relative;
    left: calc(var(--qs-carousel-peek) - var(--qs-carousel-card));
}
.qs-carousel__track > li,
.qs-carousel__track > .qs-projet-card {
    flex: 0 0 var(--qs-carousel-card) !important;
    width: auto !important;
    margin: 0 !important;
    display: flex !important;      /* la carte remplit toute la hauteur du <li> */
    break-inside: auto;
}
.qs-carousel__track > li.is-hidden { display: none !important; }

/* Cartes à hauteur égale — carrousel « laboratoire » ET grille « Autres
   projets divers » (.qs-even-cards) : hauteur pleine, titre/extrait tronqués
   pour que les hauteurs naturelles restent proches avant l'étirement flex, et
   groupe tags + CTA collé en bas (donc « Découvrir » aligné d'une carte à
   l'autre) avec un peu d'air avant le lien. */
:is(.qs-carousel, .qs-even-cards) .qs-projet-card { height: 100%; }
:is(.qs-carousel, .qs-even-cards) .qs-projet-card__title {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
}
:is(.qs-carousel, .qs-even-cards) .qs-projet-card__pitch {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    overflow: hidden;
    flex: 0 1 auto;               /* laisse l'espace libre au bloc tags + CTA */
}
:is(.qs-carousel, .qs-even-cards) .qs-projet-tags { margin-top: auto; }
:is(.qs-carousel, .qs-even-cards) .qs-projet-card__cta { margin-top: 0.7rem; }

/* Mobile : une seule carte visible à la fois (carrousel) / une seule colonne
   (« Autres projets »). L'égalisation des hauteurs — align-items:stretch +
   height:100 %, extrait tronqué à 3 lignes, tags poussés en bas par
   margin-top:auto — n'a alors plus aucun intérêt visuel et creuse de grands
   vides sous les cartes courtes (cf. « Beat forge »). Chaque carte reprend sa
   hauteur de contenu, sans troncature et sans espace mort. */
@media (max-width: 768px) {
    .qs-carousel__track { align-items: flex-start; }
    .qs-even-cards .wp-block-post-template { align-items: start; }
    :is(.qs-carousel, .qs-even-cards) .qs-projet-card { height: auto; }
    :is(.qs-carousel, .qs-even-cards) .qs-projet-card__title,
    :is(.qs-carousel, .qs-even-cards) .qs-projet-card__pitch {
        display: block;
        -webkit-line-clamp: unset;
        overflow: visible;
    }
    :is(.qs-carousel, .qs-even-cards) .qs-projet-card__pitch { flex: 0 1 auto; }
    :is(.qs-carousel, .qs-even-cards) .qs-projet-tags { margin-top: 0; }
}

/* Le carrousel a en plus besoin de forcer l'opacité (le fade-in de cascade ne
   peut pas atteindre les cartes décalées hors écran par le translateX). */
.qs-carousel .qs-projet-card {
    opacity: 1;
    transform: none;
    width: 100%;
}

/* --- Grille à cartes de hauteur égale : section « Autres projets divers » ---
   Vraie grille CSS + étirement plutôt que la maçonnerie (showcase-filter.js
   ignore .qs-even-cards). Les !important neutralisent le repli multi-colonnes
   (sélecteurs :has() plus spécifiques). */
.qs-even-cards .wp-block-post-template {
    display: grid !important;
    grid-template-columns: repeat(3, 1fr) !important;
    gap: clamp(1.1rem, 2.5vw, 1.75rem) !important;
    column-count: auto !important;
    align-items: stretch;
    margin: 0 !important;
}
.qs-even-cards .wp-block-post-template > li {
    margin: 0 !important;
    width: auto !important;
    display: flex !important;
}
@media (max-width: 991px) {
    .qs-even-cards .wp-block-post-template { grid-template-columns: repeat(2, 1fr) !important; }
}
@media (max-width: 768px) {
    .qs-even-cards .wp-block-post-template { grid-template-columns: 1fr !important; }
}

/* Espacements de la section « Autres projets divers » : peu d'air en haut (la
   vague .qs-wave-divider marque déjà la séparation avec la section précédente),
   et de l'air en bas pour que le bouton « Tous les projets » ne colle pas au
   footer. On passe par padding-bottom (et non margin) : WordPress remet à zéro
   `margin-block-end` du dernier enfant d'un conteneur `is-layout-constrained`
   (`:root :where(.is-layout-constrained) > :last-child`), ce qui écrasait la
   marge. */
.qs-band--pb-sm { padding-bottom: clamp(1.5rem, 4vw, 2.5rem); }
.qs-featured-projects {
    padding-top: clamp(0.5rem, 2vw, 1.25rem);
    padding-bottom: clamp(4rem, 10vw, 6.5rem);
}
.qs-featured-projects .wp-block-buttons { margin-top: clamp(2.5rem, 6vw, 4rem); }

/* Même respiration sous le carrousel « Explorez le laboratoire » avant le
   bouton « Voir toutes les expériences » : sans cette règle, le bouton (dernier
   enfant de .qs-band__inner) se retrouve nettement plus collé aux cartes que
   dans la section « Autres projets » ci-dessus. */
.qs-band:has(.qs-carousel) > .qs-band__inner > .wp-block-buttons {
    margin-top: clamp(2.5rem, 6vw, 4rem);
}

/* Le petit topo « Développement Python / Sites internet » sous l'intro. */
.qs-featured-projects .qs-section-head:has(+ .qs-projets-brief) {
    margin-bottom: clamp(1rem, 3vw, 1.5rem);
}
.qs-projets-brief {
    max-width: 640px;
    margin: 0 auto clamp(2.5rem, 5vw, 3.5rem);
    text-align: left;
}
.qs-projets-brief .wp-block-list.is-style-neon-list li { color: var(--wp--preset--color--gray-300) !important; }
.qs-projets-brief .wp-block-list.is-style-neon-list strong { color: var(--wp--preset--color--contrast); }
.qs-projets-brief .wp-block-list.is-style-neon-list a { color: var(--wp--preset--color--accent) !important; }

/* --- Flèches --- */
.qs-carousel__nav {
    position: absolute;
    /* Grand écran : top:50 % = centre de la carte (flèches dans la gouttière).
       Écran réduit : le JS pose --qs-carousel-arrow-top au centre de la
       vignette pour que les flèches en surimpression ne masquent pas le titre. */
    top: var(--qs-carousel-arrow-top, 50%);
    transform: translateY(-50%);
    z-index: 3;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    padding: 0;
    color: var(--wp--preset--color--accent);
    background: color-mix(in srgb, var(--wp--preset--color--surface, #12121a) 82%, transparent);
    border: 1px solid var(--qs-neon-a50);
    border-radius: 50%;
    cursor: pointer;
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    box-shadow: 0 0 14px var(--qs-neon-a30), inset 0 0 10px var(--qs-neon-a10);
    transition: color .2s ease, border-color .2s ease, box-shadow .2s ease,
                background .2s ease, transform .2s ease;
}
.qs-carousel__nav svg {
    width: 40%;
    height: 40%;
    filter: drop-shadow(0 0 4px var(--qs-neon-a50));
}
.qs-carousel__nav--prev { left: -3.25rem; }
.qs-carousel__nav--next { right: -3.25rem; }
.qs-carousel__nav:hover {
    color: var(--wp--preset--color--contrast);
    border-color: var(--qs-neon);
    background: color-mix(in srgb, var(--wp--preset--color--surface, #12121a) 88%, transparent);
    box-shadow: 0 0 24px var(--qs-neon-a50), inset 0 0 14px var(--qs-neon-a20);
    transform: translateY(-50%) scale(1.08);
}
.qs-carousel__nav:active { transform: translateY(-50%) scale(0.95); }
.qs-carousel__nav:focus-visible {
    outline: 2px solid var(--wp--preset--color--accent);
    outline-offset: 2px;
}
.qs-carousel__nav[hidden] { display: none; }

/* Sous ~1180px la gouttière ne suffit plus : flèches en surimpression sur le
   bord des cartes (le fond translucide + flou les garde lisibles). */
@media (max-width: 1180px) {
    .qs-carousel__nav--prev { left: 0.5rem; }
    .qs-carousel__nav--next { right: 0.5rem; }
}
@media (max-width: 768px) {
    .qs-carousel__nav { width: 2.4rem; height: 2.4rem; }
    .qs-carousel__nav--prev { left: 0.35rem; }
    .qs-carousel__nav--next { right: 0.35rem; }
}
@media (prefers-reduced-motion: reduce) {
    .qs-carousel__track { transition: none !important; }
}

/* Correctif du liseré lumineux hérité de .is-style-card-hover : sur un
   conic-gradient, la couleur dépend de l'angle depuis le centre, ce qui
   déforme le tracé dans les coins arrondis (surtout sur un rectangle large
   comme ces cartes). Le masque qui dessine le contour arrondi ne change
   pas — on remplace juste le dégradé conique par un balayage linéaire
   diagonal, dont la couleur ne dépend que d'un axe : les coins restent nets
   quel que soit le rayon. Couleur alignée sur la catégorie de la carte. */
.qs-projet-card.is-style-card-hover::before {
    /* Sans z-index, ce ::before reste derrière l'image (un vrai élément DOM)
       dans l'ordre d'empilement : la lumière ne traversait donc que la zone
       de texte, jamais la partie couverte par la photo. On le fait passer
       au-dessus de tout le contenu de la carte (::after du spotlight inclus)
       pour qu'elle fasse bien le tour complet. pointer-events:none est déjà
       hérité de la règle de base, donc les clics traversent toujours. */
    z-index: 3;
    background: linear-gradient(115deg,
        transparent 35%,
        var(--cat, var(--qs-neon)) 50%,
        transparent 65%);
    background-size: 260% 260%;
    background-position: 0% 0%;
    animation: none;
}
.qs-projet-card.is-style-card-hover:hover::before {
    animation: qs-card-sweep 1.6s ease-in-out infinite;
}
@keyframes qs-card-sweep {
    0%   { background-position: 0% 0%; }
    100% { background-position: 100% 100%; }
}
@media (prefers-reduced-motion: reduce) {
    .qs-projet-card.is-style-card-hover:hover::before { animation: none; opacity: 1; }
}

.qs-projet-card__image-wrap {
    position: relative;
    display: block;
    aspect-ratio: 16 / 10;
}
.qs-projet-card__image-wrap img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform .5s ease;
}
.qs-projet-card:hover .qs-projet-card__image-wrap img {
    transform: scale(1.06);
}

/* Ancrage de positionnement pour .qs-cat-badge sur les cartes générées par
   wp:post-featured-image (vitrine dynamique, Projets récents, Autres
   projets) — ces blocs n'ont pas le wrapper .qs-projet-card__image-wrap de
   l'ancien HTML statique, qui fournissait déjà ce position:relative. */
.qs-projet-card .wp-block-post-featured-image {
    position: relative;
    display: block;
}

/* Pastille de catégorie — icône générée en CSS mask, une seule source par catégorie */
.qs-cat-badge {
    position: absolute;
    top: 0.65rem;
    left: 0.65rem;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(6, 8, 10, 0.72);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    /* Repli --cat : les projets hors-vitrine (logiciels desktop, scripts
       Python…) n'ont pas de couleur de catégorie — gris neutre plutôt qu'une
       pastille invisible (bordure/ombre/icône transparentes). */
    border: 1px solid color-mix(in srgb, var(--cat, var(--wp--preset--color--gray-300)) 45%, transparent);
    box-shadow: 0 0 14px color-mix(in srgb, var(--cat, var(--wp--preset--color--gray-300)) 40%, transparent);
    /* z-index:4, pas 2 : sur les cartes générées par wp:post-featured-image
       (vitrine dynamique, Projets récents), le cœur de WordPress applique déjà
       position:relative + z-index:2 à l'<img> lui-même (support hover-zoom) —
       à z-index égal, l'image arrive après le badge dans le DOM et le
       recouvrait entièrement. */
    z-index: 4;
}
.qs-cat-badge::before {
    content: '';
    width: 16px;
    height: 16px;
    background-color: var(--cat, var(--wp--preset--color--gray-300));
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: contain;
    mask-size: contain;
    /* Icône par défaut (terminal) pour les projets hors des 6 catégories
       vitrine — logiciels desktop / scripts Python. Les 6 catégories la
       remplacent via les règles [data-cat="…"] .qs-cat-badge::before plus bas. */
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect x='1.5' y='2.5' width='13' height='11' rx='1.5' stroke='black' stroke-width='1.3' fill='none'/%3E%3Cpath d='M4.5 6.5L7 8.5 4.5 10.5' stroke='black' stroke-width='1.3' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M8.5 10.5h3.5' stroke='black' stroke-width='1.3' stroke-linecap='round'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect x='1.5' y='2.5' width='13' height='11' rx='1.5' stroke='black' stroke-width='1.3' fill='none'/%3E%3Cpath d='M4.5 6.5L7 8.5 4.5 10.5' stroke='black' stroke-width='1.3' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M8.5 10.5h3.5' stroke='black' stroke-width='1.3' stroke-linecap='round'/%3E%3C/svg%3E");
}

/* Halo spot qui suit le curseur — uniquement sur pointeurs fins (souris) */
@media (hover: hover) and (pointer: fine) {
    .qs-projet-card { position: relative; }
    .qs-projet-card::after {
        content: '';
        position: absolute;
        inset: 0;
        border-radius: 16px;
        background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%),
            color-mix(in srgb, var(--cat, var(--qs-neon)) 16%, transparent), transparent 70%);
        opacity: 0;
        transition: opacity .35s ease;
        pointer-events: none;
        z-index: 1;
    }
    .qs-projet-card:hover::after { opacity: 1; }
}

.qs-projet-card__body {
    padding: clamp(1.1rem, 2.5vw, 1.5rem);
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    flex: 1;
    position: relative;
    z-index: 1;
}
.qs-projet-card__title {
    font-size: var(--wp--preset--font-size--lg);
    font-weight: 700;
    color: var(--wp--preset--color--contrast);
    margin: 0;
}
.qs-projet-card__pitch {
    font-size: var(--wp--preset--font-size--sm);
    color: var(--wp--preset--color--gray-300);
    line-height: 1.6;
    flex: 1;
    margin: 0;
}
.qs-projet-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
}
.qs-tag {
    font-family: var(--wp--preset--font-family--jetbrains-mono, monospace);
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: var(--cat, var(--wp--preset--color--accent));
    border: 1px solid color-mix(in srgb, var(--cat, var(--wp--preset--color--accent)) 35%, transparent);
    border-radius: 999px;
    padding: 0.25rem 0.65rem;
    white-space: nowrap;
}
.qs-projet-card__cta {
    margin-top: 0.2rem;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    font-size: var(--wp--preset--font-size--sm);
    font-weight: 700;
    letter-spacing: 0.03em;
    color: var(--wp--preset--color--contrast);
}
.qs-projet-card__cta svg { transition: transform .25s ease; }
.qs-projet-card:hover .qs-projet-card__cta { color: var(--cat, var(--qs-neon)); }
.qs-projet-card:hover .qs-projet-card__cta svg { transform: translateX(4px); }

/* --- Taxonomie couleur par catégorie (dérivée des tokens du thème,
   alignée sur les 6 catégories réelles du menu du site) --- */
.qs-projet-card[data-cat="cam-music"],
.qs-filter-btn[data-qs-filter="cam-music"] { --cat: var(--wp--preset--color--accent); }

.qs-projet-card[data-cat="mic-react"],
.qs-filter-btn[data-qs-filter="mic-react"] { --cat: var(--wp--preset--color--accent-2); }

.qs-projet-card[data-cat="games"],
.qs-filter-btn[data-qs-filter="games"] { --cat: var(--wp--preset--color--accent-alt); }

.qs-projet-card[data-cat="midi-visual"],
.qs-filter-btn[data-qs-filter="midi-visual"] { --cat: var(--wp--preset--color--accent-3); }

.qs-projet-card[data-cat="cam-fun"],
.qs-filter-btn[data-qs-filter="cam-fun"] { --cat: color-mix(in srgb, var(--wp--preset--color--accent-2) 50%, var(--wp--preset--color--accent-3) 50%); }

.qs-projet-card[data-cat="music"],
.qs-filter-btn[data-qs-filter="music"] { --cat: color-mix(in srgb, var(--wp--preset--color--accent) 50%, var(--wp--preset--color--accent-alt) 50%); }

/* --- Icônes de catégorie (mask CSS, un seul jeu de définitions) --- */
.qs-projet-card[data-cat="cam-music"] .qs-cat-badge::before,
.qs-filter-btn[data-qs-filter="cam-music"] .qs-cat-dot { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='5' cy='12' r='2' fill='black'/%3E%3Cpath d='M7 12V3l6-1.3V7' stroke='black' stroke-width='1.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='11' cy='8' r='1.8' fill='black'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='5' cy='12' r='2' fill='black'/%3E%3Cpath d='M7 12V3l6-1.3V7' stroke='black' stroke-width='1.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='11' cy='8' r='1.8' fill='black'/%3E%3C/svg%3E"); }

.qs-projet-card[data-cat="mic-react"] .qs-cat-badge::before,
.qs-filter-btn[data-qs-filter="mic-react"] .qs-cat-dot { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect x='6' y='1.5' width='4' height='7' rx='2' stroke='black' stroke-width='1.3' fill='none'/%3E%3Cpath d='M4 8a4 4 0 0 0 8 0M8 12v2.5M5.5 14.5h5' stroke='black' stroke-width='1.3' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect x='6' y='1.5' width='4' height='7' rx='2' stroke='black' stroke-width='1.3' fill='none'/%3E%3Cpath d='M4 8a4 4 0 0 0 8 0M8 12v2.5M5.5 14.5h5' stroke='black' stroke-width='1.3' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); }

.qs-projet-card[data-cat="games"] .qs-cat-badge::before,
.qs-filter-btn[data-qs-filter="games"] .qs-cat-dot { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='4' r='2.2' fill='black'/%3E%3Cpath d='M8 6.2V11M4 14h8' stroke='black' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='4' r='2.2' fill='black'/%3E%3Cpath d='M8 6.2V11M4 14h8' stroke='black' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E"); }

.qs-projet-card[data-cat="midi-visual"] .qs-cat-badge::before,
.qs-filter-btn[data-qs-filter="midi-visual"] .qs-cat-dot { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1 8h2.5l1.5-5 3 10 1.5-5H15' stroke='black' stroke-width='1.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1 8h2.5l1.5-5 3 10 1.5-5H15' stroke='black' stroke-width='1.4' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }

.qs-projet-card[data-cat="cam-fun"] .qs-cat-badge::before,
.qs-filter-btn[data-qs-filter="cam-fun"] .qs-cat-dot { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1 8s2.7-4.5 7-4.5S15 8 15 8s-2.7 4.5-7 4.5S1 8 1 8Z' stroke='black' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='8' cy='8' r='2' fill='black'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M1 8s2.7-4.5 7-4.5S15 8 15 8s-2.7 4.5-7 4.5S1 8 1 8Z' stroke='black' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='8' cy='8' r='2' fill='black'/%3E%3C/svg%3E"); }

.qs-projet-card[data-cat="music"] .qs-cat-badge::before,
.qs-filter-btn[data-qs-filter="music"] .qs-cat-dot { -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 10V6M5.3 12V4M8.7 13V3M12 12V4M14 10V6' stroke='black' stroke-width='1.6' stroke-linecap='round' fill='none'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M2 10V6M5.3 12V4M8.7 13V3M12 12V4M14 10V6' stroke='black' stroke-width='1.6' stroke-linecap='round' fill='none'/%3E%3C/svg%3E"); }

/* --- Contact --- */
.qs-band--contact {
    background: linear-gradient(135deg,
        var(--wp--preset--color--base) 0%,
        color-mix(in srgb, var(--wp--preset--color--accent-alt) 10%, var(--wp--preset--color--base)) 55%,
        color-mix(in srgb, var(--wp--preset--color--accent-alt) 14%, transparent) 100%);
    text-align: center;
}
.qs-band--contact > p {
    color: var(--wp--preset--color--gray-300);
    max-width: 520px;
    margin-inline: auto;
}
