Prima Get an API key

Module kit / Foundations

Design system and tokens

_shared/design-system-and-tokens

Kind shared
Applies to frontend
State written
Stack tailwind-3.4, alchemy-ui, prima-tokens, tokens-css, css-custom-properties
Depends on nothing: a root foundation
Reference implementation
  • alchemy_labs/frontend/tailwind.config.js
  • alchemy_labs/frontend/vite.config.ts
  • alchemy_labs/frontend/src/virtual-tailwind-brand.d.ts
  • alchemy_labs/frontend/src/styles/STYLE_GUIDE.md
  • alchemy_labs/frontend/src/index.css
  • alchemy_labs/frontend/src/components/alchemy-ui/index.ts
  • alchemy_labs/frontend/src/components/alchemy-ui/atoms/Button.tsx
  • prima.li/src/styles/tokens.css
  • prima.li/src/styles/global.css
  • prima.li/src/components/Eyebrow.astro
  • prima/packages/ui/src/styles.css
  • prima-chat/web/src/styles.css
  • prima-developer/src/styles/global.css
Measured on alchemy_labs d1a-token-source fd8558f 2026-09-11
Also measured on
  • prima.li main 9c01b14 2026-09-11
  • prima feature/per-model-pricing cd2c248 2026-09-11
  • prima-chat integration/redteam-fixes e606308 2026-09-11
  • prima-developer main 4428767 2026-09-11
  • alchemy.zuerich alchemy.zuerich_TRANSMUTATION2026 77291bb 2026-09-11
  • philosophers_stone main 92f33ab 2026-09-11
Version 1

Purpose

Every frontend recipe in this kit ships a rendering, and every rendering is built from one token file and one component vocabulary, so that a brand change is one edit and a demo in one recipe looks like a demo in any other. The invariant that makes it trustworthy is that a visual value (colour, font, size, radius, shadow, motion) exists in exactly one file per design system and is reached by name from everywhere else: a hex literal, a pixel font size or a font-family string inside a component is a defect, not a style choice.

This foundation fixes three things for every other recipe: which file is the token source (decision D1a, 2026-09-11), which design system a rendering uses by surface (decision D1b, 2026-09-11), and how the three systems relate through one shared token layer so a rendering can be re-skinned by swapping the token file rather than the components.

When to use

Include it from every kind: frontend recipe and from the two frontend skeletons (_shared/react-app-skeleton, _shared/astro-site-skeleton). Read it before writing a rendering: block, before adding a component, before choosing a colour, and before answering the question "which design system does this customer project get". Trigger phrases: "which purple", "where do the tokens live", "can I add a dark mode", "make the eyebrow uppercase", "the page stretches on a 4K monitor", "re-skin this for the client", "does the chat use alchemy-ui".

Do not use it to decide layout or information architecture (that is the skeleton of the surface) and do not use it for a client's own brand values: a client site copies the file shape described here and fills in the client's values from their own design source; those values live in the client's repo and the wiki's client page, never in this kit.

Stack assumptions

Three design systems exist, each with one token file. A recipe's rendering.design_system names one of them and nothing else.

Design systemSurfaces (D1b)Token file (the only source of visual values)Component vocabulary
alchemy-ui (the default)SPA, admin and agency surfaces, anything inside the React app shellalchemy_labs/frontend/tailwind.config.js (D1a)19 atoms and 5 molecules under frontend/src/components/alchemy-ui/, the app shell, /design-system
primaPrima product surfaces: prima.li, the chat web app, the developer portal, primacode, the desktop shellprima.li/src/styles/tokens.css plus global.css for the .t-* classes and the @font-face rulesAstro components in prima.li (Section, Button, Eyebrow, blocks); @prima/ui for conversation surfaces, themed through .prima-root custom properties
tokens-cssAstro client sites built to the EmDash blueprintthe site's own src/styles/tokens.css and global.css, same shape as prima.li'sthe seven blueprint components (Button, Section, Eyebrow, SectionIntro, Card, Navigation, Footer), specified in _shared/astro-site-skeleton; the bodies in this kit are the five under modules/ui-design-system/ui/astro-design-system-showcase/components/ (Button, Section, Eyebrow, Navigation, Footer, plus FeatureCard: the measured site's files at 77291bb, with that site's variant names) and the SectionIntro and Card bodies written out in S3's Reference implementation, until a packages/astro-blueprint workspace carries all seven (RENDERING.md section 6 and open item 7.5)

Versions, measured 2026-09-11: Tailwind ^3.4.17 with @tailwindcss/forms and @tailwindcss/typography in alchemy_labs/frontend/package.json (fd8558f); prima.li builds on Astro 4 (9c01b14); @prima/ui is 0.1.0, plain CSS, no build-time dependency on either (prima/packages/ui/package.json at cd2c248: sideEffects: ["*.css"], a ./styles.css export). Astro client sites follow S3's pins.

