# Build a guesthouse rooms page with the Veranda theme

You are building one page in an existing project: the rooms page of a small guesthouse, with all six rooms as rows whose drawn pictures alternate sides, a control that shows the rooms for one, two or more guests, and a block of clay that says what every room includes. 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: the six rooms of **Kittiwake House**, the invented guesthouse described in the layout notes, at 9 Harbour Terrace, Port Lowan. Use the rooms, their facts and their prices exactly as the notes' list gives them, in that order, from the ground floor up. Each room also has one sentence, given below. Every name is invented.

Build these parts in order. This page needs `npx shadcn@latest add badge`, and it has a button group, so it makes the `outline` edit in `button.tsx` that the Buttons note describes.

### 1. Header

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

### 2. Opening

The first section, with `pt-12`, in columns 1–8 of a grid (`grid gap-6 md:grid-cols-12`, the `<div>` with `md:col-span-8`):

- A label line with the page's one badge beside it, in the row the Badges note gives: the label `Six rooms · From £78 a night` in `text-muted-foreground`, then the badge "Breakfast included".
- With `mt-2`, the `<h1>` in `text-h1 lg:text-display`: "Six rooms, no two alike", with the words "no two alike" in a `<span className="italic">`. This is the page's only italic.
- With `mt-4`, a `<p>` in `text-body`: "A price is for the room for one night, with breakfast for everyone in it. You can arrive from 15:00 and stay until 10:30 on the day you leave.", with each time in a `<time>`.

### 3. The rooms

A section with the hairline from the layout notes. Its heading is an `<h2>` with `sr-only`, "The rooms", which the section's `aria-labelledby` names; it comes first and takes no space.

**The control.** A `<div>` with `flex flex-wrap items-center justify-between gap-x-6 gap-y-4` and no top margin (it is the first thing seen under the hairline), holding:

- A button group from the layout notes with the `aria-label` "Show rooms for" and four buttons: "All rooms", "One guest", "Two guests", "Three or more". "All rooms" is chosen when the page opens.
- A `<p>` with `role="status"`, a label in `text-foreground`, saying how many rooms are shown: `Showing all 6 rooms`, `Showing 1 room for one guest`, `Showing 3 rooms for two guests` or `Showing 2 rooms for three or more guests`. Unlike the confirmations in the layout notes, it holds its first text from the start: it is a count that changes, not a message that replaces a form.

Under that row, with `mt-4`, a `<p>` of helper text in `font-mono text-label text-muted-foreground`, a sentence, not uppercased: "A guest travelling alone is welcome in any room for two, at the room's price."

**The list.** With `mt-12`, a `<ul>` with no markers and no padding, and `divide-y divide-border`, so a hairline falls between rooms and none above the first or below the last. Each `<li>` is a room row: `grid gap-6 py-12 first:pt-0 last:pb-0 md:grid-cols-12 md:items-center` (here `md:items-center` takes the place of the notes' `md:items-start`). A row has two items, the plate first in the code, then the text:

- **The plate**, with no caption, `aspect-4/3`, the plate itself the grid item.
- **The text**, a `<div>` in block flow:
  - A label in `text-muted-foreground`, the room's floor: `First floor`.
  - With `mt-2`, the room's name as an `<h3>` in `text-h2 md:text-h1` (larger than the notes' `<h3>`, because here the name stands beside a plate of seven columns).
  - With `mt-4`, the room's one sentence in `text-body`.
  - With `mt-4`, a `<p>` label in `text-foreground`, the room's three facts: how many it sleeps, its bed and its view, `Sleeps 2 · King bed · Sea view`.
  - With `mt-2`, a `<p>` label in `text-muted-foreground` with `tabular-nums`, the price per night: `From £128 a night`.
  - With `mt-6`, an onward link from the layout notes whose words name the room: "See Marram".

From `md` up the sides alternate, by the row's place in the list as it is shown, so they still alternate when the control hides some rooms:

- In the first, third and fifth row shown, the plate is in columns 1–7 (`md:col-span-7`) and the text in columns 9–12 (`md:col-span-4 md:col-start-9`).
- In the second, fourth and sixth, the text is in columns 1–4 (`md:col-span-4 md:col-start-1 md:row-start-1`) and the plate in columns 6–12 (`md:col-span-7 md:col-start-6 md:row-start-1`). Both need `md:row-start-1`, because the plate comes first in the code.

The six rooms, in this order. The plate's shapes belong to the room, not to its place in the list:

