# Stream G2: the airport page, finished
(written 2026-09-18 by the planning session "DFP - Airport Pages", from every source we hold on the
airport pages; runs as `/stream-g2`; task ids G8 to G16 on `/plan`)

> **Status 18 Sep:** not started. Stream G (G1 to G7) landed in 0.47.0 and is the floor this stream
> builds on. The catalogue refactor (K0 to K7) is merged to master, so `shops`, `places` (21 airport
> rows) and `shop_places` exist on staging; nothing here needs a migration.

**Runs:** in `/srv/apps/dutyfreeprofessor` on master, with rian reachable. Staging only; production
is never touched (the crawl window and the launch path are rian's). Rian deploys to staging at the
three browser checkpoints; the stream says "green and ready" and names the after-deploy commands.

## Goal
One airport page (Heathrow) that rian and Adam can approve as **the** airport template, then the
other eighteen populated from Adam's 13 Sep guide series through one import path, so that from then
on a new airport is a data file and a command, never code. The page shows the useful things first,
keeps the long content one click away, and draws honest, good-looking placeholders for the parts we
plan to have (a map, price charts) so the finished shape is visible before the data is.

## 1. The sources, dated, and which wins
Read them in this order; the later ones supersede the earlier where they disagree.

| Date | Source | What it settles |
|---|---|---|
| 3 Sep | `/quote` thread `airport-pages` (Adam, rian) | The promise: a page per airport for the airports we collect, and "an automated way to populate the existing ones"; new airports fit into hosting fees. **Population must be data, not code.** |
| 8 to 9 Sep | `/structure` thread `p-airport` (Mark, Adam, rian) | Terminals and specialty shops within one airport matter (JFK's map, Changi's second floor); modelled later, not a launch item; check whether prices differ between terminals. |
| 9 Sep | Adam's Heathrow **v1** (`notes/Duty_Free_Professor_Airport_Guide_Heathrow.md`) | Superseded. Lounges, transfers, food, getting there: Mark struck all of it on 10 Sep. Keep only its "Terminal Maps" note (link to the operator's map, never rehost their graphic). |
| 10 Sep | Mark, on `p-airport` and `airport-information-pages` | **The page is about the duty free situation at that airport and nothing else.** Where the shops are, specialty shops, perhaps a map; succinct; no filler, least of all for SEO. This is also the structure proposal's lede (`lib/structure.ts` p-airport). |
| 10 Sep | Adam's Heathrow **v2** (`notes/Duty_Free_Professor_Airport_Guide_Heathrow (1).md`) | The Heathrow content, duty free only, per terminal, plus Reserve & Collect. Already in code as `airport_guides.GUIDES["LHR"]`. Heathrow is **not** in the 13 Sep batch, so v2 stays its source. |
| 13 Sep | Adam's **19 Airport Information pages with Adam Comments** (`notes/initial airport page context and docs/…091326.md`; Google Doc `1zZXi8Bm…`) | Batch 2, draft v1, in Adam's own voice: operator, terminal-by-terminal shops, and his "Other things to know". **Restructured by the 16 Sep profiles, so it is no longer the population source, but it is still the source for what they dropped**: his links (Dublin's whiskey club), his figures (Dubai's raffle odds and ticket prices), and any aside the restructure cut. His header names the verification: gate numbers, brand rosters, concessionaire names; ICN, JFK and PTY have had operator changes. |
| **16 Sep** | **Adam's nineteen Airport Profiles** (`notes/initial airport page context and docs/Word_Docs/*.docx`, the PDFs are the same documents; footer "Airport Profile Template v1 · last structured Sept 2026") | **The newest source and the population source. Adam wrote them**, as his later attempt at drafting airport profiles, restructuring his own 13 Sep text. All nineteen including Heathrow, each in one key of seven section kinds used at airport level and again inside each area: **Key facts to know · Key features · Special offerings · Notable shops · Interesting things to buy · Traveler tips · Restrictions and limits**. Areas are not assumed to be terminals: Zurich's are "Airside Center" and "Dock E", which is the cruise-port consideration already proved. His own caveat rides on every profile: researched from public sources, recommend a final accuracy pass before publishing. |
| 13 Sep | Adam's cruise ports doc (Google Doc `1JsRPZRG…`) and thread `cruise-ports` | Not for this stream (rian: after the initial site). It is why the template must not assume "terminal": a port has piers and a terminal building; a mall has floors. |
| 15 Sep | `places-and-shops-2026-09-15.md` and refactor plan **W19** (built by K2) | A place is a row of a kind; a shop has one primary place; the comparison unit is the primary place. **Airport content belongs to the place, not to a shop row and not to a code constant.** |
| 11 to 13 Sep | Stream G's handoff, `airportTemplate.ts`, the 0.45 and 0.46 entries | Rian's reads of the page: 0.45 "too long, prose then grid after grid"; 0.46 compacted it (facts panel, terminal rows, three shelves as tabs) and moved the placeholder cards into one list panel because cards in place made the page longer than the page. |

