# Build a lesson booking form with the Tideline theme

You are building one page in an existing project: the booking form of a surf school, with a lesson, a date, the number of people, their ability, a name, an email, a phone number and a note, checked in code, a summary beside it whose total follows the choices, and a confirmation that replaces the form. Follow this brief closely. `DESIGN.md` decides the tokens: colors, type, spacing steps and corners. This brief decides the layout, its widths included, and which token goes where. If this brief names a color, a type size, a spacing step or a corner that `DESIGN.md` does not have, `DESIGN.md` wins.

## Before you start

1. **Check the theme is installed.** The project should have `DESIGN.md` in its root and the Tideline variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/tideline.json` and save https://strictthemes.com/themes/tideline/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/tideline/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:** Epilogue for headings, Manrope for body text, Geist Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 60px/800, `text-h1` 40px/800, `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`), 16px (`4`), 24px (`6`), 40px (`10`), 96px (`24`).
- **Corners:** `rounded-sm` 6px, `rounded-md` 12px, `rounded-xl` 20px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 12px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #264653, on-primary #FFFFFF, secondary #E9C46A, on-secondary #12262D, tertiary #F4A261, on-tertiary #12262D, neutral #4F6770, background #FBF5E6, surface #FFFFFF, foreground #12262D, border #E3D9BF, error #B3261E, lagoon #2A9D8F, on-lagoon #12262D, coral #E76F51, on-coral #12262D.

### How Tideline pages are laid out

- **Color blocks:** a large block of color is `bg-lagoon text-lagoon-foreground` or `bg-coral text-coral-foreground`, the pair always together on the block itself. The two colors and their ink are the same in both modes, so the ink class keeps the text on a block deep-sea ink (`#12262D`) in dark mode too, where the page's own text turns to sand. A block is `rounded-xl p-6 md:p-10` with the fill and the ink, `flex flex-col gap-6` inside, no border and no shadow. Everything inside it inherits the ink: no element in a block gets a text color class of its own, so never `text-foreground`, `text-muted-foreground` or `text-primary` there, also not on the labels, small figures and small print that the notes below make muted elsewhere. A block holds words and figures only: a heading, a sentence or two, a few facts. Nothing in it takes focus or carries a color of its own: no link, button, field, error line, badge, avatar, icon or card. In dark mode the main button and the focus outline are sun yellow, which is lost on lagoon and coral; the action that goes with a block stands on the sand right after it. A line drawn on a block is 1px in the block's ink: `border-lagoon-foreground` on lagoon, `border-coral-foreground` on coral. One color per block, sand between blocks: two blocks never touch or stand side by side, and a block is never inside a card. Where a block is a whole section, the `<section>` itself carries the block's classes. Where a line or an action follows a block in the same section, the section is a full-width section (below), the block is a `<div>` first in it, and what follows stands on the sand under it.
- **Page:** one `<div>` wraps the whole page with `min-h-svh bg-background text-body text-foreground`. Inside it sit the `<header>`, a `<main>` and the `<footer>`, in this order.
- **Container:** the header's row, the `<main>` and the footer's row each sit in a centered container, `mx-auto max-w-[1248px] px-6`, so the content is at most 1200px wide, the grid in `DESIGN.md`, with 24px at each side at every width. This width is the one arbitrary value allowed.
- **Main:** the `<main>` element itself carries the container classes, `flex flex-col gap-24` (96px between sections) and `pt-10 pb-24`. Its items are the page's sections, in the order the brief gives. The first is always the opening: a page opens on one block of color that runs the width of the container.
- **Sections:** where this brief calls something a section, it is a `<section>` with `aria-labelledby` pointing at its heading; without a visible heading it gets an `aria-label`. There are two kinds, and the brief says which:
  - A full-width section is `flex flex-col gap-6`: the heading (or a heading group) first, the content after it. Everything starts at the left edge of the grid; nothing on these pages is centered.
  - A section of parts side by side is itself the grid: `grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-x-6 md:items-start`, and each part gets the `md:col-span-*` (and `md:col-start-*`) the brief gives it. Below `md` the parts stack in their order, 40px apart.
