# Build a check answers page with the Civic theme

You are building one page in an existing project: the last step of a public-service form, listing the answers so far with a link to change each one, the price, a declaration to tick and the button that sends the application. 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 Civic variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/civic.json` and save https://strictthemes.com/themes/civic/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/civic/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:** Public Sans for headings, Atkinson Hyperlegible Next for body text, Atkinson Hyperlegible Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 48px/800, `text-h1` 34px/700, `text-h2` 24px/700, `text-body` 18px/400, `text-label` 16px/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`), 64px (`16`).
- **Corners:** `rounded-sm` 3px, `rounded-md` 5px, `rounded-xl` 8px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 5px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #0B3D91, on-primary #FFFFFF, secondary #0B1320, on-secondary #FFFFFF, tertiary #FFD400, on-tertiary #0B1320, neutral #414B5A, background #FFFFFF, surface #F2F5F9, foreground #0B1320, border #7B8696, error #A30F0F, success #0B5F33.

### How Civic pages are laid out

- **Container:** one column for everything a person reads or fills in: `mx-auto max-w-[720px] px-4 md:px-6`. This width is the one arbitrary value allowed. The header's row, `<main>` and the footer's row each sit in their own copy of this container, and no content is wider: only the header's border and the footer's border and fill run the full width.
- **Page structure:** the page's outermost element has `bg-background text-body text-foreground`. Inside it, in this order: the skip link, the `<header>`, a `<main>` with the `id` `main`, and the `<footer>`. `<main>` is the container with `pt-10 pb-16`. The page's `<title>` is the `<h1>`'s words, then " – Apply for a resident's parking permit – Callowmere Council", unless this brief gives another.
- **Skip link:** the first element on the page is an `<a href="#main">` "Skip to main content" with `sr-only focus-visible:not-sr-only focus-visible:block focus-visible:px-4 focus-visible:py-2 text-primary underline underline-offset-4`, so it shows only when it has focus, in the `text-body` of the page's outermost element.
- **Header:** a `<header>` that runs the full width with `border-b border-border`. Inside it, one container `<div>` is itself the row: the container classes plus `flex flex-wrap items-baseline gap-x-6 gap-y-1 py-4`. It holds the council's name, a link to `/` in `font-heading text-h2 text-foreground no-underline hover:underline underline-offset-4`, and the service's name, a link to `#` in `text-body font-bold text-foreground no-underline hover:underline underline-offset-4`. These are its only two links, in this order, and neither is marked as the current page (no `aria-current`). On a phone the service name wraps under the council's name. There is no nav, no "Menu" button and no logo.
- **Footer:** a `<footer>` that runs the full width with `border-t border-border bg-card`. Inside it, one container `<div>` with `py-10` holds a `<nav>` with the `aria-label` "Footer"; the `<ul>` in the nav has `flex flex-wrap gap-x-6` and holds four links (Accessibility, Privacy, Cookies, Contact the council), styled as a link list. Under the nav, with `mt-6`, a `<p>` in `text-muted-foreground` with the postal address "Callowmere Council, Guildhall, Market Street, Callowmere CM1 1AA", and with `mt-2` another, also muted, with `© Callowmere Council 2026`.
- **Vertical rhythm:** written as a top margin on each element, never as `space-y-*` and never as `gap-*` on a column of content (a flex row's `gap` is fine). Keep the column in normal block flow, not flex or grid. The first element in `<main>`, and the first element inside a box, has no top margin. Then:
  - Text that follows text (a paragraph or a list after a paragraph): `mt-4`.
  - The first text under the `<h1>`: `mt-6`.
  - An `<h2>`: `mt-10`, and the text under it `mt-4`. An `<h3>`: `mt-6`, and the text under it `mt-2`.
  - A box (a box, the notice, the error summary, a summary list, a table) after text, and text after a box: `mt-6`.
  - A form after the text above it: `mt-10`. Inside a form, each field, and each fieldset, sits in a wrapper `<div>` with `mt-6`, except the first. The row of buttons that ends a form has `mt-10`.
  - A main action on its own (a "Start now" link) sits in a `<p>` with `mt-10`.
  - A part this brief calls a new part is a `<section>` (or the element this brief names) with `mt-16` and `aria-labelledby` naming the `<h2>` that opens it, which has no top margin.
  - Where this brief gives an element its own top margin, it replaces the one from this list. Nothing has a bottom margin: the next element's top margin is the gap.
- **Caption over the `<h1>`:** where this brief gives one (a step number, a page type), it is a `<p>` in `font-mono text-label text-muted-foreground`, and the `<h1>` under it has `mt-2`.
- **Headings:** the `<h1>` is `text-h1`, and `text-display` from `md` up only where this brief says so. An `<h2>` is `text-h2`. An `<h3>` is `text-body font-bold`. Sentence case. Only `<h1>` to `<h6>` get the heading font on their own: any other element given `text-h1` or `text-h2` (an amount, a reference number) needs its font class written out, `font-heading` or the `font-mono` this brief names.
- **Labels:** captions, dates that stand on their own, reference numbers and button text are labels: `font-mono text-label`, sentence case, never uppercased. A label may be a fragment or a short sentence. A field's `<label>` and a `<legend>` are not labels: see Fields.
- **Text color:** headings, body text, field labels, legends and values are `text-foreground`. Hints, captions and dates under a heading are `text-muted-foreground`. Muted text only sits on `bg-background` or `bg-card`, never on `bg-muted`, `bg-tertiary`, `bg-primary` or `bg-secondary`. Running text is never muted. Every text and background pair these notes and the briefs allow meets AAA in both modes, so no pair needs measuring; a pair they do not name is not allowed.
- **Links:** a text link is `text-primary underline underline-offset-4 hover:text-foreground transition-colors duration-150` and inherits its size. A link's words say where it goes without the sentence around it, never "click here". A full stop after a link sits outside it. A link whose destination this brief does not name goes to `#`. The class lists in these notes and briefs (the skip link, the header's links, the Back link, text links, link lists) leave out the focus outline: add the outline classes from the rules to every one of them, and to each native radio input.
- **Link lists:** a list of links that stand on their own (contents, related links, the footer) is a `<ul>` with no markers and no padding. Each link is `inline-block py-2`, which makes it 44px tall, and the items have no margin between them.
- **Buttons:** the shadcn `Button` with `className="h-11 w-full px-6 font-mono text-label md:w-auto"`. Every button and field gets `h-11` (44px; stock ones are 32px). Button text is sentence case and says what happens ("Save and continue"), never "Submit" or "OK" alone. The default variant (civic blue) is the page's one main action. Any other button is `variant="secondary"` (blue-black ink). Never use the `destructive`, `ghost` or `link` variant. 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 is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "…")`, because `buttonVariants` alone does not merge classes. An `ArrowRight` icon goes after the words; any other icon goes before them. A button keeps the stock `whitespace-nowrap` and `justify-center`: every label here fits on one line in its box at 375px, and a button that fills the column has its words in the middle.
- **Button hovers:** in `components/ui/button.tsx`, change the default variant's `hover:bg-primary/80`, which leaves its text under 7:1, to `hover:bg-primary/90`; and replace the secondary variant's `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]`, which cannot be told from its resting color here, with `hover:bg-secondary/80`. Change each class in the file, once, not by adding a class beside it, so the links styled with `buttonVariants` get it too.
- **No disabled buttons:** no button on these pages is ever disabled. Pressing it checks the form and says what is missing.
- **Fields:** every field is a wrapper `<div>` holding, in this order:
  - A `<label>` with `block` and `htmlFor`, in `text-body font-bold text-foreground`: the question, in sentence case, with no colon.
  - Where this brief gives one, a hint with `mt-1`: a `<p>` in `text-muted-foreground` with an `id`.
  - Where there is an error, the error line with `mt-2`.
  - The control with `mt-2`. Its `aria-describedby` names the hint and, while there is one, the error line.
  - While it has an error, the wrapper also gets `border-l-4 border-destructive pl-4` (for a fieldset, the wrapper `<div>` around it, never the `<fieldset>` itself).
