# Agenda pe bucharestphotofest.ro — pachet de integrare

Generat: 06.10.2026 · **bloc v12 · proxy v1.3** · sursă: Airtable `26.BPF – Management`
(`appgWKrR9bjnwldTX`) / Agenda (`tblU4qiJ7eH9Au7gQ`), recitită pe 08.10.2026 la ~13:19
(39 de evenimente active, din care 36 apar pe site).

## Fișierele

| Fișier | Unde merge |
|---|---|
| `bpf-agenda-block.html` | widget **HTML** Elementor, într-un container nou pe `/program/` |
| `bpf-agenda-proxy.php` | `wp-content/mu-plugins/bpf-agenda-proxy.php` |

Copiile de referință sunt în proiect (`claude/agenda-web/`) și în Drive, în
`BPF 2026 / _Website : App / ___Agenda_block/`.

Blocul funcționează **și fără proxy**. Fără el, afișează instantaneul din 06.10 și
scrie asta în eticheta din dreapta sus. Cu proxy, datele se înlocuiesc cu cele live la
fiecare 2 minute și eticheta devine „live · HH:MM”.

## Instalare, în ordine

**1. Blocul.** Pe `/program/`: container Elementor nou → widget **HTML** → lipești tot
conținutul din `bpf-agenda-block.html`. Nu-l pune în „Text Editor”, pentru că strică scriptul.
Prima linie trebuie să fie `<!-- BPF26 Agenda Block · v12 ... -->`. Dacă nu e, ai o versiune veche.

**2. Proxy-ul.** Urcă `bpf-agenda-proxy.php` în `wp-content/mu-plugins/`. Pluginurile din
folderul ăsta se activează singure; dacă folderul nu există, creează-l. Apoi, în
`wp-config.php`, **deasupra** liniei `/* That's all, stop editing! */`:

```php
define('BPF_AIRTABLE_TOKEN', 'patXXXXXXXXXXXXXX.xxxxxxxxxxxxxxxx');
define('BPF_AIRTABLE_BASE',  'appgWKrR9bjnwldTX');
```

`appgWKrR9bjnwldTX` **este** baza `26.BPF – Management` (verificat pe 28.09 prin search_bases).

**3. Tokenul.** Airtable → Developer hub → Personal access tokens → Create token.
Scope: **doar `data.records:read`**. Access: **doar baza `26.BPF – Management`**.
Fără `schema:write` și fără `data.records:write`.

**4. Test.** `https://bucharestphotofest.ro/wp-json/bpf/v1/agenda` trebuie să întoarcă JSON.
Cu `?nocache=1` sari peste cache-ul de 60 s.

## De ce proxy și nu citire direct din pagină

O pagină statică nu poate citi Airtable fără cheie. O cheie scrisă în HTML e vizibilă
oricui deschide sursa, iar un token Airtable dă acces la **toată baza**, inclusiv la People,
cu telefoane și date de zbor. Proxy-ul ține tokenul pe server și expune doar câmpurile
de program.

Din People cere **doar câmpul `Full Name`**, ca să transforme ID-urile participanților în
nume. Telefoanele, emailurile și zborurile nu sunt cerute niciodată.

Limitarea din 00_CONTEXT §10 („Netlify nu poate fi live”) rămâne adevărată pentru Netlify.
**Pe WordPress se poate**, pentru că există un server.

## Ce face blocul

- filtre pe două rânduri compacte: zilele sunt butoane mici; tipul, locația și gruparea sunt
  meniuri derulante native
- bara de filtre **nu e sticky**: dispare la scroll, ca restul conținutului
- evenimentele cu aceeași oră de început și de sfârșit se unesc într-un singur rând de oră.
  Seara de film de pe 16 apare ca un singur rând de oră cu cinci titluri
- 9 și 18 octombrie sunt marcate cu roșul de brand (deschidere / închidere)
- expozițiile fără oră intră în secțiunea „On view throughout”, nu în programul pe ore
- înregistrările `Cancelled` nu apar, nici în instantaneu, nici prin proxy
- **v5 · gruparea pe locație:** adresa apare **o singură dată**, sub numele locației, în
  antetul grupului. Rândul cu locul se ascunde din fiecare card, pentru că îl repetă
  pe cel din antet. Gruparea pe zile și pe tip nu se schimbă.

## Acces: bilet, RSVP sau intrare liberă (v11)

Fiecare card are acum un al patrulea rând, „ce trebuie să faci ca să participi”. E generat din
Airtable, nu scris de mână:

| Situația în Airtable | Ce vede participantul |
|---|---|
| câmpul `TICKET` are un link `http(s)://…` | ● **Ticket required** + buton **Tickets ↗** (se deschide în tab nou); în serile de 9 și 18 butonul e roșu |
| `TICKET` = „NO” / gol, iar tipul e **Workshop** sau **Masterclass** | ◐ **Free · RSVP required** + buton **RSVP by email** (deschide un email către hi@bucharestphotofest.ro, cu subiectul completat: titlu, zi, oră) + adresa scrisă vizibil, pentru cine nu are client de email |
| orice altceva | ○ **Free entry** |

