# Build a sports results with the Dispatch theme

You are building one page in an existing project: a sports results page, with today's scores, a league table, fixtures to come and a filter by competition. 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 Dispatch variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/dispatch.json` and save https://strictthemes.com/themes/dispatch/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/dispatch/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:** Big Shoulders for headings, Newsreader for body text, Chivo Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 88px/800, `text-h1` 56px/800, `text-h2` 32px/700, `text-body` 18px/400, `text-label` 12px/500.
- **Font per size:** a type class does not set the font family. Headings (`<h1>` to `<h6>`) get the heading font on their own; on any other element add the font class: `font-heading` with `text-display`, `text-h1`, `text-h2`; `font-mono` with `text-label`.
- **Spacing scale**, with the Tailwind step in brackets: 4px (`1`), 8px (`2`), 16px (`4`), 24px (`6`), 48px (`12`), 96px (`24`).
- **Corners:** `rounded-none` 0px, `rounded-sm` 1px, `rounded-md` 2px.
- **Colors (light mode):** primary #BF0D2A, on-primary #FFFFFF, secondary #111111, on-secondary #FFFFFF, tertiary #FFE14D, on-tertiary #111111, neutral #5C5850, background #F4F1EA, surface #FFFFFF, foreground #111111, border #1C1C1C, error #9E0019.

### How Dispatch pages are laid out

