dsh-zen-remote 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/AGENTS.md +51 -0
  2. package/LICENSE +38 -0
  3. package/README.md +237 -0
  4. package/assets/home.png +0 -0
  5. package/assets/info.png +0 -0
  6. package/assets/pairing.png +0 -0
  7. package/assets/session.png +0 -0
  8. package/assets/sheet.png +0 -0
  9. package/cordis.patch.yml +17 -0
  10. package/cordis.patch.yml.example +16 -0
  11. package/docs/interface.md +200 -0
  12. package/docs/remote-access.en.md +335 -0
  13. package/docs/remote-access.md +422 -0
  14. package/dsh-push.mjs +213 -0
  15. package/lan-gate.mjs +78 -0
  16. package/lib/client.js +6615 -0
  17. package/lib/index.js +310 -0
  18. package/lib/index.js.map +1 -0
  19. package/lib/lan-gate-server.cjs +626 -0
  20. package/lib/types/client/MobileAttachButton.d.ts +25 -0
  21. package/lib/types/client/MobileAttachButton.d.ts.map +1 -0
  22. package/lib/types/client/MobileAttachChips.d.ts +32 -0
  23. package/lib/types/client/MobileAttachChips.d.ts.map +1 -0
  24. package/lib/types/client/MobileDrawerFooter.d.ts +21 -0
  25. package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -0
  26. package/lib/types/client/MobileHome.d.ts +24 -0
  27. package/lib/types/client/MobileHome.d.ts.map +1 -0
  28. package/lib/types/client/MobileHomeChips.d.ts +76 -0
  29. package/lib/types/client/MobileHomeChips.d.ts.map +1 -0
  30. package/lib/types/client/MobileNavOverlay.d.ts +15 -0
  31. package/lib/types/client/MobileNavOverlay.d.ts.map +1 -0
  32. package/lib/types/client/MobileNavToggle.d.ts +18 -0
  33. package/lib/types/client/MobileNavToggle.d.ts.map +1 -0
  34. package/lib/types/client/MobileSessionHeader.d.ts +58 -0
  35. package/lib/types/client/MobileSessionHeader.d.ts.map +1 -0
  36. package/lib/types/client/MobileSessionInfo.d.ts +41 -0
  37. package/lib/types/client/MobileSessionInfo.d.ts.map +1 -0
  38. package/lib/types/client/attach-upload.d.ts +78 -0
  39. package/lib/types/client/attach-upload.d.ts.map +1 -0
  40. package/lib/types/client/chips-store.d.ts +13 -0
  41. package/lib/types/client/chips-store.d.ts.map +1 -0
  42. package/lib/types/client/debug.d.ts +9 -0
  43. package/lib/types/client/debug.d.ts.map +1 -0
  44. package/lib/types/client/effects/aionui-compat.d.ts +4 -0
  45. package/lib/types/client/effects/aionui-compat.d.ts.map +1 -0
  46. package/lib/types/client/effects/gestures.d.ts +7 -0
  47. package/lib/types/client/effects/gestures.d.ts.map +1 -0
  48. package/lib/types/client/effects/header-status.d.ts +15 -0
  49. package/lib/types/client/effects/header-status.d.ts.map +1 -0
  50. package/lib/types/client/effects/phone-chrome.d.ts +106 -0
  51. package/lib/types/client/effects/phone-chrome.d.ts.map +1 -0
  52. package/lib/types/client/effects/turn-fold.d.ts +25 -0
  53. package/lib/types/client/effects/turn-fold.d.ts.map +1 -0
  54. package/lib/types/client/index.d.ts +18 -0
  55. package/lib/types/client/index.d.ts.map +1 -0
  56. package/lib/types/client/locales.d.ts +58 -0
  57. package/lib/types/client/locales.d.ts.map +1 -0
  58. package/lib/types/client/nav-store.d.ts +73 -0
  59. package/lib/types/client/nav-store.d.ts.map +1 -0
  60. package/lib/types/client/session-dot.d.ts +12 -0
  61. package/lib/types/client/session-dot.d.ts.map +1 -0
  62. package/lib/types/client/styles/base.css.d.ts +2 -0
  63. package/lib/types/client/styles/base.css.d.ts.map +1 -0
  64. package/lib/types/client/styles/chips.css.d.ts +2 -0
  65. package/lib/types/client/styles/chips.css.d.ts.map +1 -0
  66. package/lib/types/client/styles/compat.css.d.ts +2 -0
  67. package/lib/types/client/styles/compat.css.d.ts.map +1 -0
  68. package/lib/types/client/styles/composer.css.d.ts +2 -0
  69. package/lib/types/client/styles/composer.css.d.ts.map +1 -0
  70. package/lib/types/client/styles/header.css.d.ts +2 -0
  71. package/lib/types/client/styles/header.css.d.ts.map +1 -0
  72. package/lib/types/client/styles/home.css.d.ts +2 -0
  73. package/lib/types/client/styles/home.css.d.ts.map +1 -0
  74. package/lib/types/client/styles/index.d.ts +15 -0
  75. package/lib/types/client/styles/index.d.ts.map +1 -0
  76. package/lib/types/client/styles/info.css.d.ts +2 -0
  77. package/lib/types/client/styles/info.css.d.ts.map +1 -0
  78. package/lib/types/client/styles/layout.css.d.ts +2 -0
  79. package/lib/types/client/styles/layout.css.d.ts.map +1 -0
  80. package/lib/types/client/styles/misc.css.d.ts +2 -0
  81. package/lib/types/client/styles/misc.css.d.ts.map +1 -0
  82. package/lib/types/client/styles/turn-fold.css.d.ts +2 -0
  83. package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -0
  84. package/lib/types/index.d.ts +83 -0
  85. package/lib/types/index.d.ts.map +1 -0
  86. package/llms.txt +20 -0
  87. package/package.json +128 -0
  88. package/pwa/app.css +95 -0
  89. package/pwa/icons/icon-192.png +0 -0
  90. package/pwa/icons/icon-512.png +0 -0
  91. package/pwa/icons/icon-maskable-512.png +0 -0
  92. package/pwa/icons/icon-maskable.svg +1 -0
  93. package/pwa/icons/icon.svg +1 -0
  94. package/pwa/inject.js +190 -0
  95. package/pwa/manifest.json +29 -0
  96. package/pwa/offline.html +38 -0
  97. package/pwa/sw.js +176 -0
  98. package/pwa/touch-gestures.js +94 -0
  99. package/scripts/build-client.mjs +101 -0
  100. package/scripts/cdp-probe.mjs +64 -0
  101. package/scripts/check-attach-upload.mjs +99 -0
  102. package/scripts/check-sunk-viewport.mjs +33 -0
  103. package/scripts/check-upload-endpoint.mjs +168 -0
  104. package/src/client/MobileAttachButton.tsx +125 -0
  105. package/src/client/MobileAttachChips.tsx +85 -0
  106. package/src/client/MobileDrawerFooter.tsx +56 -0
  107. package/src/client/MobileHome.tsx +348 -0
  108. package/src/client/MobileHomeChips.tsx +426 -0
  109. package/src/client/MobileNavOverlay.tsx +279 -0
  110. package/src/client/MobileNavToggle.tsx +52 -0
  111. package/src/client/MobileSessionHeader.tsx +242 -0
  112. package/src/client/MobileSessionInfo.tsx +303 -0
  113. package/src/client/attach-upload.ts +140 -0
  114. package/src/client/chips-store.ts +41 -0
  115. package/src/client/debug.ts +253 -0
  116. package/src/client/effects/aionui-compat.ts +183 -0
  117. package/src/client/effects/gestures.ts +265 -0
  118. package/src/client/effects/header-status.ts +30 -0
  119. package/src/client/effects/phone-chrome.ts +265 -0
  120. package/src/client/effects/turn-fold.ts +258 -0
  121. package/src/client/index.tsx +227 -0
  122. package/src/client/locales.ts +109 -0
  123. package/src/client/nav-store.ts +78 -0
  124. package/src/client/session-dot.ts +17 -0
  125. package/src/client/styles/base.css.ts +167 -0
  126. package/src/client/styles/chips.css.ts +224 -0
  127. package/src/client/styles/compat.css.ts +681 -0
  128. package/src/client/styles/composer.css.ts +504 -0
  129. package/src/client/styles/header.css.ts +368 -0
  130. package/src/client/styles/home.css.ts +477 -0
  131. package/src/client/styles/index.ts +25 -0
  132. package/src/client/styles/info.css.ts +238 -0
  133. package/src/client/styles/layout.css.ts +501 -0
  134. package/src/client/styles/misc.css.ts +128 -0
  135. package/src/client/styles/turn-fold.css.ts +106 -0
  136. package/src/index.ts +343 -0
