PageSourceSearch

https://factozcentral.com/

html factozcentral.com collected 2026-09-24 14:42:35 UTC 760,913 bytes, 13,396 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4<!-- Google tag (gtag.js) -->
vendor: 64 bytes, lines 4-5
4
5<script async src="https://www.googletagmanager.com/gtag/js?id=
5G-ZTZYGKSF6B
vendor: 12 bytes, line 5
5"></script>
6<script>
7  
vendor: 133 bytes, lines 7-10
7window.dataLayer = window.dataLayer || [];
8  function gtag(){dataLayer.push(arguments);}
9  gtag('js', new Date());
10  gtag('config', '
10G-ZTZYGKSF6B
vendor: 6 bytes, lines 10-11
10');
11  
11gtag('config', 'G-L4P5VTL06P');
12</script>
12
13<meta charset="UTF-8">
14<meta name="viewport" content="width=device-width, initial-scale=1.0">
15<title>Factoz Command Center</title>
16<meta name="description" content="Factoz Command Center — finance content automation platform for creators and trading desks. Research signals, AI captions, TradingView charts and scheduled social posting.">
17<meta name="robots" content="index, follow">
18<link rel="canonical" href="https://factozcentral.com">
19<meta property="og:type"        content="website">
20<meta property="og:url"         content="https://factozcentral.com">
21<meta property="og:title"       content="Factoz Command Center — Finance Content Automation">
22<meta property="og:description" content="Research signals, AI captions, charts and scheduled posting — all in one platform for finance creators and trading desks.">
23<meta property="og:site_name"   content="Factoz">
24<meta name="twitter:card"        content="summary_large_image">
25<meta name="twitter:title"       content="Factoz Command Center — Finance Content Automation">
26<meta name="twitter:description" content="Research signals, AI captions, charts and scheduled posting — all in one platform.">
27<script type="application/ld+json">
28{
29  "@context": "https://schema.org",
30  "@type": "WebApplication",
31  "name": "Factoz Command Center",
32  "url": "https://factozcentral.com",
33  "description": "Finance content automation platform for creators and trading desks.",
34  "applicationCategory": "FinanceApplication",
35  "operatingSystem": "Web",
36  "offers": {
37    "@type": "Offer",
38    "price": "25",
39    "priceCurrency": "USD"
40  }
41}
42</script>
42
43<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
44<link href="https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
45<style>
46:root{--bg:#0b132b;--bg2:#0f1a36;--bg3:#131f40;--bg4:#1a274d;--border:#202e56;--teal:#E2C275;--green:#00e676;--red:#ff4444;--yellow:#ffd600;--muted:#3d4f63;--text:#c5d0dc;--text2:#7a8fa3;--mono:'Space Mono',monospace;--sans:'Syne',sans-serif}
47*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;overflow:hidden}
48body{background:var(--bg);color:var(--text);font-family:var(--sans);display:flex}
49#sidebar{width:210px;min-width:210px;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow-y:auto}
50.logo{padding:22px 18px 18px;border-bottom:1px solid var(--border)}
51.logo-title{font-family:var(--mono);font-size:.9rem;font-weight:700;color:var(--teal);letter-spacing:4px}
52.logo-sub{font-size:.58rem;color:var(--muted);letter-spacing:3px;margin-top:3px;font-family:var(--mono)}
53nav{padding:12px 0;flex:1}
54.nav-section{font-size:.62rem;font-weight:700;letter-spacing:1.5px;color:var(--text2);padding:12px 16px 4px;font-family:var(--mono);cursor:pointer;user-select:none;display:flex;justify-content:space-between;align-items:center}
55.nav-section::after{content:'▾';font-size:.6rem;opacity:.5;letter-spacing:0}
56.nav-section.collapsed::after{content:'▸'}
57.nav-section-items.collapsed{display:none}
58.nav-section-noc::after{display:none}
59.nav-item{display:flex;align-items:center;gap:8px;padding:6px 16px;cursor:pointer;font-size:.74rem;color:var(--text2);transition:all .15s;border-left:2px solid transparent;font-weight:500;letter-spacing:.2px}
60.nav-item:hover{background:var(--bg3);color:var(--text)}
61.nav-item.active{background:rgba(226,194,117,.07);color:var(--teal);border-left-color:var(--teal)}
62.nav-item.admin-item{color:#ffd600}
63.nav-item.admin-item:hover{background:rgba(255,214,0,.05)}
64.nav-item.admin-item.active{background:rgba(255,214,0,.07);color:var(--yellow);border-left-color:var(--yellow)}
65.nav-item svg{width:13px;height:13px;stroke-width:1.75;flex-shrink:0;opacity:.6;transition:opacity .15s}
66.nav-item:hover svg,.nav-item.active svg{opacity:1}
67.nav-item.admin-item svg{opacity:.8}
68.nav-icon{display:flex;align-items:center;flex-shrink:0}
69.sidebar-footer{padding:14px 18px;border-top:1px solid var(--border);font-size:.65rem;color:var(--muted);font-family:var(--mono)}
70#clock{color:var(--text2);margin-bottom:4px}
71#user-tag{color:var(--teal);font-size:.62rem;margin-bottom:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
72#tier-tag{font-size:.58rem;margin-bottom:8px;font-family:var(--mono)}
73#xp-widget{display:none;margin-bottom:10px;padding:8px 10px;background:var(--bg3);border:1px solid var(--border);border-radius:8px}
74#xp-widget .xp-row{display:flex;justify-content:space-between;align-items:center}
75#xp-progress-track{height:5px;background:var(--bg2);border-radius:3px;overflow:hidden;margin:5px 0 4px}
76#xp-progress-bar{height:100%;background:var(--teal);width:0%;transition:width .3s ease}
77.logout-btn{cursor:pointer;color:var(--muted);font-size:.62rem;transition:color .15s}
78.logout-btn:hover{color:var(--red)}
79#main{flex:1;overflow-y:auto;padding:26px 30px;background:var(--bg)}
80.page{display:none}.page.active{display:block;animation:fadeIn .2s ease}
81@keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
82/* ── Overview Bento Layout ── */
83.ov-stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;margin-bottom:14px}
84.ov-stat{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:16px 18px;border-left:3px solid var(--teal)}
85.ov-stat .ov-stat-v{font-size:1.8rem;font-weight:700;color:#e2eaf2;font-family:var(--mono);line-height:1}
86.ov-stat .ov-stat-l{font-size:.58rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-top:6px}
87.ov-stat .ov-stat-s{font-size:.68rem;color:var(--text2);margin-top:2px}
88.ad-slot{line-height:0;}.ad-slot a{display:block;}.ad-slot img{width:100%;border-radius:8px;display:block;}
89.ov-bento{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:0}
90.ov-bento-wide{grid-column:1/-1}
91.ov-card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px 16px}
92.ov-card-hdr{display:flex;align-items:center;gap:4px;margin-bottom:10px}
93.ov-card-hdr>span{font-size:.58rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;font-weight:700}
94.ov-card-hdr>a{margin-left:auto;font-size:.58rem;color:var(--teal);font-family:var(--mono);cursor:pointer;letter-spacing:1px;text-decoration:none}
95.ov-card-hdr>button{background:none;border:none;color:var(--text2);font-size:.8rem;cursor:pointer;padding:0 2px;line-height:1}
96.ov-card-hdr>button:first-of-type{margin-left:auto}
97@keyframes skelPulse{0%,100%{opacity:.3}50%{opacity:.55}}
98.ov-skel{height:11px;background:var(--bg3);border-radius:4px;animation:skelPulse 1.5s ease infinite;margin-bottom:7px}
99.ov-skel-sm{height:8px;width:55%}
100.page-header{margin-bottom:24px}
101.page-title{font-size:1.2rem;font-weight:800;color:#e2eaf2;display:flex;align-items:center;gap:12px;letter-spacing:-.3px}
102.page-title svg{width:22px;height:22px;stroke-width:1.75;flex-shrink:0;opacity:.85}
103.page-title lord-icon{flex-shrink:0}
104.pm-tab{color:var(--text2)!important;border-color:transparent!important;background:transparent!important}
105.pm-tab-active{color:var(--teal)!important;border-color:var(--teal)!important;background:rgba(0,200,83,.07)!important}
106.step-badge{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;background:rgba(226,194,117,.1);border:1px solid rgba(226,194,117,.3);border-radius:50%;font-family:var(--mono);font-size:.75rem;color:var(--teal);flex-shrink:0}
107.page-sub{font-size:.78rem;color:var(--text2);margin-top:5px;margin-left:40px;font-weight:400}
108.g2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
109.scanner-grid{display:grid;grid-template-columns:440px 1fr;gap:18px}
110.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px}
111.g4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
112.card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:16px 18px;margin-bottom:14px}
113.card h4{color:var(--teal);font-size:.72rem;margin-bottom:6px;font-family:var(--mono);letter-spacing:1px}
114.card p{color:var(--text2);font-size:.78rem;line-height:1.55}
115.stat-card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:18px}
116.stat-card .lbl{font-size:.6rem;color:var(--text2);font-family:var(--mono);letter-spacing:2px;margin-bottom:8px}
117.stat-card .val{font-size:1.7rem;font-weight:700;color:#e2eaf2;font-family:var(--mono)}
118.stat-card .sub{font-size:.7rem;color:var(--muted);margin-top:3px}
119.btn{display:inline-flex;align-items:center;gap:7px;padding:8px 16px;border-radius:7px;font-size:.78rem;font-weight:700;cursor:pointer;border:none;transition:all .15s;font-family:var(--sans);letter-spacing:.3px}
120.btn-primary{background:rgba(226,194,117,.12);color:var(--teal);border:1px solid rgba(226,194,117,.35)}
121.btn-primary:hover{background:rgba(226,194,117,.22);border-color:var(--teal)}
122.btn-green{background:rgba(0,230,118,.1);color:var(--green);border:1px solid rgba(0,230,118,.3)}
123.btn-green:hover{background:rgba(0,230,118,.18)}
124.btn-yellow{background:rgba(255,214,0,.1);color:var(--yellow);border:1px solid rgba(255,214,0,.3)}
125.btn-yellow:hover{background:rgba(255,214,0,.18)}
126.btn-red{background:rgba(255,68,68,.1);color:var(--red);border:1px solid rgba(255,68,68,.3)}
127.btn-red:hover{background:rgba(255,68,68,.18)}
128.btn-ghost{background:transparent;color:var(--text2);border:1px solid var(--border)}
129.btn-ghost:hover{background:var(--bg3);color:var(--text)}
130.btn:disabled{opacity:.35;cursor:not-allowed}
131.btn-full{width:100%;justify-content:center}
132.form-group{margin-bottom:12px}
133.form-label{display:block;font-size:.62rem;color:var(--text2);margin-bottom:5px;font-family:var(--mono);letter-spacing:1px}
134input,textarea,select{width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:8px 11px;color:var(--text);font-size:.82rem;font-family:var(--sans);outline:none;transition:border-color .15s}
135input:focus,textarea:focus,select:focus{border-color:var(--teal)}
136textarea{resize:vertical;min-height:80px;line-height:1.55}
137select option{background:var(--bg2)}
138input[type=file]{padding:6px;font-size:.75rem;color:var(--text2)}
139input[type=color]{width:36px;height:34px;padding:2px;border-radius:6px;cursor:pointer}
140input[type=checkbox]{width:auto}
141.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 9px;border-radius:20px;font-size:.68rem;font-weight:700;font-family:var(--mono)}
142.badge-green{background:rgba(0,230,118,.1);color:var(--green)}
143.badge-teal{background:rgba(226,194,117,.1);color:var(--teal)}
144.badge-yellow{background:rgba(255,214,0,.1);color:var(--yellow)}
145.badge-red{background:rgba(255,68,68,.1);color:var(--red)}
146.badge-muted{background:var(--bg3);color:var(--text2)}
147.terminal{background:#020509;border:1px solid var(--border);border-radius:8px;padding:12px 14px;font-family:var(--mono);font-size:.73rem;color:var(--green);max-height:260px;overflow-y:auto;white-space:pre-wrap;line-height:1.65;margin-top:10px}
148hr{border:none;border-top:1px solid var(--border);margin:18px 0}
149.spin{display:inline-block;width:13px;height:13px;border:2px solid rgba(226,194,117,.2);border-top-color:var(--teal);border-radius:50%;animation:spin .7s linear infinite}
150@keyframes spin{to{transform:rotate(360deg)}}
151.pm-scan-badges{display:flex;flex-wrap:wrap;gap:6px}
152.pm-badge{padding:4px 10px;border-radius:20px;font-size:.68rem;font-weight:700;font-family:var(--mono);cursor:pointer;transition:opacity .15s}
153.pm-badge:hover{opacity:.8}
154.pm-badge.bullish{background:rgba(0,230,118,.1);color:var(--green);border:1px solid rgba(0,230,118,.4)}
155.pm-badge.bearish{background:rgba(255,68,68,.1);color:var(--red);border:1px solid rgba(255,68,68,.4)}
156.pm-badge.neutral{background:var(--bg3);color:var(--muted);border:1px solid var(--border);cursor:default}
157@keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
158.wiz-bar{display:flex;align-items:center;gap:0;margin-bottom:14px}
159.wiz-step{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:60px}
160.wiz-num{width:26px;height:26px;border-radius:50%;background:var(--bg3);border:2px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:.72rem;font-weight:700;color:var(--muted);font-family:var(--mono)}
161.wiz-label{font-size:.62rem;color:var(--muted);font-family:var(--mono);white-space:nowrap}
162.wiz-line{flex:1;height:2px;background:var(--border);min-width:20px}
163.wiz-step.active .wiz-num{background:var(--teal);border-color:var(--teal);color:#000}
164.wiz-step.active .wiz-label{color:var(--teal);font-weight:700}
165.wiz-step.done .wiz-num{background:var(--green);border-color:var(--green);color:#000}
166.wiz-step.done .wiz-label{color:var(--green)}
167#toast{position:fixed;bottom:22px;right:22px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:11px 16px;font-size:.78rem;color:var(--text);transform:translateY(70px);opacity:0;transition:all .25s;z-index:999;max-width:280px;font-family:var(--mono)}
168#toast.show{transform:translateY(0);opacity:1}
169#toast.ok{border-color:var(--green);color:var(--green)}
170#toast.err{border-color:var(--red);color:var(--red)}
171#auth{position:fixed;inset:0;z-index:100;background:var(--bg);display:flex;align-items:center;justify-content:center}
172.auth-box{background:var(--bg2);border:1px solid var(--border);border-top:2px solid var(--teal);border-radius:14px;padding:38px;width:380px}
173.auth-logo{font-family:var(--mono);font-size:1.55rem;color:var(--teal);letter-spacing:6px;text-align:center;margin-bottom:6px;font-weight:700}
174.auth-sub{text-align:center;font-size:.6rem;color:var(--muted);margin-bottom:8px;letter-spacing:2px;font-family:var(--mono)}
175.auth-tagline{text-align:center;font-size:.75rem;color:var(--text2);margin-bottom:20px;line-height:1.4}
176.auth-pills{display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin-bottom:18px}
177.auth-pill{background:var(--bg3);color:var(--teal);border:1px solid var(--border);border-radius:20px;padding:4px 10px;font-size:.6rem;letter-spacing:1px;font-family:var(--mono)}
178.auth-tabs{display:flex;gap:3px;margin-bottom:20px;background:var(--bg3);padding:3px;border-radius:8px}
179.auth-tab{flex:1;text-align:center;padding:7px;border-radius:6px;cursor:pointer;font-size:.78rem;color:var(--text2);font-weight:700;transition:all .15s}
180.auth-tab.active{background:var(--bg2);color:var(--teal)}
181.lang-toggle{display:flex;gap:3px;background:var(--bg3);padding:3px;border-radius:8px;border:1px solid var(--border)}
182.lang-toggle-btn{flex:1;text-align:center;padding:4px 8px;border-radius:6px;cursor:pointer;font-size:.62rem;letter-spacing:.05em;font-family:var(--mono);color:var(--text2);font-weight:700;background:transparent;border:none;transition:all .15s}
183.lang-toggle-btn.active{background:var(--bg2);color:var(--teal)}
184.lang-toggle-auth{margin:0 auto 14px;max-width:120px}
185.lang-toggle-sidebar{margin-bottom:8px}
186#quiz-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:210;display:none;align-items:center;justify-content:center}
187#quiz-modal-overlay.show{display:flex}
188.quiz-modal{width:600px}
189.quiz-topic-badge{display:inline-block;background:var(--bg3);color:var(--teal);border:1px solid var(--border);border-radius:20px;padding:3px 11px;font-size:.62rem;letter-spacing:1px;font-family:var(--mono);text-transform:uppercase;margin-bottom:14px}
190#quiz-lesson-content{font-size:.85rem;line-height:1.65;color:var(--text2);margin-bottom:16px}
191.quiz-unlock-note{display:none;background:var(--bg3);border:1px solid var(--border);border-left:2px solid var(--teal);border-radius:6px;padding:9px 12px;font-size:.72rem;color:var(--text2);margin-bottom:20px}
192.quiz-question{margin-bottom:22px}
193.quiz-question-text{font-size:.82rem;font-weight:700;margin-bottom:10px;line-height:1.4}
194.quiz-choices{display:flex;flex-direction:column;gap:7px}
195.quiz-choice{display:flex;align-items:center;gap:10px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:9px 12px;font-size:.78rem;cursor:pointer;transition:all .15s}
196.quiz-choice:hover{border-color:var(--teal)}
197.quiz-choice.selected{border-color:var(--teal);background:rgba(45,212,191,.08);color:var(--teal)}
198.quiz-choice-letter{font-family:var(--mono);font-weight:700;font-size:.68rem;color:var(--muted);width:16px;flex-shrink:0}
199.quiz-choice.selected .quiz-choice-letter{color:var(--teal)}
200.quiz-score{text-align:center;font-size:1.6rem;font-weight:800;margin-bottom:6px}
201.quiz-stats-row{display:flex;justify-content:center;gap:28px;margin-bottom:16px}
202.quiz-stats-row>div{text-align:center}
203.quiz-stat-num{display:block;font-size:1.05rem;font-weight:800;font-family:var(--mono)}
204.quiz-stat-label{display:block;font-size:.6rem;color:var(--muted);letter-spacing:.5px;text-transform:uppercase;margin-top:2px}
205.quiz-levelup{text-align:center;background:rgba(45,212,191,.1);border:1px solid var(--teal);border-radius:8px;padding:10px;font-size:.85rem;font-weight:700;color:var(--teal);margin-bottom:18px}
206.quiz-review{display:flex;flex-direction:column;gap:12px;margin-bottom:18px}
207.quiz-review-item{border-radius:8px;padding:10px 12px;border:1px solid var(--border);background:var(--bg3)}
208.quiz-review-item.correct{border-left:3px solid var(--green)}
209.quiz-review-item.incorrect{border-left:3px solid var(--red)}
210.quiz-review-q{font-size:.76rem;font-weight:700;margin-bottom:5px}
211.quiz-review-answer{font-size:.75rem;color:var(--text2);margin-bottom:4px}
212.quiz-review-explanation{font-size:.7rem;color:var(--muted);line-height:1.4}
213#auth-err{color:var(--red);font-size:.72rem;margin-top:8px;min-height:16px;font-family:var(--mono)}
214.chart-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:12px}
215.chart-thumb{background:var(--bg3);border:1px solid var(--border);border-radius:6px;overflow:hidden;position:relative;cursor:pointer;transition:border-color .15s}
216.chart-thumb:hover{border-color:var(--teal)}
217.chart-thumb img{width:100%;display:block}
218.chart-thumb .clabel{font-size:.62rem;color:var(--muted);padding:3px 6px;text-align:center;font-family:var(--mono)}
219.sig-bull{position:absolute;top:4px;left:4px;background:rgba(0,230,118,.85);color:#000;font-size:.58rem;font-weight:700;padding:2px 5px;border-radius:4px;font-family:var(--mono)}
220.sig-bear{position:absolute;top:4px;left:4px;background:rgba(255,68,68,.85);color:#fff;font-size:.58rem;font-weight:700;padding:2px 5px;border-radius:4px;font-family:var(--mono)}
221/* Guide accordion */
222.guide-section{border:1px solid var(--border);border-radius:8px;margin-bottom:10px;overflow:hidden}
223.guide-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;cursor:pointer;background:var(--bg2);font-size:.82rem;font-weight:600;color:var(--text);user-select:none}
224.guide-header:hover{background:var(--bg3)}
225.guide-arrow{font-size:.7rem;color:var(--muted);transition:transform .2s}
226.guide-arrow.open{transform:rotate(90deg)}
227.guide-body{padding:16px;background:var(--bg);font-size:.78rem;color:var(--text2);line-height:1.7}
228.guide-steps{padding-left:20px;margin:0}
229.guide-steps li{margin-bottom:8px}
230.guide-note{background:#1e2a1e;border:1px solid #2a4a2a;border-radius:6px;padding:8px 12px;font-size:.75rem;color:#8bc88b}
231/* AI Chat */
232.chat-chip{background:var(--bg2);border:1px solid var(--border);color:var(--text2);border-radius:20px;padding:3px 10px;font-size:.72rem;cursor:pointer;transition:border-color .15s,color .15s}
233.chat-chip:hover{border-color:var(--teal);color:var(--text)}
234.chat-msg{padding:8px 12px;border-radius:8px;font-size:.78rem;line-height:1.55;max-width:88%;word-break:break-word}
235.chat-msg.user{background:#1e3a5f;align-self:flex-end;color:#d1d4dc}
236.chat-msg.ai{background:var(--bg2);border:1px solid var(--border);align-self:flex-start;color:var(--text)}
237.chat-msg.err{background:#3a1a1a;border:1px solid #a33;align-self:flex-start;color:#f88}
238.template-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:16px}
239.tmpl-card{background:var(--bg3);border:2px solid var(--border);border-radius:10px;padding:16px;cursor:pointer;transition:all .15s;text-align:center}
240.tmpl-card:hover{border-color:var(--text2)}
241.tmpl-card.selected{border-color:var(--teal);background:rgba(226,194,117,.05)}
242.tmpl-card .tmpl-icon{font-size:1.8rem;margin-bottom:8px}
243.tmpl-card .tmpl-name{font-size:.8rem;font-weight:700;color:#e2eaf2;margin-bottom:4px}
244.tmpl-card .tmpl-desc{font-size:.68rem;color:var(--text2);line-height:1.4}
245.tmpl-thumb-overlay{position:absolute;inset:0;border-radius:inherit;background:linear-gradient(to top,rgba(0,0,0,.8) 0%,transparent 55%);display:flex;flex-direction:column;justify-content:flex-end;padding:8px}
246.tmpl-thumb-overlay .tmpl-name{font-size:.8rem;font-weight:700;color:#e2eaf2;margin:0}
247.tmpl-thumb-overlay .tmpl-desc{font-size:.62rem;color:rgba(255,255,255,.7);margin:0}
248.carousel-preview{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:14px}
249.slide-thumb{background:var(--bg3);border:1px solid var(--border);border-radius:6px;overflow:hidden;aspect-ratio:1}
250.slide-thumb img{width:100%;height:100%;object-fit:cover}
251.swatch{width:28px;height:28px;border-radius:50%;cursor:pointer;border:2px solid transparent;transition:border-color .15s}
252.swatch.selected{border-color:var(--accent)}
253.file-row{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg3);border:1px solid var(--border);border-radius:7px;margin-bottom:6px;transition:border-color .15s}
254.file-row:hover{border-color:var(--text2)}
255.file-row .fname{flex:1;font-size:.78rem;color:var(--text);font-family:var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
256.file-row .fsize{font-size:.65rem;color:var(--muted);white-space:nowrap}
257.hist-row{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:6px}
258.hist-row .hplatform{font-size:.78rem;font-weight:700;color:var(--text);min-width:90px}
259.hist-row .hcaption{flex:1;font-size:.72rem;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
260.hist-row .htime{font-size:.65rem;color:var(--muted);font-family:var(--mono);white-space:nowrap}
261.caption-box{background:var(--bg3);border:1px solid var(--border);border-radius:8px;paddi
261ng:14px;font-size:.78rem;line-height:1.75;white-space:pre-wrap;color:var(--text);min-height:100px;font-family:var(--mono)}
262.color-row{display:flex;align-items:center;gap:8px}
263.color-row input[type=text]{flex:1}
264/* ADMIN SPECIFIC */
265.client-card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:16px 18px;margin-bottom:10px;transition:border-color .15s}
266.client-card:hover{border-color:var(--text2)}
267.client-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
268.client-name{font-size:.88rem;font-weight:700;color:#e2eaf2}
269.client-meta{font-size:.68rem;color:var(--text2);font-family:var(--mono)}
270.client-stats{display:flex;gap:12px;margin-top:8px}
271.client-stat{font-size:.68rem;color:var(--muted);font-family:var(--mono)}
272.client-stat span{color:var(--text2)}
273/* MODAL */
274#modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:200;display:none;align-items:center;justify-content:center}
275#modal-overlay.show{display:flex}
276.modal{background:var(--bg2);border:1px solid var(--border);border-radius:14px;padding:28px;width:560px;max-height:85vh;overflow-y:auto}
277.modal-title{font-size:1rem;font-weight:800;color:#e2eaf2;margin-bottom:20px;display:flex;align-items:center;justify-content:space-between}
278.modal-close{cursor:pointer;color:var(--muted);font-size:1.1rem;transition:color .15s}
279.modal-close:hover{color:var(--red)}
280.modal-tabs{display:flex;gap:3px;margin-bottom:18px;background:var(--bg3);padding:3px;border-radius:8px}
281.modal-tab{flex:1;text-align:center;padding:7px;border-radius:6px;cursor:pointer;font-size:.75rem;color:var(--text2);font-weight:700;transition:all .15s}
282.modal-tab.active{background:var(--bg2);color:var(--teal)}
283/* API KEY DISPLAY */
284.api-key-box{background:var(--bg3);border:1px solid var(--green);border-radius:8px;padding:14px;font-family:var(--mono);font-size:.75rem;color:var(--green);word-break:break-all;margin:10px 0}
285::-webkit-scrollbar{width:4px}
286::-webkit-scrollbar-track{background:transparent}
287::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
288/* NEWS FEED */
289.nav-badge{margin-left:auto;background:var(--red);color:#fff;border-radius:10px;font-size:.52rem;padding:1px 6px;font-family:var(--mono);font-weight:700;line-height:1.6}
290.nav-item-locked{opacity:.45}
291.nav-item-locked:hover{opacity:.7}
292.nav-lock{margin-left:auto;font-size:.68rem;opacity:.8;flex-shrink:0}
293@keyframes navShine{0%{background-position:-150% 0}100%{background-position:250% 0}}
294.nav-item-shine{background-image:linear-gradient(110deg,transparent 35%,rgba(45,212,191,.4) 50%,transparent 65%);background-size:250% 100%;animation:navShine 1.4s ease-in-out 2}
295@keyframes badgePulse{0%,100%{transform:scale(1);box-shadow:0 0 0 0 rgba(99,179,237,.7)}50%{transform:scale(1.15);box-shadow:0 0 0 6px rgba(99,179,237,0)}}
296.badge-pulse{animation:badgePulse 1.4s ease-in-out infinite}
297.badge-purple{background:rgba(180,120,255,.12);color:#c084fc}
298.badge-orange{background:rgba(255,160,60,.12);color:#fb923c}
299.news-filter-btn.active-filter{background:rgba(226,194,117,.12);color:var(--teal);border-color:rgba(226,194,117,.35)}
300.news-card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:14px;display:flex;gap:0;transition:border-color .15s}
301.news-card:hover{border-color:var(--muted)}
302.tv-chart-expanded{max-height:600px!important;cursor:zoom-out!important;object-fit:contain!important}
303.cluster-section{margin-bottom:24px}
304.cluster-header{display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;cursor:pointer;user-select:none;margin-bottom:6px;transition:background .15s}
305.cluster-header:hover{background:var(--bg3)}
306.cluster-arrow{font-size:.7rem;color:var(--muted);transition:transform .2s;margin-left:auto}
307.cluster-arrow.open{transform:rotate(90deg)}
308.cluster-body{display:none}
309.cluster-body.open{display:block;padding-left:14px}
310.cluster-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border:1px solid var(--border);border-radius:7px;margin-bottom:8px;
310cursor:pointer;background:var(--bg2);transition:background .15s}
311.cluster-row:hover{background:var(--bg3)}
312.cluster-row-title{flex:1;font-size:.87rem;font-weight:700;color:var(--teal);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
313.cluster-row-date{font-size:.65rem;color:#8b9cb8;white-space:nowrap}
314.cluster-detail{display:none;margin-bottom:8px}
315.cluster-detail.open{display:block}
316.news-thumb{width:150px;min-width:150px;height:auto;max-height:220px;object-fit:contain;background:var(--bg3);display:block}
317.news-body{padding:14px 16px;flex:1;min-width:0}
318.news-title{font-weight:700;color:#e2eaf2;font-size:.88rem;margin-bottom:6px;line-height:1.35}
319.news-date{font-size:.6rem;color:#8b9cb8;font-family:var(--mono);margin-bottom:8px}
320.news-sum{font-size:.75rem;color:var(--text2);line-height:1.55;margin-bottom:10px}
321.news-caption{font-size:.72rem;color:var(--muted);font-family:var(--mono);background:var(--bg3);border:1px solid var(--border);border-radius:5px;padding:8px;margin-bottom:10px;max-height:56px;overflow:hidden;line-height:1.5}
322.news-actions{display:flex;gap:8px;flex-wrap:wrap}
323/* ADMIN NEWS LIST */
324.news-admin-row{display:flex;align-items:center;gap:12px;padding:9px 12px;background:var(--bg3);border-radius:7px;margin-bottom:6px;font-size:.75rem}
325.news-admin-title{flex:1;color:#e2eaf2;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
326.news-admin-date{color:var(--muted);font-family:var(--mono);font-size:.62rem;white-space:nowrap}
327
328/* ── MOBILE RESPONSIVE ─────────────────────────────────────────────────── */
329#mob-bar{display:none}
330@media(max-width:768px){
331  html,body{overflow:auto}
332  /* Mobile top bar */
333  #mob-bar{display:flex;align-items:center;justify-content:space-between;
334    position:sticky;top:0;z-index:200;background:var(--bg2);
335    border-bottom:1px solid var(--border);padding:10px 16px;width:100%}
336  #mob-bar .mob-logo{font-family:var(--mono);font-size:.8rem;font-weight:700;
337    color:var(--teal);letter-spacing:3px}
338  #mob-hamburger{background:none;border:1px solid var(--border);border-radius:6px;
339    color:var(--text);font-size:1.1rem;padding:4px 9px;cursor:pointer;line-height:1}
340  /* Body becomes column on mobile */
341  body{flex-direction:column}
342  /* Sidebar becomes overlay */
343  #sidebar{position:fixed;top:0;left:0;bottom:0;z-index:300;width:220px;
344    transform:translateX(-100%);transition:transform .25s ease;box-shadow:4px 0 24px rgba(0,0,0,.5)}
345  #sidebar.mob-open{transform:translateX(0)}
346  /* Sidebar overlay backdrop */
347  #mob-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:299}
348  #mob-backdrop.visible{display:block}
349  /* Main fills full width */
350  #main{padding:16px 14px;width:100%;overflow-y:auto}
351  /* Collapse multi-col grids */
352  .g2,.g3,.g4,.scanner-grid,.ov-bento,.ov-stats{grid-template-columns:1fr}
353  .ov-bento-wide{grid-column:1}
354  .chart-grid{grid-template-columns:1fr 1fr}
355  .template-grid{grid-template-columns:1fr 1fr}
356  .carousel-preview{grid-template-columns:1fr}
357  /* Auth box full width */
358  #auth .auth-box{width:92vw;padding:32px 22px}
359  .auth-logo{font-size:1.35rem}
360  /* Modals full width */
361  .modal{width:94vw;padding:20px 16px}
362  /* Tables scroll horizontally */
363  .tbl-wrap,table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}
364  /* Reduce page title size */
365  .page-title{font-size:1rem}
366  /* Two-col carousel build layout stacks */
367  .carousel-build-layout{grid-template-columns:1fr!important}
368  /* News cards stack vertically — 150px fixed thumb overflows on 375px */
369  .news-card{flex-direction:column}
370  .news-thumb{width:100%;min-width:0;max-height:160px;object-fit:cover}
371  /* TradingView charts — 700px height is excessive on phone */
372  #heatmap-pane-stock{height:400px!important}
373  #tv-chart-wrap{height:380px!important}
374  /* Design Lab gallery modal — 40px auto margin clips on narrow screens */
375  #dl-gallery-modal > div{margin:10px!important;padding:16px!important}
376  /* Macro panes — cap height so one pane doesn't dominate the whole screen */
377  .macro-pane{max-height:480px;overflow-y:auto}
378  /* Buttons — minimum 38px tap target */
379  .btn{min-height:38px}
380}
381@media(max-width:480px){
382  .g2{grid-template-columns:1fr}
383  .chart-grid{grid-template-columns:1fr}
384  .template-grid{grid-template-columns:1fr}
385  #main{padding:12px 10px}
386  /* Form groups — allow narrower min-width on small phones so they wrap properly */
387  .form-group[style*="min-width"]{min-width:140px!important}
388}
389/* ── Macro Dashboard ── */
390.macro-tabs{display:flex;gap:6px;margin-bottom:18px;flex-wrap:wrap}
391.macro-tab{background:var(--bg2);border:1px solid var(--border);color:var(--text2);font-family:var(--mono);font-size:.72rem;padding:5px 14px;border-radius:6px;cursor:pointer;letter-spacing:.5px}
392.macro-tab.active{background:var(--teal);border-color:var(--teal);color:#000;font-weight:700}
393.macro-pane{display:none}
394.macro-pane.active{display:block}
395.macro-card{background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px 16px}
396.macro-card .mc-label{font-size:.6rem;color:var(--text2);font-family:var(--mono);letter-spacing:2px;margin-bottom:6px}
397.macro-card .mc-val{font-size:1.4rem;font-weight:700;font-family:var(--mono);color:#e2eaf2}
398.macro-card .mc-sub{font-size:.68rem;color:var(--muted);margin-top:3px}
399.mc-badge{display:inline-block;border-radius:4px;font-size:.6rem;font-family:var(--mono);font-weight:700;letter-spacing:.5px;padding:2px 8px;margin-top:6px}
400.mc-badge.ok{background:rgba(0,180,100,.15);color:#00b464;border:1px solid #00b464}
401.mc-badge.neutral{background:rgba(150,150,150,.1);color:var(--text2);border:1px solid var(--border)}
402.mc-badge.alert{background:rgba(220,50,50,.15);color:#e05050;border:1px solid #e05050}
403.macro-scorecard-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:12px}
404#mc-tooltip,#hm-tooltip{position:fixed;z-index:9999;background:#1C2128;border:1px solid #30363D;border-radius:6px;padding:10px 14px;font-size:.72rem;color:#E8F0F7;pointer-events:none;min-width:175px;box-shadow:0 4px 16px rgba(0,0,0,.5)}
405#hm-tooltip .hmt-sector{font-size:.55rem;letter-spacing:2px;color:#8B949E;margin-bottom:7px;font-family:var(--mono)}
406#hm-tooltip .hmt-label{font-size:.58rem;color:#8B949E;margin:6px 0 3px;font-family:var(--mono);letter-spacing:1px}
407#hm-tooltip .hmt-row{display:flex;justify-content:space-between;gap:14px;padding:2px 0}
408#hm-tooltip .hmt-ticker{font-weight:600}
409#hm-tooltip .hmt-up{color:#4caf82}
410#hm-tooltip .hmt-dn{color:#ef5350}
411#mc-tooltip .mct-title{font-size:.55rem;letter-spacing:2px;color:#8B949E;margin-bottom:7px;font-family:var(--mono)}
412#mc-tooltip .mct-row{display:flex;justify-content:space-between;gap:16px;padding:3px 0;border-bottom:1px solid #21262D}
413#mc-tooltip .mct-row:last-child{border-bottom:none}
414#mc-tooltip .mct-date{color:#8B949E}
415#mc-tooltip .mct-val{font-weight:600;color:#E8F0F7}
416@media(max-width:900px){.macro-scorecard-grid{grid-template-columns:repeat(2,1fr)}}
417@media(max-width:480px){.macro-scorecard-grid{grid-template-columns:1fr}}
418</style>
419<script src="https://cdn.jsdelivr.net/npm/[email protected]/i18next.min.js"></script>
vendor: 1 bytes, line 419
419
420<script src="https://unpkg.com/feather-icons/dist/feather.min.js"></script>
vendor: 1 bytes, line 420
420
421<script src="https://cdn.lordicon.com/lordicon.js"></script>
vendor: 1 bytes, line 421
421
422<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.1/fabric.min.js"></script>
vendor: 1 bytes, line 422
422
423<script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
423
424</head>
425<body>
426
427<!-- MOBILE TOP BAR -->
428<div id="mob-bar">
429  <span class="mob-logo">FACTOZ</span>
430  <button id="mob-hamburger" onclick="toggleSidebar()">☰</button>
431</div>
432<div id="mob-backdrop" onclick="toggleSidebar()"></div>
433
434<!-- AUTH -->
435<div id="auth">
436  <div class="auth-box">
437    <div class="lang-toggle lang-toggle-auth">
438      <button type="button" class="lang-toggle-btn" data-lang="es" onclick="setLocale('es')">ES</button>
439      <button type="button" class="lang-toggle-btn" data-lang="en" onclick="setLocale('en')">EN</button>
440    </div>
441    <div class="auth-logo">FACTOZ</div>
442    <div class="auth-sub" data-i18n="auth.sub">CENTRO DE COMANDO</div>
443    <div class="auth-tagline" data-i18n="auth.tagline">Aprende, avanza y supérate con Factoz hasta convertirte en experto</div>
444    <div class="auth-pills">
445      <span class="auth-pill" data-i18n="auth.pill_scanner">⚡ Scanner</span>
446      <span class="auth-pill" data-i18n="auth.pill_learn">🎓 Aprende gratis</span>
447      <span class="auth-pill" data-i18n="auth.pill_levelup">⭐ Sube de nivel</span>
448    </div>
449    <a href="https://chromewebstore.google.com/detail/factoz-intelligence-widget/jbjfefeinkalicgplonblljjpmfmcbpc" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:6px;margin:10px auto 4px;text-decoration:none;background:#1a2a1a;border:1px solid #4caf50;border-radius:20px;padding:5px 14px;font-size:.7rem;color:#4caf50;font-weight:600;letter-spacing:.02em">
450      <svg width="13" height="13" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" stroke="#4caf50" stroke-width="2"/><circle cx="12" cy="12" r="4" fill="#4caf50"/><path d="M12 8h8.5M5.5 18.5l4-7M5.5 5.5l4 7" stroke="#4caf50" stroke-width="2"/></svg>
451      <span data-i18n="auth.install_extension">Instalar gratis en Chrome</span>
452    </a>
453    <div class="auth-tabs">
454      <div class="auth-tab active" onclick="authTab('quick')" data-tab="quick" data-i18n="auth.tab_quick">Gratis</div>
455      <div class="auth-tab" onclick="authTab('login')" data-tab="login" data-i18n="auth.tab_login_short">Entrar</div>
456      <div class="auth-tab" onclick="authTab('register')" data-tab="register" data-i18n="auth.tab_register_short">Registro</div>
457    </div>
458    <div id="quick-form">
459      <p style="text-align:center;font-size:.72rem;color:var(--text2);margin-bottom:16px" data-i18n="auth.quick_intro">Entra gratis y empieza a aprender — sin contraseña, sube de nivel a tu ritmo.</p>
460      <div class="form-group"><label class="form-label" data-i18n="auth.label_email">CORREO</label><input type="email" id="q-email" placeholder="[email protected]" data-i18n-placeholder="auth.placeholder_email" onkeydown="if(event.key==='Enter')doQuickEntry()"></div>
461      <button class="btn btn-primary btn-full" onclick="doQuickEntry()" style="font-size:1rem;padding:13px;letter-spacing:1px" data-i18n="auth.btn_quick_entry">Empezar gratis →</button>
462      <p style="text-align:center;margin-top:12px;font-size:.68rem;color:var(--muted)" data-i18n="auth.quick_no_password">Sin contraseña. Podrás agregar una después si quieres.</p>
463    </div>
464    <div id="login-form" style="display:none">
465      <div class="form-group"><label class="form-label" data-i18n="auth.label_email">CORREO</label><input type="email" id="l-email" placeholder="[email protected]" data-i18n-placeholder="auth.placeholder_email"></div>
466      <div class="form-group"><label class="form-label" data-i18n="auth.label_password">CONTRASEÑA</label><input type="password" id="l-pass" placeholder="••••••••" onkeydown="if(event.key==='Enter')doLogin()"></div>
467      <button class="btn btn-primary btn-full" onclick="doLogin()" style="font-size:1rem;padding:13px;letter-spacing:1px" data-i18n="auth.btn_signin">Iniciar sesión →</button>
468      <p style="text-align:center;margin-top:12px;font-size:.72rem"><a href="/reset-password" style="color:var(--teal);text-decoration:none" data-i18n="auth.forgot_password">¿Olvidaste tu contraseña?</a></p>
469      <p style="text-align:center;margin-top:8px;font-size:.72rem;color:var(--text2)"><span data-i18n="auth.no_account">¿No tienes cuenta?</span> <a onclick="authTab('register')" style="color:var(--teal);text-decoration:none;cursor:pointer" data-i18n="auth.register_free">Regístrate gratis →</a></p>
470    </div>
471    <div id="register-form" style="display:none">
472      <div class="form-group"><label class="form-label" data-i18n="auth.label_name">NOMBRE</label><input type="text" id="r-name" placeholder="Tu nombre" data-i18n-placeholder="auth.placeholder_name"></div>
473      <div class="form-group"><label class="form-label" data-i18n="auth.label_email">CORREO</label><input type="email" id="r-email" placeholder="[email protected]" data-i18n-placeholder="auth.placeholder_email"></div>
474      <div class="form-group"><label class="form-label" data-i18n="auth.label_password">CONTRASEÑA</label><input type="password" id="r-pass" placeholder="••••••••"></div>
475      <button class="btn btn-primary btn-full" onclick="doRegister()" data-i18n="auth.btn_create_account">Crear cuenta →</button>
476    </div>
477    <div id="auth-err"></div>
478    <p style="text-align:center;margin-top:18px;font-size:.7rem;color:var(--muted)">
479      <span data-i18n="auth.not_user_yet">¿Aún no eres usuario?</span> <a href="https://factozcentral.com/home" style="color:var(--teal);text-decoration:none" data-i18n="auth.learn_more">Conoce más en factozcentral.com →</a>
480    </p>
481  </div>
482</div>
483
484<!-- DAILY QUIZ MODAL -->
485<div id="quiz-modal-overlay">
486  <div class="modal quiz-modal">
487    <div class="modal-title">
488      <span id="quiz-modal-title">Daily Quiz</span>
489      <span class="modal-close" onclick="closeQuizModal()">✕</span>
490    </div>
491    <div id="quiz-lesson-view">
492      <div class="quiz-topic-badge" id="quiz-topic-badge"></div>
493      <div id="quiz-lesson-content"></div>
494      <div class="quiz-unlock-note" id="quiz-unlocks-hint"></div>
495      <button class="btn btn-primary btn-full" onclick="_quizStartQuestions()" data-i18n="quiz.start_btn">Empezar quiz →</button>
496    </div>
497    <div id="quiz-questions-view" style="display:none">
498      <div id="quiz-questions-list"></div>
499      <button class="btn btn-primary btn-full" onclick="submitQuizAnswers()" data-i18n="quiz.submit_btn">Enviar respuestas</button>
500    </div>
501    <div id="quiz-results-view" style="display:none">
502      <div id="quiz-results-content"></div>
503      <button class="btn btn-primary btn-full" onclick="closeQuizModal()" data-i18n="quiz.done_btn">Listo</button>
504    </div>
505  </div>
506</div>
507
508<!-- CLIENT MODAL -->
509<div id="modal-overlay">
510  <div class="modal">
511    <div class="modal-title">
512      <span id="modal-title-text">Enterprise Client</span>
513      <span class="modal-close" onclick="closeModal()">✕</span>
514    </div>
515    <div class="modal-tabs">
516      <div class="modal-tab active" onclick="modalTab('info')">Info</div>
517      <div class="modal-tab" onclick="modalTab('keys')">API Keys</div>
518      <div class="modal-tab" onclick="modalTab('config')">Config</div>
519    </div>
520    <!-- INFO TAB -->
521    <div id="modal-info">
522      <div class="form-group"><label class="form-label">CLIENT NAME</label><input type="text" id="m-name" placeholder="Acme Trading Desk"></div>
523      <div class="form-group"><label class="form-label">CONTACT EMAIL</label><input type="email" id="m-email" placeholder="[email protected]"></div>
524      <div class="form-group"><label class="form-label">TIER</label>
525        <select id="m-tier">
526          <option value="enterprise_api">Enterprise API</option>
527        </select>
528      </div>
529      <div class="g2">
530        <div class="form-group"><label class="form-label">MONTHLY SCAN LIMIT</label><input type="number" id="m-scan-limit" value="500"></div>
531        <div class="form-group"><label class="form-label">MONTHLY POST LIMIT</label><input type="number" id="m-post-limit" value="200"></div>
532      </div>
533    </div>
534    <!-- KEYS TAB -->
535    <div id="modal-keys" style="display:none">
536      <div class="form-group"><label class="form-label">CHART-IMG API KEY</label><input type="password" id="m-chartimg" placeholder="6skJ8Lky..."></div>
537      <hr>
538      <div class="form-group"><label class="form-label">X Consumer Key</label><input type="password" id="m-x-api"></div>
539      <div class="form-group"><label class="form-label">X Consumer Secret</label><input type="password" id="m-x-secret"></div>
540      <div class="form-group"><label class="form-label">X Access Token</label><input type="password" id="m-x-token"></div>
541      <div class="form-group"><label class="form-label">X Access Token Secret</label><input type="password" id="m-x-tsecret"></div>
542      <hr>
543      <div class="form-group"><label class="form-label">INSTAGRAM WEBHOOK (Make.com)</label><input type="text" id="m-make-ig" placeholder="https://hook.us2.make.com/..."></div>
544      <div class="form-group"><label class="form-label">TIKTOK WEBHOOK</label><input type="text" id="m-make-tt" placeholder="https://hook.us2.make.com/..."></div>
545    </div>
546    <!-- CONFIG TAB -->
547    <div id="modal-config" style="display:none">
548      <div class="form-group"><label class="form-label">WATCHLIST (comma separated)</label><textarea id="m-watchlist" rows="3" placeholder="AAPL, NVDA, TSLA, MSFT, BTC-USD"></textarea></div>
549      <div class="form-group"><label class="form-label">CAROUSEL TEMPLATE</label>
550        <select id="m-template">
551          <option value="factoz">Factoz (Default)</option>
552          <option value="terminal">Terminal</option>
553          <option value="editorial">Editorial</option>
554        </select>
555      </div>
556      <div class="form-group"><label class="form-label">ALLOWED PLATFORMS</label>
557        <div style="display:flex;gap:16px;margin-top:4px">
558          <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:.78rem;font-weight:600"><input type="checkbox" id="m-plat-x" checked style="width:auto"> 🐦 X</label>
559          <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:.78rem;font-weight:600"><input type="checkbox" id="m-plat-ig" checked style="width:auto"> 📸 Instagram</label>
560          <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:.78rem;font-weight:600"><input type="checkbox" id="m-plat-tt" style="width:auto"> 🎵 TikTok</label>
561        </div>
562      </div>
563      <div class="form-group"><label class="form-label">CUSTOM HASHTAGS</label><input type="text" id="m-hashtags" placeholder="#Brand #Signals #Trading"></div>
564      <div class="form-group"><label class="form-label">CONTENT TONE</label>
565        <select id="m-tone">
566          <option value="professional">Professional</option>
567          <option value="aggressive">Aggressive / FOMO</option>
568          <option value="educational">Educational</option>
569        </select>
570      </div>
571      <label style="display:flex;align-items:center;gap:8px;
571cursor:pointer;font-size:.78rem;font-weight:600;margin-bottom:12px">
572        <input type="checkbox" id="m-autopost" style="width:auto"> Auto-post daily (Tier 1)
573      </label>
574    </div>
575    <hr>
576    <div style="display:flex;gap:10px;justify-content:flex-end">
577      <button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
578      <button class="btn btn-yellow" id="modal-save-btn" onclick="saveClient()">💾 Save Client</button>
579    </div>
580    <!-- API KEY RESULT -->
581    <div id="modal-key-result" style="display:none">
582      <div class="card" style="border-color:var(--green);margin-top:14px">
583        <h4>⚠️ SAVE THIS API KEY — SHOWN ONCE</h4>
584        <div class="api-key-box" id="new-api-key"></div>
585        <button class="btn btn-ghost" onclick="copyKey()" style="margin-top:8px;font-size:.72rem">📋 Copy Key</button>
586      </div>
587    </div>
588  </div>
589</div>
590
591<!-- OUTREACH COMPOSE MODAL -->
592<div id="oc-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:500;align-items:center;justify-content:center">
593  <div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:24px;max-width:560px;width:95%;position:relative;max-height:90vh;overflow-y:auto">
594    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
595      <div id="oc-modal-title" style="font-family:var(--mono);font-weight:700;font-size:.82rem;letter-spacing:1px"></div>
596      <span onclick="closeOutreachModal()" style="cursor:pointer;color:var(--muted);font-size:1.1rem">✕</span>
597    </div>
598    <div class="form-group" style="margin-bottom:8px">
599      <label class="form-label">TEMPLATE VARIANT</label>
600      <select id="oc-modal-variant"></select>
601    </div>
602    <div id="oc-modal-subj-row" class="form-group" style="margin-bottom:8px">
603      <label class="form-label">SUBJECT</label>
604      <input type="text" id="oc-modal-subj" style="width:100%">
605    </div>
606    <div class="form-group" style="margin-bottom:12px">
607      <label class="form-label">MESSAGE <span style="color:var(--muted);font-weight:400">(edit before sending — fill in {{specific_post}})</span></label>
608      <textarea id="oc-modal-body" rows="12" style="width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:10px;font-size:.78rem;line-height:1.6;resize:vertical;font-family:inherit"></textarea>
609    </div>
610    <div id="oc-modal-affiliate-row" style="display:none;margin-bottom:10px;padding:8px 10px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;font-size:.73rem">
611      <span style="color:var(--muted)">Affiliate link:</span>
612      <a id="oc-modal-affiliate-link" href="#" target="_blank" style="color:var(--teal);word-break:break-all;margin-left:6px"></a>
613      <button onclick="navigator.clipboard.writeText($('oc-modal-affiliate-link').href)" class="btn btn-ghost" style="font-size:.65rem;padding:1px 7px;margin-left:8px">Copy</button>
614    </div>
615    <div style="display:flex;gap:10px;align-items:center">
616      <button id="oc-modal-send" class="btn btn-primary" style="font-size:.78rem">Send Email</button>
617      <button id="oc-modal-copy" class="btn btn-yellow" style="font-size:.78rem">📋 Copy to Clipboard</button>
618      <button class="btn btn-ghost" style="font-size:.78rem" onclick="closeOutreachModal()">Cancel</button>
619    </div>
620    <div id="oc-modal-result" style="margin-top:8px;font-size:.73rem"></div>
621  </div>
622</div>
623
624<!-- TEMPLATE SLOT PAYMENT MODAL -->
625<div id="slot-pay-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.72);z-index:1000;align-items:center;justify-content:center">
626  <div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:28px 24px;max-width:420px;width:90%;position:relative">
627    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:18px">
628      <div style="font-family:var(--mono);font-weight:700;font-size:.85rem;letter-spacing:1px">BUY TEMPLATE SLOT</div>
629      <span onclick="closeSlotModal()" style="cursor:pointer;color:var(--muted);font-size:1.1rem">✕</span>
630    </div>
631    <div id="slot-pay-step-initiate">
632      <div style="font-size:.75rem;color:var(--muted);margin-bottom:14px">You are purchasing access to a private Templated.io render slot. Payment is processed via NOWPayments (crypto).</div>
633      <div style="margin-bottom:14px">
634        <div style="font-size:.65rem;color:var(--muted);letter-spacing:1px;margin-bottom:4px">SLOT</div>
635        <div id="slot-pay-label" style="font-size:.85rem;font-weight:700"></div>
636      </div>
637      <div style="margin-bottom:18px">
638        <div style="font-size:.65rem;color:var(--muted);letter-spacing:1px;margin-bottom:4px">PRICE</div>
639        <div id="slot-pay-price" style="font-size:1.1rem;font-weight:700;color:var(--green)"></div>
640      </div>
641      <div style="margin-bottom:18px">
642        <div style="font-size:.65rem;color:var(--muted);letter-spacing:1px;margin-bottom:6px">PAY WITH</div>
643        <div style="display:flex;gap:8px">
644          <button id="slot-curr-usdt" onclick="selectSlotCurrency('USDT')" class="btn btn-ghost" style="flex:1;font-size:.72rem">USDT (TRC-20)</button>
645          <button id="slot-curr-usdc" onclick="selectSlotCurrency('USDC')" class="btn btn-ghost" style="flex:1;font-size:.72rem">USDC (ERC-20)</button>
646        </div>
647      </div>
648      <button class="btn btn-primary" onclick="createSlotPayment()" id="slot-pay-btn" style="width:100%">Create Payment Invoice</button>
649      <div id="slot-pay-err" style="color:var(--red);font-size:.72rem;margin-top:8px;display:none"></div>
650    </div>
651    <div id="slot-pay-step-address" style="display:none">
652      <div style="font-size:.72rem;color:var(--muted);margin-bottom:14px">Send the exact amount below to the wallet address. Payment is confirmed on-chain automatically.</div>
653      <div style="margin-bottom:12px">
654        <div style="font-size:.65rem;color:var(--muted);letter-spacing:1px;margin-bottom:4px">SEND EXACTLY</div>
655        <div id="slot-pay-amount" style="font-family:var(--mono);font-size:1rem;font-weight:700;color:var(--green)"></div>
656      </div>
657      <div style="margin-bottom:16px">
658        <div style="font-size:.65rem;color:var(--muted);letter-spacing:1px;margin-bottom:4px">TO ADDRESS</div>
659        <div style="display:flex;align-items:center;gap:8px">
660          <div id="slot-pay-addr" style="font-family:var(--mono);font-size:.7rem;word-break:break-all;flex:1;background:var(--bg3);padding:8px 10px;border-radius:5px;border:1px solid var(--border)"></div>
661          <button class="btn btn-ghost" onclick="copySlotAddress()" style="font-size:.65rem;padding:6px 9px;white-space:nowrap">Copy</button>
662        </div>
663      </div>
664      <div style="font-size:.65rem;color:var(--muted);margin-bottom:14px">After sending, click below to check confirmation status.</div>
665      <button class="btn btn-primary" onclick="checkSlotPayment()" id="slot-check-btn" style="width:100%">Check Payment Status</button>
666      <div id="slot-check-result" style="font-size:.72rem;margin-top:8px"></div>
667    </div>
668    <div id="slot-pay-step-done" style="display:none;text-align:center;padding:10px 0">
669      <div style="font-size:2rem;margin-bottom:10px">✅</div>
670      <div style="font-weight:700;margin-bottom:6px">Template Slot Activated!</div>
671      <div style="font-size:.75rem;color:var(--muted);margin-bottom:18px">Your custom render slot is now available in the template grid.</div>
672      <button class="btn btn-primary" onclick="closeSlotModal()" style="width:100%">Done</button>
673    </div>
674  </div>
675</div>
676
677<!-- SIDEBAR -->
678<div id="sidebar">
679  <div class="logo">
680    <div class="logo-title">FACTO<span style="display:inline-block;transform:scaleX(-1)">Z</span></div>
681    <div class="logo-sub">COMMAND CENTER</div>
682  </div>
683  <nav id="nav-menu"></nav>
684  <div class="sidebar-footer">
685    <div id="clock"></div>
686    <div id="user-tag"></div>
687    <div id="tier-tag"></div>
688    <div id="xp-widget">
689      <div class="xp-row">
690        <span id="xp-level-label" style="color:var(--teal);font-weight:700;text-transform:uppercase;letter-spacing:.5px;font-size:.6rem"></span>
691        <span id="xp-quiz-status" style="color:var(--muted);font-size:.6rem"></span>
692      </div>
693      <div id="xp-progress-track"><div id="xp-progress-bar"></div></div>
694      <div class="xp-row">
695        <span id="xp-total-label" style="color:var(--text2);font-size:.58rem"></span>
696        <span id="xp-streak-label" style="color:var(--muted);font-size:.58rem"></span>
697      </div>
698    </div>
699    <div id="manage-sub-btn" onclick="manageSubscription()" style="display:none;font-size:.58rem;font-family:var(--mono);color:var(--muted);cursor:pointer;margin-bottom:4px;letter-spacing:.05em" onmouseover="this.style.color='var(--teal)'" onmouseout="this.style.color='var(--muted)'" data-i18n="sidebar.manage_subscription">⚙ gestionar suscripción</div>
700    <div id="sidebar-platforms" style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:8px"></div>
701    <div class="lang-toggle lang-toggle-sidebar">
702      <button type="button" class="lang-toggle-btn" data-lang="es" onclick="set
702Locale('es')">ES</button>
703      <button type="button" class="lang-toggle-btn" data-lang="en" onclick="setLocale('en')">EN</button>
704    </div>
705    <div class="logout-btn" onclick="logout()" data-i18n="sidebar.logout">← cerrar sesión</div>
706  </div>
707</div>
708
709<!-- MAIN -->
710<div id="main">
711
712<!-- TRIAL BANNER -->
713<div id="trial-banner" style="display:none;align-items:center;justify-content:center;gap:8px;background:rgba(0,200,83,.1);border-bottom:1px solid rgba(0,200,83,.25);color:var(--green);font-size:.75rem;font-family:var(--mono);padding:7px 16px;text-align:center"></div>
714
715<!-- FREE / LEVELING NUDGE BANNER -->
716<div id="free-upgrade-bar" style="display:none;align-items:center;justify-content:center;gap:8px;background:rgba(226,194,117,.08);border-bottom:1px solid rgba(226,194,117,.2);color:var(--text2);font-size:.75rem;font-family:var(--mono);padding:7px 16px;text-align:center">
717  <span data-i18n="banner.level_up">🎓 Sube de nivel aprendiendo con nuestras lecciones y quizzes diarios.</span>&nbsp;<a href="#" onclick="openQuizModal();return false;" style="color:var(--teal,#00c8a0);text-decoration:underline;font-weight:700" data-i18n="banner.level_up_cta">Hacer el quiz de hoy →</a>
718</div>
719
720<!-- TRIAL EXPIRED OVERLAY -->
721<div id="trial-expired-overlay" style="display:none;position:fixed;inset:0;background:#0d0d0d;z-index:9999;align-items:center;justify-content:center;flex-direction:column;text-align:center;padding:32px;overflow-y:auto">
722  <div style="font-family:monospace;font-size:1.1rem;color:#00e5b4;letter-spacing:2px;margin-bottom:20px">FACTOZ</div>
723  <h2 style="font-size:1.4rem;font-weight:700;margin-bottom:8px;color:#e2eaf2">Subscribe to access Pro</h2>
724  <p style="color:#8b949e;max-width:480px;line-height:1.7;margin-bottom:24px;font-size:.9rem">
725    Get full access to scanner, AI briefs, options flow, content tools, macro data, and more — $49/mo.
726  </p>
727
728  <!-- Plan comparison -->
729  <div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin-bottom:20px;max-width:780px;width:100%">
730
731    <!-- Creator -->
732    <div style="background:#111;border:1px solid #1a2a1a;border-radius:12px;padding:20px 22px;min-width:220px;flex:1;text-align:left">
733      <div style="font-family:monospace;font-size:.58rem;letter-spacing:2px;color:#00e5b4;margin-bottom:6px">CREATOR</div>
734      <div style="font-size:1.2rem;font-weight:800;color:#e2eaf2;margin-bottom:4px">$25<span style="font-size:.75rem;font-weight:400;color:#555">/mo</span></div>
735      <ul style="list-style:none;padding:0;margin:12px 0 16px;font-size:.72rem;color:#8b949e;line-height:2">
736        <li>✓ Scanner &amp; signals</li>
737        <li>✓ News feed</li>
738        <li>✓ AI chat (5/day)</li>
739        <li>✓ Analyst targets</li>
740      </ul>
741      <div id="paypal-plans-creator" class="paypal-hosted-btn-container" style="margin-bottom:6px"></div>
742      <button onclick="cryptoCheckout('creator')" style="display:block;width:100%;background:transparent;border:1px solid #00e5b4;color:#00e5b4;font-weight:700;padding:9px;border-radius:8px;cursor:pointer;font-family:monospace;font-size:.75rem;text-align:center">Pay with Crypto</button>
743    </div>
744
745    <!-- Pro -->
746    <div style="background:#111;border:1px solid #2a2a4a;border-radius:12px;padding:20px 22px;min-width:220px;flex:1;text-align:left;position:relative">
747      <div style="position:absolute;top:-10px;left:50%;transform:translateX(-50%);background:#6366f1;color:#fff;font-size:.58rem;font-weight:700;letter-spacing:1.5px;padding:3px 10px;border-radius:20px;font-family:monospace">POPULAR</div>
748      <div style="font-family:monospace;font-size:.58rem;letter-spacing:2px;color:#818cf8;margin-bottom:6px">PRO</div>
749      <div style="font-size:1.2rem;font-weight:800;color:#e2eaf2;margin-bottom:4px">$49<span style="font-size:.75rem;font-weight:400;color:#555">/mo</span></div>
750      <ul style="list-style:none;padding:0;margin:12px 0 16px;font-size:.72rem;color:#8b949e;line-height:2">
751        <li>✓ Everything in Creator</li>
752        <li>✓ Macro data &amp; heatmaps</li>
753        <li>✓ Portfolio risk / VAR</li>
754        <li>✓ Polymarket insights</li>
755        <li>✓ Pine Script library</li>
756        <li style="color:#e2c275;font-weight:600">✦ Factoz Intelligence Widget</li>
757        <li style="color:#a0aab8;font-size:.67rem;padding-left:12px">AI ticker analysis in your browser</li>
758      </ul>
759      <div id="paypal-plans-pro" class="paypal-hosted-btn-container" style="margin-bottom:6px"></div>
760      <button onclick="cryptoCheckout('pro')" style="display:block;width:100%;background:transparent;border:1px solid #6366f1;color:#a5b4fc;font-weight:700;padding:9px;border-radius:8px;
760cursor:pointer;font-family:monospace;font-size:.75rem;text-align:center">Pay with Crypto</button>
761    </div>
762
763    <!-- Stellar -->
764    <div class="stellar-paddle-btn" style="display:none;background:#111;border:1px solid #3a3010;border-radius:12px;padding:20px 22px;min-width:220px;flex:1;text-align:left">
765      <div style="font-family:monospace;font-size:.58rem;letter-spacing:2px;color:#e2c275;margin-bottom:6px">STELLAR</div>
766      <div style="font-size:1.2rem;font-weight:800;color:#e2eaf2;margin-bottom:4px">$99<span style="font-size:.75rem;font-weight:400;color:#555">/mo</span></div>
767      <ul style="list-style:none;padding:0;margin:12px 0 16px;font-size:.72rem;color:#8b949e;line-height:2">
768        <li>✓ Everything in Pro</li>
769        <li>✓ Full financial statements</li>
770        <li>✓ Priority support</li>
771        <li style="color:#e2c275;font-weight:600">✦ Factoz Intelligence Widget</li>
772        <li style="color:#a0aab8;font-size:.67rem;padding-left:12px">AI ticker analysis in your browser</li>
773      </ul>
774      <button onclick="paddleCheckout('stellar')" style="display:block;width:100%;background:#e2c275;color:#000;font-weight:700;padding:10px;border-radius:8px;border:none;cursor:pointer;font-family:monospace;font-size:.8rem;text-align:center">Subscribe</button>
775    </div>
776
777  </div>
778
779  <!-- Extension callout -->
780  <div style="max-width:540px;background:rgba(226,194,117,.06);border:1px solid rgba(226,194,117,.2);border-radius:10px;padding:14px 18px;margin-bottom:20px;text-align:left;display:flex;gap:14px;align-items:flex-start">
781    <div style="font-size:1.4rem;flex-shrink:0">🧩</div>
782    <div>
783      <div style="font-size:.78rem;font-weight:700;color:#e2c275;margin-bottom:4px">Factoz Intelligence Widget — Chrome Extension</div>
784      <div style="font-size:.72rem;color:#8b949e;line-height:1.6">Type any stock ticker and get a live AI brief — real-time news, earnings beat/miss, analyst consensus, risks &amp; catalysts — powered by Perplexity Sonar. Available on <strong style="color:#818cf8">Pro</strong> and <strong style="color:#e2c275">Stellar</strong> plans.</div>
785    </div>
786  </div>
787
788  <div style="margin-bottom:16px">
789    <button onclick="continueForFree()" style="background:transparent;border:1px solid #444;color:#8b949e;font-family:monospace;font-size:.75rem;padding:9px 22px;border-radius:8px;cursor:pointer">Continue with Free plan →</button>
790    <div style="font-size:.65rem;color:#555;margin-top:6px">Scanner, News Feed &amp; Charts — always free</div>
791  </div>
792
793  <p style="color:#6e7681;font-size:.75rem">
794    Already subscribed? <a href="#" onclick="localStorage.removeItem('fz_token');location.reload()" style="color:#00e5b4">Log in again</a>
795  </p>
796</div>
797
798<!-- OVERVIEW -->
799<div class="page active" id="page-overview">
800  <div class="page-header">
801    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/wyupmbaf.json" trigger="hover" style="width:28px;height:28px"></lord-icon>Factoz Command Center</div>
802    <div class="page-sub" style="margin-left:0">Global market intelligence — autonomous signals, daily briefs, macro research.</div>
803  </div>
804
805
806  <!-- Economic Countdown Banner -->
807  <div id="eco-countdown-banner" style="display:none;align-items:center;gap:10px;background:rgba(255,193,7,.08);border:1px solid rgba(255,193,7,.3);border-radius:6px;padding:9px 14px;margin-bottom:10px;font-size:.78rem;color:#ffc107;font-family:var(--mono)"></div>
808
809  <!-- Extension Install Banner (Pro / Stellar / Admin only) -->
810  <div id="ext-install-banner" style="display:none;align-items:center;gap:12px;background:rgba(226,194,117,.07);border:1px solid rgba(226,194,117,.25);border-radius:8px;padding:11px 16px;margin-bottom:10px">
811    <span style="font-size:1.2rem;flex-shrink:0">🧩</span>
812    <div style="flex:1;min-width:0">
813      <div style="font-size:.76rem;font-weight:700;color:#e2c275;margin-bottom:2px">Factoz Intelligence Widget — Chrome Extension</div>
814      <div style="font-size:.68rem;color:#7a8fa3;line-height:1.5">Get live AI ticker analysis as you browse Yahoo Finance, TradingView, Finviz and more. Type any ticker in the extension and Sonar researches it instantly.</div>
815    </div>
816    <a id="ext-install-btn" href="https://chromewebstore.google.com/detail/factoz-intelligence-widget/jbjfefeinkalicgplonblljjpmfmcbpc" target="_blank" style="flex-shrink:0;background:#e2c275;color:#0b132b;font-size:.72rem;font-weight:700;padding:7px 14px;border-radius:6px;text-decoration:none;white-space:nowrap">
816Install Extension</a>
817    <button onclick="dismissExtBanner()" style="background:none;border:none;cursor:pointer;color:#3d4f63;font-size:16px;padding:2px 4px;flex-shrink:0" title="Dismiss">✕</button>
818  </div>
819
820  <!-- Stat Cards -->
821  <div class="ov-stats">
822    <div class="ov-stat">
823      <div class="ov-stat-v" id="ov-signals">—</div>
824      <div class="ov-stat-l">SIGNALS TODAY</div>
825      <div class="ov-stat-s">last scanner run</div>
826    </div>
827    <div class="ov-stat" style="border-left-color:var(--yellow)">
828      <div class="ov-stat-v" id="ov-tier" style="font-size:1rem;padding-top:4px">—</div>
829      <div class="ov-stat-l" id="ov-email">—</div>
830      <div class="ov-stat-s">account</div>
831    </div>
832  </div>
833
834  <!-- Quick Actions -->
835  <div id="ov-quick-actions" style="display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap"></div>
836
837  <!-- Ad slot: overview-top -->
838  <div id="ad-overview-top" class="ad-slot" style="margin-bottom:14px"></div>
839
840  <!-- Bento Grid -->
841  <div class="ov-bento">
842
843    <!-- Pre-Market Scans — full width -->
844    <div class="ov-card ov-bento-wide">
845      <div class="ov-card-hdr">
846        <span>TODAY'S PRE-MARKET SCANS</span>
847        <div style="display:flex;align-items:center;gap:10px">
848          <a id="pm-collage-dl" href="#" download="premarket_collage.png" style="display:none;align-items:center;gap:4px;font-size:0.7rem;color:var(--green);text-decoration:none;padding:2px 8px;border:1px solid var(--green);border-radius:4px">⬇ Collage</a>
849          <a onclick="go('scanner')" style="font-size:.58rem;color:var(--teal);font-family:var(--mono);cursor:pointer;letter-spacing:1px;text-decoration:none">SCANNER →</a>
850        </div>
851      </div>
852      <div id="premarket-scans-panel">
853        <div class="ov-skel" style="width:55%;margin-bottom:10px"></div>
854        <div style="display:flex;gap:7px"><div class="ov-skel" style="width:68px;height:26px;border-radius:20px;margin-bottom:0"></div><div class="ov-skel" style="width:68px;height:26px;border-radius:20px;margin-bottom:0"></div><div class="ov-skel" style="width:68px;height:26px;border-radius:20px;margin-bottom:0"></div><div class="ov-skel" style="width:68px;height:26px;border-radius:20px;margin-bottom:0"></div></div>
855      </div>
856    </div>
857
858    <!-- Top Movers -->
859    <div class="ov-card">
860      <div class="ov-card-hdr">
861        <span>TOP MOVERS</span>
862        <button onclick="loadOverviewMovers()" title="Refresh">↻</button>
863        <a onclick="go('movers')" style="margin-left:8px">VIEW ALL →</a>
864      </div>
865      <div id="ov-movers-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
866        <div><div class="ov-skel ov-skel-sm"></div><div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div></div>
867        <div><div class="ov-skel ov-skel-sm"></div><div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div></div>
868      </div>
869    </div>
870
871    <!-- Latest Brief -->
872    <div class="ov-card" style="cursor:pointer" onclick="go('news')">
873      <div class="ov-card-hdr">
874        <span>LATEST BRIEF</span>
875        <a onclick="go('news')" style="font-size:.58rem;color:var(--teal);font-family:var(--mono);cursor:pointer;letter-spacing:1px;text-decoration:none">READ →</a>
876      </div>
877      <div id="ov-latest-brief">
878        <div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div>
879      </div>
880    </div>
881
882    <!-- Latest News -->
883    <div class="ov-card">
884      <div class="ov-card-hdr" style="flex-direction:column;align-items:stretch;gap:6px">
885        <span>LATEST NEWS</span>
886        <input id="ov-news-search" type="text" placeholder="Filter by keyword…" oninput="filterOvNews(this.value)" style="width:100%;background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:5px 10px;color:var(--text);font-size:.72rem;outline:none;box-sizing:border-box">
887      </div>
888      <div id="ov-news-preview" style="max-height:280px;overflow-y:auto">
889        <div class="ov-skel"></div><div class="ov-skel ov-skel-sm" style="margin-bottom:12px"></div>
890        <div class="ov-skel"></div><div class="ov-skel ov-skel-sm"></div>
891      </div>
892    </div>
893
894    <!-- DJT / Trump Posts (only for opted-in users) -->
895    <div class="ov-card" id="ov-djt-card" style="display:none">
896      <div class="ov-card-hdr"><span>🇺🇸 TRUMP POSTS</span></div>
897      <div id="ov-djt-preview" style="max-height:220px;overflow-y:auto;display:flex;flex-direction:column;gap:4px"></div>
898    </div>
899
900    <!-- Insider Flow -->
901    <div class="ov-card">
902      <div class="ov-card-hdr">
903        <span>INSIDER FLOW</span>
904        <button onclick="loadInsiderFlow()" title="Refresh">↻</button>
905      </div>
906      <div id="ov-insider-panel">
907        <div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div>
908      </div>
909    </div>
910
911    <!-- Options Flow -->
912    <div class="ov-card">
913      <div class="ov-card-hdr">
914        <span>⚡ OPTIONS FLOW</span>
915        <div style="display:flex;gap:6px;align-items:center">
916          <span id="ov-options-badge" style="display:none;font-size:.6rem;background:rgba(46,204,142,.15);color:#2ecc8e;border:1px solid rgba(46,204,142,.3);border-radius:4px;padding:1px 7px;font-family:var(--mono)"></span>
917          <button onclick="loadOptionsFlow()" title="Refresh">↻</button>
918        </div>
919      </div>
920      <div id="ov-options-panel">
921        <div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div>
922      </div>
923    </div>
924
925    <!-- EPS Surprises -->
926    <div class="ov-card">
927      <div class="ov-card-hdr">
928        <span>EPS SURPRISES</span>
929        <a onclick="filterNews('earnings');go('news')" style="font-size:.58rem;color:var(--teal);font-family:var(--mono);cursor:pointer;letter-spacing:1px;text-decoration:none">VIEW ALL →</a>
930      </div>
931      <div id="ov-eps-panel">
932        <div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div>
933      </div>
934    </div>
935
936    <!-- Sector Performance -->
937    <div class="ov-card" style="padding:0;overflow:hidden">
938      <div class="ov-card-hdr" style="padding:10px 12px 0"><span>SECTOR PERFORMANCE</span></div>
939      <div class="tradingview-widget-container">
940        <div class="tradingview-widget-container__widget"></div>
941        
941<script type="text/javascript" src="https://s3.tradingview.com/external-embedding/embed-widget-market-overview.js" async>{"colorTheme":"dark","dateRange":"1D","showChart":false,"locale":"en","width":"100%","height":230,"isTransparent":true,"showSymbolLogo":false,"showFloatingTooltip":false,"tabs":[{"title":"Sectors","symbols":[{"s":"AMEX:XLK","d":"Technology"},{"s":"AMEX:XLF","d":"Financials"},{"s":"AMEX:XLE","d":"Energy"},{"s":"AMEX:XLV","d":"Healthcare"},{"s":"AMEX:XLI","d":"Industrials"},{"s":"AMEX:XLY","d":"Cons. Disc."},{"s":"AMEX:XLP","d":"Cons. Staples"},{"s":"AMEX:XLU","d":"Utilities"},{"s":"AMEX:XLRE","d":"Real Estate"},{"s":"AMEX:XLB","d":"Materials"},{"s":"AMEX:XLC","d":"Comm. Svcs"}]}]}</script>
941
942      </div>
943    </div>
944
945    <!-- Analyst Ratings -->
946    <div class="ov-card">
947      <div class="ov-card-hdr">
948        <span>ANALYST RATINGS</span>
949        <button onclick="loadAnalystRatings()" title="Refresh">↻</button>
950        <a onclick="filterNews('company_analysis');go('news')" style="font-size:.58rem;color:var(--teal);font-family:var(--mono);cursor:pointer;letter-spacing:1px;text-decoration:none">VIEW ALL →</a>
951      </div>
952      <div id="ov-analyst-panel">
953        <div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div>
954      </div>
955    </div>
956
957  </div><!-- /ov-bento -->
958</div>
959
960<!-- SCANNER -->
961<div class="page" id="page-scanner">
962  <div class="page-header">
963    <div class="page-title"><span class="step-badge">1</span>Scanner</div>
964    <div class="page-sub">MA Crossover · RSI Extremes · S/R Breakout · Vol Surge · 52-Wk · Gap &amp; Hold</div>
965  </div>
966  <div class="macro-tabs" style="margin-bottom:14px">
967    <button class="macro-tab active" onclick="scannerTab('scanner')">▶ Scanner</button>
968    <button class="macro-tab" onclick="scannerTab('screener')">🔍 Screener</button>
969  </div>
970  <div id="scanner-pane-scanner" class="macro-pane active">
971  <div class="scanner-grid">
972    <div>
973      <div class="form-group">
974        <label class="form-label">SCAN TYPE</label>
975        <select id="scan-type-select" style="width:100%;background:var(--bg3);color:var(--text1);border:1px solid var(--border);border-radius:5px;padding:7px 10px;font-size:.8rem">
976          <option value="ma_crossover">MA Crossover — EMA9 / SMA50 / SMA200</option>
977          <option value="rsi">RSI Extremes — Overbought (&gt;70) / Oversold (&lt;30)</option>
978          <option value="sr_breakout">S/R Breakout — 20-day High/Low Break</option>
979          <option value="volume_surge"  class="pro-scan-opt" disabled>📊 Vol Surge — Volume ≥ 1.5× 3-month avg [PRO]</option>
980          <option value="breakout_52wk" class="pro-scan-opt" disabled>🚀 52-Wk Breakout — Within 5% of 52-week high/low [PRO]</option>
981          <option value="gap_and_hold"  class="pro-scan-opt" disabled>⚡ Gap &amp; Hold — Gapped ≥ 3% and held direction [PRO]</option>
982        </select>
983      </div>
984      <div class="form-group">
985        <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
986          <label class="form-label" style="margin-bottom:0">TICKER LIST (comma separated)</label>
987          <label style="font-size:.68rem;color:var(--teal);cursor:pointer;display:flex;align-items:center;gap:4px">
988            📎 Import .xlsx / .xls / .csv
989            <input type="file" accept=".xlsx,.xls,.csv" style="display:none" onchange="loadTickersFromFile(this)">
990          </label>
991        </div>
992        <textarea id="scan-tickers" rows="4" placeholder="AAPL, NVDA, TSLA, MSFT, AMZN, META, GOOGL, AMD, NFLX, COIN"></textarea>
993      </div>
994      <button class="btn btn-primary" onclick="runScan()" id="scan-btn">▶ Run Scanner</button>
995      <div style="display:flex;gap:6px;margin-top:7px">
996        <button class="btn btn-ghost" onclick="saveScannerList()" style="font-size:.68rem;padding:3px 10px">💾 Save list</button>
997        <button class="btn btn-ghost" onclick="loadScannerList()" style="font-size:.68rem;padding:3px 10px" id="load-list-btn">⚡ Load saved</button>
998      </div>
999      <div id="scan-status" style="margin-top:10px"></div>
1000      <hr>
1001      <div class="card"><h4>RESULTS</h4><div id="scan-results"><p>Run a scan to see signals.</p></div></div>
1002    </div>
1003    <div>
1004      <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
1005        <span class="badge badge-muted" id="chart-count">0 charts</span>
1006      </div>
1007      <div class="chart-grid" id="chart-grid"></div>
1008    </div>
1009  </div>
1010  </div><!-- /scanner-pane-scanner -->
1011
1012  <div id="scanner-pane-screener" class="macro-pane">
1013    <div style="text-align:right;margin-bottom:6px"><a href="https://in.tradingview.com/?aff_id=164648" target="_blank" rel="noopener" style="display:inline-block;background:#1a2340;border:1px solid #E2C275;border-radius:20px;padding:4px 12px;font-size:.68rem;color:#E2C275;text-decoration:none;letter-spacing:.3px">🎁 Get <strong>$15 USD off</strong> TradingView — Open here ↗</a></div>
1014    <div class="tradingview-widget-container" style="background:#0b132b;border:1px solid #202e56;border-radius:10px;overflow:hidden">
1015      <div class="tradingview-widget-container__widget"></div>
1016      <div class="tradingview-widget-copyright" style="font-size:.5rem;color:var(--muted);opacity:.35;text-align:right;padding:1px 4px"><a href="https://www.tradingview.com/screener/" rel="noopener nofollow" target="_blank" style="color:var(--muted);text-decoration:none">Stock Screener by TradingView</a></div>
1017      
1017<script type="text/javascript" src="https://s3.tradingview.com/external-embedding/embed-widget-screener.js" async>
1018      {"market":"america","showToolbar":true,"defaultColumn":"overview","defaultScreen":"most_capitalized","isTransparent":true,"locale":"en","colorTheme":"dark","width":"100%","height":700,"backgroundColor":"#0b132b"}
1019      </script>
1019
1020    </div>
1021  </div><!-- /scanner-pane-screener -->
1022
1023  <!-- SCAN HISTORY — full width -->
1024  <div class="card" style="margin-top:18px">
1025    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
1026      <h4 style="margin:0">SCAN HISTORY</h4>
1027    </div>
1028    <div id="scan-history-list"><p style="font-size:.75rem;color:var(--text2)">No saved scans yet.</p></div>
1029  </div>
1030
1031  <!-- MY CHARTS — full width -->
1032  <div class="card" style="margin-top:12px">
1033    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
1034      <h4 style="margin:0">MY CHARTS (7 days)</h4>
1035    </div>
1036    <div id="my-charts-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px">
1037      <p style="font-size:.75rem;color:var(--text2);grid-column:1/-1">Run a scan to save charts here.</p>
1038    </div>
1039  </div>
1040
1041  <!-- CHART BUILDER -->
1042  <div class="card" id="chart-builder-card" style="margin-top:18px">
1043    <h4 style="margin:0 0 12px">📐 Custom Chart Builder</h4>
1044    <div class="g2" style="gap:10px">
1045      <div>
1046        <div class="form-group" style="margin-bottom:8px">
1047          <label class="form-label">TICKER</label>
1048          <input id="cb-ticker" type="text" placeholder="AAPL" style="text-transform:uppercase">
1049        </div>
1050        <div style="display:flex;gap:8px;margin-bottom:8px">
1051          <div class="form-group" style="flex:1;margin-bottom:0">
1052            <label class="form-label">INTERVAL</label>
1053            <select id="cb-interval" style="width:100%;background:var(--bg3);color:var(--text1);border:1px solid var(--border);border-radius:5px;padding:7px 10px;font-size:.8rem">
1054              <option value="1D">1 Day</option>
1055              <option value="4h">4 Hour</option>
1056              <option value="1h">1 Hour</option>
1057              <option value="15m">15 Min</option>
1058              <option value="1W">1 Week</option>
1059            </select>
1060          </div>
1061          <div class="form-group" style="flex:1;margin-bottom:0">
1062            <label class="form-label">RANGE</label>
1063            <select id="cb-range" style="width:100%;background:var(--bg3);color:var(--text1);border:1px solid var(--border);border-radius:5px;padding:7px 10px;font-size:.8rem">
1064              <option value="3M">3 Months</option>
1065              <option value="1M">1 Month</option>
1066              <option value="6M">6 Months</option>
1067              <option value="1Y">1 Year</option>
1068            </select>
1069          </div>
1070        </div>
1071        <button class="btn btn-primary" onclick="buildCustomChart()" id="cb-btn" style="width:100%">📊 Generate Chart</button>
1072        <div id="cb-status" style="margin-top:8px;font-size:.75rem"></div>
1073      </div>
1074      <div>
1075        <label class="form-label">INDICATORS</label>
1076        <div id="cb-indicators" style="display:flex;flex-direction:column;gap:6px;max-height:260px;overflow-y:auto;padding-right:4px">
1077          <p style="font-size:.75rem;color:var(--muted)">Loading indicators…</p>
1078        </div>
1079      </div>
1080    </div>
1081    <div id="cb-chart-output" style="margin-top:14px;text-align:center"></div>
1082  </div>
1083
1084
1085  <!-- AI FINANCIAL ADVISOR CHAT -->
1086  <div class="card" id="scanner-chat" style="margin-top:18px">
1087    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
1088      <h4 style="margin:0">💬 AI Market Analyst</h4>
1089      <button class="btn btn-ghost" onclick="clearChat()" style="font-size:.72rem;padding:3px 8px">✕ Clear</button>
1090    </div>
1091    <!-- Quick-prompt chips -->
1092    <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px">
1093      <button class="chat-chip" onclick="chipPrompt('Why is [ticker] moving today?')">📰 Why moving?</button>
1094      <button class="chat-chip" onclick="chipPrompt('Summarize recent earnings for [ticker]')">📊 Earnings</button>
1095      <button class="chat-chip" onclick="chipPrompt('What do analysts say about [ticker]?')">🏦 Analyst view</button>
1096      <button class="chat-chip" onclick="chipPrompt('Key risks for [ticker] right now?')">⚠️ Risks</button>
1097    </div>
1098    <!-- Message log -->
1099    <div id="chat-log" style="max-height:320px;overflow-y:auto;display:flex;flex-direction:column;gap:8px;margin-bottom:10px"></div>
1100    <!-- Input row -->
1101    <div style="display:flex;gap:8px">
1102      <input id="chat-input" type="text" placeholder="Ask about any stock or the scan results…"
1103             style="flex:1" onkeydown="if(event.key==='Enter')sendChat()">
1104      <button class="btn btn-primary" onclick="sendChat()" id="chat-btn" style="white-space:nowrap">Send ↵</button>
1105    </div>
1106    <div id="chat-status" style="font-size:.72rem;color:var(--muted);margin-top:5px;min-height:1em"></div>
1107  </div>
1108</div>
1109
1110<!-- ANALYST TARGETS -->
1111<div class="page" id="page-analyst">
1112  <div class="page-header">
1113    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/xvmmqwjv.json" trigger="in" delay="1500" stroke="bold" state="in-reveal" style="width:28px;height:28px"></lord-icon>Analyst Price Targets</div>
1114    <div class="page-sub">Wall Street consensus price targets — powered by Yahoo Finance</div>
1115  </div>
1116  <div style="margin-bottom:20px">
1117    <div style="display:flex;gap:8px;max-width:420px">
1118      <div style="position:relative;flex:1">
1119        <input id="analyst-ticker-input" class="input" placeholder="e.g. AAPL"
1120          style="text-transform:uppercase;width:100%;box-sizing:border-box"
1121          autocomplete="off"
1122          oninput="analystAutoComplete(this.value)"
1123          onkeydown="analystInputKey(event)">
1124        <div id="analyst-suggestions"
1125          style="display:none;position:absolute;top:100%;left:0;right:0;
1126                 background:#161b22;border:1px solid #30363d;border-top:none;
1127                 border-radius:0 0 6px 6px;z-index:100;max-height:220px;overflow-y:auto"></div>
1128      </div>
1129      <button class="btn" onclick="getAnalystTarget()">Get Targets</button>
1130    </div>
1131  </div>
1132  <div id="analyst-result"></div>
1133</div>
1134
1135<!-- CAROUSEL -->
1136<div class="page" id="page-carousel">
1137  <div class="page-header">
1138    <div class="page-title"><span class="step-badge">2</span>Carousel Builder</div>
1139    <div class="page-sub">4-slide branded carousels — person cover, chart, data overlays</div>
1140  </div>
1141  <div class="wiz-bar">
1142    <div class="wiz-step active" id="rstep-1"><span class="wiz-num">1</span><span class="wiz-label">Charts</span></div>
1143    <div class="wiz-line"></div>
1144    <div class="wiz-step" id="rstep-2"><span class="wiz-num">2</span><span class="wiz-label">Configure</span></div>
1145    <div class="wiz-line"></div>
1146    <div class="wiz-step" id="rstep-3"><span class="wiz-num">3</span><span class="wiz-label">Build</span></div>
1147    <div class="wiz-line"></div>
1148    <div class="wiz-step" id="rstep-4"><span class="wiz-num">4</span><span class="wiz-label">Done</span></div>
1149  </div>
1150  <div id="carousel-hint" style="font-size:.74rem;color:var(--muted);margin-bottom:14px;font-family:var(--mono)">Upload charts or send from Scanner to begin.</div>
1151
1152  <!-- Mode tabs -->
1153  <div style="display:flex;gap:0;margin-bottom:18px;border:1px solid var(--border);border-radius:8px;overflow:hidden;width:fit-content">
1154    <button id="tab-pil" onclick="switchCarouselTab('pil')" style="display:none;padding:8px 20px;font-size:.75rem;font-weight:700;font-family:var(--mono);cursor:pointer;background:rgba(226,194,117,.12);color:var(--teal);border:none;border-right:1px solid var(--border)">⚡ Quick Build</button>
1155    <button id="tab-premium" onclick="switchCarouselTab('premium')" style="padding:8px 20px;font-size:.75rem;font-weight:700;font-family:var(--mono);cursor:pointer;background:rgba(226,194,117,.12);color:var(--teal);border:none">✨ Premium Designs</button>
1156  </div>
1157
1158  <!-- ── QUICK BUILD (PIL) — hidden ── -->
1159  <div id="carousel-pil-panel" style="display:none">
1160    <div class="g2">
1161      <div>
1162        <div class="template-grid">
1163          <div class="tmpl-card selected" data-tmpl="factoz" onclick="selectTemplate('factoz',this)"><div class="tmpl-icon">🌑</div><div class="tmpl-name">Factoz</div><div class="tmpl-desc">Dark moody, bold text</div></div>
1164          <div class="tmpl-card" data-tmpl="terminal" onclick="selectTemplate('terminal',this)"><div class="tmpl-icon">💻</div><div class="tmpl-name">Terminal</div><div class="tmpl-desc">Bloomberg feel</div></div>
1165          <div class="tmpl-card" data-tmpl="editorial" onclick="selectTemplate('editorial',this)"><div class="tmpl-icon">📰</div><div class="tmpl-name">Editorial</div><div class="tmpl-desc">Magazine style</div></div>
1166        </div>
1167        <div style="font-size:.65rem;color:var(--muted);margin-bottom:10px">Your brand kit colors &amp; logo are applied to the output.</div>
1168        <div class="form-group"><label class="form-label">UPLOAD IMAGES</label><input type="file" id="carousel-files" accept="image/*" multiple><div id="carousel-files-info" style="font-size:.7rem;color:var(--text2);margin-top:4px"></div></div>
1169        <div class="form-group">
1170          <label class="form-label">SLIDE LABELS <span style="opacity:.45;font-weight:400;font-size:.65rem">(optional)</span></label>
1171          <input type="text" id="slide-text-1" placeholder="Slide 1 text" style="width:100%;margin-bottom:6px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:7px 10px;color:var(--text);font-size:.78rem">
1172          <input type="text" id="slide-text-2" placeholder="Slide 2 text" style="width:100%;margin-bottom:6px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:7px 10px;color:var(--text);font-size:.78rem">
1173          <input type="text" id="slide-text-3" placeholder="Slide 3 text" style="width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:7px 10px;color:var(--text);font-size:.78rem">
1174        </div>
1175        <div class="form-group">
1176          <label class="form-label">OVERLAY FONT</label>
1177          <select id="overlay-font" style="width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:7px 10px;color:var(--text);font-size:.78rem">
1178            <option value="anton">Anton (Impact Style)</option>
1179            <option value="bebas">Bebas Neue</option>
1180            <option value="oswald">Oswald Bold</option>
1181            <option value="playfair">Playfair Display</option>
1182            <option value="montserrat">Montserrat Bold</option>
1183          </select>
1184        </div>
1185        <div class="form-group">
1186          <label class="form-label">FONT SIZE &nbsp;<span id="font-size-val" style="color:var(--accent);font-weight:700">40</span></label>
1187          <input type="range" id="font-size" min="20" max="80" value="40" step="2"
1188                 style="width:100%;accent-color:var(--accent)"
1189                 oninput="document.getElementById('font-size-val').textContent=this.value">
1190        </div>
1191        <div class="form-group">
1192          <label class="form-label">TEXT COLOR</label>
1193          <div id="color-swatches" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:4px">
1194            <div class="swatch selected" data-color="#FFFFFF" style="background:#fff" title="White"></div>
1195            <div class="swatch" data-color="#000000" style="background:#000" title="Black"></div>
1196            <div class="swatch" data-color="#E2C275" style="background:#E2C275" title="Gold"></div>
1197            <div class="swatch" data-color="#FFD700" style="background:#FFD700" title="Yellow"></div>
1198            <div class="swatch" data-color="#FF4444" style="background:#FF4444" title="Red"></div>
1199            <div class="swatch" data-color="#00E676" style="background:#00E676" title="Green"></div>
1200          </div>
1201          <input type="hidden" id="text-color" value="#FFFFFF">
1202        </div>
1203        <div style="display:flex;gap:10px">
1204          <button class="btn btn-primary" onclick="buildCarousel()" id="carousel-btn">⚡ Build</button>
1205        </div>
1206      </div>
1207      <div>
1208        <div class="carousel-preview" id="carousel-preview">
1209          <div style="background:var(--bg3);border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--muted)">Slide 1</div>
1210          <div style="background:var(--bg3);border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--muted)">Slide 2</div>
1211          <div style="background:var(--bg3);border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--muted)">Slide 3</div>
1212        </div>
1213        <hr>
1214        <div class="card"><h4>CAPTION</h4><textarea id="carousel-caption-preview" rows="5" placeholder="Write your caption here..." style="width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:10px;font-size:.72rem;font-family:var(--mono);color:var(--text);resize:vertical"></textarea></div>
1215        <div id="carousel-actions" style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px"></div>
1216      </div>
1217    </div>
1218  </div>
1219
1220  <!-- ── TEMPLATED DESIGNER ── -->
1221  <div id="carousel-premium-panel">
1222    <!-- Status bar -->
1223    <div id="premium-status-bar" style="background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:14px 18px;margin-bottom:18px">
1224      <div id="premium-status-text" style="font-size:.78rem;color:var(--muted)">Loading...</div>
1225    </div>
1226
1227    <!-- Build form -->
1228    <div id="premium-build-form" style="display:none">
1229      <div class="g2">
1230        <div>
1231          <!-- Dynamic template grid -->
1232          <div style="display:flex;justify-content:flex-end;margin-bottom:6px">
1233            <button onclick="reloadTemplates()" title="Reload template list" style="font-family:var(--mono);font-size:.6rem;padding:3px 9px;background:transparent;border:1px solid var(--border);border-radius:4px;color:var(--muted);cursor:pointer">↻ Reload</button>
1234          </div>
1235          <div id="premium-card-grid" class="template-grid" style="grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px;margin-bottom:14px">
1236            <!-- Cards injected by renderTemplateGrid() -->
1237          </div>
1238          <div id="premium-logo-row" style="display:none;align-items:center;gap:8px;margin-bottom:12px;font-size:.72rem;color:var(--muted)">
1239            <img id="premium-logo-preview" src="" style="width:28px;height:28px;object-fit:contain;border-radius:4px;border:1px solid var(--border)">
1240            <span>Logo from Brand Kit will be injected into <code style="font-size:.65rem">logo-1</code> on every slide</span>
1241          </div>
1242          <div style="font-size:.72rem;color:var(--muted);margin-bottom:10px">Upload your pre-formatted data PNGs — they will be centered on the finance background template. Upload 1 to 3 slides.</div>
1243          <div class="form-group">
1244            <label class="form-label">SLIDE 1 CONTENT</label>
1245            <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
1246              <label class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px;cursor:pointer;margin:0">📁 Upload<input type="file" id="premium-slide1" accept="image/*" style="display:none" onchange="clearSlotSelection(1)"></label>
1247              <button class="btn btn-ghost" onclick="openGalleryPicker(1)" style="font-size:.7rem;padding:4px 10px">📂 Pick from Gallery</button>
1248              <span id="slide-sel-1" style="font-size:.65rem;color:var(--teal);display:none">✓ Gallery image selected</span>
1249            </div>
1250            <div id="slide-preview-1" style="display:none;margin-top:6px"><img style="height:60px;border-radius:6px;border:1px solid var(--border)"></div>
1251            <input type="text" id="premium-caption-1" placeholder="Slide 1 — text-1…" style="margin-top:6px">
1252          </div>
1253          <div class="form-group">
1254            <label class="form-label">SLIDE 2 CONTENT</label>
1255            <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
1256              <label class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px;cursor:pointer;margin:0">📁 Upload<input type="file" id="premium-slide2" accept="image/*" style="display:none" onchange="clearSlotSelection(2)"></label>
1257              <button class="btn btn-ghost" onclick="openGalleryPicker(2)" style="font-size:.7rem;padding:4px 10px">📂 Pick from Gallery</button>
1258              <span id="slide-sel-2" style="font-size:.65rem;color:var(--teal);display:none">✓ Gallery image selected</span>
1259            </div>
1260            <div id="slide-preview-2" style="display:none;margin-top:6px"><img style="height:60px;border-radius:6px;border:1px solid var(--border)"></div>
1261            <input type="text" id="premium-caption-2" placeholder="Slide 2 — text-2…" style="margin-top:6px">
1262          </div>
1263          <div class="form-group">
1264            <label class="form-label">SLIDE 3 CONTENT</label>
1265            <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
1266              <label class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px;cursor:pointer;margin:0">📁 Upload<input type="file" id="premium-slide3" accept="image/*" style="display:none" onchange="clearSlotSelection(3)"></label>
1267              <button class="btn btn-ghost" onclick="openGalleryPicker(3)" style="font-size:.7rem;padding:4px 10px">📂 Pick from Gallery</button>
1268              <span id="slide-sel-3" style="font-size:.65rem;color:var(--teal);display:none">✓ Gallery image selected</span>
1269            </div>
1270            <div id="slide-preview-3" style="display:none;margin-top:6px"><img style="height:60px;border-radius:6px;border:1px solid var(--border)"></div>
1271            <input type="text" id="premium-caption-3" placeholder="Slide 3 — text-3…" style="margin-top:6px">
1272          </div>
1273          <button class="btn btn-primary" onclick="buildPremiumCarousel()" id="premium-build-btn">✨ Generate Carousel</button>
1274          <div id="premium-build-result" style="margin-top:10px"></div>
1275        </div>
1276        <div>
1277          <div id="premium-preview-grid" class="carousel-preview">
1278            <div style="background:var(--bg3);border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--muted)">Slide 1</div>
1279            <div style="background:var(--bg3);border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--muted)">Slide 2</div>
1280            <div style="background:var(--bg3);border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--muted)">Slide 3</div>
1281          </div>
1282        </div>
1283      </div>
1284    </div>
1285
1286    <!-- Factoz AI Image Generator -->
1287    <div class="card" id="ai-chart-card" style="margin-top:18px">
1288      <h4 style="margin:0 0 4px">✦ Factoz AI Image Generator</h4>
1289      <p style="font-size:.72rem;color:var(--muted);margin:0 0 14px">Pick your market view — AI generates themed finance art for your slides.</p>
1290      <div style="display:flex;gap:10px;flex-wrap:wrap">
1291        <button onclick="runAiChart('bull')" id="ai-chart-bull-btn"
1292          style="flex:1;padding:10px;border-radius:8px;cursor:pointer;background:rgba(0,200,83,.12);
1293                 color:#00c853;border:1px solid rgba(0,200,83,.3);font-weight:600;font-size:.85rem">
1294          🐂 Bull Case
1295        </button>
1296        <button onclick="runAiChart('bear')" id="ai-chart-bear-btn"
1297          style="flex:1;padding:10px;border-radius:8px;
1297cursor:pointer;background:rgba(242,54,69,.12);
1298                 color:#f23645;border:1px solid rgba(242,54,69,.3);font-weight:600;font-size:.85rem">
1299          🐻 Bear Case
1300        </button>
1301        <button onclick="runAiChart('neutral')" id="ai-chart-neutral-btn"
1302          style="flex:1;padding:10px;border-radius:8px;cursor:pointer;background:rgba(79,195,247,.10);
1303                 color:#4fc3f7;border:1px solid rgba(79,195,247,.3);font-weight:600;font-size:.85rem">
1304          ⚡ Neutral
1305        </button>
1306      </div>
1307      <div id="ai-chart-status" style="margin-top:10px;font-size:.75rem;color:var(--muted)"></div>
1308      <div id="ai-chart-result" style="margin-top:14px;display:none">
1309        <img id="ai-chart-img" src="" style="width:100%;max-width:400px;border-radius:10px;display:block">
1310        <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
1311          <button onclick="downloadAiChartImage()" style="font-size:.72rem" class="btn btn-ghost">⬇ Download</button>
1312          <button onclick="useAiImageForSlot(1)" style="font-size:.72rem" class="btn btn-ghost">→ Use for Slide 1</button>
1313          <button onclick="useAiImageForSlot(2)" style="font-size:.72rem" class="btn btn-ghost">→ Use for Slide 2</button>
1314          <button onclick="useAiImageForSlot(3)" style="font-size:.72rem" class="btn btn-ghost">→ Use for Slide 3</button>
1315        </div>
1316      </div>
1317    </div>
1318
1319    <!-- Gallery Picker Modal -->
1320    <div id="gallery-picker-modal" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.7);align-items:center;justify-content:center">
1321      <div style="background:var(--bg2);border:1px solid var(--border);border-radius:12px;width:min(700px,95vw);max-height:80vh;display:flex;flex-direction:column;overflow:hidden">
1322        <div style="display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--border)">
1323          <span style="font-weight:700;font-size:.9rem">Pick Image for Slide <span id="gp-slot-label"></span></span>
1324          <button onclick="closeGalleryPicker()" style="background:none;border:none;color:var(--text);font-size:1.2rem;cursor:pointer">✕</button>
1325        </div>
1326        <div id="gp-grid" style="padding:16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:10px;overflow-y:auto"></div>
1327      </div>
1328    </div>
1329
1330    <!-- Recent Outputs (72h) -->
1331    <div class="card" style="margin-top:18px">
1332      <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
1333        <h4 style="margin:0">🗂 Recent Outputs <span style="font-size:.7rem;color:var(--muted);font-weight:400">(last 3 days)</span></h4>
1334      </div>
1335      <div id="recent-outputs-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px">
1336        <div style="font-size:.72rem;color:var(--muted)">Loading...</div>
1337      </div>
1338    </div>
1339
1340    <!-- Setup guide -->
1341    <div id="premium-setup-guide" style="display:none">
1342      <div class="card">
1343        <h4>CAROUSEL ENGINE SETUP GUIDE</h4>
1344        <ol style="padding-left:20px;font-size:.78rem;color:var(--text2);line-height:2.2">
1345          <li>Create account at the carousel engine → copy your API key from the dashboard</li>
1346          <li>Set Railway env var: <code style="color:var(--yellow)">TEMPLATED_API_KEY</code> = your API key</li>
1347          <li>Create a carousel template with <strong>4 pages</strong> — use a plain finance-themed background image on each page</li>
1348          <li>On each page add a centered image layer and name it exactly: <code style="color:var(--yellow)">content_image_1</code> (page 1), <code style="color:var(--yellow)">content_image_2</code> (page 2), <code style="color:var(--yellow)">content_image_3</code> (page 3), <code style="color:var(--yellow)">content_image_4</code> (page 4)</li>
1349          <li>Add a footer text layer named <code style="color:var(--yellow)">brand_name</code> on <strong>all 4 pages</strong></li>
1350          <li>Add a logo layer named <code style="color:var(--yellow)">logo-1</code> (populated from your Brand Kit logo)</li>
1351          <li>
1351Set Railway env var: <code style="color:var(--yellow)">TEMPLATED_TEMPLATE_FACTOZ</code> = your template ID</li>
1352        </ol>
1353        <div style="font-size:.7rem;color:var(--muted);margin-top:8px">Use <code>GET /api/templated/layers/{template_id}</code> in DevTools to verify layer names match.</div>
1354      </div>
1355    </div>
1356  </div>
1357</div>
1358
1359<!-- COLLAGE -->
1360<div class="page" id="page-collage">
1361  <div class="page-header">
1362    <div class="page-title"><span class="step-badge">3</span>Collage Builder</div>
1363    <div class="page-sub">Branded grid collages from chart images</div>
1364  </div>
1365  <div class="wiz-bar">
1366    <div class="wiz-step active" id="cstep-1"><span class="wiz-num">1</span><span class="wiz-label">Charts</span></div>
1367    <div class="wiz-line"></div>
1368    <div class="wiz-step" id="cstep-2"><span class="wiz-num">2</span><span class="wiz-label">Configure</span></div>
1369    <div class="wiz-line"></div>
1370    <div class="wiz-step" id="cstep-3"><span class="wiz-num">3</span><span class="wiz-label">Generate</span></div>
1371    <div class="wiz-line"></div>
1372    <div class="wiz-step" id="cstep-4"><span class="wiz-num">4</span><span class="wiz-label">Done</span></div>
1373  </div>
1374  <div id="collage-hint" style="font-size:.74rem;color:var(--muted);margin-bottom:14px;font-family:var(--mono)">Upload charts or send from Scanner to begin.</div>
1375  <div class="g2">
1376    <div>
1377      <div class="form-group"><label class="form-label">UPLOAD CHARTS (or uses scan results)</label><input type="file" id="collage-files" accept="image/*" multiple><div id="collage-files-info" style="font-size:.7rem;color:var(--text2);margin-top:4px"></div></div>
1378      <div class="form-group"><label class="form-label">ORIENTATION</label>
1379        <select id="collage-orientation"><option value="square">Square</option><option value="landscape">Landscape</option><option value="portrait">Portrait</option></select>
1380      </div>
1381      <div class="form-group"><label class="form-label">EXPORT TARGET</label>
1382        <select id="collage-export">
1383          <option value="instagram_square">Instagram Square (1080×1080)</option>
1384          <option value="instagram_portrait">Instagram Portrait (1080×1350)</option>
1385          <option value="tiktok">TikTok (1080×1920)</option>
1386          <option value="twitter">Twitter / X (1600×900)</option>
1387        </select>
1388      </div>
1389      <button class="btn btn-primary" onclick="buildCollage()" id="collage-btn">🧩 Generate Collage</button>
1390    </div>
1391    <div>
1392      <div id="collage-preview"><div style="background:var(--bg3);border:1px dashed var(--border);border-radius:8px;padding:40px;color:var(--muted);font-size:.78rem;text-align:center">No collage yet</div></div>
1393      <div id="collage-actions" style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap"></div>
1394    </div>
1395  </div>
1396  <!-- ── My Collages Library ──────────────────── -->
1397  <div class="card" style="margin-top:12px">
1398    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
1399      <h4 style="margin:0">MY COLLAGES (72h)</h4>
1400      <button class="btn btn-ghost" style="font-size:.7rem;padding:3px 8px" onclick="loadMyCollages()">&#8635; Refresh</button>
1401    </div>
1402    <div id="my-collages-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px">
1403      <p style="font-size:.75rem;color:var(--text2);grid-column:1/-1">Generate a collage to save it here.</p>
1404    </div>
1405  </div>
1406</div>
1407
1408<!-- COMMENTS -->
1409
1410<!-- POST -->
1411<div class="page" id="page-post">
1412  <div class="page-header">
1413    <div class="page-title"><span class="step-badge">4</span>Post</div>
1414    <div class="page-sub">Compose and post to X, Instagram, or both</div>
1415  </div>
1416  <div class="g2">
1417    <div>
1418      <div class="form-group"><label class="form-label">IMAGE</label><input type="file" id="post-file" accept="image/*" onchange="previewPost()"></div>
1419      <div id="post-img-preview" style="margin-bottom:12px"></div>
1420      <div class="form-group"><label class="form-label">CAPTION</label><textarea id="post-caption" rows="6" oninput="syncCaption()" placeholder="Your caption here..."></textarea></div>
1421      <div id="char-count" style="text-align:right;font-size:.65rem;color:var(--muted);margin-top:-8px;margin-bottom:10px">0 / 280</div>
1422      <div style="display:flex;gap:16px;margin-bottom:12px">
1423        <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:.78rem;font-weight:600"><input type="checkbox" id="post-x" checked style="width:auto"> 🐦 X</label>
1424        <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:.78rem;font-weight:600"><input type="checkbox" id="post-ig" style="width:auto"> 📸 Instagram</label>
1425      </div>
1426      <button class="btn btn-green btn-full" onclick="doPost()" id="post-btn">📤 Post Now</button>
1427      <div id="post-result" style="margin-top:10px"></div>
1428    </div>
1429    <div>
1430      <div class="card"><h4>LIVE PREVIEW</h4></div>
1431      <div class="caption-box" id="caption-preview">Start typing...</div>
1432      <hr>
1433      <div class="card"><h4>QUICK CAPTION</h4></div>
1434      <div class="form-group" style="margin-top:10px"><input type="text" id="quick-tickers" placeholder="AAPL, NVDA, TSLA"></div>
1435      <button class="btn btn-ghost btn-full" onclick="quickCaption()">✨ Generate Caption</button>
1436    </div>
1437  </div>
1438</div>
1439
1440<!-- CALENDAR -->
1441<div class="page" id="page-calendar">
1442  <div class="page-header">
1443    <div class="page-title"><span class="step-badge">5</span>Calendar</div>
1444    <div class="page-sub">Schedule and manage your content pipeline</div>
1445  </div>
1446  <div id="calendar-embed" style="width:100%;height:calc(100vh - 140px)"></div>
1447</div>
1448
1449<!-- LOCAL FILES -->
1450<div class="page" id="page-files">
1451  <div class="page-header">
1452    <div class="page-title"><i data-feather="upload"></i>Upload Images</div>
1453    <div class="page-sub">Upload PNG / JPG images directly to your Gallery</div>
1454  </div>
1455  <div class="card" style="max-width:520px">
1456    <h4>SELECT IMAGES</h4>
1457    <p style="color:var(--muted);font-size:.82rem;margin-bottom:14px">Choose one or more PNG/JPG files. They will be saved to your Gallery immediately.</p>
1458    <label class="btn btn-primary" style="cursor:pointer">
1459      📁 Choose Files
1460      <input type="file" accept="image/png,image/jpeg" multiple style="display:none"
1461             onchange="uploadLocalPageFiles(this)">
1462    </label>
1463    <div id="files-upload-status" style="margin-top:12px;font-size:.82rem;color:var(--muted)"></div>
1464    <div id="files-go-gallery" style="display:none;margin-top:14px">
1465      <button class="btn btn-ghost" onclick="showPage('gallery')">→ View in Gallery</button>
1466    </div>
1467  </div>
1468</div>
1469
1470<!-- BRAND KIT -->
1471<div class="page" id="page-brand">
1472  <div class="page-header">
1473    <div class="page-title"><i data-feather="sliders"></i>Brand Kit</div>
1474    <div class="page-sub">Visual identity — colors, logo, and defaults applied automatically to all carousels</div>
1475  </div>
1476  <div class="g2" style="gap:28px;align-items:start">
1477
1478    <!-- LEFT: Settings form -->
1479    <div>
1480      <!-- ── BRAND IDENTITY ── -->
1481      <div style="font-size:.58rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:12px">BRAND IDENTITY</div>
1482      <div class="form-group"><label class="form-label">BRAND NAME</label><input type="text" id="b-name" placeholder="FACTOZ" oninput="updateBrandPreview()"></div>
1483      <div class="form-group"><label class="form-label">TAGLINE</label><input type="text" id="b-tagline" placeholder="DEFI YIELD INTEL" oninput="updateBrandPreview()"></div>
1484
1485      <!-- Logo drop-zone -->
1486      <div style="font-size:.58rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin:18px 0 10px">LOGO</div>
1487      <div id="b-logo-zone" onclick="document.getElementById('b-logo').click()"
1488           style="border:2px dashed var(--border);border-radius:10px;padding:18px 16px;text-align:center;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:8px;transition:border-color .2s"
1489           onmouseenter="this.style.borderColor='var(--teal)'" onmouseleave="this.style.borderColor='var(--border)'">
1490        <img id="b-logo-preview" src="" style="display:none;max-height:58px;border-radius:6px;margin-bottom:4px">
1491        <div id="b-logo-hint" style="font-size:.72rem;color:var(--muted)">
1491Click to upload PNG (transparency recommended)</div>
1492        <input type="file" id="b-logo" accept="image/*" style="display:none" onchange="uploadLogo()">
1493      </div>
1494
1495      <hr style="margin:22px 0">
1496
1497      <!-- ── COLOR PALETTE ── -->
1498      <div style="font-size:.58rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:12px">COLOR PALETTE</div>
1499
1500      <div class="form-group">
1501        <label class="form-label">BACKGROUND</label>
1502        <div style="display:flex;align-items:center;gap:10px">
1503          <label style="width:34px;height:34px;border-radius:50%;border:2px solid var(--border);overflow:hidden;cursor:pointer;flex-shrink:0;display:block">
1504            <input type="color" id="b-bg-pick" value="#0D1117" style="width:200%;height:200%;margin:-50%;border:none;padding:0;cursor:pointer" oninput="$('b-bg').value=this.value;updateBrandPreview()">
1505          </label>
1506          <input type="text" id="b-bg" value="#0D1117" style="flex:1;font-family:var(--mono)" maxlength="7" oninput="if(/^#[0-9a-fA-F]{6}$/.test(this.value))$('b-bg-pick').value=this.value;updateBrandPreview()">
1507        </div>
1508      </div>
1509
1510      <div class="form-group">
1511        <label class="form-label">ACCENT / BORDER</label>
1512        <div style="display:flex;align-items:center;gap:10px">
1513          <label style="width:34px;height:34px;border-radius:50%;border:2px solid var(--border);overflow:hidden;cursor:pointer;flex-shrink:0;display:block">
1514            <input type="color" id="b-acc-pick" value="#00BCD4" style="width:200%;height:200%;margin:-50%;border:none;padding:0;cursor:pointer" oninput="$('b-acc').value=this.value;updateBrandPreview()">
1515          </label>
1516          <input type="text" id="b-acc" value="#00BCD4" style="flex:1;font-family:var(--mono)" maxlength="7" oninput="if(/^#[0-9a-fA-F]{6}$/.test(this.value))$('b-acc-pick').value=this.value;updateBrandPreview()">
1517        </div>
1518      </div>
1519
1520      <div class="form-group">
1521        <label class="form-label">TEXT COLOR</label>
1522        <div style="display:flex;align-items:center;gap:10px">
1523          <label style="width:34px;height:34px;border-radius:50%;border:2px solid var(--border);overflow:hidden;cursor:pointer;flex-shrink:0;display:block">
1524            <input type="color" id="b-txt-pick" value="#E8F0F7" style="width:200%;height:200%;margin:-50%;border:none;padding:0;cursor:pointer" oninput="$('b-txt').value=this.value;updateBrandPreview()">
1525          </label>
1526          <input type="text" id="b-txt" value="#E8F0F7" style="flex:1;font-family:var(--mono)" maxlength="7" oninput="if(/^#[0-9a-fA-F]{6}$/.test(this.value))$('b-txt-pick').value=this.value;updateBrandPreview()">
1527        </div>
1528      </div>
1529
1530      <hr style="margin:22px 0">
1531
1532      <!-- ── CONTENT DEFAULTS ── -->
1533      <div style="font-size:.58rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:12px">CONTENT DEFAULTS</div>
1534
1535      <div class="form-group">
1536        <label class="form-label">DEFAULT CAROUSEL TEMPLATE</label>
1537        <select id="b-template">
1538          <option value="factoz">🌑 Factoz — Dark moody</option>
1539          <option value="terminal">💻 Terminal — Bloomberg</option>
1540          <option value="editorial">📰 Editorial — Magazine</option>
1541        </select>
1542      </div>
1543
1544      <div class="form-group"><label class="form-label">DEFAULT HASHTAGS</label>
1545        <input type="text" id="b-hashtags" placeholder="#Factoz #TechnicalAnalysis #Stocks">
1546      </div>
1547
1548      <div class="form-group"><label class="form-label">WATERMARK TEXT</label>
1549        <input type="text" id="b-wm" placeholder="FACTOZ" oninput="updateBrandPreview()">
1550      </div>
1551
1552      <div class="form-group">
1553        <label class="form-label">WATERMARK OPACITY — <span id="b-wm-op-label" style="color:var(--teal)">11%</span></label>
1554        <input type="range" id="b-wm-op" min="0" max="255" value="28" style="width:100%;accent-color:var(--teal)"
1555               oninput="$('b-wm-op-label').textContent=Math.round(this.value/2.55)+'%'">
1556      </div>
1557
1558      <div style="display:flex;gap:22px;margin-bottom:18px">
1559        <label style="display:flex;align-items:center;gap:7px;cursor:pointer;font-size:.78rem;font-weight:600;color:var(--text2)">
1560          <input type="checkbox" id="b-show-wm" style="width:auto;accent-color:var(--teal)"> Show watermark
1561        </label>
1562        <label style="display:flex;align-items:center;gap:7px;cursor:pointer;font-size:.78rem;font-weight:600;color:var(--text2)">
1563          <input type="checkbox" id="b-show-tag" checked style="width:auto;accent-color:var(--teal)"> Corner tag
1564        </label>
1565      </div>
1566
1567      <div class="form-group"><label class="form-label">DEFAULT EXPORT FORMAT</label>
1568        <select id="b-export">
1569          <option value="instagram_square">Instagram Square</option>
1570          <option value="instagram_portrait">Instagram Portrait</option>
1571          <option value="tiktok">TikTok</option>
1572          <option value="twitter">Twitter / X</option>
1573        </select>
1574      </div>
1575
1576      <button class="btn btn-primary btn-full" onclick="saveBrand()" id="brand-save-btn">💾 Save Brand Kit</button>
1577      <div id="brand-save-result" style="margin-top:8px"></div>
1578    </div>
1579
1580    <!-- RIGHT: Live preview -->
1581    <div style="position:sticky;top:20px">
1582      <div style="font-size:.58rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:12px">LIVE PREVIEW</div>
1583
1584      <!-- Mock slide card -->
1585      <div id="brand-preview-card" style="width:100%;max-width:300px;aspect-ratio:1;border-radius:14px;padding:24px 20px;display:flex;flex-direction:column;justify-content:space-between;background:#0D1117;border:2px solid #00BCD4;position:relative;overflow:hidden;transition:background .25s,border-color .25s">
1586        <div id="bp-accent-top" style="position:absolute;top:0;left:0;right:0;height:3px;background:#00BCD4;transition:background .25s"></div>
1587        <!-- Header -->
1588        <div style="display:flex;justify-content:space-between;align-items:flex-start">
1589          <div>
1590            <div id="bp-name" style="font-size:1rem;font-weight:800;letter-spacing:2px;color:#E8F0F7;font-family:var(--mono);transition:color .25s">FACTOZ</div>
1591            <div id="bp-tagline" style="font-size:.55rem;color:#00BCD4;margin-top:3px;font-family:var(--mono);letter-spacing:1px;transition:color .25s">MARKET INTELLIGENCE</div>
1592          </div>
1593          <img id="bp-logo" src="" style="display:none;height:32px;border-radius:4px">
1594        </div>
1595        <!-- Center mock signal -->
1596        <div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:14px 0">
1597          <div id="bp-signal" style="font-size:1.5rem;font-weight:900;letter-spacing:1px;color:#E8F0F7;font-family:var(--mono);transition:color .25s">NVDA ▲</div>
1598          <div id="bp-sub" style="font-size:.58rem;color:#00BCD4;font-family:var(--mono);transition:color .25s">
1598BULLISH · EMA9 CROSS</div>
1599          <!-- Accent divider -->
1600          <div id="bp-divider" style="width:60%;height:1px;background:#00BCD4;opacity:.5;margin-top:4px;transition:background .25s"></div>
1601        </div>
1602        <!-- Footer -->
1603        <div style="display:flex;justify-content:space-between;align-items:center">
1604          <div id="bp-wm" style="font-size:.5rem;color:rgba(232,240,247,.25);font-family:var(--mono);letter-spacing:3px;transition:color .25s">FACTOZ</div>
1605          <div id="bp-dot" style="width:7px;height:7px;border-radius:50%;background:#00BCD4;transition:background .25s"></div>
1606        </div>
1607        <!-- Corner accent triangle -->
1608        <div id="bp-corner" style="position:absolute;bottom:0;right:0;width:36px;height:36px;background:linear-gradient(135deg,transparent 50%,#00BCD4 50%);opacity:.35;transition:background .25s"></div>
1609      </div>
1610
1611      <div style="margin-top:10px;font-size:.65rem;color:var(--muted);text-align:center">Updates live as you edit ↑</div>
1612
1613      <!-- Palette chips -->
1614      <div style="margin-top:16px">
1615        <div style="font-size:.58rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:8px">CURRENT PALETTE</div>
1616        <div style="display:flex;gap:8px;align-items:center">
1617          <div id="chip-bg" title="Background" style="width:32px;height:32px;border-radius:6px;border:1px solid var(--border);background:#0D1117;transition:background .25s"></div>
1618          <div id="chip-acc" title="Accent" style="width:32px;height:32px;border-radius:6px;border:1px solid var(--border);background:#00BCD4;transition:background .25s"></div>
1619          <div id="chip-txt" title="Text" style="width:32px;height:32px;border-radius:6px;border:1px solid var(--border);background:#E8F0F7;transition:background .25s"></div>
1620          <div style="font-size:.62rem;color:var(--muted);margin-left:4px">BG · Accent · Text</div>
1621        </div>
1622      </div>
1623    </div>
1624
1625  </div>
1626</div>
1627
1628<!-- API KEYS -->
1629<div class="page" id="page-settings">
1630  <div class="page-header">
1631    <div class="page-title"><i data-feather="settings"></i>Settings</div>
1632    <div class="page-sub">Your keys are stored securely per account</div>
1633  </div>
1634
1635  <!-- ── Feed Subscriptions ────────────────────────────────────────── -->
1636  <div class="card" style="margin-bottom:18px">
1637    <h4>FEED SUBSCRIPTIONS</h4>
1638    <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 0">
1639      <div>
1640        <div style="font-size:.8rem;font-weight:600">🇺🇸 DJT Posts</div>
1641        <div style="font-size:.7rem;color:var(--muted)">Show Trump's posts from X and Truth Social in your news feed</div>
1642      </div>
1643      <button id="djt-toggle-btn" class="btn btn-ghost" onclick="toggleDjtSubscribe()" style="min-width:64px">…</button>
1644    </div>
1645  </div>
1646
1647  <div class="g2">
1648    <!-- Admin-only keys (Chart provider + Cloudinary) — hidden for non-admin -->
1649    <div id="admin-keys-section">
1650    </div>
1651    <!-- User-managed keys (X/Twitter + Integrations) — visible to all -->
1652    <div>
1653      <div class="card"><h4>X / TWITTER</h4></div>
1654      <div class="form-group"><label class="form-label">Consumer Key <span style="font-weight:400;font-size:.8em;opacity:.65">(API Key)</span></label><input type="password" id="k-x-api"></div>
1655      <div class="form-group"><label class="form-label">Consumer Secret <span style="font-weight:400;font-size:.8em;opacity:.65">(API Secret)</span></label><input type="password" id="k-x-secret"></div>
1656      <div class="form-group"><label class="form-label">Access Token</label><input type="password" id="k-x-token"></div>
1657      <div class="form-group"><label class="form-label">Access Token Secret</label><input type="password" id="k-x-tsecret"></div>
1658    </div>
1659  </div>
1660  <button class="btn btn-primary" onclick="saveKeys()">💾 Save API Keys</button>
1661  <div id="keys-result" style="margin-top:10px"></div>
1662</div>
1663
1664<!-- HISTORY -->
1665<div class="page" id="page-history">
1666  <div class="page-header">
1667    <div class="page-title"><i data-feather="clock"></i>History</div>
1668    <div class="page-sub">Posting log across all platforms</div>
1669  </div>
1670  <div class="g3" style="margin-bottom:18px">
1671    <div class="stat-card"><div class="lbl">X POSTS</div><div class="val" id="h-x">—</div><div class="sub">successful</div></div>
1672    <div class="stat-card"><div class="lbl">INSTAGRAM</div><div class="val" id="h-ig">—</div><div class="sub">successful</div></div>
1673    <div class="stat-card"><div class="lbl">TOTAL</div><div class="val" id="h-tot">—</div><div class="sub">all platforms</div></div>
1674  </div>
1675  <div style="display:flex;gap:7px;margin-bottom:14px">
1676    <button class="btn btn-primary" onclick="loadHistory('all')">All</button>
1677    <button class="btn btn-ghost" onclick="loadHistory('x')">X</button>
1678    <button class="btn btn-ghost" onclick="loadHistory('instagram')">Instagram</button>
1679  </div>
1680  <div id="history-list"></div>
1681</div>
1682
1683<!-- GUIDE -->
1684<div class="page" id="page-guide">
1685  <div class="page-header">
1686    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/shcfcebj.json" trigger="hover" style="width:28px;height:28px"></lord-icon>What is Factoz?</div>
1687    <div class="page-sub">Global market intelligence — autonomous, institutional-grade, delivered daily</div>
1688  </div>
1689
1690  <!-- Hero statement -->
1691  <div class="card" style="margin-bottom:18px;border-color:rgba(0,200,183,.3);background:linear-gradient(135deg,var(--bg2),var(--bg3))">
1692    <div style="display:flex;align-items:flex-start;gap:20px">
1693      <div style="flex:1">
1694        <div style="font-family:var(--mono);font-size:.6rem;color:var(--teal);letter-spacing:3px;margin-bottom:10px">PLATFORM OVERVIEW</div>
1695        <div style="font-size:1.05rem;font-weight:700;color:var(--text);line-height:1.5;margin-bottom:10px">Factoz is a global market intelligence and research platform.</div>
1696        <div style="font-size:.8rem;color:var(--text2);line-height:1.75">
1697          Every day, Factoz autonomously scans <strong style="color:var(--text)">5,900+ tickers</strong>, monitors global macro data, tracks options flow, and generates an institutional-grade intelligence brief — all without any user input. Social posting is available as a secondary capability for users who want to distribute their insights.
1698        </div>
1699      </div>
1700    </div>
1701  </div>
1702
1703  <!-- Core capabilities grid -->
1704  <div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:3px;margin-bottom:10px">CORE CAPABILITIES</div>
1705  <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:18px">
1706
1707    <div class="card" style="border-color:rgba(0,200,183,.2);cursor:pointer" onclick="go('news')">
1708      <div style="font-size:1.3rem;margin-bottom:8px">📋</div>
1709      <div style="font-weight:700;font-size:.82rem;margin-bottom:5px;color:var(--text)">Daily Intelligence Brief</div>
1710      <div style="font-size:.72rem;color:var(--text2);line-height:1.6">AI-written pre-market brief covering US, Europe, LATAM and Asia — real futures prices, risk alerts, top stories, editorial view. Auto-sent every weekday 6:30am EST.</div>
1711      <div style="margin-top:8px;font-size:.65rem;color:var(--teal);font-family:var(--mono)">NEWS FEED → DAILY BRIEFS</div>
1712    </div>
1713
1714    <div class="card" style="border-color:rgba(0,200,183,.2);cursor:pointer" onclick="go('scanner')">
1715      <div style="font-size:1.3rem;margin-bottom:8px">⚡</div>
1716      <div style="font-weight:700;font-size:.82rem;margin-bottom:5px;color:var(--text)">Autonomous Market Scanner</div>
1717      <div style="font-size:.72rem;color:var(--text2);line-height:1.6">
1717Scans 5,900+ tickers daily — volume surges, 52-week breakouts, gap & hold, RSI extremes, support/resistance. Runs automatically. No user input required.</div>
1718      <div style="margin-top:8px;font-size:.65rem;color:var(--teal);font-family:var(--mono)">SIGNALS → SCANNER</div>
1719    </div>
1720
1721    <div class="card" style="border-color:rgba(0,200,183,.2);cursor:pointer" onclick="go('news')">
1722      <div style="font-size:1.3rem;margin-bottom:8px">⚡</div>
1723      <div style="font-weight:700;font-size:.82rem;margin-bottom:5px;color:var(--text)">Options Flow Monitor</div>
1724      <div style="font-size:.72rem;color:var(--text2);line-height:1.6">Detects unusual options activity 4× per day across the full ticker universe — flags contracts with vol/OI >3×, premium >$50K, short-dated directional bets.</div>
1725      <div style="margin-top:8px;font-size:.65rem;color:var(--teal);font-family:var(--mono)">OVERVIEW → OPTIONS FLOW</div>
1726    </div>
1727
1728    <div class="card" style="border-color:rgba(0,200,183,.2);cursor:pointer" onclick="go('news')">
1729      <div style="font-size:1.3rem;margin-bottom:8px">🌍</div>
1730      <div style="font-weight:700;font-size:.82rem;margin-bottom:5px;color:var(--text)">Global News Intelligence</div>
1731      <div style="font-size:.72rem;color:var(--text2);line-height:1.6">Real-time market news, earnings, economic calendar, insider transactions, analyst ratings, macro data — all curated and filterable in one feed.</div>
1732      <div style="margin-top:8px;font-size:.65rem;color:var(--teal);font-family:var(--mono)">MARKETS → NEWS FEED</div>
1733    </div>
1734
1735    <div class="card" style="border-color:rgba(0,200,183,.2);cursor:pointer" onclick="go('macro')">
1736      <div style="font-size:1.3rem;margin-bottom:8px">📈</div>
1737      <div style="font-weight:700;font-size:.82rem;margin-bottom:5px;color:var(--text)">FRED Macro Dashboard</div>
1738      <div style="font-size:.72rem;color:var(--text2);line-height:1.6">Live FRED economic indicators — CPI, GDP, unemployment, yield curve — with AI-generated macro regime analysis and custom alert triggers.</div>
1739      <div style="margin-top:8px;font-size:.65rem;color:var(--teal);font-family:var(--mono)">SIGNALS → FRED / MACRO</div>
1740    </div>
1741
1742    <div class="card" style="border-color:rgba(0,200,183,.2);cursor:pointer" onclick="go('financials')">
1743      <div style="font-size:1.3rem;margin-bottom:8px">💰</div>
1744      <div style="font-weight:700;font-size:.82rem;margin-bottom:5px;color:var(--text)">Financial Statement Generator</div>
1745      <div style="font-size:.72rem;color:var(--text2);line-height:1.6">Any public company, any period — P&L, balance sheet, cash flow generated in seconds. Export-ready for client presentations or research.</div>
1746      <div style="margin-top:8px;font-size:.65rem;color:var(--teal);font-family:var(--mono)">ANALYSIS → FINANCIALS</div>
1747    </div>
1748
1749  </div>
1750
1751  <!-- Weekly digest -->
1752  <div class="card" style="margin-bottom:18px;border-color:rgba(46,204,142,.2)">
1753    <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px">
1754      <div style="font-size:1.2rem">📊</div>
1755      <div>
1756        <div style="font-weight:700;font-size:.85rem;color:var(--text)">Weekly Signal Performance Digest</div>
1757        <div style="font-size:.7rem;color:var(--muted);font-family:var(--mono)">EVERY MONDAY 7:00AM EST</div>
1758      </div>
1759    </div>
1760    <div style="font-size:.78rem;color:var(--text2);line-height:1.7">Every Monday, Factoz sends a performance report showing how every autonomous signal performed during the week — entry price, current price, % change, and win rate. It also includes the week's unusual options flow hits. Sent automatically to all subscribers.</div>
1761  </div>
1762
1763  <!-- Secondary: content & posting -->
1764  <div style="font-family:var(--mono);font-size:.6rem;color:var(--muted);letter-spacing:3px;margin-bottom:10px">ADDITIONAL CAPABILITIES</div>
1765  <div id="guide-sections">
1766
1767    <div class="guide-section">
1768      <div class="guide-header" onclick="toggleGuide('g-ai')">
1769        <span>💬 AI Market Analyst — Ticker Deep Dives</span>
1770        <span class="guide-arrow" id="g-ai-arrow">▶</span>
1771      </div>
1772      <div class="guide-body" id="g-ai" style="display:none">
1773        <p style="font-size:.8rem;color:var(--text2);margin-bottom:10px">Ask any finance question — the AI analyst uses live scanner data as context for its answers.</p>
1774        <ul class="guide-steps" style="list-style:none;padding-left:0">
1775          <li>🔍 <strong>Run a scan first</strong> — the AI automatically sees your tickers and signals</li>
1776          <li>✍️ Ask specific questions: "Why is NVDA up today?", "Summarize AAPL earnings risk", "What does this breakout signal mean?"</li>
1777          <li>⚡ Use the <strong>quick-prompt chips</strong> for the most common question types</li>
1778        </ul>
1779      </div>
1780    </div>
1781
1782  </div>
1783</div>
1784
1785<!-- NEWS FEED -->
1786<div class="page" id="page-news">
1787  <div class="page-header">
1788    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/yquwpfkc.json" trigger="hover" stroke="bold" style="width:28px;height:28px"></lord-icon>News Feed</div>
1789    <div class="page-sub">Admin-curated content — use directly in your Factoz workflow</div>
1790  </div>
1791  <div style="display:flex;gap:7px;margin-bottom:16px;flex-wrap:wrap">
1792    <button class="btn btn-ghost news-filter-btn active-filter" data-type="all" onclick="filterNews('all')">All</button>
1793    <button class="btn btn-ghost news-filter-btn" data-type="chart" onclick="filterNews('chart')">📊 Charts</button>
1794    <button class="btn btn-ghost news-filter-btn" data-type="news" onclick="filterNews('news')">📰 News</button>
1795    <button class="btn btn-ghost news-filter-btn" data-type="economic" onclick="filterNews('economic')">📈 Economic</button>
1796    <button class="btn btn-ghost news-filter-btn" data-type="earnings" onclick="filterNews('earnings')">💰 Earnings</button>
1797    <button class="btn btn-ghost news-filter-btn" data-type="company_analysis" onclick="filterNews('company_analysis')">🏢 Analysis</button>
1798    <button class="btn btn-ghost news-filter-btn" data-type="brief" onclick="filterNews('brief')">📋 Daily Briefs</button>
1799    <button class="btn btn-ghost news-filter-btn" data-type="macro" onclick="filterNews('macro')">🌍 Macro</button>
1800    <button class="btn btn-ghost news-filter-btn" data-type="tradingview" onclick="filterNews('tradingview')">💡 Ideas</button>
1801    <button class="btn btn-ghost news-filter-btn" data-type="insider" onclick="filterNews('insider')">🔍 Insider</button>
1802    <button class="btn btn-ghost news-filter-btn" id="djt-filter-btn" data-type="djt" onclick="filterNews('djt')" style="display:none">🇺🇸 DJT Tweets</button>
1803    <button class="btn btn-ghost" style="margin-left:auto;font-size:.7rem;padding:5px 10px" onclick="loadNews()">↻ Refresh</button>
1804    <button class="btn btn-ghost" style="font-size:.65rem;padding:4px 8px" title="Collapse all" onclick="collapseAllClusters()">⊖</button>
1805  </div>
1806  <!-- Ad slot: news-sidebar -->
1807  <div id="ad-news-sidebar" class="ad-slot" style="margin-bottom:14px"></div>
1808
1809  <div id="eco-calendar" style="display:none;margin-bottom:20px">
1810    <div class="tradingview-widget-container" style="border-radius:10px;overflow:hidden">
1811      <div class="tradingview-widget-container__widget"></div>
1812      
1812<script type="text/javascript"
1813        src="https://s3.tradingview.com/external-embedding/embed-widget-events.js" async>
1814      {
1815        "colorTheme": "dark",
1816        "isTransparent": true,
1817        "width": "100%",
1818        "height": "600",
1819        "locale": "en",
1820        "importanceFilter": "0,1",
1821        "countryFilter": "us"
1822      }
1823      </script>
1823
1824    </div>
1825  </div>
1826  <div style="margin-bottom:12px">
1827    <input id="news-search" type="text" placeholder="🔍  Search ticker or keyword across all categories…" oninput="searchNews(this.value)" style="width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:9px 14px;color:var(--text);font-size:.8rem;outline:none;box-sizing:border-box">
1828  </div>
1829  <div id="news-list"><div class="card"><p>Loading news...</p></div></div>
1830</div>
1831
1832<!-- FX / CRYPTO -->
1833<div class="page" id="page-forex">
1834  <div class="page-header">
1835    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/tyntlpjn.json" trigger="hover" stroke="bold" style="width:28px;height:28px"></lord-icon>FX / Crypto</div>
1836    <div class="page-sub">Live currency rates &amp; top cryptocurrencies — auto-updating</div>
1837  </div>
1838  <div style="margin-bottom:8px;font-size:.7rem;font-weight:700;color:var(--muted);letter-spacing:2px">FOREX CROSS RATES</div>
1839  <div style="border-radius:10px;overflow:hidden;border:1px solid var(--border);height:500px;margin-bottom:20px">
1840    <iframe src="https://www.widgets.investing.com/live-currency-cross-rates?theme=darkTheme&roundedCorners=true"
1841      width="100%" height="100%" frameborder="0" allowtransparency="true" marginwidth="0" marginheight="0"
1842      style="display:block"></iframe>
1843  </div>
1844  <div style="margin-bottom:8px;font-size:.7rem;font-weight:700;color:var(--muted);letter-spacing:2px">TOP CRYPTOCURRENCIES</div>
1845  <div style="border-radius:10px;overflow:hidden;border:1px solid var(--border);height:500px;margin-bottom:8px">
1846    <iframe src="https://www.widgets.investing.com/top-cryptocurrencies?theme=darkTheme&roundedCorners=true"
1847      width="100%" height="100%" frameborder="0" allowtransparency="true" marginwidth="0" marginheight="0"
1848      style="display:block"></iframe>
1849  </div>
1850  <div style="font-size:.65rem;color:var(--muted);text-align:right">
1851    Powered by <a href="https://www.investing.com/" target="_blank" rel="nofollow" style="color:var(--teal)">Investing.com</a>
1852  </div>
1853</div>
1854<!-- ADMIN PANEL -->
1855<!-- MOVERS -->
1856<div class="page" id="page-movers">
1857  <div class="page-header">
1858    <div class="page-title">
1858<lord-icon src="https://cdn.lordicon.com/mwhabkof.json" trigger="hover" style="width:28px;height:28px"></lord-icon>Pre-Market Top Movers</div>
1859    <div class="page-sub" style="margin-left:0">Top 10 gainers &amp; losers before the opening bell — updated daily at 8am EST</div>
1860  </div>
1861  <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
1862    <div id="movers-meta" style="font-size:.72rem;color:var(--text2);font-family:var(--mono)"></div>
1863  </div>
1864  <div id="movers-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
1865    <p style="font-size:.8rem;color:var(--text2);grid-column:1/-1">Loading...</p>
1866  </div>
1867</div>
1868
1869<!-- WATCHLIST -->
1870<div class="page" id="page-watchlist">
1871  <div class="page-header">
1872    <div class="page-title"><i data-feather="eye"></i><span data-i18n="watchlist.title">Watchlist</span></div>
1873    <div class="page-sub" data-i18n="watchlist.sub">Tickers que quieres seguir de cerca — sin comprar todavía</div>
1874  </div>
1875  <div style="display:flex;gap:8px;margin-bottom:18px;max-width:360px">
1876    <input type="text" id="wl-ticker-input" placeholder="AAPL" style="flex:1;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:9px 12px;color:var(--text);font-size:.8rem;text-transform:uppercase" onkeydown="if(event.key==='Enter')addToWatchlist()">
1877    <button class="btn btn-primary" onclick="addToWatchlist()" data-i18n="watchlist.add_btn">+ Agregar</button>
1878  </div>
1879  <div id="watchlist-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px">
1880    <p style="font-size:.8rem;color:var(--text2);grid-column:1/-1" data-i18n="watchlist.loading">Cargando...</p>
1881  </div>
1882</div>
1883
1884<!-- FINANCIAL STATEMENTS -->
1885<div class="page" id="page-financials">
1886  <div class="page-header">
1887    <div class="page-title"><i data-feather="file-text"></i>Financial Statements</div>
1888    <div class="page-sub">GAAP statements — public companies via Sonar AI, or upload your own Excel/CSV</div>
1889  </div>
1890
1891  <!-- Mode toggle -->
1892  <div style="display:flex;gap:8px;margin-bottom:18px">
1893    <button id="fin-mode-public" class="btn active-filter" onclick="setFinMode('public')">🏢 Public Company</button>
1894    <button id="fin-mode-private" class="btn btn-ghost" onclick="setFinMode('private')">📊 Private (Upload)</button>
1895    <button id="fin-mode-raw" class="btn btn-ghost" onclick="setFinMode('raw')">📝 Raw Input</button>
1896  </div>
1897
1898  <!-- Public mode inputs -->
1899  <div id="fin-public-form" class="card" style="margin-bottom:14px">
1900    <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
1901      <div class="form-group" style="flex:1;min-width:180px;margin-bottom:0">
1902        <label class="form-label">COMPANY / TICKER</label>
1903        <input id="fin-company" type="text" placeholder="Apple Inc or AAPL">
1904      </div>
1905      <div class="form-group" style="margin-bottom:0">
1906        <label class="form-label">PERIOD</label>
1907        <select id="fin-period">
1908          <option value="annual">Annual</option>
1909          <option value="quarterly">Quarterly</option>
1910          <option value="ttm">Trailing 12 Months</option>
1911        </select>
1912      </div>
1913      <button class="btn btn-primary" onclick="generateFinancials()">Generate Statements</button>
1914      <button class="btn btn-ghost" onclick="clearFinForm('public')" style="font-size:.75rem">✕ Clear</button>
1915    </div>
1916  </div>
1917
1918  <!-- Private mode inputs -->
1919  <div id="fin-private-form" class="card" style="margin-bottom:14px;display:none">
1920    <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
1921      <div class="form-group" style="flex:1;min-width:180px;margin-bottom:0">
1922        <label class="form-label">COMPANY NAME</label>
1923        <input id="fin-private-company" type="text" placeholder="My Company LLC">
1924      </div>
1925      <div class="form-group" style="margin-bottom:0">
1926        <label class="form-label">PERIOD</label>
1927        <select id="fin-private-period">
1928          <option value="annual">Annual</option>
1929          <option value="quarterly">Quarterly</option>
1930        </select>
1931      </div>
1932      <label class="btn btn-ghost" style="cursor:pointer;margin-bottom:0" title="Photo of handwritten statements → Claude reads it → pre-filled Excel">
1933        📷 Scan Photo
1934        <input type="file" id="fin-scan-file" accept="image/*" style="display:none" onchange="scanFinancialPhoto(this)">
1935      </label>
1936      <span id="fin-scan-status" style="font-size:.72rem;color:var(--muted);align-self:center"></span>
1937      <a class="btn btn-ghost" href="/api/financials/template" download style="margin-bottom:0">⬇ Template</a>
1938      <label class="btn btn-ghost" style="cursor:pointer;margin-bottom:0">
1939        📎 Upload Excel/CSV
1940        <input type="file" id="fin-file" accept=".xlsx,.xls,.csv" style="display:none" onchange="$('fin-file-name').textContent=this.files[0]?.name||''">
1941      </label>
1942      <span id="fin-file-name" style="font-size:.72rem;color:var(--text2);align-self:center"></span>
1943      <button class="btn btn-primary" onclick="generateFinancialsPrivate()">Generate Statements</button>
1944      <button class="btn btn-ghost" onclick="clearFinForm('private')" style="font-size:.75rem">✕ Clear</button>
1945    </div>
1946  </div>
1947
1948  <!-- Raw input form -->
1949  <div id="fin-raw-form" class="card" style="margin-bottom:14px;display:none">
1950    <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-bottom:10px">
1951      <div class="form-group" style="flex:1;min-width:180px;margin-bottom:0">
1952        <label class="form-label">COMPANY NAME</label>
1953        <input id="fin-raw-company" type="text" placeholder="My Company LLC">
1954      </div>
1955      <div class="form-group" style="margin-bottom:0">
1956        <label class="form-label">PERIOD</label>
1957        <select id="fin-raw-period">
1958          <option value="annual">Annual</option>
1959          <option value="quarterly">Quarterly</option>
1960          <option value="ttm">Trailing 12 Months</option>
1961        </select>
1962      </div>
1963      <button class="btn btn-primary" onclick="generateFinancialsRaw()">Generate Statements</button>
1964      <button class="btn btn-ghost" onclick="clearFinForm('raw')" style="font-size:.75rem">✕ Clear</button>
1965    </div>
1966    <div class="form-group" style="margin-bottom:0">
1967      <label class="form-label">PASTE RAW FINANCIAL DATA</label>
1968      <textarea id="fin-raw-text" rows="10" placeholder="Paste anything — account balances, journal entries, gains and expenses, ledger export... Sonar maps it to GAAP categories.&#10;&#10;Example:&#10;Sales Revenue: 1,200,000&#10;Cost of goods sold: 700,000&#10;Office rent: 50,000&#10;Salaries: 300,000&#10;Cash in bank: 180,000&#10;Accounts receivable: 90,000&#10;Long-term bank loan: 400,000" style="width:100%;resize:vertical;background:var(--input);border:1px solid var(--border);border-radius:6px;padding:10px;color:var(--text);font-size:.8rem;font-family:var(--mono)"></textarea>
1969    </div>
1970  </div>
1971
1972  <div id="fin-status" style="font-size:.78rem;color:var(--muted);margin-bottom:10px"></div>
1973
1974  <!-- Results — 4 tabs -->
1975  <div id="fin-results" style="display:none">
1976    <div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap">
1977      <button class="btn fin-tab active-filter" data-tab="balance" onclick="showFinTab('balance')">Balance Sheet</button>
1978      <button class="btn btn-ghost fin-tab" data-tab="income" onclick="showFinTab('income')">Income Statement</button>
1979      <button class="btn btn-ghost fin-tab" data-tab="cashflow" onclick="showFinTab('cashflow')">Cash Flow</button>
1980      <button class="btn btn-ghost fin-tab" data-tab="equity" onclick="showFinTab('equity')">Shareholders' Equity</button>
1981      <button class="btn btn-ghost fin-tab" data-tab="summary" onclick="showFinTab('summary')">Summary</button>
1982    </div>
1983    <div id="fin-tab-content" style="background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px;overflow-x:auto"></div>
1984    <div style="display:flex;gap:8px;margin-top:10px">
1985      <button class="btn btn-ghost" onclick="downloadFinTab()" style="font-size:.75rem">⬇ Download CSV</button>
1986      <button class="btn btn-ghost" onclick="newFinStatement()" style="font-size:.75rem">+ New Statement</button>
1987    </div>
1988  </div>
1989
1990  <!-- History log -->
1991  <div id="fin-history-wrap" style="margin-top:28px;display:none">
1992    <div style="font-size:.65rem;color:var(--muted);letter-spacing:1px;margin-bottom:10px">PREVIOUSLY GENERATED</div>
1993    <div id="fin-history-list"></div>
1994    <button id="fin-history-more" class="btn btn-ghost" onclick="loadFinHistory(true)" style="display:none;margin-top:8px;font-size:.75rem">Load more</button>
1995  </div>
1996
1997  <!-- A/B Comparison -->
1998  <div id="fin-compare-wrap" style="display:none;margin-top:20px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:16px">
1999    <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap">
2000      <span style="font-size:.65rem;color:var(--muted);letter-spacing:1px">COMPARISON</span>
2001      <span id="fin-compare-label" style="font-size:.78rem;font-weight:700;color:var(--text);font-family:var(--mono);flex:1"></span>
2002      <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px" onclick="downloadFinCompareCSV()">⬇ CSV</button>
2003      <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px" onclick="clearFinCompare()">✕ Clear</button>
2004    </div>
2005    <div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap">
2006      <button id="fcmp-tab-balance"  class="btn active-filter" style="font-size:.7rem;padding:4px 10px" onclick="showFinCompareTab('balance')">Balance Sheet</button>
2007      <button id="fcmp-tab-income"   class="btn btn-ghost"     style="font-size:.7rem;padding:4px 10px" onclick="showFinCompareTab('income')">Income Stmt</button>
2008      <button id="fcmp-tab-cashflow" class="btn btn-ghost"     style="font-size:.7rem;padding:4px 10px" onclick="showFinCompareTab('cashflow')">Cash Flow</button>
2009      <button id="fcmp-tab-equity"   class="btn btn-ghost"     style="font-size:.7rem;padding:4px 10px" onclick="showFinCompareTab('equity')">Equity</button>
2010    </div>
2011    <div id="fin-compare-table"></div>
2012  </div>
2013</div>
2014
2015<!-- SECTOR HEATMAP -->
2016<div class="page" id="page-heatmap">
2017  <div class="page-header">
2018    <div class="page-title"><i data-feather="grid"></i>Heatmaps</div>
2019    <div class="page-sub">Stocks · Crypto — TradingView</div>
2020  </div>
2021  <div class="macro-tabs" style="margin-bottom:14px">
2022    <button class="macro-tab active" onclick="heatmapTab('stock')">📈 Stocks</button>
2023    <button class="macro-tab" onclick="heatmapTab('crypto')">🪙 Crypto</button>
2024  </div>
2025
2026  <div id="heatmap-pane-stock" class="macro-pane active" style="height:700px">
2027    <div style="text-align:right;margin-bottom:6px"><a href="https://in.tradingview.com/?aff_id=164648" target="_blank" rel="noopener" style="display:inline-block;background:#1a2340;border:1px solid #E2C275;border-radius:20px;padding:4px 12px;font-size:.68rem;color:#E2C275;text-decoration:none;letter-spacing:.3px">🎁 Get <strong>$15 USD off</strong> TradingView — Open here ↗</a></div>
2028    <div class="tradingview-widget-container" style="height:calc(100% - 32px);background:#0b132b;border:1px solid #202e56;border-radius:10px;overflow:hidden">
2029      <div class="tradingview-widget-container__widget" style="height:calc(100% - 18px)"></div>
2030      <div class="tradingview-widget-copyright" style="font-size:.6rem;color:var(--muted);opacity:.5"><a href="https://www.tradingview.com/" rel="noopener nofollow" target="_blank" style="color:var(--muted);text-decoration:none">Stock Heatmap by TradingView</a></div>
2031      
2031<script type="text/javascript" src="https://s3.tradingview.com/external-embedding/embed-widget-stock-heatmap.js" async>
2032      {"dataSource":"SPX500","blockSize":"volume","blockColor":"change","grouping":"sector","locale":"en","symbolUrl":"","colorTheme":"dark","exchanges":[],"hasTopBar":false,"isDataSetEnabled":false,"isZoomEnabled":true,"hasSymbolTooltip":true,"isMonoSize":false,"width":"100%","height":"100%","backgroundColor":"#0b132b"}
2033      </script>
2033
2034    </div>
2035  </div>
2036
2037  <div id="heatmap-pane-crypto" class="macro-pane" style="height:700px">
2038    <div style="text-align:right;margin-bottom:6px"><a href="https://in.tradingview.com/?aff_id=164648" target="_blank" rel="noopener" style="display:inline-block;background:#1a2340;border:1px solid #E2C275;border-radius:20px;padding:4px 12px;font-size:.68rem;color:#E2C275;text-decoration:none;letter-spacing:.3px">🎁 Get <strong>$15 USD off</strong> TradingView — Open here ↗</a></div>
2039    <div class="tradingview-widget-container" style="height:calc(100% - 32px);background:#0b132b;border:1px solid #202e56;border-radius:10px;overflow:hidden">
2040      <div class="tradingview-widget-container__widget" style="height:calc(100% - 18px)"></div>
2041      <div class="tradingview-widget-copyright" style="font-size:.6rem;color:var(--muted);opacity:.5"><a href="https://www.tradingview.com/" rel="noopener nofollow" target="_blank" style="color:var(--muted);text-decoration:none">Crypto Heatmap by TradingView</a></div>
2042      
2042<script type="text/javascript" src="https://s3.tradingview.com/external-embedding/embed-widget-crypto-coins-heatmap.js" async>
2043      {"dataSource":"Crypto","blockSize":"24h_vol_cmc","blockColor":"24h_close_change|5","locale":"en","symbolUrl":"","colorTheme":"dark","hasTopBar":true,"isDataSetEnabled":true,"isZoomEnabled":true,"hasSymbolTooltip":true,"isMonoSize":false,"width":"100%","height":"100%","backgroundColor":"#0b132b"}
2044      </script>
2044
2045    </div>
2046  </div>
2047
2048  <!-- Legacy heatmap (kept, not rendered)
2049  <div class="card">
2050    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px">
2051      <span id="heatmap-as-of" style="font-size:.65rem;color:var(--muted);font-family:var(--mono)"></span>
2052      <button class="btn btn-ghost" onclick="loadHeatmap()" style="font-size:.72rem">↻ Refresh</button>
2053    </div>
2054    <div id="heatmap-status" style="font-size:.75rem;color:var(--muted);margin-bottom:10px"></div>
2055    <div id="heatmap-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px"></div>
2056  </div>
2057  -->
2058  <div id="hm-tooltip" style="display:none"></div>
2059</div>
2060
2061<!-- TRADING PERFORMANCE (MyFXBook) -->
2062<div class="page" id="page-trading">
2063  <div class="page-header">
2064    <div class="page-title"><i data-feather="activity"></i> Trading</div>
2065    <div class="page-sub">Live MT4/MT5 performance via MyFXBook — generate &amp; share performance cards</div>
2066  </div>
2067
2068  <!-- Connect section (shown when not connected) -->
2069  <div id="mfx-connect-section" style="display:none;max-width:460px;margin:0 auto;padding:32px 0">
2070    <div style="background:var(--bg2);border:1px solid var(--border);border-radius:14px;padding:28px 28px 24px">
2071      <div style="font-size:.7rem;letter-spacing:2px;color:var(--muted);font-family:var(--mono);margin-bottom:12px">CONNECT MYFXBOOK</div>
2072      <p style="font-size:.85rem;color:var(--text2);margin-bottom:20px;line-height:1.6">Enter your MyFXBook credentials to pull live performance data from your linked MT4/MT5 accounts. Your password is never stored — only an access session token is kept.</p>
2073      <div style="display:flex;flex-direction:column;gap:12px">
2074        <input id="mfx-email" type="email" placeholder="MyFXBook email" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:10px 14px;font-size:.85rem;width:100%;box-sizing:border-box">
2075        <input id="mfx-password" type="password" placeholder="MyFXBook password" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:10px 14px;font-size:.85rem;width:100%;box-sizing:border-box">
2076        <button class="btn btn-primary" style="width:100%;justify-content:center" onclick="mfxConnect()">Connect MyFXBook →</button>
2077      </div>
2078      <p style="font-size:.7rem;color:var(--muted);margin-top:14px;text-align:center">Don't have an account? <a href="https://www.myfxbook.com" target="_blank" style="color:var(--teal)">Sign up free at myfxbook.com</a></p>
2079    </div>
2080  </div>
2081
2082  <!-- Connected section -->
2083  <div id="mfx-connected-section" style="display:none">
2084    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:10px">
2085      <div id="mfx-connected-info" style="font-size:.8rem;color:var(--muted);font-family:var(--mono)"></div>
2086      <div style="display:flex;gap:8px">
2087        <button class="btn btn-ghost" style="font-size:.75rem" onclick="mfxLoadAccounts()">↻ Refresh</button>
2088        <button class="btn btn-ghost" style="font-size:.75rem;color:var(--muted)" onclick="mfxDisconnect()">Disconnect</button>
2089      </div>
2090    </div>
2091    <div id="mfx-accounts-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px"></div>
2092  </div>
2093
2094  <!-- Loading / error state -->
2095  <div id="mfx-status" style="display:none;text-align:center;padding:40px;color:var(--muted);font-size:.85rem"></div>
2096
2097  <!-- Performance card canvas (hidden, used for export) -->
2098  <canvas id="mfx-card-canvas" style="display:none"></canvas>
2099</div>
2100
2101<!-- PORTFOLIO RISK / VaR -->
2102<div class="page" id="page-var">
2103  <div class="page-header">
2104    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/knitbwfa.json" trigger="hover" stroke="bold" state="hover-look-around" style="width:28px;height:28px"></lord-icon>Portfolio Risk</div>
2105    <div class="page-sub">Value at Risk · CVaR · Monte Carlo · Max Drawdown · Efficient Frontier</div>
2106  </div>
2107
2108  <!-- Input card -->
2109  <div class="card" style="margin-bottom:16px">
2110    <div style="display:flex;gap:24px;align-items:flex-start">
2111      <!-- Left: ticker inputs + controls -->
2112      <div style="flex:0 0 auto">
2113        <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:14px">PORTFOLIO</div>
2114        <div id="var-ticker-rows" style="display:flex;flex-direction:column;gap:10px;margin-bottom:12px"></div>
2115        <button class="btn btn-ghost" style="font-size:.75rem" onclick="addVarTicker()">+ Add Ticker</button>
2116        <div style="display:flex;flex-wrap:wrap;gap:12px;margin-top:18px;align-items:flex-end">
2117          <div style="display:flex;flex-direction:column;gap:4px">
2118            <label style="font-size:.72rem;color:var(--muted)">Portfolio Value (USD)</label>
2119            <input id="var-portfolio-value" type="number" value="10000" min="1" class="input" style="width:160px">
2120          </div>
2121          <div style="display:flex;flex-direction:column;gap:4px">
2122            <label style="font-size:.72rem;color:var(--muted)">Confidence Level</label>
2123            <select id="var-confidence" class="input" style="width:120px">
2124              <option value="0.99">99%</option>
2125              <option value="0.95">95%</option>
2126              <option value="0.90">90%</option>
2127            </select>
2128          </div>
2129          <button id="var-run-btn" class="btn btn-primary" onclick="runVarAnalysis()">Run Analysis</button>
2130          <button id="var-opt-btn" class="btn btn-ghost" onclick="runPortfolioOptimize()" style="border-color:var(--teal);color:var(--teal)">⚡ Optimize</button>
2131        </div>
2132        <div id="var-error" style="display:none;margin-top:10px;font-size:.78rem;color:var(--red)"></div>
2133      </div>
2134      <!-- Right: latest prices widget -->
2135      <div style="flex:1;min-width:180px;border-left:1px solid var(--border);padding-left:20px">
2136        <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center">
2137          LATEST PRICES
2138          <button class="btn btn-ghost" style="font-size:.65rem;padding:2px 8px" onclick="fetchVarPrices()">↻</button>
2139        </div>
2140        <div id="var-price-rows"><div style="font-size:.75rem;color:var(--muted)">—</div></div>
2141      </div>
2142    </div>
2143  </div>
2144
2145  <div id="var-loading" style="display:none">
2146    <div style="text-align:center;padding:52px 20px;border:1px solid var(--border);border-radius:12px;background:var(--bg2);margin-bottom:16px">
2147      <div style="display:flex;justify-content:center;margin-bottom:18px">
2148        <div style="width:40px;height:40px;border:3px solid rgba(0,200,83,.15);border-top-color:var(--teal);border-radius:50%;animation:spin .8s linear infinite"></div>
2149      </div>
2150      <div style="font-family:var(--mono);font-size:.68rem;letter-spacing:3px;color:var(--teal);margin-bottom:8px">RUNNING MONTE CARLO</div>
2151      <div style="font-size:.8rem;color:var(--text2)">Simulating 5,000 portfolio paths…</div>
2152      <div style="font-size:.72rem;color:var(--muted);margin-top:6px">This takes 5–15 seconds</div>
2153    </div>
2154  </div>
2155
2156  <!-- Results -->
2157  <div id="var-results" style="display:none">
2158    <!-- Metrics grid -->
2159    <div style="background:rgba(0,200,83,0.05);border:1px solid rgba(0,200,83,0.15);border-radius:8px;padding:10px 14px;margin-bottom:16px;font-size:.78rem;color:var(--text2);line-height:1.6">
2160      <strong style="color:var(--green)">VaR</strong> = the most you could lose in one day with 99% confidence. &nbsp;
2161      <strong style="color:#ff6b6b">CVaR</strong> = average loss on the worst days beyond VaR. &nbsp;
2162      <strong style="color:var(--yellow)">Max Drawdown</strong> = biggest peak-to-trough drop over 1 year.
2163    </div>
2164    <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:20px" id="var-metrics-grid"></div>
2165
2166    <!-- Charts -->
2167    <div class="card" style="margin-bottom:16px">
2168      <div style="margin-bottom:12px">
2169        <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:5px">MONTE CARLO SIMULATION PATHS</div>
2170      </div>
2171      <div id="var-chart-paths" style="width:100%;height:280px"></div>
2172    </div>
2173    <div class="card">
2174      <div style="margin-bottom:12px">
2175        <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:5px">RETURN DISTRIBUTION</div>
2176      </div>
2177      <div id="var-chart-dist" style="width:100%;height:240px"></div>
2178    </div>
2179  </div>
2180
2181  <!-- Optimization Results -->
2182  <div id="var-optimize-results" style="display:none;margin-top:20px">
2183    <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:14px">PORTFOLIO OPTIMIZATION — EFFICIENT FRONTIER</div>
2184    <!-- 2 optimal portfolio cards -->
2185    <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:20px" id="var-opt-cards"></div>
2186    <!-- Efficient frontier chart -->
2187    <div class="card" style="margin-bottom:16px">
2188      <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:10px">EFFICIENT FRONTIER</div>
2189      <div id="var-chart-frontier" style="width:100%;height:300px"></div>
2190    </div>
2191    <!-- Weights comparison table -->
2192    <div class="card">
2193      <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:12px">WEIGHT COMPARISON</div>
2194      <div id="var-opt-table"></div>
2195    </div>
2196  </div>
2197  <div id="var-opt-loading" style="display:none">
2198    <div style="text-align:center;padding:52px 20px;border:1px solid var(--border);border-radius:12px;background:var(--bg2);margin-top:20px">
2199      <div style="display:flex;justify-content:center;margin-bottom:18px">
2200        <div style="width:40px;height:40px;border:3px solid rgba(226,194,117,.15);border-top-color:#e2c275;border-radius:50%;animation:spin .8s linear infinite"></div>
2201      </div>
2202      <div style="font-family:var(--mono);font-size:.68rem;letter-spacing:3px;color:#e2c275;margin-bottom:8px">OPTIMIZING PORTFOLIO</div>
2203      <div style="font-size:.8rem;color:var(--text2)">Computing efficient frontier…</div>
2204      <div style="font-size:.72rem;color:var(--muted);margin-top:6px">Calculating Sharpe ratios &amp; min-volatility weights</div>
2205    </div>
2206  </div>
2207  <div id="var-opt-error" style="display:none;margin-top:10px;font-size:.78rem;color:var(--red)"></div>
2208</div>
2209
2210<!-- OPTIONS FLOW -->
2211<div class="page" id="page-options">
2212  <div class="page-header">
2213    <div class="page-title"><i data-feather="zap" style="width:20px;height:20px;margin-right:8px;vertical-align:middle"></i>Options Flow</div>
2214    <div class="page-sub" style="margin-left:0">Unusual options activity — high vol/OI contracts flagged across 11,000+ t
2214ickers, scanned 4× daily Mon–Fri.</div>
2215  </div>
2216
2217  <!-- Filter bar -->
2218  <div style="display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
2219    <div style="display:flex;gap:0;border:1px solid var(--border);border-radius:6px;overflow:hidden;font-family:var(--mono);font-size:.68rem">
2220      <button id="opt-f-all"  onclick="optSetType('')"     style="padding:5px 12px;background:var(--teal);color:#000;border:none;cursor:pointer;font-family:inherit;font-weight:700">ALL</button>
2221      <button id="opt-f-call" onclick="optSetType('call')" style="padding:5px 12px;background:transparent;color:var(--text2);border:none;cursor:pointer;font-family:inherit">CALLS</button>
2222      <button id="opt-f-put"  onclick="optSetType('put')"  style="padding:5px 12px;background:transparent;color:var(--text2);border:none;cursor:pointer;font-family:inherit">PUTS</button>
2223    </div>
2224    <input id="opt-f-ticker" placeholder="Ticker…" maxlength="8"
2225      style="width:90px;padding:5px 10px;background:var(--bg2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.78rem;font-family:var(--mono);text-transform:uppercase"
2226      oninput="this.value=this.value.toUpperCase()" onkeydown="if(event.key==='Enter')loadOptionsPage()">
2227    <select id="opt-f-premium" onchange="loadOptionsPage()"
2228      style="padding:5px 10px;background:var(--bg2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.78rem;cursor:pointer">
2229      <option value="0">Any premium</option>
2230      <option value="100000">$100K+</option>
2231      <option value="500000">$500K+</option>
2232      <option value="1000000">$1M+</option>
2233      <option value="5000000">$5M+</option>
2234    </select>
2235    <select id="opt-f-hours" onchange="loadOptionsPage()"
2236      style="padding:5px 10px;background:var(--bg2);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.78rem;cursor:pointer">
2237      <option value="24">Last 24h</option>
2238      <option value="48">Last 48h</option>
2239      <option value="168">Last 7 days</option>
2240    </select>
2241    <button onclick="loadOptionsPage()" style="padding:5px 12px;background:transparent;border:1px solid var(--border);border-radius:6px;color:var(--text2);cursor:pointer;font-size:.72rem">↻ Refresh</button>
2242    <div id="opt-page-count" style="font-size:.7rem;color:var(--muted);font-family:var(--mono);margin-left:4px"></div>
2243  </div>
2244
2245  <!-- Sentiment summary bar -->
2246  <div id="opt-sentiment-bar" style="display:none;margin-bottom:14px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:10px 16px;display:flex;gap:24px;flex-wrap:wrap">
2247    <div>
2248      <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:1px;margin-bottom:2px">CALL PREMIUM</div>
2249      <div id="opt-call-total" style="font-size:.95rem;font-weight:700;color:var(--green);font-family:var(--mono)">—</div>
2250    </div>
2251    <div>
2252      <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:1px;margin-bottom:2px">PUT PREMIUM</div>
2253      <div id="opt-put-total" style="font-size:.95rem;font-weight:700;color:var(--red);font-family:var(--mono)">—</div>
2254    </div>
2255    <div>
2256      <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:1px;margin-bottom:2px">SENTIMENT</div>
2257      <div id="opt-sentiment-label" style="font-size:.95rem;font-weight:700;font-family:var(--mono)">—</div>
2258    </div>
2259  </div>
2260
2261  <div id="opt-page-panel" style="overflow-x:auto">
2262    <div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div><div class="ov-skel"></div>
2263  </div>
2264</div>
2265
2266<!-- POLYMARKET -->
2267<div class="page" id="page-polymarket">
2268  <div class="page-header">
2269    <div class="page-title"><i data-feather="target"></i>Polymarket</div>
2270    <div class="page-sub">Prediction markets — open interest, live volume, order book alerts</div>
2271  </div>
2272
2273  <!-- status bar -->
2274  <div style="display:flex;gap:12px;align-items:center;margin-bottom:16px;font-size:.75rem;color:var(--text2);font-family:var(--mono)">
2275    <span id="pm-monitor-dot" style="color:#888">● checking…</span>
2276    <span id="pm-alerts-today-label"></span>
2277    <span style="flex:1"></span>
2278    <button class="btn btn-ghost" style="font-size:.7rem;padding:3px 10px" onclick="pmRefreshAll()">&#8635; Refresh</button>
2279  </div>
2280
2281  <!-- live alert toast strip -->
2282  <div id="pm-live-strip" style="display:none;padding:8px 14px;background:rgba(0,200,83,.07);border:1px solid rgba(0,200,83,.25);border-radius:8px;margin-bottom:14px;font-size:.77rem;color:#00c853;font-family:var(--mono)">
2283    <span id="pm-live-text"></span>
2284  </div>
2285
2286  <!-- tabs -->
2287  <div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:18px">
2288    <button id="pm-tab-markets"  class="btn btn-ghost pm-tab pm-tab-active" style="border-radius:6px 6px 0 0;border-bottom:none;font-size:.74rem" onclick="pmTab('markets')">Markets</button>
2289    <button id="pm-tab-volume"   class="btn btn-ghost pm-tab" style="border-radius:6px 6px 0 0;border-bottom:none;font-size:.74rem" onclick="pmTab('volume')">Volume Tracker</button>
2290    <button id="pm-tab-book"     class="btn btn-ghost pm-tab" style="border-radius:6px 6px 0 0;border-bottom:none;font-size:.74rem" onclick="pmTab('book')">Order Book</button>
2291    <button id="pm-tab-alerts"   class="btn btn-ghost pm-tab" style="border-radius:6px 6px 0 0;border-bottom:none;font-size:.74rem" onclick="pmTab('alerts')">My Alerts</button>
2292  </div>
2293
2294  <!-- tab: Markets -->
2295  <div id="pm-panel-markets">
2296    <p style="font-size:.73rem;color:var(--text2);margin-bottom:12px">Top 50 active markets by 24h volume. Click <b>Watch</b> to track a market and receive focused alerts.</p>
2297    <div id="pm-markets-list"><p style="font-size:.8rem;color:var(--text2)">Loading…</p></div>
2298  </div>
2299
2300  <!-- tab: Volume Tracker -->
2301  <div id="pm-panel-volume" style="display:none">
2302    <p style="font-size:.73rem;color:var(--text2);margin-bottom:12px">Live 24h volume — top 10 markets. Auto-refreshes every 60 s.</p>
2303    <canvas id="pm-vol-chart" height="260" style="max-width:100%"></canvas>
2304    <div id="pm-vol-empty" style="display:none;font-size:.8rem;color:var(--text2)">No market data yet.</div>
2305  </div>
2306
2307  <!-- tab: Order Book -->
2308  <div id="pm-panel-book" style="display:none">
2309    <div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;flex-wrap:wrap">
2310      <select id="pm-book-select" style="flex:1;
2310min-width:200px;font-size:.77rem;background:var(--bg2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px" onchange="loadPmBook()">
2311        <option value="">— select a market —</option>
2312      </select>
2313      <button class="btn btn-ghost" style="font-size:.72rem;padding:4px 12px" onclick="loadPmBook()">&#8635; Fetch</button>
2314    </div>
2315    <div id="pm-book-result"></div>
2316  </div>
2317
2318  <!-- tab: Alerts -->
2319  <div id="pm-panel-alerts" style="display:none">
2320    <div style="display:flex;gap:10px;align-items:center;margin-bottom:14px">
2321      <div style="font-size:.73rem;color:var(--text2);flex:1">Recent global alerts fired by the monitor. SSE stream is active while this page is open.</div>
2322      <button class="btn btn-ghost" style="font-size:.72rem;padding:4px 12px" onclick="loadPmAlerts()">&#8635; Refresh</button>
2323    </div>
2324    <div id="pm-alerts-list"><p style="font-size:.8rem;color:var(--text2)">Loading…</p></div>
2325
2326    <div style="margin-top:20px;border-top:1px solid var(--border);padding-top:14px">
2327      <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:10px">WATCHED MARKETS</div>
2328      <div id="pm-watches-list"><p style="font-size:.8rem;color:var(--text2)">Loading…</p></div>
2329    </div>
2330  </div>
2331</div>
2332
2333<!-- TRENDS -->
2334<div class="page" id="page-trends">
2335  <div class="page-header">
2336    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/iuvnsegf.json" trigger="hover" stroke="bold" state="hover-rotation" style="width:28px;height:28px"></lord-icon>Google Trends</div>
2337    <div class="page-sub" style="margin-left:0">Top trending searches — updated every 30 minutes</div>
2338  </div>
2339  <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap">
2340    <select id="trends-geo" onchange="_trendsLoaded=false;loadTrends()" style="font-family:var(--mono);font-size:.78rem;background:var(--bg2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:4px 10px">
2341      <option value="US">🇺🇸 United States</option>
2342      <option value="GB">🇬🇧 United Kingdom</option>
2343      <option value="CA">🇨🇦 Canada</option>
2344      <option value="AU">🇦🇺 Australia</option>
2345      <option value="IN">🇮🇳 India</option>
2346    </select>
2347    <div id="trends-meta" style="font-size:.72rem;color:var(--text2);font-family:var(--mono)"></div>
2348    <button class="btn btn-ghost" style="font-size:.7rem;padding:3px 10px;margin-left:auto" onclick="loadTrends(true)">&#8635; Refresh</button>
2349  </div>
2350  <div id="trends-list"><p style="font-size:.8rem;color:var(--text2)">Loading...</p></div>
2351  <div style="margin-top:20px;font-size:.68rem;color:var(--muted);font-family:var(--mono)">
2352    Powered by <a href="https://trends.google.com" target="_blank" rel="noopener" style="color:var(--teal)">Google Trends</a>
2353  </div>
2354</div>
2355
2356<!-- MACRO DASHBOARD -->
2357<div class="page" id="page-macro">
2358  <div class="page-header">
2359    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/tyntlpjn.json" trigger="hover" stroke="bold" style="width:28px;height:28px"></lord-icon>Macro Dashboard</div>
2360    <div class="page-sub">FRED Economic Indicators — real-time macro regime analysis</div>
2361  </div>
2362  <div class="macro-tabs">
2363    <button class="macro-tab active" onclick="macroTab('scorecard')">Scorecard</button>
2364    <button class="macro-tab" onclick="macroTab('charts')">Charts</button>
2365    <button class="macro-tab" onclick="macroTab('compare')">Compare</button>
2366    <button class="macro-tab" onclick="macroTab('aibrief')">AI Brief</button>
2367    <button class="macro-tab" onclick="macroTab('releases')">Calendar</button>
2368    <button class="macro-tab" onclick="macroTab('alerts')">Alerts</button>
2369  </div>
2370
2371  <!-- Scorecard -->
2372  <div id="macro-pane-scorecard" class="macro-pane active">
2373    <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px">
2374      <span style="font-size:.65rem;color:var(--muted);font-family:var(--mono)">10 KEY INDICATORS · FRED DATA · 15 MIN CACHE</span>
2375      <button class="btn btn-ghost" style="font-size:.7rem;padding:3px 10px;margin-left:auto" onclick="loadMacroScorecard(true)">&#8635; Refresh</button>
2376    </div>
2377    <div class="macro-scorecard-grid" id="macro-scorecard-grid">
2378      <p style="font-size:.8rem;color:var(--text2);grid-column:1/-1">Loading scorecard...</p>
2379    </div>
2380  </div>
2381  <div id="mc-tooltip" style="display:none"></div>
2382
2383  <!-- Charts -->
2384  <div id="macro-pane-charts" class="macro-pane">
2385    <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-bottom:14px">
2386      <div class="form-group" style="flex:1;min-width:180px">
2387        <label class="form-label">SERIES</label>
2388        <select id="macro-chart-series" style="width:100%;font-family:var(--mono);font-size:.78rem;background:var(--bg2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px"></select>
2389      </div>
2390      <div class="form-group">
2391        <label class="form-label">TRANSFORM</label>
2392        <select id="macro-chart-transform" style="font-family:var(--mono);font-size:.78rem;background:var(--bg2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px">
2393          <option value="level">Level</option>
2394          <option value="yoy">YoY %</option>
2395          <option value="mom">MoM %</option>
2396        </select>
2397      </div>
2398      <div class="form-group">
2399        <label class="form-label">PERIODS</label>
2400        <select id="macro-chart-limit" style="font-family:var(--mono);font-size:.78rem;background:var(--bg2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px">
2401          <option value="60">5 Years</option>
2402          <option value="120" selected>10 Years</option>
2403          <option value="300">25 Years</option>
2404          <option value="500">All</option>
2405        </select>
2406      </div>
2407      <div class="form-group" style="display:flex;align-items:center;gap:6px;padding-bottom:2px">
2408        <input type="checkbox" id="macro-chart-recession" checked style="accent-color:var(--teal)">
2409        <label style="font-size:.72rem;color:var(--text2);cursor:pointer" for="macro-chart-recession">Recession shading</label>
2410      </div>
2411      <button class="btn btn-primary" style="padding:5px 16px;font-size:.78rem" onclick="loadMacroChart()">Draw</button>
2412    </div>
2413    <div id="macro-chart-wrap" style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;min-height:200px;display:flex;align-items:center;justify-content:center;overflow:hidden">
2414      <p style="font-size:.8rem;color:var(--text2)">Select a series and click Draw</p>
2415    </div>
2416    <div style="margin-top:10px">
2417      <button id="macro-chart-export" class="btn btn-ghost" style="font-size:.72rem;padding:4px 12px;display:none" onclick="downloadMacroChart()">&#8659; Download Chart PNG</button>
2418    </div>
2419    <!-- Raw observations table -->
2420    <div id="macro-chart-table-wrap" style="display:none;margin-top:16px">
2421      <div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
2422        <span style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px" id="macro-table-meta"></span>
2423        <button class="btn btn-ghost" style="font-size:.7rem;padding:3px 10px;margin-left:auto" onclick="downloadFredCSV()">&#8595; Download CSV</button>
2424      </div>
2425      <div style="max-height:280px;overflow-y:auto;border:1px solid var(--border);border-radius:8px">
2426        <table style="width:100%;border-collapse:collapse;font-family:var(--mono);font-size:.75rem">
2427          <thead>
2428            <tr style="background:var(--bg2);position:sticky;top:0">
2429              <th style="text-align:left;padding:7px 14px;color:var(--text2);font-weight:600;border-bottom:1px solid var(--border)">DATE</th>
2430              <th style="text-align:right;padding:7px 14px;color:var(--text2);font-weight:600;border-bottom:1px solid var(--border)" id="macro-table-unit-hdr">VALUE</th>
2431            </tr>
2432          </thead>
2433          <tbody id="macro-chart-table-body"></tbody>
2434        </table>
2435      </div>
2436    </div>
2437  </div>
2438
2439  <!-- Compare -->
2440  <div id="macro-pane-compare" class="macro-pane">
2441    <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-bottom:14px">
2442      <div class="form-group" style="flex:1;min-width:150px">
2443        <label class="form-label">SERIES 1</label>
2444        <select id="macro-cmp-s1" style="width:100%;font-family:var(--mono);font-size:.78rem;background:var(--bg2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px"></select>
2445      </div>
2446      <div class="form-group" style="flex:1;min-width:150px">
2447        <label class="form-label">SERIES 2</label>
2448        <select id="macro-cmp-s2" style="width:100%;font-family:var(--mono);font-size:.78rem;background:var(--bg2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px"><option value="">— optional —</option></select>
2449      </div>
2450      <div class="form-group" style="flex:1;min-width:150px">
2451        <label class="form-label">SERIES 3</label>
2452        <select id="macro-cmp-s3" style="width:100%;font-family:var(--mono);font-size:.78rem;background:var(--bg2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px"><option value="">— optional —</option></select>
2453      </div>
2454      <div class="form-group">
2455        <label class="form-label">TRANSFORM</label>
2456        <select id="macro-cmp-transform" style="font-family:var(--mono);font-size:.78rem;background:var(--bg2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px">
2457          <option value="level">Level</option>
2458          <option value="yoy">YoY %</option>
2459          <option value="mom">MoM %</option>
2460        </select>
2461      </div>
2462      <button class="btn btn-primary" style="padding:5px 16px;font-size:.78rem" onclick="loadMacroCompare()">Compare</button>
2463    </div>
2464    <div id="macro-compare-wrap" style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;min-height:200px;display:flex;align-items:center;justify-content:center;overflow:hidden">
2465      <p style="font-size:.8rem;color:var(--text2)">Select series and click Compare</p>
2466    </div>
2467    <div style="margin-top:10px">
2468      <button id="macro-compare-dl" class="btn btn-ghost" style="font-size:.72rem;padding:4px 12px;display:none" onclick="downloadMacroCompare()">&#8659; Download Compare PNG</button>
2469    </div>
2470  </div>
2471
2472  <!-- AI Brief -->
2473  <div id="macro-pane-aibrief" class="macro-pane">
2474    <div style="margin-bottom:14px">
2475      <button class="btn btn-primary" onclick="loadMacroAIBrief()" id="macro-brief-btn">Generate Macro Brief</button>
2476      <span style="font-size:.7rem;color:var(--muted);font-family:var(--mono);margin-left:12px">Uses Claude Haiku · ~$0.001</span>
2477    </div>
2478    <div id="macro-brief-out" style="display:none;background:var(--bg2);border:1px solid var(--teal);border-radius:10px;padding:20px 22px;font-size:.9rem;line-height:1.7;color:var(--text)"></div>
2479    <div id="macro-brief-data" style="display:none;margin-top:14px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:14px 16px;font-size:.72rem;color:var(--text2);font-family:var(--mono);line-height:1.8"></div>
2480  </div>
2481
2482  <!-- Release Calendar -->
2483  <div id="macro-pane-releases" class="macro-pane">
2484    <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px">
2485      <span style="font-size:.65rem;color:var(--muted);font-family:var(--mono)">UPCOMING FRED DATA RELEASES · NEXT 30 DAYS</span>
2486      <button class="btn btn-ghost" style="font-size:.7rem;padding:3px 10px;margin-left:auto" onclick="loadMacroReleases()">&#8635; Refresh</button>
2487    </div>
2488    <div id="macro-releases-list"><p style="font-size:.8rem;color:var(--text2)">Loading...</p></div>
2489  </div>
2490
2491  <!-- Alerts -->
2492  <div id="macro-pane-alerts" class="macro-pane">
2493    <div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:16px 18px;margin-bottom:18px">
2494      <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:12px">NEW THRESHOLD ALERT</div>
2495      <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
2496        <div class="form-group" style="flex:1;min-width:160px">
2497          <label class="form-label">SERIES</label>
2498          <select id="fred-alert-series" style="width:100%;font-family:var(--mono);font-size:.78rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px"></select>
2499        </div>
2500        <div class="form-group">
2501          <label class="form-label">TRANSFORM</label>
2502          <select id="fred-alert-transform" style="font-family:var(--mono);font-size:.78rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px">
2503            <option value="level">Level</option>
2504            <option value="yoy">YoY %</option>
2505            <option value="mom">MoM %</option>
2506          </select>
2507        </div>
2508        <div class="form-group">
2509          <label class="form-label">CONDITION</label>
2510          <select id="fred-alert-condition" style="font-family:var(--mono);font-size:.78rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px">
2511            <option value="gt">Above ▲</option>
2512            <option value="lt">Below ▼</option>
2513          </select>
2514        </div>
2515        <div class="form-group">
2516          <label class="form-label">THRESHOLD</label>
2517          <input type="number" id="fred-alert-threshold" step="0.01" placeholder="e.g. 3.5" style="width:90px;font-family:var(--mono);font-size:.78rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 10px">
2518        </div>
2519        <button class="btn btn-primary" style="padding:5px 16px;font-size:.78rem" onclick="createFredAlert()">+ Save Alert</button>
2520      </div>
2521    </div>
2522    <div id="fred-alerts-triggered" style="display:none;background:rgba(220,50,50,.08);border:1px solid #e05050;border-radius:8px;padding:12px 16px;margin-bottom:14px;font-size:.8rem;color:#e05050;font-family:var(--mono)"></div>
2523    <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:10px">ACTIVE ALERTS</div>
2524    <div id="fred-alerts-list"><p style="font-size:.8rem;color:var(--text2)">Loading...</p></div>
2525  </div>
2526</div>
2527
2528<div class="page" id="page-admin">
2529  <div class="page-header">
2530    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/wyupmbaf.json" trigger="hover" style="width:28px;height:28px"></lord-icon>Admin Panel</div>
2531    <div class="page-sub">Manage enterprise clients, API keys, usage tracking</div>
2532  </div>
2533  <!-- Platform stats -->
2534  <div class="g4" style="margin-bottom:20px">
2535    <div class="stat-card"><div class="lbl">TOTAL CLIENTS</div><div class="val" id="adm-total">—</div></div>
2536    <div class="stat-card"><div class="lbl">ACTIVE</div><div class="val" id="adm-active">—</div></div>
2537    <div class="stat-card"><div class="lbl">SCANS THIS MONTH</div><div class="val" id="adm-scans">—</div></div>
2538    <div class="stat-card"><div class="lbl">POSTS THIS MONTH</div><div class="val" id="adm-posts">—</div></div>
2539  </div>
2540
2541  <!-- Credits & Usage -->
2542  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
2543    <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px">CREDITS & USAGE — <span id="credits-month" style="color:var(--yellow)">...</span></div>
2544    <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px" onclick="loadAdminCredits()">↻ Refresh</button>
2545  </div>
2546  <div class="g3" style="margin-bottom:20px" id="credits-cards">
2547    <!-- Injected by loadAdminCredits() -->
2548    <div class="stat-card"><div class="lbl">🤖 CLAUDE CALLS</div><div class="val" id="crd-claude-calls">—</div></div>
2549    <div class="stat-card"><div class="lbl">🤖 EST. COST (USD)</div><div class="val" id="crd-claude-cost">—</div></div>
2550    <div class="stat-card"><div class="lbl">🎨 CAROUSEL RENDERS</div><div class="val" id="crd-tmpl">—</div></div>
2551    <div class="stat-card"><div class="lbl">☁️ CLD STORAGE</div><div class="val" id="crd-cld-storage">—</div></div>
2552    <div class="stat-card"><div class="lbl">☁️ CLD RESOURCES</div><div class="val" id="crd-cld-res">—</div></div>
2553    <div class="stat-card"><div class="lbl">🧩 SONAR CALLS</div><div class="val" id="crd-sonar-calls">—</div></div>
2554    <div class="stat-card"><div class="lbl">🧩 SONAR COST</div><div class="val" id="crd-sonar-cost">—</div></div>
2555    <div class="stat-card"><div class="lbl">🖼 DALL-E IMAGES</div><div class="val" id="crd-openai-calls">—</div></div>
2556    <div class="stat-card"><div class="lbl">🖼 DALL-E COST</div><div class="val" id="crd-openai-cost">—</div></div>
2557  </div>
2558  <div class="card" style="margin-bottom:20px;
2558display:none" id="credits-detail">
2559    <div style="display:flex;gap:24px;flex-wrap:wrap">
2560      <div>
2561        <div style="font-size:.6rem;color:var(--muted);letter-spacing:1px;margin-bottom:6px">🤖 CLAUDE AI</div>
2562        <div id="crd-claude-detail" style="font-size:.75rem;color:var(--text2);line-height:1.9"></div>
2563      </div>
2564      <div>
2565        <div style="font-size:.6rem;color:var(--muted);letter-spacing:1px;margin-bottom:6px">𝕏 POSTS BY CLIENT</div>
2566        <div id="crd-x-detail" style="font-size:.75rem;color:var(--text2);line-height:1.9"></div>
2567      </div>
2568      <div>
2569        <div style="font-size:.6rem;color:var(--muted);letter-spacing:1px;margin-bottom:6px">🧩 SONAR (EXTENSION)</div>
2570        <div id="crd-sonar-detail" style="font-size:.75rem;color:var(--text2);line-height:1.9"></div>
2571        <!-- Sonar weekly limit editor -->
2572        <div style="margin-top:12px;padding:10px;background:rgba(226,194,117,.05);border:1px solid rgba(226,194,117,.15);border-radius:7px">
2573          <div style="font-size:.6rem;color:var(--yellow);letter-spacing:1px;margin-bottom:8px;font-weight:700">WEEKLY CALL LIMITS</div>
2574          <div style="display:grid;grid-template-columns:auto 1fr;align-items:center;gap:5px 10px;font-size:.72rem;color:var(--text2)">
2575            <label>Beginner</label>
2576            <input id="sonar-limit-beginner" type="number" min="1" style="background:#131f40;border:1px solid #202e56;border-radius:4px;padding:3px 7px;color:#e0e8f0;font-size:.72rem;width:70px" />
2577            <label>Intermediate</label>
2578            <input id="sonar-limit-intermediate" type="number" min="1" style="background:#131f40;border:1px solid #202e56;border-radius:4px;padding:3px 7px;color:#e0e8f0;font-size:.72rem;width:70px" />
2579            <label>Advanced</label>
2580            <input id="sonar-limit-advanced" type="number" min="1" style="background:#131f40;border:1px solid #202e56;border-radius:4px;padding:3px 7px;color:#e0e8f0;font-size:.72rem;width:70px" />
2581            <label>Expert</label>
2582            <input id="sonar-limit-expert" type="number" min="1" style="background:#131f40;border:1px solid #202e56;border-radius:4px;padding:3px 7px;color:#e0e8f0;font-size:.72rem;width:70px" />
2583          </div>
2584          <div style="display:flex;align-items:center;gap:8px;margin-top:8px">
2585            <button onclick="saveSonarLimits()" class="btn btn-yellow" style="font-size:.65rem;padding:4px 12px">Save</button>
2586            <span id="sonar-limits-msg" style="font-size:.65rem;color:var(--muted)"></span>
2587          </div>
2588        </div>
2589      </div>
2590      <div>
2591        <div style="font-size:.6rem;color:var(--muted);letter-spacing:1px;margin-bottom:6px">🖼 OPENAI / DALL-E</div>
2592        <div id="crd-openai-detail" style="font-size:.75rem;color:var(--text2);line-height:1.9"></div>
2593      </div>
2594    </div>
2595  </div>
2596  <hr>
2597  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;cursor:pointer" onclick="toggleAdminSection('as-clients')">
2598    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">ENTERPRISE CLIENTS <span id="as-clients-arrow" style="margin-left:4px">▾</span></div>
2599    <button class="btn btn-yellow" onclick="event.stopPropagation();openNewClient()">+ New Client</button>
2600  </div>
2601  <div id="as-clients">
2602  <div id="client-list"></div>
2603  </div>
2604  <hr>
2605  <!-- Dashboard Users -->
2606  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;cursor:pointer" onclick="toggleAdminSection('as-users')">
2607    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">DASHBOARD USERS <span id="as-users-arrow" style="margin-left:4px">▾</span></div>
2608    <button class="btn btn-primary" style="font-size:.7rem;padding:5px 12px" onclick="event.stopPropagation();openNewUser()">+ New User</button>
2609  </div>
2610  <div id="as-users">
2611  <div id="user-list" style="margin-bottom:16px"></div>
2612  <!-- Create/Edit User modal-style inline form -->
2613  <div id="user-form-panel" style="display:none;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:18px;margin-bottom:16px">
2614    <h4 id="user-form-title" style="margin-bottom:14px">New Dashboard User</h4>
2615    <div class="g2">
2616      <div class="form-group"><label class="form-label">EMAIL</label><input type="email" id="uf-email" placeholder="[email protected]"></div>
2617      <div class="form-group"><label class="form-label">NAME (optional)</label><input type="text" id="uf-name" placeholder="Display name"></div>
2618      <div class="form-group"><label class="form-label">TIER</label>
2619        <select id="uf-tier">
2620          <option value="creator">
2620creator</option>
2621          <option value="pro" selected>pro</option>
2622          <option value="stellar">stellar</option>
2623          <option value="enterprise_api">enterprise_api</option>
2624          <option value="admin">admin</option>
2625          <option value="super_admin">super_admin</option>
2626        </select>
2627      </div>
2628      <div class="form-group"><label class="form-label">PASSWORD</label><input type="password" id="uf-password" placeholder="Set login password"></div>
2629    </div>
2630    <div style="display:flex;gap:10px;margin-top:10px">
2631      <button class="btn btn-primary" onclick="saveUser()" id="uf-save-btn">💾 Create User</button>
2632      <button class="btn btn-ghost" onclick="$('user-form-panel').style.display='none'">Cancel</button>
2633    </div>
2634    <div id="uf-result" style="margin-top:10px"></div>
2635  </div>
2636  </div><!-- /as-users -->
2637  <hr>
2638  <!-- Activity feed -->
2639  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;cursor:pointer" onclick="toggleAdminSection('as-activity')">
2640    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">RECENT ACTIVITY <span id="as-activity-arrow" style="margin-left:4px">▸</span></div>
2641    <button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();loadAdminActivity()">↻ Refresh</button>
2642  </div>
2643  <div id="as-activity" style="display:none">
2644  <div id="admin-activity-list"></div>
2645  </div>
2646  <hr>
2647  <!-- News management -->
2648  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;cursor:pointer" onclick="toggleAdminSection('as-news')">
2649    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">NEWS FEED — PUSH HISTORY <span id="as-news-arrow" style="margin-left:4px">▾</span></div>
2650    <div style="display:flex;gap:8px">
2651      <button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();loadAdminNews()">↻ Refresh</button>
2652      <button class="btn btn-primary" style="font-size:.7rem;padding:5px 12px" onclick="event.stopPropagation();adminRunJob('fetch_insider',this,'Insider transactions fetched')">⚡ Fetch Insider Transactions</button>
2653    </div>
2654  </div>
2655  <div id="as-news">
2656  <!-- AI Analysis Generator -->
2657  <div class="card" style="margin-bottom:14px;border:1px solid rgba(99,102,241,.35)">
2658    <h4>🤖 AI ANALYSIS GENERATOR</h4>
2659    <p style="margin-bottom:12px;font-size:.73rem;color:var(--muted)">Describe the analysis you want. Claude Sonnet will draft a structured report. Review and edit before pushing to clients.</p>
2660    <div class="form-group" style="margin-top:10px"><label class="form-label">TYPE</label><select id="ai-content-type"><option value="company_analysis">🏢 Company Analysis</option><option value="macro">🌍 Macro</option><option value="earnings">💰 Earnings</option><option value="news">📰 News</option></select></div>
2661    <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
2662      <div class="form-group"><label class="form-label">COMPANY / TICKER</label><input type="text" id="ai-meta-company" placeholder="e.g. NVDA / Nvidia" style="width:100%"></div>
2663      <div class="form-group"><label class="form-label">KEY DATA POINTS</label><input type="text" id="ai-meta-data" placeholder="e.g. Q4 EPS $0.89, revenue +18%" style="width:100%"></div>
2664    </div>
2665    <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
2666      <div class="form-group"><label class="form-label">TONE</label><select id="ai-meta-tone" style="width:100%"><option value="">Neutral</option><option value="bullish">Bullish</option><option value="bearish">Bearish</option><option value="cautious">Cautious</option></select></div>
2667      <div class="form-group"><label class="form-label">LANGUAGE</label><select id="ai-meta-lang" style="width:100%"><option value="en">English</option><option value="es">Spanish</option></select></div>
2668    </div>
2669    <button class="btn btn-ghost" onclick="adminBuildPrompt()" style="font-size:.72rem;margin-bottom:10px">⚡ Build Prompt from Fields</button>
2670    <div class="form-group"><label class="form-label">YOUR REQUEST</label><textarea id="ai-prompt" rows="3" placeholder="e.g. Write a company deep-dive on NVDA covering valuation, AI strategy and risks&#10;e.g. Macro analysis of US CPI for Q1 2026 and Fed implications"></textarea></div>
2671    <button class="btn btn-primary" onclick="adminGenerateAnalysis()" id="ai-gen-btn">🤖 Generate Analysis</button>
2672    <div id="ai-gen-result" style="margin-top:8px"></div>
2673    <!-- Preview section — hidden until generation completes -->
2674    <div id="ai-preview" style="display:none;margin-top:18px;border-top:1px solid rgba(255,255,255,.08);padding-top:14px">
2675      <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:10px">PREVIEW — EDIT BEFORE PUSHING</div>
2676      <div class="form-group"><label class="form-label">TITLE</label><input type="text" id="ai-title" style="width:100%"></div>
2677      <div class="form-group"><label class="form-label">SUMMARY</label><textarea id="ai-summary" rows="10" style="width:100%;font-size:.78rem;line-height:1.55"></textarea></div>
2678      <button class="btn btn-green" onclick="adminPushAnalysis()">📰 Push to Feed</button>
2679      <div id="ai-push-result" style="margin-top:8px"></div>
2680    </div>
2681  </div>
2682  </div><!-- /as-news -->
2683
2684  <!-- Daily Briefs -->
2685  <hr>
2686  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:18px;
2686cursor:pointer" onclick="toggleAdminSection('as-briefs')">
2687    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">DAILY INTELLIGENCE BRIEFS <span id="as-briefs-arrow" style="margin-left:4px">▾</span></div>
2688    <div style="display:flex;gap:8px">
2689      <button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();loadAdminBriefs()">↻ Refresh</button>
2690      <button class="btn btn-primary" style="font-size:.7rem;padding:5px 12px" onclick="event.stopPropagation();adminGenerateBrief()">⚡ Generate Today's Brief</button>
2691    </div>
2692  </div>
2693  <div id="as-briefs">
2694    <div class="card" style="margin-bottom:14px;border:1px solid rgba(46,204,142,.25)">
2695      <p style="font-size:.73rem;color:var(--muted);margin-bottom:12px">Claude generates the full HTML brief using live market context (news, movers, earnings, scanner signals) from your DB. Runs automatically Mon–Fri at 6:30am EST. Click Generate to trigger it manually now.</p>
2696      <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
2697        <label style="font-size:.78rem;display:flex;align-items:center;gap:7px;cursor:pointer"><input type="checkbox" id="brief-auto-enabled" onchange="saveDailyBriefConfig()"> Automatic daily generation enabled</label>
2698        <span id="brief-auto-result" style="font-size:.7rem;color:var(--muted)"></span>
2699      </div>
2700      <p style="font-size:.68rem;color:var(--muted);margin:-6px 0 12px">Turning this off stops the 6:30am scheduled Claude API call. "Generate Today's Brief" above still works manually regardless.</p>
2701      <div style="display:flex;gap:8px;margin-bottom:12px">
2702        <button id="btn-preview-daily"   class="btn btn-ghost" style="font-size:.7rem;padding:5px 12px" onclick="previewEmailInTab('daily')">👁 Preview Daily</button>
2703        <button id="btn-preview-weekly"  class="btn btn-ghost" style="font-size:.7rem;padding:5px 12px" onclick="previewEmailInTab('weekly')">👁 Preview Weekly</button>
2704        <button id="btn-preview-monthly" class="btn btn-ghost" style="font-size:.7rem;padding:5px 12px" onclick="previewEmailInTab('monthly')">👁 Preview Monthly</button>
2705      </div>
2706      <div id="brief-gen-result" style="margin-bottom:10px"></div>
2707      <div id="brief-list" style="margin-top:8px"></div>
2708    </div>
2709    <!-- Brief preview modal -->
2710    <div id="brief-preview-wrap" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:9999;overflow:auto;padding:24px">
2711      <div style="max-width:740px;margin:0 auto;position:relative">
2712        <button onclick="document.getElementById('brief-preview-wrap').style.display='none'" style="position:sticky;top:0;float:right;background:#e05252;color:#fff;border:none;border-radius:6px;padding:6px 14px;cursor:pointer;font-size:.8rem;z-index:10;margin-bottom:8px">✕ Close</button>
2713        <div id="brief-preview-content"></div>
2714      </div>
2715    </div>
2716  </div><!-- /as-briefs -->
2717
2718  <!-- ── NEWSROOM (world-sourced content, review queue) ── -->
2719  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:22px;cursor:pointer" onclick="toggleAdminSection('as-newsroom')">
2720    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">NEWSROOM <span id="as-newsroom-arrow" style="margin-left:4px">▸</span></div>
2721    <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px" onclick="event.stopPropagation();loadNewsroom()">↻ Refresh</button>
2722  </div>
2723  <div id="as-newsroom" style="display:none">
2724    <div class="card" style="margin-bottom:14px;border:1px solid rgba(0,188,212,.25)">
2725      <p style="font-size:.73rem;color:var(--muted);margin-bottom:12px">
2726        Story ideas sourced from live web research plus Factoz's own data, drafted per format.
2727        <strong>Nothing publishes without your approval.</strong> Spanish goes to Instagram/TikTok, English to X.
2728      </p>
2729      <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px">
2730        <!-- "Find Stories" read as "find historias" next to a beat button
2731             literally called Historias, while it actually runs all three
2732             beats at five times the Sonar cost. The labels now say which
2733             beat and what it spends. -->
2734        <button class="btn btn-ghost" style="font-size:.72rem;padding:5px 11px" onclick="newsroomDiscover()" title="Busca en los tres pilares. 5 llamadas a Sonar.">🔍 Buscar en todo (5)</button>
2735        <button class="btn btn-ghost" style="font-size:.72rem;padding:5px 11px" onclick="nrMoneyFlow()" title="Sankey de a dónde va el dinero de una empresa. No usa Sonar ni Claude — no cuesta créditos.">💰 Cómo gana dinero</button>
2736        <button class="btn btn-ghost" style="font-size:.72rem;padding:5px 11px" onclick="nrGrowth()" title="Cuánto valdrían hoy 1.000 dólares invert
2736idos hace N años. Reel. No cuesta créditos.">📈 ¿Cuánto tendrías hoy?</button>
2737        <button class="btn btn-ghost" style="font-size:.72rem;padding:5px 11px" onclick="nrLesson()" title="Lección de price action con un caso real, gráfica animada y voz. No cuesta créditos.">🎓 Lección de trading</button>
2738        <button class="btn btn-ghost" style="font-size:.72rem;padding:5px 11px" onclick="newsroomDiscover('markets')" title="Solo mercados y smart money. 3 llamadas a Sonar.">Markets (3)</button>
2739        <button class="btn btn-ghost" style="font-size:.72rem;padding:5px 11px" onclick="newsroomDiscover('macro')" title="Solo macro y economia. 1 llamada a Sonar.">Macro (1)</button>
2740        <button class="btn btn-ghost" style="font-size:.72rem;padding:5px 11px" onclick="newsroomDiscover('stories')" title="Solo historias de empresas. 1 llamada a Sonar.">Historias (1)</button>
2741        <button class="btn btn-ghost" style="font-size:.72rem;padding:5px 11px" onclick="nrToggleAll()">👁 Ver todos los drafts</button>
2742      </div>
2743      <div id="nr-status" style="font-size:.72rem;color:var(--muted);margin-bottom:10px"></div>
2744
2745      <div style="font-size:.72rem;color:var(--muted);font-family:var(--mono);letter-spacing:1px;margin:16px 0 8px">IDEA QUEUE</div>
2746      <div id="nr-ideas"></div>
2747      <div id="nr-ideas-empty" style="display:none;padding:18px 0;text-align:center;font-size:.75rem;color:var(--muted)">No pending ideas. Hit “Find Stories”.</div>
2748
2749      <div style="font-size:.72rem;color:var(--muted);font-family:var(--mono);letter-spacing:1px;margin:22px 0 8px">DRAFTS AWAITING REVIEW</div>
2750      <div id="nr-drafts"></div>
2751      <div id="nr-drafts-empty" style="display:none;padding:18px 0;text-align:center;font-size:.75rem;color:var(--muted)">Sin drafts en la cola de trabajo. Usa "Ver todos los drafts" para incluir publicados y rechazados.</div>
2752    </div>
2753  </div><!-- /as-newsroom -->
2754
2755  <!-- ── SOCIAL CONTENT PILLARS (Buffer) ── -->
2756  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:22px;cursor:pointer" onclick="toggleAdminSection('as-social-pillars')">
2757    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">SOCIAL CONTENT PILLARS <span id="as-social-pillars-arrow" style="margin-left:4px">▸</span></div>
2758    <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px" onclick="event.stopPropagation();loadSocialPillars()">↻ Refresh</button>
2759  </div>
2760  <div id="as-social-pillars" style="display:none">
2761    <div class="card" style="margin-bottom:14px;border:1px solid rgba(0,188,212,.25)">
2762      <p style="font-size:.73rem;color:var(--muted);margin-bottom:12px">5 automated brand-content pillars post straight into Buffer's queue (X, TikTok, Instagram) — no review step. Toggle any pillar off independently if it misbehaves; changes take effect on the pillar's next scheduled run.</p>
2763      <div style="display:grid;grid-template-columns:1fr auto;gap:8px 10px;align-items:center;margin-bottom:14px">
2764        <label style="font-size:.78rem;display:flex;align-items:center;gap:7px;cursor:pointer"><input type="checkbox" id="sp-tradingview"> TradingView Ideas</label>
2765        <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px" onclick="triggerSocialPillar('tradingview')">Test Now</button>
2766        <label style="font-size:.78rem;display:flex;align-items:center;gap:7px;cursor:pointer"><input type="checkbox" id="sp-eps"> EPS Beats/Misses (9:30pm ET)</label>
2767        <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px" onclick="triggerSocialPillar('eps')">Test Now</button>
2768        <label style="font-size:.78rem;display:flex;align-items:center;gap:7px;cursor:pointer"><input type="checkbox" id="sp-movers"> Top Movers (8:15am ET)</label>
2769        <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px" onclick="triggerSocialPillar('movers')">Test Now</button>
2770        <label style="font-size:.78rem;display:flex;align-items:center;gap:7px;cursor:pointer"><input type="checkbox" id="sp-options"> Unusual Options (4pm ET)</label>
2771        <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px" onclick="triggerSocialPillar('options')">Test Now</button>
2772        <label style="font-size:.78rem;display:flex;align-items:center;gap:7px;cursor:pointer"><input type="checkbox" id="sp-headline"> Top Headline (11am ET)</label>
2773        <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px" onclick="triggerSocialPillar('headline')">Test Now</button>
2774      </div>
2775      <div id="sp-trigger-result" style="font-size:.72rem;color:var(--muted);margin-bottom:6px"></div>
2776      <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px">
2777        <button class="btn btn-primary" onclick="saveSocialPillarConfig()">Save Config</button>
2778        <div id="sp-config-result" style="font-size:.73rem"></div>
2779      </div>
2780      <div style="font-size:.68rem;color:var(--muted);letter-spacing:1.5px;margin-bottom:6px">RECENT POSTS</div>
2781      <div style="overflow-x:auto">
2782        <table style="width:100%;border-collapse:collapse;font-size:.72rem" id="sp-posts-tbl">
2783          <thead>
2784            <tr style="border-bottom:1px solid var(--border)">
2785              <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">PILLAR</th>
2786              <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">TEXT</th>
2787              <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">CHANNEL</th>
2788              <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">POSTED</th>
2789            </tr>
2790          </thead>
2791          <tbody id="sp-posts-body"></tbody>
2792        </table>
2793        <div id="sp-posts-empty" style="display:none;padding:24px 0;text-align:center;font-size:.75rem;color:var(--muted)">No posts yet.</div>
2794      </div>
2795    </div>
2796  </div><!-- /as-social-pillars -->
2797
2798  <!-- Templated.io Template Manager -->
2799  <div style="display:none">
2800  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:18px;
2800cursor:pointer" onclick="toggleAdminSection('as-templated')">
2801    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">TEMPLATED.IO — TEMPLATE MANAGER <span id="as-templated-arrow" style="margin-left:4px">▸</span></div>
2802    <button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();loadTemplatedConfigs()">↻ Refresh</button>
2803  </div>
2804  <div id="as-templated" style="display:none">
2805  <div class="card" style="margin-bottom:14px;border:1px solid rgba(99,102,241,.35)">
2806    <h4>ADD NEW TEMPLATE</h4>
2807    <p style="font-size:.73rem;color:var(--muted);margin-bottom:12px">Add a Templated.io template without touching Railway env vars. No redeploy needed.</p>
2808    <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
2809      <div class="form-group"><label class="form-label">INTERNAL KEY (unique)</label><input type="text" id="tc-key" placeholder="dark_theme, premium_4…"></div>
2810      <div class="form-group"><label class="form-label">DISPLAY LABEL</label><input type="text" id="tc-label" placeholder="Dark Theme"></div>
2811    </div>
2812    <div class="form-group"><label class="form-label">TEMPLATED.IO TEMPLATE UUID</label><input type="text" id="tc-tid" placeholder="a44da04c-91c2-4c61-b556-65e8811aceb8" style="font-family:var(--mono);font-size:.78rem"></div>
2813    <div style="display:flex;align-items:center;gap:16px;margin-bottom:12px;flex-wrap:wrap">
2814      <label style="font-size:.78rem;display:flex;align-items:center;gap:6px;cursor:pointer">
2815        <input type="checkbox" id="tc-free" checked> Free template
2816      </label>
2817      <label style="font-size:.78rem;display:flex;align-items:center;gap:6px;cursor:pointer">
2818        <input type="checkbox" id="tc-user-slot"> User-purchasable slot
2819      </label>
2820      <div class="form-group" style="margin:0;flex:1"><label class="form-label">PRICE (USD, if paid)</label><input type="number" id="tc-price" placeholder="9.99" step="0.01" min="0"></div>
2821    </div>
2822    <div style="display:flex;gap:8px">
2823      <button class="btn btn-ghost" onclick="verifyTemplatedConfig()" id="tc-verify-btn">🔍 Verify Template</button>
2824      <button class="btn btn-primary" onclick="saveTemplatedConfig()" id="tc-save-btn">+ Save Template</button>
2825    </div>
2826    <div id="tc-verify-result" style="margin-top:8px;font-size:.73rem"></div>
2827  </div>
2828  <div id="tc-list" style="margin-bottom:14px"></div>
2829  </div><!-- /as-templated -->
2830  </div><!-- hidden wrapper -->
2831
2832  <!-- Polymarket Monitor Config -->
2833  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:18px;cursor:pointer" onclick="toggleAdminSection('as-polymarket')">
2834    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">POLYMARKET MONITOR — CONFIG <span id="as-polymarket-arrow" style="margin-left:4px">▸</span></div>
2835    <button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();loadPolymarketConfig()">&#8635; Refresh</button>
2836  </div>
2837  <div id="as-polymarket" style="display:none">
2838  <div class="card" style="margin-bottom:14px;border:1px solid rgba(0,200,83,.2)">
2839    <h4>MONITOR THRESHOLDS — v2</h4>
2840    <p style="font-size:.73rem;color:var(--muted);margin-bottom:12px">Controls the background monitor. Changes take effect within 10 min.</p>
2841    <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
2842      <div class="form-group"><label class="form-label">OI CHANGE THRESHOLD (%)</label><input type="number" id="pm-oi-chg" step="1" placeholder="10"></div>
2843      <div class="form-group"><label class="form-label">VOLUME SURGE MULTIPLIER (×)</label><input type="number" id="pm-vol-mult" step="0.1" placeholder="1.5"></div>
2844      <div class="form-group"><label class="form-label">BOOK IMBALANCE RATIO</label><input type="number" id="pm-book-imb" step="0.5" placeholder="3.0"></div>
2845      <div class="form-group"><label class="form-label">MAX SPREAD (% of mid)</label><input type="number" id="pm-spread-pct" step="0.5" placeholder="5.0"></div>
2846      <div class="form-group"><label class="form-label">MIN 24H VOLUME ($)</label><input type="number" id="pm-min-vol24" placeholder="10000"></div>
2847      <div class="form-group"><label class="form-label">MAX MARKETS TRACKED</label><input type="number" id="pm-max-markets" placeholder="50"></div>
2848      <div class="form-group"><label class="form-label">ALERT COOLDOWN (sec)</label><input type="number" id="pm-cooldown" placeholder="3600"></div>
2849      <div class="form-group"><label class="form-label">POLLING INTERVAL (sec)</label><input type="number" id="pm-poll-interval" placeholder="60"></div>
2850    </div>
2851    <div class="form-group"><label class="form-label">INCLUDE KEYWORDS (comma-separated, blank = all)</label><input type="text" id="pm-include-kw" placeholder="trump, fed, bitcoin"></div>
2852    <div class="form-group"><label class="form-label">EXCLUDE KEYWORDS (comma-separated)</label><input type="text" id="pm-exclude-kw" placeholder="nfl, nba, oscars"></div>
2853    <div style="margin-bottom:14px">
2854      <label style="font-size:.78rem;display:flex;align-items:center;gap:7px;cursor:pointer"><input type="checkbox" id="pm-enabled"> Monitor Enabled</label>
2855    </div>
2856    <div style="display:flex;align-items:center;gap:10px">
2857      <button class="btn btn-primary" onclick="savePolymarketConfig()">Save Config</button>
2858      <div id="pm-config-result" style="font-size:.73rem"></div>
2859    </div>
2860    <div style="margin-top:16px;border-top:1px solid var(--border);padding-top:12px">
2861      <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:8px">
2861RECENT ALERTS (ADMIN VIEW)</div>
2862      <div style="display:flex;gap:8px;margin-bottom:8px">
2863        <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px" onclick="loadAdminPolymarketAlerts()">Load Alerts</button>
2864        <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px;color:var(--red);border-color:rgba(255,68,68,.3)" onclick="clearPolymarketAlerts()">Clear History</button>
2865      </div>
2866      <div id="pm-admin-alerts-list" style="max-height:260px;overflow-y:auto"></div>
2867    </div>
2868  </div>
2869  </div><!-- /as-polymarket -->
2870
2871  <!-- DJT Signal Tools -->
2872  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:18px;cursor:pointer" onclick="toggleAdminSection('as-djt')">
2873    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">🇺🇸 DJT SIGNAL TOOLS <span id="as-djt-arrow" style="margin-left:4px">▸</span></div>
2874  </div>
2875  <div id="as-djt" style="display:none">
2876  <div class="card" style="margin-bottom:14px;border:1px solid rgba(255,68,68,.2)">
2877    <h4>TRUTH SOCIAL MONITOR — @realDonaldTrump</h4>
2878    <p style="font-size:.73rem;color:var(--muted);margin-bottom:12px">Manually trigger a fetch from Truth Social. After clicking, check Railway logs for the HTTP status — look for <code>fetch_djt_tweets: N new post(s) saved</code> or a warning with the HTTP code.</p>
2879    <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
2880      <button class="btn btn-primary" onclick="adminDjtRefresh(this)">⚡ Trigger Fetch Now</button>
2881      <div id="djt-refresh-result" style="font-size:.73rem;color:var(--muted)"></div>
2882    </div>
2883  </div>
2884  </div><!-- /as-djt -->
2885
2886  <!-- Tax Sessions (CPA) -->
2887  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:18px;cursor:pointer" onclick="toggleAdminSection('as-tax')">
2888    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">PANAMA TAX FORM — CPA REVIEW <span id="as-tax-arrow" style="margin-left:4px">▾</span></div>
2889    <button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();loadTaxSessions()">&#8635; Refresh</button>
2890  </div>
2891  <div id="as-tax">
2892  <div class="card" style="margin-bottom:10px;border:1px solid rgba(226,194,117,.2)">
2893    <h4>CPA EMAIL DESTINATIONS</h4>
2894    <p style="font-size:.73rem;color:var(--muted);margin-bottom:10px">When a user categorizes their Panama tax form, the result is emailed to these addresses for CPA review.</p>
2895    <div class="form-group" style="margin-bottom:8px">
2896      <label class="form-label">EMAIL(S) — comma-separated</label>
2897      <input type="text" id="tax-cpa-emails" placeholder="[email protected],[email protected]">
2898    </div>
2899    <div class="form-group" style="margin-bottom:8px">
2900      <label class="form-label">YAPPY QR URL — image URL shown to users for payment</label>
2901      <input type="text" id="tax-yappy-qr-url" placeholder="https://... (leave blank to hide)">
2902    </div>
2903    <div style="display:flex;align-items:center;gap:10px">
2904      <button class="btn btn-primary" onclick="saveTaxConfig()">Save</button>
2905      <span id="tax-config-result" style="font-size:.73rem"></span>
2906    </div>
2907  </div>
2908  <div class="card" style="margin-bottom:14px;padding:0;overflow:hidden">
2909    <div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between">
2910      <h4 style="margin:0">TAX SESSIONS</h4>
2911      <span id="tax-paid-badge" style="font-size:.65rem;color:var(--muted);font-family:var(--mono)"></span>
2912    </div>
2913    <div id="tax-sessions-list" style="max-height:400px;overflow-y:auto">
2914      <div style="padding:20px;text-align:center;color:var(--muted);font-size:.8rem">Click Refresh to load sessions</div>
2915    </div>
2916  </div>
2917  </div><!-- /as-tax -->
2918
2919  <!-- Tax Session Detail Modal -->
2920  <div id="tax-modal-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;overflow-y:auto;padding:20px" onclick="if(event.target===this)closeTaxModal()">
2921    <div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;max-width:760px;margin:auto;padding:24px;position:relative">
2922      <button onclick="closeTaxModal()" style="position:absolute;top:12px;right:16px;background:none;border:none;color:var(--muted);font-size:1.2rem;cursor:pointer">✕</button>
2923      <div id="tax-modal-body"></div>
2924    </div>
2925  </div>
2926
2927  <!-- Manual push form -->
2928  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:18px;
2928cursor:pointer" onclick="toggleAdminSection('as-news-push')">
2929    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">NEWS — PUSH &amp; HISTORY <span id="as-news-push-arrow" style="margin-left:4px">▸</span></div>
2930  </div>
2931  <div id="as-news-push" style="display:none">
2932  <div class="card" style="margin-bottom:14px">
2933    <h4>PUSH NEWS MANUALLY</h4>
2934    <div class="form-group" style="margin-top:10px"><label class="form-label">TYPE</label><select id="an-content-type"><option value="news">📰 News</option><option value="economic">📈 Economic</option><option value="earnings">💰 Earnings</option><option value="company_analysis">🏢 Company Analysis</option><option value="macro">🌍 Macro</option></select></div>
2935    <div class="form-group"><label class="form-label">HEADLINE</label><input type="text" id="an-title" placeholder="Breaking: ..."></div>
2936    <div class="form-group"><label class="form-label">SUMMARY</label><textarea id="an-summary" rows="2" placeholder="Article summary..."></textarea></div>
2937    <div class="form-group"><label class="form-label">IMAGE URL (Cloudinary — permanent)</label><input type="text" id="an-img" placeholder="https://res.cloudinary.com/..."></div>
2938    <div class="form-group"><label class="form-label">AI CAPTION</label><textarea id="an-caption" rows="3" placeholder="Ready-to-post caption..."></textarea></div>
2939    <div class="form-group"><label class="form-label">SOURCE URL</label><input type="text" id="an-source" placeholder="https://..."></div>
2940    <button class="btn btn-primary" onclick="adminPushNews()">📰 Push to Clients</button>
2941    <div id="an-result" style="margin-top:8px"></div>
2942  </div>
2943  <!-- Manual screenshot upload card -->
2944  <div class="card" style="margin-bottom:14px">
2945    <h4>PUSH SCREENSHOT / PNG</h4>
2946    <p style="margin-bottom:10px;font-size:.73rem;color:var(--muted)">Upload a screenshot directly — no Cloudinary URL needed. Image is uploaded automatically.</p>
2947    <div class="form-group" style="margin-top:10px"><label class="form-label">TYPE</label><select id="anu-content-type"><option value="chart">📊 Chart</option><option value="news">📰 News</option><option value="economic">📈 Economic</option><option value="earnings">💰 Earnings</option><option value="company_analysis">🏢 Company Analysis</option><option value="macro">🌍 Macro</option></select></div>
2948    <div class="form-group"><label class="form-label">HEADLINE</label><input type="text" id="anu-title" placeholder="NVDA breakout above EMA9"></div>
2949    <div class="form-group"><label class="form-label">SCREENSHOT (PNG / JPG)</label><input type="file" id="anu-file" accept="image/*" style="color:var(--text);font-size:.78rem"></div>
2950    <div class="form-group"><label class="form-label">CAPTION (optional)</label><textarea id="anu-caption" rows="2" placeholder="Ready-to-post caption..."></textarea></div>
2951    <button class="btn btn-primary" onclick="adminUploadNews()" id="anu-btn">📤 Upload & Push</button>
2952    <div id="anu-result" style="margin-top:8px"></div>
2953  </div>
2954  <!-- TradingView Ideas fetch -->
2955  <div class="card" style="margin-bottom:14px">
2956    <h4>TRADINGVIEW IDEAS</h4>
2957    <p style="margin-bottom:12px;font-size:.73rem;color:var(--muted)">Manually pull the latest ideas from TradingView (username: <strong>Factoz</strong>) into the News Feed.</p>
2958    <button class="btn btn-primary" id="tv-fetch-btn" onclick="adminFetchTVIdeas()">💡 Fetch Latest Ideas</button>
2959    <div id="tv-fetch-result" style="margin-top:8px;font-size:.75rem"></div>
2960  </div>
2961  <!-- Rich text / HTML content push -->
2962  <div class="card" style="margin-bottom:14px">
2963    <h4>PUSH RICH TEXT / HTML CONTENT <span id="nl-sub-count" style="font-size:.7rem;color:var(--text2);font-weight:400"></span> <button onclick="toggleSubList()" id="nl-view-btn" class="btn btn-ghost" style="font-size:.65rem;padding:2px 8px;margin-left:6px">View List</button></h4>
2964    <div id="nl-sub-list" style="display:none;margin-bottom:12px"></div>
2965    <p style="margin-bottom:10px;font-size:.73rem;color:var(--muted)">Push an analysis or macro post with HTML formatting — no image needed. HTML in the body renders directly in client dashboards.</p>
2966    <div class="form-group"><label class="form-label">TYPE</label>
2967      <select id="art-content-type">
2968        <option value="macro">🌍 Macro</option>
2969        <option value="company_analysis">🏢 Company Analysis</option>
2970        <option value="news">📰 News</option>
2971        <option value="economic">📈 Economic</option>
2972      </select>
2973    </div>
2974    <div class="form-group"><label class="form-label">HEADLINE</label><input type="text" id="art-title" placeholder="Q1 2026 Earnings Preview: NVDA"></div>
2975    <div class="form-group">
2976      <label class="form-label">BODY (HTML supported — &lt;b&gt;, &lt;br&gt;, &lt;ul&gt;, &lt;table&gt; etc.)</label>
2977      <textarea id="art-body" rows="8" placeholder="&lt;b&gt;Revenue&lt;/b&gt;: $39.3B (+78% YoY)&lt;br&gt;&lt;b&gt;
2977Key takeaway&lt;/b&gt;: Data center growth continues..."></textarea>
2978    </div>
2979    <div class="form-group"><label class="form-label">SOURCE URL (optional)</label><input type="text" id="art-source" placeholder="https://..."></div>
2980    <div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
2981      <input type="checkbox" id="art-newsletter" style="width:16px;height:16px;cursor:pointer;accent-color:var(--teal)">
2982      <label for="art-newsletter" style="font-size:.78rem;color:var(--text2);cursor:pointer">📧 Also send as newsletter to email subscribers</label>
2983    </div>
2984    <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px">
2985      <input type="checkbox" id="art-publish-blog" style="width:16px;height:16px;cursor:pointer;accent-color:#E2C275">
2986      <label for="art-publish-blog" style="font-size:.78rem;color:var(--text2);cursor:pointer">📝 Also publish to <strong style="color:#E2C275">factozcentral.com/blog</strong></label>
2987    </div>
2988    <button class="btn btn-primary" onclick="adminPushRichText()" id="art-btn">📝 Push to Clients</button>
2989    <div id="art-result" style="margin-top:8px"></div>
2990  </div>
2991
2992  <div id="admin-news-list"></div>
2993  </div><!-- /as-news-push -->
2994  <hr>
2995  <!-- Newsletter Subscribers -->
2996  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:18px;cursor:pointer" onclick="toggleAdminSection('as-subscribers')">
2997    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">NEWSLETTER SUBSCRIBERS <span id="as-subscribers-badge" style="color:var(--teal);margin-left:6px"></span><span id="as-subscribers-arrow" style="margin-left:4px">▾</span></div>
2998    <button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();loadSubscriberList()">↻ Refresh</button>
2999  </div>
3000  <div id="as-subscribers">
3001    <div class="card" style="margin-bottom:14px;border:1px solid rgba(0,200,183,.25)">
3002      <p style="font-size:.73rem;color:var(--muted);margin-bottom:12px">Everyone on this list receives the daily brief and weekly signal digest automatically — no account needed.</p>
3003
3004      <!-- Signup link to paste into IG/TikTok DM replies. Lives here rather
3005           than in the Newsroom because this is the list it feeds. -->
3006      <div style="background:rgba(0,188,212,.07);border:1px solid rgba(0,188,212,.28);border-radius:8px;padding:11px 13px;margin-bottom:14px">
3007        <div style="font-size:.68rem;color:var(--muted);font-family:var(--mono);letter-spacing:1px;margin-bottom:7px">LINK DE REGISTRO — para responder DMs</div>
3008        <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
3009          <code id="sub-signup-url" style="flex:1;min-width:220px;font-size:.78rem;color:var(--text);background:rgba(0,0,0,.25);padding:8px 10px;border-radius:6px;word-break:break-all"></code>
3010          <button class="btn btn-ghost" style="font-size:.7rem;padding:6px 11px" onclick="copySignupUrl(this)">📋 Copiar</button>
3011          <a id="sub-signup-open" href="#" target="_blank" rel="noopener" class="btn btn-ghost" style="font-size:.7rem;padding:6px 11px;text-decoration:none">↗ Abrir</a>
3012        </div>
3013        <div style="font-size:.68rem;color:var(--muted);margin-top:7px">Quien se registre aquí entra a esta lista automáticamente y recibe el correo de confirmación.</div>
3014      </div>
3015
3016      <!-- Single add -->
3017      <div style="display:flex;gap:8px;margin-bottom:10px;align-items:flex-end">
3018        <div class="form-group" style="flex:1;margin:0">
3019          <label class="form-label">ADD EMAIL</label>
3020          <input type="email" id="sub-email-input" placeholder="[email protected]" style="width:100%">
3021        </div>
3022        <button class="btn btn-primary" style="font-size:.75rem;padding:7px 16px;flex-shrink:0" onclick="addSubscriber()">+ Add</button>
3023      </div>
3024
3025      <!-- Bulk paste -->
3026      <div style="margin-bottom:10px">
3027        <label class="form-label">BULK ADD (one email per line or comma-separated)</label>
3028        <textarea id="sub-bulk-input" rows="4" placeholder="[email protected]&#10;[email protected]&#10;[email protected]" style="width:100%;font-size:.75rem;line-height:1.6"></textarea>
3029        <button class="btn btn-ghost" style="font-size:.72rem;margin-top:6px" onclick="addSubscribersBulk()">⬆ Import All</button>
3030      </div>
3031      <div id="sub-add-result" style="font-size:.75rem;margin-bottom:8px"></div>
3032
3033      <!-- Subscriber list -->
3034      <div id="sub-list" style="margin-top:10px;max-height:300px;overflow-y:auto"></div>
3035    </div>
3036  </div>
3037
3038  <hr>
3039  <!-- Outreach Tracker -->
3040  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:18px;
3040cursor:pointer" onclick="toggleAdminSection('as-outreach')">
3041    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">OUTREACH TRACKER <span id="as-outreach-arrow" style="margin-left:4px">▸</span></div>
3042    <button class="btn btn-yellow" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();openOutreachForm()">+ Add Prospect</button>
3043  </div>
3044  <div id="as-outreach" style="display:none">
3045    <!-- Add/Edit Form -->
3046    <div id="outreach-form-panel" style="display:none;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:18px;margin-bottom:14px">
3047      <h4 style="margin-bottom:14px;font-size:.82rem">New Prospect</h4>
3048      <div class="g2">
3049        <div class="form-group">
3050          <label class="form-label">NAME *</label>
3051          <input type="text" id="oc-name" placeholder="Jane Trader">
3052        </div>
3053        <div class="form-group">
3054          <label class="form-label">TOPIC (what they cover)</label>
3055          <input type="text" id="oc-topic" placeholder="crypto, small caps, macro">
3056        </div>
3057        <div class="form-group">
3058          <label class="form-label">EMAIL</label>
3059          <input type="email" id="oc-email" placeholder="[email protected]">
3060        </div>
3061        <div class="form-group">
3062          <label class="form-label">PRIMARY PLATFORM</label>
3063          <select id="oc-platform">
3064            <option value="x">X / Twitter</option>
3065            <option value="youtube">YouTube</option>
3066            <option value="newsletter">Newsletter</option>
3067            <option value="other">Other</option>
3068          </select>
3069        </div>
3070        <div class="form-group">
3071          <label class="form-label">SEGMENT</label>
3072          <select id="oc-segment">
3073            <option value="creator">Finance Creator</option>
3074            <option value="advisor">RIA / Advisor</option>
3075            <option value="crypto">Crypto Influencer</option>
3076            <option value="other">Other</option>
3077          </select>
3078        </div>
3079        <div class="form-group">
3080          <label class="form-label">X HANDLE</label>
3081          <input type="text" id="oc-handle-x" placeholder="@janetrader">
3082        </div>
3083        <div class="form-group">
3084          <label class="form-label">INSTAGRAM HANDLE</label>
3085          <input type="text" id="oc-handle-ig" placeholder="@janetrader">
3086        </div>
3087        <div class="form-group">
3088          <label class="form-label">TELEGRAM USERNAME</label>
3089          <input type="text" id="oc-handle-tg" placeholder="@janetrader">
3090        </div>
3091        <div class="form-group">
3092          <label class="form-label">LINKEDIN</label>
3093          <input type="text" id="oc-handle-li" placeholder="linkedin.com/in/jane">
3094        </div>
3095        <div class="form-group">
3096          <label class="form-label">TIKTOK HANDLE</label>
3097          <input type="text" id="oc-handle-tt" placeholder="@janetrader">
3098        </div>
3099      </div>
3100      <div class="form-group">
3101        <label class="form-label">PRIMARY HANDLE (if only one)</label>
3102        <input type="text" id="oc-handle" placeholder="@janetrader — fill if only one handle">
3103      </div>
3104      <div class="form-group">
3105        <label class="form-label">NOTES</label>
3106        <textarea id="oc-notes" rows="2" style="width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:8px;font-size:.78rem;resize:vertical" placeholder="Context, intro angle, mutual connections..."></textarea>
3107      </div>
3108      <div class="form-group">
3109        <label class="form-label">PADDLE AFFILIATE LINK</label>
3110        <input type="url" id="oc-affiliate" placeholder="https://buy.paddle.com/...?affiliate=xxx" style="width:100%">
3111      </div>
3112      <div style="display:flex;gap:10px;margin-top:10px">
3113        <button class="btn btn-primary" onclick="saveOutreachContact()" id="oc-save-btn">💾 Save Prospect</button>
3114        <button class="btn btn-ghost" onclick="$('outreach-form-panel').style.display='none'">Cancel</button>
3115      </div>
3116      <div id="oc-result" style="margin-top:8px;font-size:.75rem"></div>
3117    </div>
3118    <!-- Contact List -->
3119    <div id="outreach-contact-list"><p style="font-size:.73rem;color:var(--muted)">Loading...</p></div>
3120  </div>
3121  <!-- Person Library — hidden -->
3122  <div style="display:none">
3123  <hr>
3124  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;cursor:pointer" onclick="toggleAdminSection('as-carousel')">
3125    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">CAROUSEL — PERSON LIBRARY <span id="as-carousel-arrow" style="margin-left:4px">
3125▸</span></div>
3126    <button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();loadPersons()">↻ Refresh</button>
3127  </div>
3128  <div id="as-carousel" style="display:none">
3129  <div class="card" style="margin-bottom:14px">
3130    <h4>UPLOAD PERSON IMAGE</h4>
3131    <p style="margin-bottom:12px;font-size:.73rem">Upload cropped headshots of famous people (CEOs, politicians, etc). Clients will pick from this list when building carousels. Claude vision will read the image to generate captions.</p>
3132    <div class="form-group" style="margin-top:10px">
3133      <label class="form-label">DISPLAY NAME (e.g. "Elon Musk")</label>
3134      <input type="text" id="person-name" placeholder="Elon Musk">
3135    </div>
3136    <div class="form-group">
3137      <label class="form-label">PHOTO (JPG/PNG — cropped headshot, min 500×500)</label>
3138      <input type="file" id="person-file" accept=".jpg,.jpeg,.png,.webp" style="color:var(--text);font-size:.78rem">
3139    </div>
3140    <button class="btn btn-primary" onclick="uploadPerson()">📤 Upload Photo</button>
3141    <div id="person-upload-result" style="margin-top:8px"></div>
3142  </div>
3143  <!-- Person grid -->
3144  <div id="person-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px;margin-bottom:20px"></div>
3145  </div><!-- /as-carousel -->
3146  </div><!-- hidden wrapper -->
3147
3148  <hr>
3149  <!-- Ad Manager -->
3150  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:18px;cursor:pointer" onclick="toggleAdminSection('as-ads')">
3151    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">AD MANAGER <span id="as-ads-arrow" style="margin-left:4px">▸</span></div>
3152    <button class="btn btn-yellow" style="font-size:.7rem;padding:5px 10px" onclick="event.stopPropagation();document.getElementById('ad-upload-panel').style.display='block'">+ New Ad</button>
3153  </div>
3154  <div id="as-ads" style="display:none">
3155    <!-- Upload form -->
3156    <div id="ad-upload-panel" style="display:none;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:18px;margin-bottom:14px">
3157      <h4 style="margin-bottom:14px;font-size:.82rem">New Ad Slot</h4>
3158      <div class="g2">
3159        <div class="form-group">
3160          <label class="form-label">SLOT NAME</label>
3161          <select id="ad-slot-name" style="width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:7px 10px;color:var(--text);font-size:.78rem">
3162            <option value="overview-top">overview-top — Above bento grid</option>
3163            <option value="news-sidebar">news-sidebar — Below news tabs</option>
3164            <option value="scanner-bottom">scanner-bottom — Below scanner</option>
3165            <option value="carousel-side">carousel-side — Carousel sidebar</option>
3166          </select>
3167        </div>
3168        <div class="form-group">
3169          <label class="form-label">INTERNAL LABEL (optional)</label>
3170          <input type="text" id="ad-label" placeholder="Acme Corp Q1 2026">
3171        </div>
3172      </div>
3173      <div class="form-group">
3174        <label class="form-label">CLICK URL (optional)</label>
3175        <input type="text" id="ad-link-url" placeholder="https://example.com">
3176      </div>
3177      <div class="form-group">
3178        <label class="form-label">IMAGE (GIF or PNG — recommended 728×90 banner or 300×250 square)</label>
3179        <input type="file" id="ad-file" accept=".gif,.png,.jpg,.jpeg,.webp" style="color:var(--text);font-size:.78rem">
3180      </div>
3181      <div style="display:flex;gap:8px">
3182        <button class="btn btn-primary" onclick="uploadAdSlot()" id="ad-upload-btn">📤 Upload Ad</button>
3183        <button class="btn btn-ghost" onclick="document.getElementById('ad-upload-panel').style.display='none'">Cancel</button>
3184      </div>
3185      <div id="ad-upload-result" style="margin-top:8px;font-size:.75rem"></div>
3186    </div>
3187    <!-- Ad list -->
3188    <div id="ad-slot-list"></div>
3189  </div>
3190
3191  <!-- ── OPTIONS FLOW PERFORMANCE ── -->
3192  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:22px;
3192cursor:pointer" onclick="toggleAdminSection('as-opts-perf')">
3193    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">OPTIONS FLOW PERFORMANCE <span id="as-opts-perf-arrow" style="margin-left:4px">▸</span></div>
3194    <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px" onclick="event.stopPropagation();loadOptionsPerformance()">↻ Refresh</button>
3195  </div>
3196  <div id="as-opts-perf" style="display:none">
3197    <!-- Summary bar -->
3198    <div id="opts-perf-summary" style="display:flex;gap:20px;flex-wrap:wrap;margin-bottom:16px;padding:14px 16px;background:var(--bg3);border-radius:8px;border:1px solid var(--border)">
3199      <div style="text-align:center">
3200        <div style="font-size:1.4rem;font-weight:700;color:var(--green)" id="opts-perf-winrate">—</div>
3201        <div style="font-size:.62rem;color:var(--muted);letter-spacing:1px;margin-top:2px">OVERALL WIN RATE</div>
3202      </div>
3203      <div style="text-align:center">
3204        <div style="font-size:1.4rem;font-weight:700;color:var(--text)" id="opts-perf-total">—</div>
3205        <div style="font-size:.62rem;color:var(--muted);letter-spacing:1px;margin-top:2px">ARCHIVED</div>
3206      </div>
3207      <div style="text-align:center">
3208        <div style="font-size:1.4rem;font-weight:700;color:var(--text)" id="opts-perf-resolved">—</div>
3209        <div style="font-size:.62rem;color:var(--muted);letter-spacing:1px;margin-top:2px">RESOLVED</div>
3210      </div>
3211      <div id="opts-perf-tickers" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center"></div>
3212    </div>
3213    <!-- Week range filter -->
3214    <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap">
3215      <label style="font-size:.7rem;color:var(--muted)">WEEKS BACK</label>
3216      <select id="opts-perf-weeks" onchange="loadOptionsPerformance()" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:.75rem;padding:4px 8px;border-radius:5px">
3217        <option value="4">4 weeks</option>
3218        <option value="12" selected>12 weeks</option>
3219        <option value="26">26 weeks</option>
3220        <option value="52">52 weeks</option>
3221      </select>
3222      <input type="text" id="opts-perf-ticker" placeholder="Filter ticker (e.g. MSFT)" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:.75rem;padding:4px 10px;border-radius:5px;width:160px" onkeydown="if(event.key==='Enter')loadOptionsPerformance()">
3223    </div>
3224    <!-- Weekly table -->
3225    <div style="font-size:.68rem;color:var(--muted);letter-spacing:1.5px;margin-bottom:6px">WEEKLY BREAKDOWN</div>
3226    <div style="overflow-x:auto;margin-bottom:18px">
3227      <table style="width:100%;border-collapse:collapse;font-size:.76rem" id="opts-perf-weekly-tbl">
3228        <thead>
3229          <tr style="border-bottom:1px solid var(--border)">
3230            <th style="text-align:left;padding:6px 8px;color:var(--muted);font-weight:600">WEEK</th>
3231            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">TOTAL</th>
3232            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">RESOLVED</th>
3233            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">WIN</th>
3234            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">WIN RATE</th>
3235            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">CALL ITM</th>
3236            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">PUT OTM</th>
3237          </tr>
3238        </thead>
3239        <tbody id="opts-perf-weekly-body"></tbody>
3240      </table>
3241    </div>
3242    <!-- Contract list -->
3243    <div style="font-size:.68rem;color:var(--muted);letter-spacing:1.5px;margin-bottom:6px">ALL ARCHIVED CONTRACTS</div>
3244    <div style="overflow-x:auto">
3245      <table style="width:100%;border-collapse:collapse;font-size:.72rem" id="opts-perf-contracts-tbl">
3246        <thead>
3247          <tr style="border-bottom:1px solid var(--border)">
3248            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">WEEK</th>
3249            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">TICKER</th>
3250            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">TYPE</th>
3251            <th style="text-align:right;padding:5px 8px;color:var(--muted);font-weight:600">STRIKE</th>
3252            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">EXPIRY</th>
3253            <th style="text-align:right;padding:5px 8px;color:var(--muted);font-weight:600">PREMIUM</th>
3254            <th style="text-align:right;padding:5px 8px;color:var(--muted);font-weight:600">VOL/OI</th>
3255            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">RESULT</th>
3256            <th style="text-align:right;padding:5px 8px;color:var(--muted);font-weight:600">P&amp;L %</th>
3257            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">FLAGGED</th>
3258          </tr>
3259        </thead>
3260        <tbody id="opts-perf-contracts-body"></tbody>
3261      </table>
3262      <div id="opts-perf-empty" style="display:none;padding:24px 0;text-align:center;font-size:.75rem;color:var(--muted)">No archived contracts yet. Data appears here after contracts expire + newsletter is sent (30-day retention before archival).</div>
3263    </div>
3264  </div>
3265
3266  <!-- ── EQUITY SCANNER PERFORMANCE ── -->
3267  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:22px;
3267cursor:pointer" onclick="toggleAdminSection('as-scan-perf')">
3268    <div style="font-size:.8rem;color:var(--text);font-family:var(--mono);letter-spacing:2px">EQUITY SCANNER PERFORMANCE <span id="as-scan-perf-arrow" style="margin-left:4px">▸</span></div>
3269    <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px" onclick="event.stopPropagation();loadScannerPerformance()">↻ Refresh</button>
3270  </div>
3271  <div id="as-scan-perf" style="display:none">
3272    <!-- Summary bar -->
3273    <div id="scan-perf-summary" style="display:flex;gap:20px;flex-wrap:wrap;margin-bottom:16px;padding:14px 16px;background:var(--bg3);border-radius:8px;border:1px solid var(--border)">
3274      <div style="text-align:center">
3275        <div style="font-size:1.4rem;font-weight:700;color:var(--green)" id="scan-perf-winrate">—</div>
3276        <div style="font-size:.62rem;color:var(--muted);letter-spacing:1px;margin-top:2px">30D WIN RATE</div>
3277      </div>
3278      <div style="text-align:center">
3279        <div style="font-size:1.4rem;font-weight:700;color:var(--text)" id="scan-perf-total">—</div>
3280        <div style="font-size:.62rem;color:var(--muted);letter-spacing:1px;margin-top:2px">TOTAL PICKS</div>
3281      </div>
3282      <div style="text-align:center">
3283        <div style="font-size:1.4rem;font-weight:700;color:var(--text)" id="scan-perf-resolved">—</div>
3284        <div style="font-size:.62rem;color:var(--muted);letter-spacing:1px;margin-top:2px">RESOLVED (30D)</div>
3285      </div>
3286    </div>
3287    <!-- Filters -->
3288    <div style="display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap">
3289      <label style="font-size:.7rem;color:var(--muted)">DAYS BACK</label>
3290      <select id="scan-perf-days" onchange="loadScannerPerformance()" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:.75rem;padding:4px 8px;border-radius:5px">
3291        <option value="30">30 days</option>
3292        <option value="90">90 days</option>
3293        <option value="180" selected>180 days</option>
3294        <option value="365">365 days</option>
3295      </select>
3296      <label style="font-size:.7rem;color:var(--muted)">STRATEGY</label>
3297      <select id="scan-perf-strategy" onchange="loadScannerPerformance()" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:.75rem;padding:4px 8px;border-radius:5px">
3298        <option value="">All strategies</option>
3299        <option value="rsi_extremes">RSI Extremes (Pre-Market)</option>
3300        <option value="premarket_movers">Pre-Market Movers (MA Crossover, retired 2026-08-31)</option>
3301        <option value="breakout_52wk">52-Week Breakout</option>
3302        <option value="gap_and_hold">Gap &amp; Hold</option>
3303      </select>
3304      <input type="text" id="scan-perf-ticker" placeholder="Filter ticker (e.g. AAPL)" style="background:var(--bg3);border:1px solid var(--border);color:var(--text);font-size:.75rem;padding:4px 10px;border-radius:5px;width:160px" onkeydown="if(event.key==='Enter')loadScannerPerformance()">
3305    </div>
3306    <!-- Horizon breakdown table -->
3307    <div style="font-size:.68rem;color:var(--muted);letter-spacing:1.5px;margin-bottom:6px">BY STRATEGY &amp; HORIZON</div>
3308    <div style="overflow-x:auto;margin-bottom:18px">
3309      <table style="width:100%;border-collapse:collapse;font-size:.76rem" id="scan-perf-horizon-tbl">
3310        <thead>
3311          <tr style="border-bottom:1px solid var(--border)">
3312            <th style="text-align:left;padding:6px 8px;color:var(--muted);font-weight:600">STRATEGY</th>
3313            <th style="text-align:left;padding:6px 8px;color:var(--muted);font-weight:600">HORIZON</th>
3314            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">RESOLVED</th>
3315            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">PENDING</th>
3316            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">WIN RATE</th>
3317            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">95% CI</th>
3318            <th style="text-align:right;padding:6px 8px;color:var(--muted);font-weight:600">MEDIAN VS SPY</th>
3319          </tr>
3320        </thead>
3321        <tbody id="scan-perf-horizon-body"></tbody>
3322      </table>
3323    </div>
3324    <!-- Recent picks -->
3325    <div style="font-size:.68rem;color:var(--muted);letter-spacing:1.5px;margin-bottom:6px">RECENT PICKS</div>
3326    <div style="overflow-x:auto">
3327      <table style="width:100%;border-collapse:collapse;font-size:.72rem" id="scan-perf-picks-tbl">
3328        <thead>
3329          <tr style="border-bottom:1px solid var(--border)">
3330            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">TICKER</th>
3331            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">STRATEGY</th>
3332            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">DIRECTION</th>
3333            <th style="text-align:left;padding:5px 8px;color:var(--muted);font-weight:600">CALLED</th>
3334            <th style="text-align:right;padding:5px 8px;color:var(--muted);font-weight:600">ENTRY</th>
3335            <th style="text-align:right;padding:5px 8px;color:var(--muted);font-weight:600">30D VS SPY</th>
3336          </tr>
3337        </thead>
3338        <tbody id="scan-perf-picks-body"></tbody>
3339      </table>
3340      <div id="scan-perf-empty" style="display:none;padding:24px 0;text-align:center;font-size:.75rem;color:var(--muted)">No scan picks yet in this window.</div>
3341    </div>
3342  </div>
3343</div>
3344
3345<!-- ── DAILY BRIEF MODAL ── -->
3346<div id="brief-modal" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.85);backdrop-filter:blur(4px);align-items:flex-start;justify-content:center;padding:20px;overflow-y:auto" onclick="if(event.target===this)closeBriefModal()">
3347  <div style="background:#fff;border-radius:10px;width:100%;max-width:720px;margin:0 auto;overflow:hidden;position:relative">
3348    <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#0b132b;border-bottom:1px solid #202e56">
3349      <div id="brief-modal-title" style="font-size:.8rem;font-weight:700;color:#c5d0dc;font-family:monospace;letter-spacing:1px"></div>
3350      <button onclick="closeBriefModal()" style="background:none;border:none;color:#7a8fa3;font-size:1.2rem;cursor:pointer;padding:2px 6px">✕</button>
3351    </div>
3352    <iframe id="brief-modal-frame" style="width:100%;min-height:80vh;border:none;display:block"></iframe>
3353  </div>
3354</div>
3355
3356<!-- ── DESIGN STUDIO ── -->
3357<div class="page" id="page-studio">
3358  <div class="page-header">
3359    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/cbtlerlm.json" trigger="hover" stroke="bold" style="width:28px;height:28px"></lord-icon>Design Studio</div>
3360    <div class="page-sub">Create branded carousels &amp; posts — powered by ContentDrips</div>
3361  </div>
3362
3363  <div style="margin-bottom:20px">
3364    <button class="btn btn-primary" onclick="openDesignStudio()" id="studio-open-btn">✏️ Open Design Editor</button>
3365    <span style="font-size:.72rem;color:var(--muted);margin-left:12px">Design a carousel or post — your exports are saved automatically</span>
3366  </div>
3367
3368  <div id="studio-saving" style="display:none;font-size:.75rem;color:var(--yellow);margin-bottom:12px">Saving your design...</div>
3369
3370  <div style="font-size:.7rem;font-weight:700;color:var(--muted);letter-spacing:2px;margin-bottom:10px">MY DESIGNS</div>
3371  <div id="studio-gallery" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px"></div>
3372  <div id="studio-empty" style="display:none;font-size:.75rem;color:var(--muted);padding:20px 0">No designs yet. Click "Open Design Editor" to get started.</div>
3373</div>
3374
3375
3376<!-- ── DESIGN LAB ── -->
3377<div class="page" id="page-designlab">
3378  <div class="page-header">
3379    <div class="page-title">🎨 Design Lab</div>
3380    <div class="page-sub">Create branded social content — backgrounds, fonts, photos &amp; your gallery</div>
3381  </div>
3382
3383  <!-- ── Background Gallery ── -->
3384  <div style="margin-bottom:16px">
3385    <div style="font-size:.65rem;font-weight:700;color:var(--muted);letter-spacing:1.5px;margin-bottom:8px">BACKGROUND</div>
3386    <div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:10px">
3387      <button id="dl-tab-gradients" onclick="dlSetBgTab('gradients')" style="background:none;border:none;border-bottom:2px solid var(--teal);padding:6px 16px;font-size:.75rem;font-weight:700;color:var(--teal);cursor:pointer">Gradients</button>
3388      <button id="dl-tab-photos" onclick="dlSetBgTab('photos')" style="background:none;border:none;border-bottom:2px solid transparent;padding:6px 16px;font-size:.75rem;font-weight:700;color:var(--muted);cursor:pointer">Photos</button>
3389      <button id="dl-tab-colors" onclick="dlSetBgTab('colors')" style="background:none;border:none;border-bottom:2px solid transparent;padding:6px 16px;font-size:.75rem;font-weight:700;color:var(--muted);cursor:pointer">Color</button>
3390      <button id="dl-tab-mybgs" onclick="dlSetBgTab('mybgs')" style="background:none;border:none;border-bottom:2px solid transparent;padding:6px 16px;font-size:.75rem;font-weight:700;color:var(--muted);cursor:pointer">My BGs</button>
3391    </div>
3392    <div id="dl-bg-gradients" style="display:flex;flex-wrap:wrap;gap:8px"></div>
3393    <div id="dl-bg-photos" style="display:none">
3394      <div id="dl-photo-cats" style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px"></div>
3395      <div id="dl-photo-grid" style="display:flex;flex-wrap:wrap;gap:8px;max-height:280px;overflow-y:auto;padding-bottom:4px"></div>
3396    </div>
3397    <div id="dl-bg-colors" style="display:none;align-items:center;gap:10px;flex-wrap:wrap">
3398      <div style="display:flex;flex-wrap:wrap;gap:6px" id="dl-color-swatches"></div>
3399      <div style="display:flex;align-items:center;gap:6px">
3400        <label style="font-size:.65rem;color:var(--muted)">Custom:</label>
3401        <input type="color" id="dl-bg-color" value="#0b132b" title="Background color" onchange="dlSetBg(this.value)" style="width:36px;height:32px;border:none;border-radius:4px;cursor:pointer;padding:2px">
3402      </div>
3403    </div>
3404    <div id="dl-bg-mybgs" style="display:none;flex-direction:column;gap:8px">
3405      <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
3406        <input type="file" id="dl-mybg-upload" accept="image/*" style="display:none" onchange="dlUploadMyBg(this)">
3407        <button class="btn btn-ghost" style="font-size:.72rem" onclick="document.getElementById('dl-mybg-upload').click()">+ Upload Background</button>
3408        <span style="font-size:.65rem;color:var(--muted)">Saved to your Gallery — click any to set as background</span>
3409      </div>
3410      <div id="dl-mybg-grid" style="display:flex;flex-wrap:wrap;gap:8px;max-height:250px;overflow-y:auto"></div>
3411    </div>
3412  </div>
3413
3414  <!-- ── Toolbar ── -->
3415  <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;align-items:center">
3416    <button class="btn btn-ghost" style="font-size:.78rem" onclick="dlAddText()">+ Text</button>
3417    <button class="btn btn-ghost" style="font-size:.78rem" onclick="dlAddRect()">▭ Rect</button>
3418    <button class="btn btn-ghost" style="font-size:.78rem" onclick="dlAddCircle()">○ Circle</button>
3419    <select id="dl-font-select" onchange="dlLoadFont(this.value)" title="Font" style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--fg);font-size:.75rem;padding:5px 8px;cursor:pointer;max-width:150px"></select>
3420    <input type="number" id="dl-font-size" value="72" min="12" max="300" title="Font size" style="width:60px;background:var(--surface2);border:1px solid var(--border);border-radius:6px;color:var(--fg);font-size:.75rem;padding:5px 6px;text-align:center">
3421    <label style="font-size:.65rem;color:var(--muted)">Fill</label>
3422    <input type="color" id="dl-fill-color" value="#e2c275" title="Object fill color" style="width:32px;height:28px;border:none;border-radius:4px;cursor:pointer;padding:2px">
3423    <input type="file" id="dl-img-upload" accept="image/*" onchange="dlAddImageFile(this)" style="display:none">
3424    <button class="btn btn-ghost" style="font-size:.78rem" onclick="document.getElementById('dl-img-upload').click()">📁 Local</button>
3425    <button class="btn btn-ghost" style="font-size:.78rem" onclick="dlOpenGalleryPicker()">🖼 Gallery</button>
3426    <button class="btn btn-ghost" style="font-size:.78rem" title="Send selected object behind others" onclick="dlSendToBack()">⬇ Back</button>
3427    <button class="btn btn-ghost" style="font-size:.78rem" title="Bring selected object to front" onclick="dlBringToFront()">⬆ Front</button>
3428    <button class="btn btn-ghost" style="font-size:.78rem;color:var(--red)" onclick="dlDeleteSelected()">🗑 Delete</button>
3429    <button id="dl-draw-btn" class="btn btn-ghost" style="font-size:.78rem" onclick="dlToggleDraw()" title="Draw mode — use finger or stylus">✏️ Draw</button>
3430    <input id="dl-brush-size" type="range" min="1" max="20" value="3" style="display:none;width:70px;accent-color:var(--teal);cursor:pointer" oninput="dlUpdateBrush()" title="Brush size">
3431    <select id="dl-brush-type" style="display:none;font-size:.72rem;background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:4px 6px" onchange="dlUpdateBrush()">
3432      <option value="pencil">Pencil</option>
3433      <option value="marker">Marker</option>
3434      <option value="spray">Spray</option>
3435    </select>
3436    <button class="btn btn-primary" style="font-size:.78rem" onclick="dlExportPng()">↓ Export PNG</button>
3437  </div>
3438
3439  <!-- ── Canvas size presets ── -->
3440  <div style="display:flex;gap:6px;align-items:center;margin-bottom:14px;font-size:.72rem;color:var(--muted);flex-wrap:wrap">
3441    <span>Canvas:</span>
3442    <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px" onclick="dlResize(1080,1080)">1:1 Square</button>
3443    <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px" onclick="dlResize(1080,1920)">9:16 Story</button>
3444    <button class="btn btn-ghost" style="font-size:.7rem;padding:4px 10px" onclick="dlResize(1200,628)">16:9 Banner</button>
3445  </div>
3446
3447  <!-- ── Canvas ── -->
3448  <div style="border:1px solid var(--border);border-radius:10px;overflow:hidden;display:inline-block;max-width:100%">
3449    <canvas id="dl-canvas"></canvas>
3450  </div>
3451
3452  <!-- ── Gallery Picker Modal ── -->
3453  <div id="dl-gallery-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:9000;overflow-y:auto">
3454    <div style="background:var(--card);border:1px solid var(--border);border-radius:12px;max-width:680px;margin:40px auto;padding:20px">
3455      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
3456        <div style="font-size:.85rem;font-weight:700;color:var(--fg)">Pick from Gallery</div>
3457        <button onclick="document.getElementById('dl-gallery-modal').style.display='none'" style="background:none;border:none;cursor:pointer;font-size:1.2rem;color:var(--muted)">✕</button>
3458      </div>
3459      <div id="dl-gallery-picker-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px"></div>
3460      <div id="dl-gallery-picker-more" style="text-align:center;margin-top:12px;display:none">
3461        <button class="btn btn-ghost" style="font-size:.75rem" onclick="dlLoadGalleryPickerMore()">Load more</button>
3462      </div>
3463    </div>
3464  </div>
3465
3466  <!-- ── Article Reader Modal ── -->
3467  <div id="article-reader-modal" style="display:none;position:fixed;inset:0;z-index:1200;background:rgba(0,0,0,.75);overflow-y:auto;padding:20px 16px" onclick="if(event.target===this)closeArticleModal()">
3468    <div style="max-width:820px;margin:0 auto;background:var(--bg2);border:1px solid var(--border);border-radius:12px;overflow:hidden">
3469      <div id="article-reader-header" style="display:flex;align-items:center;justify-content:space-between;padding:14px 20px;border-bottom:1px solid var(--border);gap:12px">
3470        <div id="article-reader-meta" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap"></div>
3471        <button onclick="closeArticleModal()" style="background:none;border:none;color:var(--muted);font-size:1.3rem;cursor:pointer;line-height:1;padding:2px 6px">✕</button>
3472      </div>
3473      <div id="article-reader-title" style="padding:16px 20px 0;font-weight:700;font-size:1rem;color:var(--text);line-height:1.4"></div>
3474      <div id="article-reader-body" style="padding:16px 20px 20px;overflow-x:auto"></div>
3475      <div id="article-reader-actions" style="display:flex;gap:8px;flex-wrap:wrap;padding:14px 20px;border-top:1px solid var(--border)"></div>
3476    </div>
3477  </div>
3478</div>
3479
3480<!-- ── FACTOZ GRAPHS ── -->
3481<div class="page" id="page-graphs">
3482  <div class="page-header">
3483    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/pgirtdfe.json" trigger="hover" stroke="bold" state="hover-partial-roll" style="width:28px;height:28px"></lord-icon>Factoz Graphs</div>
3484    <div class="page-sub">Interactive charts — preview, download, or save to your Factoz gallery</div>
3485  </div>
3486  <div id="graphs-filters" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px"></div>
3487  <div id="graphs-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:14px"></div>
3488</div>
3489
3490<!-- ── LIVE CHARTS ──────────────────────────────────────────────────────── -->
3491<div class="page" id="page-charts">
3492  <div class="page-header">
3493    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/mwhabkof.json" trigger="hover" style="width:28px;height:28px"></lord-icon>Live Charts</div>
3494    <div class="page-sub">Interactive advanced charts — powered by TradingView</div>
3495  </div>
3496
3497  <!-- TV Advanced Chart -->
3498  <div id="tv-chart-wrap" style="width:100%;height:700px;margin-bottom:32px;background:#0b132b;border:1px solid #202e56;border-radius:10px;overflow:hidden"></div>
3499
3500  <!-- Pine Script Library -->
3501  <div style="border-top:1px solid #21262d;padding-top:28px;margin-top:8px">
3502    <div style="margin-bottom:20px">
3503      <div style="font-size:.6rem;font-family:var(--mono);letter-spacing:3px;color:var(--text2);margin-bottom:6px">PINE SCRIPT LIBRARY</div>
3504      <div style="font-size:1.1rem;font-weight:700;color:var(--text)">16 curated indicators — ready to paste into TradingView</div>
3505    </div>
3506    <div id="pine-library-wrap"></div>
3507  </div>
3508
3509</div>
3510
3511<!-- ── GALLERY ── -->
3512<div class="page" id="page-gallery">
3513  <div class="page-header">
3514    <div class="page-title"><lord-icon src="https://cdn.lordicon.com/fcwmlcjj.json" trigger="hover" style="width:28px;height:28px"></lord-icon>Gallery</div>
3515    <div class="page-sub">All your saved images — carousels, collages, AI-generated, uploads</div>
3516  </div>
3517  <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap">
3518    <label style="cursor:pointer" class="btn btn-ghost">
3519      📤 Upload Image
3520      <input type="file" accept="image/*" multiple style="display:none" onchange="uploadGalleryFiles(this)">
3521    </label>
3522    <span id="gallery-upload-status" style="font-size:.7rem;color:var(--muted)"></span>
3523  </div>
3524  <div style="display:flex;gap:7px;margin-bottom:16px;flex-wrap:wrap">
3525    <button class="btn gallery-filter active-filter" data-type="" onclick="loadGallery('')">All</button>
3526    <button class="btn btn-ghost gallery-filter" data-type="carousel_pil" onclick="loadGallery('carousel_pil')">🎠 Carousels</button>
3527    <button class="btn btn-ghost gallery-filter" data-type="collage" onclick="loadGallery('collage')">🧩 Collages</button>
3528    <button class="btn btn-ghost gallery-filter" data-type="ai_image" onclick="loadGallery('ai_image')">🤖 AI Images</button>
3529    <button class="btn btn-ghost gallery-filter" data-type="design_studio" onclick="loadGallery('design_studio')">✏️ Studio</button>
3530    <button class="btn btn-ghost gallery-filter" data-type="upload" onclick="loadGallery('upload')">📤 Uploads</button>
3531  </div>
3532  <div id="gallery-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px"></div>
3533  <div id="gallery-load-more" style="text-align:center;margin-top:16px;display:none">
3534    <button class="btn btn-ghost" onclick="loadGalleryMore()">Load more</button>
3535  </div>
3536</div>
3537
3538</div><!-- /main -->
3539
3540<!-- Gallery lightbox modal -->
3541<div id="gallery-lightbox" onclick="if(event.target===this)closeGalleryLightbox()"
3542  style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:1100;
3543         align-items:center;justify-content:center;padding:16px">
3544  <div style="position:relative;display:flex;flex-direction:column;align-items:center;max-width:900px;width:100%;max-height:96vh">
3545    <!-- top bar -->
3546    <div style="display:flex;justify-content:space-between;align-items:center;width:100%;margin-bottom:10px">
3547      <div style="display:flex;gap:8px;align-items:center">
3548        <span id="lb-type-label" style="font-size:.68rem;color:rgba(255,255,255,.5);font-family:var(--mono);letter-spacing:1px"></span>
3549        <span id="lb-slide-counter" style="font-size:.68rem;color:rgba(255,255,255,.4);font-family:var(--mono)"></span>
3550      </div>
3551      <div style="display:flex;gap:8px">
3552        <button onclick="downloadGalleryImage()" style="background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:6px;padding:5px 14px;cursor:pointer;font-size:.75rem">⬇ Download</button>
3553        <button onclick="closeGalleryLightbox()" style="background:none;border:none;color:rgba(255,255,255,.6);cursor:pointer;font-size:1.4rem;line-height:1;padding:0 4px">✕</button>
3554      </div>
3555    </div>
3556    <!-- image -->
3557    <img id="lb-img" src="" style="max-width:100%;max-height:78vh;object-fit:contain;border-radius:8px;display:block">
3558    <!-- slide nav (multi-slide only) -->
3559    <div id="lb-nav" style="display:none;align-items:center;gap:16px;margin-top:12px">
3560      <button onclick="lbNav(-1)" style="background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:6px;padding:6px 16px;cursor:pointer;font-size:.9rem">‹ Prev</button>
3561      <div id="lb-dots" style="display:flex;gap:6px"></div>
3562      <button onclick="lbNav(1)" style="background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:6px;padding:6px 16px;cursor:pointer;font-size:.9rem">Next ›</button>
3563    </div>
3564  </div>
3565</div>
3566
3567<!-- Earnings detail modal -->
3568<div id="earnings-detail-modal" onclick="if(event.target===this)this.style.display='none'"
3569  style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:1000;
3570         align-items:center;justify-content:center;padding:20px">
3571  <div style="background:var(--bg2);border-radius:12px;width:100%;max-width:520px;max-height:88vh;
3572              display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--border)">
3573    <div style="display:flex;justify-content:space-between;align-items:center;
3574                padding:14px 18px;border-bottom:1px solid var(--border);flex-shrink:0">
3575      <div id="earnings-detail-title" style="font-weight:700;font-size:1.1rem"></div>
3576      <button onclick="$('earnings-detail-modal').style.display='none'"
3577        style="background:none;border:none;cursor:pointer;font-size:1.2rem;color:var(--muted)">✕</button>
3578    </div>
3579    <div id="earnings-detail-body" style="padding:16px 18px;overflow-y:auto;flex:1"></div>
3580  </div>
3581</div>
3582
3583<!-- Graph preview modal -->
3584<div id="graph-modal-overlay" onclick="closeGraphModal(event)"
3585  style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:1000;
3586         align-items:center;justify-content:center;padding:20px">
3587  <div style="background:var(--card);border-radius:12px;width:100%;max-width:900px;
3588              height:90vh;overflow:hidden;display:flex;flex-direction:column">
3589    <div style="display:flex;justify-content:space-between;align-items:center;
3590                padding:14px 18px;border-bottom:1px solid var(--border)">
3591      <div id="graph-modal-title" style="font-weight:700;font-size:.9rem"></div>
3592      <div style="display:flex;gap:8px;align-items:center">
3593        <button id="graph-dl-btn" onclick="downloadGraphPNG()" class="btn btn-ghost"
3594          style="font-size:.72rem">↓ Download PNG</button>
3595        <button onclick="closeGraphModal(null)"
3596          style="background:none;border:none;cursor:pointer;font-size:1.2rem;color:var(--muted)">✕</button>
3597      </div>
3598    </div>
3599    <iframe id="graph-modal-iframe" src="" loading="lazy"
3600      style="width:100%;border:none;flex:1;min-height:0"></iframe>
3601    <div style="padding:6px 18px;font-size:.65rem;color:var(--muted);border-top:1px solid var(--border)">
3602      Source: <a id="graph-modal-source" href="#" target="_blank" style="color:var(--accent)">Our World in Data</a>
3603      · Data is interactive — explore countries, timeframes, and chart types above
3604    </div>
3605  </div>
3606</div>
3607
3608<div id="toast"></div>
3609
3610
3611<script>
3612const API='';
3613let TOKEN=localStorage.getItem('fz_token')||'';
3614let USER={};
3615
3616// ── I18N ───────────────────────────────
3617// Default locale is Spanish app-wide. Priority: logged-in user's saved
3618// locale (synced in onLogin) > localStorage (anonymous/pre-login) > 'es'.
3619// Never derived from navigator.language.
3620let LOCALE=localStorage.getItem('fz_locale')||'es';
3621const _I18N_RESOURCES={};
3622async function _loadLocaleFile(lng){
3623  if(_I18N_RESOURCES[lng])return _I18N_RESOURCES[lng];
3624  try{
3625    const r=await fetch('/i18n/'+lng+'.json');
3626    const j=await r.json();
3627    _I18N_RESOURCES[lng]=j;
3628    return j;
3629  }catch(e){return {};}
3630}
3631async function initI18n(){
3632  const [es,en]=await Promise.all([_loadLocaleFile('es'),_loadLocaleFile('en')]);
3633  await i18next.init({
3634    lng:LOCALE,
3635    fallbackLng:'es',
3636    resources:{es:{translation:es},en:{translation:en}},
3637  });
3638  document.documentElement.lang=LOCALE;
3639  applyStaticTranslations();
3640  _updateLangToggle();
3641}
3642function t(key){return (window.i18next&&i18next.isInitialized)?i18next.t(key):key;}
3643function applyStaticTranslations(){
3644  document.querySelectorAll('[data-i18n]').forEach(el=>{el.textContent=t(el.getAttribute('data-i18n'));});
3645  document.querySelectorAll('[data-i18n-placeholder]').forEach(el=>{el.setAttribute('placeholder',t(el.getAttribute('data-i18n-placeholder')));});
3646  if(typeof buildNav==='function'&&USER&&USER.tier)buildNav(USER.tier);
3647}
3648async function setLocale(lng){
3649  if(lng!=='es'&&lng!=='en')return;
3650  LOCALE=lng;
3651  localStorage.setItem('fz_locale',lng);
3652  await i18next.changeLanguage(lng);
3653  document.documentElement.lang=lng;
3654  applyStaticTranslations();
3655  _updateLangToggle();
3656  if(TOKEN){
3657    try{await api('PUT','/api/auth/locale',{locale:lng});}catch(e){}
3658  }
3659}
3660function _syncLocaleFromUser(userLocale){
3661  if(!userLocale||userLocale===LOCALE){_updateLangToggle();return;}
3662  LOCALE=userLocale;
3663  localStorage.setItem('fz_locale',userLocale);
3664  i18next.changeLanguage(userLocale).then(()=>{
3665    document.documentElement.lang=userLocale;
3666    applyStaticTranslations();
3667    _updateLangToggle();
3668  });
3669}
3670function _updateLangToggle(){
3671  document.querySelectorAll('.lang-toggle-btn').forEach(b=>{
3672    b.classList.toggle('active',b.getAttribute('data-lang')===LOCALE);
3673  });
3674}
3675const _I18N_READY=initI18n();
3676
3677// ── XP / LEVEL WIDGET ────────────────────────────
3678function _updateXpWidget(d){
3679  const widget=$('xp-widget');
3680  if(!widget)return;
3681  if(USER.tier==='admin'||USER.tier==='super_admin'){widget.style.display='none';return;}
3682  widget.style.display='block';
3683  $('xp-level-label').textContent=t('level.'+d.level)||d.level;
3684  $('xp-quiz-status').textContent=t('xp.quiz_today').replace('{count}',d.due?'0':'1');
3685  const pct=d.xp_needed?Math.min(100,Math.round(d.xp_total/d.xp_needed*100)):100;
3686  $('xp-progress-bar').style.width=pct+'%';
3687  $('xp-total-label').textContent=d.next_level?`${d.xp_total} / ${d.xp_needed} XP`:`${d.xp_total} XP · ${t('xp.max')}`;
3688  $('xp-streak-label').textContent=d.streak_days>0?`🔥 ${d.streak_days}`:'';
3689}
3690
3691// ── DAILY QUIZ ─────────────────────────────────
3692let _quizCurrent=null;   // {quiz_id, title, level, lesson_content, unlocks_hint, questions:[...]}
3693let _quizAnswers={};     // question_id -> 'a'|'b'|'c'|'d'
3694
3695function _quizEsc(s){
3696  const d=document.createElement('div');
3697  d.textContent=s==null?'':String(s);
3698  return d.innerHTML;
3699}
3700
3701async function checkQuizDue(){
3702  if(!TOKEN)return;
3703  if(USER.tier==='admin'||USER.tier==='super_admin')return;   // never nag admin accounts with the leveling popup
3704  try{
3705    const r=await api('GET','/api/quizzes/due-today');
3706    if(!r.ok){console.warn('checkQuizDue: due-today returned',r.status);return;}
3707    const d=await r.json();
3708    _updateXpWidget(d);
3709    if(d.due)setTimeout(openQuizModal,1200);
3710  }catch(e){console.error('checkQuizDue failed:',e);}
3711}
3712
3713async function openQuizModal(){
3714  try{
3715    const r=await api('GET','/api/quizzes/next');
3716    if(!r.ok){console.warn('openQuizModal: /next returned',r.status,'- no quiz seeded for this level/locale?');return;}
3717    const d=await r.json();
3718    _quizCurrent=d;
3719    _quizAnswers={};
3720    $('quiz-modal-title').textContent=d.title||'';
3721    $('quiz-topic-badge').textContent=t('level.'+d.level)||d.level;
3722    $('quiz-lesson-content').innerHTML=_quizEsc(d.lesson_content||'').replace(/\n/g,'<br>');
3723    const hintEl=$('quiz-unlocks-hint');
3724    if(d.unlocks_hint){
3725      hintEl.innerHTML=`🔓 ${t('quiz.unlocks_prefix')} <b>${_quizEsc(d.unlocks_hint)}</b>`;
3726      hintEl.style.display='block';
3727    }else hintEl.style.display='none';
3728    $('quiz-lesson-view').style.display='block';
3729    $('quiz-questions-view').style.display='none';
3730    $('quiz-results-view').style.display='none';
3731    $('quiz-modal-overlay').classList.add('show');
3732  }catch(e){console.error('openQuizModal failed:',e);}
3733}
3734
3735function closeQuizModal(){
3736  $('quiz-modal-overlay').classList.remove('show');
3737}
3738
3739function _quizStartQuestions(){
3740  $('quiz-lesson-view').style.display='none';
3741  $('quiz-questions-view').style.display='block';
3742  const list=$('quiz-questions-list');
3743  list.innerHTML=_quizCurrent.questions.map((q,i)=>`
3744    <div class="quiz-question">
3745      <div class="quiz-question-text">${i+1}. ${_quizEsc(q.question_text)}</div>
3746      <div class="quiz-choices">
3747        ${['a','b','c','d'].map(letter=>`
3748          <div class="quiz-choice" data-qid="${q.id}" data-letter="${letter}" onclick="_quizSelectChoice('${q.id}','${letter}')">
3749            <span class="quiz-choice-letter">${letter.toUpperCase()}</span><span>${_quizEsc(q.choices[letter])}</span>
3750          </div>
3751        `).join('')}
3752      </div>
3753    </div>
3754  `).join('');
3755}
3756
3757function _quizSelectChoice(qid,letter){
3758  _quizAnswers[qid]=letter;
3759  document.querySelectorAll(`.quiz-choice[data-qid="${qid}"]`).forEach(el=>{
3760    el.classList.toggle('selected',el.getAttribute('data-letter')===letter);
3761  });
3762}
3763
3764async function submitQuizAnswers(){
3765  if(!_quizCurrent)return;
3766  const total=_quizCurrent.questions.length;
3767  if(Object.keys(_quizAnswers).length<total){
3768    toast(t('quiz.answer_all'),'err');
3769    return;
3770  }
3771  try{
3772    const r=await api('POST',`/api/quizzes/${_quizCurrent.quiz_id}/submit`,{answers:_quizAnswers});
3773    const d=await r.json();
3774    if(!r.ok){
3775      toast(d.detail||t('quiz.submit_error'),'err');
3776      return;
3777    }
3778    _renderQuizResults(d);
3779  }catch(e){
3780    toast(t('quiz.submit_error'),'err');
3781  }
3782}
3783
3784function _renderQuizResults(d){
3785  $('quiz-questions-view').style.display='none';
3786  $('quiz-results-view').style.display='block';
3787  const qMap={};
3788  _quizCurrent.questions.forEach(q=>{qMap[q.id]=q;});
3789
3790  let html=`<div class="quiz-score">⭐ ${d.stars_earned} / ${d.total_questions}</div>`;
3791  html+=`<div class="quiz-stats-row">
3792    <div><span class="quiz-stat-num">${d.xp_total}</span><span class="quiz-stat-label">${t('quiz.xp_total')}</span></div>
3793    <div><span class="quiz-stat-num">🔥 ${d.streak_days}</span><span class="quiz-stat-label">${t('quiz.streak_days')}</span></div>
3794  </div>`;
3795  if(d.leveled_up){
3796    html+=`<div class="quiz-levelup">🎉 ${t('quiz.leveled_up_to')} <b>${t('level.'+d.level)}</b>!</div>`;
3797  }
3798  html+=`<div class="quiz-review">`+d.graded.map((g,i)=>{
3799    const q=qMap[g.question_id]||{question_text:'',choices:{}};
3800    return `<div class="quiz-review-item ${g.correct?'correct':'incorrect'}">
3801      <div class="quiz-review-q">${i+1}. ${_quizEsc(q.question_text)}</div>
3802      <div class="quiz-review-answer">${g.correct?'✅':'❌'} ${_quizEsc((q.choices||{})[g.correct_choice]||'')}</div>
3803      ${g.explanation?`<div class="quiz-review-explanation">${_quizEsc(g.explanation)}</div>`:''}
3804    </div>`;
3805  }).join('')+`</div>`;
3806
3807  $('quiz-results-content').innerHTML=html;
3808  _updateXpWidget({...d,due:false});
3809  if(d.leveled_up){
3810    toast(t('quiz.leveled_up_toast'),'ok');
3811    USER.level=d.level;
3812    _justUnlockedRank=LEVEL_RANK[d.level];
3813    buildNav(USER.tier);
3814    setTimeout(()=>{_justUnlockedRank=null;buildNav(USER.tier);},5000);
3815  }
3816}
3817
3818// ── WATCHLIST ──────────────────────────────────
3819async function loadWatchlist(){
3820  const grid=$('watchlist-grid');
3821  if(!grid)return;
3822  try{
3823    const res=await api('GET','/api/watchlist');
3824    if(!res.ok){grid.innerHTML='';return;}
3825    const items=await res.json();
3826    if(!items.length){
3827      grid.innerHTML=`<p style="font-size:.8rem;color:var(--text2);grid-column:1/-1">${t('watchlist.empty')}</p>`;
3828      return;
3829    }
3830    grid.innerHTML=items.map(i=>`
3831      <div style="background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:12px 14px;display:flex;align-items:center;justify-content:space-between">
3832        <div>
3833          <div style="font-weight:700;font-size:.85rem">${_quizEsc(i.ticker)}</div>
3834          <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono)">${new Date(i.added_at).toLocaleDateString()}</div>
3835        </div>
3836        <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px;color:var(--red)" onclick="removeFromWatchlist('${i.ticker}')">✕</button>
3837      </div>`).join('');
3838  }catch(e){grid.innerHTML='';}
3839}
3840async function addToWatchlist(){
3841  const input=$('wl-ticker-input');
3842  const ticker=(input.value||'').trim().toUpperCase();
3843  if(!ticker)return;
3844  try{
3845    const res=await api('POST','/api/watchlist',{ticker});
3846    const d=await res.json();
3847    if(res.ok){input.value='';toast(t('watchlist.added'),'ok');loadWatchlist();}
3848    else{toast(d.detail||t('watchlist.add_error'),'err');}
3849  }catch(e){toast(t('watchlist.add_error'),'err');}
3850}
3851async function removeFromWatchlist(ticker){
3852  try{
3853    const res=await api('DELETE',`/api/watchlist/${ticker}`);
3854    if(res.ok){toast(t('watchlist.removed'),'ok');loadWatchlist();}
3855  }catch(e){}
3856}
3857
3858let lastScanCharts=[];
3859let lastCollageBlob=null;
3860let _modalChart=null;
3861let _selectedChartIdxs=new Set();
3862let lastCarouselSlides=[];
3863let selectedTemplate='factoz';
3864let selectedFiles=new Set();
3865let editingClientId=null;
3866
3867const $=id=>document.getElementById(id);
3868function hdr(extra={}){return{'Authorization':`Bearer ${TOKEN}`,...extra}}
3869async function api(method,path,body=null,isForm=false){
3870  const opts={method,headers:hdr(isForm?{}:{'Content-Type':'application/json'})};
3871  if(body)opts.body=isForm?body:JSON.stringify(body);
3872  return fetch(API+path,opts);
3873}
3874// ── LIGHTBOX ──────────────────────────────────
3875let _lbUrls=[],_lbIdx=0;
3876function openLightbox(urls,idx){_lbUrls=urls;_lbIdx=idx||0;$('lightbox').style.display='flex';_lbRender();}
3877function _lbRender(){const u=_lbUrls[_lbIdx];$('lb-img').src=u;$('lb-dl').href=u;$('lb-caption').textContent=_lbUrls.length>1?`${_lbIdx+1} / ${_lbUrls.length}`:'';}
3878function lbPrev(){_lbIdx=(_lbIdx-1+_lbUrls.length)%_lbUrls.length;_lbRender();}
3879function lbNext(){_lbIdx=(_lbIdx+1)%_lbUrls.length;_lbRender();}
3880function closeLightbox(){$('lightbox').style.display='none';const tvEl=$('lb-tv-link');if(tvEl)tvEl.style.display='none';}
3881document.addEventListener('keydown',e=>{if($('lightbox').style.display==='none')return;if(e.key==='Escape')closeLightbox();if(e.key==='ArrowLeft')lbPrev();if(e.key==='ArrowRight')lbNext();});
3882
3883function toast(msg,type=''){
3884  const el=$('toast');el.textContent=msg;
3885  el.className='show '+(type==='ok'?'ok':type==='err'?'err':'');
3886  setTimeout(()=>el.className='',3200);
3887}
3888function b64toBlob(b64,mime='image/png'){
3889  const b=atob(b64),ab=new ArrayBuffer(b.length),ia=new Uint8Array(ab);
3890  for(let i=0;i<b.length;i++)ia[i]=b.charCodeAt(i);
3891  return new Blob([ab],{type:mime});
3892}
3893function updateClock(){
3894  const n=new Date();
3895  $('clock').textContent=n.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})+'  '+n.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'});
3896}
3897setInterval(updateClock,1000);updateClock();
3898
3899// ── TIER-BASED NAVIGATION ────────────────────
3900let NAV_MODE=localStorage.getItem('nav_mode')||'simple';
3901
3902function toggleNavMode(){
3903  NAV_MODE=NAV_MODE==='simple'?'power':'simple';
3904  localStorage.setItem('nav_mode',NAV_MODE);
3905  if(USER&&USER.tier)buildNav(USER.tier);
3906  _updateNavModeBtn();
3907}
3908
3909function _updateNavModeBtn(){
3910  const btn=document.getElementById('nav-mode-btn');
3911  if(!btn)return;
3912  btn.textContent=NAV_MODE==='simple'?'◉ simple mode':'○ power mode';
3913  btn.title=NAV_MODE==='simple'?'Switch to power mode (show all features)':'Switch to simple mode (focus view)';
3914}
3915
3916// ── LEVEL-GATED NAV ─────────────────────────────
3917// Frontend mirror of core/levels.py::effective_level_rank - only routes whose
3918// BACKEND has actually been migrated to require_level() should use this for
3919// nav lock/unlock (currently: Polymarket, FRED/Macro, Trading). Everything
3920// else stays tier-gated until its own backend migrates, or the nav would
3921// show a green light on something the API still 403s.
3922const LEVELS=['beginner','intermediate','advanced','expert'];
3923const LEVEL_RANK={beginner:0,intermediate:1,advanced:2,expert:3};
3924const TIER_TO_LEVEL={free:'beginner',creator:'intermediate',pro:'advanced',stellar:'expert',enterprise_api:'expert',admin:'expert',super_admin:'expert'};
3925function _effectiveLevelRank(user){
3926  const earned=LEVEL_RANK[user.level]??0;
3927  const floor=LEVEL_RANK[TIER_TO_LEVEL[user.tier]]??0;
3928  const bought=user.skip_line_purchased?LEVEL_RANK.expert:0;
3929  return Math.max(earned,floor,bought);
3930}
3931let _justUnlockedRank=null;   // set briefly right after a level-up, drives the nav shine animation
3932function _lockedNavClick(){
3933  toast(t('nav.locked_hint'),'');
3934}
3935function _levelNavItem(pageId,icon,label,minRank,unlockedNow,extraHtml,onclickPrefix){
3936  const locked=!unlockedNow;
3937  const shine=unlockedNow && _justUnlockedRank===minRank;
3938  const cls='nav-item'+(locked?' nav-item-locked':'')+(shine?' nav-item-shine':'');
3939  const click=locked?'_lockedNavClick()':`${onclickPrefix||''}go('${pageId}')`;
3940  const lock=locked?'<span class="nav-lock">🔒</span>':'';
3941  return `<div class="${cls}" onclick="${click}
3941"><span class="nav-icon"><i data-feather="${icon}"></i></span>${label}${locked?'':(extraHtml||'')}${lock}</div>`;
3942}
3943
3944function buildNav(tier){
3945  const isAdmin    = tier==='admin' || tier==='super_admin';
3946  const hasStellar = tier==='stellar' || isAdmin;
3947  const hasPro     = tier==='pro' || tier==='stellar' || tier==='enterprise_api' || isAdmin;
3948  const hasCreator = tier==='creator' || hasPro;
3949  const hasFree    = tier==='free';
3950  const canPost    = tier==='pro' || tier==='stellar' || tier==='enterprise_api' || isAdmin;
3951  const isPower    = NAV_MODE==='power';
3952  const advUnlocked = _effectiveLevelRank(USER) >= LEVEL_RANK.advanced;
3953  const intUnlocked = _effectiveLevelRank(USER) >= LEVEL_RANK.intermediate;
3954  // Financials keeps "pro" tier grandfathered server-side (it had this before
3955  // Expert-level gating existed) - the nav must mirror that or pro-tier users
3956  // would see it locked despite the API letting them through. See
3957  // routes/financials.py::_require_financials_access.
3958  const expUnlocked = _effectiveLevelRank(USER) >= LEVEL_RANK.expert || tier==='pro';
3959  const nav=$('nav-menu');
3960  let html='';
3961
3962  // Top items
3963  html+=`<div class="nav-item" onclick="go('guide')"><span class="nav-icon"><i data-feather="book-open"></i></span>${t('nav.getting_started')}</div>
3964  <div class="nav-item active" onclick="go('overview')"><span class="nav-icon"><i data-feather="home"></i></span>${t('nav.overview')}</div>`;
3965
3966  // SIGNALS — core alert engine
3967  html+=`<div class="nav-section" data-section="signals" onclick="toggleNav('signals')">${t('nav.section_signals')}</div>
3968  <div class="nav-section-items" id="ns-signals">
3969  <div class="nav-item" onclick="go('scanner')"><span class="nav-icon"><i data-feather="bar-chart-2"></i></span>${t('nav.scanner')}<span id="scanner-badge" class="nav-badge badge-pulse" style="display:none">!</span></div>`;
3970  html+=_levelNavItem('options','zap',t('nav.options_flow'),LEVEL_RANK.intermediate,intUnlocked);
3971  html+=_levelNavItem('polymarket','target',t('nav.polymarket'),LEVEL_RANK.advanced,advUnlocked);
3972  html+=_levelNavItem('macro','globe',t('nav.macro'),LEVEL_RANK.advanced,advUnlocked);
3973  html+=`</div>`;
3974
3975  // MARKETS — market data
3976  html+=`<div class="nav-section" data-section="markets" onclick="toggleNav('markets')">${t('nav.section_markets')}</div>
3977  <div class="nav-section-items" id="ns-markets">
3978  <div class="nav-item" onclick="go('watchlist')"><span class="nav-icon"><i data-feather="eye"></i></span>${t('nav.watchlist')}</div>
3979  <div class="nav-item" onclick="go('movers')"><span class="nav-icon"><i data-feather="trending-up"></i></span>${t('nav.movers')}</div>`;
3980  html+=_levelNavItem('news','rss',t('nav.news_feed'),LEVEL_RANK.intermediate,intUnlocked,'<span id="news-badge" class="nav-badge" style="display:none"></span>',"_newsFilter='all';");
3981  html+=_levelNavItem('news','calendar',t('nav.earnings'),LEVEL_RANK.intermediate,intUnlocked,'',"filterNews('earnings');");
3982  html+=_levelNavItem('trends','search',t('nav.trends'),LEVEL_RANK.intermediate,intUnlocked);
3983  html+=`</div>`;
3984
3985  // ANALYSIS — deeper tools
3986  html+=`<div class="nav-section" data-section="analysis" onclick="toggleNav('analysis')">${t('nav.section_analysis')}</div>
3987  <div class="nav-section-items" id="ns-analysis">
3988  <div class="nav-item" onclick="go('charts')"><span class="nav-icon"><i data-feather="activity"></i></span>${t('nav.live_charts')}</div>`;
3989  if(hasCreator){
3990    html+=`<div class="nav-item" onclick="go('analyst')"><span class="nav-icon"><i data-feather="crosshair"></i></span>${t('nav.analyst_targets')}</div>`;
3991  }
3992  html+=_levelNavItem('heatmap','grid',t('nav.heatmaps'),LEVEL_RANK.advanced,advUnlocked);
3993  html+=_levelNavItem('var','shield',t('nav.portfolio_risk'),LEVEL_RANK.advanced,advUnlocked);
3994  html+=_levelNavItem('trading','bar-chart',t('nav.trading'),LEVEL_RANK.advanced,advUnlocked);
3995  html+=_levelNavItem('financials','file-text',t('nav.financials'),LEVEL_RANK.expert,expUnlocked);
3996  html+=`<div class="nav-item" onclick="go('graphs')"><span class="nav-icon"><i data-feather="pie-chart"></i></span>${t('nav.graphs')}</div>`;
3997  html+=`</div>`;
3998
3999  // ACCOUNT
4000  html+=`<div class="nav-section" data-section="account" onclick="toggleNav('account')">${t('nav.section_account')}</div>
4001  <div class="nav-section-items" id="ns-account">
4002  <div class="nav-item" onclick="go('settings')"><span class="nav-icon"><i data-feather="settings"></i></span>${t('nav.settings')}</div>
4003  </div>`;
4004
4005  // ADMIN
4006  if(isAdmin){
4007    html+=`<div class="nav-section nav-section-noc" style="color:var(--yellow);opacity:.7;cursor:default">${t('nav.section_admin')}</div>
4008    <div class="nav-item admin-item" onclick="go('admin');markAdminNotifsRead()"><span class="nav-icon"><i data-feather="zap"></i></span>${t('nav.admin_panel')}<span id="admin-notif-badge" class="nav-badge" style="display:none"></span></div>`;
4009  }
4010
4011  nav.innerHTML=html;
4012  if(window.feather) feather.replace();
4013  _restoreNavState();
4014  _updateNavModeBtn();
4015}
4016
4017// ── COLLAPSIBLE NAV ───────────────────────────
4018const _NAV_SECTIONS={
4019  news:'markets',scanner:'signals',options:'signals',movers:'markets',watchlist:'markets',forex:'markets',trends:'markets',charts:'analysis',
4020  analyst:'analysis',polymarket:'signals',macro:'signals',heatmap:'analysis',var:'analysis',financials:'analysis',graphs:'analysis',trading:'analysis',
4021  carousel:'create',collage:'create',files:'create',studio:'create',gallery:'create',designlab:'create',
4022  post:'publish',calendar:'publish',
4023  brand:'account',settings:'account',history:'account'
4024};
4025
4026function toggleNav(id){
4027  const el=$('ns-'+id);
4028  const hdr=document.querySelector('[data-section="'+id+'"]');
4029  if(!el)return;
4030  const now=el.classList.toggle('collapsed');
4031  hdr&&hdr.classList.toggle('collapsed',now);
4032  now?localStorage.setItem('nav_col_'+id,'1'):localStorage.removeItem('nav_col_'+id);
4033}
4034
4035function _restoreNavState(){
4036  ['signals','markets','analysis','account'].forEach(s=>{
4037    if(localStorage.getItem('nav_col_'+s))toggleNav(s);
4038  });
4039}
4040
4041// ── NAVIGATION ────────────────────────────────
4042let _prevPage=null;
4043function go(id){
4044  document.querySelectorAll('.page').forEach(p=>p.classList.remove('active'));
4045  document.querySelectorAll('.nav-item').forEach(n=>n.classList.remove('active'));
4046  $('page-'+id).classList.add('active');
4047  document.querySelectorAll('.nav-item').forEach(n=>{
4048    if(n.getAttribute('onclick')?.includes("'"+id+"'"))n.classList.add('active');
4049  });
4050  // Auto-expand section if collapsed
4051  const _sec=_NAV_SECTIONS[id];
4052  if(_sec){const _el=$('ns-'+_sec);if(_el&&_el.classList.contains('collapsed'))toggleNav(_sec);}
4053  if(id==='history')loadHistory('all');
4054  if(id==='scanner'){loadScanHistory();loadChartIndicators();loadMyCharts();_initProScanOpts();}
4055  if(id==='collage')loadMyCollages();
4056  if(id==='brand')loadBrand();
4057  if(id==='settings'){loadConnectedAccounts();_initDjtToggle();}
4058  if(id==='overview'){loadOverview();loadOverviewMovers();loadOptionsFlow();}
4059  if(id==='admin'){loadAdminPanel();loadAdminUsers();loadAdminNews();loadAdminActivity();loadPersons();loadAdminCredits();loadTemplatedConfigs();loadPolymarketConfig();loadTaxSessions();loadOutreachContacts();loadAdminAds();loadAdminBriefs();loadSubscriberList();loadDailyBriefConfig();loadSocialPillars();}
4060  if(id==='options')loadOptionsPage();
4061  if(id==='polymarket'){pmLoadStatus();loadPmMarkets();pmStartSSE();loadPmWatches();}
4062  if(_prevPage==='polymarket'&&id!=='polymarket')pmStopSSE();
4063  if(id==='movers')loadMovers();
4064  if(id==='watchlist')loadWatchlist();
4065  if(id==='trends')loadTrends();
4066  if(id==='gallery')loadGallery('');
4067  if(id==='macro')loadMacroScorecard();
4068  if(id==='financials'){
4069    initFinancials();
4070  }
4071  // loadHeatmap() removed — heatmap page now uses TradingView widgets
4072  if(id==='var')initVarPage();
4073  if(id==='trading')mfxInit();
4074  if(id==='calendar')renderCalendar();
4075
4076  if(id==='designlab')initDesignLab();
4077  if(id==='studio')loadStudioGallery();
4078  if(id==='graphs')renderGraphsPage();
4079  if(id==='charts') initChartsPage();
4080  else if(_lcWs){ _lcWs.close(); _lcWs = null; }
4081  if(id==='carousel'){loadRecentOutputs();updateTemplatePreview(selectedTemplate||'factoz');reloadTemplates();}
4082  if(id==='analyst')_loadAnalystTickers();
4083  if(id==='news'){
4084    // mark feed as read — store timestamp so badge resets
4085    localStorage.setItem('fz_news_seen_at', new Date().toISOString());
4086    _newsUnread=0; updateNewsBadge();
4087    if(_newsItems.length) filterNews(_newsFilter, false);
4088    loadNews();
4089  }
4090  _prevPage=id;
4091}
4092
4093// ── ANALYST TARGETS ───────────────────────────
4094let _analystTickers=[];
4095
4096async function _loadAnalystTickers(){
4097  if(_analystTickers.length)return;
4098  try{
4099    const res=await api('GET','/api/scanner/tickers');
4100    const d=await res.json();
4101    if(res.ok)_analystTickers=d.tickers||[];
4102  }catch(e){}
4103}
4104
4105function analystAutoComplete(val){
4106  const box=$('analyst-suggestions');
4107  const q=val.trim().toUpperCase();
4108  if(q.length<1){box.style.display='none';return;}
4109  const matches=_analystTickers.filter(t=>t.startsWith(q)).slice(0,10);
4110  if(!matches.length){box.style.display='none';return;}
4111  box.innerHTML=matches.map(t=>
4112    `<div onclick="analystPickTicker('${t}')"
4113      onmouseover="this.style.background='#1c2128'"
4114      onmouseout="this.style.background=''"
4115      style="padding:8px 12px;cursor:pointer;color:#e6edf3;font-size:.9em">${t}</div>`
4116  ).join('');
4117  box.style.display='block';
4118}
4119
4120function analystPickTicker(t){
4121  $('analyst-ticker-input').value=t;
4122  $('analyst-suggestions').style.display='none';
4123  getAnalystTarget();
4124}
4125
4126function analystInputKey(e){
4127  if(e.key==='Enter'){$('analyst-suggestions').style.display='none';getAnalystTarget();}
4128  if(e.key==='Escape')$('analyst-suggestions').style.display='none';
4129}
4130
4131document.addEventListener('click',e=>{
4132  if(!e.target.closest('#analyst-suggestions')&&e.target.id!=='analyst-ticker-input'){
4133    const box=$('analyst-suggestions');
4134    if(box)box.style.display='none';
4135  }
4136});
4137
4138async function getAnalystTarget(){
4139  const ticker=($('analyst-ticker-input').value||'').trim().toUpperCase();
4140  if(!ticker){toast('Enter a ticker symbol','err');return;}
4141  const res=await api('GET',`/api/scanner/analyst-target?ticker=${ticker}`);
4142  const d=await res.json();
4143  const el=$('analyst-result');
4144  if(!res.ok){el.innerHTML=`<p style="color:#f87171">${d.detail||'Lookup failed'}</p>`;return;}
4145  const upSign=d.upsidePercent>=0?'+':'';
4146  const upColor=d.upsidePercent>=0?'#00e5b4':'#f87171';
4147  const recMap={strong_buy:'Strong Buy',buy:'Buy',hold:'Hold',sell:'Sell',strong_sell:'Strong Sell'};
4148  const recColors={strong_buy:'#00e5b4',buy:'#86efac',hold:'#fbbf24',sell:'#f87171',strong_sell:'#ef4444'};
4149  const rec=d.recommendation||'';
4150  const recLabel=recMap[rec]||rec;
4151  const recColor=recColors[rec]||'#9ca3af';
4152  const actionColors={Upgrades:'#00e5b4',Initiates:'#86efac',Reiterates:'#86efac',Maintains:'#6e7681',Downgrades:'#f87171'};
4153  let gradingsHtml='';
4154  if(d.gradings&&d.gradings.length){
4155    const rows=d.gradings.map(g=>{
4156      const col=actionColors[g.action]||'#9ca3af';
4157      return `<tr>
4158        <td style="padding:6px 8px;color:#e6edf3">${g.firm}</td>
4159        <td style="padding:6px 8px;color:#e6edf3">${g.grade}</td>
4160        <td style="padding:6px 8px"><span style="color:${col};font-size:.8em">${g.action}</span></td>
4161        <td style="padding:6px 8px;color:#6e7681;font-size:.8em;white-space:nowrap">${g.date}</td>
4162      </tr>`;
4163    }).join('');
4164    gradingsHtml=`
4165      <div class="card" style="padding:0;margin-top:12px;overflow:hidden">
4166        <button onclick="var p=this.nextElementSibling;p.style.display=p.style.display==='none'?'block':'none';this.querySelector('.arr').textContent=p.style.display==='none'?'▶':'▼'"
4167          style="width:100%;background:none;border:none;color:#e6edf3;padding:14px 16px;text-align:left;cursor:pointer;display:flex;justify-content:space-between;align-items:center">
4168          <span style="font-size:.8em;text-transform:uppercase;letter-spacing:.05em;color:#6e7681">${d.gradings.length} Analyst Ratings</span>
4169          <span class="arr" style="color:#6e7681;font-size:.8em">▶</span>
4170        </button>
4171        <div style="display:none;padding:0 16px 16px">
4172          <table style="width:100%;border-collapse:collapse">
4173            <thead><tr style="color:#6e7681;font-size:.72em;text-transform:uppercase">
4174              <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Firm</th>
4175              <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Rating</th>
4176              <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Action</th>
4177              <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Date</th>
4178            </tr></thead>
4179            <tbody>${rows}</tbody>
4180          </table>
4181        </div>
4182      </div>`;
4183  }
4184  el.innerHTML=`
4185    <div class="card" style="padding:20px">
4186      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
4187        <span style="font-size:1.3em;font-weight:700">${d.ticker}</span>
4188        <span style="background:${recColor}22;color:${recColor};padding:3px 12px;border-radius:20px;font-size:.85em;font-weight:600">${recLabel}</span>
4189      </div>
4190      <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
4191        <div>
4192          <div style="color:#6e7681;font-size:.75em;text-transform:uppercase;letter-spacing:.05em">Current Price</div>
4193          <div style="font-size:1.3em;font-weight:600">$${d.currentPrice?.toFixed(2)??'—'}</div>
4194        </div>
4195        <div>
4196          <div style="color:#6e7681;font-size:.75em;text-transform:uppercase;letter-spacing:.05em">Consensus Target</div>
4197          <div style="font-size:1.3em;font-weight:600">$${d.targetMean?.toFixed(2)??'—'} <span style="color:${upColor};font-size:.8em">(${upSign}${d.upsidePercent}%)</span></div>
4198        </div>
4199      </div>
4200      <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;font-size:.85em;color:#9ca3af;border-top:1px solid #30363d;padding-top:12px">
4201        <div>High Target<br><span style="color:#e6edf3;font-weight:600">$${d.targetHigh?.toFixed(2)??'—'}</span></div>
4202        <div>Low Target<br><span style="color:#e6edf3;font-weight:600">$${d.targetLow?.toFixed(2)??'—'}</span></div>
4203        <div>Analysts<br><span style="color:#e6edf3;font-weight:600">${d.numberOfAnalysts??'—'}</span></div>
4204      </div>
4205    </div>
4206  <div id="analyst-minichart-wrap" style="margin-top:12px;border-radius:8px;overflow:hidden;height:220px"></div>`+gradingsHtml+`
4207  <div id="research-financials"        style="margin-top:8px"></div>
4208  <div id="research-insider-sentiment" style="margin-top:8px"></div>
4209  <div id="research-news"              style="margin-top:8px"></div>
4210  <div id="research-filings"           style="margin-top:8px"></div>
4211  <div id="tv-tech-analysis-wrap" style="margin-top:16px"></div>
4212  <div id="tv-financials-wrap"    style="margin-top:12px"></div>`;
4213
4214  // Inject TradingView widgets
4215  _injectTVMiniChart(d.ticker);
4216  _injectTVTechWidget(d.ticker);
4217  _injectTVFinancialsWidget(d.ticker);
4218
4219  // Fire all 4 Finnhub research calls in parallel
4220  _loadResearch(ticker);
4221}
4222
4223function _injectTVMiniChart(ticker){
4224  const wrap = $('analyst-minichart-wrap');
4225  if(!wrap) return;
4226  wrap.innerHTML = '';
4227  const sym = ticker.includes(':') ? ticker : 'NASDAQ:' + ticker;
4228
4229  const container = document.createElement('div');
4230  container.className = 'tradingview-widget-container';
4231  container.style.cssText = 'height:100%;width:100%';
4232
4233  const widget = document.createElement('div');
4234  widget.className = 'tradingview-widget-container__widget';
4235  widget.style.cssText = 'height:100%;width:100%';
4236
4237  const config = {
4238    symbol: sym,
4239    colorTheme: 'dark',
4240    isTransparent: false,
4241    width: '100%',
4242    height: 220,
4243    locale: 'en',
4244    dateRange: '1M',
4245    trendLineColor: 'rgba(0, 229, 180, 1)',
4246    underLineColor: 'rgba(0, 229, 180, 0.15)',
4247    underLineBottomColor: 'rgba(0, 229, 180, 0)',
4248    isUnderLine: true,
4249    autosize: true,
4250  };
4251
4252  const script = document.createElement('script');
4253  script.type = 'text/javascript';
4254  script.src = 'https://s3.tradingview.com/external-embedding/embed-widget-mini-symbol-overview.js';
4255  script.async = true;
4256  script.textContent = JSON.stringify(config);
4257
4258  container.appendChild(widget);
4259  container.appendChild(script);
4260  wrap.appendChild(container);
4261}
4262
4263function _injectTVTechWidget(ticker){
4264  const wrap = $('tv-tech-analysis-wrap');
4265  if(!wrap) return;
4266  wrap.innerHTML = '';
4267  const sym = ticker.includes(':') ? ticker : 'NASDAQ:' + ticker;
4268
4269  const container = document.createElement('div');
4270  container.className = 'tradingview-widget-container';
4271
4272  const widget = document.createElement('div');
4273  widget.className = 'tradingview-widget-container__widget';
4274
4275  const copy = document.createElement('div');
4276  copy.className = 'tradingview-widget-copyright';
4277  copy.style.cssText = 'font-size:.5rem;color:var(--muted);opacity:.35;text-align:right;padding:1px 4px';
4278  copy.innerHTML = `<a href="https://www.tradingview.com/" rel="noopener nofollow" target="_blank" style="color:var(--muted);text-decoration:none">Technical Analysis by TradingView</a>`;
4279
4280  const config = {
4281    colorTheme: 'dark',
4282    displayMode: 'multiple',
4283    isTransparent: true,
4284    locale: 'en',
4285    interval: '4h',
4286    disableInterval: false,
4287    width: '100%',
4288    height: 450,
4289    symbol: sym,
4290    showIntervalTabs: true,
4291  };
4292
4293  const script = document.createElement('script');
4294  script.type = 'text/javascript';
4295  script.src = 'https://s3.tradingview.com/external-embedding/embed-widget-technical-analysis.js';
4296  script.async = true;
4297  script.textContent = JSON.stringify(config);
4298
4299  container.appendChild(widget);
4300  container.appendChild(copy);
4301  container.appendChild(script);
4302  wrap.appendChild(container);
4303}
4304
4305function _injectTVFinancialsWidget(ticker){
4306  const wrap = $('tv-financials-wrap');
4307  if(!wrap) return;
4308  wrap.innerHTML = '';
4309  const sym = ticker.includes(':') ? ticker : 'NASDAQ:' + ticker;
4310
4311  const container = document.createElement('div');
4312  container.className = 'tradingview-widget-container';
4313
4314  const widget = document.createElement('div');
4315  widget.className = 'tradingview-widget-container__widget';
4316
4317  const copy = document.createElement('div');
4318  copy.style.cssText = 'font-size:.5rem;color:var(--muted);opacity:.35;text-align:right;padding:1px 4px';
4319  copy.innerHTML = `<a href="https://www.tradingview.com/" rel="noopener nofollow" target="_blank" style="color:var(--muted);text-decoration:none">Fundamentals by TradingView</a>`;
4320
4321  const config = {
4322    symbol: sym,
4323    colorTheme: 'dark',
4324    displayMode: 'adaptive',
4325    isTransparent: true,
4326    locale: 'en',
4327    width: '100%',
4328    height: 550,
4329  };
4330
4331  const script = document.createElement('script');
4332  script.type = 'text/javascript';
4333  script.src = 'https://s3.tradingview.com/external-embedding/embed-widget-financials.js';
4334  script.async = true;
4335  script.textContent = JSON.stringify(config);
4336
4337  container.appendChild(widget);
4338  container.appendChild(copy);
4339  container.appendChild(script);
4340  wrap.appendChild(container);
4341}
4342
4343function _collapsibleCard(label, innerHtml){
4344  return `<div class="card" style="padding:0;overflow:hidden">
4345    <button onclick="var p=this.nextElementSibling;p.style.display=p.style.display==='none'?'block':'none';this.querySelector('.arr').textContent=p.style.display==='none'?'▶':'▼'"
4346      style="width:100%;background:none;border:none;color:#e6edf3;padding:14px 16px;text-align:left;cursor:pointer;display:flex;justify-content:space-between;align-items:center">
4347      <span style="font-size:.8em;text-transform:uppercase;letter-spacing:.05em;color:#6e7681">${label}</span>
4348      <span class="arr" style="color:#6e7681;font-size:.8em">▶</span>
4349    </button>
4350    <div style="display:none;padding:0 16px 16px">${innerHtml}</div>
4351  </div>`;
4352}
4353
4354async function _loadResearch(ticker){
4355  const [finRes, insRes, newsRes, filRes] = await Promise.allSettled([
4356    api('GET', `/api/research/financials?ticker=${ticker}`),
4357    api('GET', `/api/research/insider-sentiment?ticker=${ticker}`),
4358    api('GET', `/api/research/news?ticker=${ticker}`),
4359    api('GET', `/api/research/filings?ticker=${ticker}`),
4360  ]);
4361
4362  // ── Financials ──
4363  const finEl=$('research-financials');
4364  if(finEl&&finRes.status==='fulfilled'&&finRes.value.ok){
4365    const fd=await finRes.value.json();
4366    const m=fd.metric||{};
4367    const fmt=(v,pre='',suf='')=>v!=null?`${pre}${typeof v==='number'?v.toFixed(2):v}${suf}`:'—';
4368    const rows=[
4369      ['P/E TTM',       fmt(m.peTTM)],
4370      ['EPS TTM',       fmt(m.epsTTM,'$')],
4371      ['Revenue TTM',   m.revenueTTM!=null?`$${(m.revenueTTM/1e9).toFixed(2)}B`:'—'],
4372      ['Gross Margin',  fmt(m.grossMarginTTM,'',' %')],
4373      ['52w High',      fmt(m['52WeekHigh'],'$')],
4374      ['52w Low',       fmt(m['52WeekLow'],'$')],
4375      ['Beta',          fmt(m.beta)],
4376    ];
4377    const grid=rows.map(([k,v])=>`<div style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #30363d22;font-size:.85em">
4378      <span style="color:#6e7681">${k}</span><span style="color:#e6edf3;font-weight:600">${v}</span></div>`).join('');
4379    finEl.innerHTML=_collapsibleCard('Basic Financials',grid);
4380  }
4381
4382  // ── Insider Sentiment ──
4383  const insEl=$('research-insider-sentiment');
4384  if(insEl&&insRes.status==='fulfilled'&&insRes.value.ok){
4385    const id=await insRes.value.json();
4386    const months=(id.data||[]).slice(-12).reverse(); // most recent first
4387    let inner;
4388    if(!months.length){
4389      inner=`<p style="color:#6e7681;font-size:.85em;margin:0">No insider sentiment data available.</p>`;
4390    }else{
4391      const rows=months.map(m=>{
4392        const mspr=m.mspr??0;
4393        const col=mspr>0?'#00e5b4':mspr<0?'#f87171':'#6e7681';
4394        const sign=mspr>0?'+':'';
4395        const chg=m.change??0;
4396        const chgSign=chg>0?'+':'';
4397        return `<tr>
4398          <td style="padding:6px 8px;color:#6e7681;font-size:.78em;white-space:nowrap">${m.year}-${String(m.month).padStart(2,'0')}</td>
4399          <td style="padding:6px 8px;font-size:.82em;font-weight:600;color:${col}">${sign}${mspr.toFixed(2)}</td>
4400          <td style="padding:6px 8px;color:#9ca3af;font-size:.78em">${chgSign}${chg.toLocaleString()}</td>
4401        </tr>`;
4402      }).join('');
4403      inner=`<p style="color:#6e7681;font-size:.75em;margin:0 0 10px">MSPR = Monthly Share Purchase Ratio. Positive = net insider buying.</p>
4404        <table style="width:100%;border-collapse:collapse">
4405          <thead><tr style="color:#6e7681;font-size:.72em;text-transform:uppercase">
4406            <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Month</th>
4407            <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">MSPR</th>
4408            <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Net Shares</th>
4409          </tr></thead>
4410          <tbody>${rows}</tbody>
4411        </table>`;
4412    }
4413    insEl.innerHTML=_collapsibleCard(`Insider Sentiment (${months.length}mo)`,inner);
4414  }
4415
4416  // ── Company News ──
4417  const newsEl=$('research-news');
4418  if(newsEl&&newsRes.status==='fulfilled'&&newsRes.value.ok){
4419    const nd=await newsRes.value.json();
4420    const articles=nd.news||[];
4421    let inner;
4422    if(!articles.length){
4423      inner=`<p style="color:#6e7681;font-size:.85em;margin:0">No news published today.</p>`;
4424    }else{
4425      inner=articles.map(a=>`<div style="padding:8px 0;border-bottom:1px solid #30363d22">
4426        <a href="${a.url}" target="_blank" rel="noopener"
4427           style="color:#e6edf3;font-size:.85em;text-decoration:none;line-height:1.4">${a.headline}</a>
4428        <div style="color:#6e7681;font-size:.75em;margin-top:3px">${a.source||''}</div>
4429      </div>`).join('');
4430    }
4431    newsEl.innerHTML=_collapsibleCard(`Company News — Today (${articles.length})`,inner);
4432  }
4433
4434  // ── SEC Filings ──
4435  const filEl=$('research-filings');
4436  if(filEl&&filRes.status==='fulfilled'&&filRes.value.ok){
4437    const fdata=await filRes.value.json();
4438    const filings=fdata.filings||[];
4439    let inner;
4440    if(!filings.length){
4441      inner=`<p style="color:#6e7681;font-size:.85em;margin:0">No recent filings found.</p>`;
4442    }else{
4443      const rows=filings.map(f=>`<tr>
4444        <td style="padding:6px 8px;color:#e6edf3;font-size:.8em;white-space:nowrap">${f.form||f.type||'—'}</td>
4445        <td style="padding:6px 8px;color:#6e7681;font-size:.78em;white-space:nowrap">${(f.filedDate||f.date||'').slice(0,10)}</td>
4446        <td style="padding:6px 8px;color:#9ca3af;font-size:.78em">${f.description||''}</td>
4447        <td style="padding:6px 8px;font-size:.78em">${f.filingUrl||f.reportUrl?`<a href="${f.filingUrl||f.reportUrl}" target="_blank" rel="noopener" style="color:#58a6ff">View</a>`:'—'}</td>
4448      </tr>`).join('');
4449      inner=`<table style="width:100%;border-collapse:collapse">
4450        <thead><tr style="color:#6e7681;font-size:.72em;text-transform:uppercase">
4451          <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Type</th>
4452          <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Filed</th>
4453          <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Description</th>
4454          <th style="text-align:left;padding:4px 8px;border-bottom:1px solid #30363d">Link</th>
4455        </tr></thead>
4456        <tbody>${rows}</tbody>
4457      </table>`;
4458    }
4459    filEl.innerHTML=_collapsibleCard(`SEC Filings (${filings.length})`,inner);
4460  }
4461}
4462
4463// ── AUTH ──────────────────────────────────────
4464function authTab(tab){
4465  document.querySelectorAll('.auth-tab').forEach(t=>t.classList.toggle('active',t.getAttribute('data-tab')===tab));
4466  $('quick-form').style.display=tab==='quick'?'':'none';
4467  $('login-form').style.display=tab==='login'?'':'none';
4468  $('register-form').style.display=tab==='register'?'':'none';
4469  $('auth-err').textContent='';$('auth-err').style.color='';
4470}
4471async function doQuickEntry(){
4472  const email=$('q-email').value;
4473  if(!email){$('auth-err').textContent=t('auth.quick_email_required');return;}
4474  try{
4475    const res=await fetch(API+'/api/auth/quick-entry',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,locale:LOCALE})});
4476    const d=await res.json();
4477    if(res.ok){await _completeLogin(d.token,d);}
4478    else if(res.status===409){
4479      $('auth-err').textContent=t('auth.quick_account_exists');
4480      authTab('login');
4481      $('l-email').value=email;
4482    }else{
4483      $('auth-err').textContent=d.detail||t('auth.connection_error');
4484    }
4485  }catch(e){$('auth-err').textContent=t('auth.connection_error');}
4486}
4487async function doLogin(){
4488  const email=$('l-email').value,password=$('l-pass').value;
4489  try{
4490    const res=await fetch(API+'/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,password})});
4491    const d=await res.json();
4492    if(res.ok){await _completeLogin(d.token,d);}
4493    else if(res.status===403){
4494      // Trial expired / pending payment — show PayPal checkout
4495      $('auth-err').innerHTML=`
4496        <div style="margin-top:12px;padding:16px;background:#0d1117;border:1px solid #222;border-radius:10px;text-align:center">
4497          <div style="color:#8b949e;font-size:.72rem;margin-bottom:4px">Your trial has ended — subscribe to continue</div>
4498          <div style="color:#555;font-size:.62rem;margin-bottom:14px">Pro — $49/mo · PayPal, Visa, Mastercard</div>
4499          <div id="paypal-login-pro" class="paypal-hosted-btn-container"></div>
4500          <div style="color:#555;font-size:.62rem;margin-top:10px">Secure checkout via PayPal</div>
4501        </div>`;
4502      setTimeout(_initPayPalButton, 100);
4503    }
4504    else{
4505      // Make email addresses in error messages clickable (e.g. paywall contact)
4506      const msg=d.detail||'Login failed';
4507      $('auth-err').innerHTML=msg.replace(/([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})/g,
4508        '<a href="mailto:$1" style="color:var(--teal)">$1</a>');
4509    }
4510  }catch(e){$('auth-err').textContent='Connection error';}
4511}
4512async function doRegister(){
4513  const name=$('r-name').value,email=$('r-email').value,password=$('r-pass').value;
4514  try{
4515    const res=await fetch(API+'/api/auth/register',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,email,password,locale:LOCALE})});
4516    const d=await res.json();
4517    if(res.ok){
4518      $('auth-err').style.color='var(--green)';
4519      $('auth-err').textContent=t('auth.register_success');
4520    }else $('auth-err').textContent=d.detail||t('auth.register_failed');
4521  }catch(e){$('auth-err').textContent=t('auth.connection_error');}
4522}
4523async function _completeLogin(token,fallback){
4524  TOKEN=token;
4525  localStorage.setItem('fz_token',TOKEN);
4526  try{
4527    const meRes=await api('GET','/api/auth/me');
4528    if(meRes.ok){onLogin(await meRes.json());return;}
4529  }catch(e){}
4530  onLogin(fallback);   // /me failed - degrade to the login response shape rather than blocking entry
4531}
4532function onLogin(d){
4533  USER=d;
4534  _syncLocaleFromUser(d.locale);
4535  $('auth').style.display='none';
4536  $('user-tag').textContent=d.email||'';
4537  const tierLabels={'free':'Free','creator':'Creator','pro':'Pro','stellar':'Stellar','enterprise_api':'API Client','admin':'Admin','super_admin':'Super Admin'};
4538  const tierLabel=(tierLabels[d.tier]||d.tier||'Creator').toUpperCase();
4539  $('tier-tag').textContent=tierLabel;
4540  const _isAdmin=d.tier==='admin'||d.tier==='super_admin';
4541  $('tier-tag').style.color=_isAdmin?'var(--yellow)':d.tier==='pro'||d.tier==='stellar'||d.tier==='enterprise_api'?'var(--green)':'var(--teal)';
4542  $('ov-tier').textContent=tierLabel;
4543  // Hide admin-only settings fields for non-admin users
4544  if(!_isAdmin){
4545    const adminSec=document.getElementById('admin-keys-section');
4546    if(adminSec)adminSec.style.display='none';
4547  }
4548  $('ov-email').textContent=d.email||'';
4549  initExtBanner();
4550  // Show "Manage Subscription" only for paid users with a Paddle subscription
4551  // PayPal / manually-activated users see a contact link instead
4552  const subBtn=$('manage-sub-btn');
4553  if(subBtn){
4554    if(d.is_paid && (d.tier==='creator'||d.tier==='pro'||d.tier==='stellar')){
4555      if(d.paddle_customer_id){
4556        subBtn.style.display='block';
4557        subBtn.textContent='⚙ manage subscription';
4558        subBtn.onclick=manageSubscription;
4559      } else {
4560        subBtn.style.display='block';
4561        subBtn.textContent='✉ contact support';
4562        subBtn.onclick=()=>window.open('mailto:[email protected]?subject=Subscription%20Management','_blank');
4563      }
4564    } else {
4565      subBtn.style.display='none';
4566    }
4567  }
4568  // Trial countdown banner
4569  if(d.trial_ends_at){
4570    const trialEnd=new Date(d.trial_ends_at);
4571    const daysLeft=Math.ceil((trialEnd-Date.now())/(1000*60*60*24));
4572    if(daysLeft>0){
4573      const banner=document.getElementById('trial-banner');
4574      if(banner){
4575        banner.innerHTML=`Trial expires in <b>${daysLeft} day${daysLeft===1?'':'s'}</b> — <a href="https://factozcentral.com" target="_blank" style="color:inherit;font-weight:700">Upgrade to keep access</a>`;
4576        banner.style.display='flex';
4577      }
4578    }
4579  }
4580  // Free tier upgrade nudge bar
4581  const freeBar=document.getElementById('free-upgrade-bar');
4582  if(freeBar) freeBar.style.display=d.tier==='free'?'flex':'none';
4583  buildNav(d.tier||'creator');
4584  try{_updateScannerBadge();}catch(e){}
4585  loadAds();
4586  go('overview');
4587  loadOverviewMovers();
4588  setTimeout(loadOverview, 3000);
4589  setTimeout(loadOverview, 8000);
4590  // start polling unread badge every 5 min
4591  pollNewsUnread();
4592  setInterval(pollNewsUnread, 5*60*1000);
4593  pollAdminNotifications();
4594  setInterval(pollAdminNotifications, 60*1000);
4595  checkQuizDue();
4596}
4597function logout(){TOKEN='';localStorage.removeItem('fz_token');USER={};$('auth').style.display='flex';}
4598function startCreatorTrial(){
4599  // No free trial — redirect to plans page
4600  showPage('page-plans');
4601}
4602async function continueForFree(){
4603  if(!TOKEN){return;}
4604  try{
4605    const r=await fetch('/api/auth/downgrade-to-free',{method:'POST',headers:{'Authorization':'Bearer '+TOKEN}});
4606    if(r.ok){toast('Welcome to the Free plan','ok');location.reload();}
4607    else{const d=await r.json();toast(d.detail||'Error','err');}
4608  }catch(e){toast('Connection error','err');}
4609}
4610function showTrialExpiredOverlay(){
4611  const overlay=$('trial-expired-overlay');
4612  if(overlay)overlay.style.display='flex';
4613  $('auth').style.display='none';
4614  $('main').style.display='none';
4615}
4616
4617async function tryAutoLogin(){
4618  if(!TOKEN)return;
4619  try{
4620    const res=await api('GET','/api/auth/me');
4621    if(res.ok){const d=await res.json();onLogin(d);}
4622    else if(res.status===403){
4623      const d=await res.json();
4624      if(d.detail==='trial_expired'){showTrialExpiredOverlay();}
4625      else{TOKEN='';localStorage.removeItem('fz_token');}
4626    }
4627    else{TOKEN='';localStorage.removeItem('fz_token');}
4628  }catch(e){}
4629}
4630_I18N_READY.then(tryAutoLogin);
4631
4632// ── EMAIL VERIFICATION REDIRECT HANDLING ──────
4633(function(){
4634  const vp=new URLSearchParams(location.search).get('verified');
4635  if(!vp)return;
4636  history.replaceState(null,'',location.pathname);  // clean URL
4637  setTimeout(()=>{
4638    if(vp==='1')     toast('Email verified! You can now log in once your account is activated.','ok');
4639    else if(vp==='error')  toast('Invalid verification link.','err');
4640    else if(vp==='expired')toast('Verification link expired. Please register again.','err');
4641  },400);
4642})();
4643
4644// ── ECONOMIC COUNTDOWN BANNER ─────────────────
4645const _HIGH_IMPACT_RELEASES=[
4646  {kw:"Employment Situation",   time:"8:30 AM EST"},
4647  {kw:"Consumer Price",         time:"8:30 AM EST"},
4648  {kw:"Producer Price",         time:"8:30 AM EST"},
4649  {kw:"Gross Domestic Product", time:"8:30 AM EST"},
4650  {kw:"Retail Sales",           time:"8:30 AM EST"},
4651  {kw:"Personal Income",        time:"8:30 AM EST"},
4652  {kw:"Initial Claims",         time:"8:30 AM EST"},
4653  {kw:"Housing Starts",         time:"8:30 AM EST"},
4654  {kw:"Industrial Production",  time:"10:15 AM EST"},
4655];
4656async function loadEcoCountdown(){
4657  const banner=$('eco-countdown-banner');
4658  if(!banner)return;
4659  try{
4660    const res=await api('GET','/api/fred/releases?days=7');
4661    if(!res.ok)return;
4662    const d=await res.json();
4663    const today=new Date().toISOString().slice(0,10);
4664    const scored=[];
4665    for(const r of (d.releases||[])){
4666      if(r.date!==today)continue;
4667      const idx=_HIGH_IMPACT_RELEASES.findIndex(k=>r.release_name.includes(k.kw));
4668      if(idx>=0)scored.push({ev:r,pri:idx,time:_HIGH_IMPACT_RELEASES[idx].time});
4669    }
4670    if(!scored.length)return;
4671    scored.sort((a,b)=>a.pri-b.pri);
4672    const{ev,time}=scored[0];
4673    // Hide banner if the event release time has already passed (ET)
4674    const _parseET=t=>{const m=t.match(/(\d+):(\d+)\s*(AM|PM)/i);if(!m)return null;let h=parseInt(m[1]),min=parseInt(m[2]);if(m[3].toUpperCase()==='PM'&&h!==12)h+=12;if(m[3].toUpperCase()==='AM'&&h===12)h=0;return{h,min};};
4675    const et=_parseET(time);
4676    if(et){const nowStr=new Date().toLocaleString('en-US',{timeZone:'America/New_York',hour:'2-digit',minute:'2-digit',hour12:false});const[nowH,nowM]=nowStr.split(':').map(Number);if(nowH>et.h||(nowH===et.h&&nowM>=et.min))return;}
4677    banner.innerHTML=`
4678      <span style="font-size:.95rem">🚨</span>
4679      <span><b>${ev.release_name}</b> releasing today
4680        <span style="opacity:.7;font-size:.7rem"> · est. ${time}</span>
4681      </span>
4682      <button onclick="filterNews('economic');go('news')"
4683              style="margin-left:auto;background:transparent;border:1px solid rgba(255,193,7,.4);color:#ffc107;border-radius:4px;padding:2px 9px;cursor:pointer;font-size:.7rem">
4684        View Calendar
4685      </button>
4686      <button onclick="this.closest('#eco-countdown-banner').style.display='none'"
4687              style="background:transparent;border:none;color:inherit;cursor:pointer;opacity:.5;font-size:.85rem">✕</button>`;
4688    banner.style.display='flex';
4689  }catch(e){}
4690}
4691// ── OVERVIEW ──────────────────────────────────
4692function initExtBanner(){
4693  const proTiers=new Set(['creator','pro','stellar','enterprise_api','admin','super_admin']);
4694  if(!proTiers.has(USER.tier))return;
4695  if(localStorage.getItem('fz_ext_dismissed'))return;
4696  const b=document.getElementById('ext-install-banner');
4697  if(b)b.style.display='flex';
4698}
4699function dismissExtBanner(){
4700  localStorage.setItem('fz_ext_dismissed','1');
4701  const b=document.getElementById('ext-install-banner');
4702  if(b)b.style.display='none';
4703}
4704
4705function _lockedWidgetHtml(){
4706  return `<div style="font-size:.72rem;color:var(--muted)">${t('overview.locked_widget')}</div>`;
4707}
4708function loadOverview(){
4709  loadEcoCountdown();
4710
4711  // Quick Actions — synchronous, USER is set before loadOverview is called
4712  const isCreatorTier=USER.tier==='creator'||USER.tier==='pro'||USER.tier==='stellar'||USER.tier==='admin'||USER.tier==='super_admin';
4713  if($('ov-quick-actions')){
4714    $('ov-quick-actions').innerHTML=isCreatorTier
4715      ?`<button class="btn btn-primary" style="flex:1;justify-content:center;padding:12px 8px" onclick="go('scanner')">▶ Run Scanner</button>
4716        <button class="btn btn-primary" style="flex:1;justify-content:center;padding:12px 8px" onclick="go('scanner');setTimeout(()=>scannerTab('screener'),100)">🔍 Run Screener</button>
4717        <button class="btn btn-primary" style="flex:1;justify-content:center;padding:12px 8px" onclick="filterNews('brief');go('news')">📋 Daily Brief</button>`
4718      :`<button class="btn btn-primary" style="flex:1;justify-content:center;padding:12px 8px" onclick="go('news')">📰 News Feed</button>
4719        <button class="btn btn-primary" style="flex:1;justify-content:center;padding:12px 8px" onclick="filterNews('brief');go('news')">📋 Daily Brief</button>`;
4720  }
4721
4722  // All API sections fire in parallel — each renders independently as data arrives
4723  (async()=>{try{const res=await api('GET','/api/scanner/status');if(res.ok){const d=await res.json();$('ov-signals').textContent=(d.results||[]).filter(r=>r.direction).length;}}catch(e){}})();
4724
4725  // Latest brief widget
4726  (async()=>{try{
4727    const el=$('ov-latest-brief');
4728    if(!el)return;
4729    const r=await api('GET','/api/briefs/today');
4730    if(r.ok){
4731      const d=await r.json();
4732      const date=d.edition_date||'';
4733      const snippet=(d.html_content||'').replace(/<[^>]+>/g,' ').replace(/\s+/g,' ').trim().slice(0,180);
4734      el.innerHTML=`
4735        <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);margin-bottom:4px">${date} · PRE-MARKET</div>
4736        <div style="font-size:.75rem;color:var(--text2);line-height:1.6">${snippet}…</div>`;
4737    } else {
4738      el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No brief today yet.</div>';
4739    }
4740  }catch(e){const el=$('ov-latest-brief');if(el)el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">—</div>';}})();
4741
4742  (async()=>{try{
4743    const ro=await api('GET','/api/outputs');
4744    if(ro.ok){
4745      const rod=await ro.json();
4746      const outputs=(rod.outputs||[]).slice(0,4);
4747      const el=$('ov-outputs');
4748      if(!el)return;
4749      if(!outputs.length){el.innerHTML='<div style="font-size:.72rem;color:var(--muted);grid-column:1/-1">No outputs yet.</div>';return;}
4750      const typeLabel={'carousel_pil':'Carousel','carousel_templated':'Templated','collage':'Collage'};
4751      const typeColor={'carousel_pil':'var(--teal)','carousel_templated':'var(--yellow)','collage':'var(--green)'};
4752      el.innerHTML=outputs.map(o=>{
4753        const slides=o.slides||[];
4754        const thumb=o.thumbnail||slides[0]?.url||'';
4755        const label=typeLabel[o.type]||o.type;
4756        const color=typeColor[o.type]||'var(--muted)';
4757        const urlsJson=JSON.stringify(slides.map(s=>s.url));
4758        const thumbClick=thumb?`onclick="openLightbox(${urlsJson},0)" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:4px;background:var(--bg2);cursor:pointer"`:
4759                               `style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:4px;background:var(--bg2)"`;
4760        return `<div style="background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:6px">
4761          <div style="position:relative"><img src="${thumb}" ${thumbClick} onerror="this.style.display='none'">
4762            <span style="position:absolute;top:4px;left:4px;background:${color};color:#000;font-size:.55rem;font-weight:700;padding:2px 5px;border-radius:3px;font-family:var(--mono)">${label}</span>
4763          </div></div>`;
4764      }).join('');
4765    }
4766  }catch(e){}})();
4767
4768  (async()=>{try{
4769    const nr=await api('GET','/api/news');
4770    if(nr.ok){
4771      const nd=await nr.json();
4772      if(!_newsItems.length)_newsItems=nd.items||[];
4773      _ovNewsItems=(nd.items||[]).filter(n=>n.content_type==='news');
4774      _renderOvNews(_ovNewsItems.slice(0,5));
4775      // DJT card — only if user opted in
4776      if(USER&&USER.djt_alerts){
4777        const djtItems=(nd.items||[]).filter(n=>n.content_type==='djt').slice(0,5);
4778        const djtCard=$('ov-djt-card'),djtPrev=$('ov-djt-preview');
4779        if(djtCard&&djtPrev&&djtItems.length){
4780          djtCard.style.display='';
4781          djtPrev.innerHTML=djtItems.map(n=>`<a href="${n.source_url||'#'}" target="_blank" style="text-decoration:none;color:inherit"><div style="font-size:.68rem;line-height:1.4;padding:6px 0;border-bottom:1px solid var(--border)"><span style="color:var(--muted);font-family:var(--mono);font-size:.58rem">${new Date(n.created_at).toLocaleString(undefined,{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}</span><br>${(n.title||'').slice(0,200)}${(n.title||'').length>200?'…':''}</div></a>`).join('');
4782        }
4783      }
4784    }else if(nr.status===403){
4785      _renderOvNews([]);
4786      const el=$('ov-news-preview');
4787      if(el)el.innerHTML=_lockedWidgetHtml();
4788    }
4789  }catch(e){}})();
4790
4791
4792  (async()=>{try{
4793    const [ms,cf,fg]=await Promise.all([
4794      api('GET','/api/news/market-status'),
4795      api('GET','/api/news/crypto-feed'),
4796      api('GET','/api/news/crypto-feargreed'),
4797    ]);
4798    const strip=$('ov-market-strip');
4799    if(strip){
4800      let html='';
4801      if(ms.ok){const md=await ms.json();const excs=md.exchanges||[];if(excs.length){html=excs.map((e,i)=>`${i>0?'<span style="color:var(--border);padding:0 2px">|</span>':''}<span style="display:inline-flex;align-items:center;gap:3px">${e.flag}<span style="color:var(--text2)">${e.label}</span><span style="color:${e.isOpen?'var(--green)':'var(--muted)'}">${e.isOpen?'●':'○'}</span></span>`).join('');}}
4802      if(cf.ok){const cd=await cf.json();const assets=cd.assets||[];const SHOW=['BTC','ETH','SOL','BNB','XRP'];const coins=SHOW.map(s=>assets.find(a=>a.symbol===s)).filter(Boolean);if(coins.length){html+='<span style="color:var(--border);padding:0 6px">·</span>';html+=coins.map(c=>{const p=Number(c.priceUsd);const pct=Number(c.changePercent24Hr);const pStr=p>=1000?'$'+p.toLocaleString('en-US',{maximumFractionDigits:0}):p>=1?'$'+p.toFixed(2):'$'+p.toFixed(4);const arrow=pct>0?'▲':pct<0?'▼':'';const col=pct>0?'var(--green)':pct<0?'var(--red)':'var(--muted)';return `<span style="display:inline-flex;align-items:center;gap:3px"><span style="color:var(--text2);font-weight:700">${c.symbol}</span><span style="color:var(--text1)">${pStr}</span><span style="color:${col}">${arrow}${Math.abs(pct).toFixed(1)}%</span></span>`;}).join('<span style="color:var(--border);padding:0 3px"> </span>');}}
4803      if(fg.ok){const fd=await fg.json();const val=fd.value!=null?Math.round(fd.value):null;if(val!=null){const emoji=val<=25?'😱':val<=45?'😨':val<=55?'😐':val<=75?'😏':'🤑';const lbl=fd.classification||'';html+='<span style="color:var(--border);padding:0 6px">|</span>';html+=`<span style="display:inline-flex;align-items:center;gap:3px"><span style="color:var(--muted)">F&G</span><span style="color:var(--text1);font-weight:700">${val}</span><span>${emoji}</span><span style="color:var(--text2)">${lbl}</span></span>`;}}
4804      strip.innerHTML=html||'<span style="color:var(--muted)">Market status unavailable</span>';
4805    }
4806  }catch(e){}})();
4807
4808  (async()=>{try{
4809    const er=await api('GET','/api/news/earnings-recent');
4810    const epsel=$('ov-eps-panel');
4811    if(er.ok&&epsel){
4812      const ed=await er.json();
4813      const eps=(ed.entries||[]).slice(0,6);
4814      if(!eps.length){epsel.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No recent earnings reports.</div>';}
4815      else{epsel.innerHTML=eps.map(e=>{
4816        const sp=e.surprise_pct;
4817        const beat=sp!=null&&sp>0,miss=sp!=null&&sp<0;
4818        const badge=beat?`<span style="background:rgba(0,200,83,.15);color:var(--green);border-radius:3px;padding:1px 5px;font-size:.6rem">▲ BEAT</span>`
4819                   :miss?`<span style="background:rgba(255,68,68,.15);color:var(--red);border-radius:3px;padding:1px 5px;font-size:.6rem">▼ MISS</span>`
4820                        :`<span style="color:var(--muted);font-size:.6rem">IN-LINE</span>`;
4821        const spTxt=sp!=null?`${sp>0?'+':''}${sp.toFixed(1)}%`:'—';
4822        const fmtRev=v=>v!=null?(Math.abs(v)>=1e9?`$${(v/1e9).toFixed(2)}B`:`$${(v/1e6).toFixed(0)}M`):null;
4823        const revAct=fmtRev(e.rev_actual),revEst=fmtRev(e.rev_estimate);
4824        const _safeE=encodeURIComponent(JSON.stringify(e));
4825        return `<div onclick="openEarningsDetail(JSON.parse(decodeURIComponent('${_safeE}')))" style="cursor:pointer;display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;margin-bottom:5px;transition:border-color .15s" onmouseover="this.style.borderColor='var(--teal)'" onmouseout="this.style.borderColor='var(--border)'">
4826          <div style="font-size:.75rem;font-weight:700;color:var(--text);font-family:var(--mono);min-width:44px">${e.ticker}</div>
4827          ${badge}
4828          <div style="margin-left:auto;text-align:right">
4829            <div style="font-size:.72rem;color:${beat?'var(--green)':miss?'var(--red)':'var(--muted)'};font-weight:600">${spTxt}</div>
4830            <div style="font-size:.62rem;color:var(--text2)">EPS ${e.eps_actual!=null?e.eps_actual.toFixed(2):'—'} vs ${e.eps_estimate!=null?e.eps_estimate.toFixed(2):'—'} est</div>
4831            ${revAct?`<div style="font-size:.62rem;color:var(--text2)">Rev ${revAct}${revEst?' vs '+revEst+' est':''}</div>`:''}
4832          </div>
4833        </div>`;
4834      }).join('');}
4835    }
4836  }catch(e){}})();
4837
4838  loadInsiderFlow();
4839  loadAnalystRatings();
4840  loadPremarketScans();
4841}
4842
4843// ── PRE-MARKET SCANS PANEL ───────────────────
4844async function loadPremarketScans(){
4845  const el=$('premarket-scans-panel');
4846  if(!el)return;
4847  try{
4848    const res=await api('GET','/api/scanner/premarket-today');
4849    if(!res.ok){el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">Pre-market scan unavailable.</div>';return;}
4850    const d=await res.json();
4851    renderPremarketScans(el,d.results||[],d.collage_urls||[]);
4852  }catch(e){
4853    el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">Pre-market scan unavailable.</div>';
4854  }
4855}
4856let _pmData=[];
4857function renderPremarketScans(el,results,collageUrls){
4858  if(!results.length){
4859    el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No pre-market scan yet. Check back after 8 AM EST.</div>';
4860    return;
4861  }
4862  _pmData=results;
4863  const _collageDl=$('pm-collage-dl');
4864  if(_collageDl){const _isAdm=USER&&(USER.tier==='admin'||USER.tier==='super_admin');if(_isAdm&&collageUrls&&collageUrls.length){_collageDl.href=collageUrls[0];_collageDl.style.display='inline-flex';}else{_collageDl.style.display='none';}}
4865  const signals=results.filter(r=>r.direction==='BULLISH'||r.direction==='BEARISH');
4866  const neutral=results.filter(r=>!r.direction||r.direction==='NONE'||r.direction==='');
4867  const sigCount=signals.length;
4868
4869  let html=`<div style="font-size:.62rem;color:var(--muted);margin-bottom:10px;font-family:var(--mono)">${results.length} tickers · ${sigCount} signal${sigCount!==1?'s':''}</div>`;
4870
4871  if(signals.length){
4872    html+=`<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">`;
4873    for(let i=0;i<signals.length;i++){
4874      const r=signals[i];
4875      const pmIdx=results.indexOf(r);
4876      const isBull=r.direction==='BULLISH';
4877      const color=isBull?'var(--green)':'var(--red)';
4878      const icon=isBull?'📈':'📉';
4879      html+=`<div onclick="openPmChart(${pmIdx})" style="cursor:pointer;background:var(--bg3);
4880        border:1px solid ${isBull?'rgba(34,197,94,.25)':'rgba(239,68,68,.25)'};border-radius:8px;
4881        padding:8px 14px;display:flex;flex-direction:column;gap:3px;transition:background .15s"
4882        onmouseover="this.style.background='var(--bg2)'" onmouseout="this.style.background='var(--bg3)'">
4883        <div style="font-size:.8rem;font-weight:700;color:${color}">${icon} ${r.ticker}</div>
4884        <div style="font-size:.58rem;color:var(--muted);letter-spacing:.4px">${r.direction} · view chart ↗</div>
4885      </div>`;
4886    }
4887    html+=`</div>`;
4888  }
4889
4890  if(neutral.length){
4891    html+=`<div style="display:flex;gap:5px;flex-wrap:wrap">`;
4892    for(const r of neutral){
4893      html+=`<span class="pm-badge neutral">${r.ticker}</span>`;
4894    }
4895    html+=`</div>`;
4896  }
4897
4898  el.innerHTML=html;
4899}
4900function openPmChart(idx){
4901  try{
4902    const r=_pmData[idx];
4903    if(!r)return;
4904    $('pm-chart-caption').textContent=r.ticker+' \u00b7 '+r.direction;
4905    const container=$('pm-chart-container');
4906    container.innerHTML='';
4907    const w=document.createElement('tv-mini-chart');
4908    w.setAttribute('symbol','NASDAQ:'+r.ticker);
4909    w.style.cssText='width:100%;height:220px';
4910    container.appendChild(w);
4911    $('pm-chart-modal').style.display='flex';
4912    document.body.style.overflow='hidden';
4913  }catch(e){}
4914}
4915function closePmChart(){
4916  $('pm-chart-modal').style.display='none';
4917  document.body.style.overflow='';
4918}
4919
4920function openTickerModal(ticker){
4921  try{
4922    const overlay=document.createElement('div');
4923    overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:9999;display:flex;align-items:center;justify-content:center';
4924    overlay.onclick=e=>{if(e.target===overlay)overlay.remove();};
4925    const box=document.createElement('div');
4926    box.style.cssText='background:var(--bg2);border-radius:10px;overflow:hidden;width:min(680px,95vw);height:420px;position:relative;border:1px solid var(--border)';
4927    const close=document.createElement('button');
4928    close.textContent='✕';
4929    close.style.cssText='position:absolute;top:8px;right:10px;background:transparent;border:none;color:var(--text2);font-size:.9rem;cursor:pointer;z-index:1';
4930    close.onclick=()=>overlay.remove();
4931    const script=document.createElement('script');
4932    script.src='https://s3.tradingview.com/external-embedding/embed-widget-mini-symbol-overview.js';
4933    script.async=true;
4934    script.textContent=JSON.stringify({
4935      symbol:ticker,colorTheme:'dark',isTransparent:true,
4936      width:'100%',height:'100%',locale:'en',
4937      dateRange:'5D',trendLineColor:'rgba(0,230,118,1)',underLineColor:'rgba(0,230,118,0.1)'
4938    });
4939    const tvWrap=document.createElement('div');
4940    tvWrap.className='tradingview-widget-container';
4941    tvWrap.style.cssText='width:100%;height:100%';
4942    const inner=document.createElement('div');
4943    inner.className='tradingview-widget-container__widget';
4944    inner.style.cssText='width:100%;height:100%';
4945    tvWrap.appendChild(inner);
4946    tvWrap.appendChild(script);
4947    box.appendChild(close);
4948    box.appendChild(tvWrap);
4949    overlay.appendChild(box);
4950    document.body.appendChild(overlay);
4951  }catch(e){}
4952}
4953
4954// ── SCANNER ───────────────────────────────────
4955let _scanAnimTimer=null;
4956function _startScanAnim(tickers){
4957  const phases=['Fetching price data','Calculating MAs','Detecting signals','Generating chart'];
4958  let i=0;
4959  _scanAnimTimer=setInterval(()=>{
4960    const ticker=tickers[i%tickers.length];
4961    const phase=phases[Math.floor(i/tickers.length)%phases.length];
4962    $('scan-status').innerHTML=`<span class="badge badge-teal" style="animation:pulse 1.1s ease infinite"><span class="spin" style="margin-right:5px"></span>${ticker} — ${phase}…</span>`;
4963    i++;
4964  },900);
4965}
4966function _stopScanAnim(){clearInterval(_scanAnimTimer);_scanAnimTimer=null;}
4967function loadTickersFromFile(input){
4968  const file=input.files[0];
4969  if(!file)return;
4970  const reader=new FileReader();
4971  reader.onload=e=>{
4972    try{
4973      const wb=XLSX.read(e.target.result,{type:'array'});
4974      const ws=wb.Sheets[wb.SheetNames[0]];
4975      const rows=XLSX.utils.sheet_to_json(ws,{header:1,defval:''});
4976      const tickers=rows
4977        .map(r=>String(r[0]||'').trim().toUpperCase())
4978        .filter(t=>t&&/^[A-Z]{1,5}$/.test(t));
4979      if(!tickers.length){toast('No valid tickers found in column A','err');return;}
4980      $('scan-tickers').value=tickers.join(', ');
4981      toast(`${tickers.length} ticker(s) loaded`,'ok');
4982    }catch(err){toast('Could not read file','err');}
4983    input.value='';
4984  };
4985  reader.readAsArrayBuffer(file);
4986}
4987
4988function saveScannerList(){
4989  const val=$('scan-tickers').value.trim();
4990  if(!val){toast('No tickers to save','err');return;}
4991  localStorage.setItem('fz_default_tickers',val);
4992  toast('Ticker list saved');
4993}
4994function loadScannerList(){
4995  const saved=localStorage.getItem('fz_default_tickers');
4996  if(!saved){toast('No saved list found','err');return;}
4997  $('scan-tickers').value=saved;
4998  toast('List loaded');
4999}
5000
5001function _updateScannerBadge(){
5002  const badge=$('scanner-badge');
5003  if(!badge)return;
5004  try{
5005    const now=new Date();
5006    const day=now.toLocaleDateString('en-US',{timeZone:'America/Ne
5006w_York',weekday:'short'});
5007    const h=parseInt(now.toLocaleString('en-US',{timeZone:'America/New_York',hour:'numeric',hour12:false}));
5008    const isWeekday=!['Sat','Sun'].includes(day);
5009    const isMarketHours=h>=8&&h<17;
5010    badge.style.display=(isWeekday&&isMarketHours&&lastScanCharts.length===0)?'':'none';
5011  }catch(e){badge.style.display='none';}
5012}
5013
5014function _initProScanOpts(){
5015  const isPro=USER&&new Set(['pro','stellar','enterprise_api','admin','super_admin']).has(USER.tier);
5016  document.querySelectorAll('.pro-scan-opt').forEach(opt=>{opt.disabled=!isPro;});
5017  // Auto-fill saved ticker list if textarea is empty
5018  const saved=localStorage.getItem('fz_default_tickers');
5019  if(saved&&!$('scan-tickers').value.trim())$('scan-tickers').value=saved;
5020}
5021
5022async function runScan(){
5023  const tickers=$('scan-tickers').value.split(',').map(t=>t.trim().toUpperCase()).filter(Boolean);
5024  if(!tickers.length){toast('Enter tickers','err');return;}
5025  const scanType=$('scan-type-select').value;
5026  const btn=$('scan-btn');btn.innerHTML='<span class="spin"></span> Scanning...';btn.disabled=true;
5027  $('chart-grid').innerHTML='';
5028  _startScanAnim(tickers);
5029  try{
5030    const res=await api('POST','/api/scanner/run',{tickers,scan_type:scanType});
5031    const d=await res.json();
5032    if(!res.ok){$('scan-status').innerHTML=`<span class="badge badge-red">${d.detail}</span>`;return;}
5033    const signals=(d.results||[]).filter(r=>r.direction&&r.chart_b64);
5034    lastScanCharts=signals;
5035    _updateScannerBadge();
5036    $('scan-status').innerHTML=`<span class="badge badge-green">✓ ${d.signals_found} signal(s)</span> <span class="badge badge-muted">${d.total_scanned} scanned</span>`;
5037    $('chart-count').textContent=`${signals.length} chart(s)`;
5038    $('scan-results').innerHTML=(d.results||[]).map(r=>{
5039      let extra='';
5040      if(r.rsi!=null)extra+=`<span style="color:var(--text2);font-size:.7rem">RSI ${r.rsi}</span>`;
5041      if(r.level!=null)extra+=`<span style="color:var(--text2);font-size:.7rem">Lvl $${r.level}</span>`;
5042      if(r.anchor)extra+=`<span style="color:var(--muted);font-size:.65rem">${r.anchor}</span>`;
5043      if(r.vol_ratio!=null)extra+=`<span style="color:var(--text2);font-size:.7rem">Vol ×${r.vol_ratio}</span><span style="color:var(--muted);font-size:.65rem">${r.change_pct>0?'+':''}${r.change_pct}%</span>`;
5044      if(r.pct_from_extreme!=null)extra+=`<span style="color:var(--text2);font-size:.7rem">${r.pct_from_extreme}% from extreme</span>`;
5045      if(r.gap_pct!=null)extra+=`<span style="color:var(--text2);font-size:.7rem">Gap ${r.gap_pct>0?'+':''}${r.gap_pct}%</span>`;
5046      return `<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:6px 8px;background:var(--bg3);border-radius:5px;margin-bottom:5px;font-size:.75rem">
5047        <span style="font-family:var(--mono);font-weight:700;color:#e2eaf2;min-width:50px">${r.ticker}</span>
5048        ${r.direction?`<span class="badge ${r.direction==='BULLISH'||r.direction==='OVERSOLD'?'badge-green':'badge-red'}">${r.direction}</span>`:'<span class="badge badge-muted">—</span>'}
5049        ${r.price?`<span style="color:var(--text2)">$${parseFloat(r.price).toFixed(2)}</span>`:''}
5050        ${extra}
5051        ${r.error?`<span style="color:var(--red);font-size:.65rem">${r.error}</span>`:''}
5052      </div>`;}).join('');
5053    $('chart-grid').innerHTML=signals.map((c,i)=>`
5054      <div class="chart-thumb" id="cthumb-${i}" style="position:relative">
5055        <img src="data:image/png;base64,${c.chart_b64}" alt="${c.ticker}"
5056             onclick="showChartModal(${i})" style="cursor:pointer" title="Click to enlarge">
5057        <div class="${c.direction==='BULLISH'||c.direction==='OVERSOLD'?'sig-bull':'sig-bear'}">${c.ticker}</div>
5058        <div class="clabel">${c.direction||''}</div>
5059      </div>`).join('');
5060    toast(`${d.signals_found} signal(s) found`,d.signals_found>0?'ok':'');
5061    loadScanHistory();
5062    // Reload My Charts after a short delay (Cloudinary uploads are async)
5063    setTimeout(loadMyCharts, 4000);
5064  }catch(e){$('scan-status').innerHTML='<span class="badge badge-red">Connection error</span>';}
5065  finally{_stopScanAnim();btn.innerHTML='▶ Run Scanner';btn.disabled=false;}
5066}
5067
5068// ── CHART BUILDER ──────────────────────────────
5069async function loadChartIndicators(){
5070  try{
5071    const res=await api('GET','/api/scanner/indicators');
5072    if(!res.ok)return;
5073    const d=await res.json();
5074    const indicators=d.indicators||[];
5075    $('cb-indicators').innerHTML=indicators.map(ind=>{
5076      const params=ind.params.map(p=>`
5077        <label style="font-size:.7rem;color:var(--muted);margin-left:8px">${p.label}:</label>
5078        <input type="number" id="cb-p-${ind.id}-${p.key}" value="${p.default}"
5079               style="width:56px;padding:2px 5px;font-size:.75rem;background:var(--bg2);
5080                      color:var(--text1);border:1px solid var(--border);border-radius:4px;margin-left:2px">`).join('');
5081      return `<div style="display:flex;align-items:center;flex-wrap:wrap;gap:4px;padding:5px 8px;background:var(--bg3);border-radius:5px">
5082        <input type="checkbox" id="cb-chk-${ind.id}" value="${ind.id}" style="margin-right:4px">
5083        <label for="cb-chk-${ind.id}" data-api-name="${ind.api_name}" style="font-size:.8rem;font-weight:600;color:#e2eaf2;cursor:pointer;flex:1">${ind.label}</label>
5084        ${params}
5085      </div>`;
5086    }).join('');
5087  }catch(e){$('cb-indicators').innerHTML='<p style="font-size:.75rem;color:var(--red)">Failed to load indicators</p>';}
5088}
5089
5090async function buildCustomChart(){
5091  const ticker=($('cb-ticker').value||'').trim().toUpperCase();
5092  if(!ticker){toast('Enter a ticker','err');return;}
5093  const interval=$('cb-interval').value;
5094  const range=$('cb-range').value;
5095
5096  // Build studies array from checked indicators
5097  const studies=[];
5098  const indEls=document.querySelectorAll('#cb-indicators input[type=checkbox]:checked');
5099  indEls.forEach(chk=>{
5100    const id=chk.value;
5101    const container=chk.closest('div');
5102    const paramInputs=container.querySelectorAll(`input[id^="cb-p-${id}-"]`);
5103    const inputMap={};
5104    paramInputs.forEach(inp=>{
5105      const key=inp.id.replace(`cb-p-${id}-`,'');
5106      inputMap[key]=parseFloat(inp.value)||0;
5107    });
5108    // Look up api_name from rendered data attr or use checkbox label text
5109    const label=container.querySelector('label[for]');
5110    const apiName=label?label.dataset.apiName||label.textContent.trim():'';
5111    studies.push({name:apiName,input:inputMap});
5112  });
5113
5114  const btn=$('cb-btn');btn.innerHTML='<span class="spin"></span> Generating...';btn.disabled=true;
5115  $('cb-status').innerHTML='';
5116  $('cb-chart-output').innerHTML='';
5117  try{
5118    const res=await api('POST','/api/scanner/chart-builder',{ticker,interval,range,studies});
5119    if(!res.ok){const e=await res.json();$('cb-status').innerHTML=`<span style="color:var(--red)">${e.detail||'Error'}</span>`;return;}
5120    const blob=await res.blob();
5121    const url=URL.createObjectURL(blob);
5122    $('cb-chart-output').innerHTML=`<img src="${url}" alt="${ticker}" style="max-width:100%;border-radius:6px">`;
5123  }catch(e){$('cb-status').innerHTML=`<span style="color:var(--red)">Connection error</span>`;}
5124  finally{btn.innerHTML='📊 Generate Chart';btn.disabled=false;}
5125}
5126
5127// ── SCAN HISTORY ───────────────────────────────
5128let _scanHistoryOpen={};
5129
5130// ── MY CHARTS LIBRARY ────────────────────────────────────────────────────────
5131async function loadMyCharts(){
5132  const grid=$('my-charts-grid');
5133  if(!grid)return;
5134  try{
5135    const res=await api('GET','/api/charts');
5136    if(!res.ok){grid.innerHTML='<p style="font-size:.75rem;color:var(--text2);grid-column:1/-1">Failed to load charts.</p>';return;}
5137    const data=await res.json();
5138    const charts=data.charts||[];
5139    if(!charts.length){grid.innerHTML='<p style="font-size:.75rem;color:var(--text2);grid-column:1/-1">No saved charts yet. Run a scan to populate your library.</p>';return;}
5140    grid.innerHTML=charts.map(c=>{
5141      const ago=_relTime(c.created_at);
5142      const dirColor=c.direction==='BULLISH'?'#4caf50':c.direction==='BEARISH'?'#f44336':'#aaa';
5143      return `<div style="border:1px solid var(--border);border-radius:6px;overflow:hidden;background:var(--bg3)">
5144        <img src="${c.chart_url}" alt="${c.ticker}" style="width:100%;height:90px;object-fit:cover;display:block;cursor:pointer" onclick="openImgPreview('${c.chart_url}')">
5145        <div style="padding:5px 7px">
5146          <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
5147            <span style="font-size:.72rem;font-weight:700;font-family:var(--mono)">${c.ticker}</span>
5148            <span style="font-size:.62rem;color:${dirColor};font-weight:600">${c.direction||'—'}</span>
5149          </div>
5150          <div style="font-size:.6rem;color:var(--text2);margin-bottom:5px">${ago}</div>
5151          <div style="display:flex;gap:4px">
5152            <button class="btn btn-ghost" style="font-size:.6rem;padding:2px 6px;color:var(--red)" onclick="deleteLibChart('${c.id}',this)">✕</button>
5153          </div>
5154        </div>
5155      </div>`;
5156    }).join('');
5157  }catch(e){grid.innerHTML='<p style="font-size:.75rem;color:var(--text2);grid-column:1/-1">Error loading charts.</p>';}
5158}
5159
5160function _relTime(iso){
5161  const d=new Date(iso);const now=new Date();
5162  const diff=Math.round((now-d)/1000);
5163  if(diff<60)return'just now';
5164  if(diff<3600)return`${Math.round(diff/60)}m ago`;
5165  if(diff<86400)return`${Math.round(diff/3600)}h ago`;
5166  return`${Math.round(diff/86400)}d ago`;
5167}
5168
5169// Fetch chart from URL and add it to the carousel file inputs
5170async function addLibChartToCarousel(url, ticker){
5171  try{
5172    const resp=await fetch(url);
5173    const blob=await resp.blob();
5174    const file=new File([blob],`${ticker}.png`,{type:'image/png'});
5175    go('carousel');
5176    // Small delay to let carousel tab render
5177    setTimeout(()=>{
5178      const inp=$('carousel-files');
5179      if(inp){
5180        const dt=new DataTransfer();
5181        if(inp.files){Array.from(inp.files).forEach(f=>dt.items.add(f));}
5182        dt.items.add(file);inp.files=dt.files;
5183        inp.dispatchEvent(new Event('change'));
5184      }
5185      toast(`${ticker} added to Carousel — click Build`,'ok');
5186    },300);
5187  }catch(e){toast('Failed to load chart','err');}
5188}
5189
5190// Fetch chart from URL and add it to the collage file input
5191async function addLibChartToCollage(url, ticker){
5192  try{
5193    const resp=await fetch(url);
5194    const blob=await resp.blob();
5195    const file=new File([blob],`${ticker}.png`,{type:'image/png'});
5196    go('collage');
5197    setTimeout(()=>{
5198      const inp=$('collage-files');
5199      if(inp){
5200        const dt=new DataTransfer();
5201        // preserve existing files
5202        if(inp.files){Array.from(inp.files).forEach(f=>dt.items.add(f));}
5203        dt.items.add(file);
5204        inp.files=dt.files;
5205        inp.dispatchEvent(new Event('change'));
5206      }
5207      toast(`${ticker} added to Collage`,'ok');
5208    },300);
5209  }catch(e){toast('Failed to load chart','err');}
5210}
5211
5212async function deleteLibChart(id, btn){
5213  btn.disabled=true;
5214  try{
5215    const res=await api('DELETE',`/api/charts/${id}`);
5216    if(res.ok){btn.closest('div[style*="border-radius"]').remove();toast('Chart removed','ok');}
5217    else{btn.disabled=false;toast('Delete failed','err');}
5218  }catch(e){btn.disabled=false;toast('Error','err');}
5219}
5220
5221// ── MY COLLAGES LIBRARY ───────────────────────────────────────────────────────
5222async function loadMyCollages(){
5223  const grid=$('my-collages-grid');
5224  if(!grid)return;
5225  try{
5226    const res=await api('GET','/api/collage/saved');
5227    if(!res.ok){grid.innerHTML='<p style="font-size:.75rem;color:var(--text2);grid-column:1/-1">Failed to load collages.</p>';return;}
5228    const data=await res.json();
5229    const collages=data.collages||[];
5230    if(!collages.length){grid.innerHTML='<p style="font-size:.75rem;color:var(--text2);grid-column:1/-1">Generate a collage to save it here.</p>';return;}
5231    grid.innerHTML=collages.map(c=>{
5232      const ago=_relTime(c.created_at);
5233      return `<div style="border:1px solid var(--border);border-radius:6px;overflow:hidden;background:var(--bg3)">
5234        <img src="${c.thumbnail}" alt="collage" style="width:100%;height:90px;object-fit:cover;display:block;cursor:pointer" onclick="openLightbox(['${c.thumbnail}'],0)">
5235        <div style="padding:5px 7px">
5236          <div style="font-size:.6rem;color:var(--text2);margin-bottom:5px">${ago}</div>
5237          <div style="display:flex;gap:4px;flex-wrap:wrap">
5238            <button class="btn btn-ghost" style="font-size:.6rem;padding:2px 5px;flex:1" onclick="useLibCollage('${c.thumbnail}')">📤 Use</button>
5239            <a href="${c.thumbnail}" download="collage.png" class="btn btn-ghost" style="font-size:.6rem;padding:2px 5px">⬇</a>
5240            <button class="btn btn-ghost" style="font-size:.6rem;padding:2px 5px;color:var(--red)" onclick="deleteLibCollage('${c.id}',this)">✕</button>
5241          </div>
5242        </div>
5243      </div>`;
5244    }).join('');
5245  }catch(e){grid.innerHTML='<p style="font-size:.75rem;color:var(--text2);grid-column:1/-1">Error loading collages.</p>';}
5246}
5247
5248async function useLibCollage(url){
5249  try{
5250    const res=await fetch(url);
5251    lastCollageBlob=await res.blob();
5252    const objUrl=URL.createObjectURL(lastCollageBlob);
5253    $('collage-preview').innerHTML=`<img src="${objUrl}" style="width:100%;border-radius:8px;border:1px solid var(--border)">`;
5254    $('collage-actions').innerHTML=`<a href="${objUrl}" download="factoz_collage.png" class="btn btn-green">⬇ Download</a><button class="btn btn-primary" onclick="sendCollageToPost()">📤 Send to Post →</button>`;
5255    setCollageStep(4);
5256    $('collage-actions').scrollIntoView({behavior:'smooth'});
5257    toast('Collage loaded — click Send to Post to share','ok');
5258  }catch(e){toast('Could not load collage','err');}
5259}
5260
5261async function deleteLibCollage(id, btn){
5262  btn.disabled=true;
5263  try{
5264    const res=await api('DELETE',`/api/collage/saved/${id}`);
5265    if(res.ok){btn.closest('div[style*="border-radius"]').remove();}
5266    else{btn.disabled=false;toast('Delete failed','err');}
5267  }catch(e){btn.disabled=false;toast('Error','err');}
5268}
5269
5270async function loadScanHistory(){
5271  try{
5272    const res=await api('GET','/api/scanner/history');
5273    if(!res.ok)return;
5274    const runs=await res.json();
5275    const el=$('scan-history-list');
5276    if(!runs.length){el.innerHTML='<p style="font-size:.75rem;color:var(--text2)">No saved scans yet.</p>';return;}
5277    el.innerHTML=runs.map(r=>`
5278      <div id="shr-${r.id}" style="border:1px solid var(--border);border-radius:6px;margin-bottom:6px;overflow:hidden">
5279        <div style="display:flex;align-items:center;justify-content:space-between;padding:7px 10px;background:var(--bg3);cursor:pointer" onclick="toggleScanRun('${r.id}')">
5280          <span style="font-size:.72rem;font-family:var(--mono);color:#e2eaf2">${r.name}</span>
5281          <div style="display:flex;align-items:center;gap:6px">
5282            <span class="badge badge-muted" style="font-size:.62rem">${r.ticker_count} tickers</span>
5283            <span class="badge ${r.signal_count>0?'badge-green':'badge-muted'}" style="font-size:.62rem">${r.signal_count} signal${r.signal_count!==1?'s':''}</span>
5284            <button class="btn btn-ghost" style="font-size:.62rem;padding:2px 6px;color:var(--red)" onclick="event.stopPropagation();deleteScanRun('${r.id}')">&#x2715;</button>
5285          </div>
5286        </div>
5287        <div id="shd-${r.id}" style="display:none;padding:8px 10px"></div>
5288      </div>`).join('');
5289  }catch(e){}
5290}
5291
5292async function toggleScanRun(id){
5293  const detail=$('shd-'+id);
5294  if(!detail)return;
5295  const open=detail.style.display!=='none';
5296  detail.style.display=open?'none':'block';
5297  if(!open&&!_scanHistoryOpen[id]){
5298    _scanHistoryOpen[id]=true;
5299    detail.innerHTML='<span style="font-size:.72rem;color:var(--text2)">Loading...</span>';
5300    try{
5301      const res=await api('GET',`/api/scanner/history/${id}`);
5302      if(!res.ok){detail.innerHTML='<span style="color:var(--red);font-size:.7rem">Failed to load</span>';return;}
5303      const d=await res.json();
5304      detail.innerHTML=_renderHistoryResults(d.results,id);
5305    }catch(e){detail.innerHTML='<span style="color:var(--red);font-size:.7rem">Error loading scan</span>';}
5306  }
5307}
5308
5309function _renderHistoryResults(results,scanId){
5310  const rows=results.map(r=>`
5311    <div id="shr-row-${scanId}-${r.ticker}" style="display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:5px 0;border-bottom:1px solid var(--border);font-size:.72rem">
5312      <span style="font-family:var(--mono);font-weight:700;width:54px">${r.ticker}</span>
5313      ${r.direction?`<span class="badge ${r.direction==='BULLISH'?'badge-green':'badge-red'}" style="font-size:.6rem">${r.direction==='BULLISH'?'▲':'▼'} ${r.direction}</span>`:'<span class="badge badge-muted" style="font-size:.6rem">—</span>'}
5314      ${r.anchor?`<span style="color:var(--text2);font-size:.65rem">${r.anchor}</span>`:''}
5315      ${r.price?`<span style="color:#e2eaf2">$${parseFloat(r.price).toFixed(2)}</span>`:''}
5316      ${r.error?`<span style="color:var(--red);font-size:.65rem">${r.error}</span>`:''}
5317      <span id="shr-cmp-${scanId}-${r.ticker}"></span>
5318    </div>`).join('');
5319  return `${rows}
5320    <button class="btn btn-primary" style="margin-top:8px;font-size:.72rem;padding:5px 12px" onclick="comparePrices('${scanId}',this)">Compare Today's Prices</button>
5321    <div id="shc-${scanId}" style="margin-top:8px"></div>`;
5322}
5323
5324async function deleteScanRun(id){
5325  if(!confirm('Delete this scan?'))return;
5326  const res=await api('DELETE',`/api/scanner/history/${id}`);
5327  if(res.ok){const el=$('shr-'+id);if(el)el.remove();toast('Scan deleted','ok');}
5328  else toast('Delete failed','err');
5329}
5330
5331async function comparePrices(scanId,btn){
5332  btn.disabled=true;btn.textContent='Fetching prices...';
5333  const ctr=$('shc-'+scanId);
5334  ctr.innerHTML='<span style="font-size:.7rem;color:var(--text2)">Comparing prices vs original scan...</span>';
5335  document.querySelectorAll(`[id^="shr-cmp-${scanId}-"]`).forEach(s=>s.innerHTML='');
5336  try{
5337    const res=await api('POST',`/api/scanner/history/${scanId}/compare`);
5338    if(!res.ok){const e=await res.json();ctr.innerHTML=`<span style="color:var(--red);font-size:.7rem">${e.detail||'Error'}</span>`;return;}
5339    const d=await res.json();
5340    ctr.innerHTML='';
5341    (d.comparison||[]).forEach(r=>{
5342      if(r.error){
5343        const s=document.getElementById(`shr-cmp-${scanId}-${r.ticker}`);
5344        if(s)s.innerHTML=`<span style="color:var(--red);font-size:.65rem;margin-left:4px">${r.error}</span>`;
5345        return;
5346      }
5347      const up=r.delta>0,dn=r.delta<0;
5348      const dColor=up?'#26a69a':dn?'#ef5350':'var(--text2)';
5349      const arrow=up?'↑':dn?'↓':'→';
5350      const deltaStr=r.delta!=null?`<span style="color:${dColor};font-weight:700;margin-left:4px">${arrow} ${r.delta>0?'+':''}$${r.delta.toFixed(2)} (${r.delta_pct>0?'+':''}${r.delta_pct.toFixed(2)}%)</span>`:'';
5351      const newSig=r.new_direction?`<span class="badge ${r.new_direction==='BULLISH'?'badge-green':'badge-red'}" style="font-size:.6rem;margin-left:4px">NEW ${r.new_direction}</span>`:'';
5352      const curr=`<span style="color:var(--text2)">→ $${parseFloat(r.curr_price).toFixed(2)}</span>`;
5353      const cmpSpan=document.getElementById(`shr-cmp-${scanId}-${r.ticker}`);
5354      if(cmpSpan)cmpSpan.innerHTML=` ${curr}${deltaStr}${newSig}`;
5355      if(r.chart_b64){
5356        ctr.innerHTML+=`<div class="chart-thumb" style="margin-top:8px">
5357          <img src="data:image/png;base64,${r.chart_b64}" alt="${r.ticker}" style="width:100%;border-radius:4px">
5358          <div class="${r.new_direction==='BULLISH'?'sig-bull':'sig-bear'}">${r.ticker}</div>
5359        </div>`;
5360      }
5361    });
5362    toast('Prices updated','ok');
5363  }catch(e){ctr.innerHTML='<span style="color:var(--red);font-size:.7rem">Connection error</span>';}
5364  finally{btn.disabled=false;btn.textContent='Compare Today\'s Prices';}
5365}
5366
5367// ── CAROUSEL ──────────────────────────────────
5368let selectedPremiumTemplate='factoz';
5369
5370async function loadRecentOutputs(){
5371  const el=$('recent-outputs-grid');
5372  if(!el)return;
5373  if(!el.querySelector('img')) el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">Loading…</div>';
5374  try{
5375    const res=await api('GET','/api/outputs');
5376    const d=await res.json();
5377    const outputs=d.outputs||[];
5378    if(!outputs.length){
5379      el.innerHTML='<div style="font-size:.72rem;color:var(--muted);grid-column:1/-1">No outputs yet — generate a carousel or collage to see it here.</div>';
5380      return;
5381    }
5382    const typeLabel={'carousel_pil':'Carousel','carousel_templated':'Templated','collage':'Collage'};
5383    const typeColor={'carousel_pil':'var(--teal)','carousel_templated':'var(--yellow)','collage':'var(--green)'};
5384    el.innerHTML=outputs.map(o=>{
5385      const slides=o.slides||[];
5386      const thumb=o.thumbnail||slides[0]?.url||'';
5387      const label=typeLabel[o.type]||o.type;
5388      const color=typeColor[o.type]||'var(--muted)';
5389      const ts=new Date(o.created_at).toLocaleString(undefined,{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'});
5390      const allUrls=slides.map(s=>s.url);
5391      const dlBtns=allUrls.map((u,i)=>`<button onclick="downloadCarouselOutput('${u}','${label.toLowerCase()}_slide${i+1}.png')" style="flex:1;font-size:.55rem;padding:2px 4px;background:var(--bg2);border:1px solid var(--border);border-radius:3px;color:var(--muted);cursor:pointer">↓ Download${allUrls.length>1?' '+(i+1):''}</button>`).join('');
5392      return `<div style="background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:6px;display:flex;flex-direction:column;gap:4px">
5393        <div style="position:relative">
5394          <img src="${thumb}" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:4px;background:var(--bg2)" onerror="this.style.display='none'">
5395          <span style="position:absolute;top:4px;left:4px;background:${color};color:#000;font-size:.55rem;font-weight:700;padding:2px 5px;border-radius:3px;font-family:var(--mono)">${label}</span>
5396        </div>
5397        <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono)">${ts}</div>
5398        <div style="display:flex;gap:3px">${dlBtns}</div>
5399      </div>`;
5400    }).join('');
5401  }catch(e){
5402    if(el)el.innerHTML='<div style="font-size:.72rem;color:var(--muted);grid-column:1/-1">Could not load recent outputs.</div>';
5403  }
5404}
5405
5406async function downloadCarouselOutput(url,filename){
5407  try{
5408    const resp=await fetch(url);
5409    const blob=await resp.blob();
5410    const a=document.createElement('a');
5411    a.href=URL.createObjectURL(blob);
5412    a.download=filename||'carousel.png';
5413    a.click();
5414    setTimeout(()=>URL.revokeObjectURL(a.href),5000);
5415  }catch(e){window.open(url,'_blank');}
5416}
5417
5418async function loadCarouselPersons(){
5419  try{
5420    const res=await api('GET','/api/carousel/persons');
5421    if(!res.ok)return;
5422    const d=await res.json();
5423    const opts='<option value="">— No person (plain background) —</option>'+
5424      (d.persons||[]).map(p=>`<option value="${p.id}">${p.name}</option>`).join('');
5425    const sel=$('carousel-person');
5426    if(sel)sel.innerHTML=opts;
5427  }catch(e){}
5428}
5429
5430function switchCarouselTab(tab){
5431  const isPil=tab==='pil';
5432  $('carousel-pil-panel').style.display=isPil?'':'none';
5433  $('carousel-premium-panel').style.display=isPil?'none':'';
5434  $('tab-pil').style.background=isPil?'rgba(226,194,117,.12)':'transparent';
5435  $('tab-pil').style.color=isPil?'var(--teal)':'var(--muted)';
5436  $('tab-premium').style.background=isPil?'transparent':'rgba(226,194,117,.12)';
5437  $('tab-premium').style.color=isPil?'var(--muted)':'var(--teal)';
5438  if(!isPil)checkPremiumStatus();
5439}
5440
5441const TMPL_PREVIEWS={
5442  factoz:{bg:'#0b132b',accent:'#E2C275',text:'#FFFFFF',font:'var(--mono)',label:'Factoz — Dark & Bold',desc:'Deep navy canvas with gold accents and grid overlay. Bold white headlines.'},
5443  terminal:{bg:'#000000',accent:'#00FF41',text:'#00FF41',font:'var(--mono)',label:'Terminal — Bloomberg Feel',desc:'Pure black background, neon green monospace text. Trading terminal aesthetic.'},
5444  editorial:{bg:'#F5F0E8',accent:'#1A1A2E',text:'#1A1A2E',font:'Georgia,serif',label:'Editorial — Magazine Style',desc:'Clean cream background, dark serif headlines. Premium print magazine layout.'},
5445};
5446function updateTemplatePreview(name){
5447  const p=TMPL_PREVIEWS[name];
5448  if(!p)return;
5449  const preview=$('carousel-preview');
5450  if(!preview)return;
5451  if(preview.querySelector('.slide-thumb img'))return;
5452  preview.innerHTML=`<div style="grid-column:1/-1;border-radius:10px;overflow:hidden;border:2px solid ${p.accent};background:${p.bg};padding:26px 20px;font-family:${p.font};display:flex;flex-direction:column;gap:10px">
5453    <div style="font-size:.6rem;letter-spacing:.14em;color:${p.accent};font-weight:700;text-transform:uppercase">${p.label}</div>
5454    <div style="width:100%;height:2px;background:${p.accent};border-radius:2px;opacity:.6"></div>
5455    <div style="font-size:1.1rem;font-weight:700;color:${p.text};line-height:1.3">MARKET SIGNAL<br>REPORT</div>
5456    <div style="font-size:.63rem;color:${p.text};opacity:.55;margin-top:2px">${p.desc}</div>
5457    <div style="display:flex;gap:6px;margin-top:8px">
5458      <div style="background:${p.accent};opacity:.2;border-radius:4px;
5458height:28px;flex:1"></div>
5459      <div style="background:${p.accent};opacity:.14;border-radius:4px;height:28px;flex:2"></div>
5460      <div style="background:${p.accent};opacity:.09;border-radius:4px;height:28px;flex:1"></div>
5461    </div>
5462    <div style="font-size:.53rem;color:${p.accent};opacity:.75;text-align:right;margin-top:6px;letter-spacing:.07em">Brand kit applied ✓</div>
5463  </div>`;
5464}
5465function resetCarouselPreview(){
5466  const preview=$('carousel-preview');
5467  if(!preview)return;
5468  preview.innerHTML=`
5469    <div style="background:var(--bg3);border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--muted)">Slide 1</div>
5470    <div style="background:var(--bg3);border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--muted)">Slide 2</div>
5471    <div style="background:var(--bg3);border:1px dashed var(--border);border-radius:6px;aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:.65rem;color:var(--muted)">Slide 3</div>`;
5472}
5473function selectTemplate(name,el){
5474  selectedTemplate=name;
5475  document.querySelectorAll('#carousel-pil-panel .tmpl-card').forEach(c=>c.classList.remove('selected'));
5476  el.classList.add('selected');
5477  updateTemplatePreview(name);
5478}
5479
5480function selectPremiumTemplate(name,el){
5481  selectedPremiumTemplate=name;
5482  document.querySelectorAll('#premium-card-grid .tmpl-card').forEach(c=>c.classList.remove('selected'));
5483  if(el)el.classList.add('selected');
5484}
5485
5486// ── TEMPLATE CARD GRID ────────────────────────
5487let _allTemplatesMeta=[];
5488
5489async function loadTemplatesMeta(){
5490  try{
5491    const res=await api('GET','/api/templated/templates');
5492    if(res.ok){const d=await res.json();_allTemplatesMeta=d.templates||[];}
5493  }catch(_){
5494    _allTemplatesMeta=[
5495      {id:'factoz',    label:'Factoz',     free:true, configured:true},
5496      {id:'terminal',  label:'Terminal',   free:true, configured:true},
5497      {id:'editorial', label:'Editorial',  free:true, configured:true},
5498      {id:'template_4', label:'Template 4', free:true, configured:false},
5499      {id:'template_5', label:'Template 5', free:true, configured:false},
5500      {id:'template_6', label:'Template 6', free:true, configured:false},
5501      {id:'template_7', label:'Template 7', free:true, configured:false},
5502      {id:'template_8', label:'Template 8', free:true, configured:false},
5503      {id:'template_9', label:'Template 9', free:true, configured:false},
5504      {id:'template_10',label:'Template 10',free:true, configured:false},
5505    ];
5506  }
5507}
5508
5509async function reloadTemplates(){
5510  if(_allTemplatesMeta.length) renderTemplateGrid();
5511  await loadTemplatesMeta();
5512  renderTemplateGrid();
5513}
5514
5515function renderTemplateGrid(){
5516  const grid=$('premium-card-grid');
5517  if(!grid)return;
5518  const isAdmin=USER.tier==='admin'||USER.tier==='super_admin';
5519
5520  grid.innerHTML=_allTemplatesMeta.map(t=>{
5521    // User-purchasable slot
5522    if(t.is_user_slot){
5523      if(t.is_mine){
5524        // User owns this slot — render as a selectable card
5525        const isSelected=t.id===selectedPremiumTemplate;
5526        return `<div class="tmpl-card${isSelected?' selected':''}" onclick="selectPremiumTemplate('${t.id}',this)" style="position:relative;cursor:pointer">
5527          <div style="position:absolute;top:4px;right:4px;font-size:.5rem;background:var(--green);color:#000;padding:1px 5px;border-radius:3px;font-weight:700">MINE</div>
5528          <div class="tmpl-icon">🔒</div>
5529          <div class="tmpl-name">${t.label}</div>
5530          <div class="tmpl-desc" style="font-size:.55rem;color:var(--green)">Unlocked</div>
5531        </div>`;
5532      }
5533      if(t.is_taken){
5534        // Taken by another user — dimmed
5535        return `<div class="tmpl-card" style="opacity:.3;cursor:default;pointer-events:none">
5536          <div class="tmpl-icon" style="font-size:1.1rem">🔒</div>
5537          <div class="tmpl-name">${t.label}</div>
5538          <div class="tmpl-desc" style="font-size:.55rem;color:var(--muted)">Taken</div>
5539        </div>`;
5540      }
5541      // Available for purchase
5542      const price=t.price?`$${t.price} USDT`:'—';
5543      return `<div class="tmpl-card" onclick="openSlotModal('${t.id}','${t.label}',${t.price||0})" style="cursor:pointer;border:1px dashed var(--border)">
5544        <div class="tmpl-icon" style="font-size:1.1rem">💳</div>
5545        <div class="tmpl-name">${t.label}</div>
5546        <div class="tmpl-desc" style="font-size:.55rem;color:var(--yellow)">${price}</div>
5547      </div>`;
5548    }
5549
5550    // Unconfigured admin slot — show dimmed placeholder
5551    if(t.configured===false){
5552      return `<div class="tmpl-card" style="opacity:.3;cursor:default;pointer-events:none">
5553        <div class="tmpl-icon" style="font-size:1.1rem">+</div>
5554        <div class="tmpl-name">${t.label}</div>
5555        <div class="tmpl-desc" style="font-size:.55rem;color:var(--muted)">Not set</div>
5556      </div>`;
5557    }
5558    const isSelected=t.id===selectedPremiumTemplate;
5559    const thumb=t.thumbnail_url;
5560    const adminBtn=isAdmin
5561      ?`<button onclick="previewTemplate('${t.id}',event)" title="Regenerate preview" style="position:absolute;top:4px;right:4px;font-size:.55rem;padding:2px 5px;background:rgba(0,0,0,.65);border:1px solid #555;border-radius:4px;cursor:pointer;color:#fff;z-index:2">🔄</button>`
5562      :'';
5563
5564    if(thumb){
5565      return `<div class="tmpl-card${isSelected?' selected':''}" onclick="selectPremiumTemplate('${t.id}',this)"
5566        style="position:relative;cursor:pointer;background-image:url(${thumb});background-size:cover;background-position:center;padding:0;min-height:110px">
5567        ${adminBtn}
5568        <div class="tmpl-thumb-overlay">
5569          <div class="tmpl-name">${t.label}</div>
5570          <div class="tmpl-desc">Free</div>
5571        </div>
5572      </div>`;
5573    }
5574    return `<div class="tmpl-card${isSelected?' selected':''}" onclick="selectPremiumTemplate('${t.id}',this)" style="position:relative;cursor:pointer">
5575      ${adminBtn}
5576      <div class="tmpl-icon">🎨</div>
5577      <div class="tmpl-name">${t.label}</div>
5578      <div class="tmpl-desc" style="font-size:.58rem;color:var(--muted)">Free</div>
5579    </div>`;
5580  }).join('');
5581}
5582
5583// ─── Template Slot Purchase ───────────────────────────────────────────
5584let _slotPayKey='', _slotPayCurrency='USDT', _slotPaymentId='', _slotPayAddr='';
5585
5586function openSlotModal(key,label,price){
5587  _slotPayKey=key; _slotPayCurrency='USDT'; _slotPaymentId=''; _slotPayAddr='';
5588  $('slot-pay-label').textContent=label;
5589  $('slot-pay-price').textContent=price?`$${price} USDT`:'—';
5590  $('slot-pay-step-initiate').style.display='';
5591  $('slot-pay-step-address').style.display='none';
5592  $('slot-pay-step-done').style.display='none';
5593  $('slot-pay-err').style.display='none';
5594  selectSlotCurrency('USDT');
5595  const ov=$('slot-pay-overlay');
5596  ov.style.display='flex';
5597}
5598
5599function closeSlotModal(){
5600  $('slot-pay-overlay').style.display='none';
5601  if(_slotPaymentId) reloadTemplates();  // refresh grid if payment was initiated
5602}
5603
5604function selectSlotCurrency(c){
5605  _slotPayCurrency=c;
5606  ['USDT','USDC'].forEach(x=>{
5607    const btn=$(`slot-curr-${x.toLowerCase()}`);
5608    if(btn) btn.style.background = x===c?'var(--yellow)':'';
5609    if(btn) btn.style.color = x===c?'#000':'';
5610  });
5611}
5612
5613async function createSlotPayment(){
5614  const btn=$('slot-pay-btn');
5615  btn.disabled=true; btn.textContent='Creating…';
5616  $('slot-pay-err').style.display='none';
5617  try{
5618    const r=await api('POST','/api/payments/create',{template_id:_slotPayKey,currency:_slotPayCurrency});
5619    const d=await r.json();
5620    if(!r.ok){ throw new Error(d.detail||'Payment creation failed'); }
5621    if(d.already_unlocked){
5622      toast('You already own this template slot','ok');
5623      closeSlotModal(); return;
5624    }
5625    _slotPaymentId=d.payment_id;
5626    _slotPayAddr=d.pay_address;
5627    $('slot-pay-amount').textContent=`${d.pay_amount} ${d.pay_currency.toUpperCase()}`;
5628    $('slot-pay-addr').textContent=d.pay_address;
5629    $('slot-pay-step-initiate').style.display='none';
5630    $('slot-pay-step-address').style.display='';
5631  }catch(e){
5632    const el=$('slot-pay-err');
5633    el.textContent=e.message; el.style.display='';
5634  }finally{
5635    btn.disabled=false; btn.textContent='Create Payment Invoice';
5636  }
5637}
5638
5639function copySlotAddress(){
5640  navigator.clipboard.writeText(_slotPayAddr).then(()=>toast('Address copied','ok'));
5641}
5642
5643async function checkSlotPayment(){
5644  const btn=$('slot-check-btn');
5645  const res=$('slot-check-result');
5646  btn.disabled=true; btn.textContent='Checking…';
5647  res.textContent='';
5648  try{
5649    const r=await api('GET','/api/payments/unlocked');
5650    const d=await r.json();
5651    if((d.unlocked||[]).includes(_slotPayKey)){
5652      $('slot-pay-step-address').style.display='none';
5653      $('slot-pay-step-done').style.display='';
5654    }else{
5655      res.style.color='var(--muted)';
5656      res.textContent='Payment not yet confirmed. Please wait a few minutes and try again.';
5657    }
5658  }catch(e){
5659    res.style.color='var(--red)'; res.textContent='Error checking status.';
5660  }finally{
5661    btn.disabled=false; btn.textContent='Check Payment Status';
5662  }
5663}
5664// ─────────────────────────────────────────────────────────────────────
5665
5666async function previewTemplate(id,e){
5667  e.stopPropagation();
5668  const btn=e.currentTarget;
5669  const orig=btn.textContent;
5670  btn.textContent='...';
5671  btn.disabled=true;
5672  try{
5673    const r=await fetch(`/api/admin/templates/${id}/preview`,{
5674      method:'POST',
5675      headers:{Authorization:`Bearer ${TOKEN}`}
5676    });
5677    const d=await r.json();
5678    if(r.ok&&d.thumbnail_url){
5679      // Update cached meta with new thumbnail URL
5680      const meta=_allTemplatesMeta.find(t=>t.id===id);
5681      if(meta)meta.thumbnail_url=d.thumbnail_url+'?v='+Date.now();
5682      renderTemplateGrid();
5683      toast(`Preview updated for ${id}`,'ok');
5684    }else{
5685      toast(d.detail||'Preview failed','err');
5686      btn.textContent=orig;
5687      btn.disabled=false;
5688    }
5689  }catch(err){
5690    toast('Preview request failed','err');
5691    btn.textContent=orig;
5692    btn.disabled=false;
5693  }
5694}
5695
5696
5697function setCarouselStep(n){
5698  for(let i=1;i<=4;i++){const el=document.getElementById(`rstep-${i}`);if(!el)continue;el.className='wiz-step'+(i<n?' done':i===n?' active':'');}
5699  const hints={1:'Upload images or send from Scanner to begin.',2:'Images loaded — pick template & person, then build.',3:'Building carousel…',4:'Carousel ready — download slides or send to Post ↓'};
5700  const h=$('carousel-hint');if(h)h.textContent=hints[n]||'';
5701  if(n===1)resetCarouselPreview();
5702}
5703document.addEventListener('DOMContentLoaded',()=>{
5704  // Swatch color picker
5705  document.querySelectorAll('.swatch').forEach(s=>{
5706    s.addEventListener('click',()=>{
5707      document.querySelectorAll('.swatch').forEach(x=>x.classList.remove('selected'));
5708      s.classList.add('selected');
5709      const tc=document.getElementById('text-color');if(tc)tc.value=s.dataset.color;
5710    });
5711  });
5712  const cf=document.getElementById('carousel-files');
5713  if(cf)cf.addEventListener('change',()=>{
5714    const info=$('carousel-files-info');
5715    if(cf.files.length>0){
5716      setCarouselStep(2);
5717      if(info)info.textContent=`${cf.files.length} image${cf.files.length>1?'s':''} ready: `+Array.from(cf.files).map(f=>f.name).join(', ');
5718    }else if(info)info.textContent='';
5719  });
5720});
5721async function buildCarousel(){
5722  const btn=$('carousel-btn');
5723  btn.innerHTML='<span class="spin"></span> Building...';btn.disabled=true;
5724  const fd=new FormData();
5725  const files=$('carousel-files').files;
5726  if(files.length>
57260){Array.from(files).forEach(f=>fd.append('images',f));}
5727  else if(lastScanCharts.length>0){
5728    lastScanCharts.slice(0,3).forEach(c=>{fd.append('images',b64toBlob(c.chart_b64),`${c.ticker}.png`);});
5729  }else{toast('Run a scan or upload images first','err');btn.innerHTML='⚡ Build';btn.disabled=false;return;}
5730  fd.append('template',selectedTemplate);
5731  const t1=document.getElementById('slide-text-1')?.value||'';
5732  const t2=document.getElementById('slide-text-2')?.value||'';
5733  const t3=document.getElementById('slide-text-3')?.value||'';
5734  if(t1)fd.append('slide_1_text',t1);
5735  if(t2)fd.append('slide_2_text',t2);
5736  if(t3)fd.append('slide_3_text',t3);
5737  fd.append('overlay_font',document.getElementById('overlay-font')?.value||'anton');
5738  fd.append('text_color',document.getElementById('text-color')?.value||'#FFFFFF');
5739  fd.append('font_size',document.getElementById('font-size')?.value||'40');
5740  setCarouselStep(3);
5741  try{
5742    const res=await fetch(API+'/api/carousel/build',{method:'POST',headers:{Authorization:`Bearer ${TOKEN}`},body:fd});
5743    if(!res.ok){const e=await res.json();toast('Error: '+(e.detail||'Failed'),'err');
5743setCarouselStep(2);return;}
5744    const d=await res.json();
5745    lastCarouselSlides=d.slides_b64||[];
5746    const _slideUrls=lastCarouselSlides.map(b64=>URL.createObjectURL(b64toBlob(b64)));
5747    const _urlsJson=JSON.stringify(_slideUrls);
5748    $('carousel-preview').innerHTML=lastCarouselSlides.map((b64,i)=>`
5749      <div><div class="slide-thumb" onclick="openLightbox(${_urlsJson},${i})" style="cursor:pointer"><img src="data:image/png;base64,${b64}"></div>
5750      <div style="font-size:.62rem;color:var(--muted);text-align:center;margin-top:4px;font-family:var(--mono)">Slide ${i+1}</div></div>`).join('');
5751    $('carousel-caption-preview').value='';
5752    const dlLinks=lastCarouselSlides.map((b64,i)=>{
5753      const url=URL.createObjectURL(b64toBlob(b64));
5754      return `<a href="${url}" download="slide_${i+1}.png" class="btn btn-ghost" style="font-size:.72rem">⬇ Slide ${i+1}</a>`;
5755    }).join('');
5756    $('carousel-actions').innerHTML=dlLinks
5757      +`<button class="btn btn-primary" onclick="sendCarouselToPost()">📤 Use in Post →</button>`
5758      +`<button class="btn btn-ghost" onclick="resetCarouselBuild()" style="margin-left:auto">↺ New Build</button>`;
5759    setCarouselStep(4);
5760    toast('Carousel built!','ok');
5761  }catch(e){toast('Connection error','err');setCarouselStep(2);}
5762  finally{btn.innerHTML='⚡ Build';btn.disabled=false;}
5763}
5764function resetCarouselBuild(){
5765  lastCarouselSlides=[];
5766  const cf=$('carousel-files');
5767  if(cf){cf.value='';const info=$('carousel-files-info');if(info)info.textContent='';}
5768  $('carousel-actions').innerHTML='';
5769  $('carousel-caption-preview').value='';
5770  ['slide-text-1','slide-text-2','slide-text-3'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';});
5771  const of=document.getElementById('overlay-font');if(of)of.value='impact';
5772  document.querySelectorAll('.swatch').forEach(s=>s.classList.toggle('selected',s.dataset.color==='#FFFFFF'));
5773  const tc=document.getElementById('text-color');if(tc)tc.value='#FFFFFF';
5774  const fs=document.getElementById('font-size');if(fs){fs.value=40;const fv=document.getElementById('font-size-val');if(fv)fv.textContent='40';}
5775  setCarouselStep(1);
5776}
5777function sendCarouselToPost(){
5778  if(!lastCarouselSlides.length)return;
5779  const file=new File([b64toBlob(lastCarouselSlides[0])],'carousel_slide1.png',{type:'image/png'});
5780  const dt=new DataTransfer();dt.items.add(file);
5781  $('post-file').files=dt.files;previewPost();
5782  const cap=$('carousel-caption-preview').value.trim();
5783  if(cap){$('post-caption').value=cap;syncCaption();}
5784  go('post');toast('Sent to Post','ok');
5785}
5786async function uploadAndGetURLs(){
5787  if(!lastCarouselSlides.length){toast('Build carousel first','err');return;}
5788  const btn=$('carousel-upload-btn');btn.innerHTML='<span class="spin"></span>';btn.disabled=true;
5789  const fd=new FormData();
5790  lastCarouselSlides.forEach((b64,i)=>fd.append('images',b64toBlob(b64),`slide_${i+1}.png`));
5791  fd.append('template',selectedTemplate);fd.append('upload_slides','true');
5792  try{
5793    const res=await fetch(API+'/api/carousel/build',{method:'POST',headers:{Authorization:`Bearer ${TOKEN}`},body:fd});
5794    if(res.ok){const d=await res.json();toast(`${d.slide_count} slides uploaded`,'ok');}
5795  }catch(e){toast('Upload failed','err');}
5796  finally{btn.innerHTML='☁️ Upload';btn.disabled=false;}
5797}
5798
5799// ── TEMPLATED.IO INTEGRATION ─────────────────────────
5800async function checkPremiumStatus(){
5801  try{
5802    const res=await api('GET','/api/templated/status');
5803    if(!res.ok)return;
5804    const d=await res.json();
5805    const txt=$('premium-status-text');
5806    const buildForm=$('premium-build-form');
5807    const guide=$('premium-setup-guide');
5808
5809    if(!d.configured){
5810      if(USER.tier==='admin'||USER.tier==='super_admin'){
5811        txt.innerHTML='<span style="color:var(--red)">⚠ Carousel engine not configured</span> — set required env vars in Railway';
5812        guide.style.display='block';
5813      } else {
5814        txt.innerHTML='<span style="color:var(--yellow)">Carousel generation unavailable</span>';
5815      }
5816      buildForm.style.display='none';
5817      return;
5818    }
5819
5820    const tr=d.templates_ready||{};
5821    const anyReady=Object.values(tr).some(v=>v);
5822    txt.innerHTML='<span style="color:var(--green)">● Carousel engine ready</span>';
5823
5824    if(!anyReady){
5825      if(USER.tier==='admin')guide.style.display='block';
5826      buildForm.style.display='none';
5827      txt.innerHTML+=' — <span style="color:var(--yellow)">no templates configured yet</span>';
5828      return;
5829    }
5830    guide.style.display='none';buildForm.style.display='block';
5831
5832    // Load template metadata, then render card grid
5833    await loadTemplatesMeta();
5834    renderTemplateGrid();
5835
5836    // Pre-fill captions from brand kit + show logo indicator
5837    try{
5838      const br=await api('GET','/api/brand');
5839      if(br.ok){
5840        const bd=await br.json();
5841        if(bd.name){[1,2,3].forEach(i=>{const el=$(`premium-caption-${i}`);if(el&&!el.value)el.value=bd.name;});}
5842        if(bd.logo_url){
5843          const row=$('premium-logo-row'),img=$('premium-logo-preview');
5844          if(row&&img){img.src=bd.logo_url;row.style.display='flex';}
5845        }
5846      }
5847    }catch(_){}
5848
5849  }catch(e){console.error('Carousel status check failed',e);}
5850}
5851
5852async function buildPremiumCarousel(){
5853  const template=selectedPremiumTemplate;
5854  const btn=$('premium-build-btn');
5855  const resultEl=$('premium-build-result');
5856
5857  async function fileToB64(file){
5858    if(!file)return null;
5859    return new Promise(res=>{
5860      const r=new FileReader();r.onload=e=>res(e.target.result.split(',')[1]);r.readAsDataURL(file);
5861    });
5862  }
5863
5864  async function urlToB64(url){
5865    const resp=await fetch(url);
5866    const blob=await resp.blob();
5867    return new Promise(res=>{
5868      const r=new FileReader();r.onload=e=>res(e.target.result.split(',')[1]);r.readAsDataURL(blob);
5869    });
5870  }
5871
5872  // Collect slide images — gallery URL picks are fetched and converted to b64 client-side
5873  const slideB64s=[];
5874  for(let i=1;i<=3;i++){
5875    const sel=_slideSelections[i];
5876    if(sel&&sel.type==='url'){
5877      const b64=await urlToB64(sel.value);
5878      if(b64) slideB64s.push(b64);
5879      else break;
5880    } else {
5881      const f=$(`premium-slide${i}`)?.files[0];
5882      const b64=await fileToB64(f);
5883      if(b64) slideB64s.push(b64);
5884      else break;
5885    }
5886  }
5887
5888  const hasAny=slideB64s.length>0;
5889  if(!hasAny){
5890    toast('Upload or pick at least one slide image','err');return;
5891  }
5892
5893  btn.innerHTML='<span class="spin"></span> Rendering...';btn.disabled=true;
5894  resultEl.innerHTML=`<div style="font-size:.72rem;color:var(--muted)">Generating carousel... ~5–10 seconds</div>`;
5895
5896  try{
5897    const captions=[1,2,3].map(i=>($(`premium-caption-${i}`)?.value||'').trim());
5898    const res=await api('POST','/api/templated/render',{
5899      template,
5900      slides_b64: slideB64s,
5901      captions,
5902    });
5903    const d=await res.json();
5904    if(!res.ok){resultEl.innerHTML=`<span style="color:var(--red)">${d.detail||'Render failed'}</span>`;return;}
5905
5906    const slides=d.slides||[];
5907    const _allSlideUrls=slides.map(s=>s.cloudinary_url||s.render_url);
5908    const _allSlideUrlsJson=JSON.stringify(_allSlideUrls);
5909    $('premium-preview-grid').innerHTML=slides.map((s,i)=>`
5910      <div>
5911        <div class="slide-thumb" style="cursor:pointer" onclick="openLightbox(${_allSlideUrlsJson},${i})"><img src="${s.cloudinary_url||s.render_url}" onerror="this.src='${s.render_url}'"></div>
5912      </div>
5913    `).join('');
5914
5915    resultEl.innerHTML=`<span style="color:var(--green)">✓ ${slides.length} slides rendered!</span>`;
5916    toast('Carousel ready!','ok');
5917  }catch(e){resultEl.innerHTML=`<span style="color:var(--red)">Connection error</span>`;}
5918  finally{btn.innerHTML='✨ Generate Carousel';btn.disabled=false;}
5919}
5920
5921// ── COLLAGE ───────────────────────────────────
5922function setCollageStep(n){
5923  for(let i=1;i<=4;i++){const el=document.getElementById(`cstep-${i}`);if(!el)continue;el.className='wiz-step'+(i<n?' done':i===n?' active':'');}
5924  const hints={1:'Upload charts or send from Scanner to begin.',2:'Charts loaded — pick orientation & export target, then generate.',3:'Generating collage…',4:'Collage ready — download or send to Post ↓'};
5925  const h=$('collage-hint');if(h)h.textContent=hints[n]||'';
5926}
5927document.addEventListener('DOMContentLoaded',()=>{
5928  const cf=document.getElementById('collage-files');
5929  if(cf)cf.addEventListener('change',()=>{
5930    const info=$('collage-files-info');
5931    if(cf.files.length>0){
5932      setCollageStep(2);
5933      if(info)info.textContent=`${cf.files.length} image${cf.files.length>1?'s':''} ready: `+Array.from(cf.files).map(f=>f.name).join(', ');
5934    }else if(info)info.textContent='';
5935  });
5936});
5937async function buildCollage(){
5938  const orientation=$('collage-orientation').value,exportTarget=$('collage-export').value;
5939  const btn=$('collage-btn');btn.innerHTML='<span class="spin"></span> Generating...';btn.disabled=true;
5940  const fd=new FormData();
5941  const files=$('collage-files').files;
5942  if(files.length>
59420)Array.from(files).forEach(f=>fd.append('images',f));
5943  else if(lastScanCharts.length>0)lastScanCharts.slice(0,10).forEach(c=>fd.append('images',b64toBlob(c.chart_b64),`${c.ticker}.png`));
5944  else{toast('Upload images or run a scan first','err');btn.innerHTML='🧩 Generate Collage';btn.disabled=false;return;}
5945  fd.append('orientation',orientation);fd.append('export_target',exportTarget);
5946  setCollageStep(3);
5947  try{
5948    const res=await fetch(API+'/api/collage/generate',{method:'POST',headers:{Authorization:`Bearer ${TOKEN}`},body:fd});
5949    if(!res.ok){const e=await res.json();toast('Error: '+(e.detail||'Failed'),'err');setCollageStep(2);return;}
5950    const blob=await res.blob();lastCollageBlob=blob;
5951    const url=URL.createObjectURL(blob);
5952    $('collage-preview').innerHTML=`<img src="${url}" style="width:100%;border-radius:8px;border:1px solid var(--border)">`;
5953    $('collage-actions').innerHTML=`<a href="${url}" download="factoz_collage.png" class="btn btn-green">⬇ Download</a><button class="btn btn-primary" onclick="sendCollageToPost()">📤 Send to Post →</button>`;
5954    setCollageStep(4);
5955    toast('Collage generated!','ok');
5956    setTimeout(loadMyCollages, 4000);
5957  }catch(e){toast('Connection error','err');setCollageStep(2);}
5958  finally{btn.innerHTML='🧩 Generate Collage';btn.disabled=false;}
5959}
5960function sendCollageToPost(){
5961  if(!lastCollageBlob)return;
5962  const file=new File([lastCollageBlob],'collage.png',{type:'image/png'});
5963  const dt=new DataTransfer();dt.items.add(file);
5964  $('post-file').files=dt.files;previewPost();go('post');toast('Sent to Post','ok');
5965}
5966
5967// ── POST ──────────────────────────────────────
5968function previewPost(){const file=$('post-file').files[0];if(!file)return;$('post-img-preview').innerHTML=`<img src="${URL.createObjectURL(file)}" style="width:100%;border-radius:8px;border:1px solid var(--border);max-height:180px;object-fit:contain">`;}
5969function syncCaption(){const v=$('post-caption').value;$('caption-preview').textContent=v||'Start typing...';const el=$('char-count');el.textContent=`${v.length} / 280`;el.style.color=v.length>260?'var(--red)':'var(--muted)';}
5970async function quickCaption(){
5971  const fileInput=$('post-file');
5972  if(!fileInput?.files?.[0]){toast('Upload an image first to generate a caption','err');return;}
5973  const tickers=($('quick-tickers').value||'').trim();
5974  const btn=document.querySelector('[onclick="quickCaption()"]');
5975  const origLabel=btn.innerHTML;
5976  btn.disabled=true;btn.innerHTML='<span class="spin"></span> Analyzing...';
5977  try{
5978    const fd=new FormData();
5979    fd.append('image',fileInput.files[0]);
5980    if(tickers)fd.append('tickers',tickers);
5981    fd.append('tone','professional');
5982    const res=await fetch('/api/post/caption',{method:'POST',headers:hdr(),body:fd});
5983    const d=await res.json();
5984    if(res.status===429){
5985      const msg=d.detail?.message||'Daily AI limit reached (5/day on Creator plan). Upgrade to Pro for unlimited.';
5986      toast(msg,'err');
5987      return;
5988    }
5989    if(!res.ok){toast((typeof d.detail==='string'?d.detail:d.detail?.message)||'Caption failed','err');return;}
5990    $('post-caption').value=d.caption||'';
5991    syncCaption();
5992    const rem=d.remaining;
5993    toast(rem!=null?`Caption generated! (${rem} left this week)`:'Caption generated!','ok');
5994  }catch(e){toast('Error generating caption','err');}
5995  finally{btn.disabled=false;btn.innerHTML=origLabel;}
5996}
5997async function doPost(){
5998  const file=$('post-file').files[0],caption=$('post-caption').value;
5999  const postX=$('post-x').checked,postIg=$('post-ig').checked;
6000  if(!file){toast('Upload an image first','err');return;}
6001  if(!caption){toast('Write a caption first','err');return;}
6002  if(!postX&&!postIg){toast('Select at least one platform','err');return;}
6003  const btn=$('post-btn');btn.innerHTML='<span class="spin"></span> Posting...';btn.disabled=true;
6004  const platforms=[postX&&'x',postIg&&'instagram'].filter(Boolean).join(',');
6005  const fd=new FormData();fd.append('image',file);fd.append('caption',caption);fd.append('platforms',platforms);
6006  try{
6007    const res=await fetch(API+'/api/post/all',{method:'POST',headers:{Authorization:`Bearer ${TOKEN}`},body:fd});
6008    const d=await res.json();
6009    if(!res.ok){$('post-result').innerHTML=`<div class="card"><p style="color:var(--red)">${d.detail}</p></div>`;return;}
6010    $('post-result').innerHTML=(d.results||[]).map(r=>`
6011      <div class="card" style="padding:10px 14px;margin-bottom:8px">
6012        <div style="display:flex;align-items:center;justify-content:space-between">
6013          <span style="font-weight:700">${r.platform}</span>
6014          <span class="badge ${r.success?'badge-green':'badge-red'}">${r.success?'✓ Posted':'✗ Failed'}</span>
6015        </div>
6016        ${r.url?`<a href="${r.url}" target="_blank" style="color:var(--teal);font-size:.72rem">${r.url}</a>`:''}
6017        ${r.error?`<div style="font-size:.72rem;color:var(--red);margin-top:4px">${r.error}</div>`:''}
6018      </div>`).join('');
6019    toast((d.results||[]).every(r=>r.success)?'Posted!':'Some posts failed',(d.results||[]).every(r=>r.success)?'ok':'err');
6020  }catch(e){toast('Connection error','err');}
6021  finally{btn.innerHTML='📤 Post Now';btn.disabled=false;}
6022}
6023
6024// ── EARNINGS WEEKLY CALENDAR ──────────────────
6025async function loadEarningsCalendar(){
6026  const el=$('news-list');
6027  el.innerHTML='<div class="card"><p>Loading earnings calendar…</p></div>';
6028  try{
6029    const res=await api('GET','/api/news/earnings-week');
6030    const d=await res.json();
6031    if(!res.ok){el.innerHTML=`<div class="card"><p style="color:var(--red)">${d.detail||'Failed'}</p></div>`;return;}
6032    renderEarningsCalendar(d);
6033  }catch(e){el.innerHTML='<div class="card"><p style="color:var(--red)">Connection error</p></div>';}
6034}
6035
6036function renderEarningsCalendar(d){
6037  const el=$('news-list');
6038  const today=d.today;
6039  const dayMap={};
6040  const monday=new Date(d.week_start+'T00:00:00');
6041  const days=[];
6042  for(let i=0;i<5;i++){
6043    const dt=new Date(monday);
6044    dt.setDate(monday.getDate()+i);
6045    const iso=dt.toISOString().split('T')[0];
6046    const label=dt.toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric'});
6047    days.push({iso,label});
6048    dayMap[iso]={bmo:[],amc:[],dmh:[]};
6049  }
6050  for(const e of d.entries){
6051    const bucket=dayMap[e.date];
6052    if(!bucket)continue;
6053    const h=e.hour||'dmh';
6054    (bucket[h]||bucket.dmh).push(e);
6055  }
6056  let html=`<div style="overflow-x:auto"><div style="display:grid;grid-template-columns:repeat(5,minmax(180px,1fr));gap:10px;min-width:900px">`;
6057  for(const day of days){
6058    const isToday=day.iso===today;
6059    const bucket=dayMap[day.iso];
6060    const totalCount=bucket.bmo.length+bucket.amc.length+bucket.dmh.length;
6061    const summaryStyle=`padding:9px 12px;background:var(--bg3);font-size:.88rem;font-weight:700;color:${isToday?'var(--teal)':'var(--text)'};cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center`;
6062    html+=`<details open style="background:var(--bg2);border-radius:10px;overflow:hidden;border:1px solid ${isToday?'var(--teal)':'var(--border)'};min-width:0">
6063      <summary style="${summaryStyle}">
6064        <span>${day.label}${isToday?' <span style="font-size:.6rem;opacity:.7">TODAY</span>':''}</span>
6065        <span style="font-size:.6rem;opacity:.45;font-weight:400">${totalCount||'—'}</span>
6066      </summary>`;
6067    if(bucket.bmo.length){
6068      html+=`<details open style="border-top:1px solid var(--border)">
6069        <summary style="padding:6px 10px;font-size:.68rem;color:var(--text2);cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center">
6070          <span>☀️ BEFORE OPEN</span><span style="opacity:.5">${bucket.bmo.length}</span>
6071        </summary>
6072        <div style="padding:4px 8px 8px;display:flex;flex-direction:column;gap:3px">${bucket.bmo.map(earningsPill).join('')}</div>
6073      </details>`;
6074    }
6075    if(bucket.amc.length){
6076      html+=`<details open style="border-top:1px solid var(--border)">
6077        <summary style="padding:6px 10px;font-size:.68rem;color:var(--text2);cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center">
6078          <span>🌙 AFTER CLOSE</span><span style="opacity:.5">${bucket.amc.length}</span>
6079        </summary>
6080        <div style="padding:4px 8px 8px;display:flex;flex-direction:column;gap:3px">${bucket.amc.map(earningsPill).join('')}</div>
6081      </details>`;
6082    }
6083    if(!bucket.bmo.length&&!bucket.amc.length){
6084      if(bucket.dmh.length){
6085        html+=`<details open style="border-top:1px solid var(--border)">
6086          <summary style="padding:6px 10px;font-size:.68rem;color:var(--text2);cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center">
6087            <span>📊 DURING MARKET</span><span style="opacity:.5">${bucket.dmh.length}</span>
6088          </summary>
6089          <div style="padding:4px 8px 8px;display:flex;flex-direction:column;gap:3px">${bucket.dmh.map(earningsPill).join('')}</div>
6090        </details>`;
6091      }else{
6092        html+=`<div style="padding:12px 8px;font-size:.65rem;color:var(--muted);text-align:center">No earnings</div>`;
6093      }
6094    }else if(bucket.dmh.length){
6095      html+=`<details open style="border-top:1px solid var(--border)">
6096        <summary style="padding:6px 10px;font-size:.68rem;color:var(--text2);cursor:pointer;list-style:none;display:flex;justify-content:space-between;align-items:center">
6097          <span>📊 DURING</span><span style="opacity:.5">${bucket.dmh.length}</span>
6098        </summary>
6099        <div style="padding:4px 8px 8px;display:flex;flex-direction:column;gap:3px">${bucket.dmh.map(earningsPill).join('')}</div>
6100      </details>`;
6101    }
6102    html+=`</details>`;
6103  }
6104  html+=`</div></div>`;
6105  el.innerHTML=html;
6106}
6107
6108function earningsPill(e){
6109  const hasActual=e.eps_actual!=null;
6110  const beat=hasActual&&e.surprise_pct!=null&&e.surprise_pct>=0;
6111  const sign=(e.surprise_pct||0)>0?'+':'';
6112  const color=hasActual?(beat?'var(--green)':'var(--red)'):'var(--muted)';
6113  const badge=hasActual
6114    ?`<span style="font-size:.6rem;padding:1px 6px;border-radius:3px;background:${beat?'rgba(0,200,80,.15)':'rgba(255,60,60,.15)'};color:${beat?'var(--green)':'var(--red)'};font-weight:700">${beat?'BEAT':'MISS'}${e.surprise_pct!=null?' '+sign+e.surprise_pct.toFixed(1)+'%':''}</span>`
6115    :'';
6116  const epsStr=hasActual
6117    ?`EPS <span style="color:${color};font-weight:700">$${e.eps_actual.toFixed(2)}</span>`
6118    :(e.eps_estimate!=null?`EPS est <b>$${e.eps_estimate.toFixed(2)}</b>`:'');
6119  const fmtRev=v=>v==null?null:Math.abs(v)>=1e9?'$'+(v/1e9).toFixed(1)+'B':'$'+(v/1e6).toFixed(0)+'M';
6120  const revVal=hasActual?fmtRev(e.rev_actual):fmtRev(e.rev_estimate);
6121  const revStr=revVal?` · Rev <span style="color:${hasActual?color:'inherit'}">${revVal}${!hasActual?' est':''}</span>`:'';
6122  const safeE=JSON.stringify(e).replace(/"/g,'&quot;');
6123  return `<div onclick="openEarningsDetail(JSON.parse(this.dataset.e))" data-e="${safeE}"
6124    style="padding:6px 8px;border-radius:6px;cursor:pointer;background:var(--bg3);
6125           transition:background .15s;border:1px solid var(--border)"
6126    onmouseover="this.style.borderColor='var(--teal)'"
6127    onmouseout="this.style.borderColor='var(--border)'">
6128    <div style="display:flex;align-items:center;justify-content:space-between;gap:4px">
6129      <span style="font-size:.78rem;font-weight:700">${e.ticker}</span>${badge}
6130    </div>
6131    ${epsStr||revStr?`<div style="font-size:.65rem;color:var(--text2);margin-top:3px">${epsStr}${revStr}</div>`:''}
6132  </div>`;
6133}
6134
6135function openEarningsDetail(e){
6136  const hasActual=e.eps_actual!=null;
6137  const beat=hasActual&&e.surprise_pct!=null&&e.surprise_pct>=0;
6138  const sign=(e.surprise_pct||0)>0?'+':'';
6139  const fmtRev=v=>v==null?'—':Math.abs(v)>=1e9?'$'+(v/1e9).toFixed(2)+'B':'$'+(v/1e6).toFixed(0)+'M';
6140  const period=e.quarter?`Q${e.quarter} ${e.year||''}`:'';
6141  const hourLabel=e.hour==='bmo'?'☀️ Before Open':e.hour==='amc'?'🌙 After Close':'📊 During Market';
6142  $('earnings-detail-title').textContent=e.ticker;
6143  $('earnings-detail-body').innerHTML=`
6144    <div style="display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
6145      ${period?`<span style="font-size:.7rem;background:var(--surface);padding:2px 8px;border-radius:12px;border:1px solid var(--border)">${period}</span>`:''}
6146      <span style="font-size:.7rem;color:var(--muted)">${e.date} · ${hourLabel}</span>
6147      ${hasActual?`<span style="font-size:.7rem;padding:2px 8px;border-radius:12px;font-weight:700;background:${beat?'rgba(0,200,80,.15)':'rgba(255,60,60,.15)'};color:${beat?'var(--green)':'var(--red)'}">${beat?'✓ BEAT':'✗ MISS'}${e.surprise_pct!=null?' '+sign+e.surprise_pct.toFixed(2)+'%':''}</span>`:''}
6148    </div>
6149    <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
6150      <div style="background:var(--bg3);border-radius:8px;padding:12px;border:1px solid var(--border)">
6151        <div style="font-size:.65rem;color:var(--text2);margin-bottom:8px;font-weight:600">EPS</div>
6152        <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
6153          <div><div style="font-size:.62rem;color:var(--text2)">ESTIMATE</div>
6154               <div style="font-size:1rem;font-weight:700">${e.eps_estimate!=null?'$'+e.eps_estimate.toFixed(2):'—'}</div></div>
6155          <div><div style="font-size:.62rem;color:var(--text2)">ACTUAL</div>
6156               <div style="font-size:1rem;font-weight:700;color:${hasActual?(beat?'var(--green)':'var(--red)'):'var(--text)'}">${hasActual?'$'+e.eps_actual.toFixed(2):'⏳'}</div></div>
6157        </div>
6158      </div>
6159      <div style="background:var(--bg3);border-radius:8px;padding:12px;border:1px solid var(--border)">
6160        <div style="font-size:.65rem;color:var(--text2);margin-bottom:8px;font-weight:600">REVENUE</div>
6161        <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
6162          <div><div style="font-size:.62rem;color:var(--text2)">ESTIMATE</div>
6163               <div style="font-size:1rem;font-weight:700">${fmtRev(e.rev_estimate)}</div></div>
6164          <div><div style="font-size:.62rem;color:var(--text2)">ACTUAL</div>
6165               <div style="font-size:1rem;font-weight:700;color:${e.rev_actual!=null?(beat?'var(--green)':'var(--red)'):'var(--text)'}">${e.rev_actual!=null?fmtRev(e.rev_actual):'⏳'}</div></div>
6166        </div>
6167      </div>
6168    </div>
6169    ${!hasActual?`<div style="margin-top:12px;text-align:center;font-size:.7rem;color:var(--muted)">⏳ Awaiting results — actuals refresh hourly 4–9pm EST</div>`:''}
6170    <div id="eps-history-chart" style="margin-top:14px">
6171      <div style="font-size:.6rem;color:var(--muted)">Loading EPS history…</div>
6172    </div>`;
6173  $('earnings-detail-modal').style.display='flex';
6174  loadEpsHistory(e.ticker);
6175}
6176
6177async function loadEpsHistory(ticker){
6178  const el=$('eps-history-chart');
6179  if(!el)return;
6180  try{
6181    const res=await api('GET',`/api/news/earnings-history?ticker=${encodeURIComponent(ticker)}`);
6182    const d=await res.json();
6183    if(!res.ok||!d.quarters||!d.quarters.length){el.innerHTML='';return;}
6184    el.innerHTML=renderEpsHistoryChart(d.quarters);
6185  }catch(_){el.innerHTML='';}
6186}
6187
6188function renderEpsHistoryChart(quarters){
6189  const qs=[...quarters].reverse();
6190  const vals=[...qs.flatMap(q=>[q.actual,q.estimate]).filter(v=>v!=null)];
6191  if(!vals.length)return'';
6192  const minV=Math.min(...vals);
6193  const maxV=Math.max(...vals);
6194  const range=maxV-minV||1;
6195  const W=440,H=110,padL=36,padR=8,padT=10,padB=32;
6196  const colW=(W-padL-padR)/qs.length;
6197  const barW=Math.min(18,colW*0.35);
6198  const yScale=v=>padT+(H-padT-padB)*(1-(v-minV)/range);
6199  let bars='',axes='',labels='';
6200  for(let i=0;i<=4;i++){
6201    const v=minV+range*i/4;
6202    const y=yScale(v);
6203    axes+=`<line x1="${padL-4}" y1="${y}" x2="${W-padR}" y2="${y}" stroke="rgba(255,255,255,.06)" stroke-width="1"/>`;
6204    axes+=`<text x="${padL-6}" y="${y+4}" text-anchor="end" font-size="8" fill="rgba(255,255,255,.35)">${v<0?'-':''}$${Math.abs(v)<10?Math.abs(v).toFixed(1):Math.round(Math.abs(v))}</text>`;
6205  }
6206  if(minV<0&&maxV>0){
6207    const yz=yScale(0);
6208    axes+=`<line x1="${padL}" y1="${yz}" x2="${W-padR}" y2="${yz}" stroke="rgba(255,255,255,.2)" stroke-width="1" stroke-dasharray="3,3"/>`;
6209  }
6210  qs.forEach((q,i)=>{
6211    const cx=padL+i*colW+colW/2;
6212    const beat=q.actual!=null&&q.surprise_pct!=null&&q.surprise_pct>=0;
6213    const actColor=q.actual!=null?(beat?'#22c55e':'#ef4444'):'#555';
6214    if(q.estimate!=null){
6215      const ye=yScale(q.estimate);
6216      bars+=`<circle cx="${cx-barW*0.6}" cy="${ye}" r="${barW*0.55}" fill="#60a5fa" opacity=".75"/>`;
6217    }
6218    if(q.actual!=null){
6219      const ya=yScale(q.actual);
6220      bars+=`<circle cx="${cx+barW*0.6}" cy="${ya}" r="${barW*0.55}" fill="${actColor}" opacity=".9"/>`;
6221    }
6222    const lbl=q.quarter&&q.year?`Q${q.quarter}'${String(q.year).slice(2)}`:(q.period||'').slice(0,7);
6223    labels+=`<text x="${cx}" y="${H-padB+14}" text-anchor="middle" font-size="8.5" fill="rgba(255,255,255,.45)">${lbl}</text>`;
6224    if(q.surprise_pct!=null){
6225      const sc=q.surprise_pct>=0?'#22c55e':'#ef4444';
6226      labels+=`<text x="${cx}" y="${H-padB+25}" text-anchor="middle" font-size="7.5" fill="${sc}">${q.surprise_pct>=0?'+':''}${q.surprise_pct.toFixed(1)}%</text>`;
6227    }
6228  });
6229  return `<div style="border-top:1px solid var(--border);padding-top:12px">
6230    <div style="font-size:.65rem;font-weight:600;color:var(--muted);margin-bottom:5px">Historical EPS Surprises</div>
6231    <div style="display:flex;gap:12px;margin-bottom:6px;font-size:.58rem;color:var(--muted)">
6232      <span>🔵 Estimate</span><span style="color:#22c55e">🟢 Actual (Beat)</span><span style="color:#ef4444">🔴 Actual (Miss)</span>
6233    </div>
6234    <svg width="100%" viewBox="0 0 ${W} ${H}" style="overflow:visible">${axes}${bars}${labels}</svg>
6235  </div>`;
6236}
6237// ──────────────────────────────────────────────
6238
6239// ── CALENDAR ──────────────────────────────────
6240function renderCalendar(){
6241  window.location.href='/calendar.html';
6242}
6243
6244// ── LOCAL FILES ───────────────────────────────
6245async function uploadLocalPageFiles(input){
6246  const status = $('files-upload-status');
6247  const goBtn  = $('files-go-gallery');
6248  const files  = [...input.files];
6249  if(!files.length) return;
6250  status.textContent = `Uploading ${files.length} file(s)…`;
6251  goBtn.style.display = 'none';
6252  let ok = 0;
6253  for(const f of files){
6254    const fd = new FormData(); fd.append('file', f);
6255    try{
6256      const res = await fetch('/api/gallery/upload', {
6257        method:'POST', headers:{Authorization:'Bearer '+localStorage.getItem('fz_token')}, body: fd
6258      });
6259      if(res.ok) ok++;
6260    }catch(e){}
6261  }
6262  status.textContent = `${ok}/${files.length} uploaded successfully.`;
6263  input.value = '';
6264  if(ok > 0) goBtn.style.display = 'block';
6265}
6266
6267// ── BRAND ─────────────────────────────────────
6268// ── BRAND KIT ─────────────────────────────────
6269function updateBrandPreview(){
6270  const bg=$('b-bg')?.value||'#0D1117';
6271  const acc=$('b-acc')?.value||'#00BCD4';
6272  const txt=$('b-txt')?.value||'#E8F0F7';
6273  const name=($('b-name')?.value||'FACTOZ').toUpperCase();
6274  const tagline=($('b-tagline')?.value||'MARKET INTELLIGENCE').toUpperCase();
6275  const wm=$('b-wm')?.value||'FACTOZ';
6276  const card=$('brand-preview-card');if(!card)return;
6277  card.style.background=bg;card.style.borderColor=acc;
6278  $('bp-accent-top').style.background=acc;
6279  $('bp-name').style.color=txt;$('bp-name').textContent=name;
6280  $('bp-tagline').style.color=acc;$('bp-tagline').textContent=tagline;
6281  $('bp-signal').style.color=txt;
6282  $('bp-sub').style.color=acc;
6283  $('bp-divider').style.background=acc;
6284  $('bp-wm').textContent=wm;
6285  $('bp-dot').style.background=acc;
6286  $('bp-corner').style.backgroundImage=`linear-gradient(135deg,transparent 50%,${acc} 50%)`;
6287  $('chip-bg').style.background=bg;
6288  $('chip-acc').style.background=acc;
6289  $('chip-txt').style.background=txt;
6290}
6291
6292async function loadBrand(){
6293  try{
6294    const res=await api('GET','/api/brand');if(!res.ok)return;const d=await res.json();
6295    $('b-name').value=d.name||'';$('b-tagline').value=d.tagline||'';
6296    $('b-bg').value=d.bg_color||'#0D1117';$('b-bg-pick').value=d.bg_color||'#0D1117';
6297    $('b-acc').value=d.accent_color||'#00BCD4';$('b-acc-pick').value=d.accent_color||'#00BCD4';
6298    $('b-txt').value=d.text_color||'#E8F0F7';
6298if($('b-txt-pick'))$('b-txt-pick').value=d.text_color||'#E8F0F7';
6299    $('b-wm').value=d.watermark_text||'';
6300    const op=d.watermark_opacity??28;
6301    $('b-wm-op').value=op;$('b-wm-op-label').textContent=Math.round(op/2.55)+'%';
6302    $('b-show-wm').checked=d.show_watermark!==false;$('b-show-tag').checked=d.show_corner_tag!==false;
6303    $('b-hashtags').value=d.default_hashtags||'';$('b-template').value=d.default_template||'factoz';
6304    $('b-export').value=d.default_export_target||'instagram_square';
6305    // Logo preview (Bug 3 fix)
6306    if(d.logo_url){
6307      $('b-logo-preview').src=d.logo_url;$('b-logo-preview').style.display='block';
6308      $('b-logo-hint').textContent='Click to replace logo';
6309      $('bp-logo').src=d.logo_url;$('bp-logo').style.display='block';
6310    }
6311    // Auto-select carousel template (Bug 1 fix)
6312    if(d.default_template){
6313      const tmplCard=document.querySelector(`.tmpl-card[data-tmpl="${d.default_template}"]`);
6314      if(tmplCard)selectTemplate(d.default_template,tmplCard);
6315    }
6316    updateBrandPreview();
6317  }catch(e){}
6318}
6319
6320async function saveBrand(){
6321  const btn=$('brand-save-btn');btn.innerHTML='<span class="spin"></span> Saving...';btn.disabled=true;
6322  const body={
6323    name:$('b-name').value,tagline:$('b-tagline').value,
6324    bg_color:$('b-bg').value,accent_color:$('b-acc').value,
6325    border_color:$('b-acc').value,text_color:$('b-txt')?.value||'#E8F0F7',
6326    watermark_text:$('b-wm').value,watermark_opacity:parseInt($('b-wm-op').value),
6327    show_watermark:$('b-show-wm').checked,show_corner_tag:$('b-show-tag').checked,
6328    default_hashtags:$('b-hashtags').value,default_template:$('b-template').value,
6329    default_export_target:$('b-export').value,
6330  };
6331  const res=await api('PUT','/api/brand',body);
6332  btn.innerHTML='💾 Save Brand Kit';btn.disabled=false;
6333  $('brand-save-result').innerHTML=res.ok
6334    ?'<span class="badge badge-green">✓ Brand kit saved — applied to next carousel</span>'
6335    :'<span class="badge badge-red">✗ Save failed</span>';
6336  if(res.ok)toast('Brand kit saved!','ok');else toast('Save failed','err');
6337}
6338
6339async function uploadLogo(){
6340  const file=$('b-logo').files[0];if(!file)return;
6341  $('b-logo-hint').textContent='Uploading...';
6342  const fd=new FormData();fd.append('logo',file);
6343  const res=await fetch(API+'/api/brand/logo',{method:'POST',headers:{Authorization:`Bearer ${TOKEN}`},body:fd});
6344  if(res.ok){
6345    const d=await res.json();
6346    $('b-logo-preview').src=d.logo_url;$('b-logo-preview').style.display='block';
6347    $('b-logo-hint').textContent='Click to replace logo';
6348    $('bp-logo').src=d.logo_url;$('bp-logo').style.display='block';
6349    toast('Logo uploaded!','ok');
6350  }else{
6351    $('b-logo-hint').textContent='Upload failed — try again';
6352    toast('Upload failed','err');
6353  }
6354}
6355
6356// ── API KEYS ──────────────────────────────────
6357async function saveKeys(){
6358  const body={};
6359  [['k-x-api','x_api_key'],['k-x-secret','x_api_secret'],['k-x-token','x_access_token'],['k-x-tsecret','x_access_token_secret']].forEach(([id,key])=>{const v=$(id);if(v&&v.value)body[key]=v.value;});
6360  const res=await api('PUT','/api/auth/keys',body);
6361  $('keys-result').innerHTML=res.ok?'<span class="badge badge-green">✓ Keys saved</span>':'<span class="badge badge-red">✗ Save failed</span>';
6362  if(res.ok)toast('API keys saved!','ok');
6363}
6364
6365// ── CONNECTED ACCOUNTS (SocialBu) ─────────────────────────────────────────
6366const _PROVIDERS = [
6367  { key: 'instagram', label: 'Instagram', icon: '📸' },
6368  { key: 'reddit',    label: 'Reddit',    icon: '🤖' },
6369  { key: 'tiktok',    label: 'TikTok',    icon: '🎵' },
6370  { key: 'linkedin',  label: 'LinkedIn',  icon: '💼' },
6371];
6372
6373let _connectedAccounts = [];
6374let _connectPollInterval = null;
6375
6376async function loadConnectedAccounts(){
6377  const grid = $('connected-accounts-grid');
6378  if(!grid) return;
6379  try{
6380    const res = await api('GET','/api/socialbu/accounts');
6381    if(!res.ok){ grid.innerHTML='<p style="font-size:.75rem;color:var(--text2)">Could not load accounts.</p>'; return; }
6382    const data = await res.json();
6383    _connectedAccounts = data.accounts || [];
6384    const limit   = data.limit   ?? 1;
6385    const used    = data.slots_used ?? _connectedAccounts.length;
6386    const isAdmin = USER.tier === 'admin' || USER.tier === 'super_admin';
6387    const connMap = {};
6388    _connectedAccounts.forEach(a => connMap[a.provider] = a);
6389
6390    // Slot counter
6391    const badge = `<div style="font-size:.72rem;color:var(--text2);margin-bottom:10px">
6392      ${used} of ${isAdmin?'∞':limit} account${limit!==1?'s':''} connected
6393    </div>`;
6394
6395    const rows = _PROVIDERS.map((p) => {
6396      const acct = connMap[p.key];
6397
6398      if(acct){
6399        const disconnectBtn = isAdmin
6400          ? `<button class="btn btn-ghost" style="font-size:.65rem;padding:3px 9px;color:var(--red)"
6401               onclick="disconnectSocialAccount('${acct.id}','${p.label}')">Disconnect</button>`
6402          : '';
6403        return `<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg3)">
6404          <div style="display:flex;align-items:center;gap:10px">
6405            <span style="font-size:1.2rem">${p.icon}</span>
6406            <div>
6407              <div style="font-size:.78rem;font-weight:600">${p.label}</div>
6408              <div style="font-size:.65rem;color:#4caf50">✓ ${acct.account_name||'Connected'}</div>
6409            </div>
6410          </div>
6411          ${disconnectBtn}
6412        </div>`;
6413      }
6414
6415      return `<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg3)">
6416        <div style="display:flex;align-items:center;gap:10px">
6417          <span style="font-size:1.2rem">${p.icon}</span>
6418          <div>
6419            <div style="font-size:.78rem;font-weight:600">${p.label}</div>
6420            <div style="font-size:.65rem;color:var(--text2)">Not connected</div>
6421          </div>
6422        </div>
6423        <button class="btn btn-primary" style="font-size:.68rem;padding:4px 12px"
6424          onclick="connectSocialAccount('${p.key}')">Connect</button>
6425      </div>`;
6426    }).join('');
6427
6428    // X always posts via developer API key — always show the hint
6429    const xHint = `<div style="margin-top:10px;padding:10px 12px;background:var(--bg3);
6430        border-radius:6px;border:1px solid var(--border);font-size:.72rem;color:var(--text2)">
6431      🐦 <strong style="color:var(--text)">X (Twitter)</strong> posts use your developer API key —
6432      add it in <a href="#" onclick="go('keys')" style="color:var(--teal)">API Keys</a>.
6433    </div>`;
6434
6435    grid.innerHTML = badge + `<div style="display:flex;flex-direction:column;gap:8px">${rows}</div>` + xHint;
6436  }catch(e){
6437    grid.innerHTML='<p style="font-size:.75rem;color:var(--text2)">Error loading accounts.</p>';
6438  }
6439}
6440
6441async function connectSocialAccount(provider){
6442  try{
6443    const res = await api('POST','/api/socialbu/connect',{provider});
6444    if(!res.ok){ const e=await res.json(); toast(e.detail||'Connection failed','err'); return; }
6445    const data = await res.json();
6446    const popup = window.open(data.connect_url,'socialbu_connect','width=640,height=720,left=200,top=100');
6447    toast(`Connecting ${provider}... complete the OAuth in the popup`,'ok');
6448    // Listen for popup postMessage or poll every 3s
6449    const handler = (ev) => { if(ev.data==='socialbu_connected'){ clearInterval(_connectPollInterval); loadConnectedAccounts(); toast(`${provider} connected!`,'ok'); window.removeEventListener('message',handler); } };
6450    window.addEventListener('message', handler);
6451    _connectPollInterval = setInterval(async()=>{
6452      if(!popup || popup.closed){ clearInterval(_connectPollInterval); window.removeEventListener('message',handler); await loadConnectedAccounts(); }
6453    }, 3000);
6454  }catch(e){ toast('Connection error','err'); }
6455}
6456
6457async function disconnectSocialAccount(id, label){
6458  if(!confirm(`Disconnect ${label}?`)) return;
6459  const res = await api('DELETE',`/api/socialbu/accounts/${id}`);
6460  if(res.ok){ toast(`${label} disconnected`,'ok'); loadConnectedAccounts(); }
6461  else{ toast('Disconnect failed','err'); }
6462}
6463
6464// ── HISTORY ───────────────────────────────────
6465async function loadHistory(platform='all'){
6466  try{
6467    const path=platform==='all'?'/api/post/history?limit=100':`/api/post/history?limit=100&platform=${platform}`;
6468    const res=await api('GET',path);if(!res.ok)return;
6469    const d=await res.json();const h=d.history||[];
6470    const xOk=h.filter(p=>p.platform==='x'&&p.status==='success').length;
6471    const igOk=h.filter(p=>p.platform==='instagram'&&p.status==='success').length;
6472    $('h-x').textContent=xOk;$('h-ig').textContent=igOk;$('h-tot').textContent=xOk+igOk;
6473    if(!h.length){$('history-list').innerHTML='<div class="card"><p>No posts yet.</p></div>';return;}
6474    $('history-list').innerHTML=h.map(p=>`
6475      <div class="hist-row">
6476        <span class="badge ${p.status==='success'?'badge-green':'badge-red'}">${p.status==='success'?'✓':'✗'}</span>
6477        <span class="hplatform">${p.platform==='x'?'🐦 X':'📸 Instagram'}</span>
6478        <span class="hcaption">${p.caption||'—'}</span>
6479        ${p.post_url?`<a href="${p.post_url}" target="_blank" style="color:var(--teal);font-size:.68rem">View →</a>`:''}
6480        <span class="htime">${p.posted_at?new Date(p.posted_at).toLocaleDateString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):'—'}</span>
6481      </div>`).join('');
6482  }catch(e){}
6483}
6484
6485// ── ADMIN PANEL ───────────────────────────────
6486async function loadAdminPanel(){
6487  try{
6488    const res=await api('GET','/api/admin/clients');
6489    if(!res.ok)return;
6490    const d=await res.json();
6491    const clients=d.clients||[];
6492    $('adm-total').textContent=d.total||0;
6493    $('adm-active').textContent=clients.filter(c=>c.is_active).length;
6494    // Load platform usage
6495    try{
6496      const ur=await api('GET','/api/admin/usage');
6497      if(ur.ok){const u=await ur.json();$('adm-scans').textContent=u.enterprise_clients?.total_scans||0;$('adm-posts').textContent=u.enterprise_clients?.total_posts||0;}
6498    }catch(e){}
6499    if(!clients.length){$('client-list').innerHTML='<div class="card"><p>No enterprise clients yet. Click + New Client to create one.</p></div>';return;}
6500    $('client-list').innerHTML=clients.map(c=>`
6501      <div class="client-card">
6502        <div class="client-card-header">
6503          <div>
6504            <div class="client-name">${c.name}</div>
6505            <div class="client-meta">${c.email} · ${c.tier}</div>
6506          </div>
6507          <div style="display:flex;align-items:center;gap:8px">
6508            <span class="badge ${c.is_active?'badge-green':'badge-red'}">${c.is_active?'Active':'Inactive'}</span>
6509            <button class="btn btn-ghost" style="padding:4px 10px;font-size:.7rem" onclick="openEditClient('${c.id}')">✏️ Edit</button>
6510            <button class="btn btn-red" style="padding:4px 10px;font-size:.7rem" onclick="deactivateClient('${c.id}','${c.name}')">⊘</button>
6511          </div>
6512        </div>
6513        <div class="client-stats">
6514          <div class="client-stat">Scans: <span>${c.this_month?.scans||0}</span></div>
6515          <div class="client-stat">Posts: <span>${c.this_month?.posts||0}</span></div>
6516          <div class="client-stat">Collages: <span>${c.this_month?.collages||0}</span></div>
6517          <div class="client-stat" style="margin-left:auto">Key: <span>${c.api_key_preview||'—'}</span></div>
6518        </div>
6519        <div style="margin-top:8px;display:flex;gap:6px;flex-wrap:wrap">
6520          ${(c.config?.watchlist||[]).slice(0,8).map(t=>`<span class="badge badge-muted">${t}</span>`).join('')}
6521          ${(c.config?.watchlist||[]).length>8?`<span class="badge badge-muted">+${c.config.watchlist.length-8} more</span>`:''}
6522        </div>
6523      </div>`).join('');
6524  }catch(e){$('client-list').innerHTML='<div class="card"><p style="color:var(--red)">Failed to load clients.</p></div>';}
6525}
6526
6527function openNewClient(){
6528  editingClientId=null;
6529  $('modal-title-text').textContent='New Enterprise Client';
6530  $('modal-save-btn').textContent='💾 Create Client';
6531  $('modal-key-result').style.display='none';
6532  // Reset fields
6533  ['m-name','m-email','m-chartimg','m-x-api','m-x-secret','m-x-token','m-x-tsecret','m-make-ig','m-make-tt','m-watchlist','m-hashtags'].forEach(id=>{$(id).value='';});
6534  $('m-scan-limit').value=500;$('m-post-limit').value=200;
6535  $('m-tier').value='enterprise';$('m-template').value='factoz';$('m-tone').value='professional';
6536  $('m-plat-x').checked=true;$('m-plat-ig').checked=true;$('m-plat-tt').checked=false;
6537  $('m-autopost').checked=false;
6538  modalTab('info');
6539  $('modal-overlay').classList.add('show');
6540}
6541
6542async function openEditClient(id){
6543  editingClientId=id;
6544  $('modal-title-text').textContent='Edit Enterprise Client';
6545  $('modal-save-btn').textContent='💾 Update Client';
6546  $('modal-key-result').style.display='none';
6547  try{
6548    const res=await api('GET',`/api/admin/clients/${id}`);
6549    if(!res.ok)return;
6550    const d=await res.json();
6551    $('m-name').value=d.name||'';$('m-email').value=d.email||'';
6552    $('m-tier').value=d.tier||'enterprise';
6553    const config=d.config||{};
6554    $('m-watchlist').value=(config.watchlist||[]).join(', ');
6555    $('m-template').value=config.carousel_template||'factoz';
6556    $('m-tone').value=config.tone||'professional';
6557    $('m-hashtags').value=config.custom_hashtags||'';
6558    $('m-autopost').checked=config.auto_post||false;
6559    $('m-plat-x').checked=(config.allowed_platforms||['x']).includes('x');
6560    $('m-plat-ig').checked=(config.allowed_platforms||['instagram']).includes('instagram');
6561    $('m-plat-tt').checked=(config.allowed_platforms||[]).includes('tiktok');
6562    if(d.usage_history&&d.usage_history.length){
6563      const u=d.usage_history[0];
6564      $('m-scan-limit').value=u.limits?.scans||500;
6565      $('m-post-limit').value=u.limits?.posts||200;
6566    }
6567  }catch(e){}
6568  modalTab('info');
6569  $('modal-overlay').classList.add('show');
6570}
6571
6572function closeModal(){$('modal-overlay').classList.remove('show');editingClientId=null;}
6573
6574function modalTab(tab){
6575  document.querySelectorAll('.modal-tab').forEach((t,i)=>t.classList.toggle('active',['info','keys','config'][i]===tab));
6576  $('modal-info').style.display=tab==='info'?'':'none';
6577  $('modal-keys').style.display=tab==='keys'?'':'none';
6578  $('modal-config').style.display=tab==='config'?'':'none';
6579}
6580
6581async function saveClient(){
6582  const btn=$('modal-save-btn');btn.innerHTML='<span class="spin"></span>';btn.disabled=true;
6583  const platforms=[$('m-plat-x').checked&&'x',$('m-plat-ig').checked&&'instagram',$('m-plat-tt').checked&&'tiktok'].filter(
6583Boolean);
6584  const watchlist=$('m-watchlist').value.split(',').map(t=>t.trim().toUpperCase()).filter(Boolean);
6585  const config={watchlist,allowed_platforms:platforms,carousel_template:$('m-template').value,tone:$('m-tone').value,custom_hashtags:$('m-hashtags').value,auto_post:$('m-autopost').checked};
6586  try{
6587    let res,d;
6588    if(editingClientId){
6589      // Update existing client
6590      const body={name:$('m-name').value,email:$('m-email').value,tier:$('m-tier').value,config,monthly_scan_limit:parseInt($('m-scan-limit').value),monthly_post_limit:parseInt($('m-post-limit').value)};
6591      if($('m-chartimg').value)body.chartimg_api_key=$('m-chartimg').value;
6592      if($('m-x-api').value)body.x_api_key=$('m-x-api').value;
6593      if($('m-x-secret').value)body.x_api_secret=$('m-x-secret').value;
6594      if($('m-x-token').value)body.x_access_token=$('m-x-token').value;
6595      if($('m-x-tsecret').value)body.x_access_token_secret=$('m-x-tsecret').value;
6596      if($('m-make-ig').value)body.make_webhook_instagram=$('m-make-ig').value;
6597      if($('m-make-tt').value)body.make_webhook_tiktok=$('m-make-tt').value;
6598      res=await api('PUT',`/api/admin/clients/${editingClientId}`,body);
6599      d=await res.json();
6600      if(res.ok){toast('Client updated!','ok');closeModal();loadAdminPanel();}
6601      else toast(d.detail||'Update failed','err');
6602    }else{
6603      // Create new client
6604      const body={name:$('m-name').value,email:$('m-email').value,tier:$('m-tier').value,config,monthly_scan_limit:parseInt($('m-scan-limit').value),monthly_post_limit:parseInt($('m-post-limit').value)};
6605      res=await api('POST','/api/admin/clients',body);
6606      d=await res.json();
6607      if(res.ok){
6608        toast('Client created!','ok');
6609        // Show API key
6610        $('new-api-key').textContent=d.api_key;
6611        $('modal-key-result').style.display='block';
6612        $('modal-save-btn').style.display='none';
6613        loadAdminPanel();
6614        // Now save keys separately
6615        if($('m-chartimg').value||$('m-x-api').value||$('m-make-ig').value){
6616          const keyBody={chartimg_api_key:$('m-chartimg').value,x_api_key:$('m-x-api').value,x_api_secret:$('m-x-secret').value,x_access_token:$('m-x-token').value,x_access_token_secret:$('m-x-tsecret').value,make_webhook_instagram:$('m-make-ig').value,make_webhook_tiktok:$('m-make-tt').value};
6617          await api('PUT',`/api/admin/clients/${d.client_id}`,keyBody);
6618        }
6619      }else toast(d.detail||'Create failed','err');
6620    }
6621  }catch(e){toast('Connection error','err');}
6622  finally{btn.innerHTML=editingClientId?'💾 Update Client':'💾 Create Client';btn.disabled=false;}
6623}
6624
6625async function deactivateClient(id,name){
6626  if(!confirm(`Deactivate "${name}"?`))return;
6627  const res=await fetch(API+`/api/admin/clients/${id}`,{method:'DELETE',headers:hdr()});
6628  if(res.ok){toast(`${name} deactivated`,'ok');loadAdminPanel();}
6629  else toast('Failed','err');
6630}
6631
6632function copyKey(){
6633  const key=$('new-api-key').textContent;
6634  navigator.clipboard.writeText(key).then(()=>toast('API key copied!','ok'));
6635}
6636
6637// ── NEWS FEED ──────────────────────────────────
6638let _newsUnread=0;
6639let _newsItems=[];
6640let _newsFilter='all';
6641let _newsLoading=false;
6642
6643const NEWS_TYPE_BADGE={chart:'badge-green',news:'badge-teal',economic:'badge-yellow',earnings:'badge-yellow',company_analysis:'badge-purple',macro:'badge-orange',tradingview:'badge-blue',djt:'badge-red',brief:'badge-teal'};
6644const NEWS_TYPE_ICON={chart:'📊',news:'📰',economic:'📈',earnings:'💰',company_analysis:'🏢',macro:'🌍',tradingview:'💡',djt:'🇺🇸',brief:'📋'};
6645
6646function earningsDataGrid(cap){
6647  if(!cap) return '';
6648  const hasActual = cap.eps_actual != null;
6649  const beat = hasActual && cap.surprise_pct != null && cap.surprise_pct >= 0;
6650  const color = hasActual ? (beat ? 'var(--green)' : 'var(--red)') : 'var(--muted)';
6651  const sign  = cap.surprise_pct > 0 ? '+' : '';
6652  const rev   = cap.rev_actual != null ? `$${(cap.rev_actual/1e9).toFixed(2)}B`
6653              : cap.rev_estimate != null ? `$${(cap.rev_estimate/1e9).toFixed(2)}B est` : '—';
6654  return `<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;background:var(--bg3);border-radius:7px;padding:10px;border:1px solid var(--border);margin-bottom:8px">
6655    <div style="text-align:center"><div style="font-size:.55rem;color:var(--muted);letter-spacing:1.5px;margin-bottom:4px">EPS EST</div><div style="font-size:.88rem;font-weight:700">${cap.eps_estimate!=null?'$'+cap.eps_estimate.toFixed(2):'—'}</div></div>
6656    <div style="text-align:center"><div style="font-size:.55rem;color:var(--muted);letter-spacing:1.5px;margin-bottom:4px">EPS ACTUAL</div><div style="font-size:.88rem;font-weight:700;color:${color}">${hasActual?'$'+cap.eps_actual.toFixed(2):'—'}</div></div>
6657    <div style="text-align:center"><div style="font-size:.55rem;color:var(--muted);letter-spacing:1.5px;margin-bottom:4px">SURPRISE</div><div style="font-size:.88rem;font-weight:700;color:${color}">${cap.surprise_pct!=null?sign+cap.surprise_pct.toFixed(2)+'%':'—'}</div></div>
6658    <div style="text-align:center"><div style="font-size:.55rem;color:var(--muted);letter-spacing:1.5px;margin-bottom:4px">REVENUE</div><div style="font-size:.88rem;font-weight:700">${rev}</div></div>
6659  </div>${!hasActual?'<div style="font-size:.65rem;color:var(--muted);margin-bottom:6px">⏳ Awaiting results…</div>':''}`;
6660}
6661
6662function newsCard(n){
6663  if(n.content_type==='djt'){
6664    const date=n.created_at?new Date(n.created_at).toLocaleString('en-US',{month:'short',day:'numeric',year:'numeric',hour:'numeric',minute:'2-digit'}):'';
6665    const avatar=n.image_url||'';
6666    return `<div class="news-card" style="align-items:flex-start;gap:12px;padding:14px">
6667      <div style="flex-shrink:0;width:48px;height:48px;border-radius:50%;overflow:hidden;border:2px solid rgba(255,68,68,.4);background:var(--bg3);display:flex;align-items:center;justify-content:center">
6668        ${avatar?`<img src="${avatar}" alt="DJT" style="width:100%;height:100%;object-fit:cover" onerror="this.parentElement.innerHTML='🇺🇸'">`:`<span style="font-size:1.4rem">🇺🇸</span>`}
6669      </div>
6670      <div style="flex:1;min-width:0">
6671        <div style="display:flex;align-items:center;gap:8px;margin-bottom:5px">
6672          <span class="badge badge-red" style="font-size:.58rem">🇺🇸 DJT</span>
6673          <span style="font-size:.62rem;color:var(--muted);font-family:var(--mono)">@realDonaldTrump</span>
6674          <span class="news-date" style="margin:0">${date}</span>
6675        </div>
6676        <div class="news-title" style="font-size:.85rem;line-height:1.5">${n.title||''}</div>
6677        <div class="news-actions" style="margin-top:8px">
6678          ${n.source_url?`<a href="${n.source_url}" target="_blank" rel="noopener" class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px;text-decoration:none">🔗 View on Truth Social</a>`:''}
6679        </div>
6680      </div>
6681    </div>`;
6682  }
6683  if(n.content_type==='tradingview'){
6684    const date=n.created_at?new Date(n.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}):'';
6685    const tvTicker=((n.title||'').match(/\$([A-Z]{1,5})/)||[])[1];
6686    return `<div class="news-card" style="flex-direction:column;gap:0">
6687      <div class="news-body" style="width:100%">
6688        <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
6689          <span class="badge badge-blue" style="font-size:.58rem">💡 TRADINGVIEW</span>
6690          ${tvTicker?`<span onclick="openTickerModal('${tvTicker}')" style="font-family:var(--mono);font-size:.65rem;font-weight:700;background:rgba(0,200,83,.12);color:var(--green);padding:2px 8px;border-radius:4px;cursor:pointer">${tvTicker} 📈</span>`:''}
6691          <span class="news-date" style="margin:0">${date}</span>
6692        </div>
6693        <div class="news-title">${n.title||'Untitled'}</div>
6694        ${n.summary?`<div class="news-sum">${n.summary}</div>`:''}
6695        ${n.image_url?`<img src="${n.image_url}" alt="chart" loading="lazy" onclick="this.classList.toggle('tv-chart-expanded')" style="width:100%;border-radius:8px;margin:10px 0;cursor:zoom-in;border:1px solid var(--border);max-height:220px;object-fit:cover;transition:max-height .3s ease" onerror="this.style.display='none'">`:''}
6696        <div class="news-actions">
6697          ${n.source_url?`<a href="https://in.tradingview.com/?aff_id=164648" target="_blank" rel="noopener" class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px;text-decoration:none">🔗 View on TradingView</a>`:''}
6698        </div>
6699      </div>
6700    </div>`;
6701  }
6702  if(n.content_type==='earnings'){
6703    let cap=null;try{if(n.caption&&n.caption[0]==='{')cap=JSON.parse(n.caption);}catch(e){}
6704    return `<div class="news-card">
6705      <div class="news-thumb" style="display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:2rem">💰</div>
6706      <div class="news-body">
6707        <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
6708          <span class="badge badge-yellow" style="font-size:.58rem">💰 EARNINGS</span>
6709          <span class="news-date" style="margin:0">${n.created_at?new Date(n.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}):''}</span>
6710        </div>
6711        <div class="news-title">${n.title||'Untitled'}</div>
6712        ${earningsDataGrid(cap)}
6713        <div class="news-actions">
6714          ${n.image_url?`<a href="${n.image_url}" target="_blank" rel="noopener" class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px;text-decoration:none">🖼 Image</a>`:''}
6715        </div>
6716      </div>
6717    </div>`;
6718  }
6719  const badge=NEWS_TYPE_BADGE[n.content_type]||'badge-muted';
6720  const icon=NEWS_TYPE_ICON[n.content_type]||'📄';
6721  const label=(n.content_type||'news').replace('_',' ').toUpperCase();
6722  const isAiAnalysis = n.content_type==='company_analysis' && n.image_url;
6723  const isBrief = n.content_type==='brief';
6724
6725  // Brief cards: pill card with Read button → modal
6726  if(isBrief){
6727    const dateStr = n.created_at ? new Date(n.created_at).toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'}) : '';
6728    const preview = (n.summary||'').replace(/\\n/g,' ').replace(/\s+/g,' ').trim().slice(0,180);
6729    const safeId = n.id;
6730    return `<div class="news-card" style="flex-direction:column;gap:0;padding:16px 18px;cursor:default">
6731      <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
6732        <span class="badge ${badge}" style="font-size:.58rem">${icon} DAILY BRIEF</span>
6733        <span class="news-date" style="margin:0">${dateStr}</span>
6734      </div>
6735      <div class="news-title" style="margin-bottom:6px">${n.title||'Untitled'}</div>
6736      ${preview?`<div style="font-size:.72rem;color:var(--text2);line-height:1.5;margin-bottom:12px">${preview}…</div>`:''}
6737      <button class="btn btn-ghost" style="font-size:.72rem;padding:6px 14px;align-self:flex-start" onclick="openBriefModal('${safeId}')">📋 Read Full Brief</button>
6738    </div>`;
6739  }
6740
6741  return `<div class="news-card" ${isAiAnalysis?'style="flex-direction:column"':''}>
6742    ${!isAiAnalysis?(n.image_url?`<img class="news-thumb" src="${n.image_url}" alt="" loading="lazy" onerror="this.style.display='none'">`:n.summary&&n.summary.includes('<')?'':`<div class="news-thumb" style="display:flex;align-items:center;justify-content:center;color:var(--muted);font-size:2rem">${icon}</div>`):''}
6743    <div class="news-body" style="width:100%">
6744      <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
6745        <span class="badge ${badge}" style="font-size:.58rem">${icon} ${label}</span>
6746        ${n.source?`<span style="font-size:.58rem;color:var(--text2);font-weight:600;font-family:var(--mono)">${n.source}</span>`:''}
6747        <span class="news-date" style="margin:0">${n.created_at?new Date(n.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}):''}</span>
6748      </div>
6749      ${n.summary&&n.summary.includes('<')
6750        ?`<div class="news-title" style="cursor:pointer" onclick="toggleArticle('${n.id}')">${n.title||'Untitled'} <span id="art-toggle-${n.id}" style="font-size:.62rem;color:var(--teal);font-family:var(--mono)">READ ↓</span></div>`
6751        :`<div class="news-title">${n.title||'Untitled'}</div>`}
6752      ${isAiAnalysis?`<img src="${n.image_url}" loading="lazy" onclick="this.classList.toggle('tv-chart-expanded')" style="width:100%;border-radius:8px;margin:10px 0;cursor:zoom-in;border:1px solid var(--border);max-height:320px;object-fit:cover;transition:max-height .3s ease" onerror="this.style.display='none'">` : ''}
6753      ${n.summary&&!n.caption?n.summary.includes('<')?
6754        `<div id="art-preview-${n.id}" style="position:relative;max-height:180px;overflow:hidden;margin-bottom:6px;border-radius:6px;cursor:pointer" onclick="toggleArticle('${n.id}')">
6755          <div style="pointer-events:none;transform:scale(0.72);transform-origin:top left;width:139%;overflow:hidden">${n.summary}</div>
6756          <div style="position:absolute;bottom:0;left:0;right:0;height:60px;background:linear-gradient(transparent,var(--bg2))"></div>
6757        </div>
6758        <div id="art-body-${n.id}" style="display:none;margin-bottom:10px;border-radius:8px;overflow:hidden;border:1px solid var(--border)"></div>`:
6759        `<div class="news-sum">${n.summary}</div>`:''}
6760      ${n.caption?`<div id="news-caption-edit-${n.id}" contenteditable="true" style="font-size:.73rem;line-height:1.5;padding:6px 8px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;margin-bottom:8px;outline:none;color:var(--text2)">${n.caption}</div>`:''}
6761      <div class="news-actions">
6762        ${n.caption?`<button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="copyNewsCaption('${n.id}')">📋 Copy</button>`:''}
6763        ${n.image_url?`<a href="${n.image_url}" target="_blank" rel="noopener" class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px;text-decoration:none">🖼 Image</a>`:''}
6764        ${n.source_url?`<a href="${n.source_url}" target="_blank" rel="noopener" class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px;text-decoration:none">🔗 Source</a>`:''}
6765      </div>
6766    </div>
6767  </div>`;
6768}
6769
6770const CLUSTER_ORDER=['brief','tradingview','chart','news','economic','earnings','company_analysis','macro','
6770insider','djt'];
6771const CLUSTER_LABEL={brief:'📋 Daily Briefs',tradingview:'💡 Ideas',chart:'📊 Charts',news:'📰 News',economic:'📈 Economic',earnings:'💰 Earnings',company_analysis:'🏢 Analysis',macro:'🌍 Macro',insider:'🔍 Insider',djt:'🇺🇸 DJT Tweets'};
6772
6773function renderClusters(items){
6774  const groups={};
6775  for(const n of items){const t=n.content_type||'news';if(!groups[t])groups[t]=[];groups[t].push(n);}
6776  const order=[...CLUSTER_ORDER,...Object.keys(groups).filter(t=>!CLUSTER_ORDER.includes(t))];
6777  const globalSeenRaw=localStorage.getItem('fz_news_seen_at');
6778  return order.filter(t=>groups[t]?.length).map(t=>{
6779    const label=CLUSTER_LABEL[t]||t.replace('_',' ').toUpperCase();
6780    const count=groups[t].length;
6781    const seenRaw=localStorage.getItem('fz_seen_'+t)||globalSeenRaw;
6782    const seenAt=seenRaw?new Date(seenRaw):null;
6783    const newCount=seenAt?groups[t].filter(n=>n.created_at&&new Date(n.created_at)>seenAt).length:0;
6784    const newDot=newCount>0?`<span id="cnb-${t}" class="nav-badge badge-pulse" style="font-size:.55rem;padding:2px 5px;cursor:pointer" onclick="dismissClusterBadge('${t}',event)" title="Dismiss">✕ ${newCount}</span>`:'';
6785    const rows=groups[t].map(n=>{
6786      const date=n.created_at?new Date(n.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}):'';
6787      const badge=NEWS_TYPE_BADGE[n.content_type]||'badge-muted';
6788      const tvTicker=n.content_type==='tradingview'?((n.title||'').match(/\$([A-Z]{1,5})/)||[])[1]:null;
6789      const tickerBadge=tvTicker?`<span onclick="event.stopPropagation();openTickerModal('${tvTicker}')" style="font-family:var(--mono);font-size:.58rem;background:rgba(0,200,83,.12);color:var(--green);padding:1px 6px;border-radius:3px;flex-shrink:0;cursor:pointer">${tvTicker}</span>`:'';
6790      const srcLabel=n.source?`<span style="font-size:.55rem;color:var(--text2);font-family:var(--mono);flex-shrink:0">${n.source}</span>`:'';
6791      return `<div class="cluster-row" onclick="toggleClusterRow('cr-${n.id}')">
6792        <span class="badge ${badge}" style="font-size:.55rem;white-space:nowrap">${NEWS_TYPE_ICON[n.content_type]||'📄'}</span>
6793        <span class="cluster-row-title">${n.title||'Untitled'}</span>
6794        ${tickerBadge}
6795        ${srcLabel}
6796        <span class="cluster-row-date">${date}</span>
6797        <span style="font-size:.65rem;color:var(--muted);margin-left:4px">▸</span>
6798      </div>
6799      <div class="cluster-detail" id="cr-${n.id}">${newsCard(n)}</div>`;
6800    }).join('');
6801    return `<div class="cluster-section">
6802      <div class="cluster-header" onclick="toggleCluster('cb-${t}','ca-${t}')">
6803        <span style="font-weight:700;font-size:.82rem">${label}</span>
6804        <span class="badge badge-muted" style="font-size:.6rem">${count}</span>
6805        ${newDot}
6806        <span class="cluster-arrow open" id="ca-${t}">▶</span>
6807      </div>
6808      <div class="cluster-body open" id="cb-${t}">${rows}</div>
6809    </div>`;
6810  }).join('');
6811}
6812
6813function dismissClusterBadge(type,event){
6814  event.stopPropagation();
6815  localStorage.setItem('fz_seen_'+type, new Date().toISOString());
6816  const el=document.getElementById('cnb-'+type);
6817  if(el)el.remove();
6818}
6819
6820function toggleCluster(bodyId,arrowId){
6821  const b=$(bodyId),a=$(arrowId);
6822  if(!b)return;
6823  b.classList.toggle('open');
6824  if(a)a.classList.toggle('open');
6825}
6826function collapseAllClusters(){
6827  document.querySelectorAll('.cluster-body.open').forEach(b=>b.classList.remove('open'));
6828  document.querySelectorAll('.cluster-arrow.open').forEach(a=>a.classList.remove('open'));
6829}
6830
6831function toggleClusterRow(detailId){
6832  const d=$(detailId);
6833  if(!d)return;
6834  const isOpen=d.classList.contains('open');
6835  d.classList.toggle('open');
6836  const row=d.previousElementSibling;
6837  if(row){const arr=row.querySelector('span:last-child');if(arr)arr.textContent=isOpen?'▸':'▾';}
6838}
6839
6840function renderNewsItems(items,type){
6841  const el=$('news-list');
6842  if(!items.length){el.innerHTML=_newsLoading?'<div class="card"><p>Loading news...</p></div>':'<div class="card"><p>No items for this category yet.</p></div>';return;}
6843  if(type==='all'){el.innerHTML=renderClusters(items);return;}
6844  el.innerHTML=items.map(newsCard).join('');
6845}
6846
6847async function loadNews(retry=true){
6848  _initDjtToggle();
6849  _newsLoading=true;
6850  const el=$('news-list');
6851  el.innerHTML='<div class="card"><p>Loading news...</p></div>';
6852  try{
6853    const [newsRes, briefsRes] = await Promise.all([
6854      api('GET','/api/news'),
6855      api('GET','/api/news/briefs'),
6856    ]);
6857    const newsD   = await newsRes.json();
6858    const briefsD = briefsRes.ok ? await briefsRes.json() : {items:[]};
6859    if(!newsRes.ok){
6860      _newsLoading=false;
6861      if(retry){setTimeout(()=>loadNews(false),2500);return;}
6862      el.innerHTML=`<div class="card"><p style="color:var(--red)">${newsD.detail||'Failed to load news'}</p></div>`;
6863      return;
6864    }
6865    const briefIds = new Set((briefsD.items||[]).map(b=>b.id));
6866    _newsItems=[
6867      ...(briefsD.items||[]),
6868      ...(newsD.items||[]).filter(n=>!briefIds.has(n.id)),
6869    ];
6870    _newsLoading=false;
6871    filterNews(_newsFilter, false);
6872  }catch(e){
6873    _newsLoading=false;
6874    if(retry){setTimeout(()=>loadNews(false),2500);return;}
6875    el.innerHTML='<div class="card"><p style="color:var(--red)">Connection error.</p></div>';
6876  }
6877}
6878
6879
6880function searchNews(q){
6881  const el=$('news-search');
6882  if(!q.trim()){
6883    filterNews(_newsFilter,false);
6884    if(el)el.style.borderColor='var(--border)';
6885    return;
6886  }
6887  const term=q.toLowerCase();
6888  const searchable=_newsItems.filter(n=>['news','earnings','tradingview'].includes(n.content_type));
6889  const hits=searchable.filter(n=>
6890    (n.title||'').toLowerCase().includes(term)||
6891    (n.summary||'').toLowerCase().includes(term)||
6892    (n.caption||'').toLowerCase().includes(term)||
6893    (n.source||'').toLowerCase().includes(term)
6894  );
6895  const el2=$('news-list');
6896  if(!hits.length){el2.innerHTML=`<div class="card"><p style="color:var(--muted)">No results for "<strong>${q}</strong>"</p></div>`;return;}
6897  // deactivate tab buttons while searching
6898  document.querySelectorAll('.news-filter-btn').forEach(b=>b.classList.remove('active-filter'));
6899  el2.innerHTML=hits.map(newsCard).join('');
6900}
6901
6902function filterNews(type,scroll=true){
6903  _newsFilter=type;
6904  // clear search box when switching tabs
6905  const srch=$('news-search');if(srch)srch.value='';
6906  document.querySelectorAll('.news-filter-btn').forEach(b=>{
6907    b.classList.toggle('active-filter',b.dataset.type===type);
6908  });
6909  const cal=$('eco-calendar');
6910  if(cal)cal.style.display=type==='economic'?'block':'none';
6911  if(type==='earnings'){loadEarningsCalendar();return;}
6912  const items=type==='all'?_newsItems:_newsItems.filter(n=>n.content_type===type);
6913  renderNewsItems(items,type);
6914  if(scroll)window.scrollTo({top:0,behavior:'smooth'});
6915}
6916
6917function copyNewsCaption(id){
6918  const item=_newsItems.find(n=>n.id===id);
6919  if(!item?.caption)return;
6920  navigator.clipboard.writeText(item.caption).then(()=>toast('Caption copied!','ok'));
6921}
6922
6923async function sendToPost(id){
6924  const item=_newsItems.find(n=>n.id===id);
6925  if(!item)return;
6926  // Clear previous post-page state immediately before navigating
6927  $('post-caption').value='';
6928  $('post-img-preview').innerHTML='';
6929  $('post-file').files=new DataTransfer().files;
6930  const editEl=document.getElementById(`news-caption-edit-${id}`);
6931  const editedCaption=editEl?editEl.innerText.trim():'';
6932  const caption=editedCaption||item.caption||item.title||'';
6933  if(caption){$('post-caption').value=caption;if(typeof syncCaption==='function')syncCaption();}
6934  go('post');
6935  if(item.image_url){
6936    $('post-img-preview').innerHTML='<div style="font-size:.72rem;color:var(--muted);padding:8px">Loading image...</div>';
6937    try{
6938      const resp=await fetch(item.image_url);
6939      const blob=await resp.blob();
6940      const file=new File([blob],'news-image.jpg',{type:blob.type||'image/jpeg'});
6941      const dt=new DataTransfer();dt.items.add(file);
6942      $('post-file').files=dt.files;previewPost();
6943      toast('Image + caption loaded in Post','ok');
6944    }catch(e){
6945      $('post-img-preview').innerHTML='';
6946      toast('Caption loaded. Could not auto-load image — attach manually.','ok');
6947    }
6948  }else{
6949    toast('Caption loaded in Post tab.','ok');
6950  }
6951}
6952
6953async function postNewsItem(id, platform){
6954  const item=_newsItems.find(n=>n.id===id);
6955  if(!item){toast('Item not found','err');return;}
6956  const editEl=document.getElementById(`news-caption-edit-${id}`);
6957  const caption=editEl?editEl.innerText.trim():item.caption||'';
6958  if(!caption){toast('No caption to post','err');return;}
6959  const btnId=platform==='x'?`post-x-${id}`:`post-ig-${id}`;
6960  const btn=$(btnId);
6961  if(btn){btn.disabled=true;btn.innerHTML='<span class="spin"></span>';}
6962  try{
6963    const body={caption,platforms:[platform]};
6964    if(item.image_url)body.image_url=item.image_url;
6965    const res=await api('POST','/api/post/post',body);
6966    const d=await res.json();
6967    if(res.ok){toast(`Posted to ${platform==='x'?'X':'Instagram'}!`,'ok');}
6968    else toast(d.detail||'Post failed','err');
6969  }catch(e){toast('Connection error','err');}
6970  finally{if(btn){btn.disabled=false;btn.innerHTML=platform==='x'?'🐦 Post X':'📸 Post IG';}}
6971}
6972
6973// ── NEWS BADGE (unread counter) ─────────────────
6974function updateNewsBadge(){
6975  const badge=$('news-badge');
6976  if(!badge)return;
6977  if(_newsUnread>0){
6978    badge.textContent=_newsUnread>9?'9+':_newsUnread;
6979    badge.style.display='inline-flex';
6980    badge.classList.add('badge-pulse');
6981  }else{
6982    badge.style.display='none';
6983    badge.classList.remove('badge-pulse');
6984  }
6985}
6986
6987async function pollNewsUnread(){
6988  const tier=USER?.tier||'';
6989  if(tier!=='creator'&&tier!=='pro'&&tier!=='stellar'&&tier!=='enterprise_api'&&tier!=='admin'&&tier!=='super_admin')return;
6990  try{
6991    let seen=localStorage.getItem('fz_news_seen_at')||'';
6992    if(seen){
6993      const sevenDaysAgo=new Date(Date.now()-7*24*60*60*1000);
6994      if(new Date(seen)<sevenDaysAgo){
6995        seen=sevenDaysAgo.toISOString();
6996        localStorage.setItem('fz_news_seen_at',seen);
6997      }
6998    }
6999    const url='/api/news/unread'+(seen?`?since=${encodeURIComponent(seen)}`:'');
7000    const res=await api('GET',url);
7001    if(res.ok){const d=await res.json();_newsUnread=d.count||0;updateNewsBadge();}
7002  }catch(e){}
7003}
7004
7005// ── ADMIN TEMPLATED.IO TEMPLATE MANAGER ─────────
7006async function loadTemplatedConfigs(){
7007  const el=$('tc-list'); if(!el)return;
7008  el.innerHTML='<p style="font-size:.73rem;color:var(--muted)">Loading…</p>';
7009  try{
7010    const r=await api('GET','/api/admin/templated');
7011    const d=await r.json();
7012    if(!d.configs||!d.configs.length){el.innerHTML='<p style="font-size:.73rem;color:var(--muted)">No DB-managed templates yet.</p>';return;}
7013    let h='<div style="display:flex;flex-direction:column;gap:8px">';
7014    for(const c of d.configs){
7015      h+=`<div class="card" style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px">
7016        <div style="flex:1;min-width:0">
7017          <div style="font-size:.8rem;font-weight:600">${c.label}</div>
7018          <div style="font-size:.68rem;color:var(--muted);font-family:var(--mono)">${c.key} · ${c.template_id}</div>
7019          <div style="font-size:.68rem;color:var(--muted)">${c.is_user_slot?`User Slot · $${c.price||'?'}${c.assigned_user_id?' · Assigned':'· Available'}`:c.is_free?'Free':'Paid'+(c.price?' · $'+c.price:'')}</div>
7020        </div>
7021        <button class="btn btn-ghost" style="font-size:.68rem;padding:4px 8px;color:#f87171" onclick="deleteTemplatedConfig('${c.id}','${c.label}')">Delete</button>
7022      </div>`;
7023    }
7024    h+='</div>';
7025    el.innerHTML=h;
7026  }catch(e){el.innerHTML=`<p style="font-size:.73rem;color:#f87171">Error: ${e.message}</p>`;}
7027}
7028
7029async function verifyTemplatedConfig(){
7030  const tid=($('tc-tid').value||'').trim();
7031  if(!tid){toast('Enter a Template UUID first','err');return;}
7032  const btn=$('tc-verify-btn'); btn.disabled=true; btn.textContent='Verifying…';
7033  const res=$('tc-verify-result');
7034  try{
7035    const r=await api('GET',`/api/admin/templated/verify?template_id=${encodeURIComponent(tid)}`);
7036    const d=await r.json();
7037    if(r.ok){
7038      res.innerHTML=`<span style="color:#4ade80">✔ Valid — ${d.layers.length} layer(s): ${d.layers.join(', ')}</span>`;
7039    } else {
7040      res.innerHTML=`<span style="color:#f87171">✘ ${d.detail||'Invalid template'}</span>`;
7041    }
7042  }catch(e){res.innerHTML=`<span style="color:#f87171">Error: ${e.message}</span>`;}
7043  finally{btn.disabled=false;btn.textContent='🔍 Verify Template';}
7044}
7045
7046async function saveTemplatedConfig(){
7047  const key=($('tc-key').value||'').trim().replace(/\s+/g,'_').toLowerCase();
7048  const label=($('tc-label').value||'').trim();
7049  const tid=($('tc-tid').value||'').trim();
7050  const isFree=$('tc-free').checked;
7051  const isUserSlot=$('tc-user-slot')&&$('tc-user-slot').checked;
7052  const price=parseFloat($('tc-price').value)||null;
7053  if(!key||!label||!tid){toast('Fill in key, label and template UUID','err');return;}
7054  const btn=$('tc-save-btn'); btn.disabled=true; btn.textContent='Saving…';
7055  try{
7056    const r=await api('POST','/api/admin/templated',{key,label,template_id:tid,is_free:isFree,price:isFree?null:price,is_user_slot:!!isUserSlot});
7057    const d=await r.json();
7058    if(r.ok){
7059      toast(`Template "${label}" saved`,'ok');
7060      ['tc-key','tc-label','tc-tid','tc-price'].forEach(id=>{$( id).value='';});
7061      $('tc-free').checked=true;
7062      if($('tc-user-slot'))$('tc-user-slot').checked=false;
7063      $('tc-verify-result').innerHTML='';
7064      await loadTemplatedConfigs();
7065    } else {
7066      toast(d.detail||'Save failed','err');
7067    }
7068  }catch(e){toast('Error: '+e.message,'err');}
7069  finally{btn.disabled=false;btn.textContent='+ Save Template';}
7070}
7071
7072async function deleteTemplatedConfig(id,label){
7073  if(!confirm(`Delete template "${label}"?`))return;
7074  try{
7075    const r=await api('DELETE',`/api/admin/templated/${id}`);
7076    if(r.ok||r.status===204){toast(`Deleted "${label}"`,'ok');await loadTemplatedConfigs();}
7077    else{const d=await r.json();toast(d.detail||'Delete failed','err');}
7078  }catch(e){toast('Error: '+e.message,'err');}
7079}
7080
7081// ── ADMIN AI ANALYSIS GENERATOR ────────────────
7082function adminBuildPrompt(){
7083  const company=$('ai-meta-company').value.trim();
7084  const data=$('ai-meta-data').value.trim();
7085  const tone=$('ai-meta-tone').value;
7086  const lang=$('ai-meta-lang').value;
7087  const ct=$('ai-content-type').value;
7088  let p=`Write a ${ct.replace(/_/g,' ')} analysis`;
7089  if(company) p+=` for ${company}`;
7090  if(data) p+=`. Key data: ${data}`;
7091  if(tone) p+=`. Tone: ${tone}`;
7092  if(lang==='es') p+=`. Write the full output in Spanish.`;
7093  $('ai-prompt').value=p.trim();
7094}
7095
7096async function adminGenerateAnalysis(){
7097  const prompt=$('ai-prompt').value.trim();
7098  if(!prompt){toast('Enter a request prompt','err');return;}
7099  const btn=$('ai-gen-btn');
7100  btn.innerHTML='<span class="spin"></span> Generating…';btn.disabled=true;
7101  $('ai-gen-result').innerHTML='';
7102  $('ai-preview').style.display='none';
7103  try{
7104    const res=await api('POST','/api/admin/analysis/generate',{prompt,content_type:$('ai-content-type').value});
7105    const d=await res.json();
7106    if(res.ok){
7107      $('ai-title').value=d.title||'';
7108      $('ai-summary').value=d.summary||'';
7109      $('ai-preview').style.display='block';
7110      toast('Analysis generated — review and push','ok');
7111    }else{
7112      $('ai-gen-result').innerHTML=`<span style="color:var(--red);font-size:.75rem">${d.detail||'Generation failed'}</span>`;
7113    }
7114  }catch(e){toast('Connection error','err');}
7115  finally{btn.innerHTML='🤖 Generate Analysis';btn.disabled=false;}
7116}
7117
7118async function adminPushAnalysis(){
7119  const title=$('ai-title').value.trim();
7120  const summary=$('ai-summary').value.trim();
7121  const ct=$('ai-content-type').value;
7122  if(!title){toast('Title required','err');return;}
7123  try{
7124    const res=await api('POST','/api/admin/news',{title,content_type:ct,summary:summary||null});
7125    const d=await res.json();
7126    if(res.ok){
7127      toast('Analysis pushed to clients!','ok');
7128      $('ai-prompt').value='';$('ai-title').value='';$('ai-summary').value='';
7129      $('ai-preview').style.display='none';
7130      $('ai-push-result').innerHTML='';
7131      loadAdminNews();
7132    }else{
7133      $('ai-push-result').innerHTML=`<span style="color:var(--red);font-size:.75rem">${d.detail||'Push failed'}</span>`;
7134    }
7135  }catch(e){toast('Connection error','err');}
7136}
7137
7138// ── ADMIN NEWS ─────────────────────────────────
7139async function adminPushNews(){
7140  const title=$('an-title').value.trim();
7141  if(!title){toast('Headline required','err');return;}
7142  const ct=$('an-content-type')?.value||'news';
7143  const body={title,content_type:ct,summary:$('an-summary').value.trim()||null,image_url:$('an-img').value.trim()||null,caption:$('an-caption').value.trim()||null,source_url:$('an-source').value.trim()||null};
7144  try{
7145    const res=await api('POST','/api/admin/news',body);
7146    const d=await res.json();
7147    if(res.ok){
7148      toast('News pushed to clients!','ok');
7149      ['an-title','an-summary','an-img','an-caption','an-source'].forEach(id=>{$(id).value='';});
7150      loadAdminNews();
7151    }else $('an-result').innerHTML=`<span style="color:var(--red);font-size:.75rem">${d.detail||'Failed'}</span>`;
7152  }catch(e){toast('Connection error','err');}
7153}
7154
7155async function adminPushRichText(){
7156  const title=$('art-title').value.trim();
7157  const body=$('art-body').value.trim();
7158  if(!title){toast('Headline required','err');return;}
7159  if(!body){toast('Body content required','err');return;}
7160  const btn=$('art-btn');
7161  btn.innerHTML='<span class="spin"></span> Pushing...';btn.disabled=true;
7162  try{
7163    const publishBlog=$('art-publish-blog').checked;
7164    const d=await api('POST','/api/admin/news',{
7165      title,
7166      content_type:$('art-content-type').value,
7167      summary:body,
7168      source_url:$('art-source').value.trim()||null,
7169      send_newsletter:$('art-newsletter').checked,
7170      publish_to_blog:publishBlog,
7171      blog_content_type:$('art-content-type').value,
7172    });
7173    toast('Content pushed to clients!','ok');
7174    $('art-title').value='';$('art-body').value='';$('art-source').value='';
7175    $('art-newsletter').checked=false;$('art-publish-blog').checked=false;
7176    let resultHtml=`<span style="color:var(--green);font-size:.72rem">✓ Pushed — ID: ${d.id}</span>`;
7177    if(d.blog_slug) resultHtml+=` · <a href="/blog/${d.blog_slug}" target="_blank" style="color:var(--teal);font-size:.72rem">View blog post ↗</a>`;
7178    $('art-result').innerHTML=resultHtml;
7179    loadAdminNews();
7180  }catch(e){
7181    $('art-result').innerHTML=`<span style="color:var(--red);font-size:.75rem">${e.message||'Push failed'}</span>`;
7182  }finally{btn.innerHTML='📝 Push to Clients';btn.disabled=false;}
7183}
7184
7185async function adminFetchTVIdeas(){
7186  const btn=$('tv-fetch-btn');
7187  const res=$('tv-fetch-result');
7188  btn.disabled=true;btn.innerHTML='<span class="spin"></span> Fetching...';
7189  res.innerHTML='';
7190  try{
7191    const r=await api('POST','/api/admin/run-job?job=fetch_tv_ideas');
7192    const d=await r.json();
7193    if(r.ok){res.innerHTML='<span style="color:var(--green)">✓ Done — check Ideas tab in News Feed.</span>';}
7194    else{res.innerHTML=`<span style="color:var(--red)">${d.detail||'Error'}</span>`;}
7195  }catch(e){res.innerHTML='<span style="color:var(--red)">Connection error.</span>';}
7196  finally{btn.disabled=false;btn.innerHTML='💡 Fetch Latest Ideas';}
7197}
7198
7199async function adminUploadNews(){
7200  const title=$('anu-title').value.trim();
7201  const file=$('anu-file').files[0];
7202  if(!title){toast('Headline required','err');return;}
7203  if(!file){toast('Select an image file','err');return;}
7204  const btn=$('anu-btn');
7205  btn.innerHTML='<span class="spin"></span> Uploading...';btn.disabled=true;
7206  try{
7207    const fd=new FormData();
7208    fd.append('file',file);
7209    fd.append('title',title);
7210    fd.append('content_type',$('anu-content-type').value);
7211    fd.append('caption',$('anu-caption').value.trim());
7212    const res=await fetch(API+'/api/admin/news/upload',{method:'POST',headers:{Authorization:`Bearer ${TOKEN}`},body:fd});
7213    const d=await res.json();
7214    if(res.ok){
7215      toast('Screenshot pushed to clients!','ok');
7216      $('anu-title').value='';$('anu-caption').value='';$('anu-file').value='';
7217      $('anu-result').innerHTML=`<span style="color:var(--green);font-size:.72rem">✓ Pushed — ${d.image_url?.slice(0,60)}...</span>`;
7218      loadAdminNews();
7219    }else $('anu-result').innerHTML=`<span style="color:var(--red);font-size:.75rem">${d.detail||'Upload failed'}</span>`;
7220  }catch(e){toast('Connection error','err');}
7221  finally{btn.innerHTML='📤 Upload & Push';btn.disabled=false;}
7222}
7223
7224async function loadNewsletterSubCount(){
7225  try{
7226    const d=await api('GET','/api/admin/newsletter/stats');
7227    const el=$('nl-sub-count');
7228    if(el) el.textContent = (d && d.active_subscribers !== undefined)
7229      ? `· 📧 ${d.active_subscribers} subscriber${d.active_subscribers===1?'':'s'}`
7230      : `· 📧 0 subscribers`;
7231  }catch(e){}
7232}
7233
7234let _subListOpen = false;
7235async function toggleSubList(){
7236  const wrap = $('nl-sub-list');
7237  const btn  = $('nl-view-btn');
7238  if(!wrap) return;
7239  if(_subListOpen){ wrap.style.display='none'; btn.textContent='View List'; _subListOpen=false; return; }
7240  wrap.style.display='block';
7241  btn.textContent='Hide List';
7242  _subListOpen=true;
7243  wrap.innerHTML='<p style="font-size:.75rem;color:var(--muted)">Loading…</p>';
7244  try{
7245    const res = await api('GET','/api/admin/newsletter/subscribers');
7246    if(!res.ok){ wrap.innerHTML='<p style="color:var(--red);font-size:.75rem">Failed to load</p>'; return; }
7247    const d = await res.json();
7248    if(!d.subscribers.length){ wrap.innerHTML='<p style="font-size:.75rem;color:var(--muted)">No subscribers yet.</p>'; return; }
7249    const rows = d.subscribers.map(s=>`<tr>
7250      <td style="padding:5px 10px;font-size:.74rem">${s.email}</td>
7251      <td style="padding:5px 10px;font-size:.7rem;color:var(--muted)">${s.subscribed_at}</td>
7252      <td style="padding:5px 10px;text-align:center"><span style="font-size:.65rem;padding:2px 7px;border-radius:4px;${s.is_active?'background:rgba(0,180,100,.15);color:#4caf82;border:1px solid #4caf82':'background:rgba(200,50,50,.1);color:#ef5350;border:1px solid #ef5350'}">${s.is_active?'Active':'Unsub'}</span></td>
7253      <td style="padding:5px 10px;font-size:.7rem;color:var(--muted);text-align:right">${s.charts_used} charts</td>
7254    </tr>`).join('');
7255    wrap.innerHTML=`<div style="font-size:.65rem;color:var(--muted);margin-bottom:6px;font-family:var(--mono)">${d.active} ACTIVE · ${d.total} TOTAL</div>
7256    <div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;border:1px solid var(--border);border-radius:6px;overflow:hidden">
7257      <thead><tr style="background:var(--bg3)">
7258        <th style="padding:6px 10px;font-size:.6rem;color:var(--muted);text-align:left;font-family:var(--mono);letter-spacing:1px">EMAIL</th>
7259        <th style="padding:6px 10px;font-size:.6rem;color:var(--muted);text-align:left;font-family:var(--mono);letter-spacing:1px">SUBSCRIBED</th>
7260        <th style="padding:6px 10px;font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:1px">STATUS</th>
7261        <th style="padding:6px 10px;font-size:.6rem;color:var(--muted);text-align:right;font-family:var(--mono);letter-spacing:1px">USAGE</th>
7262      </tr></thead>
7263      <tbody>${rows}</tbody>
7264    </table></div>`;
7265  }catch(e){ wrap.innerHTML=`<p style="color:var(--red);font-size:.75rem">${e.message}</p>`; }
7266}
7267
7268async function pollAdminNotifications(){
7269  const tier=USER?.tier||'';
7270  if(tier!=='admin'&&tier!=='super_admin')return;
7271  try{
7272    const d=await api('GET','/api/admin/notifications');
7273    const badge=$('admin-notif-badge');
7274    if(!badge)return;
7275    if(d.count>0){
7276      badge.textContent=d.count>9?'9+':d.count;
7277      badge.style.display='inline-flex';
7278      badge.classList.add('badge-pulse');
7279    }else{
7280      badge.style.display='none';
7281      badge.classList.remove('badge-pulse');
7282    }
7283  }catch(e){}
7284}
7285
7286async function markAdminNotifsRead(){
7287  const tier=USER?.tier||'';
7288  if(tier!=='admin'&&tier!=='super_admin')return;
7289  try{
7290    await api('POST','/api/admin/notifications/read');
7291    const badge=$('admin-notif-badge');
7292    if(badge){badge.style.display='none';badge.classList.remove('badge-pulse');}
7293  }catch(e){}
7294}
7295
7296// ── Outreach Tracker ──────────────────────────────────────────────────────────
7297const OC_STATUS_BADGE={prospect:'badge-muted',contacted:'badge-yellow',replied:'badge-teal',trial:'badge-blue',converted:'badge-green',lost:'badge-red'};
7298const OC_STATUS_LABEL={prospect:'Prospect',contacted:'Contacted',replied:'Replied',trial:'Trial',converted:'Converted',lost:'Lost'};
7299const OC_PLATFORM_ICON={x:'𝕏',youtube:'▶',newsletter:'📧',other:'🔗'};
7300const OC_SEGMENT_LABEL={creator:'Finance Creator',advisor:'RIA / Advisor',crypto:'Crypto',other:'Other'};
7301
7302// ── Default templates ────────────────────────────────────────────────────────
7303const OC_DEFAULT_TEMPLATES={
7304  email_creator:{
7305    subject:'Quick tool for your {{topic}} content',
7306    body:`Hey {{name}},
7307
7308Been following your work — {{specific_post}} was a great take.
7309
7310I built Factoz, a command center for market content creators. It scans thousands of tickers, flags signals, and generates ready-to-post carousels and charts automatically.
7311
7312I think it fits exactly what you do. Happy to give you full free access — no demo call, no pitch deck, just log in and use it.
7313
7314Interested?
7315
7316factozcentral.com`
7317  },
7318  email_advisor:{
7319    subject:'Client-ready market visuals in seconds',
7320    body:`Hi {{name}},
7321
7322I came across your work at {{firm}} and thought this might be relevant.
7323
7324Factoz turns market scans and economic data into polished carousels and reports — the kind of thing advisors send clients. Earnings summaries, macro dashboards, sector heatmaps.
7325
7326Would a 2-week free trial be worth trying? No setup required.
7327
7328factozcentral.com`
7329  },
7330  email_crypto:{
7331    subject:'Built something for your audience',
7332    body:`Hey {{name}},
7333
7334Love how you cover {{topic}}. I built Factoz — market scanner + instant carousel generator. Your audience would eat this up.
7335
7336Free access, no strings. Want in?
7337
7338factozcentral.com`
7339  },
7340  x_creator:`Hey {{name}} — love your {{topic}} content. Built a tool called Factoz that turns market scans into post-ready carousels instantly. Thought it fits your workflow. Free access if you want to try it → factozcentral.com`,
7341  x_crypto:`{{name}} your {{topic}} takes are 🔥. Built Factoz — market scanner + auto-carousel generator. Free for creators. Worth a look? factozcentral.com`,
7342  x_advisor:`Hi {{name}}, saw your work on {{topic}}. Built Factoz for market professionals — scans, charts, carousels, all in one. Free trial if useful. factozcentral.com`,
7343
7344  // ── Spanish templates — Panama finance professionals ──────────────────────
7345
7346  es_newsletter:{
7347    subject:'Brief diario de inteligencia de mercados — gratis para profesionales',
7348    body:`Estimado/a {{name}},
7349
7350Mi nombre es Abraham, fundador de Factoz — una plataforma de inteligencia financiera global con cobertura de Estados Unidos, Europa, Latinoamérica y Asia.
7351
7352Cada mañana, antes de las 9:30 ET, generamos un **Brief Diario de Inteligencia de Mercados** que incluye:
7353
7354• Resumen de futuros y condiciones pre-mercado con precios reales
7355• Alertas de riesgo geopolítico y macro
7356• Principales noticias que mueven los mercados ese día
7357• Análisis editorial con perspectiva institucional
7358• Señales técnicas automáticas sobre más de 5,900 acciones
7359
7360Adicionalmente, cada lunes enviamos un reporte semanal con el rendimiento de las señales generadas por el sistema.
7361
7362El servicio es gratuito para profesionales del sector financiero en Panamá. Me gustaría añadirle a nuestra lista de distribución.
7363
7364¿Le interesa recibirlo?
7365
7366Saludos,
7367Abraham
7368Factoz — Global Market Intelligence
7369factozcentral.com`
7370  },
7371
7372  es_advisor:{
7373    subject:'Herramienta de análisis de mercados para profesionales financieros',
7374    body:`Estimado/a {{name}},
7375
7376Le escribo desde Factoz, una plataforma de inteligencia de mercados que combina análisis macroeconómico, señales técnicas automatizadas y cobertura de flujo de opciones.
7377
7378Hemos desarrollado una suite de herramientas pensada para asesores financieros, analistas y gestores de portafolio:
7379
7380→ Scanner técnico sobre 5,900+ activos (RSI, ruptura de 52 semanas, volumen inusual)
7381→ Brief pre-mercado generado por IA con datos reales de futuros
7382→ Estados financieros de cualquier empresa pública en segundos
7383→ Monitor de flujo de opciones inusual (4 veces al día)
7384→ Reporte semanal con rendimiento histórico de señales
7385
7386Actualmente ofrecemos acceso de prueba gratuito de 7 días. No requiere tarjeta de crédito.
7387
7388¿Tendría 5 minutos para revisarlo?
7389
7390Saludos,
7391Abraham
7392factozcentral.com`
7393  },
7394
7395  es_association:{
7396    subject:'Propuesta de colaboración — Brief de mercados para sus miembros',
7397    body:`Estimado/a {{name}},
7398
7399Le escribo en representación de Factoz, plataforma de inteligencia financiera global.
7400
7401Estamos desarrollando una alianza con asociaciones del sector financiero en Panamá para distribuir nuestro **Brief Diario de Inteligencia de Mercados** de forma gratuita a sus miembros.
7402
7403El brief cubre:
7404• Condiciones de futuros con precios en tiempo real
7405• Análisis de los principales mercados: EE.UU., Europa, LATAM y Asia
7406• Señales técnicas y flujo inusual de opciones
7407• Editorial con perspectiva institucional
7408
7409Podríamos co-brandear el reporte con el logo de su organización y envi
7409arlo directamente a su base de miembros sin costo alguno.
7410
7411El objetivo es generar valor para sus afiliados y posicionar a {{firm}} como referente en la distribución de inteligencia de mercados en la región.
7412
7413¿Podríamos agendar una llamada breve esta semana?
7414
7415Saludos cordiales,
7416Abraham
7417Factoz — Global Market Intelligence
7418factozcentral.com`
7419  },
7420
7421  es_cold:{
7422    subject:'Análisis de mercados automatizado — Factoz',
7423    body:`Hola {{name}},
7424
7425Vi su perfil en el sector financiero de Panamá y pensé que esto podría interesarle.
7426
7427Factoz es una plataforma que genera análisis de mercados en tiempo real — señales técnicas, flujo de opciones, datos macro y briefs pre-mercado — de forma automatizada.
7428
7429Actualmente estamos ofreciendo acceso gratuito a profesionales del sector. Sin contratos, sin tarjeta de crédito.
7430
7431Si le interesa explorar la plataforma, puede acceder en factozcentral.com
7432
7433Quedo atento.
7434Abraham`
7435  },
7436};
7437
7438function _ocTemplates(){
7439  try{return JSON.parse(localStorage.getItem('fz_outreach_templates'))||{};}catch{return {};}
7440}
7441function _ocTpl(key){
7442  const saved=_ocTemplates();
7443  return saved[key]!==undefined?saved[key]:OC_DEFAULT_TEMPLATES[key];
7444}
7445function _ocFill(text,c){
7446  return (text||'')
7447    .replace(/\{\{name\}\}/g, (c.name||'').split(' ')[0])
7448    .replace(/\{\{topic\}\}/g, c.topic||'[topic]')
7449    .replace(/\[AFFILIATE_LINK\]/g, c.affiliate_link||'[AFFILIATE_LINK]')
7450    .replace(/\{\{handle\}\}/g, c.handle_x||c.handle||'[handle]')
7451    .replace(/\{\{firm\}\}/g, '[firm]')
7452    .replace(/\{\{specific_post\}\}/g, '[specific post]');
7453}
7454
7455// ── Outreach modal ───────────────────────────────────────────────────────────
7456let _ocModalContact=null;
7457
7458function openOutreachModal(c, mode){
7459  _ocModalContact=c;
7460  const modal=$('oc-modal');
7461  const title=$('oc-modal-title');
7462  const ta=$('oc-modal-body');
7463  const subjRow=$('oc-modal-subj-row');
7464  const subj=$('oc-modal-subj');
7465  const sendBtn=$('oc-modal-send');
7466  const copyBtn=$('oc-modal-copy');
7467  const sel=$('oc-modal-variant');
7468  modal.style.display='flex';
7469  $('oc-modal-result').innerHTML='';
7470  const affRow=$('oc-modal-affiliate-row');
7471  const affLink=$('oc-modal-affiliate-link');
7472  if(c.affiliate_link){affRow.style.display='';affLink.href=c.affiliate_link;affLink.textContent=c.affiliate_link;}
7473  else{affRow.style.display='none';}
7474
7475  if(mode==='email'){
7476    title.textContent='✉ Send Email — '+c.name;
7477    subjRow.style.display='';
7478    sendBtn.style.display='';
7479    copyBtn.style.display='none';
7480    sel.innerHTML=`<option value="email_creator">Creator (EN)</option><option value="email_advisor">Advisor (EN)</option><option value="email_crypto">Crypto (EN)</option><optgroup label="── Español ──"><option value="es_newsletter">Newsletter ES</option><option value="es_advisor">Asesor ES</option><option value="es_association">Asociación ES</option><option value="es_cold">Cold outreach ES</option></optgroup>`;
7481    // pick default variant based on segment
7482    const def=c.segment==='advisor'?'email_advisor':c.segment==='crypto'?'email_crypto':'email_creator';
7483    sel.value=def;
7484    const tpl=_ocTpl(def);
7485    subj.value=_ocFill(tpl.subject,c);
7486    ta.value=_ocFill(tpl.body,c);
7487    sendBtn.onclick=()=>sendOutreachEmail(c.id);
7488  } else {
7489    title.textContent='𝕏 Copy X DM — '+c.name;
7490    subjRow.style.display='none';
7491    sendBtn.style.display='none';
7492    copyBtn.style.display='';
7493    sel.innerHTML=`<option value="x_creator">Creator</option><option value="x_crypto">Crypto</option><option value="x_advisor">Advisor</option>`;
7494    const def=c.segment==='crypto'?'x_crypto':c.segment==='advisor'?'x_advisor':'x_creator';
7495    sel.value=def;
7496    ta.value=_ocFill(_ocTpl(def),c);
7497    copyBtn.onclick=()=>copyOutreachDM(c.id);
7498  }
7499
7500  sel.onchange=()=>{
7501    const tpl=_ocTpl(sel.value);
7502    if(mode==='email'&&tpl&&tpl.body){
7503      subj.value=_ocFill(tpl.subject,c);
7504      ta.value=_ocFill(tpl.body,c);
7505    } else {
7506      ta.value=_ocFill(tpl,c);
7507    }
7508  };
7509}
7510
7511function closeOutreachModal(){$('oc-modal').style.display='none';_ocModalContact=null;}
7512
7513async function sendOutreachEmail(id){
7514  const subj=($('oc-modal-subj').value||'').trim();
7515  const body=($('oc-modal-body').value||'').trim();
7516  const res=$('oc-modal-result');
7517  if(!subj||!body){res.innerHTML='<span style="color:var(--red)">Subject and body required</span>';return;}
7518  const btn=$('oc-modal-send');
7519  btn.disabled=true;btn.textContent='Sending...';
7520  try{
7521    const r=await api('POST',`/api/admin/outreach/${id}/email`,{subject:subj,body});
7522    const d=await r.json();
7523    if(r.ok){
7524      res.innerHTML='<span style="color:var(--green)">✓ Email sent!</span>';
7525      setTimeout(closeOutreachModal,1200);
7526      await loadOutreachContacts();
7527    }else{res.innerHTML=`<span style="color:var(--red)">${d.detail||'Send failed'}</span>`;}
7528  }catch(e){res.innerHTML='<span style="color:var(--red)">Connection error</span>';}
7529  finally{btn.disabled=false;btn.textContent='Send Email';}
7530}
7531
7532async function copyOutreachDM(id){
7533  const text=$('oc-modal-body').value||'';
7534  try{await navigator.clipboard.writeText(text);}catch{
7535    const ta=document.createElement('textarea');ta.value=text;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();
7536  }
7537  toast('Copied — paste in X DM','ok');
7538  // log as contacted
7539  await api('PUT',`/api/admin/outreach/${id}`,{status:'contacted',last_platform:'x',last_contacted_at:new Date().toISOString()});
7540  await loadOutreachContacts();
7541  setTimeout(closeOutreachModal,800);
7542}
7543
7544function openOutreachForm(){
7545  ['oc-name','oc-handle','oc-email','oc-notes','oc-topic','oc-handle-x','oc-handle-ig','oc-handle-tg','oc-handle-li','oc-handle-tt','oc-affiliate'].forEach(id=>{const el=$(id);if(el)el.value='';});
7546  ['oc-platform','oc-segment'].forEach(id=>{const el=$(id);if(el)el.selectedIndex=0;});
7547  $('oc-result').innerHTML='';
7548  $('outreach-form-panel').style.display='';
7549  const body=$('as-outreach');
7550  if(body&&body.style.display==='none'){body.style.display='';const arr=$('as-outreach-arrow');if(arr)arr.textContent='▾';}
7551}
7552
7553async function saveOutreachContact(){
7554  const btn=$('oc-save-btn');
7555  const res=$('oc-result');
7556  const name=($('oc-name').value||'').trim();
7557  if(!name){res.innerHTML='<span style="color:var(--red)">Name is required</span>';return;}
7558  btn.disabled=true;
7559  try{
7560    const body={
7561      name,
7562      email:    ($('oc-email').value||'').trim()||null,
7563      handle:   ($('oc-handle').value||'').trim()||null,
7564      handle_x: ($('oc-handle-x').value||'').trim()||null,
7565      handle_ig:($('oc-handle-ig').value||'').trim()||null,
7566      handle_telegram:($('oc-handle-tg').value||'').trim()||null,
7567      handle_linkedin:($('oc-handle-li').value||'').trim()||null,
7568      handle_tiktok:  ($('oc-handle-tt').value||'').trim()||null,
7569      platform: $('oc-platform').value,
7570      segment:  $('oc-segment').value,
7571      topic:          ($('oc-topic').value||'').trim()||null,
7572      notes:          ($('oc-notes').value||'').trim()||null,
7573      affiliate_link: ($('oc-affiliate').value||'').trim()||null,
7574    };
7575    const r=await api('POST','/api/admin/outreach',body);
7576    const d=await r.json();
7577    if(r.ok){
7578      res.innerHTML='<span style="color:var(--green)">✓ Saved</span>';
7579      $('outreach-form-panel').style.display='none';
7580      await loadOutreachContacts();
7581    }else{res.innerHTML=`<span style="color:var(--red)">${d.detail||'Error'}</span>`;}
7582  }catch(e){res.innerHTML='<span style="color:var(--red)">Connection error</span>';}
7583  finally{btn.disabled=false;}
7584}
7585
7586async function updateOutreachStatus(id,status){
7587  try{
7588    const payload={status};
7589    if(status==='contacted'){payload.last_contacted_at=new Date().toISOString();}
7590    const r=await api('PUT',`/api/admin/outreach/${id}`,payload);
7591    if(r.ok)await loadOutreachContacts();
7592  }catch(e){}
7593}
7594
7595async function deleteOutreachContact(id){
7596  if(!confirm('Delete this contact?'))return;
7597  const r=await api('DELETE',`/api/admin/outreach/${id}`);
7598  if(r.ok){toast('Deleted','ok');await loadOutreachContacts();}
7599}
7600
7601async function loadOutreachContacts(){
7602  const el=$('outreach-contact-list');
7603  if(!el)return;
7604  try{
7605    const r=await api('GET','/api/admin/outreach');
7606    const d=await r.json();
7607    const contacts=d.contacts||[];
7608    if(!contacts.length){el.innerHTML='<p style="font-size:.73rem;color:var(--muted);padding:8px 0">No prospects yet. Click "+ Add Prospect" to start.</p>';return;}
7609    el.innerHTML=contacts.map(c=>{
7610      const statusBadge=OC_STATUS_BADGE[c.status]||'badge-muted';
7611      const statusLabel=OC_STATUS_LABEL[c.status]||c.status;
7612      const seg=OC_SEGMENT_LABEL[c.segment]||c.segment;
7613      const lastContact=c.last_contacted_at?`Last: ${new Date(c.last_contacted_at).toLocaleDateString('en-US',{month:'short',day:'numeric'})}${c.last_platform?' via '+c.last_platform:''} · `:'';
7614      const added=c.created_at?new Date(c.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric'}):'';
7615      const handles=[c.handle_x&&`𝕏${c.handle_x}`,c.handle_ig&&`IG ${c.handle_ig}`,c.handle_telegram&&`TG ${c.handle_telegram}`,c.handle_linkedin&&`LI ${c.handle_linkedin}`,c.handle_tiktok&&`TT ${c.handle_tiktok}`].filter(Boolean).join(' · ');
7616      const quickBtns=['contacted','replied','trial','converted','lost'].filter(s=>s!==c.status).map(s=>
7617        `<button class="btn btn-ghost" style="font-size:.6rem;padding:2px 7px" onclick="updateOutreachStatus('${c.id}','${s}')">${OC_STATUS_LABEL[s]}</button>`
7618      ).join('');
7619      const cStr=JSON.stringify(c).replace(/'/g,"\\'");
7620      return `<div style="background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:12px 14px;margin-bottom:8px">
7621        <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
7622          <div style="flex:1;min-width:0">
7623            <div style="font-size:.8rem;font-weight:700;color:var(--text)">${c.name}${c.topic?` <span style="color:var(--muted);font-weight:400;font-size:.72rem">· ${c.topic}</span>`:''}</div>
7624            <div style="font-size:.63rem;color:var(--muted);margin-top:1px">${seg} · ${lastContact}Added ${added}</div>
7625            ${handles?`<div style="font-size:.6rem;color:var(--teal);margin-top:2px">${handles}</div>`:''}
7626            ${c.email?`<div style="font-size:.6rem;color:var(--muted);margin-top:1px">✉ ${c.email}</div>`:''}
7627          </div>
7628          <span class="badge ${statusBadge}" style="font-size:.6rem">${statusLabel}</span>
7629          <button onclick="deleteOutreachContact('${c.id}')" style="background:none;border:none;color:#f87171;cursor:pointer;font-size:.75rem;padding:2px 4px">✕</button>
7630        </div>
7631        ${c.notes?`<div style="font-size:.7rem;color:var(--text2);margin-top:6px;padding:5px 8px;background:var(--bg3);border-radius:5px">${c.notes}</div>`:''}
7632        <div style="display:flex;gap:5px;flex-wrap:wrap;margin-top:8px;align-items:center">
7633          ${c.handle_x||c.handle?`<button class="btn btn-ghost" style="font-size:.6rem;padding:2px 8px" onclick='openOutreachModal(${cStr},"x")'>𝕏 Copy DM</button>`:''}
7634          ${c.email?`<button class="btn btn-ghost" style="font-size:.6rem;padding:2px 8px" onclick='openOutreachModal(${cStr},"email")'>✉ Email</button>`:''}
7635          <span style="flex:1"></span>
7636          ${quickBtns}
7637        </div>
7638      </div>`;
7639    }).join('');
7640  }catch(e){el.innerHTML='<p style="color:var(--red);font-size:.73rem">Failed to load contacts.</p>';}
7641}
7642// ──────────────────────────────────────────────────────────────────────────────
7643
7644async function loadAdminNews(){
7645  loadNewsletterSubCount();
7646  const el=$('admin-news-list');
7647  if(!el)return;
7648  try{
7649    const res=await api('GET','/api/admin/news');
7650    const d=await res.json();
7651    if(!res.ok||!d.items?.length){el.innerHTML='<div style="font-size:.72rem;color:var(--muted);font-family:var(--mono);padding:8px 0">No news items pushed yet.</div>';return;}
7652    const drafts=d.items.filter(n=>!n.is_active);
7653    const live=d.items.filter(n=>n.is_active);
7654
7655    const draftHtml=drafts.map(n=>{
7656      const date=n.created_at?new Date(n.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric'}):'';
7657      return `<div class="news-admin-row" style="flex-direction:column;align-items:stretch;border-left:3px solid var(--yellow);padding-left:10px;gap:6px;margin-bottom:10px">
7658        ${n.image_url?`<img src="${n.image_url}" style="width:100%;max-height:180px;object-fit:cover;border-radius:6px;border:1px solid var(--border)" onerror="this.style.display='none'">` : ''}
7659        <div id="dt-${n.id}" contenteditable="true" spellcheck="false" style="font-size:.82rem;font-weight:600;color:var(--text);outline:none;border-bottom:1px solid var(--border);padding-bottom:4px">${n.title}</div>
7660        <div id="dc-${n.id}" contenteditable="true" spellcheck="false" style="font-size:.72rem;color:var(--text2);outline:none;line-height:1.5;max-height:100px;overflow-y:auto">${n.caption||''}</div>
7661        <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
7662          <span class="badge badge-yellow" style="font-size:.55rem">📥 DRAFT</span>
7663          <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 9px;color:var(--green)" onclick="publishDraft('${n.id}')">✓ Publish to Analysis</button>
7664          <button class="btn btn-red" style="padding:3px 9px;font-size:.65rem" onclick="removeNews('${n.id}')">✕ Delete</button>
7665          <span style="font-size:.6rem;color:var(--muted);margin-left:auto">${date}</span>
7666        </div>
7667      </div>`;
7668    }).join('');
7669
7670    const liveHtml=live.map(n=>{
7671      const icon=NEWS_TYPE_ICON[n.content_type]||'📄';
7672      return `<div class="news-admin-row">
7673        <span style="font-size:.8rem">${icon}</span>
7674        <div class="news-admin-title">${n.title}</div>
7675        <div class="news-admin-date">${n.created_at?new Date(n.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric'}):''}</div>
7676        <span class="badge badge-green" style="font-size:.58rem">Live</span>
7677        <button class="btn btn-red" style="padding:3px 9px;font-size:.65rem" onclick="removeNews('${n.id}')">✕</button>
7678      </div>`;
7679    }).join('');
7680
7681    el.innerHTML=(drafts.length?`<div style="font-size:.6rem;color:var(--yellow);font-family:var(--mono);letter-spacing:2px;margin-bottom:8px">DRAFTS (${drafts.length})</div>`+draftHtml:'')+liveHtml;
7682  }catch(e){}
7683}
7684
7685async function publishDraft(id){
7686  const title=document.getElementById(`dt-${id}`)?.innerText.trim();
7687  const caption=document.getElementById(`dc-${id}`)?.innerText.trim();
7688  const res=await api('PATCH',`/api/admin/news/${id}`,{is_active:true,title,caption});
7689  if(res.ok){toast('Published to Analysis feed!','ok');loadAdminNews();}
7690  else toast('Failed to publish','err');
7691}
7692
7693async function removeNews(id){
7694  if(!confirm('Permanently delete this news item? This cannot be undone.'))return;
7695  const res=await api('DELETE',`/api/admin/news/${id}`);
7696  if(res.ok){toast('News item deleted','ok');loadAdminNews();}
7697  else toast('Failed','err');
7698}
7699
7700
7701// ── ADMIN DASHBOARD USERS ──────────────────────
7702let editingUserId=null;
7703
7704function openNewUser(){
7705  editingUserId=null;
7706  $('user-form-title').textContent='New Dashboard User';
7707  $('uf-save-btn').textContent='💾 Create User';
7708  $('uf-email').value='';$('uf-name').value='';$('uf-password').value='';
7709  $('uf-tier').value='pro';
7710  $('uf-result').innerHTML='';
7711  $('uf-email').disabled=false;
7712  $('user-form-panel').style.display='';
7713}
7714
7715function openEditUser(u){
7716  editingUserId=u.id;
7717  $('user-form-title').textContent='Edit User — '+u.email;
7718  $('uf-save-btn').textContent='💾 Update User';
7719  $('uf-email').value=u.email;$('uf-name').value=u.name||'';
7720  $('uf-password').value='';
7721  $('uf-tier').value=u.tier;
7722  $('uf-result').innerHTML='';
7723  $('uf-email').disabled=true;
7724  $('user-form-panel').style.display='';
7725}
7726
7727async function saveUser(){
7728  const btn=$('uf-save-btn');
7729  const res_el=$('uf-result');
7730  btn.disabled=true;
7731  btn.innerHTML='<span class="spin"></span>';
7732  try{
7733    if(editingUserId){
7734      // Update tier/name
7735      const body={tier:$('uf-tier').value};
7736      if($('uf-name').value.trim())body.name=$('uf-name').value.trim();
7737      let res=await api('PUT',`/api/admin/users/${editingUserId}`,body);
7738      let d=await res.json();
7739      if(!res.ok){res_el.innerHTML=`<span style="color:var(--red)">${d.detail||'Update failed'}</span>`;return;}
7740      // Set password if provided
7741      if($('uf-password').value){
7742        const pr=await api('POST',`/api/admin/users/${editingUserId}/set-password`,{password:$('uf-password').value});
7743        if(!pr.ok){const pd=await pr.json();res_el.innerHTML=`<span style="color:var(--red)">${pd.detail||'Password update failed'}</span>`;return;}
7744      }
7745      res_el.innerHTML='<span style="color:var(--green)">✓ User updated</span>';
7746    }else{
7747      if(!$('uf-password').value){res_el.innerHTML='<span style="color:var(--red)">Password is required</span>';return;}
7748      const body={email:$('uf-email').value,password:$('uf-password').value,tier:$('uf-tier').value};
7749      if($('uf-name').value.trim())body.name=$('uf-name').value.trim();
7750      let res=await api('POST','/api/admin/users',body);
7751      let d=await res.json();
7752      if(!res.ok){res_el.innerHTML=`<span style="color:var(--red)">${d.detail||'Create failed'}</span>`;return;}
7753      res_el.innerHTML=`<span style="color:var(--green)">✓ User created — ${d.email} (${d.tier})</span>`;
7754      $('uf-email').value='';$('uf-name').value='';$('uf-password').value='';
7755    }
7756    loadAdminUsers();
7757  }catch(e){res_el.innerHTML='<span style="color:var(--red)">Connection error</span>';}
7758  finally{btn.innerHTML=editingUserId?'💾 Update User':'💾 Create User';btn.disabled=false;}
7759}
7760
7761async function loadAdminUsers(){
7762  const el=$('user-list');if(!el)return;
7763  try{
7764    const res=await api('GET','/api/admin/users');
7765    const d=await res.json();
7766    if(!res.ok){el.innerHTML='';return;}
7767    if(!d.users||!d.users.length){el.innerHTML='<div style="font-size:.73rem;color:var(--muted)">No dashboard users yet.</div>';return;}
7768    el.innerHTML=d.users.map(u=>`
7769      <div style="background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:12px 16px;margin-bottom:8px;display:flex;align-items:center;gap:16px;flex-wrap:wrap">
7770        <div style="flex:1;min-width:0">
7771          <div style="font-size:.78rem;font-weight:600;color:var(--text)">${u.email}</div>
7772          <div style="font-size:.65rem;color:var(--muted);margin-top:2px">${u.name||''} &nbsp;·&nbsp; <span style="color:var(--yellow)">${u.tier}</span> &nbsp;·&nbsp; ${u.is_active?'<span style="color:var(--green)">active</span>':'<span style="color:var(--red)">inactive</span>'} &nbsp;·&nbsp; <span style="font-family:var(--mono);color:#6e7681">${u.last_ip||'—'}</span></div>
7773          <div style="margin-top:5px;display:flex;gap:6px;flex-wrap:wrap">
7774            ${u.tier!=='free'?`<span style="font-size:.62rem;padding:2px 7px;border-radius:4px;background:${u.is_paid?'rgba(0,229,180,.15)':'rgba(255,90,90,.12)'};color:${u.is_paid?'var(--green)':'var(--red)'}">
7775              ${u.is_paid?'✓ Paid':'✗ Unpaid'}
7776            </span>`:''}
7777            <span style="font-size:.62rem;padding:2px 7px;border-radius:4px;background:${u.email_verified?'rgba(0,229,180,.15)':'rgba(255,90,90,.12)'};color:${u.email_verified?'var(--green)':'var(--red)'}">
7778              ${u.email_verified?'✓ Verified':'✗ Unverified'}
7779            </span>
7780            <span style="font-size:.62rem;padding:2px 7px;border-radius:4px;background:rgba(45,212,191,.12);color:var(--teal)">
7781              🎓 ${(u.level||'beginner').charAt(0).toUpperCase()+(u.level||'beginner').slice(1)} · ${u.xp_total||0} XP
7782            </span>
7783            ${u.is_quick_entry?'<span style="font-size:.62rem;padding:2px 7px;border-radius:4px;background:rgba(226,194,117,.15);color:var(--yellow)">✉ Quick Entry</span>':''}
7784            ${u.signup_source==='factoz_financials'?'<span style="font-size:.62rem;padding:2px 7px;border-radius:4px;background:rgba(226,194,117,.15);color:var(--teal)">📋 Factoz Financials</span>':''}
7785          </div>
7786        </div>
7787        <div style="display:flex;gap:6px;flex-wrap:wrap">
7788          ${!u.email_verified?`
7789            <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 9px" onclick="toggleUserField('${u.id}','email_verified',true,'${u.email}')">✓ Verify</button>
7790            <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 9px" onclick="adminResendVerification('${u.id}','${u.email}')">↺ Resend</button>
7791          `:''}
7792          <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 9px" onclick="toggleUserField('${u.id}','is_paid',${!u.is_paid},'${u.email}')">${u.is_paid?'Revoke Pay':'✓ Activate'}</button>
7793          <button class="btn btn-ghost" style="font-size:.65rem;padding:3px 9px" onclick='openEditUser(${JSON.stringify(u)})'>✏ Edit</button>
7794          ${USER.tier==='super_admin'&&u.id!==USER.user_id?`<button class="btn btn-ghost" style="font-size:.65rem;padding:3px 9px;color:var(--red)" onclick="deleteUser('${u.id}','${u.email}')">🗑 Delete</button>`:''}
7795        </div>
7796      </div>`).join('');
7797  }catch(_){}
7798}
7799
7800async function deleteUser(userId,email){
7801  if(!confirm(`Permanently delete ${email}? This cannot be undone.`))return;
7802  const res=await api('DELETE',`/api/admin/users/${userId}`);
7803  if(res.ok){toast(`${email} deleted`,'ok');loadAdminUsers();}
7804  else{const d=await res.json();toast(d.detail||'Delete failed','err');}
7805}
7806
7807async function adminResendVerification(userId,email){
7808  const res=await api('POST',`/api/admin/users/${userId}/resend-verification`);
7809  const d=await res.json();
7810  if(res.ok)toast(d.message||`Verification email sent to ${email}`,'ok');
7811  else toast(d.detail||'Resend failed','err');
7812}
7813async function toggleUserField(userId,field,value,email){
7814  const label=field==='is_paid'?(value?'activate payment':'revoke payment'):(value?'verify email':'unverify email');
7815  // When activating payment also set tier to pro; when revoking set tier back to free
7816  const payload = {[field]:value};
7817  if(field==='is_paid') payload.tier = value ? 'pro' : 'free';
7818  const res=await api('PUT',`/api/admin/users/${userId}`,payload);
7819  if(res.ok){toast(`${email}: ${label} done`,'ok');loadAdminUsers();}
7820  else{const d=await res.json();toast(d.detail||'Update failed','err');}
7821}
7822
7823// ── ADMIN CREDITS ──────────────────────────────
7824async function loadAdminCredits(){
7825  try{
7826    const res=await api('GET','/api/admin/credits');
7827    if(!res.ok)return;
7828    const d=await res.json();
7829
7830    $('credits-month').textContent=d.month||'';
7831    $('crd-claude-calls').textContent=d.claude?.calls??'—';
7832    $('crd-claude-cost').textContent=d.claude?.cost_usd!=null?'$'+d.claude.cost_usd.toFixed(4):'—';
7833    $('crd-tmpl').textContent=d.templated?.renders??'—';
7834    const cld=d.cloudinary||{};
7835    $('crd-cld-storage').textContent=cld.storage_mb!=null?cld.storage_mb+' MB':'—';
7836    $('crd-cld-res').textContent=cld.resources!=null?cld.resources:'—';
7837    const sn=d.sonar||{};
7838    $('crd-sonar-calls').textContent=sn.calls??'—';
7839    $('crd-sonar-cost').textContent=sn.cost_usd!=null?'$'+sn.cost_usd.toFixed(4):'—';
7840    const oa=d.openai||{};
7841    $('crd-openai-calls').textContent=oa.calls??'—';
7842    $('crd-openai-cost').textContent=oa.cost_usd!=null?'$'+oa.cost_usd.toFixed(4):'—';
7843
7844    // Detail panel
7845    const c=d.claude||{};
7846    $('crd-claude-detail').innerHTML=
7847      `Calls: <strong style="color:var(--text)">${c.calls||0}</strong><br>`+
7848      `Input tokens: <strong style="color:var(--text)">
7848${(c.tokens_in||0).toLocaleString()}</strong><br>`+
7849      `Output tokens: <strong style="color:var(--text)">${(c.tokens_out||0).toLocaleString()}</strong><br>`+
7850      `Est. cost: <strong style="color:var(--yellow)">$${(c.cost_usd||0).toFixed(4)}</strong><br>`+
7851      Object.entries(c.by_action||{}).map(([a,n])=>`${a}: ${n}`).join('<br>');
7852
7853    $('crd-sonar-detail').innerHTML=
7854      `Calls: <strong style="color:var(--text)">${sn.calls||0}</strong><br>`+
7855      `Input tokens: <strong style="color:var(--text)">${(sn.tokens_in||0).toLocaleString()}</strong><br>`+
7856      `Output tokens: <strong style="color:var(--text)">${(sn.tokens_out||0).toLocaleString()}</strong><br>`+
7857      `Est. cost: <strong style="color:var(--yellow)">$${(sn.cost_usd||0).toFixed(4)}</strong><br>`+
7858      (Object.entries(sn.by_user||{}).map(([uid,n])=>`${uid.slice(0,20)}…: <strong>${n}</strong>`).join('<br>')||'<span style="color:var(--muted)">No extension calls yet</span>');
7859
7860    $('crd-openai-detail').innerHTML=
7861      `Calls: <strong style="color:var(--text)">${oa.calls||0}</strong><br>`+
7862      `Est. cost: <strong style="color:var(--yellow)">$${(oa.cost_usd||0).toFixed(4)}</strong><br>`+
7863      (Object.entries(oa.by_action||{}).map(([a,n])=>`${a}: ${n}`).join('<br>')||'<span style="color:var(--muted)">No DALL-E calls yet</span>');
7864
7865    const xPerUser=d.x_posts?.per_user||{};
7866    const xTotal=d.x_posts?.total_this_month||0;
7867    $('crd-x-detail').innerHTML=
7868      `Total: <strong style="color:var(--text)">${xTotal}</strong> posts<br>`+
7869      (Object.entries(xPerUser).map(([uid,n])=>`${uid.slice(0,20)}…: <strong>${n}</strong>`).join('<br>')||
7870       '<span style="color:var(--muted)">No X posts yet</span>');
7871
7872    $('credits-detail').style.display='block';
7873    loadSonarLimits();
7874  }catch(e){console.error('loadAdminCredits failed',e);}
7875}
7876
7877async function loadSonarLimits(){
7878  try{
7879    const res=await api('GET','/api/admin/sonar-limits');
7880    if(!res.ok)return;
7881    const d=await res.json();
7882    if($('sonar-limit-beginner'))     $('sonar-limit-beginner').value     = d.beginner     ?? '';
7883    if($('sonar-limit-intermediate')) $('sonar-limit-intermediate').value = d.intermediate ?? '';
7884    if($('sonar-limit-advanced'))     $('sonar-limit-advanced').value     = d.advanced     ?? '';
7885    if($('sonar-limit-expert'))       $('sonar-limit-expert').value       = d.expert       ?? '';
7886  }catch(e){console.warn('loadSonarLimits failed',e);}
7887}
7888
7889async function saveSonarLimits(){
7890  const msg=$('sonar-limits-msg');
7891  const body={};
7892  const beginner=parseInt($('sonar-limit-beginner')?.value);
7893  const intermediate=parseInt($('sonar-limit-intermediate')?.value);
7894  const advanced=parseInt($('sonar-limit-advanced')?.value);
7895  const expert=parseInt($('sonar-limit-expert')?.value);
7896  if(!isNaN(beginner))     body.beginner=beginner;
7897  if(!isNaN(intermediate)) body.intermediate=intermediate;
7898  if(!isNaN(advanced))     body.advanced=advanced;
7899  if(!isNaN(expert))       body.expert=expert;
7900  try{
7901    const res=await api('PUT','/api/admin/sonar-limits',body);
7902    if(res.ok){
7903      if(msg){msg.textContent='Saved ✓';msg.style.color='var(--green)';setTimeout(()=>{msg.textContent='';},2500);}
7904    } else {
7905      if(msg){msg.textContent='Error saving';msg.style.color='var(--red)';}
7906    }
7907  }catch(e){if(msg){msg.textContent='Error';msg.style.color='var(--red)';}}
7908}
7909
7910// ── ADMIN ACTIVITY ─────────────────────────────
7911async function loadAdminActivity(){
7912  const el=$('admin-activity-list');
7913  if(!el)return;
7914  try{
7915    const res=await api('GET','/api/admin/activity');
7916    const d=await res.json();
7917    if(!res.ok){el.innerHTML='';return;}
7918    const posts=d.recent_posts||[];
7919    const signups=d.recent_signups||[];
7920    let html='';
7921    if(!posts.length&&!signups.length){el.innerHTML='<div style="font-size:.72rem;color:var(--muted);font-family:var(--mono);padding:8px 0">No activity yet.</div>';return;}
7922    if(signups.length){
7923      html+='<div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:6px">RECENT SIGNUPS</div>';
7924      html+=signups.slice(0,5).map(u=>`<div class="news-admin-row">
7925        <span style="font-size:.8rem">👤</span>
7926        <div class="news-admin-title">${u.email}</div>
7927        <span class="badge badge-muted" style="font-size:.55rem">${u.tier||'creator'}</span>
7928        <div class="news-admin-date">${u.created_at?new Date(u.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric'}):''}</div>
7929      </div>`).join('');
7930    }
7931    if(posts.length){
7932      html+='<div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin:12px 0 6px">RECENT POSTS</div>';
7933      html+=posts.slice(0,10).map(p=>`<div class="news-admin-row">
7934        <span style="font-size:.8rem">${p.platform==='x'?'🐦':p.platform==='instagram'?'📸':'📤'}</span>
7935        <div class="news-admin-title" style="font-size:.7rem">${p.caption||'—'}</div>
7936        <span class="badge ${p.status==='success'?'badge-green':'badge-red'}" style="font-size:.55rem">${p.status}</span>
7937        <div class="news-admin-date">${p.posted_at?new Date(p.posted_at).toLocaleDateString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):''}</div>
7938      </div>`).join('');
7939    }
7940    el.innerHTML=html;
7941  }catch(e){$('admin-activity-list').innerHTML='';}
7942}
7943
7944// ── DAILY BRIEFS (admin) ────────────────────────
7945
7946const _previewLock = {};
7947function _loadingPage(name, est){
7948  return `<!DOCTYPE html><html><head>
7949    <meta charset="UTF-8"><title>Factoz — Generating ${name} Preview</title>
7950    <style>
7951      *{box-sizing:border-box;margin:0;padding:0}
7952      body{display:flex;flex-direction:column;align-items:center;justify-content:center;
7953           min-height:100vh;background:#0d1526;font-family:Inter,-apple-system,sans-serif;color:#eceef4}
7954      .logo{font-size:26px;font-weight:700;letter-spacing:-1px;color:#eceef4;margin-bottom:36px}
7955      .ring{width:52px;height:52px;border:4px solid rgba(46,204,142,.18);
7956            border-top-color:#2ecc8e;border-radius:50%;animation:spin .85s linear infinite;margin-bottom:28px}
7957      @keyframes spin{to{transform:rotate(360deg)}}
7958      .msg{font-size:15px;color:#8b93a8;margin-bottom:8px}
7959      .est{font-size:12px;color:#55607a;letter-spacing:.5px}
7960    </style>
7961  </head><body>
7962    <div class="logo">FACTOZ</div>
7963    <div class="ring"></div>
7964    <p class="msg">Generating ${name} preview…</p>
7965    <p class="est">Claude is writing — ${est}</p>
7966  </body></html>`;
7967}
7968
7969async function previewEmailInTab(type){
7970  if(_previewLock[type]){ toast('Already generating — please wait','warning'); return; }
7971
7972  const names = { daily:'Daily', weekly:'Weekly', monthly:'Monthly' };
7973  const ests  = { daily:'usually a few seconds', weekly:'usually ~30 seconds', monthly:'usually ~45 seconds' };
7974  const btnId = { daily:'btn-preview-daily', weekly:'btn-preview-weekly', monthly:'btn-preview-monthly' };
7975  const btn   = document.getElementById(btnId[type]);
7976  const orig  = btn ? btn.innerHTML : '';
7977
7978  // Daily still works as a fast direct fetch
7979  if(type === 'daily'){
7980    const tab = window.open('','_blank');
7981    if(!tab){ toast('Pop-up blocked','error'); return; }
7982    tab.document.write(_loadingPage('Daily','usually a few seconds'));
7983    tab.document.close();
7984    if(btn){ btn.disabled=true; btn.innerHTML='⏳ Loading…'; }
7985    try{
7986      const res = await api('GET','/api/briefs/preview/daily-email');
7987      if(!res.ok){ let m=`Preview failed (${res.status})`; try{const e=await res.json();m=e.detail||m;}catch{} tab.document.open();tab.document.write(`<body style="font-family:sans-serif;padding:48px;color:#c00"><h3>Preview failed</h3><p>${m}</p></body>`);tab.document.close();toast(m,'error');return;}
7988      const html=await res.text();tab.document.open();tab.document.write(html);tab.document.close();
7989    }finally{ if(btn){btn.disabled=false;btn.innerHTML=orig;} }
7990    return;
7991  }
7992
7993  // Weekly / Monthly — background task + polling
7994  _previewLock[type] = true;
7995  if(btn){ btn.disabled=true; btn.innerHTML='⏳ Generating…'; }
7996  const tab = window.open('','_blank');
7997  if(!tab){ _previewLock[type]=false; if(btn){btn.disabled=false;btn.innerHTML=orig;} toast('Pop-up blocked','error'); return; }
7998  tab.document.write(_loadingPage(names[type], ests[type]));
7999  tab.document.close();
8000
8001  try{
8002    // Start background task
8003    const startRes = await api('POST', `/api/briefs/preview/${type}/start`);
8004    if(!startRes.ok){ throw new Error(`Start failed (${startRes.status})`); }
8005
8006    // Poll every 4 seconds until ready or error
8007    while(true){
8008      await new Promise(r => setTimeout(r, 4000));
8009      const pollRes = await api('GET', `/api/briefs/preview/${type}/status`);
8010      if(!pollRes.ok) throw new Error(`Poll failed (${pollRes.status})`);
8011      const {status, error} = await pollRes.json();
8012      if(status === 'ready') break;
8013      if(status === 'error') throw new Error(error || 'Generation failed');
8014    }
8015
8016    // Fetch the cached HTML
8017    const htmlRes = await api('GET', `/api/briefs/preview/${type}/html`);
8018    if(!htmlRes.ok) throw new Error(`HTML fetch failed (${htmlRes.status})`);
8019    const html = await htmlRes.text();
8020    tab.document.open();
8021    tab.document.write(html);
8022    tab.document.close();
8023
8024  }catch(err){
8025    const msg = err.message || 'Preview failed';
8026    tab.document.open();
8027    tab.document.write(`<!DOCTYPE html><html><body style="font-family:sans-serif;padding:48px;color:#c00"><h3>Preview failed</h3><p>${msg}</p></body></html>`);
8028    tab.document.close();
8029    toast(msg,'error');
8030  }finally{
8031    _previewLock[type] = false;
8032    if(btn){ btn.disabled=false; btn.innerHTML=orig; }
8033  }
8034}
8035
8036async function adminRunJob(jobName,btn,successLabel){
8037  const orig=btn?btn.textContent:'';
8038  if(btn){btn.disabled=true;btn.textContent='⏳ Running…';}
8039  try{
8040    const res=await api('POST',`/api/admin/run-job?job=${encodeURIComponent(jobName)}`);
8041    const d=await res.json();
8042    if(res.ok)toast(successLabel||'Job triggered','ok');
8043    else toast(d.detail||'Job failed','err');
8044  }catch(e){
8045    toast('Job failed','err');
8046  }finally{
8047    if(btn){btn.disabled=false;btn.textContent=orig;}
8048  }
8049}
8050async function adminGenerateBrief(){
8051  const btn = document.querySelector('[onclick*="adminGenerateBrief"]');
8052  const resultEl = $('brief-gen-result');
8053  if(btn){ btn.disabled=true; btn.textContent='⏳ Starting…'; }
8054  if(resultEl) resultEl.innerHTML='<span style="color:var(--muted);font-size:.78rem">Starting generation…</span>';
8055  try{
8056    const res = await api('POST','/api/briefs/generate');
8057    const d   = await res.json();
8058    if(!res.ok){
8059      if(resultEl) resultEl.innerHTML=`<span style="color:var(--red);font-size:.78rem">❌ ${d.detail||'Failed to start'}</span>`;
8060      if(btn){ btn.disabled=false; btn.textContent='⚡ Generate Today\'s Brief'; }
8061      return;
8062    }
8063    if(d.status==='running'){
8064      if(resultEl) resultEl.innerHTML=`<span style="color:var(--amber);font-size:.78rem">⏳ ${d.message}</span>`;
8065      if(btn){ btn.disabled=false; btn.textContent='⚡ Generate Today\'s Brief'; }
8066      return;
8067    }
8068    // Poll until done
8069    if(resultEl) resultEl.innerHTML='<span style="color:var(--muted);font-size:.78rem">⏳ Claude is writing the brief — checking every 10 seconds…</span>';
8070    if(btn) btn.textContent='⏳ Generating…';
8071    let attempts = 0;
8072    const poll = setInterval(async ()=>{
8073      attempts++;
8074      try{
8075        const sr = await api('GET','/api/briefs/generate/status');
8076        const sd = await sr.json();
8077        if(sd.last_error){
8078          clearInterval(poll);
8079          if(resultEl) resultEl.innerHTML=`<span style="color:var(--red);font-size:.78rem">❌ ${sd.last_error}</span>`;
8080          if(btn){ btn.disabled=false; btn.textContent='⚡ Generate Today\'s Brief'; }
8081        } else if(!sd.running){
8082          clearInterval(poll);
8083          const sent  = sd.emails_sent!=null?` · ${sd.emails_sent} email(s) sent`:'';
8084          const xLine = sd.x_url?`<br><a href="${sd.x_url}" target="_blank" style="color:#1DA1F2;font-size:.75rem">🐦 Posted to X →</a>`:
8085                        sd.x_error?`<br><span style="color:var(--amber);font-size:.72rem">⚠️ X post failed: ${sd.x_error}</span>`:'';
8086          const dlBtn = `<br><button class="btn btn-ghost" style="font-size:.7rem;padding:4px 12px;margin-top:6px" onclick="downloadBriefCard()">⬇ Download Card PNG</button>`;
8087          if(resultEl) resultEl.innerHTML=`<span style="color:var(--green);font-size:.78rem">✅ Brief generated successfully${sent}</span>${xLine}${dlBtn}`;
8088          if(btn){ btn.disabled=false; btn.textContent='⚡ Generate Today\'s Brief'; }
8089          loadAdminBriefs();
8090        } else if(attempts >= 18){
8091          clearInterval(poll);
8092          if(resultEl) resultEl.innerHTML='<span style="color:var(--amber);font-size:.78rem">⏳ Still generating — refresh the list manually in a moment.</span>';
8093          if(btn){ btn.disabled=false; btn.textContent='⚡ Generate Today\'s Brief'; }
8094          loadAdminBriefs();
8095        }
8096      } catch(e){ clearInterval(poll); }
8097    }, 10000);
8098  } catch(e){
8099    if(resultEl) resultEl.innerHTML='<span style="color:var(--red);font-size:.78rem">❌ Request failed</span>';
8100    if(btn){ btn.disabled=false; btn.textContent='⚡ Generate Today\'s Brief'; }
8101  }
8102}
8103
8104// ── NEWSLETTER SUBSCRIBERS ──────────────────────────────────────────────────
8105
8106/* Built from the current origin rather than hardcoded, so it stays correct
8107   on the Railway URL, the custom domain, and locally without edits. */
8108function _signupUrl(){ return location.origin + '/suscribete'; }
8109
8110function _renderSignupUrl(){
8111  const el=$('sub-signup-url'), link=$('sub-signup-open');
8112  if(el) el.textContent=_signupUrl();
8113  if(link) link.href=_signupUrl();
8114}
8115
8116async function copySignupUrl(btn){
8117  const url=_signupUrl();
8118  const done=()=>{ if(btn){const o=btn.textContent;btn.textContent='✓ Copiado';setTimeout(()=>btn.textContent=o,1600);} };
8119  try{
8120    await navigator.clipboard.writeText(url); done();
8121  }catch(e){
8122    // clipboard API needs HTTPS and a user gesture; fall back so the
8123    // button still works rather than silently doing nothing.
8124    const t=document.createElement('textarea');
8125    t.value=url; document.body.appendChild(t); t.select();
8126    try{ document.execCommand('copy'); done(); }catch(_){ prompt('Copia el link:', url); }
8127    document.body.removeChild(t);
8128  }
8129}
8130
8131async function loadSubscriberList(){
8132  _renderSignupUrl();
8133  const el=$('sub-list'), badge=$('as-subscribers-badge');
8134  if(!el) return;
8135  try{
8136    const r=await api('GET','/api/admin/newsletter/subscribers');
8137    const d=await r.json();
8138    if(badge) badge.textContent=`(${d.active} active)`;
8139    if(!d.subscribers?.length){
8140      el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No subscribers yet.</div>';
8141      return;
8142    }
8143    el.innerHTML=d.subscribers.map(s=>`
8144      <div style="display:flex;align-items:center;gap:8px;padding:6px 8px;background:var(--bg3);border:1px solid var(--border);border-radius:5px;margin-bottom:4px;font-size:.73rem">
8145        <span style="flex:1;color:${s.is_active?'var(--text)':'var(--muted)'};${s.is_active?'':'text-decoration:line-through'}">${s.email}</span>
8146        <span style="font-size:.6rem;color:var(--muted);font-family:var(--mono)">${s.subscribed_at}</span>
8147        ${s.is_active?`<button class="btn btn-ghost" style="font-size:.6rem;padding:2px 8px;color:var(--red)" onclick="removeSubscriber('${s.email}')">Remove</button>`:'<span style="font-size:.6rem;color:var(--red)">Inactive</span>'}
8148      </div>`).join('');
8149  }catch(e){el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">Failed to load.</div>';}
8150}
8151
8152async function addSubscriber(){
8153  const input=$('sub-email-input');
8154  const result=$('sub-add-result');
8155  const email=(input?.value||'').trim();
8156  if(!email){if(result)result.innerHTML='<span style="color:var(--red)">Enter an email address.</span>';return;}
8157  const r=await api('POST','/api/admin/newsletter/subscribers',{email});
8158  const d=await r.json();
8159  if(r.ok){
8160    if(result)result.innerHTML=`<span style="color:var(--green)">✅ ${d.added} added, ${d.skipped} already subscribed.</span>`;
8161    if(input)input.value='';
8162    loadSubscriberList();
8163  } else {
8164    if(result)result.innerHTML=`<span style="color:var(--red)">❌ ${d.detail||'Error'}</span>`;
8165  }
8166}
8167
8168async function addSubscribersBulk(){
8169  const textarea=$('sub-bulk-input');
8170  const result=$('sub-add-result');
8171  const raw=(textarea?.value||'');
8172  const emails=raw.split(/[\n,]+/).map(e=>e.trim()).filter(e=>e&&e.includes('@'));
8173  if(!emails.length){if(result)result.innerHTML='<span style="color:var(--red)">No valid emails found.</span>';return;}
8174  const r=await api('POST','/api/admin/newsletter/subscribers',{emails});
8175  const d=await r.json();
8176  if(r.ok){
8177    if(result)result.innerHTML=`<span style="color:var(--green)">✅ ${d.added} added, ${d.skipped} already subscribed.</span>`;
8178    if(textarea)textarea.value='';
8179    loadSubscriberList();
8180  } else {
8181    if(result)result.innerHTML=`<span style="color:var(--red)">❌ ${d.detail||'Error'}</span>`;
8182  }
8183}
8184
8185async function removeSubscriber(email){
8186  if(!confirm(`Remove ${email} from newsletter?`)) return;
8187  const r=await api('DELETE',`/api/admin/newsletter/subscribers/${encodeURIComponent(email)}`);
8188  if(r.ok) loadSubscriberList();
8189  else toast('Failed to remove subscriber','error');
8190}
8191
8192async function downloadBriefCard(){
8193  try{
8194    const res = await api('GET', '/api/briefs/card/download');
8195    if(!res.ok){
8196      const d = await res.json();
8197      toast(d.detail || 'Card not available — generate a brief first', 'error');
8198      return;
8199    }
8200    const blob = await res.blob();
8201    const url  = URL.createObjectURL(blob);
8202    const a    = document.createElement('a');
8203    a.href     = url;
8204    a.download = `factoz_brief_${new Date().toISOString().slice(0,10)}.png`;
8205    document.body.appendChild(a);
8206    a.click();
8207    document.body.removeChild(a);
8208    URL.revokeObjectURL(url);
8209  } catch(e){
8210    toast('Download failed', 'error');
8211  }
8212}
8213
8214async function loadAdminBriefs(){
8215  const el = $('brief-list');
8216  if(!el) return;
8217  try{
8218    const res = await api('GET','/api/briefs?limit=10');
8219    const d   = await res.json();
8220    if(!res.ok || !d.length){
8221      el.innerHTML='<div style="font-size:.72rem;color:var(--muted);font-family:var(--mono);padding:8px 0">No briefs generated yet.</div>';
8222      return;
8223    }
8224    el.innerHTML = d.map(b => `
8225      <div class="news-admin-row" style="justify-content:space-between">
8226        <span style="font-size:.8rem">📰</span>
8227        <div class="news-admin-title">${b.title||b.edition_date}</div>
8228        <span class="badge badge-muted" style="font-size:.55rem;text-transform:uppercase">${b.edition_type}</span>
8229        <div class="news-admin-date">${b.edition_date}</div>
8230        <button class="btn btn-ghost" style="font-size:.6rem;padding:2px 8px;flex-shrink:0"
8231          onclick="previewBrief('${b.id}','${b.edition_date}')">Preview</button>
8232        ${b.edition_date===new Date().toISOString().slice(0,10)?
8233          `<button class="btn btn-ghost" style="font-size:.6rem;padding:2px 8px;flex-shrink:0" onclick="downloadBriefCard()">⬇ Card</button>`:''}
8234      </div>`).join('');
8235  } catch(e){
8236    el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">Failed to load briefs.</div>';
8237  }
8238}
8239
8240async function previewBrief(id, date){
8241  const wrap = $('brief-preview-wrap');
8242  const content = $('brief-preview-content');
8243  if(!wrap||!content) return;
8244  content.innerHTML='<div style="color:#fff;padding:40px;text-align:center">Loading…</div>';
8245  wrap.style.display='block';
8246  try{
8247    const res = await api('GET',`/api/briefs/${date}`);
8248    const d   = await res.json();
8249    if(res.ok){
8250      content.innerHTML = d.html_content;
8251    } else {
8252      content.innerHTML=`<div style="color:#e05252;padding:20px">${d.detail||'Not found'}</div>`;
8253    }
8254  } catch(e){
8255    content.innerHTML='<div style="color:#e05252;padding:20px">Failed to load brief.</div>';
8256  }
8257}
8258
8259// ── PERSON LIBRARY ──────────────────────────────
8260async function loadPersons(){
8261  const el=$('person-grid');
8262  if(!el)return;
8263  try{
8264    const res=await api('GET','/api/carousel/persons');
8265    const d=await res.json();
8266    if(!res.ok||!d.persons.length){
8267      el.innerHTML='<div style="font-size:.73rem;color:var(--muted);font-family:var(--mono);padding:8px 0;grid-column:1/-1">No persons uploaded yet.</div>';
8268      return;
8269    }
8270    el.innerHTML=d.persons.map(p=>`
8271      <div style="background:var(--bg3);border:1px solid var(--border);border-radius:9px;overflow:hidden;position:relative;text-align:center">
8272        <div style="font-size:2.4rem;padding:24px 0 8px;background:var(--bg4)">👤</div>
8273        <div style="padding:8px 10px 10px">
8274          <div style="font-weight:700;font-size:.78rem;color:#e2eaf2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${p.name}</div>
8275          <div style="font-size:.6rem;color:var(--muted);font-family:var(--mono);margin-top:2px">${p.filename}</div>
8276          <button class="btn btn-ghost" style="font-size:.62rem;padding:4px 8px;margin-top:8px;color:var(--red);border-color:rgba(255,68,68,.3)" onclick="deletePerson('${p.id}','${p.name}')">🗑 Remove</button>
8277        </div>
8278      </div>`).join('');
8279  }catch(e){el.innerHTML='';}
8280}
8281
8282async function uploadPerson(){
8283  const name=$('person-name').value.trim();
8284  const file=$('person-file').files[0];
8285  if(!name){toast('Enter a display name','err');return;}
8286  if(!file){toast('Select a photo','err');return;}
8287  const fd=new FormData();
8288  fd.append('file',file);
8289  fd.append('display_name',name);
8290  const res=await fetch('/api/carousel/persons/upload',{method:'POST',headers:hdr(),body:fd});
8291  const d=await res.json();
8292  if(res.ok){
8293    $('person-upload-result').innerHTML=`<span style="color:var(--green);font-size:.75rem">✓ Uploaded: ${d.name}</span>`;
8294    $('person-name').value='';
8295    $('person-file').value='';
8296    loadPersons();
8297  }else{
8298    $('person-upload-result').innerHTML=`<span style="color:var(--red);font-size:.75rem">${d.detail||'Upload failed'}</span>`;
8299  }
8300}
8301
8302async function deletePerson(id,name){
8303  if(!confirm(`Remove "${name}" from the person library?`))return;
8304  const res=await api('DELETE',`/api/carousel/persons/${id}`);
8305  if(res.ok){toast(`Removed ${name}`,'ok');loadPersons();}
8306  else toast('Delete failed','err');
8307}
8308
8309// ─── Movers ─────────────────────────────────────────────────
8310async function loadMovers(){
8311  const grid=$('movers-grid');
8312  const meta=$('movers-meta');
8313  if(!grid)return;
8314  grid.innerHTML='<p style="font-size:.8rem;color:var(--text2);grid-column:1/-1">Loading...</p>';
8315  try{
8316    const r=await api('GET','/api/movers/today');
8317    if(!r.ok){
8318      const d=await r.json();
8319      grid.innerHTML=`<p style="font-size:.8rem;color:var(--text2);grid-column:1/-1">${d.detail||'No movers data yet — check back after 8am EST'}</p>`;
8320      return;
8321    }
8322    const d=await r.json();
8323    if(meta) meta.textContent=`${d.date} · ${(d.total_scanned||0).toLocaleString()} tickers scanned · fetched ${new Date(d.fetched_at).toLocaleTimeString()}`;
8324    const row=(m,color)=>`
8325      <div style="display:flex;align-items:center;gap:10px;padding:8px 10px;background:var(--bg3);border-radius:6px;font-size:.78rem">
8326        <span style="font-family:var(--mono);font-weight:700;color:#e2eaf2;min-width:52px">${m.ticker}</span>
8327        <span style="flex:1;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${m.name||''}</span>
8328        <span style="font-family:var(--mono);font-weight:700;color:${color};white-space:nowrap">${m.pct>=0?'+':''}${m.pct.toFixed(2)}%</span>
8329        <span style="font-family:var(--mono);color:var(--muted);white-space:nowrap">$${m.price.toFixed(2)}</span>
8330      </div>`;
8331    const gainersHtml=(d.gainers||[]).map(m=>row(m,'#00c853')).join('');
8332    const losersHtml=(d.losers||[]).map(m=>row(m,'#f23645')).join('');
8333    grid.innerHTML=`
8334      <div>
8335        <div style="font-size:.6rem;font-family:var(--mono);letter-spacing:2px;color:var(--muted);margin-bottom:8px;color:#00c853">TOP GAINERS</div>
8336        <div style="display:flex;flex-direction:column;gap:4px">${gainersHtml||'<span style="font-size:.75rem;color:var(--muted)">No data</span>'}</div>
8337      </div>
8338      <div>
8339        <div style="font-size:.6rem;font-family:var(--mono);letter-spacing:2px;color:#f23645;margin-bottom:8px">TOP LOSERS</div>
8340        <div style="display:flex;flex-direction:column;gap:4px">${losersHtml||'<span style="font-size:.75rem;color:var(--muted)">No data</span>'}</div>
8341      </div>`;
8342  }catch(e){grid.innerHTML=`<p style="font-size:.8rem;color:var(--red);grid-column:1/-1">Error: ${e.message}</p>`;}
8343}
8344
8345async function loadSessionPrints(){
8346  const el=$('ov-session-prints');
8347  if(!el)return;
8348  try{
8349    const r=await api('GET','/api/te/session-prints');
8350    if(!r.ok){
8351      const e=await r.json().catch(()=>({}));
8352      el.innerHTML=`<div style="font-size:.72rem;color:var(--muted)">${e.detail||'Data unavailable.'}</div>`;
8353      return;
8354    }
8355    const d=await r.json();
8356    const items=d.items||[];
8357    if(!items.length){
8358      el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No major releases today.</div>';
8359      return;
8360    }
8361    el.innerHTML='<div style="font-size:.58rem;font-family:var(--mono);letter-spacing:1px;color:var(--muted);margin-bottom:4px">🇺🇸 US FRED DATA</div>'+
8362      items.map(item=>{
8363        const act=parseFloat(item.actual),prev=parseFloat(item.previous);
8364        const up=!isNaN(act)&&!isNaN(prev)&&act>prev;
8365        const dn=!isNaN(act)&&!isNaN(prev)&&act<prev;
8366        const color=up?'var(--green)':dn?'var(--red)':'var(--text)';
8367        return `<div style="display:flex;align-items:center;gap:6px;padding:4px 8px;background:var(--bg3);border:1px solid var(--border);border-radius:5px;margin-bottom:3px;font-size:.68rem">
8368          <span style="flex:1;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${item.event}</span>
8369          <span style="font-family:var(--mono);font-weight:700;color:${color};white-space:nowrap">${item.actual}</span>
8370          <span style="color:var(--muted);font-family:var(--mono);white-space:nowrap;font-size:.6rem">prev ${item.previous}</span>
8371        </div>`;
8372      }).join('');
8373  }catch(e){el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">—</div>';}
8374}
8375
8376async function loadAnalystRatings(){
8377  const el=$('ov-analyst-panel'); if(!el)return;
8378  let d=null;
8379  try{
8380    const r=await api('GET','/api/news/analyst-ratings');
8381    if(r.ok)d=await r.json();
8382  }catch(e){}
8383  if(!d||!d.ratings||!d.ratings.length){el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No ratings available.</div>';return;}
8384  const actionColor=a=>a==='upgrade'?'var(--green)':a==='downgrade'?'var(--red)':'var(--muted)';
8385  const actionLabel=a=>a==='upgrade'?'▲ UPGRADE':a==='downgrade'?'▼ DOWNGRADE':a.toUpperCase();
8386  el.innerHTML=d.ratings.slice(0,8).map(r=>`
8387    <div onclick="openTickerModal('${r.ticker}')" class="cluster-row" style="cursor:pointer;gap:6px;padding:5px 0;border-bottom:1px solid var(--border);transition:opacity .15s" onmouseover="this.style.opacity='.75'" onmouseout="this.style.opacity='1'">
8388      <span style="font-weight:700;font-size:.75rem;color:var(--text);min-width:42px">${r.ticker}</span>
8389      <span style="font-size:.6rem;font-weight:700;color:${actionColor(r.action)};min-width:80px">${actionLabel(r.action)}</span>
8390      <span style="font-size:.62rem;color:var(--text2);flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">${r.firm}</span>
8391      <span style="font-size:.6rem;color:var(--muted);white-space:nowrap">${r.date?r.date.slice(5):''}</span>
8392    </div>`).join('');
8393}
8394
8395// ── Ad Slots ──────────────────────────────────────────────────────────────────
8396let _ads = {};
8397
8398async function loadAds(){
8399  try{
8400    const resp = await fetch('/api/ads');
8401    if(!resp.ok) return;
8402    _ads = await resp.json();
8403    // Render each known slot
8404    const slots = {
8405      'overview-top':    'ad-overview-top',
8406      'news-sidebar':    'ad-news-sidebar',
8407      'scanner-bottom':  'ad-scanner-bottom',
8408      'carousel-side':   'ad-carousel-side',
8409    };
8410    for(const [name, elId] of Object.entries(slots)){
8411      const el = document.getElementById(elId);
8412      if(!el) continue;
8413      if(_ads[name]){
8414        const a = _ads[name];
8415        el.innerHTML = `<a href="${a.link_url||'#'}" target="_blank" rel="noopener sponsored" style="display:block">
8416          <img src="${a.image_url}" alt="${a.label||'Advertisement'}" style="width:100%;border-radius:8px;display:block">
8417        </a>`;
8418      } else {
8419        el.innerHTML = '';
8420      }
8421    }
8422  }catch(e){}
8423}
8424
8425async function loadAdminAds(){
8426  const list = $('ad-slot-list'); if(!list) return;
8427  const d = await api('GET','/api/admin/ads');
8428  if(!d||!d.slots||!d.slots.length){
8429    list.innerHTML='<div style="font-size:.75rem;color:var(--muted);padding:10px 0">No ad slots yet. Click "+ New Ad" to add one.</div>';
8430    return;
8431  }
8432  list.innerHTML = d.slots.map(s=>`
8433    <div style="display:flex;align-items:center;gap:12px;padding:10px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:8px">
8434      <img src="${s.image_url}" style="width:80px;height:40px;object-fit:cover;border-radius:4px;flex-shrink:0">
8435      <div style="flex:1;min-width:0">
8436        <div style="font-size:.75rem;font-weight:700;color:var(--text)">${s.slot_name}</div>
8437        ${s.label?`<div style="font-size:.65rem;color:var(--muted)">${s.label}</div>`:''}
8438        ${s.link_url?`<div style="font-size:.65rem;color:var(--teal);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.link_url}</div>`:''}
8439      </div>
8440      <div style="display:flex;align-items:center;gap:6px;flex-shrink:0">
8441        <button onclick="toggleAdSlot('${s.id}',${!s.is_active})" class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px;color:${s.is_active?'var(--green)':'var(--muted)'}">${s.is_active?'● Live':'○ Off'}</button>
8442        <button onclick="deleteAdSlot('${s.id}')" class="btn btn-ghost" style="font-size:.65rem;padding:3px 8px;color:var(--red)">✕</button>
8443      </div>
8444    </div>`).join('');
8445}
8446
8447async function uploadAdSlot(){
8448  const btn=$('ad-upload-btn'), res=$('ad-upload-result');
8449  const file = $('ad-file').files[0];
8450  const slot_name = $('ad-slot-name').value;
8451  const link_url = $('ad-link-url').value.trim();
8452  const label = $('ad-label').value.trim();
8453  if(!file){res.innerHTML='<span style="color:var(--red)">Select an image file.</span>';return;}
8454  btn.disabled=true; btn.textContent='Uploading…';
8455  const fd = new FormData();
8456  fd.append('file', file);
8457  fd.append('slot_name', slot_name);
8458  fd.append('link_url', link_url);
8459  fd.append('label', label);
8460  try{
8461    const resp = await fetch('/api/admin/ads',{method:'POST',headers:{'Authorization':'Bearer '+_token},body:fd});
8462    const data = await resp.json();
8463    if(!resp.ok) throw new Error(data.detail||'Upload failed');
8464    res.innerHTML='<span style="color:var(--green)">✓ Ad slot created.</span>';
8465    $('ad-upload-panel').style.display='none';
8466    loadAdminAds();
8467    loadAds();
8468  }catch(e){res.innerHTML=`<span style="color:var(--red)">${e.message}</span>`;}
8469  finally{btn.disabled=false;btn.textContent='📤 Upload Ad';}
8470}
8471
8472async function toggleAdSlot(id, newState){
8473  await api('PATCH',`/api/admin/ads/${id}?is_active=${newState}`);
8474  loadAdminAds();
8475  loadAds();
8476}
8477
8478async function deleteAdSlot(id){
8479  if(!confirm('Delete this ad slot? This cannot be undone.')) return;
8480  await fetch(`/api/admin/ads/${id}`,{method:'DELETE',headers:{'Authorization':'Bearer '+_token}});
8481  loadAdminAds();
8482  loadAds();
8483}
8484
8485async function loadOptionsFlow(){
8486  const el=$('ov-options-panel');
8487  const badge=$('ov-options-badge');
8488  if(!el)return;
8489  try{
8490    const r=await api('GET','/api/options/flow');
8491    if(!r.ok){el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No options data yet — scanner runs Mon–Fri 4×/day.</div>';return;}
8492    const d=await r.json();
8493    const items=(d.items||[]).slice(0,8);
8494    if(badge){badge.textContent=`${d.count} hits`;badge.style.display=d.count?'inline':'none';}
8495    if(!items.length){
8496      el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No unusual activity detected yet today.</div>';
8497      return;
8498    }
8499    el.innerHTML=items.map(o=>{
8500      const isCall=o.option_type==='call';
8501      const premium=o.premium_usd>
8501=1e6?`$${(o.premium_usd/1e6).toFixed(1)}M`:o.premium_usd>=1e3?`$${(o.premium_usd/1e3).toFixed(0)}K`:`$${o.premium_usd.toFixed(0)}`;
8502      const ratio=o.vol_oi_ratio?`${o.vol_oi_ratio.toFixed(1)}×`:'—';
8503      const expiry=o.expiry?o.expiry.slice(5):'—';
8504      return `<div onclick="go('options')" style="cursor:pointer;display:flex;align-items:center;gap:6px;padding:5px 8px;background:var(--bg3);border:1px solid var(--border);border-radius:5px;margin-bottom:4px;font-size:.7rem">
8505        <span style="font-weight:700;font-family:var(--mono);min-width:44px;color:var(--text)">${o.ticker}</span>
8506        <span style="padding:1px 6px;border-radius:3px;font-size:.6rem;font-weight:700;background:${isCall?'rgba(46,204,142,.15)':'rgba(224,82,82,.15)'};color:${isCall?'#2ecc8e':'#e05252'}">${o.option_type.toUpperCase()}</span>
8507        <span style="color:var(--text2);font-family:var(--mono);font-size:.68rem">$${o.strike} · ${expiry}</span>
8508        <div style="margin-left:auto;text-align:right;line-height:1.3">
8509          <div style="color:var(--text);font-family:var(--mono);font-size:.7rem;font-weight:600">${premium}</div>
8510          <div style="color:var(--muted);font-size:.6rem">${ratio} vol/oi</div>
8511        </div>
8512      </div>`;
8513    }).join('');
8514  }catch(e){el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">—</div>';}
8515}
8516
8517let _optSortCol='premium_usd', _optSortDir=-1;
8518
8519function optSetType(t){
8520  ['all','call','put'].forEach(x=>{ const b=$('opt-f-'+x); if(b){b.style.background='transparent';b.style.color='var(--text2)';b.style.fontWeight='';} });
8521  const active=$('opt-f-'+(t||'all'));
8522  if(active){active.style.background='var(--teal)';active.style.color='#000';active.style.fontWeight='700';}
8523  $('opt-f-ticker') && ($('opt-f-ticker').value='');
8524  loadOptionsPage();
8525}
8526
8527function _fmtPremium(v){return v>=1e6?`$${(v/1e6).toFixed(2)}M`:v>=1e3?`$${(v/1e3).toFixed(0)}K`:`$${(v||0).toFixed(0)}`;}
8528function _fmtNum(v){return v==null?'—':v>=1e6?`${(v/1e6).toFixed(1)}M`:v>=1e3?`${(v/1e3).toFixed(0)}K`:String(v);}
8529
8530function _optSort(col){
8531  if(_optSortCol===col) _optSortDir*=-1; else { _optSortCol=col; _optSortDir=-1; }
8532  document.querySelectorAll('.opt-th').forEach(th=>{ th.style.color='var(--muted)'; th.textContent=th.dataset.label; });
8533  const th=document.querySelector(`.opt-th[data-col="${col}"]`);
8534  if(th){th.style.color='var(--teal)';th.textContent=th.dataset.label+(_optSortDir===-1?' ↓':' ↑');}
8535  const tbody=document.querySelector('#opt-page-panel tbody');
8536  if(!tbody)return;
8537  const rows=[...tbody.querySelectorAll('tr')];
8538  rows.sort((a,b)=>{
8539    const av=parseFloat(a.dataset[col]||0), bv=parseFloat(b.dataset[col]||0);
8540    return _optSortDir*(av-bv);
8541  });
8542  rows.forEach(r=>tbody.appendChild(r));
8543}
8544
8545async function loadOptionsPage(){
8546  const el=$('opt-page-panel'), cnt=$('opt-page-count');
8547  if(!el)return;
8548
8549  const typeFilter = (['call','put'].includes(document.querySelector('.opt-f-active')?.dataset?.t||''))
8550    ? document.querySelector('.opt-f-active').dataset.t : '';
8551  const tickerFilter = ($('opt-f-ticker')?.value||'').trim().toUpperCase();
8552  const minPremium   = parseFloat($('opt-f-premium')?.value||0)||0;
8553  const hours        = parseInt($('opt-f-hours')?.value||24)||24;
8554
8555  // determine active type filter from button styles
8556  let activeType='';
8557  ['call','put'].forEach(t=>{ const b=$('opt-f-'+t); if(b&&b.style.background&&b.style.background!=='transparent')activeType=t; });
8558
8559  let url=`/api/options/flow?limit=500&hours=${hours}`;
8560  if(activeType) url+=`&option_type=${activeType}`;
8561  if(tickerFilter) url+=`&ticker=${tickerFilter}`;
8562  if(minPremium>0) url+=`&min_premium=${minPremium}`;
8563
8564  try{
8565    const r=await api('GET',url);
8566    if(!r.ok){el.innerHTML='<div style="color:var(--muted);font-size:.8rem;padding:20px 0">No options data yet — scanner runs Mon–Fri 4×/day.</div>';return;}
8567    const d=await r.json();
8568    const items=d.items||[];
8569    if(cnt) cnt.textContent=`${items.length} contracts`;
8570
8571    // Sentiment bar
8572    const sb=$('opt-sentiment-bar');
8573    if(sb && (d.call_premium||d.put_premium)){
8574      sb.style.display='flex';
8575      $('opt-call-total').textContent=_fmtPremium(d.call_premium);
8576      $('opt-put-total').textContent=_fmtPremium(d.put_premium);
8577      const bullish=d.call_premium>d.put_premium;
8578      const ratio=d.call_premium&&d.put_premium?(Math.max(d.call_premium,d.put_premium)/Math.min(d.call_premium,d.put_premium)).toFixed(1):'—';
8579      const sl=$('opt-sentiment-label');
8580      sl.textContent=bullish?`BULLISH ${ratio}×`:`BEARISH ${ratio}×`;
8581      sl.style.color=bullish?'var(--green)':'var(--red)';
8582    }
8583
8584    if(!items.length){el.innerHTML='<div style="color:var(--muted);font-size:.8rem;padding:20px 0">
8584No unusual activity for the selected filters.</div>';return;}
8585
8586    const thStyle='padding:7px 8px;cursor:pointer;white-space:nowrap;user-select:none;font-family:var(--mono);font-size:.62rem;text-transform:uppercase;color:var(--muted)';
8587    el.innerHTML=`<table style="width:100%;border-collapse:collapse;font-size:.78rem;min-width:860px">
8588      <thead><tr style="border-bottom:1px solid var(--border)">
8589        <th style="${thStyle};text-align:left" class="opt-th" data-col="ticker" data-label="Ticker" onclick="_optSort('ticker')">Ticker</th>
8590        <th style="${thStyle};text-align:left" class="opt-th" data-col="option_type" data-label="Type" onclick="_optSort('option_type')">Type</th>
8591        <th style="${thStyle};text-align:right" class="opt-th" data-col="strike" data-label="Strike" onclick="_optSort('strike')">Strike</th>
8592        <th style="${thStyle};text-align:right" class="opt-th" data-col="days_to_expiry" data-label="DTE" onclick="_optSort('days_to_expiry')">DTE</th>
8593        <th style="${thStyle};text-align:right" class="opt-th" data-col="premium_usd" data-label="Premium ↓" onclick="_optSort('premium_usd')" style="${thStyle};color:var(--teal)">Premium ↓</th>
8594        <th style="${thStyle};text-align:right" class="opt-th" data-col="volume" data-label="Volume" onclick="_optSort('volume')">Volume</th>
8595        <th style="${thStyle};text-align:right" class="opt-th" data-col="open_interest" data-label="Open Int." onclick="_optSort('open_interest')">Open Int.</th>
8596        <th style="${thStyle};text-align:right" class="opt-th" data-col="vol_oi_ratio" data-label="Vol/OI" onclick="_optSort('vol_oi_ratio')">Vol/OI</th>
8597        <th style="${thStyle};text-align:right" class="opt-th" data-col="implied_vol" data-label="IV" onclick="_optSort('implied_vol')">IV</th>
8598        <th style="${thStyle};text-align:right" class="opt-th" data-col="underlying_price" data-label="Underlying" onclick="_optSort('underlying_price')">Underlying</th>
8599        <th style="${thStyle};text-align:center">Result</th>
8600        <th style="${thStyle};text-align:right">Flagged</th>
8601      </tr></thead>
8602      <tbody>${items.map(o=>{
8603        const isCall=o.option_type==='call';
8604        const flagged=o.flagged_at?(new Date(o.flagged_at)).toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):'—';
8605        const iv=o.implied_vol!=null?`${(o.implied_vol*100).toFixed(0)}%`:'—';
8606        const dte=o.days_to_expiry!=null?o.days_to_expiry+'d':'—';
8607        const dteColor=o.days_to_expiry!=null&&o.days_to_expiry<=7?'var(--red)':o.days_to_expiry!=null&&o.days_to_expiry<=30?'var(--gold)':'var(--text2)';
8608        const ratio=o.vol_oi_ratio!=null?`${o.vol_oi_ratio.toFixed(1)}×`:'—';
8609        const ratioColor=o.vol_oi_ratio>=10?'var(--red)':o.vol_oi_ratio>=5?'var(--gold)':'var(--teal)';
8610
8611        // Resolution badge
8612        let resBadge='<span style="font-size:.6rem;color:var(--muted)">⏳ open</span>';
8613        if(o.resolution_type==='expired_itm'){
8614          const move=o.pnl_pct!=null?` ${o.pnl_pct>0?'+':''}${o.pnl_pct.toFixed(1)}%`:'';
8615          resBadge=`<span style="font-size:.6rem;font-weight:700;color:#2ecc8e;background:rgba(46,204,142,.12);padding:2px 6px;border-radius:3px">✅ ITM${move}</span>`;
8616        } else if(o.resolution_type==='expired_otm'){
8617          const move=o.pnl_pct!=null?` ${o.pnl_pct.toFixed(1)}%`:'';
8618          // For puts: OTM means seller won (bullish)
8619          const putSellerWon=!isCall&&o.resolution_type==='expired_otm';
8620          resBadge=putSellerWon
8621            ?`<span style="font-size:.6rem;font-weight:700;color:#2ecc8e;background:rgba(46,204,142,.12);padding:2px 6px;border-radius:3px">✅ PUT SOLD${move}</span>`
8622            :`<span style="font-size:.6rem;font-weight:700;color:#e05252;background:rgba(224,82,82,.12);padding:2px 6px;border-radius:3px">❌ OTM${move}</span>`;
8623        }
8624
8625        return `<tr style="border-bottom:1px solid var(--border20)"
8626          data-ticker="${o.ticker}" data-option_type="${o.option_type==='call'?1:0}"
8627          data-strike="${o.strike}" data-days_to_expiry="${o.days_to_expiry||9999}"
8628          data-premium_usd="${o.premium_usd||0}" data-volume="${o.volume||0}"
8629          data-open_interest="${o.open_interest||0}" data-vol_oi_ratio="${o.vol_oi_ratio||0}"
8630          data-implied_vol="${o.implied_vol||0}" data-underlying_price="${o.underlying_price||0}">
8631          <td style="padding:8px;font-weight:700;font-family:var(--mono);color:var(--text)">${o.ticker}</td>
8632          <td style="padding:8px"><span style="padding:2px 8px;border-radius:3px;font-size:.65rem;font-weight:700;background:${isCall?'rgba(46,204,142,.15)':'rgba(224,82,82,.15)'};color:${isCall?'#2ecc8e':'#e05252'}">${o.option_type.toUpperCase()}</span></td>
8633          <td style="padding:8px;text-align:right;font-family:var(--mono);color:var(--text2)">$${o.strike}</td>
8634          <td style="padding:8px;text-align:right;font-family:var(--mono);color:${dteColor}">${dte}</td>
8635          <td style="padding:8px;text-align:right;font-family:var(--mono);font-weight:700;color:var(--text)">${_fmtPremium(o.premium_usd)}</td>
8636          <td style="padding:8px;text-align:right;font-family:var(--mono);color:var(--text2)">${_fmtNum(o.volume)}</td>
8637          <td style="padding:8px;text-align:right;font-family:var(--mono);color:var(--text2)">${_fmtNum(o.open_interest)}</td>
8638          <td style="padding:8px;text-align:right;font-family:var(--mono);color:${ratioColor};font-weight:600">${ratio}</td>
8639          <td style="padding:8px;text-align:right;font-family:var(--mono);color:var(--muted)">${iv}</td>
8640          <td style="padding:8px;text-align:right;font-family:var(--mono);color:var(--text2)">${o.underlying_price?'$'+o.underlying_price.toFixed(2):'—'}</td>
8641          <td style="padding:8px;text-align:center">${resBadge}</td>
8642          <td style="padding:8px;text-align:right;color:var(--muted);font-size:.68rem;white-space:nowrap">${flagged}</td>
8643        </tr>`;
8644      }).join('')}</tbody>
8645    </table>`;
8646  }catch(e){el.innerHTML='<div style="color:var(--muted);font-size:.8rem;padding:20px 0">Error loading options flow.</div>';}
8647}
8648
8649async function loadInsiderFlow(){
8650  const el=$('ov-insider-panel');
8651  if(!el)return;
8652  try{
8653    const r=await api('GET','/api/news/insider-recent');
8654    if(r.ok){
8655      const d=await r.json();
8656      const items=(d.entries||[]).slice(0,8);
8657      if(!items.length){el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No recent filings.</div>';return;}
8658      el.innerHTML=items.map(t=>{
8659        const buy=t.transaction_code==='P';
8660        const val=t.value?(Math.abs(t.value)>=1e6?`$${(t.value/1e6).toFixed(1)}M`:`$${(t.value/1e3).toFixed(0)}K`):'—';
8661        const shs=t.shares?(t.shares>=1e6?`${(t.shares/1e6).toFixed(1)}M`:(t.shares>=1e3?`${Math.round(t.shares/1e3)}K`:String(t.shares))+' shs'):'';
8662        const nm=(t.name||'').length>18?(t.name||'').slice(0,17)+'…':(t.name||'—');
8663        return `<div style="display:flex;align-items:center;gap:6px;padding:5px 8px;background:var(--bg3);border:1px solid var(--border);border-radius:5px;margin-bottom:4px;font-size:.7rem">
8664          <span style="font-weight:700;font-family:var(--mono);min-width:44px;color:var(--text)">${t.symbol}</span>
8665          <span style="padding:1px 5px;border-radius:3px;font-size:.6rem;background:${buy?'rgba(0,200,83,.15)':'rgba(255,68,68,.15)'};color:${buy?'var(--green)':'var(--red)'}">${buy?'BUY':'SELL'}</span>
8666          <span style="flex:1;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${nm}</span>
8667          <div style="text-align:right;line-height:1.2">
8668            <div style="color:var(--text2);font-family:var(--mono);white-space:nowrap;font-size:.68rem">${val}</div>
8669            ${shs?`<div style="color:var(--text2);font-size:.62rem;white-space:nowrap">${shs}</div>`:''}
8670          </div>
8671        </div>`;
8672      }).join('');
8673    }
8674  }catch(e){el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">—</div>';}
8675}
8676
8677let _ovNewsItems=[];
8678
8679function _renderOvNews(items){
8680  const el=$('ov-news-preview');
8681  if(!el)return;
8682  if(!items.length){el.innerHTML='<div style="font-size:.72rem;color:var(--muted)">No results.</div>';return;}
8683  el.innerHTML=items.map(n=>`
8684    <div style="display:flex;align-items:center;gap:8px;padding:8px 10px;background:var(--bg3);border:1px solid var(--border);border-radius:6px;margin-bottom:6px">
8685      ${n.image_url?`<img src="${n.image_url}" style="width:36px;height:36px;object-fit:cover;border-radius:4px;flex-shrink:0" onerror="this.style.display='none'">`:''}
8686      <div style="flex:1;min-width:0">
8687        <div style="font-size:.75rem;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${n.title||'—'}</div>
8688        <div style="font-size:.62rem;color:#8b9cb8;font-family:var(--mono)">${n.created_at?new Date(n.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric'}):''}</div>
8689      </div>
8690    </div>`).join('');
8691}
8692
8693function filterOvNews(q){
8694  if(!q.trim()){_renderOvNews(_ovNewsItems.slice(0,5));return;}
8695  const term=q.toLowerCase();
8696  const hits=_ovNewsItems.filter(n=>
8697    (n.title||'').toLowerCase().includes(term)||
8698    (n.summary||'').toLowerCase().includes(term)||
8699    (n.source||'').toLowerCase().includes(term)
8700  );
8701  _renderOvNews(hits.slice(0,10));
8702}
8703
8704async function loadOverviewMovers(){
8705  const grid=$('ov-movers-grid');
8706  if(!grid)return;
8707  grid.innerHTML='<div style="font-size:.72rem;color:var(--muted);grid-column:1/-1">Loading...</div>';
8708  try{
8709    const r=await api('GET','/api/movers/today');
8710    if(!r.ok){
8711      const d=await r.json().catch(()=>({}));
8712      grid.innerHTML=`<div style="font-size:.72rem;color:var(--muted);grid-column:1/-1">${d.detail||'No data yet — check back after 8am EST'}</div>`;
8713      return;
8714    }
8715    const d=await r.json();
8716    const row=(m,color)=>`
8717      <div style="display:flex;align-items:center;gap:6px;padding:5px 8px;background:var(--bg3);border-radius:6px;font-size:.72rem">
8718        <span style="font-family:var(--mono);font-weight:700;color:#e2eaf2;min-width:44px">${m.ticker}</span>
8719        <span style="flex:1;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:.65rem">${m.name||''}</span>
8720        <span style="font-family:var(--mono);font-weight:700;color:${color};white-space:nowrap">${m.pct>=0?'+':''}${m.pct.toFixed(2)}%</span>
8721      </div>`;
8722    const gainers=(d.gainers||[]).slice(0,8).map(m=>row(m,'#00c853')).join('');
8723    const losers=(d.losers||[]).slice(0,8).map(m=>row(m,'#f23645')).join('');
8724    grid.innerHTML=`
8725      <div>
8726        <div style="font-size:.58rem;font-family:var(--mono);letter-spacing:1px;color:#00c853;margin-bottom:5px">GAINERS</div>
8727        <div style="display:flex;flex-direction:column;gap:3px">${gainers||'<span style="font-size:.7rem;color:var(--muted)">—</span>'}</div>
8728      </div>
8729      <div>
8730        <div style="font-size:.58rem;font-family:var(--mono);letter-spacing:1px;color:#f23645;margin-bottom:5px">LOSERS</div>
8731        <div style="display:flex;flex-direction:column;gap:3px">${losers||'<span style="font-size:.7rem;color:var(--muted)">—</span>'}</div>
8732      </div>`;
8733  }catch(e){
8734    grid.innerHTML=`<div style="font-size:.72rem;color:var(--red);grid-column:1/-1">Error: ${e.message}</div>`;
8735  }
8736}
8737
8738// ─── Trends ──────────────────────────────────────────────────
8739let _trendsLoaded = false;
8740async function loadTrends(force){
8741  const list=$('trends-list');
8742  const meta=$('trends-meta');
8743  const geo=($('trends-geo')||{value:'US'}).value||'US';
8744  if(!list)return;
8745  if(!force&&_trendsLoaded)return;
8746  _trendsLoaded=true;
8747  list.innerHTML='<p style="font-size:.8rem;color:var(--text2)">Loading...</p>';
8748  try{
8749    const r=await api('GET',`/api/trends?geo=${geo}`);
8750    if(!r.ok){
8751      const d=await r.json().catch(()=>({}));
8752      list.innerHTML=`<p style="font-size:.8rem;color:var(--text2)">${d.detail||'Failed to load trends.'}</p>`;
8753      return;
8754    }
8755    const d=await r.json();
8756    if(meta)meta.textContent=`${d.geo} · ${d.trends.length} trends`;
8757    list.innerHTML=d.trends.map(t=>`
8758      <div style="display:flex;align-items:flex-start;gap:12px;padding:10px 12px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:8px">
8759        <div style="font-family:var(--mono);font-size:.7rem;color:var(--muted);min-width:22px;padding-top:2px">#${t.rank}</div>
8760        <div style="flex:1;min-width:0">
8761          <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
8762            <a href="https://trends.google.com/trends/explore?q=${encodeURIComponent(t.keyword)}&geo=${d.geo}" target="_blank" rel="noopener" style="font-weight:600;font-size:.85rem;color:var(--text);text-decoration:none">${t.keyword}</a>
8763            ${t.traffic?`<span style="font-family:var(--mono);font-size:.68rem;color:var(--teal);background:rgba(226,194,117,.1);padding:1px 6px;border-radius:4px">${t.traffic} searches</span>`:''}
8764          </div>
8765          ${t.news_title?`<a href="${t.news_url||'#'}" target="_blank" rel="noopener" style="display:block;margin-top:4px;font-size:.75rem;color:var(--text2);text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${t.news_title}${t.news_source?` <span style="color:var(--muted)">· ${t.news_source}</span>`:''}</a>`:''}
8766        </div>
8767      </div>`).join('');
8768  }catch(e){
8769    _trendsLoaded=false;
8770    list.innerHTML=`<p style="font-size:.8rem;color:var(--red)">Error: ${e.message}</p>`;
8771  }
8772}
8773
8774async function generateTrendImage(evt, keyword){
8775  const btn = evt.currentTarget;
8776  btn.textContent = '⏳ Generating…'; btn.disabled = true;
8777  try {
8778    const res = await api('POST', '/api/trends/generate-image', {keyword});
8779    const d = await res.json();
8780    if(res.ok){ toast('Image saved to gallery!', 'ok'); }
8781    else toast(d.detail || 'Failed', 'err');
8782  } catch(e){ toast('Error: '+e.message, 'err'); }
8783  finally { btn.textContent = '🎨 AI Image'; btn.disabled = false; }
8784}
8785
8786// ─── Gallery ─────────────────────────────────────────────────
8787let _galleryFilter = '';
8788let _galleryOffset = 0;
8789const _GALLERY_LIMIT = 60;
8790
8791async function loadGallery(type, reset){
8792  if(reset===undefined) reset=true;
8793  _galleryFilter = type;
8794  if(reset) _galleryOffset = 0;
8795  document.querySelectorAll('.gallery-filter').forEach(b=>{
8796    b.classList.toggle('active-filter', b.dataset.type === type);
8797    b.classList.toggle('btn-ghost', b.dataset.type !== type);
8798  });
8799  const el = $('gallery-grid');
8800  if(!el) return;
8801  if(reset) el.innerHTML = '<div style="color:var(--muted);font-size:.8rem">Loading…</div>';
8802  const qs = new URLSearchParams({limit: _GALLERY_LIMIT, offset: _galleryOffset});
8803  if(type) qs.set('output_type', type);
8804  try {
8805    const res = await api('GET', `/api/gallery?${qs}`);
8806    const d = await res.json();
8807    const items = (d.items || []);
8808    if(reset) el.innerHTML = '';
8809    if(!items.length && reset){ el.innerHTML = '<div style="color:var(--muted);font-size:.78rem">No images yet.</div>'; }
8810    else el.innerHTML += items.map(galleryThumb).join('');
8811    $('gallery-load-more').style.display = items.length === _GALLERY_LIMIT ? 'block' : 'none';
8812  } catch(e){
8813    if(reset) el.innerHTML = `<div style="color:var(--red);font-size:.78rem">Error: ${e.message}</div>`;
8814  }
8815}
8816
8817const _galleryItems = {};
8818
8819function galleryThumb(item){
8820  const typeLabel = {carousel_pil:'Carousel',carousel_templated:'Carousel',collage:'Collage',design_studio:'Studio',ai_image:'AI',upload:'Upload'}[item.output_type]||item.output_type;
8821  const thumb = item.thumbnail || '';
8822  const id = item.id || '';
8823  _galleryItems[id] = item;
8824  return `<div style="position:relative;border-radius:8px;overflow:hidden;background:var(--card);border:1px solid var(--border)">
8825    <img src="${thumb}" loading="lazy" onclick="openGalleryLightbox('${id}')"
8826      style="width:100%;aspect-ratio:1;object-fit:cover;display:block;cursor:zoom-in" onerror="this.style.display='none'">
8827    <div style="padding:6px 8px;display:flex;justify-content:space-between;align-items:center">
8828      <span style="font-size:.58rem;color:var(--muted)">${typeLabel}</span>
8829      <button onclick="deleteGalleryItem('${id}',this)" style="background:none;border:none;cursor:pointer;font-size:.7rem;color:var(--muted);padding:0">🗑</button>
8830    </div>
8831  </div>`;
8832}
8833
8834async function uploadGalleryFiles(input){
8835  const status = $('gallery-upload-status');
8836  const files = [...input.files];
8837  if(!files.length) return;
8838  status.textContent = `Uploading ${files.length} file(s)…`;
8839  let ok = 0;
8840  for(const f of files){
8841    const fd = new FormData(); fd.append('file', f);
8842    try {
8843      const res = await fetch('/api/gallery/upload', {
8844        method:'POST', headers:{Authorization:'Bearer '+localStorage.getItem('fz_token')}, body: fd
8845      });
8846      if(res.ok) ok++;
8847    } catch(e){}
8848  }
8849  status.textContent = `${ok}/${files.length} uploaded`;
8850  input.value = '';
8851  loadGallery(_galleryFilter);
8852}
8853
8854async function deleteGalleryItem(id, btn){
8855  btn.textContent = '⏳';
8856  try {
8857    const res = await api('DELETE', `/api/gallery/${id}`);
8858    if(res.ok || res.status === 204) btn.closest('div').remove();
8859    else { btn.textContent = '🗑'; toast('Delete failed','err'); }
8860  } catch(e){ btn.textContent = '🗑'; toast('Error','err'); }
8861}
8862
8863function loadGalleryMore(){
8864  _galleryOffset += _GALLERY_LIMIT;
8865  loadGallery(_galleryFilter, false);
8866}
8867
8868// ─── Gallery Picker for slide slots ──────────────────────────
8869let _galleryPickTarget = null;
8870let _slideSelections = {1:null,2:null,3:null};
8871
8872function clearSlotSelection(n){
8873  _slideSelections[n]=null;
8874  const lbl=$(`slide-sel-${n}`); if(lbl) lbl.style.display='none';
8875  const prev=$(`slide-preview-${n}`); if(prev) prev.style.display='none';
8876}
8877
8878async function openGalleryPicker(slideNum){
8879  _galleryPickTarget=slideNum;
8880  $('gp-slot-label').textContent=slideNum;
8881  $('gallery-picker-modal').style.display='flex';
8882  const grid=$('gp-grid');
8883  grid.innerHTML='<div style="font-size:.72rem;color:var(--muted);padding:8px">Loading gallery…</div>';
8884  try{
8885    const res=await api('GET','/api/outputs');
8886    const data=await res.json();
8887    const outputs=Array.isArray(data)?data:(data.outputs||[]);
8888    if(!outputs.length){grid.innerHTML='<div style="font-size:.72rem;color:var(--muted);padding:8px">No recent outputs found. Generate some images first.</div>';return;}
8889    grid.innerHTML=outputs.flatMap(o=>{
8890      const slides=(o.slides||[]);
8891      if(!slides.length&&o.thumbnail) return [`<div onclick="selectGalleryImage('${o.thumbnail}',${slideNum})" style="cursor:pointer;border:2px solid transparent;border-radius:8px;overflow:hidden;transition:border .15s" onmouseover="this.style.borderColor='#E2C275'" onmouseout="this.style.borderColor='transparent'"><img src="${o.thumbnail}" style="width:100%;aspect-ratio:1;object-fit:cover" onerror="this.parentElement.style.display='none'"><div style="font-size:.6rem;color:var(--muted);padding:3px 5px">${new Date(o.created_at).toLocaleDateString()}</div></div>`];
8892      return slides.map(s=>{const url=s.cloudinary_url||s.url||s.render_url;if(!url)return'';return`<div onclick="selectGalleryImage('${url}',${slideNum})" style="cursor:pointer;border:2px solid transparent;border-radius:8px;overflow:hidden;transition:border .15s" onmouseover="this.style.borderColor='#E2C275'" onmouseout="this.style.borderColor='transparent'"><img src="${url}" style="width:100%;aspect-ratio:1;object-fit:cover" onerror="this.parentElement.style.display='none'"><div style="font-size:.6rem;color:var(--muted);padding:3px 5px">Slide ${s.slide||''}</div></div>`;});
8893    }).join('');
8894  }catch(e){grid.innerHTML='<div style="font-size:.72rem;color:var(--red);padding:8px">Failed to load gallery</div>';}
8895}
8896
8897function selectGalleryImage(url,slideNum){
8898  _slideSelections[slideNum]={type:'url',value:url};
8899  const lbl=$(`slide-sel-${slideNum}`);
8900  if(lbl) lbl.style.display='inline';
8901  const prev=$(`slide-preview-${slideNum}`);
8902  if(prev){prev.querySelector('img').src=url;prev.style.display='block';}
8903  closeGalleryPicker();
8904  toast(`Image set for Slide ${slideNum}`,'ok');
8905}
8906
8907function closeGalleryPicker(){
8908  $('gallery-picker-modal').style.display='none';
8909}
8910
8911function useAiImageForSlot(n){
8912  const img=$('ai-chart-img');
8913  if(!img||!img.src) return;
8914  selectGalleryImage(img.src,n);
8915}
8916
8917// ─── AI Chart Designer (Factoz AI Image Generator) ───────────
8918async function runAiChart(caseType){
8919  const allBtns = ['ai-chart-bull-btn','ai-chart-bear-btn','ai-chart-neutral-btn'];
8920  allBtns.forEach(id=>{ const el=$(id); if(el) el.disabled=true; });
8921  const status = $('ai-chart-status');
8922  status.textContent = '⏳ Generating AI art… (10–20s)';
8923  $('ai-chart-result').style.display = 'none';
8924  const fd = new FormData();
8925  fd.append('case', caseType);
8926  try {
8927    const res = await fetch('/api/ai-chart/analyze', {
8928      method: 'POST',
8929      headers: {Authorization: 'Bearer ' + localStorage.getItem('fz_token')},
8930      body: fd,
8931    });
8932    const d = await res.json();
8933    if(res.ok){
8934      $('ai-chart-img').src = d.url;
8935      $('ai-chart-result').style.display = 'block';
8936      status.textContent = '';
8937      toast('AI art saved to gallery!', 'ok');
8938    } else {
8939      status.textContent = '❌ ' + (d.detail || 'Failed');
8940      toast(d.detail || 'Failed', 'err');
8941    }
8942  } catch(e){
8943    status.textContent = '❌ Error: ' + e.message;
8944  }
8945  allBtns.forEach(id=>{ const el=$(id); if(el) el.disabled=false; });
8946}
8947
8948function downloadAiChartImage(){
8949  const src = $('ai-chart-img').src;
8950  if(!src) return;
8951  const a = document.createElement('a');
8952  a.href = src;
8953  a.download = 'factoz_ai_chart_' + Date.now() + '.png';
8954  a.click();
8955}
8956
8957// ─── Gallery lightbox ────────────────────────────────────────
8958let _lbSlides = [];
8959let _lbIndex  = 0;
8960
8961function openGalleryLightbox(id){
8962  const item = _galleryItems[id];
8963  if(!item) return;
8964  const typeLabel = {carousel_pil:'Carousel',carousel_templated:'Carousel',collage:'Collage',design_studio:'Studio',ai_image:'AI Image',upload:'Upload'}[item.output_type]||item.output_type;
8965  _lbSlides = (item.slides && item.slides.length) ? item.slides.map(s=>
8965s.url).filter(Boolean) : [item.thumbnail];
8966  _lbIndex  = 0;
8967  $('lb-type-label').textContent = typeLabel.toUpperCase();
8968  lbRender();
8969  const lb = $('gallery-lightbox');
8970  lb.style.display = 'flex';
8971  document.addEventListener('keydown', _lbKeyHandler);
8972}
8973
8974function lbRender(){
8975  $('lb-img').src = _lbSlides[_lbIndex] || '';
8976  const multi = _lbSlides.length > 1;
8977  $('lb-slide-counter').textContent = multi ? `${_lbIndex+1} / ${_lbSlides.length}` : '';
8978  const nav = $('lb-nav');
8979  nav.style.display = multi ? 'flex' : 'none';
8980  if(multi){
8981    $('lb-dots').innerHTML = _lbSlides.map((_,i)=>
8982      `<div onclick="lbGoto(${i})" style="width:7px;height:7px;border-radius:50%;cursor:pointer;background:${i===_lbIndex?'#fff':'rgba(255,255,255,.3)'}"></div>`
8983    ).join('');
8984  }
8985}
8986
8987function lbNav(dir){
8988  _lbIndex = (_lbIndex + dir + _lbSlides.length) % _lbSlides.length;
8989  lbRender();
8990}
8991
8992function lbGoto(i){
8993  _lbIndex = i;
8994  lbRender();
8995}
8996
8997function closeGalleryLightbox(){
8998  $('gallery-lightbox').style.display = 'none';
8999  document.removeEventListener('keydown', _lbKeyHandler);
9000}
9001
9002function _lbKeyHandler(e){
9003  if(e.key==='Escape') closeGalleryLightbox();
9004  else if(e.key==='ArrowRight') lbNav(1);
9005  else if(e.key==='ArrowLeft')  lbNav(-1);
9006}
9007
9008async function downloadGalleryImage(){
9009  const url = _lbSlides[_lbIndex];
9010  if(!url) return;
9011  try {
9012    const res = await fetch(url);
9013    const blob = await res.blob();
9014    const a = document.createElement('a');
9015    a.href = URL.createObjectURL(blob);
9016    const ext = url.split('.').pop().split('?')[0] || 'jpg';
9017    a.download = `factoz_image_${Date.now()}.${ext}`;
9018    a.click();
9019    URL.revokeObjectURL(a.href);
9020  } catch(e){
9021    // Cloudinary cross-origin fallback — open in new tab
9022    window.open(url, '_blank');
9023  }
9024}
9025
9026// ─── Financial Statements ────────────────────────────────────
9027let _finData = null;
9028let _finTab      = 'balance';
9029let _finMode     = 'public';
9030let _finHistory  = [];
9031let _finCompareA = null;
9032let _finCompareB = null;
9033let _finCompareTab = 'balance';
9034
9035function initFinancials(){ setFinMode('public'); loadFinHistory(false); }
9036
9037function newFinStatement(){
9038  _finData = null;
9039  $('fin-results').style.display = 'none';
9040  $('fin-status').textContent = '';
9041  loadFinHistory(false);
9042  $('page-financials').scrollIntoView({behavior:'smooth', block:'start'});
9043}
9044
9045// ── SECTOR HEATMAP ─────────────────────────────────────────
9046// ─── PORTFOLIO RISK / VaR ────────────────────────────────────────────────────
9047let _varTickers=[{ticker:'AAPL',weight:60},{ticker:'MSFT',weight:40}];
9048
9049function initVarPage(){
9050  renderVarTickerRows();
9051  $('var-results').style.display='none';
9052  $('var-loading').style.display='none';
9053  $('var-error').style.display='none';
9054  fetchVarPrices();
9055}
9056
9057async function fetchVarPrices(){
9058  const tickers=_varTickers.map(r=>r.ticker.trim().toUpperCase()).filter(Boolean);
9059  if(!tickers.length)return;
9060  const container=$('var-price-rows');
9061  if(!container)return;
9062  container.innerHTML='<div style="font-size:.75rem;color:var(--muted)">Loading…</div>';
9063  try{
9064    const res=await api('GET',`/api/var/prices?tickers=${tickers.join(',')}`);
9065    const data=await res.json();
9066    if(!res.ok){container.innerHTML='<div style="font-size:.75rem;color:var(--muted)">—</div>';return;}
9067    container.innerHTML=tickers.map(tk=>{
9068      const d=data[tk]||{};
9069      const price=d.price!=null?`$${d.price.toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2})}`:'—';
9070      const chg=d.change_pct??null;
9071      const chgColor=chg==null?'var(--muted)':chg>=0?'var(--green)':'var(--red)';
9072      const chgStr=chg==null?'':` ${chg>=0?'+':''}${chg.toFixed(2)}%`;
9073      const name=d.name&&d.name!==tk?`<div style="font-size:.65rem;color:var(--muted);margin-top:1px">${d.name}</div>`:'';
9074      return `<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border)">
9075        <div><span style="font-family:var(--mono);font-size:.82rem;font-weight:600;color:var(--text)">${tk}</span>${name}</div>
9076        <div style="text-align:right">
9077          <div style="font-family:var(--mono);font-size:.88rem;color:var(--text)">${price}</div>
9078          <div style="font-family:var(--mono);font-size:.72rem;color:${chgColor}">${chgStr}</div>
9079        </div>
9080      </div>`;
9081    }).join('');
9082  }catch(e){
9083    container.innerHTML='<div style="font-size:.75rem;color:var(--muted)">—</div>';
9084  }
9085}
9086
9087function renderVarTickerRows(){
9088  const container=$('var-ticker-rows');
9089  if(!container)return;
9090  container.innerHTML=_varTickers.map((row,i)=>`
9091    <div style="display:flex;gap:8px;align-items:center">
9092      <input type="text" value="${row.ticker}" placeholder="TICKER"
9093        style="width:90px;font-family:var(--mono);font-size:.8rem"
9094        class="input" oninput="_varTickers[${i}].ticker=this.value.toUpperCase()"
9095        onblur="if(this.value.trim())fetchVarPrices()">
9096      <input type="number" value="${row.weight}" min="0" max="100" step="1"
9097        style="width:80px;font-family:var(--mono);font-size:.8rem"
9098        class="input" oninput="_varTickers[${i}].weight=parseFloat(this.value)||0">
9099      <span style="font-size:.75rem;color:var(--muted)">%</span>
9100      ${_varTickers.length>1?`<button class="btn btn-ghost" style="font-size:.7rem;padding:4px 8px" onclick="removeVarTicker(${i})">✕</button>`:''}
9101    </div>`).join('');
9102}
9103
9104function addVarTicker(){
9105  if(_varTickers.length>=10){toast('Maximum 10 tickers','err');return;}
9106  _varTickers.push({ticker:'',weight:0});
9107  renderVarTickerRows();
9108  fetchVarPrices();
9109}
9110
9111function removeVarTicker(i){
9112  _varTickers.splice(i,1);
9113  renderVarTickerRows();
9114  fetchVarPrices();
9115}
9116
9117async function runVarAnalysis(){
9118  const errEl=$('var-error');
9119  errEl.style.display='none';
9120
9121  // Validate
9122  const tickers=_varTickers.map(r=>r.ticker.trim().toUpperCase()).filter(Boolean);
9123  const weights=_varTickers.filter(r=>r.ticker.trim()).map(r=>r.weight/100);
9124  const totalWeight=weights.reduce((a,b)=>a+b,0);
9125
9126  if(tickers.length===0){errEl.textContent='Add at least one ticker.';errEl.style.display='';return;}
9127  if(Math.abs(totalWeight-1)>0.02){
9128    errEl.textContent=`Weights must sum to 100% (currently ${(totalWeight*100).toFixed(1)}%).`;
9129    errEl.style.display='';return;
9130  }
9131
9132  const portfolioValue=parseFloat($('var-portfolio-value').value)||10000;
9133  const confidence=parseFloat($('var-confidence').value)||0.99;
9134
9135  const _runBtn=$('var-run-btn');
9136  if(_runBtn){_runBtn.innerHTML='<span class="spin"></span> Analyzing…';_runBtn.disabled=true;}
9137  $('var-loading').style.display='';
9138  $('var-results').style.display='none';
9139
9140  try{
9141    const res=await api('POST','/api/var/analyze',{
9142      tickers,weights,portfolio_value:portfolioValue,
9143      confidence,simulations:5000
9144    });
9145    const data=await res.json();
9146    if(!res.ok||data.historical_var_pct===undefined){
9147      throw new Error(data&&data.detail?data.detail:'Calculation failed — check your tickers and try again');
9148    }
9149    renderVarResults(data);
9150    fetchVarPrices();
9151  }catch(e){
9152    errEl.textContent=e.message||'Analysis failed. Check your tickers.';
9153    errEl.style.display='';
9154  }finally{
9155    $('var-loading').style.display='none';
9156    if(_runBtn){_runBtn.innerHTML='Run Analysis';_runBtn.disabled=false;}
9157  }
9158}
9159
9160function renderVarResults(d){
9161  // Metrics grid
9162  const pct=v=>`${v>0?'+':''}${v.toFixed(2)}%`;
9163  const usd=v=>`$${Math.abs(v).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2})}`;
9164  const metrics=[
9165    {label:'Historical VaR',value:usd(d.historical_var_usd),sub:`${Math.abs(d.historical_var_pct).toFixed(2)}% of portfolio`,color:'var(--red)'},
9166    {label:'Monte Carlo VaR',value:usd(d.monte_carlo_var_usd),sub:`${Math.abs(d.monte_carlo_var_pct).toFixed(2)}% of portfolio`,color:'var(--red)'},
9167    {label:'CVaR (Exp. Shortfall)',value:usd(d.cvar_usd),sub:`${Math.abs(d.cvar_pct).toFixed(2)}% of portfolio`,color:'#ff6b6b'},
9168    {label:'Max Drawdown',value:`${d.max_drawdown_pct.toFixed(2)}%`,sub:'1-year period',color:'var(--yellow)'},
9169    {label:'Annual Return',value:pct(d.annual_return_pct),sub:'1-year',color:d.annual_return_pct>=0?'var(--green)':'var(--red)'},
9170    {label:'Annual Volatility',value:`${d.annual_volatility_pct.toFixed(2)}%`,sub:'1-year',color:'var(--text2)'},
9171    {label:'Sharpe Ratio',value:d.sharpe_ratio.toFixed(3),sub:'Risk-adjusted return',color:d.sharpe_ratio>=1?'var(--green)':d.sharpe_ratio>=0?'var(--yellow)':'var(--red)'},
9172    {label:'Portfolio',value:d.tickers.map((t,i)=>`${t} ${(d.weights[i]*100).toFixed(0)}%`).join(' · '),sub:'allocation',color:'var(--text2)'},
9173  ];
9174  $('var-metrics-grid').innerHTML=metrics.map(m=>`
9175    <div class="card" style="padding:16px">
9176      <div style="font-size:.72rem;color:var(--muted);font-family:var(--mono);letter-spacing:1px;margin-bottom:6px">${m.label.toUpperCase()}</div>
9177      <div style="font-size:1.35rem;font-weight:700;font-family:var(--mono);color:${m.color}">${m.value}</div>
9178      <div style="font-size:.75rem;color:var(--text2);margin-top:4px">${m.sub}</div>
9179    </div>`).join('');
9180
9181  // Monte Carlo paths chart
9182  const days=Array.from({length:d.mc_paths[0].length},(_,i)=>i+1);
9183  // Sort paths by final portfolio value so we can label worst/median/best
9184  const sortedPaths=[...d.mc_paths].sort((a,b)=>a[a.length-1]-b[b.length-1]);
9185  const midIdx=Math.floor(sortedPaths.length/2);
9186  const pathTraces=sortedPaths.map((path,i)=>{
9187    const isBest=i===sortedPaths.length-1;
9188    const isWorst=i===0;
9189    const isMiddle=i===midIdx;
9190    const label=isBest?'Optimistic 📈':isWorst?'Pessimistic 📉':isMiddle?'Base Case':'';
9191    return{
9192      x:days,y:path,mode:'lines',
9193      name:label,
9194      showlegend:isBest||isWorst||isMiddle,
9195      line:{
9196        width:isBest||isWorst||isMiddle?2:0.8,
9197        color:isBest?'rgba(0,230,100,0.9)':isWorst?'rgba(220,80,80,0.8)':isMiddle?'rgba(0,200,83,0.8)':'rgba(0,180,80,0.18)',
9198        dash:isWorst?'dot':'solid',
9199      },
9200      hovertemplate:(isBest||isWorst||isMiddle)
9201        ?`${isBest?'Optimistic':isWorst?'Pessimistic':'Base Case'} — Day %{x}: $%{y:,.0f}<extra></extra>`
9202        :'Day %{x}: $%{y:,.0f}<extra></extra>',
9203    };
9204  });
9205  const varLine=d.monte_carlo_var_usd;
9206  pathTraces.push({
9207    x:[1,days.length],y:[d.portfolio_value-varLine,d.portfolio_value-varLine],
9208    mode:'lines',line:{color:'rgba(242,54,69,0.75)',width:1.5,dash:'dot'},
9209    name:`99% VaR ($${varLine.toLocaleString('en-US',{maximumFractionDigits:0})})`,
9210    hovertemplate:`VaR floor: $%{y:,.0f}<extra></extra>`
9211  });
9212  Plotly.newPlot('var-chart-paths',pathTraces,{
9213    paper_bgcolor:'transparent',plot_bgcolor:'transparent',
9214    font:{color:'#a0a0a0',size:11},
9215    xaxis:{title:'Trading Days',dtick:50,tick0:0,gridcolor:'rgba(255,255,255,0.05)',zeroline:false},
9216    yaxis:{title:'Portfolio Value (USD)',gridcolor:'rgba(255,255,255,0.05)',zeroline:false,tickformat:'$,.0f'},
9217    margin:{l:60,r:20,t:10,b:40},
9218    legend:{x:0,y:1,bgcolor:'transparent'}
9219  },{responsive:true,displayModeBar:false});
9220
9221  // Return distribution histogram
9222  Plotly.newPlot('var-chart-dist',[{
9223    x:d.return_distribution,type:'histogram',nbinsx:60,
9224    marker:{color:'rgba(0,200,83,0.5)',line:{color:'rgba(0,200,83,0.8)',width:0.5}},
9225    name:'Daily Returns',hovertemplate:'Return: %{x:.2f}%<extra></extra>'
9226  }],{
9227    paper_bgcolor:'transparent',plot_bgcolor:'transparent',
9228    font:{color:'#a0a0a0',size:11},
9229    xaxis:{title:'Daily Return (%)',gridcolor:'rgba(255,255,255,0.05)',zeroline:false},
9230    yaxis:{title:'Frequency',gridcolor:'rgba(255,255,255,0.05)',zeroline:false},
9231    margin:{l:50,r:20,t:30,b:40},
9232    bargap:0.05,
9233    showlegend:false,
9234    shapes:[
9235      {type:'line',x0:d.historical_var_pct,x1:d.historical_var_pct,y0:0,y1:1,
9236       xref:'x',yref:'paper',line:{color:'#f23645',width:2,dash:'dash'}},
9237      {type:'line',x0:d.cvar_pct,x1:d.cvar_pct,y0:0,y1:1,
9238       xref:'x',yref:'paper',line:{color:'#ff9900',width:2,dash:'dot'}},
9239    ],
9240    annotations:[
9241      {x:d.historical_var_pct,y:1,xref:'x',yref:'paper',
9242       text:`VaR ${d.historical_var_pct.toFixed(2)}%`,
9243       showarrow:true,arrowhead:2,arrowcolor:'#f23645',arrowsize:0.8,
9244       font:{color:'#f23645',size:10},ax:30,ay:-20,
9245       bgcolor:'rgba(0,0,0,0.55)',borderpad:3},
9246      {x:d.cvar_pct,y:0.78,xref:'x',yref:'paper',
9247       text:`CVaR ${d.cvar_pct.toFixed(2)}%`,
9248       showarrow:true,arrowhead:2,arrowcolor:'#ff9900',arrowsize:0.8,
9249       font:{color:'#ff9900',size:10},ax:-35,ay:-20,
9250       bgcolor:'rgba(0,0,0,0.55)',borderpad:3},
9251    ]
9252  },{responsive:true,displayModeBar:false});
9253
9254  $('var-results').style.display='';
9255}
9256async function runPortfolioOptimize(){
9257  const tickers=_varTickers.map(r=>r.ticker.trim().toUpperCase()).filter(Boolean);
9258  const errEl=$('var-opt-error');
9259  errEl.style.display='none';
9260  if(tickers.length<2){errEl.textContent='Add at least 2 tickers to optimize.';errEl.style.display='';return;}
9261  const _optBtn=$('var-opt-btn');
9262  if(_optBtn){_optBtn.innerHTML='<span class="spin"></span> Optimizing…';_optBtn.disabled=true;}
9263  $('var-opt-loading').style.display='';
9264  $('var-optimize-results').style.display='none';
9265  try{
9266    const res=await api('POST','/api/var/optimize',{tickers});
9267    if(!res.ok){const e=await res.json().catch(()=>({}));throw new Error(e.detail||'Optimization failed');}
9268    const d=await res.json();
9269    renderOptimizeResults(d,tickers);
9270  }catch(e){errEl.textContent=e.message||'Optimization error';errEl.style.display='';}
9271  finally{
9272    $('var-opt-loading').style.display='none';
9273    if(_optBtn){_optBtn.innerHTML='⚡ Optimize';_optBtn.disabled=false;}
9274  }
9275}
9276
9277function renderOptimizeResults(d,currentTickers){
9278  const cur=Object.fromEntries((_varTickers||[]).map(r=>[r.ticker.toUpperCase(),r.weight]));
9279  // ── Metric cards ─────────────────────────────────────────────────────────
9280  const card=(label,data,color)=>`
9281    <div class="card" style="border-color:${color}20">
9282      <div style="font-size:.6rem;color:${color};font-family:var(--mono);letter-spacing:1px;font-weight:700;margin-bottom:10px">${label}</div>
9283      <div style="display:flex;flex-direction:column;gap:5px;font-size:.78rem">
9284        <div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Annual Return</span><span style="color:${color};font-weight:700">${data.annual_return_pct}%</span></div>
9285        <div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Volatility</span><span style="font-weight:700">${data.annual_volatility_pct}%</span></div>
9286        <div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Sharpe Ratio</span><span style="font-weight:700">${data.sharpe_ratio}</span></div>
9287      </div>
9288    </div>`;
9289  $('var-opt-cards').innerHTML=card('MAX SHARPE RATIO',d.max_sharpe,'#e2c275')+card('MIN VOLATILITY',d.min_volatility,'var(--teal)');
9290
9291  // ── Efficient frontier chart ──────────────────────────────────────────────
9292  const frontier={x:d.frontier.x,y:d.frontier.y,mode:'lines',name:'Efficient Frontier',line:{color:'rgba(226,194,117,0.6)',width:2}};
9293  const spPoint={x:[d.max_sharpe.annual_volatility_pct],y:[d.max_sharpe.annual_return_pct],mode:'markers',name:'Max Sharpe',marker:{color:'#e2c275',size:12,symbol:'star'}};
9294  const mvPoint={x:[d.min_volatility.annual_volatility_pct],y:[d.min_volatility.annual_return_pct],mode:'markers',name:'Min Volatility',marker:{color:'#26c6da',size:12,symbol:'diamond'}};
9295  const traces=[frontier,spPoint,mvPoint];
9296  // Current portfolio point if VaR was run
9297  const curVol=$('var-metrics-grid')?.querySelector?.('[data-metric="vol"]')?.dataset?.val;
9298  const curRet=$('var-metrics-grid')?.querySelector?.('[data-metric="ret"]')?.dataset?.val;
9299  if(curVol&&curRet) traces.push({x:[parseFloat(curVol)],y:[parseFloat(curRet)],mode:'markers',name:'Current',marker:{color:'#ef4444',size:10,symbol:'circle'}});
9300  Plotly.newPlot('var-chart-frontier',traces,{
9301    paper_bgcolor:'transparent',plot_bgcolor:'transparent',
9302    font:{color:'#b0c4de',size:11},
9303    xaxis:{title:'Volatility (%)',gridcolor:'#1e2a45',zerolinecolor:'#1e2a45'},
9304    yaxis:{title:'Annual Return (%)',gridcolor:'#1e2a45',zerolinecolor:'#1e2a45'},
9305    legend:{bgcolor:'transparent'},margin:{t:10,b:50,l:50,r:10},
9306  },{responsive:true,displayModeBar:false});
9307
9308  // ── Weight comparison table ───────────────────────────────────────────────
9309  const allTickers=[...new Set([...currentTickers,...Object.keys(d.max_sharpe.weights),...Object.keys(d.min_volatility.weights)])];
9310  const rows=allTickers.map(tk=>`
9311    <tr style="border-bottom:1px solid var(--border)">
9312      <td style="padding:7px 10px;font-family:var(--mono);font-weight:700;color:var(--text)">${tk}</td>
9313      <td style="padding:7px 10px;text-align:right;color:var(--muted)">${(cur[tk]||0).toFixed(1)}%</td>
9314      <td style="padding:7px 10px;text-align:right;color:#e2c275;font-weight:700">${(d.max_sharpe.weights[tk]||0).toFixed(1)}%</td>
9315      <td style="padding:7px 10px;text-align:right;color:#26c6da;font-weight:700">${(d.min_volatility.weights[tk]||0).toFixed(1)}%</td>
9316    </tr>`).join('');
9317  $('var-opt-table').innerHTML=`
9318    <table style="width:100%;border-collapse:collapse;font-size:.78rem">
9319      <thead><tr style="border-bottom:2px solid var(--border)">
9320        <th style="padding:7px 10px;text-align:left;color:var(--muted);font-weight:600">TICKER</th>
9321        <th style="padding:7px 10px;text-align:right;color:var(--muted);font-weight:600">CURRENT</th>
9322        <th style="padding:7px 10px;text-align:right;color:#e2c275;font-weight:600">MAX SHARPE</th>
9323        <th style="padding:7px 10px;text-align:right;color:#26c6da;font-weight:600">MIN VOL</th>
9324      </tr></thead>
9325      <tbody>${rows}</tbody>
9326    </table>`;
9327
9328  $('var-optimize-results').style.display='';
9329}
9330
9331// ─── END PORTFOLIO RISK ───────────────────────────────────────────────────────
9332
9333// ─── PADDLE PORTAL ────────────────────────────────────────────────────────────
9334async function manageSubscription() {
9335  const btn = $('manage-sub-btn');
9336  const orig = btn ? btn.textContent : '';
9337  if (btn) btn.textContent = '⏳ loading…';
9338  try {
9339    const res = await api('GET', '/api/paddle/portal-url');
9340    const data = await res.json();
9341    if (data.portal_url) {
9342      window.open(data.portal_url, '_blank');
9343    } else {
9344      toast(data.detail||'Could not load subscription portal','err');
9345    }
9346  } catch(e) {
9347    toast('Connection error — please try again','err');
9348  } finally {
9349    if (btn) btn.textContent = orig;
9350  }
9351}
9352
9353// ─── PADDLE SUBSCRIPTION CHECKOUT ────────────────────────────────────────────
9354function paddleCheckout(plan, emailOverride) {
9355  const email = emailOverride || USER.email || '';
9356  const priceIds = {
9357    creator: 'pri_01kkn2vffa3v1e5ttkjakq0xwc',
9358    pro:     'pri_01kkn2x451ydvjegg0w6kzrp6d',
9359    stellar: window._paddleStellarId || '',
9360  };
9361  const priceId = priceIds[plan] || priceIds.creator;
9362  if (!priceId) { toast('Stellar checkout not yet configured — check back soon', 'err'); return; }
9363  try {
9364    Paddle.Checkout.open({
9365      items: [{ priceId, quantity: 1 }],
9366      customer: email ? { email } : undefined,
9367      settings: { successUrl: window.location.origin + '/?paid=1' },
9368    });
9369  } catch(e) {
9370    // Paddle.js not ready — fallback to hosted checkout
9371    const base = `https://buy.paddle.com/checkout?items[0][price_id]=${priceId}&items[0][quantity]=1`;
9372    window.open(email ? `${base}&customer_email=${encodeURIComponent(email)}` : base, '_blank');
9373  }
9374}
9375
9376// ─── CRYPTO SUBSCRIPTION CHECKOUT ────────────────────────────────────────────
9377async function cryptoCheckout(plan) {
9378  const btn = event.currentTarget;
9379  const orig = btn.textContent;
9380  btn.textContent = 'Creating invoice…';
9381  btn.disabled = true;
9382  try {
9383    const res = await api('GET', `/api/payments/subscription-checkout?plan=${plan}`);
9384    const data = await res.json();
9385    if (data.invoice_url) {
9386      window.open(data.invoice_url, '_blank');
9387    } else {
9388      toast('Could not create invoice — try again','err');
9389    }
9390  } catch(e) {
9391    toast('Payment provider error — try again','err');
9392  } finally {
9393    btn.textContent = orig;
9394    btn.disabled = false;
9395  }
9396}
9397
9398async function loadHeatmap(){
9399  const status=$('heatmap-status');
9400  const grid=$('heatmap-grid');
9401  status.textContent='⏳ Loading sector data…';
9402  grid.innerHTML='';
9403  try{
9404    const res=await api('GET','/api/scanner/heatmap');
9405    const d=await res.json();
9406    if(!res.ok){status.textContent='❌ '+(d.detail||'Failed');return;}
9407    status.textContent='';
9408    const asOf=new Date(d.as_of);
9409    $('heatmap-as-of').textContent='Updated '+asOf.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'});
9410    grid.innerHTML=d.sectors.map(s=>{
9411      const c=s.change_pct;
9412      const bg=c>=1.5?'#1a4731':c>=0?'#163a25':c>=-1.5?'#3d1a1a':'#5c1010';
9413      const col=c>=0?'#4caf82':'#ef5350';
9414      const sign=c>=0?'+':'';
9415      const mv=JSON.stringify(s.movers||{}).replace(/'/g,'&#39;');
9416      return `<div style="background:${bg};border:1px solid ${col}33;border-radius:10px;padding:14px 12px;cursor:default" data-movers='${mv}' data-sector="${s.sector}">
9417        <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);margin-bottom:4px">${s.ticker}</div>
9418        <div style="font-size:.82rem;font-weight:700;color:var(--text);margin-bottom:6px">${s.sector}</div>
9419        <div style="font-size:1.1rem;font-weight:800;color:${col}">${sign}${c.toFixed(2)}%</div>
9420        <div style="font-size:.68rem;color:var(--muted);margin-top:3px">$${s.price.toFixed(2)}</div>
9421      </div>`;
9422    }).join('');
9423    _attachHeatmapTooltips();
9424  }catch(e){status.textContent='❌ '+e.message;}
9425}
9426
9427function _attachHeatmapTooltips(){
9428  const tip=document.getElementById('hm-tooltip');
9429  if(!tip) return;
9430  document.querySelectorAll('#heatmap-grid > div').forEach(cell=>{
9431    cell.addEventListener('mouseenter',e=>{
9432      try{
9433        const m=JSON.parse(cell.dataset.movers||'{}');
9434        const sector=cell.dataset.sector||'';
9435        if(!m.winners&&!m.losers) return;
9436        const winRows=(m.winners||[]).map(x=>`<div class="hmt-row"><span class="hmt-ticker hmt-up">${x.ticker}</span><span class="hmt-up">+${x.change_pct.toFixed(2)}%</span></div>`).join('');
9437        const loseRows=(m.losers||[]).map(x=>`<div class="hmt-row"><span class="hmt-ticker hmt-dn">${x.ticker}</span><span class="hmt-dn">${x.change_pct.toFixed(2)}%</span></div>`).join('');
9438        tip.innerHTML=`<div class="hmt-sector">${sector.toUpperCase()}</div><div class="hmt-label">▲ WINNERS</div>${winRows}<div class="hmt-label">▼ LOSERS</div>${loseRows}`;
9439        tip.style.display='block';
9440        _moveHmTip(e);
9441      }catch(_){}
9442    });
9443    cell.addEventListener('mousemove',_moveHmTip);
9444    cell.addEventListener('mouseleave',()=>{tip.style.display='none';});
9445  });
9446}
9447function _moveHmTip(e){
9448  const tip=document.getElementById('hm-tooltip');
9449  if(!tip) return;
9450  const x=e.clientX+14,y=e.clientY+14;
9451  tip.style.left=(x+tip.offsetWidth>window.innerWidth?x-tip.offsetWidth-28:x)+'px';
9452  tip.style.top=(y+tip.offsetHeight>window.innerHeight?y-tip.offsetHeight-28:y)+'px';
9453}
9454
9455function clearFinForm(mode){
9456  if(mode==='public'){
9457    $('fin-company').value='';
9458    $('fin-period').value='annual';
9459  } else if(mode==='private'){
9460    $('fin-private-company').value='';
9461    $('fin-private-period').value='annual';
9462    $('fin-file').value='';
9463    $('fin-file-name').textContent='';
9464    $('fin-scan-status').textContent='';
9465  } else if(mode==='raw'){
9466    $('fin-raw-company').value='';
9467    $('fin-raw-period').value='annual';
9468    $('fin-raw-text').value='';
9469  }
9470  $('fin-results').style.display='none';
9471  $('fin-status').textContent='';
9472  _finData=null;
9473}
9474
9475let _finHistOffset=0;
9476const _FIN_HIST_LIMIT=10;
9477
9478async function loadFinHistory(more){
9479  if(!more){ _finHistOffset=0; _finHistory=[]; }
9480  try{
9481    const res=await api('GET',`/api/financials/history?limit=${_FIN_HIST_LIMIT}&offset=${_finHistOffset}`);
9482    const d=await res.json();
9483    if(!res.ok) return;
9484    const items=d.items||[];
9485    _finHistory = more ? [..._finHistory, ...items] : items;
9486    renderFinHistoryList();
9487    $('fin-history-wrap').style.display=d.total>0?'block':'none';
9488    $('fin-history-more').style.display=(d.total>_finHistOffset+_FIN_HIST_LIMIT)?'block':'none';
9489    _finHistOffset+=items.length;
9490  }catch(e){}
9491}
9492
9493function renderFinHistoryList(){
9494  const modeIcon={public:'🏢',private:'📊',raw:'📝'};
9495  const aId=_finCompareA?.id, bId=_finCompareB?.id;
9496  $('fin-history-list').innerHTML=_finHistory.map(s=>`
9497    <div class="hist-row" style="${s.id===aId?'border-color:var(--teal);':s.id===bId?'border-color:var(--green);':''}align-items:center">
9498      <span style="font-size:.7rem;color:var(--muted);font-family:var(--mono)">${modeIcon[s.mode]||'📋'}</span>
9499      <span style="flex:1;font-size:.8rem;font-weight:600;color:var(--text);cursor:pointer" onclick='restoreFinStatement(${JSON.stringify(JSON.stringify(s))})'>${s.company}</span>
9500      <span style="font-size:.72rem;color:var(--text2)">${s.period}</span>
9501      <span style="font-size:.65rem;color:var(--muted);font-family:var(--mono)">${new Date(s.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})}</span>
9502      <button class="btn btn-ghost" style="font-size:.62rem;padding:3px 7px;${s.id===aId?'border-color:var(--teal);color:var(--teal)':''}" onclick="selectFinCompareSlot('${s.id}','A',event)">A</button>
9503      <button class="btn btn-ghost" style="font-size:.62rem;padding:3px 7px;${s.id===bId?'border-color:var(--green);color:var(--green)':''}" onclick="selectFinCompareSlot('${s.id}','B',event)">B</button>
9504      <button class="btn btn-ghost" style="font-size:.62rem;padding:3px 7px;color:var(--red)" onclick="deleteFinStatement('${s.id}',event)">🗑</button>
9505    </div>`).join('');
9506}
9507
9508async function selectFinCompareSlot(id, slot, evt){
9509  evt.stopPropagation();
9510  const item=_finHistory.find(h=>h.id===id);
9511  if(!item?.data){ toast('No data for this statement','err'); return; }
9512  if(slot==='A') _finCompareA=item; else _finCompareB=item;
9513  renderFinHistoryList();
9514  if(_finCompareA && _finCompareB) renderFinComparison();
9515  else{ $('fin-compare-wrap').style.display='none'; toast(`Set as ${slot} — now pick ${slot==='A'?'B':'A'}`, 'ok'); }
9516}
9517
9518function renderFinComparison(){
9519  const a=_finCompareA, b=_finCompareB;
9520  if(!a||!b) return;
9521  $('fin-compare-label').textContent=`${a.company}
9521 · ${a.period}  vs  ${b.company} · ${b.period}`;
9522  $('fin-compare-wrap').style.display='block';
9523  showFinCompareTab(_finCompareTab);
9524  $('fin-compare-wrap').scrollIntoView({behavior:'smooth',block:'nearest'});
9525}
9526
9527function showFinCompareTab(tab){
9528  _finCompareTab=tab;
9529  ['balance','income','cashflow','equity'].forEach(t=>{
9530    const btn=$('fcmp-tab-'+t);
9531    if(btn){ btn.className=t===tab?'btn active-filter':'btn btn-ghost'; btn.style.fontSize='.7rem'; btn.style.padding='4px 10px'; }
9532  });
9533  const a=_finCompareA, b=_finCompareB;
9534  if(!a||!b){ $('fin-compare-table').innerHTML=''; return; }
9535  const secMap={balance:['balance_sheet','Balance Sheet'],income:['income_statement','Income Statement'],cashflow:['cash_flow','Cash Flow'],equity:['equity_statement',"Shareholders' Equity"]};
9536  const [key,title]=secMap[tab]||secMap.balance;
9537  const rowsA=flattenFinObj(a.data[key]||{});
9538  const rowsB=flattenFinObj(b.data[key]||{});
9539  const mapB={}; rowsB.forEach(r=>mapB[r.label]=r.value);
9540  const fmt=v=>typeof v==='number'?v.toLocaleString():(v||'—');
9541  const rows=rowsA.map(r=>{
9542    const vA=typeof r.value==='number'?r.value:0;
9543    const vB=typeof mapB[r.label]==='number'?mapB[r.label]:0;
9544    const delta=vB-vA;
9545    const pct=vA?((delta/Math.abs(vA))*100).toFixed(1)+'%':'—';
9546    const col=delta===0?'var(--muted)':delta>0?'var(--green)':'var(--red)';
9547    return `<tr style="border-bottom:1px solid var(--border)">
9548      <td style="padding:6px 8px;color:var(--text2);font-size:.78rem">${r.label}</td>
9549      <td style="padding:6px 8px;text-align:right;font-family:var(--mono);font-size:.78rem">${fmt(r.value)}</td>
9550      <td style="padding:6px 8px;text-align:right;font-family:var(--mono);font-size:.78rem">${fmt(mapB[r.label]??'—')}</td>
9551      <td style="padding:6px 8px;text-align:right;font-family:var(--mono);font-size:.78rem;color:${col}">${delta===0?'—':(delta>0?'+':'')+delta.toLocaleString()}</td>
9552      <td style="padding:6px 8px;text-align:right;font-family:var(--mono);font-size:.78rem;color:${col}">${pct}</td>
9553    </tr>`;
9554  }).join('');
9555  $('fin-compare-table').innerHTML=`
9556    <div style="font-size:.65rem;color:var(--muted);letter-spacing:1px;margin-bottom:8px">${title.toUpperCase()}</div>
9557    <div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse">
9558      <thead><tr style="border-bottom:1px solid var(--border)">
9559        <th style="padding:6px 8px;text-align:left;font-size:.65rem;color:var(--muted);letter-spacing:1px">LINE ITEM</th>
9560        <th style="padding:6px 8px;text-align:right;font-size:.65rem;color:var(--teal);letter-spacing:1px">A</th>
9561        <th style="padding:6px 8px;text-align:right;font-size:.65rem;color:var(--green);letter-spacing:1px">B</th>
9562        <th style="padding:6px 8px;text-align:right;font-size:.65rem;color:var(--muted);letter-spacing:1px">DELTA</th>
9563        <th style="padding:6px 8px;text-align:right;font-size:.65rem;color:var(--muted);letter-spacing:1px">%</th>
9564      </tr></thead>
9565      <tbody>${rows}</tbody>
9566    </table></div>`;
9567}
9568
9569function clearFinCompare(){
9570  _finCompareA=null; _finCompareB=null;
9571  $('fin-compare-wrap').style.display='none';
9572  renderFinHistoryList();
9573}
9574
9575async function deleteFinStatement(id, evt){
9576  evt.stopPropagation();
9577  if(!confirm('Delete this statement?')) return;
9578  const res=await api('DELETE',`/api/financials/history/${id}`);
9579  if(res.ok||res.status===204){
9580    toast('Deleted','ok');
9581    if(_finCompareA?.id===id) _finCompareA=null;
9582    if(_finCompareB?.id===id) _finCompareB=null;
9583    loadFinHistory(false);
9584  } else toast('Delete failed','err');
9585}
9586
9587function downloadFinCompareCSV(){
9588  if(!_finCompareA||!_finCompareB) return;
9589  const secMap={balance:'balance_sheet',income:'income_statement',cashflow:'cash_flow',equity:'equity_statement'};
9590  const key=secMap[_finCompareTab]||'balance_sheet';
9591  const rowsA=flattenFinObj(_finCompareA.data[key]||{});
9592  const rowsB=flattenFinObj(_finCompareB.data[key]||{});
9593  const mapB={}; rowsB.forEach(r=>mapB[r.label]=r.value);
9594  const labelA=`${_finCompareA.company} (${_finCompareA.period})`;
9595  const labelB=`${_finCompareB.company} (${_finCompareB.period})`;
9596  const csv=`"Line Item","${labelA}","${labelB}","Delta","% Change"\n`+rowsA.map(r=>{
9597    const vA=typeof r.value==='number'?r.value:0;
9598    const vB=typeof mapB[r.label]==='number'?mapB[r.label]:0;
9599    const delta=vB-vA;
9600    const pct=vA?((delta/Math.abs(vA))*100).toFixed(1)+'%':'—';
9601    return `"${r.label}",${vA},${vB},${delta},"${pct}"`;
9602  }).join('\n');
9603  const a=document.createElement('a');
9604  a.href='data:text/csv;charset=utf-8,'+encodeURIComponent(csv);
9605  a.download=`compare_${_finCompareTab}_${Date.now()}.csv`;
9606  a.click();
9607}
9608
9609function restoreFinStatement(sJson){
9610  const s=JSON.parse(sJson);
9611  _finData=s.data;
9612  $('fin-status').textContent=`✅ ${s.company} · ${s.period} · ${s.data.currency||'USD'} (${s.data.unit||''})`;
9613  showFinTab('balance');
9614  $('fin-results').style.display='block';
9615  $('fin-results').scrollIntoView({behavior:'smooth',block:'nearest'});
9616}
9617
9618function setFinMode(mode){
9619  _finMode = mode;
9620  $('fin-public-form').style.display  = mode==='public'  ? 'block' : 'none';
9621  $('fin-private-form').style.display = mode==='private' ? 'block' : 'none';
9622  $('fin-raw-form').style.display     = mode==='raw'     ? 'block' : 'none';
9623  $('fin-mode-public').className  = mode==='public'  ? 'btn active-filter' : 'btn btn-ghost';
9624  $('fin-mode-private').className = mode==='private' ? 'btn active-filter' : 'btn btn-ghost';
9625  $('fin-mode-raw').className     = mode==='raw'     ? 'btn active-filter' : 'btn btn-ghost';
9626  $('fin-results').style.display  = 'none';
9627  $('fin-status').textContent     = '';
9628}
9629
9630async function generateFinancials(){
9631  const company = ($('fin-company').value||'').trim();
9632  if(!company){ toast('Enter a company or ticker','err'); return; }
9633  $('fin-status').textContent = '⏳ Fetching financial statements via Sonar AI…';
9634  $('fin-results').style.display = 'none';
9635  try {
9636    const res = await api('POST', '/api/financials/public', { company, period: $('fin-period').value });
9637    const d = await res.json();
9638    if(!res.ok){ $('fin-status').textContent = '❌ ' + (d.detail||'Failed'); return; }
9639    _finData = d;
9640    $('fin-status').textContent = `✅ ${d.company||company} · ${d.period||''} · ${d.currency||'USD'} (${d.unit||''})`;
9641    showFinTab('balance');
9642    $('fin-results').style.display = 'block';
9643  } catch(e){ $('fin-status').textContent = '❌ Error: '+e.message; }
9644}
9645
9646async function generateFinancialsPrivate(){
9647  const company   = ($('fin-private-company').value||'').trim();
9648  const fileInput = $('fin-file');
9649  if(!company){ toast('Enter company name','err'); return; }
9650  if(!fileInput.files[0]){ toast('Upload a file','err'); return; }
9651  $('fin-status').textContent = '⏳ Processing file and generating statements…';
9652  $('fin-results').style.display = 'none';
9653  const fd = new FormData();
9654  fd.append('file', fileInput.files[0]);
9655  fd.append('company_name', company);
9656  fd.append('period', $('fin-private-period').value);
9657  try {
9658    const res = await fetch('/api/financials/private', {
9659      method:'POST', headers:{Authorization:'Bearer '+localStorage.getItem('fz_token')}, body: fd
9660    });
9661    const d = await res.json();
9662    if(!res.ok){ $('fin-status').textContent = '❌ ' + (d.detail||'Failed'); return; }
9663    _finData = d;
9664    $('fin-status').textContent = `✅ ${d.company||company} · ${d.period||''} · ${d.currency||'USD'} (${d.unit||''})`;
9665    showFinTab('balance');
9666    $('fin-results').style.display = 'block';
9667  } catch(e){ $('fin-status').textContent = '❌ Error: '+e.message; }
9668}
9669
9670async function generateFinancialsRaw(){
9671  const company = ($('fin-raw-company').value||'').trim();
9672  const rawText = ($('fin-raw-text').value||'').trim();
9673  if(!company){ toast('Enter company name','err'); return; }
9674  if(!rawText){ toast('Paste some financial data first','err'); return; }
9675  $('fin-status').textContent = '⏳ Mapping data to GAAP statements via Sonar AI…';
9676  $('fin-results').style.display = 'none';
9677  try {
9678    const res = await api('POST', '/api/financials/raw', {
9679      company, period: $('fin-raw-period').value, raw_text: rawText
9680    });
9681    const d = await res.json();
9682    if(!res.ok){ $('fin-status').textContent = '❌ ' + (d.detail||'Failed'); return; }
9683    _finData = d;
9684    $('fin-status').textContent = `✅ ${d.company||company}
9684 · ${d.period||''} · ${d.currency||'USD'} (${d.unit||''})`;
9685    showFinTab('balance');
9686    $('fin-results').style.display = 'block';
9687  } catch(e){ $('fin-status').textContent = '❌ Error: '+e.message; }
9688}
9689
9690function showFinTab(tab){
9691  _finTab = tab;
9692  document.querySelectorAll('.fin-tab').forEach(b=>{
9693    b.classList.toggle('active-filter', b.dataset.tab===tab);
9694    b.classList.toggle('btn-ghost',     b.dataset.tab!==tab);
9695  });
9696  const el = $('fin-tab-content');
9697  if(!_finData){ el.innerHTML=''; return; }
9698  if(tab==='balance')  el.innerHTML = renderFinTable('Balance Sheet',          flattenFinObj(_finData.balance_sheet||{}));
9699  if(tab==='income')   el.innerHTML = renderFinTable('Income Statement',       flattenFinObj(_finData.income_statement||{}));
9700  if(tab==='cashflow') el.innerHTML = renderFinTable('Cash Flow Statement',    flattenFinObj(_finData.cash_flow||{}));
9701  if(tab==='equity')   el.innerHTML = renderFinTable("Shareholders' Equity",   flattenFinObj(_finData.equity_statement||{}));
9702  if(tab==='summary')  el.innerHTML = renderFinSummary(_finData);
9703}
9704
9705function flattenFinObj(obj, prefix=''){
9706  const rows = [];
9707  for(const [k, v] of Object.entries(obj)){
9708    const label = (prefix ? prefix+' · ' : '') + k.replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase());
9709    if(typeof v==='object' && v!==null) rows.push(...flattenFinObj(v, label));
9710    else rows.push({label, value: v});
9711  }
9712  return rows;
9713}
9714
9715function renderFinTable(title, rows){
9716  const currency = _finData?.currency||'USD';
9717  const unit     = _finData?.unit||'';
9718  return `<div style="font-size:.65rem;color:var(--muted);letter-spacing:1px;margin-bottom:10px">${title.toUpperCase()} · ${currency} · ${unit}</div>
9719  <table style="width:100%;border-collapse:collapse;font-size:.8rem">
9720    ${rows.map(r=>`<tr style="border-bottom:1px solid var(--border)">
9721      <td style="padding:6px 8px;color:var(--text2)">${r.label}</td>
9722      <td style="padding:6px 8px;text-align:right;color:var(--text);font-family:var(--mono)">${typeof r.value==='number'?r.value.toLocaleString():r.value}</td>
9723    </tr>`).join('')}
9724  </table>`;
9725}
9726
9727function renderFinSummary(data){
9728  if(!data) return '';
9729  const currency = data.currency||'USD';
9730  const unit = data.unit||'';
9731  const sections = [
9732    {key:'balance_sheet',    label:'Balance Sheet',        id:'fin-acc-bs'},
9733    {key:'income_statement', label:'Income Statement',     id:'fin-acc-is'},
9734    {key:'cash_flow',        label:'Cash Flow Statement',  id:'fin-acc-cf'},
9735    {key:'equity_statement', label:"Shareholders' Equity", id:'fin-acc-eq'},
9736  ];
9737  return sections.map((s,i)=>{
9738    const rows = flattenFinObj(data[s.key]||{});
9739    const tbl = rows.map(r=>`<tr style="border-bottom:1px solid var(--border)">
9740      <td style="padding:6px 8px;color:var(--text2)">${r.label}</td>
9741      <td style="padding:6px 8px;text-align:right;color:var(--text);font-family:var(--mono)">${typeof r.value==='number'?r.value.toLocaleString():r.value}</td>
9742    </tr>`).join('');
9743    const open = i===0;
9744    return `<div style="border:1px solid var(--border);border-radius:8px;margin-bottom:8px;overflow:hidden">
9745      <div onclick="toggleFinAcc('${s.id}')" style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;cursor:pointer;background:var(--bg2);user-select:none">
9746        <span style="font-size:.7rem;font-family:var(--mono);letter-spacing:2px;color:var(--text);font-weight:700">${s.label.toUpperCase()}</span>
9747        <span style="display:flex;align-items:center;gap:8px">
9748          <span style="font-size:.65rem;color:var(--muted)">${currency} · ${unit}</span>
9749          <span id="${s.id}-arrow" style="color:var(--muted);font-size:.75rem">${open?'▲':'▼'}</span>
9750        </span>
9751      </div>
9752      <div id="${s.id}" style="display:${open?'block':'none'}">
9753        <table style="width:100%;border-collapse:collapse;font-size:.8rem">${tbl}</table>
9754      </div>
9755    </div>`;
9756  }).join('');
9757}
9758
9759function toggleFinAcc(id){
9760  const el=$(id), arrow=$(id+'-arrow');
9761  if(!el) return;
9762  const open = el.style.display!=='none';
9763  el.style.display = open?'none':'block';
9764  if(arrow) arrow.textContent = open?'▼':'▲';
9765}
9766
9767function downloadFinTab(){
9768  if(!_finData||!_finTab) return;
9769  let rows=[];
9770  if(_finTab==='balance')  rows=flattenFinObj(_finData.balance_sheet||{});
9771  if(_finTab==='income')   rows=flattenFinObj(_finData.income_statement||{});
9772  if(_finTab==='cashflow') rows=flattenFinObj(_finData.cash_flow||{});
9773  if(_finTab==='equity')   rows=flattenFinObj(_finData.equity_statement||{});
9774  if(_finTab==='summary')  rows=[
9775    ...flattenFinObj(_finData.balance_sheet||{}).map(r=>({label:'Balance Sheet · '+r.label,value:r.value})),
9776    ...flattenFinObj(_finData.income_statement||{}).map(r=>({label:'Income Statement · '+r.label,value:r.value})),
9777    ...flattenFinObj(_finData.cash_flow||{}).map(r=>({label:'Cash Flow · '+r.label,value:r.value})),
9778    ...flattenFinObj(_finData.equity_statement||{}).map(r=>({label:"Shareholders Equity · "+r.label,value:r.value})),
9779  ];
9780  const csv='Line Item,Value\n'+rows.map(r=>`"${r.label}","${r.value}"`).join('\n');
9781  const a=document.createElement('a');
9782  a.href='data:text/csv;charset=utf-8,'+encodeURIComponent(csv);
9783  a.download=`${_finData.company||'company'}_${_finTab}_${Date.now()}.csv`;
9784  a.click();
9785}
9786
9787async function scanFinancialPhoto(input){
9788  if(!input.files[0]) return;
9789  $('fin-scan-status').textContent = '⏳ Reading handwriting…';
9790  const fd = new FormData();
9791  fd.append('file', input.files[0]);
9792  try {
9793    const res = await fetch('/api/financials/scan', {
9794      method:'POST', headers:{Authorization:'Bearer '+localStorage.getItem('fz_token')}, body: fd
9795    });
9796    if(!res.ok){
9797      const d = await res.json();
9798      $('fin-scan-status').textContent = '❌ ' + (d.detail||'Scan failed');
9799      return;
9800    }
9801    const blob = await res.blob();
9802    const a = document.createElement('a');
9803    a.href = URL.createObjectURL(blob);
9804    a.download = 'factoz_scanned_financials.xlsx';
9805    a.click();
9806    URL.revokeObjectURL(a.href);
9807    $('fin-scan-status').textContent = '✅ Downloaded — review, then upload above';
9808  } catch(e){
9809    $('fin-scan-status').textContent = '❌ Error: '+e.message;
9810  }
9811  input.value = '';
9812}
9813
9814// ─── Polymarket v2 ───────────────────────────────────────────
9815
9816// state
9817let _pmCurrentTab = 'markets';
9818let _pmMarkets    = [];
9819let _pmVolChart   = null;
9820let _pmSSE        = null;
9821let _pmVolTimer   = null;
9822let _pmWatches    = [];
9823
9824// tab switch
9825function pmTab(name){
9826  ['markets','volume','book','alerts'].forEach(t=>{
9827    const btn=$(`pm-tab-${t}`), panel=$(`pm-panel-${t}`);
9828    if(!btn||!panel)return;
9829    const active=(t===name);
9830    panel.style.display=active?'':'none';
9831    btn.classList.toggle('pm-tab-active',active);
9832  });
9833  _pmCurrentTab=name;
9834  if(name==='markets'   && !_pmMarkets.length) loadPmMarkets();
9835  if(name==='volume')    loadPmVolumeChart();
9836  if(name==='alerts'  ){ loadPmAlerts(); loadPmWatches(); }
9837}
9838
9839// ── MYFXBOOK / TRADING ─────────────────────────────────────────────────────
9840async function mfxInit(){
9841  // Check connection status and show the right panel
9842  try{
9843    const r=await fetch('/api/myfxbook/status',{headers:{'Authorization':'Bearer '+TOKEN}});
9844    if(!r.ok){_mfxShowConnect();return;}
9845    const d=await r.json();
9846    if(d.connected){
9847      $('mfx-connected-info').textContent=`Connected as ${d.email}`;
9848      _mfxShowConnected();
9849      mfxLoadAccounts();
9850    }else{
9851      _mfxShowConnect();
9852    }
9853  }catch(e){_mfxShowConnect();}
9854}
9855
9856function _mfxShowConnect(){
9857  $('mfx-connect-section').style.display='block';
9858  $('mfx-connected-section').style.display='none';
9859  $('mfx-status').style.display='none';
9860}
9861function _mfxShowConnected(){
9862  $('mfx-connect-section').style.display='none';
9863  $('mfx-connected-section').style.display='block';
9864}
9865
9866async function mfxConnect(){
9867  const email=$('mfx-email').value.trim();
9868  const pw=$('mfx-password').value;
9869  if(!email||!pw){toast('Enter email and password','err');return;}
9870  const btn=document.querySelector('#mfx-connect-section .btn-primary');
9871  if(btn){btn.disabled=true;btn.textContent='Connecting...';}
9872  try{
9873    const r=await fetch('/api/myfxbook/connect',{
9874      method:'POST',
9875      headers:{'Authorization':'Bearer '+TOKEN,'Content-Type':'application/json'},
9876      body:JSON.stringify({email,password:pw})
9877    });
9878    const d=await r.json();
9879    if(r.ok){
9880      toast('MyFXBook connected!','ok');
9881      $('mfx-password').value='';
9882      $('mfx-connected-info').textContent=`Connected as ${email}`;
9883      _mfxShowConnected();
9884      mfxLoadAccounts();
9885    }else{
9886      toast(d.detail||'Connection failed','err');
9887    }
9888  }catch(e){toast('Connection error','err');}
9889  finally{if(btn){btn.disabled=false;btn.textContent='Connect MyFXBook →';}}
9890}
9891
9892async function mfxDisconnect(){
9893  if(!confirm('Disconnect MyFXBook? You can reconnect anytime.'))return;
9894  try{
9895    const r=await fetch('/api/myfxbook/disconnect',{method:'DELETE',headers:{'Authorization':'Bearer '+TOKEN}});
9896    if(r.ok){toast('Disconnected','ok');_mfxShowConnect();}
9897  }catch(e){toast('Error','err');}
9898}
9899
9900async function mfxLoadAccounts(){
9901  const grid=$('mfx-accounts-grid');
9902  const status=$('mfx-status');
9903  grid.innerHTML='';
9904  status.style.display='block';
9905  status.textContent='Loading accounts...';
9906  try{
9907    const r=await fetch('/api/myfxbook/accounts',{headers:{'Authorization':'Bearer '+TOKEN}});
9908    if(r.status===401){
9909      const d=await r.json();
9910      if(d.detail==='session_expired'){
9911        status.textContent='Session expired — please reconnect.';
9912        _mfxShowConnect();
9913        return;
9914      }
9915    }
9916    if(!r.ok){status.textContent='Could not load accounts.';return;}
9917    const d=await r.json();
9918    const accs=d.accounts||[];
9919    status.style.display='none';
9920    if(!accs.length){status.style.display='block';status.textContent='No trading accounts found on this MyFXBook profile.';return;}
9921    accs.forEach(acc=>{ grid.insertAdjacentHTML('beforeend',_mfxAccountCard(acc)); });
9922  }catch(e){status.textContent='Error loading accounts.';}
9923}
9924
9925function _mfxAccountCard(acc){
9926  const gain=parseFloat(acc.gain||0);
9927  const dd=parseFloat(acc.drawdown||0);
9928  const bal=parseFloat(acc.balance||0);
9929  const gainColor=gain>=0?'var(--green)':'#f85149';
9930  const gainSign=gain>=0?'+':'';
9931  const currency=acc.currency||'USD';
9932  return `<div style="background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:20px">
9933    <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:14px">
9934      <div>
9935        <div style="font-weight:700;font-size:.92rem;color:var(--text)">${acc.name||'Account'}</div>
9936        <div style="font-size:.72rem;color:var(--muted);margin-top:2px">${acc.broker||''} · ${acc.demo?'Demo':'Live'}</div>
9937      </div>
9938      <div style="text-align:right">
9939        <div style="font-size:1.6rem;font-weight:800;font-family:var(--mono);color:${gainColor}">${gainSign}${gain.toFixed(2)}%</div>
9940        <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono)">TOTAL GAIN</div>
9941      </div>
9942    </div>
9943    <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px">
9944      <div style="background:var(--bg3);border-radius:8px;padding:10px">
9945        <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);margin-bottom:3px">BALANCE</div>
9946        <div style="font-size:.95rem;font-weight:700;color:var(--text);font-family:var(--mono)">${currency} ${bal.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</div>
9947      </div>
9948      <div style="background:var(--bg3);border-radius:8px;padding:10px">
9949        <div style="font-size:.65rem;color:var(--muted);font-family:var(--mono);margin-bottom:3px">MAX DRAWDOWN</div>
9950        <div style="font-size:.95rem;font-weight:700;color:#f85149;font-family:var(--mono)">${dd.toFixed(2)}%</div>
9951      </div>
9952    </div>
9953    <div style="display:flex;gap:8px">
9954      <button class="btn btn-ghost" style="flex:1;font-size:.75rem;justify-content:center" onclick="mfxViewTrades(${acc.id})">Open Trades</button>
9955      <button class="btn btn-ghost" style="flex:1;font-size:.75rem;justify-content:center" onclick="mfxRoiChart(${JSON.stringify(acc).replace(/"/g,'&quot;')})">📈 ROI Chart</button>
9956      <button class="btn btn-primary" style="flex:1;font-size:.75rem;justify-content:center" onclick="mfxGenerateCard(${JSON.stringify(acc).replace(/"/g,'&quot;')})">📤 Share Card</button>
9957    </div>
9958  </div>`;
9959}
9960
9961async function mfxViewTrades(accountId){
9962  try{
9963    const r=await fetch(`/api/myfxbook/accounts/${accountId}/trades`,{headers:{'Authorization':'Bearer '+TOKEN}});
9964    if(!r.ok){toast('Could not load trades','err');return;}
9965    const d=await r.json();
9966    const trades=d.trades||[];
9967    if(!trades.length){toast('No open trades','ok');return;}
9968    const rows=trades.map(t=>`<tr>
9969      <td style="padding:8px 12px">${t.symbol||t.pair||'—'}</td>
9970      <td style="padding:8px 12px">${t.type||'—'}</td>
9971      <td style="padding:8px 12px;font-family:var(--mono)">${t.openPrice||'—'}</td>
9972      <td style="padding:8px 12px;font-family:var(--mono);color:${parseFloat(t.profit||0)>=0?'var(--green)':'#f85149'}">${t.profit||'—'}</td>
9973    </tr>`).join('');
9974    const html=`<table style="width:100%;border-collapse:collapse;font-size:.82rem">
9975      <thead><tr style="border-bottom:1px solid var(--border);color:var(--muted)">
9976        <th style="padding:8px 12px;text-align:left">Symbol</th><th style="padding:8px 12px;text-align:left">Type</th>
9977        <th style="padding:8px 12px;text-align:left">Open</th><th style="padding:8px 12px;text-align:left">P&L</th>
9978      </tr></thead><tbody>${rows}</tbody></table>`;
9979    _mfxModal('Open Trades', html);
9980  }catch(e){toast('Error','err');}
9981}
9982
9983function _mfxModal(title, bodyHtml){
9984  let m=document.getElementById('mfx-modal');
9985  if(!m){
9986    m=document.createElement('div');
9987    m.id='mfx-modal';
9988    m.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:9000;display:flex;align-items:center;justify-content:center;padding:20px';
9989    m.onclick=e=>{if(e.target===m)m.remove();};
9990    document.body.appendChild(m);
9991  }
9992  m.innerHTML=`<div style="background:var(--bg2);border:1px solid var(--border);border-radius:14px;padding:24px;max-width:600px;width:100%;max-height:80vh;overflow-y:auto">
9993    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
9994      <div style="font-weight:700;font-size:.95rem">${title}</div>
9995      <button onclick="document.getElementById('mfx-modal').remove()" style="background:none;border:none;color:var(--muted);font-size:1.2rem;cursor:pointer">✕</button>
9996    </div>
9997    ${bodyHtml}
9998  </div>`;
9999  m.style.display='flex';
10000}
10001
10002async function mfxGenerateCard(acc){
10003  if(typeof acc==='string') acc=JSON.parse(acc);
10004  // Fetch equity curve for sparkline
10005  let gainData=[];
10006  try{
10007    const r=await fetch(`/api/myfxbook/accounts/${acc.id}/gain`,{headers:{'Authorization':'Bearer '+TOKEN}});
10008    if(r.ok){const d=await r.json();gainData=d.gain||[];}
10009  }catch(e){}
10010
10011  const canvas=$('mfx-card-canvas');
10012  canvas.width=1080; canvas.height=1080;
10013  const ctx=canvas.getContext('2d');
10014
10015  // Background
10016  ctx.fillStyle='#080808';
10017  ctx.fillRect(0,0,1080,1080);
10018
10019  // Border accent
10020  ctx.strokeStyle='#00c853';
10021  ctx.lineWidth=3;
10022  ctx.strokeRect(20,20,1040,1040);
10023
10024  // Header band
10025  ctx.fillStyle='rgba(0,200,83,.08)';
10026  ctx.fillRect(20,20,1040,120);
10027
10028  // FACTOZ wordmark
10029  ctx.fillStyle='#00c853';
10030  ctx.font='bold 28px monospace';
10031  ctx.fillText('FACTOZ',48,72);
10032
10033  // MyFXBook label
10034  ctx.fillStyle='rgba(255,255,255,.3)';
10035  ctx.font='14px monospace';
10036  ctx.textAlign='right';
10037  ctx.fillText('via MyFXBook',1032,72);
10038  ctx.textAlign='left';
10039
10040  // Account name
10041  ctx.fillStyle='rgba(255,255,255,.85)';
10042  ctx.font='bold 20px monospace';
10043  ctx.fillText((acc.name||'Trading Account').toUpperCase(),48,115);
10044
10045  // Broker / demo badge
10046  ctx.fillStyle='rgba(255,255,255,.35)';
10047  ctx.font='14px monospace';
10048  ctx.textAlign='right';
10049  ctx.fillText(`${acc.broker||'Broker'} · ${acc.demo?'Demo':'Live'}`,1032,115);
10050  ctx.textAlign='left';
10051
10052  // Big gain number
10053  const gain=parseFloat(acc.gain||0);
10054  const gainSign=gain>=0?'+':'';
10055  ctx.fillStyle=gain>=0?'#00c853':'#f85149';
10056  ctx.font='bold 160px monospace';
10057  ctx.textAlign='center';
10058  ctx.fillText(`${gainSign}${gain.toFixed(2)}%`,540,360);
10059
10060  ctx.fillStyle='rgba(255,255,255,.35)';
10061  ctx.font='20px monospace';
10062  ctx.fillText('TOTAL GAIN',540,400);
10063  ctx.textAlign='left';
10064
10065  // Stats row
10066  const currency=acc.currency||'USD';
10067  const bal=parseFloat(acc.balance||0);
10068  const dd=parseFloat(acc.drawdown||0);
10069  const eq=parseFloat(acc.equity||bal);
10070  const stats=[
10071    {label:'BALANCE',value:`${currency} ${bal.toLocaleString(undefined,{maximumFractionDigits:0})}`,color:'#e2eaf2'},
10072    {label:'EQUITY',value:`${currency} ${eq.toLocaleString(undefined,{maximumFractionDigits:0})}`,color:'#e2eaf2'},
10073    {label:'MAX DD',value:`${dd.toFixed(2)}%`,color:'#f85149'},
10074  ];
10075  const sw=1040/3;
10076  stats.forEach((s,i)=>{
10077    const x=20+sw*i;
10078    ctx.fillStyle='rgba(255,255,255,.04)';
10079    ctx.fillRect(x+1,470,sw-2,130);
10080    ctx.fillStyle='rgba(255,255,255,.3)';
10081    ctx.font='14px monospace';
10082    ctx.textAlign='center';
10083    ctx.fillText(s.label,x+sw/2,510);
10084    ctx.fillStyle=s.color;
10085    ctx.font='bold 30px monospace';
10086    ctx.fillText(s.value,x+sw/2,560);
10087  });
10088  ctx.textAlign='left';
10089
10090  // Sparkline equity curve
10091  if(gainData.length>1){
10092    const vals=gainData.map(p=>Array.isArray(p)?parseFloat(p[1]):parseFloat(p.value||0));
10093    const minV=Math.min(...vals), maxV=Math.max(...vals);
10094    const range=maxV-minV||1;
10095    const spX=48, spY=660, spW=984, spH=180;
10096    ctx.fillStyle='rgba(0,200,83,.06)';
10097    ctx.fillRect(spX,spY,spW,spH+30);
10098    ctx.beginPath();
10099    vals.forEach((v,i)=>{
10100      const px=spX+i/(vals.length-1)*spW;
10101      const py=spY+spH-(v-minV)/range*spH;
10102      i===0?ctx.moveTo(px,py):ctx.lineTo(px,py);
10103    });
10104    ctx.strokeStyle=gain>=0?'#00c853':'#f85149';
10105    ctx.lineWidth=3;
10106    ctx.stroke();
10107    ctx.fillStyle='rgba(255,255,255,.25)';
10108    ctx.font='13px monospace';
10109    ctx.fillText('90-DAY EQUITY CURVE',spX+8,spY+spH+22);
10110  }
10111
10112  // Date stamp
10113  const today=new Date().toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
10114  ctx.fillStyle='rgba(255,255,255,.2)';
10115  ctx.font='14px monospace';
10116  ctx.textAlign='right';
10117  ctx.fillText(today,1032,980);
10118  ctx.fillStyle='rgba(0,200,83,.6)';
10119  ctx.fillText('factozcentral.com',1032,1005);
10120  ctx.textAlign='left';
10121
10122  // Preview in modal + offer download + post
10123  const dataUrl=canvas.toDataURL('image/png');
10124  const html=`<div style="text-align:center">
10125    <img src="${dataUrl}" style="max-width:100%;border-radius:8px;margin-bottom:16px">
10126    <div style="display:flex;gap:10px;justify-content:center">
10127      <a href="${dataUrl}" download="trading-card.png" class="btn btn-ghost" style="font-size:.8rem">⬇ Download PNG</a>
10128    </div>
10129  </div>`;
10130  _mfxModal(`${acc.name} — Performance Card`, html);
10131}
10132
10133async function mfxPostCard(){
10134  const dataUrl=window._mfxCardDataUrl;
10135  if(!dataUrl){toast('Generate card first','err');return;}
10136  // Convert data URL to blob and pre-load post modal
10137  const res=await fetch(dataUrl);
10138  const blob=await res.blob();
10139  const file=new File([blob],'trading-card.png',{type:'image/png'});
10140  document.getElementById('mfx-modal')?.remove();
10141  // Pre-fill the post modal if it exists, otherwise offer download
10142  const postInput=document.getElementById('post-image');
10143  const captionInput=document.getElementById('post-caption');
10144  if(postInput&&captionInput){
10145    const dt=new DataTransfer();dt.items.add(file);postInput.files=dt.files;
10146    const gain=parseFloat(window._mfxLastAcc?.gain||0);
10147    const sign=gain>=0?'+':'';
10148    captionInput.value=`${sign}${gain.toFixed(2)}% — sharing my trading performance. Track yours at myfxbook.com #forex #trading #myfxbook`;
10149    go('post');
10150    toast('Card loaded in Post tab — add platforms and publish','ok');
10151  }else{
10152    const a=document.createElement('a');a.href=dataUrl;a.download='trading-card.png';a.click();
10153    toast('Card downloaded','ok');
10154  }
10155}
10156
10157async function mfxRoiChart(acc){
10158  if(typeof acc==='string') acc=JSON.parse(acc);
10159
10160  // Show loading modal immediately
10161  _mfxModal(`${acc.name} — ROI Chart`, `<div style="text-align:center;padding:40px;color:var(--muted)">Loading equity data...</div>`);
10162
10163  // Fetch 90-day gain data
10164  let gainData=[];
10165  try{
10166    const r=await fetch(`/api/myfxbook/accounts/${acc.id}/gain`,{headers:{'Authorization':'Bearer '+TOKEN}});
10167    if(r.ok){const d=await r.json();gainData=d.gain||[];}
10168  }catch(e){}
10169
10170  if(!gainData.length){
10171    _mfxModal(`${acc.name} — ROI Chart`,`<div style="text-align:center;padding:32px;color:var(--muted)">No equity data available for this account.</div>`);
10172    return;
10173  }
10174
10175  // Parse data points: MyFXBook returns [[date, value], ...] or [{date, value}, ...]
10176  const points=gainData.map(p=>({
10177    date: Array.isArray(p)?p[0]:p.date||p[0],
10178    val:  Array.isArray(p)?parseFloat(p[1]):parseFloat(p.value||p[1]||0)
10179  })).filter(p=>!isNaN(p.val));
10180
10181  if(points.length<2){
10182    _mfxModal(`${acc.name} — ROI Chart`,`<div style="text-align:center;padding:32px;color:var(--muted)">Not enough data points to draw chart.</div>`);
10183    return;
10184  }
10185
10186  // Draw on canvas 1200×675 (16:9 — great for social sharing)
10187  const canvas=$('mfx-card-canvas');
10188  canvas.width=1200; canvas.height=675;
10189  const ctx=canvas.getContext('2d');
10190  const W=1200, H=675;
10191  const PAD={top:80,right:60,bottom:90,left:90};
10192  const cW=W-PAD.left-PAD.right, cH=H-PAD.top-PAD.bottom;
10193
10194  // Background
10195  ctx.fillStyle='#080808'; ctx.fillRect(0,0,W,H);
10196
10197  // Header bar
10198  ctx.fillStyle='rgba(0,200,83,.07)'; ctx.fillRect(0,0,W,70);
10199  ctx.fillStyle='#00c853'; ctx.font='bold 22px monospace';
10200  ctx.fillText('FACTOZ',24,44);
10201  const gain=parseFloat(acc.gain||0);
10202  const gainStr=(gain>=0?'+':'')+gain.toFixed(2)+'%';
10203  ctx.fillStyle=gain>=0?'#00c853':'#f85149';
10204  ctx.font='bold 22px monospace';
10205  ctx.textAlign='right';
10206  ctx.fillText(gainStr,W-24,44);
10207  ctx.fillStyle='rgba(255,255,255,.5)';
10208  ctx.font='13px monospace';
10209  ctx.textAlign='center';
10210  ctx.fillText((acc.name||'Account').toUpperCase()+' — 90-DAY ROI CURVE',W/2,44);
10211  ctx.textAlign='left';
10212
10213  // Compute Y range with padding
10214  const vals=points.map(p=>p.val);
10215  const minV=Math.min(...vals), maxV=Math.max(...vals);
10216  const vRange=(maxV-minV)||1;
10217  const vPad=vRange*0.12;
10218  const yMin=minV-vPad, yMax=maxV+vPad;
10219
10220  const toX=i=>PAD.left+i/(points.length-1)*cW;
10221  const toY=v=>PAD.top+cH-(v-yMin)/(yMax-yMin)*cH;
10222
10223  // Horizontal grid lines
10224  const gridCount=5;
10225  for(let i=0;i<=gridCount;i++){
10226    const v=yMin+i/gridCount*(yMax-yMin);
10227    const y=toY(v);
10228    ctx.strokeStyle='rgba(255,255,255,.07)';
10229    ctx.lineWidth=1;
10230    ctx.beginPath(); ctx.moveTo(PAD.left,y); ctx.lineTo(PAD.left+cW,y); ctx.stroke();
10231    ctx.fillStyle='rgba(255,255,255,.35)';
10232    ctx.font='11px monospace';
10233    ctx.textAlign='right';
10234    ctx.fillText((v>=0?'+':'')+v.toFixed(1)+'%',PAD.left-8,y+4);
10235  }
10236  ctx.textAlign='left';
10237
10238  // X axis date labels (show ~6 evenly spaced)
10239  const labelCount=6;
10240  for(let i=0;i<=labelCount;i++){
10241    const idx=Math.round(i/labelCount*(points.length-1));
10242    const x=toX(idx);
10243    const label=points[idx].date?(points[idx].date+'').substring(0,10):'';
10244    ctx.fillStyle='rgba(255,255,255,.3)';
10245    ctx.font='10px monospace';
10246    ctx.textAlign='center';
10247    ctx.fillText(label,x,PAD.top+cH+28);
10248    ctx.strokeStyle='rgba(255,255,255,.05)';
10249    ctx.lineWidth=1;
10250    ctx.beginPath(); ctx.moveTo(x,PAD.top); ctx.lineTo(x,PAD.top+cH); ctx.stroke();
10251  }
10252  ctx.textAlign='left';
10253
10254  // Zero line (if 0 is in range)
10255  if(yMin<0&&yMax>0){
10256    const y0=toY(0);
10257    ctx.strokeStyle='rgba(255,255,255,.2)';
10258    ctx.setLineDash([4,6]); ctx.lineWidth=1;
10259    ctx.beginPath(); ctx.moveTo(PAD.left,y0); ctx.lineTo(PAD.left+cW,y0); ctx.stroke();
10260    ctx.setLineDash([]);
10261  }
10262
10263  // Fill area under curve
10264  ctx.beginPath();
10265  ctx.moveTo(toX(0),toY(points[0].val));
10266  points.forEach((p,i)=>ctx.lineTo(toX(i),toY(p.val)));
10267  ctx.lineTo(toX(points.length-1),PAD.top+cH);
10268  ctx.lineTo(PAD.left,PAD.top+cH);
10269  ctx.closePath();
10270  const grad=ctx.createLinearGradient(0,PAD.top,0,PAD.top+cH);
10271  grad.addColorStop(0,gain>=0?'rgba(0,200,83,.25)':'rgba(248,81,73,.25)');
10272  grad.addColorStop(1,'rgba(0,0,0,0)');
10273  ctx.fillStyle=grad; ctx.fill();
10274
10275  // Main curve line
10276  ctx.beginPath();
10277  points.forEach((p,i)=>{
10278    i===0?ctx.moveTo(toX(i),toY(p.val)):ctx.lineTo(toX(i),toY(p.val));
10279  });
10280  ctx.strokeStyle=gain>=0?'#00c853':'#f85149';
10281  ctx.lineWidth=2.5; ctx.stroke();
10282
10283  // Start/end dots
10284  ctx.fillStyle=gain>=0?'#00c853':'#f85149';
10285  [[0,points[0].val],[points.length-1,points[points.length-1].val]].forEach(([i,v])=>{
10286    ctx.beginPath(); ctx.arc(toX(i),toY(v),5,0,Math.PI*2); ctx.fill();
10287  });
10288
10289  // Footer
10290  const today=new Date().toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
10291  ctx.fillStyle='rgba(255,255,255,.18)'; ctx.font='12px monospace';
10292  ctx.textAlign='right';
10293  ctx.fillText('via myfxbook.com · '+today,W-24,H-18);
10294  ctx.fillStyle='rgba(0,200,83,.5)';
10295  ctx.fillText('factozcentral.com',W-24,H-36);
10296  ctx.textAlign='left';
10297
10298  // Show in modal
10299  const dataUrl=canvas.toDataURL('image/png');
10300  const html=`<div style="text-align:center">
10301    <img src="${dataUrl}" style="max-width:100%;border-radius:8px;margin-bottom:16px">
10302    <div style="display:flex;gap:10px;justify-content:center">
10303      <a href="${dataUrl}" download="${(acc.name||'roi-chart').replace(/\s+/g,'-').toLowerCase()}.png" class="btn btn-ghost" style="font-size:.8rem">⬇ Download PNG</a>
10304    </div>
10305  </div>`;
10306  _mfxModal(`${acc.name} — ROI Chart`, html);
10307}
10308
10309// ── END MYFXBOOK ───────────────────────────────────────────────────────────
10310
10311// status bar
10312async function pmLoadStatus(){
10313  try{
10314    const r=await api('GET','/api/polymarket/status');
10315    if(!r.ok)return;
10316    const d=await r.json();
10317    const dot=$('pm-monitor-dot');
10318    const lbl=$('pm-alerts-today-label');
10319    if(dot) dot.innerHTML=`<span style="color:${d.running?'#00c853':'#888'}">${d.running?'&#9679; LIVE':'&#9675; offline'}</span>`;
10320    if(lbl) lbl.textContent=`Alerts today: ${d.alerts_today||0}`;
10321  }catch(e){}
10322}
10323
10324// full refresh (called by Refresh button + go('polymarket'))
10325async function pmRefreshAll(){
10326  pmLoadStatus();
10327  if(_pmCurrentTab==='markets')  loadPmMarkets();
10328  if(_pmCurrentTab==='volume')   loadPmVolumeChart();
10329  if(_pmCurrentTab==='alerts'){ loadPmAlerts(); loadPmWatches(); }
10330}
10331
10332// ── Markets tab ──────────────────────────────────────────────
10333async function loadPmMarkets(){
10334  const list=$('pm-markets-list');
10335  if(!list)return;
10336  list.innerHTML='<p style="font-size:.8rem;color:var(--text2)">Loading markets…</p>';
10337  try{
10338    const r=await api('GET','/api/polymarket/markets?limit=50');
10339    const d=await r.json();
10340    if(!r.ok){list.innerHTML=`<p style="font-size:.8rem;color:var(--red)">${d.detail||'Failed'}</p>`;return;}
10341    _pmMarkets=d;
10342    // populate book select
10343    const sel=$('pm-book-select');
10344    if(sel){
10345      const tids=d.filter(m=>m.clobTokenIds);
10346      sel.innerHTML='<option value="">— select a market —</option>'+
10347        tids.map(m=>{
10348          let tids2=m.clobTokenIds;
10349          if(typeof tids2==='string'){try{tids2=JSON.parse(tids2);}catch(e){tids2=[];}}
10350          const tid=Array.isArray(tids2)?tids2[0]:'';
10351          return `<option value="${tid}">${m.question.substring(0,70)}</option>`;
10352        }).join('');
10353    }
10354    if(!d.length){list.innerHTML='<p style="font-size:.8rem;color:var(--text2)">No active markets found.</p>';return;}
10355    const watchIds=new Set(_pmWatches.map(w=>w.market_id));
10356    const fmt=v=>v>=1e6?'$'+(v/1e6).toFixed(1)+'M':v>=1e3?'$'+(v/1e3).toFixed(0)+'K':'$'+v.toFixed(0);
10357    list.innerHTML=d.map(m=>{
10358      const slug=m.slug||'';
10359      const url=`https://polymarket.com/market/${slug}`;
10360      const v24=parseFloat(m.volume24hr||m.volume||0);
10361      const liq=parseFloat(m.liquidity||0);
10362      let yesPrice=null;
10363      if(m.outcomePrices){try{const op=JSON.parse(m.outcomePrices);yesPrice=parseFloat(op[0]);}catch(e){}}
10364      const cid=m.conditionId||m.condition_id||m.id;
10365      const watching=watchIds.has(cid)||watchIds.has(m.id);
10366      const yesColor=yesPrice!=null?(yesPrice>.5?'#00c853':'#f23645'):'#888';
10367      const yesPct=yesPrice!=null?(yesPrice*100).toFixed(1)+'%':'—';
10368      // probability bar width
10369      const barW=yesPrice!=null?Math.round(yesPrice*100):0;
10370      return `
10371      <div style="background:var(--bg2);border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin-bottom:10px;transition:border-color .15s" onmouseenter="this.style.borderColor='rgba(0,200,83,.35)'" onmouseleave="this.style.borderColor='var(--border)'">
10372        <div style="display:flex;align-items:flex-start;gap:12px;margin-bottom:10px">
10373          <!-- probability pill -->
10374          <div style="flex-shrink:0;display:flex;flex-direction:column;align-items:center;gap:3px;min-width:46px">
10375            <div style="font-size:1rem;font-weight:800;font-family:var(--mono);color:${yesColor};line-height:1">${yesPct}</div>
10376            <div style="font-size:.58rem;color:var(--muted);letter-spacing:1px">YES</div>
10377            <div style="width:100%;height:3px;background:var(--bg3);border-radius:2px;margin-top:2px">
10378              <div style="width:${barW}%;height:100%;background:${yesColor};border-radius:2px;transition:width .3s"></div>
10379            </div>
10380          </div>
10381          <!-- question -->
10382          <a href="${url}" target="_blank" style="flex:1;font-size:.83rem;font-weight:600;color:#e2eaf2;text-decoration:none;line-height:1.4">${m.question}</a>
10383          <!-- watch button -->
10384          <button class="btn btn-ghost" style="flex-shrink:0;font-size:.68rem;padding:4px 12px;${watching?'color:var(--teal);border-color:var(--teal);background:rgba(0,200,83,.07)':''}"
10385            data-mid="${cid||m.id}" onclick="pmToggleWatch(this)">
10386            ${watching?'✓ Watching':'Watch'}
10387          </button>
10388        </div>
10389        <!-- stats row -->
10390        <div style="display:flex;gap:18px;font-size:.71rem;font-family:var(--mono);color:var(--text2);padding-top:8px;border-top:1px solid var(--border20)">
10391          <span>24h Vol <b style="color:#c4d0e0">${fmt(v24)}</b></span>
10392          <span>Liq <b style="color:#c4d0e0">${fmt(liq)}</b></span>
10393          <span style="margin-left:auto"><a href="${url}" target="_blank" style="color:var(--teal);text-decoration:none">Trade →</a></span>
10394        </div>
10395      </div>`;
10396    }).join('');
10397  }catch(e){if(list)list.innerHTML=`<p style="font-size:.8rem;color:var(--red)">Error: ${e.message}</p>`;}
10398}
10399
10400// ── Volume chart tab ─────────────────────────────────────────
10401async function loadPmVolumeChart(){
10402  const canvas=$('pm-vol-chart');
10403  const empty=$('pm-vol-empty');
10404  if(!canvas)return;
10405  try{
10406    const r=await api('GET','/api/polymarket/markets?limit=10');
10407    const d=await r.json();
10408    if(!r.ok||!d.length){if(empty)empty.style.display='';canvas.style.display='none';return;}
10409    const labels=d.map(m=>m.question.substring(0,30)+'…');
10410    const values=d.map(m=>parseFloat(m.volume24hr||m.volume||0));
10411    if(empty)empty.style.display='none';
10412    canvas.style.display='';
10413    if(_pmVolChart){_pmVolChart.destroy();_pmVolChart=null;}
10414    _pmVolChart=new Chart(canvas,{
10415      type:'bar',
10416      data:{
10417        labels,
10418        datasets:[{
10419          label:'24h Volume ($)',
10420          data:values,
10421          backgroundColor:'rgba(0,200,83,0.25)',
10422          borderColor:'rgba(0,200,83,0.8)',
10423          borderWidth:1,
10424          borderRadius:4,
10425        }]
10426      },
10427      options:{
10428        indexAxis:'y',
10429        responsive:true,
10430        plugins:{legend:{display:false},tooltip:{callbacks:{label:ctx=>'$'+(ctx.raw||0).toLocaleString()}}},
10431        scales:{
10432          x:{ticks:{color:'#8898aa',font:{size:10},callback:v=>v>=1e6?'$'+(v/1e6).toFixed(1)+'M':v>=1e3?'$'+(v/1e3).toFixed(0)+'K':'$'+v},grid:{color:'rgba(255,255,255,.04)'}},
10433          y:{ticks:{color:'#c4d0e0',font:{size:10}},grid:{display:false}}
10434        }
10435      }
10436    });
10437  }catch(e){console.error('pmVolChart',e);}
10438}
10439
10440// ── Order Book tab ───────────────────────────────────────────
10441async function loadPmBook(){
10442  const sel=$('pm-book-select');
10443  const res=$('pm-book-result');
10444  if(!sel||!res)return;
10445  const tid=sel.value;
10446  if(!tid){res.innerHTML='<p style="font-size:.8rem;color:var(--text2)">Select a market above.</p>';return;}
10447  res.innerHTML='<p style="font-size:.8rem;color:var(--text2)">Fetching order book…</p>';
10448  try{
10449    const r=await api('GET',`/api/polymarket/book?token=${encodeURIComponent(tid)}`);
10450    const d=await r.json();
10451    if(!r.ok){res.innerHTML=`<p style="font-size:.8rem;color:var(--red)">${d.detail||'Failed'}</p>`;return;}
10452    const bids=d.bids||[];
10453    const asks=d.asks||[];
10454    const top=10;
10455    const bestBid=bids.length?parseFloat(bids[0].price):null;
10456    const bestAsk=asks.length?parseFloat(asks[0].price):null;
10457    const mid=bestBid&&bestAsk?(bestBid+bestAsk)/2:null;
10458    const spreadPct=mid&&bestBid&&bestAsk?((bestAsk-bestBid)/mid*100).toFixed(2):null;
10459    const bidDepth=bids.slice(0,5).reduce((s,l)=>s+parseFloat(l.size||0),0);
10460    const askDepth=asks.slice(0,5).reduce((s,l)=>s+parseFloat(l.size||0),0);
10461    const imbalance=askDepth>0?(bidDepth/askDepth).toFixed(2):null;
10462    res.innerHTML=`
10463      <div style="display:flex;gap:16px;flex-wrap:wrap;font-size:.75rem;font-family:var(--mono);margin-bottom:14px;color:var(--text2)">
10464        ${bestBid!=null?`<span>Best Bid <b style="color:#00c853">${(bestBid*100).toFixed(1)}¢</b>`:''}
10465        ${bestAsk!=null?`<span>Best Ask <b style="color:#f23645">${(bestAsk*100).toFixed(1)}¢</b>`:''}
10466        ${spreadPct?`<span>Spread <b style="color:#ffc107">${spreadPct}%</b>`:''}
10467        ${imbalance?`<span>Bid/Ask Depth <b style="color:#e2eaf2">${imbalance}×</b>`:''}
10468      </div>
10469      <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
10470        <div>
10471          <div style="font-size:.6rem;color:var(--muted);letter-spacing:2px;font-family:var(--mono);margin-bottom:6px">BIDS (YES buyers)</div>
10472          <table style="width:100%;border-collapse:collapse;font-size:.74rem">
10473            <tr style="color:var(--muted)"><th style="padding:3px 6px;text-align:right">Price</th><th style="padding:3px 6px;text-align:right">Size</th></tr>
10474            ${bids.slice(0,top).map(l=>`<tr>
10475              <td style="padding:3px 6px;text-align:right;color:#00c853;font-family:var(--mono)">${(parseFloat(l.price)*100).toFixed(1)}¢</td>
10476              <td style="padding:3px 6px;text-align:right;color:var(--text2);font-family:var(--mono)">${parseFloat(l.size).toFixed(0)}</td>
10477            </tr>`).join('')}
10478          </table>
10479        </div>
10480        <div>
10481          <div style="font-size:.6rem;color:var(--muted);letter-spacing:2px;font-family:var(--mono);margin-bottom:6px">ASKS (YES sellers)</div>
10482          <table style="width:100%;border-collapse:collapse;font-size:.74rem">
10483            <tr style="color:var(--muted)"><th style="padding:3px 6px;text-align:right">Price</th><th style="padding:3px 6px;text-align:right">Size</th></tr>
10484            ${asks.slice(0,top).map(l=>`<tr>
10485              <td style="padding:3px 6px;text-align:right;color:#f23645;font-family:var(--mono)">${(parseFloat(l.price)*100).toFixed(1)}¢</td>
10486              <td style="padding:3px 6px;text-align:right;color:var(--text2);font-family:var(--mono)">${parseFloat(l.size).toFixed(0)}</td>
10487            </tr>`).join('')}
10488          </table>
10489        </div>
10490      </div>`;
10491  }catch(e){if(res)res.innerHTML=`<p style="font-size:.8rem;color:var(--red)">Error: ${e.message}</p>`;}
10492}
10493
10494// ── Alerts tab ───────────────────────────────────────────────
10495async function loadPmAlerts(){
10496  const list=$('pm-alerts-list');
10497  if(!list)return;
10498  try{
10499    const r=await api('GET','/api/polymarket/alerts?limit=50');
10500    const d=await r.json();
10501    if(!r.ok){list.innerHTML=`<p style="font-size:.8rem;color:var(--red)">${d.detail||'Failed'}</p>`;return;}
10502    if(!d.length){list.innerHTML='<p style="font-size:.8rem;color:var(--text2)">No alerts fired yet. The monitor checks OI, volume, and order books every 60 s.</p>';return;}
10503    list.innerHTML=d.map(a=>_pmAlertCard(a)).join('');
10504  }catch(e){if(list)list.innerHTML=`<p style="font-size:.8rem;color:var(--red)">Error: ${e.message}</p>`;}
10505}
10506
10507function _pmAlertCard(a){
10508  const typeColor={oi_spike:'#a78bfa',volume_surge:'#00c853',book_imbalance:'#fbbf24',spread_spike:'#f97316'};
10509  const typeLabel={oi_spike:'OI SPIKE',volume_surge:'VOL SURGE',book_imbalance:'BOOK IMBALANCE',spread_spike:'SPREAD SPIKE'};
10510  const tc=typeColor[a.alert_type]||'#888';
10511  const tl=typeLabel[a.alert_type]||a.alert_type||'ALERT';
10512  let detail='';
10513  if(a.oi_change_pct!=null) detail+=`<span>OI Δ <b style="color:#e2eaf2">+${a.oi_change_pct.toFixed(1)}%</b></span>`;
10514  if(a.live_volume!=null)   detail+=`<span>Vol <b style="color:#e2eaf2">$${(a.live_volume||0).toLocaleString(undefined,{maximumFractionDigits:0})}</b></span>`;
10515  if(a.spike_ratio!=null)   detail+=`<span><b style="color:#e2eaf2">${a.spike_ratio.toFixed(1)}×</b> baseline</span>`;
10516  if(a.book_imbalance!=null)detail+=`<span>Imbalance <b style="color:#e2eaf2">${a.book_imbalance.toFixed(1)}×</b></span>`;
10517  if(a.spread_pct!=null)    detail+=`<span>Spread <b style="color:#e2eaf2">${a.spread_pct.toFixed(1)}%</b></span>`;
10518  if(a.flow_direction&&a.flow_direction!=='BOTH') detail+=`<span>Flow <b style="color:${a.flow_direction==='YES'?'#00c853':'#f23645'}">${a.flow_direction}</b></span>`;
10519  return `
10520  <div style="background:var(--bg2);border:1px solid var(--border);border-left:3px solid ${tc};border-radius:8px;padding:11px 14px;margin-bottom:8px">
10521    <div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:5px">
10522      <span style="font-size:.64rem;font-weight:700;padding:2px 7px;border-radius:4px;background:${tc}22;color:${tc};border:1px solid ${tc}44;white-space:nowrap;flex-shrink:0">${tl}</span>
10523      <div style="font-size:.81rem;font-weight:600;color:#e2eaf2;line-height:1.3;flex:1">${a.question}</div>
10524      <span style="font-size:.68rem;color:var(--muted);white-space:nowrap;flex-shrink:0">${new Date(a.created_at).toLocaleTimeString()}</span>
10525    </div>
10526    <div style="display:flex;gap:14px;font-size:.71rem;color:var(--text2);font-family:var(--mono);flex-wrap:wrap;margin-bottom:6px">${detail}</div>
10527    <a href="${a.market_url}" target="_blank" style="font-size:.7rem;color:var(--teal);text-decoration:none;font-family:var(--mono)">Trade on Polymarket →</a>
10528  </div>`;
10529}
10530
10531// ── Watches ──────────────────────────────────────────────────
10532async function loadPmWatches(){
10533  const list=$('pm-watches-list');
10534  if(!list)return;
10535  try{
10536    const r=await api('GET','/api/polymarket/watches');
10537    const d=await r.json();
10538    _pmWatches=r.ok?d:[];
10539    if(!r.ok||!d.length){list.innerHTML='<p style="font-size:.8rem;color:var(--text2)">No watched markets yet. Go to Markets tab and click Watch.</p>';return;}
10540    list.innerHTML=d.map(w=>`
10541      <div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border20)">
10542        <a href="https://polymarket.com/market/${w.market_id}" target="_blank" style="flex:1;font-size:.8rem;color:var(--text);text-decoration:none">${w.question}</a>
10543        <button class="btn btn-ghost" style="font-size:.68rem;padding:3px 9px;color:var(--red);border-color:rgba(255,68,68,.3)" onclick="pmRemoveWatch('${w.id}',this)">Remove</button>
10544      </div>`).join('');
10545  }catch(e){if(list)list.innerHTML=`<p style="font-size:.8rem;color:var(--red)">Error</p>`;}
10546}
10547
10548async function pmToggleWatch(btn){
10549  const marketId=btn.dataset.mid;
10550  if(!marketId)return;
10551  try{
10552    const existing=_pmWatches.find(w=>w.market_id===marketId);
10553    if(existing){
10554      await pmRemoveWatch(existing.id,null);
10555      btn.textContent='Watch';btn.style.color='';btn.style.borderColor='';
10556    }else{
10557      const m=_pmMarkets.find(m=>(m.conditionId||m.condition_id||m.id)===marketId);
10558      const question=m?m.question:'';
10559      let tids=m?m.clobTokenIds:null;
10560      if(typeof tids==='string'){try{tids=JSON.parse(tids);}catch(e){tids=null;}}
10561      const token_id=Array.isArray(tids)?tids[0]:null;
10562      const eid=m?(m.events&&m.events[0]?m.events[0].id:null):null;
10563      const r=await api('POST','/api/polymarket/watch',{market_id:marketId,question,condition_id:marketId,token_id,event_id:eid});
10564      if(r.ok){
10565        btn.textContent='✓ Watching';btn.style.color='var(--teal)';btn.style.borderColor='var(--teal)';
10566        await loadPmWatches();
10567        toast('Market added to watch list','ok');
10568      }else{const d=await r.json();toast(d.detail||'Failed to add watch','err');}
10569    }
10570  }catch(e){toast('Error: '+e.message,'err');}
10571}
10572
10573async function pmRemoveWatch(watchId,btn){
10574  try{
10575    const r=await api('DELETE',`/api/polymarket/watch/${watchId}`);
10576    if(r.ok){
10577      _pmWatches=_pmWatches.filter(w=>w.id!==watchId);
10578      loadPmWatches();
10579      if(btn)btn.closest('div[style*="padding:8px"]')?.remove();
10580    }
10581  }catch(e){}
10582}
10583
10584// ── SSE stream ───────────────────────────────────────────────
10585function pmStartSSE(){
10586  if(_pmSSE){_pmSSE.close();_pmSSE=null;}
10587  try{
10588    _pmSSE=new EventSource('/api/polymarket/stream?token='+encodeURIComponent(TOKEN));
10589    _pmSSE.onmessage=e=>{
10590      try{
10591        const a=JSON.parse(e.data);
10592        if(a.type==='connected')return;
10593        // live strip
10594        const strip=$('pm-live-strip');
10595        const txt=$('pm-live-text');
10596        if(strip&&txt){
10597          const typeLabel={oi_spike:'OI Spike',volume_surge:'Vol Surge',book_imbalance:'Book Imbalance',spread_spike:'Spread Spike'};
10598          txt.textContent=`[${typeLabel[a.alert_type]||'Alert'}] ${a.question.substring(0,60)}… — ${new Date(a.created_at).toLocaleTimeString()}`;
10599          strip.style.display='';
10600        }
10601        // if on alerts tab, prepend card
10602        if(_pmCurrentTab==='alerts'){
10603          const list=$('pm-alerts-list');
10604          if(list&&list.querySelector('p'))list.innerHTML='';
10605          if(list) list.insertAdjacentHTML('afterbegin',_pmAlertCard(a));
10606        }
10607        // update today count
10608        pmLoadStatus();
10609      }catch(err){}
10610    };
10611    _pmSSE.onerror=async()=>{
10612      if(_pmSSE){_pmSSE.close();_pmSSE=null;}
10613      // Verify token is still valid before reconnecting — stops the 403 retry loop
10614      try{
10615        const r=await fetch('/api/auth/me',{headers:{'Authorization':'Bearer '+TOKEN}});
10616        if(!r.ok){logout();return;}
10617      }catch(e){}
10618      setTimeout(pmStartSSE,30000);
10619    };
10620  }catch(e){}
10621}
10622
10623function pmStopSSE(){
10624  if(_pmSSE){_pmSSE.close();_pmSSE=null;}
10625}
10626
10627// ── Admin config helpers (updated for v2 fields) ─────────────
10628async function loadPolymarketConfig(){
10629  try{
10630    const r=await api('GET','/api/admin/polymarket/config');
10631    if(!r.ok)return;
10632    const d=await r.json();
10633    const s=(id,v)=>{const el=$(id);if(el)el.value=v??'';};
10634    const sc=(id,v)=>{const el=$(id);if(el)el.checked=!!v;};
10635    s('pm-oi-chg',    d.min_oi_change_pct);
10636    s('pm-vol-mult',  d.volume_multiplier);
10637    s('pm-book-imb',  d.min_book_imbalance);
10638    s('pm-spread-pct',d.max_spread_pct);
10639    s('pm-min-vol24', d.min_volume_24h);
10640    s('pm-max-markets',d.max_markets);
10641    s('pm-cooldown',  d.alert_cooldown_sec);
10642    s('pm-poll-interval',d.polling_interval_sec);
10643    s('pm-include-kw',d.include_keywords||'');
10644    s('pm-exclude-kw',d.exclude_keywords||'');
10645    sc('pm-enabled',  d.enabled);
10646  }catch(e){console.error('loadPolymarketConfig',e);}
10647}
10648
10649async function savePolymarketConfig(){
10650  const g=(id)=>{const el=$(id);return el?el.value:null;};
10651  const gc=(id)=>{const el=$(id);return el?el.checked:false;};
10652  const payload={
10653    enabled:              gc('pm-enabled'),
10654    min_oi_change_pct:    parseFloat(g('pm-oi-chg'))    ||10.0,
10655    volume_multiplier:    parseFloat(g('pm-vol-mult'))   ||1.5,
10656    min_book_imbalance:   parseFloat(g('pm-book-imb'))   ||3.0,
10657    max_spread_pct:       parseFloat(g('pm-spread-pct')) ||5.0,
10658    min_volume_24h:       parseFloat(g('pm-min-vol24'))  ||10000,
10659    max_markets:          parseInt(g('pm-max-markets'))  ||50,
10660    alert_cooldown_sec:   parseInt(g('pm-cooldown'))     ||3600,
10661    polling_interval_sec: parseInt(g('pm-poll-interval'))||60,
10662    include_keywords:     g('pm-include-kw')||'',
10663    exclude_keywords:     g('pm-exclude-kw')||'',
10664  };
10665  const res=$('pm-config-result');
10666  try{
10667    const r=await api('PUT','/api/admin/polymarket/config',payload);
10668    const d=await r.json();
10669    if(r.ok){if(res)res.innerHTML='<span style="color:var(--green)">Saved — takes effect within 10 min</span>';toast('Config saved','ok');}
10670    else{if(res)res.innerHTML=`<span style="color:var(--red)">${d.detail||'Save failed'}</span>`;}
10671  }catch(e){if(res)res.innerHTML='<span style="color:var(--red)">Error saving config</span>';}
10672}
10673
10674async function loadDailyBriefConfig(){
10675  try{
10676    const r=await api('GET','/api/admin/daily-brief/config');
10677    if(!r.ok)return;
10678    const d=await r.json();
10679    const el=$('brief-auto-enabled');
10680    if(el)el.checked=!!d.enabled;
10681  }catch(e){console.error('loadDailyBriefConfig',e);}
10682}
10683
10684async function saveDailyBriefConfig(){
10685  const el=$('brief-auto-enabled');
10686  const enabled=el?el.checked:true;
10687  const res=$('brief-auto-result');
10688  try{
10689    const r=await api('PUT','/api/admin/daily-brief/config',{enabled});
10690    const d=await r.json();
10691    if(r.ok){if(res)res.textContent=enabled?'Saved — automatic generation on':'Saved — automatic generation off';
10691toast(enabled?'Daily brief automation on':'Daily brief automation off','ok');}
10692    else{if(res)res.textContent=d.detail||'Save failed';}
10693  }catch(e){if(res)res.textContent='Error saving';}
10694}
10695
10696/* ── NEWSROOM ──────────────────────────────────────────────────────────
10697   Review queue for world-sourced content. Nothing here posts on its own:
10698   approve stages a draft, publish sends it, and both are explicit clicks. */
10699function _nrEsc(s){return (s==null?'':String(s)).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
10700function _nrSay(msg){const el=$('nr-status');if(el)el.textContent=msg||'';}
10701
10702async function newsroomDiscover(beat){
10703  _nrSay('Searching for stories… this takes 30-60s.');
10704  try{
10705    const r=await api('POST','/api/admin/newsroom/discover'+(beat?('?beat='+encodeURIComponent(beat)):''));
10706    if(!r.ok){_nrSay('Discovery failed to start.');return;}
10707    _nrSay('Discovery running in the background — hit Refresh in about a minute.');
10708  }catch(e){_nrSay('Discovery error: '+e.message);}
10709}
10710
10711let NR_SHOW_ALL=false;
10712function nrToggleAll(){NR_SHOW_ALL=!NR_SHOW_ALL;loadNewsroom();}
10713
10714async function loadNewsroom(){
10715  // "pending,approved,partial,failed" is the working queue; NR_SHOW_ALL
10716  // drops the filter so posted and rejected drafts are inspectable too —
10717  // without it there is no way to look at anything already published.
10718  const st = NR_SHOW_ALL ? 'all' : 'pending,approved,partial,failed';
10719  try{
10720    const [ri,rd]=await Promise.all([
10721      api('GET','/api/admin/newsroom/ideas?status=pending&limit=40'),
10722      api('GET','/api/admin/newsroom/drafts?status='+st+'&limit=30')
10723    ]);
10724    // A failed request must not render as "no drafts yet" — that hid a
10725    // server error behind an empty-state message and looked like data loss.
10726    if(!rd.ok){
10727      const err=await rd.text().catch(()=>'');
10728      _nrSay('Error cargando drafts ('+rd.status+'): '+err.slice(0,200));
10729      _nrRenderDrafts([]);
10730    }else{
10731      _nrRenderDrafts(await rd.json());
10732    }
10733    if(!ri.ok){
10734      const err=await ri.text().catch(()=>'');
10735      _nrSay('Error cargando ideas ('+ri.status+'): '+err.slice(0,200));
10736      _nrRenderIdeas([]);
10737    }else{
10738      _nrRenderIdeas(await ri.json());
10739    }
10740  }catch(e){_nrSay('Could not load newsroom: '+e.message);}
10741}
10742
10743function _nrRenderIdeas(ideas){
10744  const wrap=$('nr-ideas'), empty=$('nr-ideas-empty');
10745  if(!wrap)return;
10746  if(!ideas.length){wrap.innerHTML='';if(empty)empty.style.display='block';return;}
10747  if(empty)empty.style.display='none';
10748  wrap.innerHTML=ideas.map(i=>`
10749    <div style="border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:11px 13px;margin-bottom:8px">
10750      <div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start">
10751        <div style="flex:1">
10752          <div style="font-size:.8rem;color:var(--text);margin-bottom:3px">${_nrEsc(i.headline)}</div>
10753          <div style="font-size:.71rem;color:var(--muted)">${_nrEsc((i.summary||'').slice(0,190))}</div>
10754          <div style="font-size:.68rem;color:var(--muted);margin-top:5px;font-family:var(--mono)">
10755            ${_nrEsc(i.beat)} · score ${i.score} · ${_nrEsc(i.origin||'')}
10756            ${(i.sources&&i.sources.length)?` · <a href="${_nrEsc(i.sources[0])}" target="_blank" rel="noopener" style="color:var(--accent)">fuente</a>`:''}
10757          </div>
10758        </div>
10759        <div style="display:flex;flex-direction:column;gap:5px">
10760          <button class="btn btn-ghost" style="font-size:.68rem;padding:4px 9px" onclick="nrDraft('${i.id}','carousel',this)">Carousel</button>
10761          <button class="btn btn-ghost" style="font-size:.68rem;padding:4px 9px" onclick="nrDraft('${i.id}','reel',this)">Reel</button>
10762          <button class="btn btn-ghost" style="font-size:.68rem;padding:4px 9px" onclick="nrDraft('${i.id}','card',this)">Card (X)</button>
10763          <button class="btn btn-ghost" style="font-size:.68rem;padding:4px 9px" onclick="nrDismiss('${i.id}',this)">Descartar</button>
10764        </div>
10765      </div>
10766    </div>`).join('');
10767}
10768
10769function _nrRenderDrafts(drafts){
10770  const wrap=$('nr-drafts'), empty=$('nr-drafts-empty');
10771  if(!wrap)return;
10772  if(!drafts.length){wrap.innerHTML='';if(empty)empty.style.display='block';return;}
10773  if(empty)empty.style.display='none';
10774  wrap.innerHTML=drafts.map(d=>{
10775    const slides=(d.slides||[]).map((s,n)=>`
10776      <div style="font-size:.71rem;color:var(--muted);padding:3px 0">
10777        <span style="font-family:var(--mono);color:var(--accent)">${n+1}</span>
10778        <strong style="color:var(--text)">${_nrEsc(s.headline)}</strong>
10779        ${s.body?(' — '+_nrEsc(s.body)):''}
10780        ${s.chart_spec?' <span style="color:var(--accent)">[chart]</span>':''}
10781      </div>`).join('');
10782    // asset_urls is {lang: [urls]} since X renders its own English images;
10783    // older drafts stored a bare list, so accept both.
10784    // Spanish only. Flattening every language showed a pre-English-removal
10785    // draft twice — 6 Spanish slides followed by 6 English ones.
10786    const au=d.asset_urls;
10787    const urlList=Array.isArray(au)?au:((au&&typeof au==='object')?(au.es||au.en||[]):[]);
10788    // A reel's asset is an mp4, so it needs a <video>, not an <img>.
10789    const imgs=urlList.map(u=>
10790      /\.mp4($|\?)/i.test(u)
10791        ? `<video src="${_nrEsc(u)}" controls playsinline style="width:190px;border-radius:6px;border:1px solid rgba(255,255,255,.1)"></video>`
10792        : `<img src="${_nrEsc(u)}" style="width:104px;border-radius:6px;border:1px solid rgba(255,255,255,.1)">`).join('');
10793    // Approved drafts stay in the list — they are precisely the ones
10794    // waiting to be published, so hiding them stranded them.
10795    const anim = _nrAnimatedUrl(d);
10796    const pex  = _nrPexelsUrl(d);
10797    const les  = _nrKeyUrl(d,'leccion');
10798    const isLesson = (d.slides||[]).some(x=>x&&x.lesson);
10799    NR_ANIM[d.id]=anim;
10800    NR_ANIM_PX[d.id]=pex;
10801    NR_PROMOTED[d.id]=_nrPromotedKey(d);
10802    const approved = d.status==='approved';
10803    const retryable = d.status==='partial' || d.status==='failed';
10804    // A posted draft used to fall through to "PENDIENTE DE REVISIÓN" with a
10805    // live Approve button, which then 409s as already_posted — the UI was
10806    // offering an action the server always refuses.
10807    const posted = d.status==='posted';
10808    const rejected = d.status==='rejected';
10809    const badge =
10810      d.status==='partial' ? '<span style="color:#ffb020;font-weight:600">⚠ PARCIAL — faltan canales</span>'
10811      : d.status==='failed' ? '<span style="color:#ff6b6b;font-weight:600">✕ FALLÓ AL PUBLICAR</span>'
10812      : posted ? '<span style="color:#8c94a8;font-weight:600">✓ YA PUBLICADO — no se vuelve a enviar</span>'
10813      : rejected ? '<span style="color:#8c94a8;font-weight:600">✕ RECHAZADO</span>'
10814      : approved ? '<span style="color:#00c864;font-weight:600">✓ APROBADO — listo para publicar</span>'
10815      : '<span style="color:var(--muted)">PENDIENTE DE REVISIÓN</span>';
10816    const edge = retryable ? 'rgba(255,176,32,.45)'
10817      : (posted||rejected) ? 'rgba(140,148,168,.28)'
10818      : (approved ? 'rgba(0,200,100,.4)' : 'rgba(0,188,212,.22)');
10819    return `
10820    <div style="border:1px solid ${edge};border-radius:8px;padding:13px;margin-bottom:12px">
10821      <div style="font-size:.68rem;color:var(--muted);font-family:var(--mono);margin-bottom:5px">
10822        ${_nrEsc(d.beat)} · ${_nrEsc(d.content_format)} · ${(d.slides||[]).length} slides · ${badge}
10823      </div>
10824      <div style="font-size:.86rem;color:var(--text);margin-bottom:2px">${_nrEsc(d.hook_es||'')}</div>
10825      <div style="font-size:.72rem;color:var(--muted);margin-bottom:8px">EN: ${_nrEsc(d.hook_en||'')}</div>
10826      ${d.review_note?`<div style="font-size:.72rem;color:#ffb020;background:rgba(255,176,32,.08);border:1px solid rgba(255,176,32,.3);border-radius:6px;padding:7px 9px;margin-bottom:8px;white-space:pre-wrap">${_nrEsc(d.review_note)}</div>`:''}
10827      <div style="margin:8px 0">${slides}</div>
10828      ${imgs?`<div style="display:flex;gap:6px;flex-wrap:wrap;margin:10px 0">${imgs}</div>`:''}
10829      ${[['animado',anim,'REEL ANIMADO','#ffb020'],['pexels',pex,'REEL CON METRAJE (PEXELS)','#00bcd4'],['leccion',les,'LECCIÓN NARRADA','#8b5cf6']]
10830         .filter(([,u])=>u).map(([k,u,label,col])=>`<div style="margin:10px 0">
10831        <div style="font-size:.66rem;font-family:var(--mono);color:${col};margin-bottom:4px">PRUEBA · ${label} (no se publica solo)</div>
10832        <video src="${_nrEsc(u)}" controls playsinline style="width:190px;border-radius:6px;border:1px solid ${col}77"></video>
10833        <div style="margin-top:6px">
10834          ${NR_PROMOTED[d.id]===k
10835            ? `<span style="font-size:.68rem;color:#00c864">✓ Este es el que se publica — IG y TikTok como reel.</span>`
10836            : `<button class="btn btn-ghost" style="font-size:.69rem;padding:4px 10px;border-color:${col}77" onclick="nrUseAnimated('${d.id}',this,'${k}')">⬆ Usar este para publicar (IG + TikTok como reel)</button>`}
10837        </div>
10838      </div>`).join('')}
10839      <details style="margin:8px 0">
10840        <summary style="font-size:.71rem;color:var(--muted);cursor:pointer">Captions</summary>
10841        <div style="font-size:.71rem;color:var(--muted);margin-top:6px;white-space:pre-wrap">${_nrEsc(d.caption_es||'')}</div>
10842        <div style="font-size:.71rem;color:var(--muted);margin-top:8px;white-space:pre-wrap">EN — ${_nrEsc(d.caption_en||'')}</div>
10843      </details>
10844      <div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:10px">
10845        <button class="btn btn-ghost" style="font-size:.69rem;padding:4px 10px" onclick="nrRender('${d.id}',this)">🖼 Preview</button>
10846        <button class="btn btn-ghost" style="font-size:.69rem;padding:4px 10px;border-color:rgba(255,176,32,.45)" onclick="nrRenderAnimated('${d.id}',this)">🎬 Reel animado (prueba)</button>
10847        <button class="btn btn-ghost" style="font-size:.69rem;padding:4px 10px;border-color:rgba(0,188,212,.5)" onclick="nrRenderPexels('${d.id}',this)" title="Texto sobre metraje real de Pexels. Gratis, pero tarda 3-5 min.">🎞 Reel con metraje (prueba)</button>
10848        ${isLesson?`<button class="btn btn-ghost" style="font-size:.69rem;padding:4px 10px;border-color:rgba(139,92,246,.6)" onclick="nrRenderLesson('${d.id}',this)" title="Dibuja la gráfica animada y la narra. Tarda varios minutos.">🎓 Renderizar lección</button>`:''}
10849        ${posted
10850          ? `<span style="font-size:.68rem;color:var(--muted);align-self:center">Ya se envió a Buffer — para publicar otra versión, parte de un draft nuevo.</span>`
10851          : retryable
10852          ? `<button class="btn" style="font-size:.69rem;padding:4px 10px" onclick="nrPublish('${d.id}',this)">🔁 Reintentar canales faltantes</button>`
10853          : approved
10854          ? `<button class="btn" style="font-size:.69rem;padding:4px 10px" onclick="nrPublish('${d.id}',this)">📤 Publicar a Buffer</button>`
10855          : rejected
10856          // A rejected draft can still be rendered and brought back —
10857          // approve only refuses drafts already posted. The label says so,
10858          // because "Aprobar" under a RECHAZADO badge reads like a dead
10859          // button and left Abraham unsure whether a render would even run.
10860          ? `<button class="btn btn-ghost" style="font-size:.69rem;padding:4px 10px" onclick="nrApprove('${d.id}',this)">
10860↩ Recuperar y aprobar</button>`
10861          : `<button class="btn btn-ghost" style="font-size:.69rem;padding:4px 10px" onclick="nrApprove('${d.id}',this)">✓ Aprobar</button>`}
10862        ${(posted||rejected)?'':`<button class="btn btn-ghost" style="font-size:.69rem;padding:4px 10px" onclick="nrReject('${d.id}',this)">✕ Rechazar</button>`}
10863      </div>
10864      <div id="nr-msg-${d.id}" style="font-size:.7rem;color:var(--muted);margin-top:6px"></div>
10865    </div>`;
10866  }).join('');
10867}
10868
10869function _nrBusy(btn,label){if(btn){btn.disabled=true;btn._old=btn.textContent;btn.textContent=label;}}
10870function _nrDone(btn){if(btn){btn.disabled=false;if(btn._old)btn.textContent=btn._old;}}
10871
10872async function nrDraft(ideaId,fmt,btn){
10873  _nrBusy(btn,'Drafting…');
10874  try{
10875    const r=await api('POST','/api/admin/newsroom/ideas/'+ideaId+'/draft',{content_format:fmt,deep_research:true});
10876    if(!r.ok){const e=await r.json().catch(()=>({}));_nrSay('Draft failed: '+(e.detail||r.status));}
10877    else{_nrSay('Draft created.');await loadNewsroom();}
10878  }catch(e){_nrSay('Draft error: '+e.message);}
10879  _nrDone(btn);
10880}
10881
10882async function nrDismiss(ideaId,btn){
10883  _nrBusy(btn,'…');
10884  try{await api('POST','/api/admin/newsroom/ideas/'+ideaId+'/dismiss');await loadNewsroom();}
10885  catch(e){_nrSay('Error: '+e.message);}
10886  _nrDone(btn);
10887}
10888
10889function _nrAssetCount(d){
10890  const au=d&&d.asset_urls;
10891  if(Array.isArray(au))return au.length;
10892  // The experimental renders ('animado', 'pexels'), the '*_prev' copies a
10893  // promotion leaves behind, and 'music' (a string, not a list) are all
10894  // excluded — otherwise a draft that already has them makes the normal
10895  // render poll think it finished instantly.
10896  if(au&&typeof au==='object')return Object.entries(au)
10897    .filter(([k,v])=>Array.isArray(v)&&k!=='animado'&&k!=='pexels'&&!/_prev$/.test(k))
10898    .reduce((n,[,v])=>n+v.length,0);
10899  return 0;
10900}
10901
10902function _nrKeyUrl(d,key){
10903  const au=d&&d.asset_urls;
10904  return (au&&!Array.isArray(au)&&typeof au==='object'&&(au[key]||[])[0])||'';
10905}
10906function _nrAnimatedUrl(d){ return _nrKeyUrl(d,'animado'); }
10907function _nrPexelsUrl(d){   return _nrKeyUrl(d,'pexels');  }
10908
10909/* Last known animated URL per draft, refreshed every time the list renders.
10910   The re-render poll needs to know what was there BEFORE it started. */
10911const NR_ANIM={};
10912
10913/* True when the animated video is the one currently set to publish. The
10914   plain Preview re-renders into that same slot, so running it would
10915   silently swap the animated cut back for the static one. */
10916function _nrPromotedKey(d){
10917  const au=d&&d.asset_urls;
10918  if(!au||Array.isArray(au)||typeof au!=='object')return '';
10919  const cur=(au.es||[])[0]||'';
10920  if(cur&&cur===_nrAnimatedUrl(d))return 'animado';
10921  if(cur&&cur===_nrPexelsUrl(d))return 'pexels';
10922  if(cur&&cur===_nrKeyUrl(d,'leccion'))return 'leccion';
10923  return '';
10924}
10925const NR_PROMOTED={};
10926const NR_ANIM_PX={};
10927
10928/* Same background-task pattern as the normal reel render, but watching the
10929   'animado' key so the two polls can't be confused for one another.
10930
10931   `prev` is the URL that was there when the render started, and the poll
10932   waits for a DIFFERENT one. Waiting for "any animado url" finished on the
10933   first tick whenever the draft had been rendered before — it reloaded
10934   instantly and showed the previous video as if it were the new one, which
10935   made a working re-render look like it had been ignored. Every upload gets
10936   its own timestamped Cloudinary URL, so a change is a reliable signal. */
10937async function _nrPollAnimated(id,tries,prev){
10938  tries=tries||0;
10939  const msg=()=>$('nr-msg-'+id);
10940  // 10 minutes. The animated render redraws text across the reveal window
10941  // of every beat, so a 6-slide draft is ~600 frames and Railway's CPU is
10942  // several times slower than a laptop — 4 minutes gave up on renders that
10943  // were still perfectly healthy.
10944  if(tries>60){const m=msg();if(m)m.textContent='El reel animado está tardando más de 10 min — revisa los logs de Railway.';return;}
10945  await new Promise(r=>setTimeout(r,10000));
10946  try{
10947    const r=await api('GET','/api/admin/newsroom/drafts?status=pending,approved,partial,failed&limit=30');
10948    if(r.ok){
10949      const d=(await r.json()).find(x=>x.id===id);
10950      if(d&&d.error){const m=msg();
10950if(m)m.textContent='Render falló: '+d.error;return;}
10951      const now=_nrAnimatedUrl(d);
10952      if(now&&now!==prev){await loadNewsroom();return;}
10953    }
10954  }catch(e){}
10955  const m=msg();
10956  if(m)m.textContent='Renderizando el reel animado… ('+((tries+1)*10)+'s)'
10957       +(prev?' — abajo sigue el anterior hasta que termine.':'');
10958  _nrPollAnimated(id,tries+1,prev);
10959}
10960
10961/* Builds a "Cómo gana dinero X" draft. Blank ticker = next company in the
10962   watchlist not used in the last 45 days. Costs no LLM credits, so it is
10963   safe to run repeatedly while tuning. */
10964async function nrMoneyFlow(btn){
10965  const raw=prompt('Ticker (vacío = siguiente de la lista de 88 empresas):','');
10966  if(raw===null)return;          // Cancel used to fall through and build one
10967  const t=raw.trim().toUpperCase();
10968  _nrSay('Armando el gráfico de flujo de dinero…');
10969  try{
10970    const q=t?('?ticker='+encodeURIComponent(t)):'';
10971    const r=await api('POST','/api/admin/newsroom/money-flow'+q);
10972    const d=await r.json().catch(()=>({}));
10973    if(!r.ok){_nrSay('No se pudo: '+(d.detail||r.status));return;}
10974    _nrSay('Listo — '+d.company+' ('+d.ticker+', '+d.period+'). Está en la cola, sin publicar.');
10975    await loadNewsroom();
10976  }catch(e){_nrSay('Error: '+e.message);}
10977}
10978
10979/* "¿Cuánto tendrías hoy?" — reel por defecto. Sin costo de LLM, así que
10980   se puede repetir mientras se afina. */
10981async function nrGrowth(){
10982  // The null from Cancel has to be checked BEFORE normalising: the old
10983  // `(prompt(...)||'')` turned it into an empty string, so cancelling the
10984  // dialog built a draft anyway with whatever the list had next.
10985  const raw=prompt('Ticker (vacío = siguiente de la lista de 88 empresas):','');
10986  if(raw===null)return;
10987  const t=raw.trim().toUpperCase();
10988  const rawY=prompt('¿Cuántos años atrás?','10');
10989  if(rawY===null)return;
10990  const y=parseInt(rawY||'10',10);
10991  if(!y||y<1||y>40){_nrSay('Años inválidos (1-40).');return;}
10992  _nrSay('Calculando el crecimiento…');
10993  try{
10994    const p=new URLSearchParams({years:String(y)});
10995    if(t)p.set('ticker',t);
10996    const r=await api('POST','/api/admin/newsroom/growth?'+p.toString());
10997    const d=await r.json().catch(()=>({}));
10998    if(!r.ok){_nrSay('No se pudo: '+(d.detail||r.status));return;}
10999    _nrSay('Listo — '+d.company+': x'+d.multiple.toFixed(1)
11000           +' con una caída máxima de '+Math.round(d.drawdown*100)+'%. En la cola, sin publicar.');
11001    await loadNewsroom();
11002  }catch(e){_nrSay('Error: '+e.message);}
11003}
11004
11005/* Lección de trading. Sin cron — solo sale cuando Abraham la pide. */
11006async function nrLesson(){
11007  const raw=prompt(
11008    'Número de lección (vacío = la siguiente sin usar):\n\n'
11009    +'1 · Un soporte que aguantó tres veces\n'
11010    +'2 · Dónde va el stop, y por qué no a un 2%\n'
11011    +'3 · Cuando la tendencia cambia de verdad\n'
11012    +'4 · Por qué puedes acertar el 40% y aun así ganar\n'
11013    +'5 · La misma ruptura, con volumen y sin volumen\n'
11014    +'6 · Por qué el precio vuelve al nivel que rompió','');
11015  if(raw===null)return;
11016  const n=raw.trim();
11017  if(n && !/^[1-6]$/.test(n)){_nrSay('Número inválido (1-6).');return;}
11018  _nrSay('Buscando un caso real en el histórico…');
11019  try{
11020    const r=await api('POST','/api/admin/newsroom/lesson'+(n?('?lesson='+n):''));
11021    const d=await r.json().catch(()=>({}));
11022    if(!r.ok){_nrSay('No se pudo: '+(d.detail||r.status));return;}
11023    _nrSay('Listo — "'+d.title+'" con '+d.ticker+' ('+d.date+'). '
11024           +'Dale a 🎓 Renderizar lección en el draft para generar el video.');
11025    await loadNewsroom();
11026  }catch(e){_nrSay('Error: '+e.message);}
11027}
11028
11029/* El render narrado dibuja ~1.200 cuadros y sintetiza la voz, así que
11030   tiene su propio contador, más largo que el de los otros reels. */
11031async function _nrPollLesson(id,tries,prev){
11032  tries=tries||0;
11033  const msg=()=>$('nr-msg-'+id);
11034  if(tries>90){const m=msg();if(m)m.textContent='La lección tarda más de 15 min — revisa los logs.';return;}
11035  await new Promise(r=>setTimeout(r,10000));
11036  try{
11037    const r=await api('GET','/api/admin/newsroom/drafts?status=pending,approved,partial,failed&limit=30');
11038    if(r.ok){
11039      const d=(await r.json()).find(x=>x.id===id);
11040      if(d&&d.error){const m=msg();
11040if(m)m.textContent='Render falló: '+d.error;return;}
11041      const now=_nrKeyUrl(d,'leccion');
11042      if(now&&now!==prev){await loadNewsroom();return;}
11043    }
11044  }catch(e){}
11045  const m=msg();
11046  if(m)m.textContent='Narrando y dibujando la gráfica… ('+((tries+1)*10)+'s)';
11047  _nrPollLesson(id,tries+1,prev);
11048}
11049
11050async function nrRenderLesson(id,btn){
11051  _nrBusy(btn,'Narrando…');
11052  const msg=$('nr-msg-'+id);
11053  const prev=_nrKeyUrl({asset_urls:{}},'leccion');
11054  try{
11055    const r=await api('POST','/api/admin/newsroom/drafts/'+id+'/render-lesson');
11056    const d=await r.json().catch(()=>({}));
11057    if(!r.ok){if(msg)msg.textContent='No se pudo: '+(d.detail||r.status);}
11058    else{
11059      if(msg)msg.textContent='Narrando y dibujando la gráfica… tarda unos minutos.';
11060      _nrPollLesson(id,0,prev);
11061    }
11062  }catch(e){if(msg)msg.textContent='Error: '+e.message;}
11063  _nrDone(btn);
11064}
11065
11066async function nrUseAnimated(id,btn,key){
11067  _nrBusy(btn,'…');
11068  const msg=$('nr-msg-'+id);
11069  try{
11070    const r=await api('POST','/api/admin/newsroom/drafts/'+id+'/use-animated?key='+(key||'animado'));
11071    const d=await r.json().catch(()=>({}));
11072    if(!r.ok){if(msg)msg.textContent='No se pudo usar: '+(d.detail||r.status);}
11073    else{
11074      if(msg)msg.textContent=d.converted
11075        ? 'Listo — convertido a REEL: IG y TikTok reciben el animado (el reel llega a no-seguidores). Las imágenes quedan guardadas. Falta aprobar y publicar.'
11076        : 'Listo — IG y TikTok recibirán el animado. Falta aprobar y publicar.';
11077      await loadNewsroom();
11078    }
11079  }catch(e){if(msg)msg.textContent='Error: '+e.message;}
11080  _nrDone(btn);
11081}
11082
11083/* Same shape as the animated poll, watching the 'pexels' key. Given its
11084   own timer because decoding footage per frame is slower — a run that is
11085   still healthy at 4 minutes would look failed on the animated ceiling. */
11086async function _nrPollPexels(id,tries,prev){
11087  tries=tries||0;
11088  const msg=()=>$('nr-msg-'+id);
11089  if(tries>90){const m=msg();if(m)m.textContent='El reel con metraje tarda más de 15 min — revisa los logs.';return;}
11090  await new Promise(r=>setTimeout(r,10000));
11091  try{
11092    const r=await api('GET','/api/admin/newsroom/drafts?status=pending,approved,partial,failed&limit=30');
11093    if(r.ok){
11094      const d=(await r.json()).find(x=>x.id===id);
11095      if(d&&d.error){const m=msg();if(m)m.textContent='Render falló: '+d.error;return;}
11096      const now=_nrPexelsUrl(d);
11097      if(now&&now!==prev){await loadNewsroom();return;}
11098    }
11099  }catch(e){}
11100  const m=msg();
11101  if(m)m.textContent='Buscando metraje y renderizando… ('+((tries+1)*10)+'s)';
11102  _nrPollPexels(id,tries+1,prev);
11103}
11104
11105async function nrRenderPexels(id,btn){
11106  _nrBusy(btn,'Metraje…');
11107  const msg=$('nr-msg-'+id);
11108  const prev=NR_ANIM_PX[id]||'';
11109  try{
11110    const r=await api('POST','/api/admin/newsroom/drafts/'+id+'/render-video');
11111    const d=await r.json().catch(()=>({}));
11112    if(!r.ok){if(msg)msg.textContent='No se pudo: '+(d.detail||r.status);}
11113    else{
11114      if(msg)msg.textContent='Buscando metraje y renderizando… tarda 3-5 min.';
11115      _nrPollPexels(id,0,prev);
11116    }
11117  }catch(e){if(msg)msg.textContent='Error: '+e.message;}
11118  _nrDone(btn);
11119}
11120
11121async function nrRenderAnimated(id,btn){
11122  _nrBusy(btn,'Animando…');
11123  const msg=$('nr-msg-'+id);
11124  const prev=NR_ANIM[id]||'';
11125  try{
11126    const r=await api('POST','/api/admin/newsroom/drafts/'+id+'/render-animated');
11127    const d=await r.json().catch(()=>({}));
11128    if(!r.ok){if(msg)msg.textContent='Render animado falló: '+(d.detail||r.status);}
11129    else{
11130      if(msg)msg.textContent='Renderizando el reel animado… se actualiza solo al terminar.'
11131             +(prev?' El de abajo es el anterior.':'');
11132      _nrPollAnimated(id,0,prev);
11133    }
11134  }catch(e){if(msg)msg.textContent='Error: '+e.message;}
11135  _nrDone(btn);
11136}
11137
11138/* Reel encoding runs in a background task, so the render endpoint returns
11139   before the video exists. Polling here rather than telling the user to
11140   guess when to refresh — they cannot know, and asking them to keep
11141   clicking is not a reasonable answer. */
11142async function _nrPollAssets(id,tries){
11143  tries=tries||0;
11144  const msg=()=>$('nr-msg-'+id);
11145  if(tries>18){const m=msg();if(m)m.textContent='El render está tardando más de lo normal — usa ↻ Refresh.';return;}
11146  await new Promise(r=>setTimeout(r,10000));
11147  try{
11148    const r=await api('GET','/api/admin/newsroom/drafts?status=pending,approved,partial,failed&limit=30');
11149    if(r.ok){
11150      const d=(await r.json()).find(x=>x.id===id);
11151      if(d&&d.error){const m=msg();
11151if(m)m.textContent='Render falló: '+d.error;return;}
11152      if(_nrAssetCount(d)>0){await loadNewsroom();return;}
11153    }
11154  }catch(e){}
11155  const m=msg();
11156  if(m)m.textContent='Renderizando el video… ('+((tries+1)*10)+'s)';
11157  _nrPollAssets(id,tries+1);
11158}
11159
11160async function nrRender(id,btn){
11161  // Preview writes into the same slot the promoted animated video lives
11162  // in, so running it after "Usar este para publicar" would quietly put
11163  // the static version back.
11164  if(NR_PROMOTED[id] && !confirm(
11165     'Este draft está publicando el REEL ANIMADO.\n\n'
11166     +'Volver a renderizar lo reemplaza por la versión estática.\n\n¿Continuar?'))return;
11167  _nrBusy(btn,'Rendering…');
11168  const msg=$('nr-msg-'+id);
11169  try{
11170    const r=await api('POST','/api/admin/newsroom/drafts/'+id+'/render');
11171    const d=await r.json().catch(()=>({}));
11172    if(!r.ok){if(msg)msg.textContent='Render failed: '+(d.detail||r.status);}
11173    else if(d.status==='rendering'){
11174      if(msg)msg.textContent='Renderizando el video… se actualiza solo al terminar.';
11175      _nrPollAssets(id,0);
11176    }
11177    else{if(msg)msg.textContent='Rendered '+d.count+' asset(s).';await loadNewsroom();}
11178  }catch(e){if(msg)msg.textContent='Render error: '+e.message;}
11179  _nrDone(btn);
11180}
11181
11182async function nrApprove(id,btn){
11183  _nrBusy(btn,'…');
11184  const msg=$('nr-msg-'+id);
11185  try{
11186    const r=await api('POST','/api/admin/newsroom/drafts/'+id+'/approve');
11187    if(r.ok){
11188      if(msg)msg.textContent='Aprobado — aún no se publica. Usa “Publicar a Buffer”.';
11189      await loadNewsroom();
11190    }else{
11191      // Report the status and body. "Approve failed." on its own told
11192      // neither Abraham nor me anything about what to fix.
11193      const body=await r.text().catch(()=>'');
11194      if(msg)msg.textContent='Aprobar falló ('+r.status+'): '+body.slice(0,220);
11195    }
11196  }catch(e){if(msg)msg.textContent='Error: '+e.message;}
11197  _nrDone(btn);
11198}
11199
11200async function nrPublish(id,btn){
11201  if(!confirm('Publicar a Buffer? Los canales que ya recibieron este draft se omiten.'))return;
11202  _nrBusy(btn,'Publicando…');
11203  const msg=$('nr-msg-'+id);
11204  try{
11205    const r=await api('POST','/api/admin/newsroom/drafts/'+id+'/publish?mode=addToQueue');
11206    const d=await r.json().catch(()=>({}));
11207    if(!r.ok){if(msg)msg.textContent='Publish failed: '+(d.detail||r.status);}
11208    else{
11209      if(msg)msg.textContent='Enviado a '+d.posted+' canal(es)'
11210        +((d.skipped)?(' · '+d.skipped+' ya estaban publicados'):'')
11211        +((d.errors&&d.errors.length)?(' · errores: '+d.errors.join('; ')):'');
11212      await loadNewsroom();
11213    }
11214  }catch(e){if(msg)msg.textContent='Publish error: '+e.message;}
11215  _nrDone(btn);
11216}
11217
11218async function nrReject(id,btn){
11219  _nrBusy(btn,'…');
11220  try{
11221    const r=await api('POST','/api/admin/newsroom/drafts/'+id+'/reject');
11222    if(!r.ok){
11223      const body=await r.text().catch(()=>'');
11224      const m=$('nr-msg-'+id); if(m)m.textContent='Rechazar falló ('+r.status+'): '+body.slice(0,220);
11225    }else{
11226      // Say what the cleanup actually did. "Rejected" alone left it
11227      // unclear whether the Cloudinary space was reclaimed or not.
11228      const d=await r.json().catch(()=>({}));
11229      if(d.kept_because_posted) _nrSay('Rechazado. Los archivos se conservan porque ya se publicó en algún canal.');
11230      else if(d.assets_deleted) _nrSay('Rechazado y '+d.assets_deleted+' archivo(s) borrados de Cloudinary.'
11231                                       +(d.assets_failed?(' '+d.assets_failed+' no se pudieron borrar.'):''));
11232      else _nrSay('Rechazado.');
11233      await loadNewsroom();
11234    }
11235  }catch(e){_nrSay('Error: '+e.message);}
11236  _nrDone(btn);
11237}
11238
11239async function loadSocialPillars(){
11240  try{
11241    const r=await api('GET','/api/admin/social-pillars/config');
11242    if(!r.ok)return;
11243    const d=await r.json();
11244    const sc=(id,v)=>{const el=$(id);if(el)el.checked=!!v;};
11245    sc('sp-tradingview', d.tradingview_enabled);
11246    sc('sp-eps',         d.eps_enabled);
11247    sc('sp-movers',      d.movers_enabled);
11248    sc('sp-options',     d.options_enabled);
11249    sc('sp-headline',    d.headline_enabled);
11250  }catch(e){console.error('loadSocialPillars',e);}
11251
11252  try{
11253    const r=await api('GET','/api/admin/social-pillars/recent-posts?limit=30');
11254    const posts=r.ok?await r.json():[];
11255    const tb=$('sp-posts-body'), emp=$('sp-posts-empty');
11256    if(!posts.length){ if(tb)tb.innerHTML=''; if(emp)emp.style.display=''; return; }
11257    if(emp)emp.style.display='none';
11258    if(tb)tb.innerHTML=posts.map(p=>`<tr style="border-bottom:1px solid var(--border)">
11259      <td style="padding:5px 8px;color:var(--teal);font-weight:700;text-transform:uppercase;font-size:.68rem">${p.pillar}</td>
11260      <td style="padding:5px 8px;color:var(--text);max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${(p.text||'').replace(/</g,'&lt;')}</td>
11261      <td style="padding:5px 8px;color:var(--muted);font-size:.68rem">${p.channel_id}</td>
11262      <td style="padding:5px 8px;color:var(--muted);font-size:.68rem">
11262${p.posted_at?new Date(p.posted_at).toLocaleString():'—'}</td>
11263    </tr>`).join('');
11264  }catch(e){console.error('loadSocialPillars posts',e);}
11265}
11266
11267async function triggerSocialPillar(pillar){
11268  const res=$('sp-trigger-result');
11269  if(res)res.textContent=`Triggering ${pillar}…`;
11270  try{
11271    const r=await api('POST',`/api/admin/social-pillars/trigger/${pillar}`);
11272    const d=await r.json();
11273    if(r.ok){
11274      if(res)res.textContent=`${pillar} started — check Recent Posts below in ~30-60s.`;
11275      toast(`${pillar} triggered`,'ok');
11276      setTimeout(loadSocialPillars, 8000);
11277    } else {
11278      if(res)res.textContent=d.detail||'Trigger failed';
11279    }
11280  }catch(e){if(res)res.textContent='Error triggering';}
11281}
11282
11283async function saveSocialPillarConfig(){
11284  const gc=(id)=>{const el=$(id);return el?el.checked:false;};
11285  const payload={
11286    tradingview_enabled: gc('sp-tradingview'),
11287    eps_enabled:         gc('sp-eps'),
11288    movers_enabled:      gc('sp-movers'),
11289    options_enabled:     gc('sp-options'),
11290    headline_enabled:    gc('sp-headline'),
11291  };
11292  const res=$('sp-config-result');
11293  try{
11294    const r=await api('PUT','/api/admin/social-pillars/config',payload);
11295    const d=await r.json();
11296    if(r.ok){if(res)res.innerHTML='<span style="color:var(--green)">Saved</span>';toast('Social pillar config saved','ok');}
11297    else{if(res)res.innerHTML=`<span style="color:var(--red)">${d.detail||'Save failed'}</span>`;}
11298  }catch(e){if(res)res.innerHTML='<span style="color:var(--red)">Error saving</span>';}
11299}
11300
11301async function loadAdminPolymarketAlerts(){
11302  const list=$('pm-admin-alerts-list');
11303  if(!list)return;
11304  list.innerHTML='<span style="font-size:.72rem;color:var(--muted)">Loading…</span>';
11305  try{
11306    const r=await api('GET','/api/admin/polymarket/alerts');
11307    const d=await r.json();
11308    if(!r.ok||!d.length){list.innerHTML='<span style="font-size:.72rem;color:var(--muted)">No alerts in DB.</span>';return;}
11309    const typeLabel={oi_spike:'OI',volume_surge:'VOL',book_imbalance:'BOOK',spread_spike:'SPR'};
11310    const typeColor={oi_spike:'#a78bfa',volume_surge:'#00c853',book_imbalance:'#fbbf24',spread_spike:'#f97316'};
11311    list.innerHTML=d.map(a=>`
11312      <div style="display:flex;gap:8px;align-items:flex-start;padding:6px 0;border-bottom:1px solid var(--border);font-size:.72rem">
11313        <span style="color:${typeColor[a.alert_type]||'#aaa'};font-weight:700;min-width:36px;flex-shrink:0">${typeLabel[a.alert_type]||'?'}</span>
11314        <span style="flex:1;color:var(--text2);line-height:1.3">${a.question.substring(0,70)}${a.question.length>70?'&hellip;':''}</span>
11315        <span style="font-family:var(--mono);color:var(--muted);white-space:nowrap">${new Date(a.created_at).toLocaleDateString()}</span>
11316      </div>`).join('');
11317  }catch(e){list.innerHTML=`<span style="font-size:.72rem;color:var(--red)">Error: ${e.message}</span>`;}
11318}
11319
11320async function clearPolymarketAlerts(){
11321  if(!confirm('Delete all Polymarket alert history from DB?'))return;
11322  try{
11323    const r=await api('DELETE','/api/admin/polymarket/alerts');
11324    if(r.ok){toast('Alert history cleared','ok');const l=$('pm-admin-alerts-list');if(l)l.innerHTML='';}
11325    else toast('Failed to clear','err');
11326  }catch(e){toast('Error','err');}
11327}
11328
11329function _initDjtToggle(){
11330  const btn=$('djt-toggle-btn');
11331  if(!btn)return;
11332  const on=!!(USER&&USER.djt_alerts);
11333  btn.textContent=on?'ON':'OFF';
11334  btn.className=on?'btn btn-primary':'btn btn-ghost';
11335  const fb=$('djt-filter-btn');
11336  if(fb)fb.style.display=on?'':'none';
11337}
11338
11339async function toggleDjtSubscribe(){
11340  try{
11341    const r=await api('POST','/api/news/djt-subscribe');
11342    const d=await r.json();
11343    if(!r.ok){toast(d.detail||'Failed','err');return;}
11344    if(USER)USER.djt_alerts=d.djt_alerts;
11345    const btn=$('djt-toggle-btn');
11346    if(btn){btn.textContent=d.djt_alerts?'ON':'OFF';btn.className=d.djt_alerts?'btn btn-primary':'btn btn-ghost';}
11347    const fb=$('djt-filter-btn');
11348    if(fb)fb.style.display=d.djt_alerts?'':'none';
11349    toast(d.djt_alerts?'DJT feed enabled':'DJT feed disabled','ok');
11350  }catch(e){toast('Error','err');}
11351}
11352
11353async function adminDjtRefresh(btn){
11354  const res=$('djt-refresh-result');
11355  if(btn)btn.disabled=true;
11356  if(res)res.innerHTML='<span class="spin"></span> Fetching…';
11357  try{
11358    const r=await api('POST','/api/news/djt-refresh');
11359    const d=await r.json();
11360    if(!r.ok){
11361      if(res)res.textContent='Error: '+(d.detail||r.status);
11362      toast(d.detail||'DJT refresh failed','err');
11363      return;
11364    }
11365    if(res)res.textContent='✓ Triggered — check Railway logs for result';
11366    toast('DJT fetch triggered','ok');
11367  }catch(e){
11368    if(res)res.textContent='Network error';
11369    toast('Network error','err');
11370  }finally{
11371    if(btn)btn.disabled=false;
11372  }
11373}
11374
11375// ─── DESIGN LAB (Fabric.js) ───────────────────────────────────────────────────
11376let _dlCanvas=null;
11377let _dlFont='Inter';
11378let _dlBgTab='gradients';
11379
11380const DL_GRADIENTS=[
11381  {label:'Midnight Finance',stops:[{offset:0,color:'#0b132b'},{offset:1,color:'#1a2a4a'}
11381]},
11382  {label:'Dark Navy',       stops:[{offset:0,color:'#0d1b2a'},{offset:1,color:'#1b2838'}]},
11383  {label:'Deep Forest',     stops:[{offset:0,color:'#0a1a0f'},{offset:1,color:'#1a3a20'}]},
11384  {label:'Charcoal',        stops:[{offset:0,color:'#1a1a1a'},{offset:1,color:'#2d2d2d'}]},
11385  {label:'Burgundy Dark',   stops:[{offset:0,color:'#1a0a0a'},{offset:1,color:'#3a1515'}]},
11386  {label:'Deep Purple',     stops:[{offset:0,color:'#12001f'},{offset:1,color:'#2a0a4a'}]},
11387  {label:'Gold Dark',       stops:[{offset:0,color:'#1a1200'},{offset:1,color:'#2d2000'}]},
11388  {label:'Slate Blue',      stops:[{offset:0,color:'#0f1524'},{offset:1,color:'#1e2d4a'}]},
11389];
11390
11391const DL_PHOTOS=[
11392  // Finance & Markets
11393  {cat:'finance', label:'Trading Floor',   url:'https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=1080&q=80'},
11394  {cat:'finance', label:'Wall Street',     url:'https://images.unsplash.com/photo-1526304640581-d334cdbbf45e?w=1080&q=80'},
11395  {cat:'finance', label:'Gold Chart',      url:'https://images.unsplash.com/photo-1642790551116-18e150f248e3?w=1080&q=80'},
11396  {cat:'finance', label:'City Night',      url:'https://images.unsplash.com/photo-1519501025264-65ba15a82390?w=1080&q=80'},
11397  {cat:'finance', label:'Stock Screen',    url:'https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=1080&q=80'},
11398  {cat:'finance', label:'Trading Desk',    url:'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?w=1080&q=80'},
11399  {cat:'finance', label:'Bull Market',     url:'https://images.unsplash.com/photo-1543286386-713bdd548da4?w=1080&q=80'},
11400  {cat:'finance', label:'Dollar Bills',    url:'https://images.unsplash.com/photo-1580048915913-4fb768f76a0f?w=1080&q=80'},
11401  // Abstract & Dark
11402  {cat:'abstract', label:'Data Globe',     url:'https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1080&q=80'},
11403  {cat:'abstract', label:'Dark Bokeh',     url:'https://images.unsplash.com/photo-1557682224-5b8590cd9ec5?w=1080&q=80'},
11404  {cat:'abstract', label:'Tech Lines',     url:'https://images.unsplash.com/photo-1518770660439-4636190af475?w=1080&q=80'},
11405  {cat:'abstract', label:'Purple Haze',    url:'https://images.unsplash.com/photo-1579548122080-c35fd6820ecb?w=1080&q=80'},
11406  {cat:'abstract', label:'Neon Glow',      url:'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=1080&q=80'},
11407  {cat:'abstract', label:'Deep Space',     url:'https://images.unsplash.com/photo-1462331940025-496dfbfc7564?w=1080&q=80'},
11408  {cat:'abstract', label:'Dark Waves',     url:'https://images.unsplash.com/photo-1464802686167-b01fd7bdc50b?w=1080&q=80'},
11409  {cat:'abstract', label:'Blue Circuit',   url:'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=1080&q=80'},
11410  {cat:'abstract', label:'Matrix Code',    url:'https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=1080&q=80'},
11411  {cat:'abstract', label:'Dark Minimal',   url:'https://images.unsplash.com/photo-1508739773434-c26b3d09e071?w=1080&q=80'},
11412  {cat:'abstract', label:'Laser Beams',    url:'https://images.unsplash.com/photo-1550684848-fac9c2a1a574?w=1080&q=80'},
11413  {cat:'abstract', label:'Liquid Paint',   url:'https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=1080&q=80'},
11414  {cat:'abstract', label:'Gold Fluid',     url:'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=1080&q=80'},
11415  {cat:'abstract', label:'Dark Smoke',     url:'https://images.unsplash.com/photo-1567359781514-3b964e2b04d6?w=1080&q=80'},
11416  // Textures
11417  {cat:'texture', label:'Dark Marble',     url:'https://images.unsplash.com/photo-1541123437800-1bb1317badc2?w=1080&q=80'},
11418  {cat:'texture', label:'White Marble',    url:'https://images.unsplash.com/photo-1558618048-fbd6ed5c06fd?w=1080&q=80'},
11419  {cat:'texture', label:'Carbon Fiber',    url:'https://images.unsplash.com/photo-1553356084-58ef4a67b2a7?w=1080&q=80'},
11420  {cat:'texture', label:'Dark Fabric',     url:'https://images.unsplash.com/photo-1528360983277-13d401cdc186?w=1080&q=80'},
11421  {cat:'texture', label:'Concrete Wall',   url:'https://images.unsplash.com/photo-1484154218962-a197022b5858?w=1080&q=80'},
11422  {cat:'texture', label:'Dark Wood',       url:'https://images.unsplash.com/photo-1541014741259-de529411b96a?w=1080&q=80'},
11423  {cat:'texture', label:'Sand Dunes',      url:'https://images.unsplash.com/photo-1509316785289-025f5b846b35?w=1080&q=80'},
11424  {cat:'texture', label:'Water Ripple',    url:'https://images.unsplash.com/photo-1559827291-72ee739d0d9a?w=1080&q=80'},
11425  {cat:'texture', label:'Cracked Earth',   url:'https://images.unsplash.com/photo-1508193638397-1c4234db14d8?w=1080&q=80'},
11426  {cat:'texture', label:'Rust Metal',      url:'https://images.unsplash.com/photo-1504328345606-18bbc8c9d7d1?w=1080&q=80'},
11427  {cat:'texture', label:'Brushed Metal',   url:'https://images.unsplash.com/photo-1519389950473-47ba0277781c?w=1080&q=80'},
11428  {cat:'texture', label:'Dark Ink',        url:'https://images.unsplash.com/photo-1553356084-58ef4a67b2a7?w=1080&q=80'},
11429  // Architecture
11430  {cat:'architecture', label:'Glass Tower',    url:'https://images.unsplash.com/photo-1486325212027-8081e485255e?w=1080&q=80'},
11431  {cat:'architecture', label:'Night City',     url:'https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1080&q=80'},
11432  {cat:'architecture', label:'City Lights',    url:'https://images.unsplash.com/photo-1480714378408-67cf0d13bc1b?w=1080&q=80'},
11433  {cat:'architecture', label:'Skyscraper',     url:'https://images.unsplash.com/photo-1486406646926-a93f4929a2cc?w=1080&q=80'},
11434  {cat:'architecture', label:'Dark Office',    url:'https://images.unsplash.com/photo-1497366216548-37526070297c?w=1080&q=80'},
11435  {cat:'architecture', label:'Tokyo Night',    url:'https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=1080&q=80'},
11436  // Nature
11437  {cat:'nature', label:'Dark Ocean',       url:'https://images.unsplash.com/photo-1501854140801-50d01698950b?w=1080&q=80'},
11438  {cat:'nature', label:'Misty Mountain',   url:'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=1080&q=80'},
11439  {cat:'nature', label:'Dark Forest',      url:'https://images.unsplash.com/photo-1448375240586-882707db888b?w=1080&q=80'},
11440  {cat:'nature', label:'Night Sky',        url:'https://images.unsplash.com/photo-1419242902214-272b3f66ee7a?w=1080&q=80'},
11441  {cat:'nature', label:'Storm Clouds',     url:'https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?w=1080&q=80'},
11442  {cat:'nature', label:'Volcano',          url:'https://images.unsplash.com/photo-1455218873509-8097305ee378?w=1080&q=80'},
11443  {cat:'nature', label:'Dark Sea',         url:'https://images.unsplash.com/photo-1505118380757-91f5f5632de0?w=1080&q=80'},
11444  {cat:'nature', label:'Green Hills',      url:'https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?w=1080&q=80'},
11445];
11446
11447const DL_FONTS=[
11448  'Inter','Roboto','Open Sans','Montserrat','Raleway',
11449  'Playfair Display','Merriweather','Bebas Neue','Anton','Oswald',
11450  'Space Mono','IBM Plex Mono','Poppins','Nunito','Lato',
11451  'Barlow Condensed','Syne','Cinzel','Cormorant Garamond','DM Sans',
11452];
11453
11454const DL_COLOR_SWATCHES=['#0b132b','#1a1a1a','#ffffff','#e2c275','#22c55e','#ef4444','#3b82f6','#a855f7'];
11455
11456function _dlBuildGallery(){
11457  // Gradients
11458  const gWrap=document.getElementById('dl-bg-gradients');
11459  if(gWrap&&!gWrap.childElementCount){
11460    DL_GRADIENTS.forEach((g,i)=>{
11461      const btn=document.createElement('button');
11462      btn.title=g.label;
11463      btn.onclick=()=>dlApplyGradient(i);
11464      btn.style.cssText='width:64px;height:42px;border:2px solid #202e56;border-radius:6px;cursor:pointer;padding:0;overflow:hidden;transition:border-color .15s';
11465      btn.onmouseover=()=>{btn.style.borderColor='#e2c275';};
11466      btn.onmouseout=()=>{btn.style.borderColor='#202e56';};
11467      const c=document.createElement('canvas');c.width=64;c.height=42;
11468      const ctx=c.getContext('2d');
11469      const gr=ctx.createLinearGradient(0,0,0,42);
11470      g.stops.forEach(s=>gr.addColorStop(s.offset,s.color));
11471      ctx.fillStyle=gr;ctx.fillRect(0,0,64,42);
11472      btn.appendChild(c);
11473      gWrap.appendChild(btn);
11474    });
11475  }
11476  // Photos + category filters
11477  const pGrid=document.getElementById('dl-photo-grid');
11478  const pCats=document.getElementById('dl-photo-cats');
11479  if(pGrid&&!pGrid.childElementCount){
11480    // Category filter buttons
11481    const cats=['all','finance','abstract','texture','architecture','nature'];
11482    const catLabels={all:'All',finance:'Finance',abstract:'Abstract',texture:'Textures',architecture:'Cities',nature:'Nature'};
11483    cats.forEach(cat=>{
11484      const fb=document.createElement('button');
11485      fb.className='btn btn-ghost';
11486      fb.dataset.pcat=cat;
11487      fb.textContent=catLabels[cat];
11488      fb.style.cssText='font-size:.65rem;padding:3px 10px;'+(cat==='all'?'border-color:var(--teal);color:var(--teal)':'');
11489      fb.onclick=()=>dlFilterPhotos(cat);
11490      pCats.appendChild(fb);
11491    });
11492    // Photo thumbnails
11493    DL_PHOTOS.forEach(p=>{
11494      const btn=document.createElement('button');
11495      btn.title=p.label;
11496      btn.dataset.pcat=p.cat;
11497      btn.onclick=()=>dlApplyPhoto(p.url);
11498      btn.style.cssText='width:64px;height:64px;border:2px solid #202e56;border-radius:6px;cursor:pointer;padding:0;overflow:hidden;transition:border-color .15s;background:#0b132b;flex-shrink:0';
11499      btn.onmouseover=()=>{btn.style.borderColor='#e2c275';};
11500      btn.onmouseout=()=>{btn.style.borderColor='#202e56';};
11501      const img=document.createElement('img');
11502      img.src=p.url.replace('w=1080','w=128');
11503      img.style.cssText='width:100%;height:100%;object-fit:cover;display:block';
11504      img.loading='lazy';
11505      img.onerror=()=>{btn.style.display='none';};
11506      btn.appendChild(img);
11507      pGrid.appendChild(btn);
11508    });
11509  }
11510  // Color swatches
11511  const cWrap=document.getElementById('dl-color-swatches');
11512  if(cWrap&&!cWrap.childElementCount){
11513    DL_COLOR_SWATCHES.forEach(hex=>{
11514      const btn=document.createElement('button');
11515      btn.title=hex;
11516      btn.onclick=()=>{dlSetBg(hex);document.getElementById('dl-bg-color').value=hex;};
11517      btn.style.cssText=`width:28px;height:28px;background:${hex};border:2px solid #202e56;border-radius:50%;cursor:pointer;transition:border-color .15s`;
11518      btn.onmouseover=()=>{btn.style.borderColor='#e2c275';};
11519      btn.onmouseout=()=>{btn.style.borderColor='#202e56';};
11520      cWrap.appendChild(btn);
11521    });
11522  }
11523  // Font select
11524  const sel=document.getElementById('dl-font-select');
11525  if(sel&&!sel.childElementCount){
11526    DL_FONTS.forEach(f=>{
11527      const opt=document.createElement('option');opt.value=f;opt.textContent=f;
11528      if(f===_dlFont)opt.selected=true;
11529      sel.appendChild(opt);
11530    });
11531    dlLoadFont(_dlFont);
11532  }
11533}
11534
11535function dlFilterPhotos(cat){
11536  const pGrid=document.getElementById('dl-photo-grid');
11537  if(!pGrid)return;
11538  pGrid.querySelectorAll('button[data-pcat]').forEach(btn=>{
11539    btn.style.display=(cat==='all'||btn.dataset.pcat===cat)?'':'none';
11540  });
11541  const pCats=document.getElementById('dl-photo-cats');
11542  if(pCats)pCats.querySelectorAll('button[data-pcat]').forEach(fb=>{
11543    const active=fb.dataset.pcat===cat;
11544    fb.style.borderColor=active?'var(--teal)':'';
11545    fb.style.color=active?'var(--teal)':'';
11546  });
11547}
11548
11549function dlSetBgTab(tab){
11550  _dlBgTab=tab;
11551  ['gradients','photos','colors','mybgs'].forEach(t=>{
11552    const panel=document.getElementById('dl-bg-'+t);
11553    const btn=document.getElementById('dl-tab-'+t);
11554    if(!panel||!btn)return;
11555    const active=(t===tab);
11556    panel.style.display=active?(t==='mybgs'?'flex':'flex'):'none';
11557    btn.style.borderBottomColor=active?'var(--teal)':'transparent';
11558    btn.style.color=active?'var(--teal)':'var(--muted)';
11559  });
11560  if(tab==='mybgs') dlLoadMyBgs();
11561}
11562
11563function dlApplyGradient(idx){
11564  if(!_dlCanvas)return;
11565  const g=DL_GRADIENTS[idx];
11566  const h=_dlCanvas._naturalH||_dlCanvas.getHeight();
11567  const grad=new fabric.Gradient({
11568    type:'linear',
11569    coords:{x1:0,y1:0,x2:0,y2:h},
11570    colorStops:g.stops,
11571  });
11572  _dlCanvas.setBackgroundColor(grad,_dlCanvas.renderAll.bind(_dlCanvas));
11573  toast('Gradient applied','ok');
11574}
11575
11576function dlApplyPhoto(url){
11577  if(!_dlCanvas)return;
11578  fabric.Image.fromURL(url,img=>{
11579    const nw=_dlCanvas._naturalW||_dlCanvas.getWidth();
11580    const nh=_dlCanvas._naturalH||_dlCanvas.getHeight();
11581    _dlCanvas.setBackgroundImage(img,()=>{_dlCanvas.renderAll();toast('Background set','ok');},{
11582      scaleX:nw/img.width,scaleY:nh/img.height,originX:'left',originY:'top',
11583    });
11584  },{crossOrigin:'anonymous'});
11585}
11586
11587function dlLoadFont(fontFamily){
11588  _dlFont=fontFamily;
11589  const id='gf-'+fontFamily.replace(/\s/g,'-');
11590  if(!document.getElementById(id)){
11591    const link=document.createElement('link');
11592    link.id=id;link.rel='stylesheet';
11593    link.href=`https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontFamily)}:wght@400;700&display=swap`;
11594    document.head.appendChild(link);
11595  }
11596}
11597
11598function _dlCalcScale(w,h){
11599  const availW=window.innerWidth-270;
11600  const availH=window.innerHeight-240;
11601  return Math.min(1,availW/w,availH/h);
11602}
11603function initDesignLab(){
11604  if(_dlCanvas)return;
11605  const scale=_dlCalcScale(1080,1080);
11606  _dlCanvas=new fabric.Canvas('dl-canvas',{width:Math.round(1080*scale),height:Math.round(1080*scale),backgroundColor:'#0b132b'});
11607  _dlCanvas._naturalW=1080;_dlCanvas._naturalH=1080;
11608  _dlCanvas.setZoom(scale);
11609  _dlBuildGallery();
11610  window.addEventListener('resize',()=>{if(_dlCanvas)dlResize(_dlCanvas._naturalW||1080,_dlCanvas._naturalH||1080);});
11611}
11612
11613function dlAddText(){
11614  if(!_dlCanvas)return;
11615  const font=document.getElementById('dl-font-select')?.value||_dlFont||'Inter';
11616  const size=parseInt(document.getElementById('dl-font-size')?.value||72);
11617  const t=new fabric.IText('Edit me',{
11618    left:80,top:80,
11619    fill:document.getElementById('dl-fill-color').value,
11620    fontSize:Math.round(size*_dlCanvas.getZoom()),
11621    fontFamily:font,fontWeight:'700',
11622  });
11623  _dlCanvas.add(t);_dlCanvas.setActiveObject(t);_dlCanvas.renderAll();
11624  toast('Text added — click to edit','ok');
11625}
11626function dlAddRect(){
11627  if(!_dlCanvas)return;
11628  const r=new fabric.Rect({left:120,top:120,width:200,height:120,fill:document.getElementById('dl-fill-color').value});
11629  _dlCanvas.add(r);_dlCanvas.setActiveObject(r);_dlCanvas.renderAll();
11630}
11631function dlAddCircle(){
11632  if(!_dlCanvas)return;
11633  const c=new fabric.Circle({left:160,top:160,radius:80,fill:document.getElementById('dl-fill-color').value});
11634  _dlCanvas.add(c);_dlCanvas.setActiveObject(c);_dlCanvas.renderAll();
11635}
11636function dlSetBg(color){
11637  if(!_dlCanvas)return;
11638  _dlCanvas.setBackgroundImage(null,()=>{});
11639  _dlCanvas.setBackgroundColor(color,_dlCanvas.renderAll.bind(_dlCanvas));
11640}
11641function dlSendToBack(){
11642  if(!_dlCanvas)return;
11643  const o=_dlCanvas.getActiveObject();
11644  if(o){_dlCanvas.sendToBack(o);_dlCanvas.renderAll();toast('Sent to back','ok');}
11645  else{toast('Select an object first','err');}
11646}
11647function dlBringToFront(){
11648  if(!_dlCanvas)return;
11649  const o=_dlCanvas.getActiveObject();
11650  if(o){_dlCanvas.bringToFront(o);_dlCanvas.renderAll();toast('Brought to front','ok');}
11651  else{toast('Select an object first','err');}
11652}
11653function dlDeleteSelected(){
11654  if(!_dlCanvas)return;
11655  const obj=_dlCanvas.getActiveObject();
11656  if(obj){_dlCanvas.remove(obj);_dlCanvas.renderAll();}
11657}
11658function dlAddImageFile(input){
11659  if(!_dlCanvas||!input.files[0])return;
11660  const reader=new FileReader();
11661  reader.onload=e=>fabric.Image.fromURL(e.target.result,img=>{
11662    img.scaleToWidth(Math.round(400*_dlCanvas.getZoom()));
11663    img.set({left:60,top:60});
11664    _dlCanvas.add(img);_dlCanvas.setActiveObject(img);_dlCanvas.renderAll();
11665  },{crossOrigin:'anonymous'});
11666  reader.readAsDataURL(input.files[0]);
11667  input.value='';
11668}
11669function dlResize(w,h){
11670  if(!_dlCanvas)return;
11671  if(_dlCanvas.isDrawingMode){
11672    _dlCanvas.isDrawingMode=false;
11673    const b=$('dl-draw-btn');if(b){b.style.background='';b.style.borderColor='';}
11674    const s=$('dl-brush-size');if(s)s.style.display='none';
11675    const t=$('dl-brush-type');if(t)t.style.display='none';
11676  }
11677  const scale=_dlCalcScale(w,h);
11678  _dlCanvas._naturalW=w;_dlCanvas._naturalH=h;
11679  _dlCanvas.setZoom(scale);
11680  _dlCanvas.setWidth(Math.round(w*scale));
11681  _dlCanvas.setHeight(Math.round(h*scale));
11682  _dlCanvas.renderAll();
11683}
11684function dlToggleDraw(){
11685  if(!_dlCanvas)return;
11686  const on=!_dlCanvas.isDrawingMode;
11687  _dlCanvas.isDrawingMode=on;
11688  const btn=$('dl-draw-btn');
11689  const sizeEl=$('dl-brush-size');
11690  const typeEl=$('dl-brush-type');
11691  if(btn){btn.style.background=on?'rgba(226,194,117,.2)':'';btn.style.borderColor=on?'var(--teal)':'';}
11692  if(sizeEl)sizeEl.style.display=on?'inline-block':'none';
11693  if(typeEl)typeEl.style.display=on?'inline-block':'none';
11694  if(on)dlUpdateBrush();
11695  toast(on?'Draw mode on — use your finger or stylus':'Draw mode off','ok');
11696}
11697function dlUpdateBrush(){
11698  if(!_dlCanvas||!_dlCanvas.isDrawingMode)return;
11699  const size=parseInt($('dl-brush-size')?.value||3);
11700  const type=$('dl-brush-type')?.value||'pencil';
11701  const color=$('dl-fill-color')?.value||'#e2c275';
11702  let brush;
11703  if(type==='spray') brush=new fabric.SprayBrush(_dlCanvas);
11704  else brush=new fabric.PencilBrush(_dlCanvas);
11705  brush.color=color;
11706  brush.width=type==='marker'?size*3:size;
11707  if(type==='spray'){brush.density=10;brush.dotWidth=size;}
11708  _dlCanvas.freeDrawingBrush=brush;
11709}
11710function dlExportPng(){
11711  if(!_dlCanvas)return;
11712  toast('Downloading design…','ok');
11713  const zoom=_dlCanvas.getZoom();
11714  const w=_dlCanvas._naturalW||Math.round(_dlCanvas.getWidth()/zoom);
11715  const h=_dlCanvas._naturalH||Math.round(_dlCanvas.getHeight()/zoom);
11716  const url=_dlCanvas.toDataURL({format:'png',quality:1,multiplier:1/zoom,width:w,height:h});
11717  const a=document.createElement('a');a.href=url;a.download='design.png';a.click();
11718}
11719let _dlGalleryOffset=0;
11720const _DL_GALLERY_LIMIT=40;
11721
11722async function dlOpenGalleryPicker(){
11723  const modal=document.getElementById('dl-gallery-modal');
11724  if(!modal)return;
11725  modal.style.display='block';
11726  _dlGalleryOffset=0;
11727  await _dlLoadGalleryPickerPage(true);
11728}
11729
11730async function _dlLoadGalleryPickerPage(reset){
11731  const grid=document.getElementById('dl-gallery-picker-grid');
11732  const moreBtn=document.getElementById('dl-gallery-picker-more');
11733  if(!grid)return;
11734  if(reset){grid.innerHTML='<div style="color:var(--muted);font-size:.78rem;padding:20px">Loading…</div>';}
11735  try{
11736    const qs=new URLSearchParams({limit:_DL_GALLERY_LIMIT,offset:_dlGalleryOffset});
11737    const res=await api('GET',`/api/gallery?${qs}`);
11738    const d=await res.json();
11739    const items=d.items||[];
11740    if(reset)grid.innerHTML='';
11741    if(!items.length&&reset){grid.innerHTML='<div style="color:var(--muted);font-size:.78rem;padding:20px">No images in your gallery yet.</div>';return;}
11742    items.forEach(item=>{
11743      const url=item.url||item.thumbnail||'';
11744      if(!url)return;
11745      const card=document.createElement('div');
11746      card.style.cssText='border-radius:8px;overflow:hidden;background:var(--bg3);border:1px solid var(--border)';
11747      const img=document.createElement('img');
11748      img.src=item.thumbnail||url;
11749      img.loading='lazy';
11750      img.style.cssText='width:100%;aspect-ratio:1;object-fit:cover;display:block';
11751      img.onerror=()=>{card.style.display='none';};
11752      const btnRow=document.createElement('div');
11753      btnRow.style.cssText='display:flex;gap:4px;padding:4px';
11754      const btnLayer=document.createElement('button');
11755      btnLayer.className='btn btn-ghost';
11756      btnLayer.style.cssText='flex:1;font-size:.62rem;padding:3px 0';
11757      btnLayer.textContent='+ Layer';
11758      btnLayer.onclick=()=>dlPickGalleryItem(url);
11759      const btnBg=document.createElement('button');
11760      btnBg.className='btn btn-ghost';
11761      btnBg.style.cssText='flex:1;font-size:.62rem;padding:3px 0';
11762      btnBg.textContent='🏞 BG';
11763      btnBg.onclick=()=>{dlApplyPhoto(url);document.getElementById('dl-gallery-modal').style.display='none';};
11764      btnRow.appendChild(btnLayer);
11765      btnRow.appendChild(btnBg);
11766      card.appendChild(img);
11767      card.appendChild(btnRow);
11768      grid.appendChild(card);
11769    });
11770    if(moreBtn)moreBtn.style.display=items.length===_DL_GALLERY_LIMIT?'block':'none';
11771  }catch(e){
11772    if(reset)grid.innerHTML=`<div style="color:var(--red);font-size:.78rem;padding:20px">Error: ${e.message}</div>`;
11773  }
11774}
11775
11776async function dlLoadGalleryPickerMore(){
11777  _dlGalleryOffset+=_DL_GALLERY_LIMIT;
11778  await _dlLoadGalleryPickerPage(false);
11779}
11780
11781function dlPickGalleryItem(url){
11782  if(!_dlCanvas)return;
11783  document.getElementById('dl-gallery-modal').style.display='none';
11784  fabric.Image.fromURL(url,img=>{
11785    const maxW=Math.round((_dlCanvas._naturalW||_dlCanvas.getWidth())*0.6*_dlCanvas.getZoom());
11786    img.scaleToWidth(maxW);
11787    img.set({left:60,top:60});
11788    _dlCanvas.add(img);_dlCanvas.setActiveObject(img);_dlCanvas.renderAll();
11789  },{crossOrigin:'anonymous'});
11790}
11791let _dlMyBgsLoaded=false;
11792async function dlLoadMyBgs(){
11793  if(_dlMyBgsLoaded)return;
11794  _dlMyBgsLoaded=true;
11795  const grid=document.getElementById('dl-mybg-grid');
11796  if(!grid)return;
11797  grid.innerHTML='<span style="font-size:.72rem;color:var(--muted)">Loading…</span>';
11798  try{
11799    const res=await api('GET','/api/gallery?limit=60');
11800    const d=await res.json();
11801    const items=d.items||[];
11802    grid.innerHTML='';
11803    if(!items.length){
11804      grid.innerHTML='<span style="font-size:.72rem;color:var(--muted)">No uploads yet — click + Upload Background to add your own.</span>';
11805      return;
11806    }
11807    items.forEach(item=>{
11808      const url=item.url||item.thumbnail||'';
11809      if(!url)return;
11810      const btn=document.createElement('button');
11811      btn.title='Set as background';
11812      btn.onclick=()=>dlApplyPhoto(url);
11813      btn.style.cssText='width:80px;height:80px;border:2px solid #202e56;border-radius:6px;cursor:pointer;padding:0;overflow:hidden;background:#0b132b;flex-shrink:0;transition:border-color .15s';
11814      btn.onmouseover=()=>{btn.style.borderColor='#e2c275';};
11815      btn.onmouseout=()=>{btn.style.borderColor='#202e56';};
11816      const img=document.createElement('img');
11817      img.src=item.thumbnail||url;
11818      img.style.cssText='width:100%;height:100%;object-fit:cover;display:block';
11819      img.loading='lazy';
11820      img.onerror=()=>{btn.style.display='none';};
11821      btn.appendChild(img);
11822      grid.appendChild(btn);
11823    });
11824  }catch(e){
11825    grid.innerHTML=`<span style="font-size:.72rem;color:var(--red)">Error: ${e.message}</span>`;
11826  }
11827}
11828async function dlUploadMyBg(input){
11829  if(!input.files[0])return;
11830  const fd=new FormData();
11831  fd.append('file',input.files[0]);
11832  try{
11833    const res=await fetch('/api/gallery/upload',{
11834      method:'POST',
11835      headers:{Authorization:'Bearer '+localStorage.getItem('fz_token')},
11836      body:fd,
11837    });
11838    if(!res.ok){toast('Upload failed','err');return;}
11839    const item=await res.json();
11840    const url=item.url||item.thumbnail||'';
11841    if(url)dlApplyPhoto(url);
11842    _dlMyBgsLoaded=false;
11843    dlLoadMyBgs();
11844    toast('Background saved to gallery','ok');
11845  }catch(e){toast('Upload error','err');}
11846  input.value='';
11847}
11848// ─── END DESIGN LAB ───────────────────────────────────────────────────────────
11849
11850// legacy toggle (kept for compat)
11851async function togglePolymarketSubscribe(){
11852  try{
11853    const r=await api('POST','/api/polymarket/subscribe');
11854    const d=await r.json();
11855    if(!r.ok){toast(d.detail||'Failed','err');return;}
11856    toast(d.polymarket_alerts?'Alerts enabled':'Alerts disabled','ok');
11857  }catch(e){toast('Error','err');}
11858}
11859
11860// ── TAX SESSIONS (admin) ──
11861async function loadTaxSessions(){
11862  const list=$('tax-sessions-list');
11863  try{
11864    const [cfgR,sessR]=await Promise.all([
11865      api('GET','/api/admin/tax/config'),
11866      api('GET','/api/admin/tax/sessions'),
11867    ]);
11868    if(cfgR.ok){
11869      const cfg=await cfgR.json();
11870      const inp=$('tax-cpa-emails');
11871      if(inp)inp.value=cfg.cpa_emails||'';
11872      const qrInp=$('tax-yappy-qr-url');
11873      if(qrInp)qrInp.value=cfg.yappy_qr_url||'';
11874    }
11875    const d=await sessR.json();
11876    if(!sessR.ok){list.innerHTML=`<p style="padding:16px;font-size:.8rem;color:var(--red)">${d.detail||'Failed to load'}</p>`;return;}
11877    const sessions = d.sessions || [];
11878    const badge=$('tax-paid-badge');
11879    if(badge) badge.textContent=`${d.paid_count||0} paid / ${d.total||0} total`;
11880    window._adminTaxSessionsCache = sessions;
11881    if(!sessions.length){list.innerHTML='<div style="padding:20px;text-align:center;color:var(--muted);font-size:.8rem">No tax sessions yet.</div>';return;}
11882    list.innerHTML=`<table style="width:100%;border-collapse:collapse;font-size:.78rem">
11883      <thead><tr style="border-bottom:1px solid var(--border)">
11884        <th style="padding:10px 14px;text-align:left;color:var(--muted);font-weight:500">User</th>
11885        <th style="padding:10px 8px;text-align:left;color:var(--muted);font-weight:500">Label</th>
11886        <th style="padding:10px 8px;text-align:left;color:var(--muted);font-weight:500">Period</th>
11887        <th style="padding:10px 8px;text-align:left;color:var(--muted);font-weight:500">Date</th>
11888        <th style="padding:10px 8px;text-align:center;color:var(--muted);font-weight:500">Result</th>
11889        <th style="padding:10px 8px;text-align:center;color:var(--muted);font-weight:500">Paid</th>
11890        <th style="padding:10px 14px"></th>
11891      </tr></thead>
11892      <tbody>${sessions.map(s=>`<tr style="border-bottom:1px solid var(--border20);${s.is_paid?'background:rgba(0,230,118,.03)':''}">
11893        <td style="padding:9px 14px">
11894          <div style="font-weight:500">${s.user_name||''}</div>
11895          <div style="font-size:.7rem;color:var(--muted)">${s.user_email}</div>
11896        </td>
11897        <td style="padding:9px 8px">${s.label||'—'}</td>
11898        <td style="padding:9px 8px">${s.period||'—'}</td>
11899        <td style="padding:9px 8px;color:var(--muted)">${s.created_at?new Date(s.created_at).toLocaleDateString():''}</td>
11900        <td style="padding:9px 8px;text-align:center">${s.has_result?'<span style="color:var(--green)">✓</span>':'<span style="color:var(--muted)">—</span>'}</td>
11901        <td style="padding:9px 8px;text-align:center">${s.is_paid?'<span style="color:var(--green);font-size:.7rem">✓ Paid</span>':'<span style="color:var(--muted);font-size:.7rem">—</span>'}</td>
11902        <td style="padding:9px 14px;text-align:right;white-space:nowrap">
11903          <button class="btn btn-ghost" style="font-size:.68rem;padding:3px 9px" onclick="viewTaxSession('${s.id}')">View</button>
11904          <button class="btn btn-ghost" style="font-size:.68rem;padding:3px 9px;margin-left:4px;${s.is_paid?'color:var(--green);border-color:var(--green)':''}" onclick="toggleTaxPaid('${s.id}')">${s.is_paid?'✓ Paid':'Mark Paid'}</button>
11905        </td>
11906      </tr>`).join('')}</tbody>
11907    </table>`;
11908  }catch(e){if(list)list.innerHTML=`<p style="padding:16px;font-size:.8rem;color:var(--red)">Error: ${e.message}</p>`;}
11909}
11910
11911async function toggleTaxPaid(id){
11912  try{
11913    await api('PATCH',`/api/admin/tax/sessions/${id}/paid`);
11914    loadTaxSessions();
11915  }catch(e){}
11916}
11917
11918async function toggleTaxPaidModal(id){
11919  try{
11920    const r=await api('PATCH',`/api/admin/tax/sessions/${id}/paid`);
11921    const d=await r.json();
11922    const isPaid=d.is_paid;
11923    const badge=document.getElementById('modal-paid-badge');
11924    const btn=document.getElementById('modal-paid-btn');
11925    if(badge){badge.textContent=isPaid?'✓ Paid':'Unpaid';badge.style.color=isPaid?'var(--green)':'var(--muted)';}
11926    if(btn){btn.textContent=isPaid?'Mark Unpaid':'Mark Paid';btn.style.color=isPaid?'var(--green)':'';btn.style.borderColor=isPaid?'var(--green)':'';}
11927    loadTaxSessions();
11928  }catch(e){}
11929}
11930
11931async function saveTaxConfig(){
11932  const inp=$('tax-cpa-emails');
11933  const result=$('tax-config-result');
11934  if(!inp||!inp.value.trim()){if(result)result.innerHTML='<span style="color:var(--red)">Enter at least one email</span>';return;}
11935  const qrUrl=($('tax-yappy-qr-url')||{}).value||'';
11936  try{
11937    const r=await api('PUT','/api/admin/tax/config',{cpa_emails:inp.value.trim(), yappy_qr_url:qrUrl.trim()||null});
11938    const d=await r.json();
11939    if(!r.ok){if(result)result.innerHTML=`<span style="color:var(--red)">${d.detail||'Failed'}</span>`;return;}
11940    if(result)result.innerHTML='<span style="color:var(--green)">Saved ✓</span>';
11941    setTimeout(()=>{if(result)result.innerHTML='';},3000);
11942  }catch(e){if(result)result.innerHTML=`<span style="color:var(--red)">${e.message}</span>`;}
11943}
11944
11945async function viewTaxSession(id){
11946  const overlay=$('tax-modal-overlay');
11947  const body=$('tax-modal-body');
11948  if(!overlay||!body)return;
11949  body.innerHTML='<p style="text-align:center;color:var(--muted)">Loading...</p>';
11950  overlay.style.display='block';
11951  try{
11952    const r=await api('GET',`/api/admin/tax/sessions/${id}`);
11953    const s=await r.json();
11954    if(!r.ok){body.innerHTML=`<p style="color:var(--red)">${s.detail||'Failed to load'}</p>`;return;}
11955
11956    const result=s.result||[];
11957    const sections={};
11958    result.forEach(e=>{
11959      const sec=e.section||'OTHER';
11960      if(!sections[sec]){sections[sec]={total:0,items:[]};}
11961      sections[sec].total+=e.amount||0;
11962      sections[sec].items.push(e);
11963    });
11964
11965    const summaryRows=Object.entries(sections).map(([sec,data])=>
11966      `<tr style="border-bottom:1px solid var(--border20)">
11967        <td style="padding:6px 0;color:var(--muted);font-size:.78rem">${sec}</td>
11968        <td style="padding:6px 0;font-weight:600;text-align:right">$${data.total.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</td>
11969      </tr>`
11970    ).join('');
11971
11972    const itemRows=result.map(e=>
11973      `<tr style="border-bottom:1px solid var(--border20)">
11974        <td style="padding:5px 6px;color:var(--muted);font-family:var(--mono);font-size:.72rem">${e.line}</td>
11975        <td style="padding:5px 6px;font-size:.75rem">${e.description}</td>
11976        <td style="padding:5px 6px;font-size:.72rem;color:var(--teal)">${e.section}</td>
11977        <td style="padding:5px 6px;text-align:right;font-family:var(--mono);font-size:.75rem">$${(e.amount||0).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</td>
11978      </tr>`
11979    ).join('');
11980
11981    // Build compare options from same-user sessions in the already-loaded list
11982    const _adminTaxSessions = window._adminTaxSessionsCache || [];
11983    const comparableOthers = _adminTaxSessions.filter(x=>x.id!==id && x.has_result && x.user_email===s.user_email);
11984    const compareSelect = comparableOthers.length ? `
11985      <div style="border-top:1px solid var(--border);margin-top:20px;padding-top:16px">
11986        <p style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:8px">COMPARE WITH ANOTHER PERIOD</p>
11987        <div style="display:flex;gap:8px;align-items:center">
11988          <select id="tax-compare-select" style="flex:1;background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:7px 10px;color:var(--text);font-size:.78rem">
11989            <option value="">— select period —</option>
11990            ${comparableOthers.map(x=>`<option value="${x.id}">${x.label||'Untitled'} · ${x.period||''}</option>`).join('')}
11991          </select>
11992          <button class="btn btn-ghost" style="font-size:.75rem" onclick="adminCompareTaxSessions('${id}')">Compare →</button>
11993        </div>
11994        <div id="tax-admin-compare-wrap" style="margin-top:14px"></div>
11995      </div>` : '';
11996
11997    body.innerHTML=`
11998      <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px">
11999        <h3 style="margin:0">Tax Session — ${s.label||'Untitled'}</h3>
12000        <div style="display:flex;align-items:center;gap:8px">
12001          <span id="modal-paid-badge" style="font-size:.72rem;${s.is_paid?'color:var(--green)':'color:var(--muted)'}">
12002            ${s.is_paid?'✓ Paid':'Unpaid'}
12003          </span>
12004          <button class="btn btn-ghost" id="modal-paid-btn" style="font-size:.72rem;padding:4px 10px;${s.is_paid?'color:var(--green);border-color:var(--green)':''}"
12005            onclick="toggleTaxPaidModal('${id}')">
12006            ${s.is_paid?'Mark Unpaid':'Mark Paid'}
12007          </button>
12008        </div>
12009      </div>
12010      <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;font-size:.78rem">
12011        <div><span style="color:var(--muted)">User:</span> ${s.user_name||''} &lt;${s.user_email}&gt;</div>
12012        <div><span style="color:var(--muted)">Period:</span> ${s.period||'—'}</div>
12013        <div><span style="color:var(--muted)">Date:</span> ${s.created_at?new Date(s.created_at).toLocaleString():''}</div>
12014        <div><span style="color:var(--muted)">Documents:</span> ${s.documents.length}</div>
12015      </div>
12016
12017      ${summaryRows?`<p style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-top:16px">SUMMARY</p>
12018      <table style="width:100%;border-collapse:collapse;margin-bottom:16px">${summaryRows}</table>`:''}
12019
12020      ${s.documents&&s.documents.length?`
12021      <div style="margin-top:16px;border-top:1px solid var(--border);padding-top:12px">
12022        <p style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:8px">SUPPORTING DOCUMENTS (${s.documents.length})</p>
12023        <div style="display:flex;flex-wrap:wrap;gap:10px">
12024          ${s.documents.map(url=>{
12025            const isPdf=url.toLowerCase().includes('.pdf')||url.includes('/raw/');
12026            return isPdf
12027              ? `<a href="${url}" target="_blank" style="display:inline-flex;align-items:center;justify-content:center;width:72px;height:72px;border:1px solid var(--border);border-radius:6px;background:var(--bg2);text-decoration:none;font-size:1.6rem">📄</a>`
12028              : `<a href="${url}" target="_blank"><img src="${url}" style="width:72px;height:72px;object-fit:cover;border-radius:6px;border:1px solid var(--border)"></a>`;
12029          }).join('')}
12030        </div>
12031      </div>` : ''}
12032
12033      ${itemRows?`<p style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-top:16px">CATEGORIZED ITEMS</p>
12034      <div style="overflow-x:auto">
12035      <table style="width:100%;border-collapse:collapse;font-size:.78rem">
12036        <thead><tr style="border-bottom:1px solid var(--border)">
12037          <th style="padding:5px 6px;text-align:left;color:var(--muted);font-weight:500">Line</th>
12038          <th style="padding:5px 6px;text-align:left;color:var(--muted);font-weight:500">Form Description</th>
12039          <th style="padding:5px 6px;text-align:left;color:var(--muted);font-weight:500">Section</th>
12040          <th style="padding:5px 6px;text-align:right;color:var(--muted);font-weight:500">Amount</th>
12041        </tr></thead>
12042        <tbody>${itemRows}</tbody>
12043      </table></div>`:'<p style="color:var(--muted);font-size:.8rem">No categorized result yet.</p>'}
12044
12045      ${compareSelect}
12046    `;
12047    // Store current session data for comparison
12048    window._adminTaxSessionA = s;
12049  }catch(e){body.innerHTML=`<p style="color:var(--red)">${e.message}</p>`;}
12050}
12051
12052async function adminCompareTaxSessions(idA){
12053  const sel=document.getElementById('tax-compare-select');
12054  const wrap=document.getElementById('tax-admin-compare-wrap');
12055  if(!sel||!sel.value||!wrap)return;
12056  wrap.innerHTML='<p style="font-size:.78rem;color:var(--muted)">Loading...</p>';
12057  try{
12058    const r=await api('GET',`/api/admin/tax/sessions/${sel.value}`);
12059    const sB=await r.json();
12060    if(!r.ok){wrap.innerHTML=`<p style="color:var(--red)">${sB.detail}</p>`;return;}
12061    const sA=window._adminTaxSessionA;
12062    wrap.innerHTML = buildAdminCompareHTML(sA, sB);
12063  }catch(e){wrap.innerHTML=`<p style="color:var(--red)">${e.message}</p>`;}
12064}
12065
12066function buildAdminCompareHTML(a, b){
12067  const mapA={}, mapB={};
12068  (a.result||[]).forEach(e=>mapA[e.line]=e);
12069  (b.result||[]).forEach(e=>mapB[e.line]=e);
12070  const allLines=[...new Set([...Object.keys(mapA),...Object.keys(mapB)])].map(Number).sort((x,y)=>x-y);
12071  const labelA=a.label||a.period||'A', labelB=b.label||b.period||'B';
12072
12073  const rows=allLines.map(line=>{
12074    const ea=mapA[line],eb=mapB[line];
12075    const desc=(eb||ea).description, sec=(eb||ea).section;
12076    const amtA=ea?ea.amount:0, amtB=eb?eb.amount:0, delta=amtB-amtA;
12077    const pct=amtA?((delta/amtA)*100).toFixed(1)+'%':(amtB?'new':'—');
12078    const isGood=(sec==='INGRESOS'&&delta>0)||(sec!=='INGRESOS'&&delta<0);
12079    const dc=delta===0?'var(--muted)':isGood?'var(--green)':'var(--red)';
12080    const bg=delta===0?'':isGood?'background:rgba(0,230,118,.04)':'background:rgba(255,68,68,.04)';
12081    const fmt=n=>n?`$${n.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}`:'—';
12082    return `<tr style="border-bottom:1px solid rgba(255,255,255,.03);${bg}">
12083      <td style="padding:5px 4px;color:var(--muted);font-family:monospace;font-size:.7rem">L${line}</td>
12084      <td style="padding:5px 4px;font-size:.73rem">${desc}</td>
12085      <td style="padding:5px 4px;font-size:.68rem;color:var(--text2)">${sec}</td>
12086      <td style="padding:5px 8px;text-align:right;font-family:monospace;font-size:.73rem">${fmt(amtA)}</td>
12087      <td style="padding:5px 8px;text-align:right;font-family:monospace;font-size:.73rem">${fmt(amtB)}</td>
12088      <td style="padding:5px 8px;text-align:right;font-family:monospace;font-size:.73rem;color:${dc}">${delta>=0?'+':''}${fmt(delta)}</td>
12089      <td style="padding:5px 4px;text-align:right;font-size:.7rem;color:${dc}">${pct}</td>
12090    </tr>`;
12091  }).join('');
12092
12093  return `<p style="font-size:.6rem;color:var(--muted);font-family:var(--mono);letter-spacing:2px;margin-bottom:8px">PERIOD COMPARISON: ${labelA} vs ${labelB}</p>
12094  <div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:.76rem">
12095    <thead><tr style="border-bottom:1px solid var(--border)">
12096      <th style="padding:5px 4px;text-align:left;color:var(--muted);font-weight:400">Line</th>
12097      <th style="padding:5px 4px;text-align:left;color:var(--muted);font-weight:400">Description</th>
12098      <th style="padding:5px 4px;text-align:left;color:var(--muted);font-weight:400">Section</th>
12099      <th style="padding:5px 8px;text-align:right;color:var(--teal);font-weight:400">${labelA}</th>
12100      <th style="padding:5px 8px;text-align:right;color:var(--green);font-weight:400">${labelB}</th>
12101      <th style="padding:5px 8px;text-align:right;color:var(--muted);font-weight:400">Δ</th>
12102      <th style="padding:5px 4px;text-align:right;color:var(--muted);font-weight:400">%</th>
12103    </tr></thead>
12104    <tbody>${rows}</tbody>
12105  </table></div>`;
12106}
12107
12108function closeTaxModal(){
12109  const overlay=$('tax-modal-overlay');
12110  if(overlay)overlay.style.display='none';
12111}
12112
12113// ─── Chart lightbox ───────────────────────────────────────────
12114function showChartModal(idx){
12115  const c=lastScanCharts[idx];
12116  if(!c)return;
12117  _modalChart=c;
12118  const src=`data:image/png;base64,${c.chart_b64}`;
12119  document.getElementById('chart-modal-img').src=src;
12120  const dl=document.getElementById('chart-modal-dl');
12121  dl.href=src;dl.download=`${c.ticker}_chart.png`;
12122  const m=document.getElementById('chart-modal');
12123  m.style.display='flex';
12124  document.body.style.overflow='hidden';
12125}
12126function closeChartModal(){
12127  document.getElementById('chart-modal').style.display='none';
12128  document.body.style.overflow='';
12129}
12130function openImgPreview(url){
12131  $('img-preview-src').src=url;
12132  $('img-preview-dl').href=url;
12133  $('img-preview-modal').style.display='flex';
12134  document.body.style.overflow='hidden';
12135}
12136function closeImgPreview(){
12137  $('img-preview-modal').style.display='none';
12138  $('img-preview-src').src='';
12139  document.body.style.overflow='';
12140}
12141function toggleChartSelect(idx){
12142  const thumb=document.getElementById(`cthumb-${idx}`);
12143  const chk=document.getElementById(`cchk-${idx}`);
12144  if(_selectedChartIdxs.has(idx)){
12145    _selectedChartIdxs.delete(idx);
12146    thumb.style.outline='';
12147    chk.textContent='○';chk.style.borderColor='#888';
12148  } else {
12149    _selectedChartIdxs.add(idx);
12150    thumb.style.outline='2px solid #00d4e8';
12151    chk.textContent='✓';chk.style.borderColor='#00d4e8';
12152  }
12153  const bar=$('chart-selection-bar');
12154  const n=_selectedChartIdxs.size;
12155  if(n===0){bar.style.display='none';}
12156  else{bar.style.display='flex';$('chart-sel-count').textContent=`${n} chart${n>1?'s':''} selected`;}
12157}
12158function clearChartSelection(){
12159  _selectedChartIdxs.forEach(i=>{
12160    const t=document.getElementById(`cthumb-${i}`);
12161    const c=document.getElementById(`cchk-${i}`);
12162    if(t)t.style.outline='';
12163    if(c){c.textContent='○';c.style.borderColor='#888';}
12164  });
12165  _selectedChartIdxs.clear();
12166  $('chart-selection-bar').style.display='none';
12167}
12168function sendSelectedToCollage(){
12169  if(!_selectedChartIdxs.size)return;
12170  const dt=new DataTransfer();
12171  _selectedChartIdxs.forEach(i=>{
12172    const c=lastScanCharts[i];
12173    dt.items.add(new File([b64toBlob(c.chart_b64)],`${c.ticker}_chart.png`,{type:'image/png'}));
12174  });
12175  $('collage-files').files=dt.files;
12176  const n=dt.files.length;
12177  clearChartSelection();go('collage');setCollageStep(2);
12178  toast(`${n} chart${n>1?'s':''} sent to Collage`,'ok');
12179}
12180function sendSelectedToCarousel(){
12181  if(!_selectedChartIdxs.size)return;
12182  const dt=new DataTransfer();
12183  [..._selectedChartIdxs].slice(0,4).forEach(i=>{
12184    const c=lastScanCharts[i];
12185    dt.items.add(new File([b64toBlob(c.chart_b64)],`${c.ticker}_chart.png`,{type:'image/png'}));
12186  });
12187  $('carousel-files').files=dt.files;
12188  const n=dt.files.length;
12189  clearChartSelection();go('carousel');setCarouselStep(2);
12190  toast(`${n} chart${n>1?'s':''} sent to Carousel`,'ok');
12191}
12192function sendModalToCollage(){
12193  if(!_modalChart)return;
12194  const file=new File([b64toBlob(_modalChart.chart_b64)],`${_modalChart.ticker}_chart.png`,{type:'image/png'});
12195  const dt=new DataTransfer();dt.items.add(file);
12196  $('collage-files').files=dt.files;
12197  closeChartModal();go('collage');setCollageStep(2);
12198  toast(`${_modalChart.ticker} sent to Collage`,'ok');
12199}
12200function sendModalToCarousel(){
12201  if(!_modalChart)return;
12202  const file=new File([b64toBlob(_modalChart.chart_b64)],`${_modalChart.ticker}_chart.png`,{type:'image/png'});
12203  const dt=new DataTransfer();dt.items.add(file);
12204  $('carousel-files').files=dt.files;
12205  closeChartModal();go('carousel');setCarouselStep(2);
12206  toast(`${_modalChart.ticker} sent to Carousel`,'ok');
12207}
12208document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeChartModal();closeArticleModal();}});
12209
12210// ── Article Accordion ──────────────────────────────────────────────────────
12211function toggleArticle(id){
12212  const body=document.getElementById('art-body-'+id);
12213  const preview=document.getElementById('art-preview-'+id);
12214  const lbl=document.getElementById('art-toggle-'+id);
12215  if(!body)return;
12216  const isOpen=body.style.display==='block';
12217  if(isOpen){
12218    body.style.display='none';
12219    if(preview)preview.style.display='';
12220    if(lbl)lbl.textContent='READ ↓';
12221    return;
12222  }
12223  if(!body.children.length){
12224    const n=_newsItems.find(x=>String(x.id)===String(id));
12225    if(!n||!n.summary)return;
12226    const f=document.createElement('iframe');
12227    f.style.cssText='width:100%;min-height:500px;border:none;display:block';
12228    f.onload=()=>{try{f.style.height=(f.contentDocument.body.scrollHeight+20)+'px';}catch(e){}};
12229    body.appendChild(f);
12230    f.srcdoc=n.summary;
12231  }
12232  if(preview)preview.style.display='none';
12233  body.style.display='block';
12234  if(lbl)lbl.textContent='READ ↑';
12235  body.scrollIntoView({behavior:'smooth',block:'nearest'});
12236}
12237
12238// ── Article Reader (legacy, unused) ────────────────────────────────────────
12239function openArticle(id){
12240  const n=_newsItems.find(x=>String(x.id)===String(id));
12241  if(!n)return;
12242  const badge=NEWS_TYPE_BADGE[n.content_type]||'badge-muted';
12243  const icon=NEWS_TYPE_ICON[n.content_type]||'📄';
12244  const label=(n.content_type||'news').replace('_',' ').toUpperCase();
12245  const date=n.created_at?new Date(n.created_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}):'';
12246  $('article-reader-meta').innerHTML=`<span class="badge ${badge}" style="font-size:.58rem">${icon} ${label}</span>${date?`<span style="font-size:.72rem;color:var(--muted)">${date}</span>`:''}`;
12247  $('article-reader-title').textContent=n.title||'';
12248  const _arBody=$('article-reader-body');
12249  _arBody.innerHTML='';
12250  if(n.summary&&n.summary.includes('<')){
12251    const _arFrame=document.createElement('iframe');
12252    _arFrame.style.cssText='width:100%;min-height:500px;border:none;border-radius:6px;display:block';
12253    _arFrame.onload=()=>{try{_arFrame.style.height=(_arFrame.contentDocument.body.scrollHeight+40)+'px';}catch(e){}};
12254    _arBody.appendChild(_arFrame);
12255    _arFrame.srcdoc=n.summary;
12256  } else {
12257    _arBody.innerHTML=n.summary||'<p style="color:var(--muted)">No content.</p>';
12258  }
12259  const acts=[
12260    n.caption?`<button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="copyNewsCaption('${n.id}')">📋 Copy</button>`:'',
12261    `<button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px" onclick="closeArticleModal();sendToPost('${n.id}')">✏️ Edit & Post</button>`,
12262    `<button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px;color:#1DA1F2" onclick="postNewsItem('${n.id}','x')" id="ar-post-x-${n.id}">🐦 Post X</button>`,
12263    `<button class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px;color:#E1306C" onclick="postNewsItem('${n.id}','instagram')" id="ar-post-ig-${n.id}">📸 Post IG</button>`,
12264    n.source_url?`<a href="${n.source_url}" target="_blank" rel="noopener" class="btn btn-ghost" style="font-size:.7rem;padding:5px 10px;text-decoration:none">🔗 Source</a>`:''
12265  ].filter(Boolean).join('');
12266  $('article-reader-actions').innerHTML=acts;
12267  $('article-reader-modal').style.display='block';
12268  document.body.style.overflow='hidden';
12269}
12270function closeArticleModal(){
12271  $('article-reader-modal').style.display='none';
12272  document.body.style.overflow='';
12273}
12274
12275// ── Guide accordion ────────────────────────────────────────────────────────
12276function toggleGuide(id){
12277  const body=document.getElementById(id);
12278  const arrow=document.getElementById(id+'-arrow');
12279  const open=body.style.display==='block';
12280  body.style.display=open?'none':'block';
12281  if(arrow)arrow.classList.toggle('open',!open);
12282}
12283
12284// ── Admin panel collapsible sections ───────────────────────────────────────
12285function openBriefModal(id){
12286  const modal = document.getElementById('brief-modal');
12287  const frame = document.getElementById('brief-modal-frame');
12288  const titleEl = document.getElementById('brief-modal-title');
12289  if(!modal||!frame) return;
12290  const item = _newsItems.find(n=>n.id===id);
12291  titleEl.textContent = (item&&item.title) || 'Daily Intelligence Brief';
12292  frame.srcdoc = (item&&item.html_content) || '<p style="padding:20px;font-family:sans-serif">No content available.</p>';
12293  modal.style.display = 'flex';
12294  document.body.style.overflow = 'hidden';
12295}
12296function closeBriefModal(){
12297  const modal = document.getElementById('brief-modal');
12298  if(modal) modal.style.display = 'none';
12299  document.body.style.overflow = '';
12300}
12301
12302function toggleAdminSection(id){
12303  const body=document.getElementById(id);
12304  const arrow=document.getElementById(id+'-arrow');
12305  if(!body)return;
12306  const collapsed=body.style.display==='none';
12307  body.style.display=collapsed?'':'none';
12308  if(arrow)arrow.textContent=collapsed?'▾':'▸';
12309  if(collapsed && id==='as-opts-perf') loadOptionsPerformance();
12310  if(collapsed && id==='as-scan-perf') loadScannerPerformance();
12311  // Lazy — the newsroom queue costs two API calls, and the section is
12312  // collapsed by default, so don't pay for it on every admin page load.
12313  if(collapsed && id==='as-newsroom')  loadNewsroom();
12314}
12315
12316async function loadOptionsPerformance(){
12317  const weeks  = (document.getElementById('opts-perf-weeks')||{value:'12'}).value || 12;
12318  const ticker = ((document.getElementById('opts-perf-ticker')||{value:''}).value || '').trim();
12319  const token  = localStorage.getItem('factoz_token');
12320  if(!token) return;
12321
12322  let url = `/api/admin/options/performance?weeks=${weeks}`;
12323  if(ticker) url += `&ticker=${encodeURIComponent(ticker)}`;
12324
12325  let data;
12326  try {
12327    const r = await fetch(url,{headers:{Authorization:'Bearer '+token}});
12328    if(!r.ok) throw new Error(await r.text());
12329    data = await r.json();
12330  } catch(e) {
12331    console.error('opts-perf load failed',e);
12332    return;
12333  }
12334
12335  // Summary
12336  const wr = data.overall_win_rate;
12337  document.getElementById('opts-perf-winrate').textContent  = wr != null ? wr+'%' : 'N/A';
12338  document.getElementById('opts-perf-winrate').style.color  = wr==null?'var(--muted)':wr>=60?'var(--green)':wr>=40?'var(--yellow)':'var(--red)';
12339  document.getElementById('opts-perf-total').textContent    = data.total_archived ?? '—';
12340  document.getElementById('opts-perf-resolved').textContent = data.total_resolved ?? '—';
12341
12342  // Top tickers chips
12343  const tc = document.getElementById('opts-perf-tickers');
12344  tc.innerHTML = (data.top_tickers||[]).slice(0,6).map(t=>
12345    `<span style="background:var(--bg4);border:1px solid var(--border);border-radius:4px;padding:2px 8px;font-size:.65rem;color:var(--teal);cursor:pointer"
12346      onclick="document.getElementById('opts-perf-ticker').value='${t.ticker}';loadOptionsPerformance()">${t.ticker} <span style="color:var(--muted)">${t.count}</span></span>`
12347  ).join('');
12348
12349  // Weekly table
12350  const wb = document.getElementById('opts-perf-weekly-body');
12351  const weekly = data.weekly || [];
12352  if(!weekly.length){
12353    wb.innerHTML='<tr><td colspan="7" style="padding:12px 8px;color:var(--muted);font-size:.72rem">No weekly data yet.</td></tr>';
12354  } else {
12355    wb.innerHTML = weekly.map(w=>{
12356      const wr = w.win_rate;
12357      const wrColor = wr==null?'var(--muted)':wr>=60?'var(--green)':wr>=40?'var(--yellow)':'var(--red)';
12358      return `<tr style="border-bottom:1px solid var(--border)">
12359        <td style="padding:6px 8px;color:var(--text)">${w.week_label||w.week_start}</td>
12360        <td style="padding:6px 8px;text-align:right;color:var(--text)">${w.total}</td>
12361        <td style="padding:6px 8px;text-align:right;color:var(--text)">${w.resolved}</td>
12362        <td style="padding:6px 8px;text-align:right;color:var(--green)">${w.win}</td>
12363        <td style="padding:6px 8px;text-align:right;color:${wrColor};font-weight:700">${wr!=null?wr+'%':'—'}</td>
12364        <td style="padding:6px 8px;text-align:right;color:var(--text)">${w.call_itm}</td>
12365        <td style="padding:6px 8px;text-align:right;color:var(--text)">${w.put_otm}</td>
12366      </tr>`;
12367    }).join('');
12368  }
12369
12370  // Contracts table
12371  const cb   = document.getElementById('opts-perf-contracts-body');
12372  const emp  = document.getElementById('opts-perf-empty');
12373  const contracts = data.contracts || [];
12374  if(!contracts.length){
12375    cb.innerHTML='';
12376    if(emp) emp.style.display='';
12377  } else {
12378    if(emp) emp.style.display='none';
12379    cb.innerHTML = contracts.map(c=>{
12380      const res = c.resolution_type;
12381      let resultLabel='', resultColor='var(--muted)';
12382      if(res==='expired_itm'){ resultLabel='ITM ✓'; resultColor='var(--green)'; }
12383      else if(res==='expired_otm' && c.option_type==='put'){ resultLabel='PUT OTM ✓'; resultColor='var(--green)'; }
12384      else if(res==='expired_otm'){ resultLabel='OTM ✗'; resultColor='var(--red)'; }
12385      else if(!res){ resultLabel='Pending'; resultColor='var(--muted)'; }
12386      const pnl = c.pnl_pct != null ? (c.pnl_pct>0?'+':'')+c.pnl_pct.toFixed(1)+'%' : '—';
12387      const pnlColor = c.pnl_pct==null?'var(--muted)':c.pnl_pct>0?'var(--green)':'var(--red)';
12388      const premium = c.premium_usd ? '$'+(c.premium_usd>=1e6?(c.premium_usd/1e6).toFixed(2)+'M':(c.premium_usd/1e3).toFixed(0)+'K') : '—';
12389      const typeColor = c.option_type==='call'?'var(--green)':'var(--red)';
12390      return `<tr style="border-bottom:1px solid var(--border)">
12391        <td style="padding:5px 8px;color:var(--muted);font-size:.68rem">${c.week_label||'—'}</td>
12392        <td style="padding:5px 8px;font-weight:700;color:var(--teal)">${c.ticker}</td>
12393        <td style="padding:5px 8px;text-transform:uppercase;color:${typeColor};font-weight:700;font-size:.68rem">${c.option_type}</td>
12394        <td style="padding:5px 8px;text-align:right">$${c.strike}</td>
12395        <td style="padding:5px 8px;color:var(--muted)">${c.expiry||'—'}</td>
12396        <td style="padding:5px 8px;text-align:right;color:var(--yellow)">${premium}</td>
12397        <td style="padding:5px 8px;text-align:right">${c.vol_oi_ratio!=null?c.vol_oi_ratio.toFixed(1)+'x':'—'}</td>
12398        <td style="padding:5px 8px;font-weight:700;color:${resultColor}">${resultLabel||'—'}</td>
12399        <td style="padding:5px 8px;text-align:right;color:${pnlColor}">${pnl}</td>
12400        <td style="padding:5px 8px;color:var(--muted);font-size:.68rem">${c.flagged_at||'—'}</td>
12401      </tr>`;
12402    }).join('');
12403  }
12404}
12405
12406async function loadScannerPerformance(){
12407  const days     = (document.getElementById('scan-perf-days')||{value:'180'}).value || 180;
12408  const strategy = (document.getElementById('scan-perf-strategy')||{value:''}).value || '';
12409  const ticker   = ((document.getElementById('scan-perf-ticker')||{value:''}).value || '').trim();
12410  const token    = localStorage.getItem('factoz_token');
12411  if(!token) return;
12412
12413  let url = `/api/admin/scanner/performance?days=${days}`;
12414  if(strategy) url += `&strategy=${encodeURIComponent(strategy)}`;
12415  if(ticker)   url += `&ticker=${encodeURIComponent(ticker)}`;
12416
12417  let data;
12418  try {
12419    const r = await fetch(url,{headers:{Authorization:'Bearer '+token}});
12420    if(!r.ok) throw new Error(await r.text());
12421    data = await r.json();
12422  } catch(e) {
12423    console.error('scan-perf load failed',e);
12424    return;
12425  }
12426
12427  const STRATEGY_LABELS = {rsi_extremes:'RSI Extremes (Pre-Market)', premarket_movers:'Pre-Market Movers (retired)', breakout_52wk:'52-Week Breakout', gap_and_hold:'Gap & Hold'};
12428
12429  // Summary — lead with 30d overall
12430  const d30 = (data.overall||{})['30d'] || {};
12431  const wr = d30.win_rate;
12432  document.getElementById('scan-perf-winrate').textContent = wr != null ? wr+'%' : 'N/A';
12433  document.getElementById('scan-perf-winrate').style.color = wr==null?'var(--muted)':wr>=55?'var(--green)':wr>=45?'var(--yellow)':'var(--red)';
12434  document.getElementById('scan-perf-total').textContent    = data.total_picks ?? '—';
12435  document.getElementById('scan-perf-resolved').textContent = d30.resolved ?? '—';
12436
12437  // Horizon breakdown table — one row per strategy per horizon
12438  const hb = document.getElementById('scan-perf-horizon-body');
12439  const byStrategy = data.by_strategy || {};
12440  const strategies = Object.keys(byStrategy);
12441  if(!strategies.length){
12442    hb.innerHTML = '<tr><td colspan="7" style="padding:12px 8px;color:var(--muted);font-size:.72rem">No scan picks yet.</td></tr>';
12443  } else {
12444    const rowsHtml = [];
12445    for(const strat of strategies){
12446      for(const h of ['30d','60d','90d','120d']){
12447        const b = byStrategy[strat][h] || {};
12448        if(!b.resolved && !b.pending) continue;
12449        const wrColor = b.win_rate==null?'var(--muted)':b.win_rate>=55?'var(--green)':b.win_rate>=45?'var(--yellow)':'var(--red)';
12450        const ci = (b.ci_low!=null && b.ci_high!=null) ? `${b.ci_low}–${b.ci_high}%` : '—';
12451        const med = b.median_excess_return;
12452        const medColor = med==null?'var(--muted)':med>0?'var(--green)':'var(--red)';
12453        rowsHtml.push(`<tr style="border-bottom:1px solid var(--border)">
12454          <td style="padding:6px 8px;color:var(--text)">${STRATEGY_LABELS[strat]||strat}</td>
12455          <td style="padding:6px 8px;color:var(--muted)">${h}</td>
12456          <td style="padding:6px 8px;text-align:right;color:var(--text)">${b.resolved ?? 0}</td>
12457          <td style="padding:6px 8px;text-align:right;color:var(--muted)">${b.pending ?? 0}</td>
12458          <td style="padding:6px 8px;text-align:right;color:${wrColor};font-weight:700">${b.win_rate!=null?b.win_rate+'%':'—'}</td>
12459          <td style="padding:6px 8px;text-align:right;color:var(--muted);font-size:.68rem">${ci}</td>
12460          <td style="padding:6px 8px;text-align:right;color:${medColor}">${med!=null?(med>0?'+':'')+med+'%':'—'}</td>
12461        </tr>`);
12462      }
12463    }
12464    hb.innerHTML = rowsHtml.join('') || '<tr><td colspan="7" style="padding:12px 8px;color:var(--muted);font-size:.72rem">No resolved or pending picks in this window.</td></tr>';
12465  }
12466
12467  // Recent picks table
12468  const pb  = document.getElementById('scan-perf-picks-body');
12469  const emp = document.getElementById('scan-perf-empty');
12470  const picks = data.picks || [];
12471  if(!picks.length){
12472    pb.innerHTML = '';
12473    if(emp) emp.style.display = '';
12474  } else {
12475    if(emp) emp.style.display = 'none';
12476    pb.innerHTML = picks.slice(0,100).map(p=>{
12477      const excess = p.excess_return_30d;
12478      const excessColor = excess==null?'var(--muted)':excess>0?'var(--green)':'var(--red)';
12479      const dirColor = p.direction==='BULLISH'?'var(--green)':'var(--red)';
12480      return `<tr style="border-bottom:1px solid var(--border)">
12481        <td style="padding:5px 8px;font-weight:700;color:var(--teal)">${p.ticker}</td>
12482        <td style="padding:5px 8px;color:var(--muted);font-size:.68rem">${STRATEGY_LABELS[p.strategy]||p.strategy}</td>
12483        <td style="padding:5px 8px;text-transform:uppercase;color:${dirColor};font-weight:700;font-size:.68rem">${p.direction}</td>
12484        <td style="padding:5px 8px;color:var(--muted)">${p.call_date}</td>
12485        <td style="padding:5px 8px;text-align:right">$${p.entry_price}</td>
12486        <td style="padding:5px 8px;text-align:right;color:${excessColor}">${excess!=null?(excess>0?'+':'')+excess+'%':'pending'}</td>
12487      </tr>`;
12488    }).join('');
12489  }
12490}
12491
12492// ── AI Financial Advisor Chat ──────────────────────────────────────────────
12493function chipPrompt(template){
12494  const ticker = lastScanCharts.length ? lastScanCharts[0].ticker : 'TICKER';
12495  const msg = template.replace('[ticker]', ticker);
12496  const inp = document.getElementById('chat-input');
12497  inp.value = msg;
12498  inp.focus();
12499}
12500function appendMessage(role, text){
12501  const log = document.getElementById('chat-log');
12502  const div = document.createElement('div');
12503  div.className = 'chat-msg ' + role;
12504  div.textContent = text;
12505  log.appendChild(div);
12506  log.scrollTop = log.scrollHeight;
12507}
12508function clearChat(){
12509  document.getElementById('chat-log').innerHTML = '';
12510  document.getElementById('chat-status').textContent = '';
12511}
12512async function sendChat(){
12513  const inp = document.getElementById('chat-input');
12514  const msg = inp.value.trim();
12515  if(!msg) return;
12516  inp.value = '';
12517  appendMessage('user', msg);
12518
12519  const context = {
12520    tickers: lastScanCharts.map(c=>c.ticker),
12521    signals: lastScanCharts.map(c=>({ticker:c.ticker, direction:c.direction, price:c.price}))
12522  };
12523
12524  const btn = document.getElementById('chat-btn');
12525  const status = document.getElementById('chat-status');
12526  btn.disabled = true;
12527  status.textContent = '⏳ Thinking…';
12528
12529  try {
12530    const r = await fetch('/api/chat/ask', {
12531      method: 'POST',
12532      headers: {'Content-Type':'application/json', 'Authorization': `Bearer ${TOKEN}`},
12533      body: JSON.stringify({message: msg, context})
12534    });
12535    const data = await r.json();
12536    if(!r.ok){
12537      const detail = data.detail;
12538      if(typeof detail === 'object' && detail.cooldown_seconds){
12539        status.textContent = `⏱ Rate limit — try again in ${detail.cooldown_seconds}s`;
12540      } else {
12541        appendMessage('err', (typeof detail === 'string' ? detail : detail?.message) || 'Error — try again.');
12542        status.textContent = '';
12543      }
12544    } else {
12545      appendMessage('ai', data.reply);
12546      if(data.remaining > 0){
12547        status.textContent = `${data.remaining} of ${data.limit} questions remaining this window`;
12548      } else {
12549        status.textContent = `Limit reached — resets in ${data.cooldown_seconds}s`;
12550      }
12551    }
12552  } catch(e){
12553    appendMessage('err', 'Connection error. Try again.');
12554    status.textContent = '';
12555  } finally {
12556    btn.disabled = false;
12557  }
12558}
12559
12560function toggleSidebar(){
12561  const s=$('sidebar'), b=document.getElementById('mob-backdrop');
12562  const open=s.classList.toggle('mob-open');
12563  b.classList.toggle('visible',open);
12564}
12565// Close sidebar when a nav item is tapped on mobile
12566document.addEventListener('click',function(e){
12567  if(e.target.closest('.nav-item')&&window.innerWidth<=768)toggleSidebar();
12568});
12569
12570// ─── Design Studio ────────────────────────────────────────────────
12571let _studioInited = false;
12572
12573async function initDesignStudio() {
12574  if (_studioInited) return;
12575  try {
12576    const r = await api('GET', '/api/contentdrips/config');
12577    const d = await r.json();
12578    if (!d.configured || typeof contentdrips === 'undefined') return;
12579    contentdrips.init({
12580      client_id:   d.client_id,
12581      email:       USER.email,
12582      user_id:     String(USER.id),
12583      environment: 'production',
12584    });
12585    if (window._brandKit) {
12586      contentdrips.profile.set({
12587        name:    _brandKit.name    || '',
12588        handle:  _brandKit.handle  || '',
12589        avatar:  _brandKit.logo_url || '',
12590        website: _brandKit.website_url || '',
12591        bio:     _brandKit.bio     || '',
12592      });
12593    }
12594    contentdrips.on('image_export', async (data) => {
12595      await saveStudioExport('image', [data.image_url]);
12596    });
12597    contentdrips.on('carousel_export', async (data) => {
12598      await saveStudioExport('carousel', data.images);
12599    });
12600    _studioInited = true;
12601  } catch(e) { /* non-fatal — SDK may not be loaded yet */ }
12602}
12603
12604async function openDesignStudio() {
12605  await initDesignStudio();
12606  if (typeof contentdrips === 'undefined') {
12607    toast('Design Studio not configured — contact admin to activate', 'err');
12608    return;
12609  }
12610  contentdrips.open({});
12611}
12612
12613async function saveStudioExport(type, images) {
12614  const saving = $('studio-saving');
12615  if (saving) saving.style.display = '';
12616  try {
12617    const r = await api('POST', '/api/contentdrips/save', { type, images });
12618    const d = await r.json();
12619    if (r.ok) {
12620      toast('Design saved!', 'ok');
12621      await loadStudioGallery();
12622    } else {
12623      toast(d.detail || 'Save failed', 'err');
12624    }
12625  } catch(e) {
12626    toast('Error saving design', 'err');
12627  } finally {
12628    if (saving) saving.style.display = 'none';
12629  }
12630}
12631
12632async function loadStudioGallery() {
12633  const grid  = $('studio-gallery');
12634  const empty = $('studio-empty');
12635  if (!grid) return;
12636  try {
12637    const r = await api('GET', '/api/contentdrips/gallery?limit=40');
12638    const d = await r.json();
12639    const designs = d.designs || [];
12640    if (!designs.length) {
12641      grid.innerHTML = '';
12642      if (empty) empty.style.display = '';
12643      return;
12644    }
12645    if (empty) empty.style.display = 'none';
12646    grid.innerHTML = designs.map(ds => {
12647      const slideLinks = ds.slides.length > 1
12648        ? ds.slides.map((s, i) =>
12649            `<a href="${s.url}" target="_blank" download style="font-size:.62rem" class="btn btn-ghost">Slide ${i+1}</a>`
12650          ).join('')
12651        : '';
12652      return `<div class="card" style="padding:0;overflow:hidden;position:relative">
12653        <img src="${ds.thumbnail}" style="width:100%;aspect-ratio:1;object-fit:cover;display:block"
12654             onerror="this.style.background='var(--bg2)';this.removeAttribute('src')">
12655        <div style="padding:8px 10px">
12656          <div style="font-size:.68rem;color:var(--muted);margin-bottom:6px">
12657            ${ds.slides.length} slide${ds.slides.length > 1 ? 's' : ''} · ${new Date(ds.created_at).toLocaleDateString()}
12658          </div>
12659          <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
12660            <a href="${ds.thumbnail}" target="_blank" download style="font-size:.65rem" class="btn btn-ghost">↓ ${ds.slides.length > 1 ? 'Cover' : 'Download'}</a>
12661            ${slideLinks}
12662            <button onclick="deleteStudioDesign('${ds.id}')"
12663              style="font-size:.62rem;color:#f87171;background:none;border:none;cursor:pointer;padding:2px 4px;margin-left:auto">✕</button>
12664          </div>
12665        </div>
12666      </div>`;
12667    }).join('');
12668  } catch(e) {
12669    if (grid) grid.innerHTML = '<p style="font-size:.73rem;color:#f87171">Failed to load gallery.</p>';
12670  }
12671}
12672
12673async function deleteStudioDesign(id) {
12674  if (!confirm('Delete this design?')) return;
12675  const r = await api('DELETE', `/api/contentdrips/designs/${id}`);
12676  if (r.ok) { toast('Deleted', 'ok'); await loadStudioGallery(); }
12677  else toast('Delete failed', 'err');
12678}
12679// ──────────────────────────────────────────────────────────────────
12680
12681// ─── Factoz Graphs ────────────────────────────────────────────────
12682const FACTOZ_GRAPHS = [
12683  // Demographics
12684  { id:'pop',        cat:'Demographics', icon:'👥', title:'World Population',
12685    iframe:'https://ourworldindata.org/grapher/population-with-un-projections?tab=line',
12686    slug:'population-with-un-projections' },
12687  { id:'pop-growth', cat:'Demographics', icon:'📈', title:'Population Growth Rate',
12688    iframe:'https://ourworldindata.org/grapher/population-growth-rates?tab=chart',
12689    slug:'population-growth-rates' },
12690  { id:'life-exp',   cat:'Health',       icon:'❤️', title:'Life Expectancy',
12691    iframe:'https://ourworldindata.org/grapher/life-expectancy?country=OWID_WRL~Americas~OWID_EUR~OWID_AFR&tab=line',
12692    slug:'life-expectancy' },
12693  { id:'hunger',     cat:'Health',       icon:'🍽️', title:'Share Undernourished',
12694    iframe:'https://ourworldindata.org/grapher/prevalence-of-undern
12694ourishment?tab=line',
12695    slug:'prevalence-of-undernourishment' },
12696  // Economy
12697  { id:'gdp',        cat:'Economy',      icon:'💰', title:'GDP per Capita',
12698    iframe:'https://ourworldindata.org/grapher/gdp-per-capita-worldbank?tab=chart',
12699    slug:'gdp-per-capita-worldbank' },
12700  // Energy
12701  { id:'energy-pc',  cat:'Energy',       icon:'⚡', title:'Energy Use per Person',
12702    iframe:'https://ourworldindata.org/grapher/per-capita-energy-use?tab=map',
12703    slug:'per-capita-energy-use' },
12704  { id:'elec-src',   cat:'Energy',       icon:'🔋', title:'Electricity by Source',
12705    iframe:'https://ourworldindata.org/grapher/electricity-prod-source-stacked?tab=chart',
12706    slug:'electricity-prod-source-stacked' },
12707  // Technology
12708  { id:'internet',   cat:'Technology',   icon:'🌐', title:'Internet Users',
12709    iframe:'https://ourworldindata.org/grapher/share-of-individuals-using-the-internet?country=WB_SA~WB_NA~WB_SSA~WB_EAP&tab=map',
12710    slug:'share-of-individuals-using-the-internet' },
12711  { id:'space',      cat:'Technology',   icon:'🚀', title:'Annual Space Launches',
12712    iframe:'https://ourworldindata.org/grapher/yearly-number-of-objects-launched-into-outer-space?tab=line',
12713    slug:'yearly-number-of-objects-launched-into-outer-space' },
12714  // Energy
12715  { id:'elec-access', cat:'Energy',     icon:'💡', title:'Electricity Access',
12716    iframe:'https://ourworldindata.org/grapher/share-of-the-population-with-access-to-electricity?tab=map',
12717    slug:'share-of-the-population-with-access-to-electricity' },
12718  // Health
12719  { id:'vaccines',    cat:'Health',     icon:'💉', title:'Global Vaccination Coverage',
12720    iframe:'https://ourworldindata.org/grapher/global-vaccination-coverage?tab=line',
12721    slug:'global-vaccination-coverage' },
12722  // Conflict
12723  { id:'conflicts',   cat:'Conflict',   icon:'⚔️', title:'Deaths in Armed Conflicts',
12724    iframe:'https://ourworldindata.org/grapher/deaths-in-armed-conflicts-by-region?tab=chart',
12725    slug:'deaths-in-armed-conflicts-by-region' },
12726  // Society
12727  { id:'womens-rights', cat:'Society',  icon:'⚖️', title:"Women's Rights by Country",
12728    iframe:'https://ourworldindata.org/grapher/number-of-countries-with-key-economic-and-social-rights-for-women?tab=line',
12729    slug:'number-of-countries-with-key-economic-and-social-rights-for-women' },
12730];
12731
12732let _activeGraphFilter = 'All';
12733let _currentGraph = null;
12734
12735// ── LIVE CHARTS ──────────────────────────────────────────────────────────────
12736let _lcInited = false;
12737
12738function initChartsPage(){
12739  if(_lcInited) return;
12740  _lcInited = true;
12741  renderPineLibrary();
12742  loadLiveChart();
12743}
12744
12745function loadLiveChart(symbol){
12746  symbol = (symbol || 'NASDAQ:AAPL').toUpperCase();
12747  if(!symbol.includes(':')) symbol = 'NASDAQ:' + symbol;
12748  const wrap = $('tv-chart-wrap');
12749  if(!wrap) return;
12750  wrap.innerHTML = '';
12751
12752  const container = document.createElement('div');
12753  container.className = 'tradingview-widget-container';
12754  container.style.cssText = 'height:100%;width:100%';
12755
12756  const widget = document.createElement('div');
12757  widget.className = 'tradingview-widget-container__widget';
12758  widget.style.cssText = 'height:100%;width:100%';
12759
12760  const config = {
12761    allow_symbol_change: true,
12762    calendar: false,
12763    details: false,
12764    hide_side_toolbar: false,
12765    hide_top_toolbar: false,
12766    hide_legend: false,
12767    hide_volume: false,
12768    hotlist: false,
12769    interval: '240',
12770    locale: 'en',
12771    save_image: true,
12772    style: '1',
12773    symbol: symbol,
12774    theme: 'dark',
12775    timezone: 'Etc/UTC',
12776    backgroundColor: '#0b132b',
12777    gridColor: 'rgba(32, 46, 86, 0.5)',
12778    withdateranges: true,
12779    compareSymbols: [],
12780    studies: [],
12781    autosize: true,
12782  };
12783
12784  const script = document.createElement('script');
12785  script.type = 'text/javascript';
12786  script.src = 'https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js';
12787  script.async = true;
12788  script.textContent = JSON.stringify(config);
12789
12790  const copy = document.createElement('div');
12791  copy.style.cssText = 'font-size:.6rem;color:#3d4f63;opacity:.5;padding:2px 0';
12792  copy.innerHTML = `<a href="https://www.tradingview.com/" rel="noopener nofollow" target="_blank" style="color:#3d4f63;text-decoration:none">Advanced Chart by TradingView</a>`;
12793
12794  const tvRef = document.createElement('div');
12795  tvRef.style.cssText = 'text-align:right;margin-bottom:6px';
12796  tvRef.innerHTML = `<a href="https://in.tradingview.com/?aff_id=164648" target="_blank" rel="noopener" style="display:inline-block;background:#1a2340;border:1px solid #E2C275;border-radius:20px;padding:4px 12px;font-size:.68rem;color:#E2C275;text-decoration:none;letter-spacing:.3px">🎁 Get <strong>$15 USD off</strong> TradingView — Open here ↗</a>`;
12797
12798  container.appendChild(widget);
12799  container.appendChild(copy);
12800  container.appendChild(script);
12801  wrap.appendChild(tvRef);
12802  wrap.appendChild(container);
12803}
12804
12805const PINE_INDICATORS = [
12806  {name:'Supertrend',                category:'Trend',      desc:'Dynamic support/resistance that flips with trend direction',          file:'supertrend.pine'},
12807  {name:'Chandelier Exit',           category:'Trend',      desc:'ATR-based trailing stop to protect open gains',                       file:'chandelier_exit.pine'},
12808  {name:'HalfTrend',                 category:'Trend',      desc:'Smooth trend-following indicator with buy/sell signals',              file:'halftrend.pine'},
12809  {name:'Parabolic SAR',             category:'Trend',      desc:'Classic stop and reverse — catches trend reversals',                  file:'parabolic_sar.pine'},
12810  {name:'Hull Moving Average',       category:'Moving Avg', desc:'Reduced-lag MA by Alan Hull — faster and smoother',                  file:'hull_moving_average.pine'},
12811  {name:'Kaufman Adaptive MA',       category:'Moving Avg', desc:'Adapts speed to market noise using efficiency ratio',                file:'kaufman_adaptive_moving_average.pine'},
12812  {name:'Zero Lag EMA',              category:'Moving Avg', desc:'EMA with lag elimination for faster signal response',                file:'zero_lag_ema.pine'},
12813  {name:'Jurik Moving Average',      category:'Moving Avg', desc:'Low-lag, low-noise adaptive filter by Mark Jurik',                  file:'jurik_moving_average.pine'},
12814  {name:'RSI',                       category:'Oscillator', desc:'
12814Classic relative strength index — momentum & overbought/oversold',   file:'rsi.pine'},
12815  {name:'Laguerre RSI',              category:'Oscillator', desc:'Lower-noise RSI variant using the Laguerre transform',               file:'laguerre_rsi.pine'},
12816  {name:'Schaff Trend Cycle',        category:'Oscillator', desc:'Faster MACD-based cycle oscillator with cleaner signals',            file:'schaff_trend_cycle.pine'},
12817  {name:'Stochastic Momentum Index', category:'Oscillator', desc:'Refined stochastic with smoother signal line',                      file:'stochastic_momentum_index.pine'},
12818  {name:'Awesome Oscillator',        category:'Oscillator', desc:'Bill Williams momentum oscillator using SMA difference',             file:'awesome_oscillator.pine'},
12819  {name:'Jurik RSX',                 category:'Oscillator', desc:'Ultra-smooth RSI replacement with minimal lag',                     file:'jurik_rsx.pine'},
12820  {name:'On Balance Volume',         category:'Volume',     desc:'Cumulative volume — confirms or contradicts price direction',        file:'on_balance_volume.pine'},
12821  {name:'Klinger Volume Oscillator', category:'Volume',     desc:'Long-term money flow trend vs short-term fluctuations',             file:'klinger_volume_oscillator.pine'},
12822];
12823
12824const _PINE_CAT_COLORS = {
12825  'Trend':      {bg:'rgba(0,229,180,.12)',  border:'rgba(0,229,180,.4)',  text:'#00e5b4'},
12826  'Moving Avg': {bg:'rgba(33,150,243,.12)', border:'rgba(33,150,243,.4)', text:'#42a5f5'},
12827  'Oscillator': {bg:'rgba(156,39,176,.12)', border:'rgba(156,39,176,.4)', text:'#ce93d8'},
12828  'Volume':     {bg:'rgba(255,152,0,.12)',  border:'rgba(255,152,0,.4)',  text:'#ffb74d'},
12829};
12830
12831function renderPineLibrary(){
12832  const wrap = $('pine-library-wrap');
12833  if(!wrap) return;
12834  const hasPro = USER && (USER.tier==='pro'||USER.tier==='stellar'||USER.tier==='enterprise_api'||USER.tier==='admin'||USER.tier==='super_admin');
12835  if(!hasPro){
12836    wrap.innerHTML=`<div style="background:#161b22;border:1px solid #30363d;border-radius:12px;padding:32px;text-align:center">
12837      <div style="font-size:1.8rem;margin-bottom:12px">🔒</div>
12838      <div style="font-weight:700;margin-bottom:8px">Pine Script Library — Pro plan required</div>
12839      <p style="color:#8b949e;font-size:.88rem;margin-bottom:18px">Upgrade to Pro to download all 16 curated Pine Script indicators.</p>
12840      <a href="https://factozcentral.com/register" style="display:inline-block;padding:11px 26px;background:#00e5b4;color:#000;border-radius:7px;font-weight:700;text-decoration:none;font-size:.9rem">Upgrade to Pro →</a>
12841    </div>`;
12842    return;
12843  }
12844  const cats = [...new Set(PINE_INDICATORS.map(i=>i.category))];
12845  let html = '';
12846  cats.forEach(cat => {
12847    const c = _PINE_CAT_COLORS[cat] || {bg:'rgba(255,255,255,.06)',border:'rgba(255,255,255,.2)',text:'#c9d1d9'};
12848    html += `<div style="margin-bottom:24px">
12849      <div style="font-size:.65rem;font-family:var(--mono);letter-spacing:2px;color:${c.text};margin-bottom:10px;text-transform:uppercase">${cat}</div>
12850      <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px">`;
12851    PINE_INDICATORS.filter(i=>i.category===cat).forEach(ind => {
12852      html += `<div style="background:#161b22;border:1px solid #30363d;border-radius:10px;padding:16px;display:flex;flex-direction:column;gap:8px">
12853        <div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
12854          <span style="font-weight:700;font-size:.9rem;color:var(--text)">${ind.name}</span>
12855          <span style="font-size:.6rem;font-family:var(--mono);padding:2px 8px;border-radius:20px;background:${c.bg};border:1px solid ${c.border};color:${c.text};white-space:nowrap">${ind.category}</span>
12856        </div>
12857        <p style="font-size:.78rem;color:#8b949e;margin:0;line-height:1.5">${ind.desc}</p>
12858        <a href="/pine/${ind.file}" download style="margin-top:4px;padding:7px 14px;background:transparent;border:1px solid #30363d;border-radius:6px;color:#c9d1d9;font-size:.78rem;font-weight:600;text-decoration:none;text-align:center;display:block;transition:border-color .2s"
12859          onmouseover="this.style.borderColor='#00e5b4';this.style.color='#00e5b4'"
12860          onmouseout="this.style.borderColor='#30363d';this.style.color='#c9d1d9'">
12861          ⬇ Download .pine
12862        </a>
12863      </div>`;
12864    });
12865    html += `</div></div>`;
12866  });
12867  wrap.innerHTML = html;
12868}
12869// ── END LIVE CHARTS ───────────────────────────────────────────────────────────
12870
12871function renderGraphsPage() {
12872  const filters = $('graphs-filters');
12873  const grid = $('graphs-grid');
12874  if (!filters || !grid) return;
12875
12876  const cats = ['All', ...new Set(FACTOZ_GRAPHS.map(g => g.cat))];
12877  filters.innerHTML = cats.map(c => `
12878    <button onclick="filterGraphs('${c}')"
12879      style="font-size:.68rem;padding:4px 12px;border-radius:20px;cursor:pointer;
12880             border:1px solid var(--border);
12881             background:${c===_activeGraphFilter?'var(--accent)':'var(--card)'};
12882             color:${c===_activeGraphFilter?'#000':'var(--text)'}">
12883      ${c}
12884    </button>`).join('');
12885
12886  const filtered = _activeGraphFilter === 'All'
12887    ? FACTOZ_GRAPHS
12888    : FACTOZ_GRAPHS.filter(g => g.cat === _activeGraphFilter);
12889
12890  grid.innerHTML = filtered.map(g => `
12891    <div class="card" onclick="openGraphModal('${g.id}')"
12892      style="cursor:pointer;padding:16px 12px;text-align:center;
12893             transition:transform .15s;user-select:none"
12894      onmouseover="this.style.transform='translateY(-2px)'"
12895      onmouseout="this.style.transform=''">
12896      <div style="font-size:2rem;margin-bottom:8px">${g.icon}</div>
12897      <div style="font-size:.72rem;font-weight:600;line-height:1.3">${g.title}</div>
12898      <div style="font-size:.62rem;color:var(--muted);margin-top:4px">${g.cat}</div>
12899    </div>`).join('');
12900}
12901
12902function filterGraphs(cat) {
12903  _activeGraphFilter = cat;
12904  renderGraphsPage();
12905}
12906
12907function openGraphModal(id) {
12908  _currentGraph = FACTOZ_GRAPHS.find(g => g.id === id);
12909  if (!_currentGraph) return;
12910  $('graph-modal-title').textContent = _currentGraph.icon + ' ' + _currentGraph.title;
12911  $('graph-modal-iframe').src = _currentGraph.iframe;
12912  $('graph-modal-source').href = 'https://ourworldindata.org/grapher/' + _currentGraph.slug;
12913  $('graph-modal-overlay').style.display = 'flex';
12914}
12915
12916function closeGraphModal(e) {
12917  if (e && e.target !== $('graph-modal-overlay')) return;
12918  $('graph-modal-overlay').style.display = 'none';
12919  $('graph-modal-iframe').src = '';
12920  _currentGraph = null;
12921}
12922
12923function downloadGraphPNG() {
12924  if (!_currentGraph) return;
12925  // Our World in Data's chart PNGs are public — no backend proxy needed
12926  const a = document.createElement('a');
12927  a.href = 'https://ourworldindata.org/grapher/' + _currentGraph.slug + '.png';
12928  a.target = '_blank';
12929  a.rel = 'noopener';
12930  a.download = _currentGraph.slug + '.png';
12931  document.body.appendChild(a);
12932  a.click();
12933  document.body.removeChild(a);
12934}
12935// ──────────────────────────────────────────────────────────────────
12936</script>
12936
12937
12938<!-- Generic image preview modal -->
12939<div id="img-preview-modal" onclick="if(event.target===this)closeImgPreview()"
12940     style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.9);z-index:9999;
12941            align-items:center;justify-content:center;flex-direction:column;gap:14px">
12942  <img id="img-preview-src" style="max-width:92vw;max-height:80vh;border-radius:8px;box-shadow:0 4px 32px #000">
12943  <div style="display:flex;gap:12px">
12944    <button onclick="closeImgPreview()"
12945            style="background:#1e2130;color:#d1d4dc;border:1px solid #2a2e40;border-radius:6px;
12946                   padding:8px 20px;cursor:pointer;font-size:13px">✕ Close</button>
12947    <a id="img-preview-dl" download="image.png"
12948       style="background:#0d7c3d;color:#fff;border-radius:6px;padding:8px 20px;
12949              text-decoration:none;font-size:13px;cursor:pointer;display:inline-flex;align-items:center">⬇ Download</a>
12950  </div>
12951</div>
12952
12953<!-- Chart lightbox modal -->
12954<div id="chart-modal" onclick="if(event.target===this)closeChartModal()"
12955     style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:9999;
12956            align-items:center;justify-content:center;flex-direction:column;gap:14px">
12957  <img id="chart-modal-img" style="max-width:96vw;max-height:90vh;border-radius:8px;box-shadow:0 4px 32px #000">
12958  <div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center">
12959    <button onclick="closeChartModal()"
12960            style="background:#1e2130;color:#d1d4dc;border:1px solid #2a2e40;border-radius:6px;
12961                   padding:8px 20px;cursor:pointer;font-size:13px">✕ Close</button>
12962    <a id="chart-modal-dl" download="chart.png"
12963       style="background:#364fc7;color:#fff;border-radius:6px;padding:8px 20px;
12964              text-decoration:none;font-size:13px;cursor:pointer;display:inline-flex;align-items:center">⬇ Download</a>
12965  </div>
12966</div>
12967
12968<!-- ── MACRO DASHBOARD JS ── -->
12969<script>
12970let _macroSeriesCache = null;
12971let _macroCurrentChartB64 = null;
12972let _macroCompareB64 = null;
12973let _macroScorecardLoaded = false;
12974
12975async function _loadMacroSeries(){
12976  if(_macroSeriesCache) return _macroSeriesCache;
12977  try {
12978    const _r = await api('GET','/api/fred/series-list');
12979    const d = await _r.json();
12980    _macroSeriesCache = d.series||[];
12981  } catch(e){ _macroSeriesCache = []; }
12982  const opts = _macroSeriesCache.map(s=>`<option value="${s.id}">${s.id} — ${s.label}</option>`).join('');
12983  const emptyOpt = '<option value="">— optional —</option>';
12984  ['macro-chart-series','macro-cmp-s1','fred-alert-series'].forEach(id=>{
12985    const el=document.getElementById(id); if(el) el.innerHTML=opts;
12986  });
12987  ['macro-cmp-s2','macro-cmp-s3'].forEach(id=>{
12988    const el=document.getElementById(id); if(el) el.innerHTML=emptyOpt+opts;
12989  });
12990  return _macroSeriesCache;
12991}
12992
12993function scannerTab(name){
12994  document.querySelectorAll('#page-scanner .macro-tab').forEach(b=>b.classList.remove('active'));
12995  document.querySelectorAll('#page-scanner .macro-pane').forEach(p=>p.classList.remove('active'));
12996  const btn=document.querySelector(`#page-scanner .macro-tab[onclick*="'${name}'"]`);
12997  if(btn)btn.classList.add('active');
12998  const pane=document.getElementById('scanner-pane-'+name);
12999  if(pane)pane.classList.add('active');
13000}
13001
13002function heatmapTab(name){
13003  document.querySelectorAll('#page-heatmap .macro-tab').forEach(b=>b.classList.remove('active'));
13004  document.querySelectorAll('#page-heatmap .macro-pane').forEach(p=>p.classList.remove('active'));
13005  const btn=document.querySelector(`#page-heatmap .macro-tab[onclick*="'${name}'"]`);
13006  if(btn)btn.classList.add('active');
13007  const pane=document.getElementById('heatmap-pane-'+name);
13008  if(pane)pane.classList.add('active');
13009}
13010
13011function macroTab(name){
13012  document.querySelectorAll('#page-macro .macro-tab').forEach(b=>b.classList.remove('active'));
13013  document.querySelectorAll('#page-macro .macro-pane').forEach(p=>p.classList.remove('active'));
13014  const btn=document.querySelector(`#page-macro .macro-tab[onclick*="'${name}'"]`);
13015  if(btn) btn.classList.add('active');
13016  const pane=document.getElementById('macro-pane-'+name);
13017  if(pane) pane.classList.add('active');
13018  if(name==='releases' && !document.getElementById('macro-releases-list')?.dataset.loaded) loadMacroReleases();
13019  if(name==='alerts'){ loadFredAlerts(); checkFredAlerts(); }
13020}
13021
13022async function loadMacroScorecard(force){
13023  await _loadMacroSeries();
13024  if(_macroScorecardLoaded && !force) return;
13025  const grid=document.getElementById('macro-scorecard-grid');
13026  if(!grid) return;
13027  grid.innerHTML='<p style="font-size:.8rem;color:var(--text2);grid-column:1/-1">Fetching FRED data...</p>';
13028  try {
13029    const _r=await api('GET','/api/fred/scorecard');
13030    const d=await _r.json();
13031    const cards=d.cards||[];
13032    if(!cards.length){
13033      grid.innerHTML='<p style="color:var(--text2);font-size:.8rem;grid-column:1/-1">No data. Is FRED_API_KEY set?</p>';
13034      return;
13035    }
13036    grid.innerHTML=cards.map(c=>{
13037      const arrow=c.arrow||'';
13038      const delta=c.delta!=null?(c.delta>0?'+':'')+c.delta.toFixed(2):'';
13039      const hist=JSON.stringify(c.history||[]).replace(/'/g,'&#39;');
13040      return `<div class="macro-card" data-history='${hist}' data-label="${c.label}">
13041        <div class="mc-label">${c.label}</div>
13042        <div class="mc-val">${c.display}</div>
13043        <div class="mc-sub">${arrow} ${delta}&nbsp;&nbsp;${c.date}</div>
13044        <div><span class="mc-badge ${c.status}">${c.status_label}</span></div>
13045      </div>`;
13046    }).join('');
13047    _attachMacroTooltips();
13048    _macroScorecardLoaded=true;
13049  } catch(e){
13050    grid.innerHTML=`<p style="color:var(--red);font-size:.8rem;grid-column:1/-1">Error: ${e.message||e}</p>`;
13051  }
13052}
13053
13054function _attachMacroTooltips(){
13055  const tip=document.getElementById('mc-tooltip');
13056  if(!tip) return;
13057  document.querySelectorAll('.macro-card').forEach(card=>{
13058    card.addEventListener('mouseenter',e=>{
13059      try {
13060        const history=JSON.parse(card.dataset.history||'[]');
13061        const label=card.dataset.label||'';
13062        if(!history.length) return;
13063        const rows=history.map(h=>`<div class="mct-row"><span class="mct-date">${h.date}</span><span class="mct-val">${h.display}</span></div>`).join('');
13064        tip.innerHTML=`<div class="mct-title">${label.toUpperCase()}</div>${rows}`;
13065        tip.style.display='block';
13066        _moveMacroTip(e);
13067      } catch(_){}
13068    });
13069    card.addEventListener('mousemove',_moveMacroTip);
13070    card.addEventListener('mouseleave',()=>{ tip.style.display='none'; });
13071  });
13072}
13073function _moveMacroTip(e){
13074  const tip=document.getElementById('mc-tooltip');
13075  if(!tip) return;
13076  const x=e.clientX+14, y=e.clientY+14;
13077  tip.style.left=(x+tip.offsetWidth>window.innerWidth ? x-tip.offsetWidth-28 : x)+'px';
13078  tip.style.top =(y+tip.offsetHeight>window.innerHeight ? y-tip.offsetHeight-28 : y)+'px';
13079}
13080
13081let _macroCurrentObs=[];
13082
13083async function loadMacroChart(){
13084  const sid   =document.getElementById('macro-chart-series')?.value;
13085  const trans =document.getElementById('macro-chart-transform')?.value||'level';
13086  const limit =document.getElementById('macro-chart-limit')?.value||'120';
13087  const rec   =document.getElementById('macro-chart-recession')?.checked?1:0;
13088  if(!sid){ toast('Select a series','err'); return; }
13089  const wrap=document.getElementById('macro-chart-wrap');
13090  wrap.style.justifyContent='center';
13091  wrap.innerHTML='<p style="color:var(--text2);font-size:.8rem">Rendering chart...</p>';
13092  document.getElementById('macro-chart-table-wrap').style.display='none';
13093  try {
13094    const [chartRes,obsRes]=await Promise.all([
13095      api('GET',`/api/fred/chart/${sid}?transform=${trans}&limit=${limit}&recession=${rec}`),
13096      api('GET',`/api/fred/series/${sid}?transform=${trans}&limit=${limit}`)
13097    ]);
13098    const chartData=await chartRes.json();
13099    const obsData=await obsRes.json();
13100    _macroCurrentChartB64=chartData.image_b64;
13101    wrap.style.justifyContent='';
13102    wrap.innerHTML=`<img src="data:image/png;base64,${chartData.image_b64}" style="width:100%;border-radius:8px;display:block">`;
13103    document.getElementById('macro-chart-export').style.display='';
13104    _macroCurrentObs=obsData.observations||[];
13105    _renderFredTable(obsData,trans);
13106  } catch(e){
13107    wrap.innerHTML=`<p style="color:var(--red);font-size:.8rem;padding:20px">${e.message||'Chart failed'}</p>`;
13108  }
13109}
13110
13111function _renderFredTable(obsData,trans){
13112  const obs=(obsData.observations||[]).slice().reverse();
13113  const info=obsData.info||{};
13114  const wrap=document.getElementById('macro-chart-table-wrap');
13115  const body=document.getElementById('macro-chart-table-body');
13116  const meta=document.getElementById('macro-table-meta');
13117  const uhdr=document.getElementById('macro-table-unit-hdr');
13118  if(!obs.length){ wrap.style.display='none'; return; }
13119  const unitLabel=trans==='yoy'?'YoY %':trans==='mom'?'MoM %':trans==='mom_abs'?'MoM Change':(info.units||'Value');
13120  meta.textContent=`${info.title||obsData.series_id} · ${obs.length} OBSERVATIONS`;
13121  uhdr.textContent=unitLabel.toUpperCase();
13122  body.innerHTML=obs.map((r,i)=>`
13123    <tr style="border-bottom:1px solid var(--border);${i%2===0?'background:var(--bg2)':''}">
13124      <td style="padding:5px 14px;color:var(--text2)">${r.date}</td>
13125      <td style="padding:5px 14px;text-align:right;color:var(--text)">${r.value.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</td>
13126    </tr>`).join('');
13127  wrap.style.display='block';
13128}
13129
13130function downloadFredCSV(){
13131  if(!_macroCurrentObs.length){ toast('Draw a chart first','err'); return; }
13132  const sid  =document.getElementById('macro-chart-series')?.value||'FRED';
13133  const trans=document.getElementById('macro-chart-transform')?.value||'level';
13134  const lbl  =trans==='yoy'?'YoY %':trans==='mom'?'MoM %':trans==='mom_abs'?'MoM Change':'Value';
13135  const csv  =`Date,${lbl}\n`+_macroCurrentObs.map(r=>`${r.date},${r.value}`).join('\n');
13136  const blob =new Blob([csv],{type:'text/csv'});
13137  const url  =URL.createObjectURL(blob);
13138  const a    =document.createElement('a');
13139  a.href=url; a.download=`${sid}_${trans}.csv`; a.click();
13140  URL.revokeObjectURL(url);
13141}
13142
13143function downloadMacroChart(){
13144  if(!_macroCurrentChartB64){ toast('Generate a chart first','err'); return; }
13145  const sid=document.getElementById('macro-chart-series')?.value||'chart';
13146  const trans=document.getElementById('macro-chart-transform')?.value||'level';
13147  const a=document.createElement('a');
13148  a.href='data:image/png;base64,'+_macroCurrentChartB64;
13149  a.download=`FRED_${sid}_${trans}.png`;
13150  a.click();
13151}
13152
13153function downloadMacroCompare(){
13154  if(!_macroCompareB64){ toast('Run a comparison first','err'); return; }
13155  const s1=document.getElementById('macro-cmp-s1')?.value||'';
13156  const s2=document.getElementById('macro-cmp-s2')?.value||'';
13157  const s3=document.getElementById('macro-cmp-s3')?.value||'';
13158  const label=[s1,s2,s3].filter(Boolean).join('-');
13159  const a=document.createElement('a');
13160  a.href='data:image/png;base64,'+_macroCompareB64;
13161  a.download=`FRED_compare_${label}.png`;
13162  a.click();
13163}
13164
13165async function loadMacroCompare(){
13166  const s1  =document.getElementById('macro-cmp-s1')?.value;
13167  const s2  =document.getElementById('macro-cmp-s2')?.value;
13168  const s3  =document.getElementById('macro-cmp-s3')?.value;
13169  const trans=document.getElementById('macro-cmp-transform')?.value||'level';
13170  const ids=[s1,s2,s3].filter(Boolean);
13171  if(!ids.length){ toast('Select at least one series','err'); return; }
13172  const wrap=document.getElementById('macro-compare-wrap');
13173  wrap.style.justifyContent='center';
13174  wrap.innerHTML='<p style="color:var(--text2);font-size:.8rem">Rendering comparison...</p>';
13175  try {
13176    const _r=await api('GET',`/api/fred/compare?ids=${ids.join(',')}&transform=${trans}`);
13177    const d=await _r.json();
13178    wrap.style.justifyContent='';
13179    wrap.innerHTML=`<img src="data:image/png;base64,${d.image_b64}" style="width:100%;border-radius:8px;display:block">`;
13180    _macroCompareB64=d.image_b64;
13181    document.getElementById('macro-compare-dl').style.display='';
13182  } catch(e){
13183    wrap.innerHTML=`<p style="color:var(--red);font-size:.8rem;padding:20px">${e.message||'Compare failed'}</p>`;
13184  }
13185}
13186
13187async function loadMacroAIBrief(){
13188  const btn=document.getElementById('macro-brief-btn');
13189  const out=document.getElementById('macro-brief-out');
13190  const dout=document.getElementById('macro-brief-data');
13191  btn.disabled=true; btn.textContent='Generating...';
13192  out.style.display='none'; dout.style.display='none';
13193  try {
13194    const _r=await api('GET','/api/fred/ai-brief');
13195    const d=await _r.json();
13196    out.textContent=d.brief||'';
13197    out.style.display='block';
13198    if(d.data_used?.length){
13199      dout.innerHTML='<div style="font-size:.58rem;letter-spacing:2px;color:var(--muted);margin-bottom:6px">DATA USED</div>'
13200                    +d.data_used.map(r=>`<div>${r}</div>`).join('');
13201      dout.style.display='block';
13202    }
13203  } catch(e){
13204    out.textContent=`Error: ${e.message||e}`; out.style.display='block';
13205  } finally {
13206    btn.disabled=false; btn.textContent='Generate Macro Brief';
13207  }
13208}
13209
13210async function loadMacroReleases(){
13211  const el=document.getElementById('macro-releases-list');
13212  if(!el) return;
13213  el.innerHTML='<p style="font-size:.8rem;color:var(--text2)">Loading...</p>';
13214  el.dataset.loaded='1';
13215  try {
13216    const _r=await api('GET','/api/fred/releases?days=30');
13217    const d=await _r.json();
13218    const releases=d.releases||[];
13219    if(!releases.length){ el.innerHTML='<p style="font-size:.8rem;color:var(--text2)">No upcoming releases found.</p>'; return; }
13220    const byDate={};
13221    releases.forEach(r=>{ (byDate[r.date]=byDate[r.date]||[]).push(r.release_name); });
13222    const todayStr=new Date().toISOString().slice(0,10);
13223    el.innerHTML=Object.keys(byDate).sort().map(dt=>{
13224      const names=byDate[dt];
13225      const diff=(new Date(dt)-new Date())/86400000;
13226      const badge=dt===todayStr
13227        ?'<span class="mc-badge alert" style="margin-left:8px">TODAY</span>'
13228        :diff<=7?'<span class="mc-badge neutral" style="margin-left:8px">SOON</span>':'';
13229      return `<div style="display:flex;gap:14px;align-items:flex-start;padding:10px 0;border-bottom:1px solid var(--border)">
13230        <div style="font-family:var(--mono);font-size:.75rem;color:var(--teal);min-width:100px">${dt}${badge}</div>
13231        <div style="font-size:.78rem;color:var(--text);line-height:1.6">${names.join(' &nbsp;·&nbsp; ')}</div>
13232      </div>`;
13233    }).join('');
13234  } catch(e){
13235    el.innerHTML=`<p style="color:var(--red);font-size:.8rem">${e.message||e}</p>`;
13236  }
13237}
13238
13239async function loadFredAlerts(){
13240  const el=document.getElementById('fred-alerts-list');
13241  if(!el) return;
13242  el.innerHTML='<p style="font-size:.8rem;color:var(--text2)">Loading...</p>';
13243  try {
13244    const _r=await api('GET','/api/fred/alerts');
13245    const d=await _r.json();
13246    if(!d.length){ el.innerHTML='<p style="font-size:.8rem;color:var(--text2)">No alerts set.</p>'; return; }
13247    el.innerHTML=d.map(a=>`
13248      <div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:8px">
13249        <div style="flex:1">
13250          <div style="font-size:.78rem;font-weight:600">${a.series_title||a.series_id}</div>
13251          <div style="font-size:.68rem;color:var(--text2);font-family:var(--mono);margin-top:2px">
13252            ${a.transform.toUpperCase()} ${a.condition==='gt'?'▲ above':'▼ below'} <strong>${a.threshold}</strong>
13253            ${a.last_triggered?' · last fired '+a.last_triggered.slice(0,10):''}
13254          </div>
13255        </div>
13256        <button onclick="deleteFredAlert('${a.id}')" style="background:none;border:1px solid var(--border);color:var(--muted);border-radius:6px;padding:3px 10px;font-size:.7rem;cursor:pointer">✕ Delete</button>
13257      </div>`).join('');
13258  } catch(e){
13259    el.innerHTML=`<p style="color:var(--red);font-size:.8rem">${e.message||e}</p>`;
13260  }
13261}
13262
13263async function createFredAlert(){
13264  const series    =document.getElementById('fred-alert-series')?.value;
13265  const transform =document.getElementById('fred-alert-transform')?.value||'level';
13266  const condition =document.getElementById('fred-alert-condition')?.value||'gt';
13267  const threshold =parseFloat(document.getElementById('fred-alert-threshold')?.value);
13268  if(!series||isNaN(threshold)){ toast('Fill in all alert fields','err'); return; }
13269  const label=(_macroSeriesCache||[]).find(s=>
13269s.id===series)?.label||series;
13270  try {
13271    await api('POST','/api/fred/alerts',{series_id:series,series_title:label,condition,threshold,transform});
13272    toast('Alert saved','ok');
13273    document.getElementById('fred-alert-threshold').value='';
13274    loadFredAlerts();
13275  } catch(e){ toast(e.message||'Failed to save alert','err'); }
13276}
13277
13278async function deleteFredAlert(id){
13279  try {
13280    await api('DELETE',`/api/fred/alerts/${id}`);
13281    toast('Alert deleted','ok');
13282    loadFredAlerts();
13283  } catch(e){ toast(e.message||'Failed to delete','err'); }
13284}
13285
13286async function checkFredAlerts(){
13287  try {
13288    const _r=await api('GET','/api/fred/alerts/check');
13289    const d=await _r.json();
13290    const triggered=d.triggered||[];
13291    const el=document.getElementById('fred-alerts-triggered');
13292    if(!triggered.length){ if(el) el.style.display='none'; return; }
13293    if(el){
13294      el.style.display='block';
13295      el.innerHTML='<strong>ALERTS TRIGGERED</strong><br>'+triggered.map(t=>
13296        `${t.series_title||t.series_id}: ${t.transform.toUpperCase()} is ${t.current_value?.toFixed(2)} (${t.condition==='gt'?'above':'below'} ${t.threshold}) as of ${t.date}`
13297      ).join('<br>');
13298    }
13299    toast(`${triggered.length} macro alert(s) triggered`,'err');
13300  } catch(e){ /* silently ignore on non-pro tiers */ }
13301}
13302</script>
13302
13303
13304<!-- ── PRE-MARKET CHART MODAL (TradingView) ── -->
13305<div id="pm-chart-modal" onclick="closePmChart()" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.7);align-items:center;justify-content:center">
13306  <div onclick="event.stopPropagation()" style="position:relative;width:min(360px,90vw)">
13307    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
13308      <div id="pm-chart-caption" style="font-family:var(--mono);font-size:.7rem;color:var(--text2)"></div>
13309      <button onclick="closePmChart()" style="background:none;border:none;color:var(--text2);font-size:1rem;cursor:pointer;padding:0 0 0 12px">✕</button>
13310    </div>
13311    <div style="border-radius:8px;overflow:hidden">
13312      <div id="pm-chart-container"></div>
13313    </div>
13314  </div>
13315</div>
13316
13317<!-- ── LIGHTBOX ── -->
13318<div id="lightbox" onclick="closeLightbox()" style="display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.92);align-items:center;justify-content:center;gap:16px">
13319  <button onclick="event.stopPropagation();lbPrev()" style="background:none;border:1px solid var(--border);color:var(--text);font-size:1.6rem;padding:8px 14px;border-radius:6px;cursor:pointer;flex-shrink:0">‹</button>
13320  <div onclick="event.stopPropagation()" style="max-width:min(90vw,700px);display:flex;flex-direction:column;align-items:center;gap:10px">
13321    <button onclick="closeLightbox()" style="align-self:flex-end;background:none;border:none;color:var(--muted);font-size:1.2rem;cursor:pointer;padding:0">✕</button>
13322    <img id="lb-img" src="" style="max-width:100%;max-height:80vh;border-radius:8px;object-fit:contain">
13323    <div style="display:flex;align-items:center;gap:16px">
13324      <div id="lb-caption" style="font-family:var(--mono);font-size:.65rem;color:var(--muted)"></div>
13325      <a id="lb-dl" href="#" download style="font-family:var(--mono);font-size:.65rem;color:var(--teal)">↓ Download</a>
13326      <a id="lb-tv-link" href="#" target="_blank" rel="noopener" style="font-family:var(--mono);font-size:.65rem;color:var(--text2);display:none">↗ TradingView</a>
13327    </div>
13328  </div>
13329  <button onclick="event.stopPropagation();lbNext()" style="background:none;border:1px solid var(--border);color:var(--text);font-size:1.6rem;padding:8px 14px;border-radius:6px;cursor:pointer;flex-shrink:0">›</button>
13330</div>
13331<script type="module" src="https://widgets.tradingview-widget.com/w/en/tv-mini-chart.js"></script>
vendor: 1 bytes, line 13331
13331
13332<script src="https://assets.contentdrips.io/embed.js"></script>
vendor: 1 bytes, line 13332
13332
13333<script src="https://cdn.plot.ly/plotly-basic-2.26.0.min.js"></script>
vendor: 1 bytes, line 13333
13333
13334<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
13334
13335<!-- lightweight-charts removed — Live Charts now uses TradingView Advanced Chart widget -->
13336<script>
13337// Init Paddle.js with client token from backend
13338fetch(API + '/api/paddle/config')
13339  .then(r => r.json())
13340  .then(d => {
13341    if (d.client_token) Paddle.Setup({ token: d.client_token });
13342    if (d.stellar_price_id) {
13343      window._paddleStellarId = d.stellar_price_id;
13344      document.querySelectorAll('.stellar-paddle-btn').forEach(b => b.style.display = 'block');
13345    }
13346  })
13347  .catch(() => {});
13348</script>
13348
13349
13350<!-- PayPal Hosted Button SDK -->
13351<script src="https://www.paypal.com/sdk/js?client-id=BAA2PaqZ__QcJLQKIxfJE49M6SAarwmXwpN38Xi72Nudi_Fp46D46ZISVgQ7ggNd-5iCHGIQQWUurUI16E&components=hosted-buttons&disable-funding=venmo&currency=USD" data-sdk-integration-source="button-factory"></script>
vendor: 1 bytes, line 13351
13351
13352<script>
13353// Render PayPal hosted button(s) wherever the container exists
13354function _initPayPalButton() {
13355  var containers = document.querySelectorAll('.paypal-hosted-btn-container');
13356  containers.forEach(function(el) {
13357    if (el.dataset.rendered) return;
13358    el.dataset.rendered = '1';
13359    try {
13360      paypal.HostedButtons({ hostedButtonId: 'D3PHKERK9D9NU' }).render('#' + el.id);
13361    } catch(e) { /* SDK not ready yet */ }
13362  });
13363}
13364// Try immediately + on SDK ready
13365if (window.paypal) { _initPayPalButton(); }
13366else { document.currentScript && document.currentScript.addEventListener
13367  ? null
13368  : setTimeout(_initPayPalButton, 2000); }
13369window.addEventListener('load', _initPayPalButton);
13370
13371// If user lands back with ?paypal_paid=1 show a pending toast and poll status
13372(function() {
13373  if (!/paypal_paid=1/.test(location.search)) return;
13374  history.replaceState(null, '', location.pathname);
13375  setTimeout(function() {
13376    toast('Payment received — activating your account, please wait a moment...', 'ok');
13377    // Poll user status every 5 s for up to 30 s
13378    var tries = 0;
13379    var poll = setInterval(async function() {
13380      tries++;
13381      try {
13382        var r = await fetch(API + '/api/auth/me', { headers: { Authorization: 'Bearer ' + localStorage.getItem('fz_token') } });
13383        var d = await r.json();
13384        if (d && d.is_paid) {
13385          clearInterval(poll);
13386          toast('Pro access activated!', 'ok');
13387          setTimeout(() => location.reload(), 1200);
13388        }
13389      } catch(e) {}
13390      if (tries >= 6) clearInterval(poll);
13391    }, 5000);
13392  }, 1500);
13393})();
13394</script>
13394
13395</body>
13396</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.