# Nuovo sito Parrocchia — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Ricostruire parrocchiaarma.it come sito statico Astro multilingua (IT/EN/FR/DE) su Cloudflare Pages, riproponendo tutti i contenuti attuali.

**Architecture:** Astro 7 con output statico, i18n con prefisso per tutte le lingue e slug tradotti (`src/i18n/routes.ts`), testi in JSON per lingua, sezioni come componenti `.astro` montati da due route dinamiche (`[lang]/index.astro`, `[lang]/[slug].astro`). Una Pages Function alla root fa il redirect lingua. Nessun framework UI: JS minimo inline per menu, tendina lingue, lightbox e facade.

**Tech Stack:** Node 24, Astro 7.3, `@astrojs/sitemap`, `@fontsource-variable/fraunces`, `@fontsource-variable/instrument-sans`, `sharp`, `node --test` (stdlib), `wrangler` (solo dev locale delle Functions).

## Global Constraints

- Spec: `docs/superpowers/specs/2026-09-21-parrocchia-site-design.md`.
- Repo: `/Users/paolo/Server/Siti Web/Lavoro/Parrocchia/site` (git già inizializzato). Vecchio sito e materiali: `/Users/paolo/Server/Siti Web/Lavoro/Parrocchia` (`OLD` nei comandi) — **non modificarli**.
- URL sito: `https://parrocchiaarma.it` (apex canonico, `www` → apex).
- Lingue: `it` (default), `en`, `fr`, `de`. Tutte le pagine esistono nelle 4 lingue. URL sempre con prefisso e trailing slash.
- Nessun cookie banner, nessun GA. Unico cookie: `lang` (tecnico).
- Nessuna dipendenza runtime oltre a quelle elencate. Niente jQuery/Bootstrap/framework.
- Footer credit esatto: `Sito realizzato da **DueBytes** di Paolo Alberti` → `https://www.duebytes.it` (tradotto: "Website by", "Site réalisé par", "Website von").
- Dati legali: `Parrocchia Santi Giuseppe e Antonio · Via Monsignor Giauni, 14 · 18018 Arma di Taggia (IM) · P.I./C.F. 81002390086`.
- Commit frequenti, messaggi in italiano, prefissi `feat:`/`chore:`/`test:`/`docs:`, chiusi con le righe di attribuzione Claude fornite dalla sessione.
- Ogni task termina con `npm run build` verde.

---

## File structure

```
site/
├── astro.config.mjs
├── package.json
├── tsconfig.json
├── functions/index.js              # Pages Function: redirect lingua alla root
├── public/
│   ├── _redirects                  # 301 vecchi URL + www→apex
│   ├── robots.txt
│   ├── llms.txt
│   ├── favicon.png, apple-touch-icon.png
│   └── files/*.pdf
├── scripts/check-links.mjs         # verifica link interni post-build
├── tests/                          # node --test
│   ├── routes.test.ts
│   ├── i18n.test.ts
│   └── pick-lang.test.ts
└── src/
    ├── config.ts                   # costanti non traducibili (URL, telefoni, social, ID widget)
    ├── og.ts                       # immagine OG per pagina
    ├── lib/pick-lang.ts            # scelta lingua da cookie/Accept-Language (usata dalla Function)
    ├── i18n/
    │   ├── routes.ts               # LANGS, PAGE_KEYS, SLUGS, pathFor, keyFromSlug
    │   ├── index.ts                # getDict(lang)
    │   └── it.json en.json fr.json de.json
    ├── styles/global.css           # token, reset, primitive (.wrap .prose .btn .card .kicker)
    ├── layouts/Base.astro
    ├── components/
    │   ├── Seo.astro  Header.astro  Footer.astro  LanguageSwitcher.astro  Flag.astro
    │   ├── Lightbox.astro  PageHero.astro  Gallery.astro  PdfList.astro
    │   ├── MapFacade.astro  YouTubeFacade.astro  ContactCard.astro
    ├── sections/                   # una per pagina, prop `lang`
    │   ├── Home.astro Contatti.astro Catechismo.astro Matrimonio.astro Oratorio.astro
    │   ├── Grest.astro Scout.astro Coro.astro Giornalino.astro News.astro Privacy.astro
    ├── assets/                     # immagini ottimizzate da astro:assets
    │   ├── logo.png  home/altare.jpg  chiesa/navata.jpg
    │   ├── catechismo/01..08.jpg  grest/*.jpg  oratorio/*.jpg  scout/*.jpg|png  coro/natale-2019.jpg  matrimonio/fidanzati.jpg
    └── pages/[lang]/index.astro  [lang]/[slug].astro
```

---

### Task 1: Scaffold Astro, fonti, CSS base

**Files:**
- Create: `package.json`, `astro.config.mjs`, `tsconfig.json`, `.gitignore`, `src/styles/global.css`, `src/pages/index.astro` (temporaneo)

**Interfaces:**
- Produces: script npm `dev`, `build`, `preview`, `check`, `test`, `check:links`; token CSS usati da tutti i componenti (`--bg --ink --accent --navy --line --font-display --font-body --radius --shadow --wrap`).

- [ ] **Step 1: Init progetto**

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/Parrocchia/site"
npm create astro@latest . -- --template minimal --no-install --no-git --typescript strict --skip-houston
npm i astro@^7.3 @astrojs/sitemap @astrojs/check typescript sharp @fontsource-variable/fraunces @fontsource-variable/instrument-sans
npm i -D wrangler
```
Se `npm create astro` rifiuta la cartella non vuota (contiene `docs/` e `.git`), creare i file a mano come nei passi seguenti.

- [ ] **Step 2: `package.json` scripts**

```json
{
  "name": "parrocchia-arma",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "check": "astro check",
    "test": "node --test tests/",
    "check:links": "node scripts/check-links.mjs",
    "pages:dev": "wrangler pages dev dist"
  }
}
```
(le `dependencies` restano quelle installate allo step 1).

- [ ] **Step 3: `astro.config.mjs`**

```js
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://parrocchiaarma.it',
  trailingSlash: 'always',
  i18n: {
    defaultLocale: 'it',
    locales: ['it', 'en', 'fr', 'de'],
    routing: { prefixDefaultLocale: true, redirectToDefaultLocale: false },
  },
  image: { layout: 'constrained', responsiveStyles: true },
  integrations: [sitemap()],
});
```

- [ ] **Step 4: `tsconfig.json`**

```json
{
  "extends": "astro/tsconfigs/strict",
  "include": [".astro/types.d.ts", "**/*"],
  "exclude": ["dist"]
}
```

- [ ] **Step 5: `.gitignore`**

```
node_modules/
dist/
.astro/
.wrangler/
.DS_Store
```

- [ ] **Step 6: `src/styles/global.css`**

```css
:root {
  --bg: #f7f3ec;
  --bg-2: #efe8dc;
  --white: #fffdf9;
  --ink: #1d2433;
  --ink-2: #4a5266;
  --muted: #7a8194;
  --accent: #b4552d;
  --accent-2: #8f3f1f;
  --navy: #1f2a44;
  --navy-2: #16203a;
  --line: #e2d9ca;
  --font-display: 'Fraunces Variable', Georgia, 'Times New Roman', serif;
  --font-body: 'Instrument Sans Variable', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --radius: 16px;
  --radius-sm: 10px;
  --shadow: 0 12px 32px rgba(29, 36, 51, 0.08);
  --wrap: 1180px;
  --gutter: clamp(16px, 4vw, 40px);
  --measure: 68ch;
  --ease: cubic-bezier(0.2, 0.7, 0.2, 1);
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}
body.no-scroll { overflow: hidden; }
img, picture, video, svg { display: block; max-width: 100%; height: auto; }
a { color: var(--accent-2); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--accent); }
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.01em;
  margin: 0 0 0.5em;
  color: var(--ink);
  text-wrap: balance;
}
h1 { font-size: clamp(2.25rem, 5vw, 3.75rem); }
h2 { font-size: clamp(1.75rem, 3.2vw, 2.5rem); }
h3 { font-size: clamp(1.25rem, 2vw, 1.5rem); }
p { margin: 0 0 1em; }
ul, ol { padding-left: 1.2em; }
main { display: block; flex: 1 0 auto; }

:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; border-radius: 4px; }
.skip {
  position: absolute; left: 8px; top: -60px; z-index: 100;
  background: var(--navy); color: #fff; padding: 8px 14px; border-radius: 8px;
}
.skip:focus { top: 8px; }

