@vimoxshah/tokenflow 1.1.1 → 1.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 (87) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/Dockerfile.team +20 -0
  3. package/README.md +30 -11
  4. package/bin/tokenflow.js +147 -12
  5. package/design/tokens.yaml +330 -0
  6. package/docs/architecture.md +5 -4
  7. package/docs/cli.md +204 -0
  8. package/docs/configuration.md +117 -2
  9. package/docs/design-system.md +187 -0
  10. package/docs/exports-and-budgets.md +85 -0
  11. package/docs/guard-codex.md +132 -0
  12. package/docs/ledger.md +144 -0
  13. package/docs/live-mode.md +40 -0
  14. package/docs/media/overview-aurora-dark.png +0 -0
  15. package/docs/media/receipts-aurora-dark.png +0 -0
  16. package/docs/providers-otel.md +179 -0
  17. package/docs/providers.md +54 -1
  18. package/docs/receipt-schema.md +74 -0
  19. package/docs/roadmap.md +182 -0
  20. package/docs/team-server.md +170 -0
  21. package/docs/ui-views.md +322 -0
  22. package/package.json +7 -2
  23. package/schemas/receipt.v0.json +160 -0
  24. package/scripts/build-dmg.sh +11 -2
  25. package/scripts/build-menubar-app.sh +58 -7
  26. package/scripts/design-build.js +475 -0
  27. package/src/analytics/anatomy.js +467 -0
  28. package/src/analytics/branch-compare.js +159 -0
  29. package/src/analytics/cache-health.js +141 -0
  30. package/src/analytics/live-view.js +266 -0
  31. package/src/analytics/receipt-schema.js +214 -0
  32. package/src/analytics/receipt.js +709 -0
  33. package/src/analytics/rhythm.js +184 -0
  34. package/src/analytics/whatif.js +263 -0
  35. package/src/commands/budget-scopes.js +133 -0
  36. package/src/commands/doctor-checks.js +400 -0
  37. package/src/commands/guard.js +531 -0
  38. package/src/commands/hooks.js +238 -0
  39. package/src/commands/pricing-diff.js +316 -0
  40. package/src/commands/receipt.js +226 -0
  41. package/src/commands/team-serve.js +407 -0
  42. package/src/commands/week.js +86 -0
  43. package/src/core/annotations.js +97 -0
  44. package/src/core/budget.js +33 -0
  45. package/src/core/bundle.js +45 -2
  46. package/src/core/ingest.js +33 -0
  47. package/src/core/live-status.js +227 -2
  48. package/src/core/policy.js +103 -0
  49. package/src/core/receipt-note.js +123 -0
  50. package/src/core/repo.js +64 -0
  51. package/src/core/sync.js +163 -26
  52. package/src/core/team.js +0 -0
  53. package/src/export/html-snapshot.js +28 -1
  54. package/src/export/menubar.js +21 -0
  55. package/src/export/receipt-card.js +210 -0
  56. package/src/export/week-card.js +185 -0
  57. package/src/providers/mock/index.js +383 -52
  58. package/src/providers/openai/index.js +31 -1
  59. package/src/providers/otel/index.js +656 -0
  60. package/src/server/routes/annotations.js +42 -0
  61. package/src/server/routes/cache-health.js +95 -0
  62. package/src/server/routes/index.js +54 -0
  63. package/src/server/routes/session.js +157 -0
  64. package/src/server/server.js +47 -1
  65. package/src/ui/app.js +541 -308
  66. package/src/ui/charts.js +95 -0
  67. package/src/ui/first-run.js +144 -0
  68. package/src/ui/index.html +4 -1
  69. package/src/ui/palette.js +335 -0
  70. package/src/ui/styles/anatomy.css +117 -0
  71. package/src/ui/styles/annotations.css +40 -0
  72. package/src/ui/styles/branches.css +99 -0
  73. package/src/ui/styles/cache.css +6 -0
  74. package/src/ui/styles/first-run.css +31 -0
  75. package/src/ui/styles/live.css +100 -0
  76. package/src/ui/styles/palette.css +85 -0
  77. package/src/ui/styles/rhythm.css +8 -0
  78. package/src/ui/styles/whatif.css +55 -0
  79. package/src/ui/styles.css +303 -196
  80. package/src/ui/views/anatomy.js +567 -0
  81. package/src/ui/views/annotations.js +121 -0
  82. package/src/ui/views/branches.js +304 -0
  83. package/src/ui/views/cache.js +232 -0
  84. package/src/ui/views/index.js +85 -0
  85. package/src/ui/views/live.js +683 -0
  86. package/src/ui/views/rhythm.js +206 -0
  87. package/src/ui/views/whatif.js +196 -0
