# Build a lesson page with the Primer theme

You are building one page in an existing project: one lesson of an online course, with where the learner is in the course and a progress bar, the lesson's text with one code sample, a question with four answers and a Check my answer button that answers in words, a link to the next lesson, and the module's lessons with their state. 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 Primer variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/primer.json` and save https://strictthemes.com/themes/primer/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/primer/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:** Lexend for headings, Plus Jakarta Sans for body text, Fira Code for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 56px/700, `text-h1` 36px/700, `text-h2` 22px/600, `text-body` 16px/400, `text-label` 13px/500.
- **Font per size:** a type class does not set the font family. Headings (`<h1>` to `<h6>`) get the heading font on their own; on any other element add the font class: `font-heading` with `text-display`, `text-h1`, `text-h2`; `font-mono` with `text-label`.
- **Spacing scale**, with the Tailwind step in brackets: 4px (`1`), 8px (`2`), 12px (`3`), 20px (`5`), 32px (`8`), 80px (`20`).
- **Corners:** `rounded-sm` 6px, `rounded-md` 10px, `rounded-xl` 16px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 10px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #095C78, on-primary #FFFFFF, secondary #FFD166, on-secondary #073B4C, tertiary #06D6A0, on-tertiary #073B4C, neutral #3E6270, background #FFFBF0, surface #FFFFFF, foreground #073B4C, border #E4DCC6, error #B3123F, azure #118AB2, sun #FFD166, berry #EF476F.

### How Primer pages are laid out

- **Page:** cream paper with white cards on it. The outermost element is a `<div>` with `flex min-h-svh flex-col bg-background text-body text-foreground`, holding the `<header>`, a `<main>` (`flex-1`) and the `<footer>`.
- **Container:** the header's row, the content of `<main>` and the footer's row each sit in a centered container `<div>`: `mx-auto w-full max-w-[1240px] px-5`, so the content is at most 1200px wide, as `DESIGN.md` gives the grid, with 20px at the page's edge at every width. This width is the one arbitrary value allowed.
- **Sample organisation:** "Rowlark", an invented online school that teaches practical data skills in three topics: spreadsheets, SQL and charts. It has nine courses, 102 lessons in all, and three teachers: Samir Okonjo (spreadsheets), Odile Brandt (SQL) and Lena Farrow (charts). The learner is Tessa Marlow. Prices are in US dollars. The same courses, lessons, lengths and prices appear on every page; this brief gives the ones it needs.
- **Today:** the sample's today is Sunday, 14 March 2027, a fixed date in code, never the device's clock.
- **Dates and lengths:** a date is written day first with the month in full, `14 March 2027`, and the year only where this brief shows it. A date that stands on its own (a small figure, a row of a list) is a `<time>` with a `dateTime` (`2027-03-14`); inside a sentence it is plain text. A length is hours and minutes with a space before each unit, `3 h 10 min`, and minutes alone under an hour, `16 min`. A count carries its noun: `12 lessons`.
- **Addresses:** the pages live at `/catalogue`, `/course`, `/lesson`, `/progress` and `/plans`. The school's name in the header links to `/catalogue`. A link whose destination this brief does not name points to `#`.
- **Grid:** columns stand 32px apart and cards 20px, as `DESIGN.md` gives them. Where this brief puts parts side by side, their wrapper is `grid grid-cols-1 gap-8 lg:grid-cols-12`, a single column below `lg`, and each item gets the `lg:col-span-*` this brief gives it. A row of cards of the same kind is `grid grid-cols-1 gap-5` with the columns this brief names. 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-1` between a name and the small figure under it; `gap-2` between a heading and a small figure above or under it, between a field's label, the field, its helper text and its error, and between the items of a list of short lines; `gap-3` between a heading and its paragraph and between paragraphs; `gap-5` between the parts inside a card, between a section's heading and its content, and between cards; `gap-8` between the groups of a page and between columns; `gap-20` between sections, the 80px of `DESIGN.md`. 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). The scale has no 16px and no 24px step: where a habit asks for one, use `gap-3` or `gap-5`. Where this brief gives no gap, use the one that fits.
- **Main:** the container inside `<main>` is `flex flex-col` with `pt-8 pb-20` and the gap this brief names: `gap-20` on a page made of sections (Catalogue, Course, Plans), `gap-8` on a page that is one piece of work (Lesson, Progress).
- **Header:** the first thing on the page is a `<header>` with `border-b border-border`, running the full width. Its container is also the row, `flex flex-wrap items-center gap-x-8 gap-y-1 py-3`: the school's name, "Rowlark", a link in `inline-flex h-10 items-center font-heading text-h2 text-foreground`; then the `<nav>`, the page's only one named "Main" (`aria-label`), with `w-full md:w-auto`. It holds a `<ul>` with `flex gap-5` of three links, in this order: "Courses" to `/catalogue`, "My progress" to `/progress`, "Plans" to `/plans`. Each link is `inline-flex h-10 items-center font-sans text-body font-semibold text-foreground underline-offset-8`: the current page's link has `aria-current="page"` and `underline decoration-2`; the others have `hover:underline`. Below `md` the nav takes a line of its own under the name; the three links fit on it at 375px, so there is no "Menu" button and the header has no state. This brief says which link is current.
- **Footer:** a `<footer>` with `border-t border-border`, running the full width. Its container is also the row, `flex flex-wrap items-center justify-between gap-3 py-8`: the school's name in `font-heading text-h2`, as a `<p>`, and a small figure: `Rowlark · Practical data skills, one short lesson at a time · 2027`.
- **Breadcrumb:** where this brief asks for one, it is a `<nav>` with the `aria-label` "Breadcrumb" holding an `<ol>` with `flex flex-wrap items-center gap-2`, in `font-mono text-label`: each step but the last a link in `text-muted-foreground underline underline-offset-4 hover:text-foreground`, a `/` in `text-muted-foreground` with `aria-hidden="true"` between steps, and the last step in `text-foreground` with `aria-current="page"`, not a link. Because the breadcrumb then says `page`, the header's "Courses" link has `aria-current="true"` in place of `page`, with the same look.
- **Headings:** Lexend, sentence case, no full stop, at the weight its type class sets: do not add a weight class to a heading. The `<h1>` is `text-h1`; it is `text-display` only where this brief says so, and then only from `md` up (`text-h1 md:text-display`). An `<h2>` is `text-h2`. An `<h3>` is `text-h2` too, unless this brief gives it another size. A small heading inside a card that this brief calls a name is `font-sans text-body font-semibold`: body text at weight 600, not the heading font. Headings get the heading font on their own; any other element in `text-display`, `text-h1` or `text-h2` adds `font-heading`.
- **Sections:** where this brief calls something a section, it is a `<section>` with `aria-labelledby` pointing at its heading, `flex flex-col gap-5`, 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 it has no stack classes. Where a card is the section, the card carries `<section>`, and its own `gap-5` is the section's. A heading row is `flex flex-wrap items-end justify-between gap-3`: the heading on the left, and on the right what belongs to the whole section, a small figure or a control. The header, the footer and a page's opening are not sections.
- **Labels:** Fira Code sets what is counted, measured or typed: levels, lengths, counts, scores, percentages, dates, code, and also field labels, table headers, breadcrumbs, badge text and captions. A label is `font-mono text-label`, in sentence case, never `uppercase`, a short fragment. Buttons and nav links are not labels: they are in the body font.
- **Small figures:** a length, a count, a date, a percentage or a short 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. Parts of one figure are joined with ` · `. A `text-foreground` in this brief replaces the muted color: write the two through `cn`, so that one replaces the other. A small figure with an icon is `inline-flex items-center gap-2` with the icon first.
- **Key figures:** the one number a card is about (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, names, key figures and the figures of a chart are `text-foreground`. Labels, small figures and helper text are `text-muted-foreground`, and only on the page or a card, never on a fill. Paragraphs are never muted.
- **Writing:** address the reader as "you", in short, plain sentences, the way a patient teacher talks. No exclamation marks.
- **Measure:** a paragraph of body text is at most `max-w-2xl` wide.
- **Links:** a text link is the text color, underlined: `text-foreground underline underline-offset-4 hover:decoration-2`, and it inherits its size. A link that is a card's or a row's title has no underline until hover: `text-foreground underline-offset-4 hover:underline`. The class lists in these notes and in this brief leave out the focus outline: add the outline classes from the rules to every link and to every native control you write.
- **Buttons:** the shadcn `Button` with `className="h-10 px-5 font-sans text-body font-semibold"`: the body font at weight 600, in sentence case, two to four words. The stock `Button` ships a smaller size and `font-medium`; these classes replace both, and its stock corner (`rounded-lg`, 10px) stays. The default variant is for the one main action of a view, deep blue with white text, yellow with ink text in dark mode: no view shows two. Every other button is `variant="secondary"`, yellow with ink text, a lighter blue with white text in dark mode. `variant="outline"` is only for the buttons of a button group that are not chosen. No `ghost`, `link` or `destructive` buttons. 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); on a page with a button group, in the `outline` variant, replace `bg-background` with `bg-card` and take out `dark:bg-input/30`, so the button is the card's white on the cream page, its hover classes unchanged. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. No button on these pages is ever disabled, so none gets `focusableWhenDisabled`, and `button.tsx` needs no `data-disabled:` classes. A call to action that goes to another page is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "h-10 px-5 font-sans text-body font-semibold")`, because `buttonVariants` alone does not merge classes. An icon in a button is `size-4` with `aria-hidden="true"`, and sits where this brief puts it.
- **Button groups:** a set of buttons where one is chosen (a level, a week) is a `<div>` with `role="group"`, `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. Pressing one moves no focus.
- **Inputs:** the shadcn `Input` with `className="h-10 px-3 text-body md:text-body"`, its stock corner. In `components/ui/input.tsx`, replace the class `bg-transparent` and the class `dark:bg-input/30` with `bg-card`, so a field is white on the cream page in light mode and the card's color in dark mode; `file:bg-transparent` and the `disabled:` fills stay. A field is a `flex flex-col gap-2`: a visible `<label>` with `block`, a label in `text-muted-foreground`, tied to the field with `htmlFor`; then the field; then any helper text, a small figure that the field names in `aria-describedby`.
- **Errors:** an error is one line under what it is about: `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. A field gets `aria-invalid` and names the line's `id` in its `aria-describedby`; the `<fieldset>` of a group of radio buttons names the line in its `aria-describedby` and gets no `aria-invalid`, which is not for a group. A form is checked in code on submit, with `noValidate` on the `<form>`. The error line is removed when its control changes. The error color is for this line only: a wrong answer to a question is not an error and is told in words in the text color.
- **Confirmations:** a message that appears or changes after an action goes into an element with `role="status"` that is on the page from the start: one that arrives with its text is often not announced. Where it starts empty it is never hidden, has no classes that draw or take space, and is never an item of a `gap-*` stack or row: it shares a plain `<div>` with the element above it, and the message inside it, rendered only while there is one, carries the look and the space, such as `mt-5`. A status element that always holds text (a count of results) may be an item of a stack like any other.
- **Cards:** a card is a plain element with `rounded-xl border border-border bg-card p-5` (16px corners, 20px padding, a 1px border, no shadow), and `flex flex-col gap-5` 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 rules, `border-t border-border` with `pt-5`.
- **Rows with rules:** a list whose rows are divided by rules (lessons, a legend, recent activity) is a `<ul>` or an `<ol>` with `flex flex-col` and no gap. Each row has `py-3 first:pt-0 last:pb-0`, and every row after the first `border-t border-border`.
- **Badges:** the shadcn `Badge` (`npx shadcn@latest add badge`) with `className="h-auto rounded-sm border-transparent bg-tertiary px-2 py-1 font-mono text-label text-tertiary-foreground"`: green with ink text in both modes, one or two words, no icon. The corner must be in this `className`, or the stock pill wins. There is one kind of badge. Green is a fill only: never write `text-tertiary`.
- **Chart colors:** four fills, the same in both modes, used in this order: `bg-azure`, `bg-tertiary` (green), `bg-sun` (yellow), `bg-berry`. A chart with one series is azure; a chart with more takes them in this order. Never `bg-secondary`, `bg-primary` or a `chart-*` class in a chart: `bg-secondary` and `bg-primary` change with the mode, and the yellow of a chart is always `bg-sun`. Each topic keeps one color on every page: Spreadsheets azure, SQL green, Charts berry. Sun is for the time spent on practice files, which belongs to no topic. No text is ever set on azure, sun or berry, and no chart color is a text color. Sun is a pale fill on a white card, so, as with every chart color, it is never the only way to read something: the word or the figure is always beside it.
- **Topic marks and legend dots:** a topic is shown as `inline-flex items-center gap-2`: a dot, `size-2 shrink-0 rounded-full` with the topic's fill and `aria-hidden="true"`, then the topic's name, a label in `text-muted-foreground` unless this brief gives another style. The same dot is the dot of a chart's legend.
- **Progress bars:** a row, `flex items-center gap-3`: a track, `h-2 min-w-0 flex-1 overflow-hidden rounded-full bg-border`, holding a fill, `h-full rounded-full bg-tertiary`, whose width is a percentage in an inline `style`; then the percentage, `25%`, as a small figure in `text-foreground` with `w-10 shrink-0 text-right`. At 0% there is no fill. The track has `aria-hidden="true"`, which hides the fill too; the percentage beside it and the count this brief puts near it (`3 of 12 lessons done`) say the same in words. Percentages are whole numbers.
- **Charts:** drawn with plain elements, flat: a bar is a `<div>` with one fill, square corners and no border, whose height or width is a percentage in an inline `style`, worked out in code from the data. Every bar has its figure written beside or above it in `font-mono text-label tabular-nums text-foreground`. Grid lines are 1px `border-border`; axis values are small figures. A chart's drawing has `aria-hidden="true"` and the text alternative this brief names.
- **Avatars:** a circle with a person's initials: `flex size-10 shrink-0 items-center justify-center rounded-full bg-secondary font-mono text-label text-secondary-foreground`, with `aria-hidden="true"`, beside the written name. Where this brief asks for a larger one it is `size-12`, never more.
- **Lesson rows:** a lesson in a list has its number, its title and its length. The title is `text-body` and the length a small figure; the number is a small figure in a column of its own or leads the title, as this brief says. Where a lesson has a state (done, next, locked), the state is an icon and a word, never a color.
- **Accordions:** the shadcn `Accordion` (`npx shadcn@latest add accordion`), inside a card that has `px-5 py-0` in place of the card's padding and no stack classes, so each trigger's own padding is the card's. The stock `Accordion` is off the type scale. In `components/ui/accordion.tsx`: on the trigger, replace `items-start` with `items-center`, its stock vertical padding with `py-3`, and its small text size and `font-medium` with `font-sans text-body font-semibold`, and add `min-h-10 gap-3`; on the content, replace the small text size with `text-body`; on the content's inner element, replace the stock bottom padding with `pb-5` (`pt-0` stays) and delete the class that starts with `[&_p:not(:last-child)]`, which gives paragraphs a bottom margin off the scale. The items keep the stock 1px rule between them. Each trigger sits in an `<h3>` that the component renders, so an accordion always follows an `<h2>`. Give each `AccordionItem` a `value`.
- **Icons:** from `lucide-react`, with `size-4` (`size-5` where this brief says so) and the stroke they ship with, 2px with round caps: do not set `strokeWidth`. 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. Only where named. Never an emoji.
- **Corners:** `rounded-sm` (6px) for badges and the stacked bar of a chart; `rounded-lg` (10px, what buttons and inputs ship with) for buttons, inputs and a code block; `rounded-xl` (16px) for cards; `rounded-full` only for avatars, progress bars, legend dots and the stock `Switch`, none of them over 48px. A chart's bars have square corners.
- **Surfaces:** flat. No shadows, blur or gradients in the code you write.
- **Motion:** in the code you write, the only transition is color: `transition-colors duration-150` on a link whose color changes on hover. A progress bar does not animate, and nothing moves on scroll. Stock components keep the motion they ship with.