.wrap { width: min(var(--wrap), 100% - 2 * var(--gutter)); margin-inline: auto; }
.section { padding-block: clamp(56px, 8vw, 104px); }
.section--tight { padding-block: clamp(32px, 5vw, 64px); }
.section--dark { background: var(--navy); color: #e9e4da; }
.section--dark h2, .section--dark h3 { color: #fff; }
.section--alt { background: var(--bg-2); }

.kicker {
  display: inline-block;
  font-size: 0.8rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--accent); margin-bottom: 0.75rem;
}
.section--dark .kicker { color: #e8a98a; }

.prose { max-width: var(--measure); }
.prose p { font-size: 1.0625rem; }
.prose p + h2, .prose p + h3 { margin-top: 1.75em; }
.prose blockquote {
  margin: 1.5em 0; padding: 0.25em 0 0.25em 1.25em;
  border-left: 3px solid var(--accent);
  font-family: var(--font-display); font-size: 1.35rem; font-style: italic; color: var(--ink-2);
}

.btn {
  display: inline-flex; align-items: center; gap: 0.5em;
  padding: 0.8em 1.4em; border-radius: 999px;
  background: var(--accent); color: #fff; text-decoration: none;
  font-weight: 600; line-height: 1;
  transition: background 0.2s var(--ease), transform 0.2s var(--ease);
}
.btn:hover { background: var(--accent-2); color: #fff; transform: translateY(-1px); }
.btn--ghost { background: transparent; color: var(--ink); box-shadow: inset 0 0 0 1.5px var(--line); }
.btn--ghost:hover { background: var(--white); color: var(--ink); }
.btn--light { background: #fff; color: var(--navy); }
.btn--light:hover { background: var(--bg); color: var(--navy); }

.card {
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius);
  padding: clamp(20px, 3vw, 32px); box-shadow: var(--shadow);
}

.grid { display: grid; gap: clamp(16px, 2.5vw, 28px); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }

.reveal { opacity: 0; transform: translateY(14px); transition: opacity 0.6s var(--ease), transform 0.6s var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal { opacity: 1; transform: none; transition: none; }
  * { animation: none !important; transition-duration: 0.01ms !important; }
}

table { border-collapse: collapse; width: 100%; font-size: 0.95rem; }
th, td { text-align: left; padding: 0.6em 0.8em; border-bottom: 1px solid var(--line); vertical-align: top; }
th { font-weight: 600; color: var(--ink-2); }
```

- [ ] **Step 7: pagina temporanea `src/pages/index.astro`**

```astro
---
import '../styles/global.css';
---
<html lang="it"><head><meta charset="utf-8" /><title>Parrocchia</title></head><body><h1>Parrocchia</h1></body></html>
```

- [ ] **Step 8: Build**

Run: `npm run build`
Expected: `[build] Complete!`, file `dist/index.html` presente.

- [ ] **Step 9: Commit**

```bash
git add -A && git commit -m "chore: scaffold Astro, font e CSS base"
```

---

### Task 2: Config sito, routing i18n e dizionario italiano

**Files:**
- Create: `src/config.ts`, `src/i18n/routes.ts`, `src/i18n/index.ts`, `src/i18n/it.json`, `src/i18n/en.json`, `src/i18n/fr.json`, `src/i18n/de.json` (copie temporanee di `it.json`)
- Test: `tests/routes.test.ts`, `tests/i18n.test.ts`

**Interfaces:**
- Produces:
  - `LANGS: readonly ['it','en','fr','de']`, `type Lang`, `DEFAULT_LANG = 'it'`, `PAGE_KEYS`, `type PageKey`, `SLUGS: Record<PageKey, Record<Lang, string>>`
  - `pathFor(lang: Lang, key: PageKey): string` → `/it/catechismo/` (home → `/it/`)
  - `keyFromSlug(lang: Lang, slug: string): PageKey | undefined`
  - `isLang(x: string): x is Lang`
  - `getDict(lang: Lang): Dict` (`Dict = typeof it`)
  - `SITE` (costanti) in `src/config.ts`

- [ ] **Step 1: Test routing**

`tests/routes.test.ts`:
```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { LANGS, PAGE_KEYS, SLUGS, pathFor, keyFromSlug, isLang } from '../src/i18n/routes.ts';

test('ogni pagina ha uno slug in ogni lingua', () => {
  for (const key of PAGE_KEYS) for (const lang of LANGS) assert.equal(typeof SLUGS[key][lang], 'string', `${key}/${lang}`);
});

test('gli slug sono unici per lingua', () => {
  for (const lang of LANGS) {
    const slugs = PAGE_KEYS.map((k) => SLUGS[k][lang]);
    assert.equal(new Set(slugs).size, slugs.length, lang);
  }
});

test('pathFor', () => {
  assert.equal(pathFor('it', 'home'), '/it/');
  assert.equal(pathFor('it', 'catechismo'), '/it/catechismo/');
  assert.equal(pathFor('de', 'scout'), '/de/pfadfinder/');
});

test('keyFromSlug', () => {
  assert.equal(keyFromSlug('fr', 'chorale'), 'coro');
  assert.equal(keyFromSlug('en', 'nope'), undefined);
});

test('isLang', () => {
  assert.equal(isLang('fr'), true);
  assert.equal(isLang('es'), false);
});
```

- [ ] **Step 2: Esegui — deve fallire**

Run: `npm test`
Expected: FAIL, `Cannot find module '.../src/i18n/routes.ts'`.

- [ ] **Step 3: `src/i18n/routes.ts`**

```ts
export const LANGS = ['it', 'en', 'fr', 'de'] as const;
export type Lang = (typeof LANGS)[number];
export const DEFAULT_LANG: Lang = 'it';

export const PAGE_KEYS = [
  'home', 'contatti', 'catechismo', 'matrimonio', 'oratorio', 'grest',
  'scout', 'coro', 'giornalino', 'news', 'privacy',
] as const;
export type PageKey = (typeof PAGE_KEYS)[number];

export const SLUGS: Record<PageKey, Record<Lang, string>> = {
  home:       { it: '',           en: '',           fr: '',                 de: '' },
  contatti:   { it: 'contatti',   en: 'contact',    fr: 'contact',          de: 'kontakt' },
  catechismo: { it: 'catechismo', en: 'catechism',  fr: 'catechisme',       de: 'katechese' },
  matrimonio: { it: 'matrimonio', en: 'marriage',   fr: 'mariage',          de: 'ehe' },
  oratorio:   { it: 'oratorio',   en: 'oratory',    fr: 'patronage',        de: 'oratorium' },
  grest:      { it: 'grest',      en: 'grest',      fr: 'grest',            de: 'grest' },
  scout:      { it: 'scout',      en: 'scouts',     fr: 'scouts',           de: 'pfadfinder' },
  coro:       { it: 'coro',       en: 'choir',      fr: 'chorale',          de: 'chor' },
  giornalino: { it: 'giornalino', en: 'newsletter', fr: 'bulletin',         de: 'pfarrblatt' },
  news:       { it: 'news',       en: 'news',       fr: 'actualites',       de: 'aktuelles' },
  privacy:    { it: 'privacy',    en: 'privacy',    fr: 'confidentialite',  de: 'datenschutz' },
};

export function isLang(x: string): x is Lang {
  return (LANGS as readonly string[]).includes(x);
}

export function pathFor(lang: Lang, key: PageKey): string {
  const slug = SLUGS[key][lang];
  return slug ? `/${lang}/${slug}/` : `/${lang}/`;
}

export function keyFromSlug(lang: Lang, slug: string): PageKey | undefined {
  return PAGE_KEYS.find((k) => SLUGS[k][lang] === slug);
}
```

- [ ] **Step 4: Esegui — routes passa**

Run: `npm test`
Expected: 5 test PASS (i18n.test.ts non esiste ancora).

- [ ] **Step 5: `src/config.ts`**

```ts
import type { Lang } from './i18n/routes';

export const SITE = {
  url: 'https://parrocchiaarma.it',
  name: 'Parrocchia Santi Giuseppe e Antonio',
  street: 'Via Monsignor Giauni, 14',
  postalCode: '18018',
  city: 'Arma di Taggia',
  province: 'IM',
  phone: '0184 43130',
  phoneHref: 'tel:+39018443130',
  email: 'parrocchiaarmaditaggia@gmail.com',
  whatsapp: '+39 329 132 7608',
  whatsappHref: 'https://wa.me/393291327608',
  instagram: 'https://instagram.com/parrocchia.arma',
  instagramHandle: '@parrocchia.arma',
  facebook: 'https://www.facebook.com/parrocchiasgiuseppeeantonioarmaditaggia',
  vat: '81002390086',
  // Coordinate: verificare su Google Maps (tasto destro sul civico → copia coordinate) prima del deploy.
  geo: { lat: 43.8271, lng: 7.8558 },
  mapsEmbed: 'https://www.google.com/maps?q=Via+Monsignor+Giauni+14,+18018+Arma+di+Taggia+IM&output=embed',
  mapsLink: 'https://www.google.com/maps/search/?api=1&query=Via+Monsignor+Giauni+14,+18018+Arma+di+Taggia+IM',
  taggboxWidgetId: '149393',
  grestPhone: '0184 43836',
  grestPhoneHref: 'tel:+39018443836',
  scout: {
    email: 'armaditaggia_1@fse.it',
    facebook: 'https://www.facebook.com/scoutFSEArmadiTaggia1',
    youtube: 'https://www.youtube.com/user/SCOUTDEUROPAFSE',
    fse: 'https://fse.it/',
    videoId: '0Lu6OTBW9l8',
  },
  marriageForm: 'https://forms.gle/tqScqfWioMvupycm7',
  credit: { name: 'DueBytes', url: 'https://www.duebytes.it', by: 'Paolo Alberti' },
  ogLocale: { it: 'it_IT', en: 'en_GB', fr: 'fr_FR', de: 'de_DE' } satisfies Record<Lang, string>,
} as const;
```

- [ ] **Step 6: `src/i18n/it.json`** (contenuto completo, fonte: vecchio sito)

```json
{
  "site": {
    "name": "Parrocchia Santi Giuseppe e Antonio",
    "nameLine1": "Parrocchia",
    "nameLine2": "Santi Giuseppe e Antonio",
    "kicker": "Arma di Taggia · Diocesi di Ventimiglia-Sanremo",
    "tagline": "Una comunità aperta a tutti, nel cuore di Arma di Taggia.",
    "city": "18018 Arma di Taggia (IM)",
    "legal": "Parrocchia Santi Giuseppe e Antonio · Via Monsignor Giauni, 14 · 18018 Arma di Taggia (IM) · P.I./C.F. 81002390086"
  },
  "nav": {
    "activities": "Attività",
    "groups": "Gruppi",
    "menu": "Menu",
    "close": "Chiudi",
    "skip": "Vai al contenuto",
    "main": "Navigazione principale"
  },
  "footer": {
    "contacts": "Contatti",
    "navigation": "Navigazione",
    "follow": "Seguici",
    "office": "Ufficio parrocchiale",
    "officeHours": "Lun–Ven 10:00–12:00 e 15:30–17:00",
    "credit": "Sito realizzato da",
    "creditBy": "di Paolo Alberti",
    "language": "Lingua"
  },
  "common": {
    "phone": "Telefono",
    "email": "Email",
    "whatsapp": "WhatsApp",
    "address": "Indirizzo",
    "social": "Social",
    "facebookPage": "Pagina Facebook",
    "instagramProfile": "Profilo Instagram",
    "hours": "Orari",
    "contacts": "Contatti",
    "openPdf": "Apri il PDF",
    "showMap": "Mostra la mappa",
    "openMaps": "Apri in Google Maps",
    "mapTitle": "Mappa della parrocchia",
    "playVideo": "Guarda il video",
    "closeLightbox": "Chiudi",
    "discover": "Scopri",
    "monday": "Lunedì", "tuesday": "Martedì", "wednesday": "Mercoledì", "thursday": "Giovedì", "friday": "Venerdì"
  },
  "pages": {
    "home":       { "nav": "Home", "title": "Parrocchia Santi Giuseppe e Antonio – Arma di Taggia", "description": "Chiesa parrocchiale ad Arma di Taggia, Via Monsignor Giauni 14: orari delle Messe, confessioni, catechismo, oratorio, GREST, scout, coro e preparazione al matrimonio.", "h1": "Parrocchia Santi Giuseppe e Antonio" },
    "contatti":   { "nav": "Contatti", "title": "Contatti", "description": "Telefono, email, WhatsApp, indirizzo e orari dell'ufficio della Parrocchia Santi Giuseppe e Antonio ad Arma di Taggia.", "h1": "Contatti" },
    "catechismo": { "nav": "Catechismo", "title": "Catechismo – Iniziazione cristiana", "description": "I sei corsi di iniziazione cristiana della parrocchia di Arma di Taggia in preparazione a Confessione, prima Comunione e Cresima.", "h1": "Catechismo" },
    "matrimonio": { "nav": "Preparazione al Matrimonio", "title": "Preparazione al Matrimonio", "description": "Il corso per fidanzati della Parrocchia Santi Giuseppe e Antonio ad Arma di Taggia: incontri, iscrizione e rito del matrimonio.", "h1": "Preparazione al Matrimonio" },
    "oratorio":   { "nav": "Oratorio", "title": "Oratorio Don Bosco", "description": "L'oratorio Don Bosco della parrocchia di Arma di Taggia: aperto dal lunedì al venerdì dalle 16:00 alle 17:30.", "h1": "Oratorio Don Bosco" },
    "grest":      { "nav": "GR.EST", "title": "GR.EST – Gruppo Estivo", "description": "Il GREST dell'oratorio Don Bosco ad Arma di Taggia: giochi, compiti, mare, preghiera e campo estivo per bambini e ragazzi dai 6 ai 14 anni.", "h1": "GR.EST. Gruppo Estivo" },
    "scout":      { "nav": "Scout", "title": "Gruppo Scout Arma di Taggia 1 – San Giovanni Lantrua", "description": "Il gruppo scout FSE della parrocchia di Arma di Taggia: storia, unità, attività e contatti.", "h1": "Gruppo Scout Arma di Taggia 1" },
    "coro":       { "nav": "Coro", "title": "Coro", "description": "Il coro \"Per i Ragazzi\" anima la Messa delle 11:00 ad Arma di Taggia. Scarica il libro dei canti con testi e accordi.", "h1": "Coro" },
    "giornalino": { "nav": "Giornalino", "title": "Giornalino della comunità parrocchiale", "description": "Leggi online i giornalini della parrocchia di Arma di Taggia: novità, eventi e aggiornamenti da tutti i gruppi.", "h1": "Giornalino della parrocchia" },
    "news":       { "nav": "News", "title": "News, avvisi ed eventi", "description": "Avvisi, locandine ed eventi della Parrocchia Santi Giuseppe e Antonio ad Arma di Taggia.", "h1": "News" },
    "privacy":    { "nav": "Privacy", "title": "Privacy", "description": "Informativa sulla privacy del sito della Parrocchia Santi Giuseppe e Antonio.", "h1": "Informativa sulla privacy" }
  },
  "home": {
    "hero": {
      "lead": "Chiesa parrocchiale di Arma di Taggia. Messe, sacramenti, oratorio e gruppi per bambini, giovani e famiglie.",
      "ctaHours": "Orari delle Messe",
      "ctaContact": "Contatti",
      "imageAlt": "L'altare maggiore della chiesa dei Santi Giuseppe e Antonio"
    },
    "hours": {
      "title": "Orario invernale",
      "masses": "Sante Messe",
      "sunday": "Festivo",
      "sundayTimes": "8:30 · 10:00 · 11:00 · 18:00",
      "vespers": "Vespri alle 17:30",
      "saturday": "Prefestivo",
      "saturdayTimes": "18:00",
      "weekday": "Feriale",
      "weekdayTimes": "9:00 · 18:00",
      "adoration": "Adorazione Eucaristica",
      "adorationTimes": "Dal lunedì al venerdì, 7:30–9:00 e 16:30–18:00",
      "rosary": "Santo Rosario",
      "rosaryTimes": "30 minuti prima della S. Messa",
      "confession": "Confessioni",
      "confessionWeekday": "Feriale 8:15–9:00 e 17:00–18:00",
      "confessionSaturday": "Prefestivo 7:30–9:15",
      "confessionMass": "Durante la celebrazione eucaristica"
    },
    "community": {
      "kicker": "La comunità",
      "title": "Gruppi e attività",
      "cards": {
        "catechismo": "Iniziazione cristiana per bambini e ragazzi",
        "matrimonio": "Il cammino dei fidanzati verso il sacramento",
        "oratorio": "Aperto ogni pomeriggio dal lunedì al venerdì",
        "grest": "L'estate dei ragazzi all'oratorio Don Bosco",
        "scout": "Gruppo FSE Arma di Taggia 1 – San Giovanni Lantrua",
        "coro": "Anima la Messa dei ragazzi della domenica"
      }
    },
    "news": {
      "kicker": "Avvisi",
      "title": "Le ultime notizie",
      "text": "Locandine, orari speciali e appuntamenti vengono pubblicati sul profilo Instagram della parrocchia.",
      "cta": "Vedi le news",
      "instagram": "Seguici su Instagram"
    },
    "newsletter": {
      "kicker": "Giornalino",
      "title": "La voce della comunità",
      "text": "Il giornalino parrocchiale raccoglie notizie, eventi e aggiornamenti da tutti i gruppi.",
      "cta": "Leggi i numeri"
    },
    "visit": {
      "kicker": "Vieni a trovarci",
      "title": "Dove siamo",
      "office": "Ufficio parrocchiale",
      "officeHours": "Dal lunedì al venerdì, 10:00–12:00 e 15:30–17:00"
    }
  },
  "contatti": {
    "lead": "Per informazioni, sacramenti e certificati rivolgiti all'ufficio parrocchiale.",
    "office": "Ufficio parrocchiale",
    "officeHours": "Dal lunedì al venerdì, 10:00–12:00 e 15:30–17:00"
  },
  "catechismo": {
    "kicker": "Iniziazione cristiana",
    "lead": "L'attività si rivolge ai bambini delle elementari e ai ragazzi delle medie che si apprestano a ricevere i sacramenti della Confessione, della Comunione e della Cresima.",
    "intro": [
      "I corsi sono sei, di cui i primi tre hanno come scopo la preparazione alla prima Comunione. In questo percorso triennale sono coinvolti anche i genitori durante gli incontri di catechismo.",
      "Si inizia con il 1° corso con i bambini di 7 anni (2ª elementare): in questo anno ai bambini vengono trasmesse le prime conoscenze della fede cristiana. Alla fine dell'anno è previsto il rinnovo del Battesimo.",
      "I successivi due anni hanno programmi più specifici: il 2° corso per i bambini di 8 anni (3ª elementare) prepara al sacramento della Confessione; il 3° corso, a 9 anni (4ª elementare), prepara al sacramento dell'Eucarestia (prima Comunione)."
    ],
    "gallery1": ["Bambini del catechismo in chiesa", "Incontro di catechismo", "Prima Comunione", "Bambini con le catechiste"],
    "part2": [
      "Terminato questo cammino, per un anno non si riprendono i soliti incontri, perché il 4° corso (bambini di 10 anni) è l'anno della comunità. Si organizzano uscite sul territorio per conoscere le varie realtà presenti in parrocchia e in diocesi (volontariato, carità, gruppi, ecc.) alternandole a qualche incontro catechistico. La partecipazione dei genitori è facoltativa.",
      "Gli ultimi due anni sono finalizzati al sacramento della Cresima: il 5° corso con i ragazzi di 11 anni ha come programma la storia della salvezza; si conclude con i ragazzi di 12 anni (2ª media) nel 6° corso, che prepara al sacramento della Cresima.",
      "I genitori hanno qualche incontro a parte."
    ],
    "gallery2": ["Ragazzi del catechismo", "Uscita sul territorio", "Celebrazione della Cresima", "Gruppo del catechismo"]
  },
  "matrimonio": {
    "kicker": "Incontri per fidanzati in cammino verso il Matrimonio cristiano",
    "intro": [
      "Il matrimonio cristiano, a differenza del matrimonio civile, è considerato dalla Chiesa cattolica un sacramento.",
      "Chiedere il \"matrimonio sacramento\" significa chiedere di diventare discepoli di Gesù come marito e moglie."
    ],
    "quote": "…a immagine di Dio li creò, maschio e femmina li creò.",
    "pathTitle": "Percorso di preparazione",
    "path": [
      "Nella Parrocchia Santi Giuseppe e Antonio ad Arma di Taggia vengono proposti ogni anno una decina di incontri che toccano svariati temi per una riflessione senza fretta su un passo così importante.",
      "Gli incontri si svolgono solitamente in inverno, da fine gennaio a marzo, una sera alla settimana, e sono tenuti da Don Alessio affiancato da alcune coppie di sposi che portano la loro esperienza ai giovani fidanzati.",
      "Per il corso bisogna iscriversi online dopo essersi confrontati con il sacerdote della propria parrocchia di residenza."
    ],
    "stepsTitle": "Se vuoi sposarti in Chiesa",
    "steps": [
      "Almeno un anno prima del matrimonio andate a parlare con il sacerdote della parrocchia in cui risiedete: sarà lui a darvi le indicazioni e a seguire le pratiche necessarie.",
      "Dopo avergli parlato e aver concordato che è il tempo giusto per cominciare il cammino di preparazione, procedete subito con l'iscrizione online.",
      "Vi contatteremo per informarvi sui corsi disponibili."
    ],
    "cta": "Iscriviti al corso",
    "ritoTitle": "Rito del Matrimonio",
    "rito": [
      "Il Rito del matrimonio permette di scegliere fra diverse formule e diverse letture. Potete confrontarvi con il sacerdote che vi accompagna in questo cammino o con chi celebrerà il vostro matrimonio.",
      "Scegliete ciò che meglio testimonia il vostro sacramento e il desiderio di divenire discepoli di Gesù. Almeno un paio di mesi prima del matrimonio iniziate a leggere il rito e a fare le vostre scelte."
    ],
    "ritoPdf": "Rito del Matrimonio",
    "closing": [
      "La preparazione prossima al matrimonio tende a concentrarsi sugli inviti, i vestiti, la festa e gli innumerevoli dettagli che consumano tanto le risorse economiche quanto le energie e la gioia…",
      "Cari fidanzati, abbiate il coraggio di essere differenti: non lasciatevi divorare dalla società del consumo e dell'apparenza."
    ],
    "imageAlt": "Banchi della chiesa addobbati per un matrimonio"
  },
  "oratorio": {
    "lead": "Uno spazio per giocare, fare i compiti e stare insieme, nel cortile e nei locali dell'oratorio Don Bosco.",
    "hoursTitle": "Orario di apertura",
    "hours": "Dal lunedì al venerdì, 16:00–17:30",
    "gallery": ["Il cortile dell'oratorio Don Bosco", "Ragazzi che giocano in oratorio", "Attività in oratorio", "Il campo dell'oratorio"]
  },
  "grest": {
    "lead": "Ogni estate all'oratorio Don Bosco viene organizzato il GREST (Gruppo Estivo) per i bambini delle elementari e delle medie (dai 6 ai 14 anni): la \"versione\" estiva dell'oratorio dedicata ai più piccoli. Sì, perché il GREST è parte dell'oratorio, che durante l'estate non va in vacanza!",
    "intro": [
      "Un'attività estiva di grande validità educativa, basata sulla convivenza di ragazzi e bambini di diverse età e animatori che, insieme a Don Enrico, giocano, imparano, lavorano e si divertono, con una particolare attenzione alla fede cristiana.",
      "Il Gruppo Estivo parte già da metà giugno, subito terminate le lezioni scolastiche, e prosegue per tutta l'estate fino ai primi giorni di settembre, per offrire ai genitori un servizio che viene incontro a un'esigenza concreta delle famiglie al termine dell'anno scolastico.",
      "Le giornate iniziano di prima mattina, alle 8:30, con un momento di preghiera, per poi proseguire con 30–40 minuti di compiti scolastici durante i quali gli educatori seguono e aiutano i bambini a svolgere i compiti delle vacanze. Segue la merenda. Al termine della pausa vengono organizzati giochi a squadre o attività sportive.",
      "Il giovedì è un giorno particolare, dedicato alla Messa a cui tutti i bambini prendono parte prima della merenda e dei giochi.",
      "Al pomeriggio, dopo un momento di preghiera, i bambini vengono portati al mare alla spiaggia La Fortezza. Al ritorno c'è sempre ancora tempo per il gioco prima dell'arrivo dei genitori."
    ],
    "gallery": ["Attività di gruppo al GREST", "Giornata al parco acquatico Le Caravelle", "Compiti delle vacanze in oratorio", "Bambini che fanno i compiti con gli educatori", "Gli educatori del GREST"],
    "moreTitle": "Non solo oratorio e mare",
    "more": [
      "A fine luglio e a fine agosto sono previste due giornate al Parco Acquatico \"Le Caravelle\".",
      "A inizio agosto viene organizzato il Campo Estivo, della durata di una settimana, in una località montana. Un giorno è dedicato a un'escursione nei luoghi vicini, mentre negli altri giorni si svolgono attività simili a quelle in oratorio: caccia al tesoro, Olimpiadi, momenti di preghiera, giochi, Messe…",
      "Durante i mesi del GR.EST. vengono inoltre organizzate gare di vari sport olimpici con una premiazione alla fine dell'estate: coppe e medaglie per tutti i partecipanti.",
      "Che altro dirvi se non… vi aspettiamo numerosi!"
    ],
    "hoursTitle": "Orari e contatti",
    "period": "Il Gruppo Estivo ha luogo dalla seconda settimana di giugno fino alla prima settimana di settembre.",
    "days": [
      { "day": "Lunedì", "hours": "8:30–12:00" },
      { "day": "Martedì", "hours": "8:30–12:00 e 15:00–18:00" },
      { "day": "Mercoledì", "hours": "8:30–12:00 e 15:00–18:00" },
      { "day": "Giovedì", "hours": "8:30–12:00 e 15:00–18:00" },
      { "day": "Venerdì", "hours": "8:30–12:00" }
    ],
    "emailLabel": "Ufficio parrocchiale",
    "phoneLabel": "Don Enrico"
  },
  "scout": {
    "kicker": "Gruppo Scout FSE · San Giovanni Lantrua",
    "historyTitle": "Un po' di storia… le nostre origini",
    "history": [
      "Il Gruppo Scout Arma di Taggia 1 nasce nei primi anni Sessanta dal desiderio di un giovane sacerdote, Don Enrico, di far vivere l'avventura dello scautismo ai ragazzi che frequentavano l'Oratorio.",
      "Anche se non appartenente a nessuna organizzazione riconosciuta, il Gruppo è sempre stato ispirato alla metodologia educativa F.S.E. (Federazione Scout d'Europa).",
      "Nel febbraio del 2001, su suggerimento del fondatore, si è deciso di rendere \"ufficiale\" il Gruppo registrandolo come membro dell'Associazione Italiana Guide e Scout d'Europa Cattolici.",
      "Il Gruppo è stato intitolato a San Giovanni Lantrua, santo nato a Molini di Triora, nell'entroterra della nostra vallata, e morto martire in Cina durante la sua missione di evangelizzazione nei primi anni del 1800. Recentemente abbiamo anche potuto aprire una piccola sezione femminile grazie all'arrivo di una Capo brevettata, appartenente a un gruppo FSE della Sicilia, che per lavoro si è spostata nelle nostre vicinanze e ci ha contattati per dare continuità al suo Servizio."
    ],
    "gallery1": ["Il gruppo scout in uscita", "Attività all'aperto", "Campo estivo", "Lupetti in cerchio"],
    "moreTitle": "Andiamo oltre…",
    "unitsIntro": "Un Gruppo FSE è normalmente composto dalle seguenti unità:",
    "table": {
      "age": "Fasce di età",
      "male": "Sezione maschile",
      "female": "Sezione femminile",
      "rows": [
        ["9–11 anni", "Branco (Lupetti)", "Cerchio (Coccinelle)"],
        ["11–16", "Riparto (Esploratori)", "Riparto (Guide)"],
        ["16–21", "Clan (Rover)", "Fuoco (Scolte)"],
        ["oltre", "Rover in Servizio e Responsabili", "Scolte in Servizio e Responsabili"]
      ]
    },
    "countIntro": "Ad oggi il Gruppo Arma di Taggia 1 conta quasi 60 membri così ripartiti:",
    "maleCount": "10 Responsabili, 15 Lupetti, 19 Esploratori, 10 Rover, 2 Soci sostenitori",
    "femaleCount": "1 Responsabile, 2 Scolte",
    "activities": [
      "I nostri ragazzi fanno attività con cadenza settimanale da settembre a giugno, per poi arrivare ai campi estivi che si svolgono tra luglio e agosto. Tra le attività molta importanza è data alla \"vita all'aperto\", dove il contatto con la natura gioca un ruolo fondamentale.",
      "Anche durante i mesi invernali e autunnali, complice il clima mite della nostra zona, cerchiamo di proporre la maggior parte delle attività all'aperto: a partire dall'area esterna del nostro Oratorio, passando alle spiagge e, con i più grandi, spingendoci in uscite di uno o più giorni nel nostro stupendo entroterra.",
      "Abbiamo dato a ogni Unità del Gruppo un nome particolare e significativo che richiama una peculiarità della nostra cultura e del nostro territorio: il Branco \"Giglio Selvatico\", ispirato a un fiore tipico dei nostri monti, protetto e raro, che oltre a essere simbolo degli Scout rappresenta la purezza; il Riparto \"La Fortezza\", in onore della Fortezza che si erge sulla costa a custodia della vallata e in richiamo della virtù cardinale; il Clan \"Stella Maris\", in onore di Maria e della nostra vicinanza al mare; e il Fuoco femminile \"Chiara Luce Badano\", a memoria della Beata ligure il cui operato è un esempio per le nostre ragazze."
    ],
    "gallery2": ["Esploratori al campo", "Attività di riparto", "Uscita nell'entroterra", "Rover in servizio"],
    "fseText": "Per avere un'idea più precisa di quello che facciamo puoi visitare il sito della nostra Federazione, nel quale troverai tantissime informazioni.",
    "fseCta": "Visita FSE.it",
    "fseAlt": "Logo dell'Associazione Italiana Guide e Scout d'Europa Cattolici",
    "contactTitle": "Contattaci",
    "contactText": "Se vuoi puoi contattarci direttamente attraverso i seguenti canali:",
    "emailNote": "Risponderemo nel minor tempo possibile",
    "facebookNote": "Scrivici sulla nostra pagina Facebook",
    "facebookLabel": "Gruppo Scout FSE Arma di Taggia 1",
    "videoTitle": "Lo scautismo non si ferma",
    "videoText": "Ti suggeriamo un breve video dell'Associazione, pubblicato anche da \"La Gazzetta dello Sport\", relativo a un periodo \"particolare\" che nonostante tutto non ci ha fermato.",
    "followTitle": "Seguici",
    "followText": "Ti invitiamo a seguirci sui nostri canali social."
  },
  "coro": {
    "kicker": "Il coro \"Per i Ragazzi\"",
    "intro": [
      "Nella parrocchia sono presenti due cori principali: uno che anima la Messa domenicale delle 10:00 accompagnato dall'organo, e un secondo coro che, accompagnato dalle chitarre, si occupa dell'animazione liturgica della Messa dei ragazzi delle 11:00.",
      "Oltre all'animazione della Messa domenicale con i bambini, il coro \"Per i Ragazzi\" anima anche altre celebrazioni quali Cresime, Prime Comunioni, anniversari di matrimonio e feste (Don Bosco, Maria Ausiliatrice, feste patronali…).",
      "Storicamente, già 40 anni fa, la Messa dei ragazzi veniva animata dai giovani che, frequentando i due oratori salesiani (Don Bosco e Maria Ausiliatrice), imparavano a cantare e a suonare la chitarra e si mettevano al servizio dell'animazione liturgica. Tale tradizione si è tramandata fino ai giorni nostri passandosi il testimone tra chitarristi e cantori.",
      "Attualmente il coro è composto da circa 25 cantori, 4 chitarristi e 2 direttori che si alternano in base alle necessità e disponibilità.",
      "Le persone che partecipano al coro sono per lo più famiglie della parrocchia, spesso coinvolte in occasione di eventi quali Prime Comunioni o Cresime, accompagnando i propri figli e poi fermandosi nel coro stabilmente.",
      "Il coro si riunisce ogni 14 giorni, solitamente il martedì sera, per le prove dei canti, sia per prepararsi all'animazione liturgica domenicale, sia per eventi speciali come Natale, Pasqua e Cresime, che richiedono a volte prove aggiuntive."
    ],
    "photoAlt": "Il coro durante il concerto di Natale 2019",
    "filesTitle": "Libro dei canti",
    "filesText": "Testi e accordi dei canti del coro, in formato A4 e in formato libretto A5.",
    "files": {
      "chordsA4": "Libro Accordi – A4",
      "chordsA5": "Libro Accordi – A5 affiancate",
      "songsA4": "Libro Canti – A4",
      "songsA5": "Libro Canti – A5 affiancate"
    }
  },
  "giornalino": {
    "lead": "Novità, eventi e aggiornamenti da tutti i gruppi della nostra comunità.",
    "issues": ["Numero 3 – Aprile 2023", "Numero 2 – Febbraio 2023", "Numero 1 – Dicembre 2022"]
  },
  "news": {
    "lead": "Locandine, avvisi e appuntamenti della parrocchia, direttamente dal nostro profilo Instagram.",
    "fallback": "Se il riquadro non si carica, trovi tutti gli aggiornamenti su",
    "and": "e"
  },
  "privacy": {
    "updated": "Ultimo aggiornamento: settembre 2026",
    "sections": [
      { "title": "Titolare del trattamento", "text": "Parrocchia Santi Giuseppe e Antonio, Via Monsignor Giauni 14, 18018 Arma di Taggia (IM), email parrocchiaarmaditaggia@gmail.com." },
      { "title": "Dati raccolti da questo sito", "text": "Il sito è composto da pagine statiche e non raccoglie dati personali. Non utilizza cookie di profilazione né strumenti di tracciamento. Le statistiche di visita sono raccolte in forma aggregata e anonima tramite Cloudflare Web Analytics, che non usa cookie e non identifica i visitatori." },
      { "title": "Cookie tecnici", "text": "L'unico cookie impostato è \"lang\", che memorizza la lingua scelta dal visitatore per un anno. Non è un cookie di profilazione e non richiede consenso." },
      { "title": "Servizi di terze parti", "text": "Alcune pagine incorporano contenuti di terzi che vengono caricati solo su richiesta del visitatore: Google Maps (pagina Contatti e Home), YouTube (pagina Scout) e il feed Instagram tramite Taggbox (pagina News). Questi servizi applicano le proprie informative sulla privacy." },
      { "title": "Diritti dell'interessato", "text": "Puoi esercitare i diritti previsti dagli articoli 15–22 del Regolamento UE 2016/679 scrivendo all'indirizzo email del titolare." }
    ]
  }
}
```

- [ ] **Step 7: Copie temporanee delle altre lingue e `src/i18n/index.ts`**

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/Parrocchia/site/src/i18n" && cp it.json en.json && cp it.json fr.json && cp it.json de.json
```

`src/i18n/index.ts`:
```ts
import type { Lang } from './routes';
import it from './it.json';
import en from './en.json';
import fr from './fr.json';
import de from './de.json';

export type Dict = typeof it;

const dicts: Record<Lang, Dict> = { it, en, fr, de };

export function getDict(lang: Lang): Dict {
  return dicts[lang];
}
```

- [ ] **Step 8: Test coerenza dizionari**

`tests/i18n.test.ts`:
```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';

const load = (l: string) => JSON.parse(readFileSync(new URL(`../src/i18n/${l}.json`, import.meta.url), 'utf8'));

function shape(v: unknown, path = ''): string[] {
  if (Array.isArray(v)) return [`${path}[${v.length}]`];
  if (v && typeof v === 'object') return Object.entries(v).flatMap(([k, x]) => shape(x, `${path}.${k}`));
  return [`${path}:${typeof v}`];
}

const it = shape(load('it')).sort();
for (const l of ['en', 'fr', 'de']) {
  test(`${l}.json ha la stessa struttura di it.json`, () => {
    assert.deepEqual(shape(load(l)).sort(), it);
  });
}

test('nessun testo vuoto in it.json', () => {
  const walk = (v: unknown, p: string) => {
    if (typeof v === 'string') assert.notEqual(v.trim(), '', p);
    else if (v && typeof v === 'object') Object.entries(v).forEach(([k, x]) => walk(x, `${p}.${k}`));
  };
  walk(load('it'), '');
});
```

- [ ] **Step 9: Test e build**

Run: `npm test && npm run build`
Expected: tutti i test PASS; build ok.

- [ ] **Step 10: Commit**

```bash
git add -A && git commit -m "feat: routing i18n, config sito e dizionario italiano"
```

---

### Task 3: Asset (immagini, PDF, logo, favicon), redirect, robots, llms.txt, link checker

**Files:**
- Create: `src/assets/**`, `public/files/*.pdf`, `public/_redirects`, `public/robots.txt`, `public/llms.txt`, `public/favicon.png`, `public/apple-touch-icon.png`, `scripts/check-links.mjs`

**Interfaces:**
- Produces i percorsi immagine usati dalle sezioni (elenco sotto) e `npm run check:links` (esce con codice 1 se un link/`src` interno in `dist/` non esiste).

- [ ] **Step 1: Copia immagini con nomi puliti** (`magick` è in `/opt/homebrew/bin`)

```bash
OLD="/Users/paolo/Server/Siti Web/Lavoro/Parrocchia"
SITE="$OLD/site"; cd "$SITE"
mkdir -p src/assets/{home,chiesa,catechismo,grest,oratorio,scout,coro,matrimonio}

cp "$OLD/assets/images/chiesa-2-1920x1440.jpg" src/assets/home/altare.jpg
# foto navata (telefono, watermark in basso a sinistra → taglio il 10% inferiore)
magick "$OLD/ - mat/Foto/Michele/Chiesa/cc9f30f6-0a44-4256-b155-55abac09025a.JPG" -auto-orient -strip -gravity North -crop 100%x90%+0+0 +repage -quality 88 src/assets/chiesa/navata.jpg

for i in 1 2 3 4 5 6 7 8; do cp "$OLD"/assets/images/catechismo$i-*.jpg src/assets/catechismo/0$i.jpg; done

magick "$OLD/ - mat/GREST/GREST attività 1.jpg" -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/grest/attivita.jpg
magick "$OLD/ - mat/GREST/GREST caravelle.jpg"   -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/grest/caravelle.jpg
magick "$OLD/ - mat/GREST/GREST compiti 1.jpg"   -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/grest/compiti-1.jpg
magick "$OLD/ - mat/GREST/GREST compiti 2.jpg"   -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/grest/compiti-2.jpg
magick "$OLD/ - mat/GREST/GREST educatori.jpg"   -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/grest/educatori.jpg

magick "$OLD/ - mat/Oratorio/Oratorio 1.JPG"  -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/oratorio/01.jpg
magick "$OLD/ - mat/Oratorio/Oratorio 2b.jpg" -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/oratorio/02.jpg
magick "$OLD/ - mat/Oratorio/Oratorio 3b.jpg" -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/oratorio/03.jpg
magick "$OLD/ - mat/Oratorio/Oratorio 4b.jpg" -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/oratorio/04.jpg

for n in 01 02 03 05 06 07 08 11; do magick "$OLD/ - mat/Scout/scout$n.jpg" -auto-orient -strip -resize '1600x1600>' -quality 88 src/assets/scout/$n.jpg; done
cp "$OLD/assets/images/fse-632x632.png" src/assets/scout/fse.png

magick "$OLD/ - mat/Coro/Natale 2019.JPG" -auto-orient -strip -resize '1800x1800>' -quality 88 src/assets/coro/natale-2019.jpg
cp "$OLD/assets/images/fidanzati-meta.jpeg" src/assets/matrimonio/fidanzati.jpg

# logo: dal PSD se possibile, altrimenti dal JPG (sfondo bianco → trasparente)
magick "$OLD/ - mat/DISEGNO CHIESA.psd[0]" -resize '512x512>' -fuzz 8% -transparent white src/assets/logo.png \
  || magick "$OLD/assets/images/disegno-chiesa-256x256.jpg" -fuzz 8% -transparent white src/assets/logo.png
magick src/assets/logo.png -resize 64x64 -background '#f7f3ec' -flatten public/favicon.png
magick src/assets/logo.png -resize 160x160 -background '#f7f3ec' -gravity center -extent 180x180 -flatten public/apple-touch-icon.png
ls -R src/assets public/*.png
```
Expected: 8 catechismo, 5 grest, 4 oratorio, 8+1 scout, 1 coro, 1 matrimonio, altare, navata, logo, 2 favicon. Se un file `- mat` non esiste, usare l'equivalente in `$OLD/assets/images/`.

- [ ] **Step 2: PDF**

```bash
OLD="/Users/paolo/Server/Siti Web/Lavoro/Parrocchia"; cd "$OLD/site"; mkdir -p public/files
cp "$OLD/assets/files/Accordi.pdf"                      public/files/coro-accordi-a4.pdf
cp "$OLD/assets/files/Accordi--Booklet.pdf"             public/files/coro-accordi-a5.pdf
cp "$OLD/assets/files/Libretto-A4.pdf"                  public/files/coro-canti-a4.pdf
cp "$OLD/assets/files/Libretto-A5.pdf"                  public/files/coro-canti-a5.pdf
cp "$OLD/assets/files/NuovoRitodelMatrimonio.pdf"       public/files/rito-del-matrimonio.pdf
cp "$OLD/assets/files/Giornalino-01-Dicembre2022.pdf"   public/files/giornalino-01-2022-12.pdf
cp "$OLD/assets/files/GIORNALE-02-Febbraio2023.pdf"     public/files/giornalino-02-2023-02.pdf
cp "$OLD/assets/files/Giornalino-03-Aprile2023.pdf"     public/files/giornalino-03-2023-04.pdf
ls -la public/files
```

- [ ] **Step 3: `public/_redirects`**

```
# host
https://www.parrocchiaarma.it/*  https://parrocchiaarma.it/:splat  301

# vecchie pagine Mobirise
/index.html        /it/              301
/contatti.html     /it/contatti/     301
/catechismo.html   /it/catechismo/   301
/fidanzati.html    /it/matrimonio/   301
/oratorio.html     /it/oratorio/     301
/GREST.html        /it/grest/        301
/scout.html        /it/scout/        301
/coro.html         /it/coro/         301
/giornalino.html   /it/giornalino/   301
/news.html         /it/news/         301
/news_vere.html    /it/news/         301
/news_bk_con_plugin.html /it/news/   301

# vecchi PDF
/assets/files/Accordi.pdf                    /files/coro-accordi-a4.pdf        301
/assets/files/Accordi--Booklet.pdf           /files/coro-accordi-a5.pdf        301
/assets/files/Libretto-A4.pdf                /files/coro-canti-a4.pdf          301
/assets/files/Libretto-A5.pdf                /files/coro-canti-a5.pdf          301
/assets/files/NuovoRitodelMatrimonio.pdf     /files/rito-del-matrimonio.pdf    301
/assets/files/Giornalino-01-Dicembre2022.pdf /files/giornalino-01-2022-12.pdf  301
/assets/files/GIORNALE-02-Febbraio2023.pdf   /files/giornalino-02-2023-02.pdf  301
/assets/files/Giornalino-03-Aprile2023.pdf   /files/giornalino-03-2023-04.pdf  301
```
Nota: la root `/` NON va qui — la gestisce la Pages Function (Task 10).

- [ ] **Step 4: `public/robots.txt`**

```
User-agent: *
Allow: /

Sitemap: https://parrocchiaarma.it/sitemap-index.xml
```

- [ ] **Step 5: `public/llms.txt`**

```
# Parrocchia Santi Giuseppe e Antonio – Arma di Taggia

> Chiesa parrocchiale cattolica ad Arma di Taggia (Imperia, Liguria), Diocesi di Ventimiglia-Sanremo. Via Monsignor Giauni 14, 18018 Arma di Taggia (IM). Tel. 0184 43130 · parrocchiaarmaditaggia@gmail.com · WhatsApp +39 329 132 7608.

Sito in italiano (default), inglese, francese e tedesco: /it/, /en/, /fr/, /de/.

## Orari (invernale)
- Sante Messe festive: 8:30, 10:00, 11:00, 18:00 (Vespri 17:30). Prefestiva: 18:00. Feriali: 9:00 e 18:00.
- Adorazione Eucaristica: lunedì–venerdì 7:30–9:00 e 16:30–18:00.
- Santo Rosario: 30 minuti prima della Messa.
- Confessioni: feriali 8:15–9:00 e 17:00–18:00; prefestivi 7:30–9:15; durante la Messa.
- Ufficio parrocchiale: lunedì–venerdì 10:00–12:00 e 15:30–17:00.

## Pagine
- [Home](https://parrocchiaarma.it/it/): orari, contatti, mappa.
- [Contatti](https://parrocchiaarma.it/it/contatti/)
- [Catechismo](https://parrocchiaarma.it/it/catechismo/): sei corsi di iniziazione cristiana (Confessione, prima Comunione, Cresima).
- [Preparazione al Matrimonio](https://parrocchiaarma.it/it/matrimonio/): corso per fidanzati (gennaio–marzo), iscrizione online.
- [Oratorio Don Bosco](https://parrocchiaarma.it/it/oratorio/): lunedì–venerdì 16:00–17:30.
- [GR.EST – Gruppo Estivo](https://parrocchiaarma.it/it/grest/): da metà giugno a inizio settembre, bambini 6–14 anni.
- [Scout FSE Arma di Taggia 1](https://parrocchiaarma.it/it/scout/): gruppo San Giovanni Lantrua, email armaditaggia_1@fse.it.
- [Coro](https://parrocchiaarma.it/it/coro/): coro "Per i Ragazzi", Messa delle 11:00; libro canti e accordi in PDF.
- [Giornalino](https://parrocchiaarma.it/it/giornalino/): giornalino parrocchiale in PDF.
- [News](https://parrocchiaarma.it/it/news/): avvisi e locandine dal profilo Instagram @parrocchia.arma.

## Social
- Instagram: https://instagram.com/parrocchia.arma
- Facebook: https://www.facebook.com/parrocchiasgiuseppeeantonioarmaditaggia
```

- [ ] **Step 6: `scripts/check-links.mjs`**

```js
// Verifica che ogni href/src interno nei file HTML di dist/ esista in dist/.
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
import { join } from 'node:path';

const DIST = 'dist';
const html = [];
(function walk(d) {
  for (const f of readdirSync(d)) {
    const p = join(d, f);
    if (statSync(p).isDirectory()) walk(p);
    else if (p.endsWith('.html')) html.push(p);
  }
})(DIST);

const missing = new Set();
for (const file of html) {
  const src = readFileSync(file, 'utf8');
  for (const m of src.matchAll(/(?:href|src)="(\/[^"#?]*)/g)) {
    const url = decodeURIComponent(m[1]);
    const candidates = [join(DIST, url), join(DIST, url, 'index.html')];
    if (!candidates.some((c) => existsSync(c) && statSync(c).isFile())) missing.add(`${url}  (in ${file})`);
  }
}
if (missing.size) {
  console.error('Link interni mancanti:\n' + [...missing].join('\n'));
  process.exit(1);
}
console.log(`ok: ${html.length} pagine, nessun link interno rotto`);
```

- [ ] **Step 7: Build + check**

Run: `npm run build && npm run check:links`
Expected: `ok: 1 pagine, nessun link interno rotto`.

- [ ] **Step 8: Commit**

```bash
git add -A && git commit -m "chore: asset immagini/PDF, redirect, robots, llms.txt, link checker"
```

---

### Task 4: Layout, header, footer, selettore lingua, SEO e routing pagine

**Files:**
- Create: `src/layouts/Base.astro`, `src/components/Seo.astro`, `src/components/Header.astro`, `src/components/Footer.astro`, `src/components/LanguageSwitcher.astro`, `src/components/Flag.astro`, `src/components/Lightbox.astro`, `src/og.ts`, `src/pages/[lang]/index.astro`, `src/pages/[lang]/[slug].astro`, 11 file `src/sections/*.astro` (placeholder)
- Delete: `src/pages/index.astro`

**Interfaces:**
- Consumes: `getDict`, `pathFor`, `LANGS`, `PAGE_KEYS`, `SLUGS`, `SITE`.
- Produces:
  - `<Base lang pageKey ogImage?>` con slot.
  - Ogni sezione: `interface Props { lang: Lang }`, esporta default il componente; le sezioni 6–8 sostituiranno i placeholder mantenendo la stessa firma.
  - `OG: Partial<Record<PageKey, ImageMetadata>>` in `src/og.ts`.

- [ ] **Step 1: `src/og.ts`**

```ts
import type { ImageMetadata } from 'astro';
import type { PageKey } from './i18n/routes';
import catechismo from './assets/catechismo/02.jpg';
import matrimonio from './assets/matrimonio/fidanzati.jpg';
import oratorio from './assets/oratorio/01.jpg';
import grest from './assets/grest/attivita.jpg';
import scout from './assets/scout/05.jpg';
import coro from './assets/coro/natale-2019.jpg';
import navata from './assets/chiesa/navata.jpg';

export const OG: Partial<Record<PageKey, ImageMetadata>> = {
  catechismo, matrimonio, oratorio, grest, scout, coro,
  contatti: navata, giornalino: navata, news: navata, privacy: navata,
};
```

- [ ] **Step 2: `src/components/Seo.astro`**

```astro
---
import { getImage } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import type { Lang, PageKey } from '../i18n/routes';
import { LANGS, DEFAULT_LANG, pathFor } from '../i18n/routes';
import { getDict } from '../i18n';
import { SITE } from '../config';
import defaultOg from '../assets/home/altare.jpg';

interface Props { lang: Lang; pageKey: PageKey; ogImage?: ImageMetadata }
const { lang, pageKey, ogImage } = Astro.props;
const t = getDict(lang);
const page = t.pages[pageKey];
const title = pageKey === 'home' ? page.title : `${page.title} · ${t.site.name}`;
const abs = (p: string) => new URL(p, SITE.url).href;
const canonical = abs(pathFor(lang, pageKey));
const og = await getImage({ src: ogImage ?? defaultOg, width: 1200, height: 630, format: 'jpg', fit: 'cover' });

const church = {
  '@context': 'https://schema.org',
  '@type': 'Church',
  name: SITE.name,
  url: abs(pathFor(lang, 'home')),
  telephone: '+39 0184 43130',
  email: SITE.email,
  address: {
    '@type': 'PostalAddress',
    streetAddress: SITE.street,
    postalCode: SITE.postalCode,
    addressLocality: SITE.city,
    addressRegion: SITE.province,
    addressCountry: 'IT',
  },
  geo: { '@type': 'GeoCoordinates', latitude: SITE.geo.lat, longitude: SITE.geo.lng },
  sameAs: [SITE.instagram, SITE.facebook],
  openingHoursSpecification: [
    { '@type': 'OpeningHoursSpecification', dayOfWeek: 'Sunday', opens: '08:30', closes: '19:00' },
    { '@type': 'OpeningHoursSpecification', dayOfWeek: 'Saturday', opens: '07:30', closes: '19:00' },
    { '@type': 'OpeningHoursSpecification', dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], opens: '07:30', closes: '19:00' },
  ],
};
const breadcrumb = {
  '@context': 'https://schema.org',
  '@type': 'BreadcrumbList',
  itemListElement: [
    { '@type': 'ListItem', position: 1, name: t.pages.home.nav, item: abs(pathFor(lang, 'home')) },
    { '@type': 'ListItem', position: 2, name: page.h1, item: canonical },
  ],
};
const ld = pageKey === 'home' ? church : breadcrumb;
---
<title>{title}</title>
<meta name="description" content={page.description} />
<link rel="canonical" href={canonical} />
{LANGS.map((l) => <link rel="alternate" hreflang={l} href={abs(pathFor(l, pageKey))} />)}
<link rel="alternate" hreflang="x-default" href={abs(pathFor(DEFAULT_LANG, pageKey))} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={t.site.name} />
<meta property="og:title" content={title} />
<meta property="og:description" content={page.description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={abs(og.src)} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content={SITE.ogLocale[lang]} />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" href="/favicon.png" type="image/png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="sitemap" href="/sitemap-index.xml" />
<meta name="theme-color" content="#f7f3ec" />
<script type="application/ld+json" set:html={JSON.stringify(ld)} />
```

- [ ] **Step 3: `src/components/Flag.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
interface Props { lang: Lang }
const { lang } = Astro.props;
---
<svg class="flag" width="22" height="15" viewBox="0 0 60 40" aria-hidden="true" focusable="false">
  {lang === 'it' && <><rect width="20" height="40" fill="#009246"/><rect x="20" width="20" height="40" fill="#fff"/><rect x="40" width="20" height="40" fill="#ce2b37"/></>}
  {lang === 'fr' && <><rect width="20" height="40" fill="#0055a4"/><rect x="20" width="20" height="40" fill="#fff"/><rect x="40" width="20" height="40" fill="#ef4135"/></>}
  {lang === 'de' && <><rect width="60" height="13.34" fill="#000"/><rect y="13.33" width="60" height="13.34" fill="#dd0000"/><rect y="26.66" width="60" height="13.34" fill="#ffce00"/></>}
  {lang === 'en' && <>
    <clipPath id="fuk"><path d="M30,20h30v20zv20h-30zh-30v-20zv-20h30z"/></clipPath>
    <rect width="60" height="40" fill="#012169"/>
    <path d="M0,0 60,40M60,0 0,40" stroke="#fff" stroke-width="8"/>
    <path d="M0,0 60,40M60,0 0,40" clip-path="url(#fuk)" stroke="#c8102e" stroke-width="5"/>
    <path d="M30,0v40M0,20h60" stroke="#fff" stroke-width="13"/>
    <path d="M30,0v40M0,20h60" stroke="#c8102e" stroke-width="8"/>
  </>}
</svg>
<style>
  .flag { display: inline-block; border-radius: 2px; box-shadow: 0 0 0 1px rgba(0,0,0,.12); flex: none; }
</style>
```

- [ ] **Step 4: `src/components/LanguageSwitcher.astro`**

```astro
---
import type { Lang, PageKey } from '../i18n/routes';
import { LANGS, pathFor } from '../i18n/routes';
import Flag from './Flag.astro';

interface Props { lang: Lang; pageKey: PageKey; label: string }
const { lang, pageKey, label } = Astro.props;
const NAMES: Record<Lang, string> = { it: 'Italiano', en: 'English', fr: 'Français', de: 'Deutsch' };
---
<details class="lang">
  <summary aria-label={`${label}: ${NAMES[lang]}`}>
    <Flag lang={lang} /><span>{lang.toUpperCase()}</span>
    <svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 8l4-4 4 4" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
  </summary>
  <ul>
    {LANGS.filter((l) => l !== lang).map((l) => (
      <li><a href={pathFor(l, pageKey)} hreflang={l} lang={l} data-lang={l}><Flag lang={l} />{NAMES[l]}</a></li>
    ))}
  </ul>
</details>

<style>
  .lang { position: relative; }
  summary {
    list-style: none; cursor: pointer;
    display: inline-flex; align-items: center; gap: 8px;
    padding: 6px 10px; border: 1px solid rgba(255,255,255,.18); border-radius: 999px;
    font-size: 0.85rem; font-weight: 600; letter-spacing: .04em;
  }
  summary::-webkit-details-marker { display: none; }
  summary:hover { border-color: rgba(255,255,255,.4); }
  ul {
    position: absolute; bottom: calc(100% + 8px); left: 0; z-index: 20;
    list-style: none; margin: 0; padding: 6px; min-width: 160px;
    background: #fff; color: var(--ink); border-radius: 12px; box-shadow: 0 12px 32px rgba(0,0,0,.25);
  }
  li a {
    display: flex; align-items: center; gap: 10px;
    padding: 8px 10px; border-radius: 8px; color: var(--ink); text-decoration: none; font-size: .95rem;
  }
  li a:hover { background: var(--bg-2); }
</style>

<script>
  document.querySelectorAll<HTMLAnchorElement>('.lang a[data-lang]').forEach((a) =>
    a.addEventListener('click', () => {
      document.cookie = `lang=${a.dataset.lang}; path=/; max-age=31536000; SameSite=Lax`;
    }),
  );
</script>
```

- [ ] **Step 5: `src/components/Header.astro`**

```astro
---
import { Image } from 'astro:assets';
import type { Lang, PageKey } from '../i18n/routes';
import { pathFor } from '../i18n/routes';
import { getDict } from '../i18n';
import logo from '../assets/logo.png';

interface Props { lang: Lang; pageKey: PageKey }
const { lang, pageKey } = Astro.props;
const t = getDict(lang);
const link = (key: PageKey) => ({ href: pathFor(lang, key), label: t.pages[key].nav, current: key === pageKey ? 'page' : undefined });
const activities = (['catechismo', 'matrimonio', 'oratorio', 'grest'] as const).map(link);
const groups = (['scout', 'coro'] as const).map(link);
const news = link('news'); const giornalino = link('giornalino'); const contatti = link('contatti');
---
<header class="site-header">
  <div class="wrap bar">
    <a class="brand" href={pathFor(lang, 'home')} aria-label={t.site.name}>
      <Image src={logo} alt="" width={44} height={44} layout="fixed" loading="eager" />
      <span class="brand-name"><span>{t.site.nameLine1}</span><strong>{t.site.nameLine2}</strong></span>
    </a>
    <button class="nav-toggle" type="button" aria-expanded="false" aria-controls="nav" aria-label={t.nav.menu}>
      <span class="bar1"></span><span class="bar2"></span>
    </button>
    <nav id="nav" class="nav" aria-label={t.nav.main}>
      <ul>
        <li><a href={news.href} aria-current={news.current}>{news.label}</a></li>
        <li>
          <details class="menu">
            <summary>{t.nav.activities}</summary>
            <ul>{activities.map((a) => <li><a href={a.href} aria-current={a.current}>{a.label}</a></li>)}</ul>
          </details>
        </li>
        <li>
          <details class="menu">
            <summary>{t.nav.groups}</summary>
            <ul>{groups.map((a) => <li><a href={a.href} aria-current={a.current}>{a.label}</a></li>)}</ul>
          </details>
        </li>
        <li><a href={giornalino.href} aria-current={giornalino.current}>{giornalino.label}</a></li>
        <li><a class="btn btn--ghost nav-cta" href={contatti.href} aria-current={contatti.current}>{contatti.label}</a></li>
      </ul>
    </nav>
  </div>
</header>

<style>
  .site-header {
    position: sticky; top: 0; z-index: 50;
    background: color-mix(in srgb, var(--bg) 86%, transparent);
    backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
    border-bottom: 1px solid var(--line);
  }
  .bar { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 72px; }
  .brand { display: flex; align-items: center; gap: 12px; color: var(--ink); text-decoration: none; }
  .brand img { mix-blend-mode: multiply; }
  .brand-name { display: flex; flex-direction: column; line-height: 1.1; font-family: var(--font-display); }
  .brand-name span { font-size: .8rem; letter-spacing: .08em; text-transform: uppercase; color: var(--ink-2); }
  .brand-name strong { font-weight: 500; font-size: 1.05rem; }

  .nav ul { list-style: none; margin: 0; padding: 0; display: flex; align-items: center; gap: 4px; }
  .nav a, .nav summary {
    display: block; padding: 8px 12px; border-radius: 8px;
    color: var(--ink); text-decoration: none; font-weight: 500; font-size: .95rem; cursor: pointer;
  }
  .nav a:hover, .nav summary:hover { background: var(--bg-2); }
  .nav a[aria-current="page"] { color: var(--accent-2); }
  .nav .nav-cta { padding: 10px 18px; margin-left: 8px; }
  .menu { position: relative; }
  .menu summary { list-style: none; display: flex; align-items: center; gap: 6px; }
  .menu summary::-webkit-details-marker { display: none; }
  .menu summary::after { content: ''; width: 6px; height: 6px; border: solid currentColor; border-width: 0 1.5px 1.5px 0; transform: translateY(-2px) rotate(45deg); }
  .menu ul {
    position: absolute; top: calc(100% + 6px); left: 0; min-width: 240px;
    flex-direction: column; align-items: stretch; gap: 2px; padding: 8px;
    background: #fff; border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow);
  }
  .menu ul a { padding: 10px 12px; }

  .nav-toggle {
    display: none; width: 44px; height: 44px; border: 0; background: transparent; cursor: pointer;
    position: relative; border-radius: 8px;
  }
  .nav-toggle span { position: absolute; left: 11px; width: 22px; height: 2px; background: var(--ink); transition: transform .25s var(--ease), top .25s var(--ease); }
  .nav-toggle .bar1 { top: 17px; } .nav-toggle .bar2 { top: 25px; }
  [data-open] .nav-toggle .bar1 { top: 21px; transform: rotate(45deg); }
  [data-open] .nav-toggle .bar2 { top: 21px; transform: rotate(-45deg); }

  @media (max-width: 899px) {
    .nav-toggle { display: block; }
    .nav { display: none; position: fixed; inset: 72px 0 0; background: var(--bg); overflow: auto; padding: 16px var(--gutter) 40px; }
    [data-open] .nav { display: block; }
    .nav ul { flex-direction: column; align-items: stretch; gap: 0; }
    .nav > ul > li { border-bottom: 1px solid var(--line); }
    .nav a, .nav summary { padding: 16px 4px; font-size: 1.2rem; font-family: var(--font-display); }
    .menu summary { color: var(--muted); font-size: .85rem; font-family: var(--font-body); text-transform: uppercase; letter-spacing: .12em; pointer-events: none; }
    .menu summary::after { display: none; }
    .menu ul { position: static; min-width: 0; padding: 0 0 8px; background: transparent; border: 0; box-shadow: none; }
    .menu ul a { padding: 12px 4px 12px 16px; }
    .nav .nav-cta { margin: 20px 0 0; text-align: center; box-shadow: none; background: var(--accent); color: #fff; }
  }
</style>

<script>
  const header = document.querySelector<HTMLElement>('.site-header')!;
  const btn = header.querySelector<HTMLButtonElement>('.nav-toggle')!;
  const menus = [...header.querySelectorAll<HTMLDetailsElement>('details.menu')];
  const mobile = matchMedia('(max-width: 899px)');

  btn.addEventListener('click', () => {
    const open = header.toggleAttribute('data-open');
    btn.setAttribute('aria-expanded', String(open));
    document.body.classList.toggle('no-scroll', open);
  });

  // su mobile i gruppi sono sempre aperti; su desktop sono tendine esclusive
  const sync = () => menus.forEach((m) => (m.open = mobile.matches));
  sync();
  mobile.addEventListener('change', sync);
  menus.forEach((m) => m.addEventListener('toggle', () => {
    if (!mobile.matches && m.open) menus.forEach((o) => { if (o !== m) o.open = false; });
  }));
  document.addEventListener('click', (e) => {
    if (!mobile.matches) menus.forEach((m) => { if (!m.contains(e.target as Node)) m.open = false; });
  });
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && !mobile.matches) menus.forEach((m) => (m.open = false));
  });
</script>
```

- [ ] **Step 6: `src/components/Footer.astro`**

```astro
---
import type { Lang, PageKey } from '../i18n/routes';
import { pathFor } from '../i18n/routes';
import { getDict } from '../i18n';
import { SITE } from '../config';
import LanguageSwitcher from './LanguageSwitcher.astro';

interface Props { lang: Lang; pageKey: PageKey }
const { lang, pageKey } = Astro.props;
const t = getDict(lang);
const navKeys = ['news', 'catechismo', 'matrimonio', 'oratorio', 'grest', 'scout', 'coro', 'giornalino', 'contatti'] as const;
---
<footer class="site-footer">
  <div class="wrap cols">
    <div>
      <p class="f-name">{t.site.name}</p>
      <p class="f-muted">{t.site.kicker}</p>
      <address>
        {SITE.street}<br />{t.site.city}<br />
        <a href={SITE.phoneHref}>{SITE.phone}</a><br />
        <a href={`mailto:${SITE.email}`}>{SITE.email}</a><br />
        <a href={SITE.whatsappHref} rel="noopener">WhatsApp {SITE.whatsapp}</a>
      </address>
      <p class="f-muted"><strong>{t.footer.office}</strong><br />{t.footer.officeHours}</p>
    </div>
    <nav aria-label={t.footer.navigation}>
      <p class="f-title">{t.footer.navigation}</p>
      <ul>{navKeys.map((k) => <li><a href={pathFor(lang, k)}>{t.pages[k].nav}</a></li>)}</ul>
    </nav>
    <div>
      <p class="f-title">{t.footer.follow}</p>
      <ul class="social">
        <li><a href={SITE.instagram} rel="noopener" target="_blank">Instagram {SITE.instagramHandle}</a></li>
        <li><a href={SITE.facebook} rel="noopener" target="_blank">Facebook</a></li>
        <li><a href={SITE.whatsappHref} rel="noopener" target="_blank">WhatsApp</a></li>
      </ul>
    </div>
  </div>
  <div class="wrap bottom">
    <p class="legal">{t.site.legal} · <a href={pathFor(lang, 'privacy')}>{t.pages.privacy.nav}</a></p>
    <LanguageSwitcher lang={lang} pageKey={pageKey} label={t.footer.language} />
    <p class="credit">{t.footer.credit} <a href={SITE.credit.url} rel="noopener" target="_blank"><strong>{SITE.credit.name}</strong></a> {t.footer.creditBy}</p>
  </div>
</footer>

<style>
  .site-footer { background: var(--navy-2); color: #cfd3dd; margin-top: auto; font-size: .95rem; }
  .site-footer a { color: #fff; text-decoration: none; }
  .site-footer a:hover { text-decoration: underline; }
  .cols { display: grid; gap: 32px; padding-block: clamp(40px, 6vw, 72px); grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }
  .f-name { font-family: var(--font-display); font-size: 1.35rem; color: #fff; margin: 0 0 4px; }
  .f-title { color: #fff; font-weight: 600; margin: 0 0 12px; letter-spacing: .04em; text-transform: uppercase; font-size: .8rem; }
  .f-muted { color: #9aa1b3; }
  address { font-style: normal; line-height: 1.7; margin: 12px 0; }
  ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
  .bottom {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px;
    padding-block: 20px; border-top: 1px solid rgba(255,255,255,.1); font-size: .8rem; color: #9aa1b3;
  }
  .bottom p { margin: 0; }
  .legal { flex: 1 1 100%; }
  @media (min-width: 900px) { .legal { flex: 1 1 auto; } }
</style>
```

- [ ] **Step 7: `src/components/Lightbox.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
interface Props { lang: Lang }
const t = getDict(Astro.props.lang);
---
<dialog class="lightbox" id="lightbox">
  <button class="lb-close" type="button" aria-label={t.common.closeLightbox}>×</button>
  <img alt="" />
  <p class="lb-cap"></p>
</dialog>

<style>
  .lightbox { border: 0; padding: 0; background: transparent; max-width: min(96vw, 1400px); max-height: 96vh; }
  .lightbox::backdrop { background: rgba(10, 14, 24, .9); }
  .lightbox img { max-width: 96vw; max-height: 86vh; width: auto; height: auto; border-radius: 8px; margin: 0 auto; }
  .lb-cap { color: #fff; text-align: center; margin: 12px 0 0; font-size: .95rem; }
  .lb-close {
    position: fixed; top: 12px; right: 12px; width: 44px; height: 44px;
    border: 0; border-radius: 50%; background: rgba(255,255,255,.15); color: #fff; font-size: 28px; cursor: pointer;
  }
</style>

<script>
  const dlg = document.getElementById('lightbox') as HTMLDialogElement;
  const img = dlg.querySelector('img')!;
  const cap = dlg.querySelector<HTMLElement>('.lb-cap')!;
  document.querySelectorAll<HTMLAnchorElement>('a[data-lightbox]').forEach((a) =>
    a.addEventListener('click', (e) => {
      e.preventDefault();
      img.src = a.href;
      img.alt = a.dataset.alt ?? '';
      cap.textContent = a.dataset.alt ?? '';
      dlg.showModal();
    }),
  );
  dlg.addEventListener('click', (e) => {
    const t = e.target as HTMLElement;
    if (t === dlg || t.closest('.lb-close')) dlg.close();
  });
</script>
```

- [ ] **Step 8: `src/layouts/Base.astro`**

```astro
---
import '@fontsource-variable/fraunces';
import '@fontsource-variable/instrument-sans';
import '../styles/global.css';
import type { ImageMetadata } from 'astro';
import type { Lang, PageKey } from '../i18n/routes';
import { getDict } from '../i18n';
import Seo from '../components/Seo.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import Lightbox from '../components/Lightbox.astro';

interface Props { lang: Lang; pageKey: PageKey; ogImage?: ImageMetadata }
const { lang, pageKey, ogImage } = Astro.props;
const t = getDict(lang);
---
<!doctype html>
<html lang={lang}>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <Seo lang={lang} pageKey={pageKey} ogImage={ogImage} />
  </head>
  <body>
    <a class="skip" href="#main">{t.nav.skip}</a>
    <Header lang={lang} pageKey={pageKey} />
    <main id="main"><slot /></main>
    <Footer lang={lang} pageKey={pageKey} />
    <Lightbox lang={lang} />
    <script>
      const io = new IntersectionObserver((entries) => {
        for (const e of entries) if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); }
      }, { rootMargin: '0px 0px -10% 0px' });
      document.querySelectorAll('.reveal').forEach((el) => io.observe(el));
    </script>
  </body>
</html>

<style is:global>
  body { min-height: 100dvh; display: flex; flex-direction: column; }
</style>
```

- [ ] **Step 9: Sezioni placeholder** (11 file, stesso contenuto cambiando la chiave)

`src/sections/Home.astro`:
```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
interface Props { lang: Lang }
const t = getDict(Astro.props.lang);
---
<section class="section"><div class="wrap"><h1>{t.pages.home.h1}</h1></div></section>
```
Ripetere per `Contatti.astro` (`t.pages.contatti.h1`), `Catechismo.astro`, `Matrimonio.astro`, `Oratorio.astro`, `Grest.astro`, `Scout.astro`, `Coro.astro`, `Giornalino.astro`, `News.astro`, `Privacy.astro` con la chiave corrispondente.

- [ ] **Step 10: Route**

`src/pages/[lang]/index.astro`:
```astro
---
import type { Lang } from '../../i18n/routes';
import { LANGS } from '../../i18n/routes';
import Base from '../../layouts/Base.astro';
import Home from '../../sections/Home.astro';

export function getStaticPaths() {
  return LANGS.map((lang) => ({ params: { lang } }));
}
const lang = Astro.params.lang as Lang;
---
<Base lang={lang} pageKey="home"><Home lang={lang} /></Base>
```

`src/pages/[lang]/[slug].astro`:
```astro
---
import type { Lang, PageKey } from '../../i18n/routes';
import { LANGS, PAGE_KEYS, SLUGS } from '../../i18n/routes';
import { OG } from '../../og';
import Base from '../../layouts/Base.astro';
import Contatti from '../../sections/Contatti.astro';
import Catechismo from '../../sections/Catechismo.astro';
import Matrimonio from '../../sections/Matrimonio.astro';
import Oratorio from '../../sections/Oratorio.astro';
import Grest from '../../sections/Grest.astro';
import Scout from '../../sections/Scout.astro';
import Coro from '../../sections/Coro.astro';
import Giornalino from '../../sections/Giornalino.astro';
import News from '../../sections/News.astro';
import Privacy from '../../sections/Privacy.astro';

type Key = Exclude<PageKey, 'home'>;
const sections = {
  contatti: Contatti, catechismo: Catechismo, matrimonio: Matrimonio, oratorio: Oratorio, grest: Grest,
  scout: Scout, coro: Coro, giornalino: Giornalino, news: News, privacy: Privacy,
} satisfies Record<Key, unknown>;

export function getStaticPaths() {
  return LANGS.flatMap((lang) =>
    PAGE_KEYS.filter((k): k is Key => k !== 'home').map((key) => ({ params: { lang, slug: SLUGS[key][lang] }, props: { key } })),
  );
}
const lang = Astro.params.lang as Lang;
const { key } = Astro.props as { key: Key };
const Section = sections[key];
---
<Base lang={lang} pageKey={key} ogImage={OG[key]}><Section lang={lang} /></Base>
```

Cancellare `src/pages/index.astro`.

- [ ] **Step 11: Build, check, link**

Run: `npm run build && npm run check && npm run check:links && find dist -name index.html | wc -l`
Expected: build ok, `astro check` 0 errori, link ok, **44** pagine (11 × 4). Verificare a mano che `dist/de/pfadfinder/index.html` contenga `hreflang="fr" href="https://parrocchiaarma.it/fr/scouts/"` e `<html lang="de">`.

- [ ] **Step 12: Controllo visivo header/footer**

Run: `npm run preview` e aprire `http://localhost:4321/it/` a 1440px e 375px: logo + menu, tendine Attività/Gruppi (desktop) e menu a tutto schermo (mobile), footer con bandierina che apre la tendina verso l'alto, credit DueBytes.

- [ ] **Step 13: Commit**

```bash
git add -A && git commit -m "feat: layout base, header, footer, selettore lingua, SEO e routing 4 lingue"
```

---

### Task 5: Componenti condivisi (PageHero, Gallery, PdfList, MapFacade, YouTubeFacade, ContactCard)

**Files:**
- Create: `src/components/PageHero.astro`, `src/components/Gallery.astro`, `src/components/PdfList.astro`, `src/components/MapFacade.astro`, `src/components/YouTubeFacade.astro`, `src/components/ContactCard.astro`, `src/assets/scout/video.jpg`
- Modify: `src/sections/Catechismo.astro` (usa i componenti per verificarli; sarà completata nel Task 7)

**Interfaces:**
- `<PageHero title kicker? lead? image? imageAlt? />`
- `<Gallery images={ImageMetadata[]} alts={string[]} />` — link con `data-lightbox` gestiti da `Lightbox.astro`
- `<PdfList lang files={{ href: string; label: string }[]} />` — `href` relativo a `public/` (es. `/files/x.pdf`)
- `<MapFacade lang />`
- `<YouTubeFacade lang id title />`
- `<ContactCard icon label value href? external? />` con `icon` ∈ `phone|mail|whatsapp|facebook|instagram|pin|clock|youtube`

- [ ] **Step 1: `src/components/PageHero.astro`**

```astro
---
import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
interface Props { title: string; kicker?: string; lead?: string; image?: ImageMetadata; imageAlt?: string }
const { title, kicker, lead, image, imageAlt = '' } = Astro.props;
---
<section class:list={['page-hero', { 'has-image': !!image }]}>
  <div class="wrap inner">
    <div class="text">
      {kicker && <span class="kicker">{kicker}</span>}
      <h1>{title}</h1>
      {lead && <p class="lead">{lead}</p>}
    </div>
    {image && <div class="media"><Image src={image} alt={imageAlt} width={960} height={720} loading="eager" fetchpriority="high" /></div>}
  </div>
</section>

<style>
  .page-hero { padding-block: clamp(48px, 7vw, 96px) clamp(32px, 5vw, 64px); }
  .inner { display: grid; gap: clamp(24px, 4vw, 56px); align-items: center; }
  .has-image .inner { grid-template-columns: 1fr; }
  @media (min-width: 900px) { .has-image .inner { grid-template-columns: 6fr 5fr; } }
  .lead { font-size: clamp(1.1rem, 1.6vw, 1.3rem); color: var(--ink-2); max-width: 56ch; margin: 0; }
  .media img { border-radius: var(--radius); box-shadow: var(--shadow); aspect-ratio: 4/3; width: 100%; }
</style>
```

- [ ] **Step 2: `src/components/Gallery.astro`**

```astro
---
import { Image, getImage } from 'astro:assets';
import type { ImageMetadata } from 'astro';
interface Props { images: ImageMetadata[]; alts: string[] }
const { images, alts } = Astro.props;
const items = await Promise.all(images.map(async (img, i) => ({
  img, alt: alts[i] ?? '', full: (await getImage({ src: img, width: 1600, format: 'webp' })).src,
})));
---
<ul class="gallery reveal">
  {items.map(({ img, alt, full }) => (
    <li><a href={full} data-lightbox data-alt={alt}><Image src={img} alt={alt} width={800} height={600} /></a></li>
  ))}
</ul>

<style>
  .gallery { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; grid-template-columns: repeat(2, 1fr); }
  @media (min-width: 720px) {
    .gallery { grid-template-columns: repeat(4, 1fr); }
    .gallery li:first-child { grid-column: span 2; grid-row: span 2; }
  }
  .gallery a { display: block; height: 100%; border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-2); }
  .gallery img { width: 100%; height: 100%; aspect-ratio: 4/3; object-fit: cover; transition: transform .5s var(--ease); }
  .gallery a:hover img { transform: scale(1.03); }
</style>
```

- [ ] **Step 3: `src/components/PdfList.astro`**

```astro
---
import { statSync } from 'node:fs';
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
interface Props { lang: Lang; files: { href: string; label: string }[] }
const { lang, files } = Astro.props;
const t = getDict(lang);
const fmt = (b: number) => (b >= 1e6 ? `${(b / 1e6).toFixed(1)} MB` : `${Math.round(b / 1e3)} KB`);
const items = files.map((f) => ({ ...f, size: fmt(statSync(`public${f.href}`).size) }));
---
<ul class="pdfs">
  {items.map((f) => (
    <li>
      <a href={f.href} target="_blank" rel="noopener">
        <svg width="28" height="28" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 2h8l6 6v14H6z" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M14 2v6h6" fill="none" stroke="currentColor" stroke-width="1.5"/><text x="7" y="18" font-size="6" font-family="sans-serif" font-weight="700" fill="currentColor">PDF</text></svg>
        <span class="lbl">{f.label}</span>
        <span class="meta">{f.size} · {t.common.openPdf} ↗</span>
      </a>
    </li>
  ))}
</ul>

<style>
  .pdfs { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
  .pdfs a {
    display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 14px; align-items: center;
    padding: 18px 20px; background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-sm);
    color: var(--ink); text-decoration: none; transition: border-color .2s, transform .2s var(--ease);
  }
  .pdfs a:hover { border-color: var(--accent); transform: translateY(-2px); }
  .pdfs svg { grid-row: span 2; color: var(--accent); }
  .lbl { font-weight: 600; }
  .meta { font-size: .85rem; color: var(--muted); }
</style>
```

- [ ] **Step 4: `src/components/MapFacade.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import { SITE } from '../config';
interface Props { lang: Lang }
const t = getDict(Astro.props.lang);
---
<div class="map">
  <div class="map-cta">
    <p><strong>{SITE.name}</strong><br />{SITE.street}<br />{t.site.city}</p>
    <div class="actions">
      <button type="button" class="btn" data-map-src={SITE.mapsEmbed} data-map-title={t.common.mapTitle}>{t.common.showMap}</button>
      <a class="btn btn--ghost" href={SITE.mapsLink} target="_blank" rel="noopener">{t.common.openMaps} ↗</a>
    </div>
  </div>
</div>

<style>
  .map {
    position: relative; aspect-ratio: 16 / 10; border-radius: var(--radius); overflow: hidden;
    background: var(--bg-2) radial-gradient(var(--line) 1px, transparent 1px) 0 0 / 22px 22px;
    display: grid; place-items: center; border: 1px solid var(--line);
  }
  .map-cta { text-align: center; padding: 24px; }
  .actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
  .map :global(iframe) { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
</style>

<script>
  document.querySelectorAll<HTMLButtonElement>('[data-map-src]').forEach((b) =>
    b.addEventListener('click', () => {
      const wrap = b.closest('.map')!;
      const f = document.createElement('iframe');
      f.src = b.dataset.mapSrc!;
      f.title = b.dataset.mapTitle ?? 'Map';
      f.loading = 'lazy';
      f.referrerPolicy = 'no-referrer-when-downgrade';
      f.allowFullscreen = true;
      wrap.replaceChildren(f);
    }),
  );
</script>
```

- [ ] **Step 5: thumbnail video + `src/components/YouTubeFacade.astro`**

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/Parrocchia/site" && curl -sSL -o src/assets/scout/video.jpg "https://i.ytimg.com/vi/0Lu6OTBW9l8/hqdefault.jpg" && file src/assets/scout/video.jpg
```
Expected: `JPEG image data, ... 480x360`.

```astro
---
import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
interface Props { lang: Lang; id: string; title: string; poster: ImageMetadata }
const { lang, id, title, poster } = Astro.props;
const t = getDict(lang);
---
<div class="yt">
  <Image src={poster} alt="" width={960} height={540} />
  <button type="button" class="play" data-yt={id} data-yt-title={title} aria-label={`${t.common.playVideo}: ${title}`}>
    <svg width="72" height="72" viewBox="0 0 72 72" aria-hidden="true"><circle cx="36" cy="36" r="36" fill="var(--accent)"/><path d="M29 24l20 12-20 12z" fill="#fff"/></svg>
  </button>
</div>

<style>
  .yt { position: relative; aspect-ratio: 16 / 9; border-radius: var(--radius); overflow: hidden; background: #000; }
  .yt img { width: 100%; height: 100%; object-fit: cover; opacity: .85; }
  .play { position: absolute; inset: 0; width: 100%; border: 0; background: transparent; display: grid; place-items: center; cursor: pointer; }
  .play svg { transition: transform .2s var(--ease); }
  .play:hover svg { transform: scale(1.08); }
  .yt :global(iframe) { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
</style>

<script>
  document.querySelectorAll<HTMLButtonElement>('[data-yt]').forEach((b) =>
    b.addEventListener('click', () => {
      const f = document.createElement('iframe');
      f.src = `https://www.youtube-nocookie.com/embed/${b.dataset.yt}?autoplay=1&rel=0`;
      f.title = b.dataset.ytTitle ?? 'Video';
      f.allow = 'accelerometer; autoplay; encrypted-media; picture-in-picture';
      f.allowFullscreen = true;
      b.closest('.yt')!.replaceChildren(f);
    }),
  );
</script>
```

- [ ] **Step 6: `src/components/ContactCard.astro`**

```astro
---
interface Props {
  icon: 'phone' | 'mail' | 'whatsapp' | 'facebook' | 'instagram' | 'pin' | 'clock' | 'youtube';
  label: string; value: string; href?: string; external?: boolean; note?: string;
}
const { icon, label, value, href, external = false, note } = Astro.props;
const paths: Record<Props['icon'], string> = {
  phone: 'M5 3h4l2 5-2.5 1.5a11 11 0 0 0 6 6L16 13l5 2v4a2 2 0 0 1-2 2A17 17 0 0 1 3 5a2 2 0 0 1 2-2z',
  mail: 'M3 5h18v14H3zM3 6l9 7 9-7',
  whatsapp: 'M12 3a9 9 0 0 0-7.8 13.5L3 21l4.6-1.2A9 9 0 1 0 12 3zM8.5 8.5c.3-.5.8-.5 1-.5l.7 1.5-.5.8a5 5 0 0 0 3 3l.8-.6 1.6.7c0 .3 0 .8-.5 1.1-1 .8-2.8.4-4.4-1.2S7.7 9.6 8.5 8.5z',
  facebook: 'M14 8h3V4h-3a4 4 0 0 0-4 4v2H7v4h3v7h4v-7h3l1-4h-4V8z',
  instagram: 'M7 3h10a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4zm5 5a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm5.5-1.5a1 1 0 1 0 0 2 1 1 0 0 0 0-2z',
  pin: 'M12 21s-7-6.5-7-11a7 7 0 0 1 14 0c0 4.5-7 11-7 11zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z',
  clock: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zm0-14v5l3 2',
  youtube: 'M22 8.5a3 3 0 0 0-2-2C18 6 12 6 12 6s-6 0-8 .5a3 3 0 0 0-2 2C1.5 10 1.5 12 1.5 12s0 2 .5 3.5a3 3 0 0 0 2 2c2 .5 8 .5 8 .5s6 0 8-.5a3 3 0 0 0 2-2c.5-1.5.5-3.5.5-3.5s0-2-.5-3.5zM10 15V9l5 3-5 3z',
};
const Tag = href ? 'a' : 'div';
---
<Tag class="ccard" href={href} target={external ? '_blank' : undefined} rel={external ? 'noopener' : undefined}>
  <span class="icon"><svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true"><path d={paths[icon]} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/></svg></span>
  <span class="body">
    <span class="lbl">{label}</span>
    <span class="val">{value}</span>
    {note && <span class="note">{note}</span>}
  </span>
</Tag>

<style>
  .ccard {
    display: flex; gap: 14px; align-items: flex-start;
    padding: 18px 20px; background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-sm);
    color: var(--ink); text-decoration: none; transition: border-color .2s, transform .2s var(--ease);
  }
  a.ccard:hover { border-color: var(--accent); transform: translateY(-2px); color: var(--ink); }
  .icon { flex: none; width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center; background: var(--bg-2); color: var(--accent); }
  .body { display: grid; gap: 2px; min-width: 0; }
  .lbl { font-size: .78rem; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); font-weight: 600; }
  .val { font-weight: 600; overflow-wrap: anywhere; }
  .note { font-size: .9rem; color: var(--ink-2); }
</style>
```

- [ ] **Step 7: Smoke test dei componenti in `src/sections/Catechismo.astro`** (versione provvisoria)

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import PageHero from '../components/PageHero.astro';
import Gallery from '../components/Gallery.astro';
import PdfList from '../components/PdfList.astro';
import MapFacade from '../components/MapFacade.astro';
import YouTubeFacade from '../components/YouTubeFacade.astro';
import ContactCard from '../components/ContactCard.astro';
import { SITE } from '../config';
import poster from '../assets/scout/video.jpg';
import c1 from '../assets/catechismo/01.jpg';
import c2 from '../assets/catechismo/02.jpg';
import c3 from '../assets/catechismo/03.jpg';
import c4 from '../assets/catechismo/04.jpg';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
---
<PageHero kicker={t.catechismo.kicker} title={t.pages.catechismo.h1} lead={t.catechismo.lead} image={c2} imageAlt={t.catechismo.gallery1[0]} />
<section class="section wrap">
  <Gallery images={[c2, c1, c4, c3]} alts={t.catechismo.gallery1} />
  <PdfList lang={lang} files={[{ href: '/files/rito-del-matrimonio.pdf', label: 'test' }]} />
  <ContactCard icon="phone" label={t.common.phone} value={SITE.phone} href={SITE.phoneHref} />
  <MapFacade lang={lang} />
  <YouTubeFacade lang={lang} id={SITE.scout.videoId} title="test" poster={poster} />
</section>
```

- [ ] **Step 8: Build e verifica manuale**

Run: `npm run build && npm run check && npm run preview`
Aprire `http://localhost:4321/it/catechismo/`: gallery con prima foto grande, click apre lightbox e Esc chiude; card PDF con dimensione; pulsante mappa carica l'iframe; play carica YouTube.

- [ ] **Step 9: Commit**

```bash
git add -A && git commit -m "feat: componenti condivisi (hero, gallery, pdf, mappa, video, contatti)"
```

---

### Task 6: Home

**Files:**
- Modify: `src/sections/Home.astro`

**Interfaces:**
- Consumes: `t.home.*`, `SITE`, `pathFor`, `MapFacade`, `ContactCard`.

- [ ] **Step 1: `src/sections/Home.astro`**

```astro
---
import { Image } from 'astro:assets';
import type { Lang, PageKey } from '../i18n/routes';
import { pathFor } from '../i18n/routes';
import { getDict } from '../i18n';
import { SITE } from '../config';
import MapFacade from '../components/MapFacade.astro';
import ContactCard from '../components/ContactCard.astro';
import altare from '../assets/home/altare.jpg';
import imgCatechismo from '../assets/catechismo/02.jpg';
import imgMatrimonio from '../assets/matrimonio/fidanzati.jpg';
import imgOratorio from '../assets/oratorio/01.jpg';
import imgGrest from '../assets/grest/attivita.jpg';
import imgScout from '../assets/scout/05.jpg';
import imgCoro from '../assets/coro/natale-2019.jpg';
import type { ImageMetadata } from 'astro';

interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
const h = t.home;
const cards: { key: PageKey; img: ImageMetadata; cls: string }[] = [
  { key: 'catechismo', img: imgCatechismo, cls: 'c-wide' },
  { key: 'matrimonio', img: imgMatrimonio, cls: 'c-tall' },
  { key: 'oratorio', img: imgOratorio, cls: '' },
  { key: 'grest', img: imgGrest, cls: '' },
  { key: 'scout', img: imgScout, cls: '' },
  { key: 'coro', img: imgCoro, cls: 'c-full' },
];
---
<section class="hero">
  <Image class="hero-img" src={altare} alt={h.hero.imageAlt} width={1920} height={1440} layout="full-width" loading="eager" fetchpriority="high" />
  <div class="wrap hero-text">
    <span class="kicker">{t.site.kicker}</span>
    <h1>{t.site.nameLine1}<br />{t.site.nameLine2}</h1>
    <p class="lead">{h.hero.lead}</p>
    <div class="actions">
      <a class="btn" href="#orari">{h.hero.ctaHours}</a>
      <a class="btn btn--light" href={pathFor(lang, 'contatti')}>{h.hero.ctaContact}</a>
    </div>
  </div>
</section>

<section class="wrap hours-wrap" id="orari">
  <div class="card hours">
    <div class="hours-head"><span class="kicker">{t.common.hours}</span><h2>{h.hours.title}</h2></div>
    <div class="hours-grid">
      <div>
        <h3>{h.hours.masses}</h3>
        <dl>
          <dt>{h.hours.sunday}</dt><dd>{h.hours.sundayTimes}<br /><small>{h.hours.vespers}</small></dd>
          <dt>{h.hours.saturday}</dt><dd>{h.hours.saturdayTimes}</dd>
          <dt>{h.hours.weekday}</dt><dd>{h.hours.weekdayTimes}</dd>
        </dl>
      </div>
      <div>
        <h3>{h.hours.adoration}</h3>
        <p>{h.hours.adorationTimes}</p>
        <h3>{h.hours.rosary}</h3>
        <p>{h.hours.rosaryTimes}</p>
      </div>
      <div>
        <h3>{h.hours.confession}</h3>
        <ul>
          <li>{h.hours.confessionWeekday}</li>
          <li>{h.hours.confessionSaturday}</li>
          <li>{h.hours.confessionMass}</li>
        </ul>
      </div>
    </div>
  </div>
</section>

<section class="section">
  <div class="wrap">
    <span class="kicker">{h.community.kicker}</span>
    <h2>{h.community.title}</h2>
    <ul class="bento">
      {cards.map(({ key, img, cls }) => (
        <li class:list={['reveal', cls]}>
          <a href={pathFor(lang, key)}>
            <Image src={img} alt="" width={900} height={600} />
            <span class="c-text"><strong>{t.pages[key].nav}</strong><span>{h.community.cards[key as keyof typeof h.community.cards]}</span></span>
          </a>
        </li>
      ))}
    </ul>
  </div>
</section>

<section class="section section--dark">
  <div class="wrap duo">
    <div class="reveal">
      <span class="kicker">{h.news.kicker}</span>
      <h2>{h.news.title}</h2>
      <p>{h.news.text}</p>
      <div class="actions">
        <a class="btn btn--light" href={pathFor(lang, 'news')}>{h.news.cta}</a>
        <a class="btn btn--ghost on-dark" href={SITE.instagram} target="_blank" rel="noopener">{h.news.instagram} ↗</a>
      </div>
    </div>
    <div class="reveal">
      <span class="kicker">{h.newsletter.kicker}</span>
      <h2>{h.newsletter.title}</h2>
      <p>{h.newsletter.text}</p>
      <a class="btn btn--light" href={pathFor(lang, 'giornalino')}>{h.newsletter.cta}</a>
    </div>
  </div>
</section>

<section class="section">
  <div class="wrap duo visit">
    <div>
      <span class="kicker">{h.visit.kicker}</span>
      <h2>{h.visit.title}</h2>
      <div class="ccards">
        <ContactCard icon="pin" label={t.common.address} value={`${SITE.street}, ${t.site.city}`} href={SITE.mapsLink} external />
        <ContactCard icon="clock" label={h.visit.office} value={h.visit.officeHours} />
        <ContactCard icon="phone" label={t.common.phone} value={SITE.phone} href={SITE.phoneHref} />
        <ContactCard icon="mail" label={t.common.email} value={SITE.email} href={`mailto:${SITE.email}`} />
      </div>
    </div>
    <MapFacade lang={lang} />
  </div>
</section>

<style>
  .hero { position: relative; min-height: min(78vh, 760px); display: grid; align-items: end; color: #fff; isolation: isolate; }
  .hero :global(.hero-img) { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 40%; z-index: -2; }
  .hero::after { content: ''; position: absolute; inset: 0; z-index: -1; background: linear-gradient(to top, rgba(18,22,34,.85) 0%, rgba(18,22,34,.35) 55%, rgba(18,22,34,.15) 100%); }
  .hero-text { padding-block: clamp(48px, 8vw, 96px) clamp(120px, 16vw, 160px); }
  .hero .kicker { color: #f0c9b3; }
  .hero h1 { color: #fff; font-size: clamp(2.5rem, 6vw, 4.5rem); }
  .hero .lead { max-width: 52ch; font-size: clamp(1.05rem, 1.6vw, 1.25rem); color: #e9e4da; }
  .actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; }
  .on-dark { color: #fff; box-shadow: inset 0 0 0 1.5px rgba(255,255,255,.35); }
  .on-dark:hover { background: rgba(255,255,255,.1); color: #fff; }

  .hours-wrap { margin-top: clamp(-100px, -12vw, -80px); position: relative; z-index: 2; }
  .hours-head h2 { margin-bottom: .25em; }
  .hours-grid { display: grid; gap: 24px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); margin-top: 16px; }
  .hours h3 { font-size: 1.05rem; margin: 0 0 .4em; color: var(--accent-2); }
  .hours h3 + p { margin-bottom: 1.2em; }
  .hours dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 6px 16px; }
  .hours dt { font-weight: 600; color: var(--ink-2); }
  .hours dd { margin: 0; }
  .hours small { color: var(--muted); }
  .hours ul { margin: 0; padding-left: 1.1em; }

  .bento { list-style: none; margin: 24px 0 0; padding: 0; display: grid; gap: 16px; grid-template-columns: repeat(6, 1fr); }
  .bento li { grid-column: span 6; }
  @media (min-width: 640px) { .bento li { grid-column: span 3; } .bento .c-full { grid-column: span 6; } }
  @media (min-width: 960px) {
    .bento .c-wide { grid-column: span 4; }
    .bento .c-tall { grid-column: span 2; }
    .bento li { grid-column: span 2; }
    .bento .c-full { grid-column: span 6; }
  }
  .bento a {
    position: relative; display: block; height: 100%; min-height: 260px; border-radius: var(--radius); overflow: hidden;
    color: #fff; text-decoration: none; isolation: isolate; background: var(--navy);
  }
  .bento img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; transition: transform .6s var(--ease); z-index: -1; }
  .bento a::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to top, rgba(18,22,34,.85), rgba(18,22,34,.05) 60%); }
  .bento a:hover img { transform: scale(1.04); }
  .bento .c-text { position: absolute; left: 20px; right: 20px; bottom: 18px; z-index: 1; display: grid; gap: 4px; }
  .bento .c-text strong { font-family: var(--font-display); font-size: 1.5rem; font-weight: 500; }
  .bento .c-text span { font-size: .95rem; color: #e9e4da; }
  .c-full a { min-height: 220px; }

  .duo { display: grid; gap: clamp(28px, 5vw, 64px); grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
  .ccards { display: grid; gap: 12px; margin-top: 8px; }
</style>
```

- [ ] **Step 2: Build e verifica**

Run: `npm run build && npm run check && npm run preview`
Aprire `/it/`: hero con foto altare, card orari che si sovrappone al hero, griglia bento (desktop: 4+2 / 2+2+2 / 6), sezione scura news+giornalino, sezione mappa. Controllare a 375px che la card orari non esca dallo schermo e che la bento sia a una colonna.

- [ ] **Step 3: Commit**

```bash
git add -A && git commit -m "feat: home page"
```

---

### Task 7: Pagine Contatti, Catechismo, Matrimonio, Oratorio, Privacy

**Files:**
- Modify: `src/sections/Contatti.astro`, `src/sections/Catechismo.astro`, `src/sections/Matrimonio.astro`, `src/sections/Oratorio.astro`, `src/sections/Privacy.astro`

- [ ] **Step 1: `src/sections/Contatti.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import { SITE } from '../config';
import PageHero from '../components/PageHero.astro';
import ContactCard from '../components/ContactCard.astro';
import MapFacade from '../components/MapFacade.astro';
import navata from '../assets/chiesa/navata.jpg';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
---
<PageHero title={t.pages.contatti.h1} lead={t.contatti.lead} image={navata} imageAlt={t.site.name} />
<section class="section--tight">
  <div class="wrap grid grid--3">
    <ContactCard icon="phone" label={t.common.phone} value={SITE.phone} href={SITE.phoneHref} />
    <ContactCard icon="mail" label={t.common.email} value={SITE.email} href={`mailto:${SITE.email}`} />
    <ContactCard icon="whatsapp" label={t.common.whatsapp} value={SITE.whatsapp} href={SITE.whatsappHref} external />
    <ContactCard icon="facebook" label={t.common.social} value={t.common.facebookPage} href={SITE.facebook} external />
    <ContactCard icon="instagram" label={t.common.social} value={SITE.instagramHandle} href={SITE.instagram} external />
    <ContactCard icon="clock" label={t.contatti.office} value={t.contatti.officeHours} />
  </div>
</section>
<section class="section">
  <div class="wrap">
    <div class="grid grid--2 align">
      <ContactCard icon="pin" label={t.common.address} value={`${SITE.street}, ${t.site.city}`} href={SITE.mapsLink} external />
      <MapFacade lang={lang} />
    </div>
  </div>
</section>
<style>
  .align { align-items: start; }
</style>
```

- [ ] **Step 2: `src/sections/Catechismo.astro`** (sostituisce la versione provvisoria del Task 5)

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import PageHero from '../components/PageHero.astro';
import Gallery from '../components/Gallery.astro';
import c1 from '../assets/catechismo/01.jpg';
import c2 from '../assets/catechismo/02.jpg';
import c3 from '../assets/catechismo/03.jpg';
import c4 from '../assets/catechismo/04.jpg';
import c5 from '../assets/catechismo/05.jpg';
import c6 from '../assets/catechismo/06.jpg';
import c7 from '../assets/catechismo/07.jpg';
import c8 from '../assets/catechismo/08.jpg';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
const c = t.catechismo;
---
<PageHero kicker={c.kicker} title={t.pages.catechismo.h1} lead={c.lead} image={c5} imageAlt={c.gallery1[3]} />
<section class="section--tight">
  <div class="wrap">
    <div class="prose">{c.intro.map((p) => <p>{p}</p>)}</div>
  </div>
</section>
<section class="section--tight"><div class="wrap"><Gallery images={[c2, c1, c4, c5]} alts={c.gallery1} /></div></section>
<section class="section--tight">
  <div class="wrap">
    <div class="prose">{c.part2.map((p) => <p>{p}</p>)}</div>
  </div>
</section>
<section class="section--tight"><div class="wrap"><Gallery images={[c3, c6, c7, c8]} alts={c.gallery2} /></div></section>
<div class="spacer"></div>
<style>
  .spacer { height: clamp(32px, 5vw, 64px); }
</style>
```

- [ ] **Step 3: `src/sections/Matrimonio.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import { SITE } from '../config';
import PageHero from '../components/PageHero.astro';
import PdfList from '../components/PdfList.astro';
import img from '../assets/matrimonio/fidanzati.jpg';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
const m = t.matrimonio;
---
<PageHero kicker={m.kicker} title={t.pages.matrimonio.h1} lead={m.intro[0]} image={img} imageAlt={m.imageAlt} />
<section class="section--tight">
  <div class="wrap prose">
    <p>{m.intro[1]}</p>
    <blockquote>{m.quote}</blockquote>
  </div>
</section>
<section class="section section--alt">
  <div class="wrap two">
    <div class="prose">
      <h2>{m.pathTitle}</h2>
      {m.path.map((p) => <p>{p}</p>)}
    </div>
    <div class="card steps">
      <h2>{m.stepsTitle}</h2>
      <ol>{m.steps.map((s) => <li>{s}</li>)}</ol>
      <a class="btn" href={SITE.marriageForm} target="_blank" rel="noopener">{m.cta} ↗</a>
    </div>
  </div>
</section>
<section class="section">
  <div class="wrap two">
    <div class="prose">
      <h2>{m.ritoTitle}</h2>
      {m.rito.map((p) => <p>{p}</p>)}
      <PdfList lang={lang} files={[{ href: '/files/rito-del-matrimonio.pdf', label: m.ritoPdf }]} />
    </div>
    <div class="prose closing">
      <p>{m.closing[0]}</p>
      <p><strong>{m.closing[1]}</strong></p>
    </div>
  </div>
</section>
<style>
  .two { display: grid; gap: clamp(28px, 5vw, 64px); grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); align-items: start; }
  .steps ol { padding-left: 1.2em; display: grid; gap: 12px; margin: 0 0 20px; }
  .steps li::marker { color: var(--accent); font-weight: 700; }
  .closing { font-family: var(--font-display); font-size: 1.2rem; color: var(--ink-2); padding-top: 3.2rem; }
</style>
```

- [ ] **Step 4: `src/sections/Oratorio.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import PageHero from '../components/PageHero.astro';
import Gallery from '../components/Gallery.astro';
import ContactCard from '../components/ContactCard.astro';
import o1 from '../assets/oratorio/01.jpg';
import o2 from '../assets/oratorio/02.jpg';
import o3 from '../assets/oratorio/03.jpg';
import o4 from '../assets/oratorio/04.jpg';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
const o = t.oratorio;
---
<PageHero title={t.pages.oratorio.h1} lead={o.lead} />
<section class="section--tight">
  <div class="wrap two">
    <Gallery images={[o1, o2, o3, o4]} alts={o.gallery} />
    <aside class="side">
      <ContactCard icon="clock" label={o.hoursTitle} value={o.hours} />
    </aside>
  </div>
</section>
<div class="spacer"></div>
<style>
  .two { display: grid; gap: 24px; align-items: start; }
  @media (min-width: 900px) { .two { grid-template-columns: 3fr 1fr; } .side { position: sticky; top: 96px; } }
  .spacer { height: clamp(32px, 5vw, 64px); }
</style>
```

- [ ] **Step 5: `src/sections/Privacy.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import PageHero from '../components/PageHero.astro';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
---
<PageHero title={t.pages.privacy.h1} lead={t.privacy.updated} />
<section class="section--tight">
  <div class="wrap prose">
    {t.privacy.sections.map((s) => <><h2>{s.title}</h2><p>{s.text}</p></>)}
  </div>
</section>
<div style="height: 64px"></div>
```

- [ ] **Step 6: Build e verifica**

Run: `npm run build && npm run check && npm run check:links && npm run preview`
Aprire `/it/contatti/`, `/it/catechismo/`, `/it/matrimonio/`, `/it/oratorio/`, `/it/privacy/` a 1440 e 375px. Il link "Iscriviti al corso" apre il Google Form; il PDF del rito si apre.

- [ ] **Step 7: Commit**

```bash
git add -A && git commit -m "feat: pagine contatti, catechismo, matrimonio, oratorio, privacy"
```

---

### Task 8: Pagine GREST, Scout, Coro, Giornalino, News

**Files:**
- Modify: `src/sections/Grest.astro`, `src/sections/Scout.astro`, `src/sections/Coro.astro`, `src/sections/Giornalino.astro`, `src/sections/News.astro`

- [ ] **Step 1: `src/sections/Grest.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import { SITE } from '../config';
import PageHero from '../components/PageHero.astro';
import Gallery from '../components/Gallery.astro';
import ContactCard from '../components/ContactCard.astro';
import attivita from '../assets/grest/attivita.jpg';
import caravelle from '../assets/grest/caravelle.jpg';
import compiti1 from '../assets/grest/compiti-1.jpg';
import compiti2 from '../assets/grest/compiti-2.jpg';
import educatori from '../assets/grest/educatori.jpg';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
const g = t.grest;
---
<PageHero title={t.pages.grest.h1} lead={g.lead} image={attivita} imageAlt={g.gallery[0]} />
<section class="section--tight"><div class="wrap prose">{g.intro.map((p) => <p>{p}</p>)}</div></section>
<section class="section--tight"><div class="wrap"><Gallery images={[attivita, caravelle, compiti1, compiti2, educatori]} alts={g.gallery} /></div></section>
<section class="section--tight"><div class="wrap prose"><h2>{g.moreTitle}</h2>{g.more.map((p) => <p>{p}</p>)}</div></section>
<section class="section section--alt">
  <div class="wrap two">
    <div>
      <h2>{g.hoursTitle}</h2>
      <p>{g.period}</p>
      <table>
        <tbody>{g.days.map((d) => <tr><th scope="row">{d.day}</th><td>{d.hours}</td></tr>)}</tbody>
      </table>
    </div>
    <div class="cards">
      <ContactCard icon="mail" label={g.emailLabel} value={SITE.email} href={`mailto:${SITE.email}`} />
      <ContactCard icon="phone" label={g.phoneLabel} value={SITE.grestPhone} href={SITE.grestPhoneHref} />
    </div>
  </div>
</section>
<style>
  .two { display: grid; gap: clamp(28px, 5vw, 64px); grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); align-items: start; }
  .cards { display: grid; gap: 12px; padding-top: 3.6rem; }
</style>
```

- [ ] **Step 2: `src/sections/Scout.astro`**

```astro
---
import { Image } from 'astro:assets';
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import { SITE } from '../config';
import PageHero from '../components/PageHero.astro';
import Gallery from '../components/Gallery.astro';
import ContactCard from '../components/ContactCard.astro';
import YouTubeFacade from '../components/YouTubeFacade.astro';
import s01 from '../assets/scout/01.jpg';
import s02 from '../assets/scout/02.jpg';
import s03 from '../assets/scout/03.jpg';
import s05 from '../assets/scout/05.jpg';
import s06 from '../assets/scout/06.jpg';
import s07 from '../assets/scout/07.jpg';
import s08 from '../assets/scout/08.jpg';
import s11 from '../assets/scout/11.jpg';
import fse from '../assets/scout/fse.png';
import poster from '../assets/scout/video.jpg';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
const s = t.scout;
---
<PageHero kicker={s.kicker} title={t.pages.scout.h1} lead={s.history[0]} image={s05} imageAlt={s.gallery1[0]} />
<section class="section--tight"><div class="wrap prose"><h2>{s.historyTitle}</h2>{s.history.slice(1).map((p) => <p>{p}</p>)}</div></section>
<section class="section--tight"><div class="wrap"><Gallery images={[s11, s06, s03, s05]} alts={s.gallery1} /></div></section>
<section class="section section--alt">
  <div class="wrap">
    <div class="prose"><h2>{s.moreTitle}</h2><p>{s.unitsIntro}</p></div>
    <table class="units">
      <thead><tr><th>{s.table.age}</th><th>{s.table.male}</th><th>{s.table.female}</th></tr></thead>
      <tbody>{s.table.rows.map((r) => <tr>{r.map((c) => <td>{c}</td>)}</tr>)}</tbody>
    </table>
    <div class="prose counts">
      <p>{s.countIntro}</p>
      <ul>
        <li><strong>{s.table.male}:</strong> {s.maleCount}</li>
        <li><strong>{s.table.female}:</strong> {s.femaleCount}</li>
      </ul>
      {s.activities.map((p) => <p>{p}</p>)}
    </div>
  </div>
</section>
<section class="section--tight"><div class="wrap"><Gallery images={[s01, s02, s07, s08]} alts={s.gallery2} /></div></section>
<section class="section--tight">
  <div class="wrap fse card">
    <Image src={fse} alt={s.fseAlt} width={120} height={120} layout="fixed" />
    <div><p>{s.fseText}</p><a class="btn" href={SITE.scout.fse} target="_blank" rel="noopener">{s.fseCta} ↗</a></div>
  </div>
</section>
<section class="section">
  <div class="wrap two">
    <div>
      <h2>{s.contactTitle}</h2>
      <p>{s.contactText}</p>
      <div class="cards">
        <ContactCard icon="mail" label={t.common.email} value={SITE.scout.email} href={`mailto:${SITE.scout.email}`} note={s.emailNote} />
        <ContactCard icon="facebook" label="Facebook" value={s.facebookLabel} href={SITE.scout.facebook} external note={s.facebookNote} />
        <ContactCard icon="youtube" label="YouTube" value="Scout d'Europa FSE" href={SITE.scout.youtube} external note={s.followText} />
      </div>
    </div>
    <div>
      <h2>{s.videoTitle}</h2>
      <p>{s.videoText}</p>
      <YouTubeFacade lang={lang} id={SITE.scout.videoId} title={s.videoTitle} poster={poster} />
    </div>
  </div>
</section>
<style>
  .units { margin: 16px 0 24px; background: var(--white); border-radius: var(--radius-sm); overflow: hidden; border: 1px solid var(--line); }
  .units th { background: var(--bg-2); }
  .counts ul { margin-bottom: 1.2em; }
  .fse { display: flex; gap: 24px; align-items: center; flex-wrap: wrap; }
  .fse p { margin: 0 0 12px; }
  .two { display: grid; gap: clamp(28px, 5vw, 64px); grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); align-items: start; }
  .cards { display: grid; gap: 12px; }
