// page-contact.jsx const CCSS = ` .page .ct-grid{ padding:60px 56px 100px; display:grid; grid-template-columns:1.35fr 1fr; gap:64px; align-items:start; } .page .ct-grid h2{ font-family:'Instrument Serif',serif; font-style:italic; font-size:48px; margin:0 0 18px; } .page .ct-grid p{ font-size:17px; margin:0 0 18px; max-width:480px; } /* ESTIMATOR */ .page .est{ background:var(--paper); padding:0; border-radius:10px; border:1px solid var(--rule); overflow:hidden; position:relative; } .page .est .est-tape{ position:absolute; top:-14px; left:40px; width:90px; height:28px; background:rgba(217,170,87,.55); transform:rotate(-3deg); box-shadow:0 2px 6px rgba(0,0,0,.06); z-index:3; } .page .est .est-head{ padding:32px 40px 24px; border-bottom:1px dashed var(--rule); display:flex; justify-content:space-between; align-items:flex-end; gap:24px; } .page .est .est-head h3{ font-family:'Instrument Serif',serif; font-style:italic; font-size:32px; margin:0 0 6px; } .page .est .est-head .sub{ font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:var(--ink-soft); } .page .est .est-total{ text-align:right; } .page .est .est-total .lab{ font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.18em; text-transform:uppercase; color:var(--ink-soft); margin-bottom:4px; } .page .est .est-total .num{ font-family:'Instrument Serif',serif; font-size:44px; font-weight:500; color:var(--terra); transition:transform .25s; display:inline-block; } .page .est .est-total .num.bump{ transform:scale(1.08); } .page .est .est-total .rng{ font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--ink-soft); margin-top:2px; } .page .est .est-body{ padding:8px 40px 0; } .page .est .grp{ padding:24px 0; border-bottom:1px dashed var(--rule); } .page .est .grp:last-child{ border-bottom:none; } .page .est .grp-head{ display:flex; align-items:baseline; justify-content:space-between; margin-bottom:14px; gap:16px; } .page .est .grp-head .num-tag{ font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.18em; color:var(--ink-soft); margin-right:10px; } .page .est .grp-head h4{ font-family:'Instrument Serif',serif; font-style:italic; font-size:22px; margin:0; flex:1; } .page .est .grp-head .hint{ font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--ink-soft); } /* radio cards (project type) */ .page .est .opts{ display:grid; grid-template-columns:repeat(3,1fr); gap:10px; } .page .est .opt{ position:relative; padding:14px 14px 16px; border:1px solid var(--rule); border-radius:6px; background:var(--bg); cursor:pointer; transition:all .15s; } .page .est .opt:hover{ border-color:var(--ink-soft); } .page .est .opt.on{ border-color:var(--terra); border-width:2px; padding:13px 13px 15px; background:color-mix(in srgb, var(--terra) 6%, var(--bg)); } .page .est .opt .ot{ font-family:'Instrument Serif',serif; font-style:italic; font-size:17px; margin:0 0 4px; } .page .est .opt .od{ font-size:11px; color:var(--ink-soft); margin:0 0 8px; line-height:1.4; } .page .est .opt .op{ font-family:'JetBrains Mono',monospace; font-size:13px; color:var(--ink); font-weight:500; } .page .est .opt .check{ position:absolute; top:8px; right:8px; width:14px; height:14px; border-radius:50%; border:1.5px solid var(--rule); background:var(--bg); } .page .est .opt.on .check{ border-color:var(--terra); background:var(--terra); box-shadow:inset 0 0 0 3px var(--bg); } /* checkboxes (add-ons) */ .page .est .addons{ display:grid; grid-template-columns:1fr 1fr; gap:10px; } .page .est .addon{ display:flex; align-items:flex-start; gap:12px; padding:12px 14px; border:1px solid var(--rule); border-radius:6px; background:var(--bg); cursor:pointer; transition:all .15s; } .page .est .addon:hover{ border-color:var(--ink-soft); } .page .est .addon.on{ border-color:var(--ink); background:color-mix(in srgb, var(--terra) 5%, var(--bg)); } .page .est .addon .box{ width:16px; height:16px; border:1.5px solid var(--rule); border-radius:3px; flex:none; margin-top:2px; display:flex; align-items:center; justify-content:center; transition:all .15s; } .page .est .addon.on .box{ background:var(--ink); border-color:var(--ink); } .page .est .addon.on .box::after{ content:''; width:8px; height:5px; border-left:2px solid var(--bg); border-bottom:2px solid var(--bg); transform:rotate(-45deg) translate(1px,-1px); } .page .est .addon .at{ font-size:14px; color:var(--ink); display:block; line-height:1.3; } .page .est .addon .ad{ font-size:11px; color:var(--ink-soft); margin-top:2px; line-height:1.35; } .page .est .addon .ap{ font-family:'JetBrains Mono',monospace; font-size:11px; color:var(--ink-soft); margin-left:auto; flex:none; align-self:flex-start; padding-top:2px; } /* counter (pages) */ .page .est .ctr{ display:flex; align-items:center; gap:18px; padding:14px 18px; border:1px solid var(--rule); border-radius:6px; background:var(--bg); } .page .est .ctr .ctr-l{ flex:1; } .page .est .ctr .ctr-l .ct{ font-size:14px; } .page .est .ctr .ctr-l .cd{ font-size:11px; color:var(--ink-soft); margin-top:2px; } .page .est .ctr .ctr-c{ display:flex; align-items:center; gap:14px; } .page .est .ctr button{ width:32px; height:32px; border-radius:50%; border:1px solid var(--ink); background:var(--bg); font-size:18px; line-height:1; cursor:pointer; color:var(--ink); padding:0; transition:all .15s; } .page .est .ctr button:hover{ background:var(--ink); color:var(--bg); } .page .est .ctr button:disabled{ opacity:.3; cursor:not-allowed; } .page .est .ctr .ctr-n{ font-family:'Instrument Serif',serif; font-size:24px; min-width:24px; text-align:center; } /* segmented (timeline) */ .page .est .seg{ display:grid; grid-template-columns:repeat(3,1fr); gap:8px; } .page .est .seg .sg{ padding:12px 10px; border:1px solid var(--rule); border-radius:6px; background:var(--bg); cursor:pointer; text-align:center; transition:all .15s; } .page .est .seg .sg:hover{ border-color:var(--ink-soft); } .page .est .seg .sg.on{ border-color:var(--ink); background:var(--ink); color:var(--bg); } .page .est .seg .sg .sgt{ font-size:13px; } .page .est .seg .sg .sgp{ font-family:'JetBrains Mono',monospace; font-size:10px; opacity:.7; margin-top:2px; } /* contact small form below */ .page .est .est-foot{ background:color-mix(in srgb, var(--terra) 6%, var(--paper)); padding:28px 40px 32px; border-top:1px dashed var(--rule); } .page .est .est-foot label{ display:block; font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:var(--ink-soft); margin:0 0 8px; } .page .est .est-foot input, .page .est .est-foot textarea{ width:100%; padding:12px 14px; font-family:'Manrope',sans-serif; font-size:15px; border:1px solid var(--rule); border-radius:6px; background:var(--bg); color:var(--ink); margin-bottom:16px; box-sizing:border-box; } .page .est .est-foot textarea{ min-height:90px; resize:vertical; } .page .est .est-foot .row{ display:grid; grid-template-columns:1fr 1fr; gap:14px; } .page .est .send-btn{ display:inline-flex; align-items:center; gap:10px; padding:14px 28px; background:var(--ink); color:var(--bg); border:none; border-radius:999px; font-family:'Manrope',sans-serif; font-size:14px; font-weight:600; letter-spacing:.04em; cursor:pointer; transition:transform .15s; } .page .est .send-btn:hover{ transform:translateY(-1px); } /* contact info column */ .page .ct-info .it{ padding:22px 0; border-top:1px solid var(--rule); } .page .ct-info .it:last-child{ border-bottom:1px solid var(--rule); } .page .ct-info .l{ font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:var(--ink-soft); margin-bottom:6px; } .page .ct-info .v{ font-family:'Instrument Serif',serif; font-style:italic; font-size:24px; } .page .ct-info .v a{ color:inherit; text-decoration:underline; text-underline-offset:4px; } .page .ct-info .est-card{ background:var(--paper); border:1px solid var(--rule); border-radius:8px; padding:24px; margin-top:32px; position:relative; } .page .ct-info .est-card .lab{ font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.18em; text-transform:uppercase; color:var(--ink-soft); margin-bottom:8px; } .page .ct-info .est-card .lns{ font-size:13px; line-height:1.7; } .page .ct-info .est-card .lns .ln{ display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px dotted var(--rule); } .page .ct-info .est-card .lns .ln:last-child{ border-bottom:none; } .page .ct-info .est-card .lns .ln .lp{ font-family:'JetBrains Mono',monospace; color:var(--ink-soft); } .page .ct-info .est-card .lns .ln.tot{ font-family:'Instrument Serif',serif; font-style:italic; font-size:18px; padding-top:10px; margin-top:6px; border-top:1px solid var(--ink); border-bottom:none; } .page .ct-info .est-card .lns .ln.tot .lp{ font-family:'Instrument Serif',serif; font-style:normal; color:var(--terra); font-size:22px; } .page .ct-info .empty{ font-family:'Caveat',cursive; color:var(--ink-soft); font-size:18px; padding:8px 0; } @media (max-width:980px){ .page .ct-grid{ grid-template-columns:1fr; gap:40px; padding:40px 24px 60px; } .page .est .opts, .page .est .addons, .page .est .seg{ grid-template-columns:1fr; } } `; const PROJECT_TYPES = [ { id:'oneweek', t:'One-Week Site', d:'Squarespace, fixed scope, fast.', p:1800 }, { id:'custom', t:'Custom Build', d:'Designed + coded for you.', p:5400 }, { id:'brand', t:'Brand & Site', d:'Identity + custom website.', p:9500 }, ]; const ADDONS = [ { id:'cms', t:'CMS / editable content', d:'You update text & images yourself.', p:600 }, { id:'blog', t:'Journal / blog', d:'Posts, tags, RSS.', p:800 }, { id:'shop_s', t:'Small shop (≤10 items)', d:'Stripe checkout, simple catalog.', p:900, group:'shop' }, { id:'shop_m', t:'Shop (10–50 items)', d:'Categories, search, variants.', p:1800, group:'shop' }, { id:'shop_l', t:'Shop (50+ items)', d:'Full e-com, inventory.', p:3200, group:'shop' }, { id:'multi', t:'Multilingual', d:'EN + 1 additional language.', p:900 }, { id:'anim', t:'Custom animations', d:'Hover states, scroll moments.', p:700 }, { id:'illus', t:'Custom illustrations', d:'2–4 spot illustrations.', p:1200 }, { id:'copy', t:'Copywriting help', d:'Editing pass on your draft.', p:600 }, { id:'photo', t:'Photo direction', d:'Mood-board + shot list for shoot.', p:500 }, { id:'a11y', t:'Accessibility audit', d:'WCAG AA review + fixes.', p:600 }, { id:'seo', t:'SEO setup', d:'Meta, schema, redirects, sitemap.', p:400 }, ]; const TIMELINES = [ { id:'flex', t:'Flexible', d:'8+ weeks', mult:1.0 }, { id:'norm', t:'Standard', d:'5–8 weeks', mult:1.0 }, { id:'rush', t:'Rush', d:'≤ 4 weeks', mult:1.18 }, ]; function fmt(n){ return '$' + Math.round(n).toLocaleString('en-US'); } function ContactPage(){ const [type, setType] = React.useState('custom'); const [pages, setPages] = React.useState(6); const [addons, setAddons] = React.useState(['cms']); const [timeline, setTimeline] = React.useState('norm'); const [sent, setSent] = React.useState(false); const [sending, setSending] = React.useState(false); const [bump, setBump] = React.useState(0); const [errors, setErrors] = React.useState({}); const nameRef = React.useRef(); const emailRef = React.useRef(); const msgRef = React.useRef(); function validate(){ const e = {}; if (!nameRef.current?.value.trim()) e.name = 'Name is required'; const email = emailRef.current?.value.trim() || ''; if (!email) e.email = 'Email is required'; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) e.email = 'Please enter a valid email'; setErrors(e); return Object.keys(e).length === 0; } async function handleSubmit(e){ e.preventDefault(); if (!validate()) return; setSending(true); const name = nameRef.current?.value || ''; const email = emailRef.current?.value || ''; const message = msgRef.current?.value || ''; const estimateStr = fmt(total); // Send via PHP mailer try { await fetch('send.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, email: email, estimate: estimateStr, project_type: proj.t, message: message || '(no message)', }) }); } catch(err){ console.warn('Send failed:', err); } setSending(false); setSent(true); } const proj = PROJECT_TYPES.find(p => p.id === type) || PROJECT_TYPES[1]; const includedPages = type === 'oneweek' ? 4 : type === 'custom' ? 6 : 8; const extraPages = Math.max(0, pages - includedPages); const pagesCost = extraPages * 250; const addonsCost = addons.reduce((s, id) => { const a = ADDONS.find(x => x.id === id); return s + (a ? a.p : 0); }, 0); const subtotal = proj.p + pagesCost + addonsCost; const tl = TIMELINES.find(t => t.id === timeline) || TIMELINES[1]; const rushCost = subtotal * (tl.mult - 1); const total = subtotal + rushCost; React.useEffect(() => { setBump(b => b + 1); }, [type, pages, addons, timeline]); const isBumping = bump > 0; // shop is single-select within group function toggleAddon(a){ setAddons(prev => { const has = prev.includes(a.id); if (has) return prev.filter(x => x !== a.id); let next = [...prev, a.id]; if (a.group){ next = next.filter(id => { const o = ADDONS.find(x => x.id === id); return o && (id === a.id || o.group !== a.group); }); } return next; }); } return (
Contact · Booking June 2026

