@v-office/website-chat 1.2.0 → 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-BK5Lk0cP.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,6 +12,9 @@ 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
20
  "Sommerwoche für 2 Erwachsene und 2 Kinder",
@@ -19,9 +22,33 @@ const DEFAULT_COPY = {
19
22
  "Ein Wochenende zu zweit mit Sauna",
20
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,8 +66,28 @@ 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",
46
93
  rentalsFound: "Gefundene Unterkünfte",
@@ -54,6 +101,19 @@ const DEFAULT_COPY = {
54
101
  booked: "belegt",
55
102
  closed: "nicht buchbar",
56
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",
57
117
  compareRooms: "Räume",
58
118
  compareHighlights: "Highlights",
59
119
  compareRating: "Bewertung",
@@ -89,6 +149,9 @@ const DEFAULT_COPY = {
89
149
  title: "Travel assistant",
90
150
  newConversation: "New conversation",
91
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",
92
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.",
93
156
  suggestions: [
94
157
  "A summer week for 2 adults and 2 kids",
@@ -96,9 +159,33 @@ const DEFAULT_COPY = {
96
159
  "A weekend for two with a sauna",
97
160
  "Anything free next weekend?"
98
161
  ],
99
- placeholder: "When, who with, what matters to you?",
162
+ placeholder: "Describe your trip …",
100
163
  send: "Send",
101
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
+ },
102
189
  yourMessage: "Your message",
103
190
  consent: "The chat is answered by an AI. Your messages are sent to our AI provider for that.",
104
191
  consentAccept: "Start chat",
@@ -116,8 +203,28 @@ const DEFAULT_COPY = {
116
203
  other: "{n} nights"
117
204
  },
118
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
+ ],
119
224
  notBookable: "Not bookable for these dates",
120
225
  photo: "Photo",
226
+ openPhotos: "View photos of {name}",
227
+ closePhotos: "Close photos",
121
228
  previous: "Previous",
122
229
  next: "Next",
123
230
  rentalsFound: "Rentals found",
@@ -131,6 +238,19 @@ const DEFAULT_COPY = {
131
238
  booked: "taken",
132
239
  closed: "not bookable",
133
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",
134
254
  compareRooms: "Rooms",
135
255
  compareHighlights: "Highlights",
136
256
  compareRating: "Rating",
@@ -267,10 +387,17 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
267
387
  --chat-round: calc(var(--chat-base-radius) * 100);
268
388
  --chat-shadow: 0 18px 48px rgb(15 23 42 / 0.18);
269
389
  --chat-card-shadow: 0 1px 3px rgb(15 23 42 / 0.08), 0 8px 24px rgb(15 23 42 / 0.06);
390
+ /* The stars' colour: dark amber on a light surface, light amber on a dark one, AA on both. */
391
+ --chat-rating: #8a5a00;
270
392
  /* Not set here, so a site's value reaches the widget, on wide screens (from 1024px): \`--chat-top\`
271
393
  keeps a sticky header uncovered, \`--chat-width\` widens the panel. A phone's sheet covers the
272
394
  header: the panel is modal, so the header could not be tapped and would only cost height. */
273
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); }
400
+ }
274
401
  * { box-sizing: border-box; }
275
402
  .chat { color: var(--chat-text); font-size: 15px; line-height: 1.45; }
