# Build a reservation request page with the Veranda theme

You are building one page in an existing project: a page to request a reservation at a small guesthouse, with a form for the dates, the room, the guests and who is coming, checked in code, a summary beside it that works out the nights and the total, 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 Veranda variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/veranda.json` and save https://strictthemes.com/themes/veranda/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/veranda/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:** Cormorant for headings, Outfit for body text, Cutive Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 68px/500, `text-h1` 44px/500, `text-h2` 28px/600, `text-body` 16px/400, `text-label` 13px/400.
- **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`), 104px (`26`).
- **Corners:** `rounded-sm` 2px, `rounded-md` 4px, `rounded-xl` 12px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 4px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #3D405B, on-primary #F4F1DE, secondary #F2CC8F, on-secondary #2A2C40, tertiary #81B29A, on-tertiary #2A2C40, neutral #5C5F78, background #F4F1DE, surface #FBFAF1, foreground #2A2C40, border #DDD8BD, error #A63A24, clay #E07A5F, on-clay #2A2C40.

### How Veranda pages are laid out

- **The clay block:** every page has exactly one block of clay, where this brief puts it, and nothing else on the page is `bg-clay`. The block is a plain element with `rounded-xl bg-clay p-6 text-clay-foreground`, the fill and the ink always together on the block itself, with no border and no shadow. Clay and its ink are the same in both modes, so the ink class keeps text on clay dark (`#2A2C40`) in dark mode too. Everything inside the block inherits the ink: no element in it gets a text color class of its own, so never `text-foreground`, `text-muted-foreground` or `text-primary` there, also not on the labels, dates and prices that the notes below make muted elsewhere. The block holds a heading and a few facts as text, and nothing that takes focus or carries its own color: no link, button, field, error line, badge, card or picture. In dark mode the main button and the focus outline are sand, which hardly shows on clay; the action that goes with the block sits on the paper, beside it or right after it. An icon in the block draws in the ink on its own. The block has no rules inside it: its parts are set apart by the margins of the rhythm note. Never a clay block inside a card.
- **Container:** one `<div>` wraps the whole page and is the container: `mx-auto max-w-[1160px] px-4 md:px-6`, plus `text-body`. This width is the one arbitrary value allowed. Inside it sit the `<header>`, a `<main>` holding everything between the two, and the `<footer>`.
- **Grid:** where this brief puts things side by side, they are items of a grid: `grid gap-6 md:grid-cols-12`, a single column below `md`. "Columns 1–7" is `md:col-span-7`; "columns 9–12" is `md:col-span-4 md:col-start-9`, and so on. The grid's `gap-6` is the space between its items in both directions, also when they stack, so a grid item never has a top margin. Pages are uneven on purpose: a picture of seven columns stands beside text of four, with the column between them empty. Leave the columns this brief does not fill empty, and center nothing to fill them. Where this brief says a section is "a grid", the `<section>` itself carries the grid classes beside its own; where it puts a section's content "in columns 1–7 of a grid", one `<div>` in that grid spans those columns and holds the content. Add `md:items-start` to a grid of two or more items, unless this brief says how they line up; a grid of one item needs none.
- **Measure:** a paragraph of body text is at most `max-w-xl` wide (576px), however many columns it sits in. A heading may run wider.
- **Vertical rhythm:** written as a top margin on each element (`mt-*`), never as `space-y-*`, a bottom margin, or a gap on a stack of text. These are the only distances:
  - A heading under a label line, a label under the thing it belongs to (a caption under a picture, a line of facts under a name), a field under its label, and an error or helper line under its field: `mt-2`.
  - Text under a heading, and a paragraph after a paragraph: `mt-4`.
  - A block after text or after another block (a grid, a list, a table, a card, a form, a picture, the clay block, a button or a link on a line of its own): `mt-6`.
  - A new part inside a section that opens with its own heading, after other content: `mt-12`.
  - The first element inside any box (a section, a grid item, a card, the clay block, a list item, a form) has no top margin.
  - A link or a button on a line of its own sits in a `<p>` or a `<div>`, which carries the margin.
  These margins rely on normal block flow. Inside a grid item, a card or the clay block, do not make the content `flex flex-col` with a gap. Inside a flex row (the header, a label line with a badge, a group of buttons) the space is the row's `gap-*`, as this brief states it. Where this brief gives an element its own margin, it replaces the one from this list.