</style>
```

- [ ] **Step 3: `src/sections/Coro.astro`**

```astro
---
import { Image } from 'astro:assets';
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import PageHero from '../components/PageHero.astro';
import PdfList from '../components/PdfList.astro';
import natale from '../assets/coro/natale-2019.jpg';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
const c = t.coro;
const files = [
  { href: '/files/coro-canti-a4.pdf', label: c.files.songsA4 },
  { href: '/files/coro-canti-a5.pdf', label: c.files.songsA5 },
  { href: '/files/coro-accordi-a4.pdf', label: c.files.chordsA4 },
  { href: '/files/coro-accordi-a5.pdf', label: c.files.chordsA5 },
];
---
<PageHero kicker={c.kicker} title={t.pages.coro.h1} lead={c.intro[0]} />
<section class="section--tight">
  <div class="wrap">
    <Image class="photo" src={natale} alt={c.photoAlt} width={1600} height={900} layout="full-width" />
  </div>
</section>
<section class="section--tight"><div class="wrap prose">{c.intro.slice(1).map((p) => <p>{p}</p>)}</div></section>
<section class="section section--alt">
  <div class="wrap">
    <h2>{c.filesTitle}</h2>
    <p>{c.filesText}</p>
    <PdfList lang={lang} files={files} />
  </div>
