@v-office/website-chat 1.1.3 → 1.3.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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { r as MAX_MESSAGE_CHARS, t as CHAT_LOCALES } from "./protocol-BuNqZAMt.js";
2
- import { f as chatAvailable, s as Spark, t as Arrow, u as loadConversation } from "./icons-DIqOmKoe.js";
2
+ import { h as chatAvailable, l as Spark, p as loadConversation, t as Arrow } from "./icons-B0_KM_uQ.js";
3
3
  import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -12,16 +12,43 @@ const DEFAULT_COPY = {
12
12
  title: "Reiseassistent",
13
13
  newConversation: "Neues Gespräch",
14
14
  notice: "Sie chatten mit einer KI. Preise und Verfügbarkeit in den Karten kommen direkt aus unserem Buchungssystem.",
15
+ aiBadge: "KI",
16
+ noticeShort: "Preise aus dem Buchungssystem",
17
+ noticeMore: "Mehr zum KI-Hinweis",
15
18
  greeting: "Hallo! Erzählen Sie mir einfach, wie Ihr Urlaub aussehen soll: wann, mit wem und was Ihnen wichtig ist. Ich suche passende Unterkünfte, prüfe freie Termine und Preise und zeige Ihnen, was passt.",
16
19
  suggestions: [
17
- "Ein Ferienhaus für 4 Personen in den Sommerferien",
20
+ "Sommerwoche für 2 Erwachsene und 2 Kinder",
18
21
  "Eine Woche mit Hund, gern mit eingezäuntem Garten",
19
22
  "Ein Wochenende zu zweit mit Sauna",
20
- "Was ist in den Herbstferien noch frei?"
23
+ "Was ist am nächsten Wochenende noch frei?"
21
24
  ],
22
- placeholder: "Wann, mit wem, was ist Ihnen wichtig?",
25
+ placeholder: "Ihre Reise beschreiben …",
23
26
  send: "Senden",
24
27
  thinking: "Die Antwort wird geschrieben",
28
+ status: {
29
+ search: "Suche passende Unterkünfte …",
30
+ rental: "Lese die Angaben zur Unterkunft …",
31
+ availability: "Prüfe freie Tage …",
32
+ quote: "Prüfe den Preis im Buchungssystem …",
33
+ reviews: "Lese die Bewertungen …"
34
+ },
35
+ retry: "Erneut senden",
36
+ privacyLink: "Datenschutzerklärung",
37
+ yourPick: "Ihre Auswahl",
38
+ yourTrip: "Ihre Reise",
39
+ assumed: "angenommen",
40
+ guests: {
41
+ one: "{n} Gast",
42
+ other: "{n} Gäste"
43
+ },
44
+ adultsShort: { other: "{n} Erw." },
45
+ weekend: "Wochenende",
46
+ tripStarters: {
47
+ party: "Wir sind ",
48
+ period: "Andere Reisezeit: ",
49
+ nights: "Andere Dauer: ",
50
+ wishes: "Andere Wünsche: "
51
+ },
25
52
  yourMessage: "Ihre Nachricht",
26
53
  consent: "Der Chat wird von einer KI beantwortet. Ihre Nachrichten werden dazu an unseren KI-Dienstleister übermittelt.",
27
54
  consentAccept: "Chat starten",
@@ -31,7 +58,7 @@ const DEFAULT_COPY = {
31
58
  contactLink: "Kontakt aufnehmen",
32
59
  bookStay: "Zur Buchung",
33
60
  viewRental: "Zur Unterkunft",
34
- book: "Buchen",
61
+ book: "Zur Buchung",
35
62
  view: "Ansehen",
36
63
  matchBadge: "Passt genau",
37
64
  nights: {
@@ -39,10 +66,31 @@ const DEFAULT_COPY = {
39
66
  other: "{n} Nächte"
40
67
  },
41
68
  total: "Gesamtpreis",
69
+ totalForNights: {
70
+ one: "Gesamtpreis für {n} Nacht",
71
+ other: "Gesamtpreis für {n} Nächte"
72
+ },
73
+ priceDetails: "Preisdetails",
74
+ dealFallback: "Angebot",
75
+ youSave: "Sie sparen {amount}",
76
+ freeCancellation: "Kostenlos stornierbar bis {date}",
77
+ nonRefundable: "Nicht erstattbar",
78
+ arrival: "Anreise",
79
+ departure: "Abreise",
80
+ continueToBooking: "Weiter zur Buchung",
81
+ nothingBookedYet: "Noch nichts gebucht. Auf der Buchungsseite:",
82
+ handoverSteps: [
83
+ "Kontaktdaten eingeben",
84
+ "Zahlungsart wählen",
85
+ "Bestätigung per E-Mail"
86
+ ],
42
87
  notBookable: "Für diesen Zeitraum nicht buchbar",
43
88
  photo: "Foto",
89
+ openPhotos: "Fotos von {name} ansehen",
90
+ closePhotos: "Fotos schließen",
44
91
  previous: "Zurück",
45
92
  next: "Weiter",
93
+ rentalsFound: "Gefundene Unterkünfte",
46
94
  reviews: {
47
95
  one: "{n} Bewertung",
48
96
  other: "{n} Bewertungen"
@@ -53,6 +101,19 @@ const DEFAULT_COPY = {
53
101
  booked: "belegt",
54
102
  closed: "nicht buchbar",
55
103
  yourStay: "Ihr Zeitraum",
104
+ arrivalPossible: "Anreise möglich",
105
+ cheaper: "günstiger",
106
+ dearer: "teurer",
107
+ showPrice: "Preis anzeigen",
108
+ arrivalOn: "Anreise {date}",
109
+ possibleFrom: "Ab {date} möglich",
110
+ until: "bis {date}",
111
+ otherArrival: "Anderen Anreisetag wählen",
112
+ otherArrivalStarter: "Anreise am ",
113
+ freePeriods: "Freie Termine",
114
+ freePeriodsIn: "Freie Termine im {month}",
115
+ datesDiffer: "Termine je Unterkunft verschieden",
116
+ otherDate: "anderer Termin",
56
117
  compareRooms: "Räume",
57
118
  compareHighlights: "Highlights",
58
119
  compareRating: "Bewertung",
@@ -75,6 +136,10 @@ const DEFAULT_COPY = {
75
136
  children: {
76
137
  one: "{n} Kind",
77
138
  other: "{n} Kinder"
139
+ },
140
+ pets: {
141
+ one: "{n} Haustier",
142
+ other: "{n} Haustiere"
78
143
  }
79
144
  },
80
145
  en: {
@@ -84,16 +149,43 @@ const DEFAULT_COPY = {
84
149
  title: "Travel assistant",
85
150
  newConversation: "New conversation",
86
151
  notice: "You are chatting with an AI. Prices and availability in the cards come straight from our booking system.",
152
+ aiBadge: "AI",
153
+ noticeShort: "Prices from the booking system",
154
+ noticeMore: "About this AI chat",
87
155
  greeting: "Hello! Just tell me what your holiday should look like: when, who with and what matters to you. I find rentals that fit, check free dates and prices and show you what suits.",
88
156
  suggestions: [
89
- "A holiday home for 4 in the summer holidays",
157
+ "A summer week for 2 adults and 2 kids",
90
158
  "A week with our dog, ideally with a fenced garden",
91
159
  "A weekend for two with a sauna",
92
- "What is still free in the autumn holidays?"
160
+ "Anything free next weekend?"
93
161
  ],
94
- placeholder: "When, who with, what matters to you?",
162
+ placeholder: "Describe your trip …",
95
163
  send: "Send",
96
164
  thinking: "Writing the answer",
165
+ status: {
166
+ search: "Searching rentals …",
167
+ rental: "Reading about the rental …",
168
+ availability: "Checking free days …",
169
+ quote: "Checking the price with the booking system …",
170
+ reviews: "Reading reviews …"
171
+ },
172
+ retry: "Send again",
173
+ privacyLink: "Privacy policy",
174
+ yourPick: "Your choice",
175
+ yourTrip: "Your trip",
176
+ assumed: "assumed",
177
+ guests: {
178
+ one: "{n} guest",
179
+ other: "{n} guests"
180
+ },
181
+ adultsShort: { other: "{n} ad." },
182
+ weekend: "Weekend",
183
+ tripStarters: {
184
+ party: "We are ",
185
+ period: "Other dates: ",
186
+ nights: "Other length: ",
187
+ wishes: "Other wishes: "
188
+ },
97
189
  yourMessage: "Your message",
98
190
  consent: "The chat is answered by an AI. Your messages are sent to our AI provider for that.",
99
191
  consentAccept: "Start chat",
@@ -111,10 +203,31 @@ const DEFAULT_COPY = {
111
203
  other: "{n} nights"
112
204
  },
113
205
  total: "Total",
206
+ totalForNights: {
207
+ one: "Total for {n} night",
208
+ other: "Total for {n} nights"
209
+ },
210
+ priceDetails: "Price details",
211
+ dealFallback: "Offer",
212
+ youSave: "You save {amount}",
213
+ freeCancellation: "Free cancellation until {date}",
214
+ nonRefundable: "Non-refundable",
215
+ arrival: "Arrival",
216
+ departure: "Departure",
217
+ continueToBooking: "Continue to booking",
218
+ nothingBookedYet: "Nothing is booked yet. On the booking page:",
219
+ handoverSteps: [
220
+ "Enter your details",
221
+ "Choose how to pay",
222
+ "Confirmation by email"
223
+ ],
114
224
  notBookable: "Not bookable for these dates",
115
225
  photo: "Photo",
226
+ openPhotos: "View photos of {name}",
227
+ closePhotos: "Close photos",
116
228
  previous: "Previous",
117
229
  next: "Next",
230
+ rentalsFound: "Rentals found",
118
231
  reviews: {
119
232
  one: "{n} review",
120
233
  other: "{n} reviews"
@@ -125,6 +238,19 @@ const DEFAULT_COPY = {
125
238
  booked: "taken",
126
239
  closed: "not bookable",
127
240
  yourStay: "Your stay",
241
+ arrivalPossible: "Arrival possible",
242
+ cheaper: "cheaper",
243
+ dearer: "dearer",
244
+ showPrice: "Show price",
245
+ arrivalOn: "Arrival {date}",
246
+ possibleFrom: "Possible from {date}",
247
+ until: "until {date}",
248
+ otherArrival: "Choose another arrival",
249
+ otherArrivalStarter: "Arrival on ",
250
+ freePeriods: "Free dates",
251
+ freePeriodsIn: "Free dates in {month}",
252
+ datesDiffer: "Dates differ per rental",
253
+ otherDate: "other dates",
128
254
  compareRooms: "Rooms",
129
255
  compareHighlights: "Highlights",
130
256
  compareRating: "Rating",
@@ -147,6 +273,10 @@ const DEFAULT_COPY = {
147
273
  children: {
148
274
  one: "{n} child",
149
275
  other: "{n} children"
276
+ },
277
+ pets: {
278
+ one: "{n} pet",
279
+ other: "{n} pets"
150
280
  }
151
281
  }
152
282
  };
@@ -246,6 +376,8 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
246
376
  ${SITE_RADIUS.chat}: var(${SITE_RADIUS.site}, ${SITE_RADIUS.own});
247
377
  --chat-secondary: color-mix(in srgb, var(--chat-primary) 12%, var(--chat-surface));
248
378
  --chat-on-secondary: var(--chat-text);
379
+ /* The tinted bubbles' edge: on a dark surface the tint alone barely stands off the message area. */
380
+ --chat-tint-edge: color-mix(in srgb, var(--chat-primary) 30%, transparent);
249
381
  /* The small marks (the chat's name and sparkle, a card's place, icons, the ring): the primary,
250
382
  unless a site sets an accent of its own (\`colors.accent\`). */
251
383
  --chat-accent: var(--chat-primary);
@@ -255,15 +387,28 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
255
387
  --chat-round: calc(var(--chat-base-radius) * 100);
256
388
  --chat-shadow: 0 18px 48px rgb(15 23 42 / 0.18);
257
389
  --chat-card-shadow: 0 1px 3px rgb(15 23 42 / 0.08), 0 8px 24px rgb(15 23 42 / 0.06);
258
- /* Not set here, so a site's value reaches the widget: \`--chat-top\` keeps a sticky header uncovered,
259
- \`--chat-width\` widens the panel on wide screens. */
390
+ /* The stars' colour: dark amber on a light surface, light amber on a dark one, AA on both. */
391
+ --chat-rating: #8a5a00;
392
+ /* Not set here, so a site's value reaches the widget, on wide screens (from 1024px): \`--chat-top\`
393
+ keeps a sticky header uncovered, \`--chat-width\` widens the panel. A phone's sheet covers the
394
+ header: the panel is modal, so the header could not be tapped and would only cost height. */
395
+ }
396
+ @supports (color: oklch(from red calc(l + 0.1) c h)) {
397
+ :host { --chat-rating: oklch(from var(--chat-surface) calc(0.52 + (1 - l) * 0.35) 0.13 75);
398
+ /* Lines inside a raised dark card: the site's border lifted as the card is (cards below). */
399
+ --chat-card-border: oklch(from var(--chat-border) calc(l + max(0, 0.5 - l) * 0.6) c h); }
260
400
  }
261
401
  * { box-sizing: border-box; }
262
402
  .chat { color: var(--chat-text); font-size: 15px; line-height: 1.45; }
403
+ /* One focus ring for every control, in the site's primary; a click or a tap shows none. Without
404
+ specificity (:where), so a control that needs its ring elsewhere sets it with a plain rule. */
405
+ :where(a, button, [tabindex]:not([tabindex="-1"])):focus-visible { outline: 2px solid var(--chat-primary);
406
+ outline-offset: 2px; }
263
407
 
264
408
  /* The entry pill: frosted glass over any page, the chat's mark, a ring running round it until the
265
409
  visitor has asked something. Phones tap it; wide screens type into it. */
266
- .entry { position: fixed; right: 20px; bottom: 20px; z-index: 2147483000; }
410
+ .entry { position: fixed; right: 20px; bottom: calc(20px + env(safe-area-inset-bottom, 0px));
411
+ z-index: 2147483000; }
267
412
  [data-position="left"] .entry { right: auto; left: 20px; }
268
413
  .pill { position: relative; z-index: 1; display: inline-flex; align-items: center; gap: 12px;
269
414
  padding: 8px 20px 8px 8px; border-radius: var(--chat-round);
@@ -297,72 +442,132 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
297
442
  }
298
443
 
299
444
  /* The panel: over the dimmed page, a card at the screen's edge; on a phone a sheet from the bottom. */
300
- .scrim { position: fixed; left: 0; right: 0; bottom: 0; top: var(--chat-top, 0px); z-index: 2147482999;
445
+ .scrim { position: fixed; inset: 0; z-index: 2147482999;
301
446
  background: rgb(15 23 42 / 0.45); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); }
447
+ /* Clipped, not hidden, where supported: a clipping box is no scroll container, so no script, focus or
448
+ find-in-page can scroll the panel and shift its header out of view. */
302
449
  .panel { position: fixed; z-index: 2147483000; left: 0; right: 0; bottom: 0;
303
- top: calc(var(--chat-top, 0px) + 12px); display: flex; flex-direction: column;
450
+ top: 12px; display: flex; flex-direction: column;
304
451
  background: var(--chat-surface); border-radius: var(--chat-radius) var(--chat-radius) 0 0;
305
- box-shadow: var(--chat-shadow); overflow: hidden; }
452
+ box-shadow: var(--chat-shadow); overflow: hidden; overflow: clip; }
306
453
  .scrim[hidden], .panel[hidden] { display: none; }
454
+ /* Focused by script only (a phone reopening a conversation), never a stop for Tab. */
455
+ .panel:focus { outline: none; }
307
456
  @media (min-width: 1024px) {
457
+ .scrim { top: var(--chat-top, 0px); }
308
458
  .panel { left: auto; right: 20px; bottom: 20px; top: calc(var(--chat-top, 0px) + 20px);
309
459
  width: min(var(--chat-width, 700px), calc(100vw - 40px)); border-radius: var(--chat-radius); }
310
460
  [data-position="left"] .panel { right: auto; left: 20px; }
311
461
  }
312
- .header { display: flex; align-items: center; gap: 8px; padding: 20px 24px 4px; }
313
- .title { margin: 0 auto 0 0; font-size: 13px; font-weight: 600; letter-spacing: 0.16em;
314
- text-transform: uppercase; color: var(--chat-accent); }
462
+ .header { position: relative; display: flex; align-items: center; gap: 8px; padding: 12px 24px 4px; }
463
+ @media (min-width: 1024px) { .header { padding-top: 20px; } }
464
+ /* Once the conversation runs: one line under the title instead of the notice, the list gets the room. */
465
+ .header[data-slim] { padding: 10px 16px 10px 24px; border-bottom: 1px solid var(--chat-border); }
466
+ /* A small phone keeps the line on one line: less inset, and an ellipsis before it would wrap. */
467
+ @media (max-width: 359px) { .header[data-slim] { padding-inline: 16px 12px; } }
468
+ .title-block { margin-right: auto; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
469
+ .title { margin: 0; display: flex; align-items: center; gap: 8px; font-size: 13px; font-weight: 600;
470
+ letter-spacing: 0.16em; text-transform: uppercase; color: var(--chat-accent); }
471
+ /* The persistent AI mark (AI Act transparency), in words, not colour. */
472
+ .badge { padding: 0 6px; border: 1px solid var(--chat-tint-edge); border-radius: min(6px, var(--chat-radius));
473
+ background: var(--chat-secondary); color: var(--chat-on-secondary); font-size: 12px; letter-spacing: 0.04em; }
474
+ .notice-short { margin: 0; display: flex; align-items: center; gap: 4px; min-width: 0; font-size: 12px;
475
+ color: var(--chat-muted); }
476
+ .notice-short > span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
477
+ /* A small mark with a finger's target: the pseudo-element takes the tap around it. */
478
+ .info { position: relative; display: grid; place-items: center; width: 20px; height: 20px; padding: 0; border: 0;
479
+ border-radius: 999px; background: transparent; color: inherit; cursor: pointer; }
480
+ .info::after { content: ""; position: absolute; inset: -12px; }
481
+ .info svg { width: 14px; height: 14px; }
482
+ .notice-pop { position: absolute; z-index: 2; top: calc(100% + 6px); left: 16px; right: 16px; margin: 0;
483
+ padding: 12px 16px; border: 1px solid var(--chat-border); border-radius: calc(var(--chat-radius) * 0.75);
484
+ background: var(--chat-surface); box-shadow: var(--chat-shadow); font-size: 13px; color: var(--chat-text); }
315
485
  .icon-button { flex: none; display: grid; place-items: center; width: 40px; height: 40px; padding: 0;
316
486
  border: 1px solid var(--chat-border); border-radius: var(--chat-round); background: var(--chat-surface);
317
487
  color: var(--chat-text); cursor: pointer; }
318
- .icon-button:hover { border-color: var(--chat-primary); }
319
488
  /* Closing is the panel's one strong control, filled like the site's buttons. */
320
489
  .close { background: var(--chat-primary); border-color: var(--chat-primary); color: var(--chat-on-primary); }
321
- .close:hover { opacity: 0.9; }
322
- .notice { margin: 0; padding: 0 24px 14px; font-size: 12px; color: var(--chat-muted); }
323
- .messages { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 24px 24px 40px;
490
+ .notice { margin: 0; padding: 0 24px 14px; font-size: 13px; color: var(--chat-muted); }
491
+ /* Positioned: the absolute speaker labels (.sr-only) scroll with the list instead of giving the panel a
492
+ scroll range, and a turn's offsetTop is measured from the list. */
493
+ .messages { position: relative; flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 24px 24px 40px;
324
494
  display: flex; flex-direction: column; gap: 16px; background: var(--chat-surface-soft);
325
495
  container-type: inline-size; }
326
496
  .turn { display: flex; flex-direction: column; }
327
497
  .message { max-width: 88%; white-space: pre-wrap; overflow-wrap: anywhere; }
328
498
  .message[data-role="visitor"] { align-self: flex-end; margin-left: 48px; padding: 12px 16px;
329
- border-radius: calc(var(--chat-radius) * 0.75); background: var(--chat-secondary);
499
+ border: 1px solid var(--chat-tint-edge); border-radius: calc(var(--chat-radius) * 0.75);
500
+ background: var(--chat-secondary);
330
501
  color: var(--chat-on-secondary); }
331
502
  .message[data-role="assistant"] { align-self: flex-start; max-width: 100%; font-size: 16px; line-height: 1.6; }
332
503
  .message strong { font-weight: 600; }
333
504
  .ui { align-self: stretch; display: flex; flex-direction: column; gap: 12px; }
334
- .thinking { display: flex; gap: 5px; padding: 4px 2px; }
505
+ .thinking { display: flex; align-items: center; gap: 5px; padding: 4px 2px; }
506
+ .thinking-label { margin-left: 6px; color: var(--chat-muted); font-size: 14px; }
335
507
  .thinking i { width: 7px; height: 7px; border-radius: 999px; background: var(--chat-accent); opacity: 0.5; }
336
- .dock { position: relative; padding: 10px 20px 16px; background: var(--chat-surface-soft); }
508
+ .dock { position: relative; padding: 8px 12px calc(14px + env(safe-area-inset-bottom, 0px));
509
+ background: var(--chat-surface-soft); container: dock / inline-size; }
337
510
  .dock::before { content: ""; position: absolute; left: 0; right: 0; top: -28px; height: 28px;
338
511
  pointer-events: none; background: linear-gradient(to top, var(--chat-surface-soft), transparent); }
339
- .suggestions { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; margin: 0 -20px 10px;
340
- padding: 0 20px; }
341
- .suggestions::-webkit-scrollbar { display: none; }
342
- /* Suggestions and quick replies: filled pills, an icon for the topic in the accent colour. */
512
+ /* Suggestions wrap, on a phone too: in a row that scrolls, one cut chip hides the others. */
513
+ .suggestions { display: flex; flex-wrap: wrap; gap: 8px; margin: 2px 8px 10px; }
514
+ /* Suggestions and quick replies: filled pills; a suggestion has an icon for its topic in the accent
515
+ colour (a quick reply is the model's words, an icon guessed from them can be wrong). */
343
516
  .suggestion, .chip { flex: none; max-width: 100%; display: inline-flex; align-items: center; gap: 8px;
344
- border: 1px solid transparent; border-radius: var(--chat-round); background: var(--chat-secondary);
517
+ border: 1px solid var(--chat-tint-edge); border-radius: var(--chat-round); background: var(--chat-secondary);
345
518
  color: var(--chat-on-secondary); padding: 8px 16px 8px 12px; font: inherit; font-size: 14px;
346
- font-weight: 600; line-height: 1.35; text-align: left; white-space: nowrap; cursor: pointer; }
347
- .suggestion svg, .chip svg { flex: none; color: var(--chat-accent); }
348
- /* Suggestions scroll in one row; a quick reply wraps inside the message's width. */
349
- .chip { white-space: normal; }
350
- .suggestion:hover { border-color: var(--chat-primary); background: var(--chat-surface); color: var(--chat-text); }
351
- @media (min-width: 1024px) {
352
- .suggestions { flex-wrap: wrap; overflow: visible; margin: 0 0 10px; padding: 0; }
353
- .suggestion { white-space: normal; }
354
- }
519
+ font-weight: 600; line-height: 1.35; text-align: left; cursor: pointer; }
520
+ /* A quick reply is a tinted pill, never outlined: outlined is a link out of the chat (ContactLink). The
521
+ one the visitor took goes quiet. */
522
+ .chip { padding-left: 16px; }
523
+ .chip[data-chosen] { border-color: var(--chat-border); background: transparent; color: var(--chat-muted);
524
+ cursor: default; }
525
+ .suggestion svg { flex: none; color: var(--chat-accent); }
355
526
  .composer { display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 18px;
356
527
  border: 1px solid var(--chat-border); border-radius: var(--chat-round); background: var(--chat-surface);
357
528
  box-shadow: 0 4px 16px rgb(15 23 42 / 0.08); }
358
- .composer:focus-within { border-color: var(--chat-text); }
529
+ /* The field's ring, not the send button's too (that one has its own), and only for a keyboard: a text
530
+ field matches :focus-visible after a tap as well. */
531
+ .composer[data-keyboard]:has(textarea:focus-visible) { outline: 2px solid var(--chat-primary); outline-offset: 2px; }
359
532
  /* 16px keeps iOS from zooming into the field. */
360
533
  .composer textarea { flex: 1; min-width: 0; min-height: 36px; max-height: 128px; resize: none; border: 0;
361
534
  outline: none; background: transparent; padding: 6px 0; font: inherit; font-size: 16px;
362
535
  line-height: 1.5; color: inherit; field-sizing: content; }
536
+ /* The field's only visible label: the muted text colour, not the browser's grey (below AA in dark). */
537
+ .composer textarea::placeholder { color: var(--chat-muted); opacity: 1; }
363
538
  .send { flex: none; display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0;
364
539
  border-radius: var(--chat-round); background: var(--chat-primary); color: var(--chat-on-primary); cursor: pointer; }
365
540
  .send:disabled { opacity: 0.3; cursor: not-allowed; }
541
+ /* The bar over the input: the stay in reach with its total and button, or what the chat searched with.
542
+ The composer's shape, one thumb above it. */
543
+ .dock-bar { display: flex; align-items: center; gap: 8px; min-height: 50px; margin-bottom: 8px; padding: 3px 3px 3px 16px;
544
+ border: 1px solid var(--chat-border); border-radius: var(--chat-round); background: var(--chat-surface);
545
+ box-shadow: 0 4px 16px rgb(15 23 42 / 0.08); }
546
+ .dock-bar .thumb { display: none; width: 40px; height: 40px; margin-left: -10px; border-radius: 999px; }
547
+ /* A wide panel has room for the photo; a phone needs the width for the party. */
548
+ @container dock (min-width: 456px) { .dock-bar .thumb { display: block; } }
549
+ .dock-text { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; padding: 0;
550
+ border: 0; background: none; color: inherit; font: inherit; line-height: 1.3; text-align: left; cursor: pointer; }
551
+ .dock-name, .dock-sub { max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
552
+ .dock-name { font-size: 14px; font-weight: 600; }
553
+ .dock-sub { font-size: 12px; color: var(--chat-muted); }
554
+ .dock-sub .price { color: var(--chat-text); }
555
+ /* Narrower than a card's button: the bar's width goes to the party. */
556
+ .dock-bar .button.compact { margin: 0; min-height: 42px; padding: 0 11px; font-size: 13px; }
557
+ .dock-bar[data-kind="trip"] { padding: 3px 0 3px 6px; }
558
+ .trip-chips { flex: 1; min-width: 0; display: flex; gap: 4px; padding-right: 16px; overflow-x: auto;
559
+ scrollbar-width: none; mask-image: linear-gradient(to left, transparent, #000 16px); }
560
+ .trip-chips::-webkit-scrollbar { display: none; }
561
+ .trip-chip { flex: none; display: inline-flex; align-items: center; gap: 5px; min-height: 36px; padding: 0 10px;
562
+ border: 1px solid var(--chat-border); border-radius: var(--chat-round); background: transparent;
563
+ color: var(--chat-text); font: inherit; font-size: 13px; font-weight: 600; white-space: nowrap; cursor: pointer; }
564
+ /* Inside the row that scrolls, so it clips: the ring sits inside the chip. */
565
+ .trip-chip:focus-visible { outline-offset: -2px; }
566
+ /* A party the search assumed: dashed, and said in words. */
567
+ .trip-chip[data-assumed] { border-style: dashed; border-color: var(--chat-muted); }
568
+ .trip-chip em { font-style: normal; font-weight: 500; color: var(--chat-muted); }
569
+ /* The bar's stay, brought into view in the list: marked for a moment. */
570
+ [data-pulse] { outline: 2px solid var(--chat-primary); outline-offset: 2px; }
366
571
  .consent { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 14px;
367
572
  background: var(--chat-surface-soft); }
368
573
  .consent p { margin: 0; }
@@ -374,8 +579,11 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
374
579
  white-space: pre-wrap; }
375
580
  /* The bubble's own text colour, softened: the muted grey suits the surface, not a set secondary. */
376
581
  .pending figcaption { margin-bottom: 2px; color: inherit; opacity: 0.75; }
377
- .fallback { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-bottom: 10px; }
582
+ /* The chat cannot answer: its reply to the visitor's message, the ways on as buttons. */
583
+ .fallback { display: flex; flex-direction: column; gap: 12px; }
378
584
  .fallback p { margin: 0; }
585
+ .fallback-links { display: flex; flex-wrap: wrap; gap: 8px; }
586
+ .fallback-links > .button { margin: 0; }
379
587
 
380
588
  /* What the replies show. */
381
589
  .card { border-radius: var(--chat-radius); overflow: hidden; background: var(--chat-surface);
@@ -383,8 +591,26 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
383
591
  .gallery-wrap, .carousel-wrap { position: relative; }
384
592
  .gallery, .carousel { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; }
385
593
  .gallery::-webkit-scrollbar, .carousel::-webkit-scrollbar { display: none; }
386
- .gallery img { flex: 0 0 100%; scroll-snap-align: start; display: block; width: 100%; aspect-ratio: 5 / 3;
387
- height: auto; object-fit: cover; }
594
+ .gallery img, .photo-open { flex: 0 0 100%; scroll-snap-align: start; display: block; width: 100%;
595
+ aspect-ratio: 4 / 3; height: auto; object-fit: cover; }
596
+ /* Flatter in a row, to bring a card's price above the input on a phone: 5:3, and 2:1 on a card with
597
+ free periods; the single card keeps 4:3. */
598
+ .carousel :is(.gallery > img, .photo-open), .carousel .no-photo { aspect-ratio: 5 / 3; }
599
+ .carousel .rental-card:has(.periods) :is(.gallery > img, .photo-open, .no-photo) { aspect-ratio: 2 / 1; }
600
+ /* A card's photo opens the photos large. */
601
+ .photo-open { padding: 0; border: 0; background: none; cursor: zoom-in; }
602
+ .photo-open img { aspect-ratio: auto; height: 100%; }
603
+ .photo-open:focus-visible { outline-offset: -3px; }
604
+ .photo-count { position: absolute; top: 10px; right: 10px; padding: 3px 10px; border-radius: var(--chat-round);
605
+ background: rgb(15 23 42 / 0.55); color: #fff; font-size: 12px; font-weight: 600;
606
+ font-variant-numeric: tabular-nums; pointer-events: none; }
607
+ /* Place and score on the photo's foot, white on a dark gradient whatever the photo. */
608
+ .photo-meta { position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: flex-end;
609
+ justify-content: space-between; gap: 8px; padding: 36px 16px 12px; pointer-events: none;
610
+ background: linear-gradient(transparent, rgb(15 23 42 / 0.62)); color: #fff; font-size: 14px; font-weight: 600; }
611
+ .photo-rating { flex: none; }
612
+ .photo-rating .star { color: #ffd166; }
613
+ .photo-rating small { font-size: 12px; font-weight: 500; opacity: 0.9; }
388
614
  /* Dots say which photo shows; arrows come up under a pointer and on keyboard focus (a finger swipes:
389
615
  on a touch screen they take no tap, so a tap on a photo's edge does not turn it unseen). */
390
616
  .dots { position: absolute; left: 50%; bottom: 10px; translate: -50% 0; display: flex; gap: 4px; }
@@ -397,7 +623,7 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
397
623
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.18); cursor: pointer; opacity: 0; }
398
624
  .gallery-arrow[data-side="prev"] { left: 10px; }
399
625
  .gallery-arrow[data-side="next"] { right: 10px; }
400
- .gallery-wrap:hover .gallery-arrow:not(:disabled), .gallery-arrow:focus-visible { opacity: 1; }
626
+ .gallery-arrow:focus-visible { opacity: 1; }
401
627
  @media (hover: none) { .gallery-arrow:not(:focus-visible) { pointer-events: none; } }
402
628
  /* The row scrolls, so it clips: room below for the cards' shadow, taken back from the gap under it. */
403
629
  .carousel { gap: 12px; padding: 2px 2px 16px; margin-bottom: -10px; }
@@ -407,105 +633,382 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
407
633
  @container (min-width: 520px) {
408
634
  .carousel > .card { flex-basis: calc((100% - 24px) / 2.15); }
409
635
  .carousel:has(> .card:nth-child(2):last-child) > .card { flex-basis: calc((100% - 12px) / 2); }
410
- .carousel-wrap:has(> .carousel > .card:nth-child(2):last-child) > .snap-nav { display: none; }
411
- /* "n / total" counts one card per step, which a row of two no longer takes: the arrows stay. */
412
- .carousel-wrap > .snap-nav > span { visibility: hidden; }
636
+ .carousel-wrap:has(> .carousel > .card:nth-child(2):last-child) > .snap-nav > :not(.snap-caption) {
637
+ display: none; }
638
+ /* The count follows the first card in view, and two show at once: it would skip numbers. */
639
+ .carousel-wrap > .snap-nav > .snap-count { display: none; }
413
640
  }
414
- .snap-nav { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; }
415
- .snap-nav button, .calendar-head button { border: 1px solid var(--chat-border); background: var(--chat-surface);
416
- color: var(--chat-text); border-radius: var(--chat-round); width: 28px; height: 28px; cursor: pointer; font: inherit;
417
- line-height: 1; }
418
- .snap-nav button:disabled, .calendar-head button:disabled { opacity: 0.35; cursor: default; }
419
- .rating { color: #b7791f; font-weight: 700; }
420
- .breakdown .line { display: flex; justify-content: space-between; gap: 12px; }
421
- .breakdown .total { border-top: 1px solid var(--chat-border); padding-top: 6px; margin-top: 2px;
641
+ /* The row's controls, grouped at its end: "‹ 2 / 8 ›" under a pointer, dots under a finger; a caption
642
+ at its start. */
643
+ .snap-nav { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 6px; }
644
+ .snap-caption { flex: 1; min-width: 0; color: var(--chat-muted); font-size: 13px; }
645
+ .snap-dots { display: none; gap: 5px; }
646
+ .snap-dots i { width: 7px; height: 7px; border-radius: 999px; background: var(--chat-border); }
647
+ .snap-dots i[data-current] { width: 18px; background: var(--chat-primary); }
648
+ @media (pointer: coarse) {
649
+ .snap-dots { display: flex; }
650
+ .snap-nav > button.snap-arrow, .snap-count { display: none; }
651
+ }
652
+ /* More cards beyond an edge fade out there. Cards snap to the row's start, so in the middle of the row
653
+ nothing of the previous card shows: only the end fades, never the card being read. At the row's end
654
+ the previous card peeks in, and its edge fades. */
655
+ .carousel:is([data-fade="end"], [data-fade="both"]) { mask-image: linear-gradient(to left, transparent, #000 44px); }
656
+ .carousel[data-fade="start"] { mask-image: linear-gradient(to right, transparent, #000 44px); }
657
+ .snap-nav button, .calendar-head button { flex: none; display: grid; place-items: center; width: 36px;
658
+ height: 36px; padding: 0; border: 1px solid var(--chat-border); background: var(--chat-surface);
659
+ color: var(--chat-text); border-radius: var(--chat-round); cursor: pointer; }
660
+ .snap-nav button:disabled, .calendar-head button:disabled { opacity: 0.5; cursor: default; }
661
+ .rating { color: var(--chat-rating); font-weight: 700; }
662
+ .breakdown .line, .price-details .line { display: flex; justify-content: space-between; gap: 12px;
663
+ font-variant-numeric: tabular-nums; }
664
+ .breakdown .total, .price-details .total { border-top: 1px solid var(--chat-border); padding-top: 6px; margin-top: 2px;
422
665
  font-weight: 700; }
423
- .alternatives, .quick-replies { display: flex; flex-wrap: wrap; gap: 6px; }
424
- .chip-link { border: 1px solid var(--chat-primary); color: var(--chat-primary); border-radius: var(--chat-round);
425
- padding: 5px 10px; text-decoration: none; font-size: 13px; }
426
- .chip:hover { border-color: var(--chat-primary); background: var(--chat-surface); color: var(--chat-text); }
666
+ .quick-replies { display: flex; flex-wrap: wrap; gap: 6px; }
667
+ /* A chip on its own in a reply (a refused stay's "Anderen Anreisetag wählen"): as wide as its words. */
668
+ .ui > .chip { align-self: flex-start; }
669
+ /* Rows that choose or ask: other free periods (a radio mark, the total right-aligned), a refused stay's
670
+ allowed lengths ("Preis anzeigen"). Full width, a finger's height. */
671
+ .rows-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px;
672
+ border-bottom: 1px solid var(--chat-border); }
673
+ .rows-head > div { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
674
+ .rows-head strong { font-weight: 600; }
675
+ .rows-head .thumb { width: 40px; height: 40px; }
676
+ .row { display: flex; align-items: center; gap: 12px; width: 100%; min-height: 60px; padding: 8px 16px;
677
+ border: 0; border-top: 1px solid var(--chat-border); background: transparent; color: inherit; font: inherit;
678
+ text-align: left; cursor: pointer; }
679
+ .rows > .row:first-child { border-top: 0; }
680
+ .row:disabled { cursor: default; }
681
+ /* Inside the card, which clips: the ring sits inside the row. */
682
+ .row:focus-visible { outline-offset: -2px; }
683
+ .row[aria-pressed="true"] { background: var(--chat-secondary); box-shadow: inset 3px 0 0 var(--chat-primary); }
684
+ .row-when { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.3; }
685
+ .row-when strong { font-weight: 600; }
686
+ .row-when span { color: var(--chat-muted); font-size: 13px; }
687
+ .row > .price { flex: none; white-space: nowrap; }
688
+ /* The nights and a discount's badge: side by side, or the badge under the nights where the row is narrow. */
689
+ .row-when > span:has(.deal) { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; }
690
+ .row-when .deal { color: var(--chat-on-secondary); }
691
+ .row-go { display: inline-flex; align-items: center; gap: 4px; color: var(--chat-primary); font-size: 14px;
692
+ font-weight: 600; white-space: nowrap; }
693
+ .radio { flex: none; display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%;
694
+ border: 1.5px solid var(--chat-muted); }
695
+ .row[aria-pressed="true"] .radio { border-color: var(--chat-primary); background: var(--chat-primary);
696
+ color: var(--chat-on-primary); }
697
+ .refused .card-body { padding-bottom: 12px; }
698
+ .refused .rows { border-top: 1px solid var(--chat-border); }
699
+ /* A card's free periods from a flexible search: pills that switch its stay in place. */
700
+ .periods { margin: 0 18px; padding: 10px 0 12px; border-top: 1px solid var(--chat-border); }
701
+ .periods-caption { margin: 0 0 6px; font-size: 13px; color: var(--chat-muted); }
702
+ /* "24. Apr. – 1. Mai" needs about 115px: two to a carousel card's row, one on a small phone. Days of one
703
+ month ("14.–21.") fit three or four to a row. */
704
+ .period-pills { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 124px), 1fr)); gap: 6px; }
705
+ .period-pills[data-one-month] { grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); }
706
+ .period-pill { min-height: 38px; padding: 0 8px; border: 1px solid var(--chat-border);
707
+ border-radius: var(--chat-round); background: transparent; color: var(--chat-text); font: inherit;
708
+ font-size: 13px; font-weight: 600; white-space: nowrap; cursor: pointer; }
709
+ .period-pill:disabled { cursor: default; }
710
+ .period-pill[aria-pressed="true"] { border-color: var(--chat-primary); background: var(--chat-primary);
711
+ color: var(--chat-on-primary); }
712
+ .periods + .card-foot { border-top: 0; padding-top: 0; }
713
+ /* Compare: a stay on other dates than the first column's says so, in words. */
714
+ .other-date { display: inline-block; margin-left: 6px; padding: 0 6px; border: 1px solid var(--chat-border);
715
+ border-radius: var(--chat-round); color: var(--chat-muted); font-size: 12px; }
427
716
  .calendar { border: 1px solid var(--chat-border); border-radius: var(--chat-radius); padding: 12px;
428
717
  background: var(--chat-surface); }
429
718
  .calendar-head { display: flex; align-items: center; justify-content: space-between; gap: 8px;
430
719
  font-weight: 700; margin-bottom: 8px; text-align: center; }
431
720
  .calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; text-align: center;
432
- font-size: 12px; }
721
+ font-size: 13px; }
433
722
  .weekday { color: var(--chat-muted); font-weight: 600; }
434
723
  .day { padding: 4px 0; border-radius: min(6px, var(--chat-radius)); }
435
724
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
436
725
  white-space: nowrap; }
437
- .day[data-status="free"], .legend i[data-status="free"] {
438
- background: color-mix(in srgb, var(--chat-primary) 14%, var(--chat-surface)); color: var(--chat-text); }
439
- .day[data-status="booked"], .legend i[data-status="booked"] { background: var(--chat-surface-soft);
440
- color: var(--chat-muted);
441
- text-decoration: line-through; }
726
+ /* Read without colour: free days plain, taken ones hatched with a muted numeral, arrival days a dot and a
727
+ bold numeral, a free day's price level one to three ticks; the stay filled. */
728
+ .day { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
729
+ gap: 3px; min-height: 40px; padding: 0; border: 0; background: transparent; color: inherit; font: inherit;
730
+ font-variant-numeric: tabular-nums; }
731
+ button.day { cursor: pointer; }
732
+ .day[data-status="booked"], .legend i[data-status="booked"] { color: var(--chat-muted);
733
+ background: repeating-linear-gradient(135deg, transparent 0 4px, var(--chat-border) 4px 5.5px);
734
+ box-shadow: inset 0 0 0 1px var(--chat-border); }
442
735
  .day[data-status="closed"] { color: var(--chat-muted); opacity: 0.6; }
443
- .day[data-stay], .legend i[data-stay] { background: var(--chat-primary); color: var(--chat-on-primary);
444
- text-decoration: none; }
445
- .legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: 12px; }
446
- .legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 4px;
447
- vertical-align: -1px; }
736
+ .day[data-arrival] { font-weight: 700; }
737
+ /* At the numeral, however wide the cell. */
738
+ .day[data-arrival]::before, .legend i[data-arrival] { content: ""; position: absolute; top: 4px;
739
+ right: calc(50% - 16px);
740
+ width: 5px; height: 5px; border-radius: 50%; background: var(--chat-primary); }
741
+ .ticks { display: flex; gap: 2px; height: 3px; }
742
+ .ticks i { width: 6px; height: 3px; border-radius: 2px; background: var(--chat-muted); }
743
+ .day[data-stay], .legend i[data-stay], .day[aria-pressed="true"] { background: var(--chat-primary);
744
+ color: var(--chat-on-primary); }
745
+ .day[data-stay]::before, .day[data-stay] .ticks i, .day[aria-pressed="true"]::before,
746
+ .day[aria-pressed="true"] .ticks i { background: var(--chat-on-primary); }
747
+ /* A tapped arrival is the one filled day: the marked stay steps back to a tint meanwhile. */
748
+ .calendar-grid:has(.day[aria-pressed="true"]) .day[data-stay]:not([aria-pressed="true"]) {
749
+ background: var(--chat-secondary); color: var(--chat-on-secondary); }
750
+ .calendar-grid:has(.day[aria-pressed="true"]) .day[data-stay]:not([aria-pressed="true"])::before {
751
+ background: var(--chat-primary); }
752
+ .calendar-grid:has(.day[aria-pressed="true"]) .day[data-stay]:not([aria-pressed="true"]) .ticks i {
753
+ background: var(--chat-muted); }
754
+ /* A finger's height for a day that takes a tap. */
755
+ @media (pointer: coarse) { button.day { min-height: 44px; } }
756
+ .cal-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px;
757
+ padding-top: 10px; border-top: 1px solid var(--chat-border); }
758
+ .cal-foot strong { font-weight: 600; white-space: nowrap; }
759
+ .cal-foot .button { gap: 6px; }
760
+ /* Forced colours drop tints and backgrounds: borders and the system's text colour carry the marks. */
761
+ @media (forced-colors: active) {
762
+ .day[data-status="booked"], .legend i[data-status="booked"] { background: none; border: 1px dashed CanvasText;
763
+ text-decoration: line-through; }
764
+ .day[data-arrival]::before, .ticks i, .legend i[data-arrival] { background: CanvasText; }
765
+ .day[data-stay], .day[aria-pressed="true"] { border: 2px solid Highlight; }
766
+ /* The stay and the tapped day share that border: the tapped one gets a second mark. */
767
+ .day[aria-pressed="true"] { outline: 2px solid Highlight; outline-offset: -5px; }
768
+ }
769
+ .legend { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 8px; font-size: 13px; }
770
+ .legend > span { display: inline-flex; align-items: center; gap: 4px; }
771
+ .legend > span > i { display: inline-block; width: 12px; height: 12px; border-radius: 3px;
772
+ border: 1px solid var(--chat-border); }
773
+ .legend > span > i[data-arrival] { position: static; width: 6px; height: 6px; border: 0; }
774
+ .legend-levels .ticks { align-self: center; }
448
775
  .compare { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 8px; }
449
776
  .compare-col { border-radius: var(--chat-radius); padding: 10px; background: var(--chat-surface);
450
- box-shadow: var(--chat-card-shadow); display: flex; flex-direction: column; gap: 6px; font-size: 13px;
451
- min-width: 0; }
452
- /* Three columns leave a button about 90px: "Zur Buchung" fits on one line only without side padding. */
453
- .compare-col .button { padding: 8px 2px; font-size: 13px; }
777
+ box-shadow: var(--chat-card-shadow); display: flex; flex-direction: column; gap: 6px; font-size: 14px;
778
+ min-width: 0; hyphens: auto; }
454
779
  .compare-col dl { margin: 0; display: flex; flex-direction: column; gap: 2px; flex: 1; }
455
- .compare-col dt { color: var(--chat-muted); font-size: 11px; margin-top: 4px; }
456
- .compare-col dd { margin: 0; overflow-wrap: anywhere; }
780
+ .compare-col dt { color: var(--chat-muted); font-size: 12px; margin-top: 4px; }
781
+ .compare-col dd { margin: 0; overflow-wrap: break-word; }
782
+ .compare-col .price { font-size: 16px; }
783
+ /* A phone has room for two columns, not three of 100px with a word on each line: the third peeks in
784
+ and the row scrolls, with room below for the columns' shadow, as the carousel. */
785
+ @container (max-width: 519px) {
786
+ /* A phone shows one card at a time: each as tall as its own content, so the first card's price and
787
+ button are not pushed down to the tallest card's foot. */
788
+ .carousel { align-items: flex-start; }
789
+ .compare { grid-auto-columns: minmax(150px, 1fr); overflow-x: auto; scroll-snap-type: x mandatory;
790
+ scrollbar-width: none; padding: 2px 2px 16px; margin-bottom: -10px; }
791
+ .compare::-webkit-scrollbar { display: none; }
792
+ .compare-col { scroll-snap-align: start; }
793
+ /* The rows line up across the columns: one long name off screen would push every visible row down. */
794
+ .compare-col > strong { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
795
+ overflow: hidden; }
796
+ }
797
+ /* Side by side, the rows line up across the columns (a long name pushes every row of its column down
798
+ otherwise): the name, the list's eight cells and the button share the comparison's ten rows. */
799
+ @supports (grid-template-rows: subgrid) {
800
+ .compare { grid-template-rows: repeat(10, auto); row-gap: 0; }
801
+ .compare-col { display: grid; grid-row: span 10; grid-template-rows: subgrid; }
802
+ .compare-col dl { display: grid; grid-row: span 8; grid-template-rows: subgrid; }
803
+ .compare-col .button { grid-row: -2; }
804
+ }
457
805
  .card-body { padding: 16px 18px 12px; display: flex; flex-direction: column; gap: 6px; }
458
806
  .card-title { margin: 0; font-size: 18px; font-weight: 500; line-height: 1.3; }
807
+ /* One long name (v9 names often run to a dozen words) would make every card of a row taller: in a
808
+ carousel the name stops at three lines, as in a narrow Compare; the single card and the quote show it
809
+ whole. */
810
+ .carousel .card-title { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
811
+ overflow: hidden; }
459
812
  /* A rental card: place and score, name, facts and wishes; the stay's price and button below a line. */
460
813
  .rental-card, .stay-card { display: flex; flex-direction: column; }
461
814
  .rental-card .card-body { flex: 1; }
462
815
  .card-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 24px; }
463
816
  .place { color: var(--chat-accent); font-size: 14px; }
464
- .score-chip { margin-left: auto; padding: 2px 8px; border-radius: calc(var(--chat-radius) * 0.4);
465
- background: var(--chat-secondary); color: var(--chat-on-secondary); font-size: 12px; font-weight: 700; }
817
+ .card-meta .photo-rating { margin-left: auto; font-size: 14px; font-weight: 600; }
818
+ .card-meta .photo-rating .star { color: var(--chat-rating); }
466
819
  .specs { margin: 0; color: var(--chat-muted); font-size: 14px; }
467
820
  .card-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px;
468
821
  margin: 0 18px; padding: 12px 0 16px; border-top: 1px solid var(--chat-border); }
469
822
  .foot-price { display: flex; flex-direction: column; min-width: 0; }
470
823
  .foot-price .price { font-size: 18px; line-height: 1.3; }
471
- .foot-meta { color: var(--chat-muted); font-size: 12px; }
824
+ .foot-meta { color: var(--chat-muted); font-size: 14px; }
825
+ .foot-meta .period { white-space: nowrap; }
472
826
  .button.compact { margin: 0 0 0 auto; padding: 9px 20px; font-size: 14px; white-space: nowrap; }
473
827
  .muted { color: var(--chat-muted); font-size: 13px; }
474
- .price { font-weight: 700; }
475
- .button { display: inline-block; margin-top: 6px; text-align: center; text-decoration: none;
476
- padding: 10px 16px; border-radius: var(--chat-round); background: var(--chat-primary); color: var(--chat-on-primary);
477
- border: 0; cursor: pointer; font: inherit; font-weight: 600; }
828
+ .price { font-weight: 700; font-variant-numeric: tabular-nums; }
829
+ /* "Gesamtpreis für 7 Nächte": what a total covers, over it. */
830
+ .price-label { color: var(--chat-muted); font-size: 13px; }
831
+ .price-line { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 8px; }
832
+ /* Relative: the link's screen-reader name (an absolute .sr-only) stays inside a scrolled row, which
833
+ clips it, instead of sticking out of the row and widening the panel. */
834
+ .button { position: relative; display: inline-flex; align-items: center; justify-content: center; margin-top: 6px;
835
+ text-align: center; text-decoration: none; padding: 10px 16px; border-radius: var(--chat-round);
836
+ background: var(--chat-primary); color: var(--chat-on-primary); border: 0; cursor: pointer; font: inherit;
837
+ font-weight: 600; }
478
838
  .link { color: var(--chat-primary); }
479
- /* A link out of the chat while a reply still streams (ReplyLink). */
839
+ /* A button on its own in a reply: as wide as its label needs, not the panel's 650px. */
840
+ .ui > .button, .ui > .link { align-self: flex-start; min-width: 14rem; }
841
+ /* The way to the contact page in a reply (ContactLink) and in the fallback: a button, outlined. */
842
+ .ui > .link, .fallback-links > .link { display: inline-flex; align-items: center; justify-content: center; padding: 10px 16px;
843
+ border: 1px solid var(--chat-primary); border-radius: var(--chat-round); text-decoration: none;
844
+ font-weight: 600; }
845
+ /* A link out of the chat while a reply still streams (ReplyLink). A button keeps its full strength: it
846
+ waits a moment, it is not unavailable. */
480
847
  [aria-disabled="true"] { opacity: 0.5; cursor: progress; }
848
+ .button[aria-disabled="true"] { opacity: 1; }
849
+
850
+ /* Cards rise above the message area: on a dark surface a step lighter, with a 1 px edge where the shadow
851
+ cannot show; a light surface stays as it is (the lift only applies below the middle lightness). */
852
+ @supports (color: oklch(from red calc(l + 0.1) c h)) {
853
+ .card, .compare-col, .calendar { background: oklch(from var(--chat-surface) calc(l + max(0, 0.5 - l) * 0.45) c h); }
854
+ .card, .compare-col, .calendar { --chat-border: var(--chat-card-border); }
855
+ .card, .compare-col { box-shadow: var(--chat-card-shadow),
856
+ 0 0 0 1px oklch(from var(--chat-surface) calc(l + max(0, 0.5 - l) * 0.75) c h); }
857
+ }
858
+ /* A carousel card, about 290px wide on a phone and on a wide panel alike: the total's label across the
859
+ foot, the total and the button side by side under it, so the label does not wrap beside the button
860
+ and push the price under the input. */
861
+ .carousel .card-foot { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;
862
+ gap: 0 8px; }
863
+ .carousel .card-foot > .foot-price { display: contents; }
864
+ .carousel .card-foot .price-label { grid-column: 1 / -1; }
865
+ .carousel .card-foot > .button { grid-column: 2; grid-row: 2 / span 3; }
866
+ @container (min-width: 300px) {
867
+ /* With a deal badge the total and the badge take the line, and the button sits beside dates and party. */
868
+ .carousel .card-foot:has(.deal) .price-line { grid-column: 1 / -1; }
869
+ .carousel .card-foot:has(.deal) > .button { grid-row: 3 / span 2; }
870
+ /* An unbookable stay: the button beside its one line. */
871
+ .carousel .card-foot:not(:has(.price-label)) > .button { grid-row: 1; }
872
+ }
873
+ /* A small phone's card has no room beside the total: the button goes under it, full width. */
874
+ @container (max-width: 299px) {
875
+ .carousel .card-foot { grid-template-columns: minmax(0, 1fr); }
876
+ .carousel .card-foot > .button { grid-column: 1; grid-row: auto; margin-top: 8px; }
877
+ }
878
+
879
+ /* A card's photos large, in a sheet over the panel's content (sheet.tsx). */
880
+ .sheet-host { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
881
+ .sheet-host:empty { display: none; }
882
+ .sheet-scrim { position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: flex-end;
883
+ pointer-events: auto; background: rgb(15 23 42 / 0.55); }
884
+ .sheet { display: flex; flex-direction: column; height: calc(100% - 40px);
885
+ border-radius: var(--chat-radius) var(--chat-radius) 0 0; background: var(--chat-surface);
886
+ box-shadow: 0 -8px 32px rgb(15 23 42 / 0.25); overflow: hidden; }
887
+ .sheet:focus { outline: none; }
888
+ .sheet-head { display: flex; align-items: center; gap: 10px; padding: 10px 16px 10px 20px; }
889
+ .sheet-title { margin-right: auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
890
+ font-weight: 600; }
891
+ .sheet-count { color: var(--chat-muted); font-size: 13px; font-variant-numeric: tabular-nums; }
892
+ .sheet-photo { position: relative; flex: 1; min-height: 0; background: var(--chat-surface-soft); }
893
+ .sheet-photos { display: flex; height: 100%; overflow-x: auto; overscroll-behavior: contain;
894
+ scroll-snap-type: x mandatory; scrollbar-width: none; }
895
+ .sheet-photos::-webkit-scrollbar { display: none; }
896
+ /* Uncropped, at the photo's own ratio. */
897
+ .sheet-photos img { flex: 0 0 100%; width: 100%; height: 100%; object-fit: contain; scroll-snap-align: start; }
898
+ .sheet .sheet-photo > .gallery-arrow { opacity: 1; pointer-events: auto; }
899
+ .sheet .sheet-photo > .gallery-arrow:disabled { opacity: 0.4; }
900
+ .sheet-caption { margin: 0; padding: 10px 20px 0; font-size: 14px; }
901
+ .sheet-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px;
902
+ padding: 12px 20px calc(18px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--chat-border); }
481
903
 
482
904
  /* A card without photos: in a carousel a quiet block holds the photo's place, so the row lines up;
483
905
  on its own the card simply starts with its text. */
484
- .no-photo { display: none; aspect-ratio: 5 / 3; place-items: center; background: var(--chat-surface-soft);
906
+ .no-photo { display: none; aspect-ratio: 4 / 3; place-items: center; background: var(--chat-surface-soft);
485
907
  color: var(--chat-muted); }
486
908
  .carousel .no-photo { display: grid; }
909
+ /* A card on its own in a wide panel: a flatter photo, so the card's name and price fit the list with it. */
910
+ @container (min-width: 520px) {
911
+ .ui > .rental-card :is(.gallery > img, .photo-open) { aspect-ratio: 2 / 1; max-height: 280px; }
912
+ /* In a carousel too: the cards' prices and the reply under them come up above the dock sooner. */
913
+ .carousel :is(.gallery > img, .photo-open), .carousel .no-photo { aspect-ratio: 2 / 1; }
914
+ }
487
915
  /* Wishes a found stay matches, ticked under the name. */
488
916
  .wishes { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
489
917
  .wishes li { display: inline-flex; align-items: center; gap: 6px; padding: 4px 12px 4px 10px;
490
918
  border: 1px solid var(--chat-border); border-radius: var(--chat-round); font-size: 13px; font-weight: 600; }
491
919
  .wishes svg { color: var(--chat-accent); }
492
920
 
921
+ /* A quote: its dates and party, the labelled total beside the button, the discount and the
922
+ cancellation terms, then the price lines folded away under "Preisdetails". */
923
+ .stay-card .card-body { gap: 12px; }
924
+ .stay-card .card-body:last-child { padding-bottom: 16px; }
925
+ .quote-head { display: flex; align-items: center; gap: 12px; }
926
+ .quote-head > div { display: flex; flex-direction: column; min-width: 0; }
927
+ .thumb { flex: none; width: 52px; height: 52px; border-radius: calc(var(--chat-radius) * 0.75);
928
+ object-fit: cover; }
929
+ .quote-when { margin: 0; color: var(--chat-muted); font-size: 14px; }
930
+ .quote-when .period { white-space: nowrap; }
931
+ /* Wraps on a small phone: the button goes under a total that would not fit beside it. */
932
+ .price-row { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 12px;
933
+ padding-top: 12px; border-top: 1px solid var(--chat-border); }
934
+ .price-row .price { font-size: 24px; line-height: 1.2; white-space: nowrap; }
935
+ .quote-facts { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
936
+ .deal, .cancel-line { margin: 0; display: flex; align-items: center; gap: 8px; font-size: 14px; }
937
+ .deal svg, .cancel-line svg, .ho-lines svg { flex: none; color: var(--chat-accent); }
938
+ .deal strong, .cancel-line strong { font-weight: 600; }
939
+ /* The date wraps as one: "7." at a line's end and "Jan. 2027" on the next read as two. */
940
+ .cancel-line strong { white-space: nowrap; }
941
+ /* The discount in the primary's tint: its words say it, not the colour. */
942
+ .deal { padding: 4px 12px 4px 10px; border: 1px solid var(--chat-tint-edge); border-radius: var(--chat-round);
943
+ background: var(--chat-secondary); color: var(--chat-on-secondary); }
944
+ /* A badge stays one line: two would make a blob beside a card's button. */
945
+ .deal.compact { display: inline-flex; padding: 1px 8px; font-size: 12px; font-weight: 600; white-space: nowrap; }
946
+ /* The quote's line: a pill on one line, a rounded box on two. */
947
+ .deal:not(.compact) { border-radius: 16px; }
948
+ .deal .saving { white-space: nowrap; }
949
+ .price-details { padding-top: 10px; border-top: 1px solid var(--chat-border); font-size: 14px; }
950
+ .price-details summary { display: flex; align-items: center; justify-content: space-between; min-height: 32px;
951
+ list-style: none; color: var(--chat-primary); font-weight: 600; cursor: pointer; }
952
+ .price-details summary::-webkit-details-marker { display: none; }
953
+ .price-details summary svg { transform: rotate(90deg); }
954
+ .price-details[open] summary svg { transform: rotate(-90deg); }
955
+ .price-details .line { margin-top: 6px; }
956
+ /* The hand-over: what was chosen, the total as the largest figure, the conversation's one full-width
957
+ button, and what the booking page asks next. */
958
+ .ho-dates { display: flex; align-items: center; gap: 10px; padding: 12px 14px;
959
+ border-radius: calc(var(--chat-radius) * 0.75); background: var(--chat-surface-soft); }
960
+ .ho-dates > div { display: flex; flex-direction: column; }
961
+ .ho-dates > div:last-child { text-align: right; }
962
+ .ho-dates strong { font-weight: 600; white-space: nowrap; }
963
+ .ho-nights { flex: 1; display: flex; align-items: center; gap: 6px; color: var(--chat-muted); font-size: 13px;
964
+ white-space: nowrap; }
965
+ .ho-nights::before, .ho-nights::after { content: ""; flex: 1; height: 1px; background: currentColor; opacity: 0.5; }
966
+ /* A phone under 360px has no room for both dates and the nights in a row: the dates side by side, the
967
+ nights under them. */
968
+ @container (max-width: 311px) {
969
+ .ho-dates { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; }
970
+ .ho-nights { grid-column: 1 / -1; grid-row: 2; }
971
+ }
972
+ .ho-lines { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px;
973
+ font-size: 14px; }
974
+ .ho-lines li { display: flex; align-items: center; gap: 6px; }
975
+ .ho-total { display: flex; flex-direction: column; padding-top: 12px; border-top: 1px solid var(--chat-border); }
976
+ .ho-total .price { font-size: 30px; line-height: 1.15; }
977
+ .button.block { display: flex; width: 100%; min-height: 50px; margin: 0; gap: 8px; font-size: 16px; }
978
+ .ho-note { margin: 0; color: var(--chat-muted); font-size: 13px; }
979
+ .ho-steps { list-style: none; counter-reset: step; margin: -4px 0 0; padding: 0; display: flex;
980
+ flex-direction: column; gap: 5px; font-size: 13px; }
981
+ .ho-steps li { counter-increment: step; display: flex; align-items: center; gap: 8px; }
982
+ .ho-steps li::before { content: counter(step); flex: none; display: grid; place-items: center; width: 20px;
983
+ height: 20px; border: 1px solid var(--chat-border); border-radius: 50%; color: var(--chat-muted);
984
+ font-size: 12px; font-weight: 700; }
985
+
493
986
  /* The price calendar: a column per arrival, the bar's height its total. */
494
- .price-columns { list-style: none; margin: 10px 0 0; padding: 0; display: grid; grid-auto-flow: column;
495
- grid-auto-columns: minmax(68px, 1fr); gap: 6px; overflow-x: auto; scrollbar-width: none; }
496
- .price-column { width: 100%; display: flex; flex-direction: column; align-items: center; gap: 4px;
987
+ .price-columns { position: relative; list-style: none; margin: 10px 0 0; padding: 0; display: grid;
988
+ grid-auto-flow: column; grid-auto-columns: minmax(68px, 1fr); gap: 6px; overflow-x: auto;
989
+ scrollbar-width: none; }
990
+ .price-columns::-webkit-scrollbar { display: none; }
991
+ /* More columns beyond an edge fade out there. */
992
+ .price-columns[data-fade="end"] { mask-image: linear-gradient(to left, transparent, #000 32px); }
993
+ .price-columns[data-fade="start"] { mask-image: linear-gradient(to right, transparent, #000 32px); }
994
+ .price-columns[data-fade="both"] { mask-image: linear-gradient(to right, transparent, #000 32px,
995
+ #000 calc(100% - 32px), transparent); }
996
+ /* The arrival's day at the column's foot, level across the row when "nicht buchbar" takes two lines. */
997
+ .price-column { height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 4px;
497
998
  padding: 6px 2px; border: 1px solid transparent; border-radius: 10px; background: transparent;
498
999
  color: inherit; font: inherit; cursor: pointer; }
499
1000
  .price-column:disabled { cursor: default; }
500
- .price-column[data-free]:not(:disabled):hover { border-color: var(--chat-primary); }
1001
+ /* Inside the row that scrolls, so it clips: the ring sits inside the column. */
1002
+ .price-column:focus-visible { outline-offset: -2px; }
501
1003
  .price-column[data-asked] { border-color: var(--chat-border); background: var(--chat-surface-soft); }
502
1004
  .bar-track { display: flex; align-items: flex-end; justify-content: center; width: 100%; height: 72px; }
503
1005
  .bar { width: 60%; border-radius: min(6px, var(--chat-radius)) min(6px, var(--chat-radius)) 0 0;
504
1006
  background: color-mix(in srgb, var(--chat-primary) 30%, var(--chat-surface)); }
505
1007
  .price-column[data-cheapest] .bar, .legend i[data-cheapest] { background: var(--chat-primary); }
506
1008
  .legend i[data-asked] { background: var(--chat-surface-soft); border: 1px solid var(--chat-border); }
507
- .price-text { font-size: 12px; font-weight: 700; white-space: nowrap; }
508
- .price-column:not([data-free]) .price-text { color: var(--chat-muted); font-weight: 500; }
1009
+ .price-column > .muted { margin-top: auto; }
1010
+ .price-text { font-size: 14px; font-weight: 700; white-space: nowrap; }
1011
+ .price-column:not([data-free]) .price-text { color: var(--chat-muted); font-weight: 500; white-space: normal; }
509
1012
 
510
1013
  /* Reviews: the PMS's score and grade, its categories, a few texts. */
511
1014
  .score { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
@@ -531,7 +1034,7 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
531
1034
  .map-gate { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
532
1035
  min-height: 150px; padding: 16px; text-align: center; background: var(--chat-surface-soft); }
533
1036
  .map-gate p { margin: 0; }
534
- .map-attribution { margin: 0; padding: 2px 8px; font-size: 10px; color: var(--chat-muted); text-align: right; }
1037
+ .map-attribution { margin: 0; padding: 2px 8px; font-size: 12px; color: var(--chat-muted); text-align: right; }
535
1038
  .map-attribution a { color: inherit; }
536
1039
  .map-pin { display: block; width: 18px; height: 18px; border-radius: 999px; background: var(--chat-primary);
537
1040
  border: 3px solid var(--chat-surface); box-shadow: 0 2px 6px rgb(0 0 0 / 0.35); }
@@ -542,6 +1045,29 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
542
1045
  .maplibregl-canvas:focus-visible { outline: 2px solid var(--chat-primary); outline-offset: -2px; }
543
1046
  .maplibregl-marker { position: absolute; top: 0; left: 0; will-change: transform; }
544
1047
 
1048
+ /* Hover only where there is a pointer: on a touch screen a tapped chip would keep its hover look. */
1049
+ @media (hover: hover) {
1050
+ .icon-button:hover { border-color: var(--chat-primary); }
1051
+ .close:hover { opacity: 0.9; }
1052
+ .suggestion:hover, .chip:not(:disabled):hover { border-color: var(--chat-primary);
1053
+ background: var(--chat-surface); color: var(--chat-text); }
1054
+ .gallery-wrap:hover .gallery-arrow:not(:disabled) { opacity: 1; }
1055
+ .price-column[data-free]:not(:disabled):hover { border-color: var(--chat-primary); }
1056
+ button.day:hover { box-shadow: inset 0 0 0 1px var(--chat-primary); }
1057
+ .row:not(:disabled):hover { background: var(--chat-surface-soft); }
1058
+ .row[aria-pressed="true"]:hover { background: var(--chat-secondary); }
1059
+ .period-pill:not(:disabled):hover { border-color: var(--chat-primary); }
1060
+ }
1061
+
1062
+ /* Fingers: every control at least 44px each way. */
1063
+ @media (pointer: coarse) {
1064
+ .icon-button, .send, .snap-nav button, .calendar-head button, .gallery-arrow { width: 44px; height: 44px; }
1065
+ .composer { padding: 4px 4px 4px 18px; }
1066
+ .composer textarea { min-height: 44px; padding-block: 10px; }
1067
+ .chip, .suggestion, .button, .ui > .link, .fallback-links > .link, .period-pill { min-height: 44px; }
1068
+ .price-details summary { min-height: 44px; }
1069
+ }
1070
+
545
1071
  [data-theme="soft"] { --chat-radius: calc(var(--chat-base-radius) * 2.4); --chat-shadow: 0 12px 32px rgb(15 23 42 / 0.12);
546
1072
  --chat-card-shadow: 0 4px 18px rgb(15 23 42 / 0.07); }
547
1073
  [data-theme="minimal"] { --chat-radius: calc(var(--chat-base-radius) * 0.6); --chat-shadow: none; --chat-card-shadow: none; }
@@ -554,16 +1080,20 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
554
1080
  @keyframes chat-in { from { opacity: 0; transform: translateY(16px); } }
555
1081
  @keyframes chat-fade { from { opacity: 0; } }
556
1082
  @keyframes chat-pulse { 50% { opacity: 1; transform: translateY(-2px); } }
1083
+ @keyframes chat-ring { from { outline-color: transparent; outline-offset: 8px; } }
1084
+ @media (prefers-reduced-motion: no-preference) and (hover: hover) {
1085
+ .pill:hover { transform: translateY(-2px); }
1086
+ }
557
1087
  @media (prefers-reduced-motion: no-preference) {
558
1088
  .glow::before { animation: chat-spin 3.2s linear infinite; }
559
1089
  .entry { animation: chat-in 400ms cubic-bezier(0.25, 1, 0.5, 1); }
560
1090
  .pill { transition: transform 200ms ease-out; }
561
- .pill:hover { transform: translateY(-2px); }
562
1091
  .panel { animation: chat-in 400ms cubic-bezier(0.25, 1, 0.5, 1); }
563
1092
  .scrim { animation: chat-fade 300ms ease-out; }
564
1093
  .thinking i { animation: chat-pulse 1.1s ease-in-out infinite; }
565
1094
  .thinking i:nth-child(2) { animation-delay: 160ms; }
566
1095
  .thinking i:nth-child(3) { animation-delay: 320ms; }
1096
+ [data-pulse] { animation: chat-ring 500ms ease-out 2 alternate; }
567
1097
  }
568
1098
  /* Without motion the ring stands still, in the primary colour all round. */
569
1099
  @media (prefers-reduced-motion: reduce) {
@@ -572,7 +1102,7 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
572
1102
  `;
573
1103
  //#endregion
574
1104
  //#region src/WebsiteChat.tsx
575
- const Panel = lazy(() => import("./panel-BN_DMA4R.js"));
1105
+ const Panel = lazy(() => import("./panel-B_wg4Wl0.js"));
576
1106
  /** See {@link WebsiteChatComponent}. */
577
1107
  const WebsiteChat = function WebsiteChat(props) {
578
1108
  const { endpoint, locale, theme = "classic", position = "right" } = props;
@@ -644,11 +1174,12 @@ const WebsiteChat = function WebsiteChat(props) {
644
1174
  fallback: null,
645
1175
  children: /* @__PURE__ */ jsx(Panel, {
646
1176
  endpoint,
647
- clientVersion: "1.1.3",
1177
+ clientVersion: "1.3.0",
648
1178
  locale,
649
1179
  copy,
650
1180
  searchPath: props.searchPath,
651
1181
  contactPath: props.contactPath,
1182
+ ...props.privacyPath && { privacyPath: props.privacyPath },
652
1183
  open,
653
1184
  pending,
654
1185
  onSent: sent,