@oharness/server 0.9.1 → 0.9.3

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.
@@ -958,15 +958,13 @@ function showAccountUsage(body) {
958
958
  title.appendChild(el("span", "muted small",
959
959
  usageCredits(rows.reduce((sum, entry) => sum + (entry.spentMicro || 0), 0))));
960
960
  panel.appendChild(title);
961
- for (const entry of rows) {
962
- const row = el("div", "row-card");
963
- const line = el("div", "row-head");
964
- line.appendChild(el("span", "row-title", entry.key));
965
- line.appendChild(el("span", "muted small usage-amount", usageCredits(entry.spentMicro || 0)));
966
- row.appendChild(line);
967
- row.appendChild(el("div", "muted small", usageCalls(entry.calls || 0)));
968
- panel.appendChild(row);
969
- }
961
+ const groupTotal = rows.reduce((sum, entry) => sum + (entry.spentMicro || 0), 0);
962
+ panel.appendChild(usageScrollList(rows, (entry) => usageModelRow(
963
+ entry.key,
964
+ usageCredits(entry.spentMicro || 0),
965
+ groupTotal ? (entry.spentMicro || 0) / groupTotal : 0,
966
+ usageSeriesFacts(entry),
967
+ )));
970
968
  }
971
969
  } else {
972
970
  panel.appendChild(el("p", "muted", t(kind === "all" ? "usage.none" : "usage.kindNone")));
@@ -984,8 +982,6 @@ function showAccountUsage(body) {
984
982
  panelBody.appendChild(panel);
985
983
  }
986
984
 
987
- /** How many sessions the usage page lists before "View more". */
988
- const USAGE_RECENT = 10;
989
985
  /** Sessions per request on the sessions page. */
990
986
  const USAGE_SESSIONS_PAGE = 50;
991
987
 
@@ -1022,11 +1018,81 @@ function usageSessionRow(entry) {
1022
1018
  return row;
1023
1019
  }
1024
1020
 
1021
+ /**
1022
+ * One model: its name with what it cost at the top right, then a bar for its
1023
+ * share of the list's spend with what it moved written across it. The bar is
1024
+ * what makes a list of forty readable — the two that carried the bill stand
1025
+ * out before a number is read.
1026
+ */
1027
+ function usageModelRow(name, amount, share, details) {
1028
+ const row = el("div", "row-card usage-model");
1029
+ const line = el("div", "row-head");
1030
+ line.appendChild(el("span", "row-title", name));
1031
+ line.appendChild(el("span", "muted small usage-amount", amount));
1032
+ row.appendChild(line);
1033
+ const track = el("div", "usage-share");
1034
+ const fill = el("span", "usage-share-fill");
1035
+ // A sliver for anything that cost something, so it does not read as zero.
1036
+ fill.style.width = (share > 0 ? Math.max(share * 100, 0.8) : 0).toFixed(2) + "%";
1037
+ track.appendChild(fill);
1038
+ const pct = share >= 0.995 ? "100%" : share >= 0.01 ? Math.round(share * 100) + "%" : share > 0 ? "<1%" : "0%";
1039
+ const label = el("span", "usage-share-label");
1040
+ label.appendChild(el("span", "usage-model-pct", pct));
1041
+ if (details.length) label.appendChild(el("span", "usage-share-facts", details.join(" · ")));
1042
+ track.appendChild(label);
1043
+ row.appendChild(track);
1044
+ return row;
1045
+ }
1046
+
1047
+ /** What an account row moved: its tokens, or the media it made. */
1048
+ function usageSeriesFacts(entry) {
1049
+ const facts = [];
1050
+ const tokens = (entry.inputTokens || 0) + (entry.outputTokens || 0);
1051
+ if (tokens) facts.push(t("usage.tokens", { n: formatTokens(tokens) }));
1052
+ const media = entry.media || {};
1053
+ if (media.images) facts.push(t("usage.images", { n: media.images.toLocaleString(LOCALE) }));
1054
+ if (media.videoSeconds) facts.push(t("usage.videoSeconds", { n: usageSeconds(media.videoSeconds) }));
1055
+ if (media.audioSeconds) facts.push(t("usage.audioSeconds", { n: usageSeconds(media.audioSeconds) }));
1056
+ if (media.characters) facts.push(t("usage.characters", { n: media.characters.toLocaleString(LOCALE) }));
1057
+ return facts;
1058
+ }
1059
+
1060
+ /** How many rows a model list draws at a time. */
1061
+ const USAGE_LIST_PAGE = 20;
1062
+
1063
+ /**
1064
+ * A list of models in a box of its own height.
1065
+ *
1066
+ * A heavy month on OpenRouter is a hundred models, and drawn in full they
1067
+ * pushed the sessions and the scope note a dozen screens down. The box keeps
1068
+ * the page's shape and scrolls on its own; rows are drawn a page at a time as
1069
+ * its bottom comes near, because most visits read the top few.
1070
+ */
1071
+ function usageScrollList(entries, render) {
1072
+ const box = el("div", "usage-list");
1073
+ let shown = 0;
1074
+ function more() {
1075
+ for (const entry of entries.slice(shown, shown + USAGE_LIST_PAGE)) box.appendChild(render(entry));
1076
+ shown = Math.min(entries.length, shown + USAGE_LIST_PAGE);
1077
+ if (shown >= entries.length) box.removeEventListener("scroll", near);
1078
+ }
1079
+ // Read off the scroll position rather than an observer on a sentinel: a
1080
+ // sentinel that stays in view after a page lands never re-enters it, and
1081
+ // the list stopped at the second page.
1082
+ function near() {
1083
+ if (box.scrollTop + box.clientHeight >= box.scrollHeight - 160) more();
1084
+ }
1085
+ more();
1086
+ if (shown < entries.length) box.addEventListener("scroll", near, { passive: true });
1087
+ return box;
1088
+ }
1089
+
1025
1090
  /** What the sessions page is searching for, kept while the page is open. */
1026
1091
  let usageSessionsQuery = "";
1027
1092
 
1028
1093
  /**
1029
- * Every session that spent something on this machine, newest first.
1094
+ * Every session that spent something on this machine, newest first — the
1095
+ * Sessions page in the sidebar.
1030
1096
  *
1031
1097
  * Read a page at a time and drawn as the list is scrolled, because a machine
1032
1098
  * used daily has thousands and the first screen is the one almost always
@@ -1038,13 +1104,6 @@ async function showUsageSessions() {
1038
1104
  const panel = el("div", "panel usage");
1039
1105
  const head = el("div", "row-head");
1040
1106
  head.appendChild(el("h2", null, t("usage.allSessions")));
1041
- const back = el("button", "ghost", t("usage.backToUsage"));
1042
- back.type = "button";
1043
- back.addEventListener("click", () => {
1044
- setNav("usage");
1045
- void showUsage();
1046
- });
1047
- head.appendChild(back);
1048
1107
  panel.appendChild(head);
1049
1108
 
1050
1109
  const search = el("input", "art-search");
@@ -1078,7 +1137,7 @@ async function showUsageSessions() {
1078
1137
  const body = await getJson("/usage/sessions?limit=" + USAGE_SESSIONS_PAGE + "&offset=" + offset +
1079
1138
  (usageSessionsQuery ? "&q=" + encodeURIComponent(usageSessionsQuery) : ""));
1080
1139
  loading = false;
1081
- if (asked !== generation || shownPanel !== "usage-sessions") return;
1140
+ if (asked !== generation || shownPanel !== "nav-sessions") return;
1082
1141
  if (body.error) {
1083
1142
  foot.textContent = body.error;
1084
1143
  return;
@@ -1109,7 +1168,7 @@ async function showUsageSessions() {
1109
1168
  const sentinel = el("div", "usage-sessions-sentinel");
1110
1169
  panel.appendChild(sentinel);
1111
1170
  const observer = new IntersectionObserver((entries) => {
1112
- if (shownPanel !== "usage-sessions") return void observer.disconnect();
1171
+ if (shownPanel !== "nav-sessions") return void observer.disconnect();
1113
1172
  if (entries.some((entry) => entry.isIntersecting)) void load();
1114
1173
  }, { root: panelView, rootMargin: "0px 0px 400px 0px" });
1115
1174
  observer.observe(sentinel);
@@ -1215,38 +1274,11 @@ async function showUsage() {
1215
1274
 
1216
1275
  if (models.length) {
1217
1276
  panel.appendChild(el("h3", "section-title", t("usage.byModel")));
1218
- for (const entry of models) {
1219
- const row = el("div", "row-card");
1220
- const line = el("div", "row-head");
1221
- line.appendChild(el("span", "row-title", entry.model));
1222
- line.appendChild(el("span", "muted small usage-amount", "$" + (entry.costUsd || 0).toFixed(2)));
1223
- row.appendChild(line);
1224
- const bits = [
1225
- t("usage.tokens", { n: formatTokens(moved(entry)) }),
1226
- overSessions(entry.sessions),
1227
- ];
1228
- const share = cachePct(entry);
1229
- if (share !== null) bits.push(t("usage.cached", { pct: share }));
1230
- row.appendChild(el("div", "muted small", bits.join(" · ")));
1231
- panel.appendChild(row);
1232
- }
1233
- }
1234
-
1235
- const sessions = body.sessions || [];
1236
- if (sessions.length) {
1237
- // The latest few, and a way to all of them: a page of rows here pushed
1238
- // the scope note below off the screen and still was not every session.
1239
- const title = el("div", "usage-section-head");
1240
- title.appendChild(el("h3", "section-title", t("usage.bySession")));
1241
- const more = el("button", "ghost", t("usage.moreSessions"));
1242
- more.type = "button";
1243
- more.addEventListener("click", () => {
1244
- setNav("usage-sessions");
1245
- void showUsageSessions();
1246
- });
1247
- title.appendChild(more);
1248
- panel.appendChild(title);
1249
- for (const entry of sessions.slice(0, USAGE_RECENT)) panel.appendChild(usageSessionRow(entry));
1277
+ const spent = totals.costUsd || 0;
1278
+ panel.appendChild(usageScrollList(models, (entry) => {
1279
+ return usageModelRow(entry.model, "$" + (entry.costUsd || 0).toFixed(2), spent ? (entry.costUsd || 0) / spent : 0,
1280
+ [t("usage.tokens", { n: formatTokens(moved(entry)) })]);
1281
+ }));
1250
1282
  }
1251
1283
 
1252
1284
  if (!models.length) panel.appendChild(el("p", "muted", t("usage.none")));
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA4vC3B,CAAC"}
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../../../src/page/client/usage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA4xC3B,CAAC"}
@@ -32,32 +32,98 @@ code,pre,.mono{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,
32
32
 
33
33
  /* sidebar */
34
34
  aside{
35
- width:236px;flex:0 0 236px;background:var(--panel);
36
- display:flex;flex-direction:column;overflow:hidden;transition:flex-basis .18s,width .18s;
37
- }
38
- /* Folded away on a desktop. Zero width rather than display:none, for the
39
- slide, and so nothing inside has to be rebuilt when it comes back. */
35
+ --side-w:296px;width:var(--side-w);flex:0 0 var(--side-w);
36
+ display:flex;flex-direction:column;align-items:flex-end;overflow:hidden;
37
+ }
38
+ /* Folding, the sidebar narrows while its contents keep their width, pinned
39
+ to its right edge, so they slide out to the left behind the frame rather
40
+ than squeezing; the tab strip's inset grows by as much as the sidebar
41
+ loses, so the first tab glides to its place beside the toggle. Only once a
42
+ hand has touched the toggle: a page opened folded starts folded. */
43
+ #side-top,#side-body{width:var(--side-w);flex-shrink:0}
44
+ body.nav-anim>aside{transition:width .26s cubic-bezier(.2,0,0,1),flex-basis .26s cubic-bezier(.2,0,0,1)}
45
+ body.nav-anim #tabs{transition:padding-left .26s cubic-bezier(.2,0,0,1)}
46
+ body.nav-anim .wordmark{transition:opacity .14s}
47
+ body.nav-collapsed .wordmark{opacity:0}
48
+ @media (prefers-reduced-motion:reduce){body.nav-anim>aside,body.nav-anim #tabs,body.nav-anim .wordmark{transition:none}}
49
+ /*
50
+ * A frame with the panel as a card inside it.
51
+ *
52
+ * The top bar and the rail are one L in the window's own colour, --frame, and
53
+ * the panel and the page sit inside it. It takes a third colour: a top bar
54
+ * over the sidebar in either of the other two runs into the surface below it
55
+ * in that colour — a grey one boxed the white rail in, a white one ran on
56
+ * into the page. The panel is a card set into the frame, rounded all round
57
+ * and clear of the page by a margin. The traffic lights sit on the frame, one
58
+ * colour, where the rail alone is too narrow to hold them.
59
+ */
60
+ #side-top{height:36px;flex:0 0 36px;background:var(--frame);display:flex;align-items:center;overflow:hidden}
61
+ /* Just past the toggle, which ends at 40px on the web and at 108px beside
62
+ the traffic lights, and fixed there, so the mark stays in the corner when
63
+ the sidebar folds and only the name goes with it. */
64
+ .brand{position:fixed;z-index:30;top:0;left:48px;height:36px;display:flex;align-items:center;gap:8px;white-space:nowrap;pointer-events:none}
65
+ body.inset-titlebar .brand{left:116px}
66
+ body.inset-titlebar #side-top{-webkit-app-region:drag}
67
+ #side-body{flex:1 1 auto;display:flex;min-height:0;background:var(--frame)}
68
+ /*
69
+ * Two columns: a rail of the app's places, and the panel of the work.
70
+ *
71
+ * The rail is the frame and the panel a card in it, so the two read as
72
+ * different surfaces without a line between them.
73
+ */
74
+ #rail{
75
+ width:52px;flex:0 0 52px;background:var(--frame);display:flex;flex-direction:column;
76
+ align-items:center;padding:8px 0 10px;gap:6px;overflow:hidden;
77
+ }
78
+ #rail #nav{padding:0;gap:4px;align-items:center}
79
+ #rail .nav-item{width:36px;height:36px;padding:0;justify-content:center;border-radius:9px;color:var(--dim)}
80
+ #rail .nav-item:hover{color:var(--fg)}
81
+ #rail .nav-item.active{color:var(--fg)}
82
+ /* Drawn at 18px on the 16 grid, the shared 1.5 stroke would render at 1.7px
83
+ and read heavier than every other icon on the page; 1.2 lands near 1.35. */
84
+ #rail .nav-item .ico{width:18px;height:18px;stroke-width:1.2}
85
+ /* The panel's icons follow at their own 14-16px, so the rail and the panel
86
+ beside it draw at one weight. Matched on the attribute, so the mark keeps its 2.5. */
87
+ #side-panel svg[stroke-width="1.5"]{stroke-width:1.25}
88
+ #rail .nav-item>span{display:none}
89
+ .rail-tip{
90
+ position:fixed;z-index:80;transform:translateY(-50%);padding:4px 9px;border-radius:7px;
91
+ background:var(--accent);color:var(--bg);font-size:12px;line-height:1.4;white-space:nowrap;
92
+ pointer-events:none;opacity:0;transition:opacity .08s;
93
+ }
94
+ .rail-tip.on{opacity:1}
95
+ #side-panel{
96
+ width:236px;flex:0 0 236px;background:var(--panel);border-radius:12px;margin:0 8px 8px 0;
97
+ display:flex;flex-direction:column;overflow:hidden;
98
+ }
99
+ #side-new{padding:0 8px 2px;display:flex;flex-direction:column}
100
+ /* Folded away on a desktop, the whole sidebar goes, rail and all, and the
101
+ page has the window: keeping the rail left a strip of chrome and pushed the
102
+ tabs a rail's width plus the traffic lights in from the edge. */
40
103
  body.nav-collapsed>aside{width:0;flex-basis:0}