- **Container:** the `<header>`, the `<main>` and the `<footer>` each hold one `<div>` that is the container: `mx-auto max-w-[1280px]`, with `px-4`, and `px-6` from `md` up. The three landmarks carry no classes of their own; every class these notes give the header, main or footer goes on its container `<div>`. This width is the one arbitrary value allowed. Put `text-body` on the element that wraps the whole page.
- **Fonts:** headings (`<h1>` to `<h6>`) get Big Shoulders, the headline font, on their own. `font-sans` is Newsreader, the body font: in this project it is a serif, and it is the only serif. Do not write `font-serif`. `font-mono` is Chivo Mono, for labels and figures. The theme declares the weights 400, 500, 700 and 800: use only `font-normal`, `font-medium`, `font-bold` and `font-extrabold`.
- **Headlines:** every piece of text in the headline font is `uppercase`: headlines, the masthead name, a pull quote set in it. Never sentence case. A headline is at most two lines at 1280px (three are fine between `md` and `lg` in a row of three), with no word over ten letters so nothing overflows at 375px. Do not rely on word-breaking utilities.
- **Headline sizes:** the page's one main headline is the largest text on the screen, and nothing else comes near it. It is `text-h1` below `md` and `text-display` from `md` up; this brief names which headline that is. Other story headlines are `text-h2`. A short headline in a dense list (a side column, "Most read", the "Live" strip, a related story) is a **small headline**: `font-heading text-body font-bold uppercase`. A headline that is a link holds an `<a>` in `text-foreground hover:underline decoration-2 underline-offset-4`.
- **Masthead:** the paper's name is a link to `/` in `font-heading uppercase text-foreground hover:underline decoration-2 underline-offset-4`, in the header and the footer, never a heading element. It is `text-h1` from `md` up on the front page, and `text-h2` on every other page and below `md` everywhere, so that it never competes with the page's headline.
- **Header:** a `<header>` whose container holds three rows. The top row is `flex items-center justify-between gap-4 py-2`: the date as a label in `text-muted-foreground` at the left (`WEDNESDAY 11 NOVEMBER 2026`, a `<time>` with `dateTime="2026-11-11"`), and at the right the "Subscribe" link to `#` styled as a default button. The second row is `flex items-center justify-between gap-4 border-t-2 py-4`, holding the masthead name; on the front page from `md` up it is `md:justify-center` instead, with the name alone in it. The third row is a `<nav>` with the `aria-label` "Sections" and `border-y py-2`, holding a `<ul>` with `flex flex-wrap gap-x-6 gap-y-2` of the section links as labels. The section links, in this order, each to `#`: News, Politics, City, Sport, Results, Business, Culture, Opinion. Each brief names the current one. The link for the current page is `text-foreground` with `aria-current="page"` and `underline decoration-2 underline-offset-4`; the others are `text-foreground hover:underline`.
- **Menus on small screens:** below `md` the third row is hidden, and a "Menu" button (`variant="outline"`) with `aria-expanded` and `aria-controls` sits at the right end of the second row. It shows the section links inside the `<header>`, under the second row: a second `<nav>` with the same `aria-label` "Sections", shown only below `md`, which carries the `id` that `aria-controls` names and has the `hidden` attribute while closed. It has a 1px bottom border and `py-2`; each link is a block with `py-2`. It closes on Escape and gives focus back to the button. Do not use a `Sheet` or a `Dialog`.
- **Footer:** a `<footer>` whose container is `flex flex-col gap-4 border-t-2 py-6`: the masthead name, a link to `/` in `font-heading text-h2 uppercase` with `self-start` so it is only as wide as its text, then a `<nav>` with the `aria-label` "Footer" holding a `<ul>` with `flex flex-wrap gap-x-6 gap-y-2` of the section links as label links, none of them current: no `aria-current` and no underline at rest. Then a label line in `text-muted-foreground` with the copyright year and the city.
- **Vertical rhythm:** written as gaps on stacks (`flex flex-col gap-*`), never as `space-y-*`, and never as a margin on an element inside a stack unless this brief names one. The container `<div>` inside `<main>` is a stack, `flex flex-col gap-12 pt-6 pb-12`: its items are the page's blocks (the page head, the lead row, each section). Inside a block: `gap-4` between a section head and what follows, between paragraphs and between stories in a list; `gap-2` inside a story unit and between a label and its value; `gap-1` between a line and a label line directly under it that belongs to it (a headline and its time, two lines of one byline) where this brief says so.
- **Grid:** a row of columns is `grid grid-cols-1` with `lg:grid-cols-12`, and a column spans what this brief gives it (`lg:col-span-8`, `lg:col-span-4`). Below `lg` the columns stack in source order.
- **Rules between columns:** columns of stories are divided by a 1px rule, not by space. The grid is `gap-4 lg:gap-0`: a row gap while the columns are stacked, and no column gap. The column after a rule has `lg:border-l lg:pl-6`, and the column before it `lg:pr-6`, so there is 24px of space on each side of the rule. Below `lg`, where the columns stack, the rule turns into a 1px top border on each column after the first, with `pt-4`, and the grid's `gap-4` above it; from `lg` up that is `lg:border-t-0 lg:pt-0`. A stacked column that opens with a section head takes no top border and no `pt-4`: the head's 2px rule divides it. Use the same construction for three stories in a row at `md` (`md:grid-cols-3`): each story after the first has `border-t pt-4 md:border-t-0 md:pt-0 md:border-l md:pl-6`, each story before the last `md:pr-6`, and the grid has `gap-4 md:gap-0`.
- **Rules between stories:** a list of stories is a `<ul>` (or `<ol>`) with `flex flex-col gap-4`, no markers and no padding. Each item after the first has a 1px top border and `pt-4`. Every line is `border-border` (nearly black on purpose), which the base styles set, so `border` alone draws it. Never use a pale line or `border-muted`.
- **Section head:** a section opens with a row `flex items-baseline justify-between gap-4 border-t-2 pt-2`: the section's name as an `<h2>` label in `font-mono text-label uppercase text-foreground` (it needs `font-mono`, since a heading gets the headline font on its own), and at the right end, where this brief gives one, a label link such as "All politics".
- **Story unit:** a story in a list is an `<article>`, a stack with `gap-2`: an optional kicker, the headline, an optional standfirst in `text-body`, and the label line. A story with a picture puts the figure first, except a page's lead story, whose headline comes before its picture (the brief says so). A story's headline is an `<h3>` inside a section and an `<h2>` where it stands under the `<h1>` with no section head between.
- **Labels:** section names, kickers, bylines, times, captions, table headers, nav links, and button and badge text are labels: `font-mono text-label uppercase`, a short fragment and never a sentence; only a caption may be a short sentence. A label line joins its parts with ` · ` (`BY HANA LINDQVIST · 09:40`). Names in a byline are uppercased like the rest of it, as `DESIGN.md` asks.
- **Text color:** headlines, body text, kickers and section names are `text-foreground`. Bylines, times, captions and helper text are `text-muted-foreground`. Paragraphs are never muted.
- **Red:** `text-primary` and `bg-primary` mean live, new or the main action, and nothing else: the "Live" marker, the "New" label (a `<span>` label `NEW` in `text-primary`, with no dot or badge, beside the time where the brief puts it), links in running text and the default button. Never a headline, a rule or a fill for decoration. The one other red is the focus outline from the rules: `outline-ring` is red in this theme, and stays so. Red text is fine on `bg-background` and `bg-card` in both modes.
- **Live marker:** `inline-flex items-center gap-2 font-mono text-label uppercase text-primary`, holding a dot (`size-2 shrink-0 rounded-full bg-primary`, `aria-hidden="true"`) and the word "Live". The word is always there: the dot never stands alone. The dot does not pulse or blink.
- **Yellow:** `bg-tertiary` is a fill only, always with `text-tertiary-foreground` on it, which is black in both modes. It marks one phrase (a `<mark>` with `bg-tertiary px-0.5 text-tertiary-foreground`) or the "Exclusive" badge. Never red text on yellow, and never yellow as text.
- **Badges:** the shadcn `Badge` with `className="h-auto rounded-none border-transparent bg-tertiary px-2 py-1 font-mono text-label uppercase text-tertiary-foreground"`, for "Exclusive" only. At most one on a page.
- **Dates:** "today" is always the sample date, Wednesday 11 November 2026, never the clock. Nothing on these pages reads the time. Dates and times take three forms: a time today is `09:40`; another day is `TUE 10 NOV`, with no time; the long form, in the header and a page head, is `WEDNESDAY 11 NOVEMBER 2026`. Every date and time in a label, in a byline or alone, is a `<time>` that wraps only the date or time (`UPDATED <time>09:40</time>`), with `dateTime="2026-11-11T09:40"` for a time and `dateTime="2026-11-10"` for a day or a long date.
- **Numbers:** every `<time>`, the long date included, and every score, position and count outside a sentence is `font-mono tabular-nums`, labels or not. In a table, numbers are right-aligned, and so is their column header. A **big figure** (a score in a score box, a number in a fact box, a rank in a numbered list) is `font-mono text-h2 tabular-nums`: this is the one use of a mono font at a heading size, because `DESIGN.md` sets scores in Chivo Mono with tabular figures.
- **Measure:** a paragraph of body text is at most `max-w-xl` wide (576px, about 65 to 70 characters a line in Newsreader at 18px), whatever column it sits in. A standfirst in `font-sans text-h2 font-normal` is capped at `max-w-3xl` instead.
- **Emphasis:** `<em>` is a real italic. Newsreader italic is also used for a pull quote. No bold inside paragraphs.
- **Links:** a link in running text is `text-primary underline underline-offset-4 hover:text-foreground` and inherits its size. A label link (in the nav, the footer, a section head) is `text-foreground hover:underline underline-offset-4`. Links get `transition-colors duration-150`. The masthead name points to `/`; every other link, the current page's own nav link and every story link included, points to `#`, unless this brief gives another `href`.
- **Buttons:** the shadcn `Button` with `className="h-auto px-4 py-2 font-mono text-label uppercase"`. It keeps its stock corners (`rounded-lg`, 2px here). The default variant (red) is the one main action, and "Subscribe" in the header is it on every page: any other button is `variant="secondary"` (black, cream in dark mode). Its stock hover cannot be told from its resting color: in `components/ui/button.tsx`, replace the `secondary` variant's `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]` with `hover:bg-secondary/80`, once, and add no hover class on the page. 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 that its own click disables, while focus stays on it, gets `focusableWhenDisabled`; on these pages that is only the live blog's "Show new update". Base UI then sets `aria-disabled="true"` and `data-disabled` instead of the `disabled` attribute, so the stock `disabled:` classes do nothing: on a page that uses the prop, and only there, add `data-disabled:pointer-events-none data-disabled:opacity-50` beside them in `components/ui/button.tsx`. A button whose click moves focus elsewhere or removes it does not get the prop. If the project has no `Button` yet, add it with `npx shadcn@latest add button`.
- **Button groups:** a set of buttons where one is chosen (an order, a filter) is a `role="group"` with an `aria-label` and `flex flex-wrap gap-1`. The chosen button has `aria-pressed="true"` and `variant="secondary"`; the rest have `aria-pressed="false"` and `variant="outline"`. Every button in it has the Buttons classes. Pressing the chosen button again changes nothing, and a status element keeps its text.
- **Announcements:** a message that tells a screen reader what changed 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 with `hidden` or `empty:hidden`; where its text is for screen readers only, it is `sr-only`. Each status element here is a `<p>` whose only class is `sr-only`: it adds no gap and has no margin.
- **Pictures:** there are no image files. A picture is a `<figure>`, a stack with `gap-2`: a plain `<div>` with an aspect ratio (`aspect-3/2` unless this brief says otherwise), `w-full bg-muted` (the flat muted block `DESIGN.md` names, used only for this fill), `role="img"` and an `aria-label` that describes the scene; then a `<figcaption>` label in `text-muted-foreground` that says what it shows and ends with the credit after ` · ` (`THE KESSEL STREET BRIDGE ON TUESDAY · PHOTOGRAPH: ODILE MARSH`). Nothing is written on the fill. Pictures have square corners and no border.
- **Boxes:** a box (a fact box, a score box, the "Live" strip) is a plain element with `border bg-card p-6`, `p-4` below `md`, and square corners. Do not use the shadcn `Card`. Never put a box inside a box.
- **Tables:** the shadcn `Table` with `text-body`. Header cells are labels in `text-muted-foreground`; they keep their stock `h-10` and padding, and so do body cells. Rows keep their stock borders and hover color. A table that is wider than a phone scrolls sideways in its own box, reachable with the keyboard: `tabIndex={0}`, `role="region"` and an `aria-label`. Edit `components/ui/table.tsx`: put these on its scroll box, with the focus outline from the rules, and add a required `label` prop for the name. The grid column or flex item that holds the scroll box gets `min-w-0`, or `min-w-lg` widens the column and the page instead of scrolling. Name the table with a visible `<h2>` or `<h3>` above it, not a `<caption>`, which would scroll with the table. Give the `Table` `min-w-lg` where this brief says it scrolls on a phone.
- **Icons:** from `lucide-react`, with `size-4`, `strokeWidth={1.5}` and `aria-hidden="true"`, only where this brief names one. Never emoji.
- **Corners:** square. `rounded-none` for boxes, pictures and badges (boxes and pictures need no class at all). Buttons and the stock `Input` keep `rounded-lg`, which is 2px. `rounded-full` only for the "Live" dot.
- **Surfaces:** flat: ink on paper. No shadows, no blur and no gradients in the code you write. The page is `bg-background`; a box is `bg-card`. A stock component that floats over the page (a menu, a dialog) keeps its border and the one short shadow it ships with.
- **Motion:** in the code you write, the only transition is color. Nothing moves on scroll, and nothing animates in when it is added to the page. The stock `Button` keeps its `transition-all` and its 1px press nudge, as the rules say.