## The page

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

Sample content, if the user gave none: "Rowlark", an online school for practical data skills, and its course "SQL from zero": 12 lessons, 3 h 10 min, taught by Odile Brandt. The learner, Tessa Marlow, has done lessons 1 to 3 and is reading lesson 4, "Filtering rows with WHERE", 16 min. That is 3 of 12 lessons done, 25%, with 2 h 31 min left in the course. The lesson is in module 1, "Asking one table questions", 6 lessons, 1 h 28 min:

1. "What a database is", 12 min, done
2. "Your first SELECT", 14 min, done
3. "Choosing columns", 13 min, done
4. "Filtering rows with WHERE", 16 min, this lesson
5. "Sorting with ORDER BY", 15 min, next
6. "A small report from one table", 18 min, locked

The lesson's examples ask Rowlark's own table of courses, `courses`, which has four courses for beginners, three intermediate and two advanced.

The container inside `<main>` uses `gap-8` between groups. This page uses the stock `button` only. It has no button group, so `components/ui/button.tsx` needs only the `secondary` change of the Buttons note. It has native radio buttons: set `color-scheme` in the global CSS as the Dark mode rule gives it for a native control, so that they follow the mode.

Build these parts in order.

### 1. Header

The header from the layout notes, with Courses marked as the current section (`aria-current="true"`, as the Breadcrumb note says).

