# Build a match report with the Relay theme

You are building one page in an existing project: a report of one football match, with a scoreboard band for the final score, a timeline of goals and cards, the report's text, the two line-ups side by side, and the match's figures as plain bars with each figure written beside its bar. 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 Relay variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/relay.json` and save https://strictthemes.com/themes/relay/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/relay/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:** Saira for headings, Rubik for body text, Kode Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 64px/800, `text-h1` 40px/800, `text-h2` 24px/700, `text-body` 16px/400, `text-label` 13px/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`), 12px (`3`), 20px (`5`), 32px (`8`), 72px (`18`).
- **Corners:** `rounded-sm` 3px, `rounded-md` 6px, `rounded-xl` 10px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 6px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #FFB703, on-primary #023047, secondary #023047, on-secondary #FFFFFF, tertiary #FB8500, on-tertiary #023047, neutral #3F6377, background #F5FAFC, surface #FFFFFF, foreground #023047, border #C5E1EE, error #B3261E, ring #023047, navy #023047, on-navy #FFFFFF, ice #8ECAE6, on-ice #023047.

### How Relay pages are laid out

- **Page:** kit white, dense and squared up, with navy bands. The outermost element is a `<div>` with `flex min-h-svh flex-col bg-background text-body text-foreground`, holding `<header>`, `<main>` (`flex-1`) and `<footer>`.
- **Container:** the header's row, the content of `<main>` and the footer each sit in a centered container `<div>`, `mx-auto w-full max-w-[1240px] px-5`: a 20px gutter at every width. This width is the one arbitrary value allowed.
- **Stock components:** these pages use two, `Button` and `Input`. Everything else is plain elements with the classes these notes give: tables, badges, radios, checkboxes and cards included. Do not add the shadcn `Table`, `Badge`, `Card`, `Checkbox`, `RadioGroup`, `Switch` or `Tabs`.
- **Today:** the sample's now is Tuesday 8 June 2027 at 20:05, fixed in code, never the device clock. Nothing on these pages ticks: no timers.
- **Sample organisation:** "Wendmoor Football Club", written "Wendmoor FC" everywhere but the home page's `<h1>` and the footer's small print: an invented amateur club, founded in 1921, with 140 members and three teams (first team, reserves, under 18s). It plays at Millrace Park in Wendmoor, an invented town. The first team plays in the Kestrel Valley League, Division One: twelve invented clubs who meet twice, 22 matches each, on Saturdays at 15:00 from 6 March to 31 July 2027. After 14 matches Wendmoor FC are third with 27 points. Their last match was a 3–1 home win over Otterby Town on Saturday 5 June; the next is away to the leaders, Castlebrook United, on Saturday 12 June. Membership for the season costs £95 (Playing), £45 (Junior) or £30 (Social). The same figures appear on every page; this brief gives the ones it needs.
- **Clubs:** the twelve clubs, each with the letters of its crest and its ground: Wendmoor FC (`W`, Millrace Park), Castlebrook United (`CU`, Castle Meadow), Dunmarsh Athletic (`DA`, The Saltings), Otterby Town (`OT`, Weir Lane), Stenwick Albion (`SA`, Albion Field), Kirkfen Rangers (`KR`, Fen Road), Lowmere Welfare (`LW`, Welfare Ground), Pellow Green (`PG`, The Green), Hollinvale (`H`, Hollin Bank), Tarrowby United (`TU`, Station Road), Sedgeholme (`S`, Sedge Lane), Eastlow Wanderers (`EW`, Quarry Lane). A reserve or under 18 side has its club's letters and ground.
- **Vertical rhythm:** every vertical distance between siblings comes from a `gap-*` on their parent, a `flex flex-col` or a grid. Never use `space-y-*`, and no margins but the two these notes name: `mt-0.5` on a checkbox and `mt-1` on an icon beside body text. Rows and cells use padding. The container inside `<main>` is `flex flex-col gap-18 pt-8 pb-18`: its items are the page's sections, 72px apart, and this brief says what the items are. The other steps: `gap-1` between a figure and the label under it, and between the lines of one row; `gap-2` between a field's label, the field, its helper text and its error, and between an icon and its word; `gap-3` between a label and the heading under it, between a heading and its paragraph, between paragraphs, and between buttons in a row; `gap-5` inside a card, between cards, between the fields of a form and between grid columns; `gap-8` between a section's heading and its content where this brief says so, and between the blocks of one section. Where a page's own section gives another value, the page wins.
- **Grid:** where this brief puts things side by side, they are items of a 12-column grid, `grid grid-cols-1 gap-5 lg:grid-cols-12`, a single column below `lg`. "Eight columns" means `lg:col-span-8`, and so on; give every item `min-w-0`. A column of several cards is `flex flex-col gap-5`.
- **Bands:** a band is a block of the theme's `navy`: `bg-navy text-navy-foreground`, navy with white text in both modes, with no shadow. It is not `bg-secondary`, which turns ice blue in dark mode and is for buttons only. Write no `dark:` class. Three things are bands: the header, a scoreboard, and a table's head.
  - Everything on a band inherits the white of `text-navy-foreground` from the band. No element on a band gets `text-foreground`, `text-card-foreground`, `text-muted-foreground`, `text-secondary-foreground` or `text-primary`.
  - A label on a band is `text-ice`, ice blue, the one text color class a piece of text on a band may get. (A crest and the badge bring their own fill and text color.)
  - A rule drawn on a band, to divide two parts of it, is a 1px `border-ice` line.
  - The edge of a band: in dark mode a card is the band's own navy and the page is only a little darker, so a band gets a hairline of the theme's border color, written once for both modes. In light mode that hairline is a pale edge that hardly shows, and that is fine. A band that is a block of its own (a scoreboard) is `rounded-xl border border-border`. The header, across the full width with square corners, has `border-b border-border`. A table's head has no border of its own: the card's border and the rule on the first row under it frame it.
  - The focus outline on a band is `focus-visible:outline-navy-foreground` in place of `focus-visible:outline-ring`, with the rest of the outline classes from the rules unchanged: white on navy in both modes. The `ring` color is navy in light mode, the band's own color, and would not show.
  - A band holds text, crests, the one orange badge and text links. Never a button of any kind (the amber one included), a field, a card or an ice panel. A band is never inside a card; a table's head is part of its table, not a band put into a card.
