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 literalL, 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.paperDarkfalls back to the literal - Every
LVkey resolves to a--l-*custom property inlilith.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 nodata-lilith-themeattribute.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; elseprefers-color-scheme: dark→dusk; elsecream - Stored choice at
localStorage['oshun-lilith-theme']wins over system preference - System changes apply only when no user choice stored
- Cross-tab sync —
storageevent + customoshun-lilith-theme-changedevent re-apply variant - Attribute placement —
document.documentElementgetsdata-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 -
fontsalias ={ serif, sans, mono } -
typescale —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 fortara | 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 noonClickand nohrefrenders as placeholder: dimmed,aria-disabled="true",data-placeholder="true", tooltip "No handler wired" -
LCard— paper card with optional left accent stripe
Pills (pills.tsx)#
-
LSeverityPillS3 | S2 | S1 -
LStatusPillok | degraded | down | paused -
LDecisionChippending | approved | request | rejected -
LValidationPillok | warn | err | info -
LAIDisclosure— persistent ✻ mark for AI-assisted surfaces -
LSyncDotlive | offline | queued
Shells (shells.tsx)#
-
LWebShell— locks the cream-paper register viaLIGHT_LOCKED_VARSregardless 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 fromshells.tsxtoday (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 attimestamp -
LOffline— full-bleed banner withLSyncDot offline -
LPartialFailure—Partial · 3 of 4 loaded+ retry + missing list
Token contract — accessibility#
- Focus ring —
:focus-visibletwo-strokebox-shadow(paper inset + ink outer) atborder-radius: 4 -
prefers-reduced-motion: reduce—--l-motion-*collapse to0ms, all transitions/animations forced to0.001ms -
color-scheme—:rootlight;[data-lilith-theme='dusk']dark - Forced-colors — no explicit
@media (forced-colors: active)block;cream-hcis the reliance point for users onprefers-contrast: more - WCAG ratios —
lilith.csscomments annotate ≥4.5:1 (cream + dusk), ≥7:1 (cream-hc) -
::selectionterracotta on paper; customer scrollbars scoped via[data-shell-kind='customer']; nativedialogpicks 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-scopedStudio*components undercomponents/studio/layer on top ofLGraphPaper/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 — font + theme-color wiring at the root layout
- 06-keyboard-a11y.md — focus-ring + reduced-motion acceptance bar
- 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,LTopBarbut onlyLCustomerNavis exported fromshells.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 in06-keyboard-a11y.mdis acceptable, or add a forced-colors block tolilith.css -
--l-paper-rgb/--l-ink-rgbare referenced from overlay components for translucent surfaces — confirm every overlay reads RGB tuples (not hard-codedrgba(247,241,226,…)) so dusk doesn't break opacity stacks -
LBtnplaceholder 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