package/src/ui/styles.css CHANGED
@@ -1,58 +1,57 @@
1
- /* =========================================================================
2
- Tokenflow — design system
3
-
4
- Two independent axes on <html>:
5
- data-mode = dark | light ink, surfaces polarity, series steps
6
- data-skin = aurora | terminal | editorial
7
-
8
- The categorical series slots belong to the MODE, not the skin: they were run
9
- through the dataviz validator (lightness band, chroma floor, adjacent CVD
10
- separation, normal-vision floor, contrast) against each skin's actual chart
11
- surface, and all three dark surfaces and all three light surfaces pass with
12
- the same eight steps. A skin therefore restyles the room, never the data —
13
- which is the point: switching theme must not change what a colour means.
14
-
15
- Three light-mode slots (aqua, yellow, magenta) sit under 3:1 on a light
16
- surface. That is the documented relief case, and the relief ships: every
17
- chart in this app has a table twin and direct labels.
18
- ========================================================================= */
19
-
1
+ /* @generated design-tokens:start — TokenFlow design tokens for the dashboard and the offline snapshot. Source: design/tokens.yaml · rebuild: npm run design. Hand edits here are overwritten and fail test/design.test.js. */
20
2
  :root {
21
- /* --- mode-independent ------------------------------------------------- */
22
3
  --sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
23
4
  --mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
24
5
  --serif: 'Iowan Old Style', 'Palatino Linotype', Palatino, 'Book Antiqua', Georgia, serif;
25
- --display: var(--sans); /* headings */
26
- --figure: var(--sans); /* big numerals */
27
- --radius: 12px;
28
- --radius-sm: 8px;
29
- /* Density: a skin is allowed to change the amount of air, not the data. */
30
- --pad-card: 14px 16px;
31
- --gap-grid: 14px;
32
- --fig-scale: 1;
33
- --sec-space: 26px;
34
- --glow: none;
35
- --hero-glow: none;
36
- --tracking-h: -0.015em;
37
-
38
- /* status (reserved — never used for a series, never themed) ------------- */
39
- --good: #0ca30c;
40
- --warning: #fab219;
41
- --serious: #ec835a;
6
+ --display: var(--sans);
7
+ --figure: var(--sans);
8
+
9
+ /* type scale — size-specific tracking and leading */
10
+ --fs-micro: 10.5px; --tr-micro: .02em; --lh-micro: 1.3;
11
+ --fs-label: 11.5px; --tr-label: .012em; --lh-label: 1.35;
12
+ --fs-caption: 12.5px; --tr-caption: .004em; --lh-caption: 1.4;
13
+ --fs-body: 13.5px; --tr-body: 0; --lh-body: 1.45;
14
+ --fs-title: 15px; --tr-title: -.006em; --lh-title: 1.3;
15
+ --fs-figure: 25px; --tr-figure: -.015em; --lh-figure: 1.12;
16
+ --fs-hero: 42px; --tr-hero: -.025em; --lh-hero: 1.05;
17
+ --fs-display: 60px; --tr-display: -.03em; --lh-display: 1;
18
+
19
+ /* space */
20
+ --sp-2: 2px; --sp-4: 4px; --sp-6: 6px; --sp-8: 8px; --sp-10: 10px; --sp-12: 12px; --sp-14: 14px; --sp-16: 16px; --sp-20: 20px; --sp-24: 24px; --sp-32: 32px; --sp-40: 40px; --sp-56: 56px; --sp-72: 72px;
21
+
22
+ /* radius */
23
+ --radius-xs: 5px; --radius-sm: 8px; --radius: 12px; --radius-lg: 16px; --radius-pill: 999px;
24
+
25
+ /* motion · ui tier — nothing over 300ms, no animation on keyboard actions */
26
+ --dur-press: 120ms; --dur-fast: 160ms; --dur-base: 220ms; --dur-slow: 300ms;
27
+ --ease-out: cubic-bezier(.23, 1, .32, 1);
28
+ --ease-in-out: cubic-bezier(.77, 0, .175, 1);
29
+ --ease-drawer: cubic-bezier(.32, .72, 0, 1);
30
+ --stagger: 40ms; --stagger-max: 6;
31
+ /* motion · narrative tier — reveals on reading surfaces */
32
+ --nar-fast: 180ms; --nar-base: 320ms; --nar-slow: 700ms;
33
+ --nar-ease: cubic-bezier(.2, .7, .3, 1);
34
+ --nar-ease-out: cubic-bezier(.16, 1, .3, 1);
35
+ --nar-stagger: 85ms; --nar-stagger-max: 7;
36
+
37
+ /* status — reserved for state, never a series, never a mark, never themed */
38
+ --good: #0ca30c;
39
+ --warning: #fab219;
40
+ --serious: #ec835a;
42
41
  --critical: #d03b3b;
43
42
  }
44
43
 
45
- /* ======================================================== mode: series ==== */
44
+ /* series / sequential / diverging steps belong to the MODE, never the skin */
46
45
  :root, :root[data-mode='dark'] {
47
46
  color-scheme: dark;
48
- --series-1: #3987e5; /* blue */
49
- --series-2: #d95926; /* orange */
50
- --series-3: #199e70; /* aqua */
51
- --series-4: #c98500; /* yellow */
52
- --series-5: #d55181; /* magenta */
53
- --series-6: #008300; /* green */
54
- --series-7: #9085e9; /* violet */
55
- --series-8: #e66767; /* red */
47
+ --series-1: #3987e5;
48
+ --series-2: #d95926;
49
+ --series-3: #199e70;
50
+ --series-4: #c98500;
51
+ --series-5: #d55181;
52
+ --series-6: #008300;
53
+ --series-7: #9085e9;
54
+ --series-8: #e66767;
56
55
  --seq-1: #184f95;
57
56
  --seq-2: #1c5cab;
58
57
  --seq-3: #256abf;
@@ -60,8 +59,12 @@
60
59
  --seq-5: #3987e5;
61
60
  --seq-6: #5598e7;
62
61
  --seq-7: #86b6ef;
62
+ --div-1: #d95926;
63
+ --div-2: #e39a78;
64
+ --div-3: #6b7385;
65
+ --div-4: #7fb0f0;
66
+ --div-5: #3987e5;
63
67
  }
64
-
65
68
  :root[data-mode='light'] {
66
69
  color-scheme: light;
67
70
  --series-1: #2a78d6;
@@ -79,155 +82,150 @@
79
82
  --seq-5: #2a78d6;
80
83
  --seq-6: #1c5cab;
81
84
  --seq-7: #104281;
85
+ --div-1: #eb6834;
86
+ --div-2: #f2b28f;
87
+ --div-3: #9aa2b3;
88
+ --div-4: #8dbaf0;
89
+ --div-5: #2a78d6;
82
90
  }
