[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6D2GW10WYJMQ5MP0YWWX00Y":3,"skill-same:01M6D2GW10WYJMQ5MP0YWWX00Y":67},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":47,"repositoryHtmlUrl":48,"repositoryHomepageUrl":49,"ownerHtmlUrl":50,"ownerBlogUrl":49,"ownerDescription":51,"updatedAt":52,"install":53,"resources":66},"01M6D2GW10WYJMQ5MP0YWWX00Y","ui-stack-tailwind",null,"Building with Tailwind CSS v4 (and v3) without the Tailwind-default look: setup per framework (Vite, Next.js, Astro, Laravel, monorepos), @theme tokens in OKLCH named by role, dark mode with @custom-variant, the layout system as tokens (one container, section spacing, a type scale, stretched rows) and how to fix each preview layout line with classes, state, aria, data, group, peer, has, motion and container-query variants, component recipes (button, field, card, navigation, table, dialog) with every state, motion with --animate-* and starting:, cn with tailwind-merge, cva, class order, @utility, arbitrary values, typography, v3 to v4 migration, and the generated look. Read when the project uses Tailwind.","enowdev\u002Fenowxcli","enowdev","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F179420349?v=4","01M6D1MVX2KC0X3Z9NESE7P22K","Enow Dev.","01M6D1MW1FMZGBDKWAJY4GQX81",67,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-stack-tailwind\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"ui stack tailwind","ui","Tailwind","stack","generated","layout","project","preview","next","query","aria","default","fix","css","dialog","laravel","how","astro","merge","component","2026-11-10 10:12:30",false,"---\nname: ui-stack-tailwind\ndescription: \"Building with Tailwind CSS v4 (and v3) without the Tailwind-default look: setup per framework (Vite, Next.js, Astro, Laravel, monorepos), @theme tokens in OKLCH named by role, dark mode with @custom-variant, the layout system as tokens (one container, section spacing, a type scale, stretched rows) and how to fix each preview layout line with classes, state, aria, data, group, peer, has, motion and container-query variants, component recipes (button, field, card, navigation, table, dialog) with every state, motion with --animate-* and starting:, cn with tailwind-merge, cva, class order, @utility, arbitrary values, typography, v3 to v4 migration, and the generated look. Read when the project uses Tailwind.\"\n---\n\n# Tailwind CSS\n\nTailwind makes the default look very cheap: `bg-gradient-to-r\nfrom-purple-500 to-pink-500`, `rounded-2xl shadow-2xl`, `backdrop-blur` on\nevery card, slate greys with an indigo accent. Use it to apply the product's\ndecisions, not the framework's: tokens in the theme, utilities named by\nrole, a state on every control, components instead of copied class strings.\nVersion 4 (4.3 in 2026) is current; v3 notes are in section 11.\n\n## 1. Which version, and the setup\n\n- v4: the CSS starts with `@import \"tailwindcss\";` and the build uses\n  `@tailwindcss\u002Fvite`, `@tailwindcss\u002Fpostcss` or `@tailwindcss\u002Fcli`. There\n  is no `content` list: sources are found automatically (ignoring what\n  `.gitignore` ignores), and `@source \"..\u002Fpackages\u002Fui\"` adds a path.\n- v3: a `tailwind.config.js` with `content`, and `@tailwind base;\n  @tailwind components; @tailwind utilities;` in the CSS.\n- v4 needs Safari 16.4+, Chrome 111+ and Firefox 128+; a product that must\n  run on older browsers stays on v3.4.\n\n| Stack | Install | Wire it |\n|---|---|---|\n| Vite (React, Vue, Svelte, plain) | `npm i tailwindcss @tailwindcss\u002Fvite` | `plugins: [tailwindcss()]` in `vite.config.ts`, `@import \"tailwindcss\";` in the main CSS |\n| Next.js | `npm i tailwindcss @tailwindcss\u002Fpostcss postcss` | `postcss.config.mjs`: `export default { plugins: { \"@tailwindcss\u002Fpostcss\": {} } }`; the import in `app\u002Fglobals.css` |\n| Astro | `npx astro add tailwind` (the Vite plugin) | the import in `src\u002Fstyles\u002Fglobal.css`, imported by the layout |\n| Laravel | `npm i tailwindcss @tailwindcss\u002Fvite` | the Vite plugin beside `laravel-vite-plugin`; `@source \"..\u002F..\u002Fresources\u002Fviews\";` when Blade files are not found |\n| No build | `npx @tailwindcss\u002Fcli -i src\u002Fin.css -o dist\u002Fout.css --watch` | the built file linked in HTML; never the CDN script in production |\n\n- Sources: `@import \"tailwindcss\" source(\"..\u002Fsrc\");` sets where classes are\n  found; `@source \"..\u002Fpackages\u002Fui\u002Fsrc\";` adds a package in a monorepo;\n  `@source not \"..\u002Flegacy\";` leaves a folder out. A class built from strings\n  (`` `bg-${tone}` ``) is never found: map to full class names\n  (`{ ok: \"bg-ok\", warn: \"bg-warn\" }[tone]`).\n\n## 2. Theme first: tokens named by role\n\nThe palette, fonts, radii and shadows live in the theme, named by role:\n`bg-surface`, `text-muted`, `border-line`, `bg-accent`, not `bg-purple-600`\nscattered through the app.\n\n```css\n@import \"tailwindcss\";\n\n@theme {\n  --color-*: initial;                    \u002F* the product's colours only, no default palette *\u002F\n  --color-paper: oklch(0.98 0.006 85);    \u002F* values from DESIGN.md *\u002F\n  --color-surface: oklch(1 0 0);\n  --color-surface-2: oklch(0.955 0.008 85);\n  --color-ink: oklch(0.21 0.012 85);\n  --color-muted: oklch(0.49 0.012 85);\n  --color-line: oklch(0.9 0.008 85);\n  --color-accent: oklch(0.5 0.14 40);\n  --color-on-accent: oklch(0.99 0.004 85);\n  --color-danger: oklch(0.52 0.19 27);\n  --font-sans: \"Public Sans\", ui-sans-serif, system-ui, sans-serif;\n  --font-display: \"Newsreader\", ui-serif, Georgia, serif;\n  --text-display: clamp(2rem, 1.4rem + 2.6vw, 3.0625rem);\n  --text-display--line-height: 1.1;\n  --radius-sm: 4px;\n  --radius-md: 8px;\n  --shadow-overlay: 0 8px 24px oklch(0.2 0.02 85 \u002F 0.14);\n  --ease-out-soft: cubic-bezier(0.22, 0.61, 0.36, 1);\n}\n```\n\n- Each variable makes its utilities: `--color-accent` gives `bg-accent`,\n  `text-accent`, `border-accent`; `--text-display` gives `text-display`;\n  `--ease-out-soft` gives `ease-out-soft`. They are also CSS variables at\n  `:root`, for plain CSS (`var(--color-accent)`) and scripts.\n- `--color-*: initial` drops the default palette, so `bg-purple-600` stops\n  compiling; leave it out when a library in the project uses those colours.\n- Colours in OKLCH: the steps of a role (hover, border, dark) keep the hue\n  and move the lightness, so they stay one family.\n- The spacing scale is `--spacing: 0.25rem` (a 4px base, what `ui` asks\n  for); keep it. Breakpoints are `--breakpoint-*`: add one where the content\n  breaks, not per device.\n- `@theme inline` when a token's value is another variable (a next\u002Ffont\n  variable, shadcn's `--color-primary: var(--primary)`): the utility then\n  reads that variable where it is used, not a value resolved at the root.\n\n## 3. Dark mode and themes\n\n```css\n@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));\n\n@layer base {\n  [data-theme=\"dark\"] {\n    color-scheme: dark;\n    --color-paper: oklch(0.16 0.006 85);\n    --color-surface: oklch(0.2 0.007 85);\n    --color-surface-2: oklch(0.24 0.008 85);\n    --color-ink: oklch(0.93 0.006 85);\n    --color-muted: oklch(0.7 0.01 85);\n    --color-line: oklch(0.3 0.008 85);\n    --color-accent: oklch(0.72 0.15 45);\n    --color-on-accent: oklch(0.18 0.02 45);\n  }\n}\n```\n\n- Dark mode by the class or data-attribute strategy, through the variables:\n  utilities read them, so redefining them under the switch changes every\n  `bg-paper` and `text-ink` at once, and most elements need no `dark:` class\n  at all. `dark:` stays for what is not a colour token (another image, a\n  shadow that goes away).\n- Without the custom variant, `dark:` follows `prefers-color-scheme` only,\n  and a toggle cannot win. The script in `\u003Chead>` sets `data-theme` from the\n  saved choice or the system before the first paint (`ui-themes`).\n\n## 4. The layout system as tokens\n\nA page built section by section drifts in Tailwind as much as anywhere: one\nsection `max-w-3xl mx-auto`, the next `max-w-6xl px-8`, gaps of `py-4` here\nand `py-32` there, eleven text sizes. The measures in `ui-layout` go into the\ntheme once, and every section uses the same few classes:\n\n```css\n@theme {\n  --container-page: 72rem;                     \u002F* max-w-page: the one content width *\u002F\n  --container-measure: 65ch;                   \u002F* max-w-measure: running text *\u002F\n  --spacing-gutter: clamp(1rem, 4vw, 2rem);    \u002F* px-gutter *\u002F\n  --spacing-section: clamp(3rem, 8vw, 6rem);   \u002F* py-section: between sections *\u002F\n  --text-*: initial;                           \u002F* the product's scale only *\u002F\n  --text-sm: 0.875rem;  --text-sm--line-height: 1.5;\n  --text-base: 1rem;    --text-base--line-height: 1.6;\n  --text-lg: 1.25rem;   --text-lg--line-height: 1.5;\n  --text-xl: 1.563rem;  --text-xl--line-height: 1.3;\n  --text-2xl: 1.953rem; --text-2xl--line-height: 1.2;\n  --text-display: clamp(2.441rem, 1.8rem + 3vw, 3.815rem); --text-display--line-height: 1.1;\n}\n\n@utility page { width: 100%; max-width: var(--container-page); margin-inline: auto; padding-inline: var(--spacing-gutter); }\n```\n\n```html\n\u003Csection class=\"py-section\">\n  \u003Cdiv class=\"page\">\n    \u003Ch2 class=\"text-2xl font-display\">Selected work\u003C\u002Fh2>\n    \u003Cp class=\"mt-3 max-w-measure text-muted\">…\u003C\u002Fp>\n    \u003Cul class=\"mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3\">…\u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Fsection>\n```\n\n- Every section's content sits in `page`: one left edge, one width. A\n  narrower column is `max-w-measure` inside it, left-aligned, never a second\n  `mx-auto` box.\n- Text sizes come from the scale (`text-sm` to `text-2xl`, and\n  `text-display` from section 2); with `--text-*: initial` the other defaults\n  stop compiling, so a stray `text-5xl` does nothing and shows at once, and a\n  `text-[15px]` is a finding in review. The page title is `text-display`, at\n  least twice the body.\n- Spacing from the 4px scale: `gap-2`\u002F`gap-3` inside a component, `gap-6`\n  between items, `mt-8`\u002F`mt-12` between groups, `py-section` between\n  sections. A heading sits closer to what follows (`mt-3`) than to what came\n  before.\n- Rows of cards share a height: a grid stretches its items by default\n  (`grid auto-rows-fr` when every row should match), and each card is\n  `flex flex-col` with its action `mt-auto`.\n- The first section clears the header: `pt-section`, or the header's height\n  when it is sticky (`scroll-mt-16` on anchored sections too).\n\n### The `preview` layout lines, in classes\n\n| Line | Fix |\n|---|---|\n| sections start at N left edges \u002F content widths differ | Wrap each section's content in `page`; remove its own `max-w-*`, `mx-auto`, `px-*` |\n| blocks a few pixels off the edge | A `pl-*`, `ml-*`, border or `-mx-*` on one block: remove it, or move it inside |\n| the first content sits 0px from the top | `pt-section` on the first section, or header padding |\n| sections run into each other | `py-section` on every section |\n| elements overlap | A fixed `w-*`\u002F`h-*` or negative margin at this width: `shrink-0` on the media, `flex-col` below `sm:` |\n| still invisible after scrolling | An `opacity-0` start that a class never removed: start visible, animate with `motion-safe:` and `starting:` (section 8) |\n| N font sizes \u002F weak h1 | Only the scale; `text-display` for the title, `text-2xl` for section heads |\n| body text is 13px | `text-base` (16px) for body copy, `text-sm` only for secondary text |\n| paragraph lines run long | `max-w-measure` on running text |\n| boxes side by side with different heights | `items-stretch` (the grid default), `flex flex-col` in each box, `mt-auto` on its action |\n| N different text colours | `text-ink`, `text-muted`, `text-accent` and state colours only |\n\n## 5. Variants: states, structure, containers\n\nStates on every control: `hover:`, `focus-visible:`, `disabled:`, and the\nARIA and data attributes the component already sets:\n\n```html\n\u003Cbutton class=\"inline-flex h-10 items-center gap-2 rounded-md bg-accent px-4 font-medium text-on-accent\n  hover:bg-accent\u002F90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\n  disabled:cursor-not-allowed disabled:opacity-50 aria-busy:cursor-progress\">Save changes\u003C\u002Fbutton>\n\u003Cinput class=\"h-10 rounded-md border border-line bg-surface px-3 aria-invalid:border-danger user-invalid:border-danger\">\n```\n\n- Never `outline-none` without a `focus-visible` style. In v4 the old\n  `outline-none` is `outline-hidden` (it keeps an outline in forced-colours\n  mode); either needs the visible replacement.\n- `aria-expanded:`, `aria-selected:`, `data-[state=open]:` (or `data-open:`\n  for a present attribute) style the state the component exposes; no\n  second class to keep in sync. v3 writes `aria-[invalid=true]:`.\n- Parents and siblings: `group` with `group-hover:` or\n  `group-focus-within:`, `peer` with `peer-checked:`, `has-[input:checked]:`\n  for a parent that changes with its content, `not-last:` and `*:` for\n  children.\n- `motion-safe:` on animations and transform transitions;\n  `motion-reduce:transition-none` where a library adds its own\n  (`motion-stacks`).\n- Mobile-first: unprefixed for the phone, `sm:` `md:` `lg:` as it widens,\n  `max-md:` for below. Rearrange (`flex-col md:flex-row`, `order-*`), do not\n  duplicate markup behind `hidden md:block`.\n- Container queries for a component placed at different widths: `@container`\n  on the parent, `@md:grid-cols-2` on the child; named ones with\n  `@container\u002Fcard` and `@md\u002Fcard:`.\n- Page container: `mx-auto max-w-6xl px-4 sm:px-6`, text `max-w-prose`;\n  inside an app shell, no `mx-auto` (`ui-layout`).\n\n## 6. Recipes, with every state\n\n```html\n\u003C!-- Field: the label names it, the hint and the error describe it -->\n\u003Cdiv class=\"grid gap-1.5\">\n  \u003Clabel for=\"email\" class=\"text-sm font-medium text-ink\">Email\u003C\u002Flabel>\n  \u003Cinput id=\"email\" type=\"email\" aria-describedby=\"email-hint email-error\" aria-invalid=\"true\"\n    class=\"h-10 rounded-md border border-line bg-surface px-3 text-base text-ink placeholder:text-muted\n           focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent\n           aria-invalid:border-danger disabled:bg-surface-2 disabled:text-muted\">\n  \u003Cp id=\"email-hint\" class=\"text-sm text-muted\">We send the receipt here.\u003C\u002Fp>\n  \u003Cp id=\"email-error\" class=\"text-sm text-danger\">Enter an address like name@example.com.\u003C\u002Fp>\n\u003C\u002Fdiv>\n\n\u003C!-- Card: the whole card is the link; the title is the name, the action sits at the bottom -->\n\u003Carticle class=\"group relative flex flex-col rounded-md border border-line bg-surface p-5\n                hover:border-ink\u002F30 has-focus-visible:outline-2 has-focus-visible:outline-accent\">\n  \u003Ch3 class=\"text-lg font-medium\">\u003Ca href=\"\u002Fwork\u002Fledger\" class=\"after:absolute after:inset-0 focus:outline-none\">Ledger\u003C\u002Fa>\u003C\u002Fh3>\n  \u003Cp class=\"mt-2 text-muted\">…\u003C\u002Fp>\n  \u003Cspan class=\"mt-auto pt-4 text-sm text-accent group-hover:underline\">Read the case\u003C\u002Fspan>\n\u003C\u002Farticle>\n\n\u003C!-- Navigation: the current page is marked for everyone, not by colour alone -->\n\u003Ca href=\"\u002Fwork\" aria-current=\"page\"\n   class=\"rounded-sm px-2 py-1 text-muted hover:text-ink aria-[current=page]:font-medium\n          aria-[current=page]:text-ink aria-[current=page]:underline underline-offset-4\">Work\u003C\u002Fa>\n\n\u003C!-- Table: scrolls in its own box on a phone; numbers right-aligned in tabular figures -->\n\u003Cdiv class=\"overflow-x-auto rounded-md border border-line\">\n  \u003Ctable class=\"w-full text-sm\">\n    \u003Cthead class=\"bg-surface-2 text-left text-muted\">\u003Ctr>\u003Cth scope=\"col\" class=\"px-4 py-2 font-medium\">Invoice\u003C\u002Fth>\n      \u003Cth scope=\"col\" class=\"px-4 py-2 text-right font-medium\">Amount\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\n    \u003Ctbody class=\"divide-y divide-line\">\u003Ctr class=\"hover:bg-surface-2\">\n      \u003Ctd class=\"px-4 py-3\">INV-0042\u003C\u002Ftd>\u003Ctd class=\"px-4 py-3 text-right tabular-nums\">Rp 1.250.000\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\n  \u003C\u002Ftable>\n\u003C\u002Fdiv>\n\n\u003C!-- Dialog: the native element, its backdrop, and an entry that respects reduced motion -->\n\u003Cdialog class=\"m-auto w-full max-w-md rounded-md bg-surface p-6 text-ink shadow-overlay backdrop:bg-ink\u002F40\n               opacity-100 transition-[opacity,translate] duration-200 starting:open:opacity-0\n               starting:open:translate-y-2 motion-reduce:transition-none\">…\u003C\u002Fdialog>\n```\n\n- Every control has hover, `focus-visible`, disabled, and the ARIA state it\n  exposes; a busy button says so (`aria-busy`, a spinner, its text kept).\n- Touch targets reach 44px on a phone: `h-11` or `min-h-11`, or padding on\n  an icon button (`p-2.5` around a 24px icon).\n- Buttons: one filled primary per view (`bg-accent text-on-accent`), the\n  rest outline (`border border-line`) or plain (`hover:bg-surface-2`). Their\n  variants live in one `cva` (section 7), not in copied strings.\n\n## 7. Components, cn and class order\n\n- A class group that repeats becomes a component (a React, Vue or Svelte\n  component, or a partial), not a copy and not `@apply` everywhere. Keep\n  `@apply` for base element styles and markup you do not control (HTML from\n  a CMS).\n- Variants with `cva` (class-variance-authority) or a small map keyed by\n  variant (the Button in `ui-stack-react`); merge a caller's classes with\n  `cn()` (clsx and tailwind-merge) so a caller's `px-6` replaces the default\n  rather than both applying:\n\n```ts\n\u002F\u002F lib\u002Fcn.ts\nimport { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n\u002F\u002F tailwind-merge knows only the default theme: name custom text sizes, or it\n\u002F\u002F reads text-display as a colour and drops it beside text-muted.\nconst twMerge = extendTailwindMerge({ extend: { theme: { text: [\"display\"] } } });\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n```\n\n- tailwind-merge 3 is for Tailwind 4 (2 for 3). The `cn` package (from the\n  shadcn team, September 2026) replaces clsx and tailwind-merge together:\n  `export { cn } from \"cn\"`, and `createCn` from `cn\u002Fconfig` takes the same\n  `extend` for custom tokens.\n- `@utility` for a custom utility that takes variants:\n  `@utility focus-ring { outline: 2px solid var(--color-accent);\n  outline-offset: 2px; }`, then `focus-visible:focus-ring`.\n- In a Vue or Svelte `\u003Cstyle>` block, `@reference \"..\u002Fapp.css\";` first, so\n  `@apply` and theme values resolve without emitting Tailwind twice.\n- Keep class order readable: layout, box, type, colour, state, responsive.\n  The Prettier plugin sorts them, including inside `cn()` and `cva()`:\n\n```json\n{\n  \"plugins\": [\"prettier-plugin-tailwindcss\"],\n  \"tailwindStylesheet\": \".\u002Fsrc\u002Fapp.css\",\n  \"tailwindFunctions\": [\"cn\", \"cva\", \"clsx\"]\n}\n```\n\n## 8. Motion\n\n```css\n@theme {\n  --animate-rise: rise 480ms var(--ease-out-soft) both;\n  @keyframes rise {\n    from { opacity: 0; translate: 0 12px; }\n  }\n}\n```\n\n- Animate `opacity` and `translate`\u002F`scale` only: `transition-[opacity,translate]`,\n  never `transition-all` (it animates layout and colours you did not mean).\n  Durations and easing from tokens (`duration-200 ease-out-soft`).\n- Entry without JavaScript: `starting:` (`@starting-style`) for elements that\n  appear (a dialog, a popover, an item added to a list); `motion-safe:` on\n  every animation, `motion-reduce:` to turn a library's off.\n- Content is visible in the markup; an animation only moves it into place.\n  A reveal that starts at `opacity-0` needs the class that ends it to be\n  certain to arrive, or the content stays hidden (`motion-reveal`).\n\n## 9. Values, prose\n\n- Use the scale. Arbitrary values (`w-[347px]`, `text-[#1f2937]`,\n  `mt-[13px]`) are the exception with a reason, not the way to match a\n  mock-up; a value needed twice is a token.\n- A CSS variable as a value: `w-(--sidebar-width)` in v4 (`w-[--sidebar-width]`\n  was v3). `size-10` for equal width and height; `min-h-dvh`, never\n  `h-screen` on a section.\n- Long text from Markdown or a CMS: `@plugin \"@tailwindcss\u002Ftypography\";`\n  and `prose` on the wrapper (65ch), styled from the tokens with element\n  modifiers (`prose-headings:font-display prose-a:text-accent`) and\n  `dark:prose-invert` or token overrides for the dark theme.\n\n## 10. The generated look\n\nGradient text (`bg-clip-text text-transparent`), purple-to-pink gradients,\n`rounded-full` on everything, `shadow-2xl` cards, `backdrop-blur` on\nseveral surfaces, `uppercase tracking-widest` eyebrows over every heading,\nslate or zinc greys with an indigo accent, `rounded-2xl shadow-xl` on every\ncard, `ring-1 ring-white\u002F10` glass panels on a dark gradient, `h-screen`\nheroes, `text-gray-400` body text that fails contrast. `ui_check` finds\nthem.\n\n## 11. From v3 to v4\n\n- `npx @tailwindcss\u002Fupgrade` (Node 20+) on a clean branch, then read the\n  diff: it moves the config into CSS, rewrites renamed classes and swaps\n  the build plugin.\n- Renamed: `shadow-sm` to `shadow-xs` and `shadow` to `shadow-sm` (the same\n  shift for `rounded`, `blur` and `drop-shadow`), `outline-none` to\n  `outline-hidden`, `ring` to `ring-3`, `bg-gradient-to-r` to\n  `bg-linear-to-r`, `flex-shrink-0` to `shrink-0`, `bg-opacity-50` to a\n  colour with `\u002F50`.\n- Changed defaults: `border` and `ring` are `currentColor` (give them a\n  colour), `ring` is 1px, placeholders are the text colour at half opacity,\n  buttons have `cursor: default`, and `hover:` applies only where the device\n  can hover.\n- `!` goes at the end (`bg-accent!`), stacked variants read left to right,\n  and JS plugins load with `@plugin`; a JS config that must stay loads with\n  `@config \".\u002Ftailwind.config.js\"`.\n- In v3 itself: `theme.extend` in `tailwind.config`, pointing at CSS\n  variables (`colors: { ink: \"var(--ink)\" }`), so a dark theme swaps the\n  variables, not every class.\n\n## Check it\n\n- `preview` at 360, 768 and 1440px: its `layout` lines clear (section 4),\n  both themes.\n- `ui_check` for the generated look and raw values; `grep -rnE\n  \"\\[#[0-9a-fA-F]|-\\[[0-9.]+px\\]\" src` for arbitrary colours and sizes.\n- A misspelt class fails silently (it generates nothing): look at the\n  rendered page with the `preview` tool, in both themes.\n- Prettier on changed files, so class order stays sorted and diffs small.\n\n## Avoid\n\nThe default palette in components (`bg-purple-600`, `text-gray-500`);\narbitrary values where a token exists; `@apply` to rebuild components in\nCSS; `outline-none` with no focus style; `dark:` on every element instead of\ntokens; `hidden md:block` duplicates of the same markup; `cn` without a\nmerge that knows the custom tokens; `h-screen` sections; the generated look\nabove; sections with their own width or padding instead of `page`; text\nsizes outside the scale; `transition-all`; content that starts hidden.\n","# Tailwind CSS\n\nTailwind makes the default look very cheap: `bg-gradient-to-r\nfrom-purple-500 to-pink-500`, `rounded-2xl shadow-2xl`, `backdrop-blur` on\nevery card, slate greys with an indigo accent. Use it to apply the product's\ndecisions, not the framework's: tokens in the theme, utilities named by\nrole, a state on every control, components instead of copied class strings.\nVersion 4 (4.3 in 2026) is current; v3 notes are in section 11.\n\n## 1. Which version, and the setup\n\n- v4: the CSS starts with `@import \"tailwindcss\";` and the build uses\n  `@tailwindcss\u002Fvite`, `@tailwindcss\u002Fpostcss` or `@tailwindcss\u002Fcli`. There\n  is no `content` list: sources are found automatically (ignoring what\n  `.gitignore` ignores), and `@source \"..\u002Fpackages\u002Fui\"` adds a path.\n- v3: a `tailwind.config.js` with `content`, and `@tailwind base;\n  @tailwind components; @tailwind utilities;` in the CSS.\n- v4 needs Safari 16.4+, Chrome 111+ and Firefox 128+; a product that must\n  run on older browsers stays on v3.4.\n\n| Stack | Install | Wire it |\n|---|---|---|\n| Vite (React, Vue, Svelte, plain) | `npm i tailwindcss @tailwindcss\u002Fvite` | `plugins: [tailwindcss()]` in `vite.config.ts`, `@import \"tailwindcss\";` in the main CSS |\n| Next.js | `npm i tailwindcss @tailwindcss\u002Fpostcss postcss` | `postcss.config.mjs`: `export default { plugins: { \"@tailwindcss\u002Fpostcss\": {} } }`; the import in `app\u002Fglobals.css` |\n| Astro | `npx astro add tailwind` (the Vite plugin) | the import in `src\u002Fstyles\u002Fglobal.css`, imported by the layout |\n| Laravel | `npm i tailwindcss @tailwindcss\u002Fvite` | the Vite plugin beside `laravel-vite-plugin`; `@source \"..\u002F..\u002Fresources\u002Fviews\";` when Blade files are not found |\n| No build | `npx @tailwindcss\u002Fcli -i src\u002Fin.css -o dist\u002Fout.css --watch` | the built file linked in HTML; never the CDN script in production |\n\n- Sources: `@import \"tailwindcss\" source(\"..\u002Fsrc\");` sets where classes are\n  found; `@source \"..\u002Fpackages\u002Fui\u002Fsrc\";` adds a package in a monorepo;\n  `@source not \"..\u002Flegacy\";` leaves a folder out. A class built from strings\n  (`` `bg-${tone}` ``) is never found: map to full class names\n  (`{ ok: \"bg-ok\", warn: \"bg-warn\" }[tone]`).\n\n## 2. Theme first: tokens named by role\n\nThe palette, fonts, radii and shadows live in the theme, named by role:\n`bg-surface`, `text-muted`, `border-line`, `bg-accent`, not `bg-purple-600`\nscattered through the app.\n\n```css\n@import \"tailwindcss\";\n\n@theme {\n  --color-*: initial;                    \u002F* the product's colours only, no default palette *\u002F\n  --color-paper: oklch(0.98 0.006 85);    \u002F* values from DESIGN.md *\u002F\n  --color-surface: oklch(1 0 0);\n  --color-surface-2: oklch(0.955 0.008 85);\n  --color-ink: oklch(0.21 0.012 85);\n  --color-muted: oklch(0.49 0.012 85);\n  --color-line: oklch(0.9 0.008 85);\n  --color-accent: oklch(0.5 0.14 40);\n  --color-on-accent: oklch(0.99 0.004 85);\n  --color-danger: oklch(0.52 0.19 27);\n  --font-sans: \"Public Sans\", ui-sans-serif, system-ui, sans-serif;\n  --font-display: \"Newsreader\", ui-serif, Georgia, serif;\n  --text-display: clamp(2rem, 1.4rem + 2.6vw, 3.0625rem);\n  --text-display--line-height: 1.1;\n  --radius-sm: 4px;\n  --radius-md: 8px;\n  --shadow-overlay: 0 8px 24px oklch(0.2 0.02 85 \u002F 0.14);\n  --ease-out-soft: cubic-bezier(0.22, 0.61, 0.36, 1);\n}\n```\n\n- Each variable makes its utilities: `--color-accent` gives `bg-accent`,\n  `text-accent`, `border-accent`; `--text-display` gives `text-display`;\n  `--ease-out-soft` gives `ease-out-soft`. They are also CSS variables at\n  `:root`, for plain CSS (`var(--color-accent)`) and scripts.\n- `--color-*: initial` drops the default palette, so `bg-purple-600` stops\n  compiling; leave it out when a library in the project uses those colours.\n- Colours in OKLCH: the steps of a role (hover, border, dark) keep the hue\n  and move the lightness, so they stay one family.\n- The spacing scale is `--spacing: 0.25rem` (a 4px base, what `ui` asks\n  for); keep it. Breakpoints are `--breakpoint-*`: add one where the content\n  breaks, not per device.\n- `@theme inline` when a token's value is another variable (a next\u002Ffont\n  variable, shadcn's `--color-primary: var(--primary)`): the utility then\n  reads that variable where it is used, not a value resolved at the root.\n\n## 3. Dark mode and themes\n\n```css\n@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));\n\n@layer base {\n  [data-theme=\"dark\"] {\n    color-scheme: dark;\n    --color-paper: oklch(0.16 0.006 85);\n    --color-surface: oklch(0.2 0.007 85);\n    --color-surface-2: oklch(0.24 0.008 85);\n    --color-ink: oklch(0.93 0.006 85);\n    --color-muted: oklch(0.7 0.01 85);\n    --color-line: oklch(0.3 0.008 85);\n    --color-accent: oklch(0.72 0.15 45);\n    --color-on-accent: oklch(0.18 0.02 45);\n  }\n}\n```\n\n- Dark mode by the class or data-attribute strategy, through the variables:\n  utilities read them, so redefining them under the switch changes every\n  `bg-paper` and `text-ink` at once, and most elements need no `dark:` class\n  at all. `dark:` stays for what is not a colour token (another image, a\n  shadow that goes away).\n- Without the custom variant, `dark:` follows `prefers-color-scheme` only,\n  and a toggle cannot win. The script in `\u003Chead>` sets `data-theme` from the\n  saved choice or the system before the first paint (`ui-themes`).\n\n## 4. The layout system as tokens\n\nA page built section by section drifts in Tailwind as much as anywhere: one\nsection `max-w-3xl mx-auto`, the next `max-w-6xl px-8`, gaps of `py-4` here\nand `py-32` there, eleven text sizes. The measures in `ui-layout` go into the\ntheme once, and every section uses the same few classes:\n\n```css\n@theme {\n  --container-page: 72rem;                     \u002F* max-w-page: the one content width *\u002F\n  --container-measure: 65ch;                   \u002F* max-w-measure: running text *\u002F\n  --spacing-gutter: clamp(1rem, 4vw, 2rem);    \u002F* px-gutter *\u002F\n  --spacing-section: clamp(3rem, 8vw, 6rem);   \u002F* py-section: between sections *\u002F\n  --text-*: initial;                           \u002F* the product's scale only *\u002F\n  --text-sm: 0.875rem;  --text-sm--line-height: 1.5;\n  --text-base: 1rem;    --text-base--line-height: 1.6;\n  --text-lg: 1.25rem;   --text-lg--line-height: 1.5;\n  --text-xl: 1.563rem;  --text-xl--line-height: 1.3;\n  --text-2xl: 1.953rem; --text-2xl--line-height: 1.2;\n  --text-display: clamp(2.441rem, 1.8rem + 3vw, 3.815rem); --text-display--line-height: 1.1;\n}\n\n@utility page { width: 100%; max-width: var(--container-page); margin-inline: auto; padding-inline: var(--spacing-gutter); }\n```\n\n```html\n\u003Csection class=\"py-section\">\n  \u003Cdiv class=\"page\">\n    \u003Ch2 class=\"text-2xl font-display\">Selected work\u003C\u002Fh2>\n    \u003Cp class=\"mt-3 max-w-measure text-muted\">…\u003C\u002Fp>\n    \u003Cul class=\"mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3\">…\u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Fsection>\n```\n\n- Every section's content sits in `page`: one left edge, one width. A\n  narrower column is `max-w-measure` inside it, left-aligned, never a second\n  `mx-auto` box.\n- Text sizes come from the scale (`text-sm` to `text-2xl`, and\n  `text-display` from section 2); with `--text-*: initial` the other defaults\n  stop compiling, so a stray `text-5xl` does nothing and shows at once, and a\n  `text-[15px]` is a finding in review. The page title is `text-display`, at\n  least twice the body.\n- Spacing from the 4px scale: `gap-2`\u002F`gap-3` inside a component, `gap-6`\n  between items, `mt-8`\u002F`mt-12` between groups, `py-section` between\n  sections. A heading sits closer to what follows (`mt-3`) than to what came\n  before.\n- Rows of cards share a height: a grid stretches its items by default\n  (`grid auto-rows-fr` when every row should match), and each card is\n  `flex flex-col` with its action `mt-auto`.\n- The first section clears the header: `pt-section`, or the header's height\n  when it is sticky (`scroll-mt-16` on anchored sections too).\n\n### The `preview` layout lines, in classes\n\n| Line | Fix |\n|---|---|\n| sections start at N left edges \u002F content widths differ | Wrap each section's content in `page`; remove its own `max-w-*`, `mx-auto`, `px-*` |\n| blocks a few pixels off the edge | A `pl-*`, `ml-*`, border or `-mx-*` on one block: remove it, or move it inside |\n| the first content sits 0px from the top | `pt-section` on the first section, or header padding |\n| sections run into each other | `py-section` on every section |\n| elements overlap | A fixed `w-*`\u002F`h-*` or negative margin at this width: `shrink-0` on the media, `flex-col` below `sm:` |\n| still invisible after scrolling | An `opacity-0` start that a class never removed: start visible, animate with `motion-safe:` and `starting:` (section 8) |\n| N font sizes \u002F weak h1 | Only the scale; `text-display` for the title, `text-2xl` for section heads |\n| body text is 13px | `text-base` (16px) for body copy, `text-sm` only for secondary text |\n| paragraph lines run long | `max-w-measure` on running text |\n| boxes side by side with different heights | `items-stretch` (the grid default), `flex flex-col` in each box, `mt-auto` on its action |\n| N different text colours | `text-ink`, `text-muted`, `text-accent` and state colours only |\n\n## 5. Variants: states, structure, containers\n\nStates on every control: `hover:`, `focus-visible:`, `disabled:`, and the\nARIA and data attributes the component already sets:\n\n```html\n\u003Cbutton class=\"inline-flex h-10 items-center gap-2 rounded-md bg-accent px-4 font-medium text-on-accent\n  hover:bg-accent\u002F90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent\n  disabled:cursor-not-allowed disabled:opacity-50 aria-busy:cursor-progress\">Save changes\u003C\u002Fbutton>\n\u003Cinput class=\"h-10 rounded-md border border-line bg-surface px-3 aria-invalid:border-danger user-invalid:border-danger\">\n```\n\n- Never `outline-none` without a `focus-visible` style. In v4 the old\n  `outline-none` is `outline-hidden` (it keeps an outline in forced-colours\n  mode); either needs the visible replacement.\n- `aria-expanded:`, `aria-selected:`, `data-[state=open]:` (or `data-open:`\n  for a present attribute) style the state the component exposes; no\n  second class to keep in sync. v3 writes `aria-[invalid=true]:`.\n- Parents and siblings: `group` with `group-hover:` or\n  `group-focus-within:`, `peer` with `peer-checked:`, `has-[input:checked]:`\n  for a parent that changes with its content, `not-last:` and `*:` for\n  children.\n- `motion-safe:` on animations and transform transitions;\n  `motion-reduce:transition-none` where a library adds its own\n  (`motion-stacks`).\n- Mobile-first: unprefixed for the phone, `sm:` `md:` `lg:` as it widens,\n  `max-md:` for below. Rearrange (`flex-col md:flex-row`, `order-*`), do not\n  duplicate markup behind `hidden md:block`.\n- Container queries for a component placed at different widths: `@container`\n  on the parent, `@md:grid-cols-2` on the child; named ones with\n  `@container\u002Fcard` and `@md\u002Fcard:`.\n- Page container: `mx-auto max-w-6xl px-4 sm:px-6`, text `max-w-prose`;\n  inside an app shell, no `mx-auto` (`ui-layout`).\n\n## 6. Recipes, with every state\n\n```html\n\u003C!-- Field: the label names it, the hint and the error describe it -->\n\u003Cdiv class=\"grid gap-1.5\">\n  \u003Clabel for=\"email\" class=\"text-sm font-medium text-ink\">Email\u003C\u002Flabel>\n  \u003Cinput id=\"email\" type=\"email\" aria-describedby=\"email-hint email-error\" aria-invalid=\"true\"\n    class=\"h-10 rounded-md border border-line bg-surface px-3 text-base text-ink placeholder:text-muted\n           focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-accent\n           aria-invalid:border-danger disabled:bg-surface-2 disabled:text-muted\">\n  \u003Cp id=\"email-hint\" class=\"text-sm text-muted\">We send the receipt here.\u003C\u002Fp>\n  \u003Cp id=\"email-error\" class=\"text-sm text-danger\">Enter an address like name@example.com.\u003C\u002Fp>\n\u003C\u002Fdiv>\n\n\u003C!-- Card: the whole card is the link; the title is the name, the action sits at the bottom -->\n\u003Carticle class=\"group relative flex flex-col rounded-md border border-line bg-surface p-5\n                hover:border-ink\u002F30 has-focus-visible:outline-2 has-focus-visible:outline-accent\">\n  \u003Ch3 class=\"text-lg font-medium\">\u003Ca href=\"\u002Fwork\u002Fledger\" class=\"after:absolute after:inset-0 focus:outline-none\">Ledger\u003C\u002Fa>\u003C\u002Fh3>\n  \u003Cp class=\"mt-2 text-muted\">…\u003C\u002Fp>\n  \u003Cspan class=\"mt-auto pt-4 text-sm text-accent group-hover:underline\">Read the case\u003C\u002Fspan>\n\u003C\u002Farticle>\n\n\u003C!-- Navigation: the current page is marked for everyone, not by colour alone -->\n\u003Ca href=\"\u002Fwork\" aria-current=\"page\"\n   class=\"rounded-sm px-2 py-1 text-muted hover:text-ink aria-[current=page]:font-medium\n          aria-[current=page]:text-ink aria-[current=page]:underline underline-offset-4\">Work\u003C\u002Fa>\n\n\u003C!-- Table: scrolls in its own box on a phone; numbers right-aligned in tabular figures -->\n\u003Cdiv class=\"overflow-x-auto rounded-md border border-line\">\n  \u003Ctable class=\"w-full text-sm\">\n    \u003Cthead class=\"bg-surface-2 text-left text-muted\">\u003Ctr>\u003Cth scope=\"col\" class=\"px-4 py-2 font-medium\">Invoice\u003C\u002Fth>\n      \u003Cth scope=\"col\" class=\"px-4 py-2 text-right font-medium\">Amount\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\n    \u003Ctbody class=\"divide-y divide-line\">\u003Ctr class=\"hover:bg-surface-2\">\n      \u003Ctd class=\"px-4 py-3\">INV-0042\u003C\u002Ftd>\u003Ctd class=\"px-4 py-3 text-right tabular-nums\">Rp 1.250.000\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\n  \u003C\u002Ftable>\n\u003C\u002Fdiv>\n\n\u003C!-- Dialog: the native element, its backdrop, and an entry that respects reduced motion -->\n\u003Cdialog class=\"m-auto w-full max-w-md rounded-md bg-surface p-6 text-ink shadow-overlay backdrop:bg-ink\u002F40\n               opacity-100 transition-[opacity,translate] duration-200 starting:open:opacity-0\n               starting:open:translate-y-2 motion-reduce:transition-none\">…\u003C\u002Fdialog>\n```\n\n- Every control has hover, `focus-visible`, disabled, and the ARIA state it\n  exposes; a busy button says so (`aria-busy`, a spinner, its text kept).\n- Touch targets reach 44px on a phone: `h-11` or `min-h-11`, or padding on\n  an icon button (`p-2.5` around a 24px icon).\n- Buttons: one filled primary per view (`bg-accent text-on-accent`), the\n  rest outline (`border border-line`) or plain (`hover:bg-surface-2`). Their\n  variants live in one `cva` (section 7), not in copied strings.\n\n## 7. Components, cn and class order\n\n- A class group that repeats becomes a component (a React, Vue or Svelte\n  component, or a partial), not a copy and not `@apply` everywhere. Keep\n  `@apply` for base element styles and markup you do not control (HTML from\n  a CMS).\n- Variants with `cva` (class-variance-authority) or a small map keyed by\n  variant (the Button in `ui-stack-react`); merge a caller's classes with\n  `cn()` (clsx and tailwind-merge) so a caller's `px-6` replaces the default\n  rather than both applying:\n\n```ts\n\u002F\u002F lib\u002Fcn.ts\nimport { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n\u002F\u002F tailwind-merge knows only the default theme: name custom text sizes, or it\n\u002F\u002F reads text-display as a colour and drops it beside text-muted.\nconst twMerge = extendTailwindMerge({ extend: { theme: { text: [\"display\"] } } });\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n```\n\n- tailwind-merge 3 is for Tailwind 4 (2 for 3). The `cn` package (from the\n  shadcn team, September 2026) replaces clsx and tailwind-merge together:\n  `export { cn } from \"cn\"`, and `createCn` from `cn\u002Fconfig` takes the same\n  `extend` for custom tokens.\n- `@utility` for a custom utility that takes variants:\n  `@utility focus-ring { outline: 2px solid var(--color-accent);\n  outline-offset: 2px; }`, then `focus-visible:focus-ring`.\n- In a Vue or Svelte `\u003Cstyle>` block, `@reference \"..\u002Fapp.css\";` first, so\n  `@apply` and theme values resolve without emitting Tailwind twice.\n- Keep class order readable: layout, box, type, colour, state, responsive.\n  The Prettier plugin sorts them, including inside `cn()` and `cva()`:\n\n```json\n{\n  \"plugins\": [\"prettier-plugin-tailwindcss\"],\n  \"tailwindStylesheet\": \".\u002Fsrc\u002Fapp.css\",\n  \"tailwindFunctions\": [\"cn\", \"cva\", \"clsx\"]\n}\n```\n\n## 8. Motion\n\n```css\n@theme {\n  --animate-rise: rise 480ms var(--ease-out-soft) both;\n  @keyframes rise {\n    from { opacity: 0; translate: 0 12px; }\n  }\n}\n```\n\n- Animate `opacity` and `translate`\u002F`scale` only: `transition-[opacity,translate]`,\n  never `transition-all` (it animates layout and colours you did not mean).\n  Durations and easing from tokens (`duration-200 ease-out-soft`).\n- Entry without JavaScript: `starting:` (`@starting-style`) for elements that\n  appear (a dialog, a popover, an item added to a list); `motion-safe:` on\n  every animation, `motion-reduce:` to turn a library's off.\n- Content is visible in the markup; an animation only moves it into place.\n  A reveal that starts at `opacity-0` needs the class that ends it to be\n  certain to arrive, or the content stays hidden (`motion-reveal`).\n\n## 9. Values, prose\n\n- Use the scale. Arbitrary values (`w-[347px]`, `text-[#1f2937]`,\n  `mt-[13px]`) are the exception with a reason, not the way to match a\n  mock-up; a value needed twice is a token.\n- A CSS variable as a value: `w-(--sidebar-width)` in v4 (`w-[--sidebar-width]`\n  was v3). `size-10` for equal width and height; `min-h-dvh`, never\n  `h-screen` on a section.\n- Long text from Markdown or a CMS: `@plugin \"@tailwindcss\u002Ftypography\";`\n  and `prose` on the wrapper (65ch), styled from the tokens with element\n  modifiers (`prose-headings:font-display prose-a:text-accent`) and\n  `dark:prose-invert` or token overrides for the dark theme.\n\n## 10. The generated look\n\nGradient text (`bg-clip-text text-transparent`), purple-to-pink gradients,\n`rounded-full` on everything, `shadow-2xl` cards, `backdrop-blur` on\nseveral surfaces, `uppercase tracking-widest` eyebrows over every heading,\nslate or zinc greys with an indigo accent, `rounded-2xl shadow-xl` on every\ncard, `ring-1 ring-white\u002F10` glass panels on a dark gradient, `h-screen`\nheroes, `text-gray-400` body text that fails contrast. `ui_check` finds\nthem.\n\n## 11. From v3 to v4\n\n- `npx @tailwindcss\u002Fupgrade` (Node 20+) on a clean branch, then read the\n  diff: it moves the config into CSS, rewrites renamed classes and swaps\n  the build plugin.\n- Renamed: `shadow-sm` to `shadow-xs` and `shadow` to `shadow-sm` (the same\n  shift for `rounded`, `blur` and `drop-shadow`), `outline-none` to\n  `outline-hidden`, `ring` to `ring-3`, `bg-gradient-to-r` to\n  `bg-linear-to-r`, `flex-shrink-0` to `shrink-0`, `bg-opacity-50` to a\n  colour with `\u002F50`.\n- Changed defaults: `border` and `ring` are `currentColor` (give them a\n  colour), `ring` is 1px, placeholders are the text colour at half opacity,\n  buttons have `cursor: default`, and `hover:` applies only where the device\n  can hover.\n- `!` goes at the end (`bg-accent!`), stacked variants read left to right,\n  and JS plugins load with `@plugin`; a JS config that must stay loads with\n  `@config \".\u002Ftailwind.config.js\"`.\n- In v3 itself: `theme.extend` in `tailwind.config`, pointing at CSS\n  variables (`colors: { ink: \"var(--ink)\" }`), so a dark theme swaps the\n  variables, not every class.\n\n## Check it\n\n- `preview` at 360, 768 and 1440px: its `layout` lines clear (section 4),\n  both themes.\n- `ui_check` for the generated look and raw values; `grep -rnE\n  \"\\[#[0-9a-fA-F]|-\\[[0-9.]+px\\]\" src` for arbitrary colours and sizes.\n- A misspelt class fails silently (it generates nothing): look at the\n  rendered page with the `preview` tool, in both themes.\n- Prettier on changed files, so class order stays sorted and diffs small.\n\n## Avoid\n\nThe default palette in components (`bg-purple-600`, `text-gray-500`);\narbitrary values where a token exists; `@apply` to rebuild components in\nCSS; `outline-none` with no focus style; `dark:` on every element instead of\ntokens; `hidden md:block` duplicates of the same markup; `cn` without a\nmerge that knows the custom tokens; `h-screen` sections; the generated look\nabove; sections with their own width or padding instead of `page`; text\nsizes outside the scale; `transition-all`; content that starts hidden.\n",{"name":5,"description":7},"main","38293fe1b096e6bf3646c7344c38786ed688f464","crates\u002Fenowx-core\u002Fskills\u002Fui-stack-tailwind",21,"https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli","https:\u002F\u002Fenowx.ai","https:\u002F\u002Fgithub.com\u002Fenowdev","Pretending to be a Developer","2026-10-06 02:05:35",{"source":8,"skillName":5,"commands":54,"prompt":64,"zipAvailable":65},[55,58,61],{"tool":56,"command":57},"npx","npx skills add enowdev\u002Fenowxcli --skill ui-stack-tailwind",{"tool":59,"command":60},"bunx","bunx skills add enowdev\u002Fenowxcli --skill ui-stack-tailwind",{"tool":62,"command":63},"pnpm","pnpm dlx skills add enowdev\u002Fenowxcli --skill ui-stack-tailwind","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-stack-tailwind\u002FSKILL.md",true,[],[68,84,109,123,150,171],{"skillNo":69,"name":70,"nameZh":6,"description":71,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":72,"categories":73,"keywords":79,"publishedAt":39},"01M6D2HQYSQD9N7MTNDT4A7CBM","demo-skill","Project-scope demo skill for tests.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fworkspace\u002F.agents\u002Fskills\u002Fdemo-skill\u002FSKILL.md",[74],{"categoryNo":75,"categoryName":76,"taxonomyType":77,"taxonomyTypeDesc":78},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[80,81,82,25,83],"demo skill","demo","tests","read",{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":88,"categories":89,"keywords":98,"publishedAt":39},"01M6D2HM0GCPM0TM2HPG9HHR2R","writing","Writing words people read: the specific thing instead of claims, words to drop, sentences, interface copy (buttons, labels, errors, empty states, confirmations), page copy, notifications and emails, alt text and link text, numbers and dates, words that translate well, documentation and voice. Read before writing a page's copy, an interface's text or a document.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fwriting\u002FSKILL.md",[90,93],{"categoryNo":91,"categoryName":92,"taxonomyType":77,"taxonomyTypeDesc":78},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":94,"categoryName":95,"taxonomyType":96,"taxonomyTypeDesc":97},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[86,99,100,101,102,103,83,104,105,106,107,108],"text","emails","empty","drop","document","page","voice","notifications","documentation","translate",{"skillNo":110,"name":20,"nameZh":6,"description":111,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":112,"categories":113,"keywords":117,"publishedAt":39},"01M6D2HG07YS49NM196Q0R3AZ8","Designing or restyling an interface: direction, layout, type, colour, icons, components, states, responsive behaviour. Read before building a page, a screen or a set of components.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui\u002FSKILL.md",[114],{"categoryNo":115,"categoryName":116,"taxonomyType":96,"taxonomyTypeDesc":97},"01JBSEEDCAT000000000000032","设计师",[20,118,119,120,121,104,24,83,122],"icons","components","SET","responsive","before building",{"skillNo":124,"name":125,"nameZh":6,"description":126,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":127,"categories":128,"keywords":135,"publishedAt":39},"01M6D2HB59HZHWG1RYZDV4SJ91","ui-themes","Light and dark themes, and colour that holds in both: tokens with a value per theme, and what each kind of content needs to stay visible (icons, SVG drawings and line art, logos, photos, screenshots, charts, code, maps, borders, shadows, focus, form controls), switching without a flash, and checking both. Read before building a second theme or a theme toggle, and before any drawing, logo or chart on a page that has two themes.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-themes\u002FSKILL.md",[129,132],{"categoryNo":130,"categoryName":131,"taxonomyType":77,"taxonomyTypeDesc":78},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":133,"categoryName":134,"taxonomyType":96,"taxonomyTypeDesc":97},"01JBSEEDCAT000000000000030","开发工程师",[20,136,137,138,139,140,141,142,143,83,122,144,104,145,146,147,118,148,149],"ui themes","switching","photos","tokens","drawing","focus","maps","Art","flash","code","Charts","content","screenshots","logo",{"skillNo":151,"name":152,"nameZh":6,"description":153,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":154,"categories":155,"keywords":159,"publishedAt":39},"01M6D2H5R5KRAE524C8QX0T7Q0","ui-structure","Structure before styling: the blueprint written before any interface code (the screen's job, the regions its kind of page needs, their order and why, the layout skeleton in numbers, the components and their states), the anatomy and order of a homepage, a dashboard, a list, a detail page, settings, a form, sign-in, pricing, docs, an article and a checkout, and how to turn the blueprint into code (tokens, layout primitives, components, sections, page). Read first, before building or rebuilding any page or screen.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-structure\u002FSKILL.md",[156,157,158],{"categoryNo":91,"categoryName":92,"taxonomyType":77,"taxonomyTypeDesc":78},{"categoryNo":133,"categoryName":134,"taxonomyType":96,"taxonomyTypeDesc":97},{"categoryNo":94,"categoryName":95,"taxonomyType":96,"taxonomyTypeDesc":97},[160,20,161,119,162,163,145,164,165,166,24,139,167,168,169,83,122,104,170,35],"structure","ui structure","pricing","dashboard","Blueprint","article","anatomy","styling","primitives","list","why",{"skillNo":172,"name":173,"nameZh":6,"description":174,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":175,"categories":176,"keywords":184,"publishedAt":39},"01M6D2H0ZZBJG77MBGPZHA6MWD","ui-stack-vue","Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-stack-vue\u002FSKILL.md",[177,180,183],{"categoryNo":178,"categoryName":179,"taxonomyType":77,"taxonomyTypeDesc":78},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":181,"categoryName":182,"taxonomyType":77,"taxonomyTypeDesc":78},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":115,"categoryName":116,"taxonomyType":96,"taxonomyTypeDesc":97},[20,185,22,186,187,188,189,190,24,25,191,192,83,193,194,195,196,197,198,199],"vue","ui stack vue","forms","pages","watch","runtime","transitions","fetch","pinia","nuxt","template","server","config","directory","testing"]