# Landing gruppobenza.it: 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:** costruire e pubblicare `gruppobenza.it`, una landing statica in tre lingue che racconta il Gruppo Benza e smista i visitatori verso le quattro destinazioni del gruppo.

**Architecture:** Astro 5 con output statico e i18n nativo. Una sola pagina per lingua, composta da sezioni riutilizzabili, con i testi in tre dizionari TypeScript. Nessun backend, nessun CMS. I test girano su `dist/` dopo la build e verificano quello che si rompe in silenzio: dati strutturati, hreflang, link in uscita, consenso.

**Tech Stack:** Astro 5, TypeScript, CSS nativo con custom property, Fontsource (font self-hosted), Vitest + Cheerio per i test sull'HTML costruito, Cloudflare Pages per il deploy.

## Global Constraints

- Il repo è `/Users/paolo/Server/Siti Web/Lavoro/Benza/gruppobenza_26/`, già inizializzato con git.
- Package manager: **npm**.
- Output della build: **statico** (`output: 'static'`), nessun adapter server.
- Lingue: `it` (default, senza prefisso), `en` (`/en/`), `fr` (`/fr/`).
- Dominio di produzione: `https://www.gruppobenza.it`.
- **L'unico JavaScript spedito al browser è il banner di consenso e `gtag`.** Nessuna sezione della pagina può dipendere da JS per essere leggibile.
- Colori: acqua `#00a1e6`, giardino su fondo chiaro `#006000`, giardino su fondo scuro `#00a13a`, inchiostro `#0b1220`.
- Font: **Sora** per i titoli, **Source Sans 3** per il testo, entrambi self-hosted via Fontsource. Nessuna chiamata a `fonts.googleapis.com`.
- Contrasto minimo AA (4.5:1) su tutti i testi.
- Tutti i testi visibili passano dai dizionari. Nessuna stringa in italiano scritta dentro un componente.
- ID misurazione GA4: da creare, segnaposto `GA_ID` in `.env`, mai scritto a mano nel codice sorgente dei componenti.
- Le partite IVA sono `IT01060310081` (Benza Water Storage SRL) e `01360200081` (Benza srl).
- Commit in italiano, imperativo, senza gergo inutile.

---

### Task 1: Impalcatura Astro, token di stile e test di fumo

**Files:**
- Create: `package.json`, `astro.config.mjs`, `tsconfig.json`, `.gitignore`, `.env.example`
- Create: `src/styles/tokens.css`
- Create: `src/layouts/BaseLayout.astro`
- Create: `src/pages/index.astro`
- Create: `vitest.config.ts`
- Test: `tests/build.test.ts`

**Interfaces:**
- Consumes: niente.
- Produces: `BaseLayout.astro` con props `{ lang: 'it'|'en'|'fr', title: string, description: string }`, che genera `<html lang>`, `<title>`, `<meta name="description">` e uno slot per il contenuto. Le variabili CSS definite in `tokens.css` sono usate da tutti i task successivi.

- [ ] **Step 1: Creare il progetto Astro**

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/Benza/gruppobenza_26"
npm create astro@latest . -- --template minimal --typescript strict --no-install --no-git --skip-houston
npm install
npm install -D vitest cheerio
npm install @fontsource-variable/sora @fontsource-variable/source-sans-3
```

Se `npm create` si lamenta che la cartella non è vuota, rispondere di procedere: `docs/` e `.git/` devono restare.

- [ ] **Step 2: Scrivere `astro.config.mjs`**

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

export default defineConfig({
  site: 'https://www.gruppobenza.it',
  output: 'static',
  i18n: {
    defaultLocale: 'it',
    locales: ['it', 'en', 'fr'],
    routing: { prefixDefaultLocale: false },
  },
  build: { inlineStylesheets: 'always' },
});
```

`inlineStylesheets: 'always'` mette il CSS dentro l'HTML: su una pagina sola toglie una richiesta e migliora l'LCP.

- [ ] **Step 3: Scrivere `src/styles/tokens.css`**

```css
:root {
  /* Colori */
  --gb-ink: #0b1220;          /* fondo scuro del gruppo */
  --gb-ink-soft: #16203a;
  --gb-paper: #ffffff;
  --gb-paper-soft: #f4f7fa;
  --gb-text: #14213d;
  --gb-text-soft: #4a5875;
  --gb-text-invert: #eef4fb;

  /* Le due anime */
  --gb-acqua: #00a1e6;        /* Benza Water Storage */
  --gb-acqua-scuro: #0077a8;  /* acqua su fondo chiaro, per il testo */
  --gb-giardino: #006000;     /* Benza srl, dal loro logo, su fondo chiaro */
  --gb-giardino-chiaro: #00a13a; /* stesso verde schiarito, per i fondi scuri */

  /* Tipografia */
  --gb-font-titoli: 'Sora Variable', system-ui, sans-serif;
  --gb-font-testo: 'Source Sans 3 Variable', system-ui, sans-serif;

  /* Ritmo */
  --gb-sp-1: 0.5rem;
  --gb-sp-2: 1rem;
  --gb-sp-3: 1.5rem;
  --gb-sp-4: 2.5rem;
  --gb-sp-5: 4rem;
  --gb-sp-6: 6rem;
  --gb-raggio: 14px;
  --gb-larghezza: 1160px;
}

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--gb-font-testo);
  color: var(--gb-text);
  background: var(--gb-paper);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3 {
  font-family: var(--gb-font-titoli);
  line-height: 1.15;
  letter-spacing: -0.02em;
  margin: 0 0 var(--gb-sp-2);
}

a { color: inherit; }

img { max-width: 100%; height: auto; display: block; }

.gb-wrap {
  width: 100%;
  max-width: var(--gb-larghezza);
  margin-inline: auto;
  padding-inline: var(--gb-sp-3);
}

.gb-sezione { padding-block: var(--gb-sp-6); }

:focus-visible {
  outline: 3px solid var(--gb-acqua);
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
```

- [ ] **Step 4: Scrivere `src/layouts/BaseLayout.astro`**

```astro
---
import '@fontsource-variable/sora';
import '@fontsource-variable/source-sans-3';
import '../styles/tokens.css';

interface Props {
  lang: 'it' | 'en' | 'fr';
  title: string;
  description: string;
}

const { lang, title, description } = Astro.props;
---
<!doctype html>
<html lang={lang}>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{title}</title>
    <meta name="description" content={description} />
    <meta name="robots" content="index, follow" />
    <meta property="og:type" content="website" />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:locale" content={lang} />
    <slot name="head" />
  </head>
  <body>
    <slot />
  </body>
</html>
```

- [ ] **Step 5: Scrivere `src/pages/index.astro` provvisorio**

```astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
---
<BaseLayout lang="it" title="Gruppo Benza" description="Dal 1932 a Sanremo.">
  <main class="gb-wrap"><h1>Gruppo Benza</h1></main>
</BaseLayout>
```

- [ ] **Step 6: Scrivere `vitest.config.ts`**

```ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['tests/**/*.test.ts'],
    environment: 'node',
  },
});
```

- [ ] **Step 7: Scrivere il test di fumo `tests/build.test.ts`**

```ts
import { describe, it, expect } from 'vitest';
import { readFileSync, existsSync } from 'node:fs';
import { load } from 'cheerio';

const dist = (p: string) => new URL(`../dist/${p}`, import.meta.url).pathname;

export const leggi = (p: string) => readFileSync(dist(p), 'utf-8');
export const dom = (p: string) => load(leggi(p));

describe('build', () => {
  it('produce la home italiana', () => {
    expect(existsSync(dist('index.html'))).toBe(true);
  });

  it('dichiara la lingua italiana e un titolo non vuoto', () => {
    const $ = dom('index.html');
    expect($('html').attr('lang')).toBe('it');
    expect($('title').text().length).toBeGreaterThan(5);
  });

  it('non carica font da domini terzi', () => {
    const html = leggi('index.html');
    expect(html).not.toContain('fonts.googleapis.com');
    expect(html).not.toContain('fonts.gstatic.com');
  });
});
```

- [ ] **Step 8: Aggiungere gli script a `package.json`**

Dentro `"scripts"`:

```json
{
  "dev": "astro dev",
  "build": "astro build",
  "preview": "astro preview",
  "test": "astro build && vitest run"
}
```

- [ ] **Step 9: Lanciare i test e verificare che passino**

Run: `npm test`
Expected: build completata e 3 test verdi.

- [ ] **Step 10: Scrivere `.gitignore` e `.env.example`**

`.gitignore`:
```
node_modules/
dist/
.astro/
.env
.DS_Store
```

`.env.example`:
```
# ID misurazione GA4 della proprieta' gruppobenza.it
PUBLIC_GA_ID=G-XXXXXXXXXX
```

- [ ] **Step 11: Commit**

```bash
git add -A
git commit -m "Impalcatura Astro, token di stile e test sulla build"
```

---

### Task 2: Dizionari e infrastruttura i18n

**Files:**
- Create: `src/i18n/tipi.ts`, `src/i18n/it.ts`, `src/i18n/en.ts`, `src/i18n/fr.ts`, `src/i18n/index.ts`
- Test: `tests/i18n.test.ts`

**Interfaces:**
- Consumes: niente.
- Produces:
  - `type Lingua = 'it' | 'en' | 'fr'`
  - `interface Dizionario` (forma completa, definita sotto)
  - `dizionari: Record<Lingua, Dizionario>`
  - `function t(lang: Lingua): Dizionario`
  - `const LINGUE: Lingua[]`
  - `function percorso(lang: Lingua, sotto?: string): string` che restituisce `/`, `/en/`, `/fr/`, oppure `/privacy/`, `/en/privacy/`, `/fr/privacy/`.

I task successivi leggono i testi **solo** attraverso `t(lang)`.

- [ ] **Step 1: Scrivere il test `tests/i18n.test.ts`**

```ts
import { describe, it, expect } from 'vitest';
import { dizionari, LINGUE, percorso, t } from '../src/i18n';

describe('dizionari', () => {
  it('copre le tre lingue', () => {
    expect(LINGUE).toEqual(['it', 'en', 'fr']);
  });

  it('ha le stesse chiavi in tutte le lingue', () => {
    const chiavi = (o: unknown, pre = ''): string[] =>
      o && typeof o === 'object' && !Array.isArray(o)
        ? Object.entries(o).flatMap(([k, v]) => chiavi(v, `${pre}${k}.`))
        : [pre];
    const it = chiavi(dizionari.it).sort();
    expect(chiavi(dizionari.en).sort()).toEqual(it);
    expect(chiavi(dizionari.fr).sort()).toEqual(it);
  });

  it('non lascia stringhe vuote', () => {
    const vuote: string[] = [];
    const scava = (o: unknown, pre = '') => {
      if (typeof o === 'string') { if (!o.trim()) vuote.push(pre); return; }
      if (Array.isArray(o)) return o.forEach((v, i) => scava(v, `${pre}[${i}]`));
      if (o && typeof o === 'object') {
        Object.entries(o).forEach(([k, v]) => scava(v, `${pre}.${k}`));
      }
    };
    LINGUE.forEach((l) => scava(dizionari[l], l));
    expect(vuote).toEqual([]);
  });

  it('ha quattro card di smistamento per lingua', () => {
    LINGUE.forEach((l) => expect(t(l).smistamento.card).toHaveLength(4));
  });

  it('costruisce i percorsi con la barra finale', () => {
    expect(percorso('it')).toBe('/');
    expect(percorso('en')).toBe('/en/');
    expect(percorso('fr', 'privacy')).toBe('/fr/privacy/');
    expect(percorso('it', 'privacy')).toBe('/privacy/');
  });
});
```

- [ ] **Step 2: Lanciare il test e verificare che fallisca**

Run: `npx vitest run tests/i18n.test.ts`
Expected: FAIL, `Cannot find module '../src/i18n'`.

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

```ts
export type Lingua = 'it' | 'en' | 'fr';

export type Anima = 'acqua' | 'giardino';

export interface VoceTimeline {
  anno: string;
  testo: string;
}

export interface Numero {
  valore: string;
  etichetta: string;
}

export interface CardSmistamento {
  anima: Anima;
  intento: string;
  dettaglio: string;
  azione: string;
  href: string;
  /** Nome breve usato nell'evento Analytics, senza spazi. */
  evento: string;
}

export interface Dizionario {
  meta: { titolo: string; descrizione: string };
  nav: { saltaAlContenuto: string };
  /** Testi alternativi delle tre foto. Usati dal task 10. */
  immagini: { heroAlt: string; acquaAlt: string; giardinoAlt: string };
  hero: {
    occhiello: string;
    titolo: string;
    sommario: string;
    portaAcqua: string;
    portaGiardino: string;
  };
  anime: {
    titolo: string;
    sommario: string;
    acqua: { nome: string; societa: string; piva: string; testo: string; azione: string; href: string };
    giardino: { nome: string; societa: string; piva: string; testo: string; azione: string; href: string };
  };
  storia: { titolo: string; sommario: string; voci: VoceTimeline[] };
  numeri: { titolo: string; voci: Numero[] };
  marchi: { titolo: string; sommario: string; elenco: string[] };
  smistamento: { titolo: string; sommario: string; card: CardSmistamento[] };
  sedi: {
    titolo: string;
    sommario: string;
    sedeTitolo: string;
    sedeIndirizzo: string;
    atelierTitolo: string;
    atelierIndirizzo: string;
    telefonoEtichetta: string;
    emailEtichetta: string;
    mappa: string;
  };
  footer: { diritti: string; privacy: string; privacyWater: string; privacySrl: string };
  cookie: { testo: string; accetta: string; rifiuta: string; informativa: string };
  privacy: { titolo: string; descrizione: string; corpo: string[] };
}
```

