# Build a settings page with the Cipher theme

You are building one page in an existing project: the deploy console's settings page for API tokens, with a table of tokens, a form that creates one and shows it once, and a way to revoke a token by typing its name. 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; team Orrin, project `orrin-web`, signed in as `mara.lind@orrin.example`. Today is the fixed sample date, 11 November 2026, not the clock. Four tokens to start: `ci-deploy` (scope `deploy:write`, created 14 Aug 2026, last used 11 Nov 2026, 09:12, expires never), `staging-bot` (`deploy:write`, created 2 Sep 2026, last used 10 Nov 2026, 17:40, expires 1 Dec 2026), `metrics-reader` (`read`, created 19 Sep 2026, last used 11 Nov 2026, 08:03, expires 19 Sep 2027) and `mara-laptop` (`admin`, created 30 Oct 2026, never used, expires 28 Jan 2027). A token is `plt_` followed by 32 letters and digits; invent one for each sample token.

Build these parts in order.

### 1. Console bar

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

### 2. Page header

The `<h1>` "Settings". No actions.

### 3. Settings links

Two columns from `lg` up, the full width below: a `<nav>` with the `aria-label` "Settings", holding a `<ul>` of five links styled as sidebar links from the layout notes, each a block with `py-1` in `text-body`, the items with no margin: General (`#`), Members (`#`), Domains (`#`), API tokens (`/settings/tokens`, the current page), Billing (`#`). Below `lg` the `<ul>` is a row, `flex flex-wrap gap-x-6 gap-y-2`. From `lg` up the `<nav>` stays in view (`lg:sticky lg:top-6 lg:self-start`).

### 4. API tokens

Ten columns from `lg` up, the full width below, with `min-w-0`. In this order:

1. An `<h2>` "API tokens", and with `mt-3` one paragraph (`max-w-lg`): tokens let scripts and CI run `plinth` as the team, read from `PLINTH_TOKEN`.
2. The status element for the message after a token is revoked, with no margin of its own. The message is a `<p>` inside it with `mt-3` and `tabIndex={-1}`.
3. With `mt-6`, the tokens panel.
4. With `mt-6`, the create panel.

### 5. Tokens panel

A panel with rows. Its heading row holds the label heading, here an `<h3>` "Tokens" (under the `<h2>` "API tokens"), in the label-heading classes, not the `<h3>` style, and at the right end the count as a small figure (`4 tokens`). Add the components with `npx shadcn@latest add table badge input`. The table's `label` is "API tokens"; give the `<table>` `min-w-2xl`. Columns:

- **Name:** `font-mono`. On the token created last in this visit, the signal badge "new" follows the name with `gap-2`: the page's one lime mark.
- **Token:** the first eight characters, `plt_` and four more, and an ellipsis (`plt_7Kq2…`), `font-mono text-muted-foreground`, never the whole token.
- **Scope:** `font-mono`, lowercase.
- **Created**, **Last used** and **Expires:** right-aligned, a `<time>` with a `dateTime` in `font-mono tabular-nums`; a token never used, or one that never expires, shows the word `never`. Each shows the date alone (`11 Nov 2026`): the time a token was last used goes in its `dateTime` and is not shown, so that the seven columns fit the panel at 1280px.
- A last column, right-aligned, whose header is a visually hidden "Actions": a `Button` in `variant="destructive"` with the button classes and `hover:text-foreground` added: red text on the stock hover tint is too faint. Its text is "Revoke", its `aria-label` "Revoke ci-deploy".

**Revoking.** "Revoke" opens a confirmation inside the same panel, under the table: a row with a 1px top border and `px-6 py-4`, holding a `<form>` with `noValidate` named by its first line through `aria-labelledby`. That line is a `<p>` in `font-mono font-bold`: `Revoke ci-deploy`. With `mt-3`, one sentence: scripts using this token stop working at once; this cannot be undone. With `mt-4`, the field "Type ci-deploy to confirm", at most `max-w-md` wide. With `mt-4`, a row with `gap-2`: a `destructive` button "Revoke token", `type="submit"`, with the `TriangleAlert` icon before its text and the same added hover, and an `outline` button "Cancel". One is open at a time; "Revoke" on another row moves it there.