- **Ice:** the theme's own color fills quiet panels and marked rows: `bg-ice text-ice-foreground`, the pair always together on the panel or the row itself. Ice blue and the navy text on it are the same in both modes. As a text color, `text-ice` is only for labels on a band. A panel is `rounded-xl bg-ice p-5 text-ice-foreground` with `flex flex-col gap-5` inside, no border and no shadow; a marked row (the club's own row in a table, the next match in a list) gets the two classes and keeps its row padding. Everything on ice inherits the navy: no element there gets a text color class of its own, also not the labels and small print that these notes make muted elsewhere. Ice holds text only: nothing that takes focus (the focus outline is amber in dark mode and would vanish on it), so no link, button or field, and no badge. A list on an ice panel has no rules between its rows. An ice panel sits on the page, never inside a card, and a card is never inside it.
- **Amber:** `bg-primary` is a fill for one thing, the main action: the default `Button`, navy text on amber in both modes. Each page has exactly one amber button, which this brief names, and it sits on the page or on a card. Amber is never text and never a thin mark, because it cannot be read on kit white: never write `text-primary`, `border-primary`, `decoration-primary`, `accent-primary` or `outline-primary`, never color an icon amber, and never use the `link` variant of the stock `Button`. Do not use the `chart-*` colors.
- **Orange:** `bg-tertiary` is the fill of one badge and nothing else. Never `text-tertiary`.
- **Focus:** the theme has a `ring` color, navy in light mode and amber in dark mode, and the outline from the rules uses it (`focus-visible:outline-ring`): it shows on the page and on a card in both modes. It does not show on a band in light mode or on ice in dark mode, which is why a band has an outline color of its own, white, and ice holds nothing that takes focus.
- **Header:** a `<header>` that is a band: `border-b border-border bg-navy text-navy-foreground`, full width. Its container is the row, `flex flex-wrap items-center justify-between gap-x-8 gap-y-1 py-3`, with two items:
  - The club's name, a link to `/`, `inline-flex items-center gap-3`: a crest (`size-10`, the letter `W`) and "Wendmoor FC" in a `<span>` with `font-heading text-h2 uppercase`. It is never marked as the current page.
  - A `<nav>` with the `aria-label` "Main", holding a `<ul>` with `flex flex-wrap gap-x-5` of four links in this order: Fixtures (`/fixtures`), Table (`/standings`), Report (`/report`) and Membership (`/membership`). This brief says which one is current. From `md` up the nav sits at the right end of the row; on a phone it wraps to a second line under the name, where the four links fit 375px. There is no "Menu" button and no button of any kind in the header.
  - A nav link is `inline-flex h-10 items-center font-mono text-label uppercase underline-offset-4 hover:underline`; the current page's link adds `underline decoration-2` and `aria-current="page"`. Every link in the header takes the band's focus outline.