### 2. Where you are

A `<div>` with `flex flex-col gap-5`, not a section:

- The breadcrumb: `Courses` (a link to `/catalogue`), `SQL from zero` (a link to `#`) and the current step `Lesson 4`.
- A `flex flex-col gap-2`: a small figure, `Module 1 · Asking one table questions`, then the `<h1>` in `text-h1`, "Filtering rows with WHERE".
- The place in the course, a `flex flex-col gap-2 md:max-w-2xl`:
  1. A row, `flex flex-wrap items-baseline justify-between gap-x-5 gap-y-1`: a `<p>` in `font-sans text-body font-semibold`, "Lesson 4 of 12", and a small figure, `16 min · 2 h 31 min left in the course`.
  2. The progress bar from the layout notes, at 25%, with `25%` beside it.
  3. A small figure in `text-foreground`: `3 of 12 lessons done`.

### 3. The lesson and its module

A grid from the layout notes. Its first item, `lg:col-span-8`, is a `flex min-w-0 flex-col gap-8` holding the lesson's text, the question and what comes next. Its second item, `lg:col-span-4`, is the module's list. Below `lg` the list comes after everything in the first item.

**The lesson's text.** An `<article>` with `flex flex-col gap-8`. It holds three blocks, each a `flex flex-col gap-3`; paragraphs keep the measure of the layout notes. A word set in backticks below is an inline `<code>`: the label font at the size of the text around it (`font-mono`, no size class), with no fill and no border.

