# Build spec — FPV Drones for Beginners (Rotor Racing HK course site), official-HTML mirror

## Goal

The five-day beginner FPV course material lives only as a Google Site published on a Workspace domain
(`sites.google.com/rotorracinghk.com/fpvdrones`). Mirror **the site's own HTML** — not a remake — as a
static folder that renders with no login, no Google session and no network.

## Source

- Root: `https://sites.google.com/rotorracinghk.com/fpvdrones` (public, no auth).
- 20 pages; the navy bar exposes them all (`/learn/<page>`, `/tinywhoop-go/<page>`, `/flight/<page>`, `/schedule`).

## Pipeline

1. **Render, don't crawl.** `curl` returns a JS shell. Load each URL in headless Chrome (CDP :9444,
   1440×1000), scroll to the bottom in ~0.85-viewport steps to trigger lazy images, scroll back, wait.
2. **Capture the hydrated DOM.** `document.documentElement.outerHTML` — this is the site as it looks,
   including Google's inline stylesheet and computed layout.
3. **Inline images from inside the page.** `sitesv-images-rt` URLs are session-signed: `urllib` → 403,
   in-page `fetch(src,{credentials:'include'})` → 200. Convert each `<img>` to a data URL by rewriting
   `src` in the DOM before capture. Do the same for CSS `background-image` (the hero banner is a
   background, not an `<img>`): read `getComputedStyle(el).backgroundImage` and set an inline
   `background-image:url(data:…)` on the element.
4. **Strip Google's scripts.** Remove every `<script>` and `<link as="script">`. Keeping Google's bundle makes the
   snapshot try to re-hydrate from the network — it blanks the local copy when offline.
5. **Localise the nav.** Google writes tabs as root-relative `/rotorracinghk.com/fpvdrones/…`. Rewrite to
   per-depth relative paths (`../learn/index.html`) using a `{page → folder}` map; unknown targets point
   back at the live site.
6. **Shrink** — `sips -Z 1400 -s format jpeg -s formatOptions 85` for any data URL over 380 KB:
   78.8 MB → 41.1 MB across 20 files (max 5.2 MB), all under the 25 MiB Pages cap.
7. **Layout** — `demos/fpv-drones/index.html` + one folder per page (`learn/antennas/index.html`, …) so
   the site's own tabs resolve.

## Constraints

- `/demos/fpv-drones` stays in `SKIP_PREFIX` (`~/scripts/theme-inject.py`) so the site skin's +390 B and
  theme button never touch the mirror.
- Zero external requests at load: no script bundle, no remote `<img>`, no external `href` to the origin.
  `https://` links to third parties inside copy are left alone.

## Verification (all automated)

- 20 files written; per file: `href="/rotorracinghk.com` == 0, `<img src="http` == 0, Google `<script src=` == 0.

## Step 6 — restore the menus (CSS-first, 20-line JS for the drawer)

Google's JS also drove the hover dropdowns and the mobile hamburger, so a stripped snapshot loses both.
The site's own markup already contains the submenus (`li[data-nav-level="1"] > .oGuwee`); they are only
`display:none` until Google's `jsaction` toggles them, and the items carry `opacity:0` + `margin-top:20px`
for the staggered reveal. `scripts/fpv_add_dropdowns.py` appends one `<style id="fv-dropdowns">` and one
`<script id="fv-nav">` to every page (idempotent — it strips its own previous block first):

- `nav.plFg0c li[data-nav-level="1"]:hover > .oGuwee{display:block!important}` — hover reveal, pure CSS,
  `right:0` so the Flight menu can't overflow the viewport, 300 px wide, `padding:8px 18px` per item,
  `.PsKE7e{margin:0}` to kill Google's staggered-reveal spacing (570 px → 350 px tall).
- `@media (max-width:920px)` turns `nav#yuynLe` (the sidebar drawer) into a fixed panel, forces
  `opacity:1; transform:none` on its items and expands the nested `ul`s.
- The 20-line script only wires the hamburger → `nav#yuynLe` display toggle, close on link click, Escape.
  It must run on `DOMContentLoaded` — the block sits before `</head>`, where `getElementById('s9iPrd')`
  is still null.

Verify with CDP, not a screenshot alone: hover dispatch needs several `Input.dispatchMouseEvent
mouseMoved` steps before CSS `:hover` applies, and `.oGuwee` must report `display:block`, width 300 and a
rect whose right edge is inside the viewport.
- Rendered home / Antennas / Schedule from the local copies against the live site: same header, banners,
  nav highlight, timetable table intact.
- Card modal (751 px iframe) opens `./demos/fpv-drones/` and the first page renders.

## Known caveats (disclose, don't hide)

- Dropdown menus and the site search are JS-driven, so they are inert in the snapshot.
- YouTube embeds become poster frames / links.
- Web fonts come from gstatic; offline they fall back to system faces.
- Some of the origin's own image URLs already 403 — dead on the live page too.
