# Lodestay

A guest booking places to stay in Lisbon. Top-nav shell, four destinations, nine
screens plus auth. Built on this design system's own primitives, against
`AGENTS.md`.

Open `BookingDesk.dc.html`. Navigate with the top nav; the theme
toggle sits in the bar.

## What it is

| Destination | Route | Primary content |
| --- | --- | --- |
| Stays | `/` | Canvas: the results list beside a real map of Lisbon |
| Trips | `/trips` | Sectioned list, each section running its own predicate |
| Saved | `/saved` | The guest's two lists, priced for the dates in the scope |
| Messages | `/inbox` | List-detail: conversations beside one thread |

Registered but not in the bar: `/stays/:id`, `/stays/:id/book`, `/trips/:id`,
`/trips/:id/receipt`, `/hosts/:id`, `/account/profile`, `/account/payments`, and
the auth namespace: `/signin`, `/signup`, `/verify-email`, `/forgot-password`,
`/forgot-password/sent`, `/reset-password/:token`, `/reset-password/done`,
`/account-locked`, `/signout`.

The map is real. OpenStreetMap tiles under Leaflet 1.9.4, desaturated by
`kit.css` (and inverted under `.dark`) so the price pins carry the only contrast
on the plane. Both libraries are loaded by `boot.js` with the pinned,
hash-verified tags, before any screen renders, so no map component races the
library. Geography is never drawn here: the twelve places carry real
coordinates in nine real Lisbon neighbourhoods.

## The chains worth clicking

Every figure names the array it comes from, so the interesting thing about this
kit is what moves when you touch it.