</section>
<style>
  .photo { border-radius: var(--radius); aspect-ratio: 16 / 9; object-fit: cover; }
</style>
```

- [ ] **Step 4: `src/sections/Giornalino.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import PageHero from '../components/PageHero.astro';
import PdfList from '../components/PdfList.astro';
import navata from '../assets/chiesa/navata.jpg';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
const files = [
  { href: '/files/giornalino-03-2023-04.pdf', label: t.giornalino.issues[0] },
  { href: '/files/giornalino-02-2023-02.pdf', label: t.giornalino.issues[1] },
  { href: '/files/giornalino-01-2022-12.pdf', label: t.giornalino.issues[2] },
];
---
<PageHero title={t.pages.giornalino.h1} lead={t.giornalino.lead} image={navata} imageAlt={t.site.name} />
<section class="section--tight"><div class="wrap"><PdfList lang={lang} files={files} /></div></section>
<div style="height: 64px"></div>
```

- [ ] **Step 5: `src/sections/News.astro`**

```astro
---
import type { Lang } from '../i18n/routes';
import { getDict } from '../i18n';
import { SITE } from '../config';
import PageHero from '../components/PageHero.astro';
interface Props { lang: Lang }
const { lang } = Astro.props;
const t = getDict(lang);
---
<PageHero title={t.pages.news.h1} lead={t.news.lead} />
<section class="section--tight">
  <div class="wrap">
    <div class="taggbox" style="width:100%;height:100%" data-widget-id={SITE.taggboxWidgetId} data-tags="false"></div>
    <p class="fallback">{t.news.fallback} <a href={SITE.instagram} target="_blank" rel="noopener">Instagram</a> {t.news.and} <a href={SITE.facebook} target="_blank" rel="noopener">Facebook</a>.</p>
  </div>