1. No heading. Two paragraphs: "So far every query has returned every row of a table. Most questions are about some of the rows: the courses for beginners, the orders from March, the customers in one town." and "`WHERE` is how you ask for them. It comes after `FROM` and holds a condition. The database checks the condition against each row and keeps the rows where it is true."
2. An `<h2>`, "One condition". A paragraph: "This query asks Rowlark's own table of courses for the ones for beginners:". Then the code sample, below. Then a paragraph: "The text `'Beginner'` is in single quotes. Without them the database would look for a column called Beginner and stop with an error. Numbers need no quotes: `WHERE lessons > 10` keeps the courses with more than ten lessons."
3. An `<h2>`, "Things to remember". A `<ul>` with `flex flex-col gap-2`, each `<li>` a `flex items-start gap-3` in `text-body` with the `Check` icon (`mt-1 shrink-0`, `aria-hidden="true"`): "`WHERE` comes after `FROM`."; "Text goes in single quotes. Numbers stand alone."; "`=` means equal, `<>` means not equal, and `>` and `<` compare numbers."

**The code sample.** The page's one code block: a `<figure>` with `flex flex-col gap-2 md:max-w-2xl`. First a `<pre>` with `overflow-x-auto rounded-lg border border-border bg-card p-5 font-mono text-body text-foreground`, holding a `<code>` with these three lines:

