PageSourceSearch

https://cardcompass.app/login

html cardcompass.app collected 2026-10-02 01:12:48 UTC 44,135 bytes, 912 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  
4<script>
5    // Password-reset links carry their token in the URL fragment. Lift it out and
6    // clear it before analytics loads, so it never lands in history or GA.
7    var ccResetToken = null;
8    (function () {
9      var m = location.hash.match(/^#reset=([A-Za-z0-9_-]{20,100})$/);
10      if (m) { ccResetToken = m[1]; history.replaceState(null, '', location.pathname + location.search); }
11    })();
12  </script>
12
13  <!-- Google tag (gtag.js) -->
vendor: 66 bytes, lines 13-14
13
14  <script async src="https://www.googletagmanager.com/gtag/js?id=
14G-CYNSKD3EBQ
vendor: 14 bytes, lines 14-15
14"></script>
15  
15<script>
16    window.dataLayer = window.dataLayer || [];
17    function gtag(){dataLayer.push(arguments);}
18    gtag('js', new Date());
19
20    gtag('consent', 'default', (function () { var v; try { v = localStorage.getItem('cc_consent') || (/^Europe\//.test(Intl.DateTimeFormat().resolvedOptions().timeZone) ? 'denied' : 'granted'); } catch (e) { v = 'denied'; } return { analytics_storage: v, ad_storage: v, ad_user_data: v, ad_personalization: v }; })());
21    gtag('config', 'G-CYNSKD3EBQ');
22  </script>
22
23  
23<script src="/meta-pixel.js?v=1" defer></script>
23
24  <meta charset="UTF-8">
25  <meta name="viewport" content="width=device-width, initial-scale=1.0">
26  <title>Sign In — CardCompass</title>
27  <meta name="description" content="Sign in to your CardCompass account to see your personalized credit card recommendations.">
28  <meta name="robots" content="noindex,nofollow">
29  <link rel="icon" href="images/logo-icon.svg" type="image/png">
30  <link rel="icon" href="favicon.svg" type="image/svg+xml">
31  <link rel="manifest" href="site.webmanifest">
32  <meta name="theme-color" content="#071E4A">
33  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
34  
34<script>
35  window.onGoogleLibraryLoad = function() {
36    google.accounts.id.initialize({
37      client_id: '561152908430-5md4eadoq0n9uvp20omh3j8gtll35utb.apps.googleusercontent.com',
38      callback: handleGoogleCredential,
39      use_fedcm_for_prompt: false
40    });
41    var container = document.getElementById('g-btn-container');
42    if (container) {
43      google.accounts.id.renderButton(container, {
44        theme: 'outline',
45        size: 'large',
46        // GSI clamps to [200,400]; a hardcoded 400 overflowed the ~306px card on phones
47        width: Math.max(200, Math.min(400, container.offsetWidth)),
48        text: 'continue_with',
49        shape: 'rectangular',
50        logo_alignment: 'left'
51      });
52    }
53  };
54  setTimeout(function() {
55    if (typeof google === 'undefined' || !google.accounts) {
56      var c = document.getElementById('g-btn-container');
57      if (c && !c.querySelector('iframe')) {
58        c.innerHTML = '<p style="font-size:13px;color:#DC2626;text-align:center;margin:10px 0;">Google sign-in unavailable. Please use email & password below.</p>';
59      }
60    }
61  }, 5000);
62  </script>
62
63  
63<script src="https://accounts.google.com/gsi/client" async defer></script>
63
64  
64<script>
65  // Explicit render: the migrate/forgot widgets sit in hidden views, so they render when their view opens
66  // (or here on script load, if that view opened first — e.g. arriving from an expired reset link).
67  var ccTs = { login: null, migrate: null, forgot: null };
68  function ccRenderTurnstile(key) {
69    if (ccTs[key] !== null || !window.turnstile) return;
70    ccTs[key] = window.turnstile.render('#ts-' + key, { sitekey: '0x4AAAAAADiVOynXw_ZhYDP0' });
71  }
72  function ccTurnstileReady() {
73    ccRenderTurnstile('login');
74    ['migrate', 'forgot'].forEach(function (k) {
75      var view = document.getElementById(k + '-view');
76      if (view && view.style.display === 'block') ccRenderTurnstile(k);
77    });
78  }
79  function ccTurnstileToken(key) {
80    try { return ccTs[key] !== null ? window.turnstile.getResponse(ccTs[key]) : ''; } catch (e) { return ''; }
81  }
82  function ccTurnstileReset(key) {
83    try { if (ccTs[key] !== null) window.turnstile.reset(ccTs[key]); } catch (e) {}
84  }
85  </script>
85
86  
86<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=ccTurnstileReady" async defer></script>
86
87  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer">
88  <style>
89    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
90    :root {
91      --primary: #0B2D6B; --primary-dark: #071E4A; --primary-light: #1A4D9E;
92      --accent: #1E7FD4; --accent-light: #4BA3F0; --accent-gold: #F5A623;
93      --text-dark: #1A1A2E; --text-gray: #4A5568; --text-light: #718096;
94      --bg-light: #F7F9FC; --border: #E2E8F0;
95      --success: #059669; --error: #DC2626;
96      --r-sm: 8px; --r-md: 12px;
97    }
98
99    body { font-family: 'Inter', sans-serif; color: var(--text-dark); min-height: 100vh; display: flex; }
100
101    /* ─── SKIP LINK ─── */
102    .skip-link { position: absolute; top: -100%; left: 16px; background: var(--accent); color: #fff; padding: 10px 18px; border-radius: 0 0 var(--r-sm) var(--r-sm); font-size: 14px; font-weight: 700; text-decoration: none; z-index: 9999; }
103    .skip-link:focus { top: 0; }
104
105    /* ═══════ LEFT BRAND PANEL ═══════ */
106    .auth-left {
107      width: 50%;
108      background: var(--primary-dark);
109      display: flex;
110      flex-direction: column;
111      padding: 48px 56px;
112      position: relative;
113      overflow: hidden;
114    }
115    /* Decorative glows */
116    .auth-left::before {
117      content: '';
118      position: absolute;
119      top: -100px; right: -120px;
120      width: 500px; height: 500px;
121      background: radial-gradient(circle, rgba(30,127,212,.18) 0%, transparent 65%);
122      border-radius: 50%;
123    }
124    .auth-left::after {
125      content: '';
126      position: absolute;
127      bottom: -140px; left: -100px;
128      width: 480px; height: 480px;
129      background: radial-gradient(circle, rgba(75,163,240,.1) 0%, transparent 65%);
130      border-radius: 50%;
131    }
132
133    /* Logo in left panel */
134    .auth-logo { display: flex; align-items: center; gap: 10px; text-decoration: none; position: relative; z-index: 1; }
135    .auth-logo-icon { width: 42px; height: 42px; border-radius: 50%; overflow: hidden; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
136    .auth-logo-icon img { width: 100%; height: 100%; object-fit: cover; display: block; }
137    .auth-logo-text { font-size: 21px; font-weight: 800; color: #fff; letter-spacing: -.5px; }
138    .auth-logo-text span { color: var(--accent-light); }
139
140    /* Center content */
141    .auth-left-body { flex: 1; display: flex; flex-direction: column; justify-content: center; position: relative; z-index: 1; padding: 40px 0; }
142    .auth-left-body h1 { font-size: 36px; font-weight: 800; color: #fff; letter-spacing: -1px; line-height: 1.2; margin-bottom: 16px; }
143    .auth-left-body h1 span { color: var(--accent-light); }
144    .auth-left-body > p { font-size: 16px; color: rgba(255,255,255,.6); line-height: 1.65; margin-bottom: 36px; }
145
146    .auth-benefits { list-style: none; display: flex; flex-direction: column; gap: 16px; margin-bottom: 44px; }
147    .auth-benefits li { display: flex; align-items: flex-start; gap: 12px; }
148    .auth-benefits li i { color: var(--accent-light); font-size: 16px; margin-top: 2px; flex-shrink: 0; }
149    .auth-benefits li span { font-size: 15px; color: rgba(255,255,255,.82); line-height: 1.5; }
150
151    /* Quote card */
152    .auth-quote { background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.1); border-left: 3px solid var(--accent-gold); border-radius: 0 var(--r-md) var(--r-md) 0; padding: 18px 20px; }
153    .auth-quote blockquote { font-size: 14px; color: rgba(255,255,255,.75); line-height: 1.7; font-style: italic; margin-bottom: 12px; }
154    .auth-quote-author { display: flex; align-items: center; gap: 10px; }
155    .auth-quote-avatar { width: 32px; height: 32px; border-radius: 50%; background: var(--primary-light); display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: #fff; flex-shrink: 0; }
156    .auth-quote-info .name { font-size: 13px; font-weight: 700; color: rgba(255,255,255,.9); }
157    .auth-quote-info .card { font-size: 11px; color: rgba(255,255,255,.45);
157 margin-top: 1px; }
158
159    /* Stats row */
160    .auth-stats { display: flex; gap: 28px; margin-bottom: 44px; flex-wrap: wrap; }
161    .auth-stat { }
162    .auth-stat .val { font-size: 26px; font-weight: 800; color: #fff; letter-spacing: -1px; display: block; }
163    .auth-stat .lbl { font-size: 12px; color: rgba(255,255,255,.45); margin-top: 2px; line-height: 1.4; }
164
165    /* Left footer */
166    .auth-left-foot { position: relative; z-index: 1; font-size: 12px; color: rgba(255,255,255,.25); display: flex; gap: 16px; flex-wrap: wrap; }
167    .auth-left-foot a { color: rgba(255,255,255,.25); text-decoration: none; transition: color .2s; }
168    .auth-left-foot a:hover { color: rgba(255,255,255,.6); }
169
170    /* ═══════ RIGHT FORM PANEL ═══════ */
171    .auth-right {
172      flex: 1;
173      background: #fff;
174      display: flex;
175      flex-direction: column;
176      align-items: center;
177      justify-content: center;
178      padding: 48px 40px;
179      overflow-y: auto;
180    }
181
182    .auth-form-wrap { width: 100%; max-width: 400px; }
183
184    /* Mobile logo (only shown on mobile) */
185    .mobile-logo { display: none; align-items: center; gap: 10px; text-decoration: none; margin-bottom: 36px; }
186    .mobile-logo-icon { width: 40px; height: 40px; border-radius: 50%; overflow: hidden; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
187    .mobile-logo-icon img { width: 100%; height: 100%; object-fit: cover; display: block; }
188    .mobile-logo-text { font-size: 20px; font-weight: 800; color: var(--text-dark); letter-spacing: -.5px; }
189    .mobile-logo-text span { color: var(--accent); }
190
191    .auth-form-heading { font-size: 28px; font-weight: 800; letter-spacing: -.6px; margin-bottom: 6px; }
192    .auth-form-sub { font-size: 14px; color: var(--text-gray); margin-bottom: 32px; line-height: 1.55; }
193    .auth-form-sub a { color: var(--accent); text-decoration: none; font-weight: 600; }
194    .auth-form-sub a:hover { text-decoration: underline; }
195
196    /* Form elements */
197    .form-group { margin-bottom: 18px; }
198    .form-group label { display: flex; align-items: center; justify-content: space-between; font-size: 13px; font-weight: 600; margin-bottom: 7px; color: var(--text-dark); }
199    .forgot-link { font-size: 13px; font-weight: 500; color: var(--accent); text-decoration: none; }
200    .forgot-link:hover { text-decoration: underline; }
201    .form-group input {
202      width: 100%; padding: 13px 16px; border: 1.5px solid var(--border); border-radius: var(--r-sm);
203      font-size: 15px; color: var(--text-dark); background: #fff; outline: none;
204      transition: border-color .2s, box-shadow .2s; font-family: inherit;
205    }
206    .form-group input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(30,127,212,.12); }
207    .form-group input.err { border-color: var(--error); }
208
209    .pass-wrap { position: relative; }
210    .pass-wrap input { padding-right: 50px; }
211    .pass-toggle { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; color: var(--text-light); font-size: 15px; padding: 4px; line-height: 1; transition: color .2s; }
212    .pass-toggle:hover { color: var(--text-dark); }
213
214    /* Remember me row */
215    .remember-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
216    .form-check-inline { display: flex; align-items: center; gap: 8px; cursor: pointer; }
217    .form-check-inline input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; }
218    .form-check-inline span { font-size: 14px; color: var(--text-gray); }
219
220    /* Error/success alerts */
221    .alert { display: none; align-items: flex-start; gap: 10px; padding: 14px 16px; border-radius: var(--r-sm); font-size: 14px; margin-bottom: 18px; line-height: 1.5; }
222    .alert.show { display: flex; }
223    .alert-error { background: #FEF2F2; border: 1px solid #FCA5A5; color: #991B1B; }
224    .alert-success { background: #ECFDF5; border: 1px solid #6EE7B7; color: #065F46; }
225    .alert i { flex-shrink: 0;
225 margin-top: 1px; }
226
227    /* Forgot password state */
228    .forgot-form { display: none; }
229    .forgot-form.show { display: block; }
230    .forgot-back { display: inline-flex; align-items: center; gap: 7px; background: none; border: none; color: var(--text-gray); font-size: 14px; font-weight: 500; cursor: pointer; padding: 0; margin-bottom: 20px; font-family: inherit; transition: color .2s; }
231    .forgot-back:hover { color: var(--primary); }
232
233    /* Primary button */
234    .btn-signin {
235      width: 100%; padding: 15px; background: var(--primary); color: #fff; border: none;
236      border-radius: var(--r-sm); font-size: 16px; font-weight: 700; cursor: pointer;
237      transition: all .2s; font-family: inherit; display: flex; align-items: center;
238      justify-content: center; gap: 8px;
239    }
240    .btn-signin:hover { background: var(--primary-light); transform: translateY(-1px); box-shadow: 0 6px 22px rgba(11,45,107,.28); }
241    .btn-signin:active { transform: translateY(0); }
242    .btn-signin .spinner { display: none; width: 17px; height: 17px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .7s linear infinite; }
243    .btn-signin.loading .btn-text { display: none; }
244    .btn-signin.loading .spinner { display: block; }
245    @keyframes spin { to { transform: rotate(360deg); } }
246
247    /* Divider */
248    .auth-divider { display: flex; align-items: center; gap: 14px; margin: 22px 0; }
249    .auth-divider::before, .auth-divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }
250    .auth-divider span { font-size: 13px; color: var(--text-light); font-weight: 500; white-space: nowrap; }
251
252    /* Google button */
253    .btn-google {
254      width: 100%; padding: 13px 16px; background: #fff; color: var(--text-dark);
255      border: 1.5px solid var(--border); border-radius: var(--r-sm); font-size: 15px;
256      font-weight: 600; cursor: pointer; transition: all .2s; font-family: inherit;
257      display: flex; align-items: center; justify-content: center; gap: 10px;
258    }
259    .btn-google:hover { background: var(--bg-light); border-color: #aaa; box-shadow: 0 1px 4px rgba(0,0,0,.08); }
260    .btn-google svg { flex-shrink: 0; }
261
262    /* Form footer */
263    .auth-form-footer { text-align: center; margin-top: 28px; font-size: 14px; color: var(--text-gray); }
264    .auth-form-footer a { color: var(--accent); text-decoration: none; font-weight: 600; }
265    .auth-form-footer a:hover { text-decoration: underline; }
266
267    /* Toast notification */
268    .toast { position: fixed; bottom: 28px; left: 50%; transform: translateX(-50%) translateY(80px); background: var(--text-dark); color: #fff; padding: 13px 22px; border-radius: var(--r-md); font-size: 14px; font-weight: 500; z-index: 9999; opacity: 0; transition: all .35s cubic-bezier(.4,0,.2,1); white-space: nowrap; box-shadow: 0 8px 28px rgba(0,0,0,.25); }
269    .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
270
271    /* ─── RESPONSIVE ─── */
272    @media (max-width: 900px) {
273      .auth-left { width: 45%; padding: 40px 36px; }
274      .auth-left-body h1 { font-size: 28px; }
275      .auth-stats { gap: 20px; }
276      .auth-stat .val { font-size: 22px; }
277    }
278    @media (max-width: 680px) {
279      body { flex-direction: column; }
280      .auth-left { display: none; }
281      .auth-right { flex: none; width: 100%; min-height: 100vh; align-items: center; justify-content: flex-start; padding-top: 48px; }
282      .mobile-logo { display: flex; }
283    }
284    @media (max-width: 420px) {
285      .auth-right { padding: 40px 24px; }
286      .auth-form-heading { font-size: 24px; }
287    }
288    /* iOS Safari zooms the viewport on focus for any field under 16px and never zooms back. */
289    @media (max-width: 1024px) { input, select, textarea { font-size: 16px !important; } }
290  </style>
291</head>
292<body>
293<a href="#login-form" class="skip-link">Skip to sign-in form</a>
294
295<div class="auth-left" aria-hidden="true">
296  <!-- Logo -->
297  <a href="/" class="auth-logo">
298    <div class="auth-logo-icon"><img src="images/logo-icon.svg" alt="CardCompass compass logo" width="42" height="42"></div>
299    <span class="auth-logo-text">Card<span>Compass</span></span>
300  </a>
301
302  <!-- Main content -->
303  <div class="auth-left-body">
304    <h1>Welcome back to <span>smarter</span> spending.</h1>
305    <p>Your personalized card recommendations and rewards potential are waiting for you.</p>
306
307    <!-- Stats -->
308    <div class="auth-stats">
309      <div class="auth-stat">
310        <span class="val">113</span>
311        <span class="lbl">Cards compared<br>and ranked</span>
312      </div>
313      <div class="auth-stat">
314        <span class="val">$0</span>
315        <span class="lbl">Free<br>forever</span>
316      </div>
317    </div>
318
319    <!-- Benefits -->
320    <ul class="auth-benefits">
321      <li>
322        <i class="fas fa-compass"></i>
323        <span>See your personalized card matches, ranked by how much you'd actually earn</span>
324      </li>
325      <li>
326        <i class="fas fa-chart-line"></i>
327        <span>Track your rewards earning potential across all your cards</span>
328      </li>
329      <li>
330        <i class="fas fa-bell"></i>
331        <span>Get notified when better sign-up bonuses go live for your top cards</span>
332      </li>
333      <li>
334        <i class="fas fa-shield-alt"></i>
335        <span>No credit card needed — CardCompass is free, forever</span>
336      </li>
337    </ul>
338
339    <!-- Quote -->
340    <div class="auth-quote">
341      <blockquote>"I never knew which card actually fit my spending. CardCompass ranked everything by what I'd actually earn — I switched cards and I'm up over $800 this year."</blockquote>
342      <div class="auth-quote-author">
343        <div class="auth-quote-avatar">JR</div>
344        <div class="auth-quote-info">
345          <div class="name">James R.</div>
346          <div class="card">Chase Sapphire Preferred® cardholder</div>
347        </div>
348      </div>
349    </div>
350  </div>
351
352  <!-- Left footer -->
353  <div class="auth-left-foot">
354    <span>© 2026 CardCompass, Inc.</span>
355    <a href="/privacy-policy">Privacy</a>
356    <a href="/terms">Terms</a>
357    <a href="/advertiser-disclosure">Disclosure</a>
358  </div>
359</div>
360
361<!-- ═══════════ RIGHT FORM PANEL ═══════════ -->
362<div class="auth-right">
363  <div class="auth-form-wrap" id="login-form">
364
365    <!-- Mobile-only logo -->
366    <a href="/" class="mobile-logo">
367      <div class="mobile-logo-icon"><img src="images/logo-icon.svg" alt="CardCompass compass logo" width="40" height="40"></div>
368      <span class="mobile-logo-text">Card<span>Compass</span></span>
369    </a>
370
371    <!-- ─── SIGN IN FORM ─── -->
372    <div id="login-view">
373      <h2 class="auth-form-heading">Sign in to your account</h2>
374      <p class="auth-form-sub">
375        New to CardCompass? <a href="/get-started">Create a free account →</a>
376      </p>
377
378      <div class="alert alert-error" id="login-error" role="alert">
379        <i class="fas fa-exclamation-circle"></i>
380        <span id="login-error-msg">Incorrect email or password. Please try again.</span>
381      </div>
382
383      <div class="form-group">
384        <label for="l-email">Email address</label>
385        <input type="email" id="l-email" placeholder="[email protected]" autocomplete="email">
386      </div>
387
388      <div class="form-group">
389        <label for="l-pass">
390          Password
391          <a href="#" class="forgot-link" onclick="showForgot(event)">Forgot password?</a>
392        </label>
393        <div class="pass-wrap">
394          <input type="password" id="l-pass" placeholder="Your password" autocomplete="current-password">
395          <button type="button" class="pass-toggle" onclick="togglePass('l-pass', this)" aria-label="Show password">
396            <i class="fas fa-eye"></i>
397          </button>
398        </div>
399      </div>
400
401      <div class="remember-row">
402        <label class="form-check-inline">
403          <input type="checkbox" id="l-remember">
404          <span>Remember me</span>
405        </label>
406      </div>
407
408      <div id="ts-login" style="margin:0 0 14px;"></div>
409
410      <button class="btn-signin" id="signin-btn" onclick="signIn()">
411        <div class="spinner"></div>
412        <span class="btn-text">Sign In &nbsp;<i class="fas fa-arrow-right"></i></span>
413      </button>
414
415      <div class="auth-divider"><span>or continue with</span></div>
416
417      <div id="g-btn-container" style="margin-bottom:0;min-height:44px;display:flex;justify-content:center;"></div>
418
419      <div class="auth-form-footer">
420        Don't have an account? <a href="/get-started">Get Started Free →</a>
421      </div>
422    </div>
423
424    <!-- ─── ACCOUNT MIGRATION VIEW ─── -->
425    <div id="migrate-view" style="display:none;">
426      <button class="forgot-back" onclick="hideMigrate()">
427        <i class="fas fa-arrow-left"></i> Back to Sign In
428      </button>
429      <h2 class="auth-form-heading">We've upgraded CardCompass accounts</h2>
430      <p class="auth-form-sub" style="margin-bottom:28px;">
431        We found a saved profile for <strong id="migrate-email-display"></strong> on this device. Set a new password to move it to the upgraded, more secure account system — your name, preferences, and saved cards will carry over.
432      </p>
433
434      <div class="alert alert-error" id="migrate-error" role="alert">
435        <i class="fas fa-exclamation-circle"></i>
436        <span id="migrate-error-msg"></span>
437      </div>
438
439      <div class="form-group">
440        <label for="m-pass">New password</label>
441        <div class="pass-wrap">
442          <input type="password" id="m-pass" placeholder="Create a password (8+ characters)" autocomplete="new-password">
443          <button type="button" class="pass-toggle" onclick="togglePass('m-pass', this)" aria-label="Show password">
444            <i class="fas fa-eye"></i>
445          </button>
446        </div>
447      </div>
448
449      <div id="ts-migrate" style="margin:0 0 14px;"></div>
450
451      <button class="btn-signin" id="migrate-btn" onclick="submitMigration()">
452        <div class="spinner"></div>
453        <span class="btn-text">Continue &nbsp;<i class="fas fa-arrow-right"></i></span>
454      </button>
455    </div>
456
457    <!-- ─── FORGOT PASSWORD VIEW ─── -->
458    <div id="forgot-view" style="display:none;">
459      <button class="forgot-back" onclick="hideForgot()">
460        <i class="fas fa-arrow-left"></i> Back to Sign In
461      </button>
462      <h2 class="auth-form-heading">Reset your password</h2>
463      <p class="auth-form-sub" style="margin-bottom:28px;">Enter your email and we'll send you a link to reset your password.</p>
464
465      <div class="alert alert-success" id="reset-success" role="status">
466        <i class="fas fa-check-circle"></i>
467        <span>If an account exists for that email, you'll receive a reset link shortly. Check your inbox.</span>
468      </div>
469
470      <div class="alert alert-error" id="reset-error" role="alert">
471        <i class="fas fa-exclamation-circle"></i>
472        <span id="reset-error-msg">Please enter a valid email address.</span>
473      </div>
474
475      <div class="form-group" id="reset-email-group">
476        <label for="r-email">Email address</label>
477        <input type="email" id="r-email" placeholder="[email protected]" autocomplete="email">
478      </div>
479
480      <div id="ts-forgot" style="margin:0 0 14px;"></div>
481
482      <button class="btn-signin" id="reset-btn" onclick="sendReset()">
483        <div class="spinner"></div>
484        <span class="btn-text">Send Reset Link &nbsp;<i class="fas fa-paper-plane"></i></span>
485      </button>
486    </div>
487
488    <!-- ─── SET NEW PASSWORD VIEW (from the emailed reset link) ─── -->
489    <div id="newpass-view" style="display:none;">
490      <h2 class="auth-form-heading">Choose a new password</h2>
491      <p class="auth-form-sub" style="margin-bottom:28px;">
491This signs you out everywhere else and signs you in here.</p>
492
493      <div class="alert alert-error" id="newpass-error" role="alert">
494        <i class="fas fa-exclamation-circle"></i>
495        <span id="newpass-error-msg"></span>
496      </div>
497
498      <div class="form-group">
499        <label for="np-pass">New password</label>
500        <div class="pass-wrap">
501          <input type="password" id="np-pass" placeholder="At least 8 characters" autocomplete="new-password">
502          <button type="button" class="pass-toggle" onclick="togglePass('np-pass', this)" aria-label="Show password">
503            <i class="fas fa-eye"></i>
504          </button>
505        </div>
506      </div>
507
508      <div class="form-group">
509        <label for="np-confirm">Confirm new password</label>
510        <div class="pass-wrap">
511          <input type="password" id="np-confirm" placeholder="Repeat new password" autocomplete="new-password">
512          <button type="button" class="pass-toggle" onclick="togglePass('np-confirm', this)" aria-label="Show password">
513            <i class="fas fa-eye"></i>
514          </button>
515        </div>
516      </div>
517
518      <button class="btn-signin" id="newpass-btn" onclick="submitNewPassword()">
519        <div class="spinner"></div>
520        <span class="btn-text">Set Password &amp; Sign In &nbsp;<i class="fas fa-arrow-right"></i></span>
521      </button>
522    </div>
523
524  </div><!-- /.auth-form-wrap -->
525</div><!-- /.auth-right -->
526
527<!-- Toast notification -->
528<div class="toast" id="toast"></div>
529
530<script>
531/* ═══════════════════════════════════════════
532   SIGN IN
533═══════════════════════════════════════════ */
534let pendingMigrationEmail = null;
535
536async function signIn() {
537  const email = document.getElementById('l-email').value.trim();
538  const pass  = document.getElementById('l-pass').value;
539  const btn   = document.getElementById('signin-btn');
540  const errEl = document.getElementById('login-error');
541  const remember = document.getElementById('l-remember').checked;
542
543  errEl.classList.remove('show');
544  document.getElementById('l-email').classList.remove('err');
545  document.getElementById('l-pass').classList.remove('err');
546
547  // Basic validation
548  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
549    document.getElementById('login-error-msg').textContent = 'Please enter a valid email address.';
550    errEl.classList.add('show');
551    document.getElementById('l-email').classList.add('err');
552    document.getElementById('l-email').focus();
553    return;
554  }
555  if (pass.length < 8) {
556    document.getElementById('login-error-msg').textContent = 'Password must be at least 8 characters.';
557    errEl.classList.add('show');
558    document.getElementById('l-pass').classList.add('err');
559    document.getElementById('l-pass').focus();
560    return;
561  }
562
563  const cfToken = ccTurnstileToken('login');
564  if (!cfToken) {
565    document.getElementById('login-error-msg').textContent = ccTs.login === null
566      ? 'Our security check could not load. Allow challenges.cloudflare.com (ad blockers often block it) and reload, or use Google sign-in below.'
567      : 'Please complete the verification check above the Sign In button.';
568    errEl.classList.add('show');
569    return;
570  }
571
572  // Loading state
573  btn.classList.add('loading');
574  btn.disabled = true;
575
576  try {
577    const res = await fetch('/api/auth/login', {
578      method: 'POST',
579      headers: { 'Content-Type': 'application/json' },
580      body: JSON.stringify({ email, password: pass, cfToken })
581    });
582    const data = await res.json().catch(() => ({}));
583    // Turnstile tokens are single-use; any response other than a redirect needs a fresh one.
584    if (!res.ok) ccTurnstileReset('login');
585
586    if (res.ok) {
587      localStorage.setItem('cc_token', data.token);
588      localStorage.setItem('cc_user', JSON.stringify(data.user));
589      try { sessionStorage.setItem('cc_token', data.token); } catch(e) {}
590      if (remember) { try { localStorage.setItem('cc_remember_email', email); } catch(e) {} }
591      else { try { localStorage.removeItem('cc_remember_email'); } catch(e) {} }
592
593      sessionStorage.setItem('cc_welcome', `Welcome back, ${data.user.fname || 'there'}!`);
594      window.location.href = '/';
595      return;
596    }
597
598    if (res.status === 401) {
599      // The server no longer says whether an email is registered. A legacy
600      // localStorage-only profile (no server `id`) for this email on this device
601      // is the signal to offer migration; migrate.js 409s if the account exists.
602      let localUser = null;
603      try { localUser = JSON.parse(localStorage.getItem('cc_user') || 'null'); } catch(e) {}
604      if (localUser && !localUser.id && localUser.email && localUser.email.toLowerCase() === email.toLowerCase()) {
605        pendingMigrationEmail = email;
606        showMigrate(localUser);
607        return;
608      }
609      document.getElementById('login-error-msg').textContent = 'Incorrect email or password. If you signed up with Google, use the Google button below.';
610      errEl.classList.add('show');
611      return;
612    }
613
614    document.getElementById('login-error-msg').textContent = data.error || 'Incorrect email or password. Please try again.';
615    errEl.classList.add('show');
616  } catch (e) {
617    ccTurnstileReset('login');
618    document.getElementById('login-error-msg').textContent = 'Network error — please check your connection and try again.';
619    errEl.classList.add('show');
620  } finally {
621    btn.classList.remove('loading');
622    btn.disabled = false;
623  }
624}
625
626/* ═══════════════════════════════════════════
627   GOOGLE SIGN IN
628═══════════════════════════════════════════ */
629async function handleGoogleCredential(response) {
630  try {
631    const res = await fetch('/api/auth/google', {
632      method: 'POST',
633      headers: { 'Content-Type': 'application/json' },
634      body: JSON.stringify({ credential: response.credential })
635    });
636    const data = await res.json().catch(() => ({}));
637    if (!res.ok) throw new Error(data.error || 'Google sign-in failed');
638
639    localStorage.setItem('cc_token', data.token);
640    localStorage.setItem('cc_user', JSON.stringify(data.user));
641    try { sessionStorage.setItem('cc_token', data.token); } catch(e) {}
642    sessionStorage.setItem('cc_welcome', data.passwordCleared
643      ? `Welcome, ${data.user.fname}! Your account now signs in with Google. Set a password in My Account → Settings if you want one too.`
644      : `Welcome, ${data.user.fname}!`);
645    const go = () => { if (!go.done) { go.done = true; window.location.href = '/'; } };
646    if (data.created && typeof gtag === 'function') {
647      gtag('event', 'sign_up', { method: 'google' });
648      gtag('event', 'conversion', { send_to: 'AW-18399837249/Lk0eCM-rxOQcEMH43MVE', event_callback: go });
649      setTimeout(go, 1500);   // event_callback never fires when gtag.js is blocked
650    } else go();
651  } catch(err) {
652    showToast('Google sign-in failed. Please try email & password.');
653    console.error('[CC Google]', err);
654  }
655}
656
657/* ═══════════════════════════════════════════
658   ACCOUNT MIGRATION (legacy localStorage-only users)
659═══════════════════════════════════════════ */
660function showMigrate(localUser) {
661  document.getElementById('login-view').style.display = 'none';
662  document.getElementById('migrate-view').style.display = 'block';
663  document.getElementById('migrate-email-display').textContent = localUser.email;
664  ccRenderTurnstile('migrate');
665  document.getElementById('m-pass').focus();
666}
667function hideMigrate() {
668  document.getElementById('migrate-view').style.display = 'none';
669  document.getElementById('login-view').style.display = 'block';
670  document.getElementById('migrate-error').classList.remove('show');
671  pendingMigrationEmail = null;
672}
673async function submitMigration() {
674  const pass = document.getElementById('m-pass').value;
675  const errEl = document.getElementById('migrate-error');
676  errEl.classList.remove('show');
677
678  if (!pass || pass.length < 8) {
679    document.getElementById('migrate-error-msg').textContent = 'Password must be at least 8 characters.';
680    errEl.classList.add('show');
681    return;
682  }
683
684  let localUser = null;
685  try { localUser = JSON.parse(localStorage.getItem('cc_user') || 'null'); } catch(e) {}
686  if (!localUser || !pendingMigrationEmail) { hideMigrate(); return; }
687
688  const cfToken = ccTurnstileToken('migrate');
689  if (!cfToken) {
690    document.getElementById('migrate-error-msg').textContent = ccTs.migrate === null
691      ? 'Our security check could not load. Allow challenges.cloudflare.com (ad blockers often block it) and reload.'
692      : 'Please complete the verification check above the Continue button.';
693    errEl.classList.add('show');
694    return;
695  }
696
697  const btn = document.getElementById('migrate-btn');
698  btn.classList.add('loading');
699  btn.disabled = true;
700
701  try {
702    const res = await fetch('/api/auth/migrate', {
703      method: 'POST',
704      headers: { 'Content-Type': 'application/json' },
705      body: JSON.stringify({ localUser, password: pass, cfToken })
706    });
707    const data = await res.json().catch(() => ({}));
708    if (!res.ok) {
709      ccTurnstileReset('migrate');
710      document.getElementById('migrate-error-msg').textContent = data.error || 'Could not migrate your account. Please try again.';
711      errEl.classList.add('show');
712      return;
713    }
714    localStorage.setItem('cc_token', data.token);
715    localStorage.setItem('cc_user', JSON.stringify(data.user));
716    try { sessionStorage.setItem('cc_token', data.token); } catch(e) {}
717    sessionStorage.setItem('cc_welcome', `Welcome back, ${data.user.fname || 'there'}! Your account is now upgraded.`);
718    window.location.href = '/';
719  } catch (e) {
720    ccTurnstileReset('migrate');
721    document.getElementById('migrate-error-msg').textContent = 'Network error — please check your connection and try again.';
722    errEl.classList.add('show');
723  } finally {
724    btn.classList.remove('loading');
725    btn.disabled = false;
726  }
727}
728
729
730/* ═══════════════════════════════════════════
731   FORGOT PASSWORD FLOW
732═══════════════════════════════════════════ */
733function showForgot(e) {
734  e && e.preventDefault();
735  document.getElementById('login-view').style.display = 'none';
736  document.getElementById('forgot-view').style.display = 'block';
737  ccRenderTurnstile('forgot');
738  // Pre-fill email if already entered
739  const email = document.getElementById('l-email').value.trim();
740  if (email) document.getElementById('r-email').value = email;
741  document.getElementById('r-email').focus();
742}
743
744function hideForgot() {
745  document.getElementById('forgot-view').style.display = 'none';
746  document.getElementById('login-view').style.display = 'block';
747  // Reset forgot form state
748  document.getElementById('reset-success').classList.remove('show');
749  document.getElementById('reset-error').classList.remove('show');
750  document.getElementById('reset-email-group').style.display = 'block';
751  document.getElementById('ts-forgot').style.display = 'block';
752  ccTurnstileReset('forgot');
753  const btn = document.getElementById('reset-btn');
754  btn.disabled = false;
755  btn.classList.remove('loading');
756}
757
758async function sendReset() {
759  const email = document.getElementById('r-email').value.trim();
760  const btn = document.getElementById('reset-btn');
761  document.getElementById('reset-error').classList.remove('show');
762  document.getElementById('reset-success').classList.remove('show');
763
764  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
765    document.getElementById('reset-error').classList.add('show');
766    document.getElementById('r-email').focus();
767    return;
768  }
769
770  const cfToken = ccTurnstileToken('forgot');
771  if (!cfToken) {
772    document.getElementById('reset-error-msg').textContent = ccTs.forgot === null
773      ? 'Our security check could not load. Allow challenges.cloudflare.com (ad blockers often block it) and reload.'
774      : 'Please complete the verification check above the button.';
775    document.getElementById('reset-error').classList.add('show');
776    return;
777  }
778
779  btn.classList.add('loading');
780  btn.disabled = true;
781
782  try {
783    const res = await fetch('/api/auth/request-reset', {
784      method: 'POST',
785      headers: { 'Content-Type': 'application/json' },
786      body: JSON.stringify({ email, cfToken })
787    });
788    const data = await res.json().catch(() => ({}));
789    if (!res.ok) {
790      ccTurnstileReset('forgot');
791      document.getElementById('reset-error-msg').textContent = data.error || 'Could not send the reset link. Please try again.';
792      document.getElementById('reset-error').classList.add('show');
793      btn.disabled = false;
794      return;
795    }
796    // The server answers the same whether or not the account exists.
797    document.getElementById('reset-email-group').style.display = 'none';
798    document.getElementById('ts-forgot').style.display = 'none';
799    document.getElementById('reset-success').classList.add('show');
800  } catch (e) {
801    ccTurnstileReset('forgot');
802    document.getElementById('reset-error-msg').textContent = 'Network error — please check your connection and try again.';
803    document.getElementById('reset-error').classList.add('show');
804    btn.disabled = false;
805  } finally {
806    btn.classList.remove('loading');
807  }
808}
809
810/* ═══════════════════════════════════════════
811   SET NEW PASSWORD (from emailed reset link)
812═══════════════════════════════════════════ */
813async function submitNewPassword() {
814  const pass = document.getElementById('np-pass').value;
815  const confirm = document.getElementById('np-confirm').value;
816  const errEl = document.getElementById('newpass-error');
817  const msgEl = document.getElementById('newpass-error-msg');
818  errEl.classList.remove('show');
819
820  if (pass.length < 8) { msgEl.textContent = 'Password must be at least 8 characters.'; errEl.classList.add('show'); return; }
821  if (pass !== confirm) { msgEl.textContent = 'Passwords do not match.'; errEl.classList.add('show'); return; }
822
823  const btn = document.getElementById('newpass-btn');
824  btn.classList.add('loading');
825  btn.disabled = true;
826
827  try {
828    const res = await fetch('/api/auth/reset-password', {
829      method: 'POST',
830      headers: { 'Content-Type': 'application/json' },
831      body: JSON.stringify({ token: ccResetToken, newPassword: pass })
832    });
833    const data = await res.json().catch(() => ({}));
834    if (!res.ok) {
835      if (/expired/.test(data.error || '')) {
836        // Dead link: send them straight to requesting a new one.
837        ccResetToken = null;
838        document.getElementById('newpass-view').style.display = 'none';
839        showForgot();
840        document.getElementById('reset-error-msg').textContent = 'That reset link is invali
840d or has expired. Enter your email to get a new one.';
841        document.getElementById('reset-error').classList.add('show');
842      } else {
843        msgEl.textContent = data.error || 'Could not reset your password. Please try again.';
844        errEl.classList.add('show');
845      }
846      return;
847    }
848    localStorage.setItem('cc_token', data.token);
849    localStorage.setItem('cc_user', JSON.stringify(data.user));
850    try { sessionStorage.setItem('cc_token', data.token); } catch(e) {}
851    sessionStorage.setItem('cc_welcome', `Password updated. Welcome back, ${data.user.fname || 'there'}!`);
852    window.location.href = '/';
853  } catch (e) {
854    msgEl.textContent = 'Network error — please check your connection and try again.';
855    errEl.classList.add('show');
856  } finally {
857    btn.classList.remove('loading');
858    btn.disabled = false;
859  }
860}
861
862if (ccResetToken) {
863  document.getElementById('login-view').style.display = 'none';
864  document.getElementById('newpass-view').style.display = 'block';
865}
866
867/* ═══════════════════════════════════════════
868   PASSWORD SHOW/HIDE
869═══════════════════════════════════════════ */
870function togglePass(inputId, btn) {
871  const input = document.getElementById(inputId);
872  const isPass = input.type === 'password';
873  input.type = isPass ? 'text' : 'password';
874  btn.querySelector('i').className = isPass ? 'fas fa-eye-slash' : 'fas fa-eye';
875  btn.setAttribute('aria-label', isPass ? 'Hide password' : 'Show password');
876}
877
878/* ═══════════════════════════════════════════
879   TOAST
880═══════════════════════════════════════════ */
881function showToast(msg, duration = 4000) {
882  const t = document.getElementById('toast');
883  t.textContent = msg;
884  t.classList.add('show');
885  setTimeout(() => t.classList.remove('show'), duration);
886}
887
888/* ═══════════════════════════════════════════
889   PRE-FILL remembered email on load
890═══════════════════════════════════════════ */
891(function () {
892  const saved = localStorage.getItem('cc_remember_email');
893  if (saved) {
894    document.getElementById('l-email').value = saved;
895    document.getElementById('l-remember').checked = true;
896  }
897})();
898
899/* ═══════════════════════════════════════════
900   KEYBOARD — Enter submits visible form
901═══════════════════════════════════════════ */
902document.addEventListener('keydown', e => {
903  if (e.key !== 'Enter') return;
904  if (document.getElementById('forgot-view').style.display !== 'none') sendReset();
905  else if (document.getElementById('newpass-view').style.display !== 'none') submitNewPassword();
906  else if (document.getElementById('migrate-view').style.display !== 'none') submitMigration();
907  else signIn();
908});
909</script>
vendor: 1 bytes, line 909
909
910<script>(function(){function c(){var b=a.contentDocument||(a.contentWindow&&a.contentWindow.document);if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a43fd746ac8a4e06',t:'MTc5MDkwMzU2OA=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
910<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"e79d8b0449fb411087aba9b1640e3938","r":1,"spa":2}' crossorigin="anonymous"></script>
910
911</body>
912</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.