- **Footer:** a `<footer>` with `border-t border-border`, not a band. Its container is `flex flex-col gap-3 py-8`: "Wendmoor FC" in a `<p>` with `font-heading text-h2 uppercase`; a `<nav>` with the `aria-label` "Footer", a `<ul>` with `flex flex-wrap gap-x-5 gap-y-2` of four label links, Fixtures (`/fixtures`), Table (`/standings`), Report (`/report`) and Membership (`/membership`), none marked current; then a label in `text-muted-foreground`: `© 2027 WENDMOOR FOOTBALL CLUB · MILLRACE PARK, WENDMOOR · A DEMO: NOTHING IS SENT OR STORED`.
- **Page head:** the top of a page is a row, `flex flex-wrap items-end justify-between gap-5`. On the left a stack with `gap-3`: a label in `text-muted-foreground`, the `<h1>`, and, where this brief gives one, a sentence in `text-body` with `max-w-2xl`. On the right the page's one amber action: a link to `/membership` styled as the default button, "Join the club". On a phone it wraps under the stack. The membership page has no action there: its form's button is the amber one.
- **Headings:** every heading is `uppercase`, with the class on the heading element, and is set in Saira at the weight its type class gives: do not add a weight class to an `<h1>` or `<h2>`, and never slant, outline or stretch it. The `<h1>` is `text-h1` at every width. A section's or a card's `<h2>` is `text-h2`. A smaller heading (an `<h3>`, or a name set like one) is `font-heading text-body font-bold uppercase`. No word in an `<h1>` is longer than eleven letters, so it fits 375px. A section's heading row, where it also holds a link, is `flex flex-wrap items-baseline justify-between gap-3`.
- **Scores and times:** a score, a kick-off time, a position, a price or a count that stands on its own is a **figure**: `font-heading tabular-nums`, with a size: `text-h1 md:text-display` on a scoreboard, `text-h1` in a panel or a card, `text-h2` in a row. `text-display` is used for nothing else. A score is written with an en dash and no spaces, `3–1`. A figure with a name is a stack with `gap-1`, the figure above its label; in a `<dl>` the pair is a `<div>` with `flex flex-col-reverse gap-1`, the `<dt>` (the label) first in the source.
- **Labels:** dates, kick-off times in a line of text, venues, competitions, nav links, field labels, the heads of a table's columns, and button and badge text are labels: `font-mono text-label uppercase`, a short fragment and never a sentence. A label line joins its parts with ` · ` (`SAT 5 JUN · MILLRACE PARK`), unless this brief makes them separate items. A label that is a heading element needs `font-mono`, since a heading gets Saira on its own. On the page and on a card a label is `text-muted-foreground`, unless this brief says `text-foreground`; a field's label is `text-foreground`. On a band it is `text-ice`; on ice it inherits.
- **Small print:** helper text, a note under a table and a sentence of fine print are `text-label text-muted-foreground` in the body font: no `font-mono` and no `uppercase`. Only on the page or a card; on ice it inherits.
- **Text color:** headings, body text, names and figures are `text-foreground` on the page and on a card. Labels and small print are `text-muted-foreground` there, and nowhere else: muted text never sits on a band, on ice, on amber or on orange. Paragraphs are never muted.
- **Measure:** a paragraph of body text is at most `max-w-2xl` wide.
- **Dates and times:** a date in a label is `SAT 5 JUN`, in a `<time>` with `dateTime="2027-06-05"`; with its year, `SAT 5 JUN 2027`. In a sentence a date is written out, "Saturday 5 June 2027", and is not a `<time>`. A time is on the 24-hour clock, `15:00`. A minute of a match is `58'`, with a straight apostrophe.
- **Links:** a text link is `text-foreground underline underline-offset-4 hover:decoration-2` and inherits its size: links are the text color, never amber, so the hover thickens the underline and changes no color. A label link is a label in `text-foreground` with the same underline classes. A link on a band has the underline classes, no text color class, and the band's focus outline. A link whose destination this brief does not name points to `#`.
- **Buttons:** the shadcn `Button` with `className="h-auto px-5 py-2 font-mono text-label uppercase"`; it keeps its stock corner (`rounded-lg`, 6px here). The default variant (amber) is the page's one main action, "Join the club", and nothing else. Every other button is `variant="secondary"` (navy with white text; ice blue with navy text in dark mode) or, for the unchosen buttons of a group, `variant="outline"`. No `ghost`, `destructive` or `link` buttons. The stock hover of `secondary` 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 link styled as a button is an `<a>` with `cn(buttonVariants({ variant }), "h-auto px-5 py-2 font-mono text-label uppercase")`: `buttonVariants` alone does not merge classes. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. No button on these pages is ever disabled, so none gets `focusableWhenDisabled`, and `button.tsx` needs no `data-disabled:` classes. The stock `Button` keeps its `transition-all` and its nudge when pressed.
- **Button groups:** a set of buttons where one is chosen is a `role="group"` with an `aria-label` and `flex flex-wrap gap-2`, on the page, never on a band or on ice. The chosen button has `aria-pressed="true"` and `variant="secondary"`; the others have `aria-pressed="false"` and `variant="outline"`. Every button in it has the Buttons classes. Pressing the chosen button again changes nothing. Not `Tabs`: there are no tab panels.
- **Badges:** the one badge of a page says "Final" or "Live". It is a plain `<span>` with `inline-flex shrink-0 items-center rounded-sm bg-tertiary px-2 py-1 font-mono text-label uppercase whitespace-nowrap text-tertiary-foreground`: orange with navy text in both modes. At most one shows at a time. It may sit on the page, on a card or on a band, never on ice.
- **Crests:** a club's crest is drawn, never an image: a `<span>` with `flex shrink-0 items-center justify-center rounded-full` and `aria-hidden="true"`, holding the club's letters from the Clubs note, beside the club's written name. It is `bg-ice text-ice-foreground`, an ice blue disc with navy letters, on the page, on a card and on a band. On ice, where that would not show, it is `bg-navy text-navy-foreground` instead: a navy disc with white letters. Sizes: `size-8` in a row, with the letters in `font-mono text-label`; `size-10` in the header and `size-12` on a scoreboard, with the letters in `font-heading text-body font-extrabold`. 48px is the theme's limit for a full round. No border and no shadow.
- **Scoreboards:** a scoreboard is a band that is a block of its own: `rounded-xl border border-border bg-navy p-5 text-navy-foreground md:p-8`. A match on it is a row, `flex items-center justify-center gap-3 md:gap-8`, with three items: the home side, the score, the away side. Each side is `flex min-w-0 flex-1 flex-col items-center gap-2 text-center`: its crest (`size-12`), then its name in a `<p>` with `font-heading text-body font-bold uppercase md:text-h2`, then anything this brief puts under the name. The score is a `<p>` with `shrink-0`, a figure in `text-h1 md:text-display`. The home side is always on the left and its goals come first.
- **Cards:** a card is a plain element with `rounded-xl border border-border bg-card p-5` and `flex flex-col gap-5` inside: white with a 1px border, no shadow. Never a card inside a card, and never a bordered box with corners inside a card: divide a card's parts with a 1px rule (`border-t border-border`).
- **Cards with rows:** a card that holds a list of rows or a table has no padding and no gap of its own, and `overflow-hidden`: `overflow-hidden rounded-xl border border-border bg-card`. Each row has `px-5 py-3` and, except the card's first row, `border-t border-border`. A heading inside such a card is a row too, the first, with `px-5 py-3`; the first item of the list under it then has the rule like any other row.
- **Tables:** a table is a plain `<table>` in a card with rows, with `w-full border-collapse text-body`, a `<caption>` that is `sr-only` (the visible name is the heading above the card), and real header cells. Its rows take the cell padding below, not the row padding of the Cards with rows note.
  - The `<thead>` is a band: `bg-navy text-navy-foreground`, with no corner class and no border of its own, since the card clips and frames it. In dark mode it is the card's own navy, and its ice blue labels and the rule under it set it off. Each `<th scope="col">` is a label on a band, `px-2 py-3 font-mono text-label uppercase text-ice`, with `text-left` for a position or a name and `text-right` for a figure.
  - Each body `<tr>` has `border-t border-border`. Cells are `px-2 py-3`; the first cell of every row, the head's too, also has `pl-5` and the last `pr-5`.
  - A club's name is a `<th scope="row">` with `px-2 py-3 text-left font-normal`. A figure cell is `text-right tabular-nums` in the body font; the points are also `font-bold`. A position is a figure in `text-h2`, left-aligned.
  - The club's own row is a marked row: the `<tr>` adds `bg-ice text-ice-foreground`, the club's name is `font-bold` and is followed by a `<span>` with `sr-only`, " (this club)". So the row is marked by more than its color.
  - No hover fill, no stripes and no other colored row. A short header is an `<abbr>` with a `title` and `no-underline`.