41
- #nav-collapse,#nav-expand{
42
- width:28px;height:28px;border:0;border-radius:7px;background:transparent;color:var(--dim);
43
- cursor:pointer;padding:0;display:grid;place-items:center;flex:0 0 auto;-webkit-app-region:no-drag;
44
- }
45
- #nav-collapse{margin-left:auto}
46
- #nav-collapse:hover,#nav-expand:hover{background:var(--hover);color:var(--fg)}
47
- #nav-collapse svg,#nav-expand svg{width:16px;height:16px;display:block;shape-rendering:geometricPrecision}
48
- #nav-expand{display:none;position:absolute;z-index:5;top:7px;left:8px}
49
- body.nav-collapsed #nav-expand{display:grid}
50
- body.nav-collapsed #tabs{padding-left:44px}
51
- /* The traffic lights sit where the sidebar's corner was; step past them. */
52
- body.inset-titlebar.nav-collapsed #nav-expand{left:80px}
53
- body.inset-titlebar.nav-collapsed #tabs{padding-left:116px;-webkit-app-region:drag}
54
- body.inset-titlebar.nav-collapsed #tabs>*{-webkit-app-region:no-drag}
55
- aside header{padding:15px 14px 8px;display:flex;gap:8px;align-items:center}
56
- /* The desktop shell hides the title bar, so on macOS the traffic lights float
57
- * over this corner. The wordmark moves below them, and the strip they sit in
58
- * becomes the window's drag handle — without it a title-bar-less window has
59
- * nothing to grab. */
60
- body.inset-titlebar aside header{padding-top:38px;-webkit-app-region:drag}
104
+ /* In the top bar, at the same spot folded or not, so the click that folded the
105
+ sidebar can bring it straight back. On a desktop it steps past the traffic
106
+ lights (14-74px, set in desktop/main.js). */
107
+ #nav-toggle{
108
+ position:fixed;z-index:30;top:4px;left:12px;width:28px;height:28px;border:0;border-radius:7px;
109
+ background:transparent;color:var(--dim);cursor:pointer;padding:0;
110
+ display:grid;place-items:center;-webkit-app-region:no-drag;
111
+ }
112
+ #nav-toggle:hover{background:var(--hover);color:var(--fg)}
113
+ #nav-toggle svg{width:16px;height:16px;display:block;shape-rendering:geometricPrecision}
114
+ body.inset-titlebar #nav-toggle{left:80px}
115
+ body.shelf-full #nav-toggle{display:none}
116
+ /* The window has no title bar, so the strip's empty stretch is its handle.
117
+ Folded, the strip is the whole top bar and starts past the mark beside the
118
+ toggle: 66px on the web, 134px beside the traffic lights. */
119
+ body.inset-titlebar #tabs{-webkit-app-region:drag}
120
+ body.inset-titlebar #tabs>*{-webkit-app-region:no-drag}
121
+ body.nav-collapsed #tabs{padding-left:74px}
122
+ body.inset-titlebar.nav-collapsed #tabs{padding-left:142px}
123
+ /* The name is in the top bar on a wide screen, so the panel's header is only
124
+ the phone drawer's, where it carries the name and the close button. */
125
+ aside header{display:none;padding:8px 14px;min-height:44px;gap:8px;align-items:center}
126
+ #side-panel{padding-top:8px}
61
127
  .mark{width:18px;height:18px;color:var(--fg);opacity:.85;flex:0 0 auto}