- [ ] **Step 4: Scrivere `src/i18n/it.ts`**

```ts
import type { Dizionario } from './tipi';

export const it: Dizionario = {
  meta: {
    titolo: 'Gruppo Benza — Sanremo dal 1932, acqua e giardino',
    descrizione:
      'Il Gruppo Benza riunisce Benza Water Storage e Benza srl: riserve idriche, teli per laghi artificiali, giardinaggio, arredo esterno e piscine. A Sanremo dal 1932.',
  },
  nav: { saltaAlContenuto: 'Salta al contenuto' },
  immagini: {
    heroAlt: 'Un lago artificiale impermeabilizzato da Benza, visto dall’alto',
    acquaAlt: 'Vasche prefabbricate di acciaio zincato per riserve idriche',
    giardinoAlt: 'Arredo per esterni a bordo piscina in uno spazio progettato da Benza',
  },
  hero: {
    occhiello: 'Dal 1932 a Sanremo',
    titolo: 'Gruppo Benza',
    sommario:
      'Due società, una storia sola. Una si occupa di acqua: vasche, riserve antincendio, teli per laghi artificiali. L’altra di giardino: arredo per esterni, piscine, irrigazione, progettazione.',
    portaAcqua: 'Acqua',
    portaGiardino: 'Giardino',
  },
  anime: {
    titolo: 'Le due anime del gruppo',
    sommario:
      'Sono due aziende distinte, con la stessa sede e la stessa storia. Sapere quale delle due cercate serve a farvi arrivare subito nel posto giusto.',
    acqua: {
      nome: 'Acqua',
      societa: 'Benza Water Storage SRL',
      piva: 'P. IVA IT01060310081',
      testo:
        'Vasche prefabbricate di acciaio zincato per riserve idriche antincendio e irrigue, teli in EPDM e PVC per laghi artificiali, biolaghi e biopiscine, coperture, impermeabilizzazione di dighe e canali. Distributore esclusivo Genap per l’Italia dal 1978.',
      azione: 'Vai a benza.it',
      href: 'https://www.benza.it/',
    },
    giardino: {
      nome: 'Giardino',
      societa: 'Benza srl',
      piva: 'P. IVA 01360200081',
      testo:
        'Arredo per esterni e barbecue, piscine e benessere, irrigazione e idraulica, giardinaggio, prato sintetico, nautica, antinfortunistica. Progettazione, posa e assistenza, con lo showroom e l’atelier di Sanremo.',
      azione: 'Vai a benzasrl.it',
      href: 'https://www.benzasrl.it/',
    },
  },
  storia: {
    titolo: 'Novant’anni a Sanremo',
    sommario:
      'La ditta nasce nel 1932 dai fratelli Mario, Giovanni e Settimio Benza. Il controllo passa poi a Danilo e Davide Benza e a Pasquale Di Muccio, che la portano fino al gruppo di oggi.',
    voci: [
      { anno: '1932', testo: 'I fratelli Mario, Giovanni e Settimio Benza fondano la ditta a Sanremo.' },
      { anno: '1978', testo: 'Inizia l’importazione in esclusiva dei prodotti Genap per l’Italia, il sud della Francia e la Svizzera.' },
      { anno: '2003', testo: 'Premio Fedeltà al Lavoro e Progresso Economico dalla Camera di Commercio di Imperia.' },
      { anno: '2014', testo: 'Viene brevettato il PDM System per la lotta al punteruolo rosso delle palme.' },
      { anno: '2016', testo: 'Danilo Benza riceve il Premio San Romolo per l’imprenditoria.' },
      { anno: '2024', testo: 'Apre l’atelier di via Feraldi 30, vetrina per l’outdoor nel centro della Città dei Fiori.' },
    ],
  },
  numeri: {
    titolo: 'In breve',
    voci: [
      { valore: '1932', etichetta: 'Anno di fondazione' },
      { valore: 'Dal 1978', etichetta: 'Esclusiva Genap per Italia, sud della Francia e Svizzera' },
      { valore: 'Top 10', etichetta: 'Fra le aziende italiane per progettazione di giardini e arredi esterni' },
      { valore: '20 paesi', etichetta: 'Dove il gruppo ha esportato negli ultimi anni' },
    ],
  },
  marchi: {
    titolo: 'Marchi trattati',
    sommario: 'Il gruppo distribuisce in zona marchi che il settore conosce.',
    elenco: [
      'Genap', 'Toro', 'Gardena', 'Jacuzzi', 'John Guest', 'Oase', 'Echo', 'UnoPiù',
      'FIM', 'Vermobil', 'Bizzotto', 'GPB', 'Fast', 'Domiziani', 'Granulati Zandobbio',
      'Sab', 'SamiPlastic',
    ],
  },
  smistamento: {
    titolo: 'Dove vuoi andare',
    sommario: 'Scegli cosa ti serve. Ti portiamo sul sito giusto del gruppo.',
    card: [
      {
        anima: 'acqua',
        intento: 'Informarmi su vasche, laghi e riserve idriche',
        dettaglio: 'Schede tecniche, misure, normative antincendio, documenti e preventivo dei laghi artificiali.',
        azione: 'Apri benza.it',
        href: 'https://www.benza.it/',
        evento: 'water_info',
      },
      {
        anima: 'acqua',
        intento: 'Comprare teli, accessori e ricambi',
        dettaglio: 'Il negozio online di Benza Water Storage, con teli su misura, accessori e spedizione in tutta Italia.',
        azione: 'Apri il negozio',
        href: 'https://www.benza.it/shop',
        evento: 'water_shop',
      },
      {
        anima: 'giardino',
        intento: 'Arredare il giardino o progettare una piscina',
        dettaglio: 'Arredo per esterni, barbecue, piscine e benessere, progettazione, servizi e atelier di Sanremo.',
        azione: 'Apri benzasrl.it',
        href: 'https://www.benzasrl.it/',
        evento: 'garden_info',
      },
      {
        anima: 'giardino',
        intento: 'Comprare pompe e materiale per l’irrigazione',
        dettaglio: 'Il negozio online per la gestione dell’acqua: pompe, regolatori, irrigazione e idraulica.',
        azione: 'Apri benzashop.it',
        href: 'https://www.benzashop.it/',
        evento: 'garden_shop',
      },
    ],
  },
  sedi: {
    titolo: 'Dove siamo',
    sommario: 'Due indirizzi a Sanremo, la sede storica e l’atelier aperto nel 2024.',
    sedeTitolo: 'Sede',
    sedeIndirizzo: 'Via Giovanni Pascoli 163, 18038 Sanremo (IM)',
    atelierTitolo: 'Atelier',
    atelierIndirizzo: 'Via Feraldi 30, 18038 Sanremo (IM)',
    telefonoEtichetta: 'Telefono',
    emailEtichetta: 'Email',
    mappa: 'Apri nelle mappe',
  },
  footer: {
    diritti: 'Gruppo Benza. Benza Water Storage SRL, P. IVA IT01060310081. Benza srl, P. IVA 01360200081.',
    privacy: 'Privacy di questa pagina',
    privacyWater: 'Privacy di benza.it',
    privacySrl: 'Privacy di benzasrl.it',
  },
  cookie: {
    testo: 'Questa pagina usa cookie tecnici e, solo con il tuo consenso, Google Analytics per capire come viene usata.',
    accetta: 'Accetta',
    rifiuta: 'Rifiuta',
    informativa: 'Leggi l’informativa',
  },
  privacy: {
    titolo: 'Privacy — Gruppo Benza',
    descrizione: 'Come questa pagina tratta i dati di chi la visita.',
    corpo: [
      'Questa pagina presenta il Gruppo Benza e indirizza verso i siti delle società del gruppo. Non raccoglie dati che le invii tu: non ci sono moduli, non c’è registrazione, non si vende niente.',
      'Con il tuo consenso usa Google Analytics 4 per contare le visite e capire verso quale sito del gruppo si dirigono le persone. Finché non accetti, nessun cookie analitico viene scritto. La scelta resta salvata in un cookie tecnico per sei mesi: per cambiarla basta cancellare i cookie di questo sito e ricaricare la pagina.',
      'Non ci sono cookie pubblicitari e non facciamo profilazione. I dati di navigazione sono trattati in forma aggregata dal titolare, Benza Water Storage SRL, Via Giovanni Pascoli 163, 18038 Sanremo (IM), che puoi contattare a info@benza.it per esercitare i tuoi diritti.',
      'Quando esci da questa pagina verso benza.it, benzasrl.it o benzashop.it, vale l’informativa privacy di quel sito.',
    ],
  },
};
```

- [ ] **Step 5: Scrivere `src/i18n/en.ts`**