- **Description lists:** a list of facts is a `<dl>`. On a card each row is `flex items-baseline justify-between gap-3 py-2` with `border-t border-border` except the first: the `<dt>` a label, the `<dd>` in `text-right text-body`. On an ice panel the rows have no rules and no padding: the `<dl>` is `flex flex-col gap-2`.
- **Inputs:** the shadcn `Input` (`npx shadcn@latest add input`) with `className="h-10 px-3 text-body md:text-body"`, its stock corner (6px). In `components/ui/input.tsx`: replace `border-input` with `border-muted-foreground`, so a field's edge shows on a white card; replace the class `bg-transparent` and the class `dark:bg-input/30` with `bg-card` (`file:bg-transparent` and the `disabled:` fills stay); and delete `dark:aria-invalid:border-destructive/50`. A field is a `flex flex-col gap-2`: a visible `<label>` with `block`, a label in `text-foreground`, tied to the field with `htmlFor`; the field; its helper text, small print, where this brief gives one; and its error line when there is one. Name the helper text in the field's `aria-describedby`. Fields sit on a card.
- **Radios and checkboxes:** native `<input type="radio">` and `<input type="checkbox">`, each with `size-5 shrink-0 accent-secondary` and the focus outline from the rules written on it. `accent-secondary` draws the checked mark in the `secondary` color, navy in light mode and ice blue in dark mode, and the browser sets the tick or the dot against it: both show on a card in both modes. The shadcn `Checkbox` and `RadioGroup` fill their mark with amber and are not used. Radios of one choice share a `name`. They sit on a card or on the page, never on a band or on ice. A checkbox's row is a `<label>` with `flex items-start gap-3 text-body text-foreground`, holding the checkbox (with `mt-0.5`, 2px, so it sits on the first line) and a `<span>` with its sentence. These pages have no switch.
- **Errors:** an error line is `flex items-start gap-2 text-label text-destructive` in the body font (a sentence, so no `font-mono` and no `uppercase`), holding the `CircleAlert` icon (no margin: the line is as tall as the icon) and a sentence that says what to do. The field gets `aria-invalid` and names the line in its `aria-describedby`. Fields are checked in code on submit, with `noValidate` on the `<form>`; focus then moves to the first field with an error. Render the error lines first and move focus after: set the errors in state and focus the field in an effect that runs when they change, so the field's `aria-describedby` already points at a line that exists. An error line is removed when its own field changes (on `input` or `change`); every other error stays until the next submit. Every value is trimmed before it is checked.
- **Announcements:** a message that only tells a screen reader what changed goes into a `<p>` with `role="status"` whose only class is `sr-only`, on the page from the start, empty. It adds no gap.
- **Confirmations:** a message that replaces a form goes into a `role="status"` element that is on the page from the start, empty, or it may not be announced. That element is never hidden, has no classes, and is never an item of a `gap-*` stack: it shares one plain `<div>` with the `<form>`. The message inside it, rendered only when there is one, carries the look. Give the message's heading `tabIndex={-1}` and move focus to it, because the button that had focus is gone.
- **Icons:** from `lucide-react`, with `size-4 shrink-0`, `strokeWidth={2}`, `strokeLinecap="square"` and `aria-hidden="true"`, only where this brief or these notes name one, in the color of the text beside them. Beside a line of body text in an `items-start` row the icon gets `mt-1`, so it sits on the first line. Never amber, never emoji.
- **Corners:** `rounded-sm` (3px) for the badge and for drawn bars; `rounded-lg` (6px, what they ship with) for buttons and inputs; `rounded-xl` (10px, the spec's `lg`) for cards, bands and ice panels. `rounded-full` only for a crest, at most `size-12`. Nothing else is round: no pills, no avatars, no status dots on these pages.
- **Surfaces:** flat. No shadows, blur, gradients or glows in the code you write. The page is `bg-background`, a card is `bg-card`, a band is `bg-navy`, a quiet panel is `bg-ice`; do not use `bg-muted` or `bg-accent`. A drawn bar takes the fill this brief gives it.
- **Motion:** in the code you write, the only transition is color, `transition-colors duration-150` on links. Nothing moves on scroll, and nothing animates in. Stock components keep the motion they ship with.

## The page

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

Sample content, if the user gave none: "Wendmoor Football Club" and its match of Saturday 5 June 2027 in the Kestrel Valley League, Division One: Wendmoor FC 3, Otterby Town 1, at Millrace Park, kick-off 15:00, half-time 1–1, attendance 212, referee M. Garside. What happened, in order:

- `23'`: goal for Wendmoor FC, 1–0. Tobin Achebe, a low shot from Finn Marlowe's pass.
- `36'`: yellow card for Otterby Town. Dominic Ferreby, for a late tackle on Idris Farrow.
- `41'`: goal for Otterby Town, 1–1. Lars Penhale, a header from Patrick Lowther's corner.
- `57'`: yellow card for Otterby Town. Rhys Underhill, for a trip on Lewis Oyelaran in the area.
- `58'`: goal for Wendmoor FC, 2–1. Matteo Brandvold, from the penalty.
- `71'`: yellow card for Wendmoor FC. Samir Kettle, for a shirt pull.
- `84'`: goal for Wendmoor FC, 3–1. Tobin Achebe, a header from Dara Whitlock's cross.

The match's figures, Wendmoor FC first: possession 56% and 44%; shots 14 and 9; shots on target 7 and 3; corners 6 and 4; fouls 9 and 13. Wendmoor FC stay third on 27 points, two behind Castlebrook United, whom they play away on Saturday 12 June at 15:00. Name no goal, card or figure that is not in this brief.

This page uses the stock `button`; add it with `npx shadcn@latest add button` if the project lacks it.

Build these parts in order. The container inside `<main>` has four items: a `<div>` with `flex flex-col gap-8` that holds parts 2 and 3; the grid of part 4; the section of part 5; and the section of part 6.

### 1. Header

The header from the layout notes. The current page is Report.

### 2. Page head

The label `MATCH REPORT · DIVISION ONE`, then the `<h1>` "Achebe double sinks Otterby", then the sentence "Wendmoor FC were level at half-time and won 3–1 at Millrace Park, their third league win in a row." Then, as the stack's last item, a label in `text-muted-foreground`: `BY NADIA FERRIS · SAT 5 JUN 2027`, the date a `<time>`. On the right, the amber link "Join the club".

### 3. Scoreboard

A `<section>` that is a scoreboard band, with the classes of the Scoreboards note, named by an `<h2>` "Final score" that is `sr-only`, and laid out as `flex flex-col gap-5`. Every label on the band is `text-ice`. After the `<h2>`:

- A row with `flex items-center justify-between gap-3`: the label `KESTREL VALLEY LEAGUE · DIVISION ONE`, and the badge "Final". This is the page's one badge.
- The match row from the Scoreboards note: Wendmoor FC with the crest `W`, the score `3–1`, Otterby Town with the crest `OT`. Under each name, a `<ul>` with `flex flex-col gap-1` of that side's scorers, each a label: `ACHEBE 23', 84'` and `BRANDVOLD 58' (PEN)` for Wendmoor FC; `PENHALE 41'` for Otterby Town. A scorer's line may wrap on a phone.
- A stack with `gap-1` of two label lines, each centered with `text-center`: `SAT 5 JUN 2027 · 15:00 · MILLRACE PARK`, the date a `<time>`; then `HALF-TIME 1–1 · ATTENDANCE 212`.

### 4. Report and timeline

A grid row with two items.

**The report,** seven columns from `lg` up: an `<article>` named by its `<h2>` "Report" in `text-h2`, laid out as `flex flex-col gap-3`. Under the heading, five paragraphs in `text-body`, each at most `max-w-2xl` wide, with these words; the manager's words stand in double quotation marks:

1. Tobin Achebe scored twice as Wendmoor FC beat Otterby Town 3–1 at Millrace Park on Saturday, their third league win in a row and their sixth in seven home matches.
2. It was a result with some weight behind it. Otterby won 3–0 when the sides met at Weir Lane in March, and for an hour here they looked ready to make trouble again. Achebe put Wendmoor ahead in the 23rd minute, running on to Finn Marlowe's pass and finishing low past Owen Hartigan, but the visitors were level before the break when their captain, Lars Penhale, headed in Patrick Lowther's corner.
3. The match turned just before the hour. Rhys Underhill tripped Lewis Oyelaran as he cut into the area and was booked for it, and Matteo Brandvold sent the penalty high to the keeper's left. Otterby pushed on and left room behind them; Jonah Pryce had one save to make, from Connor Maddern, and made it well.
4. The third goal came from the bench. Dara Whitlock, on for Idris Farrow, crossed from the right in the 84th minute and Achebe rose at the far post to head his eleventh league goal of the season.
5. "We owed them one," said the manager, Dev Halloran. "We were patient while it was level, and the penalty opened the game up." Wendmoor stay third on 27 points, two behind Castlebrook United, where they play next Saturday.

After the last paragraph, a text link "All fixtures and results" to `/fixtures`, at its own width (`self-start`).

**The timeline,** five columns from `lg` up: a card with rows, a `<section>` named by its `<h2>`. Its first row holds the `<h2>` "Timeline" in `text-h2`. Then an `<ol>` with `role="list"` of nine rows, in order of the minute. Each row is `flex items-baseline gap-3` with the row padding of the Cards with rows note, and has three parts:

- The minute, a figure in `text-h2` with `w-12 shrink-0`: `23'`.
- A stack with `flex min-w-0 flex-1 flex-col gap-1`: what it was and for whom, a label (`GOAL · WENDMOOR FC`, `YELLOW CARD · OTTERBY TOWN`), then a sentence in `text-body` with the player and how, from the sample ("Tobin Achebe, a low shot from Finn Marlowe's pass.").
- For a goal only, the score after it, a figure in `text-h2`: `1–0`.

The seven events of the sample make seven rows. Two more mark the halves and have a label and a score but no sentence: `45'`, `HALF-TIME`, `1–1`, after the row of the 41st minute; and `90'`, `FULL TIME`, `3–1`, as the last row. A goal and a card are told apart by their words, never by a color.

### 5. Line-ups

A `<section>` named by its `<h2>` "Line-ups", laid out as `flex flex-col gap-5`: the `<h2>`, then a grid row of two cards with rows, six columns each from `lg` up, Wendmoor FC first. Each card has, in this order:

- A first row with `flex items-center justify-between gap-3`: on the left, in a `flex items-center gap-3`, the club's crest (`size-8`) and its name in an `<h3>` as the Headings note sets a smaller heading; on the right the formation as a label, `4-4-2` or `4-5-1`.
- An `<ol>` with `role="list"` of the eleven starters. Each is a row with `flex items-baseline gap-3` and, here, `px-5 py-2` in place of the note's row padding: the shirt number, a label with `w-5 shrink-0 tabular-nums`; the name in `text-body` with `min-w-0 flex-1`; and, where the player has one, a note as a label with `text-right`.
- A row with `flex flex-col gap-1`: the label `SUBSTITUTES`, then one sentence in `text-body`.
- A last row with `flex items-baseline justify-between gap-3`: the label `MANAGER`, then the name in `text-body`.

Wendmoor FC, `4-4-2` (number, name, note):

1. `1`, Jonah Pryce
2. `2`, Callum Okafor
3. `5`, Ruben Stannard, `CAPTAIN`
4. `6`, Tomasz Wren
5. `3`, Kieran Dossou
6. `7`, Idris Farrow, `OFF 66'`
7. `4`, Matteo Brandvold, `GOAL 58' (PEN)`
8. `8`, Samir Kettle, `YELLOW CARD 71'`
9. `11`, Lewis Oyelaran
10. `9`, Tobin Achebe, `GOALS 23', 84'`
11. `10`, Finn Marlowe, `OFF 78'`

Substitutes: "14 Dara Whitlock (on 66' for Farrow), 16 Niall Sorensen (on 78' for Marlowe). Not used: 12 Aaron Petty, 15 Joe Tindall, 13 Milan Reyes." Manager: Dev Halloran.

Otterby Town, `4-5-1`:

1. `1`, Owen Hartigan
2. `2`, Marcus Bele
3. `5`, Lars Penhale, `CAPTAIN · GOAL 41'`
4. `4`, Dominic Ferreby, `YELLOW CARD 36'`
5. `3`, Ashley Quaye
6. `8`, Rhys Underhill, `YELLOW CARD 57'`
7. `6`, Jude Amankwah
8. `10`, Patrick Lowther
9. `7`, Theo Vasik, `OFF 62'`
10. `11`, Connor Maddern
11. `9`, Elias Thornbury, `OFF 75'`

Substitutes: "14 Ben Iredale (on 62' for Vasik), 15 Hal Sweeting (on 75' for Thornbury). Not used: 12 Luca Dray, 16 Nico Abara, 13 Sid Carver." Manager: Graham Tull.

### 6. Match figures

A `<section>` named by its `<h2>` "Match figures", laid out as `flex flex-col gap-5`: the `<h2>`, then a grid row with two items.

**The figures,** eight columns from `lg` up: a card. It holds a `<dl>` with `flex flex-col gap-5` and, after it, one line of small print: "Figures were recorded by the club. Referee: M. Garside."

Each figure is a `<div>` with `flex flex-col gap-2`: a `<dt>`, the figure's name as a label, then two `<dd>` elements, Wendmoor FC's first. A `<dd>` is `flex items-center gap-3` with three parts:

- The club's name as a label with `w-28 shrink-0`: `WENDMOOR FC`, `OTTERBY TOWN`.
- The bar's track, a `<div>` with `min-w-0 flex-1` and no fill, which holds the bar: a `<div>` with `h-3 rounded-sm` and `aria-hidden="true"`, `bg-secondary` for Wendmoor FC (navy in light mode, ice blue in dark mode; not `bg-navy`, which would not show on a dark card) and `bg-muted-foreground` for Otterby Town, its width an inline `style` in percent.
- The figure itself, written out, in `text-h2` with `w-12 shrink-0 text-right`: `56%`, `14`.

The five figures, in this order: "Possession", `56%` and `44%`; "Shots", `14` and `9`; "Shots on target", `7` and `3`; "Corners", `6` and `4`; "Fouls", `9` and `13`. A possession bar is as wide as its percent. Any other bar is the figure's share of the larger of the two, so the larger is 100%: 9 shots against 14 is 64.3%. The widths are worked out in code. The two clubs are told apart by their names; the two bar colors only help.

**Next match,** four columns from `lg` up: an ice panel, an `<aside>` named by its `<h2>` "Next match" in `text-h2`. It holds a stack with `gap-1` of the kick-off time, `15:00`, a figure in `text-h1`, and the label `SAT 12 JUN · CASTLE MEADOW · AWAY`; then a `<p>` with `font-heading text-body font-bold uppercase`, "Castlebrook United v Wendmoor FC"; then a `<p>` in `text-body`: "First against third. A win takes Wendmoor above the leaders with seven matches left." Nothing on the panel has a text color class, and nothing on it is a link.

### 7. Footer

The footer from the layout notes.

### Behavior

- The page has no state. The bars' widths are worked out from the figures when the page renders, so a changed figure changes its bar.
- Nothing animates: the bars are at their width from the start.
- The links need no behavior beyond their `href`.

## 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`, `18`) 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 48px 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 use amber once in view, on the main action. The focus outline in dark mode does not count.
- Do set scores, times and positions large, in the heading font.
- Do keep text on a navy band white and labels on it ice blue, in both modes.
- Don't write in amber or orange on a light page, and don't use amber for a link.
- Don't slant, outline or stretch the heading font, and don't use gradients or glows.
- Don't put a card inside a card or a band inside a card.
- 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 scoreboard is a band with the final score 3–1 at `text-display` from `md` up, both clubs with a round crest drawn from plain elements, the scorers under each name and the page's one Final badge.
- [ ] The timeline lists four goals and three cards in order of the minute, each named in words, with the score after each goal, and it agrees with the scoreboard, the report and the line-ups.
- [ ] The two line-ups sit side by side from `lg` up and stack below, each with eleven numbered starters, the substitutes and the manager.
- [ ] Each match figure has one bar for each club with the figure written beside it; a bar's width comes from the data in an inline style, and the bars are hidden from screen readers.
- [ ] Join the club in the page head is the only amber button on the page, and no text, link, icon or bar is amber or orange.
- [ ] 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.