62
128
  .mark svg{width:18px;height:18px;display:block}
63
129
  /* Slightly tightened and a touch heavier than body text, which is what makes
@@ -518,31 +584,43 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
518
584
  * is in front. Spanning both left a band of empty chrome above the drawer that
519
585
  * belonged to neither.
520
586
  *
521
- * The strip is the shelf the tabs stand on, in the sidebar's grey, and the
522
- * front tab is cut from the page under it — the shape every browser uses.
523
- * It used to be the other way round: a transparent strip on the white
524
- * transcript with the front tab as a grey pill, so the row read as part of
525
- * the conversation rather than as the chrome above it. No bottom padding,
526
- * or a grey line would sit between the front tab and the page it is part of.
587
+ * The strip is the top of the frame, at #side-top's height, so one bar runs
588
+ * across the window, and the front tab is cut from the page under it — the
589
+ * shape every browser uses. No bottom padding, or a line of frame would sit
590
+ * between the front tab and the page it is part of; no left padding, so the
591
+ * first tab's edge is the page's.
527
592
  */
528
593
  #tabs{
529
- display:flex;align-items:center;gap:2px;padding:6px 12px 0;overflow-x:auto;flex:0 0 auto;
530
- background:var(--panel);
594
+ display:flex;align-items:flex-end;gap:2px;height:36px;padding:0 12px 0 0;overflow-x:auto;flex:0 0 auto;
595
+ background:var(--frame);
531
596
  }
