# Build a ferry search home page with the Passage theme

You are building one page in an existing project: the home page of a ferry line, with a journey search form in a white card on the page, popular routes with their fares, and three reasons to sail with the line. 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 Passage variables in its global CSS. If not, run `npx shadcn@latest add https://strictthemes.com/r/passage.json` and save https://strictthemes.com/themes/passage/DESIGN.md as `DESIGN.md` in the project root. In a project without shadcn, use https://strictthemes.com/themes/passage/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:** Overpass for headings, Karla for body text, Overpass Mono for labels and code.
- **Type scale** (size/weight; each class also sets line height and tracking): `text-display` 56px/800, `text-h1` 36px/800, `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`), 12px (`3`), 20px (`5`), 32px (`8`), 64px (`16`).
- **Corners:** `rounded-sm` 6px, `rounded-md` 10px, `rounded-xl` 18px (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 #023E8A, on-primary #FFFFFF, secondary #03045E, on-secondary #CAF0F8, tertiary #00B4D8, on-tertiary #03045E, neutral #35527A, background #CAF0F8, surface #FFFFFF, foreground #03045E, border #90E0EF, error #B3261E, success #0B6B4F.

### How Passage pages are laid out

- **Page:** the page is pale water: the outermost element you write is a `<div>` with `min-h-svh bg-background text-body text-foreground`. White cards sit on it. Inside the `<div>`, in this order, a `<header>`, a `<main>` and a `<footer>`.
- **Container:** the header's row, the `<main>` and the footer's content each sit in a container, `mx-auto max-w-[1120px] px-5`, at every width. This width is the one arbitrary value allowed. The `<main>` carries the container classes itself: `<main className="mx-auto max-w-[1120px] px-5">`. The header and the footer run the full width, and the container is a `<div>` inside each.
- **Grid:** where this brief puts things side by side, they are items of a grid: `grid gap-5 md:grid-cols-12`, a single column below `md`, with 20px gutters. "Columns 1–7" is `md:col-span-7`; "columns 9–12" is `md:col-span-4 md:col-start-9`, and so on. The grid's `gap-5` is the space between its items in both directions, also when they stack, so a grid item never has a top margin. Where this brief says a section is "a grid", or "in columns 1–8 of a grid", the `<section>` itself carries the grid classes and its `mt-16`; in the second case one `<div>` in it spans those columns and holds the content. Add `md:items-start` to a grid of two or more items, unless this brief says they line up; a grid of one item needs none. Where this brief gives `lg:` in place of `md:`, the grid is a single column up to `lg`.
- **Measure:** a paragraph of body text is at most `max-w-xl` wide (576px).
- **Vertical rhythm:** written as a top margin on each element, never as `space-y-*` and never as a gap on a stack of text. These are the only distances:
  - Between sections: every `<section>` that is not inside another `<section>` has `mt-16` (64px), except the first, which has `pt-8` and no top margin. A `<section>` nested inside one of them takes the margin this brief gives it, never `mt-16`. The footer has `mt-16`.
  - A heading under a label, and a label under the thing it labels: `mt-2`.
  - Text under a heading, and a paragraph after a paragraph: `mt-3`.
  - A block after text or after another block (a grid, a list, a table, a card, a form, a journey, a button on its own line): `mt-5`.
  - An `<h2>` or `<h3>` that opens a new part inside a section, after content: `mt-8`.
  - The first element inside any box (a section, a grid item, a card, a list item, a form) has no top margin.
  - A link or a button on a line of its own sits in a `<p>` or a `<div>`, which carries the margin.
  These margins rely on normal block flow. Inside a grid item or a card, do not make the content `flex flex-col` with a gap. Where a note below (Journeys, Checkboxes) or a section of this brief gives its own margin, such as `mt-1` or `mt-0.5`, that margin wins.
- **Header:** a full-width `<header>` with `border-b bg-card`. Inside it the container `<div>` is itself the row, with `py-5`: `flex flex-col items-start gap-2 md:flex-row md:items-center md:justify-between`. On the left the line's name, `Kestrel Ferries`, a link to `/` in `font-heading text-h2 text-foreground`, never marked as the current page. On the right a `<nav>` with the `aria-label` "Main", the page's only `<nav>`, holding a `<ul>` with `flex flex-wrap gap-x-5 gap-y-2` and four `<li>`s, each one link in `text-body`: Book (`/`), Departures (`/departures`), My booking (`/ticket`), Help (`#`). Every link to these places uses these addresses. Below `md` the links sit on a second line under the name; they fit at 375px, so there is no "Menu" button. A link is marked only where this brief says so: the current page's link keeps its address and gets `aria-current="page"`.
- **Footer:** a full-width `<footer>` with `mt-16 border-t bg-card`. Inside it the container `<div>` with `py-8` holds two things. First a grid `<div>` (`grid gap-5 md:grid-cols-12 md:items-start`): in columns 1–6 a `<div>` with the name `Kestrel Ferries` as a `<p>` in `font-heading text-h2` (not a heading) and, with `mt-2`, one sentence: "Crossings from Harrowmouth to Port Ellery, Brindle Ness, Wrenholm and Tollan Quay since 1987."; in columns 8–12 a `<div>` with a label `Customer desk`, then with `mt-2` a `<p>` in `text-body` holding the phone number `01632 960 512` as a link in running text to `tel:+441632960512`, then with `mt-2` a label `Every day · 06:00–22:00`, each time a `<time>`. Then, under the grid with `mt-5`, a `<p>` label: `© 2026 Kestrel Ferries`.
- **Headings:** the `<h1>` is `text-h1`, and `text-h1 md:text-display` only where this brief says so. An `<h2>` is `text-h2`. An `<h3>` is `text-body` with `font-bold`, unless this brief gives it `text-h2`. Headings get Overpass from the heading element, and a heading in `text-display`, `text-h1` or `text-h2` gets its weight (800, 800, 700) from the type class: add no weight class to those. Sentence case, no full stop.
- **Labels:** times, dates in a label line, port codes, booking references, berths, prices, durations, captions, table headers, form labels, and button and badge text are labels: `font-mono text-label`, in sentence case. Nothing is uppercased by a class; a port code (`HRW`) and a booking reference (`KF-7Q4M2R`) are written in capitals because that is how they are spelled. A label is a short fragment, except helper text under a field and any line this brief writes as a sentence. A label on its own line is a block element such as a `<p>`. Parts of a label line are joined with ` · `.
- **Times and dates:** every time is on the 24-hour clock with two-digit hours, `06:30`, `19:00`, never am or pm. A clock time or a date is a `<time>` with a `dateTime` wherever it appears, in a sentence or a summary too: `<time dateTime="09:15">09:15</time>`, `<time dateTime="2026-12-19">Saturday 19 December 2026</time>`. A date is written weekday, day, month in words, year (`Saturday 19 December 2026`); only in a table or a compact label line, where this brief says so, is it short (`Sat 19 Dec`). A span of hours in a label is two `<time>`s joined by an en dash: `06:00–22:00`. A duration is written `1 h 45 min` or `55 min`; it is a label but not a `<time>`. A time always stands beside its place: in a journey, a table row or a sentence that names the port. Times take `tabular-nums`. A copyright year is plain text. "Today" in a date field is the visitor's own clock.
- **Prices:** a price has the pound sign and two decimals, `£78.00`, in `font-mono` with `tabular-nums` and `text-foreground`, and stands at the right end of its line, where the eye ends. A price that is the point of a card or a row (a journey's fare, a total) is `font-mono text-h2 tabular-nums`; any other price is `font-mono text-label tabular-nums`. The pairing of `font-mono` with `text-h2` is deliberate, here and for the times in a journey: Overpass Mono at the h2 size, though the type summary pairs `text-h2` with `font-heading` for headings. A price inside a sentence is part of the sentence and keeps the body font. Never a price without its currency.
- **Text color:** headings, body text, times, prices and form labels are `text-foreground`, and so is any label this brief gives `text-foreground`. Every other label is `text-muted-foreground`: captions, port names under a time, durations, helper text, the label above a heading or at the top of a card, and the footer's hours and © lines. A button's text color comes from its variant. Paragraphs are never muted. Muted text sits only on the page color or the card color, never on a fill. `text-primary` (deep sea blue) is only for links in running text.
- **Links:** a link with no destination named in this brief or these notes points to `#`. A link in running text is `text-primary underline decoration-1 underline-offset-4 hover:text-foreground` and inherits its size. A link in the header is `text-foreground decoration-1 underline-offset-4 hover:underline`; the current page's link also has `underline`. Links get `transition-colors duration-150`.
- **Added components:** a component you add with `npx shadcn@latest add` during the build (an `Input`, a `Checkbox`, a `Table`, a `Badge`) gets the `cn` import, the focus outline only if it takes focus on the page, and the edits these notes give its file.
- **Buttons:** the shadcn `Button` with `className="h-auto rounded-md px-5 py-2 font-mono text-label"`: a sentence-case mono label, 10px corners, 8px by 20px of padding. The default variant (deep sea blue) is for the page's one main action, and a page has at most one. Any other button is `variant="secondary"` (navy; pale cyan in dark mode). Its stock hover mixes in the foreground, which is navy too: in `components/ui/button.tsx` replace the `secondary` variant's `hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]` with `hover:bg-secondary/80`, once, on every page whether or not it shows a secondary button, and add no hover class in a `className`. Quiet buttons (the unchosen ones in a group, a stepper's minus and plus, "Copy reference") are `variant="outline"`, which keeps its stock fill and hover. Its stock fill is `bg-background`, the pale water, which shows as a tinted chip on a white card and not at all on the page: an outline button only ever sits inside a card, and this brief places every one there. A button that sends a form needs `type="submit"`: this `Button` is `type="button"` unless told otherwise. A button that its own click disables gets `focusableWhenDisabled`, so focus stays on it; this brief names each one. Base UI then sets `aria-disabled="true"` and `data-disabled` instead of `disabled`, 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 is an `<a>` with the same classes: write `cn(buttonVariants({ variant }), "…")`, because `buttonVariants` alone does not merge classes. Button text is two to four words. The stock `transition-all` and the nudge on press stay: they are the component's own motion.
- **Button groups:** a set of buttons where one is chosen (one way or return, a sort order, a filter) is a `role="group"` with `flex flex-wrap gap-2`, named by an `aria-label`, or, where this brief puts a visible label above it, by `aria-labelledby` pointing at that label's `id`. Like every outline button, it sits inside a card. The chosen button has `aria-pressed="true"` and `variant="secondary"`; the rest have `aria-pressed="false"` and `variant="outline"`. Choosing moves no focus.
- **Steppers:** a number set with minus and plus buttons is a `role="group"` named by its visible label (`aria-labelledby` pointing at the label's `id`), with `flex items-center gap-2`: a minus `Button`, the value, a plus `Button`. Each button is `variant="outline" size="icon"` (32px, as it ships) with only `rounded-md` added (not the text button's classes) and an `aria-label` that says what it does ("Fewer adults"), holding the `Minus` or `Plus` icon. The value is a `<span>` in `inline-flex min-w-24 justify-center font-mono text-label text-foreground tabular-nums` with `aria-live="polite"`, written with its noun (`2 adults`, `1 child`). At the lowest and highest value the matching button is `disabled` with `focusableWhenDisabled`.
- **Inputs:** the shadcn `Input` with `className="h-auto rounded-md border-muted-foreground px-3 py-2 text-body md:text-body"`. The stock `border-input` is about 1.4:1 against white and too faint to find the field; `border-muted-foreground` passes 3:1 in both modes. The stock `focus-visible:border-ring` and `aria-invalid:border-destructive` stay. Its fill is `bg-card`, the select's: in `components/ui/input.tsx`, replace the stock `bg-transparent` and `dark:bg-input/30` with `bg-card` (`file:bg-transparent` stays), and delete `dark:aria-invalid:border-destructive/50`, so a field's error border matches the select's in dark mode. Every field has a visible `<label>` above it with `block`, a label in `text-foreground`, tied to it with `htmlFor`, and the field has `mt-2`. A field that may be left empty says so: `Loyalty number (optional)`. A date field is an `Input` of type `date`.
- **Selects:** a choice from a short list is a native `<select>` with `h-auto w-full rounded-md border border-muted-foreground bg-card px-3 py-2 text-body text-foreground aria-invalid:border-destructive` and the focus outline from the rules. Its first option is an empty choice in words ("Choose a port"). It gets the same label, `mt-2` and error handling as any field.
- **Checkboxes:** the shadcn `Checkbox` (`npx shadcn@latest add checkbox`). In `components/ui/checkbox.tsx`, change its `rounded-[4px]` to `rounded-sm` and its `border-input` to `border-muted-foreground`, once. Its row is `flex items-start gap-3`, with `mt-0.5` (2px, an extra value the spacing rule allows) on the checkbox so it lines up with the first line of its text, and the `<label>` in `text-body text-foreground`, not mono: it is a sentence, not a label. Give the `Checkbox` an `id` and the `<label>` `htmlFor` with it, so a click on the words ticks the box; give the `<label>` an `id` too, and the `Checkbox` `aria-labelledby` naming it, because in a Base UI project the `Checkbox`'s `id` lands on a hidden input and the visible box would otherwise have no name.
- **Forms:** a form sits in a card, so its fields stand on white. Each field sits in its own `<div>` with its label and the lines under it; these `<div>`s are `mt-5` apart (the first has none), and the button that sends the form sits in a `<div>` with `mt-5`. Where this brief puts two fields side by side, their `<div>`s are items of a `grid gap-5 md:grid-cols-2`, and the grid takes the `mt-5`.
- **Errors:** an error is a line under its field with `mt-2`: a `<p>` with an `id`, `flex items-start gap-2 font-mono text-label text-destructive`, holding the `CircleAlert` icon (`shrink-0`; `items-start` sets it at the top of the first line) and a sentence that says what to do. Where a field sits in a row with a button or another control, it sits under the row. The field gets `aria-invalid` and points to the line with `aria-describedby`, beside any helper line 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. Render the error lines first and move focus after that render, in an effect or a `requestAnimationFrame`, so the field's description is there when it is read. Before the first send no error shows. An error line and its field's `aria-invalid` are removed when that field changes (on `input` or `change`; for a checkbox, on `onCheckedChange`); every other error stays until the next send.
- **Confirmations:** a confirmation is plain text in `text-foreground`: a heading at the level this brief gives and a sentence or two, no colored box. It goes into an element with `role="status"` that is on the page from the start, empty, so that its message is announced. Where it replaces a form, the form and the status element share one wrapper, the card, so the message stands where the form stood. The status element has no classes and no margin, sits after the form in block flow, and is never hidden (no `hidden`, no `empty:hidden`), so empty it takes no space. The message in it is a `<div>` rendered only when there is a message. Where text stays above it, the message `<div>` has `mt-5`; where it is its wrapper's only content, it has none. A link or a button that follows a confirmation sits beside the status element, not inside it. Give the message's heading `tabIndex={-1}` and move focus to it.
- **States:** a state is a dot and a word, side by side, never a color alone: an `inline-flex items-center gap-2` `<span>` holding a `size-2 shrink-0 rounded-full` dot with `aria-hidden="true"` and the word as a label. These are the only states and their colors: "Seats free", for a sailing that can be booked: dot `bg-primary`, word `text-foreground`. "On time" and "Confirmed": dot `bg-success`, word `text-success`. "Delayed 20 min" and "Few seats left": dot `bg-foreground`, word `text-foreground`. "Cancelled": dot `bg-destructive`, word `text-destructive`. "Departed" and "Sold out": dot `bg-muted-foreground`, word `text-muted-foreground`. `text-success` and `bg-success` are used for nothing else. Where a state stands on a line of its own, a `<p>` or `<div>` around it carries the margin, not the `inline-flex` span.
- **Badges:** the shadcn `Badge` with `className="h-auto rounded-sm border-transparent bg-tertiary px-2 py-1 font-mono text-label text-tertiary-foreground"`: bright cyan with navy text. Cyan is a fill only: never `text-tertiary` or a cyan border, and never a cyan dot. One or two words, no icon. At most one badge on a page.
- **Cards:** a card is a plain element with `rounded-xl border bg-card p-5`, not the shadcn `Card`, which ships its own padding and ring: white, 18px corners, a 1px border and no shadow. Inside a card, a label comes first where there is one, and the text follows with `mt-2`. Never nest cards, and never put a bordered box inside a card: divide a card's parts with 1px rules instead. A card that holds a table has no padding and `overflow-hidden`, and the table touches its edges.
- **Journeys:** a journey is read left to right: where from and when, the way between, where to and when. It is a `<div>` with `flex items-center gap-3` holding three parts. On the left a `<div>` with `shrink-0`: the departure time as a `<p>` holding a `<time>`, `font-mono text-h2 text-foreground tabular-nums`, and under it with `mt-1` the port's name as a label in `text-muted-foreground`. In the middle a `<div>` with `min-w-0 flex-1 text-center`: the duration as a label in `text-muted-foreground`, then with `mt-1` the line, a `<div>` with `h-px bg-border` and `aria-hidden="true"`, then with `mt-1` a label in `text-muted-foreground`, `Direct` or `1 stop`. On the right a `<div>` with `shrink-0 text-right`: the arrival time and the port's name, set as on the left. Where this brief asks for a small journey, the two times are `font-mono text-label text-foreground tabular-nums` in place of `text-h2`, and the rest stays.
- **Description lists:** a list of terms and values (a summary, a ticket's details) is a `<dl>` with `divide-y`. Each row is a `<div>` with `flex items-baseline justify-between gap-5 py-2 first:pt-0 last:pb-0`. The term, a `<dt>`, is a label in `text-muted-foreground` with `shrink-0`; the value, a `<dd>`, is `min-w-0 text-right text-body text-foreground`, so a long name wraps inside it at 375px. A value that is a time, a code or a price takes the label or price look these notes give it, still right-aligned.
- **Rows:** a list where each row has a name on the left and a mono value on the right is a `<ul>` with no markers and no padding, and `divide-y`, so a 1px rule falls between rows and none above the first or below the last. Each `<li>` is `flex items-baseline justify-between gap-5 py-3 first:pt-0 last:pb-0`, unless this brief gives other classes; the value has `shrink-0`. A list in running text is `list-disc` or `list-decimal` with `pl-5`, and `mt-2` between its items.
- **Tables:** the shadcn `Table` with `className="text-body"`. Header cells are labels in `text-muted-foreground` with `h-auto px-5 py-3`; body cells have `px-5 py-3`. Rows do not change on hover: `hover:bg-transparent` on every row. Stock cells do not wrap, so a wide table scrolls sideways in its own box. In `components/ui/table.tsx`, give that box `tabIndex={0}`, `role="region"`, an `aria-label` and the focus outline drawn inside it (`focus-visible:-outline-offset-2` in place of `focus-visible:outline-offset-2`, since the table fills a card that clips), and add a required `label` prop for the name. A table's name is the heading above its card: `label` repeats that heading's words, and there is no `<caption>`, which would scroll away with the table.
- **Pictures:** there are no image files and no photographs. Where this brief draws something (a route map, a boarding code), it is drawn with plain elements, has `role="img"` and an `aria-label` that says what it shows, and has no border of its own.
- **Rules:** every line is 1px and `border-border`, except the dashed tear line of a ticket, which is `border-dashed border-border`.
- **Corners:** `rounded-sm` (6px) for badges and checkboxes, `rounded-md` (10px) for buttons, inputs and selects, `rounded-xl` (18px, the spec's `lg`) for cards and tickets. `rounded-lg` is 10px too; write `rounded-md`. `rounded-full` only for state dots and where this brief names it; buttons and cards are never pills.
- **Surfaces:** flat. A card on the page needs no shadow, only its 1px border. No shadows, blur, gradients or glows in the code you write. A stock component that floats over the page keeps the one short shadow it ships with.
- **Icons:** from `lucide-react`, with `size-4`, `strokeWidth={1.5}` and `aria-hidden="true"`, only where this brief or these notes name one, always beside a word that says the same. Never emoji.
- **Motion:** in the code you write, the only transition is color. 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: "Kestrel Ferries", a ferry line that sails from Harrowmouth (`HRW`) on the mainland to four island ports: Port Ellery (`PEL`), Brindle Ness (`BRN`), Wrenholm (`WRH`) and Tollan Quay (`TLQ`). Harrowmouth to Port Ellery takes 1 h 45 min direct, with five sailings a day; Brindle Ness 55 min, three a day; Wrenholm 2 h 30 min, two a day. Fares start at £29.00, £18.00 and £36.00 for one adult, one way. Children aged 4 to 15 need a ticket; under 4s travel free. Tickets are sold up to 180 days ahead. Write the rest of the copy yourself, plain and specific.

Build these parts in order.

### 1. Header

The header from the layout notes, with "Book" as the current page; its link keeps `/`.

### 2. Opening and search

The first section, with `pt-8`, as a grid (`grid gap-5 md:grid-cols-12 md:items-start`):

- In columns 1–5: a label, `Ferries to the islands · Since 1987`, then with `mt-2` the `<h1>` in `text-h1 lg:text-display` (here `lg:` in place of the Headings note's `md:`, since five columns at `md` are too narrow for the display size): "Over the water by lunchtime". With `mt-3` one or two sentences in `text-body`: five islands' worth of crossings from Harrowmouth, with a car, a bike or a dog. With `mt-5` a label line in `text-muted-foreground`: `Harrowmouth · Port Ellery · Brindle Ness · Wrenholm · Tollan Quay`.
- In columns 7–12: the search card, a card that is the wrapper for the form and the confirmation. It holds the `<form>` (with `noValidate` and the `aria-label` "Search sailings") and, after it, the empty `role="status"` element. Add the fields with `npx shadcn@latest add input` if the project has none, and give the new file the edits the Added components note lists.

On a phone the card comes under the label line, with the grid's `gap-5` between them.

The form's fields, in this order, each in its own `<div>`, `mt-5` apart, as the Forms note says:

1. **Trip**: a label `<p>` with an `id`, "Trip", in `font-mono text-label text-foreground`, then with `mt-2` a button group as the layout notes describe, named by that label with `aria-labelledby` in place of an `aria-label`: "One way" and "Return". "One way" is chosen when the page opens.
2. **From and To**: two fields side by side from `lg` up, as items of a `grid gap-5 lg:grid-cols-2` (here `lg:` in place of the Forms note's `md:`, because the card is narrow at `md`). Each is a native `<select>` styled as the layout notes describe: the label "From" and the label "To". Each list starts with the empty choice "Choose a port", then the five ports, each written with its code, as plain text: `<option value="HRW">Harrowmouth (HRW)</option>`, then Port Ellery (PEL), Brindle Ness (BRN), Wrenholm (WRH), Tollan Quay (TLQ).
3. **Dates**: with "One way" chosen, one field: the label "Outward date", then an `Input` of type `date`. With "Return" chosen, a second field follows it, "Return date", and the two sit side by side from `lg` up in a `grid gap-5 lg:grid-cols-2`; with "One way" the grid holds the outward date alone. The outward date has `min` set to today and `max` to 180 days after today, on the visitor's clock; the return date has `min` set to the outward date, or today while the outward date is empty, and the same `max`. `min` and `max` only limit the picker: a date typed outside them still reaches the check, so the code keeps the range messages below.
4. **Adults**: a label `<p>` with an `id`, "Adults", in `font-mono text-label text-foreground`, then with `mt-2` a stepper as the layout notes describe, from one to nine, starting at two. The value reads `1 adult`, `2 adults`. The buttons' `aria-label`s are "Fewer adults" and "More adults".
5. **Children**: the label "Children" set the same way, then with `mt-2` a stepper from zero to eight, starting at zero. The value reads `0 children`, `1 child`, `2 children`. The buttons' `aria-label`s are "Fewer children" and "More children". Under the stepper, with `mt-2`, a label in `text-muted-foreground`: `Ages 4 to 15. Under 4s travel free and need no ticket.`

No booking holds more than nine passengers: while adults and children add up to nine, both plus buttons are disabled. The four stepper buttons use `focusableWhenDisabled`, so this page makes the `data-disabled:` edit in `button.tsx`.

Then, in the notes' `<div>` with `mt-5`, the page's one default button, `type="submit"`: "Search sailings".

### 3. Popular routes

A section with `mt-16`: an `<h2>` "Popular routes", then with `mt-3` one sentence: every crossing leaves from Harrowmouth's harbour, a five-minute walk from the station. Then with `mt-5` a grid of three cards (`grid gap-5 md:grid-cols-3`), stacked on a phone. This grid has no `md:items-start`: the cards stretch to the same height.

Each card is an `<article>` with the card classes, holding:

- A label in `text-muted-foreground` with how often it sails: `5 sailings a day`.
- With `mt-2`, an `<h3>` in `text-h2`, the route: "Harrowmouth to Port Ellery".
- With `mt-3`, one sentence in `text-body` about the crossing.
- With `mt-5`, a row `<div>` with `flex items-end justify-between gap-5`: on the left a `<p>`, a label in `text-muted-foreground`, with the duration and the way, `1 h 45 min · Direct`; on the right a `<div>` with `shrink-0 text-right` holding a label in `text-muted-foreground`, `From, one adult`, and under it with `mt-1` the price as a `<p>` in `font-mono text-h2 text-foreground tabular-nums`, `£29.00`.
- With `mt-5`, a `<p>` holding a link in running text, "See sailings".

The three routes: Harrowmouth to Port Ellery (`5 sailings a day`, `1 h 45 min · Direct`, `£29.00`, the link to `/results`); Harrowmouth to Brindle Ness (`3 sailings a day`, `55 min · Direct`, `£18.00`, the link to `#`); Harrowmouth to Wrenholm (`2 sailings a day`, `2 h 30 min · Direct`, `£36.00`, the link to `#`). No badge on this page.

### 4. Why sail with Kestrel

A section with `mt-16`: an `<h2>` "Why sail with Kestrel", then with `mt-5` a grid of three items (`grid gap-5 md:grid-cols-3 md:items-start`), on the page, not in cards. Each item is a `<div>` with an `<h3>` (`text-body font-bold`, as the Headings note says) that is `flex items-center gap-2` and holds an icon and its words, then with `mt-3` one or two sentences in `text-body`:

- `CalendarClock`, "Change for £10.00": a sailing can be moved up to 24 hours before it leaves, for £10.00 a booking.
- `Bike`, "Bring the bike": bicycles cost £6.00 each and travel on the car deck, with a rack by the stairs.
- `Dog`, "Dogs on deck": dogs travel free on the open deck and in the dog lounge on every ship.

The prices in these sentences are part of the sentence: they keep the body font.

### 5. Footer

The footer from the layout notes.

### Behavior

- Keep every value in component state. Choosing "Return" adds the return date; choosing "One way" removes it, its value and its error line.
- When the form is sent, check it in code, in this order, and only the first check that fails for a field shows:
  - From: the empty choice gives "Choose the port you sail from."
  - To: the empty choice gives "Choose the port you sail to."; the same port as From gives "Choose a different port from the one you sail from."
  - Outward date: empty gives "Choose the date you sail."; a date before today gives "Choose today or a later date."; a date after `max` gives "We sell tickets up to 180 days ahead."
  - Return date, with "Return" chosen: empty gives "Choose the date you sail back."; a date before the outward date gives "Choose a return date on or after the outward date."; a date after `max` gives "We sell tickets up to 180 days ahead."
  - The steppers are always valid.
- Compare dates as the `YYYY-MM-DD` strings a date field gives, with today taken from the visitor's clock in their own time zone.
- Each error line follows the layout notes, which say when it shows and when it goes, and focus moves to the first field with an error.
- When everything is good, the form is replaced in its card by the confirmation in the status element: an `<h2>` in `text-h2`, "Your search is ready", with `tabIndex={-1}`, and with `mt-3` a sentence that repeats the search: "Sailings from Harrowmouth to Port Ellery on Saturday 19 December 2026, for 2 adults." With "Return" chosen it adds the way back: "…, back on Sunday 27 December 2026, for 2 adults and 1 child." Write the counts as the steppers write them, and leave out children at zero. The dates are each a `<time>`, written as the layout notes say. Move focus to the heading. After the status element, in a `<div>` with `mt-5` that is rendered only with the confirmation, an `<a>` styled as the default button, "See the sailings", that goes to `/results`: it takes the place of "Search sailings" as the page's one default button.
- The other links need no 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`, `5`, `8`, `16`) 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 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 to the one hue: depth of blue is how this theme says what matters.
- Do give a state a word as well as a color: "On time", "Delayed 20 min".
- Do put the price where the eye ends its line, on the right, in mono figures.
- Don't add a second accent color, a gradient or a photograph behind text.
- Don't write a time without its place, or a price without its currency.
- 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 search form sits in a white card on the pale page, and the choice of one way or return is a button group whose chosen button has aria-pressed set to true.
- [ ] Sending the empty form gives the From select, the To select and the outward date each their own error line with an icon, and focus moves to the From select.
- [ ] Choosing the same port twice, or a date before today, gets an error line that says why in words.
- [ ] The adults and children steppers stop at their own limits and at nine passengers in all, and every minus and plus button has an aria-label.
- [ ] Every fare on the route cards has the pound sign and two decimals in Overpass Mono with tabular figures, and the page has one default button at a time.
- [ ] 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.