```ts
import type { Dizionario } from './tipi';

export const en: Dizionario = {
  meta: {
    titolo: 'Benza Group — Sanremo since 1932, water and garden',
    descrizione:
      'The Benza Group brings together Benza Water Storage and Benza srl: water reservoirs, liners for artificial lakes, gardening, outdoor furniture and swimming pools. In Sanremo since 1932.',
  },
  nav: { saltaAlContenuto: 'Skip to content' },
  immagini: {
    heroAlt: 'An artificial lake waterproofed by Benza, seen from above',
    acquaAlt: 'Prefabricated galvanised steel tanks for water reservoirs',
    giardinoAlt: 'Outdoor furniture by a swimming pool in a space designed by Benza',
  },
  hero: {
    occhiello: 'In Sanremo since 1932',
    titolo: 'Benza Group',
    sommario:
      'Two companies, one story. One handles water: tanks, fire reservoirs, liners for artificial lakes. The other handles the garden: outdoor furniture, swimming pools, irrigation, design.',
    portaAcqua: 'Water',
    portaGiardino: 'Garden',
  },
  anime: {
    titolo: 'The two sides of the group',
    sommario:
      'Two separate companies, same address and same history. Knowing which one you are looking for takes you straight to the right place.',
    acqua: {
      nome: 'Water',
      societa: 'Benza Water Storage SRL',
      piva: 'VAT IT01060310081',
      testo:
        'Prefabricated galvanised steel tanks for fire-fighting and irrigation reservoirs, EPDM and PVC liners for artificial lakes, natural pools and swimming ponds, covers, waterproofing of dams and canals. Exclusive Genap distributor for Italy since 1978.',
      azione: 'Go to benza.it',
      href: 'https://www.benza.it/',
    },
    giardino: {
      nome: 'Garden',
      societa: 'Benza srl',
      piva: 'VAT 01360200081',
      testo:
        'Outdoor furniture and barbecues, swimming pools and wellness, irrigation and plumbing, gardening, synthetic turf, boating, safety equipment. Design, installation and service, with the Sanremo showroom and atelier.',
      azione: 'Go to benzasrl.it',
      href: 'https://www.benzasrl.it/',
    },
  },
  storia: {
    titolo: 'Ninety years in Sanremo',
    sommario:
      'The firm was founded in 1932 by the brothers Mario, Giovanni and Settimio Benza. Control later passed to Danilo and Davide Benza and to Pasquale Di Muccio, who built it into the group it is today.',
    voci: [
      { anno: '1932', testo: 'The brothers Mario, Giovanni and Settimio Benza found the firm in Sanremo.' },
      { anno: '1978', testo: 'Exclusive import of Genap products begins for Italy, southern France and Switzerland.' },
      { anno: '2003', testo: 'Loyalty to Work and Economic Progress Award from the Imperia Chamber of Commerce.' },
      { anno: '2014', testo: 'The PDM System against the red palm weevil is patented.' },
      { anno: '2016', testo: 'Danilo Benza receives the San Romolo Award for entrepreneurship.' },
      { anno: '2024', testo: 'The atelier at via Feraldi 30 opens, an outdoor showcase in the centre of Sanremo.' },
    ],
  },
  numeri: {
    titolo: 'At a glance',
    voci: [
      { valore: '1932', etichetta: 'Year the firm was founded' },
      { valore: 'Since 1978', etichetta: 'Exclusive Genap distributor for Italy, southern France and Switzerland' },
      { valore: 'Top 10', etichetta: 'Among Italian companies for garden and outdoor design' },
      { valore: '20 countries', etichetta: 'Where the group has exported in recent years' },
    ],
  },
  marchi: {
    titolo: 'Brands we carry',
    sommario: 'The group distributes brands the industry knows.',
    elenco: [
      'Genap', 'Toro', 'Gardena', 'Jacuzzi', 'John Guest', 'Oase', 'Echo', 'UnoPiù',
      'FIM', 'Vermobil', 'Bizzotto', 'GPB', 'Fast', 'Domiziani', 'Granulati Zandobbio',
      'Sab', 'SamiPlastic',
    ],
  },
  smistamento: {
    titolo: 'Where do you want to go',
    sommario: 'Pick what you need. We will take you to the right site in the group.',
    card: [
      {
        anima: 'acqua',
        intento: 'Learn about tanks, lakes and water reservoirs',
        dettaglio: 'Technical sheets, sizes, fire-safety standards, documents and the artificial lake quote tool.',
        azione: 'Open benza.it',
        href: 'https://www.benza.it/',
        evento: 'water_info',
      },
      {
        anima: 'acqua',
        intento: 'Buy liners, accessories and spare parts',
        dettaglio: 'The Benza Water Storage online shop, with made-to-measure liners, accessories and shipping across Italy.',
        azione: 'Open the shop',
        href: 'https://www.benza.it/shop',
        evento: 'water_shop',
      },
      {
        anima: 'giardino',
        intento: 'Furnish the garden or design a swimming pool',
        dettaglio: 'Outdoor furniture, barbecues, pools and wellness, design, services and the Sanremo atelier.',
        azione: 'Open benzasrl.it',
        href: 'https://www.benzasrl.it/',
        evento: 'garden_info',
      },
      {
        anima: 'giardino',
        intento: 'Buy pumps and irrigation equipment',
        dettaglio: 'The online shop for water management: pumps, controllers, irrigation and plumbing.',
        azione: 'Open benzashop.it',
        href: 'https://www.benzashop.it/',
        evento: 'garden_shop',
      },
    ],
  },
  sedi: {
    titolo: 'Where we are',
    sommario: 'Two addresses in Sanremo: the historic premises and the atelier opened in 2024.',
    sedeTitolo: 'Head office',
    sedeIndirizzo: 'Via Giovanni Pascoli 163, 18038 Sanremo (IM), Italy',
    atelierTitolo: 'Atelier',
    atelierIndirizzo: 'Via Feraldi 30, 18038 Sanremo (IM), Italy',
    telefonoEtichetta: 'Phone',
    emailEtichetta: 'Email',
    mappa: 'Open in maps',
  },
  footer: {
    diritti: 'Benza Group. Benza Water Storage SRL, VAT IT01060310081. Benza srl, VAT 01360200081.',
    privacy: 'Privacy notice for this page',
    privacyWater: 'Privacy notice of benza.it',
    privacySrl: 'Privacy notice of benzasrl.it',
  },
  cookie: {
    testo: 'This page uses technical cookies and, only with your consent, Google Analytics to understand how it is used.',
    accetta: 'Accept',
    rifiuta: 'Decline',
    informativa: 'Read the notice',
  },
  privacy: {
    titolo: 'Privacy — Benza Group',
    descrizione: 'How this page handles the data of the people who visit it.',
    corpo: [
      'This page introduces the Benza Group and points visitors to the websites of its companies. It collects no data you send us: there are no forms, no sign-up, nothing for sale.',
      'With your consent it uses Google Analytics 4 to count visits and understand which site in the group people head to. Until you accept, no analytics cookie is written. Your choice is stored in a technical cookie for six months: to change it, delete this site’s cookies and reload the page.',
      'There are no advertising cookies and no profiling. Browsing data is processed in aggregate form by the controller, Benza Water Storage SRL, Via Giovanni Pascoli 163, 18038 Sanremo (IM), Italy, reachable at info@benza.it to exercise your rights.',
      'Once you leave this page for benza.it, benzasrl.it or benzashop.it, that site’s own privacy notice applies.',
    ],
  },
};
```

- [ ] **Step 6: Scrivere `src/i18n/fr.ts`**

```ts
import type { Dizionario } from './tipi';

export const fr: Dizionario = {
  meta: {
    titolo: 'Groupe Benza — Sanremo depuis 1932, eau et jardin',
    descrizione:
      'Le Groupe Benza réunit Benza Water Storage et Benza srl : réserves d’eau, bâches pour lacs artificiels, jardinage, mobilier d’extérieur et piscines. À Sanremo depuis 1932.',
  },
  nav: { saltaAlContenuto: 'Aller au contenu' },
  immagini: {
    heroAlt: 'Un lac artificiel étanché par Benza, vu du ciel',
    acquaAlt: 'Cuves préfabriquées en acier galvanisé pour réserves d’eau',
    giardinoAlt: 'Mobilier d’extérieur au bord d’une piscine conçue par Benza',
  },
  hero: {
    occhiello: 'À Sanremo depuis 1932',
    titolo: 'Groupe Benza',
    sommario:
      'Deux sociétés, une seule histoire. L’une s’occupe de l’eau : cuves, réserves incendie, bâches pour lacs artificiels. L’autre du jardin : mobilier d’extérieur, piscines, irrigation, conception.',
    portaAcqua: 'Eau',
    portaGiardino: 'Jardin',
  },
  anime: {
    titolo: 'Les deux âmes du groupe',
    sommario:
      'Deux sociétés distinctes, même adresse et même histoire. Savoir laquelle vous cherchez vous mène directement au bon endroit.',
    acqua: {
      nome: 'Eau',
      societa: 'Benza Water Storage SRL',
      piva: 'TVA IT01060310081',
      testo:
        'Cuves préfabriquées en acier galvanisé pour réserves incendie et d’irrigation, bâches EPDM et PVC pour lacs artificiels, baignades naturelles et biopiscines, couvertures, étanchéité de barrages et de canaux. Distributeur exclusif Genap pour l’Italie depuis 1978.',
      azione: 'Aller sur benza.it',
      href: 'https://www.benza.it/',
    },
    giardino: {
      nome: 'Jardin',
      societa: 'Benza srl',
      piva: 'TVA 01360200081',
      testo:
        'Mobilier d’extérieur et barbecues, piscines et bien-être, irrigation et plomberie, jardinage, gazon synthétique, nautisme, équipements de sécurité. Conception, pose et assistance, avec le showroom et l’atelier de Sanremo.',
      azione: 'Aller sur benzasrl.it',
      href: 'https://www.benzasrl.it/',
    },
  },
  storia: {
    titolo: 'Quatre-vingt-dix ans à Sanremo',
    sommario:
      'La maison est fondée en 1932 par les frères Mario, Giovanni et Settimio Benza. Le contrôle passe ensuite à Danilo et Davide Benza et à Pasquale Di Muccio, qui en font le groupe d’aujourd’hui.',
    voci: [
      { anno: '1932', testo: 'Les frères Mario, Giovanni et Settimio Benza fondent la maison à Sanremo.' },
      { anno: '1978', testo: 'Début de l’importation exclusive des produits Genap pour l’Italie, le sud de la France et la Suisse.' },
      { anno: '2003', testo: 'Prix Fidélité au Travail et Progrès Économique de la Chambre de commerce d’Imperia.' },
      { anno: '2014', testo: 'Dépôt du brevet PDM System contre le charançon rouge du palmier.' },
      { anno: '2016', testo: 'Danilo Benza reçoit le Prix San Romolo pour l’entrepreneuriat.' },
      { anno: '2024', testo: 'Ouverture de l’atelier via Feraldi 30, vitrine outdoor au centre de Sanremo.' },
    ],
  },
  numeri: {
    titolo: 'En bref',
    voci: [
      { valore: '1932', etichetta: 'Année de fondation' },
      { valore: 'Depuis 1978', etichetta: 'Distributeur exclusif Genap pour l’Italie, le sud de la France et la Suisse' },
      { valore: 'Top 10', etichetta: 'Parmi les entreprises italiennes de conception de jardins et d’extérieurs' },
      { valore: '20 pays', etichetta: 'Où le groupe a exporté ces dernières années' },
    ],
  },
  marchi: {
    titolo: 'Marques distribuées',
    sommario: 'Le groupe distribue des marques que le métier connaît.',
    elenco: [
      'Genap', 'Toro', 'Gardena', 'Jacuzzi', 'John Guest', 'Oase', 'Echo', 'UnoPiù',
      'FIM', 'Vermobil', 'Bizzotto', 'GPB', 'Fast', 'Domiziani', 'Granulati Zandobbio',
      'Sab', 'SamiPlastic',
    ],
  },
  smistamento: {
    titolo: 'Où voulez-vous aller',
    sommario: 'Choisissez ce qu’il vous faut. Nous vous menons au bon site du groupe.',
    card: [
      {
        anima: 'acqua',
        intento: 'M’informer sur les cuves, les lacs et les réserves d’eau',
        dettaglio: 'Fiches techniques, dimensions, normes incendie, documents et devis des lacs artificiels.',
        azione: 'Ouvrir benza.it',
        href: 'https://www.benza.it/',
        evento: 'water_info',
      },
      {
        anima: 'acqua',
        intento: 'Acheter bâches, accessoires et pièces',
        dettaglio: 'La boutique en ligne de Benza Water Storage : bâches sur mesure, accessoires et expédition dans toute l’Italie.',
        azione: 'Ouvrir la boutique',
        href: 'https://www.benza.it/shop',
        evento: 'water_shop',
      },
      {
        anima: 'giardino',
        intento: 'Aménager le jardin ou concevoir une piscine',
        dettaglio: 'Mobilier d’extérieur, barbecues, piscines et bien-être, conception, services et atelier de Sanremo.',
        azione: 'Ouvrir benzasrl.it',
        href: 'https://www.benzasrl.it/',
        evento: 'garden_info',
      },
      {
        anima: 'giardino',
        intento: 'Acheter pompes et matériel d’irrigation',
        dettaglio: 'La boutique en ligne pour la gestion de l’eau : pompes, régulateurs, irrigation et plomberie.',
        azione: 'Ouvrir benzashop.it',
        href: 'https://www.benzashop.it/',
        evento: 'garden_shop',
      },
    ],
  },
  sedi: {
    titolo: 'Où nous sommes',
    sommario: 'Deux adresses à Sanremo : le siège historique et l’atelier ouvert en 2024.',
    sedeTitolo: 'Siège',
    sedeIndirizzo: 'Via Giovanni Pascoli 163, 18038 Sanremo (IM), Italie',
    atelierTitolo: 'Atelier',
    atelierIndirizzo: 'Via Feraldi 30, 18038 Sanremo (IM), Italie',
    telefonoEtichetta: 'Téléphone',
    emailEtichetta: 'Email',
    mappa: 'Ouvrir dans les cartes',
  },
  footer: {
    diritti: 'Groupe Benza. Benza Water Storage SRL, TVA IT01060310081. Benza srl, TVA 01360200081.',
    privacy: 'Confidentialité de cette page',
    privacyWater: 'Confidentialité de benza.it',
    privacySrl: 'Confidentialité de benzasrl.it',
  },
  cookie: {
    testo: 'Cette page utilise des cookies techniques et, seulement avec votre consentement, Google Analytics pour comprendre comment elle est utilisée.',
    accetta: 'Accepter',
    rifiuta: 'Refuser',
    informativa: 'Lire la notice',
  },
  privacy: {
    titolo: 'Confidentialité — Groupe Benza',
    descrizione: 'Comment cette page traite les données de ses visiteurs.',
    corpo: [
      'Cette page présente le Groupe Benza et oriente vers les sites de ses sociétés. Elle ne recueille aucune donnée que vous nous envoyez : pas de formulaire, pas d’inscription, rien à vendre.',
      'Avec votre consentement, elle utilise Google Analytics 4 pour compter les visites et comprendre vers quel site du groupe les personnes se dirigent. Tant que vous n’acceptez pas, aucun cookie analytique n’est écrit. Votre choix est conservé six mois dans un cookie technique : pour le modifier, supprimez les cookies de ce site et rechargez la page.',
      'Il n’y a ni cookies publicitaires ni profilage. Les données de navigation sont traitées de façon agrégée par le responsable de traitement, Benza Water Storage SRL, Via Giovanni Pascoli 163, 18038 Sanremo (IM), Italie, joignable à info@benza.it pour exercer vos droits.',
      'Dès que vous quittez cette page vers benza.it, benzasrl.it ou benzashop.it, la notice de confidentialité de ce site s’applique.',
    ],
  },
};
```

- [ ] **Step 7: Scrivere `src/i18n/index.ts`**

```ts
import type { Dizionario, Lingua } from './tipi';
import { it } from './it';
import { en } from './en';
import { fr } from './fr';

export type { Dizionario, Lingua, CardSmistamento, VoceTimeline, Numero, Anima } from './tipi';

export const LINGUE: Lingua[] = ['it', 'en', 'fr'];

export const dizionari: Record<Lingua, Dizionario> = { it, en, fr };

export function t(lang: Lingua): Dizionario {
  return dizionari[lang];
}

/** Percorso assoluto di una pagina. `percorso('en', 'privacy')` -> `/en/privacy/`. */
export function percorso(lang: Lingua, sotto?: string): string {
  const prefisso = lang === 'it' ? '' : `/${lang}`;
  const coda = sotto ? `/${sotto}/` : '/';
  return `${prefisso}${coda}` || '/';
}
```