532
597
  #tabs:empty{display:none}
598
+ /* A scrollbar here takes its height out of the strip and lifts every tab off
599
+ the page, and a hovered tab's buttons were enough to bring one on. Tabs
600
+ shrink first, as a browser's do, and past that the strip still scrolls by
601
+ wheel or trackpad, only without the bar. */
602
+ #tabs{scrollbar-width:none}
603
+ #tabs::-webkit-scrollbar{display:none}
533
604
  .tab{
534
- display:flex;align-items:center;gap:7px;max-width:230px;height:30px;padding:0 6px 0 10px;
605
+ display:flex;align-items:center;gap:7px;max-width:230px;min-width:72px;height:30px;padding:0 14px;
535
606
  border:0;border-radius:8px 8px 0 0;background:transparent;color:var(--dim);
536
- font:inherit;font-size:12.5px;cursor:pointer;flex:0 0 auto;
537
- }
538
- .tab:hover{background:var(--hover);color:var(--fg)}
607
+ font:inherit;font-size:12.5px;cursor:pointer;flex:0 1 auto;
608
+ }
609
+ /* The tab under the pointer or in front keeps its width, so its buttons fit
610
+ and it is the others that give way. */
611
+ .tab:hover,.tab:focus-within,.tab.on{flex-shrink:0}
612
+ /* Hovered, a tab lifts halfway toward the page it would become, the way a
613
+ browser's do, rather than sinking into a darker grey the front tab never
614
+ is. Mixed from --bg so the dark theme lifts the same way. */
615
+ .tab:hover{background:color-mix(in srgb,var(--bg) 60%,transparent);color:var(--fg)}
539
616
  .tab.on{background:var(--bg);color:var(--fg)}
