# Build a welcome page with the Sorbet theme

You are building one page in an existing project: the public page of a habit-tracking app, with a friendly opening and one main button, three things the app does, a drawn preview of the app, a quote from a user and a sign-up form. Follow this brief closely. `DESIGN.md` decides the tokens: colors, type, spacing steps and corners. This brief decides the layout, its widths included, and which token goes where. If this brief names a color, a type size, a spacing step or a corner that `DESIGN.md` does not have, `DESIGN.md` wins.

## Before you start

1. **Check the theme is installed.** The project should have `DESIGN.md` in its root and the Sorbet variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/sorbet.json` and save https://strictthemes.com/themes/sorbet/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/sorbet/tailwind.css instead. If the global CSS still imports a font the theme does not use (a new shadcn project imports Geist), remove that import, and uninstall its package if nothing else uses it. Keep every font import the theme's install added, also one for a style no page uses yet.
2. **Read `DESIGN.md` from start to finish.** It is the source of truth for every visual decision.
3. **Look at the project before writing code.** Use its framework, router and file layout. For controls (buttons, inputs, tabs, menus, dialogs, accordions, data tables) use the components in `components/ui`, and add a missing one with `npx shadcn@latest add <name>` rather than writing your own, unless this brief names a plain element. Layout is plain elements with theme classes: sections, panels, figures and lists need no component.
4. **Teach `cn()` the theme's type classes.** shadcn's class merger does not know `text-display` or `text-h1`. It takes them for colors, so `cn("text-label text-muted-foreground")` silently drops one of the two, and a type class passed to a stock component does not replace its size. In a new shadcn project, `lib/utils.ts` only passes `cn` on from the `cn` package. Unless the file already holds the lines below, change it to:

   ```ts
   import { createCn } from "cn/config"

   // The theme's type classes are font sizes, not colors.
   export const cn = createCn({
     extend: {
       classGroups: {
         "font-size": [{ text: ["display", "h1", "h2", "body", "label"] }],
       },
     },
   })
   ```

   Then make every file in `components/ui` use it: where one has `import { cn } from "cn"`, change that line to import `cn` from the project's utils file (`@/lib/utils` in a default setup). Do the same for each component you add later. If the project uses `tailwind-merge` instead of the `cn` package, its components already import from the utils file: pass the same object to `extendTailwindMerge` and keep `clsx`.
5. **Write real content.** If the user gave you details about their product, use them. Otherwise use the sample product in this brief and write the rest of the copy yourself: specific and plain, the way the samples read. Invent the company and people names; do not use real ones. Never use lorem ipsum. A link with no destination yet points to `#`. Set the page's `<title>` and meta description.

## The theme in short

- **Fonts:** Gabarito for headings, Figtree for body text, Sometype Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 56px/800, `text-h1` 36px/700, `text-h2` 24px/700, `text-body` 17px/400, `text-label` 13px/500.
- **Font per size:** a type class does not set the font family. Headings (`<h1>` to `<h6>`) get the heading font on their own; on any other element add the font class: `font-heading` with `text-display`, `text-h1`, `text-h2`; `font-mono` with `text-label`.
- **Spacing scale**, with the Tailwind step in brackets: 4px (`1`), 8px (`2`), 12px (`3`), 16px (`4`), 24px (`6`), 40px (`10`), 64px (`16`).
- **Corners:** `rounded-sm` 10px, `rounded-md` 20px, `rounded-xl` 28px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 20px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #AD1457, on-primary #FFFFFF, secondary #2B1721, on-secondary #FFF6F1, tertiary #FFD23F, on-tertiary #2B1721, neutral #735E68, background #FFF6F1, surface #FFFFFF, foreground #2B1721, border #F0D9D2, error #B42318, success #1C7C54.

### How Sorbet pages are laid out