- [ ] **Step 8: Lanciare i test e verificare che passino**

Run: `npx vitest run tests/i18n.test.ts`
Expected: 5 test verdi.

- [ ] **Step 9: Commit**

```bash
git add -A
git commit -m "Dizionari nelle tre lingue e funzioni i18n"
```

---

### Task 3: Layout della pagina, hreflang e rotte nelle tre lingue

**Files:**
- Modify: `src/layouts/BaseLayout.astro`
- Create: `src/components/Footer.astro`
- Create: `src/pages/index.astro` (riscritto), `src/pages/en/index.astro`, `src/pages/fr/index.astro`
- Create: `src/components/Pagina.astro`
- Test: `tests/rotte.test.ts`

**Interfaces:**
- Consumes: `t`, `percorso`, `LINGUE`, `Lingua` da `src/i18n`.
- Produces: `Pagina.astro` con prop `{ lang: Lingua }`, che monta tutte le sezioni. I task 4, 5 e 6 aggiungono sezioni dentro `Pagina.astro` e non toccano le rotte.

- [ ] **Step 1: Scrivere il test `tests/rotte.test.ts`**

```ts
import { describe, it, expect } from 'vitest';
import { existsSync, readFileSync } from 'node:fs';
import { load } from 'cheerio';

const file = (p: string) => new URL(`../dist/${p}`, import.meta.url).pathname;
const dom = (p: string) => load(readFileSync(file(p), 'utf-8'));

const pagine = { it: 'index.html', en: 'en/index.html', fr: 'fr/index.html' };

describe('rotte', () => {
  it('costruisce le tre lingue', () => {
    Object.values(pagine).forEach((p) => expect(existsSync(file(p))).toBe(true));
  });

  it('dichiara la lingua giusta su ogni pagina', () => {
    (Object.entries(pagine) as [string, string][]).forEach(([lang, p]) => {
      expect(dom(p)('html').attr('lang')).toBe(lang);
    });
  });

  it('ha hreflang verso tutte le lingue piu x-default su ognuna', () => {
    Object.values(pagine).forEach((p) => {
      const $ = dom(p);
      const trovati = $('link[rel="alternate"]')
        .map((_, el) => $(el).attr('hreflang'))
        .get()
        .sort();
      expect(trovati).toEqual(['en', 'fr', 'it', 'x-default']);
      expect($('link[rel="alternate"][hreflang="x-default"]').attr('href'))
        .toBe('https://www.gruppobenza.it/');
    });
  });

  it('ha un canonical assoluto e coerente', () => {
    expect(dom(pagine.it)('link[rel="canonical"]').attr('href')).toBe('https://www.gruppobenza.it/');
    expect(dom(pagine.fr)('link[rel="canonical"]').attr('href')).toBe('https://www.gruppobenza.it/fr/');
  });

  it('usa titoli e descrizioni diversi per lingua', () => {
    const titoli = Object.values(pagine).map((p) => dom(p)('title').text());
    expect(new Set(titoli).size).toBe(3);
  });

  it('ha un solo h1 per pagina', () => {
    Object.values(pagine).forEach((p) => expect(dom(p)('h1')).toHaveLength(1));
  });
});
```

- [ ] **Step 2: Lanciare il test e verificare che fallisca**

Run: `npm test`
Expected: FAIL, mancano `dist/en/index.html` e `dist/fr/index.html`.

- [ ] **Step 3: Riscrivere `src/layouts/BaseLayout.astro`**

```astro
---
import '@fontsource-variable/sora';
import '@fontsource-variable/source-sans-3';
import '../styles/tokens.css';
import { LINGUE, percorso, type Lingua } from '../i18n';

interface Props {
  lang: Lingua;
  title: string;
  description: string;
  /** Sottopagina, es. 'privacy'. Vuoto per la home. */
  sotto?: string;
}

const { lang, title, description, sotto } = Astro.props;
const SITO = 'https://www.gruppobenza.it';
const canonical = `${SITO}${percorso(lang, sotto)}`;
---
<!doctype html>
<html lang={lang}>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{title}</title>
    <meta name="description" content={description} />
    <meta name="robots" content="index, follow" />
    <link rel="canonical" href={canonical} />
    {LINGUE.map((l) => (
      <link rel="alternate" hreflang={l} href={`${SITO}${percorso(l, sotto)}`} />
    ))}
    <link rel="alternate" hreflang="x-default" href={`${SITO}${percorso('it', sotto)}`} />
    <meta property="og:type" content="website" />
    <meta property="og:url" content={canonical} />
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:site_name" content="Gruppo Benza" />
    <meta name="theme-color" content="#0b1220" />
    <slot name="head" />
  </head>
  <body>
    <slot />
  </body>
</html>
```

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

```astro
---
import { t, LINGUE, percorso, type Lingua } from '../i18n';

interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
const nomiLingua: Record<Lingua, string> = { it: 'Italiano', en: 'English', fr: 'Français' };
---
<footer class="gb-footer">
  <div class="gb-wrap gb-footer__griglia">
    <p class="gb-footer__diritti">{d.footer.diritti}</p>
    <nav class="gb-footer__link" aria-label="Privacy">
      <a href={percorso(lang, 'privacy')}>{d.footer.privacy}</a>
      <a href="https://www.benza.it/privacy" rel="noopener">{d.footer.privacyWater}</a>
      <a href="https://www.benzasrl.it/" rel="noopener">{d.footer.privacySrl}</a>
    </nav>
    <nav class="gb-footer__lingue" aria-label="Lingua">
      {LINGUE.map((l) => (
        <a href={percorso(l)} hreflang={l} aria-current={l === lang ? 'true' : undefined}>
          {nomiLingua[l]}
        </a>
      ))}
    </nav>
  </div>
</footer>

<style>
  .gb-footer {
    background: var(--gb-ink);
    color: var(--gb-text-invert);
    padding-block: var(--gb-sp-4);
    font-size: 0.9rem;
  }
  .gb-footer__griglia { display: grid; gap: var(--gb-sp-2); }
  .gb-footer__diritti { margin: 0; color: #b9c6da; }
  .gb-footer__link, .gb-footer__lingue {
    display: flex; flex-wrap: wrap; gap: var(--gb-sp-2);
  }
  .gb-footer a { color: var(--gb-text-invert); text-decoration: underline; }
  .gb-footer__lingue a[aria-current='true'] { color: var(--gb-acqua); text-decoration: none; }
</style>
```

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

Per ora monta solo il footer: le sezioni arrivano nei task 4, 5 e 6.

```astro
---
import { t, type Lingua } from '../i18n';
import Footer from './Footer.astro';

interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
---
<a class="gb-skip" href="#contenuto">{d.nav.saltaAlContenuto}</a>
<main id="contenuto">
  <h1 class="gb-wrap">{d.hero.titolo}</h1>
</main>
<Footer lang={lang} />

<style>
  .gb-skip {
    position: absolute;
    left: -9999px;
    background: var(--gb-ink);
    color: var(--gb-text-invert);
    padding: var(--gb-sp-2);
    z-index: 10;
  }
  .gb-skip:focus { left: var(--gb-sp-2); top: var(--gb-sp-2); }
</style>
```

- [ ] **Step 6: Scrivere le tre rotte**

`src/pages/index.astro`:
```astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Pagina from '../components/Pagina.astro';
import { t } from '../i18n';
const d = t('it');
---
<BaseLayout lang="it" title={d.meta.titolo} description={d.meta.descrizione}>
  <Pagina lang="it" />
</BaseLayout>
```

`src/pages/en/index.astro`:
```astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import Pagina from '../../components/Pagina.astro';
import { t } from '../../i18n';
const d = t('en');
---
<BaseLayout lang="en" title={d.meta.titolo} description={d.meta.descrizione}>
  <Pagina lang="en" />
</BaseLayout>
```

`src/pages/fr/index.astro`:
```astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import Pagina from '../../components/Pagina.astro';
import { t } from '../../i18n';
const d = t('fr');
---
<BaseLayout lang="fr" title={d.meta.titolo} description={d.meta.descrizione}>
  <Pagina lang="fr" />
</BaseLayout>
```

- [ ] **Step 7: Lanciare i test e verificare che passino**

Run: `npm test`
Expected: tutti verdi, comprese le 6 asserzioni sulle rotte.

- [ ] **Step 8: Commit**

```bash
git add -A
git commit -m "Tre rotte linguistiche con hreflang, canonical e footer"
```

---

### Task 4: Hero, le due anime, storia, numeri e marchi

**Files:**
- Create: `src/components/Hero.astro`, `src/components/DueAnime.astro`, `src/components/Storia.astro`, `src/components/Numeri.astro`, `src/components/Marchi.astro`
- Modify: `src/components/Pagina.astro`
- Test: `tests/contenuti.test.ts`

**Interfaces:**
- Consumes: `t`, `Lingua` da `src/i18n`; le variabili di `tokens.css`.
- Produces: cinque componenti con prop `{ lang: Lingua }`. `Hero` espone due ancore verso `#smistamento`, che il task 5 crea.

- [ ] **Step 1: Scrivere il test `tests/contenuti.test.ts`**

```ts
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { load } from 'cheerio';

const dom = (p: string) => load(readFileSync(new URL(`../dist/${p}`, import.meta.url).pathname, 'utf-8'));

describe('contenuti della home italiana', () => {
  const $ = dom('index.html');
  const testo = $('main').text();

  it('apre con il nome del gruppo e la data', () => {
    expect($('h1').text()).toContain('Gruppo Benza');
    expect(testo).toContain('Dal 1932 a Sanremo');
  });

  it('nomina entrambe le societa con la loro partita IVA', () => {
    expect(testo).toContain('Benza Water Storage SRL');
    expect(testo).toContain('IT01060310081');
    expect(testo).toContain('Benza srl');
    expect(testo).toContain('01360200081');
  });

  it('riporta tutte e sei le tappe della storia', () => {
    ['1932', '1978', '2003', '2014', '2016', '2024'].forEach((anno) => {
      expect($('.gb-storia time').text()).toContain(anno);
    });
  });

  it('cita i marchi principali', () => {
    ['Genap', 'Toro', 'Gardena', 'Jacuzzi'].forEach((m) => expect(testo).toContain(m));
  });

  it('le porte dell hero puntano allo smistamento', () => {
    const href = $('.gb-hero__porte a').map((_, el) => $(el).attr('href')).get();
    expect(href).toEqual(['#smistamento', '#smistamento']);
  });

  it('non ha immagini senza testo alternativo', () => {
    const senzaAlt = $('img').filter((_, el) => $(el).attr('alt') === undefined).length;
    expect(senzaAlt).toBe(0);
  });
});
```

- [ ] **Step 2: Lanciare il test e verificare che fallisca**

Run: `npm test`
Expected: FAIL, `expect(testo).toContain('Dal 1932 a Sanremo')`.

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

```astro
---
import { t, type Lingua } from '../i18n';
interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
---
<section class="gb-hero">
  <div class="gb-wrap">
    <p class="gb-hero__occhiello">{d.hero.occhiello}</p>
    <h1 class="gb-hero__titolo">{d.hero.titolo}</h1>
    <p class="gb-hero__sommario">{d.hero.sommario}</p>
    <div class="gb-hero__porte">
      <a class="gb-porta gb-porta--acqua" href="#smistamento">{d.hero.portaAcqua}</a>
      <a class="gb-porta gb-porta--giardino" href="#smistamento">{d.hero.portaGiardino}</a>
    </div>
  </div>
</section>

<style>
  .gb-hero {
    background:
      radial-gradient(1100px 520px at 12% -10%, rgba(0, 161, 230, 0.30), transparent 62%),
      radial-gradient(900px 480px at 88% 0%, rgba(0, 161, 58, 0.22), transparent 60%),
      var(--gb-ink);
    color: var(--gb-text-invert);
    padding-block: var(--gb-sp-6) var(--gb-sp-5);
  }
  .gb-hero__occhiello {
    margin: 0 0 var(--gb-sp-2);
    text-transform: uppercase;
    letter-spacing: 0.14em;
    font-size: 0.8rem;
    color: var(--gb-acqua);
  }
  .gb-hero__titolo {
    font-size: clamp(2.6rem, 8vw, 5rem);
    margin-bottom: var(--gb-sp-3);
  }
  .gb-hero__sommario {
    max-width: 62ch;
    font-size: clamp(1.05rem, 2.2vw, 1.3rem);
    color: #cfdcec;
    margin: 0 0 var(--gb-sp-4);
  }
  .gb-hero__porte { display: flex; flex-wrap: wrap; gap: var(--gb-sp-2); }
  .gb-porta {
    display: inline-flex;
    align-items: center;
    min-height: 56px;
    padding-inline: var(--gb-sp-4);
    border-radius: 999px;
    font-family: var(--gb-font-titoli);
    font-weight: 600;
    text-decoration: none;
    color: var(--gb-ink);
    transition: transform 0.2s ease;
  }
  .gb-porta:hover { transform: translateY(-2px); }
  .gb-porta--acqua { background: var(--gb-acqua); }
  .gb-porta--giardino { background: var(--gb-giardino-chiaro); }
</style>
```

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