```sql
SELECT title, lessons
FROM courses
WHERE level = 'Beginner';
```

The `<pre>` scrolls sideways when it has to, so it gets `tabIndex={0}`, `role="region"`, the `aria-label` "Code sample: a query with WHERE" and the focus outline from the rules. No syntax colors: the code is in the text color. Then the `<figcaption>`, a small figure: `It returns 4 rows. Change 'Beginner' to 'Advanced' and it returns 2.`

**The question.** A card that is a section, with the `<h2>` "Check what you learned". Under it, as the card's second item, one plain `<div>` holds the `<form>` and, after it, the `role="status"` element for the result, empty at the start. The `<form>` has `noValidate` and `flex flex-col gap-5`:

- A `<fieldset>` with `m-0 min-w-0 border-0 p-0`. Its `<legend>` has `p-0 pb-3 font-sans text-body font-semibold` and is the question: "Which query returns the intermediate courses?" After the legend, a `flex flex-col gap-3`: the hint, a small figure with an `id`, `One answer is right.`; then the four answers in a `<div>` with `flex flex-col`; then the error line, while there is one. The fieldset names the hint, and the error line while there is one, in its `aria-describedby`.
- Each answer is a `<label>` with `flex min-h-10 cursor-pointer items-start gap-3 border-t border-border py-3`, all four with the rule above them. It holds a native `<input type="radio">` with the `name` `answer` and `mt-0.5 size-5 shrink-0 accent-primary`, then the query as a `<code>` with `min-w-0 font-mono text-body break-words`. Not the shadcn `RadioGroup`. None is chosen at the start. The four, in this order:
  1. `SELECT title FROM courses WHERE level = Intermediate;`
  2. `SELECT title FROM courses WHERE level = 'Intermediate';`
  3. `SELECT title WHERE level = 'Intermediate' FROM courses;`
  4. `SELECT title FROM courses FILTER level = 'Intermediate';`
