# Pane di Triora, nuovo sito: piano di implementazione

> **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:** Sito statico Astro 7 per paneditriora.it, 6 lingue, due temi grafici (Notte e Farina) da un solo codice, pronto per due progetti Cloudflare Pages.

**Architecture:** Astro con i18n nativo (tutte le lingue con prefisso, root smistata da una Pages Function), content collections per prodotti/pagine/stringhe UI, tema scelto a build time via `THEME` (astro:env). Tre componenti hanno due varianti (Header, HeroHome, ProductGrid); tutto il resto è condiviso. Zero framework UI, JS vanilla in tre punti.

**Tech Stack:** Astro 7.3, TypeScript, `@astrojs/sitemap`, `@fontsource-variable/*`, Sharp (default), Node 24, pnpm, `node --test`, ffmpeg, ImageMagick, wrangler.

Spec di riferimento: `docs/superpowers/specs/2026-09-17-paneditriora-refactoring-design.md`. Mockup: `.superpowers/brainstorm/*/content/direzioni-visive-v4.html` (sezione B = Notte) e `fusione-ac.html` (Farina).

## Global Constraints

- Astro `>=7.3`, Node `>=22.12` (in locale c'è 24.11), pnpm.
- Lingue: `en, it, fr, de, es, nl`. Default e `x-default`: `en`. Tutte con prefisso.
- Slug prodotti identici in ogni lingua: `pane-di-triora, pane-multicereali, grissini, pane-d-orzo, bruschette, biscotti-di-triora, biscottate-di-triora`.
- Nessun cookie tranne `lang` (scritto solo al cambio lingua manuale). Nessun analytics con cookie. Nessun banner.
- Nessuna libreria JS client. Nessun framework UI.
- Font: solo `@fontsource-variable/fraunces`, `instrument-sans`, `bricolage-grotesque`, `dm-sans`. Mai Google Fonts remoto. Mai Lithos Pro.
- Colori tema Notte: bg `#140C0A`, testo `#F2E6D3`, oro `#E0A43C`, bordeaux `#7A2E2A`. Tema Farina: bg `#F4ECDC`, testo `#2B1A14`, bordeaux `#340005`, oro `#FCD255`, crosta `#B5541C`, muted `#8A7A66`.
- Claim principale: "l'unico, autentico Pane di Triora", "da oltre sessant'anni". La durata del pane solo come riga di supporto ("nata per durare giorni"). Mai "una settimana".
- Compilatore Rust di Astro 7: ogni tag HTML chiuso, nessun HTML invalido.
- Ogni pagina: un solo `<h1>`, `title`, `description`, `canonical`, 6 `hreflang` + `x-default`.
- Trailing slash sempre (`trailingSlash: 'always'`).
- Commit dopo ogni task. Messaggi in italiano, imperativo breve.
- Directory progetto: `/Users/paolo/Server/Siti Web/Lavoro/Pane di Triora` (contiene già `_assets_originali/`, `docs/`, `.gitignore`, repo git con remote `origin`).

---

## Struttura file finale

```
astro.config.mjs
package.json
tsconfig.json
functions/
  index.ts                      Pages Function: redirect root per lingua
  _lib/negotiate.ts             pickLang(cookie, acceptLanguage) puro, testato
public/
  _headers  _redirects  manifest.webmanifest  favicon.ico
  favicon/*.png
  flags/{en,it,fr,de,es,nl}.svg
  video/hero.mp4  hero.webm  hero-poster.jpg
  docs/tabelle-nutrizionali.pdf
  partner/{eu,fesr,ita,lig}.png
scripts/
  make-video.sh                 ffmpeg
  import-photos.sh              copia + ridimensiona foto originali in src/assets/photos
  check-content.mjs             ogni lingua ha gli stessi file e le stesse chiavi
  check-dist.mjs                hreflang e link interni su dist/
src/
  content.config.ts
  content/products/<lang>/<slug>.md      (42 file)
  content/pages/<lang>/<key>.md          (36 file: bakery, triora, press, contact, privacy, legal)
  content/ui/<lang>.json                 (6 file)
  data/business.ts   data/locales.ts
  lib/theme.ts   lib/content.ts   lib/seo.ts   lib/text.ts
  assets/photos/index.ts + *.jpg
  assets/logo/pittogramma.svg   wordmark.png
  styles/global.css
  layouts/Base.astro
  components/
    themed/index.ts
    Header.notte.astro  Header.farina.astro
    HeroHome.notte.astro  HeroHome.farina.astro
    ProductGrid.notte.astro  ProductGrid.farina.astro
    Footer.astro  LangSwitcher.astro  Picto.astro  JsonLd.astro
    MarchioBand.astro  Faq.astro  Gallery.astro  YouTubeFacade.astro  ContactBlock.astro
  pages/
    [lang]/index.astro
    [lang]/[products]/index.astro
    [lang]/[products]/[slug].astro
    [lang]/[page].astro
    robots.txt.ts   llms.txt.ts
tests/
  negotiate.test.ts   locales.test.ts   text.test.ts
```

---

### Task 1: Scaffold Astro 7 con i18n, tema e font

**Files:**
- Create: `package.json`, `astro.config.mjs`, `tsconfig.json`, `src/lib/theme.ts`, `src/styles/global.css`, `src/pages/[lang]/index.astro` (segnaposto, riscritto nel Task 8)
- Modify: `.gitignore`

**Interfaces:**
- Produces: `THEME` da `astro:env/server` (`'notte' | 'farina'`), `NOINDEX` boolean; `src/lib/theme.ts` esporta `theme`, `isNotte`; token CSS su `html[data-theme]`.

- [ ] **Step 1: Inizializza il progetto**

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/Pane di Triora"
pnpm init
pnpm add astro@^7.3 @astrojs/sitemap@^3.7 sharp
pnpm add @fontsource-variable/fraunces @fontsource-variable/instrument-sans @fontsource-variable/bricolage-grotesque @fontsource-variable/dm-sans
pnpm add -D @astrojs/check typescript wrangler
```

- [ ] **Step 2: Scrivi `package.json` (sostituisci scripts e name)**

```json
{
  "name": "paneditriora",
  "private": true,
  "type": "module",
  "engines": { "node": ">=22.12" },
  "scripts": {
    "dev": "astro dev",
    "dev:notte": "THEME=notte astro dev",
    "build": "astro build",
    "build:notte": "THEME=notte astro build",
    "build:farina": "THEME=farina astro build",
    "preview": "astro preview",
    "check": "astro check",
    "test": "node --test tests/",
    "check:content": "node scripts/check-content.mjs",
    "check:dist": "node scripts/check-dist.mjs",
    "verify": "pnpm test && pnpm check:content && pnpm check && pnpm build:notte && pnpm check:dist && pnpm build:farina && pnpm check:dist",
    "pages:dev": "wrangler pages dev dist",
    "deploy:notte": "NOINDEX=true THEME=notte astro build && wrangler pages deploy dist --project-name paneditriora-notte",
    "deploy:farina": "NOINDEX=true THEME=farina astro build && wrangler pages deploy dist --project-name paneditriora-farina"
  }
}
```

Mantieni le `dependencies`/`devDependencies` generate da pnpm.

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

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

const LOCALES = ['en', 'it', 'fr', 'de', 'es', 'nl'];

export default defineConfig({
  site: 'https://www.paneditriora.it',
  output: 'static',
  trailingSlash: 'always',
  i18n: {
    defaultLocale: 'en',
    locales: LOCALES,
    routing: { prefixDefaultLocale: true },
  },
  image: { layout: 'constrained' },
  env: {
    schema: {
      THEME: envField.enum({ context: 'server', access: 'public', values: ['notte', 'farina'], default: 'farina' }),
      NOINDEX: envField.boolean({ context: 'server', access: 'public', default: false }),
      CF_BEACON: envField.string({ context: 'server', access: 'public', optional: true }),
    },
  },
  integrations: [
    sitemap({
      i18n: {
        defaultLocale: 'en',
        locales: Object.fromEntries(LOCALES.map((l) => [l, l])),
      },
      filter: (page) => !/\/(privacy|note-legali|legal-notice|mentions-legales|impressum|aviso-legal|juridische-kennisgeving)\/$/.test(page),
    }),
  ],
});
```

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

```json
{
  "extends": "astro/tsconfigs/strict",
  "compilerOptions": {
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "types": ["@cloudflare/workers-types"]
  },
  "include": [".astro/types.d.ts", "src/**/*", "functions/**/*", "tests/**/*"],
  "exclude": ["dist"]
}
```

Poi `pnpm add -D @cloudflare/workers-types`.

- [ ] **Step 5: Scrivi `src/lib/theme.ts`**

```ts
import { THEME, NOINDEX, CF_BEACON } from 'astro:env/server';

export type Theme = 'notte' | 'farina';
export const theme: Theme = THEME;
export const isNotte = THEME === 'notte';
export const noindex = NOINDEX;
export const cfBeacon = CF_BEACON;
```

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

```css
/* Reset minimo */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
body { min-height: 100dvh; line-height: 1.5; -webkit-font-smoothing: antialiased; }
img, picture, video, svg { display: block; max-width: 100%; height: auto; }
a { color: inherit; }
ul[role="list"] { list-style: none; padding: 0; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

/* Token comuni */
:root {
  --wrap: 1200px;
  --gutter: clamp(16px, 4vw, 44px);
  --radius: 10px;
  --pill: 999px;
  --ease: cubic-bezier(.2, .7, .2, 1);
}

/* Tema Notte */
html[data-theme="notte"] {
  --bg: #140C0A;
  --bg-2: #1c110e;
  --ink: #F2E6D3;
  --ink-2: rgba(242, 230, 211, .78);
  --acc: #E0A43C;
  --bord: #7A2E2A;
  --line: rgba(242, 230, 211, .14);
  --paper: #F4ECDC;         /* fondo pagine interne di testo */
  --paper-ink: #2B1A14;
  --font-display: 'Bricolage Grotesque Variable', system-ui, sans-serif;
  --font-body: 'DM Sans Variable', system-ui, sans-serif;
  --h1-weight: 500;
  --h1-track: -.035em;
  --h1-case: uppercase;
}

/* Tema Farina */
html[data-theme="farina"] {
  --bg: #F4ECDC;
  --bg-2: #EFE3CC;
  --ink: #2B1A14;
  --ink-2: #4a3a30;
  --acc: #B5541C;
  --bord: #340005;
  --oro: #FCD255;
  --mut: #8A7A66;
  --line: rgba(52, 0, 5, .14);
  --paper: #F4ECDC;
  --paper-ink: #2B1A14;
  --font-display: 'Fraunces Variable', Georgia, serif;
  --font-body: 'Instrument Sans Variable', system-ui, sans-serif;
  --h1-weight: 300;
  --h1-track: -.02em;
  --h1-case: none;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1rem;
}
h1, h2, h3 { font-family: var(--font-display); line-height: 1.05; font-weight: 400; }
h1 { font-weight: var(--h1-weight); letter-spacing: var(--h1-track); text-transform: var(--h1-case); font-size: clamp(2.4rem, 7vw, 5.5rem); }
h2 { font-size: clamp(1.7rem, 3.4vw, 2.6rem); letter-spacing: -.01em; }
h3 { font-size: 1.3rem; }
html[data-theme="farina"] h1 { font-variation-settings: "opsz" 144; }
html[data-theme="farina"] h1 em { font-style: italic; font-weight: 400; color: var(--acc); }
html[data-theme="notte"] h1 em { font-style: normal; color: var(--acc); }

.wrap { width: min(var(--wrap), 100% - 2 * var(--gutter)); margin-inline: auto; }
.section { padding-block: clamp(48px, 8vw, 96px); }
.kicker { font-size: .75rem; letter-spacing: .18em; text-transform: uppercase; color: var(--acc); }
.btn { display: inline-flex; align-items: center; gap: .5em; padding: .8em 1.4em; border-radius: var(--pill); border: 1.5px solid var(--bord); color: var(--bord); font-weight: 500; text-decoration: none; transition: background .2s var(--ease), color .2s var(--ease); }
.btn--fill { background: var(--bord); color: var(--paper); }
html[data-theme="notte"] .btn { border-color: var(--acc); color: var(--acc); }
html[data-theme="notte"] .btn--fill { background: var(--acc); color: var(--bg); }
.btn:hover { background: var(--bord); color: var(--paper); }
html[data-theme="notte"] .btn:hover { background: var(--acc); color: var(--bg); }

.skip { position: absolute; left: -999px; top: 8px; padding: .6em 1em; background: var(--bord); color: #fff; border-radius: var(--radius); z-index: 100; }
.skip:focus { left: 8px; }
:focus-visible { outline: 2px solid var(--acc); outline-offset: 3px; }

/* Pagine di testo: sempre chiare, anche in Notte */
.paper { background: var(--paper); color: var(--paper-ink); }
.prose { max-width: 68ch; }
.prose p + p { margin-top: 1em; }
.prose h2 { margin-block: 2em .6em; }
.prose a { color: var(--acc); }
```

- [ ] **Step 7: Scrivi la pagina segnaposto `src/pages/[lang]/index.astro`**

```astro
---
import { theme } from '../../lib/theme';
export function getStaticPaths() {
  return ['en', 'it', 'fr', 'de', 'es', 'nl'].map((lang) => ({ params: { lang } }));
}
const { lang } = Astro.params;
---
<html lang={lang} data-theme={theme}>
  <head><meta charset="utf-8" /><title>Pane di Triora</title></head>
  <body><h1>{lang} · {theme}</h1></body>
</html>
```

- [ ] **Step 8: Build in entrambi i temi**

Run: `pnpm build:notte && ls dist/it/index.html && pnpm build:farina && grep -o 'data-theme="[a-z]*"' dist/it/index.html`
Expected: build senza errori, output `data-theme="farina"`.

- [ ] **Step 9: Aggiorna `.gitignore` e committa**

Aggiungi a `.gitignore`: `.wrangler/`, `*.log`.

```bash
git add -A
git commit -m "Scaffold Astro 7 con i18n, tema via env e font self-hosted"
```

---

### Task 2: Dati azienda, locali e helper testo

**Files:**
- Create: `src/data/locales.ts`, `src/data/business.ts`, `src/lib/text.ts`, `tests/locales.test.ts`, `tests/text.test.ts`

**Interfaces:**
- Produces: `LOCALES`, `Lang`, `DEFAULT_LANG`, `LANG_NAMES`, `SECTIONS`, `Section`, `localePath(lang, section?, slug?)`, `isLang(x)`; `business` (dati); `emphasize(str)` che trasforma `|testo|` in `<em>testo</em>`.

- [ ] **Step 1: Test per `locales.ts`**

`tests/locales.test.ts`:

```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { localePath, isLang, LOCALES, SECTIONS } from '../src/data/locales.ts';

test('localePath compone i percorsi con slash finale', () => {
  assert.equal(localePath('it'), '/it/');
  assert.equal(localePath('it', 'products'), '/it/prodotti/');
  assert.equal(localePath('en', 'products', 'grissini'), '/en/products/grissini/');
  assert.equal(localePath('de', 'legal'), '/de/impressum/');
});

test('ogni sezione ha uno slug per ogni lingua', () => {
  for (const key of Object.keys(SECTIONS) as (keyof typeof SECTIONS)[]) {
    for (const lang of LOCALES) assert.match(SECTIONS[key][lang], /^[a-z0-9-]+$/);
  }
});

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

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

Run: `pnpm test`
Expected: FAIL, `Cannot find module '../src/data/locales.ts'`.

- [ ] **Step 3: Scrivi `src/data/locales.ts`**

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

export const LANG_NAMES: Record<Lang, string> = {
  en: 'English',
  it: 'Italiano',
  fr: 'Français',
  de: 'Deutsch',
  es: 'Español',
  nl: 'Nederlands',
};

export const SECTIONS = {
  products: { en: 'products', it: 'prodotti', fr: 'produits', de: 'produkte', es: 'productos', nl: 'producten' },
  bakery: { en: 'bakery', it: 'panificio', fr: 'boulangerie', de: 'baeckerei', es: 'panaderia', nl: 'bakkerij' },
  triora: { en: 'triora', it: 'triora', fr: 'triora', de: 'triora', es: 'triora', nl: 'triora' },
  press: { en: 'press', it: 'parlano-di-noi', fr: 'presse', de: 'presse', es: 'prensa', nl: 'pers' },
  contact: { en: 'contact', it: 'contatti', fr: 'contact', de: 'kontakt', es: 'contacto', nl: 'contact' },
  privacy: { en: 'privacy', it: 'privacy', fr: 'privacy', de: 'privacy', es: 'privacy', nl: 'privacy' },
  legal: { en: 'legal-notice', it: 'note-legali', fr: 'mentions-legales', de: 'impressum', es: 'aviso-legal', nl: 'juridische-kennisgeving' },
} as const satisfies Record<string, Record<Lang, string>>;
export type Section = keyof typeof SECTIONS;

export const LEGAL_LABELS: Record<Lang, { privacy: string; legal: string }> = {
  en: { privacy: 'Privacy', legal: 'Legal notice' },
  it: { privacy: 'Privacy', legal: 'Note legali' },
  fr: { privacy: 'Confidentialité', legal: 'Mentions légales' },
  de: { privacy: 'Datenschutz', legal: 'Impressum' },
  es: { privacy: 'Privacidad', legal: 'Aviso legal' },
  nl: { privacy: 'Privacy', legal: 'Juridische kennisgeving' },
};

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

export function localePath(lang: Lang, section?: Section, slug?: string): string {
  let p = `/${lang}/`;
  if (section) p += `${SECTIONS[section][lang]}/`;
  if (slug) p += `${slug}/`;
  return p;
}
```

- [ ] **Step 4: Scrivi `src/data/business.ts`**

```ts
export const business = {
  name: 'Pane di Triora',
  legalName: 'Asplanato Angiolino SAS di Asplanato Ornella e C.',
  vat: 'IT00875900086',
  address: { street: 'Corso Italia, 37', zip: '18010', city: 'Triora', province: 'IM', region: 'Liguria', country: 'IT' },
  phone: '+39 0184 94290',
  phoneHref: 'tel:+39018494290',
  mobile: '+39 347 7675695',
  mobileHref: 'tel:+393477675695',
  whatsappHref: 'https://wa.me/393477675695',
  email: '', // da chiedere al cliente; se vuoto il link non viene mostrato
  geo: { lat: 43.9931, lng: 7.7628 },
  altitude: 780,
  mapsHref: 'https://www.google.com/maps/search/?api=1&query=Corso+Italia+37+18010+Triora+IM',
  social: {
    instagram: 'https://www.instagram.com/paneditriora/',
    facebook: 'https://www.facebook.com/paneditriora/',
  },
  shopHref: 'https://www.nuovabottegaitalia.com/panificio-esplanato-triora.htm',
  nutritionPdf: '/docs/tabelle-nutrizionali.pdf',
  press: [
    { key: 'youtube', youtubeId: '3aytM70MS7k', href: 'https://www.youtube.com/watch?v=3aytM70MS7k' },
    { key: 'rai', href: 'http://www.rai.it/dl/RaiTV/programmi/media/ContentItem-b43855c9-c81c-4008-8160-7b97bf2ae39e-tg1.html' },
    { key: 'striscia', href: 'https://www.striscialanotizia.mediaset.it/video/il-pane-di-triora_47435.shtml' },
  ],
} as const;
```

- [ ] **Step 5: Test e implementazione di `emphasize`**

`tests/text.test.ts`:

```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { emphasize } from '../src/lib/text.ts';

test('emphasize', () => {
  assert.equal(emphasize("L'unico, |autentico| Pane."), "L'unico, <em>autentico</em> Pane.");
  assert.equal(emphasize('niente'), 'niente');
  assert.equal(emphasize('a <b> |c|'), 'a &lt;b&gt; <em>c</em>');
});
```

`src/lib/text.ts`:

```ts
const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

/** `|testo|` diventa `<em>testo</em>`; il resto viene escapato. */
export function emphasize(s: string): string {
  return s
    .split('|')
    .map((part, i) => (i % 2 ? `<em>${esc(part)}</em>` : esc(part)))
    .join('');
}
```

- [ ] **Step 6: Esegui i test**

Run: `pnpm test`
Expected: 4 test PASS.

- [ ] **Step 7: Commit**

```bash
git add src/data src/lib/text.ts tests
git commit -m "Dati azienda, locali e helper testo con test"
```

---

### Task 3: Content collections e contenuti italiani

**Files:**
- Create: `src/content.config.ts`, `src/lib/content.ts`, `src/content/products/it/*.md` (7), `src/content/pages/it/*.md` (6), `src/content/ui/it.json`

**Interfaces:**
- Produces: collezioni `products`, `pages`, `ui`; helper `getUi(lang)`, `getProducts(lang)`, `getPage(lang, key)`, `slugOf(entry)`. Tipo `Ui` = `CollectionEntry<'ui'>['data']`.

- [ ] **Step 1: Scrivi `src/content.config.ts`**

```ts
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const products = defineCollection({
  loader: glob({ base: './src/content/products', pattern: '**/*.md' }),
  schema: z.object({
    title: z.string(),
    tagline: z.string(),
    description: z.string().max(160),
    order: z.number().int(),
    photo: z.enum(['pane', 'cereali', 'grissini', 'orzo', 'bruschette', 'biscotti', 'biscottate']),
    ingredients: z.array(z.string()).min(1),
    allergens: z.array(z.string()).min(1),
    formats: z.array(z.string()).default([]),
  }),
});

const pages = defineCollection({
  loader: glob({ base: './src/content/pages', pattern: '**/*.md' }),
  schema: z.object({
    title: z.string(),
    description: z.string().max(160),
    kicker: z.string().optional(),
    heading: z.string(),
  }),
});

const faq = z.array(z.object({ q: z.string(), a: z.string() })).min(3);

const ui = defineCollection({
  loader: glob({ base: './src/content/ui', pattern: '*.json' }),
  schema: z.object({
    nav: z.object({ home: z.string(), products: z.string(), bakery: z.string(), triora: z.string(), press: z.string(), contact: z.string() }),
    meta: z.object({
      home: z.object({ title: z.string(), description: z.string().max(160) }),
      products: z.object({ title: z.string(), description: z.string().max(160) }),
    }),
    hero: z.object({
      kicker: z.string(),
      title: z.string(),        // Farina, con |em|
      overline: z.string(),     // Notte, riga oro sopra il titolo
      titleNotte: z.string(),   // Notte, 2 righe separate da \n
      side: z.string(),         // Notte, testo verticale
      text: z.string(),
      ctaProducts: z.string(),
      ctaWhere: z.string(),
      photoCaption: z.string(),
    }),
    home: z.object({
      productsKicker: z.string(),
      productsHeading: z.string(),
      markHeading: z.string(),  // con |em|
      markText: z.string(),
      storyKicker: z.string(),
      storyHeading: z.string(),
      storyText: z.string(),
      storyCta: z.string(),
      faqHeading: z.string(),
      contactHeading: z.string(),
      contactText: z.string(),
    }),
    faq,
    product: z.object({
      ingredients: z.string(), allergens: z.string(), formats: z.string(),
      where: z.string(), whereText: z.string(), shop: z.string(), nutrition: z.string(),
      all: z.string(), prev: z.string(), next: z.string(), breadcrumbHome: z.string(),
    }),
    contact: z.object({
      phone: z.string(), mobile: z.string(), whatsapp: z.string(), email: z.string(), address: z.string(),
      hours: z.string(), hoursText: z.string(), map: z.string(), social: z.string(), shop: z.string(),
    }),
    press: z.object({ youtube: z.string(), rai: z.string(), striscia: z.string(), watch: z.string() }),
    footer: z.object({ language: z.string(), funded: z.string(), vat: z.string() }),
    gallery: z.object({ open: z.string(), close: z.string(), prev: z.string(), next: z.string() }),
    a11y: z.object({ skip: z.string(), menu: z.string(), closeMenu: z.string() }),
    alts: z.record(z.string()),
  }),
});

export const collections = { products, pages, ui };
```

- [ ] **Step 2: Scrivi `src/lib/content.ts`**

```ts
import { getCollection, getEntry, type CollectionEntry } from 'astro:content';
import type { Lang } from '../data/locales';

export type Ui = CollectionEntry<'ui'>['data'];
export type Product = CollectionEntry<'products'>;
export type PageKey = 'bakery' | 'triora' | 'press' | 'contact' | 'privacy' | 'legal';
export const PAGE_KEYS: PageKey[] = ['bakery', 'triora', 'press', 'contact', 'privacy', 'legal'];

export async function getUi(lang: Lang): Promise<Ui> {
  const e = await getEntry('ui', lang);
  if (!e) throw new Error(`src/content/ui/${lang}.json mancante`);
  return e.data;
}

export async function getProducts(lang: Lang): Promise<Product[]> {
  const all = await getCollection('products', (e) => e.id.startsWith(`${lang}/`));
  return all.sort((a, b) => a.data.order - b.data.order);
}

export function slugOf(e: Product): string {
  return e.id.split('/')[1];
}

export async function getPage(lang: Lang, key: PageKey) {
  const e = await getEntry('pages', `${lang}/${key}`);
  if (!e) throw new Error(`src/content/pages/${lang}/${key}.md mancante`);
  return e;
}
```

- [ ] **Step 3: Scrivi i 7 prodotti italiani in `src/content/products/it/`**

`pane-di-triora.md`:

```md
---
title: Pane di Triora
tagline: Il "principe" del nostro panificio.
description: L'unico, autentico Pane di Triora, prodotto dal Panificio Asplanato da oltre sessant'anni con farina tipo 1 e acqua di montagna.
order: 1
photo: pane
ingredients: [acqua, farina di grano tenero tipo 1, lievito di birra, sale, crusca alimentare]
allergens: [grano]
formats: [forma da 1 kg, forma da 0,5 kg]
---
È il prodotto "principe" del nostro panificio, quello storico che ci ha reso famosi e che ha contribuito a rendere famosa Triora, oggi parte dell'Associazione Nazionale Città del Pane.

Il segreto della sua bontà sta nell'aria e nell'acqua del luogo che, unite agli ingredienti che utilizziamo (in particolare la farina di grano tenero tipo 1), rendono questo pane un alimento unico. È una pagnotta nata per durare giorni.

Si trova in forme da 1 kg e da 0,5 kg. Se venite a Triora nel tardo pomeriggio o in serata (eccetto il sabato), lo trovate caldo, appena sfornato.

Se non potete risalire la Valle Argentina, il Pane di Triora lo trovate nei principali centri della Liguria: scriveteci per conoscere il punto vendita più vicino. Ma ricordate: controllate che sia quello con il marchio.
```

`pane-multicereali.md`:

```md
---
title: Pane Multicereali
tagline: La novità del Panificio Asplanato.
description: Pagnotte scure da 400-450 g con farina multicereali altoatesina, grano saraceno in superficie e olio di oliva. Morbide e di lunga conservazione.
order: 2
photo: cereali
ingredients: ["farina multicereali (farina di grano tenero tipo 0, semi di girasole, sesamo, semi di lino, lievito madre di farina di grano tenero tipo 0 in polvere, farina di segale, farine maltate di orzo e grano tenero, farina di avena, farina di orzo, alfa amilasi, agente di trattamento della farina: acido ascorbico)", acqua, olio di oliva, lievito, sale, spolvero di crusca]
allergens: [grano, sesamo]
formats: [pagnotta da 400-450 g]
---
Nato quasi per caso dopo il lockdown del 2020, il pane multicereali è l'ultima creazione del Panificio Asplanato. Sono pagnotte da 400-450 grammi, molto scure, decorate in superficie con il grano saraceno.

Lo produciamo con farina multicereali di provenienza altoatesina, olio di oliva, sale e lievito di birra. Si distingue per la morbidezza e per la buona conservabilità.
```

`grissini.md`:

```md
---
title: Grissini
tagline: Ideali a tutte le ore.
description: Grissini prodotti con la stessa farina tipo 1 del Pane di Triora, in confezioni da 200 g. Perfetti per l'aperitivo.
order: 3
photo: grissini
ingredients: [acqua, farina di grano tenero tipo 1, olio di semi di mais, lievito di birra, sale, farina di grano tenero tipo 0, emulsionante E472e, destrosio]
allergens: [grano]
formats: [confezione da 200 g]
---
Un prodotto più recente, nato una decina d'anni fa per rispondere alle richieste dei clienti. Li facciamo con la stessa farina tipo 1 che ha reso celebre il nostro pane.

Vanno bene a tutte le ore, in particolare come aperitivo. Li trovate nel nostro panificio e in vari punti vendita del ponente ligure, in confezioni da 200 grammi.
```

`pane-d-orzo.md`:

```md
---
title: Pane d'Orzo
tagline: La carpasina.
description: Il pane d'orzo o carpasina, uno dei prodotti più antichi del ponente ligure. Duro, da bagnare e condire con pomodoro, aglio, basilico e olio.
order: 4
photo: orzo
ingredients: [farina d'orzo, acqua, sale]
allergens: [orzo (glutine)]
formats: [dischi]
---
Il Pane d'Orzo, conosciuto anche come Carpasina dal vicino paese di Carpasio, è uno dei prodotti storicamente più antichi del ponente ligure.

Simile alla frisella, ha la forma di un piccolo disco di pochi centimetri di diametro ed è molto duro. Era questa la caratteristica che ne garantiva la lunga conservazione, quando i pastori ne facevano scorta per la transumanza.

Si inumidisce con un filo d'acqua e si condisce a piacere: pomodoro, aglio, basilico e olio di oliva.
```

`bruschette.md`:

```md
---
title: Bruschette
tagline: Aromatizzate all'origano.
description: Bruschette già pronte all'origano, con olio extravergine ROI, da condire con olio e pomodoro. Ottime anche nella zuppa di pesce.
order: 5
photo: bruschette
ingredients: [farina di grano tenero tipo 1, acqua, olio extravergine di oliva ROI, olio di semi di mais, lievito di birra, sale, origano, farina di grano tenero tipo 00, emulsionante E472e, destrosio]
allergens: [grano]
formats: []
---
Nate nel 2015, si stanno ritagliando il loro spazio. È una bruschetta già pronta, aromatizzata all'origano, da condire con olio e pomodoro. Da provare anche nella zuppa di pesce.
```

`biscotti-di-triora.md`:

```md
---
title: Biscotti di Triora
tagline: Tipo Lagaccio, con anice.
description: Biscotti tipo Lagaccio con farina tipo 1, semi di anice e olio extravergine taggiasco dell'Oleificio Roi di Badalucco. Per la colazione.
order: 6
photo: biscotti
ingredients: [farina di grano tenero tipo 1, acqua, zucchero, olio extravergine di oliva, lievito di birra, semi di anice, farina di grano tenero tipo 0, emulsionante E472e, destrosio]
allergens: [grano]
formats: []
---
I biscotti sono di produzione recente ma richiamano sapori di una volta, come l'anice. All'inizio usavamo la farina 00; oggi anche i biscotti sono fatti con farina di grano tenero tipo 1, a cui aggiungiamo i semi di anice e l'olio extravergine di oliva taggiasca dell'Oleificio Roi di Badalucco.

Provateli a colazione.
```

`biscottate-di-triora.md`:

```md
---
title: Biscottate di Triora
tagline: Tipo Lagaccio, senza anice.
description: Le Biscottate di Triora, tipo Lagaccio senza semi di anice e con olio di mais. Per chi preferisce un gusto più neutro.
order: 7
photo: biscottate
ingredients: [farina di grano tenero tipo 1, acqua, zucchero, olio di semi di mais, olio extravergine di oliva, lievito di birra, farina di grano tenero tipo 0, emulsionante E472e, destrosio]
allergens: [grano]
formats: []
---
Per chi non ama il gusto dell'anice produciamo un biscotto molto simile ai Biscotti di Triora: le Biscottate sono preparate senza semi di anice e con l'aggiunta di olio di mais.
```

- [ ] **Step 4: Scrivi le 6 pagine italiane in `src/content/pages/it/`**

`bakery.md`:

```md
---
title: Il Panificio Asplanato
description: La storia del Panificio Asplanato Angiolino di Triora, unico produttore del Pane di Triora dal secondo dopoguerra.
kicker: Il panificio
heading: Dove tutto nasce
---
Nel secondo dopoguerra Angiolino, panettiere del paese, riprese antiche ricette tramandate a voce e decise di rimettere in produzione una pagnotta che potesse durare diversi giorni.

All'inizio bastava soddisfare il fabbisogno di un paese di montagna che negli anni Cinquanta contava ancora più di mille abitanti. Poi, grazie all'interessamento di alcuni negozianti di Sanremo, nacque una piccola rete di distribuzione: il pane scendeva a valle con l'autobus di linea.

Oggi gli ingredienti sono gli stessi di allora. Si continua a usare la farina di grano tenero tipo 1, che dà alla pagnotta il suo colore dorato, il lievito di birra, il sale, uno spolvero di crusca su cui il pane riposa prima della cottura e l'acqua che sgorga non lontano dal Paese delle Streghe.

## La famiglia

Il panificio è ancora una cosa di famiglia: Asplanato Angiolino SAS di Asplanato Ornella e C. Siamo l'unico produttore del Pane di Triora.
```

`triora.md`:

```md
---
title: Triora, il Paese delle Streghe
description: Triora, borgo medievale a 780 metri in Valle Argentina, entroterra di Sanremo. Città del Pane, paese delle streghe, casa del Pane di Triora.
kicker: Il territorio
heading: La nostra terra
---
Triora è un borgo medievale a 780 metri di altitudine, in cima alla Valle Argentina, nell'entroterra di Sanremo e Taggia. È conosciuta come il Paese delle Streghe per i processi del 1587-1589, e fa parte dell'Associazione Nazionale Città del Pane.

L'aria e l'acqua di qui sono il segreto del nostro pane. Vicoli in pietra, archi, il campanile che guarda la valle, i boschi di castagni e le montagne delle Alpi Liguri intorno.

## Come arrivare

Da Sanremo o Arma di Taggia si risale la Valle Argentina per la SP 548: Badalucco, Molini di Triora, poi Triora. Circa 35 km e 50 minuti d'auto dalla costa. Il panificio è in Corso Italia 37, nel centro del paese.
```

`press.md`:

```md
---
title: Parlano di noi
description: Servizi e video sul Pane di Triora e sul Panificio Asplanato: Rai TG1, Striscia la Notizia, YouTube.
kicker: Rassegna
heading: Parlano di noi
---
Alcuni filmati e servizi che hanno raccontato il Pane di Triora e il nostro panificio.
```

`contact.md`:

```md
---
title: Contatti
description: Panificio Asplanato, Corso Italia 37, Triora (IM). Telefono, WhatsApp, mappa e orari del pane caldo.
kicker: Contatti
heading: Venite a trovarci
---
Il pane caldo lo trovate nel tardo pomeriggio e in serata, tutti i giorni tranne il sabato. Per sapere qual è il punto vendita più vicino a voi, scriveteci.
```

`privacy.md`:

```md
---
title: Privacy
description: Informativa sulla privacy del sito paneditriora.it.
heading: Privacy
---
Questo sito non usa cookie di profilazione e non raccoglie dati personali. Non ci sono moduli, newsletter o account.

L'unico cookie tecnico, `lang`, viene salvato solo se scegliete manualmente una lingua dal selettore a fondo pagina, per ricordare la scelta. Dura un anno.

Le statistiche di visita sono raccolte con Cloudflare Web Analytics, che non usa cookie e non identifica i visitatori.

I video di YouTube vengono caricati solo se cliccate su "guarda il video", dal dominio youtube-nocookie.com. Da quel momento valgono le condizioni di Google.

Titolare del trattamento: Asplanato Angiolino SAS di Asplanato Ornella e C., Corso Italia 37, 18010 Triora (IM). Per qualsiasi richiesta potete scriverci o telefonarci ai recapiti della pagina Contatti.
```

`legal.md`:

```md
---
title: Note legali
description: Dati societari di Asplanato Angiolino SAS, titolare del sito paneditriora.it.
heading: Note legali
---
Asplanato Angiolino SAS di Asplanato Ornella e C.
Corso Italia 37, 18010 Triora (IM), Italia
Partita IVA 00875900086

Tutti i contenuti di questo sito, testi, fotografie e marchio, sono di proprietà di Asplanato Angiolino SAS. Ne è vietata la riproduzione senza autorizzazione.

"Pane di Triora" e il marchio con la spiga sono segni distintivi del Panificio Asplanato, unico produttore.
```

- [ ] **Step 5: Scrivi `src/content/ui/it.json`**

```json
{
  "nav": { "home": "Home", "products": "Prodotti", "bakery": "Il panificio", "triora": "Triora", "press": "Parlano di noi", "contact": "Contatti" },
  "meta": {
    "home": { "title": "Pane di Triora · Panificio Asplanato, l'unico produttore", "description": "L'unico, autentico Pane di Triora. Dal secondo dopoguerra il Panificio Asplanato lo fa con farina tipo 1 e acqua di montagna, nel Paese delle Streghe." },
    "products": { "title": "I prodotti · Pane di Triora", "description": "Pane di Triora, pane multicereali, grissini, pane d'orzo, bruschette, biscotti e biscottate. Ingredienti, allergeni e dove trovarli." }
  },
  "hero": {
    "kicker": "Panificio Asplanato · Triora, 780 m · da oltre sessant'anni",
    "title": "L'unico, |autentico| Pane di Triora.",
    "overline": "L'unico, dal dopoguerra.",
    "titleNotte": "Pane\ndi Triora",
    "side": "Valle Argentina · Paese delle Streghe",
    "text": "Farina di grano tenero tipo 1, acqua e aria di montagna, un forno che non si è mai fermato. Una pagnotta nata per durare giorni, riconoscibile dal marchio.",
    "ctaProducts": "I prodotti",
    "ctaWhere": "Dove trovarlo",
    "photoCaption": "Forma da 1 kg · Farina tipo 1"
  },
  "home": {
    "productsKicker": "Dal forno",
    "productsHeading": "Sette prodotti, una farina.",
    "markHeading": "Controllate che sia |quello con il marchio.|",
    "markText": "Siamo l'unico produttore del Pane di Triora. Lo trovate nei principali centri della Liguria e caldo a Triora nel tardo pomeriggio, tranne il sabato.",
    "storyKicker": "Il panificio",
    "storyHeading": "Il forno non si è mai spento.",
    "storyText": "Nel secondo dopoguerra Angiolino riprese le ricette del paese e rimise in produzione una pagnotta fatta per durare. Il pane scendeva a Sanremo con l'autobus di linea. Gli ingredienti sono ancora quelli.",
    "storyCta": "La nostra storia",
    "faqHeading": "Domande frequenti",
    "contactHeading": "Venite a trovarci",
    "contactText": "Corso Italia 37, Triora. Pane caldo nel tardo pomeriggio, tranne il sabato."
  },
  "faq": [
    { "q": "Dove si compra il Pane di Triora?", "a": "Nel nostro panificio in Corso Italia 37 a Triora, nei principali centri della Liguria e online tramite Nuova Bottega Italia. Scriveteci su WhatsApp per il punto vendita più vicino." },
    { "q": "Quanto dura il Pane di Triora?", "a": "È una pagnotta nata per durare: si conserva bene per diversi giorni, molto più di un pane comune. Tenetelo in un sacchetto di carta o di tela." },
    { "q": "Quali sono gli ingredienti?", "a": "Acqua, farina di grano tenero tipo 1, lievito di birra, sale e crusca. Contiene glutine." },
    { "q": "Quando si trova il pane caldo?", "a": "A Triora nel tardo pomeriggio e in serata, tutti i giorni tranne il sabato." },
    { "q": "Come riconosco quello originale?", "a": "Dal marchio con la spiga che si avvolge, la forma tonda della pagnotta. Siamo l'unico produttore del Pane di Triora." },
    { "q": "Che differenza c'è tra biscotti e biscottate?", "a": "Sono entrambi tipo Lagaccio. I Biscotti di Triora hanno i semi di anice e l'olio extravergine taggiasco; le Biscottate sono senza anice, con olio di mais." }
  ],
  "product": {
    "ingredients": "Ingredienti", "allergens": "Allergeni", "formats": "Formati",
    "where": "Dove trovarlo", "whereText": "Nel panificio a Triora, nei principali centri della Liguria e online. Scriveteci per il punto vendita più vicino.",
    "shop": "Compra online", "nutrition": "Valori nutrizionali (PDF)",
    "all": "Tutti i prodotti", "prev": "Precedente", "next": "Successivo", "breadcrumbHome": "Home"
  },
  "contact": {
    "phone": "Telefono", "mobile": "Cellulare", "whatsapp": "Scrivici su WhatsApp", "email": "Email", "address": "Indirizzo",
    "hours": "Pane caldo", "hoursText": "Tardo pomeriggio e sera, tutti i giorni tranne il sabato.",
    "map": "Apri in Google Maps", "social": "Seguici", "shop": "Shop online"
  },
  "press": { "youtube": "Pane di Triora, il video", "rai": "Rai TG1 · Il Paese delle Streghe", "striscia": "Striscia la Notizia · Il Pane di Triora", "watch": "Guarda il video" },
  "footer": { "language": "Lingua", "funded": "Progetto cofinanziato da", "vat": "P. IVA" },
  "gallery": { "open": "Ingrandisci", "close": "Chiudi", "prev": "Precedente", "next": "Successiva" },
  "a11y": { "skip": "Vai al contenuto", "menu": "Menu", "closeMenu": "Chiudi il menu" },
  "alts": {
    "pane": "Pagnotta di Pane di Triora su un canovaccio",
    "cereali": "Pane multicereali con grano saraceno",
    "grissini": "Grissini del Panificio Asplanato",
    "orzo": "Pane d'orzo, la carpasina",
    "bruschette": "Bruschette all'origano",
    "biscotti": "Biscotti di Triora con anice",
    "biscottate": "Biscottate di Triora",
    "pagnotta": "Pagnotta di Pane di Triora appena sfornata",
    "banco": "Il banco del panificio con pane e grissini",
    "lievitazione": "Pagnotte in lievitazione",
    "forno": "Pagnotte sul nastro del forno",
    "panificio01": "La famiglia Asplanato nel laboratorio",
    "panificio02": "Biscotti appena tagliati sul banco",
    "panificio03": "Biscotti di Triora in cottura",
    "affettatrice": "Pagnotte all'uscita del forno",
    "borgo": "Triora vista dall'alto con il campanile",
    "panorama": "Triora arroccata sulla Valle Argentina",
    "campanile": "Il campanile di Triora",
    "monti": "Le montagne intorno a Triora",
    "mulino": "Il vecchio mulino in bianco e nero",
    "bosco": "Sentiero nel bosco di Triora nella nebbia",
    "piazza": "La piazza di Triora con i portici",
    "insegna": "L'insegna del Paese delle Streghe",
    "persona02": "Ornella Asplanato",
    "persona03": "Una persona della famiglia Asplanato",
    "persona04": "Angiolino Asplanato con una pagnotta",
    "persona06": "Una persona della famiglia Asplanato"
  }
}
```

- [ ] **Step 6: Build**

Run: `pnpm astro sync && pnpm check`
Expected: 0 errori (la pagina segnaposto non usa ancora le collezioni, ma lo schema viene validato).

- [ ] **Step 7: Commit**

```bash
git add src/content.config.ts src/lib/content.ts src/content
git commit -m "Content collections e contenuti italiani"
```

---

### Task 4: Traduzioni EN, FR, DE, ES, NL e controllo coerenza

**Files:**
- Create: `src/content/products/{en,fr,de,es,nl}/*.md` (35), `src/content/pages/{en,fr,de,es,nl}/*.md` (30), `src/content/ui/{en,fr,de,es,nl}.json` (5), `scripts/check-content.mjs`

**Interfaces:**
- Consumes: i file IT del Task 3 come sorgente.
- Produces: contenuti completi in 6 lingue; `pnpm check:content` verde.

- [ ] **Step 1: Scrivi `scripts/check-content.mjs`**

```js
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';

const LANGS = ['en', 'it', 'fr', 'de', 'es', 'nl'];
const root = new URL('../src/content/', import.meta.url).pathname;
let errors = 0;
const fail = (m) => { console.error('✗', m); errors++; };

// stessi file per lingua
for (const coll of ['products', 'pages']) {
  const ref = readdirSync(join(root, coll, 'it')).sort();
  for (const lang of LANGS) {
    const files = readdirSync(join(root, coll, lang)).sort();
    if (JSON.stringify(files) !== JSON.stringify(ref)) fail(`${coll}/${lang}: file diversi da it (${files.length} vs ${ref.length})`);
    for (const f of files) {
      const fm = (s) => s.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? '';
      const keys = (s) => fm(s).split('\n').map((l) => l.match(/^(\w+):/)?.[1]).filter(Boolean).sort();
      const a = keys(readFileSync(join(root, coll, 'it', f), 'utf8'));
      const b = keys(readFileSync(join(root, coll, lang, f), 'utf8'));
      if (JSON.stringify(a) !== JSON.stringify(b)) fail(`${coll}/${lang}/${f}: chiavi frontmatter diverse`);
    }
  }
}

// stesse chiavi nei json ui
const flat = (o, p = '') => Object.entries(o).flatMap(([k, v]) =>
  v && typeof v === 'object' && !Array.isArray(v) ? flat(v, `${p}${k}.`) : [`${p}${k}`]);
const refUi = flat(JSON.parse(readFileSync(join(root, 'ui', 'it.json'), 'utf8'))).sort();
for (const lang of LANGS) {
  const ui = flat(JSON.parse(readFileSync(join(root, 'ui', `${lang}.json`), 'utf8'))).sort();
  const missing = refUi.filter((k) => !ui.includes(k));
  const extra = ui.filter((k) => !refUi.includes(k));
  if (missing.length) fail(`ui/${lang}.json: mancano ${missing.join(', ')}`);
  if (extra.length) fail(`ui/${lang}.json: chiavi in più ${extra.join(', ')}`);
}

if (errors) process.exit(1);
console.log('✓ contenuti coerenti in', LANGS.length, 'lingue');
```

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

Run: `pnpm check:content`
Expected: FAIL con `ENOENT` su `products/en`.

- [ ] **Step 3: Traduci tutti i file IT nelle 5 lingue**

Per ogni file in `products/it`, `pages/it` e `ui/it.json` crea l'equivalente in `en`, `fr`, `de`, `es`, `nl` con lo stesso nome. Regole:

- Stesse chiavi di frontmatter e stesse chiavi JSON, stesso ordine. Valori tradotti, `order`, `photo`, `heading` e `kicker` tradotti dove testo.
- Non tradurre i nomi propri dei prodotti (`Pane di Triora`, `Grissini`, `Biscotti di Triora`, `Biscottate di Triora`, `Carpasina`), i toponimi (`Triora`, `Valle Argentina`, `Sanremo`, `Badalucco`, `Carpasio`), `Oleificio Roi`, `Città del Pane` (aggiungi la traduzione tra parentesi la prima volta: "Città del Pane (Bread Towns association)").
- "Paese delle Streghe" si traduce: Village of the Witches / Village des Sorcières / Hexendorf / Pueblo de las Brujas / Heksendorp.
- Le `description` restano sotto 160 caratteri.
- In `hero.title` e `home.markHeading` conserva i delimitatori `|...|` attorno alla parola enfatizzata.
- In `hero.titleNotte` conserva `\n`.
- In `ui.alts` traduci tutte le voci.
- Registro: semplice, caldo, seconda persona plurale dove l'italiano usa il voi (in EN "you", in DE "Sie", in FR "vous", in ES "ustedes"/impersonale, in NL "u").
- Le liste `ingredients` e `allergens` vanno tradotte con i termini alimentari corretti (es. EN "soft wheat flour type 1", "brewer's yeast", "bran"; DE "Weichweizenmehl Typ 1", "Hefe", "Kleie"; FR "farine de blé tendre type 1"; ES "harina de trigo blando tipo 1"; NL "tarwebloem type 1").
- `legal.md`: `de` usa il titolo "Impressum".

- [ ] **Step 4: Esegui il controllo e la build**

Run: `pnpm check:content && pnpm astro sync && pnpm check`
Expected: `✓ contenuti coerenti in 6 lingue`, 0 errori.

- [ ] **Step 5: Commit**

```bash
git add scripts/check-content.mjs src/content
git commit -m "Traduzioni in 5 lingue e controllo coerenza contenuti"
```

---

### Task 5: Asset statici: foto, marchio, favicon, bandiere, PDF, loghi

**Files:**
- Create: `scripts/import-photos.sh`, `src/assets/photos/*.jpg` (27), `src/assets/photos/index.ts`, `src/assets/logo/pittogramma.svg`, `src/assets/logo/wordmark.png`, `public/favicon/*`, `public/favicon.ico`, `public/manifest.webmanifest`, `public/flags/*.svg`, `public/docs/tabelle-nutrizionali.pdf`, `public/partner/*.png`, `src/components/Picto.astro`

**Interfaces:**
- Produces: `photos` (record nome → `ImageMetadata`), `galleries.bakery`, `galleries.triora`, `portraits`; `<Picto class=... />` (SVG inline con `fill="currentColor"`).

- [ ] **Step 1: Scrivi `scripts/import-photos.sh`**

```bash
#!/usr/bin/env bash
# Copia le foto originali in src/assets/photos con nome parlante, max 2400 px, senza doppioni.
set -euo pipefail
cd "$(dirname "$0")/.."
SRC="_assets_originali/foto"
DST="src/assets/photos"
mkdir -p "$DST"
declare -A MAP=(
  [pane]=pane_si1pt1z3 [cereali]=paneaicereali_nn3fqapj [grissini]=grissini_londnqg1 [orzo]=friselle_xfuh0bb2
  [bruschette]=bruschette_dmgj5ef1 [biscotti]=biscottianice_3h3btueh [biscottate]=biscotti_o3vdfnsf
  [pagnotta]=dsc00408_uxwdgbro [banco]=dsc00494_qg5jyzdr [lievitazione]=lievitazione_yvbpcugw [forno]=forno_dhbps1lo
  [panificio01]=panificio01_c3vdyzll [panificio02]=panificio02_ehvf552t [panificio03]=panificio03_apjp2glo
  [affettatrice]=affettatrice_jmfh2fio
  [borgo]=paneditrioracontattimenu_hcljybku [panorama]=triorapaneditriora_elbgkxgk [campanile]=paneditrioracontatti_gawphmtm
  [monti]=triora-web-1-41_4kinffqc [mulino]=triora-web-1-141-1076x720_fw4pkhdr [bosco]=triora-web-1-51_0glhxitb
  [piazza]=dsc01485-scaled-e1579188374480_zw4nljkp [insegna]=img_20161029_132011_3prde4hr
  [persona02]=persona02_2gofmdpb [persona03]=persona03_urqnxnaf [persona04]=persona04_yfsd4aar [persona06]=persona06_50tbsqlc
)
for name in "${!MAP[@]}"; do
  magick "$SRC/${MAP[$name]}.jpg" -auto-orient -strip -resize '2400x2400>' -quality 86 "$DST/$name.jpg"
done
ls "$DST" | wc -l
```

`bash` di macOS è 3.2 e non supporta `declare -A`: esegui con `/opt/homebrew/bin/bash scripts/import-photos.sh` (oppure `brew install bash`). Aggiungi la nota in testa allo script.

- [ ] **Step 2: Esegui**

Run: `chmod +x scripts/import-photos.sh && /opt/homebrew/bin/bash scripts/import-photos.sh && du -sh src/assets/photos`
Expected: `27`, cartella sotto 20 MB.

- [ ] **Step 3: Scrivi `src/assets/photos/index.ts`**

```ts
import pane from './pane.jpg';
import cereali from './cereali.jpg';
import grissini from './grissini.jpg';
import orzo from './orzo.jpg';
import bruschette from './bruschette.jpg';
import biscotti from './biscotti.jpg';
import biscottate from './biscottate.jpg';
import pagnotta from './pagnotta.jpg';
import banco from './banco.jpg';
import lievitazione from './lievitazione.jpg';
import forno from './forno.jpg';
import panificio01 from './panificio01.jpg';
import panificio02 from './panificio02.jpg';
import panificio03 from './panificio03.jpg';
import affettatrice from './affettatrice.jpg';
import borgo from './borgo.jpg';
import panorama from './panorama.jpg';
import campanile from './campanile.jpg';
import monti from './monti.jpg';
import mulino from './mulino.jpg';
import bosco from './bosco.jpg';
import piazza from './piazza.jpg';
import insegna from './insegna.jpg';
import persona02 from './persona02.jpg';
import persona03 from './persona03.jpg';
import persona04 from './persona04.jpg';
import persona06 from './persona06.jpg';

export const photos = {
  pane, cereali, grissini, orzo, bruschette, biscotti, biscottate,
  pagnotta, banco, lievitazione, forno, panificio01, panificio02, panificio03, affettatrice,
  borgo, panorama, campanile, monti, mulino, bosco, piazza, insegna,
  persona02, persona03, persona04, persona06,
} as const;
export type PhotoKey = keyof typeof photos;

export const galleries = {
  bakery: ['panificio01', 'panificio02', 'panificio03', 'lievitazione', 'forno', 'affettatrice', 'banco'],
  triora: ['borgo', 'panorama', 'monti', 'mulino', 'bosco', 'piazza', 'insegna'],
} as const satisfies Record<string, readonly PhotoKey[]>;

export const portraits = ['persona04', 'persona02', 'persona03', 'persona06'] as const satisfies readonly PhotoKey[];
```

- [ ] **Step 4: Marchio, wordmark, favicon, PDF, loghi partner**

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/Pane di Triora"
mkdir -p src/assets/logo public/favicon public/docs public/partner public/flags
# pittogramma: il path vettoriale, fill currentColor
python3 - <<'EOF'
import re
s=open('_assets_originali/logo/LogoPaneDiTriora.svg').read()
d=re.search(r'<path[^>]*d="([^"]+)"',s).group(1)
open('src/assets/logo/pittogramma.svg','w').write(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 131 113" fill="currentColor" fill-rule="evenodd"><path d="{d}"/></svg>')
EOF
cp _assets_originali/logo/PaneDiTriora.png src/assets/logo/wordmark.png
cp _assets_originali/favicon/*.png public/favicon/
cp _assets_originali/favicon/favicon.ico public/favicon.ico
cp _assets_originali/documenti/tabellenutrizionalipanificioasplanato.pdf public/docs/tabelle-nutrizionali.pdf
cp _assets_originali/loghi_partner/Eur.png public/partner/eu.png
cp _assets_originali/loghi_partner/Fesr.png public/partner/fesr.png
cp _assets_originali/loghi_partner/Ita.png public/partner/ita.png
cp _assets_originali/loghi_partner/Lig.png public/partner/lig.png
```

- [ ] **Step 5: Scrivi `public/manifest.webmanifest`**

```json
{
  "name": "Pane di Triora",
  "short_name": "Pane di Triora",
  "icons": [
    { "src": "/favicon/android-icon-192x192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/favicon/apple-icon-180x180.png", "sizes": "180x180", "type": "image/png" }
  ],
  "theme_color": "#340005",
  "background_color": "#F4ECDC",
  "display": "browser"
}
```

- [ ] **Step 6: Scrivi le 6 bandiere in `public/flags/`**

`it.svg`:
```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><path fill="#009246" d="M0 0h1v2H0z"/><path fill="#fff" d="M1 0h1v2H1z"/><path fill="#ce2b37" d="M2 0h1v2H2z"/></svg>
```
`fr.svg`:
```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><path fill="#0055a4" d="M0 0h1v2H0z"/><path fill="#fff" d="M1 0h1v2H1z"/><path fill="#ef4135" d="M2 0h1v2H2z"/></svg>
```
`de.svg`:
```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 3"><path d="M0 0h5v3H0z"/><path fill="#d00" d="M0 1h5v2H0z"/><path fill="#ffce00" d="M0 2h5v1H0z"/></svg>
```
`es.svg`:
```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3 2"><path fill="#aa151b" d="M0 0h3v2H0z"/><path fill="#f1bf00" d="M0 .5h3v1H0z"/></svg>
```
`nl.svg`:
```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 9 6"><path fill="#21468b" d="M0 0h9v6H0z"/><path fill="#fff" d="M0 0h9v4H0z"/><path fill="#ae1c28" d="M0 0h9v2H0z"/></svg>
```
`en.svg` (Union Jack semplificata):
```svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30"><clipPath id="c"><path d="M0 0v30h60V0z"/></clipPath><path fill="#012169" d="M0 0v30h60V0z"/><path stroke="#fff" stroke-width="6" d="m0 0 60 30m0-30L0 30" clip-path="url(#c)"/><path stroke="#C8102E" stroke-width="4" d="m0 0 60 30m0-30L0 30" clip-path="url(#c)"/><path stroke="#fff" stroke-width="10" d="M30 0v30M0 15h60"/><path stroke="#C8102E" stroke-width="6" d="M30 0v30M0 15h60"/></svg>
```

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

```astro
---
import svg from '../assets/logo/pittogramma.svg?raw';
interface Props { class?: string; title?: string }
const { class: cls, title } = Astro.props;
const html = title
  ? svg.replace('<svg ', `<svg role="img" aria-label="${title}" `)
  : svg.replace('<svg ', '<svg aria-hidden="true" ');
---
<span class:list={['picto', cls]} set:html={html}></span>
<style>
  .picto { display: inline-block; line-height: 0; }
  .picto :global(svg) { width: 100%; height: auto; }
</style>
```

- [ ] **Step 8: Build e commit**

Run: `pnpm check && pnpm build`
Expected: 0 errori.

```bash
git add scripts/import-photos.sh src/assets public src/components/Picto.astro
git commit -m "Foto ottimizzate, marchio vettoriale, favicon, bandiere, PDF"
```

---

### Task 6: Video hero

**Files:**
- Create: `scripts/make-video.sh`, `public/video/hero.mp4`, `public/video/hero.webm`, `public/video/hero-poster.jpg`

- [ ] **Step 1: Scrivi `scripts/make-video.sh`**

```bash
#!/usr/bin/env bash
# Dal video YouTube originale (1280x720, 39 s) ricava un loop di 14 s senza audio, sotto 1,5 MB.
set -euo pipefail
cd "$(dirname "$0")/.."
SRC="_assets_originali/video/hero_background_39s_H81_c2oSi0E.mp4"
OUT="public/video"
mkdir -p "$OUT"
START=2
LEN=14
ffmpeg -y -ss $START -t $LEN -i "$SRC" -an -vf "scale=1280:-2,fps=24" \
  -c:v libx264 -profile:v high -crf 28 -preset slow -pix_fmt yuv420p -movflags +faststart "$OUT/hero.mp4"
if ffmpeg -hide_banner -encoders 2>/dev/null | grep -q libsvtav1; then
  ffmpeg -y -ss $START -t $LEN -i "$SRC" -an -vf "scale=1280:-2,fps=24" -c:v libsvtav1 -crf 42 -preset 6 "$OUT/hero.webm"
else
  ffmpeg -y -ss $START -t $LEN -i "$SRC" -an -vf "scale=1280:-2,fps=24" -c:v libvpx-vp9 -crf 38 -b:v 0 -row-mt 1 "$OUT/hero.webm"
fi
ffmpeg -y -ss $START -i "$SRC" -frames:v 1 -vf "scale=1280:-2" -q:v 4 "$OUT/hero-poster.jpg"
ls -la "$OUT"
for f in "$OUT"/hero.mp4 "$OUT"/hero.webm; do
  [ "$(stat -f%z "$f")" -le 1572864 ] || { echo "✗ $f supera 1,5 MB"; exit 1; }
done
echo "✓ video ok"
```

- [ ] **Step 2: Esegui**

Run: `chmod +x scripts/make-video.sh && scripts/make-video.sh`
Expected: `✓ video ok`, tre file in `public/video/`. Se un file supera 1,5 MB alza `-crf` di 3 e riesegui.

- [ ] **Step 3: Verifica il loop a occhio**

Run: `open public/video/hero.mp4`
Expected: 14 s di forno con pane che scorre, senza stacchi bruschi. Se il punto di partenza è brutto cambia `START`.

- [ ] **Step 4: Commit**

```bash
git add scripts/make-video.sh public/video
git commit -m "Video hero self-hosted, 14 s, mp4 e webm"
```

---

### Task 7: Layout base, SEO head, testata, footer, selettore lingua

**Files:**
- Create: `src/lib/seo.ts`, `src/components/JsonLd.astro`, `src/components/themed/index.ts`, `src/components/Header.notte.astro`, `src/components/Header.farina.astro`, `src/components/Footer.astro`, `src/components/LangSwitcher.astro`, `src/layouts/Base.astro`
- Modify: `src/pages/[lang]/index.astro` (usa Base, contenuto minimo; completato nel Task 8)

**Interfaces:**
- Produces: `<Base lang title description alternates jsonLd? ogImage? transparentHeader?>`; `alternates: Record<Lang,string>` (percorsi relativi, uno per lingua); `Header`/`Footer` da `themed`; `seo.ts` con `bakeryLd(lang, ui)`, `productLd(...)`, `breadcrumbLd(...)`, `faqLd(...)`, `itemListLd(...)`, `absUrl(path)`.

- [ ] **Step 1: Scrivi `src/lib/seo.ts`**

```ts
import { business } from '../data/business';
import type { Lang } from '../data/locales';
import type { Ui, Product } from './content';

export const SITE = 'https://www.paneditriora.it';
export const absUrl = (path: string) => new URL(path, SITE).href;

export function bakeryLd(lang: Lang, ui: Ui) {
  return {
    '@context': 'https://schema.org',
    '@type': 'Bakery',
    '@id': `${SITE}/#bakery`,
    name: business.name,
    legalName: business.legalName,
    url: SITE,
    telephone: business.phone,
    image: absUrl('/favicon/android-icon-192x192.png'),
    description: ui.meta.home.description,
    address: {
      '@type': 'PostalAddress',
      streetAddress: business.address.street,
      postalCode: business.address.zip,
      addressLocality: business.address.city,
      addressRegion: business.address.region,
      addressCountry: business.address.country,
    },
    geo: { '@type': 'GeoCoordinates', latitude: business.geo.lat, longitude: business.geo.lng },
    sameAs: [business.social.instagram, business.social.facebook],
    servesCuisine: 'Italian',
    priceRange: '€',
    inLanguage: lang,
  };
}

export function productLd(p: Product, url: string, imageUrl: string) {
  return {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: p.data.title,
    description: p.data.description,
    image: imageUrl,
    url,
    brand: { '@type': 'Brand', name: business.name },
    manufacturer: { '@id': `${SITE}/#bakery` },
    additionalProperty: [
      { '@type': 'PropertyValue', name: 'ingredients', value: p.data.ingredients.join(', ') },
      { '@type': 'PropertyValue', name: 'allergens', value: p.data.allergens.join(', ') },
    ],
  };
}

export function breadcrumbLd(items: { name: string; url: string }[]) {
  return {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: items.map((it, i) => ({ '@type': 'ListItem', position: i + 1, name: it.name, item: it.url })),
  };
}

export function faqLd(faq: { q: string; a: string }[]) {
  return {
    '@context': 'https://schema.org',
    '@type': 'FAQPage',
    mainEntity: faq.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })),
  };
}

export function itemListLd(items: { name: string; url: string }[]) {
  return {
    '@context': 'https://schema.org',
    '@type': 'ItemList',
    itemListElement: items.map((it, i) => ({ '@type': 'ListItem', position: i + 1, name: it.name, url: it.url })),
  };
}
```

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

```astro
---
interface Props { data: object[] }
const { data } = Astro.props;
---
{data.map((d) => <script type="application/ld+json" set:html={JSON.stringify(d)}></script>)}
```

- [ ] **Step 3: Scrivi `src/components/themed/index.ts`**

```ts
import { isNotte } from '../../lib/theme';
import HeaderNotte from '../Header.notte.astro';
import HeaderFarina from '../Header.farina.astro';

export const Header = isNotte ? HeaderNotte : HeaderFarina;
```

(HeroHome e ProductGrid si aggiungono nel Task 8.)

- [ ] **Step 4: Scrivi `src/components/Header.farina.astro`**

```astro
---
import Picto from './Picto.astro';
import { localePath, type Lang } from '../data/locales';
import type { Ui } from '../lib/content';
interface Props { lang: Lang; ui: Ui; transparent?: boolean }
const { lang, ui } = Astro.props;
const links = [
  { href: localePath(lang, 'products'), label: ui.nav.products },
  { href: localePath(lang, 'bakery'), label: ui.nav.bakery },
  { href: localePath(lang, 'triora'), label: ui.nav.triora },
  { href: localePath(lang, 'press'), label: ui.nav.press },
  { href: localePath(lang, 'contact'), label: ui.nav.contact },
];
const current = Astro.url.pathname;
---
<header class="hd">
  <div class="wrap hd__in">
    <a class="hd__brand" href={localePath(lang)}><Picto class="hd__picto" /><span>Pane di Triora</span></a>
    <details class="hd__menu">
      <summary aria-label={ui.a11y.menu}><span class="hd__burger"></span></summary>
      <nav aria-label={ui.a11y.menu}>
        <ul role="list">
          {links.map((l) => <li><a href={l.href} aria-current={current.startsWith(l.href) ? 'page' : undefined}>{l.label}</a></li>)}
        </ul>
      </nav>
    </details>
  </div>
</header>
<style>
  .hd { border-bottom: 1px solid var(--line); background: var(--bg); position: relative; z-index: 10; }
  .hd__in { display: flex; align-items: center; justify-content: space-between; padding-block: 14px; gap: 16px; }
  .hd__brand { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--bord); font-family: var(--font-display); font-size: 1.25rem; }
  .hd__picto { width: 42px; color: var(--bord); }
  .hd__menu summary { list-style: none; display: none; width: 44px; height: 44px; align-items: center; justify-content: center; cursor: pointer; border-radius: var(--radius); }
  .hd__menu summary::-webkit-details-marker { display: none; }
  .hd__burger, .hd__burger::before, .hd__burger::after { display: block; width: 22px; height: 2px; background: var(--bord); position: relative; content: ""; }
  .hd__burger::before { position: absolute; top: -7px; }
  .hd__burger::after { position: absolute; top: 7px; }
  nav ul { display: flex; gap: 26px; }
  nav a { text-decoration: none; font-size: .9rem; letter-spacing: .03em; padding-block: 6px; border-bottom: 1.5px solid transparent; }
  nav a:hover, nav a[aria-current="page"] { border-bottom-color: var(--acc); }
  @media (max-width: 800px) {
    .hd__menu summary { display: flex; }
    nav { position: absolute; left: 0; right: 0; top: 100%; background: var(--bg); border-bottom: 1px solid var(--line); }
    nav ul { flex-direction: column; gap: 0; padding: 8px var(--gutter) 16px; }
    nav a { display: block; padding: 12px 0; font-size: 1.1rem; border-bottom: 1px solid var(--line); }
  }
</style>
```

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

```astro
---
import Picto from './Picto.astro';
import { Image } from 'astro:assets';
import wordmark from '../assets/logo/wordmark.png';
import { localePath, type Lang } from '../data/locales';
import type { Ui } from '../lib/content';
interface Props { lang: Lang; ui: Ui; transparent?: boolean }
const { lang, ui, transparent = false } = Astro.props;
const links = [
  { href: localePath(lang, 'products'), label: ui.nav.products },
  { href: localePath(lang, 'bakery'), label: ui.nav.bakery },
  { href: localePath(lang, 'triora'), label: ui.nav.triora },
  { href: localePath(lang, 'press'), label: ui.nav.press },
  { href: localePath(lang, 'contact'), label: ui.nav.contact },
];
const current = Astro.url.pathname;
---
<header class:list={['hd', { 'hd--over': transparent }]}>
  <div class="wrap hd__in">
    <a class="hd__brand" href={localePath(lang)} aria-label="Pane di Triora">
      <Picto class="hd__picto" />
      <Image src={wordmark} alt="" width={140} height={30} loading="eager" class="hd__word" />
    </a>
    <details class="hd__menu">
      <summary aria-label={ui.a11y.menu}><span class="hd__burger"></span></summary>
      <nav aria-label={ui.a11y.menu}>
        <ul role="list">
          {links.map((l) => <li><a href={l.href} aria-current={current.startsWith(l.href) ? 'page' : undefined}>{l.label}</a></li>)}
        </ul>
      </nav>
    </details>
  </div>
</header>
<style>
  .hd { background: var(--bg); position: relative; z-index: 10; }
  .hd--over { position: absolute; inset: 0 0 auto 0; background: transparent; }
  .hd__in { display: flex; align-items: center; justify-content: space-between; padding-block: 18px; gap: 16px; }
  .hd__brand { display: flex; align-items: center; gap: 12px; text-decoration: none; }
  .hd__picto { width: 46px; color: var(--acc); }
  .hd__word { height: 22px; width: auto; }
  .hd__menu summary { list-style: none; display: none; width: 44px; height: 44px; align-items: center; justify-content: center; cursor: pointer; }
  .hd__menu summary::-webkit-details-marker { display: none; }
  .hd__burger, .hd__burger::before, .hd__burger::after { display: block; width: 22px; height: 2px; background: var(--ink); position: relative; content: ""; }
  .hd__burger::before { position: absolute; top: -7px; }
  .hd__burger::after { position: absolute; top: 7px; }
  nav ul { display: flex; gap: 28px; }
  nav a { text-decoration: none; font-size: .75rem; letter-spacing: .16em; text-transform: uppercase; opacity: .9; padding-block: 6px; border-bottom: 1.5px solid transparent; }
  nav a:hover, nav a[aria-current="page"] { border-bottom-color: var(--acc); opacity: 1; }
  @media (max-width: 800px) {
    .hd__menu summary { display: flex; }
    nav { position: absolute; left: 0; right: 0; top: 100%; background: var(--bg); border-bottom: 1px solid var(--line); }
    nav ul { flex-direction: column; gap: 0; padding: 8px var(--gutter) 16px; }
    nav a { display: block; padding: 14px 0; font-size: 1rem; border-bottom: 1px solid var(--line); }
  }
</style>
```

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

```astro
---
import { LOCALES, LANG_NAMES, type Lang } from '../data/locales';
interface Props { lang: Lang; alternates: Record<Lang, string>; label: string }
const { lang, alternates, label } = Astro.props;
---
<details class="ls">
  <summary><img src={`/flags/${lang}.svg`} alt="" width="20" height="14" /><span>{LANG_NAMES[lang]}</span><span class="ls__caret" aria-hidden="true">▾</span></summary>
  <ul role="list" aria-label={label}>
    {LOCALES.filter((l) => l !== lang).map((l) => (
      <li><a href={alternates[l]} hreflang={l} lang={l} data-lang={l}><img src={`/flags/${l}.svg`} alt="" width="20" height="14" />{LANG_NAMES[l]}</a></li>
    ))}
  </ul>
</details>
<script>
  document.querySelectorAll<HTMLAnchorElement>('.ls a[data-lang]').forEach((a) => {
    a.addEventListener('click', () => {
      document.cookie = `lang=${a.dataset.lang}; Max-Age=31536000; Path=/; SameSite=Lax`;
    });
  });
</script>
<style>
  .ls { position: relative; font-size: .9rem; }
  .ls summary { list-style: none; display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; border: 1px solid var(--line); border-radius: var(--pill); cursor: pointer; }
  .ls summary::-webkit-details-marker { display: none; }
  .ls img { border-radius: 2px; }
  .ls__caret { opacity: .6; }
  .ls ul { position: absolute; bottom: calc(100% + 6px); left: 0; min-width: 180px; background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius); padding: 6px; box-shadow: 0 12px 30px -12px rgba(0,0,0,.4); }
  .ls a { display: flex; align-items: center; gap: 10px; padding: 8px 10px; text-decoration: none; border-radius: 6px; }
  .ls a:hover { background: var(--bg-2); }
</style>
```

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

```astro
---
import LangSwitcher from './LangSwitcher.astro';
import Picto from './Picto.astro';
import { business } from '../data/business';
import { localePath, LEGAL_LABELS, type Lang } from '../data/locales';
import type { Ui } from '../lib/content';
interface Props { lang: Lang; ui: Ui; alternates: Record<Lang, string> }
const { lang, ui, alternates } = Astro.props;
const a = business.address;
---
<footer class="ft">
  <div class="wrap ft__grid">
    <div class="ft__brand">
      <Picto class="ft__picto" />
      <p><strong>{business.legalName}</strong><br />{a.street}, {a.zip} {a.city} ({a.province})<br />{ui.footer.vat} {business.vat.replace('IT', '')}</p>
    </div>
    <div class="ft__contact">
      <p><a href={business.phoneHref}>{business.phone}</a><br /><a href={business.mobileHref}>{business.mobile}</a></p>
      <p><a href={business.whatsappHref} rel="noopener">WhatsApp</a> · <a href={business.social.instagram} rel="noopener">Instagram</a> · <a href={business.social.facebook} rel="noopener">Facebook</a></p>
      <p><a href={business.shopHref} rel="noopener">{ui.contact.shop}</a></p>
    </div>
    <div class="ft__legal">
      <p><a href={localePath(lang, 'privacy')}>{LEGAL_LABELS[lang].privacy}</a> · <a href={localePath(lang, 'legal')}>{LEGAL_LABELS[lang].legal}</a></p>
      <LangSwitcher lang={lang} alternates={alternates} label={ui.footer.language} />
    </div>
    <div class="ft__partner">
      <small>{ui.footer.funded}</small>
      <div><img src="/partner/eu.png" alt="Unione Europea" height="34" loading="lazy" /><img src="/partner/ita.png" alt="Repubblica Italiana" height="34" loading="lazy" /><img src="/partner/lig.png" alt="Regione Liguria" height="34" loading="lazy" /><img src="/partner/fesr.png" alt="FESR" height="34" loading="lazy" /></div>
    </div>
  </div>
</footer>
<style>
  .ft { border-top: 1px solid var(--line); padding-block: 40px 32px; font-size: .9rem; color: var(--ink-2); }
  .ft__grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 32px; }
  .ft__picto { width: 56px; color: var(--acc); margin-bottom: 12px; }
  .ft p + p { margin-top: .6em; }
  .ft a { text-decoration: none; border-bottom: 1px solid var(--line); }
  .ft a:hover { border-bottom-color: var(--acc); }
  .ft__legal { display: flex; flex-direction: column; gap: 16px; align-items: flex-start; }
  .ft__partner { grid-column: 1 / -1; display: flex; align-items: center; gap: 18px; flex-wrap: wrap; padding-top: 24px; border-top: 1px solid var(--line); }
  .ft__partner div { display: flex; gap: 14px; align-items: center; }
  .ft__partner img { height: 34px; width: auto; opacity: .85; }
  @media (max-width: 800px) { .ft__grid { grid-template-columns: 1fr; gap: 24px; } }
</style>
```

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

```astro
---
import '../styles/global.css';
import '@fontsource-variable/fraunces/opsz.css';
import '@fontsource-variable/fraunces/opsz-italic.css';
import '@fontsource-variable/instrument-sans/wght.css';
import '@fontsource-variable/bricolage-grotesque/opsz.css';
import '@fontsource-variable/dm-sans/wght.css';
import { getImage } from 'astro:assets';
import { theme, noindex, cfBeacon } from '../lib/theme';
import { LOCALES, DEFAULT_LANG, type Lang } from '../data/locales';
import { getUi } from '../lib/content';
import { absUrl } from '../lib/seo';
import { Header } from '../components/themed';
import Footer from '../components/Footer.astro';
import JsonLd from '../components/JsonLd.astro';
import { photos } from '../assets/photos';

interface Props {
  lang: Lang;
  title: string;
  description: string;
  alternates: Record<Lang, string>;
  jsonLd?: object[];
  ogImage?: ImageMetadata;
  transparentHeader?: boolean;
  bodyClass?: string;
}
const { lang, title, description, alternates, jsonLd = [], ogImage = photos.pane, transparentHeader = false, bodyClass } = Astro.props;
const ui = await getUi(lang);
const og = await getImage({ src: ogImage, width: 1200, height: 630, format: 'jpeg' });
const canonical = absUrl(alternates[lang]);
---
<!doctype html>
<html lang={lang} data-theme={theme}>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{title}</title>
    <meta name="description" content={description} />
    {noindex && <meta name="robots" content="noindex, nofollow" />}
    <link rel="canonical" href={canonical} />
    {LOCALES.map((l) => <link rel="alternate" hreflang={l} href={absUrl(alternates[l])} />)}
    <link rel="alternate" hreflang="x-default" href={absUrl(alternates[DEFAULT_LANG])} />
    <meta property="og:type" content="website" />
    <meta property="og:site_name" content="Pane di Triora" />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:url" content={canonical} />
    <meta property="og:image" content={absUrl(og.src)} />
    <meta property="og:locale" content={lang} />
    <meta name="twitter:card" content="summary_large_image" />
    <link rel="icon" href="/favicon.ico" sizes="any" />
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
    <link rel="apple-touch-icon" href="/favicon/apple-icon-180x180.png" />
    <link rel="manifest" href="/manifest.webmanifest" />
    <meta name="theme-color" content={theme === 'notte' ? '#140C0A' : '#F4ECDC'} />
    <link rel="sitemap" href="/sitemap-index.xml" />
    <JsonLd data={jsonLd} />
  </head>
  <body class={bodyClass}>
    <a class="skip" href="#main">{ui.a11y.skip}</a>
    <Header lang={lang} ui={ui} transparent={transparentHeader} />
    <main id="main"><slot /></main>
    <Footer lang={lang} ui={ui} alternates={alternates} />
    {cfBeacon && <script defer src="https://static.cloudflareinsights.com/beacon.min.js" data-cf-beacon={`{"token": "${cfBeacon}"}`}></script>}
  </body>
</html>
```

- [ ] **Step 9: Aggiorna `src/pages/[lang]/index.astro` per usare Base**

```astro
---
import Base from '../../layouts/Base.astro';
import { LOCALES, localePath, type Lang } from '../../data/locales';
import { getUi } from '../../lib/content';
export function getStaticPaths() {
  return LOCALES.map((lang) => ({ params: { lang } }));
}
const lang = Astro.params.lang as Lang;
const ui = await getUi(lang);
const alternates = Object.fromEntries(LOCALES.map((l) => [l, localePath(l)])) as Record<Lang, string>;
---
<Base lang={lang} title={ui.meta.home.title} description={ui.meta.home.description} alternates={alternates}>
  <section class="wrap section"><h1 set:html={ui.hero.title.replace(/\|/g, '')}></h1></section>
</Base>
```

- [ ] **Step 10: Build entrambi i temi e controlla l'head**

Run: `pnpm build:farina && grep -c 'hreflang' dist/it/index.html && grep -o '<link rel="canonical"[^>]*>' dist/de/index.html && pnpm build:notte && grep -o 'wordmark[^"]*' dist/en/index.html | head -1`
Expected: `7`, canonical `https://www.paneditriora.it/de/`, un file `wordmark.*.png` nel tema Notte.

- [ ] **Step 11: Apri in Chrome e guarda testata e footer**

Run: `pnpm dev` (Farina) e in un altro terminale `pnpm dev:notte --port 4322`. Apri `http://localhost:4321/it/` e `http://localhost:4322/it/` in tab normali di Chrome, anche a 375 px. Il menu mobile si apre con il pulsante, la tendina lingua nel footer mostra bandiera + nome e cambia lingua.
Expected: nessun errore in console, cookie `lang` impostato dopo un cambio lingua (`document.cookie` in console).

- [ ] **Step 12: Commit**

```bash
git add src
git commit -m "Layout base con SEO head, testata per tema, footer e selettore lingua"
```

---

### Task 8: Home page completa (hero e griglia per tema, marchio, storia, FAQ, contatti)

**Files:**
- Create: `src/components/HeroHome.notte.astro`, `src/components/HeroHome.farina.astro`, `src/components/ProductGrid.notte.astro`, `src/components/ProductGrid.farina.astro`, `src/components/MarchioBand.astro`, `src/components/Faq.astro`
- Modify: `src/components/themed/index.ts`, `src/pages/[lang]/index.astro`

**Interfaces:**
- Consumes: `getProducts`, `photos`, `emphasize`, `bakeryLd`, `faqLd`, `itemListLd`, `localePath`.
- Produces: `HeroHome`, `ProductGrid` da `themed`; `<MarchioBand ui lang />`; `<Faq items heading />`.

- [ ] **Step 1: Scrivi `src/components/HeroHome.farina.astro`**

```astro
---
import { Picture } from 'astro:assets';
import Picto from './Picto.astro';
import { photos } from '../assets/photos';
import { localePath, type Lang } from '../data/locales';
import { emphasize } from '../lib/text';
import type { Ui } from '../lib/content';
interface Props { lang: Lang; ui: Ui }
const { lang, ui } = Astro.props;
---
<section class="hero">
  <Picto class="hero__wm" />
  <div class="wrap hero__grid">
    <div class="hero__txt">
      <p class="kicker">{ui.hero.kicker}</p>
      <h1 set:html={emphasize(ui.hero.title)}></h1>
      <p class="hero__lead">{ui.hero.text}</p>
      <div class="hero__cta">
        <a class="btn btn--fill" href={localePath(lang, 'products')}>{ui.hero.ctaProducts}</a>
        <a class="btn" href={localePath(lang, 'contact')}>{ui.hero.ctaWhere}</a>
      </div>
    </div>
    <div class="hero__arch">
      <Picture src={photos.pagnotta} alt={ui.alts.pagnotta} widths={[480, 720, 960]} sizes="(min-width: 900px) 430px, 90vw" formats={['avif', 'webp']} loading="eager" fetchpriority="high" width={430} height={573} />
      <div class="hero__seal"><Picto /><small>il marchio</small></div>
    </div>
  </div>
</section>
<style>
  .hero { position: relative; overflow: hidden; padding-block: clamp(32px, 6vw, 64px) clamp(40px, 6vw, 72px); }
  .hero__wm { position: absolute; left: -90px; bottom: -120px; width: 560px; color: var(--bord); opacity: .05; pointer-events: none; }
  .hero__grid { display: grid; grid-template-columns: 1fr 430px; gap: 52px; align-items: center; position: relative; }
  .hero__txt .kicker { margin-bottom: 22px; }
  .hero__txt h1 { color: var(--bord); margin-bottom: 24px; }
  .hero__lead { font-size: 1.05rem; line-height: 1.55; max-width: 44ch; color: var(--ink-2); margin-bottom: 30px; }
  .hero__cta { display: flex; gap: 12px; flex-wrap: wrap; }
  .hero__arch { position: relative; }
  .hero__arch :global(img) { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; border-radius: 215px 215px 10px 10px; box-shadow: 0 30px 60px -30px rgba(52, 0, 5, .45); }
  .hero__seal { position: absolute; left: -34px; bottom: 26px; width: 132px; height: 132px; border-radius: 50%; background: var(--bord); display: grid; place-items: center; align-content: center; gap: 2px; transform: rotate(-8deg); box-shadow: 0 12px 30px -10px rgba(52, 0, 5, .6); color: var(--oro); }
  .hero__seal .picto { width: 76px; }
  .hero__seal small { font-size: .6rem; letter-spacing: .22em; text-transform: uppercase; }
  @media (max-width: 900px) {
    .hero__grid { grid-template-columns: 1fr; gap: 32px; }
    .hero__arch { max-width: 360px; margin-inline: auto; }
    .hero__seal { left: -10px; width: 104px; height: 104px; }
    .hero__seal .picto { width: 58px; }
  }
</style>
```

- [ ] **Step 2: Scrivi `src/components/HeroHome.notte.astro`**

```astro
---
import { Picture } from 'astro:assets';
import { photos } from '../assets/photos';
import { localePath, type Lang } from '../data/locales';
import type { Ui } from '../lib/content';
interface Props { lang: Lang; ui: Ui }
const { lang, ui } = Astro.props;
const [line1, line2] = ui.hero.titleNotte.split('\n');
---
<section class="hero">
  <Picture src={photos.lievitazione} alt="" widths={[640, 1024, 1600, 1920]} sizes="100vw" formats={['avif', 'webp']} loading="eager" fetchpriority="high" class="hero__img" width={1920} height={1280} />
  <video class="hero__video" muted loop playsinline preload="none" poster="/video/hero-poster.jpg" aria-hidden="true" tabindex="-1">
    <source src="/video/hero.webm" type="video/webm" />
    <source src="/video/hero.mp4" type="video/mp4" />
  </video>
  <p class="hero__side" aria-hidden="true">{ui.hero.side}</p>
  <div class="wrap hero__title">
    <h1><span class="hero__over">{ui.hero.overline}</span>{line1}<br />{line2}</h1>
    <p class="hero__lead">{ui.hero.text}<a href={localePath(lang, 'products')}>{ui.hero.ctaProducts} ↓</a></p>
  </div>
</section>
<script>
  const v = document.querySelector<HTMLVideoElement>('.hero__video');
  const saveData = (navigator as Navigator & { connection?: { saveData?: boolean } }).connection?.saveData;
  const ok = v && matchMedia('(min-width: 768px)').matches && !matchMedia('(prefers-reduced-motion: reduce)').matches && !saveData;
  if (ok) {
    addEventListener('load', () => {
      v.preload = 'auto';
      v.load();
      v.play().then(() => v.classList.add('is-on')).catch(() => {});
    }, { once: true });
  }
</script>
<style>
  .hero { position: relative; min-height: min(92vh, 720px); display: grid; align-items: end; overflow: hidden; isolation: isolate; }
  .hero :global(.hero__img), .hero__video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -2; filter: saturate(1.05) contrast(1.05); }
  .hero__video { display: none; opacity: 0; transition: opacity .8s var(--ease); }
  .hero__video.is-on { opacity: 1; }
  @media (min-width: 768px) { .hero__video { display: block; } }
  .hero::after { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(180deg, rgba(20,12,10,.45) 0%, rgba(20,12,10,0) 30%, rgba(20,12,10,.55) 65%, rgba(20,12,10,.95) 100%); }
  .hero__side { position: absolute; left: var(--gutter); top: 52%; transform: translateY(-50%) rotate(180deg); writing-mode: vertical-rl; font-size: .7rem; letter-spacing: .3em; text-transform: uppercase; color: var(--acc); }
  .hero__side::before { content: ""; display: inline-block; width: 1px; height: 56px; background: var(--acc); margin-bottom: 14px; vertical-align: middle; }
  .hero__title { display: flex; align-items: flex-end; justify-content: space-between; gap: 40px; padding-block: 44px; padding-left: calc(var(--gutter) + 20px); }
  .hero__title h1 { font-size: clamp(3rem, 9vw, 6rem); line-height: .92; }
  .hero__over { display: block; font-weight: 300; font-size: clamp(1.3rem, 3.4vw, 2.5rem); letter-spacing: -.01em; text-transform: none; color: var(--acc); margin-bottom: 8px; }
  .hero__lead { max-width: 34ch; font-size: .95rem; line-height: 1.55; color: var(--ink-2); padding-bottom: 10px; }
  .hero__lead a { display: block; color: var(--acc); text-decoration: none; margin-top: 14px; letter-spacing: .08em; text-transform: uppercase; font-size: .75rem; font-weight: 500; }
  @media (max-width: 800px) {
    .hero__side { display: none; }
    .hero__title { flex-direction: column; align-items: flex-start; gap: 20px; padding-left: var(--gutter); }
  }
</style>
```

- [ ] **Step 3: Scrivi `src/components/ProductGrid.farina.astro`**

```astro
---
import { Picture } from 'astro:assets';
import { photos } from '../assets/photos';
import { localePath, type Lang } from '../data/locales';
import { slugOf, type Product, type Ui } from '../lib/content';
interface Props { lang: Lang; ui: Ui; products: Product[]; heading?: boolean }
const { lang, ui, products, heading = true } = Astro.props;
---
<section class="pg">
  {heading && <div class="wrap pg__head"><p class="kicker">{ui.home.productsKicker}</p><h2>{ui.home.productsHeading}</h2></div>}
  <ul role="list" class="pg__strip">
    {products.map((p, i) => (
      <li class="pg__it">
        <a href={localePath(lang, 'products', slugOf(p))}>
          <span class="pg__n">{String(i + 1).padStart(2, '0')}</span>
          <Picture src={photos[p.data.photo]} alt={ui.alts[p.data.photo]} widths={[320, 480, 640]} sizes="(min-width: 1100px) 280px, (min-width: 700px) 45vw, 90vw" formats={['avif', 'webp']} width={480} height={360} />
          <h3>{p.data.title}</h3>
          <small>{p.data.tagline}</small>
        </a>
      </li>
    ))}
  </ul>
</section>
<style>
  .pg__head { padding-block: clamp(32px, 5vw, 56px) 20px; }
  .pg__head .kicker { margin-bottom: 10px; }
  .pg__strip { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
  .pg__it { border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
  .pg__it a { display: block; padding: 22px 24px 26px; text-decoration: none; height: 100%; transition: background .2s var(--ease); }
  .pg__it a:hover { background: var(--bg-2); }
  .pg__n { display: block; font-family: var(--font-display); font-weight: 300; font-size: 2.4rem; line-height: 1; color: var(--mut); margin-bottom: 14px; }
  .pg__it :global(img) { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px; margin-bottom: 14px; }
  .pg__it h3 { margin-bottom: 4px; color: var(--bord); font-size: 1.25rem; }
  .pg__it small { color: var(--mut); font-size: .8rem; }
</style>
```

- [ ] **Step 4: Scrivi `src/components/ProductGrid.notte.astro`**

```astro
---
import { Picture } from 'astro:assets';
import Picto from './Picto.astro';
import { photos } from '../assets/photos';
import { localePath, type Lang } from '../data/locales';
import { slugOf, type Product, type Ui } from '../lib/content';
import { emphasize } from '../lib/text';
interface Props { lang: Lang; ui: Ui; products: Product[]; heading?: boolean }
const { lang, ui, products, heading = true } = Astro.props;
---
<section class="pg">
  <ul role="list" class="pg__row">
    {heading && (
      <li class="pg__c pg__c--txt">
        <Picto class="pg__wm" />
        <h2 set:html={emphasize(ui.home.markHeading)}></h2>
        <p>{ui.home.markText}</p>
      </li>
    )}
    {products.map((p, i) => (
      <li class="pg__c">
        <a href={localePath(lang, 'products', slugOf(p))}>
          <Picture src={photos[p.data.photo]} alt={ui.alts[p.data.photo]} widths={[400, 600, 800]} sizes="(min-width: 1100px) 300px, (min-width: 700px) 45vw, 90vw" formats={['avif', 'webp']} width={600} height={630} />
          <span class="pg__lab"><small>{String(i + 1).padStart(2, '0')}</small><h3>{p.data.title}</h3></span>
        </a>
      </li>
    ))}
  </ul>
</section>
<style>
  .pg__row { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 2px; background: #241512; padding: 2px; }
  .pg__c { position: relative; height: 300px; overflow: hidden; background: var(--bg-2); }
  .pg__c a { display: block; height: 100%; text-decoration: none; }
  .pg__c :global(img) { width: 100%; height: 100%; object-fit: cover; opacity: .9; transition: transform .6s var(--ease); }
  .pg__c a:hover :global(img) { transform: scale(1.04); }
  .pg__c a::after { content: ""; position: absolute; inset: auto 0 0 0; height: 55%; background: linear-gradient(180deg, rgba(20,12,10,0), rgba(20,12,10,.85)); }
  .pg__lab { position: absolute; left: 18px; right: 18px; bottom: 16px; z-index: 2; }
  .pg__lab small { display: block; color: var(--acc); font-size: .7rem; letter-spacing: .2em; margin-bottom: 6px; }
  .pg__lab h3 { font-size: 1.35rem; letter-spacing: -.02em; }
  .pg__c--txt { display: flex; flex-direction: column; justify-content: space-between; padding: 22px; background: var(--bord); }
  .pg__wm { position: absolute; right: -70px; bottom: -60px; width: 260px; color: var(--acc); opacity: .18; }
  .pg__c--txt h2 { font-weight: 300; font-size: 1.9rem; position: relative; }
  .pg__c--txt h2 :global(em) { font-style: normal; color: var(--acc); }
  .pg__c--txt p { font-size: .85rem; opacity: .85; position: relative; }
</style>
```

- [ ] **Step 5: Scrivi `src/components/MarchioBand.astro` (solo Farina)**

```astro
---
import { Picture } from 'astro:assets';
import Picto from './Picto.astro';
import { photos } from '../assets/photos';
import { emphasize } from '../lib/text';
import type { Ui } from '../lib/content';
interface Props { ui: Ui }
const { ui } = Astro.props;
---
<section class="mb">
  <div class="mb__t">
    <Picto class="mb__wm" />
    <h2 set:html={emphasize(ui.home.markHeading)}></h2>
    <p>{ui.home.markText}</p>
  </div>
  <div class="mb__i"><Picture src={photos.panificio02} alt={ui.alts.panificio02} widths={[640, 960, 1280]} sizes="(min-width: 900px) 50vw, 100vw" formats={['avif', 'webp']} width={960} height={640} /></div>
</section>
<style>
  .mb { display: grid; grid-template-columns: 1fr 1fr; background: var(--bord); color: var(--paper); overflow: hidden; }
  .mb__t { padding: clamp(32px, 5vw, 56px) var(--gutter); position: relative; }
  .mb__wm { position: absolute; right: -40px; top: -30px; width: 260px; color: var(--oro); opacity: .16; }
  .mb__t h2 { font-weight: 300; margin-bottom: 14px; position: relative; }
  .mb__t h2 :global(em) { font-style: italic; color: var(--oro); }
  .mb__t p { opacity: .85; max-width: 42ch; position: relative; }
  .mb__i :global(img) { width: 100%; height: 100%; object-fit: cover; min-height: 280px; }
  @media (max-width: 900px) { .mb { grid-template-columns: 1fr; } }
</style>
```

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

```astro
---
interface Props { heading: string; items: { q: string; a: string }[] }
const { heading, items } = Astro.props;
---
<section class="wrap section faq">
  <h2>{heading}</h2>
  <div class="faq__list">
    {items.map((f) => (
      <details class="faq__it">
        <summary>{f.q}</summary>
        <p>{f.a}</p>
      </details>
    ))}
  </div>
</section>
<style>
  .faq h2 { margin-bottom: 24px; }
  .faq__list { max-width: 72ch; border-top: 1px solid var(--line); }
  .faq__it { border-bottom: 1px solid var(--line); }
  .faq__it summary { cursor: pointer; padding: 16px 0; font-weight: 500; list-style: none; display: flex; justify-content: space-between; gap: 16px; }
  .faq__it summary::-webkit-details-marker { display: none; }
  .faq__it summary::after { content: "+"; color: var(--acc); font-size: 1.3rem; line-height: 1; }
  .faq__it[open] summary::after { content: "−"; }
  .faq__it p { padding: 0 0 18px; color: var(--ink-2); max-width: 64ch; }
</style>
```

- [ ] **Step 7: Aggiorna `src/components/themed/index.ts`**

```ts
import { isNotte } from '../../lib/theme';
import HeaderNotte from '../Header.notte.astro';
import HeaderFarina from '../Header.farina.astro';
import HeroNotte from '../HeroHome.notte.astro';
import HeroFarina from '../HeroHome.farina.astro';
import GridNotte from '../ProductGrid.notte.astro';
import GridFarina from '../ProductGrid.farina.astro';

export const Header = isNotte ? HeaderNotte : HeaderFarina;
export const HeroHome = isNotte ? HeroNotte : HeroFarina;
export const ProductGrid = isNotte ? GridNotte : GridFarina;
```

- [ ] **Step 8: Riscrivi `src/pages/[lang]/index.astro`**

```astro
---
import { Picture } from 'astro:assets';
import Base from '../../layouts/Base.astro';
import { HeroHome, ProductGrid } from '../../components/themed';
import MarchioBand from '../../components/MarchioBand.astro';
import Faq from '../../components/Faq.astro';
import { isNotte } from '../../lib/theme';
import { LOCALES, localePath, type Lang } from '../../data/locales';
import { business } from '../../data/business';
import { getUi, getProducts, slugOf } from '../../lib/content';
import { bakeryLd, faqLd, itemListLd, absUrl } from '../../lib/seo';
import { photos } from '../../assets/photos';

export function getStaticPaths() {
  return LOCALES.map((lang) => ({ params: { lang } }));
}
const lang = Astro.params.lang as Lang;
const ui = await getUi(lang);
const products = await getProducts(lang);
const alternates = Object.fromEntries(LOCALES.map((l) => [l, localePath(l)])) as Record<Lang, string>;
const jsonLd = [
  bakeryLd(lang, ui),
  faqLd(ui.faq),
  itemListLd(products.map((p) => ({ name: p.data.title, url: absUrl(localePath(lang, 'products', slugOf(p))) }))),
];
---
<Base lang={lang} title={ui.meta.home.title} description={ui.meta.home.description} alternates={alternates} jsonLd={jsonLd} transparentHeader={isNotte} ogImage={photos.pagnotta}>
  <HeroHome lang={lang} ui={ui} />
  <ProductGrid lang={lang} ui={ui} products={products} />
  {!isNotte && <MarchioBand ui={ui} />}

  <section class="wrap section story">
    <div class="story__img"><Picture src={photos.persona04} alt={ui.alts.persona04} widths={[480, 720, 960]} sizes="(min-width: 900px) 40vw, 90vw" formats={['avif', 'webp']} width={720} height={960} /></div>
    <div class="story__txt">
      <p class="kicker">{ui.home.storyKicker}</p>
      <h2>{ui.home.storyHeading}</h2>
      <p>{ui.home.storyText}</p>
      <a class="btn" href={localePath(lang, 'bakery')}>{ui.home.storyCta}</a>
    </div>
  </section>

  <Faq heading={ui.home.faqHeading} items={ui.faq} />

  <section class="wrap section contact">
    <h2>{ui.home.contactHeading}</h2>
    <p>{ui.home.contactText}</p>
    <p class="contact__links">
      <a class="btn btn--fill" href={business.whatsappHref} rel="noopener">WhatsApp</a>
      <a class="btn" href={business.phoneHref}>{business.phone}</a>
      <a class="btn" href={business.mapsHref} rel="noopener">{ui.contact.map}</a>
    </p>
  </section>
</Base>
<style>
  .story { display: grid; grid-template-columns: 2fr 3fr; gap: clamp(24px, 5vw, 64px); align-items: center; }
  .story__img :global(img) { border-radius: var(--radius); aspect-ratio: 3 / 4; object-fit: cover; }
  .story__txt .kicker { margin-bottom: 12px; }
  .story__txt h2 { margin-bottom: 16px; }
  .story__txt p { color: var(--ink-2); margin-bottom: 24px; max-width: 52ch; }
  .contact h2 { margin-bottom: 10px; }
  .contact p { color: var(--ink-2); }
  .contact__links { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 20px; }
  @media (max-width: 800px) { .story { grid-template-columns: 1fr; } .story__img { max-width: 360px; } }
</style>
```

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

Run: `pnpm check && pnpm build:notte && grep -c 'application/ld+json' dist/it/index.html && grep -o '<h1' dist/it/index.html | wc -l && pnpm build:farina && grep -o 'hero__seal' dist/fr/index.html | head -1`
Expected: `3`, `1`, `hero__seal`.

- [ ] **Step 10: Guarda in Chrome**

Con `pnpm dev` e `pnpm dev:notte --port 4322` aperti in tab Chrome a 1440 e 375 px: Farina mostra arco + sigillo + fascia bordeaux; Notte mostra video dopo il load su desktop (in Network compare `hero.webm`), foto statica a 375 px, nessun download del video su mobile.
Expected: come nei mockup. Console senza errori.

- [ ] **Step 11: Commit**

```bash
git add src
git commit -m "Home completa con hero e griglia per tema, marchio, storia, FAQ"
```

---

### Task 9: Pagine prodotti (elenco e schede)

**Files:**
- Create: `src/pages/[lang]/[products]/index.astro`, `src/pages/[lang]/[products]/[slug].astro`

**Interfaces:**
- Consumes: `ProductGrid` (con `heading={false}`), `productLd`, `breadcrumbLd`, `getImage`.

- [ ] **Step 1: Scrivi `src/pages/[lang]/[products]/index.astro`**

```astro
---
import Base from '../../../layouts/Base.astro';
import { ProductGrid } from '../../../components/themed';
import { LOCALES, SECTIONS, localePath, type Lang } from '../../../data/locales';
import { getUi, getProducts, slugOf } from '../../../lib/content';
import { itemListLd, breadcrumbLd, absUrl } from '../../../lib/seo';

export function getStaticPaths() {
  return LOCALES.map((lang) => ({ params: { lang, products: SECTIONS.products[lang] } }));
}
const lang = Astro.params.lang as Lang;
const ui = await getUi(lang);
const products = await getProducts(lang);
const alternates = Object.fromEntries(LOCALES.map((l) => [l, localePath(l, 'products')])) as Record<Lang, string>;
const jsonLd = [
  itemListLd(products.map((p) => ({ name: p.data.title, url: absUrl(localePath(lang, 'products', slugOf(p))) }))),
  breadcrumbLd([{ name: ui.product.breadcrumbHome, url: absUrl(localePath(lang)) }, { name: ui.nav.products, url: absUrl(alternates[lang]) }]),
];
---
<Base lang={lang} title={ui.meta.products.title} description={ui.meta.products.description} alternates={alternates} jsonLd={jsonLd}>
  <section class="wrap section pl__head">
    <p class="kicker">{ui.home.productsKicker}</p>
    <h1>{ui.nav.products}</h1>
    <p class="pl__lead">{ui.meta.products.description}</p>
  </section>
  <ProductGrid lang={lang} ui={ui} products={products} heading={false} />
</Base>
<style>
  .pl__head { padding-bottom: 24px; }
  .pl__head .kicker { margin-bottom: 12px; }
  .pl__head h1 { font-size: clamp(2.2rem, 5vw, 3.6rem); margin-bottom: 12px; }
  .pl__lead { color: var(--ink-2); max-width: 60ch; }
</style>
```

- [ ] **Step 2: Scrivi `src/pages/[lang]/[products]/[slug].astro`**

```astro
---
import { Picture, getImage } from 'astro:assets';
import { render } from 'astro:content';
import Base from '../../../layouts/Base.astro';
import { LOCALES, SECTIONS, localePath, type Lang } from '../../../data/locales';
import { business } from '../../../data/business';
import { getUi, getProducts, slugOf } from '../../../lib/content';
import { productLd, breadcrumbLd, absUrl } from '../../../lib/seo';
import { photos } from '../../../assets/photos';

export async function getStaticPaths() {
  const paths = [];
  for (const lang of LOCALES) {
    const products = await getProducts(lang);
    products.forEach((p, i) => {
      paths.push({
        params: { lang, products: SECTIONS.products[lang], slug: slugOf(p) },
        props: { product: p, prev: products[(i + products.length - 1) % products.length], next: products[(i + 1) % products.length] },
      });
    });
  }
  return paths;
}
const lang = Astro.params.lang as Lang;
const { product, prev, next } = Astro.props;
const ui = await getUi(lang);
const slug = slugOf(product);
const { Content } = await render(product);
const alternates = Object.fromEntries(LOCALES.map((l) => [l, localePath(l, 'products', slug)])) as Record<Lang, string>;
const photo = photos[product.data.photo];
const ogImg = await getImage({ src: photo, width: 1200, format: 'jpeg' });
const jsonLd = [
  productLd(product, absUrl(alternates[lang]), absUrl(ogImg.src)),
  breadcrumbLd([
    { name: ui.product.breadcrumbHome, url: absUrl(localePath(lang)) },
    { name: ui.nav.products, url: absUrl(localePath(lang, 'products')) },
    { name: product.data.title, url: absUrl(alternates[lang]) },
  ]),
];
---
<Base lang={lang} title={`${product.data.title} · Pane di Triora`} description={product.data.description} alternates={alternates} jsonLd={jsonLd} ogImage={photo} bodyClass="paper">
  <article class="wrap section pd">
    <nav class="pd__crumb" aria-label="breadcrumb">
      <a href={localePath(lang)}>{ui.product.breadcrumbHome}</a> / <a href={localePath(lang, 'products')}>{ui.nav.products}</a> / <span>{product.data.title}</span>
    </nav>
    <div class="pd__grid">
      <div class="pd__img">
        <Picture src={photo} alt={ui.alts[product.data.photo]} widths={[480, 800, 1200]} sizes="(min-width: 900px) 45vw, 92vw" formats={['avif', 'webp']} loading="eager" fetchpriority="high" width={1200} height={800} />
      </div>
      <div class="pd__body">
        <p class="kicker">{product.data.tagline}</p>
        <h1>{product.data.title}</h1>
        <div class="prose"><Content /></div>
        <dl class="pd__facts">
          <dt>{ui.product.ingredients}</dt><dd>{product.data.ingredients.join(', ')}.</dd>
          <dt>{ui.product.allergens}</dt><dd>{product.data.allergens.join(', ')}.</dd>
          {product.data.formats.length > 0 && <><dt>{ui.product.formats}</dt><dd>{product.data.formats.join(', ')}.</dd></>}
          <dt>{ui.product.where}</dt><dd>{ui.product.whereText}</dd>
        </dl>
        <p class="pd__cta">
          <a class="btn btn--fill" href={business.shopHref} rel="noopener">{ui.product.shop}</a>
          <a class="btn" href={business.nutritionPdf}>{ui.product.nutrition}</a>
        </p>
      </div>
    </div>
    <nav class="pd__pn" aria-label={ui.product.all}>
      <a href={localePath(lang, 'products', slugOf(prev))}>← {prev.data.title}</a>
      <a href={localePath(lang, 'products')}>{ui.product.all}</a>
      <a href={localePath(lang, 'products', slugOf(next))}>{next.data.title} →</a>
    </nav>
  </article>
</Base>
<style>
  .pd__crumb { font-size: .85rem; color: var(--mut, var(--ink-2)); margin-bottom: 28px; }
  .pd__crumb a { text-decoration: none; }
  .pd__grid { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(24px, 5vw, 64px); align-items: start; }
  .pd__img :global(img) { border-radius: var(--radius); aspect-ratio: 3 / 2; object-fit: cover; position: sticky; top: 24px; }
  .pd__body .kicker { margin-bottom: 10px; }
  .pd__body h1 { font-size: clamp(2.2rem, 5vw, 3.6rem); margin-bottom: 20px; color: var(--bord); }
  .pd__facts { margin-top: 28px; border-top: 1px solid var(--line); }
  .pd__facts dt { font-size: .75rem; letter-spacing: .14em; text-transform: uppercase; color: var(--acc); padding-top: 14px; }
  .pd__facts dd { padding: 4px 0 14px; border-bottom: 1px solid var(--line); color: var(--ink-2); }
  .pd__cta { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 24px; }
  .pd__pn { display: flex; justify-content: space-between; gap: 16px; margin-top: 48px; padding-top: 20px; border-top: 1px solid var(--line); font-size: .9rem; }
  .pd__pn a { text-decoration: none; }
  @media (max-width: 800px) { .pd__grid { grid-template-columns: 1fr; } .pd__img :global(img) { position: static; } .pd__pn { flex-direction: column; } }
</style>
```

Nel tema Notte la classe `paper` sul body rende chiare le pagine di testo: aggiungi in `global.css`

```css
html[data-theme="notte"] body.paper { background: var(--paper); color: var(--paper-ink); --ink: var(--paper-ink); --ink-2: #4a3a30; --line: rgba(52,0,5,.14); --bord: #340005; --acc: #B5541C; --bg: var(--paper); --bg-2: #EFE3CC; }
html[data-theme="notte"] body.paper .hd { background: var(--paper); }
html[data-theme="notte"] body.paper .hd__picto { color: #340005; }
html[data-theme="notte"] body.paper .hd__word { filter: invert(1) sepia(1) saturate(4) hue-rotate(320deg) brightness(.35); }
html[data-theme="notte"] body.paper .hd__burger, html[data-theme="notte"] body.paper .hd__burger::before, html[data-theme="notte"] body.paper .hd__burger::after { background: #340005; }
```

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

Run: `pnpm build:farina && ls dist/it/prodotti/ && ls dist/de/produkte/ | wc -l && grep -c '"@type":"Product"' dist/en/products/grissini/index.html`
Expected: 7 cartelle + `index.html`, `8`, `1`.

- [ ] **Step 4: Guarda in Chrome**

`/it/prodotti/` e `/it/prodotti/pane-di-triora/` in entrambi i temi, 375 e 1440 px. In Notte la scheda ha fondo crema con testata coerente.
Expected: foto sticky a desktop, prev/next funzionanti, PDF scaricabile.

- [ ] **Step 5: Commit**

```bash
git add src
git commit -m "Pagine prodotti con schema Product e breadcrumb"
```

---

### Task 10: Pagine interne (panificio, Triora, parlano di noi, contatti, privacy, note legali)

**Files:**
- Create: `src/components/Gallery.astro`, `src/components/YouTubeFacade.astro`, `src/components/ContactBlock.astro`, `src/pages/[lang]/[page].astro`

**Interfaces:**
- Consumes: `getPage(lang, key)`, `galleries`, `portraits`, `business.press`.

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

```astro
---
import { Picture, getImage } from 'astro:assets';
import { photos, type PhotoKey } from '../assets/photos';
import type { Ui } from '../lib/content';
interface Props { keys: readonly PhotoKey[]; ui: Ui }
const { keys, ui } = Astro.props;
const large = await Promise.all(keys.map((k) => getImage({ src: photos[k], width: 1600, format: 'webp' })));
---
<ul role="list" class="gal">
  {keys.map((k, i) => (
    <li>
      <button type="button" class="gal__btn" data-i={i} aria-label={`${ui.gallery.open}: ${ui.alts[k]}`}>
        <Picture src={photos[k]} alt={ui.alts[k]} widths={[400, 640, 900]} sizes="(min-width: 900px) 30vw, 45vw" formats={['avif', 'webp']} width={640} height={480} />
      </button>
    </li>
  ))}
</ul>
<dialog class="gal__dlg" aria-label={ui.gallery.open}>
  <button type="button" class="gal__close" data-close aria-label={ui.gallery.close}>×</button>
  <button type="button" class="gal__nav gal__nav--prev" data-dir="-1" aria-label={ui.gallery.prev}>‹</button>
  <img alt="" />
  <button type="button" class="gal__nav gal__nav--next" data-dir="1" aria-label={ui.gallery.next}>›</button>
</dialog>
<script define:vars={{ srcs: large.map((l) => l.src), alts: keys.map((k) => ui.alts[k]) }}>
  const dlg = document.querySelector('.gal__dlg');
  const img = dlg.querySelector('img');
  let i = 0;
  const show = (n) => { i = (n + srcs.length) % srcs.length; img.src = srcs[i]; img.alt = alts[i]; };
  document.querySelectorAll('.gal__btn').forEach((b) => b.addEventListener('click', () => { show(Number(b.dataset.i)); dlg.showModal(); }));
  dlg.querySelectorAll('[data-dir]').forEach((b) => b.addEventListener('click', () => show(i + Number(b.dataset.dir))));
  dlg.querySelector('[data-close]').addEventListener('click', () => dlg.close());
  dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); });
  dlg.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight') show(i + 1); if (e.key === 'ArrowLeft') show(i - 1); });
</script>
<style>
  .gal { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; }
  .gal__btn { display: block; width: 100%; padding: 0; border-radius: var(--radius); overflow: hidden; }
  .gal__btn :global(img) { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; transition: transform .5s var(--ease); }
  .gal__btn:hover :global(img) { transform: scale(1.03); }
  .gal__dlg { border: 0; padding: 0; background: transparent; max-width: 96vw; max-height: 96vh; }
  .gal__dlg::backdrop { background: rgba(10, 6, 5, .92); }
  .gal__dlg img { max-width: 96vw; max-height: 90vh; width: auto; height: auto; border-radius: 6px; }
  .gal__close, .gal__nav { position: fixed; color: #fff; font-size: 2.4rem; line-height: 1; padding: 12px 18px; }
  .gal__close { top: 8px; right: 12px; }
  .gal__nav { top: 50%; transform: translateY(-50%); }
  .gal__nav--prev { left: 8px; } .gal__nav--next { right: 8px; }
</style>
```

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

```astro
---
import { Picture } from 'astro:assets';
import { photos } from '../assets/photos';
interface Props { id: string; title: string; play: string }
const { id, title, play } = Astro.props;
---
<div class="yt" data-id={id}>
  <Picture src={photos.forno} alt="" widths={[640, 960, 1280]} sizes="(min-width: 900px) 800px, 92vw" formats={['avif', 'webp']} width={1280} height={720} />
  <button type="button" class="yt__play" aria-label={`${play}: ${title}`}><span>▶</span>{play}</button>
</div>
<script>
  document.querySelectorAll<HTMLElement>('.yt').forEach((box) => {
    box.querySelector('button')?.addEventListener('click', () => {
      const f = document.createElement('iframe');
      f.src = `https://www.youtube-nocookie.com/embed/${box.dataset.id}?autoplay=1&rel=0`;
      f.title = box.querySelector('button')?.getAttribute('aria-label') ?? '';
      f.allow = 'autoplay; encrypted-media; picture-in-picture';
      f.allowFullscreen = true;
      box.replaceChildren(f);
    });
  });
</script>
<style>
  .yt { position: relative; aspect-ratio: 16 / 9; border-radius: var(--radius); overflow: hidden; background: #000; max-width: 800px; }
  .yt :global(img) { width: 100%; height: 100%; object-fit: cover; opacity: .75; }
  .yt__play { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: #fff; font-weight: 500; text-shadow: 0 2px 8px rgba(0,0,0,.6); }
  .yt__play span { width: 72px; height: 72px; border-radius: 50%; background: rgba(255,255,255,.18); backdrop-filter: blur(6px); display: grid; place-items: center; font-size: 1.6rem; padding-left: 4px; }
  .yt :global(iframe) { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
</style>
```

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

```astro
---
import { business } from '../data/business';
import type { Ui } from '../lib/content';
interface Props { ui: Ui }
const { ui } = Astro.props;
const a = business.address;
const mapEmbed = `https://www.google.com/maps?q=${encodeURIComponent(`${a.street}, ${a.zip} ${a.city} ${a.province}`)}&output=embed`;
---
<div class="cb">
  <dl class="cb__list">
    <dt>{ui.contact.address}</dt><dd>{business.legalName}<br />{a.street}, {a.zip} {a.city} ({a.province})<br /><a href={business.mapsHref} rel="noopener">{ui.contact.map}</a></dd>
    <dt>{ui.contact.phone}</dt><dd><a href={business.phoneHref}>{business.phone}</a></dd>
    <dt>{ui.contact.mobile}</dt><dd><a href={business.mobileHref}>{business.mobile}</a> · <a href={business.whatsappHref} rel="noopener">{ui.contact.whatsapp}</a></dd>
    {business.email && <><dt>{ui.contact.email}</dt><dd><a href={`mailto:${business.email}`}>{business.email}</a></dd></>}
    <dt>{ui.contact.hours}</dt><dd>{ui.contact.hoursText}</dd>
    <dt>{ui.contact.social}</dt><dd><a href={business.social.instagram} rel="noopener">Instagram</a> · <a href={business.social.facebook} rel="noopener">Facebook</a></dd>
    <dt>{ui.contact.shop}</dt><dd><a href={business.shopHref} rel="noopener">nuovabottegaitalia.com</a></dd>
  </dl>
  <div class="cb__map">
    <a class="btn btn--fill" href={business.mapsHref} rel="noopener">{ui.contact.map}</a>
    <button type="button" class="btn cb__load" data-src={mapEmbed}>Google Maps</button>
  </div>
</div>
<script>
  document.querySelector<HTMLButtonElement>('.cb__load')?.addEventListener('click', (e) => {
    const b = e.currentTarget as HTMLButtonElement;
    const f = document.createElement('iframe');
    f.src = b.dataset.src ?? '';
    f.title = 'Google Maps';
    f.loading = 'lazy';
    f.referrerPolicy = 'no-referrer-when-downgrade';
    b.parentElement?.replaceChildren(f);
  });
</script>
<style>
  .cb { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(24px, 5vw, 64px); align-items: start; }
  .cb__list dt { font-size: .75rem; letter-spacing: .14em; text-transform: uppercase; color: var(--acc); padding-top: 14px; }
  .cb__list dd { padding: 4px 0 14px; border-bottom: 1px solid var(--line); }
  .cb__list a { text-decoration: none; border-bottom: 1px solid var(--line); }
  .cb__map { display: flex; gap: 12px; flex-wrap: wrap; min-height: 320px; align-content: start; }
  .cb__map :global(iframe) { width: 100%; height: 420px; border: 0; border-radius: var(--radius); }
  @media (max-width: 800px) { .cb { grid-template-columns: 1fr; } }
</style>
```

- [ ] **Step 4: Scrivi `src/pages/[lang]/[page].astro`**

```astro
---
import { Picture } from 'astro:assets';
import { render } from 'astro:content';
import Base from '../../layouts/Base.astro';
import Gallery from '../../components/Gallery.astro';
import YouTubeFacade from '../../components/YouTubeFacade.astro';
import ContactBlock from '../../components/ContactBlock.astro';
import { LOCALES, SECTIONS, localePath, type Lang } from '../../data/locales';
import { business } from '../../data/business';
import { getUi, getPage, PAGE_KEYS, type PageKey } from '../../lib/content';
import { breadcrumbLd, absUrl } from '../../lib/seo';
import { photos, galleries, portraits } from '../../assets/photos';

export function getStaticPaths() {
  return LOCALES.flatMap((lang) => PAGE_KEYS.map((key) => ({ params: { lang, page: SECTIONS[key][lang] }, props: { key } })));
}
const lang = Astro.params.lang as Lang;
const { key } = Astro.props as { key: PageKey };
const ui = await getUi(lang);
const page = await getPage(lang, key);
const { Content } = await render(page);
const alternates = Object.fromEntries(LOCALES.map((l) => [l, localePath(l, key)])) as Record<Lang, string>;
const navLabel = key === 'privacy' || key === 'legal' ? page.data.title : ui.nav[key];
const jsonLd = [
  breadcrumbLd([{ name: ui.product.breadcrumbHome, url: absUrl(localePath(lang)) }, { name: navLabel, url: absUrl(alternates[lang]) }]),
  ...(key === 'bakery' ? [{ '@context': 'https://schema.org', '@type': 'AboutPage', name: page.data.title, url: absUrl(alternates[lang]) }] : []),
];
const heroPhoto = key === 'bakery' ? photos.banco : key === 'triora' ? photos.panorama : key === 'contact' ? photos.campanile : null;
---
<Base lang={lang} title={`${page.data.title} · Pane di Triora`} description={page.data.description} alternates={alternates} jsonLd={jsonLd} ogImage={heroPhoto ?? photos.pane} bodyClass="paper">
  <article class="wrap section pg">
    <header class="pg__head">
      {page.data.kicker && <p class="kicker">{page.data.kicker}</p>}
      <h1>{page.data.heading}</h1>
    </header>
    {heroPhoto && <div class="pg__hero"><Picture src={heroPhoto} alt={ui.alts[key === 'bakery' ? 'banco' : key === 'triora' ? 'panorama' : 'campanile']} widths={[800, 1200, 1600]} sizes="(min-width: 1200px) 1200px, 100vw" formats={['avif', 'webp']} loading="eager" fetchpriority="high" width={1600} height={900} /></div>}
    <div class="prose"><Content /></div>

    {key === 'bakery' && (
      <>
        <ul role="list" class="pg__portraits">
          {portraits.map((k) => <li><Picture src={photos[k]} alt={ui.alts[k]} widths={[300, 480]} sizes="(min-width: 900px) 22vw, 45vw" formats={['avif', 'webp']} width={480} height={640} /></li>)}
        </ul>
        <Gallery keys={galleries.bakery} ui={ui} />
      </>
    )}
    {key === 'triora' && <Gallery keys={galleries.triora} ui={ui} />}
    {key === 'press' && (
      <div class="pg__press">
        <YouTubeFacade id={business.press[0].youtubeId!} title={ui.press.youtube} play={ui.press.watch} />
        <ul role="list">
          <li><a href={business.press[0].href} rel="noopener">{ui.press.youtube}</a></li>
          <li><a href={business.press[1].href} rel="noopener">{ui.press.rai}</a></li>
          <li><a href={business.press[2].href} rel="noopener">{ui.press.striscia}</a></li>
        </ul>
      </div>
    )}
    {key === 'contact' && <ContactBlock ui={ui} />}
  </article>
</Base>
<style>
  .pg__head { margin-bottom: 24px; }
  .pg__head .kicker { margin-bottom: 10px; }
  .pg__head h1 { font-size: clamp(2.2rem, 5vw, 3.6rem); color: var(--bord); }
  .pg__hero { margin-bottom: 32px; }
  .pg__hero :global(img) { border-radius: var(--radius); aspect-ratio: 16 / 9; object-fit: cover; }
  .prose { margin-bottom: 40px; }
  .pg__portraits { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 40px; }
  .pg__portraits :global(img) { border-radius: var(--radius); aspect-ratio: 3 / 4; object-fit: cover; }
  .pg__press { display: grid; gap: 24px; }
  .pg__press ul { display: grid; gap: 10px; }
  .pg__press a { color: var(--acc); }
  @media (max-width: 700px) { .pg__portraits { grid-template-columns: repeat(2, 1fr); } }
</style>
```

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

Run: `pnpm check && pnpm build:notte && ls dist/it/ && ls dist/nl/ && grep -c 'gal__btn' dist/it/panificio/index.html`
Expected: le cartelle `panificio triora parlano-di-noi contatti privacy note-legali prodotti` in `it`, le equivalenti `bakkerij ... juridische-kennisgeving` in `nl`, `7`.

- [ ] **Step 6: Guarda in Chrome**

Tutte e sei le pagine IT in entrambi i temi. Lightbox: apre, frecce, Esc, click fuori. Video YouTube parte solo al click (nessuna richiesta a youtube in Network prima). Mappa: solo al click.
Expected: console pulita.

- [ ] **Step 7: Commit**

```bash
git add src
git commit -m "Pagine interne con gallery, video facade e contatti"
```

---

### Task 11: Root redirect (Pages Function), headers, redirects, robots, llms.txt

**Files:**
- Create: `functions/_lib/negotiate.ts`, `functions/index.ts`, `tests/negotiate.test.ts`, `public/_headers`, `public/_redirects`, `src/pages/robots.txt.ts`, `src/pages/llms.txt.ts`

- [ ] **Step 1: Test per `pickLang`**

`tests/negotiate.test.ts`:

```ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { pickLang } from '../functions/_lib/negotiate.ts';

test('cookie valido vince', () => {
  assert.equal(pickLang('foo=1; lang=fr', 'de-DE,de;q=0.9'), 'fr');
});
test('cookie non valido ignorato', () => {
  assert.equal(pickLang('lang=pt', 'de-DE,de;q=0.9'), 'de');
});
test('accept-language con qualità', () => {
  assert.equal(pickLang(null, 'pt-BR,pt;q=0.9,es;q=0.8,en;q=0.7'), 'es');
  assert.equal(pickLang(null, 'nl-BE'), 'nl');
  assert.equal(pickLang(null, 'it-IT,it;q=0.9,en-US;q=0.8'), 'it');
});
test('fallback en', () => {
  assert.equal(pickLang(null, null), 'en');
  assert.equal(pickLang(null, 'ja,zh;q=0.5'), 'en');
  assert.equal(pickLang(null, '*'), 'en');
});
```

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

Run: `pnpm test`
Expected: FAIL, modulo mancante.

- [ ] **Step 3: Scrivi `functions/_lib/negotiate.ts`**

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

/** Cookie `lang` valido, altrimenti primo Accept-Language supportato, altrimenti en. */
export function pickLang(cookie: string | null, accept: string | null): Lang {
  const m = cookie?.match(/(?:^|;\s*)lang=([a-z]{2})/);
  if (m && isLang(m[1])) return m[1];
  const ranked = (accept ?? '')
    .split(',')
    .map((part) => {
      const [tag, ...params] = part.trim().split(';');
      const q = params.map((p) => p.trim()).find((p) => p.startsWith('q='))?.slice(2);
      return { tag: tag.toLowerCase(), q: q ? Number(q) : 1 };
    })
    .filter((x) => x.tag && x.tag !== '*')
    .sort((a, b) => b.q - a.q);
  for (const { tag } of ranked) {
    const base = tag.split('-')[0];
    if (isLang(base)) return base;
  }
  return 'en';
}
```

- [ ] **Step 4: Scrivi `functions/index.ts`**

```ts
import { pickLang } from './_lib/negotiate.ts';

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

- [ ] **Step 5: Esegui i test**

Run: `pnpm test`
Expected: tutti PASS.

- [ ] **Step 6: Scrivi `public/_headers`**

```
/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  X-Frame-Options: DENY
  Content-Security-Policy: default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline'; frame-src https://www.youtube-nocookie.com https://www.google.com; connect-src 'self' https://cloudflareinsights.com; font-src 'self'; media-src 'self'; base-uri 'self'; form-action 'none'; object-src 'none'

/_astro/*
  Cache-Control: public, max-age=31536000, immutable

/video/*
  Cache-Control: public, max-age=604800

/flags/*
  Cache-Control: public, max-age=604800
```

- [ ] **Step 7: Scrivi `public/_redirects`**

```
/umbraco/*  /  301
/umbraco    /  301
```

- [ ] **Step 8: Scrivi `src/pages/robots.txt.ts`**

```ts
import type { APIRoute } from 'astro';
import { noindex } from '../lib/theme';

export const GET: APIRoute = () => {
  const body = noindex
    ? 'User-agent: *\nDisallow: /\n'
    : 'User-agent: *\nAllow: /\n\nSitemap: https://www.paneditriora.it/sitemap-index.xml\n';
  return new Response(body, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
};
```

- [ ] **Step 9: Scrivi `src/pages/llms.txt.ts`**

```ts
import type { APIRoute } from 'astro';
import { getProducts, getUi, slugOf } from '../lib/content';
import { localePath } from '../data/locales';
import { business } from '../data/business';
import { absUrl } from '../lib/seo';

export const GET: APIRoute = async () => {
  const ui = await getUi('en');
  const en = await getProducts('en');
  const it = await getProducts('it');
  const a = business.address;
  const lines = [
    '# Pane di Triora',
    '',
    `> ${ui.meta.home.description}`,
    '',
    `Bakery: ${business.legalName}, ${a.street}, ${a.zip} ${a.city} (${a.province}), Liguria, Italy. Phone ${business.phone}. The only producer of Pane di Triora since the post-war years. Instagram: ${business.social.instagram}. Facebook: ${business.social.facebook}. Online shop: ${business.shopHref}.`,
    '',
    '## Products (English)',
    ...en.map((p) => `- [${p.data.title}](${absUrl(localePath('en', 'products', slugOf(p)))}): ${p.data.description}`),
    '',
    '## Prodotti (Italiano)',
    ...it.map((p) => `- [${p.data.title}](${absUrl(localePath('it', 'products', slugOf(p)))}): ${p.data.description}`),
    '',
    '## FAQ',
    ...ui.faq.map((f) => `- ${f.q} ${f.a}`),
    '',
    '## Pages',
    `- [Home (EN)](${absUrl(localePath('en'))})`,
    `- [The bakery](${absUrl(localePath('en', 'bakery'))})`,
    `- [Triora](${absUrl(localePath('en', 'triora'))})`,
    `- [Contact](${absUrl(localePath('en', 'contact'))})`,
    `- [Home (IT)](${absUrl(localePath('it'))})`,
    '',
    'Languages: en, it, fr, de, es, nl (same paths with the language prefix).',
  ];
  return new Response(lines.join('\n') + '\n', { headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
};
```

- [ ] **Step 10: Build e prova la Function in locale**

Run: `pnpm build:farina && cat dist/robots.txt && head -5 dist/llms.txt && (pnpm pages:dev --port 8788 > /tmp/pages.log 2>&1 &) && sleep 6 && curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -H 'Accept-Language: de-DE,de;q=0.9' http://localhost:8788/ && curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:8788/ && curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -H 'Cookie: lang=fr' http://localhost:8788/ && curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8788/umbraco/x; pkill -f 'wrangler pages dev'`
Expected: `302 http://localhost:8788/de/`, `302 http://localhost:8788/en/`, `302 http://localhost:8788/fr/`, `301`.

- [ ] **Step 11: Commit**

```bash
git add functions tests public/_headers public/_redirects src/pages/robots.txt.ts src/pages/llms.txt.ts
git commit -m "Redirect root per lingua, headers, redirects, robots e llms.txt"
```

---

### Task 12: Controllo automatico di `dist/` (hreflang, link interni, H1)

**Files:**
- Create: `scripts/check-dist.mjs`

- [ ] **Step 1: Scrivi `scripts/check-dist.mjs`**

```js
import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
import { join, relative } from 'node:path';

const dist = new URL('../dist/', import.meta.url).pathname;
const LANGS = ['en', 'it', 'fr', 'de', 'es', 'nl'];
const SITE = 'https://www.paneditriora.it';
let errors = 0;
const fail = (m) => { console.error('✗', m); errors++; };

const walk = (d) => readdirSync(d).flatMap((f) => { const p = join(d, f); return statSync(p).isDirectory() ? walk(p) : p.endsWith('.html') ? [p] : []; });
const pages = walk(dist).filter((p) => LANGS.some((l) => relative(dist, p).startsWith(l + '/')));

const exists = (path) => {
  const clean = path.split('#')[0].split('?')[0];
  if (clean.endsWith('/')) return existsSync(join(dist, clean, 'index.html'));
  return existsSync(join(dist, clean));
};

for (const p of pages) {
  const rel = '/' + relative(dist, p).replace(/index\.html$/, '');
  const html = readFileSync(p, 'utf8');
  const h1 = (html.match(/<h1[\s>]/g) ?? []).length;
  if (h1 !== 1) fail(`${rel}: ${h1} h1`);
  if (!/<link rel="canonical"/.test(html)) fail(`${rel}: canonical mancante`);
  if (!/<meta name="description"/.test(html)) fail(`${rel}: description mancante`);
  const alts = [...html.matchAll(/hreflang="([a-z-]+)" href="([^"]+)"/g)];
  const langs = alts.map((m) => m[1]).sort();
  const expected = [...LANGS, 'x-default'].sort();
  if (JSON.stringify(langs) !== JSON.stringify(expected)) fail(`${rel}: hreflang ${langs.join(',')}`);
  for (const [, , href] of alts) {
    if (!href.startsWith(SITE + '/')) { fail(`${rel}: hreflang non assoluto ${href}`); continue; }
    if (!exists(href.slice(SITE.length))) fail(`${rel}: hreflang verso pagina inesistente ${href}`);
  }
  for (const [, href] of html.matchAll(/href="(\/[^"]*)"/g)) {
    if (href.startsWith('//')) continue;
    if (!exists(href)) fail(`${rel}: link rotto ${href}`);
  }
  for (const [, src] of html.matchAll(/(?:src|poster)="(\/[^"]*)"/g)) {
    if (!exists(src)) fail(`${rel}: asset mancante ${src}`);
  }
}

if (!existsSync(join(dist, 'sitemap-index.xml'))) fail('sitemap-index.xml mancante');
if (existsSync(join(dist, 'index.html'))) fail('dist/index.html non deve esistere: la root è gestita dalla Function');

if (errors) { console.error(`${errors} errori su ${pages.length} pagine`); process.exit(1); }
console.log(`✓ ${pages.length} pagine ok`);
```

- [ ] **Step 2: Esegui su entrambi i temi**

Run: `pnpm build:notte && pnpm check:dist && pnpm build:farina && pnpm check:dist`
Expected: `✓ 90 pagine ok` due volte. Correggi ogni errore segnalato prima di proseguire.

- [ ] **Step 3: Esegui `pnpm verify` completo**

Run: `pnpm verify`
Expected: tutto verde.

- [ ] **Step 4: Commit**

```bash
git add scripts/check-dist.mjs
git commit -m "Controllo automatico di dist: hreflang, link, H1"
```

---

### Task 13: Lighthouse, Rich Results, rifiniture visive

**Files:**
- Modify: quello che serve in `src/` in base ai risultati.

- [ ] **Step 1: Lighthouse su preview statica, entrambi i temi**

```bash
pnpm add -D lighthouse
pnpm build:notte && (pnpm preview --port 4323 > /dev/null 2>&1 &) && sleep 3
pnpm dlx lighthouse http://localhost:4323/it/ --preset=desktop --only-categories=performance,accessibility,best-practices,seo --output=json --output-path=/tmp/lh-notte-home.json --quiet --chrome-flags="--headless=new"
pnpm dlx lighthouse http://localhost:4323/it/prodotti/pane-di-triora/ --form-factor=mobile --only-categories=performance,accessibility,best-practices,seo --output=json --output-path=/tmp/lh-notte-prod.json --quiet --chrome-flags="--headless=new"
pkill -f 'astro preview'
pnpm build:farina && (pnpm preview --port 4323 > /dev/null 2>&1 &) && sleep 3
pnpm dlx lighthouse http://localhost:4323/it/ --form-factor=mobile --only-categories=performance,accessibility,best-practices,seo --output=json --output-path=/tmp/lh-farina-home.json --quiet --chrome-flags="--headless=new"
pkill -f 'astro preview'
for f in /tmp/lh-*.json; do node -e "const r=require('$f').categories;console.log('$f',Object.values(r).map(c=>c.title+': '+Math.round(c.score*100)).join(' | '))"; done
```

Expected: ogni voce ≥ 95. Sotto soglia: leggi `audits` nel JSON (`node -e "..."` filtrando `score < 0.9`) e correggi. Cause tipiche: immagine hero senza `fetchpriority`, contrasto del testo su foto (aggiungi overlay), `sizes` sbagliati, font non precaricato (aggiungi `<link rel="preload">` del woff2 del display font: prendi il nome hashato da `dist/_astro/`).

- [ ] **Step 2: Peso pagina**

Run: `pnpm build:farina && du -sh dist/_astro | cut -f1 && ls -la dist/it/index.html`
E in Chrome DevTools, Network, con cache disabilitata, 375 px: home Farina < 300 KB trasferiti; home Notte desktop < 2 MB incluso video.

- [ ] **Step 3: Rich Results Test**

Con `pnpm deploy:farina` fatto (Task 14) oppure esponendo la preview con `pnpm dlx cloudflared tunnel --url http://localhost:4323`: testa home e una scheda su https://search.google.com/test/rich-results.
Expected: `Bakery`/`LocalBusiness`, `FAQ`, `Product`, `Breadcrumb` riconosciuti senza errori (avvisi su campi opzionali ok).

- [ ] **Step 4: Passata visiva in Chrome**

Per ogni tema, a 375 e 1440 px: home, prodotti, una scheda, panificio, contatti, in IT e DE (i testi tedeschi sono i più lunghi e rompono i layout per primi). Correggi overflow, testi che vanno a capo male, tocchi sotto 44 px.

- [ ] **Step 5: Commit**

```bash
git add -A
git commit -m "Rifiniture da Lighthouse e controllo visivo"
```

---

### Task 14: Deploy su Cloudflare Pages (due progetti) e consegna

**Files:**
- Create: `README.md`

- [ ] **Step 1: Login e creazione progetti (una tantum, richiede il browser di Paolo)**

```bash
pnpm dlx wrangler login
pnpm dlx wrangler pages project create paneditriora-notte --production-branch main
pnpm dlx wrangler pages project create paneditriora-farina --production-branch main
```

- [ ] **Step 2: Deploy**

```bash
pnpm deploy:notte
pnpm deploy:farina
```

Expected: due URL `https://paneditriora-notte.pages.dev` e `https://paneditriora-farina.pages.dev`. Aprili in Chrome, verifica `/` → redirect per lingua, `robots.txt` con `Disallow: /` (anteprime con `NOINDEX=true`).

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

Nel dashboard Cloudflare, Web Analytics, aggiungi un sito per `paneditriora.it`, copia il token e rideploya con `CF_BEACON=<token> pnpm deploy:farina` (e notte). Finché non c'è il dominio, il token si può aggiungere dopo.

- [ ] **Step 4: Scrivi `README.md`**

```md
# paneditriora.it

Sito statico Astro, 6 lingue, due temi.

## Comandi

- `pnpm dev` (tema Farina) · `pnpm dev:notte`
- `pnpm verify`: test, coerenza contenuti, astro check, build e controllo dist per entrambi i temi
- `pnpm deploy:notte` · `pnpm deploy:farina`: build con `NOINDEX=true` e upload su Cloudflare Pages

## Andare in produzione con il tema scelto

1. Nel progetto Pages vincente: Custom domains → `paneditriora.it` e `www.paneditriora.it`.
2. Rideploy senza `NOINDEX`: `THEME=<tema> CF_BEACON=<token> astro build && wrangler pages deploy dist --project-name paneditriora-<tema>`.
3. Elimina l'altro progetto Pages.
4. Registra `paneditriora.it` in Google Search Console e invia `sitemap-index.xml`.

## Modificare i contenuti

- Testi prodotto: `src/content/products/<lingua>/<slug>.md`
- Pagine: `src/content/pages/<lingua>/<chiave>.md`
- Stringhe, FAQ, alt: `src/content/ui/<lingua>.json`
- Dati azienda (telefoni, email, social, orari): `src/data/business.ts`
- Foto: `src/assets/photos/` + `index.ts`

## DNS e dominio

Dominio su Aruba Business (via reseller WIDE-REG), scaduto il 16/09/2026. Trasferimento a IONOS con auth-code, poi nameserver Cloudflare. Prima del cambio DNS esportare tutti i record attuali; l'MX punta a `mail.paneditriora.it` (hosting Genesys): va ricreato identico finché la posta non viene migrata.
```

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

```bash
git add README.md
git commit -m "Deploy su Cloudflare Pages e README"
git push
```

- [ ] **Step 6: Consegna a Paolo**

Apri in Chrome i due URL `pages.dev` e riporta: i due link, i punteggi Lighthouse, le 7 domande al cliente dalla spec (sezione 12).

---

## Self-review

**Copertura spec:** obiettivo e due temi (T1, T7, T8); lingue e default EN (T1, T4, T11); selettore nel footer con bandiere SVG e cookie (T5, T7); niente form, contatti diretti (T10); social (T2, T7, T10); video hero solo desktop e solo Notte (T6, T8); analytics Cloudflare senza cookie (T7, T14); shop esterno (T2); routing e slug tradotti (T2, T9, T10); hreflang e x-default EN (T7, T12); content collections (T3, T4); dati azienda unici (T2); FAQ con schema (T8); schema Bakery/Product/Breadcrumb/ItemList/AboutPage (T7-T10); OG image (T7); robots, sitemap, llms.txt, manifest (T1, T5, T11); foto ottimizzate e dedotte (T5); marchio vettoriale (T5); gallery `<dialog>` (T10); YouTube facade (T10); font self-hosted (T1, T7); `_headers`, `_redirects` (T11); Pages Function root (T11); `noindex` sulle anteprime (T1, T11, T14); verifica: test, check-content, check-dist, Lighthouse, Rich Results, curl sulla root (T2, T4, T11-T13). Orari di apertura: assenti dallo schema finché il cliente non li dà, come da spec.

**Placeholder:** nessun TBD. L'email del cliente è volutamente vuota in `business.ts` e il markup la salta se vuota. Il token analytics è una env opzionale.

**Coerenza nomi:** `localePath(lang, section?, slug?)`, `SECTIONS`, `LEGAL_LABELS`, `getUi/getProducts/getPage/slugOf/PAGE_KEYS`, `photos/galleries/portraits`, `emphasize`, `bakeryLd/productLd/breadcrumbLd/faqLd/itemListLd/absUrl`, `theme/isNotte/noindex/cfBeacon`, `pickLang` usati con la stessa firma in tutti i task. `ProductGrid` accetta `heading` in entrambe le varianti. `Base` riceve `bodyClass="paper"` per le pagine interne.