83
91
 
84
- /* ============================================== skin: aurora (default) ==== */
85
- /* Deep indigo-slate, layered cards, one luminous accent, a restrained glow
86
- behind the header. The default because it reads as a product rather than a
87
- tool, without tinting the data. */
92
+ /* skin: aurora Indigo-slate, layered, luminous */
88
93
  :root, :root[data-skin='aurora'] {
89
- --plane: #0a0d14;
90
- --surface-1: #111624;
91
- --surface-2: #171d2e;
92
- --surface-3: #1f2637;
93
- --text-primary: #f3f6fc;
94
+ --plane: #0a0d14;
95
+ --surface-1: #111624;
96
+ --surface-2: #171d2e;
97
+ --surface-3: #1f2637;
98
+ --text-primary: #f3f6fc;
94
99
  --text-secondary: #a7b2c9;
95
- --text-muted: #78829a;
96
- --grid: #1e2436;
97
- --axis: #2b3348;
98
- --border: rgba(154, 172, 255, 0.11);
99
- --border-strong: rgba(154, 172, 255, 0.20);
100
- --accent: #8f9dff;
101
- --accent-solid: #4a5ae0;
102
- --accent-ink: #ffffff;
103
- --radius: 14px;
104
- --radius-sm: 9px;
100
+ --text-muted: #7d879e;
101
+ --grid: #1e2436;
102
+ --axis: #2b3348;
103
+ --border: rgba(154, 172, 255, 0.11);
104
+ --border-strong: rgba(154, 172, 255, 0.20);
105
+ --hairline: rgba(154, 172, 255, 0.066);
106
+ --accent: #8f9dff;
107
+ --accent-solid: #4a5ae0;
108
+ --accent-ink: #ffffff;
105
109
  --shadow: 0 1px 2px rgba(0, 0, 0, .45), 0 14px 40px rgba(4, 8, 20, .45);
106
- --glow:
107
- radial-gradient(1100px 460px at 8% -10%, rgba(94, 114, 255, .17), transparent 62%),
108
- radial-gradient(820px 420px at 92% -14%, rgba(25, 158, 112, .11), transparent 60%);
110
+ --glow: radial-gradient(1100px 460px at 8% -10%, rgba(94, 114, 255, .17), transparent 62%), radial-gradient(820px 420px at 92% -14%, rgba(25, 158, 112, .11), transparent 60%);
109
111
  --hero-glow: radial-gradient(460px 220px at 8% 0%, rgba(94, 114, 255, .22), transparent 72%);
112
+ --radius: 14px; --radius-sm: 9px;
113
+ --pad-card: 14px 16px; --gap-grid: 14px; --fig-scale: 1; --sec-space: 26px; --tracking-h: -0.015em;
114
+ --display: var(--sans); --figure: var(--sans);
110
115
  }
111
-
112
116
  :root[data-skin='aurora'][data-mode='light'] {
113
- --plane: #f5f7fc;
114
- --surface-1: #ffffff;
115
- --surface-2: #f1f4fa;
116
- --surface-3: #e6eaf4;
117
- --text-primary: #0d1220;
117
+ --plane: #f5f7fc;
118
+ --surface-1: #ffffff;
119
+ --surface-2: #f1f4fa;
120
+ --surface-3: #e6eaf4;
121
+ --text-primary: #0d1220;
118
122
  --text-secondary: #4b5468;
119
- --text-muted: #767f92;
120
- --grid: #e4e8f1;
121
- --axis: #c7cddb;
122
- --border: rgba(13, 18, 32, 0.09);
123
- --border-strong: rgba(13, 18, 32, 0.17);
124
- --accent: #3d4dd6;
125
- --accent-solid: #3d4dd6;
126
- --accent-ink: #ffffff;
123
+ --text-muted: #6c7488;
124
+ --grid: #e4e8f1;
125
+ --axis: #c7cddb;
126
+ --border: rgba(13, 18, 32, 0.09);
127
+ --border-strong: rgba(13, 18, 32, 0.17);
128
+ --hairline: rgba(13, 18, 32, 0.054);
129
+ --accent: #3d4dd6;
130
+ --accent-solid: #3d4dd6;
131
+ --accent-ink: #ffffff;
127
132
  --shadow: 0 1px 2px rgba(16, 24, 48, .06), 0 12px 32px rgba(16, 24, 48, .08);
128
133
  --glow: radial-gradient(1100px 460px at 8% -10%, rgba(94, 114, 255, .10), transparent 62%);
129
134
  --hero-glow: radial-gradient(420px 180px at 12% 0%, rgba(94, 114, 255, .09), transparent 70%);
130
135
  }
131
136
 
