// direction-a.jsx — "Apple product page" treatment
// Confident editorial typography, generous whitespace, chapter-style section titles
// with the period. Light-first with dark hero + dark Assist section to break rhythm.
const DA = {
bg: '#fafaf7',
bgAlt: '#f3f1ec',
ink: '#0a0a0b',
ink2: '#1a1a1c',
text: '#1a1a1c',
text2: '#5a5a60',
text3: '#6e6e74',
hairline: '#e0dcd0',
hairlineSoft: '#b8b1a0',
// Two AA-compliant tones, deliberately distinct (see docs/design/WERKD_VISUAL_LANGUAGE.md
// for the product's own light/dark brand oranges, e67e22/ee9147 — same idea, this
// marketing site's own tokens):
// - brand: for TEXT and button-fills on LIGHT backgrounds. Matches the actual
// product's current CTA/nav-accent color (#C4502A, sampled directly from the
// app). White-on-fill measures ~4.64:1 (passes AA). As direct text/icon color
// on this site's cream background it's ~4.44:1 — just under the 4.5 text
// threshold, but dramatically better than the original #F97316's ~2.7:1.
// - brandLight: for TEXT/icons on DARK backgrounds (hero, Why Werkd, Field Mode,
// demo modal, and any dark card/column nested in an otherwise-light section).
// Unchanged — the original vivid orange already passes ~6.9:1 against near-black.
// Button/badge FILLS with white text are safe with `brand` regardless of the
// surrounding page background (contrast there is fill-vs-its-own-text, not fill-vs-page).
brand: '#C4502A',
brandLight: '#F97316',
font: 'var(--font-display)',
};
window.DA = DA;
// Real production destinations (app.werkd.pro). Auth is Clerk; legal routes
// live on the app domain. External links open in a new tab.
const W_BASE = 'https://app.werkd.pro';
const WERKD_URLS = {
signIn: `${W_BASE}/sign-in`,
signUp: `${W_BASE}/sign-up`,
privacy: `${W_BASE}/privacy`,
terms: `${W_BASE}/terms`,
dpa: `${W_BASE}/dpa`,
security: `${W_BASE}/security`,
doNotSell: `${W_BASE}/do-not-sell`,
contact: 'mailto:contact@werkd.pro',
dmca: 'mailto:dmca@122ai.io',
};
window.WERKD_URLS = WERKD_URLS;
// Pricing — single source of truth lives in pricing-constants.js (loaded
// before this file). Falls back to today's values if that script is missing.
const P = window.WERKD_PRICING || { owner: 99, seat: 59, ownerAnnual: 999, seatAnnual: 599, trialDays: 14, annual: 1188 };
// ─── Responsive width model ──────────────────────────────────────────────
// The page was authored at a fixed 1280px logical width. The standalone home
// page sets window.__WERKD_RESPONSIVE = true and scales/relayouts by viewport.
// Inside the comparison canvas the flag is unset, so components always render
// the original 1280px desktop composition (the artboard handles its own size).
// vw >= 1280 → logical = vw (fluid desktop, scale 1)
// 760..1279 → logical = 1280 (desktop comp, scaled down to fit width)
// < 760 → logical = 430 (true mobile layout, scaled to device width)
function werkdLayout(vw) {
if (vw >= 1280) return { logical: vw, scale: 1 };
if (vw >= 760) return { logical: 1280, scale: vw / 1280 };
return { logical: 430, scale: vw / 430 };
}
function useWerkdWidth() {
const resp = () => !!window.__WERKD_RESPONSIVE;
const get = () => resp() ? werkdLayout(document.documentElement.clientWidth).logical : 1280;
const [w, setW] = React.useState(get);
React.useEffect(() => {
if (!resp()) return;
const on = () => setW(get());
window.addEventListener('resize', on);
on();
return () => window.removeEventListener('resize', on);
}, []);
return w;
}
// isMobile when logical collapses to the 430 mobile track.
function useIsMobile() { return useWerkdWidth() <= 430; }
window.werkdLayout = werkdLayout;
window.useWerkdWidth = useWerkdWidth;
window.useIsMobile = useIsMobile;
// Orange period only (wordmark style: "Werkd.")
// dark: true when the ADot sits on a dark background (hero, Why Werkd, Field
// Mode, demo modal, or a dark card/column nested in a light section) — uses
// DA.brandLight instead of DA.brand since a plain doesn't inherit its
// parent's color and needs to independently pick the AA-safe tone for its context.
function ADot({ dark }) {
return .;
}
// Orange last letter + orange period — used to brand-tail section headings.
// e.g. "Today." renders as "Toda" black + "y." orange.
function ATail({ letter, dark }) {
return {letter}.;
}
// Reveal on scroll — fades + lifts in when in viewport. Plays once.
function Reveal({ children, delay = 0, y = 28, as: As = 'div' }) {
const ref = React.useRef(null);
const reduced = window.usePrefersReducedMotion ? window.usePrefersReducedMotion() : false;
const [shown, setShown] = React.useState(false);
React.useEffect(() => {
if (reduced) { setShown(true); return; }
if (!ref.current) return;
const obs = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) { setShown(true); obs.disconnect(); }
}, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
obs.observe(ref.current);
return () => obs.disconnect();
}, [reduced]);
return (
{children}
);
}
window.ATail = ATail;
window.Reveal = Reveal;
// Number counter — counts up from 0 to `to` once visible. Ease-out so it
// decelerates onto the final value; raise `ease` for a longer, slower settle.
function Counter({ to, suffix = '', prefix = '', duration = 2200, startDelay = 0, ease = 3, easeFn = null, threshold = 0.4, reserve = true, format = (n) => n.toLocaleString() }) {
const [val, setVal] = React.useState(0);
const ref = React.useRef(null);
const startedRef = React.useRef(false);
const reduced = window.usePrefersReducedMotion ? window.usePrefersReducedMotion() : false;
React.useEffect(() => {
if (reduced) { setVal(to); return; } // reduced motion: show final value, no count-up
if (!ref.current) return;
const obs = new IntersectionObserver(([e]) => {
if (e.isIntersecting && !startedRef.current) {
startedRef.current = true;
const begin = () => {
const start = performance.now();
const tick = (now) => {
const t = Math.min(1, (now - start) / duration);
const eased = easeFn ? easeFn(t) : 1 - Math.pow(1 - t, ease);
setVal(eased * to);
if (t < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
};
if (startDelay > 0) setTimeout(begin, startDelay); else begin();
obs.disconnect();
}
}, { threshold });
obs.observe(ref.current);
return () => obs.disconnect();
}, [to, duration, startDelay, ease, easeFn, threshold, reduced]);
// Reserve the final value's width so the layout never reflows as digits and
// separators appear while counting (which otherwise nudges the whole card).
if (reserve) {
return (
{prefix}{format(to)}{suffix}{prefix}{format(Math.round(val))}{suffix}
);
}
return {prefix}{format(Math.round(val))}{suffix};
}
// Slide-in from a direction. Useful for sequenced cards/rows.
function Slide({ children, delay = 0, from = 'left', distance = 32 }) {
const ref = React.useRef(null);
const [shown, setShown] = React.useState(false);
React.useEffect(() => {
if (!ref.current) return;
const obs = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { setShown(true); obs.disconnect(); }
}, { threshold: 0.18 });
obs.observe(ref.current);
return () => obs.disconnect();
}, []);
const offX = from === 'left' ? -distance : from === 'right' ? distance : 0;
const offY = from === 'up' ? distance : from === 'down' ? -distance : 0;
return (
{children}
);
}
window.Counter = Counter;
window.Slide = Slide;
function APrimaryButton({ children, dark = false, big = false, onClick, href }) {
const Tag = href ? 'a' : 'button';
const linkProps = href ? { href, target: '_blank', rel: 'noopener noreferrer' } : {};
return (
{children}
);
}
function ASecondaryButton({ children, dark = false, big = false, onClick, href }) {
const Tag = href ? 'a' : 'button';
const linkProps = href ? { href, target: '_blank', rel: 'noopener noreferrer' } : {};
return (
{children}
);
}
// Top nav
const NAV_LINKS = [
['Why werkd', '#intelligence'], ['Today', '#today'], ['Ledger', '#ledger'],
['For Customers', '#customers'], ['Pricing', '#pricing'],
];
function ANav() {
const m = useIsMobile();
const [open, setOpen] = React.useState(false);
// Close the mobile menu if we leave the mobile track
React.useEffect(() => { if (!m) setOpen(false); }, [m]);
return (
);
}
// HERO
const SCRIM_DEFAULTS = /*EDITMODE-BEGIN*/{
"scrimTL": 0.82,
"scrimTR": 0.48,
"scrimBL": 0.95,
"scrimBR": 0.55,
"shadowOpacity": 0.45,
"shadowBlur": 18,
"eyebrowOpacity": 1,
"eyebrowBlur": 7,
"headlineGrayOpacity": 1,
"showPhone": false,
"textFullWidth": false
}/*EDITMODE-END*/;
function buildScrim(tl, tr, bl, br) {
const c = 'rgba(10,10,11,';
return [
`radial-gradient(ellipse 75% 75% at 0% 0%, ${c}${tl}) 0%, transparent 65%)`,
`radial-gradient(ellipse 75% 75% at 100% 0%, ${c}${tr}) 0%, transparent 65%)`,
`radial-gradient(ellipse 75% 75% at 0% 100%, ${c}${bl}) 0%, transparent 65%)`,
// Bottom-right is the most prominent corner: a larger, denser, softer pool
// that both hides the footage watermark and bleeds well into the video. Its
// solid-ish core (controlled by `br`) feathers all the way out — no hard edge.
`radial-gradient(ellipse 135% 120% at 100% 100%, ${c}${br}) 0%, ${c}${Math.min(br, 0.9) * br}) 26%, ${c}${0.42 * br}) 52%, transparent 82%)`,
`linear-gradient(90deg, ${c}0.55) 0%, ${c}0.10) 50%, ${c}0.18) 100%)`,
].join(', ');
}
function AHero() {
const m = useIsMobile();
const TweaksPanel = window.TweaksPanel;
const TweakSection = window.TweakSection;
const TweakSlider = window.TweakSlider;
const TweakToggle = window.TweakToggle;
const [t, setTweak] = window.useTweaks(SCRIM_DEFAULTS);
const scrim = buildScrim(t.scrimTL, t.scrimTR, t.scrimBL, t.scrimBR);
// Static hero still (replaced the video loop, Jul 2026). One JPEG under the
// same tweakable corner scrim. A one-time 5s settle-zoom (CSS, ends on its
// own) adds life without needing WCAG 2.2.2's pause control (motion ≤ 5s),
// and is disabled under prefers-reduced-motion.
const showStill = !m;
return (
{/* Dusk-driveway hero still, under the same tweakable corner scrim.
Subject (tech + van) sits at the right where the phone overlaps it —
the scrim keeps it a rim-lit silhouette so the phone stays dominant. */}
{showStill && (
)}
{/* Hero rail: identical to the sections below (1120, centered) on normal
laptop widths — but on very wide windows the left inset is capped at
10vw so the text stays anchored toward the photo's dark left edge
instead of drifting toward center (there's no phone balancing the
right side by default). marginLeft wins over the shorthand's auto. */}
{/* Eyebrow tag — category + audience */}
0 ? `0 1px ${t.eyebrowBlur}px rgba(0,0,0,0.6)` : 'none',
}}>
COMING SOON · THE AI BUSINESS APP FOR TRADES
{/* Two-column hero: copy left, phone right */}
{/* LEFT — Headline + sub + CTAs + chips. Capped at 692px — exactly
the width the column has when the phone (380px + 48px gap) is
showing on the 1120 rail — so the text sits identically whether
the phone is on or off. */}
You do the work.
werkd.{' '}
does the rest.
Book jobs that fit your route. Quote on the spot. Invoice before you leave. Close the day clean.
Built for solo trades and small crews who need less admin between jobs.
window.openEarlyAccess()}>Request early access window.dispatchEvent(new CustomEvent('werkd:open-demo'))}>See 90-sec demo
{/* Reassurance line below CTAs */}
No card. No contract. Pays for itself with one extra job.
{/* RIGHT — Phone, glowing + floating (toggleable in Tweaks to judge the bg photo) */}
{t.showPhone && (
Every job. Every dollar. Every customer waiting. One screen.
{/* Phone + side callouts */}
{/* Left callouts — the Next Up card, the hero of Today */}
{/* Phone center — spotlighted on the Next Up card */}
{/* glow concentrated on the upper third, where the Next Up card sits */}
{/* Right callouts — top-right money, the intelligence, the alerts (top→bottom to match the screen) */}
);
}
function Callout({ title, body, dir = 'left' }) {
return (
—
{title}
{body}
);
}
// LEDGER chapter (light) — was "Money" under the old Today/Work/Office/Money
// nav; that surface is now Ledger (Today/Desk/Ledger + ⊕), so the chapter
// name follows the product's actual current IA instead of the retired one.
function AMoney() {
const m = useIsMobile();
return (
QUOTE → PAID
Ledger
Quote at the truck. Invoice from the front porch. Send the invoice before you leave the driveway.
Your customer gets a clean web page — no app to install, no account to make.
Approve, pick a time, or pay with card or bank transfer through Stripe. Standard Stripe fees apply.
{/* Money capability strip */}
{[
['Quotes', 'AI-drafted line items, you review and send.'],
['Invoices', 'Auto-drafted when you mark a job done. One tap to send.'],
['Reminders', 'Polite, escalating payment reminders — sent automatically on the schedule you set.'],
['Books', 'Expenses, mileage, profit & loss — organized monthly for your accountant.'],
['Snap the receipt', 'AI reads the vendor, amount, and every line item. Total logs as an expense, syncs to QuickBooks automatically when connected.'],
['Price Book learns', 'Every part on every receipt is saved at cost — what you paid. Future quotes pull your real prices, not guesses.'],
].map(([t, d], i) => (
Snap a receipt on the job — werkd reads every line item. The total goes straight to your books and syncs to QuickBooks when connected. Every real part goes into your Price Book at cost — what you actually paid, no markup baked in.
Next time you build a quote for a similar job, your part prices are already there. Your Price Book gets smarter with every receipt — no guessing, no looking it up.
{[
'AI reads vendor, amount, and every line item',
'Junk lines (tax, fees, MISC) filtered out automatically',
'Parts saved at cost — markup applied at quote time',
'Total syncs to QuickBooks or Xero automatically — when connected',
].map((t, i) => (
Free for 14 days. No card. No contract. Cancel anytime.
window.openEarlyAccess()}>Request early access →Questions? Email us
{/* Footer */}
);
}
// CUSTOMERS chapter
function ACustomers() {
const m = useIsMobile();
return (
YOUR PEOPLE
Customers
Every job. Every conversation. Every dollar — attached to the person who paid it.
Werkd remembers, so you don't have to.
{/* CRM strip */}
{[
['Helps catch duplicates', 'Imports collapse obvious duplicates on conflict and flag the rest for review.'],
['Smart suggestions', 'Quarterly checkups, renewal nudges, slow-pay flags — surfaced for your approval.'],
['Text from the card', 'One tap opens your phone to text them — their number and history right there.'],
['Outstanding balances', 'See exactly what they owe you, right on the card.'],
].map(([t, d], i) => (
= 2 ? `1px solid ${DA.hairline}` : 'none',
}}>
{t}
{d}
))}
);
}
// FAQ
function FAQItem({ q, a, isOpen, onToggle, m }) {
const ref = React.useRef(null);
const [h, setH] = React.useState(0);
React.useLayoutEffect(() => {
const measure = () => { if (ref.current) setH(ref.current.scrollHeight); };
measure();
window.addEventListener('resize', measure);
return () => window.removeEventListener('resize', measure);
}, [a, m]);
return (
{a}
);
}
function AFAQ() {
const items = [
{
q: 'Do I need a separate scheduling app, CRM, invoicing tool, and bookkeeper?',
a: 'No — Today, Desk, Ledger, plus the ⊕ button to capture anything. That\'s the whole app: scheduling, CRM, quotes, invoices, and payments in one. The books stay organized as you go, which shrinks the cleanup a bookkeeper would normally charge for. For taxes, payroll, and accounting advice, keep your accounting pro — werkd makes their job easier, it doesn\'t do it.',
},
{
q: 'Does it work offline? My basement signal is garbage.',
a: 'Yes. Werkd keeps working offline — quotes, jobs, photos, and timers all keep going when you\'re in a crawl space or a dead zone. It syncs the moment you have signal again.',
},
{
q: 'Will my customers have to download an app?',
a: 'They start with a link — no account, no App Store. After opening it, they can add it to their home screen in one tap for push notifications and quick access.',
},
{
q: 'How fast is "Photo Diagnosis," really?',
a: 'Fast enough to draft a quote while you\'re still standing next to the unit. Werkd uses multiple frontier vision models to suggest a likely cause, propose materials and labor, and queue up a quote draft. You review the draft — nothing goes to the customer until you tap send. Use it as a second set of eyes, not a certified diagnosis.',
},
{
q: 'What about my QuickBooks, my Stripe, my existing customer list?',
a: 'Stripe is the payments engine — it\'s built in. Spreadsheet / Google Contacts / QuickBooks all import in one screen, with duplicate-merge. When connected, every invoice and expense syncs to QuickBooks or Xero automatically as you go.',
},
{
q: 'I run a 4-truck crew. Can my techs use it without seeing the money side?',
a: `Yes. Roles are baked in. Owners get the full app. Techs see Today — their jobs, their route, their daily log — at $${P.seat}/mo per tech. Dispatchers see the board. Nobody sees money unless you say so.`,
},
{
q: 'What trades does werkd work for?',
a: 'Plumbing, electrical, HVAC, roofing, landscaping, appliance repair, flooring, concrete, pool service, pest control, septic, and general contracting — 17 trades modeled on day one, each with its own rate card and job catalog. If your trade isn\'t on the list, werkd still works — you\'d start with a blank rate card and build it your way.',
},
{
q: 'What happens to my data if I cancel?',
a: 'It\'s yours. You can export your customers, invoices, quotes, and jobs from the app at any time — before or after you cancel. We don\'t hold your data hostage.',
},
{
q: 'What does it cost?',
a: `$${P.owner}/mo for you, with everything included — portal, voice, photo diagnosis, books, dispatch, the works. Extra hands (techs and other non-admin users) are $${P.seat}/mo each. Free for ${P.trialDays} days, no card. No feature add-ons, no contract. Prefer annual? $${P.ownerAnnual}/year for the owner and $${P.seatAnnual}/year per extra tech, saving $${P.annual - P.ownerAnnual} and $${P.seat * 12 - P.seatAnnual}/seat. The calculator above (move the sliders) shows what most trades net.`,
},
];
const m = useIsMobile();
const [open, setOpen] = React.useState(null);
return (
);
}
function AHowItWorks() {
const m = useIsMobile();
const steps = [
{ n: '01', t: 'Book & route the day', d: 'Jobs land on your calendar and slot into your route automatically — no double-booking, no backtracking across town.', icon: 'calendar' },
{ n: '02', t: 'Quote, work, invoice', d: 'Werkd drafts the quote on-site and the invoice when you mark the job done. You read it, tap approve, and send.', icon: 'file-text' },
{ n: '03', t: 'Get paid & close out', d: 'Customers pay through your branded portal. Expenses, mileage, and books update themselves — ready for your accountant.', icon: 'dollar' },
];
return (
HOW IT WORKS
The whole job, start to paid
Three steps, same as your day — werkd handles the booking, quoting, invoicing, and books in the background, so you can stay on the tools.
{steps.map((s, i) => (
{s.n}
{s.t}
{s.d}
))}
);
}
function AInlineCTA({ heading, sub }) {
const m = useIsMobile();
return (
{heading}
{sub &&
{sub}
}
window.openEarlyAccess()}>Request early access
);
}
function DirectionA() {
return (
Skip to main content
{window.ALoadedDayOne && }
{window.ALiveDemos && }
{window.DemoModal && }
{window.EarlyAccessModal && }