@3sln/trove 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1227 -0
- package/package.json +75 -0
- package/packages/core/src/collections/index.js +249 -0
- package/packages/core/src/errors.js +186 -0
- package/packages/core/src/identity/discovery.js +210 -0
- package/packages/core/src/identity/index.js +188 -0
- package/packages/core/src/identity/jwt.js +199 -0
- package/packages/core/src/index.js +104 -0
- package/packages/core/src/indexers/contribution.js +115 -0
- package/packages/core/src/indexers/registry.js +162 -0
- package/packages/core/src/indexing.js +340 -0
- package/packages/core/src/issues.js +150 -0
- package/packages/core/src/kv.js +0 -0
- package/packages/core/src/links.js +141 -0
- package/packages/core/src/metadata/cursor.js +73 -0
- package/packages/core/src/metadata/interface.js +244 -0
- package/packages/core/src/metadata/memory.js +270 -0
- package/packages/core/src/metadata/sqlite.js +412 -0
- package/packages/core/src/notifications/index.js +139 -0
- package/packages/core/src/notifications/webpush.js +217 -0
- package/packages/core/src/plugins/contributions.js +177 -0
- package/packages/core/src/plugins/identity.js +98 -0
- package/packages/core/src/plugins/index.js +225 -0
- package/packages/core/src/plugins/indexers.js +142 -0
- package/packages/core/src/plugins/installStore.js +134 -0
- package/packages/core/src/plugins/package.js +102 -0
- package/packages/core/src/plugins/packageStore.js +61 -0
- package/packages/core/src/plugins/runtime.js +101 -0
- package/packages/core/src/plugins/sql.js +52 -0
- package/packages/core/src/retry.js +74 -0
- package/packages/core/src/scan.js +302 -0
- package/packages/core/src/search/embeddings.js +128 -0
- package/packages/core/src/search/index.js +200 -0
- package/packages/core/src/search/keywordStore.js +107 -0
- package/packages/core/src/search/sqliteStores.js +455 -0
- package/packages/core/src/search/tagMatch.js +59 -0
- package/packages/core/src/search/transformer.js +195 -0
- package/packages/core/src/search/vectorStore.js +274 -0
- package/packages/core/src/search/vectorize.js +249 -0
- package/packages/core/src/sidecar/document.js +213 -0
- package/packages/core/src/sidecar/index.js +174 -0
- package/packages/core/src/sidecar/manager.js +239 -0
- package/packages/core/src/sidecar/store.js +46 -0
- package/packages/core/src/signedUrls.js +170 -0
- package/packages/core/src/sqlite-d1.js +162 -0
- package/packages/core/src/sqlite-driver.js +42 -0
- package/packages/core/src/sqlite.js +162 -0
- package/packages/core/src/storage/filesystem.js +283 -0
- package/packages/core/src/storage/interface.js +222 -0
- package/packages/core/src/storage/memory.js +113 -0
- package/packages/core/src/storage/prefixed.js +75 -0
- package/packages/core/src/storage/s3.js +316 -0
- package/packages/core/src/storage/s3sigv4.js +185 -0
- package/packages/core/src/tasks.js +228 -0
- package/packages/core/src/uploads.js +386 -0
- package/packages/core/src/util.js +125 -0
- package/packages/core/src/vfs.js +666 -0
- package/packages/plugin-sdk/src/browser.js +316 -0
- package/packages/plugin-sdk/src/index.js +32 -0
- package/packages/plugin-sdk/src/protocol.js +59 -0
- package/packages/plugin-sdk/src/rpc.js +95 -0
- package/packages/server/src/adapters/bun.js +78 -0
- package/packages/server/src/adapters/node.js +115 -0
- package/packages/server/src/adapters/staticAssets.js +123 -0
- package/packages/server/src/adapters/webDist.js +70 -0
- package/packages/server/src/adapters/worker-tasks.js +206 -0
- package/packages/server/src/adapters/worker.js +159 -0
- package/packages/server/src/cachePolicy.js +34 -0
- package/packages/server/src/engine/README.md +88 -0
- package/packages/server/src/engine/actions/scanCollection.js +114 -0
- package/packages/server/src/engine/index.js +95 -0
- package/packages/server/src/engine/lazy.js +25 -0
- package/packages/server/src/engine/providers/access.js +363 -0
- package/packages/server/src/engine/providers/core.js +405 -0
- package/packages/server/src/engine/providers/scan.js +67 -0
- package/packages/server/src/index.js +698 -0
- package/packages/server/src/manifest.js +98 -0
- package/packages/server/src/mcp/auth.js +40 -0
- package/packages/server/src/mcp/index.js +213 -0
- package/packages/server/src/mcp/protocol.js +181 -0
- package/packages/server/src/mcp/tools.js +351 -0
- package/packages/server/src/router.js +229 -0
- package/packages/server/src/routes.js +1066 -0
- package/packages/server/src/scope.js +43 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js +5 -0
- package/packages/web/dist/assets/chunk-4xqbzebh.js.map +9 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js +5 -0
- package/packages/web/dist/assets/chunk-h05bxfbs.js.map +10 -0
- package/packages/web/dist/assets/main-4cxs7prw.js +356 -0
- package/packages/web/dist/assets/main-4cxs7prw.js.map +103 -0
- package/packages/web/dist/assets/styles-kcx1x337.css +1 -0
- package/packages/web/dist/icon.svg +11 -0
- package/packages/web/dist/index.html +16 -0
- package/packages/web/dist/sql-wasm.wasm +0 -0
- package/packages/web/dist/sw.js +186 -0
- package/packages/web/src/bl/actions.js +410 -0
- package/packages/web/src/bl/activity.js +306 -0
- package/packages/web/src/bl/commands.js +274 -0
- package/packages/web/src/bl/fileType.js +49 -0
- package/packages/web/src/bl/index.js +70 -0
- package/packages/web/src/bl/links.js +54 -0
- package/packages/web/src/bl/offline.js +268 -0
- package/packages/web/src/bl/openers.js +71 -0
- package/packages/web/src/bl/pluginInstall.js +59 -0
- package/packages/web/src/bl/services.js +143 -0
- package/packages/web/src/bl/social.js +234 -0
- package/packages/web/src/bl/tagQuery.js +44 -0
- package/packages/web/src/main.js +10 -0
- package/packages/web/src/platform/api.js +529 -0
- package/packages/web/src/platform/commands.js +89 -0
- package/packages/web/src/platform/context.js +77 -0
- package/packages/web/src/platform/contributions.js +156 -0
- package/packages/web/src/platform/index.js +150 -0
- package/packages/web/src/platform/keybindings.js +199 -0
- package/packages/web/src/platform/mediaUrls.js +137 -0
- package/packages/web/src/platform/navigation.js +131 -0
- package/packages/web/src/platform/notifications.js +50 -0
- package/packages/web/src/platform/overlay.js +81 -0
- package/packages/web/src/platform/pluginClientDb.js +132 -0
- package/packages/web/src/platform/pluginDock.js +141 -0
- package/packages/web/src/platform/pluginFrames.js +194 -0
- package/packages/web/src/platform/pluginHost.js +648 -0
- package/packages/web/src/platform/pluginMedia.js +62 -0
- package/packages/web/src/platform/pluginModules.js +90 -0
- package/packages/web/src/platform/pluginNet.js +71 -0
- package/packages/web/src/platform/pluginPackage.js +247 -0
- package/packages/web/src/platform/pluginRpc.js +377 -0
- package/packages/web/src/platform/pluginSigning.js +168 -0
- package/packages/web/src/platform/pluginStore.js +67 -0
- package/packages/web/src/platform/settings.js +101 -0
- package/packages/web/src/platform/spatialNav.js +286 -0
- package/packages/web/src/platform/viewport.js +123 -0
- package/packages/web/src/platform/voice.js +133 -0
- package/packages/web/src/platform/voiceSearch.js +155 -0
- package/packages/web/src/platform/whenclause.js +162 -0
- package/packages/web/src/platform/workbench.js +156 -0
- package/packages/web/src/runtime.js +73 -0
- package/packages/web/src/styles.css +1382 -0
- package/packages/web/src/ui/components/activityBar.js +35 -0
- package/packages/web/src/ui/components/activityPanel.js +132 -0
- package/packages/web/src/ui/components/commandPalette.js +154 -0
- package/packages/web/src/ui/components/editorArea.js +75 -0
- package/packages/web/src/ui/components/launcher.js +392 -0
- package/packages/web/src/ui/components/openers/index.js +212 -0
- package/packages/web/src/ui/components/openers/markdown.js +222 -0
- package/packages/web/src/ui/components/overlays.js +255 -0
- package/packages/web/src/ui/components/phoneChrome.js +188 -0
- package/packages/web/src/ui/components/pluginReview.js +151 -0
- package/packages/web/src/ui/components/pluginsView.js +120 -0
- package/packages/web/src/ui/components/settingsView.js +258 -0
- package/packages/web/src/ui/components/social.js +290 -0
- package/packages/web/src/ui/components/statusBar.js +198 -0
- package/packages/web/src/ui/components/views/grid.js +115 -0
- package/packages/web/src/ui/components/views/index.js +155 -0
- package/packages/web/src/ui/components/views/list.js +50 -0
- package/packages/web/src/ui/components/views/parts.js +58 -0
- package/packages/web/src/ui/compositions/workbench.js +125 -0
- package/packages/web/src/ui/format.js +33 -0
- package/packages/web/src/ui/icon.js +81 -0
- package/packages/web/src/ui/media.js +114 -0
- package/packages/web/src/ui/sanitize.js +86 -0
- package/packages/web/src/workbench.js +205 -0
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
/* Trove workbench theme. Three palettes via [data-theme]; everything else keys
|
|
2
|
+
off the CSS custom properties so components never hardcode a color. Tuned for
|
|
3
|
+
a calm, dense, professional feel — a real tool, not a toy. */
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
7
|
+
--font-mono: 'SF Mono', 'JetBrains Mono', 'Fira Code', ui-monospace, Menlo, Consolas, monospace;
|
|
8
|
+
--radius: 8px;
|
|
9
|
+
--radius-sm: 5px;
|
|
10
|
+
--activity-w: 52px;
|
|
11
|
+
--sidebar-w: 280px;
|
|
12
|
+
--statusbar-h: 24px;
|
|
13
|
+
--ease: cubic-bezier(0.2, 0.7, 0.2, 1);
|
|
14
|
+
/* Focus is one colour and one halo everywhere, so a keyboard user learns a single
|
|
15
|
+
cue rather than a different one per component. */
|
|
16
|
+
--focus: var(--accent);
|
|
17
|
+
--focus-halo: color-mix(in srgb, var(--accent) 32%, transparent);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
:root, :root[data-theme='dark'] {
|
|
21
|
+
--bg: #181a1f;
|
|
22
|
+
--bg-elevated: #1f2228;
|
|
23
|
+
--bg-sidebar: #1b1d23;
|
|
24
|
+
--bg-activity: #16181d;
|
|
25
|
+
--bg-input: #12141a;
|
|
26
|
+
--bg-hover: #ffffff0d;
|
|
27
|
+
--bg-active: #ffffff14;
|
|
28
|
+
--bg-selected: #2d6fd128;
|
|
29
|
+
--border: #ffffff14;
|
|
30
|
+
--border-strong: #ffffff26;
|
|
31
|
+
--text: #d7dce5;
|
|
32
|
+
--text-dim: #9aa4b2;
|
|
33
|
+
--text-faint: #6b7280;
|
|
34
|
+
--accent: #6ea8fe;
|
|
35
|
+
--accent-2: #a679f0;
|
|
36
|
+
--accent-text: #0b1020;
|
|
37
|
+
--danger: #f2607a;
|
|
38
|
+
--warn: #e5b567;
|
|
39
|
+
--success: #6cc48a;
|
|
40
|
+
--shadow: 0 12px 40px -8px #000000a8, 0 2px 8px -2px #0008;
|
|
41
|
+
--scrollbar: #ffffff22;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:root[data-theme='midnight'] {
|
|
45
|
+
--bg: #0c0e14;
|
|
46
|
+
--bg-elevated: #12151e;
|
|
47
|
+
--bg-sidebar: #0e111a;
|
|
48
|
+
--bg-activity: #090b11;
|
|
49
|
+
--bg-input: #080a10;
|
|
50
|
+
--bg-hover: #ffffff0a;
|
|
51
|
+
--bg-active: #ffffff12;
|
|
52
|
+
--bg-selected: #4b7bff22;
|
|
53
|
+
--border: #ffffff10;
|
|
54
|
+
--border-strong: #ffffff1f;
|
|
55
|
+
--text: #cdd6f4;
|
|
56
|
+
--text-dim: #8b95b0;
|
|
57
|
+
--text-faint: #5a6482;
|
|
58
|
+
--accent: #89b4fa;
|
|
59
|
+
--accent-2: #cba6f7;
|
|
60
|
+
--danger: #f38ba8;
|
|
61
|
+
--warn: #f9e2af;
|
|
62
|
+
--success: #a6e3a1;
|
|
63
|
+
--shadow: 0 16px 50px -10px #000000cc, 0 2px 10px -2px #000a;
|
|
64
|
+
--scrollbar: #ffffff1a;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:root[data-theme='light'] {
|
|
68
|
+
--bg: #f6f7f9;
|
|
69
|
+
--bg-elevated: #ffffff;
|
|
70
|
+
--bg-sidebar: #eef0f3;
|
|
71
|
+
--bg-activity: #e6e9ee;
|
|
72
|
+
--bg-input: #ffffff;
|
|
73
|
+
--bg-hover: #0000000a;
|
|
74
|
+
--bg-active: #00000012;
|
|
75
|
+
--bg-selected: #2d6fd11f;
|
|
76
|
+
--border: #00000014;
|
|
77
|
+
--border-strong: #00000024;
|
|
78
|
+
--text: #1c2430;
|
|
79
|
+
--text-dim: #55606f;
|
|
80
|
+
--text-faint: #8a95a4;
|
|
81
|
+
--accent: #2d6fd1;
|
|
82
|
+
--accent-2: #7c50c8;
|
|
83
|
+
--accent-text: #ffffff;
|
|
84
|
+
--danger: #d63b5b;
|
|
85
|
+
--warn: #b3801f;
|
|
86
|
+
--success: #2f9e57;
|
|
87
|
+
--shadow: 0 12px 40px -10px #0000002e, 0 2px 8px -3px #0000001f;
|
|
88
|
+
--scrollbar: #00000026;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
* { box-sizing: border-box; }
|
|
92
|
+
html, body { height: 100%; margin: 0; }
|
|
93
|
+
/* No grey flash on tap. Mobile WebKit and Chrome paint a translucent block over any
|
|
94
|
+
element it considers clickable the moment a finger lands on it — a rectangle that
|
|
95
|
+
ignores border-radius, so a round icon button flashes square, and a row flashes
|
|
96
|
+
before its own :active styling has a chance to say anything. Every control here
|
|
97
|
+
already answers a press with its own hover/active vocabulary, so the browser's
|
|
98
|
+
version is a second, uglier one. Set on the elements themselves rather than just
|
|
99
|
+
the root: the property inherits, but a UA stylesheet rule on buttons and links
|
|
100
|
+
overrides an inherited value. */
|
|
101
|
+
* , *::before, *::after { -webkit-tap-highlight-color: transparent; }
|
|
102
|
+
body {
|
|
103
|
+
font-family: var(--font-ui);
|
|
104
|
+
font-size: 13px;
|
|
105
|
+
color: var(--text);
|
|
106
|
+
background: var(--bg);
|
|
107
|
+
-webkit-font-smoothing: antialiased;
|
|
108
|
+
text-rendering: optimizeLegibility;
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
}
|
|
111
|
+
/* A full reset, not a partial one. Inheriting only font/colour left every button that
|
|
112
|
+
didn't name its own background wearing the UA's `ButtonFace` — a light-grey 3D box
|
|
113
|
+
with `var(--text)` on it, i.e. light-on-light in the dark theme. It stayed invisible
|
|
114
|
+
for as long as every button in the app happened to be styled, and surfaced the moment
|
|
115
|
+
one wasn't: turning context-menu rows into real <button>s made every menu in the app
|
|
116
|
+
unreadable. `.btn` and friends set their own background and border below. */
|
|
117
|
+
button {
|
|
118
|
+
font: inherit; color: inherit; cursor: pointer;
|
|
119
|
+
background: none; border: 0; text-align: inherit;
|
|
120
|
+
}
|
|
121
|
+
input, select, textarea { font: inherit; color: inherit; }
|
|
122
|
+
::selection { background: var(--accent); color: var(--accent-text); }
|
|
123
|
+
|
|
124
|
+
/* ---- Keyboard focus -------------------------------------------------------
|
|
125
|
+
Keyboard only, never on a mouse click. Two rules govern everything below.
|
|
126
|
+
|
|
127
|
+
A focus style may not change an element's SHAPE. The old base rule set
|
|
128
|
+
`border-radius: 3px`, which snapped every pill button and rounded row to near-square
|
|
129
|
+
the moment it was focused — a jolt that reads as breakage rather than as a cursor.
|
|
130
|
+
|
|
131
|
+
And a ring is the FALLBACK, not the goal. Where an element can show focus in its own
|
|
132
|
+
vocabulary — a field that lights up, a row that gets the same accent bar as the
|
|
133
|
+
selected one — that reads better and is never removed without a replacement. Anything
|
|
134
|
+
not named below still gets the ring, which is the point of keeping it. */
|
|
135
|
+
:focus-visible {
|
|
136
|
+
outline: 2px solid var(--focus);
|
|
137
|
+
outline-offset: 2px;
|
|
138
|
+
/* deliberately no border-radius: outline already follows the element's own */
|
|
139
|
+
}
|
|
140
|
+
:focus:not(:focus-visible) { outline: none; }
|
|
141
|
+
|
|
142
|
+
/* Buttons: the border becomes the indicator, with a soft halo around it. Hugs whatever
|
|
143
|
+
radius the button already has, because a box-shadow ring always does. */
|
|
144
|
+
.btn:focus-visible {
|
|
145
|
+
outline: none;
|
|
146
|
+
border-color: var(--focus);
|
|
147
|
+
box-shadow: 0 0 0 3px var(--focus-halo);
|
|
148
|
+
}
|
|
149
|
+
.btn.primary:focus-visible { box-shadow: 0 0 0 3px var(--focus-halo); }
|
|
150
|
+
|
|
151
|
+
/* Icon-only buttons have no border to light up, so the halo does the work. */
|
|
152
|
+
.iconbtn:focus-visible, .act-action:focus-visible, .launch-clear:focus-visible,
|
|
153
|
+
.pb-status:focus-visible, .pb-brand:focus-visible, .vn-back:focus-visible,
|
|
154
|
+
.c-link:focus-visible, .react:focus-visible {
|
|
155
|
+
outline: none;
|
|
156
|
+
color: var(--text);
|
|
157
|
+
background: var(--bg-active);
|
|
158
|
+
box-shadow: 0 0 0 2px var(--focus-halo);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* The search field. The input itself is borderless — the BOX around it is the control,
|
|
162
|
+
so that is what lights up. Without this, tabbing into the drive's primary input
|
|
163
|
+
showed nothing at all: the input suppressed its own outline and nothing replaced it. */
|
|
164
|
+
.launch-box:focus-within {
|
|
165
|
+
border-color: var(--focus);
|
|
166
|
+
box-shadow: var(--shadow), 0 0 0 3px var(--focus-halo);
|
|
167
|
+
}
|
|
168
|
+
.launch-input:focus-visible { outline: none; }
|
|
169
|
+
|
|
170
|
+
/* A file row. It already has a vocabulary for "this one" — the selected background —
|
|
171
|
+
so focus uses it plus an accent edge, which distinguishes keyboard focus from the
|
|
172
|
+
launcher's own highlighted index without adding a second visual language. */
|
|
173
|
+
.launch-item:focus-visible, .sheet-row:focus-visible, .inbox-item:focus-visible {
|
|
174
|
+
outline: none;
|
|
175
|
+
background: var(--bg-selected);
|
|
176
|
+
box-shadow: inset 3px 0 0 0 var(--focus), 0 0 0 1px var(--focus-halo);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Rail items already draw an accent bar when active; focus borrows it. */
|
|
180
|
+
.activitybar .item:focus-visible {
|
|
181
|
+
outline: none;
|
|
182
|
+
color: var(--text);
|
|
183
|
+
background: var(--bg-active);
|
|
184
|
+
/* The halo is what makes focus legible on the item that is ALREADY active — without
|
|
185
|
+
it, focusing the current destination changed nothing at all. */
|
|
186
|
+
box-shadow: 0 0 0 2px var(--focus-halo);
|
|
187
|
+
}
|
|
188
|
+
.activitybar .item:focus-visible::before {
|
|
189
|
+
content: ''; position: absolute; left: -6px; top: 8px; bottom: 8px; width: 3px;
|
|
190
|
+
border-radius: 0 3px 3px 0; background: linear-gradient(var(--accent), var(--accent-2));
|
|
191
|
+
}
|
|
192
|
+
.activitybar .brand-mark:focus-visible { outline: none; background: var(--bg-active); box-shadow: 0 0 0 2px var(--focus-halo); }
|
|
193
|
+
|
|
194
|
+
/* Phone tabs: a bar above the icon, matching where the eye already looks for "current". */
|
|
195
|
+
.pb-tab:focus-visible {
|
|
196
|
+
outline: none;
|
|
197
|
+
color: var(--accent);
|
|
198
|
+
box-shadow: inset 0 3px 0 0 var(--focus);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Status-bar segments are short and inline; a filled background reads better than a
|
|
202
|
+
ring squeezed between neighbours. */
|
|
203
|
+
.statusbar .seg:focus-visible {
|
|
204
|
+
outline: none;
|
|
205
|
+
background: var(--bg-active);
|
|
206
|
+
color: var(--text);
|
|
207
|
+
box-shadow: inset 0 -2px 0 0 var(--focus);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Text inside prose keeps a ring — there is no box to light up — but offset so it
|
|
211
|
+
clears the descenders instead of cutting through them. */
|
|
212
|
+
.md a:focus-visible, .link:focus-visible, .launch-up:focus-visible {
|
|
213
|
+
outline: 2px solid var(--focus);
|
|
214
|
+
outline-offset: 3px;
|
|
215
|
+
border-radius: 3px;
|
|
216
|
+
}
|
|
217
|
+
.lh-example:focus-visible {
|
|
218
|
+
outline: none;
|
|
219
|
+
border-color: var(--focus);
|
|
220
|
+
box-shadow: 0 0 0 3px var(--focus-halo);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
::-webkit-scrollbar { width: 11px; height: 11px; }
|
|
224
|
+
::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 6px; border: 3px solid transparent; background-clip: content-box; }
|
|
225
|
+
::-webkit-scrollbar-thumb:hover { background: var(--border-strong); background-clip: content-box; }
|
|
226
|
+
|
|
227
|
+
.workbench { height: 100%; }
|
|
228
|
+
|
|
229
|
+
/* ---- Shell layout -------------------------------------------------------- */
|
|
230
|
+
.shell {
|
|
231
|
+
display: grid;
|
|
232
|
+
grid-template-rows: 1fr var(--statusbar-h);
|
|
233
|
+
height: 100vh;
|
|
234
|
+
height: 100dvh;
|
|
235
|
+
}
|
|
236
|
+
.body {
|
|
237
|
+
display: grid;
|
|
238
|
+
grid-template-columns: var(--activity-w) 1fr;
|
|
239
|
+
min-height: 0;
|
|
240
|
+
overflow: hidden;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
kbd {
|
|
244
|
+
font-family: var(--font-ui); font-size: 10.5px; color: var(--text-dim);
|
|
245
|
+
background: var(--bg-hover); border: 1px solid var(--border);
|
|
246
|
+
border-radius: 4px; padding: 1px 5px; min-width: 16px; text-align: center;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* A shortcut you can click to change. It looks like the label it replaces until you
|
|
250
|
+
reach it, then says plainly that it is waiting for keys. */
|
|
251
|
+
.kbd-edit {
|
|
252
|
+
display: inline-flex; align-items: center; gap: 6px; padding: 2px 4px;
|
|
253
|
+
border: 1px solid transparent; border-radius: 6px; background: none;
|
|
254
|
+
color: var(--text-dim); font: inherit; cursor: pointer;
|
|
255
|
+
}
|
|
256
|
+
.kbd-edit:hover { border-color: var(--border); background: var(--bg-hover); }
|
|
257
|
+
.kbd-edit:focus-visible { outline: none; border-color: var(--focus); box-shadow: 0 0 0 3px var(--focus-halo); }
|
|
258
|
+
.kbd-clash { display: inline-flex; align-items: center; color: var(--warn); margin-right: 4px; }
|
|
259
|
+
.kbd-edit.clash kbd { border-color: color-mix(in srgb, var(--warn) 50%, transparent); color: var(--warn); }
|
|
260
|
+
.kbd-edit.listening {
|
|
261
|
+
border-color: var(--accent); background: var(--bg-active); color: var(--accent);
|
|
262
|
+
font-size: 11px; padding: 3px 8px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* ---- Activity bar (the left rail) --------------------------------------- */
|
|
266
|
+
.activitybar {
|
|
267
|
+
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
|
268
|
+
padding: 8px 0 10px; background: var(--bg-activity); border-right: 1px solid var(--border);
|
|
269
|
+
}
|
|
270
|
+
.activitybar .brand-mark {
|
|
271
|
+
width: 40px; height: 40px; border: 0; background: transparent; border-radius: 10px;
|
|
272
|
+
display: grid; place-items: center; margin-bottom: 6px; cursor: pointer;
|
|
273
|
+
transition: background .12s var(--ease), transform .12s var(--ease);
|
|
274
|
+
}
|
|
275
|
+
.activitybar .brand-mark img { width: 24px; height: 24px; }
|
|
276
|
+
.activitybar .brand-mark:hover { background: var(--bg-hover); }
|
|
277
|
+
.activitybar .brand-mark:active { transform: translateY(1px); }
|
|
278
|
+
.activitybar .item {
|
|
279
|
+
position: relative; width: 40px; height: 40px; border: 0; background: transparent;
|
|
280
|
+
color: var(--text-faint); border-radius: 9px; display: grid; place-items: center;
|
|
281
|
+
transition: color .12s var(--ease), background .12s var(--ease);
|
|
282
|
+
}
|
|
283
|
+
.activitybar .item:hover { color: var(--text); background: var(--bg-hover); }
|
|
284
|
+
.activitybar .item.active { color: var(--text); background: var(--bg-active); }
|
|
285
|
+
.activitybar .item.active::before {
|
|
286
|
+
content: ''; position: absolute; left: -6px; top: 8px; bottom: 8px; width: 3px;
|
|
287
|
+
border-radius: 0 3px 3px 0; background: linear-gradient(var(--accent), var(--accent-2));
|
|
288
|
+
}
|
|
289
|
+
.activitybar .item .badge {
|
|
290
|
+
position: absolute; top: 3px; right: 3px; min-width: 15px; height: 15px; padding: 0 3px;
|
|
291
|
+
background: var(--accent); color: var(--accent-text); border-radius: 8px;
|
|
292
|
+
font-size: 9.5px; font-weight: 700; display: grid; place-items: center;
|
|
293
|
+
}
|
|
294
|
+
.activitybar .spacer { flex: 1; }
|
|
295
|
+
/* Bottom-cluster controls (notifications, principal) styled to match nav items. */
|
|
296
|
+
.activitybar .bell-wrap { display: grid; place-items: center; }
|
|
297
|
+
.activitybar .iconbtn { width: 40px; height: 40px; border-radius: 9px; color: var(--text-faint); }
|
|
298
|
+
.activitybar .iconbtn:hover { color: var(--text); background: var(--bg-hover); }
|
|
299
|
+
.activitybar .principal { margin: 4px 0 0; cursor: default; }
|
|
300
|
+
|
|
301
|
+
/* ---- Sidebar ------------------------------------------------------------- */
|
|
302
|
+
.sidebar {
|
|
303
|
+
width: var(--sidebar-w); min-width: 0; background: var(--bg-sidebar);
|
|
304
|
+
border-right: 1px solid var(--border); display: flex; flex-direction: column;
|
|
305
|
+
overflow: hidden;
|
|
306
|
+
}
|
|
307
|
+
.sidebar .head {
|
|
308
|
+
display: flex; align-items: center; gap: 6px; padding: 10px 12px 8px;
|
|
309
|
+
text-transform: uppercase; font-size: 11px; letter-spacing: .6px; color: var(--text-dim); font-weight: 600;
|
|
310
|
+
}
|
|
311
|
+
.sidebar .head .actions { margin-left: auto; display: flex; gap: 2px; }
|
|
312
|
+
.sidebar .scroll { flex: 1; overflow-y: auto; overflow-x: hidden; }
|
|
313
|
+
|
|
314
|
+
.iconbtn {
|
|
315
|
+
width: 26px; height: 26px; border: 0; background: transparent; color: var(--text-dim);
|
|
316
|
+
border-radius: 6px; display: grid; place-items: center; transition: all .12s var(--ease);
|
|
317
|
+
}
|
|
318
|
+
.iconbtn:hover { background: var(--bg-hover); color: var(--text); }
|
|
319
|
+
.iconbtn:active { transform: translateY(1px); }
|
|
320
|
+
.iconbtn.danger:hover { color: var(--danger); }
|
|
321
|
+
|
|
322
|
+
/* ---- Collection switcher ------------------------------------------------- */
|
|
323
|
+
.collection-switch {
|
|
324
|
+
display: flex; align-items: center; gap: 8px; width: calc(100% - 16px); margin: 4px 8px 0;
|
|
325
|
+
padding: 7px 10px; border-radius: 8px; border: 1px solid var(--border);
|
|
326
|
+
background: var(--bg-elevated); color: var(--text); transition: border-color .12s var(--ease);
|
|
327
|
+
}
|
|
328
|
+
.collection-switch:hover { border-color: var(--accent); }
|
|
329
|
+
.collection-switch .cs-name { flex: 1; text-align: left; font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
330
|
+
.collection-switch svg:last-child { color: var(--text-faint); }
|
|
331
|
+
|
|
332
|
+
/* ---- Breadcrumb ---------------------------------------------------------- */
|
|
333
|
+
.breadcrumb {
|
|
334
|
+
display: flex; align-items: center; gap: 2px; flex-wrap: wrap;
|
|
335
|
+
padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 12px; color: var(--text-dim);
|
|
336
|
+
}
|
|
337
|
+
.breadcrumb .crumb { padding: 2px 6px; border-radius: 5px; border: 0; background: transparent; color: inherit; }
|
|
338
|
+
.breadcrumb .crumb:hover { background: var(--bg-hover); color: var(--text); }
|
|
339
|
+
.breadcrumb .sep { opacity: .4; }
|
|
340
|
+
.breadcrumb .crumb.current { color: var(--text); font-weight: 500; }
|
|
341
|
+
|
|
342
|
+
/* ---- File list ----------------------------------------------------------- */
|
|
343
|
+
.filelist { padding: 4px; }
|
|
344
|
+
.row {
|
|
345
|
+
display: flex; align-items: center; gap: 9px; height: 30px; padding: 0 8px;
|
|
346
|
+
border-radius: 6px; cursor: default; user-select: none; color: var(--text);
|
|
347
|
+
transition: background .08s var(--ease);
|
|
348
|
+
}
|
|
349
|
+
.compact .row { height: 26px; }
|
|
350
|
+
.row:hover { background: var(--bg-hover); }
|
|
351
|
+
.row.selected { background: var(--bg-selected); box-shadow: inset 0 0 0 1px var(--accent); }
|
|
352
|
+
.row.drop-target { box-shadow: inset 0 0 0 2px var(--accent); background: var(--bg-selected); }
|
|
353
|
+
.row .ico { color: var(--text-dim); flex: none; display: grid; place-items: center; }
|
|
354
|
+
.row .ico.folder { color: var(--accent); }
|
|
355
|
+
.row .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
356
|
+
.row .meta { color: var(--text-faint); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
357
|
+
.row .pin-dot { color: var(--success); display: grid; place-items: center; flex: none; }
|
|
358
|
+
.statusbar .offline-badge { color: var(--warn); }
|
|
359
|
+
.statusbar .offline-badge span { color: var(--warn); }
|
|
360
|
+
.row .rename-input {
|
|
361
|
+
flex: 1; background: var(--bg-input); border: 1px solid var(--accent); border-radius: 4px;
|
|
362
|
+
padding: 2px 6px; outline: none; color: var(--text);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.empty {
|
|
366
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
367
|
+
gap: 10px; height: 100%; color: var(--text-faint); text-align: center; padding: 32px;
|
|
368
|
+
}
|
|
369
|
+
.empty .big { font-size: 34px; opacity: .5; }
|
|
370
|
+
.empty .cta {
|
|
371
|
+
margin-top: 4px; padding: 7px 14px; border-radius: 8px; border: 1px dashed var(--border-strong);
|
|
372
|
+
background: transparent; color: var(--text-dim);
|
|
373
|
+
}
|
|
374
|
+
.empty .cta:hover { border-color: var(--accent); color: var(--text); }
|
|
375
|
+
|
|
376
|
+
/* ---- Editor area --------------------------------------------------------- */
|
|
377
|
+
.editor { display: flex; flex-direction: column; min-width: 0; background: var(--bg); overflow: hidden; }
|
|
378
|
+
.tabs { display: flex; align-items: stretch; height: 36px; background: var(--bg-activity); border-bottom: 1px solid var(--border); overflow-x: auto; }
|
|
379
|
+
.tab {
|
|
380
|
+
display: flex; align-items: center; gap: 8px; padding: 0 10px 0 14px; max-width: 220px;
|
|
381
|
+
border: 0; border-right: 1px solid var(--border); background: transparent; color: var(--text-dim);
|
|
382
|
+
position: relative; white-space: nowrap;
|
|
383
|
+
}
|
|
384
|
+
.tab.active { background: var(--bg); color: var(--text); }
|
|
385
|
+
.tab.active::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, var(--accent), var(--accent-2)); }
|
|
386
|
+
.tab .label { overflow: hidden; text-overflow: ellipsis; }
|
|
387
|
+
.tab .close { width: 18px; height: 18px; border-radius: 4px; display: grid; place-items: center; opacity: .5; }
|
|
388
|
+
.tab .close:hover { background: var(--bg-hover); opacity: 1; }
|
|
389
|
+
.editor .stage { flex: 1; min-height: 0; overflow: auto; position: relative; }
|
|
390
|
+
|
|
391
|
+
.welcome { display: grid; place-items: center; height: 100%; }
|
|
392
|
+
.welcome .card { text-align: center; max-width: 460px; padding: 40px; }
|
|
393
|
+
.welcome h1 { font-size: 26px; margin: 12px 0 6px; font-weight: 650; }
|
|
394
|
+
.welcome .logo { font-size: 46px; }
|
|
395
|
+
.welcome p { color: var(--text-dim); line-height: 1.6; }
|
|
396
|
+
.welcome .hints { margin-top: 22px; display: grid; gap: 8px; text-align: left; }
|
|
397
|
+
.welcome .hint { display: flex; align-items: center; gap: 10px; color: var(--text-dim); }
|
|
398
|
+
|
|
399
|
+
/* ---- Status bar ---------------------------------------------------------- */
|
|
400
|
+
.statusbar {
|
|
401
|
+
display: flex; align-items: center; gap: 2px; padding: 0 8px;
|
|
402
|
+
background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 20%, var(--bg-activity)), var(--bg-activity));
|
|
403
|
+
border-top: 1px solid var(--border); font-size: 11.5px; color: var(--text-dim);
|
|
404
|
+
}
|
|
405
|
+
.statusbar .seg { display: flex; align-items: center; gap: 5px; padding: 0 8px; height: 100%; border: 0; background: transparent; color: inherit; }
|
|
406
|
+
.statusbar .seg:hover { background: var(--bg-hover); color: var(--text); }
|
|
407
|
+
.statusbar .spacer { flex: 1; }
|
|
408
|
+
|
|
409
|
+
/* ---- Buttons / forms ----------------------------------------------------- */
|
|
410
|
+
.btn {
|
|
411
|
+
display: inline-flex; align-items: center; gap: 7px; padding: 7px 13px; border-radius: 7px;
|
|
412
|
+
border: 1px solid var(--border-strong); background: var(--bg-elevated); color: var(--text);
|
|
413
|
+
transition: all .12s var(--ease);
|
|
414
|
+
}
|
|
415
|
+
.btn:hover { border-color: var(--accent); }
|
|
416
|
+
.btn.primary { background: linear-gradient(180deg, var(--accent), color-mix(in srgb, var(--accent) 82%, black)); color: var(--accent-text); border: 0; font-weight: 600; }
|
|
417
|
+
/* Two modifiers eight call sites already used and no rule had ever defined, so a
|
|
418
|
+
"small ghost" button came out full-size and solid — two of them wrapped onto two
|
|
419
|
+
lines each in the 380px Activity panel. */
|
|
420
|
+
.btn.small { padding: 4px 9px; font-size: 12px; gap: 5px; border-radius: 6px; }
|
|
421
|
+
.btn.ghost { background: none; border-color: transparent; color: var(--text-dim); }
|
|
422
|
+
.btn.ghost:hover { background: var(--bg-hover); border-color: var(--border); color: var(--text); }
|
|
423
|
+
/* The pinned/"available offline" state of the info panel's toggle. */
|
|
424
|
+
.btn.on { border-color: var(--accent); color: var(--accent); background: var(--bg-active); }
|
|
425
|
+
.btn.primary:hover { filter: brightness(1.08); }
|
|
426
|
+
.btn.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, var(--border)); }
|
|
427
|
+
.btn.danger:hover { background: color-mix(in srgb, var(--danger) 14%, transparent); }
|
|
428
|
+
.btn:disabled { opacity: .45; pointer-events: none; }
|
|
429
|
+
|
|
430
|
+
.field { display: flex; flex-direction: column; gap: 6px; }
|
|
431
|
+
.field label { font-size: 12px; color: var(--text-dim); }
|
|
432
|
+
.input, select.input {
|
|
433
|
+
width: 100%; padding: 8px 11px; background: var(--bg-input); color: var(--text);
|
|
434
|
+
border: 1px solid var(--border-strong); border-radius: 7px; outline: none;
|
|
435
|
+
transition: border-color .12s var(--ease), box-shadow .12s var(--ease);
|
|
436
|
+
}
|
|
437
|
+
.input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
438
|
+
|
|
439
|
+
.switch { position: relative; width: 38px; height: 22px; flex: none; }
|
|
440
|
+
/* Invisible but REAL: it covers the whole track rather than collapsing to 0×0. A
|
|
441
|
+
zero-sized control is skipped by the TV shell's spatial navigation (which measures
|
|
442
|
+
rectangles), which made every boolean setting unreachable by remote; it is also what
|
|
443
|
+
the pointer should be hitting, and the cursor confirms the control is live. */
|
|
444
|
+
.switch input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 1; }
|
|
445
|
+
.switch .track { position: absolute; inset: 0; background: var(--border-strong); border-radius: 22px; transition: background .15s var(--ease); }
|
|
446
|
+
.switch .track::before { content: ''; position: absolute; left: 3px; top: 3px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: transform .16s var(--ease); }
|
|
447
|
+
/* The real checkbox is visually hidden, so focusing it showed nothing whatsoever — a
|
|
448
|
+
keyboard user tabbing through Settings could not tell which toggle they were on. The
|
|
449
|
+
track is the visible control, so the track carries the focus. */
|
|
450
|
+
.switch input:focus-visible + .track {
|
|
451
|
+
box-shadow: 0 0 0 3px var(--focus-halo);
|
|
452
|
+
outline: 1px solid var(--focus);
|
|
453
|
+
}
|
|
454
|
+
.switch input:checked + .track { background: var(--accent); }
|
|
455
|
+
.switch input:checked + .track::before { transform: translateX(16px); }
|
|
456
|
+
|
|
457
|
+
/* ---- Overlays: palette, dialog, menu, panel ------------------------------ */
|
|
458
|
+
.scrim { position: fixed; inset: 0; background: #0006; backdrop-filter: blur(1.5px); z-index: 40; }
|
|
459
|
+
|
|
460
|
+
.palette {
|
|
461
|
+
position: fixed; top: 12vh; left: 50%; transform: translateX(-50%);
|
|
462
|
+
width: min(640px, 92vw); background: var(--bg-elevated); border: 1px solid var(--border-strong);
|
|
463
|
+
border-radius: 12px; box-shadow: var(--shadow); z-index: 50; overflow: hidden;
|
|
464
|
+
animation: pop .13s var(--ease);
|
|
465
|
+
}
|
|
466
|
+
@keyframes pop { from { opacity: 0; transform: translate(-50%, -6px) scale(.98); } to { opacity: 1; transform: translate(-50%, 0) scale(1); } }
|
|
467
|
+
.palette .search { display: flex; align-items: center; gap: 9px; padding: 12px 14px; border-bottom: 1px solid var(--border); }
|
|
468
|
+
.palette .search input { flex: 1; background: transparent; border: 0; outline: none; font-size: 15px; color: var(--text); }
|
|
469
|
+
.palette .results { max-height: 52vh; overflow-y: auto; padding: 6px; }
|
|
470
|
+
.palette .opt {
|
|
471
|
+
display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 7px; cursor: default;
|
|
472
|
+
}
|
|
473
|
+
.palette .opt.active { background: var(--bg-active); }
|
|
474
|
+
.palette .opt .ico { color: var(--text-dim); }
|
|
475
|
+
.palette .opt .title { flex: 1; }
|
|
476
|
+
.palette .opt .cat { color: var(--text-faint); font-size: 11px; }
|
|
477
|
+
.palette .opt .kbd { margin-left: auto; }
|
|
478
|
+
.palette .opt .sub { color: var(--text-faint); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
479
|
+
.palette .opt.unavailable { opacity: .5; }
|
|
480
|
+
.palette .opt.unavailable .title { text-decoration: line-through; text-decoration-color: var(--text-faint); }
|
|
481
|
+
.offline-tag { font-size: 9.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--warn); border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent); border-radius: 5px; padding: 1px 5px; }
|
|
482
|
+
|
|
483
|
+
/* plugin live-capability report */
|
|
484
|
+
.plugin-features { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 10px; }
|
|
485
|
+
.pf-head { display: flex; justify-content: space-between; font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-dim); margin-bottom: 8px; }
|
|
486
|
+
.pf-list { display: flex; flex-direction: column; gap: 4px; }
|
|
487
|
+
.pf-row { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 6px; font-size: 12.5px; }
|
|
488
|
+
.pf-row.off { opacity: .55; }
|
|
489
|
+
.pf-row .pf-title { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
490
|
+
.pf-row .pf-kind { font-size: 10px; color: var(--text-faint); }
|
|
491
|
+
.pf-badge {
|
|
492
|
+
font-size: 9.5px; color: var(--text-faint); border: 1px solid var(--border);
|
|
493
|
+
border-radius: 5px; padding: 1px 5px; margin-left: auto; white-space: nowrap;
|
|
494
|
+
}
|
|
495
|
+
.pf-badge.offline-ok { font-size: 9.5px; color: var(--success); border: 1px solid color-mix(in srgb, var(--success) 40%, transparent); border-radius: 5px; padding: 1px 5px; }
|
|
496
|
+
.pf-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
|
|
497
|
+
.pf-dot.on { background: var(--success); }
|
|
498
|
+
.pf-dot.no { background: var(--text-faint); }
|
|
499
|
+
.palette .none { padding: 22px; text-align: center; color: var(--text-faint); display: flex; align-items: center; justify-content: center; gap: 8px; }
|
|
500
|
+
.palette .none.error { color: var(--danger, #e5534b); }
|
|
501
|
+
.palette .none .spinner { width: 13px; height: 13px; }
|
|
502
|
+
|
|
503
|
+
.dialog {
|
|
504
|
+
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
505
|
+
width: min(440px, 92vw); background: var(--bg-elevated); border: 1px solid var(--border-strong);
|
|
506
|
+
border-radius: 12px; box-shadow: var(--shadow); z-index: 50; padding: 20px; animation: pop .13s var(--ease);
|
|
507
|
+
}
|
|
508
|
+
.dialog h3 { margin: 0 0 12px; font-size: 16px; }
|
|
509
|
+
/* `display: block` because `.body` is ALSO the shell's rail+main grid (see above),
|
|
510
|
+
and that rule matches in here too. A dialog's prose was landing in a column sized
|
|
511
|
+
to the activity rail — 52px — so every confirmation rendered one word per line.
|
|
512
|
+
The text was right, which is why nothing that reads innerText ever noticed. */
|
|
513
|
+
.dialog .body { display: block; color: var(--text-dim); line-height: 1.5; margin-bottom: 16px; }
|
|
514
|
+
.dialog .row-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
|
|
515
|
+
|
|
516
|
+
/* Opener chooser */
|
|
517
|
+
.opener-chooser .opener-list { display: flex; flex-direction: column; gap: 6px; margin: 4px 0 12px; }
|
|
518
|
+
.opener-opt { display: flex; align-items: center; gap: 10px; padding: 9px 11px; border: 1px solid var(--border); border-radius: 9px; cursor: pointer; }
|
|
519
|
+
.opener-opt:hover { background: var(--bg-hover); }
|
|
520
|
+
.opener-opt.sel { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
521
|
+
.opener-opt input { accent-color: var(--accent); }
|
|
522
|
+
.opener-opt .oo-main { display: flex; flex-direction: column; gap: 1px; }
|
|
523
|
+
.opener-opt .oo-title { font-size: 13px; font-weight: 500; }
|
|
524
|
+
.opener-opt .oo-src { font-size: 11px; color: var(--text-dim); }
|
|
525
|
+
.opener-remember { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--text-dim); cursor: pointer; }
|
|
526
|
+
.opener-remember input { accent-color: var(--accent); }
|
|
527
|
+
|
|
528
|
+
.menu {
|
|
529
|
+
/* A menu can be long — the offline-files list runs to 24 entries — and it had no
|
|
530
|
+
height bound and no scrolling, so anything past the viewport was simply
|
|
531
|
+
unreachable. Unpinning has no other route in the UI, so a clipped row is a lost
|
|
532
|
+
action. */
|
|
533
|
+
max-height: calc(100vh - 24px); overflow-y: auto; overscroll-behavior: contain;
|
|
534
|
+
position: fixed; z-index: 60; min-width: 200px; background: var(--bg-elevated);
|
|
535
|
+
border: 1px solid var(--border-strong); border-radius: 9px; box-shadow: var(--shadow); padding: 5px;
|
|
536
|
+
animation: pop .1s var(--ease);
|
|
537
|
+
}
|
|
538
|
+
.menu .mi { display: flex; align-items: center; gap: 10px; padding: 7px 9px; border-radius: 6px; cursor: default; }
|
|
539
|
+
.menu .mi:hover { background: var(--bg-active); }
|
|
540
|
+
.menu .mi.danger:hover { background: color-mix(in srgb, var(--danger) 16%, transparent); color: var(--danger); }
|
|
541
|
+
.menu .mi .kbd { margin-left: auto; color: var(--text-faint); }
|
|
542
|
+
.menu .sep { height: 1px; background: var(--border); margin: 5px 2px; }
|
|
543
|
+
|
|
544
|
+
/* ---- Toasts / transfer tray --------------------------------------------- */
|
|
545
|
+
.toasts { position: fixed; bottom: calc(var(--statusbar-h) + 12px); right: 14px; z-index: 70; display: flex; flex-direction: column; gap: 8px; width: 340px; }
|
|
546
|
+
.toast {
|
|
547
|
+
display: flex; gap: 10px; padding: 11px 13px; border-radius: 10px; background: var(--bg-elevated);
|
|
548
|
+
border: 1px solid var(--border-strong); box-shadow: var(--shadow); animation: slidein .16s var(--ease);
|
|
549
|
+
}
|
|
550
|
+
@keyframes slidein { from { opacity: 0; transform: translateX(12px); } }
|
|
551
|
+
.toast .bar { width: 3px; border-radius: 3px; flex: none; }
|
|
552
|
+
.toast.info .bar { background: var(--accent); }
|
|
553
|
+
.toast.success .bar { background: var(--success); }
|
|
554
|
+
.toast.warn .bar { background: var(--warn); }
|
|
555
|
+
.toast.error .bar { background: var(--danger); }
|
|
556
|
+
.toast .msg { flex: 1; line-height: 1.4; }
|
|
557
|
+
.toast .x { color: var(--text-faint); align-self: flex-start; }
|
|
558
|
+
|
|
559
|
+
.tray { position: fixed; bottom: calc(var(--statusbar-h) + 12px); left: calc(var(--activity-w) + 12px); z-index: 65; width: 320px; background: var(--bg-elevated); border: 1px solid var(--border-strong); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; }
|
|
560
|
+
.tray .head { display: flex; align-items: center; padding: 10px 12px; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 12px; }
|
|
561
|
+
.tray .head .actions { margin-left: auto; }
|
|
562
|
+
.tray .items { max-height: 300px; overflow-y: auto; }
|
|
563
|
+
.xfer { padding: 9px 12px; border-bottom: 1px solid var(--border); }
|
|
564
|
+
.xfer:last-child { border-bottom: 0; }
|
|
565
|
+
.xfer .top { display: flex; align-items: center; gap: 8px; }
|
|
566
|
+
.xfer .top .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
|
567
|
+
.xfer .pct { color: var(--text-faint); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
568
|
+
.progress { height: 4px; background: var(--bg-hover); border-radius: 3px; overflow: hidden; margin-top: 7px; }
|
|
569
|
+
.progress > div { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width .2s var(--ease); }
|
|
570
|
+
.xfer.error .progress > div { background: var(--danger); }
|
|
571
|
+
.xfer.cancelled { opacity: .55; }
|
|
572
|
+
|
|
573
|
+
/* ---- Search view --------------------------------------------------------- */
|
|
574
|
+
.searchview { padding: 10px; display: flex; flex-direction: column; gap: 10px; height: 100%; }
|
|
575
|
+
.searchview .box { display: flex; gap: 6px; }
|
|
576
|
+
.searchview .modes { display: flex; gap: 4px; }
|
|
577
|
+
.chip { padding: 4px 10px; border-radius: 20px; border: 1px solid var(--border); background: transparent; color: var(--text-dim); font-size: 11.5px; }
|
|
578
|
+
.chip.active { background: var(--bg-selected); border-color: var(--accent); color: var(--text); }
|
|
579
|
+
.results { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
|
|
580
|
+
.result { padding: 10px 11px; border-radius: 9px; border: 1px solid var(--border); background: var(--bg-elevated); cursor: default; }
|
|
581
|
+
.result:hover { border-color: var(--border-strong); }
|
|
582
|
+
.result .top { display: flex; align-items: center; gap: 8px; }
|
|
583
|
+
.result .top .name { font-weight: 550; }
|
|
584
|
+
.result .top .score { margin-left: auto; font-size: 10.5px; color: var(--text-faint); }
|
|
585
|
+
.result .path { color: var(--text-faint); font-size: 11px; margin: 2px 0 0 24px; }
|
|
586
|
+
.result .snippet { color: var(--text-dim); font-size: 12px; margin: 6px 0 0 24px; line-height: 1.5; }
|
|
587
|
+
.result .snippet mark { background: color-mix(in srgb, var(--accent) 32%, transparent); color: var(--text); border-radius: 3px; padding: 0 2px; }
|
|
588
|
+
.result .badge { font-size: 10px; padding: 1px 6px; border-radius: 5px; background: var(--bg-hover); color: var(--text-dim); }
|
|
589
|
+
|
|
590
|
+
/* ---- Settings view ------------------------------------------------------- */
|
|
591
|
+
.settings { max-width: 720px; margin: 0 auto; padding: 26px 24px 60px; }
|
|
592
|
+
.settings h2 { font-size: 20px; margin: 0 0 4px; }
|
|
593
|
+
.settings .sub { color: var(--text-dim); margin-bottom: 22px; }
|
|
594
|
+
.settings .group { margin-bottom: 28px; }
|
|
595
|
+
.settings .group h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .6px; color: var(--accent); margin: 0 0 10px; }
|
|
596
|
+
.setting { display: flex; align-items: center; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--border); }
|
|
597
|
+
.setting .info { flex: 1; }
|
|
598
|
+
.setting .info .t { font-weight: 550; }
|
|
599
|
+
.setting .info .d { color: var(--text-faint); font-size: 12px; margin-top: 2px; line-height: 1.4; }
|
|
600
|
+
.setting .control { width: 200px; flex: none; display: flex; justify-content: flex-end; }
|
|
601
|
+
|
|
602
|
+
/* ---- Plugins view -------------------------------------------------------- */
|
|
603
|
+
.plugins { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
|
|
604
|
+
.plugin-card { border: 1px solid var(--border); border-radius: 11px; background: var(--bg-elevated); padding: 14px; }
|
|
605
|
+
.plugin-card .top { display: flex; align-items: center; gap: 10px; }
|
|
606
|
+
.plugin-card .avatar { width: 38px; height: 38px; border-radius: 9px; background: linear-gradient(135deg, var(--accent), var(--accent-2)); display: grid; place-items: center; color: var(--accent-text); font-weight: 700; }
|
|
607
|
+
.plugin-card .name { font-weight: 600; }
|
|
608
|
+
.plugin-card .status { margin-left: auto; font-size: 11px; padding: 2px 8px; border-radius: 12px; }
|
|
609
|
+
.plugin-card .status.active { background: color-mix(in srgb, var(--success) 20%, transparent); color: var(--success); }
|
|
610
|
+
.plugin-card .status.error { background: color-mix(in srgb, var(--danger) 20%, transparent); color: var(--danger); }
|
|
611
|
+
.plugin-card .status.loading { background: var(--bg-hover); color: var(--text-dim); }
|
|
612
|
+
.plugin-card .caps { display: flex; gap: 5px; flex-wrap: wrap; margin: 10px 0 0; }
|
|
613
|
+
.plugin-card .cap { font-size: 10.5px; padding: 2px 7px; border-radius: 6px; background: var(--bg-hover); color: var(--text-dim); }
|
|
614
|
+
.plugin-card .desc { color: var(--text-dim); font-size: 12px; margin-top: 8px; line-height: 1.5; }
|
|
615
|
+
.plugin-card .actions { display: flex; gap: 8px; margin-top: 12px; }
|
|
616
|
+
|
|
617
|
+
.plugin-panel {
|
|
618
|
+
position: fixed; top: 12px; right: 12px; z-index: 55;
|
|
619
|
+
background: var(--bg-elevated); border: 1px solid var(--border-strong); border-radius: 12px;
|
|
620
|
+
box-shadow: var(--shadow); overflow: hidden; animation: pop .13s var(--ease);
|
|
621
|
+
}
|
|
622
|
+
.plugin-panel .head { display: flex; align-items: center; gap: 8px; padding: 9px 12px; border-bottom: 1px solid var(--border); font-size: 12px; font-weight: 600; }
|
|
623
|
+
.plugin-panel .head .x { margin-left: auto; }
|
|
624
|
+
.plugin-panel .host { display: block; }
|
|
625
|
+
|
|
626
|
+
/* ---- Plugin install + trust --------------------------------------------- */
|
|
627
|
+
.plugin-install { display: flex; gap: 8px; margin-top: 10px; }
|
|
628
|
+
.trust { display: inline-flex; align-items: center; gap: 4px; font-size: 10.5px; padding: 2px 7px; border-radius: 12px; border: 1px solid var(--border); white-space: nowrap; }
|
|
629
|
+
.trust.verified { color: var(--success); border-color: color-mix(in srgb, var(--success) 45%, transparent); background: color-mix(in srgb, var(--success) 12%, transparent); }
|
|
630
|
+
.trust.signed { color: var(--text-dim); }
|
|
631
|
+
.trust.unverified { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
|
|
632
|
+
.trust.invalid { color: var(--danger, #e5484d); border-color: color-mix(in srgb, var(--danger, #e5484d) 55%, transparent); background: color-mix(in srgb, var(--danger, #e5484d) 14%, transparent); font-weight: 600; }
|
|
633
|
+
|
|
634
|
+
.dialog.review { padding: 0; overflow: hidden; }
|
|
635
|
+
.review-head { display: flex; align-items: center; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--border); }
|
|
636
|
+
.review-head .avatar { width: 44px; height: 44px; border-radius: 11px; }
|
|
637
|
+
.review-head .rh-main { flex: 1; }
|
|
638
|
+
.review-head .rh-name { font-size: 16px; font-weight: 650; display: flex; align-items: baseline; gap: 8px; }
|
|
639
|
+
.review-head .rh-ver { font-size: 11px; color: var(--text-faint); font-weight: 400; }
|
|
640
|
+
.review-head .rh-author { color: var(--text-dim); font-size: 12px; margin-top: 2px; }
|
|
641
|
+
.review-body { max-height: 60vh; overflow-y: auto; padding: 14px 18px; }
|
|
642
|
+
.review-desc { color: var(--text-dim); line-height: 1.55; margin: 0 0 14px; }
|
|
643
|
+
.review-section { margin-bottom: 16px; }
|
|
644
|
+
.review-section .rs-title { font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: var(--accent); margin-bottom: 8px; }
|
|
645
|
+
.cap-list { display: flex; flex-direction: column; gap: 8px; }
|
|
646
|
+
.cap-row { display: flex; gap: 10px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 9px; cursor: pointer; align-items: flex-start; }
|
|
647
|
+
.cap-row:hover { border-color: var(--border-strong); }
|
|
648
|
+
/* Blocked means the plugin asked for something it will not get. It has to look
|
|
649
|
+
different from something it will — `.contrib-row.blocked` was styled nowhere, so a
|
|
650
|
+
refused command and an available one were identical. */
|
|
651
|
+
.cap-row.blocked, .contrib-row.blocked { opacity: .6; cursor: not-allowed; }
|
|
652
|
+
.contrib-row.blocked .pf-title { text-decoration: line-through; }
|
|
653
|
+
.cap-row input { margin-top: 2px; accent-color: var(--accent); }
|
|
654
|
+
.cap-row .cap-name { font-weight: 600; font-size: 13px; }
|
|
655
|
+
.cap-row .cap-desc { color: var(--text-dim); font-size: 12px; margin-top: 2px; line-height: 1.4; }
|
|
656
|
+
.contrib-list { display: flex; flex-direction: column; gap: 3px; }
|
|
657
|
+
.contrib-row { display: flex; align-items: center; gap: 8px; font-size: 12.5px; padding: 3px 4px; }
|
|
658
|
+
.contrib-row .cr-title { color: var(--text); }
|
|
659
|
+
.contrib-row .cr-detail { color: var(--text-faint); font-size: 11px; }
|
|
660
|
+
.contrib-row .pf-kind { margin-left: auto; font-size: 10px; color: var(--text-faint); }
|
|
661
|
+
.review-meta { color: var(--text-faint); font-size: 11px; margin-top: 6px; }
|
|
662
|
+
.dialog.review .row-actions { padding: 14px 18px; border-top: 1px solid var(--border); margin: 0; }
|
|
663
|
+
|
|
664
|
+
/* ---- Media / audiobook opener ------------------------------------------- */
|
|
665
|
+
.viewer { height: 100%; display: flex; flex-direction: column; }
|
|
666
|
+
.viewer.text pre { margin: 0; padding: 18px 20px; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; color: var(--text); }
|
|
667
|
+
.viewer.image { display: grid; place-items: center; background: repeating-conic-gradient(var(--bg-elevated) 0% 25%, var(--bg) 0% 50%) 50% / 22px 22px; }
|
|
668
|
+
.viewer.image img { max-width: 100%; max-height: 100%; object-fit: contain; box-shadow: var(--shadow); }
|
|
669
|
+
.viewer .loading, .viewer .fallback { margin: auto; color: var(--text-faint); display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 40px; text-align: center; }
|
|
670
|
+
/* A plugin opener's iframe is a host-owned, position:fixed overlay (managed in
|
|
671
|
+
pluginHost.js) — never a child of this box, because moving an <iframe> in the DOM
|
|
672
|
+
reloads it. .pv-host is just the tracking target the overlay is sized/positioned to. */
|
|
673
|
+
.viewer.plugin-viewer { display: block; position: relative; }
|
|
674
|
+
.viewer.plugin-viewer .pv-host { width: 100%; height: 100%; }
|
|
675
|
+
/* Loading/error overlay shown until the plugin opens the file (see pluginOpenerView). */
|
|
676
|
+
.viewer.plugin-viewer .pv-status { position: absolute; inset: 0; z-index: 2; display: grid; place-items: center; background: var(--bg); }
|
|
677
|
+
.viewer.plugin-viewer .pv-status .loading, .viewer.plugin-viewer .pv-status .fallback { color: var(--text-faint); display: flex; flex-direction: column; align-items: center; gap: 12px; text-align: center; padding: 40px; }
|
|
678
|
+
|
|
679
|
+
/* Docked viewer — the plugin's iframe floats over .vd-body as a small frame kept
|
|
680
|
+
alive after you navigate away. Its background is transparent, so a video/audio
|
|
681
|
+
player shows through over the app. */
|
|
682
|
+
.viewer-dock { position: fixed; right: 18px; bottom: 18px; z-index: 60; display: none; flex-direction: column; background: transparent; border-radius: 12px; box-shadow: var(--shadow); }
|
|
683
|
+
.viewer-dock .vd-bar { display: flex; align-items: center; gap: 6px; height: 26px; padding: 0 4px 0 10px; background: var(--bg-elevated); border: 1px solid var(--border-strong); border-bottom: 0; border-radius: 12px 12px 0 0; }
|
|
684
|
+
.viewer-dock .vd-title { flex: 1; min-width: 0; font-size: 11.5px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
685
|
+
.viewer-dock .vd-bar button { width: 20px; height: 20px; padding: 0; border: 0; border-radius: 5px; background: transparent; color: var(--text-dim); font-size: 12px; cursor: pointer; display: grid; place-items: center; }
|
|
686
|
+
.viewer-dock .vd-bar button:hover { background: var(--bg-hover); color: var(--text); }
|
|
687
|
+
.viewer-dock .vd-body { flex: 1; min-height: 0; background: transparent; border: 1px solid var(--border-strong); border-top: 0; border-radius: 0 0 12px 12px; }
|
|
688
|
+
|
|
689
|
+
.audiobook-host { height: 100%; min-height: 0; }
|
|
690
|
+
.audiobook { display: grid; grid-template-columns: 300px 1fr; height: 100%; min-height: 0; }
|
|
691
|
+
.audiobook .cover-pane { background: var(--bg-elevated); border-right: 1px solid var(--border); padding: 26px; display: flex; flex-direction: column; align-items: center; gap: 18px; overflow-y: auto; }
|
|
692
|
+
.audiobook .cover { width: 220px; height: 220px; border-radius: 14px; box-shadow: var(--shadow); object-fit: cover; background: linear-gradient(135deg, var(--accent), var(--accent-2)); display: grid; place-items: center; color: #fff; font-size: 60px; }
|
|
693
|
+
.audiobook .meta { text-align: center; }
|
|
694
|
+
.audiobook .meta .title { font-size: 17px; font-weight: 650; }
|
|
695
|
+
.audiobook .meta .author { color: var(--text-dim); margin-top: 3px; }
|
|
696
|
+
.audiobook .meta .narrator { color: var(--text-faint); font-size: 12px; margin-top: 2px; }
|
|
697
|
+
.audiobook .transport { width: 100%; display: flex; flex-direction: column; gap: 12px; }
|
|
698
|
+
.audiobook .scrub { display: flex; align-items: center; gap: 10px; }
|
|
699
|
+
.audiobook .scrub input[type=range] { flex: 1; accent-color: var(--accent); }
|
|
700
|
+
.audiobook .time { font-variant-numeric: tabular-nums; font-size: 11px; color: var(--text-faint); min-width: 42px; }
|
|
701
|
+
.audiobook .buttons { display: flex; align-items: center; justify-content: center; gap: 14px; }
|
|
702
|
+
.audiobook .buttons .play { width: 54px; height: 54px; border-radius: 50%; background: linear-gradient(180deg, var(--accent), var(--accent-2)); color: var(--accent-text); border: 0; display: grid; place-items: center; box-shadow: var(--shadow); }
|
|
703
|
+
.audiobook .buttons .play:hover { filter: brightness(1.08); }
|
|
704
|
+
.audiobook .buttons .skip { width: 40px; height: 40px; border-radius: 50%; background: var(--bg-hover); border: 0; color: var(--text); display: grid; place-items: center; }
|
|
705
|
+
.audiobook .buttons .rate { min-width: 46px; padding: 6px 8px; border-radius: 8px; background: var(--bg-hover); border: 0; color: var(--text-dim); font-variant-numeric: tabular-nums; }
|
|
706
|
+
.audiobook .chapters { overflow-y: auto; padding: 10px; }
|
|
707
|
+
.audiobook .chapters .ch-head { padding: 8px 10px; font-size: 11px; text-transform: uppercase; letter-spacing: .6px; color: var(--text-dim); display: flex; }
|
|
708
|
+
.audiobook .chapters .ch-head .count { margin-left: auto; color: var(--text-faint); }
|
|
709
|
+
.audiobook .chapter { display: flex; align-items: center; gap: 10px; padding: 9px 11px; border-radius: 8px; cursor: default; }
|
|
710
|
+
.audiobook .chapter:hover { background: var(--bg-hover); }
|
|
711
|
+
.audiobook .chapter.current { background: var(--bg-selected); }
|
|
712
|
+
.audiobook .chapter .n { width: 22px; text-align: right; color: var(--text-faint); font-variant-numeric: tabular-nums; font-size: 12px; }
|
|
713
|
+
.audiobook .chapter .t { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
714
|
+
.audiobook .chapter .d { color: var(--text-faint); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
715
|
+
.audiobook .chapter.current .n { color: var(--accent); }
|
|
716
|
+
|
|
717
|
+
.spinner { width: 20px; height: 20px; border: 2px solid var(--border-strong); border-top-color: var(--accent); border-radius: 50%; animation: spin .7s linear infinite; }
|
|
718
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
719
|
+
|
|
720
|
+
.dragging-file .editor::after, .dragging-file .sidebar::after {
|
|
721
|
+
content: 'Drop to upload'; position: absolute; inset: 8px; border: 2px dashed var(--accent);
|
|
722
|
+
border-radius: 12px; display: grid; place-items: center; color: var(--accent); font-weight: 600;
|
|
723
|
+
background: color-mix(in srgb, var(--accent) 8%, transparent); pointer-events: none; z-index: 30;
|
|
724
|
+
}
|
|
725
|
+
.editor, .sidebar { position: relative; }
|
|
726
|
+
|
|
727
|
+
/* ---- Notifications + principal (bottom of the rail) --------------------- */
|
|
728
|
+
.principal { width: 30px; height: 30px; border-radius: 50%; overflow: hidden; display: grid; place-items: center; background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: #fff; font-weight: 700; font-size: 12px; }
|
|
729
|
+
.principal .avatar-img { width: 100%; height: 100%; object-fit: cover; }
|
|
730
|
+
|
|
731
|
+
.bell-wrap { position: relative; }
|
|
732
|
+
.bell-badge { position: absolute; top: 3px; right: 3px; min-width: 15px; height: 15px; padding: 0 3px; background: var(--danger); color: #fff; border-radius: 8px; font-size: 9.5px; font-weight: 700; display: grid; place-items: center; }
|
|
733
|
+
/* Opens up-and-to-the-right of the rail, anchored to the bottom cluster. */
|
|
734
|
+
.inbox { position: fixed; left: calc(var(--activity-w) + 8px); bottom: 8px; width: 340px; background: var(--bg-elevated); border: 1px solid var(--border-strong); border-radius: 12px; box-shadow: var(--shadow); z-index: 55; overflow: hidden; animation: pop .12s var(--ease); }
|
|
735
|
+
.inbox-head { display: flex; align-items: center; justify-content: space-between; padding: 11px 13px; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 12px; }
|
|
736
|
+
.inbox-head .link, .link { background: none; border: 0; color: var(--accent); font-size: 12px; }
|
|
737
|
+
.inbox-head .muted, .muted { color: var(--text-faint); }
|
|
738
|
+
.inbox-items { max-height: 380px; overflow-y: auto; }
|
|
739
|
+
.inbox-item { padding: 10px 13px; border-bottom: 1px solid var(--border); cursor: default; }
|
|
740
|
+
.inbox-item:hover { background: var(--bg-hover); }
|
|
741
|
+
.inbox-item.unread { border-left: 2px solid var(--accent); }
|
|
742
|
+
.inbox-item .ii-title { font-size: 12.5px; font-weight: 550; }
|
|
743
|
+
.inbox-item .ii-excerpt { color: var(--text-dim); font-size: 12px; margin-top: 2px; }
|
|
744
|
+
.inbox-item .ii-time { color: var(--text-faint); font-size: 10.5px; margin-top: 3px; }
|
|
745
|
+
.inbox-empty { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 30px; color: var(--text-faint); }
|
|
746
|
+
|
|
747
|
+
/* ---- Info panel: tags + conversation ------------------------------------ */
|
|
748
|
+
.editor-split { display: grid; grid-template-columns: 1fr 340px; min-width: 0; overflow: hidden; }
|
|
749
|
+
/* `min-width: 0` matters as much as min-height here. A grid/flex item defaults to
|
|
750
|
+
`min-width: auto` — "never shrink below your content" — so on a phone, where the panel
|
|
751
|
+
is the whole 1fr column, one long filename in the trove: link pushed it wider than the
|
|
752
|
+
screen and took its own Close button and the Comment button off the right edge.
|
|
753
|
+
`body { overflow: hidden }` then hid the evidence: it just looked cropped. */
|
|
754
|
+
.infopanel { border-left: 1px solid var(--border); background: var(--bg-sidebar); display: flex; flex-direction: column; min-height: 0; min-width: 0; }
|
|
755
|
+
.infopanel > * { min-width: 0; }
|
|
756
|
+
.ip-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 12px; }
|
|
757
|
+
.ip-head .iconbtn { margin-left: auto; }
|
|
758
|
+
.ip-body { flex: 1; overflow-y: auto; }
|
|
759
|
+
.ip-empty { padding: 30px; color: var(--text-faint); text-align: center; }
|
|
760
|
+
.ip-file { padding: 12px; border-bottom: 1px solid var(--border); }
|
|
761
|
+
.ip-file .ip-name { font-weight: 600; }
|
|
762
|
+
.ip-file .ip-path { color: var(--text-faint); font-size: 11px; margin-top: 2px; }
|
|
763
|
+
.ip-section { padding: 12px; border-bottom: 1px solid var(--border); }
|
|
764
|
+
/* A divider separates two sections. The last one has nothing under it but the panel's
|
|
765
|
+
empty tail, so its rule was a line drawn across open space. */
|
|
766
|
+
.ip-body > :last-child { border-bottom: 0; }
|
|
767
|
+
.ip-label { font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: var(--text-dim); margin-bottom: 9px; }
|
|
768
|
+
.tags { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
769
|
+
.tag { display: inline-flex; align-items: center; gap: 5px; padding: 3px 4px 3px 9px; border-radius: 14px; background: var(--bg-selected); border: 1px solid var(--border); font-size: 12px; }
|
|
770
|
+
.tag-x { width: 16px; height: 16px; border: 0; background: transparent; color: var(--text-faint); border-radius: 50%; display: grid; place-items: center; }
|
|
771
|
+
.tag-x:hover { background: var(--bg-active); color: var(--danger); }
|
|
772
|
+
.tag-input { background: var(--bg-input); border: 1px solid var(--border); border-radius: 14px; padding: 4px 10px; outline: none; color: var(--text); width: 84px; font-size: 12px; }
|
|
773
|
+
.tag-input:focus { border-color: var(--accent); width: 120px; }
|
|
774
|
+
|
|
775
|
+
.conv .thread { display: flex; flex-direction: column; gap: 12px; margin-bottom: 12px; }
|
|
776
|
+
.conv-empty { color: var(--text-faint); font-size: 12px; line-height: 1.5; margin-bottom: 12px; }
|
|
777
|
+
.conv-signin { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; color: var(--text-faint); font-size: 12px; padding: 8px 0; }
|
|
778
|
+
.comment { font-size: 13px; }
|
|
779
|
+
.c-head { display: flex; align-items: center; gap: 7px; }
|
|
780
|
+
.c-avatar { width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(135deg, var(--accent-2), var(--accent)); color: #fff; font-size: 11px; font-weight: 700; display: grid; place-items: center; flex: none; }
|
|
781
|
+
.c-author { font-weight: 600; font-size: 12.5px; }
|
|
782
|
+
.c-time { color: var(--text-faint); font-size: 11px; }
|
|
783
|
+
.c-body { margin: 4px 0 4px 29px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
|
784
|
+
.c-body.deleted { color: var(--text-faint); font-style: italic; }
|
|
785
|
+
.c-body .mention { color: var(--accent); font-weight: 600; background: color-mix(in srgb, var(--accent) 14%, transparent); border-radius: 4px; padding: 0 3px; }
|
|
786
|
+
.c-actions { display: flex; align-items: center; gap: 4px; margin-left: 29px; flex-wrap: wrap; }
|
|
787
|
+
.react { border: 0; background: transparent; border-radius: 12px; padding: 2px 6px; font-size: 12px; display: inline-flex; align-items: center; gap: 3px; }
|
|
788
|
+
.react:hover { background: var(--bg-hover); }
|
|
789
|
+
.react.on { background: var(--bg-selected); }
|
|
790
|
+
.react .rc { font-size: 10px; color: var(--text-dim); }
|
|
791
|
+
.c-link { border: 0; background: transparent; color: var(--text-faint); font-size: 11.5px; padding: 2px 5px; border-radius: 5px; }
|
|
792
|
+
.c-link:hover { color: var(--text); background: var(--bg-hover); }
|
|
793
|
+
.c-link.danger:hover { color: var(--danger); }
|
|
794
|
+
.replies { margin-top: 10px; border-left: 2px solid var(--border); }
|
|
795
|
+
.composer { display: flex; flex-direction: column; gap: 6px; }
|
|
796
|
+
.composer .replying { font-size: 11px; color: var(--text-dim); display: flex; gap: 8px; align-items: center; }
|
|
797
|
+
.composer-input { background: var(--bg-input); border: 1px solid var(--border-strong); border-radius: 9px; padding: 8px 10px; resize: vertical; outline: none; color: var(--text); font-family: inherit; }
|
|
798
|
+
.composer-input:focus { border-color: var(--accent); }
|
|
799
|
+
.composer-actions { display: flex; align-items: center; justify-content: space-between; }
|
|
800
|
+
.composer-actions .btn { padding: 5px 12px; }
|
|
801
|
+
|
|
802
|
+
@media (max-width: 720px) {
|
|
803
|
+
:root { --sidebar-w: 220px; }
|
|
804
|
+
.audiobook { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
|
|
805
|
+
.audiobook .cover-pane { border-right: 0; border-bottom: 1px solid var(--border); }
|
|
806
|
+
.editor-split { grid-template-columns: 1fr; }
|
|
807
|
+
/* Only when it is a SPLIT does the details rail have to go — there is no room for
|
|
808
|
+
both. The phone shell gives it the whole panel instead, and hiding it there would
|
|
809
|
+
make "Details & conversation" a menu entry that silently does nothing. */
|
|
810
|
+
.editor-split .infopanel { display: none; }
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/* --- Launcher (search-first main panel; replaces the explorer sidebar) ------ */
|
|
814
|
+
.launcher { display: flex; flex-direction: column; height: 100%; max-width: 820px; margin: 0 auto; width: 100%; padding: 0 20px; }
|
|
815
|
+
.launch-box { display: flex; align-items: center; gap: 10px; margin: min(12vh, 90px) 0 4px; padding: 12px 16px; background: var(--bg-input); border: 1px solid var(--border-strong); border-radius: 12px; box-shadow: var(--shadow); }
|
|
816
|
+
/* The LEADING icon only. As `.launch-box svg` this also captured the icons inside the
|
|
817
|
+
clear and microphone buttons, pinning them to one colour — so the clear button's
|
|
818
|
+
hover never lit its glyph, and the microphone's listening state was a blue block with
|
|
819
|
+
an invisible grey mic in it. A button owns the colour of its own icon. */
|
|
820
|
+
.launch-box > svg { color: var(--text-dim); flex: none; }
|
|
821
|
+
.launch-box button > svg { flex: none; }
|
|
822
|
+
/* Honest "what was actually searched" bar (search transformer output). */
|
|
823
|
+
.launch-resolved { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; margin: 8px 4px 0; font-size: 12px; color: var(--text-dim); }
|
|
824
|
+
.launch-resolved .rq-label { text-transform: uppercase; font-size: 10px; letter-spacing: .05em; color: var(--text-faint); font-weight: 600; }
|
|
825
|
+
.launch-resolved .rq-text { color: var(--text); }
|
|
826
|
+
.launch-resolved .rq-chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 12px; background: var(--bg-hover); border: 1px solid var(--border); color: var(--text-dim); }
|
|
827
|
+
.launch-input { flex: 1; background: none; border: 0; outline: none; color: var(--text); font: 500 17px/1.3 var(--font-ui); }
|
|
828
|
+
.launch-input::placeholder { color: var(--text-faint); font-weight: 400; }
|
|
829
|
+
.launch-clear { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: 0; border-radius: 6px; background: none; color: var(--text-faint); cursor: pointer; }
|
|
830
|
+
.launch-clear:hover { background: var(--bg-hover); color: var(--text); }
|
|
831
|
+
/* The microphone, shown only where the browser can transcribe on-device. `on` is the
|
|
832
|
+
listening state: it pulses so there is no doubt the room is being heard, because a
|
|
833
|
+
microphone whose state you have to guess at is worse than none. */
|
|
834
|
+
.launch-mic { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: 0; border-radius: 7px; background: none; color: var(--text-faint); cursor: pointer; flex: none; }
|
|
835
|
+
.launch-mic:hover { background: var(--bg-hover); color: var(--text); }
|
|
836
|
+
.launch-mic.on { color: var(--accent-text); background: var(--accent); animation: mic-pulse 1.4s var(--ease) infinite; }
|
|
837
|
+
@keyframes mic-pulse {
|
|
838
|
+
0%, 100% { box-shadow: 0 0 0 0 var(--focus-halo); }
|
|
839
|
+
50% { box-shadow: 0 0 0 5px transparent; }
|
|
840
|
+
}
|
|
841
|
+
/* Someone who asked for no animation gets a steady fill instead of none of the signal. */
|
|
842
|
+
@media (prefers-reduced-motion: reduce) { .launch-mic.on { animation: none; } }
|
|
843
|
+
.launch-body { flex: 1; overflow-y: auto; padding: 8px 0 40px; }
|
|
844
|
+
.launch-group { margin-top: 14px; }
|
|
845
|
+
.launch-h { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 4px 10px; font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-faint); }
|
|
846
|
+
.launch-list { display: flex; flex-direction: column; }
|
|
847
|
+
.launch-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; cursor: pointer; color: var(--text); }
|
|
848
|
+
.launch-item svg { color: var(--text-dim); flex: none; }
|
|
849
|
+
/* The NAME is what you are looking for; the content type is a footnote. Capping the
|
|
850
|
+
name at 60% of the row reserved the other 40% for "application/pdf" and truncated
|
|
851
|
+
the filename to do it. So: the name takes what it needs, and the type gives way. */
|
|
852
|
+
.launch-item .name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
853
|
+
.launch-item:hover { background: var(--bg-hover); }
|
|
854
|
+
/* The keyboard selection — the row Enter will open. It used to be `bg-hover` plus a
|
|
855
|
+
1px border, which is what a MOUSE HOVER looks like, so arrowing down produced a
|
|
856
|
+
highlight indistinguishable from wherever the pointer happened to be resting. It gets
|
|
857
|
+
the same accent edge focus uses elsewhere, so "the app is pointing at this" reads the
|
|
858
|
+
same way everywhere. */
|
|
859
|
+
.launch-item.active {
|
|
860
|
+
background: var(--bg-selected);
|
|
861
|
+
box-shadow: inset 3px 0 0 0 var(--focus);
|
|
862
|
+
}
|
|
863
|
+
.launch-detail { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-faint); font-size: 12.5px; }
|
|
864
|
+
/* The row's overflow menu. Hidden until the row is hovered, selected or the button
|
|
865
|
+
itself has focus — a column of ⋯ down every row is noise, but it must never be
|
|
866
|
+
keyboard-only-invisible. */
|
|
867
|
+
.launch-more {
|
|
868
|
+
flex: none; display: inline-flex; align-items: center; justify-content: center;
|
|
869
|
+
width: 26px; height: 26px; margin: -4px -4px -4px 0; border: 0; border-radius: 7px;
|
|
870
|
+
background: none; color: var(--text-faint); cursor: pointer; opacity: 0;
|
|
871
|
+
transition: opacity .1s var(--ease), background .1s var(--ease);
|
|
872
|
+
}
|
|
873
|
+
.launch-item:hover .launch-more, .launch-item.active .launch-more, .launch-more:focus-visible { opacity: 1; }
|
|
874
|
+
.launch-more:hover { background: var(--bg-active); color: var(--text); }
|
|
875
|
+
.launch-more:focus-visible { outline: none; color: var(--text); background: var(--bg-active); box-shadow: 0 0 0 2px var(--focus-halo); }
|
|
876
|
+
/* On a touch screen there is no hover to reveal it, so it is simply always there. */
|
|
877
|
+
@media (hover: none) { .launch-more { opacity: 1; } }
|
|
878
|
+
.launch-kind { flex: none; color: var(--text-faint); font-size: 11px; padding: 1px 6px; border: 1px solid var(--border); border-radius: 5px; }
|
|
879
|
+
.launch-up { display: inline-flex; align-items: center; gap: 3px; border: 0; background: none; color: var(--text-dim); font-size: 12px; cursor: pointer; padding: 2px 4px; border-radius: 5px; }
|
|
880
|
+
.launch-up:hover { background: var(--bg-hover); color: var(--text); }
|
|
881
|
+
.launch-empty { padding: 8px 12px; color: var(--text-faint); font-size: 13px; }
|
|
882
|
+
|
|
883
|
+
/* --- Views: how the results are drawn -------------------------------------- */
|
|
884
|
+
/* The switcher lives in the search box, on the right of the microphone: it belongs to
|
|
885
|
+
the results, and the box is the one thing on this surface that never scrolls away.
|
|
886
|
+
It is only rendered at all when more than one view is registered. */
|
|
887
|
+
.view-switch { flex: none; display: inline-flex; align-items: center; gap: 2px; padding-left: 4px; margin-left: 2px; border-left: 1px solid var(--border); }
|
|
888
|
+
.vs-btn { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: 0; border-radius: 7px; background: none; color: var(--text-faint); cursor: pointer; }
|
|
889
|
+
.vs-btn:hover { background: var(--bg-hover); color: var(--text); }
|
|
890
|
+
.vs-btn.on { background: var(--bg-active); color: var(--text); }
|
|
891
|
+
.vs-btn:focus-visible { outline: none; color: var(--text); box-shadow: 0 0 0 2px var(--focus-halo); }
|
|
892
|
+
/* A view that threw. The list is drawn underneath it, so the drive is still usable —
|
|
893
|
+
this says why it doesn't look the way it was asked to. */
|
|
894
|
+
.view-error { display: flex; align-items: center; gap: 6px; margin: 8px 10px 0; padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-hover); color: var(--text-dim); font-size: 12.5px; }
|
|
895
|
+
.view-error svg { flex: none; color: var(--warn); }
|
|
896
|
+
|
|
897
|
+
/* The grid. `auto-fill` rather than a fixed column count: the same view has to work in
|
|
898
|
+
a 1100px main panel, a 380px phone and a television. */
|
|
899
|
+
.grid-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; padding: 4px 10px; }
|
|
900
|
+
.grid-tile { display: flex; flex-direction: column; gap: 6px; border-radius: 10px; padding: 6px; cursor: pointer; color: var(--text); }
|
|
901
|
+
.grid-tile:hover { background: var(--bg-hover); }
|
|
902
|
+
.grid-tile.active { background: var(--bg-selected); box-shadow: inset 0 0 0 2px var(--focus); }
|
|
903
|
+
/* Square, so a wall of tiles lines up whatever shape the pictures are. */
|
|
904
|
+
.gt-media { position: relative; display: grid; place-items: center; aspect-ratio: 1; border-radius: 8px; background: var(--bg-hover); overflow: hidden; }
|
|
905
|
+
.gt-media > svg { color: var(--text-faint); }
|
|
906
|
+
/* The image sits over the icon and covers it. When it fails to load the view hides it,
|
|
907
|
+
uncovering the icon — which is exactly what the list would have shown. */
|
|
908
|
+
.gt-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
|
909
|
+
.gt-badge { position: absolute; left: 5px; bottom: 5px; padding: 1px 6px; border-radius: 5px; background: rgba(0,0,0,.6); color: #fff; font-size: 10.5px; }
|
|
910
|
+
.gt-media .launch-more { position: absolute; top: 3px; right: 3px; margin: 0; background: var(--bg-elevated); }
|
|
911
|
+
.grid-tile:hover .launch-more, .grid-tile.active .launch-more { opacity: 1; }
|
|
912
|
+
/* Two lines of filename, then an ellipsis. One line truncates "IMG_20240817_113244.jpg"
|
|
913
|
+
into uselessness; three turns the grid into a wall of text. */
|
|
914
|
+
.gt-name { font-size: 12.5px; line-height: 1.35; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; word-break: break-word; }
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
/* --- Viewer stack: nav bar (Back + breadcrumb trail) ----------------------- */
|
|
918
|
+
.viewer-nav { display: flex; align-items: center; gap: 6px; height: 40px; padding: 0 8px; background: var(--bg-activity); border-bottom: 1px solid var(--border); }
|
|
919
|
+
.vn-back { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 28px; border: 1px solid var(--border); border-radius: 7px; background: var(--bg-elevated); color: var(--text-dim); }
|
|
920
|
+
.vn-back:hover { color: var(--text); background: var(--bg-hover); }
|
|
921
|
+
.vn-trail { flex: 1; min-width: 0; display: flex; align-items: center; gap: 2px; overflow-x: auto; }
|
|
922
|
+
.vn-crumb { display: inline-flex; align-items: center; gap: 5px; max-width: 240px; padding: 4px 8px; border: 0; background: none; color: var(--text-dim); border-radius: 6px; white-space: nowrap; }
|
|
923
|
+
.vn-crumb .label { overflow: hidden; text-overflow: ellipsis; }
|
|
924
|
+
.vn-crumb:hover { background: var(--bg-hover); color: var(--text); }
|
|
925
|
+
.vn-crumb.active { color: var(--text); font-weight: 550; }
|
|
926
|
+
.vn-crumb:not(:last-child)::after { content: '›'; margin-left: 6px; color: var(--text-faint); }
|
|
927
|
+
.vn-actions { display: flex; gap: 2px; }
|
|
928
|
+
|
|
929
|
+
/* --- Double-shift modal search --------------------------------------------- */
|
|
930
|
+
.search-modal { position: fixed; inset: 0; z-index: 60; }
|
|
931
|
+
.search-modal .scrim { position: absolute; inset: 0; z-index: 0; background: rgba(0,0,0,.45); }
|
|
932
|
+
.search-modal-panel { z-index: 1; position: absolute; left: 50%; top: 12vh; transform: translateX(-50%); width: min(680px, 94vw); max-height: 72vh; background: var(--bg-elevated); border: 1px solid var(--border-strong); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; display: flex; }
|
|
933
|
+
.search-modal-panel .launcher { max-width: none; padding: 0 12px; height: 100%; }
|
|
934
|
+
.search-modal-panel .launch-box { margin: 12px 0 4px; }
|
|
935
|
+
.search-modal-panel .launch-body { max-height: 56vh; }
|
|
936
|
+
|
|
937
|
+
/* Info panel: the item's own trove: link, and the documents that link to it —
|
|
938
|
+
what replaces "which folder is this in?" now that there are no folders. */
|
|
939
|
+
.ip-uri {
|
|
940
|
+
display: flex; align-items: center; gap: 6px; width: 100%;
|
|
941
|
+
padding: 6px 8px; border-radius: 6px; border: 1px solid var(--border);
|
|
942
|
+
background: var(--bg-elevated); color: var(--text-dim);
|
|
943
|
+
font: 11px/1.4 var(--font-mono, ui-monospace, monospace); text-align: left; cursor: pointer;
|
|
944
|
+
min-width: 0; overflow: hidden;
|
|
945
|
+
}
|
|
946
|
+
/* The ellipsis has to live on the TEXT, not on the flex container — `text-overflow` does
|
|
947
|
+
nothing to a flex box, and a flex item defaults to `min-width: auto`, meaning "never
|
|
948
|
+
shrink below your content". So a long name pushed the panel wider than the phone,
|
|
949
|
+
putting Close and Comment off-screen (`body { overflow: hidden }` hid the evidence),
|
|
950
|
+
and on desktop hard-clipped the link with no `…` at all — presenting half a URI as
|
|
951
|
+
though it were the whole one. */
|
|
952
|
+
.ip-uri > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
953
|
+
.ip-uri:hover { color: var(--text); border-color: var(--accent); }
|
|
954
|
+
.ip-backlinks { display: flex; flex-direction: column; gap: 4px; }
|
|
955
|
+
.ip-backlink {
|
|
956
|
+
display: flex; align-items: center; gap: 6px; width: 100%;
|
|
957
|
+
padding: 5px 8px; border-radius: 6px; border: 1px solid transparent;
|
|
958
|
+
background: none; color: var(--text-dim); font-size: 12px; text-align: left; cursor: pointer;
|
|
959
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
960
|
+
}
|
|
961
|
+
.ip-backlink:hover { background: var(--bg-elevated); color: var(--text); }
|
|
962
|
+
.ip-muted { color: var(--text-faint); font-size: 12px; padding: 2px 0; }
|
|
963
|
+
.ip-muted.error { color: var(--danger, #e5534b); }
|
|
964
|
+
|
|
965
|
+
/* Rendered markdown. Documents are the organising structure now that folders are
|
|
966
|
+
gone, so they get real typography rather than a <pre> block. */
|
|
967
|
+
.viewer.markdown { overflow: auto; padding: 32px 40px; }
|
|
968
|
+
.md { max-width: 760px; margin: 0 auto; color: var(--text); line-height: 1.65; font-size: 14px; }
|
|
969
|
+
.md-h { font-weight: 650; line-height: 1.3; margin: 1.6em 0 .6em; }
|
|
970
|
+
.md-h:first-child { margin-top: 0; }
|
|
971
|
+
.md-h1 { font-size: 1.7em; } .md-h2 { font-size: 1.35em; } .md-h3 { font-size: 1.12em; }
|
|
972
|
+
/* Levels past 3 render as an <h3> element, whose UA font-size is 1.17em — bigger than
|
|
973
|
+
our own .md-h3. Without these the outline inverted at #### and then flattened. */
|
|
974
|
+
.md-h4 { font-size: 1em; } .md-h5 { font-size: .92em; color: var(--text-dim); }
|
|
975
|
+
.md-h6 { font-size: .88em; color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; }
|
|
976
|
+
.md-p { margin: 0 0 1em; }
|
|
977
|
+
.md-list { margin: 0 0 1em; padding-left: 1.4em; }
|
|
978
|
+
.md-li { margin: .25em 0; }
|
|
979
|
+
.md-quote {
|
|
980
|
+
margin: 0 0 1em; padding: .1em 0 .1em 1em;
|
|
981
|
+
border-left: 3px solid var(--border); color: var(--text-dim);
|
|
982
|
+
}
|
|
983
|
+
.md-hr { border: none; border-top: 1px solid var(--border); margin: 1.8em 0; }
|
|
984
|
+
.md-code {
|
|
985
|
+
margin: 0 0 1em; padding: 12px 14px; border-radius: 8px; overflow-x: auto;
|
|
986
|
+
background: var(--bg-elevated); border: 1px solid var(--border);
|
|
987
|
+
font: 12px/1.55 var(--font-mono, ui-monospace, monospace);
|
|
988
|
+
}
|
|
989
|
+
.md-inline-code {
|
|
990
|
+
padding: .12em .35em; border-radius: 4px; background: var(--bg-elevated);
|
|
991
|
+
font: .9em var(--font-mono, ui-monospace, monospace);
|
|
992
|
+
}
|
|
993
|
+
.md-link { color: var(--accent); text-decoration: none; border-bottom: 1px solid transparent; }
|
|
994
|
+
.md-link:hover { border-bottom-color: currentColor; }
|
|
995
|
+
/* An in-drive link reads differently from one that leaves the app. */
|
|
996
|
+
.md-trove { border-bottom: 1px dashed color-mix(in srgb, var(--accent) 55%, transparent); }
|
|
997
|
+
.md-deadlink { color: var(--text-faint); text-decoration: line-through; cursor: not-allowed; }
|
|
998
|
+
.md-truncated {
|
|
999
|
+
margin: 1.5em 0 0; padding: 10px 12px; border-radius: 8px;
|
|
1000
|
+
border: 1px dashed var(--border); color: var(--text-faint); font-size: 12.5px;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/* --- Activity: running work, and problems that outlived it ------------------- */
|
|
1004
|
+
/* Anchored above the status bar rather than centred: it reports on work that keeps
|
|
1005
|
+
going while you use the app, so it must not sit in front of the app. */
|
|
1006
|
+
.activity-panel {
|
|
1007
|
+
position: fixed; right: 12px; bottom: calc(var(--statusbar-h) + 10px); z-index: 60;
|
|
1008
|
+
width: 380px; max-height: 62vh; overflow-y: auto;
|
|
1009
|
+
background: var(--bg-elevated); border: 1px solid var(--border-strong);
|
|
1010
|
+
border-radius: var(--radius); box-shadow: var(--shadow);
|
|
1011
|
+
display: flex; flex-direction: column;
|
|
1012
|
+
}
|
|
1013
|
+
.act-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
|
1014
|
+
.act-head h3 { margin: 0; font-size: 13px; font-weight: 600; }
|
|
1015
|
+
.act-section { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
|
1016
|
+
.act-section:last-of-type { border-bottom: none; }
|
|
1017
|
+
.act-section-title { font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint); margin-bottom: 8px; }
|
|
1018
|
+
.act-empty { margin: 0; color: var(--text-faint); font-size: 12.5px; }
|
|
1019
|
+
.act-row { display: flex; align-items: flex-start; gap: 8px; }
|
|
1020
|
+
.act-body { flex: 1; min-width: 0; }
|
|
1021
|
+
.act-title { font-size: 12.5px; overflow-wrap: anywhere; }
|
|
1022
|
+
.act-detail, .act-meta { font-size: 11.5px; color: var(--text-dim); margin-top: 2px; overflow-wrap: anywhere; }
|
|
1023
|
+
.act-meta { color: var(--text-faint); }
|
|
1024
|
+
.act-error { font-size: 11.5px; color: var(--danger); margin-top: 2px; overflow-wrap: anywhere; }
|
|
1025
|
+
.act-amount { font-size: 11px; color: var(--text-faint); margin-top: 3px; }
|
|
1026
|
+
.act-action { background: none; border: none; color: var(--text-faint); cursor: pointer; padding: 2px; border-radius: 4px; }
|
|
1027
|
+
.act-action:hover { background: var(--bg-hover); color: var(--text); }
|
|
1028
|
+
.act-task, .act-issue { padding: 7px 0; }
|
|
1029
|
+
.act-task + .act-task, .act-issue + .act-issue { border-top: 1px solid var(--border); }
|
|
1030
|
+
.act-actions { display: flex; gap: 6px; margin: 6px 0 0 21px; }
|
|
1031
|
+
.act-done .act-title, .act-cancelled .act-title { color: var(--text-dim); }
|
|
1032
|
+
.act-failed .act-title { color: var(--danger); }
|
|
1033
|
+
.act-sev-warning .act-title { color: var(--warn); }
|
|
1034
|
+
.act-offline { display: flex; align-items: center; gap: 6px; padding: 8px 12px; font-size: 11.5px; color: var(--warn); background: var(--bg-hover); }
|
|
1035
|
+
.act-foot { padding: 10px 12px; }
|
|
1036
|
+
|
|
1037
|
+
.act-bar { height: 3px; margin: 6px 0 0 21px; background: var(--bg-input); border-radius: 2px; overflow: hidden; }
|
|
1038
|
+
.act-fill { height: 100%; background: var(--accent); border-radius: 2px; transition: width .25s var(--ease); }
|
|
1039
|
+
/* Indeterminate: it slides, it does not claim a position. A task that doesn't know its
|
|
1040
|
+
total must not render a percentage — a progress bar that lies is worse than a spinner. */
|
|
1041
|
+
.act-bar.indeterminate .act-fill { width: 35%; animation: act-slide 1.1s var(--ease) infinite; }
|
|
1042
|
+
@keyframes act-slide { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
|
|
1043
|
+
|
|
1044
|
+
.sb-attention { color: var(--warn); }
|
|
1045
|
+
.sb-attention:hover { color: var(--warn); }
|
|
1046
|
+
|
|
1047
|
+
/* --- Storage usage: shown only when the backend can actually answer ---------- */
|
|
1048
|
+
.sb-usage { gap: 6px; }
|
|
1049
|
+
.usage-bar { width: 42px; height: 4px; background: var(--bg-input); border-radius: 2px; overflow: hidden; }
|
|
1050
|
+
.usage-fill { height: 100%; background: var(--text-dim); border-radius: 2px; transition: width .3s var(--ease); }
|
|
1051
|
+
.sb-usage.low .usage-fill { background: var(--warn); }
|
|
1052
|
+
.sb-usage.low { color: var(--warn); }
|
|
1053
|
+
.sb-usage.critical .usage-fill { background: var(--danger); }
|
|
1054
|
+
.sb-usage.critical { color: var(--danger); }
|
|
1055
|
+
|
|
1056
|
+
/* --- Phone shell ------------------------------------------------------------
|
|
1057
|
+
A thin top bar (title + one status icon), the panel, and a bottom tab bar. Both
|
|
1058
|
+
bars respect the safe-area insets so the tab row clears the home indicator and the
|
|
1059
|
+
title clears the notch — without that the primary navigation sits under the OS. */
|
|
1060
|
+
:root {
|
|
1061
|
+
--phonebar-h: 46px;
|
|
1062
|
+
--phonetabs-h: 56px;
|
|
1063
|
+
}
|
|
1064
|
+
.shell.phone {
|
|
1065
|
+
grid-template-rows: calc(var(--phonebar-h) + env(safe-area-inset-top)) 1fr
|
|
1066
|
+
calc(var(--phonetabs-h) + env(safe-area-inset-bottom));
|
|
1067
|
+
}
|
|
1068
|
+
.shell.phone .body { grid-template-columns: 1fr; }
|
|
1069
|
+
|
|
1070
|
+
.phonebar {
|
|
1071
|
+
display: flex; align-items: center; background: var(--bg-activity);
|
|
1072
|
+
color: var(--text-dim);
|
|
1073
|
+
}
|
|
1074
|
+
.phonebar.top {
|
|
1075
|
+
gap: 8px; padding: env(safe-area-inset-top) 6px 0;
|
|
1076
|
+
border-bottom: 1px solid var(--border);
|
|
1077
|
+
}
|
|
1078
|
+
.pb-brand {
|
|
1079
|
+
width: 34px; height: 34px; flex: none; border: 0; background: transparent;
|
|
1080
|
+
border-radius: 9px; display: grid; place-items: center;
|
|
1081
|
+
}
|
|
1082
|
+
.pb-brand img { width: 21px; height: 21px; }
|
|
1083
|
+
.pb-title {
|
|
1084
|
+
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1085
|
+
font-size: 15px; font-weight: 600; color: var(--text);
|
|
1086
|
+
}
|
|
1087
|
+
.pb-status {
|
|
1088
|
+
position: relative; width: 38px; height: 38px; flex: none; border: 0; background: transparent;
|
|
1089
|
+
border-radius: 10px; display: grid; place-items: center; color: var(--text-dim);
|
|
1090
|
+
}
|
|
1091
|
+
.pb-status:active { background: var(--bg-hover); }
|
|
1092
|
+
.pb-status.warn { color: var(--warn); }
|
|
1093
|
+
.pb-status.danger { color: var(--danger); }
|
|
1094
|
+
.pb-badge {
|
|
1095
|
+
position: absolute; top: 2px; right: 2px; min-width: 15px; height: 15px; padding: 0 3px;
|
|
1096
|
+
background: var(--danger); color: #fff; border-radius: 8px;
|
|
1097
|
+
font-size: 9.5px; font-weight: 700; display: grid; place-items: center;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.phonebar.bottom {
|
|
1101
|
+
justify-content: space-around; padding: 0 4px env(safe-area-inset-bottom);
|
|
1102
|
+
border-top: 1px solid var(--border);
|
|
1103
|
+
background: color-mix(in srgb, var(--accent) 6%, var(--bg-activity));
|
|
1104
|
+
}
|
|
1105
|
+
.pb-tab {
|
|
1106
|
+
position: relative; flex: 1; height: var(--phonetabs-h); border: 0; background: transparent;
|
|
1107
|
+
color: var(--text-faint); display: flex; flex-direction: column; align-items: center;
|
|
1108
|
+
justify-content: center; gap: 3px; border-radius: 10px;
|
|
1109
|
+
}
|
|
1110
|
+
.pb-tab .pb-label { font-size: 10.5px; font-weight: 550; }
|
|
1111
|
+
.pb-tab.active { color: var(--accent); }
|
|
1112
|
+
.pb-tab:active { background: var(--bg-hover); }
|
|
1113
|
+
.pb-tab .pb-badge { top: 6px; right: 22%; background: var(--accent); color: var(--accent-text); }
|
|
1114
|
+
|
|
1115
|
+
/* --- Bottom sheet (status + overflow) --------------------------------------- */
|
|
1116
|
+
.sheet-wrap { position: fixed; inset: 0; z-index: 58; }
|
|
1117
|
+
/* The shared .scrim carries z-index 40, so the sheet has to clear it explicitly —
|
|
1118
|
+
otherwise the backdrop sits ON TOP of the sheet and swallows every tap. */
|
|
1119
|
+
.sheet-wrap .scrim { position: absolute; inset: 0; background: rgba(0,0,0,.5); z-index: 0; }
|
|
1120
|
+
.sheet {
|
|
1121
|
+
position: absolute; left: 0; right: 0; bottom: 0; z-index: 41;
|
|
1122
|
+
max-height: 82vh; overflow-y: auto;
|
|
1123
|
+
background: var(--bg-elevated); border-top: 1px solid var(--border-strong);
|
|
1124
|
+
border-radius: 16px 16px 0 0; box-shadow: var(--shadow);
|
|
1125
|
+
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
|
1126
|
+
animation: sheet-up .18s var(--ease);
|
|
1127
|
+
}
|
|
1128
|
+
@keyframes sheet-up { from { transform: translateY(18px); opacity: .4; } to { transform: none; opacity: 1; } }
|
|
1129
|
+
.sheet-grip {
|
|
1130
|
+
height: 22px; display: grid; place-items: center; border: 0; background: transparent; width: 100%;
|
|
1131
|
+
}
|
|
1132
|
+
.sheet-grip::before { content: ''; width: 38px; height: 4px; border-radius: 2px; background: var(--border-strong); }
|
|
1133
|
+
.sheet-body { padding: 2px 10px 6px; display: flex; flex-direction: column; }
|
|
1134
|
+
.sheet-title {
|
|
1135
|
+
padding: 4px 8px 10px; font-size: 12px; font-weight: 600; letter-spacing: .04em;
|
|
1136
|
+
text-transform: uppercase; color: var(--text-faint);
|
|
1137
|
+
}
|
|
1138
|
+
.sheet-row {
|
|
1139
|
+
display: flex; align-items: center; gap: 12px; width: 100%; min-height: 46px; padding: 0 8px;
|
|
1140
|
+
border: 0; background: transparent; color: var(--text); font-size: 14.5px; text-align: left;
|
|
1141
|
+
border-radius: 10px;
|
|
1142
|
+
}
|
|
1143
|
+
.sheet-row:active { background: var(--bg-hover); }
|
|
1144
|
+
.sheet-row.static { color: var(--text-dim); }
|
|
1145
|
+
.sheet-row.danger { color: var(--danger); }
|
|
1146
|
+
.sheet-row svg { flex: none; color: var(--text-dim); }
|
|
1147
|
+
.sheet-row.danger svg { color: var(--danger); }
|
|
1148
|
+
.sheet-row .sr-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1149
|
+
.sheet-row .sr-value { color: var(--text-dim); font-size: 13px; }
|
|
1150
|
+
.sheet-row .sr-go { color: var(--text-faint); }
|
|
1151
|
+
.sheet-row .sb-usage { padding: 0; }
|
|
1152
|
+
.sheet-actions { display: flex; gap: 8px; padding: 12px 8px 4px; border-top: 1px solid var(--border); margin-top: 8px; }
|
|
1153
|
+
.sheet-actions .btn { flex: 1; justify-content: center; }
|
|
1154
|
+
.sheet-me { display: flex; align-items: center; gap: 10px; padding: 4px 8px 12px; border-bottom: 1px solid var(--border); margin-bottom: 6px; }
|
|
1155
|
+
.sheet-me .avatar-img, .sheet-me .avatar-txt {
|
|
1156
|
+
width: 36px; height: 36px; border-radius: 50%; flex: none; display: grid; place-items: center;
|
|
1157
|
+
background: linear-gradient(135deg, var(--accent-2), var(--accent)); color: #fff; font-weight: 700;
|
|
1158
|
+
}
|
|
1159
|
+
.sheet-me .sm-name { font-size: 14.5px; font-weight: 600; color: var(--text); }
|
|
1160
|
+
.sheet-me .sm-sub { font-size: 12px; color: var(--text-faint); }
|
|
1161
|
+
|
|
1162
|
+
/* Panels that assume a wide window need to give it up on a phone. */
|
|
1163
|
+
.shell.phone .editor-split { grid-template-columns: 1fr; }
|
|
1164
|
+
.shell.phone .infopanel { border-left: 0; }
|
|
1165
|
+
.shell.phone .launcher { padding: 0 12px; }
|
|
1166
|
+
.shell.phone .launch-box { margin-top: 14px; padding: 10px 12px; }
|
|
1167
|
+
/* 16px or larger, or iOS zooms the page on focus and the layout is never the same again. */
|
|
1168
|
+
.shell.phone .launch-input { font-size: 16px; }
|
|
1169
|
+
.shell.phone .search-modal-panel { top: 0; width: 100vw; max-height: 100dvh; height: 100dvh; border-radius: 0; }
|
|
1170
|
+
/* Both of these are anchored to desktop chrome that isn't here: the activity panel sits
|
|
1171
|
+
above a 24px status bar, the inbox beside a left rail. Re-anchor them to the tab bar. */
|
|
1172
|
+
.shell.phone .activity-panel {
|
|
1173
|
+
left: 8px; right: 8px; width: auto;
|
|
1174
|
+
bottom: calc(var(--phonetabs-h) + env(safe-area-inset-bottom) + 8px);
|
|
1175
|
+
max-height: 68vh;
|
|
1176
|
+
}
|
|
1177
|
+
.shell.phone .inbox,
|
|
1178
|
+
.shell.phone .toasts,
|
|
1179
|
+
.shell.phone .tray {
|
|
1180
|
+
left: 8px; right: 8px; width: auto;
|
|
1181
|
+
bottom: calc(var(--phonetabs-h) + env(safe-area-inset-bottom) + 8px);
|
|
1182
|
+
}
|
|
1183
|
+
/* The docked plugin mini-player was missed when the rest were re-anchored: pinned to
|
|
1184
|
+
the viewport bottom at up to 480px wide, it covered the Files/Upload/Plugins/More row
|
|
1185
|
+
and hung off the left edge of a 390px screen. `!important` because the width is an
|
|
1186
|
+
inline style — the dock sizes itself from the plugin's declared minimum, which is a
|
|
1187
|
+
desktop number that a phone simply cannot honour. */
|
|
1188
|
+
.shell.phone .viewer-dock {
|
|
1189
|
+
left: 8px; right: 8px; width: auto !important;
|
|
1190
|
+
bottom: calc(var(--phonetabs-h) + env(safe-area-inset-bottom) + 8px);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/* --- TV shell ----------------------------------------------------------------
|
|
1194
|
+
A television is a screen you read from three metres away with a four-button remote.
|
|
1195
|
+
Two things follow. Everything is bigger — not "zoomed", but sized so a filename is
|
|
1196
|
+
legible at that distance. And the focus ring is not a subtle affordance: it is the
|
|
1197
|
+
only cursor there is, so it has to be findable at a glance from across a room. */
|
|
1198
|
+
.shell.tv {
|
|
1199
|
+
font-size: 17px;
|
|
1200
|
+
/* Overscan. Plenty of sets still crop the outer few percent of the signal, which is
|
|
1201
|
+
exactly where a rail and a status bar live — without this the first column of
|
|
1202
|
+
navigation is off the edge of the picture on the TV and nowhere else. */
|
|
1203
|
+
padding: 2.2% 2.6%;
|
|
1204
|
+
--activity-w: 84px;
|
|
1205
|
+
--statusbar-h: 34px;
|
|
1206
|
+
}
|
|
1207
|
+
.shell.tv .activitybar { gap: 10px; padding: 16px 0 18px; }
|
|
1208
|
+
.shell.tv .activitybar .item,
|
|
1209
|
+
.shell.tv .activitybar .iconbtn,
|
|
1210
|
+
.shell.tv .activitybar .brand-mark { width: 64px; height: 64px; border-radius: 14px; }
|
|
1211
|
+
.shell.tv .statusbar { font-size: 15px; }
|
|
1212
|
+
.shell.tv .launcher { max-width: 1100px; }
|
|
1213
|
+
.shell.tv .launch-input { font-size: 24px; }
|
|
1214
|
+
.shell.tv .launch-box { padding: 16px 22px; border-radius: 16px; }
|
|
1215
|
+
.shell.tv .launch-mic { width: 40px; height: 40px; border-radius: 11px; }
|
|
1216
|
+
.shell.tv .launch-item { padding: 14px 16px; font-size: 18px; border-radius: 12px; }
|
|
1217
|
+
.shell.tv .launch-item .name { max-width: none; }
|
|
1218
|
+
.shell.tv .launch-h { font-size: 14px; }
|
|
1219
|
+
.shell.tv .btn { padding: 11px 20px; font-size: 16px; }
|
|
1220
|
+
/* And the content, which is the point of opening the file. A 12.5px monospace line on
|
|
1221
|
+
a 1080p panel viewed from three metres is roughly what 4px looks like on a laptop —
|
|
1222
|
+
scaling the chrome without scaling the document would be scaling the wrong half. */
|
|
1223
|
+
.shell.tv .viewer.text pre { font-size: 19px; line-height: 1.7; padding: 28px 34px; }
|
|
1224
|
+
.shell.tv .md { font-size: 21px; max-width: 1000px; }
|
|
1225
|
+
.shell.tv .viewer.markdown { padding: 40px 52px; }
|
|
1226
|
+
.shell.tv .viewer-nav { height: 54px; padding: 0 14px; }
|
|
1227
|
+
.shell.tv .vn-crumb { font-size: 17px; max-width: 420px; padding: 8px 12px; }
|
|
1228
|
+
.shell.tv .vn-back { width: 44px; height: 40px; }
|
|
1229
|
+
.shell.tv .settings { max-width: 1000px; font-size: 17px; }
|
|
1230
|
+
.shell.tv .settings .setting .info .d { font-size: 15px; }
|
|
1231
|
+
.shell.tv .infopanel { font-size: 16px; }
|
|
1232
|
+
|
|
1233
|
+
/* The overlays. `.shell.tv` reserves an overscan-safe area with padding, but a fixed
|
|
1234
|
+
overlay is positioned against the VIEWPORT and so sits outside it — on a set that
|
|
1235
|
+
crops the outer few percent, the toast, the activity panel and the plugin panel are
|
|
1236
|
+
partly or wholly off the picture. The phone shell re-anchors all of these; the TV
|
|
1237
|
+
shell has to as well. */
|
|
1238
|
+
.shell.tv .toasts { right: 2.6%; bottom: calc(var(--statusbar-h) + 2.2%); }
|
|
1239
|
+
.shell.tv .activity-panel { right: 2.6%; bottom: calc(var(--statusbar-h) + 2.2%); }
|
|
1240
|
+
.shell.tv .plugin-panel { top: 2.2%; right: 2.6%; }
|
|
1241
|
+
.shell.tv .tray { left: calc(var(--activity-w) + 2.6%); bottom: calc(var(--statusbar-h) + 2.2%); }
|
|
1242
|
+
.shell.tv .inbox { left: calc(var(--activity-w) + 2.6%); bottom: 2.2%; }
|
|
1243
|
+
.shell.tv .viewer-dock { right: 2.6%; bottom: calc(var(--statusbar-h) + 2.2%); }
|
|
1244
|
+
|
|
1245
|
+
/* The remote's cursor. Both :focus and :focus-visible — a d-pad move is a programmatic
|
|
1246
|
+
focus() and browsers disagree about whether that counts as "visible".
|
|
1247
|
+
|
|
1248
|
+
No `border-radius` here. It out-specifies `.btn` (7px), `.tag-x` (50%) and `.iconbtn`
|
|
1249
|
+
(6px), so focusing one of those squared it off mid-interaction — the exact shape-jolt
|
|
1250
|
+
the base focus rule was rewritten to remove. The outline already follows whatever
|
|
1251
|
+
radius the element has. */
|
|
1252
|
+
.shell.tv :focus,
|
|
1253
|
+
.shell.tv :focus-visible {
|
|
1254
|
+
outline: 3px solid var(--accent);
|
|
1255
|
+
outline-offset: 3px;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/* ONE indicator, not two. The rule above is a blanket over every focusable element, and
|
|
1259
|
+
the base stylesheet spent its whole focus section REPLACING rings with each control's
|
|
1260
|
+
own vocabulary — a field whose box lights up, a row that gets an accent edge. Stacked,
|
|
1261
|
+
both drew: the search box showed its lit border AND a hard rectangle around the input
|
|
1262
|
+
sitting inside it, and the selected row showed its accent edge AND a ring.
|
|
1263
|
+
|
|
1264
|
+
On a TV the ring wins — it is what carries three metres — so the bespoke indicators
|
|
1265
|
+
stand down here rather than the other way round. Each control gets exactly one. */
|
|
1266
|
+
.shell.tv .launch-item:focus-visible,
|
|
1267
|
+
.shell.tv .sheet-row:focus-visible,
|
|
1268
|
+
.shell.tv .inbox-item:focus-visible,
|
|
1269
|
+
.shell.tv .activitybar .item:focus-visible,
|
|
1270
|
+
.shell.tv .activitybar .brand-mark:focus-visible,
|
|
1271
|
+
.shell.tv .statusbar .seg:focus-visible,
|
|
1272
|
+
.shell.tv .pb-tab:focus-visible,
|
|
1273
|
+
.shell.tv .btn:focus-visible,
|
|
1274
|
+
.shell.tv .iconbtn:focus-visible,
|
|
1275
|
+
.shell.tv .act-action:focus-visible,
|
|
1276
|
+
.shell.tv .launch-clear:focus-visible,
|
|
1277
|
+
.shell.tv .vn-back:focus-visible,
|
|
1278
|
+
.shell.tv .c-link:focus-visible,
|
|
1279
|
+
.shell.tv .react:focus-visible,
|
|
1280
|
+
.shell.tv .lh-example:focus-visible,
|
|
1281
|
+
.shell.tv .kbd-edit:focus-visible { box-shadow: none; }
|
|
1282
|
+
/* The rail's accent bar is the "active" mark; letting focus draw one too put a second
|
|
1283
|
+
bar beside the ring. */
|
|
1284
|
+
.shell.tv .activitybar .item:focus-visible::before { content: none; }
|
|
1285
|
+
|
|
1286
|
+
/* The search box is the control; the input inside it is not. The base rule already says
|
|
1287
|
+
so (`.launch-input:focus-visible { outline: none }`) and the blanket above out-specified
|
|
1288
|
+
it, which is where the inner rectangle came from. Ring the box, and drop its halo so
|
|
1289
|
+
the ring is not doubled by a glow 3px underneath it. */
|
|
1290
|
+
.shell.tv .launch-input:focus,
|
|
1291
|
+
.shell.tv .launch-input:focus-visible { outline: none; }
|
|
1292
|
+
.shell.tv .launch-box:focus-within {
|
|
1293
|
+
outline: 3px solid var(--accent);
|
|
1294
|
+
outline-offset: 3px;
|
|
1295
|
+
/* The base rule lights the border AND lays a halo under it, which next to the ring
|
|
1296
|
+
reads as three concentric lines. On a TV the ring alone is the indicator, so the
|
|
1297
|
+
border stays the colour it always was. */
|
|
1298
|
+
border-color: var(--border-strong);
|
|
1299
|
+
box-shadow: var(--shadow);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.shell.tv .launch-item:focus,
|
|
1303
|
+
.shell.tv .sheet-row:focus { background: var(--bg-selected); }
|
|
1304
|
+
/* The launcher keeps its OWN selection while focus stays in the search box — up/down
|
|
1305
|
+
walk the results without focus ever leaving the field, which is the right behaviour
|
|
1306
|
+
for typing and picking in one motion. But then the browser's focus ring is on the
|
|
1307
|
+
search box while the thing that will actually open is four rows below it, and at
|
|
1308
|
+
three metres the default 1px highlight on that row is invisible. So the launcher's
|
|
1309
|
+
selection gets the same ring the remote's focus does. */
|
|
1310
|
+
.shell.tv .launch-item.active {
|
|
1311
|
+
outline: 3px solid var(--accent);
|
|
1312
|
+
outline-offset: 3px;
|
|
1313
|
+
background: var(--bg-selected);
|
|
1314
|
+
/* The base row draws an inset accent edge for "selected". Here the ring says it, and
|
|
1315
|
+
an edge tucked inside a ring is the same message twice. */
|
|
1316
|
+
box-shadow: none;
|
|
1317
|
+
}
|
|
1318
|
+
/* …but only while the search box still holds focus. Once the remote has moved INTO the
|
|
1319
|
+
list, the focused row IS the cursor, and the standing selection drawing an identical
|
|
1320
|
+
ring somewhere else means two rows look equally chosen while only one will open. */
|
|
1321
|
+
.shell.tv .launch-body:focus-within .launch-item.active:not(:focus) {
|
|
1322
|
+
outline: none;
|
|
1323
|
+
background: var(--bg-hover);
|
|
1324
|
+
}
|
|
1325
|
+
/* A focus ring clipped by an ancestor's overflow is a selection that vanishes mid-list.
|
|
1326
|
+
`overflow-x: visible` cannot express that — CSS computes it back to `auto` whenever
|
|
1327
|
+
the other axis scrolls — so the room for the ring has to be real space instead. */
|
|
1328
|
+
.shell.tv .launch-body { padding-left: 8px; padding-right: 8px; }
|
|
1329
|
+
|
|
1330
|
+
/* --- "How does this search box work" — only after a search found nothing ------ */
|
|
1331
|
+
.launch-help { margin: 6px 10px 0; padding: 12px 0 0; border-top: 1px solid var(--border); }
|
|
1332
|
+
.lh-hint { display: flex; align-items: flex-start; gap: 8px; color: var(--text-dim); font-size: 12.5px; line-height: 1.55; }
|
|
1333
|
+
.lh-hint svg { flex: none; margin-top: 2px; color: var(--text-faint); }
|
|
1334
|
+
.lh-examples { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
1335
|
+
.lh-example {
|
|
1336
|
+
display: inline-flex; align-items: baseline; gap: 7px; padding: 5px 10px; border-radius: 8px;
|
|
1337
|
+
border: 1px solid var(--border); background: var(--bg-hover); color: var(--text); font-size: 12.5px;
|
|
1338
|
+
}
|
|
1339
|
+
.lh-example:hover { border-color: var(--accent); }
|
|
1340
|
+
.lh-example .lh-q { font-family: var(--font-mono); font-size: 12px; }
|
|
1341
|
+
.lh-example .lh-label { color: var(--text-faint); font-size: 11.5px; }
|
|
1342
|
+
|
|
1343
|
+
/* --- MCP settings ------------------------------------------------------------ */
|
|
1344
|
+
.mcp-url { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
1345
|
+
.mcp-url .mono {
|
|
1346
|
+
font-family: var(--font-mono); font-size: 12.5px; color: var(--text);
|
|
1347
|
+
background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px;
|
|
1348
|
+
padding: 5px 9px; overflow-x: auto; white-space: nowrap; max-width: 100%;
|
|
1349
|
+
}
|
|
1350
|
+
.mcp-url .mono.small { font-size: 11.5px; color: var(--text-dim); }
|
|
1351
|
+
.settings .control .mono { font-family: var(--font-mono); font-size: 12.5px; color: var(--text-dim); }
|
|
1352
|
+
.mcp-warn {
|
|
1353
|
+
display: flex; align-items: flex-start; gap: 9px; margin: 4px 0 10px; padding: 10px 12px;
|
|
1354
|
+
border-radius: 9px; font-size: 12.5px; line-height: 1.55;
|
|
1355
|
+
color: var(--warn); background: color-mix(in srgb, var(--warn) 10%, transparent);
|
|
1356
|
+
border: 1px solid color-mix(in srgb, var(--warn) 34%, transparent);
|
|
1357
|
+
}
|
|
1358
|
+
.mcp-warn svg { flex: none; margin-top: 1px; }
|
|
1359
|
+
|
|
1360
|
+
/* A two-column setting row assumes a window. At 390px the fixed 200px control leaves
|
|
1361
|
+
the description in a gutter, and a text field in a gutter is not usable — so the row
|
|
1362
|
+
stacks and the control takes the full width. */
|
|
1363
|
+
.shell.phone .settings { padding: 18px 14px 40px; }
|
|
1364
|
+
.shell.phone .setting { flex-direction: column; align-items: stretch; gap: 8px; }
|
|
1365
|
+
.shell.phone .setting .control { width: 100%; justify-content: stretch; }
|
|
1366
|
+
.shell.phone .setting .control > * { flex: 1; }
|
|
1367
|
+
/* Not everything wants the full width. A toggle stretched across the screen reads as a
|
|
1368
|
+
button; a copy icon stretched across it reads as broken. */
|
|
1369
|
+
.shell.phone .setting .control .switch,
|
|
1370
|
+
.shell.phone .setting .control .iconbtn,
|
|
1371
|
+
.shell.phone .mcp-url .btn { flex: none; }
|
|
1372
|
+
.shell.phone .setting .control .switch { align-self: flex-start; }
|
|
1373
|
+
.shell.phone .mcp-url .mono { flex: 1; min-width: 0; }
|
|
1374
|
+
/* A row whose only control is an icon button has nothing to stack — keep it on one
|
|
1375
|
+
line, with the button where the thumb expects it. */
|
|
1376
|
+
.shell.phone .setting:has(.control > .iconbtn:only-child) { flex-direction: row; align-items: center; }
|
|
1377
|
+
.shell.phone .setting:has(.control > .iconbtn:only-child) .control { width: auto; justify-content: flex-end; }
|
|
1378
|
+
/* The part of a group header that is the user's own text — case preserved, because
|
|
1379
|
+
`#draft` uppercased to `#DRAFT` names a tag they did not type. */
|
|
1380
|
+
.launch-h .lh-title { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
|
1381
|
+
.launch-h .lh-actions { display: flex; align-items: center; gap: 2px; flex: none; }
|
|
1382
|
+
.launch-h .lh-verbatim { text-transform: none; font-weight: 500; color: var(--text-dim); letter-spacing: 0; }
|