# Theme Engine — Lucian Labs system A

Not a look. A **contract for what a look is**, plus eleven instances of it and a
per-theme motion signature. Shipped in Forget Me Not (`tasks.lucianlabs.ca`).

Use this when the app must be re-skinnable — by the user, by a URL, or by you at
build time — rather than committed to one house look.

- Live reference: `https://lucianlabs.ca/branding/theme-engine.html`
- Source: `forget-me-not/src/themes.ts`, `src/styles.ts`, `src/animate.ts`

## The contract

A theme is one object. Everything the UI can vary lives in it; nothing visual
lives outside it.

```ts
type ThemeStyle = {
  name: string            // slug, used in URLs and storage
  label: string           // display name
  colors: {
    bg: string            // page ground
    surface: string       // cards, panels, inputs
    border: string        // every boundary
    text: string          // primary ink
    dim: string           // secondary ink, labels
    accent: string        // the theme's voice — one hue
    green: string         // ok / fresh
    orange: string        // warning / due soon
    red: string           // overdue / destructive
    cyan: string          // info / neutral highlight
  }
  borderRadius: number    // px, applied everywhere as --radius
  fontSize: number        // px base
  headerFont: string      // Google font family
  bodyFont: string        // Google font family
  fontFamily: string      // local fallback stack
  spacing: 'compact' | 'normal' | 'relaxed'
  animation: AnimStyle    // the theme's exit motion — see below
  sound: { preset, bpm, volume, mode }   // notification character
}
```

Ten colour roles, one radius, two faces, one density, one motion, one sound.
That is the whole surface area. **If a component needs a colour that isn't one
of the ten roles, the component is wrong, not the theme.**

The four semantic colours (`green`/`orange`/`red`/`cyan`) are separate from
`accent` on purpose: accent is identity, the other four are state. A theme
changes its accent freely; it may not repurpose `red` to mean something other
than overdue.

## Application

`applyTheme()` writes every token to `document.documentElement` as a custom
property — `--bg`, `--surface`, `--border`, `--text`, `--dim`, `--accent`,
`--green`, `--orange`, `--red`, `--cyan`, `--radius`, plus font and spacing
vars. Nothing else in the app reads a colour any other way.

Two consequences worth copying:

- **Fonts load lazily and once.** `loadGoogleFont()` keeps a `Set` of what it
  has injected and appends a `<link>` per new family. Switching themes pulls
  only the faces that theme needs; a session that never visits Sakura never
  downloads Kaisei Tokumin.
- **Resolve is preset + overrides.** `resolveTheme()` returns
  `{ ...preset, ...settings.custom* }`, so a user's custom colour, radius, font
  or spacing layers on top of any preset without forking it. User themes append
  to the same array as built-ins and are indistinguishable downstream.

Themes travel: `#theme=sakura` in the URL, copy/paste as JSON, or load via a
script tag. A look that can't be shared as data is a look that stays in one app.

## Motion is part of the theme

This is the idea worth stealing. Each theme names one **exit animation**, and
that animation is the theme's signature as much as its palette:

| Theme | Motion | Shape of it |
| --- | --- | --- |
| Midnight | `fade` | opacity out, scale to .95 — 0.4s ease-out |
| Sunrise | `float` | rises 40px and fades — 0.5s ease-in-out |
| Selva | `grow` | swells to 1.1 and blurs 4px — 0.5s ease-out |
| Kente | `slide` | feints right, then hard left off-screen — 0.4s, sharp cubic-bezier |
| Neon | `glitch` | 7 stepped keyframes, hue-rotates 720°, clip-path tears, collapses scaleY — 0.6s `steps(1, end)` |
| Cloud | `drift` | up 20px, right 30px, 2px blur — 0.6s ease-in-out |
| Terracotta | `crumble` | settles 4px, then falls 30px rotating 2° — 0.5s ease-in |
| Matcha | `zen` | 8px blur + brightness lift, barely moves — 0.7s ease-out |
| Vinyl | `spin` | 180° rotation shrinking to .3 — 0.5s |
| Océano | `wave` | rides up, down, then away on scaleX — 0.6s ease-in-out |
| Sakura | `petals` | 5 keyframes drifting up-and-sideways, rotating 8° — 0.7s ease-out |

Rules that make it work:

1. **One motion per theme, applied everywhere.** `animateOut()` reads the
   current theme and applies `fmn-anim-{style}`. No component picks its own.
2. **Enter is exit reversed.** `.fmn-anim-enter-{style}` replays the same
   keyframes with `reverse` at ~0.3s — faster in than out. One definition, two
   directions, guaranteed coherence.
3. **Motion matches temperament.** Zen dissolves, Neon tears itself apart,
   Vinyl spins like a record. The palette and the motion are describing the same
   personality; a theme whose motion contradicts its colours reads as broken.
4. **Animate, then collapse.** The element's height is measured and pinned
   before the animation, then `fmn-collapsing` transitions it to zero so the
   list closes smoothly. Both stages carry safety timeouts (800ms / 400ms) and
   resolve a promise, so a dropped `animationend` can never strand a row.

Durations sit between 0.4s and 0.7s — long enough to read as character, short
enough to stay out of the way.

## Building your own theme

1. Fill in all ten colour roles. Contrast-check `text` on `bg` and on `surface`.
2. Pick a radius and commit — it applies to every corner in the app.
3. Pick two faces: header and body. They may be the same.
4. Pick a density: `compact`, `normal`, `relaxed`.
5. Pick or write one exit animation, 0.4–0.7s, that matches the temperament.
6. Register it in the array. Everything downstream is already wired.

## Honest caveat

These eleven presets are a **user-facing customization system**, not the Lucian
Labs house brand. Several use faces the house ban list rejects for brand work
(Poppins, Inter, Nunito) — that's fine here, because the point of this system is
range, not identity.

Two things are still wrong and worth fixing: `midnight` and `neon` fall back to
`'SF Mono'` in their `fontFamily` stacks, which the house rule forbids outright
— that should be Inconsolata. And if you are building a Lucian Labs product
rather than a re-skinnable toy, use Console, Punch Clock, or House Pour and
take only the *architecture* from this page.
