generated from bisco/codex-bootstrap
feat: build headless theatre laboratory website
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import type { SiteSettings } from "../lib/api";
|
||||
interface Props { settings: SiteSettings }
|
||||
const { settings } = Astro.props;
|
||||
const tel = settings.phone.replace(/[^+\d]/g, "");
|
||||
const whatsapp = settings.whatsapp.replace(/\D/g, "");
|
||||
const hasContacts = settings.email || settings.phone || settings.whatsapp;
|
||||
---
|
||||
|
||||
<section class="contact section" id="contatti" aria-labelledby="contact-title">
|
||||
<div class="container contact-grid">
|
||||
<div class="contact-intro">
|
||||
<p class="eyebrow">Facciamo conoscenza</p>
|
||||
<h2 id="contact-title">Vieni a trovarci</h2>
|
||||
<p class="lead">Se vuoi capire se questo percorso fa per te, scrivici o chiamaci. Ti risponderemo con calma, senza formule automatiche.</p>
|
||||
{settings.address && <address>{settings.address}</address>}
|
||||
</div>
|
||||
{hasContacts ? (
|
||||
<div class="contact-actions" aria-label="Canali di contatto">
|
||||
{settings.email && <a href={`mailto:${settings.email}`}><span>Scrivi una mail</span><strong>{settings.email}</strong><span aria-hidden="true">↗</span></a>}
|
||||
{settings.phone && <a href={`tel:${tel}`}><span>Chiama</span><strong>{settings.phone}</strong><span aria-hidden="true">↗</span></a>}
|
||||
{settings.whatsapp && <a href={`https://wa.me/${whatsapp}`} target="_blank" rel="noreferrer"><span>WhatsApp</span><strong>{settings.whatsapp}</strong><span aria-hidden="true">↗</span></a>}
|
||||
</div>
|
||||
) : <p class="empty-state contact-empty">I contatti saranno disponibili presto.</p>}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import type { FeatureCardContent } from "../lib/api";
|
||||
interface Props { cards: FeatureCardContent[] }
|
||||
const { cards } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="features section section-light" aria-labelledby="features-title">
|
||||
<div class="container">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Tre modi per cominciare</p>
|
||||
<h2 id="features-title">Perché partecipare</h2>
|
||||
</div>
|
||||
{cards.length ? (
|
||||
<div class="feature-grid">
|
||||
{cards.map((card, index) => (
|
||||
<article class="feature-card">
|
||||
<span class="card-number" aria-hidden="true">0{index + 1}</span>
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.text}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : <p class="empty-state">I percorsi del laboratorio saranno pubblicati presto.</p>}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
import type { SiteSettings } from "../lib/api";
|
||||
interface Props { settings: SiteSettings }
|
||||
const { settings } = Astro.props;
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-main">
|
||||
<div><a class="wordmark" href="#inizio">{settings.site_name}</a><p>{settings.footer_text}</p></div>
|
||||
<nav aria-label="Social media">
|
||||
{settings.instagram && <a href={settings.instagram} target="_blank" rel="noreferrer">Instagram</a>}
|
||||
{settings.facebook && <a href={settings.facebook} target="_blank" rel="noreferrer">Facebook</a>}
|
||||
</nav>
|
||||
</div>
|
||||
<div class="container footer-bottom">
|
||||
<p>© {year} {settings.site_name}</p>
|
||||
<p>Progetto realizzato con cura · <span>Privacy (TODO)</span></p>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import type { GalleryItemContent } from "../lib/api";
|
||||
interface Props { items: GalleryItemContent[] }
|
||||
const { items } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="gallery section" id="galleria" aria-labelledby="gallery-title">
|
||||
<div class="container">
|
||||
<div class="section-heading"><p class="eyebrow">Dentro il laboratorio</p><h2 id="gallery-title">Galleria</h2></div>
|
||||
{items.length ? (
|
||||
<div class="gallery-grid">
|
||||
{items.map((item) => (
|
||||
<figure class="gallery-item">
|
||||
<img src={item.image.url} alt={item.image.alt} width="860" height="700" loading="lazy" />
|
||||
<figcaption><span>{item.category}</span>{item.caption}</figcaption>
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
) : <p class="empty-state">Le fotografie del laboratorio arriveranno presto.</p>}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
interface Props {
|
||||
siteName: string;
|
||||
ctaLabel: string;
|
||||
ctaUrl: string;
|
||||
}
|
||||
|
||||
const { siteName, ctaLabel, ctaUrl } = Astro.props;
|
||||
const links = [
|
||||
["Il laboratorio", "#laboratorio"],
|
||||
["Il maestro", "#maestro"],
|
||||
["Le lezioni", "#lezioni"],
|
||||
["Progetti", "#spettacoli"],
|
||||
["Galleria", "#galleria"],
|
||||
];
|
||||
---
|
||||
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="wordmark" href="#inizio" aria-label={`${siteName}, torna all’inizio`}>{siteName}</a>
|
||||
<nav class="desktop-nav" aria-label="Navigazione principale">
|
||||
{links.map(([label, href]) => <a href={href}>{label}</a>)}
|
||||
</nav>
|
||||
<a class="button button-small desktop-cta" href={ctaUrl}>{ctaLabel}</a>
|
||||
<details class="mobile-menu">
|
||||
<summary aria-label="Apri il menu"><span aria-hidden="true"></span><span class="menu-label">Menu</span></summary>
|
||||
<nav aria-label="Navigazione mobile">
|
||||
{links.map(([label, href]) => <a href={href}>{label}</a>)}
|
||||
<a class="button button-small" href={ctaUrl}>{ctaLabel}</a>
|
||||
</nav>
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import type { HomePageContent } from "../lib/api";
|
||||
interface Props { content: HomePageContent }
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="hero section" id="inizio" aria-labelledby="hero-title">
|
||||
<div class="container hero-grid">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Laboratorio teatrale · corpo, voce, relazione</p>
|
||||
<h1 id="hero-title">{content.hero_title}</h1>
|
||||
<p class="hero-subtitle">{content.hero_subtitle}</p>
|
||||
<div class="button-row">
|
||||
<a class="button" href={content.cta_primary_url}>{content.cta_primary_label}</a>
|
||||
<a class="text-link" href={content.cta_secondary_url}>{content.cta_secondary_label}<span aria-hidden="true"> ↓</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<figure class="hero-visual">
|
||||
<img src={content.hero_image.url} alt={content.hero_image.alt} width="960" height="1120" fetchpriority="high" />
|
||||
<figcaption>Il gruppo è materia viva.</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
import type { HomePageContent } from "../lib/api";
|
||||
interface Props { content: HomePageContent }
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="split-section section" id="laboratorio" aria-labelledby="laboratory-title">
|
||||
<div class="container split-grid">
|
||||
<div class="editorial-image">
|
||||
<img src={content.laboratory_image.url} alt={content.laboratory_image.alt} width="960" height="760" loading="lazy" />
|
||||
</div>
|
||||
<div class="split-copy">
|
||||
<p class="eyebrow">Pratica e ricerca</p>
|
||||
<h2 id="laboratory-title">{content.laboratory_title}</h2>
|
||||
<div class="lead rich-text" set:html={content.laboratory_body} />
|
||||
<ul class="practice-list" aria-label="Aree di lavoro">
|
||||
<li>Corpo</li><li>Voce</li><li>Improvvisazione</li><li>Ascolto</li><li>Presenza scenica</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import type { LessonContent } from "../lib/api";
|
||||
interface Props { lesson: LessonContent | null; contactUrl: string }
|
||||
const { lesson, contactUrl } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="lessons section" id="lezioni" aria-labelledby="lessons-title">
|
||||
<div class="container">
|
||||
<div class="section-heading horizontal-heading">
|
||||
<div><p class="eyebrow">Informazioni pratiche</p><h2 id="lessons-title">Le lezioni</h2></div>
|
||||
<p>Un appuntamento settimanale per allenarsi con continuità, senza fretta.</p>
|
||||
</div>
|
||||
{lesson ? (
|
||||
<div class="lesson-panel">
|
||||
<dl class="lesson-details">
|
||||
<div><dt>Quando</dt><dd>{lesson.schedule}</dd></div>
|
||||
<div><dt>Dove</dt><dd>{lesson.location}</dd></div>
|
||||
<div><dt>Per chi</dt><dd>{lesson.audience}</dd></div>
|
||||
<div><dt>Cosa facciamo</dt><dd><div class="rich-text" set:html={lesson.description} /></dd></div>
|
||||
</dl>
|
||||
<aside class="trial-card" aria-label="Lezione di prova">
|
||||
<p class="eyebrow">Puoi provare</p>
|
||||
<h3>{lesson.trial_lesson}</h3>
|
||||
{lesson.notes && <p>{lesson.notes}</p>}
|
||||
<a class="button" href={contactUrl}>Prenota un incontro</a>
|
||||
</aside>
|
||||
</div>
|
||||
) : <p class="empty-state">Il calendario delle lezioni sarà pubblicato presto.</p>}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
interface Props { title: string; body: string }
|
||||
const { title, body } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="manifesto section" aria-labelledby="manifesto-title">
|
||||
<div class="container narrow">
|
||||
<p class="eyebrow">Il nostro modo di lavorare</p>
|
||||
<h2 id="manifesto-title">{title}</h2>
|
||||
<div class="manifesto-text rich-text" set:html={body} />
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
import type { ShowContent } from "../lib/api";
|
||||
interface Props { shows: ShowContent[] }
|
||||
const { shows } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="shows section section-light" id="spettacoli" aria-labelledby="shows-title">
|
||||
<div class="container">
|
||||
<div class="section-heading horizontal-heading">
|
||||
<div><p class="eyebrow">La ricerca incontra il pubblico</p><h2 id="shows-title">Spettacoli e progetti</h2></div>
|
||||
<p>Esiti, attraversamenti e storie nate durante il lavoro in sala.</p>
|
||||
</div>
|
||||
{shows.length ? (
|
||||
<div class="show-grid">
|
||||
{shows.map((show) => (
|
||||
<article class="show-card">
|
||||
<div class="show-image"><img src={show.image.url} alt={show.image.alt} width="700" height="880" loading="lazy" /></div>
|
||||
<div class="show-copy">
|
||||
<p class="show-meta"><span>{show.year}</span>{show.location && <span>{show.location}</span>}</p>
|
||||
<h3>{show.title}</h3>
|
||||
<p>{show.short_description}</p>
|
||||
{show.cast && <p class="muted">{show.cast}</p>}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : <p class="empty-state">Nuovi spettacoli e progetti sono in preparazione.</p>}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
import type { TeacherContent } from "../lib/api";
|
||||
interface Props { teacher: TeacherContent | null }
|
||||
const { teacher } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="teacher section section-sand" id="maestro" aria-labelledby="teacher-title">
|
||||
<div class="container">
|
||||
{teacher ? (
|
||||
<div class="teacher-grid">
|
||||
<div class="teacher-photo"><img src={teacher.photo.url} alt={teacher.photo.alt} width="760" height="900" loading="lazy" /></div>
|
||||
<div class="teacher-copy">
|
||||
<p class="eyebrow">La guida del percorso</p>
|
||||
<h2 id="teacher-title">Il maestro</h2>
|
||||
<h3>{teacher.name}</h3>
|
||||
<p class="lead">{teacher.short_bio}</p>
|
||||
{teacher.full_bio && <div class="rich-text" set:html={teacher.full_bio} />}
|
||||
{teacher.quote && <blockquote>“{teacher.quote}”</blockquote>}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty-state"><h2 id="teacher-title">Il maestro</h2><p>Il profilo dell’insegnante sarà disponibile presto.</p></div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly PUBLIC_CMS_API_URL?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="theme-color" content="#F3EBDD" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#contenuto">Vai al contenuto</a>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,321 @@
|
||||
export interface CmsImage {
|
||||
url: string;
|
||||
alt: string;
|
||||
}
|
||||
|
||||
export interface SiteSettings {
|
||||
site_name: string;
|
||||
short_claim: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
whatsapp: string;
|
||||
instagram: string;
|
||||
facebook: string;
|
||||
address: string;
|
||||
footer_text: string;
|
||||
}
|
||||
|
||||
export interface HomePageContent {
|
||||
hero_title: string;
|
||||
hero_subtitle: string;
|
||||
hero_image: CmsImage;
|
||||
intro_title: string;
|
||||
intro_body: string;
|
||||
laboratory_title: string;
|
||||
laboratory_body: string;
|
||||
laboratory_image: CmsImage;
|
||||
cta_primary_label: string;
|
||||
cta_primary_url: string;
|
||||
cta_secondary_label: string;
|
||||
cta_secondary_url: string;
|
||||
}
|
||||
|
||||
export interface FeatureCardContent {
|
||||
title: string;
|
||||
text: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface TeacherContent {
|
||||
name: string;
|
||||
photo: CmsImage;
|
||||
short_bio: string;
|
||||
full_bio: string;
|
||||
quote: string;
|
||||
}
|
||||
|
||||
export interface LessonContent {
|
||||
schedule: string;
|
||||
location: string;
|
||||
audience: string;
|
||||
description: string;
|
||||
trial_lesson: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
export interface ShowContent {
|
||||
title: string;
|
||||
year: string;
|
||||
short_description: string;
|
||||
description: string;
|
||||
image: CmsImage;
|
||||
location: string;
|
||||
cast: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface GalleryItemContent {
|
||||
image: CmsImage;
|
||||
category: string;
|
||||
caption: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface HomeData {
|
||||
settings: SiteSettings;
|
||||
homepage: HomePageContent;
|
||||
feature_cards: FeatureCardContent[];
|
||||
teacher: TeacherContent | null;
|
||||
lesson_info: LessonContent | null;
|
||||
shows: ShowContent[];
|
||||
gallery_items: GalleryItemContent[];
|
||||
}
|
||||
|
||||
const placeholder = (name: string, alt: string): CmsImage => ({
|
||||
url: `/images/${name}.svg`,
|
||||
alt,
|
||||
});
|
||||
|
||||
export const fallbackHomeData: HomeData = {
|
||||
settings: {
|
||||
site_name: "Laboratorio Teatrale",
|
||||
short_claim: "Il teatro diventa presenza.",
|
||||
email: "ciao@laboratorioteatrale.it",
|
||||
phone: "+39 333 123 4567",
|
||||
whatsapp: "+39 333 123 4567",
|
||||
instagram: "https://instagram.com/",
|
||||
facebook: "https://facebook.com/",
|
||||
address: "Via della Scena 12, Roma",
|
||||
footer_text: "Uno spazio aperto a chi desidera incontrare il teatro, insieme.",
|
||||
},
|
||||
homepage: {
|
||||
hero_title: "Il teatro diventa presenza.",
|
||||
hero_subtitle: "Un laboratorio dove corpo, voce, ascolto e relazione diventano scena.",
|
||||
hero_image: placeholder("hero", "Il gruppo durante un esercizio teatrale"),
|
||||
intro_title: "Uno spazio per esserci davvero",
|
||||
intro_body:
|
||||
"Ogni incontro è uno spazio di pratica: si prova, si sbaglia, si ascolta, si ricomincia. Il gruppo diventa materia viva, la scena diventa occasione di scoperta.",
|
||||
laboratory_title: "Il laboratorio",
|
||||
laboratory_body:
|
||||
"Lavoriamo con il corpo, la voce e l’improvvisazione per allenare ascolto e presenza scenica. Attraverso esercizi individuali e di gruppo, ogni persona trova un modo autentico di stare nello spazio e nella relazione.",
|
||||
laboratory_image: placeholder("laboratory", "Esercizio di movimento nello spazio"),
|
||||
cta_primary_label: "Vieni a conoscerci",
|
||||
cta_primary_url: "#contatti",
|
||||
cta_secondary_label: "Guarda la galleria",
|
||||
cta_secondary_url: "#galleria",
|
||||
},
|
||||
feature_cards: [
|
||||
{
|
||||
title: "Per chi inizia",
|
||||
text: "Non serve esperienza: serve curiosità, ascolto e voglia di mettersi in gioco.",
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
title: "Per chi vuole crescere",
|
||||
text: "Un tempo regolare per approfondire strumenti, consapevolezza e libertà espressiva.",
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
title: "Per chi ama il gruppo",
|
||||
text: "La scena nasce dalla fiducia: si crea insieme, rispettando tempi e sensibilità diverse.",
|
||||
order: 3,
|
||||
},
|
||||
],
|
||||
teacher: {
|
||||
name: "Andrea Morelli",
|
||||
photo: placeholder("teacher", "Ritratto del maestro Andrea Morelli"),
|
||||
short_bio:
|
||||
"Attore, regista e formatore, accompagna gruppi di ogni esperienza con cura e concretezza.",
|
||||
full_bio:
|
||||
"Da oltre quindici anni conduce percorsi dedicati alla ricerca dell’autenticità scenica, intrecciando pedagogia teatrale, movimento e lavoro sulla voce.",
|
||||
quote: "Il teatro non è fingere: è imparare a essere presenti.",
|
||||
},
|
||||
lesson_info: {
|
||||
schedule: "Ogni martedì, dalle 20:00 alle 22:30",
|
||||
location: "Spazio Scena, Via della Scena 12, Roma",
|
||||
audience: "Adulti, con o senza esperienza",
|
||||
description: "Training fisico e vocale, improvvisazione, ascolto, costruzione del personaggio e lavoro di scena.",
|
||||
trial_lesson: "La prima lezione di prova è gratuita, su prenotazione.",
|
||||
notes: "Abiti comodi e calze antiscivolo consigliati.",
|
||||
},
|
||||
shows: [
|
||||
{
|
||||
title: "Le cose che restano",
|
||||
year: "2025",
|
||||
short_description: "Un lavoro corale su memoria, gesti quotidiani e piccoli cambiamenti.",
|
||||
description: "",
|
||||
image: placeholder("show-one", "Locandina dello spettacolo Le cose che restano"),
|
||||
location: "Teatro di Quartiere",
|
||||
cast: "Gruppo avanzato",
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
title: "Fuori campo",
|
||||
year: "2024",
|
||||
short_description: "Storie ai margini della scena che chiedono, finalmente, di essere ascoltate.",
|
||||
description: "",
|
||||
image: placeholder("show-two", "Locandina dello spettacolo Fuori campo"),
|
||||
location: "Spazio Scena",
|
||||
cast: "Laboratorio annuale",
|
||||
order: 2,
|
||||
},
|
||||
],
|
||||
gallery_items: [
|
||||
{ image: placeholder("gallery-one", "Esercizio di gruppo durante una lezione"), category: "Lezioni", caption: "Ascolto e movimento", order: 1 },
|
||||
{ image: placeholder("gallery-two", "Il gruppo dietro le quinte"), category: "Backstage", caption: "Prima di entrare in scena", order: 2 },
|
||||
{ image: placeholder("gallery-three", "Una scena dello spettacolo finale"), category: "Spettacoli", caption: "Le cose che restano", order: 3 },
|
||||
{ image: placeholder("gallery-four", "Ritratto del gruppo teatrale"), category: "Gruppo", caption: "Il laboratorio, insieme", order: 4 },
|
||||
],
|
||||
};
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
|
||||
const record = (value: unknown): UnknownRecord =>
|
||||
value !== null && typeof value === "object" ? (value as UnknownRecord) : {};
|
||||
|
||||
const text = (value: unknown, fallback = ""): string =>
|
||||
typeof value === "string" && value.trim() ? value : fallback;
|
||||
|
||||
const optionalText = (value: unknown, fallback = ""): string =>
|
||||
typeof value === "string" ? value.trim() : fallback;
|
||||
|
||||
const number = (value: unknown, fallback = 0): number =>
|
||||
typeof value === "number" ? value : Number(value) || fallback;
|
||||
|
||||
const image = (value: unknown, fallback: CmsImage): CmsImage => {
|
||||
if (typeof value === "string" && value) return { url: value, alt: fallback.alt };
|
||||
const item = record(value);
|
||||
return {
|
||||
url: text(item.url, text(item.src, fallback.url)),
|
||||
alt: text(item.alt, text(item.title, fallback.alt)),
|
||||
};
|
||||
};
|
||||
|
||||
const absoluteImage = (value: CmsImage, baseUrl: string): CmsImage => {
|
||||
if (!value.url.startsWith("/media/")) return value;
|
||||
try {
|
||||
return { ...value, url: new URL(value.url, baseUrl).toString() };
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
function normalizeHomeData(payload: unknown, baseUrl: string): HomeData {
|
||||
const source = record(payload);
|
||||
const settings = record(source.settings);
|
||||
const homepage = record(source.homepage);
|
||||
const teacher = source.teacher === null ? null : record(source.teacher);
|
||||
const lesson = source.lesson_info === null ? null : record(source.lesson_info);
|
||||
const fallback = fallbackHomeData;
|
||||
|
||||
const resolveImage = (value: unknown, defaultImage: CmsImage) =>
|
||||
absoluteImage(image(value, defaultImage), baseUrl);
|
||||
|
||||
return {
|
||||
settings: {
|
||||
site_name: text(settings.site_name, text(settings.laboratory_name, text(settings.name, fallback.settings.site_name))),
|
||||
short_claim: optionalText(settings.short_claim, optionalText(settings.claim, fallback.settings.short_claim)),
|
||||
email: optionalText(settings.email, fallback.settings.email),
|
||||
phone: optionalText(settings.phone, fallback.settings.phone),
|
||||
whatsapp: optionalText(settings.whatsapp, fallback.settings.whatsapp),
|
||||
instagram: optionalText(settings.instagram, fallback.settings.instagram),
|
||||
facebook: optionalText(settings.facebook, fallback.settings.facebook),
|
||||
address: optionalText(settings.address, fallback.settings.address),
|
||||
footer_text: optionalText(settings.footer_text, fallback.settings.footer_text),
|
||||
},
|
||||
homepage: {
|
||||
hero_title: text(homepage.hero_title, fallback.homepage.hero_title),
|
||||
hero_subtitle: text(homepage.hero_subtitle, fallback.homepage.hero_subtitle),
|
||||
hero_image: resolveImage(homepage.hero_image, fallback.homepage.hero_image),
|
||||
intro_title: text(homepage.intro_title, fallback.homepage.intro_title),
|
||||
intro_body: text(homepage.intro_body, fallback.homepage.intro_body),
|
||||
laboratory_title: text(homepage.laboratory_title, fallback.homepage.laboratory_title),
|
||||
laboratory_body: text(homepage.laboratory_body, fallback.homepage.laboratory_body),
|
||||
laboratory_image: resolveImage(homepage.laboratory_image, fallback.homepage.laboratory_image),
|
||||
cta_primary_label: text(homepage.cta_primary_label, fallback.homepage.cta_primary_label),
|
||||
cta_primary_url: text(homepage.cta_primary_url, fallback.homepage.cta_primary_url),
|
||||
cta_secondary_label: text(homepage.cta_secondary_label, fallback.homepage.cta_secondary_label),
|
||||
cta_secondary_url: text(homepage.cta_secondary_url, fallback.homepage.cta_secondary_url),
|
||||
},
|
||||
feature_cards: Array.isArray(source.feature_cards)
|
||||
? source.feature_cards.map((item, index) => {
|
||||
const card = record(item);
|
||||
return { title: text(card.title), text: text(card.text, text(card.body)), order: number(card.order, index + 1) };
|
||||
}).filter((item) => item.title).sort((a, b) => a.order - b.order)
|
||||
: fallback.feature_cards,
|
||||
teacher: teacher
|
||||
? {
|
||||
name: text(teacher.name, fallback.teacher?.name),
|
||||
photo: resolveImage(teacher.photo, fallback.teacher?.photo ?? placeholder("teacher", "Ritratto del maestro")),
|
||||
short_bio: text(teacher.short_bio, text(teacher.bio_breve, fallback.teacher?.short_bio)),
|
||||
full_bio: text(teacher.full_bio, text(teacher.bio_completa, fallback.teacher?.full_bio)),
|
||||
quote: text(teacher.quote, text(teacher.citazione, fallback.teacher?.quote)),
|
||||
}
|
||||
: null,
|
||||
lesson_info: lesson
|
||||
? {
|
||||
schedule: text(lesson.schedule, text(lesson.day_time, text(lesson.giorno_orario, fallback.lesson_info?.schedule))),
|
||||
location: text(lesson.location, text(lesson.luogo, fallback.lesson_info?.location)),
|
||||
audience: text(lesson.audience, text(lesson.destinatari, fallback.lesson_info?.audience)),
|
||||
description: text(lesson.description, text(lesson.descrizione, fallback.lesson_info?.description)),
|
||||
trial_lesson: text(lesson.trial_lesson, text(lesson.lezione_di_prova, fallback.lesson_info?.trial_lesson)),
|
||||
notes: text(lesson.notes, text(lesson.note, fallback.lesson_info?.notes)),
|
||||
}
|
||||
: null,
|
||||
shows: Array.isArray(source.shows)
|
||||
? source.shows.map((item, index) => {
|
||||
const show = record(item);
|
||||
const fallbackImage = placeholder(index % 2 ? "show-two" : "show-one", "Locandina dello spettacolo");
|
||||
return {
|
||||
title: text(show.title, text(show.titolo)),
|
||||
year: text(show.year, text(show.anno)),
|
||||
short_description: text(show.short_description, text(show.descrizione_breve)),
|
||||
description: text(show.description, text(show.descrizione)),
|
||||
image: resolveImage(show.image ?? show.poster ?? show.immagine, fallbackImage),
|
||||
location: text(show.location, text(show.luogo)),
|
||||
cast: text(show.cast),
|
||||
order: number(show.order, text(show.ordine) ? number(show.ordine) : index + 1),
|
||||
};
|
||||
}).filter((item) => item.title).sort((a, b) => a.order - b.order)
|
||||
: fallback.shows,
|
||||
gallery_items: Array.isArray(source.gallery_items)
|
||||
? source.gallery_items.map((item, index) => {
|
||||
const galleryItem = record(item);
|
||||
const fallbackImage = placeholder(`gallery-${["one", "two", "three", "four"][index % 4]}`, "Immagine dal laboratorio");
|
||||
return {
|
||||
image: resolveImage(galleryItem.image ?? galleryItem.immagine, fallbackImage),
|
||||
category: text(galleryItem.category, text(galleryItem.categoria, "Laboratorio")),
|
||||
caption: text(galleryItem.caption, text(galleryItem.didascalia)),
|
||||
order: number(galleryItem.order, text(galleryItem.ordine) ? number(galleryItem.ordine) : index + 1),
|
||||
};
|
||||
}).sort((a, b) => a.order - b.order)
|
||||
: fallback.gallery_items,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getHomeData(): Promise<HomeData> {
|
||||
const baseUrl = import.meta.env.PUBLIC_CMS_API_URL?.replace(/\/$/, "");
|
||||
if (!baseUrl) return fallbackHomeData;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/site/home/`, {
|
||||
headers: { Accept: "application/json" },
|
||||
signal: AbortSignal.timeout(3500),
|
||||
});
|
||||
if (!response.ok) throw new Error(`CMS returned ${response.status}`);
|
||||
return normalizeHomeData(await response.json(), baseUrl);
|
||||
} catch (error) {
|
||||
console.warn("CMS unavailable; using local demo content.", error);
|
||||
return fallbackHomeData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import Header from "../components/Header.astro";
|
||||
import Hero from "../components/Hero.astro";
|
||||
import Manifesto from "../components/Manifesto.astro";
|
||||
import FeatureCards from "../components/FeatureCards.astro";
|
||||
import LaboratorySection from "../components/LaboratorySection.astro";
|
||||
import TeacherSection from "../components/TeacherSection.astro";
|
||||
import LessonsSection from "../components/LessonsSection.astro";
|
||||
import ShowsSection from "../components/ShowsSection.astro";
|
||||
import GallerySection from "../components/GallerySection.astro";
|
||||
import ContactSection from "../components/ContactSection.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import { getHomeData } from "../lib/api";
|
||||
|
||||
const data = await getHomeData();
|
||||
---
|
||||
|
||||
<BaseLayout title={`${data.settings.site_name} · ${data.settings.short_claim}`} description={data.homepage.hero_subtitle}>
|
||||
<Header siteName={data.settings.site_name} ctaLabel={data.homepage.cta_primary_label} ctaUrl={data.homepage.cta_primary_url} />
|
||||
<main id="contenuto">
|
||||
<Hero content={data.homepage} />
|
||||
<Manifesto title={data.homepage.intro_title} body={data.homepage.intro_body} />
|
||||
<FeatureCards cards={data.feature_cards} />
|
||||
<LaboratorySection content={data.homepage} />
|
||||
<TeacherSection teacher={data.teacher} />
|
||||
<LessonsSection lesson={data.lesson_info} contactUrl="#contatti" />
|
||||
<ShowsSection shows={data.shows} />
|
||||
<GallerySection items={data.gallery_items} />
|
||||
<ContactSection settings={data.settings} />
|
||||
</main>
|
||||
<Footer settings={data.settings} />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,972 @@
|
||||
:root {
|
||||
--color-bg: #f3ebdd;
|
||||
--color-bg-light: #fff8ef;
|
||||
--color-dark: #4a433a;
|
||||
--color-dark-soft: #5a5046;
|
||||
--color-dark-text: #1d1a17;
|
||||
--color-primary: #a4513b;
|
||||
--color-primary-soft: #b8674c;
|
||||
--color-sage: #515d53;
|
||||
--color-sand: #d8c7ae;
|
||||
--color-muted: #7a7067;
|
||||
--color-border: #e6d8c8;
|
||||
--font-display: "Cormorant Garamond", "Iowan Old Style", "Palatino Linotype", Georgia, serif;
|
||||
--font-body: "Source Sans 3", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--shadow-soft: 0 24px 60px rgb(74 67 58 / 10%);
|
||||
--radius: 0.35rem;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 5rem;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
overflow-x: hidden;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-dark-text);
|
||||
font-family: var(--font-body);
|
||||
font-size: 1rem;
|
||||
line-height: 1.65;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body::before {
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 180 180' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.035'/%3E%3C/svg%3E");
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
border-radius: 0.15rem;
|
||||
outline: 3px solid var(--color-primary);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p,
|
||||
figure,
|
||||
blockquote,
|
||||
dl,
|
||||
dd {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 500;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.025em;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
h1 {
|
||||
max-width: 11ch;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: clamp(3.5rem, 15vw, 7.5rem);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: clamp(2.7rem, 10vw, 5.3rem);
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: clamp(1.7rem, 5vw, 2.35rem);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: min(100% - 2.5rem, 76rem);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.narrow {
|
||||
max-width: 58rem;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-block: clamp(5rem, 12vw, 9.5rem);
|
||||
}
|
||||
|
||||
.section-light {
|
||||
background: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.section-sand {
|
||||
background: color-mix(in srgb, var(--color-sand) 68%, var(--color-bg));
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.73rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
line-height: 1.4;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.lead {
|
||||
color: var(--color-dark-soft);
|
||||
font-size: clamp(1.15rem, 2vw, 1.38rem);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.rich-text > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.rich-text a {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--color-dark-soft);
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
padding: 0.65rem 1rem;
|
||||
transform: translateY(-180%);
|
||||
background: var(--color-dark-text);
|
||||
color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.site-header {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
padding-block: 1.25rem;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.wordmark {
|
||||
max-width: 10rem;
|
||||
color: var(--color-dark-text);
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.desktop-nav,
|
||||
.desktop-cta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mobile-menu summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
min-height: 2.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--color-dark-text);
|
||||
cursor: pointer;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
list-style: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mobile-menu summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-menu summary > span:first-child,
|
||||
.mobile-menu summary > span:first-child::before,
|
||||
.mobile-menu summary > span:first-child::after {
|
||||
display: block;
|
||||
width: 1rem;
|
||||
height: 1px;
|
||||
background: currentcolor;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.mobile-menu summary > span:first-child::before {
|
||||
transform: translateY(-0.3rem);
|
||||
}
|
||||
|
||||
.mobile-menu summary > span:first-child::after {
|
||||
transform: translateY(0.25rem);
|
||||
}
|
||||
|
||||
.mobile-menu[open] summary > span:first-child {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.mobile-menu[open] summary > span:first-child::before {
|
||||
transform: translateY(0.05rem) rotate(45deg);
|
||||
}
|
||||
|
||||
.mobile-menu[open] summary > span:first-child::after {
|
||||
transform: translateY(-0.02rem) rotate(-45deg);
|
||||
}
|
||||
|
||||
.mobile-menu nav {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.6rem);
|
||||
right: 0;
|
||||
display: grid;
|
||||
width: min(18rem, calc(100vw - 2.5rem));
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-bg-light);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.mobile-menu nav > a:not(.button) {
|
||||
padding: 0.7rem 0.25rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mobile-menu .button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-flex;
|
||||
min-height: 3.15rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem 1.35rem;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-primary);
|
||||
color: #fff8ef;
|
||||
font-size: 0.83rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
border-color: var(--color-dark-text);
|
||||
background: var(--color-dark-text);
|
||||
color: var(--color-bg-light);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.button-small {
|
||||
min-height: 2.65rem;
|
||||
padding: 0.65rem 1rem;
|
||||
font-size: 0.73rem;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.text-link {
|
||||
padding-block: 0.65rem;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-decoration-thickness: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero {
|
||||
position: relative;
|
||||
min-height: min(48rem, 100svh);
|
||||
padding-bottom: 4.5rem;
|
||||
padding-top: 7.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 7%;
|
||||
right: -12rem;
|
||||
width: 23rem;
|
||||
height: 23rem;
|
||||
border: 1px solid rgb(164 81 59 / 20%);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
max-width: 34rem;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--color-dark-soft);
|
||||
font-size: clamp(1.18rem, 2vw, 1.45rem);
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
position: relative;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.hero-visual::before {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
inset: -1rem 1rem 1rem -1rem;
|
||||
border: 1px solid var(--color-sand);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.hero-visual img {
|
||||
max-height: 28rem;
|
||||
aspect-ratio: 4 / 3;
|
||||
object-fit: cover;
|
||||
object-position: center 42%;
|
||||
}
|
||||
|
||||
.hero-visual figcaption {
|
||||
position: absolute;
|
||||
right: -0.2rem;
|
||||
bottom: 0;
|
||||
padding: 0.65rem 0 0.15rem 1rem;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-dark-soft);
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.05rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.manifesto {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.manifesto h2 {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.manifesto-text {
|
||||
max-width: 37ch;
|
||||
margin-right: auto;
|
||||
margin-bottom: 0;
|
||||
margin-left: auto;
|
||||
color: var(--color-dark-soft);
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.55rem, 4vw, 2.45rem);
|
||||
line-height: 1.42;
|
||||
}
|
||||
|
||||
.manifesto-text > :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
margin-bottom: clamp(2.5rem, 7vw, 4.5rem);
|
||||
}
|
||||
|
||||
.section-heading h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
min-height: 17rem;
|
||||
padding: 1.75rem;
|
||||
border: 1px solid var(--color-border);
|
||||
background: rgb(255 255 255 / 24%);
|
||||
}
|
||||
|
||||
.feature-card:nth-child(2) {
|
||||
background: color-mix(in srgb, var(--color-sage) 8%, var(--color-bg-light));
|
||||
}
|
||||
|
||||
.card-number {
|
||||
display: block;
|
||||
margin-bottom: 3.75rem;
|
||||
color: var(--color-primary);
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
max-width: 31ch;
|
||||
margin-bottom: 0;
|
||||
color: var(--color-dark-soft);
|
||||
}
|
||||
|
||||
.split-grid,
|
||||
.teacher-grid {
|
||||
display: grid;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.editorial-image,
|
||||
.teacher-photo,
|
||||
.show-image {
|
||||
overflow: hidden;
|
||||
background: var(--color-sand);
|
||||
}
|
||||
|
||||
.editorial-image img {
|
||||
aspect-ratio: 5 / 4;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.split-copy {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.practice-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
margin: 2.25rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.practice-list li {
|
||||
padding: 0.5rem 0.85rem;
|
||||
border: 1px solid var(--color-sand);
|
||||
border-radius: 99rem;
|
||||
color: var(--color-sage);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.teacher-photo img {
|
||||
aspect-ratio: 4 / 5;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.teacher-copy {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.teacher-copy h2 {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.teacher-copy h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
position: relative;
|
||||
margin: 2.5rem 0 0;
|
||||
padding: 1.5rem 0 0 1.5rem;
|
||||
border-top: 1px solid rgb(74 67 58 / 20%);
|
||||
color: var(--color-dark);
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.65rem, 3vw, 2.35rem);
|
||||
font-style: italic;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
blockquote::before {
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
left: 0;
|
||||
color: var(--color-primary);
|
||||
content: "—";
|
||||
}
|
||||
|
||||
.horizontal-heading > p {
|
||||
max-width: 33rem;
|
||||
margin-bottom: 0;
|
||||
color: var(--color-dark-soft);
|
||||
}
|
||||
|
||||
.lesson-panel {
|
||||
border-top: 1px solid var(--color-sand);
|
||||
}
|
||||
|
||||
.lesson-details {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.lesson-details > div {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
padding-block: 1.4rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.lesson-details dt {
|
||||
color: var(--color-primary);
|
||||
font-size: 0.73rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.13em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.lesson-details dd {
|
||||
margin-bottom: 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.trial-card {
|
||||
padding: 2rem;
|
||||
background: var(--color-sage);
|
||||
color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.trial-card .eyebrow {
|
||||
color: var(--color-sand);
|
||||
}
|
||||
|
||||
.trial-card h3 {
|
||||
font-size: clamp(1.9rem, 4vw, 2.7rem);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.trial-card p:not(.eyebrow) {
|
||||
color: rgb(255 248 239 / 78%);
|
||||
}
|
||||
|
||||
.trial-card .button {
|
||||
margin-top: 1rem;
|
||||
border-color: var(--color-bg-light);
|
||||
background: var(--color-bg-light);
|
||||
color: var(--color-sage);
|
||||
}
|
||||
|
||||
.show-grid {
|
||||
display: grid;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.show-card {
|
||||
display: grid;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.show-image img {
|
||||
aspect-ratio: 4 / 5;
|
||||
object-fit: cover;
|
||||
transition: transform 500ms ease;
|
||||
}
|
||||
|
||||
.show-card:hover .show-image img {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.show-copy {
|
||||
padding: 1.5rem 0 2rem;
|
||||
}
|
||||
|
||||
.show-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
color: var(--color-primary);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.show-copy p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.gallery-item img {
|
||||
aspect-ratio: 5 / 4;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.gallery-item figcaption {
|
||||
padding-top: 0.75rem;
|
||||
color: var(--color-dark-soft);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.gallery-item figcaption span {
|
||||
margin-right: 0.75rem;
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.contact {
|
||||
background: var(--color-dark);
|
||||
color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.contact-grid {
|
||||
display: grid;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.contact .eyebrow {
|
||||
color: var(--color-sand);
|
||||
}
|
||||
|
||||
.contact .lead {
|
||||
color: rgb(255 248 239 / 78%);
|
||||
}
|
||||
|
||||
.contact address {
|
||||
color: var(--color-sand);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
border-top: 1px solid rgb(255 248 239 / 20%);
|
||||
}
|
||||
|
||||
.contact-actions a {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.3rem 1rem;
|
||||
align-items: center;
|
||||
padding-block: 1.35rem;
|
||||
border-bottom: 1px solid rgb(255 248 239 / 20%);
|
||||
color: var(--color-bg-light);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.contact-actions a > span:first-child {
|
||||
color: var(--color-sand);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.contact-actions strong {
|
||||
grid-row: 2;
|
||||
min-width: 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.15rem, 4vw, 1.65rem);
|
||||
font-weight: 500;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.contact-actions a > span:last-child {
|
||||
grid-row: 1 / 3;
|
||||
grid-column: 2;
|
||||
font-size: 1.4rem;
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.contact-actions a:hover {
|
||||
color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.contact-actions a:hover > span:last-child {
|
||||
transform: translate(0.2rem, -0.2rem);
|
||||
}
|
||||
|
||||
.contact a:focus-visible {
|
||||
outline-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 2rem;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-dark-soft);
|
||||
}
|
||||
|
||||
.contact-empty {
|
||||
border-color: rgb(255 248 239 / 20%);
|
||||
color: var(--color-sand);
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
padding-block: 3rem 1.5rem;
|
||||
background: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.footer-main,
|
||||
.footer-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-main > div p {
|
||||
max-width: 35rem;
|
||||
margin: 1rem 0 0;
|
||||
color: var(--color-dark-soft);
|
||||
}
|
||||
|
||||
.footer-main nav {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
margin-top: 2.5rem;
|
||||
padding-top: 1.25rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
color: var(--color-dark-soft);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.footer-bottom p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 36rem) {
|
||||
.container {
|
||||
width: min(100% - 4rem, 76rem);
|
||||
}
|
||||
|
||||
.feature-grid,
|
||||
.gallery-grid,
|
||||
.show-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.gallery-item:nth-child(3n + 1) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.gallery-item:nth-child(3n + 1) img {
|
||||
aspect-ratio: 2 / 1;
|
||||
}
|
||||
|
||||
.lesson-details > div {
|
||||
grid-template-columns: 9rem 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.lesson-details dd {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 54rem) {
|
||||
.site-header {
|
||||
padding-block: 1.75rem;
|
||||
}
|
||||
|
||||
.desktop-nav,
|
||||
.desktop-cta {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.desktop-nav {
|
||||
align-items: center;
|
||||
gap: clamp(1rem, 2.5vw, 2rem);
|
||||
}
|
||||
|
||||
.desktop-nav a {
|
||||
font-size: 0.79rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hero {
|
||||
min-height: min(54rem, 100svh);
|
||||
padding-top: 9.5rem;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
grid-template-columns: minmax(0, 1.05fr) minmax(20rem, 0.7fr);
|
||||
align-items: center;
|
||||
gap: clamp(3rem, 7vw, 7rem);
|
||||
}
|
||||
|
||||
.hero-visual {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero-visual img {
|
||||
max-height: none;
|
||||
aspect-ratio: 6 / 7;
|
||||
}
|
||||
|
||||
.horizontal-heading {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.feature-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
min-height: 20rem;
|
||||
padding: 2.25rem;
|
||||
}
|
||||
|
||||
.feature-card:nth-child(2) {
|
||||
transform: translateY(1.75rem);
|
||||
}
|
||||
|
||||
.split-grid {
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(20rem, 0.9fr);
|
||||
gap: clamp(4rem, 8vw, 8rem);
|
||||
}
|
||||
|
||||
.teacher-grid {
|
||||
grid-template-columns: minmax(18rem, 0.65fr) minmax(25rem, 1fr);
|
||||
gap: clamp(4rem, 8vw, 8rem);
|
||||
}
|
||||
|
||||
.teacher-photo {
|
||||
transform: rotate(-1.25deg);
|
||||
}
|
||||
|
||||
.lesson-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(18rem, 0.65fr);
|
||||
gap: clamp(3rem, 8vw, 7rem);
|
||||
align-items: start;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.trial-card {
|
||||
margin-top: 1rem;
|
||||
padding: 2.5rem;
|
||||
}
|
||||
|
||||
.show-grid {
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
.show-copy {
|
||||
padding: 1.75rem 0 2.25rem;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.gallery-item,
|
||||
.gallery-item:nth-child(3n + 1) {
|
||||
grid-column: span 6;
|
||||
}
|
||||
|
||||
.gallery-item:nth-child(4n + 2),
|
||||
.gallery-item:nth-child(4n + 3) {
|
||||
grid-column: span 5;
|
||||
}
|
||||
|
||||
.gallery-item:nth-child(4n + 3) {
|
||||
grid-column-start: 8;
|
||||
margin-top: 4rem;
|
||||
}
|
||||
|
||||
.gallery-item:nth-child(4n + 4) {
|
||||
grid-column: 3 / span 8;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.gallery-item img,
|
||||
.gallery-item:nth-child(3n + 1) img {
|
||||
aspect-ratio: 5 / 4;
|
||||
}
|
||||
|
||||
.contact-grid {
|
||||
grid-template-columns: minmax(0, 0.9fr) minmax(26rem, 1fr);
|
||||
gap: clamp(5rem, 10vw, 10rem);
|
||||
}
|
||||
|
||||
.footer-main,
|
||||
.footer-bottom {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user