```astro
---
import { t, type Lingua } from '../i18n';
interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
const blocchi = [
  { chiave: 'acqua' as const, dati: d.anime.acqua },
  { chiave: 'giardino' as const, dati: d.anime.giardino },
];
---
<section class="gb-sezione gb-anime">
  <div class="gb-wrap">
    <h2>{d.anime.titolo}</h2>
    <p class="gb-anime__sommario">{d.anime.sommario}</p>
    <div class="gb-anime__griglia">
      {blocchi.map(({ chiave, dati }) => (
        <article class={`gb-anima gb-anima--${chiave}`}>
          <p class="gb-anima__etichetta">{dati.nome}</p>
          <h3>{dati.societa}</h3>
          <p class="gb-anima__piva">{dati.piva}</p>
          <p>{dati.testo}</p>
          <a class="gb-anima__azione" href={dati.href} rel="noopener">{dati.azione}</a>
        </article>
      ))}
    </div>
  </div>
</section>

<style>
  .gb-anime__sommario { max-width: 68ch; color: var(--gb-text-soft); }
  .gb-anime__griglia {
    display: grid;
    gap: var(--gb-sp-3);
    margin-top: var(--gb-sp-4);
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  }
  .gb-anima {
    border: 1px solid #dde5ef;
    border-top: 5px solid var(--gb-acqua);
    border-radius: var(--gb-raggio);
    padding: var(--gb-sp-3);
    background: var(--gb-paper);
  }
  .gb-anima--giardino { border-top-color: var(--gb-giardino); }
  .gb-anima__etichetta {
    margin: 0;
    font-family: var(--gb-font-titoli);
    font-size: 0.8rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--gb-acqua-scuro);
  }
  .gb-anima--giardino .gb-anima__etichetta { color: var(--gb-giardino); }
  .gb-anima__piva { margin: 0 0 var(--gb-sp-2); color: var(--gb-text-soft); font-size: 0.9rem; }
  .gb-anima__azione { font-weight: 600; color: var(--gb-acqua-scuro); }
  .gb-anima--giardino .gb-anima__azione { color: var(--gb-giardino); }
</style>
```

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

```astro
---
import { t, type Lingua } from '../i18n';
interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
---
<section class="gb-sezione gb-storia">
  <div class="gb-wrap">
    <h2>{d.storia.titolo}</h2>
    <p class="gb-storia__sommario">{d.storia.sommario}</p>
    <ol class="gb-storia__linea">
      {d.storia.voci.map((v) => (
        <li class="gb-storia__voce">
          <time datetime={v.anno}>{v.anno}</time>
          <p>{v.testo}</p>
        </li>
      ))}
    </ol>
  </div>
</section>

<style>
  .gb-storia { background: var(--gb-paper-soft); }
  .gb-storia__sommario { max-width: 68ch; color: var(--gb-text-soft); }
  .gb-storia__linea {
    list-style: none;
    margin: var(--gb-sp-4) 0 0;
    padding: 0 0 0 var(--gb-sp-4);
    border-left: 2px solid #cbd8e6;
  }
  .gb-storia__voce { position: relative; padding-bottom: var(--gb-sp-4); }
  .gb-storia__voce::before {
    content: '';
    position: absolute;
    left: calc(var(--gb-sp-4) * -1 - 7px);
    top: 6px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: var(--gb-acqua);
    box-shadow: 0 0 0 4px var(--gb-paper-soft);
  }
  .gb-storia__voce time {
    display: block;
    font-family: var(--gb-font-titoli);
    font-weight: 700;
    font-size: 1.35rem;
    color: var(--gb-acqua-scuro);
  }
  .gb-storia__voce p { margin: var(--gb-sp-1) 0 0; max-width: 62ch; }
</style>
```

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

```astro
---
import { t, type Lingua } from '../i18n';
interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
---
<section class="gb-sezione gb-numeri">
  <div class="gb-wrap">
    <h2>{d.numeri.titolo}</h2>
    <dl class="gb-numeri__griglia">
      {d.numeri.voci.map((n) => (
        <div class="gb-numeri__voce">
          <dt>{n.valore}</dt>
          <dd>{n.etichetta}</dd>
        </div>
      ))}
    </dl>
  </div>
</section>

<style>
  .gb-numeri { background: var(--gb-ink); color: var(--gb-text-invert); }
  .gb-numeri__griglia {
    display: grid;
    gap: var(--gb-sp-3);
    margin: var(--gb-sp-4) 0 0;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  }
  .gb-numeri__voce { border-top: 3px solid var(--gb-acqua); padding-top: var(--gb-sp-2); }
  .gb-numeri__voce:nth-child(even) { border-top-color: var(--gb-giardino-chiaro); }
  .gb-numeri__voce dt {
    font-family: var(--gb-font-titoli);
    font-size: clamp(1.6rem, 3.4vw, 2.2rem);
    font-weight: 700;
  }
  .gb-numeri__voce dd { margin: var(--gb-sp-1) 0 0; color: #b9c6da; }
</style>
```

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

```astro
---
import { t, type Lingua } from '../i18n';
interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
---
<section class="gb-sezione gb-marchi">
  <div class="gb-wrap">
    <h2>{d.marchi.titolo}</h2>
    <p class="gb-marchi__sommario">{d.marchi.sommario}</p>
    <ul class="gb-marchi__elenco">
      {d.marchi.elenco.map((m) => <li>{m}</li>)}
    </ul>
  </div>
</section>

<style>
  .gb-marchi__sommario { color: var(--gb-text-soft); }
  .gb-marchi__elenco {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: var(--gb-sp-1) var(--gb-sp-3);
    margin: var(--gb-sp-3) 0 0;
    padding: 0;
    font-family: var(--gb-font-titoli);
    font-size: clamp(1rem, 2vw, 1.25rem);
    color: var(--gb-text);
  }
  .gb-marchi__elenco li { padding-block: var(--gb-sp-1); }
</style>
```

- [ ] **Step 8: Montare le sezioni in `src/components/Pagina.astro`**

```astro
---
import { t, type Lingua } from '../i18n';
import Hero from './Hero.astro';
import DueAnime from './DueAnime.astro';
import Storia from './Storia.astro';
import Numeri from './Numeri.astro';
import Marchi from './Marchi.astro';
import Footer from './Footer.astro';

interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
---
<a class="gb-skip" href="#contenuto">{d.nav.saltaAlContenuto}</a>
<Hero lang={lang} />
<main id="contenuto">
  <DueAnime lang={lang} />
  <Storia lang={lang} />
  <Numeri lang={lang} />
  <Marchi lang={lang} />
</main>
<Footer lang={lang} />

<style>
  .gb-skip {
    position: absolute;
    left: -9999px;
    background: var(--gb-ink);
    color: var(--gb-text-invert);
    padding: var(--gb-sp-2);
    z-index: 10;
  }
  .gb-skip:focus { left: var(--gb-sp-2); top: var(--gb-sp-2); }
</style>
```

L'`h1` sta nell'hero, quindi va tolto da `Pagina.astro`: il test `tests/rotte.test.ts` continua a chiedere un solo `h1` per pagina.

- [ ] **Step 9: Lanciare i test e verificare che passino**

Run: `npm test`
Expected: tutti verdi. Il test "le porte dell hero puntano allo smistamento" passa perché controlla l'`href`, non l'esistenza della sezione: quella arriva nel task 5.

- [ ] **Step 10: Commit**

```bash
git add -A
git commit -m "Hero, le due anime, timeline, numeri e marchi"
```

---

### Task 5: La sezione di smistamento

**Files:**
- Create: `src/components/Smistamento.astro`
- Modify: `src/components/Pagina.astro`
- Test: `tests/smistamento.test.ts`

**Interfaces:**
- Consumes: `t`, `CardSmistamento` da `src/i18n`.
- Produces: la sezione con `id="smistamento"` e quattro `<a class="gb-card" data-gb-dest="...">`. Il task 8 aggancia il tracciamento a `data-gb-dest`, che vale `water_info`, `water_shop`, `garden_info`, `garden_shop`.

- [ ] **Step 1: Scrivere il test `tests/smistamento.test.ts`**

```ts
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { load } from 'cheerio';

const dom = (p: string) => load(readFileSync(new URL(`../dist/${p}`, import.meta.url).pathname, 'utf-8'));
const pagine = ['index.html', 'en/index.html', 'fr/index.html'];

describe('smistamento', () => {
  it('esiste su tutte le lingue con quattro card', () => {
    pagine.forEach((p) => {
      const $ = dom(p);
      expect($('#smistamento')).toHaveLength(1);
      expect($('#smistamento a.gb-card')).toHaveLength(4);
    });
  });

  it('punta esattamente alle quattro destinazioni del gruppo', () => {
    pagine.forEach((p) => {
      const $ = dom(p);
      const href = $('#smistamento a.gb-card').map((_, el) => $(el).attr('href')).get();
      expect(href).toEqual([
        'https://www.benza.it/',
        'https://www.benza.it/shop',
        'https://www.benzasrl.it/',
        'https://www.benzashop.it/',
      ]);
    });
  });

  it('marca ogni card con la sua destinazione per Analytics', () => {
    const $ = dom('index.html');
    const dest = $('#smistamento a.gb-card').map((_, el) => $(el).attr('data-gb-dest')).get();
    expect(dest).toEqual(['water_info', 'water_shop', 'garden_info', 'garden_shop']);
  });

  it('protegge i link in uscita e li apre in una scheda nuova', () => {
    const $ = dom('index.html');
    $('#smistamento a.gb-card').each((_, el) => {
      expect($(el).attr('target')).toBe('_blank');
      expect($(el).attr('rel')).toContain('noopener');
    });
  });

  it('non lascia card senza intento leggibile', () => {
    const $ = dom('index.html');
    $('#smistamento a.gb-card').each((_, el) => {
      expect($(el).find('.gb-card__intento').text().trim().length).toBeGreaterThan(10);
    });
  });
});
```

- [ ] **Step 2: Lanciare il test e verificare che fallisca**

Run: `npm test`
Expected: FAIL, `expect($('#smistamento')).toHaveLength(1)` riceve 0.

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

```astro
---
import { t, type Lingua } from '../i18n';
interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
---
<section class="gb-sezione gb-smista" id="smistamento">
  <div class="gb-wrap">
    <h2>{d.smistamento.titolo}</h2>
    <p class="gb-smista__sommario">{d.smistamento.sommario}</p>
    <div class="gb-smista__griglia">
      {d.smistamento.card.map((c) => (
        <a
          class={`gb-card gb-card--${c.anima}`}
          href={c.href}
          target="_blank"
          rel="noopener noreferrer"
          data-gb-dest={c.evento}
        >
          <span class="gb-card__intento">{c.intento}</span>
          <span class="gb-card__dettaglio">{c.dettaglio}</span>
          <span class="gb-card__azione">{c.azione}</span>
        </a>
      ))}
    </div>
  </div>
</section>

<style>
  .gb-smista { background: var(--gb-paper-soft); }
  .gb-smista__sommario { max-width: 62ch; color: var(--gb-text-soft); }
  .gb-smista__griglia {
    display: grid;
    gap: var(--gb-sp-3);
    margin-top: var(--gb-sp-4);
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  }
  .gb-card {
    display: flex;
    flex-direction: column;
    gap: var(--gb-sp-2);
    background: var(--gb-paper);
    border: 1px solid #dde5ef;
    border-left: 6px solid var(--gb-acqua);
    border-radius: var(--gb-raggio);
    padding: var(--gb-sp-3);
    text-decoration: none;
    color: var(--gb-text);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
  }
  .gb-card--giardino { border-left-color: var(--gb-giardino); }
  .gb-card:hover {
    transform: translateY(-3px);
    box-shadow: 0 14px 34px rgba(10, 37, 64, 0.14);
  }
  .gb-card__intento {
    font-family: var(--gb-font-titoli);
    font-size: 1.2rem;
    font-weight: 600;
    line-height: 1.25;
  }
  .gb-card__dettaglio { color: var(--gb-text-soft); font-size: 0.96rem; }
  .gb-card__azione {
    margin-top: auto;
    font-weight: 600;
    color: var(--gb-acqua-scuro);
  }
  .gb-card--giardino .gb-card__azione { color: var(--gb-giardino); }
  .gb-card__azione::after { content: ' \2192'; }
</style>
```

- [ ] **Step 4: Montare la sezione in `src/components/Pagina.astro`**

