# Sito bg-perspective.it, fase 2: piano di lavoro

> **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:** nella scheda del gioco compaiono i video tutorial in italiano (accordion chiuso, caricamento da YouTube solo al click) e i giochi della stessa famiglia già sul sito; i tag categoria e meccanica diventano link a pagine nuove `/categoria/<slug>` e `/meccanica/<slug>`, che stanno anche in sitemap.

**Architecture:** due colonne json nuove in `giochi` (`video`, `collegati`) riempite da `sito.py pubblica` leggendo `portale.dati_gioco`; il sito le legge come le altre colonne json. Le pagine per tag sono un'unica route con param matcher (`/[tipo=tag]/[slug]`, `tipo` ∈ {categoria, meccanica}) che filtra in memoria `visibili()`, come già fa `perAutore`. Nessuna tabella nuova.

**Tech Stack:** SvelteKit 2 / Svelte 5 (runes), `@libsql/client`, Vitest; Python 3 (`codice/sito.py`, `codice/portale.py`); Turso CLI (loggato come `paoloalby`); Vercel CLI (scope `paoloalbys-projects`).

Specifica: `docs/superpowers/specs/2026-09-14-sito-fase2-video-meccaniche-design.md`. Anteprime approvate: `_instagram/sito-concept/F-post-v2.html` (scheda) e `G-meccanica.html` (pagina per tag). Il CSS dei blocchi nuovi si copia da lì.

## Global Constraints

