// 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) => `${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] || ""}
${a.t}${a.src ? `${a.src}` : ""}
`; thread.appendChild(d); if (a.working) timers.push(setTimeout(() => d.classList.remove("working"), at.unit * 0.9)); }; const turn = (t) => { const d = document.createElement("div"); d.className = "olc-turn " + t.who; d.innerHTML = `
${t.who === "agent" ? "OpenLines" : "Caller"}${ t.flag ? `${t.flag}` : "" }
`; d.querySelector(".say").textContent = t.text; thread.appendChild(d); }; s.turns.forEach((t) => { (t.pre || []).forEach((a) => at(() => act(a))); at(() => { turn(t); if (clockRef.current) clockRef.current.textContent = "00:" + String(Math.min(59, at.step() * 2)).padStart(2, "0"); }); (t.acts || []).forEach((a) => at(() => act(a))); }); at(() => { mail.innerHTML = ""; if (noteRef.current) noteRef.current.textContent = "Writing…"; }); s.mail.forEach((r) => at(() => { const d = document.createElement("div"); d.className = "olc-mrow"; const v = Array.isArray(r.v) ? `
    ${r.v.map((x) => `
  • ${x}
  • `).join("")}
` : r.v; d.innerHTML = `
${r.k}
${v}
`; 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 finish Sent

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) => (
))}
); } // -------------------------------------------------------------- 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) => (
))}
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

English, Spanish, Russian
and more.

Your caller speaks. The agent answers in the same language — mid‑sentence, on the same number.

Detected automatically · nothing to set up
); } // -------------------------------------------------------------- WhoMarquee const MARQUEE_A = [ "Independent shops", "Family-run businesses", "Service businesses", "Trades", "Salons", "Personal-care studios", "Multi-location teams", "Growing teams", ]; const MARQUEE_B = [ "bakeries", "heating & air", "barbers", "auto shops", "florists", "law offices", "gyms", "event venues", "landscapers", "delis", "roofers", "cafés", ]; function Band({ items, rev }) { const doubled = items.concat(items); return ( ); } function WhoMarquee() { return (
{/* 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.

); } Object.assign(window, { CallStory, HowLine, FitsJourney, LanguagesGlobe, WhoMarquee, OL_CALL_SCENARIOS: SCENARIOS }); })();