540
- .tab .label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
617
+ .tab-page .ico-sm{width:14px;height:14px;opacity:.75}
618
+ .tab .label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
541
619
  /* Which project a tab is in, ahead of its title. Set quieter than the title,
542
620
  which is still what identifies it. */
543
621
  .tab .where{
544
622
  max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
545
- font-size:11px;opacity:.6;flex:0 0 auto;
623
+ font-size:11px;opacity:.6;flex:0 1 auto;min-width:0;
546
624
  }
547
625
  .tab .where::after{content:"/";padding-left:5px;opacity:.5}
548
626
  .tab .dot{width:6px;height:6px;border-radius:50%;background:var(--brand);flex:0 0 auto}
@@ -556,6 +634,10 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
556
634
  transparent — a transparent button still holds its space. A running
557
635
  browser keeps its button, because its dot is what says so. */
558
636
  .tab:hover .x,.tab:focus-within .x{display:grid}
637
+ /* Even padding while the title is alone, as a label is set; the right side
638
+ gives some back when a button comes in, so the button sits near the edge
639
+ and the tab grows by less. */
640
+ .tab:hover,.tab:focus-within,.tab:has(.tab-browser.running){padding-right:6px}
559
641
  /* The tab's browser button's size. Much smaller and the 1.5px strokes thin
560
642
  below a pixel and blur; the full 16px outweighs the tab's own text. */
