Live site: https://madebymotionx.com · Founder: Chunyu (Charles) Dai
Two systems in one repository:
- A production marketing site — Next.js App Router, TypeScript, Tailwind v4, CSS-only animation (no animation library), deployed on Vercel. 46 TS/TSX modules, Playwright end-to-end coverage, no backend required for the contact flow.
- An agentic outreach engine (
outreach/) — 40 Python modules that research a prospect, draft a single cold email, and refuse to send it unless it clears a deterministic gate stack. 10 test modules, all passing.
It is built around the assumption that the language model will occasionally produce something confident and wrong, and that a human should never have to catch it manually:
preflight.pyis a hard gate, not a linter. Every draft must pass a central validator returningPASS/FAILplus failure codes — word-count and one-offer limits, URL canonicalisation, a medical/regulated-claim guard, an AI-and-literary-language gate, and a diagnosis-ratio rule that forces most of the email to be the idea rather than the critique.- Recipient-certainty engine. Addressing a person by first name on a shared
info@/support@mailbox is a failure condition unless the research packet justifies it. - Read-back integrity. After a draft is created, the stored plaintext is read back from Gmail and re-checked, because the transport layer can silently rewrite links.
- State-transition protection. The drafting path is structurally incapable of
writing
sent,contacted, orfollow_up. Sending is a separate, human decision. - Caps that cannot be argued with. Max drafts per run, one follow-up maximum, a 90-day same-domain rule, and a suppression list.
CRM records and credentials live outside the repository by design; the engine reads
them from ~/.motionx-outreach/ and the test suite runs entirely on fixtures.
Note on this repository. This is a curated public snapshot of a private working repository. All prospect identities in the outreach fixtures, runbooks and tests have been replaced with fictional companies on the reserved
.exampleTLD, so nothing here resolves to a real business or mailbox. Behaviour is unchanged.
A premium, cinematic website for MOTIONX, a motion-first creative studio: we create the product film and build the motion-first website, launch page, and campaign system where it lives. Built with Next.js (App Router), TypeScript, Tailwind CSS v4, and CSS-only animation (no animation library). Deployable to Vercel as-is.
The homepage is intentionally edited for a fast read: thesis → selected proof → connected system → services → pilot bridge → process → trust → inquiry. Each section has a clear primary hierarchy; deeper scope and concept notes live on the dedicated routes or in native disclosures. The desktop header and mobile menu keep three primary choices, while the footer keeps the complete site index.
Founder: Charles Dai · Availability: Available Worldwide Offer: Product Films · Motion-First Websites · Product Launch Pages · Campaign Content Systems
- Node.js 22 (pinned in
.nvmrc) and npm. ffprobe— required by the asset-integrity check and full quality gate.- Optional:
ffmpeg— needed only when regenerating videos, posters, or social cards.
This machine had no system Node/ffmpeg, so they were installed into an isolated conda environment named
motionx. Ifnodeisn't on yourPATH, activate it:conda activate motionx # or: export PATH="/opt/anaconda3/envs/motionx/bin:$PATH"A standard Node 22 install works equally well.
npm installnpm run dev -- -p 3939Then open http://localhost:3939.
export PATH="/opt/anaconda3/envs/motionx/bin:$PATH" # only if Node isn't already on PATH
cd ~/Desktop/MotionX
npm run dev -- -p 3939npm run build # optimized, statically-prerendered build
npm run start # serve the production build locallyOther checks:
npm run content:check # source-level honesty/concept/public-contact guard
npm run assets:check # video master/derivative/poster/OG asset guard
npm run seo:check # route metadata/schema sanity guard
npm run metadata:check # built canonical/social/JSON-LD guard
npm run privacy:check # built browser-artifact private-address guard
npm run browser:verify # browser screenshots + console/overflow/reduced-motion pass
npm run typecheck # tsc --noEmit
npm run lint # eslint (next/core-web-vitals + next/typescript)
npm run test:e2e # Playwright browser tests (functional, no pixel diffing)
npm run gate # content → assets → SEO → types/lint → build → metadata/privacy → e2enpm run test:e2e runs the end-to-end suite in e2e/ against a server on port
3939 — it auto-starts one (next dev) and reuses a running one. Browsers are
installed once with npx playwright install chromium webkit (cached outside the
repo). Coverage: routes/status (including /system-study), a three-link primary
desktop/mobile menu plus the complete five-link footer, canonical case-study links +
native disclosures, AURA principle controls and
no-JavaScript readability, pilot→contact preselection + paired totals, contact validation +
Gmail-compose-to-hello@ + no false "sent" state, normal vs pilot inquiry subjects,
concept-label/private-address integrity, copy-email, no horizontal overflow at
375/390/430, reduced-motion/Save-Data media behavior, and the hero poster→play
behaviour. Tests are functional only (no
screenshot diffing — portfolio video frames would make snapshots flaky). Point the
suite at any origin with PLAYWRIGHT_BASE_URL=… (e.g. a production next start, or
https://madebymotionx.com); set MOTIONX_NODE to override the Node path used to
start the dev server. The local preview server (.claude/launch.json) also runs
on port 3939.
⚠️ Don't runnpm run buildwhilenpm run devis running — the build rewrites.next/and can break the live dev server. Stop dev first, orrm -rf .nextafterwards.
npm run content:check scans the source for MOTIONX-specific public-trust rules:
concept labels/disclosures on every project, public hello@ contact, no private
backup email or founder direct address in source/built browser artifacts, no placeholder
social links, no unsupported proof phrases, visible concept-study notes, and no
unsupported medical/health claims. The full npm run gate runs it before asset
integrity, SEO sanity, typecheck, lint, build, built-output metadata/privacy checks,
and e2e.
npm run assets:check fingerprints the immutable root hf_2026…*.mp4 masters,
proves public videos are derivatives, validates exact video/audio stream contracts
with ffprobe, requires fast-start MP4 layout, parses social cards as genuine
1200×630 JPEGs, checks source-visible media references, and enforces local size
budgets. In a clean CI clone, only the absent gitignored masters are explicitly
skipped; every public-asset contract still runs. See docs/asset-integrity.md.
npm run seo:check verifies route metadata/schema basics, expected sitemap/robots
expressions, JSON-LD escaping, and absence of unsupported schema fields such as
address, phone, awards, reviews, ratings, employee count, and fake social profiles.
For visual or behavioral changes, run a local server on port 3939, then run
npm run browser:verify. It exercises every public route at 1440, 1280, 1024, 768,
430, 390, and 375 pixels; captures desktop/mobile and full-page evidence for
every public route under
test-results/browser-verify/; and writes a machine-readable evidence.json.
The pass checks console/page errors, horizontal overflow, responsive nav state,
reduced-motion and Save-Data poster-only behavior, normal/pilot contact subjects,
and explicit video playback.
.github/workflows/website-quality.yml always runs the same production gate on
pull requests and pushes to main using Node 22, ffprobe, Chromium, and WebKit.
It has read-only repository permission, uses no secrets, performs no deployment,
and uploads the HTML Playwright report and diagnostics when available.
The site does not assume a domain exists. The base URL comes from an environment variable with a localhost fallback:
NEXT_PUBLIC_SITE_URL (e.g. https://your-domain.com) → defaults to http://localhost:3000
It feeds metadataBase, canonical URLs, Open Graph URLs, JSON-LD, robots.txt,
and sitemap.xml (see src/content/site.ts → url).
- Local: nothing to do — it falls back to
http://localhost:3000. - Vercel: add
NEXT_PUBLIC_SITE_URLin Project → Settings → Environment Variables (e.g.https://motionx.vercel.appor your custom domain), then redeploy.
- Push this folder to a Git repo (GitHub/GitLab/Bitbucket).
- Import the repo at vercel.com/new — the Next.js preset is auto-detected; no config needed.
- (Recommended) set
NEXT_PUBLIC_SITE_URLto your deployed URL. Deploy.
public/
videos/ agentic-trading-hero.<hash>.mp4 ← silent, claim-light hero teaser
renewal-botanical.<hash>.mp4 · liquid-signal.<hash>.mp4 ·
furia.<hash>.mp4 · agentic-trading.<hash>.mp4
(web-ready case-study copies; the FURIA copy is cut with the
wordmark-removal step — see docs/asset-integrity.md)
*-mobile.<hash>.mp4 ← smaller ladder variants for ≤767px viewports
(re-encoded from the web copies; see docs/asset-integrity.md)
captions/ <name>.<hash>.en.vtt
posters/ <name>.<hash>.webp
images/ work/agentic-trading/strategy-board-{1920,960}.<hash>.webp
← responsive concept-art stills; exact source hashes/recipes are documented
og/ og-image.jpg · renewal.jpg · liquid-signal.jpg · furia.jpg · agentic-trading.jpg · system-study.jpg
src/
app/ layout.tsx · page.tsx · globals.css · robots.ts · sitemap.ts
error.tsx (branded runtime recovery)
system-study/ expanded AURA concept proof route
work/[slug]/ project detail pages (SSG)
components/ navigation, hero (server shell) + hero-reel (client island),
studio-intro, launch-system, selected-work + selected-work-edit-bay,
video-player, services, process, studio-section,
contact-section (server shell) + contact-form / copy-email-button
(client islands), contact-type-link, start-project-link, footer, …
content/ site.ts · contact.ts · interface.ts · projects.ts · services.ts ·
pilot.ts · system-study.ts
← EDIT CONTENT HERE
hooks/ use-in-view-video.ts · use-reduced-motion.ts ·
use-video-autoplay-policy.ts
lib/ utilities.ts (mail links, clipboard, contact scrolling + intent sync)
The original camera files are preserved untouched in the project root
(hf_2026…*.mp4). Everything under public/videos/ is a derivative. Nothing in
this project deletes, renames, or recompresses the originals.
src/content/site.ts → contact.email is the public address
(hello@madebymotionx.com, a Google Workspace alias). It drives the contact form
(Gmail compose + mailto), the copy buttons, the footer, and JSON-LD.
The founder's direct address and the personal backup Gmail address are
operational documentation only (CLAUDE.md) — they must never appear in src/,
because website source is serialized into public build artifacts. npm run content:check scans the source tree and npm run privacy:check scans the built
.next browser artifacts (the gate runs both).
All offer copy and prices live in src/content/pilot.ts. The /pilot page
(src/app/pilot/page.tsx) and the home-page PilotCallout both read from it —
edit prices in one place. Each offer's formType must match a contact-form
project-type option so a pilot CTA can pre-select it. This applies to base offers;
the Campaign Add-On is a referral-only scope with no fallback form type and never
appears in the general contact select. Pilot links are built by
getPilotInquiryHref as /?type=…&pilot=<offer-id>#contact; the explicit offer id
prevents general service links from accidentally inheriting pilot pricing. The
campaign add-on must first be paired with one base offer; its URL adds the single
marker addon=campaign-add-on, and the contact summary/Gmail brief preserve both
scopes. The selector derives the displayed base-plus-add-on total from the two
central price strings. Unknown, duplicate, mismatched, or standalone add-on markers
fail closed.
Status: MX → Google (smtp.google.com); DKIM → google._domainkey (active);
SPF → v=spf1 include:_spf.google.com ~all; DMARC → monitoring
(p=none, rua=mailto:charles@madebymotionx.com). To verify after changes:
dig +short TXT madebymotionx.com # SPF
dig +short TXT _dmarc.madebymotionx.com # DMARC
dig +short TXT google._domainkey.madebymotionx.com # DKIM
dig +short MX madebymotionx.com # Google MXThe hello@ alias exists (alternate address on the charles@ Workspace user).
The Gmail display name (Charles Dai | MOTIONX) and signature
(outreach/signature.html) are set in Gmail settings (not in code). Outreach
templates + the email-safe reel thumbnail live in outreach/.
All reusable text lives in src/content/site.ts (hero, studio statement,
studio bio, closing CTA, footer). Services, the Launch System, and the process
steps live in src/content/services.ts; project copy in src/content/projects.ts.
Inquiry option values live in src/content/contact.ts; add a new service CTA value
there first so the form, services, projects, and pilot offers stay type-safe.
Primary and footer navigation sets live in src/content/chrome.ts; keep the header
focused and the footer complete.
src/content/site.ts → social is an empty array on purpose, so no
placeholder/# profiles are ever shown. Add a real profile only when it exists:
social: [{ label: "Instagram", href: "https://instagram.com/yourhandle" }]
as ReadonlyArray<SocialLink>,The footer and contact section render the social row conditionally (hasSocial)
— while the array is empty the row is removed cleanly with no empty heading.
- Render the replacement outside the immutable public roots (for example,
/tmp/motionx-video.mp4) and review it there. - Compute its SHA-256, insert the first 12 hex characters before the extension,
and move it into
public/videos/. Never overwrite a published hashed URL. - Regenerate and hash its poster the same way (see below).
- For case studies, update the entry in
src/content/projects.ts(video,poster,width,height,orientation). For the hero, update the centralized paths insrc/content/media.ts. Update the contracts inscripts/asset-integrity.mjs, then runnpm run assets:check.
The hero uses a short, muted Agentic Trading concept teaser while the complete
narrated film remains in Selected Work. To rebuild it, use the exact claim-safe
timings and widescreen/mobile recipes in CLAUDE.md → "Hero teaser". Then
refresh its claim-safe poster from the operator-supplied still. This crop
deliberately excludes the source image's SCORE 0.84 label and baked timeline:
ffmpeg -y -i /Users/prince/Downloads/hero_final.png \
-vf "crop=2560:1440:600:0,scale=1920:1080:flags=lanczos" \
-frames:v 1 -c:v libwebp -quality 82 -compression_level 6 \
/tmp/agentic-trading-hero.webp
ffmpeg -y -i /Users/prince/Downloads/hero_final.png \
-vf "crop=978:1738:2300:0,scale=1080:1920:flags=lanczos" \
-frames:v 1 -c:v libwebp -quality 82 -compression_level 6 \
/tmp/agentic-trading-hero-mobile.webpffmpeg -y -ss 9 -i public/videos/liquid-signal.c24f676131a6.mp4 -frames:v 1 -c:v libwebp -quality 82 /tmp/liquid-signal.webp
ffmpeg -y -ss 13 -i public/videos/renewal-botanical.d503303f4c24.mp4 -frames:v 1 -c:v libwebp -quality 84 /tmp/renewal.webpThere is no server backend, and the UI never pretends a message was sent. The
section shell (src/components/contact-section.tsx) renders on the server; the
inquiry form itself (src/components/contact-form.tsx) is a focused client island
with a robust multi-path flow:
- Client-side validation — name, a valid email, and a description are required. Company, budget, and timeline remain optional and collapse behind one disclosure on mobile. Trusted service/pilot links preselect the project type; pilot links also show the selected package summary. A campaign add-on is valid only when paired with an exact base Pilot package, and both scopes remain visible in the summary and prepared brief.
- Continue in Gmail (primary) — builds a URL-encoded Gmail compose link
addressed to
contact.email(subjectMOTIONX Project Inquiry — …, all fields in the body) and opens it by clicking a transient real<a target="_blank" rel="noopener noreferrer">. It does not usewindow.open()— that can returnnulleven when the tab opens, which used to cause a false "Gmail didn't open" error. Only a thrown exception counts as failure. Status message: "Your project brief is ready in Gmail. Review it and press Send to complete your inquiry." — it never claims the message was sent. - "Having trouble?" helper — appears only after an attempt, presented as assistance (not an error), and is dismissible. It offers Try Gmail again, Copy email, Copy prepared message, and Open default mail app. The full brief is shown inline only on a genuine caught error. The helper resets on each new submit and on refresh (it's plain component state — the initial state is clean).
- Copy email address — copies
contact.emailand shows a live-region confirmation (Clipboard API with a timeout + hidden-textarea fallback). - Open default mail app — a clearly-labelled
mailto:fallback available before the Gmail attempt and again in the helper. The email is also rendered as selectable text.
Every "Start a Project" CTA (hero + nav) only smooth-scrolls to the contact section and moves focus to its heading — it never opens email or submits the form, and it preserves query parameters such as attribution or project preselection.
The form is structured so you can add real delivery without exposing secrets.
In onSubmit (contact-form.tsx), after buildMessage(), POST the fields
instead of (or in addition to) opening Gmail:
- Formspree / Basin —
fetch("https://formspree.io/f/XXXX", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(fields) }). - Resend (recommended, own backend) — create
src/app/api/contact/route.ts(a Next.js Route Handler) or a Server Action that calls the Resend SDK withRESEND_API_KEYfrom a server-side env var (neverNEXT_PUBLIC_), thenfetch("/api/contact", …)from the form. Only show a success state once that request actually resolves200.
Keep the Gmail/mailto paths as graceful fallbacks.
- A bespoke Agentic Trading hero teaser uses three approved claim-light mascot/research/resolved beats, separate widescreen and vertical reframes, crossfades, no audio, and faststart delivery (2.22 MB / 0.89 MB). Numeric trade-plan frames and the fictional-product end card are excluded.
- Web-ready case-study copies sit in
public/videos/. Originals were not modified or recompressed. - Replaceable videos, posters, editorial images, and captions use content-hashed
URLs and a one-year immutable cache policy.
npm run assets:checkverifies both the filename hashes and the narrowly scopedvercel.jsonheaders; Open Graph cards keep their stable names and existing cache behavior. - Posters and editorial stills are deterministic WebP derivatives (video frames except the supplied-and-cropped Agentic hero/artwork sources). Social share cards are separate 1200×630 JPEG files for crawler compatibility.
- Aspect ratios are declared on every media frame to prevent layout shift.
- Keyboard-accessible nav, video controls, and Selected Work index with separate
preview buttons and canonical case-study links plus URL/history restoration.
Its mobile cue sheet uses one compact row per project; the desktop index remains
sticky beside one media stage. Case routes include keyboard-sized return links,
a compact anchor index, native disclosures for secondary metadata/notes, and a
concept-labelled visual Up Next path. AURA's three reasoning controls expose one
visual principle at a time with
aria-pressed. The mobile menu retains its focus trap, Escape-to-close, destination-heading focus, and body-scroll lock/restore. CTAs move focus to the contact heading. prefers-reduced-motionrespected throughout (no decorative autoplay; CSS entrances and progress motion collapse to the final state).- Videos use
playsInline, muted autoplay only, and pause when offscreen or when the tab is hidden. Portfolio clips begin poster-first withpreload="none"until preferences are known; normal visitors then get in-view autoplay and metadata preload, while reduced-motion and Save-Data visitors remain paused until they use the explicit play control. The hero teaser ispreload="none"with a poster-first first paint — the ~2.2 MB mp4 is never on the critical path and only loads (on play) after mount for visitors who aren't on a data-saver connection and don't prefer reduced motion; otherwise the poster is the static fallback. The hero teaser has no audio track, so it carries a play/pause control only. - Core marketing copy is server-visible without waiting for Motion hydration; the
E2E suite includes a JavaScript-disabled readability check. Motion is reserved for
meaningful media/process state (see
CLAUDE.md→ Animation conventions). - SEO & icons: title/description, Open Graph + Twitter cards, JSON-LD,
robots.txt,sitemap.xml(all usingNEXT_PUBLIC_SITE_URL), an SVG monogram favicon (src/app/icon.svg), plus a rasterizedfavicon.icoand 180×180apple-icon.pngfor legacy browsers and iOS home-screen.