# Build a deploys list with the Cipher theme

You are building one page in an existing project: the deploy console's list of deployments, with filters, a table of commits, branches, environments, states, durations and times, and the log of the selected deployment. 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 Cipher variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/cipher.json` and save https://strictthemes.com/themes/cipher/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/cipher/tailwind.css instead. One of the theme's fonts is Geist, which a new shadcn project already imports: keep that import. If the global CSS imports any other font the theme does not use, 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:** JetBrains Mono for headings, Geist for body text, JetBrains Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 44px/800, `text-h1` 30px/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`; the two classes are the same family in this theme.
- **Spacing scale**, with the Tailwind step in brackets: 4px (`1`), 8px (`2`), 12px (`3`), 16px (`4`), 24px (`6`), 48px (`12`).
- **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 #5B32E6, on-primary #FFFFFF, secondary #0E1116, on-secondary #FFFFFF, tertiary #C6F135, on-tertiary #0E1116, neutral #565E6E, background #F1F3F1, surface #FFFFFF, foreground #0E1116, border #D3D8DA, error #C4281C, success #177A45.

### How Cipher pages are laid out

- **Fonts:** headings, labels, code and figures are JetBrains Mono; body text is Geist. `font-heading` and `font-mono` are the same family here: write `font-mono` on labels, code and figures, and `font-heading` only on heading-sized text that is not a heading element.
- **Page:** left-aligned, nothing centered or justified; the outermost element is `min-h-svh bg-background text-body text-foreground`.
- **Container:** the header's row, the main content and the footer's row sit in a centered container, `mx-auto max-w-[1200px]`, with `px-4`, and `px-6` from `md` up. This width is the one arbitrary value.
- **Grid:** where this brief gives columns, the element is a 12-column grid with `gap-6` (24px gutters), and a single column below the breakpoint the brief names (`md` unless it says `lg`). The grid's `gap-6` is also the vertical distance between its items when they stack: its items get no top margin. A grid item that holds code or a table gets `min-w-0`, or a long line widens it past the screen.
- **Vertical rhythm:** inside a column, a panel or a section, the distance between two blocks is a top margin on the lower one, never `space-y-*` or a vertical `gap` on a stack. The first element of a container has no top margin. The steps: `mt-2` between a label and the thing it labels, between a field's label and the field, and between the items of a list of text (links that are blocks with their own `py-*`, as in a menu or a sidebar, get no margin); `mt-3` between a heading and the text under it and between paragraphs; `mt-4` between form fields; `mt-6` between blocks (a code block, a table, a note, a form, a row of buttons) and between a block and the paragraph after it; `mt-12` between the sections of a page. Inside a row (`flex` on one line), the distance is a `gap-*`: `gap-2` between a word and its icon or dot and between buttons that belong together, `gap-3` between the items of a row of figures. Where this brief gives a margin, it replaces the step.
- **Headings:** the `<h1>` is `text-h1`; `text-display` only where this brief says so. An `<h2>` is `text-h2`. An `<h3>` is `text-body font-bold`, in the mono font it gets as a heading. Sentence case, no full stop; a command in a heading keeps its case (`plinth deploy`).
- **Labels:** eyebrows, field labels, table headers, captions, nav links, the name strip of a code block, and button and badge text are labels: `font-mono text-label`. Write them in sentence case, or lowercase where this brief writes them so. Nothing is uppercased, with a class or in the text. A label is a short fragment; only helper text and error lines, set in the label style, are sentences. A label on its own line is a `<p>` or other block. Values from the tool (a branch, an environment, a scope, a kind of change, a state) are lowercase, as the CLI prints them: `main`, `production`, `deploy:write`, `fixed`, `ready`.
- **Figures:** commands, paths, flags, versions, commit hashes, tokens, durations, dates and times are always `font-mono`, in running text too, at the size of the text around them; standing on their own (a table cell, a row of figures) they also get `tabular-nums`. A "small figure" is one in `font-mono text-label tabular-nums text-muted-foreground`. Every date and time shown is a `<time>` with a `dateTime`, written one way: a date `10 Nov 2026` (no leading zero, the month's first three letters; `dateTime="2026-11-10"`), a date with a time `11 Nov 2026, 14:05` (24-hour; `dateTime="2026-11-11T14:05"`), a log line's time `14:05:12` (`dateTime="2026-11-11T14:05:12"`). In a table, durations and times are right-aligned, and so is their header (`text-right` on the `TableHead`). A cell with no value holds an em dash, `—`.
- **Text color:** headings, body text, field labels, values and code are `text-foreground`. Other labels, small figures, helper text, code comments and a terminal's prompt sign are `text-muted-foreground`. Paragraphs are never muted. `text-primary` is for links and, where this brief says so, a config file's keys. `text-tertiary` is never used: lime is a fill only.
- **Measure:** a paragraph of body text is at most `max-w-lg` wide (512px).
- **Links:** a link this brief gives no address goes to `#`. The product's name goes to `/`, Docs to `/docs`, Changelog to `/changelog`, Open console to `/deploys`, and a link to the page itself to its own address. A text link is `text-primary underline underline-offset-4 hover:text-foreground transition-colors duration-150` and inherits its size. The product's name in a header is `text-foreground` and never current. Other links in a header, a footer or a sidebar are `text-muted-foreground hover:text-foreground transition-colors duration-150`, without an underline; the link for the current page has `aria-current="page"` and is `text-foreground underline underline-offset-4`, so the mark is not color alone.
- **Site header:** on the landing, docs and changelog pages, the first thing is a `<header>` with `border-b border-border`, running the full width of the window. Inside the container, one row, `flex flex-wrap items-center gap-x-6 gap-y-2 py-3`: on the left the product's name, a link to `/` in `font-heading text-h2`, lowercase (`plinth`); then a `<nav>` with the `aria-label` "Main" holding the nav links as labels with `gap-6`; then the header's actions in one `flex items-center gap-3` element at the right end (`ml-auto`).
- **Console bar:** on the deploys and tokens pages, the site header's place is taken by a full-width `<header>` with `border-b border-border bg-card` and the same row: the product's name, then the project as a small figure (`orrin / orrin-web`), then the `<nav>` "Main", then at the right end the signed-in person's email as a small figure, hidden below `md`. Below `md` its nav links go behind the "Menu" button.
- **Footer:** site pages only: a full-width `<footer>` with `mt-12` and `border-t border-border`. Inside the container, one row, `flex flex-wrap items-center gap-x-6 gap-y-2 py-6`: the copyright as a label in `text-muted-foreground` (`© 2026 Plinth Labs`), then at the right end (`ml-auto`) a `<nav>` with the `aria-label` "Footer" holding three footer links as labels, `flex flex-wrap gap-x-6 gap-y-2`.
- **Main:** `<main>` holds the container. On the site pages `<main>` has `pt-12` and no bottom padding (the footer's `mt-12` gives the space). On the console pages `<main>` has `py-6`, and its container is a 12-column grid whose first item, spanning all twelve columns, is the page header: a row that is not a panel, `flex flex-wrap items-center justify-between gap-3`, with the `<h1>` on the left and the page's actions on the right, `gap-2` apart.
- **Panels:** a panel is a plain element with `rounded-xl border border-border bg-card p-6`, no shadow. Not the shadcn `Card`, which ships its own padding and a ring. Never put a panel or a bordered code block inside a panel: divide a panel's parts with 1px borders.
- **Panel heading:** a panel opens with a heading row, unless this brief says it has none: `flex min-h-8 flex-wrap items-center justify-between gap-3`, holding the panel's heading, an `<h2>` or the `<h3>` this brief names, as a label heading: `font-mono text-label text-muted-foreground` in place of the heading style of its level, and, at its right end, anything that belongs to the whole panel. Content follows with `mt-4`.
- **Panels with rows:** a panel that holds a table, a list of rows or a log has no padding of its own and `overflow-hidden`. Its heading row sits in a wrapper with `px-6 py-3` and a 1px bottom border. A row that closes such a panel (paging, a form) has a 1px top border and `px-6 py-4`.
- **Buttons:** the shadcn `Button` with `className="px-4 font-mono text-label"`. It keeps its stock height and `rounded-lg` corners. The default variant (violet) is for the one main action of a screen: at most one enabled default button per screen. Other actions are `variant="secondary"` (near-black, light in dark mode). Its stock hover changes nothing in this theme: in `components/ui/button.tsx`, in the `secondary` variant, replace the class `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]` with `hover:bg-secondary/80`, once, and add no hover class to a secondary button you write. Quiet actions are `variant="outline"`. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. 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. Button text: sentence case, one to three words. An icon in a `Button` keeps the button's stock gap; the rhythm's `gap-2` is for rows you write. No button here keeps the focus while its own click disables it ("Clear filters" moves focus to the search field), so none gets `focusableWhenDisabled` and `components/ui/button.tsx` needs no `data-disabled:` classes.
- **Button groups:** a set of buttons where one is chosen (a filter) is a `role="group"` with an `aria-label` and `flex flex-wrap gap-2`. The chosen button has `aria-pressed="true"` and `variant="secondary"`; the rest have `aria-pressed="false"` and `variant="outline"`.
- **Badges:** the shadcn `Badge` with `className="h-auto rounded-sm px-2 py-1 font-mono text-label"`, in the `outline` variant. The signal badge adds `border-transparent bg-tertiary text-tertiary-foreground`. A page has at most one lime mark, where this brief puts it.
- **Inputs:** the shadcn `Input` with `className="text-body md:text-body"`, keeping its stock height, padding and corners. In `components/ui/input.tsx` remove the class `dark:bg-input/30` (`disabled:bg-input/50` and `dark:disabled:bg-input/80` stay), so a field shows the surface behind it in both modes, as a select does. Every field has a visible `<label>` above it with `block` and `htmlFor`, in `font-mono text-label text-foreground`; the field has `mt-2`. Helper text under a field is a small figure with `mt-2`, named in the field's `aria-describedby`.
- **Selects:** a native `<select>` with `h-8 w-full rounded-lg border border-input bg-transparent px-2 text-body aria-invalid:border-destructive`, its width set on a wrapper, with a label, errors and the focus outline like any field. A select of tool values (a scope) adds `font-mono`.
- **Errors:** an error is a line with `mt-2` under its field: `flex items-start gap-2 font-mono text-label text-destructive`, holding the `CircleAlert` icon (`size-4 shrink-0`, `aria-hidden="true"`, no margin: `items-start` lines its top up with the 15.6px line) and a sentence that says what to do. The field gets `aria-invalid`, and its `aria-describedby` lists the line's id after the helper text's. Fields are checked in code when the form is sent, with `noValidate` on the `<form>`, and focus moves to the first field with an error. An error line is removed when its own field changes (on `input` or `change`); every other error stays until the next submit.
- **Confirmations:** a message that appears after an action goes into an element with `role="status"` that is on the page from the start, empty, and never hidden. The status element itself has no classes that draw or take space: the message inside it, rendered only when there is one, carries the margin and any fill. A message that replaces a form shares one wrapper with it, which carries the top margin; buttons that follow the message ("Copy", "Done") sit beside the status element in that wrapper, not inside it. Where the message replaces the control that had focus, give it `tabIndex={-1}` and move focus to it.
- **State:** a state is a dot and a word, side by side: `inline-flex items-center gap-2`. The dot is `size-2 shrink-0 rounded-full` with `aria-hidden="true"`: `bg-success` for passed or ready, `bg-destructive` for failed, `bg-primary` for under way, `bg-muted-foreground` for waiting or stopped. The word is `text-foreground`, lowercase. Never show a state by color alone. `text-success` and `text-destructive` are only for a short message or log line that also says in words what it is, and only on `bg-card` or `bg-background`, never on `bg-muted`.
- **Code blocks:** a `<figure>` with `rounded-xl border border-border bg-card overflow-hidden`. First a strip, `flex items-center justify-between gap-3 border-b border-border px-4 py-2`: the file name or the language as a label in `text-muted-foreground` (a `<figcaption>`), and at the right a "Copy" `Button` in `variant="outline"` and `size="sm"` with the button classes. Then the `<pre>`, `overflow-x-auto p-4 font-mono text-body`, scrolling sideways as the rules say, with an `aria-label` such as "Code: plinth.toml" and its outline drawn inside. Lines never wrap; comments are `text-muted-foreground`. After a copy, the button reads "Copied" with a `Check` icon for two seconds; if the copy fails, "Copy failed" for as long. Beside it, a visually hidden `role="status"` element, there from the start, says the same; it is the one announcement, since the button's changed text is not a live region. It copies the code alone, with no prompt sign or output, through `navigator.clipboard.writeText`.
- **Inline code:** a `<code>` in running text is `rounded-sm bg-muted px-1 py-0.5 font-mono`, at the size of the text around it.
- **Tables:** the shadcn `Table` with `text-body`. Header cells are labels in `text-muted-foreground`; they keep their stock height. Every cell gets `px-3`, the first `first:pl-6` and the last `last:pr-6`; body cells also get `py-2`. Stock cells do not wrap: a cell that holds a sentence gets `whitespace-normal`. Rows keep their 1px borders and stock hover, also where a row does nothing: no stripes. No `<caption>`: the heading above the table names it on screen, and the `label` names its scroll box. A table here fills a panel with rows and scrolls sideways in its own box: 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 it (`focus-visible:-outline-offset-2`).
- **Icons:** from `lucide-react`, with `size-4`, `strokeWidth={1.5}` and `aria-hidden="true"`, beside a word or above a heading, and only where this brief or these notes name one. No emoji, and no check or cross typed as a character.
- **Corners:** `rounded-sm` (3px) for badges and inline code, `rounded-lg` (6px, as shipped) for buttons, inputs and selects, `rounded-xl` (10px) for panels, code blocks and the terminal, `rounded-full` only for status dots.
- **Surfaces:** flat: no shadows, glows, blur, gradients or scanlines in your code.
- **Menus on small screens:** below `md` (below `lg` on the docs page) the nav links, and what this brief names, go behind a "Menu" button (`variant="outline"`) with `aria-expanded` and `aria-controls`, at the right end of the row. It shows them inside the `<header>`, under its row: a second `<nav>` with the same `aria-label`, shown only below the breakpoint, with the `id` that `aria-controls` names and the `hidden` attribute while closed; at any width one of the two is hidden, so they may share a name. It has a 1px top border across the full width and, inside the container, `py-2`; each link, "Open console" and "Sign in" too, is a block with `py-2` in the header-link style, not a button. The "Menu" button has `ml-auto` and is shown only below the breakpoint (`md:hidden`, `lg:hidden` on docs); the row's `<nav>` and actions element are hidden below it (`hidden md:flex`, `hidden lg:flex` on docs). Escape closes it and refocuses the button. No `Sheet`, no `Dialog`, and one "Menu" button per page.
- **Motion:** in the code you write, the only transition is color. Nothing blinks, types itself out or moves on scroll.

## The page

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

Sample content, if the user gave none: the console of **Plinth**, a deploy tool by Plinth Labs. The team is Orrin and the project `orrin-web`; the signed-in person is `mara.lind@orrin.example`. Invent 20 deployments over the last three days, the newest on 11 November 2026, the sample's fixed today (no clock): commits as seven-character hashes (`8f3c2a1`) with a one-line message, branches (`main`, `feat/search`, `fix/cart-total`, `chore/deps`), environments `production` (always from `main`) and `preview` (all other branches). States: one `queued` (the newest), one `building` (the one before it), two `failed`, one `canceled`, the rest `ready`. Durations from `38s` to `3m 12s`.

Build these parts in order.

### 1. Console bar

The console bar from the layout notes. Nav links, in order: Deploys (`/deploys`, the current page), Domains (`#`), Logs (`#`), Settings (`/settings/tokens`).

### 2. Page header

The `<h1>` "Deploys". The action is the default button "Deploy main".

### 3. Filters

A panel spanning twelve columns, with no heading row; the panel element itself is the row, `flex flex-wrap items-end gap-6`. Add the components with `npx shadcn@latest add input table badge`.

- A search field with the visible label "Branch" and the placeholder "main, feat/…", in a wrapper with `w-64`, full width below `md`. It matches any part of the branch name, ignoring case.
- The environment filter: a visible label "Environment" (a `<p>` styled as a field label, since a `<label>` cannot name a group), and with `mt-2` a button group with All, Production and Preview. Here the group's name comes from the `<p>` through `aria-labelledby`, in place of the notes' `aria-label`.
- The state filter: a labelled `<select>` "State" in `font-mono`, with All states, `ready`, `building`, `queued`, `failed` and `canceled`, in a wrapper with `w-48`.
- At the right end (`ml-auto`), an `outline` button "Clear filters", disabled while no filter is set.

### 4. Deployments

A panel with rows spanning twelve columns. Its heading row holds the label heading `Deployments` and, at the right end, a small figure with `aria-live="polite"`: `20 deployments`, or `6 of 20` when filters are set (`0 of 20` when none match).

The table's `label` is "Deployments". Give the `<table>` `min-w-4xl`. Columns:

- **Commit:** the hash as a `<button>` in `font-mono`, styled as a text link, with the `aria-label` "Show log of 8f3c2a1". It is what selects the row.
- **Message:** the commit message, with `whitespace-normal`.
- **Branch:** `font-mono`.
- **Environment:** the word, lowercase. On the newest `ready` deployment to `production`, the signal badge "live" follows the word, with `gap-2`: the page's one lime mark.
- **State:** a state from the layout notes: `ready` passed, `failed` failed, `building` under way, `queued` and `canceled` waiting or stopped.
- **Duration:** right-aligned, `font-mono tabular-nums`. A queued deployment shows an em dash, `—`, with `aria-hidden="true"` and "not started" in a visually hidden span after it; the building one an em dash with "not finished" hidden the same way; a canceled one the time it ran before it was canceled.
- **Created:** right-aligned, a `<time>` with a `dateTime` in `font-mono tabular-nums` (`11 Nov 2026, 14:05`).

The selected row has `data-state="selected"` and `data-[state=selected]:bg-secondary/10`: the stock fill for a selected row cannot be told from the panel in dark mode. Its hash button has `aria-pressed="true"` and is `font-bold text-foreground no-underline`, so the mark is more than a color; the others have `aria-pressed="false"`.

When nothing matches, the table's body is one row with one cell across all columns, `whitespace-normal py-6`: the sentence "No deployments match these filters." and, with `mt-3`, an `outline` button "Clear filters".

### 5. Log

A panel with rows spanning twelve columns, under the table. Its heading row holds the label heading `Log` and, at the right end, a small figure naming the deployment: `8f3c2a1 · production · ready`. It is not a live region: the count above is the page's one, and the hash buttons' `aria-pressed` tells which log shows.

Under the heading row, the log: a `<div>` that scrolls on its own, both ways: `max-h-96 overflow-auto py-3`, with `tabIndex={0}`, `role="region"`, the `aria-label` "Log of 8f3c2a1" and the focus outline drawn inside it. It holds an `<ol>` with no markers and no padding, `w-max min-w-full`, so that each line is as wide as the longest and keeps its right padding at the scroll end. The lines have no margin between them, in place of the notes' `mt-2` for list items. Each line is an `<li>` with `flex gap-3 px-6 font-mono text-label whitespace-pre`: the time as a small figure in a `<time>` (`14:05:12`), then the text in `text-foreground`. Lines do not wrap; on a phone the box scrolls sideways.

Write 8 to 12 lines per deployment from its data: cloning the commit, installing, building, uploading, then the end. A `ready` deployment ends with a line whose first word, `ready`, is `text-success`, followed by the address and the time taken. A `failed` one has a line starting with the word `error`, in `text-destructive`, then the reason, and ends with `failed: build exited with code 1`, the word `failed` in `text-destructive`. A `building` one ends at its last line so far; a `queued` one has the single line `queued behind 1 deployment`; a `canceled` one ends with `canceled by mara.lind`.

### Behavior

- All of it works on the sample data in component state.
- The table is sorted by Created, newest first. The `building` deployment, the second row, is selected at the start.
- The three filters work together. "Clear filters" empties the search, chooses All and All states, and moves focus to the search field.
- If the selected deployment is filtered out, the first row shown is selected; when rows show again after none did, the first row shown is selected. When no row is shown, the log panel's figure reads `No deployment selected`, the box's `aria-label` is "Log", and in place of the `<ol>` it holds a `<p>` in `text-body` with `px-6`: "No deployment matches these filters, so no log is shown."
- Choosing a hash selects the row and shows its log; focus stays on the button.
- The "Menu" button works as the layout notes say. "Deploy main" and the links need no other behavior.

## Rules

- **Colors:** only the theme's colors, through the semantic classes (`bg-background`, `text-foreground`, `bg-card`, `bg-primary`, `text-primary-foreground`, `text-muted-foreground`, `border-border` and so on). No hex, rgb or named colors in components, and no Tailwind palette classes such as `bg-blue-500` or `text-gray-600`.
- **Spacing:** padding, margin and gap use only the scale's steps (`1`, `2`, `3`, `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 set every command, path, flag and version in the mono font.
- Do keep lime to one mark per screen.
- Do left-align, and let lines be long before you center anything.
- Don't use violet as a fill for large areas, and never as a gradient.
- Don't use glows, neon outlines or scanline effects. This is a tool, not a poster of one.
- 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 state in the table is a dot and a lowercase word, never a color alone.
- [ ] The branch search, the environment filter and the state filter work together, a count says how many deployments match, and an empty result shows a message and a way to clear the filters.
- [ ] Choosing a deployment's commit hash shows its log in the log panel, whose lines do not wrap, and the chosen row is marked by more than its background.
- [ ] Commit hashes, branches, durations and times are in the mono font, and durations and times are right-aligned.
- [ ] The one lime mark is the "live" badge on the deployment that serves production.
- [ ] 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.