276
403
  /* One focus ring for every control, in the site's primary; a click or a tap shows none. Without
@@ -332,10 +459,29 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
332
459
  width: min(var(--chat-width, 700px), calc(100vw - 40px)); border-radius: var(--chat-radius); }
333
460
  [data-position="left"] .panel { right: auto; left: 20px; }
334
461
  }
335
- .header { display: flex; align-items: center; gap: 8px; padding: 12px 24px 4px; }
462
+ .header { position: relative; display: flex; align-items: center; gap: 8px; padding: 12px 24px 4px; }
336
463
  @media (min-width: 1024px) { .header { padding-top: 20px; } }
337
- .title { margin: 0 auto 0 0; font-size: 13px; font-weight: 600; letter-spacing: 0.16em;
338
- text-transform: uppercase; color: var(--chat-accent); }
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); }
339
485
  .icon-button { flex: none; display: grid; place-items: center; width: 40px; height: 40px; padding: 0;
340
486
  border: 1px solid var(--chat-border); border-radius: var(--chat-round); background: var(--chat-surface);
341
487
  color: var(--chat-text); cursor: pointer; }
@@ -359,25 +505,30 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
359
505
  .thinking { display: flex; align-items: center; gap: 5px; padding: 4px 2px; }
360
506
  .thinking-label { margin-left: 6px; color: var(--chat-muted); font-size: 14px; }
361
507
  .thinking i { width: 7px; height: 7px; border-radius: 999px; background: var(--chat-accent); opacity: 0.5; }
362
- .dock { position: relative; padding: 10px 20px calc(16px + env(safe-area-inset-bottom, 0px));
363
- 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; }
364
510
  .dock::before { content: ""; position: absolute; left: 0; right: 0; top: -28px; height: 28px;
365
511
  pointer-events: none; background: linear-gradient(to top, var(--chat-surface-soft), transparent); }
366
512
  /* Suggestions wrap, on a phone too: in a row that scrolls, one cut chip hides the others. */
367
- .suggestions { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 10px; }
513
+ .suggestions { display: flex; flex-wrap: wrap; gap: 8px; margin: 2px 8px 10px; }
368
514
  /* Suggestions and quick replies: filled pills; a suggestion has an icon for its topic in the accent
369
515
  colour (a quick reply is the model's words, an icon guessed from them can be wrong). */
370
516
  .suggestion, .chip { flex: none; max-width: 100%; display: inline-flex; align-items: center; gap: 8px;
371
517
  border: 1px solid var(--chat-tint-edge); border-radius: var(--chat-round); background: var(--chat-secondary);
372
518
  color: var(--chat-on-secondary); padding: 8px 16px 8px 12px; font: inherit; font-size: 14px;
373
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. */
374
522
  .chip { padding-left: 16px; }
523
+ .chip[data-chosen] { border-color: var(--chat-border); background: transparent; color: var(--chat-muted);
524
+ cursor: default; }
375
525
  .suggestion svg { flex: none; color: var(--chat-accent); }
376
526
  .composer { display: flex; align-items: flex-end; gap: 8px; padding: 6px 6px 6px 18px;
377
527
  border: 1px solid var(--chat-border); border-radius: var(--chat-round); background: var(--chat-surface);
378
528
  box-shadow: 0 4px 16px rgb(15 23 42 / 0.08); }
379
- /* The field's ring, not the send button's too (that one has its own). */
380
- .composer:has(textarea:focus-visible) { outline: 2px solid var(--chat-primary); outline-offset: 2px; }
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; }
381
532
  /* 16px keeps iOS from zooming into the field. */
382
533
  .composer textarea { flex: 1; min-width: 0; min-height: 36px; max-height: 128px; resize: none; border: 0;
383
534
  outline: none; background: transparent; padding: 6px 0; font: inherit; font-size: 16px;
@@ -387,6 +538,36 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
387
538
  .send { flex: none; display: grid; place-items: center; width: 36px; height: 36px; padding: 0; border: 0;
388
539
  border-radius: var(--chat-round); background: var(--chat-primary); color: var(--chat-on-primary); cursor: pointer; }
389
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; }
390
571
  .consent { flex: 1; overflow-y: auto; padding: 24px; display: flex; flex-direction: column; gap: 14px;
391
572
  background: var(--chat-surface-soft); }
392
573
  .consent p { margin: 0; }
@@ -410,8 +591,26 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
410
591
  .gallery-wrap, .carousel-wrap { position: relative; }
411
592
  .gallery, .carousel { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; }
412
593
  .gallery::-webkit-scrollbar, .carousel::-webkit-scrollbar { display: none; }
413
- .gallery img { flex: 0 0 100%; scroll-snap-align: start; display: block; width: 100%; aspect-ratio: 5 / 3;
414
- 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; }
415
614
  /* Dots say which photo shows; arrows come up under a pointer and on keyboard focus (a finger swipes:
416
615
  on a touch screen they take no tap, so a tap on a photo's edge does not turn it unseen). */
417
616
  .dots { position: absolute; left: 50%; bottom: 10px; translate: -50% 0; display: flex; gap: 4px; }
