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