# Shell: Design system and global tokens

Source: `apps/oshun/web/src/design-system/lilith/tokens.ts`,
`apps/oshun/web/src/design-system/lilith/lilith.css`,
`apps/oshun/web/src/design-system/lilith/primitives.tsx`,
`apps/oshun/web/src/design-system/lilith/pills.tsx`,
`apps/oshun/web/src/design-system/lilith/shells.tsx`,
`apps/oshun/web/src/design-system/lilith/edge-states.tsx`,
`apps/oshun/web/src/design-system/lilith/index.ts`,
`apps/oshun/web/src/design-system/lilith/LilithThemeBootstrap.tsx`

Lilith is the design system — one paper-warm palette, one terracotta accent,
three theme variants, two registers (manuscript for customer surfaces,
plate-of-record for operator surfaces). The token contract is the load-bearing
piece: literal `L.*` constants are locked at compile time, `LV.*` CSS-variable
references react to the active theme. Walk this whenever a token is added, a
theme variant changes, or a primitive grows new states.

## The L object (compile-time) and LV (CSS vars)

From `tokens.ts`. `L` is literal hex (locked to cream-paper). `LV` exposes the
same keys as `var(--l-…)` references. **Most files import `LV as L` from
`@/design-system/lilith`** so theme swaps take effect automatically.

- [ ] `L.bg #f1ebdd`, `L.paper #f7f1e2`, `L.paper2 #ede4cf` (surfaces)
- [ ] `L.ink #1f1a14`, `L.ink2 #3b3325`, `L.soft #6b5d49` (foreground)
- [ ] `L.muted = var(--l-muted)` — always a CSS-var, even on literal `L`, so
      contrast stays valid across variants
- [ ] `L.rule #d9cfb7`, `L.rule2 #e6dec6`, `L.grid #dfd4b4`, `L.chip #e8dfc7`
- [ ] `L.accent #9a3e1c` (terracotta), `L.accent2 #c97247`
- [ ] `L.ok / warn / alert` = `var(--l-…)` (theme-aware status)
- [ ] `L.paperDark #221d14` — no CSS-var counterpart; `LV.paperDark` falls back
      to the literal
- [ ] Every `LV` key resolves to a `--l-*` custom property in `lilith.css`

## Theme variants

`L_VARIANTS` in `tokens.ts` + CSS overrides in `lilith.css`:

- **`cream`** — base register. Cream paper (`#f1ebdd`), terracotta accent
  (`#9a3e1c`), ink (`#1f1a14`). Default when no `data-lilith-theme` attribute.
- **`cream-hc`** — high-contrast accessibility (≥7:1). Selector
  `[data-lilith-theme='cream-hc']`. Status colors lifted, ink to true black.
- **`dusk`** — evening reading register (warm low-light, **not** a dark theme).
  Selector `[data-lilith-theme='dusk']`. `color-scheme: dark`, paper goes to
  `#2c2418`, accent lifts to `#e58a5e`, domain text accents and KG entity hues
  re-tuned for ≥4.5:1 against the dark paper.

### `LilithThemeBootstrap`

- [ ] **First visit** — reads `prefers-contrast: more` → `cream-hc`; else
      `prefers-color-scheme: dark` → `dusk`; else `cream`
- [ ] **Stored choice** at `localStorage['oshun-lilith-theme']` wins over system
      preference
- [ ] **System changes** apply only when no user choice stored
- [ ] **Cross-tab sync** — `storage` event + custom `oshun-lilith-theme-changed`
      event re-apply variant
- [ ] **Attribute placement** — `document.documentElement` gets
      `data-lilith-theme="cream-hc" | "dusk"` (cream removes the attribute)

## Font stacks

From `tokens.ts`:

- [ ] **`SERIF_L`** = `"Cormorant Garamond", "EB Garamond", Georgia, serif`
- [ ] **`SANS_L`** = `"Inter", system-ui, sans-serif`
- [ ] **`MONO_L`** =
      `"JetBrains Mono", "IBM Plex Mono", ui-monospace, monospace`
- [ ] **`fonts`** alias = `{ serif, sans, mono }`
- [ ] **`type`** scale — `display` (40/56/72/96), `headline` (22/28/36/56),
      `body` (14/16/18), `ui` (9/10/11/12/14), `mono` (9/10/11/12)

## Primitives (`primitives.tsx`)

Inline-style components — token values come from `LV as L`, so theme-aware.

- [ ] `LEyebrow`, `LRule` / `LRule2` / `LDotRule` / `LDoubleRule` / `LThickRule`
      (hairlines), `LColumnRule`
- [ ] `LChip`, `LCode`, `LField` (key/value), `LSourceItem` (citation row)
- [ ] `LDisplay`, `LDropCap`, `LOpenQuote`, `LMasthead`, `LPullQuote`
- [ ] `LDomainMark` — SVG glyph for
      `tara | arete | veritas | nyx | nisaba | lilith | metis`
- [ ] `LConfidence` — five-dot rail
      (`strong | moderate | mixed | contested | evolving`)
- [ ] `LDots` — segmented progress
- [ ] `LBtn` — `primary | accent | ghost | quiet` × `sm | md | lg`. With no
      `onClick` and no `href` renders as placeholder: dimmed,
      `aria-disabled="true"`, `data-placeholder="true"`, tooltip "No handler
      wired"
