Listen to this article · 15 min listen

Mastering the art of keyword research isn’t just about finding popular terms; it’s about uncovering the precise language your target audience uses to solve their problems, a foundational element in any successful digital marketing strategy. Are you truly prepared to unearth the hidden gems that will drive unprecedented organic growth?

Key Takeaways

  • Utilize the Google Keyword Planner to identify high-volume, low-competition keywords by filtering for search volume above 1,000 and competition below “Medium.”
  • Implement a competitor keyword analysis by inputting up to 10 competitor domains into Semrush’s Organic Research tool to uncover their top-performing search terms.
  • Prioritize long-tail keywords (3+ words) found in AnswerThePublic by analyzing the “Questions” and “Prepositions” visualizations for specific user intent.
  • Integrate identified keywords into content, meta descriptions, and image alt text, aiming for a natural keyword density of 0.5-1% for primary terms.
  • Regularly review keyword performance in Google Search Console, focusing on “Queries” with high impressions but low click-through rates (CTRs) for optimization opportunities.

As a digital strategist with over a decade in the trenches, I’ve seen countless businesses flounder because they guessed at what their audience wanted. Guessing is for charades, not for marketing budgets. The real secret? Data-driven keyword research. We’re going to walk through the exact process I use with my clients, focusing on the 2026 interface of Google Keyword Planner, a tool that, despite its quirks, remains indispensable.

Step 1: Setting Up Your Google Keyword Planner Workspace

Before we dive into the data, you need a proper workspace. This isn’t just about logging in; it’s about configuring the tool to give you actionable insights, not just a firehose of terms.

1.1 Accessing the Keyword Planner

  1. First, ensure you have an active Google Ads account. You don’t need to be running active campaigns to use the planner, but it’s where the tool lives.
  2. Log in to your Google Ads account.
  3. In the top navigation bar, locate and click Tools and Settings (represented by a wrench icon).
  4. From the dropdown menu, under the “Planning” column, select Keyword Planner.
  5. You’ll be presented with two options: “Discover new keywords” and “Get search volume and forecasts.” For our initial research, we’re going with Discover new keywords.

Pro Tip: Google sometimes buries the Keyword Planner if you’re a new account or haven’t used it much. If you can’t find it, try searching for “Keyword Planner” directly in the Google Ads search bar at the top of the interface. It usually pops right up.

Common Mistake: Many users jump straight into “Get search volume and forecasts” with a pre-conceived list. This limits your discovery. Always start with “Discover new keywords” to broaden your horizons.

Expected Outcome: You should now be on a screen with an input field where you can enter seed keywords or a website URL.

1.2 Configuring Your Target Settings

This is where precision begins. Generic data is useless. We need data relevant to your specific market.

  1. On the “Discover new keywords” page, look for the Location setting, usually a dropdown menu near the top left. Click it.
  2. Enter your target country (e.g., “United States”), specific states (e.g., “Georgia”), or even cities (e.g., “Atlanta”). For a local business, this is paramount. We recently worked with a client, “Atlanta Auto Glass Solutions” in the West Midtown area, and limiting our search to “Atlanta, Georgia” dramatically improved the relevance of our keyword suggestions.
  3. Once you’ve selected your desired locations, click Save.
  4. Next, check the Language setting. Ensure it matches your target audience’s primary language.
  5. (Optional but recommended for advanced users) Click the Date Range dropdown. While the default “Last 12 months” is often fine, if you’re in a highly seasonal industry, adjusting this to capture specific periods (e.g., Q4 for holiday shopping) can be insightful.

Pro Tip: For businesses serving a specific metropolitan area, don’t just pick the state. Go granular. The difference between “plumber Georgia” and “plumber Atlanta GA” in terms of intent and competition is massive.

Common Mistake: Forgetting to set the location, leading to global keyword data that’s irrelevant for a local or regional business. I had a client last year who was targeting “luxury handbags” globally, but their shipping only covered North America. We wasted weeks on keywords with huge volumes from Europe before we tightened the location settings.

Expected Outcome: Your keyword suggestions will now be filtered to reflect search behavior within your specified geographical and linguistic parameters.

Step 2: Uncovering Keyword Ideas with Seed Keywords and Competitor Analysis

Now for the fun part: finding the keywords themselves. This isn’t a one-and-done process; it’s iterative.

2.1 Brainstorming Seed Keywords

  1. In the “Discover new keywords” input field, enter 3-5 broad terms related to your product or service. Think like your customer. If you sell custom furniture, don’t just type “furniture.” Try “custom dining tables,” ” bespoke bookcases,” “handmade wooden chairs.”
  2. Click Get results.
  3. Google Keyword Planner will now present you with two main sections: “Keyword ideas” and “Refine keywords.” Focus on “Keyword ideas” first.

Pro Tip: Don’t censor yourself here. Enter anything that comes to mind. We’ll filter the noise later. Sometimes the most obvious terms are the ones you overlook.

Common Mistake: Entering only one or two very generic seed keywords. This limits the breadth of suggestions the tool can provide.

Expected Outcome: A long list of keyword suggestions, along with metrics like Average Monthly Searches, Competition, and Top of page bid (low range and high range).

2.2 Leveraging Competitor Websites for Keyword Inspiration

Your competitors have likely done some heavy lifting already. Why not learn from them?

  1. Back on the “Discover new keywords” screen, instead of entering seed keywords, click on the option Start with a website.
  2. Enter the URL of a direct competitor (e.g., if you sell artisan coffee, try a local competitor like “Chrome Yellow Trading Co.” in Atlanta’s Old Fourth Ward).
  3. Choose whether to “Use the entire site” or “Use only this page.” For initial discovery, I always recommend Use the entire site.
  4. Click Get results.

Pro Tip: Don’t just pick the biggest competitor. Sometimes smaller, niche competitors are ranking for incredibly specific, high-intent keywords that the giants overlook. Also, consider using a tool like Semrush or Ahrefs for a more comprehensive competitor analysis; they can reveal hundreds of keywords a competitor ranks for, including those Google Keyword Planner might miss. For more on this, check out our guide on Ahrefs SEO tactics.

Common Mistake: Only analyzing one competitor. You need a broader view. Aim for 3-5 direct competitors if possible.

Expected Outcome: A list of keywords that Google believes are relevant to your competitor’s website, giving you insights into their organic strategy.

Step 3: Filtering and Analyzing Your Keyword List

Raw data is just noise without analysis. This step is about refining your massive list into a manageable, actionable set of targets.

3.1 Applying Essential Filters

  1. On the “Keyword ideas” results page, look for the filter options above the keyword table.
  2. Click on Add filter.
  3. Select Average monthly searches. I generally set a minimum of 100 or 1,000, depending on the niche. For a brand new business, even 100-500 searches can be valuable. For established players, I often start at 1,000+.
  4. Add another filter for Competition. I almost always filter for “Low” or “Medium” competition first. Why bang your head against a brick wall trying to rank for “insurance” when “affordable car insurance for new drivers in Georgia” is far more attainable?
  5. (Optional but powerful) Add a filter for Keyword text. Use this to include specific brand names (yours or competitors’), product categories, or to exclude irrelevant terms. For instance, if you sell artisanal coffee beans but not coffee makers, you’d add “coffee maker” to the exclusion list.

Pro Tip: Don’t be afraid to experiment with these filters. Start broad, then narrow down. You might discover a niche with surprisingly high volume and low competition by playing with the search volume thresholds.

Common Mistake: Not using filters at all, or setting them too aggressively initially. You might miss valuable long-tail keywords if you filter for too high a search volume right away.

Expected Outcome: A refined list of keywords that meet your specified criteria for search volume and competition, making the list much more manageable.

3.2 Identifying Long-Tail Keywords and User Intent

This is where the real value lies. Short, fat keywords are often vanity metrics; long-tail keywords drive conversions.

  1. Scroll through your filtered keyword list. Pay close attention to phrases with three or more words. These are your long-tail keywords.
  2. Examine the keyword phrasing. Does it suggest a user looking for information (e.g., “how to fix a leaky faucet”) or someone ready to buy (e.g., “emergency plumber service Atlanta”)? This is inferring user intent.
  3. Look at the “Top of page bid (low range)” and “Top of page bid (high range)” columns. Higher bids often indicate higher commercial intent, meaning those keywords are valuable to advertisers because they lead to sales.
  4. Consider using a tool like AnswerThePublic in conjunction with Keyword Planner. Input some of your promising mid-tail keywords there, and it will generate a visual map of questions, prepositions, and comparisons related to your topic. This is gold for understanding exactly what your audience asks.

Pro Tip: Long-tail keywords often have lower search volume individually, but collectively, they can account for a significant portion of your traffic. More importantly, their conversion rates are typically much higher because they represent more specific user intent. I once helped a small accounting firm in Buckhead pivot from targeting “tax accountant” (impossible competition) to “CPA for small business Atlanta GA” and “quickbooks specialist Buckhead.” Within six months, their organic leads tripled.