- Legenda celor trei simboluri apare deasupra programului. Bara de filtre are un meniu nou, **Access**.
- În subsol scrie „Entry is free unless marked otherwise. RSVP: hi@bucharestphotofest.ro”.
- Pe telefon butoanele au 44 px înălțime.
- Ca să schimbi regula: `RSVP_TYPES` și `RSVP_MAIL` în JS-ul blocului (`template_v12.html`).
- Proxy v1.3 trimite câmpul `TICKET` ca `tk`, doar dacă e link valid. Restul deciziei e luat în bloc,
  deci instantaneul și datele live dau același rezultat.

Pe 06.10, seara: 11 evenimente cu bilet, 4 cu RSVP (How to Be Lost on Purpose, No Such Thing as Bad Weather,
Second Shot, The Siege of Mariupol), 21 cu intrare liberă. Toate cele 7 linkuri distincte de bilete au fost
deschise și duc la evenimentul corect.

## Evenimente ascunse intenționat (v6)

Trei evenimente rămân în Airtable, dar **nu apar pe site**: sunt sold out și ar crea confuzie.
Decizia e din 06.10.2026.

| ID Airtable | Eveniment | Zi |
|---|---|---|
| `recrHpqAramtRO7yE` | In Plain Sight I — workshop x Save the Children | sâmbătă 10.10 |
| `recMi0WvV6BHku1h6` | In Plain Sight II — workshop x Save the Children | sâmbătă 10.10 |
| `rec852oA3g6ZRlsSK` | Somewhere to Look From — workshop x UNHCR | duminică 11.10 |

Lista e în **trei locuri**, care trebuie să rămână identice:

- `HIDE_IDS` în `_build/build_v12.py`, care le scoate din instantaneu
- `var HIDE` în JS-ul blocului (din v7), care le scoate și din datele live, oricare ar fi versiunea proxy-ului
- `BPF_HIDE_IDS` în `bpf-agenda-proxy.php`, ca să nu plece deloc de pe server

Ca să reapară un eveniment, ștergi ID-ul lui din toate trei, rulezi din nou `build_v12.py` și urci
din nou proxy-ul.

### Dacă le vezi totuși pe site

Din v6 încoace, cele trei nu există nici în cod, nici în date. Dacă apar, pagina rulează o versiune mai veche:

1. Uită-te în subsolul agendei: trebuie să scrie **„Block v12”**, iar eticheta din dreapta sus
   trebuie să fie „snapshot · 06.10.2026” sau „live · HH:MM”. Dacă scrie „Block v5” sau
   „snapshot · 28.09.2026”, widgetul HTML din Elementor are încă blocul vechi.
2. După ce lipești v12: Elementor → **Update**, apoi golești cache-ul (pluginul de cache / CDN),
   apoi deschizi pagina într-o fereastră privată.
3. Dacă pe `/program/` mai e și embed-ul Airtable vechi (baza `appfNTymeVKSQZYmn`), acela își
   ia datele din altă bază și nu e controlat de blocul ăsta.

Dacă vrei ca un eveniment să dispară și din Airtable, nu doar de pe site,
`Status = Cancelled` e suficient: îl filtrează deja ambele.

## Specificația de design, măsurată

| | |
|---|---|
| Font | DM Sans — **400 / 500 / 700 reale**. Site-ul încarcă azi doar 400 și le sintetizează pe celelalte. |
| Cerneală | `#111013` titluri · `#161518` text · `#5e5e64` secundar · `#8a8a8e` estompat |
| Accent | `#dd0000`, hover `#a52626` — doar la filtrul activ și la cele două seri |
| Linii | 1 px `rgba(136,136,137,.28)`; linii de secțiune 3 px `#111013` |
| Lățime | `max-width: 1312px` = containerul Ohio (1344 − 2×16) |
| Bara de filtre | **statică** — 83 px la ≥768 px, 178 px la 390 px |

### Ritmul cardului — trei nivele (v4, păstrat în v5)

| distanță | valoare | rol |
|---|---|---|
| etichetă de tip → titlu | **10 px** | eticheta anunță titlul |
| titlu → artist | **5 px** | titlul și artistul se citesc ca un singur bloc de identitate |
| artist → locație | **17 px** | locația e un nivel separat (~1:3,4 față de titlu → artist) |
| înălțimea unui eveniment (titlu pe un rând, 1440 px) | **174 px** | v3: 196 px |

Artistul are 16,5 px / 500; țara e în griul estompat, ca să nu concureze cu numele.
Titlul are `line-height: 1.14`.