@@ -434,25 +633,86 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
434
633
  @container (min-width: 520px) {
435
634
  .carousel > .card { flex-basis: calc((100% - 24px) / 2.15); }
436
635
  .carousel:has(> .card:nth-child(2):last-child) > .card { flex-basis: calc((100% - 12px) / 2); }
437
- .carousel-wrap:has(> .carousel > .card:nth-child(2):last-child) > .snap-nav { display: none; }
636
+ .carousel-wrap:has(> .carousel > .card:nth-child(2):last-child) > .snap-nav > :not(.snap-caption) {
637
+ display: none; }
438
638
  /* The count follows the first card in view, and two show at once: it would skip numbers. */
439
- .carousel-wrap > .snap-nav > span { display: none; }
639
+ .carousel-wrap > .snap-nav > .snap-count { display: none; }
440
640
  }
441
- /* The row's controls, grouped at its end: "‹ 2 / 8 ›". */
641
+ /* The row's controls, grouped at its end: "‹ 2 / 8 ›" under a pointer, dots under a finger; a caption
642
+ at its start. */
442
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); }
443
657
  .snap-nav button, .calendar-head button { flex: none; display: grid; place-items: center; width: 36px;
444
658
  height: 36px; padding: 0; border: 1px solid var(--chat-border); background: var(--chat-surface);
445
659
  color: var(--chat-text); border-radius: var(--chat-round); cursor: pointer; }
446
660
  .snap-nav button:disabled, .calendar-head button:disabled { opacity: 0.5; cursor: default; }
447
- .rating { color: #b7791f; font-weight: 700; }
448
- .breakdown .line { display: flex; justify-content: space-between; gap: 12px; }
449
- .breakdown .total { border-top: 1px solid var(--chat-border); padding-top: 6px; margin-top: 2px;
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;
450
665
  font-weight: 700; }
451
- .alt-chips, .quick-replies { display: flex; flex-wrap: wrap; gap: 6px; }
452
- .alt-caption { margin: 0 0 4px; font-size: 13px; color: var(--chat-muted); }
453
- .chip-link { display: inline-flex; align-items: center; border: 1px solid var(--chat-primary);
454
- color: var(--chat-primary); border-radius: var(--chat-round); padding: 5px 10px; text-decoration: none;
455
- font-size: 14px; }
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; }
456
716
  .calendar { border: 1px solid var(--chat-border); border-radius: var(--chat-radius); padding: 12px;
457
717
  background: var(--chat-surface); }
458
718
  .calendar-head { display: flex; align-items: center; justify-content: space-between; gap: 8px;
@@ -463,19 +723,55 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
463
723
  .day { padding: 4px 0; border-radius: min(6px, var(--chat-radius)); }
464
724
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%);
465
725
  white-space: nowrap; }
466
- /* Free, booked and the stay must tell apart in dark too: a stronger tint for free, an edge for booked
467
- (inset, so a booked day is no taller than the others). */
468
- .day[data-status="free"], .legend i[data-status="free"] {
469
- background: color-mix(in srgb, var(--chat-primary) 26%, var(--chat-surface)); color: var(--chat-text); }
470
- .day[data-status="booked"], .legend i[data-status="booked"] { background: var(--chat-surface-soft);
471
- box-shadow: inset 0 0 0 1px var(--chat-border); color: var(--chat-muted);
472
- 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); }
473
735
  .day[data-status="closed"] { color: var(--chat-muted); opacity: 0.6; }
474
- .day[data-stay], .legend i[data-stay] { background: var(--chat-primary); color: var(--chat-on-primary);
475
- text-decoration: none; }
476
- .legend { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: 13px; }
477
- .legend i { display: inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 4px;
478
- vertical-align: -1px; border: 1px solid var(--chat-border); }
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; }
479
775
  .compare { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 8px; }
480
776
  .compare-col { border-radius: var(--chat-radius); padding: 10px; background: var(--chat-surface);
481
777
  box-shadow: var(--chat-card-shadow); display: flex; flex-direction: column; gap: 6px; font-size: 14px;
@@ -494,21 +790,23 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
494
790
  scrollbar-width: none; padding: 2px 2px 16px; margin-bottom: -10px; }
