claude-roi 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,59 +4,214 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Codelens AI — Agent Productivity Dashboard</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
7
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet">
8
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
9
+ <script>
10
+ (function() {
11
+ var pref = localStorage.getItem('codelens-theme') || 'system';
12
+ var resolved = pref === 'system'
13
+ ? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
14
+ : pref;
15
+ if (resolved === 'light') document.documentElement.setAttribute('data-theme', 'light');
16
+ })();
17
+ </script>
9
18
  <style>
10
19
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11
20
 
21
+ @property --grade-deg {
22
+ syntax: '<angle>';
23
+ initial-value: 0deg;
24
+ inherits: false;
25
+ }
26
+
12
27
  :root {
13
- --bg-primary: #0d1117;
14
- --bg-secondary: #161b22;
15
- --bg-card: #1c2128;
16
- --bg-hover: #262c36;
17
- --border: #30363d;
18
- --text-primary: #e6edf3;
19
- --text-secondary: #8b949e;
20
- --text-muted: #6e7681;
21
- --accent-green: #3fb950;
22
- --accent-red: #f85149;
23
- --accent-orange: #d29922;
24
- --accent-blue: #58a6ff;
25
- --accent-purple: #bc8cff;
26
- --accent-cyan: #39d2c0;
27
- --grade-a: #3fb950;
28
- --grade-b: #58a6ff;
29
- --grade-c: #d29922;
28
+ --bg-primary: #0a0e17;
29
+ --bg-secondary: #111827;
30
+ --bg-card: #151d2b;
31
+ --bg-hover: #1e2a3a;
32
+ --bg-elevated: #1a2332;
33
+ --border: #1f2d3d;
34
+ --border-subtle: #162030;
35
+ --text-primary: #f0f4f8;
36
+ --text-secondary: #94a3b8;
37
+ --text-muted: #64748b;
38
+ --accent-green: #22d3a8;
39
+ --accent-red: #ef4444;
40
+ --accent-orange: #f59e0b;
41
+ --accent-blue: #3b82f6;
42
+ --accent-purple: #a855f7;
43
+ --accent-cyan: #06b6d4;
44
+ --grade-a: #22d3a8;
45
+ --grade-b: #3b82f6;
46
+ --grade-c: #f59e0b;
30
47
  --grade-d: #f0883e;
31
- --grade-f: #f85149;
48
+ --grade-f: #ef4444;
49
+ --glow-green: rgba(34, 211, 168, 0.15);
50
+ --glow-blue: rgba(59, 130, 246, 0.15);
51
+ --glow-purple: rgba(168, 85, 247, 0.15);
52
+ --glow-orange: rgba(245, 158, 11, 0.15);
53
+ --glow-red: rgba(239, 68, 68, 0.15);
32
54
  --radius: 12px;
33
55
  --radius-sm: 8px;
56
+ --font-display: 'JetBrains Mono', 'SF Mono', monospace;
57
+ --font-body: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
58
+
59
+ /* Glassmorphism & overlay tokens */
60
+ --glass-bg-from: rgba(21, 29, 43, 0.8);
61
+ --glass-bg-to: rgba(15, 23, 35, 0.6);
62
+ --glass-border: rgba(255, 255, 255, 0.05);
63
+ --glass-border-hover: rgba(255, 255, 255, 0.1);
64
+ --overlay-subtle: rgba(255, 255, 255, 0.01);
65
+ --overlay-light: rgba(255, 255, 255, 0.02);
66
+ --overlay-soft: rgba(255, 255, 255, 0.03);
67
+ --overlay-medium: rgba(255, 255, 255, 0.04);
68
+ --overlay-strong: rgba(255, 255, 255, 0.06);
69
+ --overlay-intense: rgba(255, 255, 255, 0.08);
70
+ --shadow-card: rgba(0, 0, 0, 0.3);
71
+ --shadow-card-hover: rgba(0, 0, 0, 0.4);
72
+ --shadow-medium: rgba(0, 0, 0, 0.2);
73
+ --shadow-elevated: rgba(0, 0, 0, 0.25);
74
+ --tooltip-bg: rgba(10, 14, 23, 0.95);
75
+ --tooltip-border: rgba(255, 255, 255, 0.06);
76
+ --noise-opacity: 0.015;
77
+ --funnel-text: rgba(255, 255, 255, 0.9);
78
+
79
+ /* Grade background tokens */
80
+ --grade-a-bg: rgba(34, 211, 168, 0.13);
81
+ --grade-b-bg: rgba(59, 130, 246, 0.13);
82
+ --grade-c-bg: rgba(245, 158, 11, 0.13);
83
+ --grade-d-bg: rgba(240, 136, 62, 0.13);
84
+ --grade-f-bg: rgba(239, 68, 68, 0.13);
85
+ }
86
+
87
+ /* ── Light Theme ─────────────────────────────── */
88
+ [data-theme="light"] {
89
+ --bg-primary: #f8fafc;
90
+ --bg-secondary: #f1f5f9;
91
+ --bg-card: #ffffff;
92
+ --bg-hover: #e2e8f0;
93
+ --bg-elevated: #f1f5f9;
94
+ --border: #cbd5e1;
95
+ --border-subtle: #e2e8f0;
96
+ --text-primary: #0f172a;
97
+ --text-secondary: #475569;
98
+ --text-muted: #64748b;
99
+ --accent-green: #0d9488;
100
+ --accent-red: #dc2626;
101
+ --accent-orange: #d97706;
102
+ --accent-blue: #2563eb;
103
+ --accent-purple: #7c3aed;
104
+ --accent-cyan: #0891b2;
105
+ --grade-a: #0d9488;
106
+ --grade-b: #2563eb;
107
+ --grade-c: #d97706;
108
+ --grade-d: #ea580c;
109
+ --grade-f: #dc2626;
110
+ --glow-green: rgba(13, 148, 136, 0.1);
111
+ --glow-blue: rgba(37, 99, 235, 0.1);
112
+ --glow-purple: rgba(124, 58, 237, 0.1);
113
+ --glow-orange: rgba(217, 119, 6, 0.1);
114
+ --glow-red: rgba(220, 38, 38, 0.1);
115
+ --glass-bg-from: rgba(255, 255, 255, 0.7);
116
+ --glass-bg-to: rgba(241, 245, 249, 0.5);
117
+ --glass-border: rgba(0, 0, 0, 0.06);
118
+ --glass-border-hover: rgba(0, 0, 0, 0.12);
119
+ --overlay-subtle: rgba(0, 0, 0, 0.01);
120
+ --overlay-light: rgba(0, 0, 0, 0.02);
121
+ --overlay-soft: rgba(0, 0, 0, 0.03);
122
+ --overlay-medium: rgba(0, 0, 0, 0.04);
123
+ --overlay-strong: rgba(0, 0, 0, 0.06);
124
+ --overlay-intense: rgba(0, 0, 0, 0.08);
125
+ --shadow-card: rgba(0, 0, 0, 0.06);
126
+ --shadow-card-hover: rgba(0, 0, 0, 0.1);
127
+ --shadow-medium: rgba(0, 0, 0, 0.08);
128
+ --shadow-elevated: rgba(0, 0, 0, 0.12);
129
+ --tooltip-bg: rgba(15, 23, 42, 0.92);
130
+ --tooltip-border: rgba(255, 255, 255, 0.1);
131
+ --noise-opacity: 0.008;
132
+ --funnel-text: rgba(0, 0, 0, 0.8);
133
+ --grade-a-bg: rgba(13, 148, 136, 0.1);
134
+ --grade-b-bg: rgba(37, 99, 235, 0.1);
135
+ --grade-c-bg: rgba(217, 119, 6, 0.1);
136
+ --grade-d-bg: rgba(234, 88, 12, 0.1);
137
+ --grade-f-bg: rgba(220, 38, 38, 0.1);
34
138
  }
35
139
 
36
140
  body {
37
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
38
- background: var(--bg-primary);
141
+ font-family: var(--font-body);
142
+ background:
143
+ radial-gradient(ellipse 80% 50% at 50% -20%, rgba(59, 130, 246, 0.03), transparent),
144
+ radial-gradient(ellipse 60% 40% at 100% 50%, rgba(168, 85, 247, 0.02), transparent),
145
+ radial-gradient(ellipse 50% 30% at 0% 80%, rgba(34, 211, 168, 0.02), transparent),
146
+ var(--bg-primary);
39
147
  color: var(--text-primary);
40
148
  line-height: 1.6;
41
149
  min-height: 100vh;
42
150
  }
151
+ body::after {
152
+ content: '';
153
+ position: fixed;
154
+ inset: 0;
155
+ pointer-events: none;
156
+ z-index: 9999;
157
+ opacity: var(--noise-opacity);
158
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
159
+ background-repeat: repeat;
160
+ background-size: 256px 256px;
161
+ }
43
162
 
44
- .container { max-width: 1280px; margin: 0 auto; padding: 24px; }
163
+ .container { max-width: 1320px; margin: 0 auto; padding: 32px; position: relative; z-index: 1; }
45
164
 
46
165
  /* Header */
