llm_cost_tracker 0.2.0.alpha2 → 0.2.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -1
  3. data/README.md +4 -3
  4. data/app/assets/llm_cost_tracker/application.css +760 -0
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +1 -7
  6. data/app/controllers/llm_cost_tracker/assets_controller.rb +13 -0
  7. data/app/controllers/llm_cost_tracker/calls_controller.rb +29 -12
  8. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +5 -1
  9. data/app/helpers/llm_cost_tracker/application_helper.rb +46 -5
  10. data/app/helpers/llm_cost_tracker/chart_helper.rb +133 -0
  11. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +42 -0
  12. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +34 -0
  13. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +58 -0
  14. data/app/helpers/llm_cost_tracker/pagination_helper.rb +18 -0
  15. data/app/services/llm_cost_tracker/dashboard/filter.rb +0 -3
  16. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +16 -1
  17. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +79 -0
  18. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +19 -46
  19. data/app/services/llm_cost_tracker/dashboard/top_models.rb +17 -8
  20. data/app/services/llm_cost_tracker/pagination.rb +6 -0
  21. data/app/views/layouts/llm_cost_tracker/application.html.erb +35 -333
  22. data/app/views/llm_cost_tracker/calls/index.html.erb +106 -74
  23. data/app/views/llm_cost_tracker/calls/show.html.erb +58 -1
  24. data/app/views/llm_cost_tracker/dashboard/index.html.erb +201 -111
  25. data/app/views/llm_cost_tracker/data_quality/index.html.erb +178 -78
  26. data/app/views/llm_cost_tracker/errors/database.html.erb +3 -3
  27. data/app/views/llm_cost_tracker/errors/invalid_filter.html.erb +3 -3
  28. data/app/views/llm_cost_tracker/errors/not_found.html.erb +3 -3
  29. data/app/views/llm_cost_tracker/models/index.html.erb +66 -58
  30. data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +16 -0
  31. data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +23 -0
  32. data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +18 -0
  33. data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +15 -0
  34. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +3 -2
  35. data/app/views/llm_cost_tracker/tags/index.html.erb +55 -12
  36. data/app/views/llm_cost_tracker/tags/show.html.erb +88 -39
  37. data/config/routes.rb +3 -0
  38. data/lib/llm_cost_tracker/assets.rb +24 -0
  39. data/lib/llm_cost_tracker/engine.rb +2 -0
  40. data/lib/llm_cost_tracker/llm_api_call.rb +1 -1
  41. data/lib/llm_cost_tracker/price_registry.rb +17 -6
  42. data/lib/llm_cost_tracker/pricing.rb +19 -6
  43. data/lib/llm_cost_tracker/retention.rb +34 -0
  44. data/lib/llm_cost_tracker/tag_query.rb +7 -2
  45. data/lib/llm_cost_tracker/tags_column.rb +13 -1
  46. data/lib/llm_cost_tracker/version.rb +1 -1
  47. data/lib/llm_cost_tracker.rb +1 -0
  48. data/lib/tasks/llm_cost_tracker.rake +8 -0
  49. data/llm_cost_tracker.gemspec +1 -2
  50. metadata +17 -5
  51. data/PLAN_0.2.md +0 -488