495
791
  .compare::-webkit-scrollbar { display: none; }
496
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; }
497
796
  }
498
797
  /* Side by side, the rows line up across the columns (a long name pushes every row of its column down
499
798
  otherwise): the name, the list's eight cells and the button share the comparison's ten rows. */
500
799
  @supports (grid-template-rows: subgrid) {
501
- @container (min-width: 520px) {
502
- .compare { grid-template-rows: repeat(10, auto); row-gap: 0; }
503
- .compare-col { display: grid; grid-row: span 10; grid-template-rows: subgrid; }
504
- .compare-col dl { display: grid; grid-row: span 8; grid-template-rows: subgrid; }
505
- .compare-col .button { grid-row: -2; }
506
- }
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; }
507
804
  }
508
805
  .card-body { padding: 16px 18px 12px; display: flex; flex-direction: column; gap: 6px; }
509
806
  .card-title { margin: 0; font-size: 18px; font-weight: 500; line-height: 1.3; }
510
807
  /* One long name (v9 names often run to a dozen words) would make every card of a row taller: in a
511
- carousel the name stops at three lines. The single card, the quote and Compare show it whole. */
808
+ carousel the name stops at three lines, as in a narrow Compare; the single card and the quote show it
809
+ whole. */
512
810
  .carousel .card-title { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
513
811
  overflow: hidden; }
514
812
  /* A rental card: place and score, name, facts and wishes; the stay's price and button below a line. */
@@ -516,8 +814,8 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
516
814
  .rental-card .card-body { flex: 1; }
517
815
  .card-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 24px; }
518
816
  .place { color: var(--chat-accent); font-size: 14px; }
519
- .score-chip { margin-left: auto; padding: 2px 8px; border-radius: calc(var(--chat-radius) * 0.4);
520
- background: var(--chat-secondary); color: var(--chat-on-secondary); font-size: 13px; 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); }
521
819
  .specs { margin: 0; color: var(--chat-muted); font-size: 14px; }
522
820
  .card-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px;
523
821
  margin: 0 18px; padding: 12px 0 16px; border-top: 1px solid var(--chat-border); }
@@ -527,7 +825,10 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
527
825
  .foot-meta .period { white-space: nowrap; }
528
826
  .button.compact { margin: 0 0 0 auto; padding: 9px 20px; font-size: 14px; white-space: nowrap; }
529
827
  .muted { color: var(--chat-muted); font-size: 13px; }
530
- .price { font-weight: 700; }
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; }
531
832
  /* Relative: the link's screen-reader name (an absolute .sr-only) stays inside a scrolled row, which
532
833
  clips it, instead of sticking out of the row and widening the panel. */
533
834
  .button { position: relative; display: inline-flex; align-items: center; justify-content: center; margin-top: 6px;
@@ -541,19 +842,75 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
541
842
  .ui > .link, .fallback-links > .link { display: inline-flex; align-items: center; justify-content: center; padding: 10px 16px;
542
843
  border: 1px solid var(--chat-primary); border-radius: var(--chat-round); text-decoration: none;
543
844
  font-weight: 600; }
544
- /* A link out of the chat while a reply still streams (ReplyLink). */
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. */
545
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); }
546
903
 
547
904
  /* A card without photos: in a carousel a quiet block holds the photo's place, so the row lines up;
548
905
  on its own the card simply starts with its text. */
549
- .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);
550
907
  color: var(--chat-muted); }
551
908
  .carousel .no-photo { display: grid; }
552
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. */
553
910
  @container (min-width: 520px) {
554
- .ui > .rental-card .gallery img { aspect-ratio: 2 / 1; max-height: 280px; }
911
+ .ui > .rental-card :is(.gallery > img, .photo-open) { aspect-ratio: 2 / 1; max-height: 280px; }
555
912
  /* In a carousel too: the cards' prices and the reply under them come up above the dock sooner. */
556
- .carousel .gallery img, .carousel .no-photo { aspect-ratio: 2 / 1; }
913
+ .carousel :is(.gallery > img, .photo-open), .carousel .no-photo { aspect-ratio: 2 / 1; }
557
914
  }
558
915
  /* Wishes a found stay matches, ticked under the name. */