- **Sections:** `<main>` holds a stack of `<section>` elements, each with `aria-labelledby` pointing at its heading, or an `aria-label` where it has no heading of its own. The first has `pt-12` and no border: the header's hairline is above it. Every other section opens with one hairline and 104px of space above it: `mt-12 border-t border-border pt-6 md:mt-26` (48px above the line on a phone). The one exception is a section that this brief says is the clay block itself or holds nothing but it and its action: it has `mt-12 md:mt-26` and no border or padding, because the block needs no line. A `<section>` nested inside another takes the margin this brief gives it, never these classes.
- **Section heads:** where this brief gives a section a label line, the line comes first, in `text-muted-foreground`, right under the hairline; then with `mt-2` the section's `<h2>`; then with `mt-4` a sentence or two where this brief gives them.
- **Header:** the `<header>` is itself the row, with `border-b border-border py-6`: `flex flex-col items-start gap-4 md:flex-row md:items-baseline md:justify-between`. On the left the house's name, `Kittiwake House`, a link to `/` in `font-heading text-h2`, styled as a header link and never marked as the current page. It is not a heading element and has no weight class. On the right a `<nav>` with the `aria-label` "Main", the page's only `<nav>`, holding a `<ul>` with `flex flex-wrap gap-x-6 gap-y-2` and four `<li>`s, each one link as a label, in this order: "Rooms" (`/rooms`), "Menu" (`/menu`), "Find us" (`/#find-us`) and "Reserve" (`/reserve`). Every link to these places uses these addresses. Below `md` the links sit under the name; the four fit on one line at 375px, so there is no "Menu" button. Mark one link as current only where this brief names it.
- **Footer:** a `<footer>` with `mt-12 border-t border-border py-12 md:mt-26`. Inside it a grid (`grid gap-6 md:grid-cols-12 md:items-start`) with two parts, then one line:
  - Columns 1–5 (`md:col-span-5`): the house's name as a `<p>` in `font-heading text-h2` (not a heading, not a link), then with `mt-2` one sentence in `text-body`: "Six rooms above the harbour at Port Lowan, with breakfast on the veranda."
  - Columns 8–12 (`md:col-span-5 md:col-start-8`): an `<address className="not-italic">` in `text-body` holding two block `<span>`s, `9 Harbour Terrace` and `Port Lowan LW6 3QT`; then, outside the `<address>`, with `mt-4` a `<p>` holding the telephone number `01632 960 352` as a link to `tel:+441632960352`, and with `mt-2` a `<p>` holding `stay@kittiwakehouse.example` as a link to `mailto:stay@kittiwakehouse.example`. Both are footer links in `text-body`.
  - Under the grid, with `mt-6`, a `<p>` label in `text-muted-foreground`: `© 2027 Kittiwake House · Open all year`.
- **The house:** every page is about the same invented guesthouse, and uses these facts word for word. Kittiwake House, 9 Harbour Terrace, Port Lowan LW6 3QT, an invented harbour town on a south-facing bay. Built in 1894 as the harbour pilot's house, with a glazed veranda across its front where breakfast is served; kept by Nell Carrow and Jonah Pike since 2016. Telephone `01632 960 352`, email `stay@kittiwakehouse.example`. Six rooms, open all year. Guests arrive from 15:00 to 20:00 and leave by 10:30. Breakfast is included in every price and served from 08:00 to 10:00, on Sundays until 10:30. Supper is served from Thursday to Saturday, from 18:30 to 20:30, to guests who ask by 14:00. In July and August a stay is two nights or more. A price is for the room for one night, whatever the number of guests it sleeps, and depends on the season of that night: winter (1 November to 31 March), spring and autumn (1 April to 30 June, and 1 September to 31 October), summer (1 July to 31 August). The rooms, from the ground floor up, each with its address, floor, bed, size, view, the number it sleeps and its three prices (winter, spring and autumn, summer):
  - Thrift (`/rooms/thrift`): ground floor, double bed, 16 m², garden view, sleeps 2; £104, £124, £146.
  - Campion (`/rooms/campion`): first floor, single bed, 11 m², garden view, sleeps 1; £78, £92, £108.
  - Samphire (`/rooms/samphire`): first floor, twin beds, 18 m², harbour view, sleeps 2; £112, £132, £156.
  - Marram (`/rooms/marram`): first floor, king bed, 22 m², sea view, sleeps 2; £128, £152, £178.
  - Sea Holly (`/rooms/sea-holly`): second floor, king bed and a daybed, 24 m², sea view, sleeps 3; £142, £168, £196.
  - Tamarisk (`/rooms/tamarisk`): second floor, king bed and two singles, 31 m², harbour view, sleeps 4; £164, £192, £224.
  A room's "from" price is its winter price: `From £128 a night`. Only Marram has a page: a link to any other room points to `#`.
