@guowenzhang/dsh-pocket 0.0.0-stage → 2.12.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.
@@ -0,0 +1,1052 @@
1
+ /**
2
+ * Mobile stylesheet for the DSH web shell.
3
+ *
4
+ * Hooks are the stable framework attributes only — no hashed classes:
5
+ * - `[data-mobile-nav="frame"]` our marker on the AppFrame element (value-scoped so
6
+ * it never matches the plugin's own controls)
7
+ * - `[data-sidebar-collapsed]` AppFrame: sidebar is in the compact rail state
8
+ * - `[data-side="sidebar"|"details"]` AppFrame drag handles
9
+ * - `[data-shell-overlay]` AppFrame overlay layer (used to locate the frame)
10
+ * - `[data-phase]` conversation root phase (hero|active|settling)
11
+ *
12
+ * Below the official auto-collapse breakpoint (1024px) the rail is removed
13
+ * from the grid entirely; the sidebar column becomes an overlay drawer that
14
+ * slides in when the frame leaves the collapsed state (narrowExpanded).
15
+ */
16
+ export const MOBILE_CSS = `
17
+ /* ---------- base control styles (rendered at any width, hidden where unused) ---------- */
18
+
19
+ [data-mobile-nav="toggle"],
20
+ [data-mobile-nav="files"] {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ width: 28px;
25
+ height: 28px;
26
+ flex: none;
27
+ padding: 0;
28
+ border: none;
29
+ border-radius: 50%;
30
+ background: transparent;
31
+ color: var(--dsw-alias-label-secondary, inherit);
32
+ cursor: pointer;
33
+ -webkit-tap-highlight-color: transparent;
34
+ }
35
+ [data-mobile-nav="toggle"]:hover,
36
+ [data-mobile-nav="files"]:hover {
37
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
38
+ }
39
+ [data-mobile-nav="toggle"]:focus-visible,
40
+ [data-mobile-nav="files"]:focus-visible {
41
+ outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7);
42
+ outline-offset: 1px;
43
+ }
44
+
45
+ /* Drawer footer actions: the relocated Session log download plus the Files
46
+ action that opens the dsh-web-ui explorer sheet. */
47
+ [data-mobile-nav="drawer-actions"] {
48
+ display: inline-flex;
49
+ align-items: center;
50
+ gap: 8px;
51
+ }
52
+ /* 宿主没有 aionui explorer 列(官方 DSH 无 dsh-web-ui,issue #48)时隐藏
53
+ 移动端「文件浏览」入口(header 图标 + drawer footer 项)——不然点了没反应。 */
54
+ [data-mobile-nav-explorer="0"] [data-mobile-nav="files"],
55
+ [data-mobile-nav-explorer="0"] [data-mobile-nav="explorer"] {
56
+ display: none !important;
57
+ }
58
+ [data-mobile-nav="session-log"],
59
+ [data-mobile-nav="explorer"] {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ gap: 6px;
64
+ height: 34px;
65
+ padding: 0 12px;
66
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12));
67
+ border-radius: 12px;
68
+ background: transparent;
69
+ color: var(--dsw-alias-label-primary, inherit);
70
+ font-family: inherit;
71
+ font-size: 13px;
72
+ line-height: 20px;
73
+ cursor: pointer;
74
+ -webkit-tap-highlight-color: transparent;
75
+ }
76
+ [data-mobile-nav="session-log"]:hover:not(:disabled),
77
+ [data-mobile-nav="explorer"]:hover {
78
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
79
+ }
80
+ [data-mobile-nav="session-log"]:disabled {
81
+ color: var(--dsw-alias-label-dimmed, rgba(0, 0, 0, .35));
82
+ cursor: default;
83
+ }
84
+
85
+ /* Floating fallback button (hero / blank phases without a session header).
86
+ The top clears the camera band below the status bar; when the client has
87
+ set viewport-fit=cover the safe-area inset moves it below the notch too. */
88
+ [data-mobile-nav="fab"] {
89
+ position: absolute;
90
+ top: calc(env(safe-area-inset-top, 0px) + 72px);
91
+ left: 10px;
92
+ z-index: 21;
93
+ display: inline-flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ width: 38px;
97
+ height: 38px;
98
+ padding: 0;
99
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12));
100
+ border-radius: 50%;
101
+ background: var(--dsw-alias-button-floating-fill, #ffffff);
102
+ color: var(--dsw-alias-label-primary, inherit);
103
+ cursor: pointer;
104
+ box-shadow: 0 2px 12px rgba(0, 0, 0, .18);
105
+ -webkit-tap-highlight-color: transparent;
106
+ }
107
+ [data-mobile-nav="fab"]:hover {
108
+ background: var(--dsw-alias-button-floating-hover, rgba(0, 0, 0, .08));
109
+ }
110
+ [data-mobile-nav="fab"]:focus-visible {
111
+ outline: 2px solid var(--dsw-alias-state-business-primary, #4f6ef7);
112
+ outline-offset: 2px;
113
+ }
114
+
115
+ /* Dimmed backdrop under the open drawer; above every column, below the drawer.
116
+ pointer-events: none —— 点击穿透(issue #38):backdrop 只负责视觉压暗,
117
+ 不抢点击。关闭抽屉改由 MobileNavOverlay 的 document 级「抽屉外点击」监听处理
118
+ (等价于原来的点击遮罩关闭,且抽屉内点击不再被 backdrop 吃掉)。 */
119
+ [data-mobile-nav="backdrop"] {
120
+ position: absolute;
121
+ inset: 0;
122
+ z-index: 30;
123
+ background: rgba(0, 0, 0, .45);
124
+ pointer-events: none;
125
+ animation: dsh-mobile-nav-fade .2s var(--ds-ease-in-out, ease-in-out);
126
+ -webkit-tap-highlight-color: transparent;
127
+ }
128
+ @keyframes dsh-mobile-nav-fade {
129
+ from { opacity: 0; }
130
+ to { opacity: 1; }
131
+ }
132
+ /* Settings sheet entrance: the official dialog mounts with no animation at
133
+ all, so it snaps in. Fade + slight rise/scale reads as a proper sheet. */
134
+ @keyframes dsh-mobile-nav-sheet-in {
135
+ from {
136
+ opacity: 0;
137
+ transform: translateY(14px) scale(.98);
138
+ }
139
+ to {
140
+ opacity: 1;
141
+ transform: none;
142
+ }
143
+ }
144
+ /* Preview sheet rise: the aionui preview column opens as a bottom sheet. */
145
+ @keyframes dsh-mobile-nav-sheet-up {
146
+ from {
147
+ opacity: 0;
148
+ transform: translateY(28px);
149
+ }
150
+ to {
151
+ opacity: 1;
152
+ transform: none;
153
+ }
154
+ }
155
+
156
+ /* ---------- mobile-only layout ---------- */
157
+
158
+ @media (max-width: 1023px) {
159
+ /* --- Phone chrome ---
160
+ The system status bar stays visible (no fullscreen). Two adjustments
161
+ make it behave:
162
+ - touch-action: manipulation kills double-tap-to-zoom (and the 300ms
163
+ tap delay) while keeping pan and pinch zoom; the client also
164
+ suppresses legacy-iOS gesturestart as a fallback.
165
+ - With the client's viewport-fit=cover, env(safe-area-inset-top) is the
166
+ status bar / notch height; the rules below push the app content below
167
+ it so the status bar never covers anything. Off notched phones (or in
168
+ a normal browser tab where the layout viewport already sits below the
169
+ status bar) the inset is 0 and nothing shifts. */
170
+ html,
171
+ body {
172
+ touch-action: manipulation !important;
173
+ }
174
+
175
+ /* AppFrame: the drawer takes the sidebar column out of grid flow, so the
176
+ remaining in-flow items (center, details) land in tracks 1..2: give the
177
+ center every pixel and keep the details track at zero. The top padding
178
+ clears the status bar / notch for every in-flow surface (session header,
179
+ messages, composer); the absolutely-positioned drawer is unaffected (its
180
+ containing block is the frame's padding box, i.e. still the frame top).
181
+
182
+ DSH 0.2.2+ ships its own phone drawer: below 768px the frame carries a
183
+ data-sidebar-drawer attribute, the sidebar column is absolutely positioned
184
+ by the host, and the host writes its own inline
185
+ grid-template-columns: 0px minmax(0px, 1fr) minmax(0px, 0px) plus a
186
+ first-child drawerMask. Our old restructure then landed on the wrong
187
+ elements: the mask (child 1) got the drawer box while :nth-child(2) hit
188
+ the sidebar instead of the center column, so the center column fell into a
189
+ 0px track — the whole app rendered 0px wide with the session list stranded
190
+ off-screen: it had data, but nothing showed and nothing could be tapped.
191
+ Every restructure rule below is therefore scoped to hosts WITHOUT a native
192
+ drawer; on 0.2.2+ only the safe-area padding and typography rules apply. */
193
+ [data-mobile-nav="frame"] {
194
+ position: relative !important;
195
+ padding-top: env(safe-area-inset-top, 0px) !important;
196
+ }
197
+
198
+ [data-mobile-nav="frame"]:not([data-sidebar-drawer]) {
199
+ grid-template-columns: minmax(0, 1fr) 0 0 !important;
200
+ }
201
+
202
+ /* 主内容列(第 2 个网格子元素)在官方样式里有显式 grid-column: 2——
203
+ 网格被压缩成 [1fr, 0, 0] 后它会落在 0px 的第 2 轨,整个主界面被挤出
204
+ 视口(只剩背景图)。必须显式把它拉回第 1 轨(issue #5)。
205
+ 第 3 列(details)保持 0 轨即可,无需处理。 */
206
+ [data-mobile-nav="frame"]:not([data-sidebar-drawer]) > :nth-child(2) {
207
+ grid-column: 1 !important;
208
+ grid-row: 1 !important;
209
+ min-width: 0 !important;
210
+ }
211
+
212
+ /* The sidebar column (first grid child) becomes a left drawer. The drawer
213
+ hugs the sidebar content exactly (the wide sidebar carries an inline
214
+ width, ~280px): a fixed 92vw box would leave a white strip where the
215
+ container background shows beside the content.
216
+ Closed state: translateX(-110%) — more than -100% of the max-content
217
+ width — guarantees the whole drawer (and its shadow, had it one) leaves
218
+ the viewport. A mere -100% leaves a sliver on screen; -105% (as used
219
+ before) left 14px of the drawer plus a long 32px-blur shadow gradient
220
+ visible along the left edge of the main UI. No box-shadow at all: the
221
+ dimmed backdrop already separates drawer from content.
222
+ Z-index note: the backdrop renders inside the shell's overlay layer
223
+ ([data-shell-overlay]), which forms its own stacking context. Third-party
224
+ plugins can force that layer up with !important (dsh-update-checker sets
225
+ it to 500), and when the layer outranks the drawer, the backdrop paints
226
+ ABOVE the drawer and swallows every tap — the drawer opens but no row
227
+ can be pressed (every tap just closes it). The drawer must therefore
228
+ outrank any such raise.
229
+ 1200 (was 600) clears the mobile layers shipped by
230
+ @linxin666/dsh-web-ui-all — its sidebar pane is z-index 1100, its
231
+ details pane 1000 and its full-screen frame ::after mask 1050 (issue
232
+ #67: that mask sat on top of the 600 drawer and ate every tap). Still
233
+ far under the fixed-position banners/toasts (z 9999) that float at the
234
+ viewport level. */
235
+ [data-mobile-nav="frame"]:not([data-sidebar-drawer]) > :first-child {
236
+ position: absolute !important;
237
+ inset: 0 auto 0 0 !important;
238
+ width: max-content !important;
239
+ max-width: 92vw !important;
240
+ z-index: 1200 !important;
241
+ transform: translateX(-110%);
242
+ transition: transform .28s var(--ds-ease-in-out, ease-in-out);
243
+ background: var(--dsw-alias-bg-base, #ffffff);
244
+ /* Keep the drawer's own content below the status bar / notch: the drawer
245
+ spans the full frame height (its absolute containing block is the
246
+ frame's padding box, so the frame's own safe-area padding does NOT
247
+ reach it). The drawer background paints the status-bar strip, which
248
+ the client's theme-color meta matches, so the strip reads seamless. */
249
+ padding-top: env(safe-area-inset-top, 0px) !important;
250
+ /* Kill the official sidebarCol right border: with the backdrop the edge
251
+ reads cleanly, and the settings dialog (width:100% of this box) stays
252
+ pixel-flush with the drawer. */
253
+ border-right: none !important;
254
+ }
255
+
256
+ /* Expanded state (frame without data-sidebar-collapsed) slides the drawer in.
257
+ The open state must be transform:none — NOT translateX(0): an identity
258
+ transform still makes the drawer the containing block for fixed-position
259
+ descendants (the settings dialog's .VOzbGW_overlay is portaled into the
260
+ sidebar DOM). With the identity transform the wide settings sheet
261
+ (100vw-16) overflows the 280px drawer, the dialog's focus scrolls the
262
+ overflow:hidden drawer to scrollLeft=102, and every static child (plus the
263
+ fixed overlay) shifts 102px off-screen. With transform:none the overlay is
264
+ viewport-anchored: it dims the full screen and the sheet sits at left:8. */
265
+ [data-mobile-nav="frame"]:not([data-sidebar-drawer]):not([data-sidebar-collapsed]) > :first-child {
266
+ transform: none !important;
267
+ }
268
+
269
+ /* 宿主自带抽屉时,遮罩(含点外部关闭)由它的 drawerMask 负责。插件自己的
270
+ 那层只做重复压暗,还会让「点一下关不掉」看起来像卡住,直接不显示。 */
271
+ [data-mobile-nav="frame"][data-sidebar-drawer] [data-mobile-nav="backdrop"] {
272
+ display: none !important;
273
+ }
274
+
275
+ /* Kill a competing full-screen mask (issue #67).
276
+ @linxin666/dsh-web-ui-all ships its own mobile drawer, and part of it is
277
+
278
+ [data-dsh-frame]:not([data-sidebar-collapsed])::after {
279
+ content: ""; position: fixed; inset: 0; z-index: 1050;
280
+ background: rgb(0 0 0 / 24%);
281
+ }
282
+
283
+ The pseudo-element belongs to the frame we already mark, and the frame
284
+ carries only "position: relative" with z-index auto — no stacking context
285
+ — so this mask competes with the drawer in the parent stacking context
286
+ and, at 1050, paints over it. It covers the whole viewport, so every tap
287
+ on a session row lands on the mask instead: the drawer opens but nothing
288
+ inside it can be pressed, and the page behind cannot be scrolled.
289
+ Removing it is safe: the mobile stylesheet already renders its own
290
+ backdrop, and tapping outside the drawer is handled in JS.
291
+
292
+ The attribute selector is repeated on purpose. Their rule has the same
293
+ specificity (0,2,1) once ours is written the obvious way, and plugin
294
+ stylesheets are injected in load order, so a tie would be decided by
295
+ whichever plugin happened to load last. Doubling the attribute makes it
296
+ (0,3,1) and deterministic. */
297
+ [data-mobile-nav="frame"][data-mobile-nav="frame"]:not([data-sidebar-collapsed])::after {
298
+ content: none !important;
299
+ }
300
+
301
+ /* Drag handles are useless on touch and would float over the drawer. */
302
+ [data-side="sidebar"],
303
+ [data-side="details"] {
304
+ display: none !important;
305
+ }
306
+
307
+ /* --- Conversation text on mobile ---
308
+ The official message flow keeps desktop's 32px side gutters and 16px
309
+ type. On a phone: shrink the type a notch and widen the lines by
310
+ trimming the gutters (the sidebar drawer list keeps its size). The
311
+ flow's scroll container is the only _scroll element holding markdown
312
+ <p> paragraphs — the composer's own scroll (textarea) is excluded
313
+ via :has(p). */
314
+ /* The official main scroll body reserves scrollbar-gutter for desktop
315
+ scrollbars (8px), which shoves every column off-center on a phone.
316
+ Classic desktop scrollbars (Edge/Chrome) also occupy ~8-17px in a
317
+ phone-sized viewport, shifting the column further. Mobile scrolling
318
+ is touch/wheel, so remove the scrollbar entirely on phones: the
319
+ column is then exactly centered in every browser. */
320
+ [data-phase] [class$="_scrollBody"] {
321
+ scrollbar-gutter: auto !important;
322
+ scrollbar-width: none !important;
323
+ }
324
+ [data-phase] [class$="_scrollBody"]::-webkit-scrollbar {
325
+ display: none !important;
326
+ width: 0 !important;
327
+ height: 0 !important;
328
+ }
329
+ /* Message action rows (copy / run-time badges) can overflow the right
330
+ edge on narrow screens — keep them inside the message width. */
331
+ [data-phase] [class$="_actions"] {
332
+ overflow: hidden !important;
333
+ }
334
+ [data-phase] [class$="_actions"] [class$="_timeEnd"] {
335
+ flex: 0 1 auto !important;
336
+ min-width: 0 !important;
337
+ overflow: hidden !important;
338
+ text-overflow: ellipsis !important;
339
+ white-space: nowrap !important;
340
+ }
341
+
342
+ [data-phase] [class$="_scroll"]:has(p) {
343
+ padding-left: 20px !important;
344
+ padding-right: 20px !important;
345
+ font-size: 15px !important;
346
+ }
347
+ /* The official markdown styles set an explicit 16px on paragraphs and
348
+ list items, so the container's inherited 15px is not enough. User
349
+ messages render their text in a div whose class carries _text_
350
+ (16px too) — cover it as well. */
351
+ [data-phase] [class$="_scroll"]:has(p) p,
352
+ [data-phase] [class$="_scroll"]:has(p) li,
353
+ [data-phase] [class$="_scroll"]:has(p) [class*="_text_"] {
354
+ font-size: 15px !important;
355
+ }
356
+
357
+ /* Keep DSH's own process disclosures and their expand/collapse behaviour,
358
+ but remove desktop-sized vertical breathing room between consecutive
359
+ context, Skill, and system-prompt entries. */
360
+ [data-phase] [data-turn-process] {
361
+ height: 28px !important;
362
+ padding-bottom: 4px !important;
363
+ margin-bottom: 4px !important;
364
+ }
365
+ [data-phase] [data-turn-process][data-open] {
366
+ margin-bottom: 4px !important;
367
+ }
368
+ [data-phase] [data-disclosure-row] {
369
+ min-height: 24px !important;
370
+ }
371
+ [data-phase] :is([data-context-injection-body], [data-system-prompt-body]) {
372
+ margin-top: 2px !important;
373
+ }
374
+
375
+ /* --- Composer bottom row on mobile ---
376
+ Let the row wrap instead of locking it to one line (issue #170): any
377
+ plugin can register extra non-shrinking controls here, and a hard
378
+ "nowrap" plus ":last-child { flex:1 1 0 }" squeezed the model pill (long
379
+ text, max-width:220px upstream) down to 0 width at 360px. Allowing
380
+ wrap keeps every control visible; the row simply spans two lines on
381
+ narrow screens. DSH's stable data-composer-card hook survives the
382
+ editor's textarea -> contenteditable migration. */
383
+ [data-phase] [data-composer-card="true"] > [class$="_row"] {
384
+ flex-wrap: wrap !important;
385
+ gap: 6px !important;
386
+ }
387
+ [data-phase] [data-composer-card="true"] > [class$="_row"] > :first-child {
388
+ gap: 8px !important;
389
+ min-width: 0 !important;
390
+ }
391
+ [data-phase] [data-composer-card="true"] > [class$="_row"] > :first-child > :nth-child(2) {
392
+ flex: 0 1 auto !important;
393
+ min-width: 0 !important;
394
+ }
395
+
396
+ /* --- Composer popups as bottom sheets on mobile ---
397
+ Two composer-anchored popups break on phones (field: bottom + side
398
+ cut, only part of the popup visible):
399
+ 1. the model pill menu ([role=menu], max 360px, opens upward from the
400
+ pill inside the composer card);
401
+ 2. the "/" command palette (max 320px card with the search box — it
402
+ hosts the /model popupSelect list: search + provider-grouped rows).
403
+ Both are position:absolute INSIDE the conversation scrollBody
404
+ (overflow:hidden) and the shell center column (overflow:hidden), so
405
+ the scroll containers clip them mid-list. Forensics showed neither
406
+ layer creates a containing block (no transform/contain/will-change),
407
+ so on mobile we snap whichever popup is open to a viewport-anchored
408
+ sheet: fixed positioning escapes the scroll clip entirely, width is
409
+ deterministic, safe-area keeps it off the gesture bar. A transient
410
+ picker covering the composer is standard mobile UX; selection or an
411
+ outside tap still dismisses it. */
412
+ [data-phase] [class$="_root"]:has(> [aria-haspopup="menu"]) > [role="menu"],
413
+ [data-phase] [class$="_card"]:has(> [class$="_search"]) {
414
+ position: fixed !important;
415
+ left: 12px !important;
416
+ right: 12px !important;
417
+ top: auto !important;
418
+ bottom: calc(env(safe-area-inset-bottom, 0px) + 12px) !important;
419
+ width: auto !important;
420
+ min-width: 0 !important;
421
+ max-width: none !important;
422
+ max-height: min(65vh, 480px) !important;
423
+ max-height: min(65dvh, 480px) !important;
424
+ z-index: 130 !important;
425
+ border-radius: 14px !important;
426
+ box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important;
427
+ }
428
+
429
+ /* --- Session header on mobile ---
430
+ Layout goal: [toggle] [session title] [mode badge] in a row. The optional
431
+ rightbar entry uses the shell's stable header-corner hook. */
432
+ [data-phase] header {
433
+ padding: 8px 12px 0 !important;
434
+ }
435
+ /* The directory and Files controls are absolutely positioned, so reserve
436
+ their lanes and let the title use the remaining width without squeezing. */
437
+ [data-phase] header > :first-child {
438
+ min-height: 36px !important;
439
+ padding: 0 32px !important;
440
+ }
441
+ [data-phase] header [class$="_titleCluster"],
442
+ [data-phase] header [class$="_crumbs"] {
443
+ min-width: 0 !important;
444
+ }
445
+ [data-phase] header button[class*="_crumb"] {
446
+ max-width: calc(100vw - 104px) !important;
447
+ padding-left: 0 !important;
448
+ padding-right: 0 !important;
449
+ }
450
+ /* The directory toggle sits at the far left of the header (the header
451
+ is position:relative; the data-slot wrappers are display:contents). */
452
+ [data-mobile-nav="toggle"] {
453
+ position: absolute !important;
454
+ left: 8px !important;
455
+ top: 12px !important;
456
+ z-index: 2 !important;
457
+ }
458
+ /* The Files action sits at the FAR RIGHT of the header so it reads as a
459
+ distinct control from the directory toggle on the left (which opens
460
+ the history sidebar). */
461
+ [data-mobile-nav="files"] {
462
+ position: absolute !important;
463
+ left: auto !important;
464
+ right: 8px !important;
465
+ top: 12px !important;
466
+ z-index: 2 !important;
467
+ }
468
+ /* The native rightbar entry is visible by default. Users who prefer the
469
+ compact header can turn it off in Pocket settings. */
470
+ body[data-dsh-pocket-mobile-rightbar="off"] [data-conversation-header-corner] {
471
+ display: none !important;
472
+ }
473
+ body:not([data-dsh-pocket-mobile-rightbar="off"]) [data-mobile-nav="files"] {
474
+ right: 44px !important;
475
+ }
476
+
477
+ /* --- Settings dialog on mobile ---
478
+ Desktop: 800px two-column flex (188px nav + content). Mobile: a
479
+ near-full-width sheet — nav tabs wrap into rows on top, option rows
480
+ stay horizontal (title+description left, control right). Structural
481
+ selectors are scoped to the unique aria-modal dialog; every
482
+ settings-specific rule is gated with
483
+ :has(> :first-child > :last-child > button) — the settings nav tab
484
+ list holds <button> tabs, so the transient export dialog (the same
485
+ primitives Modal, header(title+close)+description+body) keeps its
486
+ official centered card layout. Requires :has() support
487
+ (Chromium 105+, 2022). */
488
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) {
489
+ position: absolute !important;
490
+ left: 8px !important;
491
+ /* Fixed top (no translateY): a transform on the panel combined with the
492
+ panel overflowing the max-content drawer shifts the fixed overlay's
493
+ coordinate frame, dragging the whole sidebar content off-screen. The
494
+ safe-area inset keeps the sheet below the status bar / notch. */
495
+ top: calc(env(safe-area-inset-top, 0px) + 12px) !important;
496
+ width: calc(100vw - 16px) !important;
497
+ max-width: calc(100vw - 16px) !important;
498
+ /* Height follows the content (no dead space under a short page); it
499
+ caps at 100dvh-24 (less the safe-area top) and the options area
500
+ scrolls only then. */
501
+ height: auto !important;
502
+ max-height: min(800px, calc(100vh - 24px - env(safe-area-inset-top, 0px))) !important;
503
+ max-height: min(800px, calc(100dvh - 24px - env(safe-area-inset-top, 0px))) !important;
504
+ flex-direction: column !important;
505
+ border-radius: 14px !important;
506
+ animation: dsh-mobile-nav-sheet-in .22s var(--ds-ease-out, ease-in-out);
507
+ }
508
+ /* The settings sheet's dimmed mask fades in with the panel (the mask is
509
+ the first child of the overlay that directly contains the sheet). */
510
+ :has(> [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"]))) > :first-child {
511
+ animation: dsh-mobile-nav-fade .18s var(--ds-ease-out, ease-in-out);
512
+ }
513
+ @media (prefers-reduced-motion: reduce) {
514
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])),
515
+ :has(> [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"]))) > :first-child {
516
+ animation: none !important;
517
+ }
518
+ }
519
+ /* The export dialog (not the settings sheet) must never overflow the
520
+ viewport: the official centered card can be wider than 390px. */
521
+ [aria-modal="true"]:not(:has(> :first-child > :last-child > button)) {
522
+ max-width: calc(100vw - 32px) !important;
523
+ }
524
+ /* Nav bar: hide the "Settings" caption (redundant on a full-width sheet)
525
+ and wrap the tab list so every tab is visible — a horizontal scroll cut
526
+ the last tab ("Plugins") off with no affordance to scroll. */
527
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :first-child {
528
+ width: 100% !important;
529
+ flex-direction: row !important;
530
+ align-items: center !important;
531
+ gap: 6px !important;
532
+ padding: 10px 12px 8px !important;
533
+ }
534
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :first-child > :first-child {
535
+ display: none !important;
536
+ }
537
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :first-child > :last-child {
538
+ flex-direction: row !important;
539
+ flex-wrap: wrap !important;
540
+ width: 100% !important;
541
+ gap: 6px !important;
542
+ overflow: visible !important;
543
+ }
544
+ /* Content toolbar (Open configuration file + close): spread to the edges
545
+ instead of clustering right with a dead zone on the left. The toolbar
546
+ children carry official auto-margins that would defeat space-between,
547
+ so neutralize them. The close button gets a round tappable base so it
548
+ reads as its own control, not part of the outline button. */
549
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child > :first-child {
550
+ justify-content: space-between !important;
551
+ align-items: center !important;
552
+ padding: 0 12px !important;
553
+ min-height: 40px !important;
554
+ }
555
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child > :first-child > * {
556
+ margin-left: 0 !important;
557
+ margin-right: 0 !important;
558
+ }
559
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child > :first-child > :last-child {
560
+ width: 32px !important;
561
+ height: 32px !important;
562
+ border-radius: 50% !important;
563
+ display: inline-flex !important;
564
+ align-items: center !important;
565
+ justify-content: center !important;
566
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06)) !important;
567
+ }
568
+ /* Appearance mode cards: the official cube row renders three tall
569
+ vertical cards (~268px) that eat half the sheet. Turn them into a
570
+ compact horizontal trio (icon + label inline, equal widths).
571
+ Relies on the official cube-row class name of this version. */
572
+ [aria-modal="true"] [class$="_cubeRow"] {
573
+ gap: 6px !important;
574
+ }
575
+ [aria-modal="true"] [class$="_cubeRow"] > * {
576
+ flex: 1 1 0 !important;
577
+ flex-direction: row !important;
578
+ align-items: center !important;
579
+ justify-content: center !important;
580
+ gap: 6px !important;
581
+ padding: 10px 8px !important;
582
+ min-height: 0 !important;
583
+ }
584
+ /* Content: the options scroll area gets bottom breathing room so the last
585
+ row never sits flush against the sheet's rounded corner. */
586
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child {
587
+ flex: 1 1 auto !important;
588
+ min-height: 0 !important;
589
+ }
590
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :last-child > :last-child {
591
+ padding: 0 12px 24px !important;
592
+ }
593
+
594
+ /* ---------- dsh-web-ui family compatibility ----------
595
+ The linxin666 plugin suite extends the shell frame directly:
596
+ - aionui-panel appends two trailing grid columns (explorer / preview)
597
+ plus absolute drag handles to [data-dsh-frame]; its 5-track inline
598
+ grid is already overridden above, but the handles and columns would
599
+ still float over the main UI. On mobile the columns leave the grid
600
+ as floating bottom sheets and keep their own visibility state —
601
+ the suite's collapse chevron / preview tabs still work, so no
602
+ feature is lost. The task-board / ssh plugins inject sidebar
603
+ entries and center-column takeover panels; the entries need
604
+ spacing and the kanban needs scrollable columns. */
605
+
606
+ /* Touch devices: the drag handles are useless — the floating expand
607
+ button is the opener. */
608
+ .aionui-explorer-handle,
609
+ .aionui-preview-handle {
610
+ display: none !important;
611
+ }
612
+
613
+ /* Shared base: both columns leave the grid as floating panels. The
614
+ explorer is gated shut by default (its own persisted expanded state
615
+ must never cover the mobile UI on load); the header Files action opens
616
+ it via the frame marker below, and the sheet's own collapse chevron
617
+ clears it. Preview stays owned by the suite (hidden while no tab is
618
+ open). The per-column rules below override the geometry. */
619
+ [data-aionui-explorer-col],
620
+ [data-aionui-preview-col] {
621
+ position: fixed !important;
622
+ z-index: 55 !important;
623
+ background: var(--aion-bg-base, #ffffff) !important;
624
+ border-left: none !important;
625
+ }
626
+ /* Explorer (file tree) bottom sheet: bottom edge aligned exactly with
627
+ the composer card's bottom line — the card sits 36px above the
628
+ viewport bottom (8px composer padding + the 28px stats strip below
629
+ the card), so the sheet uses the same 36px bottom offset. */
630
+ [data-aionui-explorer-col] {
631
+ visibility: hidden !important;
632
+ left: 8px !important;
633
+ right: 8px !important;
634
+ top: auto !important;
635
+ bottom: 36px !important;
636
+ width: auto !important;
637
+ height: min(55dvh, 460px) !important;
638
+ max-height: calc(100dvh - 44px) !important;
639
+ border-radius: 14px !important;
640
+ overflow: hidden !important;
641
+ box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important;
642
+ animation: dsh-mobile-nav-sheet-up .24s var(--ds-ease-out, ease-in-out) !important;
643
+ }
644
+ /* Preview (file content) bottom sheet. Gated shut by default: the suite
645
+ persists open preview tabs in localStorage and restores them on load,
646
+ which would pop the sheet over the fresh UI. The client only sets the
647
+ frame marker after the user taps a file row in the explorer; the
648
+ suite's own collapse chevron clears it via the visibility watcher. */
649
+ [data-aionui-preview-col] {
650
+ visibility: hidden !important;
651
+ position: fixed !important;
652
+ left: 8px !important;
653
+ right: 8px !important;
654
+ top: auto !important;
655
+ bottom: 40px !important;
656
+ width: auto !important;
657
+ height: min(50dvh, 420px) !important;
658
+ max-height: calc(100dvh - 48px) !important;
659
+ border-radius: 14px !important;
660
+ overflow: hidden !important;
661
+ box-shadow: 0 -4px 28px rgba(0, 0, 0, .18) !important;
662
+ z-index: 56 !important;
663
+ animation: dsh-mobile-nav-sheet-up .24s var(--ds-ease-out, ease-in-out) !important;
664
+ }
665
+ /* User-opened preview sheet (frame marker, set on file-row tap). */
666
+ [data-mobile-nav="frame"][data-aionui-preview-open] [data-aionui-preview-col] {
667
+ visibility: visible !important;
668
+ }
669
+ /* The Files action opens the explorer sheet (frame marker). */
670
+ [data-mobile-nav="frame"][data-aionui-explorer-open] [data-aionui-explorer-col] {
671
+ visibility: visible !important;
672
+ }
673
+ /* The open drawer must never sit under a sheet: while the frame is in the
674
+ narrow-expanded state both sheets yield (later in the file than the
675
+ open marker rule, so it wins at equal specificity). */
676
+ [data-mobile-nav="frame"]:not([data-sidebar-collapsed]) [data-aionui-explorer-col],
677
+ [data-mobile-nav="frame"]:not([data-sidebar-collapsed]) [data-aionui-preview-col] {
678
+ visibility: hidden !important;
679
+ }
680
+ /* The suite's own expand button reads the store state we bypass on
681
+ mobile — hide it; the header Files action is the opener. */
682
+ .aionui-floating-expand {
683
+ display: none !important;
684
+ }
685
+
686
+ /* dsh-web-ui sidebar entries (task board / ssh) sit flush against each
687
+ other — give the injected rows breathing room. */
688
+ button[data-dsh-taskboard-entry],
689
+ button[data-dsh-ssh-entry] {
690
+ margin-bottom: 8px !important;
691
+ }
692
+
693
+ /* Task board: five kanban columns at minmax(0,1fr) crush into ~78px phone
694
+ strips. Give every column a usable minimum and let the row scroll. */
695
+ [data-dsh-taskboard-board] > [class$="_columns"] {
696
+ grid-template-columns: repeat(5, minmax(240px, 1fr)) !important;
697
+ overflow-x: auto !important;
698
+ }
699
+ /* The floating button must not float over a takeover panel (task board /
700
+ ssh own the center column while active). */
701
+ html[data-dsh-taskboard-active] [data-mobile-nav="fab"],
702
+ html[data-dsh-ssh-active] [data-mobile-nav="fab"],
703
+ html[data-dsh-taskboard-active] [data-mobile-nav="backdrop"],
704
+ html[data-dsh-ssh-active] [data-mobile-nav="backdrop"] {
705
+ display: none !important;
706
+ }
707
+ /* Board header: let the search field take the slack instead of squeezing
708
+ the action buttons. */
709
+ [data-dsh-taskboard-board] > [class$="_boardHeader"] [class$="_search"] {
710
+ flex: 1 1 auto !important;
711
+ min-width: 80px !important;
712
+ }
713
+
714
+ /* ---------- dsh-web-ui polish: plugin market search ----------
715
+ The market tab row (Discover / Themes / Installed + the plugin search
716
+ box) is a no-wrap flex: at 390px the tabs plus the ~218px search box
717
+ (~475px total) overflow the ~334px sheet and the search box runs off
718
+ the right edge of the screen (it also forces a horizontal scrollbar on
719
+ the sheet's options area). Let the row wrap: the tabs keep the first
720
+ line and the search box gets its own full-width second line. */
721
+
722
+ [aria-modal="true"] [class$="_tabs"] {
723
+ flex-wrap: wrap !important;
724
+ row-gap: 8px !important;
725
+ }
726
+ [aria-modal="true"] [class$="_searchInline"] {
727
+ flex: 1 1 100% !important;
728
+ width: 100% !important;
729
+ max-width: 100% !important;
730
+ }
731
+
732
+ /* ---------- dsh-usage-stats polish: usage & balance panel ----------
733
+ The panel's stats row shows three token counters side by side
734
+ (today / month / total). The counters use tabular nowrap figures whose
735
+ min-content width overflows the ~336px panel body on a phone: figures
736
+ clip at the row's edges and the panel grows a horizontal scrollbar.
737
+ Stack the three counters vertically — full-width rows, so the figures
738
+ always fit. */
739
+
740
+ [class*="usg_"][class$="_statsRow"] {
741
+ flex-direction: column !important;
742
+ }
743
+ [class*="usg_"][class$="_stat"] {
744
+ flex: 0 0 auto !important;
745
+ width: 100% !important;
746
+ min-width: 0 !important;
747
+ }
748
+
749
+ /* ---------- dsh-web-ui polish: settings sheet ----------
750
+ The official dialog is a desktop two-column form; on a phone the
751
+ label/control split leaves a huge dead gap and long descriptions wrap
752
+ into tall stacks. Stack each row (text above, control full-width) and
753
+ compact the nav tabs into an even wrap. */
754
+
755
+ /* Nav tabs: a stable 3-per-row grid (two clean rows instead of a ragged
756
+ wrap) with tighter cells. */
757
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])) > :first-child > :last-child {
758
+ display: grid !important;
759
+ grid-template-columns: repeat(3, 1fr) !important;
760
+ gap: 6px !important;
761
+ }
762
+ [aria-modal="true"] [class$="_navCell"] {
763
+ padding: 6px 8px !important;
764
+ gap: 6px !important;
765
+ font-size: 13px !important;
766
+ justify-content: flex-start !important;
767
+ }
768
+ [aria-modal="true"] [class$="_navCell"] svg {
769
+ width: 14px !important;
770
+ height: 14px !important;
771
+ flex: none !important;
772
+ }
773
+ /* Setting rows: text on top, control below at full width. */
774
+ [aria-modal="true"] [class$="_section"] [class$="_row"] {
775
+ flex-direction: column !important;
776
+ align-items: stretch !important;
777
+ gap: 8px !important;
778
+ }
779
+ [aria-modal="true"] [class$="_section"] [class$="_row"] > :first-child {
780
+ width: 100% !important;
781
+ max-width: none !important;
782
+ }
783
+ [aria-modal="true"] [class$="_section"] [class$="_row"] > :last-child {
784
+ width: 100% !important;
785
+ max-width: none !important;
786
+ }
787
+ /* Appearance mode group: give the cube row a consistent bordered
788
+ segmented look (the official borders differ per state). */
789
+ [aria-modal="true"] [class$="_cubeRow"] > * {
790
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;
791
+ }
792
+
793
+ /* ---------- dsh-web-ui polish: explorer sheet ----------
794
+ The aionui explorer was designed for a desktop side column: compact the
795
+ header, search box and tree rows so a phone shows more entries, and pad
796
+ the scroll bottom so the last row never sits flush on the edge. */
797
+
798
+ [data-aionui-explorer-col] [class$="_tabBar"] {
799
+ height: 36px !important;
800
+ }
801
+ [data-aionui-explorer-col] [class$="_tabBtn"],
802
+ [data-aionui-explorer-col] [class$="_tabBtnActive"] {
803
+ padding: 0 12px !important;
804
+ font-size: 13px !important;
805
+ }
806
+ [data-aionui-explorer-col] [class$="_searchBox"] {
807
+ height: 32px !important;
808
+ font-size: 13px !important;
809
+ }
810
+ [data-aionui-explorer-col] [class$="_treeRow"] {
811
+ height: 30px !important;
812
+ font-size: 13px !important;
813
+ }
814
+ [data-aionui-explorer-col] [class$="_treeRow"] svg {
815
+ width: 14px !important;
816
+ height: 14px !important;
817
+ }
818
+ [data-aionui-explorer-col] [class$="_scrollArea"] {
819
+ padding-bottom: 28px !important;
820
+ }
821
+
822
+ /* ---------- dsh-web-ui polish: drawer footer ----------
823
+ The injected footer actions (Files + Session log) become two equal pill
824
+ buttons instead of text-width capsules. */
825
+
826
+ /* The official footerActions row also hosts the remote-web-ui entry
827
+ row (two icon buttons); without wrapping the two groups squeeze each
828
+ other on one line. Wrap so each group gets its own full-width row. */
829
+ [data-mobile-nav="frame"] [class$="_footerActions"] {
830
+ flex-wrap: wrap !important;
831
+ gap: 6px !important;
832
+ }
833
+ [data-mobile-nav="drawer-actions"] {
834
+ width: 100% !important;
835
+ }
836
+ [data-mobile-nav="drawer-actions"] > button {
837
+ flex: 1 1 0 !important;
838
+ padding: 0 8px !important;
839
+ white-space: nowrap !important;
840
+ }
841
+
842
+ /* ---------- dsh-web-ui polish: floating pet ----------
843
+ The whale-girl pet (dsh-pet) floats at the viewport corner with a
844
+ persisted, draggable position. On phones the pet is scaled down so
845
+ it does not dominate the screen; the plugin's own drag + persist
846
+ still work (the position itself is left alone — the mobile default
847
+ position is seeded via the pet API to just above the composer). */
848
+
849
+ body > [class$="_float"]:has([class$="_sprite"][role="button"]) {
850
+ transform: scale(.66);
851
+ transform-origin: bottom right;
852
+ }
853
+ /* While a modal dialog (settings sheet / export) owns the screen the pet
854
+ floats ABOVE it and covers the dialog content; modal semantics say the
855
+ background is inert, so hide the pet for the modal's lifetime. */
856
+ body:has([aria-modal="true"]) > [class$="_float"]:has([class$="_sprite"][role="button"]) {
857
+ display: none !important;
858
+ }
859
+
860
+ /* ---------- dsh-web-ui polish: conversation stats line ----------
861
+ The official session-status row (turns / steps / LLM time / TTFT /
862
+ cache) is long. The client marks the exact row with
863
+ [data-mobile-nav="stats"] (text-anchored, hashed classes can't be
864
+ targeted). Layout: ONE fixed-height (28px) flex strip that scrolls
865
+ horizontally — the full metrics stream stays reachable by swiping,
866
+ the row never grows vertically, no ellipsis or fade, 12px gaps
867
+ between metric groups, a 2px scrollbar as the swipe affordance.
868
+
869
+ Opt-out: the strip is scoped under body:not([data-pocket-stats="off"])
870
+ so a skin/operator plugin can legally reclaim this 28px band by
871
+ setting document.body.dataset.pocketStats = 'off' (avoids the
872
+ !important weight war that let two footers print on top of each
873
+ other — issue #165). Default behavior is unchanged.
874
+
875
+ Opt-out is two halves, not one. The rules above only stand pocket's
876
+ styling down; the row itself is the CORE's (conversation StatsPills
877
+ dock), so it keeps occupying the band with the core's own styling and
878
+ still collides with whoever took the band over (issue #172).
879
+ [data-mobile-nav="stats"] is a mark pocket writes itself
880
+ (mobile-apply.tsx), so opting out hides that element too — together with
881
+ the core readouts it carries (turns / steps / LLM time / TTFT / cache) and
882
+ the TPS figure pocket folds into it. That is what reclaiming the band
883
+ costs: whoever takes it over renders their own footer. */
884
+
885
+ body:not([data-pocket-stats="off"]) [data-mobile-nav="stats"] {
886
+ display: flex !important;
887
+ flex-flow: row nowrap !important;
888
+ align-items: center !important;
889
+ width: 100% !important;
890
+ max-width: 100% !important;
891
+ min-width: 0 !important;
892
+ height: 28px !important;
893
+ min-height: 28px !important;
894
+ max-height: 28px !important;
895
+ box-sizing: border-box !important;
896
+ white-space: nowrap !important;
897
+ overflow-x: auto !important;
898
+ overflow-y: hidden !important;
899
+ -webkit-overflow-scrolling: touch;
900
+ overscroll-behavior-x: contain;
901
+ scrollbar-width: thin !important;
902
+ scrollbar-color: var(--dsw-alias-border-l1, rgba(0, 0, 0, .28)) transparent !important;
903
+ padding: 0 0 4px !important;
904
+ line-height: 20px !important;
905
+ font-size: 12px !important;
906
+ }
907
+ body:not([data-pocket-stats="off"]) [data-mobile-nav="stats"]::-webkit-scrollbar {
908
+ height: 2px !important;
909
+ }
910
+ body:not([data-pocket-stats="off"]) [data-mobile-nav="stats"]::-webkit-scrollbar-thumb {
911
+ background: var(--dsw-alias-label-tertiary, rgba(0, 0, 0, .3)) !important;
912
+ border-radius: 2px !important;
913
+ }
914
+ body:not([data-pocket-stats="off"]) [data-mobile-nav="stats"]::-webkit-scrollbar-track {
915
+ background: transparent !important;
916
+ }
917
+ body:not([data-pocket-stats="off"]) [data-mobile-nav="stats"] > * {
918
+ display: flex !important;
919
+ flex: 0 0 auto !important;
920
+ flex-flow: row nowrap !important;
921
+ align-items: center !important;
922
+ width: max-content !important;
923
+ min-width: max-content !important;
924
+ max-width: none !important;
925
+ white-space: nowrap !important;
926
+ margin-right: 12px !important;
927
+ padding: 0 !important;
928
+ }
929
+ body:not([data-pocket-stats="off"]) [data-mobile-nav="stats"] > *:last-child {
930
+ margin-right: 0 !important;
931
+ }
932
+ body:not([data-pocket-stats="off"]) [data-mobile-nav="stats"] * {
933
+ white-space: nowrap !important;
934
+ }
935
+
936
+ /* The other half of that opt-out: standing pocket's styling down leaves
937
+ the core's own row in the band, so a skin that took the band over still
938
+ prints underneath it (issue #172). [data-mobile-nav="stats"] is a mark
939
+ pocket writes itself (mobile-apply.tsx) — opting out hides it with the
940
+ mark, and with it the core readouts that row carries. The default path
941
+ never matches this rule.
942
+ (A skin that wants the band *and* the row can take it back with an equally
943
+ specific rule plus !important — this rule carries no extra specificity.) */
944
+ body[data-pocket-stats="off"] [data-mobile-nav="stats"] {
945
+ display: none !important;
946
+ }
947
+
948
+ /* ---------- hero composer on mobile ----------
949
+ The official hero card carries a 2-line textarea plus a tall tool row,
950
+ which reads oversized on a phone. Tighten the empty-state rhythm: keep
951
+ the official centered hero, shrink the textarea line box, slim the card
952
+ padding and the tool row, and close the gap under the headline. */
953
+
954
+ [data-phase="hero"] [class$="_card"]:has(textarea) {
955
+ padding-top: 6px !important;
956
+ gap: 8px !important;
957
+ }
958
+ /* The official composer autosizes the textarea and writes an inline
959
+ height (2 lines on the hero empty state) on the textarea's scroll/grow
960
+ wrappers. :placeholder-shown lets us collapse the EMPTY state to one
961
+ line with !important; as soon as the user types, the pseudo-class no
962
+ longer matches and the autosizer's inline height takes over again — so
963
+ multi-line growth keeps working. */
964
+ [data-phase="hero"] textarea:placeholder-shown {
965
+ height: 28px !important;
966
+ }
967
+ [data-phase="hero"] [class$="_card"]:has(textarea:placeholder-shown) > [class$="_scroll"],
968
+ [data-phase="hero"] [class$="_card"]:has(textarea:placeholder-shown) [class$="_grow"] {
969
+ height: 28px !important;
970
+ }
971
+ [data-phase="hero"] [class$="_card"]:has(textarea) > [class$="_row"] {
972
+ padding-top: 2px !important;
973
+ }
974
+ [data-phase="hero"] [class$="_headline"] {
975
+ line-height: 1.15 !important;
976
+ margin-bottom: 0 !important;
977
+ }
978
+ [data-phase="hero"] [class$="_stack"] {
979
+ gap: 0 !important;
980
+ }
981
+
982
+ /* ---------- 隐藏「添加工作区」入口(手机上配工作区无意义,issue #17 修正) ----------
983
+ 图标按钮的 aria-label 随语言变化(zh「添加工作区」/ en「Add workspace」),
984
+ 两种都覆盖;下拉菜单里的「添加工作区…」项由 fileGuard.ts 的 MutationObserver
985
+ 按文案兜底隐藏(CSS 选不到纯文本节点)。只在窄屏生效——桌面端照常保留。 */
986
+ button[aria-label="添加工作区"],
987
+ button[aria-label="添加工作区…"],
988
+ button[aria-label="Add workspace"],
989
+ button[aria-label="Add workspace…"] {
990
+ display: none !important;
991
+ }
992
+
993
+ /* ---------- 文件链接旁的「复制」按钮(issue #17:复制文件内容) ----------
994
+ 挂在对话里的文件链接(<button>/<a>,文案即路径)紧邻位置,由 fileGuard.ts
995
+ 注入。只屏内可见:桌面端不注入、不显示;这里再兜底一层,避免任何遗漏。
996
+ 文件链接多为 inline,按钮用 inline-flex 紧跟其后即可。 */
997
+ [data-mobile-nav="copy-file"] {
998
+ display: inline-flex !important;
999
+ align-items: center;
1000
+ justify-content: center;
1001
+ margin-left: 6px !important;
1002
+ vertical-align: baseline !important;
1003
+ height: 22px !important;
1004
+ padding: 0 8px !important;
1005
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .14)) !important;
1006
+ border-radius: 6px !important;
1007
+ background: var(--dsw-alias-bg-layer-1, #fff) !important;
1008
+ color: var(--dsw-alias-label-primary, inherit) !important;
1009
+ font-family: inherit !important;
1010
+ font-size: 11px !important;
1011
+ line-height: 1 !important;
1012
+ cursor: pointer !important;
1013
+ -webkit-tap-highlight-color: transparent !important;
1014
+ box-shadow: 0 1px 3px rgba(0, 0, 0, .12) !important;
1015
+ }
1016
+ [data-mobile-nav="copy-file"]:active {
1017
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06)) !important;
1018
+ }
1019
+ [data-mobile-nav="copy-file"][disabled] {
1020
+ opacity: .55 !important;
1021
+ cursor: default !important;
1022
+ }
1023
+ }
1024
+
1025
+ /* ---------- mobile: stop iOS Safari forced zoom on input focus ----------
1026
+ * Inputs are rendered with inline fontSize 13-14px, below the 16px threshold
1027
+ * that makes iOS Safari zoom the whole page on focus (and never recover).
1028
+ * Force the safe 16px minimum on narrow viewports only, so desktop keeps its
1029
+ * tighter metrics. !important is required to beat the inline styles. */
1030
+ @media (max-width: 1024px) {
1031
+ input,
1032
+ textarea,
1033
+ [contenteditable="true"] {
1034
+ font-size: 16px !important;
1035
+ }
1036
+ }
1037
+
1038
+ /* ---------- desktop: the mobile controls must never appear ---------- */
1039
+
1040
+ @media (min-width: 1024px) {
1041
+ [data-mobile-nav="toggle"],
1042
+ [data-mobile-nav="files"],
1043
+ [data-mobile-nav="fab"],
1044
+ [data-mobile-nav="backdrop"],
1045
+ [data-mobile-nav="session-log"],
1046
+ [data-mobile-nav="explorer"],
1047
+ [data-mobile-nav="drawer-actions"] {
1048
+ display: none !important;
1049
+ }
1050
+ }
1051
+
1052
+ `