/*
 * Il CSS della tavola «Catalogo». Fonte unica: da qui si sincronizza la
 * regione @catalog:css della tavola e si genera assets/css/catalogo.css nel
 * child theme (al passo WordPress). Consuma i token del chrome: nella pagina
 * va caricato DOPO tokens.css e components.css.
 */

    /* ---- Base della tavola ----
       Scope .catalog: stesso impianto di .homepage-tavolo. Reset a bassa
       specificità (:where) così il vestito del chrome (.button, header,
       footer) vince col suo font-weight 700 — è la lezione del ponte. I token
       stanno su :root e arrivano anche qui. */
    .catalog {
      box-sizing: border-box;
      width: 100%;
      min-width: 320px;
      color: var(--ink);
      background: var(--paper);
      font-family: var(--font-body);
      font-size: var(--body-size);
      font-weight: var(--body-weight);
      line-height: var(--body-leading);
      -webkit-font-smoothing: antialiased;
      /* --nzb-filtri-offset (Task 1, «catalogo-filtri-applicazione-
         immediata»): quanto chrome sta sopra il contenuto — l'header sticky,
         + la barra admin quando c'è. Dichiarata QUI, su `.catalog` (Task 3,
         2 ago 2026, ex `.filters`): `.filters` e `.results-toolbar` la
         consumano entrambi (sticky `top`/`max-height` la prima, `scroll-
         margin-top` la seconda), ma sono RAMI diversi dell'albero — un
         custom property attraversa la discendenza, mai i rami — e serve un
         antenato comune. `.catalog` è il più vicino. L'aritmetica che
         giustifica 81/113 e la guardia che la misura (#30) restano accanto
         a `.filters`, dove questo numero è nato: qui vive solo la
         dichiarazione, UNA sola per un solo chrome, ora vera anche per i
         due consumatori. */
      --nzb-filtri-offset: 81px;
    }
    .admin-bar .catalog { --nzb-filtri-offset: 113px; }
    .catalog *, .catalog *::before, .catalog *::after { box-sizing: border-box; }
    /* :where() azzera la specificità del reset (0,0,1) così i colori dei
       componenti (.active-chip, .shortcut, .breadcrumb a) vincono: con
       `.catalog a` (0,1,1) la chip ereditava l'inchiostro scuro su fondo bark. */
    :where(.catalog) a { color: inherit; }
    :where(.catalog) button, :where(.catalog) input { font: inherit; }
    .catalog button { cursor: pointer; }

    /* ---- Hero ----
       Trama millimetrata dietro breadcrumb, titolo e ricerca; card pulite più
       sotto. Sulla carta i toni tenui (--ink-2/--olive) scendono sotto 4,5:1
       nel punto più scuro (misura della home): qui il testo sta su --bark/--ink. */
    .catalog-hero { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); }
    /* Il fondo a carta millimetrata e il respiro della shell sono nella terza
       casa (`.page-hero` in partials/componenti.css, Task 11, 7 ago 2026): la
       stessa <section> porta ora entrambe le classi — `page-hero` dal partial
       di sistema, `catalog-hero` come classe di superficie — e la testata dei
       percorsi riceve lo stesso fondo senza che nessuno lo ricopi. Qui resta
       il solo bordo inferiore, che è composizione del catalogo: separa la
       testata dalla griglia dei risultati, e nessun'altra testata ce l'ha. */

    /* Il componente è nella terza casa (partials/componenti.css): qui non
       resta nulla da comporre (Task 8, 6 ago 2026) — margine, colore,
       inchiostro dei link e stato attivo erano già gli stessi nell'unica
       superficie che finora lo possedeva; non è mai stata una variante
       propria del catalogo, solo la sua unica casa fino ad oggi. */

    /* Il componente è nella terza casa (partials/componenti.css): qui resta
       solo il colore, composizione della testata. */
    .catalog-hero .section-kicker { color: var(--bark); }
    .catalog-hero h1 { max-width: 720px; margin: var(--space-sm) 0 var(--space-md); color: var(--bark); font: var(--display-weight) clamp(2rem, 4vw, 2.75rem)/1.05 var(--font-display); letter-spacing: var(--display-tracking); text-wrap: balance; }
    /* Il componente è nella terza casa (partials/componenti.css): qui non
       resta nulla da comporre. Colonna, margine, inchiostro e tipografia
       che questa regola dichiarava (54ch, --space-lg, --ink, --lead-*)
       ERANO già, uno per uno, i valori di default del componente — la
       testata del catalogo non ha mai avuto una variante propria, solo la
       stessa forma scritta due volte. `.section-intro` nella testata riceve
       tutto da lì, senza bisogno di scoparlo qui. */

    /* Ricerca: il componente condiviso vive in partials/componenti.css
       (Task 10, 7 ago 2026) — padding/border/border-radius/background di
       `.course-search`, la label, la base del control e la sua svg, più lo
       spegnimento dell'anello di fuoco sull'input. Qui restano solo le
       proprietà che divergono davvero dalla home (max-width, margin) e le
       due regole che il catalogo soltanto possiede (sotto): non sono mai
       state portate sulla home, e restano perciò varianti scopate al
       catalogo, non componente. */
    /* Stessa fuga di `.sort-field` più sotto (`form, .form-item { margin-
       bottom: 2rem }`, theme.css:1534): anche `.course-search` è un `<form>`
       (template-parts/catalogo/hero.php:58) e non dichiarava un margine
       proprio — 32px non voluti sotto la ricerca, mai misurati finché non
       si è cercata la causa di `.sort-field`. */
    .course-search { max-width: 620px; margin: 0; }
    /* Correzione visiva V5 (giro di correzione, 31 lug 2026): nessuna altezza
       dichiarata — la riga cresceva col più alto dei suoi figli (il bottone
       «Cerca», che da `.button` senza override porta 12+12 di padding +
       27,2px di interlinea (var(--body-leading) 1,6 su var(--body-size)
       17px) + 2px di bordo = 53,2px, la stessa misura di `.catalog-empty__
       actions .button`/`.course-card__actions .button` PRIMA che questo
       progetto gli dichiarasse un'altezza fissa) più i 7+7 di padding
       verticale del control: 53,2+14=67,2, contro i 44+14=58 misurati sul
       servito quando il bottone perde l'altezza naturale. L'altezza va
       DICHIARATA qui — non lasciata al figlio più alto — per la stessa
       lezione del progetto (min-height non allinea niente): il campo `.button
       --primary` dentro riceve la propria altezza esplicita nel ponte (53,2,
       vedi chrome-wp.css), ma il control non deve dipendere da quel numero
       per restare a 67,2 — lo dichiara anche lui, `padding-block: 0`. Questa
       correzione non è mai stata portata sulla home (Task 10): resta quindi
       variante del catalogo, non componente — la home lascia la riga
       crescere col figlio più alto, come sempre. */
    .course-search__control { height: 67.2px; padding-block: 0; padding-inline: 18px 7px; }
    /* L'altezza esplicita dell'svg (22px), stessa storia: mai portata sulla
       home, che senza dichiararla riceve comunque 22px dal rapporto
       d'aspetto quadrato del viewBox — resa identica, valore scritto in un
       posto solo per superficie. */
    .course-search__control svg { height: 22px; }
    /* Correzione visiva V5 (giro di correzione, 31 lug 2026; ridotta al giro
       di correzione 1 del Task 10, 7 ago 2026 — vedi sotto). Il campo è
       `type="search"` (template-parts/catalogo/hero.php:62). Il tema padre
       porta `input[name="bbp_search"], input[type="search"], input.search {
       padding-left: 2rem; background-image: …lente… }` (theme.css:1458-1466)
       a specificità (0,1,1) — la STESSA di `.course-search input` (0,1,1):
       a parità vince chi carica dopo (theme.css, dopo catalogo.css nella coda
       WP), e sulla shorthand `padding: 0` di qui sopra vince solo la riga
       PIÙ SPECIFICA per il longhand `padding-left`, non l'intera dichiarazione
       — 32px misurati contro gli 0 della tavola. Stessa guerra per lo sfondo:
       la `background-image` del tema ospite va coperta da un `background`
       nostro qui, non basterebbe dichiararlo nel componente a (0,1,1). Sono le
       DUE sole proprietà con un concorrente reale a questa specificità: tutto
       il resto del campo (width, min-width, height, border, border-radius,
       outline, color, font-size) non ha un concorrente documentato altrettanto
       forte ed è salito in partials/componenti.css sotto il selettore NUDO
       `.course-search input` (giro di correzione 1) — la regola qualificata
       qui sotto resta comunque più specifica (0,2,1) e vincerebbe su
       qualunque proprietà la ridichiarasse, ma non lo fa più: zero
       sovrapposizione con il componente. Questo qualificatore non è mai stato
       portato sulla home (Task 10): resta perciò una copia propria del
       catalogo per padding/background soltanto — unificare IL SELETTORE
       alzerebbe anche la specificità del campo della home su proprietà che
       lì hanno un concorrente reale (padding/background), e da qui non si
       può provare che la sua resa resti la stessa. */
    .course-search input[type="search"] { padding: 0; background: transparent; }
    /* Lo spegnimento dell'anello di fuoco (focus-visible + placeholder) è
       salito nel componente (partials/componenti.css, Task 10): identico
       byte per byte alla copia che la home aveva, con lo stesso selettore
       nudo `input` — non dipende dal qualificatore di tipo qui sopra. */

    /* Chip rapide: scorciatoie a filtri. `.shortcut` è il componente
       condiviso (partials/componenti.css, Task 9, 6 ago 2026): qui resta
       solo la spaziatura verso l'elemento sopra, che diverge per pagina
       (24px qui, 16px sulla home) — composizione, non identità. */
    .chip-list { margin-top: var(--space-md); }

    /* ---- Corpo a due colonne ---- */
    .catalog-body { display: grid; grid-template-columns: 272px minmax(0, 1fr); gap: clamp(24px, 3vw, 40px); align-items: start; padding: var(--space-lg) 0 var(--space-section); }

    /* Azione scura e quieta: l'ember del sistema è una CTA sola per gruppo
       (la ricerca dell'hero), quindi «Applica filtri» e «Scopri il corso»
       usano il marrone, non l'arancione. */
    .catalog .button--dark { color: var(--paper); background: var(--bark); }
    .catalog .button--dark:hover, .catalog .button--dark:focus { color: var(--paper); background: var(--bark-deep); }

    /* ---- Filtri (form vero, sticky) ----
       ⚠️ NIENTE `max-height` + `overflow-y` + `overscroll-behavior` +
       `scrollbar-gutter` qui. C'erano (Task 1, «catalogo-filtri-
       applicazione-immediata»): senza, una colonna più alta del viewport si
       inchioda e la sua barra azioni non risale MAI — misurato il 2 ago
       2026 su staging, «Applica filtri» visibile solo a scrollY 4035 su una
       pagina di 4779 (l'84%). Ma quella cura era peggiore del male: una
       barra di scorrimento DENTRO i filtri, cioè un secondo asse in una
       pagina che ne ha già uno. Rilievo del committente sul servito, 3 ago
       2026: «mi va bene che la sidebar funziona tipo sticky che si aggancia
       ad un certo punto e sgancia alla fine, ma non voglio la barra
       verticale di scorrimento» — lo sticky resta, la barra no.
       Il difetto di «Applica filtri» irraggiungibile NON è dimenticato:
       chiude al Task 9, dove su desktop il pulsante sparisce del tutto (i
       filtri si applicano da soli, niente più «Applica» da raggiungere).
       Resta solo nel ripiego senza JavaScript, dove si scorre fino alla
       fine di `.catalog-body` e lo sticky si sgancia da sé — lì il fondo
       della colonna torna raggiungibile senza bisogno di un overflow.
       Chi legge questo blocco e pensa di «riparare» l'irraggiungibilità
       rimettendo `max-height` + `overflow-y`: è già stato provato, prodotto
       il difetto peggiore qui sopra, ed è la ragione di questo commento —
       e della guardia #29, che misura DIRETTAMENTE che la colonna non
       produce un secondo asse di scorrimento e che lo sticky resta acceso.
       `--nzb-filtri-offset` è l'UNICA dichiarazione di quanto chrome sta sopra
       la colonna — dal Task 3 (2 ago 2026) vive su `.catalog` (vedi lì),
       non più qui: `.results-toolbar`, che la consuma nel proprio `scroll-
       margin-top`, è un ramo diverso dell'albero da `.filters`, e un
       custom property non lo attraverserebbe se restasse dichiarata a
       questo livello. `.filters` la eredita esattamente come prima. Il
       numero è LETTO nel CSS vero (Task 1, «catalogo-filtri-applicazione-
       immediata»), non dedotto — il capitolato proponeva 101px come
       ipotesi, e la sua stessa aritmetica dichiarata (61+20) non tornava a
       quel numero:
         · header COMPATTO — `.site-header[data-compact="true"] .header-inner`
           (site-chrome.css:105) è alto 60px, + 1px di `border-bottom` di
           `.site-header` (site-chrome.css:95) = 61px. Compatto perché lo
           scroll a cui la colonna resta agganciata (qui e nella guardia, 1200px)
           sta ben oltre il `compactAfter: 120` di navigation.js — e comunque
           81px basta anche per l'header ESPANSO (77px, 4px di margine).
         · + 20px di aria: lo stesso numero del `top: 20px` che questa regola
           aveva prima di questo Task — non è un token di sistema (`--space-md`
           vale 24px, non 20): è l'intento originario, tenuto, con sopra
           l'header di cui prima non teneva conto.
       Totale SENZA barra admin: 61 + 20 = 81px. La barra admin di WordPress
       non aggiunge altezza all'header, gli sposta il `top` — la regola sta
       nel ponte, buddyboss-theme-child/assets/css/chrome-wp.css, e si cerca
       per SELETTORE (`body.admin-bar.nzeb-chrome :is(.site-header, .cart-
       drawer__panel)`), non per numero di riga: il «110» che questo commento
       citava era invecchiato di 27 righe, ed è il rilievo I4 della revisione
       finale del 3 ago 2026. L'header comincia 32px più in basso e quindi
       finisce 32px più in basso. Con barra: 61+32+20 = 113px.
       ⚠️ E il conto vale SOPRA i 782px. Sotto, la stessa regola nel ponte ha
       una controparte a `top: 46px` (la barra admin di WordPress diventa
       alta 46 su schermi stretti): là il totale sarebbe 61+46+20 = 127, non
       113. Questa dichiarazione non lo prevede — e non deve: sotto i 1100px
       `.filters` è un drawer `position: fixed`, che `top` lo ignora del
       tutto. Resta scoperto un solo consumatore, `.results-toolbar` qui
       sotto (`scroll-margin-top`), che sotto i 782px atterrerebbe 14px più
       in alto del dovuto per chi ha la barra admin — cioè lo staff, su
       telefono. Limite dichiarato, misurabile, non chiuso: la barra admin è
       spenta per i `customer` da `wc_disable_admin_bar()`, quindi non
       riguarda nessun visitatore vero.
       La guardia #30 misura la testa della colonna contro il fondo
       dell'header sul sito SERVITO: se questo conto è sbagliato, torna rossa. */
    /* ---- L'aggancio è col FONDO, non con la testa (correzione, 3 ago 2026) ----
       Il paragrafo sopra bocciava la barra di scorrimento e teneva lo sticky
       "classico" (`top: var(--nzb-filtri-offset)`, fisso): il committente
       l'ha visto sul servito e ha trovato da solo il difetto residuo —
       «fintanto che non scorro tutti i corsi, [la colonna] rimane ovviamente
       sticky e non scorre, quindi non riesco a visualizzarne tutte le
       opzioni». Ha ragione: con un `top` fisso la colonna si aggancia SUBITO
       in testa e il suo fondo — gli ultimi gruppi, `.filters__actions` —
       resta fuori dal viewport per tutta la discesa (lo scrollY 4035/4779
       misurato il 2 ago, sopra, è esattamente questo).
       La cura NON è tornare a `max-height`+`overflow-y`: resta bocciata,
       stessa ragione di sopra, stessa guardia #29 a sorvegliarla. La cura è
       far agganciare lo sticky col FONDO invece che con la testa.
       `position: sticky` si aggancia quando il bordo indicato da `top`
       incontra il bordo del viewport. Con un `top` NEGATIVO pari a
       (altezza finestra − altezza colonna − aria), a incontrarlo per primo è
       il FONDO della colonna: finché non è stata vista tutta scorre con la
       pagina, poi si ferma e la accompagna fino in fondo. Nessuna barra,
       un solo asse di scorrimento — quello della pagina.
       Il numero dipende da due altezze che QUESTO foglio non conosce (la
       finestra, e la colonna — che cambia col `<details>` «Mostra tutti gli
       argomenti», coi conteggi a zero che compaiono/spariscono e, dal
       Task 8 non ancora scritto, a ogni filtro applicato senza ricaricare):
       le legge `catalogo.js` e scrive il risultato come `--nzb-filtri-top`,
       inline, su QUESTO stesso elemento — mai su `.catalog`, per non
       confonderla con `--nzb-filtri-offset` (quanto chrome sta sopra, non
       quanto lo sticky è agganciato), che resta un'altra cosa e non si
       tocca.
       ⚠️ Senza JavaScript la catena `var(--nzb-filtri-top, var(--nzb-filtri-
       offset))` ripiega sull'offset dell'header: torna lo sticky classico
       di QUESTA riga prima di questa correzione — il rilievo del
       committente si ripresenterebbe, ma non è rotto: è il ripiego
       dichiarato, non un buco silenzioso.
       ⚠️ `.admin-bar .catalog { --nzb-filtri-offset: 113px }` (sopra) non ha
       bisogno di un gemello per `--nzb-filtri-top`: sia il calcolo JS (che
       legge `--nzb-filtri-offset` dal vivo dentro il proprio `min()`) sia il
       ripiego CSS senza JavaScript ereditano già il valore giusto — 81 o
       113 — da quella stessa dichiarazione. Verificato leggendo il file
       prima di scrivere, non assunto. */
    .filters {
      position: sticky;
      /* ⚠️ CORREZIONE (revisione, 3 ago 2026): `.results-toolbar` (sotto)
         porta il letterale di ripiego `81px` su `--nzb-filtri-offset`,
         questa dichiarazione no. Se quella variabile sparisse (un refuso
         nella dichiarazione su `.catalog`, un selettore riscritto), la
         toolbar reggerebbe e questa riga risolverebbe `top: auto` — uno
         sticky che non si aggancia mai, senza nessun sintomo finché
         qualcuno non prova a scorrere. Stesso ripiego, stesso numero. */
      top: var(--nzb-filtri-top, var(--nzb-filtri-offset, 81px));
    }
    .filters__card { background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-lg); padding: var(--space-md); }
    /* Niente border sul fieldset: con un border-top la legenda ci si incassa
       sopra (la «tacca» del <legend>), e la linea sembra attaccata al testo. Il
       separatore è un ::before del gruppo, con padding-top che stacca la
       legenda dalla linea. */
    .filters__group { position: relative; margin: 0; padding: 0; border: 0; min-width: 0; }
    .filters__group + .filters__group { margin-top: var(--space-md); padding-top: var(--space-md); }
    .filters__group + .filters__group::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: var(--line); }
    .filters__legend { margin: 0 0 12px; color: var(--bark); font-size: var(--label-size); font-weight: var(--label-weight); line-height: var(--label-leading); letter-spacing: var(--label-tracking); text-transform: uppercase; }

    .filters__option { display: grid; grid-template-columns: 20px 1fr auto; align-items: center; gap: 10px; padding: 6px 0; color: var(--ink); font-size: var(--body-size); cursor: pointer; }
    .filters__option--single { grid-template-columns: 20px 1fr; }
    /* Correzione visiva V2 (giro di correzione, 31 lug 2026): il gruppo
       Argomento è reso con `type="radio"` accanto ai `type="checkbox"` degli
       altri gruppi — deviazione dichiarata allora, mai accompagnata dal CSS.
       ⚠️ FATTO SUPERATO, corretto alla revisione finale (3 ago 2026 — I4):
       dal 2 ago l'Argomento è a scelta MULTIPLA e i suoi controlli sono
       `input[type="checkbox"] name="argomento[]"` (si cerca quel `name` in
       `template-parts/catalogo/filtri.php`, non un numero di riga — i numeri
       invecchiano, i simboli no). Questa dichiarazione RESTA e non è morta:
       vale per l'uno e per l'altro tipo, e senza il ramo `radio` una futura
       scelta singola tornerebbe a collassare a 20×2px. Il fatto storico
       resta scritto perché spiega perché il selettore nomina due tipi.
       Misurato sul servito: il tema padre porta
       `input[type="radio"], input[type="checkbox"] { width: auto; height:
       auto; padding: 0 }` (theme.css:1473-1479) a specificità (0,1,1) —
       ESATTAMENTE quella di `.filters__option input` (0,1,1): a parità vince
       chi carica dopo, e `theme.css` carica dopo `catalogo.css` nella coda di
       WordPress (misurato sull'ordine dei <link> in /shop/). Risultato: la
       casella collassa a un trattino (20×2 a riposo, 20×14 spuntata).
       Qualificare il tipo qui non è un vezzo: porta la specificità a (0,2,1),
       che batte (0,1,1) A PRESCINDERE dall'ordine di caricamento — niente
       :is() nel ponte, il problema si chiude qui. La tavola non prevede una
       forma diversa per il radio (nessun border-radius:50% dichiarato in
       nessuna variante): resta quadrato come il checkbox, per scelta della
       tavola stessa. */
    .filters__option input[type="checkbox"], .filters__option input[type="radio"] { appearance: none; -webkit-appearance: none; width: 20px; height: 20px; margin: 0; border: 1px solid var(--line-strong); border-radius: 5px; background: var(--surface); cursor: pointer; transition: border-color .15s var(--ease-out), background-color .15s var(--ease-out); }
    /* `.is-checked` accanto a `:checked`: la tavola è statica e il canvas non
       onora l'attributo checked al primo render — la classe mostra lo stato
       spunto (nel passo WP il server renderà classe E input insieme). */
    .filters__option input:checked,
    .filters__option.is-checked input { border-color: var(--lime); background: var(--lime); }
    .filters__option input:checked::after,
    .filters__option.is-checked input::after { display: block; width: 5px; height: 10px; margin: 2px auto 0; border: solid var(--ink-on-accent); border-width: 0 2px 2px 0; content: ""; transform: rotate(45deg); }
    /* Il focus lo dà il ring di sistema del chrome (:where(a,button,input)). */
    .filters__option:hover .filters__option-label { color: var(--bark); }
    .filters__count { color: var(--ink-2); font-size: var(--label-size); font-variant-numeric: tabular-nums; }

    /* Una voce esaurita resta al suo posto e lo dichiara (Task 4, 2 ago
       2026): sparire toglierebbe la casella con cui si torna indietro,
       restare premibile senza dirlo prometterebbe un risultato che non
       c'è. Il contrasto resta sopra 4,5:1 — è un'etichetta, non una
       decorazione (--ink-2 su --surface, la stessa coppia già usata per
       .filters__count qui sopra).
       ⚠️ `:has()`: primo uso di questo selettore in questo foglio (gli
       unici altri usi del progetto sono nel ponte, chrome-wp.css — un
       file diverso, con la propria giustificazione). Non è un problema di
       supporto (è nei motori evergreen dal 2023), ma qui costruisce lo
       stato dell'INTERO <label> a partire da un discendente (l'input),
       cosa che il resto di questo foglio fa sempre e solo con `:checked`
       diretto sull'input — una scelta dichiarata, non un'abitudine
       silenziosa in un foglio che si copia da un mockup.
       ⚠️ CORREZIONE (revisione esterna, 2 ago 2026, notte) — la STESSA
       meccanica di cascata dei cinque rilievi visivi del 2 agosto: dove il
       componente non dichiara un valore SUL NODO GIUSTO, lo decide
       qualcun altro. Due proprietà, misurate dal revisore, ripetevano
       l'errore:
       (1) `color` in hover — la riga qui sopra,
       `.filters__option:hover .filters__option-label { color: var(--bark)
       }`, scrive un valore DICHIARATO sullo `<span>`, e un dichiarato
       batte sempre un ereditato: a riposo l'etichetta esaurita era
       --ink-2 per EREDITÀ dal `<label>`, ma al passaggio del mouse
       tornava --bark, IDENTICA a una voce operabile — misurato:
       #6F635C a riposo contro #5A4A44 in ENTRAMBE in hover. La voce si
       dichiarava spenta finché nessuno la guardava.
       (2) `cursor` sul quadrato 20×20 — la regola dei checkbox/radio due
       righe sotto dichiara `cursor: pointer` SULL'INPUT, e quel
       dichiarato batte il `cursor: not-allowed` ereditato dal `<label>`:
       sopra il bersaglio vero (l'input, non il testo) il cursore diceva
       ancora «premimi».
       Le due righe sotto dichiarano ENTRAMBE le proprietà sul nodo che le
       mostra davvero (lo `<span>`, l'`<input>`), qualificate con
       `:has(input:disabled)` in più rispetto ai concorrenti — la stessa
       ricetta già in uso su `.filters__option input[type="checkbox"]`
       qui sopra (V2). Con l'approssimazione di specificità del banco
       (tests/lib/css-rules.cjs, `specificita()`: id/classi-e-pseudo/
       elementi, `:has(...)` contato come UNA pseudo-classe): (0,4,0)
       batte (0,3,0) sul `color` in hover, (0,3,1) batte (0,2,1) sul
       `cursor` — A PRESCINDERE dall'ordine di scrittura (la spec vera
       ricorre nell'argomento di `:has()` e darebbe (0,4,1)/(0,3,2), ma
       l'esito del confronto — chi vince — è lo stesso con entrambi i
       conteggi: decide già la seconda cifra, prima di arrivare alla
       terza). Guardia: tests/catalogo-filtri-parametri.test.cjs, con
       specificita()/piuForte() sulle stringhe VERE lette da questo file,
       non su una copia incollata a mano. */
    .filters__option:has(input:disabled) { color: var(--ink-2); cursor: not-allowed; }
    .filters__option:has(input:disabled) .filters__option-label,
    .filters__option:has(input:disabled):hover .filters__option-label { color: var(--ink-2); }
    .filters__option input:disabled { opacity: .45; }
    .filters__option:has(input:disabled) input:disabled { cursor: not-allowed; }

    .filters__more { margin-top: 2px; }
    .filters__more summary { display: inline-flex; align-items: center; gap: 7px; padding: 6px 0; color: var(--bark); font-size: var(--label-size); font-weight: 700; cursor: pointer; list-style: none; }
    .filters__more summary::-webkit-details-marker { display: none; }
    .filters__more summary::after { width: 7px; height: 7px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; content: ""; transform: rotate(45deg); transition: transform .2s var(--ease-out); }
    .filters__more[open] summary::after { transform: rotate(-135deg); }

    .filters__group--price .filters__legend { margin-bottom: 8px; }
    .filters__price-track { position: relative; height: 4px; margin: 6px 9px 18px; border-radius: 2px; background: var(--paper-alt); }
    .filters__price-fill { position: absolute; inset: 0 35% 0 0; border-radius: 2px; background: var(--lime); }
    /* Maniglie alle estremità del fill: decorative per costruzione (la tavola
       è statica, senza JS — il controllo dichiarato lì è sempre e solo gli
       input «Da/A»). Restano l'aspetto di riposo — a `--price-min`/
       `--price-max` non ancora calcolate (JS assente/non eseguito) il fill è
       fisso a «0 35% 0 0», la stessa geometria della tavola. */
    .filters__price-fill::before, .filters__price-fill::after { content: ""; position: absolute; top: 50%; width: 16px; height: 16px; border-radius: 50%; background: var(--surface); border: 2px solid var(--lime); box-shadow: 0 1px 3px rgba(42, 33, 28, .18); transform: translate(-50%, -50%); }
    .filters__price-fill::before { left: 0; }
    .filters__price-fill::after { left: 100%; }
    /* Riconciliazione (giro di riconciliazione, 1 ago 2026, difetto
       strutturale «la slide bar non esiste»): `input[type=range]` misurato a
       0 nel servito — le due maniglie sono SOLO le pseudo-classi qui sopra,
       non ricevono mai un evento. Il controllo reale restano gli input
       «Da/A» (mandato: «con i due campi numerici che già esistono come
       ripiego senza JavaScript») — qui si aggiunge un layer di
       potenziamento SOLO quando JS gira: `catalogo.js` inietta due
       `.filters__price-handle` reali (trascinabili, navigabili da tastiera,
       `role="slider"`) dentro `.filters__price-track` e marca il track
       `.is-enhanced`, che spegne le pseudo-maniglie decorative (sennò si
       vedrebbero doppie) e lascia il fill calcolato dai valori VERI dei due
       input, invece che fisso a «0 35%». A JS assente il track resta
       ESATTAMENTE come lo rende la tavola: null-safe by construction, non
       una classe scritta a mano nel PHP (che sarebbe presente anche senza
       script). */
    .filters__price-track.is-enhanced .filters__price-fill::before,
    .filters__price-track.is-enhanced .filters__price-fill::after { content: none; }
    .filters__price-handle { position: absolute; top: 50%; z-index: 1; width: 16px; height: 16px; border-radius: 50%; background: var(--surface); border: 2px solid var(--lime); box-shadow: 0 1px 3px rgba(42, 33, 28, .18); transform: translate(-50%, -50%); cursor: grab; touch-action: none; }
    .filters__price-handle:active { cursor: grabbing; }
    .filters__price-handle:focus-visible { outline: 3px solid var(--focus-ring, var(--tech)); outline-offset: 2px; }
    .filters__price-range { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
    /* Correzione visiva V6 (giro di correzione, 31 lug 2026): item di grid con
       `min-width: auto` implicito (il default UA) dentro `.filters__price-
       range { grid-template-columns: 1fr 1fr }`. L'input, senza `size` e
       senza `width` dichiarato, porta la sua larghezza intrinseca (~170-190px
       nella UA di Chrome): un `1fr` non può comprimerlo sotto quella misura
       finché `min-width` resta `auto`, e straripa fuori dalla colonna dei
       filtri (272px) — misurato bordo destro a 664px contro sidebar che
       finisce a 390. `min-width: 0` sul grid item E sull'input rompe il
       pavimento intrinseco; `width: 100%` sull'input lo fa aderire alla
       colonna invece di tornare al default. */
    /* `margin: 0` per la stessa fuga di `.sort-field__label`: anche questi
       due sono `<label>` (template-parts/catalogo/filtri.php), e prendevano
       i 4px di `label { margin-bottom: 0.25rem }` del tema padre. Qui non
       sposta un testo — la cella è una grid — ma allarga di 4px lo stacco
       sotto i campi Da/A, cioè un numero che questo foglio non ha mai
       scelto. Misurato insieme al rilievo sulla toolbar, 2 ago 2026. */
    .filters__price-field { display: grid; gap: 5px; min-width: 0; margin: 0; color: var(--ink-2); font-size: var(--label-size); }
    .filters__price-field input { width: 100%; min-width: 0; height: 40px; padding: 0 10px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); font-size: var(--body-size); }
    .filters__price-field input:focus-visible { border-color: var(--bark); }

    /* 🔴 LA BARRA AZIONI È INCHIODATA IN FONDO SOLO DENTRO IL DRAWER, ed è un
       RITORNO deliberato (revisione finale, 3 ago 2026 — B3): due correzioni
       giuste che si annullavano a vicenda.
       Il 2 ago il Task 1 aveva esteso `position: sticky; bottom: 0` a ogni
       larghezza, con una motivazione vera per quel momento: la colonna era
       diventata scorrevole e il suo fondo — «Applica filtri» — poteva uscire
       dal viewport. Poi sono arrivate due cose. (1) L'AGGANCIO COL FONDO (3
       ago, sotto `.filters`): la colonna si ferma col proprio fondo a filo
       del bordo della finestra, quindi il fondo della colonna È raggiungibile
       PER COSTRUZIONE — la ragione che aveva motivato l'estensione non esiste
       più. (2) Il Task 9 ha tolto «Applica filtri» dal flusso su desktop:
       restava una barra sticky con dentro il solo «Azzera», con bordo, fondo
       pieno, margini negativi e `z-index`, incollata al fondo dello schermo a
       COPRIRE l'ultimo gruppo di filtri per tutta la discesa. Cioè
       l'aggancio, nato dal rilievo del committente «non riesco a
       visualizzare tutte le opzioni», e la barra incollata sopra quelle
       stesse opzioni si cancellavano a vicenda su ~60px di colonna.
       Su desktop «Azzera» sta ora in fondo alla colonna, dove l'aggancio lo
       porta sotto gli occhi da sé. Nel drawer la barra resta inchiodata: lì
       non c'è nessun aggancio, il pannello è `position: fixed` e «Applica
       filtri» è l'unico modo di chiudere applicando.
       ⚠️ La regola del drawer sta più sotto, DENTRO `@media (max-width:
       1099.98px)`: più in basso nel file, quindi vince a parità di
       specificità — è il verso giusto. Il rischio che il commento del Task 1
       segnalava (una seconda dichiarazione più IN ALTO che vince col suo
       `padding-top`) qui non si presenta, ed è la ragione per cui questa
       riga non porta più `padding`/`margin` shorthand: dichiara solo lo
       stacco verticale del caso desktop, e il drawer lo sovrascrive per
       intero. Guardia: caso di geometria #49. */
    .filters__actions { display: flex; align-items: center; gap: 14px; margin-top: var(--space-md); padding-top: var(--space-md); border-top: 1px solid var(--line); }
    /* Stessa altezza dei bottoni delle card e della ricerca in topbar: 44px
       dichiarati, padding verticale azzerato. Senza, `.button` cresceva col
       proprio contenuto fino a 53,19 e «Applica filtri» era il solo controllo
       fuori misura della pagina. */
    .filters__actions .button { flex: 1; height: 44px; padding: 0 20px; }
    /* Il bottone si ritira da JavaScript su desktop (Task 9, catalogo.js,
       `aggiornaBottoneApplica()`): con ogni filtro che applica già da sé,
       «Applica filtri» confermerebbe un gesto già avvenuto.
       ⚠️ NON basta `.filters__actions .button[hidden]` (3 classi/attributi,
       specificità 0,3,0) — misurato sul RESO via CDP
       (`CSS.getMatchedStylesForNode`, non a mente): SU WORDPRESS il vero
       avversario non è `.button` da solo (site-chrome.css, `display:
       inline-flex`, 0,1,0), è IL PONTE in chrome-wp.css — `body.nzeb-chrome
       :is(.site-header, .megamenu, .site-footer, .cart-drawer, .cart-page,
       .catalog) .button` — che porta ANCHE il tipo `body` e vale 0,3,1: la
       stessa somma di classi (3) ma un tipo in più, e in CSS lo si confronta
       a cifre — 1 tipo batte 0 tipi anche a parità di classi, PRIMA ancora
       di guardare l'ordine nel foglio. Un primo giro è salito a 0,4,1 con lo
       stesso `body.nzeb-chrome` che usa il ponte, e si è fermato — SBAGLIATO
       (revisione, 3 ago 2026): `chrome-wp.css` è un file scritto a mano nel
       tema, MAI generato da `sync-chrome.cjs` (si veda `costruisci()` in
       quello script: tokens/css/home-css/catalog-css, niente chrome-wp), e
       la TAVOLA (`Catalogo.dc.html`, `<body>` senza classi — verificato)
       carica lo STESSO `catalogo.js` ma non quel foglio: lì il ponte non
       esiste, `body.nzeb-chrome` non corrisponde a niente, e un selettore
       che lo pretendesse lascerebbe il bottone visibile anche lì. Due
       selettori nella stessa dichiarazione, non uno: quello semplice serve
       dove il ponte non c'è (la tavola, 0,3,0 contro `.button` 0,1,0 basta),
       quello qualificato dove c'è (WordPress, 0,4,1 contro 0,3,1 del ponte). */
    .filters__actions .button[hidden], body.nzeb-chrome .filters__actions .button[hidden] { display: none; }
    .filters__reset { color: var(--bark); font-size: var(--label-size); font-weight: 700; text-decoration: underline; text-underline-offset: 3px; white-space: nowrap; }
    /* Stessa famiglia delle chip: «Azzera» non aveva uno stato dichiarato, e
       `a:hover` del tema padre gli dava il verde BuddyBoss. Qui l'hover si
       limita a scurire l'inchiostro — un comando quieto resta quieto. */
    .filters__reset:hover, .filters__reset:focus-visible { color: var(--bark-deep); }
    .filters__note { margin: 12px 2px 0; color: var(--ink-2); font-size: var(--label-size); line-height: 1.5; }

    /* ---- Griglia + card «fascicolo» ----
       Card PULITE: niente carta millimetrata (su una griglia densa diventa
       rumore). Ogni fatto sparisce se il dato manca: mai ore/CFP/pareri
       inventati. Social proof qualitativo (❝ N pareri), MAI stelle o media. */
    .course-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-md); }

    /* Lo stato «sto aggiornando» (Task 8, 3 ago 2026): la spunta di un filtro
       riscrive la lista in posto, e fra la spunta e la lista nuova passa il
       tempo di una richiesta. Non uno spinner — a 700ms lampeggerebbe più di
       quanto informi: la griglia si smorza e smette di ricevere click, così
       nessuno apre una card che sta per essere sostituita né spunta un
       secondo filtro su una lista che sta già cambiando.
       Niente ramo `prefers-reduced-motion` qui: la regola generale in fondo a
       questo foglio spegne già ogni transizione dentro `.catalog` con
       `transition-duration: .01ms !important`, e `.course-grid` è dentro
       `.catalog` — un secondo ramo sarebbe una riga che non può mai
       applicarsi, cioè una decorazione. */
    body.catalog-updating .course-grid { opacity: .55; pointer-events: none; transition: opacity .12s var(--ease-out); }

    /* `container-type: inline-size` (rilievo del committente, 11 ago 2026):
       l'etichetta del «+» qui sotto (`.quick-add::after`) deve avere un tetto
       di larghezza che SEGUA questa card — 293px in griglia a 3 colonne, più
       larga a 2 e a 1 — senza un numero cablato che scada al primo cambio di
       griglia. Un custom property dichiarato qui non basterebbe: il `%` di
       un discendente `position: absolute` risolve contro il SUO contenitore
       di posizionamento (`.quick-add`, 44px), non contro questa card, quindi
       una percentuale scritta qui non arriverebbe mai laggiù col valore
       giusto. La query container sì: rende questa card interrogabile in
       larghezza (`cqi`) da qualunque discendente, a qualunque breakpoint,
       senza una riga di CSS per soglia. Solo l'asse inline (nessun
       `container-type: size`): l'altezza della card resta governata dal
       flusso flex normale, non da questo. */
    .course-card { display: flex; flex-direction: column; overflow: hidden; container-type: inline-size; background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-lg); transition: border-color .2s var(--ease-out), box-shadow .2s var(--ease-out); }
    .course-card:hover { border-color: var(--bark); box-shadow: 0 8px 22px -14px color-mix(in srgb, var(--bark) 55%, transparent); }
    .course-card__media { position: relative; height: 116px; background: linear-gradient(135deg, var(--paper-alt), color-mix(in srgb, var(--paper-alt) 65%, var(--surface))); }
    .course-card__badge { position: absolute; top: 10px; left: 10px; padding: 4px 10px; border-radius: var(--radius-sm); font-size: var(--label-size); font-weight: 700; }
    .course-card__badge--soon { color: var(--paper); background: var(--bark); }
    .course-card__badge--new { color: var(--ink-on-accent); background: var(--lime); }
    .course-card__badge--free { color: var(--paper); background: var(--olive); }
    .course-card__badge--opening { color: var(--ink); background: var(--surface); border: 1px solid var(--line-strong); }

    .course-card__body { display: flex; flex: 1; flex-direction: column; padding: var(--space-md); }
    .course-label { margin: 0 0 6px; color: var(--olive); font-size: var(--label-size); font-weight: var(--label-weight); line-height: var(--label-leading); letter-spacing: var(--label-tracking); text-transform: uppercase; }
    .course-card h3 { margin: 0 0 7px; font: var(--title-weight) var(--title-size)/var(--title-leading) var(--font-display); letter-spacing: var(--title-tracking); }
    .course-card h3 a { color: var(--bark); text-decoration: none; }
    .course-card h3 a:hover { text-decoration: underline; text-underline-offset: 3px; }
    /* Il taglio a 3 righe (richiesta diretta del committente, 3 ago 2026): SOLO
       visivo, con `line-clamp`/`-webkit-line-clamp` — il testo intero resta
       nel DOM, letto per intero da motori di ricerca e screen reader. Un
       `wp_trim_words` in PHP butterebbe via parole per sempre, e questo campo
       ha già una decisione a registro contro quella strada («descrizione, on
       demand e ore ci saranno SEMPRE», Task 8 del lotto originale): il
       contenuto non si tocca, si veste. `-webkit-line-clamp` serve ancora a
       WebKit/Safari (fetta grossa del pubblico di questo sito): la sola
       proprietà standard non basta. `display: -webkit-box` e
       `-webkit-box-orient: vertical` sono i compagni OBBLIGATI di entrambe le
       forme — senza, né la prefissata né la standard clampano niente, restano
       lettera morta. `overflow: hidden` è ciò che nasconde le righe in più.
       ⚠️ Il numero di righe vive in UN POSTO SOLO, la custom property qui
       sotto: cambiarlo costa un carattere, non una ricerca nel foglio.

       Ritocco (richiesta diretta del committente, 3 ago 2026, dopo aver visto
       le 3 righe su staging): font più piccolo e interlinea più stretta. I
       due gradini del sistema non bastavano — `--body-size`/`--body-leading`
       è quello che c'era, `--label-size`/`--label-leading` è categoricamente
       un'altra cosa (grassetto 700, maiuscolo, `--label-tracking` 0.08em: le
       etichette, non la prosa). Serviva un gradino INTERMEDIO che il sistema
       non aveva: 0.9375rem/1.5, sotto le due custom property qui sotto,
       stessa disciplina di `--nzb-card-desc-righe` — un posto solo, un
       carattere per cambiarlo. Contrasto invariato (stesso `--ink-2` su
       `--surface`, 5,8:1 — il colore non dipende dalla dimensione del
       carattere, misurato di nuovo qui e non solo dedotto). */
    .course-card__desc { --nzb-card-desc-righe: 3; --nzb-card-desc-size: 0.9375rem; --nzb-card-desc-leading: 1.5; margin: 0 0 var(--space-sm); overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--nzb-card-desc-righe); line-clamp: var(--nzb-card-desc-righe); color: var(--ink-2); font-size: var(--nzb-card-desc-size); line-height: var(--nzb-card-desc-leading); }

    /* display/flex-wrap/list-style sono nel componente (partials/
       componenti.css, Task 10): qui resta la sola veste del catalogo — bordo
       superiore e voci separate da «·», non le pillole della home. */
    .course-facts { align-items: center; margin: 0 0 10px; padding-top: 10px; border-top: 1px solid var(--line); color: var(--ink); font-size: var(--label-size); }
    .course-facts li { display: inline-flex; }
    .course-facts li:not(:last-child)::after { content: "·"; margin: 0 8px; color: var(--ink-2); }
    .course-facts .is-cfp { color: var(--tech); font-weight: 700; }

    .course-pareri { display: inline-block; margin: 0 0 var(--space-sm); color: var(--ink-2); font-size: var(--label-size); text-decoration: none; }
    .course-pareri span { color: var(--bark); font-weight: 700; }
    .course-pareri:hover { color: var(--bark); text-decoration: underline; text-underline-offset: 3px; }

    .course-card__foot { margin-top: auto; }
    .course-card__price { margin: 0 0 var(--space-sm); color: var(--ink); font-size: var(--body-size); }
    .course-card__price .amount { color: var(--ink); font: 800 var(--title-size)/1 var(--font-display); }
    .course-card__price .amount--free { color: var(--olive); }
    .course-card__price s { margin-right: 4px; color: var(--ink-2); font-weight: 400; }
    .course-card__price .vat { color: var(--ink-2); font-size: var(--label-size); font-weight: 400; }
    .course-card__price--pending { color: var(--ink-2); font-weight: 700; }

    /* Riconciliazione del prezzo scontato (giro di riconciliazione, 1 ago
       2026): il Task 8 stampa il prezzo con `get_price_html()` di WooCommerce
       — corretto, è l'unico modo di non perdere il periodo dei due
       abbonamenti (D13) — ma quel markup non è mai stato vestito. La tavola
       disegna lo sconto con `<s>420 €</s> <span class="amount">349 €</span>`
       (`<s>` sorella di `.amount`, MAI un suo contenitore): `.course-card__
       price s` più sopra veste quella forma. WooCommerce non emette MAI `<s>`
       — emette `<del><span class="woocommerce-Price-amount amount">…</span>
       </del><ins><span class="woocommerce-Price-amount amount">…</span>
       </ins>`, verificato dal vivo su staging (curl, 8 card scontate su 12
       della prima pagina). Lo span interno porta GIÀ la classe `.amount`
       (oltre a `.woocommerce-Price-amount`, di WooCommerce): senza queste
       righe, `.course-card__price .amount` veste ALLO STESSO MODO il prezzo
       vecchio (dentro `<del>`) e quello nuovo (dentro `<ins>`) — stessa
       taglia (title-size/800), stesso inchiostro pieno, con in più la sola
       riga di sbarratura nativa del browser: il difetto riportato dal
       committente, «prezzi barrati male, dimensioni e colori diversi dal
       mockup». Si resta sull'ELEMENTO reale (`del`/`ins`), mai su una classe
       che il markup non porta — `prezzo-nudo.test.cjs` vieta di ricostruire
       il prezzo a mano, qui non si tocca il PHP, si veste il DOM che
       WooCommerce genera davvero. `margin-right: 4px` replica la stessa
       misura di `.course-card__price s` qui sopra (lo spazio fra vecchio e
       nuovo prezzo non può dipendere dal testo intermedio nascosto agli
       screen reader, che è `position: absolute` e non conta nel flusso). */
    .course-card__price del { margin-right: 4px; text-decoration: line-through; }
    .course-card__price del .amount { color: var(--ink-2); font: 400 var(--body-size)/1 var(--font-body); }
    .course-card__price ins { text-decoration: none; }
    /* Il suffisso IVA: la tavola lo scrive `<span class="vat">+IVA</span>`,
       WooCommerce lo scrive `<small class="woocommerce-price-suffix">+ IVA
       </small>` (stringa impostata nelle opzioni del negozio, non nel
       template — un'altra ragione per non ricostruirlo a mano). `<small>`
       eredita il colore `--ink` di `.course-card__price` e si limita a
       rimpicciolire il testo di suo (~80%, il default del browser): non è lo
       stesso valore di `--label-size` che la tavola dichiara, e non è mai
       stato `--ink-2`. Stessa ricetta di `.vat`, sull'elemento vero. */
    .course-card__price .woocommerce-price-suffix { color: var(--ink-2); font-size: var(--label-size); font-weight: 400; }

    /* `align-items: center` e non `stretch`: lo stretch tirava il «+»
       all'altezza del CTA (misurato 44×53,19 il 30 lug 2026), cioè un
       rettangolo verticale. Le due altezze ora sono dichiarate, e sono la
       stessa: 44px, la misura del bersaglio touch di questo progetto. */
    .course-card__actions { display: flex; align-items: center; gap: 10px; margin-top: var(--space-sm); }
    /* 🔴 Il «Visualizza carrello» che non deve esserci (rilievo del committente
       con schermata, 4 ago 2026, un'ora dopo il cablaggio del «+»).
       Riuscita l'aggiunta asincrona, WooCommerce INIETTA un link «Visualizza
       carrello» subito dopo il pulsante premuto — `add-to-cart.js`,
       `onAddedToCart`: `$button.after('<a class="added_to_cart wc-forward">…')`,
       salvo che su /cart/. È il canonico che ci si prende insieme al bottone
       canonico, e qui è due volte sbagliato: la riga azioni è una flex
       costruita per DUE elementi (CTA + «+») e il terzo la sfonda oltre il
       bordo della card, e soprattutto il pannello del carrello si è APPENA
       aperto sopra la pagina, con dentro «Vai al checkout» — il link iniettato
       è la stessa promessa, detta peggio e in un posto che non la regge.
       Si spegne qui e non con JavaScript, di proposito: togliere il nodo
       chiederebbe di agganciarsi a `added_to_cart` DOPO WooCommerce, cioè di
       dipendere dall'ordine in cui due `jQuery(document).ready` si registrano
       — una corsa che si vince quasi sempre, ed è il «quasi» a fare i difetti
       che non si riproducono. `display: none` lo toglie anche dall'albero di
       accessibilità: non resta un comando annunciato e invisibile.
       ⚠️ LA SPECIFICITÀ NON SI DEDUCE, SI MISURA — e la prima stesura di
       questa riga era `.course-card__actions .added_to_cart`, cioè (0,2,0),
       che il gate ha trovato PERDENTE sul servito. WooCommerce dichiara
       `.woocommerce a.added_to_cart { display: inline-block }` — (0,2,1), con
       l'elemento nel selettore — e batte (0,2,0) a prescindere dall'ordine dei
       fogli: il link restava lì, visibile, con la regola scritta e verde a
       leggerla. `.catalog` in testa e `a` esplicito portano questa a (0,3,1),
       che passa sopra senza dipendere da chi viene accodato prima. È la stessa
       legge dei dodici elementi del 2 agosto, applicata prima che il
       committente la vedesse una seconda volta. */
    .catalog .course-card__actions a.added_to_cart { display: none; }
    /* `padding-block: 0` perché `.button` porta 12+12 e con un'altezza fissa
       schiaccerebbe i 27,2px di interlinea contro i bordi. */
    .course-card__actions .button { flex: 1; height: 44px; padding: 0 20px; }
    /* `+` discreto: bordo lime, secondario rispetto a «Scopri il corso» (D5).
       Stessa altezza del CTA, non più basso: la gerarchia la dà il bordo/colore,
       non una taglia diversa. Quadrato per dichiarazione — width e height —
       non per effetto del vicino. */
    /* Correzione visiva V4 (giro di correzione, 31 lug 2026): `.quick-add` è un
       `<button>` SENZA classe `.button` (template-parts/catalogo/card.php) —
       non lo protegge il ponte, che veste solo `.button`. Il tema padre porta
       `button, .button, input[type="button"], input[type="submit"], … {
       padding: 10px 20px }` (theme.css:1733-1774): sull'elemento nudo la sua
       riga in testa (`button`, specificità 0,0,1) è l'UNICA dichiarazione di
       padding che lo tocca, perché QUI non ne avevamo mai scritta una — vince
       per assenza di concorrente, non per specificità. Su una scatola 44×44
       border-box con bordo 1px, 10px 20px lascia 22×2px di area utile: l'SVG
       (18×18 dichiarato) non ci sta, reso a competere con l'HTML nella
       verticale — misurato 2px. `padding: 0` qui (0,1,0, beats 0,0,1 a
       prescindere dall'ordine) chiude l'area utile a 42×42. SVG a 20×20 (da
       18): margine di sicurezza rispetto al bordo, misurato dal vivo. */
    .quick-add { flex: 0 0 auto; position: relative; display: inline-flex; align-items: center; justify-content: center; width: 44px; height: 44px; padding: 0; border: 1px solid var(--lime); border-radius: var(--radius-sm); background: var(--surface); color: var(--olive); cursor: pointer; transition: background-color .18s var(--ease-out), border-color .18s var(--ease-out); }
    .quick-add svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-width: 2; }
    /* Il «+» che spariva in hover/focus (rilievo del committente con
       schermata, 3 ago 2026): stessa legge dei dodici elementi corretti la
       sera del 2 agosto — l'ospite si prende esattamente le proprietà che lo
       stato non nomina. Misurato dal vivo su staging PRIMA di scegliere la
       cura (mai un numero a memoria, lezione già scritta su questo stesso
       file per `a:hover`): la riga che vince è `button:hover, button:focus,
       …` di theme.css (BuddyBoss) — bersaglio bare `button` + pseudo-classe,
       specificità (0,1,1) — con `color: var(--bb-primary-button-text-hover)`,
       che risolve a #FFFFFF. `.quick-add:hover` qui sotto dichiarava
       `border-color` e `background` ma MAI `color`: durante l'hover la sola
       regola che tocca `color` resta quella di riposo, `.quick-add { … color:
       var(--olive); … }`, specificità (0,1,0) — e (0,1,1) batte (0,1,0). Lo
       stroke dell'icona (`currentColor`) diventava bianco su `--lime-wash`:
       sparito. Sfondo e bordo NON si toccano, sono corretti (`--olive` su
       `--lime-wash`, il committente lo conferma esplicitamente).
       La cura: ridichiarare `color` qui, specificità (0,2,0) — una classe più
       una pseudo-classe — che batte (0,1,1) a prescindere dall'ordine dei
       fogli. `:focus-visible` accanto a `:hover`, stessa disciplina di
       `.filters-toggle`/`.filters__close`: chi arriva da tastiera ha lo
       stesso identico difetto e nessuno lo guardava. */
    .quick-add:hover, .quick-add:focus-visible { border-color: var(--olive); background: var(--lime-wash); color: var(--olive); }

    /* L'ETICHETTA DEL «+» AL PASSAGGIO DEL MOUSE (richiesta del committente, 4
       ago 2026). Il «+» è l'unico comando della card senza parole: dieci
       quadrati identici in pagina, e cosa faccia lo si scopre solo premendolo.

       Il testo NON si scrive qui: è `attr(aria-label)`, cioè la STESSA stringa
       che il pulsante annuncia alle tecnologie assistive (card.php). Una
       seconda copia nel foglio sarebbe uno stato duplicato — la forma di
       difetto che questo ramo ha già incontrato tre volte (gli hidden dei tre
       form, la classe `is-checked`, le chip dell'hero): il giorno in cui
       l'etichetta cambia, il vestito continuerebbe a dire quella vecchia.

       `:focus-visible` accanto a `:hover`, come ovunque su questa superficie:
       chi arriva da tastiera ha lo stesso bisogno e nessuno lo guarda mai.

       Ancorata a DESTRA e non centrata: `.course-card` porta `overflow:
       hidden` e il «+» è l'ultimo elemento della riga azioni — un'etichetta
       centrata sul pulsante sborderebbe dalla card e verrebbe tagliata a metà.
       Cresce verso sinistra, dentro la card.

       ⚠️ CORREZIONE (rilievo del committente, 11 ago 2026): «Cresce verso
       sinistra, dentro i 272px della card» era la promessa scritta qui, ma
       nessuna riga la faceva vera — `white-space: nowrap` teneva l'etichetta
       su UNA riga, larga quanto il suo testo (misurato 259,8px per
       «Aggiungi al carrello: Nuovi Requisiti Minimi Edifici 2026», 462px per
       il titolo più lungo del catalogo), e la card (293px) la tagliava
       sull'unico lato che `overflow: hidden` sorveglia in silenzio: quello
       SINISTRO, mai quello destro dove l'etichetta è ancorata. `white-space:
       normal` + `max-width` (sul tetto della card, `cqi`, vedi il commento
       su `.course-card` più sopra) È la riga che manca: l'etichetta va a
       capo invece di sfondare. Il testo resta `attr(aria-label)` — un
       secondo attributo/stringa per un'etichetta più corta è la stessa
       decisione già presa e già motivata qui sopra, non ribaltata per
       estetica. `* 2` sottrae il padding di ENTRAMBI i lati di
       `.course-card__body` (var(--space-md), 24px ciascuno): il bordo
       destro dell'etichetta combacia col bordo destro del pulsante, cioè col
       bordo destro della card meno un solo padding — sottrarne due lascia
       un margine di sicurezza pari al secondo padding, misurato e voluto,
       non un arrotondamento.

       ⚠️ `width: max-content` è OBBLIGATA insieme a `max-width`, non
       decorativa — misurato dal vivo prima di scriverla. Senza, `width`
       resta `auto`: per un fuori-flusso con `right` dichiarato e `left`
       auto, lo shrink-to-fit del browser calcola lo spazio disponibile dal
       CONTENITORE DI POSIZIONAMENTO (`.quick-add`, 44px), non da
       `max-width` — la riga clampava un valore che l'algoritmo non usava
       mai, e il tetto restava (misurato) ~79px, quasi una parola per riga.
       `width: max-content` esce dallo shrink-to-fit (la larghezza non è più
       `auto`) e lascia che sia `max-width` a decidere davvero: con la card
       a 293px il tetto misurato è 243,3px, la larghezza resa è
       ESATTAMENTE quella — la prova che il clamp funziona.

       `pointer-events: none`: l'etichetta nasce sotto il puntatore e non deve
       mai rubare il click al comando che descrive. `text-decoration: none`
       perché da quando il «+» è un `<a>` (4 ago 2026) l'ospite BuddyBoss
       sottolinea i link in hover — sul glifo non si vedeva, su un'etichetta di
       testo sì. (La proprietà non si eredita in un fuori-flusso, ma è una
       regola di specifica che non vale la pena di dover ricordare.)

       Il moto: nessun ramo `prefers-reduced-motion` qui — il reset generale in
       fondo a questo foglio copre già `.catalog *::after`. */
    .quick-add::after { content: attr(aria-label); position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 2; padding: 6px 10px; width: max-content; max-width: calc(100cqi - var(--space-md) * 2); border-radius: var(--radius-sm); background: var(--bark); color: var(--paper); font-size: var(--label-size); line-height: 1.2; font-weight: 600; white-space: normal; text-decoration: none; pointer-events: none; opacity: 0; transform: translateY(4px); transition: opacity .18s var(--ease-out), transform .18s var(--ease-out); }
    .quick-add:hover::after, .quick-add:focus-visible::after { opacity: 1; transform: translateY(0); }

    /* ---- Toolbar risultati ---- */
    /* L'ancora #risultati (il form unico vi atterra dopo ogni invio, Task 2)
       atterra SOTTO l'header sticky senza questa riga — e sotto il banner del
       consenso quando c'è. Stesso offset della colonna filtri, promosso
       (Task 3) su `.catalog`, l'antenato comune: un solo numero per un solo
       chrome, non uno per ciascun consumatore. */
    .results-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 20px; margin-bottom: var(--space-md); scroll-margin-top: calc(var(--nzb-filtri-offset, 81px) + 8px); }
    .results-toolbar__lead { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; }
    .results-count { margin: 0; color: var(--ink); font: 700 var(--body-size)/1.2 var(--font-body); }
    .results-count strong { font-weight: 800; }
    .active-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
    .active-chip { display: inline-flex; align-items: center; gap: 7px; padding: 5px 11px; border-radius: 999px; color: var(--paper); background: var(--bark); font-size: var(--label-size); font-weight: 700; text-decoration: none; }
    .active-chip:hover { background: var(--bark-deep); }
    .active-clear { color: var(--bark); font-size: var(--label-size); font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }

    /* Correzione visiva (rimisura del committente, 1 ago 2026, seguito di R1):
       `.sort-field` è un `<form>` (woocommerce/archive-product.php:165), e
       il tema padre porta `form, .form-item { margin-bottom: 2rem }`
       (theme.css:1534-1537, specificità 0,0,1) — mai contestato qui, perché
       non avevamo mai dichiarato un margine. I 32px di margin-bottom, nel
       calcolo dell'allineamento incrociato di `.results-toolbar` (flex,
       align-items:center), entrano nel BOX DEI MARGINI di `.sort-field`:
       la riga (`.results-toolbar`) si allunga a 76px (44 di contenuto + 32
       di margine) invece di restare a 44, e `.results-toolbar__lead` (senza
       margine, 20,4px) si centra dentro quei 76px invece che allinearsi a
       `.sort-field` — la stessa radice del disallineamento misurato sulla
       riga (V5, «.results-toolbar 76 vs 44»), non un effetto di V1. */
    .sort-field { display: inline-flex; align-items: center; gap: 8px; margin: 0; }
    /* Correzione visiva (rilievo del committente, 2 ago 2026, sera): stessa
       famiglia della fuga qui sopra, su un elemento diverso — il tema padre
       porta `label { margin-bottom: 0.25rem }` (theme.css, specificità
       (0,0,1)) e questa riga non aveva mai dichiarato un margine. In un flex
       `align-items: center` si centra il BOX DEI MARGINI, non il testo: i
       4px sotto l'etichetta la spingono in su di 2. Misurato sul servito
       (staging, /shop/, viewport 1700): centro dell'etichetta a 722,84
       contro i 724,84 identici di `<select>`, bottone «Applica» e riga
       `.sort-field` — «Ordina» era l'unica parola fuori registro della
       toolbar. `margin: 0` e non `margin-bottom: 0`: la spaziatura di questo
       componente la dà `gap`, e una sola dichiarazione chiude anche gli
       altri tre lati contro qualunque regola futura del padre. */
    .sort-field__label { margin: 0; color: var(--ink-2); font-size: var(--label-size); font-weight: 700; }
    /* Riga di controlli (select + bottone «Applica», Task 8b — la tavola statica
       non ha questo bottone: senza JS il <select> non si invia da sé). Stessa
       lezione del 30 lug, ripetuta qui: `.sort` porta un'altezza dichiarata da
       sempre (42px), ma `.button` no — da `.button` prenderebbe `min-height:
       44px` + padding 12+12, cresciuta a 53,19px per l'interlinea (misurato,
       giro di correzione 1 del Task 8b). Il bottone nuovo dichiara 44px (il
       bersaglio touch del progetto, .filters__actions .button / .course-card__
       actions .button), e il select sale da 42 a 44 per stare sulla STESSA riga
       alla STESSA altezza — non un'inconsistenza lasciata alla centratura del
       flex, come il resto del chrome (.header-search: input/bottone/chiusura
       tutti a 44px sulla stessa riga).
       ⚠️ FATTO SUPERATO, dichiarato alla revisione finale (3 ago 2026 — I4):
       quel bottone «Applica» dentro `.sort-field` NON ESISTE PIÙ. Il Task 2
       (2 ago) ha lasciato UN SOLO «Applica filtri», quello della colonna
       filtri, e ha tolto il doppione dalla toolbar. La riga
       `.sort-field .button` qui sotto è quindi una regola che oggi non veste
       NESSUN elemento: si tiene — costa zero e riaggancerebbe da sé un
       bottone che qualcuno rimettesse lì — ma è DICHIARATA morta invece di
       restare a far credere a chi legge che un bottone ci sia. Il suo
       gemello nel ponte (buddyboss-theme-child/assets/css/chrome-wp.css,
       selettore `body.nzeb-chrome .catalog .sort-field .button`) porta la
       stessa dichiarazione, per la stessa ragione, ed è morto insieme a
       questo. `.sort` invece è vivissimo: il `<select>` c'è. */
    .sort { height: 44px; padding: 0 38px 0 12px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background-color: var(--surface); background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none' stroke='%235A4A44' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 13px center; background-size: 15px; color: var(--ink); font-size: var(--label-size); font-weight: 700; cursor: pointer; appearance: none; -webkit-appearance: none; }
    .sort-field .button { height: 44px; padding: 0 20px; }
    .sort:focus-visible { border-color: var(--bark); }
    /* select e summary non rientrano nel :where(a,button,input) del chrome:
       ricevono qui lo stesso ring di sistema (3px tech). */
    .catalog select:focus-visible, .catalog summary:focus-visible { outline: 3px solid var(--focus-ring, var(--tech)); outline-offset: 2px; }

    /* ---- Box editoriale «Percorsi» ---- */
    .route-callout { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px 24px; margin: var(--space-lg) 0; padding: var(--space-md) var(--space-lg); border: 1px solid var(--lime); border-radius: var(--radius-lg); background: var(--lime-wash); }
    .route-callout__title { margin: 0 0 4px; color: var(--bark); font: var(--title-weight) var(--title-size)/1.2 var(--font-display); }
    .route-callout__desc { margin: 0; color: var(--ink); font-size: var(--body-size); }
    .route-callout .button { flex: 0 0 auto; }

    /* Azione d'uscita quieta (Percorsi, edizione superata): profilo, non pieno. */
    .catalog .button--outline { color: var(--bark); background: transparent; border-color: var(--bark); }
    .catalog .button--outline:hover, .catalog .button--outline:focus { color: var(--paper); background: var(--bark); }

    /* Variante «edizione superata»: quieta, non un errore. */
    .course-card__badge--past { color: var(--ink-2); background: var(--paper-alt); }
    .course-card--past { background: var(--paper); }
    .course-card--past h3 a { color: var(--ink-2); }
    .course-card--past .course-card__desc { color: var(--ink-2); }

    /* ---- Paginazione classica (pagina nell'URL) ----
       Riconciliazione (giro di riconciliazione, 1 ago 2026): il tema padre
       porta un componente `.pagination` GENERICO (probabilmente pensato per
       un widget nativo, non per questo catalogo — theme.css, selettore
       `.pagination span, .pagination a { display: inline-block; padding:
       .5rem .75rem; … }`, specificità (0,1,1): batte `.pagination__page` da
       solo (0,1,0) A PRESCINDERE dall'ordine di caricamento, la stessa
       famiglia di V2/V4/V5a — la quarta ricorrenza in questo lotto di una
       proprietà `display` spenta da una regola più specifica del padre, non
       dall'ordine. `align-items`/`justify-content` restavano dichiarate ma
       morte (servono solo su `flex`/`grid`, mai su `inline-block`): il
       numero non si centrava nel riquadro 42×42. `.pagination span,
       .pagination li.active a` e `.pagination a:hover` aggiungono `color:
       rgba(0,0,0,.5)` — fuori dal design system, misurato dal vivo,
       corrisponde esattamente al rilievo del committente. Anche il
       contenitore `.pagination` stesso è generico (theme.css: `margin: 27px
       0; font-size: 16px`, stessa specificità (0,1,0), stesso esito): tutta
       la famiglia si richiude qualificando ogni selettore con un antenato
       VERO già scritto nel markup — `.catalog` per il blocco `.pagination`
       stesso (stessa ricetta di `.catalog .button--dark` più sopra),
       `.pagination` per i suoi elementi BEM (`__page`/`__next`/`__dots`,
       che vivono SEMPRE dentro `.pagination`) — mai un antenato inventato.
       Specificità (0,2,0) o superiore: vince sempre, nessun `:is()` nel
       ponte necessario, il conflitto è tutto interno alla fonte del
       catalogo. */
    .catalog .pagination { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 8px; margin-top: var(--space-lg); font-size: var(--body-size); }
    .pagination .pagination__page { display: inline-flex; align-items: center; justify-content: center; min-width: 42px; height: 42px; padding: 0 6px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); font-weight: 700; text-decoration: none; }
    /* ⚠️ `color` in hover: il tema padre porta `.pagination a:hover { color:
       rgba(0, 0, 0, .5) }` (0,2,1) — lo stesso grigio semitrasparente già
       tolto ai numeri a riposo dalla riconciliazione qui sopra, che però
       vestiva solo lo stato di riposo. Passandoci sopra, il numero tornava
       grigio: il difetto era ancora lì, in agguato in uno stato che nessuno
       aveva guardato. `.pagination .pagination__page:hover` fa (0,3,0) e
       batte (0,2,1) a prescindere dall'ordine. */
    .pagination .pagination__page:hover { color: var(--ink); border-color: var(--bark); background: var(--lime-wash); }
    .pagination .pagination__page--current { color: var(--paper); background: var(--bark); border-color: var(--bark); pointer-events: none; }
    .pagination .pagination__next { display: inline-flex; align-items: center; gap: 6px; height: 42px; padding: 0 16px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--bark); font-weight: 700; text-decoration: none; }
    .pagination .pagination__next:hover { color: var(--bark); border-color: var(--bark); background: var(--lime-wash); }
    /* La freccia DENTRO «Avanti» (`next_text` di paginate_links() è
       `Avanti <span aria-hidden="true">→</span>`, woocommerce/archive-
       product.php): uno `<span>` che vive dentro `.pagination`, quindi lo
       prende in pieno la stessa `.pagination span` del tema padre citata
       qui sopra — `display: inline-block; padding: .5rem .75rem; border:
       1px solid transparent; border-radius: 3px` più la riga che le dà
       `border-color: #E0E1E1; color: rgba(0, 0, 0, .5); cursor: text`.
       Le tre regole di questo blocco vestono i figli DIRETTI di
       `.pagination`, non i loro discendenti: la freccia non aveva un
       concorrente, e usciva come una tessera grigia 41,9×35 dentro il
       bottone — il «bordo strano sulla freccia» misurato dal committente il
       2 ago 2026, con in più un inchiostro rgba(0,0,0,.5) diverso dal
       `--bark` della parola «Avanti» accanto. `.pagination .pagination__next
       span` fa (0,2,1): batte (0,1,1) a prescindere dall'ordine, come le
       altre riconciliazioni di questo blocco. `color: inherit` e non un
       token: la freccia è la stessa parola, deve seguire il colore del
       bottone anche nello stato :hover. */
    .pagination .pagination__next span { display: inline; padding: 0; border: 0; border-radius: 0; color: inherit; background: none; line-height: inherit; }
    /* I puntini (paginate_links(), classe "dots" sui salti — es. 1 2 3 … 7):
       la tavola non li mostra mai (solo 3 pagine, mai un salto), quindi non
       c'è una regola da copiare pari pari. Uno `<span>` senza href, MAI
       vestito da tessera cliccabile (niente bordo/sfondo/dimensione fissa di
       .pagination__page): il giro di correzione 1 del Task 8b l'ha misurato
       vestito come una tessera reale — un elemento che sembra un bottone e
       non lo è. Stesso ingombro minimo (min-width) solo per non far saltare
       la riga, nessun altro tratto da tessera. Stessa riconciliazione di
       specificità delle tre regole qui sopra: senza l'antenato, il tema
       padre vince anche qui su `.pagination span`.

       ⚠️ «Nessun altro tratto da tessera» era un'intenzione, non una misura:
       la cascata si decide PER PROPRIETÀ, e questa riga non ne dichiarava
       due — `border` e `padding`. Su quelle due `.pagination span` (0,1,1)
       non aveva concorrenti e vinceva per ASSENZA, non per specificità:
       misurato sul servito il 2 ago 2026, `border: 1px solid rgb(224, 225,
       225)` e `padding: 8px 12px` sui puntini — esattamente il riquadro che
       questo commento dichiarava di aver evitato, accanto ai numeri veri.
       Si dichiarano qui, in riga: vincere una proprietà non protegge le sue
       vicine. */
    .pagination .pagination__dots { display: inline-flex; align-items: center; justify-content: center; min-width: 42px; height: 42px; padding: 0; border: 0; background: none; color: var(--ink-2); font-weight: 700; }

    /* ---- Empty state ----
       ⚠️ Dal 2 ago 2026 (Task 3, correzione della revisione) resta DENTRO
       `.results` — niente `grid-column`. Il giro precedente l'aveva resa
       figlia diretta di `.catalog-body` con `grid-column: 1 / -1`, per
       farla occupare le due colonne intere: misurato dal vivo (Playwright,
       1440×900), la riga 1 della griglia è già piena (`.filters` +
       `.results`), quindi l'auto-placement spinge un terzo figlio a due
       colonne in riga 2 — sotto i 799px fissi di `.filters`, fuori
       viewport appena si atterra su `#risultati`. 928px dentro `.results`
       non è un difetto: è la colonna giusta, accanto ai filtri. */
    .catalog-empty { padding: 0 0 var(--space-section); }
    .catalog-empty__note { margin: 0 0 12px; color: var(--ink-2); font-size: var(--label-size); font-weight: 700; letter-spacing: var(--label-tracking); text-transform: uppercase; }
    .catalog-empty__box { padding: clamp(28px, 4vw, 48px); border: 1px dashed var(--line-strong); border-radius: var(--radius-lg); background: var(--surface); text-align: center; }
    .catalog-empty__title { margin: 0 0 10px; color: var(--bark); font: var(--headline-weight) var(--headline-size)/1.1 var(--font-display); letter-spacing: var(--headline-tracking); }
    .catalog-empty__lead { max-width: 52ch; margin: 0 auto var(--space-md); color: var(--ink-2); font-size: var(--lead-size); line-height: var(--lead-leading); }
    .catalog-empty__actions { margin-bottom: var(--space-md); }
    /* «Azzera i filtri» è un <a> vestito da bottone: senza altezza dichiarata
       cresceva col contenuto come gli altri .button (53,19px) e restava fuori
       misura rispetto alla riga dei filtri e alle card. */
    .catalog-empty__actions .button { height: 44px; padding: 0 20px; }
    .catalog-empty__hints { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 12px; margin: 0; padding: 0; list-style: none; }
    .catalog-empty__hints a { display: inline-flex; min-height: 40px; align-items: center; padding: 8px 14px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); color: var(--bark); font-size: var(--label-size); font-weight: 700; text-decoration: none; }
    .catalog-empty__hints a:hover { border-color: var(--bark); background: var(--lime-wash); }

    /* Elementi del drawer: nascosti su desktop, i filtri sono già in colonna. */
    .filters-toggle, .filters__drawer-head { display: none; }
    /* `text-decoration: none` (3 ago 2026, Task 6): `.filters-toggle` era un
       `<button>`, ora è un `<a href="#catalog-filters">` (senza JavaScript
       deve poter APRIRE il pannello navigando l'hash, un bottone non lo fa
       da solo) — e uno `<a>` senza questa riga porta la sottolineatura di
       riposo che il browser dà di suo a un link, mai dichiarata finché
       l'elemento era un bottone. */
    .filters-toggle { align-items: center; gap: 8px; height: 42px; padding: 0 16px; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--bark); font-size: var(--label-size); font-weight: 700; text-decoration: none; cursor: pointer; }
    .filters-toggle svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-width: 2; }
    .filters-toggle__count { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 5px; border-radius: 999px; background: var(--lime); color: var(--ink-on-accent); font-size: 12px; }
    /* `color`/`border-color` RIDICHIARATI in hover (3 ago 2026, Task 6, stessa
       famiglia di difetti dell'hover delle chip — otto punti il 2 ago sera,
       vedi `.breadcrumb a:hover` in partials/componenti.css, Task 8): ora che `.filters-toggle` è un
       `<a>`, il tema padre porta `a:hover { color: var(--bb-primary-color) }`
       a specificità (0,1,1) — NON QUALIFICATA (assets/css/theme.css del tema
       padre, verificato sul foglio servito: `a:hover { color: var(--bb-
       primary-color); … }`, senza ancestor), non (0,2,1) come le regole
       qualificate della stessa famiglia (`.breadcrumb a:hover` e simili) —
       che batte la regola di riposo qui sopra (0,1,0). La sotto-regola qui
       sotto, `.filters-toggle:hover, .filters-toggle:focus-visible`, è
       (0,2,0) — una classe più una pseudo-classe — e vince PER SPECIFICITÀ
       (2 batte 1 nella colonna delle classi), non per l'ordine di
       caricamento. Senza questa riga il bottone «Filtri» diventerebbe verde
       BuddyBoss al passaggio del mouse. Il colore in hover resta lo STESSO
       di riposo (`--bark`): non è un restyling, è la stessa identità che il
       tema ospite altrimenti si riprende. */
    .filters-toggle:hover, .filters-toggle:focus-visible { color: var(--bark); border-color: var(--bark); background: var(--lime-wash); }

    /* ---- Responsive alle soglie del chrome ----
       < 1100px: una colonna, filtri in drawer off-canvas, griglia a 2.
       < 768px: griglia a 1.
       ⚠️ `1099.98px` e non `1099px` (revisione finale, 3 ago 2026 — I3):
       catalogo.js dichiara la soglia complementare `(min-width: 1100px)`, e
       la coppia `1099` / `1100` lasciava scoperta la fascia frazionaria fra
       i due interi — a 1099,5px (zoom del browser, DPR non intero, un
       `innerWidth` frazionario) non corrispondeva NÉ questa media query né
       quella dello script, e la pagina non si comportava come nessuna delle
       due versioni. `.98` è il passo standard: sta sotto la risoluzione con
       cui i motori valutano le media query (1/64 px), quindi non lascia
       scoperto nessun valore rappresentabile. La guardia pretende che le due
       soglie COINCIDANO, non i due letterali — tests/catalogo-drawer.test
       .cjs. */
    @media (max-width: 1099.98px) {
      .catalog-body { grid-template-columns: 1fr; }
      .course-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
      .filters-toggle { display: inline-flex; }

      /* Fino al 2 ago 2026 qui c'era un reset esplicito (`max-height: none;
         overflow-y: visible; overscroll-behavior: auto; scrollbar-gutter:
         auto;`) perché la regola generale di `.filters` portava quelle
         stesse proprietà per lo sticky desktop, e senza azzerarle qui
         comprimevano l'overlay `position:fixed; inset:0` SOTTO i suoi
         100vh/100vw (misurato: 799px invece di 900) — con lui si
         comprimeva anche `.filters__form`, assoluto dentro questo
         contenitore. Dal 3 ago 2026 (rilievo del committente sul servito:
         via la barra di scorrimento dalla colonna) `.filters` desktop non
         porta più quelle proprietà: non c'è più niente da azzerare, e un
         reset che non difende più niente è una decorazione — tolto. Il
         contenitore che scorre DAVVERO sul drawer resta `.filters__card`,
         due righe più giù — non `.filters`. */
      .filters { position: fixed; inset: 0; z-index: 1200; padding: 0; background: color-mix(in srgb, var(--bark-deep) 55%, transparent); visibility: hidden; opacity: 0; transition: opacity .25s var(--ease-out), visibility .25s var(--ease-out); }
      /* Due strade per aprire, UNA verità sullo stato (3 ago 2026, Task 6 —
         la lezione del pannello materiali, 29 lug 2026): `:target` regge
         quando manca JavaScript o prima che sia girato (delay-JS di WP
         Rocket in produzione, il caso normale, non l'eccezione), `is-open`
         quando c'è. Il modulo (catalogo.js) si allinea all'hash all'init e a
         ogni `hashchange`, altrimenti Escape e backdrop diventerebbero
         decorativi su un pannello aperto con l'hash già in URL — esattamente
         il vicolo cieco già pagato una volta su questo stesso progetto.
         Caso di geometria #34: prova che senza JavaScript il click sul
         bottone «Filtri» apre DAVVERO il pannello. */
      .filters:target, .filters.is-open { visibility: visible; opacity: 1; }
      /* ⚠️ CORREZIONE — R1 della revisione (3 ago 2026): `body.filters-open
         { overflow: hidden }` viveva QUI FUORI dalla media query (Task 9),
         globale su ogni larghezza. Prima di questo task poteva scattare
         SOLO dal click sul toggle, che sopra la soglia è `display: none`:
         irraggiungibile, quindi innocuo. Da questo task l'accende anche
         l'URL (`allinea()`, catalogo.js, senza soglia di viewport): chi apre
         `/shop/#catalog-filters` a 1440px — l'URL che il delay-JS di WP
         Rocket lascia in barra a QUALUNQUE utente mobile che tocchi «Filtri»
         prima che lo script giri, e che basta condividere — otteneva
         `body.filters-open` con drawer, toggle e ✕ tutti `display: none`
         lì: pagina bloccata, senza uscita visibile. Qui dentro la media
         query la stessa riga si applica SOLO dove il drawer esiste.
         `:has(.filters:target)` copre anche il ramo SENZA JavaScript (che
         non aggiunge mai `body.filters-open`, solo l'hash): stessa cura,
         nello stesso posto, per i due percorsi che questo task tiene
         allineati.

         ⚠️ DUE REGOLE, NON UNA CON LA VIRGOLA — controintuitivo, e la ragione
         va tenuta scritta qui o il prossimo passaggio le riunisce «per
         pulizia». `body.filters-open, body:has(.filters:target) { … }` in
         UNA sola selector list è un gruppo NON forgiving: in CSS un membro
         invalido rende invalida l'INTERA regola (stesso principio già a
         registro in assets/css/chrome-wp.css:1014-1024, dove è dichiarato
         «FAIL-CLOSED» di proposito). Su un motore senza `:has()` la regola
         unica cadrebbe TUTTA — anche `body.filters-open`, che PRIMA di
         questa correzione era una riga a sé e funzionava ovunque. L'effetto
         sarebbe la riapparizione SILENZIOSA di R1b (la pagina scorre dietro
         al drawer aperto) ma stavolta anche nel percorso CON JavaScript, su
         qualunque motore senza `:has()` — e nessun banco su Chromium può
         vederlo, perché Chromium lo supporta sempre: è un limite noto,
         dichiarato, non una guardia mancante per distrazione. Qui — a
         differenza di chrome-wp.css, dove il degrado va verso il rendering
         SICURO del tema padre — un fallimento del gruppo intero
         peggiorerebbe le cose, non le renderebbe sicure: le due regole
         restano SEPARATE apposta, così un motore senza `:has()` perde SOLO
         il ramo di ripiego, non anche quello che già funzionava. */
      body.filters-open { overflow: hidden; }
      body:has(.filters:target) { overflow: hidden; }
      /* Pannello: colonna con testata fissa in alto e azioni fisse in basso,
         solo i gruppi scorrono. scrollbar-gutter riserva lo spazio della barra
         così i conteggi non le finiscono sotto. */
      /* Stessa fuga di `.sort-field`/`.course-search` (`form, .form-item {
         margin-bottom: 2rem }`, theme.css:1534): qui è più grave, perché
         `.filters__form` è `position:absolute; top:0; bottom:0` — un
         margin-bottom non azzerato NON sposta solo il box, lo ACCORCIA (il
         browser lo sottrae all'altezza disponibile fra top e bottom).
         Misurato: 868px invece di 900 dentro un `.filters` a tutto
         schermo — 32px di corpo scorrevole in meno proprio dove V7 ne
         aveva già bisogno. */
      .filters__form { position: absolute; top: 0; bottom: 0; left: 0; margin: 0; display: flex; flex-direction: column; width: min(86%, 340px); background: var(--paper); transform: translateX(-100%); transition: transform .28s var(--ease-out); }
      .filters:target .filters__form, .filters.is-open .filters__form { transform: translateX(0); }
      .filters__drawer-head { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; padding: var(--space-md) var(--space-md) var(--space-sm); border-bottom: 1px solid var(--line); }
      .filters__drawer-title { color: var(--bark); font: var(--title-weight) var(--title-size)/1 var(--font-display); }
      /* Correzione visiva V4, stessa radice di `.quick-add` qui sopra:
         `.filters__close` è anch'esso un comando senza `.button`
         (si cerca `class="filters__close"` in template-parts/catalogo/
         filtri.php — il simbolo, non il numero di riga: quel «181» è
         invecchiato di 221 righe, ed è il rilievo I4 della revisione
         finale), preso in pieno dal
         `padding: 10px 20px` del tema padre perché qui non ne avevamo mai
         scritto uno. Su 42×42 border-box lascia 22×0px di area utile —
         l'SVG (18×18) reso a 0px, invisibile. `padding: 0` (0,1,0, batte
         0,0,1 a prescindere dall'ordine) + SVG a 20×20.

         Rinforzo (rimisura del committente, 1 ago 2026, R2): la rimisura ha
         trovato il bottone collassato a 0×0. Una rimisura indipendente con
         Chromium headless (Playwright, contesto pulito, stessa pagina
         servita, viewport 414×900, drawer aperto) lo trova invece 40×40 con
         l'SVG 20×20 reso correttamente — non ho riprodotto lo 0×0 nel mio
         test, quindi NON dichiaro qui una causa certa. Ho però trovato,
         confrontandolo con `.quick-add` qui sopra, un'unica differenza
         strutturale verificabile: `.quick-add` porta `flex: 0 0 auto`
         (impedisce allo shrink del flex padre di comprimerlo), `.filters__
         close` no — dentro `.filters__drawer-head { justify-content:
         space-between }` è un item flex-shrink:1 di default, l'unico dei
         due bottoni del giro V4 senza questa riga. Aggiunta per chiudere il
         divario con l'unico caso gemello che funziona, non perché ne abbia
         la controprova diretta: se il difetto persiste dopo questo
         rilascio, serve la rimisura del committente per isolare la causa
         vera, che i miei strumenti da riga di comando non hanno trovato. */
      /* `text-decoration: none` (3 ago 2026, Task 6): stessa ragione di
         `.filters-toggle` qui sopra — `.filters__close` era un `<button>`,
         ora è un `<a href="#risultati">` (senza JavaScript deve poter
         CHIUDERE navigando fuori dall'hash `#catalog-filters`), e un `<a>`
         senza questa riga porta di suo la sottolineatura di riposo. */
      .filters__close { flex: 0 0 auto; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 0; border: 1px solid var(--line-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--ink); text-decoration: none; cursor: pointer; }
      .filters__close svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-width: 2; }
      /* `color` RIDICHIARATO in hover (3 ago 2026, Task 6, stessa famiglia
         dell'hover di `.filters-toggle` qui sopra e delle chip, 2 ago sera):
         ora che `.filters__close` è un `<a>`, `a:hover { color: var(--bb-
         primary-color) }` del tema padre (0,1,1), NON QUALIFICATA — stesso
         chiarimento della nota su `.filters-toggle:hover` più sopra —
         batterebbe la regola di riposo (0,1,0) e la ✕ diventerebbe verde
         BuddyBoss al passaggio del mouse; la sotto-regola qui sotto è
         (0,2,0) e vince per specificità, non per ordine. Resta `--ink`, lo
         STESSO colore di riposo dichiarato due righe sopra — non `--bark`
         come per `.filters-toggle`: i due comandi
         non condividono lo stesso inchiostro a riposo, e la cura di questa
         famiglia di difetti è «tenere lo stato hover uguale a quello di
         riposo», non introdurre un colore nuovo che il riposo non ha mai
         dichiarato. */
      .filters__close:hover, .filters__close:focus-visible { color: var(--ink); text-decoration: none; }
      /* Correzione visiva V7 (giro di correzione, 31 lug 2026): la struttura
         header/corpo/footer c'era già (`.filters__drawer-head` fisso,
         `.filters__card` con `overflow-y: auto`, `.filters__actions`
         `sticky`) — il sospetto iniziale era che mancasse `min-height: 0`
         (lo scoglio classico di flexbox: un item porta `min-height: auto`
         di default, che senza questa riga equivale all'altezza intrinseca
         del contenuto e impedirebbe la compressione).

         ⚠️ Corretto dopo la guardia di geometria (tests-geometria/
         geometria.test.cjs, caso #10, 1 ago 2026): il banco di mutazione ha
         provato che rimuovere SOLO `min-height: 0` non riapre il difetto.
         La ragione è nella spec (CSS Flexbox §4.5, «automatic minimum
         size»): un flex item che è ANCHE uno scroll container — qualunque
         `overflow` diverso da `visible` su un asse — ha già dimensione
         minima automatica 0, senza bisogno di dichiararla. `overflow-y:
         auto`, qui sotto, c'è dal Task 9 e non l'ha mai toccata questo giro:
         bastava lei. `min-height: 0` resta nel CSS — non è sbagliata, è
         ridondante, e la sua ridondanza è ora DIMOSTRATA (non presunta) dal
         banco di mutazione, che muta `overflow-y` per riprodurre il
         pannello che non scorre, non `min-height`. Il difetto originale
         (gruppo Prezzo a `bottom: 746` contro un `.filters.is-open` di
         723px) restava comunque risolto da questa correzione: solo
         l'attribuzione della causa, in questo commento, era imprecisa. */
      .filters__card { flex: 1 1 auto; min-height: 0; overflow-y: auto; scrollbar-gutter: stable; padding: var(--space-md); border: 0; }
      .filters__note { display: none; }
      /* 🔴 LA BARRA AZIONI TORNA A ESSERE SOLO DEL DRAWER (revisione finale,
         3 ago 2026 — B3; il perché per esteso sta sopra `.filters__actions`,
         nella regola generale). In breve: qui l'aggancio col fondo non
         esiste (il pannello è `position: fixed`, i gruppi scorrono dentro
         `.filters__card`) e «Applica filtri» è l'unico modo di chiudere
         applicando — inchiodarla in fondo è ciò che la rende raggiungibile.
         Su desktop invece l'aggancio porta il fondo della colonna a filo
         della finestra da sé, e una barra incollata lì coprirebbe l'ultimo
         gruppo di filtri: due correzioni giuste che si annullavano.
         `margin` e `padding` sono shorthand di proposito — devono
         sovrascrivere per intero il `margin-top`/`padding-top` della regola
         generale, non sommarcisi. */
      .filters__actions { position: sticky; bottom: 0; z-index: 1; margin: var(--space-md) calc(-1 * var(--space-md)) 0; padding: var(--space-sm) var(--space-md); background: var(--surface); border-top: 1px solid var(--line); }
    }

    @media (max-width: 767px) {
      .course-grid { grid-template-columns: 1fr; }
    }

    /* Rispetta la preferenza di moto ridotto: il chrome spegne solo `.button`,
       una tavola vergine non eredita l'interruttore generale della home. Qui il
       reset copre drawer, hover delle card, chevron del disclosure. !important
       perché deve battere le transizioni dichiarate sulle classi del catalogo. */
    @media (prefers-reduced-motion: reduce) {
      .catalog *, .catalog *::before, .catalog *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
      }
    }