Common Mistake: Obsessing over keywords with extremely high search volume and ignoring the valuable, high-intent long-tail phrases. Quality over quantity, always. You can also explore common marketing myths that often lead to these mistakes.

Expected Outcome: A prioritized list of keywords, including both foundational short-tail terms and highly specific, high-intent long-tail phrases, categorized by estimated search volume, competition, and inferred user intent.

Step 4: Exporting and Organizing Your Keyword Data

Your research isn’t done until it’s organized and ready for implementation.

4.1 Exporting Your Keyword List

  1. On the “Keyword ideas” page, above the keyword table, locate the Download keyword ideas button.
  2. Click it and choose Google Sheets or CSV. I prefer Google Sheets for easier collaboration and manipulation.
  3. Open the downloaded file.

Pro Tip: Exporting to Google Sheets allows you to easily share with content writers, ad specialists, or other team members. It also integrates well with other tools if you’re building a more complex content calendar.

Common Mistake: Just leaving the data in Keyword Planner. It needs to be in a format where you can annotate, sort, and prioritize easily.

Expected Outcome: A spreadsheet containing all your filtered keyword data, ready for further organization.

4.2 Structuring Your Keyword Spreadsheet

This is my personal workflow, honed over years of managing campaigns from small startups to Fortune 500 companies. It’s robust.

  1. Create new columns in your spreadsheet for:
    • Keyword Category/Topic Cluster: Group similar keywords together. For instance, “custom dining tables” and “bespoke wooden dining room furniture” would go under a “Dining Tables” cluster.
    • Content Idea: What kind of content would this keyword best serve? A blog post, a product page, a service page?
    • Target Page URL: Which existing or new page on your site will target this keyword (or cluster)?
    • Primary/Secondary Keyword: Designate one or two keywords as primary for a page, and others as secondary.
    • Current Rank (if applicable): If you’re updating existing content, note its current ranking position for this term.
    • Notes: Any specific thoughts or instructions for content creation.
  2. Sort your keywords by Average monthly searches (descending) and then by Competition (ascending) to identify high-volume, lower-competition terms first.
  3. Begin populating the new columns, assigning keywords to content clusters and outlining potential content ideas.

Pro Tip: Don’t try to cram too many primary keywords onto one page. One to three primary keywords per page, along with several secondary, semantically related terms, is generally a good rule of thumb. Remember, Google’s algorithms are sophisticated; they understand context. Focus on answering the user’s query comprehensively.

Common Mistake: Creating a massive, unsorted list and then trying to implement it without a clear content strategy. This leads to keyword cannibalization (multiple pages competing for the same keyword) and diluted effort.

Expected Outcome: A well-organized, actionable keyword plan that directly informs your content strategy, mapping specific keywords to specific content pieces and their purpose.

Step 5: Implementing and Monitoring Your Keyword Strategy

Keyword research isn’t a static exercise. It’s a continuous cycle of implementation, measurement, and refinement.

5.1 Integrating Keywords into Your Content

