AndyKandy26 commited on
Commit
e75cf94
ยท
verified ยท
1 Parent(s): c564672

Upload 2 files

Browse files
Files changed (2) hide show
  1. dividends.html +1128 -0
  2. index.html +13 -2
dividends.html ADDED
@@ -0,0 +1,1128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Dividend Harvesting - FinWise</title>
7
+ <link rel="stylesheet" href="shared.css"/>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ <style>
10
+ :root {
11
+ --cyan:#22d3ee; --emerald:#10b981; --violet:#8b5cf6;
12
+ --amber:#f59e0b; --rose:#f43f5e; --card:#1e2535;
13
+ --border:#2a3347; --bg3:#151c2c; --text2:#94a3b8;
14
+ --bg:#0f1623; --text:#e2e8f0;
15
+ --gold:#fbbf24; --lime:#84cc16;
16
+ }
17
+ *{box-sizing:border-box;margin:0;padding:0}
18
+ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex}
19
+
20
+ /* โ”€โ”€ Sidebar โ”€โ”€ */
21
+ .sidebar{width:240px;min-height:100vh;background:var(--bg3);border-right:1px solid var(--border);padding:1.5rem 1rem;display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;overflow-y:auto;z-index:100}
22
+ .sidebar-logo{font-size:1.4rem;font-weight:800;color:var(--cyan);letter-spacing:-.5px;margin-bottom:2rem;padding-left:.5rem;display:flex;align-items:center;gap:.5rem}
23
+ .sidebar-logo span{color:var(--text)}
24
+ .nav-label{font-size:.65rem;font-weight:700;text-transform:uppercase;letter-spacing:1.2px;color:var(--text2);padding:.5rem .5rem .25rem;margin-top:.75rem}
25
+ .nav-item{display:flex;align-items:center;gap:.6rem;padding:.55rem .75rem;border-radius:8px;color:var(--text2);text-decoration:none;font-size:.875rem;font-weight:500;transition:all .15s}
26
+ .nav-item:hover{background:var(--card);color:var(--text)}
27
+ .nav-item.active{background:rgba(251,191,36,.1);color:var(--gold)}
28
+ .nav-icon{font-size:1rem;width:20px;text-align:center}
29
+ .nav-badge{margin-left:auto;font-size:.6rem;font-weight:700;padding:2px 6px;border-radius:20px;text-transform:uppercase;letter-spacing:.5px}
30
+ .nav-badge.new{background:var(--emerald);color:#000}
31
+ .nav-badge.picks{background:var(--emerald);color:#000}
32
+
33
+ /* โ”€โ”€ Main โ”€โ”€ */
34
+ .main-content{margin-left:240px;padding:1.5rem;flex:1;min-width:0}
35
+
36
+ /* โ”€โ”€ Hero banner โ”€โ”€ */
37
+ .div-hero{
38
+ background:linear-gradient(135deg,rgba(251,191,36,.08) 0%,rgba(16,185,129,.05) 100%);
39
+ border:1px solid rgba(251,191,36,.25);border-radius:14px;
40
+ padding:24px 28px;margin-bottom:1.5rem;
41
+ display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;
42
+ position:relative;overflow:hidden;
43
+ }
44
+ .div-hero::before{content:'';position:absolute;top:-50px;right:-50px;width:220px;height:220px;border-radius:50%;background:radial-gradient(circle,rgba(251,191,36,.1) 0%,transparent 70%);pointer-events:none}
45
+ .hero-title{font-size:1.7rem;font-weight:800;letter-spacing:-.5px}
46
+ .hero-title span{color:var(--gold)}
47
+ .hero-sub{color:var(--text2);font-size:.875rem;margin-top:.3rem}
48
+ .hero-actions{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center}
49
+
50
+ /* โ”€โ”€ Btn โ”€โ”€ */
51
+ .btn{display:inline-flex;align-items:center;gap:.4rem;padding:.5rem 1.1rem;border-radius:8px;border:none;font-size:.85rem;font-weight:600;cursor:pointer;transition:all .15s;text-decoration:none}
52
+ .btn-gold{background:var(--gold);color:#000}
53
+ .btn-gold:hover{opacity:.88}
54
+ .btn-gold:disabled{background:var(--border);color:var(--text2);cursor:not-allowed}
55
+ .btn-ghost{background:transparent;color:var(--text2);border:1px solid var(--border)}
56
+ .btn-ghost:hover{background:var(--card);color:var(--text)}
57
+ .btn-emerald{background:var(--emerald);color:#000}
58
+ .btn-emerald:hover{opacity:.88}
59
+
60
+ /* โ”€โ”€ Status bar โ”€โ”€ */
61
+ .status-bar{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:.6rem 1rem;margin-bottom:1.25rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem}
62
+ .data-badge{display:inline-flex;align-items:center;gap:.3rem;padding:3px 10px;border-radius:20px;font-size:.72rem;font-weight:700}
63
+ .data-badge.live{background:rgba(16,185,129,.15);color:var(--emerald);border:1px solid rgba(16,185,129,.3)}
64
+ .data-badge.cached{background:rgba(245,158,11,.12);color:var(--amber);border:1px solid rgba(245,158,11,.3)}
65
+ .data-badge.seed{background:rgba(148,163,184,.1);color:var(--text2);border:1px solid var(--border)}
66
+ .data-badge.error{background:rgba(244,63,94,.12);color:var(--rose);border:1px solid rgba(244,63,94,.3)}
67
+ .status-msg{font-size:.75rem;color:var(--text2)}
68
+ .status-msg span{color:var(--cyan)}
69
+
70
+ /* โ”€โ”€ Stat grid โ”€โ”€ */
71
+ .stat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:1rem;margin-bottom:1.5rem}
72
+ .stat-card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1rem 1.25rem;position:relative;overflow:hidden}
73
+ .stat-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:var(--accent,var(--gold))}
74
+ .stat-label{font-size:.7rem;color:var(--text2);text-transform:uppercase;letter-spacing:.8px;font-weight:600}
75
+ .stat-value{font-size:1.65rem;font-weight:800;margin:.25rem 0 .1rem;font-variant-numeric:tabular-nums}
76
+ .stat-sub{font-size:.72rem;color:var(--text2)}
77
+
78
+ /* โ”€โ”€ Layout โ”€โ”€ */
79
+ .div-layout{display:grid;grid-template-columns:280px 1fr;gap:1.25rem;align-items:start}
80
+
81
+ /* โ”€โ”€ Filter panel โ”€โ”€ */
82
+ .filter-panel{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:1.25rem;position:sticky;top:1.5rem}
83
+ .filter-section{margin-bottom:1.25rem;padding-bottom:1.25rem;border-bottom:1px solid var(--border)}
84
+ .filter-section:last-child{margin-bottom:0;padding-bottom:0;border-bottom:none}
85
+ .filter-section-title{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:var(--text2);margin-bottom:.75rem;display:flex;align-items:center;gap:.4rem}
86
+ .filter-group{margin-bottom:.85rem}
87
+ .filter-group:last-child{margin-bottom:0}
88
+ .filter-group label{display:block;font-size:.75rem;color:var(--text2);margin-bottom:.3rem;font-weight:600}
89
+ .filter-group input[type=range]{width:100%;accent-color:var(--gold);cursor:pointer}
90
+ .range-row{display:flex;justify-content:space-between;margin-top:.2rem}
91
+ .range-val{font-size:.78rem;color:var(--gold);font-weight:700}
92
+ .range-label{font-size:.72rem;color:var(--text2)}
93
+ .filter-group select{width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:.4rem .6rem;font-size:.82rem}
94
+ .toggle-row{display:flex;align-items:center;justify-content:space-between;padding:.4rem 0}
95
+ .toggle-label{font-size:.82rem;color:var(--text);font-weight:500}
96
+ .toggle{position:relative;width:38px;height:20px;flex-shrink:0}
97
+ .toggle input{opacity:0;width:0;height:0}
98
+ .toggle-slider{position:absolute;inset:0;background:var(--border);border-radius:20px;cursor:pointer;transition:.2s}
99
+ .toggle-slider::before{content:'';position:absolute;width:16px;height:16px;left:2px;bottom:2px;background:#fff;border-radius:50%;transition:.2s}
100
+ .toggle input:checked+.toggle-slider{background:var(--gold)}
101
+ .toggle input:checked+.toggle-slider::before{transform:translateX(18px)}
102
+ .checkboxes{display:flex;flex-direction:column;gap:.3rem}
103
+ .checkboxes label{display:flex;align-items:center;gap:.4rem;font-size:.8rem;color:var(--text);cursor:pointer}
104
+ .checkboxes input[type=checkbox]{accent-color:var(--gold)}
105
+ .search-box{display:flex;align-items:center;gap:.4rem;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:.4rem .7rem;margin-bottom:1rem}
106
+ .search-box input{background:transparent;border:none;outline:none;color:var(--text);font-size:.82rem;flex:1}
107
+ .search-box input::placeholder{color:var(--text2)}
108
+
109
+ /* โ”€โ”€ Sort bar โ”€โ”€ */
110
+ .sort-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:.85rem;flex-wrap:wrap;gap:.5rem}
111
+ .results-count{font-size:.82rem;color:var(--text2)}
112
+ .results-count strong{color:var(--text)}
113
+ .sort-controls{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}
114
+ .sort-label{font-size:.75rem;color:var(--text2)}
115
+ .sort-btn{padding:.3rem .7rem;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text2);font-size:.75rem;font-weight:600;cursor:pointer;transition:all .15s}
116
+ .sort-btn.active{background:var(--gold);color:#000;border-color:var(--gold)}
117
+ .export-btn{display:flex;align-items:center;gap:.35rem;padding:.35rem .8rem;border-radius:6px;border:1px solid var(--emerald);background:transparent;color:var(--emerald);font-size:.75rem;font-weight:600;cursor:pointer;transition:all .15s}
118
+ .export-btn:hover{background:rgba(16,185,129,.1)}
119
+
120
+ /* โ”€โ”€ Dividend table โ”€โ”€ */
121
+ .table-wrap{background:var(--card);border:1px solid var(--border);border-radius:12px;overflow:hidden}
122
+ .table-scroll{overflow-x:auto}
123
+ table{width:100%;border-collapse:collapse;font-size:.82rem}
124
+ thead tr{background:var(--bg3);border-bottom:1px solid var(--border)}
125
+ thead th{padding:.7rem .9rem;text-align:left;font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.7px;color:var(--text2);white-space:nowrap;cursor:pointer;user-select:none;transition:color .15s}
126
+ thead th:hover{color:var(--gold)}
127
+ thead th .si{margin-left:.25rem;opacity:.4;font-size:.65rem}
128
+ thead th.sorted .si{opacity:1;color:var(--gold)}
129
+ tbody tr{border-bottom:1px solid var(--border);cursor:pointer;transition:background .12s}
130
+ tbody tr:last-child{border-bottom:none}
131
+ tbody tr:hover{background:rgba(251,191,36,.04)}
132
+ tbody tr.expanded{background:rgba(251,191,36,.06);border-bottom:none}
133
+ td{padding:.7rem .9rem;white-space:nowrap;vertical-align:middle}
134
+
135
+ /* โ”€โ”€ Ex-div badges โ”€โ”€ */
136
+ .exdiv-badge{display:inline-flex;align-items:center;gap:.3rem;padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:700}
137
+ .exdiv-hot{background:rgba(16,185,129,.2);color:var(--emerald);border:1px solid rgba(16,185,129,.4)}
138
+ .exdiv-soon{background:rgba(245,158,11,.15);color:var(--amber);border:1px solid rgba(245,158,11,.35)}
139
+ .exdiv-upcoming{background:rgba(148,163,184,.1);color:var(--text2);border:1px solid var(--border)}
140
+
141
+ /* โ”€โ”€ Ticker cell โ”€โ”€ */
142
+ .ticker-cell{display:flex;flex-direction:column;gap:.15rem}
143
+ .ticker-sym{font-size:.9rem;font-weight:800;font-family:'Courier New',monospace;color:var(--text)}
144
+ .ticker-name{font-size:.7rem;color:var(--text2);max-width:140px;white-space:normal;line-height:1.3}
145
+
146
+ /* โ”€โ”€ Freq badge โ”€โ”€ */
147
+ .freq-badge{display:inline-block;padding:2px 7px;border-radius:20px;font-size:.65rem;font-weight:700}
148
+ .freq-q{background:rgba(34,211,238,.12);color:var(--cyan)}
149
+ .freq-m{background:rgba(16,185,129,.15);color:var(--emerald)}
150
+ .freq-s{background:rgba(139,92,246,.12);color:var(--violet)}
151
+ .freq-a{background:rgba(245,158,11,.12);color:var(--amber)}
152
+
153
+ /* โ”€โ”€ Sector badge โ”€โ”€ */
154
+ .badge{display:inline-block;padding:2px 7px;border-radius:20px;font-size:.65rem;font-weight:700}
155
+ .badge-tech{background:rgba(34,211,238,.12);color:var(--cyan)}
156
+ .badge-energy{background:rgba(245,158,11,.12);color:var(--amber)}
157
+ .badge-health{background:rgba(16,185,129,.12);color:var(--emerald)}
158
+ .badge-finance{background:rgba(139,92,246,.12);color:var(--violet)}
159
+ .badge-consumer{background:rgba(244,63,94,.1);color:var(--rose)}
160
+ .badge-industrial{background:rgba(148,163,184,.1);color:var(--text2)}
161
+ .badge-reit{background:rgba(251,191,36,.12);color:var(--gold)}
162
+ .badge-telecom{background:rgba(34,211,238,.08);color:var(--cyan)}
163
+
164
+ /* โ”€โ”€ Yield bar โ”€โ”€ */
165
+ .yield-bar-wrap{display:flex;align-items:center;gap:.5rem}
166
+ .yield-bar{background:var(--bg3);border-radius:3px;height:5px;width:50px;overflow:hidden}
167
+ .yield-fill{height:100%;border-radius:3px;background:var(--gold)}
168
+ .yield-val{font-family:monospace;font-weight:700;font-size:.82rem}
169
+
170
+ /* โ”€โ”€ Consistency meter โ”€โ”€ */
171
+ .consistency-dots{display:flex;gap:2px}
172
+ .c-dot{width:8px;height:8px;border-radius:2px;background:var(--bg3)}
173
+ .c-dot.filled{background:var(--emerald)}
174
+ .c-dot.partial{background:var(--amber)}
175
+
176
+ /* โ”€โ”€ Expand drawer โ”€โ”€ */
177
+ .drawer-row{display:none}
178
+ .drawer-row.open{display:table-row}
179
+ .drawer-cell{padding:0!important}
180
+ .drawer-inner{padding:1.25rem 1.5rem;background:rgba(251,191,36,.03);border-bottom:1px solid var(--border)}
181
+ .drawer-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:1.25rem}
182
+ .drawer-section-title{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.7px;color:var(--text2);margin-bottom:.6rem}
183
+ .drawer-stat-row{display:flex;justify-content:space-between;padding:.3rem 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:.8rem}
184
+ .drawer-stat-row:last-child{border-bottom:none}
185
+ .drawer-stat-label{color:var(--text2)}
186
+ .drawer-stat-val{font-weight:600;font-family:monospace}
187
+ .hist-chart-wrap{height:80px;position:relative}
188
+ .div-event-card{background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:.85rem;margin-top:.5rem}
189
+ .div-event-card .event-row{display:flex;justify-content:space-between;font-size:.78rem;padding:.25rem 0;border-bottom:1px solid rgba(255,255,255,.04)}
190
+ .div-event-card .event-row:last-child{border-bottom:none}
191
+ .event-label{color:var(--text2)}
192
+ .event-val{font-weight:600}
193
+ .howto-note{font-size:.75rem;color:var(--text2);line-height:1.6;margin-top:.75rem;padding:.7rem .85rem;background:var(--bg3);border-radius:8px;border-left:3px solid var(--gold)}
194
+
195
+ /* โ”€โ”€ Cooldown ring โ”€โ”€ */
196
+ .cooldown-ring{width:16px;height:16px;border-radius:50%;border:2px solid var(--text2);border-top-color:var(--gold);animation:spin .8s linear infinite;display:none}
197
+ .cooldown-ring.visible{display:inline-block}
198
+ @keyframes spin{to{transform:rotate(360deg)}}
199
+
200
+ /* โ”€โ”€ Empty state โ”€โ”€ */
201
+ .empty-state{text-align:center;padding:3rem 1rem;color:var(--text2)}
202
+ .empty-state .ei{font-size:2.5rem;margin-bottom:.75rem}
203
+
204
+ /* โ”€โ”€ Modal โ”€โ”€ */
205
+ .modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:1000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
206
+ .modal-backdrop.hidden{display:none}
207
+ .modal-box{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:2rem;width:min(460px,92vw);box-shadow:0 24px 64px rgba(0,0,0,.5)}
208
+ .modal-title{font-size:1.1rem;font-weight:800;margin-bottom:.4rem}
209
+ .modal-title span{color:var(--gold)}
210
+ .modal-sub{font-size:.82rem;color:var(--text2);margin-bottom:1.25rem;line-height:1.65}
211
+ .modal-sub a{color:var(--cyan);text-decoration:none}
212
+ .modal-input{width:100%;background:var(--bg3);border:1px solid var(--border);border-radius:8px;color:var(--text);padding:.65rem .85rem;font-size:.88rem;font-family:monospace;outline:none;transition:border-color .15s}
213
+ .modal-input:focus{border-color:var(--gold)}
214
+ .modal-actions{display:flex;gap:.75rem;margin-top:1rem;flex-wrap:wrap}
215
+ .modal-note{font-size:.72rem;color:var(--text2);margin-top:.85rem;line-height:1.55;padding:.65rem .75rem;background:var(--bg3);border-radius:8px}
216
+ .modal-note strong{color:var(--emerald)}
217
+
218
+ /* โ”€โ”€ Loading overlay โ”€โ”€ */
219
+ .table-loading{position:relative}
220
+ .tbl-overlay{position:absolute;inset:0;background:rgba(15,22,35,.8);display:flex;align-items:center;justify-content:center;z-index:10;border-radius:12px;backdrop-filter:blur(2px)}
221
+ .tbl-overlay.hidden{display:none}
222
+ .spin-lg{width:40px;height:40px;border-radius:50%;border:3px solid var(--border);border-top-color:var(--gold);animation:spin .7s linear infinite}
223
+
224
+ /* โ”€โ”€ Mobile bottom nav โ”€โ”€ */
225
+ .bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;background:var(--bg3);border-top:1px solid var(--border);z-index:200;padding:.4rem 0}
226
+ .bottom-nav-inner{display:flex;justify-content:space-around;align-items:center}
227
+ .bottom-nav-item{display:flex;flex-direction:column;align-items:center;gap:2px;color:var(--text2);text-decoration:none;font-size:.65rem;font-weight:600;padding:.3rem .5rem;border-radius:8px}
228
+ .bottom-nav-item .bn-icon{font-size:1.1rem}
229
+ .bottom-nav-item.active{color:var(--gold)}
230
+
231
+ /* โ”€โ”€ up/down โ”€โ”€ */
232
+ .up{color:var(--emerald);font-weight:600}
233
+ .down{color:var(--rose);font-weight:600}
234
+
235
+ @media(max-width:1024px){.div-layout{grid-template-columns:1fr}.filter-panel{position:static}}
236
+ @media(max-width:768px){
237
+ .sidebar{display:none}
238
+ .main-content{margin-left:0;padding:1rem;padding-bottom:5rem}
239
+ .bottom-nav{display:block}
240
+ .stat-grid{grid-template-columns:1fr 1fr}
241
+ .drawer-grid{grid-template-columns:1fr}
242
+ }
243
+ @media(max-width:480px){.stat-grid{grid-template-columns:1fr}}
244
+ </style>
245
+ </head>
246
+ <body>
247
+
248
+ <!-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• SIDEBAR โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -->
249
+ <nav class="sidebar">
250
+ <div class="sidebar-logo">๐Ÿ“ˆ <span>Fin</span>Wise</div>
251
+ <div class="nav-label">Main</div>
252
+ <a href="index.html" class="nav-item"><span class="nav-icon">๐Ÿ </span> Dashboard</a>
253
+ <a href="portfolio.html" class="nav-item"><span class="nav-icon">๐Ÿ“Š</span> Portfolio Builder</a>
254
+ <a href="risk.html" class="nav-item"><span class="nav-icon">๐ŸŽฏ</span> Risk Analyzer</a>
255
+ <a href="tracker.html" class="nav-item"><span class="nav-icon">๐Ÿ“ˆ</span> Tracker</a>
256
+ <div class="nav-label">Tools</div>
257
+ <a href="calculators.html" class="nav-item"><span class="nav-icon">๐Ÿงฎ</span> Calculators</a>
258
+ <a href="insights.html" class="nav-item"><span class="nav-icon">๐Ÿ’ก</span> Insights <span class="nav-badge">New</span></a>
259
+ <a href="picks.html" class="nav-item"><span class="nav-icon">๐Ÿน</span> Stock &amp; Option Picks <span class="nav-badge picks">New</span></a>
260
+ <a href="dividends.html" class="nav-item active"><span class="nav-icon">๐Ÿ’ฐ</span> Dividend Harvesting <span class="nav-badge new">New</span></a>
261
+ </nav>
262
+
263
+ <!-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• MAIN โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -->
264
+ <main class="main-content">
265
+
266
+ <!-- Hero -->
267
+ <div class="div-hero">
268
+ <div>
269
+ <div class="hero-title">๐Ÿ’ฐ <span>Dividend Harvesting</span></div>
270
+ <div class="hero-sub">Upcoming dividends in the next 6โ€“8 weeks ยท Filter by date, yield, sector ยท Click any row to expand details</div>
271
+ </div>
272
+ <div class="hero-actions">
273
+ <div class="search-box" style="margin-bottom:0;width:200px">
274
+ <span>๐Ÿ”</span>
275
+ <input type="text" id="globalSearch" placeholder="Search tickerโ€ฆ" oninput="applyFilters()"/>
276
+ </div>
277
+ <button class="btn btn-gold" id="fetchBtn" onclick="userFetch()">
278
+ <span class="cooldown-ring" id="cdRing"></span>
279
+ <span id="fetchLabel">โฌ‡๏ธ Fetch Live Data</span>
280
+ </button>
281
+ <button class="btn btn-ghost" onclick="showKeyModal()" title="Manage API key">๐Ÿ”‘</button>
282
+ </div>
283
+ </div>
284
+
285
+ <!-- Status bar -->
286
+ <div class="status-bar">
287
+ <div style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap">
288
+ <span id="statusBadge" class="data-badge seed">๐Ÿ“ฆ Seed Data</span>
289
+ <span class="status-msg" id="statusMsg">Press "Fetch Live Data" to load real dividend data from Finnhub</span>
290
+ </div>
291
+ <div style="font-size:.72rem;color:var(--text2)">
292
+ Source: Finnhub.io ยท <span style="cursor:pointer;color:var(--cyan)" onclick="showKeyModal()">๐Ÿ”‘ Manage key</span> ยท 10-min cooldown ยท Dividend history per stock
293
+ </div>
294
+ </div>
295
+
296
+ <!-- Stat strip -->
297
+ <div class="stat-grid">
298
+ <div class="stat-card" style="--accent:var(--gold)">
299
+ <div class="stat-label">Upcoming Events</div>
300
+ <div class="stat-value" id="statTotal" style="color:var(--gold)">โ€”</div>
301
+ <div class="stat-sub">ex-div in next 8 weeks</div>
302
+ </div>
303
+ <div class="stat-card" style="--accent:var(--emerald)">
304
+ <div class="stat-label">Avg Dividend Yield</div>
305
+ <div class="stat-value" id="statAvgYield" style="color:var(--emerald)">โ€”</div>
306
+ <div class="stat-sub">filtered results</div>
307
+ </div>
308
+ <div class="stat-card" style="--accent:var(--cyan)">
309
+ <div class="stat-label">Avg Days to Ex-Div</div>
310
+ <div class="stat-value" id="statAvgDays" style="color:var(--cyan)">โ€”</div>
311
+ <div class="stat-sub">from today</div>
312
+ </div>
313
+ <div class="stat-card" style="--accent:var(--amber)">
314
+ <div class="stat-label">High Yield (&gt;4%)</div>
315
+ <div class="stat-value" id="statHighYield" style="color:var(--amber)">โ€”</div>
316
+ <div class="stat-sub">picks in filtered view</div>
317
+ </div>
318
+ <div class="stat-card" style="--accent:var(--rose)">
319
+ <div class="stat-label">Ex-Div This Week</div>
320
+ <div class="stat-value" id="statThisWeek" style="color:var(--rose)">โ€”</div>
321
+ <div class="stat-sub">act fast โ€” buy before ex-date</div>
322
+ </div>
323
+ </div>
324
+
325
+ <!-- Main layout: filters + table -->
326
+ <div class="div-layout">
327
+
328
+ <!-- โ”€โ”€ Filter Panel โ”€โ”€ -->
329
+ <aside class="filter-panel">
330
+
331
+ <div class="filter-section">
332
+ <div class="filter-section-title">๐Ÿ“… Ex-Dividend Date Range</div>
333
+ <div class="filter-group">
334
+ <label>Max days out</label>
335
+ <input type="range" min="7" max="60" value="56" id="fDays"
336
+ oninput="document.getElementById('fDaysVal').textContent=this.value+'d (~'+Math.round(this.value/7)+'wk)';applyFilters()">
337
+ <div class="range-row">
338
+ <span class="range-val" id="fDaysVal">56d (~8wk)</span>
339
+ <span class="range-label">Today โ†’ +8 wk</span>
340
+ </div>
341
+ </div>
342
+ </div>
343
+
344
+ <div class="filter-section">
345
+ <div class="filter-section-title">๐Ÿ’ธ Dividend Yield</div>
346
+ <div class="filter-group">
347
+ <label>Min yield (%)</label>
348
+ <input type="range" min="0" max="10" step="0.5" value="0" id="fYieldMin"
349
+ oninput="document.getElementById('fYieldMinVal').textContent=this.value+'%';applyFilters()">
350
+ <div class="range-row">
351
+ <span class="range-val" id="fYieldMinVal">0%</span>
352
+ <span class="range-label">Min threshold</span>
353
+ </div>
354
+ </div>
355
+ <div class="filter-group">
356
+ <label>Max yield (%)</label>
357
+ <input type="range" min="1" max="15" step="0.5" value="15" id="fYieldMax"
358
+ oninput="document.getElementById('fYieldMaxVal').textContent=this.value+'%';applyFilters()">
359
+ <div class="range-row">
360
+ <span class="range-val" id="fYieldMaxVal">15%</span>
361
+ <span class="range-label">Max threshold</span>
362
+ </div>
363
+ </div>
364
+ <div class="filter-group">
365
+ <label>Yield type</label>
366
+ <select id="fYieldType" onchange="applyFilters()">
367
+ <option value="ttm">Trailing 12-Month (TTM)</option>
368
+ <option value="fwd">Forward Yield</option>
369
+ </select>
370
+ </div>
371
+ </div>
372
+
373
+ <div class="filter-section">
374
+ <div class="filter-section-title">๐Ÿข Company Filters</div>
375
+ <div class="filter-group">
376
+ <label>Market Cap</label>
377
+ <div class="checkboxes">
378
+ <label><input type="checkbox" checked value="Large" class="f-cap" onchange="applyFilters()"> Large Cap (&gt;$10B)</label>
379
+ <label><input type="checkbox" checked value="Mid" class="f-cap" onchange="applyFilters()"> Mid Cap ($2Bโ€“$10B)</label>
380
+ <label><input type="checkbox" checked value="Small" class="f-cap" onchange="applyFilters()"> Small Cap (&lt;$2B)</label>
381
+ </div>
382
+ </div>
383
+ <div class="filter-group">
384
+ <label>Sector</label>
385
+ <select id="fSector" onchange="applyFilters()">
386
+ <option value="">All Sectors</option>
387
+ <option>Technology</option>
388
+ <option>Healthcare</option>
389
+ <option>Finance</option>
390
+ <option>Energy</option>
391
+ <option>Consumer</option>
392
+ <option>Industrial</option>
393
+ <option>Telecom</option>
394
+ <option>REIT</option>
395
+ </select>
396
+ </div>
397
+ <div class="filter-group">
398
+ <label>Frequency</label>
399
+ <select id="fFreq" onchange="applyFilters()">
400
+ <option value="">All</option>
401
+ <option value="Monthly">Monthly</option>
402
+ <option value="Quarterly">Quarterly</option>
403
+ <option value="Semi-Annual">Semi-Annual</option>
404
+ <option value="Annual">Annual</option>
405
+ </select>
406
+ </div>
407
+ </div>
408
+
409
+ <div class="filter-section">
410
+ <div class="filter-section-title">โš™๏ธ Toggles</div>
411
+ <div class="toggle-row">
412
+ <span class="toggle-label">Exclude ETFs</span>
413
+ <label class="toggle"><input type="checkbox" id="tExclETF" onchange="applyFilters()"><span class="toggle-slider"></span></label>
414
+ </div>
415
+ <div class="toggle-row">
416
+ <span class="toggle-label">Exclude REITs</span>
417
+ <label class="toggle"><input type="checkbox" id="tExclREIT" onchange="applyFilters()"><span class="toggle-slider"></span></label>
418
+ </div>
419
+ <div class="toggle-row">
420
+ <span class="toggle-label">High Yield Only (&gt;4%)</span>
421
+ <label class="toggle"><input type="checkbox" id="tHighYield" onchange="applyFilters()"><span class="toggle-slider"></span></label>
422
+ </div>
423
+ <div class="toggle-row">
424
+ <span class="toggle-label">Consistent Payers Only</span>
425
+ <label class="toggle"><input type="checkbox" id="tConsistent" onchange="applyFilters()"><span class="toggle-slider"></span></label>
426
+ </div>
427
+ <div style="margin-top:.85rem">
428
+ <label style="font-size:.72rem;color:var(--text2);font-weight:600;margin-bottom:.35rem;display:block">Min Avg Volume</label>
429
+ <select id="fVolume" onchange="applyFilters()">
430
+ <option value="0">Any</option>
431
+ <option value="500000">500K+</option>
432
+ <option value="1000000">1M+</option>
433
+ <option value="5000000">5M+</option>
434
+ </select>
435
+ </div>
436
+ </div>
437
+
438
+ <button class="btn btn-ghost" style="width:100%;justify-content:center;margin-top:.5rem" onclick="resetFilters()">โ†บ Reset Filters</button>
439
+ </aside>
440
+
441
+ <!-- โ”€โ”€ Results Panel โ”€โ”€ -->
442
+ <div>
443
+ <!-- Sort + export bar -->
444
+ <div class="sort-bar">
445
+ <div class="results-count">Found <strong id="resultCount">โ€”</strong> dividend events</div>
446
+ <div class="sort-controls">
447
+ <span class="sort-label">Sort:</span>
448
+ <button class="sort-btn active" id="sb-date" onclick="setSort('daysOut',this)">Soonest</button>
449
+ <button class="sort-btn" id="sb-yield" onclick="setSort('yield',this)">Highest Yield</button>
450
+ <button class="sort-btn" id="sb-cap" onclick="setSort('marketCapRank',this)">Market Cap</button>
451
+ <button class="sort-btn" id="sb-cons" onclick="setSort('consistency',this)">Consistency</button>
452
+ <button class="export-btn" onclick="exportCSV()">โฌ‡ Export CSV</button>
453
+ </div>
454
+ </div>
455
+
456
+ <!-- Table -->
457
+ <div class="table-wrap table-loading" style="position:relative">
458
+ <div class="tbl-overlay hidden" id="tableOverlay">
459
+ <div style="text-align:center">
460
+ <div class="spin-lg"></div>
461
+ <div style="color:var(--text2);font-size:.8rem;margin-top:.75rem" id="overlayMsg">Fetching dividend dataโ€ฆ</div>
462
+ </div>
463
+ </div>
464
+ <div class="table-scroll">
465
+ <table id="divTable">
466
+ <thead><tr>
467
+ <th onclick="setSort('ticker',null)">Ticker <span class="si">โ‡…</span></th>
468
+ <th onclick="setSort('daysOut',null)">Ex-Div Date <span class="si">โ‡…</span></th>
469
+ <th onclick="setSort('amount',null)">Amount <span class="si">โ‡…</span></th>
470
+ <th onclick="setSort('yield',null)">Yield <span class="si">โ‡…</span></th>
471
+ <th onclick="setSort('price',null)">Price <span class="si">โ‡…</span></th>
472
+ <th onclick="setSort('chg1d',null)">1D % <span class="si">โ‡…</span></th>
473
+ <th>Pay Date</th>
474
+ <th>Frequency</th>
475
+ <th>Sector</th>
476
+ <th onclick="setSort('marketCapRank',null)">Mkt Cap <span class="si">โ‡…</span></th>
477
+ <th onclick="setSort('consistency',null)">Consistency <span class="si">โ‡…</span></th>
478
+ </tr></thead>
479
+ <tbody id="divBody"></tbody>
480
+ </table>
481
+ </div>
482
+ </div>
483
+
484
+ <!-- How to read note -->
485
+ <div class="howto-note" style="margin-top:1rem">
486
+ ๐Ÿ’ก <strong>How to read this:</strong> You must own shares <em>before the close of the day prior to the Ex-Dividend Date</em> to receive the dividend. The Payment Date is when cash hits your account. Breakeven = Stock Price โˆ’ Dividend Amount.
487
+ </div>
488
+ </div>
489
+ </div>
490
+
491
+ </main>
492
+
493
+ <!-- Mobile Bottom Nav -->
494
+ <nav class="bottom-nav">
495
+ <div class="bottom-nav-inner">
496
+ <a href="index.html" class="bottom-nav-item"><span class="bn-icon">๐Ÿ </span>Home</a>
497
+ <a href="portfolio.html" class="bottom-nav-item"><span class="bn-icon">๐Ÿ“Š</span>Portfolio</a>
498
+ <a href="picks.html" class="bottom-nav-item"><span class="bn-icon">๐Ÿน</span>Picks</a>
499
+ <a href="calculators.html" class="bottom-nav-item"><span class="bn-icon">๐Ÿงฎ</span>Calc</a>
500
+ <a href="dividends.html" class="bottom-nav-item active"><span class="bn-icon">๐Ÿ’ฐ</span>Divs</a>
501
+ </div>
502
+ </nav>
503
+
504
+ <!-- API Key Modal -->
505
+ <div class="modal-backdrop hidden" id="keyModal" onclick="if(event.target===this)hideKeyModal()">
506
+ <div class="modal-box">
507
+ <div class="modal-title">๐Ÿ”‘ Finnhub <span>API Key</span></div>
508
+ <div class="modal-sub">
509
+ Same key as Stock &amp; Option Picks page. Get a free key at
510
+ <a href="https://finnhub.io/register" target="_blank">finnhub.io/register</a>
511
+ โ€” takes 30 seconds, no credit card.<br><br>
512
+ Dividend data uses <code>/stock/dividend</code> + <code>/quote</code> endpoints (both free tier).
513
+ </div>
514
+ <input class="modal-input" id="keyInput" type="text"
515
+ placeholder="e.g. d0abc123xyz456..." autocomplete="off" spellcheck="false"/>
516
+ <div class="modal-actions">
517
+ <button class="btn btn-gold" onclick="confirmKey()">โœ… Save &amp; Fetch</button>
518
+ <button class="btn btn-ghost" onclick="hideKeyModal()">Cancel</button>
519
+ <button class="btn btn-ghost" onclick="clearKey()" style="margin-left:auto;color:var(--rose);border-color:var(--rose)">๐Ÿ—‘ Clear</button>
520
+ </div>
521
+ <div class="modal-note">
522
+ <strong>๐Ÿ”’ Privacy:</strong> Key stored only in your browser's localStorage. Never sent anywhere except Finnhub directly.
523
+ </div>
524
+ </div>
525
+ </div>
526
+
527
+ <!-- โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• SCRIPT โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -->
528
+ <script>
529
+ 'use strict';
530
+
531
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
532
+ CONSTANTS
533
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
534
+ const COOLDOWN_MS = 10 * 60 * 1000;
535
+ const CACHE_TTL = 15 * 60 * 1000;
536
+ const CACHE_KEY = 'fw_div_data';
537
+ const CACHE_TIME = 'fw_div_time';
538
+ const CD_KEY = 'fw_div_cd';
539
+ const KEY_STORE = 'fw_finnhub_key'; // shared with picks.html
540
+ const FH_BASE = 'https://finnhub.io/api/v1';
541
+
542
+ let FINNHUB_KEY = localStorage.getItem(KEY_STORE) || '';
543
+ let cdTimer = null;
544
+ let currentSort = { key:'daysOut', asc:true };
545
+ let activeDrawer = null; // ticker of open drawer
546
+ let histCharts = {}; // Chart instances keyed by ticker
547
+ let filteredData = [];
548
+
549
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
550
+ HELPERS
551
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
552
+ const $ = id => document.getElementById(id);
553
+ const fv = (id,v) => { const e=$(id); if(e) e.textContent=v; };
554
+ const hasKey = () => FINNHUB_KEY.length > 0;
555
+ const today = () => { const d=new Date(); d.setHours(0,0,0,0); return d; };
556
+ const addDays = (d,n) => { const r=new Date(d); r.setDate(r.getDate()+n); return r; };
557
+ const fmtDate = d => {
558
+ if(!d) return 'โ€”';
559
+ const dt = typeof d==='string' ? new Date(d) : d;
560
+ return dt.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'2-digit'});
561
+ };
562
+ const daysFromToday = dateStr => {
563
+ if(!dateStr) return 999;
564
+ const d = new Date(dateStr);
565
+ d.setHours(0,0,0,0);
566
+ return Math.round((d - today()) / 86400000);
567
+ };
568
+ const sleep = ms => new Promise(r => setTimeout(r, ms));
569
+
570
+ function exDivBadge(dateStr) {
571
+ const days = daysFromToday(dateStr);
572
+ const label = fmtDate(dateStr);
573
+ if (days < 0) return `<span class="exdiv-badge exdiv-upcoming" title="Already passed">${label} โœ“</span>`;
574
+ if (days <= 7) return `<span class="exdiv-badge exdiv-hot">๐Ÿ”ฅ ${label} (${days}d)</span>`;
575
+ if (days <= 14)return `<span class="exdiv-badge exdiv-soon">โšก ${label} (${days}d)</span>`;
576
+ return `<span class="exdiv-badge exdiv-upcoming">${label} (${days}d)</span>`;
577
+ }
578
+ function freqBadge(f) {
579
+ const m={Monthly:'freq-m',Quarterly:'freq-q','Semi-Annual':'freq-s',Annual:'freq-a'};
580
+ return `<span class="freq-badge ${m[f]||'freq-q'}">${f||'Quarterly'}</span>`;
581
+ }
582
+ function sectorBadge(s) {
583
+ const m={Technology:'badge-tech',Healthcare:'badge-health',Finance:'badge-finance',
584
+ Energy:'badge-energy',Consumer:'badge-consumer',Industrial:'badge-industrial',
585
+ REIT:'badge-reit',Telecom:'badge-telecom'};
586
+ return `<span class="badge ${m[s]||'badge-tech'}">${s}</span>`;
587
+ }
588
+ function yieldBar(y) {
589
+ const pct = Math.min(100, (y/10)*100);
590
+ const col = y>=6?'var(--amber)':y>=4?'var(--gold)':'var(--emerald)';
591
+ return `<div class="yield-bar-wrap">
592
+ <div class="yield-bar"><div class="yield-fill" style="width:${pct}%;background:${col}"></div></div>
593
+ <span class="yield-val" style="color:${col}">${y.toFixed(2)}%</span>
594
+ </div>`;
595
+ }
596
+ function consistencyDots(pct) {
597
+ const filled = Math.round(pct/12.5); // 8 dots = 100%
598
+ let html = '<div class="consistency-dots">';
599
+ for(let i=0;i<8;i++) html+=`<div class="c-dot ${i<filled?'filled':''}"></div>`;
600
+ html += `</div><span style="font-size:.7rem;color:var(--text2);margin-left:.3rem">${pct}%</span>`;
601
+ return html;
602
+ }
603
+ function chgCell(v){
604
+ if(v==null||isNaN(v)) return '<span style="color:var(--text2)">โ€”</span>';
605
+ if(v>0) return `<span class="up">โ–ฒ ${v.toFixed(2)}%</span>`;
606
+ if(v<0) return `<span class="down">โ–ผ ${Math.abs(v).toFixed(2)}%</span>`;
607
+ return '<span style="color:var(--text2)">0.00%</span>';
608
+ }
609
+ function fmtPrice(p){return p==null?'โ€”':'$'+p.toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2});}
610
+ function capLabel(r){return r<=1?'Large':r<=2?'Mid':'Small';}
611
+ function capBadge(r){
612
+ const cls=r<=1?'badge-tech':r<=2?'badge-health':'badge-industrial';
613
+ return `<span class="badge ${cls}">${capLabel(r)}</span>`;
614
+ }
615
+
616
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
617
+ SEED DATA
618
+ Ex-div dates ~8 weeks from May 7, 2026
619
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
620
+ const SEED = [
621
+ // === HOT (โ‰ค7 days) ===
622
+ {ticker:'AAPL', company:'Apple Inc.', sector:'Technology', exDivDate:'2026-05-09', payDate:'2026-05-15', recordDate:'2026-05-10', annDate:'2026-04-30', amount:0.25, yield:0.47, fwdYield:0.48, annualDiv:1.00, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:58000000, price:211.30, chg1d:0.4, consistency:100, history:[0.23,0.23,0.24,0.24,0.25,0.25,0.25,0.25]},
623
+ {ticker:'JNJ', company:'Johnson & Johnson', sector:'Healthcare', exDivDate:'2026-05-12', payDate:'2026-06-03', recordDate:'2026-05-13', annDate:'2026-04-15', amount:1.24, yield:3.12, fwdYield:3.15, annualDiv:4.96, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:7200000, price:158.40, chg1d:0.2, consistency:100, history:[1.19,1.19,1.24,1.24,1.24,1.24,1.24,1.24]},
624
+ {ticker:'O', company:'Realty Income Corp', sector:'REIT', exDivDate:'2026-05-13', payDate:'2026-05-31', recordDate:'2026-05-14', annDate:'2026-04-28', amount:0.264,yield:5.82, fwdYield:5.85, annualDiv:3.17, frequency:'Monthly', marketCapRank:1, isETF:false, isREIT:true, avgVolume:8100000, price:54.50, chg1d:-0.3, consistency:100, history:[0.257,0.258,0.258,0.260,0.261,0.262,0.263,0.264]},
625
+ {ticker:'MAIN', company:'Main Street Capital Corp', sector:'Finance', exDivDate:'2026-05-14', payDate:'2026-05-31', recordDate:'2026-05-15', annDate:'2026-04-20', amount:0.245,yield:5.92, fwdYield:5.95, annualDiv:2.94, frequency:'Monthly', marketCapRank:2, isETF:false, isREIT:false, avgVolume:1200000, price:49.60, chg1d:0.5, consistency:96, history:[0.235,0.235,0.240,0.240,0.240,0.245,0.245,0.245]},
626
+ // === SOON (8-14 days) ===
627
+ {ticker:'PG', company:'Procter & Gamble Co', sector:'Consumer', exDivDate:'2026-05-15', payDate:'2026-06-15', recordDate:'2026-05-16', annDate:'2026-04-10', amount:1.01, yield:2.48, fwdYield:2.50, annualDiv:4.04, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:6800000, price:162.70, chg1d:0.1, consistency:100, history:[0.94,0.94,0.97,0.97,1.01,1.01,1.01,1.01]},
628
+ {ticker:'KO', company:'Coca-Cola Company', sector:'Consumer', exDivDate:'2026-05-16', payDate:'2026-06-30', recordDate:'2026-05-17', annDate:'2026-04-18', amount:0.485,yield:3.14, fwdYield:3.16, annualDiv:1.94, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:14600000, price:61.80, chg1d:0.3, consistency:100, history:[0.46,0.46,0.46,0.475,0.475,0.485,0.485,0.485]},
629
+ {ticker:'VZ', company:'Verizon Communications', sector:'Telecom', exDivDate:'2026-05-19', payDate:'2026-06-30', recordDate:'2026-05-20', annDate:'2026-04-01', amount:0.675,yield:6.48, fwdYield:6.50, annualDiv:2.70, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:18000000, price:41.65, chg1d:-0.4, consistency:100, history:[0.665,0.665,0.665,0.665,0.670,0.675,0.675,0.675]},
630
+ {ticker:'T', company:'AT&T Inc', sector:'Telecom', exDivDate:'2026-05-21', payDate:'2026-06-30', recordDate:'2026-05-22', annDate:'2026-04-05', amount:0.2775,yield:5.28,fwdYield:5.30, annualDiv:1.11, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:32000000, price:21.00, chg1d:0.2, consistency:88, history:[0.2775,0.2775,0.2775,0.2775,0.2775,0.2775,0.2775,0.2775]},
631
+ // === UPCOMING (>14 days) ===
632
+ {ticker:'XOM', company:'ExxonMobil Corp', sector:'Energy', exDivDate:'2026-05-28', payDate:'2026-06-10', recordDate:'2026-05-29', annDate:'2026-04-25', amount:0.99, yield:3.35, fwdYield:3.38, annualDiv:3.96, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:18000000, price:118.25, chg1d:1.8, consistency:100, history:[0.91,0.91,0.91,0.95,0.95,0.99,0.99,0.99]},
633
+ {ticker:'CVX', company:'Chevron Corp', sector:'Energy', exDivDate:'2026-06-02', payDate:'2026-06-10', recordDate:'2026-06-03', annDate:'2026-04-30', amount:1.71, yield:4.26, fwdYield:4.28, annualDiv:6.84, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:9800000, price:160.20, chg1d:0.7, consistency:100, history:[1.51,1.51,1.63,1.63,1.71,1.71,1.71,1.71]},
634
+ {ticker:'MCD', company:"McDonald's Corp", sector:'Consumer', exDivDate:'2026-06-05', payDate:'2026-06-18', recordDate:'2026-06-06', annDate:'2026-05-01', amount:1.77, yield:2.43, fwdYield:2.44, annualDiv:7.08, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:3100000, price:291.60, chg1d:0.2, consistency:100, history:[1.67,1.67,1.67,1.73,1.73,1.77,1.77,1.77]},
635
+ {ticker:'WMT', company:'Walmart Inc', sector:'Consumer', exDivDate:'2026-06-09', payDate:'2026-07-01', recordDate:'2026-06-10', annDate:'2026-05-10', amount:0.235,yield:1.38, fwdYield:1.39, annualDiv:0.94, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:11200000, price:68.40, chg1d:0.3, consistency:100, history:[0.21,0.21,0.21,0.22,0.22,0.235,0.235,0.235]},
636
+ {ticker:'HD', company:'Home Depot Inc', sector:'Consumer', exDivDate:'2026-06-12', payDate:'2026-06-26', recordDate:'2026-06-13', annDate:'2026-05-15', amount:2.25, yield:2.52, fwdYield:2.53, annualDiv:9.00, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:4200000, price:357.40, chg1d:0.5, consistency:100, history:[2.09,2.09,2.09,2.03,2.25,2.25,2.25,2.25]},
637
+ {ticker:'GS', company:'Goldman Sachs Group', sector:'Finance', exDivDate:'2026-06-10', payDate:'2026-06-27', recordDate:'2026-06-11', annDate:'2026-05-05', amount:3.00, yield:2.60, fwdYield:2.62, annualDiv:12.00,frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:2800000, price:461.50, chg1d:-0.5, consistency:96, history:[2.75,2.75,2.75,3.00,3.00,3.00,3.00,3.00]},
638
+ {ticker:'IBM', company:'IBM Corp', sector:'Technology', exDivDate:'2026-06-16', payDate:'2026-06-30', recordDate:'2026-06-17', annDate:'2026-05-05', amount:1.67, yield:2.94, fwdYield:2.96, annualDiv:6.68, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:4800000, price:227.40, chg1d:0.3, consistency:100, history:[1.65,1.65,1.65,1.65,1.67,1.67,1.67,1.67]},
639
+ {ticker:'ABBV', company:'AbbVie Inc', sector:'Healthcare', exDivDate:'2026-06-19', payDate:'2026-07-01', recordDate:'2026-06-20', annDate:'2026-05-10', amount:1.64, yield:3.62, fwdYield:3.65, annualDiv:6.56, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:6200000, price:180.90, chg1d:0.8, consistency:100, history:[1.48,1.48,1.55,1.55,1.64,1.64,1.64,1.64]},
640
+ {ticker:'MMM', company:'3M Company', sector:'Industrial', exDivDate:'2026-06-17', payDate:'2026-06-30', recordDate:'2026-06-18', annDate:'2026-05-12', amount:0.70, yield:2.48, fwdYield:2.50, annualDiv:2.80, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:3900000, price:112.80, chg1d:0.1, consistency:88, history:[1.51,1.51,0.70,0.70,0.70,0.70,0.70,0.70]},
641
+ {ticker:'MO', company:'Altria Group Inc', sector:'Consumer', exDivDate:'2026-06-22', payDate:'2026-07-09', recordDate:'2026-06-23', annDate:'2026-05-18', amount:1.02, yield:7.91, fwdYield:7.95, annualDiv:4.08, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:9500000, price:51.58, chg1d:0.4, consistency:100, history:[0.94,0.94,0.94,0.98,0.98,1.02,1.02,1.02]},
642
+ {ticker:'JPM', company:'JPMorgan Chase & Co', sector:'Finance', exDivDate:'2026-06-25', payDate:'2026-07-10', recordDate:'2026-06-26', annDate:'2026-05-20', amount:1.40, yield:2.64, fwdYield:2.65, annualDiv:5.60, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:11800000, price:212.70, chg1d:-0.5, consistency:96, history:[1.15,1.15,1.25,1.25,1.40,1.40,1.40,1.40]},
643
+ {ticker:'BAC', company:'Bank of America Corp', sector:'Finance', exDivDate:'2026-06-27', payDate:'2026-07-14', recordDate:'2026-06-28', annDate:'2026-05-22', amount:0.26, yield:2.75, fwdYield:2.76, annualDiv:1.04, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:38000000, price:37.85, chg1d:0.3, consistency:92, history:[0.22,0.22,0.24,0.24,0.26,0.26,0.26,0.26]},
644
+ {ticker:'PEP', company:'PepsiCo Inc', sector:'Consumer', exDivDate:'2026-06-30', payDate:'2026-07-17', recordDate:'2026-07-01', annDate:'2026-05-28', amount:1.355,yield:3.55, fwdYield:3.58, annualDiv:5.42, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:5400000, price:152.60, chg1d:-0.2, consistency:100, history:[1.265,1.265,1.265,1.325,1.325,1.355,1.355,1.355]},
645
+ {ticker:'PM', company:'Philip Morris Intl', sector:'Consumer', exDivDate:'2026-07-01', payDate:'2026-07-18', recordDate:'2026-07-02', annDate:'2026-06-02', amount:1.35, yield:4.88, fwdYield:4.90, annualDiv:5.40, frequency:'Quarterly', marketCapRank:1, isETF:false, isREIT:false, avgVolume:5800000, price:110.45, chg1d:0.6, consistency:100, history:[1.27,1.27,1.27,1.31,1.31,1.35,1.35,1.35]},
646
+ {ticker:'STAG', company:'STAG Industrial Inc', sector:'REIT', exDivDate:'2026-05-13', payDate:'2026-05-30', recordDate:'2026-05-14', annDate:'2026-04-22', amount:0.124,yield:3.88, fwdYield:3.90, annualDiv:1.49, frequency:'Monthly', marketCapRank:2, isETF:false, isREIT:true, avgVolume:2900000, price:38.40, chg1d:0.3, consistency:100, history:[0.122,0.122,0.122,0.122,0.123,0.124,0.124,0.124]},
647
+ {ticker:'SCHD', company:'Schwab US Dividend ETF', sector:'Consumer', exDivDate:'2026-06-20', payDate:'2026-06-26', recordDate:'2026-06-21', annDate:'2026-06-01', amount:0.778,yield:3.52, fwdYield:3.54, annualDiv:3.11, frequency:'Quarterly', marketCapRank:1, isETF:true, isREIT:false, avgVolume:9800000, price:88.20, chg1d:0.2, consistency:92, history:[0.72,0.72,0.745,0.745,0.765,0.778,0.778,0.778]},
648
+ {ticker:'JEPI', company:'JPMorgan Equity Premium ETF',sector:'Finance', exDivDate:'2026-06-25', payDate:'2026-07-01', recordDate:'2026-06-26', annDate:'2026-06-05', amount:0.42, yield:6.12, fwdYield:6.15, annualDiv:5.04, frequency:'Monthly', marketCapRank:1, isETF:true, isREIT:false, avgVolume:11200000, price:56.30, chg1d:0.1, consistency:88, history:[0.40,0.41,0.41,0.42,0.42,0.42,0.42,0.42]},
649
+ ];
650
+
651
+ /* Working copy โ€” prices updated by Finnhub */
652
+ let DATA = JSON.parse(JSON.stringify(SEED));
653
+
654
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
655
+ CACHE
656
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
657
+ function saveCache(d) {
658
+ try { localStorage.setItem(CACHE_KEY,JSON.stringify(d)); localStorage.setItem(CACHE_TIME,Date.now()); } catch{}
659
+ }
660
+ function loadCache() {
661
+ try {
662
+ const t=+localStorage.getItem(CACHE_TIME)||0;
663
+ if(Date.now()-t>CACHE_TTL) return null;
664
+ const r=localStorage.getItem(CACHE_KEY);
665
+ return r?JSON.parse(r):null;
666
+ } catch{ return null; }
667
+ }
668
+ function remainingCD() {
669
+ const t=+localStorage.getItem(CD_KEY)||0;
670
+ return Math.max(0, COOLDOWN_MS-(Date.now()-t));
671
+ }
672
+ function setCD(){ try{localStorage.setItem(CD_KEY,Date.now());}catch{} }
673
+ function canFetch(){ return remainingCD()===0; }
674
+
675
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
676
+ FINNHUB FETCH
677
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
678
+ async function finnhubQuote(sym) {
679
+ const url=`${FH_BASE}/quote?symbol=${sym}&token=${FINNHUB_KEY}`;
680
+ const r=await fetch(url,{signal:AbortSignal.timeout(8000)});
681
+ if(!r.ok) throw new Error('HTTP '+r.status);
682
+ const j=await r.json();
683
+ return j.c>0 ? {price:j.c, chg1d:j.dp||0} : null;
684
+ }
685
+
686
+ async function finnhubDividend(sym) {
687
+ const now = new Date();
688
+ const from = new Date(now); from.setMonth(from.getMonth()-3);
689
+ const to = new Date(now); to.setMonth(to.getMonth()+3);
690
+ const fmt = d => d.toISOString().split('T')[0];
691
+ const url = `${FH_BASE}/stock/dividend?symbol=${sym}&from=${fmt(from)}&to=${fmt(to)}&token=${FINNHUB_KEY}`;
692
+ try {
693
+ const r=await fetch(url,{signal:AbortSignal.timeout(10000)});
694
+ if(!r.ok) return null;
695
+ const j=await r.json();
696
+ const divs=(j.data||[]).sort((a,b)=>new Date(b.date)-new Date(a.date));
697
+ if(!divs.length) return null;
698
+ const upcoming=divs.find(d=>new Date(d.date)>=new Date());
699
+ const recent =divs[0];
700
+ return { exDivDate: upcoming?.date||recent?.date, amount: upcoming?.amount||recent?.amount, payDate: upcoming?.payDate||recent?.payDate, recordDate: upcoming?.recordDate||recent?.recordDate };
701
+ } catch { return null; }
702
+ }
703
+
704
+ async function fetchLiveData() {
705
+ if(!hasKey()) { showKeyModal(); return; }
706
+ showOverlay(true,'Fetching quotes and dividend dataโ€ฆ');
707
+ updateStatus('loading');
708
+
709
+ const CONCURRENCY=4;
710
+ const tickers=DATA.map(d=>d.ticker);
711
+ let successCount=0;
712
+
713
+ for(let i=0;i<tickers.length;i+=CONCURRENCY) {
714
+ const chunk=tickers.slice(i,i+CONCURRENCY);
715
+ await Promise.all(chunk.map(async sym => {
716
+ try {
717
+ const [q,div]=await Promise.all([finnhubQuote(sym), finnhubDividend(sym)]);
718
+ const idx=DATA.findIndex(d=>d.ticker===sym);
719
+ if(idx<0) return;
720
+ if(q) { DATA[idx].price=q.price; DATA[idx].chg1d=q.chg1d; }
721
+ if(div) {
722
+ if(div.exDivDate) DATA[idx].exDivDate=div.exDivDate;
723
+ if(div.amount) DATA[idx].amount=div.amount;
724
+ if(div.payDate) DATA[idx].payDate=div.payDate;
725
+ if(div.recordDate)DATA[idx].recordDate=div.recordDate;
726
+ // Recalculate yield
727
+ if(q&&div.amount) DATA[idx].yield=parseFloat(((div.amount*4)/q.price*100).toFixed(2));
728
+ }
729
+ successCount++;
730
+ } catch(e){ console.warn('[Divs]',sym,e.message); }
731
+ }));
732
+ showOverlay(true,`Fetched ${Math.min(i+CONCURRENCY,tickers.length)}/${tickers.length} tickersโ€ฆ`);
733
+ if(i+CONCURRENCY<tickers.length) await sleep(280); // ~rate-limit safe
734
+ }
735
+
736
+ saveCache(DATA);
737
+ showOverlay(false);
738
+ updateStatus(successCount>0?'live':'error');
739
+ applyFilters();
740
+ }
741
+
742
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
743
+ USER ACTIONS
744
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
745
+ async function userFetch() {
746
+ if(!hasKey()) { showKeyModal(); return; }
747
+ if(!canFetch()) { flashCD(); return; }
748
+
749
+ // Check cache first
750
+ const cached=loadCache();
751
+ if(cached) {
752
+ DATA=cached;
753
+ updateStatus('cached');
754
+ applyFilters();
755
+ startCDDisplay();
756
+ return;
757
+ }
758
+
759
+ setCD();
760
+ startCDDisplay();
761
+ await fetchLiveData();
762
+ }
763
+
764
+ function startCDDisplay() {
765
+ clearInterval(cdTimer);
766
+ const btn=$('fetchBtn'); const lbl=$('fetchLabel'); const ring=$('cdRing');
767
+ btn.disabled=true; ring.classList.add('visible');
768
+ cdTimer=setInterval(()=>{
769
+ const r=remainingCD();
770
+ if(r<=0){ clearInterval(cdTimer); btn.disabled=false; ring.classList.remove('visible'); lbl.textContent='โฌ‡๏ธ Fetch Live Data'; return; }
771
+ const m=Math.floor(r/60000), s=Math.floor((r%60000)/1000);
772
+ lbl.textContent=`โณ ${m}m ${s}s`;
773
+ },1000);
774
+ }
775
+
776
+ function flashCD() {
777
+ const lbl=$('fetchLabel'), orig=lbl.textContent;
778
+ const r=remainingCD();
779
+ lbl.textContent=`โ›” Wait ${Math.ceil(r/60000)}m`;
780
+ setTimeout(()=>lbl.textContent=orig,2000);
781
+ }
782
+
783
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
784
+ STATUS BAR
785
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
786
+ function updateStatus(state) {
787
+ const badge=$('statusBadge'), msg=$('statusMsg');
788
+ const ts=+localStorage.getItem(CACHE_TIME)||0;
789
+ const ago=ts?Math.round((Date.now()-ts)/60000):null;
790
+ const map={
791
+ loading:{cls:'seed', icon:'โณ', text:'Fetching from Finnhubโ€ฆ'},
792
+ live: {cls:'live', icon:'๐ŸŸข', text:`Finnhub ยท ${ago===0?'just now':ago+'min ago'} ยท real-time quotes`},
793
+ cached: {cls:'cached', icon:'๐Ÿ“ฆ', text:`Cached ยท ${ago} min ago ยท press Fetch to refresh`},
794
+ error: {cls:'error', icon:'โŒ', text:'Finnhub error โ€” check API key. Showing best available data.'},
795
+ nokey: {cls:'seed', icon:'๐Ÿ”‘', text:'No API key โ€” click Fetch Live Data to enter your free Finnhub key'},
796
+ seed: {cls:'seed', icon:'๐Ÿ“ฆ', text:'Seed data โ€” press Fetch Live Data to load real Finnhub prices'},
797
+ };
798
+ const s=map[state]||map.seed;
799
+ badge.className=`data-badge ${s.cls}`;
800
+ badge.textContent=`${s.icon} ${state==='live'?'Live':state==='cached'?'Cached':state==='error'?'Error':state==='nokey'?'No Key':'Seed Data'}`;
801
+ msg.textContent=s.text;
802
+ }
803
+
804
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
805
+ OVERLAY
806
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
807
+ function showOverlay(show, msg='') {
808
+ const el=$('tableOverlay');
809
+ el.classList.toggle('hidden',!show);
810
+ if(msg) $('overlayMsg').textContent=msg;
811
+ }
812
+
813
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
814
+ FILTERS & RENDER
815
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
816
+ function getFilterValues() {
817
+ return {
818
+ days: +$('fDays').value,
819
+ yieldMin: +$('fYieldMin').value,
820
+ yieldMax: +$('fYieldMax').value,
821
+ yieldType: $('fYieldType').value,
822
+ caps: [...document.querySelectorAll('.f-cap:checked')].map(e=>e.value),
823
+ sector: $('fSector').value,
824
+ freq: $('fFreq').value,
825
+ exclETF: $('tExclETF').checked,
826
+ exclREIT: $('tExclREIT').checked,
827
+ highYield: $('tHighYield').checked,
828
+ consistent: $('tConsistent').checked,
829
+ volume: +$('fVolume').value,
830
+ q: ($('globalSearch').value||'').trim().toUpperCase(),
831
+ };
832
+ }
833
+
834
+ function applyFilters() {
835
+ const f=getFilterValues();
836
+ const td=today();
837
+ const maxDate=addDays(td,f.days);
838
+
839
+ filteredData = DATA.filter(d => {
840
+ const exDate = new Date(d.exDivDate);
841
+ exDate.setHours(0,0,0,0);
842
+ if(exDate<td || exDate>maxDate) return false;
843
+
844
+ const y = f.yieldType==='fwd' ? (d.fwdYield||d.yield) : d.yield;
845
+ if(y<f.yieldMin || y>f.yieldMax) return false;
846
+ if(!f.caps.includes(capLabel(d.marketCapRank))) return false;
847
+ if(f.sector && d.sector!==f.sector) return false;
848
+ if(f.freq && d.frequency!==f.freq) return false;
849
+ if(f.exclETF && d.isETF) return false;
850
+ if(f.exclREIT && d.isREIT) return false;
851
+ if(f.highYield && y<4) return false;
852
+ if(f.consistent && d.consistency<90) return false;
853
+ if(d.avgVolume<f.volume) return false;
854
+ if(f.q && !d.ticker.includes(f.q) && !d.company.toUpperCase().includes(f.q)) return false;
855
+ return true;
856
+ });
857
+
858
+ // Sort
859
+ filteredData.sort((a,b) => {
860
+ const k=currentSort.key;
861
+ let av=a[k], bv=b[k];
862
+ if(k==='yield') { av=f.yieldType==='fwd'?(a.fwdYield||a.yield):a.yield; bv=f.yieldType==='fwd'?(b.fwdYield||b.yield):b.yield; }
863
+ if(k==='daysOut') { av=daysFromToday(a.exDivDate); bv=daysFromToday(b.exDivDate); }
864
+ if(typeof av==='string') return currentSort.asc?av.localeCompare(bv):bv.localeCompare(av);
865
+ return currentSort.asc ? av-bv : bv-av;
866
+ });
867
+
868
+ renderTable();
869
+ updateStats();
870
+ }
871
+
872
+ function renderTable() {
873
+ const tbody=$('divBody');
874
+ if(!filteredData.length) {
875
+ tbody.innerHTML=`<tr><td colspan="11"><div class="empty-state"><div class="ei">๐Ÿ“ญ</div><p>No dividend events match your filters.<br><span style="font-size:.8rem">Try widening the date range or yield window.</span></p></div></td></tr>`;
876
+ $('resultCount').textContent='0';
877
+ return;
878
+ }
879
+ $('resultCount').textContent=filteredData.length;
880
+
881
+ tbody.innerHTML = filteredData.map(d => {
882
+ const yld = d.yield;
883
+ const rowId = 'row-' + d.ticker;
884
+ const drawId = 'draw-' + d.ticker;
885
+ return `<tr id="${rowId}" onclick="toggleDrawer('${d.ticker}')">
886
+ <td><div class="ticker-cell"><span class="ticker-sym">${d.ticker}${d.isETF?' <span style="font-size:.6rem;color:var(--text2)">ETF</span>':''}</span><span class="ticker-name">${d.company}</span></div></td>
887
+ <td>${exDivBadge(d.exDivDate)}</td>
888
+ <td style="font-family:monospace;font-weight:700;color:var(--gold)">$${d.amount.toFixed(3)}</td>
889
+ <td>${yieldBar(yld)}</td>
890
+ <td style="font-family:monospace">${fmtPrice(d.price)}</td>
891
+ <td>${chgCell(d.chg1d)}</td>
892
+ <td style="color:var(--text2);font-size:.8rem">${fmtDate(d.payDate)}</td>
893
+ <td>${freqBadge(d.frequency)}</td>
894
+ <td>${sectorBadge(d.sector)}</td>
895
+ <td>${capBadge(d.marketCapRank)}</td>
896
+ <td>${consistencyDots(d.consistency)}</td>
897
+ </tr>
898
+ <tr class="drawer-row" id="${drawId}">
899
+ <td class="drawer-cell" colspan="11">
900
+ <div class="drawer-inner" id="drawerContent-${d.ticker}"></div>
901
+ </td>
902
+ </tr>`;
903
+ }).join('');
904
+ }
905
+
906
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
907
+ DRAWER
908
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
909
+ function toggleDrawer(ticker) {
910
+ const drawEl = document.getElementById('draw-' + ticker);
911
+ const rowEl = document.getElementById('row-' + ticker);
912
+ if(!drawEl) return;
913
+
914
+ const isOpen = drawEl.classList.contains('open');
915
+
916
+ // Close any open drawer
917
+ if(activeDrawer && activeDrawer!==ticker) {
918
+ const prev = document.getElementById('draw-' + activeDrawer);
919
+ const prevRow = document.getElementById('row-' + activeDrawer);
920
+ if(prev) prev.classList.remove('open');
921
+ if(prevRow) prevRow.classList.remove('expanded');
922
+ // Destroy chart
923
+ if(histCharts[activeDrawer]) { histCharts[activeDrawer].destroy(); delete histCharts[activeDrawer]; }
924
+ }
925
+
926
+ if(isOpen) {
927
+ drawEl.classList.remove('open');
928
+ rowEl.classList.remove('expanded');
929
+ if(histCharts[ticker]) { histCharts[ticker].destroy(); delete histCharts[ticker]; }
930
+ activeDrawer=null;
931
+ } else {
932
+ drawEl.classList.add('open');
933
+ rowEl.classList.add('expanded');
934
+ activeDrawer=ticker;
935
+ renderDrawer(ticker);
936
+ }
937
+ }
938
+
939
+ function renderDrawer(ticker) {
940
+ const d = DATA.find(x=>x.ticker===ticker);
941
+ if(!d) return;
942
+ const el = document.getElementById('drawerContent-'+ticker);
943
+ const days = daysFromToday(d.exDivDate);
944
+ const annDiv = d.annualDiv.toFixed(2);
945
+ const breakeven = d.price ? (d.price - d.amount).toFixed(2) : 'โ€”';
946
+
947
+ el.innerHTML = `
948
+ <div class="drawer-grid">
949
+ <div>
950
+ <div class="drawer-section-title">๐Ÿ“… Upcoming Dividend Event</div>
951
+ <div class="div-event-card">
952
+ <div class="event-row"><span class="event-label">Ex-Dividend Date</span><span class="event-val" style="color:${days<=7?'var(--emerald)':days<=14?'var(--amber)':'var(--text)'}">${fmtDate(d.exDivDate)} (${days}d away)</span></div>
953
+ <div class="event-row"><span class="event-label">Record Date</span><span class="event-val">${fmtDate(d.recordDate)}</span></div>
954
+ <div class="event-row"><span class="event-label">Payment Date</span><span class="event-val">${fmtDate(d.payDate)}</span></div>
955
+ <div class="event-row"><span class="event-label">Announced</span><span class="event-val">${fmtDate(d.annDate)}</span></div>
956
+ <div class="event-row"><span class="event-label">Dividend Amount</span><span class="event-val" style="color:var(--gold)">$${d.amount.toFixed(3)} / share</span></div>
957
+ <div class="event-row"><span class="event-label">Annual Dividend</span><span class="event-val">$${annDiv}</span></div>
958
+ <div class="event-row"><span class="event-label">Frequency</span><span class="event-val">${d.frequency}</span></div>
959
+ <div class="event-row"><span class="event-label">Breakeven Price</span><span class="event-val">$${breakeven}</span></div>
960
+ </div>
961
+ <div style="margin-top:.6rem;font-size:.72rem;color:var(--text2);line-height:1.5">
962
+ โš ๏ธ <strong>Must own shares by:</strong> Close of <strong style="color:var(--amber)">${fmtDate(addDays(new Date(d.exDivDate),-1))}</strong> to receive this dividend.
963
+ </div>
964
+ </div>
965
+ <div>
966
+ <div class="drawer-section-title">๐Ÿ“Š Yield &amp; Valuation</div>
967
+ <div class="drawer-stat-row"><span class="drawer-stat-label">TTM Yield</span><span class="drawer-stat-val" style="color:var(--gold)">${d.yield.toFixed(2)}%</span></div>
968
+ <div class="drawer-stat-row"><span class="drawer-stat-label">Forward Yield</span><span class="drawer-stat-val" style="color:var(--gold)">${(d.fwdYield||d.yield).toFixed(2)}%</span></div>
969
+ <div class="drawer-stat-row"><span class="drawer-stat-label">Annual Dividend</span><span class="drawer-stat-val">$${annDiv}</span></div>
970
+ <div class="drawer-stat-row"><span class="drawer-stat-label">Current Price</span><span class="drawer-stat-val">${fmtPrice(d.price)}</span></div>
971
+ <div class="drawer-stat-row"><span class="drawer-stat-label">1D Change</span><span class="drawer-stat-val">${chgCell(d.chg1d)}</span></div>
972
+ <div class="drawer-stat-row"><span class="drawer-stat-label">Market Cap</span><span class="drawer-stat-val">${capLabel(d.marketCapRank)}</span></div>
973
+ <div class="drawer-stat-row"><span class="drawer-stat-label">Avg Volume</span><span class="drawer-stat-val">${(d.avgVolume/1e6).toFixed(1)}M</span></div>
974
+ <div class="drawer-stat-row"><span class="drawer-stat-label">Payout Consistency</span><span class="drawer-stat-val" style="color:var(--emerald)">${d.consistency}%</span></div>
975
+ <div class="drawer-stat-row"><span class="drawer-stat-label">Sector</span><span class="drawer-stat-val">${d.sector}</span></div>
976
+ <div class="drawer-stat-row"><span class="drawer-stat-label">Is ETF</span><span class="drawer-stat-val">${d.isETF?'Yes':'No'}</span></div>
977
+ <div class="drawer-stat-row"><span class="drawer-stat-label">Is REIT</span><span class="drawer-stat-val">${d.isREIT?'Yes':'No'}</span></div>
978
+ </div>
979
+ <div>
980
+ <div class="drawer-section-title">๐Ÿ“ˆ Dividend History (Last 8 Qtrs)</div>
981
+ <div class="hist-chart-wrap"><canvas id="histChart-${ticker}"></canvas></div>
982
+ <div style="margin-top:.5rem;font-size:.72rem;color:var(--text2)">
983
+ Each bar = one dividend payment. Height = per-share amount.
984
+ </div>
985
+ <div style="margin-top:.85rem">
986
+ <a href="https://finance.yahoo.com/quote/${ticker}" target="_blank"
987
+ class="btn btn-ghost" style="font-size:.78rem;padding:.35rem .75rem;text-decoration:none">
988
+ ๐Ÿ“Š Yahoo Finance โ†’
989
+ </a>
990
+ <a href="https://www.dividend.com/stocks/${ticker.toLowerCase()}" target="_blank"
991
+ class="btn btn-ghost" style="font-size:.78rem;padding:.35rem .75rem;text-decoration:none;margin-left:.4rem">
992
+ ๐Ÿ’ฐ Dividend.com โ†’
993
+ </a>
994
+ </div>
995
+ </div>
996
+ </div>`;
997
+
998
+ // Draw history chart
999
+ setTimeout(() => {
1000
+ const canvas = document.getElementById('histChart-'+ticker);
1001
+ if(!canvas || !d.history?.length) return;
1002
+ if(histCharts[ticker]) histCharts[ticker].destroy();
1003
+ const trending = d.history[d.history.length-1] >= d.history[0];
1004
+ histCharts[ticker] = new Chart(canvas, {
1005
+ type:'bar',
1006
+ data:{
1007
+ labels: d.history.map((_,i)=>`Q${i+1}`),
1008
+ datasets:[{
1009
+ data: d.history,
1010
+ backgroundColor: d.history.map(v => v===Math.max(...d.history)?'rgba(251,191,36,.9)':'rgba(251,191,36,.4)'),
1011
+ borderColor:'rgba(251,191,36,.6)',
1012
+ borderWidth:1,
1013
+ borderRadius:3,
1014
+ }]
1015
+ },
1016
+ options:{
1017
+ responsive:true, maintainAspectRatio:false, animation:false,
1018
+ plugins:{legend:{display:false},tooltip:{callbacks:{label:c=>`$${c.raw.toFixed(3)}`}}},
1019
+ scales:{
1020
+ x:{grid:{display:false},ticks:{color:'#94a3b8',font:{size:10}}},
1021
+ y:{grid:{color:'rgba(255,255,255,.05)'},ticks:{color:'#94a3b8',font:{size:10},callback:v=>'$'+v.toFixed(2)}}
1022
+ }
1023
+ }
1024
+ });
1025
+ }, 50);
1026
+ }
1027
+
1028
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1029
+ SORT
1030
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
1031
+ function setSort(key, btn) {
1032
+ if(currentSort.key===key) currentSort.asc=!currentSort.asc;
1033
+ else { currentSort={key,asc:key==='daysOut'}; }
1034
+ // Update sort buttons
1035
+ document.querySelectorAll('.sort-btn').forEach(b=>b.classList.remove('active'));
1036
+ if(btn) btn.classList.add('active');
1037
+ applyFilters();
1038
+ }
1039
+
1040
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1041
+ STAT STRIP
1042
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
1043
+ function updateStats() {
1044
+ fv('statTotal', filteredData.length);
1045
+ if(!filteredData.length) { fv('statAvgYield','โ€”'); fv('statAvgDays','โ€”'); fv('statHighYield','โ€”'); fv('statThisWeek','โ€”'); return; }
1046
+
1047
+ const avgY = filteredData.reduce((a,d)=>a+d.yield,0)/filteredData.length;
1048
+ fv('statAvgYield', avgY.toFixed(2)+'%');
1049
+
1050
+ const avgD = filteredData.reduce((a,d)=>a+daysFromToday(d.exDivDate),0)/filteredData.length;
1051
+ fv('statAvgDays', Math.round(avgD)+'d');
1052
+
1053
+ const high = filteredData.filter(d=>d.yield>=4).length;
1054
+ fv('statHighYield', high);
1055
+
1056
+ const week = filteredData.filter(d=>daysFromToday(d.exDivDate)<=7).length;
1057
+ fv('statThisWeek', week);
1058
+ }
1059
+
1060
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1061
+ EXPORT CSV
1062
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
1063
+ function exportCSV() {
1064
+ const headers=['Ticker','Company','Sector','Ex-Div Date','Pay Date','Amount','Yield%','Annual Div','Frequency','Market Cap','Price','1D%','Consistency%','Is ETF','Is REIT'];
1065
+ const rows = filteredData.map(d=>[
1066
+ d.ticker, `"${d.company}"`, d.sector, d.exDivDate, d.payDate,
1067
+ d.amount, d.yield, d.annualDiv, d.frequency, capLabel(d.marketCapRank),
1068
+ d.price||'', d.chg1d||'', d.consistency, d.isETF?'Yes':'No', d.isREIT?'Yes':'No'
1069
+ ]);
1070
+ const csv=[headers,...rows].map(r=>r.join(',')).join('\n');
1071
+ const a=document.createElement('a');
1072
+ a.href='data:text/csv;charset=utf-8,'+encodeURIComponent(csv);
1073
+ a.download=`finwise_dividends_${new Date().toISOString().slice(0,10)}.csv`;
1074
+ a.click();
1075
+ }
1076
+
1077
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1078
+ RESET FILTERS
1079
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
1080
+ function resetFilters() {
1081
+ $('fDays').value=56; fv('fDaysVal','56d (~8wk)');
1082
+ $('fYieldMin').value=0; fv('fYieldMinVal','0%');
1083
+ $('fYieldMax').value=15; fv('fYieldMaxVal','15%');
1084
+ $('fSector').value=''; $('fFreq').value=''; $('fVolume').value=0;
1085
+ $('tExclETF').checked=false; $('tExclREIT').checked=false;
1086
+ $('tHighYield').checked=false; $('tConsistent').checked=false;
1087
+ $('globalSearch').value='';
1088
+ document.querySelectorAll('.f-cap').forEach(c=>c.checked=true);
1089
+ applyFilters();
1090
+ }
1091
+
1092
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1093
+ API KEY MODAL
1094
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
1095
+ function showKeyModal(){ $('keyModal').classList.remove('hidden'); $('keyInput').value=FINNHUB_KEY; setTimeout(()=>$('keyInput').focus(),50); }
1096
+ function hideKeyModal(){ $('keyModal').classList.add('hidden'); }
1097
+ async function confirmKey(){
1098
+ const k=$('keyInput').value.trim();
1099
+ if(!k){ $('keyInput').style.borderColor='var(--rose)'; return; }
1100
+ FINNHUB_KEY=k; localStorage.setItem(KEY_STORE,k);
1101
+ hideKeyModal();
1102
+ setCD(); startCDDisplay();
1103
+ await fetchLiveData();
1104
+ }
1105
+ function clearKey(){ FINNHUB_KEY=''; localStorage.removeItem(KEY_STORE); $('keyInput').value=''; updateStatus('nokey'); }
1106
+
1107
+ document.addEventListener('keydown', e=>{
1108
+ if(e.key==='Escape') hideKeyModal();
1109
+ });
1110
+ $('keyInput')?.addEventListener('keydown',e=>{ if(e.key==='Enter') confirmKey(); });
1111
+
1112
+ /* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1113
+ INIT
1114
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */
1115
+ document.addEventListener('DOMContentLoaded',()=>{
1116
+ // Load from cache if fresh
1117
+ const cached=loadCache();
1118
+ if(cached) { DATA=cached; updateStatus('cached'); }
1119
+ else updateStatus(hasKey()?'seed':'nokey');
1120
+
1121
+ // Restore cooldown
1122
+ if(remainingCD()>0) startCDDisplay();
1123
+
1124
+ applyFilters();
1125
+ });
1126
+ </script>
1127
+ </body>
1128
+ </html>
index.html CHANGED
@@ -189,6 +189,8 @@
189
  <a href="insights.html" class="nav-item"><span class="nav-icon">๐Ÿ’ก</span> Insights <span class="nav-badge">New</span></a>
