1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <!-- Google tag (gtag.js) -->
vendor: 66 bytes, lines 4-5
4 5 <script async src="https://www.googletagmanager.com/gtag/js?id=
5G-CYNSKD3EBQ
vendor: 14 bytes, lines 5-6
5"></script> 6
6<script> 7 window.dataLayer = window.dataLayer || []; 8 function gtag(){dataLayer.push(arguments);} 9 gtag('js', new Date()); 10 11 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'; } })() }); 12 gtag('config', 'G-CYNSKD3EBQ'); 13 </script>
13 14 <meta charset="UTF-8"> 15 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 16 <title>My Account â CardCompass</title> 17 <meta name="robots" content="noindex,nofollow"> 18 <link rel="icon" href="images/logo-icon.svg" type="image/png"> 19 <link rel="icon" href="favicon.svg" type="image/svg+xml"> 20 <link rel="manifest" href="site.webmanifest"> 21 <meta name="theme-color" content="#071E4A"> 22 <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> 23 <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"> 24 <link rel="stylesheet" href="/styles.css?v=2"> 25 <style> 26 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 27 :root { 28 --primary: #0B2D6B; --primary-dark: #071E4A; --primary-light: #1A4D9E; 29 --accent: #1E7FD4; --accent-light: #4BA3F0; --accent-gold: #F5A623; 30 --text-dark: #1A1A2E; --text-gray: #4A5568; --text-light: #718096; 31 --bg-light: #F7F9FC; --border: #E2E8F0; 32 --success: #059669; --error: #DC2626; 33 --r-sm: 8px; --r-md: 12px; --r-lg: 16px; 34 } 35 body { font-family: 'Inter', sans-serif; color: var(--text-dark); background: var(--bg-light); min-height: 100vh; display: flex; flex-direction: column; } 36 37 /* âââ NAVBAR âââ */ 38 .navbar { background: var(--primary-dark); padding: 0 24px; box-shadow: 0 2px 12px rgba(0,0,0,.3); position: sticky; top: 0; z-index: 100; } 39 .nav-inner { max-width: 1280px; margin: 0 auto; display: flex; align-items: center; height: 64px; gap: 20px; } 40 .logo { display: flex; align-items: center; gap: 10px; text-decoration: none; } 41 .logo-icon { width: 40px; height: 40px; border-radius: 50%; overflow: hidden; flex-shrink: 0; display: flex; align-items: center; justify-content: center; } 42 .logo-icon img { width: 100%; height: 100%; object-fit: cover; display: block; } 43 .logo-text { font-size: 20px; font-weight: 800; color: #fff; letter-spacing: -.5px; } 44 .logo-text span { color: var(--accent-light); } 45 .nav-right { margin-left: auto; display: flex; align-items: center; gap: 10px; } 46 .nav-home { padding: 8px 16px; background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.2); color: rgba(255,255,255,.85); border-radius: var(--r-sm); font-size: 14px; font-weight: 500; text-decoration: none; transition: all .2s; } 47 .nav-home:hover { background: rgba(255,255,255,.18); color: #fff; } 48 .nav-signout { padding: 8px 16px; background: none; border: none; color: rgba(255,255,255,.55); font-size: 14px; font-weight: 500; cursor: pointer; font-family: inherit; transition: color .2s; display: flex; align-items: center; gap: 6px; } 49 .nav-signout:hover { color: #fff; } 50 51 /* âââ PROFILE HERO âââ */ 52 .profile-hero { background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 60%, var(--primary-light) 100%); padding: 40px 24px; position: relative; overflow: hidden; } 53 .profile-hero::before { content: ''; position: absolute; top: -80px; right: -80px; width: 400px; height: 400px; background: radial-gradient(circle, rgba(30,127,212,.18) 0%, transparent 65%); border-radius: 50%; } 54 .profile-hero-inner { max-width: 1100px; margin: 0 auto; display: flex; align-items: center; gap: 22px; position: relative; z-index: 1; flex-wrap: wrap; } 55 .profile-avatar { width: 72px; height: 72px; border-radius: 50%; background: var(--accent); color: #fff; font-size: 24px; font-weight: 800; display: flex; align-items: center; justify-content: center; border: 3px solid rgba(255,255,255,.25); flex-shrink: 0; letter-spacing: -1px; } 56 .profile-info { flex: 1; min-width: 0; } 57 .profile-name { font-size: 26px; font-weight: 800; color: #fff; letter-spacing: -.5px; } 58 .profile-email { font-size: 14px; color: rgba(255,255,255,.6); margin-top: 4px; } 59 .profile-since { font-size: 12px; color: rgba(255,255,255,.38); margin-top: 8px; display: flex; align-items: center; gap: 6px; } 60 .profile-stats { display: flex; gap: 28px; flex-wrap: wrap; } 61 .profile-stat { text-align: center; } 62 .profile-stat .val { font-size: 22px; font-weight: 800; color: #fff; letter-spacing: -.5px; display: block; } 63 .profile-stat .lbl { font-size: 11px; color: rgba(255,255,255,.45); margin-top: 2px; white-space: nowrap; } 64 .btn-hero-signout { padding: 10px 20px; background: rgba(255,255,255,.1); border: 1.5px solid rgba(255,255,255,.22); color: rgba(255,255,255,.8); border-radius: var(--r-sm); font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all .2s; display: flex; align-items: center; gap: 7px; flex-shrink: 0; } 65 .btn-hero-signout:hover { background: rgba(255,255,255,.18); color: #fff; border-color: rgba(255,255,255,.5); } 66 67 /* âââ MOBILE TABS âââ */ 68 .account-tabs { display: none; background: #fff; border-bottom: 1px solid var(--border); overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; } 69 .account-tabs::-webkit-scrollbar { display: none; } 70 .account-tabs-inner { display: flex; padding: 0 16px; white-space: nowrap; min-width: max-content; } 71 .tab-btn { padding: 14px 18px; background: none; border: none; border-bottom: 3px solid transparent; font-size: 14px; font-weight: 600; color: var(--text-gray); cursor: pointer; font-family: inherit; transition: all .2s; display: flex; align-items: center; gap: 7px; } 72 .tab-btn:hover { color: var(--primary); } 73 .tab-btn.active { color: var(--primary); border-bottom-color: var(--accent); } 74 75 /* âââ ACCOUNT LAYOUT âââ */ 76 .account-body { max-width: 1100px; margin: 0 auto; padding: 32px 24px 80px; display: grid; grid-template-columns: 210px 1fr; gap: 24px; align-items: start; flex: 1; } 77 78 /* âââ SIDEBAR âââ */ 79 .account-sidebar { background: #fff; border-radius: var(--r-lg); border: 1px solid var(--border); overflow: hidden; position: sticky; top: 88px; } 80 .sidebar-link { display: flex; align-items: center; gap: 12px; padding: 14px 18px; color: var(--text-gray); text-decoration: none; font-size: 14px; font-weight: 500; transition: all .15s; border-left: 3px solid transparent; cursor: pointer; background: none; border-top: none;
80 border-right: none; border-bottom: none; font-family: inherit; width: 100%; text-align: left; } 81 .sidebar-link:hover { background: var(--bg-light); color: var(--primary); border-left-color: rgba(30,127,212,.3); } 82 .sidebar-link.active { background: rgba(30,127,212,.07); color: var(--primary); border-left-color: var(--accent); font-weight: 700; } 83 .sidebar-link i { width: 16px; text-align: center; font-size: 13px; } 84 .sidebar-divider { height: 1px; background: var(--border); } 85 .sidebar-link.danger-link { color: var(--error); } 86 .sidebar-link.danger-link:hover { background: #FEF2F2; border-left-color: var(--error); } 87 88 /* âââ SECTIONS âââ */ 89 .account-section { display: none; } 90 .account-section.active { display: block; } 91 92 /* âââ SECTION CARD âââ */ 93 .sc { background: #fff; border-radius: var(--r-lg); border: 1px solid var(--border); padding: 28px; margin-bottom: 20px; } 94 .sc-hd { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; } 95 .sc-hd-left h2 { font-size: 18px; font-weight: 800; letter-spacing: -.3px; } 96 .sc-hd-left p { font-size: 13px; color: var(--text-gray); margin-top: 4px; line-height: 1.5; } 97 .btn-edit { padding: 8px 16px; background: var(--bg-light); border: 1.5px solid var(--border); color: var(--text-dark); border-radius: var(--r-sm); font-size: 13px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all .2s; display: flex; align-items: center; gap: 7px; } 98 .btn-edit:hover { border-color: var(--accent); color: var(--accent); } 99 100 /* No prefs CTA */ 101 .no-prefs { text-align: center; padding: 48px 24px; } 102 .no-prefs i { font-size: 40px; color: var(--border); display: block; margin-bottom: 16px; } 103 .no-prefs h3 { font-size: 18px; font-weight: 700; margin-bottom: 8px; } 104 .no-prefs p { font-size: 14px; color: var(--text-gray); margin-bottom: 24px; line-height: 1.6; } 105 .btn-setup { padding: 12px 24px; background: var(--primary); color: #fff; border: none; border-radius: var(--r-sm); font-size: 15px; font-weight: 700; cursor: pointer; font-family: inherit; transition: all .2s; } 106 .btn-setup:hover { background: var(--primary-light); } 107 108 /* âââ MATCH CARDS âââ */ 109 .match-card { border: 1.5px solid var(--border); border-radius: var(--r-md); padding: 18px 20px; margin-bottom: 12px; display: flex; align-items: center; gap: 16px; transition: all .2s; flex-wrap: wrap; } 110 .match-card:hover { border-color: var(--accent); box-shadow: 0 4px 16px rgba(30,127,212,.08); } 111 .match-card.top-pick { border-color: var(--accent-gold); background: #FFFEF5; } 112 .match-rank { font-size: 18px; font-weight: 800; color: var(--border); min-width: 28px; text-align: center; flex-shrink: 0; } 113 .match-card.top-pick .match-rank { color: var(--accent-gold); } 114 .match-img { width: 58px; height: 37px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; flex-shrink: 0; letter-spacing: .4px; color: #fff; } 115 .match-info { flex: 1; min-width: 140px; } 116 .match-name { font-size: 14px; font-weight: 700; line-height: 1.3; } 117 .match-for { font-size: 12px; color: var(--text-gray); margin-top: 2px; } 118 .match-badge { display: inline-flex; align-items: center; gap: 4px; background: var(--accent-gold); color: #1A1A2E; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 3px; margin-bottom: 4px; } 119 .match-relationship-badge { display: inline-flex; align-items: center; gap: 4px; background: var(--accent); color: #fff; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 3px; margin-bottom: 4px; margin-left: 6px; } 120 .field-hint { font-size: 12px; color: var(--text-light); margin-top: 4px; line-height: 1.5; } 121 .match-numbers { text-align: right; flex-shrink: 0; min-width: 110px; } 122 .match-earn-val { font-size: 22px; font-weight: 800; color: var(--primary); line-height: 1; } 123 .match-earn-lbl { font-size: 11px; color: var(--text-gray); margin-top: 2px; } 124 .match-fee { font-size: 11px; color: var(--text-light); margin-top: 4px; } 125 .match-actions { display: flex; gap: 8px; flex-shrink: 0; } 126 .match-apply { padding: 9px 16px; background: var(--primary); color: #fff; border: none; border-radius: var(--r-sm); font-size: 13px; font-weight: 700; cursor: pointer; font-family: inherit; transition: all .2s; white-space: nowrap; } 127 .match-apply:hover { background: var(--primary-light); } 128 .match-detail { padding: 9px 14px; background: var(--bg-light); border: 1.5px solid var(--border); border-radius: var(--r-sm); font-size: 13px; font-weight: 600; text-decoration: none; color: var(--text-dark); transition: all .2s; white-space: nowrap; } 129 .match-detail:hover { border-color: var(--accent); color: var(--accent); } 130 131 /* Card image classes */ 132 .ci-chase { background: linear-gradient(135deg,#1A1A2E,#2D3561); }
133 .ci-amex { background: linear-gradient(135deg,#2B7CE9,#1B5BB3); } 134 .ci-citi { background: linear-gradient(135deg,#003B70,#005EB8); } 135 .ci-cap { background: linear-gradient(135deg,#8B0000,#CC0000); } 136 .ci-disc { background: linear-gradient(135deg,#FF6200,#FF8C42); } 137 138 /* Earnings summary bar */ 139 .earn-summary { background: linear-gradient(135deg, var(--primary), var(--primary-light)); border-radius: var(--r-md); padding: 20px 24px; display: flex; align-items: center; gap: 24px; flex-wrap: wrap; margin-bottom: 20px; } 140 .earn-summary-val { font-size: 36px; font-weight: 800; color: #fff; letter-spacing: -1px; line-height: 1; } 141 .earn-summary-lbl { font-size: 13px; color: rgba(255,255,255,.65); margin-top: 4px; } 142 .earn-summary-vs { font-size: 13px; color: rgba(255,255,255,.55); flex: 1; line-height: 1.6; } 143 .earn-summary-vs strong { color: var(--accent-light); } 144 145 /* âââ PREFERENCES FORM âââ */ 146 .form-group { margin-bottom: 16px; } 147 .form-group label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 7px; } 148 .form-group select, .form-group input[type="text"], .form-group input[type="email"], .form-group input[type="password"], .form-group input[type="number"] { 149 width: 100%; padding: 12px 14px; border: 1.5px solid var(--border); border-radius: var(--r-sm); 150 font-size: 14px; color: var(--text-dark); background: #fff; outline: none; 151 transition: border-color .2s; font-family: inherit; 152 } 153 .form-group select:focus, .form-group input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(30,127,212,.1); } 154 .form-group select { appearance: none; cursor: pointer; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%234A5568' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 14px center; } 155 .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } 156 .form-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; } 157 .form-actions { display: flex; gap: 10px; margin-top: 20px; } 158 .pref-collapse-summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: var(--bg-light); border-radius: var(--r-sm); cursor: pointer; font-size: 13px; color: var(--text-gray); } 159 .pref-collapse-summary strong { color: var(--text-dark); } 160 .pref-collapse-summary i { transition: transform .2s; } 161 .pref-collapse-summary.open i { transform: rotate(180deg); } 162 .pill-check-group { display: flex; flex-wrap: wrap; gap: 8px; } 163 .pill-check { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border: 1.5px solid var(--border); border-radius: 100px; font-size: 13px; cursor: pointer; user-select: none; transition: all .15s; } 164 .pill-check input { margin: 0; } 165 .pill-check.checked { border-color: var(--accent); background: rgba(30,127,212,.08); color: var(--accent); font-weight: 600; } 166 .btn-save { padding: 11px 22px; background: var(--primary); color: #fff; border: none; border-radius: var(--r-sm); font-size: 14px; font-weight: 700; cursor: pointer; font-family: inherit; transition: all .2s; } 167 .btn-save:hover { background: var(--primary-light); } 168 .btn-cancel { padding: 11px 18px; background: var(--bg-light); border: 1.5px solid var(--border); color: var(--text-gray); border-radius: var(--r-sm); font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all .2s; } 169 .btn-cancel:hover { border-color: var(--text-gray); color: var(--text-dark); } 170 171 /* âââ SETTINGS âââ */ 172 .settings-row { display: flex; justify-content: space-between; align-items: center; padding: 16px 0; border-bottom: 1px solid var(--border); } 173 .settings-row:last-child { border-bottom: none; } 174 .settings-lbl { font-size: 13px; font-weight: 600; margin-bottom: 2px; } 175 .settings-val { font-size: 14px; color: var(--text-gray); } 176 .settings-note { font-size: 12px; color: var(--text-light); margin-top: 2px; } 177 .danger-zone { border: 1.5px solid #FCA5A5; border-radius: var(--r-lg); padding: 24px; } 178 .danger-zone h3 { font-size: 15px; font-weight: 700; color: var(--error); margin-bottom: 6px; } 179 .danger-zone p { font-size: 13px; color: var(--text-gray); margin-bottom: 16px; line-height: 1.5; } 180 .btn-danger { padding: 10px 20px; background: transparent; border: 1.5px solid var(--error); color: var(--error); border-radius: var(--r-sm); font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all .2s; } 181 .btn-danger:hover { background: #FEF2F2; } 182 183 /* Alert inline */ 184 .inline-alert { display: none; padding: 12px 16px; border-radius: var(--r-sm); font-size: 13px; margin-top: 14px; } 185 .inline-alert.show { display: flex; align-items: center; gap: 8px; } 186 .inline-alert.success { background: #ECFDF5; border: 1px solid #6EE7B7; color: #065F46; } 187 .inline-alert.error { background: #FEF2F2; border: 1px solid #FCA5A5; color: #991B1B; } 188 189 /* Pass wrap */ 190 .pass-wrap { position: relative; } 191 .pass-wrap input { padding-right: 48px; } 192 .pass-toggle { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; color: var(--text-light); font-size: 14px; padding: 4px; } 193 .pass-toggle:hover { color: var(--text-dark); } 194 195 /* âââ WALLET âââ */ 196 .wallet-search-wrap { position: relative; margin-bottom: 20px; } 197 .wallet-search-wrap input { width: 100%; padding: 12px 14px; border: 1.5px solid var(--border); border-radius: var(--r-sm); font-size: 14px; color: var(--text-dark); background: #fff; outline: none; transition: border-color .2s; font-family: inherit; } 198 .wallet-search-wrap input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(30,127,212,.1); } 199 .wallet-search-results { position: absolute; top: calc(100% + 4px); left: 0; right: 0; background: #fff; border: 1.5px solid var(--border); border-radius: var(--r-sm); box-shadow: 0 8px 24px rgba(0,0,0,.1); z-index: 50; display: none; max-height: 280px; overflow-y: auto; } 200 .wallet-search-results.open { display: block; } 201 .wallet-search-item { display: flex; align-items: center; gap: 12px; padding: 10px 14px; cursor: pointer; transition: background .15s; } 202 .wallet-search-item:hover { background: var(--bg-light); } 203 .wallet-search-thumb { width: 48px; height: 30px; border-radius: 5px; flex-shrink: 0; overflow: hidden; background: var(--bg-light); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; font-size: 8px; font-weight: 700; color: var(--text-light); } 204 .wallet-search-thumb img { width: 100%; height: 100%; object-fit: cover; } 205 .wallet-search-name { font-size: 14px; font-weight: 600; } 206 .wallet-search-sub { font-size: 12px; color: var(--text-gray);
206 margin-top: 1px; } 207 .wallet-search-add { margin-left: auto; padding: 4px 10px; background: var(--accent); color: #fff; border: none; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; flex-shrink: 0; } 208 .wallet-owned { display: flex; flex-direction: column; gap: 10px; margin-bottom: 8px; } 209 .wallet-owned:empty::after { content: 'No cards added yet. Search above to add your cards.'; display: block; padding: 16px 0; text-align: center; font-size: 14px; color: var(--text-light); } 210 .wallet-card-row { display: flex; align-items: center; gap: 14px; padding: 12px 14px; border: 1.5px solid var(--border); border-radius: var(--r-sm); } 211 .wallet-card-img { width: 64px; height: 40px; border-radius: 6px; overflow: hidden; flex-shrink: 0; background: var(--bg-light); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; font-size: 9px; font-weight: 700; color: var(--text-light); } 212 .wallet-card-img img { width: 100%; height: 100%; object-fit: cover; } 213 .wallet-card-info { flex: 1; min-width: 0; } 214 .wallet-card-name { font-size: 14px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } 215 .wallet-card-issuer { font-size: 12px; color: var(--text-gray); margin-top: 2px; } 216 .wallet-remove { padding: 6px 12px; background: none; border: 1.5px solid var(--border); color: var(--text-gray); border-radius: var(--r-sm); font-size: 12px; cursor: pointer; font-family: inherit; transition: all .15s; flex-shrink: 0; white-space: nowrap; } 217 .wallet-remove:hover { border-color: var(--error); color: var(--error); background: #FEF2F2; } 218 .wallet-divider { height: 1px; background: var(--border); margin: 24px 0; } 219 .wallet-unknown-intro { font-size: 13px; color: var(--text-gray); margin-bottom: 16px; line-height: 1.5; } 220 221 /* âââ EXPENSES âââ */ 222 .expense-row { display: flex; align-items: center; gap: 14px; padding: 12px 14px; border: 1.5px solid var(--border); border-radius: var(--r-sm); margin-bottom: 10px; } 223 .expense-row:last-child { margin-bottom: 0; } 224 .expense-main { flex: 1; min-width: 0; } 225 .expense-merchant { font-size: 14px; font-weight: 700; } 226 .expense-meta { font-size: 12px; color: var(--text-gray); margin-top: 3px; } 227 .expense-amount { font-size: 15px; font-weight: 800; white-space: nowrap; } 228 .expense-verdict { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 100px; font-size: 11px; font-weight: 700; margin-top: 6px; white-space: nowrap; } 229 .expense-verdict.optimal { background: rgba(5,150,105,.1); color: var(--success); } 230 .expense-verdict.suboptimal { background: rgba(245,166,35,.12); color: #B45309; } 231 .expense-delete { padding: 6px 10px; background: none; border: 1.5px solid var(--border); color: var(--text-gray); border-radius: var(--r-sm); font-size: 12px; cursor: pointer; font-family: inherit; flex-shrink: 0; } 232 .expense-delete:hover { border-color: var(--error); color: var(--error); background: #FEF2F2; } 233 .insight-recommend { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; padding: 14px 16px; background: var(--bg-light); border-radius: var(--r-sm); margin-bottom: 10px; } 234 .insight-recommend:last-child { margin-bottom: 0; } 235 .insight-recommend-text { font-size: 13px; color: var(--text-dark); line-height: 1.5; flex: 1; min-width: 200px; } 236 .insight-apply-btn { padding: 8px 16px; background: var(--primary); color: #fff; border: none; border-radius: var(--r-sm); font-size: 13px; font-weight: 700; cursor: pointer; font-family: inherit; white-space: nowrap; } 237 238 /* âââ TOAST âââ */ 239 .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); } 240 .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } 241 242 /* âââ FOOTER âââ */ 243 footer { background: var(--primary-dark); color: rgba(255,255,255,.35); padding: 18px 24px; text-align: center; font-size: 13px; margin-top: auto; } 244 footer a { color: rgba(255,255,255,.35); text-decoration: none; margin: 0 10px; transition: color .2s; } 245 footer a:hover { color: rgba(255,255,255,.7); } 246 247 /* âââ RESPONSIVE âââ */ 248 @media (max-width: 880px) { 249 .account-body { grid-template-columns: 1fr; } 250 .account-sidebar { display: none; } 251 .account-tabs { display: block; } 252 .profile-stats { display: none; } 253 .earn-summary-vs { display: none; } 254 } 255 @media (max-width: 600px) { 256 .profile-hero { padding: 28px 20px; } 257 .profile-name { font-size: 20px; } 258 .profile-avatar { width: 56px; height: 56px; font-size: 18px; } 259 .account-body { padding: 20px 16px 60px; } 260 .sc { padding: 20px; } 261 .form-row { grid-template-columns: 1fr; } 262 .form-row-3 { grid-template-columns: 1fr; } 263 .match-card { gap: 10px; } 264 .match-numbers { min-width: auto; } 265 .earn-summary-val { font-size: 28px; } 266 } 267 </style> 268
268<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
268 269</head> 270<body> 271 272<!-- âââââââââââ NAVBAR âââââââââââ --> 273<nav class="navbar"> 274 <div class="nav-inner"> 275 <a href="/" class="logo"> 276 <div class="logo-icon"><img src="images/logo-icon.svg" alt="CardCompass compass logo" width="40" height="40"></div> 277 <span class="logo-text">Card<span>Compass</span></span> 278 </a> 279 <div class="nav-links" id="nav-links"> 280 <div class="nav-item"> 281 <button class="nav-link" aria-haspopup="true">Credit Cards <i class="fas fa-chevron-down"></i></button> 282 <div class="dropdown"> 283 <a href="/all-cards" style="font-weight:700"><i class="fas fa-th-list"></i> All Credit Cards</a> 284 <div style="height:1px;background:#E2E8F0;margin:6px 0;"></div> 285 <a href="/bonus-offers"><i class="fas fa-gift"></i> Current Bonus Offers</a> 286 <a href="/cards?cat=cashback"><i class="fas fa-star"></i> Best Cash Back Cards</a> 287 <a href="/cards?cat=travel"><i class="fas fa-plane"></i> Best Travel Cards</a> 288 <a href="/cards?cat=hotel"><i class="fas fa-hotel"></i> Best Hotel Cards</a> 289 <a href="/cards?cat=airline"><i class="fas fa-plane-departure"></i> Best Airline Cards</a> 290 <a href="/cards?cat=gas"><i class="fas fa-gas-pump"></i> Best Gas Cards</a> 291 <a href="/cards?cat=dining"><i class="fas fa-utensils"></i> Best Dining Cards</a> 292 <a href="/cards?cat=nofee"><i class="fas fa-shield-alt"></i> No Annual Fee Cards</a> 293 <a href="/cards?cat=balancetransfer"><i class="fas fa-sync-alt"></i> Balance Transfer Cards</a> 294 <a href="/cards?cat=business"><i class="fas fa-briefcase"></i> Best Business Cards</a> 295 <a href="/cards?cat=student"><i class="fas fa-graduation-cap"></i> Student Cards</a> 296 <a href="/cards?cat=secured"><i class="fas fa-lock"></i> Build Credit / Secured</a> 297 </div> 298 </div> 299 <div class="nav-item"> 300 <button class="nav-link" aria-haspopup="true">Rewards <i class="fas fa-chevron-down"></i></button> 301 <div class="dropdown"> 302 <a href="/rewards-calculator"><i class="fas fa-calculator"></i> Points Calculator</a> 303 <a href="/rewards-partners"><i class="fas fa-exchange-alt"></i> Transfer Partners</a> 304 <a href="/rewards-maximize"><i class="fas fa-chart-line"></i> Maximize Your Points</a> 305 <a href="/rewards-award-travel"><i class="fas fa-suitcase"></i> Award Travel Guide</a> 306 <a href="/rewards-optimizer"><i class="fas fa-dollar-sign"></i> Cash Back Optimizer</a> 307 <a href="/balance-transfer-calculator"><i class="fas fa-sync-alt"></i> Balance Transfer Calc</a> 308 </div> 309 </div> 310 <div class="nav-item"> 311 <button class="nav-link" aria-haspopup="true">Airlines <i class="fas fa-chevron-down"></i></button> 312 <div class="dropdown"> 313 <a href="/airlines" style="font-weight:700"><i class="fas fa-plane"></i> All Airlines</a> 314 <div style="height:1px;background:#E2E8F0;margin:6px 0;"></div> 315 <a href="/airline-transfer-matrix"><i class="fas fa-table"></i> Transfer Partner Matrix</a> 316 <a href="/airline-search-programs"><i class="fas fa-search"></i> Search All 23 Programs</a> 317 <div style="height:1px;background:#E2E8F0;margin:6px 0;"></div> 318 <a href="/airlines/united"><i class="fas fa-plane-departure"></i> United MileagePlus</a> 319 <a href="/airlines/delta"><i class="fas fa-plane-departure"></i> Delta SkyMiles</a> 320 <a href="/airlines/american"><i class="fas fa-plane-departure"></i> American AAdvantage</a> 321 <a href="/airlines/southwest"><i class="fas fa-plane-departure"></i> Southwest Rapid Rewards</a> 322 <a href="/airlines/alaska"><i class="fas fa-plane-departure"></i> Alaska Mileage Plan</a> 323 <a href="/airlines/jetblue"><i class="fas fa-plane-departure"></i> JetBlue TrueBlue</a> 324 <a href="/airlines/hawaiian"><i class="fas fa-plane-departure"></i> Hawaiian Miles</a> 325 </div> 326 </div> 327 <a href="/quiz" class="nav-link">Card Quiz</a> 328 <a href="/compare" class="nav-link">Compare Cards</a> 329 <a href="/my-current-wallet" class="nav-link">My Current Wallet</a> 330 <a href="/guides" class="nav-link">Guides</a> 331 </div> 332 <div class="nav-right"> 333 <a href="/" class="nav-home"><i class="fas fa-home"></i> Home</a> 334 <button class="nav-signout" onclick="signOut()"><i class="fas fa-sign-out-alt"></i> Sign Out</button> 335 </div> 336 <button class="hamburger" id="hamburger" aria-label="Toggle menu" aria-expanded="false">
337 <span></span><span></span><span></span> 338 </button> 339 </div> 340</nav> 341 342<!-- âââââââââââ PROFILE HERO âââââââââââ --> 343<div class="profile-hero"> 344 <div class="profile-hero-inner"> 345 <div class="profile-avatar" id="p-avatar">?</div> 346 <div class="profile-info"> 347 <div class="profile-name" id="p-name">Loadingâ¦</div> 348 <div class="profile-email" id="p-email"></div> 349 <div class="profile-since" id="p-since"><i class="fas fa-calendar-alt"></i></div> 350 </div> 351 <div class="profile-stats" id="p-stats"></div> 352 <button class="btn-hero-signout" onclick="signOut()"> 353 <i class="fas fa-sign-out-alt"></i> Sign Out 354 </button> 355 </div> 356</div> 357 358<!-- âââââââââââ MOBILE TABS âââââââââââ --> 359<div class="account-tabs"> 360 <div class="account-tabs-inner"> 361 <button class="tab-btn active" data-section="matches" onclick="showSection('matches', this)"> 362 <i class="fas fa-compass"></i> My Matches 363 </button> 364 <button class="tab-btn" data-section="prefs" onclick="showSection('prefs', this)"> 365 <i class="fas fa-sliders-h"></i> Preferences 366 </button> 367 <button class="tab-btn" data-section="wallet" onclick="showSection('wallet', this)"> 368 <i class="fas fa-wallet"></i> My Wallet 369 </button> 370 <button class="tab-btn" data-section="expenses" onclick="showSection('expenses', this)"> 371 <i class="fas fa-receipt"></i> Expenses 372 </button> 373 <button class="tab-btn" data-section="settings" onclick="showSection('settings', this)"> 374 <i class="fas fa-user-cog"></i> Settings 375 </button> 376 </div> 377</div> 378 379<!-- âââââââââââ ACCOUNT BODY âââââââââââ --> 380<div class="account-body"> 381 382 <!-- âââ SIDEBAR âââ --> 383 <aside class="account-sidebar"> 384 <button class="sidebar-link active" data-section="matches" onclick="showSection('matches', this)"> 385 <i class="fas fa-compass"></i> My Card Matches 386 </button> 387 <button class="sidebar-link" data-section="prefs" onclick="showSection('prefs', this)"> 388 <i class="fas fa-sliders-h"></i> My Preferences 389 </button> 390 <button class="sidebar-link" data-section="wallet" onclick="showSection('wallet', this)"> 391 <i class="fas fa-wallet"></i> My Wallet 392 </button> 393 <button class="sidebar-link" data-section="expenses" onclick="showSection('expenses', this)"> 394 <i class="fas fa-receipt"></i> Expenses 395 </button> 396 <button class="sidebar-link" data-section="settings" onclick="showSection('settings', this)"> 397 <i class="fas fa-user-cog"></i> Account Settings 398 </button> 399 <div class="sidebar-divider"></div> 400 <button class="sidebar-link danger-link" onclick="signOut()"> 401 <i class="fas fa-sign-out-alt"></i> Sign Out 402 </button> 403 </aside> 404 405 <!-- âââ MAIN CONTENT âââ --> 406 <main> 407 408 <!-- ââââ SECTION: MATCHES ââââ --> 409 <section class="account-section active" id="section-matches"> 410 411 <!-- No prefs state --> 412 <div class="sc" id="no-prefs-card" style="display:none;"> 413 <div class="no-prefs"> 414 <i class="fas fa-compass"></i> 415 <h3>Complete your profile to see matches</h3> 416 <p>Tell us about your spending habits and we'll rank every card by exactly how much you'd earn â personalized to you.</p> 417 <button class="btn-setup" onclick="showSection('prefs', document.querySelector('[data-section=prefs]'))"> 418 Set My Preferences â 419 </button> 420 </div> 421 </div> 422 423 <!-- Has prefs state --> 424 <div id="has-prefs-matches"> 425 <!-- Earnings summary --> 426 <div class="earn-summary" id="earn-summary" style="display:none;"> 427 <div> 428 <div class="earn-summary-val" id="earn-top-val">â</div> 429 <div class="earn-summary-lbl">Projected yearly value with your best match</div> 430 </div> 431 <div class="earn-summary-vs" id="earn-vs"></div> 432 </div> 433 434 <!-- Match list --> 435 <div class="sc"> 436 <div class="sc-hd"> 437 <div class="sc-hd-left"> 438 <h2>Your Personalized Card Ranking</h2> 439 <p>Sorted by projected net yearly value based on your spending profile.</p> 440 </div> 441 <button class="btn-edit" onclick="showSection('prefs', document.querySelector('[data-section=prefs]'))"> 442 <i class="fas fa-sliders-h"></i> Update Preferences 443 </button> 444 </div> 445 <div id="matches-list"> 446 <div style="text-align:center;padding:40px;color:var(--text-light);"> 447 <i class="fas fa-spinner fa-spin" style="font-size:24px;margin-bottom:12px;display:block;"></i> 448 Computing your matches⦠449 </div> 450 </div> 451 </div> 452 </div> 453 454 </section> 455 456 <!-- ââââ SECTION: PREFERENCES ââââ --> 457 <section class="account-section" id="section-prefs"> 458 <!-- Your Goal --> 459 <div class="sc"> 460 <div class="sc-hd"> 461 <div class="sc-hd-left"> 462 <h2>Your Goal</h2> 463 <p>
463The single highest-signal input for your card rankings.</p> 464 </div> 465 </div> 466 <div class="form-group"> 467 <label for="pf-goal">What's your primary goal?</label> 468 <select id="pf-goal" onchange="updateTravelCollapseState()"> 469 <option value="travel">Maximize travel rewards & points</option> 470 <option value="cashback">Earn cash back on everything</option> 471 <option value="category">Save on a specific spending category</option> 472 <option value="bonus">Get the best sign-up bonus</option> 473 <option value="credit">Build or improve my credit</option> 474 <option value="business">Business spending & expense management</option> 475 </select> 476 </div> 477 </div> 478 479 <!-- Travel Habits (progressive disclosure â collapsed unless goal is travel-related) --> 480 <div class="sc"> 481 <div class="sc-hd"> 482 <div class="sc-hd-left"> 483 <h2>Travel Habits</h2> 484 <p>Airline and hotel loyalty affect which card earns you the most.</p> 485 </div> 486 </div> 487 <div class="pref-collapse-summary" id="travel-collapse-summary" onclick="toggleTravelCollapse()"> 488 <span id="travel-collapse-text">Not set â click to expand</span> 489 <i class="fas fa-chevron-down"></i> 490 </div> 491 <div id="travel-collapse-body" style="display:none;margin-top:16px;"> 492 <div class="form-row"> 493 <div class="form-group"> 494 <label for="pf-travel-freq">How often do you travel?</label> 495 <select id="pf-travel-freq" onchange="updateTravelCollapseState()"> 496 <option value="frequent">Frequent (monthly+)</option> 497 <option value="monthly">Regularly (every few months)</option> 498 <option value="occasional" selected>Occasionally (a few times a year)</option> 499 <option value="rarely">Rarely / never</option> 500 </select> 501 </div> 502 <div class="form-group"> 503 <label for="pf-hotel">Preferred hotel chain</label> 504 <select id="pf-hotel" onchange="updateTravelCollapseState()"> 505 <option value="flexible" selected>Flexible / no preference</option> 506 <option value="marriott">Marriott Bonvoy</option> 507 <option value="hilton">Hilton Honors</option> 508 <option value="hyatt">World of Hyatt</option> 509 <option value="ihg">IHG One Rewards</option> 510 <option value="notimportant">Not important to me</option> 511 </select> 512 </div> 513 </div> 514 <div class="form-group"> 515 <label>Preferred airline(s)</label> 516 <div class="pill-check-group" id="pf-airlines-group"> 517 <label class="pill-check"><input type="checkbox" value="delta" onchange="onAirlinePillChange(this)"> Delta</label> 518 <label class="pill-check"><input type="checkbox" value="united" onchange="onAirlinePillChange(this)"> United</label> 519 <label class="pill-check"><input type="checkbox" value="american" onchange="onAirlinePillChange(this)"> American</label> 520 <label class="pill-check"><input type="checkbox" value="southwest" onchange="onAirlinePillChange(this)"> Southwest</label> 521 <label class="pill-check"><input type="checkbox" value="international" onchange="onAirlinePillChange(this)"> Frequent international carrier</label> 522 <label class="pill-check"><input type="checkbox" value="none" onchange="onAirlinePillChange(this)"> No preference</label> 523 </div> 524 </div> 525 <div class="form-group" style="margin-top:14px;"> 526 <label for="pf-intl">International travel frequency</label> 527 <select id="pf-intl"> 528 <option value="rarely" selected>Rarely</option> 529 <option value="sometimes">Sometimes</option> 530 <option value="often">Often</option> 531 </select> 532 </div> 533 </div> 534 </div> 535 536 <!-- Monthly Spending --> 537 <div class="sc"> 538 <div class="sc-hd"> 539 <div class="sc-hd-left"> 540 <h2>Monthly Spending</h2> 541 <p>Per-category spend drives your projected yearly rewards value.</p> 542 </div> 543 </div> 544 <div class="form-row-3"> 545 <div class="form-group"><label for="pf-spend-travel"><i class="fas fa-plane"></i> Travel</label><input type="number" id="pf-spend-travel" min="0" placeholder="e.g. 200"></div> 546 <div class="form-group"><label for="pf-spend-dining"><i class="fas fa-utensils"></i> Dining</label><input type="number" id="pf-spend-dining" min="0" placeholder="e.g. 400"></div> 547 <div class="form-group"><label for="pf-spend-grocery"><i class="fas fa-shopping-cart"></i> Grocery</label><input type="number" id="pf-spend-grocery" min="0" placeholder="e.g. 500"></div> 548 <div class="form-group"><label for="pf-spend-gas"><i class="fas fa-gas-pump"></i> Gas</label><input type="number" id="pf-spend-gas" min="0" placeholder="e.g. 150"></div> 549 <div class="form-group"><label for="pf-spend-online"><i class="fas fa-laptop"></i> Online</label><input type="number" id="pf-spend-online" min="0" placeholder="e.g. 300"></div> 550 <div class="form-group"><label for="pf-spend-other"><i class="fas fa-receipt"></i> Everything Else</label><input type="number" id="pf-spend-other" min="0" placeholder="e.g. 300"></div> 551 </div> 552 </div> 553 554 <!-- Banking Relationships --> 555 <div class="sc"> 556 <div class="sc-hd"> 557 <div class="sc-hd-left"> 558 <h2>Banking Relationships</h2> 559 <p>Combined balances with a bank can boost your rewards on that bank's cards â tell us
559yours to see it reflected in your matches.</p> 560 </div> 561 </div> 562 <div class="form-group"> 563 <label for="pf-bofa-balance">Bank of America + Merrill combined balance</label> 564 <input type="number" id="pf-bofa-balance" min="0" step="1000" placeholder="e.g. 60000" oninput="updateBankTierHint('Bank of America')"> 565 <p class="field-hint" id="pf-bofa-tier-hint">3-month average combined balance across BofA banking + Merrill investment accounts. Used only to estimate your BofA Rewards tier â not verified or shared with Bank of America.</p> 566 </div> 567 <div class="form-group"> 568 <label for="pf-usbank-balance">U.S. Bank combined balance</label> 569 <input type="number" id="pf-usbank-balance" min="0" step="1000" placeholder="e.g. 25000" oninput="updateBankTierHint('U.S. Bank')"> 570 <p class="field-hint" id="pf-usbank-tier-hint">Combined balance across U.S. Bank Smartly Checking/Savings accounts. Used only to estimate your Smartly Rewards tier â not verified or shared with U.S. Bank.</p> 571 </div> 572 </div> 573 574 <!-- Credit & Fees --> 575 <div class="sc"> 576 <div class="sc-hd"> 577 <div class="sc-hd-left"> 578 <h2>Credit & Fees</h2> 579 </div> 580 </div> 581 <div class="form-row"> 582 <div class="form-group"> 583 <label for="pf-credit">Your credit score range</label> 584 <select id="pf-credit"> 585 <option value="excellent">Excellent (750+)</option> 586 <option value="verygood">Very Good (700 â 749)</option> 587 <option value="good">Good (650 â 699)</option> 588 <option value="fair">Fair (600 â 649)</option> 589 <option value="poor">Building credit (under 600)</option> 590 <option value="unknown">I'm not sure</option> 591 </select> 592 </div> 593 <div class="form-group"> 594 <label for="pf-fee">Annual fee tolerance</label> 595 <select id="pf-fee"> 596 <option value="nofee">No annual fee only</option> 597 <option value="low">Under $100/yr</option> 598 <option value="mid">Under $350/yr</option> 599 <option value="high">Any fee â if the value is there</option> 600 </select> 601 </div> 602 </div> 603 </div> 604 605 <div class="sc"> 606 <div class="form-actions" style="margin-top:0;"> 607 <button class="btn-save" onclick="savePrefs()">Save Preferences</button> 608 </div> 609 <div class="inline-alert" id="pref-saved-alert"> 610 <i class="fas fa-check-circle"></i> Preferences saved! Your matches have been updated. 611 </div> 612 </div> 613 </section> 614 615 <!-- ââââ SECTION: WALLET ââââ --> 616 <section class="account-section" id="section-wallet"> 617 618 <!-- My Cards --> 619 <div class="sc"> 620 <div class="sc-hd"> 621 <div class="sc-hd-left"> 622 <h2>My Cards</h2> 623 <p>Track the credit cards you own. We'll use them to optimize your spending below.</p> 624 </div> 625 </div> 626 627 <div class="wallet-search-wrap"> 628 <input type="text" id="wallet-search" placeholder="Search cards to addâ¦" oninput="walletSearchCards(this.value)" autocomplete="off"> 629 <div class="wallet-search-results" id="wallet-search-results"></div> 630 </div> 631 632 <div class="wallet-owned" id="wallet-owned-list"></div> 633 634 <div class="wallet-divider"></div> 635 <p class="wallet-unknown-intro"><strong>Don't see your card?</strong> Submit it below and we'll review it for addition to our database.</p> 636 <div class="form-row"> 637 <div class="form-group"> 638 <label>Card Name</label> 639 <input type="text" id="uk-name" placeholder="e.g. Chase Freedom Flex"> 640 </div> 641 <div class="form-group"> 642 <label>Issuer / Bank</label> 643 <input type="text" id="uk-issuer" placeholder="e.g. Chase"> 644 </div> 645 </div> 646 <div class="form-row"> 647 <div class="form-group"> 648 <label>Network</label> 649 <select id="uk-network"> 650 <option value="">Select networkâ¦</option> 651 <option>Visa</option> 652 <option>Mastercard</option> 653 <option>American Express</option> 654 <option>Discover</option> 655 </select> 656 </div> 657 <div class="form-group"></div> 658 </div> 659 <div class="cf-turnstile" data-sitekey="0x4AAAAAADiVOynXw_ZhYDP0" style="margin:0 0 14px;"></div> 660 <div class="form-actions"> 661 <button class="btn-save" onclick="submitUnknownCard()">Submit Card for Review</button> 662 </div> 663 <div class="inline-alert" id="uk-alert"></div> 664 </div> 665 666 </section> 667 668 <!-- ââââ SECTION: EXPENSES ââââ --> 669 <section class="account-section" id="section-expenses"> 670 671 <!-- Insights --> 672 <div class="sc" id="expense-insights-card" style="display:none;"> 673 <div class="sc-hd"> 674 <div class="sc-hd-left"> 675 <h2>Insights</h2> 676 <p>
676Based on your last 90 days of logged expenses.</p> 677 </div> 678 </div> 679 <div id="expense-insights-body"></div> 680 </div> 681 682 <!-- Log an expense --> 683 <div class="sc"> 684 <div class="sc-hd"> 685 <div class="sc-hd-left"> 686 <h2>Log an Expense</h2> 687 <p>Track what you actually spent and which card you used â we'll tell you if it was the right one.</p> 688 </div> 689 </div> 690 691 <div id="expense-no-cards-msg" style="display:none;"> 692 <div class="no-prefs" style="padding:32px 0;"> 693 <i class="fas fa-wallet"></i> 694 <h3>Add your cards first</h3> 695 <p>Add the cards you own in the Wallet tab before logging expenses.</p> 696 <button class="btn-setup" onclick="showSection('wallet', document.querySelector('[data-section=wallet]'))">Go to My Wallet â</button> 697 </div> 698 </div> 699 700 <div id="expense-form-wrap"> 701 <div class="form-row-3"> 702 <div class="form-group"> 703 <label for="ex-amount">Amount</label> 704 <input type="number" id="ex-amount" placeholder="e.g. 42.50" min="0" step="0.01"> 705 </div> 706 <div class="form-group"> 707 <label for="ex-category">Category</label> 708 <select id="ex-category"> 709 <option value="dining">Dining</option> 710 <option value="travel">Travel</option> 711 <option value="grocery">Grocery</option> 712 <option value="gas">Gas</option> 713 <option value="online">Online</option> 714 <option value="other">Everything Else</option> 715 </select> 716 </div> 717 <div class="form-group"> 718 <label for="ex-card">Card Used</label> 719 <select id="ex-card"></select> 720 </div> 721 </div> 722 <div class="form-row"> 723 <div class="form-group"> 724 <label for="ex-date">Date</label> 725 <input type="date" id="ex-date"> 726 </div> 727 <div class="form-group"> 728 <label for="ex-merchant">Merchant (optional)</label> 729 <input type="text" id="ex-merchant" placeholder="e.g. Whole Foods" maxlength="200"> 730 </div> 731 </div> 732 <div class="form-actions"> 733 <button class="btn-save" onclick="logExpense()">Log Expense</button> 734 </div> 735 <div class="inline-alert" id="expense-alert"></div> 736 </div> 737 </div> 738 739 <!-- Transaction list --> 740 <div class="sc"> 741 <div class="sc-hd"> 742 <div class="sc-hd-left"> 743 <h2>Recent Expenses</h2> 744 </div> 745 </div> 746 <div id="expense-list"></div> 747 <div id="expense-empty" class="no-prefs" style="padding:32px 0;display:none;"> 748 <i class="fas fa-receipt"></i> 749 <h3>No expenses logged yet</h3> 750 <p>Log your first expense above to start getting card-usage insights.</p> 751 </div> 752 </div> 753 754 </section> 755 756 <!-- ââââ SECTION: SETTINGS ââââ --> 757 <section class="account-section" id="section-settings"> 758 759 <!-- Profile Info --> 760 <div class="sc"> 761 <div class="sc-hd"> 762 <div class="sc-hd-left"> 763 <h2>Profile</h2> 764 <p>Update your name displayed across CardCompass.</p> 765 </div> 766 </div> 767 <div class="form-row"> 768 <div class="form-group"> 769 <label for="s-fname">First name</label> 770 <input type="text" id="s-fname" autocomplete="given-name"> 771 </div> 772 <div class="form-group"> 773 <label for="s-lname">Last name</label> 774 <input type="text" id="s-lname" autocomplete="family-name"> 775 </div> 776 </div> 777 <div class="form-group"> 778 <label>Email address</label> 779 <input type="email" id="s-email" disabled style="opacity:.6;cursor:not-allowed;"> 780 <p style="font-size:12px;color:var(--text-light);margin-top:6px;">Email cannot be changed. Contact <a href="/cdn-cgi/l/email-protection#c2a1a3b0a6a1adafb2a3b1b1aeaea182a5afa3abaeeca1adaf" style="color:var(--accent);"><span class="__cf_email__" data-cfemail="1370726177707c7e637260607f7f7053747e727a7f3d707c7e">[email protected]</span></a> for help.</p> 781 </div> 782 <button class="btn-save" onclick="saveName()">Save Changes</button> 783 <div class="inline-alert" id="name-saved-alert"> 784 <i class="fas fa-check-circle"></i> Profile updated successfully. 785 </div> 786 </div> 787 788 <!-- Change Password --> 789 <div class="sc"> 790 <div class="sc-hd"> 791 <div class="sc-hd-left"> 792 <h2>Change Password</h2> 793 <p>Choose a strong password with at least 8 characters.</p> 794 </div> 795 </div> 796 <div class="form-group"> 797 <label for="s-pass-current">Current password</label> 798 <div class="pass-wrap"> 799 <input type="password" id="s-pass-current" placeholder="Your current password" autocomplete="current-password"> 800 <button type="button" class="pass-toggle" onclick="togglePassVis('s-pass-current', this)" aria-label="Show password"><i class="fas fa-eye"></i></button> 801 </div> 802 </div> 803 <div class="form-group"> 804 <label for="s-pass-new">New password</label> 805 <div class="pass-wrap"> 806 <input type="password" id="s-pass-new" placeholder="New password (min. 8 chars)" autocomplete="new-password"> 807 <button type="button" class="pass-toggle" onclick="togglePassVis('s-pass-new', this)" aria-label="Show password"><i class="fas fa-eye"></i></button> 808 </div> 809 </div> 810 <div class="form-group"> 811 <label for="s-pass-confirm">Confirm new password</label> 812 <div class="pass-wrap"> 813 <input type="password" id="s-pass-confirm" placeholder="Repeat new password" autocomplete="new-password"> 814 <button type="button" class="pass-toggle" onclick="togglePassVis('s-pass-confirm', this)" aria-label="Show password"><i class="fas fa-eye"></i></button> 815 </div> 816 </div> 817 <button class="btn-save" onclick="changePassword()">Update Password</button> 818 <div class="inline-alert" id="pass-alert"></div> 819 </div> 820 821 <!-- Preferences quick-link --> 822 <div class="sc" style="display:flex;align-items:center;gap:16px;flex-wrap:wrap;"> 823 <div style="flex:1;"> 824 <div style="font-size:15px;font-weight:700;">Card Preferences</div> 825 <div style="font-size:13px;color:var(--text-gray);margin-top:3px;">Your goal, spending range, credit score, and fee preference.</div> 826 </div> 827 <button class="btn-edit" onclick="showSection('prefs', document.querySelector('[data-section=prefs]'))"> 828 <i class="fas fa-sliders-h"></i> View & Edit 829 </button> 830 </div> 831 832 <!-- Danger Zone --> 833 <div class="sc"> 834 <div class="danger-zone"> 835 <h3><i class="fas fa-exclamation-triangle"></i> Danger Zone</h3> 836 <p>Deleting your account permanently removes your profile, preferences, and saved data from this device. This cannot be undone.</p> 837 <button class="btn-danger" onclick="deleteAccount()">
837Delete My Account</button> 838 </div> 839 </div> 840 841 </section> 842 843 </main> 844</div><!-- /.account-body --> 845 846<footer> 847 <a href="/">Home</a> 848 <a href="/privacy-policy">Privacy</a> 849 <a href="/terms">Terms</a> 850 <a href="/advertiser-disclosure">Disclosure</a> 851 <span style="margin-left:14px;">© 2026 CardCompass, Inc. <span class="footer-social" style="display:inline-flex;align-items:center;gap:14px;vertical-align:middle;margin-left:10px;"><a href="https://www.instagram.com/cardcompassapp/" target="_blank" rel="noopener" aria-label="CardCompass on Instagram" style="display:inline-flex;margin:0;"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="20" height="20" x="2" y="2" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" x2="17.51" y1="6.5" y2="6.5"/></svg></a><a href="https://www.facebook.com/cardcompassapp" target="_blank" rel="noopener" aria-label="CardCompass on Facebook" style="display:inline-flex;margin:0;"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/></svg></a></span></span> 852</footer> 853 854<div class="toast" id="toast"></div>
vendor: 110 bytes, lines 854-856
854 855 856<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>
856<script src="cards-data.js?v=13"></script>
vendor: 1 bytes, line 856
856
857<script src="recommend-engine.js?v=4"></script>
vendor: 1 bytes, line 857
857
858<script src="nav-auth.js?v=2"></script>
vendor: 1 bytes, line 858
858
859<script> 860/* âââââââââââââââââââââââââââââââââââââââââââ 861 Card pool + scoring now live in window.CCEngine, shared with 862 index.html/quiz.html/get-started.html â see recommend-engine.js 863âââââââââââââââââââââââââââââââââââââââââââ */ 864 865/* âââââââââââââââââââââââââââââââââââââââââââ 866 STATE 867âââââââââââââââââââââââââââââââââââââââââââ */ 868let currentUser = null; 869 870/* âââââââââââââââââââââââââââââââââââââââââââ 871 INIT â check auth, load data 872âââââââââââââââââââââââââââââââââââââââââââ */ 873(async function init() { 874 if (!window.ccAuth || !window.ccAuth.getToken()) { 875 window.location.href = '/login'; 876 return; 877 } 878 879 // Capture whatever's cached locally before requireAuth() re-syncs identity 880 // fields from the server â prefs/myCards aren't in D1 yet (Milestone 4), so 881 // they only exist here and must be preserved across the auth check. 882 let cached = {}; 883 try { cached = JSON.parse(localStorage.getItem('cc_user') || '{}') || {}; } catch(e) {} 884 885 const serverUser = await window.ccAuth.requireAuth(); 886 if (!serverUser) { 887 window.location.href = '/login'; 888 return; 889 } 890 891 currentUser = Object.assign({}, cached, serverUser, { 892 prefs: cached.prefs || null, 893 myCards: cached.myCards || [] 894 }); 895 896 // D1 is authoritative for preferences as of Milestone 5 â fetch before 897 // the first render so the expanded form and Matches tab reflect it 898 // immediately rather than flashing stale/empty local state first. 899 try { 900 const res = await fetch('/api/prefs', { headers: { 'Authorization': 'Bearer ' + window.ccAuth.getToken() } }); 901 if (res.ok) { 902 const data = await res.json(); 903 if (data.prefs) currentUser.prefs = data.prefs; 904 } 905 } catch (e) {} 906 907 localStorage.setItem('cc_user', JSON.stringify(currentUser)); 908 909 renderProfile(); 910 renderMatches(); 911 renderPrefsDisplay(); 912 renderSettings(); 913 renderWallet(); 914 915 const dateInput = document.getElementById('ex-date'); 916 if (dateInput) dateInput.value = new Date().toISOString().slice(0, 10); 917 loadExpenses(); 918 919 // Show section from hash if present 920 const hash = window.location.hash.replace('#',''); 921 if (['matches','prefs','settings','wallet','expenses'].includes(hash)) { 922 const el = document.querySelector(`[data-section="${hash}"]`); 923 if (el) showSection(hash, el); 924 } 925 926 // Reconcile wallet with D1 in the background (server is now authoritative 927 // for cross-device sync) â myCards' {id,addedAt} shape matches /api/wallet 928 // exactly, so this is safe to merge in without a UI rewrite. 929 fetch('/api/wallet', { headers: { 'Authorization': 'Bearer ' + window.ccAuth.getToken() } }) 930 .then(r => r.ok ? r.json() : null) 931 .then(data => { 932 if (!data || !Array.isArray(data.cards)) return; 933 const byId = new Map(currentUser.myCards.map(c => [c.id, c])); 934 data.cards.forEach(c => { if (!byId.has(c.id)) byId.set(c.id, { id: c.id, addedAt: c.addedAt }); }); 935 currentUser.myCards = Array.from(byId.values()); 936 localStorage.setItem('cc_user', JSON.stringify(currentUser)); 937 renderWallet(); 938 }) 939 .catch(() => {}); 940})(); 941 942/* âââââââââââââââââââââââââââââââââââââââââââ 943 RENDER PROFILE HERO 944âââââââââââââââââââââââââââââââââââââââââââ */ 945function renderProfile() { 946 const u = currentUser; 947 const initials = ((u.fname?.[0]||'') + (u.lname?.[0]||'')).toUpperCase() || '?'; 948 document.getElementById('p-avatar').textContent = initials; 949 document.getElementById('p-name').textContent = [u.fname, u.lname].filter(
949Boolean).join(' ') || 'My Account'; 950 document.getElementById('p-email').textContent = u.email || ''; 951 if (u.createdAt) { 952 const d = new Date(u.createdAt).toLocaleDateString('en-US',{month:'long',year:'numeric'}); 953 document.getElementById('p-since').innerHTML = `<i class="fas fa-calendar-alt"></i> Member since ${d}`; 954 } 955 // Stats 956 if (u.prefs && u.prefs.goal) { 957 const scored = scoreCards(u.prefs); 958 const topEarn = scored.length ? scored[0].net : 0; 959 document.getElementById('p-stats').innerHTML = ` 960 <div class="profile-stat"><span class="val">$${topEarn.toLocaleString()}</span><span class="lbl">Top match<br>projected / yr</span></div> 961 <div class="profile-stat"><span class="val">${scored.length}</span><span class="lbl">Cards<br>matched</span></div>`; 962 } 963} 964 965/* âââââââââââââââââââââââââââââââââââââââââââ 966 SCORE CARDS â ranks the full window.ALL_CARDS pool via window.CCEngine 967 (used to only ever consider 5 hardcoded cards). `prefs` is the full 968 D1 preferences shape (Milestone 5) â converted to the engine's 969 profile shape directly, no lossy lump-sum spend approximation needed 970 now that per-category spend is collected natively. 971âââââââââââââââââââââââââââââââââââââââââââ */ 972function prefsToProfile(p) { 973 return { 974 goal: p.goal, travelFreq: p.travelFreq, airlines: p.airlines || [], hotel: p.hotel, 975 intl: p.intlTravel, fee: p.feeTolerance, score: p.creditScore, 976 bankRelationships: p.bankRelationships || [], 977 spend: { 978 travel: p.spendTravel || 0, dining: p.spendDining || 0, grocery: p.spendGrocery || 0, 979 gas: p.spendGas || 0, online: p.spendOnline || 0, other: p.spendOther || 0 980 } 981 }; 982} 983 984function scoreCards(prefs) { 985 const profile = prefsToProfile(prefs); 986 const feeMax = { nofee: 0, low: 100, mid: 350, high: 9999 }[profile.fee] ?? 9999; 987 return window.CCEngine.rankCards(profile, { limit: 20 }) 988 .filter(c => c.annualFee <= feeMax) 989 .map(c => { 990 const yearly = Object.keys(profile.spend).reduce( 991 (sum, cat) => sum + window.CCEngine.estimateEarn(c, cat, profile.spend[cat], profile) * 12, 0); 992 const net = Math.round(yearly + (c.bonusValue / 2) - c.annualFee); 993 const relTier = window.CCEngine.getRelationshipMultiplier(c, profile); 994 return Object.assign({}, c, { yearly: Math.round(yearly), net: net, _relTier: relTier }); 995 }) 996 .sort((a, b) => b.net - a.net); 997} 998 999/* âââââââââââââââââââââââââââââââââââââââââââ 1000 CARD IMAGE & APPLY URL HELPERS â scored cards are now full 1001 window.ALL_CARDS objects already, no id-lookup needed. 1002âââââââââââââââââââââââââââââââââââââââââââ */ 1003function cardImgHtml(c) { 1004 if (c.cardImage) return `<div class="match-img"><img src="${c.cardImage}" alt="${c.name}" style="width:100%;height:100%;object-fit:cover;border-radius:inherit;"></div>`; 1005 const bg = (c.cardArt && c.cardArt.bg) || 'linear-gradient(135deg,#1A3F6F 0%,#0D2A4A 100%)'; 1006 const label = (c.issuer || '').substring(0, 4).toUpperCase(); 1007 return `<div class="match-img" style="background:${bg};color:#fff;font-size:10px;font-weight:800;display:flex;align-items:center;justify-content:center;">${label}</div>`; 1008} 1009 1010function matchApplyUrl(c) { 1011 return window.getApplyUrl ? window.getApplyUrl(c) : c.applyUrl; 1012} 1013 1014/* âââââââââââââââââââââââââââââââââââââââââââ 1015 RENDER MATCHES 1016âââââââââââââââââââââââââââââââââââââââââââ */ 1017function renderMatches() { 1018 const u = currentUser; 1019 if (!u.prefs || !u.prefs.goal) { 1020 document.getElementById('no-prefs-card').style.display = 'block'; 1021 document.getElementById('has-prefs-matches').style.display = 'none'; 1022 return; 1023 } 1024 document.getElementById('no-prefs-card').style.display = 'none'; 1025 document.getElementById('has-prefs-matches').style.display = 'block'; 1026 1027 const scored = scoreCards(u.prefs); 1028 const list = document.getElementById('matches-list'); 1029 1030 if (!scored.length) { 1031 list.innerHTML = `<div class="no-prefs"><i class="fas fa-search-minus"></i><h3>No cards match your filters</h3><p>Try updating your annual fee or credit score preference.</p><button class="btn-setup" onclick="showSection('prefs', document.querySelector('[data-section=prefs]'))">Update Preferences</button></div>`; 1032 return; 1033 } 1034 1035 // Summary bar 1036 const top = scored[0]; 1037 document.getElementById('earn-summary').style.display = 'flex'; 1038 document.getElementById('earn-top-val').textContent = '$' + top.net.toLocaleStr
1038ing(); 1039 const totalSpend = (u.prefs.spendTravel||0) + (u.prefs.spendDining||0) + (u.prefs.spendGrocery||0) + 1040 (u.prefs.spendGas||0) + (u.prefs.spendOnline||0) + (u.prefs.spendOther||0); 1041 const flatCashback = Math.round(totalSpend * 0.015 * 12); 1042 document.getElementById('earn-vs').innerHTML = `With <strong>${top.name}</strong> â based on your spending profile.<br>That's <strong>$${Math.max(0,(top.net-flatCashback)).toLocaleString()} more</strong> per year than a standard 1.5% cashback card at your spending level.`; 1043 1044 list.innerHTML = scored.map((c, i) => ` 1045 <div class="match-card${i===0?' top-pick':''}"> 1046 <div class="match-rank">#${i+1}</div> 1047 ${cardImgHtml(c)} 1048 <div class="match-info"> 1049 ${i===0?'<div class="match-badge"><i class="fas fa-star" style="font-size:9px;margin-right:3px;"></i>Best Match</div>':''} 1050 ${c._relTier && c._relTier.tier ? `<div class="match-relationship-badge"><i class="fas fa-university"></i> ${c._relTier.program.label} ${c._relTier.tier.name} â +${Math.round(c._relTier.tier.bonus*100)}%</div>` : ''} 1051 <div class="match-name">${c.name}</div> 1052 <div class="match-for">${c.rateLabel} · Best for ${c.bestFor}</div> 1053 </div> 1054 <div class="match-numbers"> 1055 <div class="match-earn-val">$${c.net.toLocaleString()}</div> 1056 <div class="match-earn-lbl">/ year est.</div> 1057 <div class="match-fee">${c.annualFee>0?'$'+c.annualFee+' annual fee':'No annual fee'}</div> 1058 </div> 1059 <div class="match-actions"> 1060 <button class="match-apply" onclick="window.open('${matchApplyUrl(c)}','_blank','noopener,noreferrer')">Apply Now</button> 1061 <a class="match-detail" href="/cards#${c.id}">Details</a> 1062 </div> 1063 </div>`).join(''); 1064} 1065 1066/* âââââââââââââââââââââââââââââââââââââââââââ 1067 RENDER PREFERENCES â pre-fills the always-visible, grouped form from 1068 currentUser.prefs (the full D1 preferences shape, fetched in init()). 1069âââââââââââââââââââââââââââââââââââââââââââ */ 1070const TRAVEL_RELATED_GOALS = ['travel', 'airline', 'hotel']; 1071 1072function renderPrefsDisplay() { 1073 const p = currentUser.prefs || {}; 1074 if (p.goal) document.getElementById('pf-goal').value = p.goal; 1075 document.getElementById('pf-travel-freq').value = p.travelFreq || 'occasional'; 1076 document.getElementById('pf-hotel').value = p.hotel || 'flexible'; 1077 document.getElementById('pf-intl').value = p.intlTravel || 'rarely'; 1078 document.getElementById('pf-credit').value = p.creditScore || 'good'; 1079 document.getElementById('pf-fee').value = p.feeTolerance || 'high'; 1080 document.getElementById('pf-spend-travel').value = p.spendTravel || ''; 1081 document.getElementById('pf-spend-dining').value = p.spendDining || ''; 1082 document.getElementById('pf-spend-grocery').value = p.spendGrocery || ''; 1083 document.getElementById('pf-spend-gas').value = p.spendGas || ''; 1084 document.getElementById('pf-spend-online').value = p.spendOnline || ''; 1085 document.getElementById('pf-spend-other').value = p.spendOther || ''; 1086 1087 const airlines = p.airlines || []; 1088 document.querySelectorAll('#pf-airlines-group input[type=checkbox]').forEach(cb => { 1089 cb.checked = airlines.includes(cb.value); 1090 cb.closest('.pill-check').classList.toggle('checked', cb.checked); 1091 }); 1092 1093 REL_ISSUERS.forEach(cfg => { 1094 const rel = (p.bankRelationships || []).find(r => r.issuer === cfg.issuer); 1095 document.getElementById(cfg.inputId).value = rel ? rel.balance : ''; 1096 updateBankTierHint(cfg.issuer); 1097 }); 1098 1099 updateTravelCollapseState(); 1100} 1101 1102// Every issuer with a modeled relationship-bonus program (recommend-engine.js's 1103// RELATIONSHIP_PROGRAMS) gets one balance field here. Adding a new issuer later 1104// is just a new HTML form-group + a new entry in this list, not new JS logic. 1105const REL_ISSUERS = [ 1106 { issuer: 'Bank of America', inputId: 'pf-bofa-balance', hintId: 'pf-bofa-tier-hint', 1107 defaultHint: '3-month average combined balance across BofA banking
1107+ Merrill investment accounts. Used only to estimate your BofA Rewards tier â not verified or shared with Bank of America.' }, 1108 { issuer: 'U.S. Bank', inputId: 'pf-usbank-balance', hintId: 'pf-usbank-tier-hint', 1109 defaultHint: 'Combined balance across U.S. Bank Smartly Checking/Savings accounts. Used only to estimate your Smartly Rewards tier â not verified or shared with U.S. Bank.' } 1110]; 1111 1112// Live preview of the relationship-bonus tier a declared balance would unlock â 1113// mirrors the tier ladder in recommend-engine.js's RELATIONSHIP_PROGRAMS 1114// so this hint always matches what scoring actually applies. 1115function updateBankTierHint(issuer) { 1116 const cfg = REL_ISSUERS.find(c => c.issuer === issuer); 1117 const hint = document.getElementById(cfg.hintId); 1118 const balance = parseFloat(document.getElementById(cfg.inputId).value) || 0; 1119 const info = window.CCEngine.getRelationshipTier(issuer, balance); 1120 if (info.tier) { 1121 hint.innerHTML = `<strong>${info.program.label} ${info.tier.name}</strong> â +${Math.round(info.tier.bonus * 100)}% rewards on ${issuer} cards.`; 1122 } else { 1123 hint.textContent = cfg.defaultHint; 1124 } 1125} 1126 1127function toggleTravelCollapse() { 1128 const body = document.getElementById('travel-collapse-body'); 1129 const summary = document.getElementById('travel-collapse-summary'); 1130 const isOpen = body.style.display !== 'none'; 1131 body.style.display = isOpen ? 'none' : 'block'; 1132 summary.classList.toggle('open', !isOpen); 1133} 1134 1135// Collapsed by default unless the selected goal is travel-related, in 1136// which case it's expanded â this is the progressive-disclosure 1137// mechanism that keeps the page from being a wall of irrelevant fields 1138// for e.g. a pure-cashback user. 1139function updateTravelCollapseState() { 1140 const goal = document.getElementById('pf-goal').value; 1141 const relevant = TRAVEL_RELATED_GOALS.includes(goal); 1142 const body = document.getElementById('travel-collapse-body'); 1143 const summary = document.getElementById('travel-collapse-summary'); 1144 const summaryText = document.getElementById('travel-collapse-text'); 1145 1146 const freq = document.getElementById('pf-travel-freq').value; 1147 const hotel = document.getElementById('pf-hotel').value; 1148 const checkedAirlines = Array.from(document.querySelectorAll('#pf-airlines-group input:checked')).map(cb => cb.value); 1149 const hasDetail = freq !== 'occasional' || hotel !== 'flexible' || checkedAirlines.length > 0; 1150 1151 if (hasDetail) { 1152 const freqLabel = { frequent: 'Frequent traveler', monthly: 'Regular traveler', occasional: 'Occasional traveler', rarely: 'Rarely travels' }[freq] || ''; 1153 const hotelLabel = (hotel !== 'flexible' && hotel !== 'notimportant') ? hotel.charAt(0).toUpperCase() + hotel.slice(1) : ''; 1154 summaryText.innerHTML = '<strong>' + [freqLabel, hotelLabel].filter(Boolean).join(' · ') + '</strong>'; 1155 } else { 1156 summaryText.textContent = 'Not set â click to expand'; 1157 } 1158 1159 body.style.display = relevant ? 'block' : 'none'; 1160 summary.classList.toggle('open', relevant); 1161} 1162 1163/* âââââââââââââââââââââââââââââââââââââââââââ 1164 RENDER SETTINGS FIELDS 1165âââââââââââââââââââââââââââââââââââââââââââ */
1166function renderSettings() { 1167 const u = currentUser; 1168 if (u.fname) document.getElementById('s-fname').value = u.fname; 1169 if (u.lname) document.getElementById('s-lname').value = u.lname; 1170 if (u.email) document.getElementById('s-email').value = u.email; 1171} 1172 1173/* âââââââââââââââââââââââââââââââââââââââââââ 1174 SECTION SWITCHING 1175âââââââââââââââââââââââââââââââââââââââââââ */ 1176function showSection(name, triggerEl) { 1177 document.querySelectorAll('.account-section').forEach(s => s.classList.remove('active')); 1178 document.querySelectorAll('.sidebar-link, .tab-btn').forEach(l => l.classList.remove('active')); 1179 document.getElementById('section-' + name).classList.add('active'); 1180 if (triggerEl) triggerEl.classList.add('active'); 1181 // Also activate matching sidebar/tab 1182 document.querySelectorAll(`[data-section="${name}"]`).forEach(el => el.classList.add('active')); 1183 window.location.hash = name; 1184 window.scrollTo({ top: 0, behavior: 'smooth' }); 1185} 1186 1187/* âââââââââââââââââââââââââââââââââââââââââââ 1188 PREFERENCES EDIT / SAVE 1189âââââââââââââââââââââââââââââââââââââââââââ */ 1190function onAirlinePillChange(cb) { 1191 cb.closest('.pill-check').classList.toggle('checked', cb.checked); 1192 updateTravelCollapseState(); 1193} 1194 1195async function savePrefs() { 1196 const prefs = { 1197 goal: document.getElementById('pf-goal').value, 1198 travelFreq: document.getElementById('pf-travel-freq').value, 1199 hotel: document.getElementById('pf-hotel').value, 1200 intlTravel: document.getElementById('pf-intl').value, 1201 creditScore: document.getElementById('pf-credit').value, 1202 feeTolerance: document.getElementById('pf-fee').value, 1203 airlines: Array.from(document.querySelectorAll('#pf-airlines-group input:checked')).map(cb => cb.value), 1204 bankRelationships: REL_ISSUERS 1205 .map(cfg => ({ issuer: cfg.issuer, balance: parseFloat(document.getElementById(cfg.inputId).value) || 0 })) 1206 .filter(r => r.balance > 0), 1207 spendTravel: parseFloat(document.getElementById('pf-spend-travel').value) || 0, 1208 spendDining: parseFloat(document.getElementById('pf-spend-dining').value) || 0, 1209 spendGrocery: parseFloat(document.getElementById('pf-spend-grocery').value) || 0, 1210 spendGas: parseFloat(document.getElementById('pf-spend-gas').value) || 0, 1211 spendOnline: parseFloat(document.getElementById('pf-spend-online').value) || 0, 1212 spendOther: parseFloat(document.getElementById('pf-spend-other').value) || 0 1213 }; 1214 currentUser.prefs = prefs; 1215 localStorage.setItem('cc_user', JSON.stringify(currentUser)); 1216 renderMatches(); 1217 renderProfile(); 1218 1219 const alert = document.getElementById('pref-saved-alert'); 1220 try { 1221 const res = await fetch('/api/prefs', { 1222 method: 'PUT', 1223 headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.ccAuth.getToken() }, 1224 body: JSON.stringify(prefs) 1225 }); 1226 if (!res.ok) throw new Error('save failed'); 1227 alert.className = 'inline-alert show success'; 1228 alert.innerHTML = '<i class="fas fa-check-circle"></i> Preferences saved! Your matches have been updated.'; 1229 } catch (e) { 1230 // Local scoring already updated above â only the D1 sync failed. 1231 alert.className = 'inline-alert show error'; 1232 alert.innerHTML = '<i class="fas fa-exclamation-circle"></i> Saved locally, but couldn\'t sync to your account. Check your connection and try again.'; 1233 showToast('Preferences updated locally â sync failed, will retry on next save.'); 1234 setTimeout(() => alert.classList.remove('show'), 5000); 1235 return; 1236 } 1237 1238 showToast('Preferences saved â your matches have been updated!'); 1239 setTimeout(() => alert.classList.remove('show'), 4000); 1240} 1241 1242/* âââââââââââââââââââââââââââââââââââââââââââ 1243 SETTINGS SAVE 1244âââââââââââââââââââââââââââââââââââââââââââ */ 1245async function saveName() { 1246 const fname = document.getElementById('s-fname').value.trim(); 1247 const lname = document.getElementById('s-lname').value.trim(); 1248 if (!fname) { showToast('First name cannot be empty.'); return; } 1249 1250 const res = await fetch('/api/account', { 1251 method: 'PUT', 1252 headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.ccAuth.getToken() }, 1253 body: JSON.stringify({ fname, lname }) 1254 }).catch(() => null); 1255 const data = res ? await res.json().catch(() => ({})) : {}; 1256 if (!res || !res.ok) { showToast(data.error || 'Could not save your name. Please try again.'); return; } 1257 1258 currentUser.fname = fname; 1259 currentUser.lname = lname; 1260 localStorage.setItem('cc_user', JSON.stringify(currentUser)); 1261 renderProfile(); 1262 const alert = document.getElementById('name-saved-alert'); 1263 alert.classList.add('show', 'success'); 1264 showToast('Profile updated!'); 1265 setTimeout(() => alert.classList.remove('show'), 4000); 1266} 1267 1268async function changePassword() { 1269 const cp = document.getElementById('s-pass-current').value; 1270 const np = document.getElementById('s-pass-new').value; 1271 const cf = document.getElementById('s-pass-confirm').value; 1272 const al = document.getElementById('pass-alert'); 1273 al.className = 'inline-alert'; 1274 if (np.length < 8) { 1275 al.className = 'inline-alert show error'; 1276 al.innerHTML = '<i class="fas fa-exclamation-circle"></i> Password must be at least 8 characters.'; return; 1277 } 1278 if (np !== cf) { 1279 al.className = 'inline-alert show error'; 1280 al.innerHTML = '<i class="fas fa-exclamation-circle"></i> Passwords do not match.'; return; 1281 } 1282 1283 const res = await fetch('/api/auth/change-password', { 1284 method: 'POST', 1285 headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.ccAuth.getToken() }, 1286 body: JSON.stringify({ currentPassword: cp, newPassword: np }) 1287 }).catch(() => null); 1288 const data = res ? await res.json().catch(() => ({})) : {}; 1289 if (!res || !res.ok) { 1290 al.className = 'inline-alert show error'; 1291 al.innerHTML = '<i class="fas fa-exclamation-circle"></i>
1291 ' + (data.error || 'Could not update your password. Please try again.'); 1292 return; 1293 } 1294 1295 document.getElementById('s-pass-current').value = ''; 1296 document.getElementById('s-pass-new').value = ''; 1297 document.getElementById('s-pass-confirm').value = ''; 1298 al.className = 'inline-alert show success'; 1299 al.innerHTML = '<i class="fas fa-check-circle"></i> Password updated successfully.'; 1300 showToast('Password changed!'); 1301 setTimeout(() => al.classList.remove('show'), 5000); 1302} 1303 1304async function deleteAccount() { 1305 if (!confirm('Are you sure? This will permanently delete your account and all saved data. This cannot be undone.')) return; 1306 const password = prompt('Please re-enter your password to confirm account deletion:'); 1307 if (password === null) return; 1308 1309 const res = await fetch('/api/auth/delete-account', { 1310 method: 'POST', 1311 headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.ccAuth.getToken() }, 1312 body: JSON.stringify({ password }) 1313 }).catch(() => null); 1314 const data = res ? await res.json().catch(() => ({})) : {}; 1315 if (!res || !res.ok) { showToast(data.error || 'Could not delete your account. Please try again.'); return; } 1316 1317 window.ccAuth.clearAuth(); 1318 localStorage.removeItem('cc_remember_email'); 1319 showToast('Account deleted. Redirectingâ¦'); 1320 setTimeout(() => window.location.href = '/', 1500); 1321} 1322 1323// signOut() is provided by nav-auth.js (window.signOut) â it clears cc_token/cc_user 1324// and calls /api/auth/logout. Not redefined here so that shared implementation wins. 1325 1326/* âââââââââââââââââââââââââââââââââââââââââââ 1327 HELPERS 1328âââââââââââââââââââââââââââââââââââââââââââ */ 1329function togglePassVis(id, btn) { 1330 const input = document.getElementById(id); 1331 const isPass = input.type === 'password'; 1332 input.type = isPass ? 'text' : 'password'; 1333 btn.querySelector('i').className = isPass ? 'fas fa-eye-slash' : 'fas fa-eye'; 1334} 1335 1336function showToast(msg, duration=4000) { 1337 const t = document.getElementById('toast'); 1338 t.textContent = msg; 1339 t.classList.add('show'); 1340 setTimeout(() => t.classList.remove('show'), duration); 1341} 1342 1343/* âââââââââââââââââââââââââââââââââââââââââââ 1344 WALLET â MY CARDS 1345âââââââââââââââââââââââââââââââââââââââââââ */ 1346 1347function renderWallet() { 1348 const list = document.getElementById('wallet-owned-list'); 1349 if (!list) return; 1350 populateExpenseCardSelect(); 1351 const owned = currentUser.myCards || []; 1352 if (!owned.length) { 1353 list.innerHTML = ''; 1354 return; 1355 } 1356 const allCards = window.ALL_CARDS || []; 1357 list.innerHTML = owned.map(entry => { 1358 const card = allCards.find(c => c.id === entry.id); 1359 const name = card ? card.name : entry.name; 1360 const issuer = card ? (card.issuer || '') : ''; 1361 const imgSrc = card && card.cardImage; 1362 const imgHtml = imgSrc 1363 ? `<div class="wallet-card-img"><img src="${imgSrc}" alt="${name}"></div>` 1364 : `<div class="wallet-card-img">${issuer.slice(0, 4).toUpperCase() || '?'}</div>`; 1365 return ` 1366 <div class="wallet-card-row"> 1367 ${imgHtml} 1368 <div class="wallet-card-info"> 1369 <div class="wallet-card-name">${name}</div> 1370 ${issuer ? `<div class="wallet-card-issuer">${issuer}</div>` : ''} 1371 </div> 1372 <button class="wallet-remove" onclick="walletRemoveCard('${entry.id}')"><i class="fas fa-times"></i> Remove</button> 1373 </div>`; 1374 }).join(''); 1375} 1376 1377function walletSearchCards(q) { 1378 const resultsEl = document.getElementById('wallet-search-results'); 1379 if (!resultsEl) return; 1380 if (!q || q.length < 2) { 1381 resultsEl.classList.remove('open'); 1382 resultsEl.innerHTML = ''; 1383 return; 1384 } 1385 const allCards = window.ALL_CARDS || []; 1386 const owned = (currentUser.myCards || []).map(e => e.id); 1387 const lower = q.toLowerCase(); 1388 const matches = allCards 1389 .filter(c => !owned.includes(c.id) && (c.name.toLowerCase().includes(lower) || (c.issuer && c.issuer.toLowerCase().includes(lower)))) 1390 .slice(0, 8); 1391 if (!matches.length) { 1392 resultsEl.innerHTML = '<div style="padding:12px 14px;font-size:13px;color:var(--text-light);">
1392No matching cards found. Submit yours below.</div>'; 1393 resultsEl.classList.add('open'); 1394 return; 1395 } 1396 resultsEl.innerHTML = matches.map(c => { 1397 const thumb = c.cardImage 1398 ? `<div class="wallet-search-thumb"><img src="${c.cardImage}" alt="${c.name}"></div>` 1399 : `<div class="wallet-search-thumb">${(c.issuer||'').slice(0,4).toUpperCase()}</div>`; 1400 return ` 1401 <div class="wallet-search-item" onclick="walletAddCard('${c.id}')"> 1402 ${thumb} 1403 <div style="flex:1;min-width:0;"> 1404 <div class="wallet-search-name">${c.name}</div> 1405 <div class="wallet-search-sub">${c.issuer || ''}</div> 1406 </div> 1407 <button class="wallet-search-add" tabindex="-1">Add</button> 1408 </div>`; 1409 }).join(''); 1410 resultsEl.classList.add('open'); 1411} 1412 1413function walletAddCard(id) { 1414 const allCards = window.ALL_CARDS || []; 1415 const card = allCards.find(c => c.id === id); 1416 if (!card) return; 1417 if (!currentUser.myCards) currentUser.myCards = []; 1418 if (currentUser.myCards.find(e => e.id === id)) { showToast('Card already in your wallet.'); return; } 1419 currentUser.myCards.push({ id, name: card.name, addedAt: Date.now() }); 1420 localStorage.setItem('cc_user', JSON.stringify(currentUser)); 1421 const searchEl = document.getElementById('wallet-search'); 1422 const resultsEl = document.getElementById('wallet-search-results'); 1423 if (searchEl) searchEl.value = ''; 1424 if (resultsEl) { resultsEl.classList.remove('open'); resultsEl.innerHTML = ''; } 1425 renderWallet(); 1426 showToast(card.name + ' added to your wallet!'); 1427 1428 fetch('/api/wallet', { 1429 method: 'POST', 1430 headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.ccAuth.getToken() }, 1431 body: JSON.stringify({ cardId: id }) 1432 }).catch(() => {}); // best-effort â local state already updated above 1433} 1434 1435function walletRemoveCard(id) { 1436 if (!currentUser.myCards) return; 1437 currentUser.myCards = currentUser.myCards.filter(e => e.id !== id); 1438 localStorage.setItem('cc_user', JSON.stringify(currentUser)); 1439 renderWallet(); 1440 renderExpenseInsights(); // owned-card set changed, re-evaluate "apply for a better card" recs 1441 1442 fetch('/api/wallet', { 1443 method: 'DELETE', 1444 headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.ccAuth.getToken() }, 1445 body: JSON.stringify({ cardId: id }) 1446 }).catch(() => {}); 1447} 1448 1449/* âââââââââââââââââââââââââââââââââââââââââââ 1450 WALLET â UNKNOWN CARD SUBMISSION 1451âââââââââââââââââââââââââââââââââââââââââââ */ 1452 1453async function submitUnknownCard() { 1454 const name = document.getElementById('uk-name').value.trim(); 1455 const issuer = document.getElementById('uk-issuer').value.trim(); 1456 const network = document.getElementById('uk-network').value; 1457 const alertEl = document.getElementById('uk-alert'); 1458 alertEl.className = 'inline-alert'; 1459 if (!name || !issuer) { 1460 alertEl.className = 'inline-alert show error'; 1461 alertEl.innerHTML = '<i class="fas fa-exclamation-circle"></i> Please enter a card name and issuer.'; 1462 return; 1463 } 1464 const token = window.turnstile?.getResponse(); 1465 if (!token) { 1466 alertEl.className = 'inline-alert show error'; 1467 alertEl.innerHTML = '<i class="fas fa-exclamation-circle"></i> Please complete the verification below.'; 1468 return; 1469 } 1470 try { 1471 const res = await fetch('/api/log-card', { 1472 method: 'POST', 1473 headers: { 'Content-Type': 'application/json' }, 1474 body: JSON.stringify({ cardName: name, issuer, network, submittedBy: currentUser.email || '', cfToken: token }) 1475 }); 1476 if (res.ok) { 1477 alertEl.className = 'inline-alert show success'; 1478 alertEl.innerHTML = "<i class='fas fa-check-circle'></i> Thanks! We'll review your submission and add it soon."; 1479 document.getElementById('uk-name').value = ''; 1480 document.getElementById('uk-issuer').value = ''; 1481 document.getElementById('uk-network').value = ''; 1482 window.turnstile?.reset(); 1483 } else { 1484 throw new Error('server'); 1485 } 1486 } catch (e) { 1487 alertEl.className = 'inline-alert show error'; 1488 alertEl.innerHTML = '<i class="fas fa-exclamation-circle"></i> Something went wrong. Please try again.'; 1489 window.turnstile?.reset(); 1490 } 1491} 1492 1493/* âââââââââââââââââââââââââââââââââââââââââââ 1494 WALLET â SPENDING OPTIMIZER 1495âââââââââââââââââââââââââââââââââââââââââââ */ 1496
1497/* âââââââââââââââââââââââââââââââââââââââââââ 1498 EXPENSES â log/list/delete + card-usage analysis. Replaces the old 1499 manual-estimate Spending Optimizer with real transaction tracking. 1500âââââââââââââââââââââââââââââââââââââââââââ */ 1501const CATEGORY_ICONS = { travel: 'fa-plane', dining: 'fa-utensils', grocery: 'fa-shopping-cart', gas: 'fa-gas-pump', online: 'fa-laptop', other: 'fa-receipt' }; 1502const CATEGORY_LABELS = { travel: 'Travel', dining: 'Dining', grocery: 'Grocery', gas: 'Gas', online: 'Online', other: 'Everything Else' }; 1503let currentExpenses = []; 1504 1505function ownedCardObjects() { 1506 const allCards = window.ALL_CARDS || []; 1507 return (currentUser.myCards || []).map(e => allCards.find(c => c.id === e.id)).filter(Boolean); 1508} 1509 1510function populateExpenseCardSelect() { 1511 const sel = document.getElementById('ex-card'); 1512 const owned = ownedCardObjects(); 1513 const noCardsMsg = document.getElementById('expense-no-cards-msg'); 1514 const formWrap = document.getElementById('expense-form-wrap'); 1515 if (!owned.length) { 1516 noCardsMsg.style.display = 'block'; 1517 formWrap.style.display = 'none'; 1518 return; 1519 } 1520 noCardsMsg.style.display = 'none'; 1521 formWrap.style.display = 'block'; 1522 sel.innerHTML = owned.map(c => `<option value="${c.id}">${c.name}</option>`).join(''); 1523} 1524 1525async function loadExpenses() { 1526 try { 1527 const res = await fetch('/api/expenses?limit=50', { headers: { 'Authorization': 'Bearer ' + window.ccAuth.getToken() } }); 1528 if (!res.ok) return; 1529 const data = await res.json(); 1530 currentExpenses = data.expenses || []; 1531 } catch (e) { 1532 currentExpenses = []; 1533 } 1534 renderExpenses(); 1535 renderExpenseInsights(); 1536} 1537 1538const escHtml = s => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); 1539 1540function renderExpenses() { 1541 const list = document.getElementById('expense-list'); 1542 const empty = document.getElementById('expense-empty'); 1543 if (!currentExpenses.length) { 1544 list.innerHTML = ''; 1545 empty.style.display = 'block'; 1546 return; 1547 } 1548 empty.style.display = 'none'; 1549 1550 const owned = ownedCardObjects(); 1551 const allCards = window.ALL_CARDS || []; 1552 const profile = prefsToProfile(currentUser.prefs || {}); 1553 const analysis = window.CCEngine.analyzeExpenses(currentExpenses, owned, allCards, profile); 1554 const verdictById = new Map(analysis.perExpenseVerdicts.map(v => [v.id, v])); 1555 1556 list.innerHTML = currentExpenses.map(exp => { 1557 const card = allCards.find(c => c.id === exp.cardId); 1558 const cardName = card ? card.name : 'Unknown card'; 1559 const v = verdictById.get(exp.id); 1560 const dateStr = new Date(exp.occurredAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); 1561 let verdictHtml = ''; 1562 if (v && v.optimal === true) { 1563 verdictHtml = `<div class="expense-verdict optimal"><i class="fas fa-check-circle"></i> Optimal card</div>`; 1564 } else if (v && v.optimal === false) { 1565 verdictHtml = `<div class="expense-verdict suboptimal"><i class="fas fa-exclamation-circle"></i> Could've earned $${v.delta.toFixed(2)} more with ${v.bestOwned.name}</div>`; 1566 } 1567 return ` 1568 <div class="expense-row"> 1569 <div class="expense-main"> 1570 <div class="expense-merchant">${exp.merchant ? escHtml(exp.merchant) : CATEGORY_LABELS[exp.category]}</div> 1571 <div class="expense-meta"><i class="fas ${CATEGORY_ICONS[exp.category]}"></i> ${CATEGORY_LABELS[exp.category]} · ${cardName} · ${dateStr}</div> 1572 ${verdictHtml} 1573 </div> 1574 <div class="expense-amount">$${exp.amount.toFixed(2)}</div> 1575 <button class="expense-delete" onclick="deleteExpense('${exp.id}')"><i class="fas fa-times"></i></button> 1576 </div>`; 1577 }).join(''); 1578} 1579 1580function renderExpenseInsights() { 1581 const card = document.getElementById('expense-insights-card'); 1582 const body = document.getElementById('expense-insights-body'); 1583 const owned = ownedCardObjects(); 1584 const allCards = window.ALL_CARDS || []; 1585 1586 // Trailing 90 days, matching the annualization assumption in analyzeExpenses(). 1587 const cutoff = Date.now() - 90 * 86400000; 1588 const recent = currentExpenses.filter(e => e.occurredAt >= cutoff); 1589 if (!recent.length || !owned.length) { card.style.display = 'none'; return; } 1590 1591 const profile = prefsToProfile(currentUser.prefs || {});
1592 const analysis = window.CCEngine.analyzeExpenses(recent, owned, allCards, profile); 1593 const recs = Object.keys(analysis.categoryRollups) 1594 .map(cat => Object.assign({ cat }, analysis.categoryRollups[cat])) 1595 .filter(r => r.recommendApply) 1596 .sort((a, b) => b.annualDelta - a.annualDelta); 1597 1598 if (!recs.length) { card.style.display = 'none'; return; } 1599 card.style.display = 'block'; 1600 1601 body.innerHTML = recs.map(r => ` 1602 <div class="insight-recommend"> 1603 <div class="insight-recommend-text"> 1604 You spent <strong>$${r.spent.toLocaleString()}</strong> on ${CATEGORY_LABELS[r.cat].toLowerCase()} in the last 90 days â 1605 <strong>${r.bestOverall.name}</strong> would earn ~<strong>$${Math.round(r.annualDelta).toLocaleString()}/year</strong> 1606 more than your best current ${CATEGORY_LABELS[r.cat].toLowerCase()} card${r.bestOwned ? ' (' + r.bestOwned.name + ')' : ''}. 1607 </div> 1608 <button class="insight-apply-btn" onclick="window.open('${window.getApplyUrl ? window.getApplyUrl(r.bestOverall) : r.bestOverall.applyUrl}','_blank','noopener,noreferrer')">Apply Now</button> 1609 </div>`).join(''); 1610} 1611 1612async function logExpense() { 1613 const amount = parseFloat(document.getElementById('ex-amount').value); 1614 const category = document.getElementById('ex-category').value; 1615 const cardId = document.getElementById('ex-card').value; 1616 const dateVal = document.getElementById('ex-date').value; 1617 const merchant = document.getElementById('ex-merchant').value.trim(); 1618 const alert = document.getElementById('expense-alert'); 1619 alert.className = 'inline-alert'; 1620 1621 if (!Number.isFinite(amount) || amount <= 0) { 1622 alert.className = 'inline-alert show error'; 1623 alert.innerHTML = '<i class="fas fa-exclamation-circle"></i> Enter a valid amount.'; 1624 return; 1625 } 1626 if (!cardId) { 1627 alert.className = 'inline-alert show error'; 1628 alert.innerHTML = '<i class="fas fa-exclamation-circle"></i> Select a card.'; 1629 return; 1630 } 1631 const occurredAt = dateVal ? new Date(dateVal + 'T12:00:00').getTime() : Date.now(); 1632 1633 try { 1634 const res = await fetch('/api/expenses', { 1635 method: 'POST', 1636 headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.ccAuth.getToken() }, 1637 body: JSON.stringify({ amount, category, cardId, occurredAt, merchant: merchant || undefined }) 1638 }); 1639 const data = await res.json().catch(() => ({})); 1640 if (!res.ok) throw new Error(data.error || 'Could not log expense'); 1641 1642 currentExpenses.unshift(data.expense); 1643 renderExpenses(); 1644 renderExpenseInsights(); 1645 1646 document.getElementById('ex-amount').value = ''; 1647 document.getElementById('ex-merchant').value = ''; 1648 alert.className = 'inline-alert show success'; 1649 alert.innerHTML = '<i class="fas fa-check-circle"></i> Expense logged!'; 1650 setTimeout(() => alert.classList.remove('show'), 3000); 1651 } catch (e) { 1652 alert.className = 'inline-alert show error'; 1653 alert.innerHTML = '<i class="fas fa-exclamation-circle"></i> ' + (e.message || 'Network error â please try again.'); 1654 } 1655} 1656 1657async function deleteExpense(id) { 1658 if (!confirm('Delete this expense?')) return; 1659 currentExpenses = currentExpenses.filter(e => e.id !== id); 1660 renderExpenses(); 1661 renderExpenseInsights(); 1662 1663 try { 1664 await fetch('/api/expenses/' + encodeURIComponent(id), { 1665 method: 'DELETE', 1666 headers: { 'Authorization': 'Bearer ' + window.ccAuth.getToken() } 1667 }); 1668 } catch (e) {} // list already updated optimistically above 1669} 1670 1671document.addEventListener('click', function(e) { 1672 const wrap = document.querySelector('.wallet-search-wrap'); 1673 if (wrap && !wrap.contains(e.target)) { 1674 const results = document.getElementById('wallet-search-results'); 1675 if (results) results.classList.remove('open'); 1676 } 1677}); 1678 1679const hamburger = document.getElementById('hamburger'); 1680const navLinks = document.getElementById('nav-links'); 1681if (hamburger && navLinks) { 1682 hamburger.addEventListener('click', () => { 1683 const open = hamburger.classList.toggle('open'); 1684 navLinks.classList.toggle('mobile-open', open); 1685 hamburger.setAttribute('aria-expanded', open ? 'true' : 'false'); 1686 }); 1687 document.addEventListener('click', e => { 1688 if (!hamburger.contains(e.target) && !navLinks.contains(e.target)) { 1689 hamburger.classList.remove('open'); 1690 navLinks.classList.remove('mobile-open'); 1691 hamburger.setAttribute('aria-expanded', 'false'); 1692 } 1693 }); 1694} 1695</script>
vendor: 1 bytes, line 1695
1695
1696<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:'a4027cd3bf1c36b2',t:'MTc5MDI2MDIzMg=='};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>
1696<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>
1696 1697</body> 1698</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.