- **Inputs:** the shadcn `Input` with `className="h-11 text-body md:text-body"`, plus the width this brief gives: a field for a short value is about as wide as the value. They keep the border and the fill they ship with, in dark mode too. No placeholders, as a label, a hint or an example. Set `autoComplete` to the right token, or to `off` where there is none, and `spellCheck={false}` on codes and registration numbers.
- **Fieldsets:** a group of controls that answer one question (radios, a date in three fields) is a `<fieldset>` with `m-0 border-0 p-0`, inside its wrapper `<div>`, with a `<legend>` with `p-0` as its first child, in `text-body font-bold text-foreground`. Its hint follows with `mt-1`, then the error line while there is one, with `mt-2`. The fieldset's `aria-describedby` names the hint and the error line. The red line of an error goes on the wrapper, as the Fields note says.
- **Radios:** native `<input type="radio">` elements, not the shadcn `RadioGroup`, which is round. Each option is a row with `mt-2`, `flex min-h-11 items-center gap-4`, holding the input with `size-6 shrink-0 accent-primary` and its `<label>` in `text-body`, not bold: it is an answer, not the question. All options share a `name`.
- **Checkboxes:** the shadcn `Checkbox`. It ships at 16px with a 4px corner: in `components/ui/checkbox.tsx`, change its `size-4` to `size-6` and its `rounded-[4px]` to `rounded-sm`, and make the check icon inside it `size-5`: change the indicator's `[&>svg]:size-3.5` to `[&>svg]:size-5` (where the size sits on the icon instead, change it there). Its row is `flex min-h-11 items-start gap-4`, with `mt-0.5` (2px, an extra value the spacing rule allows) on the checkbox so it lines up with the first line of its label, and the label in `text-body`, not bold.
- **Errors:** an error line is a `<p>` with an `id`, `flex items-start gap-2 text-body font-bold text-destructive`. It holds two children: the `CircleAlert` icon (`mt-1 shrink-0`, `aria-hidden="true"`), and one `<span>` that holds a visually hidden `<span className="sr-only">Error: </span>` followed by a sentence that names the field and says what to do: "Enter your vehicle's registration number", not "This field is required". The field gets `aria-invalid`; the stock `aria-invalid` border stays. Fields are checked in code when the form is sent, with `noValidate` on the `<form>`. An error line, with its field's `aria-invalid` and red line, is removed when its own field changes (on `input` or `change`; for a date, any of its parts); an error that depends on a second field is also removed when that field changes. Every other error, the error summary and the title's prefix stay until the next send.
- **Error summary:** after a form is sent with errors, a box appears before the page's caption: `rounded-xl border-2 border-destructive bg-background p-6`, with `tabIndex={-1}` and `aria-labelledby` naming its title. Inside it the title, "Your answers need a change": a `<p>` with an `id` in `font-heading text-h2`, not a heading element, so that the page's headings still start at the `<h1>`. Then with `mt-4` a `<ul>` with no markers of one link per error, in page order, each with the same words as its error line, in `font-bold text-destructive underline underline-offset-4`, `mt-2` between items. It follows the Back link with `mt-6`, and the caption after it keeps its `mt-6`. Focus moves to the box, which shows the outline the rules give an element that only code focuses. A link in it moves focus to its field in code with `focus({ preventScroll: true })`, then calls `scrollIntoView()` on the field's label (for a fieldset, its `<legend>`, also when focus goes to one of its parts): radios go to their first option, a date to its first wrong part. While the summary is shown, the page's `<title>` starts with "Error: ". Each send checks every field afresh; with nothing wrong, the summary and the prefix go away.
- **Confirmations:** a message that appears after an action goes into an element with `role="status"` that is on the page from the start, empty: one that arrives together with its text is often not announced. It is never hidden (no `hidden` attribute, no `empty:hidden`). The status element has no classes that draw or take space, no margin, and is never an item of a `gap-*` row: the message inside it, a `<p>` in `text-body text-foreground` rendered only while there is a message, carries the margin and the type. Buttons and links stay outside it.
- **Boxes:** a plain element with `rounded-xl border border-border bg-card p-6`. Not the shadcn `Card`. Never a box inside a box.
- **The notice:** at most one per page, only where this brief asks for it: `rounded-xl bg-tertiary p-6 text-tertiary-foreground`. It opens with a `<p>` with `flex items-start gap-2 font-bold` holding the `TriangleAlert` icon (`mt-1 shrink-0`, `aria-hidden="true"`) and the word "Important", then the message as one `<p>` with `mt-2`. Nothing in it is muted, and nothing in it is a link or can take focus: the link color and the focus outline cannot be read on yellow. `bg-tertiary` is used for nothing else, and `text-tertiary` never.
- **Lists:** a list in running text is `list-disc` or `list-decimal` with `pl-6`, and `mt-2` between its items. The sentence before it ends with a colon.
- **Dates and amounts:** a date is a `<time>` with a `dateTime`, written in full ("1 December 2026"); a range is two `<time>` elements. "Today" is the sample date this brief gives, fixed in code, not the device clock, so the page behaves the same on any day. In a sentence it is body text; on its own line it is a label. A date inside an error line, a summary link or a status message is plain text, not a `<time>`. Amounts have the pound sign and no pence ("£96"). Phone numbers are written in groups ("01632 960 418"); a `tel:` link's `href` has the digits with no spaces (`tel:01632960418`).
- **Tables:** the shadcn `Table` with `className="min-w-lg text-body"`, so it scrolls in its box on a phone. Header cells get `h-auto px-4 py-2 whitespace-normal font-bold text-foreground`; body cells `px-4 py-2 align-top whitespace-normal`; the first and last cell of each row `first:pl-0 last:pr-0`, so the table lines up with the text. Rows are divided by 1px borders and do not change on hover: `hover:bg-transparent` on every row, the header row included. No text in a table is muted. Numbers and their header are right-aligned with `tabular-nums`. Edit `components/ui/table.tsx`: give its scroll box `tabIndex={0}`, `role="region"` and an `aria-label`, and add a required `label` prop for the name. No `<caption>`, which would scroll sideways with the table: an `<h3>` above names it, and the `label` repeats its words.
- **Icons:** from `lucide-react`, only where this brief or these notes name one, `size-5` with `strokeWidth={2}` unless this brief gives another size. Every icon has a word beside it and is `aria-hidden="true"`. No button holds only an icon.
- **Corners:** `rounded-sm` (3px) for checkboxes, `rounded-lg` (5px) for buttons and inputs, `rounded-xl` (8px) for boxes, the notice and the error summary. No pills and no `rounded-full`: nothing on these pages is round.
- **Surfaces:** flat. No shadows, no blur and no gradients in the code you write.
- **Motion:** in the code you write, the only transition is color. Nothing moves on scroll. Stock components keep the motion they ship with.

