# Build a product page with the Kiln theme

You are building one page in an existing project: one piece from a small ceramics studio's shop, with pictures, its price, a choice of glaze, a quantity, an add-to-basket button with a confirmation, its details, care notes and related pieces. 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 Kiln variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/kiln.json` and save https://strictthemes.com/themes/kiln/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/kiln/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:** Bricolage Grotesque for headings, DM Sans for body text, DM Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 60px/700, `text-h1` 38px/700, `text-h2` 24px/600, `text-body` 16px/400, `text-label` 12px/500.
- **Font per size:** a type class does not set the font family. Headings (`<h1>` to `<h6>`) get the heading font on their own; on any other element add the font class: `font-heading` with `text-display`, `text-h1`, `text-h2`; `font-mono` with `text-label`.
- **Spacing scale**, with the Tailwind step in brackets: 4px (`1`), 8px (`2`), 16px (`4`), 24px (`6`), 40px (`10`), 80px (`20`).
- **Corners:** `rounded-sm` 2px, `rounded-md` 4px, `rounded-xl` 10px (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 #963614, on-primary #FFF6EC, secondary #2A1D14, on-secondary #FBF6EE, tertiary #2F4A7A, on-tertiary #FBF6EE, neutral #6B5B4E, background #F4ECE1, surface #FBF6EE, foreground #2A1D14, border #DBCDBA, error #B3122D, success #2F6B3A, glaze-pale #F2EADC, glaze-dark #5A4132.

### How Kiln pages are laid out

- **Page:** the page is `bg-background`. Put `bg-background text-body text-foreground` on the outermost element you write.
- **Container:** `mx-auto max-w-[1120px] px-4 md:px-6`. The header's row, the footer's row and `<main>` itself carry it. This width is the one arbitrary value allowed.
- **Grid:** where this brief gives columns, the element it names is a 12-column grid, `grid gap-x-6 md:grid-cols-12`. It sets columns and side gaps only. Below `md` the columns stack; a column that comes under another gets `mt-10 md:mt-0`.
- **Vertical rhythm:** written in one way only. `<main>` also carries `flex flex-col gap-20 pt-10 pb-20`: each section is an item of that stack, and a section never gets a top or bottom margin of its own. Inside a section, every element after the first gets its own top margin: `mt-2` between a label and what it labels, `mt-4` between a heading and its paragraph and between paragraphs, `mt-2` between a title in `text-body font-medium` and the text under it, `mt-6` between a heading and the block it opens and between form fields, `mt-10` between blocks inside a section. Never use `space-y-*`. The one other source of vertical space is the `gap-*` or `gap-y-*` of a list or grid of like items, where this brief gives one; its items never get a top margin as well. In a column of a 12-column grid, the first element has no top margin, except the `mt-10 md:mt-0` above. A list "with no markers" needs no class: Tailwind's base styles remove markers and padding.
- **Section heading:** a section opens with an `<h2>` in `text-h2`. Where a link belongs to the whole section, the heading and the link share a row, `flex flex-wrap items-baseline justify-between gap-x-6 gap-y-2`, and the row is the section's first element.
- **Headings:** a page has one `<h1>`, in `text-h1` unless this brief says `text-display`. Headings are sentence case, with no full stop. A title in `text-body font-medium`, such as a product's name in a tile, also gets `font-sans`.
- **Header:** a `<header>` that runs the full width with a 1px bottom border (`border-b`). Inside it, one `<div>` is the container and the row: `flex flex-wrap items-center gap-x-4 gap-y-2 py-4 md:gap-x-6`. On the left the studio's name, a link to `/` in `font-heading text-h2`. Then a `<nav>` with the `aria-label` "Main" and `hidden md:block`, holding a `<ul>` with `flex gap-6` of four links, in this order: Shop (`/`), Tableware (`/tableware`), Vases (`/vases`) and Studio (`/studio`). At the right end, a wrapper `ml-auto flex items-center gap-4` holds the "Menu" button (below `md` only) and the basket link, `inline-flex items-center gap-2`: a `ShoppingBag` icon, the word "Basket" (`hidden md:inline`) and the count in brackets in `font-mono tabular-nums` (`Basket (3)`, `Basket (0)`), with an `aria-label` that says it in words ("Basket, 3 pieces", "Basket, empty"). It goes to `/basket`, on the basket page too. The link for the current page keeps its `href` and gets `aria-current="page"` and `underline`; mark one only where this brief names the current page. A link whose destination is not given is `#`.
- **Menus on small screens:** below `md` the nav links go behind a "Menu" button (`variant="outline"`) with `aria-expanded` and `aria-controls`, in the wrapper before the basket link. The button shows the links as a list inside the `<header>`, under its row: a second `<nav>` with `md:hidden`, the `id` that `aria-controls` names, and the `hidden` attribute while closed. It also has the `aria-label` "Main": the two are never shown at once. The second `<nav>` has a 1px top border across the full width; inside it a `<ul>` that is the container, with `py-2`, holds the same links, each a block with `py-2`. Escape closes it and moves focus to the "Menu" button. Do not use a `Sheet` or a `Dialog`.
- **Footer:** a `<footer>` that runs the full width with a 1px top border. Inside it, a `<div>` that is the container, with `py-10`, holds a row `flex flex-col gap-6 md:flex-row md:justify-between`. On the left the studio's name as plain text in `font-heading text-h2` and, with `mt-2`, the address as an `<address>` with `not-italic`, two lines split by a `<br />` (the name is not repeated): "Unit 3, Coldharbour Yard" and "Bramwick BW4 2LT". On the right a `<nav>` with the `aria-label` "Footer" holding a `<ul>` with `flex flex-col gap-2` of four links, styled as header links, in this order: Shop (`/`), Studio (`/studio`), Delivery and returns (`/delivery`) and Care of stoneware (`/care`). Under the row, with `mt-6`, a label line with the copyright, `© 2026` and the studio's name.
- **Labels:** captions, field labels, button and badge text, small print and the line of details under a product's name are labels: `font-mono text-label`. Nothing is uppercased, in the text or with a class. A label is a short fragment; only helper text and small print may be a short sentence. Other sentences are body text. A label on a line of its own is a `<p>`; one inside a line is a `<span>`.
- **Text color:** headings, body text, prices and field labels (also the `<p>` that names a button group or a quantity) are `text-foreground`. Other labels, captions and helper text are `text-muted-foreground`. A paragraph is never muted.
- **Prices and quantities:** a price is `font-mono tabular-nums` with the currency sign against the figure and two decimals (`£34.00`), in `text-foreground`, never in the primary color. A price in a tile, a list or a summary is `text-body`; the one large price on a product page and a basket's total are `text-h2 font-medium` and stay `font-mono`, in place of the rule's `font-heading` for `text-h2`. A quantity, a count and a piece's measurements are `font-mono tabular-nums` as well. Work out money in whole pence and format it with `Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP" })`.
- **Measure:** a paragraph of body text is at most `max-w-lg` wide.
- **Dates and times:** a date is written `6 November 2026` (`24 December` where the year is plain), a time `10:00`. Each is a `<time>` with a `dateTime` (`2026-11-06`, `10:00`), and a range is two; a year alone or "early December" is plain text. A date takes the font of the text around it (body, or mono in a label); a time is `font-mono tabular-nums`.
- **Links:** a link in running text is `text-primary underline underline-offset-4 hover:text-foreground` and inherits its size. Links in the header, the footer, a breadcrumb and a section heading's row are `text-foreground underline-offset-4 hover:underline`. Links get `transition-colors duration-150`.
- **Buttons:** the shadcn `Button` with `className="h-auto px-4 py-2 font-mono text-label"`. The default variant (terracotta) is for the one thing on a screen the visitor should press; a screen shows at most one, and this brief names it or says the page has none. Quiet actions are `variant="outline"`: the "Menu" button, the minus and plus of a quantity, "Remove", and the unchosen buttons of a group. Every other button is `variant="secondary"`. The stock secondary hover does not show in this theme: 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, and add nothing on the buttons. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A `w-full` button keeps the stock `whitespace-nowrap`: its label here fits at 375px. Only the minus and plus of a quantity, which their own click can disable, get `focusableWhenDisabled`, so that keyboard focus stays on them. They then carry `data-disabled` in place of `disabled`, so on a page with a quantity, and only there, add `data-disabled:pointer-events-none data-disabled:opacity-50` beside the stock `disabled:` classes in `components/ui/button.tsx`. A call to action that goes to another page is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "…")`. A button that holds only an icon is `size="icon"` with an `aria-label`; it keeps its stock size and padding, and takes none of the classes above.
- **Button groups:** a set of buttons where one is chosen (a category, a glaze, a delivery option) is a `role="group"` with `flex flex-wrap gap-2`. Its name is an `aria-label`; where this brief asks for a visible label above the group, the label is a `<p>` and the group points to it with `aria-labelledby`. The chosen button has `aria-pressed="true"` and `variant="secondary"`; the rest have `aria-pressed="false"` and `variant="outline"`.
- **Quantity:** a `role="group"`, named as this brief says, with `inline-flex items-center gap-2`: an `outline` button with `size="icon"`, the `Minus` icon and an `aria-label` ("One fewer"); the number in an `<output>` with `aria-live="polite"` and `inline-flex w-10 justify-center font-mono tabular-nums`, with a visually hidden "Quantity " (`sr-only`) before the figure, so a change reads "Quantity 3"; and a matching button with the `Plus` icon ("One more"). The minus is disabled at 1, the plus at the limit this brief gives.
- **Badges:** the shadcn `Badge` (add it with `npx shadcn@latest add badge`) with `className="h-auto rounded-sm border-transparent bg-tertiary px-2 py-1 font-mono text-label text-tertiary-foreground"`: one or two words. `text-tertiary-foreground` is the theme's own color for text on denim: keep it. At most one badge on a tile. Denim is a badge fill only.
- **Inputs:** the shadcn `Input` and `Textarea` with `className="h-auto px-4 py-2 text-body md:text-body"`. In `components/ui/input.tsx` and `textarea.tsx`, replace the stock `bg-transparent` and `dark:bg-input/30` with `bg-card`; `file:bg-transparent` in `input.tsx` stays. Every field has a visible `<label>` above it, a label in `text-foreground` with `block`, tied to it with `htmlFor`; the field has `mt-2`. Helper text under a field is a label in `text-muted-foreground` with `mt-2`.
- **Selects:** a choice from a list is a native `<select>` with `h-auto w-full rounded-lg border border-input bg-card px-4 py-2 text-body aria-invalid:border-destructive`. Set its width on a wrapper. It gets the same label, focus outline and error line as any field.
- **Errors:** an error is a line with `mt-2`: `flex items-start gap-2 font-mono text-label text-destructive`, holding the `CircleAlert` icon (`shrink-0`) and a sentence that says what to do. It sits under its field; where a field and a button share a row, it sits under the row. The field gets `aria-invalid` and points to the line with `aria-describedby`. Fields are checked in code when the form is sent, with `noValidate` on the `<form>`, and focus moves to the first field with an error. An error line is removed when its own field changes (on `input` or `change`); otherwise it stays until the next submit.
- **Confirmations:** a message that appears after an action goes into an element with `role="status"` that is on the page from the start, empty. It is never hidden (no `hidden` attribute, no `empty:hidden`), has no classes that draw or take space, and is never an item of a `gap-*` stack or row. The message is an element inside it, rendered only when there is a message, and it carries any margin: a row, `flex items-start gap-2 text-body`, with the `CircleCheck` icon in `text-success` and `mt-1`, and a sentence in `text-foreground`. A control that follows a message sits beside the status element, not in it. Where a message replaces a form, the form and the status element share one wrapper, and the wrapper carries the top margin this brief gives, so the message stands where the form stood; give the message `tabIndex={-1}` and move focus to it.
- **Stock and state:** a state is a mark and a word, side by side: `inline-flex items-center gap-2`. The mark is a `size-2 shrink-0 rounded-sm` square with `aria-hidden="true"`, `bg-success` for in stock and `bg-muted-foreground` for made to order or sold out; the word is a label in `text-foreground`. A state on a line of its own sits in a `<p>`, which carries its top margin. Never show a state by color alone, and never put text on `bg-success`.
- **Cards:** a card is a plain element with `rounded-xl border bg-card p-6`. Do not use the shadcn `Card`. Never put a card in a card; divide its parts with 1px borders.
- **Product tiles:** a grid of tiles is a `<ul>` with `grid grid-cols-2 gap-x-4 gap-y-10 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4`. Each tile is an `<li>` holding one `<a>` to the piece, with `flex h-full flex-col overflow-hidden rounded-xl border bg-card`: the picture at its top with a 1px bottom border and no corners of its own, then the text part with `p-4`. The text part holds the piece's name as an `<h3>` in `font-sans text-body font-medium`, with `mt-1` its glaze as a label in `text-muted-foreground`, and with `mt-2` a row `flex flex-wrap items-center justify-between gap-2` holding the price and, where there is one, the badge. The tile's link draws the focus outline inside it (`focus-visible:-outline-offset-2`). A piece's address is `/tableware/` (or `/vases/`) and its name in lower case, each run of other characters than letters and figures made one hyphen, none at the ends: `/tableware/tumbler-set-of-two`.
- **Pictures:** there are no image files. A picture of a piece is a plain element with `role="img"` and an `aria-label` that says what it shows, in a fixed aspect ratio (`aspect-square` or `aspect-4/5`), `bg-muted` (a theme color, as every semantic class is), with `flex items-center justify-center`. Inside it, the piece is drawn as one plain element with `border border-foreground/40` and square corners (no `rounded-*`): `bg-glaze-pale` for a pale glaze (Ash, Oat), `bg-glaze-dark` for a dark one (Iron, Slate). Both keep their color in dark mode. Its size: `h-1/2 w-2/5` for a tall piece (a mug, a cup, a beaker, a tumbler, a jug, a vase), `h-1/4 w-3/5` for a bowl, `h-1/6 w-3/4` for a plate or a platter. In a tile the picture has `aria-hidden="true"` in place of `role="img"`. A picture that stands on its own (not in a tile or a basket line) has `rounded-xl border` and is a `<figure>` with a `<figcaption>` under it with `mt-2`, a label in `text-muted-foreground`.
- **Description lists:** details and hours are a `<dl>`. Each row is a `<div>` with a 1px top border and `py-2`, `grid grid-cols-3 items-baseline gap-x-4`: the `<dt>` is a label in `text-muted-foreground`, the `<dd>` is `col-span-2` in `text-body`. The last row also has a 1px bottom border.
- **Avatars:** `size-10 rounded-full border bg-card`, with the person's initials centered in it in `font-heading text-body`, and `aria-hidden="true"`.
- **Icons:** from `lucide-react`, with `size-4` and `strokeWidth={1.5}`, only where this brief or these notes name one. An icon beside a word has `aria-hidden="true"`; an icon that stands alone sits in a button with an `aria-label`.
- **Corners:** `rounded-sm` (2px) for badges and state marks, `rounded-lg` (4px, the stock corner) for buttons, inputs and selects, `rounded-xl` (10px) for cards, tiles and pictures, `rounded-full` only for avatars. A drawn piece has square corners. No pills.
- **Primary color:** terracotta is never for any fill but the default button, nor for a heading, a price, an icon or a border. The focus color (`ring`) is terracotta too: the focus outline and a field's stock `focus-visible:border-ring` stay.
- **Surfaces:** flat. No shadows, no blur and no gradients in the code you write. A stock component that floats keeps its one short shadow.
- **Motion:** in the code you write, the only transition is color. Nothing moves on scroll or is sticky. Stock components keep the motion they ship with.

## The page

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

Sample content, if the user gave none: "Whitlow Pottery", a stoneware studio of two potters, Nell Varga and Tomas Reid, in Bramwick. This page sells the Fold bowl, £34.00: a low bowl for soup, noodles or fruit, 16 cm across and 6 cm high, holding 550 ml, 420 g, thrown in buff stoneware with an unglazed, sanded foot. It comes in three glazes: Iron (dark brown), 6 in stock; Ash (pale grey-white), 2 in stock; and Slate (dark blue-grey), made to order and posted in about three weeks, at most 4 to an order. The price is the same in every glaze. This batch was fired on 6 November 2026.

Build these parts in order.

### 1. Header

The header from the layout notes. No link is the current page. The basket holds 3 pieces to start.

### 2. The piece

The first section of `<main>`, with the `aria-label` "Fold bowl". It opens with a breadcrumb, and under it with `mt-6` a 12-column grid with `md:items-start` holding two columns.

**Breadcrumb.** A `<nav>` with the `aria-label` "Breadcrumb" holding an `<ol>` in one row, `flex flex-wrap gap-2 font-mono text-label text-muted-foreground`: a link "Shop" to `/`, a `/`, a link "Tableware" to `/tableware`, a `/` and "Fold bowl" with `aria-current="page"`. The links are styled as header links and so are `text-foreground`; each `/` has `aria-hidden="true"`, and the slashes and "Fold bowl" stay muted.

**Pictures**, columns 1–7. A large picture and, with `mt-4`, two smaller ones side by side, `grid grid-cols-2 gap-4`. Each is a `<figure>` as the Pictures note says; the picture element inside it carries `rounded-xl border bg-muted` and the aspect ratio, `aspect-square` for the large one and `aspect-4/5` for the small ones. They show the bowl from the side (`h-1/4 w-3/5`), from above (`size-3/5`, a square like every drawn piece: the theme allows no round shape this large) and its foot (`h-1/4 w-2/5`), in the chosen glaze as the Pictures note says: `bg-glaze-dark` for Iron and Slate, `bg-glaze-pale` for Ash. Each `aria-label` names the view and the glaze ("Fold bowl in iron glaze, seen from above"), and each caption says the view in words: "From the side", "From above", "The unglazed foot".

**Buying**, columns 8–12 (`md:col-span-5 md:col-start-8`). Below `md` this column comes under the pictures with `mt-10 md:mt-0`. In order:

- The `<h1>` in `text-h1`: "Fold bowl".
- With `mt-2` a label in `text-muted-foreground`: "Stoneware · 16 cm across".
- With `mt-4` the price, `£34.00`, as the Prices note says for the one large price.
- With `mt-4` one paragraph of two sentences about the bowl.
- With `mt-6` the glaze: a visible label "Glaze" (a `<p>`), then with `mt-2` a button group named by it, with Iron, Ash and Slate. Iron is chosen to start.
- With `mt-4` the stock line, a state in a `<p>` as the notes say: "In stock, 6 left" or "In stock, 2 left" with a success mark, or "Made to order, posted in about 3 weeks" with a neutral mark.
- With `mt-6` the quantity, as the Quantity note says: a visible label "Quantity" (a `<p>`), then with `mt-2` the group, named by it with `aria-labelledby`, its buttons "One fewer" and "One more".
- With `mt-6` the default button "Add to basket", `w-full`, with the `ShoppingBag` icon before the words. This is the page's one terracotta button.
- Then a wrapper `<div>` holding, first, a link "View basket" to `/basket`, styled as a header link, in a block element with `mt-2`, rendered once something has been added; and after it, beside the link and not around it, the status element for the confirmation, as the Confirmations note says. The status element comes last, so clearing it moves nothing above it. The message inside it has `mt-4`: "Added 2 Fold bowls in iron glaze to your basket." It does not replace a control, so focus stays on the button.

### 3. Details and care

A `<section>` with the `aria-label` "Details and care", a 12-column grid.

- Columns 1–7: the `<h2>` "Details", then with `mt-6` a `<dl>` as the Description lists note says, with seven rows: Size (`16 cm across, 6 cm high`), Holds (`550 ml`), Weight (`420 g`), Clay (Buff stoneware), Glaze (the chosen glaze), Fired (6 November 2026, a `<time>` with `dateTime="2026-11-06"`), Foot (Unglazed and sanded smooth). The `<dd>` of Size, Holds and Weight is `font-mono tabular-nums` as a whole, units included; the other four are body text, the date too, as the Dates note says.
- Columns 9–12: a card, as the Cards note says, with the `<h2>` "Care", then with `mt-4` a `list-disc` list with `pl-6`, `mt-2` between its items, of four: safe in the dishwasher and the microwave; not for the oven or the hob; let it cool before it meets cold water; a fine crackle in Ash glaze is part of the glaze, not a crack. Below `md` the card comes under the list with `mt-10 md:mt-0`.

### 4. Goes well with

The section heading row from the layout notes: the `<h2>` "Goes well with" and the link "See all tableware" to `/tableware`. Then with `mt-6` the grid of product tiles from the Product tiles note, with four tiles (`aspect-square` pictures): Breakfast bowl, Ash, £26.00; Serving bowl, Ash, £72.00, badge "Last one"; Nest of three bowls, Slate, £58.00; Moor mug, Ash, £28.00. Each links to the piece's address, as the Product tiles note says: `/tableware/nest-of-three-bowls`. Add the `Badge` with `npx shadcn@latest add badge`.

### 5. Footer

The footer from the layout notes.

### Behavior

- Keep the glaze, the quantity and the basket count in component state. The glaze sets the pictures' fills and labels, the stock line, the quantity's limit and the Glaze row of the details.
- The quantity starts at 1. Its limit is the stock for Iron (6) and Ash (2); Slate is made to order and has no stock, so its limit is the order limit, 4. "One fewer" is disabled at 1 and "One more" at the limit; both get `focusableWhenDisabled`. Choosing a glaze whose limit is below the quantity sets the quantity to that limit.
- "Add to basket" adds the quantity to the header's count (`Basket (5)` after adding 2) and updates the link's `aria-label`. It writes the confirmation into the status element: clear it first and write the new sentence on the next frame, so that adding the same thing twice is read out twice. The sentence uses the singular for one ("Added 1 Fold bowl…"). The quantity goes back to 1.
- The "Menu" button opens and closes the small-screen menu and sets `aria-expanded`; Escape closes it as the notes say.
- The tiles and the other links need no behavior.

## Rules

- **Colors:** only the theme's colors, through the semantic classes (`bg-background`, `text-foreground`, `bg-card`, `bg-primary`, `text-primary-foreground`, `text-muted-foreground`, `border-border` and so on). No hex, rgb or named colors in components, and no Tailwind palette classes such as `bg-blue-500` or `text-gray-600`.
- **Spacing:** padding, margin and gap use only the scale's steps (`1`, `2`, `4`, `6`, `10`, `20`) plus 0, 1px and 2px. No arbitrary values such as `p-[18px]`, and no other steps such as `p-3` or `gap-5`. `auto` margins are fine. Widths and heights such as `w-64` or `size-4` are sizes, not spacing. This applies to the code you write: a stock component keeps the spacing inside it, unless this brief changes it.
- **Type:** every piece of text you write gets its size from one of `text-display`, `text-h1`, `text-h2`, `text-body`, `text-label`, set on the element or on a parent it inherits from. Put `text-body` on the outermost element of the page you write. No other sizes. A stock shadcn component keeps the size it ships with, unless `DESIGN.md` or this brief gives that component its own style. A type class does not replace a weight class: where a stock component ships `font-medium` and the text should be regular, add `font-normal`.
- **Fonts:** only the theme's fonts, through `font-sans`, `font-heading` and `font-mono`. Do not add a font.
- **Corners:** only the `rounded-*` classes, which the theme already sets. No arbitrary radius, and `rounded-full` only where `DESIGN.md` or this brief names a full round, such as an avatar or a status dot. The theme allows a full round on a box up to 64px across; Strict Check reports a larger one, and a pill counts by its height.
- **Contrast:** all text meets WCAG AA against its background.
- **Stock components:** a component from `components/ui`, and an `<a>` styled with `buttonVariants`, keeps the hover, pressed and disabled colors and the motion it ships with, unless this brief changes them. They are the theme's colors under other names.
- **Counts:** a number with a noun reads right at one: "1 set", not "1 sets". Where this brief gives the words for only one of the two, write the other the same way.
- **Sizes from data:** a height or a width that comes from data (a chart bar, a meter) is set with an inline `style`. That is the one use for an inline style.
- **Focus:** every link and control shows a 2px outline on keyboard focus: `focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`. Keep `focus-visible:outline-solid` in it: in Tailwind 4 an `outline-none` on the same element, which stock components carry, otherwise hides the outline. An element that only gets focus from code (a heading or a message with `tabIndex={-1}`) gets the same outline written with `focus:` in place of `focus-visible:` (`focus:outline-solid focus:outline-2 focus:outline-offset-2 focus:outline-ring`): after a mouse click, a browser does not match `:focus-visible` on an element that code focuses. A link, a button or a field that code focuses keeps the `focus-visible:` classes it has: after a mouse click it shows no outline, which is how a browser treats every control, and is right. A stock component that ships its own focus ring (`Button`, `buttonVariants`, `Input`, `Textarea`, `Checkbox`, `Switch`, `AccordionTrigger`, the close button of a `Sheet`) must lose that ring. Do it once, in the component's file in `components/ui`, which is yours to change: replace its `focus-visible:ring-*` classes with the outline classes, and in a field replace its `aria-invalid:ring-*` classes, the `dark:` ones included, with `aria-invalid:ring-0`, or a field with an error shows a tinted ring. `Button` has two more sets. Its base classes also carry `aria-invalid:ring-*` classes: replace them with `aria-invalid:ring-0`, as in a field. Its `destructive` variant carries `focus-visible:ring-*` classes of its own, the `dark:` one included: delete them and put nothing in their place, because the base classes already give every variant the outline. Wherever this rule says replace, change the classes in the file and do not add `ring-0` beside them: an added class only wins where it comes later and passes through `cn`. The stock `focus-visible:border-ring` can stay. These ring changes go only into the files of components that this page uses and that can take focus on it: a `Badge` that is not a link keeps its ring classes. The `cn` import from the setup steps is another matter and goes into every file. A box that scrolls sideways (a code block, a wide table) gets `tabIndex={0}`, `role="region"`, an `aria-label` and the same outline, also at widths where it happens to fit. Where a focusable box touches the edges of a parent that has `overflow-hidden`, draw its outline inside it: `focus-visible:-outline-offset-2` in place of `focus-visible:outline-offset-2`.
- **Not allowed:** purple or multi-color gradients; emoji used as icons or bullets; glows and colored shadows.
- **Avoid:** a card inside a card.
- **Dark mode:** the page must work with the `dark` class on `<html>`. Because every color comes from a semantic class, this needs no extra code; do not write `dark:` color overrides. A new shadcn project follows the system's mode and switches on the `d` key: leave that as it is, and add no mode switch of your own. If the page has a native `<select>` or a date or time field, tell the browser the mode, or its list and its picker stay light in dark mode: in the global CSS, give `:root` `color-scheme: light` and `.dark` `color-scheme: dark`.

From the theme's own do's and don'ts:

- Do let the clay background show: most of a page is paper.
- Do use a terracotta fill once per screen, for the thing you want pressed.
- Do set prices and quantities in the mono font.
- Don't use pure black or pure white. The ink is brown and the paper is clay.
- Don't use gradients, glows or drop shadows on cards.
- 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.

- [ ] Choosing a glaze changes the pictures' text alternatives, the stock line and the glaze in the details, and the chosen button has `aria-pressed="true"`.
- [ ] The quantity cannot go below 1 or above what is in stock, and a minus or plus button disabled at its limit keeps keyboard focus.
- [ ] The "Add to basket" button is the page's only terracotta button; pressing it raises the count on the header's basket link and shows a confirmation that a screen reader announces.
- [ ] The details are a description list (`<dl>`), and the price, the quantity and every measurement are in mono figures.
- [ ] 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.
