# Cinematic site — the founder's hand-off homepage (Main · Public site)

**Portal:** Main · **Routes:** `GET /` (`landing` — the "choose your market" screen, or a redirect to the remembered market), `GET /{country}` (`main.site.index` — `/my` Kuala Lumpur, `/hk` Hong Kong, `/ae` Dubai), `GET /partners` (`main.site.partners`), `GET /about`
(`main.site.cinematic-about`), `GET /contact` (`main.site.contact`), `GET /preview/{cinematic,partners,about,contact}`
and the two form posts `POST /partners/apply`, `POST /contact` — **all only when `site.cinematic_home`
is on** (wk) · **Nav:** the pages carry their own header
and footer; the portal sidebar is not involved · **Gated by:** nothing, it is **PUBLIC** ·
**Writes:** `partner_applications`, `enquiries`; each submission is then emailed to the founder, announced by WhatsApp and listed in the CEO section (see *Website forms* below).

## What it does

A scroll-driven cinematic homepage (hero + five chapters — Discover, Evidence, AI + Plan, Connect,
Beyond the keys — with Next/Back stepping, then How it works, the app, Partners, About and the footer)
and three inner pages: Partners (with an application form), About, and Contact (six enquiry topics,
`?type=careers` pre-selects one). It arrived as a finished, approved package
(`propertylab-cinematic-v10.0-laravel.zip`, 2026-10-03) and went live as wk's front door the same day.

## How it works