Once you have your prioritized list, it’s time to put those keywords to work. This isn’t about keyword stuffing; it’s about natural integration.

  1. For each target page, ensure your primary keyword appears naturally in the page title ( tag)</strong>, the <strong>H1 heading</strong>, and the <strong>meta description</strong>.</li> <li>Sprinkle your primary and secondary keywords throughout the body copy, aiming for a natural flow. Don’t force them.</li> <li>Include keywords in <strong>subheadings (H2, H3)</strong> where appropriate.</li> <li>Use keywords in <strong>image alt text</strong> and <strong>file names</strong>.</li> <li>Internally link from other relevant pages on your site to your new or updated content, using your target keywords as anchor text.</li> </ol> <p><strong>Pro Tip:</strong> Focus on writing for humans first, search engines second. If your content is genuinely helpful and answers the user’s query, Google will reward you. A good rule of thumb for keyword density is around 0.5-1% for your primary keyword – anything more starts to feel unnatural.</p> <p><strong>Common Mistake:</strong> Over-optimizing or “keyword stuffing.” This used to work, but now it’s a surefire way to get penalized by search engines. Write naturally.</p> <p><strong>Expected Outcome:</strong> High-quality, keyword-rich content that clearly communicates its topic to both users and search engines.</p> <h3>5.2 Monitoring Performance with Google Search Console</h3> <p>The work doesn’t end when the content is published. You need to see if your efforts are paying off.</p> <ol> <li>Log in to <a href="https://search.google.com/search-console/" target="_blank" rel="noopener">Google Search Console</a>.</li> <li>In the left-hand navigation, click <strong>Performance</strong>, then <strong>Search results</strong>.</li> <li>Under the “Queries” tab, you’ll see the actual keywords people used to find your site.</li> <li>Filter by <strong>Page</strong> to see which keywords are driving traffic to your newly optimized content.</li> <li>Pay close attention to keywords with high <strong>Impressions</strong> but low <strong>Click-Through Rate (CTR)</strong>. These are opportunities. It means people are seeing your content in search results, but not clicking. This often indicates your title tag or meta description isn’t compelling enough, or your content isn’t truly matching the search intent. Learn more about improving your <a href="https://ppcgrowthstudio.com/ga4-conversion-tracking-boost-roi-by-30-in-2026/">GA4 conversion tracking</a> for better insights.</li> </ol> <p><strong>Pro Tip:</strong> Use the data from Search Console to continually refine your strategy. If a page is ranking well for an unexpected keyword, consider optimizing it further for that term. If a target keyword isn’t performing, revisit your content or consider creating new, more focused content. This iterative process is what separates good SEOs from great ones.</p> <p><strong>Common Mistake:</strong> Publishing content and never checking its performance. SEO is an ongoing process, not a one-time task.</p> <p><strong>Expected Outcome:</strong> Ongoing insights into your keyword performance, allowing you to identify new opportunities, optimize underperforming content, and measure the real-world impact of your keyword research.</p> <p>By diligently following these steps and embracing a data-driven approach to showcasing specific tactics like keyword research, you’ll transform your marketing efforts from hopeful guesses to strategic victories.</p> <div class="faq-section"> <div class="faq-item"> <h3 class="faq-question">How often should I conduct keyword research?</h3> <div class="faq-answer"> <p>I recommend a comprehensive keyword audit at least once a year, with more frequent, smaller-scale research for specific campaigns or new content initiatives. The digital landscape changes rapidly, so staying current with search trends and competitor movements is essential.</p> </div> </div> <div class="faq-item"> <h3 class="faq-question">What’s the difference between short-tail and long-tail keywords?</h3> <div class="faq-answer"> <p><strong>Short-tail keywords</strong> are typically 1-2 words (e.g., “marketing strategy”). They have high search volume but are very competitive and often have broad, ambiguous user intent. <strong>Long-tail keywords</strong> are 3+ words (e.g., “digital marketing strategy for small businesses”). They have lower individual search volume but are less competitive and indicate much clearer, often commercial, user intent, leading to higher conversion rates.</p> </div> </div> <div class="faq-item"> <h3 class="faq-question">Can I do keyword research without Google Ads?</h3> <div class="faq-answer"> <p>While the Google Keyword Planner is part of Google Ads, you don’t need to run active campaigns to use it. You just need an active Google Ads account. However, there are many other excellent keyword research tools like Semrush, Ahrefs, and Ubersuggest that offer more features and insights, often for a subscription fee.</p> </div> </div> <div class="faq-item"> <h3 class="faq-question">Is “competition” in Google Keyword Planner reliable?</h3> <div class="faq-answer"> <p>The “Competition” metric in Google Keyword Planner specifically refers to competition among advertisers bidding on that keyword in Google Ads. While it can be an indicator of organic competition (high ad competition often means high organic competition), it’s not a direct measure. For organic competition, I rely more on domain authority metrics from tools like Semrush or Ahrefs, looking at the strength of sites already ranking for that term.</p> </div> </div> <div class="faq-item"> <h3 class="faq-question">How do I handle keywords with very low search volume?</h3> <div class="faq-answer"> <p>Don’t dismiss them outright! If a keyword has very low search volume (e.g., 10-50 searches per month) but extremely high commercial intent and relevance to your niche, it can still be incredibly valuable. These often represent highly specific problems your product or service solves. Focus on grouping these into content clusters where a single, comprehensive page can address several related low-volume, high-intent queries.</p> </div> </div> </div> </div> <div class="t1-feedback" id="t1-feedback"> <span class="t1-feedback-q">Was this article helpful?</span> <button type="button" data-vote="yes">Yes</button> <button type="button" data-vote="no">No</button> </div> <script> (function(){ var fb = document.getElementById('t1-feedback'); if(!fb) return; fb.querySelectorAll('button').forEach(function(btn){ btn.addEventListener('click', function(){ fetch('/wp-json/satellite/v1/feedback', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({post_id:10637, vote:this.dataset.vote})}).catch(function(){}); fb.innerHTML = '<span class="t1-feedback-thanks">Thanks for your feedback!</span>'; }); }); })(); </script> <div class="t1-author-bio"> <div><img fetchpriority="low" loading="lazy" alt='' src='https://secure.gravatar.com/avatar/b7fd50a9d0c5d6e20947d500964c66c1b6cc7b7c674729f45a2759f1a382ac3d?s=84&d=mm&r=g' srcset='https://secure.gravatar.com/avatar/b7fd50a9d0c5d6e20947d500964c66c1b6cc7b7c674729f45a2759f1a382ac3d?s=168&d=mm&r=g 2x' class='avatar avatar-84 photo' height='84' width='84' decoding='async'/></div> <div> <h3 class="t1-author-name"><a href="https://ppcgrowthstudio.com/author/ppcgrowteditorialteam/">Editorial Team</a></h3> <p class="t1-author-desc">The editorial team behind PPC Growth Studio.</p> </div> </div> </div> </div> <section class="t1-related"> <div class="t1-section-header"><h2 class="t1-section-title">Related Articles</h2></div> <div class="t1-grid t1-grid-3"> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/2026-marketing-bridging-the-40-segmentation-gap/" aria-label="2026 Marketing: Bridging the 40% Segmentation Gap"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/2026-marketing-cutting-edge-trends-audience-targeting-featured-600x400.webp?v=1775212369" class="attachment-satellite-card size-satellite-card wp-post-image" alt="2026 Marketing: Bridging the 40% Segmentation Gap" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/2026-marketing-cutting-edge-trends-audience-targeting-featured-600x400.webp?v=1775212369 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/2026-marketing-cutting-edge-trends-audience-targeting-featured-300x200.webp?v=1775212369 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/digital-marketing/" style="background:#f8b739"> Digital Marketing </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="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"/></svg> <time datetime="2026-07-31T11:48:01+00:00">Jul 31, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/2026-marketing-bridging-the-40-segmentation-gap/">2026 Marketing: Bridging the 40% Segmentation Gap</a> </h3> </div> </article> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/marketing-strategy-2026-5-steps-to-ai-growth/" aria-label="Marketing Strategy 2026: 5 Steps to AI Growth"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/05/boost-marketing-roi-5-data-driven-steps-for-2026-featured-600x400.webp?v=1778107469" class="attachment-satellite-card size-satellite-card wp-post-image" alt="Marketing Strategy 2026: 5 Steps to AI Growth" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/05/boost-marketing-roi-5-data-driven-steps-for-2026-featured-600x400.webp 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/05/boost-marketing-roi-5-data-driven-steps-for-2026-featured-300x200.webp 300w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/05/boost-marketing-roi-5-data-driven-steps-for-2026-featured-400x267.webp 400w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/digital-marketing/" style="background:#f8b739"> Digital Marketing </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="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"/></svg> <time datetime="2026-07-31T10:54:54+00:00">Jul 31, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/marketing-strategy-2026-5-steps-to-ai-growth/">Marketing Strategy 2026: 5 Steps to AI Growth</a> </h3> </div> </article> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/keyword-research-why-75-miss-page-one-in-2026/" aria-label="Keyword Research: Why 75% Miss Page One in 2026"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/keyword-research-roi-prove-your-tactics-work-featured-600x400.webp?v=1775747826" class="attachment-satellite-card size-satellite-card wp-post-image" alt="Keyword Research: Why 75% Miss Page One in 2026" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/keyword-research-roi-prove-your-tactics-work-featured-600x400.webp?v=1775747826 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/keyword-research-roi-prove-your-tactics-work-featured-300x200.webp?v=1775747826 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/digital-marketing/" style="background:#f8b739"> Digital Marketing </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="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"/></svg> <time datetime="2026-07-31T09:45:16+00:00">Jul 31, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/keyword-research-why-75-miss-page-one-in-2026/">Keyword Research: Why 75% Miss Page One in 2026</a> </h3> </div> </article> </div> </section> <section class="t1-related"> <div class="t1-section-header"><h2 class="t1-section-title">Popular Posts</h2></div> <div class="t1-grid t1-grid-3"> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026/" aria-label="A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-600x400.webp?v=1776360237" class="attachment-satellite-card size-satellite-card wp-post-image" alt="A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-600x400.webp?v=1776360237 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-300x200.webp?v=1776360237 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/customer-experience/" style="background:#e8543f"> Customer Experience </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="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"/></svg> <time datetime="2026-04-16T16:06:03+00:00">Apr 16, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026/">A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)</a> </h3> </div> </article> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/ppc-a-b-testing-case-study-175-conversion-lift/" aria-label="PPC A/B Testing Case Study: 175% Conversion Lift"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/03/ppc-a-b-testing-case-study-175-conversion-lift-featured-600x400.webp?v=1773385149" class="attachment-satellite-card size-satellite-card wp-post-image" alt="PPC A/B Testing Case Study: 175% Conversion Lift" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/03/ppc-a-b-testing-case-study-175-conversion-lift-featured-600x400.webp?v=1773385149 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/03/ppc-a-b-testing-case-study-175-conversion-lift-featured-300x200.webp?v=1773385149 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/campaign-analysis/" style="background:#db3d67"> Campaign Analysis </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="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"/></svg> <time datetime="2026-03-13T06:59:09+00:00">Mar 13, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/ppc-a-b-testing-case-study-175-conversion-lift/">PPC A/B Testing Case Study: 175% Conversion Lift</a> </h3> </div> </article> <article class="t1-card t1-reveal"> <div class="t1-card-media"> <a class="t1-card-image" href="https://ppcgrowthstudio.com/bid-management-avoid-these-costly-mistakes-in-2026/" aria-label="Bid Management: Avoid These Costly Mistakes in 2026"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/03/bid-management-avoid-these-costly-mistakes-in-2026-featured-600x400.webp?v=1773851924" class="attachment-satellite-card size-satellite-card wp-post-image" alt="Bid Management: Avoid These Costly Mistakes in 2026" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/03/bid-management-avoid-these-costly-mistakes-in-2026-featured-600x400.webp?v=1773851924 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/03/bid-management-avoid-these-costly-mistakes-in-2026-featured-300x200.webp?v=1773851924 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> </a> <a class="t1-pill t1-pill--overlay" href="https://ppcgrowthstudio.com/category/social-media/" style="background:#8fc475"> Social Media </a> </div> <div class="t1-card-body"> <div class="t1-card-date"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="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"/></svg> <time datetime="2026-03-18T16:38:45+00:00">Mar 18, 2026</time> </div> <h3 class="t1-card-title"> <a href="https://ppcgrowthstudio.com/bid-management-avoid-these-costly-mistakes-in-2026/">Bid Management: Avoid These Costly Mistakes in 2026</a> </h3> </div> </article> </div> </section> </div> </article> </div><!-- /.t1-site-main --> <footer class="t1-footer" role="contentinfo"> <div class="t1-container"> <div class="t1-footer-grid"> <div class="t1-footer-col"> <h4 class="t1-footer-title">About PPC Growth Studio</h4> <p class="t1-footer-about">Expert insights, guides, and stories about marketing</p> </div> <div class="t1-footer-col"> <h4 class="t1-footer-title">Latest Articles</h4> <div class="t1-footer-post"> <a class="t1-pill" href="https://ppcgrowthstudio.com/category/campaign-analysis/" style="background:#db3d67">Campaign Analysis</a> <a class="t1-footer-post-title" href="https://ppcgrowthstudio.com/pmax-and-agent-traffic-2026-strategy-teardown/">PMax & Agent Traffic: 2026 Strategy Teardown</a> <div class="t1-footer-post-date">Jul 31, 2026</div> </div> <div class="t1-footer-post"> <a class="t1-pill" href="https://ppcgrowthstudio.com/category/marketing-technology/" style="background:#2d70df">Marketing Technology</a> <a class="t1-footer-post-title" href="https://ppcgrowthstudio.com/google-ads-why-30-of-conversions-vanish-in-2026/">Google Ads: Why 30% of Conversions Vanish in 2026</a> <div class="t1-footer-post-date">Jul 31, 2026</div> </div> </div> <div class="t1-footer-col"> <h4 class="t1-footer-title">Quick Links</h4> <ul class="t1-footer-links"> <li><a href="https://ppcgrowthstudio.com/about/">About</a></li><li><a href="https://ppcgrowthstudio.com/contact/">Contact</a></li><li><a href="https://ppcgrowthstudio.com/privacy-policy/">Privacy Policy</a></li><li><a href="https://ppcgrowthstudio.com/terms-of-service/">Terms of Service</a></li><li><a href="https://ppcgrowthstudio.com/category/ai-agent-attribution/">AI Agent Attribution</a></li><li><a href="https://ppcgrowthstudio.com/category/digital-marketing/">Digital Marketing</a></li><li><a href="https://ppcgrowthstudio.com/category/campaign-analysis/">Campaign Analysis</a></li><li><a href="https://ppcgrowthstudio.com/category/marketing-analytics/">Marketing Analytics</a></li> </ul> </div> <div class="t1-footer-col"> <h4 class="t1-footer-title">Trending</h4> <a class="t1-footer-feature" href="https://ppcgrowthstudio.com/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026/"> <img width="600" height="400" src="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-600x400.webp?v=1776360237" class="attachment-satellite-card size-satellite-card wp-post-image" alt="A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)" loading="lazy" decoding="async" srcset="https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-600x400.webp?v=1776360237 600w, https://ppcgrowthstudio.com/wp-content/uploads/sites/37/2026/04/a-b-test-ad-copy-meta-ads-manager-step-by-step-2026-featured-300x200.webp?v=1776360237 300w" sizes="auto, (max-width: 600px) 100vw, 600px" /> <span class="t1-footer-feature-shade"></span> <span class="t1-footer-feature-content"> <span class="t1-pill" style="background:#e8543f">Customer Experience</span> <span class="t1-footer-feature-title">A/B Test Ad Copy: Meta Ads Manager Step-by-Step (2026)</span> </span> </a> </div> </div> <div class="t1-footer-bottom"> <div class="t1-footer-copy">© 2026 <strong>PPC Growth Studio</strong>. All rights reserved.</div> <form class="t1-footer-subscribe" id="t1-subscribe" data-t1-cta-trigger> <input type="email" name="email" placeholder="Enter your email address" aria-label="Enter your email address" /> <button type="submit" class="t1-subscribe">Subscribe</button> </form> </div> </div> </footer> <script> (function(){ // Route SUBSCRIBE / footer form / sticky CTA into the existing lead-popup // mu-plugins. Graceful fallback: scroll to the footer subscribe field. function openLead(source){ try { if (window.LawyerLeadFormV2 && window.LawyerLeadFormV2.open) { window.LawyerLeadFormV2.open(source); return true; } if (window.AeoLeadPopup && window.AeoLeadPopup.open) { window.AeoLeadPopup.open(source); return true; } if (window.ExitIntentPopup && window.ExitIntentPopup.open) { window.ExitIntentPopup.open(source); return true; } document.dispatchEvent(new CustomEvent('satellite:open-lead-popup', {detail: {source: source}})); return true; } catch(e){ return false; } } document.querySelectorAll('[data-t1-cta-trigger]').forEach(function(el){ el.addEventListener(el.tagName === 'FORM' ? 'submit' : 'click', function(e){ e.preventDefault(); if (!openLead('t1_cta')) { var f = document.getElementById('t1-subscribe'); if (f) f.scrollIntoView({behavior:'smooth', block:'center'}); } }); }); })(); </script> <script id="sat-tts-script"> (function(){ if (!('speechSynthesis' in window) || !('SpeechSynthesisUtterance' in window)) { document.querySelectorAll('.sat-tts-player').forEach(function(el){ el.setAttribute('data-state', 'unsupported'); var t = el.querySelector('.sat-tts-title-text'); var i18n = el.querySelector('.sat-tts-i18n'); if (t && i18n) t.textContent = i18n.dataset.unsupported; }); return; } var player = document.querySelector('.sat-tts-player[data-sat-tts]'); if (!player) return; var contentRoot = document.querySelector('.post-content') || document.querySelector('article .single-post-main') || document.querySelector('article'); if (!contentRoot) return; var toggleBtn = player.querySelector('.sat-tts-toggle'); var rateBtn = player.querySelector('.sat-tts-rate'); var titleText = player.querySelector('.sat-tts-title-text'); var progressEl = player.querySelector('.sat-tts-progress-fill'); var i18n = player.querySelector('.sat-tts-i18n'); var STORAGE_KEY = 'sat_tts_v1_' + (location.pathname || '/'); var RATE_CYCLE = [1, 1.25, 1.5, 2, 0.85]; // ── Build chunks ───────────────────────────────────────────── // Strip HTML to a clean, sequential text array. We skip elements // that read awkwardly aloud (figures, embedded video/audio, the // related-callout sidebars, FAQ schema-heavy sections, code). function buildChunks(root) { var clone = root.cloneNode(true); // Remove things we never want spoken. clone.querySelectorAll( 'script,style,figure,iframe,video,audio,svg,noscript,' + 'aside,.related-callout,.sidebar-share,.sat-tts-player,' + '.article-feedback,.author-bio,.post-tags,.read-next-bar,' + '.post-navigation,.related-posts,form,nav,.toc-container' ).forEach(function(n){ n.parentNode && n.parentNode.removeChild(n); }); var blockSel = 'h1,h2,h3,h4,h5,h6,p,li,blockquote,td,th,dt,dd'; var blocks = clone.querySelectorAll(blockSel); var chunks = []; blocks.forEach(function(b){ var t = (b.textContent || '').replace(/\s+/g, ' ').trim(); if (!t) return; // Long paragraphs: break on sentence boundaries so Chrome // doesn't silently drop after ~15s of a single utterance. if (t.length > 220) { var sentences = t.match(/[^.!?]+[.!?]+(?:\s|$)|[^.!?]+$/g) || [t]; var buf = ''; sentences.forEach(function(s){ s = s.trim(); if (!s) return; if ((buf + ' ' + s).trim().length > 220 && buf) { chunks.push(buf.trim()); buf = s; } else { buf = (buf ? buf + ' ' : '') + s; } }); if (buf.trim()) chunks.push(buf.trim()); } else { chunks.push(t); } }); return chunks; } var chunks = buildChunks(contentRoot); if (chunks.length === 0) return; var totalChars = chunks.reduce(function(a,c){ return a + c.length; }, 0); // ── State machine ─────────────────────────────────────────── var state = { playing: false, paused: false, chunkIndex: 0, charsSpoken: 0, rate: 1, voice: null, currentUtter: null, }; try { var saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null'); if (saved && typeof saved.chunkIndex === 'number' && saved.chunkIndex < chunks.length) { state.chunkIndex = saved.chunkIndex; state.charsSpoken = saved.charsSpoken || 0; state.rate = saved.rate || 1; } } catch (e) {} applyRate(state.rate); updateProgress(); // ── Voice selection ───────────────────────────────────────── // speechSynthesis populates voices async on most browsers. The // picker: // 1. filters by html lang prefix (en/es/...) // 2. drops novelty/character voices (Apple ships ~30 of them // and they sort BEFORE the natural ones on macOS, which is // why an unguarded pool[0] fallback ends up reading // Spanish articles in Eddy/Flo/Reed instead of Mónica) // 3. ranks survivors by an explicit per-language preference // list of high-quality voices, with a fallback that prefers // `localService` (built-in premium) over remote voices. var NOVELTY_NAME_RE = /^(Albert|Bad News|Bahh|Bells|Boing|Bubbles|Cellos|Deranged|Good News|Hysterical|Pipe Organ|Trinoids|Whisper|Wobble|Zarvox|Eddy|Flo|Grandma|Grandpa|Jester|Junior|Kathy|Organ|Princess|Ralph|Reed|Rocko|Sandy|Shelley|Superstar|Vicki|Victoria|Bahh|Boing|Cellos)\b/i; var PREFERRED_BY_LANG = { es: [ // macOS / iOS premium Spanish voices (best quality) /^M[oó]nica/i, // es-ES, very natural /^Paulina/i, // es-MX, very natural /^Jorge\b/i, // es-ES /^Diego\b/i, // es-AR /^Juan\b/i, // Microsoft Edge / Windows neural Spanish voices /Microsoft.*\b(Elvira|Dalia|Alvaro|Jorge|Helena|Sabina)\b.*Online/i, /Microsoft.*\b(Elvira|Dalia|Alvaro)\b/i, // Google Spanish (Chrome desktop, Android) /^Google\s+espa[ñn]ol(?:\s+de\s+(?:M[eé]xico|Estados Unidos))?$/i, /^Google\s+espa[ñn]ol/i, ], en: [ /^Google\s.*(US|UK|English)/i, /^Microsoft.*(Aria|Jenny|Guy|Ryan|Davis)\b.*Online/i, /^Microsoft.*(Aria|Jenny|Guy|Ryan|Davis)\b/i, /^Samantha$/i, /^Alex$/i, /English/i, ], }; function scoreVoice(v, preferredList) { for (var i = 0; i < preferredList.length; i++) { if (preferredList[i].test(v.name)) { // Earlier list entries score higher. localService gets a // small bump so a built-in voice wins over a remote one // when both match the same pattern. return 1000 - i * 10 + (v.localService ? 1 : 0); } } // No name match — still prefer non-novelty + localService. return (v.localService ? 1 : 0); } function pickVoice() { var voices = speechSynthesis.getVoices() || []; if (voices.length === 0) return null; var lang = (document.documentElement.lang || 'en').toLowerCase().split('-')[0]; var pool = voices.filter(function(v){ return (v.lang || '').toLowerCase().indexOf(lang) === 0; }); if (pool.length === 0) pool = voices; // Strip novelty voices unless that leaves us with nothing. var filtered = pool.filter(function(v){ return !NOVELTY_NAME_RE.test(v.name); }); if (filtered.length > 0) pool = filtered; var preferredList = PREFERRED_BY_LANG[lang] || PREFERRED_BY_LANG.en; var best = null, bestScore = -Infinity; pool.forEach(function(v){ var s = scoreVoice(v, preferredList); if (s > bestScore) { bestScore = s; best = v; } }); return best || pool[0]; } if (typeof speechSynthesis.addEventListener === 'function') { speechSynthesis.addEventListener('voiceschanged', function(){ state.voice = pickVoice(); }); } state.voice = pickVoice(); // ── Utterance queue ───────────────────────────────────────── function speakNext() { if (state.chunkIndex >= chunks.length) { stopAll(true); return; } var u = new SpeechSynthesisUtterance(chunks[state.chunkIndex]); u.rate = state.rate; u.pitch = 1; u.volume = 1; // Setting `lang` explicitly is what triggers the Spanish // synthesis backend on Chrome/Edge (which shipped neural // Spanish voices that aren't always exposed via getVoices // until referenced). When `voice` is set we prefer the // voice's own lang to avoid a mismatch. if (state.voice) { u.voice = state.voice; if (state.voice.lang) u.lang = state.voice.lang; } else { u.lang = (document.documentElement.lang || 'en'); } u.onend = function() { if (!state.playing) return; state.charsSpoken += chunks[state.chunkIndex].length; state.chunkIndex += 1; persist(); updateProgress(); if (state.chunkIndex < chunks.length) { speakNext(); } else { stopAll(true); } }; u.onerror = function(ev) { if (ev && ev.error === 'interrupted') return; state.playing = false; state.paused = false; state.currentUtter = null; renderState(); }; u.onboundary = function(ev) { if (ev && typeof ev.charIndex === 'number') { var partial = state.charsSpoken + Math.min(ev.charIndex, chunks[state.chunkIndex].length); var pct = Math.min(100, (partial / totalChars) * 100); progressEl.style.width = pct + '%'; } }; state.currentUtter = u; speechSynthesis.speak(u); } function play() { if (state.paused) { speechSynthesis.resume(); state.paused = false; state.playing = true; renderState(); return; } // Some browsers leave the queue stuck after a previous error; // cancel before starting fresh. try { speechSynthesis.cancel(); } catch (e) {} state.playing = true; state.paused = false; renderState(); speakNext(); } function pause() { if (!state.playing) return; try { speechSynthesis.pause(); } catch (e) {} state.paused = true; state.playing = false; persist(); renderState(); } function stopAll(reset) { try { speechSynthesis.cancel(); } catch (e) {} state.playing = false; state.paused = false; state.currentUtter = null; if (reset) { state.chunkIndex = 0; state.charsSpoken = 0; progressEl.style.width = '0%'; try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} } renderState(); } // ── Rate cycle ───────────────────────────────────────────── function applyRate(r) { state.rate = r; rateBtn.textContent = r.toFixed(2).replace(/\.?0+$/, '') + 'x'; persist(); } rateBtn.addEventListener('click', function(){ var i = RATE_CYCLE.indexOf(state.rate); var next = RATE_CYCLE[(i + 1) % RATE_CYCLE.length]; applyRate(next); // If currently playing, restart the current chunk at the // new rate (Web Speech doesn't let you change rate mid-utter). if (state.playing && state.currentUtter) { stopAll(false); state.playing = true; renderState(); speakNext(); } }); toggleBtn.addEventListener('click', function(){ if (state.playing) { pause(); } else { play(); } }); // ── Chrome 15-second cutoff workaround ───────────────────── // Chrome stops Web Speech after ~15s of continuous output. We // ping pause+resume every 10s while playing to keep the queue // alive. Harmless on browsers that don't need it. setInterval(function(){ if (state.playing && !state.paused) { try { speechSynthesis.pause(); speechSynthesis.resume(); } catch (e) {} } }, 10000); // ── Persistence + UI ─────────────────────────────────────── function persist() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ chunkIndex: state.chunkIndex, charsSpoken: state.charsSpoken, rate: state.rate, })); } catch (e) {} } function updateProgress() { var pct = totalChars ? Math.min(100, (state.charsSpoken / totalChars) * 100) : 0; progressEl.style.width = pct + '%'; } function renderState() { if (!i18n) return; if (state.playing) { player.setAttribute('data-state', 'playing'); titleText.textContent = i18n.dataset.playing; toggleBtn.setAttribute('aria-label', 'Pause'); } else if (state.paused) { player.setAttribute('data-state', 'paused'); titleText.textContent = i18n.dataset.paused; toggleBtn.setAttribute('aria-label', 'Resume'); } else { player.removeAttribute('data-state'); titleText.textContent = i18n.dataset.listen; toggleBtn.setAttribute('aria-label', 'Play'); } } // ── GA4 listen events ───────────────────────────────────── // Fire a small custom event the first time a visitor presses // play, so we can answer "does the listen button move // engagement?" from the analytics dashboard. var firedFirstPlay = false; toggleBtn.addEventListener('click', function(){ if (firedFirstPlay) return; if (typeof window.gtag === 'function') { try { window.gtag('event', 'tts_listen_play', { event_category: 'engagement', event_label: location.pathname, value: 1, }); } catch (e) {} } firedFirstPlay = true; }); // Stop speech when the visitor leaves the page so it doesn't // continue narrating in the background after navigation. window.addEventListener('beforeunload', function(){ try { speechSynthesis.cancel(); } catch (e) {} }); })(); </script> <script type="speculationrules"> {"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/sites/37/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/satellite-theme1/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]} </script> <script src="https://ppcgrowthstudio.com/wp-content/themes/satellite-theme1/assets/main.js?ver=1.1.5" id="satellite-theme1-main-js"></script> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=DM+Mono:wght@400;500&display=swap"></noscript> <style id="aeo-popup-styles"> #aeo-popup-root { --accent: #E8471A; --accent-dim: #C93C12; --accent-mid: #F07040; --accent-soft: #F9A07A; --card-bg: linear-gradient(105deg, #ffffff 0%, #fff4f0 40%, #ffeee7 70%, #ffffff 100%); --card-fade: #fff8f5; --bar-bg: linear-gradient(105deg, #ffffff 0%, #fff4f0 40%, #ffeee7 70%, #ffffff 100%); --cta-grad: linear-gradient(90deg, #E8471A 0%, #F07040 50%, #E8471A 100%); --cta-text: #ffffff; --shadow-rgb: 232,71,26; --ink: #111111; --ink-soft: #555555; --ink-faint: #999999; --border: #E2E4DC; --banner-h: 88px; /* "Google Sans" is in the stack as the brand fallback for installs that have it locally (e.g. internal Google environments); Inter is the loaded webfont and the de-facto free substitute. */ --font-display: 'Inter', 'Google Sans', system-ui, sans-serif; --font-body: 'Inter', 'Google Sans', system-ui, sans-serif; --font-mono: 'DM Mono', ui-monospace, Menlo, monospace; } #aeo-popup-root, #aeo-popup-root * { box-sizing: border-box; } @keyframes aeoPulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:.4; transform:scale(.7); } } @keyframes aeoCtaShimmer { 0% { background-position:0% 0%; } 50% { background-position:100% 0%; } 100% { background-position:0% 0%; } } @keyframes aeoBannerBreathe{ 0% { background-position:0% 50%; } 50% { background-position:100% 50%; } 100% { background-position:0% 50%; } } @keyframes aeoCardSlideUp { to { transform: translateY(0); opacity: 1; } } @keyframes aeoCardSlideDown{ to { transform: translateY(calc(100% + 32px)); opacity: 0; } } @keyframes aeoBarSlideUp { to { transform: translateY(0); opacity: 1; } } @keyframes aeoScTicker { 0% { transform: translateX(0); } 100% { transform: translateX(-33.3333%); } } /* ── STICKY CARD (bottom-left, initial state) ────────────────────── Default state is hidden -- JS adds .aeo-iframe-ready on the root once the modal iframe has finished loading in the background (or after a safety timeout), so the slide-up coincides with the funnel being click-ready. */ #aeo-popup-root .sticky-card { position: fixed; bottom: 24px; left: 24px; z-index: 9990; width: 340px; border-radius: 16px; overflow: hidden; border: 1px solid var(--border); box-shadow: 0 8px 40px rgba(var(--shadow-rgb),0.15), 0 2px 8px rgba(0,0,0,0.07); background: var(--card-bg); background-size: 300% 300%; transform: translateY(calc(100% + 32px)); opacity: 0; } #aeo-popup-root.aeo-iframe-ready .sticky-card { animation: aeoCardSlideUp 0.6s cubic-bezier(0.22,1,0.36,1) forwards, aeoBannerBreathe 5s ease-in-out 0.6s infinite; } #aeo-popup-root.aeo-skip-card .sticky-card { display: none; } #aeo-popup-root.aeo-lead-done .sticky-card, #aeo-popup-root.aeo-lead-done .sticky-banner { display: none; } #aeo-popup-root .sticky-card.hiding { animation: aeoCardSlideDown 0.4s cubic-bezier(0.4,0,1,1) forwards !important; } /* ── Border Beam ─────────────────────────────────────── A bright accent-coloured "comet" travels around the card perimeter. Implemented as a single ::before pseudo-element: 1. conic-gradient paints the comet (transparent for ~75% of the circle, then a soft -> bright -> soft fade across the last ~100°), 2. mask-composite:exclude cuts the inside out so only a 2px ring remains -- the comet appears to glide along the border, 3. @property animates the gradient's `from` angle so the rotation is on the gradient itself, not via transform (no sub-pixel shimmer, no GPU compositing cost, and the corners stay sharp). Browsers without @property (older Firefox <128, older Safari <16.4) render the gradient statically -- graceful degradation. */ @property --aeo-beam-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; } #aeo-popup-root .sticky-card::before { content: ''; position: absolute; inset: 0; border-radius: inherit; padding: 2px; background: conic-gradient( from var(--aeo-beam-angle), transparent 0deg, transparent 250deg, var(--accent-soft) 305deg, var(--accent) 340deg, var(--accent-soft) 360deg ); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask-composite: exclude; pointer-events: none; z-index: 1; opacity: 0; transition: opacity .4s ease; } #aeo-popup-root.aeo-iframe-ready .sticky-card::before { opacity: 1; animation: aeoBeamRotate 5s linear infinite; } @keyframes aeoBeamRotate { to { --aeo-beam-angle: 360deg; } } #aeo-popup-root .sc-inner { padding: 18px 18px 20px; } #aeo-popup-root .sc-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; } #aeo-popup-root .sc-label { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-dim); display: flex; align-items: center; gap: 6px; } #aeo-popup-root .sc-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: aeoPulse 2s ease-in-out infinite; flex-shrink: 0; } #aeo-popup-root .sc-close { width: 26px; height: 26px; border-radius: 50%; border: 1px solid var(--border); background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--ink-faint); font-size: 16px; line-height: 1; transition: border-color .2s, color .2s, background .2s; flex-shrink: 0; padding: 0; } #aeo-popup-root .sc-close:hover { border-color: var(--ink); color: var(--ink); background: #F5F6F2; } #aeo-popup-root .sc-headline { font-family: var(--font-display); font-size: 19px; font-weight: 800; color: var(--ink); letter-spacing: -0.4px; line-height: 1.25; margin: 0 0 8px; } #aeo-popup-root .sc-sub { font-size: 13px; color: var(--ink-faint); line-height: 1.5; margin: 0 0 12px; font-family: var(--font-body); } #aeo-popup-root .sc-ticker-wrap { position: relative; overflow: hidden; margin-bottom: 14px; height: 28px; } #aeo-popup-root .sc-ticker-wrap::before, #aeo-popup-root .sc-ticker-wrap::after { content: ''; position: absolute; inset-block: 0; width: 20px; z-index: 2; pointer-events: none; } #aeo-popup-root .sc-ticker-wrap::before { left: 0; background: linear-gradient(to right, var(--card-fade), transparent); } #aeo-popup-root .sc-ticker-wrap::after { right: 0; background: linear-gradient(to left, var(--card-fade), transparent); } #aeo-popup-root .sc-ticker { /* Marquee math (do not change without revisiting both this rule AND the aeoScTicker keyframe + the JS that builds the chip list): - Each chip is 28px wide with an 8px trailing margin = 36px slot. - We render 3 copies of the 6-engine ticker = 18 chips = 648px. - One copy width = 6 * 36 = 216px = exactly 1/3 of the total. - The keyframe translates -33.3333% (= -216px), so the moment the first copy finishes scrolling out, the second copy is already occupying its exact original position. Seamless loop. We use margin-right on the chip (not flex gap) on purpose: flex gap only sits *between* siblings, which is one short for the cycle to line up with translateX. margin-right counts on every chip, so the total width is N*(chip+gap) and -1/N translation hits the boundary cleanly. Three copies is also what keeps the right edge of the card from ever going empty mid-scroll -- a single copy (216px) is narrower than the visible card content area, so doubled content would leave a gap on the right at the end of every cycle. */ display: flex; width: max-content; animation: aeoScTicker 18s linear infinite; align-items: center; height: 100%; } #aeo-popup-root .sc-logo-chip { width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0; margin-right: 8px; display: flex; align-items: center; justify-content: center; overflow: hidden; /* Faint inset border. On dark/coloured chips it's invisible; on the white Copilot chip it's what keeps the icon from floating against the light card background. */ box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06); } #aeo-popup-root .sc-logo-chip img { width: 18px; height: 18px; object-fit: contain; display: block; filter: drop-shadow(0 1px 1px rgba(0,0,0,0.15)); } /* Copilot-specific override: Microsoft's multicolor ribbon mark has finer detail than any of the other engines' marks (which are single- colour silhouettes), and its lighter gradient tips (cyan, yellow, pink) wash out at small sizes on a white chip. Bumping the image to nearly fill the 28px chip restores legibility. We target by [title] so future engine additions don't accidentally pick this up. */ #aeo-popup-root .sc-logo-chip[title="Copilot"] img { width: 24px; height: 24px; /* No drop-shadow on the Copilot icon -- the soft glow muddies the gradient ribbon. */ filter: none; } #aeo-popup-root .sc-cta { display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; font-family: var(--font-body); font-size: 14px; font-weight: 600; color: var(--cta-text); background: var(--cta-grad); background-size: 200% 100%; border: none; border-radius: 100px; padding: 11px 20px; cursor: pointer; text-decoration: none; animation: aeoCtaShimmer 3s ease-in-out 2.4s infinite; transition: transform .15s, box-shadow .2s; } #aeo-popup-root .sc-cta:hover { transform: scale(1.02); box-shadow: 0 4px 20px rgba(var(--shadow-rgb),0.4); animation-play-state: paused; } #aeo-popup-root .sc-cta .arrow { font-size: 15px; transition: transform .2s; } #aeo-popup-root .sc-cta:hover .arrow { transform: translateX(3px); } /* ── STICKY BAR (after card dismissed) ──────────────────────────── */ #aeo-popup-root .sticky-banner { position: fixed; bottom: 0; left: 0; right: 0; z-index: 9990; transform: translateY(100%); opacity: 0; pointer-events: none; background: var(--bar-bg); background-size: 300% 300%; border-top: 1px solid var(--border); box-shadow: 0 -4px 32px rgba(var(--shadow-rgb),0.08), 0 -1px 0 rgba(var(--shadow-rgb),0.15); } #aeo-popup-root .sticky-banner.visible { pointer-events: all; animation: aeoBarSlideUp 0.5s cubic-bezier(0.22,1,0.36,1) forwards, aeoBannerBreathe 5s ease-in-out 0.5s infinite; } #aeo-popup-root .sticky-banner::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, var(--accent) 0%, var(--accent-soft) 60%, transparent 100%); } #aeo-popup-root .banner-inner { max-width: 1240px; margin: 0 auto; padding: 0 32px; height: var(--banner-h); display: flex; align-items: center; gap: 24px; } #aeo-popup-root .banner-label { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-dim); white-space: nowrap; flex-shrink: 0; display: flex; align-items: center; gap: 6px; } #aeo-popup-root .banner-label::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: aeoPulse 2s ease-in-out infinite; } #aeo-popup-root .banner-divider-v { width: 1px; height: 32px; background: var(--border); flex-shrink: 0; } #aeo-popup-root .banner-headline { font-family: var(--font-display); font-size: 15px; font-weight: 800; color: var(--ink); letter-spacing: -0.3px; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0; } #aeo-popup-root .banner-sub { font-size: 13px; color: var(--ink-faint); flex-shrink: 0; white-space: nowrap; font-family: var(--font-body); } #aeo-popup-root .banner-cta { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-body); font-size: 14px; font-weight: 600; color: var(--cta-text); background: var(--cta-grad); background-size: 200% 100%; border: none; border-radius: 100px; padding: 11px 28px; cursor: pointer; text-decoration: none; white-space: nowrap; flex-shrink: 0; animation: aeoCtaShimmer 3s ease-in-out 0.5s infinite; transition: transform .15s, box-shadow .2s; } #aeo-popup-root .banner-cta:hover { transform: scale(1.03); box-shadow: 0 4px 20px rgba(var(--shadow-rgb),0.4); animation-play-state: paused; } #aeo-popup-root .banner-cta .arrow { font-size: 16px; line-height: 1; transition: transform .2s; } #aeo-popup-root .banner-cta:hover .arrow { transform: translateX(3px); } /* ── IFRAME MODAL (full-screen takeover) ────────────────────────── */ /* Matches the Verdict V2 full-page lead capture pattern: edge-to-edge shell, no card chrome, just a translucent floating top bar with "Back to article" and a close pill. The AEO funnel's own branding inside the iframe carries the visual weight. */ #aeo-popup-root .aeo-modal { position: fixed; inset: 0; z-index: 10000; background: #ffffff; display: flex; flex-direction: column; opacity: 0; pointer-events: none; transition: opacity .25s ease; } #aeo-popup-root .aeo-modal.open { opacity: 1; pointer-events: all; } #aeo-popup-root .aeo-modal-backdrop { display: none; } #aeo-popup-root .aeo-modal-shell { position: relative; z-index: 1; display: flex; flex-direction: column; width: 100%; height: 100%; background: #ffffff; } #aeo-popup-root .aeo-modal-bar { position: relative; z-index: 3; display: flex; align-items: center; justify-content: space-between; padding: 12px 20px; background: #ffffff; border-bottom: 1px solid rgba(0,0,0,0.06); flex-shrink: 0; } #aeo-popup-root .aeo-modal-back { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-body); font-size: 13px; font-weight: 500; color: var(--ink-soft); background: transparent; border: none; cursor: pointer; padding: 6px 8px; border-radius: 8px; transition: background .15s, color .15s; } #aeo-popup-root .aeo-modal-back:hover { background: #F0F1EC; color: var(--ink); } #aeo-popup-root .aeo-modal-iframe-wrap { position: relative; flex: 1; background: #ffffff; overflow: hidden; } #aeo-popup-root .aeo-modal-iframe { width: 100%; height: 100%; border: 0; display: block; background: #ffffff; } #aeo-popup-root .aeo-modal-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-faint); background: #ffffff; transition: opacity .3s ease; z-index: 1; pointer-events: none; } #aeo-popup-root .aeo-modal-iframe-wrap.loaded .aeo-modal-loading { opacity: 0; } #aeo-popup-root .aeo-modal-loading .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); animation: aeoPulse 1.2s ease-in-out infinite; margin-right: 8px; } /* lock background scroll when modal is open */ body.aeo-modal-open { overflow: hidden; } /* Bar collapses to headline + CTA on narrower viewports, but the card keeps its 340px bottom-left footprint all the way down to phone widths. Real phones (≤480px) get the full-width card so the CTA stays tappable. */ @media (max-width: 900px) { #aeo-popup-root .banner-inner { padding: 0 16px; gap: 12px; } #aeo-popup-root .banner-label, #aeo-popup-root .banner-divider-v, #aeo-popup-root .banner-sub { display: none; } #aeo-popup-root .banner-headline { font-size: 13px; } #aeo-popup-root .banner-cta { font-size: 13px; padding: 9px 18px; } } @media (max-width: 480px) { #aeo-popup-root .sticky-card { left: 12px; bottom: 12px; width: calc(100vw - 24px); } } @media (prefers-reduced-motion: reduce) { #aeo-popup-root .sticky-card, #aeo-popup-root .sticky-banner.visible, #aeo-popup-root .sc-cta, #aeo-popup-root .banner-cta { animation: none !important; } #aeo-popup-root.aeo-iframe-ready .sticky-card { transform: none; opacity: 1; } } </style> <div id="aeo-popup-root" data-variant="small-report" data-domain="ppcgrowthstudio.com"> <div class="sticky-card" id="aeoStickyCard" role="complementary" aria-label="AI Visibility Radar — Free AEO Report"> <div class="sc-inner"> <div class="sc-header"> <span class="sc-label"><span class="sc-dot"></span>AI VISIBILITY RADAR</span> <button type="button" class="sc-close" id="aeoCardClose" aria-label="Dismiss">×</button> </div> <p class="sc-headline">Are AI engines recommending your brand?</p> <p class="sc-sub">Check your score on 5 AI engines — instantly.</p> <div class="sc-ticker-wrap"><div class="sc-ticker" id="aeoScTicker"></div></div> <a href="#" class="sc-cta" id="aeoCardCta"> Check for Free <span class="arrow">→</span> </a> </div> </div> <div class="sticky-banner" id="aeoStickyBanner" role="complementary" aria-label="AI Visibility Radar — Free AEO Report"> <div class="banner-inner"> <span class="banner-label">AI VISIBILITY RADAR</span> <div class="banner-divider-v"></div> <p class="banner-headline">Are AI engines recommending your brand?</p> <span class="banner-sub">Check your score on 5 AI engines — instantly.</span> <a href="#" class="banner-cta" id="aeoBarCta"> Check for Free <span class="arrow">→</span> </a> </div> </div> <div class="aeo-modal" id="aeoModal" role="dialog" aria-modal="true" aria-label="AI Visibility Radar — Free AEO Report" aria-hidden="true"> <div class="aeo-modal-backdrop" data-aeo-close></div> <div class="aeo-modal-shell"> <header class="aeo-modal-bar"> <button type="button" class="aeo-modal-back" data-aeo-close> <span aria-hidden="true">←</span> Back to article </button> </header> <div class="aeo-modal-iframe-wrap" id="aeoModalIframeWrap"> <div class="aeo-modal-loading"><span class="dot"></span>Loading report…</div> <iframe id="aeoModalIframe" class="aeo-modal-iframe" title="AI Visibility Radar — Free AEO Report" referrerpolicy="no-referrer-when-downgrade" allow="clipboard-write" src="https://aeo.prod-mobtools.com/aeo/small-report?embed=1&utm_source=ppcgrowthstudio.com&utm_medium=satellite&utm_campaign=aeo-popup&utm_content=small-report"></iframe> </div> </div> </div> </div> <script id="aeo-popup-script"> (function () { var root = document.getElementById('aeo-popup-root'); if (!root) return; var card = document.getElementById('aeoStickyCard'); var bar = document.getElementById('aeoStickyBanner'); var modal = document.getElementById('aeoModal'); var iframe = document.getElementById('aeoModalIframe'); var wrap = document.getElementById('aeoModalIframeWrap'); var closeBtn = document.getElementById('aeoCardClose'); var cardCta = document.getElementById('aeoCardCta'); var barCta = document.getElementById('aeoBarCta'); var ticker = document.getElementById('aeoScTicker'); /* ---------- analytics -------------------------------- Posts to the existing /wp-json/satellite/v1/lead-event proxy, which forwards to the backend's POST /api/leads/event. Events here are prefixed with "aeo_" so the lawyer funnel chart on the same dashboard never picks them up. ----------------------------------------------------- */ var AEO_DOMAIN = "ppcgrowthstudio.com"; var AEO_VARIANT = "small-report"; var AEO_NICHE = "marketing"; var AEO_EVENT_URL = "https:\/\/ppcgrowthstudio.com\/wp-json\/satellite\/v1\/lead-event"; var AEO_EVENT_NONCE = "48b9e011a9"; /* localStorage visitor_id so unique-clicker counts on the dashboard are stable across sessions on the same browser. UUIDv4-ish via random bytes; no PII, just a stable handle for COUNT(DISTINCT) on the backend. */ function aeoGetVisitorId() { try { var KEY = 'aeo_vid'; var existing = localStorage.getItem(KEY); if (existing) return existing; var b = (crypto && crypto.getRandomValues) ? crypto.getRandomValues(new Uint8Array(16)) : (function () { var arr = []; for (var i=0;i<16;i++) arr.push(Math.floor(Math.random()*256)); return arr; })(); b[6] = (b[6] & 0x0f) | 0x40; // version 4 b[8] = (b[8] & 0x3f) | 0x80; // variant var hex = Array.prototype.map.call(b, function (x) { return ('0' + x.toString(16)).slice(-2); }).join(''); var id = hex.slice(0,8)+'-'+hex.slice(8,12)+'-'+hex.slice(12,16)+'-'+hex.slice(16,20)+'-'+hex.slice(20,32); localStorage.setItem(KEY, id); return id; } catch (e) { return ''; // private mode / disabled storage -- silently drop } } var AEO_VISITOR_ID = aeoGetVisitorId(); function aeoTrack(eventType, extraMeta) { try { var meta = { variant: AEO_VARIANT, niche: AEO_NICHE }; if (extraMeta) for (var k in extraMeta) if (Object.prototype.hasOwnProperty.call(extraMeta, k)) meta[k] = extraMeta[k]; var payload = { domain: AEO_DOMAIN, event_type: eventType, visitor_id: AEO_VISITOR_ID, page_url: location.href, metadata_json: JSON.stringify(meta) }; var body = JSON.stringify(payload); if (navigator.sendBeacon) { navigator.sendBeacon( AEO_EVENT_URL + '?_wpnonce=' + encodeURIComponent(AEO_EVENT_NONCE), new Blob([body], { type: 'application/json' }) ); } else { fetch(AEO_EVENT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': AEO_EVENT_NONCE }, body: body, keepalive: true }); } } catch (e) { /* analytics must never break the popup */ } } /* ---------- cookies ---------- */ function readCookie(name) { var m = document.cookie.match('(?:^|;)\\s*' + name + '=([^;]*)'); return m ? decodeURIComponent(m[1]) : ''; } function writeCookie(name, value, days) { var d = new Date(); d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000); document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + d.toUTCString() + '; path=/; SameSite=Lax'; } /* Cookie names are versioned. An earlier iteration of this script set aeo_lead_done from a 15s dwell-time heuristic (since removed); any tester who left the modal open during that period now carries a 30-day cookie that silently hides the popup forever. Bumping the name to _v2 retires that stale state without forcing anyone to clear browser cookies. */ var CARD_DISMISSED = 'aeo_card_dismissed_v2'; var LEAD_DONE = 'aeo_lead_done_v2'; if (readCookie(LEAD_DONE) === '1') { root.classList.add('aeo-lead-done'); return; } /* ---------- reveal the card (or bar) ---------- The iframe is preloading in the background regardless of card state, so we don't gate reveal on it -- we just slide the card up on a quick fixed timer so the popup is visible reliably on every refresh, even when the cross-origin iframe's load event is slow or never fires. By the time the user finishes reading the article and clicks the CTA, the iframe is virtually always fully loaded, so clicks still open the modal instantly. */ var revealed = false; var dismissedBefore = readCookie(CARD_DISMISSED) === '1'; if (dismissedBefore) root.classList.add('aeo-skip-card'); function reveal() { if (revealed) return; revealed = true; if (dismissedBefore) { if (bar) bar.classList.add('visible'); aeoTrack('aeo_bar_impression', { source: 'bar' }); } else { root.classList.add('aeo-iframe-ready'); aeoTrack('aeo_impression', { source: 'card' }); } } /* 600ms is long enough to feel intentional (not jarring on first paint), short enough that it always wins against a slow cross-origin iframe. */ setTimeout(reveal, 600); /* ---------- ticker ---------- Three full copies of the chip list, not two. The aeoScTicker keyframe translates by exactly one copy's width (-33.3333%) so the second copy slides into the first's position seamlessly; the third copy is there to keep the right edge of the card filled while the second is doing the heavy lifting -- one copy alone (216px) is narrower than the visible card content area, so a doubled ticker leaves a visible gap at the right edge near the end of every animation cycle. */ var TOOLS = [{"name":"Gemini","bg":"#1A73E8","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/gemini.svg"},{"name":"Grok","bg":"#111111","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/grok.svg"},{"name":"Perplexity","bg":"#1FB8CD","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/perplexity.svg"},{"name":"ChatGPT","bg":"#10A37F","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/chatgpt.svg"},{"name":"Copilot","bg":"#FFFFFF","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/copilot.svg"},{"name":"Claude","bg":"#D97757","logo":"https:\/\/ppcgrowthstudio.com\/wp-content\/mu-plugins\/aeo-assets\/logos\/claude.svg"}]; if (ticker) { var tripled = TOOLS.concat(TOOLS).concat(TOOLS); ticker.innerHTML = tripled.map(function (t) { return '<div class="sc-logo-chip" title="' + t.name + '" style="background:' + t.bg + ';">' + '<img src="' + t.logo + '" alt="' + t.name + '" loading="lazy" width="18" height="18">' + '</div>'; }).join(''); } /* ---------- card -> bar dismissal ---------- The slide-down animation is 0.4s. We use a matched setTimeout instead of animationend because Chrome occasionally swallows the event when another animation (the breathing background) is in flight on the same element. */ if (closeBtn) { closeBtn.addEventListener('click', function () { writeCookie(CARD_DISMISSED, '1', 30); aeoTrack('aeo_card_dismiss', { source: 'card' }); card.classList.add('hiding'); setTimeout(function () { card.style.display = 'none'; if (bar) bar.classList.add('visible'); }, 420); }); } /* ---------- modal open / close ---------- */ if (iframe && wrap) { if (iframe.complete) wrap.classList.add('loaded'); else iframe.addEventListener('load', function () { wrap.classList.add('loaded'); }, { once: true }); } function swapCardToBar() { if (!card || card.style.display === 'none' || card.classList.contains('hiding')) return; card.classList.add('hiding'); setTimeout(function () { card.style.display = 'none'; if (bar) bar.classList.add('visible'); }, 420); } function openModal(e) { if (e) e.preventDefault(); if (!modal) return; modal.setAttribute('aria-hidden', 'false'); requestAnimationFrame(function () { modal.classList.add('open'); }); document.body.classList.add('aeo-modal-open'); } function closeModal() { if (!modal || !modal.classList.contains('open')) return; modal.classList.remove('open'); document.body.classList.remove('aeo-modal-open'); setTimeout(function () { modal.setAttribute('aria-hidden', 'true'); }, 320); aeoTrack('aeo_modal_close'); /* If the card was still showing behind the modal, collapse it to the bar so the user has an obvious re-entry point. No cookie is set here -- only the explicit X on the card persists dismissal across page loads. */ swapCardToBar(); } if (cardCta) cardCta.addEventListener('click', function (e) { aeoTrack('aeo_cta_click', { source: 'card' }); openModal(e); }); if (barCta) barCta.addEventListener('click', function (e) { aeoTrack('aeo_cta_click', { source: 'bar' }); openModal(e); }); /* Generic trigger: any element with [data-aeo-open] opens the same modal. The in-article banner (aeo-inline-banner.php) uses this so it doesn't have to duplicate the modal HTML/JS -- it just renders a CTA with data-aeo-open + data-aeo-source="inline" and rides on the popup infrastructure that's already in the footer. Source is read from data-aeo-source so we can split CTR by surface in the analytics dashboard (card / bar / inline / future surfaces). */ document.querySelectorAll('[data-aeo-open]').forEach(function (el) { el.addEventListener('click', function (e) { var source = el.getAttribute('data-aeo-source') || 'external'; aeoTrack('aeo_cta_click', { source: source }); openModal(e); }); }); if (modal) { modal.querySelectorAll('[data-aeo-close]').forEach(function (el) { el.addEventListener('click', closeModal); }); } document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && modal && modal.classList.contains('open')) closeModal(); }); /* ---------- future postMessage handshake (stub) ---------- Listens for a "lead-complete" event from the iframe so we can flip the LEAD_DONE cookie immediately on real submission (not on dwell time). Wire this up on the AEO side and remove the dwell-time fallback above. */ window.addEventListener('message', function (e) { if (!e || !e.origin || e.origin.indexOf('aeo.prod-mobtools.com') === -1) return; var data = e.data || {}; if (data && data.type === 'aeo:lead-complete') { writeCookie(LEAD_DONE, '1', 30); root.classList.add('aeo-lead-done'); closeModal(); } }); })(); </script> <script id="satellite-cta-fallback">(function(){ function ready(fn){ if(document.readyState!=='loading'){fn();} else {document.addEventListener('DOMContentLoaded',fn);} } ready(function(){ document.querySelectorAll('[class*="-sticky-cta"]').forEach(function(el){ var b = el.querySelector('[class*="-sticky-cta-btn"]'); if (b && /subscribe/i.test(b.textContent)) { el.remove(); } }); }); document.addEventListener('satellite:open-lead-popup', function(){ var has = (window.LawyerLeadFormV2&&window.LawyerLeadFormV2.open)||(window.AeoLeadPopup&&window.AeoLeadPopup.open)||(window.ExitIntentPopup&&window.ExitIntentPopup.open); if (!has) { window.location.href = 'https://ppcgrowthstudio.com/contact/'; } }); })();</script> <script> (function(){ var bar = document.getElementById('reading-progress'); if(!bar) return; var article = document.querySelector('.post-content'); if(!article) return; var milestones = {25:false,50:false,75:false,100:false}; var startTime = Date.now(); function sendGA4(name, params){ if (window.gtag) window.gtag('event', name, params); } window.addEventListener('scroll', function(){ var rect = article.getBoundingClientRect(); var total = article.offsetHeight - window.innerHeight; var progress = Math.min(100, Math.max(0, (-rect.top / total) * 100)); bar.style.width = progress + '%'; var pct = Math.floor(progress); [25,50,75,100].forEach(function(m){ if (pct >= m && !milestones[m]) { milestones[m] = true; sendGA4('scroll_depth', {percent: m, reading_seconds: Math.round((Date.now()-startTime)/1000)}); } }); }, {passive:true}); function sendReadingTime(){ var seconds = Math.round((Date.now()-startTime)/1000); if (seconds > 3) sendGA4('reading_time', {seconds: seconds}); } document.addEventListener('visibilitychange', function(){ if(document.visibilityState==='hidden') sendReadingTime(); }); window.addEventListener('beforeunload', sendReadingTime); })(); </script> <script> (function(){ var toc = document.querySelector('.toc-container'); if(!toc) return; var title = toc.querySelector('.toc-title'); if(window.innerWidth <= 768 && title) { toc.classList.add('toc-collapsed'); title.addEventListener('click', function(){ toc.classList.toggle('toc-collapsed'); }); } var links = toc.querySelectorAll('.toc-list a'); var sections = []; links.forEach(function(a){ var id = a.getAttribute('href'); if(id) { var el = document.querySelector(id); if(el) sections.push({el:el,link:a}); } }); if(!sections.length) return; var raf; window.addEventListener('scroll', function(){ if(raf) return; raf = requestAnimationFrame(function(){ raf = null; var scrollY = window.scrollY + 120; var active = sections[0]; for(var i=0;i<sections.length;i++){ if(sections[i].el.offsetTop <= scrollY) active = sections[i]; } links.forEach(function(l){ l.classList.remove('toc-active'); }); if(active) active.link.classList.add('toc-active'); }); }, {passive:true}); })(); </script> </body> </html>