Fonts: ES Rebond Grotesque (Regular and Medium woff2) is the body and display face on all three systems; Tiempos Text italic is the single serif accent; TheSecretThings is the script accent; a mono face is used for facts (paths, commits, counts). All are self-hosted from /fonts (alchemy_labs/frontend/src/index.css lines 11 to 45; prima.li/src/styles/global.css lines 7 to 26; prima-chat/web/src/styles.css lines 53 to 76). The Labs Tailwind config also lists Karla and Advercase as fallbacks (fontFamily, lines 184 to 192). Licence: the faces are licensed to the agency; a customer project that inherits the file shape brings its own faces (see Security & tenancy).

Environment: none. A token file has no runtime configuration; nothing about it is read from import.meta.env or a VITE_* variable.

Data model

There is no database. The data is the token file, and its shape is the contract.

The canonical source (alchemy-ui, D1a). alchemy_labs/frontend/tailwind.config.js at fd8558f holds every hex value exactly once: a BRAND object (lines 19 to 29: orange, orange-dark, orange-light, purple, purple-dark, purple-light, cream, beige, beige-dark), BRAND_BLACK (line 7), and theme.extend keys that point at them: brand (line 53), primary.500 and primary.600 (66, 67), purple.DEFAULT, purple.500, purple.600 (79, 85, 86), alchemy.primary (166). Also there: the shell neutrals ink (DEFAULT, 60, 40), paper, surface, line (93 to 100), the semantic scales success, warning, error, info, warn, danger (121 to 127), fontFamily (184 to 192), borderRadius.pill and 4xl (193 to 196), boxShadow glass, glow, card, lift, pop (200 to 210), transitionDuration fast, slow, slower (211 to 215), the keyframes and animation names (223 to 272). darkMode: 'class' is declared (line 36) but the class is never set (see Invariants).

JavaScript that needs the raw string (a default stored with a record, an embed snippet for a third-party page, hex-alpha concatenation, contrast maths, a chart prop) imports it from a Vite virtual module generated from the same config: import { brand, brandBlack } from 'virtual:tailwind-brand' (vite.config.ts lines 6 to 32, types in src/virtual-tailwind-brand.d.ts). The module is built at dev-server start and at build; a config edit needs a restart.

