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 <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 <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 <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 & Sign In <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.