Skins
Representation leaves game logic. An app's board is a grid of role integers (0 = void); a skin is the one dictionary that says what each role wears; faces just apply it.
The law
- Apps emit role ids. No color names, chars, or emoji tables in
`mrlyapps` or in web views - ever.
- A skin is data: role -> visual. Curated per-app dictionaries live in
`mrlyui::skin::<app>` presets, keyed by the app's `skin` setting.
- The state carries the resolved dictionary next to the ids, so the web
grid, the canvas, the CLI, and PNG all read the same one.
The shape
`mrlyui::skin` defines the closed enum:
- `Visual { bg, motif, face }` - a cell background, an optional motif
pattern (bg doubles as its color), an optional face on top.
- `Face::Glyph(text)` - baked from the bitmap font.
- `Face::Emoji(value)` - a unicode string; DOM-only until the emoji
atlas exists (`FACE.md`), so canvas surfaces skip it.
- `Face::Sprite(rows)` - a small bitmap, e.g. chess pieces.
On the wire, state gains two keys: `ids` (rows of role integers) and `skin` (an array indexed by role; each entry `{ bg?, motif?, face?: { as, value | rows } }`).
`Skin::tileset(k, ink)` builds the canvas `TileSet` from the same dictionary: solid or motif base, glyph faces baked in ink, emoji faces skipped. Web views use `src/skin.tsx` (`visual()`, `face()`).
Why
Same seed, same verbs, N skins: identical id grids, N renders. One replayed episode times N skins is N perfectly labeled samples with identical semantics - the gym's augmentation story lives in this layer.