1 2 3 4<!DOCTYPE html> 5<html lang="en"> 6<head> 7 <meta charset="UTF-8"> 8 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 9 <title>Pricing â GrammarMama</title> 10 <meta name="description" content="Simple, transparent pricing for GrammarMama. Choose the plan that fits your learning goals."> 11 <link rel="canonical" href="https://grammarmama.com/pricing.html"> 12 <link rel="icon" href="/favicon.ico"> 13 <link rel="stylesheet" href="/styles.css"> 14
14<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
14 15
15<script>Paddle.Initialize({ token: "live_80240f1cc8e9e00e6819671de20" });</script>
15 16 <style> 17 .pricing-hero { 18 text-align: center; 19 margin-bottom: 2rem; 20 } 21 .pricing-hero h1 { 22 font-size: 2rem; 23 color: var(--accent-dark); 24 margin-bottom: 0.5rem; 25 } 26 .pricing-hero p { 27 color: var(--text-muted); 28 font-size: 1.05rem; 29 } 30 31 /* ---------- Billing toggle ---------- */ 32 .billing-toggle { 33 display: flex; 34 align-items: center; 35 justify-content: center; 36 gap: 0.75rem; 37 margin-bottom: 2rem; 38 } 39 .billing-toggle__label { 40 font-size: 0.9rem; 41 color: var(--text-muted); 42 cursor: pointer; 43 } 44 .billing-toggle__label--active { 45 color: var(--text-main); 46 font-weight: 600; 47 } 48 .toggle-switch { 49 position: relative; 50 width: 44px; 51 height: 24px; 52 flex-shrink: 0; 53 } 54 .toggle-switch input { 55 opacity: 0; 56 width: 0; 57 height: 0; 58 position: absolute; 59 } 60 .toggle-track { 61 position: absolute; 62 inset: 0; 63 background: var(--accent); 64 border-radius: 99px; 65 cursor: pointer; 66 transition: background 0.2s; 67 } 68 .toggle-track::after { 69 content: ""; 70 position: absolute; 71 top: 3px; 72 left: 3px; 73 width: 18px; 74 height: 18px; 75 background: #fff; 76 border-radius: 50%; 77 transition: transform 0.2s; 78 } 79 .toggle-switch input:checked + .toggle-track { 80 background: var(--accent); 81 } 82 .toggle-switch input:not(:checked) + .toggle-track { 83 background: var(--beige-darker); 84 } 85 .toggle-switch input:checked + .toggle-track::after { 86 transform: translateX(20px); 87 } 88 .billing-save-pill { 89 background: var(--ok-bg); 90 color: var(--ok-green); 91 font-size: 0.75rem; 92 font-weight: 700; 93 padding: 0.15rem 0.55rem; 94 border-radius: 99px; 95 white-space: nowrap; 96 } 97 98 /* ---------- Cards ---------- */ 99 .pricing-grid { 100 display: grid; 101 grid-template-columns: 1fr 1fr 1fr; 102 gap: 1.25rem; 103 max-width: 1040px; 104 margin: 0 auto 2rem; 105 } 106 @media (max-width: 820px) { 107 .pricing-grid { grid-template-columns: 1fr; max-width: 420px; } 108 } 109 .pricing-card { 110 background: var(--card-bg); 111 border: 1.5px solid var(--border); 112 border-radius: 12px; 113 padding: 2rem 1.5rem; 114 box-shadow: 0 2px 8px var(--shadow); 115 display: flex; 116 flex-direction: column; 117 } 118 .pricing-card--featured { 119 border-color: var(--accent); 120 position: relative; 121 } 122 /* Free tier: visually lighter than the paid cards */ 123 .pricing-card--free { 124 background: transparent; 125 border-style: dashed; 126 box-shadow: none; 127 } 128 .pricing-card--free .pricing-exercises__num, 129 .pricing-card--free .pricing-tier { 130 color: var(--text-muted); 131 } 132 /* Checker tier: a paid plan, but visually lighter than Starter/Learner */ 133 .pricing-card--checker { 134 background: transparent; 135 box-shadow: none; 136 } 137.pricing-badge { 138 position: absolute; 139 top: -0.75rem; 140 left: 50%; 141 transform: translateX(-50%); 142 background: var(--accent); 143 color: #fff; 144 font-size: 0.72rem; 145 font-weight: 700; 146 letter-spacing: 0.05em; 147 text-transform: uppercase; 148 padding: 0.2rem 0.75rem; 149 border-radius: 99px; 150 white-space: nowrap; 151 } 152 .pricing-tier { 153 font-size: 1.2rem; 154 font-weight: 700; 155 color: var(--accent-dark); 156 margin-bottom: 0.75rem; 157 } 158 159 /* exercises highlight */ 160 .pricing-exercises { 161 display: flex; 162 flex-direction: column; 163 align-items: flex-start; 164 gap: 0.3rem; 165 margin-bottom: 0.4rem; 166 min-height: 6.5rem; 167 } 168 .pricing-exercises__row { 169 display: flex; 170 align-items: baseline; 171 gap: 0.35rem; 172 } 173 .pricing-exercises__num { 174 font-size: 3.5rem; 175 font-weight: 800; 176 line-height: 1; 177 color: var(--accent-dark); 178 } 179 .pricing-exercises__label { 180 font-size: 0.9rem; 181 color: var(--text-muted); 182 line-height: 1.3; 183 } 184 .pricing-exercises__subline { 185 font-size: 0.82rem; 186 color: var(--text-muted); 187 line-height: 1.4; 188 } 189 190 /* chars/day line below the hero */ 191 .pricing-chars { 192 font-size: 0.82rem; 193 color: var(--text-muted); 194 margin-bottom: 1rem; 195 padding-bottom: 1rem; 196 border-bottom: 1.5px solid var(--border); 197 } 198 .pricing-chars strong { 199 font-size: 1.35rem; 200 font-weight: 800; 201 color: var(--text-main); 202 vertical-align: baseline; 203 } 204 205 .pricing-price-block { 206 margin-bottom: 0.2rem; 207 } 208 .pricing-price { 209 display: flex; 210 align-items: flex-start; 211 line-height: 1; 212 color: var(--text-main); 213 } 214 .pricing-price__currency { 215 font-size: 2rem; 216 font-weight: 700; 217 margin-top: 0.3rem; 218 color: var(--text-main); 219 } 220 .pricing-price__amount { 221 font-size: 3rem; 222 font-weight: 800; 223 color: var(--text-main); 224 } 225 .pricing-price__period { 226 font-size: 1rem; 227 font-weight: 400; 228 color: var(--text-muted); 229 align-self: flex-end; 230 margin-bottom: 0.3rem; 231 } 232 .pricing-was { 233 font-size: 1.3rem; 234 color: var(--error-red); 235 text-decoration: line-through; 236 margin-left: 0.25rem; 237 } 238 .pricing-billing { 239 font-size: 0.82rem; 240 color: var(--text-muted); 241 margin-bottom: 1.25rem; 242 min-height: 1.1rem; 243 } 244 .pricing-features { 245 list-style: none; 246 padding: 0; 247 margin: 0 0 1.5rem; 248 flex: 1; 249 } 250 .pricing-features li { 251 padding: 0.4rem 0; 252 font-size: 0.95rem; 253 color: var(--text-main); 254 display: flex; 255 align-items: flex-start; 256 gap: 0.5rem; 257 } 258 .pricing-features li::before { 259 content: "â"; 260 color: var(--accent); 261 font-weight: 700; 262 flex-shrink: 0; 263 } 264 .pricing-features li.pricing-feature--no { 265 color: var(--text-muted); 266 } 267 .pricing-features li.pricing-feature--no::before { 268 content: "â"; 269 color: var(--text-muted); 270 } 271 .pricing-features li strong { 272 color: var(--accent-dark); 273 font-weight: 700; 274 } 275 .pricing-cta { 276 display: block; 277 text-align: center; 278 padding: 0.75rem 1rem; 279 border-radius: 8px; 280 font-weight: 600; 281 font-size: 0.95rem; 282 cursor: pointer; 283 text-decoration: none; 284 transition: background 0.15s; 285 background: var(--btn-bg); 286 color: #fff; 287 border: none; 288 } 289 .pricing-cta:hover { 290 background: var(--btn-hover); 291 color: #fff; 292 } 293 .pricing-cta--outline { 294 background: transparent; 295 color: var(--accent-dark); 296 border: 1.5px solid var(--border); 297 } 298 .pricing-cta--outline:hover { 299 background: var(--beige-darker); 300 color: var(--accent-dark); 301 } 302 /* Free price: word ("Free") rendered in the large amount style */ 303 .pricing-price__free { 304 font-size: 2.4rem; 305 font-weight: 800; 306 color: var(--text-muted); 307 line-height: 1; 308 } 309 .pricing-note { 310 text-align: center; 311 color: var(--text-muted); 312 font-size: 0.85rem; 313 margin-top: 0.5rem; 314 } 315 316 /* checker hero â stacked feature list */ 317 .pricing-checker-hero { 318 list-style: none; 319 padding: 0; 320 margin: 0; 321 } 322 .pricing-checker-hero li { 323 font-size: 1rem; 324 color: var(--text-muted); 325 padding: 0.15rem 0; 326 display: flex; 327 align-items: center; 328 gap: 0.45rem; 329 } 330 .pricing-checker-hero li::before { 331 content: "â"; 332 color: var(--accent); 333 font-weight: 700; 334 flex-shrink: 0; 335 } 336 337 /* chars/day tooltip */ 338 .feat-chars { 339 cursor: help; 340 border-bottom: 1px dotted var(--text-muted); 341 position: relative; 342 white-space: nowrap; 343 } 344 .feat-chars::after { 345 content: " â"; 346 font-size: 0.72em; 347 color: var(--text-muted); 348 } 349 .feat-chars[data-tip]:hover::before { 350 content: attr(data-tip); 351 position: absolute; 352 bottom: calc(100% + 8px); 353 left: 50%; 354 transform: translateX(-50%); 355 background: #333; 356 color: #fff; 357 font-size: 0.78rem; 358 font-weight: 400; 359 padding: 0.45rem 0.65rem; 360 border-radius: 6px; 361 white-space: normal; 362 width: 230px; 363 z-index: 20; 364 pointer-events: none; 365 line-height: 1.4; 366 } 367 .feat-chars[data-tip]:hover::after { 368 color: var(--accent); 369 } 370 </style> 371</head> 372<body> 373 <main class="content-page"> 374 <div class="content-container"> 375 <div class="blog-post-nav"> 376 <a href="/" class="back-to-blog">â GrammarMama</a> 377 </div> 378 379 <div class="pricing-hero"> 380 <h1>Simple Pricing</h1> 381 <p>Just need a checker? Start there. Ready to practise? Upgrade anytime. Cancel any time.</p> 382 </div> 383 384 <!-- Billing toggle: monthly = unchecked (default) --> 385 <div class="billing-toggle" id="billingToggle">
386 <span class="billing-toggle__label billing-toggle__label--active" id="labelMonthly">Monthly</span> 387 <label class="toggle-switch" aria-label="Switch billing period"> 388 <input type="checkbox" id="annualToggle"> 389 <span class="toggle-track"></span> 390 </label> 391 <span class="billing-toggle__label" id="labelAnnual"> 392 Annual 393 <span class="billing-save-pill" id="savePill">Save ~17%</span> 394 </span> 395 </div> 396 397 <div class="pricing-grid"> 398 <!-- Checker --> 399 <div class="pricing-card pricing-card--checker"> 400 <div class="pricing-tier">Checker</div> 401 <div class="pricing-exercises"> 402 <ul class="pricing-checker-hero"> 403 404 <li>grammar checks</li> 405 406 <li>translations</li> 407 408 <li>text to speech</li> 409 410 </ul> 411 </div> 412 <div class="pricing-chars"> 413 <span class="feat-chars" data-tip="Characters are counted across grammar checks, text-to-speech, smart hints, and vocabulary imports.">Up to <strong>5,000</strong> characters/day</span> 414 </div> 415 <div class="pricing-price-block"> 416 <span class="pricing-price"> 417 <span class="pricing-price__currency js-currency">$</span><span class="pricing-price__amount js-price">19.99</span><span class="pricing-price__period js-period">/yr</span> 418 </span> 419 <span class="pricing-was js-was">$23.88</span> 420 </div> 421 <div class="pricing-billing js-billing">billed annually</div> 422 <ul class="pricing-features"> 423 424 <li class="">Any language</li> 425 426 <li class="pricing-feature--no">No exercises</li> 427 428 <li class="pricing-feature--no">No priority support</li> 429 430 </ul> 431 <a href="/profile/?plan=2" class="pricing-cta pricing-cta--outline">Get Started</a> 432 </div> 433 434 <!-- Starter --> 435 <div class="pricing-card"> 436 <div class="pricing-tier">Starter</div> 437 <div class="pricing-exercises"> 438 <div class="pricing-exercises__row"> 439 <span class="pricing-exercises__num">7</span> 440 <span class="pricing-exercises__label">exercises a week</span> 441 </div> 442 <div class="pricing-exercises__subline">Based on your mistakes, learning plan and goal</div> 443 </div> 444 <div class="pricing-chars"> 445 <span class="feat-chars" data-tip="Characters are counted across grammar checks, text-to-speech, smart hints, and vocabulary imports.">Up to <strong>10,000</strong> characters/day</span> 446 </div> 447 <div class="pricing-price-block"> 448 <span class="pricing-price"> 449 <span class="pricing-price__currency js-currency">$</span><span class="pricing-price__amount js-price">49.99</span><span class="pricing-price__period js-period">/yr</span> 450 </span> 451 <span class="pricing-was js-was">$59.88</span> 452 </div> 453 <div class="pricing-billing js-billing">billed annually</div> 454 <ul class="pricing-features"> 455 456 <li><strong>Perfect for maintaining your current level</strong></li> 457 458 <li>Any language</li> 459 460 <li>
460Priority support</li> 461 462 </ul> 463 <a href="/profile/?plan=0" class="pricing-cta">Get Started</a> 464 </div> 465 466 <!-- Learner --> 467 <div class="pricing-card pricing-card--featured"> 468 <div class="pricing-badge">Most Popular</div> 469 <div class="pricing-tier">Learner</div> 470 <div class="pricing-exercises"> 471 <div class="pricing-exercises__row"> 472 <span class="pricing-exercises__num">21</span> 473 <span class="pricing-exercises__label">exercises a week</span> 474 </div> 475 <div class="pricing-exercises__subline">Based on your mistakes, learning plan and goal</div> 476 </div> 477 <div class="pricing-chars"> 478 <span class="feat-chars" data-tip="Characters are counted across grammar checks, text-to-speech, smart hints, and vocabulary imports.">Up to <strong>15,000</strong> characters/day</span> 479 </div> 480 <div class="pricing-price-block"> 481 <span class="pricing-price"> 482 <span class="pricing-price__currency js-currency">$</span><span class="pricing-price__amount js-price">79.99</span><span class="pricing-price__period js-period">/yr</span> 483 </span> 484 <span class="pricing-was js-was">$95.88</span> 485 </div> 486 <div class="pricing-billing js-billing">billed annually</div> 487 <ul class="pricing-features"> 488 489 <li><strong>Designed to improve your skills over time</strong></li> 490 491 <li>Any language</li> 492 493 <li>Priority support</li> 494 495 </ul> 496 <a href="/profile/?plan=1" class="pricing-cta">Get Started</a> 497 </div> 498 </div> 499 500 <p class="pricing-note">Grammar checking is free to try â up to 500 characters/day, no card required.</p> 501 <p class="pricing-note" style="margin-top:1rem;"> 502 <a href="/terms.html">T&C</a> · 503 <a href="/privacy.html">Privacy</a> · 504 <a href="/refund.html">Refund Policy</a> 505 </p> 506 </div> 507 </main> 508 509 510 511 512
513<script> 514(function () { 515 var REDIRECTABLE = ['/', '/practice/', '/vocabulary/', '/profile/']; 516 if (REDIRECTABLE.indexOf(window.location.pathname) === -1) return; 517 // Same key/TTL as profile.njk's _TELEGRAM_INTENT_KEY â duplicated here (not shared) because 518 // this script and profile.njk's are two separate template scopes. 519 function _hasTelegramConnectIntent() { 520 try { 521 var t = parseInt(localStorage.getItem('gm_telegram_connect_intent'), 10); 522 return !!t && (Date.now() - t) < 30 * 60 * 1000; 523 } catch (e) { return false; } 524 } 525 window.addEventListener('load', function () { 526 if (!window.Clerk) return; 527 window.Clerk.load().then(function () { 528 if (!window.Clerk.user || !window.Clerk.session) return; 529 window.Clerk.session.getToken({ template: 'grammar-mama-template' }) 530 .then(function (token) { 531 if (!token) return Promise.resolve(null); 532 return fetch('/api/user', { headers: { Authorization: 'Bearer ' + token } }); 533 }) 534 .then(function (resp) { 535 if (!resp || !resp.ok) return Promise.resolve(null); 536 return resp.json(); 537 }) 538 .then(function (user) { 539 if (!user) return; 540 // Clerk's post-login redirect does not reliably land back on /profile (it can land 541 // on this page's own path instead, e.g. home) â if there's a pending Telegram-connect 542 // intent (see profile.njk), finish the trip there instead of the normal locale redirect. 543 // profile.njk itself already handles the rest once it's the page loaded. 544 if (_hasTelegramConnectIntent() && !user.telegramLinked && window.location.pathname.indexOf('/profile/') === -1) { 545 var langPrefix = user.nativeLanguage && user.nativeLanguage !== 'en' ? '/' + user.nativeLanguage : ''; 546 window.location.replace(langPrefix + '/profile/?connectTelegram=1'); 547 return; 548 } 549 if (!user.nativeLanguage || user.nativeLanguage === 'en') return; 550 window.location.replace('/' + user.nativeLanguage + window.location.pathname); 551 }) 552 .catch(function () {}); 553 }); 554 }); 555})(); 556</script>
556 557 558 <footer> 559 <a href="/">Home</a> 560 <a href="/learn/">Learn</a> 561 <a href="/blog.html">Blog</a> 562 <a href="/pricing.html">Pricing</a> 563 <a href="/terms.html">T&C</a> 564 <a href="/privacy.html">Privacy</a> 565 <a href="/refund.html">Refund Policy</a> 566 <a href="/#contact">Contact</a> 567 <a href="/#feedback">Feedback</a> 568 <a href="#" id="gmCookieSettingsLink">Cookie settings</a> 569 </footer> 570 571 572 <div class="cookie-banner" id="gmCookieBanner" role="dialog" aria-live="polite" aria-label="Cookie settings" hidden> 573 <div class="cookie-banner__box"> 574 <p>We use optional analytics cookies to see how visitors use GrammarMama, so we can keep improving it. Essential cookies (like keeping you signed in) are always on. <a href="/privacy.html">Privacy policy</a></p> 575 <div class="cookie-banner__actions"> 576 <button type="button" class="cookie-banner__reject" id="gmCookieReject">Decline</button> 577 <button type="button" class="cookie-banner__accept" id="gmCookieAccept">Accept</button> 578 </div> 579 </div> 580 </div> 581
581<script> 582 (function () { 583 var KEY = 'gm_cookie_consent'; 584 var banner = document.getElementById('gmCookieBanner'); 585 var acceptBtn = document.getElementById('gmCookieAccept'); 586 var rejectBtn = document.getElementById('gmCookieReject'); 587 var settingsLink = document.getElementById('gmCookieSettingsLink'); 588 589 function getConsent() { 590 try { return localStorage.getItem(KEY); } catch (e) { return null; } 591 } 592 function setConsent(v) { 593 try { localStorage.setItem(KEY, v); } catch (e) {} 594 } 595 596 window.GM_CONSENT = { granted: getConsent() === 'accepted', onChange: null }; 597 598 function show() { if (banner) banner.hidden = false; } 599 function hide() { if (banner) banner.hidden = true; } 600 601 function decide(value) { 602 setConsent(value); 603 window.GM_CONSENT.granted = (value === 'accepted'); 604 hide(); 605 if (typeof window.GM_CONSENT.onChange === 'function') { 606 window.GM_CONSENT.onChange(window.GM_CONSENT.granted); 607 } 608 } 609 610 if (acceptBtn) acceptBtn.addEventListener('click', function () { decide('accepted'); }); 611 if (rejectBtn) rejectBtn.addEventListener('click', function () { decide('rejected'); }); 612 if (settingsLink) { 613 settingsLink.addEventListener('click', function (e) { e.preventDefault(); show(); }); 614 } 615 616 if (getConsent() === null) show(); 617 618 // Funnel event helper. Fans out one call to both GA4 (gtag) and Clarity. 619 // No-op until an analytics script is actually loaded (i.e. consent granted), 620 // so callers everywhere can fire events unconditionally. props become GA4 621 // event params and Clarity custom tags. 622 window.GM_TRACK = function (name, props) { 623 props = props || {}; 624 try { 625 if (typeof window.gtag === 'function') window.gtag('event', name, props); 626 if (typeof window.clarity === 'function') { 627 window.clarity('event', name); 628 for (var k in props) { 629 if (Object.prototype.hasOwnProperty.call(props, k) && props[k] != null) { 630 window.clarity('set', k, String(props[k])); 631 } 632 } 633 } 634 } catch (e) {} 635 }; 636 637 // Signup-completion detector. A signup CTA sets localStorage 'gm_signup_intent' 638 // before Clerk redirects away; on the page it lands back on, wait for Clerk to 639 // report a signed-in user, then fire one 'sign_up' event and clear the flag. 640 (function () { 641 var FLAG = 'gm_signup_intent'; 642 var pending = null; 643 try { pending = localStorage.getItem(FLAG); } catch (e) {} 644 if (!pending) return; 645 var tries = 0; 646 var iv = setInterval(function () { 647 tries += 1; 648 if (window.Clerk && window.Clerk.user) { 649 clearInterval(iv); 650 try { localStorage.removeItem(FLAG); } catch (e) {} 651 if (window.GM_TRACK) window.GM_TRACK('sign_up', { source: pending }); 652 } else if (tries > 40) { 653 clearInterval(iv); 654 } 655 }, 500); 656 })(); 657 })(); 658 </script>
658 659 660 661 662
662<script> 663 (function () { 664 var GA_ID = "G-KMYM08QRY7"; 665 var CLARITY_ID = "yditf2dtk5"; 666 var _loaded = false; 667 668 function loadGa() { 669 if (!GA_ID) return; 670 var s = document.createElement('script'); 671 s.async = true; 672 s.src = 'https://www.googletagmanager.com/gtag/js?id=' + GA_ID; 673 document.head.appendChild(s); 674 window.dataLayer = window.dataLayer || []; 675 window.gtag = function () { window.dataLayer.push(arguments); }; 676 window.gtag('js', new Date()); 677 window.gtag('config', GA_ID); 678 } 679 680 function loadClarity() { 681 if (!CLARITY_ID) return; 682 (function (c, l, a, r, i, t, y) { 683 c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments); }; 684 t = l.createElement(r); t.async = 1; t.src = 'https://www.clarity.ms/tag/' + i; 685 y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y); 686 })(window, document, 'clarity', 'script', CLARITY_ID); 687 } 688 689 function setContext() { 690 var loc = document.documentElement.lang || 'en'; 691 var pt = window.GM_PAGE_TYPE || ''; 692 var authState = (window.Clerk && window.Clerk.user) ? 'signedIn' : 'anonymous'; 693 try { 694 if (typeof window.gtag === 'function') { 695 window.gtag('set', 'user_properties', { locale: loc, page_type: pt, auth_state: authState }); 696 } 697 if (typeof window.clarity === 'function') { 698 window.clarity('set', 'locale', loc); 699 if (pt) window.clarity('set', 'pageType', pt); 700 window.clarity('set', 'authState', authState); 701 } 702 } catch (e) {} 703 } 704 705 function loadAll() { 706 if (_loaded) return; 707 _loaded = true; 708 loadGa(); 709 loadClarity(); 710 setContext(); 711 // GM_PAGE_TYPE / Clerk.user are often set by scripts that run after this one. 712 window.setTimeout(setContext, 1500); 713 } 714 715 if (window.GM_CONSENT && window.GM_CONSENT.granted) loadAll(); 716 if (window.GM_CONSENT) { 717 window.GM_CONSENT.onChange = function (granted) { if (granted) loadAll(); }; 718 } 719 })(); 720 </script>
720 721 722 723 724
724<script> 725 (function () { 726 var PRICES = { 727 checkerMonthly: "pri_01ktrnkjdpa359b0k3md2aksp1", 728 checkerAnnual: "pri_01ktrnmtv4rskkz6tnympw9r15", 729 starterMonthly: "pri_01kpb0ayjv6dsc8z5ywq6mgrwg", 730 starterAnnual: "pri_01kpb0d6z45en8z5m17r3582f7", 731 learnerMonthly: "pri_01kpb0ca0k994k2zhnym2qydt0", 732 learnerAnnual: "pri_01kpb0e4knmkx3hrc4w5wbnrwn" 733 }; 734 735 // Fallback values shown immediately while Paddle loads â order matches DOM: Checker, Starter, Learner 736 var plans = [ 737 { prices: ["19.99", "1.99"], was: ["$23.88", ""], symbol: "$" }, 738 { prices: ["49.99", "4.99"], was: ["$59.88", ""], symbol: "$" }, 739 { prices: ["79.99", "7.99"], was: ["$95.88", ""], symbol: "$" } 740 ]; 741 742 var toggle = document.getElementById("annualToggle"); 743 var labelMonthly = document.getElementById("labelMonthly"); 744 var labelAnnual = document.getElementById("labelAnnual"); 745 var priceEls = document.querySelectorAll(".js-price"); 746 var periodEls = document.querySelectorAll(".js-period"); 747 var wasEls = document.querySelectorAll(".js-was"); 748 var billingEls = document.querySelectorAll(".js-billing"); 749 var currencyEls = document.querySelectorAll(".js-currency"); 750 751 function update() { 752 var annual = toggle.checked; 753 var idx = annual ? 0 : 1; 754 currencyEls.forEach(function (el, i) { el.textContent = plans[i].symbol; }); 755 priceEls.forEach(function (el, i) { el.textContent = plans[i].prices[idx]; }); 756 periodEls.forEach(function (el) { el.textContent = annual ? "/yr" : "/mo"; }); 757 wasEls.forEach(function (el, i) { el.textContent = plans[i].was[idx]; el.style.display = (annual && plans[i].was[0]) ? "" : "none"; }); 758 billingEls.forEach(function (el) { el.textContent = annual ? "billed annually" : "billed monthly"; }); 759 labelAnnual.classList.toggle("billing-toggle__label--active", annual); 760 labelMonthly.classList.toggle("billing-toggle__label--active", !annual); 761 } 762 763 toggle.addEventListener("change", update); 764 update(); 765 766 // Fetch live prices from Paddle 767 if (window.Paddle && PRICES.starterMonthly) { 768 Paddle.PricePreview({ 769 items: [ 770 { priceId: PRICES.checkerMonthly, quantity: 1 }, 771 { priceId: PRICES.checkerAnnual, quantity: 1 }, 772 { priceId: PRICES.starterMonthly, quantity: 1 }, 773 { priceId: PRICES.starterAnnual, quantity: 1 }, 774 { priceId: PRICES.learnerMonthly, quantity: 1 }, 775 { priceId: PRICES.learnerAnnual, quantity: 1 } 776 ] 777 }).then(function (result) { 778 var items = result.data.details.lineItems; 779 function fmtWas(sym, cents) { return sym + (cents / 100).toFixed(2); } 780 function c(item) { return parseInt(item.totals.total, 10); } 781 function sym(item) { return item.formattedTotals.total.replace(/[\d.,\s]/g, '').trim() || '$'; } 782 783 // items: [0]=checkerMonthly [1]=checkerAnnual [2]=starterMonthly [3]=starterAnnual [4]=learnerMonthly [5]=learnerAnnual 784 // each plan: prices=[annual, monthly], was=[annual strikethrough (monthlyÃ12), ""] 785 plans = [0, 2, 4].map(function (m) { 786 return { 787 prices: [items[m + 1].formattedTotals.total, items[m].formattedTotals.total], 788 was: [fmtWas(sym(items[m]), c(items[m]) * 12), ""], 789 symbol: "" 790 }; 791 }); 792 update(); 793 }).catch(function () { /* keep fallback values */ }); 794 } 795 })(); 796 </script>
796 797</body> 798</html> 799
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.