// phone-value.jsx — "What would the same phone time cost as a person?" // Companion section to PricingSection. Shows OpenLines monthly cost side-by-side // with the fully-burdened cost of a person doing the same phone-handling hours. // // Same fonts + PALETTE + brutalist controls as pricing.jsx; uses the live // /api/public/landing-config so margin + vendor cost stay in sync with // pricing.yaml (no fallback drift). // // Intent: anchor the OpenLines price against a familiar P&L line item — hourly // labor — without claiming to "replace" anyone. The lede is honest about that. (function () { const { PALETTE, SectionLabel } = 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`; // Burdened-cost multiplier — captures payroll taxes (~8%), workers' comp, // sick/PTO (~10%), and modest after-hours premium. Industry conventional // wisdom is "1.25× to 1.4× the wage" — we go with 1.3. const BURDENED_MULT = 1.3; // Plan presets correspond to common quote points; bucket is computed live // from pricing.yaml so the comparison stays accurate if margin changes. const PLAN_OPTIONS = [ { value: 80, label: "Light", sub: "~5 hr/mo of calls" }, { value: 120, label: "Standard", sub: "~9 hr/mo of calls" }, { value: 200, label: "Busy", sub: "~15+ hr/mo of calls" }, ]; // Compute bucket minutes from price + margin + overhead + vendor cost. // Identical formula to wizard_handlers_new_production._compute_tier_cap // and the operator-side pricing-ladder UI on the dashboard. function computeBucketMinutes({ price, marginPercent, sharedOverhead, vendorPerMin }) { if (!vendorPerMin || vendorPerMin <= 0) return 0; const margin = marginPercent / 100; const cap = (price * (1 - margin) - sharedOverhead) / vendorPerMin; return Math.max(0, Math.floor(cap)); } // Inject only the styles we need; reuse the slider class from pricing.jsx // (already injected by it) to avoid duplicate keyframes. if (typeof document !== "undefined" && !document.getElementById("v1-phonevalue-styles")) { const s = document.createElement("style"); s.id = "v1-phonevalue-styles"; s.textContent = ` .v1-phonevalue-savings-pos { color: ${PALETTE.accent}; } .v1-phonevalue-card { padding: 28px 32px; background: ${PALETTE.bg2}; border: 1px solid ${PALETTE.line}; } .v1-phonevalue-card-label { font-family: ${mono}; font-size: 10.5px; letter-spacing: 1.6px; text-transform: uppercase; font-weight: 600; color: ${PALETTE.sub}; margin-bottom: 14px; display: flex; align-items: center; gap: 10px; } .v1-phonevalue-card-num { font-family: ${serif}; font-size: 56px; line-height: 0.95; letter-spacing: -1.6px; font-weight: 500; color: ${PALETTE.ink}; font-variant-numeric: tabular-nums; margin-bottom: 6px; } .v1-phonevalue-card-meta { font-family: ${sans}; font-size: 13px; color: ${PALETTE.sub}; margin-top: 14px; line-height: 1.5; } .v1-phonevalue-divider { font-family: ${serif}; font-size: 22px; color: ${PALETTE.sub}; text-align: center; font-style: italic; padding: 2px 0; } /* Tablet breakpoint aligns to --bp-tablet-landscape (1024px) from foundation tokens */ @media (max-width: 1023px) { .v1-phonevalue { padding: 100px 32px 80px !important; } .v1-phonevalue h2 { font-size: 46px !important; letter-spacing: -1.2px !important; } .v1-phonevalue-grid { gap: 56px !important; } .v1-phonevalue-cards { padding-left: 40px !important; } } /* Mobile breakpoint aligns to --bp-mobile (700px) from foundation tokens */ @media (max-width: 700px) { .v1-phonevalue { padding: 64px 20px 64px !important; } .v1-phonevalue h2 { font-size: 36px !important; letter-spacing: -0.8px !important; } .v1-phonevalue > p { font-size: 16px !important; margin-bottom: 44px !important; } .v1-phonevalue-grid { grid-template-columns: 1fr !important; gap: 48px !important; } .v1-phonevalue-cards { padding: 32px 0 0 !important; border-left: none !important; } .v1-phonevalue-card-num { font-size: 42px !important; letter-spacing: -1.2px !important; } .v1-phonevalue-card { padding: 22px 22px !important; } } `; document.head.appendChild(s); } function PhoneTimeValue() { // Live config from API; will be null until first fetch resolves. const [cfg, setCfg] = React.useState(null); const [hourly, setHourly] = React.useState(20); // wage slider default const [planPrice, setPlanPrice] = React.useState(120); // plan toggle default const [includeBurden, setIncludeBurden] = React.useState(true); React.useEffect(() => { fetch("/api/public/landing-config") .then(r => r.ok ? r.json() : null) .then(data => { if (data) setCfg(data); }) .catch(() => { /* leave cfg null — render shows neutral state */ }); }, []); // Until the live config arrives, render the section with neutral // placeholders rather than wrong numbers from a stale fallback. const margin = cfg ? cfg.target_margin_percent : null; const overhead = cfg ? (cfg.marketing_calculator && cfg.marketing_calculator.fixed_overhead) : null; // The landing-config endpoint exposes margin + overage but not vendor cost. // We derive a "minutes per dollar of price" rate from the calculator constants // already exposed, so the comparison stays driven by pricing.yaml without // adding new fields to the public API. const perMinRate = cfg ? (cfg.marketing_calculator && cfg.marketing_calculator.per_min_rate) : null; // Recovered phone-handling minutes for the chosen plan, using the same // bucket math the operator-side wizard uses. We approximate vendor_per_min // via the published target_margin + overage rate (overage = vendor / (1 - margin)). let bucket = 0; if (cfg) { const overageRate = cfg.overage_rate_usd_per_min; // e.g. 0.20 const targetMargin = (cfg.target_margin_percent || 0) / 100; const vendorPerMin = overageRate * (1 - targetMargin); bucket = computeBucketMinutes({ price: planPrice, marginPercent: cfg.target_margin_percent, sharedOverhead: cfg.marketing_calculator?.fixed_overhead ?? 0, vendorPerMin, }); } const bucketHours = bucket / 60; const effectiveWage = includeBurden ? hourly * BURDENED_MULT : hourly; const employeeMonthly = bucketHours * effectiveWage; const delta = employeeMonthly - planPrice; const deltaPct = employeeMonthly > 0 ? Math.round((delta / employeeMonthly) * 100) : 0; const savings = delta > 0; // Mailto with the comparison the user is looking at — same pattern as // PricingSection so the inbound email already has context. const mailtoUrl = "mailto:lucas@getopenlines.com?subject=" + encodeURIComponent(`OpenLines value question — $${planPrice}/mo vs $${hourly}/hr`) + "&body=" + encodeURIComponent( `Hi Lucas,\n\n` + `I was on the OpenLines pricing page. For my situation:\n\n` + `· OpenLines plan I was looking at: $${planPrice}/mo (~${Math.round(bucketHours * 10) / 10} hr of phone time)\n` + `· Hourly rate I'd otherwise pay: $${hourly}/hr ${includeBurden ? "(burdened)" : "(base wage)"}\n` + `· Equivalent labor cost: ~$${Math.round(employeeMonthly)}/mo\n\n` + `Can we schedule a 15-min call to talk through whether this is a fit?\n\n` + `Thanks!` ); return (
Value comparison

