@tobq/boardclip 0.0.0-stage → 2.1.108

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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -2
  3. package/assets/boardclip-icon.png +0 -0
  4. package/bin/boardclip.js +43 -0
  5. package/editor-preload.js +71 -0
  6. package/editor.html +220 -0
  7. package/icon.png +0 -0
  8. package/icon@2x.png +0 -0
  9. package/iconTemplate.png +0 -0
  10. package/iconTemplate@2x.png +0 -0
  11. package/index.html +1339 -0
  12. package/lib/appearance.js +183 -0
  13. package/lib/auto-update.js +306 -0
  14. package/lib/backup-worker.js +46 -0
  15. package/lib/backup.js +275 -0
  16. package/lib/blob-field-store.js +275 -0
  17. package/lib/blob-store.js +68 -0
  18. package/lib/build-info.js +89 -0
  19. package/lib/clip-blob-store.js +161 -0
  20. package/lib/clip-revision.js +81 -0
  21. package/lib/clipboard-capture.js +95 -0
  22. package/lib/clipboard-follow.js +98 -0
  23. package/lib/clipboard-model.js +1235 -0
  24. package/lib/cloud-accounts.js +250 -0
  25. package/lib/conflict-model.js +186 -0
  26. package/lib/control-client.js +87 -0
  27. package/lib/control-server.js +149 -0
  28. package/lib/diagnostics.js +121 -0
  29. package/lib/drag-files.js +48 -0
  30. package/lib/ensure-directory.js +56 -0
  31. package/lib/fork-names.js +35 -0
  32. package/lib/fs-probe.js +132 -0
  33. package/lib/history-feed.js +48 -0
  34. package/lib/hmac-auth.js +78 -0
  35. package/lib/macos-hotkey.js +297 -0
  36. package/lib/macos-paste.js +112 -0
  37. package/lib/mcp-core.js +270 -0
  38. package/lib/mcp-installers.js +284 -0
  39. package/lib/mcp-paths.js +99 -0
  40. package/lib/p2p-crypto.js +54 -0
  41. package/lib/p2p-discovery.js +191 -0
  42. package/lib/quick-paste.js +187 -0
  43. package/lib/retention.js +43 -0
  44. package/lib/sync-delta.js +227 -0
  45. package/lib/sync-journal.js +181 -0
  46. package/lib/sync-paths.js +73 -0
  47. package/lib/tailscale.js +80 -0
  48. package/lib/text-blob-store.js +38 -0
  49. package/lib/windows-clipboard.js +213 -0
  50. package/lib/windows-console.js +63 -0
  51. package/lib/windows-dwm.js +183 -0
  52. package/lib/windows-hook-worker.js +236 -0
  53. package/lib/windows-hook.js +91 -0
  54. package/lib/windows-paste.js +185 -0
  55. package/main.js +7540 -0
  56. package/mcp/boardclip-mcp.js +255 -0
  57. package/mcp-approval-preload.js +22 -0
  58. package/mcp-approval.html +218 -0
  59. package/package.json +55 -4
  60. package/preload.js +106 -0
  61. package/site/index.html +1624 -0
  62. package/site/shared/clip-search.js +2330 -0
  63. package/site/shared/clipboard-popup.css +1046 -0
  64. package/site/shared/clipboard-tokens.css +326 -0
  65. package/site/shared/clipboard-ui-core.js +6921 -0
  66. package/site/shared/clipboard-window.css +36 -0
  67. package/site/shared/vendor/cm5/README.md +52 -0
  68. package/site/shared/vendor/cm5/codemirror.css +344 -0
  69. package/site/shared/vendor/cm5/codemirror.js +9884 -0
  70. package/site/shared/vendor/cm5/diff-match-patch.js +2228 -0
  71. package/site/shared/vendor/cm5/merge.css +119 -0
  72. package/site/shared/vendor/cm5/merge.js +1182 -0
  73. package/site/shared/vendor/fonts/README.md +19 -0
  74. package/site/shared/vendor/fonts/material-symbols-rounded.woff2 +0 -0
  75. package/viewer-preload.js +51 -0
  76. package/viewer.html +129 -0
