# Build an event page with the Voltage theme

You are building one page in an existing project: the opening page of a two-day festival, with a large headline, a countdown to the start, the first names on the bill, three facts and a call to buy tickets. 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, an old harbour in the city of Varsby. It runs on Friday 4 and Saturday 5 December 2026; the gates open on Friday at 16:00 (UTC+1). There are 19 acts on three stages: Crane Stage and Pump House for music, and the Drawing Office for design talks and workshops. A weekend ticket is €118, a day ticket from €64. All acts, people and places are invented: the first names on the bill are Ossa Bloom, The Night Ferries and Kettle Drum Union, then Lumen Varga, Sister Antenna, Pia Strand, Odd Weather, Gravel & Gold, Juno Halde and Neon Allotment.

Build these parts in order, under the header from the layout notes, which marks no link as the current page.

### 1. Hero

A section, but it opens with no heading row: the `<h1>` is its heading. Its stack has `gap-5`:

- A stack with `gap-2`: a label, `Music and design · Varsby Docks`, then the `<h1>` "Tremor Days".
- The dates and the place in `font-heading text-h2`, on one `<p>`: "4–5 December 2026 · Varsby Docks", the dates as the range from the Numbers note, "4" in a `<time dateTime="2026-12-04">` and "5 December 2026" in a `<time dateTime="2026-12-05">`. On a phone the line wraps; it never runs off the screen.
- One paragraph of at most two sentences, in `text-body` straight on the page, `max-w-xl`: what the festival is, in plain words.
- A row of two links styled as buttons, `flex flex-wrap gap-3`: "Buy tickets" (`/tickets`) in the default variant, and "See the line-up" (`/line-up`) in the `secondary` variant. This is the page's one default button.

### 2. Countdown

A section on the page, not on a panel: large numbers are part of the look. Its heading row holds an `<h2>` "Gates open in". The content is a stack with `gap-3`:

- The four units, in a row with `grid grid-cols-2 gap-5 md:flex md:items-center md:gap-8`: two by two on a phone, one line from `md` up. Each unit is a stack with `gap-1`: the number, two digits with a leading zero (`04`), in `font-heading text-h1 tabular-nums` below `md` and `md:text-display` from `md` up; under it its label, `Days`, `Hours`, `Minutes` or `Seconds`. These four numbers are the one other `text-display` text on the page. The days count may have three digits; it does not need a leading zero then.
- Between the units, from `md` up only, a separator: `hidden md:flex flex-col gap-2` holding two countdown dots, each `size-3 rounded-full bg-foreground`. Below `md` they are not shown, so the grid holds only the four units.
- The whole row of units and dots has `aria-hidden="true"`, which hides the separators too; they need no `aria-hidden` of their own. Beside it, a `<p>` with `sr-only` gives the same time in words: "12 days, 4 hours and 31 minutes until the gates open." It is not a live region: it changes once a minute and is read when someone reaches it.
- Under the row, a label line with the start as a `<time dateTime="2026-12-04T16:00+01:00">`: `Fri 4 Dec · 16:00`.

### 3. First names

A section. Its heading row holds the `<h2>` "First names" and, at the right end, a text link "Full line-up" (`/line-up`).

The content is a stack with `gap-3`:

- A `<ul>` of ten names with `flex flex-wrap items-baseline gap-x-5 gap-y-2`, no markers and no padding. The first three names (the headliners) are in `font-heading text-h2 md:text-h1`; the other seven in `font-heading text-h2`. Each name is a link to the act's page (`/artists/ossa-bloom`, and so on) that has no underline at rest and `underline underline-offset-4` on hover, like a nav link, and no transition. Names keep their own case. No separators between them: the gap does the work.
- Small print under the list: "Nine more acts on the full line-up."

### 4. Facts

A section with a visually hidden `<h2>` "The festival in numbers" and the grid from the layout notes: three panels, each `md:col-span-4`. Each panel is the stock panel with `gap-3`, holding:

1. A label: `Acts`, `Stages` and `Last train`.
2. The large number in `font-heading text-h1 tabular-nums`: `19`, `3` and `01:40`. The last one is a `<time dateTime="01:40">`, with the time alone as its `dateTime`, since it holds after both nights.
3. One sentence in `text-body`. Acts: "Bands, live sets and design talks across two days." Stages: "Two for music and one, the Drawing Office, for talks and workshops." Last train: "Extra trains run back to Varsby Central after both nights."

### 5. Tickets

A section whose one child is a panel across the full width, with no grid around it; the panel carries the section's heading. The panel has `gap-5` and lays its two parts side by side from `md` up: `md:flex-row md:items-center md:justify-between`.

- At the left, a stack with `gap-3`: the `<h2>` "Tickets on sale" in `text-h2 md:text-h1`, and two sentences in `text-body`: "Weekend €118, day tickets from €64. Saturday day tickets are sold out."
- At the right, `self-start md:self-auto md:shrink-0`: a link styled as the `secondary` button, "Buy tickets" (`/tickets`). It is blue, not black, because the hero's is the page's one default button, and a tall screen can show both. Below `md` it sits under the text, at its own width.

### 6. Footer

The footer from the layout notes.

### Behavior

- The start time is one constant: `new Date("2026-12-04T16:00:00+01:00")`. Work out the time left from it and `Date.now()` in the state's first value, so the first render already shows the right numbers, then update it once a second with a `setInterval` that is cleared when the component unmounts.
- The digits change and nothing else happens: no flip, no fade, no slide, no pulse, and no transition on the numbers. This holds with reduced motion as well, since there is no motion to reduce. The stock button's nudge while pressed stays, from the Motion note.
- The words in the `sr-only` sentence are derived from the same state, rounded down to the minute, so they change at most once a minute. Use "1 day", "1 hour" and "1 minute" in the singular. Leave out a unit that is 0 ("4 hours and 31 minutes until the gates open."), join two parts with "and" and three with a comma and "and". With less than a minute left, the sentence is "Less than a minute until the gates open."
- Once the start time has passed, stop the interval, show `00` in every unit, change the `<h2>` to "Gates are open" and the sentence to "The gates are open."
- The menu button is the only other state on the page. "Buy tickets", "See the line-up", "Full line-up" and the names are links and need no behavior.

## Rules

- **Colors:** only the theme's colors, through the semantic classes (`bg-background`, `text-foreground`, `bg-card`, `bg-primary`, `text-primary-foreground`, `text-muted-foreground`, `border-border` and so on). No hex, rgb or named colors in components, and no Tailwind palette classes such as `bg-blue-500` or `text-gray-600`.
- **Spacing:** padding, margin and gap use only the scale's steps (`1`, `2`, `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 countdown changes every second and shows days, hours, minutes and seconds as real text; nothing on the page moves on its own except its digits.
- [ ] A screen reader gets the time left as a sentence, and the start of the festival is a `<time>` element with a `dateTime`.
- [ ] The `<h1>` is five words or fewer, `text-display` from `md` up and `text-h1` below, and at 375px no word in it runs past the edge.
- [ ] The three facts are white panels with a 1px border, in one row from `md` up and stacked below.
- [ ] The page has one default (black) button, the hero's "Buy tickets"; the tickets panel's "Buy tickets" is blue.
- [ ] 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.