Aggiungere l'import `import Smistamento from './Smistamento.astro';` e inserire `<Smistamento lang={lang} />` dentro `<main>` **subito dopo** `<DueAnime lang={lang} />`, così chi clicca una porta dell'hero atterra vicino all'inizio della pagina invece che in fondo.

- [ ] **Step 5: Lanciare i test e verificare che passino**

Run: `npm test`
Expected: tutti verdi, comprese le 5 asserzioni sullo smistamento.

- [ ] **Step 6: Commit**

```bash
git add -A
git commit -m "Sezione di smistamento verso le quattro destinazioni del gruppo"
```

---

### Task 6: Sedi e contatti

**Files:**
- Create: `src/components/Sedi.astro`
- Modify: `src/components/Pagina.astro`
- Test: `tests/sedi.test.ts`

**Interfaces:**
- Consumes: `t`, `Lingua`.
- Produces: la sezione `#sedi` con i due indirizzi e i due telefoni come link `tel:`.

- [ ] **Step 1: Scrivere il test `tests/sedi.test.ts`**

```ts
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { load } from 'cheerio';

const dom = (p: string) => load(readFileSync(new URL(`../dist/${p}`, import.meta.url).pathname, 'utf-8'));

describe('sedi', () => {
  const $ = dom('index.html');

  it('mostra i due indirizzi di Sanremo', () => {
    const testo = $('#sedi').text();
    expect(testo).toContain('Via Giovanni Pascoli 163');
    expect(testo).toContain('Via Feraldi 30');
  });

  it('rende i telefoni cliccabili', () => {
    const tel = $('#sedi a[href^="tel:"]').map((_, el) => $(el).attr('href')).get();
    expect(tel).toEqual(['tel:+390184501855', 'tel:+390184846132']);
  });

  it('rende l email cliccabile', () => {
    expect($('#sedi a[href="mailto:info@benza.it"]')).toHaveLength(1);
  });

  it('offre due link alle mappe senza incorporare iframe di terze parti', () => {
    expect($('#sedi a.gb-sede__mappa')).toHaveLength(2);
    expect($('#sedi iframe')).toHaveLength(0);
  });
});
```

- [ ] **Step 2: Lanciare il test e verificare che fallisca**

Run: `npm test`
Expected: FAIL, `#sedi` non esiste.

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

```astro
---
import { t, type Lingua } from '../i18n';
interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);

// Link alle mappe invece di una mappa incorporata: un iframe di Google Maps
// carica script di terze parti e cookie, che con il Consent Mode e' una
// complicazione che questa pagina non ha motivo di portarsi dietro.
const mappa = (q: string) => `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(q)}`;
const mappaSede = mappa('Benza, Via Giovanni Pascoli 163, 18038 Sanremo IM');
const mappaAtelier = mappa('Benza Atelier, Via Feraldi 30, 18038 Sanremo IM');
---
<section class="gb-sezione gb-sedi" id="sedi">
  <div class="gb-wrap">
    <h2>{d.sedi.titolo}</h2>
    <p class="gb-sedi__sommario">{d.sedi.sommario}</p>
    <div class="gb-sedi__griglia">
      <article class="gb-sede">
        <h3>{d.sedi.sedeTitolo}</h3>
        <address>{d.sedi.sedeIndirizzo}</address>
        <p><span>{d.sedi.telefonoEtichetta}:</span> <a href="tel:+390184501855">+39 0184 501855</a></p>
        <p><a class="gb-sede__mappa" href={mappaSede} target="_blank" rel="noopener">{d.sedi.mappa}</a></p>
      </article>
      <article class="gb-sede">
        <h3>{d.sedi.atelierTitolo}</h3>
        <address>{d.sedi.atelierIndirizzo}</address>
        <p><span>{d.sedi.telefonoEtichetta}:</span> <a href="tel:+390184846132">+39 0184 846132</a></p>
        <p><a class="gb-sede__mappa" href={mappaAtelier} target="_blank" rel="noopener">{d.sedi.mappa}</a></p>
      </article>
      <article class="gb-sede">
        <h3>{d.sedi.emailEtichetta}</h3>
        <p><a href="mailto:info@benza.it">info@benza.it</a></p>
      </article>
    </div>
  </div>
</section>

<style>
  .gb-sedi__sommario { max-width: 62ch; color: var(--gb-text-soft); }
  .gb-sedi__griglia {
    display: grid;
    gap: var(--gb-sp-3);
    margin-top: var(--gb-sp-4);
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  }
  .gb-sede { border-top: 3px solid var(--gb-acqua); padding-top: var(--gb-sp-2); }
  .gb-sede address { font-style: normal; color: var(--gb-text-soft); }
  .gb-sede a { color: var(--gb-acqua-scuro); font-weight: 600; }
  .gb-sede__mappa { text-decoration: underline; }
  .gb-sede span { color: var(--gb-text-soft); }
</style>
```

- [ ] **Step 4: Montare la sezione**

In `src/components/Pagina.astro`: import `Sedi` e inserire `<Sedi lang={lang} />` come ultima sezione dentro `<main>`.

- [ ] **Step 5: Lanciare i test e verificare che passino**

Run: `npm test`
Expected: tutti verdi.

- [ ] **Step 6: Commit**

```bash
git add -A
git commit -m "Sezione sedi con indirizzi, telefoni ed email"
```

---

### Task 7: Dati strutturati, robots, sitemap e llms.txt

**Files:**
- Create: `src/components/DatiStrutturati.astro`
- Create: `public/robots.txt`, `public/llms.txt`
- Modify: `src/pages/index.astro`, `src/pages/en/index.astro`, `src/pages/fr/index.astro`
- Modify: `astro.config.mjs` (integrazione sitemap)
- Test: `tests/geo.test.ts`

**Interfaces:**
- Consumes: `Lingua`.
- Produces: `DatiStrutturati.astro` con prop `{ lang: Lingua }`, da mettere nello slot `head` di `BaseLayout`.

- [ ] **Step 1: Scrivere il test `tests/geo.test.ts`**

```ts
import { describe, it, expect } from 'vitest';
import { existsSync, readFileSync } from 'node:fs';
import { load } from 'cheerio';

const file = (p: string) => new URL(`../dist/${p}`, import.meta.url).pathname;
const dom = (p: string) => load(readFileSync(file(p), 'utf-8'));

const jsonld = (p: string) => {
  const $ = dom(p);
  const grezzo = $('script[type="application/ld+json"]').first().html() ?? '';
  return JSON.parse(grezzo);
};

describe('dati strutturati', () => {
  const dati = jsonld('index.html');

  it('descrive il gruppo come Organization fondata nel 1932', () => {
    expect(dati['@type']).toBe('Organization');
    expect(dati.name).toBe('Gruppo Benza');
    expect(dati.foundingDate).toBe('1932');
  });

  it('dichiara le due societa come sotto-organizzazioni', () => {
    const figlie = dati.subOrganization;
    expect(figlie).toHaveLength(2);
    expect(figlie.map((f: any) => f.url).sort()).toEqual([
      'https://www.benza.it/',
      'https://www.benzasrl.it/',
    ]);
    expect(figlie.map((f: any) => f.vatID).sort()).toEqual(['01360200081', 'IT01060310081']);
  });

  it('ogni figlia rimanda al gruppo come organizzazione madre', () => {
    dati.subOrganization.forEach((f: any) => {
      expect(f.parentOrganization['@id']).toBe('https://www.gruppobenza.it/#gruppo');
    });
  });

  it('e valido JSON in tutte le lingue', () => {
    ['en/index.html', 'fr/index.html'].forEach((p) => {
      expect(() => jsonld(p)).not.toThrow();
    });
  });
});

describe('file di servizio', () => {
  it('pubblica robots.txt, llms.txt e la sitemap', () => {
    ['robots.txt', 'llms.txt', 'sitemap-index.xml'].forEach((f) => {
      expect(existsSync(file(f))).toBe(true);
    });
  });

  it('robots.txt nomina la sitemap e accoglie i crawler dei modelli', () => {
    const robots = readFileSync(file('robots.txt'), 'utf-8');
    expect(robots).toContain('Sitemap: https://www.gruppobenza.it/sitemap-index.xml');
    ['GPTBot', 'ClaudeBot', 'PerplexityBot', 'Google-Extended'].forEach((bot) => {
      expect(robots).toContain(bot);
    });
  });

  it('llms.txt elenca le quattro destinazioni', () => {
    const llms = readFileSync(file('llms.txt'), 'utf-8');
    ['benza.it', 'benza.it/shop', 'benzasrl.it', 'benzashop.it'].forEach((d) => {
      expect(llms).toContain(d);
    });
  });
});
```

- [ ] **Step 2: Lanciare il test e verificare che fallisca**

Run: `npm test`
Expected: FAIL, nessuno `script[type="application/ld+json"]` nella pagina.

- [ ] **Step 3: Installare e configurare la sitemap**

```bash
npx astro add sitemap --yes
```

Verificare che `astro.config.mjs` contenga `integrations: [sitemap()]` e che `site` sia rimasto `https://www.gruppobenza.it`.

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

```astro
---
import type { Lingua } from '../i18n';
interface Props { lang: Lingua }
const { lang } = Astro.props;

const SITO = 'https://www.gruppobenza.it';
const madre = { '@id': `${SITO}/#gruppo` };

const indirizzoSede = {
  '@type': 'PostalAddress',
  streetAddress: 'Via Giovanni Pascoli, 163',
  postalCode: '18038',
  addressLocality: 'Sanremo',
  addressRegion: 'IM',
  addressCountry: 'IT',
};

const dati = {
  '@context': 'https://schema.org',
  '@type': 'Organization',
  '@id': `${SITO}/#gruppo`,
  name: 'Gruppo Benza',
  url: `${SITO}/`,
  foundingDate: '1932',
  description:
    'Gruppo di società con sede a Sanremo dal 1932. Riunisce Benza Water Storage SRL, che si occupa di riserve idriche e impermeabilizzazioni, e Benza srl, che si occupa di giardinaggio, arredo per esterni e piscine.',
  address: indirizzoSede,
  telephone: '+39 0184 501855',
  email: 'info@benza.it',
  areaServed: [
    { '@type': 'Country', name: 'Italia' },
    { '@type': 'Country', name: 'Francia' },
    { '@type': 'Country', name: 'Svizzera' },
  ],
  sameAs: [
    'https://www.facebook.com/profile.php?id=100075931934637',
    'https://www.youtube.com/@RiserveIdriche',
  ],
  subOrganization: [
    {
      '@type': 'Organization',
      '@id': 'https://www.benza.it/#azienda',
      name: 'Benza Water Storage SRL',
      url: 'https://www.benza.it/',
      vatID: 'IT01060310081',
      parentOrganization: madre,
      address: indirizzoSede,
      telephone: '+39 0184 501855',
      knowsAbout: [
        'vasche prefabbricate di acciaio zincato',
        'riserve idriche antincendio UNI EN 12845',
        'teli in EPDM e PVC per laghi artificiali',
        'biolaghi e biopiscine',
        'impermeabilizzazione di dighe e canali',
      ],
      sameAs: ['https://www.ebay.it/str/benzawaterstorage'],
    },
    {
      '@type': 'Organization',
      '@id': 'https://www.benzasrl.it/#azienda',
      name: 'Benza srl',
      url: 'https://www.benzasrl.it/',
      vatID: '01360200081',
      parentOrganization: madre,
      address: {
        '@type': 'PostalAddress',
        streetAddress: 'Via Feraldi, 30',
        postalCode: '18038',
        addressLocality: 'Sanremo',
        addressRegion: 'IM',
        addressCountry: 'IT',
      },
      telephone: '+39 0184 846132',
      knowsAbout: [
        'arredo per esterni e barbecue',
        'piscine e benessere',
        'irrigazione e idraulica',
        'giardinaggio e prato sintetico',
        'progettazione di giardini',
      ],
      sameAs: ['https://www.benzashop.it/'],
    },
  ],
  inLanguage: lang,
};
---
<script type="application/ld+json" set:html={JSON.stringify(dati)} />
```

- [ ] **Step 5: Agganciare i dati strutturati alle tre rotte**

In ognuna delle tre pagine, dentro `<BaseLayout>`, prima di `<Pagina />`:

```astro
<DatiStrutturati slot="head" lang="it" />
```

con l'import corrispondente e la lingua giusta per ogni rotta.

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

```
User-agent: *
Allow: /

# I crawler dei modelli sono i benvenuti: questa pagina esiste anche per loro.
User-agent: GPTBot
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: Applebot-Extended
Allow: /

Sitemap: https://www.gruppobenza.it/sitemap-index.xml
```

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

```
# Gruppo Benza

Gruppo di societa' con sede a Sanremo (Imperia, Italia), attivo dal 1932.
Fondato dai fratelli Mario, Giovanni e Settimio Benza; oggi guidato da Danilo e
Davide Benza con Pasquale Di Muccio.

