tokenbiryani 0.2.0__py3-none-any.whl

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. tokenbiryani/__init__.py +3 -0
  2. tokenbiryani/api/__init__.py +0 -0
  3. tokenbiryani/api/app.py +583 -0
  4. tokenbiryani/api/asgi.py +32 -0
  5. tokenbiryani/cli.py +1045 -0
  6. tokenbiryani/config.py +532 -0
  7. tokenbiryani/core/__init__.py +0 -0
  8. tokenbiryani/core/account.py +258 -0
  9. tokenbiryani/core/batch.py +135 -0
  10. tokenbiryani/core/breaker.py +53 -0
  11. tokenbiryani/core/cacheadvice.py +239 -0
  12. tokenbiryani/core/diagnostics.py +131 -0
  13. tokenbiryani/core/estimator.py +180 -0
  14. tokenbiryani/core/gateway.py +2395 -0
  15. tokenbiryani/core/handoff.py +87 -0
  16. tokenbiryani/core/keys.py +199 -0
  17. tokenbiryani/core/limits.py +440 -0
  18. tokenbiryani/core/oauth.py +222 -0
  19. tokenbiryani/core/pacing.py +320 -0
  20. tokenbiryani/core/queue.py +132 -0
  21. tokenbiryani/core/router.py +323 -0
  22. tokenbiryani/core/secrets.py +114 -0
  23. tokenbiryani/core/session.py +117 -0
  24. tokenbiryani/dashboard/__init__.py +56 -0
  25. tokenbiryani/dashboard/console.css +610 -0
  26. tokenbiryani/dashboard/console.html +3250 -0
  27. tokenbiryani/observability/__init__.py +0 -0
  28. tokenbiryani/observability/events.py +171 -0
  29. tokenbiryani/observability/usage.py +226 -0
  30. tokenbiryani/prices.yaml +77 -0
  31. tokenbiryani/providers/__init__.py +0 -0
  32. tokenbiryani/providers/anthropic_api.py +118 -0
  33. tokenbiryani/providers/base.py +173 -0
  34. tokenbiryani/providers/bedrock.py +182 -0
  35. tokenbiryani/providers/oauth.py +165 -0
  36. tokenbiryani/providers/oauth_credentials.py +293 -0
  37. tokenbiryani/providers/translate.py +35 -0
  38. tokenbiryani/providers/vertex.py +144 -0
  39. tokenbiryani/proxy/__init__.py +0 -0
  40. tokenbiryani/proxy/errors.py +169 -0
  41. tokenbiryani/proxy/sse.py +98 -0
  42. tokenbiryani/store/__init__.py +0 -0
  43. tokenbiryani/store/base.py +150 -0
  44. tokenbiryani/store/memory.py +149 -0
  45. tokenbiryani/store/redis_store.py +222 -0
  46. tokenbiryani/store/sqlite.py +336 -0
  47. tokenbiryani-0.2.0.dist-info/METADATA +697 -0
  48. tokenbiryani-0.2.0.dist-info/RECORD +51 -0
  49. tokenbiryani-0.2.0.dist-info/WHEEL +4 -0
  50. tokenbiryani-0.2.0.dist-info/entry_points.txt +2 -0
  51. tokenbiryani-0.2.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,610 @@