- A row, `flex flex-wrap items-center gap-3`: the page's one default `Button`, `type="submit"`, "Check my answer", and a small figure, `You can try as often as you like`.

The second answer is the right one.

**The result.** Rendered inside the status element only after an answer has been checked: a `<div>` with `mt-5 flex items-start gap-3 border-t border-border pt-5`. It holds an icon with `mt-0.5 size-5 shrink-0` in `text-foreground`, `CircleCheck` for a right answer and `CircleX` for a wrong one, then a `flex flex-col gap-1`: a name (the small heading of the Headings note, as a `<p>`) and one paragraph in `text-body`. Nothing in it is green or red: the words say it.

- Right: the name "That is right", and "WHERE comes after FROM, and the text 'Intermediate' is in single quotes. The query returns 3 rows, one for each intermediate course. Lesson 4 is done."
- Wrong: the name "Not quite", and two sentences. The first says what is wrong with the answer chosen: for the first answer, "Without quotes the database looks for a column called Intermediate and stops with an error."; for the third, "WHERE cannot come before FROM: the database has to know the table first."; for the fourth, "SQL has no FILTER word for this: the word is WHERE." The second is the same for all three: "The right answer is the second query: WHERE after FROM, with the text in single quotes."

**What comes next.** A `<div>` with `flex flex-wrap items-center justify-between gap-3 border-t border-border pt-5`: on the left a `flex flex-col gap-1` with a small figure, `Up next · 15 min`, and a name as a `<p>`, "Lesson 5: Sorting with ORDER BY"; on the right an `<a href="#">` styled as a `secondary` button, "Next lesson", with the `ArrowRight` icon after its text.

**The module's list.** An `<aside>` that is a card and is named by its heading with `aria-labelledby`, with `lg:sticky lg:top-8 lg:self-start`, so it stays in view from `lg` up while the lesson scrolls. It holds a `flex flex-col gap-2` with the `<h2>` "In this module" and a small figure, `Module 1 · 6 lessons · 1 h 28 min`; then the six lessons as an `<ol>`, set as the Rows with rules note says; then a small figure: `Lessons open one at a time. Lesson 6 opens when you start lesson 5.`

Each `<li>` is also `flex items-start gap-3`: the state's icon with `mt-0.5 size-5 shrink-0` and `aria-hidden="true"`, then a `flex min-w-0 flex-col gap-1` with the lesson's number and title on one line, `4. Filtering rows with WHERE`, in `text-body`, and under it a small figure with the length and the state, `16 min · This lesson`. The four states:

- **Done** (lessons 1 to 3): the `CircleCheck` icon in `text-foreground`; the title is a title link from the Links note, to `#`; the figure ends in `Done`.
- **This lesson** (lesson 4): the `Play` icon in `text-foreground`; the title is a `<span>` with `font-semibold` and `aria-current="page"`, not a link; the figure ends in `This lesson`.
- **Next** (lesson 5): the `ArrowRight` icon in `text-foreground`; the title is a title link to `#`; the figure ends in `Next`.
- **Locked** (lesson 6): the `Lock` icon in `text-muted-foreground`; the title is plain text in `text-muted-foreground`, not a link; the figure ends in `Locked`.