Tell me what
you’re building.

Use the estimator below to get a rough number — it’s not a quote, just a friendly ballpark. Real proposals come after we talk.

replies in ~1 day ✨ no pitch
decks here ✦

Project estimator

~ a friendly ballpark, not a quote ~
Estimated total
{fmt(total)}
~ {fmt(total * 0.9)} – {fmt(total * 1.15)}
{/* 1. Project type */}

01What are we making?

Pick one
{PROJECT_TYPES.map(p => (
setType(p.id)}>
{p.t}
{p.d}
{fmt(p.p)}
))}
{/* 2. Pages */}

02How many pages?

{includedPages} included · +$250 each
{pages} {pages === 1 ? 'page' : 'pages'}
{extraPages > 0 ? `${extraPages} extra × $250 = ${fmt(pagesCost)}` : `Within the ${includedPages} included`}
{pages}
{/* 3. Add-ons */}

03Anything extra?

Pick any · shop options are mutually exclusive
{ADDONS.map(a => (
toggleAddon(a)}>
{a.t}
{a.d}
+{fmt(a.p)}
))}
{/* 4. Timeline */}

04How urgent?

Rush adds ~18%
{TIMELINES.map(t => (
setTimeline(t.id)}>
{t.t} · {t.d}
{t.mult > 1 ? `+${Math.round((t.mult-1)*100)}%` : 'no rush fee'}
))}
{/* contact form */} {sent ? (
thanks! ✨

Got it — your estimate of {fmt(total)} is on its way. I’ll be in touch within a day.

) : (
errors.name && setErrors(p => ({...p, name: null}))}/> {errors.name &&
{errors.name}
}
errors.email && setErrors(p => ({...p, email: null}))}/> {errors.email &&
{errors.email}
}