47
166
  header {
48
167
  text-align: center;
49
- padding: 40px 0 32px;
50
- border-bottom: 1px solid var(--border);
51
- margin-bottom: 32px;
168
+ padding: 56px 0 40px;
169
+ border-bottom: none;
170
+ margin-bottom: 40px;
171
+ position: relative;
172
+ overflow: hidden;
173
+ }
174
+ header::before {
175
+ content: '';
176
+ position: absolute;
177
+ inset: 0;
178
+ background:
179
+ radial-gradient(ellipse 60% 50% at 20% 50%, rgba(59, 130, 246, 0.08), transparent),
180
+ radial-gradient(ellipse 50% 60% at 80% 30%, rgba(168, 85, 247, 0.06), transparent),
181
+ radial-gradient(ellipse 40% 40% at 50% 80%, rgba(34, 211, 168, 0.04), transparent);
182
+ z-index: 0;
183
+ animation: headerShift 20s ease-in-out infinite alternate;
184
+ }
185
+ @keyframes headerShift {
186
+ 0% { opacity: 0.7; transform: scale(1); }
187
+ 100% { opacity: 1; transform: scale(1.05); }
188
+ }
189
+ header > * { position: relative; z-index: 1; }
190
+ header::after {
191
+ content: '';
192
+ position: absolute;
193
+ bottom: 0;
194
+ left: 10%;
195
+ right: 10%;
196
+ height: 1px;
197
+ background: linear-gradient(90deg, transparent, var(--border), transparent);
198
+ z-index: 1;
52
199
  }
53
200
  header h1 {
54
- font-size: 2.5rem;
55
- font-weight: 700;
56
- background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
201
+ font-family: var(--font-display);
202
+ font-size: 2.8rem;
203
+ font-weight: 800;
204
+ letter-spacing: -0.02em;
205
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple), var(--accent-cyan));
206
+ background-size: 200% 200%;
57
207
  -webkit-background-clip: text;
58
208
  -webkit-text-fill-color: transparent;
59
- margin-bottom: 8px;
209
+ animation: gradientMove 8s ease-in-out infinite;
210
+ margin-bottom: 12px;
211
+ }
212
+ @keyframes gradientMove {
213
+ 0%, 100% { background-position: 0% 50%; }
214
+ 50% { background-position: 100% 50%; }
60
215
  }
61
216
  header .tagline {
62
217
  color: var(--text-secondary);
@@ -73,21 +228,40 @@
73
228
  color: var(--text-muted);
74
229
  }
75
230
  .meta-info .badge {
76
- background: var(--bg-card);
77
- border: 1px solid var(--border);
231
+ background: var(--overlay-medium);
232
+ backdrop-filter: blur(12px);
233
+ border: 1px solid var(--overlay-strong);
78
234
  border-radius: 20px;
79
- padding: 4px 12px;
235
+ padding: 5px 14px;
236
+ transition: background 0.3s;
237
+ }
238
+ .meta-info .badge:hover {
239
+ background: var(--overlay-intense);
80
240
  }
81
241
 
82
242
  /* Shared section heading style */
83
243
  .stats-section {
84
- margin-bottom: 32px;
244
+ margin-bottom: 48px;
85
245
  }
86
246
  .stats-section h2 {
87
- font-size: 1.1rem;
88
- margin-bottom: 12px;
89
- color: var(--text-secondary);
90
- font-weight: 500;
247
+ font-family: var(--font-display);
248
+ font-size: 0.85rem;
249
+ margin-bottom: 16px;
250
+ color: var(--text-muted);
251
+ font-weight: 600;
252
+ text-transform: uppercase;
253
+ letter-spacing: 0.12em;
254
+ display: flex;
255
+ align-items: center;
256
+ gap: 8px;
257
+ }
258
+ .stats-section h2::before {
259
+ content: '';
260
+ display: inline-block;
261
+ width: 3px;
262
+ height: 16px;
263
+ background: var(--accent-blue);
264
+ border-radius: 2px;
91
265
  }
92
266
 
93
267
  /* Hero Stats — 3+1 layout */
@@ -103,33 +277,56 @@
103
277
  gap: 12px;
104
278
  }
105
279
  .stat-card {
106
- background: var(--bg-card);
107
- border: 1px solid var(--border);
280
+ background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
281
+ backdrop-filter: blur(16px);
282
+ border: 1px solid var(--glass-border);
108
283
  border-radius: var(--radius);
109
284
  padding: 20px 24px;
110
285
  display: flex;
111
286
  align-items: center;
112
287
  gap: 16px;
113
- transition: transform 0.2s, border-color 0.2s;
288
+ transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94),
289
+ border-color 0.3s,
290
+ box-shadow 0.3s;
114
291
  position: relative;
115
292
  overflow: visible;
116
293
  text-align: left;
117
294
  }
118
295
  .stat-card:hover {
119
- transform: translateY(-2px);
120
- border-color: var(--accent-blue);
296
+ transform: translateY(-3px);
297
+ border-color: var(--glass-border-hover);
298
+ box-shadow: 0 8px 32px var(--shadow-card),
299
+ 0 0 0 1px var(--glass-border);
121
300
  }
122
301
  .stat-card.glow::before {
123
302
  content: '';
124
303
  position: absolute;
125
304
  top: 0; left: 0; right: 0;
126
- height: 3px;
305
+ height: 2px;
127
306
  border-radius: var(--radius) var(--radius) 0 0;
128
307
  }
308
+ .stat-card.glow::after {
309
+ content: '';
310
+ position: absolute;
311
+ inset: -1px;
312
+ border-radius: var(--radius);
313
+ opacity: 0;
314
+ transition: opacity 0.3s;
315
+ pointer-events: none;
316
+ z-index: -1;
317
+ }
129
318
  .stat-card.glow.cost-card::before { background: var(--accent-orange); }
319
+ .stat-card.glow.cost-card::after { box-shadow: 0 4px 24px var(--glow-orange); }
320
+ .stat-card.glow.cost-card:hover::after { opacity: 1; }
130
321
  .stat-card.glow.commits-card::before { background: var(--accent-green); }
322
+ .stat-card.glow.commits-card::after { box-shadow: 0 4px 24px var(--glow-green); }
323
+ .stat-card.glow.commits-card:hover::after { opacity: 1; }
131
324
  .stat-card.glow.avg-card::before { background: var(--accent-blue); }
325
+ .stat-card.glow.avg-card::after { box-shadow: 0 4px 24px var(--glow-blue); }
326
+ .stat-card.glow.avg-card:hover::after { opacity: 1; }
132
327
  .stat-card.glow.grade-card::before { background: linear-gradient(90deg, var(--accent-blue), var(--accent-green)); }
328
+ .stat-card.glow.grade-card::after { box-shadow: 0 4px 24px var(--glow-blue); }
329
+ .stat-card.glow.grade-card:hover::after { opacity: 1; }
133
330
  .stat-card .legend-dot {
134
331
  width: 10px;
135
332
  height: 10px;
@@ -145,13 +342,14 @@
145
342
  text-align: right;
146
343
  }
147
344
  .stat-card .label {
148
- font-size: 0.8rem;
345
+ font-size: 0.75rem;
149
346
  color: var(--text-secondary);
150
347
  text-transform: uppercase;
151
- letter-spacing: 0.5px;
348
+ letter-spacing: 0.08em;
152
349
  margin-bottom: 2px;
153
350
  }
154
351
  .stat-card .value {
352
+ font-family: var(--font-display);
155
353
  font-size: 1.8rem;
156
354
  font-weight: 700;
157
355
  white-space: nowrap;
@@ -169,6 +367,7 @@
169
367
  padding: 24px;
170
368
  }