561
643
  .tab .x svg{width:14px;height:14px}
@@ -571,7 +653,7 @@ details.provider>summary .vmark.filled,.pp-item .vmark.filled{padding:0}
571
653
  .tab:hover .tab-browser,.tab.on .tab-browser,.tab-browser.running{opacity:.62}
572
654
  .tab-browser:hover,.tab-browser.on{opacity:1;background:var(--sel)}
573
655
  .tab-browser svg{width:14px;height:14px}
574
- .tab-browser i{display:none;position:absolute;right:2px;top:2px;width:5px;height:5px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 1.5px var(--panel)}
656
+ .tab-browser i{display:none;position:absolute;right:2px;top:2px;width:5px;height:5px;border-radius:50%;background:var(--ok);box-shadow:0 0 0 1.5px var(--frame)}
575
657
  .tab.on .tab-browser i{box-shadow:0 0 0 1.5px var(--bg)}
576
658
  .tab-browser.running i{display:block}
577
659
  #tab-add{
@@ -1472,6 +1554,17 @@ details.setting>summary:focus-visible{outline:none;background:var(--hover)}
1472
1554
  .usage-kind.on{background:var(--accent);color:var(--bg);box-shadow:none}
1473
1555
  .usage-kind.on .usage-kind-label,.usage-kind.on .usage-kind-sub,.usage-kind.on.empty .usage-kind-value{color:var(--bg);opacity:.72}
1474
1556
  .usage-counted{margin:0 0 10px}
