# Build a part page with the Gauge theme

You are building one page in an existing project: one part's page as a spec sheet, with a drawing of the part in outline with its dimensions, its part number, stock and lead time, price breaks by quantity, a quantity field with Add to quote, and a full specification table. 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 Gauge variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/gauge.json` and save https://strictthemes.com/themes/gauge/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/gauge/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:** Chivo for headings, Libre Franklin for body text, Roboto Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 48px/800, `text-h1` 32px/700, `text-h2` 20px/700, `text-body` 15px/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`), 12px (`3`), 16px (`4`), 24px (`6`), 48px (`12`).
- **Corners:** `rounded-sm` 2px, `rounded-md` 4px, `rounded-xl` 8px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 4px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #212529, on-primary #F8F9FA, secondary #E9ECEF, on-secondary #212529, tertiary #ADB5BD, on-tertiary #212529, neutral #495057, background #F8F9FA, surface #FFFFFF, foreground #212529, border #CED4DA, error #B3261E, success #1E7B4B.

### How Gauge pages are laid out

- **Page:** steel white, dense on purpose. 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's line each sit in a centered container `<div>`, `mx-auto w-full max-w-[1280px] px-4`: a 16px gutter at every width. This width is the one arbitrary value allowed. The `<main>` element itself has `py-6`; the container inside it carries the rhythm below.
- **Today:** the sample's today is Thursday 12 November 2026, a fixed date in code, never the device clock.
- **Dates and times:** a date that stands on its own (a table cell, a value, a small figure) is `12 Nov 2026`, in a `<time>` with `dateTime="2026-11-12"`. Inside a sentence a date is written out, `13 November 2026`, and is not a `<time>`, in small print too. A time is on the 24-hour clock, `15:00`, and is a `<time>` only where it stands on its own. Dates and times take `tabular-nums` only where they are `font-mono`.
- **Sample organisation:** "Torvale Bearings Ltd", an invented supplier of industrial bearings with its works and stores in Brigholt. Prices are in pounds and exclude VAT, dimensions are in millimetres, loads in kilonewtons. The buyer filling in the forms is "Pellam Pumps Ltd". The same parts, figures and prices appear on every page; this brief gives the ones it needs.
- **Links:** the product's name links to `/catalogue`. The nav links, in this order, are Catalogue (`/catalogue`), Compare (`/compare`), Downloads (`/downloads`) and Quote (`/quote`), the same in both navs; this brief says which is current. Any other link without a named destination is `#`. A text link is `text-foreground underline underline-offset-4 hover:decoration-2` and inherits its size: links are the text color, so the hover thickens the underline and changes no color. Add `transition-colors duration-150`.
- **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-*`. Rows and cells use padding. The only margins are the Confirmations note's. The container inside `<main>` is `flex flex-col gap-6`: the page header, then each row of panels. On a page this brief divides into sections (each a `<section>` named by its `<h2>` with `aria-labelledby`), the container is `gap-12`, the spec's 48px between sections, and each section is `flex flex-col gap-4`. The other steps: `gap-1` between a label and the value under it; `gap-2` between a field and its helper text and error, between the options of a group, and between buttons in a row; `gap-3` between a label and its field or button group; `gap-4` inside a panel (between its heading row and its content, between groups, between the fields of a form), between panels and between grid columns. Where a page's own section gives another value, the page wins.
- **Grid:** where this brief puts panels side by side, they are items of a 12-column grid, `grid grid-cols-1 gap-4 lg:grid-cols-12`, a single column below `lg`. "Three columns" means `lg:col-span-3`, and so on. A column of several panels is `flex flex-col gap-4`, and its last panel has `flex-1`, so the columns of a row end level.
- **Header:** a full-width `<header>` with `border-b border-border bg-card`. Its container is the row, `flex items-center gap-6 py-3`: the product's name, "Torvale", in `font-heading text-h2 text-foreground`, no underline; then a `<nav>` with the `aria-label` "Main" and `hidden md:block`, holding a `<ul>` with `flex gap-6` of the nav links; then, at the right end (`ml-auto`), the sales line as a small figure, `Sales 01632 960 418`, with `hidden md:block`; and the "Menu" button of the note on menus below. A nav link is a label: the current page's link is `text-foreground underline underline-offset-4` with `aria-current="page"`, the others `text-muted-foreground hover:text-foreground`.
- **Footer:** a `<footer>` with `border-t border-border` across the full width. Its container has `py-6` and holds one small figure: "Torvale Bearings Ltd · Brigholt works · Prices exclude VAT · A demo: nothing is sold or sent."
- **Page header:** a row that is not a panel: `flex flex-wrap items-end justify-between gap-4`. On the left a stack with `gap-1`: the breadcrumb or label this brief gives, then the `<h1>` in `text-h1`. On the right the page's actions, `flex flex-wrap gap-2`.
- **Breadcrumb:** where this brief asks for one, it is a `<nav>` with the `aria-label` "Breadcrumb" holding an `<ol>` with `flex flex-wrap items-center gap-2`, in `font-mono text-label`: each step but the last a link in `text-muted-foreground underline underline-offset-4 hover:text-foreground`, a `/` in `text-muted-foreground` with `aria-hidden="true"` between steps, and the last step in `text-foreground` with `aria-current="page"`, not a link. Because the breadcrumb then says `page`, the header's nav link for the section it belongs to has `aria-current="true"` in place of `page`, with the same look.
- **Headings:** Chivo, sentence case, at the weight its type class sets: do not add `font-bold`, `font-semibold` or `font-medium` to a heading. A panel's or a section's `<h2>` is `text-h2`. A smaller heading inside a panel is an `<h3>` in `font-sans text-body font-bold`: body text in bold, not the heading font.
- **Panels:** a panel is a plain element with `rounded-xl border border-border bg-card p-4`, white on steel white, not the shadcn `Card`, which ships its own padding and a ring. It is `flex flex-col gap-4` when it holds a heading row and content. Never put a panel inside a panel, and never a bordered box with rounded corners inside a panel: divide a panel's parts with 1px rules (`border-t border-border`). No tinted panel: every panel is `bg-card`.
- **Panel heading:** a panel opens with a heading row, unless this brief says it has none: `flex flex-wrap items-center justify-between gap-3`. It holds the panel's `<h2>` in `text-h2` and, at its right end, anything that belongs to the whole panel: a count, a link, a filter, a badge.
- **Panels with rows:** a panel that holds a table or a list of rows has no padding of its own, no gap, and `overflow-hidden`. Its heading row is one element: the heading row's classes plus `border-b border-border px-4 py-3`, with no wrapper around it. Each row of a list has `px-4 py-3` and a 1px top border, except the first. Anything under the table inside the panel (a note, a total) is a row with `border-t border-border px-4 py-3`.
- **Labels:** table headers, field labels, the names of groups, nav links, breadcrumbs and button and badge text are labels: `font-mono text-label`, in sentence case, never `uppercase`, a short fragment and never a sentence. They are `text-muted-foreground` unless a note or this brief gives another color; button and badge text keep their component's color.
- **Small figures:** a part number under a name, a date, a count, a file size, a reference, helper text and small print are small figures: `font-mono text-label tabular-nums text-muted-foreground`, the look of a label used for a value or a sentence. A `text-foreground` in this brief replaces the muted color.
- **Numbers and units:** every number that stands on its own (a table cell, a value, a price) is `font-mono tabular-nums`; inside a sentence it stays in the body font. A quantity, a dimension, a load, a price and a file size are right-aligned in a table, and so is their column's header. A unit is given once, where the eye finds it: in a table it is in the column header, "Bore (mm)", and the cells hold the bare figure, `20`; beside a single value it follows the figure with a space, `20 mm`, `13.40 kN`. Prices have two decimals and a thousands comma (`1,137.80`); outside a priced column they carry the pound sign, `£2.94`. A minus sign is U+2212 (`−30 °C`). Never round a figure the buyer relies on: write the decimals this brief gives.
- **Part numbers:** a part number is `font-mono` and written in full everywhere (`TVL-2047-RS`), never shortened or hidden behind a click. In a table's first column it is a text link in `font-mono text-body`; under a name it is a small figure.
- **Text color:** headings, body text and values are `text-foreground`. Labels, small figures and helper text are `text-muted-foreground`, and only on the page or a panel, never on a fill.
- **Measure:** a paragraph of body text is `text-foreground`, never muted, and at most `max-w-lg` wide.
- **Buttons:** the shadcn `Button` with `className="px-4 font-mono text-label"`, its stock height and corner, in sentence case; add it with `npx shadcn@latest add button` if missing. There are two looks and no others. The default variant (graphite) is for the one main action of a view, "Add to quote" or "Request a quote": at most one enabled default button on screen. Every other button is `variant="secondary"` (pale steel with graphite text): no `outline`, `ghost` or `destructive` buttons on these pages. In `components/ui/button.tsx`, replace the `secondary` variant's stock hover class, `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]`, with `hover:bg-secondary/80`, once, on every page (each page has a secondary button, the "Menu" button at least), and add no hover class beside it or in a `className`. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A button that is disabled before anyone clicks it, or whose click moves focus elsewhere, uses the plain `disabled` prop. This brief says whether any button gets `focusableWhenDisabled`; on a page where none does, `button.tsx` needs no `data-disabled:` classes. The stock `Button` keeps its `transition-all` and its nudge when pressed. A link styled as a button is an `<a>` with `cn(buttonVariants({ variant }), "px-4 font-mono text-label")`: `buttonVariants` alone does not merge classes.
- **Button groups:** a set of buttons where one is chosen (a filter) is a `role="group"` with `flex flex-wrap gap-1` and an `aria-label`. Every button in it is `variant="secondary"` with `aria-pressed` and the `className` `aria-pressed:border-foreground aria-pressed:font-bold` beside the Buttons note's classes: the chosen one, `aria-pressed="true"`, is marked by a 1px graphite border and bold text, never by a fill of its own. The stock button's `border-transparent` is what this border replaces.
- **Badges:** a badge marks a part: "New", "Made to order". It is the shadcn `Badge` (`npx shadcn@latest add badge`) in the `outline` variant with `className="h-auto rounded-sm border-transparent bg-tertiary px-2 py-1 font-mono text-label text-tertiary-foreground"`: mid steel with graphite text. The corner must be in this `className`, or the stock pill wins. One or two words, no icon. Steel is a fill only: never write `text-tertiary`.
- **Stock states:** a stock state is a status dot and a word, `inline-flex items-center gap-2 whitespace-nowrap`. The dot is `size-2 shrink-0 rounded-full` with `aria-hidden="true"`: `bg-success` for "In stock", `bg-muted-foreground` for "Low stock" and "Made to order", `bg-destructive` for "Discontinued". The word is `text-body text-foreground`. The dot is the only circle on these pages, and the green and the red appear nowhere but beside their word: never show a state by color alone, never color a row, a cell or a panel.
- **Inputs:** the shadcn `Input` (`npx shadcn@latest add input`) with `className="text-body md:text-body"`, its stock height and corner. In `components/ui/input.tsx`, replace the class `bg-transparent` and the class `dark:bg-input/30` with `bg-card`, so a field is white on the white panel and matches the selects; `file:bg-transparent` and the `disabled:` fills stay. A quantity field is also `font-mono tabular-nums text-right` with `inputMode="numeric"`. A field is a `flex flex-col gap-3`: a visible `<label>` with `block`, styled as a label and tied to the field with `htmlFor`; then a stack with `gap-2` holding the field, any helper text (a small figure) and the error line when there is one. Name the helper text in the field's `aria-describedby`. An optional field says so in its label, "Phone (optional)"; a required one has no mark.
- **Selects:** a native `<select>` with `h-8 w-full rounded-lg border border-input bg-card px-2 text-body text-foreground aria-invalid:border-destructive` and the focus outline from the rules (`focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`). Set a width on an element around it, not on the `<select>`. Same label and stack as any field.
- **Checkboxes:** the shadcn `Checkbox` (`npx shadcn@latest add checkbox`) with `className="rounded-sm"`, the theme's 2px corner. Each option is a `<label>` with `flex items-center gap-2 text-body text-foreground` wrapping the `Checkbox` and a `<span>` with its text, so the whole line toggles it; the `Checkbox` also gets `aria-labelledby` naming that `<span>`'s `id`. A set of options is a `<div>` with `role="group"`, `aria-labelledby` naming its label (a `<p>` styled as a label), and `flex flex-col gap-2`, the label its first item.
- **Errors:** an error line is `flex items-start gap-2 font-mono text-label text-destructive`, not a label (a sentence), holding the `CircleAlert` icon (`shrink-0`) 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.
- **Confirmations:** a message after an action goes into a `role="status"` element that is on the page from the start, empty, or it may not be announced. It is never hidden, has no classes that draw or take space, and is never an item of a `gap-*` stack or row: put it in a plain `<div>` with the element above it. The message inside it, rendered only when there is one, carries the look and the space, such as `mt-3`. Where a message replaces a form, the form and the status element share that `<div>`. Where the message replaces the control that had focus, give its heading or first element `tabIndex={-1}` and move focus to it.
- **Description lists:** a list of properties is a `<dl>`. Each row is `flex items-baseline justify-between gap-3 py-2` with a 1px top border except the first: the `<dt>` a label in `text-muted-foreground`, the `<dd>` in `text-right`, `font-mono tabular-nums` when it is a figure and `text-body` when it is a phrase.
- **Tables:** the shadcn `Table` (`npx shadcn@latest add table`) with `text-body`. The header row is mono: header cells are labels in `text-muted-foreground`, keep their stock `h-10`, `font-medium` and `whitespace-nowrap` (so a header is one short line) and get `px-4`. Body and footer cells get `px-4 py-2`. Rows are divided by the stock 1px borders and keep the stock `TableRow` hover fill, also where a row does nothing; no zebra stripes and no colored rows. Cells that hold a sentence get `whitespace-normal`. No `<caption>`, which would scroll away: the table's visible name is the heading above its box, and the `label` prop below repeats its words. A wide table scrolls sideways in its own box, which is fine on a phone: edit `components/ui/table.tsx` to give its scroll box `tabIndex={0}`, `role="region"`, an `aria-label` from a required `label` prop, and the focus outline drawn inside (`focus-visible:-outline-offset-2`, since a table here fills a panel that clips). A total row sits in a `<tfoot>`: the stock `TableFooter` ships a fill and `font-medium`, so give it `bg-transparent`, and each of its `TableRow`s `hover:bg-transparent`.
- **Drawings:** a part is drawn in outline with plain elements, never an image file, an SVG or an icon: 1px lines in the text color (`border-foreground`) on the panel's white, with no fill of their own, and its dimensions in labels in `text-foreground`. Every bordered box of a drawing has square corners: no `rounded-*` class, since a bordered box with rounded corners inside a panel reads as a card inside a card. No circles, no gradients, no hatching and no shadows. Sizes that come from the part's millimetres are inline `style` widths and heights at the scale this brief gives. The drawing has `aria-hidden="true"`; the figure's caption gives every dimension in words.
- **Icons:** from `lucide-react`, with `size-4` and `strokeWidth={1.5}`, only where this brief or these notes name one, with `aria-hidden="true"` beside a word. Never emoji.
- **Corners:** `rounded-sm` (2px) for badges and checkboxes, `rounded-lg` (4px, what they ship with) for buttons, inputs and selects, `rounded-xl` (8px) for panels, `rounded-full` only for the status dot, which is `size-2`, under the theme's 12px limit. No avatars and no pills.
- **Surfaces:** flat. No shadows, blur or gradients in the code you write. A stock component that floats over the page keeps the one short shadow it ships with.
- **Menus on small screens:** below `md` the nav links go behind a "Menu" button (`variant="secondary"`, `md:hidden`) with `aria-expanded` and `aria-controls`, at the right end of the header's row (`ml-auto`). It opens a second `<nav>` under the row, inside the `<header>`, also named "Main" because only one of the two is ever shown: it has `md:hidden`, the `hidden` attribute while closed, the `id` that `aria-controls` names and a full-width 1px top border; in its container, a `<ul>` with `py-2`, each link a block with `py-2`. Escape closes it only while focus is inside the menu or on its button, and focus then goes to the button. Do not use a `Sheet` or a `Dialog`. Whether the menu is open is page state, and a page that has no other state still has this one.
- **Motion:** in the code you write, only colors transition. Nothing moves on scroll. Stock components keep the motion they ship with.

## The page

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

Sample content, if the user gave none: "Torvale Bearings Ltd" and its part `TVL-2047-RS`, a deep groove ball bearing with a 20 mm bore, a 47 mm outside diameter and a 14 mm width, with two rubber seals. Dynamic load rating 13.40 kN, static load rating 6.55 kN, limiting speed 13,000 r/min, mass 0.103 kg, operating temperature −30 to +110 °C. 1,240 in stock at Brigholt; more in 10 working days. Price breaks per bearing: 1 to 49 at £2.94, 50 to 249 at £2.61, 250 to 999 at £2.28, 1,000 and over at £1.97.

This page is divided into sections: the container inside `<main>` is `flex flex-col gap-12` with two items, a `<div>` with `flex flex-col gap-6` holding the page header and the grid row of parts 3 and 4, then the Specification section.

This page uses the stock `button`, `input` and `table`; add any the project lacks with `npx shadcn@latest add button input table`.

Build these parts in order; the footer is as the layout notes describe it.

### 1. Header

The product's name, the four nav links with Catalogue marked as the current section (`aria-current="true"`, as the Breadcrumb note says), the sales line at the right end from `md` up, and the "Menu" button below `md`.

### 2. Page header

On the left, the breadcrumb: `Catalogue` (`/catalogue`), `Deep groove` (`/catalogue`) and the current step `TVL-2047-RS`. Then the `<h1>` "TVL-2047-RS" in `font-mono text-h1`, and under it, as the stack's last item, one line in `text-body`: "Deep groove ball bearing, 20 × 47 × 14 mm, two rubber seals". On the right, two actions: links styled as `secondary` buttons, "Compare" (`/compare`) and "Data sheet" (`/downloads`).

### 3. Drawing

A grid row. The drawing is a panel spanning five columns from `lg` up, full width below, with the heading `<h2>` "Drawing" and, at the right end of its heading row, a small figure, `Dimensions in mm`. Its content is a `<figure>` with `flex flex-col gap-4`.

The drawing is the figure's first item, a `<div>` with `aria-hidden="true"` and `flex items-start justify-center gap-2 py-6`. It is a side view of the bearing at 4px to the millimetre: the bearing's axis runs across, so its width B is the drawing's width and its outside diameter D its height. Every size below is an inline `style` in pixels, worked out in code from the part's millimetres times 4, so another part draws right. Every line is 1px `border-foreground`; no box has a `rounded-*` class or a fill. Three items, left to right:

1. **Bore dimension:** a `<div>` with `flex items-center gap-2`, as tall as the body (`height` 188px, D × 4): a label `d 20` in `font-mono text-label tabular-nums text-foreground`, then a bracket, `w-2 border-y border-l border-foreground`, `height` 80px (d × 4). The parent centers it, so it spans the bore.
2. **The part:** a `<div>` with `flex flex-col items-center gap-2`. First the body, `relative border border-foreground`, `width` 56px (B × 4) and `height` 188px (D × 4). Inside it, the bore as two hidden lines, each `absolute inset-x-0 border-t border-dashed border-foreground`: the upper one with `top` 54px and the lower one with `bottom` 54px, both (D − d) / 2 × 4. Under the body, the width dimension: a bracket `h-2 self-stretch border-x border-b border-foreground`, then the label `B 14`, styled as the other labels.
3. **Outside dimension:** a `<div>` with `flex gap-2`, `height` 188px: a bracket `w-2 self-stretch border-y border-r border-foreground`, then the label `D 47`, which sits in the middle of the height (`self-center`).

The drawing is about 190px wide, which fits the panel at 375px. The `<figcaption>`, the figure's last item, is a small figure: "Side view of TVL-2047-RS. Bore d 20 mm, outside diameter D 47 mm, width B 14 mm. The dashed lines show the bore."

### 4. Price and stock

A panel spanning seven columns from `lg` up, full width below, with the heading `<h2>` "Price and stock". Under its heading row, three parts, the panel's `gap-4` between them:

**Stock.** A `<dl>` as the Description lists note sets it, with three rows:

- "Stock": the stock state "In stock" with its success dot, then, `gap-2` after it in a `flex flex-wrap items-center justify-end gap-2`, the small figure `1,240 at Brigholt`.
- "Dispatch": "Same working day if ordered by 15:00", a phrase in `text-body`.
- "Lead time beyond stock": "10 working days", a phrase in `text-body`.

**Price breaks.** A stack with `gap-3`: an `<h3>` "Price breaks" as the Headings note sets it, then the shadcn `Table` with the label "Price breaks" and three columns: "Quantity" (the range, `1 to 49`, `50 to 249`, `250 to 999`, `1,000 and over`, in `font-mono tabular-nums`, right-aligned, header too), "Unit price (£)" (`2.94`, `2.61`, `2.28`, `1.97`, right-aligned, header too) and "Your quantity" (left-aligned). This table sits in a padded panel, so the first and last cells of every row, the header row included, get `first:pl-0 last:pr-0`, and its edges line up with the text above. The row that the quantity in the field falls in is marked in weight and in words, never by a fill: its three cells get `font-bold`, and its last cell holds the word "Applies" in `text-body`; the other rows' last cells are empty. While the field holds no valid quantity, no row is marked.

**Add to quote.** A `<form>` with `noValidate`, then the confirmation element, in one plain `<div>`. The form is a stack with `gap-4`:

- A row, `flex flex-wrap items-end gap-3`: the field "Quantity" in a wrapper with `w-32`, an `Input` with `inputMode="numeric"`, the quantity look of the Inputs note, starting at `1`; and the default button "Add to quote" with `type="submit"`. The field's helper text and its error line go under the row, not in the narrow wrapper: a stack with `gap-2` after the row holds the helper text, a small figure, "1,240 in stock. Larger orders ship in 10 working days.", and the error line when there is one. The field names both in its `aria-describedby`.
- The line total, a stack with `gap-1`: the label "Line total", then the total in `font-heading text-h1 tabular-nums`, `£2.94` for 1, then a small figure with the sum, `1 at £2.94 each`. While the field holds no valid quantity, the total reads `£0.00` and the small figure "Enter a quantity to see the price.".

The confirmation, rendered inside the status element only after a click, is a `<p>` with `mt-3 flex items-start gap-2 text-body`: the `Check` icon in `text-success`, then "Added to your quote: 250 at £2.28 each, £570.00." and, after one space, a text link "View quote" to `/quote`.

### 5. Specification

A `<section>` named by its `<h2>` "Specification" in `text-h2`, laid out as `flex flex-col gap-4`, then a grid row:

**Specification table.** A panel with rows spanning eight columns from `lg` up, full width below, with no heading row of its own: the section's `<h2>` names it. The shadcn `Table` with the label "Specification" and four columns: "Property" (in `text-body`), "Symbol" (`font-mono`), "Value" (right-aligned, header too) and "Unit" (`font-mono`). A value that is a figure is `font-mono tabular-nums`; a value that is a phrase is `text-body`. A row with no symbol or no unit leaves that cell empty. The fourteen rows, in this order:

1. Bore diameter, d, 20, mm
2. Outside diameter, D, 47, mm
3. Width, B, 14, mm
4. Dynamic load rating, C, 13.40, kN
5. Static load rating, C0, 6.55, kN (write `C0` as two plain characters: a `<sub>` would take a size outside the type scale)
6. Limiting speed, no symbol, 13,000, r/min
7. Mass, no symbol, 0.103, kg
8. Operating temperature, no symbol, `−30 to +110`, °C
9. Seals, no symbol, "Two rubber seals, RS", no unit
10. Cage, no symbol, "Pressed steel", no unit
11. Ring material, no symbol, "Chrome steel, 100Cr6", no unit
12. Grease, no symbol, "Lithium, 20% fill", no unit
13. Radial clearance, no symbol, "CN, normal", no unit
14. Precision class, no symbol, "P0, normal", no unit

**Documents.** A panel with rows spanning four columns from `lg` up, full width below. Heading row: the `<h3>` "Documents" in `text-h2` (an `<h3>`, to keep heading order under the section's `<h2>`) and, at its right end, a text link "All documents" to `/downloads`, as a label in `text-foreground`. Three rows, each a stack with `gap-1`: a text link to `#` in `text-body`, then a small figure.