**v5**, în antetul grupului de locație: nume → adresă la **10 px**, adresa la 14 px,
estompată, cu același pătrat de 8 px folosit în card.

Tot blocul e izolat sub `#bpf-agenda { all: initial }`, ca Ohio, UIkit și Elementor
să nu-i modifice stilurile. E același tipar ca widgetul de donații `#bpf-widget`, deja pe site.

## Instantaneu și live arată identic

Blocul (`build_v12.py`) și proxy-ul (v1.2) aplică aceleași normalizări:

- nume scurte de locație: `CdRF - Centrul de Resurse în Fotografie` → `CdRF`,
  `National Museum of Maps and Old Books` → `Museum of Maps and Old Books`
- în titluri, ` - ` devine ` — `, iar `w/Nume` devine `w/ Nume`
- participanții `Collective`, `tba` și `to be announced` nu se afișează, indiferent de majuscule
- adresa se scurtează la stradă și număr, cu sau fără cod poștal
- peste 3 participanți: primul nume + „+N”
- `international` se scrie cu majusculă și când apare lângă alte țări
- ID-urile din lista de ascunse sunt excluse în ambele

Verificat programatic: pe aceleași date, ieșirea PHP e **identică** câmp cu câmp cu instantaneul.

## Verificat programatic, 08.10.2026 (v12)

- Airtable recitit la ~13:19: 39 active, 36 pe site, accesul neschimbat (11 cu bilet, 4 cu RSVP, 21 liber). Față de v11:
  No Such Thing as Bad Weather (11.10) → 14:00–15:00; How to Be Lost on Purpose (10.10) → 10:00–11:00;
  Afterlight (14.10, OTOTO) → 20:00–22:00; CTRL 18 → „… Finissage party …” (cu majusculă)
- testul de rezistență: răspuns live cu cele 3 ascunse → 36 pe pagină, 0 din cele 3; proxy v1.3 ↔ instantaneu: identic
- ritm 10 / 5 / 17 / 16 px; butoane de 36 px pe desktop și 44 px pe telefon; 0 erori JS

## Verificat programatic, 06.10.2026 (v11)

- Airtable recitit seara: 39 active, 36 pe site. Față de v10: In Plain Sight (15.10, ARCUB) are ca participanți
  Save the Children și George Ioniță
- corectat în Airtable, cu acordul lui Ionuț: cele 4 scurtmetraje din 16.10 (An Underground Bird, Forgetting,
  Finding the Landscape, KIN) aveau linkul spre biletele „In Passing” (17.10). Acum au linkul paginii
  „Ernest Cole: Lost and Found + shorts”, care le conține explicit
- acces: 11 bilete, 4 RSVP, 21 liber; filtrul Access dă 11 / 4 / 21; butoanele au 36 px pe desktop și 44 px pe telefon
- ritmul vechi e neatins: 10 / 5 / 17 px. Rândul de acces vine la 16 px (14 pe telefon). Un eveniment pe un rând
  are acum 201 px la 1440 (174 înainte), pentru că rândul de acces e nou
- testul de rezistență: răspuns live cu cele 3 ascunse → 36 pe pagină, 0 din cele 3; proxy v1.3 ↔ instantaneu: identic

## Verificat programatic, 06.10.2026 (v10)

- Airtable recitit la ~19:21: 39 active, 36 pe site (34 cu oră + 2 fără dată). Față de v9:
  Night at Museum: Afterlight (10.10, Museum of Maps and Old Books) anulat; In Plain Sight (15.10, ARCUB)
  are acum țara Romania, nu UK
- Museum of Maps and Old Books nu mai are niciun eveniment și a ieșit din filtrul de locații (8 locații)
- testul de rezistență: răspuns live cu cele 3 ascunse → 36 pe pagină, 0 din cele 3
- proxy v1.2 ↔ instantaneu v10: identic; ritm 10 / 5 / 17 px, 174 px; 0 erori JS

## Verificat programatic, 06.10.2026 (v9)

- Airtable recitit la ~13:00: 40 active, 37 pe site (35 cu oră + 2 fără dată). Față de v8:
  Fig. M. Photobooks Signings anulat; lansările „Bucharest” și „Reykjanes Awekens” (12.10, CdRF)
  încep acum la 19:00, nu la 18:00 (19:00–22:00)
- testul de rezistență: răspuns live cu cele 3 ascunse → 37 pe pagină, 0 din cele 3
- proxy v1.2 ↔ instantaneu v9: identic; ritm 10 / 5 / 17 px, 174 px; 0 erori JS

## Verificat programatic, 06.10.2026 (v8)

