# Build a plans page with the Sorbet theme

You are building one page in an existing project: the plans page of a habit-tracking app, with a monthly and yearly switch, three plan cards with one recommended, what each includes in words and a short FAQ. 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, Ines Okafor, is on the free plan. Three plans: Free ($0, up to 5 habits, reminders, your streak with the week and month views, 30 days of history); Plus ($4 a month, or $36 a year, which is $3 a month; unlimited habits, all your history, the year view, a note on each day, and everything in Free); Together ($8 a month, or $72 a year, which is $6 a month; Plus for up to 4 people, shared habits you can tick off together, and everything in Plus). Plus is recommended. Prices are in US dollars, taxes included.

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

Build these parts in order.

### 1. Header

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

### 2. Opening

A `flex flex-col gap-4` holding the page header from the layout notes, with the small figure `You are on Free` above the `<h1>` "Pick what fits you", in `text-h1 md:text-display`; then, `gap-4` under it as a heading's paragraph, one paragraph in `text-body`: "Free covers most people. Plus is for when five habits are not enough."

### 3. Plans

A section with a visually hidden `<h2>` "Plans" (`sr-only`), then its content in the section's `gap-6`:

**The switch.** A row, `flex flex-wrap items-center gap-3`: a button group from the layout notes with the `aria-label` "Billing" and two buttons, "Monthly" and "Yearly", with Monthly chosen; then a `<div>` with `flex h-11 items-center` holding a small figure, `Yearly saves 3 months`. When Yearly is chosen the small figure is replaced, inside the same `<div>`, by a good-news badge with the same words, since it is now true for the prices on screen. The `<div>` is 44px tall either way, so nothing below moves when the switch is used, also at 375px, where it wraps under the buttons.

**The cards.** A row of three cards: a grid from the layout notes with `gap-6`, each card `md:col-span-4`, in the order Free, Plus, Together, stacked below `md` in the same order. Each card, in its `gap-4`, top to bottom:

1. A row, `flex flex-wrap items-center gap-3`: the plan's name as an `<h3>` in `text-h2`, and beside it a badge where the plan has one (add it with `npx shadcn@latest add badge`). Only Plus has one: a `secondary` badge "Recommended". Free has none; its last line says it is your plan.
2. One `flex flex-col gap-2`, a figure and the line about it, holding the price as a key figure, `$4` with `a month` beside it (Free shows `$0` with `a month`), and under it a small figure, the billing note: `Billed monthly`, or `Billed $36 once a year` on yearly. Free says `Free for as long as you like`.
3. One sentence in `text-body` saying who the plan is for: "For getting started with a few habits." / "For when five habits are not enough, or you want all your history." / "For a household that keeps habits together."
4. The features: a `<ul>` with `flex flex-col gap-2` and `border-t border-border pt-4`. Each item is a `flex items-start gap-2` in `text-body`: the `Check` icon (`mt-1 shrink-0`, `aria-hidden="true"`, `text-foreground`; `mt-1` sets it on the first line) and the feature in words. The items, exactly: Free: "Daily reminders", "Your streak, by week and month", then its limits with the `Minus` icon in place of the check: "Up to 5 habits", "30 days of history". Plus: "Unlimited habits", "All your history", "The year view", "A note on each day", "Everything in Free". Together: "Plus for up to 4 people", "Shared habits you tick off together", "Everything in Plus".
5. The call to action, last, with `mt-auto` so the three sit on one line from `md` up, full width (`w-full`):
   - Plus: an `<a href="#">` styled as a default button, "Try Plus free".
   - Together: an `<a href="#">` styled as a `secondary` button, "Choose Together".
   - Free: no button. In its place, a `<p>` `mt-auto flex h-11 items-center gap-2 font-mono text-label text-muted-foreground` (the `mt-auto` as on the buttons, no `w-full`), with the `CircleCheck` icon and "You are on this plan", so the three still line up.

Plus is marked only by its badge and its raspberry button. Do not enlarge it, lift it, give it a colored border or tint its fill.

### 4. Questions

A section with an `<h2>` "Questions people ask", `md:max-w-3xl`. Under the heading, an `Accordion` (add it with `npx shadcn@latest add accordion`) with five items and short, direct answers: Can I try Plus before paying? (14 days, no card), What happens to my habits if I go back to Free? (all kept; you choose which five stay active), Can I pay once a year? (yes, $36), How does Together work? (one person pays, up to 4 people join with a link), Can I cancel any time? (yes, in Profile, and you keep Plus to the end of the period).

The stock `Accordion` is off the type scale and its trigger is under 44px. In `components/ui/accordion.tsx`: on the trigger, replace `items-start` with `items-center`, its stock vertical padding with `py-3` and its small text size with `font-sans text-body`, and add `min-h-11 gap-4` (its `font-medium` stays); on the content, replace the small text size with `text-body`, and on its inner element the stock bottom padding with `pb-4` (`pt-0` stays). Every item has its 1px line under it, the last one too: on the item, replace the stock `not-last:border-b` with `border-b border-border`. Both chevron icons get `strokeWidth={1.5}`. The trigger keeps its stock `rounded-lg` corner, which only shows in its focus outline.

### 5. A last word

A card that is a section, `md:max-w-lg`: an `<h2>` "Not sure yet?" and one sentence: "Stay on Free. Nothing you add there is lost if you move to Plus later." No button: the plan row may still be on screen.

### 6. Footer

The footer from the layout notes.

### Behavior

- Keep the billing period in component state. The prices, the billing notes and the small figure or badge beside the switch all read from it. On yearly, Plus shows `$3` and Together `$6`, each with `a month`, and the note says what is billed once a year.
- The switch moves no focus; the pressed button changes with it.
- The plan links point to `#`; the nav links go to the addresses in the layout notes. Neither needs 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.

- [ ] Switching between monthly and yearly changes both paid plans' prices and billing notes, and the chosen button has `aria-pressed="true"`.
- [ ] Exactly one plan carries the "Recommended" badge, and its button is the only raspberry button among the plans.
- [ ] The plans' buttons sit on one line from `md` up, whatever the length of each plan's list.
- [ ] Every feature in a plan is written in words, and the free plan's limits are stated, not shown by a missing check.
- [ ] Each FAQ item opens and closes with the keyboard, and its trigger is at least 44px tall.
- [ ] 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.
