Spun out of code review on #103 (item #5).
Context
renderMap() in layouts/index.html builds popup HTML via template-literal string concatenation of region fields:
```js
const popupHTML = `
${region.name}
...
${edition} · ${tier}
`;
```
Today the data is YAML, Zod-validated, and controlled by maintainers, so practical XSS risk is near zero. But the trust boundary is implicit — if the schema ever loosens (free-text `edition`, scraped fields, etc.) this code becomes injectable.
Proposed fix
Add a tiny escape helper and apply it to every interpolation in the popup builder.
```js
const esc = s => String(s).replace(/[&<>"']/g, c => (
{ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]
));
```
Keep the schema as the primary defense; make the rendering layer the secondary one.
Acceptance
Spun out of code review on #103 (item #5).
Context
renderMap()inlayouts/index.htmlbuilds popup HTML via template-literal string concatenation of region fields:```js
const popupHTML = `
...
${edition} · ${tier}
`;
```
Today the data is YAML, Zod-validated, and controlled by maintainers, so practical XSS risk is near zero. But the trust boundary is implicit — if the schema ever loosens (free-text `edition`, scraped fields, etc.) this code becomes injectable.
Proposed fix
Add a tiny escape helper and apply it to every interpolation in the popup builder.
```js
const esc = s => String(s).replace(/[&<>"']/g, c => (
{ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c]
));
```
Keep the schema as the primary defense; make the rendering layer the secondary one.
Acceptance