# Build a progress screen with the Sorbet theme

You are building one page in an existing project: the progress screen of a habit-tracking app, with a week and month switch, the streak as a key figure, a chart of completed habits with a text alternative and each habit's completion in words and numbers. 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 Sorbet variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/sorbet.json` and save https://strictthemes.com/themes/sorbet/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/sorbet/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:** Gabarito for headings, Figtree for body text, Sometype Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 56px/800, `text-h1` 36px/700, `text-h2` 24px/700, `text-body` 17px/400, `text-label` 13px/500.
- **Font per size:** a type class does not set the font family. Headings (`<h1>` to `<h6>`) get the heading font on their own; on any other element add the font class: `font-heading` with `text-display`, `text-h1`, `text-h2`; `font-mono` with `text-label`.
- **Spacing scale**, with the Tailwind step in brackets: 4px (`1`), 8px (`2`), 12px (`3`), 16px (`4`), 24px (`6`), 40px (`10`), 64px (`16`).
- **Corners:** `rounded-sm` 10px, `rounded-md` 20px, `rounded-xl` 28px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 20px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #AD1457, on-primary #FFFFFF, secondary #2B1721, on-secondary #FFF6F1, tertiary #FFD23F, on-tertiary #2B1721, neutral #735E68, background #FFF6F1, surface #FFFFFF, foreground #2B1721, border #F0D9D2, error #B42318, success #1C7C54.

### How Sorbet pages are laid out

