rift-ds 1.6.0 → 1.8.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 (133) hide show
  1. package/README.md +34 -13
  2. package/bin/rift-ds.mjs +231 -44
  3. package/bin/templates/chat/.env.chat.example +21 -0
  4. package/bin/templates/chat/app/api/chat/feedback/route.ts +12 -0
  5. package/bin/templates/chat/app/api/chat/followups/route.ts +23 -0
  6. package/bin/templates/chat/app/api/chat/guardrails.ts +18 -0
  7. package/bin/templates/chat/app/api/chat/route.ts +31 -0
  8. package/bin/templates/chat/components/chat/ChatMount.tsx +52 -0
  9. package/bin/templates/chat/components/chat/models.ts +29 -0
  10. package/chat/AssistantTurn.d.ts +5 -0
  11. package/chat/AssistantTurn.js +82 -0
  12. package/chat/ChatAgentPane.css +56 -0
  13. package/chat/ChatAgentPane.d.ts +21 -0
  14. package/chat/ChatAgentPane.js +25 -0
  15. package/chat/ChatLauncher.d.ts +64 -0
  16. package/chat/ChatLauncher.js +347 -0
  17. package/chat/ChatProvider.d.ts +53 -0
  18. package/chat/ChatProvider.js +181 -0
  19. package/chat/ChatTabs.d.ts +13 -0
  20. package/chat/ChatTabs.js +24 -0
  21. package/chat/ChatThreads.d.ts +26 -0
  22. package/chat/ChatThreads.js +55 -0
  23. package/chat/ChatWidget.css +1072 -0
  24. package/chat/ChatWidget.d.ts +113 -0
  25. package/chat/ChatWidget.js +495 -0
  26. package/chat/DemoWelcome.d.ts +13 -0
  27. package/chat/DemoWelcome.js +131 -0
  28. package/chat/FollowupSuggestions.d.ts +23 -0
  29. package/chat/FollowupSuggestions.js +34 -0
  30. package/chat/ResponseActions.d.ts +18 -0
  31. package/chat/ResponseActions.js +79 -0
  32. package/chat/TurnContent.d.ts +9 -0
  33. package/chat/TurnContent.js +12 -0
  34. package/chat/context.d.ts +65 -0
  35. package/chat/context.js +27 -0
  36. package/chat/demo-agent-panes.d.ts +5 -0
  37. package/chat/demo-agent-panes.js +49 -0
  38. package/chat/demo-agent.d.ts +3 -0
  39. package/chat/demo-agent.js +28 -0
  40. package/chat/demo-card-components.d.ts +46 -0
  41. package/chat/demo-card-components.js +59 -0
  42. package/chat/demo-cards.d.ts +32 -0
  43. package/chat/demo-cards.js +59 -0
  44. package/chat/demo-fixtures.d.ts +20 -0
  45. package/chat/demo-fixtures.js +113 -0
  46. package/chat/demo-script.d.ts +6 -0
  47. package/chat/demo-script.js +215 -0
  48. package/chat/demo.css +46 -0
  49. package/chat/demo.d.ts +9 -0
  50. package/chat/demo.js +24 -0
  51. package/chat/followups.d.ts +41 -0
  52. package/chat/followups.js +55 -0
  53. package/chat/greeting.d.ts +11 -0
  54. package/chat/greeting.js +21 -0
  55. package/chat/index.d.ts +34 -0
  56. package/chat/index.js +80 -0
  57. package/chat/placement.d.ts +48 -0
  58. package/chat/placement.js +39 -0
  59. package/chat/queries.d.ts +9 -0
  60. package/chat/queries.js +22 -0
  61. package/chat/registry.d.ts +25 -0
  62. package/chat/registry.js +9 -0
  63. package/chat/registry.json.d.ts +69 -0
  64. package/chat/registry.json.js +11 -0
  65. package/chat/server.d.ts +217 -0
  66. package/chat/server.js +488 -0
  67. package/chat/sim.d.ts +71 -0
  68. package/chat/sim.js +125 -0
  69. package/chat/starters.d.ts +15 -0
  70. package/chat/starters.js +38 -0
  71. package/chat/suggestions.d.ts +22 -0
  72. package/chat/suggestions.js +9 -0
  73. package/chat/threads.d.ts +73 -0
  74. package/chat/threads.js +270 -0
  75. package/chat/transport.d.ts +49 -0
  76. package/chat/transport.js +105 -0
  77. package/chat/types.d.ts +210 -0
  78. package/chat/types.js +16 -0
  79. package/chat/upstash.d.ts +63 -0
  80. package/chat/upstash.js +147 -0
  81. package/chat/useChat.d.ts +64 -0
  82. package/chat/useChat.js +241 -0
  83. package/cli/templates/chat/app/api/chat/feedback/route.d.ts +3 -0
  84. package/cli/templates/chat/app/api/chat/followups/route.d.ts +3 -0
  85. package/cli/templates/chat/app/api/chat/guardrails.d.ts +10 -0
  86. package/cli/templates/chat/app/api/chat/route.d.ts +4 -0
  87. package/cli/templates/chat/components/chat/ChatMount.d.ts +1 -0
  88. package/cli/templates/chat/components/chat/models.d.ts +11 -0
  89. package/components/AgentRail/AgentRail.css +2 -2
  90. package/components/Attachment/Attachment.css +14 -0
  91. package/components/Attachment/AttachmentGroup.d.ts +2 -2
  92. package/components/Attachment/AttachmentGroup.js +7 -2
  93. package/components/Composer/Composer.css +88 -4
  94. package/components/Composer/Composer.d.ts +21 -0
  95. package/components/Composer/Composer.js +199 -14
  96. package/components/Mention/Mention.css +195 -0
  97. package/components/Mention/Mention.d.ts +68 -0
  98. package/components/Mention/Mention.js +145 -0
  99. package/components/Mention/mentions.d.ts +81 -0
  100. package/components/Mention/mentions.js +83 -0
  101. package/components/ShaderField/ShaderField.d.ts +6 -5
  102. package/components/ShaderField/useShaderField.d.ts +2 -1
  103. package/components/ShaderField/useShaderField.js +6 -6
  104. package/components/StatusDot/StatusDot.css +36 -13
  105. package/components/StatusDot/StatusDot.d.ts +16 -4
  106. package/components/StatusDot/StatusDot.js +25 -5
  107. package/components/ThreadPanel/ThreadPanel.css +20 -11
  108. package/components/ThreadPanel/ThreadPanel.d.ts +15 -3
  109. package/components/ThreadPanel/ThreadPanel.js +30 -6
  110. package/components/ThreadTabs/ThreadTabs.css +18 -8
  111. package/components/ThreadTabs/ThreadTabs.d.ts +8 -2
  112. package/components/ThreadTabs/ThreadTabs.js +24 -5
  113. package/components/registry.json +9 -1
  114. package/components/registry.json.d.ts +9 -1
  115. package/components/registry.json.js +1 -1
  116. package/fonts/material-symbols.css +2 -1
  117. package/index.d.ts +1 -0
  118. package/index.js +11 -0
  119. package/package.json +43 -2
  120. package/tokens/presets/coral.css +18 -2
  121. package/tokens/presets/forest.css +18 -2
  122. package/tokens/presets/gold.css +18 -2
  123. package/tokens/presets/mono.css +18 -2
  124. package/tokens/presets/pink.css +18 -2
  125. package/tokens/presets/terminal.css +18 -2
  126. package/tokens/presets/violet.css +18 -2
  127. package/tokens/presets/warm.css +18 -2
  128. package/tokens/presets/zest.css +18 -2
  129. package/tokens/registry.json +6 -0
  130. package/tokens/registry.json.d.ts +6 -0
  131. package/tokens/registry.json.js +1 -1
  132. package/tokens/tokens-dark.css +10 -0
  133. package/tokens/tokens-light.css +10 -0
