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