## The page

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

Sample content, if the user gave none: **The Port Avery Ledger**, the daily paper of Port Avery, an invented harbour city. The day is Wednesday 11 November 2026. Three competitions: the Coastal League (eight men's clubs: Port Avery Harriers, Saltgate United, Westmere Athletic, Dunmore City, Brackwater Town, Calloway Rovers, Fenwick Albion, Holm Bay), the County Cup, and the Women's Coastal League. Today: four Coastal League results, one still being played; two County Cup results between four clubs from outside the league, which you name, one won after extra time and one on penalties; no Women's Coastal League match. Invent the scores, the table as it stood before today (every club has played twelve games; P is W + D + L, PTS is 3 × W + D, and the goal differences add up to 0), and six fixtures over the next four days, Thursday 12 to Sunday 15 November: two County Cup on Thursday between four other clubs from outside the league, four Coastal League on Saturday and Sunday, so that no club plays two days running, and none in the Women's league. The filter buttons, the status text and the empty sentence use the short name "Women's League"; the results' competition `<h3>`s and the fixture label lines use the full names (`COASTAL LEAGUE`, `COUNTY CUP`).

Build these parts in order.

### 1. Header

The header from the layout notes, with the masthead name in `text-h2`. The current page is Results.

### 2. Page head

The first item of `<main>`: a stack with `gap-4`.

- A row styled as the section head, but with the name `SPORT` in a `<p>`, not an `<h2>`, since it comes before the `<h1>`.
- The `<h1>` `RESULTS`, `text-h1` below `md`, `text-display` from `md` up.
- A row with `flex flex-wrap items-center justify-between gap-4`: the date as a label in `text-muted-foreground` (`WEDNESDAY 11 NOVEMBER 2026`, a `<time>`), and the filter.
- The filter, the second item of that row: a button group with the `aria-label` "Competition", with All (chosen at the start), Coastal League, County Cup and Women's League. On a phone it wraps onto two lines.
- The status element from the Announcements note, right after the row, empty at the start.

### 3. Today's results

The next item of `<main>`: a `<section>` with the section head `TODAY'S RESULTS`. Under it, with `gap-4`, the results grouped by competition: for each competition that has results under the filter, an `<h3>` label in `font-mono text-label uppercase text-foreground` with the competition's name, and under it with `gap-2` a grid of score boxes, `grid gap-4 md:grid-cols-2 lg:grid-cols-3`. The groups are a stack with `gap-6`.

Each score box is an `<article>` with the `aria-label` of the match, home side first, ending with its state: "Port Avery Harriers 2, Saltgate United 1, full time"; "…, live, 67th minute"; "…, after extra time"; "Rookhaven 1, Elmsford 1, Rookhaven win 4–3 on penalties". It is a box from the layout notes, a stack with `gap-2`:

- A label line with `flex items-center justify-between gap-4`: the ground in `text-muted-foreground` (`HARBOUR ROAD`) at the left, and at the right the state: `FT` for a finished match, `AET` or `PENS` for one decided after extra time or on penalties, each in `text-foreground`; for a match being played, the live marker followed, `gap-2` after it, by the minute as a label in `text-primary` (`67'`).
- Two team rows, home first, each `flex items-baseline justify-between gap-4`: the club's name in `text-body`, and at the right its score as a big figure (`font-mono text-h2 tabular-nums`). In a finished match the winning side's name is `font-bold`, and under the rows of the penalty match only, since its score is level, a label line in `text-muted-foreground` says it in words (`ROOKHAVEN WIN 4–3 ON PENALTIES`). An extra-time win needs no words line: its score shows the winner. A draw and a match being played have neither the bold nor the line.
- Where a match has goals worth naming, a label line in `text-muted-foreground` with the scorers and minutes (`OKONJO 12', 58' · BRANDT 71'`).

When a competition has no result today and it is the one chosen, the section shows one sentence in `text-body` in place of the groups: "No Women's League matches today." With All chosen, competitions without results are left out.

### 4. Table and fixtures

The next item of `<main>`: a row of two columns with a rule between them, as the layout notes describe.

**League table,** `lg:col-span-8 min-w-0 lg:pr-6`: a `<section>`, a stack with `gap-4`: the section head `COASTAL LEAGUE TABLE`, and the `Table` (add it with `npx shadcn@latest add table`) with the `label` "Coastal League table" and `min-w-lg`, so it scrolls in its box on a phone. Columns: `POS`, `CLUB`, `P`, `W`, `D`, `L`, `GD`, `PTS`. Each short header holds an `<abbr>` with a `title` ("Position", "Played", "Won", "Drawn", "Lost", "Goal difference", "Points"). Every column but Club is a number, right-aligned with its header, except `POS`: here, instead of the Tables note, it is left-aligned with its header, so that it does not sit against the club name (positions are one digit, so they still line up). Goal difference has a sign (`+7`, `−3`, `0`). Points are `font-bold`. Eight rows. The row of Port Avery Harriers, the city's club, has the name in `font-bold` and no other mark. Under the table, with the section's `gap-4`, a label line in `text-muted-foreground`: `TOP TWO GO UP · AFTER 12 GAMES`. The filter does not change the table. While another competition is chosen, the label line under it adds ` · NOT FILTERED`.

**Fixtures,** `lg:col-span-4 lg:border-l lg:pl-6`: a `<section>` with the section head `FIXTURES`. Under it, with `gap-4`, the fixtures grouped by day: each day an `<h3>` label in `text-foreground` holding a `<time>` (`THU 12 NOV`), and under it with `gap-2` a list of fixtures divided by rules. The days are a stack with `gap-6`. Each fixture is a stack with `gap-1`: the two clubs in `text-body` ("Dunmore City v Fenwick Albion"), then a label line in `text-muted-foreground` with, here, the kick-off time as a `<time>` (`dateTime="2026-11-14T15:00"`), the competition and the ground (`15:00 · COASTAL LEAGUE · MILL LANE`, under `SAT 14 NOV`). A day with no fixture under the filter is left out, and with none at all the column shows "No fixtures in the next four days."

### 5. Footer

The footer from the layout notes.

### Behavior

- All of it works on the sample data in component state.
- Choosing a competition shows only its results and fixtures. The chosen button's `aria-pressed` and variant follow the choice. Focus stays on the button.
- After each choice, the status element names the button and the counts, a match being played included: "All: 6 results, 6 fixtures.", "County Cup: 2 results, 2 fixtures.", "Women's League: 0 results, 0 fixtures." One is "1 result", "1 fixture".
- The match being played does not change on its own: no timers.

## 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`, `12`, `24`) plus 0, 1px and 2px. No arbitrary values such as `p-[18px]`, and no other steps such as `p-3` or `gap-5`. `auto` margins are fine. Widths and heights such as `w-64` or `size-4` are sizes, not spacing. This applies to the code you write: a stock component keeps the spacing inside it, unless this brief changes it.
- **Type:** every piece of text you write gets its size from one of `text-display`, `text-h1`, `text-h2`, `text-body`, `text-label`, set on the element or on a parent it inherits from. Put `text-body` on the outermost element of the page you write. No other sizes. A stock shadcn component keeps the size it ships with, unless `DESIGN.md` or this brief gives that component its own style. A type class does not replace a weight class: where a stock component ships `font-medium` and the text should be regular, add `font-normal`.
- **Fonts:** only the theme's fonts, through `font-sans`, `font-heading` and `font-mono`. Do not add a font.
- **Corners:** only the `rounded-*` classes, which the theme already sets. No arbitrary radius, and `rounded-full` only where `DESIGN.md` or this brief names a full round, such as an avatar or a status dot. The theme allows a full round on a box up to 12px 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 make one headline much larger than everything else on the screen.
- Do divide with rules and keep the columns tight.
- Do keep red for what is live, new or the main action. The focus outline is red as well: it marks where the reader is, and it is the one other use.
- Don't soften it: no rounded cards, no shadows, no pale grey borders.
- Don't set body copy in the headline font, or headlines in sentence case.
- 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.

- [ ] Every score, position, points total and kick-off time is set in the mono font with `tabular-nums`, and the numbers in the table are right-aligned.
- [ ] Choosing a competition hides the results and fixtures of the others, and a competition with none today shows a sentence saying so.
- [ ] The filter is a group of buttons with `aria-pressed`, and after each choice a `role="status"` element says how many results and fixtures are shown.
- [ ] At 375px the league table scrolls sideways in its own box, which has the name "Coastal League table".
- [ ] A match in progress shows the live marker with the word "Live" and the minute, and a finished one says "FT", "AET" or "PENS".
- [ ] 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.