- **Headings:** the `<h1>` is `text-h1`, and `text-display` from `lg` up where this brief says so (`text-h1 lg:text-display`). A section's `<h2>` is `text-h2 md:text-h1`. An `<h3>` or `<h4>` in the heading font is `text-h2`. Headings get the heading font on their own; any other element in `text-display`, `text-h1` or `text-h2` adds `font-heading`. Nothing in the heading font is smaller than `text-h2`: a smaller heading is a label heading. Sentence case, no full stop.
- **No bold:** Cormorant is never bold. A type class already gives a heading its weight, so no heading and no text in `font-heading` gets a weight class: never `font-bold`, `font-semibold` or `font-medium`. A browser makes `<strong>` and `<b>` bolder than the text around them, so neither goes into a heading or into text in the heading font: mark a phrase there with a plain `<span>`. If the user's own heading comes with a `<strong>` or a `<b>` that has to stay, give it the weight of its heading, so it does not thicken: `font-medium` inside `text-display` or `text-h1`, `font-semibold` inside `text-h2`. Body text and labels have no bold either: no `<strong>`, no `<b>` and no weight class on them. A `<th>` is bold in a browser too: every header cell gets `font-normal`.
- **Italic:** the italic of the heading font is the theme's one flourish, and it is installed. It is set with the `italic` class on a `<span>` around one phrase of a heading, or on a guest's own words set in `font-heading`. At most one phrase in a heading, and only where this brief names it; at most two italics on a page. Nothing else is italic: Outfit and Cutive Mono have no italic here and a browser would only slant them, so no `<em>`, `<i>` or `<cite>` in body text or labels, and every `<address>` gets `not-italic`.
- **Labels:** nav links, form labels, the lines of facts about a room, floors, dates, times, prices, captions, table headers, terms in a `<dl>`, and button and badge text are labels: `font-mono text-label uppercase`. The wide tracking comes with `text-label`; add no `tracking-*` class. A label is a short fragment, not a sentence. Helper text, error lines and any line this brief writes as a sentence use the label's font and size but are not uppercased. Parts of a label line are joined with ` · `. A label on its own line is a block element such as a `<p>`. An email address is never uppercased: inside a label it sits in a `<span>` (or its link gets) `normal-case`.
- **Label headings:** a heading styled as a label (Getting here, To start, Check your dates) is the heading element that keeps the heading order, with `font-mono text-label uppercase text-foreground`. Keep the `font-mono`: a heading element gets the heading font on its own.
- **Text color:** headings, label headings, body text, names, form labels, nav links, the line of facts about a room, a total and a menu price are `text-foreground`. Captions, floors, dates, a price per night, `<dt>` terms, the label line that opens a section, helper text and the footer's © line are `text-muted-foreground`. Muted text sits only on `bg-background` or `bg-card`. Paragraphs are never muted. A button's or a badge's text color comes from its variant or its classes, and text on the clay block from the block. No other text colors: slate (`primary`), sand, sage and clay are never text colors.
- **Dates and times:** a date or a clock time is a `<time>` with a `dateTime` wherever it appears, in a sentence, a label or a row too, except inside an `<option>`: `<time dateTime="2027-05-14">Friday 14 May 2027</time>`, `<time dateTime="08:00">08:00</time>`. A date is written with the weekday, the day, the month in words and the year, with no comma (`Friday 14 May 2027`), or without the weekday where this brief shows it so (`1 April 2027`). A clock time is 24-hour with two-digit hours: `08:00`, `18:30`. In a label or a row, a span of hours is two `<time>` elements joined by an en dash, `08:00–10:00`; in a sentence it reads "from 15:00 to 20:00", each time a `<time>`. A season's dates without a year (`1 November to 31 March`) and the copyright year are plain text. "Today" is always the visitor's own clock, never a sample date.
- **Prices:** a room's price is in whole pounds, `£128`; a menu price has two decimals, `£9.50`. A price is a label with `tabular-nums`: a price per night in `text-muted-foreground` (`From £128 a night`), a menu price or a total in `text-foreground`. A price is set large, in `font-heading text-h1` or `font-heading text-h2`, only where this brief says so.
- **Nights, guests and rooms:** a count is written with its noun, and reads right at one: `1 night`, `2 nights`; `1 guest`, `2 guests`; `1 room`, `2 rooms`.
- **Links:** a link in running text is `text-foreground underline decoration-1 underline-offset-4 hover:text-muted-foreground`, and inherits its size and font. A link in the header or the footer, and a name that is a link, is `text-foreground decoration-1 underline-offset-4 hover:underline`; the link for the current page has `aria-current="page"` and `underline`. Links are never colored, and get `transition-colors duration-150`. A link whose destination this brief and these notes do not give points to `#`.
- **Onward links:** a link that stands alone at the end of a part ("See Marram", "See the menu") is a `<p>` holding the link, a label (`font-mono text-label uppercase`) styled as a link in running text, in `inline-flex items-center gap-2`, with the `ArrowRight` icon after the words. Its words say where it goes: "See Marram", never "Read more".
- **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-auto rounded-md px-6 py-2 font-mono text-label font-normal uppercase"`: `font-normal` because the typewriter face has one weight. The default variant (slate, sand in dark mode) is for the page's one main action: this brief names it, and no page has two. Every other button is `variant="secondary"` (sand, a lighter slate in dark mode). Its stock hover 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, not beside it, on every page, so each page's `button.tsx` is the same. The unchosen buttons of a button group are `variant="outline"`; on a page that has a button group, and only there, in the `outline` variant replace `border-border` with `border-muted-foreground` and delete `dark:border-input`, so their outline is as clear as a field's. Do not use the `ghost`, `link` or `destructive` variants, or the `xs`, `sm` or icon sizes. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A call to action that goes to another page, or a `mailto:` link styled as a button, is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "h-auto rounded-md px-6 py-2 font-mono text-label font-normal uppercase")`, because `buttonVariants` alone does not merge classes. Button text is two or three words, with no icon.
- **Button groups:** a set of buttons where one is chosen (a filter) 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 rest have `aria-pressed="false"` and `variant="outline"`. Not `Tabs`: there are no tab panels.
- **Inputs:** the shadcn `Input` with `className="h-11 rounded-md px-4 text-body md:text-body"` (44px tall), and the shadcn `Textarea` with `className="min-h-24 rounded-md px-4 py-2 text-body md:text-body"`. Their border is slate-grey and their fill is the card's, in both modes: in `components/ui/input.tsx` and `components/ui/textarea.tsx`, replace `border-input` with `border-muted-foreground`, 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 a field's error border matches the select's in dark mode. The stock `focus-visible:border-ring` and `aria-invalid:border-destructive` stay. Every field has a visible `<label>` above it with `block`, a label in `text-foreground`, tied to it with `htmlFor`, and the field has `mt-2`. A field that may be left empty says so in its label: `Phone (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 focus-visible:border-ring` and the focus outline from the rules (`focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`), so it stands beside an `Input` at the same height and with the same border. Unless this brief says it starts on a value, its first option is an empty choice in words ("Choose a room") with an empty `value`. It gets the same label, `mt-2` and error handling as any field. An `<option>` holds plain text.
- **Forms:** a form sits in a card, so its fields stand on the lighter cream. Each field sits in its own `<div>` with its label and the lines under it; these `<div>`s are `mt-6` apart (the first has none), and the button that sends the form sits in a `<div>` with `mt-6`. Where this brief puts two fields side by side, their two `<div>`s sit in one row `<div>` with `grid gap-6 md:grid-cols-2`, which takes the `mt-6` in their place; below `md` they stack, 24px apart.
- **Errors:** an error is a line under its field with `mt-2`: a `<p>` with an `id` and `flex items-start gap-2 font-mono text-label text-destructive`, not uppercased, holding the `CircleAlert` icon (`shrink-0`, no margin) and a sentence that says what to do. Where this brief puts fields and a button in one row, the error lines sit under the row. The field gets `aria-invalid` and names the line in `aria-describedby`, after any helper line 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. Render the error lines first and move focus after that render, in an effect or a `requestAnimationFrame`, so the field's description is there when it is read. Before the first send no error shows. 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. An email address is well formed when, trimmed, it matches `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`.
- **Confirmations:** a confirmation is plain text in `text-foreground`: a heading at the level this brief gives and a sentence or two, no colored box. It goes into an element with `role="status"` that is on the page from the start, empty, so that its message is announced. The form and the status element share one wrapper, the card, so the message stands where the form stood. The status element has no classes and no margin, sits after the form in the card's block flow, and is never hidden (no `hidden`, no `empty:hidden`), so empty it takes no space. The message in it is a `<div>` rendered only when there is a message, and the form is removed when it appears. A button that follows a confirmation sits after the status element in the card, not inside it. Give the message's heading `tabIndex={-1}` and move focus to it, because the button that had focus is gone.
- **Badges:** exactly one badge on a page, where this brief puts it, and it is the page's one use of sage: the shadcn `Badge` (`npx shadcn@latest add badge`) with `className="h-auto rounded-sm border-transparent bg-tertiary px-2 py-1 font-mono text-label font-normal uppercase text-tertiary-foreground"`. One to three words, no icon. Sage is a fill only: never `text-tertiary` or a sage border, and nothing else on the page is `bg-tertiary`. It sits on the paper or on a card, never on the clay block. Where a badge stands beside a label line, the two sit in a `<div>` with `flex flex-wrap items-center gap-x-4 gap-y-2`, the label line first.
- **Cards:** a card is a plain element with `rounded-xl border border-border bg-card p-6`: the lighter cream, 12px corners, 24px padding, a hairline and no shadow. Not the shadcn `Card`, which ships its own padding and a ring. Use one only where this brief asks for it. Never a card inside a card, and nothing with a border and corners inside a card except the fields and buttons of a form: divide a card's parts with space or a `border-t border-border`.
- **Pictures:** there are no image files. A picture is drawn as a plate: a plain `<div>` with `relative overflow-hidden rounded-xl border border-border bg-muted`, the aspect ratio this brief gives (`aspect-4/3` or `aspect-3/2`), `role="img"` and an `aria-label` that describes, in a sentence, the photograph it stands for. Inside it are two flat shapes in sand, each an empty `<div>` with no border, no corners and no text: the ground, a band across the foot of the plate (`absolute inset-x-0 bottom-0 bg-secondary`, with `h-1/3` unless this brief gives `h-1/4` or `h-1/2`), and the window, a rectangle above it (`absolute top-1/6 h-1/3 w-1/4 bg-secondary`, with `left-1/6` or `right-1/6` as this brief says). Nothing else is drawn in a plate: no icon, no text, no gradient, no third shape. Where this brief gives a caption, the plate sits in a `<figure>` with a `<figcaption>` after it, a label in `text-muted-foreground` with `mt-2`; where it gives none, the plate stands alone, with no `<figure>`. A plate has a border and corners of its own, so it never sits inside a card.
- **Rows of times and prices:** a list where each row has a name on the left and a figure on the right (hours, ways to get here, other rooms) is a `<ul>` with no markers and no padding. Each `<li>` is `flex items-baseline gap-2 py-2`: the name in `text-body` with `min-w-0`; then the hairline, an empty `<span>` with `aria-hidden="true"` and `min-w-4 flex-1 border-b border-border`, which runs from the name to the figure on the text's baseline; then the figure, a label in `text-foreground` with `shrink-0 whitespace-nowrap tabular-nums`. The hairline is plain, not dotted, and it is the only rule the list has: no `divide-y` and no border on the rows. The name may wrap; the figure never does.
- **Facts lists:** a `<dl>` of facts (bed, size, view, floor; the lines of a summary) has `divide-y divide-border border-y border-border`. Each row is a `<div>` with `flex items-baseline justify-between gap-4 py-2`: the `<dt>` is a label in `text-muted-foreground`, the `<dd>` is `text-body text-foreground text-right`. A size keeps its unit as written, `22 m²`: it is body text, never an uppercased label.
- **Tables:** the shadcn `Table` (`npx shadcn@latest add table`) with `className="text-body"`. Header cells are labels in `text-muted-foreground` with `h-auto px-4 py-2 font-normal`; body cells have `px-4 py-2 whitespace-normal`; the first and last cell of a row add `first:pl-0 last:pr-0`. Rows do not change on hover: `hover:bg-transparent` on every row. The stock `Table` wraps the `<table>` in a box that scrolls sideways; make it reachable with the keyboard even where the table fits: in `components/ui/table.tsx`, give that box `tabIndex={0}`, `role="region"`, an `aria-label` and the focus outline from the rules, drawn inside it (`focus-visible:-outline-offset-2`), and add a required `label` prop for the name. `label` and a visually hidden `<caption>` (`sr-only`) repeat the words of the heading above the table.
- **Rules:** every line is 1px and `border-border`: the header's, a section's, a list's, a card's. Hairlines and space divide a page, not boxes or colored bands.
- **Corners:** `rounded-sm` (2px) for the badge; `rounded-md` (4px) for buttons, inputs and selects; `rounded-xl` (12px, the `lg` corner in `DESIGN.md`) for cards, plates and the clay block. `rounded-lg` is 4px too, shadcn's name for the base corner; write `rounded-md`. Nothing is a pill or a circle: no badge, no button, no dot and no picture is round, and a shape drawn in a plate has no corners at all.
- **Icons:** from `lucide-react`, with `size-4`, `strokeWidth={1.5}` and `aria-hidden="true"`, only where this brief or these notes name one. Never emoji.
- **Surfaces:** flat, like a printed card. No shadows, blur, gradients or glows in the code you write. A stock component that floats over the page keeps the one soft shadow it ships with; these pages use none.
- **Motion:** in the code you write, the only transition is color, `transition-colors duration-150`. Nothing moves on scroll, nothing counts down and nothing flashes. Stock components keep the motion they ship with.
- **Writing:** plain and unhurried, by the two people who keep the house, as "we" to "you". Say prices per night, what is included and when to arrive in so many words. No superlatives, no exclamation marks, and nothing that presses the guest: no "only 1 left", no "book now".

