SEO Meta Data: Best Practices for Rankings in 2025

Key Takeaways

  • Organic search still drives roughly half of all website traffic in 2025, so dialing in your SEO meta data (titles, descriptions, schema) is one of the fastest ways to win higher-quality pipeline from search without increasing ad spend.
  • Treat every title tag like a mini sales pitch: lead with the buyer's problem, front-load the primary keyword, and promise a concrete outcome to boost click-through rates and demo requests.
  • Google rewrites an estimated 60-70% of meta descriptions and over 25% of top-ranking pages don't have one at all, so you need scalable templates and prioritization instead of trying to hand-craft every single page.
  • Schema markup (FAQ, HowTo, Organization, Product, Review) and clean meta data dramatically increase your chances of rich results and visibility in AI summaries, which is critical as zero-click and AI-driven searches keep rising.
  • Revenue teams should co-own SEO meta data: marketing writes it, but SDRs and AEs need to feed real objection language, use cases, and messaging from live conversations back into titles and descriptions.
  • Your best short-term win: audit the 20-50 pages most tied to opportunities (product, pricing, comparison, core blogs), rewrite meta data around search intent and value, then track CTR and conversion lift in Google Search Console and your CRM.
Executive Summary

In 2025, organic search still drives about 47-53% of all website traffic, and 66% of B2B buyers use search engines to research products-long before they talk to sales. Smart SEO meta data is now a sales tool as much as a marketing lever. This guide shows B2B teams how to write titles, descriptions, and schema that win more clicks, feed AI results, and convert anonymous searchers into qualified meetings.

Introduction

If you’re running a B2B sales or marketing team in 2025, you’re playing on a tilted field.

Organic search still drives close to half of all website traffic worldwide-around 47-53% depending on the study. At the same time, 66% of B2B buyers say they use search engines when researching products, long before they ever talk to a rep. Add in AI summaries and rising zero‑click searches, and the fight for visibility on page one has never been fiercer.

In that environment, SEO meta data-your title tags, meta descriptions, and structured data-has quietly become one of the highest‑leverage tools you have to influence pipeline. Done right, it’s the difference between being the vendor buyers skim past and the one they click, remember, and book a meeting with.

In this guide, we’ll break down:

  • Why SEO meta data still matters in 2025 (even with AI search and zero‑click results)
  • How to write B2B titles and descriptions that win clicks and qualified traffic
  • The role of schema markup and rich results in the new SERP
  • How to align your meta data with the B2B buying journey and your sales process
  • A practical workflow your marketing and sales teams can actually run

We’ll keep it grounded in reality-what actually moves the needle for SDR teams, pipeline, and revenue.

Why SEO Meta Data Still Matters in 2025

Let’s tackle the obvious question: If AI is answering everything on the SERP, why should you care about meta data?

Search Is Still the Front Door to Your Funnel

Multiple 2025 studies show traditional search is still the biggest traffic driver on the web. One global analysis found organic search now accounts for roughly 46.98% of all website traffic, beating social, paid, and referrals combined. Another breakdown puts organic at 53% of traffic on average across channels.

On the B2B side, 66% of buyers use search engines to research products they plan to purchase. Gartner also reports that around 61% of B2B buyers prefer a rep‑free experience, choosing to self‑educate via digital channels instead of talking to sales. In other words: buyers are Googling you long before your SDR ever dials them.

So whether demand starts from SEO, a LinkedIn ad, or a cold call, search is where buyers:

  • Validate your brand
  • Compare you to 4-5 alternative vendors
  • Decide who looks legit enough to short‑list

If your SERP snippet looks weak next to competitors, you lose that race.

Rankings Get You Seen-Meta Data Gets You Clicked

We all obsess over rankings, but clicks are where pipeline comes from.

Recent CTR benchmarks show the #1 organic result gets about 28.5% of clicks, with sharp drop‑offs from positions 2-10. Another analysis attributes 67.6% of organic clicks to just the top five results.

So if you’ve fought your way into the top 5, but your title and description read like they were written by a tired CMS, you’re literally throwing away high‑intent traffic.

AI Summaries and Zero‑Click: Meta Data as Training Data

Yes, AI summaries and zero‑click results are rising fast-especially in news. One SimilarWeb report found that after Google rolled out AI Overviews, zero‑click rates for news‑related queries jumped from 56% to 69%, crushing traffic for many publishers. In B2B, the impact is a bit softer so far, but AI search is growing: by mid‑2025, AI‑based searches already accounted for 5.6% of U.S. desktop search traffic, more than double the year prior.

For B2B vendors, that means two things:

  1. Your content and structured data are training data. Clean titles, descriptions, and schema help AI systems understand what your page is about and when to surface it.
  2. Rich results and summaries are the new prime real estate. If AI only shows one or two sources in its answer, you want to be one of them.

Your meta data is no longer just a “nice to have” for Google; it’s how humans and machines interpret your offer.

Anatomy of High-Performing SEO Meta Data

Before we get tactical, let’s get on the same page about what we’re actually optimizing.

The Core Pieces