- Cartella del sito `_instagram/sito/`, comando Python `_instagram/codice/sito.py`, database Turso `bg-perspective`, progetto Vercel `bg-perspective`, dominio `https://bg-perspective.it`.
- Nel sito la parola è "scheda", mai "recensione". Testi in italiano. Token colore già in `app.css`: `--legno --legno-luce --carta --ambra --carta-12 --carta-45 --carta-70`.
- `video`: json `[{"url","id","titolo","autore"}]`, solo `lingua == "Italian"` e (`galleria == "instructional"` oppure "tutorial" nel titolo, senza distinzione di maiuscole), senza duplicati per `id` (id YouTube ricavato dall'url).
- `collegati`: json `[{"bgg_id","nome","tipo"}]` dal campo `collegati` del portale (`relazione` ∈ base, espansione, precedente).
- Nessuna chiamata a YouTube prima del click: l'anteprima è `https://i.ytimg.com/vi/<id>/hqdefault.jpg`, l'iframe `https://www.youtube-nocookie.com/embed/<id>?autoplay=1` si monta solo al click. L'intera riga del video è cliccabile.
- Nota sotto i video, testuale: "I video sono di chi li ha fatti. Si caricano da YouTube solo quando li apri."
- Etichette famiglia: base → "Gioco base", espansione → "Espansione", precedente → "Precedente"; card con `padding-top:30px` e etichetta in alto a destra.
- Sezioni vuote non si stampano (né "Video tutorial" né "Della stessa famiglia"). Pagine per tag vuote → 404.
- Ogni query pubblica passa da `visibili()`/`perSlug()` che filtrano `pubblicato_il <= now`: le bozze restano invisibili anche nelle pagine per tag e nella famiglia.
- Chiavi solo in `_instagram/sito/.env` (ignorato) e su Vercel. Mai stamparle, mai `cat .env`.
- Commit: solo i file toccati, sempre `git add <percorsi>` espliciti, mai `git add -A` (il repo ha molti file non correlati modificati). Messaggi in italiano, chiusi dalle righe di attribuzione della sessione.
- Verifiche locali: `cd _instagram/sito && npx vitest run` per i test; `npx vite dev --port 5173` per il server (legge `.env` da solo) e `curl` sulle pagine. Il server dev va fermato a fine task.

---

### Task 1: dati. Colonne nuove, `sito.py`, ricarico delle 15 righe

**Files:**
- Modify: `_instagram/sito/schema.sql`
- Modify: `_instagram/codice/sito.py` (funzioni `arricchisci`, `pubblica`, `demo`; funzione nuova `video_it`)
- Turso: `alter table` sul database `bg-perspective`

**Interfaces:**
- Produces: colonne `giochi.video` e `giochi.collegati` (json, testo) riempite per tutte le 15 righe; `video_it(lista) -> list[dict]` in `sito.py`.

- [ ] **Step 1: schema locale**

In `_instagram/sito/schema.sql` sostituire la riga
```sql
  partite integer, durate text, foto text not null, alt text, scatola_piatta text, scatola_3d text,
```
con
```sql
  partite integer, durate text, video text, collegati text, foto text not null, alt text, scatola_piatta text, scatola_3d text,
```

- [ ] **Step 2: schema su Turso**

```bash
turso db shell bg-perspective "alter table giochi add column video text"
turso db shell bg-perspective "alter table giochi add column collegati text"
turso db shell bg-perspective "select count(*) as righe, count(video) as con_video from giochi"
```
Atteso: `15 | 0`. Se il secondo `alter` dice che la colonna esiste già, va bene: non è un errore.

- [ ] **Step 3: test Python (demo) che fallisce**

In `_instagram/codice/sito.py`, dentro `demo()`, dopo la riga `assert d["durate"] == [...]` aggiungere:

```python
    assert d["video"] == [{"url": "https://www.youtube.com/watch?v=Em0hLjA_iMw", "id": "Em0hLjA_iMw",
                           "titolo": "Tutorial in italiano di Cryptid", "autore": "Cercatore di Perle"}]   # la Recensione Minuta resta fuori
    assert d["collegati"] == []
    w = arricchisci({}, 473508, usa_cache=True)   # Wingspan Pocket: video solo in inglese, un solo parente
    assert w["video"] == [] and w["collegati"] == [{"bgg_id": 266192, "nome": "Wingspan", "tipo": "precedente"}]
    assert video_it([{"url": "https://youtu.be/abcdefghijk", "titolo": "Tutorial", "lingua": "Italian", "galleria": "review"},
                     {"url": "https://www.youtube.com/watch?v=abcdefghijk", "titolo": "Tutorial", "lingua": "Italian", "galleria": "instructional"},
                     {"url": "https://www.youtube.com/watch?v=zzzzzzzzzzz", "titolo": "How to play", "lingua": "English", "galleria": "instructional"}]) \
        == [{"url": "https://youtu.be/abcdefghijk", "id": "abcdefghijk", "titolo": "Tutorial", "autore": None}]
```

- [ ] **Step 4: eseguire la demo, deve fallire**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo/_instagram" && python3 codice/sito.py demo
```
Atteso: `KeyError: 'video'`.

- [ ] **Step 5: implementazione**

Dopo `def handle_autore` (o subito prima di `def arricchisci`) aggiungere:

```python
def video_it(lista):
    """Solo i tutorial in italiano: galleria "instructional" o "tutorial" nel titolo. Uno per id YouTube."""
    out, visti = [], set()
    for v in lista or []:
        m = re.search(r"(?:v=|youtu\.be/|/embed/)([\w-]{11})", v.get("url") or "")
        if not m or v.get("lingua") != "Italian": continue
        if v.get("galleria") != "instructional" and "tutorial" not in (v.get("titolo") or "").lower(): continue
        if m.group(1) in visti: continue
        visti.add(m.group(1)); out.append({"url": v["url"], "id": m.group(1), "titolo": v["titolo"], "autore": v.get("autore")})
    return out
```

In `arricchisci`, dentro `d.update(...)`, dopo la riga `autori=[...]` aggiungere:

```python
        video=video_it(p.get("video")),
        collegati=[{"bgg_id": c["gioco"]["id"], "nome": c["gioco"]["nome"], "tipo": c["relazione"]} for c in p.get("collegati") or []],
```

In `pubblica`, nel dizionario `cols`, dopo `partite=d["partite"], durate=j(d["durate"]),` aggiungere:

```python
        video=j(d["video"]), collegati=j(d["collegati"]),
```

- [ ] **Step 6: demo verde**

```bash
python3 codice/sito.py demo
```
Atteso: `demo ok`.

- [ ] **Step 7: ricarico delle 15 righe già online**

`pubblica` riscrive tutte le colonne (upsert su slug). Per le righe già uscite serve `--gia-uscito`, se no i permalink tornano a null: si decide dalla data (`storico.tsv`, colonna 2, in UTC) confrontata con l'ora di adesso. Per tre post (post1, post5, post8) il riconoscimento della chicca chiede quale paragrafo: la risposta giusta è `5`, gli altri non leggono lo stdin.

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo/_instagram"
ORA=$(date -u +%FT%TZ)
while IFS=$'\t' read -r post data ig th stato; do
  flag=""; [[ "$data" < "$ORA" ]] && flag="--gia-uscito"
  printf '5\n' | python3 codice/sito.py pubblica "$post" --data "$data" --ig "$ig" --th "$th" $flag --senza-deploy || { echo "FALLITO $post"; break; }
done < sito/storico.tsv
```
Atteso: 15 righe `ok <slug> <data>`, nessun `FALLITO`. Se compare un prompt diverso da quello della chicca (per esempio `id BGG di «…»`), fermarsi e segnalarlo: non va risposto con `5`.

- [ ] **Step 8: controllo su Turso**

```bash
turso db shell bg-perspective "select count(*) as righe, count(video) as con_video, sum(video != '[]') as con_tutorial, sum(instagram_url is not null) as con_permalink from giochi"
python3 codice/sito.py verifica cryptid | grep -E '^(video|collegati|chicca|instagram_url):'
```
Atteso: `15 | 15 | n>0 | 6` (i cinque usciti più post6 se il ricarico avviene dopo le 19:00 di oggi, altrimenti 5); Cryptid con un video (`Em0hLjA_iMw`), `collegati: []`, la chicca che inizia con "Gli indizi" e il permalink Instagram valorizzato.

- [ ] **Step 9: commit**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo"
git add _instagram/sito/schema.sql _instagram/codice/sito.py
git commit -m "sito fase 2: colonne video e collegati, sito.py le riempie dal portale"
```

---

### Task 2: lettura dal database, filtro per tag, sitemap

**Files:**
- Create: `_instagram/sito/src/lib/tag.js`
- Create: `_instagram/sito/src/lib/tag.test.js`
- Modify: `_instagram/sito/src/lib/db.js`
- Modify: `_instagram/sito/src/routes/sitemap.xml/+server.js`

**Interfaces:**
- Produces: `COLONNA = { categoria: 'categorie', meccanica: 'meccaniche' }`, `conTag(giochi, colonna, slug)`, `nomeTag(giochi, colonna, slug)` in `$lib/tag.js`; `perTag(tipo, slug)` in `$lib/db.js`; `visibili()` restituisce anche `bgg_id` e `meccaniche`; `apri` parsa anche `video` e `collegati` (array vuoto se null).

- [ ] **Step 1: test che fallisce**

`_instagram/sito/src/lib/tag.test.js`:

```js
import { describe, it, expect } from 'vitest';
import { conTag, nomeTag, COLONNA } from './tag.js';

const G = [
  { slug: 'a', categorie: ['Deduttivo'], meccaniche: ['Griglia esagonale', 'Deduzione'] },
  { slug: 'b', categorie: ['Piazzamento lavoratori'], meccaniche: ['Deduzione'] },
  { slug: 'c', categorie: [], meccaniche: [] }
];

describe('tag', () => {
  it('filtra per slug del tag', () => {
    expect(conTag(G, 'meccaniche', 'deduzione').map((g) => g.slug)).toEqual(['a', 'b']);
    expect(conTag(G, 'categorie', 'piazzamento-lavoratori').map((g) => g.slug)).toEqual(['b']);
    expect(conTag(G, 'categorie', 'inesistente')).toEqual([]);
  });
  it('ritrova il nome originale del tag', () => {
    const t = conTag(G, 'meccaniche', 'griglia-esagonale');
    expect(nomeTag(t, 'meccaniche', 'griglia-esagonale')).toBe('Griglia esagonale');
    expect(nomeTag([], 'meccaniche', 'x')).toBeUndefined();
  });
  it('mappa il segmento dell’URL sulla colonna', () => {
    expect(COLONNA.categoria).toBe('categorie');
    expect(COLONNA.meccanica).toBe('meccaniche');
  });
});
```

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

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo/_instagram/sito" && npx vitest run src/lib/tag.test.js
```
Atteso: FAIL, `Failed to resolve import "./tag.js"`.

- [ ] **Step 3: `tag.js`**

```js
import { slugifica } from './testo.js';
export const COLONNA = { categoria: 'categorie', meccanica: 'meccaniche' };
export const conTag = (giochi, colonna, slug) => giochi.filter((g) => g[colonna].some((t) => slugifica(t) === slug));
export const nomeTag = (giochi, colonna, slug) => giochi[0]?.[colonna].find((t) => slugifica(t) === slug);
```

- [ ] **Step 4: test verde**

```bash
npx vitest run
```
Atteso: tutti i file PASS (buffer, db, testo, tag).

- [ ] **Step 5: `db.js`**

Sostituire il file con:

```js
import { createClient } from '@libsql/client';
import { env } from '$env/dynamic/private';
import { VISIBILE } from './sql.js';
import { COLONNA, conTag } from './tag.js';
export const db = createClient({ url: env.TURSO_DATABASE_URL, authToken: env.TURSO_AUTH_TOKEN });
export const adesso = () => new Date().toISOString();
const J = ['autori', 'categorie', 'meccaniche', 'durate', 'foto', 'alt', 'video', 'collegati'];
const apri = (r) => { const g = { ...r }; for (const k of J) g[k] = r[k] ? JSON.parse(r[k]) : []; return g; };
const q = async (sql, args = []) => (await db.execute({ sql, args })).rows.map(apri);
export const visibili = () => q(`select slug,bgg_id,titolo,autori,editore_it,editore_slug,scatola_piatta,pubblicato_il,categorie,meccaniche from giochi where ${VISIBILE} order by datetime(pubblicato_il) desc`, [adesso()]);
export const ultimo = async () => (await q(`select * from giochi where ${VISIBILE} order by datetime(pubblicato_il) desc limit 1`, [adesso()]))[0];
export const perSlug = async (slug) => (await q(`select * from giochi where slug = ? and ${VISIBILE}`, [slug, adesso()]))[0];
export const perEditore = (slug) => q(`select slug,titolo,autori,editore_it,editore_slug,editore_handle,scatola_piatta,pubblicato_il from giochi where editore_slug = ? and ${VISIBILE} order by datetime(pubblicato_il) desc`, [slug, adesso()]);
export const perAutore = async (slug) => (await visibili()).filter((g) => g.autori.some((a) => a.slug === slug));
export const perTag = async (tipo, slug) => conTag(await visibili(), COLONNA[tipo], slug);
```

- [ ] **Step 6: sitemap**

Sostituire `_instagram/sito/src/routes/sitemap.xml/+server.js` con:

```js
import { visibili } from '$lib/db.js';
import { slugifica } from '$lib/testo.js';
import { env } from '$env/dynamic/public';

export const GET = async () => {
  const B = env.PUBLIC_BASE_URL, g = await visibili();
  const url = new Set([
    `${B}/`,
    ...g.map((x) => `${B}/gioco/${x.slug}`),
    ...g.map((x) => `${B}/editore/${x.editore_slug}`),
    ...g.flatMap((x) => x.autori.map((a) => `${B}/autore/${a.slug}`)),
    ...g.flatMap((x) => x.categorie.map((c) => `${B}/categoria/${slugifica(c)}`)),
    ...g.flatMap((x) => x.meccaniche.map((m) => `${B}/meccanica/${slugifica(m)}`))
  ]);
  const xml = `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${[...url].map((u) => `<url><loc>${u}</loc></url>`).join('')}</urlset>`;
  return new Response(xml, { headers: { 'Content-Type': 'application/xml' } });
};
```

- [ ] **Step 7: controllo con il server dev**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo/_instagram/sito" && (npx vite dev --port 5173 > /tmp/sito-dev.log 2>&1 &) && sleep 4
curl -s localhost:5173/sitemap.xml | grep -o '/meccanica/[a-z0-9-]*' | sort -u | head
curl -s localhost:5173/sitemap.xml | grep -o '/categoria/[a-z0-9-]*' | sort -u | head
curl -s -o /dev/null -w '%{http_code}\n' localhost:5173/
curl -s -o /dev/null -w '%{http_code}\n' localhost:5173/gioco/cryptid
```
Atteso: URL `/meccanica/deduzione`, `/categoria/deduttivo` fra quelli elencati (Cryptid è visibile); home e scheda 200. Fermare il server: `pkill -f 'vite dev --port 5173'`.

- [ ] **Step 8: commit**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo"
git add _instagram/sito/src/lib/tag.js _instagram/sito/src/lib/tag.test.js _instagram/sito/src/lib/db.js _instagram/sito/src/routes/sitemap.xml/+server.js
git commit -m "sito fase 2: filtro per categoria e meccanica, pagine per tag in sitemap"
```

---

### Task 3: pagine `/categoria/[slug]` e `/meccanica/[slug]`

Una route sola con param matcher: `src/routes/[tipo=tag]/[slug]`. Il matcher accetta solo `categoria` e `meccanica`, quindi `/gioco/...`, `/autore/...`, `/editore/...` continuano ad andare alle loro route e qualsiasi altro primo segmento dà 404.

**Files:**
- Create: `_instagram/sito/src/params/tag.js`
- Create: `_instagram/sito/src/routes/[tipo=tag]/[slug]/+page.server.js`
- Create: `_instagram/sito/src/routes/[tipo=tag]/[slug]/+page.svelte`

**Interfaces:**
- Consumes: `perTag(tipo, slug)` da `$lib/db.js`; `COLONNA`, `nomeTag` da `$lib/tag.js`; `Testata` (prop `indietro`), `Tavolo` (prop `giochi`), `tipografico`.
- Produces: URL `/categoria/<slug>` e `/meccanica/<slug>` (usati dai tag della scheda nel Task 4).

- [ ] **Step 1: matcher**

`_instagram/sito/src/params/tag.js`:

```js
export const match = (p) => p === 'categoria' || p === 'meccanica';
```

- [ ] **Step 2: load**

`_instagram/sito/src/routes/[tipo=tag]/[slug]/+page.server.js`:

```js
import { error } from '@sveltejs/kit';
import { perTag } from '$lib/db.js';
import { COLONNA, nomeTag } from '$lib/tag.js';

export const load = async ({ params }) => {
  const giochi = await perTag(params.tipo, params.slug);
  if (!giochi.length) error(404);
  return { tipo: params.tipo, nome: nomeTag(giochi, COLONNA[params.tipo], params.slug), slug: params.slug, giochi };
};
```

- [ ] **Step 3: pagina**

`_instagram/sito/src/routes/[tipo=tag]/[slug]/+page.svelte` (stessa struttura di `editore/[slug]/+page.svelte`, senza la riga dell'handle, h1 in grassetto come nell'anteprima G):

```svelte
<script>
  import { env } from '$env/dynamic/public';
  import Testata from '$lib/Testata.svelte';
  import Tavolo from '$lib/Tavolo.svelte';
  import { tipografico } from '$lib/testo.js';
  let { data } = $props();
  const occhiello = $derived(data.tipo === 'categoria' ? 'Categoria' : 'Meccanica');
</script>

<svelte:head>
  <title>{tipografico(data.nome)} · bg_perspective</title>
  <meta name="description" content="{occhiello} {tipografico(data.nome)}: le schede dei giochi che abbiamo portato sul tavolo.">
  <link rel="canonical" href="{env.PUBLIC_BASE_URL}/{data.tipo}/{data.slug}">
</svelte:head>

<Testata indietro />
<main>
  <section class="hero">
    <div class="et">{occhiello}</div>
    <h1><b>{tipografico(data.nome)}</b></h1>
  </section>
  <div class="tit"><h2>Sul <b>tavolo</b></h2><small>{data.giochi.length} {data.giochi.length === 1 ? 'gioco' : 'giochi'}</small></div>
  <Tavolo giochi={data.giochi} />
</main>

<style>
  main{position:relative;z-index:1;padding:6px 16px 60px;max-width:1100px;margin:0 auto}
  .hero{margin:6px 0 18px}
  .hero h1{font-family:var(--display);font-weight:300;font-size:clamp(30px,8vw,54px);line-height:1;margin:0 0 8px;text-wrap:balance}
  .hero h1 b{font-weight:600}
  .hero .et{color:var(--ambra);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:600}

  .tit{display:flex;align-items:baseline;justify-content:space-between;margin:0 0 6px}
  .tit h2{font-family:var(--display);font-weight:300;font-size:22px;margin:0}
  .tit h2 b{font-weight:600}
  .tit small{color:var(--carta-45);font-size:12px;font-variant-numeric:tabular-nums}
</style>
```

- [ ] **Step 4: controllo con il server dev**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo/_instagram/sito" && (npx vite dev --port 5173 > /tmp/sito-dev.log 2>&1 &) && sleep 4
for u in /meccanica/deduzione /categoria/deduttivo /meccanica/non-esiste /altro/deduzione /editore/asmodee-italia /gioco/cryptid; do printf '%s ' "$u"; curl -s -o /dev/null -w '%{http_code}\n' "localhost:5173$u"; done
curl -s localhost:5173/meccanica/deduzione | grep -o -E '<title>[^<]*</title>|Meccanica|Cryptid' | sort -u
pkill -f 'vite dev --port 5173'
```
Atteso: 200, 200, 404, 404, 200 (o 404 se quello slug editore non esiste: non è un errore del task), 200; nella pagina `<title>Deduzione · bg_perspective</title>`, "Meccanica", "Cryptid". Nessun errore in `/tmp/sito-dev.log`.

- [ ] **Step 5: commit**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo"
git add _instagram/sito/src/params/tag.js "_instagram/sito/src/routes/[tipo=tag]"
git commit -m "sito fase 2: pagine per categoria e per meccanica"
```

---

### Task 4: scheda del gioco. Tag cliccabili, video tutorial, famiglia

**Files:**
- Modify: `_instagram/sito/src/routes/gioco/[slug]/+page.server.js`
- Modify: `_instagram/sito/src/routes/gioco/[slug]/+page.svelte`

**Interfaces:**
- Consumes: `g.video`, `g.collegati` (array, mai null, da `apri`), `visibili()` con `bgg_id`; `slugifica` da `$lib/testo.js`; URL `/categoria/<slug>` e `/meccanica/<slug>` del Task 3.
- Produces: `data.famiglia`: array di righe di `visibili()` più `tipo` (base | espansione | precedente).

- [ ] **Step 1: load**

Sostituire `_instagram/sito/src/routes/gioco/[slug]/+page.server.js` con:

```js
import { error } from '@sveltejs/kit';
import { perSlug, visibili } from '$lib/db.js';
export const load = async ({ params }) => {
  const g = await perSlug(params.slug);
  if (!g) error(404);
  const altri = (await visibili()).filter((x) => x.slug !== g.slug);
  const stessa = altri.filter((x) => x.categorie.some((c) => g.categorie.includes(c)));
  // solo i parenti che hanno una scheda visibile: gli altri non si nominano
  const famiglia = g.collegati.flatMap((c) => { const x = altri.find((y) => y.bgg_id === c.bgg_id); return x ? [{ ...x, tipo: c.tipo }] : []; });
  return { g, vicini: (stessa.length >= 2 ? stessa : altri).slice(0, 2), famiglia };
};
```

- [ ] **Step 2: script della pagina**

In `+page.svelte`, nello `<script>`:
- l'import diventa `import { tipografico, dataItaliana, slugifica } from '$lib/testo.js';`
- dopo `const vicini = $derived(data.vicini);` aggiungere:

```js
  const famiglia = $derived(data.famiglia);
  const TIPO = { base: 'Gioco base', espansione: 'Espansione', precedente: 'Precedente' };
  let aperto = $state(null);   // id del video il cui iframe è montato: uno alla volta
```

- [ ] **Step 3: tag cliccabili**

Sostituire il blocco

```svelte
    <div class="tag">
      {#each g.categorie as c}<span class="a">{tipografico(c)}</span>{/each}
      {#each g.meccaniche as m}<span>{tipografico(m)}</span>{/each}
    </div>
```
con
```svelte
    <div class="tag">
      {#each g.categorie as c}<a class="a" href="/categoria/{slugifica(c)}">{tipografico(c)}</a>{/each}
      {#each g.meccaniche as m}<a href="/meccanica/{slugifica(m)}">{tipografico(m)}</a>{/each}
    </div>
```
e nello `<style>` le due regole `.tag span{...}` e `.tag span.a{...}` con:
```css
  .tag a{font-size:12px;padding:5px 10px;border:1px solid var(--carta-45);border-radius:999px;color:var(--carta-70);transition:.2s}
  .tag a:hover{border-color:var(--carta);color:var(--carta)}
  .tag a.a{border-color:var(--ambra);color:var(--ambra)}
```

- [ ] **Step 4: sezioni video e famiglia**

Subito dopo la `</section>` di "La scatola" (prima di `<section class="dom">`) inserire:

```svelte
    {#if g.video.length}
      <section>
        <h2>Video tutorial</h2>
        <details class="video">
          <summary><span>Mostra i video tutorial <span class="pill">{g.video.length} in italiano</span></span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></summary>
          <ul>
            {#each g.video as v (v.id)}
              <li>
                <div class="ant">
                  {#if aperto === v.id}
                    <iframe src="https://www.youtube-nocookie.com/embed/{v.id}?autoplay=1" title={v.titolo} allow="autoplay; fullscreen" allowfullscreen></iframe>
                  {:else}
                    <img src="https://i.ytimg.com/vi/{v.id}/hqdefault.jpg" alt="" loading="lazy">
                  {/if}
                </div>
                <div class="t">
                  {#if aperto === v.id}{tipografico(v.titolo)}{:else}<a href={v.url} onclick={(e) => { e.preventDefault(); aperto = v.id; }}>{tipografico(v.titolo)}</a>{/if}
                  <small>{#if v.autore}{tipografico(v.autore)} · {/if}YouTube</small>
                </div>
              </li>
            {/each}
          </ul>
          <div class="nota">I video sono di chi li ha fatti. Si caricano da YouTube solo quando li apri.</div>
        </details>
      </section>
    {/if}

    {#if famiglia.length}
      <section class="fam">
        <h2>Della stessa famiglia</h2>
        <div class="vicini">
          {#each famiglia as v (v.slug)}
            <a href="/gioco/{v.slug}"><img src={img(v.slug, v.scatola_piatta)} alt=""><span>{tipografico(v.titolo)}<small>{tipografico(v.editore_it)}</small></span><small class="tipo">{TIPO[v.tipo] ?? v.tipo}</small></a>
          {/each}
        </div>
      </section>
    {/if}
```

Il link sul titolo è "steso" con un `::after` su tutta la riga (CSS sotto): tutta la riga è cliccabile e da tastiera, senza JavaScript porta a YouTube. Quando l'iframe è montato il link sparisce, così i click arrivano al player.

- [ ] **Step 5: CSS**

Nello `<style>`, dopo il blocco `/* vicini */` e prima della `@media`, aggiungere:

```css
  /* video tutorial */
  .video summary{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border:1px solid var(--carta-45);border-radius:999px;font-weight:500}
  .video summary::-webkit-details-marker{display:none}
  .video .pill{display:inline-block;margin-left:8px;padding:3px 9px;border-radius:999px;background:rgba(224,163,64,.15);color:var(--ambra);font-size:11px;font-weight:600;letter-spacing:.04em;vertical-align:middle}
  .video summary svg{transition:transform .25s}
  .video[open] summary{border-color:var(--ambra)}
  .video[open] summary svg{transform:rotate(180deg)}
  .video ul{list-style:none;margin:14px 0 0;padding:0;display:grid;gap:14px}
  .video li{position:relative;display:grid;grid-template-columns:150px 1fr;gap:14px;align-items:center;border:1px solid var(--carta-12);border-radius:12px;padding:10px;background:rgba(0,0,0,.2)}
  .video li:has(a:hover){border-color:var(--carta-45)}
  .video .ant{position:relative;aspect-ratio:16/9;border-radius:8px;overflow:hidden;background:#000}
  .video .ant img{width:100%;height:100%;object-fit:cover;display:block;opacity:.85}
  .video .ant:has(img):after{content:"";position:absolute;left:50%;top:50%;width:0;height:0;transform:translate(-40%,-50%);border-left:16px solid #fff;border-top:10px solid transparent;border-bottom:10px solid transparent;filter:drop-shadow(0 2px 6px rgba(0,0,0,.7))}
  .video .ant iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
  .video .t{font-weight:500;line-height:1.3}
  .video .t a::after{content:"";position:absolute;inset:0}
  .video .t small{display:block;color:var(--carta-45);font-size:12px;font-weight:400;margin-top:3px}
  .nota{font-size:12px;color:var(--carta-45);margin-top:10px}

  /* famiglia */
  .fam .vicini a{position:relative;padding-top:30px}
  .fam .vicini small.tipo{position:absolute;right:10px;top:10px;color:var(--ambra);font-size:10px;letter-spacing:.1em;text-transform:uppercase}
```

e nella `@media (max-width:420px)` esistente aggiungere `.video li{grid-template-columns:110px 1fr}`.

- [ ] **Step 6: controllo con il server dev**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo/_instagram/sito" && (npx vite dev --port 5173 > /tmp/sito-dev.log 2>&1 &) && sleep 4
curl -s localhost:5173/gioco/cryptid | grep -o -E 'href="/(categoria|meccanica)/[a-z0-9-]+"|Video tutorial|1 in italiano|i\.ytimg\.com/vi/Em0hLjA_iMw/hqdefault\.jpg|youtube-nocookie|Della stessa famiglia|Recensioni Minute' | sort | uniq -c
grep -i -E 'error|warn' /tmp/sito-dev.log | head
pkill -f 'vite dev --port 5173'
```
Atteso: i quattro link tag (`/categoria/deduttivo`, `/meccanica/deduzione`, `/meccanica/griglia-esagonale`, `/meccanica/plancia-modulare`), "Video tutorial", "1 in italiano", l'anteprima `Em0hLjA_iMw`; **nessuna** occorrenza di `youtube-nocookie` (l'iframe si monta solo al click), di "Della stessa famiglia" (Cryptid non ha parenti) né di "Recensioni Minute" (esclusa dal filtro). Nessun errore o warning a11y nel log.

- [ ] **Step 7: prova a mano nel browser**

Aprire `http://localhost:5173/gioco/cryptid` (server dev acceso) e verificare: l'accordion parte chiuso; aperto, la riga intera è cliccabile e monta il player; il tag "Deduttivo" porta a `/categoria/deduttivo`. Su una larghezza di 400 px la riga del video sta in due colonne 110 px + testo. Poi fermare il server.

- [ ] **Step 8: commit**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo"
git add "_instagram/sito/src/routes/gioco/[slug]/+page.server.js" "_instagram/sito/src/routes/gioco/[slug]/+page.svelte"
git commit -m "sito fase 2: tag cliccabili, video tutorial in italiano e famiglia nella scheda"
```

---

### Task 5: messa online, controlli, memoria

**Files:**
- Modify: `memoria/sito-bg-perspective.md` e la copia in `/Users/paolo/.claude/projects/-Users-paolo-Documents-Giochi-da-Tavolo/memory/sito-bg-perspective.md` (devono restare identiche)
- Modify: `.superpowers/sdd/progress.md` (fuori dal repo)

- [ ] **Step 1: test e deploy**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo/_instagram/sito" && npx vitest run && vercel deploy --prod --yes --scope paoloalbys-projects
```
Atteso: test verdi, deploy concluso con l'URL di produzione.

- [ ] **Step 2: controlli online**

```bash
for u in /meccanica/deduzione /categoria/deduttivo /meccanica/non-esiste /gioco/cryptid; do printf '%s ' "$u"; curl -s -o /dev/null -w '%{http_code}\n' "https://bg-perspective.it$u"; done
curl -s https://bg-perspective.it/gioco/cryptid | grep -o -E 'Video tutorial|1 in italiano|href="/meccanica/deduzione"' | sort -u
curl -s https://bg-perspective.it/sitemap.xml | grep -o -E '/(categoria|meccanica)/[a-z0-9-]+' | sort -u | wc -l
```
Atteso: 200, 200, 404, 200; le tre stringhe nella scheda; sitemap con più di zero URL per tag. Poi aprire `https://bg-perspective.it/gioco/cryptid` in Chrome per Paolo, in fondo alla scheda.

- [ ] **Step 3: memoria**

In entrambe le copie di `sito-bg-perspective.md`: la fase 2 passa da "approvata, da fare" a "online il 2026-09-14" (colonne `video`/`collegati` riempite da `pubblica`, pagine `/categoria/` e `/meccanica/`, accordion video, famiglia). Nella lista delle cose da fare togliere la fase 2. La regola "dopo ogni messa in coda su Buffer → `sito.py pubblica`" resta invariata: è lo stesso comando che ora riempie anche video e famiglia. In `.superpowers/sdd/progress.md` aggiungere le righe dei Task 1–5 della fase 2 con gli esiti.

- [ ] **Step 4: commit**

```bash
cd "/Users/paolo/Documents/Giochi da Tavolo"
git add memoria/sito-bg-perspective.md docs/superpowers/plans/2026-09-14-sito-fase2-video-meccaniche.md
git commit -m "sito fase 2 online: memoria e piano"
```

---

## Auto-revisione

Copertura della specifica: colonne `video` e `collegati` con i filtri e la deduplica (T1, `video_it`), riempimento da `pubblica` e ricarico dei 15 (T1 step 7), tag cliccabili (T4 step 3), sezione video con details chiuso, pill, riga intera cliccabile, anteprima ytimg, iframe youtube-nocookie solo al click, nota, sezione assente se vuota (T4 step 4–5), famiglia con match su `bgg_id` fra i visibili, etichette, padding 30 px, assente se vuota (T4 step 1, 4, 5), pagine per categoria e meccanica con struttura di editore, filtro in memoria, nome originale del tag, 404 se vuote (T2, T3), sitemap (T2 step 6). Fuori dalla specifica: nessuna descrizione delle meccaniche (l'anteprima G ne aveva una d'esempio, non si stampa).

Coerenza dei nomi: `video_it`, `arricchisci`, `pubblica` (T1); `COLONNA`, `conTag`, `nomeTag` (T2) usati in `perTag` (T2) e nel load di T3; `visibili()` restituisce `bgg_id` e `meccaniche` (T2) come richiesto da T3 (sitemap, filtro) e T4 (famiglia); `data.famiglia` prodotto in T4 step 1 e letto in T4 step 2; `slugifica` importato in T2 (sitemap), T2 (`tag.js`), T4.