- **Three markets, one chooser (2026-10-04, owner).** [`CinematicSiteController`](/app/Http/Controllers/Main/CinematicSiteController.php)
  holds the market list (`MARKETS`: `ae` Dubai, `my` Kuala Lumpur, `hk` Hong Kong — add a market there).
  - `GET /` → `choose()`: no `site_market` cookie → `cinematic/chooser.blade.php`, **two steps, bilingual (English +
    中文 shown together — owner, 2026-10-04)**: (1) three diagonal city panels — Kuala Lumpur · **Dubai in the middle** · Hong Kong — the About hero's design and its
    daylight 4K pictures (light theme; on Continue the chosen city fills the screen and fades to the page), with a staggered rise-in, slow drift and a light sweep; (2) clicking a city opens its panel and asks
    **two questions, one after the other**: "Where are you from? 您来自哪里？" (`ORIGINS`: China, Hong Kong, Taiwan,
    Singapore, Malaysia, Others → the display currency; Others keeps the market's) then "Which language do you
    prefer? 您偏好哪种语言？" (`LANGUAGES`: English, 简体中文 → `zh_CN`, 繁體中文 → `zh_HK`). **Pre-selected from the
    visitor's IP country** — Cloudflare's `CF-IPCountry` header, via `preference()`; an unlisted country is
    "Others" — and the language follows the origin (China → 简体, HK / Taiwan → 繁體, else English) until the visitor
    picks one. A saved choice (`site_origin` cookie + `site_lang`) wins over the IP guess. It submits to
    **`GET /enter/{country}?from=&lang=`** (`enter()`, `main.site.enter`; `enter` is a reserved funnel slug), which
    queues `site_origin`, `site_cur`, `site_lang` and `site_country` and
    redirects to `/{country}`. With the market cookie → 302 to `/{code}`. `/?choose=1` always shows the screen.
    ⚠️ **Never put text inside the skewed panels** — skew + counter-skew rasterises it soft (owner: "blur to see
    those font"); labels and the two question forms live in straight overlay layers (`.labs`, `.dets`). And **no
    white wash over the pictures**: it hid the tower tops ("something cover klcc top"). The owner prefers the
    full-screen panels with the headline laid straight over them (a separate headline band above was tried and
    rejected), so the headline is navy text directly on the pictures.
    The panel under the cursor **floats in 3D** (the row has `perspective`; the panel comes forward 150px, tilts
    with the cursor via `--rx` / `--ry`, casts a deep shadow, neighbours blur; its label grows with it) — this
    needs `body.ready`, set 2.4 s after load, which switches the entrance animations off: their held
    `clip-path` would clip the shadow and their held transform would block the lift. The whole headline is navy
    (the gradient on "across borders" was removed on request, 2026-10-05).
    **Phones have their own layout** (last `@media (max-width:760px)` block; the slanted version was "super
    weird" there): headline in its own block, three rounded un-slanted picture cards, and step 2 = the city's
    picture on the top 38% with the questions on a plain sheet below.
    (The first design, "One home. Three skylines." — one living room, three window views — was replaced.)
  - `GET /{country}` → `show()`: renders that market's view, queues the `site_market` cookie (1 year) and
    **appends `cinematic/partials/market-switch.blade.php` before `</body>`** — a pin pill in the nav
    (desktop) and a chip row in the menu sheet (phones) to change market. **One control (owner, 2026-10-05)**: the pill
    reads "{city} · {language}" and its dropdown lists the three markets, then "Your selection" (from, language)
    and "Select again · 重新选择" (→ `/?choose=1`); the pages' static "EN / 中文" is removed, and on phones the
    sheet's language row becomes that link. It is injected at render time
    because the pages are generated and frozen; its script moves the markup into `.nav-in` / `#sheet`.
  - **`hk.blade.php` and `ae.blade.php` are BUILT from `home.blade.php`** by the founder's
    `tools/apply_hk.py` / `tools/apply_ae.py` (118 text/asset rules each; prints OK/MISS per rule).
    **After any change to `home.blade.php`, re-run both** and expect `118 OK, 0 MISS`:
    `python3 tools/apply_hk.py resources/views/cinematic/home.blade.php resources/views/cinematic/hk.blade.php`.
    Their own films and photos live in `public/cinematic/{hk,ae}/assets/` (~57 MB each); everything else is
    shared from `/cinematic/assets/`. Packages: `/file/da90ce00-…` (HK), `/file/fd7a338e-…` (AE).
  - ⚠️ **The HK page is a placeholder build** (its HANDOFF): project name "Kai Tak New Project", developer,
    rent/yield figures, Discover cards 2–4 and all project photos are placeholders to replace before launch.
  - The consultant's T-shirt logo re-bake (below) was done on the KL films only.

- **The pages are generated, frozen output.** `resources/views/cinematic/home.blade.php` and
  `resources/views/pages/*.blade.php` are produced by the package's `source/build.py` and
  `source/pages/pages.py`; **never hand-edit them** — change the package source and rebuild, then copy
  the output over. Design, copy, timing and assets are frozen by the package's own `CLAUDE.md`.
  The package is not in the repo; ask the founder for the current zip.
- **Plain HTML, not Inertia.** No Vite build, no external JS libraries; Google Fonts (Sora, Inter,
  Cormorant Garamond) only. Every route runs the `full-page` middleware
  ([`ForceFullPageVisit`](/app/Http/Middleware/ForceFullPageVisit.php)): an Inertia visit (a `<Link>`,
  or a redirect an Inertia XHR follows, e.g. `redirect()->route('landing')`) gets a 409 +
  `X-Inertia-Location` and the browser does a full load, instead of Inertia painting the HTML in a modal.
- **Behind a per-deployment flag.** `SITE_CINEMATIC_HOME=true` → `config('site.cinematic_home')`
  swaps `/` from `Main\SiteController@home` to the cinematic view and adds `/partners`, `/about`,
  `/contact`, the `/preview/*` copies and the form posts. Off by default, so merging the code changes no other site's homepage. The Inertia home
  stays reachable at `/home` and `/{country}/home` either way. Routes are evaluated at
  `route:cache` time — after flipping the flag run `config:cache` **then** `route:cache`.
- **Assets** live in `public/cinematic/assets/` (~98 MB: 12 scroll-scrubbed films, Duo phone frames,
  partner logos), referenced by absolute `/cinematic/assets/...` paths. **`public/cinematic/` is
  committed** (2026-10-03, owner's call): it was gitignored and existed only on wk's disk, with the
  founder's zip in a `/tmp` folder, so one lost server would have lost the films. Every file is under
  GitHub's 100 MB limit (largest 20 MB). Git keeps every version of a binary forever, so replace a film
  only when it really changes, under a NEW name (see the cache rule below). The routes still sit behind
  the flag, so other deployments carry the files but never serve them. The set came from the v10.0 zip
  uploaded at `/file/ab914e36-64dc-41a8-b4f2-9115e2e269ce`. Phones and desktops load
  different files on purpose (iPhone Safari memory). Do not rename, re-encode or move them.
  - **Films need HTTP byte ranges (206)** or they freeze on frame one — Apache's static serving does it.
  - `public/.htaccess` sets `Cache-Control: public, max-age=31536000, immutable` on
    `/cinematic/assets/*` (the package renames a file when it changes). Apache's deflate gzips the
    `.json` bundles and leaves `.mp4` / `.webp` alone, as the package requires.
- **Forms** post with `fetch()` + `X-CSRF-TOKEN` (each view prints `<meta name="csrf-token">`) to
  [`EnquiryController`](/app/Http/Controllers/EnquiryController.php) — the package's starter
  controller, kept as shipped: inline validation, a raw `DB::table()` insert of a JSON `payload`, then
  `Mail::raw()` to support. ⚠️ It does not follow GUIDELINES (no Form Request / Repository) and the
  email is sent **after** the insert, so a mail failure returns a 500 with the row already saved
  (2026-10-03: Mailgun had disabled `mg.propertylab.com.my` for spam). Port it to the CRM before
  relying on it.
- **Funnel slugs** `partners`, `about`, `contact`, `preview`, `cinematic` are reserved
  (`Funnels\StoreRequest::RESERVED_SLUGS`) so a funnel can never shadow these pages or be shadowed.
- **Acceptance tests** live in the package (`tests/e2e/cinematic.spec.ts`, Playwright, five tests,
  run against `/preview/*`). Run them from a scratch folder, never by adding Playwright to
  `package.json`: `BASE_URL=https://wk.propertylab.com.my npx playwright test`.

### Local patches on top of v10.0 — re-apply after any rebuild from the founder's source

The home page's changes are in [`template.html.patch`](/docs/modules_handbook/main/cinematic-site/template.html.patch)
(a diff against `source/template.html`; `patch -p1` from the package root, rebuild with `source/build.py laravel`,
then wrap the output in the Blade header + `@verbatim` as shipped). The inner pages' changes are in
[`pages.py.patch`](/docs/modules_handbook/main/cinematic-site/pages.py.patch), a
unified diff against the package's `source/pages/pages.py`. From the package root:
`patch -p1 < pages.py.patch && cd source/pages && python3 pages.py laravel <out>`, then copy
`<out>/*.blade.php` to `resources/views/pages/`. v10.0 + the patch reproduces the committed pages byte
for byte. A new package version from the founder may not apply cleanly; re-do each change by hand.

- **`source/template.html` (home): scroll-scrubbed films no longer blink back to the previous scene**
  (2026-10-03, *"when I scroll down using mouse, sometimes the scene is like blinking with earlier
  scenes"*). The scene switches were gated on the film's live `readyState >= 2`, but Chromium drops
  `readyState` to 1 for the length of every seek, and scrubbing seeks on almost every frame. Each scroll step
  therefore swapped in the scene underneath for a frame: the closed-laptop hero over the lid-opening film,
  and the Connect film over *Beyond the keys*. A mouse wheel (big jumps, long seeks) made it worst. The
  fix: a `hasFrame(v)` latch (true once the film has painted a frame, reset only if it empties); the
  browser keeps that frame on screen while it seeks. `lidReady()` is latched the same way, so a dip
  can no longer clamp the scroll back to the closed lid. Measured in headless Chrome with 185 wheel steps
  through the story: 20–38 scene flips per pass before the fix, 0 after.
- **`source/pages/pages.py`: `.dark::before` / `.dark::after` → `section.dark::before` /
  `section.dark::after`** (2026-10-03). The drifting blurred glow meant for dark SECTIONS also hit every
  `btn dark` button; a button is `position: static`, so its 864 px blur(40px) blob escaped to the
  fixed nav / hero and drifted behind the frosted nav and the hero's glass card, which a GPU re-blurs
  every frame — the Partners hero "kept blinking" (About / Contact carry the same nav button).
  Rebuilt with `python3 pages.py laravel <out>` — the unpatched source reproduces the shipped Blade
  byte for byte, so the patch is the only difference. Send it upstream so the next package has it.
- **`source/pages/pages.py`: added `.frow>.ff+.ff{margin-top:0}`** right after the
  `.frow+.frow,.ff+.ff,… {margin-top:16px}` rule (2026-10-03). That stacking margin also hit the second
  field of every two-column `.frow`, so on desktop the right-hand field (Contact person, Phone, Company
  website, Licence; Contact's form too) sat 16 px below its neighbour, and on phones the row's own
  16 px gap doubled. The row's `gap` already spaces its fields in both layouts.

- **How it works — `/how-it-works`** (`cinematic/how.blade.php`, hand-written, 2026-10-04; linked from every top
  menu's "How it works": `market-switch.blade.php` rewrites the cinematic pages' `#how` link; `pages.py` and
  `CinematicNav.vue` link to it directly). **It explains the member portal's SIX sidebar sections, in sidebar
  order** — Dashboard, Wealth Planning, Analyze Property, Learning Hub, AI Coach, Landlord Management — because the
  owner rejected a five-"journey-step" version as made up ("you simply do, do not really go through the code").
  Each section = a pinned block with three beats on the left and a flat product window on the right whose content
  changes per beat; light aurora film, standard top menu, stat strip, one dark band, closing CTA. Rejected looks:
  black background, floating pill menu, tilting window / rainbow glow / bouncing cards, "cropped fragment" cards.
  - **Every claim was taken from a code review of the section** (controllers, Vue pages, services), not from the
    labels. Things that review corrected and the page must keep right: the AI Chatbot is an educator and never
    rates a specific property; AI Debate = 5 models, 0–3 rebuttal rounds, 6 / 11 / 16 / 21 credits, then a final
    verdict; Investment Prompt = 66 plays (34 / 10 / 8 / 6 / 5 / 3); Investment Prompt, Vibe Coding and Grant
    Application are gated to the "AI Active Blueprint" membership (`feature_gates`); a unit appears under My
    Properties only from a sales booking and its stages are set by the team; Tenancies / Rent / Property care are
    "Coming soon"; the Antserv standards carry the disclaimer "not a PropertyLab guarantee"; Wealth Planning's bank
    rules are DSR 70%, rent recognised 80%, margin 90% then 70% from the 3rd, seasoning 12 months; Learning Hub =
    7 road stops, 7 courses, 8 glossary chapters / 64 terms, 中文 / English.
  - **Not shown, on purpose:** the portal's Analyze Property tools (New Project, Layout / Location Analysis) —
    admin-only while `ANALYZE_PROPERTY_LOCKED` is on; that section shows the public project page going from
    padlocked to unlocked instead (the owner locks tabs to drive sign-in). Also Area Guide and the AI Advisor
    research tab (locked), Vibe Coding's community boards (off).
  - **Example figures are the portal's own demo member "Ryan"** (`utils/road/cast.js`, shown live in Learning Hub →
    How to Use → Wealth Planning: 1 / 1 approved, market value RM 552,500, loan RM 449,865, instalment RM 1,992,
    cash RM 63,481, rent RM 3,200, target RM 6,000 a month by 45). Net equity / cash flow / gap on the page are
    derived from those. Project figures are Binastra Cochrane Type A.
  - **The windows show the REAL portal screens (owner, 2026-10-05: "some screenshots are not even actual
    screenshots from code and database").** The hand-drawn panels (`.step.gs`, `#evidence`) are gone. Each of the
    six sections is now a `.step.rs` block: three beats on the left; on the right a browser window (a phone frame
    below 900px) holding a **static copy of the real portal page** in an iframe, as the demo member Ryan
    (`member@propertylab.test`) sees it. Per beat, a "camera" (CSS transform) glides and zooms onto the card the
    beat talks about and rings it.
    - **Copies:** `public/cinematic/how/` — `s/{screen}_{d|m}.html` (1440px / 390px), `a/` images + fonts, `css/`,
      and `manifest.json` (each copy's size and the rectangle of every highlighted card). Made by
      [`tools/how-it-works/capture.py`](/tools/how-it-works/capture.py) (Playwright + Chrome; signs Ryan in with a
      one-time magic link; `snaplib.py` strips every script, token and link). Its `SCREENS` table lists each
      screen's URL, tabs to click and the phrases whose card is highlighted. **Re-run it after a portal screen
      changes** (`--only dash_d,dash_m` for some); the Blade view reads `manifest.json` at render time, so no
      page edit is needed.
    - **Section copy and beats:** [`tools/how-it-works/build_page.py`](/tools/how-it-works/build_page.py)'s
      `SECTIONS` table, which rewrites the six sections + their CSS/JS inside `how.blade.php`. Edit there and re-run,
      not in the Blade file.
    - **Ryan's demo data on wk** (written through the real repositories, 2026-10-05): his wealth plan, one bought
      unit at Maxim Risen (B-12-08) with its seven stages, a Property Concierge request (find a tenant from
      7 Dec 2026), and one genuine AI Coach conversation. The captures depend on them; the uuids are in
      `capture.py`. Analyze Property shows the public project page (Binastra Cochrane), not the locked member tools.
    - A capture-side trap: `innerText` follows CSS `text-transform`, so phrase matching is lower-cased; a chat
      opens scrolled to its last message, so inner scroll boxes are reset to the top before measuring.

- **"Analyze Property" opens the market's New Projects page under the same top menu** (owner, 2026-10-04).
  - Cinematic pages: `market-switch.blade.php` replaces the menu's `#evidence` links with `/{market}/new-projects`
    (a clean copy of each link, because the page's own script binds a scroll handler on them).
  - Partners / About / Contact (no market in the URL): `pages.py` links to **`GET /analyze`**
    (`main.site.analyze`, `CinematicSiteController@analyze`) → the remembered market's New Projects, `my` by
    default. `analyze` is a reserved funnel slug.
  - The Inertia public pages: where `site.cinematic_home` is on, `SetPublicSite` shares `site.cinematicMarkets`
    and [`SiteLayout`](/resources/js/Layouts/SiteLayout.vue) renders
    [`Components/Site/CinematicNav.vue`](/resources/js/Components/Site/CinematicNav.vue) instead of its own header
    (not on `overlay` pages). It mirrors the cinematic nav and keeps the language + currency picker, since those
    pages show prices. Its phone menu is Teleported to `body` (the header's backdrop-filter clips `fixed`).
    Other deployments are unchanged. Needs a client build (`scripts/live-build.sh`).

- **The Cyberport CCMF and REACH Middle East credential cards are hidden** (owner, 2026-10-04, "hide … 1st" —
  for now). `data-cred="cyberport"` / `data-cred="reach"` carry `hidden style="display:none"` on the home
  page (edited in `home.blade.php`, then `apply_hk.py` / `apply_ae.py` re-run) and on About (`pages.py`);
  the remaining Malaysia card is centred with an inline grid style on its container. To bring them back,
  remove those two attributes and the container's inline style. ⚠️ The text above the cards still says
  "Proven in three markets … Malaysia, Hong Kong and the UAE have each … chosen to support it" — the
  owner has been asked whether to reword it.

- **About hero: three diagonal city panels, click one to open its story** (`pages.py` `about_body`, `.hero.tri`;
  owner, 2026-10-04, fifth round — earlier rounds: one merged skyline picture "confusing"; a pinned scroll story
  with films; "weird to show aurora", "titles stay the same"; films "still blur" → 4K stills; then "show all three
  side by side … diagonal, click the landmark and the country details show up"). One 100vh hero: Kuala Lumpur /
  Hong Kong / Dubai as three skewed panels (stacked slanted bands on phones), each with its landmark, name and a
  "View details" pill; the headline sits centred above. Clicking (or Enter) a panel grows it to almost full width —
  the other two stay as dark slivers you can click — and shows that market's eyebrow, headline ("It began in Kuala
  Lumpur." / "Then came Hong Kong." / "And now, Dubai."), two sentences and "← All three markets" (also Esc).
  No scroll pinning any more. **Light theme (owner, 2026-10-04):** the night skylines did not match the white,
  pastel-aurora cinematic page the chooser opens, so both the chooser and this hero use DAYLIGHT stills in that
  palette, with navy text on white glass — COMMITTED `public/main/images/about/day-{kl,hk,ae}-4k.jpg` (native
  3840×2160, GPT Image 2 at 4K, jobs `e97d0e3c` / `65703a2d` / `a6558309`) and `-2k.jpg`, via `image-set`.
  `--bp` plus the per-slot `.tri-bg` insets centre each landmark in its slice; the hero carries class `light`
  so the page script keeps the nav in its light state. ⚠️ Headlines and descriptions are drafted copy — owner to confirm.
  `ab-hero.webp` is no longer used by About.

- **Top menu: links sit left beside the logo; only Log in / Sign up is on the right** (owner, 2026-10-04).
  Inner pages: `pages.py` (`.links{margin-left:8px}`, `.nav-in>.btn{margin-left:auto}`, no wrapping, language
  toggle hidden under 1100 px). Cinematic pages: two rules at the top of
  `cinematic/partials/market-switch.blade.php` (the generated pages are not edited).

- **Partners → "Custom AI for partners" (`#ai`) redesigned at the owner's request** (2026-10-03: *"this
  section uiux is not first class"*). Was: a narrow column (headline broken over four lines, a plain
  dot list) beside a small floating image, and no call to action. Now: a header row (headline on
  two lines | lead + **Talk to us about custom AI**), then a full-height image carrying an illustrative
  AI-assistant chat card beside the four features as a 2×2 grid of cards with the page's gradient
  icon tiles (icon-beside-text rows on phones). Copy unchanged; the CTA jumps to `#apply` and the
  shared JS pre-ticks *Yes, tell me more* (`[data-ai-yes]`).
  ⚠️ **Trap, package-wide:** the reveal script adds class `in` to every `.rv` on scroll — the same
  name as the `.in` container (auto side margins + 56 px side padding). A `.rv` grid item therefore
  shrinks to fit its content once revealed (the image box collapsed to 114 px) and every revealed
  block gains 56 px side padding. The redesign resets it for its own elements only; the other
  sections were approved with the side effect baked in, so they are left alone.

- **Films: the consultant's T-shirt logo re-baked** (2026-10-03, *"the propertylab logo at the shirt got
  error"*). The logo is painted into the film pixels by the package's per-frame `logobake3.py`, which
  jumped it between both sides of her chest, printed it over her hair and left the magenta chest
  marker showing. Re-done with tracking in [`logo-rebake/`](/docs/modules_handbook/main/cinematic-site/logo-rebake/README.md)
  and shipped as **`beyond-a-v3.mp4`** and **`connect-film-v5.mp4`** (2026-10-04: logo pinned to her neckline, centred, in every shot of the film; v2, 2026-10-03, smeared blurry
  squares over hair and skyline — see the folder's README; originals and older versions removed). The generated Blade
  and `public/cinematic/index.html` reference the new names; **`source/build.py` still says
  `beyond-a.mp4` / `connect-film.mp4`** — rename them there after any rebuild.

- **About → Leadership: the founder's tile is his photo** (2026-10-03), not the "WK" initials.
  `lead_tile()` + `LEAD_PHOTOS` in `pages.py` — add a name → path there to give another leader a photo.
  The image lives in the COMMITTED [`public/main/images/about/`](/public/main/images/about/) (256×256
  webp, cropped from the owner's portrait), not `public/cinematic/` (the package's own asset folder).

### Open before a real launch (from the package's HANDOFF)

- Placeholder links: `#login`, `#app-store`, `#google-play`, footer `#learn` / `#help` / `#press` /
  `#privacy` / `#terms` / `#pdpa`, the four social icons; the EN / 中文 toggle is visual only.
- Claims to confirm: the Cyberport and REACH credential cards, leadership names, the KL address, the
  "reply within one business day" promise; illustrative figures baked into the films.
- Test 01 fails on the shipped page: it has two `<h1>` (the laptop mock-up's "Analyze Property" title
  and the real headline) — fix in `source/template.html`.

## Related files

**Backend**
- [routes/main.php](/routes/main.php) — the flag-gated block at the top (every route above, declared
  before the `/{slug}` and `/{funnel}/{slot}` catch-alls).
- [config/site.php](/config/site.php) — `cinematic_home` (`SITE_CINEMATIC_HOME`).
- [app/Http/Middleware/ForceFullPageVisit.php](/app/Http/Middleware/ForceFullPageVisit.php) —
  registered as `full-page` in [app/Http/Kernel.php](/app/Http/Kernel.php).
- [app/Http/Controllers/EnquiryController.php](/app/Http/Controllers/EnquiryController.php) — as shipped.
- [app/Http/Requests/Manage/Events/Funnels/StoreRequest.php](/app/Http/Requests/Manage/Events/Funnels/StoreRequest.php) — reserved slugs.

**Frontend (generated — do not edit)**
- [resources/views/cinematic/home.blade.php](/resources/views/cinematic/home.blade.php)
- [resources/views/pages/partners.blade.php](/resources/views/pages/partners.blade.php),
  [about.blade.php](/resources/views/pages/about.blade.php),
  [contact.blade.php](/resources/views/pages/contact.blade.php)
- `public/cinematic/index.html` (static copy of the home, for QA) + `public/cinematic/assets/`
- [public/.htaccess](/public/.htaccess) — the asset cache rule.
- [pages.py.patch](/docs/modules_handbook/main/cinematic-site/pages.py.patch) and
  [template.html.patch](/docs/modules_handbook/main/cinematic-site/template.html.patch) — our changes to the package source (see *Local patches*).
- [logo-rebake/](/docs/modules_handbook/main/cinematic-site/logo-rebake/README.md) — the T-shirt logo re-bake scripts and the exact runs.

**Migrations**
- [2026_10_03_000001_create_enquiry_tables.php](/database/migrations/2026_10_03_000001_create_enquiry_tables.php) — `partner_applications`, `enquiries` (as shipped).

Related: the earlier candidate design [Company](/docs/modules_handbook/main/company/readMe.md)
(`/{country}/company`, Inertia, still unlinked) and the Inertia home it replaced at `/` on wk
([Landing & Lead Capture](/docs/modules_handbook/main/landing-lead-capture/readMe.md)).

**Company details on About / Contact (owner-supplied, 2026-10-05)** — in `pages.py`, About's "Where we are"
cards and Contact's side column: Kuala Lumpur HQ = PropertyLab Technology Sdn Bhd (1547842-D), 13-16, V Office 1,
Lingkaran Sunway Velocity, Sunway Velocity, 55100 Kuala Lumpur; Hong Kong = PropertyLab Global Limited, Unit
2904-05, 29/F, Universal Trade Centre, 3 Arbuthnot Road, Central. About also carries a founder contact card
(Cheng Wai Kit, Founder & CEO, phone and email). The earlier "Exchange 106" address was a placeholder.

**One top menu on every public page (owner, 2026-10-05)** — full width (no centred max-width), logo in the left
corner, then the five links at **16px** (14.5px on narrow laptops), and the market / language control + Log in /
Sign up in the right corner. It is set in four places because there are four menus: `market-switch.blade.php`
(the three city pages), the end of `how.blade.php`'s `<style>`, `CSS +=` in `pages.py` (Partners / About /
Contact) and `CinematicNav.vue` (New Projects and the other Inertia pages). Change all four together.
"Get the app" buttons are hidden on the city pages (`a.cta[href="#app"]`) until the app is released; the footer's
"coming soon" store badges are left.

**The market / language control is on every public page, placed SERVER-SIDE (2026-10-05).**
`CinematicSiteController::withSwitcher()` renders `partials/market-switch.blade.php` and splits it into the page
before it is sent: its `<style>` into `<head>`, the control in place of the nav's static `<div class="lang">`, the
phone sheet + script before `</body>`. City pages go through `show()`; How it works, Partners, About and Contact
through `page()` (routes carry the view name as a `view` default — they are no longer `Route::view`).
`CinematicNav.vue` renders the same two-column dropdown for the Inertia pages, with the origin from the shared
`site.cinematicOrigin`. ⚠️ Do NOT go back to appending the partial at the end of the body and moving it with
script: every page then painted the old menu first and jumped to the new one (owner report). ⚠️ The control's
open state is the class **`mk-on`**, not `open`: `how.blade.php` already uses `.open{display:none}` for its
padlock demo, and the control vanished when clicked there.

**Chooser hover (2026-10-05)** — the photo pans right to left (`translate` on `.bg`, since its `transform` is
already animated) and soft CSS clouds (`.cl`, radial gradients) drift across the sky; desktop only. A generated
"film" per city (flowing aurora, sparkles) was tried first and rejected by the owner; its files are deleted. On
desktop the pictures start 17% down and fade in from the page colour, so the headline sits on clear sky.

**Chooser performance rules (owner, 2026-10-05: "not smooth when more than two things move", "sometimes I have
to click twice").** Kept in the last CSS block of `chooser.blade.php`:
- Pictures are WebP (`day-*-2k.webp` ~210 KB, `-4k.webp` ~520 KB, JPEG fallback) and preloaded; the entrance
  animations are paused until all three are decoded (`body.go`, 2.5 s cap).
- Only the hovered city moves. No idle drift, no light sweep, no `filter` (blur / saturate) or `mask` on the big
  picture layers, no `backdrop-filter` — the other two cities just fade to 50%.
- The pan and clouds reset instantly when the cursor leaves (transition only on the `:hover` rule).
- A city opens on **`pointerdown`**: the panel lifts and tilts under the cursor, so a click's press and release
  could land on different elements and the first click was lost. `.dets.lock` blocks the form for 500 ms so the
  same press cannot also pick an option.

## Website forms — email, CEO list, WhatsApp (owner, 2026-10-05)

*"Whatever form is submitted, always email it to waikit@propertylab.tech; the CEO page must show who submitted;
the system also sends a WhatsApp."* The two forms are the Partners application and the Contact enquiry.

- `EnquiryController` stores the row, then calls `App\Actions\Site\AnnounceWebsiteForm`: a
  `WebsiteFormSubmittedMail` to `config('site.enquiry.email')` (cc `site.enquiry.cc`, Reply-To = the visitor) and
  the queued `App\Jobs\Site\SendWebsiteFormWhatsApp`. Neither can fail the visitor's submission — failures are
  logged (`Website form email failed.` / `Website form WhatsApp alert skipped…`).
- `App\Support\WebsiteForm` is the one reading of both tables (JSON `payload` → labelled fields) used by the
  email, the WhatsApp text and the list.
- **CEO → Website Forms** (`/manage/ceo/website-forms`, `Manage\Ceo\WebsiteFormsController`,
  `Pages/Manage/Ceo/WebsiteForms.vue`, gate `view-ceo-dashboard`): newest first, filter by form, search, open a
  row for every field, Email / WhatsApp / Call the sender, download a Careers CV.
- **WhatsApp** goes to `site.enquiry.whatsapp_to` from the first CONNECTED inbox line that is not Cloud API
  (or `SITE_ENQUIRY_WHATSAPP_CHANNEL`): a plain text can only be started from a bridge line; a Cloud API line
  would need an approved template. Never from a CEO line.
- Config: `site.enquiry.*` (`SITE_ENQUIRY_EMAIL`, `_CC`, `_WHATSAPP`, `_WHATSAPP_CHANNEL`) — run
  `php artisan config:cache` after changing; `horizon:terminate` after changing the job.
- ⚠️ **State on wk, 2026-10-05:** the live test stored and listed correctly, but (1) every email from this box
  fails — the SMTP provider answers `421 Domain mg.propertylab.com.my is not allowed to send: Disabled for
  sending spam` (an account matter, affects all site email), and (2) no bridge line is connected (only the Cloud
  API line is), so the WhatsApp alert was skipped. Both start working without code changes once fixed.
- Test: `tests/Feature/Main/WebsiteFormAnnounceTest.php` (not run — no phpunit on this box).

**Chooser pictures were re-graded (2026-10-05, owner: "all three photos are very light")** — the `day-*` files are
the GPT Image originals with a mid-tone curve (gamma 1.22, +8% contrast) and +28% saturation baked in (not a CSS
`filter`, which the performance rules above forbid), served with `?v=2`; the white washes over them were cut back
to a short fade behind the city name and under the headline. The About hero uses the same files.

**About — lower half rebuilt (owner, 2026-10-05: "still not world class", plus the team photo).** In `pages.py`
(`about_body` + `about_css`, classes `ab-*`):
- **Milestones** — the page's one dark band: a horizontal timeline whose line draws itself and whose six points
  rise in turn (vertical on phones).
- **Recognition** — heading left, one card per body right (MDEC, Cradle; the hidden Cyberport / REACH cards keep
  their `data-cred` + `hidden`), press line under the heading.
- **Our team** — the owner's team photo (`public/main/images/about/team.webp` + `.jpg`, 2200 px, from his
  `/file/6c284818…` upload). Its studio backdrop is `#faf8f7`, so the section uses that colour and the figure has
  an inset shadow of it — the photo has no visible edge. The four leaders sit under it as name cards.
- **Our offices** ("Visit us.", 2026-10-05) — Kuala Lumpur and Hong Kong only (the Dubai card was removed: there
  is no office there), each with an embedded Google map (`google.com/maps?q=…&output=embed`, no API key, lazy)
  and an "Open in Google Maps" link. It replaced "Where we are", which was a card per market with a skyline picture; the founder's phone / email card that
  was added earlier the same day was removed at his request.
- **Call to action** — a light aurora card (the page no longer ends on a second dark band).
- ⚠️ **`.rv` elements get the class `in` when revealed, and `.in` is ALSO the page's container class**
  (`max-width:1200px;margin:0 auto;padding:0 56px`). A revealed grid item therefore shrinks and gains side padding
  unless it sets its own — the office pictures sat 56px inside their cards until `.ab-lc.in{padding:0;margin:0}`.
- ⚠️ `market-switch.blade.php`'s language span is `.mk-lg`, not `.lg`: About's `.lg` is the logo tile, and the
  menu button showed a white box there.

**About → team photo is interactive (2026-10-05).** Pointing at (or tapping / tabbing to) a person fades the
others and shows a card with their name, role and one line. Pure CSS: one transparent `<button class="tm">` per
person over the photo, `.ab-team-ph:has(.tm:nth-of-type(n):hover){--cx:…}` moves the spotlight. The people are
the `TEAM` list in `pages.py`, left to right: (centre x %, top of head %, name, role, line). The names and roles were given by the owner, left to right (2026-10-05); the dots sit under each person's feet
and the separate four-leader row under the photo was removed at his request. Numbered reference for future edits:
`public/main/images/about/team-guide.jpg`. Never guess who is who from the photo.

**About hero = a cinematic scroll that carries the milestones (owner, 2026-10-05).** The three-panel click hero and
the separate dark Milestones band are gone; `.cs` in `pages.py` is one pinned scene over 520vh with five
chapters — intro, Kuala Lumpur 2024 (3 milestones), "Recognised at home" 2025–26 (2), Hong Kong (1), Dubai (today).
`ABOUT_CS_JS` sets `data-c` (chapter) and `--p` (progress) on scroll; the city pictures cross-fade and push in,
and a rail at the bottom fills. Copy is deliberately one line per milestone. (The earlier `.tri-*` CSS is unused.)

**Phones — one menu everywhere (owner, 2026-10-05).** Every cinematic page (city pages, How it works, Partners,
About, Contact) hides its own burger and sheet (`#menuBtn` / `#burger` / `#sheet`); `market-switch.blade.php`
supplies `#plMb` + `#plSheet`, placed in the header server-side by `withSwitcher()`. Same items as the desktop
menu in the same order (Home, About, How it works, Analyze Property, For Partners, Contact us — "Contact us" was added to
every menu on 2026-10-05: `withSwitcher()` appends it to the cinematic pages' desktop row), then Market
chips, Language (current + "Change" → chooser), and Log in / Sign up pinned at the bottom. `CinematicNav.vue`'s
phone sheet matches it. The desktop links switch to the phone header at 980px on every page (it was 860 on some,
980 on others), and the phone header is 60px with a 26px logo everywhere. How it works' own button had stopped
opening its sheet; it is replaced like the others.
- `withSwitcher()` also rewrites the shipped `#login` placeholders to the real `/login`.
- Home, phone: "Get started" is a small glass pill under the hero text; the bottom bar (`#mActions`) is hidden.
- About, phone: Our Story stacks; Our Team keeps the group photo and adds a swipeable row of portraits
  (`public/main/images/about/team/1-9.webp`, 960×1200, in the `TEAM` order of `pages.py`). Since 2026-10-05 they are
  the owner's individual studio photos (`/file/c3f76e59-…`, a zip of nine full-length PNGs), cropped head to knee at
  4:5; before that they were cut from the group photo. Their `src` carries `?v=2` so phones drop the old crops.