- Airtable recitit la ~09:52: 41 active, 38 pe site (36 cu oră + 2 fără dată). Față de v7:
  Meet the artist: Vedad Divović anulat; Meet the artist: Joakim → 14:00–15:00 (avea start = end);
  fostul „Meet the artist: Chiara Ernandes” e acum „Guided tour: Still Birth”, Fernanda Prado Vercic
  (Brazil), 14:00–15:00; Witnessing on the streets of Bucharest → Kulturhaus; Pinhole photography
  → OTOTO Station, 14:00–17:00; No Such Thing as Bad Weather → 13:00–14:00; Still Birth → doar Chiara Ernandes
- „Bucharest Downtown / Outdoor” nu mai are niciun eveniment și a dispărut din filtrul de locații (9 locații)
- testul de rezistență, repetat: un răspuns live cu cele 3 ascunse → tot 38 pe pagină, 0 din cele 3
- proxy v1.2 ↔ instantaneu v8: identic; ritm 10 / 5 / 17 px, 174 px; 0 erori JS

## Verificat programatic, 06.10.2026 (v7)

- datele recitite la ~09:50; singura schimbare față de v6 e titlul CTRL 18, acum
  „CTRL 18. Countless versions of becoming. finissage party w. Cliza & Nek”
- **test de rezistență:** am servit blocului un răspuns live care le conține pe cele 3, cum ar face un
  proxy vechi. Pagina a trecut pe „live”, a afișat tot 39 de intrări, iar cele 3 nu au apărut, la 1440 și la 390 px
- `/wp-json/bpf/v1/agenda` răspundea **404** pe 06.10 la ~09:45, deci proxy-ul nu e instalat pe site.
  Până va fi, pagina afișează instantaneul
- proxy-ul v1.2 produce o ieșire identică cu instantaneul v7, pe date simulate
- ritmul 10 / 5 / 17 px și evenimentul de 174 px sunt neschimbate; 0 erori JS; fără scroll orizontal

## Verificat programatic, 06.10.2026 (v6)

- pe site apar 39 de evenimente (37 cu oră + 2 fără dată); din cele 42 active în Airtable, lipsesc exact cele 3 ascunse
- textele „Save the Children”, „UNHCR”, „In Plain Sight I” și „Somewhere to Look From” nu apar nicăieri în pagină,
  la 1440 și la 390 px. Sâmbătă 10.10 are 4 evenimente, duminică 11.10 are 5
- proxy-ul v1.2, rulat pe aceleași date în formatul API-ului REST (cele 3 incluse), le exclude
  și produce o ieșire identică cu instantaneul
- ritmul cardului e neschimbat: 10 / 5 / 17 px; eveniment de 174 px la 1440; fără scroll orizontal; 0 erori JS

## Verificat programatic, 28.09.2026 (Playwright/Chromium, DM Sans încărcat local)

- lățime randată: **1312 px** în containerul Ohio reprodus (1344 / 16 / 12vh)
- distanțe în card: 10 / 5 / 17 px la 1440, 1024, 768 și 390 px; eveniment de 174 px la 1440
- filtre: `Film` → 7 intrări; 16 oct → 2 rânduri de oră / 6 evenimente; grupare pe locație → 11 grupuri
- **v5, gruparea pe locație:** 11 adrese în antete, 0 rânduri de loc vizibile în carduri;
  adresa de la POINT apare o singură dată (înainte de v5 apărea de 3 ori). Lista scade de la
  9141 la 8139 px la 1440 (−11%) și de la 11044 la 9579 px la 390 (−13%). Gruparea pe zile
  și pe tip păstrează toate cele 44 de rânduri de loc.
- 0 erori JS proprii. Cele două erori din sandbox sunt așteptate: Google Fonts e blocat de proxy,
  iar fetch-ul pe `file://` eșuează — adică pagina cade pe instantaneu, cum trebuie.
- fără scroll orizontal la nicio lățime
- `php -l` fără erori; proxy-ul, rulat pe date simulate în formatul API-ului REST
  (participanți ca ID-uri), produce exact instantaneul

## Neverificat

- **nu am putut testa pe site-ul real**, pentru că nu am acces de scriere la WordPress
- proxy-ul nu a rulat pe serverul real și nici pe API-ul Airtable real, doar pe date simulate.
  În special nu am verificat dacă `add_query_arg` trimite `fields[0]=…` într-o formă pe care
  Airtable o acceptă; dacă nu, câmpurile vin toate și proxy-ul le ignoră pe cele de care n-are nevoie
- posibilă coliziune de rută pe namespace-ul `bpf/v1`; plugin de cache/minify pe `/program/`
- afișarea în secțiunile `clb__dark_section` ale temei: blocul își impune fundal alb, dar nu l-am văzut pe site
- pe `/program/` există deja un embed Airtable din baza **`appfNTymeVKSQZYmn`**, care nu e
  niciuna din sursele de adevăr ale proiectului. Trebuie clarificat cui aparține înainte de a-l înlocui.
