Fragua design language
Clarity through restraint — data is the decoration, structure is the grid, motion indicates state.
Foundational language for packages/web. If a pattern isn't here, derive it from the principles. When principles conflict, restraint wins — the default answer to "should I add something" is no.
Principles (earlier wins on conflict)
- Calm control & progressive disclosure. Surface status and the 2–3 numbers that matter. Logs, metadata, and secondary actions live behind hover, tooltip, or drawer. More than ~7 primary signals per view is failure.
- Data as decor. Sparklines, status dots, and aligned numbers are the visual rhythm. No shadows, decorative gradients, or heavy borders. Ornament gets deleted.
- Strict structural layout. Bento grid, tight consistent padding, sections separated by a hairline — never by a different background shade. Flex rows for micro-data:
[status] [name] [metric].
- Typographic discipline. Monospace voice. Hierarchy via weight, case, and spacing — never size jumps. Numbers always tabular.
- Restrained semantic color. Surfaces nearly indistinguishable from background. Accents reserved for state — not branding.
- Functional motion. Motion says "this is happening." If removing it loses no information, remove it.
Stack & how tokens fit the codebase
Tailwind v4, CSS-first config. There is no tailwind.config.ts. All theme mapping lives in packages/web/src/styles/globals.css inside an @theme inline { … } block; raw design-language CSS vars live in packages/web/src/styles/theme.css. The build pipeline is @tailwindcss/vite (no PostCSS, no Autoprefixer — v4 handles vendor prefixing via Lightning CSS). Animations come from tw-animate-css (imported as @import "tw-animate-css"; in globals).
Two token layers coexist. Know which to reach for before writing a class:
--sw-* (Fragua design language) — defined in theme.css. Surfaces, state accents, typography scale, spacing scale, radii, motion durations. Exposed as Tailwind utilities via @theme inline: bg-sw-surface, text-sw-muted, border-sw-border, bg-sw-accent-thinking, rounded-sw-card, text-sw-md, etc. Reach here first for product surfaces.
- shadcn vars (no prefix:
--background, --foreground, --card, --muted, --border, --popover, --sidebar*) — baseline the bundled shadcn / AI Elements components are wired to. Exposed as Tailwind utilities too (bg-card, text-foreground, border-border, bg-sidebar). Don't fight them inside shadcn primitives; match them when extending those primitives.
Rule of thumb: pick one layer per element's visual identity. A bento cell you're authoring → bg-sw-surface + border-sw-border. A shadcn <Dialog> you're skinning → shadcn tokens. Mixing the two inside one element's palette is how the design drifts.
Already enforced globally — don't re-implement
In globals.css:
box-shadow: none on * — elevation isn't a tool available to you, by design.
border-radius: var(--sw-radius-default, 2px) default on *.
font-variant-numeric: tabular-nums + "tnum" 1 on body — numbers are tabular everywhere.
html, body inherit the mono stack via --font-mono.
- Tailwind's default
text-base is rebound onto --sw-text-sm so stray text-base follows the Fragua scale rather than jumping to 16px.
.sw-pulse utility — 1.0 → 0.55 → 1.0 opacity, 1800ms, respects prefers-reduced-motion. Use this; don't hand-roll keyframes for "thinking."
dark variant wired via @custom-variant dark (&:where(.dark, .dark *)) — dark:bg-… works off a class="dark" anywhere up the tree.
v4 idioms to prefer, v3 relics to avoid
- Config belongs in CSS (
@theme inline), not a .ts file. If you need a new theme token, add it to @theme inline in globals.css, not a new config file.
- Scan paths use
@source "…" inside globals.css (already wired for streamdown). Don't reach for a content: [] array — no config file exists.
- Plugins load via
@import "tw-animate-css" / @plugin "…" in CSS. Don't import animate from "tailwindcss-animate" — that package is gone.
@apply still works for shared bits (e.g., border-border on *), but prefer token utilities in JSX.
Themes
Light is the default. Fragua flips the dev-tool dark convention. Dark is a peer; every token has both values in theme.css (:root vs .dark), both intentionally designed — not auto-inverted.
Both modes share the vibe: low contrast between surface layers. A card and the page are nearly the same value — separation is a 1px hairline, not a lighter panel.
Color
Accents communicate state. No decorative accents. No "primary" color that isn't a state. Desaturate 15–30% from the first guess. No gradients on surfaces (inside a sparkline is fine). Status dots carry the color; labels stay text-sw-text or text-sw-muted.
| Token (CSS var) |
Tailwind utility prefix |
Vibe |
Use for |
--sw-bg |
bg-sw-bg |
Near-paper (light) / near-ink (dark) |
Page, root |
--sw-surface |
bg-sw-surface |
One notch off bg, barely perceptible |
Cards, panels, drawers |
--sw-border |
border-sw-border |
Hairline, visible but quiet |
Section edges |
--sw-text |
text-sw-text |
High legibility, never pure black/white |
Copy, numerics |
--sw-muted |
text-sw-muted |
Dimmed, still readable |
Labels, secondary metadata |
--sw-accent-success |
bg-sw-accent-success |
Calm green, not lime |
Done, healthy, passed |
--sw-accent-error |
bg-sw-accent-error |
Serious red, not neon |
Failure, blocker, over-budget |
--sw-accent-thinking |
bg-sw-accent-thinking |
Warm amber — the "alive" color |
Processing, awaiting, streaming |
--sw-accent-idle |
bg-sw-accent-idle |
Neutral gray |
Queued, paused |
--sw-accent-warn |
bg-sw-accent-warn |
Muted orange |
Resource pressure, soft threshold |
Never hex literals. Prefer the Tailwind utility; drop to style={{ color: 'var(--sw-accent-error)' }} only when a utility doesn't exist (e.g., gradient stop, box-shadow mask).
Typography
Monospace is the voice. Body already inherits the mono stack from :root. Don't reach for Tailwind's font-sans on product surfaces just because shadcn ships with it — font-sans is Geist, bound that way only for shadcn's own internals.
Stack (in --font-mono):
"Berkeley Mono", "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace
Scale. Hierarchy is weight, case, and spacing — not size. Exposed as Tailwind utilities; use those.
| CSS var |
Tailwind utility |
px |
Use |
--sw-text-xs |
text-sw-xs |
13 |
Timestamps, dense metadata |
--sw-text-sm |
text-sw-sm |
15 |
Default body (already on root) |
--sw-text-base |
text-sw-base |
16 |
Emphasized body, primary metric values |
--sw-text-md |
text-sw-md |
18 |
Section headings |
--sw-text-lg |
text-sw-lg |
22 |
Page title — one per screen |
Weights. 400, 500, 600. No 700+. No italics except inline code in prose.
Case. UPPERCASE with ~0.06em letter-spacing for section labels and column headers. Sentence case elsewhere. Never Title Case.
Line-height. 1.4 body, 1.2 headings, 1.0 for dense numeric tables.
Tabular figures are global — don't add tabular-nums redundantly, but also don't disable them.
Spacing
4px base. These steps only — no arbitrary px.
| CSS var |
Tailwind (matches) |
px |
Use |
--sw-space-05 |
p-0.5 / gap-0.5 |
2 |
Icon-to-label |
--sw-space-1 |
p-1 / gap-1 |
4 |
Row gap, button padding-y |
--sw-space-2 |
p-2 / gap-2 |
8 |
Card padding, input padding-x |
--sw-space-3 |
p-3 / gap-3 |
12 |
Card padding-y, section internal gap |
--sw-space-4 |
p-4 / gap-4 |
16 |
Card-to-card, panel padding |
--sw-space-6 |
p-6 / gap-6 |
24 |
Page gutter |
--sw-space-8 |
p-8 / gap-8 |
32 |
Large region separation |
Tailwind's default --spacing base is 4px, so p-3 is exactly --sw-space-3. Prefer the Tailwind utility. Drop to p-[var(--sw-space-3)] only if you need the var reference for a calc or a non-spacing property.
First drafts should feel slightly too tight. Air comes from alignment, not margin.
Borders & elevation
- 1px only,
border-sw-border. Tone shifts by theme, width never.
- Radius:
rounded-sw-default (2px) default, rounded-sw-card (4px) for cards/drawers, rounded-sw-none (0) for table rows and dense stacks. Pills only where the pill is the status shape.
- Elevation: none.
box-shadow: none is already set on *. Drawers separate via backdrop scrim + hairline. Reaching for shadow means you need more spacing or a hairline.
Layout
- Bento grid. Rectangles divided by hairlines, sized by content weight, not equal thirds.
- Flex rows for micro-data:
[status-dot] [name — flex:1] [metric — tabular, right-aligned].
- Consistent padding inside every cell —
p-3 for a status card and a log card alike. Don't vary padding by "importance."
Motion
Only animate transform and opacity. Paired elements (drawer + scrim, tooltip + arrow) share easing and duration. Motion durations are exposed as CSS vars so timings stay uniform across components — reach for them in transition-* arbitrary values (duration-[var(--sw-duration-hover)]).
| State |
Animation |
Token / duration |
Easing |
| Processing / awaiting |
Opacity pulse 1.0 → 0.55 → 1.0 |
--sw-duration-pulse (1800ms) |
ease-in-out |
| Drawer / panel enter-exit |
Slide + fade (paired with scrim) |
--sw-duration-enter (200ms) |
ease-out |
| Status transition |
Dot color crossfade |
--sw-duration-status (160ms) |
ease |
| Numeric update in place |
Roll or crossfade |
200ms |
ease-in-out |
| Hover, color shift |
Bg + border shift |
--sw-duration-hover (120ms) |
ease |
| Active / press |
transform: scale(0.97) |
--sw-duration-press (80ms) |
ease-out |
| Focus ring |
Instant |
0ms |
— |
Rules:
- Use
.sw-pulse for all "thinking/streaming/awaiting" indicators. It already handles duration and prefers-reduced-motion.
- Use
tw-animate-css utilities for entrance/exit on shadcn primitives (animate-in, fade-in-0, slide-in-from-top-2, etc.). Don't hand-roll keyframes for these.
- Easing by intent.
ease-out enter/exit; ease-in-out on-screen morph; ease hover and color; linear only for constant-motion indicators — never for color.
- Pulse is slow. A fast pulse reads as error.
- High-frequency numerics. If a counter updates >5×/sec, batch to ~200ms ticks before animating — don't stack crossfades.
prefers-reduced-motion: for anything you author outside .sw-pulse, swap pulse for static dimmed state, disable rolls, keep hovers (they're informational).
- Hover on hot rows. Omit hover animation on list rows users traverse hundreds of times per session. Keep it on buttons.
A conforming component (sketch)
A bento cell — status dot, name, metric. Shows the common v4 moves:
<div className="flex items-center gap-2 p-3 border border-sw-border rounded-sw-card bg-sw-surface">
<span
aria-hidden
className="sw-pulse size-2 rounded-full bg-sw-accent-thinking"
/>
<span className="flex-1 truncate text-sw-sm">agent-3</span>
<span className="text-sw-base">1,284</span>
</div>
What's doing the work: mono inherited from root, no shadow (global), 1px hairline via border-sw-border, 12px padding (p-3), tabular-nums global, status carried by the dot's color not the label's, pulse that respects reduced motion — all expressed as first-class Tailwind utilities, no style={{}} escape hatch needed.
Authoring checklist
Anti-patterns
- Shadow to separate cards → hairline. (And
box-shadow: none is already global — if you see one, something is fighting the reset.)
- Background shade for hierarchy → same surface, hairline.
- Icon-only status → always label.
- "Primary" color that isn't a state → no brand-blue buttons.
font-sans on product surfaces because shadcn has it → inherit mono.
- Mixing
sw-* and shadcn tokens inside one element's palette → pick a layer.
- Hex literals,
#fff, #000 → token utilities only.
- Giant headings → weight + case do the work.
- Welcome-theater animation on load → motion is for state.
- Linear easing on color or hover → use
ease.
- Hand-rolled "thinking" keyframes → use
.sw-pulse.
- Reaching for
tailwindcss-animate or a tailwind.config.ts → both gone under v4; use tw-animate-css utilities and @theme inline.
- Title Case anywhere.
Source: purrgrammer/fragua — distributed by TomeVault.
1---2name: purrgrammer-fragua-design3description: Fragua design language4---56# Fragua design language78*Clarity through restraint — data is the decoration, structure is the grid, motion indicates state.*910Foundational language for `packages/web`. If a pattern isn't here, derive it from the principles. When principles conflict, **restraint wins** — the default answer to "should I add something" is no.1112---1314## Principles (earlier wins on conflict)15161. **Calm control & progressive disclosure.** Surface status and the 2–3 numbers that matter. Logs, metadata, and secondary actions live behind hover, tooltip, or drawer. More than ~7 primary signals per view is failure.172. **Data as decor.** Sparklines, status dots, and aligned numbers *are* the visual rhythm. No shadows, decorative gradients, or heavy borders. Ornament gets deleted.183. **Strict structural layout.** Bento grid, tight consistent padding, sections separated by a hairline — never by a different background shade. Flex rows for micro-data: `[status] [name] [metric]`.194. **Typographic discipline.** Monospace voice. Hierarchy via weight, case, and spacing — never size jumps. Numbers always tabular.205. **Restrained semantic color.** Surfaces nearly indistinguishable from background. Accents reserved for state — not branding.216. **Functional motion.** Motion says "this is happening." If removing it loses no information, remove it.2223---2425## Stack & how tokens fit the codebase2627**Tailwind v4, CSS-first config.** There is no `tailwind.config.ts`. All theme mapping lives in `packages/web/src/styles/globals.css` inside an `@theme inline { … }` block; raw design-language CSS vars live in `packages/web/src/styles/theme.css`. The build pipeline is `@tailwindcss/vite` (no PostCSS, no Autoprefixer — v4 handles vendor prefixing via Lightning CSS). Animations come from `tw-animate-css` (imported as `@import "tw-animate-css";` in globals).2829Two token layers coexist. Know which to reach for before writing a class:3031- **`--sw-*` (Fragua design language)** — defined in `theme.css`. Surfaces, state accents, typography scale, spacing scale, radii, motion durations. Exposed as Tailwind utilities via `@theme inline`: **`bg-sw-surface`**, **`text-sw-muted`**, **`border-sw-border`**, **`bg-sw-accent-thinking`**, **`rounded-sw-card`**, **`text-sw-md`**, etc. **Reach here first** for product surfaces.32- **shadcn vars (no prefix: `--background`, `--foreground`, `--card`, `--muted`, `--border`, `--popover`, `--sidebar*`)** — baseline the bundled shadcn / AI Elements components are wired to. Exposed as Tailwind utilities too (`bg-card`, `text-foreground`, `border-border`, `bg-sidebar`). Don't fight them inside shadcn primitives; match them when extending those primitives.3334Rule of thumb: **pick one layer per element's visual identity.** A bento cell you're authoring → `bg-sw-surface` + `border-sw-border`. A shadcn `<Dialog>` you're skinning → shadcn tokens. Mixing the two inside one element's palette is how the design drifts.3536### Already enforced globally — don't re-implement3738In `globals.css`:3940- `box-shadow: none` on `*` — elevation isn't a tool available to you, by design.41- `border-radius: var(--sw-radius-default, 2px)` default on `*`.42- `font-variant-numeric: tabular-nums` + `"tnum" 1` on body — numbers are tabular everywhere.43- `html, body` inherit the mono stack via `--font-mono`.44- Tailwind's default `text-base` is rebound onto `--sw-text-sm` so stray `text-base` follows the Fragua scale rather than jumping to 16px.45- **`.sw-pulse` utility** — 1.0 → 0.55 → 1.0 opacity, 1800ms, respects `prefers-reduced-motion`. Use this; don't hand-roll keyframes for "thinking."46- **`dark` variant** wired via `@custom-variant dark (&:where(.dark, .dark *))` — `dark:bg-…` works off a `class="dark"` anywhere up the tree.4748### v4 idioms to prefer, v3 relics to avoid4950- Config belongs in CSS (`@theme inline`), not a `.ts` file. If you need a new theme token, add it to `@theme inline` in `globals.css`, not a new config file.51- Scan paths use `@source "…"` inside `globals.css` (already wired for streamdown). Don't reach for a `content: []` array — no config file exists.52- Plugins load via `@import "tw-animate-css"` / `@plugin "…"` in CSS. Don't `import animate from "tailwindcss-animate"` — that package is gone.53- `@apply` still works for shared bits (e.g., `border-border` on `*`), but prefer token utilities in JSX.5455---5657## Themes5859**Light is the default.** Fragua flips the dev-tool dark convention. Dark is a peer; every token has both values in `theme.css` (`:root` vs `.dark`), both intentionally designed — not auto-inverted.6061Both modes share the vibe: **low contrast between surface layers**. A card and the page are nearly the same value — separation is a 1px hairline, not a lighter panel.6263---6465## Color6667Accents communicate state. No decorative accents. No "primary" color that isn't a state. Desaturate 15–30% from the first guess. No gradients on surfaces (inside a sparkline is fine). Status dots carry the color; labels stay `text-sw-text` or `text-sw-muted`.6869| Token (CSS var) | Tailwind utility prefix | Vibe | Use for |70|-------------------------|----------------------------------|---------------------------------------------|-----------------------------------|71| `--sw-bg` | `bg-sw-bg` | Near-paper (light) / near-ink (dark) | Page, root |72| `--sw-surface` | `bg-sw-surface` | One notch off bg, barely perceptible | Cards, panels, drawers |73| `--sw-border` | `border-sw-border` | Hairline, visible but quiet | Section edges |74| `--sw-text` | `text-sw-text` | High legibility, never pure black/white | Copy, numerics |75| `--sw-muted` | `text-sw-muted` | Dimmed, still readable | Labels, secondary metadata |76| `--sw-accent-success` | `bg-sw-accent-success` | Calm green, not lime | Done, healthy, passed |77| `--sw-accent-error` | `bg-sw-accent-error` | Serious red, not neon | Failure, blocker, over-budget |78| `--sw-accent-thinking` | `bg-sw-accent-thinking` | Warm amber — the "alive" color | Processing, awaiting, streaming |79| `--sw-accent-idle` | `bg-sw-accent-idle` | Neutral gray | Queued, paused |80| `--sw-accent-warn` | `bg-sw-accent-warn` | Muted orange | Resource pressure, soft threshold |8182Never hex literals. Prefer the Tailwind utility; drop to `style={{ color: 'var(--sw-accent-error)' }}` only when a utility doesn't exist (e.g., gradient stop, box-shadow mask).8384---8586## Typography8788Monospace is the voice. Body already inherits the mono stack from `:root`. Don't reach for Tailwind's `font-sans` on product surfaces just because shadcn ships with it — `font-sans` is Geist, bound that way only for shadcn's own internals.8990**Stack** (in `--font-mono`):9192```93"Berkeley Mono", "JetBrains Mono", "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace94```9596**Scale.** Hierarchy is weight, case, and spacing — not size. Exposed as Tailwind utilities; use those.9798| CSS var | Tailwind utility | px | Use |99|------------------|------------------|----|------------------------------------------|100| `--sw-text-xs` | `text-sw-xs` | 13 | Timestamps, dense metadata |101| `--sw-text-sm` | `text-sw-sm` | 15 | **Default body** (already on root) |102| `--sw-text-base` | `text-sw-base` | 16 | Emphasized body, primary metric values |103| `--sw-text-md` | `text-sw-md` | 18 | Section headings |104| `--sw-text-lg` | `text-sw-lg` | 22 | Page title — one per screen |105106**Weights.** 400, 500, 600. No 700+. No italics except inline code in prose.107108**Case.** `UPPERCASE` with ~0.06em letter-spacing for section labels and column headers. Sentence case elsewhere. **Never Title Case.**109110**Line-height.** 1.4 body, 1.2 headings, 1.0 for dense numeric tables.111112Tabular figures are global — don't add `tabular-nums` redundantly, but also don't disable them.113114---115116## Spacing1171184px base. These steps only — no arbitrary px.119120| CSS var | Tailwind (matches) | px | Use |121|-----------------|---------------------|----|--------------------------------------|122| `--sw-space-05` | `p-0.5` / `gap-0.5` | 2 | Icon-to-label |123| `--sw-space-1` | `p-1` / `gap-1` | 4 | Row gap, button padding-y |124| `--sw-space-2` | `p-2` / `gap-2` | 8 | Card padding, input padding-x |125| `--sw-space-3` | `p-3` / `gap-3` | 12 | Card padding-y, section internal gap |126| `--sw-space-4` | `p-4` / `gap-4` | 16 | Card-to-card, panel padding |127| `--sw-space-6` | `p-6` / `gap-6` | 24 | Page gutter |128| `--sw-space-8` | `p-8` / `gap-8` | 32 | Large region separation |129130Tailwind's default `--spacing` base is 4px, so `p-3` is exactly `--sw-space-3`. **Prefer the Tailwind utility.** Drop to `p-[var(--sw-space-3)]` only if you need the var reference for a calc or a non-spacing property.131132First drafts should feel slightly too tight. Air comes from alignment, not margin.133134---135136## Borders & elevation137138- **1px only**, `border-sw-border`. Tone shifts by theme, width never.139- **Radius:** `rounded-sw-default` (2px) default, `rounded-sw-card` (4px) for cards/drawers, `rounded-sw-none` (0) for table rows and dense stacks. Pills only where the pill *is* the status shape.140- **Elevation: none.** `box-shadow: none` is already set on `*`. Drawers separate via backdrop scrim + hairline. Reaching for shadow means you need more spacing or a hairline.141142---143144## Layout145146- **Bento grid.** Rectangles divided by hairlines, sized by content weight, not equal thirds.147- **Flex rows** for micro-data: `[status-dot] [name — flex:1] [metric — tabular, right-aligned]`.148- **Consistent padding** inside every cell — `p-3` for a status card and a log card alike. Don't vary padding by "importance."149150---151152## Motion153154Only animate `transform` and `opacity`. Paired elements (drawer + scrim, tooltip + arrow) share easing and duration. Motion durations are exposed as CSS vars so timings stay uniform across components — reach for them in `transition-*` arbitrary values (`duration-[var(--sw-duration-hover)]`).155156| State | Animation | Token / duration | Easing |157|---------------------------|-----------------------------------|----------------------------------|---------------|158| Processing / awaiting | Opacity pulse 1.0 → 0.55 → 1.0 | `--sw-duration-pulse` (1800ms) | `ease-in-out` |159| Drawer / panel enter-exit | Slide + fade (paired with scrim) | `--sw-duration-enter` (200ms) | `ease-out` |160| Status transition | Dot color crossfade | `--sw-duration-status` (160ms) | `ease` |161| Numeric update in place | Roll or crossfade | 200ms | `ease-in-out` |162| Hover, color shift | Bg + border shift | `--sw-duration-hover` (120ms) | `ease` |163| Active / press | `transform: scale(0.97)` | `--sw-duration-press` (80ms) | `ease-out` |164| Focus ring | Instant | 0ms | — |165166Rules:167168- **Use `.sw-pulse`** for all "thinking/streaming/awaiting" indicators. It already handles duration and `prefers-reduced-motion`.169- **Use `tw-animate-css`** utilities for entrance/exit on shadcn primitives (`animate-in`, `fade-in-0`, `slide-in-from-top-2`, etc.). Don't hand-roll keyframes for these.170- **Easing by intent.** `ease-out` enter/exit; `ease-in-out` on-screen morph; `ease` hover and color; `linear` only for constant-motion indicators — never for color.171- **Pulse is slow.** A fast pulse reads as error.172- **High-frequency numerics.** If a counter updates >5×/sec, batch to ~200ms ticks before animating — don't stack crossfades.173- **`prefers-reduced-motion`:** for anything you author outside `.sw-pulse`, swap pulse for static dimmed state, disable rolls, keep hovers (they're informational).174- **Hover on hot rows.** Omit hover animation on list rows users traverse hundreds of times per session. Keep it on buttons.175176---177178## A conforming component (sketch)179180A bento cell — status dot, name, metric. Shows the common v4 moves:181182```tsx183<div className="flex items-center gap-2 p-3 border border-sw-border rounded-sw-card bg-sw-surface">184 <span185 aria-hidden186 className="sw-pulse size-2 rounded-full bg-sw-accent-thinking"187 />188 <span className="flex-1 truncate text-sw-sm">agent-3</span>189 <span className="text-sw-base">1,284</span>190</div>191```192193What's doing the work: mono inherited from root, no shadow (global), 1px hairline via `border-sw-border`, 12px padding (`p-3`), tabular-nums global, status carried by the dot's color not the label's, pulse that respects reduced motion — all expressed as first-class Tailwind utilities, no `style={{}}` escape hatch needed.194195---196197## Authoring checklist198199- [ ] No `box-shadow`, no decorative gradient, no border ≥ 2px200- [ ] No hex literals — `sw-*` or shadcn token utilities only201- [ ] Picked **one** token layer (Fragua or shadcn), not both, for the element's palette202- [ ] Every accent maps to a state203- [ ] Padding and radius are tokens (`p-3`, `rounded-sw-card`)204- [ ] Font size uses the scale (`text-sw-sm` / `text-sw-md` / …), never raw `text-base` / `text-lg` / px values205- [ ] Hierarchy survives the desaturate test (works if you strip color)206- [ ] Both themes verified — not just light207- [ ] Secondary info behind hover / drawer208- [ ] Live-updating numbers have a transition209- [ ] Processing elements use `.sw-pulse`210- [ ] Animations touch only `transform` and `opacity`211- [ ] No `font-sans` on a product surface212213---214215## Anti-patterns216217- Shadow to separate cards → hairline. (And `box-shadow: none` is already global — if you see one, something is fighting the reset.)218- Background shade for hierarchy → same surface, hairline.219- Icon-only status → always label.220- "Primary" color that isn't a state → no brand-blue buttons.221- `font-sans` on product surfaces because shadcn has it → inherit mono.222- Mixing `sw-*` and shadcn tokens inside one element's palette → pick a layer.223- Hex literals, `#fff`, `#000` → token utilities only.224- Giant headings → weight + case do the work.225- Welcome-theater animation on load → motion is for state.226- Linear easing on color or hover → use `ease`.227- Hand-rolled "thinking" keyframes → use `.sw-pulse`.228- Reaching for `tailwindcss-animate` or a `tailwind.config.ts` → both gone under v4; use `tw-animate-css` utilities and `@theme inline`.229- Title Case anywhere.230231---232> Source: [purrgrammer/fragua](https://github.com/purrgrammer/fragua) — distributed by [TomeVault](https://tomevault.io).233<!-- tomevault:4.0:skill_md:2026-06-02 -->