# map-popup

The Leaflet popup treatment for gf.cx neighbourhood maps — a rounded Street
View thumbnail above a padded body, with the title as a link.

Built once on `london.gf.cx/map/`, then hand-copied. By 2026-09-23 **four**
pages rendered **one** dataset (`london.gf.cx/data/recommendations.json` +
`map-meta.json`) from four copies of this code, and the thumbnails existed on
exactly one of them — which is how it got reported as *"abbeyroad lost the
pop-out pictures."* Nothing was lost; the feature had never been there.

**When one dataset feeds several rendered pages, the renderer is the thing
that must be shared** — otherwise every improvement lands on one page and
silently reads as a regression on the others.

## Live URL

```
https://assets.gf.cx/map-popup/map-popup.css
https://assets.gf.cx/map-popup/map-popup.js
```

## Use

```html
<link rel="stylesheet" href="https://assets.gf.cx/map-popup/map-popup.css">
<script src="https://assets.gf.cx/map-popup/map-popup.js" data-cfasync="false"></script>
```

`data-cfasync="false"` keeps Cloudflare Rocket Loader from rewriting the tag.

```js
const P = GfcxMapPopup;
P.bind(marker, {
  thumb: meta.thumbs[r.id],                        // {src, alt} or undefined
  title: { text: `${r.n}. ${r.name}`, url: r.url }, // url optional
  lines: [ P.meta(`${r.walk_from_flat} walk`), r.blurb, P.link(r.url) ]
});
```

`bind()` is preferred over `popupHTML()` — it applies the matching popup
options for you, so you cannot pass a thumb and forget them.

## Contract

- **A missing thumb returns `''`.** The degrade path is part of the contract,
  not an afterthought: a place with no photo must never render an empty
  `<figure>` or a broken `<img>`.
- `popOpts(thumb)` pins a photo popup to 236px. Without it the image drives the
  box wider on first paint.
- `loading="lazy"` + explicit `width="236" height="150"` — no layout shift.
- **Optional `photo`** (2026-09-27): when `src` is a stylised render (the
  pen-and-ink thumbs) and `photo` is the colour original, the figure becomes
  `.pw-thumb--duo`: the photo sits stacked and hidden, fades in on hover /
  keyboard focus, and toggles per tap on touch. Absent `photo` → unchanged.
  Print always shows `src`.

## Gotchas

- **The thumbnail image is `width:100%`, never a hardcoded 236px.** Leaflet
  measures content with `offsetWidth` and writes an inline width that rounds
  **up** to 237px, which beats the CSS. A fixed-width image then left-aligns in
  a 1px-wider box and leaks a white hairline down the right edge of every
  thumbnail. Filling the box makes the photo track whatever width Leaflet lands
  on. (Cost real debugging — london `630caf7`. Do not "simplify" it back.)
- `.leaflet-popup-content-wrapper` must lose its padding and clip, or the
  thumbnail's top corners square off against the popup's rounded corner.
- **The classes are `pw` / `pw-thumb` / `pw-body`.** Grepping for
  `popout|lightbox|data-thumb` returns zero on every surface and reads as
  "the feature never existed."

## Scope

Popup only. Marker chrome (`.pin` / `.tube` / `.flat` / `.ring-lbl`) and legend
type stay with the consuming page — those differ per surface on purpose
(maps.gf.cx's flat marker is 30px against london's 28px), and sharing them
would move pixels for no benefit.