- **Phone first:** build every part for a 375px phone first, then widen it from `md` up. At 375px every control is at least 44px tall and nothing sits side by side that this brief does not put side by side.
- **Page:** one `<div>` wraps the whole page with `min-h-screen bg-background text-body text-foreground`. Inside it sit the `<header>`, a `<main>` and the `<footer>`.
- **Container:** the header's row, the main content and the footer's row each sit in a centered container: `mx-auto max-w-[1168px] px-4`, with `px-6` from `md` up, so the content is at most 1120px wide, as `DESIGN.md` gives the grid. This width is the one arbitrary value allowed.
- **Grid:** where this brief puts parts side by side, their wrapper is `grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-x-6`, and each item gets the `md:col-span-*` this brief gives it. A row of cards of the same kind (key figures, plans, features) is `grid grid-cols-1 gap-6 md:grid-cols-12`: `gap-6` in both directions, with no `md:gap-x-6`. Items in one row stretch to the same height unless this brief says otherwise.
- **Vertical rhythm:** written as gaps on stacks, never as top margins and never as `space-y-*`. Every element that holds more than one thing in a column is `flex flex-col` with one of these gaps: `gap-2` between a label and what it labels, between a field's label, the field, its helper text and its error, and between a heading and a label or small figure under it (inside a card too: a label and its line get their own `flex flex-col gap-2`, and the card's `gap-4` sits between such groups); `gap-3` between the cards of one list; `gap-4` between a heading and its paragraph (one sentence in `text-body` counts as a paragraph), and between the parts inside a card; `gap-6` between the fields of a form, between a part's heading and its content, and between cards of the same kind; `gap-10` between groups on an app screen; `gap-16` between sections on the Welcome and Plans pages. The brief says which of the last two the `<main>` uses. A row of things side by side is `flex flex-wrap items-center` with `gap-3`, or `gap-2` for things that belong together (an icon and its word). Where this brief gives no gap, use the one that fits.
- **Main:** the `<main>` element itself carries the container classes, `flex flex-col`, the gap the brief names, and `pt-10 pb-16`.
- **Header:** the first thing on the page is a `<header>` with `border-b border-border`, running the full width. Inside it, one `<div>` carries both the container classes and the row's classes, `flex flex-wrap items-center gap-x-6 gap-y-2 py-3`: the app's name on the left, a link in `inline-flex h-11 items-center font-heading text-h2 text-foreground`; then the `<nav>`, the page's only one, with the `aria-label` "Main" and `w-full md:w-auto`. From `md` up the nav sits right after the name, `gap-x-6` from it, not pushed to the row's right end. The `<nav>` holds four links as a `<ul>` with `flex gap-1`, in this order: "Today" to `/today`, "Progress" to `/progress`, "Plans" to `/plans`, "Profile" to `/profile`. Welcome is not among them; the brief says which link is the current page. Each link is `inline-flex h-11 items-center rounded-lg px-3 font-mono text-label`: the current page's link has `aria-current="page"` and `bg-secondary text-secondary-foreground`; the others are `text-foreground hover:underline underline-offset-4`. Below `md` the nav takes a line of its own under the name; the four links fit on that line at 375px, so there is no "Menu" button. Anything at the row's right end has `ml-auto`.
- **Footer:** a `<footer>` with `border-t border-border`, running the full width. Inside it, one `<div>` carries both the container classes and the row's classes, `flex flex-wrap items-center justify-between gap-3 py-10`: the app's name in `font-heading text-h2`, and a line in `font-mono text-label text-muted-foreground` with the copyright year as plain text: `Mossy · Made for small, daily steps · 2026`.
- **Page header:** on an app screen the `<main>` opens with a `flex flex-col gap-2`: a small figure above (the date, or a short line this brief gives), then the `<h1>` in `text-h1`. The `<h1>` is `text-display` only where this brief says so, and then only from `md` up (`text-h1 md:text-display`).
- **Headings:** an `<h2>` is `text-h2`. An `<h3>` is `text-h2` too, unless this brief gives it another size. Headings get the heading font on their own; any other element in `text-display`, `text-h1` or `text-h2` adds `font-heading`. Sentence case, no full stop.
- **Sections:** where this brief calls something a section, it is a `<section>` with `aria-labelledby` pointing at its heading, `flex flex-col gap-6`, with the heading (or a heading row) first and the content after it. Without a visible heading it gets an `aria-label`, and where it then holds one element (a grid, a card, a quote) it has no `flex flex-col gap-6`. Where a card is the section, the card carries `<section>`, and its own `gap-4` replaces the `gap-6`. The header, the page header and the footer are not sections.
- **Labels:** field labels, nav links, button and badge text, captions and the names of small groups are labels: `font-mono text-label`, in sentence case. Nothing is uppercased, in the text or with a class. A label is a short fragment. Helper text and error lines use the label style too and may be one short sentence; any longer text is body text.
- **Small figures:** a count, a time, a date, a percentage or a price note that this brief calls a small figure is `font-mono text-label tabular-nums text-muted-foreground`. The classes go on the element that holds the whole figure: a `<time>` that is the whole figure carries them itself; a `<time>` inside a longer figure (`Every day · Done at 08:12`) inherits them from its `<p>`.
- **Key figures:** the one number a card is about (a streak, a count, a price) is `font-heading text-display tabular-nums`, with its unit beside it as a small figure in a row with `flex flex-wrap items-baseline gap-2`. Where this brief asks for a smaller one, it is `font-heading text-h1 tabular-nums`.
- **Text color:** headings, body text, key figures and the names of things are `text-foreground`. Labels, small figures and helper text are `text-muted-foreground`. Paragraphs are never muted.
- **Writing:** address the reader as "you", in short sentences. An error says what happened and what to do.
- **Measure:** a paragraph of body text is at most `max-w-lg` wide.
- **Addresses:** the pages live at `/` (Welcome), `/today`, `/progress`, `/plans` and `/profile`. The app's name in the header links to `/` on Welcome and to `/today` on the other pages; each nav link points to its page's address, the current page's own included. A link whose destination this brief does not name points to `#`.
- **Dates:** "today" is the fixed sample date, Wednesday, 11 November 2026; only a time someone records (a habit marked done) and the greeting on Today come from the clock. A period such as "the last 7 days" includes today. A date is written day first with the month in full, `11 November`; the weekday goes before it (`Wednesday, 11 November`) and the year after it (`2 September 2026`) only where the brief shows them. Only a chart's axis labels shorten (`Thu`, `4 Nov`). A clock time is 24-hour, `08:12`. A date or time the brief marks as a `<time>` gets a `dateTime` (`2026-11-11`, `08:12`); one inside an `aria-hidden` drawing is plain text.
- **Links:** a text link is `text-primary underline underline-offset-4 hover:text-foreground` with `transition-colors duration-150`, and inherits its size.
- **Buttons:** the shadcn `Button` with `className="h-11 px-6 font-mono text-label"`. The stock `Button` is 32px tall and touch targets are 44px: every button and every link styled as one gets `h-11`. Button text is sentence case, two or three words; a choice in a button group may be one. The default variant (raspberry) is for the one main action of a view; no view shows two. Other actions are `variant="secondary"`, or `variant="outline"` for quiet ones. In `components/ui/button.tsx`, once: in the `secondary` variant, replace `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]` with `hover:bg-secondary/80` (the stock hover cannot be told from the resting color); in the `outline` variant, replace `bg-background` with `bg-card` and take out `dark:bg-input/30`: the card's white in both modes, its hover classes unchanged. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A button that is disabled until something is typed or that its own click disables gets `focusableWhenDisabled`, so that keyboard focus stays on it; in this app that is only "Delete my account" on Profile. A button whose click moves focus somewhere else does not need it. With it, Base UI sets `aria-disabled="true"` and `data-disabled` in place of the `disabled` attribute, so the stock `disabled:` classes do nothing: on a page that uses the prop, and only there, add `data-disabled:pointer-events-none data-disabled:opacity-50` beside them in `components/ui/button.tsx`. A call to action that goes to another page or jumps within this one is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "h-11 px-6 font-mono text-label")`, because `buttonVariants` alone does not merge classes. An icon inside a button sits before the text.
- **Button groups:** a set of buttons where one is chosen (a period, a billing switch) is a `role="group"` with `flex flex-wrap gap-2` and an `aria-label`. The chosen button has `aria-pressed="true"` and `variant="secondary"`; the rest have `aria-pressed="false"` and `variant="outline"`. Not `Tabs`: there are no tab panels.
- **Inputs:** the shadcn `Input` with `className="h-11 px-4 text-body md:text-body"`. Fields only ever sit in cards, and a field and a select look the same there: in `components/ui/input.tsx`, take out the stock `dark:bg-input/30` (that class only: `bg-transparent`, `file:bg-transparent`, `disabled:bg-input/50` and `dark:disabled:bg-input/80` stay), so the input is transparent on the card in both modes, as the select's `bg-card` is. Every field has a visible `<label>` above it with `block`, as a label in `text-muted-foreground`, tied to the field with `htmlFor`. The label, the field, helper text and the error sit in a `flex flex-col gap-2`. Helper text is a small figure, and the field names it in `aria-describedby`.
- **Selects:** a choice from a short list is a native `<select>` with the classes `h-11 w-full rounded-lg border border-input bg-card px-4 text-body text-foreground` and the focus outline from the rules (`focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring`). A width the brief gives goes on a `<div>` around the `<select>` alone, inside the field's stack. It gets the same label and wrapper as any field; with an error it also gets `aria-invalid` and `aria-invalid:border-destructive`, like the input.
- **Errors:** an error is a line under its field, inside the field's stack: `flex items-start gap-2 font-mono text-label text-destructive`, holding the `CircleAlert` icon (`size-4 shrink-0`, `aria-hidden="true"`) and one sentence that says what to do. The field gets `aria-invalid` and adds the line's `id` to its `aria-describedby`. Fields are checked in code on submit, with `noValidate` on the `<form>`; focus moves to the first field with an error. An email is well formed when, trimmed, it matches `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`. 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: one that arrives with its text is often not announced. It is never hidden (no `hidden` attribute, no `empty:hidden`), and it has no classes that draw or take space: the classes, the margin included, go on the message inside it, which is rendered only while there is a message. So that an empty one adds no gap, the status element is never an item of a `gap-*` stack or row; the brief says where it sits. A short confirmation is a line `flex w-fit items-center gap-2 font-mono text-label text-success` with the `CircleCheck` icon (`size-4`, `aria-hidden="true"`) and a word or two. Where a message replaces a form, the form and the status element share one wrapper; the message's first element gets `tabIndex={-1}` and focus moves to it, because the button that had focus is gone.
- **Cards:** a card is a plain element with `rounded-xl border border-border bg-card p-6` (28px corners, 24px padding, a 1px border), and `flex flex-col gap-4` inside unless this brief says otherwise. Not the shadcn `Card`, which ships its own padding and a ring. Never put a card inside a card, and never a bordered box inside a card: divide a card's parts with 1px `border-t border-border` lines and `pt-4`.
- **Badges:** the shadcn `Badge` with `className="h-auto gap-1 rounded-sm px-2 py-1 font-mono text-label"`. A good-news badge (a streak, all done, a saving) adds `border-transparent bg-tertiary text-tertiary-foreground`: butter yellow with dark text, in both modes, and for good news only. Any other badge is `variant="secondary"`. One to four words. An icon in a badge comes before the words with `aria-hidden="true"` and no size class: the stock `Badge` sets its icons to 12px. `bg-tertiary` is a fill only: never use butter yellow as a text color, a border or a chart color.
- **State:** never shown by color alone. `text-success` and `text-destructive` are only for a short message that also has an icon.
- **Meters:** a bar that shows a share (a habit's completion) is a track `h-6 w-full overflow-hidden rounded-sm border border-border bg-background` holding a fill `h-full bg-secondary`, whose width is set with an inline `style`. The track has `aria-hidden="true"`, which hides the fill too; the number beside it says the share.
- **Charts:** drawn with plain elements. Bars are `bg-secondary` with square corners, on 1px `border-border` grid lines; axis values are small figures, in a column as tall as the plot: the top value hangs under the top line (`absolute top-0`), the bottom value stands on the bottom line (`absolute bottom-0`). No primary, yellow or success fills in a chart. A chart's drawing has `aria-hidden="true"` and a text alternative.
- **Pictures:** no image files. A picture is drawn from plain elements in a card with `role="img"` and an `aria-label`.
- **Avatars:** a circle with the person's initials: `flex size-11 shrink-0 items-center justify-center rounded-full bg-secondary font-mono text-label text-secondary-foreground`, `aria-hidden="true"` beside the written name.
- **Icons:** from `lucide-react`, with `size-4` (`size-6` where this brief says so; none inside a `Badge`) and `strokeWidth={1.5}`. An icon beside a word has `aria-hidden="true"`. Beside a line of body text in an `items-start` row, the icon gets `mt-1` to sit on the first line. Never an emoji. Only where named.
- **Corners:** `rounded-sm` (10px) for badges and small tiles (the drawn check boxes in the preview, meters), never on a shape less than 20px tall; `rounded-lg` (20px, what buttons, inputs and selects ship with) for buttons, inputs, selects and nav links; `rounded-xl` (28px) for cards; `rounded-full` only for avatars and the stock `Switch`.
- **Surfaces:** flat: no shadows, blur or gradients in the code you write.
- **Motion:** in the code you write, the only transition is color. Nothing moves on scroll, and nothing bounces when a habit is done. 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: "Mossy", a habit tracker for small, daily steps. The person is Ines Okafor, with five habits: Drink 8 glasses of water, Read 10 pages, Walk after lunch, Stretch for 5 minutes, No phone after 22:00. Today is Wednesday, 11 November 2026. Her streak is 12 days; her best was 19, in September. The daily counts, out of 5: 13 to 29 October are 2, 3, 5, 4, 5, 5, 5, 2, 4, 4, 5, 3, 4, 5, 3, 5, 3; the 12 days from 30 October to 10 November are all at 5 (that is the streak); today is at 2 so far. So the last 7 days come to 32 of 35 and the last 30 to 129 of 150. Invent which days each habit was done so that the totals agree: over the week, Drink 8 glasses of water and Walk after lunch (the two done today) are at 7 of 7 and the other three at 6 of 7; over the month, Drink 8 glasses of water 29, Walk after lunch 28, Read 10 pages 25, Stretch for 5 minutes 24, No phone after 22:00 23, of 30, so no habit is at 100% in the month.

The `<main>` uses `gap-10` between groups.

Build these parts in order.

### 1. Header

The header from the layout notes, with Progress as the current page.

### 2. Page header

The page header from the layout notes: the small figure `Your last 7 days` (or `Your last 30 days`, following the switch; both periods end with and include today, the fixed sample date, so the week is 5 to 11 November and the month 13 October to 11 November; the range itself is not written on the page), then the `<h1>` "How it is going". Under the `<h1>`, in the page header's `gap-2`, the switch: a button group from the layout notes with the `aria-label` "Period" and two buttons, "Week" and "Month", with Week chosen.

### 3. Key figures

A section with an `aria-label` "Key figures" and no visible heading, holding only a row of three cards: a grid from the layout notes with `gap-6`, each card `md:col-span-4`. Each card, in its `gap-4`:

- A `flex flex-col gap-2` holding a label in `text-muted-foreground`, as an `<h2>` with `font-mono text-label`, and under it the key figure with its unit beside it.
- Where the card has one, the badge.
- One sentence in `text-body`.

The three cards:

1. "Current streak": `12` with `days`, and a good-news badge (add it with `npx shadcn@latest add badge`) "Your longest since September" with the `Flame` icon. Sentence: "Finish today's habits and it becomes 13." The streak does not change with the switch.
2. "Habits done": `32` with `of 35` (week) or `129` with `of 150` (month). Sentence: "That is 91% of everything you planned." (week) or "That is 86% …" (month).
3. "Best day": `Tuesday` (week) or `Monday` (month) in `font-heading text-h1` (a word, so the smaller key figure), with no unit. Sentence: "You finish the most on Tuesdays." or "… on Mondays." The best day is the weekday with the highest total in the period; where weekdays tie, the most recent wins. In the week the six days before today all tie at 5, so Tuesday; in the month Monday leads with 20.

### 4. Chart

A card, full width, that is a section. It opens with a `flex flex-col gap-2` holding the `<h2>` "Habits done each day" and a small figure under it, `Out of 5 habits`; the chart follows in the card's `gap-4`.

The chart is a `flex gap-3`: the scale, `relative h-48 w-6`, and the plot, `relative flex-1 h-48`, a fixed height for the bars' percentages. The scale has two values: `5` hangs under the top line (`absolute top-0 right-0`) and `0` stands on the bottom line (`absolute bottom-0 right-0`). The plot has two 1px `border-border` grid lines, at the top and the bottom (`absolute inset-x-0`), and the bars over them in a row aligned to the bottom (`flex h-full items-end`): `gap-2` between the 7 bars of a week, `gap-0.5` between the 30 bars of a month. Each bar is `flex-1 bg-secondary` with square corners, its height the day's share of 5 as a percentage in an inline `style`. A day with 0 has no bar, only the gap. All bars are the same color, today's included.

Under the plot, in the card's `gap-4`, the day labels, lined up with the bars: a second `flex gap-3` with an empty `w-6` column first, then a row with the same gap as the bars and one `flex-1 min-w-0` cell per bar (`min-w-0`, or a cell grows to fit its label and stops lining up with its bar). The labels are small figures, centered in their cells and not wrapped (`whitespace-nowrap`; a month's label may run wider than its cell, over the empty cells beside it): for a week, every day (`Thu` to `Tue`, then "Today" in `text-foreground` under the last bar, written through `cn` so that it replaces the small figure's color); for a month, a date every seventh cell counting back from today (`4 Nov`, `28 Oct`, `21 Oct`, `14 Oct`), ending with "Today" under the last bar, and the other cells empty. In the week view at 375px, a centered "Today" (about 40px in a 30px cell) runs into the gaps beside it and stops short of "Tue", which is right. In the month view, "Today" ends at the plot's right edge rather than being centered (`flex justify-end` on its cell), so it does not run past the plot.

The drawing (scale, plot and labels) has `aria-hidden="true"`. The text alternative is a plain `<table>` (not the shadcn `Table`) inside a `<div>` with `sr-only` (the class does not work on a `<table>` itself), with a `<caption>` "Habits done each day, out of 5", and a row for every day in the period: the date as a `<time>`, written `Thursday, 5 November` (`dateTime="2026-11-05"`), and the number.

At 375px the month's 30 bars (about 7px each) fit the card. Nothing in the chart scrolls.

### 5. Each habit

A section with an `<h2>` "By habit". Under it, one card holding a `<ul>` with `flex flex-col gap-4`. Each `<li>` is a `flex flex-col gap-2`, and every `<li>` after the first has `border-t border-border pt-4`:

- A row, `flex flex-wrap items-center justify-between gap-3`: first a `flex flex-wrap items-center gap-2` holding the habit's name in `text-body font-medium` (add `font-sans`) and, for a habit at 100%, a good-news badge "Every day" after it; then, at the right end, the numbers as a small figure: `6 of 7 days · 86%`.
- The meter from the layout notes, its fill the same percentage.

Sort the habits from the highest share to the lowest; habits with the same share keep the sample's order. Percentages are rounded to whole numbers. A habit under 50% gets no warning color: its numbers say it.

### 6. Footer

The footer from the layout notes.

### Behavior

- Keep the period in component state. The small figure in the page header, "Habits done", "Best day", the chart, its text alternative and the habit list all read from it. The streak does not.
- Switching the period moves no focus and scrolls nothing; the pressed button changes with it.
- The nav links go to the addresses in the layout notes and 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`, `10`, `16`) plus 0, 1px and 2px. No arbitrary values such as `p-[18px]`, and no other steps such as `p-5` or `gap-9`. `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 64px 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 keep one main action per view, in raspberry.
- Do use yellow for good news and nothing else.
- Do write the way you would say it out loud.
- Don't use gradients or glows to look playful: the shapes and the color do that.
- Don't put a card inside a card.
- Don't introduce a color, size or radius that is not a token above.

## Check before you finish

Go through this list against the code you wrote, and fix anything that fails.

- [ ] The week and month switch is a button group with `aria-pressed`, not tabs, and it changes the key figures, the chart, its text alternative and the habit list.
- [ ] The chart is drawn from plain elements with `aria-hidden="true"`, and a visually hidden table gives every day's number.
- [ ] Every habit's completion is given in words and numbers ("6 of 7 days", "86%"), and its meter is not the only way to read it.
- [ ] Butter yellow appears only on the good-news badge, never as text, a bar or a border.
- [ ] At 375px the month chart fits its card without sideways scroll.
- [ ] 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.
