PageSourceSearch

https://explainly-ai.netlify.app/app/upgrade

html explainly-ai.netlify.app collected 2026-10-03 10:43:01 UTC 23,368 bytes, 468 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en" data-theme="dark" data-accent="indigo" data-font="humanist" data-card="elevated" data-radius="rounded">
3<head>
4  <meta charset="UTF-8" />
5<!-- This site is hosted on Netlify. Anyone can build and deploy a site
6     like this one for free: https://netlify.new/?utm_campaign=loops&utm_source=ai-legible&utm_medium=owned&utm_content=comment&utm_id=4ef755cc-f491-47e9-aa70-2db5b14bb1d0
7     Netlify hosting facts for this site: static/SSR served via Netlify Edge. -->
8  <link rel="icon" href="/icons/icon-192.png" type="image/png" />
9  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
10  <title>Upgrade — Explainly</title>
11  <link rel="preconnect" href="https://fonts.googleapis.com" />
12  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
13  <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&family=Bricolage+Grotesque:opsz,[email protected],500;12..96,600;12..96,700&family=Sora:wght@500;600;700&display=swap" rel="stylesheet" />
14  <link rel="stylesheet" href="/css/app.css" />
15  <link rel="manifest" href="/manifest.json" />
16  <meta name="theme-color" content="#06070f" />
17  <meta name="apple-mobile-web-app-capable" content="yes" />
18  <style>
19    .poll-row{display:none;align-items:center;gap:10px;padding:14px 18px;border-radius:var(--r);
20      border:1px solid color-mix(in oklab,var(--a1) 30%,transparent);
21      background:color-mix(in oklab,var(--a1) 10%,transparent);
22      font-size:13.5px;color:var(--a-glow);margin-bottom:18px;font-family:var(--mono)}
23    .poll-row.show{display:flex}
24    .poll-dot{width:9px;height:9px;border-radius:50%;background:var(--a-glow);
25      animation:pdot 1.2s ease-in-out infinite}
26    @keyframes pdot{0%,100%{opacity:.3;transform:scale(.85)}50%{opacity:1;transform:scale(1.15)}}
27    .provider-row{display:flex;gap:9px;justify-content:center;flex-wrap:wrap}
28    .provider-btn{padding:10px 22px;border-radius:999px;border:1px solid var(--line);
29      background:var(--surface);color:var(--ink-2);font-weight:600;font-size:13px;
30      cursor:pointer;transition:.2s}
31    .provider-btn:hover{border-color:var(--line-2);color:var(--ink)}
32    .provider-btn.active{color:#fff;background:linear-gradient(120deg,var(--a1),var(--a2));border-color:transparent}
33  </style>
34</head>
35<body>
36<canvas id="starfield"></canvas>
37<div class="bg-aura"></div>
38
39<div class="app-shell" id="app-shell">
40
41  <!-- Sidebar (desktop) -->
42  <aside class="sidebar">
43    <a class="side-brand" href="/">
44      <img class="brand-logo" src="/icons/logo-indigo.png" alt="" />
45      <span><span class="lite">Explain</span>ly</span>
46    </a>
47    <nav class="side-nav">
48      <a class='side-link' href='/app/library'>
49        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M4 5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16l-8-4-8 4V5Z"/></svg>
50        <span>Library</span>
51      </a>
52      <a class='side-link' href='/app/account'>
53        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
54        <span>Account</span>
55      </a>
56      <a class='side-link active' href='/app/upgrade'>
57        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="m12 2 2.39 6.96L21.6 9.2l-5.3 4.61 1.7 7.19L12 17.27 5.99 21l1.7-7.19L2.4 9.2l7.21-.24L12 2Z"/></svg>
58        <span>Upgrade</span>
59      </a>
60    </nav>
61    <div class="side-foot">
62      <button class="theme-btn" id="theme-side" aria-label="Toggle theme" style="align-self:flex-start">
63        <svg class="ic-moon" viewBox="0 0 24 24" fill="none"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>
64        <svg class="ic-sun" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="4.2" stroke="currentColor" stroke-width="1.8"/><path d="M12 2.5v2.2M12 19.3v2.2M21.5 12h-2.2M4.7 12H2.5M18.7 5.3l-1.6 1.6M6.9 17.1l
64-1.6 1.6M18.7 18.7l-1.6-1.6M6.9 6.9 5.3 5.3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
65      </button>
66      <a class='side-account' href='/app/account'>
67        <span class="avatar" id="side-avatar">?</span>
68        <span class="who"><b id="side-name">—</b><span id="side-email">—</span></span>
69      </a>
70    </div>
71  </aside>
72
73  <!-- Top bar (mobile) -->
74  <header class="topbar">
75    <a class="brand" href="/">
76      <img class="brand-logo" src="/icons/logo-indigo.png" alt="" />
77      <span><span class="lite">Explain</span>ly</span>
78    </a>
79    <span class="spacer"></span>
80    <button class="theme-btn" id="theme-top" aria-label="Toggle theme">
81      <svg class="ic-moon" viewBox="0 0 24 24" fill="none"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>
82      <svg class="ic-sun" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="4.2" stroke="currentColor" stroke-width="1.8"/><path d="M12 2.5v2.2M12 19.3v2.2M21.5 12h-2.2M4.7 12H2.5M18.7 5.3l-1.6 1.6M6.9 17.1l-1.6 1.6M18.7 18.7l-1.6-1.6M6.9 6.9 5.3 5.3" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
83    </button>
84    <a class='avatar' href='/app/account' id='top-avatar' style='width:38px;height:38px;font-size:15px'>?</a>
85  </header>
86
87  <main class="app-content">
88    <section class="page active" id="page-upgrade">
89
90      <!-- Banners -->
91      <div class="banner success" id="success-banner">
92        <h3>You're upgraded!</h3>
93        <p id="success-msg">Your plan has been updated. Enjoy your new features.</p>
94      </div>
95      <div class="banner fail" id="fail-banner">
96        <p id="fail-msg">Payment not completed. If you were charged, please wait a moment and refresh — it can take up to 60 seconds to confirm.</p>
97        <button class="btn btn-ghost btn-sm" id="dismiss-fail" style="margin-top:10px">Try Again</button>
98      </div>
99
100      <!-- Polling indicator -->
101      <div class="poll-row" id="poll-row">
102        <div class="poll-dot"></div>
103        <span>Waiting for payment confirmation…</span>
104      </div>
105
106      <!-- Hero -->
107      <div class="page-head" style="text-align:center">
108        <div class="up-hero">
109          <span class="eyebrow" style="justify-content:center">Plans &amp; pricing</span>
110          <h1 class="page-title">Unlock more of <span class="grad-text">Explainly</span></h1>
111          <p class="page-sub" style="margin-inline:auto">Higher daily limits, PDF support, cloud sync and priority processing — for the way you read.</p>
112        </div>
113        <div class="provider-row" style="margin-top:18px;margin-bottom:4px">
114          <button class="provider-btn" id="btn-razorpay">Razorpay (India)</button>
115          <button class="provider-btn" id="btn-paddle">Paddle (International)</button>
116        </div>
117        <div class="bill-toggle" id="bill-toggle">
118          <span class="thumb"></span>
119          <button data-period="monthly" class="on">Monthly</button>
120          <button data-period="annual">Annual <span class="save-pill">–37%</span></button>
121        </div>
122      </div>
123
124      <!-- Plans -->
125      <div class="plans" id="plans"></div>
126
127      <!-- Comparison table -->
128      <div class="compare" id="compare">
129        <button class="cmp-toggle" id="cmp-toggle">
130          Compare all features
131          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
132        </button>
133        <div class="cmp-wrap"><div class="cmp-inner"><div class="cmp-scroll">
134          <table class="cmp" id="cmp-table"></table>
135        </div></div></div>
136      </div>
137
138      <!-- Trust -->
139      <div class="trust">
140        <span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>30-day money-back</span>
141        <span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>Secure checkout</span>
142        <span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>Cancel anytime</span>
143      </div>
144      <p class="legal">Payments via Razorpay (India) or Paddle (International). <a href='/refund'>Refund policy</a></p>
145
146    </section>
147  </main>
148
149  <!-- Tab bar (mobile) -->
150  <nav class="tabbar">
151    <a class='tab-item' href='/app/library'>
152      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M4 5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16l-8-4-8 4V5Z"/></svg>
153      <span>Library</span>
154    </a>
155    <a class='tab-item' href='/app/account'>
156      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
157      <span>Account</span>
158    </a>
159    <a class='tab-item active' href='/app/upgrade'>
160      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path d="m12 2 2.39 6.96L21.6 9.2l-5.3 4.61 1.7 7.19L12 17.27 5.99 21l1.7-7.19L2.4 9.2l7.21-.24L12 2Z"/></svg>
161      <span>Upgrade</span>
162    </a>
163  </nav>
164</div>
165
166<div class="toast" id="toast"></div>
167
168<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
vendor: 1 bytes, line 168
168
169<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"></script>
vendor: 1 bytes, line 169
169
170<script src="/js/config.js"></script>
vendor: 1 bytes, line 170
170
171<script src="/js/auth.js"></script>
vendor: 1 bytes, line 171
171
172<script src="/js/api.js"></script>
vendor: 1 bytes, line 172
172
173<script src="/js/app-shell.js"></script>
vendor: 1 bytes, line 173
173
174<script>
175  var CHECK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>';
176
177  var _provider    = detectProvider();
178  var _period      = 'monthly';
179  var _currentTier = 'free';
180  var _pollTimer   = null;
181  var _pollCount   = 0;
182  var MAX_POLLS    = 200;
183
184  var PLANS = [
185    {
186      id: 'free', name: 'Free', tag: 'For the occasional reader', featured: false,
187      monthly:     { cur: '$', amt: '0', per: '/mo', sub: 'Free forever' },
188      annual:      { cur: '$', amt: '0', per: '/yr', sub: 'Free forever' },
189      monthly_inr: { cur: '₹', amt: '0', per: '/mo', sub: 'Free forever' },
190      annual_inr:  { cur: '₹', amt: '0', per: '/yr', sub: 'Free forever' },
191      features: ['15 explains / day', '2 Deep · 1 Why? / day', 'Local saves (50)', { off: 'PDF viewer' }, { off: 'Cloud sync' }],
192    },
193    {
194      id: 'pro', name: 'Pro', tag: 'For regular readers', featured: false,
195      monthly:     { cur: '$', amt: '4.99', per: '/mo', sub: '₹149 / month' },
196      annual:      { cur: '$', amt: '3.33', per: '/mo', sub: '$39.99 / year' },
197      monthly_inr: { cur: '₹', amt: '149',  per: '/mo', sub: '$4.99 / month' },
198      annual_inr:  { cur: '₹', amt: '120',  per: '/mo', sub: '₹1,440 / year', save: 'Save ~20%' },
199      features: ['100 explains / day', '10 Deep · 10 Why? / day', 'PDF viewer', 'Cloud saves (500)', 'Email support'],
200    },
201    {
202      id: 'premium', name: 'Premium', tag: 'For power users', featured: true, badge: 'Most popular',
203      monthly:     { cur: '$', amt: '11.99', per: '/mo', sub: '₹399 / month' },
204      annual:      { cur: '$', amt: '8.25',  per: '/mo', sub: '$99 / year', save: 'Save ~37%' },
205      monthly_inr: { cur: '₹', amt: '399',   per: '/mo', sub: '$11.99 / month' },
206      annual_inr:  { cur: '₹', amt: '250',   per: '/mo', sub: '₹2,999 / year', save: 'Save ~37%' },
207      features: ['500 explains / day', '75 Deep · 75 Why? / day', 'Everything in Pro', 'Cloud saves (2,500)', 'Priority queue', 'Premium AI models'],
208    },
209  ];
210
211  var CMP_ROWS = [
212    { label: 'Explains / day',     free: '15',   pro: '100',   premium: '500'   },
213    { label: 'Deep mode / day',    free: '2',    pro: '10',    premium: '75'    },
214    { label: 'Why? mode / day',    free: '1',    pro: '10',    premium: '75'    },
215    { label: 'Saved explanations', free: '50',   pro: '500',   premium: '2,500' },
216    { label: 'Cloud sync',         free: false,  pro: true,    premium: true    },
217    { label: 'PDF viewer',         free: false,  pro: true,    premium: true    },
218    { label: 'Priority queue',     free: false,  pro: false,   premium: true    },
219    { label: 'Premium AI models',  free: false,  pro: false,   premium: true    },
220  ];
221
222  // ── Paddle init ───────────────────────────────────────
223  Paddle.Initialize({
224    token: PADDLE_CLIENT_TOKEN,
225    eventCallback: function (data) {
226      if (data.name === 'checkout.completed') Paddle.Checkout.close();
227      if (data.name === 'checkout.closed' || data.name === 'checkout.error') {
228        clearInterval(_pollTimer);
229        document.getElementById('poll-row').classList.remove('show');
230        showFail('Payment cancelled. You have not been charged.');
231      }
232    }
233  });
234
235  // ── Deep-link auto-highlight from extension ───────────
236  (function () {
237    var params = new URLSearchParams(window.location.search);
238    var planParam     = params.get('plan');
239    var providerParam = params.get('provider');
240    if (providerParam === 'razorpay' || providerParam === 'paddle') _provider = providerParam;
241    if (planParam) {
242      setTimeout(function () {
243        var card = document.getElementById('card-' + planParam);
244        if (!card) return;
245        card.style.transition = 'box-shadow .4s';
246        card.style.boxShadow  = '0 0 0 2px var(--a1), var(--shadow)';
247        card.scrollIntoView({ behavior: 'smooth', block: 'center' });
248      }, 300);
249    }
250  })();
251
252  // ── Boot ──────────────────────────────────────────────
253  updateProviderUI();
254  renderPlans();
255  renderCmpTable();
256  positionThumb();
257
258  if (getToken()) {
259    fetchUsage().then(function (data) {
260      _currentTier = (data.tier || 'free').toLowerCase();
261      renderPlans();
262    }).catch(function () {});
263  }
264
265  // ── Provider ──────────────────────────────────────────
266  function detectProvider() {
267    try {
268      var tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
269      return (tz === 'Asia/Kolkata' || tz === 'Asia/Calcutta') ? 'razorpay' : 'paddle';
270    } catch (e) { return 'paddle'; }
271  }
272
273  function updateProviderUI() {
274    document.getElementById('btn-razorpay').classList.toggle('active', _provider === 'razorpay');
275    document.getElementById('btn-paddle').classList.toggle('active',   _provider === 'paddle');
276  }
277
278  document.getElementById('btn-razorpay').addEventListener('click', function () { _provider = 'razorpay'; updateProviderUI(); renderPlans(); });
279  document.getElementById('btn-paddle').addEventListener('click',   function () { _provider = 'paddle';   updateProviderUI(); renderPlans(); });
280
281  // ── Billing toggle ────────────────────────────────────
282  document.getElementById('bill-toggle').addEventListener('click', function (e) {
283    var b = e.target.closest('button'); if (!b) return;
284    document.querySelectorAll('#bill-toggle button').forEach(function (x) { x.classList.remove('on'); });
285    b.classList.add('on');
286    _period = b.dataset.period;
287    positionThumb();
288    renderPlans();
289  });
290
291  function positionThumb() {
292    requestAnimationFrame(function () {
293      var on    = document.querySelector('#bill-toggle button.on');
294      var thumb = document.querySelector('#bill-toggle .thumb');
295      if (on && thumb) { thumb.style.left = on.offsetLeft + 'px'; thumb.style.width = on.offsetWidth + 'px'; }
296    });
297  }
298
299  // ── Plans ─────────────────────────────────────────────
300  function renderPlans() {
301    document.getElementById('plans').innerHTML = PLANS.map(function (pl) {
302      var prKey = _period + (_provider === 'razorpay' ? '_inr' : '');
303      var pr    = pl[prKey] || pl[_period];
304      var isCurrent = pl.id === _currentTier;
305      var badge = '';
306      if (isCurrent)                              badge = '<div class="plan-badge current">Current plan</div>';
307      else if (pl.featured)                       badge = '<div class="plan-badge">' + pl.badge + '</div>';
308      else if (_period === 'annual' && pr.save)   badge = '<div class="plan-badge save">' + pr.save + '</div>';
309
310      var feats = pl.features.map(function (f) {
311        return typeof f === 'object'
312          ? '<li class="off"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>' + f.off + '</li>'
313          : '<li>' + CHECK + f + '</li>';
314      }).join('');
315
316      var labels = { free: 'Current plan', pro: 'Get Pro', premium: 'Get Premium' };
317      var cta = isCurrent
318        ? '<button class="btn btn-ghost btn-full" disabled>Current plan</button>'
319        : '<button class="btn ' + (pl.featured ? 'btn-primary' : 'btn-ghost') + ' btn-full" data-buy="' + pl.id + '">' + (labels[pl.id] || 'Upgrade') + '</button>';
320
321      return '<div class="card plan ' + (pl.featured ? 'featured' : '') + ' ' + (isCurrent ? 'current-plan' : '') + '" id="card-' + pl.id + '">' +
322        badge +
323        '<div class="plan-name">' + pl.name + '</div>' +
324        '<div class="plan-tag">' + pl.tag + '</div>' +
325        '<div class="plan-price"><span class="cur">' + pr.cur + '</span><span class="amt">' + pr.amt + '</span><span class="per">' + pr.per + '</span></div>' +
326        '<div class="plan-usd">' + pr.sub + '</div>' +
327        '<ul>' + feats + '</ul>' +
328        '<div class="plan-spacer"></div>' +
329        cta +
330      '</div>';
331    }).join('');
332
333    document.querySelectorAll('#plans [data-buy]').forEach(function (btn) {
334      btn.addEventListener('click', function () { handleBuy(btn.dataset.buy, btn); });
335    });
336  }
337
338  // ── Buy handler ───────────────────────────────────────
339  function handleBuy(planKey, btn) {
340    if (!getToken()) {
341      window.location.href = '/app/login.html?next=/app/upgrade.html';
342      return;
343    }
344    var targetTier = planKey === 'premium_yearly' ? 'premium' : planKey;
345    if (TIER_ORDER[_currentTier] >= TIER_ORDER[targetTier]) return;
346
347    btn.disabled = true;
348    btn.textContent = 'Opening…';
349
350    if (_provider === 'paddle') {
351      var paddleKey = (_period === 'annual' && planKey === 'premium') ? 'premium_yearly' : planKey;
352      Paddle.Checkout.open({
353        items: [{ priceId: PADDLE_PRICE_IDS[paddleKey], quantity: 1 }],
354        customer: { email: getEmail() },
355        customData: { user_email: getEmail() },
356        settings: { displayMode: 'overlay' }
357      });
358      btn.disabled = false;
359      btn.textContent = getBuyLabel(planKey);
360      startPolling(targetTier);
361      return;
362    }
363
364    // Razorpay
365    var rzpKey = (_period === 'annual' && planKey === 'premium') ? 'premium_yearly' : planKey;
366    var rzpUrl = PAYMENT_LINKS.razorpay[rzpKey];
367    if (!rzpUrl) {
368      showToast('Payment link not available yet.');
369      btn.disabled = false;
370      btn.textContent = getBuyLabel(planKey);
371      return;
372    }
373    var email = getEmail();
374    if (email) rzpUrl += '?prefill[email]=' + encodeURIComponent(email);
375    window.open(rzpUrl, '_blank');
376    btn.disabled = false;
377    btn.textContent = getBuyLabel(planKey);
378    startPolling(targetTier);
379  }
380
381  function getBuyLabel(key) {
382    return { free: 'Current plan', pro: 'Get Pro', premium: 'Get Premium' }[key] || 'Upgrade';
383  }
384
385  // ── Polling ───────────────────────────────────────────
386  function startPolling(targetTier) {
387    clearInterval(_pollTimer);
388    _pollCount = 0;
389    document.getElementById('poll-row').classList.add('show');
390
391    _pollTimer = setInterval(function () {
392      _pollCount++;
393      if (_pollCount > MAX_POLLS) {
394        clearInterval(_pollTimer);
395        document.getElementById('poll-row').classList.remove('show');
396        showFail('Payment not confirmed after 10 minutes. If you completed payment, please refresh the page.');
397        return;
398      }
399      fetchUsage().then(function (data) {
400        var newTier = (data.tier || 'free').toLowerCase();
401        if (TIER_ORDER[newTier] >= TIER_ORDER[targetTier]) {
402          clearInterval(_pollTimer);
403          document.getElementById('poll-row').classList.remove('show');
404          _currentTier = newTier;
405          showSuccess(newTier);
406          renderPlans();
407        }
408      }).catch(function () {});
409    }, 3000);
410  }
411
412  // ── Banners ───────────────────────────────────────────
413  function showSuccess(tier) {
414    document.getElementById('success-msg').textContent =
415      'Your account has been upgraded to ' + cap(tier) + '. Enjoy your new features!';
416    document.getElementById('fail-banner').classList.remove('show');
417    var b = document.getElementById('success-banner');
418    b.classList.add('show');
419    b.scrollIntoView({ behavior: 'smooth', block: 'start' });
420    showToast('Upgraded to ' + cap(tier));
421  }
422
423  function showFail(msg) {
424    document.getElementById('fail-msg').textContent = msg || 'Payment not completed.';
425    var b = document.getElementById('fail-banner');
426    b.classList.add('show');
427    b.scrollIntoView({ behavior: 'smooth', block: 'start' });
428  }
429
430  document.getElementById('dismiss-fail').addEventListener('click', function () {
431    document.getElementById('fail-banner').classList.remove('show');
432  });
433
434  // ── Comparison table ──────────────────────────────────
435  function renderCmpTable() {
436    var YES = '<span class="yes">' + CHECK + '</span>';
437    var NO  = '<span class="no">—</span>';
438    function cell(v) {
439      if (v === true)  return YES;
440      if (v === false) return NO;
441      return String(v);
442    }
443    document.getElementById('cmp-table').innerHTML =
444      '<thead><tr>' +
445        '<th scope="col" style="text-align:left">Feature</th>' +
446        '<th scope="col">Free</th>' +
447        '<th scope="col">Pro</th>' +
448        '<th scope="col" class="colpop">Premium</th>' +
449      '</tr></thead>' +
450      '<tbody>' + CMP_ROWS.map(function (r) {
451        return '<tr>' +
452          '<th scope="row">' + r.label + '</th>' +
453          '<td>' + cell(r.free) + '</td>' +
454          '<td>' + cell(r.pro) + '</td>' +
455          '<td class="colpop">' + cell(r.premium) + '</td>' +
456        '</tr>';
457      }).join('') + '</tbody>';
458  }
459
460  document.getElementById('cmp-toggle').addEventListener('click', function () {
461    document.getElementById('compare').classList.toggle('open');
462  });
463
464  // ── Helper ────────────────────────────────────────────
465  function cap(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
466</script>
466
467</body>
468</html>

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.