# Build a membership page with the Nocturne theme

You are building one page in an existing project: the membership page of an independent cinema, with three plans, a monthly and yearly switch, and a sign-up form that checks its fields and confirms the sign-up without taking a payment. 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 Nocturne variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/nocturne.json` and save https://strictthemes.com/themes/nocturne/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/nocturne/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:** Syne for headings, Onest for body text, Azeret Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 64px/800, `text-h1` 40px/700, `text-h2` 24px/700, `text-body` 16px/400, `text-label` 12px/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`), 16px (`4`), 24px (`6`), 48px (`12`), 96px (`24`).
- **Corners:** `rounded-sm` 3px, `rounded-md` 8px, `rounded-xl` 16px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 8px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #6B4A00, on-primary #FFF8E6, secondary #17131F, on-secondary #F2F0F5, tertiary #C9B6FF, on-tertiary #17131F, neutral #625B6E, background #F2F0F5, surface #FFFFFF, foreground #17131F, border #DAD5E2, error #B3261E.

### How Nocturne pages are laid out

- **Page:** one `<div>` wraps the whole page: `min-h-screen bg-background text-body text-foreground`. Inside it sit the `<header>`, the `<main>` holding everything between the two, and the `<footer>`.
- **Container:** `mx-auto max-w-[1280px] px-4 md:px-6`. This width is the one arbitrary value allowed. The `<main>` element is itself a container and also carries `pt-12`. The header and the footer run the full width of the window, with their lines; the container is a `<div>` inside each. Every other line stays inside the container.
- **Grid:** where this brief gives columns, the element is `grid grid-cols-1 md:grid-cols-12 md:gap-x-6` (24px gutters) and each item says its span, such as `md:col-span-7`. The grid sets columns only, never vertical space. Below `md` items stack in source order.
- **Vertical rhythm:** written as a top margin on the later element, never as `space-y-*` and never as a gap on a column of mixed content. A heading under a label has `mt-2`. A paragraph under a heading, and a paragraph under a paragraph, has `mt-4`. A block under text (a row of buttons, a list, a form, a figure, a table, a row of cards) has `mt-6`. A new group inside a section has `mt-12`. A `<section>` inside `<main>` has `mt-24`, except the first, which has none: the `pt-12` on `<main>` is the space above it. Where two grid items share a row from `md` up and stack below it, the second gets `mt-6 md:mt-0` when it follows on from the first (text under a still) and `mt-12 md:mt-0` when it is a separate group (a card, a form, a column with its own heading); each brief writes which. Gaps are only for items side by side in a row (`flex`, `flex-wrap`, a grid of cards), where this brief or these notes name one. Inside a list of rows, a row has its own top border and padding and no margin.
- **Headings:** headings are Syne, which `<h1>` to `<h6>` get on their own, in sentence case with no full stop. Titles wrap; never truncate one. The page's big title is `text-h1 md:text-display`, the only `text-display` text on the page. A section's `<h2>` is `text-h2 md:text-h1`. A film title in a row or a card is an `<h3>` in `text-h2`. Syne has no italic: never set a heading or a quote in italics.
- **Labels:** dates, days, times, running times, ratings, seat counts, screen names, eyebrows, captions, nav links, field labels, table headers and button and badge text are labels: `font-mono text-label uppercase`, showtime buttons included. Write their text in sentence case ("22:45 · Late"); the `uppercase` class sets the capitals. The class `text-label` already carries the wide tracking; add no `tracking-*` class. A label is a short fragment; helper text and errors are sentences in body text. Parts of a label line are joined with ` · `. A label on its own line is a block element such as a `<p>`.
- **Times and figures:** a showtime, a date, a price or a seat count that stands on its own (in a label, a button, a table cell, or a price set large) gets `tabular-nums`, so times line up in a column. A date or a time that stands on its own is a `<time>` with a `dateTime` (`2026-11-13`, or `2026-11-13T20:30` for a showtime). Times are 24-hour (`20:30`). In a label a date is short, `Fri 13 Nov` (`11 Nov` without the day, a year only where a brief writes one); in a sentence it is written in full, `Friday 13 November`, and with `2026` only where the brief's own example sentence writes the year. A year alone (a film's year) is plain text, not a `<time>`. A date range in a label (`11–15 Nov`, `20 Nov – 18 Dec`) is two `<time>` elements joined by an en dash in plain text, each with its full `dateTime`. A number or a date inside a sentence is plain text. "Today" is always the fixed sample date, Wednesday 11 November 2026, never the clock.
- **Credits:** the line of credits under a film title (director, country, year) is a `<p>` in `text-body text-muted-foreground`, with its parts joined by ` · `. Names in it are never uppercased. The running time and the rating go on a label line instead. Running text (a synopsis, a note on a film, an introduction, the sentences under a section heading) is never muted. Helper text is a short line apart from running text that a brief sets in `text-muted-foreground`, such as a field's hint or a demo note.
- **Text color:** headings, body text, field labels, nav links and table body cells are `text-foreground`. Other labels, credits, captions and helper text are `text-muted-foreground`. A button's text color comes from its variant.
- **Gold:** `text-primary` and `bg-primary` are bronze in light mode and gold in dark mode. Each screen has one gold thing, besides a link under the pointer and the focus outline (`outline-ring` is gold too, and stays): the page's one main action, a default `Button` or a link styled as one, as this brief names it. `DESIGN.md` allows one gold title in place of an action; these pages spend it on the action, so headings are never gold. Primary is never a fill, a border or a band.
- **Lavender:** `bg-tertiary` is for one badge on a page, always with `text-tertiary-foreground`. It is never a text color or a border.
- **Measure:** a paragraph of running text is at most `max-w-xl` wide (about 70 characters a line in Onest).
- **Links:** a text link is `text-foreground underline decoration-1 underline-offset-4 hover:text-primary`, so gold shows only under the pointer. A link that holds a title (inside a heading, and the cinema's name in the header) is never gold: `text-foreground decoration-1 underline-offset-4 hover:underline`. A nav link is a label in `text-foreground` with `hover:underline underline-offset-4`; the link for the current page has `aria-current="page"` and `underline`. Links get `transition-colors duration-150`.
- **Link destinations:** the five pages are `/` (Programme), `/film`, `/season`, `/membership` and `/visit`; where the project has another router layout, use its paths. A link whose destination this brief does not name points to `#`.
- **Header:** a `<header>` with `border-b border-border`. Its container `<div>` is also the row: `flex flex-wrap items-center justify-between gap-4 py-4`. On the left the cinema's name, a link to `/` in `font-heading text-h2`, never marked as the current page. On the right, a `<nav>` with the `aria-label` "Main" and `hidden md:block`, holding a `<ul>` with `flex gap-6` of four label links, in this order: Programme (`/`), Seasons (`/season`), Membership (`/membership`), Visit (`/visit`). The brief names the current one, or says that none is.
- **Menu on small screens:** below `md` the `<nav>` is hidden and a "Menu" button (`variant="outline"`, with the button classes below) stands at the right end of the row, with `aria-expanded` and `aria-controls`. It shows the links as a list inside the `<header>`, under its row: a second `<nav>` with the same `aria-label` and `md:hidden`, which carries the `id` that `aria-controls` names and has the `hidden` attribute while closed. It has a 1px top border across the full width and, inside the container, `py-2`; each link is a block with `py-2`. It closes on Escape, pressed while focus is on the Menu button or in the menu (one `onKeyDown` handler on the `<header>`, acting only while the menu is open), and focus then goes to the Menu button; it also closes when a link in it is chosen. Do not use a `Sheet` or a `Dialog`.
- **Footer:** a `<footer>` with `mt-24`, `border-t border-border` and `py-12`. Inside it, the container `<div>` holds a grid (`md:grid-cols-12`): columns 1–6 hold the cinema's name as plain text in `font-heading text-h2` and, with `mt-2`, the address as a `<p>` in `text-body text-muted-foreground` ("21 Tallow Row, Oldmere OM2 4LT"); columns 7–12 hold a `<ul>` (not a `<nav>`) of four text links (Programme, Seasons, Membership, Visit) in `text-body` with `flex flex-wrap gap-x-6 gap-y-2`, with `mt-6 md:mt-0`. Under the grid, with `mt-12`, a label line in `text-muted-foreground`: `© 2026 The Vesper`.
- **Buttons:** the shadcn `Button` with `className="h-auto px-6 py-2 font-mono text-label uppercase"`. It keeps its stock `rounded-lg` corner. The default variant (gold) is the screen's one main action. Every other action is `variant="secondary"`, or `variant="outline"` for quiet ones. In this theme the `secondary` variant's stock hover is the same color as its fill: in `components/ui/button.tsx`, replace its `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]` with `hover:bg-secondary/80`, once, and add no hover class where you use it. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A link that looks like a button (to another page, or to a part of this one) is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "…")`, because `buttonVariants` alone does not merge classes. Such a link is `inline-flex`, so a top margin on it works: it needs no wrapper.
- **Button groups:** a set of buttons where one is chosen (a day, a billing period, a showtime) is a `role="group"` with an `aria-label` and `flex flex-wrap gap-2`. The chosen button has `aria-pressed="true"` and `variant="secondary"`; the others have `aria-pressed="false"` and `variant="outline"`. Not `Tabs`: there are no tab panels.
- **Badges:** the shadcn `Badge` with `variant="secondary"` and `className="h-auto rounded-sm border-transparent bg-tertiary px-2 py-1 font-mono text-label uppercase text-tertiary-foreground"`: the variant is named only because its classes hold no gold, and these classes replace its fill and text color. One or two words, and at most one badge on a page.
- **Inputs:** the shadcn `Input` and `Textarea` with `className="h-auto px-4 py-2 text-body md:text-body"`. They keep their stock `rounded-lg` corner. So that fields and selects share one fill in both modes, edit `components/ui/input.tsx` and `components/ui/textarea.tsx`: replace the class `bg-transparent` and the class `dark:bg-input/30` with `bg-card`; `file:bg-transparent` and `dark:disabled:bg-input/80` stay. Every field has a visible `<label>` above it: `block font-mono text-label uppercase text-foreground`, tied to the field with `htmlFor`; the field has `mt-2`. Helper text under a field is a `<p>` in `text-body text-muted-foreground` with `mt-2`.
- **Selects:** a choice from a short list is a native `<select>` with `w-full rounded-lg border border-input bg-card px-4 py-2 text-body text-foreground`, under a `<label>` like any field, with the same focus outline. These selects always have a value, so they never show an error.
- **Errors:** an error is a line under its field with `mt-2`: `flex items-start gap-2 text-body text-destructive`, holding the `CircleAlert` icon (`shrink-0`, `mt-1`) and a sentence that says what to do, not uppercased. The field gets `aria-invalid` and points to the line with `aria-describedby`, next to any helper text it already names. Fields are checked in code when the form is sent, with `noValidate` on the `<form>`, and focus moves to the first field with an error. An error line is removed when its own field changes (on `change`); every other error stays until the next submit.
- **Confirmations:** a message that replaces a form goes into an element with `role="status"` that is on the page from the start, empty: The form and the status element share one wrapper, and the wrapper carries the top margin, so the message stands where the form stood. The status element itself has no classes and no margin, and is never hidden (no `hidden`, no `empty:hidden`). The message inside it is rendered only once there is one, and has no fill or border: its first element is an `<h3>` in `text-h2` with `tabIndex={-1}`, then the sentences follow with `mt-4`. Move focus to the `<h3>`. A link that follows a confirmation sits after the status element, in the shared wrapper, rendered only with the message, inside a `<p>` with `mt-6` (a margin on a plain inline text link does nothing).
- **Cards:** a card is a plain element with `rounded-xl border border-border bg-card p-6`. Inside it, a label comes first where it has one, and the rest follows the rhythm above. Never put a card inside a card, and do not use the shadcn `Card`.
- **Pictures:** there are no image files. A picture is a plain element with a 1px border and a fill, `border border-border bg-muted`, with `role="img"` and an `aria-label` that says what it shows. A poster is `aspect-2/3 rounded-xl`; a still is `aspect-video rounded-xl` (16:9). Show them whole, in those proportions: never crop one to another shape. A poster in a row is a thumbnail, `w-24 shrink-0 self-start` and `md:w-32` (`self-start`, or the row stretches it to the height of the text beside it and it loses its proportions), with the film's year inside it in its lower left: the element is `flex items-end p-2`, the year a `<span>` in `font-mono text-label text-muted-foreground`. A screen reader does not read it (`role="img"` hides what it holds), which is right: the credits line gives the year. A still is wide, in a `<figure>`, with a `<figcaption>` under it with `mt-2`: a label in `text-muted-foreground`, as `DESIGN.md`'s caption is, naming the film and what is in the frame in a short fragment (`Low Tide at Merrow · Maren on the harbour wall`). Pictures sit on the page, never in a card: in dark mode `bg-muted` and `bg-card` are nearly the same color.
- **Lists of rows:** a list of films or dates is an `<ol>` or `<ul>` with no markers and no padding. Each row has `border-t border-border py-6`, and the list has `border-b border-border` so the last row closes. A row with a poster is `flex gap-4 md:gap-6`: the poster, then a `min-w-0 flex-1` column holding the text.
- **Tables:** the shadcn `Table` with `text-body`. Edit `components/ui/table.tsx`: its scroll box, the `<div>` around the `<table>`, gets `tabIndex={0}`, `role="region"`, an `aria-label` from a new required `label` prop, and the focus outline. Header cells are labels in `text-muted-foreground` with `px-4`; body cells have `px-4 py-2`. Add `first:pl-0 last:pr-0` to every cell. Times in cells are `font-mono tabular-nums`. Rows are divided by 1px borders, with no zebra stripes. The stock `TableBody` drops its last row's border: take `[&_tr:last-child]:border-0` out of it. The rows do nothing, so take `hover:bg-muted/50` out of the stock `TableRow`. Header cells keep the stock `h-10` and, like all cells, `whitespace-nowrap`. The table's name is the heading above it and the `label`; it has no `<caption>`.
- **Block quotes:** a `<figure>` with a `<blockquote>` holding a `<p>` in `font-heading text-h2`, upright, with no quotation-mark graphic, and a `border-l-2 border-foreground pl-6` on the `<blockquote>`. Who said it is a `<figcaption>` with `mt-4`: a label in `text-muted-foreground`.
- **Icons:** from `lucide-react`, with `size-4`, `strokeWidth={1.5}` and `aria-hidden="true"`, and only where this brief or these notes name one. Never emoji.
- **Corners:** `rounded-sm` (3px) for badges, `rounded-lg` (8px, the stock corner) for buttons, inputs and selects, `rounded-xl` (16px) for cards, posters and stills. `rounded-full` is for avatars only, and these pages have none.
- **Surfaces:** flat: no shadow, blur or gradient in the code you write. A stock component that floats keeps its one short shadow.
- **Motion:** in the code you write, the only transition is color. Nothing moves on scroll.

## The page

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

Sample content, if the user gave none: **The Vesper**, an independent cinema with three screens and a late bar at 21 Tallow Row, Oldmere. Three plans: **Friend** (£6 a month, or £60 a year): 10% off every ticket, members' screenings once a month, the printed programme by post. **Regular** (£12 a month, or £120 a year), the recommended one: two free tickets a month, 20% off every other ticket, priority booking for seasons and premieres, 10% off at the bar. **Patron** (£25 a month, or £250 a year): everything in Regular, four free tickets a month, a named seat in Screen 2, and an invitation to the season launches. A yearly plan costs ten months. Membership starts on the day of sign-up, Wednesday 11 November 2026.

Build these parts in order.

### 1. Header

The header from the layout notes. "Membership" is the current page.

### 2. Introduction

The first `<section>`: a label "Membership"; with `mt-2`, the `<h1>` "Become a member of the Vesper" in `text-h1 md:text-display`, inside a `<div>` with `max-w-4xl`, so that it breaks onto three short lines from `md` up; with `mt-6`, one paragraph within `max-w-xl` on what membership pays for (the late shows, the 35mm projector, the seasons).

### 3. Plans

The second `<section>`, with `mt-24`. An `<h2>` "Plans" in `text-h2 md:text-h1`.

With `mt-6`, a row `flex flex-wrap items-center gap-4`: a button group with the `aria-label` "Billing period" holding "Monthly" and "Yearly", then a label in `text-muted-foreground`: `Yearly · two months free`. "Monthly" is chosen at first.

With `mt-6`, the three plans as cards from the layout notes, in a grid `grid grid-cols-1 gap-6 md:grid-cols-3`. Each card is an `<article>` holding, in order:

1. The plan's name as an `<h3>` in `text-h2`. On Regular, the badge "Recommended" (add the component with `npx shadcn@latest add badge`) stands on the same line, after the name: the `<h3>` and the badge sit in a row `flex flex-wrap items-center gap-2`, so the three names stay level and no card needs a spacer.
2. With `mt-4`, the price as a `<p>` in `font-heading text-h1 tabular-nums`, e.g. `£12`, followed in the same `<p>` by a `<span>` label in `text-muted-foreground`, `/ month` or `/ year`, with `ml-2`.
3. With `mt-2`, the billing note, a label in `text-muted-foreground`: `Billed monthly · cancel any time`, or for yearly `Billed yearly · £10 a month`, with the plan's own per-month figure to the penny (`£5 a month`, `£10 a month`, `£20.83 a month`).
4. With `mt-4`, one sentence on who the plan is for.
5. With `mt-6`, a `<ul>` of what the plan includes, in words, `list-disc pl-4`, with `mt-2` on every item but the first.
6. The plan's button, full width (`w-full`), in a `<div>` with `mt-auto pt-6`: "Choose Friend", "Choose Regular", "Choose Patron". Add `whitespace-normal text-center`: at 768px a card is about 174px wide inside, so a label may need two lines, and the stock button does not wrap. Regular's is `variant="secondary"`; the other two are `variant="outline"`. None is gold: the gold action on this page is the form's.

Each card is `flex flex-col`, so the button's `mt-auto` takes up the spare height and the three buttons line up at the bottom of the cards from `md` up, where the grid makes the cards one height. Do not enlarge, lift or tint the recommended plan.

Below `md` the cards stack in the order Friend, Regular, Patron.

### 4. Sign-up

The third `<section>`, with `mt-24` and the `id` "join". A grid (`md:grid-cols-12`):

- Columns 1–5: an `<h2>` "Join" in `text-h2 md:text-h1`, then with `mt-4` two sentences: "Your membership starts today, Wednesday 11 November 2026. You can cancel it at any time from your account page." With `mt-4`, a `<p>` in `text-body text-muted-foreground`: "This is a demo. No card details are asked for and nothing is charged."
- Columns 7–12, with `mt-12 md:mt-0`: a wrapper holding the form and the status element from the layout notes.

The form, with `noValidate`:

1. "Full name", an `Input` with `autoComplete="name"` (add the component with `npx shadcn@latest add input`).
2. With `mt-6`, "Email address", an `Input` with `type="email"` and `autoComplete="email"`, and helper text: "We send your membership card here."
3. With `mt-6`, "Plan", a native `<select>` with Friend, Regular and Patron. Regular is chosen at first.
4. With `mt-6`, a summary line, a `<p>` with `aria-live="polite"` in `text-body`, built from the plan and the billing period: "Regular, billed monthly: £12 a month, starting Wednesday 11 November 2026." or "Regular, billed yearly: £120 a year, starting Wednesday 11 November 2026."
5. With `mt-6`, the default `Button` "Join the Vesper", with `type="submit"`. It is the page's one gold action.

### 5. Footer

The footer from the layout notes.

### Behavior

- The billing period and the chosen plan are component state. The prices, the billing notes, the `/ month` or `/ year` labels and the summary line all read from them, and the `<select>` and the "Choose" buttons both set the plan.
- A "Choose" button sets the plan and moves focus to the "Full name" field, which scrolls the form into view. It does not submit anything. Once the confirmation is shown, the form does not come back: a "Choose" button then only moves focus to the confirmation's `<h3>`, and the confirmation keeps the plan, period and price that were sent, whatever is switched after.
- When the form is sent, check it in code: the name must not be empty ("Enter your full name."), and the email must not be empty and must look like an address, with one `@` and a dot after it ("Enter an email address such as name@example.com."). Show every error at once, and move focus to the first field with an error.
- A good form is replaced by the confirmation. Its `<h3>`: "Welcome to the Vesper, Mara", with the first word of the name the person typed and no full stop. Under it, with `mt-4`, these sentences: "Your Regular membership, billed monthly at £12 a month, starts on Wednesday 11 November 2026." (yearly: "billed yearly at £120") "Your card will reach you by email. This was a demo: no payment was taken." After the status element, in the shared wrapper as the layout notes say, a text link "See what's on" to `/`.
- No payment form, no card field, no price is charged anywhere on the page.

## 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`, `4`, `6`, `12`, `24`) plus 0, 1px and 2px. No arbitrary values such as `p-[18px]`, and no other steps such as `p-3` or `gap-5`. `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 design the dark mode first: it is where this theme lives.
- Do keep gold to one action or one title per screen.
- Do let images be large and uncropped.
- Don't add glows, neon or blurred glass, however much the subject invites it.
- Don't mix gold with other warm accents: lavender is the only second color.
- 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 every plan's price and its billing note, the form's summary line follows, and the chosen button has `aria-pressed="true"`.
- [ ] Exactly one plan, Regular, has the "Recommended" badge, which is the page's only badge; the plans are not enlarged, lifted or tinted.
- [ ] The page has one default (gold) button, "Join the Vesper", and asks for no card or bank details.
- [ ] An empty name, or an empty or malformed email address, shows a sentence with an icon under the field, sets `aria-invalid` and moves focus to the first field with an error.
- [ ] A good sign-up replaces the form with a confirmation in a `role="status"` element that names the plan, the price and the start date, and says no payment was taken.
- [ ] 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.
