# Build a film season page with the Nocturne theme

You are building one page in an existing project: the page of a curated season of films at an independent cinema, with an introduction, the films in order with their dates and a note on each, and a season pass. 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 Nocturne variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/nocturne.json` and save https://strictthemes.com/themes/nocturne/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/nocturne/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:** Syne for headings, Onest for body text, Azeret Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 64px/800, `text-h1` 40px/700, `text-h2` 24px/700, `text-body` 16px/400, `text-label` 12px/500.
- **Font per size:** a type class does not set the font family. Headings (`<h1>` to `<h6>`) get the heading font on their own; on any other element add the font class: `font-heading` with `text-display`, `text-h1`, `text-h2`; `font-mono` with `text-label`.
- **Spacing scale**, with the Tailwind step in brackets: 4px (`1`), 8px (`2`), 16px (`4`), 24px (`6`), 48px (`12`), 96px (`24`).
- **Corners:** `rounded-sm` 3px, `rounded-md` 8px, `rounded-xl` 16px (the `lg` corner in `DESIGN.md`). `rounded-lg` is 8px as well: it is shadcn's name for the base corner, the one its stock buttons and inputs use.
- **Colors (light mode):** primary #6B4A00, on-primary #FFF8E6, secondary #17131F, on-secondary #F2F0F5, tertiary #C9B6FF, on-tertiary #17131F, neutral #625B6E, background #F2F0F5, surface #FFFFFF, foreground #17131F, border #DAD5E2, error #B3261E.

### How Nocturne pages are laid out

- **Page:** one `<div>` wraps the whole page: `min-h-screen bg-background text-body text-foreground`. Inside it sit the `<header>`, the `<main>` holding everything between the two, and the `<footer>`.
- **Container:** `mx-auto max-w-[1280px] px-4 md:px-6`. This width is the one arbitrary value allowed. The `<main>` element is itself a container and also carries `pt-12`. The header and the footer run the full width of the window, with their lines; the container is a `<div>` inside each. Every other line stays inside the container.
- **Grid:** where this brief gives columns, the element is `grid grid-cols-1 md:grid-cols-12 md:gap-x-6` (24px gutters) and each item says its span, such as `md:col-span-7`. The grid sets columns only, never vertical space. Below `md` items stack in source order.
- **Vertical rhythm:** written as a top margin on the later element, never as `space-y-*` and never as a gap on a column of mixed content. A heading under a label has `mt-2`. A paragraph under a heading, and a paragraph under a paragraph, has `mt-4`. A block under text (a row of buttons, a list, a form, a figure, a table, a row of cards) has `mt-6`. A new group inside a section has `mt-12`. A `<section>` inside `<main>` has `mt-24`, except the first, which has none: the `pt-12` on `<main>` is the space above it. Where two grid items share a row from `md` up and stack below it, the second gets `mt-6 md:mt-0` when it follows on from the first (text under a still) and `mt-12 md:mt-0` when it is a separate group (a card, a form, a column with its own heading); each brief writes which. Gaps are only for items side by side in a row (`flex`, `flex-wrap`, a grid of cards), where this brief or these notes name one. Inside a list of rows, a row has its own top border and padding and no margin.
- **Headings:** headings are Syne, which `<h1>` to `<h6>` get on their own, in sentence case with no full stop. Titles wrap; never truncate one. The page's big title is `text-h1 md:text-display`, the only `text-display` text on the page. A section's `<h2>` is `text-h2 md:text-h1`. A film title in a row or a card is an `<h3>` in `text-h2`. Syne has no italic: never set a heading or a quote in italics.
- **Labels:** dates, days, times, running times, ratings, seat counts, screen names, eyebrows, captions, nav links, field labels, table headers and button and badge text are labels: `font-mono text-label uppercase`, showtime buttons included. Write their text in sentence case ("22:45 · Late"); the `uppercase` class sets the capitals. The class `text-label` already carries the wide tracking; add no `tracking-*` class. A label is a short fragment; helper text and errors are sentences in body text. Parts of a label line are joined with ` · `. A label on its own line is a block element such as a `<p>`.
- **Times and figures:** a showtime, a date, a price or a seat count that stands on its own (in a label, a button, a table cell, or a price set large) gets `tabular-nums`, so times line up in a column. A date or a time that stands on its own is a `<time>` with a `dateTime` (`2026-11-13`, or `2026-11-13T20:30` for a showtime). Times are 24-hour (`20:30`). In a label a date is short, `Fri 13 Nov` (`11 Nov` without the day, a year only where a brief writes one); in a sentence it is written in full, `Friday 13 November`, and with `2026` only where the brief's own example sentence writes the year. A year alone (a film's year) is plain text, not a `<time>`. A date range in a label (`11–15 Nov`, `20 Nov – 18 Dec`) is two `<time>` elements joined by an en dash in plain text, each with its full `dateTime`. A number or a date inside a sentence is plain text. "Today" is always the fixed sample date, Wednesday 11 November 2026, never the clock.
- **Credits:** the line of credits under a film title (director, country, year) is a `<p>` in `text-body text-muted-foreground`, with its parts joined by ` · `. Names in it are never uppercased. The running time and the rating go on a label line instead. Running text (a synopsis, a note on a film, an introduction, the sentences under a section heading) is never muted. Helper text is a short line apart from running text that a brief sets in `text-muted-foreground`, such as a field's hint or a demo note.
- **Text color:** headings, body text, field labels, nav links and table body cells are `text-foreground`. Other labels, credits, captions and helper text are `text-muted-foreground`. A button's text color comes from its variant.
- **Gold:** `text-primary` and `bg-primary` are bronze in light mode and gold in dark mode. Each screen has one gold thing, besides a link under the pointer and the focus outline (`outline-ring` is gold too, and stays): the page's one main action, a default `Button` or a link styled as one, as this brief names it. `DESIGN.md` allows one gold title in place of an action; these pages spend it on the action, so headings are never gold. Primary is never a fill, a border or a band.
- **Lavender:** `bg-tertiary` is for one badge on a page, always with `text-tertiary-foreground`. It is never a text color or a border.
- **Measure:** a paragraph of running text is at most `max-w-xl` wide (about 70 characters a line in Onest).
- **Links:** a text link is `text-foreground underline decoration-1 underline-offset-4 hover:text-primary`, so gold shows only under the pointer. A link that holds a title (inside a heading, and the cinema's name in the header) is never gold: `text-foreground decoration-1 underline-offset-4 hover:underline`. A nav link is a label in `text-foreground` with `hover:underline underline-offset-4`; the link for the current page has `aria-current="page"` and `underline`. Links get `transition-colors duration-150`.
- **Link destinations:** the five pages are `/` (Programme), `/film`, `/season`, `/membership` and `/visit`; where the project has another router layout, use its paths. A link whose destination this brief does not name points to `#`.
- **Header:** a `<header>` with `border-b border-border`. Its container `<div>` is also the row: `flex flex-wrap items-center justify-between gap-4 py-4`. On the left the cinema's name, a link to `/` in `font-heading text-h2`, never marked as the current page. On the right, a `<nav>` with the `aria-label` "Main" and `hidden md:block`, holding a `<ul>` with `flex gap-6` of four label links, in this order: Programme (`/`), Seasons (`/season`), Membership (`/membership`), Visit (`/visit`). The brief names the current one, or says that none is.
- **Menu on small screens:** below `md` the `<nav>` is hidden and a "Menu" button (`variant="outline"`, with the button classes below) stands at the right end of the row, with `aria-expanded` and `aria-controls`. It shows the links as a list inside the `<header>`, under its row: a second `<nav>` with the same `aria-label` and `md:hidden`, which carries the `id` that `aria-controls` names and has the `hidden` attribute while closed. It has a 1px top border across the full width and, inside the container, `py-2`; each link is a block with `py-2`. It closes on Escape, pressed while focus is on the Menu button or in the menu (one `onKeyDown` handler on the `<header>`, acting only while the menu is open), and focus then goes to the Menu button; it also closes when a link in it is chosen. Do not use a `Sheet` or a `Dialog`.
- **Footer:** a `<footer>` with `mt-24`, `border-t border-border` and `py-12`. Inside it, the container `<div>` holds a grid (`md:grid-cols-12`): columns 1–6 hold the cinema's name as plain text in `font-heading text-h2` and, with `mt-2`, the address as a `<p>` in `text-body text-muted-foreground` ("21 Tallow Row, Oldmere OM2 4LT"); columns 7–12 hold a `<ul>` (not a `<nav>`) of four text links (Programme, Seasons, Membership, Visit) in `text-body` with `flex flex-wrap gap-x-6 gap-y-2`, with `mt-6 md:mt-0`. Under the grid, with `mt-12`, a label line in `text-muted-foreground`: `© 2026 The Vesper`.
- **Buttons:** the shadcn `Button` with `className="h-auto px-6 py-2 font-mono text-label uppercase"`. It keeps its stock `rounded-lg` corner. The default variant (gold) is the screen's one main action. Every other action is `variant="secondary"`, or `variant="outline"` for quiet ones. In this theme the `secondary` variant's stock hover is the same color as its fill: in `components/ui/button.tsx`, replace its `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]` with `hover:bg-secondary/80`, once, and add no hover class where you use it. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A link that looks like a button (to another page, or to a part of this one) is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "…")`, because `buttonVariants` alone does not merge classes. Such a link is `inline-flex`, so a top margin on it works: it needs no wrapper.
- **Button groups:** a set of buttons where one is chosen (a day, a billing period, a showtime) 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 others have `aria-pressed="false"` and `variant="outline"`. Not `Tabs`: there are no tab panels.
- **Badges:** the shadcn `Badge` with `variant="secondary"` and `className="h-auto rounded-sm border-transparent bg-tertiary px-2 py-1 font-mono text-label uppercase text-tertiary-foreground"`: the variant is named only because its classes hold no gold, and these classes replace its fill and text color. One or two words, and at most one badge on a page.
- **Inputs:** the shadcn `Input` and `Textarea` with `className="h-auto px-4 py-2 text-body md:text-body"`. They keep their stock `rounded-lg` corner. So that fields and selects share one fill in both modes, edit `components/ui/input.tsx` and `components/ui/textarea.tsx`: replace the class `bg-transparent` and the class `dark:bg-input/30` with `bg-card`; `file:bg-transparent` and `dark:disabled:bg-input/80` stay. Every field has a visible `<label>` above it: `block font-mono text-label uppercase text-foreground`, tied to the field with `htmlFor`; the field has `mt-2`. Helper text under a field is a `<p>` in `text-body text-muted-foreground` with `mt-2`.
- **Selects:** a choice from a short list is a native `<select>` with `w-full rounded-lg border border-input bg-card px-4 py-2 text-body text-foreground`, under a `<label>` like any field, with the same focus outline. These selects always have a value, so they never show an error.
- **Errors:** an error is a line under its field with `mt-2`: `flex items-start gap-2 text-body text-destructive`, holding the `CircleAlert` icon (`shrink-0`, `mt-1`) and a sentence that says what to do, not uppercased. The field gets `aria-invalid` and points to the line with `aria-describedby`, next to any helper text it already names. 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 `change`); every other error stays until the next submit.
- **Confirmations:** a message that replaces a form goes into an element with `role="status"` that is on the page from the start, empty: The form and the status element share one wrapper, and the wrapper carries the top margin, so the message stands where the form stood. The status element itself has no classes and no margin, and is never hidden (no `hidden`, no `empty:hidden`). The message inside it is rendered only once there is one, and has no fill or border: its first element is an `<h3>` in `text-h2` with `tabIndex={-1}`, then the sentences follow with `mt-4`. Move focus to the `<h3>`. A link that follows a confirmation sits after the status element, in the shared wrapper, rendered only with the message, inside a `<p>` with `mt-6` (a margin on a plain inline text link does nothing).
- **Cards:** a card is a plain element with `rounded-xl border border-border bg-card p-6`. Inside it, a label comes first where it has one, and the rest follows the rhythm above. Never put a card inside a card, and do not use the shadcn `Card`.
- **Pictures:** there are no image files. A picture is a plain element with a 1px border and a fill, `border border-border bg-muted`, with `role="img"` and an `aria-label` that says what it shows. A poster is `aspect-2/3 rounded-xl`; a still is `aspect-video rounded-xl` (16:9). Show them whole, in those proportions: never crop one to another shape. A poster in a row is a thumbnail, `w-24 shrink-0 self-start` and `md:w-32` (`self-start`, or the row stretches it to the height of the text beside it and it loses its proportions), with the film's year inside it in its lower left: the element is `flex items-end p-2`, the year a `<span>` in `font-mono text-label text-muted-foreground`. A screen reader does not read it (`role="img"` hides what it holds), which is right: the credits line gives the year. A still is wide, in a `<figure>`, with a `<figcaption>` under it with `mt-2`: a label in `text-muted-foreground`, as `DESIGN.md`'s caption is, naming the film and what is in the frame in a short fragment (`Low Tide at Merrow · Maren on the harbour wall`). Pictures sit on the page, never in a card: in dark mode `bg-muted` and `bg-card` are nearly the same color.
- **Lists of rows:** a list of films or dates is an `<ol>` or `<ul>` with no markers and no padding. Each row has `border-t border-border py-6`, and the list has `border-b border-border` so the last row closes. A row with a poster is `flex gap-4 md:gap-6`: the poster, then a `min-w-0 flex-1` column holding the text.
- **Tables:** the shadcn `Table` with `text-body`. Edit `components/ui/table.tsx`: its scroll box, the `<div>` around the `<table>`, gets `tabIndex={0}`, `role="region"`, an `aria-label` from a new required `label` prop, and the focus outline. Header cells are labels in `text-muted-foreground` with `px-4`; body cells have `px-4 py-2`. Add `first:pl-0 last:pr-0` to every cell. Times in cells are `font-mono tabular-nums`. Rows are divided by 1px borders, with no zebra stripes. The stock `TableBody` drops its last row's border: take `[&_tr:last-child]:border-0` out of it. The rows do nothing, so take `hover:bg-muted/50` out of the stock `TableRow`. Header cells keep the stock `h-10` and, like all cells, `whitespace-nowrap`. The table's name is the heading above it and the `label`; it has no `<caption>`.
- **Block quotes:** a `<figure>` with a `<blockquote>` holding a `<p>` in `font-heading text-h2`, upright, with no quotation-mark graphic, and a `border-l-2 border-foreground pl-6` on the `<blockquote>`. Who said it is a `<figcaption>` with `mt-4`: a label in `text-muted-foreground`.
- **Icons:** from `lucide-react`, with `size-4`, `strokeWidth={1.5}` and `aria-hidden="true"`, and only where this brief or these notes name one. Never emoji.
- **Corners:** `rounded-sm` (3px) for badges, `rounded-lg` (8px, the stock corner) for buttons, inputs and selects, `rounded-xl` (16px) for cards, posters and stills. `rounded-full` is for avatars only, and these pages have none.
- **Surfaces:** flat: no shadow, blur or gradient in the code you write. A stock component that floats keeps its one short shadow.
- **Motion:** in the code you write, the only transition is color. Nothing 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 Vesper**, an independent cinema with three screens and a late bar at 21 Tallow Row, Oldmere. The season is "Ilse Varga: the night films", a retrospective of an invented Hungarian director, six of her films shot mostly after dark, shown on Friday 20 November, Friday 27 November, Tuesday 1 December, Friday 4 December, Friday 11 December and Friday 18 December 2026, all at 20:30 on Screen 2. The films: "Corridor Light" (1989, 94 min), "The Long Blue" (1998, 142 min), "Kestrel Street" (2003, 101 min), "Winter Switchboard" (2009, 117 min), "The Last Tram Home" (2014, 88 min) and "Small Fires" (2019, 109 min). The first four films are shown on 35mm prints; "The Last Tram Home" and "Small Fires" were shot on digital and are shown that way. The season is curated by Petra Lindqvist, the Vesper's programmer. A season pass costs £38 for all six films, £30 for members.

Build these parts in order.

### 1. Header

The header from the layout notes. "Seasons" is the current page.

### 2. Introduction

The first `<section>`, a grid (`md:grid-cols-12`):

- A `<div>` in `md:col-span-7` (columns 1–7): a label "Season · 20 Nov – 18 Dec 2026", in `text-muted-foreground`, built from two `<time>` elements; with `mt-2`, the `<h1>` "Ilse Varga: the night films" in `text-h1 md:text-display`; with `mt-6`, three paragraphs of introduction to read, each within `max-w-xl`, the second and third with `mt-4`. They say who Varga is, why these six films belong together (night, small rooms, people waiting for something), and how the season is shown (in order, the first four on 35mm prints, the last two digital); then, still in this `<div>`, with `mt-4`, a credits line from the layout notes, a `<p>` in `text-body text-muted-foreground`: "Curated by Petra Lindqvist" (not a label, so the name is not uppercased).
- The season pass card (part 4 below), with `mt-12 md:mt-0`, in `md:col-span-5` (columns 8–12) and from `lg` up in `lg:col-span-4 lg:col-start-9` (columns 9–12). From `md` up it stands beside the introduction, at the top of the row; below `md` it comes after the introduction. Give it `md:self-start`, so it keeps its own height.

### 3. The films

The second `<section>`, with `mt-24`. An `<h2>` "Six films, in order" in `text-h2 md:text-h1`, then with `mt-4` one sentence: "The six films run from Friday 20 November to Friday 18 December, and every screening starts at 20:30 on Screen 2." (The dates are not weekly: one of them is a Tuesday.)

With `mt-6`, an `<ol>` of the six films as a list of rows from the layout notes. Each `<li>` holds an `<article>` laid out as a row with a poster:

- The poster thumbnail, with the `aria-label` "Poster for" and the title, and the year inside it.
- The text column. First the date as a label line in `text-foreground`: the film's number and the date and time, e.g. `01 · Fri 20 Nov · 20:30`, the date and time in one `<time dateTime="2026-11-20T20:30">` with `tabular-nums`. Then, with `mt-2`, the title as an `<h3>` in `text-h2`; with `mt-2`, the credits line ("Ilse Varga · Hungary · 1998"); with `mt-2`, a label line in `text-muted-foreground` with the running time and the format: `142 min · 35mm print` for the first four films, `88 min · Digital` for the last two; with `mt-4`, the note: two or three sentences in running text, within `max-w-xl`, on what to watch for in this film and where it sits in her work.
- Last in the text column, a `<p>` with `mt-4` holding a text link "Film details" (to `#`: these films have no page yet).