171
369
  .stat-card.grade .value {
370
+ font-family: var(--font-display);
172
371
  font-size: 3.5rem;
173
372
  line-height: 1;
174
373
  font-weight: 800;
@@ -197,10 +396,10 @@
197
396
  .hero-legend, .cost-legend, .token-legend {
198
397
  display: flex;
199
398
  gap: 24px;
200
- margin-top: 12px;
201
- padding: 10px 16px;
202
- background: var(--bg-secondary);
203
- border: 1px solid var(--border);
399
+ margin-top: 16px;
400
+ padding: 12px 20px;
401
+ background: var(--overlay-light);
402
+ border: 1px solid var(--overlay-medium);
204
403
  border-radius: var(--radius-sm);
205
404
  }
206
405
  .hero-legend span, .cost-legend span, .token-legend span {
@@ -220,13 +419,16 @@
220
419
 
221
420
  /* Insights */
222
421
  .insights {
223
- margin-bottom: 32px;
422
+ margin-bottom: 48px;
224
423
  }
225
424
  .insights h2 {
226
- font-size: 1.1rem;
227
- margin-bottom: 12px;
228
- color: var(--text-secondary);
229
- font-weight: 500;
425
+ font-family: var(--font-display);
426
+ font-size: 0.85rem;
427
+ margin-bottom: 16px;
428
+ color: var(--text-muted);
429
+ font-weight: 600;
430
+ text-transform: uppercase;
431
+ letter-spacing: 0.12em;
230
432
  }
231
433
  .insight-list {
232
434
  display: grid;
@@ -236,11 +438,17 @@
236
438
  display: flex;
237
439
  align-items: flex-start;
238
440
  gap: 10px;
239
- padding: 12px 16px;
240
- background: var(--bg-secondary);
241
- border: 1px solid var(--border);
441
+ padding: 14px 18px;
442
+ background: var(--overlay-light);
443
+ border: 1px solid var(--overlay-medium);
242
444
  border-radius: var(--radius-sm);
243
- font-size: 0.9rem;
445
+ font-size: 0.88rem;
446
+ transition: background 0.2s, border-color 0.2s, transform 0.2s;
447
+ }
448
+ .insight:hover {
449
+ background: var(--overlay-medium);
450
+ border-color: var(--overlay-intense);
451
+ transform: translateX(4px);
244
452
  }
245
453
  .insight .icon { font-size: 1.1rem; flex-shrink: 0; }
246
454
  .insight.warning .icon { color: var(--accent-orange); }
@@ -252,23 +460,32 @@
252
460
  .charts-grid {
253
461
  display: grid;
254
462
  grid-template-columns: 1fr 1fr;
255
- gap: 16px;
256
- margin-bottom: 32px;
463
+ gap: 20px;
464
+ margin-bottom: 48px;
257
465
  }
258
466
  .chart-card {
259
- background: var(--bg-card);
260
- border: 1px solid var(--border);
467
+ background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
468
+ backdrop-filter: blur(16px);
469
+ border: 1px solid var(--glass-border);
261
470
  border-radius: var(--radius);
262
- padding: 20px;
471
+ padding: 24px;
472
+ transition: border-color 0.3s, box-shadow 0.3s;
473
+ }
474
+ .chart-card:hover {
475
+ border-color: var(--overlay-intense);
476
+ box-shadow: 0 4px 20px var(--shadow-medium);
263
477
  }
264
478
  .chart-card.full-width {
265
479
  grid-column: 1 / -1;
266
480
  }
267
481
  .chart-card h3 {
268
- font-size: 0.9rem;
269
- color: var(--text-secondary);
482
+ font-family: var(--font-display);
483
+ font-size: 0.8rem;
484
+ color: var(--text-muted);
270
485
  margin-bottom: 16px;
271
- font-weight: 500;
486
+ font-weight: 600;
487
+ text-transform: uppercase;
488
+ letter-spacing: 0.08em;
272
489
  }
273
490
  .chart-header {
274
491
  display: flex;
@@ -278,16 +495,22 @@
278
495
  }
279
496
  .chart-header h3 { margin-bottom: 0; }
280
497
  .scale-toggle {
281
- padding: 4px 10px;
282
- border: 1px solid var(--border);
283
- background: var(--bg-secondary);
498
+ padding: 5px 12px;
499
+ border: 1px solid var(--overlay-intense);
500
+ background: var(--overlay-soft);
284
501
  color: var(--text-secondary);
285
502
  border-radius: 6px;
286
503
  cursor: pointer;
287
- font-family: inherit;
288
- font-size: 0.75rem;
504
+ font-family: var(--font-display);
505
+ font-size: 0.7rem;
506
+ letter-spacing: 0.05em;
507
+ transition: all 0.2s;
508
+ }
509
+ .scale-toggle:hover {
510
+ color: var(--text-primary);
511
+ border-color: var(--accent-blue);
512
+ background: var(--glow-blue);
289
513
  }
290
- .scale-toggle:hover { color: var(--text-primary); border-color: var(--accent-blue); }
291
514
  .chart-container {
292
515
  position: relative;
293
516
  width: 100%;
@@ -296,19 +519,23 @@
296
519
 
297
520
  /* Survival bar */
298
521
  .survival-section {
299
- margin-bottom: 32px;
522
+ margin-bottom: 48px;
300
523
  }
301
524
  .survival-card {
302
- background: var(--bg-card);
303
- border: 1px solid var(--border);
525
+ background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
526
+ backdrop-filter: blur(16px);
527
+ border: 1px solid var(--glass-border);
304
528
  border-radius: var(--radius);
305
529
  padding: 24px;
306
530
  }
307
531
  .survival-card h3 {
308
- font-size: 0.9rem;
309
- color: var(--text-secondary);
532
+ font-family: var(--font-display);
533
+ font-size: 0.8rem;
534
+ color: var(--text-muted);
310
535
  margin-bottom: 16px;
311
- font-weight: 500;
536
+ font-weight: 600;
537
+ text-transform: uppercase;
538
+ letter-spacing: 0.08em;
312
539
  display: flex;
313
540
  align-items: center;
314
541
  gap: 6px;
@@ -320,7 +547,7 @@
320
547
  width: 16px;
321
548
  height: 16px;
322
549
  border-radius: 50%;
323
- background: var(--bg-hover);
550
+ background: var(--glass-border);
324
551
  color: var(--text-muted);
325
552
  font-size: 0.6rem;
326
553
  font-style: normal;
@@ -329,28 +556,39 @@
329
556
  position: relative;
330
557
  vertical-align: middle;
331
558
  flex-shrink: 0;
559
+ transition: background 0.2s, color 0.2s;
560
+ }
561
+ .info-tip:hover {
562
+ background: var(--glow-blue);
563
+ color: var(--accent-blue);
564
+ }
565
+ @keyframes tooltipFade {
566
+ from { opacity: 0; transform: translateX(-50%) translateY(4px); }
567
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
332
568
  }
333
569
  .info-tip:hover::after {
334
570
  content: attr(data-tip);
335
571
  position: absolute;
336
- bottom: calc(100% + 8px);
572
+ bottom: calc(100% + 10px);
337
573
  left: 50%;
338
- transform: translateX(-50%);
339
- background: var(--bg-primary);
574
+ transform: translateX(-50%) translateY(0);
575
+ background: var(--tooltip-bg);
576
+ backdrop-filter: blur(16px);
340
577
  color: var(--text-secondary);
341
- padding: 8px 12px;
342
- border-radius: 6px;
578
+ padding: 10px 14px;
579
+ border-radius: 8px;
343
580
  font-size: 0.75rem;
344
581
  font-weight: 400;
345
582
  width: 280px;
346
- line-height: 1.4;
347
- border: 1px solid var(--border);
583
+ line-height: 1.5;
584
+ border: 1px solid var(--tooltip-border);
348
585
  z-index: 100;
349
586
  pointer-events: none;
350
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
587
+ box-shadow: 0 8px 32px var(--shadow-card-hover);
351
588
  white-space: normal;
352
589
  text-transform: none;
353
590
  letter-spacing: normal;
591
+ animation: tooltipFade 0.2s ease forwards;
354
592
  }
355
593
  /* Table header tooltips: show below the icon instead of above */
356
594
  thead .info-tip:hover::after {
@@ -359,12 +597,13 @@
359
597
  }
360
598
  /* Make table header tooltip icons more visible */
361
599
  thead .info-tip {
362
- background: rgba(255,255,255,0.1);
600
+ background: var(--glass-border-hover);
363
601
  color: var(--text-secondary);
364
602
  }
365
603
  .survival-bar {
366
604
  height: 24px;
367
- background: var(--bg-hover);
605
+ background: var(--overlay-medium);
606
+ border: 1px solid var(--overlay-strong);
368
607
  border-radius: 12px;
369
608
  overflow: hidden;
370
609
  margin-bottom: 12px;
@@ -398,12 +637,18 @@
398
637
  }
399
638
  .heatmap-cell {
400
639
  aspect-ratio: 1;
401
- border-radius: 3px;
640
+ border-radius: 4px;
402
641
  min-height: 14px;
403
- transition: transform 0.15s;
642
+ transition: transform 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94),
643
+ box-shadow 0.2s;
404
644
  cursor: default;
405
645
  }
406
- .heatmap-cell:hover { transform: scale(1.3); }
646
+ .heatmap-cell:hover {
647
+ transform: scale(1.4);
648
+ box-shadow: 0 0 8px rgba(34, 211, 168, 0.3);
649
+ z-index: 10;
650
+ position: relative;
651
+ }
407
652
  .heatmap-hour-labels {
408
653
  display: grid;
409
654
  grid-template-columns: 40px repeat(24, 1fr);
@@ -418,17 +663,21 @@
418
663
 
419
664
  /* Sessions table */
420
665
  .sessions-section {
421
- margin-bottom: 32px;
666
+ margin-bottom: 48px;
422
667
  }
423
668
  .sessions-section h2 {
424
- font-size: 1.1rem;
425
- margin-bottom: 12px;
426
- color: var(--text-secondary);
427
- font-weight: 500;
669
+ font-family: var(--font-display);
670
+ font-size: 0.85rem;
671
+ margin-bottom: 16px;
672
+ color: var(--text-muted);
673
+ font-weight: 600;
674
+ text-transform: uppercase;
675
+ letter-spacing: 0.12em;
428
676
  }
429
677
  .sessions-table-wrap {
430
- background: var(--bg-card);
431
- border: 1px solid var(--border);
678
+ background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
679
+ backdrop-filter: blur(16px);
680
+ border: 1px solid var(--glass-border);
432
681
  border-radius: var(--radius);
433
682
  overflow-x: auto;
434
683
  overflow-y: visible;
@@ -439,43 +688,64 @@
439
688
  font-size: 0.85rem;
440
689
  }
441
690
  thead th {
442
- padding: 12px 12px;
691
+ padding: 14px 14px;
443
692
  text-align: left;
444
- color: var(--text-secondary);
693
+ color: var(--text-muted);
694
+ font-family: var(--font-display);
445
695
  font-weight: 500;
446
- border-bottom: 1px solid var(--border);
696
+ font-size: 0.75rem;
697
+ text-transform: uppercase;
698
+ letter-spacing: 0.08em;
699
+ border-bottom: 1px solid var(--overlay-strong);
447
700
  cursor: pointer;
448
701
  user-select: none;
449
702
  white-space: nowrap;
703
+ transition: color 0.2s;
704
+ position: relative;
450
705
  }
451
706
  thead th:hover { color: var(--text-primary); }
452
707
  thead th.sorted { color: var(--accent-blue); }
708
+ thead th.sorted::after {
709
+ content: '';
710
+ position: absolute;
711
+ bottom: 0;
712
+ left: 0;
713
+ right: 0;
714
+ height: 2px;
715
+ background: var(--accent-blue);
716
+ }
453
717
  tbody tr {
454
- border-bottom: 1px solid var(--border);
455
- transition: background 0.15s;
718
+ border-bottom: 1px solid var(--overlay-soft);
719
+ transition: background 0.2s;
456
720
  }
457
- tbody tr:hover { background: var(--bg-hover); }
721
+ tbody tr:hover { background: var(--overlay-soft); }
458
722
  tbody tr:last-child { border-bottom: none; }
459
723
  tbody td {
460
- padding: 10px 12px;
724
+ padding: 12px 14px;
461
725
  white-space: nowrap;
726
+ font-size: 0.85rem;
462
727
  }
463
728
  .grade-badge {
464
729
  display: inline-flex;
465
730
  align-items: center;
466
731
  justify-content: center;
467
- width: 28px;
468
- height: 28px;
469
- border-radius: 6px;
732
+ width: 30px;
733
+ height: 30px;
734
+ border-radius: 8px;
735
+ font-family: var(--font-display);
470
736
  font-weight: 700;
471
- font-size: 0.85rem;
737
+ font-size: 0.8rem;
738
+ transition: transform 0.2s, box-shadow 0.2s;
739
+ }
740
+ tbody tr:hover .grade-badge {
741
+ transform: scale(1.1);
472
742
  }
473
- tr.orphaned { background: rgba(210, 153, 34, 0.08); }
743
+ tr.orphaned { background: rgba(245, 158, 11, 0.06); }
474
744
  .expand-row {
475
745
  display: none;
476
- background: var(--bg-secondary);
746
+ background: var(--overlay-subtle);
477
747
  }
478
- .expand-row.open { display: table-row; }
748
+ .expand-row.open { display: table-row; animation: fadeSlideUp 0.3s ease forwards; }
479
749
  .expand-row td {
480
750
  white-space: normal;
481
751
  max-width: 1px;
@@ -522,14 +792,20 @@
522
792
  border-top: 1px solid var(--border);
523
793
  }
524
794
  .pagination button {
525
- padding: 6px 14px;
526
- border: 1px solid var(--border);
527
- background: var(--bg-secondary);
795
+ padding: 8px 18px;
796
+ border: 1px solid var(--overlay-intense);
797
+ background: var(--overlay-soft);
528
798
  color: var(--text-primary);
529
- border-radius: 6px;
799
+ border-radius: 8px;
530
800
  cursor: pointer;
531
- font-family: inherit;
532
- font-size: 0.8rem;
801
+ font-family: var(--font-body);
802
+ font-size: 0.82rem;
803
+ transition: all 0.2s;
804
+ }
805
+ .pagination button:hover:not(:disabled) {
806
+ background: var(--overlay-intense);
807
+ border-color: var(--accent-blue);
808
+ box-shadow: 0 0 12px var(--glow-blue);
533
809
  }
534
810
  .pagination button:disabled { opacity: 0.4; cursor: default; }
535
811
  .pagination span { font-size: 0.8rem; color: var(--text-muted); }
@@ -537,13 +813,23 @@
537
813
  /* Footer */
538
814
  footer {
539
815
  text-align: center;
540
- padding: 32px 0;
541
- border-top: 1px solid var(--border);
816
+ padding: 40px 0;
817
+ border-top: none;
542
818
  color: var(--text-muted);
543
819
  font-size: 0.8rem;
820
+ position: relative;
544
821
  }
545
- footer a { color: var(--accent-blue); text-decoration: none; }
546
- footer a:hover { text-decoration: underline; }
822
+ footer::before {
823
+ content: '';
824
+ position: absolute;
825
+ top: 0;
826
+ left: 10%;
827
+ right: 10%;
828
+ height: 1px;
829
+ background: linear-gradient(90deg, transparent, var(--border), transparent);
830
+ }
831
+ footer a { color: var(--accent-blue); text-decoration: none; transition: color 0.2s; }
832
+ footer a:hover { color: var(--accent-cyan); text-decoration: underline; }
547
833
 
548
834
  /* Loading */
549
835
  .loading {
@@ -571,31 +857,35 @@
571
857
  gap: 16px;
572
858
  }
573
859
  .token-stat-card {
574
- background: var(--bg-card);
575
- border: 1px solid var(--border);
860
+ background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
861
+ backdrop-filter: blur(16px);
862
+ border: 1px solid var(--glass-border);
576
863
  border-radius: var(--radius);
577
864
  padding: 20px 24px;
578
865
  text-align: center;
579
- transition: transform 0.2s, border-color 0.2s;
580
- border-top: 3px solid var(--border);
866
+ transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94),
867
+ box-shadow 0.3s;
868
+ border-top: 2px solid var(--border);
581
869
  display: flex;
582
870
  flex-direction: column;
583
871
  }
584
872
  .token-stat-card:hover {
585
- transform: translateY(-2px);
873
+ transform: translateY(-3px);
874
+ box-shadow: 0 8px 32px var(--shadow-elevated);
586
875
  }
587
876
  .token-stat-card.burned { border-top-color: var(--accent-orange); }
588
877
  .token-stat-card.wasted { border-top-color: var(--accent-red); }
589
878
  .token-stat-card.efficiency { border-top-color: var(--accent-green); }
590
879
  .token-stat-card.per-commit { border-top-color: var(--accent-cyan); }
591
880
  .token-stat-card .label {
592
- font-size: 0.8rem;
881
+ font-size: 0.75rem;
593
882
  color: var(--text-secondary);
594
883
  text-transform: uppercase;
595
- letter-spacing: 0.5px;
884
+ letter-spacing: 0.08em;
596
885
  margin-bottom: 8px;
597
886
  }
598
887
  .token-stat-card .value {
888
+ font-family: var(--font-display);
599
889
  font-size: 2rem;
600
890
  font-weight: 700;
601
891
  }
@@ -705,8 +995,9 @@
705
995
  display: grid;
706
996
  grid-template-columns: repeat(4, 1fr);
707
997
  gap: 0;
708
- background: var(--bg-card);
709
- border: 1px solid var(--border);
998
+ background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
999
+ backdrop-filter: blur(16px);
1000
+ border: 1px solid var(--glass-border);
710
1001
  border-radius: var(--radius);
711
1002
  overflow: hidden;
712
1003
  position: relative;
@@ -731,7 +1022,7 @@
731
1022
  position: relative;
732
1023
  }
733
1024
  .period-card + .period-card {
734
- border-left: 1px solid var(--border);
1025
+ border-left: 1px solid var(--glass-border);
735
1026
  }
736
1027
  .period-card:hover {
737
1028
  background: var(--bg-hover);
@@ -746,7 +1037,7 @@
746
1037
  position: relative;
747
1038
  z-index: 2;
748
1039
  }
749
- .period-card.today::before { background: var(--accent-green); box-shadow: 0 0 8px rgba(63, 185, 80, 0.4); }
1040
+ .period-card.today::before { background: var(--accent-green); box-shadow: 0 0 8px rgba(34, 211, 168, 0.4); }
750
1041
  .period-card.week::before { background: var(--accent-blue); }
751
1042
  .period-card.month::before { background: var(--accent-purple); }
752
1043
  .period-card.all-time::before { background: var(--accent-orange); }
@@ -762,6 +1053,7 @@
762
1053
  margin-bottom: 10px;
763
1054
  }