### 4. Footer

The footer from the layout notes.

### Behavior

- Keep the chosen answer, the checked result and whether lesson 4 is done in component state.
- "Check my answer" with no answer chosen shows the error line "Choose one of the four answers first." under the answers, names it in the fieldset's `aria-describedby` and moves focus to the first radio button. Render the line first and move focus after, in an effect. Choosing an answer removes the line.
- With an answer chosen, "Check my answer" puts the result into the status element and keeps focus on the button. Choosing another answer afterwards empties the status element, so a result never stands beside an answer it does not belong to. The learner may check again as often as they like.
- The first right answer marks lesson 4 done, and it stays done whatever is checked later. From then on the place in the course reads `4 of 12 lessons done` with the bar and its figure at 33%, the small figure beside "Lesson 4 of 12" reads `16 min · 2 h 15 min left in the course`, and the lesson's row in the module's list has the `CircleCheck` icon and `16 min · Done`. Its title stays as it is, with `aria-current="page"`. Lessons 5 and 6 do not change.
- The percentage is the lessons done out of 12, rounded to a whole number, and sets the fill's width.
- "Next lesson" and every other link need no behavior beyond their `href`.

## Rules

- **Colors:** only the theme's colors, through the semantic classes (`bg-background`, `text-foreground`, `bg-card`, `bg-primary`, `text-primary-foreground`, `text-muted-foreground`, `border-border` and so on). No hex, rgb or named colors in components, and no Tailwind palette classes such as `bg-blue-500` or `text-gray-600`.
- **Spacing:** padding, margin and gap use only the scale's steps (`1`, `2`, `3`, `5`, `8`, `20`) plus 0, 1px and 2px. No arbitrary values such as `p-[18px]`, and no other steps such as `p-6` or `gap-10`. `auto` margins are fine. Widths and heights such as `w-64` or `size-4` are sizes, not spacing. This applies to the code you write: a stock component keeps the spacing inside it, unless this brief changes it.
- **Type:** every piece of text you write gets its size from one of `text-display`, `text-h1`, `text-h2`, `text-body`, `text-label`, set on the element or on a parent it inherits from. Put `text-body` on the outermost element of the page you write. No other sizes. A stock shadcn component keeps the size it ships with, unless `DESIGN.md` or this brief gives that component its own style. A type class does not replace a weight class: where a stock component ships `font-medium` and the text should be regular, add `font-normal`.
- **Fonts:** only the theme's fonts, through `font-sans`, `font-heading` and `font-mono`. Do not add a font.
- **Corners:** only the `rounded-*` classes, which the theme already sets. No arbitrary radius, and `rounded-full` only where `DESIGN.md` or this brief names a full round, such as an avatar or a status dot. The theme allows a full round on a box up to 56px 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 write the figure beside every bar and every progress bar: color alone says nothing.
- Do keep one main action in view: the next lesson, the next question.
- Do tell a learner where they are: lesson 4 of 12, 35 minutes left.
- Don't set text on azure, sun or berry, and don't use any chart color for text.
- Don't use more than the four chart colors, and don't use gradients or glows.
- 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 opening says Lesson 4 of 12 in words, with a progress bar whose percentage and count of lessons done are written beside it.
- [ ] The four answers are native radio buttons in one `<fieldset>` with the question as its `<legend>`, and Check my answer is the only default button on the page.
- [ ] Check my answer with nothing chosen shows an error line with an icon, tied to the fieldset; with an answer chosen it says in words, in a `role=status` element that was on the page from the start, whether the answer is right, and explains the right one.
- [ ] A right answer marks lesson 4 done: the bar goes to 33%, the count to 4 of 12 lessons done, and the lesson's row in the module list says Done.
- [ ] Every lesson in the module list has its state as an icon and a word (Done, This lesson, Next or Locked), and the locked lesson is not a link.
- [ ] The code sample is a `<pre>` in the label font that scrolls inside its own box and can be reached with the keyboard.
- [ ] 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.