@@ -0,0 +1,504 @@
1
+ // composer — phone-only composer reflow (S3, 2026-08-17).
2
+ //
3
+ // Appended after home/header so its < 768px rules win the ties against the
4
+ // shared <= 1023px block in layout/compat/misc (which keeps the v1.0.0
5
+ // composer treatment alive for the 768-1023px tablet range).
6
+ //
7
+ // Official structure this file reshapes (verified live at 390px, see the S3
8
+ // report): InputBar renders
9
+ // _card > _row > [_tools > (_add, _modes > PermissionSelect, input.left)]
10
+ // [_trailing > (input.right, input.model, ContextMeter, _primary)]
11
+ // with `_tools` / `_trailing` as inner flex groups. Flattening both to
12
+ // `display: contents` turns every control into a direct flex item of `_row`,
13
+ // which is what makes a pure `order` reflow possible without touching a
14
+ // single official component.
15
+
16
+ /** The composer input card's bottom control row (the only `_row` that is a direct card child — the ContextMeter panel also has `_row` descendants). */
17
+ const ROW = '[data-slot="conversation.composer.bar"] [class$="_card"] > [class$="_row"]'
18
+ /** The model seat's own root inside the trailing group (long form so it beats the <=1023px pill rules in layout.css.ts). */
19
+ const MODEL = `${ROW} > [class$="_trailing"] > [data-slot="conversation.input.model"] > [class$="_root"]`
20
+ /** The permission select's trigger button (structure only — the PermissionSelect hashed classes are never named). */
21
+ const PERM = `${ROW} > [class$="_tools"] > [class$="_modes"]`
22
+
23
+ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- */
24
+
25
+ /* The file picker the attachment button drives (S7) — hidden at EVERY width,
26
+ deliberately OUTSIDE the phone media block below. A bare <input type="file">
27
+ renders as a native "Choose Files" control, and the slot wrapper around it is
28
+ \`display: contents\`, so an un-hidden one becomes a flex item of the official
29
+ tool row. Scoping this to < 768px once put that control in the DESKTOP
30
+ composer (caught on a live deploy, 2026-08-17): the button that drives it is
31
+ phone-only, but the input it drives is in the DOM at all widths. */
32
+ [data-mobile-nav="attach-picker"] {
33
+ display: none !important;
34
+ }
35
+
36
+ @media (max-width: 767px) {
37
+ /* --- 1. flatten the two official groups ---
38
+ \`_tools\` and \`_trailing\` only exist to cluster controls left/right.
39
+ display:contents dissolves them into \`_row\`, so the six leaf controls
40
+ become siblings in one flex line and \`order\` alone drives the layout:
41
+ [attach · + · permission · model] …elastic gap… [context ring · send]. */
42
+ ${ROW} {
43
+ justify-content: flex-start !important;
44
+ gap: 5px !important;
45
+ padding: 2px 8px 8px !important;
46
+ }
47
+ ${ROW} > [class$="_tools"],
48
+ ${ROW} > [class$="_trailing"] {
49
+ display: contents !important;
50
+ }
51
+
52
+ /* --- 2. the running order ---
53
+ The attachment button (S3 placeholder, S7 wires it up) sits leftmost;
54
+ the official "+" command menu keeps its seat right next to it, where
55
+ the two "insert something" affordances read as one cluster. */
56
+ ${ROW} [data-mobile-nav="attach"] {
57
+ order: 1 !important;
58
+ }
59
+ ${ROW} > [class$="_tools"] > [class$="_add"] {
60
+ order: 2 !important;
61
+ }
62
+ ${ROW} > [class$="_tools"] > [class$="_modes"] {
63
+ order: 3 !important;
64
+ flex: 0 1 auto !important;
65
+ min-width: 0 !important;
66
+ gap: 6px !important;
67
+ }
68
+ /* The model seat carries the elastic gap: everything after it is pushed to
69
+ the right edge, so no spacer element is needed. */
70
+ ${MODEL} {
71
+ order: 4 !important;
72
+ flex: 0 1 auto !important;
73
+ min-width: 0 !important;
74
+ margin-right: auto !important;
75
+ }
76
+ /* Third-party input.right entries park next to the ring rather than
77
+ landing at order 0 (= far left) once the groups are flattened. */
78
+ ${ROW} > [class$="_trailing"] > [data-slot="conversation.input.right"] > * {
79
+ order: 5 !important;
80
+ }
81
+ /* ContextMeter — the only \`span\` ending in _root inside the row. */
82
+ ${ROW} > [class$="_trailing"] > span[class$="_root"] {
83
+ order: 6 !important;
84
+ flex: none !important;
85
+ }
86
+ ${ROW} > [class$="_trailing"] > [class$="_primary"] {
87
+ order: 7 !important;
88
+ }
89
+
90
+ /* --- 3. permission + model as icon-only pills (real-device round 2, 2026-08-17) ---
91
+ S3's icon-and-label capsules read as noise on an actual phone — there
92
+ is no room to usefully show a preset name or a model id, so the label
93
+ text is now hidden outright and both triggers collapse to a plain
94
+ ~44x30 icon button. This AGREES with (rather than fights) the official
95
+ container query (\`@container (width <= 460px) { .trigger:has(.triggerIcon)
96
+ .triggerLabel { display: none } }\`) that S3 had to override — no need
97
+ to override it back. Accessible name is unaffected: both official
98
+ triggers already ship a descriptive aria-label independent of the
99
+ visible text (PermissionSelect: t('input.accessMode', {name}); the
100
+ model trigger: t('trigger.aria'/'trigger.ariaEffort')) — verified live
101
+ in dsh-client-ui-conversation / dsh-client-ui-model-selection lib/
102
+ client.js, 2026-08-17 — so hiding the label costs nothing for screen
103
+ readers. The permission trigger only renders an icon for the
104
+ "read-only" / "workspace-write" presets (permissionGlyphs in
105
+ dsh-client-ui-conversation) — "Full access" and any host-configured
106
+ preset name fall back to chevron-only, a known gap in the official
107
+ markup this plugin cannot fill without inventing new icon meaning.
108
+ The model trigger never renders an icon at all (only label + optional
109
+ effort text + chevron), so its ::before below draws one from a
110
+ primitives icon path (Sparkle — the closest existing "model" glyph) as
111
+ a CSS-only pseudo-element: it survives React re-renders for free
112
+ (unlike a DOM-injected node, which would need a MutationObserver, see
113
+ the preview-full-toggle pitfall in AGENTS.md) and does not touch
114
+ accessible-name computation (empty generated content). S3's
115
+ rtl-ellipsis trick on the model label is simply inert under
116
+ display:none now; left alone rather than unpicked. */
117
+ ${PERM} [class$="_triggerLabel"],
118
+ ${MODEL} > [class$="_trigger"] > [class$="_triggerLabel"],
119
+ ${MODEL} > [class$="_trigger"] > [class$="_triggerEffort"] {
120
+ display: none !important;
121
+ }
122
+ ${PERM} button[class$="_trigger"],
123
+ ${MODEL} > [class$="_trigger"] {
124
+ background: var(--dsw-specific-selector, rgba(127, 127, 127, .12)) !important;
125
+ width: 44px !important;
126
+ height: 30px !important;
127
+ max-width: none !important;
128
+ min-width: 0 !important;
129
+ padding: 0 !important;
130
+ gap: 2px !important;
131
+ justify-content: center !important;
132
+ border-radius: 999px !important;
133
+ touch-action: manipulation !important;
134
+ }
135
+ /* PermissionSelect wraps its trigger in the Menu primitive's root span,
136
+ which must shrink to the icon button's fixed width. */
137
+ ${PERM} > span:has(> button[class$="_trigger"]) {
138
+ flex: 0 0 auto !important;
139
+ min-width: 0 !important;
140
+ }
141
+ /* ic_ds_sparkle_16 (@deepseek-ai/dsh-client-ui-primitives IconSparkle16
142
+ path, copied verbatim) as a mask so it inherits currentColor like every
143
+ other icon in the row — the model trigger has no official icon slot to
144
+ hook into. */
145
+ ${MODEL} > [class$="_trigger"]::before {
146
+ content: '';
147
+ width: 16px;
148
+ height: 16px;
149
+ flex: none;
150
+ background: currentColor;
151
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6.1 3.1Q6.6 7.8 11.3 8.3Q6.6 8.8 6.1 13.5Q5.6 8.8 0.9 8.3Q5.6 7.8 6.1 3.1Z'/%3E%3Cpath d='M11.9 1Q12.2 3.7 14.9 4Q12.2 4.3 11.9 7Q11.6 4.3 8.9 4Q11.6 3.7 11.9 1Z'/%3E%3C/svg%3E");
152
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6.1 3.1Q6.6 7.8 11.3 8.3Q6.6 8.8 6.1 13.5Q5.6 8.8 0.9 8.3Q5.6 7.8 6.1 3.1Z'/%3E%3Cpath d='M11.9 1Q12.2 3.7 14.9 4Q12.2 4.3 11.9 7Q11.6 4.3 8.9 4Q11.6 3.7 11.9 1Z'/%3E%3C/svg%3E");
153
+ -webkit-mask-size: contain;
154
+ mask-size: contain;
155
+ -webkit-mask-repeat: no-repeat;
156
+ mask-repeat: no-repeat;
157
+ }
158
+
159
+ /* --- 4. both menus become bottom sheets ---
160
+ The permission menu is the Menu primitive (role=menu, absolute, side=top)
161
+ and the model menu is ModelSelect's own \`_menu\` (absolute, bottom+right).
162
+ Neither has a transformed ancestor between it and the viewport, so
163
+ position:fixed re-anchors both to the screen edge. Only the shell moves —
164
+ the items, the two-level model panes and every selection handler stay
165
+ official. */
166
+ ${PERM} [role="menu"],
167
+ ${MODEL} > [class$="_menu"] {
168
+ position: fixed !important;
169
+ left: 0 !important;
170
+ right: 0 !important;
171
+ bottom: 0 !important;
172
+ top: auto !important;
173
+ transform: none !important;
174
+ width: 100% !important;
175
+ min-width: 0 !important;
176
+ max-width: none !important;
177
+ max-height: min(70dvh, 520px) !important;
178
+ box-sizing: border-box !important;
179
+ border-radius: 16px 16px 0 0 !important;
180
+ border-bottom: none !important;
181
+ padding: 8px 8px calc(8px + var(--mnav-sab)) !important;
182
+ z-index: 60 !important;
183
+ box-shadow: 0 -8px 32px rgba(0, 0, 0, .18) !important;
184
+ }
185
+ /* 44pt+ rows in both sheets (Menu items, model options, and the model
186
+ sheet's two root cells that drill into the model / effort panes). */
187
+ ${PERM} [role="menu"] [role="menuitem"],
188
+ ${MODEL} > [class$="_menu"] [class$="_option"],
189
+ ${MODEL} > [class$="_menu"] [class$="_cell"] {
190
+ min-height: 48px !important;
191
+ border-radius: 12px !important;
192
+ font-size: 15px !important;
193
+ }
194
+ /* --- 4b. the official scroll-to-bottom button must not poke through the
195
+ sheet (real-device follow-up, 2026-08-17) ---
196
+ ChatView's own "jump to latest" button (aria-label t("chat.toBottom"))
197
+ is \`position: sticky; z-index: 8\` inside the message column, not
198
+ \`position: fixed\` — it never escapes to the same top-level stacking
199
+ context our sheets get promoted to, so raising the sheet's z-index
200
+ further does nothing (measured live: it still rendered on top at
201
+ z-index 60 vs 8). Rather than chase engine-specific stacking-context
202
+ semantics (Chromium and WebKit do not always agree here — see AGENTS.md
203
+ CDP/document.hidden lesson for another instance of that), this hides
204
+ the button outright while either sheet is open and lets it reappear on
205
+ close: a plain \`display: none\` behind a live \`:has()\` read is correct
206
+ regardless of which stacking rules the engine happens to apply.
207
+ Selector: no hashed classes (dsh-client-ui-conversation lib/client.js,
208
+ verified 2026-08-17) — \`data-chat-flow\` is the one stable attribute on
209
+ the message column, and the button's wrapper is its only sibling
210
+ (ChatView only ever renders the column and, conditionally, this one
211
+ slot), so the adjacent-sibling combinator pins it precisely. */
212
+ body:has(${PERM} [role="menu"]) [data-chat-flow] + div,
213
+ body:has(${MODEL} > [class$="_menu"]) [data-chat-flow] + div {
214
+ display: none !important;
215
+ }
216
+ /* --- 5. input box: two lines minimum, five lines maximum ---
217
+ The official autosizer drives the box off the hidden mirror's height, so
218
+ a min-height on the mirror IS the min-height of the field; the scroll cap
219
+ rides the official custom property. 52px = 2 x 24px line box + 4px pad,
220
+ 124px = 5 lines. The hero card keeps its own one-line collapse (misc.css
221
+ pins _scroll/_grow while the placeholder shows). */
222
+ [data-slot="conversation.composer.bar"] [class$="_card"] [class$="_mirror"] {
223
+ min-height: 52px !important;
224
+ }
225
+ [class$="_composerSeat"] {
226
+ --dsh-composer-text-max-height: 124px;
227
+ }
228
+
229
+ /* --- 6. no divider above OR below the message list ---
230
+ Instead of a rule the messages butt against, the message scroller
231
+ fades out over its last 26px (S3, no divider above the composer) AND
232
+ fades in over its first 20px (S3.1 real-device round 2: the header's
233
+ own bottom line is removed too, styles/header.css.ts's
234
+ \`header::after\` rule). The mask lives on the scroll body (NOT on the
235
+ header or the composer): a mask clips everything it paints, and both
236
+ surfaces host position:fixed children (composer's permission/model
237
+ sheets, any future header overlay) that must not be clipped.
238
+ \`mask-image\` can only be declared once per element, so both fades are
239
+ ONE linear-gradient rather than two separate declarations (the second
240
+ would silently replace the first) — this is the merge of what used to
241
+ be S3's bottom-only mask. */
242
+ [class$="_scrollBody"] {
243
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 26px), transparent 100%);
244
+ mask-image: linear-gradient(to bottom, transparent 0, #000 20px, #000 calc(100% - 26px), transparent 100%);
245
+ }
246
+ [class$="_composerSeat"],
247
+ [class$="_composerStack"] {
248
+ border-top: none !important;
249
+ }
250
+
251
+ /* --- 7. dock entries become mini chips above the input card ---
252
+ conversation.input.dock is display:contents (inline style), so its
253
+ entries are stacked rows of the composer column. Forcing the slot itself
254
+ to flex turns them into ONE scrollable chip line outside and above the
255
+ card. Written against the slot, not against any particular plugin's chip
256
+ (the git branch chip and the todo panel are third-party and may not be
257
+ installed at all). */
258
+ [data-slot="conversation.input.dock"] {
259
+ display: flex !important;
260
+ flex-flow: row nowrap !important;
261
+ align-items: center !important;
262
+ gap: 6px !important;
263
+ min-width: 0 !important;
264
+ margin: 0 16px !important;
265
+ overflow-x: auto !important;
266
+ overflow-y: hidden !important;
267
+ scrollbar-width: none !important;
268
+ }
269
+ [data-slot="conversation.input.dock"]::-webkit-scrollbar {
270
+ display: none !important;
271
+ }
272
+ [data-slot="conversation.input.dock"] > * {
273
+ flex: 0 0 auto !important;
274
+ max-width: 70% !important;
275
+ min-height: 26px !important;
276
+ max-height: 26px !important;
277
+ border-radius: 999px !important;
278
+ font-size: 11.5px !important;
279
+ line-height: 18px !important;
280
+ overflow: hidden !important;
281
+ }
282
+ /* The git-graph chip carries a 34px tablet target from misc.css; that
283
+ selector is more specific, so restate it at the phone breakpoint. */
284
+ [data-slot="conversation.input.dock"] [data-gitgraph-chip-anchor] [data-gitgraph-chip] {
285
+ min-height: 26px !important;
286
+ padding: 0 10px !important;
287
+ font-size: 11.5px !important;
288
+ }
289
+
290
+ /* --- 7b. attachment chips (S7.1) ---
291
+ Our own dock entry opts OUT of the 26px pill cage above: an image chip is
292
+ a 48px tile. The row sits tight under the card's top edge, so the chips
293
+ read as part of the composer rather than as a floating strip. */
294
+ [data-slot="conversation.input.dock"] > [data-mobile-nav="attach-chips"] {
295
+ display: flex !important;
296
+ align-items: center !important;
297
+ gap: 6px !important;
298
+ max-width: none !important;
299
+ min-height: 0 !important;
300
+ max-height: none !important;
301
+ border-radius: 0 !important;
302
+ overflow: visible !important;
303
+ margin-bottom: -2px;
304
+ }
305
+ [data-mobile-nav="attach-chip"] {
306
+ position: relative;
307
+ display: inline-flex;
308
+ align-items: center;
309
+ gap: 6px;
310
+ flex: 0 0 auto;
311
+ max-width: 62vw;
312
+ background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, .12));
313
+ color: var(--dsw-alias-label-primary, inherit);
314
+ }
315
+ [data-mobile-nav="attach-chip"][data-kind="file"] {
316
+ height: 26px;
317
+ padding: 0 4px 0 8px;
318
+ border-radius: 999px;
319
+ font-size: 11.5px;
320
+ line-height: 18px;
321
+ }
322
+ /* An image chip is the thumbnail: no name line, the filename lives in the
323
+ title attribute (and in the draft text right below). */
324
+ [data-mobile-nav="attach-chip"][data-kind="image"] {
325
+ width: 48px;
326
+ height: 48px;
327
+ padding: 0;
328
+ border-radius: 10px;
329
+ overflow: hidden;
330
+ }
331
+ [data-mobile-nav="attach-chip-art"] {
332
+ position: relative;
333
+ display: grid;
334
+ place-items: center;
335
+ flex: none;
336
+ overflow: hidden;
337
+ }
338
+ [data-kind="file"] > [data-mobile-nav="attach-chip-art"] {
339
+ width: 16px;
340
+ height: 16px;
341
+ opacity: .7;
342
+ }
343
+ [data-kind="image"] > [data-mobile-nav="attach-chip-art"] {
344
+ width: 100%;
345
+ height: 100%;
346
+ }
347
+ /* The <img> is stacked over the paperclip, not swapped for it: a format the
348
+ engine cannot decode (HEIC outside WebKit) simply paints nothing and the
349
+ icon shows through — no onError state to carry. */
350
+ [data-mobile-nav="attach-chip-art"] img {
351
+ position: absolute;
352
+ inset: 0;
353
+ width: 100%;
354
+ height: 100%;
355
+ object-fit: cover;
356
+ background: var(--dsw-alias-bg-layer-2, rgba(127, 127, 127, .18));
357
+ }
358
+ [data-mobile-nav="attach-chip-name"] {
359
+ min-width: 0;
360
+ overflow: hidden;
361
+ white-space: nowrap;
362
+ }
363
+ [data-mobile-nav="attach-chip-remove"] {
364
+ display: grid;
365
+ place-items: center;
366
+ flex: none;
367
+ width: 18px;
368
+ height: 18px;
369
+ padding: 0;
370
+ border: none;
371
+ border-radius: 999px;
372
+ background: transparent;
373
+ color: inherit;
374
+ cursor: pointer;
375
+ touch-action: manipulation;
376
+ }
377
+ /* On a tile the × floats in the corner over the picture, so it needs its own
378
+ ground to stay legible against an arbitrary photo. */
379
+ [data-kind="image"] > [data-mobile-nav="attach-chip-remove"] {
380
+ position: absolute;
381
+ top: 2px;
382
+ right: 2px;
383
+ background: rgba(0, 0, 0, .55);
384
+ color: #fff;
385
+ }
386
+ [data-mobile-nav="attach-chip-remove"]:active {
387
+ transform: scale(.9);
388
+ }
389
+
390
+ /* --- 8. the official stats strip leaves the composer ---
391
+ Its data moves into the session info card (S4). The row is the composer
392
+ dock's own \`_root\` entry; the slot itself stays live for later entries. */
393
+ [data-slot="conversation.composer.dock"] > [class$="_root"] {
394
+ display: none !important;
395
+ }
396
+
397
+ /* --- 9. the attachment button (S7; its file input is hidden at the top of
398
+ this file, at every width) --- */
399
+ [data-mobile-nav="attach"] {
400
+ position: relative;
401
+ width: 28px !important;
402
+ height: 28px !important;
403
+ flex: none !important;
404
+ display: grid !important;
405
+ place-items: center !important;
406
+ padding: 0 !important;
407
+ border: none !important;
408
+ border-radius: 999px !important;
409
+ background: var(--dsw-specific-selector, rgba(127, 127, 127, .12));
410
+ color: var(--dsw-alias-label-primary, inherit);
411
+ cursor: pointer;
412
+ touch-action: manipulation;
413
+ }
414
+ [data-mobile-nav="attach"]:active {
415
+ transform: scale(.94);
416
+ transition: transform .12s;
417
+ }
418
+ /* Busy: a ring sweeps around the paperclip. Drawn with a conic gradient in
419
+ a pseudo-element rather than a spinner node, so the official React tree
420
+ around us has nothing extra to re-render (same reasoning as the model
421
+ pill's mask icon, S3.1). */
422
+ [data-mobile-nav="attach"][data-busy]::after {
423
+ content: "";
424
+ position: absolute;
425
+ inset: -2px;
426
+ border-radius: 999px;
427
+ background: conic-gradient(from 0deg, transparent 0 65%, currentColor 100%);
428
+ mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
429
+ -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 2px), #000 calc(100% - 2px));
430
+ animation: mnav-attach-spin .9s linear infinite;
431
+ pointer-events: none;
432
+ }
433
+ @keyframes mnav-attach-spin {
434
+ to { transform: rotate(1turn); }
435
+ }
436
+ @media (prefers-reduced-motion: reduce) {
437
+ [data-mobile-nav="attach"][data-busy]::after {
438
+ animation-duration: 3s;
439
+ }
440
+ }
441
+ /* Failure note: one line above the button, tap-to-dismiss. Sits on the
442
+ composer card (z 2) rather than in an overlay layer — nothing here needs
443
+ to clear the permission/model sheets. */
444
+ [data-mobile-nav="attach-error"] {
445
+ position: absolute;
446
+ bottom: calc(100% + 6px);
447
+ left: 0;
448
+ z-index: 2;
449
+ max-width: 62vw;
450
+ padding: 5px 8px;
451
+ border-radius: 8px;
452
+ font-size: 12px;
453
+ line-height: 1.3;
454
+ white-space: nowrap;
455
+ overflow: hidden;
456
+ text-overflow: ellipsis;
457
+ background: var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, .82));
458
+ color: var(--dsw-alias-label-primary, #fff);
459
+ box-shadow: 0 2px 8px rgba(0, 0, 0, .24);
460
+ }
461
+
462
+ /* --- 9. home-indicator clearance (S4.1, 2026-08-17) ---
463
+ Owned HERE, not by the gateway half. That plugin used to carry
464
+ [data-slot="conversation.composer"] { padding-bottom: max(env(safe-area-inset-bottom), 8px) }
465
+ in both pwa/app.css and the gateway's inline DEVICE_CSS. Both were
466
+ INERT and had always been: that slot element is \`display: contents\`
467
+ (measured 2026-08-17 — the slot wrapper generates no box, so padding on
468
+ it is discarded), which is why raising it never moved anything. The rule
469
+ is deleted on the PWA side and restated here on an element that actually
470
+ lays out, reading --mnav-sab so ?mobile-nav-inset=54,34 can regress it
471
+ off-device (env() is hard 0 on desktop — the whole reason S2.1 exists).
472
+
473
+ Target: the card should look EQUALLY inset on all four sides. This same
474
+ element carries the card's side inset (\`padding: 0 16px\` officially,
475
+ measured 16px left and 16px right at 390px), so the bottom gap is simply
476
+ capped at that same 16px. Clearing the FULL 34px inset — the naive
477
+ max(sab, 8px) — is what read as "too thick": it is more than double the
478
+ side margin, so the card looked shoved up off the bottom edge.
479
+
480
+ clamp() says all three requirements at once:
481
+ - floor 8px -> a device with no home indicator (sab: 0) keeps the
482
+ official 8px exactly, so this is a strict no-op there;
483
+ - track sab -> a shallower inset than 16px is honoured as-is;
484
+ - cap 16px -> a full-size indicator (sab: 34) lands on 16px, equal
485
+ to the side inset, which is the look being asked for.
486
+ Keep the 16px in step with the side padding above if that ever changes;
487
+ that equality IS the spec here, not a coincidence. */
488
+ [data-slot="conversation.composer.bar"] > [class$="_root"] {
489
+ padding-bottom: clamp(8px, var(--mnav-sab), 16px) !important;
490
+ }
491
+ }
492
+
493
+ /* The attachment button and its preview row only exist for the phone shell.
494
+ Both render at every width (the slots are not breakpoint-aware), so both
495
+ need hiding here — the dock-row rules that shape the chips live in the
496
+ < 768px block and would otherwise leave a bare, unstyled chip line in the
497
+ desktop composer. */
498
+ @media (min-width: 768px) {
499
+ [data-mobile-nav="attach"],
500
+ [data-mobile-nav="attach-chips"] {
501
+ display: none !important;
502
+ }
503
+ }
504
+ `