@@ -0,0 +1,760 @@
1
+ :root {
2
+ --lct-bg: #f6f9fc;
3
+ --lct-panel: #ffffff;
4
+ --lct-surface: #eef2f7;
5
+ --lct-border: #e3e8ee;
6
+ --lct-text: #0a2540;
7
+ --lct-muted: #697386;
8
+ --lct-accent: #635bff;
9
+ --lct-accent-hover: #5048e5;
10
+ --lct-accent-soft: #f0edff;
11
+ --lct-success: #0f766e;
12
+ --lct-success-soft: #ccfbf1;
13
+ --lct-warning: #d97706;
14
+ --lct-warning-soft: #ffedd5;
15
+ --lct-warning-border: #fdba74;
16
+ --lct-danger: #dc2626;
17
+ --lct-danger-soft: #fee2e2;
18
+ --lct-danger-border: #fca5a5;
19
+ --lct-row-hover: #f8fafc;
20
+ --lct-shadow: 0 1px 2px rgba(10, 37, 64, 0.05), 0 1px 1px rgba(10, 37, 64, 0.03);
21
+ --lct-focus-ring: 0 0 0 3px rgba(99, 91, 255, 0.25);
22
+
23
+ --fs-2xs: 10px;
24
+ --fs-xs: 11px;
25
+ --fs-sm: 13px;
26
+ --fs-md: 14px;
27
+ --fs-lg: 18px;
28
+ --fs-xl: 22px;
29
+ --fs-xxl: 28px;
30
+ --fs-display: 30px;
31
+ --fs-hero: 44px;
32
+ --lct-control-height: 40px;
33
+
34
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
35
+ }
36
+
37
+ body:has(> .lct-app) { margin: 0; }
38
+
39
+ .lct-app {
40
+ background: var(--lct-bg);
41
+ color: var(--lct-text);
42
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
43
+ line-height: 1.5;
44
+ -webkit-font-smoothing: antialiased;
45
+ min-height: 100vh;
46
+ }
47
+
48
+ .lct-shell {
49
+ max-width: 1180px;
50
+ margin: 0 auto;
51
+ padding: 24px;
52
+ }
53
+
54
+ .lct-header {
55
+ display: flex;
56
+ align-items: flex-start;
57
+ justify-content: space-between;
58
+ gap: 24px;
59
+ margin-bottom: 20px;
60
+ }
61
+
62
+ .lct-header-copy { display: grid; gap: 6px; }
63
+
64
+ .lct-title {
65
+ margin: 0;
66
+ font-size: var(--fs-display);
67
+ font-weight: 700;
68
+ letter-spacing: -0.01em;
69
+ line-height: 1.2;
70
+ }
71
+
72
+ .lct-nav { display: flex; gap: 4px; flex-wrap: wrap; }
73
+
74
+ .lct-nav a {
75
+ color: var(--lct-muted);
76
+ text-decoration: none;
77
+ font-size: var(--fs-md);
78
+ font-weight: 500;
79
+ padding: 8px 12px;
80
+ border-radius: 6px;
81
+ }
82
+
83
+ .lct-nav a:hover { color: var(--lct-accent); background: rgba(99, 91, 255, 0.06); }
84
+ .lct-nav .lct-active { background: var(--lct-accent-soft); color: var(--lct-accent); font-weight: 700; }
85
+
86
+ .lct-panel {
87
+ background: var(--lct-panel);
88
+ border: 1px solid var(--lct-border);
89
+ border-radius: 8px;
90
+ padding: 20px;
91
+ box-shadow: var(--lct-shadow);
92
+ }
93
+
94
+ .lct-panel-tight { padding: 16px 18px; }
95
+
96
+ .lct-banner {
97
+ align-items: center;
98
+ border-radius: 8px;
99
+ border: 1px solid;
100
+ display: flex;
101
+ gap: 20px;
102
+ justify-content: space-between;
103
+ margin-bottom: 16px;
104
+ padding: 12px 16px;
105
+ }
106
+
107
+ .lct-banner-body { flex: 1; min-width: 0; }
108
+ .lct-banner-title { font-size: var(--fs-md); font-weight: 600; margin: 0; }
109
+ .lct-banner-muted { color: var(--lct-muted); font-weight: 400; }
110
+
111
+ .lct-banner-warning { background: var(--lct-warning-soft); border-color: var(--lct-warning-border); color: #7c2d12; }
112
+ .lct-banner-warning .lct-banner-muted,
113
+ .lct-banner-warning .lct-banner-copy { color: #9a3412; }
114
+
115
+ .lct-banner-danger { background: var(--lct-danger-soft); border-color: var(--lct-danger-border); color: #991b1b; }
116
+ .lct-banner-danger .lct-banner-muted,
117
+ .lct-banner-danger .lct-banner-copy { color: #b91c1c; }
118
+
119
+ .lct-muted { color: var(--lct-muted); }
120
+
121
+ .lct-code {
122
+ background: var(--lct-surface);
123
+ border-radius: 4px;
124
+ padding: 2px 6px;
125
+ font-family: var(--mono);
126
+ font-size: 0.92em;
127
+ }
128
+
129
+ .lct-grid { display: grid; gap: 16px; margin-bottom: 16px; }
130
+
131
+ .lct-hero {
132
+ align-items: stretch;
133
+ display: grid;
134
+ gap: 14px;
135
+ grid-template-columns: minmax(0, 0.65fr) minmax(320px, 1fr);
136
+ margin-bottom: 16px;
137
+ }
138
+
139
+ .lct-hero-primary { align-content: space-between; display: grid; gap: 14px; }
140
+
141
+ .lct-hero-value {
142
+ font-size: var(--fs-hero);
143
+ font-variant-numeric: tabular-nums;
144
+ font-weight: 750;
145
+ letter-spacing: -0.02em;
146
+ line-height: 1.05;
147
+ margin: 0;
148
+ }
149
+
150
+ .lct-hero-side { display: grid; gap: 14px; }
151
+
152
+ .lct-stat-grid {
153
+ display: grid;
154
+ gap: 14px;
155
+ grid-auto-rows: 1fr;
156
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
157
+ }
158
+
159
+ .lct-hero-side .lct-stat-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
160
+ .lct-stat-grid-spaced { margin-bottom: 16px; }
161
+ .lct-two-col { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
162
+
163
+ .lct-stat {
164
+ align-items: flex-start;
165
+ background: var(--lct-panel);
166
+ border: 1px solid var(--lct-border);
167
+ border-radius: 8px;
168
+ display: flex;
169
+ flex-direction: column;
170
+ padding: 16px;
171
+ }
172
+ .lct-stat .lct-stat-value { order: 1; }
173
+ .lct-stat .lct-stat-label { order: 2; margin: 6px 0 0; }
174
+ .lct-stat .lct-delta-badge { order: 3; }
175
+
176
+ .lct-stat-value {
177
+ font-size: var(--fs-xxl);
178
+ font-variant-numeric: tabular-nums;
179
+ font-weight: 700;
180
+ letter-spacing: -0.01em;
181
+ margin: 0;
182
+ }
183
+
184
+ .lct-stat-sub { color: var(--lct-muted); font-size: var(--fs-xs); margin: 4px 0 0; }
185
+
186
+ /* Shared "small uppercase-ish label" recipe */
187
+ .lct-stat-label,
188
+ .lct-field label,
189
+ .lct-dl dt,
190
+ .lct-call-summary-label,
191
+ .lct-call-breakdown-title,
192
+ .lct-chip-label {
193
+ color: var(--lct-muted);
194
+ font-size: var(--fs-xs);
195
+ font-weight: 600;
196
+ letter-spacing: 0;
197
+ text-transform: none;
198
+ }
199
+ .lct-stat-label { margin: 0 0 6px; }
200
+ .lct-call-breakdown-title { color: var(--lct-text); font-size: var(--fs-md); margin: 0 0 10px; }
201
+ .lct-chip-label { color: var(--lct-accent); font-weight: 700; }
202
+ .lct-field label { color: var(--lct-text); font-size: var(--fs-md); font-weight: 500; }
203
+
204
+ /* Shared "muted body copy" recipe */
205
+ .lct-section-copy,
206
+ .lct-stat-copy,
207
+ .lct-banner-copy,
208
+ .lct-call-breakdown-empty {
209
+ color: var(--lct-muted);
210
+ margin: 0;
211
+ }
212
+ .lct-section-copy { font-size: var(--fs-md); margin-top: 4px; }
213
+ .lct-stat-copy { font-size: var(--fs-sm); margin-top: 6px; }
214
+ .lct-banner-copy { font-size: var(--fs-sm); margin-top: 4px; }
215
+ .lct-call-breakdown-empty { font-size: var(--fs-sm); }
216
+
217
+ .lct-section-title { margin: 0 0 8px; font-size: var(--fs-lg); font-weight: 600; letter-spacing: -0.01em; }
218
+
219
+ .lct-section-head {
220
+ align-items: flex-start;
221
+ display: flex;
222
+ justify-content: space-between;
223
+ gap: 16px;
224
+ margin-bottom: 14px;
225
+ }
226
+
227
+ .lct-table { border-collapse: collapse; width: 100%; font-size: var(--fs-sm); }
228
+
229
+ .lct-num { font-variant-numeric: tabular-nums; text-align: right !important; white-space: nowrap; }
230
+ .lct-num-muted { color: var(--lct-muted); }
231
+
232
+ .lct-table-compact th,
233
+ .lct-table-compact td { padding: 10px 12px; }
234
+
235
+ .lct-delta-badge {
236
+ align-items: center;
237
+ border-radius: 999px;
238
+ display: inline-flex;
239
+ font-size: var(--fs-xs);
240
+ font-variant-numeric: tabular-nums;
241
+ font-weight: 600;
242
+ gap: 6px;
243
+ letter-spacing: 0.01em;
244
+ line-height: 1;
245
+ margin: 10px 0 0;
246
+ padding: 4px 10px 4px 8px;
247
+ }
248
+ .lct-delta-dot {
249
+ border-radius: 999px;
250
+ display: inline-block;
251
+ height: 6px;
252
+ width: 6px;
253
+ background: currentColor;
254
+ flex-shrink: 0;
255
+ }
256
+ .lct-delta-neutral { background: var(--lct-surface); color: var(--lct-muted); box-shadow: inset 0 0 0 1px rgba(105, 115, 134, 0.2); }
257
+ .lct-delta-up { background: var(--lct-danger-soft); color: #991b1b; box-shadow: inset 0 0 0 1px rgba(220, 38, 38, 0.2); }
258
+ .lct-delta-down { background: var(--lct-success-soft); color: #115e59; box-shadow: inset 0 0 0 1px rgba(15, 118, 110, 0.2); }
259
+
260
+ .lct-table-wrap { overflow-x: auto; }
261
+
262
+ .lct-table th,
263
+ .lct-table td {
264
+ border-bottom: 1px solid var(--lct-border);
265
+ padding: 12px;
266
+ text-align: left;
267
+ vertical-align: middle;
268
+ }
269
+
270
+ .lct-table tbody tr:last-child td { border-bottom: 0; }
271
+
272
+ .lct-table th {
273
+ background: #fbfcfe;
274
+ color: var(--lct-muted);
275
+ font-weight: 600;
276
+ font-size: var(--fs-xs);
277
+ letter-spacing: 0;
278
+ text-transform: none;
279
+ }
280
+
281
+ .lct-table tbody tr:hover { background: var(--lct-row-hover); }
282
+
283
+ .lct-table td:last-child,
284
+ .lct-table th:last-child,
285
+ .lct-calls-table td:last-child,
286
+ .lct-calls-table th:last-child { text-align: right; }
287
+
288
+ /* Track + fill primitives — shared by bar / budget / stack */
289
+ .lct-bar-track,
290
+ .lct-budget-track,
291
+ .lct-stack-track {
292
+ background: var(--lct-surface);
293
+ border-radius: 999px;
294
+ overflow: hidden;
295
+ }
296
+ .lct-bar-track { height: 8px; min-width: 96px; }
297
+ .lct-budget-track { height: 10px; border: 1px solid var(--lct-border); position: relative; }
298
+ .lct-stack-track { height: 12px; display: flex; }
299
+
300
+ .lct-bar-fill,
301
+ .lct-budget-fill,
302
+ .lct-stack-fill { height: 100%; display: block; }
303
+ .lct-bar-fill { background: var(--lct-accent); }
304
+ .lct-budget-fill { background: linear-gradient(90deg, #635bff, #7a73ff); transition: width 0.2s ease; }
305
+ .lct-budget-fill--warn { background: linear-gradient(90deg, #f59e0b, #d97706); }
306
+ .lct-budget-fill--over { background: linear-gradient(90deg, #ef4444, #b91c1c); }
307
+ .lct-stack-fill-input { background: var(--lct-accent); }
308
+ .lct-stack-fill-output { background: #0ea5e9; }
309
+
310
+ .lct-budget { display: grid; gap: 10px; }
311
+ .lct-budget-head { align-items: baseline; display: flex; gap: 10px; justify-content: space-between; }
312
+ .lct-budget-spent {
313
+ font-size: var(--fs-xl);
314
+ font-variant-numeric: tabular-nums;
315
+ font-weight: 700;
316
+ letter-spacing: -0.01em;
317
+ }
318
+ .lct-budget-of { color: var(--lct-muted); font-size: var(--fs-sm); font-weight: 500; }
319
+ .lct-budget-percent { font-size: var(--fs-md); font-variant-numeric: tabular-nums; font-weight: 700; }
320
+ .lct-budget-marker {
321
+ border-left: 2px dashed rgba(10, 37, 64, 0.45);
322
+ bottom: 0;
323
+ position: absolute;
324
+ top: 0;
325
+ }
326
+ .lct-budget-projection {
327
+ align-items: baseline;
328
+ color: var(--lct-muted);
329
+ display: flex;
330
+ flex-wrap: wrap;
331
+ font-size: var(--fs-sm);
332
+ gap: 8px 12px;
333
+ justify-content: space-between;
334
+ margin: 0;
335
+ }
336
+ .lct-budget-projection strong { color: var(--lct-text); }
337
+ .lct-budget-projection-status { font-weight: 600; }
338
+ .lct-budget-projection-status--over { color: #9a3412; }
339
+ .lct-budget-projection-status--under { color: var(--lct-muted); }
340
+ .lct-budget-meta { color: var(--lct-muted); font-size: var(--fs-xs); }
341
+
342
+ .lct-chart { display: block; height: 180px; width: 100%; }
343
+ .lct-chart-area { fill: rgba(99, 91, 255, 0.12); }
344
+ .lct-chart-line-secondary {
345
+ fill: none;
346
+ opacity: 0.75;
347
+ stroke: rgba(10, 37, 64, 0.42);
348
+ stroke-dasharray: 5 4;
349
+ stroke-linejoin: round;
350
+ stroke-linecap: round;
351
+ stroke-width: 1.5;
352
+ }
353
+ .lct-chart-line { fill: none; stroke: var(--lct-accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
354
+ .lct-chart-dot { fill: var(--lct-accent); }
355
+ .lct-chart-grid { stroke: var(--lct-border); stroke-dasharray: 2 3; }
356
+ .lct-chart-axis { fill: var(--lct-muted); font-family: var(--mono); font-size: var(--fs-2xs); }
357
+ .lct-chart-legend {
358
+ color: var(--lct-muted);
359
+ display: flex;
360
+ font-size: var(--fs-xs);
361
+ font-variant-numeric: tabular-nums;
362
+ justify-content: space-between;
363
+ margin-top: 10px;
364
+ }
365
+ .lct-chart-legend-compare { align-items: center; display: inline-flex; gap: 12px; }
366
+ .lct-chart-key { align-items: center; display: inline-flex; gap: 6px; }
367
+ .lct-chart-key-line { background: var(--lct-accent); border-radius: 999px; display: inline-block; height: 2px; width: 16px; }
368
+ .lct-chart-key-line-secondary { background: rgba(10, 37, 64, 0.42); position: relative; }
369
+ .lct-chart-key-line-secondary::after {
370
+ background: linear-gradient(90deg, transparent 40%, var(--lct-bg) 40%, var(--lct-bg) 60%, transparent 60%);
371
+ content: "";
372
+ inset: -1px 0;
373
+ position: absolute;
374
+ }
375
+ .lct-chart-empty {
376
+ align-items: center;
377
+ color: var(--lct-muted);
378
+ display: flex;
379
+ font-size: var(--fs-sm);
380
+ height: 180px;
381
+ justify-content: center;
382
+ }
383
+ .lct-tag-chips { display: flex; flex-wrap: wrap; gap: 4px; max-width: 380px; }
384
+ .lct-tag-chip {
385
+ align-items: center;
386
+ background: var(--lct-surface);
387
+ border: 1px solid var(--lct-border);
388
+ border-radius: 4px;
389
+ color: var(--lct-text);
390
+ display: inline-flex;
391
+ font-family: var(--mono);
392
+ font-size: var(--fs-xs);
393
+ gap: 0;
394
+ line-height: 1.4;
395
+ padding: 2px 6px;
396
+ text-decoration: none;
397
+ white-space: nowrap;
398
+ }
399
+ .lct-tag-chip-more { background: transparent; border: 1px dashed var(--lct-border); color: var(--lct-muted); }
400
+ .lct-tag-empty { color: var(--lct-muted); font-size: var(--fs-sm); font-style: italic; }
401
+
402
+ .lct-empty { padding: 28px; text-align: center; }
403
+ .lct-state-title { font-size: var(--fs-xl); font-weight: 600; margin: 0 0 8px; }
404
+ .lct-state-copy { color: var(--lct-muted); margin: 0 auto; max-width: 640px; }
405
+ .lct-state-actions { display: flex; gap: 8px; justify-content: center; margin-top: 16px; }
406
+
407
+ .lct-toolbar {
408
+ background: rgba(255, 255, 255, 0.72);
409
+ backdrop-filter: blur(10px);
410
+ border-color: #e7ecf3;
411
+ box-shadow: none;
412
+ padding: 16px;
413
+ margin-bottom: 16px;
414
+ position: sticky;
415
+ top: 12px;
416
+ z-index: 10;
417
+ }
418
+
419
+ .lct-toolbar-head {
420
+ align-items: flex-start;
421
+ display: flex;
422
+ gap: 16px;
423
+ justify-content: space-between;
424
+ margin-bottom: 12px;
425
+ }
426
+
427
+ .lct-toolbar-actions { align-items: center; display: flex; gap: 8px; flex-wrap: wrap; }
428
+
429
+ .lct-toolbar-actions .lct-button,
430
+ .lct-banner .lct-button { font-size: var(--fs-sm); height: 32px; padding: 0 12px; }
431
+
432
+ .lct-filters { display: grid; gap: 8px; }
433
+
434
+ .lct-filter-row { align-items: end; display: grid; gap: 12px; grid-template-columns: 1fr; }
435
+ .lct-filter-row-basic { grid-template-columns: 144px 144px 180px minmax(260px, 1fr) auto; }
436
+ .lct-filter-row-with-sort { grid-template-columns: 144px 144px 180px minmax(240px, 1fr) 160px auto; }
437
+
438
+ .lct-filter-actions {
439
+ align-items: end;
440
+ display: flex;
441
+ flex-wrap: wrap;
442
+ gap: 8px;
443
+ justify-content: flex-start;
444
+ }
445
+
446
+ .lct-field { display: inline-block; }
447
+
448
+ .lct-field input,
449
+ .lct-field select {
450
+ border: 0;
451
+ border-radius: 8px;
452
+ color: var(--lct-text);
453
+ font-family: inherit;
454
+ font-size: var(--fs-md);
455
+ background: var(--lct-panel);
456
+ box-shadow: inset 0 0 0 1px var(--lct-border);
457
+ box-sizing: border-box;
458
+ height: var(--lct-control-height);
459
+ line-height: 1.2;
460
+ padding: 0 12px;
461
+ width: 100%;
462
+ }
463
+
464
+ .lct-field input::placeholder { color: #8792a2; }
465
+
466
+ .lct-field input:focus,
467
+ .lct-field select:focus {
468
+ box-shadow: inset 0 0 0 2px var(--lct-accent);
469
+ outline: none;
470
+ }
471
+
472
+ .lct-button {
473
+ align-items: center;
474
+ background: var(--lct-accent);
475
+ border: 1px solid var(--lct-accent);
476
+ border-radius: 8px;
477
+ box-shadow: 0 1px 2px rgba(10, 37, 64, 0.08);
478
+ color: #ffffff;
479
+ cursor: pointer;
480
+ display: inline-flex;
481
+ font-family: inherit;
482
+ font-size: var(--fs-md);
483
+ font-weight: 600;
484
+ justify-content: center;
485
+ line-height: 1.2;
486
+ height: var(--lct-control-height);
487
+ padding: 0 14px;
488
+ text-decoration: none;
489
+ transition: background-color 0.12s ease, border-color 0.12s ease;
490
+ white-space: nowrap;
491
+ }
492
+
493
+ .lct-button:hover { background: var(--lct-accent-hover); border-color: var(--lct-accent-hover); }
494
+
495
+ .lct-button:focus-visible,
496
+ .lct-chip-remove:focus-visible,
497
+ .lct-clear-link:focus-visible,
498
+ .lct-nav a:focus-visible {
499
+ border-radius: 6px;
500
+ box-shadow: var(--lct-focus-ring);
501
+ outline: none;
502
+ }
503
+
504
+ .lct-button-secondary {
505
+ background: var(--lct-panel);
506
+ border-color: var(--lct-border);
507
+ color: var(--lct-text);
508
+ }
509
+ .lct-button-secondary:hover {
510
+ background: var(--lct-row-hover);
511
+ border-color: var(--lct-border);
512
+ }
513
+
514
+ .lct-button-compact {
515
+ font-size: var(--fs-sm);
516
+ height: 32px;
517
+ min-height: 32px;
518
+ padding: 0 10px;
519
+ }
520
+
521
+ .lct-toolbar-note { color: var(--lct-muted); font-size: var(--fs-xs); margin: 10px 0 0; }
522
+
523
+ .lct-chip-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
524
+
525
+ .lct-chip {
526
+ align-items: center;
527
+ background: var(--lct-accent-soft);
528
+ border: 1px solid rgba(99, 91, 255, 0.12);
529
+ border-radius: 999px;
530
+ color: var(--lct-text);
531
+ display: inline-flex;
532
+ font-size: var(--fs-sm);
533
+ gap: 8px;
534
+ min-height: 28px;
535
+ padding: 0 10px;
536
+ }
537
+
538
+ .lct-chip-remove {
539
+ color: rgba(10, 37, 64, 0.58);
540
+ font-size: var(--fs-lg);
541
+ line-height: 1;
542
+ text-decoration: none;
543
+ }
544
+
545
+ .lct-chip-remove:hover,
546
+ .lct-clear-link:hover { color: var(--lct-accent); }
547
+
548
+ .lct-clear-link {
549
+ color: var(--lct-accent);
550
+ font-size: var(--fs-sm);
551
+ font-weight: 600;
552
+ margin-left: 4px;
553
+ text-decoration: none;
554
+ }
555
+
556
+ .lct-summary-row {
557
+ align-items: center;
558
+ color: var(--lct-muted);
559
+ display: flex;
560
+ flex-wrap: wrap;
561
+ gap: 8px;
562
+ font-size: var(--fs-sm);
563
+ margin-top: 12px;
564
+ }
565
+
566
+ .lct-pagination {
567
+ align-items: center;
568
+ display: flex;
569
+ justify-content: space-between;
570
+ gap: 12px 20px;
571
+ margin-top: 18px;
572
+ flex-wrap: wrap;
573
+ padding-top: 16px;
574
+ border-top: 1px solid var(--lct-border);
575
+ }
576
+
577
+ .lct-pagination-info {
578
+ display: flex;
579
+ align-items: center;
580
+ gap: 12px 18px;
581
+ color: var(--lct-muted);
582
+ font-size: var(--fs-sm);
583
+ font-variant-numeric: tabular-nums;
584
+ flex-wrap: wrap;
585
+ }
586
+
587
+ .lct-pagination-info strong { color: var(--lct-text); font-weight: 600; }
588
+
589
+ .lct-pagination-per { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
590
+ .lct-pagination-per-label { color: var(--lct-muted); font-size: var(--fs-sm); font-weight: 500; }
591
+
592
+ .lct-pagination-per-option {
593
+ color: var(--lct-muted);
594
+ text-decoration: none;
595
+ padding: 2px 8px;
596
+ border-radius: 999px;
597
+ font-variant-numeric: tabular-nums;
598
+ }
599
+
600
+ .lct-pagination-per-option:hover { background: transparent; color: var(--lct-text); }
601
+ .lct-pagination-per-option.is-active { color: var(--lct-text); font-weight: 600; background: var(--lct-surface); }
602
+
603
+ .lct-pagination-nav {
604
+ display: inline-flex;
605
+ align-items: center;
606
+ gap: 0;
607
+ margin-left: auto;
608
+ border: 1px solid var(--lct-border);
609
+ border-radius: 8px;
610
+ background: var(--lct-panel);
611
+ overflow: hidden;
612
+ }
613
+
614
+ .lct-page-link {
615
+ min-width: 40px;
616
+ height: 40px;
617
+ padding: 0;
618
+ display: inline-flex;
619
+ align-items: center;
620
+ justify-content: center;
621
+ border-radius: 0;
622
+ border: 0;
623
+ border-right: 1px solid var(--lct-border);
624
+ background: transparent;
625
+ color: var(--lct-text);
626
+ font-size: var(--fs-sm);
627
+ font-variant-numeric: tabular-nums;
628
+ font-weight: 500;
629
+ text-decoration: none;
630
+ transition: background-color 0.1s ease, border-color 0.1s ease, color 0.1s ease;
631
+ }
632
+
633
+ .lct-page-link:hover { background: var(--lct-row-hover); }
634
+ .lct-page-link.is-current { background: var(--lct-accent); color: #fff; font-weight: 600; cursor: default; }
635
+ .lct-page-link.is-disabled { color: var(--lct-muted); background: transparent; cursor: not-allowed; opacity: 0.55; }
636
+
637
+ .lct-page-arrow { font-size: var(--fs-lg); line-height: 1; min-width: 40px; }
638
+
639
+ .lct-page-gap {
640
+ align-items: center;
641
+ border-right: 1px solid var(--lct-border);
642
+ display: inline-flex;
643
+ height: 40px;
644
+ justify-content: center;
645
+ min-width: 40px;
646
+ text-align: center;
647
+ color: var(--lct-muted);
648
+ font-variant-numeric: tabular-nums;
649
+ user-select: none;
650
+ }
651
+
652
+ .lct-pagination-nav > *:last-child { border-right: 0; }
653
+
654
+ .lct-tags { display: inline-block; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
655
+ .lct-nowrap { white-space: nowrap; }
656
+
657
+ .lct-detail-grid { display: grid; gap: 16px; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
658
+
659
+ .lct-dl {
660
+ display: grid;
661
+ gap: 12px;
662
+ grid-template-columns: minmax(120px, 0.4fr) minmax(0, 1fr);
663
+ margin: 0;
664
+ }
665
+
666
+ .lct-dl dd { margin: 0; }
667
+
668
+ .lct-pre {
669
+ background: #0f172a;
670
+ border-radius: 8px;
671
+ color: #e5e7eb;
672
+ margin: 0;
673
+ overflow: auto;
674
+ padding: 16px;
675
+ }
676
+
677
+ .lct-call-hero {
678
+ align-items: start;
679
+ display: flex;
680
+ gap: 20px;
681
+ justify-content: space-between;
682
+ margin-bottom: 20px;
683
+ }
684
+
685
+ .lct-call-title { margin-bottom: 8px; }
686
+
687
+ .lct-call-subtitle {
688
+ align-items: center;
689
+ color: var(--lct-muted);
690
+ display: flex;
691
+ flex-wrap: wrap;
692
+ gap: 8px;
693
+ margin: 0;
694
+ }
695
+
696
+ .lct-call-summary {
697
+ display: grid;
698
+ gap: 10px;
699
+ grid-template-columns: repeat(2, minmax(0, 1fr));
700
+ min-width: min(420px, 100%);
701
+ }
702
+
703
+ .lct-call-summary-item {
704
+ background: var(--lct-row-hover);
705
+ border: 1px solid var(--lct-border);
706
+ border-radius: 8px;
707
+ display: grid;
708
+ gap: 4px;
709
+ padding: 10px 12px;
710
+ }
711
+
712
+ .lct-call-breakdown-grid {
713
+ display: grid;
714
+ gap: 16px;
715
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
716
+ margin-bottom: 16px;
717
+ }
718
+
719
+ .lct-stack-legend { display: grid; gap: 8px; margin-top: 12px; }
720
+
721
+ .lct-stack-legend-item {
722
+ align-items: center;
723
+ display: flex;
724
+ font-size: var(--fs-sm);
725
+ gap: 12px;
726
+ justify-content: space-between;
727
+ }
728
+
729
+ .lct-stack-key { align-items: center; display: inline-flex; gap: 8px; }
730
+ .lct-stack-swatch { border-radius: 999px; display: inline-block; height: 8px; width: 8px; }
731
+ .lct-stack-meta { color: var(--lct-muted); font-variant-numeric: tabular-nums; }
732
+
733
+ @media (max-width: 800px) {
734
+ .lct-header { flex-direction: column; }
735
+
736
+ .lct-hero,
737
+ .lct-stat-grid,
738
+ .lct-two-col { grid-template-columns: 1fr; }
739
+
740
+ .lct-filters { gap: 12px; }
741
+
742
+ .lct-filter-row,
743
+ .lct-filter-row-basic,
744
+ .lct-filter-row-with-sort { align-items: stretch; grid-template-columns: 1fr; }
745
+
746
+ .lct-filter-actions { justify-content: flex-start; width: 100%; }
747
+
748
+ .lct-toolbar { padding: 16px; position: static; }
749
+
750
+ .lct-pagination { align-items: flex-start; flex-direction: column; }
751
+
752
+ .lct-pagination-nav { margin-left: 0; width: 100%; overflow-x: auto; }
753
+
754
+ .lct-detail-grid { grid-template-columns: 1fr; }
755
+
756
+ .lct-call-hero { flex-direction: column; }
757
+
758
+ .lct-call-summary,
759
+ .lct-call-breakdown-grid { grid-template-columns: 1fr; min-width: 0; }
760
+ }