- **Heading groups:** where a label stands over a heading, the two sit in a `flex flex-col gap-2`, the label first. Where a sentence follows a heading, the heading (or its group) and the sentence sit in a `flex flex-col gap-4`.
- **Rows of three:** three cards or three items side by side are a `grid grid-cols-1 gap-6 lg:grid-cols-3`: one column up to `lg`, three from there, `gap-6` in both directions. Cards in one row stretch to the same height. Two cards side by side are `grid grid-cols-1 gap-6 md:grid-cols-2`.
- **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 figure and the note under it, and between a field's label, the field, its helper text and its error; `gap-4` between a heading and its paragraph, between paragraphs, and between the parts inside a card; `gap-6` between the fields of a form, between a section's heading and its content, between cards of the same kind, and between the groups of a color block; `gap-10` between the parts of a long read; `gap-24` between sections, on the `<main>`. A row of things side by side is `flex flex-wrap items-center gap-4`, or `gap-2` for things that belong together (an icon and its word, a figure and its unit). Where this brief gives no gap, use the one of these that fits. The only margins these pages use are `mt-auto`, to push the last part of a card to its foot, and the `mt-1` that sets an icon on the first line of its text.
- **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 py-2`. On the left the school's short name, "Sandhopper", a link to `/` in `inline-flex h-11 items-center font-heading text-h2 text-foreground`, never marked as the current page. Then the `<nav>`, the page's only one, with the `aria-label` "Main" and `w-full md:ml-auto md:w-auto`, so from `md` up it sits at the row's right end and below `md` it takes a line of its own under the name. It holds a `<ul>` with `flex flex-wrap gap-x-6` and four links, in this order: "Lessons" to `/lessons`, "Forecast" to `/forecast`, "Guide" to `/guide`, "Book" to `/booking`. Each link is `inline-flex h-11 items-center font-mono text-label uppercase text-foreground underline-offset-4 hover:underline`. The current page's link, where the brief names one, keeps its address and adds `aria-current="page"` and `underline`. The four fit on one line at 375px, so there is no "Menu" button.
- **Footer:** a `<footer>` with `border-t border-border`, running the full width. Inside it, one `<div>` carries both the container classes and `flex flex-col gap-6 py-10 md:flex-row md:items-start md:justify-between`. First a `flex flex-col gap-2`: the name as a `<p>` in `font-heading text-h2`, "Sandhopper Surf School" (not a heading and not a link), and a `<p>` in `text-body`: "Surf lessons and kit hire at Pentarrow Sands since 2009." Then a `flex flex-col gap-2 md:items-end`: a label in `text-muted-foreground`, `The Boat Shed · Beach Road · Pentarrow Sands · ZP7 3SB`; a `<p>` in `text-body` with two links in running text joined by ` · `, `01632 960 418` to `tel:+441632960418` and `hello@sandhopper.example` to `mailto:hello@sandhopper.example`; and a label in `text-muted-foreground`, `Every day · 08:30–18:00 · © 2027 Sandhopper Surf School`, each time a `<time>`.
- **The school:** Sandhopper Surf School works from the Boat Shed, Beach Road, Pentarrow Sands, ZP7 3SB, on the Tolven coast: an invented school on an invented beach. Open every day of the year, 08:30 to 18:00. Phone 01632 960 418, email hello@sandhopper.example. Head coach: Tamsin Rowe. Three lessons: First Waves (beginners, 2 h, up to 8 per coach, ages 8 and up, £42 a person), Green Waves (improvers, 2 h 30 min, up to 6 per coach, ages 12 and up, £55 a person) and Private Coaching (any level, 1 h 30 min, 1 to 4 people, ages 6 and up, £85 a person). Every lesson includes a wetsuit, a board and a coach in the water. The start time is set by the tide, between 09:00 and 15:00, and emailed by 18:00 the day before. "Today" in the conditions and the forecast is Thursday 17 June 2027, fixed sample text: low water at 13:34 (1.5 m), high water at 19:41 (4.8 m), a 3 ft swell from the west at 9 s, an 8 mph offshore wind from the east, the water at 15 °C. Every page uses these names and figures, word for word.
- **Headings:** the `<h1>` is `text-h1`, and `text-display` from `md` up where this brief says so (`text-h1 md:text-display`). An `<h2>` is `text-h2`. An `<h3>` is `text-h2` too, unless this brief gives it `text-body font-bold`. Headings get Epilogue on their own; any other element in `text-display`, `text-h1` or `text-h2` adds `font-heading`. The type class sets the weight (800, 800, 700): add no weight class to text in those three sizes. Short, in sentence case, no full stop.
- **Labels:** nav links, field labels, button and badge text, table headers, the label over a heading or a figure, units, and short lines of facts (a length, a group size, an age, a distance) are labels: `font-mono text-label uppercase`. Write them in sentence case in the code; the class sets the capitals. A label is a short fragment, never a sentence. Parts of a label line are joined with ` · `. A label on its own line is a block element such as a `<p>`.
- **Small print:** helper text under a field, an error line and a caption that is a whole sentence are small print: `font-mono text-label`, without `uppercase`, because a sentence in capitals is hard to read.
- **Large figures:** a figure a visitor plans by (a tide time, a wave height, a wind speed, a price that is the point of a card, a total) is the number in `font-heading tabular-nums` at the size this brief gives (`text-h1` or `text-h2`), and its unit beside it as a label, the two in a `flex flex-wrap items-baseline gap-x-2`: `3` and `ft`, `8` and `mph`, `15` and `°C`, `13:34` and `low`. A price carries its pound sign in the number (`£42`), and what it is for is the label beside it (`a person`). On sand and on a card the number is `text-foreground` and the unit `text-muted-foreground`; on a color block neither has a color class.
- **Conditions:** a set of conditions is a `<dl>` on a color block: `grid grid-cols-2 gap-6 md:grid-cols-4`. Each fact is a `<div>` with `flex flex-col gap-2 border-t pt-4` and the block's ink for the line (`border-lagoon-foreground`), holding a `<dt>` (a label: `Tide`, `Swell`, `Wind`, `Water`), a `<dd>` with the large figure and its unit, and a second `<dd>` with the fact's note, a label like the first (`9 s · From the west`). Nothing in it has a text color class: it stands on a block.
- **Units:** tide heights in metres (`1.5 m`), swell in feet (`3 ft`), wind in miles an hour (`8 mph`), the water in degrees Celsius (`15 °C`), the time between waves in seconds (`9 s`), a length of time as `2 h` or `2 h 30 min`. A space stands between the number and its unit.
- **Small figures:** a time, a date, a height, a distance or a count that this brief calls a small figure is `font-mono text-label uppercase tabular-nums text-muted-foreground`, on the element that holds the whole figure.
- **Text color:** on sand and on cards, headings, body text, large figures, prices, field labels and the names of things are `text-foreground`. Other labels, small figures, captions and helper text are `text-muted-foreground`. Paragraphs are never muted. Muted text stands only on sand or on a card, never on a fill. Lagoon, coral, sun yellow and orange are fills, never text colors. On a color block every text inherits the block's ink, as the first note says.
- **Writing:** plain and friendly, in short sentences, addressing the visitor as "you" and written by the school as "we". Give the facts a visitor plans by: time, place, price, what to bring. No exclamation marks. An error says what to do.
- **Measure:** a paragraph of body text is at most `max-w-xl` wide.
- **Dates and times:** a date or a clock time is a `<time>` with a `dateTime` wherever it appears, in a sentence, a label, a figure or a table cell too, except inside an `<option>`: `<time dateTime="2027-06-17">Thursday 17 June 2027</time>`, `<time dateTime="13:34">13:34</time>`. A date is written with the weekday, the day, the month in words and the year, with no comma (`Thursday 17 June 2027`), or shorter only where this brief shows it (`Thu 17 Jun`, `15 May`). A clock time is 24-hour with two-digit hours, `06:40`, `13:34`, and takes `tabular-nums`. A span of hours is two `<time>`s joined by an en dash: `08:30–18:00`. A length of time (`2 h 30 min`) is not a `<time>`. A copyright year is plain text.
- **Prices:** whole pounds with the pound sign and no pence: `£42`, `£5`. Never a price without its currency.
- **Links:** links are the text color, underlined: a link in running text is `text-foreground underline decoration-1 underline-offset-4 hover:decoration-2`, and inherits its size. A link whose destination this brief does not name points to `#`. Links stand on sand or on a card, never on a color block.
- **Added components:** a component you add with `npx shadcn@latest add` during the build (an `Input`, a `Textarea`, a `Badge`, a `Table`) gets the `cn` import, the focus outline only if it takes focus on the page, and the edits these notes give its file.
- **Buttons:** the shadcn `Button` with `className="h-11 rounded-md px-6 font-mono text-label uppercase"`: 44px tall, 12px corners, an uppercase mono label of two to four words, written in sentence case in the code. The default variant (deep-sea charcoal, sun yellow in dark mode) is for the page's one main action; the brief names it, and no page has two. Every other button is `variant="secondary"`, sun yellow with ink (sand with ink in dark mode). Its stock hover mixes in the foreground and can hardly be told from its resting color: in `components/ui/button.tsx`, replace the `secondary` variant's `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]` with `hover:bg-secondary/80`, once, on every page whether or not it shows a secondary button. Quiet buttons (the unchosen ones in a group, a stepper's minus and plus) are `variant="outline"`; on a page that uses one, in the `outline` variant replace `bg-background` with `bg-card` and take out `dark:bg-input/30`, so it is the card's color in both modes, on sand and on a card, its hover classes unchanged. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A button that its own click disables gets `focusableWhenDisabled`, so keyboard focus stays on it: only a stepper's minus and plus. Base UI then 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 is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "h-11 rounded-md px-6 font-mono text-label uppercase")`, because `buttonVariants` alone does not merge classes. A button below `md` is as wide as its column where this brief says so (`w-full md:w-auto`). Buttons stand on sand or on a card, never on a color block.
- **Button groups:** a set of buttons where one is chosen is a `role="group"` with `flex flex-wrap gap-2`, named by its visible label (`aria-labelledby` pointing at the label's `id`). The chosen button has `aria-pressed="true"` and `variant="secondary"`; the others have `aria-pressed="false"` and `variant="outline"`. Choosing moves no focus. Not `Tabs`: there are no tab panels.
- **Steppers:** a number set with minus and plus buttons is a `role="group"` named by its visible label (`aria-labelledby`), with `flex items-center gap-2`: a minus `Button`, the value, a plus `Button`. Each button is `variant="outline" size="icon"` with `className="size-11 rounded-md"` (not the text button's classes) and an `aria-label` that says what it does, holding the `Minus` or `Plus` icon. The value is a `<span>` in `inline-flex min-w-24 justify-center font-mono text-label uppercase text-foreground tabular-nums` with `aria-live="polite"`, written with its noun (`2 people`). At the lowest and highest value the matching button is `disabled` with `focusableWhenDisabled`.
- **Inputs:** the shadcn `Input` with `className="h-11 rounded-md border-muted-foreground px-4 text-body md:text-body"`, and the `Textarea` with `className="min-h-32 rounded-md border-muted-foreground px-4 py-2 text-body md:text-body"`. The stock `border-input` is the card's own pale border, too faint to find a field by; `border-muted-foreground` is clear in both modes. The stock `focus-visible:border-ring` and `aria-invalid:border-destructive` stay. Fields only ever sit in cards, and their fill is the card's, like the select's: in `components/ui/input.tsx` and `components/ui/textarea.tsx`, replace the stock `bg-transparent` and `dark:bg-input/30` with `bg-card` (in `input.tsx`, `file:bg-transparent` stays), and delete `dark:aria-invalid:border-destructive/50`, so an error border is the same red as the select's in dark mode. Every field has a visible `<label>` above it with `block`, a label in `text-foreground`, tied to it with `htmlFor`. The label, the field, helper text and the error sit in a `flex flex-col gap-2`. Helper text is small print in `text-muted-foreground`, and the field names it in `aria-describedby`. A field that may be left empty says so in its label: `A note (optional)`. A date field is an `Input` of type `date`.
- **Selects:** a choice from a short list is a native `<select>` with `h-11 w-full rounded-md border border-muted-foreground bg-card px-4 text-body text-foreground aria-invalid:border-destructive` and the focus outline from the rules (`focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`). Its first option is an empty choice in words ("Choose a lesson"), with the value `""`. It gets the same label, stack and error handling as any field.
- **Forms:** a form sits in a card, the card is the wrapper for the form and the confirmation that replaces it, and the form's heading sits above the card, in the part's stack, not inside it. In place of the card's column stack, such a card is a plain block (`rounded-xl border border-border bg-card p-6` with no `flex`), holding the `<form>` and, after it, the status element. The `<form>` has `noValidate`, an `aria-label` and `flex flex-col gap-6`; its last item is the submit button, full width below `md` (`w-full md:w-auto md:self-start`).
- **Errors:** an error is a line under its field, inside the field's stack, after any helper text: a `<p>` with an `id` and `flex items-start gap-2 font-mono text-label text-destructive`, holding the `CircleAlert` icon (`size-4 shrink-0`, `aria-hidden="true"`; `items-start` sets it on the first line) and one sentence that says what to do. The field gets `aria-invalid` and adds the line's `id` to its `aria-describedby`, beside the helper text it already names. Fields are checked in code when the form is sent, and focus moves to the first field with an error; before the first send no error shows. To focus a field in the same step that adds its error line, set the errors in state and move the focus in an effect that runs after that render (or wrap the state change in `flushSync`), so the line is in the page when focus lands. An email is well formed when, trimmed, it matches `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`. An error line and its field's `aria-invalid` are removed when that field changes (on `input` or `change`); every other error stays until the next send.
- **Confirmations:** a confirmation is plain text in `text-foreground`, no colored box: a heading at the level this brief gives and a sentence or two. It goes into an element with `role="status"` that is in the card from the start, empty, so that its message is announced. It is never hidden (no `hidden` attribute, no `empty:hidden`) and has no classes and no margin; empty, it takes no space. The message in it is a `<div>` with `flex flex-col gap-4`, rendered only once there is a message, and the form is removed when it appears, so the message stands where the form stood. Give the message's heading `tabIndex={-1}` and move focus 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` (white on sand, 20px corners, 24px padding, a 1px border, no shadow) 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 a card inside a card, never a bordered box inside a card, and never a color block inside a card: divide a card's parts with a 1px `border-t border-border` and `pt-4`.
- **Rows:** a list of terms and values (a summary, the facts about a place) is a `<dl>` with `divide-y divide-border`, so a 1px rule falls between rows and none above the first or below the last. Each row is a `<div>` with `flex items-baseline justify-between gap-6 py-4 first:pt-0 last:pb-0`: the term, a `<dt>`, is a label in `text-muted-foreground` with `shrink-0`; the value, a `<dd>`, is `min-w-0 text-right text-body text-foreground`, so a long value wraps inside its own cell at 375px. Where this brief says the rows are stacked, each row is `flex flex-col gap-2 py-4 first:pt-0 last:pb-0` in place of that, the term over the value, and the value has no `text-right`.
- **Tables:** the shadcn `Table` (`npx shadcn@latest add table`) with `className="text-body"`, in a card that has no padding and `overflow-hidden` (`overflow-hidden rounded-xl border border-border bg-card`, with no `flex` and no `gap-4`), so the table touches the card's edges. Header cells are labels in `text-muted-foreground` with `h-auto px-6 py-4` and `scope="col"`; body cells have `px-6 py-4` and `text-foreground`, and a cell that holds a figure (a time, a height, a speed, a price) adds `tabular-nums`. Figures in a table stay in the body font, with their unit: `1.5 m`, `£10`. Rows do not change on hover: `hover:bg-transparent` on every row. Stock cells do not wrap, so a wide table scrolls sideways in its own box. In `components/ui/table.tsx`, give that box `tabIndex={0}`, `role="region"`, an `aria-label` and the focus outline drawn inside it (`focus-visible:-outline-offset-2` in place of `focus-visible:outline-offset-2`, since the box fills a card that clips), and add a required `label` prop for the name. A table's name is the heading above its card: `label` repeats that heading's words, and there is no `<caption>`, which would scroll away with the table.
- **Badges:** the shadcn `Badge` with `className="h-auto rounded-sm border-transparent bg-tertiary px-2 py-1 font-mono text-label uppercase text-tertiary-foreground"`: warm orange with ink, in both modes. One or two words, no icon, at most one on a card. Orange is a fill only: never `text-tertiary` or an orange border. Badges stand on sand or on a card, never on a color block.
- **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`, with `aria-hidden="true"` beside the written name. 44px, under the theme's 64px limit for full rounds, and the only round thing on these pages.
- **Pictures:** there are no image files and no drawn pictures on these pages: no waves, suns, boards or maps built from elements. The color blocks, the large type and the figures carry the page.
- **Icons:** from `lucide-react`, with `size-4`, `strokeWidth={2}` and `aria-hidden="true"`, only where this brief or these notes name one. An icon never stands alone: it sits beside words, or in a button that has an `aria-label`. 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.
- **Corners:** `rounded-sm` (6px) for badges; `rounded-md` (12px) for buttons, inputs and selects; `rounded-xl` (20px, the spec's `lg`) for cards and color blocks. `rounded-lg` is 12px too, shadcn's name for the base corner; write `rounded-md`. `rounded-full` only for avatars.
- **Surfaces:** flat, like paint on a beach hut. No shadows, blur, gradients or glows in the code you write, and no decoration drawn in code. A card has its 1px border and a color block has none. A stock component that floats over the page keeps the one short shadow it ships with.
- **Motion:** the code you write adds no transition and no animation, and nothing moves on scroll. 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: booking a lesson with "Sandhopper Surf School" at Pentarrow Sands. Three lessons: First Waves (beginners, 2 h, £42 a person), Green Waves (improvers, 2 h 30 min, £55 a person) and Private Coaching (any level, 1 h 30 min, £85 a person). One booking online covers 1 to 4 people; bigger groups book by phone on 01632 960 418. Nothing is paid online: the total is paid at the Boat Shed on the day, by card or cash. The start time is set by the tide, between 09:00 and 15:00, and emailed by 18:00 the day before. The school decides by 07:30 on the day whether the sea is safe; a lesson it calls off is moved or refunded in full.

Build these parts in order.

### 1. Header

The header from the layout notes, with "Book" as the current page; its link keeps `/booking`.

### 2. Opening

A full-width section that is a color block in lagoon, as the layout notes describe it, with its `aria-labelledby` pointing at the `<h1>`. The block element itself carries `bg-lagoon text-lagoon-foreground`, the pair from the first layout note, so its text stays ink in dark mode; nothing inside it gets a text color class of its own. It holds one `flex flex-col gap-4`: a heading group, the label `Lessons every day · Pay on the day` over the `<h1>` "Book a lesson" in `text-h1 md:text-display`, and one paragraph in `text-body` with `max-w-xl`: "Choose a lesson and a date, tell us who is coming, and we hold your places. It takes two minutes, and you pay at the Boat Shed on the day."

### 3. The form and your booking

A section of parts side by side, from the layout notes, with its `aria-labelledby` pointing at the `<h2>` "Your lesson".

**First part,** `md:col-span-7`: a `flex flex-col gap-6` with the `<h2>` "Your lesson" and the form card, as the Forms note describes it. Add the fields with `npx shadcn@latest add input textarea` if the project has none, and give the new files the edits the Added components note lists. This page uses the `outline` variant and `focusableWhenDisabled`, so it makes both of those edits in `button.tsx`, as well as the `secondary` one. The `<form>` has the `aria-label` "Book a lesson". Its fields, in this order, each in its own `flex flex-col gap-2`:

1. **Lesson**: the label "Lesson", then a native `<select>` styled as the layout notes describe, with the empty choice "Choose a lesson" and three options, each its plain text: `First Waves · Beginners · £42 a person`, `Green Waves · Improvers · £55 a person`, `Private Coaching · Any level · £85 a person`.
2. **Date**: the label "Date", an `Input` of type `date` with `min` set to today on the visitor's clock, and the helper text `We set the start time by the tide and email it to you the day before.` `min` only limits the picker: a date typed before it still reaches the check.
3. **People**: a label as a `<p>` with an `id`, "People", in `font-mono text-label uppercase text-foreground` (a `<label>` needs a field to point at; this one names the group), then a stepper from the layout notes, from 1 to 4, starting at 1. The value reads `1 person`, `2 people` and so on. The buttons' `aria-label`s are "Fewer people" and "More people". Under it the helper text `Up to 4 people online. For a bigger group, call 01632 960 418.`, named in the group's `aria-describedby`.
4. **Ability**: the label "Surfing so far", then a native `<select>` with the empty choice "Choose the closest" and four options: `Never surfed`, `One or two lessons`, `Catching white water waves alone`, `Riding unbroken waves`. Under it the helper text `For a group, choose for the least experienced person.`
5. **Name**: the label "Your name", an `Input` with `autoComplete="name"`.
6. **Email and phone**, side by side from `md` up: a `grid grid-cols-1 gap-6 md:grid-cols-2 md:items-start` whose two items are the fields' own stacks. The label "Email address", an `Input` of type `email` with `autoComplete="email"`, and the helper text `We send your start time here.` The label "Mobile phone", an `Input` of type `tel` with `autoComplete="tel"`, and the helper text `We text you if the sea changes our plans.`
7. **Note**: the label "A note for your coach (optional)", a `Textarea` with `maxLength={300}`, and the helper text `An old injury, nerves, a birthday: anything that helps us plan.`

The last item of the form is the page's one main action: the default `Button`, `type="submit"`, "Book my lesson", full width below `md` as the Forms note says. After the form, in the card, the empty `role="status"` element.

**Second part,** `md:col-span-4 md:col-start-9`: a card as an `<aside>` with `aria-labelledby` pointing at its `<h2>`, and `md:sticky md:top-6`, so it stays in view beside the form. Below `md` it comes after the form card. In the card's `gap-4`:

- The `<h2>` "Your booking".
- Rows from the layout notes, a `<dl>` with four rows that read from the form:
  - `Lesson`: the lesson's name alone, `First Waves`; before one is chosen, `Not chosen yet`.
  - `Date`: the date written out, `Saturday 19 June 2027`, in a `<time>` with the field's value as its `dateTime`; while the field is empty, `Not chosen yet`.
  - `People`: as the stepper writes it, `2 people`.
  - `Price`: `£42 a person`, with `tabular-nums`; before a lesson is chosen, `Not chosen yet`.
- The total line: a `<div>` with `flex items-baseline justify-between gap-6 border-t border-border pt-4`, holding a label in `text-foreground`, `Total`, and the total as a `<p>` in `font-heading text-h2 text-foreground tabular-nums`: the lesson's price times the number of people, `£84`. Before a lesson is chosen it reads `£0`.
- A `<p>` of small print in `text-muted-foreground`: "Wetsuit, board and coach included. Pay at the Boat Shed on the day, by card or cash."

The summary is not a live region: the stepper already announces its value, and the confirmation says the total.

### 4. What happens next

A full-width section with its `aria-labelledby` pointing at the `<h2>`. It opens with the `<h2>` "What happens next". Then, in the section's `gap-6`, a row of three from the layout notes, as an `<ol>` with no markers and no padding that carries the grid classes. The three steps stand on the sand, not in cards. Each `<li>` is `flex flex-col gap-2 border-t border-border pt-4`, holding the step's number as a `<p>` in `font-heading text-h1 text-foreground` with `aria-hidden="true"` (the list already counts), an `<h3>` in `text-h2`, and one or two sentences in `text-body`:

1. `1`, "We hold your places": "As soon as you send the form, your places are yours. Nothing is paid online."
2. `2`, "We set the time": "By 18:00 the day before, we email your start time. The tide decides it: somewhere between 09:00 and 15:00.", each time a `<time>`.
3. `3`, "You come 20 minutes early": "Find us at the Boat Shed at the foot of the slipway. You pay, we fit your wetsuit, and we walk down to the water together."

### 5. If the sea says no

A full-width section that is a color block in coral, as the layout notes describe it, with its `aria-labelledby` pointing at its `<h2>`. The block element itself carries `bg-coral text-coral-foreground`; nothing inside it gets a text color class of its own. It holds one `flex flex-col gap-4`: a heading group, the label `Our weather promise` over the `<h2>` "If the sea says no, you choose", and one paragraph in `text-body` with `max-w-xl`: "We decide by 07:30 on the day whether the sea is safe. If we call your lesson off, you pick another date or get all of your money back.", the time a `<time>`. The block holds no link and no button.

### 6. Footer

The footer from the layout notes.

### Behavior

- Keep every value in component state: the lesson, the date, the number of people, the ability, the name, the email, the phone and the note. The summary reads from the same state, so its rows and its total follow each change as it is made.
- The date in the summary is written from the field's `YYYY-MM-DD` value as the layout notes write a date, weekday first and with no comma. Read the value as a local date from its three numbers, not with `new Date(value)`, which reads it as UTC and can give the day before.
- When the form is sent, check it in code, in this order:
  - Lesson: the empty choice gives "Choose a lesson."
  - Date: empty gives "Choose a date."; a date before today gives "Choose today or a later date." Compare dates as the `YYYY-MM-DD` strings a date field gives, with today taken from the visitor's clock in their own time zone.
  - The number of people is always valid, since the stepper cannot leave 1 to 4.
  - Ability: the empty choice gives "Choose the closest match, so we can plan the lesson."
  - Name: empty, or only spaces, gives "Write your name."
  - Email: empty gives "Write your email address, so we can send your start time."; a malformed one gives "Check the email address: it needs an @ and a domain."
  - Phone: empty gives "Write a mobile number we can text on the day."; fewer than 10 digits, counting digits only and allowing spaces, `+` and `-`, gives "Check the mobile number: it needs at least 10 digits."
  - The note is optional and never gets an error.
- Each error line follows the layout notes, which say when it shows, when it goes and how focus reaches it. Focus moves to the first field with an error in the order of the form.
- When everything is good, the form is replaced in its card by the confirmation in the status element: an `<h3>` in `text-h2`, "Your places are held, Imogen Tarrant" (the name as typed, trimmed), with `tabIndex={-1}`; then a sentence that repeats the booking: "We have 2 people down for First Waves on Saturday 19 June 2027. The total is £84, to pay at the Boat Shed on the day.", the count written as the stepper writes it, the lesson by its name alone, and the date in a `<time>`; then a sentence: "We will email your start time to imogen@example.com by 18:00 the day before. To change anything, call 01632 960 418.", with the address they typed, the time a `<time>` and the number a link in running text to `tel:+441632960418`. The heading and the sentences sit in the message `<div>`; no button follows. Move focus to the heading. The card "Your booking" stays as it is.
- The other links need no behavior.

## Rules

- **Colors:** only the theme's colors, through the semantic classes (`bg-background`, `text-foreground`, `bg-card`, `bg-primary`, `text-primary-foreground`, `text-muted-foreground`, `border-border` and so on). No hex, rgb or named colors in components, and no Tailwind palette classes such as `bg-blue-500` or `text-gray-600`.
- **Spacing:** padding, margin and gap use only the scale's steps (`1`, `2`, `4`, `6`, `10`, `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 keep text in ink: lagoon and coral are fills, never text colors.
- Do keep buttons, links and fields on sand or white: a color block holds text only.
- Do use one color per block, and sand between blocks.
- Do give the facts a visitor plans by (time, place, price, what to bring) plainly.
- Don't draw waves, suns or palm trees in code, and don't use gradients or glows.
- Don't put a color block inside a card or a card inside a card.
- Don't introduce a color, size or radius that is not a token above.

## Check before you finish

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

- [ ] The lesson and the ability are native selects whose first option is an empty choice in words, and the date is a date field whose earliest day is today on the visitor's clock.
- [ ] The number of people is set with minus and plus buttons from 1 to 4, each with an aria-label, and the value is announced with its noun, 1 person or 2 people.
- [ ] The summary's total is the lesson's price times the number of people and follows both: First Waves for 2 people reads £84, and with no lesson chosen it reads £0.
- [ ] Sending the empty form gives the lesson, the date, the ability, the name, the email and the phone each their own error line with an icon, tied to its field, and focus moves to the lesson select.
- [ ] A good booking replaces the form, inside the same card, with a confirmation that repeats the lesson, the date, the number of people and the total, a screen reader announces it, and focus moves to its heading.
- [ ] The opening block element carries bg-lagoon text-lagoon-foreground and the promise block element bg-coral text-coral-foreground, so their text stays deep-sea ink in dark mode, and every field and button stands on a card or on the sand.
- [ ] 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.