132
- /* ======================================================= skin: terminal ==== */
133
- /* Near-black, hairlines, mono numerals, one electric accent. An instrument. */
137
+ /* skin: terminal Near-black, hairlines, mono */
134
138
  :root[data-skin='terminal'] {
135
- --plane: #06070a;
136
- --surface-1: #0c0e13;
137
- --surface-2: #11141a;
138
- --surface-3: #171b22;
139
- --text-primary: #eef2f6;
139
+ --plane: #06070a;
140
+ --surface-1: #0c0e13;
141
+ --surface-2: #11141a;
142
+ --surface-3: #171b22;
143
+ --text-primary: #eef2f6;
140
144
  --text-secondary: #9aa4ad;
141
- --text-muted: #6b737c;
142
- --grid: #171b22;
143
- --axis: #262c34;
144
- --border: rgba(255, 255, 255, 0.09);
145
- --border-strong: rgba(255, 255, 255, 0.16);
146
- --accent: #24d67e;
147
- --accent-solid: #24d67e;
148
- --accent-ink: #04140b;
149
- --radius: 5px;
150
- --radius-sm: 4px;
145
+ --text-muted: #737b85;
146
+ --grid: #171b22;
147
+ --axis: #262c34;
148
+ --border: rgba(255, 255, 255, 0.09);
149
+ --border-strong: rgba(255, 255, 255, 0.16);
150
+ --hairline: rgba(255, 255, 255, 0.054);
151
+ --accent: #24d67e;
152
+ --accent-solid: #24d67e;
153
+ --accent-ink: #04140b;
151
154
  --shadow: 0 0 0 1px rgba(255, 255, 255, .05), 0 10px 26px rgba(0, 0, 0, .55);
152
155
  --glow: none;
153
156
  --hero-glow: none;
154
- --display: var(--mono);
155
- --figure: var(--mono);
156
- --tracking-h: 0.01em;
157
- --pad-card: 11px 13px;
158
- --gap-grid: 9px;
159
- --fig-scale: 0.94;
160
- --sec-space: 20px;
157
+ --radius: 5px; --radius-sm: 4px;
158
+ --pad-card: 11px 13px; --gap-grid: 9px; --fig-scale: 0.94; --sec-space: 20px; --tracking-h: 0.01em;
159
+ --display: var(--mono); --figure: var(--mono);
161
160
  }
162
-
163
161
  :root[data-skin='terminal'][data-mode='light'] {
164
- --plane: #f7f8f9;
165
- --surface-1: #ffffff;
166
- --surface-2: #f2f4f5;
167
- --surface-3: #e8eaec;
168
- --text-primary: #0a0c0f;
162
+ --plane: #f7f8f9;
163
+ --surface-1: #ffffff;
164
+ --surface-2: #f2f4f5;
165
+ --surface-3: #e8eaec;
166
+ --text-primary: #0a0c0f;
169
167
  --text-secondary: #494f57;
170
- --text-muted: #6f767e;
171
- --grid: #e8eaec;
172
- --axis: #c9cdd2;
173
- --border: rgba(10, 12, 15, 0.10);
174
- --border-strong: rgba(10, 12, 15, 0.18);
175
- --accent: #0b6b3d;
176
- --accent-solid: #0b6b3d;
177
- --accent-ink: #ffffff;
168
+ --text-muted: #666d75;
169
+ --grid: #e8eaec;
170
+ --axis: #c9cdd2;
171
+ --border: rgba(10, 12, 15, 0.10);
172
+ --border-strong: rgba(10, 12, 15, 0.18);
173
+ --hairline: rgba(10, 12, 15, 0.06);
174
+ --accent: #0b6b3d;
175
+ --accent-solid: #0b6b3d;
176
+ --accent-ink: #ffffff;
178
177
  --shadow: 0 0 0 1px rgba(10, 12, 15, .05), 0 8px 20px rgba(10, 12, 15, .07);
178
+ --glow: none;
179
+ --hero-glow: none;
179
180
  }
180
181
 
181
- /* ====================================================== skin: editorial ==== */
182
- /* Warm charcoal, wide margins, serif display, numerals treated as type. */
182
+ /* skin: editorial Warm charcoal, serif figures */
183
183
  :root[data-skin='editorial'] {
184
- --plane: #16130c;
185
- --surface-1: #201c13;
186
- --surface-2: #282318;
187
- --surface-3: #322b1f;
188
- --text-primary: #f8f4ec;
184
+ --plane: #16130c;
185
+ --surface-1: #201c13;
186
+ --surface-2: #282318;
187
+ --surface-3: #322b1f;
188
+ --text-primary: #f8f4ec;
189
189
  --text-secondary: #c4bcab;
190
- --text-muted: #948c7c;
191
- --grid: #262218;
192
- --axis: #3a3529;
193
- --border: rgba(255, 240, 214, 0.10);
194
- --border-strong: rgba(255, 240, 214, 0.18);
195
- --accent: #e0a852;
196
- --accent-solid: #e0a852;
197
- --accent-ink: #1a1409;
198
- --radius: 8px;
199
- --radius-sm: 6px;
190
+ --text-muted: #998f7d;
191
+ --grid: #262218;
192
+ --axis: #3a3529;
193
+ --border: rgba(255, 240, 214, 0.10);
194
+ --border-strong: rgba(255, 240, 214, 0.18);
195
+ --hairline: rgba(255, 240, 214, 0.06);
196
+ --accent: #e0a852;
197
+ --accent-solid: #e0a852;
198
+ --accent-ink: #1a1409;
200
199
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 12px 34px rgba(20, 14, 4, .38);
201
200
  --glow: radial-gradient(900px 420px at 88% -12%, rgba(224, 168, 82, .11), transparent 60%);
202
201
  --hero-glow: none;
203
- --display: var(--serif);
204
- --figure: var(--serif);
205
- --tracking-h: -0.005em;
206
- --pad-card: 20px 22px;
207
- --gap-grid: 18px;
208
- --fig-scale: 1.12;
209
- --sec-space: 38px;
202
+ --radius: 8px; --radius-sm: 6px;
203
+ --pad-card: 20px 22px; --gap-grid: 18px; --fig-scale: 1.12; --sec-space: 38px; --tracking-h: -0.005em;
204
+ --display: var(--serif); --figure: var(--serif);
210
205
  }
