@ticketboothapp/booking 1.2.24 → 1.2.25-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/package.json +29 -2
  2. package/src/assets/icons/minus.svg +7 -0
  3. package/src/assets/icons/partner-logos/getyourguide.svg +8 -0
  4. package/src/assets/icons/plus.svg +3 -0
  5. package/src/colours.css +23 -0
  6. package/src/components/BookingDetails.module.css +1591 -0
  7. package/src/components/BookingDetails.tsx +2264 -0
  8. package/src/components/BookingWidget.tsx +302 -0
  9. package/src/components/ManageBookingView.tsx +437 -0
  10. package/src/components/PhoneInputWithCountry.module.css +131 -0
  11. package/src/components/PhoneInputWithCountry.tsx +44 -0
  12. package/src/components/PickupLocationDialog.module.css +360 -0
  13. package/src/components/PickupLocationDialog.tsx +357 -0
  14. package/src/components/PostBookingDependentAddOnUpsell.module.css +174 -0
  15. package/src/components/PostBookingDependentAddOnUpsell.tsx +407 -0
  16. package/src/components/booking/AddOnsSection.module.css +10 -0
  17. package/src/components/booking/AddOnsSection.tsx +184 -0
  18. package/src/components/booking/AdminPaymentChoiceModal.tsx +98 -0
  19. package/src/components/booking/BookingDialog.module.css +643 -0
  20. package/src/components/booking/BookingDialog.tsx +356 -0
  21. package/src/components/booking/BookingFlow.tsx +4385 -0
  22. package/src/components/booking/BookingFlowCollage.module.css +148 -0
  23. package/src/components/booking/BookingFlowCollage.tsx +184 -0
  24. package/src/components/booking/BookingFlowPlaceholder.module.css +27 -0
  25. package/src/components/booking/BookingFlowPlaceholder.tsx +25 -0
  26. package/src/components/booking/BookingFlowPreview.tsx +51 -0
  27. package/src/components/booking/BookingProductGrid.module.css +359 -0
  28. package/src/components/booking/BookingProductGrid.tsx +497 -0
  29. package/src/components/booking/Calendar.module.css +616 -0
  30. package/src/components/booking/Calendar.tsx +1123 -0
  31. package/src/components/booking/CancellationPolicySelector.module.css +124 -0
  32. package/src/components/booking/CancellationPolicySelector.tsx +142 -0
  33. package/src/components/booking/ChangeBookingDialog.tsx +562 -0
  34. package/src/components/booking/CheckoutForm.module.css +244 -0
  35. package/src/components/booking/CheckoutForm.tsx +364 -0
  36. package/src/components/booking/CheckoutModal.tsx +451 -0
  37. package/src/components/booking/CurrencySwitcher.tsx +81 -0
  38. package/src/components/booking/DapFlowCollage.tsx +88 -0
  39. package/src/components/booking/DapTourDescription.tsx +35 -0
  40. package/src/components/booking/DependentAddOnBookingDialog.tsx +1350 -0
  41. package/src/components/booking/DependentAddOnPaymentForm.tsx +124 -0
  42. package/src/components/booking/ErrorBoundary.tsx +63 -0
  43. package/src/components/booking/InfoTooltip.tsx +108 -0
  44. package/src/components/booking/ItineraryBox.module.css +258 -0
  45. package/src/components/booking/ItineraryBox.tsx +550 -0
  46. package/src/components/booking/ItineraryBuilder.tsx +82 -0
  47. package/src/components/booking/ItineraryPlaceholder.module.css +45 -0
  48. package/src/components/booking/ItineraryPlaceholder.tsx +26 -0
  49. package/src/components/booking/MealDrinkAddOnSelector.tsx +338 -0
  50. package/src/components/booking/PickupLocationSelector.module.css +124 -0
  51. package/src/components/booking/PickupLocationSelector.tsx +1566 -0
  52. package/src/components/booking/PickupTimeSelector.module.css +134 -0
  53. package/src/components/booking/PickupTimeSelector.tsx +112 -0
  54. package/src/components/booking/PriceBreakdown.tsx +154 -0
  55. package/src/components/booking/PriceSummary.tsx +234 -0
  56. package/src/components/booking/PrivateShuttleBookingFlow.module.css +357 -0
  57. package/src/components/booking/PrivateShuttleBookingFlow.tsx +2662 -0
  58. package/src/components/booking/PromoCodeInput.module.css +166 -0
  59. package/src/components/booking/PromoCodeInput.tsx +99 -0
  60. package/src/components/booking/ReturnTimeSelector.module.css +173 -0
  61. package/src/components/booking/ReturnTimeSelector.tsx +145 -0
  62. package/src/components/booking/TermsAcceptance.tsx +111 -0
  63. package/src/components/booking/TicketSelector.module.css +164 -0
  64. package/src/components/booking/TicketSelector.tsx +199 -0
  65. package/src/components/booking/TourDescription.module.css +304 -0
  66. package/src/components/booking/TourDescription.tsx +273 -0
  67. package/src/components/booking/booking-flow-ui.ts +38 -0
  68. package/src/components/booking/booking-flow.css +944 -0
  69. package/src/components/button.css +245 -0
  70. package/src/components/button.tsx +152 -0
  71. package/src/components/colorable-svg.tsx +29 -0
  72. package/src/components/image.css +29 -0
  73. package/src/components/image.tsx +113 -0
  74. package/src/components/partner/PartnerBookingPage.module.css +130 -0
  75. package/src/components/partner/PartnerBookingPage.tsx +390 -0
  76. package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +45 -0
  77. package/src/components/product-tag.module.css +30 -0
  78. package/src/components/product-tag.tsx +34 -0
  79. package/src/components/product-theme-pages/image-modal.tsx +248 -0
  80. package/src/components/product-theme-pages/photo-gallery.module.css +200 -0
  81. package/src/components/terms/TermsContent.tsx +178 -0
  82. package/src/components/value-pill.module.css +59 -0
  83. package/src/components/value-pill.tsx +46 -0
  84. package/src/constants/images.ts +556 -0
  85. package/src/constants/pill-values.ts +210 -0
  86. package/src/constants/products.ts +155 -0
  87. package/src/contexts/AvailabilitiesCacheContext.tsx +125 -0
  88. package/src/contexts/BookingAppContext.tsx +134 -0
  89. package/src/contexts/CompanyContext.tsx +70 -0
  90. package/src/data/dap-descriptions/session-couples-families-friends.en.json +61 -0
  91. package/src/data/dap-descriptions/session-elopements.en.json +60 -0
  92. package/src/data/dap-descriptions/session-proposals.en.json +60 -0
  93. package/src/data/product-descriptions/afternoon-delight.en.json +35 -0
  94. package/src/data/product-descriptions/emerald-lake-escape.en.json +68 -0
  95. package/src/data/product-descriptions/lake-louise-adventure.en.json +74 -0
  96. package/src/data/product-descriptions/moraine-lake-adventure.en.json +78 -0
  97. package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +65 -0
  98. package/src/data/product-descriptions/moraine-lake-sunrise.en.json +64 -0
  99. package/src/data/product-descriptions/private-tour.en.json +80 -0
  100. package/src/data/product-descriptions/two-lakes-combo.en.json +65 -0
  101. package/src/data/products-config.json +101 -0
  102. package/src/hooks/useBookingSourceMetadataFromLocation.ts +21 -0
  103. package/src/hooks/useIsBookingLaunchLive.ts +49 -0
  104. package/src/index.ts +79 -0
  105. package/src/lib/analytics.ts +197 -0
  106. package/src/lib/booking/booking-source.ts +51 -0
  107. package/src/lib/booking/checkout-breakdown.ts +69 -0
  108. package/src/lib/booking/correlation-id.ts +46 -0
  109. package/src/lib/booking/i18n/config.ts +21 -0
  110. package/src/lib/booking/i18n/index.tsx +144 -0
  111. package/src/lib/booking/i18n/messages/en.json +236 -0
  112. package/src/lib/booking/i18n/messages/fr.json +236 -0
  113. package/src/lib/booking/itinerary-display.ts +36 -0
  114. package/src/lib/booking/itinerary-labels.ts +70 -0
  115. package/src/lib/booking/location-calculations.ts +43 -0
  116. package/src/lib/booking/location-utils.ts +165 -0
  117. package/src/lib/booking/map-utils.ts +153 -0
  118. package/src/lib/booking/marker-icons.ts +113 -0
  119. package/src/lib/booking/normalize-booking-product-id.ts +21 -0
  120. package/src/lib/booking/pickup-location-types.ts +25 -0
  121. package/src/lib/booking/places-api.ts +154 -0
  122. package/src/lib/booking/pricing.ts +466 -0
  123. package/src/lib/booking/product-option-id.ts +35 -0
  124. package/src/lib/booking/source-metadata.ts +226 -0
  125. package/src/lib/booking/sunday-week.ts +14 -0
  126. package/src/lib/booking/theme.ts +83 -0
  127. package/src/lib/booking/trace-context.ts +62 -0
  128. package/src/lib/booking/utils.ts +9 -0
  129. package/src/lib/booking-api.ts +1793 -0
  130. package/src/lib/booking-constants.ts +23 -0
  131. package/src/lib/booking-ref.ts +13 -0
  132. package/src/lib/booking-types.ts +36 -0
  133. package/src/lib/currency.ts +81 -0
  134. package/src/lib/dap-descriptions.ts +50 -0
  135. package/src/lib/dap-itinerary-preview.ts +315 -0
  136. package/src/lib/dependent-add-on-api.ts +434 -0
  137. package/src/lib/env.ts +96 -0
  138. package/src/lib/firebase.ts +20 -0
  139. package/src/lib/job-application-api.ts +83 -0
  140. package/src/lib/manage-booking-embed-print.ts +16 -0
  141. package/src/lib/manage-booking-post-checkout.ts +68 -0
  142. package/src/lib/photo-dap-config.ts +228 -0
  143. package/src/lib/photo-packages.ts +75 -0
  144. package/src/lib/pickup/map-utils.ts +56 -0
  145. package/src/lib/pickup/marker-icons.ts +19 -0
  146. package/src/lib/product-descriptions.ts +66 -0
  147. package/src/lib/products-config.ts +73 -0
  148. package/src/providers/booking-dialog-provider.tsx +282 -0
  149. package/src/providers/dependent-add-on-dialog-provider.tsx +105 -0
  150. package/src/radius.css +5 -0
  151. package/src/spacing.css +7 -0
  152. package/src/strings/en.json +1774 -0
  153. package/src/strings/es.json +1573 -0
  154. package/src/strings/fr.json +1573 -0
  155. package/src/strings/index.js +23 -0
  156. package/src/text-style.css +56 -0
  157. package/src/utils/currency-converter.ts +101 -0
  158. package/tsconfig.json +8 -2
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { loadStripe } from '@stripe/stripe-js';
5
+ import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
6
+ import { ENV } from '@/lib/env';
7
+ import { formatCurrencyAmount, type Currency } from '@/lib/currency';
8
+ import bookingStyles from './BookingDialog.module.css';
9
+
10
+ const stripePromise = ENV.STRIPE_PUBLISHABLE_KEY
11
+ ? loadStripe(ENV.STRIPE_PUBLISHABLE_KEY)
12
+ : null;
13
+
14
+ function toCurrency(code: string | undefined): Currency {
15
+ const c = (code ?? 'CAD').toUpperCase();
16
+ if (c === 'USD' || c === 'EUR' || c === 'GBP' || c === 'AUD' || c === 'CAD') {
17
+ return c;
18
+ }
19
+ return 'CAD';
20
+ }
21
+
22
+ function PaymentSubmitInner({
23
+ returnUrl,
24
+ totalAmount,
25
+ currencyCode,
26
+ onSuccess,
27
+ onError,
28
+ }: {
29
+ returnUrl: string;
30
+ totalAmount: number;
31
+ currencyCode: string;
32
+ onSuccess: () => void;
33
+ onError: (message: string) => void;
34
+ }) {
35
+ const stripe = useStripe();
36
+ const elements = useElements();
37
+ const [loading, setLoading] = useState(false);
38
+ const currency = toCurrency(currencyCode);
39
+
40
+ const handleSubmit = async (e: React.FormEvent) => {
41
+ e.preventDefault();
42
+ if (!stripe || !elements) return;
43
+ setLoading(true);
44
+ onError('');
45
+ const { error: submitError } = await elements.submit();
46
+ if (submitError) {
47
+ onError(submitError.message ?? 'Validation failed');
48
+ setLoading(false);
49
+ return;
50
+ }
51
+ const { error: confirmError } = await stripe.confirmPayment({
52
+ elements,
53
+ confirmParams: {
54
+ return_url: returnUrl,
55
+ },
56
+ redirect: 'if_required',
57
+ });
58
+ if (confirmError) {
59
+ onError(confirmError.message ?? 'Payment failed');
60
+ setLoading(false);
61
+ return;
62
+ }
63
+ onSuccess();
64
+ setLoading(false);
65
+ };
66
+
67
+ return (
68
+ <form onSubmit={handleSubmit} className={`dap-add-on-payment-form ${bookingStyles.dapForm}`}>
69
+ <PaymentElement />
70
+ <button
71
+ type="submit"
72
+ disabled={!stripe || loading}
73
+ className="dap-add-on-primary"
74
+ >
75
+ {loading
76
+ ? 'Processing…'
77
+ : `Pay ${formatCurrencyAmount(totalAmount, currency)}`}
78
+ </button>
79
+ </form>
80
+ );
81
+ }
82
+
83
+ export function DependentAddOnPaymentForm({
84
+ clientSecret,
85
+ returnUrl,
86
+ totalAmount,
87
+ currency,
88
+ onSuccess,
89
+ onError,
90
+ }: {
91
+ clientSecret: string;
92
+ returnUrl: string;
93
+ totalAmount: number;
94
+ currency: string;
95
+ onSuccess: () => void;
96
+ onError: (message: string) => void;
97
+ }) {
98
+ if (!stripePromise) {
99
+ return (
100
+ <p className={bookingStyles.dapStripeNotice}>
101
+ Online payment is not configured. Set NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY or contact us
102
+ to complete your add-on.
103
+ </p>
104
+ );
105
+ }
106
+
107
+ return (
108
+ <Elements
109
+ stripe={stripePromise}
110
+ options={{
111
+ clientSecret,
112
+ appearance: { theme: 'stripe' },
113
+ }}
114
+ >
115
+ <PaymentSubmitInner
116
+ returnUrl={returnUrl}
117
+ totalAmount={totalAmount}
118
+ currencyCode={currency}
119
+ onSuccess={onSuccess}
120
+ onError={onError}
121
+ />
122
+ </Elements>
123
+ );
124
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
4
+
5
+ interface Props {
6
+ children: ReactNode;
7
+ fallback?: ReactNode;
8
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
9
+ }
10
+
11
+ interface State {
12
+ hasError: boolean;
13
+ error: Error | null;
14
+ }
15
+
16
+ /**
17
+ * Error Boundary component to catch and handle React component errors
18
+ * Prevents the entire app from crashing when a component throws an error
19
+ */
20
+ export class ErrorBoundary extends Component<Props, State> {
21
+ constructor(props: Props) {
22
+ super(props);
23
+ this.state = { hasError: false, error: null };
24
+ }
25
+
26
+ static getDerivedStateFromError(error: Error): State {
27
+ return { hasError: true, error };
28
+ }
29
+
30
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
31
+ console.error('ErrorBoundary caught an error:', error, errorInfo);
32
+ if (this.props.onError) {
33
+ this.props.onError(error, errorInfo);
34
+ }
35
+ }
36
+
37
+ render() {
38
+ if (this.state.hasError) {
39
+ if (this.props.fallback) {
40
+ return this.props.fallback;
41
+ }
42
+
43
+ return (
44
+ <div className="p-6 bg-red-50 border border-red-200 rounded-lg">
45
+ <h2 className="text-lg font-semibold text-red-900 mb-2">
46
+ Something went wrong
47
+ </h2>
48
+ <p className="text-red-700 mb-4">
49
+ {this.state.error?.message || 'An unexpected error occurred'}
50
+ </p>
51
+ <button
52
+ onClick={() => this.setState({ hasError: false, error: null })}
53
+ className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
54
+ >
55
+ Try again
56
+ </button>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ return this.props.children;
62
+ }
63
+ }
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import { useRef, useEffect, useState } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+
6
+ interface InfoTooltipProps {
7
+ text: string;
8
+ }
9
+
10
+ function useIsMobile(): boolean {
11
+ const [isMobile, setIsMobile] = useState(false);
12
+ useEffect(() => {
13
+ const mq = window.matchMedia('(max-width: 640px)');
14
+ const handler = () => setIsMobile(mq.matches);
15
+ handler();
16
+ mq.addEventListener('change', handler);
17
+ return () => mq.removeEventListener('change', handler);
18
+ }, []);
19
+ return isMobile;
20
+ }
21
+
22
+ export function InfoTooltip({ text }: InfoTooltipProps) {
23
+ const anchorRef = useRef<HTMLSpanElement>(null);
24
+ const [showTooltip, setShowTooltip] = useState(false);
25
+ const [tooltipStyle, setTooltipStyle] = useState<React.CSSProperties>({});
26
+ const isMobile = useIsMobile();
27
+
28
+ useEffect(() => {
29
+ if (!showTooltip || !anchorRef.current || typeof document === 'undefined') return;
30
+ const rect = anchorRef.current.getBoundingClientRect();
31
+ const vw = typeof window !== 'undefined' ? window.innerWidth : 375;
32
+ const vh = typeof window !== 'undefined' ? window.innerHeight : 667;
33
+ const tooltipWidth = 280;
34
+ const padding = 16;
35
+
36
+ if (isMobile) {
37
+ // Clamp horizontal position so tooltip stays within viewport
38
+ const effectiveWidth = Math.min(tooltipWidth, vw - padding * 2);
39
+ const leftClamped = Math.max(padding, Math.min(rect.left, vw - effectiveWidth - padding));
40
+ // Prefer below icon; if not enough space, show above
41
+ const spaceBelow = vh - rect.bottom - padding;
42
+ const preferAbove = spaceBelow < 100;
43
+
44
+ setTooltipStyle({
45
+ position: 'fixed',
46
+ left: leftClamped,
47
+ ...(preferAbove
48
+ ? { bottom: vh - rect.top + 4, maxHeight: rect.top - padding }
49
+ : { top: rect.bottom + 4, maxHeight: Math.min(200, spaceBelow - 8) }),
50
+ width: effectiveWidth,
51
+ overflowY: 'auto',
52
+ zIndex: 99999,
53
+ });
54
+ } else {
55
+ // Desktop: constrain width so text wraps; clamp horizontal position to stay in viewport
56
+ const maxW = Math.min(320, vw - padding * 2);
57
+ const centerX = rect.left + rect.width / 2;
58
+ const left = Math.max(padding + maxW / 2, Math.min(centerX, vw - padding - maxW / 2));
59
+
60
+ setTooltipStyle({
61
+ position: 'fixed',
62
+ left,
63
+ top: rect.top - 4,
64
+ transform: 'translate(-50%, -100%)',
65
+ maxWidth: maxW,
66
+ zIndex: 99999,
67
+ });
68
+ }
69
+ }, [showTooltip, isMobile]);
70
+
71
+ return (
72
+ <>
73
+ <span className="relative inline-block shrink-0" ref={anchorRef}>
74
+ <svg
75
+ className="w-4 h-4 text-stone-400 cursor-pointer shrink-0"
76
+ fill="none"
77
+ stroke="currentColor"
78
+ viewBox="0 0 24 24"
79
+ onClick={(e) => {
80
+ e.stopPropagation();
81
+ setShowTooltip((v) => !v);
82
+ }}
83
+ onMouseEnter={() => !isMobile && setShowTooltip(true)}
84
+ onMouseLeave={() => !isMobile && setShowTooltip(false)}
85
+ aria-label="More information"
86
+ >
87
+ <path
88
+ strokeLinecap="round"
89
+ strokeLinejoin="round"
90
+ strokeWidth={2}
91
+ d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
92
+ />
93
+ </svg>
94
+ </span>
95
+ {showTooltip &&
96
+ typeof document !== 'undefined' &&
97
+ createPortal(
98
+ <span
99
+ className="whitespace-normal text-xs bg-stone-800 text-white px-3 py-2 rounded shadow-lg pointer-events-none"
100
+ style={tooltipStyle}
101
+ >
102
+ {text}
103
+ </span>,
104
+ document.body
105
+ )}
106
+ </>
107
+ );
108
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Itinerary box - sticky schedule display
3
+ * Uses --booking-* variables from booking-flow.css
4
+ */
5
+
6
+ .box {
7
+ position: sticky;
8
+ top: 0;
9
+ z-index: 10;
10
+ margin-top: 1rem;
11
+ margin-bottom: 1rem;
12
+ background: var(--light-orange-background-dark);
13
+ border-radius: 0.5rem;
14
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
15
+ border: 1px solid var(--booking-stone-200, #e7e5e4);
16
+ transition: all 0.3s;
17
+ overflow: hidden;
18
+ }
19
+
20
+ .boxSticky {
21
+ padding: 0.5rem;
22
+ /* Flush under host sticky headers; base .box margin-top would otherwise show as a gap. */
23
+ margin-top: 0;
24
+ }
25
+
26
+ @media (min-width: 640px) {
27
+ .boxSticky {
28
+ padding: 0.5rem 0.625rem;
29
+ }
30
+ }
31
+
32
+ .boxExpanded {
33
+ padding: 0.75rem;
34
+ }
35
+
36
+ /* Embedded in DAP dialog — not sticky, no outer margin (parent controls spacing). */
37
+ .boxEmbedded {
38
+ position: relative;
39
+ top: auto;
40
+ z-index: 0;
41
+ margin-top: 0;
42
+ margin-bottom: 0;
43
+ }
44
+
45
+ /* Sticky inside dialog `.content` scroll area (below fixed header). */
46
+ .boxStickyInDialog {
47
+ position: sticky;
48
+ top: 0;
49
+ z-index: 20;
50
+ margin-top: 0;
51
+ margin-bottom: 0.75rem;
52
+ }
53
+
54
+ .readOnlyPhotoTime {
55
+ font-weight: 700;
56
+ color: var(--booking-emerald-700, #047857);
57
+ }
58
+
59
+ .readOnlyPhotoDash {
60
+ color: var(--booking-emerald-500, #10b981);
61
+ }
62
+
63
+ .readOnlyPhotoLabel {
64
+ color: var(--booking-emerald-800, #065f46);
65
+ font-size: inherit;
66
+ }
67
+
68
+ .readOnlyPhotoFallbackFoot {
69
+ font-family: 'Figtree', sans-serif;
70
+ font-size: 0.8125rem;
71
+ line-height: 1.4;
72
+ color: var(--booking-stone-500, #78716c);
73
+ margin: 0.75rem 0 0;
74
+ }
75
+
76
+ .readOnlySummaryMessage {
77
+ font-family: 'Figtree', sans-serif;
78
+ font-size: 1rem;
79
+ line-height: 1.55;
80
+ color: var(--booking-stone-600, #57534e);
81
+ margin: 0.25rem 0 0;
82
+ }
83
+
84
+ .readOnlySummaryError {
85
+ font-family: 'Figtree', sans-serif;
86
+ font-size: 0.9375rem;
87
+ line-height: 1.45;
88
+ color: #b91c1c;
89
+ margin: 0.25rem 0 0;
90
+ }
91
+
92
+ .title {
93
+ font-family: 'Poppins', sans-serif;
94
+ font-weight: 700;
95
+ font-size: 1.125rem;
96
+ text-transform: lowercase;
97
+ color: var(--accent-orange);
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 0.5rem;
101
+ transition: all 0.3s;
102
+ }
103
+
104
+ .titleSticky {
105
+ font-size: 1rem;
106
+ margin-bottom: 0.25rem;
107
+ }
108
+
109
+ @media (min-width: 640px) {
110
+ .titleSticky {
111
+ font-size: 1.125rem;
112
+ margin-bottom: 0.375rem;
113
+ }
114
+ }
115
+
116
+ .titleExpanded {
117
+ margin-bottom: 1rem;
118
+ }
119
+
120
+ @media (min-width: 768px) {
121
+ .title {
122
+ font-size: 1.375rem;
123
+ }
124
+ }
125
+
126
+ .dateSubtitle {
127
+ font-weight: 400;
128
+ color: var(--accent-orange);
129
+ font-size: inherit;
130
+ opacity: 0.9;
131
+ }
132
+
133
+ .icon {
134
+ flex-shrink: 0;
135
+ color: var(--accent-orange);
136
+ }
137
+
138
+ .iconSticky {
139
+ width: 1rem;
140
+ height: 1rem;
141
+ }
142
+
143
+ @media (min-width: 640px) {
144
+ .iconSticky {
145
+ width: 1.25rem;
146
+ height: 1.25rem;
147
+ }
148
+ }
149
+
150
+ .iconExpanded {
151
+ width: 1.25rem;
152
+ height: 1.25rem;
153
+ }
154
+
155
+ .itemsSticky {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 0.125rem;
159
+ flex-wrap: wrap;
160
+ transition: gap 0.2s ease-out;
161
+ }
162
+
163
+ @media (min-width: 640px) {
164
+ .itemsSticky {
165
+ gap: 0.25rem;
166
+ }
167
+ }
168
+
169
+ .itemsExpanded {
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 0.25rem;
173
+ max-height: 35rem; /* Enough for long itineraries; collapses to sticky height when sticky */
174
+ transition: max-height 0.25s ease-out, gap 0.2s ease-out;
175
+ }
176
+
177
+ .item {
178
+ font-size: 0.875rem;
179
+ line-height: 1.25;
180
+ }
181
+
182
+ .itemSticky {
183
+ display: inline-flex;
184
+ align-items: center;
185
+ font-size: 0.75rem;
186
+ }
187
+
188
+ @media (min-width: 640px) {
189
+ .itemSticky {
190
+ font-size: 0.875rem;
191
+ }
192
+ }
193
+
194
+ .separator {
195
+ margin: 0 0.125rem;
196
+ color: var(--booking-stone-400, #a8a29e);
197
+ flex-shrink: 0;
198
+ display: inline-flex;
199
+ }
200
+
201
+ @media (min-width: 640px) {
202
+ .separator {
203
+ margin: 0 0.25rem;
204
+ }
205
+ }
206
+
207
+ .timeBold {
208
+ font-weight: 700;
209
+ color: var(--booking-stone-900, #1c1917);
210
+ }
211
+
212
+ .timeMuted {
213
+ font-weight: 700;
214
+ color: var(--booking-stone-400, #a8a29e);
215
+ }
216
+
217
+ .placeLink {
218
+ color: var(--booking-stone-400, #a8a29e);
219
+ text-decoration: underline;
220
+ cursor: pointer;
221
+ }
222
+
223
+ .placeLink:hover {
224
+ color: var(--booking-stone-600, #57534e);
225
+ }
226
+
227
+ .tooltip {
228
+ position: absolute;
229
+ font-size: 0.75rem;
230
+ background: var(--booking-stone-800, #292524);
231
+ color: #fff;
232
+ padding: 0.5rem 0.75rem;
233
+ border-radius: 0.25rem;
234
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
235
+ z-index: 50;
236
+ pointer-events: none;
237
+ white-space: normal;
238
+ }
239
+
240
+ .tooltipMobile {
241
+ top: 100%;
242
+ margin-top: 0.25rem;
243
+ left: 0;
244
+ width: 280px;
245
+ }
246
+
247
+ @media (min-width: 640px) {
248
+ .tooltip {
249
+ white-space: nowrap;
250
+ }
251
+ }
252
+
253
+ .tooltipDesktop {
254
+ left: 50%;
255
+ transform: translateX(-50%);
256
+ bottom: 100%;
257
+ margin-bottom: 0.5rem;
258
+ }