// final-sections.jsx — the five sections rebuilt in the 2026-08 pass.
//
// CallStory hero — one call, start to finish (replaces HeroStory)
// HowLine "four things happen" as a single drawn line
// FitsJourney "can I speak to someone?" — the handoff, beat by beat
// LanguagesGlobe English / Spanish / Russian and more, on a wire globe
// WhoMarquee who it's for, as two scrolling bands
//
// Design source of truth: prototypes/25-final-site.html. If you change the shape
// of a section here, change it there too — that file is what the operator reviews.
//
// WHY the hero animation is a loop of SEVEN calls and not one: a single scenario
// can only demonstrate one capability. The product's differentiators — reading the
// client's own website mid-call, connecting a caller to a human, REFUSING to answer
// what isn't published, and answering in the caller's language — need four
// different calls to show honestly. It auto-advances so a visitor who touches
// nothing still sees all of them.
//
// EVERY business in SCENARIOS is invented. None is a customer. The industries
// mirror the tmpl_* starter businesses (bakery, auto repair, trades, barber/salon,
// market/deli, roofing, law office) precisely so nothing here can be mistaken for
// a real client's line, and the .example TLD is reserved by RFC 2606 so no address
// can ever resolve to someone's real site.
(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`;
const svg = (p) =>
``;
const ICON = {
globe: svg(``),
search: svg(``),
check: svg(``),
script: svg(``),
memo: svg(``),
person: svg(``),
hook: svg(``),
nofind: svg(``),
hang: svg(``),
ring: svg(``),
phonein: svg(``),
speech: svg(``),
mail: svg(``),
dial: svg(``),
reroute: svg(``),
talking: svg(``),
};
const SCENARIOS = [
{
name: "Bakery · reads the price off their own site",
to: "orders@cedarstreetbakery.example",
subject: "Call summary — custom cake quote, callback wanted",
turns: [
{ who: "agent", text: "Thanks for calling Cedar Street Bakery. We're closed right now, but I can help — what do you need?" },
{ who: "caller", text: "Hi — what do you charge for a two-tier birthday cake?" },
{
who: "agent",
text: "A two-tier starts at $95, and custom lettering is $8 on top. They need three days' notice.",
pre: [
{ icon: "search", t: "Reading the website — the page that holds this answer", src: "cedarstreetbakery.example/custom-cakes/", working: true },
{ icon: "check", t: "Found it — two-tier from $95, lettering +$8, 3 days' notice" },
],
},
{ who: "caller", text: "Perfect. Can someone call me to nail down the design? Dana Whitfield, 503-555-0142." },
{
who: "agent",
text: "Got it — Dana Whitfield at 503-555-0142, two-tier birthday cake, wants to talk design. Someone will call you in the morning.",
acts: [{ icon: "memo", t: "Read the number back — 503-555-0142, confirmed" }],
},
{ who: "caller", text: "Great, thank you!" },
{ who: "agent", text: "Thank you for calling. Have a good night!", acts: [{ icon: "hang", t: "Call closed — caller said goodbye" }] },
],
mail: [
{ k: "What happened", v: "After-hours call about a custom cake. Priced it from the website, then took a callback request." },
{ k: "Caller", v: "Dana Whitfield · (503) 555-0142 — read back and confirmed" },
{ k: "Answered", v: ["Two-tier from $95 (read live from /custom-cakes/)", "Custom lettering +$8", "Three days' notice required"] },
{ k: "Needs you", v: "Call Dana in the morning to work out the design." },
],
},
{
name: "Auto repair · caller asks for a person, gets one",
to: "front@riverbendauto.example",
subject: "Call summary — transferred to Marco",
turns: [
{ who: "agent", text: "Riverbend Auto Repair, this is the front desk line. How can I help?" },
{ who: "caller", text: "My truck's making a grinding noise on the highway. Honestly I'd rather just talk to a mechanic — is anyone there?" },
{
who: "agent",
text: "Of course — let me connect you to Marco now. One moment.",
acts: [
{ icon: "person", t: "Caller asked for a person — transfer is on for this line" },
{ icon: "ring", t: "Ringing Marco's cell", src: "→ (503) 555-0177 · the number you named" },
],
},
{ who: "caller", text: "Appreciate it." },
{
who: "agent",
text: "Connecting you now — thanks for calling.",
acts: [
{ icon: "check", t: "Marco picked up — call handed over, agent stepped out" },
{ icon: "memo", t: "Summary flushed before the handoff — nothing after this point is ours to log" },
],
},
],
mail: [
{ k: "What happened", v: "Caller asked to speak to a person and was connected to Marco. Grinding noise on the highway." },
{ k: "Transferred to", v: "Marco · (503) 555-0177 — answered" },
{ k: "Answered", v: ["Handed the call over without a menu or hold music", "Summary sent before the transfer, so nothing was lost"] },
{ k: "Needs you", v: "Nothing — Marco has it. This email is just the paper trail." },
],
},
{
name: "Heating & air · won't guess what isn't published",
to: "dispatch@northgatehvac.example",
subject: "Call summary — no heat, urgent callback",
turns: [
{ who: "agent", text: "Thanks for calling Northgate Heating & Air — everyone's with a customer right now. I can help, or take a message." },
{ who: "caller", text: "My furnace won't kick on and it's 38 degrees. Do you do emergency calls tonight?" },
{
who: "agent",
text: "I don't want to promise something I can't confirm — I don't have tonight's emergency availability in front of me. Let me take your details and have a tech call you straight back.",
pre: [
{ icon: "search", t: "Reading the website — checking for an after-hours policy", src: "northgatehvac.example/services/", working: true },
{ icon: "nofind", t: "Not published anywhere — so it won't invent one" },
],
},
{ who: "caller", text: "Okay — Marisol Vega, 503-555-0184." },
{
who: "agent",
text: "Got it — Marisol Vega at 503-555-0184, furnace not igniting, no heat tonight. A tech will call you right back.",
acts: [
{ icon: "memo", t: "Remembered the callback — name and number, read back to confirm" },
{ icon: "person", t: "Flagged for a person — urgent, no heat overnight" },
{ icon: "hook", t: "Sent to their systems", src: "POST /openlines/call → their dispatch board" },
],
},
],
mail: [
{ k: "What happened", v: "No heat, furnace not igniting, 38°F overnight. Caller needs a callback tonight." },
{ k: "Caller", v: "Marisol Vega · (503) 555-0184 — read back and confirmed" },
{ k: "Answered", v: ["Explained a tech would call back", "Did not promise emergency availability — it isn't published anywhere"] },
{ k: "Needs you", v: "Call Marisol back tonight. No heat, freezing temperatures." },
],
},
{
name: "Roofing · caller switches to Spanish",
to: "office@ridgelineroofing.example",
subject: "Call summary — active leak, inspection requested (answered in Spanish)",
turns: [
{ who: "agent", text: "Thanks for calling Ridgeline Roofing. How can I help?" },
{
who: "caller",
text: "Hola — tengo una gotera en el techo desde la tormenta de anoche. ¿Pueden venir a verlo?",
flag: "es-MX",
acts: [{ icon: "globe", t: "Heard Spanish — switching to it for the rest of the call" }],
},
{
who: "agent",
text: "Claro que sí. Las inspecciones son gratuitas y normalmente se programan dentro de dos días hábiles. ¿Me da su nombre y un número?",
pre: [
{ icon: "search", t: "Reading the website — the inspection page", src: "ridgelineroofing.example/inspections/", working: true },
{ icon: "check", t: "Found it — free inspection, typically within 2 business days" },
],
},
{ who: "caller", text: "Sí — Andrés Peña, 971-555-0113. La gotera está sobre la cocina." },
{
who: "agent",
text: "Perfecto — Andrés Peña, 971-555-0113, gotera activa sobre la cocina. Alguien lo llamará hoy mismo para agendar la inspección.",
acts: [
{ icon: "memo", t: "Read the number back in Spanish — 971-555-0113, confirmed" },
{ icon: "person", t: "Flagged for a person — active leak, weather-related" },
],
},
{ who: "caller", text: "Muchas gracias." },
{ who: "agent", text: "A usted. Que tenga buen día.", acts: [{ icon: "hang", t: "Call closed — caller said goodbye" }] },
],
mail: [
{ k: "What happened", v: "Active roof leak over the kitchen after last night's storm. Caller wants an inspection. Whole call was in Spanish." },
{ k: "Language", v: "Spanish (es-MX) — detected automatically, no setup" },
{ k: "Caller", v: "Andrés Peña · (971) 555-0113 — read back and confirmed" },
{ k: "Answered", v: ["Inspections are free (read live from /inspections/)", "Usually scheduled within 2 business days"] },
{ k: "Needs you", v: "Call Andrés today to book the inspection. Active leak." },
],
},
{
name: "Barber shop · books the slot into their software",
to: "hello@maplerowbarbers.example",
subject: "Call summary — Saturday appointment requested",
turns: [
{ who: "agent", text: "Maple Row Barbers, thanks for calling. What can I do for you?" },
{ who: "caller", text: "Do you have anything Saturday morning? Two haircuts, me and my son." },
{
who: "agent",
text: "Saturday we're open eight to two, and walk-ins are welcome before ten. Let me take your details so the shop can confirm a time for two.",
pre: [{ icon: "script", t: "Already in the script — Saturday 8–2, walk-ins before 10. No lookup needed." }],
},
{ who: "caller", text: "Sounds good. Ray Okonkwo, 971-555-0168." },
{
who: "agent",
text: "Got it — Ray Okonkwo at 971-555-0168, two cuts Saturday morning. The shop will text you to confirm the time.",
acts: [
{ icon: "memo", t: "Read the number back — 971-555-0168, confirmed" },
{ icon: "hook", t: "Sent to their systems", src: "POST /openlines/call → their booking software, via webhook" },
],
},
{ who: "caller", text: "Thanks, see you then." },
{ who: "agent", text: "Thanks Ray — see you Saturday.", acts: [{ icon: "hang", t: "Call closed — caller said goodbye" }] },
],
mail: [
{ k: "What happened", v: "Booking request for two haircuts Saturday morning. Details pushed to the shop's booking software." },
{ k: "Caller", v: "Ray Okonkwo · (971) 555-0168 — read back and confirmed" },
{ k: "Answered", v: ["Saturday hours 8–2 (from your script)", "Walk-ins welcome before 10"] },
{ k: "Needs you", v: "Text Ray to confirm a Saturday slot for two." },
],
},
{
name: "Market & deli · stays in its lane, hands off the rest",
to: "frontdesk@fifthandmain.example",
subject: "Call summary — catering enquiry, needs a quote",
turns: [
{ who: "agent", text: "Fifth & Main Market, how can I help?" },
{ who: "caller", text: "Are you open on Sunday? And what's the soup today?" },
{
who: "agent",
text: "We're open Sunday nine to five. Today's soup is roasted tomato basil, and the sandwich special is the turkey club.",
pre: [
{ icon: "search", t: "Reading the website — today's board", src: "fifthandmain.example/daily-menu/", working: true },
{ icon: "check", t: "Found it — roasted tomato basil; turkey club special" },
],
},
{ who: "caller", text: "Great. One more thing — could you cater a 40-person lunch next month? What would that run?" },
{
who: "agent",
text: "Catering is something the owner prices personally, so I don't want to quote you a number. Let me take your details and have her get back to you with a real figure.",
acts: [{ icon: "nofind", t: "Out of scope on purpose — no catering price in the script or on the site" }],
},
{ who: "caller", text: "That's fine. Priya Raman, 503-555-0159." },
{
who: "agent",
text: "Thank you — Priya Raman at 503-555-0159, catering for forty next month. She'll call you back.",
acts: [
{ icon: "memo", t: "Read the number back — 503-555-0159, confirmed" },
{ icon: "person", t: "Flagged for a person — needs a real quote" },
],
},
],
mail: [
{ k: "What happened", v: "Caller asked about Sunday hours and the daily soup — both answered. Then asked for a catering quote, which was handed to you rather than guessed at." },
{ k: "Caller", v: "Priya Raman · (503) 555-0159 — read back and confirmed" },
{ k: "Answered", v: ["Sunday hours 9–5 (from your script)", "Today's soup + special (read live from /daily-menu/)", "Did not quote catering — that's yours to price"] },
{ k: "Needs you", v: "Call Priya back with a catering figure for 40 people." },
],
},
{
name: "Law office · caller speaks Russian",
to: "intake@harborstreetlaw.example",
subject: "Call summary — consultation enquiry (answered in Russian)",
turns: [
{ who: "agent", text: "Harbor Street Law, thanks for calling. How can I help?" },
{
who: "caller",
text: "Здравствуйте. Сколько стоит первая консультация?",
flag: "ru-RU",
acts: [{ icon: "globe", t: "Heard Russian — switching to it for the rest of the call" }],
},
{
who: "agent",
text: "Здравствуйте. Первая консультация — тридцать минут, бесплатно. Записаться можно на этой неделе.",
pre: [
{ icon: "search", t: "Reading the website — the consultations page", src: "harborstreetlaw.example/consultations/", working: true },
{ icon: "check", t: "Found it — first consultation free, 30 minutes" },
],
},
{ who: "caller", text: "Отлично. Меня зовут Ирина Соколова, телефон 971-555-0126." },
{
who: "agent",
text: "Записала — Ирина Соколова, 971-555-0126, первая консультация. С вами свяжутся сегодня.",
acts: [
{ icon: "memo", t: "Read the number back in Russian — 971-555-0126, confirmed" },
{ icon: "nofind", t: "No legal advice given — the script forbids it, in any language" },
],
},
{ who: "caller", text: "Спасибо большое." },
{ who: "agent", text: "Всего доброго.", acts: [{ icon: "hang", t: "Call closed — caller said goodbye" }] },
],
mail: [
{ k: "What happened", v: "Caller asked about the cost of a first consultation. Answered in Russian; details taken for intake." },
{ k: "Language", v: "Russian (ru-RU) — detected automatically, no setup" },
{ k: "Caller", v: "Irina Sokolova · (971) 555-0126 — read back and confirmed" },
{ k: "Answered", v: ["First consultation is free, 30 minutes (read live from /consultations/)", "Availability this week", "Did not give legal advice — the script forbids it"] },
{ k: "Needs you", v: "Contact Irina today to schedule the consultation." },
],
},
];
// --- styles ------------------------------------------------------------
if (typeof document !== "undefined" && !document.getElementById("v1-final-styles")) {
const s = document.createElement("style");
s.id = "v1-final-styles";
s.textContent = `
/* ── hero call ── */
.olc-wrap{display:grid;grid-template-columns:1.1fr .9fr;gap:24px;align-items:start}
.olc-box{border:1px solid ${PALETTE.hairline};background:${PALETTE.bg2}}
.olc-head{display:flex;justify-content:space-between;align-items:center;padding:12px 18px;
border-bottom:1px solid ${PALETTE.hairline};background:${PALETTE.bg}}
.olc-head h4{font-family:${sans};font-size:11px;letter-spacing:1.6px;text-transform:uppercase;margin:0;font-weight:600}
.olc-head .meta{font-family:${mono};font-size:10.5px;color:${PALETTE.sub}}
.olc-mhead{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 18px;
border-bottom:1px solid ${PALETTE.hairline};background:${PALETTE.bg};
font-family:${mono};font-size:10.5px;color:${PALETTE.sub};flex-wrap:wrap}
.olc-mhead b{color:${PALETTE.ink};font-weight:600}
.olc-thread{padding:20px;min-height:472px}
.olc-turn{margin-bottom:16px;opacity:0;animation:olc-fade .3s ease forwards}
@keyframes olc-fade{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
.olc-turn .who{font-family:${sans};font-size:9.5px;letter-spacing:1.4px;text-transform:uppercase;
color:${PALETTE.sub};margin-bottom:5px;display:flex;gap:8px;align-items:center}
.olc-turn.agent .who{color:${PALETTE.accent}}
.olc-turn .say{font-family:${serif};font-size:16.5px;line-height:1.5;max-width:54ch}
.olc-turn.agent .say{border-left:2px solid ${PALETTE.accent};padding-left:13px}
.olc-flag{font-family:${mono};font-size:9px;border:1px solid ${PALETTE.hairline};padding:1px 5px;color:${PALETTE.sub}}
.olc-act{display:flex;gap:10px;align-items:flex-start;margin:0 0 13px 13px;padding-left:13px;
border-left:1px dashed ${PALETTE.hairline};opacity:0;animation:olc-fade .3s ease forwards}
.olc-act svg{width:17px;height:17px;flex:none;margin-top:1px;color:${PALETTE.accent}}
.olc-act .txt{font-family:${sans};font-size:12.5px;line-height:1.45;color:${PALETTE.sub}}
.olc-act .txt b{color:${PALETTE.ink};font-weight:600}
.olc-act .src{display:block;font-family:${mono};font-size:10.5px;color:${PALETTE.accent};margin-top:2px;word-break:break-all}
.olc-act.working .txt b::after{content:"";display:inline-block;width:5px;height:5px;background:${PALETTE.accent};
margin-left:7px;vertical-align:middle;animation:olc-blink 1s steps(2) infinite}
@keyframes olc-blink{50%{opacity:0}}
.olc-mbody{padding:18px;min-height:300px}
.olc-idle{font-family:${sans};font-size:12.5px;color:${PALETTE.sub};line-height:1.6}
.olc-mrow{margin-bottom:13px;opacity:0;animation:olc-fade .3s ease forwards}
.olc-mk{font-family:${sans};font-size:9.5px;letter-spacing:1.4px;text-transform:uppercase;color:${PALETTE.sub};margin-bottom:3px}
.olc-mv{font-family:${serif};font-size:14.5px;line-height:1.5}
.olc-mv ul{margin:4px 0 0;padding-left:16px}.olc-mv li{margin-bottom:3px}
.olc-send{border-top:1px solid ${PALETTE.hairline};padding:11px 18px;display:flex;justify-content:space-between;
align-items:center;font-family:${sans};font-size:11.5px;color:${PALETTE.sub};gap:12px}
.olc-sent{color:${PALETTE.bg};background:${PALETTE.accent};padding:4px 9px;font-size:10px;letter-spacing:1.4px;
text-transform:uppercase;opacity:0;transition:opacity .3s}
.olc-sent.on{opacity:1}
/* No replay / next / speed controls and no scenario ticks (removed
2026-08-06, operator). A hero shouldn't ask the visitor to operate it —
the buttons made an illustration read as a demo they had to drive. The
seven calls now run back to back on their own. */
.olc-disc{font-family:${sans};font-size:11.5px;color:${PALETTE.sub};margin:18px 0 0}
/* ── how it works: the drawn line ── */
.olh-track{position:relative;margin-top:50px;padding-bottom:26px}
.olh-rail{position:absolute;left:64px;top:0;bottom:26px;width:2px;background:${PALETTE.line}}
.olh-spine{position:absolute;left:64px;top:0;bottom:26px;width:2px;pointer-events:none}
.olh-spine svg{height:100%;width:2px;overflow:visible}
.olh-spine path{stroke:${PALETTE.accent};stroke-width:2;fill:none;stroke-dasharray:1000;stroke-dashoffset:1000}
.olh-step{position:relative;padding:0 0 62px 132px;min-height:130px}
.olh-step:last-of-type{padding-bottom:0}
.olh-node{position:absolute;left:44px;top:2px;width:44px;height:44px;border:1px solid ${PALETTE.hairline};
background:${PALETTE.bg};display:flex;align-items:center;justify-content:center;
transition:border-color .4s,color .4s,box-shadow .4s}
.olh-node svg{width:22px;height:22px}
.olh-step.on .olh-node{border-color:${PALETTE.accent};color:${PALETTE.accent};box-shadow:4px 4px 0 0 ${PALETTE.accentSoft}}
.olh-num{font-family:${sans};font-size:10px;letter-spacing:1.6px;color:${PALETTE.sub};margin-bottom:7px}
.olh-step h3{font-family:${serif};font-size:29px;line-height:1.15;letter-spacing:-.7px;margin:0 0 10px;
font-weight:500;opacity:.35;transition:opacity .45s}
.olh-step p{font-family:${serif};margin:0;font-size:16.5px;line-height:1.55;max-width:52ch;opacity:.28;transition:opacity .45s}
.olh-step.on h3{opacity:1}.olh-step.on p{opacity:.84}
/* ── how it fits: the handoff ── */
.olf-journey{margin-top:44px;border:1px solid ${PALETTE.hairline};background:${PALETTE.bg2}}
.olf-head{display:flex;justify-content:space-between;align-items:center;gap:14px;padding:14px 22px;
border-bottom:1px solid ${PALETTE.hairline};background:${PALETTE.bg};flex-wrap:wrap}
.olf-head h3{font-family:${sans};font-size:11px;letter-spacing:1.6px;text-transform:uppercase;margin:0;font-weight:600}
.olf-head .q{font-family:${serif};font-size:17px;font-style:italic;opacity:.8}
.olf-beats{display:grid;grid-template-columns:repeat(4,1fr)}
.olf-beat{padding:28px 22px 26px;border-right:1px solid ${PALETTE.line}}
.olf-beat:last-child{border-right:none}
.olf-beat .n{font-family:${sans};font-size:10px;letter-spacing:1.6px;color:${PALETTE.sub};margin-bottom:12px}
.olf-icon{width:46px;height:46px;border:1px solid ${PALETTE.hairline};background:${PALETTE.bg};display:flex;
align-items:center;justify-content:center;margin-bottom:14px;
transition:border-color .3s,color .3s,box-shadow .3s}
.olf-icon svg{width:23px;height:23px}
.olf-beat.live .olf-icon{border-color:${PALETTE.accent};color:${PALETTE.accent};box-shadow:4px 4px 0 0 ${PALETTE.accentSoft}}
.olf-beat h4{font-family:${serif};font-size:19px;margin:0 0 7px;font-weight:500;letter-spacing:-.3px;line-height:1.25}
.olf-beat p{font-family:${serif};margin:0;font-size:14.5px;line-height:1.5;opacity:.84}
.olf-beat .said{font-style:italic;opacity:.72;margin-top:9px}
.olf-wire{position:relative;height:2px;background:${PALETTE.line};margin:0 22px}
.olf-pip{position:absolute;top:-4px;left:0;width:10px;height:10px;background:${PALETTE.accent};
animation:olf-travel 8s linear infinite}
@keyframes olf-travel{0%{left:0}100%{left:100%}}
.olf-fallback{border-top:1px solid ${PALETTE.hairline};padding:18px 22px;display:flex;gap:14px;align-items:flex-start;
font-family:${sans};font-size:13px;line-height:1.55;color:${PALETTE.sub}}
.olf-fallback svg{width:19px;height:19px;flex:none;margin-top:1px;color:${PALETTE.accent}}
.olf-fallback b{color:${PALETTE.ink};font-weight:600}
/* ── languages ── */
.oll-sec{display:flex;flex-direction:column;align-items:center;text-align:center}
.oll-stage{position:relative;width:min(460px,80vw);aspect-ratio:1;margin-top:24px}
.oll-stage canvas{display:block;width:100%;height:100%}
.oll-greet{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
text-align:center;pointer-events:none;
background:radial-gradient(closest-side,${PALETTE.bg} 36%,rgba(244,237,225,.72) 60%,rgba(244,237,225,0) 80%)}
.oll-greet .g{font-family:${serif};font-size:clamp(34px,5vw,54px);line-height:1.1;letter-spacing:-1.2px;
transition:opacity .5s,transform .5s}
.oll-greet .g.out{opacity:0;transform:translateY(-10px)}
.oll-greet .nm{font-family:${sans};font-size:11px;letter-spacing:1.8px;text-transform:uppercase;
color:${PALETTE.sub};margin-top:11px;transition:opacity .5s}
.oll-greet .nm.out{opacity:0}
/* ── who: marquee ── */
.olw-band{border-top:1px solid ${PALETTE.line};border-bottom:1px solid ${PALETTE.line};padding:20px 0;
overflow:hidden;white-space:nowrap;
-webkit-mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);
mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent)}
.olw-band + .olw-band{border-top:none}
.olw-track{display:inline-block;white-space:nowrap;animation:olw-slide 42s linear infinite}
.olw-band.rev .olw-track{animation-direction:reverse}
@keyframes olw-slide{from{transform:translateX(0)}to{transform:translateX(-50%)}}
.olw-item{display:inline-block;font-family:${serif};font-size:30px;letter-spacing:-.7px;padding:0 28px;opacity:.85}
.olw-item .dot{color:${PALETTE.accent};margin-left:28px}
.olw-band.rev .olw-item{font-style:italic;opacity:.6;font-size:26px}
.olw-band:hover .olw-track{animation-play-state:paused}
@media (max-width: 1023px){
.olc-wrap{grid-template-columns:1fr}
.olf-beats{grid-template-columns:repeat(2,1fr)}
.olf-beat{border-bottom:1px solid ${PALETTE.line}}
}
@media (max-width: 700px){
.olc-thread{min-height:0}
.olh-rail,.olh-spine{left:20px}
.olh-step{padding-left:74px;padding-bottom:44px}
.olh-node{left:0}
.olh-step h3{font-size:23px}
.olf-beats{grid-template-columns:1fr}
.olf-beat{border-right:none}
.olc-dots{margin-left:0}
.olw-item{font-size:22px;padding:0 18px}
.olw-band.rev .olw-item{font-size:19px}
}
@media (prefers-reduced-motion: reduce){
.olc-turn,.olc-act,.olc-mrow{animation:none;opacity:1}
.olh-step h3{opacity:1}.olh-step p{opacity:.84}
.olw-track,.olf-pip{animation:none}
}
`;
document.head.appendChild(s);
}
const prefersReduced = () =>
typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches;
// ---------------------------------------------------------------- CallStory
//
// The transcript and the summary are built imperatively into two refs rather
// than held in React state. Each line appears on its own timer and never
// changes once written, so re-rendering the whole list on every tick would be
// pure overhead — and the CSS entry animation only fires on real DOM insert.
// 620ms * 2 is the reading pace. It was a toggle until 2026-08-06; there is no
// speed control now, so this is the only pace anyone ever sees.
const BEAT_MS = 620 * 2;
function CallStory() {
const [scen, setScen] = React.useState(0);
const threadRef = React.useRef(null);
const mailRef = React.useRef(null);
const clockRef = React.useRef(null);
const noteRef = React.useRef(null);
const sentRef = React.useRef(null);
const s = SCENARIOS[scen];
React.useEffect(() => {
const thread = threadRef.current;
const mail = mailRef.current;
if (!thread || !mail) return;
const reduced = prefersReduced();
const timers = [];
const at = (() => {
let step = 0;
const unit = reduced ? 0 : BEAT_MS;
const fn = (cb) => { timers.push(setTimeout(cb, step * unit)); step++; };
fn.step = () => step;
fn.unit = unit;
return fn;
})();
thread.innerHTML = "";
mail.innerHTML = `
The summary writes itself when the call ends.
`;
if (sentRef.current) sentRef.current.classList.remove("on");
if (noteRef.current) noteRef.current.textContent = "Waiting for the call to finish";
if (clockRef.current) clockRef.current.textContent = "00:00";
const act = (a) => {
const d = document.createElement("div");
d.className = "olc-act" + (a.working ? " working" : "");
d.innerHTML = `${ICON[a.icon] || ""}
`;
mail.appendChild(d);
})
);
at(() => {
if (sentRef.current) sentRef.current.classList.add("on");
if (noteRef.current) noteRef.current.textContent = "Delivered to the owner's inbox";
});
// The seven calls run back to back, forever. Between them the finished
// summary sits on screen long enough to read — that pause is the payoff of
// the whole animation, so it's longer than any gap inside a call.
const hold = reduced ? 9000 : 5200;
timers.push(
setTimeout(() => setScen((c) => (c + 1) % SCENARIOS.length), at.step() * at.unit + hold)
);
return () => timers.forEach(clearTimeout);
}, [scen, s]);
return (
The call
00:00
To: {s.to}
Subject: {s.subject}
Waiting for the call to finishSent
Sample calls from example businesses — illustrative, not recordings.
);
}
// ----------------------------------------------------------------- HowLine
const HOW_STEPS = [
{ n: "01", icon: "phonein", title: "A call comes in.", body: "Every call, the ones you miss, or only after hours — however you set it up. OpenLines picks up as your business." },
{ n: "02", icon: "speech", title: "It handles the conversation — or connects a person.", body: "Answers from your script, with optional live checks on websites you list. Captures name, number with read-back, and reason. Can connect to a teammate you name." },
{ n: "03", icon: "mail", title: "You get a summary by email.", body: "What was asked, handled, and needs follow-up. Conversations aren't fully recorded — the summary is what you keep." },
{ n: "04", icon: "dial", title: "You stay in control.", body: "Script, voice, transfer target, voicemail mode, listed websites — built with you and easy to change." },
];
function HowLine() {
const trackRef = React.useRef(null);
const spineRef = React.useRef(null);
React.useEffect(() => {
const track = trackRef.current;
if (!track) return;
const steps = [...track.querySelectorAll("[data-hs]")];
const io = new IntersectionObserver(
(es) => es.forEach((e) => { if (e.isIntersecting) e.target.classList.add("on"); }),
{ rootMargin: "0px 0px -30% 0px" }
);
steps.forEach((el) => io.observe(el));
const draw = () => {
const r = track.getBoundingClientRect();
const seen = Math.min(1, Math.max(0, (innerHeight * 0.75 - r.top) / r.height));
if (spineRef.current) spineRef.current.style.strokeDashoffset = String(1000 * (1 - seen));
};
addEventListener("scroll", draw, { passive: true });
addEventListener("resize", draw, { passive: true });
draw();
return () => {
io.disconnect();
removeEventListener("scroll", draw);
removeEventListener("resize", draw);
};
}, []);
return (
{HOW_STEPS.map((s) => (
{s.n}
{s.title}
{s.body}
))}
);
}
// -------------------------------------------------------------- FitsJourney
const BEATS = [
{ n: "01", icon: "speech", title: "They ask", body: "In words, or by pressing a key — whichever they reach for first.", said: "“Can I talk to someone?”" },
{ n: "02", icon: "reroute", title: "It says so plainly", body: "No hold music, no menu tree, no pretending to be a person.", said: "“Of course — connecting you now.”" },
{ n: "03", icon: "ring", title: "Your phone rings", body: "A number you name — your cell, the back office, a direct extension. Never a call centre." },
{ n: "04", icon: "talking", title: "You're talking", body: "Same call, same caller. You take it from there." },
];
function FitsJourney() {
const [live, setLive] = React.useState(0);
React.useEffect(() => {
if (prefersReduced()) return;
const id = setInterval(() => setLive((i) => (i + 1) % BEATS.length), 2000);
return () => clearInterval(id);
}, []);
return (
Asking for a person
“Is there someone I can talk to?”
{BEATS.map((b, i) => (
{b.n}
{b.title}
{b.body}
{b.said ?
{b.said}
: null}
))}
And if you can't pick up? It doesn't dump them into a dead line. The agent comes back
on, takes their name, number and reason, reads it back to confirm, and emails it to you — so
a missed handoff is still a captured lead. Transfer stays off entirely until you give it a number.
);
}
// ------------------------------------------------------------ LanguagesGlobe
//
// three.js is pulled in at runtime with `new Function("return import(...)")`.
// A bare `import()` here would be rewritten by @babel/standalone (the page has
// no build step and compiles JSX in the browser), so the indirection is what
// keeps it a real dynamic import. The URL is fully qualified — no import map
// needed — and unpkg is already the origin serving React and Babel.
//
// If the fetch fails, the greeting card still animates over an empty canvas:
// the section reads fine without the globe, so there's no fallback branch.
const NAMED = [
{ n: "English", g: "Hello", lat: 40, lon: -95 },
{ n: "Spanish", g: "Hola", lat: 23, lon: -102 },
{ n: "Russian", g: "Привет", lat: 61, lon: 100 },
];
const OTHERS = [
{ lat: 40, lon: -4 }, { lat: 47, lon: 2 }, { lat: 51, lon: 10 }, { lat: 42, lon: 13 },
{ lat: -14, lon: -52 }, { lat: 39, lon: -8 }, { lat: 27, lon: 30 }, { lat: 24, lon: 45 },
{ lat: 24, lon: 54 }, { lat: 21, lon: 78 }, { lat: 24, lon: 90 }, { lat: 35, lon: 105 },
{ lat: 36, lon: 138 }, { lat: 36, lon: 128 }, { lat: -1, lon: 114 }, { lat: 39, lon: 35 },
{ lat: 14, lon: 108 },
];
function LanguagesGlobe() {
const stageRef = React.useRef(null);
const canvasRef = React.useRef(null);
const gRef = React.useRef(null);
const nRef = React.useRef(null);
React.useEffect(() => {
let disposed = false;
let raf = 0;
let onResize = null;
const swaps = [];
// Text cycles on its own timer so the section still reads if three fails.
let active = 0;
const setText = (i) => {
const l = NAMED[i];
if (!gRef.current || !nRef.current) return;
gRef.current.classList.add("out");
nRef.current.classList.add("out");
swaps.push(setTimeout(() => {
if (!gRef.current || !nRef.current) return;
gRef.current.textContent = l.g;
nRef.current.textContent = l.n;
gRef.current.classList.remove("out");
nRef.current.classList.remove("out");
}, 500));
};
new Function('return import("https://unpkg.com/three@0.169.0/build/three.module.js")')()
.then((THREE) => {
if (disposed || !canvasRef.current) return;
const INK = 0x241c12, ACCENT = 0xa04936;
const reduced = prefersReduced();
const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.current, antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(36, 1, 0.1, 100);
camera.position.set(0, 0, 13);
const globe = new THREE.Group();
scene.add(globe);
const R = 4;
for (let i = 0; i < 5; i++) {
const t = new THREE.Mesh(
new THREE.TorusGeometry(R, 0.0045, 6, 140),
new THREE.MeshBasicMaterial({ color: INK, transparent: true, opacity: 0.18 })
);
t.rotation.y = (i / 5) * Math.PI;
globe.add(t);
}
[-0.55, 0, 0.55].forEach((y) => {
const r = Math.sqrt(Math.max(0.02, 1 - y * y)) * R;
const t = new THREE.Mesh(
new THREE.TorusGeometry(r, 0.0045, 6, 140),
new THREE.MeshBasicMaterial({ color: INK, transparent: true, opacity: 0.14 })
);
t.rotation.x = Math.PI / 2;
t.position.y = y * R;
globe.add(t);
});
const toXYZ = (lat, lon, r = R) => {
const phi = ((90 - lat) * Math.PI) / 180, th = ((lon + 180) * Math.PI) / 180;
return new THREE.Vector3(
-r * Math.sin(phi) * Math.cos(th),
r * Math.cos(phi),
r * Math.sin(phi) * Math.sin(th)
);
};
const others = OTHERS.map((o, i) => {
const p = toXYZ(o.lat, o.lon);
const d = new THREE.Mesh(
new THREE.SphereGeometry(0.052, 8, 6),
new THREE.MeshBasicMaterial({ color: INK, transparent: true, opacity: 0.3 })
);
d.position.copy(p);
globe.add(d);
return { d, p, phase: i * 0.7 };
});
const named = NAMED.map((l) => {
const p = toXYZ(l.lat, l.lon);
const d = new THREE.Mesh(
new THREE.SphereGeometry(0.085, 12, 10),
new THREE.MeshBasicMaterial({ color: INK, transparent: true, opacity: 0.55 })
);
d.position.copy(p);
const ring = new THREE.Mesh(
new THREE.TorusGeometry(0.26, 0.008, 6, 40),
new THREE.MeshBasicMaterial({ color: ACCENT, transparent: true, opacity: 0 })
);
ring.position.copy(p);
ring.lookAt(p.clone().multiplyScalar(2));
globe.add(d);
globe.add(ring);
return { d, ring, p };
});
let spin = 0, target = 0, tilt = 0, t0 = 0;
const select = (i) => {
active = i;
const l = NAMED[i];
setText(i);
target = -(((l.lon + 180) * Math.PI) / 180) + Math.PI / 2;
tilt = THREE.MathUtils.clamp(((l.lat * Math.PI) / 180) * 0.38, -0.38, 0.38);
};
const fwd = new THREE.Vector3(0, 0, 1);
const size = () => {
const el = stageRef.current;
if (!el) return;
const w = el.clientWidth, h = el.clientHeight;
renderer.setSize(w, h, false);
camera.aspect = w / h;
camera.updateProjectionMatrix();
};
const frame = (now) => {
if (disposed) return;
if (now - t0 > 3400) { t0 = now; select((active + 1) % NAMED.length); }
spin += (target - spin) * 0.04;
globe.rotation.y = spin;
globe.rotation.x += (tilt - globe.rotation.x) * 0.04;
others.forEach((o) => {
const f = Math.max(0.04, o.p.clone().applyMatrix4(globe.matrixWorld).normalize().dot(fwd));
const br = reduced ? 1 : 0.82 + Math.sin(now * 0.0011 + o.phase) * 0.18;
o.d.material.opacity = f * 0.42 * br;
});
named.forEach((m, i) => {
const f = Math.max(0.04, m.p.clone().applyMatrix4(globe.matrixWorld).normalize().dot(fwd));
const on = i === active;
m.d.material.color.setHex(on ? ACCENT : INK);
m.d.material.opacity = on ? 1 : f * 0.5;
m.d.scale.setScalar(on ? 1.5 : 1);
m.ring.material.opacity = on ? 0.75 * f : 0;
m.ring.scale.setScalar(on ? 1 + Math.sin(now * 0.003) * 0.12 : 1);
});
renderer.render(scene, camera);
raf = requestAnimationFrame(frame);
};
onResize = size;
size();
select(0);
raf = requestAnimationFrame(frame);
addEventListener("resize", onResize, { passive: true });
})
.catch(() => {
// No globe. Keep the greeting rotating so the section still says
// something rather than freezing on "Hello".
const id = setInterval(() => { active = (active + 1) % NAMED.length; setText(active); }, 3400);
swaps.push(id);
});
return () => {
disposed = true;
cancelAnimationFrame(raf);
swaps.forEach(clearTimeout);
swaps.forEach(clearInterval);
if (onResize) removeEventListener("resize", onResize);
};
}, []);
return (
Languages
Hello
English
English, Spanish, Russian
and more.
Your caller speaks. The agent answers in the same language — mid‑sentence, on the same number.
{/* The bands are decorative duplicates; this is what a screen reader gets. */}
Independent shops, family-run businesses, service businesses and trades, salons and
personal-care studios, and growing multi-location teams — bakeries, heating and air,
barbers, auto shops, florists, law offices, gyms, event venues, landscapers, delis,
roofers and cafés.