# Build a guidance page with the Civic theme

You are building one page in an existing project: a page of plain-language guidance for a public service, with a contents list, short sections, a list, a table of fees, one important notice and related links. 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 guidance "Resident parking permits: who can get one and what it costs", updated on 4 November 2026. Permits are for homes in zones A to D, last 12 months, and at most two are given per address. Prices for 12 months, first permit and second permit at the same address: petrol or diesel £96 and £144, hybrid £72 and £108, fully electric £30 and £45. Visitor permits are £24 for a book of 20 day permits. From 1 April 2027, petrol and diesel permits go up to £102 and £150. Homes at Linden Yard, built without parking, cannot get a permit. Use these facts as they are; 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. Title

The first element of `<main>`:

- A caption: `Guidance`.
- With `mt-2`, the `<h1>`: "Resident parking permits: who can get one and what it costs". The page's `<title>` is these words then " – Callowmere Council", here in place of the note's.
- With `mt-6`, one paragraph of two sentences: what a permit is and who this page is for.
- With `mt-4`, a label: a `<p>` in `font-mono text-label text-muted-foreground`, "Updated" and the date in a `<time>` with a `dateTime`.

### 3. Contents

With `mt-10`: a `<nav>` with `aria-labelledby` naming its heading, `border-l-4 border-border pl-4`. Inside it an `<h2>` "Contents" in `font-mono text-label text-muted-foreground`, here styled as a label in place of the Headings note's `text-h2`, with no top margin, then with `mt-2` a link list from the layout notes with one link for each of the five sections below, in their order.

### 4. Body

Five sections. They are not new parts: no `<section>` around them and no `mt-16`. Each is an `<h2>` with an `id`, `scroll-mt-6` and the `mt-10` from the layout notes, followed by its content, all directly in `<main>`. Paragraphs are `text-body`, at most three sentences each.

**Who can get a permit.** One paragraph: whether you can get one depends on where you live, on the vehicle and on how many permits your address has. Then with `mt-4` a sentence "You can get a permit if:" and with `mt-4` a bulleted list of three conditions: you live in zone A, B, C or D; the vehicle is registered to you at your address, or is a company car you are the main driver of; and your address has fewer than two permits. Then with `mt-4` a paragraph: homes at Linden Yard cannot get a permit, because they were built without parking. Then with `mt-4` a paragraph "Not sure which zone you live in? Find your permit zone.", in which "Find your permit zone" is the text link, a sentence of its own, with its full stop outside the link.

**What a permit costs.** One paragraph: the price depends on what the vehicle runs on and on whether it is the first or the second permit at the address. Then:

- An `<h3>` "Prices for a 12-month permit", with the `mt-6` from the layout notes.
- With `mt-4`, here in place of the `mt-2` under an `<h3>`, a wrapper `<div>` around the fee table: the shadcn `Table` as the layout notes set it, with the `label` "Prices for a 12-month permit". Three columns, "Vehicle runs on", "First permit" and "Second permit"; the two price columns and their headers are right-aligned (`text-right tabular-nums`). Three rows: petrol or diesel, hybrid, fully electric, with the prices from the sample. Add it with `npx shadcn@latest add table`.
- With `mt-6`, one paragraph: visitor permits cost £24 for a book of 20 day permits. It ends with the sentence "Read about visitor parking permits.", in which "visitor parking permits", lower case, is the text link.
- With `mt-6`, the notice from the layout notes, the only one on the page: from 1 April 2027 petrol and diesel permits cost £102 for a first permit and £150 for a second. A permit bought before then keeps its price until it ends. The date is a `<time>`.
- With `mt-6`, one paragraph: if you get council tax support, your first permit costs half the price.

**What you need to apply.** A sentence "You will need:" and with `mt-4` a bulleted list of three things: the vehicle's registration number, the date you want the permit to start, and proof of address from the last 3 months, such as a council tax bill, a tenancy agreement or a utility bill. Then with `mt-4` a paragraph: for a company car, a letter from your employer that says you are the main driver. Then the page's main action, in a `<p>` with `mt-10`: an `<a href="#">` styled as a default button, `cn(buttonVariants(), "h-11 w-full px-6 font-mono text-label md:w-auto")`, "Apply for a permit", with the `ArrowRight` icon after the words.

**How long a permit lasts.** Two paragraphs: a permit lasts 12 months from its start date; a reminder email comes 4 weeks before it ends, and you renew online in the same way you applied.

**If something changes.** A sentence, then with `mt-4` a bulleted list of three cases, each one or two sentences: you change vehicle (update the registration online, free); you move house (cancel the permit; you get back the full months left, less a £10 fee); you sell the vehicle and do not replace it (cancel it the same way).

### 5. Related

A new part of the page: a `<section>` (not an `<aside>`, which inside `<main>` would be a landmark nested in a landmark) with `mt-16 border-t border-border pt-6`. Inside it an `<h2>` "Related" with no top margin, then with `mt-4` a link list of three links: "Apply for a resident's parking permit", "Visitor parking permits", "Pay or challenge a parking fine". Each goes to `#`; the first has the words and the destination of the header's service name, which is right: both lead to the service's start page.

### 6. Footer

The footer from the layout notes.

### Behavior

- The contents links jump to their headings. Nothing else on the page needs behavior; every other link goes to `#`.
- On a phone the fee table scrolls sideways inside its box, the button fills the column, and the long `<h1>` wraps.

## 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 contents list has one link for each `<h2>` section of the body, and each link jumps to its heading.
- [ ] The fee table has one header row, its amounts are right-aligned, and its scroll box is named "Prices for a 12-month permit".
- [ ] There is exactly one signal-yellow notice, with dark text, the word "Important" beside its icon, and no link or muted text inside it.
- [ ] The page has one civic-blue main action, "Apply for a permit", which is a link styled as a button.
- [ ] Paragraphs are short (at most three sentences), and no text is muted except hints, captions and the updated date.
- [ ] 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.