From `md` up, the date comes first in the row, as the theme's listings do: add a column before the poster: a `<p>` label in `text-foreground` with `tabular-nums` and `hidden md:block md:w-32 md:shrink-0` (the `<p>` is the column, with no wrapper), holding the same date: one `<time dateTime="2026-11-20T20:30">` holding two `<span className="block">`, `Fri 20 Nov` and `20:30`, and from `md` up hide the date in the text column's label line, but not the film's number: put the separator and the `<time>` (` · Fri 20 Nov · 20:30`) in a `<span>` with `md:hidden`, so that the line reads `01` alone from `md` up. Only one of the two dates is shown at a time, and both are hidden with display classes, so a screen reader reads the date once.

On a phone the row is the poster at `w-24` with the text beside it.

### 4. Season pass card

A card from the layout notes, holding, in order:

- A label "Season pass".
- With `mt-2`, the price as a `<p>` in `font-heading text-h1 tabular-nums`: `£38`. With `mt-2`, a label in `text-muted-foreground`: `Six films · £30 for members`.
- With `mt-4`, a `<ul>` of what the pass includes, in words, `list-disc pl-4` in `text-body`, with `mt-2` on every item but the first: a seat at all six screenings, the same seat each night if you want it, a printed programme with an essay by Petra Lindqvist, and 10% off at the bar on season nights.
- With `mt-6`, the default-variant link "Buy a season pass" (to `#`), full width, `w-full whitespace-normal text-center`: the sample label fits on one line at every width, and these classes let a longer label from the user's content wrap instead of overflowing, since the stock button does not wrap its text. It is the page's one gold action.
- With `mt-4`, a `<p>` in `text-body text-muted-foreground`: single tickets go on sale two weeks before each screening.

### 5. Footer

The footer from the layout notes.

### Behavior

- The page has no state but the small-screen menu's. "Buy a season pass" points to `#`. No link needs behavior.
- Use the six dates of the sample in that order, with the weekdays given.

## 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`, `4`, `6`, `12`, `24`) plus 0, 1px and 2px. No arbitrary values such as `p-[18px]`, and no other steps such as `p-3` or `gap-5`. `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 design the dark mode first: it is where this theme lives.
- Do keep gold to one action or one title per screen.
- Do let images be large and uncropped.
- Don't add glows, neon or blurred glass, however much the subject invites it.
- Don't mix gold with other warm accents: lavender is the only second color.
- 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 films are an `<ol>` in screening order, and each has a `<time>` with a full `dateTime`, a poster with `role="img"` and an `aria-label`, and a short note.
- [ ] The season's title is the page's `<h1>` and its only `text-display` text, and the introduction's paragraphs are no wider than `max-w-xl`.
- [ ] The season pass card states its price and what it includes in words, and its button is the page's only default (gold) action.
- [ ] The cards on the page hold no poster or still, and no card sits inside another.
- [ ] 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.
