──────────────────────────────────────────────────────────────────
customizable-workspace
──────────────────────────────────────────────────────────────────
Two issues caught in the audit pass:
1. **Corrupt persisted spans broke the grid layout.**
localStorage entries from older versions (or hand-edited debug
sessions) could contain span=NaN / 0 / -3 / 99. These flowed
straight into `grid-column: span <bad>` which silently broke
the whole row. The visual symptom was a tile rendering at zero
width or pushing every sibling off-screen.
Fix: `reconcile()` now clamps every span (including newly
appended tiles' defaultSpan) to the legal `[1, 4]` range via
`sanitiseSpan()`.
2. **Re-reconcile effect could loop when callers forget to memoise.**
The `useEffect([tiles, hydrated])` always called `setLayout`
with a fresh `{ entries: [...] }` object reference, even when
the content was identical. If `tiles` itself was a fresh
reference per parent render (e.g. `tiles=[{...}]` inline),
every render \u2192 setLayout \u2192 save effect \u2192 (no loop because
tiles ref same), but constant unnecessary writes to
localStorage.
Fix: added `sameLayout(a, b)` structural-equality check;
setLayout now short-circuits to the previous state when the
reconciled output is identical.
Tests: 10 \u2192 11
reconcile \u00b7 sanitises corrupt spans (NaN/0/negative/>4) \u2192 clamp
──────────────────────────────────────────────────────────────────
generative-theme
──────────────────────────────────────────────────────────────────
Cosmetic but worth fixing: the `rose` palette regex included
`warm` as a keyword, but the `citrus` palette \u2014 listed earlier in
the PALETTES table \u2014 also matched `warm`. Since first-match wins,
`warm` was unreachable in rose and the entry was misleading.
Dropped `warm` from the rose regex. Citrus retains it (was always
where it routed in practice). All 18 existing tests still pass.