190
  <!-- โœ… NEW: Stock & Option Picks -->
191
  <a href="picks.html" class="nav-item"><span class="nav-icon">๐Ÿน</span> Stock &amp; Option Picks <span class="nav-badge" style="background:var(--emerald);color:#000">New</span></a>
 
 
192
  </div>
193
  <div class="sidebar-footer">
194
  <div class="market-ticker">Live Market</div>
@@ -349,6 +351,15 @@
349
  <a href="picks.html" class="btn btn-primary" style="white-space:nowrap;flex-shrink:0">Explore Picks โ†’</a>
350
  </div>
351
 
 
 
 
 
 
 
 
 
 
352
  </main>
353
  </div>
354
 
@@ -359,9 +370,9 @@
359
  <a href="portfolio.html" class="bottom-nav-item"><span class="bnav-icon">๐Ÿ“Š</span>Portfolio</a>
360
  <a href="risk.html" class="bottom-nav-item"><span class="bnav-icon">๐ŸŽฏ</span>Risk</a>
361
  <a href="tracker.html" class="bottom-nav-item"><span class="bnav-icon">๐Ÿ“ˆ</span>Track</a>
362
- <a href="calculators.html" class="bottom-nav-item"><span class="bnav-icon">๐Ÿงฎ</span>Calc</a>
363
- <!-- โœ… NEW -->
364
  <a href="picks.html" class="bottom-nav-item"><span class="bnav-icon">๐Ÿน</span>Picks</a>
 
 
