Author a live tile
How do I get those animated scenes on my own pages?
Tiles are JSON, not code: shapes, palette roles, motion, and a cost budget the engine enforces. Write a spec, mount it behind any heading or card.
Typical effort: 30 minutes
The recipe — paste this to your AI
# Make a live tile
Paste-ready recipe. Your site ships a **live-tile engine**: small animated scenes drawn
on a canvas, behind your headings and inside your cards. They are configured as **data**,
not code — a tile is a JSON object describing shapes, and the engine draws it.
This is a lane you own. Adding tiles never takes your site off the upgrade path.
---
## The one-minute version
```ts
// components/my-tiles.ts (components/** is YOUR lane)
import type { TileSpec } from '@/components/visual';
export const MY_TILE: TileSpec = {
v: 1,
id: 'my-tile',
palette: { key: '#e8bd7a', ink: '#8a7460' }, // your colours, by ROLE
colors: { star: '#e8d0b0', moon: '#d0b898', screenTop: '#2a1e18', screenBottom: '#1c1512', screenStroke: '#4a3626' },
layers: [
{ elements: [{ kind: 'glow', x: 0.5, y: 0.35, r: { frac: 0.6, of: 'h' }, color: '$key', alpha: 0.08 }] },
{ elements: [{ kind: 'shape', x: 0.5, y: 0.32, r: { frac: 0.18, of: 'h' }, sides: 6, color: '$key', alpha: 0.4 }] },
{ elements: [{ kind: 'motes', x: 0, y: 0, x2: 1, count: 10, r: 1.6, color: '$key', alpha: 0.25 }] },
],
};
```
```tsx
import { VisualTile } from '@/components/visual';
import { MY_TILE } from '@/components/my-tiles';
<VisualTile spec={MY_TILE} className="rounded-md bg-slate-900">
<div className="flex h-[287px] flex-col justify-end p-6">
<h2>Your heading</h2>
</div>
</VisualTile>
```
That is a working tile: a soft glow, a hexagon, and dust drifting through it.
---
## How a spec is built
**`layers` are depth, back to front.** Each layer holds `elements`. Each element has a
`kind` and a position.
**Positions are FRACTIONS of the tile, never pixels.** `x: 0.5, y: 0.3` is centre,
slightly high — and it stays there on a phone and on a 5K monitor. This is the single
most important habit: a tile authored in pixels breaks at every size but the one you
tested.
**Sizes come off HEIGHT, not width.** `{ frac: 0.2, of: 'h' }`. A hero is short and very
wide; size something off width and it becomes enormous on a desktop.
**Colours are `$tokens`.** `$key` is your main light, `$accent` the saturated highlight,
`$cool` the counter-light, `$warm` warmth in the air, `$ink` structure (lines, not light).
Declare them in the spec's own `palette`. Using tokens means the same tile can wear a
different brand without editing it.
---
## The shapes you have
**Flat shapes** — `shape` (polygon or ellipse: `sides: 0` ellipse, `3` triangle, `6`
hexagon), `rect` (own width, height and `radius`), `arc` (part of a circle, or a pie),
`curve` (a smooth line through points), `silhouette` (a polyline — ridges, skylines),
`burst` (rays from a centre: a sun, a sparkle, a flower), `grid` (a repeating field),
`bars` (a row on a baseline; with `levels` it becomes a meter).
**Trees — THREE parts, never one** — `trunk` (the bole: `form: 'straight' | 'flared' |
'stout'`, an `x`/`y` at the base, a `height`), `fork` (the branching armature: `form: 'open'
| 'upright' | 'spreading'`, attached at the trunk's top), and `canopy` (the foliage mass:
`shape: 'lobed' | 'tiered' | 'columnar' | 'weeping'`, an `x`/`y` at its CENTRE and an `r`).
**Reach for the part, not the tree.** Each is useful alone, which is why each is its own
kind: a `trunk` by itself is a post, a stump, a palm bole, a piling; a `fork` by itself is a
bare winter tree, a dead snag, driftwood, a bramble; a `canopy` by itself is a bush, a hedge,
a shrub, a topiary. A single welded `tree` kind could be none of those, and could not be
two-toned either — one element carries one `color`, so brown bark under green leaves was
impossible until these came apart.
**And the variety is combinatorial**, which is the real reason to compose rather than to add
species: 3 trunks × 4 forks × 4 canopies = 48 silhouettes before the `seed` moves a single
branch, and the seed moves every branch and every lobe. **Canopies also layer** — put two or
three at small offsets in slightly different greens and they fuse into one lumpy mass whose
outline no seed alone would produce, which is the cure for a forest of identical stamps.
Stack them bottom-up: `trunk`, then `fork` at the trunk's top, then `canopy` over the
branches. Size the canopy off `FORK_REACH[form] × fork height` (art.ts) and every twig is
covered; go smaller on purpose and bare tips show through, which is autumn. All three parts
are optional — a fir is `trunk` + `canopy` with no fork, a bush is a `canopy` on its own.
**None of them draws a leaf, deliberately.** Foliage detail, blossom, snow and lanterns are
ordinary elements composed ON TOP in a later layer, out of kinds you already have. That keeps
all three cheap and lets one canopy be spring, autumn or winter with no parameter at all.
They take your `color` and `alpha`, and they are **exempt from the alpha cap** because a tree
is an object, not light. Dark and opaque is a night silhouette; pale is haze; `stroke: true`
is a line drawing. "Silhouette" is a way of USING them, not what they are.
**For the GROUND under it, use what you have**: a `silhouette` with `fill` + `occlude`, whose
points you also pass to `starField.horizonPoints`. That works because ground sits at the
BOTTOM of the frame, where the page behind the tile is already dark — occluding erases, so it
reveals the page. Do **not** reach for `occlude` on the trees themselves: they sit higher up
where the page gradient is light, and an erased tree comes out PALER than the night behind it
(shipped exactly that way on a live masthead, 2026-08-12).
**Solid forms** — `cylinder` and `cone`. These are drawn as real constructions, so they
read as objects with volume rather than as outlines.
**Light** — `glow` (a soft pool), `wash` (a full-frame gradient), `shadow` (the pool
under an object that seats it on a surface), `beam` (a shaft from an undrawn fixture —
you see the light, never the lamp), `neon` (a bent tube with a slow buzz; never lettering,
because the moment a sign says a word the scene belongs to one venue), `bokeh` (a field
of out-of-focus discs — compose where the light lives and the field fills it), `reflect`
(a source smeared straight down onto wet asphalt or still water; the break at the ground
line is hard by design), `wall` (a large emitting panel, feathered top and bottom so it
reads as a light source rather than a painted rectangle — the biggest-area kind there is,
keep its alpha low).
**Night sky** — `star` (one point), `starField` (hundreds, as a cached sprite, so `count`
costs one `drawImage` per frame), `milkyWay` (the band; omit it for a city, where you
could not see it), `moon`, `shootingStar` (rare and quick — low alpha, ~1s), `satellite`
(a slow steady crossing, no streak), `horizonFade` (fades the tile's bottom edge to real
transparency). **A `starField` in front of a landscape needs its `horizon`** — a
`silhouette` ridge cannot be opaque under the alpha cap, so every star behind it shows
through as a ghost. The validator now refuses that combination.
**And prefer `horizonPoints` to `horizon`** when there IS a ridge: hand it the same points
the `silhouette` is drawn from and each star is cut at the mountain height above it,
instead of at one flat line across the highest peak. The flat version leaves a starless
band between that line and the actual skyline, and it reads — accurately — as two
different pictures joined by a rule.
**Movement** — `motes` (drifting particles; `rise: true` for bubbles or embers),
`confetti` (flakes that tumble), `ripples` (expanding rings), `liquid` (a body with a
surface and bubbles that die at it — one kind rather than three elements, because a
surface that leans while its bubbles do not is wine outside its own glass).
**Making something look like a LIGHT** — set `flicker` (0–1) on a `shape`. A lamp, a bulb
or a sign does not change size, it changes how hard it is burning. Reach for `pulse` only
when the thing really does grow and shrink: on a small shape it moves a fraction of a
pixel and looks static even though every check counts it as motion.
**Putting something on a BEAT** — `beatS` (seconds per beat) with `beatDepth` on
`glow`, `beam`, `head` or `headphones`. A sine is a *breath*: it rises exactly as slowly
as it falls, and a room of things breathing is ambience, not music. A beat is a hit and a
decay. Elements sharing a `beatS` land on the same frame, so one scene can run two tempos
and visibly say "two different tracks". The authored alpha is the LIT PEAK and the
envelope only pulls it down — which is also why it is free: modulating pixels you are
already painting changes the whole frame's colour for no extra fill.
**Ready-made objects** — `mic`, `record`, `arch`, `chairs`, `aisle`, `bulbs`, `pendants`,
`marquee`, `bowl`, `lectern`, `presentation`, `crowd`, `rail` (a rooftop rail, with
`sheen` pointed at whatever light the scene already has), `screen`, `windows` (a lit
façade; each window runs a slow on/off *schedule*, a switch and not a dimmer), `branch`
(line geometry, because at hero alpha a soft canopy reads as light rather than as
foliage), `cloud`, `goblet` (`belly` is the glass's identity — wine bulges below the rim,
a flute barely does, and straight walls to a point is a martini), `cropMarks` (camera
brackets that compose the copy inside them), `head` (a non-gendered head-and-shoulders
silhouette, front-on or `profile` — the shape a crowd is made of), `headphones`,
`tacoShell`, `citrusWheel` (lime, lemon or orange — colour and `segments` are the only
difference), `papelPicado` (a strung banner of cut-paper flags).
Import a shipped spec and read it; every one is a worked example.
**Drawing people: shape, never portrait.** The catalog's old rule was "never draw
people", and `crowd` exists to honour it — a sea of catchlights, no bodies. The rule was
never really about the human outline, though; it is about IDENTITY. A face, a hairstyle
or a body type is a claim about who your customer is: get it wrong and the picture tells
a whole audience the event is not for them, get it "right" and it is stock photography,
which everyone discounts. And an *accidental* figure is worse than a deliberate one — a
forked tree trunk that reads as a person with arms is a defect nobody chose.
So `head` ships deliberately featureless: no hair, no face at any size, no jaw or
shoulder line that reads as a gender. Use it when individual people are the subject. Use
`crowd` when you need a full room — it is cheaper, and hundreds of catchlights say
"packed hall" better than figures ever will.
**Before you hand-build a shape, check this list.** An author who cannot find a primitive
builds it again out of polygons — eight hand-placed spokes instead of `citrusWheel`, a
scatter of rects instead of `windows`. That is slower to write, heavier to render (each
primitive carries an honest cost entry; a pile of generic shapes does not), and it leaves
one-off geometry nobody else inherits, which is the opposite of what a shape kit is for.
`every-kind-is-documented.test.ts` keeps this list complete, so if a kind exists it is
named here.
**Combining shapes** — a `group` holds several elements positioned relative to each other
and moves them as one thing:
```ts
{ kind: 'group', x: 0.5, y: 0.3, bob: 0.01, periodS: 6, children: [ /* … */ ] }
```
**If it is ONE OBJECT rather than an arrangement, set `rigid: true`** — a child's offset
normally reads `x * width, y * height`, so a group authored on a card comes apart on a
wide hero. `rigid` puts every width-unit field on the height unit, and the assembly stays
a shape instead of becoming a layout.
**Inside a rigid group, never clamp a size.** `{ frac: 0.19, of: 'h', min: 26, max: 58 }`
next to plain fractions is the same bug through a different door: past the clamp that part
stops growing and the rest keeps going. Author every part as a bare fraction and set the
assembly's overall size with the group's own `scale`, which moves all of them at once.
A test enforces this (`parts-agree-at-every-size.test.ts`) — it was written after a
magnifying glass shipped three times with its handle floating clear of the lens.
---
## First, the posture: the art serves the words
Steve, 2026-08-05, after a round of live tiles:
> "on live tiles we should always be thinking about best photography rules and then also
> layout with wording to make the scene work together. images are a background that should
> enhance our wording. add depth to a story and invoke imagination."
That is the frame every rule below sits inside, and it decides the arguments the rules
cannot:
- **The scene is a BACKGROUND. The words are the subject.** If a viewer reads the art
before the headline, the tile has won an argument it should not have entered. Every
choice about brightness, size and position is really the question *does this help the
sentence land, or compete with it?*
- **Compose like a photographer, not like a diagram.** Nothing dead-centre; the subject
set off to one side with air opposite; a horizon that puts both halves in one room. The
classical marks (thirds, the golden ratio) are the instinct — where a surface's copy
channel is stricter, the copy wins, but the instinct still says *never centred*.
- **Depth, then story.** Something near, something far, and light between them. A flat
scene reads as a pattern; a scene with distance in it reads as a place, and a place is
what a reader puts themselves into.
- **Invoke, do not depict.** The best tile leaves room for the reader to finish it — an
empty aisle, three candles, a still pool. Their imagination outdraws you every time, and
it is already tuned to *their* event rather than to the one you happened to picture.
---
## What makes a tile good
These are not style preferences. Every one of them came from a scene that failed:
1. **One thing a viewer can NAME.** A mic, a lamp, an arch, a sign. Scenes made only of
abstract light read as decoration and nobody knows what they are looking at.
2. **A soft fill reads as LIGHT; only an outline reads as an OBJECT.** If a shape should
be a thing rather than a glow, stroke it — or set `fill` *and* `stroke` with a
`fillAlpha`, which gives you a lit face with a crisp edge from one element.
3. **Give it something to stand on — and make it TOUCH.** A ground line, a counter, a
horizon, or a `shadow`. Objects with nothing beneath them hang in the frame; objects
drawn a few hundredths ABOVE the ground they were meant to rest on hang just as badly,
and that one is harder to see in the source than on the screen. Compute each object's
base from the ground line's own height at that object's x, and set it a few
thousandths PAST the line so the contact is an overlap rather than a coincidence. A
still life is held together by one shared ground; without contact it is a collage.
(Murder Mystery shipped with all five objects floating over their table, and Steve's
report was "the objects are not grouped and as one" — never "they are too high".)
4. **Something must TRAVEL.** Motion smaller than a few pixels is invisible — the tile
just looks static. Something should cross real distance, or change brightness hard.
5. **Keep out of the copy — measured against the REAL copy, not half the frame.** In a
card, your heading owns the bottom: put the subject around `y: 0.30`. In a wide hero,
the copy owns the middle third horizontally. And check it in the NARROWEST column the
card ships in: in a five-across grid a two-line title, three lines of body and a CTA
run well past half the card, so "top half" still lands the subject behind the headline.
The rule is *above where the copy actually starts*.
6. **No lettering.** Words in the art tie the scene to one moment and one business. Let
the reader's own copy do the talking.
7. **Draw the CONTAINER, not the contents.** This is the same rule as "no lettering", one
level up, and it is the one people break most. A buffet tile drawn as a stack of
pancakes tells a client booking a corporate brunch that you serve pancakes; the empty
chafing dish tells them you serve whatever they want. Draw the aisle with nobody in it,
the mic with no singer, the couch with nobody sitting on it. The specific thing is
always smaller than what the reader would have imagined.
8. **Check your new tile's SILHOUETTE against the ones next to it — before you touch its
colours.** If a scene reads wrong, the cause is usually that it shares a SHAPE with
another tile, and no amount of re-tinting will fix that. Two scenes drawn in the same
shapes are the same scene, however different the subject was in your head.
9. **Anything that must touch the floor has to be measured against the floor's axis.** A
`{ frac, of: 'w' }` size makes an object's HEIGHT a moving fraction of the frame, so it
stands on the ground at one aspect ratio and floats or sinks at every other. If a thing
sits on a surface, size it `of: 'h'` — the same axis the surface is positioned on.
---
## Staying fast
Tiles run in your visitors' browsers, sixty times a second, so the engine bounds them:
- **Counts are clamped** (48 per element) and **arrays are limited** (64 points in a
polyline, 24 children in a group, 60 elements and 12 layers per spec).
- **A cost model** scores fill rate — how many pixels get blended per frame. Big soft
gradients are what actually costs; a hundred thin strokes are free.
- **A page ceiling** covers all mounted tiles together, because six tiles that each pass
on their own is a page nobody checked.
- **One animation loop** drives every tile on the page, and it measures its own frame
time. On a struggling device it sheds density automatically.
- Tiles **pause** when scrolled off-screen or when the tab is hidden, and render a single
still frame for visitors who prefer reduced motion.
Check your own spec before shipping it:
```ts
import { checkCard, checkHero, checkPage } from '@/components/visual';
checkCard(MY_TILE).problems; // a card: cost, composition, and "does it look alive"
checkHero(MY_TILE).problems; // a full-width scene behind a headline
checkPage([{ spec: MY_TILE, w: 477, h: 287 }, /* … */ ]).problems; // a whole page
```
Each returns plain sentences describing what is wrong. An empty array means you are fine.
---
## Cards and heroes are not the same job
Use `checkHero` — not `checkCard` — for anything full width. **The rules are inverted**,
and mounting one on the other's surface is the single most common way a good scene looks
bad:
| | Card | Hero |
|---|---|---|
| shape | ~1.6:1 tile in a grid | ~4.9:1 band across the page |
| copy sits | across the **bottom** | down the **middle third** |
| subject goes | centred, near `y: 0.30` | off to the sides, `x: 0.35–0.65` **clear** |
| needs | one nameable object | a left, a right, and a ground running between |
A card spec stretched into a hero puts its subject directly behind the headline. That is
what "everything looks centred and unprofessional" always turns out to be.
> **There is a third class, and it has its own recipe.** An **option row** — the 640×56
> strip behind an answer in a question flow — is neither of these: its copy runs down the
> **left**, so a card spec dropped into one puts the subject exactly where the label is.
> Rows also have their own gate (one mover, a copy edge, a much tighter budget) and their
> own hard-won lesson: at 56px tall, **line art does not read** and area beats alpha. See
> **`make-a-live-row.md`**.
If a card scene is nearly right at width, don't copy it — **derive** it, so there is still
only one of it:
```ts
import { heroFrom } from '@/components/visual';
export const MY_HERO = heroFrom(MY_CARD, {
id: 'my-hero',
front: [{ elements: [/* the horizon and the counterweight the wide frame needs */] }],
});
```
## Mounting one behind a headline (use SceneHero, and here is why)
Two mistakes are almost guaranteed if you hand-roll a hero. Both shipped live on
bringthefront.com on 2026-08-01 and were reported as "the live feature at top looks bad"
and "not rendering as readable with text not fitting background".
**1. It comes out boxed.** Pages are usually wrapped in something like
`max-w-4xl mx-auto px-4`. Mount a tile inside that and `w-full` means 896px, so the scene
stops well short of the screen and reads as a decoration rather than a place. A hero has to
own the viewport width and cap only its *copy*. Half-fixing this is easy to do by accident:
going full-bleed only under `sm:` leaves desktop — where most people will see it — still
boxed.
**2. The headline is invisible.** The obvious token for text on dark art is
`text-content-inverse`. Do not use it here. That token is **theme-relative**: in the dark
theme `globals.css` sets `--content-inverse` to `#0a0a0a`, near-black. Tile art is
**always** dark whatever the site theme is, so on a dark-themed site you get near-black
text on near-black art — and it looks fine in light mode, which is how it ships. Copy over
tile art needs a fixed light value plus a scrim.
Both are already solved. Use the component:
```tsx
import { SceneHero } from '@/components/visual';
<SceneHero
kind="valley"
eyebrow="Optional"
title="The headline"
blurb="One or two lines underneath."
/>
```
It picks `HERO_SPECS` first and falls back to `CARD_SPECS` (see the section above on why
that matters), goes full-bleed from any container, carries its own scrim, and renders
copy in colours that do not move with the theme. An unknown `kind` renders the copy with
no art rather than crashing — a missing scene is a cosmetic loss, not an outage.
If you are writing your own instead, the scrim needs `z-index: 1`: a pseudo-element paints
*after* its host's children, so without it the scrim darkens the very headline it exists
to rescue.
## Telling a story instead of showing a picture
A surface that has to stand for several different things cannot say them in one frame.
A **story** is a list of ordinary specs that cross-fade in sequence — no new format, and
every scene in it is still judged by the same rules:
```ts
import { VisualTile } from '@/components/visual';
import type { TileStory } from '@/components/visual';
const MY_STORY: TileStory = {
v: 1, id: 'an-evening', holdS: 10, fadeS: 3,
chapters: [ARRIVAL, DINNER, THE_BAND, THE_FLOOR], // up to 8
};
<VisualTile story={MY_STORY} className="…">…</VisualTile>
```
Order it the way a customer LIVES it, not the way your services are listed. Costs are
charged on the most expensive **adjacent pair**, since exactly two chapters ever draw at
once — so a heavy scene is fine as long as its neighbours are light. Check with
`storyCost(MY_STORY, 1400, 391)`, and check every chapter with `checkHero`.
---
## Previewing at the real sizes
Your site ships a gallery at **`/play/tile/cards`**. It shows every tile at the sizes they
actually ship at, with a live cost readout.
**Use it before you trust a tile.** A scene composed in a square box and shipped into a
wide one is the most common way a tile goes wrong, and it is invisible until you look at
the real proportions.
---
## Every check can pass and the tile can still be wrong
This is the most important section here, because it is the failure the rest of the tooling
cannot reach.
`checkCard` proves a tile is cheap enough, composed in the right half, and genuinely moving.
All three are **structural**. None of them is *appearance*. On 2026-07-31 five tiles shipped
or nearly shipped with every automated check green, and a person looking at them caught all
five in seconds:
| What was drawn | What it looked like | Why |
|---|---|---|
| A wine glass | A **martini** | Straight walls to a point. Wine bellies out *below* the rim and tapers back in. |
| Four stacked chips | A **spring** | Each disc was translucent, so you saw the far edge of the ones behind it. |
| A stack of food warmers | A **coil** | Same cause: top and bottom ellipse both visible, nothing opaque. |
| A cut lime | A **paper fan** | Three segment lines all met at one point. Fans meet at a point; citrus has crossing chords. |
| A crowd on a lawn | **Nothing**, then **floating balls** | Dark silhouettes on a dark card have nothing to be dark against; then heads without shoulders. |
Four rules fall out of those, and they are worth more than any check:
1. **Check the silhouette before the colours.** Every failure above is a wrong OUTLINE. If the
shape is wrong, no amount of palette work rescues it.
2. **Something opaque has to hide what is behind it.** Stacked or overlapping objects need a
real fill (`fillAlpha: 1`), or you are drawing wireframes and the eye reads a spring, a coil,
a cage.
3. **A silhouette needs a light behind it.** Dark on dark is invisible. Put the dark shape where
the brightest thing on the tile is.
4. **Draw the container, not the contents.** A buffet is chafing dishes, not pancakes — people
picture their own food. A margarita is the glass, not the garnish.
### So the last step is always the same
**Render it and look at it, at the real size, next to its neighbours.**
Not next to a blank page — next to the tiles it will actually sit beside in the grid. "Is this
good?" is the wrong question; **"can I tell this apart from the one next to it?"** is the one
that finds real bugs. Two tiles that share a silhouette are the single most-reported problem in
this catalog's history, and the grid is the only place that shows it.
---
## Replacing a card's art
Cards on your site render through a `kind` string. To take one over — or to add a new one
— put your spec in **`components/visual/specs/cards/site.ts`**, a lane file that ships empty and is yours
to fill. Upgrades never overwrite it, your kind wins over ours if the names collide, and
**`npm run test` checks your tile the same way it checks ours** — cost, composition and
liveness — so you do not have to remember to.
```ts
// components/visual/specs/cards/site.ts
import { MY_WEDDING_TILE } from './my-wedding';
export const SITE_CARD_SPECS: Record<string, TileSpec> = {
wedding: MY_WEDDING_TILE, // replaces the base one everywhere `wedding` is used
'my-thing': MY_OWN_TILE, // or add a brand-new kind
};
```
A kind with no spec falls back to static art, so you can convert one card at a time and
never have a half-finished page.
---
## Asking an AI to build one
> Read `docs/prompts/make-a-live-tile.md`. Build me a live tile for our **[what it is
> for]** page. The subject should be **[one nameable thing]**. Use our brand colours from
> `site.config.ts`. Keep the subject in the top half — the heading sits at the bottom.
> Make sure something actually travels so it does not look static. Put it in
> `components/visual/specs/cards/site.ts` so the test suite checks it. Then render it at
> `/play/tile/cards` and show me a screenshot at the real size.
---
## The failure that costs the most: two products sharing one `kind`
This one is worth its own section because it has now happened five times on one site, it
never looks like the bug it is, and no amount of art fixes it.
A `kind` is a *scene*, not a *label*. The moment two products point at the same one, you
have two pages that look identical — and the report you get back is never "these share a
spec", it is **"this tile doesn't feel right"** or **"these two look the same"**. So the
instinct is to go and re-tune the shared scene, which cannot possibly work: whatever you
change lands on both.
Real examples, all from the same site:
| symptom reported | actual cause |
|---|---|
| "murder mystery just looks like movie night" | both mapped to `cinema` |
| "foam party should have bubbles" | mapped to `club` — every foam party has a DJ, only a foam party has foam |
| "wine night and bourbon tasting are too similar" | both mapped to `tasting` |
| "monday madness and every game are the same tile" | both mapped to `screens` — **two rows apart on one page** |
| "foam party should have bubbles" *(again)* | a second, hardcoded copy of the mapping in a page file |
### Two rules that prevent all five
1. **Before adding a mapping, grep for the `kind` you are about to use.** If something
else already claims it, you need a new scene — not a palette. Ask what physically
differs between the two products and draw *that*: bourbon and wine are separated by the
GLASS (short and heavy vs tall and stemmed), not by colour.
2. **Keep every mapping in one table.** The fifth row above happened because a journal
page chose its `kind` inline, where no registry could see it. A `kind` picked in page
code cannot be enumerated, cannot be diffed against the others, and fixing one copy
provably does not fix the rest.
### Checking it went live
When you re-point a `kind`, confirm the new one actually resolved:
```js
// in the browser console, on the page you changed
document.querySelectorAll('canvas').length // live tiles
document.querySelectorAll('svg.card-scene').length // static fallbacks
```
A `kind` that does not exist **falls back silently** to the static SVG — by design, so a
typo can never break a page. The cost is that a typo looks exactly like a styling choice.
If the fallback count is not zero, one of your kinds is misspelled.
Source of truth
This recipe ships inside the template as docs/prompts/make-a-live-tile.md — this page renders that exact file. Your own copy is always the newest version: take the template and you get every recipe locally, versioned with the release you are on, and each upgrade brings the newer ones with it. How to get it →