1
+ /* ============================================================================
2
+ tokenbiryani console — design system
3
+
4
+ Minimal by construction: one accent, hairline borders, no shadows doing work
5
+ a border can do, and a single 4px spacing scale. Colour is reserved for
6
+ state, and state colour is validated for colour-vision deficiency — the
7
+ accent is deliberately off that ramp so it can mean "interactive".
8
+ ========================================================================= */
9
+
10
+ :root{
11
+ /* neutrals, very slightly cool so the warm accent reads as chosen */
12
+ --bg:#0A0C10; --surface:#111419; --surface-2:#171B22; --surface-3:#1D222B;
13
+ --border:#1E242D; --border-strong:#2C3540;
14
+ --text:#E9ECF1; --text-2:#99A2AE; --text-3:#616B79;
15
+
16
+ --accent:#E9A94A; --accent-ink:#1A1204; --accent-soft:#241C0F;
17
+ --ready:#2FA47A; --ready-t:#4FD09B; --ready-w:#0E2620;
18
+ --cool:#4A85DE; --cool-t:#7FADEE; --cool-w:#101B2B;
19
+ --crit:#DA6355; --crit-t:#F08878; --crit-w:#2A1614;
20
+ --cache:#9068DE; --cache-t:#B18CF0; --cache-w:#1C1630;
21
+
22
+ --r-xs:5px; --r-sm:8px; --r:11px; --r-lg:16px;
23
+ --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; --s7:48px;
24
+ --row:14px;
25
+ --sans:"Inter var",Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,
26
+ "Helvetica Neue",Arial,sans-serif;
27
+ --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
28
+ --ease:cubic-bezier(.4,0,.2,1);
29
+ }
30
+ :root[data-theme="light"]{
31
+ --bg:#FAFAFB; --surface:#FFFFFF; --surface-2:#F4F5F7; --surface-3:#ECEEF1;
32
+ --border:#E6E8EC; --border-strong:#D0D5DC;
33
+ --text:#14181F; --text-2:#5A6472; --text-3:#8892A0;
34
+ --accent:#A8670F; --accent-ink:#FFFFFF; --accent-soft:#FBF2E3;
35
+ --ready:#12795B; --ready-t:#0D5B44; --ready-w:#E6F3EE;
36
+ --cool:#1F63C4; --cool-t:#17509F; --cool-w:#E7EEFB;
37
+ --crit:#A93125; --crit-t:#8A2318; --crit-w:#FAE9E6;
38
+ --cache:#7A4FD0; --cache-t:#5C39A4; --cache-w:#F1EBFC;
39
+ }
40
+
41
+ *,*::before,*::after{box-sizing:border-box}
42
+ html,body{height:100%}
43
+ body{margin:0;background:var(--bg);color:var(--text);font-family:var(--sans);
44
+ font-size:13.5px;line-height:1.5;-webkit-font-smoothing:antialiased;
45
+ text-rendering:optimizeLegibility}
46
+ .mono,code,kbd{font-family:var(--mono);font-variant-numeric:tabular-nums}
47
+ h1,h2,h3,h4{margin:0;font-weight:600;letter-spacing:-.018em}
48
+ p{margin:0}
49
+ button{font:inherit;cursor:pointer;border:0;background:none;color:inherit}
50
+ input,select,textarea{font:inherit}
51
+ a{color:var(--accent);text-underline-offset:2px}
52
+ :focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--r-xs)}
53
+ ::selection{background:var(--accent-soft);color:var(--text)}
54
+ .hidden{display:none!important}
55
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
56
+
57
+ /* ---------- shell ---------- */
58
+ .shell{display:grid;grid-template-columns:236px minmax(0,1fr);min-height:100vh}
59
+ @media(max-width:900px){.shell{grid-template-columns:1fr}}
60
+
61
+ .side{background:var(--surface);border-right:1px solid var(--border);
62
+ display:flex;flex-direction:column;gap:var(--s5);padding:var(--s5) var(--s4);
63
+ position:sticky;top:0;height:100vh;overflow-y:auto}
64
+ @media(max-width:900px){.side{position:static;height:auto;border-right:0;
65
+ border-bottom:1px solid var(--border)}}
66
+ .brand{display:flex;align-items:center;gap:var(--s2);padding:0 var(--s2)}
67
+ .brand .dot{width:22px;height:22px;border-radius:7px;background:var(--accent);
68
+ display:grid;place-items:center;font-size:12px;flex:none}
69
+ .brand b{font-size:14px;font-weight:650;letter-spacing:-.02em}
70
+ .brand span{font-size:10.5px;color:var(--text-3);display:block;letter-spacing:.02em}
71
+
72
+ .nav{display:flex;flex-direction:column;gap:2px;flex:1}
73
+ .nav button{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:var(--r-sm);
74
+ color:var(--text-2);font-size:13px;font-weight:500;text-align:left;width:100%;
75
+ transition:background .14s var(--ease),color .14s var(--ease)}
76
+ .nav button svg{width:16px;height:16px;flex:none;opacity:.8}
77
+ .nav button:hover{background:var(--surface-2);color:var(--text)}
78
+ .nav button.on{background:var(--surface-2);color:var(--text)}
79
+ .nav button.on svg{opacity:1;color:var(--accent)}
80
+ .nav .sep{margin:var(--s3) 10px var(--s1);font-size:9.5px;letter-spacing:.12em;
81
+ text-transform:uppercase;color:var(--text-3);font-weight:600}
82
+
83
+ .side-foot{display:flex;flex-direction:column;gap:var(--s2);
84
+ border-top:1px solid var(--border);padding-top:var(--s3)}
85
+ .side-foot .row{display:flex;gap:6px}
86
+ .chip{flex:1;border:1px solid var(--border);border-radius:var(--r-sm);padding:5px 8px;
87
+ font-size:11px;color:var(--text-2);text-align:center;
88
+ transition:border-color .14s var(--ease),color .14s var(--ease)}
89
+ .chip:hover{border-color:var(--border-strong);color:var(--text)}
90
+ .status{display:flex;align-items:center;gap:7px;font-size:11px;color:var(--text-3);padding:0 2px}
91
+ .status i{width:6px;height:6px;border-radius:50%;background:var(--text-3);flex:none}
92
+ .status.on i{background:var(--ready);box-shadow:0 0 0 3px var(--ready-w)}
93
+ .status.off i{background:var(--crit);box-shadow:0 0 0 3px var(--crit-w)}
94
+
95
+ .main{min-width:0;display:flex;flex-direction:column}
96
+ .topbar{display:flex;align-items:baseline;gap:var(--s3);padding:var(--s5) var(--s6) var(--s4);
97
+ flex-wrap:wrap}
98
+ .topbar h1{font-size:22px}
99
+ .topbar .sub{color:var(--text-3);font-size:12.5px}
100
+ .topbar .spacer{flex:1}
101
+ .view{padding:0 var(--s6) var(--s7);max-width:1320px;width:100%}
102
+ @media(max-width:640px){.topbar,.view{padding-left:var(--s4);padding-right:var(--s4)}}
103
+ .view[hidden]{display:none}
104
+ @media (prefers-reduced-motion:no-preference){
105
+ .view{animation:rise .22s var(--ease)}
106
+ @keyframes rise{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
107
+ }
108
+
109
+ /* ---------- primitives ---------- */
110
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);
111
+ overflow:hidden}
112
+ .card>header{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s3);
113
+ padding:var(--s3) var(--s4);border-bottom:1px solid var(--border);flex-wrap:wrap}
114
+ .card>header h3{font-size:12.5px}
115
+ .card>header .note{font-size:11px;color:var(--text-3);font-weight:400}
116
+ .card .pad{padding:var(--s4)}
117
+ .grid{display:grid;gap:var(--s3)}
118
+ .g2{grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}
119
+ .g3{grid-template-columns:repeat(auto-fit,minmax(230px,1fr))}
120
+ .stack{display:flex;flex-direction:column;gap:var(--s3)}
121
+ .row{display:flex;gap:var(--s2);align-items:center;flex-wrap:wrap}
122
+
123
+ .btn{border:1px solid var(--border-strong);border-radius:var(--r-sm);padding:7px 13px;
124
+ font-size:12.5px;font-weight:500;color:var(--text-2);
125
+ transition:border-color .14s var(--ease),color .14s var(--ease),background .14s var(--ease)}
126
+ .btn:hover{color:var(--text);border-color:var(--text-3)}
127
+ .btn.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink);font-weight:600}
128
+ .btn.primary:hover{filter:brightness(1.07)}
129
+ .btn.ghost{border-color:transparent;color:var(--text-3)}
130
+ .btn.ghost:hover{background:var(--surface-2);color:var(--text)}
131
+ .btn.danger{border-color:var(--crit);color:var(--crit-t)}
132
+ .btn.danger:hover{background:var(--crit-w)}
133
+ .btn:disabled{opacity:.45;cursor:default;filter:none}
134
+ .btn.sm{padding:4px 9px;font-size:11.5px}
135
+
136
+ label.f,.f{display:flex;flex-direction:column;gap:5px;font-size:11px;font-weight:600;
137
+ letter-spacing:.05em;text-transform:uppercase;color:var(--text-3);min-width:0}
138
+ label.f input,label.f select,label.f textarea{background:var(--surface-2);
139
+ border:1px solid var(--border);border-radius:var(--r-sm);padding:9px 11px;color:var(--text);
140
+ font-size:13px;font-weight:400;letter-spacing:normal;text-transform:none;width:100%;
141
+ transition:border-color .14s var(--ease),background .14s var(--ease)}
142
+ label.f input:focus,label.f select:focus{border-color:var(--accent);outline:none;
143
+ background:var(--surface)}
144
+ label.f .hint,.f .hint{font-size:10.5px;font-weight:400;letter-spacing:normal;text-transform:none;
145
+ color:var(--text-3)}
146
+
147
+ .pill{display:inline-flex;align-items:center;gap:6px;font-size:10px;font-weight:650;
148
+ letter-spacing:.06em;text-transform:uppercase;padding:3px 8px;border-radius:99px;white-space:nowrap}
149
+ .pill i{width:5px;height:5px;border-radius:50%;flex:none}
150
+ .pill.ready{background:var(--ready-w);color:var(--ready-t)} .pill.ready i{background:var(--ready)}
151
+ .pill.cooling{background:var(--cool-w);color:var(--cool-t)} .pill.cooling i{background:var(--cool)}
152
+ .pill.disabled{background:var(--crit-w);color:var(--crit-t)} .pill.disabled i{background:var(--crit)}
153
+ .pill.muted{background:var(--surface-2);color:var(--text-3)} .pill.muted i{background:var(--text-3)}
154
+ .pill.accent{background:var(--accent-soft);color:var(--accent)} .pill.accent i{background:var(--accent)}
155
+
156
+ /* Bare .note is the quiet caption beside a heading. Give it a tier and it becomes
157
+ a callout — same word, because it is the same thing said louder. */
158
+ .note{font-size:11px;color:var(--text-3);font-weight:400}
159
+ .note.err,.note.ok,.note.info,.note.warn{padding:11px 14px;border-radius:var(--r-sm);
160
+ font-size:12.5px;border:1px solid;display:flex;gap:10px;align-items:flex-start}
161
+ .note.err{background:var(--crit-w);border-color:var(--crit);color:var(--crit-t)}
162
+ .note.ok{background:var(--ready-w);border-color:var(--ready);color:var(--ready-t)}
163
+ .note.info{background:var(--cool-w);border-color:var(--cool);color:var(--cool-t)}
164
+ .note.warn{background:var(--accent-soft);border-color:var(--accent);color:var(--accent)}
165
+
166
+ .empty{padding:var(--s7) var(--s4);text-align:center;color:var(--text-3);font-size:12.5px;
167
+ line-height:1.8}
168
+ .empty b{color:var(--text-2);font-weight:550;display:block;font-size:14px;margin-bottom:4px}
169
+
170
+ table{width:100%;border-collapse:collapse}
171
+ th{text-align:left;font-size:10px;letter-spacing:.1em;text-transform:uppercase;
172
+ color:var(--text-3);font-weight:650;padding:0 var(--s3) var(--s2);white-space:nowrap}
173
+ th.r{text-align:right}
174
+ td{border-top:1px solid var(--border);padding:var(--row) var(--s3);color:var(--text-2);
175
+ vertical-align:middle}
176
+ td.r{text-align:right;white-space:nowrap;color:var(--text)}
177
+ tbody tr{transition:background .12s var(--ease)}
178
+ tbody tr:hover td{background:var(--surface-2)}
179
+ tr.click{cursor:pointer}
180
+ tr.click:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
181
+ .scroll{overflow-x:auto}
182
+
183
+ /* ---------- stat tiles ---------- */
184
+ .tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:var(--s3)}
185
+ .tile{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);
186
+ padding:var(--s4);display:flex;flex-direction:column;gap:6px}
187
+ .tile .lb{font-size:10.5px;font-weight:650;letter-spacing:.09em;text-transform:uppercase;
188
+ color:var(--text-3)}
189
+ .tile .vl{font-size:26px;font-weight:600;letter-spacing:-.03em;line-height:1.05}
190
+ .tile .vl small{font-size:13px;font-weight:500;color:var(--text-3);letter-spacing:0}
191
+ .tile .sub{font-size:11.5px;color:var(--text-3)}
192
+ .tile.good .vl{color:var(--ready-t)} .tile.cache .vl{color:var(--cache-t)}
193
+ .tile.bad .vl{color:var(--crit-t)}
194
+
195
+ /* Chart chrome lives with the series palette at the end of this file. */
196
+
197
+ .meter{display:flex;flex-direction:column;gap:5px;min-width:150px}
198
+ .meter .bar{display:flex;align-items:center;gap:8px}
199
+ .meter .k{font-size:9.5px;color:var(--text-3);width:20px;font-family:var(--mono)}
200
+ .meter .track{flex:1;height:5px;background:var(--surface-3);border-radius:99px;overflow:hidden}
201
+ .meter .track i{display:block;height:100%;border-radius:99px;transition:width .3s var(--ease)}
202
+ .meter .v{font-size:10.5px;color:var(--text-2);width:70px;text-align:right;font-family:var(--mono)}
203
+
204
+ /* ---------- request stream ---------- */
205
+ .stream{display:flex;flex-direction:column}
206
+ .stream .r{display:grid;grid-template-columns:70px 108px minmax(0,1fr) 72px 64px 62px;
207
+ gap:var(--s3);padding:var(--row) var(--s4);border-top:1px solid var(--border);
208
+ font-size:11.5px;color:var(--text-2);align-items:center;cursor:pointer;
209
+ font-family:var(--mono);transition:background .12s var(--ease)}
210
+ .stream .r:hover{background:var(--surface-2)}
211
+ .stream .r:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
212
+ .stream .r.head{cursor:default;font-family:var(--sans);font-size:10px;letter-spacing:.1em;
213
+ text-transform:uppercase;color:var(--text-3);font-weight:650;border-top:0;padding-bottom:var(--s2)}
214
+ .stream .r.head:hover{background:none}
215
+ .stream .r .m{color:var(--text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
216
+ .stream .r.brk{background:var(--cache-w)}
217
+ .ok{color:var(--ready-t)} .er{color:var(--crit-t)} .ca{color:var(--cache-t)} .dm{color:var(--text-3)}
218
+ @media(max-width:760px){.stream .r{grid-template-columns:1fr 1fr;gap:2px var(--s3)}}
219
+
220
+ /* ---------- inspector ---------- */
221
+ .chain{display:flex;align-items:stretch;flex-wrap:wrap;gap:0}
222
+ .node{background:var(--surface-2);border:1px solid var(--border-strong);border-radius:var(--r-sm);
223
+ padding:9px 13px;display:flex;flex-direction:column;gap:3px}
224
+ .node .n{font-size:12px;color:var(--text);font-family:var(--mono)}
225
+ .node .s{font-size:10.5px;font-family:var(--mono)}
226
+ .node.fail{border-color:var(--crit)} .node.fail .s{color:var(--crit-t)}
227
+ .node.win{border-color:var(--ready)} .node.win .s{color:var(--ready-t)}
228
+ .arrow{padding:0 12px;font-size:10.5px;color:var(--text-3);align-self:center;font-family:var(--mono)}
229
+ .kv{display:grid;grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:1px;
230
+ background:var(--border);border:1px solid var(--border);border-radius:var(--r-sm);overflow:hidden}
231
+ .kv>div{background:var(--surface);padding:10px 13px;display:flex;flex-direction:column;gap:3px}
232
+ .kv .lb{font-size:9.5px;font-weight:650;letter-spacing:.09em;text-transform:uppercase;color:var(--text-3)}
233
+ .kv .vl{font-size:14px;color:var(--text);font-family:var(--mono)}
234
+ .kv .vl.ca{color:var(--cache-t)}
235
+ tr.won td{background:var(--ready-w)} tr.won td:first-child{color:var(--ready-t)}
236
+
237
+ /* ---------- modal ---------- */
238
+ .scrim{position:fixed;inset:0;background:rgba(6,8,11,.62);backdrop-filter:blur(3px);
239
+ display:grid;place-items:center;padding:var(--s4);z-index:50}
240
+ :root[data-theme="light"] .scrim{background:rgba(20,24,31,.34)}
241
+ .modal{background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--r-lg);
242
+ width:min(520px,100%);max-height:88vh;overflow:auto}
243
+ .modal>header{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s3);
244
+ padding:var(--s5) var(--s5) var(--s3)}
245
+ .modal>header h3{font-size:16px}
246
+ .modal .pad{padding:0 var(--s5) var(--s5);display:flex;flex-direction:column;gap:var(--s3)}
247
+ .modal footer{display:flex;gap:var(--s2);justify-content:flex-end;padding:var(--s3) var(--s5) var(--s5)}
248
+ @media (prefers-reduced-motion:no-preference){
249
+ .scrim{animation:fade .16s var(--ease)}
250
+ .modal{animation:pop .2s var(--ease)}
251
+ @keyframes fade{from{opacity:0}to{opacity:1}}
252
+ @keyframes pop{from{opacity:0;transform:translateY(8px) scale(.99)}to{opacity:1;transform:none}}
253
+ }
254
+
255
+ /* ---------- onboarding ----------
256
+ A full page, not a dialog. The rail states the whole shape of the setup up
257
+ front — four steps, where you are, what is left — because the thing being
258
+ asked for is a credential, and a wizard that reveals its length one screen at
259
+ a time earns none of the trust that asking for one requires. */
260
+ .onboard{min-height:100vh;display:grid;grid-template-columns:296px minmax(0,1fr)}
261
+
262
+ .wiz-rail{background:var(--surface);border-right:1px solid var(--border);
263
+ padding:var(--s5) var(--s4);display:flex;flex-direction:column;gap:var(--s5)}
264
+ .wiz-rail .brand{padding:0}
265
+
266
+ .wiz-steps{display:flex;flex-direction:column;gap:2px;flex:1}
267
+ .wiz-step{display:flex;gap:var(--s3);align-items:flex-start;padding:10px var(--s2);
268
+ border-radius:var(--r-sm);text-align:left;width:100%}
269
+ .wiz-step .n{width:22px;height:22px;flex:none;border-radius:50%;display:grid;
270
+ place-items:center;font-size:11px;font-weight:650;font-family:var(--mono);
271
+ background:var(--surface-3);color:var(--text-3);border:1px solid transparent;
272
+ transition:background .2s var(--ease),color .2s var(--ease)}
273
+ .wiz-step .t{min-width:0}
274
+ .wiz-step .t b{display:block;font-size:12.5px;font-weight:550;color:var(--text-3)}
275
+ .wiz-step .t span{display:block;font-size:11px;color:var(--text-3);line-height:1.5;
276
+ margin-top:2px}
277
+ .wiz-step.on{background:var(--surface-2)}
278
+ .wiz-step.on .n{background:var(--accent);color:var(--accent-ink)}
279
+ .wiz-step.on .t b{color:var(--text)}
280
+ .wiz-step.done .n{background:var(--ready-w);color:var(--ready-t);
281
+ border-color:var(--ready)}
282
+ .wiz-step.done .t b{color:var(--text-2)}
283
+
284
+ .wiz-foot{display:flex;flex-direction:column;gap:var(--s2);
285
+ border-top:1px solid var(--border);padding-top:var(--s4)}
286
+ .wiz-foot .row{display:flex;gap:6px}
287
+ .wiz-foot .chip{flex:1;white-space:nowrap}
288
+ .wiz-foot .addr{font-size:10.5px;color:var(--text-3);font-family:var(--mono);
289
+ word-break:break-all}
290
+
291
+ .wiz-body .card>footer{display:flex;gap:var(--s2);justify-content:flex-end;
292
+ padding:0 var(--s4) var(--s4);flex-wrap:wrap}
293
+ .wiz-main{padding:var(--s6);overflow-y:auto;min-width:0}
294
+ /* The pane is the content. An inner cap left a wizard's worth of empty gutter on
295
+ any real monitor; the prose keeps its own measure below, which is the only part
296
+ that actually wants to be narrow. */
297
+ .wiz-body{width:100%;max-width:1320px;display:flex;flex-direction:column;gap:var(--s4)}
298
+ .onboard h2{font-size:22px}
299
+ .onboard .lede{color:var(--text-3);font-size:13px;line-height:1.65;max-width:70ch}
300
+
301
+ /* Step 1: the routes in, one card each. A radio dressed as a card — the label
302
+ is the hit target, so the whole card is clickable without any script. */
303
+ .routes{display:flex;flex-direction:column;gap:var(--s2)}
304
+ .route{display:flex;gap:var(--s3);align-items:flex-start;padding:var(--s4);
305
+ border:1px solid var(--border);border-radius:var(--r);background:var(--surface);
306
+ cursor:pointer;transition:border-color .14s var(--ease),background .14s var(--ease)}
307
+ .route:hover{border-color:var(--border-strong);background:var(--surface-2)}
308
+ .route:has(input:checked){border-color:var(--accent);background:var(--accent-soft)}
309
+ .route:has(input:focus-visible){outline:2px solid var(--accent);outline-offset:2px}
310
+ .route input{margin-top:2px;accent-color:var(--accent);flex:none;width:auto}
311
+ .route .rb{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1}
312
+ .route .rt{display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap;
313
+ font-size:13.5px;font-weight:600;color:var(--text)}
314
+ .route .rs{font-size:11.5px;color:var(--text-3);line-height:1.6}
315
+
316
+ /* Step 3's verdict, one line per thing checked. */
317
+ .checks{display:flex;flex-direction:column;gap:var(--s2)}
318
+ .check{display:flex;gap:var(--s2);align-items:flex-start;font-size:12.5px;
319
+ color:var(--text-2)}
320
+ .check i{width:16px;height:16px;flex:none;border-radius:50%;margin-top:1px;
321
+ display:grid;place-items:center;font-size:10px;font-style:normal;font-weight:700}
322
+ .check.ok i{background:var(--ready-w);color:var(--ready-t)}
323
+ .check.no i{background:var(--crit-w);color:var(--crit-t)}
324
+ .check.na i{background:var(--surface-3);color:var(--text-3)}
325
+
326
+ @media(max-width:900px){
327
+ .onboard{grid-template-columns:1fr}
328
+ .wiz-rail{flex-direction:row;align-items:center;gap:var(--s4);flex-wrap:wrap;
329
+ border-right:0;border-bottom:1px solid var(--border);padding:var(--s4)}
330
+ .wiz-steps{flex-direction:row;gap:var(--s2);flex:1 1 100%;overflow-x:auto}
331
+ .wiz-step{padding:8px;flex:none;width:auto}
332
+ .wiz-step .t{display:none}
333
+ .wiz-step.on .t{display:block}
334
+ .wiz-step.on .t span{display:none}
335
+ .wiz-foot{border-top:0;padding-top:0;flex-direction:row;align-items:center;
336
+ flex:1 1 100%}
337
+ .wiz-main{padding:var(--s5) var(--s4)}
338
+ }
339
+
340
+ pre.code{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--r-sm);
341
+ padding:13px 15px;font-family:var(--mono);font-size:11.5px;color:var(--text-2);
342
+ overflow-x:auto;margin:0;line-height:1.85;white-space:pre-wrap;word-break:break-all}
343
+ pre.code b{color:var(--ready-t);font-weight:400}
344
+ pre.code .c{color:var(--text-3)}
345
+ .copyrow{display:flex;gap:var(--s2);align-items:center}
346
+
347
+ .guide{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s5);max-width:820px}
348
+ .guide section{display:flex;flex-direction:column;gap:var(--s3)}
349
+ .guide h2{font-size:17px;display:flex;align-items:center;gap:10px}
350
+ .guide h2 .num{width:22px;height:22px;border-radius:7px;background:var(--surface-2);
351
+ color:var(--accent);display:grid;place-items:center;font-size:11px;font-family:var(--mono);flex:none}
352
+ .guide p{color:var(--text-2);font-size:13px;line-height:1.72}
353
+ .guide ul{margin:0;padding-left:18px;color:var(--text-2);font-size:13px;line-height:1.8}
354
+ .guide .card .pad{display:flex;flex-direction:column;gap:var(--s3)}
355
+
356
+ .skel{background:linear-gradient(90deg,var(--surface-2) 25%,var(--surface-3) 37%,var(--surface-2) 63%);
357
+ background-size:400% 100%;border-radius:var(--r-xs);height:11px}
358
+ @media (prefers-reduced-motion:no-preference){
359
+ .skel{animation:shimmer 1.4s ease infinite}
360
+ @keyframes shimmer{0%{background-position:100% 0}100%{background-position:0 0}}
361
+ }
362
+
363
+ /* ---------- capacity horizon ----------
364
+ Not a .chart: it is a burn-down of *future* windows, so each bar is a bucket and
365
+ the interesting mark is the step where a window resets, not the trend line. */
366
+ .hz{display:flex;align-items:flex-end;gap:3px;height:118px}
367
+ .hz div{flex:1;background:var(--cool);border-radius:3px 3px 0 0;min-height:4px;
368
+ opacity:.62;transition:opacity .14s var(--ease),height .3s var(--ease)}
369
+ .hz div:hover{opacity:1}
370
+ .hz div.step{background:var(--ready);opacity:.85}
371
+ .hzx{display:flex;gap:3px;margin-top:6px}
372
+ .hzx span{flex:1;text-align:center;font-size:9.5px;color:var(--text-3)}
373
+ .hznote{display:flex;gap:var(--s4);flex-wrap:wrap;margin-top:var(--s3);
374
+ font-size:11.5px;color:var(--text-3)}
375
+ .hznote b{color:var(--text);font-weight:500}
376
+
377
+ /* ---------- table cell helpers ---------- */
378
+ .spark{display:inline-block;vertical-align:middle}
379
+ .aid{display:flex;flex-direction:column;gap:2px;min-width:0}
380
+ .aid .nm{color:var(--text);font-weight:550;display:flex;align-items:center;gap:6px}
381
+ .aid .sub{font-size:10.5px;color:var(--text-3);font-family:var(--mono)}
382
+ .acts{display:flex;gap:5px;justify-content:flex-end;flex-wrap:nowrap}
383
+ .addr{font-size:11.5px;color:var(--text-3)}
384
+ .tog{width:34px;height:19px;border-radius:99px;background:var(--surface-3);
385
+ border:1px solid var(--border-strong);position:relative;flex:none;padding:0;
386
+ transition:background .16s var(--ease),border-color .16s var(--ease)}
387
+ .tog i{position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;
388
+ background:var(--text-3);transition:transform .16s var(--ease),background .16s var(--ease)}
389
+ .tog.on{background:var(--ready-w);border-color:var(--ready)}
390
+ .tog.on i{transform:translateX(15px);background:var(--ready)}
391
+
392
+ /* ---------- form details ----------
393
+ label.f stacks, so anything inline becomes its own row. The caption line needs
394
+ to be one element or the "optional" tag lands under the label. */
395
+ label.f .t,.f .t{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap}
396
+ label.f .t em{font-style:normal;text-transform:none;letter-spacing:normal;
397
+ font-weight:400;color:var(--text-3);font-size:10.5px}
398
+ .tog:disabled{opacity:.38;cursor:default}
399
+ /* A disabled control dispatches no pointer events, so its tooltip has to hang
400
+ on a wrapper with a box of its own. */
401
+ .held{display:inline-block;cursor:help}
402
+
403
+ /* A modal is narrower than a view, so its grids need their own breakpoints or
404
+ every field stacks and the dialog turns into a scroll. */
405
+ /* ---------- Advanced disclosure ----------
406
+ The add-account form is essentials above, admin fields folded away below. The
407
+ summary has to look like a control rather than a heading, or nobody opens it. */
408
+ .adv{border-top:1px solid var(--border);padding-top:var(--s3)}
409
+ .adv>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:6px;
410
+ font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;
411
+ color:var(--text-3);user-select:none}
412
+ .adv>summary::-webkit-details-marker{display:none}
413
+ .adv>summary::before{content:"+";font-family:var(--mono);font-size:13px;color:var(--accent)}
414
+ .adv[open]>summary::before{content:"\2212"}
415
+ .adv>summary:hover{color:var(--text)}
416
+ .adv>summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:3px}
417
+ .adv[open]>summary{margin-bottom:var(--s3)}
418
+
419
+ .modal{width:min(580px,100%)}
420
+ .modal .grid.g2{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}
421
+ .modal .grid.g3{grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}
422
+
423
+ /* ============================================================================
424
+ Categorical series palette
425
+ ---------------------------------------------------------------------------
426
+ The four state colours above are RESERVED: ready/cooling/critical mean health
427
+ and cache means the cache, so none of them may stand in for "account number 4".
428
+ Series identity therefore needs its own ramp, and this is it — eight fixed
429
+ slots, assigned in order and never cycled.
430
+
431
+ Validated with the data-viz validator against THIS console's surfaces, not a
432
+ generic white/black:
433
+ dark (#111419): lightness, chroma, CVD, normal-vision and 3:1 contrast all pass
434
+ (worst adjacent ΔE 8.4 protan, 19.3 normal)
435
+ light (#FFFFFF): passes, with three slots under 3:1 — which is why every chart
436
+ on the Usage screen ships a totals table beside it. That table
437
+ is the relief the contrast warning obliges, not a nicety.
438
+ Do not re-step these by eye. Re-run the validator.
439
+ ========================================================================= */
440
+ :root{
441
+ --series-1:#3987e5; --series-2:#d95926; --series-3:#199e70; --series-4:#c98500;
442
+ --series-5:#d55181; --series-6:#008300; --series-7:#9085e9; --series-8:#e66767;
443
+ --series-other:#616B79;
444
+ }
445
+ :root[data-theme="light"]{
446
+ --series-1:#2a78d6; --series-2:#eb6834; --series-3:#1baf7a; --series-4:#eda100;
447
+ --series-5:#e87ba4; --series-6:#008300; --series-7:#4a3aa7; --series-8:#e34948;
448
+ --series-other:#8892A0;
449
+ }
450
+
451
+ /* ---------- chart chrome ---------- */
452
+ .plot{position:relative}
453
+ .chart{width:100%;height:auto;display:block;overflow:visible}
454
+ .chart text{fill:var(--text-3);font-family:var(--mono);font-size:9px}
455
+ .chart .grid-line{stroke:var(--border);stroke-width:1;shape-rendering:crispEdges}
456
+ .chart .axis{stroke:var(--border-strong);stroke-width:1;shape-rendering:crispEdges}
457
+ /* The gap between stacked segments is surface-coloured space, never a stroke. */
458
+ .chart .seg{stroke:var(--surface);stroke-width:2;stroke-linejoin:miter}
459
+ .chart .ln{fill:none;stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
460
+ .chart .dot{stroke:var(--surface);stroke-width:2}
461
+ .chart .hit{fill:transparent;cursor:crosshair}
462
+ .chart .cross{stroke:var(--border-strong);stroke-width:1;pointer-events:none}
463
+
464
+ .legend{display:flex;gap:var(--s4);flex-wrap:wrap;margin-top:var(--s3);font-size:11px;
465
+ color:var(--text-2)}
466
+ .legend span{display:flex;align-items:center;gap:6px}
467
+ .legend i{width:9px;height:9px;border-radius:3px;flex:none}
468
+
469
+ /* Tooltips enhance; they never gate a value — the totals table carries them all. */
470
+ .tip{position:absolute;pointer-events:none;z-index:20;background:var(--surface-2);
471
+ border:1px solid var(--border-strong);border-radius:var(--r-sm);padding:8px 10px;
472
+ font-size:11px;color:var(--text-2);white-space:nowrap;
473
+ box-shadow:0 6px 20px rgba(0,0,0,.28)}
474
+ .tip b{color:var(--text);font-weight:600;display:block;margin-bottom:4px;font-size:11.5px}
475
+ .tip .tr{display:flex;align-items:center;gap:7px;justify-content:space-between;gap:14px}
476
+ .tip .tr i{width:8px;height:8px;border-radius:2px;flex:none}
477
+ .tip .tr .nm{display:flex;align-items:center;gap:6px}
478
+ .tip .tr .v{font-family:var(--mono);color:var(--text)}
479
+
480
+ .filters{display:flex;gap:var(--s3);align-items:center;flex-wrap:wrap;
481
+ padding-bottom:var(--s4)}
482
+ .seg-ctl{display:inline-flex;border:1px solid var(--border);border-radius:var(--r-sm);
483
+ overflow:hidden;background:var(--surface)}
484
+ .seg-ctl button{padding:6px 12px;font-size:12px;color:var(--text-3);
485
+ border-right:1px solid var(--border);transition:background .14s var(--ease),color .14s var(--ease)}
486
+ .seg-ctl button:last-child{border-right:0}
487
+ .seg-ctl button:hover{background:var(--surface-2);color:var(--text)}
488
+ .seg-ctl button.on{background:var(--accent-soft);color:var(--accent);font-weight:600}
489
+ /* Refetch holds the previous render rather than flashing a skeleton. */
490
+ .stale{opacity:.45;transition:opacity .18s var(--ease)}
491
+
492
+ /* ============================================================================
493
+ The mark
494
+ ---------------------------------------------------------------------------
495
+ A sealed, layered pot: dum biryani, and a pool of accounts, in one shape. The
496
+ vessel takes currentColor so it sits on any surface; the lid and the layers
497
+ are the accent. Drawn in strokes at a 32 viewBox because it has to survive a
498
+ 16px favicon — an emoji did not, and neither did anything with more detail.
499
+ ========================================================================= */
500
+ .mark{width:26px;height:26px;flex:none;color:var(--text-2)}
501
+ .brand .mark{color:var(--text-2)}
502
+ .mark-lockup{display:flex;align-items:center;gap:9px}
503
+ .mark-lockup b{font-size:15px;font-weight:650;letter-spacing:-.02em}
504
+ .mark-lockup .mark{width:28px;height:28px}
505
+
506
+ /* ============================================================================
507
+ Landing page
508
+ ---------------------------------------------------------------------------
509
+ The only thing an unauthenticated visitor sees, so it has to say what this is
510
+ before it asks for anything. Two columns on a desktop — the pitch, and the one
511
+ field that gets you in — stacking on a phone with the sign-in card first,
512
+ because someone on a phone already knows what they came for.
513
+ ========================================================================= */
514
+ .lp{min-height:100vh;display:flex;flex-direction:column;
515
+ background:
516
+ radial-gradient(900px 420px at 82% -8%, var(--accent-soft), transparent 62%),
517
+ radial-gradient(700px 380px at 8% 108%, var(--surface-2), transparent 60%),
518
+ var(--bg)}
519
+ .lp-top{display:flex;align-items:center;justify-content:space-between;gap:var(--s4);
520
+ padding:var(--s5) var(--s6);flex-wrap:wrap}
521
+ .lp-addr{font-size:11.5px;color:var(--text-3)}
522
+ .lp-addr:empty{display:none}
523
+
524
+ .lp-main{flex:1;display:grid;grid-template-columns:minmax(0,1.15fr) minmax(340px,.85fr);
525
+ gap:var(--s7);align-items:center;
526
+ padding:var(--s5) var(--s6) var(--s7);max-width:1180px;width:100%;margin:0 auto}
527
+ @media(max-width:940px){
528
+ .lp-main{grid-template-columns:minmax(0,1fr);gap:var(--s6);align-items:start}
529
+ .lp-auth{order:-1}
530
+ }
531
+
532
+ .lp-eyebrow{display:inline-block;font-size:11px;font-weight:650;letter-spacing:.1em;
533
+ text-transform:uppercase;color:var(--accent);
534
+ background:var(--accent-soft);border-radius:99px;padding:5px 11px;margin-bottom:var(--s4)}
535
+ .lp-hero h1{font-size:clamp(28px,4.4vw,42px);line-height:1.12;letter-spacing:-.03em;
536
+ margin-bottom:var(--s4)}
537
+ .lp-lede{color:var(--text-2);font-size:15px;line-height:1.65;max-width:52ch}
538
+
539
+ .lp-points{list-style:none;margin:var(--s6) 0 0;padding:0;
540
+ display:flex;flex-direction:column;gap:var(--s4)}
541
+ .lp-points li{display:flex;gap:13px;align-items:flex-start}
542
+ .lp-points svg{width:19px;height:19px;flex:none;color:var(--accent);margin-top:2px}
543
+ .lp-points b{display:block;font-size:13.5px;font-weight:600;margin-bottom:3px}
544
+ .lp-points span{color:var(--text-3);font-size:12.5px;line-height:1.6;display:block}
545
+
546
+ .lp-auth .card{background:var(--surface)}
547
+ .lp-auth h2{font-size:17px}
548
+ .lp-sub{color:var(--text-3);font-size:12.5px;margin-top:5px;line-height:1.55}
549
+ .lp-go{width:100%;justify-content:center;padding:10px}
550
+
551
+ .lp-help{border-top:1px solid var(--border);padding-top:var(--s3);margin-top:var(--s1)}
552
+ .lp-help summary{cursor:pointer;font-size:12px;color:var(--text-3);
553
+ list-style:none;display:flex;align-items:center;gap:6px}
554
+ .lp-help summary::-webkit-details-marker{display:none}
555
+ .lp-help summary::before{content:"+";font-family:var(--mono);color:var(--accent)}
556
+ .lp-help[open] summary::before{content:"−"}
557
+ .lp-help summary:hover{color:var(--text)}
558
+ .lp-help p{color:var(--text-3);font-size:12px;line-height:1.65;margin:var(--s3) 0}
559
+
560
+ .lp-foot{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;
561
+ padding:var(--s4) var(--s6) var(--s5);border-top:1px solid var(--border);
562
+ font-size:11.5px;color:var(--text-3)}
563
+ .lp-dot{opacity:.5}
564
+ @media(max-width:640px){.lp-top,.lp-main,.lp-foot{padding-left:var(--s4);padding-right:var(--s4)}}
565
+ .lp-endpoint{margin-top:var(--s6);max-width:52ch}
566
+ .lp-endpoint-label{display:block;font-size:10.5px;font-weight:650;letter-spacing:.09em;
567
+ text-transform:uppercase;color:var(--text-3);margin-bottom:var(--s2)}
568
+ .lp-endpoint .code{background:var(--surface);font-size:11.5px;line-height:1.9}
569
+
570
+ /* ---- token source picker ---------------------------------------------------
571
+ Three ways a subscription account gets its token. A segmented control rather
572
+ than a dropdown because the choice carries a trade-off per option, and a
573
+ trade-off you have to open a menu to read is one nobody reads. */
574
+ .seg{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:var(--s2)}
575
+ .segb{text-align:left;border:1px solid var(--border);border-radius:var(--r-sm);
576
+ background:var(--surface-2);color:var(--text-2);padding:9px 11px;cursor:pointer;
577
+ font:inherit;font-size:12px;font-weight:600;transition:border-color .12s var(--ease)}
578
+ .segb:hover{border-color:var(--border-strong);color:var(--text)}
579
+ .segb.on{border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
580
+ .segt{display:block;font-weight:400;font-size:10.5px;color:var(--text-3);margin-top:3px}
581
+ .segb.on .segt{color:var(--accent)}
582
+
583
+ /* One found credentials file. The path is the point — picking the wrong profile
584
+ is the failure this whole panel exists to prevent — so it leads, in mono. */
585
+ .pick{display:flex;gap:var(--s2);align-items:flex-start;padding:9px 11px;cursor:pointer;
586
+ border:1px solid var(--border);border-radius:var(--r-sm);background:var(--surface-2)}
587
+ .pick:hover{border-color:var(--border-strong)}
588
+ /* Never full width: these sit inside form blocks whose inputs stretch, and a
589
+ checkbox stretched to the row pushes its own label off the end. */
590
+ .pick input{margin-top:3px;accent-color:var(--accent);width:auto;flex:none}
591
+ .pickb{display:flex;flex-direction:column;gap:3px;min-width:0}
592
+ .pickt{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-family:var(--mono);
593
+ font-size:11.5px;color:var(--text);word-break:break-all}
594
+ .picks{font-size:11px;color:var(--text-3);line-height:1.5}
595
+
596
+ /* ---- Efficiency ----------------------------------------------------------- */
597
+ /* A verdict that needs acting on. Colour only, never colour alone: every one of
598
+ these sits beside the sentence saying what is wrong. */
599
+ .warn{color:var(--crit-t)}
600
+
601
+ /* The pace track carries a second mark — where spending *should* be by now — so
602
+ unlike the headroom meters it has to position a child and must not clip it.
603
+ Scope names are account ids rather than "req"/"in"/"out", so the label column
604
+ is wider than the 20px those get. */
605
+ .pace .track{position:relative;overflow:visible}
606
+ .pace .k{width:auto;min-width:72px;font-size:10.5px}
607
+ .pace .v{width:48px}
608
+ /* The target mark. Taller than the track so it reads as a threshold rather than
609
+ as part of the fill. */
610
+ .pace .track b{position:absolute;top:-3px;bottom:-3px;border-radius:1px}