# Build an information page with the Voltage theme

You are building one page in an existing project: the festival's practical information, with how to get there, opening times in a table, what to bring and what to leave at home, access, and questions that open and close. 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 Voltage variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/voltage.json` and save https://strictthemes.com/themes/voltage/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/voltage/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:** Unbounded for headings, Familjen Grotesk for body text, Space Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 64px/900, `text-h1` 38px/800, `text-h2` 22px/700, `text-body` 17px/400, `text-label` 12px/700.
- **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`), 12px (`3`), 20px (`5`), 32px (`8`), 64px (`16`).
- **Corners:** `rounded-sm` 6px, `rounded-md` 14px, `rounded-xl` 22px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 14px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #0A0A0A, on-primary #FFE500, secondary #2B2BF0, on-secondary #FFFFFF, tertiary #FF4F9A, on-tertiary #0A0A0A, neutral #4A4838, background #FFE500, surface #FFFFFF, foreground #0A0A0A, border #0A0A0A, error #B00020.

### How Voltage pages are laid out

- **Page:** the outermost element of the page is a `<div>` with `min-h-screen bg-background font-sans text-body text-foreground`. The page carries the color: paint nothing else yellow, hover colors included.
- **Container:** `mx-auto max-w-[1200px] px-3 md:px-5`. This width is the one arbitrary value allowed. Sections fill its full width.
- **Grid:** where a section has panels side by side, it is a grid with `grid grid-cols-1 gap-5 md:grid-cols-12`, and each item spans the columns this brief gives it (`md:col-span-4` and so on). Below `md` every item takes the full width, in source order. Panels in one row stretch to the same height.
- **Vertical rhythm:** every vertical distance is a `gap-*` on a `flex flex-col` stack or on a grid, or a padding this brief names. No `mt-*`, `mb-*` or `space-y-*`, except the icon margin in the Icons note. The steps: `gap-1` between a value and the small line under it; `gap-2` between a label and the thing it labels, between a field and its error line, and between the buttons of a group; `gap-3` between a heading and the text under it, between paragraphs, between the items of a list and between buttons side by side; `gap-5` between blocks inside a panel, between form fields and between panels; `gap-8` between a section's heading row and its content; `gap-16` between sections.
- **Main:** the `<main>` element carries the container and the stack: `mx-auto max-w-[1200px] px-3 md:px-5 flex flex-col gap-16 py-8 md:py-16`. Each section is a `<section>` with `flex flex-col gap-8` and no padding, whatever its children. No background bands and no borders around sections.
- **Links and addresses:** the pages live at `/` (the event page), `/line-up`, `/tickets`, `/info` and `/artists/<slug>` for an act (`/artists/ossa-bloom`). A link this brief names without an address goes to the page it names; any other link with no destination is `#`.
- **Header:** a `<header>` that runs the full width of the window with `border-b border-border`. Inside it, a `<div>` carries the container and the row: `flex flex-wrap items-center gap-x-8 gap-y-3 py-3`. On the left the festival's name, "Tremor Days", a link to `/` in `font-heading text-h2 text-foreground` with no underline at rest and `hover:underline underline-offset-4`; then a `<nav>` with the `aria-label` "Main" and `hidden md:flex gap-5`, holding three links as labels, in this order and with no `<ul>`: Line-up (`/line-up`), Tickets (`/tickets`) and Info (`/info`). Then at the right end (`ml-auto`) the festival's dates as a label, `4–5 Dec 2026`, `hidden md:block`. A nav link is `text-foreground hover:underline underline-offset-4`. The current page's link has `aria-current="page"` and `underline decoration-2 underline-offset-4`; this brief says which one it is, if any. The header holds no button besides "Menu".
- **Menus on small screens:** below `md` a "Menu" button with `aria-expanded` and `aria-controls` sits at the right end of the row, with `ml-auto md:hidden`: the dates that carry the `ml-auto` from `md` up are hidden below it. It is a quiet button from the Buttons note. It shows the same three links, stacked, inside the `<header>`, under its row: a second `<nav>` with the same `aria-label` "Main", `md:hidden`, which carries the `id` that `aria-controls` names and has the `hidden` attribute while closed. The second nav has a 1px `border-t border-border` across the full width, and inside it a `<div>` with the container and `py-2` that holds the links directly, with no `<ul>`; each link is `block py-2`. It closes on Escape, which returns focus to the button. Do not use a `Sheet` or a `Dialog`.
- **Footer:** a `<footer>` that runs the full width with `border-t border-border`. Inside it, a `<div>` carries the container and the stack, `flex flex-col gap-3 py-8`: the festival's name as plain text in `font-heading text-h2`, one sentence in `text-body` saying what it is, and a label line with the place, the dates and the copyright (`Varsby Docks · 4–5 Dec 2026 · © 2026 Tremor Days`).
- **Headings:** Headings stay short: five words or fewer, sentence case, no full stop, and no single word longer than nine letters in an `<h1>`. The page's `<h1>` is `text-h1 md:text-display`: 38px on a phone, 64px from `md` up. It is the only `text-display` text on the page, unless this brief names another element that uses it. A section's `<h2>` is `text-h2 md:text-h1`. A heading inside a panel is an `<h2>` or `<h3>`, whichever keeps the order, in `text-h2`.
- **Section heading row:** a section opens with a row, `flex flex-wrap items-end justify-between gap-3`, holding its `<h2>` and, where this brief gives one, a link at the right end.
- **Labels:** dates, times, prices, stage names, field labels, nav links, captions, table headers, and button and badge text are labels: `font-mono text-label font-bold uppercase`, in `text-foreground`. The `font-bold` holds the weight inside stock components. A label is a short fragment. Inside a label line, stage names (Crane Stage) and the festival's name are uppercased with the rest; in the header's and the footer's name, a heading and a sentence they keep their case. Only the name of a person or an act keeps its case in a label line: put it in a `<span>` with `normal-case`.
- **Small print:** helper text, the note under a total, a count such as `3 of 10 sets`: `font-mono text-label text-muted-foreground`, never uppercased, fragment or sentence. A count in small print adds `tabular-nums`. A caption is a label in `text-muted-foreground`. Paragraphs are never muted, and nothing inside a button or a field is muted.
- **Numbers and dates:** a date, a time, a price or a count that stands on its own is `font-mono tabular-nums`. A large number, the one figure a panel or a block is about, is `font-heading text-h1 tabular-nums`. A range is two `<time>` elements joined by an en dash: `<time dateTime="2026-12-04">4</time>–<time dateTime="2026-12-05">5 Dec 2026</time>`, and `17:30–18:15` the same way with full date-times. Every date and clock time that stands on its own or sits in a label, a heading or a table is a `<time>` with a `dateTime` (`2026-12-04T16:00+01:00`, or `2026-12-04` for a day), the dates in the header and the footer included; a year alone (the copyright) and a button's text stay plain. A number in running text (a sentence, or a line of a list in `text-body`) is plain text in that text's font; a date or a time there is a `<time>` only where this brief says so. Dates come day first, with no leading zero: in a label the short form (`Fri 4 Dec`, `4–5 Dec 2026`), in a heading, a sentence or a table cell the long form (`Friday 4 December`). Times are 24-hour (`16:00`). Nothing on these pages depends on today's date except the countdown, which reads the clock.
- **Text on the page and on panels:** short text may sit straight on the yellow page: headings, labels, one line of facts, and at most two sentences of body text. Anything longer goes on a panel. A paragraph is at most `max-w-xl` wide.
- **Panels:** a panel is a plain element with `flex flex-col gap-3 rounded-xl border border-border bg-card p-5`. Where this brief gives a panel another gap, it replaces `gap-3`. Do not use the shadcn `Card`. Never put a panel inside a panel: divide a panel's parts with a 1px `border-t border-border` and the panel's gap.
- **Panels with rows:** a panel that holds a list of rows edge to edge has no padding of its own, `gap-0` and `overflow-hidden`. Its heading row sits in a wrapper with `px-5 pt-5 pb-3` and a 1px `border-b border-border`. Each row has `px-5 py-3` and a 1px `border-t border-border`, except the first.
- **Links:** a text link is `text-foreground underline underline-offset-4 hover:decoration-2` and inherits its size. No link changes color, so a link has no transition. Links are never blue or pink.
- **Focus:** a link or a `<select>` you write also gets the outline from the Focus rule, which the class lists here leave out; a stock component gets it in its own file.
- **Buttons:** the shadcn `Button` with `className="h-auto px-5 py-2 font-mono text-label font-bold uppercase"`, keeping its stock `rounded-lg` (14px). The default variant (black with yellow text) is for the one main action, and a screen shows at most one. The second action is `variant="secondary"` (electric blue, white text). In `components/ui/button.tsx`, in the secondary variant, replace the class `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]` with `hover:bg-secondary/80`, since the stock hover cannot be told from the resting color; add no hover class where you use the variant. Quiet actions, the "Menu" button included, are `variant="outline"`: white with a black outline on the page and on a panel. In the same file, in the outline variant, replace `bg-background` (the page's yellow) with `bg-card`; replace `hover:bg-muted`, `aria-expanded:bg-muted` and `dark:hover:bg-input/50` with `hover:bg-accent`, `aria-expanded:bg-accent` and `dark:hover:bg-accent` (`bg-muted` is yellow too); and delete `dark:bg-input/30`, which would beat `bg-card` in dark mode and leave the button see-through. Keep `dark:border-input`. A button that holds only an icon is `size="icon"`, quiet, with an `aria-label`. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A button that keeps the focus while its own click disables it gets `focusableWhenDisabled`; this brief names each one. Base UI then sets `data-disabled` and not `disabled`, so on a page that uses the prop, 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 }), "…")`, because `buttonVariants` alone does not merge classes. A button or a link with `inline-flex` that is a direct child of a `flex-col` stack stretches to the stack's width: give it `self-start` unless this brief makes it full width.
- **Button groups:** a set of buttons where one is chosen (a day) is a `role="group"` with `flex flex-wrap gap-2`, named by an `aria-label`, or `aria-labelledby` where this brief gives it a visible label. The chosen button has `aria-pressed="true"`, `variant="secondary"` and the `Check` icon before its text, so the fill is not the only sign; the rest have `aria-pressed="false"` and are quiet buttons. Not `Tabs`: there are no tab panels.
- **Badges:** the shadcn `Badge` with `className="h-auto rounded-sm border-transparent bg-tertiary px-2 py-1 font-mono text-label font-bold uppercase text-tertiary-foreground"`: hot pink with black text, one or two words, no icon. At most one badge on a page. Any other state is a word.
- **Inputs:** the shadcn `Input` with `className="h-auto px-3 py-2 text-body md:text-body"`. It keeps its stock `rounded-lg` and black `border-input` outline. Give it the same fill as a select: in `components/ui/input.tsx`, replace the class `bg-transparent` with `bg-card` and delete `dark:bg-input/30`; `file:bg-transparent`, `aria-invalid:border-destructive` and `dark:aria-invalid:border-destructive/50` stay. Fields sit on panels. Every field has a visible `<label>` above it with `block`, tied to it with `htmlFor`, styled as a label; a field and its label are a stack with `gap-2`. No placeholder text.
- **Selects:** a native `<select>` with `h-auto w-full rounded-lg border border-input bg-card px-3 py-2 text-body text-foreground aria-invalid:border-destructive`. Set a width on an element around it, not on the `<select>`. It gets a label and an error line like any field.
- **Errors:** an error is a line under its field, in the field's stack: `flex items-start gap-2 font-mono text-label text-destructive`, not uppercased, holding the `CircleAlert` icon (`shrink-0`) and a sentence that says what to do. 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`); an error that depends on a second field is also removed when that field changes. Every other error stays until the next submit.
- **Confirmations:** a message that replaces a form goes into an element with `role="status"` that is on the page from the start, empty; one that arrives with its text is often not announced. The status element has no classes that draw or take space, and is never hidden. The form, the status element and any button that follows the message share one wrapper, a plain `<div>` with no gap, so the empty status element adds no space. Inside the status element, rendered only with the message, an element carries the message's layout; its first element has `tabIndex={-1}` and gets focus, because the button that had focus is gone. A button after the message sits in the wrapper, outside the status element, in a `<div>` with `pt-3` rendered only with the message. The theme has no success color: a confirmation is `text-foreground`, with the `Check` icon and words that say what happened.
- **Tables:** the shadcn `Table` with `text-body` on it. Header cells are labels in `text-muted-foreground`, with `px-3` and their stock height. Body cells get `px-3 py-2`. Add `first:pl-0 last:pr-0` to both, since a table here sits in a padded panel. Rows are divided by 1px borders; in `components/ui/table.tsx` remove the row's yellow fills: `hover:bg-muted/50`, `has-aria-expanded:bg-muted/50` and `data-[state=selected]:bg-muted`. The table scrolls sideways in its own box: in the same file give the scroll box `tabIndex={0}`, `role="region"` and an `aria-label`, and add a required `label` prop for the name. The visible name is the heading above the table's panel; no `<caption>`.
- **Pictures:** there are no image files. A picture is a plain element with an aspect ratio (`aspect-square` or `aspect-video`), `rounded-xl border border-border bg-foreground`, `role="img"` and an `aria-label` that says what it shows. Anything drawn inside it is `bg-background`, with no corners, or text in `text-background`. A picture with a caption is a `<figure>` with `flex flex-col gap-2`, its `<figcaption>` a caption.
- **Avatars:** a circle, `size-12 shrink-0 rounded-full bg-foreground`, holding the act's initials in `font-mono text-label font-bold text-background`, centered with `flex items-center justify-center`, with `aria-hidden="true"`, since the name stands beside it.
- **Icons:** from `lucide-react`, with `size-4` and the default 2px stroke. Use them only where this brief or these notes name one. An icon beside a word has `aria-hidden="true"`. In an `items-start` row beside `text-body`, an icon gets `mt-1` so it lines up with the first line; beside a label it needs none.
- **Corners:** `rounded-sm` (6px) for badges and checkboxes, `rounded-lg` (14px) for buttons, inputs and selects, `rounded-xl` (22px, the `lg` corner in `DESIGN.md`) for panels and pictures, `rounded-full` only for avatars and countdown dots.
- **Colors that are not text:** `bg-secondary` (blue) fills buttons only. `bg-tertiary` (pink) fills the one badge only. Never use `text-secondary`, `text-tertiary` or `text-primary`: text is `text-foreground`, `text-muted-foreground`, `text-destructive`, or the foreground class of the fill it sits on.
- **Surfaces:** flat. No shadows in the code you write, offset "sticker" shadows included, no glow, no blur and no gradients. Every line is 1px `border-border`. A stock menu or dialog keeps the short shadow it ships with.
- **Motion:** in the code you write, the only transition is color. Nothing moves on scroll, nothing pulses or blinks, and a number that changes just shows its new value. Stock components keep the motion they ship with, such as the `Button`'s 1px nudge while pressed.

## The page

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

Sample content, if the user gave none: **Tremor Days**, a two-day music and design festival at Varsby Docks, Kajgatan 2, in the invented city of Varsby, on Friday 4 and Saturday 5 December 2026. The gates open at 16:00 on Friday and at 12:00 on Saturday, the last set ends at 00:30 on Friday and at 01:00 on Saturday, and last entry is at 22:00. The docks are a 12-minute walk from Varsby Central station, and tram 4 stops at the gate. Most of the site is indoors in old harbour halls; the Crane Stage is outdoors under a roof. All names and places are invented; the contact address is `info@tremordays.example`.

Build these parts in order, under the header from the layout notes, in which Info is the current page.

### 1. Page heading

A section with no heading row. Its stack has `gap-3`:

- A stack with `gap-2`: the label `Tremor Days · 4–5 Dec 2026`, its dates the range from the Numbers note, then the `<h1>` "Before you go".
- The address, straight on the page: an `<address>` with `not-italic` in `font-heading text-h2`, "Varsby Docks, Kajgatan 2, Varsby".

### 2. Getting there

A section. Its heading row holds the `<h2>` "Getting there". Then the grid from the layout notes with three panels, each `md:col-span-4`. Each panel is the stock panel with `gap-3`: an `<h3>` in `text-h2` ("By train", "By tram", "By bike"), a label line with the one fact that matters most (`12 min walk from Varsby Central`, `Tram 4 · Docks Gate stop`, `400 bike stands at the north gate`), and two sentences in `text-body` with the details. The trains and trams run late after both nights: the last train to Varsby Central leaves at 01:40 and the last tram 4 at 01:55. In those sentences the two times are `<time>` elements in the sentence's font, with the time alone as their `dateTime` (`01:40`), since it holds after both nights.

### 3. Opening times

A section. Its heading row holds the `<h2>` "Opening times".

One panel across the full width, with `gap-5`, holding the shadcn `Table` (add it with `npx shadcn@latest add table`) as the Tables note sets it up, with the `label` "Opening times". Columns: Day, Gates open, First set, Last set ends, Last entry. Two rows:

- Friday 4 December: 16:00, 17:00, 00:30, 22:00.
- Saturday 5 December: 12:00, 13:00, 01:00, 22:00.

The header cells are `TableHead` with `scope="col"`. The day in each row is a `<th scope="row">` with the body cells' padding (`px-3 py-2 first:pl-0`), `whitespace-nowrap align-middle`, as the stock `TableCell` has, plus `text-left text-body font-bold`, and its date in a `<time dateTime="2026-12-04">`. Every time is a `<time>` with a full `dateTime` and the offset, in `font-mono tabular-nums`; a set that ends after midnight ends on the next date (`2026-12-05T00:30+01:00`, `2026-12-06T01:00+01:00`). The stock cells do not wrap, so on a phone the table is wider than its panel and scrolls inside its box.

Under the table, in the panel's stack, small print: "Weekend wristbands can leave and come back until 22:00."

### 4. What to bring

A section. Its heading row holds the `<h2>` "What to bring". Then the grid with two panels, each `md:col-span-6`. Each panel is the stock panel with `gap-5`:

- An `<h3>` in `text-h2`: "Bring" and "Leave at home".
- A `<ul>` with `flex flex-col gap-3`, no markers and no padding, the six items below, each a row with `flex items-start gap-2` in `text-body`. In the first list each item starts with the `Check` icon, in the second with the `X` icon, both `shrink-0 mt-1` (from the Icons note) and `aria-hidden="true"`: the heading says which list is which. Bring: your ticket on your phone or printed, a photo ID if you are 13 to 17, earplugs, a warm layer for the outdoor stage, a card or phone to pay (the site takes no cash), an empty bottle to fill. Leave at home: glass, large bags (bigger than 30 × 20 cm), umbrellas with a point, drones, your own food beyond a snack, professional cameras with a removable lens.

### 5. Access

A section. Its heading row holds the `<h2>` "Access". One panel across the full width, with `gap-5`:

- Two paragraphs in `text-body` in a stack with `gap-3`, each at most `max-w-xl`: the site is step-free on hard ground, and a free ticket for a companion comes with any ticket on request.
- A list in running text, `list-disc pl-5` with `flex flex-col gap-3` in `text-body`, six items: a raised viewing platform at both music stages, a quiet room in the Drawing Office open all day, accessible toilets at every bar, sign language interpreters at every talk, assistance dogs welcome, and last, which sets use strobe lighting: Ossa Bloom and Sister Antenna.
- A sentence with a text link to the email address: "Write to info@tremordays.example and we answer within two working days." The address is the link, `mailto:` the same address.

### 6. Questions

A section. Its heading row holds the `<h2>` "Questions". One panel across the full width, with `gap-0`, holding the shadcn `Accordion` (add it with `npx shadcn@latest add accordion`) with six items and the `multiple` prop (`<Accordion multiple>`), so more than one may be open at a time; Base UI's default is one.

- Each trigger gets `className="items-center gap-3 py-3 font-sans text-body font-bold"`, which replaces its stock size and weight. Keep its stock corner, its hover underline and its chevron icons.
- Each content gets `className="pb-3 text-body"`. Every answer is one short paragraph of one to three sentences, so the stock space between paragraphs never applies.
- Items keep the stock line between them, a 1px `border-border` line.
- Each trigger sits in the stock `<h3>`. A question is not a headline: the Headings note's five-word limit does not apply to it.

The questions: Can I leave and come back? What happens if it rains? Is there a cloakroom? Can I bring a camera? Can I change the name on my ticket? Do children need a ticket? Write plain, direct answers that agree with the rest of this page: re-entry until 22:00, most stages indoors, a cloakroom for €3, phone and compact cameras are fine, names can be changed until 1 December, under-13s go free with an adult.

### 7. Footer

The footer from the layout notes.

### Behavior

- The `Accordion` opens and closes an item on a click, on Enter and on Space, which the stock component does; do not add your own key handling. Each trigger keeps `aria-expanded`, set by the component.
- The table's box is reachable with Tab at every width, also where it happens to fit.
- The email link and the menu button are the only other interactive parts. Besides the menu's open state and the `Accordion`'s own, the page has no state.

## 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`, `3`, `5`, `8`, `16`) plus 0, 1px and 2px. No arbitrary values such as `p-[18px]`, and no other steps such as `p-6` or `gap-10`. `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 keep the page yellow and put content on white panels.
- Do set one short headline very large.
- Do outline every panel and field in black.
- Don't put small body text straight on yellow for long passages: use a panel.
- Don't add gradients, glows or hard offset shadows.
- 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 opening times are a real table with header cells, every time is a `<time>` element, and on a phone the table scrolls inside its own box, which the keyboard can reach.
- [ ] The lists of what to bring and what to leave at home say so in their headings, so that their icons are not the only sign.
- [ ] Every question in the FAQ opens and closes with Enter and with Space, and shows the focus outline.
- [ ] Every passage longer than two sentences sits on a white panel, not straight on the yellow page.
- [ ] 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.