1. Thrift. `Ground floor`. "On the garden side, with a door of its own to the walled garden and no stairs to climb." `Sleeps 2 · Double bed · Garden view`, `From £104 a night`, "See Thrift" to `#`. Plate: the window at `right-1/6`, the ground `h-1/2`; `aria-label` "Thrift on the ground floor, its door open to the walled garden".
2. Campion. `First floor`. "The smallest room, with a writing desk at the window and the garden below." `Sleeps 1 · Single bed · Garden view`, `From £78 a night`, "See Campion" to `#`. Plate: the window at `left-1/6`; `aria-label` "Campion, a single bed and a desk at a tall window".
3. Samphire. `First floor`. "Two beds under a tall window that looks along the quay." `Sleeps 2 · Twin beds · Harbour view`, `From £112 a night`, "See Samphire" to `#`. Plate: the window at `right-1/6`, the ground `h-1/4`; `aria-label` "Samphire, twin beds under a window that looks along the quay".
4. Marram. `First floor`. "The bay window at the front of the house, with the sea in all three panes." `Sleeps 2 · King bed · Sea view`, `From £128 a night`, "See Marram" to `/rooms/marram`. Plate: the window at `left-1/6`; `aria-label` "The bay window of Marram, with the bed facing the sea".
5. Sea Holly. `Second floor`. "Up under the roof, with a daybed in the dormer for a third guest or an afternoon." `Sleeps 3 · King bed and a daybed · Sea view`, `From £142 a night`, "See Sea Holly" to `#`. Plate: the window at `right-1/6`; `aria-label` "Sea Holly under the roof, a daybed in the dormer window".
6. Tamarisk. `Second floor`. "Two rooms joined by a short landing, for a family or for four friends." `Sleeps 4 · King bed and two singles · Harbour view`, `From £164 a night`, "See Tamarisk" to `#`. Plate: the window at `left-1/6`, the ground `h-1/4`; `aria-label` "Tamarisk under the eaves, two rooms joined by a short landing".

### 4. In every room

A section that holds nothing but the clay block and its action, so it has `mt-12 md:mt-26` and no hairline, as the Sections note says. Its `aria-labelledby` names the block's heading. It is a grid (`grid gap-6 md:grid-cols-12 md:items-end`; here `md:items-end` takes the place of `md:items-start`, so the action lines up with the foot of the block):

- In columns 1–7 (`md:col-span-7`): the page's clay block, holding:
  - A label with no text color class, so it takes the block's ink: `Included in every price`.
  - With `mt-2`, an `<h2>`: "In every room".
  - With `mt-6`, a `<ul>` with no markers and no padding, `grid gap-x-6 gap-y-2 md:grid-cols-2`, of six items. Each `<li>` is `flex items-start gap-2` in `text-body`: the `Check` icon with `mt-1 shrink-0`, then the words. The items: "Breakfast on the veranda", "Linen sheets and wool blankets", "A kettle, loose tea and fresh milk", "Wifi through the house", "Free parking at the quay", "A key of your own, to come and go as you like".
- In columns 9–12 (`md:col-span-4 md:col-start-9`), on the paper: a `<div>` holding a label in `text-muted-foreground`, `No payment now`; with `mt-2` a `<p>` in `text-body`, "Send a request and we reply within a day."; and with `mt-6` a `<p>` holding the page's one default button, an `<a>` styled as a button: "Request a room", to `/reserve`.

On a phone the block comes first and the action under it.

### 5. Footer

The footer from the layout notes.

### Behavior

- Keep the chosen button in component state. "All rooms" shows all six. "One guest" shows the rooms that sleep 1 (Campion); "Two guests" the rooms that sleep 2 (Thrift, Samphire, Marram); "Three or more" the rooms that sleep 3 or 4 (Sea Holly, Tamarisk). The rooms keep their order.
- Hidden rooms are not rendered, so the hairlines between rows and the `first:` and `last:` paddings stay right, and the sides alternate from the first row shown.
- The status line updates with each choice. Choosing a button moves no focus: it stays on the button.
- In the sample every choice shows at least one room. With the user's own rooms, a choice that shows none puts, in place of the list and with `mt-12`, one line in `text-body`: "No room sleeps that many. Write to us and we will see what we can do."
- The control changes only what is shown; it does not change the address of 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.

- [ ] All six rooms are rows of one list, each with a drawn plate, the room's name in the heading font, a label line of three facts, a price per night and one link whose words name the room.
- [ ] From `md` up the plate stands on the left in the first, third and fifth row shown and on the right in the others, with one empty column between plate and text; on a phone the plate always comes first.
- [ ] Choosing One guest, Two guests or Three or more leaves only the rooms that sleep that many, the chosen button has `aria-pressed` set to true, and a status line says in words how many rooms are shown.
- [ ] The block that says what every room includes is the page's only `bg-clay` element, all its text is ink in both modes, and it holds no link or button; Request a room, the page's one default (slate) button, stands on the paper beside it.
- [ ] The `<h1>` has one phrase in italics and no heading is bold; Breakfast included is the page's one badge, and nothing on the page is a pill or a circle.
- [ ] 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.