## The page

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

Sample content, if the user gave none: "Callowmere Council", an invented town council, and its service "Apply for a resident's parking permit". This page is step 3 of 3 for the applicant Priya Okafor: 14 Wharf Row, Callowmere CM4 7QJ, permit zone B, email priya.okafor@mail.example, phone 07700 900 214, proof of address "Council tax bill, uploaded 9 November 2026". Her vehicle: registration KX19 TDV, a Velmar Arden, petrol or diesel, the permit to start on 1 December 2026. As a first permit for a petrol or diesel vehicle it costs £96 for 12 months, 1 December 2026 to 30 November 2027, paid after approval. Write the rest of the copy yourself, plain and short.

Build these parts in order.

### 1. Header

The header from the layout notes: "Callowmere Council" and "Apply for a resident's parking permit".

### 2. Back link

The first element of `<main>`: a `<p>` holding an `<a href="#">` "Back", `inline-flex min-h-11 items-center gap-2 text-primary underline underline-offset-4 hover:text-foreground transition-colors duration-150`, with the `ChevronLeft` icon before the word (`aria-hidden="true"`).

### 3. Error summary

The error summary from the layout notes, shown after a send that finds the declaration not ticked, until the next send, after the Back link with `mt-6`. It has one link, which moves focus to the checkbox.