## The page

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

Sample content, if the user gave none: requesting a room at **Kittiwake House**, the invented guesthouse described in the layout notes. Use its six rooms, the number each sleeps and their prices by season exactly as the notes' list gives them. A stay is 1 to 14 nights, and two nights or more in July and August. This is a request, not a payment: the house replies within a day, and the guest pays at the house on leaving. The sample stay, wherever this brief needs one, is Marram for 2 guests from Friday 14 May 2027 to Monday 17 May 2027: 3 nights at £152, £456 in all. Every name is invented.

Build these parts in order. This page needs `npx shadcn@latest add input textarea badge`.

### 1. Header

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

### 2. Opening

The first section, with `pt-12`, as a grid (`grid gap-6 md:grid-cols-12 md:items-start`):

- In columns 1–7 (`md:col-span-7`): a label line in `text-muted-foreground`, `Six rooms · We reply within a day`; with `mt-2` the `<h1>` in `text-h1 lg:text-display`, "Request a reservation", with no italic; with `mt-4` a `<p>` in `text-body`: "Tell us the days and the room you would like. Nothing is charged now: we write back to confirm, and you pay at the house when you leave."
- In columns 9–12 (`md:col-span-4 md:col-start-9`): the page's clay block, holding:
  - A label with no text color class, so it takes the block's ink: `How it works`.
  - With `mt-2`, an `<h2>` in `text-h2` (not the notes' `md:text-h1`: the block is narrow): "A request, not a payment".
  - With `mt-6`, an `<ol>` with no markers and no padding, three items, the second and third with `mt-4`. Each `<li>` is `flex items-baseline gap-4`: the step's number as a label with `tabular-nums`, `01`, `02`, `03`, then a `<p>` in `text-body`. The steps: "You send the days and the room you would like." "We check the room and write back within a day." "You pay at the house when you leave, by card or in cash."

### 3. Request and summary

A section with the hairline from the layout notes and the `aria-label` "Request and summary", as a grid (`grid gap-6 md:grid-cols-12 md:items-start`):

- In columns 1–7 (`md:col-span-7`): a card that is the wrapper for the form and the confirmation. It holds the `<form>` (with `noValidate` and `aria-labelledby` pointing at its heading) and, after it, the empty `role="status"` element.
- In columns 9–12 (`md:col-span-4 md:col-start-9`), with `md:sticky md:top-6`: the summary card of part 4. On a phone it comes after the form card.

The form, in this order. Each field sits in its own `<div>`, and rows and single fields are `mt-6` apart, as the Forms note says:

1. An `<h2>` in `text-h2` with an `id`: "Your request". It is the form's first element; the row after it has `mt-6`.
2. A row of two: **"Arrival date"**, an `Input` of type `date` with `min` set to today; and **"Departure date"**, an `Input` of type `date` with `min` set to the day after the arrival date, or to tomorrow while there is none. `min` only limits the picker: a date typed outside it still reaches the check.
3. A row of two: **"Room"**, a native `<select>` whose first option is the empty choice "Choose a room", followed by the six rooms in the notes' order, each written with the number it sleeps, `Thrift · sleeps 2`, `Campion · sleeps 1` and so on, with the values `thrift`, `campion`, `samphire`, `marram`, `sea-holly` and `tamarisk`; and **"Guests"**, a native `<select>` with the options `1 guest`, `2 guests`, `3 guests` and `4 guests` and the values `1` to `4`, which starts on `2 guests` and has no empty choice.
4. **"Name"**: an `Input` with `autoComplete="name"`.
5. A row of two: **"Email address"**, an `Input` of type `email` with `autoComplete="email"`; and **"Phone (optional)"**, an `Input` of type `tel` with `autoComplete="tel"`.
6. **"Time of arrival"**: a native `<select>` whose first option is the empty choice "Choose a time", followed by `15:00 to 16:00`, `16:00 to 17:00`, `17:00 to 18:00`, `18:00 to 19:00`, `19:00 to 20:00` and `Later than 20:00`. Under it, with `mt-2`, a `<p>` of helper text in `font-mono text-label text-muted-foreground`, a sentence, not uppercased, with an `id` the field names in `aria-describedby` at all times: "The door is open from 15:00 to 20:00. For a later arrival we leave a key in the porch.", each time a `<time>`.
7. **"Note (optional)"**: a `Textarea` with `maxLength={400}`. Under it, with `mt-2`, a helper line as above, named by the field: "An allergy, a cot, a late train: anything we should know."
8. In a `<div>` with `mt-6` and `flex flex-wrap items-center gap-x-6 gap-y-2`: the page's one default button, `type="submit"`, "Send request"; and beside it a `<p>` of helper text in `font-mono text-label text-muted-foreground`, not uppercased: "Nothing is paid now."

Where a field has both a helper line and an error line, the error comes after the helper, also with `mt-2`, and the field names both.

### 4. Summary

The summary card is an `<aside>` with `aria-labelledby` pointing at its heading. Inside it:

- A label line with the page's one badge beside it, in the row the Badges note gives: the label `Your stay` in `text-muted-foreground`, then the badge "Breakfast included".
- With `mt-2`, an `<h2>` in `text-h2` (the card is narrow): "Summary".
- With `mt-6`, a facts list from the layout notes, five rows: Arrive, Leave, Nights, Room, Guests. A date reads `Friday 14 May 2027`, in a `<time>`; the nights read `3 nights`; the room is its name, `Marram`; the guests read `2 guests`. A value that is still missing, and the nights while the two dates do not give at least one, reads "Not chosen yet", with `text-muted-foreground` on that `<dd>`.
- With `mt-6`, a second facts list, the price. It has one row for each season the stay has nights in, in the order the nights come: the `<dt>` reads `3 nights at £152` and the `<dd>` the sum, `£456`, as a label in `text-foreground` with `tabular-nums`. Its last row is the total: the `<dt>` reads `Total` and the `<dd>` the sum of the rows in `font-heading text-h2 tabular-nums`, `£456`. While the dates or the room are missing, or the dates do not give at least one night, the list has only the total row, and its `<dd>` reads "Not worked out yet" in `text-body text-muted-foreground`.
- With `mt-4`, a `<p>` of helper text in `font-mono text-label text-muted-foreground`, a sentence, not uppercased: "Breakfast is included. You pay at the house when you leave."

The facts lists have only hairlines, no corners, so they are not cards in a card. The summary is not a live region: it changes with every choice, and announcing each one would be noise.

### 5. Before you arrive

A section with the hairline, as a grid (`grid gap-6 md:grid-cols-12 md:items-start`):

- In columns 1–4 (`md:col-span-4`): a label line, `Good to know`; with `mt-2` an `<h2>`, "Before you arrive".
- In columns 6–12 (`md:col-span-7 md:col-start-6`): rows from the layout notes, four rows:
  - Arrive: `15:00–20:00`, two `<time>` elements
  - Leave by: `10:30`, a `<time>`
  - Change or cancel, free of charge: `Up to 7 days before`
  - Stays in July and August: `Two nights or more`

  Then, with `mt-4`, a `<p>` in `text-body`: "For anything else, write to stay@kittiwakehouse.example or call 01632 960 352.", the address a link in running text to `mailto:stay@kittiwakehouse.example` and the number one to `tel:+441632960352`.

### 6. Footer

The footer from the layout notes.

### Behavior

- **State:** keep every value in component state; the summary reads from the same state, so it follows each field as it changes. The name, the email address, the phone, the time of arrival and the note do not appear in the summary.
- **From the address:** when the page opens, read `arrive`, `leave`, `guests` and `room` from the query string of the address (`/reserve?arrive=2027-05-14&leave=2027-05-17&guests=2&room=marram`) and start the matching fields with them. Take a value only if it is well formed: a date as `YYYY-MM-DD` that is a real day, guests from `1` to `4`, a room that is one of the six values. Ignore anything else, and show no error for it. With no query string the form starts empty, with `2 guests`.
- **Dates:** "today" and "tomorrow" come from the visitor's clock. Work with the calendar dates as written: read the year, the month and the day from a field's value and count with `Date.UTC`, so that no time zone moves a day. The nights are the days from the arrival date up to, and not including, the departure date.
- **Price:** each night is charged at the room's price for the season of the date the night begins on. The season comes from that date's month: November to March is winter; April, May, June, September and October are spring and autumn; July and August are summer. So Marram from 29 June to 3 July 2027 is `2 nights at £152`, `£304`, then `2 nights at £178`, `£356`, and a total of `£660`. A sum of a thousand pounds or more has a comma: `£3,136`.
- **Sending:** check the fields in code, in the order of the form, and show one error per field, the first that applies:
  - Arrival date: empty gives "Choose the day you arrive."; a day before today gives "Choose today or a later day."
  - Departure date: empty gives "Choose the day you leave."; a day that is not after the arrival date gives "Choose a day after the day you arrive."; more than 14 nights gives "We take requests for up to 14 nights. For a longer stay, write to us."; a single night that begins in July or August gives "In July and August we take stays of two nights or more. Choose a later day."
  - Room: the empty choice gives "Choose a room."
  - Guests: more guests than the chosen room sleeps gives a line that names the room and its number: "Marram sleeps 2. Choose fewer guests or another room." Without a room this field has no error.
  - Name: empty gives "Write your name."
  - Email address: empty gives "Write your email address."; a malformed one gives "Check the email address: it needs an @ and a domain."
  - Phone: empty is fine. Anything but digits, spaces and one leading `+`, or fewer than 7 digits, gives "Write the number in digits, such as 01632 960 352, or leave it empty."
  - Time of arrival: the empty choice gives "Choose when you expect to arrive."
  - The note is never checked.
- **Errors and focus:** each error line follows the Errors note, and focus moves to the first field with an error. Two errors depend on a second field, and also go when that field changes: the departure date's when the arrival date changes, and the guests' when the room changes.
- **Confirmation:** when everything is good, the form is replaced in its card by the message in the status element: an `<h2>` in `text-h2`, "Thank you. We have your request", with `tabIndex={-1}` and focus moved to it. Then with `mt-4` a `<p>` in `text-body` that repeats the stay as the summary has it: "Marram for 2 guests, from Friday 14 May 2027 to Monday 17 May 2027: 3 nights, £456 in all.", each date a `<time>`. Then with `mt-4` a `<p>` in `text-body`: "We will write to ines@example.com within a day to say whether the room is free. Nothing has been paid.", with the email address as it was typed. Then with `mt-4` a label in `text-muted-foreground`: `Reference · KH-2705-014`, a fixed sample reference. The heading has no top margin: the message is the card's only content.
- **Again:** after the status element, in the card and only while the message shows, a `<div>` with `mt-6` holding a `secondary` button, "Make another request". It empties the status element, brings back the form as it starts with no query string and no errors, and moves focus to the arrival date. The summary follows, since it reads the same state.
- Nothing is sent anywhere: the confirmation is shown in the page. 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`, `12`, `26`) 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. This theme has no full rounds: do not use `rounded-full`, and give a small mark a corner under half its height or none, because a box whose corner is half its height or more is a full round however it is written.
- **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 use clay once on a page, for one block.
- Do say prices per night, what is included and when to arrive plainly.
- Do leave room: a heading has an empty column beside it.
- Don't set Cormorant in bold, and don't set body text in it.
- Don't count down, flash "only 1 left" or press the guest in any other way.
- Don't use gradients, glows 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.

- [ ] Every field has a visible label; the two dates are date fields, and the room, the guests and the time of arrival are native `<select>` elements.
- [ ] Sending the empty form gives the arrival date, the departure date, the room, the name, the email address and the time of arrival each their own error line with an icon, and focus moves to the arrival date.
- [ ] The summary works out the nights and the total as the dates and the room are chosen: Marram from 14 to 17 May 2027 reads 3 nights at £152 and a total of £456, and Marram from 29 June to 3 July 2027 shows two lines, £304 and £356, and a total of £660.
- [ ] The page says in words that this is a request and not a payment, on the block of clay, which is the page's only `bg-clay` element, has ink text in both modes and holds no link, button or field.
- [ ] A good request replaces the form, inside the same card, with a confirmation that repeats the room, the guests, the dates, the nights and the total; its heading takes focus and a screen reader announces it.
- [ ] Send request is the page's one default (slate) button and Breakfast included its one badge; no heading is bold, and nothing is italic or round.
- [ ] 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.
