PageSourceSearch

https://cardcompass.app/account

html cardcompass.app collected 2026-10-02 01:12:51 UTC 96,087 bytes, 1,695 lines download raw bytes

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 &amp; 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 &amp; 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 &amp; 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&#160;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 &amp; 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} &nbsp;·&nbsp; 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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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]} &nbsp;·&nbsp; ${cardName} &nbsp;·&nbsp; ${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.