764
1055
  .period-card .period-cost {
1056
+ font-family: var(--font-display);
765
1057
  font-size: 1.6rem;
766
1058
  font-weight: 700;
767
1059
  margin-bottom: 8px;
@@ -783,19 +1075,23 @@
783
1075
  }
784
1076
  /* Token Flow Funnel */
785
1077
  .token-funnel {
786
- margin-bottom: 32px;
1078
+ margin-bottom: 48px;
787
1079
  }
788
1080
  .token-funnel-card {
789
- background: var(--bg-card);
790
- border: 1px solid var(--border);
1081
+ background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
1082
+ backdrop-filter: blur(16px);
1083
+ border: 1px solid var(--glass-border);
791
1084
  border-radius: var(--radius);
792
1085
  padding: 24px;
793
1086
  }
794
1087
  .token-funnel-card h3 {
795
- font-size: 0.9rem;
796
- color: var(--text-secondary);
1088
+ font-family: var(--font-display);
1089
+ font-size: 0.8rem;
1090
+ color: var(--text-muted);
797
1091
  margin-bottom: 20px;
798
- font-weight: 500;
1092
+ font-weight: 600;
1093
+ text-transform: uppercase;
1094
+ letter-spacing: 0.08em;
799
1095
  }
800
1096
  .funnel-row {
801
1097
  margin-bottom: 16px;
@@ -817,16 +1113,20 @@
817
1113
  }
818
1114
  .funnel-bar .segment {
819
1115
  height: 100%;
820
- transition: width 0.8s ease;
1116
+ transition: width 0.8s ease, filter 0.2s, transform 0.2s;
821
1117
  display: flex;
822
1118
  align-items: center;
823
1119
  justify-content: center;
824
1120
  font-size: 0.7rem;
825
1121
  font-weight: 500;
826
- color: rgba(255,255,255,0.9);
1122
+ color: var(--funnel-text);
827
1123
  overflow: hidden;
828
1124
  white-space: nowrap;
829
1125
  }
1126
+ .funnel-bar .segment:hover {
1127
+ filter: brightness(1.2);
1128
+ transform: scaleY(1.1);
1129
+ }
830
1130
  .funnel-bar .segment:first-child { border-radius: 8px 0 0 8px; }
