The wireframe puts two blocks of text on the header picture. The page had one `phrase`, drawn under it.
`header_overlays` is a jsonb list — `[{ text, x, y, size, color, align }]` — rather than a pair of
`header_text_1_*` columns. The design asks for two today; a third is then data rather than a migration,
and nothing has to decide what an empty second block means. `headerPhrase` is left alone: folding them
together would silently move somebody's existing copy on top of a photograph.
Everything is relative and has to be. x and y are percentages of the picture, so a position means the
same thing on a phone and a desktop. `size` is a percentage of the picture's WIDTH, because a headline
set at 40px is a headline on a laptop and a caption on a large screen. The preview renders that with
container query units against the picture's own box, which is exactly what the percentage means — the
page will do the same, so the two cannot drift.
The editor is the picture with the text on it, at the shape the page uses, because where a line sits on
a photograph is not a decision anybody can make from two number fields.
The API clamps every field rather than rejecting: a position into the picture, a size into something
legible, the colour to a hex triple. A caller sending nonsense gets a usable overlay instead of a 400,
and the page can never be handed a position that puts text off the edge of its own header.
Pushed to both databases and verified by querying information_schema rather than by reading db:push's
exit code, which is documented to lie.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>