**The Google Docs are not reachable from the server** (Adam's sharing; the export answers 401), so
the files in `notes/initial airport page context and docs/` are the text of record. The `.docx` files
read with `zipfile` and a tag strip, no new dependency. Adam wrote both, so G8 asks rian one
question only: whether Adam has edited either document since 16 Sep.

## 2. Recommendations (the judgment calls, made)

### 2.0 Adam's profiles are input, not convention
Rian told Adam he would read the profiles and incorporate his concepts as best he can, and that the
page is ultimately rian's to make as good as it can be. So the profiles bind the **content** and not
the **design**: every fact Adam wrote is carried, credited and never invented over, and none of his
presentation choices survives on the page unless it makes the page better.

The line runs between the document and the page:
- **The stored document is faithful.** It keeps Adam's seven kinds and his wording, so nothing is
  lost, provenance is plain, and a later change of mind costs an edit to the renderer rather than a
  re-reading of nineteen Word files.
- **The page is ours.** It decides which kinds are shown, where they sit, whether two of them are one
  thing to a reader, and whether a kind earns a place at all. That is a rendering decision, taken in
  the browser passes against the mission, and it never edits the document.

Three of his conventions are already suspect, and the browser passes settle them rather than adopting
them by default. **Key facts to know and Key features** are a distinction a reader will not feel: on
the page they will probably be one thing, separated by where they sit (the airport, or one area).
**The colour key** is worth having only if a reader learns it once and it helps on the next airport;
if the second pass cannot see it working, it goes. **Interesting things to buy** is the strongest idea
in the set and is currently buried in the middle of each terminal: it deserves promotion, and it is
the one kind the priced data could later join onto.

### 2.1 Richer beats leaner, and Adam's seven kinds are how it is stored
Mark's rule (duty free and nothing else) and Adam's colour are not in conflict, and the profiles have
already resolved them: every fact in them is about shopping there. So **take Adam's seven kinds as
the document's structure rather than inventing one**: they are consistent across nineteen airports,
they repeat at two levels, and a faithful import is the cheapest thing to review. Where each one
lands on the page is the recommendation below, not a property of the document, and section 2.0
governs it.

| Kind | Identifier | Where it currently earns its place |
|---|---|---|
| Key facts to know | `key_facts` | Airport level, near the top: the three or four things that decide how you shop. |
| Special offerings | `special_offerings` | Airport level near the top (Reserve and Collect, a price promise), and inside an area where it is that area's. |
| Key features | `key_features` | Inside an area: what this terminal is. |
| Notable shops | `notable_shops` | Inside an area: the roster. |
| Interesting things to buy | `interesting_to_buy` | Inside an area: what is worth a detour. |
| Traveler tips | `traveler_tips` | Below the products, in Know before you shop. |
| Restrictions and limits | `restrictions` | Below the products, under Arriving and clearing customs. |

**Keep the items as the profiles wrote them; do not parse them into fields.** Allowances arrive as
bullets ("up to 4 litres of spirits over 22% ABV"), and turning those into `{kind, quantity}` means
guessing at ABV bands and per-country rules, which is exactly the guessing the project forbids.
Structuring allowances later is worth an issue, not a launch task; the JSON-LD gets the airport and
its hours, which is what search actually reads.

**What we hold that the profiles do not leads the page**: hours with provenance, which shops we read
and when, the four counts, best value, exclusives, categories, currency. Where a profile's operator
line and our retailer row disagree (SAL names five concessionaires, we read Attenza), the guide keeps
every shop the profile names and the page marks the ones we read. That list is the seed of the shop
and retailer pages without building either now.

**Adam's voice is not lost in the restructure.** Where the 13 Sep text has something the profile
dropped, G13 puts it back as an item of the right kind: Dublin's Duty Free Whiskey Club link, Dubai's
raffle ticket prices and odds, the STEB-bag tip at San Salvador, the last-minute kiosks at Athens.

### 2.2 The guide moves out of code and onto the place
`airport_guides.GUIDES` was right for one airport and wrong for nineteen: every airport would be a
code deploy, which is the opposite of the 3 Sep promise. The guide becomes a **JSON document stored
on the place row** (`places.attributes["guide"]`, no migration; K2 built the attribute registry for
exactly this), imported from a **source file per place in the repo** (`import/places/<slug>.json`,
reviewed by diff, versioned by git) with `app.cli places guide import <file> [--check]`, idempotent,
`--by <account>`; a human value is never overwritten by a machine, so `guide` is a hand value and
only the import writes it. `guide_for()` reads the place; the constant is deleted after LHR round-trips
(a test). Hours stay in `airport_hours` with their provenance (Stream G); the guide never carries hours.

**The document mirrors the profiles, which makes it kind-neutral for nothing extra.** An area is an
area, not a terminal: Zurich's are "Airside Center" and "Dock E", and a cruise port's piers or a
mall's floors fit the same field with a different label from the place-kind registry
(`PlaceKind.area_label`). Fields, all optional except `overview`:

```
guide: {
  version: 1,
  sources: ["profile-2026-09-16", "adam-2026-09-13", "adam-lhr-v2-2026-09-10"],
  structured: "September 2026",          # the profiles' own footer date
  verify: [str],                          # every profile's accuracy caveat, owner-facing, never shown
  strapline: str,                         # "Terminals 2, 3, 4 and 5, duty free run by World Duty Free (Avolta)"
  operator: str | null,
  overview: str | null,                   # prose, where a profile has it rather than bullets
  sections: [{ kind, items: [str] }],     # airport level; kind from the seven
  areas: [{ code, name, strapline, sections: [{ kind, items: [str] }] }],
  arrivals: { title, sections: [...] },   # "Arriving and clearing customs"
  shops: [{ name, retailer_slug | null, areas: [code], kinds: ["main","boutique","specialty"] }],
  services: [{ title, body }],            # a named service worth its own line (Reserve and Collect)
  map: { url, label } | null,
  links: [{ title, url }]
}
```

An unknown `kind` fails the import rather than rendering unlabelled. The items are strings with
inline emphasis stripped; the page decides the styling, never the document. No terminal table and no
shop-within-airport table now (A18's proposal stands; that model change touches addresses), and the
`areas` and `shops` entries are shaped so a later migration promotes them to rows without rewriting
the documents.

**Spelling:** identifiers stay American (`traveler_tips`, per `test_house_style.py`), and the label a
reader sees is the site's British ("Traveller tips"), decided once in the label map.

### 2.3 The page: useful first, the rest one click away, nothing to scroll past
Rian's brief: a very good, usable airport listing page, core numbers and summaries up front, no
clutter, detail reachable but never a jungle, and never the recipe page. Against today's Heathrow:
- **Keep** the hero and its two actions; the facts panel; the terminal rows as `<details>`; the three
  shelves as tabs with the shelf in the URL; the category rail; the sponsor slots.
- **The bulk is "Featured at this airport": four families times four cards is sixteen cards before
  the shelves.** Cut it to **one row of four, the best comparison per family**, with "all best value
  here" opening the Best value shelf. That alone halves the page above the products.
- **Hours become one short line** ("05:00 to 22:00, most stores, collected 13 Sep") with the per-terminal
  text behind an expand; today the whole paragraph sits in the facts list.
- **The airport-level Key facts and Special offerings go high**, as a two-card row under the facts
  panel: three or four lines each, and precisely the "core information up front" the mission asks
  for (Heathrow: four terminals, who runs them, where the range is deepest; Reserve and Collect,
  personal shopping, Avios).
- **Each area's sections live inside that area's row**, which already opens in place: Key features,
  Notable shops, Interesting things to buy, and a Special offering where the area has its own.
- **A "Know before you shop" section** holds the rest below the products, as accordions in a fixed
  order: Arriving and clearing customs (the restrictions), Traveller tips, Shops and boutiques (the
  `shops` list), Services, Links. Closed by default on the phone; the first open on desktop.
  Rendered in the server body as the same `<details>` markup (crawlers read closed details; the
  content counts, the scroll does not).
- **Adam's colour key is tried, not assumed.** If it is kept: one accent per kind, the same on every
  airport, on the section label and a thin left rule only, defined in one map beside the label map
  and used nowhere else. Never the filled pastel blocks of the documents, which at page width are the
  clutter being designed out. The second browser pass decides whether it survives, and the handoff
  says which way and why.
- **Merging or dropping a kind on the page is allowed and expected** (section 2.0). Say so in the
  handoff with the reason, leave the document untouched, and make sure nothing Adam wrote becomes
  unreachable: a kind that loses its own block still appears somewhere, or it is named as dropped.
- **Jump links** under the hero on the phone (Where to shop · Best value · Know before you shop),
  so the page is a lookup, not a scroll.
- **Retire the separate "Professor's write-up" part**: Adam's overview is the write-up. The
  `airport_writeup` article kind stays for an airport where he wants a real column; the template
  stops listing it as a gap.
- **Schema, out of sight:** JSON-LD `Airport` (name, iataCode, address country, `openingHours`
  from the hours line, `containsPlace` for the named shops) in the head; the allowances table and
  the terminal rows are already in the body. Nothing is added to the visible page for search.

### 2.4 Placeholders that look like the part, with the instruction on hover
The list panel (`TemplateGaps`) is the wrong thing: rian asked to see what the finished page would
look like, not a task list. Replace it with **in-place skeletons**, each the size and shape of the
real part: a map frame with three faint pins and a "Terminal map" caption; a sparkline card row for
Notable price moves; a hours row; a text block for an area that has no write-up. Each carries a
small corner tag ("placeholder"), and hovering (or focusing) shows the note that the list used to
show: what it is, who fills it, what to gather. **Placeholder view off draws nothing**: the part is
absent and the page must still look finished, which is the test of every layout decision (G11
reviews the page both ways). The server body never draws a placeholder (`TestAirportGuide` pins
this; keep it). Where the real part is collapsible, the skeleton is drawn collapsed so the page
does not get longer, which is what sank the 0.45 cards. Keep the one-line count ("4 of 13 parts to
fill") as a small pill under the hero in placeholder view, since it is how rian sees the work left.

### 2.5 Heathrow has two sources that disagree, and both are kept
The 16 Sep profile and Adam's 10 Sep v2 describe the same terminals differently: v2 has the store
sizes, the gate positions (T2's A20 and B35) and the Flight Connection Centre store for passengers
connecting without clearing the border; the profile has the roster (Bladnoch, Fielden, the Macallan
boutique, Ladurée, Hamleys), the concession end date, and the correction that T2 is four stores
rather than one hall. **The profile is the structure and the baseline; v2's specifics are merged in
as items of the right kind**, because "where exactly is it" is what Mark asked the page to answer and
the restructure lost it. Anything the two flatly contradict goes into `verify` and onto Adam's list
rather than being silently resolved. The same rule covers the other eighteen wherever the 13 Sep text
is more specific than the profile.

### 2.6 One airport first, then eighteen, and where the gate is
Heathrow is finished first (G8 to G12) and handed to rian with an explicit approval ask. The
conversion of the eighteen (G13) and their import to staging (G14) proceed without waiting,
because they are data work against a schema fixed in G9 and any template change rian asks for
applies to all nineteen by construction; nothing is published beyond staging. If rian wants a hard
stop after Heathrow he says so at the first checkpoint and G13 waits.

### 2.7 What stays out
Terminal and shop-within-airport rows (A18), price charts (after launch; the placeholder stands in),
our own maps (the placeholder stands in; the operator's map link is the data we can have now),
cruise ports (a kind and a source file when Adam's list is agreed), retailer and shop pages (the
`shops` list in the guide is their seed), any address change, anything on production.

## 3. What already exists, so you build on it
- `AirportDetail.guide` (`models/hubs.py`), `services/airport_guides.py` (LHR only, in code),
  `guide_for(iata, db)` merging the hours row; `catalog_queries.py:1187` is the one caller.
- `services/hours/` and `airport_hours` (LHR and DUB collected; YYZ refused by a captcha; the other
  sixteen wait for hand rows: `app.cli hours set <IATA> --file --by`).
- `services/places.py` (kinds registry, `unit_count`), `models/places.py` (`attributes` JSONB),
  `backfill places` already run on staging (21 airport places, one primary per shop).
- `pages/AirportPage.tsx`, `components/AirportGuide.tsx` (facts panel, terminal rows),
  `AirportFeatured.tsx`, `AirportFilters.tsx`, `AirportHours.tsx`, `MissingPart.tsx` (the list
  panel), `lib/airportTemplate.ts` (fourteen parts with `present()`), `lib/settings.ts`
  (`placeholders`), `pages/AirportCategoryPage.tsx`.
- `seo.py` `airport_body`, `airport_facts_html`, `airport_terminals_html`, `airport_featured_html`,
  `airport_head`; `tests/test_seo_airport.py` (forty tests pinning server and SPA say the same words).
- `cli_editorial.py` (`articles import --kind airport_writeup`), `import/articles/`.
- The source material, all under `notes/initial airport page context and docs/`: the nineteen
  profiles as `Word_Docs/<IATA>_*.docx` (the PDFs are the same documents, and are what a person
  should read to see the shape they intend), with Adam's 13 Sep markdown beside them; Heathrow v2 is
  `notes/Duty_Free_Professor_Airport_Guide_Heathrow (1).md`. A `.docx` reads with `zipfile` and a tag
  strip: paragraphs are `</w:p>`, and `&amp;`, `&apos;` and `&quot;` need unescaping.

## 4. Tasks, in order (ids on /plan; `main/scripts/plan-set.py`; commit prefix `G2:`)
1. **G8 Sources reconciled, the guide document defined.** Ask rian one question at the start (he is
   present) and carry on while he answers: has Adam edited either document since 16 Sep. Read one
   profile as a PDF, not only as text, so you can judge his presentation and not only his words; then
   write down, before any code, which of his seven kinds you expect to keep as their own block on the
   page and which you expect to merge, with the reason (section 2.0). It is a first view to be
   overturned in the browser, not a decision. Write the guide document schema (section 2.2) as
   a Pydantic model in a new `models/place_guide.py`, the seven kinds as one enum with the label and
   accent maps beside them, and `PlaceKind.area_label` added to the registry ("Terminal" for airport,
   "Pier" for port, "Floor" for mall). One home for the mechanism: a "Place guides" section in
   `main/docs/DATA-MODEL.md` (what the document is, where it lives, how it is imported, that it never
   carries hours, that an unknown kind fails the import), and `docs-check.sh` green. No page change yet.
2. **G9 The guide lives on the place, and Heathrow is the first document.** `app.cli places guide
   import <file> [--check] [--by <username>]` and `places guide export <slug>` in a new
   `cli_places.py` (one `register` line in `cli.py`). `import/places/heathrow-lhr-london.json` is
   built from the 16 Sep profile with v2's specifics merged in per section 2.5 and the
   contradictions listed in `verify`. `guide_for` reads `places.attributes["guide"]` through the
   attribute accessor; the code constant is deleted once the tests pass. Verified three ways, because
   Heathrow's content genuinely changes here and the change must be deliberate rather than noticed
   later: export of an imported document equals the file; a second import writes nothing and
   `--check` prints the diff and writes nothing; and `airport_body` for LHR is captured before and
   after on a copy, with the diff read line by line and summarised in the handoff. Tests on SQLite.
3. **G10 The page, pass one, in the browser.** Before touching the layout, **re-read rian's brief
   for this stream verbatim** (the "Mission" block at the end of this file) and section 2.3; list
   in the handoff which of its asks the current page fails. Then: featured cut to one row; hours to
   one line with an expand; the Know-before-you-shop accordions; jump links; the write-up part
   retired from the template; JSON-LD Airport in the head. Server body and SPA move together
   (`test_seo_airport.py` pins them; extend the pins to every new block). Measure the page height
   with placeholder view off before and after, desktop and 390px, and write both numbers down.
   **Checkpoint 1:** "green and ready"; rian deploys to staging; look at it together in Chrome.
4. **G11 Placeholders as the part.** Section 2.4: a `Placeholder` component with one skeleton per
   template part that can be missing (map, charts, hours, an area's write-up, allowances, tips,
   services, shops), the corner tag, the hover note, the count pill; `TemplateGaps` deleted;
   `airportTemplate.ts` reduced to the parts that remain and each part's `render` for its skeleton.
   Verify both settings states on LHR and on an airport with no guide at all (ICN) at desktop and
   phone widths; with the view off, ICN must look finished, not empty.
5. **G12 Pass two and three, then the approval ask.** Two more browser rounds on LHR, each starting
   from a fresh screenshot set saved under `.logs/verification/airport-page-2026-09-<dd>/` (desktop,
   390px, placeholder view on and off), each fixing what the screenshots show and nothing the
   screenshots do not. The third round is the review against the Mission block line by line.
   **Checkpoint 2:** rian deploys; a `decide` on the running list, `--blocks G14 --weight blocking`:
   "Approve the airport template on Heathrow", with the assumption you proceed under (approved as
   shown; changes apply to all nineteen).
6. **G13 The eighteen converted.** A script `main/scripts/convert_place_profiles.py` reads each
   `Word_Docs/<IATA>_*.docx` and emits a draft document: the strapline from the sub-title, the
   airport-level sections, an area per block with its code, name and strapline, the arrivals block,
   and each section's bullets as items under its kind. The extraction is mechanical because the
   profiles are consistent; anything that does not match the key fails loudly naming the airport and
   the line, and is fixed by hand rather than guessed. Then a **pass per airport** that reads the
   13 Sep text beside the draft and puts back what the restructure dropped (section 2.1's last
   paragraph), fills `shops` with every concessionaire named and sets `retailer_slug` where we read
   it, writes `verify` from the profile's own caveat plus Adam's (ICN, JFK and PTY operator changes,
   gate numbers), and corrects the voice (no em dashes, British prose). Every file passes the
   schema; `test_house_style.py` walks `import/places/`. Delegate the extraction to a cheaper model;
   never the merge pass, the shops mapping, or anything a shopper reads.
7. **G14 The eighteen imported and checked.** `places guide import` for each on staging's copy on
   `dfp-devdb` first, then the after-deploy command list for staging (one import per file, in
   order, each safe to repeat). Every one of the nineteen pages rendered from the copy and its
   height and part count recorded in the handoff; any airport whose page looks wrong (SIN and ICN
   are thin on products) named with what it needs. Adam's own caveat becomes one `do` for rian: the
   profiles are researched from public sources and he recommends an accuracy pass before publishing,
   so the item names the airports and the fields at issue (gate numbers, shop rosters, the operator
   changes at ICN, JFK and PTY) as the list to check or to send back to Adam.
8. **G15 Hours by hand where a source states them, provenance kept.** Where a profile or the 13 Sep
   text states hours (Athens' main store is 24 hours; an inference from "early and late flights" is
   not a time), `hours set <IATA> --file --by rian` with the source named in `detail` ("from the
   airport profile, 16 Sep"); where none is stated, nothing is entered and the hours line stays
   absent (empty beats guessed). A run note in `.logs/runs/`. No network.
9. **G16 The airports index and the loose ends.** `/airports` reads `has_guide` from the place;
   the sitemap unchanged; `CHANGELOG.md` Unreleased; `main/docs/SEO.md` names the Airport JSON-LD;
   `COLLECTORS.md` unchanged; a `v8-feedback.md` line for anything the substrate cost you. **Checkpoint
   3:** green and ready with the full after-deploy list (imports, hours, nothing else).

## 5. Owns
New: `main/app/models/place_guide.py`, `main/app/cli_places.py`, `main/app/services/place_guides.py`,
`import/places/*`, `main/scripts/convert_adam_airports.py`, `main/web/src/components/Placeholder.tsx`
and `.css`, `main/web/src/components/AirportKnow.tsx` and `.css`, `main/tests/test_place_guides.py`,
`main/tests/test_airport_page_parts.py`, `main/tests/fixtures/place_guides/*`,
`.logs/verification/airport-page-*`.
Existing: `main/app/services/airport_guides.py` (to delete), `main/app/services/places.py` (the
registry line), `main/app/models/hubs.py`, `main/app/services/seo.py` (airport functions only),
`main/app/services/catalog_queries.py` (the guide call only), `main/app/services/coverage.py`
(`has_guide`), `main/web/src/pages/AirportPage.tsx` and `.css`, `main/web/src/pages/AirportsPage.tsx`,
`main/web/src/components/Airport*.tsx` and `.css`, `main/web/src/components/MissingPart.tsx` and
`.css` (to delete), `main/web/src/lib/airportTemplate.ts`, `main/web/src/lib/settings.ts` (the
setting's comment only), `main/tests/test_seo_airport.py`, `main/tests/test_house_style.py` (the
walked paths only), `main/docs/DATA-MODEL.md`, `main/docs/SEO.md`, `main/docs/RUNBOOK.md` (your
commands under CLI reference), `main/CHANGELOG.md`.

## 6. Must not touch
The review area and the ledger (`services/decisions*`, `services/review*`, `routers/review.py`,
`pages/Review*`), identity and matching (`normalize.py`, `merges.py`, `lines.py`, `identity*`),
the retail collectors and `ingest.py`, `services/hours/` beyond calling `hours set`, `access.py`
beyond your own route keys, `REVIEW-PROCESS.md`, any migration, `.env`, `.app.env`, `deploy/`,
production in any form, the live database (rehearse on `dfp-devdb`, 127.0.0.1:5433, in a fresh
copy of the newest `backups/dfp-nightly-*.dump`).

## 7. Rules
- **No migration.** The guide is an attribute on the place; if you find you need a column, stop
  and file a decide with the reason.
- **Adam's facts, our voice, nothing invented.** Every sentence on a page traces to his text, our
  data, or a field he left empty that the page simply omits. Never fill an empty field.
- **A GET never writes; a human value is never overwritten by a machine; empty beats guessed.**
- **Server body and SPA say the same words**, class for class; extend the pins as you add blocks.
- **Placeholders never reach the server body or a shopper**; the setting off must look finished.
- No em dashes and never "cheap" or "free" alone (duty free is the product) in anything a person
  reads; `test_house_style.py` walks your new files and `import/places/`.
- **No address moves.** `/airports/<name>-<iata>-<city>` and the category pair stay.
- `main/check.sh` green before every commit; one task, one commit, `G2:` prefix; handoff ≤25
  lines; `/plan` updated; decisions on the running list with `--blocks` and `--weight`.
- Everything else per `OVERNIGHT-RULES.md`, except that rian is present: ask him the G8 question,
  and say when a checkpoint is ready instead of filing it.

## Mission (rian, 18 Sep, verbatim; re-read before G10 and again in G12's third round)
> We started the airport pages a while ago, but I need to finish them off. Put together
> recommendations for how we 1) get the template finalized 2) populate all the airports.
>
> We've moved to a places / shops / retailers model with the thought in mind that we will expand
> beyond airports and there are multiple shops in some airports. That doesn't mean we can't have
> different templates for other types of places (like cruise ports), but it should be at least a
> consideration as we build out the airports template. We've only promised an airport template,
> but I imagine in the near future we will have shop pages and retailer pages.
>
> I'd like to get one completed airport done first so we can approve the basic template, then get
> the rest done.
>
> Adam's newest docs may not have all the detail we collected. Let's make a judgment call on how
> to deal with that. It's better to have richer information, especially if it is being auto
> collected and if it's structured for schema.
>
> When doing the template, the plan should involve looking at it a few times in the browser and
> trying to improve it. The goal is to have a very good, usable "airport listing page" that shows
> some core information up front including some numbers and summaries, but not overdoing that with
> too much clutter and information that nobody really cares about. If there's more detail that we
> want for SEO/schema/etc, it could be accessible via expanded areas or tabs or something. With all
> the terminal writeups and stuff, that's a lot of content which we just don't want to become bulk
> that people have to scroll past to get what they want. I do not want this to be like those
> horrible SEOed recipe pages where you have to scroll past the cook's life story just to find the
> ingredients. We want to get the most useful information up front and easy to access, and the rest
> should be easily accessible but not a jungle that people have to navigate through. Think
> accordions, tabs, TOCs, stuff like that. Not those things just because I said them, just ideas.
>
> We have ideas in our template that are not covered by any of Adam's docs or our crawling. For
> example, an airport map is a great idea, but we don't have that data. Let's plan for the idea that
> we may soon add that on SOME airports. There should be a place in the template where that can
> live nicely, but also the template will still look great without those things. We can toggle
> placeholders for all the core parts of our template on and off in the demo settings. Right now we
> kind of have that, but it looks more like a list of things we should get, not the actual
> placeholders. I wanted to see a placeholder showing what that map would look like if we had it.
> Hovering over the placeholder may give instructions of what we should gather, but by default it
> just looks like there's a placeholder there ready to be replaced, and when the setting is toggled
> off, it's just shown as it would to the user now (which should still look good).