211
-
212
206
  :root[data-skin='editorial'][data-mode='light'] {
213
- --plane: #f9f6ef;
214
- --surface-1: #fffdf8;
215
- --surface-2: #f4f0e6;
216
- --surface-3: #eae5d8;
217
- --text-primary: #1a1712;
207
+ --plane: #f9f6ef;
208
+ --surface-1: #fffdf8;
209
+ --surface-2: #f4f0e6;
210
+ --surface-3: #eae5d8;
211
+ --text-primary: #1a1712;
218
212
  --text-secondary: #544e42;
219
- --text-muted: #7d766a;
220
- --grid: #e8e2d4;
221
- --axis: #cbc4b2;
222
- --border: rgba(26, 23, 18, 0.10);
223
- --border-strong: rgba(26, 23, 18, 0.18);
224
- --accent: #8a5514;
225
- --accent-solid: #8a5514;
226
- --accent-ink: #ffffff;
213
+ --text-muted: #736c60;
214
+ --grid: #e8e2d4;
215
+ --axis: #cbc4b2;
216
+ --border: rgba(26, 23, 18, 0.10);
217
+ --border-strong: rgba(26, 23, 18, 0.18);
218
+ --hairline: rgba(26, 23, 18, 0.06);
219
+ --accent: #8a5514;
220
+ --accent-solid: #8a5514;
221
+ --accent-ink: #ffffff;
227
222
  --shadow: 0 1px 2px rgba(60, 45, 20, .06), 0 12px 30px rgba(60, 45, 20, .08);
228
223
  --glow: radial-gradient(900px 420px at 88% -12%, rgba(224, 168, 82, .13), transparent 60%);
224
+ --hero-glow: none;
229
225
  }
230
226
 
227
+ /* @generated design-tokens:end */
228
+
231
229
  * { box-sizing: border-box; }
232
230
 
