# Company — the corporate story, one cinematic scroll (Main · Public site)

**Portal:** Main · **Route:** `GET /{country}/company` (`main.site.company`) → `Main\CompanyController@index` →
`Pages/Main/Site/Company.vue` · **Nav:** **none yet**. It is a candidate design, reached by URL only
(`/my/company`, `/hk/company`, `/ae/company`), and it emits `noindex`. When it replaces Home or
About, add the header tab, drop `->noindex()` in the controller, and update this line.
*(2026-10-03: the founder's separate [Cinematic site](/docs/modules_handbook/main/cinematic-site/readMe.md) package took `/` on wk instead; this page is unchanged.)*
**Gated by:** nothing, it is **PUBLIC** (`public.site`) · **Writes:** none.

## What it does

Tells a first-time visitor what PropertyLab is in one dark, animated scroll, in this order:

1. **Hero — "scan a city".** A generated night skyline per market (a hundred-odd towers in three depth
   rows, the market's signature tower near the centre), a giant ghost word behind it, a glossy ground
   and low haze. A beam in the market's colour sweeps the city every ~9 s: towers stay dark, the ones
   that pass light up along their edges. Left: *"Every new launch, checked five ways."*, that market's
   REAL project count, and the five check labels, which light one by one in step with the beam. Right:
   who we are (the About hero sentence). Bottom: one tab per market (10.5-second autoplay bar), the
   scroll cue, prev/next. Switching sinks one city below ground and raises the next, recoloured. The
   page opens on the visitor's own market.
   **Design rule (founder review, 2026-09-25):** nothing decorative moves — the v1 had one floating
   tower per market with orbiting chips and drifting crystals, and he ruled *"instead of one building
   per country, show the skyline; the flying cubes are meaningless."* If it is on the stage, it is
   something PropertyLab does: the city is the market, the beam is the screening, the lit towers are
   the ones that passed.
2. **Manifesto.** One sentence, lit word by word as it is scrolled through.
3. **Five checks.** A tall section whose content is pinned: scrolling runs the checks one at a time —
   the left side explains the current check, the right-hand card ticks each as it passes, and
   *"A project that fails them does not appear here…"* appears only after the fifth tick.
4. **What we do.** The three pillars, staggered in, each card lit by a pointer-following spotlight.
5. **Where we work.** One card per market: that market's night skyline (a still), the real count
   counting up, a link to that market's projects.
6. **Who pays whom.** The About page's approved wording, drawn as two money flows (member → membership
   → PropertyLab; developer → commission → Agency Partner).
7. **For agencies.** PETA (propertylab.tech) in one band, with two rows of example buyer signals
   drifting past.
8. **Close.** *"Start with the numbers."* → Talk to us (`/{country}/about#contact`), memberships,
   projects; the visitor's own market skyline rises beneath it.

## How it works

- **Data:** the controller sends one prop, `markets` — this hostname's `site.countries`, in site order,
  each with `projects` = `CatalogProject` count for that country (ONE grouped query). Every other word
  on the page is fixed copy.
- **The claims are worded once.** `WHAT` (the three pillars) and `CHECKS` (the five checks, each with a
  `short` form for the hero chips) live in `Pages/Main/Site/siteStory.js`, and **About reads the same
  file**. Change a check there and both pages change.
- **Per-market look** (`Partials/Company/looks.js`): signature tower shape, accent colour, ghost word,
  still skyline, one line — keyed by ISO2 (`MY` blue rounded tower, `HK` violet stepped slab, `AE` gold
  twisting spindle). A market added later with no entry still renders: it borrows a look by position and uses
  its own name as the ghost word.
- **All animation is motion-v** (Motion for Vue — the Vue port of Framer Motion). The page is wrapped
  in `<MotionConfig reduced-motion="user">`. Patterns used, so a later page can copy them:
  `whileInView` + `inViewOptions: { once: true }` for entrances; parent `variants` with
  `staggerChildren` for staggered reveals; `whileHover` / `whilePress` springs on every button and card;
  `useScroll({ target, offset })` + `useTransform` for scroll-linked motion (the manifesto words, the
  pinned checks, the hero's scroll-out); `AnimatePresence mode="wait"` for per-market
  text swaps; `animate()` for the count-up (`CountUp.vue`).
- **The 3D stage** (`Partials/Company/skylineStage.js`) is plain three.js, loaded with a dynamic
  `import()` so it is its own chunk and never runs during SSR. Everything behind the text is in ONE
  opaque WebGL scene — sky, stars, ghost word, city, ground, beam — because the neon glow is an
  `UnrealBloomPass`, and bloom only composites correctly over an opaque canvas. Every tower is GENERATED
  (a profile swept up the Y axis + a canvas-drawn window facade; three facades shared by all cities), so
  there are no model files and no real landmark. ~26 % of foreground towers are "passers" with a hidden
  neon outline the beam reveals. The stage reports the beam's progress (`onScan`, 0..1 or null while
  paused) and the hero maps it to the check index — five reactive updates a sweep, not sixty a second.
- **It stops when nobody is looking.** An `IntersectionObserver` and `visibilitychange` pause the render
  loop and the autoplay when the hero is off screen or the tab is hidden.
- **Fallbacks.** No WebGL, a stage that fails to build, or reduced motion → that market's still skyline
  (`public/main/images/company/skyline-*.webp`, full-bleed at the bottom) and a DOM ghost word; nothing
  autoplays and all five check labels show as done.
- **Stills are opaque, full-bleed images** — the v1 cut-out towers needed alpha and taught one trap
  worth keeping: never rely on `mix-blend-mode: screen` to hide a black ground. A blend mode only blends
  inside its stacking context, and every motion-v element that animates a transform starts one, so the
  black box comes back.
- **Header.** `SiteLayout` has an `overlay` prop for this page: the header is fixed and transparent in
  white type over the hero, turning to dark glass after 24px of scroll; the white logo is
  `/images/logo/propertylab-white@2x.png`; the footer loses its top margin. `onHome` also excludes
  `/company`, so Home is not lit on a page that has no tab.
- **Images** were generated for this page with Bloom (the PropertyLab brand, 2026-09-25): one night
  skyline per market, prompted as generic towers with a neon-edged signature tower in that market's
  colour. They are illustrations — not photographs of any real city, building or project — and they
  are separate renders from the generated 3D cities, sharing colour and mood, not geometry.

⚠️ **Scroll-linked OPACITY does not work through a template `:style` in motion-v 2.4.** A
`MotionValue` for `opacity` on a `motion.*` element renders once and never moves, while a transform
(`y`, `scaleX`) on the same element updates every frame (measured in headless Chrome, 2026-09-25: the
hero text's `y` followed the scroll and its opacity stayed `1`). Use `Partials/Company/bindStyle.js`:
opacity on a PLAIN wrapper, the transform on the `motion.*` element inside it. Never put both writers
on one element — motion-v's next render puts its stale opacity back. Entrance fades via
`initial` / `whileInView` are unaffected; this is only for values driven by `useScroll`.

⚠️ **Copy rules still apply.** Every figure is real (the catalogue count), the markets are the live
country list, and the PETA band shows example behaviours, never results — no number on it could be
mistaken for a client outcome. Keep it that way when editing.

⚠️ **After changing the route, run `php artisan route:cache`** — this box caches routes, and a new
route 404s (and `route:list` does not show it) until the cache is rebuilt.

## Related files

**Backend**
- `app/Http/Controllers/Main/CompanyController.php` — `index()` (SEO: canonical, `noindex`,
  Organization JSON-LD) + `markets()` (grouped catalogue count).

**Frontend**
- `resources/js/Pages/Main/Site/Company.vue` — the page; maps `markets` → `look` + formatted count.
- `resources/js/Pages/Main/Site/siteStory.js` — `WHAT` + `CHECKS`, shared with `About.vue`.
- `resources/js/Pages/Main/Site/Partials/Company/`
  - `HeroShowcase.vue` — the hero (stage mount, the five check labels driven by `onScan`, autoplay,
    market tabs, scroll-out).
  - `skylineStage.js` — the three.js scene (`createSkylineStage` → `setLook` / `setPointer` /
    `setScroll` / `setActive` / `dispose`; returns `null` without WebGL).
  - `looks.js` — per-market signature tower, accent, ghost word, skyline still, line.
  - `ScrollManifesto.vue`, `FiveChecks.vue`, `Pillars.vue`, `MarketsBand.vue`, `WhoPays.vue`,
    `ForAgencies.vue`, `ClosingCta.vue` — the sections, in page order.
  - `CountUp.vue` — a real number counting up once in view (screen readers get the final value).
  - `bindStyle.js` — writes a MotionValue onto a plain element's style (the opacity workaround above).
  - `looks.test.js` — an unknown market still gets a look, and never another market's line.
- `resources/js/Layouts/SiteLayout.vue` — the `overlay` header mode.
- `public/main/images/company/` — `skyline-my.46a97c76.webp`, `skyline-hk.cad7ce82.webp`,
  `skyline-ae.a6e6fdff.webp` — the suffix is the content hash, because Cloudflare caches `/main/images` for
  4 hours: a changed image keeps its old bytes on the same URL. Re-hash the name whenever the file changes.

**Dependencies**
- `motion-v` (package.json) — Motion for Vue. `three` was already a dependency.

**Routes**
- `routes/main.php` — `GET {country}/company` inside the country-prefixed public group, beside `about`.

See also: [About & Contact](/docs/modules_handbook/main/about/readMe.md) (shares `siteStory.js`).