### 4. Title

- A caption with `mt-6`: `Step 3 of 3`.
- With `mt-2`, the `<h1>` "Check your application".
- With `mt-6`, one paragraph: read your answers, and change any that are wrong before you send them.

### 5. Your details

An `<h2>` "Your details", then with `mt-6` a summary list: a `<dl>` with `border-t border-border`, of six rows: Name, Address, Permit zone, Email, Phone, Proof of address. The address is three lines in its `<dd>`, one `<span className="block">` per line.

Each row is a `<div>` with `border-b border-border py-4`, holding:

- The question as the `<dt>`, in `text-body font-bold`.
- The answer as a `<dd>` with `mt-1` and `break-words`.
- A `<dd>` with `mt-2` holding the "Change" link: a text link, `inline-flex min-h-11 items-center`, whose text is "Change" with the rest in a visually hidden span: `Change<span className="sr-only"> email address</span>`. The hidden words, row by row: name, address, permit zone, email address, phone number, proof of address; then vehicle registration number, make and model, what the vehicle runs on, permit start date. It goes to `#`.

From `md` up the row is a grid: `md:grid md:grid-cols-12 md:items-start md:gap-x-4`, where `md:items-start` keeps a long answer from stretching the link. The `<dt>` has `md:col-span-4`, the answer `md:col-span-6 md:mt-0`, and the link's `<dd>` `md:col-span-2 md:mt-0 md:text-right`. The row's `py-4` is the only space between rows.