233
231
  html, body {
@@ -250,12 +248,18 @@ a:hover { text-decoration: underline; }
250
248
  /* ---------------------------------------------------------------- layout -- */
251
249
  .wrap { max-width: 1680px; margin: 0 auto; padding: 0 20px 72px; }
252
250
 
251
+ /* A translucent material: content scrolls under it, and the blur + saturation
252
+ keep the header legible over any chart. Frostier when the user asks for it. */
253
253
  header.top {
254
254
  position: sticky; top: 0; z-index: 40;
255
- background: color-mix(in srgb, var(--plane) 88%, transparent);
256
- backdrop-filter: blur(10px);
255
+ background: color-mix(in srgb, var(--plane) 78%, transparent);
256
+ backdrop-filter: blur(18px) saturate(1.6);
257
+ -webkit-backdrop-filter: blur(18px) saturate(1.6);
257
258
  border-bottom: 1px solid var(--border);
258
259
  }
260
+ @media (prefers-reduced-transparency: reduce) {
261
+ header.top { background: var(--plane); backdrop-filter: none; -webkit-backdrop-filter: none; }
262
+ }
259
263
  .top-inner {
260
264
  max-width: 1680px; margin: 0 auto; padding: 14px 20px 12px;
261
265
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
@@ -280,13 +284,20 @@ header.top {
280
284
  /* ---------------------------------------------------------------- inputs -- */
281
285
  .btn {
282
286
  appearance: none; border: 1px solid var(--border-strong); background: var(--surface-2);
283
- color: var(--text-primary); font: inherit; font-size: 13px; padding: 7px 12px;
287
+ color: var(--text-primary); font: inherit; font-size: 13px; letter-spacing: var(--tr-body); padding: 7px 12px;
284
288
  border-radius: var(--radius-sm); cursor: pointer; display: inline-flex;
285
- align-items: center; gap: 7px; transition: background .14s, border-color .14s, transform .08s;
286
- white-space: nowrap;
289
+ align-items: center; gap: 7px; white-space: nowrap;
290
+ transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out), transform var(--dur-press) var(--ease-out);
291
+ }
292
+ @media (hover: hover) and (pointer: fine) {
293
+ .btn:hover { background: var(--surface-3); border-color: var(--text-muted); }
294
+ }
295
+ /* Feedback lives on the press, not the release: the interface heard you. */
296
+ .btn:active { transform: scale(.97); }
297
+ .btn:focus-visible, .chip:focus-visible, nav.tabs button:focus-visible, select:focus-visible,
298
+ input:focus-visible, .theme-row:focus-visible, .ms-row:focus-visible, table.tbl tbody tr:focus-visible {
299
+ outline: 2px solid var(--accent); outline-offset: 2px;
287
300
  }
288
- .btn:hover { background: var(--surface-3); border-color: var(--text-muted); }
289
- .btn:active { transform: translateY(1px); }
290
301
  .btn[disabled] { opacity: .5; cursor: default; }
291
302
  .btn.primary { background: var(--accent-solid); border-color: transparent; color: var(--accent-ink); }
292
303
  .btn.primary:hover { filter: brightness(1.08); }
@@ -310,9 +321,14 @@ label.fld { display: flex; flex-direction: column; gap: 4px; font-size: 11px; co
310
321
  .chips { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
311
322
  .chip {
312
323
  border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--text-secondary);
313
- border-radius: 999px; padding: 4px 10px; font-size: 12px; cursor: pointer; white-space: nowrap;
324
+ border-radius: var(--radius-pill); padding: 4px 10px; font-size: 12px; letter-spacing: var(--tr-caption);
325
+ cursor: pointer; white-space: nowrap;
326
+ transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out), transform var(--dur-press) var(--ease-out);
327
+ }
328
+ @media (hover: hover) and (pointer: fine) {
329
+ .chip:hover { background: var(--surface-3); color: var(--text-primary); }
314
330
  }
315
- .chip:hover { background: var(--surface-3); color: var(--text-primary); }
331
+ .chip:active { transform: scale(.97); }
316
332
  .chip[aria-pressed=true] { background: var(--accent-solid); border-color: transparent; color: var(--accent-ink); }
317
333
  .chip .x { opacity: .7; margin-left: 4px; }
318
334
 
@@ -323,8 +339,17 @@ label.fld { display: flex; flex-direction: column; gap: 4px; font-size: 11px; co
323
339
  position: absolute; z-index: 60; top: calc(100% + 5px); left: 0; width: 268px;
324
340
  background: var(--surface-1); border: 1px solid var(--border-strong);
325
341
  border-radius: var(--radius); box-shadow: var(--shadow); padding: 8px; display: none;
342
+ /* A popover grows from its trigger, never from its own centre. */
343
+ transform-origin: top left;
344
+ transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
345
+ }
346
+ .ms.open .ms-pop { display: block; opacity: 1; transform: scale(1); }
347
+ @starting-style {
348
+ .ms.open .ms-pop { opacity: 0; transform: scale(.96); }
349
+ .theme.open .theme-pop { opacity: 0; transform: scale(.96); }
350
+ dialog[open] { opacity: 0; transform: scale(.97); }
351
+ dialog[open]::backdrop { opacity: 0; }
326
352
  }
327
- .ms.open .ms-pop { display: block; }
328
353
  .ms-pop input { width: 100%; margin-bottom: 7px; }
329
354
  .ms-list { max-height: 258px; overflow: auto; }
330
355
  .ms-row {
@@ -346,31 +371,44 @@ label.fld { display: flex; flex-direction: column; gap: 4px; font-size: 11px; co
346
371
  .card {
347
372
  background: var(--surface-1); border: 1px solid var(--border);
348
373
  border-radius: var(--radius); padding: var(--pad-card); min-width: 0;
349
- transition: border-color .16s ease;
350
- }
351
- .card:hover { border-color: var(--border-strong); }
374
+ transition: border-color var(--dur-fast) var(--ease-out);
375
+ }
376
+ @media (hover: hover) and (pointer: fine) {
377
+ .card:hover { border-color: var(--border-strong); }
378
+ }
379
+ /* Cards stagger in once, when the VIEW changes (app.js sets .view-enter for
380
+ that render only). A filter change re-renders the same view instantly. */
381
+ @keyframes card-in { from { opacity: 0; transform: translateY(6px); } }
382
+ #view.view-enter .card { animation: card-in var(--dur-base) var(--ease-out) both; }
383
+ #view.view-enter .card:nth-child(2) { animation-delay: var(--stagger); }
384
+ #view.view-enter .card:nth-child(3) { animation-delay: calc(var(--stagger) * 2); }
385
+ #view.view-enter .card:nth-child(4) { animation-delay: calc(var(--stagger) * 3); }
386
+ #view.view-enter .card:nth-child(5) { animation-delay: calc(var(--stagger) * 4); }
387
+ #view.view-enter .card:nth-child(6) { animation-delay: calc(var(--stagger) * 5); }
388
+ #view.view-enter .card:nth-child(n+7) { animation-delay: calc(var(--stagger) * 6); }
352
389
  /* The hero KPI is the one place a gradient is allowed: a wash behind a single
353
390
  number, never behind a mark whose colour has to stay comparable. */
354
391
  .card.hero-card { background-image: var(--hero-glow); }
355
392
  .card.pad0 { padding: 0; }
356
393
  .card h3 {
357
- margin: 0 0 2px; font-size: 12.5px; font-weight: 600; color: var(--text-secondary);
358
- font-family: var(--display);
359
- letter-spacing: .01em; display: flex; align-items: center; gap: 8px;
394
+ margin: 0 0 2px; font-size: var(--fs-caption); font-weight: 600; color: var(--text-secondary);
395
+ font-family: var(--display); line-height: var(--lh-caption);
396
+ letter-spacing: var(--tr-caption); display: flex; align-items: center; gap: 8px;
360
397
  }
361
- .card .hint { color: var(--text-muted); font-size: 11.5px; margin: 0 0 10px; }
398
+ .card .hint { color: var(--text-muted); font-size: var(--fs-label); letter-spacing: var(--tr-label); line-height: var(--lh-label); margin: 0 0 10px; }
362
399
  .card-head { display: flex; align-items: flex-start; gap: 10px; padding: var(--pad-card); padding-bottom: 0; }
363
400
  .card-head .spacer { flex: 1; }
364
401
  .card-body { padding: 10px 16px 14px; }
365
402
 
366
403
  .kpi { display: flex; flex-direction: column; gap: 3px; }
367
- .kpi .k-label { font-size: 11.5px; color: var(--text-muted); }
404
+ .kpi .k-label { font-size: var(--fs-label); letter-spacing: var(--tr-label); color: var(--text-muted); }
368
405
  .kpi .k-value {
369
- font-size: calc(25px * var(--fig-scale)); font-weight: 640;
370
- letter-spacing: var(--tracking-h); line-height: 1.12;
406
+ font-size: calc(var(--fs-figure) * var(--fig-scale)); font-weight: 640;
407
+ letter-spacing: var(--tracking-h); line-height: var(--lh-figure);
371
408
  font-family: var(--figure); font-variant-numeric: tabular-nums;
372
409
  }
373
- .kpi .k-value.hero { font-size: calc(42px * var(--fig-scale)); }
410
+ /* The one hero number on a view: bigger, and tighter still as type grows. */
411
+ .kpi .k-value.hero { font-size: calc(var(--fs-hero) * var(--fig-scale)); letter-spacing: calc(var(--tracking-h) - .01em); line-height: var(--lh-hero); }
374
412
  /* A long identifier is not a hero figure: keep it readable and let it wrap. */
375
413
  .kpi .k-value.str { font-size: 17px; font-weight: 600; line-height: 1.25; overflow-wrap: anywhere; }
376
414
  .card.wide { grid-column: span 2; }
@@ -392,19 +430,28 @@ label.fld { display: flex; flex-direction: column; gap: 4px; font-size: 11px; co
392
430
  }
393
431
  .sec-title::after { content: ''; flex: 1; height: 1px; background: var(--border); }
394
432
 
395
- nav.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-top: 4px; overflow-x: auto; }
433
+ /* The tab bar wraps to a second (or third) row rather than scrolling: with
434
+ twenty-plus tabs registered across several views, every tab and the ⌘K
435
+ chip stay visible with no clipped label at any width, and the active tab
436
+ never needs a scrollIntoView() call to stay on screen. */
437
+ .tabbar { display: flex; align-items: flex-end; gap: 8px; border-bottom: 1px solid var(--border); margin-top: 4px; }
438
+ nav.tabs { display: flex; flex-wrap: wrap; gap: 4px; flex: 1 1 auto; min-width: 0; }
439
+ .tabbar .palette-chip { flex: none; margin-bottom: 6px; }
396
440
  nav.tabs button {
397
441
  appearance: none; background: none; border: 0; border-bottom: 2px solid transparent;
398
- color: var(--text-muted); font: inherit; font-size: 13.5px; padding: 9px 12px;
442
+ color: var(--text-muted); font: inherit; font-size: var(--fs-body); letter-spacing: var(--tr-body); padding: 9px 12px;
399
443
  cursor: pointer; white-space: nowrap;
444
+ transition: color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
445
+ }
446
+ @media (hover: hover) and (pointer: fine) {
447
+ nav.tabs button:hover { color: var(--text-primary); }
400
448
  }
401
- nav.tabs button:hover { color: var(--text-primary); }
402
449
  nav.tabs button[aria-selected=true] { color: var(--text-primary); border-bottom-color: var(--accent); }
403
450
 
404
451
  /* ---------------------------------------------------------------- charts -- */
405
452
  .chart { width: 100%; display: block; overflow: visible; }
406
453
  .chart text { font-family: var(--sans); }
407
- .tick text { fill: var(--text-muted); font-size: 10.5px; font-variant-numeric: tabular-nums; }
454
+ .tick text { fill: var(--text-muted); font-size: var(--fs-micro); letter-spacing: var(--tr-micro); font-variant-numeric: tabular-nums; }
408
455
  .grid-line { stroke: var(--grid); stroke-width: 1; }
409
456
  .axis-line { stroke: var(--axis); stroke-width: 1; }
410
457
  .series-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
@@ -430,9 +477,11 @@ nav.tabs button[aria-selected=true] { color: var(--text-primary); border-bottom-
430
477
  position: fixed; z-index: 200; pointer-events: none; min-width: 150px; max-width: 320px;
431
478
  background: var(--surface-1); border: 1px solid var(--border-strong);
432
479
  border-radius: var(--radius-sm); box-shadow: var(--shadow); padding: 9px 11px;
433
- font-size: 12px; opacity: 0; transition: opacity .1s; color: var(--text-primary);
480
+ font-size: 12px; opacity: 0; transform: scale(.98); transform-origin: top left;
481
+ transition: opacity var(--dur-press) var(--ease-out), transform var(--dur-press) var(--ease-out);
482
+ color: var(--text-primary);
434
483
  }
435
- .tip.on { opacity: 1; }
484
+ .tip.on { opacity: 1; transform: scale(1); }
436
485
  .tip .t-head { color: var(--text-secondary); font-size: 11px; margin-bottom: 6px; }
437
486
  .tip .t-row { display: flex; align-items: baseline; gap: 8px; padding: 1.5px 0; }
438
487
  .tip .t-key { width: 12px; height: 2.5px; border-radius: 2px; flex: none; align-self: center; }
@@ -493,10 +542,10 @@ table.tbl .mini {
493
542
 
494
543
  /* --------------------------------------------------------------- badges -- */
495
544
  .badge {
496
- display: inline-flex; align-items: center; gap: 4px; font-size: 10.5px;
497
- padding: 2px 7px; border-radius: 999px; border: 1px solid var(--border-strong);
545
+ display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-micro);
546
+ padding: 2px 7px; border-radius: var(--radius-pill); border: 1px solid var(--border-strong);
498
547
  color: var(--text-secondary); background: var(--surface-2); white-space: nowrap;
499
- letter-spacing: .02em;
548
+ letter-spacing: var(--tr-micro);
500
549
  }
501
550
  .badge.est { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 45%, transparent); }
502
551
  .badge.meas { color: var(--good); border-color: color-mix(in srgb, var(--good) 45%, transparent); }
@@ -537,12 +586,14 @@ table.tbl .mini {
537
586
  .kv dt { color: var(--text-muted); }
538
587
  .kv dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
539
588
 
589
+ /* A modal is the one popover that stays centred: it is not anchored to a trigger. */
540
590
  dialog {
541
591
  border: 1px solid var(--border-strong); border-radius: var(--radius); background: var(--surface-1);
542
592
  color: var(--text-primary); padding: 0; max-width: 720px; width: calc(100vw - 40px);
543
- box-shadow: var(--shadow);
593
+ box-shadow: var(--shadow); opacity: 1; transform: scale(1);
594
+ transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
544
595
  }
545
- dialog::backdrop { background: rgba(0,0,0,.55); backdrop-filter: blur(2px); }
596
+ dialog::backdrop { background: rgba(0,0,0,.55); backdrop-filter: blur(2px); opacity: 1; transition: opacity var(--dur-base) var(--ease-out); }
546
597
  dialog .d-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--border); }
