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,368 @@
1
+ // header — session-page header five-piece reflow (S2, 2026-08-17). Scoped
2
+ // entirely to (max-width: 767px) and appended LAST (after home.css.ts) so
3
+ // its rules win ties against the shared <=1023px block in layout.css.ts —
4
+ // same reasoning as home.css.ts: 768-1023px keeps the v1.0.0 drawer header
5
+ // untouched, >=1024px is a strict no-op.
6
+ //
7
+ // DOM this reflows (dsh-client-ui-conversation lib/client.js:6949-7009,
8
+ // verified live 2026-08-17 at 375px — class names below are the CURRENT
9
+ // hashed values, always targeted by suffix):
10
+ // header ConversationSessionHeader root
11
+ // div.titleRow
12
+ // div.titleCluster (official: a flex cluster; we flatten it)
13
+ // nav.crumbs breadcrumb chain, last segment = the title
14
+ // div.headerActions [data-slot="conversation.session.header.actions"]
15
+ // div.headerUtilities [data-slot="conversation.session.header.utilities"]
16
+ // div.tabs[role="tablist"] Chat/Trajectory, only when there is >1 view
17
+
18
+ export const HEADER_CSS = `/* ---------- session header five-piece reflow (< 768px) ---------- */
19
+
20
+ /* New elements render unconditionally (React does not know about media
21
+ queries); default them to hidden so 768px+ never sees them, then
22
+ re-enable inside the phone block below. Belt-and-braces alongside the
23
+ scoped rules — mirrors the existing [data-mobile-nav="*"] desktop no-op
24
+ list in misc.css.ts. */
25
+ [data-mobile-nav="header-back"],
26
+ [data-mobile-nav="header-viewrow"],
27
+ [data-mobile-nav="header-info"],
28
+ [data-mobile-nav="header-workbench"],
29
+ /* The workbench close pill's default-hidden used to live in compat.css.ts —
30
+ but EVERYTHING in layout/compat/misc sits inside one shared
31
+ (max-width: 1023px) block (opened at the top of layout, closed at the end
32
+ of misc), so that "top-level" rule silently never applied at >=1024 and
33
+ the pill rendered as an unstyled button on desktop (2026-08-17 report).
34
+ This file is appended after the shared block closes, so here it is truly
35
+ global; the <=767px :has() show rule in compat overrides it with
36
+ !important. */
37
+ [data-mobile-nav="better-sidebar-close"] {
38
+ display: none;
39
+ }
40
+
41
+ @media (max-width: 767px) {
42
+ /* The Files action targeted the dsh-web-ui-all (aionui) explorer sheet —
43
+ a different, unrelated suite from the workbench entry this slice adds
44
+ (dsh-better-sidebar). Hidden here only; 768-1023px keeps it exactly as
45
+ v1.0.0 shipped it. */
46
+ [data-mobile-nav="files"] {
47
+ display: none !important;
48
+ }
49
+
50
+ /* --- three-column header: [back 92px] [title, truly centered] [utilities 92px] --- */
51
+ /* The bottom padding is the view-switch row's seat: that row is
52
+ position:absolute (see the bottom of this file — it renders two levels
53
+ deep inside headerActions and can never be a grid item of titleRow), so
54
+ without a reserve it would hang over the message list. 28px = its
55
+ height. Unconditional rather than header:has([data-mobile-nav=
56
+ "header-viewrow"]) on purpose: :has() is silently dropped by pre-105
57
+ WebViews (see AGENTS.md) and a 28px overlap on the first message is a
58
+ worse failure than 28px of empty band in the (never observed in
59
+ practice) single-view session. Total header chrome: 48 + 28 = 76px.
60
+ Left/right 8px (S2.1 report leftover, fixed in S4): the official header
61
+ had zero horizontal padding, so the utilities button cluster's right
62
+ edge sat flush against the screen edge (x=390 at 390px width, no
63
+ margin). The 92px grid columns absorb this fine — they're fixed track
64
+ widths, only the center 1fr column shrinks by 16px. */
65
+ [data-phase] header {
66
+ position: relative;
67
+ padding: 0 8px 28px !important;
68
+ }
69
+ /* No header bottom line — fade instead (real-device round 2, 2026-08-17).
70
+ The official header draws its border as a \`::after\` 1px bar (the
71
+ \`border-bottom\` on the header itself is transparent, just a layout
72
+ reserve — dsh-client-ui-conversation lib/client.js ".wSkVaW_header{
73
+ border-bottom:1px solid #0000}.wSkVaW_header:after{...height:1px;
74
+ position:absolute;bottom:1px...}", verified live 2026-08-17), so
75
+ hiding the pseudo-element is enough; the header's own border-bottom
76
+ never had a visible color to begin with. The message scroller now
77
+ fades in from under the header instead (styles/composer.css.ts, "no
78
+ divider above OR below the message list" — the two edges share one
79
+ mask-image on [class$="_scrollBody"]). */
80
+ [data-phase] header::after {
81
+ display: none !important;
82
+ }
83
+ /* grid-row: 1 on every item is load-bearing, not decoration (S2.1 fix for
84
+ the "标题被挤下去" report). The three items are placed with explicit
85
+ grid-column but arrive in DOM order crumbs(2) → headerActions(1) →
86
+ headerUtilities(3): sparse auto-placement never moves its cursor
87
+ backwards, so headerActions and headerUtilities were pushed onto an
88
+ implicit SECOND row. Measured at 390px: row1 = crumbs 28px, row2 =
89
+ back button 44px, titleRow = 72px with the title stuck at the top
90
+ instead of centered. Pinning the row makes it one 48px band again. */
91
+ [data-phase] header [class$="_titleRow"] {
92
+ position: relative;
93
+ display: grid;
94
+ grid-template-columns: 92px 1fr 92px;
95
+ grid-template-rows: minmax(48px, auto);
96
+ align-items: center;
97
+ min-height: 48px;
98
+ padding: 0 !important;
99
+ }
100
+ /* Flatten the official cluster so its two children (crumbs, headerActions)
101
+ become direct grid items of titleRow instead of a nested flex box. */
102
+ [data-phase] header [class$="_titleCluster"] {
103
+ display: contents;
104
+ }
105
+ /* margin-left: 0 cancels layout.css.ts's shared <=1023px
106
+ "margin-left: auto !important" on this exact selector (it pushed the
107
+ Files button to the row's right edge in the old non-grid header;
108
+ inside our grid column it instead shoves the back button 48px right of
109
+ the column's start — the auto margin absorbs the leftover space
110
+ between the button's content width and the 92px column). */
111
+ [data-phase] header [class$="_headerActions"] {
112
+ grid-column: 1;
113
+ grid-row: 1;
114
+ justify-self: start;
115
+ margin-left: 0 !important;
116
+ display: flex;
117
+ align-items: center;
118
+ min-width: 0;
119
+ }
120
+ /* layout.css.ts's shared <=1023px block hides this element outright
121
+ (header > :first-child > :last-child { display: none !important },
122
+ specificity (0,3,1) — v1.0.0 relocated the desktop Session-log capsule
123
+ to the drawer footer on every narrow width). That selector is repeated
124
+ here at equal specificity so the later source order wins the display
125
+ property; the class-suffix selector alone (0,2,1) would silently lose. */
126
+ [data-phase] header [class$="_headerUtilities"],
127
+ [data-phase] header > :first-child > :last-child {
128
+ grid-column: 3;
129
+ grid-row: 1;
130
+ justify-self: end;
131
+ display: flex !important;
132
+ align-items: center;
133
+ gap: 4px;
134
+ min-width: 0;
135
+ }
136
+ /* justify-self: stretch (the grid default) so this fills the whole 1fr
137
+ column as a definite-width box — center/center-content happens inside
138
+ it. justify-self: center (tried first) sizes the item to its content
139
+ with no automatic column cap, and the obvious fix — max-width: 100% —
140
+ resolves against the WRONG box here (measured 93.6px at 390px, where
141
+ the column is ~206px): the item's DOM parent (titleCluster) is
142
+ display:contents, and percentage max-width does not reliably resolve
143
+ through a boxless ancestor to the grid track. Stretching sidesteps the
144
+ percentage entirely — but layout.css.ts's shared <=1023px block ALSO
145
+ caps this exact selector at max-width: 24vw !important (93.6px at
146
+ 390px — the value this rule used to silently inherit once its own
147
+ max-width was dropped in favor of stretch), so that must be reset to
148
+ none here too. */
149
+ [data-phase] header [class$="_crumbs"] {
150
+ grid-column: 2;
151
+ grid-row: 1;
152
+ justify-self: stretch;
153
+ max-width: none !important;
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ min-width: 0;
158
+ overflow: hidden;
159
+ }
160
+ /* Only the current session's own name is the "title" — the parent chain
161
+ (subagent breadcrumbs) is the "多余项" the design calls out to hide,
162
+ while the current segment (and its running dot, see below) stays. */
163
+ [data-phase] header [class$="_crumbSeg"]:not(:last-child) {
164
+ display: none !important;
165
+ }
166
+ [data-phase] header [class$="_crumbSeg"]:last-child {
167
+ display: flex;
168
+ align-items: center;
169
+ min-width: 0;
170
+ max-width: 100%;
171
+ }
172
+ [data-phase] header [class$="_crumbSeg"]:last-child [class$="_crumbSep"] {
173
+ display: none !important;
174
+ }
175
+ /* BOTH selectors are required: [class$=] matches the whole class
176
+ ATTRIBUTE's suffix, and the current-session title button's attribute is
177
+ "wSkVaW_crumb wSkVaW_crumbCurrent" — it ends in _crumbCurrent, so the _crumb
178
+ suffix selector alone silently missed the one element that matters
179
+ (measured live: the title stayed 14px while the rule sat in the bundle,
180
+ 2026-08-17). */
181
+ [data-phase] header [class$="_crumb"],
182
+ [data-phase] header [class$="_crumbCurrent"] {
183
+ overflow: hidden;
184
+ text-overflow: ellipsis;
185
+ white-space: nowrap;
186
+ min-width: 0;
187
+ max-width: 100%;
188
+ /* Real-device feedback (2026-08-17): the session title read too small
189
+ next to the enlarged header icons — 19px sits just under the home
190
+ screen's 22px workspace title, keeping the two-level hierarchy. */
191
+ font-size: 19px;
192
+ font-weight: 600;
193
+ }
194
+
195
+ /* Running-status dot: no official element to reposition (the header
196
+ renders only the crumb title, the two action slots, and the tablist —
197
+ no status indicator), so this is the plan's documented fallback —
198
+ read the data ourselves and draw it (effects/header-status.ts stamps
199
+ data-mobile-nav-dot on the frame). A ::after on the title crumb keeps
200
+ it truly inline with the (centered, truncated) text instead of a
201
+ separately positioned box that would fight the centering math. */
202
+ [data-phase] header [class$="_crumbCurrent"]::after {
203
+ content: '';
204
+ display: none;
205
+ width: 6px;
206
+ height: 6px;
207
+ margin-left: 6px;
208
+ border-radius: 50%;
209
+ vertical-align: middle;
210
+ }
211
+ [data-mobile-nav="frame"][data-mobile-nav-dot] header [class$="_crumbCurrent"]::after {
212
+ display: inline-block;
213
+ }
214
+ [data-mobile-nav="frame"][data-mobile-nav-dot="ongoing"] header [class$="_crumbCurrent"]::after {
215
+ background: var(--dsw-alias-state-business-primary, #4f6ef7);
216
+ }
217
+ [data-mobile-nav="frame"][data-mobile-nav-dot="warning"] header [class$="_crumbCurrent"]::after {
218
+ background: var(--dsw-alias-state-warn-primary, #d97706);
219
+ }
220
+ [data-mobile-nav="frame"][data-mobile-nav-dot="done"] header [class$="_crumbCurrent"]::after {
221
+ background: var(--dsw-alias-state-success-primary, #16a34a);
222
+ }
223
+
224
+ /* Everything else the official header puts in these two list slots — the
225
+ agent-preset mode badge, the jobs/subagent trigger, the desktop
226
+ Session-log download capsule, and this plugin's own legacy directory
227
+ toggle — is the "不常驻" set the design defers to the S4 info card.
228
+ Scoped by the slots' own [data-slot] wrapper (a stable, non-hashed
229
+ contract marker) rather than enumerating each registrant, so a future
230
+ third-party header action is hidden by default too. */
231
+ [data-phase] header [data-slot="conversation.session.header.actions"] > *,
232
+ [data-phase] header [data-slot="conversation.session.header.utilities"] > * {
233
+ display: none !important;
234
+ }
235
+ /* The agent-preset mode badge specifically survives the blanket hide
236
+ above: layout.css.ts's shared <=1023px block targets it directly
237
+ (header [class$="_label"]:has(> svg) { display: block !important }),
238
+ specificity (0,3,1), higher than the [data-slot] wrapper rule (0,2,1) —
239
+ display wins on specificity before source order, so a same-selector
240
+ override is needed here too. */
241
+ [data-phase] header [class$="_label"]:has(> svg) {
242
+ display: none !important;
243
+ }
244
+ [data-phase] header [data-slot="conversation.session.header.actions"] > [data-mobile-nav="header-back"] {
245
+ display: inline-flex !important;
246
+ }
247
+ /* header-viewrow is position:absolute (see below), so its re-shown
248
+ display value must match the flex layout its own rule declares. */
249
+ [data-phase] header [data-slot="conversation.session.header.actions"] > [data-mobile-nav="header-viewrow"] {
250
+ display: flex !important;
251
+ }
252
+ [data-phase] header [data-slot="conversation.session.header.utilities"] > [data-mobile-nav="header-info"],
253
+ [data-phase] header [data-slot="conversation.session.header.utilities"] > [data-mobile-nav="header-workbench"] {
254
+ display: inline-flex !important;
255
+ }
256
+
257
+ /* Back button: 44pt touch target, leftmost column. */
258
+ [data-mobile-nav="header-back"] {
259
+ display: inline-flex;
260
+ align-items: center;
261
+ justify-content: center;
262
+ width: 44px;
263
+ height: 44px;
264
+ padding: 0;
265
+ border: none;
266
+ background: transparent;
267
+ color: var(--dsw-alias-label-primary, inherit);
268
+ cursor: pointer;
269
+ touch-action: manipulation;
270
+ -webkit-tap-highlight-color: transparent;
271
+ }
272
+ [data-mobile-nav="header-back"]:active {
273
+ opacity: .6;
274
+ }
275
+
276
+ /* Info + workbench utility buttons, rightmost column. */
277
+ [data-mobile-nav="header-info"],
278
+ [data-mobile-nav="header-workbench"] {
279
+ display: inline-flex;
280
+ align-items: center;
281
+ justify-content: center;
282
+ width: 34px;
283
+ height: 34px;
284
+ padding: 0;
285
+ border: none;
286
+ border-radius: 50%;
287
+ background: transparent;
288
+ color: var(--dsw-alias-label-secondary, inherit);
289
+ cursor: pointer;
290
+ touch-action: manipulation;
291
+ -webkit-tap-highlight-color: transparent;
292
+ }
293
+ [data-mobile-nav="header-info"]:active,
294
+ [data-mobile-nav="header-workbench"]:active {
295
+ background: var(--dsw-alias-interactive-bg-hover, rgba(0, 0, 0, .06));
296
+ }
297
+ /* Icon family unification (real-device round 2, 2026-08-17): the ⓘ text
298
+ glyph is gone (MobileSessionHeader.tsx now renders IconInfoOutline16,
299
+ a local 16px SVG built to match the primitives icon family) and the
300
+ workbench button's IconPanelLeftOutline16 is mirrored into a
301
+ panel-RIGHT glyph — there's no IconPanelRightOutline16 in primitives
302
+ (checked lib/types/icons/index.d.ts), and the plugin's own right-side
303
+ panel semantics are exactly the left icon flipped. Both buttons now
304
+ carry a same-size (16px), same-stroke-weight icon. */
305
+ [data-mobile-nav="header-workbench"] svg {
306
+ transform: scaleX(-1);
307
+ }
308
+
309
+ /* Official Chat/Trajectory tablist: removed from layout entirely (S2.1 —
310
+ visibility:hidden still held a 27px row, so the header carried the
311
+ view-switch row's band TWICE: 72 + 27 + gap = 104px measured at 390px,
312
+ and a real iPhone added ~54px of notch on top of that). The header's own
313
+ padding-bottom above is now the row's seat.
314
+ display:none does NOT break the view switch: HTMLElement.click()
315
+ dispatches synthetically and fires React's handler regardless of
316
+ rendering — only real pointer hit-testing needs a box, and this plugin
317
+ never relies on it (MobileSessionHeader always calls .click()). */
318
+ [data-phase] header [class$="_tabs"][role="tablist"] {
319
+ display: none;
320
+ }
321
+
322
+ /* View-switch row: "current view name + dots", replacing the (still
323
+ present, still clickable) official tablist visually. It renders inside
324
+ headerActions/headerUtilities, but titleRow's own position:relative
325
+ (set above) makes IT the containing block for absolute descendants —
326
+ display:contents on titleCluster does not break that search — so
327
+ top:100% sits directly under the title regardless of the title row's
328
+ actual height, with no hardcoded offset to keep in sync. */
329
+ [data-mobile-nav="header-viewrow"] {
330
+ position: absolute;
331
+ left: 0;
332
+ right: 0;
333
+ top: 100%;
334
+ display: flex;
335
+ align-items: center;
336
+ justify-content: center;
337
+ gap: 6px;
338
+ height: 28px;
339
+ padding: 0;
340
+ border: none;
341
+ background: transparent;
342
+ color: var(--dsw-alias-label-secondary, rgba(0, 0, 0, .5));
343
+ font-family: inherit;
344
+ font-size: 13px;
345
+ line-height: 18px;
346
+ cursor: pointer;
347
+ touch-action: manipulation;
348
+ -webkit-tap-highlight-color: transparent;
349
+ }
350
+ [data-mobile-nav="header-viewrow-dots"] {
351
+ display: inline-flex;
352
+ align-items: center;
353
+ gap: 3px;
354
+ }
355
+ [data-mobile-nav="header-viewrow-dots"] > i {
356
+ display: block;
357
+ width: 4px;
358
+ height: 4px;
359
+ border-radius: 50%;
360
+ background: currentColor;
361
+ opacity: .35;
362
+ font-style: normal;
363
+ }
364
+ [data-mobile-nav="header-viewrow-dots"] > i[data-active] {
365
+ opacity: 1;
366
+ }
367
+ }
368
+ `