831
1131
  .funnel-bar .segment:last-child { border-radius: 0 8px 8px 0; }
832
1132
  .funnel-legend {
@@ -853,6 +1153,152 @@
853
1153
  animation: pulse-red 2s ease-in-out infinite;
854
1154
  }
855
1155
 
1156
+ /* Staggered reveal animation */
1157
+ @keyframes fadeSlideUp {
1158
+ from { opacity: 0; transform: translateY(20px); }
1159
+ to { opacity: 1; transform: translateY(0); }
1160
+ }
1161
+ .reveal {
1162
+ opacity: 0;
1163
+ animation: fadeSlideUp 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
1164
+ }
1165
+ .reveal-delay-1 { animation-delay: 0.05s; }
1166
+ .reveal-delay-2 { animation-delay: 0.1s; }
1167
+ .reveal-delay-3 { animation-delay: 0.15s; }
1168
+ .reveal-delay-4 { animation-delay: 0.2s; }
1169
+ .reveal-delay-5 { animation-delay: 0.25s; }
1170
+ .reveal-delay-6 { animation-delay: 0.3s; }
1171
+ .reveal-delay-7 { animation-delay: 0.35s; }
1172
+ .reveal-delay-8 { animation-delay: 0.4s; }
1173
+ .reveal-delay-9 { animation-delay: 0.45s; }
1174
+ .reveal-delay-10 { animation-delay: 0.5s; }
1175
+
1176
+ /* Scroll-triggered reveal */
1177
+ .scroll-reveal {
1178
+ opacity: 0;
1179
+ transform: translateY(16px);
1180
+ transition: opacity 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94),
1181
+ transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
1182
+ }
1183
+ .scroll-reveal.revealed {
1184
+ opacity: 1;
1185
+ transform: translateY(0);
1186
+ }
1187
+
1188
+ /* Grade circle animation */
1189
+ @keyframes gradeReveal {
1190
+ from { --grade-deg: 0deg; }
1191
+ }
1192
+ .grade-circle {
1193
+ animation: gradeReveal 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
1194
+ }
1195
+
1196
+ /* ── Header Actions (top-right buttons) ─────── */
1197
+ .header-actions {
1198
+ position: absolute;
1199
+ top: 16px;
1200
+ right: 16px;
1201
+ z-index: 10;
1202
+ display: flex;
1203
+ align-items: center;
1204
+ gap: 8px;
1205
+ }
1206
+ .header-action-btn {
1207
+ width: 42px;
1208
+ height: 42px;
1209
+ border-radius: 50%;
1210
+ border: 1px solid var(--glass-border);
1211
+ background: var(--overlay-medium);
1212
+ backdrop-filter: blur(12px);
1213
+ color: var(--text-secondary);
1214
+ cursor: pointer;
1215
+ display: flex;
1216
+ align-items: center;
1217
+ justify-content: center;
1218
+ transition: background 0.3s, border-color 0.3s, color 0.3s, transform 0.2s;
1219
+ }
1220
+ .header-action-btn:hover {
1221
+ background: var(--overlay-intense);
1222
+ border-color: var(--glass-border-hover);
1223
+ color: var(--text-primary);
1224
+ transform: scale(1.1);
1225
+ }
1226
+ .header-action-btn svg {
1227
+ width: 18px;
1228
+ height: 18px;
1229
+ }
1230
+ .refresh-btn.refreshing svg {
1231
+ animation: spin 0.8s linear infinite;
1232
+ }
1233
+ .theme-switcher {
1234
+ display: flex;
1235
+ align-items: center;
1236
+ border-radius: 22px;
1237
+ border: 1px solid var(--glass-border);
1238
+ background: var(--overlay-medium);
1239
+ backdrop-filter: blur(12px);
1240
+ padding: 3px;
1241
+ gap: 2px;
1242
+ }
1243
+ .theme-switcher button {
1244
+ width: 34px;
1245
+ height: 34px;
1246
+ border-radius: 50%;
1247
+ border: none;
1248
+ background: transparent;
1249
+ color: var(--text-muted);
1250
+ cursor: pointer;
1251
+ display: flex;
1252
+ align-items: center;
1253
+ justify-content: center;
1254
+ transition: background 0.25s, color 0.25s, transform 0.15s;
1255
+ }
1256
+ .theme-switcher button:hover {
1257
+ color: var(--text-primary);
1258
+ transform: scale(1.08);
1259
+ }
1260
+ .theme-switcher button.active {
1261
+ background: var(--overlay-intense);
1262
+ color: var(--text-primary);
1263
+ box-shadow: 0 1px 4px var(--shadow-card);
1264
+ }
1265
+ .theme-switcher button svg {
1266
+ width: 16px;
1267
+ height: 16px;
1268
+ }
1269
+
1270
+ /* ── Light Theme Overrides ───────────────────── */
1271
+ [data-theme="light"] body {
1272
+ background:
1273
+ radial-gradient(ellipse 80% 50% at 50% -20%, rgba(37, 99, 235, 0.05), transparent),
1274
+ radial-gradient(ellipse 60% 40% at 100% 50%, rgba(124, 58, 237, 0.04), transparent),
1275
+ radial-gradient(ellipse 50% 30% at 0% 80%, rgba(13, 148, 136, 0.04), transparent),
1276
+ var(--bg-primary);
1277
+ }
1278
+ [data-theme="light"] header::before {
1279
+ background:
1280
+ radial-gradient(ellipse 60% 50% at 20% 50%, rgba(37, 99, 235, 0.07), transparent),
1281
+ radial-gradient(ellipse 50% 60% at 80% 30%, rgba(124, 58, 237, 0.05), transparent),
1282
+ radial-gradient(ellipse 40% 40% at 50% 80%, rgba(13, 148, 136, 0.04), transparent);
1283
+ }
1284
+ [data-theme="light"] .info-tip:hover::after {
1285
+ color: #e2e8f0;
1286
+ }
1287
+ [data-theme="light"] tr.orphaned {
1288
+ background: rgba(217, 119, 6, 0.06);
1289
+ }
1290
+ [data-theme="light"] .main-badge {
1291
+ background: rgba(13, 148, 136, 0.12);
1292
+ }
1293
+
1294
+ /* Smooth theme transition */
1295
+ html.theme-transition,
1296
+ html.theme-transition *,
1297
+ html.theme-transition *::before,
1298
+ html.theme-transition *::after {
1299
+ transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, background 0.3s ease !important;
1300
+ }
1301
+
856
1302
  /* Responsive */
857
1303
  @media (max-width: 900px) {
858
1304
  .hero-stats { grid-template-columns: 1fr; }
@@ -861,7 +1307,7 @@
861
1307
  .period-stats { grid-template-columns: repeat(2, 1fr); }
862
1308
  .period-stats::before { display: none; }
863
1309
  .period-card:nth-child(3) { border-left: none; }
864
- .period-card:nth-child(3), .period-card:nth-child(4) { border-top: 1px solid var(--border); }
1310
+ .period-card:nth-child(3), .period-card:nth-child(4) { border-top: 1px solid var(--glass-border); }
865
1311
  .charts-grid { grid-template-columns: 1fr; }
866
1312
  }