</section>
<script is:inline src="https://widget.taggbox.com/embed-lite.min.js" type="text/javascript" async></script>
<div style="height: 64px"></div>
<style>
  .taggbox { min-height: 480px; }
  .fallback { margin-top: 24px; color: var(--ink-2); }
</style>
```

- [ ] **Step 6: Build e verifica**

Run: `npm run build && npm run check && npm run check:links && npm run preview`
Aprire `/it/grest/`, `/it/scout/`, `/it/coro/`, `/it/giornalino/`, `/it/news/`. Su news: se il widget Taggbox resta vuoto (come sul sito attuale) è un problema dell'account Taggbox, non del sito — segnalarlo nel report finale.

- [ ] **Step 7: Commit**

```bash
git add -A && git commit -m "feat: pagine grest, scout, coro, giornalino, news"
```

---

### Task 9: Traduzioni EN / FR / DE

**Files:**
- Modify: `src/i18n/en.json`, `src/i18n/fr.json`, `src/i18n/de.json`

**Interfaces:**
- Stessa struttura di `it.json` (il test `tests/i18n.test.ts` lo verifica: stesse chiavi, stesse lunghezze degli array).

- [ ] **Step 1: Tradurre `it.json` in inglese in `en.json`**

Regole: tradurre **tutti** i valori stringa; mantenere identiche le chiavi, la lunghezza degli array e i valori che sono nomi propri (Don Bosco, GREST, FSE, "La Fortezza", "Le Caravelle", nomi delle unità scout tra virgolette, "Per i Ragazzi"). Termini liturgici: Messa → Mass, Vespri → Vespers, Adorazione Eucaristica → Eucharistic Adoration, Confessioni → Confessions, Cresima → Confirmation, prima Comunione → First Communion, Iniziazione cristiana → Christian initiation, Oratorio → Oratory, Giornalino → Parish newsletter, Prefestivo → Saturday evening (vigil), Feriale → Weekdays, Festivo → Sundays and holy days. `footer.credit` = "Website by", `footer.creditBy` = "by Paolo Alberti" → attenzione: la frase finale deve leggersi "Website by **DueBytes** by Paolo Alberti" — usare invece `credit` = "Website by", `creditBy` = "— Paolo Alberti". `privacy.updated` = "Last updated: September 2026". Le `pages.*.title` e `description` vanno riscritte in inglese naturale con "Arma di Taggia" e "parish" per la SEO locale.

- [ ] **Step 2: Tradurre in francese in `fr.json`**

Termini: Messe, Vêpres, Adoration eucharistique, Confessions, Confirmation, Première Communion, Initiation chrétienne, Patronage (Oratorio), Bulletin paroissial, Samedi soir (messe anticipée), En semaine, Dimanche et fêtes. `footer.credit` = "Site réalisé par", `footer.creditBy` = "— Paolo Alberti". `privacy.updated` = "Dernière mise à jour : septembre 2026".

- [ ] **Step 3: Tradurre in tedesco in `de.json`**

Termini: Heilige Messe, Vesper, Eucharistische Anbetung, Beichte, Firmung, Erstkommunion, Christliche Initiation (Katechese), Oratorium, Pfarrblatt, Vorabendmesse (Samstag), Werktags, Sonn- und Feiertage. `footer.credit` = "Website von", `footer.creditBy` = "— Paolo Alberti". `privacy.updated` = "Letzte Aktualisierung: September 2026".

- [ ] **Step 4: Test, build**

Run: `npm test && npm run build && npm run check`
Expected: i 3 test di struttura PASS; build ok.

- [ ] **Step 5: Verifica visiva**

`npm run preview`, aprire `/en/`, `/fr/`, `/de/` e `/de/pfadfinder/`: nessun testo italiano residuo (cercare "della" e "Messa" nel sorgente delle pagine EN/FR/DE: `grep -rl "della " dist/en dist/fr dist/de` deve essere vuoto).

- [ ] **Step 6: Commit**

```bash
git add -A && git commit -m "feat: traduzioni inglese, francese, tedesco"
```

---

### Task 10: Redirect lingua alla root (Pages Function)

**Files:**
- Create: `src/lib/pick-lang.ts`, `functions/index.js`, `wrangler.jsonc`
- Test: `tests/pick-lang.test.ts`

**Interfaces:**
- `pickLang(cookie: string | null, acceptLanguage: string | null): 'it' | 'en' | 'fr' | 'de'`

- [ ] **Step 1: Test**

`tests/pick-lang.test.ts`:
```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { pickLang } from '../src/lib/pick-lang.ts';

