// pricing.jsx — "What would this cost?" mini-calculator section. // 3 inputs (calls/week slider, avg-length segmented, setup-help toggle) → // 2 outputs (monthly range + optional setup fee). Pure client-side compute, // brutalist controls that match the rest of the page kit-of-parts. (function () { const { PALETTE, SectionLabel, OLButton } = window; const serif = `"Iowan Old Style", "Hoefler Text", "Georgia", "Times New Roman", serif`; const sans = `"Inter", ui-sans-serif, system-ui, -apple-system, sans-serif`; const mono = `ui-monospace, SFMono-Regular, Menlo, monospace`; // --- Pricing constants — fetched live from /api/public/landing-config --- // SINGLE SOURCE OF TRUTH: pricing.yaml (defaults + marketing_calculator blocks). // Refactor 2026-05-18: was previously hardcoded here; would drift from pricing.yaml. // The fallback values below are used ONLY if the API fetch fails on first load — // they match the YAML defaults as of 2026-05-18, but stale fallbacks are fine // since the live values arrive within a few hundred ms of page load. const FALLBACK_CONFIG = { target_margin_percent: 60, overage_rate_usd_per_min: 0.20, setup_fee_usd: 149, trial_days: 7, marketing_calculator: { per_min_rate: 0.41, fixed_overhead: 8, weeks_per_month: 4.33, }, }; // React state holds the live config — populated by useEffect on mount. // Math below reads from state, so it re-runs whenever the config arrives. // --- Inject pricing-specific styles once --- if (typeof document !== "undefined" && !document.getElementById("v1-pricing-styles")) { const s = document.createElement("style"); s.id = "v1-pricing-styles"; s.textContent = ` /* Custom range slider — brutalist square thumb with oxblood offset shadow */ .v1-pricing-slider { -webkit-appearance: none; appearance: none; position: relative; width: 100%; height: 2px; background: ${PALETTE.line}; outline: none; margin: 0; padding: 0; cursor: pointer; } .v1-pricing-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 22px; height: 22px; background: ${PALETTE.ink}; border: none; cursor: grab; box-shadow: 3px 3px 0 0 ${PALETTE.accent}; transition: box-shadow .2s ease, transform .2s ease; } .v1-pricing-slider::-webkit-slider-thumb:hover { box-shadow: 2px 2px 0 0 ${PALETTE.accent}; transform: translate(1px, 1px); } .v1-pricing-slider::-webkit-slider-thumb:active { cursor: grabbing; } .v1-pricing-slider::-moz-range-thumb { width: 22px; height: 22px; background: ${PALETTE.ink}; border: none; cursor: grab; box-shadow: 3px 3px 0 0 ${PALETTE.accent}; } .v1-pricing-slider-fill { position: absolute; left: 0; top: 0; height: 2px; background: ${PALETTE.ink}; pointer-events: none; } /* Segmented brutalist buttons */ .v1-pricing-seg { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; } .v1-pricing-seg-2 { grid-template-columns: 1fr 1fr; } .v1-pricing-seg-btn { font-family: ${sans}; font-size: 13.5px; font-weight: 500; padding: 14px 14px; background: transparent; color: ${PALETTE.ink}; border: none; box-shadow: inset 0 0 0 1.5px ${PALETTE.hairline}; cursor: pointer; transition: all .2s ease; white-space: nowrap; text-align: left; } .v1-pricing-seg-btn:hover:not(.v1-pricing-seg-btn-active) { box-shadow: inset 0 0 0 1.5px ${PALETTE.ink}; } .v1-pricing-seg-btn-active { background: ${PALETTE.ink}; color: ${PALETTE.bg}; box-shadow: inset 0 0 0 1.5px ${PALETTE.ink}, 4px 4px 0 0 ${PALETTE.accent}; transform: translate(0, 0); } .v1-pricing-seg-btn-active:hover { box-shadow: inset 0 0 0 1.5px ${PALETTE.ink}, 2px 2px 0 0 ${PALETTE.accent}; transform: translate(2px, 2px); } /* Layout — mobile stacks inputs above output */ /* Tablet breakpoint aligns to --bp-tablet-landscape (1024px) from foundation tokens */ @media (max-width: 1023px) { .v1-pricing { padding: 100px 32px 80px !important; } .v1-pricing h2 { font-size: 46px !important; letter-spacing: -1.2px !important; } .v1-pricing-grid { gap: 56px !important; } .v1-pricing-output { padding-left: 40px !important; } } /* Mobile breakpoint aligns to --bp-mobile (700px) from foundation tokens */ @media (max-width: 700px) { .v1-pricing { padding: 64px 20px 64px !important; } .v1-pricing h2 { font-size: 36px !important; letter-spacing: -0.8px !important; } .v1-pricing > p { font-size: 16px !important; margin-bottom: 44px !important; } .v1-pricing-grid { grid-template-columns: 1fr !important; gap: 48px !important; } .v1-pricing-output { padding: 32px 0 0 !important; border-left: none !important; border-top: 1px solid ${PALETTE.line} !important; min-height: 0 !important; } .v1-pricing-output-num { font-size: 56px !important; letter-spacing: -1.4px !important; } .v1-pricing-seg-btn { font-size: 12.5px !important; padding: 12px 10px !important; } .v1-pricing-seg { gap: 6px !important; } } `; document.head.appendChild(s); } // --- Math (cfg = live config from /api/public/landing-config) --- function compute({ callsPerWeek, avgMin, setupHelp, cfg }) { const mc = cfg.marketing_calculator; const targetMargin = cfg.target_margin_percent / 100; const monthlyCalls = callsPerWeek * mc.weeks_per_month; const monthlyMinutes = monthlyCalls * avgMin; const monthlyCost = monthlyMinutes * mc.per_min_rate + mc.fixed_overhead; const suggestedPrice = monthlyCost / (1 - targetMargin); let low = Math.floor((suggestedPrice * 0.85) / 5) * 5; let high = Math.ceil((suggestedPrice * 1.15) / 5) * 5; low = Math.max(0, low); if (high <= low) high = low + 5; return { low, high, setup: setupHelp ? cfg.setup_fee_usd : 0 }; } function PricingSection({ buttonVariant = "brutal" }) { const [callsPerWeek, setCallsPerWeek] = React.useState(10); const [avgMin, setAvgMin] = React.useState(2); const [setupHelp, setSetupHelp] = React.useState(true); // Live config from API; falls back to hardcoded if fetch fails on first load. const [cfg, setCfg] = React.useState(FALLBACK_CONFIG); React.useEffect(() => { fetch("/api/public/landing-config") .then(r => r.ok ? r.json() : null) .then(data => { if (data) setCfg(data); }) .catch(() => { /* keep fallback */ }); }, []); const { low, high, setup } = compute({ callsPerWeek, avgMin, setupHelp, cfg }); const fillPct = (callsPerWeek / 100) * 100; // Build mailto with the user's current estimate baked into subject + body const lengthLabel = avgMin === 0.75 ? "Short (under 1 min)" : avgMin === 2 ? "Normal (1–3 min)" : "Long (3+ min)"; const mailtoUrl = "mailto:lucas@getopenlines.com?subject=" + encodeURIComponent(`OpenLines pricing question — $${low}–$${high}/mo estimate`) + "&body=" + encodeURIComponent( `Hi Lucas,\n\n` + `I'm interested in OpenLines. Based on the calculator on your site:\n\n` + `· About ${callsPerWeek} missed calls per week\n` + `· Avg call length: ${lengthLabel}\n` + `· Setup help: ${setupHelp ? "Yes — please handle the setup" : "No — I'll handle the call forwarding myself"}\n` + `· Estimated range: $${low}–$${high}/month${setup ? ` + $${setup} one-time setup` : ""}\n\n` + `I'd like to schedule a 15-min call to get an exact quote.\n\n` + `Thanks!` ); return (
Ballpark pricing

What would this cost?

Plug in a rough estimate of your missed-call volume to see a ballpark monthly price. The real number comes from a 15-minute call.

{/* === INPUTS === */}
{/* 1 — Slider */}
setCallsPerWeek(parseInt(e.target.value, 10))} className="v1-pricing-slider" aria-label="Calls per week" />
{callsPerWeek}
{/* 2 — Segmented (length) */}
{[ { value: 0.75, label: "Short", sub: "under 1 min" }, { value: 2, label: "Normal", sub: "1–3 min" }, { value: 4, label: "Long", sub: "3+ min" }, ].map((opt) => { const active = avgMin === opt.value; return ( ); })}
{/* 3 — Toggle (setup) */}
{/* === OUTPUT === */}
Ballpark estimate
${low} ${high}
per month
{setup ? (
One-time setup
${setup}
) : null}

Ballpark only. Your actual price depends on your call patterns and how custom we make the agent. We'll confirm the real number in a free 15-minute call — no surprises.

Get an exact quote
); } function PricingInput({ label, hint, htmlFor, children }) { return (
{children} {hint ? (

{hint}

) : null}
); } window.PricingSection = PricingSection; })();