867
1313
  @media (max-width: 600px) {
@@ -877,6 +1323,30 @@
877
1323
  <body>
878
1324
  <div class="container">
879
1325
  <header>
1326
+ <div class="header-actions">
1327
+ <button class="header-action-btn refresh-btn" id="refresh-btn" aria-label="Refresh data" title="Refresh dashboard data">
1328
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1329
+ <polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
1330
+ </svg>
1331
+ </button>
1332
+ <div class="theme-switcher" id="theme-switcher">
1333
+ <button data-theme-pref="system" title="System theme" aria-label="Use system theme">
1334
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1335
+ <rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
1336
+ </svg>
1337
+ </button>
1338
+ <button data-theme-pref="dark" title="Dark theme" aria-label="Dark theme">
1339
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1340
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
1341
+ </svg>
1342
+ </button>
1343
+ <button data-theme-pref="light" title="Light theme" aria-label="Light theme">
1344
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1345
+ <circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
1346
+ </svg>
1347
+ </button>
1348
+ </div>
1349
+ </div>
880
1350
  <h1>Codelens AI</h1>
881
1351
  <p class="tagline">Correlates your AI coding agent's token spend with actual git output — see what shipped, what churned, and what it cost.</p>
882
1352
  <div class="meta-info">
@@ -900,10 +1370,106 @@
900
1370
  </div>
901
1371
 
902
1372
  <script>
903
- const GRADE_COLORS = { A: '#3fb950', B: '#58a6ff', C: '#d29922', D: '#f0883e', F: '#f85149' };
1373
+ const GRADE_VAR = { A: 'var(--grade-a)', B: 'var(--grade-b)', C: 'var(--grade-c)', D: 'var(--grade-d)', F: 'var(--grade-f)' };
1374
+ const GRADE_BG_VAR = { A: 'var(--grade-a-bg)', B: 'var(--grade-b-bg)', C: 'var(--grade-c-bg)', D: 'var(--grade-d-bg)', F: 'var(--grade-f-bg)' };
904
1375
  const INSIGHT_ICONS = { warning: '!', success: '+', info: 'i', tip: '*' };
905
1376
  const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
906
1377
  let DATA = null;
1378
+ let initialRenderDone = false;
1379
+
1380
+ function getThemePref() {
1381
+ return localStorage.getItem('codelens-theme') || 'system';
1382
+ }
1383
+
1384
+ function resolveTheme(pref) {
1385
+ if (pref === 'system') return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
1386
+ return pref;
1387
+ }
1388
+
1389
+ function getTheme() {
1390
+ return document.documentElement.getAttribute('data-theme') || 'dark';
1391
+ }
1392
+
1393
+ function applyTheme(pref) {
1394
+ const resolved = resolveTheme(pref);
1395
+ document.documentElement.classList.add('theme-transition');
1396
+ if (resolved === 'light') {
1397
+ document.documentElement.setAttribute('data-theme', 'light');
1398
+ } else {
1399
+ document.documentElement.removeAttribute('data-theme');
1400
+ }
1401
+ localStorage.setItem('codelens-theme', pref);
1402
+ updateChartTheme(resolved);
1403
+ updateThemeSwitcher(pref);
1404
+ setTimeout(() => document.documentElement.classList.remove('theme-transition'), 350);
1405
+ }
1406
+
1407
+ function updateThemeSwitcher(pref) {
1408
+ const switcher = document.getElementById('theme-switcher');
1409
+ if (!switcher) return;
1410
+ switcher.querySelectorAll('button').forEach(btn => {
1411
+ btn.classList.toggle('active', btn.dataset.themePref === pref);
1412
+ });
1413
+ }
1414
+
1415
+ function updateChartTheme(theme) {
1416
+ const isLight = theme === 'light';
1417
+ Chart.defaults.color = isLight ? '#475569' : '#94a3b8';
1418
+ Chart.defaults.borderColor = isLight ? '#cbd5e1' : '#1f2d3d';
1419
+ Chart.defaults.plugins.tooltip.backgroundColor = isLight ? 'rgba(15, 23, 42, 0.92)' : 'rgba(10, 14, 23, 0.95)';
1420
+ Chart.defaults.plugins.tooltip.borderColor = isLight ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.08)';
1421
+ Object.values(Chart.instances || {}).forEach(chart => {
1422
+ if (chart.options.scales) {
1423
+ Object.values(chart.options.scales).forEach(scale => {
1424
+ if (scale.grid) scale.grid.color = isLight ? '#e2e8f0' : '#1f2d3d';
1425
+ if (scale.ticks) scale.ticks.color = isLight ? '#475569' : '#94a3b8';
1426
+ if (scale.title) scale.title.color = isLight ? '#475569' : '#94a3b8';
1427
+ });
1428
+ }
1429
+ chart.update('none');
1430
+ });
1431
+ }
1432
+
1433
+ function initTheme() {
1434
+ const switcher = document.getElementById('theme-switcher');
1435
+ if (!switcher) return;
1436
+ const pref = getThemePref();
1437
+ updateThemeSwitcher(pref);
1438
+ switcher.addEventListener('click', (e) => {
1439
+ const btn = e.target.closest('button[data-theme-pref]');
1440
+ if (!btn) return;
1441
+ applyTheme(btn.dataset.themePref);
1442
+ });
1443
+ window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
1444
+ if (getThemePref() === 'system') applyTheme('system');
1445
+ });
1446
+ }
1447
+
1448
+ async function refreshData() {
1449
+ const btn = document.getElementById('refresh-btn');
1450
+ if (!btn || btn.classList.contains('refreshing')) return;
1451
+ btn.classList.add('refreshing');
1452
+ try {
1453
+ // Trigger server-side re-parse: clears cache, re-reads sessions, re-analyzes git, recomputes metrics
1454
+ const refreshRes = await fetch('/api/refresh', { method: 'POST' });
1455
+ if (!refreshRes.ok) {
1456
+ const err = await refreshRes.json().catch(() => ({}));
1457
+ throw new Error(err.error || 'Refresh failed');
1458
+ }
1459
+ // Fetch the newly computed payload
1460
+ const res = await fetch('/api/all');
1461
+ DATA = await res.json();
1462
+ initialRenderDone = false;
1463
+ render();
1464
+ } catch (err) {
1465
+ document.getElementById('app').innerHTML = `
1466
+ <div class="loading" style="color: var(--accent-red);">
1467
+ Failed to refresh: ${err.message}
1468
+ </div>`;
1469
+ } finally {
1470
+ btn.classList.remove('refreshing');
1471
+ }
1472
+ }
907
1473
 