test('cookie valido vince', () => {
  assert.equal(pickLang('foo=1; lang=de', 'it-IT,it;q=0.9'), 'de');
});
test('cookie non valido ignorato', () => {
  assert.equal(pickLang('lang=es', 'fr-FR,fr;q=0.9,en;q=0.8'), 'fr');
});
test('Accept-Language con qualità', () => {
  assert.equal(pickLang(null, 'es;q=0.9,en-GB;q=0.8,de;q=0.7'), 'en');
});
test('prima lingua supportata nell ordine dichiarato', () => {
  assert.equal(pickLang(null, 'pt-BR,pt;q=0.9,fr;q=0.8'), 'fr');
});
test('fallback italiano', () => {
  assert.equal(pickLang(null, null), 'it');
  assert.equal(pickLang('', 'es,pt'), 'it');
});
```

- [ ] **Step 2: Esegui — fallisce** (`Cannot find module`)

- [ ] **Step 3: `src/lib/pick-lang.ts`**

```ts
const LANGS = ['it', 'en', 'fr', 'de'] as const;
type Lang = (typeof LANGS)[number];
const isLang = (x: string): x is Lang => (LANGS as readonly string[]).includes(x);

export function pickLang(cookie: string | null, acceptLanguage: string | null): Lang {
  const m = /(?:^|;\s*)lang=([a-z]{2})(?:;|$)/.exec(cookie ?? '');
  if (m && isLang(m[1]!)) return m[1] as Lang;
  const prefs = (acceptLanguage ?? '')
    .split(',')
    .map((part, i) => {
      const [tag = '', ...params] = part.trim().split(';');
      const q = params.map((p) => p.trim()).find((p) => p.startsWith('q='));
      return { code: tag.slice(0, 2).toLowerCase(), q: q ? parseFloat(q.slice(2)) : 1, i };
    })
    .sort((a, b) => b.q - a.q || a.i - b.i);
  return prefs.find((p) => isLang(p.code))?.code as Lang | undefined ?? 'it';
}
```

- [ ] **Step 4: Esegui — passa**

Run: `npm test` → 5 test pick-lang PASS.

- [ ] **Step 5: `functions/index.js`**

```js
import { pickLang } from '../src/lib/pick-lang';