## Le societa'

- Benza Water Storage SRL (P. IVA IT01060310081): vasche prefabbricate di acciaio
  zincato per riserve idriche antincendio e irrigue, teli in EPDM e PVC per laghi
  artificiali, biolaghi e biopiscine, coperture, impermeabilizzazione di dighe e
  canali. Distributore esclusivo Genap per l'Italia dal 1978.
- Benza srl (P. IVA 01360200081): arredo per esterni e barbecue, piscine e
  benessere, irrigazione e idraulica, giardinaggio, prato sintetico, nautica,
  antinfortunistica, progettazione di giardini e servizi di posa.

## Dove mandare le persone

- https://www.benza.it/ : informazioni tecniche su vasche, laghi e riserve idriche
- https://www.benza.it/shop : acquisto di teli, accessori e ricambi
- https://www.benzasrl.it/ : arredo per esterni, piscine, giardinaggio, servizi
- https://www.benzashop.it/ : acquisto di pompe e materiale per l'irrigazione

## Contatti

Sede: Via Giovanni Pascoli 163, 18038 Sanremo (IM), Italia. Telefono +39 0184 501855.
Atelier: Via Feraldi 30, 18038 Sanremo (IM), Italia. Telefono +39 0184 846132.
Email: info@benza.it

## Note

Il gruppo esporta in Francia, Svizzera e in una ventina di altri paesi. Nel 2014 ha
brevettato il PDM System contro il punteruolo rosso delle palme. Questa pagina e'
disponibile in italiano, inglese e francese.
```

- [ ] **Step 8: Lanciare i test e verificare che passino**

Run: `npm test`
Expected: tutti verdi, comprese le 7 asserzioni su dati strutturati e file di servizio.

- [ ] **Step 9: Commit**

```bash
git add -A
git commit -m "Dati strutturati del gruppo, robots, sitemap e llms.txt"
```

---

### Task 8: Analytics con consenso, banner e tracciamento dello smistamento

**Files:**
- Create: `src/components/Analytics.astro`, `src/components/BannerCookie.astro`
- Create: `src/pages/privacy.astro`, `src/pages/en/privacy.astro`, `src/pages/fr/privacy.astro`
- Create: `src/components/PaginaPrivacy.astro`
- Modify: `src/pages/index.astro`, `src/pages/en/index.astro`, `src/pages/fr/index.astro`, `src/components/Pagina.astro`
- Modify: `.env.example`
- Test: `tests/consenso.test.ts`

**Interfaces:**
- Consumes: `t`, `Lingua`, `percorso`; l'attributo `data-gb-dest` prodotto dal task 5.
- Produces: `Analytics.astro` (slot `head`) e `BannerCookie.astro` (in fondo al body).

- [ ] **Step 1: Creare la proprietà GA4**

Nell'account Analytics `duebytes` (accesso con `duebytes@gmail.com`), creare una proprietà nuova chiamata `Gruppo Benza - gruppobenza.it` con flusso di dati web su `https://www.gruppobenza.it`. Copiare l'ID misurazione in `.env`:

```
PUBLIC_GA_ID=G-XXXXXXXXXX
```

Finché la proprietà non esiste, i test girano lo stesso: `Analytics.astro` non emette niente se `PUBLIC_GA_ID` è vuoto, e il test lo verifica.

- [ ] **Step 2: Scrivere il test `tests/consenso.test.ts`**

```ts
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { load } from 'cheerio';

const file = (p: string) => new URL(`../dist/${p}`, import.meta.url).pathname;
const leggi = (p: string) => readFileSync(file(p), 'utf-8');
const dom = (p: string) => load(leggi(p));

describe('consenso e analytics', () => {
  it('parte con il consenso analitico negato', () => {
    const html = leggi('index.html');
    expect(html).toContain("analytics_storage: 'denied'");
  });

  it('non chiede consensi pubblicitari', () => {
    const html = leggi('index.html');
    expect(html).not.toContain('ad_storage');
    expect(html).not.toContain('ad_personalization');
  });

  it('mostra il banner in tutte le lingue con i due pulsanti', () => {
    ['index.html', 'en/index.html', 'fr/index.html'].forEach((p) => {
      const $ = dom(p);
      expect($('#gbCookieBanner')).toHaveLength(1);
      expect($('#gbCookieAccetta')).toHaveLength(1);
      expect($('#gbCookieRifiuta')).toHaveLength(1);
    });
  });

  it('il banner rimanda alla privacy della lingua giusta', () => {
    expect(dom('index.html')('#gbCookieBanner a').attr('href')).toBe('/privacy/');
    expect(dom('fr/index.html')('#gbCookieBanner a').attr('href')).toBe('/fr/privacy/');
  });

  it('pubblica la privacy nelle tre lingue', () => {
    ['privacy/index.html', 'en/privacy/index.html', 'fr/privacy/index.html'].forEach((p) => {
      expect(dom(p)('h1').text().length).toBeGreaterThan(3);
    });
  });

  it('traccia i clic sulle card di smistamento', () => {
    const html = leggi('index.html');
    expect(html).toContain('data-gb-dest');
    expect(html).toContain('smistamento_click');
  });
});
```

- [ ] **Step 3: Lanciare il test e verificare che fallisca**

Run: `npm test`
Expected: FAIL, `analytics_storage: 'denied'` non compare.

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

```astro
---
const GA = import.meta.env.PUBLIC_GA_ID ?? '';
---
{GA && (
  <>
    <script is:inline define:vars={{ GA }}>
      window.dataLayer = window.dataLayer || [];
      function gtag() { dataLayer.push(arguments); }
      window.gtag = gtag;
      gtag('consent', 'default', { analytics_storage: 'denied' });
      gtag('js', new Date());
      gtag('config', GA);
    </script>
    <script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${GA}`}></script>
  </>
)}
```

Il test "parte con il consenso analitico negato" richiede che `PUBLIC_GA_ID` sia valorizzato quando si lancia `npm test`. Copiare `.env.example` in `.env` e mettere l'ID vero prima di eseguire i test.

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

```astro
---
import { t, percorso, type Lingua } from '../i18n';
interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
const hrefPrivacy = percorso(lang, 'privacy');
---
<div id="gbCookieBanner" class="gb-cookie" role="dialog" aria-label="Cookie" hidden>
  <p class="gb-cookie__testo">
    {d.cookie.testo} <a href={hrefPrivacy}>{d.cookie.informativa}</a>
  </p>
  <div class="gb-cookie__azioni">
    <button type="button" id="gbCookieRifiuta" class="gb-cookie__btn gb-cookie__btn--ghost">
      {d.cookie.rifiuta}
    </button>
    <button type="button" id="gbCookieAccetta" class="gb-cookie__btn">
      {d.cookie.accetta}
    </button>
  </div>
</div>

<script is:inline>
  (function () {
    var NOME = 'gb_cookie_consent';
    var GIORNI = 180;
    var banner = document.getElementById('gbCookieBanner');
    if (!banner) return;

    function leggi() {
      var m = document.cookie.match('(?:^|; )' + NOME + '=([^;]*)');
      return m ? decodeURIComponent(m[1]) : null;
    }
    function scrivi(valore) {
      var s = new Date();
      s.setTime(s.getTime() + GIORNI * 864e5);
      document.cookie = NOME + '=' + valore + '; expires=' + s.toUTCString() + '; path=/; SameSite=Lax';
    }
    function aggiorna(ok) {
      if (typeof window.gtag === 'function') {
        window.gtag('consent', 'update', { analytics_storage: ok ? 'granted' : 'denied' });
      }
    }
    function scegli(ok) {
      scrivi(ok ? 'accepted' : 'rejected');
      aggiorna(ok);
      banner.remove();
    }

    var scelta = leggi();
    if (scelta === 'accepted') { aggiorna(true); }
    else if (scelta !== 'rejected') { banner.hidden = false; }

    document.getElementById('gbCookieAccetta').addEventListener('click', function () { scegli(true); });
    document.getElementById('gbCookieRifiuta').addEventListener('click', function () { scegli(false); });

    // Ogni clic sullo smistamento e' un evento: e' l'unica misura che dice se il centralino funziona.
    document.querySelectorAll('[data-gb-dest]').forEach(function (a) {
      a.addEventListener('click', function () {
        if (typeof window.gtag === 'function') {
          window.gtag('event', 'smistamento_click', {
            destinazione: a.getAttribute('data-gb-dest'),
            lingua: document.documentElement.lang,
          });
        }
      });
    });
  })();
</script>