- **Reserve a place** (`/` → a card's Reserve → checkout → Reserve) writes the
  booking, takes the dates off the market, and lands you on the new trip with
  the payment schedule the checkout screen had already printed. Go back to
  Stays: the count in the heading is one lower, and the place says "Booked" in
  your saved list.
- **Write a review** on the Belém stay (Trips → *Review your stay*) writes it
  onto the booking *and* onto the place, so the place's rating — the mean of its
  own reviews — moves. Do it on the boat instead and it crosses the
  three-review line into having a rating at all.
- **Pay the balance** or **retry the declined card** moves one payment, which
  moves the count in the top bar, because the badge counts a predicate rather
  than a stored flag.
- **Open the unread conversation** from Lena and the Messages count drops.
- **Cancel a booking** quotes `refundFor` in the dialog and then writes exactly
  that refund; the dates go back on the market and the search count rises.
- **Change the dates** in the scope bar to 28–30 Sept: two nights is under the
  minimum at four places, and their Reserve buttons disable with the reason.
  Change the party to five and the small places disable for a different reason.
- **The place on hold** (Baixa) is bookable in eleven minutes or not at all —
  disabled with the hold's expiry, never hidden.

## The product's own files

Everything Lodestay needs is in this folder; deleting the folder deletes the
product.

| File | Holds |
| --- | --- |
| `enums.js` | `listing.availability`, `booking.status`, `payment.status`, the classification lists, the derived flags, the transitions, and `STAY_POLICY` |
| `nav.js` | The route table and the projections the bar renders |
| `metrics.js` | Every number: `quoteFor`, `paymentPlan`, `refundFor`, `searchResults`, `ratingOf`, the trip partitions, the nav badges |
| `fixtures.js` | One guest, five hosts, twelve places, nine bookings, five conversations |
| `lib/` | `contracts.js`, `ui-runtime.js`, `command-palette.js` — byte-identical copies, no domain value in them |
| `LodestayParts.jsx` | Local parts: `Photo`, `PlaceCard`, `QuoteBreakdown`, `Rating`, `Transitions`, `PageHead`, the two dialogs |
| `MapView.jsx` | `MapPane` and `MapSelectionCard` |
| `LodestayShell.jsx` | The top-nav shell |
| `*Screen*.jsx` | One file per screen |
| `App.jsx` | Routing and every commit in the product |
| `kit.css` | Layout only; every value is an Reckon token |

## Local additions, reported

- **`Photo`** — the source repo ships no listing photography, so every picture
  is a declared frame: bordered, muted, with a mono caption naming what belongs
  in it. Drop real images in and the frame is what they replace.
- **`MapPane`** — no map primitive exists in this design system. It owns the
  Leaflet instance, the pins as `divIcon`s, and one piece of defensive work
  worth keeping: a map created inside a `display:none` pane believes it is 0×0
  and draws a single tile, so the component watches its own box and calls
  `invalidateSize` when it gets one.
- **`Floating` / `FloatingTip`** — inherited from the `lib/ui-runtime.js` copy.
  The design system's own overlays portal now too; these stay for their different
  API (one element as the trigger, a render callback that receives `close`).

## Judgements taken

- **No calendar.** `Calendar` selects a single day; a stay is a range. Stating
  the dates in words is honest, and inventing a range picker would have been a
  new primitive in a kit that is meant to demonstrate this one.
- **Search results are a grid, not column stacks.** §5.1's column-stack rule is
  about mixed cards sharing a page; twelve equal-weight place cards are the
  auto-fit exception, the same way metric tiles are.
- **Money is euros, and `lib/ui-runtime.js` is dollar-locked.** `formatMoney`
  there hard-codes USD, and the `lib/` copies may not be locally edited, so
  `metrics.js` carries `formatPrice` as the product's one spelling. Worth a rule
  change: currency belongs to the product, so `formatMoney` should take it.
- **App-scope search is the ⌘K palette, not the where-and-when field.** The
  scope bar on the Stays screen is a filter and lives inside that screen, which
  is why the bar's field is read-only and opens the overlay.
- **No sign-in providers.** Official provider marks are not in this design
  system, hand-drawing them is forbidden, so the one strategy offered is a
  password with a reveal toggle. The demo password is `lodestay`.

## Added in this pass

- **`/trips/:id/receipt` — the receipt.** The one page a guest sends to somebody else: the stay's facts, `quoteLines` in full, every movement with the card it went to, and what the cancellation policy was. A booking with no movements gets the designed state that says why. Local addition: `.ls-rc*`, a document measure and its facts grid.
- **Preview page menu** (`lib/template-menu.js`, byte-identical across the kit): the floating button bottom-right lists every registered route and carries the theme control for the auth screens that render no shell.

## Added in this pass

- **A brand mark** (`assets/templates/logos/28-logo-demo.svg`) in the bar and above every auth form, replacing the icon tile.
- **`/hosts/:id` — the host.** Reached from the place, the trip and the conversation, never from the bar. Nothing on it is stored: their places filter the listings array, their rating is the mean of the reviews those places carry (under the same three-review threshold one place obeys, so a host below it prints no average either), the reviews are the three most recent of that same array, and your bookings with them filter the bookings array. The places are priced for the dates in the scope, so changing the dates on the Stays screen reprices this page too. Local addition: `.ls-stays` / `.ls-stay-row`, one divided list.
- **The rest of auth.** `/signup` and `/verify-email` (what happens next is a screen, not a toast); the four screens of a password reset — request, sent, set-new, changed — with the token in the URL and the dead-link state derived from its expiry against the clock, both tokens reachable from the preview menu; and `/account-locked`, a bounded wait with a real countdown and a way out. Sign-in gained the “Forgot password?” link at the password field's trailing label edge, the “Create account” sibling line, and an attempt counter that says how many are left and then locks — the number in the sentence and the number in the lock are one constant.
- **`AUTH_POLICY` and `AUTH_FLAGS` in `enums.js`.** Every figure the auth copy promises — password minimum, attempts, lock minutes, link life, resend cooldown — is one constant, and the password checklist is the array validation reads. Two countdown spellings in `metrics.js`, one job each: `formatMinutesLeft` for what a link has left, `formatClockLeft` for a wait somebody is sitting through.