### 6. Create panel

A padded panel. Its heading row holds the label heading, an `<h3>` "Create a token", styled as the "Tokens" one. With `mt-4`, one wrapper holding the form, then the `role="status"` element that replaces it, then the buttons after the message.

The form has `noValidate` and three fields, each at most `max-w-md` wide:

- "Name": an `Input` with helper text: lowercase letters, numbers and hyphens, up to 32 characters.
- "Scope": a `<select>` with `font-mono` and `read`, `deploy:write` and `admin`, starting on `deploy:write`, with helper text that says what the chosen scope can do and changes with it.
- "Expires": a `<select>` with `font-mono`, since its options are durations: 30 days, 90 days, 1 year and Never, starting on 90 days.

With `mt-6`, the default button "Create token", `type="submit"`.

When a token is made, the form goes. Inside the status element, in this order:

1. A `<p>` with `tabIndex={-1}`, which gets focus: `Token ci-preview created.`
2. With `mt-3`, one sentence: copy it now; it is shown once, Plinth keeps only a hash of it, and it expires on the date (a `<time>`, such as `9 Feb 2027`), or never.
3. With `mt-6`, the token in a `<code>` with `block break-all rounded-sm bg-muted px-2 py-1 font-mono text-body`. No border, so it is not a box in the panel; on a phone it wraps, not scrolls.

After the status element, in the wrapper and rendered only while a token is shown, a row with `mt-6` and `flex flex-wrap gap-2`: the "Copy" button from the code-block recipe (`outline`), which copies the token, here in the default size in place of `size="sm"`, so it is as tall as "Done"; and a `secondary` button "Done". Here, in place of the recipe's "beside it", the Copy button's hidden status is not in the row: it is the wrapper's last child, rendered at all times, so it is on the page before the first copy.

### Behavior

- All of it works in component state. "Create token" checks the name, with these errors: empty, "Enter a name for the token."; characters that are not allowed, "Use only lowercase letters, numbers and hyphens."; more than 32 characters, "Use 32 characters or fewer."; a name already in the table, "A token named ci-deploy already exists. Choose another name."
- A good name makes the token with `crypto.getRandomValues`, adds a row on top (created 11 Nov 2026, last used `never`, the expiry counted from that date: 30 and 90 days in days (11 Dec 2026, 9 Feb 2027), 1 year to the same date (11 Nov 2027), and the "new" badge), updates the count and shows the token. It is kept only until "Done".
- "Done" empties the status element, brings back an empty form and focuses the Name field; the whole token is gone for good.
- "Revoke" opens the confirmation and moves focus to its field. "Revoke token" is always enabled; when the field does not hold the token's name exactly, it shows the error "Type ci-deploy exactly to revoke this token." under the field. "Cancel" closes it and focuses that row's "Revoke".
- With the exact name, "Revoke token" removes the row, closes the confirmation, updates the count, puts `Token ci-deploy revoked.` in the status element under the paragraph and moves focus to it. With no tokens left, the count reads `0 tokens` and the table's body is one row, across all columns: "No tokens. Create one below."
- The "Menu" button works as the notes say; links need nothing more.

## 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.

- [ ] An empty, malformed or duplicate token name gets an error in words with an icon, tied to the field, and focus moves to the field.
- [ ] A new token is shown once, in full, with a working "Copy" button; after "Done" it cannot be seen again, and the table shows only its first characters.
- [ ] A revoke with a name that does not match shows an error and revokes nothing; with the exact name, the row is gone and a message is announced and focused.
- [ ] The warning before revoking is said in words, not by the red color alone.
- [ ] The new token wraps inside its box at 375px, and its expiry date shows in the table.
- [ ] 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.