What would the same phone time cost as a person?

Most of our customers aren't paying anyone to answer these calls today — they're just missing them. But if you were, here's the math: the same phone-handling time billed at a normal hourly wage. OpenLines is the lower line, and you don't pay for the hours nothing happens.

{/* === INPUTS === */}
{/* 1 — Hourly wage slider */}
setHourly(parseInt(e.target.value, 10))} className="v1-pricing-slider" aria-label="Hourly wage" />
${hourly}/hr

Slide to your local rate. Front-office hires in Oregon typically run $16–$25.

{/* 2 — Plan preset (segmented) */}
{PLAN_OPTIONS.map((opt) => { const active = planPrice === opt.value; return ( ); })}

Plans show typical price points. Your actual quote comes from a 15-min call.

{/* 3 — Burdened toggle */}
{/* === OUTPUT === */}
{/* OpenLines card */}
OpenLines — ${planPrice}/mo
${planPrice}
{cfg ? <>Includes about {Math.round(bucketHours * 10) / 10} hr of phone time per month — roughly {Math.floor(bucket / 2)} calls at a 2-min average. Overage runs ${cfg.overage_rate_usd_per_min}/min only when you exceed the bucket. : Loading live pricing…}
vs.
{/* Employee card */}
Hourly hire — ${hourly}/hr {includeBurden && (burdened ${effectiveWage.toFixed(2)})}
${cfg ? Math.round(employeeMonthly).toLocaleString() : "—"}
{cfg ? <>For the same {Math.round(bucketHours * 10) / 10} hours of phone-handling time at ${hourly}/hr {includeBurden ? <> with a 1.3× burden for taxes, benefits, and after-hours premium : null}. This counts only the minutes actually on the phone — you'd still pay them to be on call for the hours nothing happens. :  }
{/* Delta line */} {cfg && (
{savings ? "OpenLines saves" : "OpenLines costs more"}
${Math.abs(Math.round(delta)).toLocaleString()}/mo ({savings ? "" : "+"}{deltaPct}%)
)}

We're not here to replace your team. But when you're choosing between missing a call and paying for it, this is the math.

Talk through your numbers →
); } window.PhoneTimeValue = PhoneTimeValue; })();