# Build a profile and settings screen with the Sorbet theme

You are building one page in an existing project: the profile screen of a habit-tracking app, with name and email that are checked and saved, reminders as labelled switches, a reminder time and a way to delete the account after typing a word. 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. The person is Ines Okafor, ines.okafor@example.com, on the free plan since 2 September 2026. Her reminders: a morning nudge at 08:00 (on), an evening check-in (on), a weekly summary on Sundays (off), a note when a streak is at risk (on).

The `<main>` uses `gap-10` between groups.

Build these parts in order.

### 1. Header

The header from the layout notes, with Profile as the current page.

### 2. Page header

The page header from the layout notes, with the small figure `With Mossy since 2 September 2026` (the date a `<time dateTime="2026-09-02">`) above the `<h1>` "Your profile".

### 3. Settings

A grid from the layout notes. Its first item, `md:col-span-7`, is a `flex flex-col gap-10` holding the parts "You" and "Reminders"; its second, `md:col-span-5`, holds "Your plan" and "Delete your account" in the same way. Below `md` they stack in that order. Each part is a card that is a section, with its `<h2>` first in the card's `gap-4` (in "You", the row that holds the avatar and the `<h2>` comes first).

**You.** The card opens with a row, `flex items-center gap-3`: an avatar with the initials `IO` and the `<h2>` "You". The form has `noValidate` and `flex flex-col gap-6`:

- "Name": an `Input` (add it with `npx shadcn@latest add input`) with `autoComplete="name"`.
- "Email": an `Input` with `type="email"` and `autoComplete="email"`, with helper text `We only write to you about your account.`
- A row, `flex items-center` with no gap of its own and no wrap (at 375px both fit on one line, and `ml-3` never indents a second line): the page's one default `Button`, `type="submit"`, "Save changes", and beside it the `role="status"` element for the confirmation, empty at first. The confirmation line inside it carries `ml-3`, so the empty element adds no space.

**Reminders.** The `<h2>` "Reminders" and, in the card's `gap-4`, one sentence: "Changes here save as you make them." Then four rows in a `flex flex-col`, each row `flex min-h-11 items-center justify-between gap-4 py-3`, every row after the first with `border-t border-border`:

- At the left, a `flex min-w-0 flex-col gap-2`: the title, a `<p>` with an `id` in `text-body font-medium` (add `font-sans`), and under it a small figure with an `id` that says what the reminder does. For example: Morning nudge / `A reminder to start your habits`; Evening check-in / `At 20:00, if something is left`; Weekly summary / `Sundays at 18:00`; Streak at risk / `When your streak could end tonight`.
- At the right, the shadcn `Switch` (add it with `npx shadcn@latest add switch`), tied to the title with `aria-labelledby` and to the small figure with `aria-describedby`.

The stock `Switch` is a pill, which suits this theme, but it is 18px tall and its touch area 34px. In `components/ui/switch.tsx`, replace `data-[size=default]:h-[18.4px] data-[size=default]:w-[32px]` with `data-[size=default]:h-6 data-[size=default]:w-10`, the thumb's `group-data-[size=default]/switch:size-4` with `group-data-[size=default]/switch:size-5`, and `after:-inset-y-2` with `after:-inset-y-3`, so the touch area reaches 48px. The `sm` size's classes and the thumb's `translate-x-[calc(100%-2px)]` stay: at the new sizes the thumb still ends at the track's inner edge, as it ships. On, its track is raspberry, as it ships. Off, its stock track is the border color, which is too faint against a white card: replace `data-unchecked:bg-input dark:data-unchecked:bg-input/80` on the track with `data-unchecked:bg-muted-foreground`. On the thumb, take out `dark:data-unchecked:bg-foreground`, so an off thumb stays `bg-background` in both modes: the stock light thumb on the new track is under 2:1 in dark mode, the background color over 5:1 in both. The thumb's other stock colors stay, and its place says which state the switch is in.

Under the rows, the reminder time: a field from the layout notes, "Morning reminder time", a `<select>` in its own `<div>` with `w-full md:w-40` inside the field's stack (the label and helper text keep the card's width), with the times 06:00 to 11:00 every half hour, 08:00 chosen, and the helper text `When Mossy sends your morning nudge`. It is disabled while "Morning nudge" is off (add `disabled:opacity-50` to the `<select>`), and the helper text then reads `Turn on Morning nudge to choose a time.`

**Your plan.** The `<h2>` "Your plan", then the key figure's row from the layout notes (`flex flex-wrap items-baseline gap-2`): `Free` as the smaller key figure, `font-heading text-h1`, with the small figure `5 of 5 habits in use` beside it as its unit. Then one sentence, "You are using all five habits on Free.", and a text link "See plans" to `/plans` with `self-start`, so the link is as wide as its words. No button.

**Delete your account.** The `<h2>` "Delete your account". Under it, in the card's `gap-4`, one `<div>` with no gap is the wrapper for the delete form and its status element. It holds a `flex flex-col gap-4` and, after it, the `role="status"` element, empty at first. The stack holds: one paragraph in `text-body` that says what is lost, in words: "This deletes your habits, your streak and all your history. It cannot be undone."; a field from the layout notes, "Type delete to confirm", an `Input` with `autoComplete="off"`; and the button "Delete my account": `variant="destructive"` with the `Trash2` icon before its text, `focusableWhenDisabled`, full width below `md` (`w-full md:w-auto md:self-start`). Add `hover:text-foreground` to it: the stock hover tint leaves too little contrast for the red text.

### 4. Footer

The footer from the layout notes.

### Behavior

- "Save changes" checks the fields: an empty name shows "Add your name, so Mossy knows what to call you."; a malformed email shows "Check your email: it needs an @ and a domain." Each error sits under its field, and focus moves to the first field with an error. Nothing is saved while an error shows.
- A good save stores the values in component state and shows the confirmation line from the layout notes with the word "Saved" in the status element. It goes away after four seconds. The avatar's initials follow the saved name.
- Each switch keeps its value in state and changes at once. Turning "Morning nudge" off disables the time `<select>` and swaps its helper text; turning it on enables it again with the time it had.
- "Delete my account" is disabled until the field holds `delete`, ignoring case and spaces around it. Clicking it removes the stack and puts one `<p>` in the `role="status"` element: "This is where your account would be deleted." That sentence gets `tabIndex={-1}` and focus.
- The nav links and "See plans" go to their addresses and need no other 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.

- [ ] An empty name or a malformed email gets a message in words with an icon, tied to its field, and is not saved; a good save shows "Saved" with an icon in a `role="status"` element.
- [ ] Every switch has a visible label tied to it with `aria-labelledby`, announces whether it is on, and has a touch area at least 44px tall.
- [ ] The reminder time is a native `<select>` with a visible label.
- [ ] The "Delete my account" button stays disabled until the word `delete` is typed, keeps keyboard focus while disabled, and its warning is stated in words, not by the red color alone.
- [ ] Profile has one raspberry button, "Save changes".
- [ ] 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.