For each key page, you’re typically working with:

  • Title tag (``)</strong>, The blue link in search results; a strong predictor of clicks and relevance.</li> <li><strong>Meta description (`<meta name="description">`)</strong>, The snippet under the title; not a direct ranking factor, but a big driver of CTR and <a href="https://saleshive.com/glossary/qualification/" class="sh-internal-link" data-wpel-link="internal">qualification</a>.</li> <li><strong>URL slug</strong>, The part after your domain; contributes to clarity, trust, and in some cases, relevance.</li> <li><strong>H1 (on-page heading)</strong>, Often mirrors or complements the title; helps both users and search engines confirm they’re in the right place.</li> <li><strong>Open Graph / Twitter tags</strong>, Control how your page appears when shared in email, Slack, or social.</li> <li><strong>Structured data (schema markup)</strong>, Machine‑readable data that unlocks rich results and gives AI systems cleaner signals.</li> </ul><p>Each of these is a <strong>micro sales asset</strong>. Instead of thinking “technical SEO,” think: <em>this is my 1-2 sentence pitch to a motivated buyer who’s deciding where to click.</em></p><h2 id="toc-2025-best-practices-for-b2b-title-tags-and-meta-descriptions-3">2025 Best Practices for B2B Title Tags and Meta Descriptions</h2><p>Let’s get into the practical stuff. How do you actually write meta data that moves pipeline?</p><h3>Title Tags: Your First Sales Pitch</h3><p>In 2025, the old “60-character rule” has evolved. Google renders titles inside a pixel‑based container, typically truncating around <strong>580-600 pixels</strong> on desktop. Because wide letters take more space than narrow ones, titles with lots of capital letters or symbols can get cut even if they’re short.</p><p><strong>Practical range:</strong> ~45-65 characters, validated with a SERP preview tool.</p><p>For B2B, high‑performing titles tend to follow a few patterns:</p><ol> <li><strong>Lead with the outcome, not the product.</strong></li> <ul class="sh-sub-list"> <li>Weak: <em>“Cloud Contact Center Software | BrandName”</em></li> <li>Strong: <em>“Cut Call Handling Time by 30% with Cloud Contact Center Software”</em></li> </ul> </ol><ol> <li><strong>Front‑load the primary keyword.</strong></li> </ol> You still want the main keyword early-both for relevance and so it doesn’t get truncated.</p><ol> <li><strong>Use buyer‑centric modifiers.</strong></li> </ol> Words like <em>for B2B SaaS</em>, <em>for MSPs</em>, <em>for Finance Teams</em>, etc., pre‑qualify traffic.</p><ol> <li><strong>Don’t be afraid of specificity.</strong></li> </ol> Titles like <em>“<a href="https://saleshive.com/" class="sh-internal-link" data-wpel-link="internal">B2B Lead Generation Agency</a> for Mid-Market SaaS”</em> may get fewer impressions, but they pull in far more qualified clicks than a generic “<a href="https://saleshive.com/glossary/lead-generation-agency/" class="sh-internal-link" data-wpel-link="internal">Lead Generation Agency</a>.”</p><h4>Example: Title Tag Makeovers</h4><p>Scenario: You sell an outbound <a href="https://saleshive.com/platform/" class="sh-internal-link" data-wpel-link="internal">sales platform</a>. Here’s a typical “meh” title vs. an optimized one.</p><ul> <li><strong>Before:</strong></li> </ul> <em>“<a href="https://saleshive.com/glossary/outbound-sales/" class="sh-internal-link" data-wpel-link="internal">Outbound Sales</a> Software | BrandName”</em> <br> Problems: Too generic, no outcome, brand leads instead of value.</p><ul> <li><strong>After:</strong></li> </ul> <em>“Outbound Sales Platform for B2B Teams that Need More Meetings”</em> <br> Why it works: States ICP (B2B teams) and outcome (more meetings). If you want to squeeze brand in, add it at the end: <em>“…, BrandName”.</em></p><h3>Meta Descriptions: Mini Email CTAs</h3><p>Meta descriptions aren’t a ranking factor, and Google rewrites them a <strong>lot</strong>-around <strong>60-70% of the time</strong>, according to large‑scale analyses. But when yours <em>is</em> shown, it’s prime persuasion real estate.</p><p>Aim for <strong>120-155 characters</strong>. The formula that works well in B2B looks a lot like a good outbound email line:</p><p>> <strong>[Pain or situation] + [Unique angle or proof] + [Clear next step]</strong></p><p>Example for a B2B lead gen platform:</p><p>> <em>Struggling to keep SDR calendars full? See how our outbound platform helps B2B teams book 40% more meetings in 90 days. Compare plans and ROI in minutes.</em></p><p>That’s not about keyword stuffing; it’s about clarity and intent. If someone is searching “outbound sales platform for B2B,” this speaks directly to their job.</p><h4>Match Intent, Not Just Keywords</h4><p>For each page, ask: <em>What is the dominant search intent?</em></p><ul> <li><strong>Problem intent:</strong> “why are cold emails not working”, “how to improve SDR productivity”</li> </ul> → Use language that acknowledges the struggle and suggests education. <ul> <li><strong>Solution intent:</strong> “<a href="https://saleshive.com/glossary/b2b-lead-generation/" class="sh-internal-link" data-wpel-link="internal">b2b lead generation</a> agency”, “<a href="https://saleshive.com/pricing/" class="sh-internal-link" data-wpel-link="internal">sales outsourcing company</a>”</li> </ul> → Highlight who you serve and the key differentiator. <ul> <li><strong>Comparison intent:</strong> “X vs Y”, “best <a href="https://saleshive.com/blog/sales-development-rep-outsourcing-does-work/" class="sh-internal-link" data-wpel-link="internal">SDR outsourcing</a> firms”</li> </ul> → Promise honest comparison, criteria, and social proof. <ul> <li><strong>Transactional intent:</strong> “pricing”, “plans”, “demo”</li> </ul> → Get to the point on cost, timelines, and next steps.</p><p>Your meta description should <strong>finish the sentence</strong> the buyer started when they typed their query.</p><h2 id="toc-structured-data-rich-results-and-ai-meta-data-beyond-blue-links-4">Structured Data, Rich Results, and AI: Meta Data Beyond Blue Links</h2><p>Titles and descriptions are table stakes. To really compete in 2025, you need to think about how your meta data feeds <strong>rich results and AI systems</strong>.</p><h3>Google Rewrites Your Snippets More Than You Think</h3><p>Studies aggregating Ahrefs data show that Google rewrites <strong>around 60-70% of meta descriptions</strong>, and about a quarter of top‑ranking pages don’t have a meta description at all. That sounds discouraging, but the takeaway is simple:</p><ul> <li>You <strong>can’t</strong> control every snippet.</li> <li>You <strong>can</strong> control making sure Google has better options to work with.</li> </ul><p>Well‑structured content with clear headings, bullet points, and schema gives Google more relevant text to pull into snippets and AI summaries.</p><h3>Schema Markup: Your SERP Force Multiplier</h3><p>Schema (structured data) is a way of tagging your content so search engines can understand it more precisely. For B2B sales and marketing teams, a few types are especially useful:</p><ul> <li><strong>Organization</strong>, Clarifies who you are, your logo, and your brand details.</li> <li><strong>Product / Service</strong>, Describes your offering, features, and in some cases, pricing or tiers.</li> <li><strong>FAQ</strong>, Surfaces common questions and answers directly in the SERP.</li> <li><strong>HowTo</strong>, Explains step‑by‑step processes (great for onboarding and implementation guides).</li> <li><strong>Review / AggregateRating</strong>, Highlights customer ratings and reviews where allowed.</li> </ul><p>These don’t just help you “rank better.” They help you:</p><ul> <li>Take up more visual space on the SERP</li> <li>Answer objections before a prospect hits your site</li> <li>Become a trusted source for AI systems that are looking for structured, verifiable information</li> </ul><p>If buyers are using AI tools for vendor research—47% say they already do for market discovery and 38% for vetting and shortlisting-then clear, structured data increases the odds you’re in that answer set. </p><h3>Practical Schema Plays for B2B</h3><p>If you’re not doing anything with schema yet, start simple:</p><ol> <li><strong>Add Organization schema</strong> site‑wide.</li> </ol> Make sure your name, URL, logo, and social profiles are clearly defined.</p><ol> <li><strong>Wrap Q&A content in FAQ schema.</strong></li> </ol> Take existing FAQ sections on pricing, implementation, security, and integrations and mark them up. These are exactly the questions prospects ask SDRs.</p><ol> <li><strong>Use Product/Service schema on key offerings.</strong></li> </ol> For each core product page, define the service, its category, and key properties like deployment type or target industry.</p><ol> <li><strong>Validate everything.</strong></li> </ol> Use Google’s Rich Results Test to catch errors before they cause issues.</p><p>This is not busywork. It’s the structured version of the discovery and qualification conversations your sales team has every day.</p><h2 id="toc-building-meta-data-around-the-b2b-buying-journey-5">Building Meta Data Around the B2B Buying Journey</h2><p>Good meta data doesn’t just chase keywords-it <strong>maps to the buying jobs your prospects are trying to complete.</strong></p><p>Gartner’s research on the B2B buying journey highlights six recurring “jobs” buyers loop through: problem identification, solution exploration, requirements building, supplier selection, validation, and consensus creation. Buyers also typically look at around <strong>five different software providers</strong> before making a choice. </p><p>Your meta data should explicitly help them with those jobs.</p><h3>Mapping Intent to Buying Jobs</h3><p>Here’s how intent, buying jobs, and meta data strategy line up:</p><ol> <li><strong>Problem Identification ("We need to do something")</strong></li> <ul class="sh-sub-list"> <li>Queries: <em>“SDR team not hitting quota”</em>, <em>“why outbound emails don’t work”</em></li> <li>Title angle: Name the problem and hint at a new way.</li> <li>Description angle: Promise clarity, benchmarks, and practical next steps.</li> </ul> </ol><ol> <li><strong>Solution Exploration ("What’s out there?")</strong></li> <ul class="sh-sub-list"> <li>Queries: <em>“b2b <a href="https://saleshive.com/glossary/lead-generation/" class="sh-internal-link" data-wpel-link="internal">lead generation</a> agency”</em>, <em>“<a href="https://saleshive.com/glossary/outsourced-sdr-company/" class="sh-internal-link" data-wpel-link="internal">SDR outsourcing company</a>”</em></li> <li>Title angle: Who you serve + primary outcome (e.g., “B2B Lead Generation Agency for Mid-Market SaaS Teams”).</li> <li>Description angle: ICP, core benefit, and proof (case studies, # meetings booked).</li> </ul> </ol><ol> <li><strong>Requirements Building ("What exactly do we need?")</strong></li> <ul class="sh-sub-list"> <li>Queries: <em>“in-house vs <a href="https://saleshive.com/glossary/outsourced-sdr-team/" class="sh-internal-link" data-wpel-link="internal">outsourced SDRs</a>”</em>, <em>“sdR outsourcing pricing models”</em></li> <li>Title angle: Comparison and clarity (e.g., “In-House vs Outsourced SDRs: Costs, Ramp Time, and ROI”).</li> <li>Description angle: Highlight frameworks, calculators, and templates.</li> </ul> </ol><ol> <li><strong>Supplier Selection & Validation ("Is this the right vendor?")</strong></li> <ul class="sh-sub-list"> <li>Queries: <em>“<a href="https://saleshive.com/" class="sh-internal-link" data-wpel-link="internal">SalesHive</a> review”</em>, <em>“SalesHive vs [Competitor]”</em></li> <li>Title angle: Embrace the comparison; don’t hide from it.</li> <li>Description angle: Emphasize transparent criteria, customer stories, and who you’re not a fit for.</li> </ul> </ol><ol> <li><strong>Consensus Creation ("Can we get everyone on board?")</strong></li> <ul class="sh-sub-list"> <li>Queries: <em>“business case for SDR outsourcing”</em>, <em>“ROI of <a href="https://saleshive.com/glossary/outbound-lead-generation/" class="sh-internal-link" data-wpel-link="internal">outbound lead generation</a>”</em></li> <li>Title angle: Business case and ROI.</li> <li>Description angle: Promise numbers, models, and plug‑and‑play slides.</li> </ul> </ol><p>When you write titles and descriptions with these jobs in mind, you’re not just fishing for traffic; you’re helping buying committees move forward-*which is exactly what your sales team needs.*</p><h3>Example: Rewriting for Buying Jobs</h3><p>Take a typical case study page. Here’s the “before” meta data you’ll often see:</p><ul> <li><strong>Title:</strong> <em>“Customer Success Story | BrandName”</em></li> <li><strong>Description:</strong> <em>“Read how BrandName helped a customer succeed with our solution.”</em></li> </ul><p>This could be <em>anyone</em> doing <em>anything</em>.</p><p>Now re‑write it for a real job: a VP of Sales building confidence and internal consensus.</p><ul> <li><strong>Title:</strong> <em>“How ACME SaaS 3x’d SQLs in 6 Months with SDR Outsourcing”</em></li> <li><strong>Description:</strong> <em>“See how ACME SaaS added 3x more SQLs and cut SDR ramp time by 50% with outsourced SDRs. Includes metrics, timelines, and lessons you can use in your business case.”</em></li> </ul><p>Now it’s obvious who it’s for, what it delivers, and why it’s worth the click.</p><h2 id="toc-workflow-how-sales-and-marketing-should-co-own-meta-data-6">Workflow: How Sales and Marketing Should Co‑Own Meta Data</h2><p>The biggest gap we see in B2B companies isn’t knowing <em>what</em> good meta data looks like. It’s having a process to actually produce and maintain it.</p><p>Here’s a practical workflow you can run without hiring a full SEO team.</p><h3>1. Prioritize Pages by Revenue, Not Vanity Metrics</h3><p>Start with:</p><ul> <li>Product and solution pages</li> <li>Pricing and plans</li> <li>Comparison pages (including “vs” content)</li> <li>High‑intent blogs (e.g., “how to choose a B2B lead gen agency”)</li> <li>Top case studies</li> </ul><p>Pull from your CRM which URLs show up most often in opportunities and closed‑won deals. That’s your Tier 1 list.</p><h3>2. Run a Quick Meta Data Audit</h3><p>For each Tier 1 URL, note:</p><ul> <li>Current title and its length</li> <li>Current meta description and its length</li> <li>Whether H1 matches or complements the title</li> <li>Presence (or absence) of schema</li> <li>Alignment to search intent and buying stage</li> </ul><p>Flag:</p><ul> <li>Titles over ~65 characters or that get truncated in SERP tools</li> <li>Generic titles like “Solutions | BrandName”</li> <li>Duplicate meta descriptions</li> <li>Pages without any description at all</li> </ul><h3>3. Draft New Meta Data Using Templates</h3><p>Create a small library of templates for different page types, like:</p><ul> <li><strong>Solution page:</strong></li> </ul> Title: `[Primary keyword] for [ICP] that [primary outcome]` <br> Description: `[ICP] struggling with [problem]? See how [solution] helps you [outcome] in [timeframe]. [CTA].`</p><ul> <li><strong>Comparison page:</strong></li> </ul> Title: `[Product A] vs [Product B]: [Key differentiator] for [ICP]` <br> Description: `Compare [A] and [B] on price, features, and onboarding. See which is better for [ICP] and download a checklist to share with your team.`</p><ul> <li><strong>Case study:</strong></li> </ul> Title: `How [Customer Type] at [Company] [achieved result] with [solution]` <br> Description: `Learn how [Company] used [solution] to [result] in [timeframe]. Includes metrics, playbook, and lessons you can copy.`</p><p>Have marketing/SEO draft new meta data using these templates, focusing hard on clarity and real benefits.</p><h3>4. Get Sales to Sanity‑Check Language</h3><p>Once a batch of drafts is ready, run them by your sales leadership or a few sharp AEs/SDRs. Ask:</p><ul> <li>Does this sound like something a real buyer would say or care about?</li> <li>Does it reflect the way prospects describe their problem on calls?</li> <li>Would this snippet make <em>you</em> click if you were in their shoes?</li> </ul><p>Sales does not need to wordsmith every comma. They just need to flag where messaging is off, jargon is heavy, or claims feel unrealistic.</p><h3>5. Implement, Track, and Iterate</h3><p>Ship the new meta data and then:</p><ul> <li>Annotate the change in Google Search Console and your analytics tool.</li> <li>Track impressions, CTR, and average position for 4-8 weeks.</li> <li>Track demo/meeting conversions and opportunities from organic search for those pages.</li> </ul><p>If a page’s CTR jumps and conversion holds or improves, that’s a win. If CTR improves but conversion tanks, you might be over‑promising in your snippet.</p><p>Keep a simple experiment log with:</p><ul> <li>URL</li> <li>Before/after meta data</li> <li>Date changed</li> <li>Key metrics before/after</li> <li>A quick verdict (keep, roll back, iterate)</li> </ul><p>This doesn’t need to be fancy. A spreadsheet and a recurring calendar reminder beat “we should really fix our SEO one of these days.”</p><h3>6. Close the Loop with SDR Sequences and Campaigns</h3><p>Any time you launch a new outbound motion-say, a sequence targeting heads of sales in B2B SaaS-you should:</p><ol> <li>Identify the main page those prospects will see (usually a tailored landing page or core solution page).</li> <li>Align the landing page’s title, meta description, and on‑page H1 with the promise in the email or call script.</li> <li>Make sure branded search results for your company echo that same promise.</li> </ol><p>This is where agencies like SalesHive live every day: we see how often prospects receive an outbound touch, then immediately Google the vendor name plus a phrase from the email. If your search snippet reinforces that message and offers a clear next step, you win more of those curiosity clicks.</p><h2 id="toc-how-this-applies-to-your-sales-team-7">How This Applies to Your Sales Team</h2><p>Let’s bring this down from the SEO clouds to what your SDR manager actually cares about: <strong>more good meetings, less wasted effort.</strong></p><h3>Better Meta Data = More Inbound Fuel</h3><p>If you’re doing any kind of inbound at all, ranking in the top 5 for a handful of high‑intent keywords can be the difference between “marketing‑sourced pipeline is nice to have” and “we can actually hit quota with this.”</p><p>Because roughly <strong>67.6% of organic traffic goes to the top five results</strong>, even small CTR gains there compound quickly. For example:</p><ul> <li>You rank #3 for “B2B lead generation agency,” getting ~11-12% CTR.</li> <li>You optimize your title and description, nudging CTR to 15-17%.</li> <li>That might mean dozens or hundreds of additional high‑intent visitors per month, a slice of whom will request demos or respond to outbound because they already recognize your brand.</li> </ul><h3>Meta Data as Sales Enablement</h3><p>Your SERP snippets are often the <strong>first time a buying committee member encounters your brand</strong>. They may not have opened your email or taken your call-but they will Google your category.</p><p>If your snippets clearly state:</p><ul> <li>Who you’re for (ICP)</li> <li>What you help them achieve (outcomes)</li> <li>What makes you different (positioning)</li> </ul><p>…then every search impression is a micro touchpoint warming the account before an SDR ever enters the picture.</p><h3>Reducing Friction in the Rep‑Free Journey</h3><p>With 61%+ of B2B buyers preferring a rep‑free experience, and most buyers anonymous for 70% of their journey, your website and SERP presence are doing a lot of selling without you. </p><p>Strong meta data helps by:</p><ul> <li>Setting accurate expectations before buyers land on your site</li> <li>Making it clear what stage a page is for (learn vs compare vs buy)</li> <li>Guiding self‑serve visitors to the right next step (calculator, demo, pricing, case study)</li> </ul><p>That means when they <em>do</em> talk to your SDRs, they’re better educated, more qualified, and closer to a decision.</p><h3>Giving SDRs Language That Already Resonates Online</h3><p>The best wording for titles and descriptions isn’t invented in a vacuum-it comes from:</p><ul> <li>The way high‑intent buyers search</li> <li>The phrases they use on discovery calls</li> <li>The objections they raise in late‑stage deals</li> </ul><p>When you distill that language into your meta data, you’re not just improving SEO-you’re also building a messaging library SDRs can pull into emails, call openers, and LinkedIn messages.</p><p>Example: If a lot of search queries hitting your site mention “book more meetings without hiring more SDRs,” that exact phrase probably belongs in your outbound copy <em>and</em> your meta data.</p><h2 id="toc-how-saleshive-can-help-8">How SalesHive Can Help</h2><p>At SalesHive, we live at the intersection of <a href="https://saleshive.com/glossary/sales-development/" class="sh-internal-link" data-wpel-link="internal">sales development</a> and digital visibility. Since 2016, we’ve booked <strong>100,000+ meetings</strong> for <strong>1,500+ B2B clients</strong> across SaaS, manufacturing, financial services, and more.</p><p>Our core services-cold calling, cold <a href="https://saleshive.com/blog/best-tactics-for-email-outreach-campaigns/" class="sh-internal-link" data-wpel-link="internal">email outreach</a>, SDR outsourcing, and <a href="https://saleshive.com/custom-list-building/" class="sh-internal-link" data-wpel-link="internal">list building</a>-are designed to generate meetings. But the reality we see every day is this: <strong>outbound lives and dies by what prospects see when they Google you.</strong></p><p>When we spin up outbound programs, our strategists don’t just write sequences. They:</p><ul> <li>Review your current SERPs for branded and high‑intent category keywords</li> <li>Flag weak or confusing title tags and descriptions on key pages</li> <li>Recommend meta data and landing page tweaks so your search presence matches your outbound promise</li> <li>Feed real objection language from SDR calls back to your SEO and content teams</li> </ul><p>Our AI-powered tools, like <a href="https://saleshive.com/platform/" class="sh-internal-link" data-wpel-link="internal">eMod</a> for email personalization, help us match messaging to buyer pain points at scale. Our US‑based and Philippines‑based SDR teams then execute across channels, while your website and meta data do their part turning curious searchers into qualified meetings.</p><p>We’re not your SEO agency-but we are the ones who feel it when your SERP presence doesn’t sell. So we make sure search, outbound, and sales development all pull in the same direction.</p><h2 id="toc-conclusion-and-next-steps-9">Conclusion and Next Steps</h2><p>SEO meta data isn’t glamorous. You’ll never see a LinkedIn thread bragging about “our incredible new meta description strategy.” But in 2025—when organic search still drives nearly half of all traffic and buyers are doing more research on their own-it’s one of the cleanest, most controllable levers you have to grow pipeline.</p><p>If you do nothing else after reading this, do this:</p><ol> <li><strong>List your top 20-50 revenue‑driving pages.</strong></li> <li><strong>Audit titles and descriptions</strong> for length, clarity, and intent alignment.</li> <li><strong>Rewrite them</strong> using buyer‑centric templates and real sales language.</li> <li><strong>Ship, track, and iterate</strong>, watching both CTR <strong>and</strong> demo/opportunity conversion.</li> <li><strong>Loop in your SDR team</strong> so messaging stays consistent across search, email, and calls.</li> </ol><p>Do that once and you’ll see a bump. Build it into your quarterly rhythm, and you’ll quietly stack meaningful gains in traffic, meetings, and revenue-while competitors keep wondering why their “SEO efforts” never seem to move the sales needle.</p><p>And if you want an outbound engine that’s built to take advantage of that improved search presence, that’s exactly what SalesHive is here for.</p> </div> <!-- Key Statistics --> <section class="sh-post-stats"> <h3 class="sh-post-stats-title">📊 Key Statistics</h3> <div class="sh-post-stats-grid"> <div class="sh-post-stat-card"> <div class="sh-post-stat-value">46.98%</div> <div class="sh-post-stat-context">Share of all global website traffic coming from organic search in 2025, meaning your title tags and meta data heavily influence nearly half of your potential inbound pipeline.</div> <div class="sh-post-stat-source">Source with link: <a href="https://seranking.com/blog/social-media-traffic-research-study/" target="_blank" rel="noopener noreferrer external" class="sh-citation" data-wpel-link="external">SE Ranking</a></div> </div> <div class="sh-post-stat-card"> <div class="sh-post-stat-value">53%</div> <div class="sh-post-stat-context">Average portion of site traffic driven by organic search across channels in recent data, reinforcing SEO as the top traffic and lead source for many B2B companies.</div> <div class="sh-post-stat-source">Source with link: <a href="https://www.seoinc.com/seo-blog/much-traffic-comes-organic-search/" target="_blank" rel="noopener noreferrer external" class="sh-citation" data-wpel-link="external">SEOInc</a></div> </div> <div class="sh-post-stat-card"> <div class="sh-post-stat-value">66%</div> <div class="sh-post-stat-context">Percentage of B2B buyers who use search engines when researching products to purchase-if your meta data is weak, you're invisible for two-thirds of active buyers.</div> <div class="sh-post-stat-source">Source with link: <a href="https://www.dbswebsite.com/blog/b2b-marketing-statistics-trends/" target="_blank" rel="noopener noreferrer external" class="sh-citation" data-wpel-link="external">DBS Interactive / Statista</a></div> </div> <div class="sh-post-stat-card"> <div class="sh-post-stat-value">67.6%</div> <div class="sh-post-stat-context">Share of organic traffic that goes to just the top five Google results, making click-worthy titles and descriptions critical for capturing market share in your category.</div> <div class="sh-post-stat-source">Source with link: <a href="https://www.dbswebsite.com/blog/b2b-marketing-statistics-trends/" target="_blank" rel="noopener noreferrer external" class="sh-citation" data-wpel-link="external">DBS Interactive / Gartner</a></div> </div> <div class="sh-post-stat-card"> <div class="sh-post-stat-value">28.5%</div> <div class="sh-post-stat-context">Approximate organic click-through rate for the #1 Google result; moving a key page's meta data from position 3 to 1 can more than double the clicks and leads it generates.</div> <div class="sh-post-stat-source">Source with link: <a href="https://www.seoinc.com/seo-blog/much-traffic-comes-organic-search/" target="_blank" rel="noopener noreferrer external" class="sh-citation" data-wpel-link="external">SEOInc</a></div> </div> <div class="sh-post-stat-card"> <div class="sh-post-stat-value">60–70%</div> <div class="sh-post-stat-context">Estimated share of meta descriptions that Google rewrites, which means B2B SEO teams must focus on intent alignment and snippets that work even when partially replaced.</div> <div class="sh-post-stat-source">Source with link: <a href="https://www.digitalsilk.com/digital-trends/top-seo-statistics/" target="_blank" rel="noopener noreferrer external" class="sh-citation" data-wpel-link="external">Digital Silk (summarizing Ahrefs)</a></div> </div> <div class="sh-post-stat-card"> <div class="sh-post-stat-value">61%</div> <div class="sh-post-stat-context">B2B buyers who prefer a rep-free buying experience, meaning your search snippets and on-page content often sell before an SDR ever gets involved.</div> <div class="sh-post-stat-source">Source with link: <a href="https://mediabrief.com/b2b-buyers-prefer-digital-research-as-61-opt-for-rep-free-experience-says-gartner/" target="_blank" rel="noopener noreferrer external" class="sh-citation" data-wpel-link="external">Gartner via MediaBrief</a></div> </div> <div class="sh-post-stat-card"> <div class="sh-post-stat-value">47%</div> <div class="sh-post-stat-context">B2B buyers using AI tools for market research and discovery, so your metadata and structured data now have to serve both traditional search and AI-driven answers.</div> <div class="sh-post-stat-source">Source with link: <a href="https://www.emarketer.com/content/ai-competes-with-search-b2b-buying-reshaping-vendor-discovery-funnel" target="_blank" rel="noopener noreferrer external" class="sh-citation" data-wpel-link="external">Insider Intelligence / eMarketer</a></div> </div> <div class="sh-post-stat-card"> <div class="sh-post-stat-value">91%</div> <div class="sh-post-stat-context">SEO practitioners who said SEO positively impacted website performance and marketing goals in 2024, showing that search-and by extension meta data-remains a reliable growth lever.</div> <div class="sh-post-stat-source">Source with link: <a href="https://thedigitalbloom.com/learn/2025-organic-traffic-crisis-analysis-report/" target="_blank" rel="noopener noreferrer external" class="sh-citation" data-wpel-link="external">Conductor via The Digital Bloom</a></div> </div> </div> </section> <!-- Expert Insights --> <!-- Common Mistakes --> <!-- Action Items --> <section class="sh-post-actions"> <h3 class="sh-post-actions-title">Action Items</h3> <div class="sh-post-action-item"> <div class="sh-post-action-num">1</div> <div class="sh-post-action-content"> <h4>Run a quick meta data audit on your top 50 revenue-driving pages</h4> <p>Pull URLs from your CRM and analytics that generate the most opportunities, then inspect titles, descriptions, and schema for each. Flag anything over ~65 characters, duplicated, or misaligned with modern buyer intent for rewrite.</p> </div> </div> <div class="sh-post-action-item"> <div class="sh-post-action-num">2</div> <div class="sh-post-action-content"> <h4>Build 3–5 meta description templates aligned to search intent</h4> <p>Create reusable patterns for problem, solution, comparison, pricing, and case study pages so your team can scale updates fast while still keeping each snippet unique and buyer-centric.</p> </div> </div> <div class="sh-post-action-item"> <div class="sh-post-action-num">3</div> <div class="sh-post-action-content"> <h4>Integrate SEO meta data into your outbound campaign planning</h4> <p>When launching a new SDR sequence, make sure the landing page title and description mirror the promise in the email or call script. That consistency increases trust and conversion when prospects inevitably Google you first.</p> </div> </div> <div class="sh-post-action-item"> <div class="sh-post-action-num">4</div> <div class="sh-post-action-content"> <h4>Set up a quarterly SEO x Sales alignment session</h4> <p>Review Google Search Console CTR data, top search queries, and call transcript snippets. Use this session to agree on new messaging and test hypotheses for updated titles and descriptions on key pages.</p> </div> </div> <div class="sh-post-action-item"> <div class="sh-post-action-num">5</div> <div class="sh-post-action-content"> <h4>Layer in FAQ or HowTo schema on your most common objection pages</h4> <p>Identify pages that address pricing, implementation, security, or integrations, then add FAQ schema that directly answers those questions so they can surface in rich results and AI summaries.</p> </div> </div> <div class="sh-post-action-item"> <div class="sh-post-action-num">6</div> <div class="sh-post-action-content"> <h4>Track CTR and conversion impact in a simple experiment log</h4> <p>For every batch of meta data changes, log the before/after versions, the date of change, CTR in GSC, and demo/meeting conversion rates. This keeps experiments organized and helps you prove ROI to leadership.</p> </div> </div> </section> <!-- SalesHive Solution --> <section class="sh-post-solution"> <div class="sh-post-solution-badge">How SalesHive Can Help</div> <h3 class="sh-post-solution-title">Partner with SalesHive</h3> <div class="sh-post-solution-content"> Most SEO conversations stop at rankings and traffic. SalesHive cares about meetings booked and pipeline created. That’s why our outbound programs-cold email, cold calling, and SDR outsourcing-are built to plug directly into how your buyers search and what they see when they Google you.</p><p>When our strategists and SDR teams plan a campaign, we don’t just write email copy and call scripts. We look at the actual SERPs for your priority keywords, analyze how your title tags and meta descriptions position you against competitors, and recommend changes so your landing pages and search snippets tell the same story as your outbound messaging. That consistency dramatically boosts trust and response when prospects click from an email or call follow‑up into a branded search.</p><p>With 100,000+ meetings booked for 1,500+ clients, SalesHive has seen which combinations of messaging, positioning, and search presence actually move the needle. Our list-building and research team surfaces the right accounts and buyer personas; our AI-powered tools like eMod personalize outreach at scale; and our US-based and Philippines-based SDR teams convert that interest into conversations. Along the way, we’ll flag meta data gaps and opportunities so your search presence works as hard as your SDRs. </div> <style> .sh-book-call-btn { display: inline-flex; align-items: center; gap: 12px; padding: 18px 36px; background: var(--sh-page-highlight); color: var(--sh-text-inverted) !important; font-weight: 800; font-size: 14px; letter-spacing: 2px; text-transform: uppercase; text-decoration: none; border: none; cursor: pointer; border-radius: 12px; transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); position: relative; overflow: hidden; } .sh-book-call-btn::before { content: ""; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent); animation: shBtnShimmer 3s infinite; } .sh-book-call-btn:hover { transform: translateY(-3px); color: var(--sh-text-inverted) !important; } .sh-book-call-btn svg { width: 18px; height: 18px; fill: var(--sh-text-inverted); } @keyframes shBtnShimmer { 0% { left: -100%; } 50%, 100% { left: 100%; } } @media (max-width: 768px) { .sh-book-call-btn { display: none !important; } } </style><button type="button" data-open-modal class="sh-book-call-btn"><svg viewBox="0 0 448 512"><path d="M400 64h-48V12c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v52H160V12c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v52H48C21.5 64 0 85.5 0 112v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zm-6 400H54c-3.3 0-6-2.7-6-6V160h352v298c0 3.3-2.7 6-6 6z"/></svg>Schedule a Consultation</button> </section> <!-- FAQs --> <section class="sh-post-faqs"> <h3 class="sh-post-faqs-title">❓ Frequently Asked Questions</h3> <div class="sh-post-faq-item active"> <div class="sh-post-faq-question"> <h4>Does SEO meta data still matter in 2025 with AI summaries and zero-click searches?</h4> <span class="sh-post-faq-icon">+</span> </div> <div class="sh-post-faq-answer"> <p>Yes-arguably more than ever. Organic search still drives around 47-53% of website traffic, and most of that traffic flows through traditional SERPs where titles, descriptions, and schema heavily influence clicks. At the same time, structured data and clear page signals help AI systems decide what to surface in summaries. For B2B teams, strong meta data is now a bridge between classic search, AI answers, and your sales funnel.</p> </div> </div> <div class="sh-post-faq-item "> <div class="sh-post-faq-question"> <h4>How long should my title tags and meta descriptions be for B2B pages?</h4> <span class="sh-post-faq-icon">+</span> </div> <div class="sh-post-faq-answer"> <p>In 2025, think in pixels, not just characters. Most studies suggest aiming for roughly 45-65 characters for titles so they fit within ~580-600 pixels on desktop, and 120-155 characters for meta descriptions. That said, your priority is clarity and a strong value proposition. If a slightly longer title is significantly more compelling and still renders cleanly in a SERP preview tool, use it.</p> </div> </div> <div class="sh-post-faq-item "> <div class="sh-post-faq-question"> <h4>Do meta descriptions impact rankings or just click-through rate?</h4> <span class="sh-post-faq-icon">+</span> </div> <div class="sh-post-faq-answer"> <p>Meta descriptions are not a direct ranking factor, but they strongly influence click-through rate-which indirectly affects performance because Google pays attention to how users engage with results. For B2B, the bigger win is qualification: a precise description repels the wrong traffic and attracts prospects who are actually evaluating your type of solution, leading to better pipeline, not just more sessions.</p> </div> </div> <div class="sh-post-faq-item "> <div class="sh-post-faq-question"> <h4>How should we adapt meta data for AI-driven search in B2B?</h4> <span class="sh-post-faq-icon">+</span> </div> <div class="sh-post-faq-answer"> <p>Focus on clarity, structure, and explicit answers. Use descriptive titles and H1s that clearly state who the page is for and what problem it solves. Add schema (FAQ, HowTo, Product, Organization) so AI systems can understand entities, relationships, and key facts. And make sure pages that answer high-value questions (e.g., pricing, implementation, security) are optimized to be quoted directly in summaries.</p> </div> </div> <div class="sh-post-faq-item "> <div class="sh-post-faq-question"> <h4>What's the best way to prioritize which pages to optimize first?</h4> <span class="sh-post-faq-icon">+</span> </div> <div class="sh-post-faq-answer"> <p>Start from revenue, not rankings. Pull a list of URLs associated with opportunities and closed-won deals-product, pricing, comparison, core solution pages, and a handful of high-intent blogs. Those are the pages where a jump in CTR and conversion will show up in pipeline fastest. Only after that should you work your way down to broader top-of-funnel content.</p> </div> </div> <div class="sh-post-faq-item "> <div class="sh-post-faq-question"> <h4>How often should we refresh SEO meta data on B2B pages?</h4> <span class="sh-post-faq-icon">+</span> </div> <div class="sh-post-faq-answer"> <p>For core product and solution pages, review at least quarterly, or whenever messaging, pricing, or ICP focus changes. For blogs and resources, prioritize refreshes based on traffic, rankings, and strategic importance (e.g., pillar content). The key is to treat meta data as living sales copy-if your sales deck has changed and your audience's language has evolved, your title tags and descriptions should reflect that.</p> </div> </div> <div class="sh-post-faq-item "> <div class="sh-post-faq-question"> <h4>Who should own SEO meta data—the SEO team, marketing, or sales?</h4> <span class="sh-post-faq-icon">+</span> </div> <div class="sh-post-faq-answer"> <p>Ownership should sit with marketing/SEO, but input must come from sales and customer success. SEO brings the keyword and SERP data; sales brings the voice of the customer, objections, and real-world use cases. The most effective B2B teams run a shared, lightweight process where marketing drafts meta data, then sales quickly sanity-checks it against what they hear in the field.</p> </div> </div> <div class="sh-post-faq-item "> <div class="sh-post-faq-question"> <h4>How do we measure the impact of better meta data on pipeline?</h4> <span class="sh-post-faq-icon">+</span> </div> <div class="sh-post-faq-answer"> <p>Tie search data to CRM data. For each key URL, track impressions, CTR, and average position in Google Search Console, and then monitor how visitors from organic search convert to demo requests and opportunities in your CRM. When you deploy a meta data change, annotate the date and compare 4-8 weeks of before/after data. Over time, you'll see exactly which snippets drive not just more traffic, but more revenue.</p> </div> </div> </section> </article> </div> <!-- ======================================== RELATED POSTS ======================================== --> <section class="sh-related-section"> <div class="sh-related-container"> <div class="sh-related-header"> <span class="sh-related-eyebrow">Keep Reading</span> <h2 class="sh-related-title">Related Articles</h2> </div> <div class="sh-related-grid"> <article class="sh-related-card"> <div class="sh-related-card-image" style="background-image: url('https://saleshive.com/wp-content/uploads/2025/03/b2b-lead-generation-seo-optimization-tactics-featured-image-600x343.jpg');"></div> <div class="sh-related-card-content"> <a href="https://saleshive.com/blog/?category=search-engine-optimization" class="sh-related-card-category" data-wpel-link="internal"> Search Engine Optimization </a> <h3 class="sh-related-card-title"> <a href="https://saleshive.com/blog/b2b-lead-generation-seo-optimization-tactics/" data-wpel-link="internal">SEO Optimization Tactics for B2B Lead Generation</a> </h3> <span class="sh-related-card-date">Mar 21, 2025</span> </div> </article> <article class="sh-related-card"> <div class="sh-related-card-image" style="background-image: url('https://saleshive.com/wp-content/uploads/2025/03/b2b-ab-testing-seo-titles-traffic-data-driven-guide-featured-image-600x343.jpg');"></div> <div class="sh-related-card-content"> <a href="https://saleshive.com/blog/?category=search-engine-optimization" class="sh-related-card-category" data-wpel-link="internal"> Search Engine Optimization </a> <h3 class="sh-related-card-title"> <a href="https://saleshive.com/blog/b2b-ab-testing-seo-titles-traffic-data-driven-guide/" data-wpel-link="internal">A/B Testing SEO Titles for B2B Traffic: A Data-Driven Guide</a> </h3> <span class="sh-related-card-date">Mar 18, 2025</span> </div> </article> <article class="sh-related-card"> <div class="sh-related-card-image" style="background-image: url('https://saleshive.com/wp-content/uploads/2025/03/b2b-content-creation-outsourcing-blogs-featured-image-600x343.jpg');"></div> <div class="sh-related-card-content"> <a href="https://saleshive.com/blog/?category=search-engine-optimization" class="sh-related-card-category" data-wpel-link="internal"> Search Engine Optimization </a> <h3 class="sh-related-card-title"> <a href="https://saleshive.com/blog/b2b-content-creation-outsourcing-blogs/" data-wpel-link="internal">Content Creation: Outsourcing B2B Blogs</a> </h3> <span class="sh-related-card-date">Mar 18, 2025</span> </div> </article> </div> </div> </section> <script> // Sticky TOC with Active State document.addEventListener('DOMContentLoaded', function() { const tocLinks = document.querySelectorAll('.sh-toc-link'); const toc = document.querySelector('.sh-toc'); const tocSidebar = document.querySelector('.sh-toc-sidebar'); const postContent = document.querySelector('.sh-post-content'); const headings = []; // Don't run on mobile/tablet if (!toc || !tocSidebar || !postContent || window.innerWidth <= 1024) return; tocLinks.forEach(link => { const id = link.getAttribute('href').substring(1); const heading = document.getElementById(id); if (heading) { headings.push({ id, element: heading, link }); } }); // Calculate initial positions const sidebarTop = tocSidebar.offsetTop; const headerHeight = 100; let isHidden = false; let isFixed = false; function handleScroll() { // Don't run sticky logic on mobile if (window.innerWidth <= 1024) { toc.style.position = ''; toc.style.width = ''; toc.style.top = ''; toc.style.left = ''; toc.style.display = 'block'; return; } const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const documentHeight = document.documentElement.scrollHeight; const windowHeight = window.innerHeight; const distanceFromBottom = documentHeight - (scrollTop + windowHeight); // Hide if within 1700px of bottom if (distanceFromBottom < 1700) { if (!isHidden) { toc.style.display = 'none'; isHidden = true; isFixed = false; } } else if (scrollTop > sidebarTop - headerHeight) { // Stick to top while in content if (!isFixed || isHidden) { const sidebarRect = tocSidebar.getBoundingClientRect(); toc.style.display = 'block'; toc.style.position = 'fixed'; toc.style.top = headerHeight + 'px'; toc.style.width = tocSidebar.offsetWidth + 'px'; toc.style.left = sidebarRect.left + 'px'; isFixed = true; isHidden = false; } } else { // Reset to normal position at top if (isFixed || isHidden) { toc.style.display = 'block'; toc.style.position = ''; toc.style.width = ''; toc.style.top = ''; toc.style.left = ''; isFixed = false; isHidden = false; } } // Update active link let current = ''; const scrollPos = scrollTop + 150; headings.forEach(({ id, element }) => { if (element.offsetTop <= scrollPos) { current = id; } }); tocLinks.forEach(link => { link.classList.remove('active'); if (link.getAttribute('href') === '#' + current) { link.classList.add('active'); } }); } window.addEventListener('scroll', handleScroll); window.addEventListener('resize', handleScroll); handleScroll(); // Smooth scroll for TOC links tocLinks.forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); const id = this.getAttribute('href').substring(1); const target = document.getElementById(id); if (target) { window.scrollTo({ top: target.offsetTop - 100, behavior: 'smooth' }); } }); }); // FAQ Accordion document.querySelectorAll('.sh-post-faq-question').forEach(question => { question.addEventListener('click', function() { const item = this.closest('.sh-post-faq-item'); item.classList.toggle('active'); }); }); }); </script> <script nitro-exclude> document.cookie = 'nitroCachedPage=' + (!window.NITROPACK_STATE ? '0' : '1') + '; path=/; SameSite=Lax'; </script> <script nitro-exclude> if (!window.NITROPACK_STATE || window.NITROPACK_STATE != 'FRESH') { var proxyPurgeOnly = 0; if (typeof navigator.sendBeacon !== 'undefined') { var nitroData = new FormData(); nitroData.append('nitroBeaconUrl', 'aHR0cHM6Ly9zYWxlc2hpdmUuY29tL2Jsb2cvc2VvLW1ldGEtZGF0YS1iZXN0LXByYWN0aWNlcy1yYW5raW5ncy0yMDI1Lw=='); nitroData.append('nitroBeaconCookies', 'W10='); nitroData.append('nitroBeaconHash', '0ad87ef7644f421a2dda713c159583e94e1efa129e29e7a7eb323ba301ab29dabc0b8514534af5ed5c7320161b61e09b7151ab43f5b9bb609c5e9811f15797c7'); nitroData.append('proxyPurgeOnly', ''); nitroData.append('layout', 'post'); navigator.sendBeacon(location.href, nitroData); } else { var xhr = new XMLHttpRequest(); xhr.open('POST', location.href, true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('nitroBeaconUrl=aHR0cHM6Ly9zYWxlc2hpdmUuY29tL2Jsb2cvc2VvLW1ldGEtZGF0YS1iZXN0LXByYWN0aWNlcy1yYW5raW5ncy0yMDI1Lw==&nitroBeaconCookies=W10=&nitroBeaconHash=0ad87ef7644f421a2dda713c159583e94e1efa129e29e7a7eb323ba301ab29dabc0b8514534af5ed5c7320161b61e09b7151ab43f5b9bb609c5e9811f15797c7&proxyPurgeOnly=&layout=post'); } } </script> </main><!-- .sh-main --> <style> /* ===================================================== FOOTER STYLES Highlight color CSS variables are output in <head> via wp_head to prevent flash of default color before footer loads ===================================================== */ /* Footer base styles - hidden by default, revealed via floating bar toggle */ .sh-footer { position: relative; background: var(--sh-bg-primary); border-top: 1px solid var(--sh-border-subtle); max-height: 0; overflow: hidden; opacity: 0; transition: max-height 0.5s ease, opacity 0.4s ease; } /* ===================================================== LIGHT MODE - Component-specific overrides (Only for elements that need special treatment) ===================================================== */ /* Background for light mode - white base, same mesh colors */ body.light-mode .sh-global-bg { background: var(--sh-bg-base); } /* Mesh canvas uses same 30% opacity in both modes */ body.light-mode .sh-bg-overlay { background: var(--sh-bg-overlay); } /* Header glass effect for light mode */ body.light-mode .sh-header { background: var(--sh-bg-glass); border-bottom: 1px solid var(--sh-border-default); } </style> <!-- Global Site Background - Mesh Gradient --> <div class="sh-global-bg"> <div class="sh-mesh-gradient"></div> <div class="sh-bg-overlay"></div> </div> <style> /* ===================================================== GLOBAL SITE BACKGROUND - STRIPE MESH GRADIENT ===================================================== */ .sh-global-bg { position: fixed; top: 0; left: 0; width: 100%; height: 110vh; /* Extra tall to cover expanded viewport on iOS */ z-index: -2; overflow: hidden; pointer-events: none; background: var(--sh-bg-base); } .sh-mesh-gradient { position: fixed; top: 0; left: 0; width: 100%; height: 110vh; z-index: -1; background: /* Primary glow - top right corner - deep purple #360947 */ radial-gradient(ellipse 140% 100% at 100% 0%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.7) 0%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.3) 35%, transparent 70%), /* Bottom left - deep purple #360947 */ radial-gradient(ellipse 100% 120% at 0% 100%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.7) 0%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.3) 40%, transparent 65%), /* Subtle center highlight - purple */ radial-gradient(ellipse 80% 80% at 50% 50%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.15) 0%, transparent 60%), /* Base - black */ #000000; } .sh-bg-overlay { display: none; /* Overlay removed */ } /* Prevent horizontal overflow from any element */ html, body { overflow-x: hidden; max-width: 100%; } /* Make main content wrapper transparent by default */ .sh-site { background: transparent !important; } .sh-main { background: transparent !important; } /* All sections transparent by default */ .sh-section, .sh-hero, .sh-logos, .sh-cta, .sh-service-hero, .sh-sample-call-section, .sh-platform-showcase, .sh-ph-hero, .sh-list-hero, .sh-list-cta, .sh-pricing-hero, .sh-pricing-main, .sh-comparison-section, .sh-sdr-hero, .sh-cost-section, .sh-compare-section, .sh-email-hero, .sh-campaign-types, .sh-inbox-section, .sh-infrastructure, .sh-emod-section, .sh-email-deliverability, .sh-email-cta { background: transparent !important; } /* CTA color variants - transparent */ .sh-cta-blue, .sh-cta-green, .sh-cta-purple { background: transparent !important; } .sh-cta-blue::before, .sh-cta-green::before, .sh-cta-purple::before { display: none !important; } /* Override any gradient/dark section backgrounds */ .sh-section-dark, .sh-section-gradient { background: transparent !important; } /* Remove ::before pseudo-element backgrounds on service pages */ .sh-service-hero::before, .sh-sample-call-section::before, .sh-stats-banner::before, .sh-ph-hero::before, .sh-list-hero::before, .sh-list-cta::before, .sh-pricing-hero::before, .sh-sdr-hero::before, .sh-email-hero::before, .sh-campaign-types::before, .sh-inbox-section::before, .sh-emod-section::before, .sh-email-deliverability::before, .sh-email-cta::before { background: transparent !important; display: none !important; } /* Remove overflow clipping that creates visible edges */ .sh-service-hero, .sh-sample-call-section, .sh-ph-hero, .sh-list-hero, .sh-list-cta, .sh-pricing-hero, .sh-pricing-main, .sh-comparison-section, .sh-sdr-hero, .sh-cost-section, .sh-compare-section, .sh-email-hero, .sh-campaign-types, .sh-inbox-section, .sh-infrastructure, .sh-emod-section, .sh-email-deliverability, .sh-email-cta { overflow: visible !important; background: none !important; background-color: transparent !important; background-image: none !important; } /* Transparent section base class (for explicit use) */ .sh-section-transparent { background: transparent !important; } /* Semi-transparent gradient overlay */ .sh-section-overlay { position: relative; background: transparent !important; } .sh-section-overlay::before { content: ''; position: absolute; inset: 0; background: var(--sh-bg-overlay); pointer-events: none; z-index: 0; } .sh-section-overlay > * { position: relative; z-index: 1; } /* Light mode: use white overlay instead of dark */ body.light-mode .sh-section-overlay::before { background: var(--sh-bg-overlay); } /* Transparent Hero */ .sh-hero-transparent { background: transparent !important; } .sh-hero-transparent::before { display: none !important; } /* Hide any hero orbs when using global bg */ .sh-hero-transparent .sh-hero-orb { display: none; } @media (max-width: 768px) { .sh-global-orb-1 { width: 300px; height: 300px; } .sh-global-orb-2 { width: 250px; height: 250px; } .sh-global-orb-3 { width: 200px; height: 200px; } } </style> <!-- ======================================== CLIENTS + TESTIMONIALS (Global Footer) ======================================== --> <style> /* Combined Clients + Testimonials Section */ .sh-clients-testimonials-2 { padding: 80px 0 60px; overflow: hidden; background: var(--sh-bg-card) !important; } .sh-clients-testimonials-2 .sh-section-header { margin-bottom: 50px; max-width: 100%; } /* Testimonial Fader - Quote Only */ .sh-ct-testimonial-wrapper { max-width: 1000px; margin: 0 auto 15px; padding: 0 80px; position: relative; } .sh-ct-quote-container { position: relative; min-height: 80px; display: flex; align-items: center; justify-content: center; } .sh-ct-quote-item { position: absolute; top: 0; left: 0; right: 0; opacity: 0; visibility: hidden; transition: opacity 1s ease-in-out, visibility 1s ease-in-out; text-align: center; padding: 0 20px; } .sh-ct-quote-item.active { opacity: 1; visibility: visible; } .sh-ct-quote-marks { position: absolute; font-size: 100px; font-family: Georgia, serif; color: var(--sh-page-highlight); opacity: 0.35; line-height: 1; pointer-events: none; } .sh-ct-quote-marks.open { top: -20px; left: 2%; } .sh-ct-quote-marks.close { bottom: -10px; right: 2%; } .sh-ct-quote-text { font-size: 24px; line-height: 1.6; color: var(--sh-text-primary); font-style: italic; font-weight: 500; margin: 0; position: relative; z-index: 1; letter-spacing: -0.3px; } /* Logos Marquee */ .sh-ct-logos-wrapper { overflow: hidden; mask-image: linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%); -webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 90%, transparent 100%); } .sh-ct-logos-marquee { position: relative; width: 100%; padding: 20px 0; } .sh-ct-logos-track { display: flex; gap: 20px; animation: ctMarqueeScroll 40s linear infinite; width: max-content; } @keyframes ctMarqueeScroll { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } } .sh-ct-logo-item { flex-shrink: 0; padding: 18px 36px; cursor: default; } .sh-ct-logo-item span { font-size: 17px; font-weight: 600; color: var(--sh-text-secondary); letter-spacing: 0.5px; white-space: nowrap; transition: color 0.3s ease; } @media (max-width: 768px) { .sh-clients-testimonials-2 { padding: 60px 0 40px; } .sh-ct-testimonial-wrapper { padding: 0 20px; margin-bottom: 20px; } .sh-ct-quote-container { min-height: 100px; } .sh-ct-quote-text { font-size: 18px; } .sh-ct-quote-marks { font-size: 60px; } .sh-ct-quote-marks.open { top: -15px; left: 0; } .sh-ct-logo-item { padding: 14px 24px; } .sh-ct-logo-item span { font-size: 15px; } .sh-ct-logos-track { animation-duration: 30s; } } </style> <section class="sh-section sh-clients-testimonials-2" id="sh-clients-testimonials-694295682e562"> <div class="sh-section-header"> <span class="sh-section-eyebrow">Our Clients</span> <h2 class="sh-section-title no-text-reveal-off">Trusted by Top B2B Companies</h2> <p class="sh-section-subtitle">From fast-growing startups to Fortune 500 companies, we've helped them all book more meetings.</p> </div> <div class="sh-ct-logos-wrapper"> <div class="sh-ct-logos-marquee"> <div class="sh-ct-logos-track"> <div class="sh-ct-logo-item"><span>Shopify</span></div> <div class="sh-ct-logo-item"><span>Siemens</span></div> <div class="sh-ct-logo-item"><span>Otter.ai</span></div> <div class="sh-ct-logo-item"><span>Mrs. Fields</span></div> <div class="sh-ct-logo-item"><span>Revenue.io</span></div> <div class="sh-ct-logo-item"><span>GigXR</span></div> <div class="sh-ct-logo-item"><span>SimpliSafe</span></div> <div class="sh-ct-logo-item"><span>Zoho</span></div> <div class="sh-ct-logo-item"><span>InsightRX</span></div> <div class="sh-ct-logo-item"><span>Dext</span></div> <div class="sh-ct-logo-item"><span>YouGov</span></div> <div class="sh-ct-logo-item"><span>Mostly AI</span></div> <!-- Duplicate for seamless loop --> <div class="sh-ct-logo-item"><span>Shopify</span></div> <div class="sh-ct-logo-item"><span>Siemens</span></div> <div class="sh-ct-logo-item"><span>Otter.ai</span></div> <div class="sh-ct-logo-item"><span>Mrs. Fields</span></div> <div class="sh-ct-logo-item"><span>Revenue.io</span></div> <div class="sh-ct-logo-item"><span>GigXR</span></div> <div class="sh-ct-logo-item"><span>SimpliSafe</span></div> <div class="sh-ct-logo-item"><span>Zoho</span></div> <div class="sh-ct-logo-item"><span>InsightRX</span></div> <div class="sh-ct-logo-item"><span>Dext</span></div> <div class="sh-ct-logo-item"><span>YouGov</span></div> <div class="sh-ct-logo-item"><span>Mostly AI</span></div> </div> </div> </div> </section> <!-- ======================================== INLINE BOOKING SECTION (Global Footer) ======================================== --> <section class="sh-section sh-booking-section"> <div class="sh-section-header"> <h2 class="sh-section-title">Ready to Scale Your Pipeline?</h2> <p class="sh-section-subtitle">Learn how we have helped hundreds of B2B companies scale their sales.</p> </div> <div class="sh-inline-booking sh-parallax" id="sh-booking-1131" data-form-type="Footer" data-autofocus="false" data-url-date="" data-url-firstname="" data-url-lastname="" data-url-company="" data-url-email="" data-url-title=""> <div class="sh-inline-booking-topbar"> <span class="sh-booking-pulse-dot"></span> SCHEDULE YOUR MEETING TODAY! <span class="sh-booking-pulse-dot"></span> </div> <!-- Mobile Step Indicator (4 steps) --> <div class="sh-mobile-steps-indicator sh-mobile-only"> <div class="sh-mobile-step-dot active" data-step="1"><span>1</span></div> <div class="sh-mobile-step-line"></div> <div class="sh-mobile-step-dot" data-step="2"><span>2</span></div> <div class="sh-mobile-step-line"></div> <div class="sh-mobile-step-dot" data-step="3"><span>3</span></div> <div class="sh-mobile-step-line"></div> <div class="sh-mobile-step-dot" data-step="4"><span>4</span></div> </div> <div class="sh-inline-booking-content"> <!-- STEP 1: Form (Mobile) / Form Side (Desktop) --> <div class="sh-inline-booking-form sh-mobile-step active" data-mobile-step="1"> <div class="sh-inline-form-header sh-desktop-only"> <div class="sh-inline-form-icon"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/> <line x1="16" y1="2" x2="16" y2="6"/> <line x1="8" y1="2" x2="8" y2="6"/> <line x1="3" y1="10" x2="21" y2="10"/> <polyline points="9 16 11 18 15 14"/> </svg> </div> <h3>Enter Your Details</h3> </div> <form class="sh-inline-form" novalidate autocomplete="on"> <div class="sh-inline-input-group sh-inline-email-group"> <input type="email" name="email" placeholder="Work Email*" required autocomplete="email" class="sh-inline-input"> <div class="sh-inline-loader"></div> </div> <div class="sh-inline-error sh-inline-error-email"></div> <div class="sh-inline-row"> <input type="text" name="firstName" placeholder="First Name*" required autocomplete="given-name" class="sh-inline-input"> <input type="text" name="lastName" placeholder="Last Name*" required autocomplete="family-name" class="sh-inline-input"> </div> <input type="text" name="title" placeholder="Job Title*" required autocomplete="organization-title" class="sh-inline-input"> <input type="text" name="company" placeholder="Company Name*" required autocomplete="organization" class="sh-inline-input"> <input type="hidden" name="phone" autocomplete="tel"> <p class="sh-inline-selected-time"></p> <!-- Desktop: Submit button --> <div class="sh-inline-submit-wrapper sh-desktop-only"> <button type="submit" class="sh-inline-submit" disabled> <span>BOOK MY CALL</span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg> </button> </div> <div class="sh-inline-error sh-inline-error-time sh-desktop-only"></div> <!-- Mobile: Next button --> <button type="button" class="sh-mobile-next-btn sh-mobile-only" disabled> <span>BOOK MY CALL</span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg> </button> </form> <!-- Success State (shown after booking on desktop) --> <div class="sh-inline-success sh-desktop-only" style="display: none;"> <div class="sh-inline-success-icon"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <polyline points="22 4 12 14.01 9 11.01"/> </svg> </div> <h3>You're All Set!</h3> <p class="sh-inline-success-details"></p> <p class="sh-inline-success-note">Check your email for confirmation details.</p> </div> </div> <!-- Calendar Side (Desktop) / Contains Steps 2 & 3 on Mobile --> <div class="sh-inline-booking-scheduler"> <div class="sh-inline-scheduler-header sh-desktop-only"> <h3>Select Your Meeting Date</h3> </div> <!-- Desktop Calendar View (shown first) --> <div class="sh-desktop-calendar-view sh-desktop-only"> <div class="sh-inline-booking-calendar"> <div class="sh-inline-cal-header"> <button type="button" class="sh-inline-cal-nav sh-inline-cal-prev" aria-label="Previous month"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <span class="sh-inline-cal-month"></span> <button type="button" class="sh-inline-cal-nav sh-inline-cal-next" aria-label="Next month"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg> </button> </div> <div class="sh-inline-cal-grid"> <div class="sh-inline-cal-weekdays"> <span>MON</span><span>TUE</span><span>WED</span><span>THU</span><span>FRI</span> </div> <div class="sh-inline-cal-days"></div> </div> </div> </div> <!-- Desktop Times View (replaces calendar after date selection) --> <div class="sh-desktop-times-view sh-desktop-only" style="display: none;"> <!-- Back to Calendar button with date --> <button type="button" class="sh-back-to-calendar"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> <span class="sh-selected-date-text"></span> </button> <!-- Timezone below date --> <div class="sh-inline-cal-timezone"> <label>Timezone:</label> <select class="sh-inline-tz-select sh-desktop-tz-select"></select> </div> <div class="sh-inline-slots-header">Available Times</div> <div class="sh-desktop-slots-container"> <div class="sh-inline-slots-placeholder"> <p>Loading times...</p> </div> </div> </div> <!-- Mobile Steps (unchanged) --> <div class="sh-inline-scheduler-content sh-mobile-only"> <!-- STEP 2: Pick a Day (Mobile) --> <div class="sh-inline-booking-calendar sh-mobile-step" data-mobile-step="2"> <div class="sh-mobile-step-header sh-mobile-only"> <button type="button" class="sh-mobile-back-btn" data-goto-step="1"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <h3>Pick a Day</h3> </div> <div class="sh-inline-cal-header"> <button type="button" class="sh-inline-cal-nav sh-inline-cal-prev-mobile" aria-label="Previous month"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <span class="sh-inline-cal-month-mobile"></span> <button type="button" class="sh-inline-cal-nav sh-inline-cal-next-mobile" aria-label="Next month"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg> </button> </div> <div class="sh-inline-cal-grid"> <div class="sh-inline-cal-weekdays"> <span>MON</span><span>TUE</span><span>WED</span><span>THU</span><span>FRI</span> </div> <div class="sh-inline-cal-days-mobile"></div> </div> <div class="sh-inline-cal-timezone"> <label>Timezone:</label> <select class="sh-mobile-tz-select"></select> </div> </div> <!-- STEP 3: Pick a Time (Mobile) --> <div class="sh-inline-cal-slots sh-mobile-step" data-mobile-step="3"> <div class="sh-mobile-step-header sh-mobile-only"> <button type="button" class="sh-mobile-back-btn" data-goto-step="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <h3>Pick a Time</h3> </div> <!-- Selected date display (mobile) --> <div class="sh-mobile-selected-date sh-mobile-only"> <span class="sh-mobile-date-text"></span> </div> <!-- Timezone selector (mobile) --> <div class="sh-mobile-timezone sh-mobile-only"> <label>Timezone:</label> <select class="sh-mobile-tz-select-slots"></select> </div> <div class="sh-inline-slots-placeholder"> <p>Select a date</p> </div> </div> </div> </div> <!-- STEP 4: Confirm (Mobile Only) --> <div class="sh-mobile-confirm-step sh-mobile-step sh-mobile-only" data-mobile-step="4"> <div class="sh-mobile-step-header"> <button type="button" class="sh-mobile-back-btn" data-goto-step="3"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <h3>Confirm</h3> </div> <div class="sh-mobile-confirm-card"> <p class="sh-mobile-confirm-date"></p> <p class="sh-mobile-confirm-time"></p> </div> <button type="button" class="sh-mobile-confirm-btn"> <span>BOOK MY CALL</span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg> </button> <!-- Mobile Success State --> <div class="sh-mobile-success" style="display: none;"> <div class="sh-inline-success-icon"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <polyline points="22 4 12 14.01 9 11.01"/> </svg> </div> <h3>You're All Set!</h3> <p class="sh-mobile-success-details"></p> <p class="sh-inline-success-note">Check your email for confirmation.</p> </div> </div> </div> </div> <style> /* Inline Booking Shortcode Styles */ .sh-inline-booking { background:rgb(216, 216, 216); border: 0px solid var(--sh-border-default); border-radius: 16px; padding: 0; margin: 20px auto; max-width: 700px; overflow: hidden; } .sh-inline-booking-topbar { background: #000000; color: #ffffff; text-align: center; padding: 14px 20px; font-size: 14px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 28px; display: flex; align-items: center; justify-content: center; gap: 10px; } .sh-booking-pulse-dot { width: 8px; height: 8px; background: var(--sh-page-highlight); border-radius: 50%; animation: sh-booking-pulse 1.5s ease-in-out infinite; box-shadow: 0 0 8px var(--sh-page-highlight); } @keyframes sh-booking-pulse { 0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 8px var(--sh-page-highlight); } 50% { opacity: 0.4; transform: scale(0.8); box-shadow: 0 0 4px var(--sh-page-highlight); } } .sh-inline-booking-header { text-align: center; padding: 32px 32px 0; margin-bottom: 32px; } .sh-inline-booking-header strong { color: var(--sh-page-highlight); } .sh-inline-booking-title { font-size: 28px; font-weight: 700; color: #1a1a2e; margin: 0 0 8px; } .sh-inline-booking-subtitle { font-size: 16px; color: rgba(0, 0, 0, 0.7); margin: 0; } .sh-inline-booking-content { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; align-items: stretch; padding: 0 32px 32px; } @media (max-width: 900px) { .sh-inline-booking-content { grid-template-columns: 1fr; } } @media (max-width: 768px) { .sh-inline-booking { padding: 0; margin: 10px; border-radius: 12px; } } /* Scheduler Container (Calendar + Times) */ .sh-inline-booking-scheduler { background: #e9eaec; border-radius: 12px; padding: 20px; } .sh-inline-scheduler-header h3 { font-size: 18px; font-weight: 600; color: #1a1a2e; margin: 0 0 20px; text-align: center; } .sh-inline-scheduler-content { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } @media (max-width: 700px) { .sh-inline-scheduler-content { grid-template-columns: 1fr; } } /* Desktop Two-Step Calendar Flow */ .sh-desktop-calendar-view, .sh-desktop-times-view { width: 100%; } /* Back to Calendar button */ .sh-back-to-calendar { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 8px 12px; margin-bottom: 10px; background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.1); border: 1px solid rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.3); border-radius: 8px; color: var(--sh-page-highlight); font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .sh-back-to-calendar:hover { background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.2); } .sh-back-to-calendar svg { width: 18px; height: 18px; } /* Desktop slots container */ .sh-desktop-slots-container { max-height: 220px; overflow-y: auto; } .sh-desktop-slots-container .sh-inline-slots-grid { display: flex; flex-direction: column; gap: 8px; } .sh-desktop-times-view .sh-back-to-calendar { margin-bottom: 8px; } .sh-desktop-times-view .sh-inline-cal-timezone { margin-bottom: 10px; } .sh-desktop-times-view .sh-inline-cal-timezone label { font-size: 12px; } .sh-desktop-times-view .sh-inline-tz-select { padding: 6px 10px; font-size: 12px; } /* Calendar Styles */ .sh-inline-booking-calendar { /* No extra background since parent has it */ } .sh-inline-cal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } .sh-inline-cal-month, .sh-inline-cal-month-mobile { font-size: 14px; font-weight: 600; color: #1a1a2e; } .sh-inline-cal-nav { width: 32px; height: 32px; border: 1px solid rgba(0, 0, 0, 0.12); background: rgb(216, 216, 216); border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .sh-inline-cal-nav:hover { background: #f0f1f3; } .sh-inline-cal-nav svg { width: 18px; height: 18px; color: rgba(0, 0, 0, 0.7); } .sh-inline-cal-weekdays { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; margin-bottom: 8px; } .sh-inline-cal-weekdays span { text-align: center; font-size: 11px; font-weight: 600; color: rgba(0, 0, 0, 0.5); text-transform: uppercase; padding: 8px 0; } .sh-inline-cal-days, .sh-inline-cal-days-mobile { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; } .sh-inline-cal-day { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; font-size: 14px; border-radius: 8px; cursor: pointer; transition: all 0.2s; background: transparent; border: none; color: #1a1a2e; } .sh-inline-cal-day:hover:not(.disabled):not(.selected) { background: #f0f1f3; } .sh-inline-cal-day.disabled { color: rgba(0, 0, 0, 0.5); cursor: not-allowed; } .sh-inline-cal-day.selected { background: var(--sh-page-highlight); color: var(--sh-text-inverted); } /* Today is disabled - no special styling needed */ .sh-inline-cal-timezone { margin-top: 16px; display: flex; align-items: center; gap: 8px; font-size: 13px; color: #555555 !important; /* Hardcoded - no theme change */ } .sh-inline-cal-timezone label { color: #555555 !important; /* Hardcoded - no theme change */ } .sh-inline-tz-select { flex: 1; padding: 8px 12px; background: #ffffff !important; /* Hardcoded - no theme change */ border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: 8px; color: #1a1a2e !important; /* Hardcoded - no theme change */ font-size: 13px; } .sh-inline-cal-slots { display: flex; flex-direction: column; } .sh-inline-slots-header { font-size: 13px; font-weight: 600; color: #1a1a2e; margin-bottom: 8px; } .sh-inline-slots-placeholder { text-align: center; padding: 40px 20px; color: rgba(0, 0, 0, 0.5); font-size: 14px; background: #ffffff; border-radius: 8px; flex: 1; display: flex; align-items: center; justify-content: center; } .sh-inline-slots-grid { display: flex; flex-direction: column; gap: 8px; max-height: 280px; overflow-y: auto; } .sh-inline-slot { padding: 12px 16px; background: #ffffff; border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 8px; font-size: 14px; color: #1a1a2e; cursor: pointer; transition: all 0.2s; text-align: center; font-weight: 500; } .sh-inline-slot:hover { border-color: var(--sh-page-highlight); background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.1); } .sh-inline-slot.selected { background: var(--sh-page-highlight); border-color: var(--sh-page-highlight); color: var(--sh-text-inverted); } .sh-inline-selected-time { display: none !important; } .sh-inline-slots-loading { text-align: center; padding: 20px; color: rgba(0, 0, 0, 0.5); } .sh-inline-spinner { width: 24px; height: 24px; border: 2px solid rgba(0, 0, 0, 0.12); border-top-color: var(--sh-page-highlight); border-radius: 50%; animation: sh-inline-spin 0.8s linear infinite; margin: 0 auto 8px; } @keyframes sh-inline-spin { to { transform: rotate(360deg); } } /* Form Styles */ .sh-inline-booking-form { display: flex; flex-direction: column; background: #e9eaec; border-radius: 12px; padding: 20px; } .sh-inline-form-header { margin-bottom: 16px; text-align: center; } .sh-inline-form-icon { width: 48px; height: 48px; margin: 0 auto 12px; background: var(--sh-page-highlight); border-radius: 50%; display: flex; align-items: center; justify-content: center; } .sh-inline-form-icon svg { width: 24px; height: 24px; color: var(--sh-text-inverted); } .sh-inline-form-header h3 { font-size: 18px; font-weight: 600; color: #1a1a2e; margin: 0; } .sh-inline-form { display: flex; flex-direction: column; gap: 10px; } .sh-inline-input-group { position: relative; } .sh-inline-loader { position: absolute; right: 12px; top: 50%; margin-top: -9px; /* Half of height to center vertically */ width: 18px; height: 18px; border: 2px solid rgba(0, 0, 0, 0.12); border-top-color: var(--sh-page-highlight); border-radius: 50%; animation: sh-inline-spin 0.8s linear infinite; display: none; } .sh-inline-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .sh-inline-input { width: 100%; padding: 14px 16px; background: #f0f1f3; border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 10px; font-size: 14px; color: #1a1a2e; transition: border-color 0.2s, box-shadow 0.2s; box-sizing: border-box; } .sh-inline-input::placeholder { color: rgba(0, 0, 0, 0.5); } /* Fix browser autofill styling */ .sh-inline-input:-webkit-autofill, .sh-inline-input:-webkit-autofill:hover, .sh-inline-input:-webkit-autofill:focus, .sh-inline-input:-webkit-autofill:active { -webkit-box-shadow: 0 0 0 30px #f0f1f3 inset !important; -webkit-text-fill-color: #1a1a2e !important; caret-color: #1a1a2e; transition: background-color 5000s ease-in-out 0s; animation-name: onAutoFillStart; } /* Animation to detect autofill */ @keyframes onAutoFillStart { from { /* empty */ } to { /* empty */ } } .sh-inline-input:focus { outline: none; border-color: var(--sh-page-highlight); box-shadow: 0 0 0 3px rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.15); } .sh-inline-input.error { border-color: var(--sh-red); } .sh-inline-error { font-size: 12px; color: var(--sh-red); min-height: 0; margin-top: -4px; } .sh-inline-error:empty { display: none; } .sh-inline-error-time { margin-top: 8px; font-size: 13px; font-weight: 500; text-align: center; } @keyframes sh-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-8px); } 40% { transform: translateX(8px); } 60% { transform: translateX(-6px); } 80% { transform: translateX(6px); } } .sh-inline-submit { width: 100%; padding: 16px 24px; margin-top: 8px; background: var(--sh-page-highlight); border: none; border-radius: 10px; font-size: 16px; font-weight: 600; color: var(--sh-text-inverted); cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; } .sh-inline-submit:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.4); } .sh-inline-submit-wrapper { position: relative; width: 100%; } .sh-inline-submit:disabled { cursor: not-allowed; pointer-events: none; /* Keep button eye-catching even when disabled */ } /* Make wrapper clickable when button is disabled */ .sh-inline-submit-wrapper:has(.sh-inline-submit:disabled) { cursor: pointer; } .sh-inline-submit svg { width: 20px; height: 20px; } /* Success State */ .sh-inline-success { text-align: center; padding: 40px 20px; } .sh-inline-success-icon { width: 64px; height: 64px; margin: 0 auto 20px; background: rgba(var(--sh-green-rgb, 34, 197, 94), 0.15); border-radius: 50%; display: flex; align-items: center; justify-content: center; } .sh-inline-success-icon svg { width: 32px; height: 32px; color: var(--sh-green); } .sh-inline-success h3 { font-size: 24px; font-weight: 600; color: #1a1a2e; margin: 0 0 12px; } .sh-inline-success-details { font-size: 16px; color: var(--sh-page-highlight); margin: 0 0 8px; } .sh-inline-success-note { font-size: 14px; color: rgba(0, 0, 0, 0.7); margin: 0; } /* ===================================================== MOBILE STEP-BY-STEP FLOW (3 Steps) ===================================================== */ /* Hide mobile-only elements on desktop */ .sh-mobile-only { display: none !important; } /* Hide desktop-only elements on mobile */ @media (max-width: 768px) { .sh-desktop-only { display: none !important; } .sh-mobile-only { display: block !important; } /* Mobile step indicator - hidden on step 1 to reduce friction */ .sh-mobile-steps-indicator { display: flex !important; align-items: center; justify-content: center; gap: 12px; padding: 20px 20px 0; margin-bottom: 16px; } /* Hide step indicator on first step to not scare users */ .sh-inline-booking:has(.sh-inline-booking-form.active) .sh-mobile-steps-indicator { display: none !important; } .sh-mobile-step-dot { width: 28px; height: 28px; border-radius: 50%; background: #e9eaec; border: 2px solid rgba(0, 0, 0, 0.12); display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: rgba(0, 0, 0, 0.5); transition: all 0.2s; } .sh-mobile-step-dot.active { background: var(--sh-page-highlight); border-color: var(--sh-page-highlight); color: var(--sh-text-inverted); } .sh-mobile-step-dot.completed { background: var(--sh-green); border-color: var(--sh-green); color: var(--sh-text-inverted); } .sh-mobile-step-line { width: 32px; height: 2px; background: rgba(0, 0, 0, 0.12); } /* Mobile step containers */ .sh-mobile-step { display: none !important; } .sh-mobile-step.active { display: block !important; } /* Override booking content grid on mobile */ .sh-inline-booking-content { display: block; } /* Mobile step headers */ .sh-mobile-step-header { display: flex !important; align-items: center; gap: 12px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid rgba(0, 0, 0, 0.06); } .sh-mobile-step-header h3 { font-size: 16px; font-weight: 600; color: #1a1a2e; margin: 0; flex: 1; } /* Mobile back button */ .sh-mobile-back-btn { width: 32px; height: 32px; padding: 0; background: transparent; border: none; border-radius: 8px; color: rgba(0, 0, 0, 0.7); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; flex-shrink: 0; } .sh-mobile-back-btn:hover, .sh-mobile-back-btn:active { background: #e9eaec; color: #1a1a2e; } .sh-mobile-back-btn svg { width: 20px; height: 20px; } /* Mobile next button */ .sh-mobile-next-btn { width: 100%; padding: 14px 20px; margin-top: 8px; background: var(--sh-page-highlight); border: none; border-radius: 10px; font-size: 15px; font-weight: 600; color: var(--sh-text-inverted); cursor: pointer; display: flex !important; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; } .sh-mobile-next-btn:disabled { cursor: not-allowed; /* Keep button eye-catching even when disabled */ } .sh-mobile-next-btn svg { width: 18px; height: 18px; } /* Scheduler container on mobile */ .sh-inline-booking-scheduler { display: contents; /* Let children flow into parent grid */ } .sh-inline-booking-scheduler .sh-inline-scheduler-content { display: contents; } /* Step 2: Calendar */ .sh-inline-booking-calendar.active { display: block !important; background: #e9eaec; border-radius: 12px; padding: 16px; } /* Step 3: Time Slots */ .sh-inline-cal-slots.active { display: block !important; background: #e9eaec; border-radius: 12px; padding: 16px; } /* Show mobile elements inside time slots */ .sh-inline-cal-slots .sh-mobile-step-header { display: flex !important; } .sh-inline-cal-slots .sh-mobile-selected-date { display: block !important; } .sh-inline-cal-slots .sh-mobile-timezone { display: flex !important; } /* Hide desktop-only header inside time slots on mobile */ .sh-inline-cal-slots .sh-inline-slots-header.sh-desktop-only { display: none !important; } /* Mobile selected date display */ .sh-mobile-selected-date { background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.1); border: 1px solid rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.3); border-radius: 10px; padding: 12px 16px; margin-bottom: 12px; text-align: center; } .sh-mobile-date-text { font-size: 15px; font-weight: 600; color: var(--sh-page-highlight); } /* Mobile timezone selector - Hardcoded colors */ .sh-mobile-timezone { align-items: center; gap: 8px; margin-bottom: 16px; font-size: 14px; color: #555555 !important; } .sh-mobile-timezone label { flex-shrink: 0; color: #555555 !important; } .sh-mobile-tz-select { flex: 1; padding: 10px 12px; background: #f0f1f3 !important; border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: 8px; color: #1a1a2e !important; font-size: 16px; /* Prevent iOS zoom */ } /* Full width time slots with scrolling */ .sh-inline-cal-slots .sh-inline-slots-grid { display: flex; flex-direction: column; gap: 10px; max-height: 45vh; overflow-y: auto; padding-right: 4px; -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ } .sh-inline-cal-slots .sh-inline-slot { width: 100%; padding: 14px 16px; font-size: 16px; flex-shrink: 0; /* Prevent slots from shrinking */ } /* Prevent iOS zoom on input focus - must be 16px minimum */ .sh-inline-input { font-size: 16px !important; } .sh-inline-tz-select { font-size: 16px !important; } /* Full width form on mobile */ .sh-inline-booking { margin: 0; border-radius: 0; border-left: none; border-right: none; max-width: 100%; } .sh-inline-booking-topbar { padding: 12px 16px; font-size: 13px; letter-spacing: 1px; margin-bottom: 0px; } .sh-inline-booking-header { padding: 20px 20px 0; margin-bottom: 20px; } .sh-inline-booking-content { padding: 0 20px 20px; } .sh-inline-booking-form { padding: 16px; } /* Stack first/last name on mobile */ .sh-inline-row { grid-template-columns: 1fr; } /* Mobile confirm step */ .sh-mobile-confirm-step { padding: 0; } .sh-mobile-confirm-card { padding: 16px; background: #e9eaec; border-radius: 12px; margin-bottom: 16px; text-align: center; } .sh-mobile-confirm-date { font-size: 15px; font-weight: 600; color: #1a1a2e; margin: 0 0 4px; } .sh-mobile-confirm-time { font-size: 18px; color: var(--sh-page-highlight); margin: 0; font-weight: 700; } .sh-mobile-confirm-btn { width: 100%; padding: 14px 20px; background: var(--sh-page-highlight); border: none; border-radius: 10px; font-size: 15px; font-weight: 600; color: var(--sh-text-inverted); cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; } .sh-mobile-confirm-btn:disabled { opacity: 0.5; cursor: not-allowed; } .sh-mobile-confirm-btn svg { width: 18px; height: 18px; } /* Mobile success state */ .sh-mobile-success { text-align: center; padding: 32px 16px; } .sh-mobile-success h3 { font-size: 20px; font-weight: 600; color: #1a1a2e; margin: 0 0 8px; } .sh-mobile-success-details { font-size: 14px; color: var(--sh-page-highlight); margin: 0 0 4px; } /* Hide form header on mobile */ .sh-inline-form-header { display: none; } } /* ===================================================== MODAL CONTEXT OVERRIDES When shortcode is inside the booking modal ===================================================== */ .sh-modal-inner .sh-inline-booking { margin: 0; max-width: none; } .sh-modal-inner .sh-inline-booking-header { display: none; } /* Modal: ensure desktop views display properly */ .sh-modal-inner .sh-desktop-calendar-view, .sh-modal-inner .sh-desktop-times-view { min-height: 320px; } .sh-modal-inner .sh-desktop-slots-container { max-height: 200px; } </style> <script> // Wait for shBooking to be available (loaded in footer) (function initBookingWidget() { if (typeof window.shBooking === 'undefined') { // shBooking not ready yet, wait and retry setTimeout(initBookingWidget, 100); return; } const container = document.getElementById('sh-booking-1131'); if (!container) return; // DOM Elements - Desktop const calDays = container.querySelector('.sh-inline-cal-days'); const calMonth = container.querySelector('.sh-inline-cal-month'); const calPrev = container.querySelector('.sh-inline-cal-prev'); const calNext = container.querySelector('.sh-inline-cal-next'); const tzSelect = container.querySelector('.sh-inline-tz-select'); // Desktop two-step view elements const desktopCalendarView = container.querySelector('.sh-desktop-calendar-view'); const desktopTimesView = container.querySelector('.sh-desktop-times-view'); const backToCalendarBtn = container.querySelector('.sh-back-to-calendar'); const selectedDateText = container.querySelector('.sh-selected-date-text'); const desktopSlotsContainer = container.querySelector('.sh-desktop-slots-container'); const desktopTzSelect = container.querySelector('.sh-desktop-tz-select'); // DOM Elements - Mobile const calDaysMobile = container.querySelector('.sh-inline-cal-days-mobile'); const calMonthMobile = container.querySelector('.sh-inline-cal-month-mobile'); const calPrevMobile = container.querySelector('.sh-inline-cal-prev-mobile'); const calNextMobile = container.querySelector('.sh-inline-cal-next-mobile'); const calSlots = container.querySelector('.sh-inline-cal-slots'); const mobileTzSelect = container.querySelector('.sh-mobile-tz-select'); const mobileTzSelectSlots = container.querySelector('.sh-mobile-tz-select-slots'); // Form elements const form = container.querySelector('.sh-inline-form'); const emailInput = container.querySelector('input[name="email"]'); const emailLoader = container.querySelector('.sh-inline-loader'); const errorEmail = container.querySelector('.sh-inline-error-email'); const submitBtn = container.querySelector('.sh-inline-submit'); const selectedTimeDisplay = container.querySelector('.sh-inline-selected-time'); const successDiv = container.querySelector('.sh-inline-success'); const successDetails = container.querySelector('.sh-inline-success-details'); // State let currentMonth = new Date(); let selectedDate = null; let selectedSlot = null; let selectedCalendarId = null; let slotAvailability = {}; let zoomInfoLookedUp = false; let lastLookedUpEmail = ''; // Track which email was looked up let userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; let currentMobileStep = 1; // Session ID for lead deduplication (persists across form submissions) let sessionId = localStorage.getItem('sh_lead_session_id'); if (!sessionId) { sessionId = 'sh_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); localStorage.setItem('sh_lead_session_id', sessionId); } // Landing Page URL - capture the first page with ad parameters // This persists the original landing page URL even if user navigates to other pages let landingPageUrl = localStorage.getItem('sh_landing_page_url'); const currentUrl = window.location.href; const hasAdParams = /[?&](gclid|hsa_|utm_|gbraid|wbraid)/i.test(currentUrl); // If current URL has ad params, always update (they may have clicked a new ad) // Otherwise, keep the stored landing page or use current URL as fallback if (hasAdParams) { landingPageUrl = currentUrl; localStorage.setItem('sh_landing_page_url', currentUrl); } else if (!landingPageUrl) { // No stored landing page and no ad params - use current URL landingPageUrl = currentUrl; } // Check if mobile view (either by screen width or if mobile step indicator is visible) function isMobileView() { // Check if the mobile step indicator is displayed (more reliable than just width) const stepIndicator = container.querySelector('.sh-mobile-steps-indicator'); if (stepIndicator) { const style = window.getComputedStyle(stepIndicator); if (style.display !== 'none') { return true; } } return window.innerWidth <= 768; } // Desktop: Toggle between calendar and times view function showDesktopCalendarView() { if (isMobileView()) return; if (desktopCalendarView) desktopCalendarView.style.display = 'block'; if (desktopTimesView) desktopTimesView.style.display = 'none'; } function showDesktopTimesView() { if (isMobileView()) return; if (desktopCalendarView) desktopCalendarView.style.display = 'none'; if (desktopTimesView) desktopTimesView.style.display = 'block'; // Update the selected date text in back button (abbreviated to fit one line) if (selectedDate && selectedDateText) { selectedDateText.textContent = selectedDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', timeZone: userTimezone }); } } // Back to calendar button handler if (backToCalendarBtn) { backToCalendarBtn.addEventListener('click', function() { showDesktopCalendarView(); }); } // Mobile step navigation (3 steps: 1=Form, 2=Date&Time, 3=Confirm) function goToMobileStep(step) { if (!isMobileView()) return; currentMobileStep = step; // Update step visibility container.querySelectorAll('.sh-mobile-step').forEach(el => { const stepNum = parseInt(el.dataset.mobileStep); if (stepNum === step) { el.classList.add('active'); } else { el.classList.remove('active'); } }); // Update step indicator dots container.querySelectorAll('.sh-mobile-step-dot').forEach(dot => { const dotStep = parseInt(dot.dataset.step); dot.classList.remove('active', 'completed'); if (dotStep === step) { dot.classList.add('active'); } else if (dotStep < step) { dot.classList.add('completed'); } }); // If going to form step (1), focus email field if (step === 1) { setTimeout(() => { const emailField = container.querySelector('input[name="email"]'); if (emailField && !emailField.value) { emailField.focus(); } }, 100); } // If going to time step (3), show selected date if (step === 3) { updateMobileDateDisplay(); } // If going to confirm step (4), populate the confirm details if (step === 4) { populateMobileConfirm(); } } // Update mobile date display on step 3 function updateMobileDateDisplay() { const dateText = container.querySelector('.sh-mobile-date-text'); if (dateText && selectedDate) { dateText.textContent = selectedDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', timeZone: userTimezone }); } } // Populate mobile confirm step function populateMobileConfirm() { const mobileConfirmDate = container.querySelector('.sh-mobile-confirm-date'); const mobileConfirmTime = container.querySelector('.sh-mobile-confirm-time'); if (selectedSlot && mobileConfirmDate && mobileConfirmTime) { const slotDate = new Date(selectedSlot); mobileConfirmDate.textContent = slotDate.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric', timeZone: userTimezone }); mobileConfirmTime.textContent = slotDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); } } // Validate form fields for mobile step 1 // Free email domains that are not allowed const freeEmailDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'aol.com', 'icloud.com', 'mail.com', 'protonmail.com', 'zoho.com', 'yandex.com', 'gmx.com', 'live.com', 'msn.com']; // Strict email validation: name@domain.tld (TLD must be 2-6 letters only) function isValidEmail(email) { // Basic format check: something@something.something const basicRegex = /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,6}$/; if (!basicRegex.test(email)) return false; // Additional check: TLD should only be letters const parts = email.split('.'); const tld = parts[parts.length - 1]; if (!/^[a-zA-Z]{2,6}$/.test(tld)) return false; return true; } function validateFormFields() { const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); // Check if email is valid format if (!isValidEmail(email)) { return { valid: false, error: email.includes('@') ? 'Please enter a valid email address' : null }; } // Check for free email domains const domain = email.split('@')[1]?.toLowerCase(); if (domain && freeEmailDomains.includes(domain)) { return { valid: false, error: 'Please use your work email address' }; } // Check other required fields if (!firstName || !lastName || !title || !company) { return { valid: false, error: null }; } return { valid: true, error: null }; } // Update mobile next button state and show email errors function updateMobileNextBtn() { const btn = container.querySelector('.sh-mobile-next-btn'); const emailError = container.querySelector('.sh-inline-error-email'); const validation = validateFormFields(); if (btn) { btn.disabled = !validation.valid; } if (emailError) { emailError.textContent = validation.error || ''; } } // Direct click handlers for mobile buttons const mobileNextBtn = container.querySelector('.sh-mobile-next-btn'); if (mobileNextBtn) { mobileNextBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); const validation = validateFormFields(); if (validation.valid) { // Send form data to Zapier on step 1 completion (lead capture) sendFormToZapier(); goToMobileStep(2); } }); } // Send form data to Lead CPT + HubSpot (step 1 completion - lead capture) function sendFormToZapier() { const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); const phone = container.querySelector('input[name="phone"]').value.trim(); // Also submit to HubSpot for the step 1 completion submitToHubSpotPartial(email, firstName, lastName, title, company, phone, 'Step 1 - Info Submitted'); // Device type const ua = navigator.userAgent; let deviceType = 'Desktop'; if (/tablet|ipad|playbook|silk/i.test(ua)) deviceType = 'Tablet'; else if (/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i.test(ua)) deviceType = 'Mobile'; // GCLID const params = new URLSearchParams(window.location.search); let gclid = params.get('gclid') || ''; if (!gclid) { const cookie = document.cookie.match(/(?:^|; )gclid=([^;]*)/); gclid = cookie ? cookie[1] : ''; } // UTMs const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; const utms = {}; utmKeys.forEach(key => { let value = params.get(key); if (!value) { const cookie = document.cookie.match(new RegExp('(?:^|; )' + key + '=([^;]*)')); value = cookie ? cookie[1] : ''; } utms[key] = value; }); const payload = { 'Email': email, 'First Name': firstName, 'Last Name': lastName, 'Job Title': title, 'Company': company, 'Phone': phone || '', 'Device': deviceType, 'Form Type': (container.dataset.formType || 'Inline Booking Shortcode') + ' - Step 1', 'Form Step': 'Step 1 - Info Submitted', 'Page Url': landingPageUrl, 'Page Title': document.title, 'Gclid': gclid, 'Utm Source': utms.utm_source || '', 'Utm Medium': utms.utm_medium || '', 'Utm Campaign': utms.utm_campaign || '', 'Utm Term': utms.utm_term || '', 'Utm Content': utms.utm_content || '', 'Session ID': sessionId, 'Theme Mode': document.body.classList.contains('light-mode') ? 'light' : 'dark', 'Highlight Color': getComputedStyle(document.documentElement).getPropertyValue('--sh-page-highlight').trim() }; // Save to localStorage for later localStorage.setItem('sh_chatbot_user', JSON.stringify({ email, firstName, lastName, title, company, phone })); // Save to Leads CPT (will deduplicate and send to Zapier after 2 min) const formData = new FormData(); formData.append('action', 'sh_save_lead'); formData.append('nonce', shBooking.nonce); formData.append('payload', JSON.stringify(payload)); fetch(shBooking.ajaxUrl, { method: 'POST', body: formData }) .then(r => r.json()) .catch(() => {}); } // Back buttons - use event delegation since they're in different steps container.addEventListener('click', function(e) { const backBtn = e.target.closest('.sh-mobile-back-btn'); if (backBtn) { e.preventDefault(); e.stopPropagation(); const gotoStep = parseInt(backBtn.dataset.gotoStep); if (gotoStep) { goToMobileStep(gotoStep); } } }); // Confirm button const mobileConfirmBtn = container.querySelector('.sh-mobile-confirm-btn'); if (mobileConfirmBtn) { mobileConfirmBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); handleMobileConfirm(); }); } // Mobile confirm button handler async function handleMobileConfirm() { const mobileConfirmBtn = container.querySelector('.sh-mobile-confirm-btn'); if (!selectedSlot || !selectedCalendarId || !mobileConfirmBtn) { return; } const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); const phone = container.querySelector('input[name="phone"]').value.trim(); // Validate email before submission if (!isValidEmail(email)) { alert('Please enter a valid business email address'); goToMobileStep(1); return; } // Check for free email domains const emailDomain = email.split('@')[1]?.toLowerCase(); if (emailDomain && freeEmailDomains.includes(emailDomain)) { alert('Please use your business email'); goToMobileStep(1); return; } mobileConfirmBtn.disabled = true; mobileConfirmBtn.innerHTML = '<span>BOOKING...</span>'; try { const bookingData = new FormData(); bookingData.append('action', 'sh_create_calendar_booking'); bookingData.append('nonce', shBooking.nonce); bookingData.append('startTime', selectedSlot); bookingData.append('name', `${firstName} ${lastName}`); bookingData.append('email', email); bookingData.append('company', company); bookingData.append('timezone', userTimezone); // Rep will be randomly selected from available reps for this slot const bookingResponse = await fetch(shBooking.ajaxUrl, { method: 'POST', body: bookingData }).then(r => r.json()); if (bookingResponse.success) { localStorage.setItem('sh_chatbot_user', JSON.stringify({ email, firstName, lastName, title, company, phone })); sendToZapier({ email, firstName, lastName, title, company, phone }); showMobileSuccess(); } else { throw new Error(bookingResponse.data?.message || 'Booking failed'); } } catch (err) { mobileConfirmBtn.disabled = false; mobileConfirmBtn.innerHTML = '<span>BOOK MY CALL</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>'; alert(err.message || 'Booking failed. Please try again.'); } } // Show mobile success state function showMobileSuccess() { const confirmStep = container.querySelector('.sh-mobile-confirm-step'); const mobileSuccess = container.querySelector('.sh-mobile-success'); const mobileConfirmBtn = container.querySelector('.sh-mobile-confirm-btn'); const mobileSuccessDetails = container.querySelector('.sh-mobile-success-details'); const confirmCard = container.querySelector('.sh-mobile-confirm-card'); const stepHeader = confirmStep?.querySelector('.sh-mobile-step-header'); if (confirmStep && mobileSuccess) { if (confirmCard) confirmCard.style.display = 'none'; if (mobileConfirmBtn) mobileConfirmBtn.style.display = 'none'; if (stepHeader) stepHeader.style.display = 'none'; mobileSuccess.style.display = 'block'; if (mobileSuccessDetails && selectedSlot) { const slotDate = new Date(selectedSlot); mobileSuccessDetails.textContent = slotDate.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); } // Mark all steps completed container.querySelectorAll('.sh-mobile-step-dot').forEach(dot => { dot.classList.remove('active'); dot.classList.add('completed'); }); } } // Initialize mobile view if (isMobileView()) { goToMobileStep(1); } // Listen for form input changes (including keyup for better responsiveness) container.querySelectorAll('.sh-inline-form input').forEach(input => { input.addEventListener('input', updateMobileNextBtn); input.addEventListener('keyup', updateMobileNextBtn); input.addEventListener('change', updateMobileNextBtn); }); // Initial button state check updateMobileNextBtn(); // Timezones const timezones = [ { value: 'America/New_York', label: 'Eastern Time (ET)' }, { value: 'America/Chicago', label: 'Central Time (CT)' }, { value: 'America/Denver', label: 'Mountain Time (MT)' }, { value: 'America/Los_Angeles', label: 'Pacific Time (PT)' }, { value: 'America/Phoenix', label: 'Arizona (AZ)' }, { value: 'America/Anchorage', label: 'Alaska (AK)' }, { value: 'Pacific/Honolulu', label: 'Hawaii (HI)' }, { value: 'Europe/London', label: 'London (GMT)' }, { value: 'Europe/Paris', label: 'Paris (CET)' }, { value: 'Asia/Tokyo', label: 'Tokyo (JST)' }, { value: 'Australia/Sydney', label: 'Sydney (AEST)' } ]; // All timezone selects const allTzSelects = [tzSelect, desktopTzSelect, mobileTzSelect, mobileTzSelectSlots].filter(el => el); // Initialize all timezone selectors function populateTimezoneSelect(select) { if (!select) return; select.innerHTML = ''; // Add user's timezone first if not in list if (!timezones.find(t => t.value === userTimezone)) { const opt = document.createElement('option'); opt.value = userTimezone; opt.textContent = userTimezone; opt.selected = true; select.appendChild(opt); } timezones.forEach(tz => { const opt = document.createElement('option'); opt.value = tz.value; opt.textContent = tz.label; if (tz.value === userTimezone) opt.selected = true; select.appendChild(opt); }); } // Populate all selects allTzSelects.forEach(select => populateTimezoneSelect(select)); // Sync all timezone selects function syncTimezones(newValue) { userTimezone = newValue; allTzSelects.forEach(select => { if (select) select.value = newValue; }); if (selectedDate) fetchSlots(selectedDate); } // Add change listeners to all selects allTzSelects.forEach(select => { if (select) { select.addEventListener('change', () => syncTimezones(select.value)); } }); // Get URL parameters from data attributes const urlDate = container.dataset.urlDate || ''; const urlFirstname = container.dataset.urlFirstname || ''; const urlLastname = container.dataset.urlLastname || ''; const urlCompany = container.dataset.urlCompany || ''; const urlEmail = container.dataset.urlEmail || ''; const urlTitle = container.dataset.urlTitle || ''; // If URL params exist, save to localStorage (they take priority) if (urlEmail || urlFirstname || urlLastname || urlCompany || urlTitle) { try { const existing = JSON.parse(localStorage.getItem('sh_chatbot_user') || '{}'); const updated = { ...existing, ...(urlEmail && { email: urlEmail }), ...(urlFirstname && { firstName: urlFirstname }), ...(urlLastname && { lastName: urlLastname }), ...(urlCompany && { company: urlCompany }), ...(urlTitle && { title: urlTitle }) }; localStorage.setItem('sh_chatbot_user', JSON.stringify(updated)); } catch (e) {} } // Pre-fill form from localStorage try { const savedUser = localStorage.getItem('sh_chatbot_user'); if (savedUser) { const user = JSON.parse(savedUser); if (user.email) container.querySelector('input[name="email"]').value = user.email; if (user.firstName) container.querySelector('input[name="firstName"]').value = user.firstName; if (user.lastName) container.querySelector('input[name="lastName"]').value = user.lastName; if (user.title) container.querySelector('input[name="title"]').value = user.title; if (user.company) container.querySelector('input[name="company"]').value = user.company; if (user.phone) container.querySelector('input[name="phone"]').value = user.phone; if (user.firstName && user.lastName && user.company) zoomInfoLookedUp = true; } } catch (e) {} // Update mobile button state after pre-fill setTimeout(updateMobileNextBtn, 100); // Handle pre-selected date from URL // Using cached Google Calendar slots (synced every 5 min via cron) function handlePreselectDate() { if (!urlDate) return; const dateParts = urlDate.split('-'); if (dateParts.length !== 3) return; const targetYear = parseInt(dateParts[0]); const targetMonth = parseInt(dateParts[1]) - 1; // JS months are 0-indexed const targetDay = parseInt(dateParts[2]); const targetDate = new Date(targetYear, targetMonth, targetDay); // Set currentMonth to the target month for calendar rendering currentMonth = new Date(targetYear, targetMonth, 1); // Select the date (this will render calendar and fetch slots) selectDate(targetDate, false); // false = not user click, so won't trigger mobile step change // On desktop, show times view directly if (!isMobileView()) { showDesktopTimesView(); } } // Delay preselect to ensure calendar is ready setTimeout(handlePreselectDate, 300); // Calendar rendering (weekdays only - no weekends) function renderCalendar() { const year = currentMonth.getFullYear(); const month = currentMonth.getMonth(); const today = new Date(); today.setHours(0, 0, 0, 0); const monthText = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); // Update month text for both desktop and mobile if (calMonth) calMonth.textContent = monthText; if (calMonthMobile) calMonthMobile.textContent = monthText; const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); // Calculate empty cells for start of month (only for weekdays Mon-Fri) let emptyDays = 0; if (firstDay === 0) emptyDays = 0; else if (firstDay === 6) emptyDays = 0; else emptyDays = firstDay - 1; // Generate calendar days HTML function generateCalendarDays(container) { if (!container) return; container.innerHTML = ''; for (let i = 0; i < emptyDays; i++) { const empty = document.createElement('div'); empty.className = 'sh-inline-cal-day disabled'; container.appendChild(empty); } for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day); const dayOfWeek = date.getDay(); // Skip Saturday (6) and Sunday (0) if (dayOfWeek === 0 || dayOfWeek === 6) continue; const dayEl = document.createElement('button'); dayEl.type = 'button'; dayEl.className = 'sh-inline-cal-day'; dayEl.textContent = day; // Disable today and all past dates (bookings start tomorrow) if (date <= today) { dayEl.classList.add('disabled'); } else { if (selectedDate && date.toDateString() === selectedDate.toDateString()) { dayEl.classList.add('selected'); } dayEl.addEventListener('click', () => selectDate(date, true)); } container.appendChild(dayEl); } } // Render to both desktop and mobile containers generateCalendarDays(calDays); generateCalendarDays(calDaysMobile); } // Get the next available weekday (Mon-Fri) - starts from tomorrow function getNextWeekday() { const tomorrow = new Date(); tomorrow.setHours(0, 0, 0, 0); tomorrow.setDate(tomorrow.getDate() + 1); // Start from tomorrow const dayOfWeek = tomorrow.getDay(); // If tomorrow is Saturday, next weekday is Monday (+2) if (dayOfWeek === 6) { tomorrow.setDate(tomorrow.getDate() + 2); } // If tomorrow is Sunday, next weekday is Monday (+1) else if (dayOfWeek === 0) { tomorrow.setDate(tomorrow.getDate() + 1); } // Otherwise, tomorrow is a weekday return tomorrow; } function selectDate(date, isUserClick = false) { selectedDate = date; selectedSlot = null; selectedCalendarId = null; updateSubmitState(); renderCalendar(); fetchSlots(date); // On desktop, show times view (replace calendar) - only on user click if (isUserClick && !isMobileView()) { showDesktopTimesView(); } // On mobile, advance to time selection (step 3) - only on user click if (isUserClick && isMobileView()) { goToMobileStep(3); } } // Cached slots from Google Calendar (fetched once, used for all dates) let cachedSlots = null; let slotsLoading = false; let slotsFetchPromise = null; function fetchAllSlots() { if (cachedSlots) { return Promise.resolve(cachedSlots); } if (slotsFetchPromise) { return slotsFetchPromise; } slotsLoading = true; const formData = new FormData(); formData.append('action', 'sh_get_calendar_slots'); formData.append('nonce', shBooking.nonce); formData.append('timezone', userTimezone); slotsFetchPromise = fetch(shBooking.ajaxUrl, { method: 'POST', body: formData }) .then(r => { if (!r.ok) throw new Error('Network response was not ok'); return r.json(); }) .then(response => { slotsLoading = false; if (response.success && response.data?.slots) { cachedSlots = response.data.slots; return cachedSlots; } return {}; }) .catch(() => { slotsLoading = false; return {}; }); return slotsFetchPromise; } function fetchSlots(date) { if (!window.shBooking) { if (desktopSlotsContainer) { desktopSlotsContainer.innerHTML = '<div class="sh-inline-slots-placeholder"><p>Calendar not configured.</p></div>'; } if (calSlots) { calSlots.innerHTML = '<div class="sh-inline-slots-placeholder"><p>Calendar not configured.</p></div>'; } return; } // Show loading state if (desktopSlotsContainer) { desktopSlotsContainer.innerHTML = '<div class="sh-inline-slots-loading"><div class="sh-inline-spinner"></div><p>Loading times...</p></div>'; } if (calSlots) { calSlots.innerHTML = '<div class="sh-inline-slots-loading"><div class="sh-inline-spinner"></div><p>Loading times...</p></div>'; } // Use cached slots (fetched once from Google Calendar) fetchAllSlots().then(slots => { if (slots && Object.keys(slots).length > 0) { renderSlots(slots); } else { renderSlotsPlaceholder('<p>No times available for this date</p>'); } }).catch(() => { renderSlotsPlaceholder('<p>Error loading times</p>'); }); } function renderSlots(slots) { // slots is an object keyed by date: { "2025-12-18": [{time: "...", calendars: [...]}] } const year = selectedDate.getFullYear(); const month = String(selectedDate.getMonth() + 1).padStart(2, '0'); const day = String(selectedDate.getDate()).padStart(2, '0'); const dateKey = `${year}-${month}-${day}`; const daySlots = slots[dateKey] || []; if (daySlots.length === 0) { renderSlotsPlaceholder('<p>No times available</p>'); return; } // Clear previous slot availability data slotAvailability = {}; // Build sorted times array const sortedTimes = daySlots.map(slot => { // Store which reps have this slot available (for random selection on booking) slotAvailability[slot.time] = slot.reps || slot.calendars || []; return slot.time; }).sort(); // Render to desktop container if (desktopSlotsContainer) { const desktopGrid = document.createElement('div'); desktopGrid.className = 'sh-inline-slots-grid'; sortedTimes.forEach(time => { const slotDate = new Date(time); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'sh-inline-slot'; btn.textContent = slotDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); if (selectedSlot === time) { btn.classList.add('selected'); } btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); selectSlot(time, this); }); desktopGrid.appendChild(btn); }); desktopSlotsContainer.innerHTML = ''; desktopSlotsContainer.appendChild(desktopGrid); } // Render to mobile container if (calSlots) { const mobileGrid = document.createElement('div'); mobileGrid.className = 'sh-inline-slots-grid'; sortedTimes.forEach(time => { const slotDate = new Date(time); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'sh-inline-slot'; btn.textContent = slotDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); if (selectedSlot === time) { btn.classList.add('selected'); } btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); selectSlot(time, this); }); mobileGrid.appendChild(btn); }); // Clear and rebuild mobile slots container calSlots.innerHTML = ''; // Add mobile header (back button + title) calSlots.appendChild(createMobileSlotsHeader()); // Add mobile date display calSlots.appendChild(createMobileDateDisplay()); // Add mobile timezone selector calSlots.appendChild(createMobileTimezoneSelector()); // Add the time slots grid calSlots.appendChild(mobileGrid); } } // Render placeholder for both desktop and mobile function renderSlotsPlaceholder(html) { if (desktopSlotsContainer) { desktopSlotsContainer.innerHTML = '<div class="sh-inline-slots-placeholder">' + html + '</div>'; } if (calSlots) { calSlots.innerHTML = ''; calSlots.appendChild(createMobileSlotsHeader()); calSlots.appendChild(createMobileDateDisplay()); calSlots.appendChild(createMobileTimezoneSelector()); const placeholder = document.createElement('div'); placeholder.className = 'sh-inline-slots-placeholder'; placeholder.innerHTML = html; calSlots.appendChild(placeholder); } } // Helper: Create mobile slots header with back button function createMobileSlotsHeader() { const header = document.createElement('div'); header.className = 'sh-mobile-step-header sh-mobile-only'; header.innerHTML = ` <button type="button" class="sh-mobile-back-btn" data-goto-step="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <h3>Pick a Time</h3> `; return header; } // Helper: Create mobile date display function createMobileDateDisplay() { const dateDiv = document.createElement('div'); dateDiv.className = 'sh-mobile-selected-date sh-mobile-only'; const dateText = document.createElement('span'); dateText.className = 'sh-mobile-date-text'; if (selectedDate) { dateText.textContent = selectedDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', timeZone: userTimezone }); } dateDiv.appendChild(dateText); return dateDiv; } // Helper: Create mobile timezone selector function createMobileTimezoneSelector() { const tzDiv = document.createElement('div'); tzDiv.className = 'sh-mobile-timezone sh-mobile-only'; tzDiv.innerHTML = `<label>Timezone:</label>`; const select = document.createElement('select'); select.className = 'sh-mobile-tz-select'; // Add timezone options timezones.forEach(tz => { const opt = document.createElement('option'); opt.value = tz.value; opt.textContent = tz.label; if (tz.value === userTimezone) opt.selected = true; select.appendChild(opt); }); // Add user's timezone if not in list if (!timezones.find(t => t.value === userTimezone)) { const opt = document.createElement('option'); opt.value = userTimezone; opt.textContent = userTimezone; opt.selected = true; select.insertBefore(opt, select.firstChild); } // Handle timezone change - sync all selects select.addEventListener('change', () => { syncTimezones(select.value); }); tzDiv.appendChild(select); return tzDiv; } // Helper: Rebuild slots container with placeholder message function selectSlot(time, clickedBtn) { selectedSlot = time; // Randomly pick a calendar that has this slot const availableCalendars = slotAvailability[time] || []; if (availableCalendars.length > 0) { selectedCalendarId = availableCalendars[Math.floor(Math.random() * availableCalendars.length)]; } // Clear time error when a slot is selected const errorTime = container.querySelector('.sh-inline-error-time'); if (errorTime) errorTime.textContent = ''; // Update slot buttons UI container.querySelectorAll('.sh-inline-slot').forEach(el => el.classList.remove('selected')); if (clickedBtn) clickedBtn.classList.add('selected'); // Show selected time in form const slotDate = new Date(time); const dateStr = slotDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', timeZone: userTimezone }); const timeStr = slotDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); selectedTimeDisplay.textContent = `📅 ${dateStr} at ${timeStr}`; selectedTimeDisplay.classList.add('active'); updateSubmitState(); // On mobile, advance to confirm step (step 4) if (isMobileView()) { goToMobileStep(4); } } function updateSubmitState() { const validation = validateFormFields(); const emailError = container.querySelector('.sh-inline-error-email'); // Show email error if any if (emailError) { emailError.textContent = validation.error || ''; } // Desktop submit requires slot selection + valid form submitBtn.disabled = !(selectedSlot && validation.valid); } // Form input listeners form.querySelectorAll('input').forEach(input => { input.addEventListener('input', updateSubmitState); }); // ZoomInfo lookup emailInput.addEventListener('blur', async function() { const email = this.value.trim().toLowerCase(); const fnInput = container.querySelector('input[name="firstName"]'); // Always move focus to firstName field after email blur (Tab behavior) // Use setTimeout to ensure this happens after the blur event completes setTimeout(() => { if (fnInput && document.activeElement === document.body) { fnInput.focus(); } }, 0); if (!email) return; // Use strict email validation if (!isValidEmail(email)) return; // Skip if we already looked up this exact email if (email === lastLookedUpEmail) return; // Check if free email domain const genericDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'aol.com', 'icloud.com', 'mail.com', 'protonmail.com', 'zoho.com', 'yandex.com', 'gmx.com', 'live.com', 'msn.com']; const domain = email.split('@')[1]; const isFreeEmail = genericDomains.includes(domain.toLowerCase()); emailLoader.style.display = 'block'; lastLookedUpEmail = email; // Track this email zoomInfoLookedUp = true; try { const response = await fetch('/wp-admin/admin-ajax.php?action=zoominfo_proxy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ matchPersonInput: [{ emailAddress: email }], outputFields: ["firstName", "lastName", "jobTitle", "companyName", "phone", "mobilePhone"] }) }); const data = await response.json(); if (data?.data?.data?.result?.length) { const person = data.data.data.result[0].data[0]; const lnInput = container.querySelector('input[name="lastName"]'); const titleInput = container.querySelector('input[name="title"]'); const companyInput = container.querySelector('input[name="company"]'); const phoneInput = container.querySelector('input[name="phone"]'); // Replace field values with ZoomInfo data if (person.firstName && fnInput) { fnInput.value = person.firstName; } if (person.lastName && lnInput) { lnInput.value = person.lastName; } if (person.jobTitle && titleInput) { titleInput.value = person.jobTitle; } if ((person.companyName || person.company?.name) && companyInput) { companyInput.value = person.companyName || person.company?.name || ''; } if ((person.phone || person.mobilePhone) && phoneInput) { phoneInput.value = person.mobilePhone || person.phone || ''; } // Move focus to the first empty required field after populating const fieldsInOrder = [fnInput, lnInput, titleInput, companyInput]; for (const field of fieldsInOrder) { if (field && !field.value) { field.focus(); break; } } // Update both desktop and mobile button states updateSubmitState(); updateMobileNextBtn(); } } catch (err) { // ZoomInfo lookup failed silently } finally { emailLoader.style.display = 'none'; // Also update mobile button in case email was the last field needed updateMobileNextBtn(); // Send form details to Zapier after ZoomInfo lookup (even with partial data) // Mark free emails differently so we can track them const zapierStep = isFreeEmail ? 'Free Email Captured' : 'ZoomInfo Enriched'; sendPartialFormToZapier(zapierStep); } }); // HubSpot config for partial submissions const HUBSPOT_PORTAL_ID = '6880333'; const HUBSPOT_PARTIAL_FORM_GUID = '7bc3cca4-6c16-4a78-9707-561cefd46848'; // Get HubSpot tracking cookie function getHubspotUTK() { const match = document.cookie.match(/(?:^|; )hubspotutk=([^;]*)/); return match ? match[1] : ''; } // Get visitor IP address for HubSpot tracking let cachedIP = null; function getVisitorIP(callback) { if (cachedIP) { callback(cachedIP); return; } fetch('https://api.ipify.org?format=json') .then(r => r.json()) .then(d => { cachedIP = d.ip; callback(d.ip); }) .catch(() => callback('')); } // Submit to HubSpot Forms API (with IP tracking) function submitToHubSpotPartial(email, firstName, lastName, title, company, phone, formStep) { // Get IP first, then submit getVisitorIP(function(ip) { const fields = [ { name: 'email', value: email }, { name: 'firstname', value: firstName || '' }, { name: 'lastname', value: lastName || '' }, { name: 'jobtitle', value: title || '' }, { name: 'company', value: company || '' }, { name: 'phone', value: phone || '' } ]; // Add GCLID if available const params = new URLSearchParams(window.location.search); let gclid = params.get('gclid') || ''; if (!gclid) { const cookie = document.cookie.match(/(?:^|; )gclid=([^;]*)/); gclid = cookie ? cookie[1] : ''; } if (gclid) { fields.push({ name: 'hs_google_click_id', value: gclid }); } const body = { fields: fields, context: { hutk: getHubspotUTK(), pageUri: window.location.href, pageName: document.title + ' - ' + formStep } }; // Add IP if available if (ip) { body.context.ipAddress = ip; } fetch(`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_PARTIAL_FORM_GUID}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) .catch(() => {}); }); } // Send partial form data to Lead CPT AND HubSpot (used after ZoomInfo lookup) function sendPartialFormToZapier(formStep) { const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); const phone = container.querySelector('input[name="phone"]').value.trim(); // Must have at least email if (!email) return; // Also submit to HubSpot submitToHubSpotPartial(email, firstName, lastName, title, company, phone, formStep); // Device type const ua = navigator.userAgent; let deviceType = 'Desktop'; if (/tablet|ipad|playbook|silk/i.test(ua)) deviceType = 'Tablet'; else if (/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i.test(ua)) deviceType = 'Mobile'; // GCLID const params = new URLSearchParams(window.location.search); let gclid = params.get('gclid') || ''; if (!gclid) { const cookie = document.cookie.match(/(?:^|; )gclid=([^;]*)/); gclid = cookie ? cookie[1] : ''; } // UTMs const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; const utms = {}; utmKeys.forEach(key => { let value = params.get(key); if (!value) { const cookie = document.cookie.match(new RegExp('(?:^|; )' + key + '=([^;]*)')); value = cookie ? cookie[1] : ''; } utms[key] = value; }); const payload = { 'Email': email, 'First Name': firstName || '', 'Last Name': lastName || '', 'Job Title': title || '', 'Company': company || '', 'Phone': phone || '', 'Device': deviceType, 'Form Type': (container.dataset.formType || 'Inline Booking Shortcode') + ' - ' + formStep, 'Form Step': formStep, 'Page Url': landingPageUrl, 'Page Title': document.title, 'Gclid': gclid, 'Utm Source': utms.utm_source || '', 'Utm Medium': utms.utm_medium || '', 'Utm Campaign': utms.utm_campaign || '', 'Utm Term': utms.utm_term || '', 'Utm Content': utms.utm_content || '', 'Session ID': sessionId, 'Theme Mode': document.body.classList.contains('light-mode') ? 'light' : 'dark', 'Highlight Color': getComputedStyle(document.documentElement).getPropertyValue('--sh-page-highlight').trim() }; // Save to Leads CPT (will deduplicate and send to Zapier after 2 min) const formData = new FormData(); formData.append('action', 'sh_save_lead'); formData.append('nonce', shBooking.nonce); formData.append('payload', JSON.stringify(payload)); fetch(shBooking.ajaxUrl, { method: 'POST', body: formData }) .then(r => r.json()) .catch(() => {}); } // Time error element const errorTime = container.querySelector('.sh-inline-error-time'); const submitWrapper = container.querySelector('.sh-inline-submit-wrapper'); // Show time error when clicking disabled button (via wrapper) if (submitWrapper) { submitWrapper.addEventListener('click', function(e) { if (submitBtn.disabled && (!selectedSlot || !selectedCalendarId)) { e.preventDefault(); e.stopPropagation(); if (errorTime) { errorTime.textContent = 'Please select a time 👉'; errorTime.style.display = 'block'; // Shake the calendar area to draw attention const scheduler = container.querySelector('.sh-inline-booking-scheduler'); if (scheduler) { scheduler.style.animation = 'none'; scheduler.offsetHeight; // Trigger reflow scheduler.style.animation = 'sh-shake 0.5s ease'; } } } }); } // Form submission form.addEventListener('submit', async function(e) { e.preventDefault(); const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); const phone = container.querySelector('input[name="phone"]').value.trim(); // Validate email format before submission if (!isValidEmail(email)) { errorEmail.textContent = 'Please enter a valid business email address'; return; } // Check for free email domains const emailDomain = email.split('@')[1]?.toLowerCase(); if (emailDomain && freeEmailDomains.includes(emailDomain)) { errorEmail.textContent = 'Please use your business email'; return; } if (!selectedSlot) { if (errorTime) { errorTime.textContent = '👉 Please select a time on the right first'; errorTime.style.display = 'block'; } return; } // Clear time error on successful validation if (errorTime) errorTime.textContent = ''; submitBtn.disabled = true; submitBtn.innerHTML = '<span>BOOKING...</span>'; try { // Book via Google Calendar + Zoom const bookingData = new FormData(); bookingData.append('action', 'sh_create_calendar_booking'); bookingData.append('nonce', shBooking.nonce); bookingData.append('startTime', selectedSlot); bookingData.append('name', `${firstName} ${lastName}`); bookingData.append('email', email); bookingData.append('company', company); bookingData.append('timezone', userTimezone); // Rep will be randomly selected from available reps for this slot const bookingResponse = await fetch(shBooking.ajaxUrl, { method: 'POST', body: bookingData }).then(r => r.json()); if (bookingResponse.success) { // Save to localStorage localStorage.setItem('sh_chatbot_user', JSON.stringify({ email, firstName, lastName, title, company, phone })); // Send to Zapier sendToZapier({ email, firstName, lastName, title, company, phone }); // Show success showSuccess(); } else { throw new Error(bookingResponse.data?.message || 'Booking failed'); } } catch (err) { errorEmail.textContent = err.message || 'Booking failed. Please try again.'; submitBtn.disabled = false; submitBtn.innerHTML = '<span>BOOK MY CALL</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>'; } }); function showSuccess() { form.style.display = 'none'; container.querySelector('.sh-inline-form-header').style.display = 'none'; successDiv.style.display = 'block'; const slotDate = new Date(selectedSlot); successDetails.textContent = slotDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); } // Lead CPT submission with all fields + HubSpot (after booking completed) function sendToZapier(info) { // Also submit to HubSpot for the completed booking submitToHubSpotPartial(info.email, info.firstName, info.lastName, info.title, info.company, info.phone, 'Booking Completed'); // Device type const ua = navigator.userAgent; let deviceType = 'Desktop'; if (/tablet|ipad|playbook|silk/i.test(ua)) deviceType = 'Tablet'; else if (/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i.test(ua)) deviceType = 'Mobile'; // GCLID const params = new URLSearchParams(window.location.search); let gclid = params.get('gclid') || ''; if (!gclid) { const cookie = document.cookie.match(/(?:^|; )gclid=([^;]*)/); gclid = cookie ? cookie[1] : ''; } // UTMs const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; const utms = {}; utmKeys.forEach(key => { let value = params.get(key); if (!value) { const cookie = document.cookie.match(new RegExp('(?:^|; )' + key + '=([^;]*)')); value = cookie ? cookie[1] : ''; } utms[key] = value; }); // Meeting time const meetingDate = new Date(selectedSlot); const meetingTime = meetingDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); const payload = { 'Email': info.email, 'First Name': info.firstName, 'Last Name': info.lastName, 'Job Title': info.title, 'Company': info.company, 'Phone': info.phone || '', 'Device': deviceType, 'Meeting Date': selectedSlot, 'Meeting Time': meetingTime, 'Timezone': userTimezone, 'Calendar ID': selectedCalendarId ? String(selectedCalendarId) : '', 'Form Type': container.dataset.formType || 'Inline Booking Shortcode', 'Form Step': 'Booking Completed', 'Page Url': landingPageUrl, 'Page Title': document.title, 'Gclid': gclid, 'Utm Source': utms.utm_source || '', 'Utm Medium': utms.utm_medium || '', 'Utm Campaign': utms.utm_campaign || '', 'Utm Term': utms.utm_term || '', 'Utm Content': utms.utm_content || '', 'Session ID': sessionId, 'Theme Mode': document.body.classList.contains('light-mode') ? 'light' : 'dark', 'Highlight Color': getComputedStyle(document.documentElement).getPropertyValue('--sh-page-highlight').trim() }; // Save to Leads CPT (will deduplicate and send to Zapier after 2 min) const formData = new FormData(); formData.append('action', 'sh_save_lead'); formData.append('nonce', shBooking.nonce); formData.append('payload', JSON.stringify(payload)); fetch(shBooking.ajaxUrl, { method: 'POST', body: formData }) .then(r => r.json()) .catch(() => {}); } // Calendar navigation - Desktop if (calPrev) { calPrev.addEventListener('click', () => { currentMonth.setMonth(currentMonth.getMonth() - 1); renderCalendar(); }); } if (calNext) { calNext.addEventListener('click', () => { currentMonth.setMonth(currentMonth.getMonth() + 1); renderCalendar(); }); } // Calendar navigation - Mobile if (calPrevMobile) { calPrevMobile.addEventListener('click', () => { currentMonth.setMonth(currentMonth.getMonth() - 1); renderCalendar(); }); } if (calNextMobile) { calNextMobile.addEventListener('click', () => { currentMonth.setMonth(currentMonth.getMonth() + 1); renderCalendar(); }); } // Initialize - render calendar (no auto-select on desktop to show calendar first) renderCalendar(); // Check if autofocus is enabled for this instance const autofocusEnabled = container.dataset.autofocus === 'true'; // Auto-focus email field to trigger browser autofill (only if enabled) function autoFocusEmail() { if (!autofocusEnabled) return; // Only focus if email is empty (not pre-filled from localStorage) if (emailInput && !emailInput.value) { // Use IntersectionObserver to focus only when visible const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Small delay to ensure page is fully rendered setTimeout(() => { if (!emailInput.value) { emailInput.focus(); } }, 300); observer.disconnect(); } }); }, { threshold: 0.5 }); observer.observe(container); } } // Listen for autofill changes on email field emailInput.addEventListener('change', function() { const email = this.value.trim(); if (email && !zoomInfoLookedUp) { // Trigger ZoomInfo lookup via blur event this.blur(); this.focus(); } updateMobileNextBtn(); updateSubmitState(); }); // Also detect autofill via animation (Chrome hack) emailInput.addEventListener('animationstart', function(e) { if (e.animationName === 'onAutoFillStart') { // Autofill detected, trigger blur to run ZoomInfo lookup setTimeout(() => { if (emailInput.value && !zoomInfoLookedUp) { emailInput.blur(); } }, 100); } }); // Periodic check for autofilled values (only if autofocus enabled) if (autofocusEnabled) { let autofillCheckCount = 0; const autofillChecker = setInterval(() => { autofillCheckCount++; if (autofillCheckCount > 20) { // Stop after 10 seconds clearInterval(autofillChecker); return; } // Check if email was autofilled if (emailInput.value && !zoomInfoLookedUp) { clearInterval(autofillChecker); // Trigger ZoomInfo lookup emailInput.blur(); } }, 500); } // Trigger auto-focus after short delay (only if enabled) if (autofocusEnabled) { setTimeout(autoFocusEmail, 500); } })(); </script> </section> <style> .sh-booking-section { min-height: 100vh; padding: 40px 40px; display: flex; flex-direction: column; justify-content: center; } .sh-booking-section .sh-section-header { margin-bottom: 0px; } .sh-booking-section .sh-section-title { margin-bottom: 0; } .sh-booking-section .sh-section-subtitle { margin-top: 8px; margin-bottom: 0; } .sh-booking-section-compact .sh-section-header { margin-bottom: 24px; } .sh-booking-section-compact .sh-section-title { margin-bottom: 0; } .sh-booking-section .sh-inline-booking { margin-top: 10px; margin-bottom: 0; } .sh-booking-section .sh-inline-booking-header { display: none; /* Hide inner header since section has its own */ } /* Highlight background variant */ .sh-booking-section.sh-booking-section-highlight { background: var(--sh-page-highlight) !important; position: relative; } .sh-booking-section-highlight .sh-section-eyebrow { background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.15) !important; border-color: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.3) !important; color: var(--sh-page-highlight) !important; } .sh-booking-section-highlight .sh-section-title { color: var(--sh-text-inverted) !important; } .sh-booking-section-highlight .sh-section-subtitle { color: var(--sh-text-inverted) !important; margin-top: 8px !important; margin-bottom: 16px !important; } .sh-booking-section-highlight .sh-inline-booking-card { background: var(--sh-bg-card) !important; border-color: var(--sh-border-default) !important; } @media (max-width: 768px) { .sh-booking-section { display: none; } } </style> <!-- Footer --> <footer class="sh-footer"> <!-- Main Footer --> <div class="sh-footer-main"> <div class="sh-container"> <div class="sh-footer-grid"> <!-- Brand Column --> <div class="sh-footer-brand"> <a href="https://saleshive.com/" class="sh-footer-logo" data-wpel-link="internal"> <img src="https://saleshive.com/wp-content/uploads/2024/08/SalesHive-Logo-large-2.webp" alt="SalesHive" class="sh-footer-logo-img-logo"> </a> <p class="sh-footer-tagline">The AI-powered B2B sales development agency that delivers qualified meetings at scale.</p> <div class="sh-footer-contact"> <p class="sh-footer-contact-item"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;margin-right:8px;vertical-align:middle;"> <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/> </svg> <a href="tel:+18883333096" data-wpel-link="internal">+1 (888) 333-3096</a> </p> <p class="sh-footer-contact-item"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;margin-right:8px;vertical-align:middle;"> <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/> </svg> <a href="mailto:sales@saleshive.com">sales@saleshive.com</a> </p> <p class="sh-footer-contact-item"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;margin-right:8px;vertical-align:middle;"> <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/> </svg> <a href="mailto:info@saleshive.com">info@saleshive.com</a> </p> </div> </div> <!-- Services Column --> <div class="sh-footer-column"> <h4 class="sh-footer-heading">Services</h4> <ul class="sh-footer-menu"> <li><a href="https://saleshive.com/cold-calling/" data-wpel-link="internal">Cold Calling - US</a></li> <li><a href="https://saleshive.com/cold-calling-philippines/" data-wpel-link="internal">Cold Calling - PH</a></li> <li><a href="https://saleshive.com/email-outreach/" data-wpel-link="internal">Email Outreach</a></li> <li><a href="https://saleshive.com/list-building/" data-wpel-link="internal">List Building</a></li> <li><a href="https://saleshive.com/pricing/" data-wpel-link="internal">Appointment Setting</a></li> <li><a href="https://saleshive.com/sdr-outsourcing/" data-wpel-link="internal">SDR Outsourcing</a></li> </ul> </div> <!-- Company Column --> <div class="sh-footer-column"> <h4 class="sh-footer-heading">Company</h4> <ul class="sh-footer-menu"> <li><a href="https://saleshive.com/about/" data-wpel-link="internal">About Us</a></li> <li><a href="https://saleshive.com/pricing/" data-wpel-link="internal">Pricing</a></li> <li><a href="https://saleshive.com/process/" data-wpel-link="internal">Process</a></li> <li><a href="https://saleshive.com/platform/" data-wpel-link="internal">Platform</a></li> <li><a href="https://saleshive.com/reviews/" data-wpel-link="internal">Reviews</a></li> <li><a href="https://saleshive.com/careers/" data-wpel-link="internal">Careers</a></li> </ul> </div> <!-- Resources Column --> <div class="sh-footer-column"> <h4 class="sh-footer-heading">Resources</h4> <ul class="sh-footer-menu"> <li><a href="https://saleshive.com/blog/" data-wpel-link="internal">Sales Blog</a></li> <li><a href="https://saleshive.com/sales-glossary/" data-wpel-link="internal">Sales Glossary</a></li> <li><a href="https://saleshive.com/cold-calling-guide/" data-wpel-link="internal">Cold Calling Guide</a></li> <li><a href="https://saleshive.com/case-studies/" data-wpel-link="internal">Case Studies</a></li> <li><a href="https://saleshive.com/vendors/" data-wpel-link="internal">Vendor Catalog</a></li> <li><a href="https://appv2.saleshive.com" target="_blank" rel="noopener external noreferrer" data-wpel-link="external">Client Login</a></li> </ul> </div> </div> </div> </div> <!-- Footer Bottom --> <div class="sh-footer-bottom"> <div class="sh-container"> <div class="sh-footer-bottom-inner"> <p class="sh-footer-copyright"> © 2025 SalesHive. All rights reserved. </p> <div class="sh-footer-legal"> <a href="https://saleshive.com/privacy/" data-wpel-link="internal">Privacy Policy</a> <a href="https://saleshive.com/terms-conditions/" data-wpel-link="internal">Terms & Conditions</a> </div> </div> </div> </div> </footer> </div><!-- .sh-site --> <!-- Floating Bottom Bar --> <div class="sh-bottom-bar"> <div class="sh-bottom-bar-inner sh-bottom-bar-centered"> <button class="sh-show-footer-btn" id="shShowFooterBtn" aria-label="Show full footer"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 15l-6-6-6 6"/></svg> <span>View Footer</span> </button> <button class="sh-bottom-bar-cta sh-cta-hidden" data-open-modal> Schedule a Call <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg> </button> </div> </div> <!-- Scroll Progress Bar - Right Side --> <div class="sh-scroll-progress"> <div class="sh-scroll-progress-bar"></div> </div> <style> /* ===================================================== FLOATING BOTTOM BAR - THIN & SLEEK ===================================================== */ .sh-bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; z-index: 9998; background: var(--sh-black); border-top: 1px solid var(--sh-gray-800); opacity: 1; transform: translateY(0); transition: opacity 0.3s ease, transform 0.3s ease; } /* Hide entire floating footer when at top of page */ .sh-bottom-bar.sh-bar-hidden { opacity: 0; transform: translateY(100%); pointer-events: none; } /* Scroll Progress Bar - Right Side Vertical */ .sh-scroll-progress { position: fixed; top: 0; right: 0; bottom: 0; width: 5px; background: rgba(255, 255, 255, 0.1); z-index: 9999; overflow: hidden; } .sh-scroll-progress-bar { width: 100%; height: 0%; background: var(--sh-page-highlight); transition: height 0.1s ease-out; box-shadow: 0 0 10px var(--sh-page-highlight), 0 0 5px var(--sh-page-highlight); } .sh-bottom-bar-inner { max-width: 1400px; margin: 0 auto; padding: 10px 0; display: flex; align-items: center; justify-content: space-between; position: relative; } .sh-bottom-bar-inner.sh-bottom-bar-centered { justify-content: center; gap: 20px; } .sh-bottom-bar-stats { display: flex; align-items: center; gap: 16px; } .sh-bottom-bar-live { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 700; letter-spacing: 1.5px; color: var(--sh-page-highlight); text-transform: uppercase; } .sh-live-dot { width: 6px; height: 6px; background: var(--sh-page-highlight); border-radius: 50%; animation: sh-live-pulse 1.5s ease-in-out infinite; box-shadow: 0 0 8px var(--sh-page-highlight); } @keyframes sh-live-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.8); } } .sh-bottom-bar-divider { width: 1px; height: 20px; background: var(--sh-border-default); } .sh-bottom-bar-count { display: flex; align-items: baseline; gap: 8px; } .sh-bottom-bar-value { font-family: 'SF Mono', 'Consolas', 'Monaco', monospace; font-size: 18px; font-weight: 700; color: var(--sh-page-highlight); letter-spacing: 1px; } .sh-bottom-bar-label { font-size: 12px; font-weight: 500; color: var(--sh-text-muted); text-transform: lowercase; } /* Show Footer Button - center of floating bar */ .sh-show-footer-btn { position: absolute; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 6px; padding: 6px 14px; background: transparent; border: 1px solid var(--sh-border-default); border-radius: 20px; font-size: 11px; font-weight: 600; color: var(--sh-text-muted); cursor: pointer; transition: all 0.3s ease; opacity: 0; visibility: hidden; pointer-events: none; } .sh-show-footer-btn svg { width: 14px; height: 14px; transition: transform 0.3s ease; transform: rotate(180deg); /* Points down when closed */ } .sh-show-footer-btn:hover { background: var(--sh-bg-card); border-color: var(--sh-page-highlight); color: var(--sh-page-highlight); } .sh-show-footer-btn:hover svg { transform: rotate(180deg) translateY(2px); } .sh-show-footer-btn.sh-footer-open:hover svg { transform: rotate(0deg) translateY(-2px); } /* Show button only when at bottom of page */ .sh-show-footer-btn.sh-visible { opacity: 1; visibility: visible; pointer-events: auto; } /* Show footer when revealed via toggle button */ .sh-footer.sh-footer-visible { max-height: 1000px; /* Large enough to fit footer content */ opacity: 1; } /* When footer is visible, rotate arrow up and change text color */ .sh-show-footer-btn.sh-footer-open svg { transform: rotate(0deg); /* Points up when open */ } .sh-show-footer-btn.sh-footer-open { color: var(--sh-page-highlight); border-color: var(--sh-page-highlight); } @media (max-width: 768px) { .sh-show-footer-btn { display: none; } } .sh-bottom-bar-cta { display: flex; align-items: center; gap: 8px; padding: 8px 20px; background: var(--sh-page-highlight); color: #1a1a2e !important; /* Hardcoded dark text */ border: none; border-radius: 6px; font-size: 12px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; } /* Hide CTA when near bottom of page */ .sh-bottom-bar-cta.sh-cta-hidden { opacity: 0; transform: translateX(20px); pointer-events: none; } .sh-bottom-bar-cta svg { width: 14px; height: 14px; transition: transform 0.2s ease; } .sh-bottom-bar-cta:hover { box-shadow: 0 4px 16px rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.4); } .sh-bottom-bar-cta:hover svg { transform: translateX(3px); } /* Shimmer effect */ .sh-bottom-bar-cta::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); animation: barCtaShimmer 3s ease-in-out infinite; } @keyframes barCtaShimmer { 0% { left: -100%; } 50%, 100% { left: 100%; } } /* Hide on mobile - use mobile sticky CTA instead */ @media (max-width: 768px) { .sh-bottom-bar { display: none; } } /* Light mode */ body.light-mode .sh-bottom-bar { background: var(--sh-black); border-top-color: var(--sh-gray-800); } /* Mobile Sticky CTA - Only visible on mobile */ .sh-mobile-sticky-cta { display: none; position: fixed; bottom: 0; left: 0; right: 0; z-index: 9999; padding: 12px 16px; padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); background: transparent; border-top: none; } @media (max-width: 768px) { .sh-mobile-sticky-cta { display: block; } } .sh-mobile-cta-btn { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 16px 24px; background: var(--sh-page-highlight); color: #1a1a2e !important; /* Hardcoded dark text */ font-size: 17px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; text-decoration: none; border-radius: 12px; box-shadow: none; transition: all 0.2s ease; cursor: pointer; border: none; } .sh-mobile-cta-btn:hover { transform: translateY(-1px); box-shadow: none; color: #1a1a2e !important; /* Hardcoded dark text */ } .sh-mobile-cta-btn svg { width: 20px; height: 20px; flex-shrink: 0; } /* Light mode mobile CTA */ body.light-mode .sh-mobile-sticky-cta { background: transparent; border-top: none; } /* Hide mobile CTA when near bottom of page */ .sh-mobile-sticky-cta.sh-cta-hidden { opacity: 0; transform: translateY(100%); pointer-events: none; transition: all 0.3s ease; } /* ===================================================== BOOKING MODAL - Full width container for shortcode ===================================================== */ .sh-booking-modal { display: none; position: fixed; inset: 0; z-index: 999999; overflow-y: auto; -webkit-overflow-scrolling: touch; } .sh-booking-modal.active { display: flex; align-items: center; justify-content: center; min-height: 100vh; } .sh-booking-modal-backdrop { position: fixed; inset: 0; /* Mesh gradient - purple, black, orange base */ background: /* Primary glow - top right corner - deep purple #360947 */ radial-gradient(ellipse 140% 100% at 100% 0%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.7) 0%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.3) 35%, transparent 70%), /* Bottom left - deep purple #360947 */ radial-gradient(ellipse 100% 120% at 0% 100%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.7) 0%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.3) 40%, transparent 65%), /* Subtle center highlight - purple */ radial-gradient(ellipse 80% 80% at 50% 50%, rgba(var(--sh-mesh-primary-r), var(--sh-mesh-primary-g), var(--sh-mesh-primary-b), 0.15) 0%, transparent 60%), /* Solid dark base with orange tint */ #0d0906; /* Fix for iOS Safari dynamic viewport */ min-height: 100vh; min-height: 100dvh; height: calc(100% + 100px); } /* Overlay layer removed */ .sh-booking-modal-content { position: relative; width: 100%; max-width: 800px; margin: 20px auto; background: transparent; border: none; border-radius: 0; z-index: 1; } .sh-booking-modal-content .sh-inline-booking-header { display: none; } .sh-booking-modal-header { text-align: center; margin-bottom: 0; width: 100vw; margin-left: calc(-50vw + 50%); padding: 0 20px; box-sizing: border-box; } .sh-booking-modal-title { font-size: clamp(32px, 5vw, 48px); font-weight: 700; color: var(--sh-text-primary); margin: 0; line-height: 1.1; } .sh-booking-modal-subtitle { font-size: 18px; color: var(--sh-text-secondary); margin: 8px 0 0; } .sh-booking-modal-close { position: fixed; top: 20px; right: 20px; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.2); border-radius: 50%; color: #fff; cursor: pointer; transition: all 0.2s ease; z-index: 100; } .sh-booking-modal-close:hover { background: rgba(0,0,0,0.8); transform: scale(1.1); } .sh-booking-modal-close svg { width: 22px; height: 22px; } /* Light mode uses same mesh - CSS vars handle the color changes */ body.light-mode .sh-booking-modal-content { background: transparent; } body.light-mode .sh-booking-modal-close { background: rgba(255,255,255,0.9); border-color: rgba(0,0,0,0.2); color: #000; } body.light-mode .sh-booking-modal-close:hover { background: #fff; } /* Mobile modal adjustments */ @media (max-width: 768px) { .sh-booking-modal.active { align-items: flex-start; } .sh-booking-modal-content { margin: 0; padding: 20px; } .sh-booking-modal-content .sh-inline-booking { margin: 0; } .sh-booking-modal-header { display: none; } } /* Prevent body scroll when modal is open */ body.sh-modal-open { overflow: hidden; } </style> <!-- Mobile Sticky CTA --> <div class="sh-mobile-sticky-cta sh-cta-hidden"> <button type="button" class="sh-mobile-cta-btn" data-open-booking-modal> <span>Book a Call</span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg> </button> </div> <!-- Booking Modal --> <div id="sh-booking-modal" class="sh-booking-modal"> <div class="sh-booking-modal-backdrop" data-close-booking-modal></div> <div class="sh-booking-modal-content"> <button type="button" class="sh-booking-modal-close" data-close-booking-modal aria-label="Close"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg> </button> <div class="sh-booking-modal-header"> <h2 class="sh-booking-modal-title">Ready to Scale Your Pipeline?</h2> <p class="sh-booking-modal-subtitle">Learn how we have helped hundreds of B2B companies scale their sales.</p> </div> <div class="sh-inline-booking sh-parallax" id="sh-booking-6051" data-form-type="Modal" data-autofocus="false" data-url-date="" data-url-firstname="" data-url-lastname="" data-url-company="" data-url-email="" data-url-title=""> <div class="sh-inline-booking-topbar"> <span class="sh-booking-pulse-dot"></span> SCHEDULE YOUR MEETING TODAY! <span class="sh-booking-pulse-dot"></span> </div> <!-- Mobile Step Indicator (4 steps) --> <div class="sh-mobile-steps-indicator sh-mobile-only"> <div class="sh-mobile-step-dot active" data-step="1"><span>1</span></div> <div class="sh-mobile-step-line"></div> <div class="sh-mobile-step-dot" data-step="2"><span>2</span></div> <div class="sh-mobile-step-line"></div> <div class="sh-mobile-step-dot" data-step="3"><span>3</span></div> <div class="sh-mobile-step-line"></div> <div class="sh-mobile-step-dot" data-step="4"><span>4</span></div> </div> <div class="sh-inline-booking-content"> <!-- STEP 1: Form (Mobile) / Form Side (Desktop) --> <div class="sh-inline-booking-form sh-mobile-step active" data-mobile-step="1"> <div class="sh-inline-form-header sh-desktop-only"> <div class="sh-inline-form-icon"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <rect x="3" y="4" width="18" height="18" rx="2" ry="2"/> <line x1="16" y1="2" x2="16" y2="6"/> <line x1="8" y1="2" x2="8" y2="6"/> <line x1="3" y1="10" x2="21" y2="10"/> <polyline points="9 16 11 18 15 14"/> </svg> </div> <h3>Enter Your Details</h3> </div> <form class="sh-inline-form" novalidate autocomplete="on"> <div class="sh-inline-input-group sh-inline-email-group"> <input type="email" name="email" placeholder="Work Email*" required autocomplete="email" class="sh-inline-input"> <div class="sh-inline-loader"></div> </div> <div class="sh-inline-error sh-inline-error-email"></div> <div class="sh-inline-row"> <input type="text" name="firstName" placeholder="First Name*" required autocomplete="given-name" class="sh-inline-input"> <input type="text" name="lastName" placeholder="Last Name*" required autocomplete="family-name" class="sh-inline-input"> </div> <input type="text" name="title" placeholder="Job Title*" required autocomplete="organization-title" class="sh-inline-input"> <input type="text" name="company" placeholder="Company Name*" required autocomplete="organization" class="sh-inline-input"> <input type="hidden" name="phone" autocomplete="tel"> <p class="sh-inline-selected-time"></p> <!-- Desktop: Submit button --> <div class="sh-inline-submit-wrapper sh-desktop-only"> <button type="submit" class="sh-inline-submit" disabled> <span>BOOK MY CALL</span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg> </button> </div> <div class="sh-inline-error sh-inline-error-time sh-desktop-only"></div> <!-- Mobile: Next button --> <button type="button" class="sh-mobile-next-btn sh-mobile-only" disabled> <span>BOOK MY CALL</span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg> </button> </form> <!-- Success State (shown after booking on desktop) --> <div class="sh-inline-success sh-desktop-only" style="display: none;"> <div class="sh-inline-success-icon"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <polyline points="22 4 12 14.01 9 11.01"/> </svg> </div> <h3>You're All Set!</h3> <p class="sh-inline-success-details"></p> <p class="sh-inline-success-note">Check your email for confirmation details.</p> </div> </div> <!-- Calendar Side (Desktop) / Contains Steps 2 & 3 on Mobile --> <div class="sh-inline-booking-scheduler"> <div class="sh-inline-scheduler-header sh-desktop-only"> <h3>Select Your Meeting Date</h3> </div> <!-- Desktop Calendar View (shown first) --> <div class="sh-desktop-calendar-view sh-desktop-only"> <div class="sh-inline-booking-calendar"> <div class="sh-inline-cal-header"> <button type="button" class="sh-inline-cal-nav sh-inline-cal-prev" aria-label="Previous month"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <span class="sh-inline-cal-month"></span> <button type="button" class="sh-inline-cal-nav sh-inline-cal-next" aria-label="Next month"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg> </button> </div> <div class="sh-inline-cal-grid"> <div class="sh-inline-cal-weekdays"> <span>MON</span><span>TUE</span><span>WED</span><span>THU</span><span>FRI</span> </div> <div class="sh-inline-cal-days"></div> </div> </div> </div> <!-- Desktop Times View (replaces calendar after date selection) --> <div class="sh-desktop-times-view sh-desktop-only" style="display: none;"> <!-- Back to Calendar button with date --> <button type="button" class="sh-back-to-calendar"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> <span class="sh-selected-date-text"></span> </button> <!-- Timezone below date --> <div class="sh-inline-cal-timezone"> <label>Timezone:</label> <select class="sh-inline-tz-select sh-desktop-tz-select"></select> </div> <div class="sh-inline-slots-header">Available Times</div> <div class="sh-desktop-slots-container"> <div class="sh-inline-slots-placeholder"> <p>Loading times...</p> </div> </div> </div> <!-- Mobile Steps (unchanged) --> <div class="sh-inline-scheduler-content sh-mobile-only"> <!-- STEP 2: Pick a Day (Mobile) --> <div class="sh-inline-booking-calendar sh-mobile-step" data-mobile-step="2"> <div class="sh-mobile-step-header sh-mobile-only"> <button type="button" class="sh-mobile-back-btn" data-goto-step="1"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <h3>Pick a Day</h3> </div> <div class="sh-inline-cal-header"> <button type="button" class="sh-inline-cal-nav sh-inline-cal-prev-mobile" aria-label="Previous month"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <span class="sh-inline-cal-month-mobile"></span> <button type="button" class="sh-inline-cal-nav sh-inline-cal-next-mobile" aria-label="Next month"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg> </button> </div> <div class="sh-inline-cal-grid"> <div class="sh-inline-cal-weekdays"> <span>MON</span><span>TUE</span><span>WED</span><span>THU</span><span>FRI</span> </div> <div class="sh-inline-cal-days-mobile"></div> </div> <div class="sh-inline-cal-timezone"> <label>Timezone:</label> <select class="sh-mobile-tz-select"></select> </div> </div> <!-- STEP 3: Pick a Time (Mobile) --> <div class="sh-inline-cal-slots sh-mobile-step" data-mobile-step="3"> <div class="sh-mobile-step-header sh-mobile-only"> <button type="button" class="sh-mobile-back-btn" data-goto-step="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <h3>Pick a Time</h3> </div> <!-- Selected date display (mobile) --> <div class="sh-mobile-selected-date sh-mobile-only"> <span class="sh-mobile-date-text"></span> </div> <!-- Timezone selector (mobile) --> <div class="sh-mobile-timezone sh-mobile-only"> <label>Timezone:</label> <select class="sh-mobile-tz-select-slots"></select> </div> <div class="sh-inline-slots-placeholder"> <p>Select a date</p> </div> </div> </div> </div> <!-- STEP 4: Confirm (Mobile Only) --> <div class="sh-mobile-confirm-step sh-mobile-step sh-mobile-only" data-mobile-step="4"> <div class="sh-mobile-step-header"> <button type="button" class="sh-mobile-back-btn" data-goto-step="3"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <h3>Confirm</h3> </div> <div class="sh-mobile-confirm-card"> <p class="sh-mobile-confirm-date"></p> <p class="sh-mobile-confirm-time"></p> </div> <button type="button" class="sh-mobile-confirm-btn"> <span>BOOK MY CALL</span> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg> </button> <!-- Mobile Success State --> <div class="sh-mobile-success" style="display: none;"> <div class="sh-inline-success-icon"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <polyline points="22 4 12 14.01 9 11.01"/> </svg> </div> <h3>You're All Set!</h3> <p class="sh-mobile-success-details"></p> <p class="sh-inline-success-note">Check your email for confirmation.</p> </div> </div> </div> </div> <style> /* Inline Booking Shortcode Styles */ .sh-inline-booking { background:rgb(216, 216, 216); border: 0px solid var(--sh-border-default); border-radius: 16px; padding: 0; margin: 20px auto; max-width: 700px; overflow: hidden; } .sh-inline-booking-topbar { background: #000000; color: #ffffff; text-align: center; padding: 14px 20px; font-size: 14px; font-weight: 700; letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 28px; display: flex; align-items: center; justify-content: center; gap: 10px; } .sh-booking-pulse-dot { width: 8px; height: 8px; background: var(--sh-page-highlight); border-radius: 50%; animation: sh-booking-pulse 1.5s ease-in-out infinite; box-shadow: 0 0 8px var(--sh-page-highlight); } @keyframes sh-booking-pulse { 0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 8px var(--sh-page-highlight); } 50% { opacity: 0.4; transform: scale(0.8); box-shadow: 0 0 4px var(--sh-page-highlight); } } .sh-inline-booking-header { text-align: center; padding: 32px 32px 0; margin-bottom: 32px; } .sh-inline-booking-header strong { color: var(--sh-page-highlight); } .sh-inline-booking-title { font-size: 28px; font-weight: 700; color: #1a1a2e; margin: 0 0 8px; } .sh-inline-booking-subtitle { font-size: 16px; color: rgba(0, 0, 0, 0.7); margin: 0; } .sh-inline-booking-content { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; align-items: stretch; padding: 0 32px 32px; } @media (max-width: 900px) { .sh-inline-booking-content { grid-template-columns: 1fr; } } @media (max-width: 768px) { .sh-inline-booking { padding: 0; margin: 10px; border-radius: 12px; } } /* Scheduler Container (Calendar + Times) */ .sh-inline-booking-scheduler { background: #e9eaec; border-radius: 12px; padding: 20px; } .sh-inline-scheduler-header h3 { font-size: 18px; font-weight: 600; color: #1a1a2e; margin: 0 0 20px; text-align: center; } .sh-inline-scheduler-content { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } @media (max-width: 700px) { .sh-inline-scheduler-content { grid-template-columns: 1fr; } } /* Desktop Two-Step Calendar Flow */ .sh-desktop-calendar-view, .sh-desktop-times-view { width: 100%; } /* Back to Calendar button */ .sh-back-to-calendar { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 8px 12px; margin-bottom: 10px; background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.1); border: 1px solid rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.3); border-radius: 8px; color: var(--sh-page-highlight); font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; } .sh-back-to-calendar:hover { background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.2); } .sh-back-to-calendar svg { width: 18px; height: 18px; } /* Desktop slots container */ .sh-desktop-slots-container { max-height: 220px; overflow-y: auto; } .sh-desktop-slots-container .sh-inline-slots-grid { display: flex; flex-direction: column; gap: 8px; } .sh-desktop-times-view .sh-back-to-calendar { margin-bottom: 8px; } .sh-desktop-times-view .sh-inline-cal-timezone { margin-bottom: 10px; } .sh-desktop-times-view .sh-inline-cal-timezone label { font-size: 12px; } .sh-desktop-times-view .sh-inline-tz-select { padding: 6px 10px; font-size: 12px; } /* Calendar Styles */ .sh-inline-booking-calendar { /* No extra background since parent has it */ } .sh-inline-cal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } .sh-inline-cal-month, .sh-inline-cal-month-mobile { font-size: 14px; font-weight: 600; color: #1a1a2e; } .sh-inline-cal-nav { width: 32px; height: 32px; border: 1px solid rgba(0, 0, 0, 0.12); background: rgb(216, 216, 216); border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } .sh-inline-cal-nav:hover { background: #f0f1f3; } .sh-inline-cal-nav svg { width: 18px; height: 18px; color: rgba(0, 0, 0, 0.7); } .sh-inline-cal-weekdays { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; margin-bottom: 8px; } .sh-inline-cal-weekdays span { text-align: center; font-size: 11px; font-weight: 600; color: rgba(0, 0, 0, 0.5); text-transform: uppercase; padding: 8px 0; } .sh-inline-cal-days, .sh-inline-cal-days-mobile { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; } .sh-inline-cal-day { aspect-ratio: 1; display: flex; align-items: center; justify-content: center; font-size: 14px; border-radius: 8px; cursor: pointer; transition: all 0.2s; background: transparent; border: none; color: #1a1a2e; } .sh-inline-cal-day:hover:not(.disabled):not(.selected) { background: #f0f1f3; } .sh-inline-cal-day.disabled { color: rgba(0, 0, 0, 0.5); cursor: not-allowed; } .sh-inline-cal-day.selected { background: var(--sh-page-highlight); color: var(--sh-text-inverted); } /* Today is disabled - no special styling needed */ .sh-inline-cal-timezone { margin-top: 16px; display: flex; align-items: center; gap: 8px; font-size: 13px; color: #555555 !important; /* Hardcoded - no theme change */ } .sh-inline-cal-timezone label { color: #555555 !important; /* Hardcoded - no theme change */ } .sh-inline-tz-select { flex: 1; padding: 8px 12px; background: #ffffff !important; /* Hardcoded - no theme change */ border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: 8px; color: #1a1a2e !important; /* Hardcoded - no theme change */ font-size: 13px; } .sh-inline-cal-slots { display: flex; flex-direction: column; } .sh-inline-slots-header { font-size: 13px; font-weight: 600; color: #1a1a2e; margin-bottom: 8px; } .sh-inline-slots-placeholder { text-align: center; padding: 40px 20px; color: rgba(0, 0, 0, 0.5); font-size: 14px; background: #ffffff; border-radius: 8px; flex: 1; display: flex; align-items: center; justify-content: center; } .sh-inline-slots-grid { display: flex; flex-direction: column; gap: 8px; max-height: 280px; overflow-y: auto; } .sh-inline-slot { padding: 12px 16px; background: #ffffff; border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 8px; font-size: 14px; color: #1a1a2e; cursor: pointer; transition: all 0.2s; text-align: center; font-weight: 500; } .sh-inline-slot:hover { border-color: var(--sh-page-highlight); background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.1); } .sh-inline-slot.selected { background: var(--sh-page-highlight); border-color: var(--sh-page-highlight); color: var(--sh-text-inverted); } .sh-inline-selected-time { display: none !important; } .sh-inline-slots-loading { text-align: center; padding: 20px; color: rgba(0, 0, 0, 0.5); } .sh-inline-spinner { width: 24px; height: 24px; border: 2px solid rgba(0, 0, 0, 0.12); border-top-color: var(--sh-page-highlight); border-radius: 50%; animation: sh-inline-spin 0.8s linear infinite; margin: 0 auto 8px; } @keyframes sh-inline-spin { to { transform: rotate(360deg); } } /* Form Styles */ .sh-inline-booking-form { display: flex; flex-direction: column; background: #e9eaec; border-radius: 12px; padding: 20px; } .sh-inline-form-header { margin-bottom: 16px; text-align: center; } .sh-inline-form-icon { width: 48px; height: 48px; margin: 0 auto 12px; background: var(--sh-page-highlight); border-radius: 50%; display: flex; align-items: center; justify-content: center; } .sh-inline-form-icon svg { width: 24px; height: 24px; color: var(--sh-text-inverted); } .sh-inline-form-header h3 { font-size: 18px; font-weight: 600; color: #1a1a2e; margin: 0; } .sh-inline-form { display: flex; flex-direction: column; gap: 10px; } .sh-inline-input-group { position: relative; } .sh-inline-loader { position: absolute; right: 12px; top: 50%; margin-top: -9px; /* Half of height to center vertically */ width: 18px; height: 18px; border: 2px solid rgba(0, 0, 0, 0.12); border-top-color: var(--sh-page-highlight); border-radius: 50%; animation: sh-inline-spin 0.8s linear infinite; display: none; } .sh-inline-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .sh-inline-input { width: 100%; padding: 14px 16px; background: #f0f1f3; border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 10px; font-size: 14px; color: #1a1a2e; transition: border-color 0.2s, box-shadow 0.2s; box-sizing: border-box; } .sh-inline-input::placeholder { color: rgba(0, 0, 0, 0.5); } /* Fix browser autofill styling */ .sh-inline-input:-webkit-autofill, .sh-inline-input:-webkit-autofill:hover, .sh-inline-input:-webkit-autofill:focus, .sh-inline-input:-webkit-autofill:active { -webkit-box-shadow: 0 0 0 30px #f0f1f3 inset !important; -webkit-text-fill-color: #1a1a2e !important; caret-color: #1a1a2e; transition: background-color 5000s ease-in-out 0s; animation-name: onAutoFillStart; } /* Animation to detect autofill */ @keyframes onAutoFillStart { from { /* empty */ } to { /* empty */ } } .sh-inline-input:focus { outline: none; border-color: var(--sh-page-highlight); box-shadow: 0 0 0 3px rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.15); } .sh-inline-input.error { border-color: var(--sh-red); } .sh-inline-error { font-size: 12px; color: var(--sh-red); min-height: 0; margin-top: -4px; } .sh-inline-error:empty { display: none; } .sh-inline-error-time { margin-top: 8px; font-size: 13px; font-weight: 500; text-align: center; } @keyframes sh-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-8px); } 40% { transform: translateX(8px); } 60% { transform: translateX(-6px); } 80% { transform: translateX(6px); } } .sh-inline-submit { width: 100%; padding: 16px 24px; margin-top: 8px; background: var(--sh-page-highlight); border: none; border-radius: 10px; font-size: 16px; font-weight: 600; color: var(--sh-text-inverted); cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; } .sh-inline-submit:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.4); } .sh-inline-submit-wrapper { position: relative; width: 100%; } .sh-inline-submit:disabled { cursor: not-allowed; pointer-events: none; /* Keep button eye-catching even when disabled */ } /* Make wrapper clickable when button is disabled */ .sh-inline-submit-wrapper:has(.sh-inline-submit:disabled) { cursor: pointer; } .sh-inline-submit svg { width: 20px; height: 20px; } /* Success State */ .sh-inline-success { text-align: center; padding: 40px 20px; } .sh-inline-success-icon { width: 64px; height: 64px; margin: 0 auto 20px; background: rgba(var(--sh-green-rgb, 34, 197, 94), 0.15); border-radius: 50%; display: flex; align-items: center; justify-content: center; } .sh-inline-success-icon svg { width: 32px; height: 32px; color: var(--sh-green); } .sh-inline-success h3 { font-size: 24px; font-weight: 600; color: #1a1a2e; margin: 0 0 12px; } .sh-inline-success-details { font-size: 16px; color: var(--sh-page-highlight); margin: 0 0 8px; } .sh-inline-success-note { font-size: 14px; color: rgba(0, 0, 0, 0.7); margin: 0; } /* ===================================================== MOBILE STEP-BY-STEP FLOW (3 Steps) ===================================================== */ /* Hide mobile-only elements on desktop */ .sh-mobile-only { display: none !important; } /* Hide desktop-only elements on mobile */ @media (max-width: 768px) { .sh-desktop-only { display: none !important; } .sh-mobile-only { display: block !important; } /* Mobile step indicator - hidden on step 1 to reduce friction */ .sh-mobile-steps-indicator { display: flex !important; align-items: center; justify-content: center; gap: 12px; padding: 20px 20px 0; margin-bottom: 16px; } /* Hide step indicator on first step to not scare users */ .sh-inline-booking:has(.sh-inline-booking-form.active) .sh-mobile-steps-indicator { display: none !important; } .sh-mobile-step-dot { width: 28px; height: 28px; border-radius: 50%; background: #e9eaec; border: 2px solid rgba(0, 0, 0, 0.12); display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: rgba(0, 0, 0, 0.5); transition: all 0.2s; } .sh-mobile-step-dot.active { background: var(--sh-page-highlight); border-color: var(--sh-page-highlight); color: var(--sh-text-inverted); } .sh-mobile-step-dot.completed { background: var(--sh-green); border-color: var(--sh-green); color: var(--sh-text-inverted); } .sh-mobile-step-line { width: 32px; height: 2px; background: rgba(0, 0, 0, 0.12); } /* Mobile step containers */ .sh-mobile-step { display: none !important; } .sh-mobile-step.active { display: block !important; } /* Override booking content grid on mobile */ .sh-inline-booking-content { display: block; } /* Mobile step headers */ .sh-mobile-step-header { display: flex !important; align-items: center; gap: 12px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid rgba(0, 0, 0, 0.06); } .sh-mobile-step-header h3 { font-size: 16px; font-weight: 600; color: #1a1a2e; margin: 0; flex: 1; } /* Mobile back button */ .sh-mobile-back-btn { width: 32px; height: 32px; padding: 0; background: transparent; border: none; border-radius: 8px; color: rgba(0, 0, 0, 0.7); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; flex-shrink: 0; } .sh-mobile-back-btn:hover, .sh-mobile-back-btn:active { background: #e9eaec; color: #1a1a2e; } .sh-mobile-back-btn svg { width: 20px; height: 20px; } /* Mobile next button */ .sh-mobile-next-btn { width: 100%; padding: 14px 20px; margin-top: 8px; background: var(--sh-page-highlight); border: none; border-radius: 10px; font-size: 15px; font-weight: 600; color: var(--sh-text-inverted); cursor: pointer; display: flex !important; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; } .sh-mobile-next-btn:disabled { cursor: not-allowed; /* Keep button eye-catching even when disabled */ } .sh-mobile-next-btn svg { width: 18px; height: 18px; } /* Scheduler container on mobile */ .sh-inline-booking-scheduler { display: contents; /* Let children flow into parent grid */ } .sh-inline-booking-scheduler .sh-inline-scheduler-content { display: contents; } /* Step 2: Calendar */ .sh-inline-booking-calendar.active { display: block !important; background: #e9eaec; border-radius: 12px; padding: 16px; } /* Step 3: Time Slots */ .sh-inline-cal-slots.active { display: block !important; background: #e9eaec; border-radius: 12px; padding: 16px; } /* Show mobile elements inside time slots */ .sh-inline-cal-slots .sh-mobile-step-header { display: flex !important; } .sh-inline-cal-slots .sh-mobile-selected-date { display: block !important; } .sh-inline-cal-slots .sh-mobile-timezone { display: flex !important; } /* Hide desktop-only header inside time slots on mobile */ .sh-inline-cal-slots .sh-inline-slots-header.sh-desktop-only { display: none !important; } /* Mobile selected date display */ .sh-mobile-selected-date { background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.1); border: 1px solid rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.3); border-radius: 10px; padding: 12px 16px; margin-bottom: 12px; text-align: center; } .sh-mobile-date-text { font-size: 15px; font-weight: 600; color: var(--sh-page-highlight); } /* Mobile timezone selector - Hardcoded colors */ .sh-mobile-timezone { align-items: center; gap: 8px; margin-bottom: 16px; font-size: 14px; color: #555555 !important; } .sh-mobile-timezone label { flex-shrink: 0; color: #555555 !important; } .sh-mobile-tz-select { flex: 1; padding: 10px 12px; background: #f0f1f3 !important; border: 1px solid rgba(0, 0, 0, 0.12) !important; border-radius: 8px; color: #1a1a2e !important; font-size: 16px; /* Prevent iOS zoom */ } /* Full width time slots with scrolling */ .sh-inline-cal-slots .sh-inline-slots-grid { display: flex; flex-direction: column; gap: 10px; max-height: 45vh; overflow-y: auto; padding-right: 4px; -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ } .sh-inline-cal-slots .sh-inline-slot { width: 100%; padding: 14px 16px; font-size: 16px; flex-shrink: 0; /* Prevent slots from shrinking */ } /* Prevent iOS zoom on input focus - must be 16px minimum */ .sh-inline-input { font-size: 16px !important; } .sh-inline-tz-select { font-size: 16px !important; } /* Full width form on mobile */ .sh-inline-booking { margin: 0; border-radius: 0; border-left: none; border-right: none; max-width: 100%; } .sh-inline-booking-topbar { padding: 12px 16px; font-size: 13px; letter-spacing: 1px; margin-bottom: 0px; } .sh-inline-booking-header { padding: 20px 20px 0; margin-bottom: 20px; } .sh-inline-booking-content { padding: 0 20px 20px; } .sh-inline-booking-form { padding: 16px; } /* Stack first/last name on mobile */ .sh-inline-row { grid-template-columns: 1fr; } /* Mobile confirm step */ .sh-mobile-confirm-step { padding: 0; } .sh-mobile-confirm-card { padding: 16px; background: #e9eaec; border-radius: 12px; margin-bottom: 16px; text-align: center; } .sh-mobile-confirm-date { font-size: 15px; font-weight: 600; color: #1a1a2e; margin: 0 0 4px; } .sh-mobile-confirm-time { font-size: 18px; color: var(--sh-page-highlight); margin: 0; font-weight: 700; } .sh-mobile-confirm-btn { width: 100%; padding: 14px 20px; background: var(--sh-page-highlight); border: none; border-radius: 10px; font-size: 15px; font-weight: 600; color: var(--sh-text-inverted); cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; } .sh-mobile-confirm-btn:disabled { opacity: 0.5; cursor: not-allowed; } .sh-mobile-confirm-btn svg { width: 18px; height: 18px; } /* Mobile success state */ .sh-mobile-success { text-align: center; padding: 32px 16px; } .sh-mobile-success h3 { font-size: 20px; font-weight: 600; color: #1a1a2e; margin: 0 0 8px; } .sh-mobile-success-details { font-size: 14px; color: var(--sh-page-highlight); margin: 0 0 4px; } /* Hide form header on mobile */ .sh-inline-form-header { display: none; } } /* ===================================================== MODAL CONTEXT OVERRIDES When shortcode is inside the booking modal ===================================================== */ .sh-modal-inner .sh-inline-booking { margin: 0; max-width: none; } .sh-modal-inner .sh-inline-booking-header { display: none; } /* Modal: ensure desktop views display properly */ .sh-modal-inner .sh-desktop-calendar-view, .sh-modal-inner .sh-desktop-times-view { min-height: 320px; } .sh-modal-inner .sh-desktop-slots-container { max-height: 200px; } </style> <script> // Wait for shBooking to be available (loaded in footer) (function initBookingWidget() { if (typeof window.shBooking === 'undefined') { // shBooking not ready yet, wait and retry setTimeout(initBookingWidget, 100); return; } const container = document.getElementById('sh-booking-6051'); if (!container) return; // DOM Elements - Desktop const calDays = container.querySelector('.sh-inline-cal-days'); const calMonth = container.querySelector('.sh-inline-cal-month'); const calPrev = container.querySelector('.sh-inline-cal-prev'); const calNext = container.querySelector('.sh-inline-cal-next'); const tzSelect = container.querySelector('.sh-inline-tz-select'); // Desktop two-step view elements const desktopCalendarView = container.querySelector('.sh-desktop-calendar-view'); const desktopTimesView = container.querySelector('.sh-desktop-times-view'); const backToCalendarBtn = container.querySelector('.sh-back-to-calendar'); const selectedDateText = container.querySelector('.sh-selected-date-text'); const desktopSlotsContainer = container.querySelector('.sh-desktop-slots-container'); const desktopTzSelect = container.querySelector('.sh-desktop-tz-select'); // DOM Elements - Mobile const calDaysMobile = container.querySelector('.sh-inline-cal-days-mobile'); const calMonthMobile = container.querySelector('.sh-inline-cal-month-mobile'); const calPrevMobile = container.querySelector('.sh-inline-cal-prev-mobile'); const calNextMobile = container.querySelector('.sh-inline-cal-next-mobile'); const calSlots = container.querySelector('.sh-inline-cal-slots'); const mobileTzSelect = container.querySelector('.sh-mobile-tz-select'); const mobileTzSelectSlots = container.querySelector('.sh-mobile-tz-select-slots'); // Form elements const form = container.querySelector('.sh-inline-form'); const emailInput = container.querySelector('input[name="email"]'); const emailLoader = container.querySelector('.sh-inline-loader'); const errorEmail = container.querySelector('.sh-inline-error-email'); const submitBtn = container.querySelector('.sh-inline-submit'); const selectedTimeDisplay = container.querySelector('.sh-inline-selected-time'); const successDiv = container.querySelector('.sh-inline-success'); const successDetails = container.querySelector('.sh-inline-success-details'); // State let currentMonth = new Date(); let selectedDate = null; let selectedSlot = null; let selectedCalendarId = null; let slotAvailability = {}; let zoomInfoLookedUp = false; let lastLookedUpEmail = ''; // Track which email was looked up let userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; let currentMobileStep = 1; // Session ID for lead deduplication (persists across form submissions) let sessionId = localStorage.getItem('sh_lead_session_id'); if (!sessionId) { sessionId = 'sh_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); localStorage.setItem('sh_lead_session_id', sessionId); } // Landing Page URL - capture the first page with ad parameters // This persists the original landing page URL even if user navigates to other pages let landingPageUrl = localStorage.getItem('sh_landing_page_url'); const currentUrl = window.location.href; const hasAdParams = /[?&](gclid|hsa_|utm_|gbraid|wbraid)/i.test(currentUrl); // If current URL has ad params, always update (they may have clicked a new ad) // Otherwise, keep the stored landing page or use current URL as fallback if (hasAdParams) { landingPageUrl = currentUrl; localStorage.setItem('sh_landing_page_url', currentUrl); } else if (!landingPageUrl) { // No stored landing page and no ad params - use current URL landingPageUrl = currentUrl; } // Check if mobile view (either by screen width or if mobile step indicator is visible) function isMobileView() { // Check if the mobile step indicator is displayed (more reliable than just width) const stepIndicator = container.querySelector('.sh-mobile-steps-indicator'); if (stepIndicator) { const style = window.getComputedStyle(stepIndicator); if (style.display !== 'none') { return true; } } return window.innerWidth <= 768; } // Desktop: Toggle between calendar and times view function showDesktopCalendarView() { if (isMobileView()) return; if (desktopCalendarView) desktopCalendarView.style.display = 'block'; if (desktopTimesView) desktopTimesView.style.display = 'none'; } function showDesktopTimesView() { if (isMobileView()) return; if (desktopCalendarView) desktopCalendarView.style.display = 'none'; if (desktopTimesView) desktopTimesView.style.display = 'block'; // Update the selected date text in back button (abbreviated to fit one line) if (selectedDate && selectedDateText) { selectedDateText.textContent = selectedDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', timeZone: userTimezone }); } } // Back to calendar button handler if (backToCalendarBtn) { backToCalendarBtn.addEventListener('click', function() { showDesktopCalendarView(); }); } // Mobile step navigation (3 steps: 1=Form, 2=Date&Time, 3=Confirm) function goToMobileStep(step) { if (!isMobileView()) return; currentMobileStep = step; // Update step visibility container.querySelectorAll('.sh-mobile-step').forEach(el => { const stepNum = parseInt(el.dataset.mobileStep); if (stepNum === step) { el.classList.add('active'); } else { el.classList.remove('active'); } }); // Update step indicator dots container.querySelectorAll('.sh-mobile-step-dot').forEach(dot => { const dotStep = parseInt(dot.dataset.step); dot.classList.remove('active', 'completed'); if (dotStep === step) { dot.classList.add('active'); } else if (dotStep < step) { dot.classList.add('completed'); } }); // If going to form step (1), focus email field if (step === 1) { setTimeout(() => { const emailField = container.querySelector('input[name="email"]'); if (emailField && !emailField.value) { emailField.focus(); } }, 100); } // If going to time step (3), show selected date if (step === 3) { updateMobileDateDisplay(); } // If going to confirm step (4), populate the confirm details if (step === 4) { populateMobileConfirm(); } } // Update mobile date display on step 3 function updateMobileDateDisplay() { const dateText = container.querySelector('.sh-mobile-date-text'); if (dateText && selectedDate) { dateText.textContent = selectedDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', timeZone: userTimezone }); } } // Populate mobile confirm step function populateMobileConfirm() { const mobileConfirmDate = container.querySelector('.sh-mobile-confirm-date'); const mobileConfirmTime = container.querySelector('.sh-mobile-confirm-time'); if (selectedSlot && mobileConfirmDate && mobileConfirmTime) { const slotDate = new Date(selectedSlot); mobileConfirmDate.textContent = slotDate.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric', timeZone: userTimezone }); mobileConfirmTime.textContent = slotDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); } } // Validate form fields for mobile step 1 // Free email domains that are not allowed const freeEmailDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'aol.com', 'icloud.com', 'mail.com', 'protonmail.com', 'zoho.com', 'yandex.com', 'gmx.com', 'live.com', 'msn.com']; // Strict email validation: name@domain.tld (TLD must be 2-6 letters only) function isValidEmail(email) { // Basic format check: something@something.something const basicRegex = /^[^\s@]+@[^\s@]+\.[a-zA-Z]{2,6}$/; if (!basicRegex.test(email)) return false; // Additional check: TLD should only be letters const parts = email.split('.'); const tld = parts[parts.length - 1]; if (!/^[a-zA-Z]{2,6}$/.test(tld)) return false; return true; } function validateFormFields() { const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); // Check if email is valid format if (!isValidEmail(email)) { return { valid: false, error: email.includes('@') ? 'Please enter a valid email address' : null }; } // Check for free email domains const domain = email.split('@')[1]?.toLowerCase(); if (domain && freeEmailDomains.includes(domain)) { return { valid: false, error: 'Please use your work email address' }; } // Check other required fields if (!firstName || !lastName || !title || !company) { return { valid: false, error: null }; } return { valid: true, error: null }; } // Update mobile next button state and show email errors function updateMobileNextBtn() { const btn = container.querySelector('.sh-mobile-next-btn'); const emailError = container.querySelector('.sh-inline-error-email'); const validation = validateFormFields(); if (btn) { btn.disabled = !validation.valid; } if (emailError) { emailError.textContent = validation.error || ''; } } // Direct click handlers for mobile buttons const mobileNextBtn = container.querySelector('.sh-mobile-next-btn'); if (mobileNextBtn) { mobileNextBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); const validation = validateFormFields(); if (validation.valid) { // Send form data to Zapier on step 1 completion (lead capture) sendFormToZapier(); goToMobileStep(2); } }); } // Send form data to Lead CPT + HubSpot (step 1 completion - lead capture) function sendFormToZapier() { const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); const phone = container.querySelector('input[name="phone"]').value.trim(); // Also submit to HubSpot for the step 1 completion submitToHubSpotPartial(email, firstName, lastName, title, company, phone, 'Step 1 - Info Submitted'); // Device type const ua = navigator.userAgent; let deviceType = 'Desktop'; if (/tablet|ipad|playbook|silk/i.test(ua)) deviceType = 'Tablet'; else if (/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i.test(ua)) deviceType = 'Mobile'; // GCLID const params = new URLSearchParams(window.location.search); let gclid = params.get('gclid') || ''; if (!gclid) { const cookie = document.cookie.match(/(?:^|; )gclid=([^;]*)/); gclid = cookie ? cookie[1] : ''; } // UTMs const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; const utms = {}; utmKeys.forEach(key => { let value = params.get(key); if (!value) { const cookie = document.cookie.match(new RegExp('(?:^|; )' + key + '=([^;]*)')); value = cookie ? cookie[1] : ''; } utms[key] = value; }); const payload = { 'Email': email, 'First Name': firstName, 'Last Name': lastName, 'Job Title': title, 'Company': company, 'Phone': phone || '', 'Device': deviceType, 'Form Type': (container.dataset.formType || 'Inline Booking Shortcode') + ' - Step 1', 'Form Step': 'Step 1 - Info Submitted', 'Page Url': landingPageUrl, 'Page Title': document.title, 'Gclid': gclid, 'Utm Source': utms.utm_source || '', 'Utm Medium': utms.utm_medium || '', 'Utm Campaign': utms.utm_campaign || '', 'Utm Term': utms.utm_term || '', 'Utm Content': utms.utm_content || '', 'Session ID': sessionId, 'Theme Mode': document.body.classList.contains('light-mode') ? 'light' : 'dark', 'Highlight Color': getComputedStyle(document.documentElement).getPropertyValue('--sh-page-highlight').trim() }; // Save to localStorage for later localStorage.setItem('sh_chatbot_user', JSON.stringify({ email, firstName, lastName, title, company, phone })); // Save to Leads CPT (will deduplicate and send to Zapier after 2 min) const formData = new FormData(); formData.append('action', 'sh_save_lead'); formData.append('nonce', shBooking.nonce); formData.append('payload', JSON.stringify(payload)); fetch(shBooking.ajaxUrl, { method: 'POST', body: formData }) .then(r => r.json()) .catch(() => {}); } // Back buttons - use event delegation since they're in different steps container.addEventListener('click', function(e) { const backBtn = e.target.closest('.sh-mobile-back-btn'); if (backBtn) { e.preventDefault(); e.stopPropagation(); const gotoStep = parseInt(backBtn.dataset.gotoStep); if (gotoStep) { goToMobileStep(gotoStep); } } }); // Confirm button const mobileConfirmBtn = container.querySelector('.sh-mobile-confirm-btn'); if (mobileConfirmBtn) { mobileConfirmBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); handleMobileConfirm(); }); } // Mobile confirm button handler async function handleMobileConfirm() { const mobileConfirmBtn = container.querySelector('.sh-mobile-confirm-btn'); if (!selectedSlot || !selectedCalendarId || !mobileConfirmBtn) { return; } const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); const phone = container.querySelector('input[name="phone"]').value.trim(); // Validate email before submission if (!isValidEmail(email)) { alert('Please enter a valid business email address'); goToMobileStep(1); return; } // Check for free email domains const emailDomain = email.split('@')[1]?.toLowerCase(); if (emailDomain && freeEmailDomains.includes(emailDomain)) { alert('Please use your business email'); goToMobileStep(1); return; } mobileConfirmBtn.disabled = true; mobileConfirmBtn.innerHTML = '<span>BOOKING...</span>'; try { const bookingData = new FormData(); bookingData.append('action', 'sh_create_calendar_booking'); bookingData.append('nonce', shBooking.nonce); bookingData.append('startTime', selectedSlot); bookingData.append('name', `${firstName} ${lastName}`); bookingData.append('email', email); bookingData.append('company', company); bookingData.append('timezone', userTimezone); // Rep will be randomly selected from available reps for this slot const bookingResponse = await fetch(shBooking.ajaxUrl, { method: 'POST', body: bookingData }).then(r => r.json()); if (bookingResponse.success) { localStorage.setItem('sh_chatbot_user', JSON.stringify({ email, firstName, lastName, title, company, phone })); sendToZapier({ email, firstName, lastName, title, company, phone }); showMobileSuccess(); } else { throw new Error(bookingResponse.data?.message || 'Booking failed'); } } catch (err) { mobileConfirmBtn.disabled = false; mobileConfirmBtn.innerHTML = '<span>BOOK MY CALL</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>'; alert(err.message || 'Booking failed. Please try again.'); } } // Show mobile success state function showMobileSuccess() { const confirmStep = container.querySelector('.sh-mobile-confirm-step'); const mobileSuccess = container.querySelector('.sh-mobile-success'); const mobileConfirmBtn = container.querySelector('.sh-mobile-confirm-btn'); const mobileSuccessDetails = container.querySelector('.sh-mobile-success-details'); const confirmCard = container.querySelector('.sh-mobile-confirm-card'); const stepHeader = confirmStep?.querySelector('.sh-mobile-step-header'); if (confirmStep && mobileSuccess) { if (confirmCard) confirmCard.style.display = 'none'; if (mobileConfirmBtn) mobileConfirmBtn.style.display = 'none'; if (stepHeader) stepHeader.style.display = 'none'; mobileSuccess.style.display = 'block'; if (mobileSuccessDetails && selectedSlot) { const slotDate = new Date(selectedSlot); mobileSuccessDetails.textContent = slotDate.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); } // Mark all steps completed container.querySelectorAll('.sh-mobile-step-dot').forEach(dot => { dot.classList.remove('active'); dot.classList.add('completed'); }); } } // Initialize mobile view if (isMobileView()) { goToMobileStep(1); } // Listen for form input changes (including keyup for better responsiveness) container.querySelectorAll('.sh-inline-form input').forEach(input => { input.addEventListener('input', updateMobileNextBtn); input.addEventListener('keyup', updateMobileNextBtn); input.addEventListener('change', updateMobileNextBtn); }); // Initial button state check updateMobileNextBtn(); // Timezones const timezones = [ { value: 'America/New_York', label: 'Eastern Time (ET)' }, { value: 'America/Chicago', label: 'Central Time (CT)' }, { value: 'America/Denver', label: 'Mountain Time (MT)' }, { value: 'America/Los_Angeles', label: 'Pacific Time (PT)' }, { value: 'America/Phoenix', label: 'Arizona (AZ)' }, { value: 'America/Anchorage', label: 'Alaska (AK)' }, { value: 'Pacific/Honolulu', label: 'Hawaii (HI)' }, { value: 'Europe/London', label: 'London (GMT)' }, { value: 'Europe/Paris', label: 'Paris (CET)' }, { value: 'Asia/Tokyo', label: 'Tokyo (JST)' }, { value: 'Australia/Sydney', label: 'Sydney (AEST)' } ]; // All timezone selects const allTzSelects = [tzSelect, desktopTzSelect, mobileTzSelect, mobileTzSelectSlots].filter(el => el); // Initialize all timezone selectors function populateTimezoneSelect(select) { if (!select) return; select.innerHTML = ''; // Add user's timezone first if not in list if (!timezones.find(t => t.value === userTimezone)) { const opt = document.createElement('option'); opt.value = userTimezone; opt.textContent = userTimezone; opt.selected = true; select.appendChild(opt); } timezones.forEach(tz => { const opt = document.createElement('option'); opt.value = tz.value; opt.textContent = tz.label; if (tz.value === userTimezone) opt.selected = true; select.appendChild(opt); }); } // Populate all selects allTzSelects.forEach(select => populateTimezoneSelect(select)); // Sync all timezone selects function syncTimezones(newValue) { userTimezone = newValue; allTzSelects.forEach(select => { if (select) select.value = newValue; }); if (selectedDate) fetchSlots(selectedDate); } // Add change listeners to all selects allTzSelects.forEach(select => { if (select) { select.addEventListener('change', () => syncTimezones(select.value)); } }); // Get URL parameters from data attributes const urlDate = container.dataset.urlDate || ''; const urlFirstname = container.dataset.urlFirstname || ''; const urlLastname = container.dataset.urlLastname || ''; const urlCompany = container.dataset.urlCompany || ''; const urlEmail = container.dataset.urlEmail || ''; const urlTitle = container.dataset.urlTitle || ''; // If URL params exist, save to localStorage (they take priority) if (urlEmail || urlFirstname || urlLastname || urlCompany || urlTitle) { try { const existing = JSON.parse(localStorage.getItem('sh_chatbot_user') || '{}'); const updated = { ...existing, ...(urlEmail && { email: urlEmail }), ...(urlFirstname && { firstName: urlFirstname }), ...(urlLastname && { lastName: urlLastname }), ...(urlCompany && { company: urlCompany }), ...(urlTitle && { title: urlTitle }) }; localStorage.setItem('sh_chatbot_user', JSON.stringify(updated)); } catch (e) {} } // Pre-fill form from localStorage try { const savedUser = localStorage.getItem('sh_chatbot_user'); if (savedUser) { const user = JSON.parse(savedUser); if (user.email) container.querySelector('input[name="email"]').value = user.email; if (user.firstName) container.querySelector('input[name="firstName"]').value = user.firstName; if (user.lastName) container.querySelector('input[name="lastName"]').value = user.lastName; if (user.title) container.querySelector('input[name="title"]').value = user.title; if (user.company) container.querySelector('input[name="company"]').value = user.company; if (user.phone) container.querySelector('input[name="phone"]').value = user.phone; if (user.firstName && user.lastName && user.company) zoomInfoLookedUp = true; } } catch (e) {} // Update mobile button state after pre-fill setTimeout(updateMobileNextBtn, 100); // Handle pre-selected date from URL // Using cached Google Calendar slots (synced every 5 min via cron) function handlePreselectDate() { if (!urlDate) return; const dateParts = urlDate.split('-'); if (dateParts.length !== 3) return; const targetYear = parseInt(dateParts[0]); const targetMonth = parseInt(dateParts[1]) - 1; // JS months are 0-indexed const targetDay = parseInt(dateParts[2]); const targetDate = new Date(targetYear, targetMonth, targetDay); // Set currentMonth to the target month for calendar rendering currentMonth = new Date(targetYear, targetMonth, 1); // Select the date (this will render calendar and fetch slots) selectDate(targetDate, false); // false = not user click, so won't trigger mobile step change // On desktop, show times view directly if (!isMobileView()) { showDesktopTimesView(); } } // Delay preselect to ensure calendar is ready setTimeout(handlePreselectDate, 300); // Calendar rendering (weekdays only - no weekends) function renderCalendar() { const year = currentMonth.getFullYear(); const month = currentMonth.getMonth(); const today = new Date(); today.setHours(0, 0, 0, 0); const monthText = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); // Update month text for both desktop and mobile if (calMonth) calMonth.textContent = monthText; if (calMonthMobile) calMonthMobile.textContent = monthText; const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); // Calculate empty cells for start of month (only for weekdays Mon-Fri) let emptyDays = 0; if (firstDay === 0) emptyDays = 0; else if (firstDay === 6) emptyDays = 0; else emptyDays = firstDay - 1; // Generate calendar days HTML function generateCalendarDays(container) { if (!container) return; container.innerHTML = ''; for (let i = 0; i < emptyDays; i++) { const empty = document.createElement('div'); empty.className = 'sh-inline-cal-day disabled'; container.appendChild(empty); } for (let day = 1; day <= daysInMonth; day++) { const date = new Date(year, month, day); const dayOfWeek = date.getDay(); // Skip Saturday (6) and Sunday (0) if (dayOfWeek === 0 || dayOfWeek === 6) continue; const dayEl = document.createElement('button'); dayEl.type = 'button'; dayEl.className = 'sh-inline-cal-day'; dayEl.textContent = day; // Disable today and all past dates (bookings start tomorrow) if (date <= today) { dayEl.classList.add('disabled'); } else { if (selectedDate && date.toDateString() === selectedDate.toDateString()) { dayEl.classList.add('selected'); } dayEl.addEventListener('click', () => selectDate(date, true)); } container.appendChild(dayEl); } } // Render to both desktop and mobile containers generateCalendarDays(calDays); generateCalendarDays(calDaysMobile); } // Get the next available weekday (Mon-Fri) - starts from tomorrow function getNextWeekday() { const tomorrow = new Date(); tomorrow.setHours(0, 0, 0, 0); tomorrow.setDate(tomorrow.getDate() + 1); // Start from tomorrow const dayOfWeek = tomorrow.getDay(); // If tomorrow is Saturday, next weekday is Monday (+2) if (dayOfWeek === 6) { tomorrow.setDate(tomorrow.getDate() + 2); } // If tomorrow is Sunday, next weekday is Monday (+1) else if (dayOfWeek === 0) { tomorrow.setDate(tomorrow.getDate() + 1); } // Otherwise, tomorrow is a weekday return tomorrow; } function selectDate(date, isUserClick = false) { selectedDate = date; selectedSlot = null; selectedCalendarId = null; updateSubmitState(); renderCalendar(); fetchSlots(date); // On desktop, show times view (replace calendar) - only on user click if (isUserClick && !isMobileView()) { showDesktopTimesView(); } // On mobile, advance to time selection (step 3) - only on user click if (isUserClick && isMobileView()) { goToMobileStep(3); } } // Cached slots from Google Calendar (fetched once, used for all dates) let cachedSlots = null; let slotsLoading = false; let slotsFetchPromise = null; function fetchAllSlots() { if (cachedSlots) { return Promise.resolve(cachedSlots); } if (slotsFetchPromise) { return slotsFetchPromise; } slotsLoading = true; const formData = new FormData(); formData.append('action', 'sh_get_calendar_slots'); formData.append('nonce', shBooking.nonce); formData.append('timezone', userTimezone); slotsFetchPromise = fetch(shBooking.ajaxUrl, { method: 'POST', body: formData }) .then(r => { if (!r.ok) throw new Error('Network response was not ok'); return r.json(); }) .then(response => { slotsLoading = false; if (response.success && response.data?.slots) { cachedSlots = response.data.slots; return cachedSlots; } return {}; }) .catch(() => { slotsLoading = false; return {}; }); return slotsFetchPromise; } function fetchSlots(date) { if (!window.shBooking) { if (desktopSlotsContainer) { desktopSlotsContainer.innerHTML = '<div class="sh-inline-slots-placeholder"><p>Calendar not configured.</p></div>'; } if (calSlots) { calSlots.innerHTML = '<div class="sh-inline-slots-placeholder"><p>Calendar not configured.</p></div>'; } return; } // Show loading state if (desktopSlotsContainer) { desktopSlotsContainer.innerHTML = '<div class="sh-inline-slots-loading"><div class="sh-inline-spinner"></div><p>Loading times...</p></div>'; } if (calSlots) { calSlots.innerHTML = '<div class="sh-inline-slots-loading"><div class="sh-inline-spinner"></div><p>Loading times...</p></div>'; } // Use cached slots (fetched once from Google Calendar) fetchAllSlots().then(slots => { if (slots && Object.keys(slots).length > 0) { renderSlots(slots); } else { renderSlotsPlaceholder('<p>No times available for this date</p>'); } }).catch(() => { renderSlotsPlaceholder('<p>Error loading times</p>'); }); } function renderSlots(slots) { // slots is an object keyed by date: { "2025-12-18": [{time: "...", calendars: [...]}] } const year = selectedDate.getFullYear(); const month = String(selectedDate.getMonth() + 1).padStart(2, '0'); const day = String(selectedDate.getDate()).padStart(2, '0'); const dateKey = `${year}-${month}-${day}`; const daySlots = slots[dateKey] || []; if (daySlots.length === 0) { renderSlotsPlaceholder('<p>No times available</p>'); return; } // Clear previous slot availability data slotAvailability = {}; // Build sorted times array const sortedTimes = daySlots.map(slot => { // Store which reps have this slot available (for random selection on booking) slotAvailability[slot.time] = slot.reps || slot.calendars || []; return slot.time; }).sort(); // Render to desktop container if (desktopSlotsContainer) { const desktopGrid = document.createElement('div'); desktopGrid.className = 'sh-inline-slots-grid'; sortedTimes.forEach(time => { const slotDate = new Date(time); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'sh-inline-slot'; btn.textContent = slotDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); if (selectedSlot === time) { btn.classList.add('selected'); } btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); selectSlot(time, this); }); desktopGrid.appendChild(btn); }); desktopSlotsContainer.innerHTML = ''; desktopSlotsContainer.appendChild(desktopGrid); } // Render to mobile container if (calSlots) { const mobileGrid = document.createElement('div'); mobileGrid.className = 'sh-inline-slots-grid'; sortedTimes.forEach(time => { const slotDate = new Date(time); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'sh-inline-slot'; btn.textContent = slotDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); if (selectedSlot === time) { btn.classList.add('selected'); } btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); selectSlot(time, this); }); mobileGrid.appendChild(btn); }); // Clear and rebuild mobile slots container calSlots.innerHTML = ''; // Add mobile header (back button + title) calSlots.appendChild(createMobileSlotsHeader()); // Add mobile date display calSlots.appendChild(createMobileDateDisplay()); // Add mobile timezone selector calSlots.appendChild(createMobileTimezoneSelector()); // Add the time slots grid calSlots.appendChild(mobileGrid); } } // Render placeholder for both desktop and mobile function renderSlotsPlaceholder(html) { if (desktopSlotsContainer) { desktopSlotsContainer.innerHTML = '<div class="sh-inline-slots-placeholder">' + html + '</div>'; } if (calSlots) { calSlots.innerHTML = ''; calSlots.appendChild(createMobileSlotsHeader()); calSlots.appendChild(createMobileDateDisplay()); calSlots.appendChild(createMobileTimezoneSelector()); const placeholder = document.createElement('div'); placeholder.className = 'sh-inline-slots-placeholder'; placeholder.innerHTML = html; calSlots.appendChild(placeholder); } } // Helper: Create mobile slots header with back button function createMobileSlotsHeader() { const header = document.createElement('div'); header.className = 'sh-mobile-step-header sh-mobile-only'; header.innerHTML = ` <button type="button" class="sh-mobile-back-btn" data-goto-step="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg> </button> <h3>Pick a Time</h3> `; return header; } // Helper: Create mobile date display function createMobileDateDisplay() { const dateDiv = document.createElement('div'); dateDiv.className = 'sh-mobile-selected-date sh-mobile-only'; const dateText = document.createElement('span'); dateText.className = 'sh-mobile-date-text'; if (selectedDate) { dateText.textContent = selectedDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', timeZone: userTimezone }); } dateDiv.appendChild(dateText); return dateDiv; } // Helper: Create mobile timezone selector function createMobileTimezoneSelector() { const tzDiv = document.createElement('div'); tzDiv.className = 'sh-mobile-timezone sh-mobile-only'; tzDiv.innerHTML = `<label>Timezone:</label>`; const select = document.createElement('select'); select.className = 'sh-mobile-tz-select'; // Add timezone options timezones.forEach(tz => { const opt = document.createElement('option'); opt.value = tz.value; opt.textContent = tz.label; if (tz.value === userTimezone) opt.selected = true; select.appendChild(opt); }); // Add user's timezone if not in list if (!timezones.find(t => t.value === userTimezone)) { const opt = document.createElement('option'); opt.value = userTimezone; opt.textContent = userTimezone; opt.selected = true; select.insertBefore(opt, select.firstChild); } // Handle timezone change - sync all selects select.addEventListener('change', () => { syncTimezones(select.value); }); tzDiv.appendChild(select); return tzDiv; } // Helper: Rebuild slots container with placeholder message function selectSlot(time, clickedBtn) { selectedSlot = time; // Randomly pick a calendar that has this slot const availableCalendars = slotAvailability[time] || []; if (availableCalendars.length > 0) { selectedCalendarId = availableCalendars[Math.floor(Math.random() * availableCalendars.length)]; } // Clear time error when a slot is selected const errorTime = container.querySelector('.sh-inline-error-time'); if (errorTime) errorTime.textContent = ''; // Update slot buttons UI container.querySelectorAll('.sh-inline-slot').forEach(el => el.classList.remove('selected')); if (clickedBtn) clickedBtn.classList.add('selected'); // Show selected time in form const slotDate = new Date(time); const dateStr = slotDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', timeZone: userTimezone }); const timeStr = slotDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); selectedTimeDisplay.textContent = `📅 ${dateStr} at ${timeStr}`; selectedTimeDisplay.classList.add('active'); updateSubmitState(); // On mobile, advance to confirm step (step 4) if (isMobileView()) { goToMobileStep(4); } } function updateSubmitState() { const validation = validateFormFields(); const emailError = container.querySelector('.sh-inline-error-email'); // Show email error if any if (emailError) { emailError.textContent = validation.error || ''; } // Desktop submit requires slot selection + valid form submitBtn.disabled = !(selectedSlot && validation.valid); } // Form input listeners form.querySelectorAll('input').forEach(input => { input.addEventListener('input', updateSubmitState); }); // ZoomInfo lookup emailInput.addEventListener('blur', async function() { const email = this.value.trim().toLowerCase(); const fnInput = container.querySelector('input[name="firstName"]'); // Always move focus to firstName field after email blur (Tab behavior) // Use setTimeout to ensure this happens after the blur event completes setTimeout(() => { if (fnInput && document.activeElement === document.body) { fnInput.focus(); } }, 0); if (!email) return; // Use strict email validation if (!isValidEmail(email)) return; // Skip if we already looked up this exact email if (email === lastLookedUpEmail) return; // Check if free email domain const genericDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'aol.com', 'icloud.com', 'mail.com', 'protonmail.com', 'zoho.com', 'yandex.com', 'gmx.com', 'live.com', 'msn.com']; const domain = email.split('@')[1]; const isFreeEmail = genericDomains.includes(domain.toLowerCase()); emailLoader.style.display = 'block'; lastLookedUpEmail = email; // Track this email zoomInfoLookedUp = true; try { const response = await fetch('/wp-admin/admin-ajax.php?action=zoominfo_proxy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ matchPersonInput: [{ emailAddress: email }], outputFields: ["firstName", "lastName", "jobTitle", "companyName", "phone", "mobilePhone"] }) }); const data = await response.json(); if (data?.data?.data?.result?.length) { const person = data.data.data.result[0].data[0]; const lnInput = container.querySelector('input[name="lastName"]'); const titleInput = container.querySelector('input[name="title"]'); const companyInput = container.querySelector('input[name="company"]'); const phoneInput = container.querySelector('input[name="phone"]'); // Replace field values with ZoomInfo data if (person.firstName && fnInput) { fnInput.value = person.firstName; } if (person.lastName && lnInput) { lnInput.value = person.lastName; } if (person.jobTitle && titleInput) { titleInput.value = person.jobTitle; } if ((person.companyName || person.company?.name) && companyInput) { companyInput.value = person.companyName || person.company?.name || ''; } if ((person.phone || person.mobilePhone) && phoneInput) { phoneInput.value = person.mobilePhone || person.phone || ''; } // Move focus to the first empty required field after populating const fieldsInOrder = [fnInput, lnInput, titleInput, companyInput]; for (const field of fieldsInOrder) { if (field && !field.value) { field.focus(); break; } } // Update both desktop and mobile button states updateSubmitState(); updateMobileNextBtn(); } } catch (err) { // ZoomInfo lookup failed silently } finally { emailLoader.style.display = 'none'; // Also update mobile button in case email was the last field needed updateMobileNextBtn(); // Send form details to Zapier after ZoomInfo lookup (even with partial data) // Mark free emails differently so we can track them const zapierStep = isFreeEmail ? 'Free Email Captured' : 'ZoomInfo Enriched'; sendPartialFormToZapier(zapierStep); } }); // HubSpot config for partial submissions const HUBSPOT_PORTAL_ID = '6880333'; const HUBSPOT_PARTIAL_FORM_GUID = '7bc3cca4-6c16-4a78-9707-561cefd46848'; // Get HubSpot tracking cookie function getHubspotUTK() { const match = document.cookie.match(/(?:^|; )hubspotutk=([^;]*)/); return match ? match[1] : ''; } // Get visitor IP address for HubSpot tracking let cachedIP = null; function getVisitorIP(callback) { if (cachedIP) { callback(cachedIP); return; } fetch('https://api.ipify.org?format=json') .then(r => r.json()) .then(d => { cachedIP = d.ip; callback(d.ip); }) .catch(() => callback('')); } // Submit to HubSpot Forms API (with IP tracking) function submitToHubSpotPartial(email, firstName, lastName, title, company, phone, formStep) { // Get IP first, then submit getVisitorIP(function(ip) { const fields = [ { name: 'email', value: email }, { name: 'firstname', value: firstName || '' }, { name: 'lastname', value: lastName || '' }, { name: 'jobtitle', value: title || '' }, { name: 'company', value: company || '' }, { name: 'phone', value: phone || '' } ]; // Add GCLID if available const params = new URLSearchParams(window.location.search); let gclid = params.get('gclid') || ''; if (!gclid) { const cookie = document.cookie.match(/(?:^|; )gclid=([^;]*)/); gclid = cookie ? cookie[1] : ''; } if (gclid) { fields.push({ name: 'hs_google_click_id', value: gclid }); } const body = { fields: fields, context: { hutk: getHubspotUTK(), pageUri: window.location.href, pageName: document.title + ' - ' + formStep } }; // Add IP if available if (ip) { body.context.ipAddress = ip; } fetch(`https://api.hsforms.com/submissions/v3/integration/submit/${HUBSPOT_PORTAL_ID}/${HUBSPOT_PARTIAL_FORM_GUID}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) .catch(() => {}); }); } // Send partial form data to Lead CPT AND HubSpot (used after ZoomInfo lookup) function sendPartialFormToZapier(formStep) { const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); const phone = container.querySelector('input[name="phone"]').value.trim(); // Must have at least email if (!email) return; // Also submit to HubSpot submitToHubSpotPartial(email, firstName, lastName, title, company, phone, formStep); // Device type const ua = navigator.userAgent; let deviceType = 'Desktop'; if (/tablet|ipad|playbook|silk/i.test(ua)) deviceType = 'Tablet'; else if (/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i.test(ua)) deviceType = 'Mobile'; // GCLID const params = new URLSearchParams(window.location.search); let gclid = params.get('gclid') || ''; if (!gclid) { const cookie = document.cookie.match(/(?:^|; )gclid=([^;]*)/); gclid = cookie ? cookie[1] : ''; } // UTMs const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; const utms = {}; utmKeys.forEach(key => { let value = params.get(key); if (!value) { const cookie = document.cookie.match(new RegExp('(?:^|; )' + key + '=([^;]*)')); value = cookie ? cookie[1] : ''; } utms[key] = value; }); const payload = { 'Email': email, 'First Name': firstName || '', 'Last Name': lastName || '', 'Job Title': title || '', 'Company': company || '', 'Phone': phone || '', 'Device': deviceType, 'Form Type': (container.dataset.formType || 'Inline Booking Shortcode') + ' - ' + formStep, 'Form Step': formStep, 'Page Url': landingPageUrl, 'Page Title': document.title, 'Gclid': gclid, 'Utm Source': utms.utm_source || '', 'Utm Medium': utms.utm_medium || '', 'Utm Campaign': utms.utm_campaign || '', 'Utm Term': utms.utm_term || '', 'Utm Content': utms.utm_content || '', 'Session ID': sessionId, 'Theme Mode': document.body.classList.contains('light-mode') ? 'light' : 'dark', 'Highlight Color': getComputedStyle(document.documentElement).getPropertyValue('--sh-page-highlight').trim() }; // Save to Leads CPT (will deduplicate and send to Zapier after 2 min) const formData = new FormData(); formData.append('action', 'sh_save_lead'); formData.append('nonce', shBooking.nonce); formData.append('payload', JSON.stringify(payload)); fetch(shBooking.ajaxUrl, { method: 'POST', body: formData }) .then(r => r.json()) .catch(() => {}); } // Time error element const errorTime = container.querySelector('.sh-inline-error-time'); const submitWrapper = container.querySelector('.sh-inline-submit-wrapper'); // Show time error when clicking disabled button (via wrapper) if (submitWrapper) { submitWrapper.addEventListener('click', function(e) { if (submitBtn.disabled && (!selectedSlot || !selectedCalendarId)) { e.preventDefault(); e.stopPropagation(); if (errorTime) { errorTime.textContent = 'Please select a time 👉'; errorTime.style.display = 'block'; // Shake the calendar area to draw attention const scheduler = container.querySelector('.sh-inline-booking-scheduler'); if (scheduler) { scheduler.style.animation = 'none'; scheduler.offsetHeight; // Trigger reflow scheduler.style.animation = 'sh-shake 0.5s ease'; } } } }); } // Form submission form.addEventListener('submit', async function(e) { e.preventDefault(); const email = container.querySelector('input[name="email"]').value.trim(); const firstName = container.querySelector('input[name="firstName"]').value.trim(); const lastName = container.querySelector('input[name="lastName"]').value.trim(); const title = container.querySelector('input[name="title"]').value.trim(); const company = container.querySelector('input[name="company"]').value.trim(); const phone = container.querySelector('input[name="phone"]').value.trim(); // Validate email format before submission if (!isValidEmail(email)) { errorEmail.textContent = 'Please enter a valid business email address'; return; } // Check for free email domains const emailDomain = email.split('@')[1]?.toLowerCase(); if (emailDomain && freeEmailDomains.includes(emailDomain)) { errorEmail.textContent = 'Please use your business email'; return; } if (!selectedSlot) { if (errorTime) { errorTime.textContent = '👉 Please select a time on the right first'; errorTime.style.display = 'block'; } return; } // Clear time error on successful validation if (errorTime) errorTime.textContent = ''; submitBtn.disabled = true; submitBtn.innerHTML = '<span>BOOKING...</span>'; try { // Book via Google Calendar + Zoom const bookingData = new FormData(); bookingData.append('action', 'sh_create_calendar_booking'); bookingData.append('nonce', shBooking.nonce); bookingData.append('startTime', selectedSlot); bookingData.append('name', `${firstName} ${lastName}`); bookingData.append('email', email); bookingData.append('company', company); bookingData.append('timezone', userTimezone); // Rep will be randomly selected from available reps for this slot const bookingResponse = await fetch(shBooking.ajaxUrl, { method: 'POST', body: bookingData }).then(r => r.json()); if (bookingResponse.success) { // Save to localStorage localStorage.setItem('sh_chatbot_user', JSON.stringify({ email, firstName, lastName, title, company, phone })); // Send to Zapier sendToZapier({ email, firstName, lastName, title, company, phone }); // Show success showSuccess(); } else { throw new Error(bookingResponse.data?.message || 'Booking failed'); } } catch (err) { errorEmail.textContent = err.message || 'Booking failed. Please try again.'; submitBtn.disabled = false; submitBtn.innerHTML = '<span>BOOK MY CALL</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14M12 5l7 7-7 7"/></svg>'; } }); function showSuccess() { form.style.display = 'none'; container.querySelector('.sh-inline-form-header').style.display = 'none'; successDiv.style.display = 'block'; const slotDate = new Date(selectedSlot); successDetails.textContent = slotDate.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); } // Lead CPT submission with all fields + HubSpot (after booking completed) function sendToZapier(info) { // Also submit to HubSpot for the completed booking submitToHubSpotPartial(info.email, info.firstName, info.lastName, info.title, info.company, info.phone, 'Booking Completed'); // Device type const ua = navigator.userAgent; let deviceType = 'Desktop'; if (/tablet|ipad|playbook|silk/i.test(ua)) deviceType = 'Tablet'; else if (/mobile|iphone|ipod|android|blackberry|opera mini|iemobile/i.test(ua)) deviceType = 'Mobile'; // GCLID const params = new URLSearchParams(window.location.search); let gclid = params.get('gclid') || ''; if (!gclid) { const cookie = document.cookie.match(/(?:^|; )gclid=([^;]*)/); gclid = cookie ? cookie[1] : ''; } // UTMs const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']; const utms = {}; utmKeys.forEach(key => { let value = params.get(key); if (!value) { const cookie = document.cookie.match(new RegExp('(?:^|; )' + key + '=([^;]*)')); value = cookie ? cookie[1] : ''; } utms[key] = value; }); // Meeting time const meetingDate = new Date(selectedSlot); const meetingTime = meetingDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: userTimezone }); const payload = { 'Email': info.email, 'First Name': info.firstName, 'Last Name': info.lastName, 'Job Title': info.title, 'Company': info.company, 'Phone': info.phone || '', 'Device': deviceType, 'Meeting Date': selectedSlot, 'Meeting Time': meetingTime, 'Timezone': userTimezone, 'Calendar ID': selectedCalendarId ? String(selectedCalendarId) : '', 'Form Type': container.dataset.formType || 'Inline Booking Shortcode', 'Form Step': 'Booking Completed', 'Page Url': landingPageUrl, 'Page Title': document.title, 'Gclid': gclid, 'Utm Source': utms.utm_source || '', 'Utm Medium': utms.utm_medium || '', 'Utm Campaign': utms.utm_campaign || '', 'Utm Term': utms.utm_term || '', 'Utm Content': utms.utm_content || '', 'Session ID': sessionId, 'Theme Mode': document.body.classList.contains('light-mode') ? 'light' : 'dark', 'Highlight Color': getComputedStyle(document.documentElement).getPropertyValue('--sh-page-highlight').trim() }; // Save to Leads CPT (will deduplicate and send to Zapier after 2 min) const formData = new FormData(); formData.append('action', 'sh_save_lead'); formData.append('nonce', shBooking.nonce); formData.append('payload', JSON.stringify(payload)); fetch(shBooking.ajaxUrl, { method: 'POST', body: formData }) .then(r => r.json()) .catch(() => {}); } // Calendar navigation - Desktop if (calPrev) { calPrev.addEventListener('click', () => { currentMonth.setMonth(currentMonth.getMonth() - 1); renderCalendar(); }); } if (calNext) { calNext.addEventListener('click', () => { currentMonth.setMonth(currentMonth.getMonth() + 1); renderCalendar(); }); } // Calendar navigation - Mobile if (calPrevMobile) { calPrevMobile.addEventListener('click', () => { currentMonth.setMonth(currentMonth.getMonth() - 1); renderCalendar(); }); } if (calNextMobile) { calNextMobile.addEventListener('click', () => { currentMonth.setMonth(currentMonth.getMonth() + 1); renderCalendar(); }); } // Initialize - render calendar (no auto-select on desktop to show calendar first) renderCalendar(); // Check if autofocus is enabled for this instance const autofocusEnabled = container.dataset.autofocus === 'true'; // Auto-focus email field to trigger browser autofill (only if enabled) function autoFocusEmail() { if (!autofocusEnabled) return; // Only focus if email is empty (not pre-filled from localStorage) if (emailInput && !emailInput.value) { // Use IntersectionObserver to focus only when visible const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Small delay to ensure page is fully rendered setTimeout(() => { if (!emailInput.value) { emailInput.focus(); } }, 300); observer.disconnect(); } }); }, { threshold: 0.5 }); observer.observe(container); } } // Listen for autofill changes on email field emailInput.addEventListener('change', function() { const email = this.value.trim(); if (email && !zoomInfoLookedUp) { // Trigger ZoomInfo lookup via blur event this.blur(); this.focus(); } updateMobileNextBtn(); updateSubmitState(); }); // Also detect autofill via animation (Chrome hack) emailInput.addEventListener('animationstart', function(e) { if (e.animationName === 'onAutoFillStart') { // Autofill detected, trigger blur to run ZoomInfo lookup setTimeout(() => { if (emailInput.value && !zoomInfoLookedUp) { emailInput.blur(); } }, 100); } }); // Periodic check for autofilled values (only if autofocus enabled) if (autofocusEnabled) { let autofillCheckCount = 0; const autofillChecker = setInterval(() => { autofillCheckCount++; if (autofillCheckCount > 20) { // Stop after 10 seconds clearInterval(autofillChecker); return; } // Check if email was autofilled if (emailInput.value && !zoomInfoLookedUp) { clearInterval(autofillChecker); // Trigger ZoomInfo lookup emailInput.blur(); } }, 500); } // Trigger auto-focus after short delay (only if enabled) if (autofocusEnabled) { setTimeout(autoFocusEmail, 500); } })(); </script> </div> </div> <script> // Booking Modal - Simple open/close (function() { 'use strict'; const modal = document.getElementById('sh-booking-modal'); if (!modal) return; // Open modal triggers document.querySelectorAll('[data-open-booking-modal], [data-open-modal]').forEach(function(trigger) { trigger.addEventListener('click', function(e) { e.preventDefault(); modal.classList.add('active'); document.body.classList.add('sh-modal-open'); }); }); // Close modal triggers document.querySelectorAll('[data-close-booking-modal]').forEach(function(trigger) { trigger.addEventListener('click', function() { modal.classList.remove('active'); document.body.classList.remove('sh-modal-open'); }); }); // Close on Escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && modal.classList.contains('active')) { modal.classList.remove('active'); document.body.classList.remove('sh-modal-open'); } }); })(); </script> <script> // Meeting Counter Count-Up Animation (function() { const counter = document.querySelector('.sh-bottom-bar-value'); if (!counter) return; const target = parseInt(counter.dataset.target, 10); const startFrom = target - 20; // Start from target minus 20 const increment = 1; const duration = 400; // 0.4 seconds total const steps = 20; // 20 steps to count up const stepTime = duration / steps; let current = startFrom; let hasAnimated = false; function formatNumber(num) { return num.toLocaleString(); } // Show starting number immediately counter.textContent = formatNumber(startFrom); function animateCounter() { if (hasAnimated) return; hasAnimated = true; const interval = setInterval(() => { current += increment; if (current >= target) { current = target; clearInterval(interval); } counter.textContent = formatNumber(current); }, stepTime); } // Start animation when counter is visible const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { animateCounter(); observer.disconnect(); } }); }, { threshold: 0.5 }); observer.observe(counter); // Also start if already visible on load if (counter.getBoundingClientRect().top < window.innerHeight) { setTimeout(animateCounter, 500); } })(); </script> <!-- Meeting Notification Popup --> <div id="sh-meeting-popup" class="sh-meeting-popup"> <div class="sh-meeting-popup-content"> <div class="sh-meeting-popup-icon"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/> <polyline points="22 4 12 14.01 9 11.01"/> </svg> </div> <div class="sh-meeting-popup-text"> <div class="sh-meeting-popup-title">New Meeting Booked!</div> <div class="sh-meeting-popup-details"> <span class="sh-meeting-popup-company"></span> <span class="sh-meeting-popup-meta"></span> </div> </div> <button class="sh-meeting-popup-close">×</button> </div> </div> <style> /* Meeting Notification Popup */ .sh-meeting-popup { position: fixed; top: 20px; right: 20px; z-index: 99999; opacity: 0; transform: translateX(100%); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); pointer-events: none; } .sh-meeting-popup.active { opacity: 1; transform: translateX(0); pointer-events: auto; } .sh-meeting-popup-content { display: flex; align-items: center; gap: 12px; background: var(--sh-bg-secondary); border: 1px solid var(--sh-border-default); border-left: 4px solid var(--sh-page-highlight); border-radius: 12px; padding: 16px 20px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); min-width: 300px; max-width: 400px; } .sh-meeting-popup-icon { width: 40px; height: 40px; background: rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.15); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; animation: sh-popup-bounce 0.6s ease; } .sh-meeting-popup-icon svg { width: 22px; height: 22px; color: var(--sh-page-highlight); } @keyframes sh-popup-bounce { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } .sh-meeting-popup-text { flex: 1; } .sh-meeting-popup-title { font-size: 11px; font-weight: 700; color: var(--sh-page-highlight); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; } .sh-meeting-popup-company { display: block; font-size: 16px; font-weight: 600; color: var(--sh-text-primary); } .sh-meeting-popup-meta { display: block; font-size: 13px; color: var(--sh-text-muted); margin-top: 2px; } .sh-meeting-popup-close { background: none; border: none; color: var(--sh-text-muted); font-size: 24px; cursor: pointer; padding: 0; line-height: 1; transition: color 0.2s; } .sh-meeting-popup-close:hover { color: var(--sh-text-primary); } /* Counter update animation */ .sh-bottom-bar-value.updating { animation: sh-counter-pulse 0.5s ease; } @keyframes sh-counter-pulse { 0% { transform: scale(1); } 50% { transform: scale(1.15); color: var(--sh-page-highlight); text-shadow: 0 0 10px rgba(var(--sh-page-highlight-r), var(--sh-page-highlight-g), var(--sh-page-highlight-b), 0.5); } 100% { transform: scale(1); } } @media (max-width: 768px) { .sh-meeting-popup { top: auto; bottom: 80px; right: 10px; left: 10px; } .sh-meeting-popup-content { min-width: auto; max-width: none; } } </style> <script> // Real-time Meeting Notifications (Desktop only) (function() { // Don't run on mobile if (window.innerWidth <= 768) return; const popup = document.getElementById('sh-meeting-popup'); const counter = document.querySelector('.sh-bottom-bar-value'); if (!popup) return; const companyEl = popup.querySelector('.sh-meeting-popup-company'); const metaEl = popup.querySelector('.sh-meeting-popup-meta'); const closeBtn = popup.querySelector('.sh-meeting-popup-close'); let lastTimestamp = Math.floor(Date.now() / 1000); let currentCount = counter ? parseInt(counter.dataset.target, 10) : 0; // Close popup on click closeBtn.addEventListener('click', () => { popup.classList.remove('active'); }); // Auto-hide after 8 seconds function showPopup(meeting) { companyEl.textContent = meeting.company; metaEl.textContent = meeting.title + (meeting.source ? ' • via ' + meeting.source : ''); popup.classList.add('active'); setTimeout(() => { popup.classList.remove('active'); }, 8000); } // Update counter with animation function updateCounter(newCount) { if (!counter || newCount <= currentCount) return; currentCount = newCount; counter.dataset.target = newCount; counter.textContent = newCount.toLocaleString(); counter.classList.add('updating'); setTimeout(() => { counter.classList.remove('updating'); }, 500); } // Poll for new meetings (fetches static JSON - no PHP execution, CDN cacheable) async function checkForNewMeetings() { try { const response = await fetch('/meeting-data.json?t=' + Date.now()); const data = await response.json(); if (data.meeting && data.meeting.timestamp > lastTimestamp) { lastTimestamp = data.meeting.timestamp; updateCounter(data.count); showPopup(data.meeting); } else if (data.count > currentCount) { // Count increased but no meeting data (from cron sync) updateCounter(data.count); } } catch (err) { console.log('Meeting check error:', err); } } // DISABLED: Polling temporarily disabled to reduce server load // setInterval(checkForNewMeetings, 120000); // setTimeout(checkForNewMeetings, 10000 + Math.random() * 5000); })(); </script> <script> // Hide floating footer bar when at top, hide CTA when at bottom // Show footer toggle button when at bottom (function() { const bottomBar = document.querySelector('.sh-bottom-bar'); const desktopCta = document.querySelector('.sh-bottom-bar-cta'); const mobileCta = document.querySelector('.sh-mobile-sticky-cta'); const showFooterBtn = document.getElementById('shShowFooterBtn'); const footer = document.querySelector('.sh-footer'); const topThreshold = 300; // Hide entire bar when within 300px of top const bottomThreshold = 200; // Hide CTA & show footer button when within 200px of bottom let ticking = false; // Hide bar on page load (at top) if (bottomBar) bottomBar.classList.add('sh-bar-hidden'); function checkScrollPosition() { const scrollPosition = window.scrollY; const viewportHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; const distanceFromBottom = documentHeight - (scrollPosition + viewportHeight); const distanceFromTop = scrollPosition; // Show/hide entire floating bar based on distance from top if (bottomBar) { if (distanceFromTop <= topThreshold) { // Near top - hide entire bar bottomBar.classList.add('sh-bar-hidden'); } else { // Not at top - show bar bottomBar.classList.remove('sh-bar-hidden'); } } // Show/hide CTA button based on distance from bottom if (distanceFromBottom <= bottomThreshold) { // Near bottom - hide CTA buttons if (desktopCta) desktopCta.classList.add('sh-cta-hidden'); if (mobileCta) mobileCta.classList.add('sh-cta-hidden'); } else if (distanceFromTop > topThreshold) { // Not at top or bottom - show CTA buttons if (desktopCta) desktopCta.classList.remove('sh-cta-hidden'); if (mobileCta) mobileCta.classList.remove('sh-cta-hidden'); } // Show footer button only when at bottom of page if (showFooterBtn) { if (distanceFromBottom <= bottomThreshold) { showFooterBtn.classList.add('sh-visible'); } else { showFooterBtn.classList.remove('sh-visible'); } } ticking = false; } function onScroll() { if (!ticking) { requestAnimationFrame(checkScrollPosition); ticking = true; } } // Toggle footer visibility on button click if (showFooterBtn && footer) { showFooterBtn.addEventListener('click', function() { const isOpening = !footer.classList.contains('sh-footer-visible'); footer.classList.toggle('sh-footer-visible'); showFooterBtn.classList.toggle('sh-footer-open'); // Scroll to bottom when opening if (isOpening) { // Instant scroll to current bottom, then smooth scroll after transition window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'auto' }); // Final smooth scroll after footer fully expands const scrollAfterTransition = function(e) { if (e.propertyName === 'max-height') { window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' }); footer.removeEventListener('transitionend', scrollAfterTransition); } }; footer.addEventListener('transitionend', scrollAfterTransition); } }); } window.addEventListener('scroll', onScroll, { passive: true }); // Initial check checkScrollPosition(); })(); </script> <script> // Scroll Progress Bar - Vertical Right Side (function() { const progressBar = document.querySelector('.sh-scroll-progress-bar'); if (!progressBar) return; function updateProgress() { const scrollTop = window.scrollY || document.documentElement.scrollTop; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; progressBar.style.height = Math.min(100, Math.max(0, progress)) + '%'; } window.addEventListener('scroll', updateProgress, { passive: true }); window.addEventListener('resize', updateProgress, { passive: true }); // Initial update updateProgress(); })(); </script> <script> // Card Tilt Effect - Add .sh-tilt class to any element (function() { const tiltElements = document.querySelectorAll('.sh-tilt'); if (!tiltElements.length) return; tiltElements.forEach(el => { const maxRotation = parseFloat(getComputedStyle(el).getPropertyValue('--sh-tilt-max')) || 10; el.addEventListener('mousemove', (e) => { const rect = el.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const rotateX = ((e.clientY - centerY) / (rect.height / 2)) * -maxRotation; const rotateY = ((e.clientX - centerX) / (rect.width / 2)) * maxRotation; el.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; }); el.addEventListener('mouseleave', () => { el.style.transform = 'perspective(1000px) rotateX(0) rotateY(0)'; }); }); })(); </script> <!-- DISABLED: Parallax effect <script> // Parallax Scroll Effect - Add .sh-parallax class to any element (function() { const parallaxElements = document.querySelectorAll('.sh-parallax'); if (!parallaxElements.length) return; const speed = 0.15; let ticking = false; function updateParallax() { const windowHeight = window.innerHeight; parallaxElements.forEach(el => { const rect = el.getBoundingClientRect(); const elementCenter = rect.top + rect.height / 2; const viewportCenter = windowHeight / 2; const offset = (elementCenter - viewportCenter) * speed; el.style.transform = `translateY(${offset}px)`; }); } window.addEventListener('scroll', () => { if (!ticking) { requestAnimationFrame(() => { updateParallax(); ticking = false; }); ticking = true; } }, { passive: true }); updateParallax(); })(); </script> --> <script> // Site Settings Dropdown & Color Palette Script (function() { const settingsBtn = document.getElementById('shSiteSettingsBtn'); const settingsDropdown = document.getElementById('shSiteSettingsDropdown'); const swatches = document.querySelectorAll('.sh-color-swatch'); // Default color is set via SalesHive Settings - window.SH_DEFAULT_HIGHLIGHT_COLOR is output in wp_head const defaultColor = window.SH_DEFAULT_HIGHLIGHT_COLOR; // Check if URL highlight param is active (set by header script) const urlHighlightActive = window.SH_URL_HIGHLIGHT_ACTIVE || false; // Toggle dropdown if (settingsBtn && settingsDropdown) { settingsBtn.addEventListener('click', function(e) { e.stopPropagation(); settingsDropdown.classList.toggle('active'); }); // Close on outside click document.addEventListener('click', function(e) { if (!settingsDropdown.contains(e.target) && e.target !== settingsBtn) { settingsDropdown.classList.remove('active'); } }); } // Convert hex to RGB function hexToRgb(hex) { hex = hex.replace('#', ''); if (hex.length === 3) { hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; } return { r: parseInt(hex.substring(0, 2), 16), g: parseInt(hex.substring(2, 4), 16), b: parseInt(hex.substring(4, 6), 16) }; } // Apply color to CSS variables function applyHighlightColor(color) { const rgb = hexToRgb(color); const root = document.documentElement; // Main highlight color root.style.setProperty('--sh-page-highlight', color); root.style.setProperty('--sh-page-highlight-r', rgb.r); root.style.setProperty('--sh-page-highlight-g', rgb.g); root.style.setProperty('--sh-page-highlight-b', rgb.b); // Mesh gradient variants root.style.setProperty('--sh-mesh-v1-r', Math.min(255, rgb.r + 40)); root.style.setProperty('--sh-mesh-v1-g', Math.max(0, rgb.g - 30)); root.style.setProperty('--sh-mesh-v1-b', Math.max(0, rgb.b - 50)); root.style.setProperty('--sh-mesh-v2-r', Math.max(0, rgb.r - 100)); root.style.setProperty('--sh-mesh-v2-g', Math.max(0, rgb.g - 60)); root.style.setProperty('--sh-mesh-v2-b', Math.min(255, rgb.b + 120)); root.style.setProperty('--sh-mesh-v3-r', Math.max(0, rgb.r - 60)); root.style.setProperty('--sh-mesh-v3-g', Math.max(0, rgb.g - 80)); root.style.setProperty('--sh-mesh-v3-b', Math.min(255, rgb.b + 80)); // Update active swatch swatches.forEach(s => { s.classList.toggle('active', s.dataset.color.toUpperCase() === color.toUpperCase()); }); } // Handle swatch clicks swatches.forEach(swatch => { swatch.addEventListener('click', function() { const color = this.dataset.color; applyHighlightColor(color); }); }); })(); </script> <script> // Theme Toggle Script (function() { const toggle = document.getElementById('themeToggle'); const mobileToggle = document.getElementById('mobileThemeToggle'); const headerToggle = document.getElementById('headerThemeToggle'); const footerToggle = document.getElementById('footerThemeToggle'); // Update Trustpilot widget theme and reload it function updateTrustpilotTheme(isLight) { const widget = document.getElementById('sh-trustpilot-widget'); if (widget) { widget.setAttribute('data-theme', isLight ? 'light' : 'dark'); // Reload the widget if Trustpilot API is available if (window.Trustpilot) { window.Trustpilot.loadFromElement(widget, true); } } } // Toggle theme function function toggleTheme() { // Toggle on both html and body for CSS compatibility document.documentElement.classList.toggle('light-mode'); document.body.classList.toggle('light-mode'); const isLight = document.body.classList.contains('light-mode'); updateTrustpilotTheme(isLight); } // Sync Trustpilot theme with current mode (already set correctly by head script) const isLight = document.body.classList.contains('light-mode'); updateTrustpilotTheme(isLight); // Desktop floating toggle if (toggle) { toggle.addEventListener('click', toggleTheme); } // Mobile toggle if (mobileToggle) { mobileToggle.addEventListener('click', toggleTheme); } // Header toggle if (headerToggle) { headerToggle.addEventListener('click', toggleTheme); } // Footer settings toggle if (footerToggle) { footerToggle.addEventListener('click', toggleTheme); } })(); </script> <script> // Calendar booking configuration (Google Calendar + Zoom direct integration) var shBooking = { ajaxUrl: 'https://saleshive.com/wp-admin/admin-ajax.php', nonce: 'e6bd03e3f5' }; </script> <script type="speculationrules"> {"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/saleshive/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]} </script> <script defer src="https://saleshive.com/wp-content/themes/saleshive/assets/js/main.js?ver=1765852083" id="saleshive-main-js"></script> <script id="saleshive-chatbot-js-extra"> var shChatbot = {"ajaxUrl":"https://saleshive.com/wp-admin/admin-ajax.php","nonce":"731e3fe4bc"}; //# sourceURL=saleshive-chatbot-js-extra </script> <script defer src="https://saleshive.com/wp-content/themes/saleshive/assets/js/chatbot.js?ver=1765918512" id="saleshive-chatbot-js"></script> <!-- Start of HubSpot Embed Code --> <script type="text/javascript" id="hs-script-loader" async defer src="//js.hs-scripts.com/6880333.js"></script> <!-- End of HubSpot Embed Code --> </body> </html>