@@ -0,0 +1,1046 @@
1
+ @import url("clipboard-tokens.css");
2
+
3
+ /* The icon font, self-hosted (vendor/fonts/README.md): every app window and the
4
+ website load THIS sheet, so this is its one declaration. No window waits on
5
+ Google Fonts, and font-display: block means a reload never flashes the
6
+ ligature words ("settings", "close") while the font loads. */
7
+ @font-face {
8
+ font-family: "Material Symbols Rounded";
9
+ font-display: block;
10
+ src: url("vendor/fonts/material-symbols-rounded.woff2") format("woff2");
11
+ }
12
+
13
+ /* ONE icon spec: Material Symbols Rounded (the vendored variable font above),
14
+ axes pinned HERE and nowhere else. FILL 1 is a state signal only
15
+ (pinned star, active facet): set --icon-fill: 1 on the icon or a parent. */
16
+ .mi {
17
+ font-family: "Material Symbols Rounded";
18
+ font-size: var(--icon-md);
19
+ font-weight: var(--fw-regular);
20
+ font-style: normal;
21
+ font-variation-settings: "FILL" var(--icon-fill, 0), "wght" 400, "GRAD" 0, "opsz" 20;
22
+ line-height: 1;
23
+ letter-spacing: normal;
24
+ text-transform: none;
25
+ display: inline-block;
26
+ white-space: nowrap;
27
+ word-wrap: normal;
28
+ direction: ltr;
29
+ -webkit-font-smoothing: antialiased;
30
+ }
31
+ .mi.filled { --icon-fill: 1; }
32
+ .mi.sm { font-size: var(--icon-sm); }
33
+ .mi.lg { font-size: var(--icon-lg); }
34
+ .mi.mid { vertical-align: middle; }
35
+
36
+ /* ---------- Shared primitives (every window + the website demo) ---------- */
37
+
38
+ /* ONE motion switch: with prefers-reduced-motion every state transition (they
39
+ all run on the shared --dur, the reveal and the panel fold included) is instant. */
40
+ @media (prefers-reduced-motion: reduce) {
41
+ :root { --dur: 0ms; }
42
+ }
43
+
44
+ /* ONE keyboard focus ring for every BoardClip control: a box-shadow, so no
45
+ outline and no layout shift. Text fields show focus on their own edge. */
46
+ :is(:root[data-theme], .bc-popup) :where(button, [role="button"], a[href], .filter-tag, .seg-btn, .np-btn, .bc-menu-item):focus-visible {
47
+ outline: none; box-shadow: var(--focus-ring);
48
+ }
49
+
50
+ /* ONE floating surface for every panel that floats over content: menus,
51
+ submenus, popovers (a row's keypad / group picker), suggest, dialogs, the
52
+ toast and the Newest pill. Flat in-window, this is the only elevation. */
53
+ .search-suggest, .dialog, .toast, .list-newest, .tag-submenu, .bc-menu { background: var(--menu); border-radius: var(--r-panel); padding: var(--sp-2);
54
+ box-shadow: 0 8px 24px var(--shadow), 0 0 0 1px var(--menu-edge); }
55
+
56
+ /* ONE button family: a fill, never a border. Default / primary / danger /
57
+ quiet, plus .sm (20px text affordance in bars and footers). */
58
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); height: var(--ctl-lg); padding: 0 var(--sp-6);
59
+ border: none; border-radius: var(--r-ctl); background: var(--ctl-fill); color: var(--text); font-family: inherit; font-size: var(--fs-ui);
60
+ font-weight: var(--fw-regular); line-height: var(--lh-ui); white-space: nowrap; cursor: pointer;
61
+ transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
62
+ .btn:hover { background: var(--ctl-fill-hover); }
63
+ .btn.primary { background: var(--accent); color: var(--active-fg); }
64
+ .btn.primary:hover { background: var(--accent-hover); }
65
+ .btn.danger { background: var(--red); color: var(--danger-fg); }
66
+ .btn.danger:hover { background: color-mix(in srgb, var(--red) 86%, var(--text)); }
67
+ .btn.quiet { background: transparent; color: var(--text-dim); }
68
+ .btn.quiet:hover { background: var(--hover); color: var(--text); }
69
+ .btn.sm { height: var(--ctl-sm); padding: 0 var(--sp-3); font-size: var(--fs-meta); }
70
+ /* An accent text action (Undo in a toast, "Did you mean", a pane head's
71
+ Accept, the merge note's "Show them"): a quiet button in the accent TEXT
72
+ colour, the accent tint on hover. */
73
+ .btn.quiet.accent, .btn.quiet.accent:hover { color: var(--accent-text); }
74
+ .btn.quiet.accent:hover { background: var(--accent-bg); }
75
+ /* A default button whose action throws something away (Remove conflict): the
76
+ menu's danger cue on hover / focus, never a second button family. */
77
+ .btn.discard:hover, .btn.discard:focus-visible { background: var(--red-bg); color: var(--red); }
78
+ .btn:disabled, .btn:disabled:hover { background: var(--ctl-fill); color: var(--text-dim); cursor: not-allowed; }
79
+ .btn.quiet:disabled, .btn.quiet:disabled:hover { background: transparent; }
80
+
81
+ /* ONE overline: the small uppercase heading over a SECTION (a settings
82
+ section, the approval prompt's clip card, the demo's note). Window titles
83
+ are NOT overlines (they are .bc-bar-title, sentence case). */
84
+ .overline, .settings-note, .settings-section h3 {
85
+ font-size: var(--fs-meta); font-weight: var(--fw-strong); line-height: var(--lh-ui); letter-spacing: .06em; text-transform: uppercase; color: var(--text-dim);
86
+ }
87
+ /* ONE small label, sentence case: names a group of controls or a pane inside
88
+ a section (an options-panel row, a list in a settings section, a merge
89
+ pane head). Deliberately not an overline: it sits beside
90
+ controls, where an uppercase heading would shout. */
91
+ .bc-label, .opts-facet-label, .settings-list-title, .bc-head-label {
92
+ font-size: var(--fs-meta); font-weight: var(--fw-regular); line-height: var(--lh-ui); color: var(--text-dim);
93
+ }
94
+
95
+ /* ONE scrollbar for every BoardClip surface: the app windows (data-theme on
96
+ <html>) and the demo popup (.bc-popup). The thumb is inset by a transparent
97
+ border clipped to the padding box, so it works on any background. Chromium
98
+ ignores these pseudo-elements on an element that sets scrollbar-width or
99
+ scrollbar-color, so nothing else may set them (except to hide a bar). */
100
+ :is(:root[data-theme], .bc-popup) ::-webkit-scrollbar { width: var(--scrollbar-size); height: var(--scrollbar-size); }
101
+ :is(:root[data-theme], .bc-popup) ::-webkit-scrollbar-track,
102
+ :is(:root[data-theme], .bc-popup) ::-webkit-scrollbar-corner { background: transparent; }
103
+ :is(:root[data-theme], .bc-popup) ::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); background-clip: padding-box; border: 2px solid transparent; border-radius: var(--r-panel); }
104
+ :is(:root[data-theme], .bc-popup) ::-webkit-scrollbar-thumb:hover { background-color: var(--scrollbar-thumb-hover); }
105
+ @supports not selector(::-webkit-scrollbar) {
106
+ :where(:root[data-theme], .bc-popup) * { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) transparent; }
107
+ }
108
+
109
+ /* ONE text-selection colour (typed text, a find match left selected, any
110
+ selectable copy), in every window and the demo. */
111
+ :is(:root[data-theme], .bc-popup) ::selection { background: var(--accent-bg-strong); }
112
+
113
+ /* Filled text fields: a fill, never a border; focus (and a recording shortcut
114
+ field) draws an inset accent edge, which never shifts layout. The popup's
115
+ search field and the clip windows' title / find fields are the flat
116
+ exception (no fill, a hairline underline). */
117
+ .setting-row input:focus, .prompt-input:focus,
118
+ .shortcut-btn:hover, .shortcut-btn.recording { box-shadow: inset 0 0 0 1px var(--accent); }
119
+
120
+ /* ---------- Components ---------- */
121
+
122
+ .main-view { display: flex; flex-direction: column; flex: 1; min-height: 0; height: 100%; }
123
+ .main-view.hidden { display: none; }
124
+ /* TWO tones, the same in every window: the chrome (the popup's header with
125
+ the search, the options panel and the chips; the settings header; every
126
+ window bar and foot) is a band in --surface, the content (the results, the
127
+ settings body, the editor's text, the viewer) sits on --canvas, which the
128
+ window (body, or the demo's frame) paints; the band ends on the --line
129
+ hairline. Under glass --surface is a translucent lift over the one scrim
130
+ (no second blur). */
131
+ .sticky { flex-shrink: 0; background: var(--surface); padding-bottom: var(--sp-2); border-bottom: 1px solid var(--line); z-index: 10; position: relative; }
132
+ /* The header (and the settings header) are NOT -webkit-app-region: drag: that
133
+ swallowed every click and double-click-maximised. Core.attachWindowDrag owns
134
+ them: a click focuses the search, a press-and-move past 4 px moves the window. */
135
+ .sticky header { display: flex; align-items: center; padding: var(--sp-4) var(--edge-ctl) 0 var(--gutter); gap: var(--sp-3); }
136
+ .count { font-size: var(--fs-meta); color: var(--text-dim); flex: 1; }
137
+ /* .close-btn is only a hook now: every close is <span class="mi">close</span>
138
+ inside a plain .icon-btn. */
139
+ .icon-btn { background: none; border: none; border-radius: var(--r-ctl); color: var(--text-dim); cursor: pointer;
140
+ width: var(--ctl-md); height: var(--ctl-md); display: flex; align-items: center; justify-content: center;
141
+ transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); padding: 0; }
142
+ .icon-btn:hover { color: var(--text); background: var(--hover); }
143
+ .icon-btn.danger:hover, .icon-btn.danger:focus-visible { color: var(--red); background: var(--red-bg); }
144
+ .icon-btn:disabled { opacity: .45; cursor: not-allowed; }
145
+ .icon-btn:disabled:hover { color: var(--text-dim); background: none; }
146
+ .icon-btn .mi { font-size: var(--icon-md); }
147
+ /* ONE reveal: a control that slides in and out sits in a 0fr -> 1fr width
148
+ track (Forge's QueueStrip collapser), so the text beside it takes the full
149
+ width while it is hidden and the reveal animates instead of snapping. Two
150
+ belts, both unconditional: `min-width: 0` on the track's child (the
151
+ 0fr track is minmax(auto, 0fr), and auto = the child's min-content width,
152
+ i.e. no collapse) and a padding-less inner wrapper (a border box cannot be
153
+ crushed below its own padding). Open = .open on the track (or a consumer's
154
+ own selector, e.g. a row hover). Closed controls go invisible once the
155
+ track has shut, so Tab never lands on them. The space before a revealed
156
+ control is its own (--reveal-gap, a margin INSIDE the track), never the
157
+ container's flex gap: a gap stays between closed, 0 px tracks. Used by the
158
+ search field's buttons and, next, the row actions. Markup:
159
+ <span class="bc-reveal"><span class="bc-reveal-inner">...</span></span> */
160
+ .bc-reveal { display: grid; grid-template-columns: 0fr; flex-shrink: 0; overflow: clip; overflow-clip-margin: var(--sp-2);
161
+ transition: grid-template-columns var(--dur) var(--ease); }
162
+ .bc-reveal > * { min-width: 0; }
163
+ /* visibility interpolates as "visible" for the whole transition when either end is
164
+ visible: shown at once on open, hidden only once the track has shut.
165
+ content-visibility does the same (allow-discrete) and also skips laying out a
166
+ closed reveal's contents: every list row carries two closed reveals, and
167
+ laying out their buttons cost ~8 ms of each keystroke's rebuild (measured,
168
+ owner scale). Every open selector sets both. */
169
+ .bc-reveal-inner { display: flex; align-items: center; gap: var(--sp-1); padding: 0; visibility: hidden; content-visibility: hidden;
170
+ transition: visibility var(--dur) var(--ease), content-visibility var(--dur) var(--ease) allow-discrete; }
171
+ .bc-reveal-inner > :first-child { margin-inline-start: var(--reveal-gap, 0); }
172
+ .bc-reveal.open { grid-template-columns: 1fr; }
173
+ .bc-reveal.open > .bc-reveal-inner { visibility: visible; content-visibility: visible; }
174
+
175
+ /* The search field is FLAT: no fill, no border, a hairline underline (an inset
176
+ shadow, so focus never shifts layout) that turns accent while focused. */
177
+ .search-row { display: flex; align-items: center; gap: 0; --reveal-gap: var(--sp-1); margin: var(--sp-3) var(--gutter) var(--sp-2); background: transparent; border: none; border-radius: 0;
178
+ box-shadow: inset 0 -1px 0 var(--line); transition: box-shadow var(--dur) var(--ease); }
179
+ /* Focused = ONE state, set by Core.attachSearchBox: the field (or its buttons)
180
+ has the focus AND the window does - the same state that swaps the
181
+ placeholder, so an unfocused popup never shows an accent line beside
182
+ "Click here to search...". */
183
+ .search-row.is-focused { box-shadow: inset 0 -1px 0 var(--accent); }
184
+ .search-field { position: relative; flex: 1; min-width: 0; display: flex; }
185
+ .search { flex: 1; background: transparent; border: none; padding: var(--sp-3) var(--sp-2) var(--sp-3) 0; color: var(--text); font-size: var(--fs-text); outline: none; font-family: inherit; min-width: 0; }
186
+ .search::placeholder { color: var(--text-dim); }
187
+ /* The live-highlight input paints its text transparent (the mirror shows it);
188
+ ::placeholder inherits that fill, so it must set its own. */
189
+ .search-live::placeholder { -webkit-text-fill-color: var(--text-dim); }
190
+ .search-row .icon-btn { flex-shrink: 0; }
191
+ /* The last in-field button's glyph (tune, in the tools reveal: open whenever
192
+ any in-field button is) ends on the gutter with the header's gear and the
193
+ selection bar's close, the underline under it. */
194
+ .search-row > .bc-reveal[data-reveal="tools"] { transition: grid-template-columns var(--dur) var(--ease), margin-right var(--dur) var(--ease); }
195
+ .search-row > .bc-reveal[data-reveal="tools"].open { margin-right: calc(var(--edge-ctl) - var(--gutter)); } /* closed, the field keeps the whole row */
196
+ /* A toggled in-field control: Regex (the search field's and the find bar's),
197
+ match case, Recent order, the options panel open (or a filter it owns in
198
+ the query). */
199
+ .rx-btn.active, .case-btn.active, .sort-btn.active, .opts-btn.active { color: var(--accent); background: var(--accent-bg); }
200
+ .rx-btn.active:hover, .case-btn.active:hover, .sort-btn.active:hover, .opts-btn.active:hover { color: var(--accent); background: var(--accent-bg-strong); }
201
+
202
+ /* Live query-syntax highlight: a colored backdrop mirror UNDER a transparent input. The
203
+ two must share identical text metrics (font/size/padding/line-height) so glyphs align. */
204
+ .search-hl { position: absolute; inset: 0; padding: var(--sp-3) var(--sp-2) var(--sp-3) 0;
205
+ font-size: var(--fs-text); font-family: inherit; line-height: normal; color: var(--text);
206
+ white-space: pre; overflow: hidden; pointer-events: none; z-index: 0; border: none;
207
+ display: flex; align-items: center; }
208
+ .search-live { position: relative; z-index: 1; background: transparent; -webkit-text-fill-color: transparent; caret-color: var(--text); }
209
+ .qh-prefix { color: var(--accent-text); font-weight: var(--fw-strong); }
210
+ .qh-neg { color: var(--red); font-weight: var(--fw-strong); }
211
+ .qh-value { color: var(--text); }
212
+ .qh-regex { color: var(--green); }
213
+ .qh-quote { color: var(--text-dim); }
214
+ /* OR and the brackets of a group: the language's operators, like a key. */
215
+ .qh-op { color: var(--accent-text); font-weight: var(--fw-strong); }
216
+ /* Exactly the invalid key or value (Search.validateQuery), nothing around it. */
217
+ .qh-unknown { color: var(--red); text-decoration: underline wavy var(--red); text-decoration-thickness: 1px; text-underline-offset: 3px; }
218
+ .qh-ws { color: transparent; }
219
+ /* The top suggestion's rest, after the typed text (Tab or Right at the end takes it). */
220
+ .qh-ghost { color: var(--text-dim); opacity: .7; }
221
+
222
+ /* ONE hint line under the field (an invalid token, a broken regex): a rows
223
+ fold, 0 px and nothing reserved while there is nothing to say. */
224
+ .search-hint { display: grid; grid-template-rows: 0fr; opacity: 0; margin: 0 var(--gutter);
225
+ transition: grid-template-rows var(--dur) var(--ease), opacity var(--dur) var(--ease); }
226
+ .search-hint.show { grid-template-rows: 1fr; opacity: 1; }
227
+ .search-hint-clip { min-height: 0; overflow: hidden; }
228
+ .search-hint-row { display: flex; align-items: baseline; gap: var(--sp-2); padding: 0 0 var(--sp-2); font-size: var(--fs-meta); line-height: var(--lh-ui); color: var(--text-dim); }
229
+ .search-hint-row > .mi { font-size: var(--icon-sm); color: var(--red); align-self: center; }
230
+ .search-hint-fix { margin-left: var(--sp-1); } /* the shared .btn.quiet.sm.accent */
231
+ .search-hint-fix code { font-family: var(--font-mono); }
232
+
233
+ /* Autocomplete dropdown for the token being typed. */
234
+ /* .search-suggest shares the floating-surface bg/shadow/radius rule (top of file); only its
235
+ positioning + scroll live here. Its rows ARE menu rows (.bc-menu-item): the
236
+ token in mono, its own hint dim at the end. */
237
+ .search-suggest { position: absolute; top: calc(100% + var(--sp-2)); left: 0; right: 0; z-index: 40;
238
+ overflow: hidden; max-height: 240px; overflow-y: auto; }
239
+ .search-suggest.hidden { display: none; }
240
+ .search-suggest-item .ss-text { color: var(--text); font-family: var(--font-mono); }
241
+ .search-suggest-item .ss-hint { flex-shrink: 0; color: var(--text-dim); font-size: var(--fs-meta); }
242
+ .search-suggest-item.active { background: var(--menu-hover); }
243
+
244
+ /* Search options panel (the "tune" toggle): an in-flow fold under the search
245
+ field, inside the sticky header. Closed = grid row 0fr + opacity 0 + inert
246
+ (Forge's SearchFacets fold); open = 1fr. Its scroller is capped by
247
+ --opts-h (Core.attachSearchBox: ~40 % of the popup by default, dragged on
248
+ the bottom handle, kept per machine) and fades its edges (.bc-scroll-fade). */
249
+ .search-opts { display: grid; grid-template-rows: 0fr; opacity: 0;
250
+ transition: grid-template-rows var(--dur) var(--ease), opacity var(--dur) var(--ease); }
251
+ .search-opts.open { grid-template-rows: 1fr; opacity: 1; }
252
+ .search-opts-clip { min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
253
+ .search-opts-scroll { max-height: var(--opts-h, 240px); overflow-y: auto; padding: var(--sp-2) var(--gutter) var(--sp-4); }
254
+ .search-opts.resizing .search-opts-scroll { max-height: none; height: var(--opts-h, 240px); }
255
+ /* Filter units flow and wrap (Forge's FacetRows): as many per line as fit.
256
+ Every label takes the same width, so units stacked in a narrow popup still
257
+ line their chips up in one column. */
258
+ .opts-facets { --opts-label-w: 3.2em; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-10); }
259
+ .opts-unit { display: flex; align-items: center; gap: var(--sp-3); flex: 0 0 auto; max-width: 100%; } /* whole units move to the next line; only one wider than the panel wraps its chips */
260
+ /* A real width, not just a flex-basis: Chromium sizes the unit from the
261
+ label's TEXT width, then lays the label out at its basis, taking the
262
+ difference from the chips (the last chip of a unit that fit wrapped). */
263
+ .opts-unit > .opts-facet-label { flex: 0 0 auto; width: var(--opts-label-w); }
264
+ .opts-facet-chips { display: flex; flex-wrap: wrap; gap: var(--sp-1); min-width: 0; }
265
+ /* A key chip paints its prefix as the field will (.qh-prefix), and the notes
266
+ their tokens (.qh-neg, .qh-quote): what the panel shows is what you type. */
267
+ .opts-notes { flex: 1 0 100%; margin: 0; padding-left: calc(var(--opts-label-w) + var(--sp-3) + var(--sp-3)); font-size: var(--fs-meta); line-height: var(--lh-ui); color: var(--text-dim); }
268
+ .opts-notes-sep { margin: 0 var(--sp-2); }
269
+ /* Bottom edge: the resize handle (Forge's ResizeHandle: a chrome-less strip,
270
+ accent while hovered or dragged) over the panel's divider line. */
271
+ .search-opts-resize { position: relative; flex-shrink: 0; height: var(--sp-3); cursor: row-resize; touch-action: none; }
272
+ .search-opts-resize::after { content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: var(--line);
273
+ transition: background-color var(--dur) var(--ease), height var(--dur) var(--ease); }
274
+ .search-opts-resize:hover::after, .search-opts.resizing .search-opts-resize::after { height: 2px; background: color-mix(in srgb, var(--accent) 55%, transparent); }
275
+
276
+ /* ONE scroll-edge fade (Forge's useScrollFade): a real alpha mask, so content
277
+ dissolves into whatever is behind it, glass included. Core.attachScrollFade
278
+ sets --fade-top / --fade-bottom to the preset size only while content is
279
+ hidden past that edge; @property makes them animatable, so the fade eases in
280
+ and out as you reach an end. */
281
+ @property --fade-top { syntax: "<length>"; inherits: false; initial-value: 0px; }
282
+ @property --fade-bottom { syntax: "<length>"; inherits: false; initial-value: 0px; }
283
+ .bc-scroll-fade {
284
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0, black var(--fade-top), black calc(100% - var(--fade-bottom)), transparent 100%);
285
+ mask-image: linear-gradient(to bottom, transparent 0, black var(--fade-top), black calc(100% - var(--fade-bottom)), transparent 100%);
286
+ transition: --fade-top var(--dur) var(--ease), --fade-bottom var(--dur) var(--ease);
287
+ }
288
+ /* The same fade sideways, for a one-line strip that scrolls (the chip row, a
289
+ window bar's keys): Core.attachScrollFade(el, preset, { axis: 'x' }). */
290
+ @property --fade-left { syntax: "<length>"; inherits: false; initial-value: 0px; }
291
+ @property --fade-right { syntax: "<length>"; inherits: false; initial-value: 0px; }
292
+ .bc-scroll-fade-x {
293
+ -webkit-mask-image: linear-gradient(to right, transparent 0, black var(--fade-left), black calc(100% - var(--fade-right)), transparent 100%);
294
+ mask-image: linear-gradient(to right, transparent 0, black var(--fade-left), black calc(100% - var(--fade-right)), transparent 100%);
295
+ transition: --fade-left var(--dur) var(--ease), --fade-right var(--dur) var(--ease);
296
+ }
297
+
298
+ /* The chip row (Core.attachChipStrip): ONE line that scrolls sideways under
299
+ the mask fade (.bc-scroll-fade-x), every chip once the search options panel
300
+ is open (.expanded, wrapped). .strip-moving is the switch: the row's height
301
+ follows while each chip glides to its new place (FLIP). */
302
+ .group-filters { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-1) var(--gutter) var(--sp-1) calc(var(--gutter) - var(--sp-2)); box-sizing: border-box;
303
+ flex-wrap: nowrap; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; position: relative; z-index: 30; }
304
+ .group-filters::-webkit-scrollbar { display: none; }
305
+ .group-filters > * { flex-shrink: 0; }
306
+ .group-filters.expanded { flex-wrap: wrap; overflow: visible; }
307
+ .group-filters.strip-moving { overflow: hidden; }
308
+ /* ONE chip (Core.renderChip): the chip bar's icon facets and group chips and
309
+ the options panel's facet chips. Idle = dim text, no fill, no border (a
310
+ group chip: its name in the accent, so the groups read at a glance; owner
311
+ 2026-10-09: "too hard to see before selected"); hover = the hover overlay;
312
+ active (include) = the whole chip lit, a solid accent fill with the accent's
313
+ ink and a filled glyph; excluded = struck through. One shape: the pill. */
314
+ .filter-tag { min-height: var(--ctl-sm); max-width: 170px; box-sizing: border-box; font-size: var(--fs-ui); padding: 0 var(--sp-3); border-radius: var(--r-chip); border: 0;
315
+ background: transparent; color: var(--text-dim); cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); line-height: var(--lh-ui); position: relative;
316
+ display: inline-flex; align-items: center; gap: var(--sp-1); vertical-align: top; font-family: inherit; }
317
+ .filter-tag.icon-filter { min-width: var(--ctl-sm); height: var(--ctl-sm); justify-content: center; padding: 0 var(--sp-2); }
318
+ .filter-tag:hover { background: var(--hover); color: var(--text); }
319
+ .filter-tag.group-tag, .filter-tag.group-tag:hover { color: var(--accent-text); }
320
+ .filter-tag.active, .filter-tag.active:hover { background: var(--accent); color: var(--active-fg); --icon-fill: 1; }
321
+ .filter-tag.active:hover { background: var(--accent-hover); }
322
+ .filter-tag.excluded { color: var(--text-dim); text-decoration: line-through; text-decoration-thickness: 1px; opacity: .6; }
323
+ /* A greyed chip (the availability census, Forge's disabledTier): it would show
324
+ nothing under the other filters (.4), or can never match with them
325
+ (.dis-structural, .25). Inert (the controller ignores aria-disabled; panel
326
+ chips are disabled buttons), the reason is its tooltip, it never moves. */
327
+ .filter-tag:disabled, .filter-tag.is-disabled { opacity: .4; cursor: default; }
328
+ .filter-tag.is-disabled.dis-structural { opacity: .25; }
329
+ .filter-tag:disabled:hover, .filter-tag.is-disabled:hover { background: transparent; color: var(--text-dim); }
330
+ /* The selection at the front of the chip row (Core.renderFilterBar): the
331
+ selected groups (then the excluded ones), led by their or / and when two or
332
+ more are joined, a hairline either side. The panel's Type row leads with
333
+ the same toggle. Chips glide in and out (Core.flipChildren). */
334
+ .cluster-sep { flex: 0 0 1px; align-self: stretch; margin: var(--sp-1) 0; background: var(--line); }
335
+ .conn-toggle { flex-shrink: 0; min-height: var(--ctl-sm); padding: 0 var(--sp-2); border: 0; border-radius: var(--r-chip); background: transparent;
336
+ color: var(--accent-text); font: inherit; font-size: var(--fs-ui); font-weight: var(--fw-strong); cursor: pointer; }
337
+ .conn-toggle:hover { background: var(--accent-bg); }
338
+
339
+ /* The list sits in a wrapper so the "Newest" pill can float over its top edge
340
+ without being part of the scrolled rows (Core.createClipList owns both). */
341
+ .list-wrap { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
342
+ .list-newest { position: absolute; top: var(--sp-4); left: 50%; z-index: 30; display: inline-flex; align-items: center; gap: var(--sp-2);
343
+ height: var(--ctl-md); padding: 0 var(--sp-5) 0 var(--sp-4); border: none; border-radius: var(--r-chip); color: var(--text);
344
+ font: inherit; font-size: var(--fs-meta); cursor: pointer;
345
+ opacity: 0; pointer-events: none; transform: translate(-50%, -6px); transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); }
346
+ .list-newest.show { opacity: 1; pointer-events: auto; transform: translate(-50%, 0); }
347
+ .list-newest:hover { color: var(--accent-text); }
348
+ .list-newest .mi.sm { font-size: var(--icon-sm); }
349
+ .list-newest-dot { display: none; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
350
+ .list-newest.has-new .list-newest-dot { display: inline-block; }
351
+ /* The results fade out at an edge while more is past it (the shared
352
+ .bc-scroll-fade, Core.createClipList); scroll-padding keeps the keyboard
353
+ cursor out of the fades when it is scrolled into view. */
354
+ .list { flex: 1; overflow-y: auto; padding: 0 0 var(--sp-4); min-height: 0; scroll-padding-block: 36px; }
355
+ .item { padding: var(--sp-4) var(--gutter); cursor: pointer; transition: background-color var(--dur) var(--ease); position: relative; border-bottom: 1px solid var(--line-faint); color: inherit; }
356
+ /* Row states: hover = the --hover overlay; the keyboard cursor (the row Enter
357
+ pastes) = --accent-bg; a candidate duplicate of the hovered or cursor row
358
+ (.similar, Core.similarClipIds) = a NEUTRAL wash under --hover plus a dotted
359
+ dim edge, never the selection hue, so it can't read as the cursor or a
360
+ checked row. ONE 2px left edge, an overlay so it never shifts the row's
361
+ content: similar = dotted --text-dim, pin = --pin (wins over similar),
362
+ multi-select = --accent (wins over both). No state changes a row's size. */
363
+ .item::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: var(--row-edge, transparent); pointer-events: none; }
364
+ .item.similar { background: color-mix(in srgb, var(--hover) var(--similar-mix), transparent); --row-edge: repeating-linear-gradient(to bottom, var(--text-dim) 0 3px, transparent 3px 6px); }
365
+ .item.has-pin { --row-edge: var(--pin); }
366
+ .item:hover { background: var(--hover); }
367
+ .item.selected { background: var(--accent-bg); }
368
+ /* No flex gap: the space after the star is its own margin, so a closed
369
+ actions track at the end of the row takes exactly 0 px. */
370
+ .item-row { display: flex; align-items: flex-start; }
371
+ .pin-area { position: relative; flex-shrink: 0; display: flex; align-items: center; margin-right: var(--sp-4); margin-top: calc((var(--fs-ui) * var(--lh-ui) - var(--ctl-md)) / 2); }
372
+ .star { display: flex; align-items: center; justify-content: center; width: var(--ctl-md); height: var(--ctl-md); background: none; border: none; color: var(--text-dim); cursor: pointer; padding: 0;
373
+ transition: color var(--dur) var(--ease); line-height: 1; }
374
+ .star.active, .star:hover { color: var(--pin); }
375
+ /* The keypad (renderKeypadMenu) as a block inside a floating surface: the
376
+ clip menu's Numpad submenu and a row's # popover. Real numpad formation:
377
+ 7 8 9 / 4 5 6 / 1 2 3 (the 3-column grid lays it out like the keypad), keys
378
+ aligned with the menu rows' icon column. Neutral keys; the clip's own key is
379
+ the accent (the selection colour); a key another clip holds carries a dot. */
380
+ .numpad-picker { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-1) var(--sp-3); }
381
+ /* The keypad alone (a clip with no key yet): the same inset on all four
382
+ sides; the icon-column alignment only matters beside menu rows. */
383
+ .numpad-picker:only-child { padding: var(--sp-1); }
384
+ .np-row { display: grid; grid-template-columns: repeat(3, var(--ctl-md)); gap: var(--sp-1); justify-content: start; }
385
+ .np-btn { position: relative; width: var(--ctl-md); height: var(--ctl-md); display: flex; align-items: center; justify-content: center; padding: 0 0 var(--sp-1); border: none; border-radius: var(--r-ctl);
386
+ background: var(--ctl-fill); color: var(--text); font-family: inherit; font-size: var(--fs-ui); line-height: 1; font-variant-numeric: tabular-nums;
387
+ cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
388
+ .np-btn:hover { background: var(--ctl-fill-hover); }
389
+ /* A key another clip holds: a dot under the digit (every digit sits a step
390
+ high, so the dot keeps its clearance). */
391
+ .np-btn.taken::after { content: ""; position: absolute; left: 0; right: 0; bottom: 1px; width: var(--sp-2); height: var(--sp-2); margin: 0 auto;
392
+ border-radius: var(--r-chip); background: var(--text-dim); }
393
+ .np-btn.current { background: var(--accent-bg); color: var(--accent-text); font-weight: var(--fw-strong); }
394
+ .np-btn.current:hover { background: var(--accent-bg-strong); }
395
+ .bc-menu.bc-keypad, .bc-menu .tag-submenu.bc-keypad { min-width: 0; }
396
+ .filter-tag.virtual { opacity: .82; font-style: italic; }
397
+ .tag-caret { flex: 0 0 auto; font-size: var(--icon-md); line-height: 1; opacity: .72; margin-left: auto; }
398
+ .filter-tag .tag-caret { margin-left: var(--sp-1); margin-right: calc(-1 * var(--sp-1)); }
399
+ .tag-label { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: var(--lh-ui); }
400
+ .tag-menu-node { position: relative; display: block; }
401
+ .group-filters > .tag-menu-node { display: inline-flex; }
402
+ .tag-submenu { display: none; position: absolute; top: 100%; left: 0; min-width: 150px; max-width: 240px; z-index: 70; }
403
+ .tag-menu-node:hover > .tag-submenu, .tag-menu-node:focus-within > .tag-submenu { display: block; }
404
+ .tag-submenu .tag-submenu { top: calc(-1 * var(--sp-2)); left: calc(100% - 1px); }
405
+ /* A chip's submenu in the top layer (popover="manual", placed and shown by
406
+ installSubmenuAutoflip): its own box, and shown only while it is open (the
407
+ :hover rule above would otherwise show it in place inside the strip). */
408
+ .group-filters .tag-menu-node > .tag-submenu[popover] { position: fixed; inset: auto; margin: 0; border: 0; overflow: visible; color: inherit; }
409
+ .group-filters .tag-menu-node > .tag-submenu[popover]:not(:popover-open) { display: none; }
410
+ .group-filters .tag-menu-node > .tag-submenu[popover]:popover-open { display: block; }
411
+ /* A group chip's dropdown holds menu rows (renderTagTreeMenuNode, depth > 0):
412
+ an excluded group is struck through like its chip, a greyed one dims in the
413
+ chips' two tiers, and a parent row stays lit while its submenu is out. */
414
+ .bc-menu-item.excluded > .bc-menu-label { text-decoration: line-through; }
415
+ .bc-menu-item.is-disabled { opacity: .4; cursor: default; }
416
+ .bc-menu-item.is-disabled.dis-structural { opacity: .25; }
417
+ .bc-menu-item.is-disabled:hover { background: none; }
418
+ .group-filters .tag-menu-node:hover > .bc-menu-item { background: var(--menu-hover); }
419
+ .content { flex: 1; min-width: 0; }
420
+ /* Row anatomy (Core.renderClipItem): the primary line (a real title at the
421
+ strong weight, or an untitled clip's first line at the regular weight), the
422
+ dim mono preview of the rest, then the meta line. Every line is ONE line, so
423
+ a row's height depends only on which lines it has, never on its width. */
424
+ .clip-title { font-size: var(--fs-ui); line-height: var(--lh-ui); font-weight: var(--fw-strong); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
425
+ .clip-title.derived { font-weight: var(--fw-regular); }
426
+ .clip-title + .preview { margin-top: var(--sp-1); }
427
+ .preview { font-size: var(--fs-ui); line-height: var(--lh-text); color: var(--text-dim); font-family: var(--font-mono); }
428
+ .preview.collapsed { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
429
+ .preview.expanded { white-space: pre-wrap; word-break: break-word; max-height: 300px; overflow-y: auto; background: var(--surface); border-radius: var(--r-2); padding: var(--sp-4); margin: var(--sp-1) 0; }
430
+ /* Image previews render at --clip-img-h (Ctrl+wheel / Settings, default 60px),
431
+ capped by --clip-img-cap (the list's visible height, set by Core.createImageZoom)
432
+ so one preview never outgrows the list. The wrapper is the picture's box (so
433
+ the row's buttons can float over its corner): with known dims (--ar/--nw from
434
+ the row markup) its width is the smallest of the row, height x aspect ratio
435
+ and the image's real width - so the ratio holds, nothing overflows sideways
436
+ and nothing is enlarged past its real size. Legacy clips without dims fall
437
+ back to max-width/max-height, which also keeps the ratio. */
438
+ .preview-img { --img-h: min(var(--clip-img-h, 60px), var(--clip-img-cap, 100000px));
439
+ position: relative; display: block; width: fit-content; max-width: 100%; }
440
+ .preview-img[style*="--ar"] { width: min(100%, calc(var(--img-h) * var(--ar)), var(--nw)); }
441
+ .preview-img img { display: block; max-width: 100%; max-height: var(--img-h); height: auto; border-radius: var(--r-1); }
442
+ .preview-img[style*="--ar"] img { width: 100%; max-height: none; }
443
+ /* An image row's buttons sit where a text row's do (the row's top right, on
444
+ the first line), floating so the picture never resizes and the row never
445
+ grows. Plain like a text row's; only while the picture reaches under them
446
+ (.img-under, Core.markImageUnder) do they get a frosted chip in the opaque
447
+ menu colour, which reads on any picture in both themes and every accent.
448
+ The chip's padding is cancelled by its margin, so the buttons never move. */
449
+ .item-row { position: relative; }
450
+ .img-actions { position: absolute; top: calc((var(--fs-ui) * var(--lh-ui) - var(--ctl-md)) / 2); right: 0; display: flex; align-items: center; gap: var(--sp-1);
451
+ border-radius: var(--r-panel); opacity: 0; visibility: hidden;
452
+ transition: opacity var(--dur) var(--ease), visibility var(--dur) var(--ease); }
453
+ .img-under .img-actions { padding: var(--sp-1); margin: calc(-1 * var(--sp-1));
454
+ background: color-mix(in srgb, var(--menu) 78%, transparent); -webkit-backdrop-filter: blur(10px) saturate(1.3); backdrop-filter: blur(10px) saturate(1.3);
455
+ box-shadow: 0 1px 6px var(--shadow); }
456
+ .img-under .img-actions .icon-btn { color: var(--text); }
457
+ /* A text row's buttons ride the shared reveal at the row's end: closed = 0 px
458
+ (the text runs the full width), open while the row is hovered, holds focus,
459
+ has its menu open or is being dragged (.actions-held); the text's ellipsis
460
+ moves with them. The meta line's ghost # / + open the same way. */
461
+ .row-actions { --reveal-gap: var(--sp-2); margin-top: calc((var(--fs-ui) * var(--lh-ui) - var(--ctl-md)) / 2); }
462
+ .item:hover .bc-reveal.row-actions, .item:focus-within .bc-reveal.row-actions, .item.actions-held .bc-reveal.row-actions,
463
+ .item:hover .bc-reveal.meta-reveal, .item:focus-within .bc-reveal.meta-reveal, .item.actions-held .bc-reveal.meta-reveal { grid-template-columns: 1fr; }
464
+ .item:hover .bc-reveal.row-actions > .bc-reveal-inner, .item:focus-within .bc-reveal.row-actions > .bc-reveal-inner, .item.actions-held .bc-reveal.row-actions > .bc-reveal-inner,
465
+ .item:hover .bc-reveal.meta-reveal > .bc-reveal-inner, .item:focus-within .bc-reveal.meta-reveal > .bc-reveal-inner, .item.actions-held .bc-reveal.meta-reveal > .bc-reveal-inner,
466
+ .item:hover .img-actions, .item:focus-within .img-actions, .item.actions-held .img-actions { visibility: visible; content-visibility: visible; }
467
+ .item:hover .img-actions, .item:focus-within .img-actions, .item.actions-held .img-actions { opacity: 1; }
468
+ /* Meta line (Core.renderClipMeta): time, size, the numpad badge and the clip's
469
+ groups as plain text (each a filter: click includes, right-click excludes),
470
+ then the hover-only ghost # and + on the reveal. ONE line that never wraps:
471
+ as the ghosts slide in, group names that no longer fit drop out whole (.meta-tags), so no row changes height
472
+ and nothing is reserved while they are hidden (the reveal's negative margin
473
+ cancels the flex gap before a closed track). */
474
+ .meta { --meta-h: calc(var(--fs-meta) * var(--lh-text)); margin-top: var(--sp-1); display: flex; align-items: center; gap: var(--sp-3); min-width: 0; height: var(--meta-h);
475
+ font-size: var(--fs-meta); line-height: 1; color: var(--text-dim); white-space: nowrap; overflow: clip; overflow-clip-margin: var(--sp-1); }
476
+ .meta-time, .meta-size, .meta-np { flex-shrink: 0; }
477
+ .meta-np, .meta-tag, .meta-ghost { height: var(--meta-h); padding: 0 var(--sp-1); border: 0; border-radius: var(--r-1); background: transparent; color: inherit;
478
+ font: inherit; line-height: 1; cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
479
+ /* The badge and the names pad their hover fill but pull it back out of the
480
+ flow, so every text-to-text gap on the line is the one --sp-3. */
481
+ .meta-np, .meta-tag { margin-inline: calc(-1 * var(--sp-1)); }
482
+ .meta-np { color: var(--text); }
483
+ /* The group names show whole or not at all, never as fragments: they wrap onto
484
+ a second line the fixed-height box clips away, so a name that does not fit
485
+ is left out whole (each name's own title still names it on hover); only a
486
+ name alone on the line ellipsizes. The + picker lists every membership. */
487
+ .meta-tags { display: flex; flex-wrap: wrap; align-content: flex-start; column-gap: var(--sp-3); flex: 0 1 auto; min-width: 0; height: var(--meta-h); overflow: hidden;
488
+ padding-inline: var(--sp-1); margin-inline: calc(-1 * var(--sp-1)); /* room for the names' hover fill */ }
489
+ /* Hover (or focus, or a held row): the names that would not fit beside the
490
+ ghosts are set aside whole (Core.fitMetaTags marks them), so the box ends
491
+ at the last name and the + follows it instead of the box's far edge. */
492
+ .item:hover .meta-tag.meta-cut, .item:focus-within .meta-tag.meta-cut, .item.actions-held .meta-tag.meta-cut { display: none; }
493
+ .meta-tag { display: block; flex: 0 1 auto; min-width: 0; max-width: 140px; overflow: hidden; text-overflow: ellipsis; }
494
+ .meta-np:hover, .meta-tag:hover, .meta-ghost:hover { color: var(--text); background: var(--hover); }
495
+ .meta-ghost { display: inline-flex; align-items: center; justify-content: center; width: var(--meta-h); padding: 0; }
496
+ .meta-ghost .mi { font-size: var(--icon-sm); }
497
+ .meta > .meta-reveal, .bc-tag-strip > .meta-reveal { margin-left: calc(-1 * var(--sp-3)); --reveal-gap: var(--sp-3); }
498
+ .meta-reveal .bc-reveal-inner { gap: var(--sp-1); }
499
+ /* Empty list (Core.renderEmptyState): a quiet icon, what is empty and why, the
500
+ way out; .empty-nudge is the slot for the search behaviour's suggestion. */
501
+ .list-empty { display: flex; flex-direction: column; align-items: center; gap: var(--sp-2); padding: var(--sp-12) var(--gutter); text-align: center; color: var(--text-dim); }
502
+ .list-empty .empty-icon { font-size: var(--icon-lg); color: var(--text-dim); margin-bottom: var(--sp-1); }
503
+ .empty-title { margin: 0; max-width: 100%; font-size: var(--fs-ui); line-height: var(--lh-ui); font-weight: var(--fw-strong); color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
504
+ .empty-hint { margin: 0; font-size: var(--fs-meta); line-height: var(--lh-ui); color: var(--text-dim); }
505
+ .empty-action { margin-top: var(--sp-2); }
506
+ .empty-nudge[hidden] { display: none; }
507
+ /* The relax nudge (Core.renderRelaxNudge): one quiet line, the blocking token
508
+ in mono, the count, an arrow; a click drops that filter. */
509
+ .empty-nudge-btn { display: inline-flex; align-items: center; gap: var(--sp-2); min-height: var(--ctl-md); padding: 0 var(--sp-3); border: 0; border-radius: var(--r-ctl);
510
+ background: transparent; color: var(--text-dim); font: inherit; font-size: var(--fs-ui); cursor: pointer;
511
+ transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
512
+ .empty-nudge-btn code { font-family: var(--font-mono); color: var(--text); }
513
+ .empty-nudge-btn .mi { font-size: var(--icon-sm); color: var(--accent); }
514
+ .empty-nudge-btn:hover { background: var(--hover); color: var(--text); }
515
+
516
+ .settings-view { display: none; flex-direction: column; height: 100%; color: var(--text); background: transparent; }
517
+ .settings-view.show { display: flex; }
518
+ .settings-hdr { display: flex; align-items: center; flex-shrink: 0; height: var(--bar-h); padding: 0 var(--edge-ctl); gap: var(--sp-3);
519
+ border-bottom: 1px solid var(--line); background: var(--surface); }
520
+ .settings-hdr h2 { font-size: var(--fs-ui); font-weight: var(--fw-strong); flex: 1; margin: 0; color: var(--text); }
521
+ .settings-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-6) var(--gutter); }
522
+ /* Settings (Core.renderSettingsBody, Core.renderSettingsItem): ONE row grid and
523
+ ONE list style on the same three columns, label | control | reset, so every
524
+ control and every row action (a reset, a remove x) ends on one right edge;
525
+ the third column is reserved even when empty. Rows are spaced, never boxed;
526
+ list rows are split by --line-faint hairlines only. */
527
+ .setting-row, .settings-item, .settings-list-cols { display: grid; grid-template-columns: minmax(0, 1fr) auto var(--ctl-md); column-gap: var(--sp-4); align-items: center;
528
+ color: var(--text); font-size: var(--fs-ui); line-height: var(--lh-ui); }
529
+ .setting-row { min-height: 36px; padding: var(--sp-3) 0; }
530
+ .setting-row.note { min-height: 0; }
531
+ .setting-text, .si-text { grid-column: 1; display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
532
+ .setting-control, .setting-row > .switch, .si-control { grid-column: 2; justify-self: end; display: inline-flex; align-items: center; gap: var(--sp-2); min-width: 0; }
533
+ .setting-reset, .si-action { grid-column: 3; display: inline-flex; justify-content: center; }
534
+ .setting-label { color: var(--text); }
535
+ /* One-line help / status under a label; an empty one takes no space. */
536
+ .setting-help { font-size: var(--fs-meta); color: var(--text-dim); overflow-wrap: anywhere; }
537
+ .setting-help:empty { display: none; }
538
+ .setting-help.mono { font-family: var(--font-mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; overflow-wrap: normal; }
539
+ .setting-help.error { color: var(--red); }
540
+ /* A status that reports something running carries the green success dot. */
541
+ .setting-help.active::before { content: ""; display: inline-block; width: 6px; height: 6px; margin-right: var(--sp-2); border-radius: 50%; background: var(--green); vertical-align: 1px; }
542
+ /* Free text takes the full width under its label (pinned peers), up to the
543
+ shared control edge. */
544
+ .setting-row.stack > .setting-control { grid-column: 1 / 3; justify-self: stretch; margin-top: var(--sp-3); }
545
+ .setting-row.stack input { flex: 1; }
546
+ /* Filled fields: a fill, never a border. A number field carries its unit
547
+ inside (.input-affix: the fill and the focus edge sit on the wrapper). */
548
+ .setting-row input[type="text"], .input-affix { height: var(--ctl-lg); background: var(--input); border: none; border-radius: var(--r-ctl); color: var(--text); font: inherit; font-size: var(--fs-ui); }
549
+ .setting-row input[type="text"] { min-width: 0; padding: 0 var(--sp-4); outline: none; }
550
+ .input-affix { display: inline-flex; align-items: center; gap: var(--sp-2); width: 104px; padding: 0 var(--sp-4); color: var(--text-dim); cursor: text; }
551
+ .input-affix:focus-within { box-shadow: inset 0 0 0 1px var(--accent); }
552
+ .input-affix input { flex: 1; width: 0; min-width: 0; height: 100%; padding: 0; border: none; background: transparent; color: var(--text); font: inherit; text-align: right; outline: none; }
553
+ .input-affix input:focus { box-shadow: none; }
554
+ /* ONE switch (a row's, or one inside a list row via .switch-wrap). */
555
+ .switch-row, .switch-wrap { position: relative; cursor: pointer; }
556
+ .switch-wrap { display: inline-flex; }
557
+ .switch-row input, .switch-wrap input { position: absolute; opacity: 0; pointer-events: none; }
558
+ .switch { width: 32px; height: 18px; position: relative; flex-shrink: 0; border-radius: var(--r-chip); background: var(--line-strong); transition: background-color var(--dur) var(--ease); }
559
+ .switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--text); transition: transform var(--dur) var(--ease), background-color var(--dur) var(--ease); }
560
+ input:checked + .switch { background: var(--accent); }
561
+ input:checked + .switch::after { transform: translateX(14px); background: var(--active-fg); }
562
+ input:focus-visible + .switch { box-shadow: var(--focus-ring); }
563
+ input:disabled + .switch { opacity: .55; }
564
+ .switch-wrap.is-disabled { cursor: default; }
565
+ /* Lists: one row primitive for numpad slots, groups, sync providers, peers,
566
+ AI clients, grants and conflicts. */
567
+ .settings-list { display: flex; flex-direction: column; }
568
+ .settings-item { min-height: 34px; padding: var(--sp-2) 0; border-bottom: 1px solid var(--line-faint); }
569
+ .settings-item:last-child { border-bottom: 0; }
570
+ .si-title { display: flex; align-items: baseline; gap: var(--sp-3); min-width: 0; }
571
+ .si-key { flex-shrink: 0; color: var(--text); font-variant-numeric: tabular-nums; }
572
+ .si-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
573
+ .si-meta { flex-shrink: 0; font-size: var(--fs-meta); color: var(--text-dim); }
574
+ .si-sub { font-size: var(--fs-meta); color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
575
+ .si-name.mono, .si-sub.mono { font-family: var(--font-mono); }
576
+ .settings-item.dim .si-name { color: var(--text-dim); }
577
+ /* A row that acts on a click (a numpad slot copies its clip): the hover
578
+ overlay reaches a little past the text (two side shadows, so neither the
579
+ columns nor the hairline move). */
580
+ .settings-item.clickable { cursor: pointer; }
581
+ .settings-item.clickable:hover { background: var(--hover); box-shadow: calc(-1 * var(--sp-3)) 0 0 var(--hover), var(--sp-3) 0 0 var(--hover); }
582
+ .settings-list-title { padding: var(--sp-4) 0 var(--sp-1); } /* type from the shared .bc-label rule */
583
+ /* A quiet text button under a list ("Show all clients"): its label lines up
584
+ with the rows' text. */
585
+ .settings-more { margin-top: var(--sp-2); margin-left: calc(-1 * var(--sp-3)); }
586
+ /* Accent: one swatch per choice, each dot (--dot) previewing the accent that
587
+ choice paints in THIS theme: the presets' steps mirror clipboard-tokens.css
588
+ (a step darker in light), System and Custom get their colour's per-theme
589
+ shade inline (--dot-dark / --dot-light + data-own, Core.mountSettings), a
590
+ Custom with no colour yet shows the wheel. The chosen one carries its ink
591
+ check. System, while not chosen, is a RING of its colour: "follows the
592
+ system", never a twin of Blue when the OS gives no colour. */
593
+ .accent-swatches { display: inline-flex; gap: var(--sp-1); }
594
+ .accent-swatch { display: inline-flex; align-items: center; justify-content: center; width: var(--ctl-md); height: var(--ctl-md); padding: 0; border: none; border-radius: var(--r-chip);
595
+ background: transparent; cursor: pointer; transition: background-color var(--dur) var(--ease); --dot: var(--dot-dark, var(--blue-500)); }
596
+ .accent-swatch[data-accent-mode="teal"] { --dot: var(--teal-500); }
597
+ .accent-swatch[data-accent-mode="mono"] { --dot: var(--g-300); }
598
+ :where(:root, .bc-popup)[data-theme="light"] .accent-swatch { --dot: var(--dot-light, var(--blue-600)); }
599
+ :where(:root, .bc-popup)[data-theme="light"] .accent-swatch[data-accent-mode="teal"] { --dot: var(--teal-600); }
600
+ :where(:root, .bc-popup)[data-theme="light"] .accent-swatch[data-accent-mode="mono"] { --dot: var(--g-700); }
601
+ .accent-swatch:hover { background: var(--hover); }
602
+ .accent-dot { display: inline-flex; align-items: center; justify-content: center; width: var(--icon-md); height: var(--icon-md); border-radius: 50%; background: var(--dot); }
603
+ .accent-swatch[data-accent-mode="custom"]:not([data-own]) .accent-dot { background: conic-gradient(var(--red-500), var(--amber-500), var(--green-500), var(--teal-500), var(--blue-500), var(--red-500)); }
604
+ .accent-swatch[data-accent-mode="system"]:not([aria-checked="true"]) .accent-dot { background: transparent; box-shadow: inset 0 0 0 3px var(--dot); }
605
+ .accent-dot .mi { font-size: var(--icon-sm); color: var(--active-fg); visibility: hidden; }
606
+ .accent-swatch[aria-checked="true"] .accent-dot .mi { visibility: visible; }
607
+ /* Custom colour: a hue slider (its rainbow track is painted inline from the
608
+ same hue maths) plus a #rrggbb field. */
609
+ .accent-hue { -webkit-appearance: none; appearance: none; width: 120px; height: var(--sp-3); margin: 0; border-radius: var(--r-chip); cursor: pointer; outline: none; }
610
+ .accent-hue::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: var(--icon-sm); height: var(--icon-sm); border-radius: 50%; background: var(--text); box-shadow: 0 0 0 2px var(--bg); }
611
+ .accent-hue::-moz-range-thumb { width: var(--icon-sm); height: var(--icon-sm); border: none; border-radius: 50%; background: var(--text); box-shadow: 0 0 0 2px var(--bg); }
612
+ .accent-hue:focus-visible { box-shadow: var(--focus-ring); }
613
+ .setting-row input.accent-hex { width: 88px; font-family: var(--font-mono); }
614
+ .seg { display: inline-flex; height: var(--ctl-lg); padding: var(--sp-1); gap: var(--sp-1); background: var(--input); border: none; border-radius: var(--r-ctl); }
615
+ .seg-btn { height: calc(var(--ctl-lg) - 2 * var(--sp-1)); border: none; background: transparent; color: var(--text-dim); font-size: var(--fs-ui); font-family: inherit; padding: 0 var(--sp-4); border-radius: var(--r-1); cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
616
+ .seg-btn:hover { color: var(--text); }
617
+ .seg-btn.active { background: var(--accent); color: var(--active-fg); }
618
+ /* Sections: the shared .overline heading, --sp-12 above each one but the first
619
+ shown (the conflicts section on top is hidden while there are none). */
620
+ .settings-section { margin-top: var(--sp-12); }
621
+ .settings-section:first-child, .settings-section.hidden + .settings-section { margin-top: 0; }
622
+ .settings-section h3 { margin: 0 0 var(--sp-2); } /* type from the shared .overline rule */
623
+
624
+ @media (max-width: 760px) {
625
+ .item { padding-inline: var(--sp-5); }
626
+ }
627
+
628
+ /* Popup theme variables + design tokens live in clipboard-tokens.css
629
+ (imported at the top of this file), so the app and demo render from
630
+ one palette and cannot drift. Only component rules live here. */
631
+
632
+ /* The popup root carries the base text color so popup content never inherits a
633
+ host page's color (the website embeds the popup inside the marketing
634
+ page; the desktop app sets this on <body>). Without it, unstyled text like
635
+ the dialog title rendered dark-on-dark. */
636
+ .bc-popup { color: var(--text); }
637
+ .hidden { display: none; }
638
+ mark { background: var(--mark-bg); color: var(--mark-fg); border-radius: var(--r-1); padding: 0 var(--sp-1); }
639
+
640
+ /* Toast: a neutral floating surface (shared rule at the top), never a status
641
+ colour; an action (Undo) is an accent text button. */
642
+ .toast { position: fixed; bottom: var(--sp-8); left: 50%; transform: translateX(-50%) translateY(16px); display: flex; align-items: center; gap: var(--sp-4);
643
+ width: max-content; max-width: calc(100% - 2 * var(--gutter)); padding: var(--sp-3) var(--sp-5); color: var(--text); font-size: var(--fs-ui); line-height: var(--lh-ui);
644
+ opacity: 0; transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease); pointer-events: none; z-index: 50; }
645
+ .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
646
+
647
+ /* Confirm / prompt dialog */
648
+ .overlay { display: none; pointer-events: none; position: fixed; inset: 0; background: var(--scrim); z-index: 30; align-items: center; justify-content: center; }
649
+ .overlay.show { display: flex; pointer-events: auto; }
650
+ /* Dialog = the shared floating surface (bg, --r-panel, shadow at the top) with
651
+ room to breathe; sans body copy, an optional mono preview of clip text, and
652
+ the shared .btn set (confirm is .primary, or .danger for destructive ones). */
653
+ .dialog { padding: var(--sp-8); max-width: 320px; width: 90%; color: var(--text); }
654
+ .dialog h3 { font-size: var(--fs-ui); font-weight: var(--fw-strong); line-height: var(--lh-ui); color: var(--text); margin-bottom: var(--sp-3); }
655
+ .dialog p { font-size: var(--fs-ui); line-height: var(--lh-text); color: var(--text-dim); margin-bottom: var(--sp-6); word-break: break-word; }
656
+ .dialog p:empty { display: none; }
657
+ .dialog-preview { font-family: var(--font-mono); font-size: var(--fs-ui); line-height: var(--lh-text); color: var(--text); background: var(--input); border-radius: var(--r-ctl);
658
+ padding: var(--sp-3) var(--sp-4); margin-bottom: var(--sp-6); white-space: pre-wrap; word-break: break-word;
659
+ display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
660
+ .dialog-preview:empty { display: none; }
661
+ .dialog-btns { display: flex; gap: var(--sp-4); justify-content: flex-end; }
662
+ .prompt-input { width: 100%; height: var(--ctl-lg); background: var(--input); border: none; border-radius: var(--r-ctl); padding: 0 var(--sp-4); color: var(--text); font-size: var(--fs-text); outline: none; margin-bottom: var(--sp-6); font-family: inherit; }
663
+
664
+ /* Settings: the shortcut fields (sans key names; "Not set" dim) and the sync
665
+ button's live spin. Buttons are the shared .btn / .icon-btn. */
666
+ .shortcut-btn { min-width: 130px; max-width: 190px; height: var(--ctl-lg); background: var(--input); border: none; border-radius: var(--r-ctl); color: var(--text);
667
+ padding: 0 var(--sp-4); font-size: var(--fs-ui); font-family: inherit; white-space: nowrap; overflow: hidden;
668
+ text-overflow: ellipsis; cursor: pointer; text-align: center; }
669
+ .shortcut-btn.unset { color: var(--text-dim); }
670
+ .shortcut-btn.recording { color: var(--accent-text); background: var(--accent-bg); }
671
+ /* Live sync feedback: the icon itself spins while a merge is in flight. */
672
+ @keyframes bc-sync-spin { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } }
673
+ .sync-btn.syncing .mi { display: inline-block; animation: bc-sync-spin 1.1s linear infinite; }
674
+
675
+ /* --- Shared plain-text editor (Core.createEditor): app window + demo clip window --- */
676
+ /* Clip windows (editor, viewer, unify + conflict; the demo's clip window too):
677
+ ONE surface per window. The root paints --surface, every band in it (bar,
678
+ find, pane heads, merge gap, footer) is transparent and split by one
679
+ --line hairline, so the writing surface is never grey under white chrome.
680
+ Under glass the root goes transparent too and the window's single scrim
681
+ (clipboard-window.css) is the only frost. */
682
+ .bc-editor { display: flex; flex-direction: column; height: 100%; width: 100%; background: transparent; color: var(--text); min-height: 0; }
683
+ /* ONE window bar (.bc-bar, Core.renderWindowBar): [lead] title [context]
684
+ [tags] spacer actions. It is the window's drag region (a double-click
685
+ maximises on Windows, zooms or minimises per the macOS setting); its
686
+ controls are not. With the OS's own window controls (data-window-controls
687
+ on <html>, Core.attachWindowControls) it reserves their room: the traffic
688
+ lights' inset on the left (macOS, --wc-left from main), the caption
689
+ buttons' width on the right (Windows, the Window Controls Overlay env()). */
690
+ .bc-bar { -webkit-app-region: drag; display: flex; align-items: center; gap: var(--sp-3); height: var(--bar-h); flex-shrink: 0;
691
+ padding: 0 var(--edge-ctl) 0 var(--gutter); border-bottom: 1px solid var(--line); background: var(--surface); color: var(--text); }
692
+ :root[data-window-controls="left"] .bc-bar { padding-left: var(--wc-left, var(--gutter)); }
693
+ :root[data-window-controls="right"] { --wc-right: calc(100vw - env(titlebar-area-x, 0px) - env(titlebar-area-width, 100vw)); }
694
+ :root[data-window-controls="right"] .bc-bar { padding-right: max(var(--edge-ctl), calc(var(--wc-right) + var(--sp-2))); }
695
+ .bc-bar-title { flex: 0 1 auto; min-width: 0; font-size: var(--fs-ui); font-weight: var(--fw-strong); line-height: var(--lh-ui); color: var(--text);
696
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
697
+ /* The editable title (editor, merge): a flat field sized to its text (the
698
+ rest of the bar stays draggable), underline on hover / focus like the
699
+ search field. An untitled clip's placeholder is its first line, dim and
700
+ regular weight, as an untitled row reads in the list. */
701
+ input.bc-bar-title { -webkit-app-region: no-drag; field-sizing: content; min-width: 6ch; max-width: 60%; height: var(--ctl-md); margin-left: calc(-1 * var(--sp-2)); text-overflow: ellipsis;
702
+ padding: 0 var(--sp-2); background: transparent; border: none; border-radius: 0; font-family: inherit; outline: none;
703
+ box-shadow: inset 0 -1px 0 transparent; transition: box-shadow var(--dur) var(--ease); }
704
+ input.bc-bar-title:hover { box-shadow: inset 0 -1px 0 var(--line); }
705
+ input.bc-bar-title:focus { box-shadow: inset 0 -1px 0 var(--accent); }
706
+ input.bc-bar-title::placeholder { color: var(--text-dim); font-weight: var(--fw-regular); }
707
+ .bc-bar-context { flex: 0 1000 auto; min-width: 0; font-size: var(--fs-meta); /* gives way before the title in a narrow window */ color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
708
+ .bc-bar-spacer { flex: 1 1 0; min-width: var(--sp-4); align-self: stretch; }
709
+ .bc-bar-actions { -webkit-app-region: no-drag; display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; }
710
+ .bc-bar-actions .icon-btn.active { color: var(--accent); background: var(--accent-bg); }
711
+ /* A window on a page (the website demo, Core.attachWebWindow) draws the
712
+ visitor's OS controls in its bar (renderWindowBar webControls): macOS
713
+ traffic lights first (glyphs on hover; grey while the window is not the
714
+ active one), Windows caption buttons last, flush with the window's corner
715
+ and the bar's full height, close going red. */
716
+ .bc-wc { display: flex; flex-shrink: 0; align-self: stretch; align-items: center; -webkit-app-region: no-drag; }
717
+ .bc-wc-btn { display: grid; place-items: center; padding: 0; border: 0; cursor: default; }
718
+ .bc-wc-btn svg { display: block; fill: none; stroke: currentColor; }
719
+ .bc-wc-btn .wc-max, .bc-ww[data-max] .bc-wc-btn .wc-norm { display: none; }
720
+ .bc-ww[data-max] .bc-wc-btn .wc-max { display: block; }
721
+ .bc-wc[data-os="mac"] { gap: var(--sp-4); margin-right: var(--sp-5); }
722
+ .bc-wc[data-os="mac"] .bc-wc-btn { width: 12px; height: 12px; border-radius: var(--r-pill); color: var(--os-mac-ink); box-shadow: inset 0 0 0 .5px var(--os-mac-ink); }
723
+ .bc-wc[data-os="mac"] .bc-wc-btn[data-wc="close"] { background: var(--os-mac-close); }
724
+ .bc-wc[data-os="mac"] .bc-wc-btn[data-wc="min"] { background: var(--os-mac-min); }
725
+ .bc-wc[data-os="mac"] .bc-wc-btn[data-wc="max"] { background: var(--os-mac-max); }
726
+ .bc-wc[data-os="mac"] .bc-wc-btn svg { width: 12px; height: 12px; stroke-width: 1.2; opacity: 0; }
727
+ .bc-wc[data-os="mac"] .bc-wc-btn .wc-fill { fill: currentColor; stroke: none; }
728
+ .bc-wc[data-os="mac"]:hover .bc-wc-btn svg { opacity: 1; }
729
+ .bc-wc[data-os="mac"] .bc-wc-btn:active { filter: brightness(.82); }
730
+ .bc-ww:not([data-active]) .bc-wc[data-os="mac"]:not(:hover) .bc-wc-btn { background: var(--line); box-shadow: inset 0 0 0 .5px var(--line); }
731
+ .bc-wc[data-os="win"] { margin: 0 calc(-1 * var(--edge-ctl)) 0 var(--sp-2); }
732
+ .bc-wc[data-os="win"] .bc-wc-btn { width: 46px; height: 100%; background: transparent; color: var(--text); transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
733
+ .bc-wc[data-os="win"] .bc-wc-btn svg { width: 10px; height: 10px; stroke-width: 1; }
734
+ .bc-wc[data-os="win"] .bc-wc-btn:hover { background: var(--hover); }
735
+ .bc-wc[data-os="win"] .bc-wc-btn:active { background: var(--line-faint); }
736
+ .bc-wc[data-os="win"] .bc-wc-btn[data-wc="close"]:hover { background: var(--os-win-close); color: var(--white); }
737
+ .bc-wc[data-os="win"] .bc-wc-btn[data-wc="close"]:active { background: color-mix(in srgb, var(--os-win-close) 85%, var(--white)); color: var(--white); }
738
+ .bc-ww:not([data-active]) .bc-wc[data-os="win"] .bc-wc-btn:not(:hover) { color: var(--text-dim); }
739
+ /* The web window itself: placed in the page, raised by z-index, resized from
740
+ eight invisible grips on its edges and corners (none while maximised). */
741
+ .bc-ww { position: absolute; }
742
+ .bc-ww[hidden] { display: none !important; }
743
+ .bc-ww[data-max] { border-radius: 0; box-shadow: none; }
744
+ .bc-ww-moving, .bc-ww-sizing { user-select: none; }
745
+ .bc-ww-rs { position: absolute; z-index: 60; }
746
+ .bc-ww-rs[data-rs="n"], .bc-ww-rs[data-rs="s"] { left: var(--sp-6); right: var(--sp-6); height: var(--sp-3); cursor: ns-resize; }
747
+ .bc-ww-rs[data-rs="e"], .bc-ww-rs[data-rs="w"] { top: var(--sp-6); bottom: var(--sp-6); width: var(--sp-3); cursor: ew-resize; }
748
+ .bc-ww-rs[data-rs="n"], .bc-ww-rs[data-rs="ne"], .bc-ww-rs[data-rs="nw"] { top: 0; }
749
+ .bc-ww-rs[data-rs="s"], .bc-ww-rs[data-rs="se"], .bc-ww-rs[data-rs="sw"] { bottom: 0; }
750
+ .bc-ww-rs[data-rs="e"], .bc-ww-rs[data-rs="ne"], .bc-ww-rs[data-rs="se"] { right: 0; }
751
+ .bc-ww-rs[data-rs="w"], .bc-ww-rs[data-rs="nw"], .bc-ww-rs[data-rs="sw"] { left: 0; }
752
+ .bc-ww-rs[data-rs="ne"], .bc-ww-rs[data-rs="nw"], .bc-ww-rs[data-rs="se"], .bc-ww-rs[data-rs="sw"] { width: var(--sp-6); height: var(--sp-6); }
753
+ .bc-ww-rs[data-rs="ne"], .bc-ww-rs[data-rs="sw"] { cursor: nesw-resize; }
754
+ .bc-ww-rs[data-rs="nw"], .bc-ww-rs[data-rs="se"] { cursor: nwse-resize; }
755
+ .bc-ww[data-max] .bc-ww-rs { display: none; }
756
+ /* The viewer's leading image glyph is the file itself (a title-bar proxy
757
+ icon): drag it out into another app or a folder. */
758
+ .bc-drag-handle { -webkit-app-region: no-drag; flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: var(--ctl-md); height: var(--ctl-md);
759
+ margin-left: calc(-1 * var(--sp-2)); border-radius: var(--r-ctl); color: var(--text-dim); cursor: grab;
760
+ transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
761
+ .bc-drag-handle:hover { color: var(--text); background: var(--hover); }
762
+ .bc-drag-handle:active { cursor: grabbing; }
763
+ /* A clip window's star and keys (Core.updateTagStrip): the row's own star and
764
+ meta keys (renderClipKeys: the numpad badge, the group names, the ghosts #
765
+ and + on the reveal), so pin, key and groups read and work as in the list.
766
+ The star's glyph lands on the gutter when it leads the bar. The strip is
767
+ ONE line that scrolls sideways under the mask fade (attachScrollFade x);
768
+ the ghosts slide in while the bar is pointed at or holds the focus. */
769
+ .bc-bar > .star { -webkit-app-region: no-drag; flex-shrink: 0; }
770
+ .bc-bar > .star:first-child { margin-left: calc(var(--edge-ctl) - var(--gutter)); }
771
+ .bc-bar > .star[hidden] { display: none; }
772
+ .bc-tag-strip { -webkit-app-region: no-drag; --meta-h: calc(var(--fs-meta) * var(--lh-text)); display: flex; align-items: center; gap: var(--sp-3); min-width: 0; max-width: 50%;
773
+ padding-inline: var(--sp-1); overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
774
+ font-size: var(--fs-meta); line-height: 1; color: var(--text-dim); white-space: nowrap; }
775
+ .bc-tag-strip::-webkit-scrollbar { display: none; }
776
+ .bc-tag-strip[hidden] { display: none; }
777
+ .bc-tag-strip .meta-tags { flex-wrap: nowrap; flex: 0 0 auto; height: auto; overflow: visible; }
778
+ .bc-tag-strip .meta-tag { flex: 0 0 auto; }
779
+ .bc-bar:hover .bc-tag-strip .bc-reveal.meta-reveal, .bc-bar:focus-within .bc-tag-strip .bc-reveal.meta-reveal { grid-template-columns: 1fr; }
780
+ .bc-bar:hover .bc-tag-strip .meta-reveal > .bc-reveal-inner, .bc-bar:focus-within .bc-tag-strip .meta-reveal > .bc-reveal-inner { visibility: visible; content-visibility: visible; }
781
+ /* Find: chrome, so a band under the window bar like the merge pane heads
782
+ (--surface, ending on the --line hairline). The band's hairline IS the
783
+ field's underline: accent while anything in it has the focus (one line,
784
+ never a field underline 4 px above a band edge). The close's glyph ends on
785
+ the gutter. */
786
+ .bc-find { -webkit-app-region: no-drag; display: flex; align-items: center; gap: var(--sp-1); margin: 0; padding: var(--sp-1) var(--gutter); flex-shrink: 0;
787
+ background: var(--surface); border-bottom: 1px solid var(--line); transition: border-color var(--dur) var(--ease); }
788
+ .bc-find:focus-within { border-bottom-color: var(--accent); }
789
+ .bc-find[hidden] { display: none; }
790
+ .bc-find > :last-child { margin-right: calc(var(--edge-ctl) - var(--gutter)); }
791
+ .bc-find-input { flex: 1; min-width: 0; height: var(--ctl-md); background: transparent; border: none; border-radius: 0; color: var(--text); font: inherit; font-size: var(--fs-text);
792
+ padding: 0; margin-right: var(--sp-2); outline: none; }
793
+ .bc-find-input::placeholder { color: var(--text-dim); }
794
+ .bc-find-count { font-size: var(--fs-meta); color: var(--text-dim); min-width: 34px; text-align: right; font-variant-numeric: tabular-nums; margin-right: var(--sp-2); }
795
+ .bc-find.bad-pattern .bc-find-count { color: var(--red); }
796
+ .bc-editor-area { flex: 1; min-height: 0; width: 100%; resize: none; border: 0; outline: none; background: transparent; color: var(--text); }
797
+ .bc-editor-area, .bc-editor-hl {
798
+ font-family: var(--font-mono); font-size: var(--fs-text); line-height: var(--lh-text); padding: var(--sp-6) var(--gutter); tab-size: 4; white-space: pre-wrap; word-break: break-word; }
799
+ .bc-editor-area::placeholder { color: var(--text-dim); }
800
+ /* Find-match highlight backdrop: mirrors the textarea's text (transparent) so
801
+ <mark> backgrounds line up under the real text. Textarea and wrap are
802
+ transparent so the marks show through; the window's one surface (.bc-editor)
803
+ is the writing surface. */
804
+ .bc-editor-area-wrap { position: relative; flex: 1; min-height: 0; display: flex; background: transparent; }
805
+ .bc-editor-area-wrap .bc-editor-area { background: transparent; position: relative; z-index: 1; overflow-y: scroll; }
806
+ /* Width-only override of the shared scrollbar: the highlight mirror reserves
807
+ exactly this gutter (inset 0 10px 0 0 below), so it must stay fixed. */
808
+ .bc-editor-area-wrap .bc-editor-area::-webkit-scrollbar { width: 10px; }
809
+ .bc-editor-hl { position: absolute; inset: 0 10px 0 0; overflow: hidden; pointer-events: none; color: transparent; margin: 0; box-sizing: border-box; }
810
+ .bc-editor-hl mark { color: transparent; background: var(--mark-bg); border-radius: var(--r-1); padding: 0; }
811
+ .bc-editor-hl mark.cur { background: var(--mark-bg-current); }
812
+ /* Footer: stats left; the clipboard follow state, the live save state (or the
813
+ viewer's zoom controls) right. */
814
+ .bc-editor-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-5); padding: 0 var(--gutter); height: var(--ctl-lg); flex-shrink: 0;
815
+ border-top: 1px solid var(--line); background: var(--surface); font-size: var(--fs-meta); color: var(--text-dim); }
816
+ .bc-editor-foot > [data-x="stats"], .bc-editor-foot > [data-x="dims"] { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
817
+ .bc-editor-foot-end { display: flex; align-items: center; gap: var(--sp-5); flex-shrink: 0; }
818
+ /* Text ends on the gutter: a trailing quiet button (the viewer's 100%) pulls
819
+ in by its own padding. */
820
+ .bc-editor-foot-end > .btn.sm:last-child { margin-right: calc(-1 * var(--sp-3)); }
821
+ .bc-save-state { white-space: nowrap; }
822
+ .bc-save-state.failed { color: var(--red); }
823
+ /* Clipboard follow status: "On clipboard" (accent) / a Copy button / a brief notice. */
824
+ .bc-editor-clip { display: inline-flex; align-items: center; gap: var(--sp-2); white-space: nowrap; }
825
+ .bc-editor-clip.on { color: var(--accent-text); }
826
+ .bc-editor-clip.notice { color: var(--text); }
827
+ .bc-editor-clip .mi.sm { font-size: var(--icon-sm); }
828
+ /* The footer Copy is the shared quiet .btn.sm (20px text affordance). */
829
+ @media (max-width: 480px) { .bc-editor-clip.on > span:last-child { display: none; } }
830
+ /* Viewer zoom: zoom out | percentage | zoom in | Fit | 100%, quiet 20px
831
+ controls; the view that is on (Fit / 100%) reads in the accent. */
832
+ .bc-zoom { gap: var(--sp-1); }
833
+ .bc-zoom .icon-btn { width: var(--ctl-sm); height: var(--ctl-sm); }
834
+ .bc-zoom .icon-btn .mi { font-size: var(--icon-sm); }
835
+ .bc-zoom-pct { min-width: 4ch; text-align: center; font-variant-numeric: tabular-nums; }
836
+ .bc-zoom .btn.quiet.active { color: var(--accent-text); background: var(--accent-bg); }
837
+ /* Glass: the window roots go transparent so the OS acrylic + the one tint
838
+ scrim show through; every band in them is already transparent. */
839
+ :root[data-surface="glass"] .bc-editor,
840
+ :root[data-surface="glass"] .bc-reconcile,
841
+ .bc-popup[data-surface="glass"][data-glass-scope="all"] .bc-editor,
842
+ .bc-popup[data-surface="glass"][data-glass-scope="all"] .bc-reconcile { background: transparent; }
843
+ /* In-app image viewer: same window chrome as the editor (the .bc-bar + foot on
844
+ the one surface); the stage is the image's matte, centres the image and
845
+ hosts zoom/pan (transform-positioned <img>). */
846
+ .bc-viewer { display: flex; flex-direction: column; height: 100%; width: 100%; background: transparent; color: var(--text); min-height: 0; outline: none; }
847
+ .bc-viewer-stage { position: relative; flex: 1; min-height: 0; overflow: hidden; background: var(--canvas); }
848
+ .bc-viewer-img { position: absolute; left: 0; top: 0; transform-origin: 0 0; user-select: none; image-rendering: auto; }
849
+ :root[data-surface="glass"] .bc-viewer, :root[data-surface="glass"] .bc-viewer-stage { background: transparent; }
850
+ /* Demo overlay host: the editor floats over the demo popup, standing in for
851
+ the app's editor WINDOW, so it follows "Glass on" (data-glass-scope on the
852
+ popup, Core.applyVariants) like the app does: Popup only = a solid window
853
+ (it re-takes the solid surface tokens the popup's glass overrides, so it
854
+ paints exactly the app editor's --surface); All windows = one frosted
855
+ layer over the list (the window roots inside go transparent above). */
856
+ /* IntelliJ-style merge (CodeMirror 5 merge addon, skinned with our tokens):
857
+ Current (read-only) | Result (editable) | Incoming (read-only); 2-pane =
858
+ Current (edited in place into the result) | Incoming. One plane: the root
859
+ paints the window surface, heads / panes / gap / footer are transparent. */
860
+ .bc-reconcile { display: flex; flex-direction: column; height: 100%; width: 100%; background: transparent; color: var(--text); min-height: 0; }
861
+ /* Pane heads aligned to the addon's pane widths (2-pane 47%/6%, 3-pane
862
+ 31%/3.5%): the pane's name, its title as a one-click pick when the two
863
+ titles differ (the result's title is the bar's field), and the whole-side
864
+ accept at the end of the head of the pane it takes. */
865
+ .bc-merge-heads { flex-shrink: 0; display: flex; align-items: center; height: var(--ctl-lg); border-bottom: 1px solid var(--line); background: var(--surface); }
866
+ .bc-merge-head { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; height: 100%; padding: 0 var(--sp-2) 0 var(--sp-4); container: bc-head / inline-size; }
867
+ /* The first head starts on the window's gutter, like the bar title above it
868
+ and the first pane's text below it (one left edge). */
869
+ .bc-merge-head:first-child { padding-left: var(--gutter); }
870
+ .bc-merge-heads.bc-heads-2 > .bc-merge-head { width: 47%; }
871
+ .bc-merge-heads.bc-heads-2 > .bc-merge-heads-gap { width: 6%; }
872
+ .bc-merge-heads.bc-heads-3 > .bc-merge-head { width: 31%; }
873
+ .bc-merge-heads.bc-heads-3 > .bc-merge-heads-gap { width: 3.5%; }
874
+ .bc-head-label { flex-shrink: 0; white-space: nowrap; } /* type from the shared .bc-label rule */
875
+ .bc-head-title { flex: 0 1 auto; min-width: 6ch; height: var(--ctl-sm); padding: 0 var(--sp-2); border: none; border-radius: var(--r-ctl); background: transparent; color: var(--text);
876
+ font-family: inherit; font-size: var(--fs-meta); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;
877
+ transition: background-color var(--dur) var(--ease); }
878
+ .bc-head-title:hover { background: var(--hover); }
879
+ .bc-merge-head .bc-head-accept { margin-left: auto; flex-shrink: 0; } /* the shared .btn.quiet.sm.accent */
880
+ /* The accept says only "Accept" (its head names the side; the tooltip and
881
+ aria-label the whole action), and a title pick ellipsizes first, so the
882
+ default 3-pane window keeps the word. Only a head too narrow for label +
883
+ pick + "Accept" drops to the glyph. */
884
+ @container bc-head (max-width: 179px) {
885
+ .bc-merge-head:has(.bc-head-title) .bc-accept-label { display: none; }
886
+ }
887
+ @container bc-head (max-width: 139px) {
888
+ .bc-accept-label { display: none; }
889
+ }
890
+ /* ONE counter in the bar ("2 of 5 resolved", a text button to the next open
891
+ change) and a red conflict chip only while conflicts remain. */
892
+ .bc-chg-count { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-meta); color: var(--text-dim); margin-right: var(--sp-2); white-space: nowrap; }
893
+ .bc-chg-progress { height: var(--ctl-sm); padding: 0 var(--sp-2); border: none; border-radius: var(--r-ctl); background: transparent; color: var(--text-dim);
894
+ font-family: inherit; font-size: var(--fs-meta); cursor: pointer; transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
895
+ .bc-chg-progress:hover { color: var(--text); background: var(--hover); }
896
+ .bc-conflict-chip { border: none; background: var(--red-bg); color: var(--red); font-size: var(--fs-meta); font-family: inherit; height: var(--ctl-sm); padding: 0 var(--sp-3); border-radius: var(--r-chip); cursor: pointer; }
897
+ /* --- CodeMirror skin: tokens only, no stock palette. --- */
898
+ .bc-merge-host { flex: 1; min-height: 0; position: relative; }
899
+ /* Full-height chain: the addon's default is a fixed 350px and its panes have
900
+ AUTO height (so CodeMirror height:100% would resolve against auto and grow to
901
+ content — no internal scroll, the user's "can't scroll" bug). Pin every level
902
+ to 100% so each pane scrolls internally. */
903
+ .bc-merge-host .CodeMirror-merge,
904
+ .bc-merge-host .CodeMirror-merge-pane,
905
+ .bc-merge-host .CodeMirror-merge .CodeMirror { height: 100%; }
906
+ .bc-merge-host .CodeMirror-merge { border: 0; }
907
+ /* Every pane is the same plane (no read-only dimming: that changed the text,
908
+ the background and every highlight at once); the caret and the pane heads
909
+ say which pane takes edits. */
910
+ .bc-merge-host .CodeMirror { background: transparent; color: var(--text); font-family: var(--font-mono); font-size: var(--fs-text); line-height: var(--lh-text); }
911
+ .bc-merge-host .CodeMirror-cursor { border-left: 1.5px solid var(--text); }
912
+ /* Text edges: each pane's text starts where its head's label does (--sp-4),
913
+ the first pane's on the window gutter with the bar title and its head.
914
+ The -like rule is CodeMirror's measuring copy, so wrapping and the caret
915
+ follow the padding. */
916
+ .bc-merge-host .CodeMirror pre.CodeMirror-line, .bc-merge-host .CodeMirror pre.CodeMirror-line-like { padding-left: var(--sp-4); }
917
+ .bc-merge-host .CodeMirror-merge-pane:first-child .CodeMirror pre.CodeMirror-line,
918
+ .bc-merge-host .CodeMirror-merge-pane:first-child .CodeMirror pre.CodeMirror-line-like { padding-left: var(--gutter); }
919
+ .bc-merge-host .CodeMirror-selected { background: var(--accent-bg); }
920
+ /* The gap between panes holds the connectors and the gutter buttons: no fill,
921
+ no box, ONE hairline on the side that faces the read-only pane. */
922
+ .bc-merge-host .CodeMirror-merge-gap { background: transparent; border: 0; }
923
+ .bc-merge-host .CodeMirror-merge-gap:has(+ .CodeMirror-merge-pane-rightmost) { box-shadow: inset -1px 0 0 var(--line); }
924
+ .bc-merge-host .CodeMirror-merge-3pane .CodeMirror-merge-pane:first-child + .CodeMirror-merge-gap { box-shadow: inset 1px 0 0 var(--line); }
925
+ .bc-merge-host .CodeMirror-merge-scrolllock-wrap { display: none; } /* scroll stays locked; the toggle was an unexplained glyph */
926
+ /* Chunks: ONE green layer per change (the wash; no start/end hairlines, the
927
+ connector the same wash with no stroke). Inside it the word-level marks
928
+ (vendored bcWordDiff; a whole-line edit gets none) are a step stronger.
929
+ Conflicts go red on every pane that holds them, their marks too. */
930
+ .bc-merge-host .CodeMirror-merge-l-chunk, .bc-merge-host .CodeMirror-merge-r-chunk { background: var(--green-bg); }
931
+ .bc-merge-host .CodeMirror-merge-l-chunk-start, .bc-merge-host .CodeMirror-merge-r-chunk-start { border-top: 0; }
932
+ .bc-merge-host .CodeMirror-merge-l-chunk-end, .bc-merge-host .CodeMirror-merge-r-chunk-end { border-bottom: 0; }
933
+ .bc-merge-host .CodeMirror-merge-l-inserted, .bc-merge-host .CodeMirror-merge-r-inserted,
934
+ .bc-merge-host .CodeMirror-merge-l-deleted, .bc-merge-host .CodeMirror-merge-r-deleted {
935
+ background: color-mix(in srgb, var(--green) 28%, transparent); border-radius: var(--r-1); -webkit-box-decoration-break: clone; box-decoration-break: clone; text-decoration: none; }
936
+ .bc-merge-host .CodeMirror-merge-l-connect, .bc-merge-host .CodeMirror-merge-r-connect { fill: var(--green-bg); stroke: none; }
937
+ .bc-merge-host .CodeMirror-merge-l-connect.bc-conflict-connect, .bc-merge-host .CodeMirror-merge-r-connect.bc-conflict-connect { fill: var(--red-bg); }
938
+ .bc-merge-host .bc-conflict { background: var(--red-bg); }
939
+ .bc-merge-host .bc-conflict-line .CodeMirror-merge-l-inserted, .bc-merge-host .bc-conflict-line .CodeMirror-merge-r-inserted,
940
+ .bc-merge-host .bc-conflict-line .CodeMirror-merge-l-deleted, .bc-merge-host .bc-conflict-line .CodeMirror-merge-r-deleted { background: color-mix(in srgb, var(--red) 28%, transparent); }
941
+ /* Chunk-state paint from the wrapper: 'quiet' (whitespace-only) chunks lose the
942
+ green wash entirely; declined chunks dim their read-only text and get
943
+ a dashed hollow connector. */
944
+ .bc-merge-host .bc-quiet.CodeMirror-merge-l-chunk, .bc-merge-host .bc-quiet.CodeMirror-merge-r-chunk { background: transparent; }
945
+ .bc-merge-host .bc-dim-line { opacity: .45; }
946
+ .bc-merge-host .CodeMirror-merge-l-connect.bc-declined,
947
+ .bc-merge-host .CodeMirror-merge-r-connect.bc-declined { fill: transparent; stroke: var(--line-strong); stroke-dasharray: 3 3; }
948
+ /* The gutter buttons (vendored merge.js: Material glyphs): the arrow pulls a
949
+ chunk into the result, the x below it dismisses it. Quiet 20px icon
950
+ buttons, centred in the gap (a 3-pane gap is about one button wide). */
951
+ .bc-merge-host .CodeMirror-merge-gap .CodeMirror-merge-copy, .bc-merge-host .CodeMirror-merge-gap .CodeMirror-merge-copy-reverse {
952
+ display: flex; align-items: center; justify-content: center; width: var(--ctl-sm); height: var(--ctl-sm); left: 50%; right: auto;
953
+ margin-left: calc(var(--ctl-sm) / -2); border-radius: var(--r-ctl); color: var(--text-dim); font-size: var(--icon-sm);
954
+ transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease); }
955
+ .bc-merge-host .CodeMirror-merge-gap .CodeMirror-merge-copy:hover { color: var(--accent); background: var(--hover); }
956
+ .bc-merge-host .CodeMirror-merge-gap .bc-decline { margin-top: var(--ctl-sm); }
957
+ .bc-merge-host .CodeMirror-merge-gap .bc-decline:hover { color: var(--red); }
958
+ /* Collapsed identical stretch ("12 unchanged lines"): quiet text, no box;
959
+ a hover fill only, like an icon button. */
960
+ .bc-merge-host .CodeMirror-merge-collapsed-widget { background: transparent; border: none; border-radius: var(--r-ctl); color: var(--text-dim); font-family: var(--font-sans); font-size: var(--fs-meta); padding: 0 var(--sp-3); cursor: pointer; }
961
+ .bc-merge-host .CodeMirror-merge-collapsed-widget:hover { color: var(--text); background: var(--hover); }
962
+ .bc-merge-host .CodeMirror-merge-collapsed-widget:before { content: "\22EF\00A0"; }
963
+ .bc-merge-note { flex-shrink: 0; display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--gutter); background: var(--accent-bg); color: var(--text); font-size: var(--fs-ui); border-bottom: 1px solid var(--line); }
964
+ .bc-merge-note[hidden] { display: none; }
965
+ .bc-merge-note .btn { flex-shrink: 0; } /* the shared .btn.quiet.sm.accent */
966
+ /* Footer: ONE row in every mode (unify, conflict, 2- or 3-pane), centred:
967
+ Keep both, Remove conflict (conflicts only), the primary. The whole-side
968
+ accepts live in the pane heads, so nothing here jumps between modes.
969
+ Footer buttons are the shared .btn set; the primary label changes on a
970
+ Unify's last step, and a fixed width keeps it from reflowing under a cursor
971
+ that is about to click. */
972
+ .bc-reconcile-actions { position: relative; flex-shrink: 0; display: flex; align-items: center; justify-content: center; gap: var(--sp-3); padding: var(--sp-3) var(--gutter);
973
+ border-top: 1px solid var(--line); background: var(--surface); }
974
+ .bc-reconcile-actions .btn.primary { min-width: 10.5em; }
975
+ /* A multi-step Unify's "Step 2 of 3": on the footer's left gutter, out of the
976
+ flow, so the buttons stay centred and never move between steps. */
977
+ .bc-step { position: absolute; left: var(--gutter); top: 50%; transform: translateY(-50%); font-size: var(--fs-meta); color: var(--text-dim); white-space: nowrap; font-variant-numeric: tabular-nums; }
978
+
979
+ /* --- Multi-select: checked-row tint + slim contextual toolbar --- */
980
+ /* `.selected` is the keyboard cursor (--accent-bg, defined with the row); a
981
+ checked row (Ctrl/Shift-click) adds the accent edge so it reads as part of
982
+ the bulk set even when it isn't the cursor; the cursor inside the set is a
983
+ step stronger. */
984
+ .item.multi-selected { background: var(--accent-bg); --row-edge: var(--accent); }
985
+ .item.multi-selected:hover, .item.selected.multi-selected { background: var(--accent-bg-strong); }
986
+ /* The selection bar takes the chip bar's place while 2+ clips are selected:
987
+ both sit in ONE grid cell (.chip-row), the chips go invisible, the cell's
988
+ height is the taller of the two, so nothing below moves; no band, no tint,
989
+ no border (the checked rows carry the only accent). The cell is never lower
990
+ than a populated chip bar, so an empty one (no pins, keys, images or groups
991
+ yet) does not grow when the selection bar or the first chip shows. */
992
+ .chip-row { display: grid; min-height: max(var(--ctl-md), calc(var(--ctl-sm) + 2 * var(--sp-1))); }
993
+ .chip-row > .group-filters, .chip-row > .selection-bar { grid-area: 1 / 1; }
994
+ .chip-row:has(> .selection-bar:not(.hidden)) > .group-filters { visibility: hidden; }
995
+ .selection-bar { display: flex; align-items: center; align-self: start; gap: var(--sp-3); min-height: var(--ctl-md); padding: 0 var(--edge-ctl) 0 var(--gutter); z-index: 31; }
996
+ .selection-bar.hidden { display: none; }
997
+ .selection-count { flex: 1; font-size: var(--fs-ui); font-weight: var(--fw-strong); color: var(--text); }
998
+ .selection-actions { display: flex; align-items: center; gap: var(--sp-1); }
999
+
1000
+ /* Rows are click-to-act (click pastes, shift/ctrl-click selects) — native text
1001
+ selection inside them is never meaningful and shift-click was smearing a text
1002
+ highlight across rows. The editor is the copy-a-fragment surface. */
1003
+ .list .item { user-select: none; -webkit-user-select: none; }
1004
+
1005
+ /* Submenu auto-flip (set by installSubmenuAutoflip when a submenu would leave
1006
+ the window): side-opening submenus open leftward. */
1007
+ .tag-menu-node > .tag-submenu.flip-x { left: auto; right: calc(100% - 1px); }
1008
+
1009
+ /* --- Shared popover menu: the per-clip "..." menu, the bulk menu, a row's
1010
+ keypad / group popovers, the editor + viewer menus. ONE row primitive
1011
+ (Core.menuRowHtml): icon column, label, optional hint / caret, --ctl-md tall,
1012
+ contiguous. Every submenu and group checklist uses the same row, so labels
1013
+ line up across levels. Membership = the accent check glyph in the icon
1014
+ column (a dash for "some of the selection"), never a row fill; --text-dim is
1015
+ for disabled rows only. Keyboard focus paints like hover (a menu has one
1016
+ active row), danger rows turn red on hover / focus. --- */
1017
+ .bc-menu { position: absolute; z-index: 200; min-width: 176px; max-width: 260px; } /* surface look comes from the shared floating-surface rule */
1018
+ .bc-menu-list { display: flex; flex-direction: column; }
1019
+ .bc-menu-item { display: flex; align-items: center; gap: var(--sp-3); width: 100%; min-height: var(--ctl-md); box-sizing: border-box; padding: 0 var(--sp-3);
1020
+ background: none; border: none; border-radius: var(--r-1); color: var(--text); font-family: inherit; font-size: var(--fs-ui); line-height: var(--lh-ui);
1021
+ text-align: left; cursor: pointer; position: relative; }
1022
+ .bc-menu-item:hover, .bc-menu .bc-menu-item:focus-visible { background: var(--menu-hover); box-shadow: none; }
1023
+ .bc-menu-item > .mi { width: 1em; font-size: var(--icon-md); color: var(--text-dim); flex-shrink: 0; }
1024
+ .bc-menu-item > .bc-menu-check { color: var(--accent); }
1025
+ .bc-menu-item.danger:hover, .bc-menu-item.danger:focus-visible { background: var(--red-bg); }
1026
+ .bc-menu-item.danger:hover, .bc-menu-item.danger:hover > .mi, .bc-menu-item.danger:focus-visible, .bc-menu-item.danger:focus-visible > .mi { color: var(--red); }
1027
+ .bc-menu-item:disabled { color: var(--text-dim); cursor: default; }
1028
+ .bc-menu-item:disabled:hover { background: none; }
1029
+ .bc-menu-label { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1030
+ .bc-menu-hint { flex-shrink: 0; color: var(--text-dim); font-size: var(--fs-meta); font-variant-numeric: tabular-nums; }
1031
+ .bc-menu-item > .tag-caret { margin-left: 0; font-size: var(--icon-sm); color: var(--text-dim); opacity: 1; }
1032
+ .bc-menu-sep { height: 1px; margin: var(--sp-2) 0; background: var(--line); }
1033
+ /* A submenu opens beside its row (the row and its .tag-submenu share one
1034
+ .tag-menu-node): on hover, or .open from the keyboard (Right / Enter). Focus
1035
+ on a parent row alone does not open it. */
1036
+ .bc-menu .tag-submenu { top: -4px; left: calc(100% - 1px); min-width: 150px; }
1037
+ .bc-menu .tag-menu-node.open > .tag-submenu { display: block; }
1038
+ .bc-menu .tag-menu-node:hover > .bc-menu-item, .bc-menu .tag-menu-node.open > .bc-menu-item { background: var(--menu-hover); } /* the row whose submenu is out */
1039
+ .bc-menu .tag-menu-node:not(.open):not(:hover) > .tag-submenu { display: none; }
1040
+
1041
+ /* --- Undo toast action: the shared accent text action (.btn.quiet.sm.accent),
1042
+ pulled in by its own padding. The toast itself ignores the pointer (it
1043
+ floats over the last row); the action opts back in only while the toast is
1044
+ shown (hidden, it still sits over the last row). --- */
1045
+ .toast-action { margin-right: calc(-1 * var(--sp-3)); }
1046
+ .toast.show .toast-action { pointer-events: auto; }