<style>
  .gb-cookie {
    position: fixed;
    left: 1rem;
    right: 1rem;
    bottom: 1rem;
    z-index: 2000;
    max-width: 680px;
    margin-inline: auto;
    background: var(--gb-paper);
    border-radius: var(--gb-raggio);
    box-shadow: 0 16px 44px rgba(10, 37, 64, 0.28);
    padding: var(--gb-sp-3);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gb-sp-2);
  }
  .gb-cookie[hidden] { display: none; }
  .gb-cookie__testo { flex: 1 1 320px; margin: 0; font-size: 0.92rem; }
  .gb-cookie__testo a { color: var(--gb-acqua-scuro); text-decoration: underline; }
  .gb-cookie__azioni { display: flex; gap: var(--gb-sp-1); flex-shrink: 0; }
  .gb-cookie__btn {
    min-height: 44px;
    padding-inline: var(--gb-sp-3);
    border: 1px solid var(--gb-acqua);
    border-radius: 999px;
    background: var(--gb-acqua);
    color: var(--gb-ink);
    font: inherit;
    font-weight: 600;
    cursor: pointer;
  }
  .gb-cookie__btn--ghost { background: transparent; color: var(--gb-text); border-color: #c4d1e0; }
  @media (max-width: 576px) {
    .gb-cookie { flex-direction: column; align-items: stretch; }
    .gb-cookie__azioni { justify-content: flex-end; }
  }
</style>
```

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

```astro
---
import { t, percorso, type Lingua } from '../i18n';
import Footer from './Footer.astro';

interface Props { lang: Lingua }
const { lang } = Astro.props;
const d = t(lang);
---
<main class="gb-wrap gb-privacy">
  <p class="gb-privacy__torna"><a href={percorso(lang)}>&larr; {d.hero.titolo}</a></p>
  <h1>{d.privacy.titolo}</h1>
  {d.privacy.corpo.map((p) => <p>{p}</p>)}
</main>
<Footer lang={lang} />

<style>
  .gb-privacy { padding-block: var(--gb-sp-5); max-width: 72ch; }
  .gb-privacy__torna a { color: var(--gb-acqua-scuro); text-decoration: none; font-weight: 600; }
  .gb-privacy p { color: var(--gb-text-soft); }
</style>
```

- [ ] **Step 7: Scrivere le tre pagine privacy**

`src/pages/privacy.astro`:
```astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import PaginaPrivacy from '../components/PaginaPrivacy.astro';
import { t } from '../i18n';
const d = t('it');
---
<BaseLayout lang="it" sotto="privacy" title={d.privacy.titolo} description={d.privacy.descrizione}>
  <PaginaPrivacy lang="it" />
</BaseLayout>
```

`src/pages/en/privacy.astro`:
```astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import PaginaPrivacy from '../../components/PaginaPrivacy.astro';
import { t } from '../../i18n';
const d = t('en');
---
<BaseLayout lang="en" sotto="privacy" title={d.privacy.titolo} description={d.privacy.descrizione}>
  <PaginaPrivacy lang="en" />
</BaseLayout>
```

`src/pages/fr/privacy.astro`:
```astro
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import PaginaPrivacy from '../../components/PaginaPrivacy.astro';
import { t } from '../../i18n';
const d = t('fr');
---
<BaseLayout lang="fr" sotto="privacy" title={d.privacy.titolo} description={d.privacy.descrizione}>
  <PaginaPrivacy lang="fr" />
</BaseLayout>
```

- [ ] **Step 8: Agganciare Analytics e banner**

In ognuna delle tre home, dentro `<BaseLayout>`: `<Analytics slot="head" />`.
In `src/components/Pagina.astro`: import `BannerCookie` e inserire `<BannerCookie lang={lang} />` **dopo** `<Footer lang={lang} />`, così lo script trova già nel DOM le card con `data-gb-dest`.

- [ ] **Step 9: Lanciare i test e verificare che passino**

```bash
cp .env.example .env   # e mettere dentro l'ID GA4 vero
npm test
```
Expected: tutti verdi, comprese le 6 asserzioni sul consenso.

- [ ] **Step 10: Verifica manuale nel browser**

```bash
npm run build && npm run preview
```

Aprire `http://localhost:4321/` in una finestra anonima e controllare, negli strumenti di sviluppo:
1. il banner compare;
2. prima di scegliere, nessun cookie che inizia per `_ga`;
3. dopo "Accetta", compare `_ga_<ID>` e la chiamata a `google-analytics.com/g/collect` porta `gcs=G1-1`;
4. cliccando una card dello smistamento parte un evento `smistamento_click` con il parametro `destinazione`.

- [ ] **Step 11: Commit**

```bash
git add -A
git commit -m "Analytics con Consent Mode, banner cookie, privacy e tracciamento dello smistamento"
```

---

### Task 9: Pubblicazione su Cloudflare Pages

**Files:**
- Create: `public/_headers`
- Create: `README.md`
- Modify: `docs/superpowers/plans/2026-09-07-landing-gruppo-benza.md` (spuntare i passi)

**Interfaces:**
- Consumes: la build prodotta dai task precedenti.
- Produces: il sito in produzione su `https://www.gruppobenza.it`.

- [ ] **Step 1: Scrivere `public/_headers`**

```
/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  X-Frame-Options: DENY
  Permissions-Policy: geolocation=(), microphone=(), camera=()

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

- [ ] **Step 2: Scrivere `README.md`**

```markdown
# gruppobenza.it

Landing del Gruppo Benza: racconta il gruppo e smista i visitatori verso i quattro
siti delle due societa'. Astro statico, tre lingue, pubblicata su Cloudflare Pages.

## Comandi

- `npm run dev` - sviluppo su http://localhost:4321
- `npm run build` - build statica in `dist/`
- `npm test` - build piu' test sull'HTML costruito
- `npm run preview` - serve `dist/` in locale

## Configurazione

Copiare `.env.example` in `.env` e mettere l'ID misurazione GA4 in `PUBLIC_GA_ID`.
Senza quella variabile la pagina si costruisce lo stesso, senza Analytics.

## Contenuti

Tutti i testi visibili stanno in `src/i18n/it.ts`, `en.ts`, `fr.ts`. Per cambiare
una frase si tocca solo il dizionario, mai un componente.

## Deploy

Ogni push su `main` fa partire la build su Cloudflare Pages. Comando di build
`npm run build`, cartella di output `dist`, variabile d'ambiente `PUBLIC_GA_ID`
da impostare nel pannello Cloudflare.
```

- [ ] **Step 3: Verificare che i test passino e pubblicare il repo su GitHub**

```bash
npm test
git add -A
git commit -m "Intestazioni HTTP e istruzioni del progetto"
gh repo create gruppobenza --private --source=. --push
```

- [ ] **Step 4: Registrare il dominio**

Su Aruba registrare `gruppobenza.it`, **intestandolo a Benza Water Storage SRL**, e portare i nameserver su quelli che Cloudflare indica al passo successivo.

- [ ] **Step 5: Creare il progetto su Cloudflare Pages**

Nel pannello Cloudflare: Workers & Pages, Create, Pages, Connect to Git, scegliere il repo `gruppobenza`.
- Framework preset: Astro
- Build command: `npm run build`
- Build output directory: `dist`
- Variabile d'ambiente: `PUBLIC_GA_ID` con l'ID della proprietà GA4

- [ ] **Step 6: Collegare il dominio**

In Cloudflare, aggiungere il sito `gruppobenza.it`, copiare i due nameserver e impostarli su Aruba. Poi, nel progetto Pages, Custom domains: aggiungere `www.gruppobenza.it` e `gruppobenza.it` con reindirizzamento sul `www`.

- [ ] **Step 7: Verificare la produzione**

Con il sito raggiungibile:
1. `curl -sI https://www.gruppobenza.it/ | head -5` deve dare `200`;
2. `curl -s https://www.gruppobenza.it/robots.txt | grep Sitemap` deve mostrare l'indirizzo della sitemap;
3. aprire `https://www.gruppobenza.it/` in finestra anonima e ripetere le quattro verifiche del task 8, passo 10;
4. controllare che `/en/` e `/fr/` rispondano e mostrino la lingua giusta;
5. lanciare Lighthouse sulla pagina italiana: prestazioni, accessibilità e best practice devono stare a 100, e l'LCP sotto 1,5 s. Se l'accessibilità scende, il sospetto numero uno è il contrasto del verde: su fondo scuro va usato `--gb-giardino-chiaro`, mai `--gb-giardino`.

- [ ] **Step 8: Chiudere il giro sugli altri due siti**

Non appena la pagina è online, sui siti esistenti:
1. in `sito_26/footer_pub.php` aggiungere un link a `https://www.gruppobenza.it/`;
2. in `sito_26/header_pub.php`, dentro il blocco JSON-LD `Organization`, aggiungere
   `"parentOrganization": { "@id": "https://www.gruppobenza.it/#gruppo" }`;
3. chiedere a Davide di far aggiungere lo stesso link nel footer di `benzasrl.it`.

Senza questo passo il triangolo di menzioni resta aperto e metà del valore GEO della pagina non si realizza.

- [ ] **Step 9: Commit finale e registrazione del lavoro**

```bash
git add -A
git commit -m "Pubblicazione su Cloudflare Pages"
git push
```

Poi aggiornare `docs/REFACTORING_LOG.md` di `sito_26` con le modifiche del passo 8, e la memoria `project_gruppobenza_landing` con l'indirizzo di produzione e l'ID della proprietà GA4.

---

### Task 10: Le tre fotografie

**Files:**
- Create: `src/assets/hero-lago.jpg`, `src/assets/anima-acqua.jpg`, `src/assets/anima-giardino.jpg`
- Modify: `src/components/Hero.astro`, `src/components/DueAnime.astro`
- Test: `tests/immagini.test.ts`

**Interfaces:**
- Consumes: `d.immagini.heroAlt`, `d.immagini.acquaAlt`, `d.immagini.giardinoAlt` dai dizionari del task 2.
- Produces: niente per i task successivi. È l'ultimo passo prima della pubblicazione e si può fare anche dopo il task 9, senza rompere niente: la pagina sta in piedi senza foto.

- [ ] **Step 1: Raccogliere le tre immagini dai siti del gruppo**

Sono materiale del cliente, quindi non c'è un problema di diritti. Salvare in `src/assets/` con questi nomi esatti:

- `hero-lago.jpg`: una veduta dall'alto di un lago artificiale, dalla gallery di `https://www.benza.it/` (sezione laghi). Serve un'immagine orizzontale, almeno 1920 px di larghezza.
- `anima-acqua.jpg`: vasche prefabbricate di acciaio, dalla gallery `Vasche prefabbricate di acciaio zincato` di `https://www.benza.it/`. Almeno 1200 px.
- `anima-giardino.jpg`: arredo esterno a bordo piscina, dalla home di `https://www.benzasrl.it/`. Almeno 1200 px.

I due mondi visivi sono diversi, render di arredo contro foto di cantiere. Per tenerli insieme, tutte e tre le immagini vanno ritagliate in 16:9 e ricevono lo stesso trattamento in CSS (vedi passo 3).

- [ ] **Step 2: Scrivere il test `tests/immagini.test.ts`**

```ts
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { load } from 'cheerio';

const dom = (p: string) => load(readFileSync(new URL(`../dist/${p}`, import.meta.url).pathname, 'utf-8'));

describe('immagini', () => {
  const $ = dom('index.html');

  it('ci sono tre foto', () => {
    expect($('main img, .gb-hero img').length).toBe(3);
  });

  it('ognuna ha testo alternativo e misure, e l hero non e pigra', () => {
    $('img').each((_, el) => {
      const img = $(el);
      expect((img.attr('alt') ?? '').length).toBeGreaterThan(5);
      expect(img.attr('width')).toBeTruthy();
      expect(img.attr('height')).toBeTruthy();
    });
    expect($('.gb-hero img').attr('loading')).toBe('eager');
    expect($('.gb-anima img').first().attr('loading')).toBe('lazy');
  });

  it('le serve in formato moderno', () => {
    const src = $('img').map((_, el) => $(el).attr('src')).get().join(' ');
    expect(src).toMatch(/\.(avif|webp)/);
  });

  it('usa il testo alternativo della lingua giusta', () => {
    expect(dom('fr/index.html')('.gb-hero img').attr('alt')).toContain('lac');
  });
});
```

- [ ] **Step 3: Lanciare il test e verificare che fallisca**

Run: `npm test`
Expected: FAIL, `expect($('main img, .gb-hero img').length).toBe(3)` riceve 0.

- [ ] **Step 4: Mettere la foto nell'hero**

In `src/components/Hero.astro`, aggiungere in cima al frontmatter:

```ts
import { Image } from 'astro:assets';
import heroLago from '../assets/hero-lago.jpg';
```

e dentro `.gb-hero`, come primo figlio della sezione, prima di `<div class="gb-wrap">`:

```astro
<Image
  class="gb-hero__foto"
  src={heroLago}
  alt={d.immagini.heroAlt}
  widths={[720, 1280, 1920]}
  sizes="100vw"
  format="avif"
  quality={70}
  loading="eager"
  fetchpriority="high"
/>
```

`loading="eager"` va messo a mano: il componente `Image` di Astro mette `lazy` di default, e sull'immagine che fa da LCP è esattamente quello che non si vuole.

e nel blocco `<style>`:

```css
  .gb-hero { position: relative; isolation: isolate; overflow: hidden; }
  .gb-hero__foto {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: -1;
    filter: saturate(0.75) brightness(0.5);
  }
```

Il filtro comune è quello che tiene insieme i due mondi visivi: le foto di cantiere e i render di arredo, desaturati e scuriti allo stesso modo, smettono di litigare.

- [ ] **Step 5: Mettere le due foto nelle card delle anime**

In `src/components/DueAnime.astro`, nel frontmatter:

```ts
import { Image } from 'astro:assets';
import fotoAcqua from '../assets/anima-acqua.jpg';
import fotoGiardino from '../assets/anima-giardino.jpg';
```

e cambiare l'array `blocchi` così:

```ts
const blocchi = [
  { chiave: 'acqua' as const, dati: d.anime.acqua, foto: fotoAcqua, alt: d.immagini.acquaAlt },
  { chiave: 'giardino' as const, dati: d.anime.giardino, foto: fotoGiardino, alt: d.immagini.giardinoAlt },
];
```

Dentro `<article>`, come primo elemento prima di `<p class="gb-anima__etichetta">`:

```astro
<Image
  class="gb-anima__foto"
  src={foto}
  alt={alt}
  widths={[400, 800]}
  sizes="(max-width: 700px) 100vw, 45vw"
  format="avif"
  quality={70}
  loading="lazy"
/>
```

e nel `<style>`:

```css
  .gb-anima { padding-top: 0; overflow: hidden; }
  .gb-anima__foto {
    width: calc(100% + var(--gb-sp-3) * 2);
    margin: 0 calc(var(--gb-sp-3) * -1) var(--gb-sp-3);
    aspect-ratio: 16 / 9;
    object-fit: cover;
    filter: saturate(0.85);
  }
```

Nel `map` sopra, la destrutturazione va aggiornata a `{({ chiave, dati, foto, alt }) => (`.

- [ ] **Step 6: Lanciare i test e verificare che passino**

Run: `npm test`
Expected: tutti verdi, comprese le 4 asserzioni sulle immagini.

- [ ] **Step 7: Ricontrollare il contrasto dell'hero**

Con la foto di sfondo il testo bianco deve restare leggibile. Con Lighthouse o con il controllo contrasto degli strumenti di sviluppo, verificare che il titolo e il sommario dell'hero stiano sopra 4.5:1 sul punto più chiaro della foto. Se non ci arrivano, portare `brightness` da `0.5` a `0.4`, non alleggerire il testo.

- [ ] **Step 8: Commit**

```bash
git add -A
git commit -m "Fotografie dell'hero e delle due anime, con trattamento comune"
```

---

## Note per chi implementa

- **Non inventare testi.** Se manca una frase, sta nel dizionario o non deve esistere. I fatti storici vengono dalla pagina "Azienda" di benzasrl.it e non vanno arricchiti a fantasia: un dato inventato su una pagina che dichiara dati strutturati è peggio di un dato mancante.
- **Nessuna sezione può dipendere da JavaScript per essere letta.** Se una sezione sparisce con JS disattivato, è sbagliata.
- **Il verde `#006000` non passa il contrasto su fondo scuro.** Su `--gb-ink` si usa `--gb-giardino-chiaro`.
- **Le foto arrivano nel task 10, non prima.** I task da 1 a 9 producono una pagina tipografica che sta in piedi da sola. È voluto: così la pubblicazione non aspetta il ritaglio delle immagini, e se una foto si rivela inutilizzabile la pagina non si rompe.
- **Il wordmark bianco del gruppo non esiste ancora.** L'hero è tipografico di proposito e non lo richiede. Quando ci sarà, va messo sopra l'occhiello dell'hero e nel footer, con `alt="Gruppo Benza"`, e a quel punto l'`h1` testuale diventa `class="sr-only"` invece di sparire: il titolo deve restare nel testo, altrimenti si perde per i motori e per i modelli.
- **La mappa incorporata è stata scartata.** Lo spec parlava di una mappa statica; in fase di piano è diventata due link a Google Maps, che non richiedono chiave API, non scaricano niente da terzi e non aggiungono cookie da dichiarare.
