@krtclcdy/tersio 2.6.0 → 2.8.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.
@@ -8,26 +8,24 @@
8
8
  <link rel="icon" type="image/png" href="brand.webp">
9
9
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
10
10
  <script src="https://unpkg.com/lucide@latest"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/outfit@5/index.css">
11
14
  <style>
12
15
  :root {
13
16
  --bg: #f4f4f5; --panel: #ffffff; --ink: #18181b; --dim: #52525b;
14
17
  --line: rgba(24, 24, 27, .12); --accent: #047857; --accent-soft: rgba(4, 120, 87, .12);
15
18
  --track: rgba(24, 24, 27, .08); --grid: rgba(4, 120, 87, .07);
16
- --cell-0: rgba(24, 24, 27, .06);
17
- --cell-1: rgba(4, 120, 87, .25); --cell-2: rgba(4, 120, 87, .5);
18
- --cell-3: rgba(4, 120, 87, .75); --cell-4: #047857;
19
19
  }
20
20
  @media (prefers-color-scheme: dark) {
21
21
  :root {
22
22
  --bg: #09090b; --panel: #101014; --ink: #f4f4f5; --dim: #a1a1aa;
23
23
  --line: rgba(255, 255, 255, .09); --accent: #34d399; --accent-soft: rgba(52, 211, 153, .1);
24
24
  --track: rgba(255, 255, 255, .07); --grid: rgba(52, 211, 153, .05);
25
- --cell-0: rgba(255, 255, 255, .06);
26
- --cell-1: rgba(52, 211, 153, .25); --cell-2: rgba(52, 211, 153, .5);
27
- --cell-3: rgba(52, 211, 153, .75); --cell-4: #34d399;
28
25
  }
29
26
  }
30
27
  body { background: var(--bg); color: var(--ink); }