export const onRequest = ({ request }) => {
  const lang = pickLang(request.headers.get('Cookie'), request.headers.get('Accept-Language'));
  return new Response(null, {
    status: 302,
    headers: { Location: `/${lang}/`, Vary: 'Accept-Language, Cookie', 'Cache-Control': 'no-store' },
  });
};
```

- [ ] **Step 6: `wrangler.jsonc`** (solo per il dev locale)

```jsonc
{
  "name": "parrocchia-arma",
  "compatibility_date": "2026-09-01",
  "pages_build_output_dir": "dist"
}
```

- [ ] **Step 7: Test locale**

```bash
npm run build && (npx wrangler pages dev dist --port 8788 & sleep 6
curl -sI http://localhost:8788/ -H 'Accept-Language: de-DE,de;q=0.9' | grep -i location
curl -sI http://localhost:8788/ -H 'Accept-Language: es' | grep -i location
curl -sI http://localhost:8788/ -H 'Cookie: lang=fr' -H 'Accept-Language: en' | grep -i location
curl -sI http://localhost:8788/contatti.html | grep -i location
kill %1)
```
Expected: `location: /de/`, `location: /it/`, `location: /fr/`, `location: /it/contatti/`.

- [ ] **Step 8: Commit**

```bash
git add -A && git commit -m "feat: redirect lingua alla root con Pages Function"
```

---

### Task 11: Verifica finale (build, test, screenshot, Lighthouse)

**Files:**
- Modify: eventuali correzioni CSS/contenuto emerse dalla verifica.

- [ ] **Step 1: Suite completa**

Run: `npm test && npm run check && npm run build && npm run check:links`
Expected: tutto verde, 44 pagine, `dist/sitemap-index.xml` e `dist/sitemap-0.xml` presenti con 44 `<loc>`:
```bash
grep -o '<loc>' dist/sitemap-0.xml | wc -l   # 44
```

- [ ] **Step 2: Coordinate mappa**

Aprire `https://www.google.com/maps/search/Via+Monsignor+Giauni+14,+18018+Arma+di+Taggia` nel browser, tasto destro sul segnaposto → copia le coordinate, aggiornare `SITE.geo` in `src/config.ts` e rimuovere il commento "verificare".