- [ ] `LCard` — paper card with optional left accent stripe

## Pills (`pills.tsx`)

- [ ] `LSeverityPill` `S3 | S2 | S1`
- [ ] `LStatusPill` `ok | degraded | down | paused`
- [ ] `LDecisionChip` `pending | approved | request | rejected`
- [ ] `LValidationPill` `ok | warn | err | info`
- [ ] `LAIDisclosure` — persistent ✻ mark for AI-assisted surfaces
- [ ] `LSyncDot` `live | offline | queued`

## Shells (`shells.tsx`)

- [ ] `LWebShell` — locks the cream-paper register via `LIGHT_LOCKED_VARS`
      regardless of active Lilith theme (keeps broadsheet prose readable when
      user is in dusk)
- [ ] `LCustomerNav` — primary nav (Today / Explore / Activity / Library),
      active accent underline, ⌘K search affordance, Ask Lilith, avatar
- [ ] `LSubNav` — sub-nav row with active-item terracotta underline
- [ ] `LSectionHead` — section heading with kicker + title + right slot
- [ ] `LPhoneShell` / `LStatusBar` / `LHomeIndicator` / `LTabBar` — phone-mockup
      chrome (design comps + onboarding)
- [ ] `LGraphPaper`, `LOperatorBar`, `LDataCell`, `LDataRow` — operator
      (plate-of-record) chrome
- [ ] README references `LMobileNav`, `LSidebar`, `LTopBar` — not exported from
      `shells.tsx` today (see Open questions)

## Edge states (`edge-states.tsx`)

The calm-under-stress register — no shimmer, no alarming red, no spinners.
Collectively the contract for empty / loading / drifted / completed states
across the customer surface; per-view files reference these as `LEdgeState`.

- [ ] `LSkeleton` (dotted-rule lines), `LEmpty` (kicker + serif italic title +
      body + single action), `LPwaUpdate` ("A new edition." ribbon)
- [ ] `LError` — stamped notice (`Notice · E.04`), serif title, action,
      `logged at` timestamp
- [ ] `LOffline` — full-bleed banner with `LSyncDot offline`
- [ ] `LPartialFailure` — `Partial · 3 of 4 loaded` + retry + missing list

## Token contract — accessibility

- [ ] **Focus ring** — `:focus-visible` two-stroke `box-shadow` (paper inset +
      ink outer) at `border-radius: 4`
- [ ] **`prefers-reduced-motion: reduce`** — `--l-motion-*` collapse to `0ms`,
      all transitions/animations forced to `0.001ms`
- [ ] **`color-scheme`** — `:root` light; `[data-lilith-theme='dusk']` dark
- [ ] **Forced-colors** — no explicit `@media (forced-colors: active)` block;
      `cream-hc` is the reliance point for users on `prefers-contrast: more`
- [ ] **WCAG ratios** — `lilith.css` comments annotate ≥4.5:1 (cream + dusk),
      ≥7:1 (cream-hc)
- [ ] **`::selection`** terracotta on paper; **customer scrollbars** scoped via
      `[data-shell-kind='customer']`; **native `dialog`** picks up Lilith chrome
      (paper bg, ink border, sans font, soft shadow)

## When NOT to use Lilith primitives

- **Studio / operator surfaces** (`/lilith-studio/*`, admin inbox, the Bellona /
  Yemaya / Hathor workspaces) — workspace-scoped `Studio*` components under
  `components/studio/` layer on top of `LGraphPaper` / `LOperatorBar` /
  `LDataCell` / `LDataRow`
- **Domain content kernels** with authored visual fidelity (Veritas claim cards,
  Atelier bake/mix/cast) — consume tokens but not primitives
- **Marketing / public routes** — minimal header variant bypasses `LCustomerNav`
  / `LSubNav`; tokens still in play

## Cross-references

- [01-app-shell.md](./01-app-shell.md) — font + theme-color wiring at the root
  layout
- [06-keyboard-a11y.md](./06-keyboard-a11y.md) — focus-ring + reduced-motion
  acceptance bar
- [12-overlays-and-panels.md](./12-overlays-and-panels.md) — overlay chrome
  built on these primitives
- README: `apps/oshun/web/src/design-system/lilith/README.md`

## Open questions / known gaps

- [ ] README references `LMobileNav`, `LSidebar`, `LTopBar` but only
      `LCustomerNav` is exported from `shells.tsx` — confirm whether the other
      three are coming online or whether the README is aspirational
- [ ] No explicit `@media (forced-colors: active)` styles — verify the Windows
      high-contrast walkthrough in `06-keyboard-a11y.md` is acceptable, or add a
      forced-colors block to `lilith.css`
- [ ] `--l-paper-rgb` / `--l-ink-rgb` are referenced from overlay components for
      translucent surfaces — confirm every overlay reads RGB tuples (not
      hard-coded `rgba(247,241,226,…)`) so dusk doesn't break opacity stacks
- [ ] `LBtn` placeholder treatment (`data-placeholder="true"` when no handler)
      is documented but not enforced by a lint — consider a codemod sweep to
      catch unwired buttons before they ship