- **Phone first:** build every part for a 375px phone first, then widen it from `md` up. At 375px every control is at least 44px tall and nothing sits side by side that this brief does not put side by side.
- **Page:** one `<div>` wraps the whole page with `min-h-screen bg-background text-body text-foreground`. Inside it sit the `<header>`, a `<main>` and the `<footer>`.
- **Container:** the header's row, the main content and the footer's row each sit in a centered container: `mx-auto max-w-[1168px] px-4`, with `px-6` from `md` up, so the content is at most 1120px wide, as `DESIGN.md` gives the grid. This width is the one arbitrary value allowed.
- **Grid:** where this brief puts parts side by side, their wrapper is `grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-x-6`, and each item gets the `md:col-span-*` this brief gives it. A row of cards of the same kind (key figures, plans, features) is `grid grid-cols-1 gap-6 md:grid-cols-12`: `gap-6` in both directions, with no `md:gap-x-6`. Items in one row stretch to the same height unless this brief says otherwise.
- **Vertical rhythm:** written as gaps on stacks, never as top margins and never as `space-y-*`. Every element that holds more than one thing in a column is `flex flex-col` with one of these gaps: `gap-2` between a label and what it labels, between a field's label, the field, its helper text and its error, and between a heading and a label or small figure under it (inside a card too: a label and its line get their own `flex flex-col gap-2`, and the card's `gap-4` sits between such groups); `gap-3` between the cards of one list; `gap-4` between a heading and its paragraph (one sentence in `text-body` counts as a paragraph), and between the parts inside a card; `gap-6` between the fields of a form, between a part's heading and its content, and between cards of the same kind; `gap-10` between groups on an app screen; `gap-16` between sections on the Welcome and Plans pages. The brief says which of the last two the `<main>` uses. A row of things side by side is `flex flex-wrap items-center` with `gap-3`, or `gap-2` for things that belong together (an icon and its word). Where this brief gives no gap, use the one that fits.
- **Main:** the `<main>` element itself carries the container classes, `flex flex-col`, the gap the brief names, and `pt-10 pb-16`.
- **Header:** the first thing on the page is a `<header>` with `border-b border-border`, running the full width. Inside it, one `<div>` carries both the container classes and the row's classes, `flex flex-wrap items-center gap-x-6 gap-y-2 py-3`: the app's name on the left, a link in `inline-flex h-11 items-center font-heading text-h2 text-foreground`; then the `<nav>`, the page's only one, with the `aria-label` "Main" and `w-full md:w-auto`. From `md` up the nav sits right after the name, `gap-x-6` from it, not pushed to the row's right end. The `<nav>` holds four links as a `<ul>` with `flex gap-1`, in this order: "Today" to `/today`, "Progress" to `/progress`, "Plans" to `/plans`, "Profile" to `/profile`. Welcome is not among them; the brief says which link is the current page. Each link is `inline-flex h-11 items-center rounded-lg px-3 font-mono text-label`: the current page's link has `aria-current="page"` and `bg-secondary text-secondary-foreground`; the others are `text-foreground hover:underline underline-offset-4`. Below `md` the nav takes a line of its own under the name; the four links fit on that line at 375px, so there is no "Menu" button. Anything at the row's right end has `ml-auto`.
- **Footer:** a `<footer>` with `border-t border-border`, running the full width. Inside it, one `<div>` carries both the container classes and the row's classes, `flex flex-wrap items-center justify-between gap-3 py-10`: the app's name in `font-heading text-h2`, and a line in `font-mono text-label text-muted-foreground` with the copyright year as plain text: `Mossy · Made for small, daily steps · 2026`.
- **Page header:** on an app screen the `<main>` opens with a `flex flex-col gap-2`: a small figure above (the date, or a short line this brief gives), then the `<h1>` in `text-h1`. The `<h1>` is `text-display` only where this brief says so, and then only from `md` up (`text-h1 md:text-display`).
- **Headings:** an `<h2>` is `text-h2`. An `<h3>` is `text-h2` too, unless this brief gives it another size. Headings get the heading font on their own; any other element in `text-display`, `text-h1` or `text-h2` adds `font-heading`. Sentence case, no full stop.
- **Sections:** where this brief calls something a section, it is a `<section>` with `aria-labelledby` pointing at its heading, `flex flex-col gap-6`, with the heading (or a heading row) first and the content after it. Without a visible heading it gets an `aria-label`, and where it then holds one element (a grid, a card, a quote) it has no `flex flex-col gap-6`. Where a card is the section, the card carries `<section>`, and its own `gap-4` replaces the `gap-6`. The header, the page header and the footer are not sections.
- **Labels:** field labels, nav links, button and badge text, captions and the names of small groups are labels: `font-mono text-label`, in sentence case. Nothing is uppercased, in the text or with a class. A label is a short fragment. Helper text and error lines use the label style too and may be one short sentence; any longer text is body text.
- **Small figures:** a count, a time, a date, a percentage or a price note that this brief calls a small figure is `font-mono text-label tabular-nums text-muted-foreground`. The classes go on the element that holds the whole figure: a `<time>` that is the whole figure carries them itself; a `<time>` inside a longer figure (`Every day · Done at 08:12`) inherits them from its `<p>`.
- **Key figures:** the one number a card is about (a streak, a count, a price) is `font-heading text-display tabular-nums`, with its unit beside it as a small figure in a row with `flex flex-wrap items-baseline gap-2`. Where this brief asks for a smaller one, it is `font-heading text-h1 tabular-nums`.
- **Text color:** headings, body text, key figures and the names of things are `text-foreground`. Labels, small figures and helper text are `text-muted-foreground`. Paragraphs are never muted.
- **Writing:** address the reader as "you", in short sentences. An error says what happened and what to do.
- **Measure:** a paragraph of body text is at most `max-w-lg` wide.
- **Addresses:** the pages live at `/` (Welcome), `/today`, `/progress`, `/plans` and `/profile`. The app's name in the header links to `/` on Welcome and to `/today` on the other pages; each nav link points to its page's address, the current page's own included. A link whose destination this brief does not name points to `#`.
- **Dates:** "today" is the fixed sample date, Wednesday, 11 November 2026; only a time someone records (a habit marked done) and the greeting on Today come from the clock. A period such as "the last 7 days" includes today. A date is written day first with the month in full, `11 November`; the weekday goes before it (`Wednesday, 11 November`) and the year after it (`2 September 2026`) only where the brief shows them. Only a chart's axis labels shorten (`Thu`, `4 Nov`). A clock time is 24-hour, `08:12`. A date or time the brief marks as a `<time>` gets a `dateTime` (`2026-11-11`, `08:12`); one inside an `aria-hidden` drawing is plain text.
- **Links:** a text link is `text-primary underline underline-offset-4 hover:text-foreground` with `transition-colors duration-150`, and inherits its size.
- **Buttons:** the shadcn `Button` with `className="h-11 px-6 font-mono text-label"`. The stock `Button` is 32px tall and touch targets are 44px: every button and every link styled as one gets `h-11`. Button text is sentence case, two or three words; a choice in a button group may be one. The default variant (raspberry) is for the one main action of a view; no view shows two. Other actions are `variant="secondary"`, or `variant="outline"` for quiet ones. In `components/ui/button.tsx`, once: in the `secondary` variant, replace `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]` with `hover:bg-secondary/80` (the stock hover cannot be told from the resting color); in the `outline` variant, replace `bg-background` with `bg-card` and take out `dark:bg-input/30`: the card's white in both modes, its hover classes unchanged. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A button that is disabled until something is typed or that its own click disables gets `focusableWhenDisabled`, so that keyboard focus stays on it; in this app that is only "Delete my account" on Profile. A button whose click moves focus somewhere else does not need it. With it, Base UI sets `aria-disabled="true"` and `data-disabled` in place of the `disabled` attribute, so the stock `disabled:` classes do nothing: on a page that uses the prop, and only there, add `data-disabled:pointer-events-none data-disabled:opacity-50` beside them in `components/ui/button.tsx`. A call to action that goes to another page or jumps within this one is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "h-11 px-6 font-mono text-label")`, because `buttonVariants` alone does not merge classes. An icon inside a button sits before the text.
- **Button groups:** a set of buttons where one is chosen (a period, a billing switch) is a `role="group"` with `flex flex-wrap gap-2` and an `aria-label`. The chosen button has `aria-pressed="true"` and `variant="secondary"`; the rest have `aria-pressed="false"` and `variant="outline"`. Not `Tabs`: there are no tab panels.
- **Inputs:** the shadcn `Input` with `className="h-11 px-4 text-body md:text-body"`. Fields only ever sit in cards, and a field and a select look the same there: in `components/ui/input.tsx`, take out the stock `dark:bg-input/30` (that class only: `bg-transparent`, `file:bg-transparent`, `disabled:bg-input/50` and `dark:disabled:bg-input/80` stay), so the input is transparent on the card in both modes, as the select's `bg-card` is. Every field has a visible `<label>` above it with `block`, as a label in `text-muted-foreground`, tied to the field with `htmlFor`. The label, the field, helper text and the error sit in a `flex flex-col gap-2`. Helper text is a small figure, and the field names it in `aria-describedby`.
- **Selects:** a choice from a short list is a native `<select>` with the classes `h-11 w-full rounded-lg border border-input bg-card px-4 text-body text-foreground` and the focus outline from the rules (`focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`). A width the brief gives goes on a `<div>` around the `<select>` alone, inside the field's stack. It gets the same label and wrapper as any field; with an error it also gets `aria-invalid` and `aria-invalid:border-destructive`, like the input.
- **Errors:** an error is a line under its field, inside the field's stack: `flex items-start gap-2 font-mono text-label text-destructive`, holding the `CircleAlert` icon (`size-4 shrink-0`, `aria-hidden="true"`) and one sentence that says what to do. The field gets `aria-invalid` and adds the line's `id` to its `aria-describedby`. Fields are checked in code on submit, with `noValidate` on the `<form>`; focus moves to the first field with an error. An email is well formed when, trimmed, it matches `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`. An error line is removed when its own field changes (on `input` or `change`); every other error stays until the next submit.
- **Confirmations:** a message that appears after an action goes into an element with `role="status"` that is on the page from the start, empty: one that arrives with its text is often not announced. It is never hidden (no `hidden` attribute, no `empty:hidden`), and it has no classes that draw or take space: the classes, the margin included, go on the message inside it, which is rendered only while there is a message. So that an empty one adds no gap, the status element is never an item of a `gap-*` stack or row; the brief says where it sits. A short confirmation is a line `flex w-fit items-center gap-2 font-mono text-label text-success` with the `CircleCheck` icon (`size-4`, `aria-hidden="true"`) and a word or two. Where a message replaces a form, the form and the status element share one wrapper; the message's first element gets `tabIndex={-1}` and focus moves to it, because the button that had focus is gone.
- **Cards:** a card is a plain element with `rounded-xl border border-border bg-card p-6` (28px corners, 24px padding, a 1px border), and `flex flex-col gap-4` inside unless this brief says otherwise. Not the shadcn `Card`, which ships its own padding and a ring. Never put a card inside a card, and never a bordered box inside a card: divide a card's parts with 1px `border-t border-border` lines and `pt-4`.
- **Badges:** the shadcn `Badge` with `className="h-auto gap-1 rounded-sm px-2 py-1 font-mono text-label"`. A good-news badge (a streak, all done, a saving) adds `border-transparent bg-tertiary text-tertiary-foreground`: butter yellow with dark text, in both modes, and for good news only. Any other badge is `variant="secondary"`. One to four words. An icon in a badge comes before the words with `aria-hidden="true"` and no size class: the stock `Badge` sets its icons to 12px. `bg-tertiary` is a fill only: never use butter yellow as a text color, a border or a chart color.
- **State:** never shown by color alone. `text-success` and `text-destructive` are only for a short message that also has an icon.
- **Meters:** a bar that shows a share (a habit's completion) is a track `h-6 w-full overflow-hidden rounded-sm border border-border bg-background` holding a fill `h-full bg-secondary`, whose width is set with an inline `style`. The track has `aria-hidden="true"`, which hides the fill too; the number beside it says the share.
- **Charts:** drawn with plain elements. Bars are `bg-secondary` with square corners, on 1px `border-border` grid lines; axis values are small figures, in a column as tall as the plot: the top value hangs under the top line (`absolute top-0`), the bottom value stands on the bottom line (`absolute bottom-0`). No primary, yellow or success fills in a chart. A chart's drawing has `aria-hidden="true"` and a text alternative.
- **Pictures:** no image files. A picture is drawn from plain elements in a card with `role="img"` and an `aria-label`.
- **Avatars:** a circle with the person's initials: `flex size-11 shrink-0 items-center justify-center rounded-full bg-secondary font-mono text-label text-secondary-foreground`, `aria-hidden="true"` beside the written name.
- **Icons:** from `lucide-react`, with `size-4` (`size-6` where this brief says so; none inside a `Badge`) and `strokeWidth={1.5}`. An icon beside a word has `aria-hidden="true"`. Beside a line of body text in an `items-start` row, the icon gets `mt-1` to sit on the first line. Never an emoji. Only where named.
- **Corners:** `rounded-sm` (10px) for badges and small tiles (the drawn check boxes in the preview, meters), never on a shape less than 20px tall; `rounded-lg` (20px, what buttons, inputs and selects ship with) for buttons, inputs, selects and nav links; `rounded-xl` (28px) for cards; `rounded-full` only for avatars and the stock `Switch`.
- **Surfaces:** flat: no shadows, blur or gradients in the code you write.
- **Motion:** in the code you write, the only transition is color. Nothing moves on scroll, and nothing bounces when a habit is done. Stock components keep the motion they ship with.

## The page

Where this section and the layout notes above differ, this section wins.

Sample content, if the user gave none: "Mossy", a habit tracker for small, daily steps. You pick up to five habits, tick them off each day and watch a streak grow. It is free for five habits; Mossy Plus is $4 a month. The person in the preview is Ines, on Wednesday, 11 November 2026, with five habits: Drink 8 glasses of water, Read 10 pages, Walk after lunch, Stretch for 5 minutes, No phone after 22:00. Write the copy yourself, warm and plain, with no superlatives.

The `<main>` uses `gap-16` between sections.

Build these parts in order.

### 1. Header

The header from the layout notes, without the app's nav: this page is for people who have no account yet. At the right end (`ml-auto`), a text link "Sign in" to `#`, as a label (`font-mono text-label`) in the link colors from the notes; the `<a>` itself carries `inline-flex h-11 items-center`, so it is a 44px target.

### 2. Opening

A grid from the layout notes, `md:items-center`. Its first item, `md:col-span-6`, is a `flex flex-col gap-6`:

- A `flex flex-col gap-4` with the `<h1>` in `text-h1 md:text-display` ("Small steps, every day") and one paragraph under it: "Mossy helps you keep a few small habits. Tick each one off when it is done. It takes about ten seconds a day."
- A row (`flex flex-wrap items-center gap-3`) with the page's one main button: an `<a>` to `#signup` styled as a default button, "Start for free". Beside it, a small figure: `Free for 5 habits · No card needed`.

Its second item, `md:col-span-6`, is the preview (part 3). Below `md` it sits under the text.

### 3. Preview of the app

A `<figure>` with `flex w-full flex-col gap-2 md:mx-auto md:max-w-sm`: full width below `md`, at most 384px wide and centered in its column from `md` up. Inside it, a card with `role="img"` and the `aria-label` "The Mossy home screen: three of five habits done today, and a 12-day streak", as wide as the figure. Everything inside the card has `aria-hidden="true"`. The card is drawn from plain elements, top to bottom, in its own `gap-4`:

- A small figure, `Wednesday, 11 November`, then the greeting "Good morning, Ines" in `font-heading text-h2`, in a `flex flex-col gap-2`. Both are fixed text, not read from the clock.
- A row with a good-news badge (add it with `npx shadcn@latest add badge`), "12-day streak", with the `Flame` icon.
- Five habit rows, each a `flex items-center gap-3` with a 1px `border-t border-border pt-4` (`gap-4` from the card's stack holds them apart): a box `size-6 shrink-0 rounded-sm border border-border` that, for the three done habits (the first three in the sample's order), is `bg-secondary` with a `Check` icon (`text-secondary-foreground`) inside; then the habit's name in `text-body`; then at the right end (`ml-auto`) a small figure, "Done" or "To do". The rows are not cards and have no fill.

The `<figcaption>` under the card is a label in `text-muted-foreground`, `md:text-center`, so it is centered under the card from `md` up and left-aligned below: "What your mornings look like in Mossy".

### 4. What it does

A section with an `<h2>` "What Mossy does for you", then a row of three cards: a grid from the layout notes with `gap-6`, each card `md:col-span-4`. Each card, in its `gap-4`:

1. An icon, `size-6`, in `text-foreground`: `ListChecks`, `Bell`, `Flame`.
2. An `<h3>` in `text-h2`: "Tick off your day", "A nudge at the right time", "See your streak grow".
3. One or two sentences in `text-body`. For example: "Pick up to five habits. Each one is a single tap when it is done." / "Choose when Mossy reminds you. It never sends more than two a day." / "Every day you finish everything adds one to your streak. Miss a day, and you start again, kindly."

### 5. What people say

A section with an `aria-label` "What people say" and no visible heading, holding only a `<blockquote>`, `md:max-w-3xl`, with `flex flex-col gap-4`:

- The quote as a `<p>` in `font-heading text-h1`, with no quotation marks. Here, instead of the notes' `max-w-lg`, it has no width of its own and fills the blockquote's `md:max-w-3xl`. One or two sentences, e.g. "I wanted to read more for years. Ten pages a day did it, and the streak kept me honest."
- A `<footer>` inside the `<blockquote>`, a row with `gap-3`: an avatar with the initials, then a `flex flex-col gap-2` holding the name in `text-body text-foreground` ("Marta Lindqvist") and a small figure ("Nurse, 41 days in a row").

### 6. Sign-up

A section with the `id` `signup` and `scroll-mt-10`, its `aria-labelledby` pointing at the `<h2>` below. It holds only a grid from the layout notes, so it is a plain `<section>` with no `flex flex-col gap-6` of its own. Its first item, `md:col-span-5`, is a `flex flex-col gap-4` with the `<h2>` "Start with one habit" and one paragraph: "You can add more habits later. Free stays free, for up to five."

Its second item, `md:col-span-7`, is a card that is the wrapper for the form and for the status element that replaces it. In place of the card's column stack, this card is a plain block, so the empty status element after the form adds no gap. The form has `noValidate` and `flex flex-col gap-6`, with:

- "Your first name": an `Input` (add it with `npx shadcn@latest add input`) with `autoComplete="given-name"`.
- "Email": an `Input` with `type="email"` and `autoComplete="email"`, with helper text `We send one email to confirm, and nothing else.`
- A `<select>` "Your first habit", full width: the five habits in the sample's order, then "I will choose later". "Drink 8 glasses of water", the first, is chosen at the start.
- The default `Button` with `type="submit"`, "Create my account", full width below `md` (`w-full md:w-auto md:self-start`).

The sign-up button is the second raspberry button on the page; it is far below the opening and never on screen with it.

### 7. Footer

The footer from the layout notes.

### Behavior

- "Start for free" keeps `href="#signup"`, so it works without code. Its `onClick` calls `event.preventDefault()`, then `scrollIntoView()` on the sign-up section (no smooth scrolling), then focuses the first name field with `focus({ preventScroll: true })`. Once the form has been replaced, it focuses the confirmation's `<h3>` instead.
- On submit, check the fields: the name must not be empty ("Add your first name so we know what to call you."), the email must be well formed, as the layout notes define it ("Check your email: it needs an @ and a domain, like ines@example.com."). Show each error under its field and move focus to the first field with an error.
- A good form is replaced in the card by the confirmation in the `role="status"` element: an `<h3>` in `text-h2` with `tabIndex={-1}` that gets focus, "You are in, Ines" (with the name they typed), and one sentence: "We sent a link to ines@example.com. Open it on your phone and your first habit is waiting." Use the address they typed. Then a good-news badge, "Day 1". The three sit in one `flex flex-col items-start gap-4` inside the status element, rendered only once the form is sent.
- "Sign in" and the footer need no behavior.

## Rules

- **Colors:** only the theme's colors, through the semantic classes (`bg-background`, `text-foreground`, `bg-card`, `bg-primary`, `text-primary-foreground`, `text-muted-foreground`, `border-border` and so on). No hex, rgb or named colors in components, and no Tailwind palette classes such as `bg-blue-500` or `text-gray-600`.
- **Spacing:** padding, margin and gap use only the scale's steps (`1`, `2`, `3`, `4`, `6`, `10`, `16`) plus 0, 1px and 2px. No arbitrary values such as `p-[18px]`, and no other steps such as `p-5` or `gap-9`. `auto` margins are fine. Widths and heights such as `w-64` or `size-4` are sizes, not spacing. This applies to the code you write: a stock component keeps the spacing inside it, unless this brief changes it.
- **Type:** every piece of text you write gets its size from one of `text-display`, `text-h1`, `text-h2`, `text-body`, `text-label`, set on the element or on a parent it inherits from. Put `text-body` on the outermost element of the page you write. No other sizes. A stock shadcn component keeps the size it ships with, unless `DESIGN.md` or this brief gives that component its own style. A type class does not replace a weight class: where a stock component ships `font-medium` and the text should be regular, add `font-normal`.
- **Fonts:** only the theme's fonts, through `font-sans`, `font-heading` and `font-mono`. Do not add a font.
- **Corners:** only the `rounded-*` classes, which the theme already sets. No arbitrary radius, and `rounded-full` only where `DESIGN.md` or this brief names a full round, such as an avatar or a status dot. The theme allows a full round on a box up to 64px across; Strict Check reports a larger one, and a pill counts by its height.
- **Contrast:** all text meets WCAG AA against its background.
- **Stock components:** a component from `components/ui`, and an `<a>` styled with `buttonVariants`, keeps the hover, pressed and disabled colors and the motion it ships with, unless this brief changes them. They are the theme's colors under other names.
- **Counts:** a number with a noun reads right at one: "1 set", not "1 sets". Where this brief gives the words for only one of the two, write the other the same way.
- **Sizes from data:** a height or a width that comes from data (a chart bar, a meter) is set with an inline `style`. That is the one use for an inline style.
- **Focus:** every link and control shows a 2px outline on keyboard focus: `focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`. Keep `focus-visible:outline-solid` in it: in Tailwind 4 an `outline-none` on the same element, which stock components carry, otherwise hides the outline. An element that only gets focus from code (a heading or a message with `tabIndex={-1}`) gets the same outline written with `focus:` in place of `focus-visible:` (`focus:outline-solid focus:outline-2 focus:outline-offset-2 focus:outline-ring`): after a mouse click, a browser does not match `:focus-visible` on an element that code focuses. A link, a button or a field that code focuses keeps the `focus-visible:` classes it has: after a mouse click it shows no outline, which is how a browser treats every control, and is right. A stock component that ships its own focus ring (`Button`, `buttonVariants`, `Input`, `Textarea`, `Checkbox`, `Switch`, `AccordionTrigger`, the close button of a `Sheet`) must lose that ring. Do it once, in the component's file in `components/ui`, which is yours to change: replace its `focus-visible:ring-*` classes with the outline classes, and in a field replace its `aria-invalid:ring-*` classes, the `dark:` ones included, with `aria-invalid:ring-0`, or a field with an error shows a tinted ring. `Button` has two more sets. Its base classes also carry `aria-invalid:ring-*` classes: replace them with `aria-invalid:ring-0`, as in a field. Its `destructive` variant carries `focus-visible:ring-*` classes of its own, the `dark:` one included: delete them and put nothing in their place, because the base classes already give every variant the outline. Wherever this rule says replace, change the classes in the file and do not add `ring-0` beside them: an added class only wins where it comes later and passes through `cn`. The stock `focus-visible:border-ring` can stay. These ring changes go only into the files of components that this page uses and that can take focus on it: a `Badge` that is not a link keeps its ring classes. The `cn` import from the setup steps is another matter and goes into every file. A box that scrolls sideways (a code block, a wide table) gets `tabIndex={0}`, `role="region"`, an `aria-label` and the same outline, also at widths where it happens to fit. Where a focusable box touches the edges of a parent that has `overflow-hidden`, draw its outline inside it: `focus-visible:-outline-offset-2` in place of `focus-visible:outline-offset-2`.
- **Not allowed:** purple or multi-color gradients; emoji used as icons or bullets; glows and colored shadows.
- **Avoid:** a card inside a card.
- **Dark mode:** the page must work with the `dark` class on `<html>`. Because every color comes from a semantic class, this needs no extra code; do not write `dark:` color overrides. A new shadcn project follows the system's mode and switches on the `d` key: leave that as it is, and add no mode switch of your own. If the page has a native `<select>` or a date or time field, tell the browser the mode, or its list and its picker stay light in dark mode: in the global CSS, give `:root` `color-scheme: light` and `.dark` `color-scheme: dark`.

From the theme's own do's and don'ts:

- Do keep one main action per view, in raspberry.
- Do use yellow for good news and nothing else.
- Do write the way you would say it out loud.
- Don't use gradients or glows to look playful: the shapes and the color do that.
- Don't put a card inside a card.
- Don't introduce a color, size or radius that is not a token above.

## Check before you finish

Go through this list against the code you wrote, and fix anything that fails.

- [ ] The opening has exactly one raspberry button, a link that jumps to the sign-up form, and no other raspberry button is on screen with it.
- [ ] The preview of the app is drawn from plain elements, with `role="img"` and an `aria-label` that says what it shows, and no image file is loaded.
- [ ] An empty name or a malformed email gets a message in words with an icon, tied to its field with `aria-describedby`, and focus moves to the first field with an error.
- [ ] After a good sign-up, the form is replaced by a confirmation that a screen reader announces, and focus moves to it.
- [ ] At 375px the preview sits under the opening text, and the page does not scroll sideways.
- [ ] No color is written as a hex, rgb or Tailwind palette value in the page's code.
- [ ] Every padding, margin and gap in the code you wrote is a step of the spacing scale.
- [ ] Every text element you wrote gets its size from a type-scale class, its own or an inherited one. Visually hidden text is exempt.
- [ ] No file in `components/ui` imports `cn` from `"cn"` directly.
- [ ] Text contrast meets WCAG AA in light and dark mode.
- [ ] The page has one `<h1>`, headings are in order, and every image has alt text.
- [ ] Every interactive element can be reached with the keyboard and shows the focus outline from the rules.
- [ ] At 375px wide the page does not scroll sideways. Wide tables and code blocks scroll inside their own box.
- [ ] The project builds without errors or warnings.

Then tell the user which checks pass, which you could not verify, and anything in this brief you did not do and why.