The Prima layer (prima). prima.li/src/styles/tokens.css at 9c01b14 is one :root block of 160 custom properties: raw palette --c-* (lines 10 to 16), semantic aliases (19 to 41: --bg-page, --bg-surface, --bg-dark, --fg-default, --fg-muted, --fg-accent, --bg-accent, --fg-on-dark, --border-default, --border-strong, --fg-on-light), an on-image rebinding block (48 to 51), fonts (68 to 72), the clamped type scale --fs-display to --fs-overline (75 to 83), line heights (102 to 115), letter spacing (118 to 121), the spacing scale --space-xs to --space-4xl and --space-section (124 to 136), radii --radius-sm, --radius, --radius-lg, --radius-xl (139 to 143), layout widths --content-width 1360px, --content-narrow 760px, --page-gutter 48px (156 to 189), shadows (192 to 202), motion (205 to 209). Components read aliases only; the raw --c-* names appear in components in two documented exceptions (Button.astro's light variant paints --c-white; the stone ramp is scoped to the hero).

@prima/ui (prima/packages/ui/src/styles.css, lines 21 to 62) declares 25 custom properties on .prima-root: --prima-bg, --prima-bg-sunk, --prima-bg-raised, --prima-fg, --prima-fg-muted, --prima-fg-faint, --prima-line, --prima-line-strong, --prima-accent, --prima-accent-soft, --prima-warn, --prima-danger, --prima-ok (each with a -soft), --prima-radius, --prima-radius-sm, --prima-gap, --prima-pad, --prima-font (inherits the host), --prima-mono, --prima-size, --prima-size-sm, --prima-size-xs. A host overrides them on .prima-root or an ancestor and never forks a component (README, "Theming"). The chat web app does exactly that for the model picker (prima-chat/web/src/styles.css line 1851: --prima-accent: var(--violet-on-ink)).

The Astro layer (tokens-css). The same file shape as prima.li's tokens.css: --c-* raw palette, the nine semantic aliases of the standards guide (--bg-page, --bg-surface, --bg-dark, --fg-default, --fg-muted, --fg-accent, --fg-on-dark, --border-default, --border-strong), --fs-* (every heading step clamped), --lh-*, --space-*, --radius*, --content-*, --shadow-*, --transition-*, --font-display, --font-body; .t-h1 to .t-caption in global.css. The values are the client's.

The shared token layer (D1b). The names below exist in every system, and a rendering is re-skinned by changing their values in that system's file and nothing else:

Shared namealchemy-ui (Tailwind key)prima and tokens-css (custom property)@prima/ui (.prima-root)
accent (the purple)brand.purple, primary.500, purple.DEFAULT--fg-accent (via --c-violet)--prima-accent
ink (text on light)ink.DEFAULT--fg-default (via --c-ink)--prima-fg
muted textink.60--fg-muted--prima-fg-muted
page groundpaper--bg-page--prima-bg
raised surfacesurface--bg-surface--prima-bg-raised, --prima-bg-sunk
hairlineline--border-default--prima-line
success, warning, dangersuccess, warn, dangersite-specific; rule 12 below applies--prima-ok, --prima-warn, --prima-danger
body facefontFamily.sans--font-body--prima-font (inherits)
spacing stepTailwind's 4 px scale--space-* (0.5rem, 0.75rem, 1rem, 1.5rem, 2rem, 3rem, 4rem, 6rem: every step a multiple of 4 px)--prima-gap 12px, --prima-pad 16px
control radiuspill ratio of height (Button.tsx lines 31 to 38)--radius 20px for buttons, --radius-lg 28px for cards--prima-radius 10px

The canonical value of a shared token is what tailwind.config.js says (D1a). Measured 2026-09-11, the files do not all agree yet: the Labs purple is #6844C4 and the Labs ink is #1B1726; prima.li's --c-violet is #6642C1 and --c-ink is #1a1420 (and its --work-purple, line 220, is #6844C4); the developer portal's --purple is #6844C4 with --ink #1D1B17 (prima-developer/src/styles/global.css lines 33 to 45); @prima/ui's default --prima-accent is a gold, #8a6a1f, meant to be overridden by the host. Which of the two purples the Prima surfaces settle on is an open founder question listed under Gotchas; this recipe changes no shipped value. Until it is answered, a Prima-surface rendering uses prima.li's file as it is, and the acceptance check for the shared layer is the name contract, not hex equality.

Client state: none. Browser storage: alchemy-theme in localStorage is written by the Labs ThemeProvider and is always light (src/contexts/ThemeContext.tsx); no other system stores a theme.

API surface

No backend contract. The contract is between files:

  • tailwind.config.js exposes token names as Tailwind classes (bg-brand-purple, text-ink-60, border-line, shadow-card, rounded-pill, font-sans, animate-fade-up, duration-fast), as theme() calls inside src/index.css (theme('colors.brand.purple') at line 88), and as virtual:tailwind-brand for scripts.
  • tokens.css exposes custom properties; global.css exposes .t-* classes; components read both and export nothing.
  • @prima/ui/styles.css exposes .prima-* classes and the .prima-root custom properties; a host imports the stylesheet once (import '@prima/ui/styles.css', README "Using it") and overrides properties.
  • A frontend recipe exposes its rendering as rendering: { design_system, entry, tree, demo, states, hydrate, copy_language } in its frontmatter (RENDERING.md section 2, TEMPLATE-frontend.md); scripts/check.mjs refuses a written frontend recipe whose demo file is missing, whose exported states differ from rendering.states, or which still carries a screenshot or storybook key, and allows demo: null only with a dated demo_blocked_by.

Application logic

How a rendering gets built, in order:

  1. Pick the design system by surface (D1b): SPA and admin surfaces render in alchemy-ui; prima.li, chat, portal, primacode and desktop render in prima; an Astro client site renders in tokens-css. Write it in rendering.design_system. A recipe never mixes two on one page.
  2. Open the token file of that system before writing any component. Use only names that exist there. A value the design needs and the file lacks is added to the file with a semantic name, then used; it is never inlined.
  3. Style with the hybrid rule: Tailwind utilities (or plain flex and grid CSS on an Astro site) decide where things go (flex, grid, gap-*, items-center, px-*, hidden md:block); the token file decides how things look (colour, font, size, radius, shadow, border, transition). On an Astro component the look lives in the scoped <style> as var(--token); on a React component it is the Tailwind class the config emits. Tailwind's arbitrary-value syntax is never used for a visual value (bg-[#6844C4], bg-[var(--bg-surface)], text-[13.5px] are all defects): if the value is worth having it is worth a name.
  4. Compose from the vocabulary: on alchemy-ui, the atoms and molecules through the barrel src/components/alchemy-ui/index.ts (145 importing files at fd8558f); on prima and tokens-css, the shared Astro components. A block never re-declares a button, a card or a section wrapper.
  5. Thread tone: a dark section tells its children (tone="dark", tone="on-dark"); a child never hard-codes color: white.
  6. Keep type on the scale: .t-* classes on Astro sites; the Heading and Text atoms on alchemy-ui. Every heading token clamps, or none does; an h2 is --fs-h2 at every breakpoint.
  7. Set sentence case: no text-transform: uppercase and no non-zero letter-spacing unless the founder or the client asked for it on that element (founder rule, confirmed 2026-08-08, first applied on prima.li; prima.li/src/components/Eyebrow.astro is the reference: letter-spacing: 0; text-transform: none;).
  8. Cap the page at 1920 px: the whole page, not just the text column, is max-width: 1920px and centred with auto side margins on body (or the outermost wrapper); any position: fixed element (the header, a mobile overlay) carries the same cap (left: 50%; width: 100%; max-width: 1920px; transform: translateX(-50%)) or it spans the physical viewport while the page under it is capped (founder rule, 2026-08-08). The readable column (--content-width, max-w-[1100px] in the shell) is a separate, narrower thing.
  9. Write the rendering as the live demo (modules/<domain>/ui/<slug>/demo.tsx or demo.astro, modules/AUTHORING.md "Renderings"): the component rebuilt from the packages in this repo with literal sample data, one exported state per row of rendering.states; node tooling/verify/render-smoke.mjs renders every state, and node tooling/verify/compare.mjs compares a state against the app at the commit named in measured_on and writes its report under tooling/verify/out/, which is never committed.
  10. Document the rendering as a tree in rendering.tree (component names, the tokens the page uses, the states it renders), never as a paragraph.

Invariants

  • One token file per design system; a second palette (a tokens.ts, a variables.css, a style guide with its own hex values) is a defect. Labs retired its three on 2026-09-11 (commit fd105ee: alchemy-ui/tokens.ts, styles/variables.css, typography.css, components.css, a third legacy stylesheet and the 15-file components/design-system/ folder deleted, STYLE_GUIDE.md rewritten).
  • No brand hex literal in src/. On Labs at fd8558f, grep -rni "#6844C4" frontend/src returns only src/styles/STYLE_GUIDE.md (the guide's own grep line) and the unreferenced src/logo.svg. On an Astro site, grep -rnE "#[0-9a-fA-F]{6}" src/components src/layouts src/pages returns nothing beyond the dated exceptions its recipe lists.
  • Semantic names only in components: var(--fg-accent), never var(--c-violet); text-ink-60, never text-[#6B6577].
  • Light theme only. Labs' ThemeProvider removes .dark from <html> on mount and toggleTheme is a no-op (ThemeContext.tsx lines 29 and 40), so every dark: variant in the codebase is dead. @prima/ui defines a dark scheme (styles.css lines 64 to 121); a host that mounts it inside a light-locked app sets nothing and gets light.
  • Sentence case by default; uppercase and tracking are opt-in per element (step 7 above).
  • The page is capped at 1920 px (step 8 above).
  • Every heading token clamps or none does; no heading downgrades on mobile.
  • Only interactive elements have hover states: a button lifts 1 px, a linked card lifts 4 px, a static element does not move; :active resets the lift.
  • Semantic colours (success, warning, danger) carry meaning and are never re-themed by a partner accent; a semantic colour that appears as text uses the darker step that clears 4.5:1 on the canvas the text sits on.
  • A rendering is the live demo: component code in the recipe's folder, built from the packages, rendered by the docs host; a screenshot is a verification tool's output (compare.mjs), never the rendering and never committed under modules/.

Frontend

This foundation has no page of its own; its rendering is the component library page of each system, and the six ui-design-system recipes carry the trees and demos:

ui-design-system/
  ui/alchemy-ui-component-library   Labs /design-system: every atom and molecule on one page (alchemy-ui)
  ui/appshell-navigation            the three-column shell every SPA page renders inside (alchemy-ui)
  ui/onboarding-coachmarks          the coachmark overlay on the shell tokens (alchemy-ui)
  ui/merchant-home-dashboard        the Home page, tiles generated from the nav model (alchemy-ui)
  ui/prima-ui-package               @prima/ui: conversation, picker, usage, layered job (prima)
  ui/astro-design-system-showcase   the /design-system pattern for an Astro site (tokens-css shape)

The 13 rules of the standards guide, restated for a reader who has only this file:

  1. Never a raw value. Every colour, font size, spacing (except 0), shadow and font family in a component comes from a token. When the client changes the brand colour, one line in the token file changes and the site follows; with literals you grep and hope.
  2. Semantic names. Components read --fg-accent, not --color-3. The nine aliases every Astro project defines: --bg-page, --bg-surface, --bg-dark, --fg-default, --fg-muted, --fg-accent, --fg-on-dark, --border-default, --border-strong. Add more when the design needs them; start with these.
  3. Shared components exist and are used. At minimum Button (renders <a> or <button> from one component: const Tag = href ? 'a' : 'button'), Section (spacing, tone, container width) and Eyebrow; usually Card, SectionIntro, a Tag or Badge.
  4. Compose, do not duplicate. A block imports Section, Eyebrow, Button; it never copies their styles into its own <style>.
  5. Thread tone. Section(tone="dark") passes tone="on-dark" to Eyebrow and a secondary Button; the child adjusts itself; nothing checks its parent.
  6. Named slots for flexible composition. A hero with text, media and actions uses <slot name="media" /> and renders the wrapper only when Astro.slots.has('media').
  7. Variant props, not open class strings. variant?: 'primary' | 'secondary' | 'ghost', mapped to custom-property overrides on the root (.card--dark { --card-bg: var(--bg-dark); }), so every valid state is visible and testable.
  8. Standard interaction states. Buttons: hover translateY(-1px), active translateY(0), focus ring via outline. Linked cards: hover translateY(-4px) plus a stronger shadow, active translateY(-2px). Static elements: no hover. Always transition, always define :active.
  9. Type scale discipline. Only the .t-* classes (.t-h1 to .t-caption, .t-overline) or the Heading and Text atoms. Never a freestyle font-size. If a token in the heading scale clamps, all of them clamp with a shared cap and ordered floors, or the hierarchy inverts on a phone (measured on a client site, 2026-07-23: an h4 at a fixed 32 px rendered larger than an h3 clamped to 28 px at 390 px width, for months). Grid tracks are minmax(0, 1fr), not bare 1fr, and shrinkable flex children get min-width: 0, or a long German word bursts the page sideways; overflow-wrap: break-word on body is the last resort, not the fix. Measure horizontal overflow per route, locale and breakpoint with a same-origin iframe (documentElement.scrollWidth - clientWidth must be 0); one sweep on a client site was 72 combinations with 4 breaks, one at 1024 px that "test on a phone" would never have found.
  10. Split brand from accent when a UI can be co-branded. The accent token (buttons, selected states, focus rings, tints) may be recoloured by a partner; the brand token (the mark, its glow, the "powered by" line, the signature motif) never is; semantic colours follow nobody. Derive tints from the token (color-mix(in srgb, var(--accent) 16%, transparent)), never as frozen rgba(<brand rgb>, .12) literals: audit them with a grep for the brand's rgb and classify every hit.
  11. A state class is never a layout class. .done, .active, .open describe state; a container gets a noun that can only be a place (.finishscreen, .rail). A layout rule on .done once broke three unrelated components whose completed items inherited a 1280 px max-width and 90 px of padding (measured 2026-07-29).
  12. Fill colours are not text colours. A mid-ramp amber, orange or green passes 3:1 as a chip fill and fails 4.5:1 as text on white (amber-600 at 2.94:1, green-600 at 3.30:1); default to the 700 step for any semantic colour that can appear as text, check against the canvas (an off-white ground costs a few tenths), and split the token when one colour does both jobs.
  13. Supplied artwork is not a component. Check that an "SVG" is vector and not a wrapped PNG (magic bytes, not the extension); a raster cannot take currentColor, will not follow a theme and blurs when scaled. Confine rasters to surfaces that never re-theme; put mixed artwork in identical fixed-size chips with a transparent spacer for the empty slot; never ship a third-party mark unless the thing it labels names that product; crop label bands out of rasters so a language toggle does not show the wrong language; encode at 2 to 3 times the display box, not at the export size.

Plus the two founder rules (2026-08-08), applying to every EmDash and Prima surface and, since D1b, to alchemy-ui renderings:

  1. No uppercase, no letter-spacing, unless asked. Not on eyebrows, kickers, labels, chips, badges, nav links or buttons. Default to sentence case and letter-spacing: 0. Add either property only when the founder or the client asks for that treatment on that element. An earlier hero-kicker exception is retired.
  2. The page is capped at 1920 px, always. max-width: 1920px, centred, on body (not per section), so full-bleed photos and dark bands stop growing on ultra-wide monitors; every position: fixed element carries the same cap or it spans the physical viewport while the page sits under it capped and centred.

The hybrid styling rule, in one table:

SystemJobUse for
Tailwind utilities (or plain layout CSS on an Astro site)where things goflex, grid, gap-4, items-center, px-4, hidden md:block, max-w-*
the token file, read by class (alchemy-ui) or by var() in a scoped <style> (prima, tokens-css)how things lookcolour, font, font size, shadow, radius, border, transition

Never Tailwind's arbitrary-value syntax for a visual property, in either system.

How a customer project picks one: an admin or operator app that people sign into gets alchemy-ui and the React skeleton (S2); a marketing site, portal or docs site gets an Astro site (S3) with tokens-css (the client's values in the same file shape); a Prima product surface, or a surface that embeds a Prima conversation, gets prima, and the conversation itself is @prima/ui themed through .prima-root. A project that needs two of them (an Astro site with one signed-in React island, say) keeps the token values equal by construction: the island reads the same custom properties the site defines, either by mounting inside .prima-root or by generating the Tailwind theme.extend.colors from the same --c-* values at build time. Whichever way, the values are typed once.

Responsive and states: a design-system rendering shows every state the vocabulary has (default, hover, active, focus, disabled, loading) at 1440 px, and the same page at 390 px; the harness produces both. Empty and error states belong to the page recipes, not to the token layer.

Security & tenancy

Nothing in a token file is sensitive; nothing in it is per-tenant. The rules that still apply:

  • A VITE_* variable is public and string-inlined into the bundle; the token layer never reads one, so there is nothing to leak here, and a theme loaded from a third-party stylesheet at runtime is not a pattern this kit uses. Fonts are self-hosted on every surface for the same reason the chat app's stylesheet header gives: a Swiss sovereignty product does not fetch a font from a CDN on load.
  • Fonts under the agency's licence (ES Rebond Grotesque, Tiempos Text, Advercase, TheSecretThings, Helvetica Now on the developer portal) ship only inside the agency's own surfaces. A customer clone of this kit does not receive the font files; the @font-face block is where they drop their own licensed files, and the fallback stack (-apple-system, BlinkMacSystemFont, sans-serif) renders until they do.
  • Co-branding (rule 10) is a tenancy concern in disguise: a partner's accent must not be able to repaint the brand mark or the semantic colours, so the split is enforced by token naming, not by review.
  • The demo /design-system route in Labs is public and unauthenticated by design (App.tsx line 216, no PrivateRoute); it renders no data and no user content. Fail direction for the harness: a rendering with a thrown page error is not evidence (page_errors must be empty).

Tests

  • Token grep gates, run before every commit on the surface: Labs, grep -rli "6844c4" frontend/src | wc -l equals 2 at fd8558f (the guide and logo.svg) and 0 on a new app; Astro site, grep -rnE "#[0-9a-fA-F]{6}" src/components src/layouts src/pages prints only the dated exceptions and grep -rn "font-family: *'" src/components prints nothing.
  • Case and tracking gate (rule 14): grep -rnE "text-transform: *uppercase|uppercase tracking-" src lists only elements with a written exception. Measured 2026-09-11 on prima.li 9c01b14: global.css .t-overline and .t-mono-label (lines 96 to 107) still carry text-transform: uppercase, and three pages use them; on Labs fd8558f the same grep over src/pages/Home.tsx and src/onboarding/Coachmark.tsx prints 6 lines. All are pre-rule code and are listed in the respective recipes' Gotchas.
  • Cap gate (rule 15): open the site at 2560 px wide; document.body.getBoundingClientRect().width is 1920 and the fixed header's width is 1920.
  • Type-scale gate (rule 9): grep -c "clamp(" src/styles/tokens.css for --fs-h* equals the number of --fs-h* tokens; the overflow sweep from S3 (a same-origin iframe per route, locale and breakpoint) reports 0 px overflow.
  • @prima/ui: npm test in prima/packages/ui runs node --test test/core.test.cjs; the test named "the package contains no hardcoded model alias" walks every .ts, .tsx and .css file (lines 35 to 90), so a model name in the stylesheet fails the build.
  • alchemy-ui: no colocated test for the atoms at fd8558f; the render check is the /design-system page (this kit's alchemy-ui-component-library demo) and npm run build (tsc --noEmit && vite build), which passed on both D1a commits (fd105ee, fd8558f, 2026-09-11) with vitest unchanged at 160 passed and one pre-existing failure in App.test.tsx.
  • Render check and comparison: node tooling/verify/render-smoke.mjs renders every state of every React demo with fetch stubbed (the jsdom check every frontend recipe's Tests section names); node tooling/verify/compare.mjs <recipe> <state> captures the demo's frame and the app's route, runs pixelmatch and reports mismatch_ratio, and a ratio above the recipe's stated tolerance at a later commit is a regression.

Deploy & ops

A token file is built into the surface that uses it and has no deploy of its own:

  • alchemy-ui: tailwind.config.js is read by Vite at build; the classes it emits are tree-shaken by the content globs (./index.html, ./src/**/*.{js,jsx,ts,tsx}, lines 32 to 35). A token used only from a string that Tailwind cannot see (a computed class name) is purged; use theme() in index.css or the virtual module for those. Build path: S2, scripts/build-frontend-staging.sh.
  • prima and tokens-css: global.css imports tokens.css on its first line and is imported once in the base layout; Astro inlines or hashes it at build. Fonts ship from public/fonts/ as verbatim files. Build path: S3 and S13.
  • @prima/ui: npm run build (tsc -p tsconfig.build.json) writes dist/; the stylesheet is exported from src/ unbuilt ("./styles.css": "./src/styles.css"). The chat web app's Dockerfile (prima-chat e606308, lines 26 to 34) runs npm ci and npm run build inside prima/packages/ui/ in the image before building the app, because package.json depends on it by relative path ("@prima/ui": "file:../prima/packages/ui").
  • CSP: fonts and styles are same-origin on every surface; no font-src or style-src entry for a third party is needed. Labs' index.html loads Karla from Google Fonts (S2 Deploy & ops), the one exception, and only on Labs.
  • Rollback is the surface's rollback; a token change is a normal commit on the surface's branch.

Gotchas

  • 2026-09-11 (measured at b1089b6, fixed at fd8558f): #6844C4 appeared 1040 times in 130 files under frontend/src, 993 of them inside Tailwind arbitrary values such as bg-[#6844C4]/10; siblings #BA573D (73 in 13 files), #A04D35 (7 in 2), #FBF6F0 (46 in 9). Fix (commit fd8558f): the config hoists the palette into one BRAND object, 1105 arbitrary-value classes in 112 files became token classes with the same opacity modifiers, 24 files that need the string import brand from virtual:tailwind-brand; the built CSS was diffed rule by rule and nothing visible changed.
  • 2026-09-11 (measured at fd8558f): 156 arbitrary hex classes in 48 files remain, none of them a brand value: #5D4EA7 (28), #907BE6 (12, the pre-rebrand purple), #6642C1 (11, prima.li's violet), #9d8ef0, #9F8FEF, #6359c7 (the Button primary hover, line 43) and greys. They are the next grep-replace; a new app starts with zero.
  • 2026-09-11 (measured): the two purples. Labs #6844C4; prima.li --c-violet #6642C1 (with --work-purple #6844C4 in the same file); the developer portal --purple #6844C4; the chat app's header comment (prima-chat/web/src/styles.css lines 1 to 12) calls #6844c4 the old identity superseded by the brand guide and copies prima.li's #6642c1. D1a makes the Tailwind config canonical, D1b makes prima.li's stylesheet the Prima layer; they disagree by two hex digits. Open founder question: reconcile prima.li (and the chat app) to #6844C4, or record #6642C1 as the Prima accent and update the Tailwind BRAND.purple. Until then no recipe changes either value.
  • 2026-09-11 (measured): the developer portal (prima-developer 4428767) still runs the older Prima token set (--purple, --sand, --ink #1D1B17, Helvetica Now) that the chat app says was superseded; its own header comment says the tokens are copied verbatim from prima.li's global.css, which is no longer true. A rendering of the portal is honest about being on the older set until it is re-pointed.
  • 2026-09-11 (measured): @prima/ui's default accent is a gold (#8a6a1f, styles.css line 32), not the purple. It is a placeholder for the host's override; a host that forgets to set --prima-accent ships gold controls.
  • 2026-09-11 (measured, prima.li 9c01b14): the 1920 px cap is applied to .pnav-inner (PrimaNav.astro line 158) and .pha-inner (PrimaHeroAlps.astro line 92), not to body (PrimaBase.astro line 51 carries no cap). The founder rule says body; the page reads as capped because the header and hero are, and a full-bleed band added later will not be. On the agency site (77291bb) the same per-block pattern appears (BrandBand.astro line 38, CaseIntro.astro line 55, ProjectConfigurator.astro line 367).
  • 2026-09-11 (measured, prima.li 9c01b14): .t-overline and .t-mono-label in global.css set text-transform: uppercase with --ls-wide and --ls-label; DocsShell.astro, waitlist.astro and work-with-us.astro use them. The Eyebrow component is sentence case as the rule says; the overline classes are the exception that predates the rule.
  • 2026-09-11 (measured, Labs fd8558f): darkMode: 'class' is still declared in tailwind.config.js and the atoms still carry dark: variants (Button.tsx lines 45 to 68) that can never apply. Writing new ones is wasted work and a false promise in a demo; deleting the existing ones is a cleanup, not a fix.
  • 2026-09-11 (measured, Labs fd8558f): the brand-legacy teal compat scale (tailwind.config.js lines 151 to 162) maps the old numeric brand-500 onto neutrals so a leftover class resolves to grey instead of teal; the config's own note says to remove it once the no-orphan grep passes. Do not copy it into a new app.
  • 2026-09-11 (measured): alchemy-ui's shell uses pixel-precise arbitrary sizes for type (text-[13.5px], text-[11px] in AppShell.tsx and Home.tsx) rather than a named scale. They render correctly and they are the kind of literal rule 1 forbids; a new app names them in fontSize of the config.
  • 2026-08-08 (founder correction, wiki changelog): the brand system's source of truth is the component files of the agency site's theme, not a newer landing page built on it; a newer single page is a consumer of the system, not the system. The same reasoning is why D1a names the config file and not the pages that use it.
  • 2026-09-11 (measured): re-rendering Labs /design-system and /dashboard at fd8558f produced PNGs byte-identical to the captures taken at b1089b6 (same md5 on all four files), which is the visual half of the D1a proof that replacing 1105 literal classes with tokens changed nothing.
  • 2026-09-11 (harness): tooling/render/shot.mjs recorded authenticated: true for a logged-out page whenever a session file supplied only the consent entry; fixed the same day so the flag means a token or a cookie was applied, and --scroll-to <selector> was added for a page whose subject sits below a preamble. The two evidence files captured before the fix were re-rendered at fd8558f.

Reference implementation

Paths are relative to each repo's root; commits as in measured_on and also_measured_on.

  • alchemy_labs (branch d1a-token-source, forked from feat/service-desk at b1089b6; D1a commits fd105ee and fd8558f, 2026-09-11):
    • frontend/tailwind.config.js: BRAND_BLACK (7), BRAND (19 to 29), colors (39 to 183), fontFamily (184 to 192), borderRadius (193 to 196), boxShadow (200 to 210), transitionDuration, spacing, letterSpacing (211 to 222), keyframes and animation (223 to 272), plugins (275 to 278).
    • frontend/vite.config.ts lines 6 to 32: tailwindBrandPlugin, the virtual:tailwind-brand source; frontend/src/virtual-tailwind-brand.d.ts: its types.
    • frontend/src/styles/STYLE_GUIDE.md: the retirement table, the token table, the four ways to reach a token, the rules.
    • frontend/src/index.css: @font-face (11 to 45), @layer base with theme('colors.brand.purple') (85 to 88), .card (252), .ctx-link (271 to 278), .chip (431).
    • frontend/src/components/alchemy-ui/index.ts, atoms/index.ts (19 atom modules), molecules/index.ts (5); atoms/Button.tsx lines 31 to 38 (the pill ratio), 40 to 70 (variants).
    • frontend/src/contexts/ThemeContext.tsx line 29 (classList.remove('dark')), 40 (toggleTheme: () => {}).
    • frontend/src/pages/DesignSystemDemo.tsx (843 lines): the public showcase, routed at App.tsx line 216.
  • prima.li (main, 9c01b14): src/styles/tokens.css (405 lines, sections as listed under Data model), src/styles/global.css (fonts 7 to 26, reset 38 to 49, .t-* 52 to 59, .t-section-title 65 to 74, .t-overline 96 to 107), src/components/Eyebrow.astro (the sentence-case eyebrow), src/components/Button.astro (seven variants on tokens), src/components/Section.astro, src/layouts/PrimaBase.astro, src/components/PrimaNav.astro line 158.
  • prima (feature/per-model-pricing, cd2c248): packages/ui/src/styles.css (922 lines; .prima-root 21 to 62, schemes 64 to 121), packages/ui/README.md ("Theming", "The three hosts"), packages/ui/package.json (exports).
  • prima-chat (integration/redteam-fixes, e606308): web/src/styles.css (header 1 to 52, fonts 53 to 76, tokens 77 to 130, .picker-slot override 1851 onward), Dockerfile lines 26 to 34.
  • prima-developer (main, 4428767): src/styles/global.css lines 1 to 60 (the older Prima set and the Helvetica Now faces).
  • alchemy.zuerich (the agency's own Astro site, branch alchemy.zuerich_TRANSMUTATION2026, 77291bb): src/styles/tokens.css (the same --c-* values as prima.li's file), src/data/ds-registry.ts, src/components/_ds/ (the showcase scaffold; see ui-design-system/ui/astro-design-system-showcase).
  • Wiki philosophers_stone (92f33ab): guides/design-system-standards.md (updated 2026-08-28, rules 1 to 13 and the health checklist), guides/alchemy-brand-system.md (the eyebrow rule and the 2026-08-08 correction), guides/design-system-showcase.md (2026-08-24), meta/changelog.md entry of 2026-08-08 (the 1920 px cap and the uppercase rule as founder decisions).

Deviations of the references from this recipe, all dated 2026-09-11: the two purples and the developer portal's older set (Gotchas); the 1920 cap applied per block instead of on body; .t-overline uppercase on prima.li; uppercase tracking-* headings in Home.tsx and Coachmark.tsx; 156 non-brand arbitrary hex classes and pixel-literal type sizes in the Labs shell; dead dark: variants and the brand-legacy shim; @prima/ui's gold default accent; darkMode: 'class' declared on a light-locked app.

Acceptance checks

Run without the author at the commits named above. Expected observations are exact.

  1. Labs, frontend/: grep -rli "6844c4" src | wc -l prints 2; grep -rli "6844c4" src prints src/logo.svg and src/styles/STYLE_GUIDE.md and nothing else. ls src/components/alchemy-ui/tokens.ts src/styles/variables.css src/components/design-system 2>&1 reports all three as missing.
  2. Labs: node -e "const c=require('./tailwind.config.js').theme.extend.colors; console.log(c.brand.purple, c.primary[500], c.purple.DEFAULT, c.alchemy.primary)" prints #6844C4 four times.
  3. Labs: npm run build exits 0; with npm run dev running, curl -s http://127.0.0.1:3000/@id/__x00__virtual:tailwind-brand returns a module beginning export const brand =.
  4. Labs: open /design-system in a browser at 1280 px and measure the three button sizes in the inspector: heights 40, 48, 64 px; radii 16, 20, 26 px. Compare with modules/ui-design-system/ui/alchemy-ui-component-library.png.
  5. Labs: in devtools, document.documentElement.classList.contains('dark') is false after any navigation; localStorage.getItem('alchemy-theme') is "light".
  6. prima.li: grep -c "^ --" src/styles/tokens.css prints 160; grep -rnE "#[0-9a-fA-F]{6}" src/components src/layouts src/pages | wc -l prints 8 (the dated exceptions S3 lists) and 0 on a site that keeps rule 1. grep -c "letter-spacing: 0" src/components/Eyebrow.astro prints 1.
  7. prima.li: grep -n "1920px" src/components/PrimaNav.astro src/layouts/PrimaBase.astro prints the nav line and nothing from the layout (the deviation in Gotchas); on a site that keeps rule 15 the layout's body rule prints instead.
  8. @prima/ui: cd prima/packages/ui && npm test passes, including "the package contains no hardcoded model alias"; sed -n 21,62p src/styles.css | grep -c "^ --prima-" prints 25.
  9. Shared layer, name contract: grep -c "brand.*purple\|ink:\|paper:\|surface:\|line:" tailwind.config.js in Labs prints 8; grep -cE "^ --(fg-accent|fg-default|fg-muted|bg-page|bg-surface|border-default):" src/styles/tokens.css in prima.li prints 6; sed -n 21,62p src/styles.css | grep -cE "^ --prima-(accent|fg|fg-muted|bg|bg-raised|line):" in @prima/ui prints 6.
  10. Shared layer, value check: node -e "console.log(require('./tailwind.config.js').theme.extend.colors.brand.purple)" in Labs and grep -m1 "c-violet:" src/styles/tokens.css in prima.li print the same hex. Expected to fail on 2026-09-11 (#6844C4 versus #6642C1); it passes once the founder answers the open question in Gotchas, and this recipe's version is bumped with the date.
  11. Rendering: every written kind: frontend recipe has rendering.demo pointing at a demo.tsx or demo.astro that exists beside it (or demo: null with a dated demo_blocked_by), rendering.states equal to the demo's exported state keys in order, and no screenshot or storybook key; find modules -name '*.png' | wc -l prints 0; node tooling/verify/render-smoke.mjs renders every state without a page error; npm run check in this repo prints 0 problems.

Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.