- "Data sheet, TVL-2047-RS", then `PDF · Rev C · 0.84 MB`
- "3D model, TVL-2047-RS", then `STEP · Rev C · 1.62 MB`
- "Certificate of conformity, 2026", then `PDF · Rev A · 0.21 MB`

### Behavior

- The quantity field is read as a whole number after trimming spaces and dropping thousands commas, so `1,000` is 1000. A valid quantity is a whole number from 1 to 10,000. While the field holds one, the line total, its small figure and the marked price break follow it at every keystroke: 250 gives `£570.00` and `250 at £2.28 each`; 1,000 gives `£1,970.00` and `1,000 at £1.97 each`. Totals are worked in pence, so they never come out as `£569.9999`.
- "Add to quote" checks the field. Empty: "Enter a quantity." Anything else that is not valid: "Enter a whole number from 1 to 10,000." Focus moves to the field after the error line is rendered. With a valid quantity it puts the confirmation into the status element, worded from the quantity, its unit price and its total; a second click replaces it with the new wording. The quote is not stored: the quote page has its own sample lines.
- "Add to quote" is never disabled, so no button on this page gets `focusableWhenDisabled`, and `components/ui/button.tsx` needs no `data-disabled:` classes.
- "Compare", "Data sheet" and 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`, `4`, `6`, `12`) plus 0, 1px and 2px. No arbitrary values such as `p-[18px]`, and no other steps such as `p-5` 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 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 give every number its unit, once, where the eye finds it.
- Do keep the page grey: a state color appears only beside its word.
- Do let a table be a table: no cards for rows of comparable figures.
- Don't add an accent color, a gradient or a tinted panel.
- Don't round a figure the buyer has to rely on, or hide a part number behind a click.
- 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 drawing is plain elements in 1px lines of the text color with square corners, has `aria-hidden=true`, and its caption gives every dimension in words.
- [ ] The part number `TVL-2047-RS` is in the `<h1>` in full, in the mono font, and no figure on the page is rounded beyond the decimals the brief gives.
- [ ] The price break that applies to the quantity typed is marked in bold and in words, and the line total follows the quantity.
- [ ] A quantity that is not a whole number from 1 to 10,000 gets an error in words with an icon, tied to the field, and Add to quote then adds nothing.
- [ ] After Add to quote a confirmation is announced from a `role=status` element that was on the page from the start, and the only default button on the page is Add to quote.
- [ ] 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.
