Tailwind v4 (semantic tokens)
Two jobs in one skill:
- Author (always-on). When writing or editing Tailwind, follow the house style below.
- Cleanup (on request). When asked to clean/audit/simplify Tailwind classes, read
references/cleanup.md and follow it.
Hard rule: this is Tailwind v4 only. Never emit v3 patterns — no tailwind.config.js as the default, no content/purge array, no darkMode: 'class' config, no require() plugin syntax, no @tailwind base/components/utilities (the entry point is @import "tailwindcss";), no bg-opacity-* / text-opacity-* / border-opacity-* (removed — use the /50 modifier), and no @layer utilities for custom utilities (that is @utility). If a project genuinely needs v3, say so explicitly first.
House style: semantic tokens
Dark mode and colour are driven by semantic CSS-variable tokens, not raw colour utilities. Tokens live as complete colour values in :root / .dark, bridged into utilities with @theme inline; the variable flips under the dark selector, so dark: prefixes are rare. Full scaffold in references/setup.md.
Read the project's token names out of its CSS — never assume them. Where there is no theme yet, shadcn's names are the default to scaffold (background/foreground, card, popover, primary, secondary, muted, accent, destructive, border, input, ring), and the examples below use them. A project with its own vocabulary keeps it; the pattern is the rule, the names are not.
Colour values: OKLCH only
Every colour token is oklch(L C H) or oklch(L C H / A). No hex, rgb(), or hsl() in :root, .dark, or @theme. (L = perceived lightness 0–1, C = how vivid 0–~0.4, H = hue 0–360.)
Store the complete colour function. Never v3-style bare channels (--background: 0 0% 100%) — the utility emits var(--background) straight into background-color, so the token dies entirely, with or without a /opacity modifier. A wrapped hsl(var(--background)) is a complete colour and works; convert it for house style only.
Spaces, not commas; slash for alpha. oklch(0.7 0.1 250, 0.5) passes the build untouched with no warning; the browser drops the declaration at parse time. Write oklch(0.7 0.1 250 / 0.5), and omit the alpha when it is 1.
Keep theme tokens opaque. Put transparency on the utility (bg-primary/30), not inside the token — otherwise the two compound into a double fade. The one standing exception is shadcn's dark-mode hairlines (--border: oklch(1 0 0 / 10%), --input: … / 15%), where the alpha is the colour; leave those as shipped.
Every fill token has a paired -foreground, and the contrast between them is a lightness gap.
Fix contrast by moving L. Push L further from the background and leave C and H alone; then re-check the ratio. Never raise C to "add contrast" — on some hues it measurably lowers it.
If a colour looks wrong or over-saturated, lower C and keep L and H. Ceilings vary enormously by hue — at L 0.55 the maximum in-gamut chroma runs from ~0.09 (cyan) to ~0.27 (purple). Treat C ≤ 0.04 as the grey band and C ≤ 0.12 as comfortable for most accents; above that, copy a known-good value rather than inventing one. Vivid intent roles legitimately go higher — --destructive is oklch(0.577 0.245 27.325).
Never compute OKLCH by hand. This skill ships a converter — zero dependencies, node only:
node ~/.claude/skills/tailwind/scripts/oklch.mjs '#3b82f6' # oklch(0.623 0.188 259.815)
node ~/.claude/skills/tailwind/scripts/oklch.mjs --table '#0f172a' '#f5f5f4'
Takes hex / rgb() / hsl() / oklch(); --hex reverses it; warns on stderr outside sRGB. When converting an existing palette, convert the values only — leave currentColor, CSS keywords, gradient interpolation, and third-party library configs alone.
No brand ramp unless asked. This system is ~12 semantic roles, not a 50–950 palette. And don't build dark mode by inverting a ramp — re-set the same semantic roles under .dark.
Authoring rules
- Reach for a semantic token before any raw colour — a surface token for surfaces, a muted-text token for secondary text, a border token for borders, an intent token for primary/destructive. Under shadcn's names:
bg-background/bg-card, text-muted-foreground, border-border, bg-primary/bg-destructive.
dark: is rarely needed — a hand-rolled bg-white dark:bg-gray-900 pair is a smell; use the surface token.
- Read what consumes a token before editing it. Names state intent, not binding: grep the
bg-* / text-* / border-* on the component and change that token. Recolouring --sidebar-primary does nothing when the item paints with data-active:bg-sidebar-accent. And a token is a role — editing one restyles everything bound to it, so changing --primary for a button also repaints every default badge.
- Check the live docs before asserting a version-specific fact — a utility's default value, a CLI flag, a plugin's rule or option name.
- Set radius via
rounded-md/rounded-lg (bound to --radius), not arbitrary rounded-[6px].
- Before writing a bracket, walk the ladder:
- Native scale step? Use the token — spacing on the 4px grid (
p-1=4px … p-4=16px; p-px=1px), rounded-md, z-40, opacity-70, text-sm. Never p-[16px] for p-4.
The spacing scale is unbounded — every integer works, compiling to calc(var(--spacing) * N). p-18, mt-21, gap-13, w-101 are all real, as are open-ended z-N and grid-cols-N. Never reach for a bracket because a number "looks too big for the scale": divide by 4 and use the step. For the same reason, never add --spacing-18: 4.5rem to @theme — p-18 already is 4.5rem. Named --spacing-* keys are for names (--spacing-gutter), not for filling holes in a scale that has none.
1b. A width? max-w-* / min-w-* read the named container scale first — max-w-md is 28rem, max-w-4xl is 56rem (896px). Prefer it for anything page- or card-sized. A near-miss is a design call — offer the delta, never rewrite silently. max-h-* / min-h-* are spacing-only.
- A colour? Walk the colour ladder:
- Has a role (surface, text, border, primary/brand, destructive, muted, ring, a chart series that themes) → use the semantic
@theme token (bg-primary, text-muted-foreground). Never re-invent these with bg-white / text-gray-500 / dark: pairs.
- Decorative, categorical, or a true one-off with no role → soft-allow the nearest stock palette shade (
bg-sky-600, text-amber-500). Match token count to the variability of the visual language — don't add a @theme token for a colour with no fixed meaning.
- Promote to
@theme once the colour carries brand meaning, must flip under .dark, or repeats in more than one place/file.
- Never a raw arbitrary colour (
bg-[#3b82f6], text-[rgb(...)]) — snap to the nearest stop or extend the theme once; never scatter hex and grow a parallel shadow palette.
- Value repeats (>1 place or file)? Promote it to
@theme and reference the generated token.
- Genuine one-off (a
calc(), a grid-cols-[200px_1fr] template, a single magic offset)? An arbitrary value is correct — that's the escape hatch.
-px utilities are intentional. Keep p-px, mt-px, gap-px, w-px as-is; rewrite the long form p-[1px] → p-px.
- Get the two custom-CSS directives right — both have a v3/beta lookalike.
- A custom utility is
@utility name { … }. @layer utilities { .name { … } } still emits the class, so it looks like it worked, but the utility is never registered and hover:name / lg:name won't exist.
@utility is also how a reusable affordance is written — but in a component framework a repeated class string is a missing component first. @utility is for markup no component can own. See references/affordances.md.
- A custom variant is
@custom-variant: @custom-variant theme-midnight (&:where([data-theme="midnight"] *));. @variant is a different directive that applies an already-registered variant inside CSS (.x { @variant dark { … } }). Defining with @variant name (selector) is v4-beta syntax — it is still silently accepted for compatibility, so it will not error, it will just be undocumented and ambiguous.
Canonical syntax
Training data is full of v3 and of verbose arbitrary variants. Emit the first-class form:
| Instead of |
Write |
|
[&>*]: / [&_*]: |
*: / **: |
direct children / all descendants |
[&>[role=checkbox]]: |
*:[[role=checkbox]]: |
outer [] = arbitrary variant, inner [] = the attribute selector |
[&>[data-open]]: |
*:data-open: |
data-* / aria-* are first-class |
[&:has(...)]: [&:not(:first-child)]: [&:nth-child(odd)]: |
has-[...]: not-first: odd: |
|
group-[.foo]: |
group-hover: / peer-invalid: |
only when a named state variant exists; a class-qualified group is otherwise fine |
[@media_print]: / [@media(width>=…)]: |
print: / lg: / max-lg: |
|
!flex |
flex! |
v4's marker is the suffix; the prefix still parses, so it is non-canonical, not broken |
bg-[--token] / bg-[var(--token)] |
bg-(--token) |
same for modifiers: bg-primary/(--alpha) |
grid-cols-[auto,1fr] |
grid-cols-[auto_1fr] |
underscore is the space; no padding underscores |
bg-gradient-to-r flex-grow overflow-ellipsis break-words decoration-clone bg-left-top |
bg-linear-to-r grow text-ellipsis wrap-break-word box-decoration-clone bg-top-left |
v3 names |
A component library can redefine these variant names. shadcn/tailwind.css ships its own @custom-variant data-open matching [data-state="open"] and [data-open], plus data-closed / data-checked / data-selected / data-disabled / data-active / data-horizontal / data-vertical. Where it is imported those are broader than stock Tailwind's — read the project's CSS before assuming data-x: means the bare attribute.
Fewer classes, same result:
- Collapse same-value sides.
size-4 over w-4 h-4; p-4 over px-4 py-4; m-4 over four sides; inset-0 over four offsets; text-sm/7 over text-sm leading-7.
- Don't restate a default.
flex flex-row is just flex (row is the default). Same for opacity-100, scale-100, rotate-0, order-0, basis-auto — emitting them adds a class that changes nothing.
- Don't emit two classes that set the same property. Write the one you mean. When you find a pair in existing code, do not assume the last one written wins — that is decided by Tailwind's emission order, not markup order (see
references/cleanup.md).
Do not over-correct. These are already right, and "fixing" them changes behaviour or is plainly wrong:
[&:hover]: is not the same as hover: — the named variant also wraps @media (hover: hover). Only use hover: when you mean that.
- Never apply the v3→v4 rename table to v4 code.
shadow, rounded, ring, outline-none are all valid v4 classes; remapping them to shadow-sm / rounded-sm / ring-3 / outline-hidden changes the render (v4 ring is 1px, so ring-3 triples it) or is a pointless no-op rename.
- Never rewrite
shadow-sm / blur-sm / rounded-sm / drop-shadow-sm / backdrop-blur-sm to -xs. The rename moved v3's shadow-sm to shadow-xs; it did not delete shadow-sm, which is its own v4 utility with its own value. Doing this shrinks every shadow, blur and radius by one step. (v4's smallest shadow is shadow-2xs.)
- Don't convert viewport variants into container queries.
md:/lg: and @md:/@lg: are both first-class and mean different things. Viewport is the default for page chrome; reach for @container when authoring a component that will live in more than one slot width. See references/gotchas.md.
- Leave anything that only looks non-canonical — a stacked
data-active:hover:, data-[foo=bar]:, [figure>&]:, :where() wrappers. Arbitrary variants are the escape hatch; the full list is in references/cleanup.md under Never touch.
Where the project has a linter configured, finish an editing pass with npx eslint --fix — see references/editor.md.
When to load more
- Scaffolding a project — no
globals.css, no @theme block, wiring the PostCSS/Vite entry, adding the theme toggle, or setting up cn() or the button cursor for the first time: read references/setup.md before writing CSS. It wires the token contract and leaves the palette values to shadcn init or the user; it carries three decisions that must be asked, not assumed.
- A v4 trap — a utility not applying, "Cannot apply unknown utility class",
@apply in a Vue/Svelte/Astro <style> or CSS Module, h-screen on mobile, a dynamic bg-${x} class, truncate not clipping, container queries / @md: vs md:, or a token that looks v3-shaped: read references/gotchas.md.
- A repeated class string, or an existing
@layer components block — whether a look should become a named @utility: read references/affordances.md.
- Tooling — editor autocomplete inside
cva/cn, class sorting, or a lint rule to enforce this house style: read references/editor.md.
- Cleanup / audit / simplify — the user asked, or you are reviewing a component for class drift: read
references/cleanup.md and follow its process and output format.
1---2name: tailwind3description: Tailwind v4 (semantic tokens)4---56# Tailwind v4 (semantic tokens)78Two jobs in one skill:9101. **Author (always-on).** When writing or editing Tailwind, follow the house style below.112. **Cleanup (on request).** When asked to clean/audit/simplify Tailwind classes, read `references/cleanup.md` and follow it.1213**Hard rule: this is Tailwind v4 only.** Never emit v3 patterns — no `tailwind.config.js` as the default, no `content`/purge array, no `darkMode: 'class'` config, no `require()` plugin syntax, no `@tailwind base/components/utilities` (the entry point is `@import "tailwindcss";`), no `bg-opacity-*` / `text-opacity-*` / `border-opacity-*` (removed — use the `/50` modifier), and no `@layer utilities` for custom utilities (that is `@utility`). If a project genuinely needs v3, say so explicitly first.1415---1617## House style: semantic tokens1819Dark mode and colour are driven by **semantic CSS-variable tokens**, not raw colour utilities. Tokens live as complete colour values in `:root` / `.dark`, bridged into utilities with `@theme inline`; the variable flips under the dark selector, so `dark:` prefixes are rare. Full scaffold in `references/setup.md`.2021**Read the project's token names out of its CSS — never assume them.** Where there is no theme yet, shadcn's names are the default to scaffold (`background`/`foreground`, `card`, `popover`, `primary`, `secondary`, `muted`, `accent`, `destructive`, `border`, `input`, `ring`), and the examples below use them. A project with its own vocabulary keeps it; the pattern is the rule, the names are not.2223## Colour values: OKLCH only2425- **Every colour token is `oklch(L C H)` or `oklch(L C H / A)`.** No hex, `rgb()`, or `hsl()` in `:root`, `.dark`, or `@theme`. (L = perceived lightness 0–1, C = how vivid 0–~0.4, H = hue 0–360.)26- **Store the complete colour function.** Never v3-style bare channels (`--background: 0 0% 100%`) — the utility emits `var(--background)` straight into `background-color`, so the token dies entirely, with or without a `/opacity` modifier. A wrapped `hsl(var(--background))` is a *complete* colour and works; convert it for house style only.27- **Spaces, not commas; slash for alpha.** `oklch(0.7 0.1 250, 0.5)` passes the build untouched with no warning; the browser drops the declaration at parse time. Write `oklch(0.7 0.1 250 / 0.5)`, and omit the alpha when it is 1.28- **Keep theme tokens opaque.** Put transparency on the utility (`bg-primary/30`), not inside the token — otherwise the two compound into a double fade. The one standing exception is shadcn's dark-mode hairlines (`--border: oklch(1 0 0 / 10%)`, `--input: … / 15%`), where the alpha *is* the colour; leave those as shipped.29- **Every fill token has a paired `-foreground`,** and the contrast between them is a **lightness gap**.30- **Fix contrast by moving L.** Push L further from the background and leave C and H alone; then re-check the ratio. Never raise C to "add contrast" — on some hues it measurably *lowers* it.31- **If a colour looks wrong or over-saturated, lower C and keep L and H.** Ceilings vary enormously by hue — at L 0.55 the maximum in-gamut chroma runs from ~0.09 (cyan) to ~0.27 (purple). Treat C ≤ 0.04 as the grey band and C ≤ 0.12 as comfortable for most accents; above that, copy a known-good value rather than inventing one. Vivid intent roles legitimately go higher — `--destructive` is `oklch(0.577 0.245 27.325)`.32- **Never compute OKLCH by hand.** This skill ships a converter — zero dependencies, `node` only:3334 ```35 node ~/.claude/skills/tailwind/scripts/oklch.mjs '#3b82f6' # oklch(0.623 0.188 259.815)36 node ~/.claude/skills/tailwind/scripts/oklch.mjs --table '#0f172a' '#f5f5f4'37 ```3839 Takes hex / `rgb()` / `hsl()` / `oklch()`; `--hex` reverses it; warns on stderr outside sRGB. When converting an existing palette, convert the **values only** — leave `currentColor`, CSS keywords, gradient interpolation, and third-party library configs alone.40- **No brand ramp unless asked.** This system is ~12 semantic roles, not a 50–950 palette. And don't build dark mode by inverting a ramp — re-set the same semantic roles under `.dark`.4142## Authoring rules4344- Reach for a **semantic token** before any raw colour — a surface token for surfaces, a muted-text token for secondary text, a border token for borders, an intent token for primary/destructive. Under shadcn's names: `bg-background`/`bg-card`, `text-muted-foreground`, `border-border`, `bg-primary`/`bg-destructive`.45- `dark:` is rarely needed — a hand-rolled `bg-white dark:bg-gray-900` pair is a smell; use the surface token.46- **Read what consumes a token before editing it.** Names state intent, not binding: grep the `bg-*` / `text-*` / `border-*` on the component and change *that* token. Recolouring `--sidebar-primary` does nothing when the item paints with `data-active:bg-sidebar-accent`. And a token is a **role** — editing one restyles everything bound to it, so changing `--primary` for a button also repaints every default badge.47- **Check the live docs before asserting a version-specific fact** — a utility's default value, a CLI flag, a plugin's rule or option name.48- Set radius via `rounded-md`/`rounded-lg` (bound to `--radius`), not arbitrary `rounded-[6px]`.49- **Before writing a bracket, walk the ladder:**50 1. **Native scale step?** Use the token — spacing on the 4px grid (`p-1`=4px … `p-4`=16px; `p-px`=1px), `rounded-md`, `z-40`, `opacity-70`, `text-sm`. Never `p-[16px]` for `p-4`.51 **The spacing scale is unbounded** — every integer works, compiling to `calc(var(--spacing) * N)`. `p-18`, `mt-21`, `gap-13`, `w-101` are all real, as are open-ended `z-N` and `grid-cols-N`. Never reach for a bracket because a number "looks too big for the scale": divide by 4 and use the step. For the same reason, never add `--spacing-18: 4.5rem` to `@theme` — `p-18` already *is* 4.5rem. Named `--spacing-*` keys are for names (`--spacing-gutter`), not for filling holes in a scale that has none.52 1b. **A width?** `max-w-*` / `min-w-*` read the **named container scale** first — `max-w-md` is 28rem, `max-w-4xl` is 56rem (896px). Prefer it for anything page- or card-sized. A near-miss is a **design** call — offer the delta, never rewrite silently. `max-h-*` / `min-h-*` are spacing-only.53 2. **A colour?** Walk the colour ladder:54 - **Has a role** (surface, text, border, primary/brand, destructive, muted, ring, a chart series that themes) → use the semantic `@theme` token (`bg-primary`, `text-muted-foreground`). Never re-invent these with `bg-white` / `text-gray-500` / `dark:` pairs.55 - **Decorative, categorical, or a true one-off** with no role → soft-allow the nearest stock palette shade (`bg-sky-600`, `text-amber-500`). Match token count to the variability of the visual language — don't add a `@theme` token for a colour with no fixed meaning.56 - **Promote to `@theme`** once the colour carries brand meaning, must flip under `.dark`, or repeats in more than one place/file.57 - **Never a raw arbitrary colour** (`bg-[#3b82f6]`, `text-[rgb(...)]`) — snap to the nearest stop or extend the theme once; never scatter hex *and* grow a parallel shadow palette.58 3. **Value repeats (>1 place or file)?** Promote it to `@theme` and reference the generated token.59 4. **Genuine one-off** (a `calc()`, a `grid-cols-[200px_1fr]` template, a single magic offset)? An arbitrary value is correct — that's the escape hatch.60- **`-px` utilities are intentional.** Keep `p-px`, `mt-px`, `gap-px`, `w-px` as-is; rewrite the long form `p-[1px]` → `p-px`.61- **Get the two custom-CSS directives right — both have a v3/beta lookalike.**62 - A custom utility is `@utility name { … }`. `@layer utilities { .name { … } }` still emits the class, so it *looks* like it worked, but the utility is never registered and `hover:name` / `lg:name` won't exist.63 - **`@utility` is also how a reusable affordance is written** — but in a component framework a repeated class string is a missing **component** first. `@utility` is for markup no component can own. See `references/affordances.md`.64 - A custom variant is **`@custom-variant`**: `@custom-variant theme-midnight (&:where([data-theme="midnight"] *));`. `@variant` is a different directive that *applies* an already-registered variant inside CSS (`.x { @variant dark { … } }`). Defining with `@variant name (selector)` is v4-**beta** syntax — it is still silently accepted for compatibility, so it will not error, it will just be undocumented and ambiguous.6566### Canonical syntax6768Training data is full of v3 and of verbose arbitrary variants. Emit the first-class form:6970| Instead of | Write | |71| --- | --- | --- |72| `[&>*]:` / `[&_*]:` | `*:` / `**:` | direct children / all descendants |73| `[&>[role=checkbox]]:` | `*:[[role=checkbox]]:` | outer `[]` = arbitrary variant, inner `[]` = the attribute selector |74| `[&>[data-open]]:` | `*:data-open:` | `data-*` / `aria-*` are first-class |75| `[&:has(...)]:` `[&:not(:first-child)]:` `[&:nth-child(odd)]:` | `has-[...]:` `not-first:` `odd:` | |76| `group-[.foo]:` | `group-hover:` / `peer-invalid:` | only when a **named state variant** exists; a class-qualified group is otherwise fine |77| `[@media_print]:` / `[@media(width>=…)]:` | `print:` / `lg:` / `max-lg:` | |78| `!flex` | `flex!` | v4's marker is the suffix; the prefix still parses, so it is non-canonical, not broken |79| `bg-[--token]` / `bg-[var(--token)]` | `bg-(--token)` | same for modifiers: `bg-primary/(--alpha)` |80| `grid-cols-[auto,1fr]` | `grid-cols-[auto_1fr]` | underscore is the space; no padding underscores |81| `bg-gradient-to-r` `flex-grow` `overflow-ellipsis` `break-words` `decoration-clone` `bg-left-top` | `bg-linear-to-r` `grow` `text-ellipsis` `wrap-break-word` `box-decoration-clone` `bg-top-left` | v3 names |8283**A component library can redefine these variant names.** `shadcn/tailwind.css` ships its own `@custom-variant data-open` matching `[data-state="open"]` *and* `[data-open]`, plus `data-closed` / `data-checked` / `data-selected` / `data-disabled` / `data-active` / `data-horizontal` / `data-vertical`. Where it is imported those are broader than stock Tailwind's — read the project's CSS before assuming `data-x:` means the bare attribute.8485Fewer classes, same result:8687- **Collapse same-value sides.** `size-4` over `w-4 h-4`; `p-4` over `px-4 py-4`; `m-4` over four sides; `inset-0` over four offsets; `text-sm/7` over `text-sm leading-7`.88- **Don't restate a default.** `flex flex-row` is just `flex` (row is the default). Same for `opacity-100`, `scale-100`, `rotate-0`, `order-0`, `basis-auto` — emitting them adds a class that changes nothing.89- **Don't emit two classes that set the same property.** Write the one you mean. When you find a pair in existing code, do not assume the last one written wins — that is decided by Tailwind's emission order, not markup order (see `references/cleanup.md`).9091**Do not over-correct.** These are already right, and "fixing" them changes behaviour or is plainly wrong:9293- `[&:hover]:` is **not** the same as `hover:` — the named variant also wraps `@media (hover: hover)`. Only use `hover:` when you mean that.94- **Never apply the v3→v4 rename table to v4 code.** `shadow`, `rounded`, `ring`, `outline-none` are all valid v4 classes; remapping them to `shadow-sm` / `rounded-sm` / `ring-3` / `outline-hidden` changes the render (v4 `ring` is 1px, so `ring-3` triples it) or is a pointless no-op rename.95- **Never rewrite `shadow-sm` / `blur-sm` / `rounded-sm` / `drop-shadow-sm` / `backdrop-blur-sm` to `-xs`.** The rename moved *v3's* `shadow-sm` to `shadow-xs`; it did not delete `shadow-sm`, which is its own v4 utility with its own value. Doing this shrinks every shadow, blur and radius by one step. (v4's smallest shadow is `shadow-2xs`.)96- **Don't convert viewport variants into container queries.** `md:`/`lg:` and `@md:`/`@lg:` are both first-class and mean different things. Viewport is the default for page chrome; reach for `@container` when authoring a component that will live in more than one slot width. See `references/gotchas.md`.97- Leave anything that only *looks* non-canonical — a stacked `data-active:hover:`, `data-[foo=bar]:`, `[figure>&]:`, `:where()` wrappers. Arbitrary variants are the escape hatch; the full list is in `references/cleanup.md` under *Never touch*.9899Where the project has a linter configured, finish an editing pass with `npx eslint --fix` — see `references/editor.md`.100101---102103## When to load more104105- **Scaffolding a project** — no `globals.css`, no `@theme` block, wiring the PostCSS/Vite entry, adding the theme toggle, or setting up `cn()` or the button cursor for the first time: read `references/setup.md` before writing CSS. It wires the token contract and leaves the palette values to `shadcn init` or the user; it carries three decisions that must be **asked, not assumed**.106- **A v4 trap** — a utility not applying, "Cannot apply unknown utility class", `@apply` in a Vue/Svelte/Astro `<style>` or CSS Module, `h-screen` on mobile, a dynamic `bg-${x}` class, `truncate` not clipping, container queries / `@md:` vs `md:`, or a token that looks v3-shaped: read `references/gotchas.md`.107- **A repeated class string, or an existing `@layer components` block** — whether a look should become a named `@utility`: read `references/affordances.md`.108- **Tooling** — editor autocomplete inside `cva`/`cn`, class sorting, or a lint rule to enforce this house style: read `references/editor.md`.109- **Cleanup / audit / simplify** — the user asked, or you are reviewing a component for class drift: read `references/cleanup.md` and follow its process and output format.