28
+ .display { font-family: "Outfit", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
31
29
  .bg-blueprint {
32
30
  background-image: linear-gradient(var(--grid) 1px, transparent 1px),
33
31
  linear-gradient(90deg, var(--grid) 1px, transparent 1px);
@@ -38,15 +36,10 @@
38
36
  .rowline { border-bottom: 1px solid var(--line); }
39
37
  .bar-fill { background: var(--accent); border-radius: 999px; transform-origin: left; }
40
38
  .bar-track { background: var(--track); border-radius: 999px; }
41
- .bar-dim { background: var(--dim); opacity: .55; border-radius: 999px; transform-origin: bottom; }
42
39
  .live-dot { background: var(--accent); box-shadow: 0 0 0 0 var(--accent-soft); animation: ping 2s cubic-bezier(0, 0, .2, 1) infinite; }
43
40
  @keyframes ping { 75%, 100% { box-shadow: 0 0 0 9px transparent; } }
44
41
  .rise { opacity: 0; transform: translateY(26px); }
45
42
  .rise.in { opacity: 1; transform: none; transition: opacity .7s cubic-bezier(.16, 1, .3, 1), transform .7s cubic-bezier(.16, 1, .3, 1); }
46
- .grow { transform: scaleX(0); transition: transform 1.1s cubic-bezier(.16, 1, .3, 1); }
47
- .grow.in { transform: scaleX(1); }
48
- .growY { transform: scaleY(0); transition: transform .9s cubic-bezier(.16, 1, .3, 1); }
49
- .growY.in { transform: scaleY(1); }
50
43
  .tilt { transition: transform .25s cubic-bezier(.16, 1, .3, 1), background .25s; }
51
44
  .tilt:hover { transform: translateX(6px); background: var(--accent-soft); }
52
45
  .btn-push { transition: transform .12s, background .2s; }
@@ -54,9 +47,12 @@
54
47
  .btn-push:active { transform: scale(.96); }
55
48
  a.social { color: var(--ink); }
56
49
  a.social:hover { color: var(--accent); }
57
- .cell { width: 11px; height: 11px; border-radius: 2.5px; background: var(--cell-0); }
58
- .cell.l1 { background: var(--cell-1); } .cell.l2 { background: var(--cell-2); }
59
- .cell.l3 { background: var(--cell-3); } .cell.l4 { background: var(--cell-4); }
50
+ .seg button { color: var(--dim); border-radius: 8px; transition: background .2s, color .2s; }
51
+ .seg button.on { background: var(--accent-soft); color: var(--ink); }
52
+ .seg button:not(.on):hover { color: var(--ink); }
53
+ .mbar { display: flex; flex-direction: column-reverse; border-radius: 6px 6px 2px 2px; overflow: hidden; min-height: 3px; background: var(--track); }
54
+ .mbar > span { display: block; width: 100%; }
55
+ .monogram { width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; font-weight: 700; font-size: 13px; color: #fff; flex-shrink: 0; }
60
56
  .dock {
61
57
  position: sticky; top: 12px; z-index: 50;
62
58
  background: color-mix(in srgb, var(--panel) 84%, transparent);
@@ -64,25 +60,18 @@
64
60
  border: 1px solid var(--line); border-radius: 20px;
65
61
  box-shadow: 0 12px 40px rgba(0, 0, 0, .25);
66
62
  }
67
- .dock a.docklink { transition: transform .22s cubic-bezier(.34, 1.56, .64, 1), background .2s; border-radius: 12px; }
68
- .dock a.docklink:hover { transform: scale(1.28) translateY(-3px); background: var(--accent-soft); }
69
- .dock a.docklink:active { transform: scale(1.05); }
70
63
  .dock img.brand { border-radius: 10px; }
71
- @media (prefers-reduced-motion: reduce) {
72
- .dock a.docklink { transition: none; }
73
- .dock a.docklink:hover { transform: none; }
74
- }
75
64
  @media (prefers-reduced-motion: reduce) {
76
65
  .rise { opacity: 1; transform: none; }
77
- .grow, .growY { transform: none; transition: none; }
78
66
  .live-dot { animation: none; }
79
- .tilt, .btn-push { transition: none; }
67
+ .tilt, .btn-push, .seg button { transition: none; }
80
68
  }
81
69
  </style>
82
70
  </head>
83
71
  <body class="min-h-[100dvh] antialiased">
84
72
  <div class="bg-blueprint pointer-events-none fixed inset-0" aria-hidden="true"></div>
85
73
 
74
+ <main class="overflow-x-hidden w-full max-w-full">
86
75
  <div class="relative mx-auto max-w-7xl px-4 sm:px-6 pb-16">
87
76
 
88
77
  <!-- Floating dock navbar -->
@@ -98,94 +87,99 @@
98
87
  <span class="live-dot inline-block size-2 rounded-full"></span>
99
88
  <span id="clock">--:--</span>
100
89
  </span>
101
- <button id="reload" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--line)" aria-label="Reload data">
90
+ <button id="reload" class="btn-push mono flex shrink-0 items-center gap-2 text-xs pl-2.5 pr-3 py-2 rounded-xl" style="border: 1px solid var(--line); color: var(--ink)" aria-label="Reload data">
102
91
  <i data-lucide="refresh-cw" class="size-3.5"></i><span class="hidden md:inline">Reload</span>
103
92
  </button>
104
93
  </header>
105
94
 
106
- <!-- Hero: asymmetric 7/5 -->
107
- <section class="grid grid-cols-1 lg:grid-cols-12 gap-8 pt-10 pb-8 items-end">
108
- <div class="lg:col-span-7 rise">
109
- <p class="mono text-[11px] uppercase tracking-[0.22em] mb-3" style="color: var(--accent)">Live token feed</p>
110
- <p class="mono font-bold tracking-tighter leading-none text-7xl md:text-8xl" id="total">0</p>
111
- <p class="mt-3 text-base max-w-[52ch]" style="color: var(--dim)">session tokens parsed from OMP transcripts. Input, output, cache read, cache write, USD cost, CO2.</p>
95
+ <!-- Hero: centered total -->
96
+ <section class="mx-auto max-w-6xl text-center pt-14 pb-10">
97
+ <p class="hero-in mono text-[11px] uppercase tracking-[0.22em] mb-4" style="color: var(--accent)">Live token feed</p>
98
+ <p class="hero-in mono font-bold tracking-tighter leading-none text-6xl md:text-7xl" id="total">0</p>
99
+ <p class="hero-in mono mt-4 text-sm max-w-[62ch] mx-auto" style="color: var(--dim)" id="herosub">session tokens parsed from OMP transcripts. Input, output, cache read, cache write, USD cost, CO2.</p>
100
+ </section>
101
+
102
+ <!-- Savings bento: 3+3+2+2+2 -->
103
+ <section class="grid grid-cols-12 grid-flow-dense gap-3" aria-label="Savings">
104
+ <div class="rise col-span-6 md:col-span-3 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
105
+ <p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">usd cost</p>
106
+ <p class="mono font-bold text-3xl mt-1" id="usd">-</p>
107
+ <p class="mono text-[11px] mt-1" style="color: var(--dim)" id="usdNote">per-model price table</p>
108
+ </div>
109
+ <div class="rise col-span-6 md:col-span-3 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
110
+ <p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">saved by cache</p>
111
+ <p class="mono font-bold text-3xl mt-1" style="color: var(--accent)" id="saved">-</p>
112
+ <p class="mono text-[11px] mt-1" style="color: var(--dim)">est. vs full input price</p>
113
+ </div>
114
+ <div class="rise col-span-4 md:col-span-2 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
115
+ <p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">co2</p>
116
+ <p class="mono font-bold text-3xl mt-1" id="co2">-</p>
117
+ <p class="mono text-[11px] mt-1" style="color: var(--dim)">est. from output</p>
112
118
  </div>
113
- <div class="lg:col-span-5 rise" style="transition-delay: .12s">
114
- <dl class="mono text-sm">
115
- <div class="flex justify-between gap-4 py-2 rowline"><dt style="color: var(--dim)">usd cost</dt><dd id="usd">-</dd></div>
116
- <div class="flex justify-between gap-4 py-2 rowline"><dt style="color: var(--dim)">co2</dt><dd id="co2">-</dd></div>
117
- <div class="flex justify-between gap-4 py-2"><dt style="color: var(--dim)">assistant msgs</dt><dd id="msgs">0</dd></div>
118
- </dl>
119
+ <div class="rise col-span-4 md:col-span-2 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
120
+ <p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">cache share</p>
121
+ <p class="mono font-bold text-3xl mt-1" id="cacheShare">-</p>
122
+ <p class="mono text-[11px] mt-1" style="color: var(--dim)">of tokens cached</p>
123
+ </div>
124
+ <div class="rise col-span-4 md:col-span-2 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
125
+ <p class="mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">assistant msgs</p>
126
+ <p class="mono font-bold text-3xl mt-1" id="msgs">0</p>
127
+ <p class="mono text-[11px] mt-1" style="color: var(--dim)" id="msgNote">sessions parsed</p>
119
128
  </div>
120
129
  </section>
121
130
 
122
- <!-- Token strip: plain stats, dividers, no cards -->
123
- <section class="rise grid grid-cols-2 sm:grid-cols-4 rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" id="tokens" aria-label="Tokens by bucket"></section>
131
+ <!-- Token strip: nonzero buckets only -->
132
+ <section class="rise mt-3 flex rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)" id="tokens" aria-label="Tokens by bucket"></section>
124
133
 
125
- <!-- Contribution graph, GitHub style -->
126
- <section id="activity" class="rise mt-8 rounded-xl p-5 overflow-hidden" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Activity graph">
127
- <div class="flex items-center gap-2 mb-1">
128
- <i data-lucide="calendar-days" class="size-4" style="color: var(--accent)"></i>
129
- <h2 class="font-bold tracking-tight text-xl">Activity</h2>
130
- <span class="mono text-xs ml-auto" style="color: var(--dim)" id="graphRange"></span>
131
- </div>
132
- <p class="mono text-xs mb-4" style="color: var(--dim)">daily session tokens / last 26 weeks / darker means more</p>
133
- <div class="overflow-x-auto pb-1">
134
- <div class="flex gap-[3px] w-max" id="graph"></div>
135
- </div>
136
- <div class="mono text-[11px] mt-3 flex items-center gap-1.5" style="color: var(--dim)">
137
- <span>less</span>
138
- <span class="cell"></span><span class="cell l1"></span><span class="cell l2"></span><span class="cell l3"></span><span class="cell l4"></span>
139
- <span>more</span>
134
+ <!-- Token activity: stacked per-model bars + range toggle -->
135
+ <section id="activity" class="rise mt-8 rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Token activity">
136
+ <div class="flex flex-wrap items-center gap-3 mb-1">
137
+ <i data-lucide="chart-column" class="size-4" style="color: var(--accent)"></i>
138
+ <h2 class="display font-bold tracking-tight text-xl">Token activity</h2>
139
+ <div class="seg mono text-xs flex items-center gap-1 ml-auto p-1 rounded-[10px]" style="border: 1px solid var(--line)" role="tablist" aria-label="Range">
140
+ <button data-range="daily" class="on px-3 py-1.5" role="tab">Daily</button>
141
+ <button data-range="weekly" class="px-3 py-1.5" role="tab">Weekly</button>
142
+ <button data-range="cumulative" class="px-3 py-1.5" role="tab">Cumulative</button>
143
+ </div>
140
144
  </div>
145
+ <p class="mono text-xs mb-4" style="color: var(--dim)" id="rangeNote">stacked by model / last 60 days</p>
146
+ <div class="flex items-end gap-[3px] h-56" id="bars"></div>
147
+ <div class="flex gap-[3px] mt-2 mono text-[10px]" style="color: var(--dim)" id="barDays"></div>
148
+ <div class="flex flex-wrap gap-x-4 gap-y-1.5 mt-4 mono text-[11px]" style="color: var(--dim)" id="legend"></div>
141
149
  </section>
142
150
 
143
- <!-- Main: 8/4 split -->
144
- <main class="grid grid-cols-1 lg:grid-cols-12 gap-8 mt-8">
151
+ <!-- Top models: 3 cards -->
152
+ <section class="mt-8" aria-label="Top models">
153
+ <div class="flex items-center gap-2 mb-1">
154
+ <i data-lucide="cpu" class="size-4" style="color: var(--accent)"></i>
155
+ <h2 class="display font-bold tracking-tight text-xl">Top models</h2>
156
+ <span class="mono text-xs ml-auto" style="color: var(--dim)">usage across sessions</span>
157
+ </div>
158
+ <p class="mono text-xs mb-4" style="color: var(--dim)">week-over-week vs prior 7 days</p>
159
+ <div class="grid grid-cols-1 md:grid-cols-12 grid-flow-dense gap-3" id="modelCards"></div>
160
+ </section>
145
161
 
146
- <div class="lg:col-span-8 flex flex-col gap-8">
147
- <section id="daily" class="rise" style="scroll-margin-top: 90px" aria-label="Daily chart">
162
+ <!-- Models list + top tools: 8 + 4 -->
163
+ <main class="grid grid-cols-1 lg:grid-cols-12 grid-flow-dense gap-8 mt-8">
164
+ <div class="lg:col-span-8">
165
+ <section class="rise" aria-label="All models">
148
166
  <div class="flex items-center gap-2 mb-1">
149
- <i data-lucide="chart-column" class="size-4" style="color: var(--accent)"></i>
150
- <h2 class="font-bold tracking-tight text-xl">Daily input vs output</h2>
151
- </div>
152
- <p class="mono text-xs mb-4" style="color: var(--dim)">last 14 days / tall bar input, short bar output</p>
153
- <div class="rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel)">
154
- <div class="flex items-end gap-2 h-36" id="chart"></div>
155
- <div class="flex gap-2 mt-2 mono text-[10px]" style="color: var(--dim)" id="chartDays"></div>
167
+ <i data-lucide="layers" class="size-4" style="color: var(--accent)"></i>
168
+ <h2 class="display font-bold tracking-tight text-xl">Models</h2>
156
169
  </div>
170
+ <p class="mono text-xs mb-4" style="color: var(--dim)">ranked by tokens / top 8</p>
171
+ <ol id="models" class="rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)"></ol>
157
172
  </section>
158
-
159
- <section id="commands" class="rise" style="scroll-margin-top: 90px" aria-label="Top commands">
173
+ </div>
174
+ <div class="lg:col-span-4">
175
+ <section id="tools" class="rise rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel); scroll-margin-top: 90px" aria-label="Top tools">
160
176
  <div class="flex items-center gap-2 mb-1">
161
177
  <i data-lucide="terminal" class="size-4" style="color: var(--accent)"></i>
162
- <h2 class="font-bold tracking-tight text-xl">Top commands</h2>
163
- </div>
164
- <p class="mono text-xs mb-4" style="color: var(--dim)">ranked by ledger count / top 10</p>
165
- <ol id="top" class="rounded-xl overflow-hidden" style="border: 1px solid var(--line); background: var(--panel)"></ol>
166
- <p id="emptyTop" class="mono text-sm py-8 text-center hidden" style="color: var(--dim)">No ledger rows yet. Run tersio commands to record activity.</p>
167
- </section>
168
- </div>
169
-
170
- <div class="lg:col-span-4 flex flex-col gap-8">
171
- <section id="savings" class="rise rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel); transition-delay: .1s; scroll-margin-top: 90px" aria-label="Savings">
172
- <div class="flex items-center gap-2 mb-3">
173
- <i data-lucide="gauge" class="size-4" style="color: var(--accent)"></i>
174
- <h2 class="font-bold tracking-tight text-xl">Savings</h2>
175
- </div>
176
- <ul class="mono text-[13px] leading-relaxed space-y-2">
177
- <li class="flex gap-2"><i data-lucide="circle-dollar-sign" class="size-4 shrink-0 mt-0.5" style="color: var(--accent)"></i><span id="usdLine">USD from per-model price table.</span></li>
178
- <li class="flex gap-2"><i data-lucide="leaf" class="size-4 shrink-0 mt-0.5" style="color: var(--accent)"></i><span id="co2Line">CO2 ~est. from output tokens.</span></li>
179
- <li class="flex gap-2"><i data-lucide="database" class="size-4 shrink-0 mt-0.5" style="color: var(--accent)"></i><span id="cacheLine">Cache share loads with session data.</span></li>
180
- </ul>
181
- </section>
182
-
183
- <section class="rise rounded-xl p-5" style="border: 1px solid var(--line); background: var(--panel); transition-delay: .18s" aria-label="Models">
184
- <div class="flex items-center gap-2 mb-3">
185
- <i data-lucide="cpu" class="size-4" style="color: var(--accent)"></i>
186
- <h2 class="font-bold tracking-tight text-xl">Models</h2>
178
+ <h2 class="display font-bold tracking-tight text-xl">Top tools</h2>
187
179
  </div>
188
- <ol id="models" class="mono text-[13px] space-y-2" style="scroll-margin-top: 90px"></ol>
180
+ <p class="mono text-xs mb-4" style="color: var(--dim)">tool calls in sessions</p>
181
+ <ol id="top" class="mono text-[13px] space-y-3"></ol>
182
+ <p id="emptyTop" class="mono text-sm py-6 text-center hidden" style="color: var(--dim)">No tool calls yet.</p>
189
183
  </section>
190
184
  </div>
191
185
  </main>
@@ -200,24 +194,29 @@
200
194
  <a id="rawlink" href="data.json" class="underline underline-offset-2">raw data.json</a>
201
195
  </footer>
202
196
  </div>
197
+ </main>
203
198
 
204
199
  <script>
205
200
  (function () {
206
201
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
202
+ var hasGsap = !reduce && typeof window.gsap !== 'undefined';
203
+ var hasST = hasGsap && typeof window.ScrollTrigger !== 'undefined';
204
+ if (hasST) gsap.registerPlugin(ScrollTrigger);
207
205
 
208
206
  function fmt(n) { return Number(n).toLocaleString('en-US'); }
207
+ function fmtShort(n) {
208
+ if (n >= 1e12) return (n / 1e12).toFixed(1) + 'T';
209
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
210
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
211
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
212
+ return String(Math.round(n));
213
+ }
209
214
 
210
215
  function countUp(el, target, money) {
211
216
  function text(v) { return money ? '$' + v.toFixed(2) : fmt(Math.round(v)); }
212
- if (reduce || target <= 0) { el.textContent = text(target); return; }
213
- var t0 = null, dur = 900;
214
- function step(t) {
215
- if (!t0) t0 = t;
216
- var p = Math.min((t - t0) / dur, 1), e = 1 - Math.pow(1 - p, 3);
217
- el.textContent = text(target * e);
218
- if (p < 1) requestAnimationFrame(step);
219
- }
220
- requestAnimationFrame(step);
217
+ if (!hasGsap || target <= 0) { el.textContent = text(target); return; }
218
+ var o = { v: 0 };
219
+ gsap.to(o, { v: target, duration: 1.1, ease: 'power3.out', onUpdate: function () { el.textContent = text(o.v); } });
221
220
  }
222
221
 
223
222
  function tick() {
@@ -226,86 +225,257 @@
226
225
  }
227
226
  tick(); setInterval(tick, 1000);
228
227
 
228
+ var VENDORS = [
229
+ [/glm|z-ai|zhipu/i, 'Zhipu', 'Z', '#3b82f6'],
230
+ [/muse/i, 'Meta', 'M', '#0082fb'],
231
+ [/qwen|qwq/i, 'Alibaba', 'Q', '#7c3aed'],
232
+ [/deepseek/i, 'DeepSeek', 'D', '#4d6bfe'],
233
+ [/mimo/i, 'Xiaomi', 'Mi', '#ff6900'],
234
+ [/kimi|moonshot/i, 'Moonshot', 'K', '#a855f7'],
235
+ [/minimax/i, 'MiniMax', 'M', '#e11d48'],
236
+ [/nemotron|nvidia/i, 'NVIDIA', 'N', '#76b900'],
237
+ [/gpt|openai|codex/i, 'OpenAI', 'O', '#10a37f'],
238
+ [/claude|anthropic/i, 'Anthropic', 'A', '#d97757'],
239
+ [/gemini|google/i, 'Google', 'G', '#4285f4'],
240
+ [/grok|xai/i, 'xAI', 'X', '#71717a']
241
+ ];
242
+ function vendorOf(model) {
243
+ for (var i = 0; i < VENDORS.length; i++) {
244
+ if (VENDORS[i][0].test(model)) return { name: VENDORS[i][1], mark: VENDORS[i][2], color: VENDORS[i][3] };
245
+ }
246
+ return { name: 'Other', mark: (model || '?').charAt(0).toUpperCase(), color: '#71717a' };
247
+ }
248
+ var PALETTE = ['#34d399', '#818cf8', '#22d3ee', '#fbbf24', '#f472b6', '#a78bfa', '#fb923c', '#2dd4bf'];
249
+
229
250
  function dayKey(d) {
230
251
  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
231
252
  }
253
+ function modelTotal(byModel, m) {
254
+ var b = byModel[m] || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
255
+ return b.input + b.output + b.cacheRead + b.cacheWrite;
256
+ }
232
257
 
233
- function renderGraph(byDay) {
234
- var g = document.getElementById('graph');
235
- g.innerHTML = '';
236
- var today = new Date(); today.setHours(0, 0, 0, 0);
237
- var start = new Date(today); start.setDate(start.getDate() - (26 * 7 - 1) - today.getDay());
238
- var max = 1, sums = {};
239
- Object.keys(byDay).forEach(function (day) {
240
- var b = byDay[day];
241
- sums[day] = b.input + b.output + b.cacheRead + b.cacheWrite;
242
- if (sums[day] > max) max = sums[day];
243
- });
244
- var weeks = 0;
245
- for (var w = 0; w < 26; w++) {
258
+ var DATA = null, RANGE = 'daily';
259
+
260
+ function topModels(byModel, n) {
261
+ return Object.keys(byModel).sort(function (a, b) { return modelTotal(byModel, b) - modelTotal(byModel, a); }).slice(0, n);
262
+ }
263
+
264
+ function allDays() {
265
+ var set = {};
266
+ Object.keys(DATA.byDay || {}).forEach(function (d) { set[d] = 1; });
267
+ Object.keys(DATA.byDayModel || {}).forEach(function (d) { set[d] = 1; });
268
+ return Object.keys(set).sort();
269
+ }
270
+
271
+ // Buckets: [{ key, label, total, segs: [[model, v]] }]
272
+ function buckets() {
273
+ var days = allDays(), tops = topModels(DATA.byModel || {}, 8), out = [];
274
+ if (RANGE === 'daily') {
275
+ var last = days.slice(-60);
276
+ last.forEach(function (d) {
277
+ var per = (DATA.byDayModel || {})[d] || {}, segs = [], total = 0, other = 0;
278
+ tops.forEach(function (m) { var v = per[m] || 0; if (v) { segs.push([m, v]); total += v; } });
279
+ Object.keys(per).forEach(function (m) { if (tops.indexOf(m) < 0) other += per[m]; });
280
+ if (other) { segs.push(['other', other]); total += other; }
281
+ var dt = new Date(d + 'T12:00:00');
282
+ out.push({ key: d, label: isNaN(dt) ? '' : String(dt.getDate()), total: total, segs: segs });
283
+ });
284
+ document.getElementById('rangeNote').textContent = 'stacked by model / last 60 days';
285
+ } else if (RANGE === 'weekly') {
286
+ var weeks = {}, order = [];
287
+ days.forEach(function (d) {
288
+ var dt = new Date(d + 'T12:00:00'); if (isNaN(dt)) return;
289
+ var monday = new Date(dt); monday.setDate(dt.getDate() - ((dt.getDay() + 6) % 7));
290
+ var k = dayKey(monday);
291
+ if (!weeks[k]) { weeks[k] = {}; order.push(k); }
292
+ var per = (DATA.byDayModel || {})[d] || {};
293
+ Object.keys(per).forEach(function (m) { weeks[k][m] = (weeks[k][m] || 0) + per[m]; });
294
+ });
295
+ order.slice(-26).forEach(function (k) {
296
+ var per = weeks[k], segs = [], total = 0, other = 0;
297
+ tops.forEach(function (m) { var v = per[m] || 0; if (v) { segs.push([m, v]); total += v; } });
298
+ Object.keys(per).forEach(function (m) { if (tops.indexOf(m) < 0) other += per[m]; });
299
+ if (other) { segs.push(['other', other]); total += other; }
300
+ var dt = new Date(k + 'T12:00:00');
301
+ out.push({ key: k, label: isNaN(dt) ? '' : (dt.toLocaleString('en-US', { month: 'short' }) + ' ' + dt.getDate()), total: total, segs: segs });
302
+ });
303
+ document.getElementById('rangeNote').textContent = 'stacked by model / last 26 weeks';
304
+ } else {
305
+ var run = {}, cum = 0;
306
+ days.forEach(function (d) {
307
+ var per = (DATA.byDayModel || {})[d] || {}, segs = [], other = 0;
308
+ tops.forEach(function (m) {
309
+ var v = per[m] || 0;
310
+ run[m] = (run[m] || 0) + v;
311
+ if (run[m]) segs.push([m, run[m]]);
312
+ });
313
+ Object.keys(per).forEach(function (m) { if (tops.indexOf(m) < 0) { run[m] = (run[m] || 0) + per[m]; } });
314
+ other = Object.keys(run).reduce(function (a, m) { return tops.indexOf(m) < 0 ? a + run[m] : a; }, 0);
315
+ var tot = segs.reduce(function (a, s) { return a + s[1]; }, 0) + other;
316
+ if (other) segs.push(['other', other]);
317
+ cum = tot;
318
+ var dt = new Date(d + 'T12:00:00');
319
+ out.push({ key: d, label: isNaN(dt) ? '' : String(dt.getDate()), total: tot, segs: segs });
320
+ });
321
+ out = out.slice(-60);
322
+ document.getElementById('rangeNote').textContent = 'running total by model / all time';
323
+ }
324
+ return { list: out, tops: tops };
325
+ }
326
+
327
+ function renderBars() {
328
+ var b = buckets(), wrap = document.getElementById('bars'), axis = document.getElementById('barDays');
329
+ wrap.innerHTML = ''; axis.innerHTML = '';
330
+ var max = 1;
331
+ b.list.forEach(function (r) { max = Math.max(max, r.total); });
332
+ b.list.forEach(function (r) {
246
333
  var col = document.createElement('div');
247
- col.className = 'flex flex-col gap-[3px]';
248
- for (var i = 0; i < 7; i++) {
249
- var dt = new Date(start); dt.setDate(dt.getDate() + weeks * 7 + i);
250
- var key = dayKey(dt), v = sums[key] || 0, lvl = 0;
251
- if (v > 0) lvl = Math.min(4, 1 + Math.floor((v / max) * 3.99));
252
- var cell = document.createElement('span');
253
- cell.className = 'cell' + (lvl ? ' l' + lvl : '');
254
- cell.title = key + ': ' + fmt(v) + ' tokens';
255
- col.appendChild(cell);
334
+ col.className = 'flex-1 flex items-end h-full';
335
+ col.title = r.key + ': ' + fmt(r.total) + ' tokens' +
336
+ (r.segs.length ? '\n' + r.segs.map(function (s) { return s[0] + ' ' + fmt(s[1]); }).join('\n') : '');
337
+ var bar = document.createElement('div');
338
+ bar.className = 'mbar w-full';
339
+ bar.style.height = Math.max(2, Math.round(r.total / max * 100)) + '%';
340
+ var order = r.segs.slice().sort(function (x, y) { return y[1] - x[1]; });
341
+ order.forEach(function (s) {
342
+ var seg = document.createElement('span');
343
+ var ci = s[0] === 'other' ? -1 : b.tops.indexOf(s[0]);
344
+ seg.style.background = ci < 0 ? 'var(--dim)' : PALETTE[ci % PALETTE.length];
345
+ seg.style.height = Math.max(1, Math.round(s[1] / r.total * 100)) + '%';
346
+ bar.appendChild(seg);
347
+ });
348
+ col.appendChild(bar); wrap.appendChild(col);
349
+ var lb = document.createElement('span');
350
+ lb.className = 'flex-1 text-center truncate'; lb.textContent = r.label;
351
+ axis.appendChild(lb);
352
+ });
353
+ var legend = document.getElementById('legend');
354
+ legend.innerHTML = '';
355
+ b.tops.slice(0, 8).forEach(function (m, i) {
356
+ var s = document.createElement('span');
357
+ s.className = 'flex items-center gap-1.5';
358
+ var dot = document.createElement('span');
359
+ dot.style.cssText = 'width:9px;height:9px;border-radius:3px;background:' + PALETTE[i % PALETTE.length] + ';display:inline-block';
360
+ s.appendChild(dot);
361
+ s.appendChild(document.createTextNode(m));
362
+ legend.appendChild(s);
363
+ });
364
+ if (hasGsap) {
365
+ gsap.from('#bars > div', { scaleY: 0.2, opacity: 0.2, transformOrigin: 'bottom', duration: 0.5, ease: 'power2.out', stagger: 0.008, overwrite: true });
366
+ if (hasST) {
367
+ gsap.from('#modelCards > div', {
368
+ scale: 0.92, opacity: 0.2, duration: 0.6, ease: 'power2.out', stagger: 0.08, overwrite: true,
369
+ scrollTrigger: { trigger: '#modelCards', start: 'top 88%' }
370
+ });
256
371
  }
257
- weeks++;
258
- g.appendChild(col);
259
372
  }
260
- document.getElementById('graphRange').textContent =
261
- dayKey(start) + ' to ' + dayKey(today);
262
373
  }
263
374
 
264
- function renderChart(byDay) {
265
- var chart = document.getElementById('chart'), days = document.getElementById('chartDays');
266
- chart.innerHTML = ''; days.innerHTML = '';
267
- var list = [], today = new Date(); today.setHours(0, 0, 0, 0);
268
- for (var i = 13; i >= 0; i--) {
269
- var dt = new Date(today); dt.setDate(dt.getDate() - i);
270
- list.push({ key: dayKey(dt), label: String(dt.getDate()), b: byDay[dayKey(dt)] || { input: 0, output: 0 } });
271
- }
375
+ function wow(model) {
376
+ var days = allDays().slice(-14), cur = 0, prev = 0;
377
+ days.forEach(function (d, i) {
378
+ var v = ((DATA.byDayModel || {})[d] || {})[model] || 0;
379
+ if (i < days.length - 7) prev += v; else cur += v;
380
+ });
381
+ if (!prev) return cur ? 'new' : '—';
382
+ var p = Math.round((cur - prev) / prev * 100);
383
+ return (p >= 0 ? '+' : '') + p + '%';
384
+ }
385
+
386
+ function renderModels() {
387
+ var byModel = DATA.byModel || {}, tops = topModels(byModel, 8);
388
+ var cards = document.getElementById('modelCards');
389
+ cards.innerHTML = '';
390
+ tops.slice(0, 3).forEach(function (m, i) {
391
+ var v = vendorOf(m), d = wow(m);
392
+ var card = document.createElement('div');
393
+ card.className = 'rise md:col-span-4 rounded-xl p-5 overflow-hidden relative';
394
+ card.style.cssText = 'border: 1px solid var(--line); background: var(--panel)';
395
+ var ghost = document.createElement('span');
396
+ ghost.className = 'display pointer-events-none select-none absolute -right-2 -bottom-7 font-bold';
397
+ ghost.style.cssText = 'font-size: 120px; line-height: 1; opacity: .07';
398
+ ghost.textContent = v.mark;
399
+ var head = document.createElement('p');
400
+ head.className = 'mono text-xs mb-3'; head.style.color = 'var(--dim)';
401
+ head.textContent = '0' + (i + 1);
402
+ var row = document.createElement('div');
403
+ row.className = 'flex items-center gap-3';
404
+ var badge = document.createElement('span');
405
+ badge.className = 'monogram'; badge.style.background = v.color; badge.textContent = v.mark;
406
+ var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
407
+ var nm = document.createElement('p'); nm.className = 'mono text-sm font-bold truncate'; nm.textContent = m;
408
+ var vn = document.createElement('p'); vn.className = 'mono text-xs truncate'; vn.style.color = 'var(--dim)'; vn.textContent = v.name;
409
+ mid.appendChild(nm); mid.appendChild(vn);
410
+ var tot = document.createElement('p'); tot.className = 'mono font-bold text-lg shrink-0'; tot.textContent = fmtShort(modelTotal(byModel, m));
411
+ row.appendChild(badge); row.appendChild(mid); row.appendChild(tot);
412
+ var delta = document.createElement('p');
413
+ delta.className = 'mono text-xs mt-3';
414
+ delta.style.color = d.charAt(0) === '-' ? '#f87171' : 'var(--accent)';
415
+ delta.textContent = d;
416
+ card.appendChild(ghost); card.appendChild(head); card.appendChild(row); card.appendChild(delta);
417
+ cards.appendChild(card);
418
+ });
419
+ var ol = document.getElementById('models');
420
+ ol.innerHTML = '';
272
421
  var max = 1;
273
- list.forEach(function (d) { max = Math.max(max, d.b.input, d.b.output); });
274
- list.forEach(function (d, i) {
275
- var wrap = document.createElement('div');
276
- wrap.className = 'flex-1 flex items-end justify-center gap-1 h-full';
277
- wrap.title = d.key + ': in ' + fmt(d.b.input) + ' / out ' + fmt(d.b.output);
278
- var bi = document.createElement('div');
279
- bi.className = 'bar-fill growY w-3';
280
- bi.style.height = Math.max(2, Math.round(d.b.input / max * 100)) + '%';
281
- bi.style.transitionDelay = reduce ? '' : (i * 40) + 'ms';
282
- var bo = document.createElement('div');
283
- bo.className = 'bar-dim growY w-3';
284
- bo.style.height = Math.max(2, Math.round(d.b.output / max * 100)) + '%';
285
- bo.style.transitionDelay = reduce ? '' : (i * 40 + 60) + 'ms';
286
- wrap.appendChild(bi); wrap.appendChild(bo); chart.appendChild(wrap);
287
- var lb = document.createElement('span');
288
- lb.className = 'flex-1 text-center'; lb.textContent = d.label;
289
- days.appendChild(lb);
422
+ tops.forEach(function (m) { max = Math.max(max, modelTotal(byModel, m)); });
423
+ tops.forEach(function (m, i) {
424
+ var v = vendorOf(m);
425
+ var li = document.createElement('li');
426
+ li.className = 'tilt flex items-center gap-3 px-4 py-3' + (i < tops.length - 1 ? ' rowline' : '');
427
+ var badge = document.createElement('span');
428
+ badge.className = 'monogram'; badge.style.cssText = 'width:28px;height:28px;font-size:11px;border-radius:8px;background:' + v.color;
429
+ badge.textContent = v.mark;
430
+ badge.title = v.name;
431
+ var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
432
+ var nm = document.createElement('p'); nm.className = 'mono text-sm truncate'; nm.textContent = m;
433
+ var track = document.createElement('div'); track.className = 'bar-track mt-1.5 h-1.5 overflow-hidden';
434
+ var fill = document.createElement('div'); fill.className = 'bar-fill h-full';
435
+ fill.style.width = Math.max(2, Math.round(modelTotal(byModel, m) / max * 100)) + '%';
436
+ track.appendChild(fill); mid.appendChild(nm); mid.appendChild(track);
437
+ var n = document.createElement('span'); n.className = 'mono font-bold shrink-0'; n.textContent = fmt(modelTotal(byModel, m));
438
+ li.appendChild(badge); li.appendChild(mid); li.appendChild(n);
439
+ ol.appendChild(li);
290
440
  });
441
+ if (!tops.length) ol.innerHTML = '<li class="mono text-sm px-4 py-6" style="color: var(--dim)">no session tokens yet</li>';
291
442
  }
292
443
 
293
- function render(d) {
294
- var t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
295
- var total = t.input + t.output + t.cacheRead + t.cacheWrite;
296
- document.getElementById('meta').textContent =
297
- fmt(total) + ' session tokens / ' + d.total + ' ledger rows';
298
- countUp(document.getElementById('total'), total);
299
- countUp(document.getElementById('usd'), d.usd || 0, true);
300
- document.getElementById('co2').textContent = '~' + (d.co2g || 0).toFixed(1) + 'g (est.)';
301
- document.getElementById('msgs').textContent = fmt(d.messages || 0);
444
+ function renderTools() {
445
+ var ol = document.getElementById('top');
446
+ ol.innerHTML = '';
447
+ var tools = DATA.byTool || [], max = 1;
448
+ tools.forEach(function (r) { max = Math.max(max, r[1]); });
449
+ tools.forEach(function (r) {
450
+ var li = document.createElement('li');
451
+ var top = document.createElement('div');
452
+ top.className = 'flex justify-between gap-2';
453
+ var nm = document.createElement('span'); nm.className = 'truncate'; nm.textContent = r[0];
454
+ var n = document.createElement('span'); n.className = 'font-bold'; n.textContent = fmt(r[1]);
455
+ top.appendChild(nm); top.appendChild(n);
456
+ var track = document.createElement('div'); track.className = 'bar-track mt-1 h-1 overflow-hidden';
457
+ var fill = document.createElement('div'); fill.className = 'bar-fill h-full';
458
+ fill.style.width = Math.max(2, Math.round(r[1] / max * 100)) + '%';
459
+ track.appendChild(fill);
460
+ li.appendChild(top); li.appendChild(track);
461
+ ol.appendChild(li);
462
+ });
463
+ document.getElementById('emptyTop').classList.toggle('hidden', tools.length > 0);
464
+ }
302
465
 
303
- var buckets = [['input', t.input, 'arrow-down-to-line'], ['output', t.output, 'arrow-up-from-line'], ['cache read', t.cacheRead, 'hard-drive-download'], ['cache write', t.cacheWrite, 'hard-drive-upload']];
466
+ function renderStrip(t) {
467
+ var defs = [
468
+ ['input', t.input, 'arrow-down-to-line'],
469
+ ['output', t.output, 'arrow-up-from-line'],
470
+ ['cache read', t.cacheRead, 'hard-drive-download'],
471
+ ['cache write', t.cacheWrite, 'hard-drive-upload']
472
+ ].filter(function (b) { return b[1] > 0; });
473
+ if (!defs.length) defs = [['input', 0, 'arrow-down-to-line'], ['output', 0, 'arrow-up-from-line']];
304
474
  var strip = document.getElementById('tokens');
305
475
  strip.innerHTML = '';
306
- buckets.forEach(function (b, i) {
476
+ defs.forEach(function (b, i) {
307
477
  var cell = document.createElement('div');
308
- cell.className = 'tilt px-4 py-4' + (i > 0 ? ' sm:border-l' : '');
478
+ cell.className = 'tilt flex-1 px-4 py-4' + (i > 0 ? ' sm:border-l' : '');
309
479
  cell.style.borderColor = 'var(--line)';
310
480
  cell.innerHTML = '<div class="flex items-center gap-2 mono text-[11px] uppercase tracking-[0.14em]" style="color: var(--dim)">' +
311
481
  '<i data-lucide="' + b[2] + '" class="size-3.5"></i><span></span></div>' +
@@ -314,63 +484,60 @@
314
484
  cell.querySelector('p').textContent = fmt(b[1]);
315
485
  strip.appendChild(cell);
316
486
  });
487
+ }
317
488
 
318
- renderGraph(d.byDay || {});
319
- renderChart(d.byDay || {});
320
-
321
- var cacheShare = total ? Math.round((t.cacheRead + t.cacheWrite) / total * 100) : 0;
322
- document.getElementById('usdLine').textContent =
323
- '$' + (d.usd || 0).toFixed(2) + (d.priced ? ' measured (price table).' : ' incl. default pricing.');
324
- document.getElementById('co2Line').textContent =
325
- '~' + (d.co2g || 0).toFixed(1) + 'g CO2 est. from output tokens.';
326
- document.getElementById('cacheLine').textContent =
327
- cacheShare + '% of tokens served from cache.';
489
+ function money(v) { return '$' + v.toFixed(2); }
328
490
 
329
- var models = document.getElementById('models');
330
- models.innerHTML = '';
331
- Object.entries(d.byModel || {}).sort(function (a, b) {
332
- return (b[1].input + b[1].output) - (a[1].input + a[1].output);
333
- }).slice(0, 5).forEach(function (row) {
334
- var li = document.createElement('li');
335
- li.className = 'flex justify-between gap-2';
336
- var name = document.createElement('span'); name.className = 'truncate'; name.textContent = row[0];
337
- var n = document.createElement('span'); n.textContent = fmt(row[1].input + row[1].output);
338
- li.appendChild(name); li.appendChild(n); models.appendChild(li);
491
+ function render(d) {
492
+ DATA = d;
493
+ var t = d.tokens || { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
494
+ var total = t.input + t.output + t.cacheRead + t.cacheWrite;
495
+ document.getElementById('meta').textContent = fmt(total) + ' session tokens / ' + d.total + ' ledger rows';
496
+ var sub = document.getElementById('herosub');
497
+ sub.innerHTML = '';
498
+ ('input ' + fmt(t.input) + ' · output ' + fmt(t.output) + ' · cache read ' + fmt(t.cacheRead) +
499
+ (t.cacheWrite ? ' · cache write ' + fmt(t.cacheWrite) : '') + ' · ' + money(d.usd || 0)).split(' ').forEach(function (w) {
500
+ var s = document.createElement('span'); s.textContent = w + ' ';
501
+ sub.appendChild(s);
339
502
  });
340
- if (!models.children.length) {
341
- models.innerHTML = '<li style="color: var(--dim)">no session tokens yet</li>';
503
+ var totalEl = document.getElementById('total');
504
+ if (hasGsap) {
505
+ var o = { v: 0 };
506
+ gsap.to(o, { v: total, duration: 1.2, ease: 'power3.out', onUpdate: function () { totalEl.textContent = fmt(o.v); } });
507
+ } else {
508
+ totalEl.textContent = fmt(total);
342
509
  }
510
+ countUp(document.getElementById('usd'), d.usd || 0, true);
511
+ document.getElementById('usdNote').textContent = d.priced ? 'per-model price table' : 'incl. default pricing';
512
+ countUp(document.getElementById('saved'), d.savedUsd || 0, true);
513
+ document.getElementById('co2').textContent = '~' + (d.co2g || 0).toFixed(1) + 'g';
514
+ var share = total ? Math.round((t.cacheRead + t.cacheWrite) / total * 100) : 0;
515
+ document.getElementById('cacheShare').textContent = share + '%';
516
+ document.getElementById('msgs').textContent = fmt(d.messages || 0);
343
517
 
344
- var ol = document.getElementById('top');
345
- ol.innerHTML = '';
346
- var max = 1;
347
- (d.byDetail || []).forEach(function (row) { if (row[1] > max) max = row[1]; });
348
- (d.byDetail || []).forEach(function (row, i) {
349
- var li = document.createElement('li');
350
- li.className = 'tilt flex items-center gap-3 px-4 py-3' + (i < d.byDetail.length - 1 ? ' rowline' : '');
351
- var rank = document.createElement('span');
352
- rank.className = 'mono text-xs w-6 shrink-0'; rank.style.color = 'var(--dim)';
353
- rank.textContent = String(i + 1).padStart(2, '0');
354
- var mid = document.createElement('div'); mid.className = 'min-w-0 flex-1';
355
- var name = document.createElement('p'); name.className = 'mono text-sm truncate'; name.textContent = row[0];
356
- var track = document.createElement('div'); track.className = 'bar-track mt-1.5 h-1.5 overflow-hidden';
357
- var fill = document.createElement('div'); fill.className = 'bar-fill grow h-full';
358
- fill.style.width = Math.max(2, Math.round(row[1] / max * 100)) + '%';
359
- track.appendChild(fill); mid.appendChild(name); mid.appendChild(track);
360
- var n = document.createElement('span'); n.className = 'mono font-bold text-lg shrink-0'; n.textContent = String(row[1]);
361
- li.appendChild(rank); li.appendChild(mid); li.appendChild(n);
362
- ol.appendChild(li);
363
- });
364
- document.getElementById('emptyTop').classList.toggle('hidden', (d.total || 0) > 0);
365
- ol.classList.toggle('hidden', (d.total || 0) === 0);
518
+ renderStrip(t);
519
+ renderBars();
520
+ renderModels();
521
+ renderTools();
366
522
 
367
523
  if (window.lucide) lucide.createIcons();
368
- observe();
524
+ if (hasGsap) {
525
+ gsap.from('.hero-in', { y: 26, opacity: 0, duration: 0.8, ease: 'power3.out', stagger: 0.08 });
526
+ // Scrub: hero sub words fade in sequence on scroll.
527
+ if (hasST) {
528
+ gsap.fromTo('#herosub span', { opacity: 0.15 }, {
529
+ opacity: 1, ease: 'none', stagger: 0.05,
530
+ scrollTrigger: { trigger: '#herosub', start: 'top 92%', end: 'top 55%', scrub: true }
531
+ });
532
+ }
533
+ } else {
534
+ observe();
535
+ }
369
536
  }
370
537
 
371
538
  var seen = new WeakSet();
372
539
  function observe() {
373
- var els = document.querySelectorAll('.rise, .grow, .growY');
540
+ var els = document.querySelectorAll('.rise');
374
541
  if (!('IntersectionObserver' in window) || reduce) {
375
542
  els.forEach(function (el) { el.classList.add('in'); });
376
543
  return;
@@ -385,10 +552,19 @@
385
552
  els.forEach(function (el) { if (!seen.has(el)) io.observe(el); });
386
553
  }
387
554
 
555
+ document.querySelectorAll('.seg button').forEach(function (btn) {
556
+ btn.addEventListener('click', function () {
557
+ document.querySelectorAll('.seg button').forEach(function (b) { b.classList.remove('on'); });
558
+ btn.classList.add('on');
559
+ RANGE = btn.getAttribute('data-range');
560
+ if (DATA) renderBars();
561
+ });
562
+ });
563
+
388
564
  function load() {
389
565
  document.getElementById('meta').textContent = 'connecting to ledger';
390
566
  fetch('data.json').then(function (r) { return r.json(); }).then(render).catch(function () {
391
- document.getElementById('meta').textContent = 'No live data. Serve with: tersio dashboard';
567
+ document.getElementById('meta').textContent = 'No live data. Open with: tersio gain --open';
392
568
  document.getElementById('rawlink').style.display = 'none';
393
569
  document.getElementById('emptyTop').classList.remove('hidden');
394
570
  if (window.lucide) lucide.createIcons();