### 6. Your vehicle

An `<h2>` "Your vehicle", then with `mt-6` a summary list like the one above, of four rows: Registration number, Make and model, Runs on, Permit start date. The start date is a `<time>` with a `dateTime`, in body text like every answer, not a label; so is the date in the proof of address answer.

### 7. What you will pay

A box with `mt-10`. Inside it:

- An `<h2>` "What you will pay", with no top margin.
- With `mt-2`, the amount as a `<p>` in `font-heading text-h1`: "£96".
- With `mt-2`, a `<p>`: "For 12 months, from 1 December 2026 to 30 November 2027", both dates in `<time>` elements.
- With `mt-4`, a `<p>`: you pay after the council approves the application, and it emails you a link to pay.

### 8. Declaration

A wrapper with `mt-10` holding the form and the status element. The form has `noValidate`:

- An `<h2>` "Declaration", with no top margin.
- With `mt-4`, one paragraph: giving false information to get a permit can mean it is cancelled with no refund.
- With `mt-6`, the checkbox field, which here departs from the Fields note's order, since its label sits beside the box: a wrapper that gets the red line while it has an error, holding the error line while there is one, with no top margin as the wrapper's first element, and the checkbox row from the layout notes, which has `mt-2` while the error line is above it. Add the checkbox with `npx shadcn@latest add checkbox` and change it as the layout notes say. The `<label>`: "I confirm that I live at 14 Wharf Row, that the vehicle is registered to me at this address, and that my answers are correct." Give the `Checkbox` an `id` and the `<label>` `htmlFor` with it, so a click on the words ticks the box. Give the `<label>` an `id` too, and the `Checkbox` `aria-labelledby` naming it: in a Base UI project the `Checkbox`'s `id` lands on a hidden input, and the visible box would otherwise have no name. The checkbox names the error line in its `aria-describedby` and gets `aria-invalid` while there is an error.
- The row of buttons with `mt-10`: the default `Button` "Submit application" with `type="submit"`, the only button on the page.

After the form, in the same wrapper, the status element from the layout notes; its message is a `<p>` with `mt-6`.

### 9. Footer

The footer from the layout notes.

### Behavior

- "Submit application" checks the declaration in code. If it is not ticked: the error line "Tick the box to confirm your answers are correct" shows beside the checkbox with the red line, the error summary appears with the same words as its link and takes focus, and the page's `<title>` starts with "Error: ".
- Ticking the box removes the error line, the red line and `aria-invalid`, as the Errors note says; the error summary and the title's prefix stay until the next send.
- A send with the box ticked removes the summary and the prefix, and the status element reads "In the full service, your application would be sent now and the confirmation page would open." Focus stays on the button. Nothing is sent anywhere.
- The "Change" links and the Back link go to `#`.
- On a phone each row stacks: question, answer, then the "Change" link, all left-aligned. The email address breaks if it has to.