@@ -0,0 +1,1072 @@
1
+ /* ============================================
2
+ CHAT WIDGET — the composed chat and the launcher's panel chrome.
3
+
4
+ .ds-chat-widget is the widget's internals: header, thread, composer,
5
+ starters, disclaimer, and the optional rails. It fills whichever container
6
+ hosts it; compact hosts (phone-width panels) set data-compact for the
7
+ narrower insets.
8
+
9
+ .ds-chat-launcher__* is ChatLauncher's chrome: the floating button, the
10
+ fixed panel in its docked, overlay, floating and takeover forms, the scrim
11
+ and the grips. The three variables on :root are the panel's geometry and
12
+ the one contract a host stylesheet reads: --ds-chat-width (rewritten on
13
+ <html> by the widen grip), --ds-chat-inset and --ds-chat-float-height.
14
+ ============================================ */
15
+
16
+ :root {
17
+ /* The docked panel's resting width; DOCK_MIN_WIDTH in placement.ts mirrors it. */
18
+ --ds-chat-width: 420px;
19
+ /* The gap the panel and the launcher float off the viewport edges. */
20
+ --ds-chat-inset: 20px;
21
+ /* The floating card's resting height. */
22
+ --ds-chat-float-height: 640px;
23
+ }
24
+
25
+ .ds-chat-widget {
26
+ display: flex;
27
+ flex-direction: column;
28
+ width: 100%;
29
+ height: 100%;
30
+ min-height: 0;
31
+ /* A small breath above the header on every host: it seats the title row on
32
+ the same line as the threads rail's own header, whose panel carries this
33
+ inset built in. */
34
+ padding-top: var(--padding-200);
35
+ /* The one side inset every row shares (header controls, greeting, thread
36
+ bubbles, composer edge, starter pills, disclaimer), so they sit on a
37
+ single rail. A step tighter on compact hosts. */
38
+ --ds-chat-widget-inset: var(--padding-500);
39
+ /* A size container, so the welcome dressing below can step aside when the
40
+ host is too short for it. */
41
+ container-type: size;
42
+ container-name: ds-chat-widget;
43
+ }
44
+
45
+ .ds-chat-widget[data-compact] {
46
+ --ds-chat-widget-inset: var(--padding-400);
47
+ }
48
+
49
+ /* The header rides the same rail as everything under it; the component's
50
+ own default is a step tighter than the widget wants. */
51
+ .ds-chat-widget .ds-chat-header {
52
+ padding-inline: var(--ds-chat-widget-inset);
53
+ }
54
+
55
+ /* With the controls lifted to the card's edge, the header row has nothing
56
+ 40px tall left in it and would collapse to the title's own line. The row
57
+ holds the height its buttons were giving it: the control plus the
58
+ header's own block padding either side. */
59
+ .ds-chat-widget[data-lifted-actions] .ds-chat-header {
60
+ min-height: calc(40px + var(--padding-150) * 2);
61
+ }
62
+
63
+ /* The close button hugs the panel's clipped right edge, so its centred
64
+ bottom tooltip would lose its right cap to overflow: hidden. End-align
65
+ the last header tooltip to the button and walk the arrow back to the
66
+ button's centre (20px in, minus half the 8px arrow). */
67
+ .ds-chat-widget .ds-chat-header__actions .ds-tooltip:last-child .ds-tooltip__panel--bottom,
68
+ .ds-chat-widget__rail-actions .ds-tooltip:last-child .ds-tooltip__panel--bottom {
69
+ left: auto;
70
+ right: 0;
71
+ transform: translateY(-4px);
72
+ }
73
+
74
+ .ds-chat-widget .ds-chat-header__actions .ds-tooltip:last-child .ds-tooltip__panel--bottom.ds-tooltip__panel--visible,
75
+ .ds-chat-widget__rail-actions .ds-tooltip:last-child .ds-tooltip__panel--bottom.ds-tooltip__panel--visible {
76
+ transform: translateY(0);
77
+ }
78
+
79
+ .ds-chat-widget .ds-chat-header__actions .ds-tooltip:last-child .ds-tooltip__panel--bottom .ds-tooltip__arrow,
80
+ .ds-chat-widget__rail-actions .ds-tooltip:last-child .ds-tooltip__panel--bottom .ds-tooltip__arrow {
81
+ left: auto;
82
+ right: 16px;
83
+ margin-left: 0;
84
+ }
85
+
86
+ /* The placement menu's wrapper is only there to catch Escape; it must not
87
+ become a box of its own in the header's action row. */
88
+ .ds-chat-widget__placement-menu {
89
+ display: contents;
90
+ }
91
+
92
+ /* Open, the menu says what the button is, so the tooltip stands down. And
93
+ the trigger holds the passive wash while its menu is up: a control that
94
+ is open says so. */
95
+ .ds-chat-widget__placement-menu .ds-tooltip:has([aria-expanded="true"]) .ds-tooltip__panel {
96
+ display: none;
97
+ }
98
+
99
+ .ds-chat-widget__placement-menu .ds-circular-button[aria-expanded="true"],
100
+ .ds-chat-widget__placement-menu .ds-circular-button[aria-expanded="true"]:hover {
101
+ background-color: var(--color-action-passive-bg-hover);
102
+ }
103
+
104
+ /* The open-sessions tab strip: its own row under the header, on the shared
105
+ side inset. */
106
+ .ds-chat-widget__tabs-row {
107
+ min-width: 0;
108
+ padding: var(--padding-150) var(--ds-chat-widget-inset) 0;
109
+ }
110
+
111
+ /* The header brand: the mark beside the chat name. */
112
+ .ds-chat-widget__brand {
113
+ display: inline-flex;
114
+ align-items: center;
115
+ gap: var(--gap-200);
116
+ min-width: 0;
117
+ }
118
+
119
+ .ds-chat-widget__brand img {
120
+ flex-shrink: 0;
121
+ }
122
+
123
+ .ds-chat-widget__brand-name {
124
+ font-family: var(--font-title-body-family);
125
+ font-size: var(--font-title-body-size);
126
+ font-weight: var(--font-title-body-weight);
127
+ line-height: var(--font-title-body-line-height);
128
+ color: var(--color-text-primary);
129
+ overflow: hidden;
130
+ text-overflow: ellipsis;
131
+ white-space: nowrap;
132
+ }
133
+
134
+ /* The top region: header + thread. Zero basis so welcome mode splits the
135
+ height exactly with the bottom region, centring the composer. */
136
+ .ds-chat-widget__top {
137
+ flex: 1 1 0;
138
+ min-height: 0;
139
+ display: flex;
140
+ flex-direction: column;
141
+ }
142
+
143
+ /* The body hosts the thread, with the welcome greeting overlaid while the
144
+ conversation is empty. */
145
+ .ds-chat-widget__body {
146
+ flex: 1;
147
+ min-height: 0;
148
+ position: relative;
149
+ display: flex;
150
+ flex-direction: column;
151
+ }
152
+
153
+ /* The thread owns scrolling, gutters, fades, and send anchoring. However
154
+ wide the container gets, the conversation column caps at 768px and
155
+ centres; the header above is the one row that spans the full width. */
156
+ .ds-chat-widget__thread {
157
+ flex: 1;
158
+ --ds-chat-thread-content-max-width: 768px;
159
+ /* Match the composer and header inset, so bubbles, composer edge, and
160
+ the header controls all share one right rail. */
161
+ --ds-chat-thread-padding-inline: var(--ds-chat-widget-inset);
162
+ }
163
+
164
+ /* ============================================
165
+ WELCOME — the new-chat empty state
166
+ ============================================ */
167
+
168
+ .ds-chat-widget__welcome {
169
+ position: absolute;
170
+ inset: 0;
171
+ display: flex;
172
+ flex-direction: column;
173
+ justify-content: flex-end;
174
+ width: 100%;
175
+ max-width: 768px;
176
+ margin-inline: auto;
177
+ /* The block-end padding is the gap between the greeting and the composer;
178
+ block-start is inert, since the overlay bottom-aligns its content. */
179
+ padding: var(--padding-500) var(--ds-chat-widget-inset) var(--gap-1000);
180
+ pointer-events: none;
181
+ }
182
+
183
+ .ds-chat-widget__greeting {
184
+ display: flex;
185
+ flex-direction: column;
186
+ gap: var(--gap-200);
187
+ }
188
+
189
+ /* Small and semibold over the ask line's large light face: the pair
190
+ separates by weight contrast. Tertiary text, a step quieter than the ask
191
+ line, so the time-of-day pleasantry supports the invitation rather than
192
+ competing with it. */
193
+ .ds-chat-widget__hello {
194
+ margin: 0;
195
+ /* The greeting is read from the visitor's clock after mount, so this line
196
+ is empty for the first paint. An empty block has no line box at all:
197
+ reserve the height, or the ask line below jumps up a row and back. */
198
+ min-height: var(--font-heading-3-line-height);
199
+ font-family: var(--font-heading-3-family);
200
+ font-size: var(--font-heading-3-size);
201
+ font-weight: var(--font-heading-3-weight);
202
+ line-height: var(--font-heading-3-line-height);
203
+ letter-spacing: var(--font-heading-3-letter-spacing);
204
+ color: var(--color-text-tertiary);
205
+ }
206
+
207
+ .ds-chat-widget__ask {
208
+ margin: 0;
209
+ font-family: var(--font-sub-display-family);
210
+ font-size: var(--font-sub-display-size);
211
+ font-weight: var(--font-sub-display-weight);
212
+ line-height: var(--font-sub-display-line-height);
213
+ letter-spacing: var(--font-sub-display-letter-spacing);
214
+ color: var(--color-text-primary);
215
+ }
216
+
217
+ /* The panel is a viewport of its own. tokens-typography.css collapses
218
+ sub-display to 24px/36px under a 768px *viewport*, but a docked panel is
219
+ phone-width on a desktop window, where that media query still serves the
220
+ 30px desktop size into a 420px card. So the ask line takes the same step
221
+ on the *container's* width, at the same 768px line. The values restate
222
+ the token layer's own phone step rather than referencing it, because a
223
+ value inside an unmatched media query is unreachable; tokens-typography.css
224
+ owns the pair, and the two must move together. */
225
+ /* ds-allow(typography): the token layer's 768px sub-display step, restated for the panel's container width — tokens-typography.css owns the values */
226
+ @container ds-chat-widget (max-width: 768px) {
227
+ .ds-chat-widget__ask {
228
+ --font-sub-display-size: 24px;
229
+ --font-sub-display-line-height: 36px;
230
+ }
231
+ }
232
+
233
+ /* The bottom region: starters at its top, disclaimer pinned to its bottom.
234
+ Grown while the chat is empty (centring the composer against the
235
+ zero-basis top region), collapsing to the disclaimer's height on the
236
+ first utterance. Basis stays 0 in both states so only flex-grow ever
237
+ animates: the flow-down is one continuous glide. Reduced motion zeroes it
238
+ via the token. */
239
+ .ds-chat-widget__bottom {
240
+ display: flex;
241
+ flex-direction: column;
242
+ flex: 0 1 0;
243
+ transition: flex-grow var(--motion-duration-slower) var(--motion-ease-entrance);
244
+ }
245
+
246
+ .ds-chat-widget__bottom--welcome {
247
+ flex-grow: 1;
248
+ }
249
+
250
+ .ds-chat-widget__starters {
251
+ width: 100%;
252
+ max-width: 768px;
253
+ margin-inline: auto;
254
+ padding: var(--gap-1000) var(--ds-chat-widget-inset) 0;
255
+ }
256
+
257
+ /* The widget slot: any content, under the composer (and under the starters
258
+ when both show), on the composer's own bounding box. The cap is the
259
+ conversation column plus the side insets, so the slot's content edges
260
+ land exactly on the composer's. */
261
+ .ds-chat-widget__widgets {
262
+ width: 100%;
263
+ max-width: calc(768px + 2 * var(--ds-chat-widget-inset));
264
+ margin-inline: auto;
265
+ padding: var(--gap-1000) var(--ds-chat-widget-inset) var(--padding-400);
266
+ }
267
+
268
+ /* With widgets the region takes its content's height before the slack is
269
+ split, so the composer sits above centre by half the widgets' height
270
+ rather than being squeezed against them. */
271
+ .ds-chat-widget__bottom--widgets {
272
+ flex-basis: auto;
273
+ flex-shrink: 0;
274
+ }
275
+
276
+ /* The widgets claim their height first, so on a short window an even split
277
+ of what is left would crowd the greeting into the header. The top region
278
+ keeps room for the header and the greeting. A phone never centres, so it
279
+ has nothing to protect. */
280
+ .ds-chat-widget[data-widgets="fit"]:not([data-phone]) .ds-chat-widget__top {
281
+ min-height: 236px;
282
+ }
283
+
284
+ /* The welcome dressing needs roughly 580px of widget for the greeting and
285
+ 460px for the starters. Shorter than that, each is decoration the layout
286
+ cannot afford, so it steps aside and the empty state falls back to header,
287
+ centred composer, disclaimer. */
288
+ @container ds-chat-widget (max-height: 579px) {
289
+ .ds-chat-widget__welcome {
290
+ display: none;
291
+ }
292
+ }
293
+
294
+ @container ds-chat-widget (max-height: 459px) {
295
+ .ds-chat-widget__starters {
296
+ display: none;
297
+ }
298
+ }
299
+
300
+ /* ============================================
301
+ WIDGETS AS A PAGE — data-widgets="scroll"
302
+ The welcome screen stops fitting itself to one viewport: the top region
303
+ takes a fixed share of the height, the widgets take whatever they need on
304
+ a wider column, and the widget scrolls as one page with the composer
305
+ travelling with it. Nothing here outlives the welcome.
306
+ ============================================ */
307
+
308
+ .ds-chat-widget[data-widgets="scroll"] {
309
+ overflow-y: auto;
310
+ overscroll-behavior: contain;
311
+
312
+ /* The thread's scroll treatment, on the welcome page too. The scrollbar
313
+ is invisible until the page is actually scrolling (data-scrolling, set
314
+ by the widget and settled by MOTION_SCROLL_SETTLE_MS), with its gutter
315
+ reserved so it never reflows the content sideways. */
316
+ scrollbar-width: thin;
317
+ scrollbar-gutter: stable;
318
+ scrollbar-color: transparent transparent;
319
+ transition: scrollbar-color var(--motion-duration-base) var(--motion-ease-standard);
320
+
321
+ /* The bottom fade the thread has under the conversation: a CSS mask, so
322
+ the widgets scrolling out through the floor pass through it. The root
323
+ carries no background of its own, so only the content fades.
324
+ ds-allow(color): mask alpha keywords (black/transparent), not palette colours */
325
+ -webkit-mask-image: linear-gradient(to bottom, black calc(100% - var(--gap-1000)), transparent);
326
+ mask-image: linear-gradient(to bottom, black calc(100% - var(--gap-1000)), transparent);
327
+ }
328
+
329
+ .ds-chat-widget[data-widgets="scroll"][data-scrolling] {
330
+ scrollbar-color: var(--color-divider) transparent;
331
+ }
332
+
333
+ .ds-chat-widget[data-widgets="scroll"] .ds-chat-widget__top {
334
+ flex: 0 0 auto;
335
+ height: max(236px, 36cqh);
336
+ }
337
+
338
+ .ds-chat-widget[data-widgets="scroll"] .ds-chat-widget__footer {
339
+ flex: 0 0 auto;
340
+ }
341
+
342
+ /* Never smaller than its content, and it takes any height left over, so
343
+ when the widgets are short the disclaimer still rides the floor. */
344
+ .ds-chat-widget[data-widgets="scroll"] .ds-chat-widget__bottom {
345
+ flex: 1 0 auto;
346
+ transition: none;
347
+ }
348
+
349
+ /* A scrolling slot is never dropped for want of height, and ends with room
350
+ under its last row. */
351
+ .ds-chat-widget[data-widgets="scroll"] .ds-chat-widget__widgets {
352
+ display: block;
353
+ padding-block: var(--gap-1000) var(--padding-1000);
354
+ }
355
+
356
+ /* A wide slot breaks out of the composer's box: a dashboard wants more
357
+ width than a line of prose does. */
358
+ .ds-chat-widget[data-widgets-wide] .ds-chat-widget__widgets {
359
+ max-width: 1080px;
360
+ }
361
+
362
+ /* The phone's bottom-pinned composer has nowhere to pin on a scrolling
363
+ page, so the greeting goes back to sitting on the composer. */
364
+ .ds-chat-widget[data-widgets="scroll"][data-phone] .ds-chat-widget__welcome {
365
+ justify-content: flex-end;
366
+ }
367
+
368
+ /* Widgets are the first dressing to go: they are the tallest, and a window
369
+ too short for them still has room for the greeting and the composer. */
370
+ @container ds-chat-widget (max-height: 719px) {
371
+ .ds-chat-widget__widgets {
372
+ display: none;
373
+ }
374
+ }
375
+
376
+ /* ============================================
377
+ WELCOME ON A PHONE — data-phone
378
+ The soft keyboard claims the lower half of a phone screen the moment the
379
+ composer is focused, so a phone never centres: the top region takes all
380
+ the slack, which pins the composer to the bottom from the first paint,
381
+ the greeting floats in the middle of the empty thread, and the starters
382
+ render directly above the composer.
383
+ ============================================ */
384
+
385
+ .ds-chat-widget[data-phone] .ds-chat-widget__welcome {
386
+ /* Restated, not inherited: the desktop height guard above hides the
387
+ overlay under 580px, which is most phones once the keyboard is up. */
388
+ display: flex;
389
+ justify-content: center;
390
+ padding-block: var(--padding-400);
391
+ }
392
+
393
+ .ds-chat-widget[data-phone] .ds-chat-widget__bottom--welcome {
394
+ flex-grow: 0;
395
+ }
396
+
397
+ .ds-chat-widget[data-phone] .ds-chat-widget__bottom {
398
+ transition: none;
399
+ }
400
+
401
+ /* Above the composer: its gap is the block-end padding now. */
402
+ .ds-chat-widget[data-phone] .ds-chat-widget__starters {
403
+ display: block;
404
+ padding-block: 0 var(--gap-400);
405
+ }
406
+
407
+ .ds-chat-widget[data-phone][data-widgets="fit"] .ds-chat-widget__widgets {
408
+ padding-block: 0 var(--gap-400);
409
+ }
410
+
411
+ .ds-chat-widget[data-phone][data-widgets="scroll"] .ds-chat-widget__widgets {
412
+ padding-block: var(--gap-500) var(--padding-500);
413
+ }
414
+
415
+ .ds-chat-widget[data-phone][data-widgets="scroll"] .ds-chat-widget__bottom--welcome {
416
+ flex-grow: 1;
417
+ }
418
+
419
+ /* The phone stack needs less height than the centred layout, and it is the
420
+ keyboard, not a short window, that usually takes it away. The greeting
421
+ steps aside first, the starters only when even they would crowd the
422
+ composer. */
423
+ @container ds-chat-widget (max-height: 499px) {
424
+ .ds-chat-widget[data-phone] .ds-chat-widget__welcome {
425
+ display: none;
426
+ }
427
+ }
428
+
429
+ @container ds-chat-widget (max-height: 379px) {
430
+ .ds-chat-widget[data-phone] .ds-chat-widget__starters {
431
+ display: none;
432
+ }
433
+ }
434
+
435
+ /* Locked to the bottom of the widget, in every state. */
436
+ .ds-chat-widget__disclaimer-row {
437
+ margin-top: auto;
438
+ padding: var(--gap-100) var(--ds-chat-widget-inset) var(--padding-200);
439
+ }
440
+
441
+ .ds-chat-widget__response {
442
+ display: flex;
443
+ flex-direction: column;
444
+ gap: var(--gap-500);
445
+ }
446
+
447
+ /* No divider: the composer floats free of the transcript. */
448
+ .ds-chat-widget__footer {
449
+ display: flex;
450
+ flex-direction: column;
451
+ /* The composer never gives up height in a squeeze. */
452
+ flex-shrink: 0;
453
+ padding: 0 var(--ds-chat-widget-inset);
454
+ }
455
+
456
+ /* The composer shares the conversation column's cap and centre. */
457
+ .ds-chat-widget__composer {
458
+ width: 100%;
459
+ max-width: 768px;
460
+ margin-inline: auto;
461
+ /* The panel is narrow and its welcome screen centres the composer, so
462
+ queued files sit in smaller tiles here than a full-width composer's. */
463
+ --ds-attachment-tile-size: 104px;
464
+ }
465
+
466
+ /* One quiet line, centred with the composer column, at the floor of the
467
+ type scale: a visitor can find it, and never has to read it to chat. */
468
+ .ds-chat-widget__disclaimer {
469
+ margin: 0;
470
+ text-align: center;
471
+ font-family: var(--font-caption-family);
472
+ font-size: var(--font-caption-size);
473
+ font-weight: var(--font-caption-weight);
474
+ line-height: var(--font-caption-line-height);
475
+ letter-spacing: var(--font-caption-letter-spacing);
476
+ color: var(--color-text-tertiary);
477
+ }
478
+
479
+ .ds-chat-widget__disclaimer a {
480
+ color: inherit;
481
+ text-decoration: underline;
482
+ text-underline-offset: 2px;
483
+ }
484
+
485
+ /* ============================================
486
+ MESSAGE ACTIONS — the copy and thumbs row under a committed assistant turn.
487
+ ============================================ */
488
+
489
+ .ds-chat-widget__action {
490
+ display: grid;
491
+ place-items: center;
492
+ padding: var(--padding-100);
493
+ border: none;
494
+ border-radius: var(--radius-pill);
495
+ background: transparent;
496
+ cursor: pointer;
497
+ transition: background-color var(--motion-duration-fast) var(--motion-ease-standard);
498
+ }
499
+
500
+ .ds-chat-widget__action:hover {
501
+ background-color: var(--color-action-passive-bg-hover);
502
+ }
503
+
504
+ .ds-chat-widget__action:active {
505
+ background-color: var(--color-action-passive-bg-active);
506
+ }
507
+
508
+ .ds-chat-widget__action-icon {
509
+ /* Both of the copy button's icons share this single grid cell, so the
510
+ swap happens in place rather than reflowing the row. */
511
+ grid-area: 1 / 1;
512
+ --icon-size: var(--icon-size-500);
513
+ color: var(--color-icon-secondary);
514
+ transition:
515
+ opacity var(--motion-duration-fast) var(--motion-ease-standard),
516
+ transform var(--motion-duration-fast) var(--motion-ease-standard),
517
+ color var(--motion-duration-fast) var(--motion-ease-standard);
518
+ }
519
+
520
+ .ds-chat-widget__action:hover .ds-chat-widget__action-icon {
521
+ color: var(--color-icon-primary);
522
+ }
523
+
524
+ /* The hidden half of the copy swap: faded and slightly shrunk, so the
525
+ arriving icon reads as popping in rather than teleporting. */
526
+ .ds-chat-widget__action-icon--hidden {
527
+ opacity: 0;
528
+ transform: scale(0.6);
529
+ }
530
+
531
+ /* The tick wears the positive status role while it shows. */
532
+ .ds-chat-widget__action-icon--tick,
533
+ .ds-chat-widget__action:hover .ds-chat-widget__action-icon--tick {
534
+ color: var(--color-status-positive-border);
535
+ }
536
+
537
+ /* A chosen thumb: filled glyph, full-strength colour, no hover needed. */
538
+ .ds-chat-widget__action[aria-pressed="true"] .ds-chat-widget__action-icon {
539
+ --material-symbols-fill: 1;
540
+ color: var(--color-icon-primary);
541
+ }
542
+
543
+ /* ============================================
544
+ FOLLOW-UP SUGGESTIONS — the chips under an answer.
545
+ ============================================ */
546
+
547
+ .ds-chat-widget__followups {
548
+ width: 100%;
549
+ margin-top: var(--gap-200);
550
+ /* They land a beat after the answer commits, so they arrive rather than
551
+ appear. Reduced motion zeroes the duration through the token. */
552
+ animation: ds-chat-widget-followups-in var(--motion-duration-slow) var(--motion-ease-entrance) both;
553
+ }
554
+
555
+ @keyframes ds-chat-widget-followups-in {
556
+ from {
557
+ opacity: 0;
558
+ transform: translateY(var(--gap-100));
559
+ }
560
+ }
561
+
562
+ /* Visually hidden, still announced. */
563
+ .ds-chat-widget__sr-only {
564
+ position: absolute;
565
+ width: 1px;
566
+ height: 1px;
567
+ margin: -1px;
568
+ padding: 0;
569
+ overflow: hidden;
570
+ clip: rect(0 0 0 0);
571
+ white-space: nowrap;
572
+ border: 0;
573
+ }
574
+
575
+ /* ============================================
576
+ THE RAILS — the optional session-history rail (leading) and agent pane
577
+ (trailing). Wide widgets seat them inline; narrow ones summon the history
578
+ from the header hamburger as a slide-over sheet and the agent pane from a
579
+ header button as a bottom sheet, each behind a scrim.
580
+ ============================================ */
581
+
582
+ .ds-chat-widget__rails {
583
+ position: relative;
584
+ display: flex;
585
+ width: 100%;
586
+ height: 100%;
587
+ min-height: 0;
588
+ overflow: hidden;
589
+ }
590
+
591
+ .ds-chat-widget--with-threads {
592
+ flex: 1;
593
+ min-width: 0;
594
+ }
595
+
596
+ .ds-chat-widget__threads-rail {
597
+ flex: none;
598
+ display: flex;
599
+ min-height: 0;
600
+ border-right: var(--border-025) solid var(--color-bg-container-border);
601
+ }
602
+
603
+ /* The sheet layer: hidden entirely (visibility) while closed, so nothing in
604
+ it can take focus or reach assistive technology. */
605
+ .ds-chat-widget__threads-overlay,
606
+ .ds-chat-widget__aside-overlay {
607
+ position: absolute;
608
+ inset: 0;
609
+ z-index: 5;
610
+ visibility: hidden;
611
+ transition: visibility 0s var(--motion-duration-slow);
612
+ }
613
+
614
+ .ds-chat-widget__threads-overlay--open,
615
+ .ds-chat-widget__aside-overlay--open {
616
+ visibility: visible;
617
+ transition: visibility 0s;
618
+ }
619
+
620
+ .ds-chat-widget__rail-scrim {
621
+ position: absolute;
622
+ inset: 0;
623
+ border: none;
624
+ padding: 0;
625
+ background-color: var(--color-scrim);
626
+ cursor: pointer;
627
+ opacity: 0;
628
+ transition: opacity var(--motion-duration-slow) var(--motion-ease-standard);
629
+ }
630
+
631
+ .ds-chat-widget__threads-overlay--open .ds-chat-widget__rail-scrim,
632
+ .ds-chat-widget__aside-overlay--open .ds-chat-widget__rail-scrim {
633
+ opacity: 1;
634
+ }
635
+
636
+ /* The glass recipe: the translucent fill over a backdrop blur, so the
637
+ dimmed chat shows through the sheet. */
638
+ .ds-chat-widget__threads-sheet {
639
+ position: absolute;
640
+ top: 0;
641
+ bottom: 0;
642
+ left: 0;
643
+ display: flex;
644
+ background-color: var(--color-bg-glass);
645
+ backdrop-filter: blur(24px);
646
+ border-right: var(--border-025) solid var(--color-bg-container-border);
647
+ box-shadow: var(--shadow-floating);
648
+ transform: translateX(-100%);
649
+ transition: transform var(--motion-duration-slow) var(--motion-ease-emphasized);
650
+ }
651
+
652
+ .ds-chat-widget__threads-overlay--open .ds-chat-widget__threads-sheet {
653
+ transform: translateX(0);
654
+ }
655
+
656
+ /* The agent rail floats rather than butting against the widget's edges: its
657
+ own rounded card, inset on three sides. The top margin is the header
658
+ row's exact height (the chat column's top padding, plus the header's own
659
+ block padding either side of a 40px control), so the panel begins where
660
+ that row ends and the lifted controls sit in the clear above it. */
661
+ .ds-chat-widget__aside-rail {
662
+ flex: none;
663
+ min-height: 0;
664
+ margin: calc(var(--padding-200) + var(--padding-150) * 2 + 40px) 0 var(--padding-500) 0;
665
+ /* The open and close glide, the threads rail's own: a one-column grid
666
+ whose track runs 0fr to 1fr, so the pane's width animates without the
667
+ widget knowing it, while the pane's own card fades with it. Closed, the
668
+ rail keeps no width, no inset and no border, and goes invisible once
669
+ the collapse has finished (the visibility delay), so nothing of it can
670
+ be seen, hovered or reached. The reduced-motion guard in
671
+ tokens-motion.css zeroes every duration here. */
672
+ display: grid;
673
+ grid-template-columns: 0fr;
674
+ opacity: 0;
675
+ visibility: hidden;
676
+ transition:
677
+ grid-template-columns var(--motion-duration-slow) var(--motion-ease-emphasized),
678
+ margin var(--motion-duration-slow) var(--motion-ease-emphasized),
679
+ opacity var(--motion-duration-base) var(--motion-ease-standard),
680
+ visibility 0s linear var(--motion-duration-slow);
681
+ }
682
+
683
+ .ds-chat-widget__aside-rail--open {
684
+ grid-template-columns: 1fr;
685
+ margin-right: var(--padding-500);
686
+ opacity: 1;
687
+ visibility: visible;
688
+ transition:
689
+ grid-template-columns var(--motion-duration-slow) var(--motion-ease-emphasized),
690
+ margin var(--motion-duration-slow) var(--motion-ease-emphasized),
691
+ opacity var(--motion-duration-base) var(--motion-ease-standard) calc(var(--motion-duration-slow) - var(--motion-duration-base)),
692
+ visibility 0s linear 0s;
693
+ }
694
+
695
+ /* The pane's card: the border and radius live here, inside the animated
696
+ track, so the collapsing rail never shows a sliver of frame. */
697
+ .ds-chat-widget__aside-rail-inner {
698
+ display: flex;
699
+ min-width: 0;
700
+ min-height: 0;
701
+ border: var(--border-025) solid var(--color-bg-container-border);
702
+ border-radius: var(--radius-600);
703
+ overflow: hidden;
704
+ }
705
+
706
+ /* The widget's controls, lifted out of the chat header to the card's own
707
+ right edge while the rail is seated inline. */
708
+ .ds-chat-widget__rail-actions {
709
+ position: absolute;
710
+ top: calc(var(--padding-200) + var(--padding-150));
711
+ right: var(--padding-500);
712
+ z-index: 6;
713
+ display: flex;
714
+ align-items: center;
715
+ gap: var(--gap-100);
716
+ }
717
+
718
+ /* Narrow hosts raise the agent pane from the bottom: a phone's secondary
719
+ surface comes up from the edge the thumb is nearest. Deliberately short
720
+ of the full height, so the last turns stay visible above it. */
721
+ .ds-chat-widget__aside-sheet {
722
+ position: absolute;
723
+ right: 0;
724
+ bottom: 0;
725
+ left: 0;
726
+ display: flex;
727
+ flex-direction: column;
728
+ max-height: 82%;
729
+ background-color: var(--color-bg-glass);
730
+ backdrop-filter: blur(24px);
731
+ border-top: var(--border-025) solid var(--color-bg-container-border);
732
+ border-radius: var(--radius-600) var(--radius-600) 0 0;
733
+ box-shadow: var(--shadow-floating);
734
+ transform: translateY(100%);
735
+ transition: transform var(--motion-duration-slow) var(--motion-ease-emphasized);
736
+ }
737
+
738
+ .ds-chat-widget__aside-overlay--open .ds-chat-widget__aside-sheet {
739
+ transform: translateY(0);
740
+ }
741
+
742
+ .ds-chat-widget__aside-grabber {
743
+ flex: none;
744
+ width: 36px;
745
+ height: 4px;
746
+ margin: var(--gap-200) auto 0;
747
+ border-radius: var(--radius-pill);
748
+ background-color: var(--color-divider);
749
+ }
750
+
751
+ .ds-chat-widget__aside-sheet-body {
752
+ display: flex;
753
+ flex: 1;
754
+ min-height: 0;
755
+ }
756
+
757
+ /* The agent toggle's on-state: the passive wash, held down while the panel
758
+ is up. Paired with the library class so it beats the variant's resting
759
+ background whatever order the stylesheets load in. */
760
+ .ds-circular-button.ds-chat-widget__agent-toggle--on,
761
+ .ds-circular-button.ds-chat-widget__agent-toggle--on:hover {
762
+ background-color: var(--color-action-passive-bg-hover);
763
+ }
764
+
765
+ /* ============================================
766
+ THE LAUNCHER'S PANEL — a card inset from the viewport edges, docked above
767
+ the threshold to the edge the visitor chose; an overlay with a scrim below
768
+ it; a free, non-modal card when floated; a blurred full takeover in
769
+ fullscreen view. On phones it fills the viewport.
770
+ ============================================ */
771
+
772
+ .ds-chat-launcher__panel {
773
+ position: fixed;
774
+ top: var(--ds-chat-inset);
775
+ /* Placed from the left on both docks, so one property carries every move
776
+ the card makes and they all glide. */
777
+ left: calc(100% - var(--ds-chat-width) - var(--ds-chat-inset));
778
+ height: calc(100dvh - var(--ds-chat-inset) * 2);
779
+ width: var(--ds-chat-width);
780
+ max-width: calc(100vw - var(--ds-chat-inset) * 2);
781
+ display: flex;
782
+ flex-direction: column;
783
+ /* The glass token: deliberately translucent in both themes, so the page's
784
+ colours reach the surface; the blur is what keeps the transcript
785
+ readable over them. */
786
+ background: var(--color-bg-glass);
787
+ border: var(--border-025) solid var(--color-bg-container-border);
788
+ border-radius: var(--radius-600);
789
+ box-shadow: var(--shadow-floating);
790
+ overflow: hidden;
791
+ backdrop-filter: blur(24px);
792
+ z-index: 1100;
793
+ /* The panel-to-takeover glide: the same card grows to fill the viewport,
794
+ so every property that differs between the two views animates together. */
795
+ transition:
796
+ top var(--motion-duration-slow) var(--motion-ease-emphasized),
797
+ left var(--motion-duration-slow) var(--motion-ease-emphasized),
798
+ width var(--motion-duration-slow) var(--motion-ease-emphasized),
799
+ height var(--motion-duration-slow) var(--motion-ease-emphasized),
800
+ max-width var(--motion-duration-slow) var(--motion-ease-emphasized),
801
+ border-radius var(--motion-duration-slow) var(--motion-ease-emphasized),
802
+ border-color var(--motion-duration-slow) var(--motion-ease-emphasized),
803
+ backdrop-filter var(--motion-duration-slow) var(--motion-ease-emphasized);
804
+ /* Open: the card fades in with a small rise from its resting spot. Plays
805
+ on mount, so the view toggle, which keeps the node, never replays it. */
806
+ animation: ds-chat-launcher-panel-in var(--motion-duration-slow) var(--motion-ease-entrance);
807
+ }
808
+
809
+ /* The widen grip drives --ds-chat-width frame by frame; the width glide
810
+ above would rubber-band behind the pointer, so resizing suspends it. */
811
+ html[data-chat-resizing] .ds-chat-launcher__panel {
812
+ transition: none;
813
+ }
814
+
815
+ /* Close: the mirror image. The provider keeps the panel mounted for
816
+ MOTION_EXIT_SYNC_MS while this plays, and pointer-events off so a dying
817
+ panel swallows no clicks. */
818
+ .ds-chat-launcher__panel--closing {
819
+ animation: ds-chat-launcher-panel-out var(--motion-duration-base) var(--motion-ease-standard) forwards;
820
+ pointer-events: none;
821
+ }
822
+
823
+ @keyframes ds-chat-launcher-panel-in {
824
+ from {
825
+ opacity: 0;
826
+ transform: translateY(10px);
827
+ }
828
+
829
+ to {
830
+ opacity: 1;
831
+ transform: none;
832
+ }
833
+ }
834
+
835
+ @keyframes ds-chat-launcher-panel-out {
836
+ to {
837
+ opacity: 0;
838
+ transform: translateY(10px);
839
+ }
840
+ }
841
+
842
+ /* The left dock: the same card on the other edge. */
843
+ .ds-chat-launcher__panel--left {
844
+ left: var(--ds-chat-inset);
845
+ }
846
+
847
+ /* Fullscreen view: a true viewport takeover. The stronger blur keeps the
848
+ transcript readable while the page's colours still shine through. The
849
+ border fades rather than vanishing, so the geometry never snaps mid-glide. */
850
+ .ds-chat-launcher__panel--full {
851
+ top: 0;
852
+ left: 0;
853
+ height: 100dvh;
854
+ width: 100vw;
855
+ max-width: 100vw;
856
+ border-color: transparent;
857
+ border-radius: 0;
858
+ backdrop-filter: blur(40px);
859
+ }
860
+
861
+ /* Below the dock threshold the panel overlays the page behind a scrim. */
862
+ .ds-chat-launcher__scrim {
863
+ position: fixed;
864
+ inset: 0;
865
+ background: var(--color-scrim);
866
+ z-index: 1090;
867
+ animation: ds-chat-launcher-scrim-in var(--motion-duration-slow) var(--motion-ease-standard);
868
+ }
869
+
870
+ @keyframes ds-chat-launcher-scrim-in {
871
+ from {
872
+ opacity: 0;
873
+ }
874
+ }
875
+
876
+ /* The widen grip: a 20px grab zone straddling the panel's inner edge, with
877
+ a hover-revealed 4×48px pill. A fixed sibling of the panel, positioned
878
+ from the same variables, so it tracks the edge as the drag rewrites
879
+ --ds-chat-width. */
880
+ .ds-chat-launcher__dock-handle {
881
+ position: fixed;
882
+ top: var(--ds-chat-inset);
883
+ height: calc(100dvh - var(--ds-chat-inset) * 2);
884
+ right: calc(var(--ds-chat-inset) + var(--ds-chat-width) - 10px);
885
+ width: 20px;
886
+ display: flex;
887
+ align-items: center;
888
+ justify-content: center;
889
+ cursor: ew-resize;
890
+ touch-action: none;
891
+ z-index: 1101;
892
+ }
893
+
894
+ .ds-chat-launcher__dock-handle--left {
895
+ right: auto;
896
+ left: calc(var(--ds-chat-inset) + var(--ds-chat-width) - 10px);
897
+ }
898
+
899
+ .ds-chat-launcher__dock-handle::after {
900
+ content: "";
901
+ width: 4px;
902
+ height: 48px;
903
+ border-radius: var(--radius-pill);
904
+ background: transparent;
905
+ transition: background var(--motion-duration-fast) var(--motion-ease-standard);
906
+ }
907
+
908
+ .ds-chat-launcher__dock-handle:hover::after,
909
+ .ds-chat-launcher__dock-handle--resizing::after {
910
+ background: var(--color-text-tertiary);
911
+ }
912
+
913
+ .ds-chat-launcher__dock-handle:active::after {
914
+ background: var(--color-text-secondary);
915
+ }
916
+
917
+ /* On short viewports the vertical insets are height the widget needs more
918
+ than the chrome does. */
919
+ @media (max-height: 619px) {
920
+ .ds-chat-launcher__panel {
921
+ top: 0;
922
+ height: 100dvh;
923
+ }
924
+
925
+ .ds-chat-launcher__dock-handle {
926
+ top: 0;
927
+ height: 100dvh;
928
+ }
929
+ }
930
+
931
+ /* Floating: popped out of the dock into a shorter card the visitor drags by
932
+ its header. The launcher writes the position variables once it has been
933
+ moved; until then it rests in the launcher's corner. After the
934
+ short-viewport rule above on purpose, so the card keeps its own height. */
935
+ .ds-chat-launcher__panel--floating {
936
+ --ds-chat-float-h: min(
937
+ var(--ds-chat-float-height),
938
+ calc(100dvh - var(--ds-chat-inset) * 2)
939
+ );
940
+ top: var(--ds-chat-float-y, calc(100dvh - var(--ds-chat-float-h) - var(--ds-chat-inset)));
941
+ left: var(--ds-chat-float-x, calc(100% - var(--ds-chat-width) - var(--ds-chat-inset)));
942
+ width: var(--ds-chat-float-w, var(--ds-chat-width));
943
+ height: var(--ds-chat-float-h);
944
+ max-height: 100dvh;
945
+ }
946
+
947
+ /* The size grips: an 8px zone along each edge and a 16px square in each
948
+ corner, all inside the card. */
949
+ .ds-chat-launcher__float-grip {
950
+ position: absolute;
951
+ z-index: 10;
952
+ display: flex;
953
+ align-items: center;
954
+ justify-content: center;
955
+ touch-action: none;
956
+ }
957
+
958
+ .ds-chat-launcher__float-grip[data-dir="n"],
959
+ .ds-chat-launcher__float-grip[data-dir="s"] {
960
+ left: 16px;
961
+ right: 16px;
962
+ height: 8px;
963
+ cursor: ns-resize;
964
+ }
965
+
966
+ .ds-chat-launcher__float-grip[data-dir="e"],
967
+ .ds-chat-launcher__float-grip[data-dir="w"] {
968
+ top: 16px;
969
+ bottom: 16px;
970
+ width: 8px;
971
+ cursor: ew-resize;
972
+ }
973
+
974
+ .ds-chat-launcher__float-grip[data-dir="n"] { top: 0; }
975
+ .ds-chat-launcher__float-grip[data-dir="s"] { bottom: 0; }
976
+ .ds-chat-launcher__float-grip[data-dir="e"] { right: 0; }
977
+ .ds-chat-launcher__float-grip[data-dir="w"] { left: 0; }
978
+
979
+ .ds-chat-launcher__float-grip[data-dir="ne"],
980
+ .ds-chat-launcher__float-grip[data-dir="nw"],
981
+ .ds-chat-launcher__float-grip[data-dir="se"],
982
+ .ds-chat-launcher__float-grip[data-dir="sw"] {
983
+ width: 16px;
984
+ height: 16px;
985
+ }
986
+
987
+ .ds-chat-launcher__float-grip[data-dir="ne"] { top: 0; right: 0; cursor: nesw-resize; }
988
+ .ds-chat-launcher__float-grip[data-dir="sw"] { bottom: 0; left: 0; cursor: nesw-resize; }
989
+ .ds-chat-launcher__float-grip[data-dir="nw"] { top: 0; left: 0; cursor: nwse-resize; }
990
+ .ds-chat-launcher__float-grip[data-dir="se"] { bottom: 0; right: 0; cursor: nwse-resize; }
991
+
992
+ .ds-chat-launcher__float-grip[data-dir="n"]::after,
993
+ .ds-chat-launcher__float-grip[data-dir="s"]::after,
994
+ .ds-chat-launcher__float-grip[data-dir="e"]::after,
995
+ .ds-chat-launcher__float-grip[data-dir="w"]::after {
996
+ content: "";
997
+ border-radius: var(--radius-pill);
998
+ background: transparent;
999
+ transition: background var(--motion-duration-fast) var(--motion-ease-standard);
1000
+ }
1001
+
1002
+ .ds-chat-launcher__float-grip[data-dir="n"]::after,
1003
+ .ds-chat-launcher__float-grip[data-dir="s"]::after {
1004
+ width: 48px;
1005
+ height: 4px;
1006
+ }
1007
+
1008
+ .ds-chat-launcher__float-grip[data-dir="e"]::after,
1009
+ .ds-chat-launcher__float-grip[data-dir="w"]::after {
1010
+ width: 4px;
1011
+ height: 48px;
1012
+ }
1013
+
1014
+ .ds-chat-launcher__float-grip:hover::after {
1015
+ background: var(--color-text-tertiary);
1016
+ }
1017
+
1018
+ .ds-chat-launcher__float-grip:active::after {
1019
+ background: var(--color-text-secondary);
1020
+ }
1021
+
1022
+ .ds-chat-launcher__panel--floating .ds-chat-header {
1023
+ cursor: grab;
1024
+ touch-action: none;
1025
+ }
1026
+
1027
+ html[data-chat-resizing] .ds-chat-launcher__panel--floating .ds-chat-header {
1028
+ cursor: grabbing;
1029
+ }
1030
+
1031
+ /* Phone widths: the chat is the document, not an overlay. The panel leaves
1032
+ the fixed layer for normal flow at 100dvh, and the host hides the page
1033
+ around it on the takeover attribute, so the soft keyboard is handled the
1034
+ way the browser handles any page with a field at the bottom. The launcher
1035
+ reads the same threshold as TAKEOVER_QUERY; this rule covers the first
1036
+ render, before a media query has been read, so the two must stay in step.
1037
+ No transition: there is no card form to glide from. */
1038
+ @media (max-width: 719px) {
1039
+ .ds-chat-launcher__panel {
1040
+ position: relative;
1041
+ top: auto;
1042
+ left: auto;
1043
+ height: 100dvh;
1044
+ width: 100%;
1045
+ max-width: none;
1046
+ border: none;
1047
+ border-radius: 0;
1048
+ box-shadow: none;
1049
+ transition: none;
1050
+ }
1051
+ }
1052
+
1053
+ /* ============================================
1054
+ THE FAB — the chat's persistent entry point, bottom right on every page.
1055
+ Hidden while the panel is open; the panel's close returns here.
1056
+ ============================================ */
1057
+
1058
+ .ds-chat-launcher__fab {
1059
+ position: fixed;
1060
+ right: var(--ds-chat-inset);
1061
+ bottom: var(--ds-chat-inset);
1062
+ z-index: 1080;
1063
+ }
1064
+
1065
+ /* The button's own solid field keeps it legible over whatever content
1066
+ scrolls beneath it; the FAB only adds the floating elevation. No
1067
+ backdrop-filter here: the fill is opaque, and an ancestor backdrop root
1068
+ would blind the summary panel's frost. */
1069
+ .ds-chat-launcher__fab .ds-ai-button {
1070
+ border-radius: var(--radius-pill);
1071
+ box-shadow: var(--shadow-floating);
1072
+ }