547
598
  dialog .d-body { padding: 14px 16px; max-height: 66vh; overflow: auto; }
548
599
  dialog .d-foot { display: flex; gap: 8px; justify-content: flex-end; padding: 12px 16px; border-top: 1px solid var(--border); }
@@ -570,7 +621,9 @@ dialog h2 { margin: 0; font-size: 15px; }
570
621
 
571
622
  @media print {
572
623
  header.top { position: static; }
573
- .btn, nav.tabs, .filters { display: none !important; }
624
+ /* .tabbar, not nav.tabs: the wrapper also carries the ⌘K chip, and hiding
625
+ only the nested nav would leave that chip printed on its own row. */
626
+ .btn, .tabbar, .filters { display: none !important; }
574
627
  .card { break-inside: avoid; }
575
628
  }
576
629
 
@@ -579,8 +632,10 @@ dialog h2 { margin: 0; font-size: 15px; }
579
632
  position: absolute; z-index: 70; top: calc(100% + 6px); right: 0; width: 268px;
580
633
  background: var(--surface-1); border: 1px solid var(--border-strong);
581
634
  border-radius: var(--radius); box-shadow: var(--shadow); padding: 8px; display: none;
635
+ transform-origin: top right;
636
+ transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
582
637
  }
583
- .theme.open .theme-pop { display: block; }
638
+ .theme.open .theme-pop { display: block; opacity: 1; transform: scale(1); }
584
639
  .theme { position: relative; }