## 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`, `16`) 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 AAA against its background.
- **Stock components:** a component from `components/ui`, and an `<a>` styled with `buttonVariants`, keeps the hover, pressed and disabled colors and the motion it ships with, unless this brief changes them. They are the theme's colors under other names.
- **Counts:** a number with a noun reads right at one: "1 set", not "1 sets". Where this brief gives the words for only one of the two, write the other the same way.
- **Sizes from data:** a height or a width that comes from data (a chart bar, a meter) is set with an inline `style`. That is the one use for an inline style.
- **Focus:** every link and control shows a 2px outline on keyboard focus: `focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`. Keep `focus-visible:outline-solid` in it: in Tailwind 4 an `outline-none` on the same element, which stock components carry, otherwise hides the outline. An element that only gets focus from code (a heading or a message with `tabIndex={-1}`) gets the same outline written with `focus:` in place of `focus-visible:` (`focus:outline-solid focus:outline-2 focus:outline-offset-2 focus:outline-ring`): after a mouse click, a browser does not match `:focus-visible` on an element that code focuses. A link, a button or a field that code focuses keeps the `focus-visible:` classes it has: after a mouse click it shows no outline, which is how a browser treats every control, and is right. A stock component that ships its own focus ring (`Button`, `buttonVariants`, `Input`, `Textarea`, `Checkbox`, `Switch`, `AccordionTrigger`, the close button of a `Sheet`) must lose that ring. Do it once, in the component's file in `components/ui`, which is yours to change: replace its `focus-visible:ring-*` classes with the outline classes, and in a field replace its `aria-invalid:ring-*` classes, the `dark:` ones included, with `aria-invalid:ring-0`, or a field with an error shows a tinted ring. `Button` has two more sets. Its base classes also carry `aria-invalid:ring-*` classes: replace them with `aria-invalid:ring-0`, as in a field. Its `destructive` variant carries `focus-visible:ring-*` classes of its own, the `dark:` one included: delete them and put nothing in their place, because the base classes already give every variant the outline. Wherever this rule says replace, change the classes in the file and do not add `ring-0` beside them: an added class only wins where it comes later and passes through `cn`. The stock `focus-visible:border-ring` can stay. These ring changes go only into the files of components that this page uses and that can take focus on it: a `Badge` that is not a link keeps its ring classes. The `cn` import from the setup steps is another matter and goes into every file. A box that scrolls sideways (a code block, a wide table) gets `tabIndex={0}`, `role="region"`, an `aria-label` and the same outline, also at widths where it happens to fit. Where a focusable box touches the edges of a parent that has `overflow-hidden`, draw its outline inside it: `focus-visible:-outline-offset-2` in place of `focus-visible:outline-offset-2`.
- **Not allowed:** purple or multi-color gradients; emoji used as icons or bullets; glows and colored shadows.
- **Avoid:** a card inside a card.
- **Dark mode:** the page must work with the `dark` class on `<html>`. Because every color comes from a semantic class, this needs no extra code; do not write `dark:` color overrides. A new shadcn project follows the system's mode and switches on the `d` key: leave that as it is, and add no mode switch of your own. If the page has a native `<select>` or a date or time field, tell the browser the mode, or its list and its picker stay light in dark mode: in the global CSS, give `:root` `color-scheme: light` and `.dark` `color-scheme: dark`.

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

- Do keep every text pair at 7:1 or better, in both modes.
- Do put a visible label on every field and a word beside every icon.
- Do say what went wrong and what to do next, in the error itself.
- Don't rely on color, position or shape alone to say something.
- Don't use placeholder text as a label, or light grey text for anything.
- Don't introduce a color, size or radius that is not a token above.

## Check before you finish

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

- [ ] The answers are description lists (`<dl>`), one `<dt>` and `<dd>` pair per answer, and on a phone each answer stacks under its question.
- [ ] Every "Change" link says to a screen reader what it changes ("Change vehicle registration number"), not just "Change".
- [ ] Sending without ticking the declaration shows the error summary with a link that moves focus to the checkbox, and an error line with an icon beside the checkbox; the button is never disabled.
- [ ] The checkbox and its label make a target at least 44px tall, and the checkbox has 3px corners.
- [ ] The price box shows the amount, the dates the permit covers and when payment is taken, with each date in a `<time>` element.
- [ ] 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 AAA 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.
