diff --git a/src/components/CalendarioModal.astro b/src/components/CalendarioModal.astro
new file mode 100644
index 0000000..870730d
--- /dev/null
+++ b/src/components/CalendarioModal.astro
@@ -0,0 +1,203 @@
+---
+// Popup a pieno schermo con la griglia delle class in Sala Piccola.
+//
+// Il calendario è ricostruito dal dato (`data/calendari.ts`) e non è l'immagine ricevuta dal
+// cliente: si legge da telefono, si stampa, e per correggere una cella non serve rifare uno
+// screenshot. Quale griglia compare lo decide la data — quella in vigore, o la prossima
+// dichiarando da quando entra.
+//
+// Si apre da qualunque elemento con `data-apri-calendario` nella stessa pagina.
+import { calendarioDaMostrare, GIORNI, DISCIPLINE, type Giorno } from '../data/calendari';
+import { t } from '../lib/content';
+
+const scelta = calendarioDaMostrare(new Date());
+const cal = scelta?.cal ?? null;
+const dal = cal ? new Date(cal.from).toLocaleDateString('it-IT', {
+ day: 'numeric', month: 'long', year: 'numeric', timeZone: 'Europe/Rome',
+}) : '';
+// Discipline davvero presenti in questa griglia: una legenda che elenca anche ciò che non c'è
+// fa cercare lezioni che non esistono.
+const presenti = cal
+ ? DISCIPLINE.filter((d) => cal.righe.some((r) => Object.values(r.celle).includes(d)))
+ : [];
+---
+{cal && (
+
+)}
+
+
+
+
diff --git a/src/data/calendari.ts b/src/data/calendari.ts
new file mode 100644
index 0000000..40fcbdc
--- /dev/null
+++ b/src/data/calendari.ts
@@ -0,0 +1,125 @@
+// Calendari delle class in Sala Piccola.
+//
+// Trascritti dai due fogli forniti dal cliente il 10/08/2026 (immagini su Wasabi, cartella
+// del progetto). Sono dato e non immagini di proposito: così la griglia si legge da telefono,
+// si stampa in PDF e si corregge una cella senza rifare uno screenshot.
+//
+// Ogni calendario porta la sua finestra di validità: la pagina mostra quello in vigore, e
+// quando nessuno lo è ancora mostra il prossimo dichiarando da quando entra. Il passaggio da
+// un orario all'altro avviene da solo, senza deploy — stesso principio delle edizioni della
+// pagina promo (`data/promo-edizioni.ts`).
+
+export const GIORNI = ['LUN', 'MAR', 'MER', 'GIO', 'VEN', 'SAB', 'DOM'] as const;
+export type Giorno = (typeof GIORNI)[number];
+
+/** Le discipline della sala: il colore di ognuna è definito nel componente del calendario. */
+export const DISCIPLINE = ['REFORMER', 'PILATES', 'YOGA', 'MOBILITY'] as const;
+export type Disciplina = (typeof DISCIPLINE)[number];
+
+export interface RigaCalendario {
+ ora: string;
+ /** Fascia messa in risalto nel foglio del cliente (le righe gialle). */
+ clou?: boolean;
+ celle: Partial>;
+}
+
+export interface Calendario {
+ id: string;
+ /** Come lo chiama il cliente: è anche l'etichetta mostrata in pagina. */
+ titolo: string;
+ sala: string;
+ /** Validità, ISO con offset esplicito (il server gira in UTC). `to: null` = a tempo indeterminato. */
+ from: string;
+ to: string | null;
+ /** Giorni in cui la sala è chiusa: colonna barrata, non vuota — vuoto e chiuso non sono lo stesso. */
+ chiusi: Giorno[];
+ /** Precisazione su una colonna, es. il sabato aperto una volta sola. */
+ noteGiorno?: Partial>;
+ righe: RigaCalendario[];
+}
+
+export const calendari: Calendario[] = [
+ {
+ id: 'settembre-1-18',
+ titolo: "Dall'1 al 18 settembre",
+ sala: 'Sala piccola',
+ from: '2026-09-01T00:00:00+02:00',
+ to: '2026-09-18T23:59:59+02:00',
+ chiusi: ['DOM'],
+ noteGiorno: { SAB: 'solo il 12 settembre' },
+ righe: [
+ { ora: '6.15', celle: {} },
+ { ora: '7.00', clou: true, celle: { MAR: 'YOGA' } },
+ { ora: '7.45', clou: true, celle: { MAR: 'PILATES', MER: 'REFORMER' } },
+ { ora: '8.30', celle: { MER: 'REFORMER', VEN: 'MOBILITY' } },
+ { ora: '9.15', celle: { VEN: 'REFORMER', SAB: 'MOBILITY' } },
+ { ora: '10.00', celle: { VEN: 'REFORMER', SAB: 'PILATES' } },
+ { ora: '10.45', celle: { SAB: 'REFORMER' } },
+ { ora: '11.30', celle: { SAB: 'REFORMER' } },
+ { ora: '12.15', clou: true, celle: { MAR: 'REFORMER', MER: 'REFORMER', GIO: 'REFORMER' } },
+ { ora: '13.00', clou: true, celle: { MAR: 'REFORMER', MER: 'PILATES', GIO: 'REFORMER' } },
+ { ora: '13.45', celle: {} },
+ { ora: '14.30', celle: { MAR: 'REFORMER' } },
+ { ora: '15.15', celle: { MAR: 'MOBILITY' } },
+ { ora: '16.00', celle: {} },
+ { ora: '16.45', celle: { MAR: 'REFORMER' } },
+ { ora: '17.30', clou: true, celle: { MAR: 'REFORMER', GIO: 'REFORMER' } },
+ { ora: '18.15', clou: true, celle: { MAR: 'REFORMER', GIO: 'REFORMER' } },
+ { ora: '19.00', clou: true, celle: { MAR: 'REFORMER', GIO: 'REFORMER' } },
+ { ora: '19.45', clou: true, celle: { MAR: 'REFORMER', GIO: 'PILATES' } },
+ ],
+ },
+ {
+ id: 'settembre-21-in-poi',
+ titolo: 'Dal 21 settembre in poi',
+ sala: 'Sala piccola',
+ from: '2026-09-21T00:00:00+02:00',
+ to: null,
+ chiusi: ['DOM'],
+ righe: [
+ { ora: '6.15', celle: { MER: 'MOBILITY' } },
+ { ora: '7.00', clou: true, celle: { MAR: 'YOGA', MER: 'REFORMER' } },
+ { ora: '7.45', clou: true, celle: { MAR: 'PILATES', MER: 'REFORMER', VEN: 'PILATES' } },
+ { ora: '8.30', celle: { MAR: 'REFORMER', MER: 'REFORMER', VEN: 'MOBILITY' } },
+ { ora: '9.15', celle: { VEN: 'REFORMER', SAB: 'MOBILITY' } },
+ { ora: '10.00', celle: { VEN: 'REFORMER', SAB: 'PILATES' } },
+ { ora: '10.45', celle: { SAB: 'REFORMER' } },
+ { ora: '11.30', celle: { GIO: 'YOGA', SAB: 'REFORMER' } },
+ { ora: '12.15', clou: true, celle: { MAR: 'REFORMER', MER: 'REFORMER', GIO: 'REFORMER' } },
+ { ora: '13.00', clou: true, celle: { MAR: 'REFORMER', MER: 'PILATES', GIO: 'REFORMER' } },
+ { ora: '13.45', celle: {} },
+ { ora: '14.30', celle: { MAR: 'REFORMER' } },
+ { ora: '15.15', celle: { MAR: 'MOBILITY' } },
+ { ora: '16.00', celle: {} },
+ { ora: '16.45', celle: { MAR: 'REFORMER' } },
+ // ⚠️ Mercoledì 17.30 e 18.15: nel foglio ricevuto quelle due celle sono coperte dal
+ // riquadro "Opzioni Incolla" di Excel, quindi non sono leggibili. Qui restano vuote
+ // perché inventarle sarebbe peggio: vanno confermate dal cliente.
+ { ora: '17.30', clou: true, celle: { LUN: 'PILATES', MAR: 'REFORMER', GIO: 'REFORMER' } },
+ { ora: '18.15', clou: true, celle: { LUN: 'REFORMER', MAR: 'REFORMER', GIO: 'REFORMER' } },
+ { ora: '19.00', clou: true, celle: { LUN: 'REFORMER', MAR: 'REFORMER', GIO: 'REFORMER' } },
+ { ora: '19.45', clou: true, celle: { LUN: 'REFORMER', MAR: 'REFORMER', GIO: 'PILATES' } },
+ ],
+ },
+];
+
+/** Servizi dalla cui pagina si apre il calendario della sala. */
+export const SERVIZI_CON_CALENDARIO = ['performance-class', 'holistic-class'];
+
+/**
+ * Il calendario da mostrare: quello in vigore adesso, oppure — se nessuno lo è ancora — il
+ * primo che entrerà in vigore, segnalato come futuro. `null` solo se non ce n'è nessuno da
+ * mostrare, e in quel caso la voce non compare in pagina.
+ *
+ * Serve il ripiego sul futuro perché il link esiste **prima** che il primo orario cominci: un
+ * "nessun calendario disponibile" ad agosto non direbbe niente a chi vuole sapere gli orari
+ * di settembre.
+ */
+export function calendarioDaMostrare(now: Date, list: Calendario[] = calendari):
+ { cal: Calendario; futuro: boolean } | null {
+ const t = now.getTime();
+ const attivo = list.find((c) => t >= Date.parse(c.from) && (c.to === null || t <= Date.parse(c.to)));
+ if (attivo) return { cal: attivo, futuro: false };
+ const prossimi = list.filter((c) => Date.parse(c.from) > t).sort((a, b) => Date.parse(a.from) - Date.parse(b.from));
+ return prossimi.length ? { cal: prossimi[0], futuro: true } : null;
+}
diff --git a/src/data/content-seed.ts b/src/data/content-seed.ts
index c8396a9..286899e 100644
--- a/src/data/content-seed.ts
+++ b/src/data/content-seed.ts
@@ -386,6 +386,15 @@ export const contentSeed: SeedEntry[] = [
T('promo.faq.6.q', 'Posso chiedere un rimborso?'),
H('promo.faq.6.a', 'Per richieste di rimborso o problemi con l\'acquisto scrivici su WhatsApp o via email. Valutiamo ogni situazione in modo diretto e umano.'),
+ // --- Calendario delle class (popup dalle pagine servizio) ---
+ // La griglia è dato (`data/calendari.ts`); qui ci sono solo le etichette attorno.
+ T('calendario.link-label', 'Calendario', []),
+ T('calendario.pdf-label', 'Scarica in PDF', []),
+ T('calendario.chiudi-label', 'Chiudi', []),
+ T('calendario.futuro-label', 'In vigore dal', []),
+ T('calendario.chiuso-label', 'Chiuso', []),
+ T('calendario.nota', 'Gli orari possono subire variazioni. Le lezioni si prenotano dall\'app Bsport.'),
+
// --- Pagina promo, stato "nessuna promozione in corso" ---
// Fra un'edizione e la successiva la pagina resta raggiungibile (è nel menu e nei link già
// girati ai clienti): mostra questo invece dei prezzi di una promozione finita.
diff --git a/src/pages/services/[slug].astro b/src/pages/services/[slug].astro
index fdfa72f..8b5ef1c 100644
--- a/src/pages/services/[slug].astro
+++ b/src/pages/services/[slug].astro
@@ -7,12 +7,18 @@ import { performanceClassPricing } from '../../data/pricing';
import { serviceIcons } from '../../data/service-icons';
import T from '../../components/content/T.astro';
import TImg from '../../components/content/TImg.astro';
+import CalendarioModal from '../../components/CalendarioModal.astro';
+import { calendarioDaMostrare, SERVIZI_CON_CALENDARIO } from '../../data/calendari';
import { t } from '../../lib/content';
export const prerender = false;
const service = getService(Astro.params.slug!);
if (!service) return new Response(null, { status: 404 });
const slug = service.slug;
+// Il calendario della sala si apre dalle pagine delle class. La voce compare solo se c'è
+// davvero una griglia da mostrare (in vigore o prossima): un link che apre un popup vuoto è
+// peggio di nessun link.
+const conCalendario = SERVIZI_CON_CALENDARIO.includes(slug) && calendarioDaMostrare(new Date()) !== null;
const others = services.filter((p) => p.slug !== service.slug && p.grid !== false);
const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/program-*.jpg', { eager: true });
const imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default;
@@ -59,6 +65,16 @@ const tones = ['tan', 'taupe', 'dark'];
))}
+ {conCalendario && (
+
+ )}
@@ -95,6 +111,8 @@ const tones = ['tan', 'taupe', 'dark'];
)}
+
+ {conCalendario && }