908
1474
  function formatTokens(n) {
909
1475
  if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(1) + 'B';
@@ -911,6 +1477,21 @@ function formatTokens(n) {
911
1477
  if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K';
912
1478
  return n.toString();
913
1479
  }
1480
+
1481
+ function animateValue(element, start, end, duration, prefix = '', suffix = '') {
1482
+ const startTime = performance.now();
1483
+ const range = end - start;
1484
+ function update(currentTime) {
1485
+ const elapsed = currentTime - startTime;
1486
+ const progress = Math.min(elapsed / duration, 1);
1487
+ const eased = 1 - Math.pow(1 - progress, 3);
1488
+ const current = start + range * eased;
1489
+ element.textContent = prefix + (Number.isInteger(end) ? Math.round(current).toLocaleString() : current.toFixed(2)) + suffix;
1490
+ if (progress < 1) requestAnimationFrame(update);
1491
+ }
1492
+ requestAnimationFrame(update);
1493
+ }
1494
+
914
1495
  let currentPage = 1;
915
1496
  let sortCol = 'startTime';
916
1497
  let sortOrder = -1;
@@ -918,6 +1499,9 @@ let timelineChart = null;
918
1499
  let timelineLogScale = false;
919
1500
 
920
1501
  document.addEventListener('DOMContentLoaded', async () => {
1502
+ initTheme();
1503
+ const refreshBtn = document.getElementById('refresh-btn');
1504
+ if (refreshBtn) refreshBtn.addEventListener('click', refreshData);
921
1505
  try {
922
1506
  const res = await fetch('/api/all');
923
1507
  DATA = await res.json();
@@ -936,14 +1520,17 @@ function render() {
936
1520
  const summary = d.summary;
937
1521
  const t = d.tokenAnalytics;
938
1522
 
1523
+ const revealClass = !initialRenderDone ? 'reveal' : '';
1524
+ const rd = (n) => !initialRenderDone ? `reveal reveal-delay-${n}` : '';
1525
+
939
1526
  document.getElementById('app').innerHTML = `
940
- ${renderHeroStats(summary)}
941
- ${renderCostByPeriod(summary.costByPeriod)}
942
- ${renderTokenHeroStats(t)}
943
- ${renderTokenFunnel(t)}
944
- ${renderTokenFunFacts(t.funFacts)}
945
- ${renderInsights(d.insights)}
946
- <div class="charts-grid">
1527
+ <div class="${rd(1)}">${renderHeroStats(summary)}</div>
1528
+ <div class="${rd(2)}">${renderCostByPeriod(summary.costByPeriod)}</div>
1529
+ <div class="${rd(3)}">${renderTokenHeroStats(t)}</div>
1530
+ <div class="${rd(4)}">${renderTokenFunnel(t)}</div>
1531
+ <div class="${rd(5)}">${renderTokenFunFacts(t.funFacts)}</div>
1532
+ <div class="${rd(6)}">${renderInsights(d.insights)}</div>
1533
+ <div class="charts-grid scroll-reveal">
947
1534
  <div class="chart-card full-width">
948
1535
  <div class="chart-header">
949
1536
  <h3>Token Burn Rate <i class="info-tip" data-tip="Daily token consumption stacked by type (input/output/cache). Dashed orange line shows cumulative total — watch it climb.">i</i></h3>
@@ -974,9 +1561,9 @@ function render() {
974
1561
  <div id="heatmap-container"></div>
975
1562
  </div>
976
1563
  </div>
977
- ${renderCacheEfficiency(t)}
978
- ${renderSurvival(d.lineSurvival)}
979
- ${renderSessionsTable(d.sessions)}
1564
+ <div class="scroll-reveal">${renderCacheEfficiency(t)}</div>
1565
+ <div class="scroll-reveal">${renderSurvival(d.lineSurvival)}</div>
1566
+ <div class="scroll-reveal">${renderSessionsTable(d.sessions)}</div>
980
1567
  `;
981
1568
 
982
1569
  if (typeof Chart !== 'undefined') {
@@ -987,7 +1574,7 @@ function render() {
987
1574
  }
988
1575
 
989
1576
  function renderHeroStats(s) {
990
- const gradeColor = GRADE_COLORS[s.overallGrade] || GRADE_COLORS.F;
1577
+ const gradeColor = GRADE_VAR[s.overallGrade] || GRADE_VAR.F;
991
1578
  const GRADE_DEG = { A: 324, B: 270, C: 216, D: 144, F: 72 };
992
1579
  const gradeDeg = GRADE_DEG[s.overallGrade] || 72;
993
1580
  return `<div class="stats-section">
@@ -1001,7 +1588,7 @@ function renderHeroStats(s) {
1001
1588
  <div class="sub">${s.totalSessions} sessions</div>
1002
1589
  </div>
1003
1590
  <div class="stat-value-wrap">
1004
- <div class="value" style="color: var(--accent-orange);">$${s.totalCost.toFixed(2)}</div>
1591
+ <div class="value" style="color: var(--accent-orange);" data-animate-value="${s.totalCost.toFixed(2)}" data-animate-prefix="$">$0.00</div>
1005
1592
  </div>
1006
1593
  </div>
1007
1594
  <div class="stat-card glow commits-card">
@@ -1011,7 +1598,7 @@ function renderHeroStats(s) {
1011
1598
  <div class="sub">${s.totalLinesAdded.toLocaleString()} lines added</div>
1012
1599
  </div>
1013
1600
  <div class="stat-value-wrap">
1014
- <div class="value" style="color: var(--accent-green);">${s.totalCommits}</div>
1601
+ <div class="value" style="color: var(--accent-green);" data-animate-value="${s.totalCommits}" data-animate-prefix="">0</div>
1015
1602
  </div>
1016
1603
  </div>
1017
1604
  <div class="stat-card glow avg-card">
@@ -1021,7 +1608,7 @@ function renderHeroStats(s) {
1021
1608
  <div class="sub">${s.totalFilesChanged} files changed</div>
1022
1609
  </div>
1023
1610
  <div class="stat-value-wrap">
1024
- <div class="value" style="color: var(--accent-blue);">${s.avgCostPerCommit !== null ? '$' + s.avgCostPerCommit.toFixed(2) : 'N/A'}</div>
1611
+ <div class="value" style="color: var(--accent-blue);" ${s.avgCostPerCommit !== null ? `data-animate-value="${s.avgCostPerCommit.toFixed(2)}" data-animate-prefix="$"` : ''}>${s.avgCostPerCommit !== null ? '$0.00' : 'N/A'}</div>
1025
1612
  </div>
1026
1613
  </div>
1027
1614
  </div>
@@ -1098,19 +1685,24 @@ function renderInsights(insights) {
1098
1685
 
1099
1686
  function renderSurvival(s) {
1100
1687
  const pct = s.survivalRate;
1101
- const color = pct >= 80 ? 'var(--accent-green)' : pct >= 50 ? 'var(--accent-orange)' : 'var(--accent-red)';
1688
+ const accentColor = pct >= 80 ? 'var(--accent-purple)' : pct >= 50 ? 'var(--accent-orange)' : 'var(--accent-red)';
1689
+ const gradient = pct >= 80
1690
+ ? 'linear-gradient(90deg, var(--accent-purple), var(--accent-blue))'
1691
+ : pct >= 50
1692
+ ? 'linear-gradient(90deg, var(--accent-orange), var(--accent-purple))'
1693
+ : 'var(--accent-red)';
1102
1694
  return `<div class="survival-section">
1103
1695
  <div class="survival-card">
1104
1696
  <h3>
1105
1697
  Line Survival Rate
1106
1698
  <i class="info-tip" data-tip="Of all lines your AI agent added, how many are still in the codebase after 24 hours. Approximate — based on churn detection, not exact git blame tracking.">i</i>
1107
1699
  </h3>
1108
- <div class="survival-bar"><div class="fill" style="width: ${pct}%; background: ${color};"></div></div>
1700
+ <div class="survival-bar"><div class="fill" style="width: ${pct}%; background: ${gradient};"></div></div>
1109
1701
  <div class="survival-stats">
1110
1702
  <div><span>${s.totalAdded.toLocaleString()}</span> lines added</div>
1111
1703
  <div><span>${s.totalChurned.toLocaleString()}</span> churned within 24h</div>
1112
1704
  <div><span>${s.surviving.toLocaleString()}</span> surviving</div>
1113
- <div><span style="color:${color};">${pct}%</span> survival rate</div>
1705
+ <div><span style="color:${accentColor};">${pct}%</span> survival rate</div>
1114
1706
  </div>
1115
1707
  </div>
1116
1708
  </div>`;
@@ -1231,12 +1823,12 @@ function renderTokenFunnel(t) {
1231
1823
  </div>
1232
1824
  <div class="funnel-bar">
1233
1825
  <div class="segment" style="width:${pProd}%;background:var(--accent-green);" title="Productive: ${formatTokens(t.funnel.productive)}">${parseFloat(pProd) > 8 ? pProd + '% Shipped' : ''}</div>
1234
- <div class="segment" style="width:${pExpl}%;background:rgba(88,166,255,0.4);" title="Exploratory: ${formatTokens(t.funnel.exploratory)}">${parseFloat(pExpl) > 8 ? pExpl + '% Idle' : ''}</div>
1826
+ <div class="segment" style="width:${pExpl}%;background:rgba(59,130,246,0.4);" title="Exploratory: ${formatTokens(t.funnel.exploratory)}">${parseFloat(pExpl) > 8 ? pExpl + '% Idle' : ''}</div>
1235
1827
  <div class="segment ${wasteHigh ? 'waste-pulse' : ''}" style="width:${pOrph}%;background:var(--accent-red);" title="WASTED: ${formatTokens(t.funnel.orphaned)}">${parseFloat(pOrph) > 5 ? pOrph + '% WASTED' : ''}</div>
1236
1828
  </div>
1237
1829
  <div class="funnel-legend">
1238
1830
  <span><span class="dot" style="background:var(--accent-green);"></span>Productive (${pProd}%) — led to commits</span>
1239
- <span><span class="dot" style="background:rgba(88,166,255,0.4);"></span>Exploratory (${pExpl}%) — short sessions, no commits</span>
1831
+ <span><span class="dot" style="background:rgba(59,130,246,0.4);"></span>Exploratory (${pExpl}%) — short sessions, no commits</span>
1240
1832
  <span><span class="dot" style="background:var(--accent-red);"></span>WASTED (${pOrph}%) — long sessions, zero output</span>
1241
1833
  </div>
1242
1834
  </div>
@@ -1261,19 +1853,24 @@ function renderTokenFunFacts(facts) {
1261
1853
 
1262
1854
  function renderCacheEfficiency(t) {
1263
1855
  const hitRate = t.cacheHitRate;
1264
- const color = hitRate >= 60 ? 'var(--accent-green)' : hitRate >= 30 ? 'var(--accent-orange)' : 'var(--accent-red)';
1856
+ const accentColor = hitRate >= 60 ? 'var(--accent-blue)' : hitRate >= 30 ? 'var(--accent-orange)' : 'var(--accent-red)';
1857
+ const gradient = hitRate >= 60
1858
+ ? 'linear-gradient(90deg, var(--accent-blue), var(--accent-cyan))'
1859
+ : hitRate >= 30
1860
+ ? 'linear-gradient(90deg, var(--accent-orange), var(--accent-blue))'
1861
+ : 'var(--accent-red)';
1265
1862
  return `<div class="survival-section">
1266
1863
  <div class="survival-card">
1267
1864
  <h3>
1268
1865
  Cache Efficiency
1269
1866
  <i class="info-tip" data-tip="Cache reads are 90% cheaper than regular input tokens. This shows what percentage of your input context was served from cache and how much money that saved you.">i</i>
1270
1867
  </h3>
1271
- <div class="survival-bar"><div class="fill" style="width: ${hitRate}%; background: ${color};"></div></div>
1868
+ <div class="survival-bar"><div class="fill" style="width: ${hitRate}%; background: ${gradient};"></div></div>
1272
1869
  <div class="survival-stats">
1273
1870
  <div><span>${formatTokens(t.totalCacheReadTokens)}</span> tokens from cache</div>
1274
1871
  <div><span>${formatTokens(t.totalInputTokens)}</span> fresh input tokens</div>
1275
- <div><span style="color:var(--accent-green);">$${t.cacheSavingsDollars.toFixed(2)}</span> saved</div>
1276
- <div><span style="color:${color};">${hitRate}%</span> cache hit rate</div>
1872
+ <div><span style="color:var(--accent-cyan);">$${t.cacheSavingsDollars.toFixed(2)}</span> saved</div>
1873
+ <div><span style="color:${accentColor};">${hitRate}%</span> cache hit rate</div>
1277
1874
  </div>
1278
1875
  </div>
1279
1876
  </div>`;
@@ -1312,7 +1909,8 @@ function renderSessionsTable(sessions) {
1312
1909
  <tbody>
1313
1910
  ${pageData.map((s, i) => {
1314
1911
  const idx = (page - 1) * pageSize + i;
1315
- const gradeColor = GRADE_COLORS[s.grade] || GRADE_COLORS.F;
1912
+ const gradeColor = GRADE_VAR[s.grade] || GRADE_VAR.F;
1913
+ const gradeBg = GRADE_BG_VAR[s.grade] || GRADE_BG_VAR.F;
1316
1914
  const primaryName = formatModelName(s.model || 'unknown');
1317
1915
  const subModels = Object.keys(s.modelBreakdown || {})
1318
1916
  .filter(m => m !== s.model)
@@ -1330,8 +1928,8 @@ function renderSessionsTable(sessions) {
1330
1928
  <td>${s.userMessageCount + s.assistantMessageCount}</td>
1331
1929
  <td>$${s.cost.totalCost.toFixed(2)}</td>
1332
1930
  <td>${s.commitCount}</td>
1333
- <td><span style="color:#3fb950">+${s.linesAdded.toLocaleString()}</span> / <span style="color:#f85149">-${s.linesDeleted.toLocaleString()}</span></td>
1334
- <td><span class="grade-badge" style="background:${gradeColor}22;color:${gradeColor};">${s.grade}</span></td>
1931
+ <td><span style="color:var(--accent-green)">+${s.linesAdded.toLocaleString()}</span> / <span style="color:var(--accent-red)">-${s.linesDeleted.toLocaleString()}</span></td>
1932
+ <td><span class="grade-badge" style="background:${gradeBg};color:${gradeColor};">${s.grade}</span></td>
1335
1933
  </tr>
1336
1934
  <tr class="expand-row" id="expand-${idx}">
1337
1935
  <td colspan="8">
@@ -1384,9 +1982,17 @@ function formatDate(iso) {
1384
1982
  }
1385
1983
 
1386
1984
  function initCharts() {
1387
- Chart.defaults.color = '#8b949e';
1388
- Chart.defaults.borderColor = '#30363d';
1389
- Chart.defaults.font.family = 'Inter';
1985
+ const isLight = getTheme() === 'light';
1986
+ Chart.defaults.color = isLight ? '#475569' : '#94a3b8';
1987
+ Chart.defaults.borderColor = isLight ? '#cbd5e1' : '#1f2d3d';
1988
+ Chart.defaults.font.family = "'DM Sans', sans-serif";
1989
+ Chart.defaults.plugins.tooltip.backgroundColor = isLight ? 'rgba(15, 23, 42, 0.92)' : 'rgba(10, 14, 23, 0.95)';
1990
+ Chart.defaults.plugins.tooltip.borderColor = isLight ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.08)';
1991
+ Chart.defaults.plugins.tooltip.borderWidth = 1;
1992
+ Chart.defaults.plugins.tooltip.cornerRadius = 8;
1993
+ Chart.defaults.plugins.tooltip.titleFont = { family: "'JetBrains Mono', monospace", size: 12, weight: '600' };
1994
+ Chart.defaults.plugins.tooltip.bodyFont = { family: "'DM Sans', sans-serif", size: 12 };
1995
+ Chart.defaults.plugins.tooltip.padding = { top: 10, right: 14, bottom: 10, left: 14 };
1390
1996
 
1391
1997
  // Token Burn Rate chart
1392
1998
  const dailyTokens = DATA.daily;
@@ -1401,21 +2007,21 @@ function initCharts() {
1401
2007
  {
1402
2008
  label: 'Input',
1403
2009
  data: dailyTokens.map(d => d.inputTokens || 0),
1404
- backgroundColor: 'rgba(88, 166, 255, 0.8)',
2010
+ backgroundColor: 'rgba(59, 130, 246, 0.8)',
1405
2011
  stack: 'tokens',
1406
2012
  borderRadius: 2,
1407
2013
  },
1408
2014
  {
1409
2015
  label: 'Output',
1410
2016
  data: dailyTokens.map(d => d.outputTokens || 0),
1411
- backgroundColor: 'rgba(188, 140, 255, 0.8)',
2017
+ backgroundColor: 'rgba(168, 85, 247, 0.8)',
1412
2018
  stack: 'tokens',
1413
2019
  borderRadius: 2,
1414
2020
  },
1415
2021
  {
1416
2022
  label: 'Cache Read',
1417
2023
  data: dailyTokens.map(d => d.cacheReadTokens || 0),
1418
- backgroundColor: 'rgba(57, 210, 192, 0.6)',
2024
+ backgroundColor: 'rgba(6, 182, 212, 0.6)',
1419
2025
  stack: 'tokens',
1420
2026
  borderRadius: 2,
1421
2027
  },
@@ -1423,7 +2029,7 @@ function initCharts() {
1423
2029
  label: 'Cumulative',
1424
2030
  data: cumulativeData,
1425
2031
  type: 'line',
1426
- borderColor: '#d29922',
2032
+ borderColor: '#f59e0b',
1427
2033
  borderDash: [5, 5],
1428
2034
  pointRadius: 0,
1429
2035
  fill: false,
@@ -1475,8 +2081,8 @@ function initCharts() {
1475
2081
  {
1476
2082
  label: 'Cost ($)',
1477
2083
  data: daily.map(d => d.cost),
1478
- borderColor: '#d29922',
1479
- backgroundColor: 'rgba(210,153,34,0.1)',
2084
+ borderColor: '#f59e0b',
2085
+ backgroundColor: 'rgba(245,158,11,0.1)',
1480
2086
  fill: true,
1481
2087
  tension: 0.3,
1482
2088
  yAxisID: 'y',
@@ -1485,8 +2091,8 @@ function initCharts() {
1485
2091
  {
1486
2092
  label: 'Lines Added',
1487
2093
  data: daily.map(d => d.linesAdded),
1488
- borderColor: '#3fb950',
1489
- backgroundColor: 'rgba(63,185,80,0.1)',
2094
+ borderColor: '#22d3a8',
2095
+ backgroundColor: 'rgba(34,211,168,0.1)',
1490
2096
  fill: true,
1491
2097
  tension: 0.3,
1492
2098
  yAxisID: 'y1',
@@ -1555,7 +2161,7 @@ function initCharts() {
1555
2161
  // Model doughnut
1556
2162
  const models = DATA.modelBreakdown;
1557
2163
  const modelLabels = Object.keys(models).filter(k => models[k].cost > 0);
1558
- const modelColors = { opus: '#bc8cff', sonnet: '#58a6ff', haiku: '#3fb950', unknown: '#8b949e' };
2164
+ const modelColors = { opus: '#a855f7', sonnet: '#3b82f6', haiku: '#22d3a8', unknown: '#94a3b8' };
1559
2165
  const modelTotalCost = modelLabels.reduce((s, m) => s + models[m].cost, 0);
1560
2166
  new Chart(document.getElementById('chart-models'), {
1561
2167
  type: 'doughnut',
@@ -1566,7 +2172,7 @@ function initCharts() {
1566
2172
  }),
1567
2173
  datasets: [{
1568
2174
  data: modelLabels.map(m => models[m].cost),
1569
- backgroundColor: modelLabels.map(m => modelColors[m] || '#8b949e'),
2175
+ backgroundColor: modelLabels.map(m => modelColors[m] || '#94a3b8'),
1570
2176
  borderWidth: 0,
1571
2177
  }],
1572
2178
  },
@@ -1626,14 +2232,14 @@ function initCharts() {
1626
2232
  {
1627
2233
  label: 'Avg $/Commit',
1628
2234
  data: bucketLabels.map(b => buckets[b].avgCostPerCommit),
1629
- backgroundColor: '#58a6ff',
2235
+ backgroundColor: '#3b82f6',
1630
2236
  borderRadius: 4,
1631
2237
  yAxisID: 'y',
1632
2238
  },
1633
2239
  {
1634
2240
  label: 'Sessions',
1635
2241
  data: bucketLabels.map(b => buckets[b].sessions),
1636
- backgroundColor: 'rgba(188,140,255,0.4)',
2242
+ backgroundColor: 'rgba(168,85,247,0.4)',
1637
2243
  borderRadius: 4,
1638
2244
  yAxisID: 'y1',
1639
2245
  },
@@ -1663,7 +2269,7 @@ function initHeatmap() {
1663
2269
  for (let hour = 0; hour < 24; hour++) {
1664
2270
  const val = heatmap[day][hour];
1665
2271
  const intensity = val / maxVal;
1666
- const bg = val === 0 ? 'var(--bg-hover)' : `rgba(63,185,80,${0.2 + intensity * 0.8})`;
2272
+ const bg = val === 0 ? 'var(--bg-hover)' : `rgba(34,211,168,${0.2 + intensity * 0.8})`;
1667
2273
  html += `<div class="heatmap-cell" style="background:${bg};" title="${DAY_LABELS[day]} ${hour}:00 — ${val} commits"></div>`;
1668
2274
  }
1669
2275
  }
@@ -1680,14 +2286,47 @@ function initHeatmap() {
1680
2286
  html += '<span>Fewer</span><div style="display:flex;gap:1px;">';
1681
2287
  const legendSteps = [0.2, 0.4, 0.6, 0.8, 1.0];
1682
2288
  for (const step of legendSteps) {
1683
- html += `<div style="width:14px;height:14px;border-radius:3px;background:rgba(63,185,80,${0.2 + step * 0.8});"></div>`;
2289
+ html += `<div style="width:14px;height:14px;border-radius:3px;background:rgba(34,211,168,${0.2 + step * 0.8});"></div>`;
1684
2290
  }
1685
2291
  html += '</div><span>More commits</span></div>';
1686
2292
 
1687
2293
  container.innerHTML = html;
1688
2294
  }
1689
2295
 
1690
- function bindEvents() {}
2296
+ function bindEvents() {
2297
+ // Animate hero stat values on first render
2298
+ if (!initialRenderDone) {
2299
+ document.querySelectorAll('[data-animate-value]').forEach(el => {
2300
+ const target = parseFloat(el.dataset.animateValue);
2301
+ const prefix = el.dataset.animatePrefix || '';
2302
+ const suffix = el.dataset.animateSuffix || '';
2303
+ animateValue(el, 0, target, 1200, prefix, suffix);
2304
+ });
2305
+
2306
+ // Animate bar fills
2307
+ document.querySelectorAll('.survival-bar .fill, .waste-bar .fill').forEach(el => {
2308
+ const targetWidth = el.style.width;
2309
+ el.style.width = '0%';
2310
+ requestAnimationFrame(() => {
2311
+ el.style.transition = 'width 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
2312
+ el.style.width = targetWidth;
2313
+ });
2314
+ });
2315
+
2316
+ // Scroll-triggered reveals
2317
+ const observer = new IntersectionObserver((entries) => {
2318
+ entries.forEach(entry => {
2319
+ if (entry.isIntersecting) {
2320
+ entry.target.classList.add('revealed');
2321
+ observer.unobserve(entry.target);
2322
+ }
2323
+ });
2324
+ }, { threshold: 0.1 });
2325
+ document.querySelectorAll('.scroll-reveal').forEach(el => observer.observe(el));
2326
+
2327
+ initialRenderDone = true;
2328
+ }
2329
+ }
1691
2330
 
1692
2331
  window.toggleExpand = function(idx) {
1693
2332
  const row = document.getElementById(`expand-${idx}`);