@materialwebunofficial/antigravity-themes 1.0.5 → 1.0.13

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.
package/src/theme-host.js CHANGED
@@ -11,7 +11,10 @@ function agyThemeHost(bundle) {
11
11
  // Bump when this file's code changes. A same-version host only takes the
12
12
  // new bundle; an older one would keep running its old code through
13
13
  // update(), so it is disposed and replaced (same task: no flash).
14
- const HOST_VERSION = 20; // also bump when a module it starts (context-card, turn-tokens, loading-/progress-indicator, code-tools) changes: the host starts them once
14
+ const HOST_VERSION = 31; // also bump when a module it starts (context-card, turn-tokens, loading-/progress-indicator, quota rings, code-tools) changes: the host starts them once
15
+ // Sidecar frames (plugin://) have their own host, frame-host.js: this one
16
+ // is the window's
17
+ if (location.protocol === 'plugin:') return null;
15
18
  const existing = window.__agyThemeHost;
16
19
  if (existing?.version === HOST_VERSION) return existing.update(bundle);
17
20
  existing?.dispose();
@@ -36,14 +39,22 @@ function agyThemeHost(bundle) {
36
39
  // Fonts come as bytes and are registered with the FontFace API: no
37
40
  // network fetch, no CSP font-src, and no 300 KB data URI in the CSS text.
38
41
  const loadedFonts = new Set();
42
+ const fontBytes = new Map(); // base64 -> ArrayBuffer, decoded once (the frames ask for copies)
43
+ function fontBuffer(f) {
44
+ let buffer = fontBytes.get(f.data);
45
+ if (!buffer) {
46
+ buffer = Uint8Array.from(atob(f.data), (c) => c.charCodeAt(0)).buffer;
47
+ fontBytes.set(f.data, buffer);
48
+ }
49
+ return buffer;
50
+ }
39
51
  function ensureFonts() {
40
52
  for (const f of bundle.fonts?.[current.theme] ?? []) {
41
53
  const key = `${f.family}|${f.weight}|${f.stretch}`;
42
54
  if (loadedFonts.has(key) || !document.fonts) continue;
43
55
  loadedFonts.add(key);
44
56
  try {
45
- const bytes = Uint8Array.from(atob(f.data), (c) => c.charCodeAt(0));
46
- const face = new FontFace(f.family, bytes.buffer, { weight: f.weight, stretch: f.stretch, style: 'normal', display: 'swap' });
57
+ const face = new FontFace(f.family, fontBuffer(f), { weight: f.weight, stretch: f.stretch, style: 'normal', display: 'swap' });
47
58
  document.fonts.add(face);
48
59
  face.load().catch(() => {});
49
60
  } catch {
@@ -74,6 +85,7 @@ function agyThemeHost(bundle) {
74
85
  watchMode();
75
86
  if (!syncMode()) return;
76
87
  announceTheme();
88
+ syncFrames();
77
89
  // The app re-sends its own (mis-parsed) overlay color on theme changes
78
90
  requestAnimationFrame(() => syncTitleBarOverlay());
79
91
  setTimeout(syncTitleBarOverlay, 300);
@@ -81,6 +93,57 @@ function agyThemeHost(bundle) {
81
93
  // The vendored md3e-web components cache their colors; the package's own
82
94
  // window event makes them read the tokens again (the app uses no such event)
83
95
  const announceTheme = () => window.dispatchEvent(new Event('theme-change'));
96
+
97
+ // ---- Sidecar frames --------------------------------------------------------
98
+ // A sidecar pane (Swarm) is a cross-origin plugin:// iframe with its own
99
+ // host, frame-host.js. This side tells it the theme, seed and mode: when a
100
+ // frame asks (it does on load) and to every frame when they change. It also
101
+ // repeats the app's own "theme-change" message, the tokens the app computes
102
+ // from this document and gives a sidecar once, on load: after a theme switch
103
+ // they would be the previous theme's.
104
+ const APP_TOKENS = '--background --foreground --primary --primary-foreground --secondary --secondary-foreground --muted --muted-foreground --placeholder --sidebar --sidebar-secondary --sidebar-muted --content --card --card-border --border --accent --code-foreground --error --editor-font-family'.split(' ');
105
+ const currentMode = () => document.documentElement?.getAttribute('data-agy-mode') || 'dark';
106
+ const themeMessage = () => ({ type: 'agy-theme-change', theme: current.theme, seed: current.seed ?? null, key: current.key, mode: currentMode() });
107
+ function tokensMessage() {
108
+ const cs = getComputedStyle(document.documentElement);
109
+ const cssVariables = {};
110
+ for (const name of APP_TOKENS) {
111
+ const value = cs.getPropertyValue(name).trim();
112
+ if (value) cssVariables[name] = value;
113
+ }
114
+ if (!cssVariables['--accent'] && cssVariables['--primary']) cssVariables['--accent'] = cssVariables['--primary'];
115
+ return { type: 'theme-change', payload: { themeType: currentMode(), cssVariables } };
116
+ }
117
+ function syncFrames() {
118
+ if (!document.documentElement || !current) return;
119
+ const frames = document.querySelectorAll('iframe[src^="plugin:"]');
120
+ if (!frames.length) return;
121
+ const theme = themeMessage(), tokens = tokensMessage();
122
+ for (const frame of frames) {
123
+ try {
124
+ frame.contentWindow?.postMessage(theme, '*');
125
+ frame.contentWindow?.postMessage(tokens, '*');
126
+ } catch {
127
+ // a frame between documents: it asks again when it loads
128
+ }
129
+ }
130
+ }
131
+ function onFrameMessage(e) {
132
+ if (e.data?.type !== 'agy-theme-query' || !current) return;
133
+ const frame = e.source;
134
+ try {
135
+ if (!frame || frame === window || frame.parent !== window) return;
136
+ frame.postMessage(themeMessage(), '*');
137
+ const fonts = e.data.fonts === true ? bundle.fonts?.[current.theme] : null;
138
+ if (fonts?.length) {
139
+ const copies = fonts.map((f) => ({ family: f.family, weight: f.weight, stretch: f.stretch, buffer: fontBuffer(f).slice(0) }));
140
+ frame.postMessage({ type: 'agy-theme-fonts', theme: current.theme, fonts: copies }, '*', copies.map((f) => f.buffer));
141
+ }
142
+ } catch {
143
+ // the frame went away
144
+ }
145
+ }
146
+
84
147
  let modeTarget = null;
85
148
  function watchMode() {
86
149
  const target = document.body ?? document.documentElement;
@@ -118,6 +181,7 @@ function agyThemeHost(bundle) {
118
181
  if (style.parentNode !== parent || style !== parent.lastElementChild) parent.appendChild(style);
119
182
  }
120
183
  announceTheme();
184
+ syncFrames();
121
185
  renderMenu();
122
186
  if (appMenuBuilt) buildAppMenu(false);
123
187
  if (current.theme !== 'md3e') closeAppMenu();
@@ -195,7 +259,9 @@ function agyThemeHost(bundle) {
195
259
  display: flex; align-items: center; padding: 0 8px; border: 0; border-radius: 6px;
196
260
  background: transparent; color: var(--muted-foreground, #999); font: inherit; cursor: default;
197
261
  }
198
- .trigger:hover, .trigger[aria-expanded="true"] { background: var(--secondary, #333); color: var(--foreground, #eee); }
262
+ /* hover and open: a state layer of the text color, so it shows on any
263
+ theme's surface (Nordic's --secondary is its card color) */
264
+ .trigger:hover, .trigger[aria-expanded="true"] { background: color-mix(in srgb, var(--foreground, #eee) 10%, transparent); color: var(--foreground, #eee); }
199
265
  .menu {
200
266
  position: absolute; top: calc(100% + 6px); left: 0; min-width: 230px; padding: 6px;
201
267
  border: 1px solid var(--border, #333); border-radius: 12px;
@@ -208,7 +274,7 @@ function agyThemeHost(bundle) {
208
274
  border-radius: 8px; font: inherit; color: inherit; cursor: default; outline: none;
209
275
  }
210
276
  [role="menuitemradio"]:hover, [role="menuitemradio"]:focus-visible,
211
- .menu-sub-trigger:hover, .menu-sub-wrapper:hover .menu-sub-trigger { background: var(--secondary, #333); }
277
+ .menu-sub-trigger:hover, .menu-sub-wrapper:hover .menu-sub-trigger { background: color-mix(in srgb, var(--foreground, #eee) 10%, transparent); }
212
278
  .swatch { width: 12px; height: 12px; border-radius: 999px; box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.2); }
213
279
  .name { flex: 1; }
214
280
  .check { visibility: hidden; }
@@ -503,15 +569,12 @@ function agyThemeHost(bundle) {
503
569
  const forced = force === true;
504
570
  if (!appMenu || (!forced && appMenu.open)) return;
505
571
  if (appMenuBuilt && !forced) {
506
- window.__buildAppMenuHitCache = (window.__buildAppMenuHitCache || 0) + 1;
507
572
  for (const item of appMenu.querySelectorAll('md-menu-item[value^="theme:"]')) {
508
573
  const val = item.getAttribute('value') || '';
509
574
  item.selected = (val === `theme:${current?.key}`);
510
575
  }
511
- fixMenuStyle(appMenu);
512
576
  return;
513
577
  }
514
- window.__buildAppMenuFullBuild = (window.__buildAppMenuFullBuild || 0) + 1;
515
578
  appMenus = readAppMenus();
516
579
  if (!appMenus.length && !forced) return;
517
580
  const items = appMenus.map((m, i) => {
@@ -655,7 +718,7 @@ function agyThemeHost(bundle) {
655
718
  if (!(bundle.ripple ?? []).includes(current?.theme) || e.button !== 0 || reducedMotion?.matches) return;
656
719
  if (hostEl && e.composedPath().includes(hostEl)) return;
657
720
  const target = e.target instanceof Element ? e.target.closest(RIPPLE_TARGETS) : null;
658
- if (!target || target.isContentEditable) return;
721
+ if (!target || target.isContentEditable || target.matches('[role="menu"] [role^="menuitem"]') || target.closest('[role="menu"]')) return;
659
722
  const x = e.clientX, y = e.clientY, start = performance.now();
660
723
  let released = false, onRelease = null;
661
724
  const release = () => {
@@ -667,14 +730,12 @@ function agyThemeHost(bundle) {
667
730
  window.addEventListener('pointerup', release, true);
668
731
  window.addEventListener('pointercancel', release, true);
669
732
  requestAnimationFrame(() => {
733
+ if (!target.isConnected) return;
670
734
  const box = target.getBoundingClientRect();
671
735
  if (box.width < 8 || box.height < 8) return;
672
736
  const cs = getComputedStyle(target);
673
- // md-menu items draw their shape 4px inside the row (::before): ripple there
674
- const shape = target.matches('[role="menu"] [role^="menuitem"]') ? getComputedStyle(target, '::before') : null;
675
- const inset = shape?.content && shape.content !== 'none' ? parseFloat(shape.left) || 0 : 0;
676
- const r = { left: box.left + inset, top: box.top, width: box.width - inset * 2, height: box.height };
677
- const radius = inset ? shape.borderRadius : cs.borderRadius;
737
+ const r = { left: box.left, top: box.top, width: box.width, height: box.height };
738
+ const radius = cs.borderRadius;
678
739
  if (!rippleLayer?.isConnected) {
679
740
  rippleLayer = document.createElement('div');
680
741
  rippleLayer.id = 'agy-ripple-layer';
@@ -683,7 +744,7 @@ function agyThemeHost(bundle) {
683
744
  }
684
745
  const clip = document.createElement('div');
685
746
  clip.style.cssText = `position:absolute;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;border-radius:${radius};overflow:hidden;`;
686
- const endR = Math.hypot(r.width, r.height) / 2 + 10;
747
+ const endR = Math.hypot(r.width, r.height) / 2 + 16;
687
748
  const startScale = (Math.max(r.width, r.height) * 0.3) / endR;
688
749
  const cx = r.width / 2, cy = r.height / 2;
689
750
  const ink = document.createElement('div');
@@ -705,6 +766,21 @@ function agyThemeHost(bundle) {
705
766
  };
706
767
  if (released) fadeOut();
707
768
  else onRelease = fadeOut;
769
+ // The target may morph while the ripple plays (corners closing on
770
+ // press, a selection springing round): the clip follows its box and
771
+ // corners each frame until the ripple is gone, so the ink never shows
772
+ // outside the shape.
773
+ const follow = () => {
774
+ if (!clip.isConnected || !target.isConnected) return;
775
+ const b = target.getBoundingClientRect();
776
+ clip.style.left = b.left + 'px';
777
+ clip.style.top = b.top + 'px';
778
+ clip.style.width = b.width + 'px';
779
+ clip.style.height = b.height + 'px';
780
+ clip.style.borderRadius = getComputedStyle(target).borderRadius;
781
+ requestAnimationFrame(follow);
782
+ };
783
+ requestAnimationFrame(follow);
708
784
  });
709
785
  }
710
786
 
@@ -769,8 +845,6 @@ function agyThemeHost(bundle) {
769
845
  function onPointerDown(e) {
770
846
  startRipple(e);
771
847
  if (hostEl && !e.composedPath().includes(hostEl)) closeMenu();
772
- const transcript = document.querySelector('[data-testid="autoscroll-viewport"]');
773
- if (transcript && !transcript.classList.contains('no-focus-agent-input')) transcript.classList.add('no-focus-agent-input');
774
848
  }
775
849
  function cycleTheme() {
776
850
  const keys = [...options.keys()];
@@ -789,47 +863,7 @@ function agyThemeHost(bundle) {
789
863
  window.addEventListener('focus', syncTitleBarOverlay);
790
864
  document.fonts?.addEventListener('loadingdone', position);
791
865
 
792
- // ---- Virtual list layout guard --------------------------------------------
793
- // Antigravity virtualizes the sidebar and auxiliary panels with TanStack
794
- // Virtual. On mounting long lists (e.g. 299 diffs in Review or 95 sidebar
795
- // items), TanStack Virtual synchronously reads offsetHeight on every
796
- // [data-index] element while dirtying container styles between each item.
797
- // In a large conversation, each forced reflow costs 80-100ms, causing a 2s+
798
- // freeze upon entry. This guard serves known fixed heights or cached values
799
- // for virtual rows, preventing layout thrashing while letting ResizeObserver
800
- // handle real dynamic size changes asynchronously on idle frames.
801
- const origOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight')?.get;
802
- const virtualHeightCache = new WeakMap();
803
- if (origOffsetHeight) {
804
- Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
805
- get() {
806
- if (this.hasAttribute('data-index')) {
807
- const cached = virtualHeightCache.get(this);
808
- if (cached !== undefined) return cached;
809
- if (this.firstElementChild?.classList.contains('sticky')) {
810
- virtualHeightCache.set(this, 33);
811
- return 33;
812
- }
813
- if (this.classList.contains('bg-sidebar') || this.querySelector('h2')) {
814
- virtualHeightCache.set(this, 24);
815
- return 24;
816
- }
817
- if (this.querySelector('.agy-sidebar-row') || this.classList.contains('pb-[1px]')) {
818
- virtualHeightCache.set(this, 33);
819
- return 33;
820
- }
821
- if (!this.textContent.trim()) {
822
- virtualHeightCache.set(this, 16);
823
- return 16;
824
- }
825
- }
826
- const val = origOffsetHeight.call(this);
827
- if (this.hasAttribute('data-index')) virtualHeightCache.set(this, val);
828
- return val;
829
- },
830
- configurable: true,
831
- });
832
- }
866
+ window.addEventListener('message', onFrameMessage);
833
867
 
834
868
  // Start the Overview panel's Context card; every theme gets it
835
869
  const contextCard = typeof agyContextCard === 'function' ? agyContextCard() : null;
@@ -839,6 +873,8 @@ function agyThemeHost(bundle) {
839
873
  const loadingIndicator = typeof agyLoadingIndicator === 'function' ? agyLoadingIndicator() : null;
840
874
  // ...and wavy progress bars on running background work
841
875
  const progressIndicators = typeof agyProgressIndicators === 'function' ? agyProgressIndicators() : null;
876
+ // ...and the M3 Expressive gap in the usage rings
877
+ const quotaRings = typeof agyQuotaRings === 'function' ? agyQuotaRings() : null;
842
878
  // ...and Wrap lines / Line numbers in every code block's header
843
879
  const codeTools = typeof agyCodeTools === 'function' ? agyCodeTools() : null;
844
880
 
@@ -848,6 +884,7 @@ function agyThemeHost(bundle) {
848
884
  window.removeEventListener('keydown', onKeyDown, true);
849
885
  window.removeEventListener('resize', position);
850
886
  window.removeEventListener('focus', syncTitleBarOverlay);
887
+ window.removeEventListener('message', onFrameMessage);
851
888
  document.fonts?.removeEventListener('loadingdone', position);
852
889
  hostEl?.remove();
853
890
  document.documentElement?.removeAttribute('data-agy-appmenu');
@@ -856,24 +893,18 @@ function agyThemeHost(bundle) {
856
893
  turnTokens?.dispose();
857
894
  loadingIndicator?.dispose();
858
895
  progressIndicators?.dispose();
896
+ quotaRings?.dispose();
859
897
  codeTools?.dispose();
860
- if (origOffsetHeight) {
861
- Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
862
- get: origOffsetHeight,
863
- configurable: true,
864
- });
865
- }
866
898
  document.querySelectorAll('[data-agy-ghost]').forEach((g) => g.remove());
867
899
  document.getElementById('agy-theme-css')?.remove();
868
900
  document.documentElement?.removeAttribute('data-agy-theme');
869
901
  document.documentElement?.removeAttribute('data-agy-seed');
870
902
  modeObserver.disconnect();
871
903
  document.documentElement?.removeAttribute('data-agy-mode');
872
- document.querySelectorAll('[data-testid="autoscroll-viewport"].no-focus-agent-input')
873
- .forEach((el) => el.classList.remove('no-focus-agent-input'));
874
904
  syncTitleBarOverlay();
875
905
  delete window.__agyThemeHost;
876
906
  delete window.agyTheme;
907
+ delete window.__agyPageScript; // the page script may run here again
877
908
  }
878
909
 
879
910
  const api = {
@@ -102,7 +102,7 @@ function agyTurnTokens() {
102
102
  }
103
103
  }
104
104
 
105
- const timer = setInterval(scan, 2000);
105
+ const timer = setInterval(scan, 10000);
106
106
  scan();
107
107
 
108
108
  return {
@@ -0,0 +1,211 @@
1
+ /* ==========================================================================
2
+ AGY "Matte" — sidecar frames (the Swarm pane)
3
+
4
+ Loaded by frame-host.js inside the plugin:// frame, after matte-tokens.css.
5
+ Swarm's own design is already Matte's: monochrome, quiet, no outlines on
6
+ the rows. So this is mostly the token map; the few rules below bring it in
7
+ line with the window: raised things carry the 1px hairline, radii follow
8
+ the panel's, and nothing loops.
9
+ ========================================================================== */
10
+
11
+ :root[data-agy-theme="matte"] {
12
+ color-scheme: dark;
13
+
14
+ /* The app's tokens. It sends the window's values (--background there is
15
+ the window, not this panel), and the sidecar SDK writes them inline on
16
+ <html>: hence !important. This frame sits in the panel. */
17
+ --background: var(--m-panel) !important;
18
+ --foreground: var(--m-text) !important;
19
+ --primary: var(--m-primary) !important;
20
+ --primary-foreground: var(--m-primary-ink) !important;
21
+ --secondary: var(--m-high) !important;
22
+ --secondary-foreground: var(--m-text-2) !important;
23
+ --muted: var(--m-raised) !important;
24
+ --muted-foreground: var(--m-text-3) !important;
25
+ --placeholder: var(--m-text-3) !important;
26
+ --card: var(--m-raised) !important;
27
+ --card-border: var(--m-line-2) !important;
28
+ --border: var(--m-line) !important;
29
+ --accent: var(--m-primary) !important;
30
+ --error: var(--m-error) !important;
31
+
32
+ /* Swarm's own tokens */
33
+ --sw-bg: var(--m-panel);
34
+ --sw-card: var(--m-raised);
35
+ --sw-secondary: var(--m-high);
36
+ --sw-muted: var(--m-inset);
37
+ --sw-fg: var(--m-text);
38
+ --sw-fg-2: var(--m-text-2);
39
+ --sw-fg-muted: var(--m-text-3);
40
+ --sw-fg-faint: var(--m-text-3);
41
+ --sw-border: var(--m-line-2);
42
+ --sw-card-border: var(--m-line);
43
+ --sw-primary: var(--m-primary);
44
+ --sw-primary-fg: var(--m-primary-ink);
45
+ --sw-error: var(--m-error);
46
+ --sw-success: var(--m-success);
47
+ --sw-warning: var(--m-warning);
48
+ --sw-hover: var(--m-hover);
49
+ --sw-press: var(--m-select);
50
+ --sw-ring: var(--m-line-3);
51
+ --sw-line: var(--m-line);
52
+ --sw-fill: var(--m-select);
53
+ --sw-field: var(--m-raised);
54
+ --r-sm: 5px;
55
+ --r-md: 8px;
56
+ --r-lg: 10px;
57
+ }
58
+ :root[data-agy-theme="matte"][data-agy-mode="light"] {
59
+ color-scheme: light;
60
+ }
61
+
62
+ :root[data-agy-theme="matte"] {
63
+ /* Type stays Swarm's own (Google Sans Flex, the app's face): Matte leaves
64
+ the window's UI font alone too */
65
+ /* Scrollbars: Swarm styles a slim thumb (::-webkit-scrollbar), but its
66
+ scrollbar-width: thin on every element switches Chromium to the native
67
+ bar, arrow buttons and all, and those rules never apply */
68
+ &,
69
+ * {
70
+ scrollbar-width: auto;
71
+ scrollbar-color: auto;
72
+ }
73
+ body {
74
+ background: var(--sw-bg) !important; /* the SDK's inline body background is the window's */
75
+ }
76
+ .swarm-title {
77
+ color: var(--m-text-max);
78
+ }
79
+
80
+ /* Nothing loops: a status dot is a dot, the skeleton a still placeholder */
81
+ .live-dot::after {
82
+ content: none;
83
+ }
84
+ .skel {
85
+ animation: none;
86
+ background: var(--m-select);
87
+ }
88
+
89
+ /* Raised things carry the hairline: the fields and the floating button */
90
+ .search,
91
+ .pane .o2-msearch {
92
+ border-color: var(--m-line);
93
+ background: var(--m-raised);
94
+ }
95
+ .search:focus-within,
96
+ .pane .o2-msearch:focus-within {
97
+ border-color: var(--m-line-3);
98
+ background: var(--m-raised);
99
+ }
100
+ .jump {
101
+ border-color: var(--m-line-2);
102
+ background: var(--m-raised);
103
+ box-shadow: 0 6px 18px -8px var(--m-shadow);
104
+ }
105
+ .jump:hover {
106
+ background: var(--m-high);
107
+ }
108
+ .toast {
109
+ border: 1px solid var(--m-line-2);
110
+ background: var(--m-high);
111
+ box-shadow: 0 10px 28px -12px var(--m-shadow-menu);
112
+ color: var(--m-text);
113
+ }
114
+ /* The list: Chromium's customizable select (appearance: base-select)
115
+ draws the picker in the page, so it follows the theme (a raised panel with a hairline, rows 30px) */
116
+ .select select,
117
+ .select select::picker(select) {
118
+ appearance: base-select;
119
+ }
120
+ .select select::picker-icon {
121
+ display: none; /* the wrapper draws the arrow */
122
+ }
123
+ .select select {
124
+ align-items: center; /* a base-select is a flex box: its label would sit at the top */
125
+ }
126
+ .select select::picker(select) {
127
+ margin-block: 4px;
128
+ padding: 4px;
129
+ border: 1px solid var(--m-line-2);
130
+ border-radius: 10px;
131
+ background: var(--m-raised);
132
+ box-shadow: 0 16px 40px -12px var(--m-shadow-menu);
133
+ color: var(--m-text);
134
+ }
135
+ .select select option {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 6px;
139
+ min-height: 30px;
140
+ padding: 0 10px;
141
+ border-radius: 6px;
142
+ background: transparent;
143
+ color: var(--m-text);
144
+ font-size: 13px;
145
+ }
146
+ .select select option:is(:hover, :focus-visible) {
147
+ background: var(--m-hover);
148
+ outline: none;
149
+ }
150
+ .select select option:checked {
151
+ background: var(--m-select);
152
+ color: var(--m-text-max);
153
+ }
154
+ .select select option::checkmark {
155
+ order: 1;
156
+ margin-inline-start: auto;
157
+ font-size: 12px;
158
+ }
159
+ .select select option:not(:checked)::checkmark {
160
+ display: none;
161
+ }
162
+
163
+ /* The one bright thing stays the window's send button: the unread tint
164
+ and the "new" divider are neutral here */
165
+ .feed .msg.unread-by-me {
166
+ background: var(--m-select);
167
+ }
168
+ .divider {
169
+ color: var(--m-text-2);
170
+ }
171
+ .divider::after {
172
+ background: var(--m-line-2);
173
+ }
174
+ .msg-channel {
175
+ background: var(--m-select);
176
+ }
177
+ .msg-channel:hover {
178
+ background: var(--m-high);
179
+ }
180
+
181
+ /* hive.css cards: raised, hairline, the window's 12px */
182
+ .pane .o2 {
183
+ --o2-card: var(--m-raised);
184
+ --o2-border: var(--m-line);
185
+ --o2-info: var(--m-blue);
186
+ }
187
+ .pane .o2-lead,
188
+ .pane .o2-card,
189
+ .pane .o2-goal,
190
+ .pane .o2-run,
191
+ .pane .o2-prog,
192
+ .pane .o2-th,
193
+ .pane .o2-reel {
194
+ border-radius: 12px;
195
+ }
196
+ .pane .o2-rundot {
197
+ box-shadow: none;
198
+ }
199
+ .pane .o2-mrow.bees {
200
+ border-radius: 10px;
201
+ }
202
+
203
+ /* Graph: nodes are raised chips on the panel */
204
+ .graph .node-box {
205
+ fill: var(--m-raised);
206
+ stroke: var(--m-line-2);
207
+ }
208
+ .graph .node:hover .node-box {
209
+ stroke: var(--m-line-3);
210
+ }
211
+ }