1557
+ /* A model list: its own height, its own scroll, no frame drawn round it. */
1558
+ .usage-list{max-height:440px;overflow-y:auto;overscroll-behavior:contain}
1559
+ .usage-list>.row-card:last-of-type{margin-bottom:0}
1560
+ /* The share bar: a filled track, no outline, thick enough to carry its facts. */
1561
+ .usage-model{padding:12px 14px}
1562
+ .usage-model .row-head{margin-bottom:8px}
1563
+ .usage-share{position:relative;height:28px;border-radius:6px;background:var(--field);overflow:hidden}
1564
+ .usage-share-fill{position:absolute;inset:0 auto 0 0;border-radius:6px;background:color-mix(in srgb,var(--brand) 34%,transparent)}
1565
+ .usage-share-label{position:relative;display:flex;align-items:center;gap:12px;height:100%;padding:0 10px;min-width:0;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}
1566
+ .usage-model-pct{flex:none;color:var(--fg);font-weight:600}
1567
+ .usage-share-facts{min-width:0;overflow:hidden;text-overflow:ellipsis;color:var(--fg);opacity:.72}
1475
1568
  /* Amounts in one column at the right edge, so a list is compared by glancing down it. */
1476
1569
  .usage .row-head .usage-amount{margin-left:auto;flex:none;font-variant-numeric:tabular-nums}
1477
1570
  .usage .row-head .row-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -2902,19 +2995,31 @@ dialog::backdrop{background:rgba(0,0,0,.55)}
2902
2995
  */
2903
2996
  @media (max-width:760px){
2904
2997
  body>aside{
2905
- position:fixed;inset:0 auto 0 0;z-index:20;width:min(280px,84vw);flex-basis:auto;
2998
+ position:fixed;inset:0 auto 0 0;z-index:20;width:min(332px,90vw);flex-basis:auto;
2906
2999
  transform:translateX(-100%);transition:transform .2s;
2907
3000
  padding-bottom:env(safe-area-inset-bottom);
2908
3001
  }
2909
3002
  body>aside.open{transform:none;box-shadow:0 0 0 1px #0000000a,0 12px 40px #00000040}
2910
3003
  body.nav-open #scrim{display:block}
2911
3004
  #nav-close{display:grid}
2912
- #nav-collapse,body.nav-collapsed #nav-expand{display:none}
2913
- body.nav-collapsed>aside{width:min(280px,84vw);flex-basis:auto}
2914
- body.nav-collapsed #tabs{padding-left:12px}
3005
+ #nav-toggle,#side-top{display:none}
3006
+ aside header{display:flex}
3007
+ #side-panel{padding-top:0}
3008
+ #side-panel{border-radius:0;margin:0}
3009
+ body.nav-collapsed>aside{width:min(332px,90vw);flex-basis:auto}
3010
+ #side-body{width:auto;align-self:stretch}
3011
+ #side-panel,body.nav-collapsed #side-panel{flex:1 1 auto;width:auto;min-width:0;margin:0}
2915
3012
  #burger{display:grid}
2916
- #top{padding:6px 10px}
2917
- #top:has(#changes-btn:not([hidden])){padding:6px 10px}
3013
+ /* One bar, not two: the burger sits at the head of the tab strip, in the
3014
+ frame's colour, rather than on a white row of its own above it. #top
3015
+ keeps its row only while it has the changes button to show. */
3016
+ #top{padding:0;min-height:0}
3017
+ #top:has(#changes-btn:not([hidden])){padding:6px 10px 6px 52px;background:var(--frame)}
3018
+ #burger{position:fixed;z-index:15;top:0;left:4px;width:40px;height:36px;margin:0;border-radius:8px}
3019
+ #tabs,body.nav-collapsed #tabs{padding-left:44px}
3020
+ /* The front tab has the width it needs: its buttons show on a phone, where
3021
+ there is no hover to wait for, and they left the title as "Unt…". */
3022
+ .tab.on{max-width:none}
2918
3023
 
2919
3024
  /* Both panes are the screen while open, and each gets a way out of its own:
2920
3025
  the top bar with the changes button is underneath them. */
@@ -1 +1 @@
1
- {"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;EAC7B,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAm+FV,CAAC"}
1
+ {"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,0EAA0E;AAC1E,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;EAC7B,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4kGV,CAAC"}