- [ ] **Step 3: Screenshot responsive** (Chrome MCP o DevTools) con `npm run preview`

Per ogni pagina IT (`/it/`, `/it/contatti/`, `/it/catechismo/`, `/it/matrimonio/`, `/it/oratorio/`, `/it/grest/`, `/it/scout/`, `/it/coro/`, `/it/giornalino/`, `/it/news/`, `/it/privacy/`) a 375, 768 e 1440 px, più `/en/`, `/fr/`, `/de/` a 1440. Verificare: nessuno scroll orizzontale, immagini non deformate, tendine e menu mobile funzionanti, footer con bandiera e credit, testi non tagliati. Salvare le schermate in `docs/screenshots/` (solo le 1440 IT, per il report al cliente).

- [ ] **Step 4: Lighthouse mobile** su `/it/` e `/it/scout/`

```bash
npx lighthouse http://localhost:4321/it/ --preset=perf --form-factor=mobile --quiet --output=json --output-path=/tmp/lh-home.json && node -e "const r=require('/tmp/lh-home.json').categories;console.log(Object.fromEntries(Object.entries(r).map(([k,v])=>[k,v.score*100])))"
```
Expected: performance ≥ 95; se sotto, ridurre `width` delle immagini hero/bento o aggiungere `loading="lazy"` dove manca. Ripetere con `--preset=desktop`. Eseguire anche senza `--preset` per Accessibilità/SEO/Best practices ≥ 95.

- [ ] **Step 5: Accessibilità rapida**

Navigare `/it/` solo da tastiera: skip link visibile al primo Tab, tendine Attività/Gruppi apribili con Invio, chiudibili con Esc, tendina lingua nel footer raggiungibile, lightbox chiudibile con Esc.

- [ ] **Step 6: Commit correzioni**

```bash
git add -A && git commit -m "fix: correzioni da verifica responsive e Lighthouse"
```

---

### Task 12: Deploy su Cloudflare Pages e migrazione dominio

**Files:**
- Create: `README.md` (istruzioni di aggiornamento per Paolo), `docs/deploy.md` (checklist dominio)

- [ ] **Step 1: `README.md`**

```markdown
# parrocchiaarma.it

Sito statico Astro, 4 lingue. Hosting: Cloudflare Pages (deploy automatico a ogni push su `main`).

## Comandi
- `npm run dev` — sviluppo su http://localhost:4321/it/
- `npm run build` — build in `dist/`
- `npm test` — test routing/i18n/redirect lingua
- `npm run check:links` — verifica link interni dopo la build

## Dove si cambiano le cose
- Testi: `src/i18n/{it,en,fr,de}.json` (stessa struttura in tutte le lingue; `npm test` lo verifica)
- Orari Messe: `home.hours` nei JSON
- Telefoni, email, social, ID widget Instagram: `src/config.ts`
- PDF (giornalino, coro): `public/files/` + elenco in `src/sections/Giornalino.astro` / `Coro.astro`
- Immagini: `src/assets/<pagina>/`
- Nuova pagina: aggiungere la chiave in `PAGE_KEYS`/`SLUGS` (`src/i18n/routes.ts`), i testi in `pages.*` dei 4 JSON, la sezione in `src/sections/` e la voce in `sections` di `src/pages/[lang]/[slug].astro`.
```

- [ ] **Step 2: Repo GitHub e progetto Pages**

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/Parrocchia/site"
# repo già creato: https://github.com/paoloalby/parrocchia-arma (origin)
git push
```
Poi su dash.cloudflare.com → Workers & Pages → Create → Pages → Connect to Git → repo `parrocchia-arma`, branch `main`, framework preset **Astro**, build command `npm run build`, output `dist`, Node version env `NODE_VERSION=24`. Deploy. Verificare `https://parrocchia-arma.pages.dev/` → redirect su `/it/`; `/contatti.html` → 301 `/it/contatti/`.

- [ ] **Step 3: Web Analytics**

Pages → progetto → Settings → **Web Analytics** → Enable (iniezione automatica del beacon, nessun cookie). Nessuna modifica al codice.

- [ ] **Step 4: `docs/deploy.md` — checklist dominio** (da eseguire con il cliente)

```markdown
# Migrazione dominio parrocchiaarma.it

1. **TopHost**: pannello → dominio → sblocca il dominio (unlock) e richiedi l'AuthCode (EPP). Verificare che l'email del registrante sia raggiungibile (servirà per confermare il trasferimento).
2. **IONOS**: Domini → Trasferisci dominio → `parrocchiaarma.it` + AuthCode. Solo dominio, nessun pacchetto hosting/mail. Il trasferimento .it richiede 1–5 giorni; il vecchio sito resta online nel frattempo.
3. **Cloudflare**: Add a site → `parrocchiaarma.it` → piano Free → non importare i record vecchi (nessun record MX: la mail è Gmail non legata al dominio). Annotare i 2 nameserver assegnati.
4. **IONOS**: dominio → Nameserver → usa nameserver personalizzati → inserire i 2 di Cloudflare. Attendere l'attivazione della zona (email di Cloudflare, di solito < 1 h).
5. **Cloudflare Pages**: progetto → Custom domains → aggiungere `parrocchiaarma.it` e `www.parrocchiaarma.it` (i record CNAME vengono creati automaticamente).
6. Cloudflare → SSL/TLS → Full; Edge Certificates → Always Use HTTPS ON.
7. Verifiche: `curl -I https://www.parrocchiaarma.it/` → 301 su apex; `curl -I https://parrocchiaarma.it/coro.html` → 301 `/it/coro/`; `https://parrocchiaarma.it/` con browser italiano → `/it/`.
8. Google Search Console: aggiungere la proprietà dominio `parrocchiaarma.it` (verifica via record TXT su Cloudflare) e inviare `https://parrocchiaarma.it/sitemap-index.xml`.
9. Dopo 30 giorni: disdire l'hosting TopHost.
```

- [ ] **Step 5: Commit e push**

```bash
git add -A && git commit -m "docs: README e checklist deploy/dominio" && git push
```

---

## Self-review

- **Spec coverage**: contenuti di tutte le 11 pagine (T6–T8), news = Taggbox + fallback (T8), privacy (T7), slug tradotti + hreflang + x-default (T2, T4), redirect root con cookie (T10), selettore lingua con bandierina nel footer (T4), credit DueBytes (T4), `_redirects` vecchi URL e PDF (T3), Web Analytics senza cookie (T12), JSON-LD Church/Breadcrumb, OG, sitemap, robots, llms.txt (T3–T4), immagini responsive via `astro:assets`, facade mappa/video (T5), reveal con reduced-motion (T1, T4), deploy e dominio (T12), verifica (T11).
- **Placeholder**: nessun TBD; le traduzioni (T9) sono definite da regole esplicite sul contenuto di `it.json`; le coordinate geo hanno una procedura di verifica (T11 step 2).
- **Coerenza tipi**: `getDict`, `pathFor`, `LANGS`, `PAGE_KEYS`, `SLUGS`, `SITE`, `OG`, props dei componenti usate con gli stessi nomi in T4–T8; `pickLang(cookie, acceptLanguage)` uguale in T10 test e Function.