585
640
  .theme-row {
586
641
  display: flex; align-items: center; gap: 10px; padding: 7px 8px; width: 100%;
@@ -621,6 +676,8 @@ dialog h2 { margin: 0; font-size: 15px; }
621
676
  }
622
677
 
623
678
  /* ---------------------------------------------------------------- live --- */
679
+ /* The pill lives in the header, so it stays here. The Live TAB's own rules
680
+ moved to src/ui/styles/live.css when the tab became a registered view. */
624
681
 
625
682
  .live-pill {
626
683
  display: inline-flex;
@@ -639,6 +696,56 @@ dialog h2 { margin: 0; font-size: 15px; }
639
696
  .badge.warn { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 45%, transparent); }
640
697
  .badge.stale { color: var(--text-muted); }
641
698
 
642
- .sev-high { color: #fff; background: var(--critical); border-color: transparent; font-weight: 640; }
643
- .sev-warn { color: var(--warning); border-color: color-mix(in srgb, var(--warning) 45%, transparent); }
644
- .sev-info { color: var(--text-muted); }
699
+ /* ---------------------------------------------------------------- story --- */
700
+ /* The three sentences that matter, before any chart. Numbers wear the figure
701
+ face so the eye lands on them; the words stay words. */
702
+ .story {
703
+ display: grid; gap: 10px;
704
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
705
+ }
706
+ .story-line {
707
+ display: flex; gap: 12px; align-items: flex-start;
708
+ padding: 14px 16px; background: var(--surface-1); border: 1px solid var(--border);
709
+ border-radius: var(--radius); background-image: var(--hero-glow);
710
+ }
711
+ .story-line.correlation { border-left: 2px solid var(--series-7); }
712
+ .story-ico { font-size: 18px; line-height: 1.25; flex: none; }
713
+ .story-text {
714
+ margin: 0; font-size: var(--fs-title); letter-spacing: var(--tr-title); line-height: 1.4;
715
+ color: var(--text-secondary); text-wrap: pretty;
716
+ }
717
+ .story-text strong {
718
+ color: var(--text-primary); font-family: var(--figure); font-weight: 680;
719
+ font-variant-numeric: tabular-nums; letter-spacing: var(--tracking-h);
720
+ }
721
+
722
+ /* -------------------------------------------------------------- receipts --- */
723
+ .branch-cell { display: inline-flex; align-items: center; gap: 8px; }
724
+ .branch-name { font-family: var(--mono); font-size: 12px; color: var(--text-primary); }
725
+ .receipt { display: grid; gap: 14px; padding: 4px 2px; }
726
+ .receipt-head { display: flex; flex-direction: column; gap: 2px; }
727
+ .receipt-kicker { font-size: var(--fs-label); letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted); }
728
+ .receipt-branch { font-family: var(--mono); font-size: var(--fs-title); color: var(--text-primary); overflow-wrap: anywhere; }
729
+ .receipt-total { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
730
+ .receipt-amount {
731
+ font-family: var(--figure); font-size: calc(var(--fs-hero) * var(--fig-scale)); font-weight: 680;
732
+ letter-spacing: calc(var(--tracking-h) - .01em); line-height: var(--lh-hero); font-variant-numeric: tabular-nums;
733
+ }
734
+ .receipt-amount-sub { color: var(--text-muted); font-size: var(--fs-caption); }
735
+ .split-bar { display: flex; height: 10px; border-radius: var(--radius-pill); overflow: hidden; gap: 2px; background: var(--surface-3); }
736
+ .split-bar .seg { display: block; height: 100%; }
737
+ .split-bar .seg.ctx, .sw.ctx { background: var(--series-1); }
738
+ .split-bar .seg.work, .sw.work { background: var(--series-2); flex: 1; }
739
+ .split-legend { display: flex; gap: 16px; font-size: var(--fs-caption); color: var(--text-secondary); }
740
+ .split-legend .sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; vertical-align: -1px; }
741
+ .receipt-kv { font-size: var(--fs-caption); }
742
+ .receipt-kv dd { color: var(--text-primary); }
743
+ .receipt-foot { margin: 0; color: var(--text-muted); font-size: var(--fs-label); line-height: var(--lh-label); }
744
+
745
+ /* --------------------------------------------------------- reduced motion --- */
746
+ /* Every primitive degrades to an instant, complete state — never a half-state. */
747
+ @media (prefers-reduced-motion: reduce) {
748
+ *, *::before, *::after { animation-duration: 1ms !important; animation-iteration-count: 1 !important; transition-duration: 1ms !important; }
749
+ .dot.busy { animation: none; opacity: 1; }
750
+ .btn:active, .chip:active { transform: none; }
751
+ }