PageSourceSearch

https://cardcompass.app/account

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.