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