365
  </div>
366
  </nav>
367
 
 
189
  <a href="insights.html" class="nav-item"><span class="nav-icon">๐Ÿ’ก</span> Insights <span class="nav-badge">New</span></a>
190
  <!-- โœ… NEW: Stock & Option Picks -->
191
  <a href="picks.html" class="nav-item"><span class="nav-icon">๐Ÿน</span> Stock &amp; Option Picks <span class="nav-badge" style="background:var(--emerald);color:#000">New</span></a>
192
+ <!-- โœ… NEW: Dividend Harvesting -->
193
+ <a href="dividends.html" class="nav-item"><span class="nav-icon">๐Ÿ’ฐ</span> Dividend Harvesting <span class="nav-badge" style="background:var(--gold,#fbbf24);color:#000">New</span></a>
194
  </div>
195
  <div class="sidebar-footer">
196
  <div class="market-ticker">Live Market</div>
 
351
  <a href="picks.html" class="btn btn-primary" style="white-space:nowrap;flex-shrink:0">Explore Picks โ†’</a>
352
  </div>
353
 
354
+ <!-- โœ… NEW: Dividend Harvesting CTA Banner -->
355
+ <div class="picks-cta fade-in" style="background:linear-gradient(135deg,rgba(251,191,36,.1),rgba(16,185,129,.06));border-color:rgba(251,191,36,.4);margin-top:12px">
356
+ <div>
357
+ <div class="picks-cta-title">๐Ÿ’ฐ Dividend Harvesting โ€” Track Upcoming Payouts</div>
358
+ <div class="picks-cta-sub">Find stocks paying dividends in the next 6โ€“8 weeks. Filter by yield, sector, frequency and ex-div date. Includes payout history, consistency scores and CSV export.</div>
359
+ </div>
360
+ <a href="dividends.html" class="btn btn-primary" style="white-space:nowrap;flex-shrink:0;background:#fbbf24;color:#000">Explore Dividends โ†’</a>
361
+ </div>
362
+
363
  </main>
364
  </div>
365
 
 
370
  <a href="portfolio.html" class="bottom-nav-item"><span class="bnav-icon">๐Ÿ“Š</span>Portfolio</a>
371
  <a href="risk.html" class="bottom-nav-item"><span class="bnav-icon">๐ŸŽฏ</span>Risk</a>
372
  <a href="tracker.html" class="bottom-nav-item"><span class="bnav-icon">๐Ÿ“ˆ</span>Track</a>
 
 
373
  <a href="picks.html" class="bottom-nav-item"><span class="bnav-icon">๐Ÿน</span>Picks</a>
374
+ <!-- โœ… NEW -->
375
+ <a href="dividends.html" class="bottom-nav-item"><span class="bnav-icon">๐Ÿ’ฐ</span>Divs</a>
376
  </div>
377
  </nav>
378