559
916
  .wishes { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
@@ -561,6 +918,71 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
561
918
  border: 1px solid var(--chat-border); border-radius: var(--chat-round); font-size: 13px; font-weight: 600; }
562
919
  .wishes svg { color: var(--chat-accent); }
563
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
+
564
986
  /* The price calendar: a column per arrival, the bar's height its total. */
565
987
  .price-columns { position: relative; list-style: none; margin: 10px 0 0; padding: 0; display: grid;
566
988
  grid-auto-flow: column; grid-auto-columns: minmax(68px, 1fr); gap: 6px; overflow-x: auto;
@@ -631,6 +1053,10 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
631
1053
  background: var(--chat-surface); color: var(--chat-text); }
632
1054
  .gallery-wrap:hover .gallery-arrow:not(:disabled) { opacity: 1; }
633
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); }
634
1060
  }
635
1061
 
636
1062
  /* Fingers: every control at least 44px each way. */
@@ -638,7 +1064,8 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
638
1064
  .icon-button, .send, .snap-nav button, .calendar-head button, .gallery-arrow { width: 44px; height: 44px; }
639
1065
  .composer { padding: 4px 4px 4px 18px; }
640
1066
  .composer textarea { min-height: 44px; padding-block: 10px; }
641
- .chip, .suggestion, .button, .chip-link, .ui > .link, .fallback-links > .link { min-height: 44px; }
1067
+ .chip, .suggestion, .button, .ui > .link, .fallback-links > .link, .period-pill { min-height: 44px; }
1068
+ .price-details summary { min-height: 44px; }
642
1069
  }
643
1070
 
644
1071
  [data-theme="soft"] { --chat-radius: calc(var(--chat-base-radius) * 2.4); --chat-shadow: 0 12px 32px rgb(15 23 42 / 0.12);
@@ -653,6 +1080,7 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
653
1080
  @keyframes chat-in { from { opacity: 0; transform: translateY(16px); } }
654
1081
  @keyframes chat-fade { from { opacity: 0; } }
655
1082
  @keyframes chat-pulse { 50% { opacity: 1; transform: translateY(-2px); } }
1083
+ @keyframes chat-ring { from { outline-color: transparent; outline-offset: 8px; } }
656
1084
  @media (prefers-reduced-motion: no-preference) and (hover: hover) {
657
1085
  .pill:hover { transform: translateY(-2px); }
658
1086
  }
@@ -665,6 +1093,7 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
665
1093
  .thinking i { animation: chat-pulse 1.1s ease-in-out infinite; }
666
1094
  .thinking i:nth-child(2) { animation-delay: 160ms; }
667
1095
  .thinking i:nth-child(3) { animation-delay: 320ms; }
1096
+ [data-pulse] { animation: chat-ring 500ms ease-out 2 alternate; }
668
1097
  }
669
1098
  /* Without motion the ring stands still, in the primary colour all round. */
670
1099
  @media (prefers-reduced-motion: reduce) {
@@ -673,7 +1102,7 @@ ${SITE_COLORS.map((t) => ` ${t.chat}: var(${t.site}, ${t.own});`).join("\n")}
673
1102
  `;
674
1103
  //#endregion
675
1104
  //#region src/WebsiteChat.tsx
676
- const Panel = lazy(() => import("./panel-CfS3SuRX.js"));
1105
+ const Panel = lazy(() => import("./panel-B_wg4Wl0.js"));
677
1106
  /** See {@link WebsiteChatComponent}. */
678
1107
  const WebsiteChat = function WebsiteChat(props) {
679
1108
  const { endpoint, locale, theme = "classic", position = "right" } = props;
@@ -745,11 +1174,12 @@ const WebsiteChat = function WebsiteChat(props) {
745
1174
  fallback: null,
746
1175
  children: /* @__PURE__ */ jsx(Panel, {
747
1176
  endpoint,
748
- clientVersion: "1.2.0",
1177
+ clientVersion: "1.3.0",
749
1178
  locale,
750
1179
  copy,
751
1180
  searchPath: props.searchPath,
752
1181
  contactPath: props.contactPath,
1182
+ ...props.privacyPath && { privacyPath: props.privacyPath },
753
1183
  open,
754
1184
  pending,
755
1185
  onSent: sent,