PageSourceSearch

https://grammarmama.com/pricing.html

html grammarmama.com collected 2026-09-24 14:44:06 UTC 30,889 bytes, 799 lines download raw bytes

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&amp;C</a> &nbsp;·&nbsp;
503                <a href="/privacy.html">Privacy</a> &nbsp;·&nbsp;
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&amp;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.