PageSourceSearch

https://transfer-limo-tour-v2.netlify.app/js/main.js

js transfer-limo-tour-v2.netlify.app collected 2026-10-03 11:31:55 UTC 65,722 bytes, 1,654 lines download raw bytes

1/* ================================================
2   TRANSFER LIMO TOUR EXCLUSIVE - Booking Calculator & UI Logic
3   ================================================ */
4
5// --- PRICING DATA (market-average, adjustable) ---
6// Fixed prices for local routes only. Long distance uses per-km calculation.
7const PRICES = {
8  'civitavecchia-port': {
9    'rome-center':    { t1: 160, t2: 180, t3: 210, km: 80 },
10    'rome-termini':   { t1: 160, t2: 180, t3: 210, km: 80 },
11    'fiumicino':      { t1: 150, t2: 170, t3: 190, km: 60 },
12    'ciampino':       { t1: 180, t2: 200, t3: 230, km: 100 },
13  },
14  'fiumicino': {
15    'rome-center':    { t1: 60,  t2: 75,  t3: 95,  km: 30 },
16    'rome-termini':   { t1: 60,  t2: 75,  t3: 95,  km: 33 },
17    'civitavecchia-port': { t1: 150, t2: 170, t3: 190, km: 60 },
18    'ciampino':       { t1: 80,  t2: 95,  t3: 110, km: 45 },
19  },
20  'ciampino': {
21    'rome-center':    { t1: 55,  t2: 70,  t3: 90,  km: 15 },
22    'rome-termini':   { t1: 55,  t2: 70,  t3: 90,  km: 18 },
23    'civitavecchia-port': { t1: 180, t2: 200, t3: 230, km: 100 },
24    'fiumicino':      { t1: 80,  t2: 95,  t3: 110, km: 45 },
25  },
26  'rome-center': {
27    'civitavecchia-port': { t1: 160, t2: 180, t3: 210, km: 80 },
28    'fiumicino':      { t1: 60,  t2: 75,  t3: 95,  km: 30 },
29    'ciampino':       { t1: 55,  t2: 70,  t3: 90,  km: 15 },
30    'rome-termini':   { t1: 25,  t2: 35,  t3: 45,  km: 5 },
31  },
32  'rome-termini': {
33    'civitavecchia-port': { t1: 160, t2: 180, t3: 210, km: 80 },
34    'fiumicino':      { t1: 60,  t2: 75,  t3: 95,  km: 33 },
35    'ciampino':       { t1: 55,  t2: 70,  t3: 90,  km: 18 },
36    'rome-center':    { t1: 25,  t2: 35,  t3: 45,  km: 5 },
37  },
38};
39
40const NIGHT_SURCHARGE = 10;
41const CHILD_SEAT_FEE = 10;
42const EXTRA_BAGGAGE_FEE = 5;   // +€5 per extra piece
43const ANIMAL_FEE = 15;          // +€15 per pet
44const WAITING_FREE_MIN = 20;    // 20 min free waiting
45const WAITING_HOURLY = 50;      // €50/hr after free period
46const ROUNDTRIP_DISCOUNT = 0.10; // 10% discount for round-trip bookings
47const WHATSAPP_NUMBER = '393803444687';
48
49const LOCATION_COORDS = {
50  'civitavecchia-port': [42.0984, 11.7892],   // Largo della Pace - NCC meeting point
51  'fiumicino': [41.7928, 12.2522],            // T3 NCC/Limo Service zone
52  'ciampino': [41.7994, 12.5916],
53  'rome-center': [41.89571, 12.48304],
54  'rome-termini': [41.9022, 12.5009],         // Via Marsala 27 - NCC stop
55  'florence': [43.7766, 11.2479],             // Largo V. Giudice - SMN drop-off
56  'naples': [40.8518, 14.2681],
57  'amalfi-coast': [40.6334, 14.6026],         // Piazza Flavio Gioia, Amalfi
58  'reggia-caserta': [41.0732, 14.3271],       // Piazza Carlo di Borbone
59};
60
61// Predefined address labels for destination pre-fill (NCC-specific stop points)
62const LOCATION_ADDRESSES = {
63  'civitavecchia-port': 'Largo della Pace, Civitavecchia (Cruise Terminal shuttle hub)',
64  'fiumicino': 'Terminal 3 - NCC/Limo Meeting Point, Fiumicino Airport (FCO)',
65  'rome-termini': 'Via Marsala 27, Roma Termini (NCC pickup zone)',
66  'rome-center': 'Monti, Municipio Roma I, Rome, 00187',
67  'florence': 'Largo Vincenzo Giudice, Stazione Santa Maria Novella, Florence',
68  'amalfi-coast': 'Piazza Flavio Gioia, Amalfi (port & bus terminal)',
69  'reggia-caserta': 'Piazza Carlo di Borbone, Caserta (main entrance)',
70};
71const CUSTOM_RATES = { t1: 1.80, t2: 2.10, t3: 2.40, minFare: 40 };
72const LONG_DISTANCE_THRESHOLD_KM = 100; // Road km above this = round-trip charge
73const CARD_SURCHARGE = 0.10; // 10% card surcharge
74const AIRPORT_LOCATIONS = ['fiumicino', 'ciampino'];
75const PORT_LOCATIONS = ['civitavecchia-port'];
76const AIRPORT_KEYWORDS = /airport|aeroporto|aeropuerto|a[eé]roport|flughafen|aerodrome|aeroport/i;
77const PORT_KEYWORDS = /\bport[oe]?\b|harbour|harbor|cruise.?terminal|terminal.?crociere|molo|banchina|porto\b/i;
78
79// --- LOCATION LABELS (updated dynamically by i18n) ---
80let LOCATION_NAMES = {
81  'civitavecchia-port': 'Civitavecchia Cruise Port',
82  'fiumicino': 'Fiumicino Airport (FCO)',
83  'ciampino': 'Ciampino Airport (CIA)',
84  'rome-center': 'Rome City Center',
85  'rome-termini': 'Roma Termini Station',
86  'florence': 'Florence',
87  'naples': 'Naples',
88  'amalfi-coast': 'Amalfi Coast',
89  'reggia-caserta': 'Reggia di Caserta',
90  'custom': 'Custom location',
91};
92
93// --- DOM ELEMENTS ---
94const $ = (sel) => document.querySelector(sel);
95const pickup = $('#pickupLocation');
96const dropoff = $('#dropoffLocation');
97const passengers = $('#passengers');
98const tripDate = $('#tripDate');
99const tripTime = $('#tripTime');
100const priceAmount = $('#priceAmount');
101const priceNote = $('#priceNote');
102const priceRoundTripInfo = $('#priceRoundTripInfo');
103const priceRoundTripText = $('#priceRoundTripText');
104const bookingForm = $('#bookingForm');
105const childSeatCheckbox = $('#childSeat');
106const flightShipRow = $('#flightShipRow');
107const flightShipRef = $('#flightShipRef');
108const flightShipIcon = $('#flightShipIcon');
109const flightShipLabelText = $('#flightShipLabelText');
110let currentPaymentMethod = 'cash';
111
112// --- CURRENT LANGUAGE ---
113let currentLang = localStorage.getItem('transfer-limo-lang') || 'en';
114
115// --- FLATPICKR INSTANCES ---
116let fpDate = null, fpTime = null;
117
118// --- i18n SYSTEM ---
119function setLanguage(lang) {
120  if (!TRANSLATIONS[lang]) return;
121  currentLang = lang;
122  localStorage.setItem('transfer-limo-lang', lang);
123  document.documentElement.lang = lang === 'en' ? 'en' : lang === 'it' ? 'it' : 'es';
124
125  // Update all elements with data-i18n
126  document.querySelectorAll('[data-i18n]').forEach(el => {
127    const key = el.getAttribute('data-i18n');
128    const translation = TRANSLATIONS[lang][key];
129    if (!translation) return;
130
131    if (el.hasAttribute('data-i18n-html')) {
132      el.innerHTML = translation;
133    } else if (el.tagName === 'OPTION') {
134      el.textContent = translation;
135    } else {
136      el.textContent = translation;
137    }
138  });
139
140  // Update placeholders
141  document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
142    const key = el.getAttribute('data-i18n-placeholder');
143    const translation = TRANSLATIONS[lang][key];
144    if (translation) el.placeholder = translation;
145  });
146
147  // Update location names for booking messages
148  LOCATION_NAMES = {
149    'civitavecchia-port': TRANSLATIONS[lang]['loc.civitavecchia'],
150    'fiumicino': TRANSLATIONS[lang]['loc.fiumicino'],
151    'ciampino': TRANSLATIONS[lang]['loc.ciampino'],
152    'rome-center': TRANSLATIONS[lang]['loc.rome-center'],
153    'rome-termini': TRANSLATIONS[lang]['loc.rome-termini'],
154    'florence': TRANSLATIONS[lang]['loc.florence'],
155    'naples': TRANSLATIONS[lang]['loc.naples'],
156    'custom': TRANSLATIONS[lang]['loc.custom'],
157  };
158
159  // Update active button state
160  document.querySelectorAll('.lang-btn').forEach(btn => {
161    btn.classList.toggle('active', btn.getAttribute('data-lang') === lang);
162  });
163
164  // Update Flatpickr locale
165  if (fpDate) { fpDate.set('locale', getFpLocale(lang)); fpDate.redraw(); }
166  if (fpTime) { fpTime.set('locale', getFpLocale(lang)); fpTime.redraw(); }
167
168  // Update flight/ship field labels for new language
169  updateFlightShipField();
170
171  // Recalculate price to update note text
172  calculatePrice();
173
174  // Re-render time slots
175  renderTimeSlots();
176}
177
178function initLanguage() {
179  // Bind click handlers to all lang buttons
180  document.querySelectorAll('.lang-btn').forEach(btn => {
181    btn.addEventListener('click', () => setLanguage(btn.getAttribute('data-lang')));
182  });
183
184  // Set initial language
185  setLanguage(currentLang);
186}
187
188// --- SET DEFAULT DATE (tomorrow) with Flatpickr ---
189function getFpLocale(lang) {
190  if (lang === 'it' && flatpickr.l10ns.it) return 'it';
191  if (lang === 'es' && flatpickr.l10ns.es) return 'es';
192  return 'default';
193}
194
195function initDate() {
196  const tomorrow = new Date();
197  tomorrow.setDate(tomorrow.getDate() + 1);
198
199  if (typeof flatpickr !== 'undefined') {
200    fpDate = flatpickr(tripDate, {
201      dateFormat: 'Y-m-d',
202      altInput: true,
203      altFormat: 'D, j M Y',
204      defaultDate: tomorrow,
205      minDate: 'today',
206      disableMobile: true,
207      locale: getFpLocale(currentLang),
208      onChange: function() {
209        renderTimeSlots();
210        calculatePrice();
211      }
212    });
213
214    fpTime = flatpickr(tripTime, {
215      enableTime: true,
216      noCalendar: true,
217      dateFormat: 'H:i',
218      defaultDate: '10:00',
219      time_24hr: true,
220      disableMobile: true,
221      locale: getFpLocale(currentLang),
222      onChange: function() {
223        renderTimeSlots();
224        calculatePrice();
225      }
226    });
227  } else {
228    // Fallback if Flatpickr CDN fails
229    tripDate.value = tomorrow.toISOString().split('T')[0];
230    tripDate.min = new Date().toISOString().split('T')[0];
231    tripTime.value = '10:00';
232  }
233}
234
235// --- DISTANCE HELPERS ---
236function haversineKm(lat1, lon1, lat2, lon2) {
237  var R = 6371;
238  var dLat = (lat2 - lat1) * Math.PI / 180;
239  var dLon = (lon2 - lon1) * Math.PI / 180;
240  var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
241    Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
242    Math.sin(dLon / 2) * Math.sin(dLon / 2);
243  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
244}
245
246function getMarkerCoords(type) {
247  var marker = type === 'pickup' ? pickupMarker : dropoffMarker;
248  if (!marker) return null;
249  var ll = marker.getLatLng();
250  return [ll.lat, ll.lng];
251}
252
253// --- ROUND-TRIP INFO HELPER ---
254function showRoundTripInfo(show, t) {
255  if (!priceRoundTripInfo) return;
256  if (show && t) {
257    priceRoundTripText.textContent = t['booking.price.roundTripInfo'] || 'Distance over 100 km \u2014 price includes driver\u2019s return trip';
258    priceRoundTripInfo.style.display = 'flex';
259  } else {
260    priceRoundTripInfo.style.display = 'none';
261  }
262}
263
264// --- PRICE CALCULATOR ---
265function calculatePrice() {
266  const from = pickup.value;
267  const to = dropoff.value;
268  const pax = parseInt(passengers.value);
269  const t = TRANSLATIONS[currentLang];
270
271  if (!from || !to) {
272    priceAmount.textContent = '\u2014';
273    priceNote.textContent = t['booking.price.select'] || 'Select pickup & destination';
274    showRoundTripInfo(false);
275    return null;
276  }
277
278  if (from === 'custom' || to === 'custom') {
279    let fromCoords = from === 'custom' ? getMarkerCoords('pickup') : LOCATION_COORDS[from];
280    let toCoords = to === 'custom' ? getMarkerCoords('dropoff') : LOCATION_COORDS[to];
281
282    if (!fromCoords || !toCoords) {
283      priceAmount.textContent = '~';
284      priceNote.textContent = t['booking.price.customMap'] || 'Pick a spot on the map for an estimate';
285      showRoundTripInfo(false);
286      return null;
287    }
288
289    let straightKm = haversineKm(fromCoords[0], fromCoords[1], toCoords[0], toCoords[1]);
290    let roadKmOneWay = Math.round(straightKm * 1.3);
291    let isRoundTrip = roadKmOneWay > LONG_DISTANCE_THRESHOLD_KM;
292    let roadKm = isRoundTrip ? roadKmOneWay * 2 : roadKmOneWay;
293    let rate = pax <= 3 ? CUSTOM_RATES.t1 : pax <= 6 ? CUSTOM_RATES.t2 : CUSTOM_RATES.t3;
294    let vehicleType = pax <= 3 ? 'E-Class (1-3 pax)' : pax <= 6 ? 'V-Class (4-6 pax)' : 'V-Class (7-8 pax)';
295    let basePrice = Math.max(CUSTOM_RATES.minFare, Math.round(roadKm * rate));
296
297    let time2 = tripTime.value;
298    let isNight2 = false;
299    if (time2) {
300      let h = parseInt(time2.split(':')[0]);
301      isNight2 = h >= 22 || h < 6;
302    }
303    let hasChildSeat2 = childSeatCheckbox && childSeatCheckbox.checked;
304    let tripPrice2 = getCardPrice(basePrice);
305    let totalPrice2 = tripPrice2 + (isNight2 ? NIGHT_SURCHARGE : 0) + (hasChildSeat2 ? CHILD_SEAT_FEE : 0);
306    let displayPrice2 = totalPrice2;
307
308    priceAmount.style.opacity = '0.3';
309    setTimeout(function() {
310      priceAmount.textContent = '~\u20AC' + displayPrice2;
311      priceAmount.style.opacity = '1';
312    }, 150);
313    let nightText2 = isNight2 ? ' (incl. night surcharge)' : '';
314    let seatText2 = hasChildSeat2 ? ' + child seat' : '';
315    let cardText2 = currentPaymentMethod === 'card' ? ' \u2022 Card +10%' : '';
316    let rtText2 = isRoundTrip ? ' \u2022 ' + (t['booking.price.roundTrip'] || 'Incl. return') : '';
317    priceNote.textContent = vehicleType + ' \u2022 ' + (t['booking.price.estimate'] || 'Estimated') + rtText2 + nightText2 + seatText2 + cardText2;
318    showRoundTripInfo(isRoundTrip, t);
319
320    return { price: totalPrice2, displayPrice: displayPrice2, vehicleType: vehicleType, km: roadKm, isNight: isNight2, estimated: true, roundTrip: isRoundTrip, childSeat: hasChildSeat2, paymentMethod: currentPaymentMethod };
321  }
322
323  if (from === to) {
324    priceAmount.textContent = '\u2014';
325    priceNote.textContent = t['booking.price.sameLocation'] || 'Pickup and destination must be different';
326    showRoundTripInfo(false);
327    return null;
328  }
329
330  // Look up price (try both directions)
331  let route = PRICES[from]?.[to] || PRICES[to]?.[from];
332
333  if (!route) {
334    // No fixed price — calculate per-km for long distance routes
335    let fromCoords2 = LOCATION_COORDS[from];
336    let toCoords2 = LOCATION_COORDS[to];
337    if (fromCoords2 && toCoords2) {
338      let straightKm2 = haversineKm(fromCoords2[0], fromCoords2[1], toCoords2[0], toCoords2[1]);
339      let roadKmOneWay2 = Math.round(straightKm2 * 1.3);
340      let isRoundTrip2 = roadKmOneWay2 > LONG_DISTANCE_THRESHOLD_KM;
341      let roadKm2 = isRoundTrip2 ? roadKmOneWay2 * 2 : roadKmOneWay2;
342      let rate2 = pax <= 3 ? CUSTOM_RATES.t1 : pax <= 6 ? CUSTOM_RATES.t2 : CUSTOM_RATES.t3;
343      let vehicleType2 = pax <= 3 ? 'E-Class (1-3 pax)' : pax <= 6 ? 'V-Class (4-6 pax)' : 'V-Class (7-8 pax)';
344      let basePrice2 = Math.max(CUSTOM_RATES.minFare, Math.round(roadKm2 * rate2));
345      let time3 = tripTime.value;
346      let isNight3 = false;
347      if (time3) { let h3 = parseInt(time3.split(':')[0]); isNight3 = h3 >= 22 || h3 < 6; }
348      let hasChildSeat3 = childSeatCheckbox && childSeatCheckbox.checked;
349      let tripPrice3 = getCardPrice(basePrice2);
350      let totalPrice3 = tripPrice3 + (isNight3 ? NIGHT_SURCHARGE : 0) + (hasChildSeat3 ? CHILD_SEAT_FEE : 0);
351      let displayPrice3 = totalPrice3;
352      priceAmount.style.opacity = '0.3';
353      setTimeout(function() { priceAmount.textContent = '~\u20AC' + displayPrice3; priceAmount.style.opacity = '1'; }, 150);
354      let rtText3 = isRoundTrip2 ? ' \u2022 ' + (t['booking.price.roundTrip'] || 'Incl. return') : '';
355      let nightText3 = isNight3 ? ' (incl. night surcharge)' : '';
356      let seatText3 = hasChildSeat3 ? ' + child seat' : '';
357      let cardText3 = currentPaymentMethod === 'card' ? ' \u2022 Card +10%' : '';
358      priceNote.textContent = vehicleType2 + ' \u2022 ' + (t['booking.price.estimate'] || 'Estimated') + rtText3 + nightText3 + seatText3 + cardText3;
359      showRoundTripInfo(isRoundTrip2, t);
360      return { price: totalPrice3, displayPrice: displayPrice3, vehicleType: vehicleType2, km: roadKm2, isNight: isNight3, estimated: true, roundTrip: isRoundTrip2, childSeat: hasChildSeat3, paymentMethod: currentPaymentMethod };
361    }
362    priceAmount.textContent = 'Quote';
363    priceNote.textContent = t['booking.price.select'] || 'Contact us for this route\'s price';
364    showRoundTripInfo(false);
365    return null;
366  }
367
368  // 3-tier pricing: 1-3 pax, 4-6 pax, 7-8 pax
369  const basePrice = pax <= 3 ? route.t1 : pax <= 6 ? route.t2 : route.t3;
370  const vehicleType = pax <= 3 ? 'E-Class (1-3 pax)' : pax <= 6 ? 'V-Class (4-6 pax)' : 'V-Class (7-8 pax)';
371
372  // Night surcharge check
373  const time = tripTime.value;
374  let isNight = false;
375  if (time) {
376    const hour = parseInt(time.split(':')[0]);
377    isNight = hour >= 22 || hour < 6;
378  }
379
380  const hasChildSeat = childSeatCheckbox && childSeatCheckbox.checked;
381  const tripPrice = getCardPrice(basePrice);
382  const totalPrice = tripPrice + (isNight ? NIGHT_SURCHARGE : 0) + (hasChildSeat ? CHILD_SEAT_FEE : 0);
383  const displayPrice = totalPrice;
384
385  priceAmount.style.opacity = '0.3';
386  setTimeout(() => {
387    priceAmount.textContent = `\u20AC${displayPrice}`;
388    priceAmount.style.opacity = '1';
389  }, 150);
390  const nightText = isNight ? ' (incl. night surcharge)' : '';
391  const seatText = hasChildSeat ? ' + child seat' : '';
392  const cardText = currentPaymentMethod === 'card' ? ' \u2022 Card +10%' : '';
393  priceNote.textContent = `${vehicleType} \u2022 All inclusive${nightText}${seatText}${cardText}`;
394  showRoundTripInfo(false);
395
396  return { price: totalPrice, displayPrice, vehicleType, km: route.km, isNight, childSeat: hasChildSeat, paymentMethod: currentPaymentMethod };
397}
398
399// --- LUGGAGE HELPERS ---
400function getLuggageSummary() {
401  const cabin = parseInt(document.getElementById('luggageCabin')?.textContent || '0');
402  const medium = parseInt(document.getElementById('luggageMedium')?.textContent || '0');
403  const large = parseInt(document.getElementById('luggageLarge')?.textContent || '0');
404  if (cabin + medium + large === 0) return '';
405  const t = TRANSLATIONS[currentLang];
406  const parts = [];
407  if (cabin > 0) parts.push(`${cabin} ${t['booking.luggage.cabin'] || 'Cabin'}`);
408  if (medium > 0) parts.push(`${medium} ${t['booking.luggage.medium'] || 'Medium'}`);
409  if (large > 0) parts.push(`${large} ${t['booking.luggage.large'] || 'Large'}`);
410  return parts.join(' + ');
411}
412
413function initLuggage() {
414  document.querySelectorAll('.luggage-btn').forEach(btn => {
415    btn.addEventListener('click', () => {
416      const targetId = btn.dataset.target;
417      const countEl = document.getElementById(targetId);
418      if (!countEl) return;
419      let val = parseInt(countEl.textContent) || 0;
420      if (btn.classList.contains('luggage-plus')) {
421        if (val < 10) countEl.textContent = val + 1;
422      } else {
423        if (val > 0) countEl.textContent = val - 1;
424      }
425    });
426  });
427}
428
429// --- FLIGHT / SHIP REFERENCE ---
430function detectLocationCategory(locValue, type) {
431  // 1. Check predefined dropdown locations
432  if (AIRPORT_LOCATIONS.includes(locValue)) return 'airport';
433  if (PORT_LOCATIONS.includes(locValue)) return 'port';
434  // 2. For custom locations, check Nominatim metadata
435  if (locValue === 'custom') {
436    const meta = customLocationType[type];
437    if (meta) {
438      if (meta.class === 'aeroway' || meta.type === 'aerodrome') return 'airport';
439      if (meta.type === 'port' || meta.type === 'harbour' || meta.class === 'harbour') return 'port';
440      // 3. Keyword fallback on display_name
441      if (AIRPORT_KEYWORDS.test(meta.display_name)) return 'airport';
442      if (PORT_KEYWORDS.test(meta.display_name)) return 'port';
443    }
444    // 4. Also check the address input text directly
445    const input = document.getElementById(type === 'pickup' ? 'pickupCustom' : 'dropoffCustom');
446    if (input && input.value) {
447      if (AIRPORT_KEYWORDS.test(input.value)) return 'airport';
448      if (PORT_KEYWORDS.test(input.value)) return 'port';
449    }
450  }
451  return null;
452}
453
454let flightShipMandatory = false;
455
456function updateFlightShipField() {
457  const from = pickup.value;
458  const to = dropoff.value;
459  const t = TRANSLATIONS[currentLang];
460  const pickupCat = detectLocationCategory(from, 'pickup');
461  const dropoffCat = detectLocationCategory(to, 'dropoff');
462  const hasAirport = pickupCat === 'airport' || dropoffCat === 'airport';
463  const hasPort = pickupCat === 'port' || dropoffCat === 'port';
464
465  // Mandatory when predefined airport or port is selected from dropdown
466  const predefinedAirportOrPort =
467    AIRPORT_LOCATIONS.includes(from) || AIRPORT_LOCATIONS.includes(to) ||
468    PORT_LOCATIONS.includes(from) || PORT_LOCATIONS.includes(to);
469  flightShipMandatory = predefinedAirportOrPort;
470  flightShipRef.required = predefinedAirportOrPort;
471
472  // Always visible — adapt icon and label to context
473  flightShipRow.style.display = '';
474  const optionalTag = predefinedAirportOrPort ? '' : (' (' + (t['booking.form.optional'] || 'optional') + ')');
475  if (hasAirport) {
476    flightShipIcon.className = 'fas fa-plane';
477    flightShipLabelText.textContent = (t['booking.form.flightNumber'] || 'Flight number') + optionalTag;
478    flightShipRef.placeholder = t['booking.form.flightPlaceholder'] || 'e.g. FR1234';
479  } else if (hasPort) {
480    flightShipIcon.className = 'fas fa-ship';
481    flightShipLabelText.textContent = (t['booking.form.shipName'] || 'Ship / cruise name') + optionalTag;
482    flightShipRef.placeholder = t['booking.form.shipPlaceholder'] || 'e.g. MSC Grandiosa';
483  } else {
484    flightShipIcon.className = 'fas fa-ticket-alt';
485    flightShipLabelText.textContent = (t['booking.form.refNumber'] || 'Flight / ship / train number') + optionalTag;
486    flightShipRef.placeholder = t['booking.form.refPlaceholder'] || 'e.g. FR1234, MSC Grandiosa';
487  }
488}
489
490// --- PAYMENT METHOD ---
491function initPaymentToggle() {
492  const btns = document.querySelectorAll('.payment-btn');
493  btns.forEach(function(btn) {
494    btn.addEventListener('click', function() {
495      btns.forEach(function(b) { b.classList.remove('active'); });
496      btn.classList.add('active');
497      currentPaymentMethod = btn.dataset.method;
498      calculatePrice();
499    });
500  });
501}
502
503function getCardPrice(basePrice) {
504  if (currentPaymentMethod === 'card') {
505    return Math.ceil(basePrice * (1 + CARD_SURCHARGE));
506  }
507  return basePrice;
508}
509
510// --- BOOKING DATA ---
511// --- STRUCTURED ADDRESS STORAGE (from Nominatim addressdetails) ---
512var customLocationDetails = { pickup: null, dropoff: null };
513var customLocationType = { pickup: null, dropoff: null }; // { class, type, display_name }
514
515// Extract a professional label from stored Nominatim address parts
516// Fallback chain: road+city → city/town/village → county → state
517function getLabelFromAddressDetails(addr) {
518  if (!addr) return null;
519  var city = addr.city || addr.town || addr.village || addr.municipality || addr.hamlet || '';
520  var road = addr.road || '';
521  var county = addr.county || addr.state_district || '';
522  var state = addr.state || '';
523  if (road && city) return road + ', ' + city;
524  if (city) return city;
525  if (county) return county;
526  if (state) return state;
527  return null;
528}
529
530// Extract a short, professional location label from a Nominatim display_name string
531// "Strada Pozzacchio, Latina, Lazio, 04100, Italy" → "Strada Pozzacchio, Latina"
532function getShortLocationName(fullAddress) {
533  if (!fullAddress) return null;
534  var trimmed = fullAddress.trim();
535  if (!trimmed) return null;
536  // Raw coordinates aren't useful as a name
537  if (/^\d+\.\d+\s*,\s*\d+\.\d+$/.test(trimmed)) return null;
538  var parts = trimmed.split(',').map(function(p) { return p.trim(); }
538).filter(Boolean);
539  // Filter out postal codes and country names
540  var meaningful = parts.filter(function(p) {
541    return !/^\d{4,5}$/.test(p) && !/^(italy|italia|españa|spain|france|francia|deutschland|germany)$/i.test(p);
542  });
543  return meaningful.length > 0
544    ? meaningful.slice(0, 2).join(', ')
545    : trimmed.substring(0, 50);
546}
547
548function getBookingData() {
549  const fromVal = pickup.value;
550  const toVal = dropoff.value;
551  const fromCustomAddr = fromVal === 'custom' ? ($('#pickupCustom')?.value || '') : '';
552  const toCustomAddr = toVal === 'custom' ? ($('#dropoffCustom')?.value || '') : '';
553  return {
554    name: $('#clientName').value || 'Not provided',
555    phone: $('#clientPhone').value || 'Not provided',
556    email: $('#clientEmail').value || '',
557    notes: $('#clientNotes').value || '',
558    from: fromVal === 'custom'
559      ? (getShortLocationName(fromCustomAddr) || getLabelFromAddressDetails(customLocationDetails.pickup) || LOCATION_NAMES['custom'])
560      : (LOCATION_NAMES[fromVal] || fromVal),
561    to: toVal === 'custom'
562      ? (getShortLocationName(toCustomAddr) || getLabelFromAddressDetails(customLocationDetails.dropoff) || LOCATION_NAMES['custom'])
563      : (LOCATION_NAMES[toVal] || toVal),
564    pax: passengers.value,
565    date: tripDate.value,
566    time: tripTime.value,
567    priceInfo: calculatePrice(),
568    fromAddr: fromCustomAddr,
569    toAddr: toCustomAddr,
570    fromCoords: fromVal === 'custom' ? getMarkerCoords('pickup') : LOCATION_COORDS[fromVal],
571    toCoords: toVal === 'custom' ? getMarkerCoords('dropoff') : LOCATION_COORDS[toVal],
572    hasChildSeat: childSeatCheckbox && childSeatCheckbox.checked,
573    luggage: getLuggageSummary(),
574    flightShip: flightShipRef.value || '',
575    paymentMethod: currentPaymentMethod
576  };
577}
578
579// --- WHATSAPP BOOKING ---
580function buildWhatsAppMessage() {
581  const d = getBookingData();
582  const dispPrice = d.priceInfo ? (d.priceInfo.displayPrice || d.priceInfo.price) : null;
583  const priceText = dispPrice ? `\u20AC${dispPrice} (${d.priceInfo.vehicleType})` : 'To be quoted';
584  const payLabel = d.paymentMethod === 'card' ? 'Card' : 'Cash';
585
586  let msg = `\u2736 *BOOKING REQUEST*\n\n`;
587
588  msg += `\u25B8 *ROUTE*\n`;
589  msg += `*From:* ${d.from}\n`;
590  if (d.fromAddr) msg += `  _${d.fromAddr}_\n`;
591  if (d.fromCoords) msg += `  Map: https://maps.google.com/?q=${d.fromCoords[0]},${d.fromCoords[1]}\n`;
592  msg += `*To:* ${d.to}\n`;
593  if (d.toAddr) msg += `  _${d.toAddr}_\n`;
594  if (d.toCoords) msg += `  Map: https://maps.google.com/?q=${d.toCoords[0]},${d.toCoords[1]}\n`;
595  if (d.flightShip) msg += `*Flight/Ship:* ${d.flightShip}\n`;
596
597  msg += `\n\u25B8 *PASSENGERS & LUGGAGE*\n`;
598  msg += `*Travelers:* ${d.pax}\n`;
599  msg += `*Luggage:* ${d.luggage || 'No luggage'}\n`;
600  if (d.hasChildSeat) msg += `*Child Seat:* Yes (+\u20AC${CHILD_SEAT_FEE})\n`;
601
602  msg += `\n\u25B8 *SCHEDULE*\n`;
603  msg += `*Date:* ${d.date}\n`;
604  msg += `*Time:* ${d.time}\n`;
605
606  msg += `\n\u25B8 *CLIENT*\n`;
607  msg += `*Name:* ${d.name}\n`;
608  msg += `*Phone:* ${d.phone}\n`;
609  if (d.email) msg += `*Email:* ${d.email}\n`;
610
611  if (d.notes) {
612    msg += `\n\u25B8 *NOTES*\n`;
613    msg += `${d.notes}\n`;
614  }
615
616  msg += `\n\u25B8 *Price:* ${priceText}`;
617  if (d.priceInfo?.roundTrip) msg += ` \u2714 incl. driver return`;
618  msg += `\n\u25B8 *Payment:* ${payLabel}`;
619  if (d.paymentMethod === 'card') msg += ` (incl. 10% card fee)`;
620  msg += `\n\n_Sent from transferlimotour.it_`;
621
622  return msg;
623}
624
625function buildEmailMessage() {
626  const d = getBookingData();
627  const dispPrice = d.priceInfo ? (d.priceInfo.displayPrice || d.priceInfo.price) : null;
628  const priceText = dispPrice ? `\u20AC${dispPrice} (${d.priceInfo.vehicleType})` : 'To be quoted';
629  const payLabel = d.paymentMethod === 'card' ? 'Card' : 'Cash';
630
631  const subject = `Transfer Booking: ${d.from} \u2192 ${d.to} on ${d.date} at ${d.time}`;
632
633  let body = `BOOKING REQUEST\n\n`;
634
635  body += `--- ROUTE ---\n`;
636  body += `From: ${d.from}\n`;
637  if (d.fromAddr) body += `  ${d.fromAddr}\n`;
638  if (d.fromCoords) body += `  Map: https://maps.google.com/?q=${d.fromCoords[0]},${d.fromCoords[1]}\n`;
639  body += `To: ${d.to}\n`;
640  if (d.toAddr) body += `  ${d.toAddr}\n`;
641  if (d.toCoords) body += `  Map: https://maps.google.com/?q=${d.toCoords[0]},${d.toCoords[1]}\n`;
642  if (d.flightShip) body += `Flight/Ship: ${d.flightShip}\n`;
643
644  body += `\n--- PASSENGERS & LUGGAGE ---\n`;
645  body += `Travelers: ${d.pax}\n`;
646  body += `Luggage: ${d.luggage || 'No luggage'}\n`;
647  if (d.hasChildSeat) body += `Child Seat: Yes (+\u20AC${CHILD_SEAT_FEE})\n`;
648
649  body += `\n--- SCHEDULE ---\n`;
650  body += `Date: ${d.date}\n`;
651  body += `Time: ${d.time}\n`;
652
653  body += `\n--- CLIENT ---\n`;
654  body += `Name: ${d.name}\n`;
655  body += `Phone: ${d.phone}\n`;
656  if (d.email) body += `Email: ${d.email}\n`;
657
658  if (d.notes) {
659    body += `\n--- NOTES ---\n`;
660    body += `${d.notes}\n`;
661  }
662
663  body += `\nPrice: ${priceText}`;
664  if (d.priceInfo?.roundTrip) body += ` - incl. driver return`;
665  body += `\nPayment: ${payLabel}`;
666  if (d.paymentMethod === 'card') body += ` (incl. 10% card fee)`;
667  body += `\n\nSent from transferlimotour.it`;
668
669  return { subject, body };
670}
671
672// --- PREVIEW MODAL ---
673function formatPreviewHTML(rawMsg) {
674  let html = rawMsg
675    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
676    .replace(/\*([^*]+)\*/g, '<strong>$1</strong>')
677    .replace(/_([^_]+)_/g, '<em>$1</em>')
678    .replace(/-{4,}/g, '<div class="preview-divider"></div>')
679    .replace(/https:\/\/maps\.google\.com[^\s<]+/g, '<a href="$&" target="_blank" rel="noopener">View on Map</a>')
680    .replace(/\n/g, '<br>');
681  return html;
682}
683
684function buildPreviewHTML() {
685  const d = getBookingData();
686  const priceAmount = d.priceInfo ? `\u20AC${d.priceInfo.price}` : 'To be quoted';
687  const vehicleText = d.priceInfo ? d.priceInfo.vehicleType : '';
688  const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
689  const mapLink = coords => coords
690    ? `<a class="preview-map-link" href="https://maps.google.com/?q=${coords[0]},${coords[1]}" target="_blank" rel="noopener"><i class="fas fa-map-marker-alt"></i> View on Map</a>`
691    : '';
692
693  let h = '<div class="preview-title">BOOKING REQUEST</div>';
694
695  // Route
696  h += '<div class="preview-section">';
697  h += '<div class="preview-section-header"><i class="fas fa-route"></i> Route</div>';
698  h += '<div class="preview-section-content">';
699  h += `<div class="preview-field"><strong>From:</strong> ${esc(d.from)}</div>`;
700  if (d.fromAddr) h += `<div class="preview-field-sub">${esc(d.fromAddr)}</div>`;
701  h += mapLink(d.fromCoords);
702  h += `<div class="preview-field" style="margin-top:6px"><strong>To:</strong> ${esc(d.to)}</div>`;
703  if (d.toAddr) h += `<div class="preview-field-sub">${esc(d.toAddr)}</div>`;
704  h += mapLink(d.toCoords);
705  h += '</div></div>';
706
707  // Passengers & Luggage
708  h += '<div class="preview-section">';
709  h += '<div class="preview-section-header"><i class="fas fa-users"></i> Passengers & Luggage</div>';
710  h += '<div class="preview-section-content">';
711  h += `<div class="preview-field"><strong>Travelers:</strong> ${esc(d.pax)}</div>`;
712  h += `<div class="preview-field"><strong>Luggage:</strong> ${esc(d.luggage || 'No luggage')}</div>`;
713  if (d.hasChildSeat) h += `<div class="preview-field"><strong>Child Seat:</strong> Yes (+\u20AC${CHILD_SEAT_FEE})</div>`;
714  h += '</div></div>';
715
716  // Schedule
717  h += '<div class="preview-section">';
718  h += '<div class="preview-section-header"><i class="fas fa-calendar-alt"></i> Schedule</div>';
719  h += '<div class="preview-section-content">';
720  h += `<div class="preview-field"><strong>Date:</strong> ${esc(d.date)}</div>`;
721  h += `<div class="preview-field"><strong>Time:</strong> ${esc(d.time)}</div>`;
722  h += '</div></div>';
723
724  // Client
725  h += '<div class="preview-section">';
726  h += '<div class="preview-section-header"><i class="fas fa-user"></i> Client Details</div>';
727  h += '<div class="preview-section-content">';
728  h += `<div class="preview-field"><strong>Name:</strong> ${esc(d.name)}</div>`;
729  h += `<div class="preview-field"><strong>Phone:</strong> ${esc(d.phone)}</div>`;
730  if (d.email) h += `<div class="preview-field"><strong>Email:</strong> ${esc(d.email)}</div>`;
731  h += '</div></div>';
732
733  // Notes
734  if (d.notes) {
735    h += '<div class="preview-section">';
736    h += '<div class="preview-section-header"><i class="fas fa-comment-alt"></i> Notes</div>';
737    h += '<div class="preview-section-content">';
738    h += `<div class="preview-notes">${esc(d.notes).replace(/\n/g, '<br>')}</div>`;
739    h += '</div></div>';
740  }
741
742  // Price
743  h += '<div class="preview-price">';
744  h += `<i class="fas fa-tag"></i> ${priceAmount}`;
745  if (vehicleText) h += ` <span>${esc(vehicleText)}</span>`;
746  h += '</div>';
747
748  h += '<div class="preview-footer-text">Sent from transferlimotour.it</div>';
749  return h;
750}
751
752function buildEmailPreviewHTML() {
753  const d = getBookingData();
754  const priceAmount = d.priceInfo ? `\u20AC${d.priceInfo.price}` : 'To be quoted';
755  const vehicleText = d.priceInfo ? d.priceInfo.vehicleType : '';
756  const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
757  const mapLink = coords => coords
758    ? `<a class="preview-map-link" href="https://maps.google.com/?q=${coords[0]},${coords[1]}" target="_blank" rel="noopener"><i class="fas fa-map-marker-alt"></i> View on Map</a>`
759    : '';
760
761  let h = '<div class="preview-title">EMAIL PREVIEW</div>';
762
763  // Subject
764  h += '<div class="preview-section">';
765  h += '<div class="preview-section-header"><i class="fas fa-envelope"></i> Subject</div>';
766  h += '<div class="preview-section-content">';
767  h += `<div class="preview-field"><strong>${esc(d.from)}</strong> \u2192 <strong>${esc(d.to)}</strong> on ${esc(d.date)}</div>`;
768  h += '</div></div>';
769
770  // Route
771  h += '<div class="preview-section">';
772  h += '<div class="preview-section-header"><i class="fas fa-route"></i> Route</div>';
773  h += '<div class="preview-section-content">';
774  h += `<div class="preview-field"><strong>From:</strong> ${esc(d.from)}</div>`;
775  if (d.fromAddr) h += `<div class="preview-field-sub">${esc(d.fromAddr)}</div>`;
776  h += mapLink(d.fromCoords);
777  h += `<div class="preview-field" style="margin-top:6px"><strong>To:</strong> ${esc(d.to)}</div>`;
778  if (d.toAddr) h += `<div class="preview-field-sub">${esc(d.toAddr)}</div>`;
779  h += mapLink(d.toCoords);
780  h += '</div></div>';
781
782  // Passengers & Luggage
783  h += '<div class="preview-section">';
784  h += '<div class="preview-section-header"><i class="fas fa-users"></i> Passengers & Luggage</div>';
785  h += '<div class="preview-section-content">';
786  h += `<div class="preview-field"><strong>Travelers:</strong> ${esc(d.pax)}</div>`;
787  h += `<div class="preview-field"><strong>Luggage:</strong> ${esc(d.luggage || 'No luggage')}</div>`;
788  if (d.hasChildSeat) h += `<div class="preview-field"><strong>Child Seat:</strong> Yes (+\u20AC${CHILD_SEAT_FEE})</div>`;
789  h += '</div></div>';
790
791  // Schedule
792  h += '<div class="preview-section">';
793  h += '<div class="preview-section-header"><i class="fas fa-calendar-alt"></i> Schedule</div>';
794  h += '<div class="preview-section-content">';
795  h += `<div class="preview-field"><strong>Date:</strong> ${esc(d.date)}</div>`;
796  h += `<div class="preview-field"><strong>Time:</strong> ${esc(d.time)}</div>`;
797  h += '</div></div>';
798
799  // Client
800  h += '<div class="preview-section">';
801  h += '<div class="preview-section-header"><i class="fas fa-user"></i> Client Details</div>';
802  h += '<div class="preview-section-content">';
803  h += `<div class="preview-field"><strong>Name:</strong> ${esc(d.name)}</div>`;
804  h += `<div class="preview-field"><strong>Phone:</strong> ${esc(d.phone)}</div>`;
805  if (d.email) h += `<div class="preview-field"><strong>Email:</strong> ${esc(d.email)}</div>`;
806  h += '</div></div>';
807
808  // Notes
809  if (d.notes) {
810    h += '<div class="preview-section">';
811    h += '<div class="preview-section-header"><i class="fas fa-comment-alt"></i> Notes</div>';
812    h += '<div class="preview-section-content">';
813    h += `<div class="preview-notes">${esc(d.notes).replace(/\n/g, '<br>')}</div>`;
814    h += '</div></div>';
815  }
816
817  // Price
818  h += '<div class="preview-price">';
819  h += `<i class="fas fa-tag"></i> ${priceAmount}`;
820  if (vehicleText) h += ` <span>${esc(vehicleText)}</span>`;
821  h += '</div>';
822
823  h += '<div class="preview-footer-text">Sent from transferlimotour.it</div>';
824  return h;
825}
826
827function showPreview(type) {
828  const overlay = $('#previewOverlay');
829  const titleEl = $('#previewTitle');
830  const textEl = $('#previewText');
831  const sendBtn = $('#previewSend');
832
833  let previewMsg = '';
834  let sendAction = null;
835
836  const t = TRANSLATIONS[currentLang] || TRANSLATIONS['en'];
837
838  if (type === 'whatsapp') {
839    previewMsg = buildPreviewHTML();
840    titleEl.textContent = t['preview.whatsapp.title'] || 'WhatsApp Message Preview';
841    sendBtn.innerHTML = `<i class="fab fa-whatsapp"></i> ${t['preview.send.whatsapp'] || 'Send via WhatsApp'}`;
842    sendBtn.className = 'btn btn-gold';
843    sendAction = () => {
844      const encoded = encodeURIComponent(buildWhatsAppMessage());
845      window.open(`https://wa.me/${WHATSAPP_NUMBER}?text=${encoded}`, '_blank');
846    };
847  } else {
848    previewMsg = buildEmailPreviewHTML();
849    titleEl.textContent = t['preview.email.title'] || 'Email Preview';
850    sendBtn.innerHTML = `<i class="fas fa-envelope"></i> ${t['preview.send.email'] || 'Send Email'}`;
851    sendBtn.className = 'btn btn-gold';
852    sendAction = () => {
853      const email2 = buildEmailMessage();
854      window.open(`mailto:[email protected]?subject=${encodeURIComponent(email2.subject)}&body=${encodeURIComponent(email2.body)}`, '_blank');
855    };
856  }
857
858  textEl.innerHTML = previewMsg;
859
860  // No-luggage warning
861  const existingWarning = overlay.querySelector('.preview-luggage-warning');
862  if (existingWarning) existingWarning.remove();
863  if (!getLuggageSummary()) {
864    const warning = document.createElement('div');
865    warning.className = 'preview-luggage-warning';
866    warning.innerHTML = `<i class="fas fa-exclamation-triangle"></i> ${t['preview.noLuggage'] || 'No luggage selected. Go back to add luggage if needed.'}`;
867    textEl.parentNode.insertBefore(warning, textEl.nextSibling);
868  }
869
870  overlay.classList.add('active');
871  document.body.style.overflow = 'hidden';
872
873  // Wire up send button (replace old listeners)
874  const newSendBtn = sendBtn.cloneNode(true);
875  sendBtn.parentNode.replaceChild(newSendBtn, sendBtn);
876  newSendBtn.addEventListener('click', () => {
877    overlay.classList.remove('active');
878    document.body.style.overflow = '';
879    sendAction();
880  });
881}
882
883function closePreview() {
884  const overlay = $('#previewOverlay');
885  overlay.classList.remove('active');
886  document.body.style.overflow = '';
887}
888
889function checkFlightShipWarning() {
890  const warning = document.getElementById('flightShipWarning');
891  if (!warning) return true;
892  // Skip soft warning for mandatory fields — browser required validation handles those
893  if (flightShipMandatory) return true;
894  // For optional: if field is visible but empty, show soft warning once
895  if (flightShipRow.style.display !== 'none' && !flightShipRef.value.trim()) {
896    warning.style.display = '';
897    flightShipRef.style.borderColor = '#eab308';
898    flightShipRef.focus();
899    // Auto-hide warning after interaction
900    flightShipRef.addEventListener('input', function onInput() {
901      if (flightShipRef.value.trim()) {
902        warning.style.display = 'none';
903        flightShipRef.style.borderColor = '';
904      }
905      flightShipRef.removeEventListener('input', onInput);
906    });
907    return false;
908  }
909  warning.style.display = 'none';
910  flightShipRef.style.borderColor = '';
911  return true;
912}
913
914let flightShipWarningShown = false;
915
916function bookViaWhatsApp(e) {
917  e.preventDefault();
918  if (!bookingForm.checkValidity()) {
919    bookingForm.reportValidity();
920    return;
921  }
922  // Show warning once — second click proceeds anyway
923  if (!flightShipWarningShown && !checkFlightShipWarning()) {
924    flightShipWarningShown = true;
925    return;
926  }
927  flightShipWarningShown = false;
928  showPreview('whatsapp');
929}
930
931function sendEmailInquiry() {
932  if (!bookingForm.checkValidity()) {
933    bookingForm.reportValidity();
934    return;
935  }
936  if (!flightShipWarningShown && !checkFlightShipWarning()) {
937    flightShipWarningShown = true;
938    return;
939  }
940  flightShipWarningShown = false;
941  showPreview('email');
942}
943
944// --- FLEET GALLERY ---
945function initFleetGallery() {
946  document.querySelectorAll('.fleet-showcase').forEach(showcase => {
947    const thumbs = showcase.querySelectorAll('.fleet-thumb');
948    const mainImg = showcase.querySelector('.fleet-img-main img');
949    if (!mainImg) return;
950
951    thumbs.forEach(thumb => {
952      const switchImage = () => {
953        thumbs.forEach(t => t.classList.remove('active'));
954        thumb.classList.add('active');
955        const src = thumb.getAttribute('data-img');
956        mainImg.src = src;
957        mainImg.alt = thumb.querySelector('img')?.alt || '';
958      };
959      thumb.addEventListener('click', switchImage);
960      thumb.addEventListener('keydown', (e) => {
961        if (e.key === 'Enter' || e.key === ' ') {
962          e.preventDefault();
963          switchImage();
964        }
965      });
966    });
967  });
968}
969
970// --- FLEET LIGHTBOX ---
971function initFleetLightbox() {
972  // Create lightbox DOM
973  const overlay = document.createElement('div');
974  overlay.className = 'lightbox-overlay';
975  overlay.innerHTML = `
976    <button class="lightbox-close" aria-label="Close">&times;</button>
977    <button class="lightbox-nav lightbox-prev" aria-label="Previous"><i class="fas fa-chevron-left"></i></button>
978    <img class="lightbox-img" src="" alt="">
979    <button class="lightbox-nav lightbox-next" aria-label="Next"><i class="fas fa-chevron-right"></i></button>
980    <div class="lightbox-counter"></div>
981  `;
982  document.body.appendChild(overlay);
983
984  const lbImg = overlay.querySelector('.lightbox-img');
985  const lbCounter = overlay.querySelector('.lightbox-counter');
986  let images = [];
987  let currentIdx = 0;
988
989  function open(showcase, startIdx) {
990    // Collect all image srcs from this showcase's thumbs
991    const thumbs = showcase.querySelectorAll('.fleet-thumb');
992    images = Array.from(thumbs).map(t => t.getAttribute('data-img'));
993    if (images.length === 0) return;
994    currentIdx = startIdx || 0;
995    show();
996    overlay.classList.add('active');
997    document.body.style.overflow = 'hidden';
998    // Push history state so mobile back button closes lightbox instead of navigating away
999    history.pushState({ lightbox: true }, '');
1000  }
1001
1002  function close() {
1003    if (!overlay.classList.contains('active')) return;
1004    overlay.classList.remove('active');
1005    document.body.style.overflow = '';
1006  }
1007
1008  // Mobile back button closes lightbox
1009  window.addEventListener('popstate', function(e) {
1010    if (overlay.classList.contains('active')) {
1011      close();
1012    }
1013  });
1014
1015  function show() {
1016    lbImg.style.opacity = '0';
1017    setTimeout(() => {
1018      lbImg.src = images[currentIdx];
1019      lbImg.alt = `Fleet image ${currentIdx + 1} of ${images.length}`;
1020      lbImg.style.opacity = '1';
1021    }, 100);
1022    lbCounter.textContent = `${currentIdx + 1} / ${images.length}`;
1023  }
1024
1025  function next() { currentIdx = (currentIdx + 1) % images.length; show(); }
1026  function prev() { currentIdx = (currentIdx - 1 + images.length) % images.length; show(); }
1027
1028  // Close via history.back() so the pushed state gets popped cleanly
1029  function closeViaHistory() { if (overlay.classList.contains('active')) history.back(); }
1030
1031  overlay.querySelector('.lightbox-close').addEventListener('click', closeViaHistory);
1032  overlay.querySelector('.lightbox-prev').addEventListener('click', prev);
1033  overlay.querySelector('.lightbox-next').addEventListener('click', next);
1034  overlay.addEventListener('click', (e) => { if (e.target === overlay) closeViaHistory(); });
1035
1036  document.addEventListener('keydown', (e) => {
1037    if (!overlay.classList.contains('active')) return;
1038    if (e.key === 'Escape') closeViaHistory();
1039    if (e.key === 'ArrowRight') next();
1040    if (e.key === 'ArrowLeft') prev();
1041  });
1042
1043  // Touch swipe support (single finger only — ignore pinch zoom)
1044  let touchStartX = 0;
1045  let touchFingers = 0;
1046  overlay.addEventListener('touchstart', (e) => {
1047    touchFingers = e.touches.length;
1048    if (touchFingers === 1) touchStartX = e.changedTouches[0].screenX;
1049  }, { passive: true });
1050  overlay.addEventListener('touchend', (e) => {
1051    if (touchFingers !== 1) return; // was a pinch/zoom gesture, skip
1052    const diff = e.changedTouches[0].screenX - touchStartX;
1053    if (Math.abs(diff) > 50) { diff > 0 ? prev() : next(); }
1054  }, { passive: true });
1055
1056  // Bind click on each showcase
1057  document.querySelectorAll('.fleet-showcase').forEach(showcase => {
1058    const mainImg = showcase.querySelector('.fleet-img-main');
1059    if (mainImg) {
1060      mainImg.addEventListener('click', () => {
1061        const thumbs = showcase.querySelectorAll('.fleet-thumb');
1062        const activeSrc = mainImg.querySelector('img')?.src || '';
1063        let idx = 0;
1064        thumbs.forEach((t, i) => {
1065          if (activeSrc.includes(t.getAttribute('data-img'))) idx = i;
1066        });
1067        open(showcase, idx);
1068      });
1069    }
1070    showcase.querySelectorAll('.fleet-thumb').forEach((thumb, i) => {
1071      thumb.addEventListener('dblclick', () => open(showcase, i));
1072    });
1073  });
1074}
1075
1076// --- FLEET CARD THUMBNAILS ---
1077function initFleetCardThumbs() {
1078  document.querySelectorAll('.fleet-card').forEach(card => {
1079    const thumbs = card.querySelectorAll('.fleet-card-thumb');
1080    const mainImg = card.querySelector('.fleet-card-img img');
1081    if (!mainImg || !thumbs.length) return;
1082
1083    thumbs.forEach(thumb => {
1084      thumb.addEventListener('click', (e) => {
1085        e.stopPropagation();
1086        thumbs.forEach(t => t.classList.remove('active'));
1087        thumb.classList.add('active');
1088        mainImg.src = thumb.getAttribute('data-img');
1089        mainImg.alt = thumb.querySelector('img')?.alt || '';
1090      });
1091    });
1092  });
1093}
1094
1095// --- FLEET STRIP MOUSE DRAG SCROLL ---
1096function initFleetDragScroll() {
1097  const strip = document.querySelector('.fleet-strip');
1098  if (!strip) return;
1099
1100  let isDown = false, startX, scrollLeft;
1101
1102  strip.addEventListener('mousedown', (e) => {
1103    if (e.target.closest('.fleet-card-thumb, a, button')) return;
1104    isDown = true;
1105    strip.classList.add('dragging');
1106    startX = e.pageX - strip.offsetLeft;
1107    scrollLeft = strip.scrollLeft;
1108  });
1109  strip.addEventListener('mouseleave', () => { isDown = false; strip.classList.remove('dragging'); });
1110  strip.addEventListener('mouseup', () => { isDown = false; strip.classList.remove('dragging'); });
1111  strip.addEventListener('mousemove', (e) => {
1112    if (!isDown) return;
1113    e.preventDefault();
1114    const x = e.pageX - strip.offsetLeft;
1115    strip.scrollLeft = scrollLeft - (x - startX) * 1.5;
1116  });
1117}
1118
1119// --- FAQ ACCORDION ---
1120function initFAQ() {
1121  document.querySelectorAll('.faq-question').forEach(btn => {
1122    btn.addEventListener('click', () => {
1123      const item = btn.closest('.faq-item');
1124      const wasOpen = item.classList.contains('open');
1125
1126      // Close all others
1127      document.querySelectorAll('.faq-item.open').forEach(openItem => {
1128        openItem.classList.remove('open');
1129        openItem.querySelector('.faq-question').setAttribute('aria-expanded', 'false');
1130      });
1131
1132      // Toggle current
1133      if (!wasOpen) {
1134        item.classList.add('open');
1135        btn.setAttribute('aria-expanded', 'true');
1136      }
1137    });
1138  });
1139}
1140
1141// --- NAVIGATION ---
1142function initNav() {
1143  const toggle = $('#navToggle');
1144  const menu = $('#navMenu');
1145  const navbar = $('#navbar');
1146
1147  // Mobile toggle
1148  toggle.addEventListener('click', () => {
1149    menu.classList.toggle('open');
1150    toggle.classList.toggle('active');
1151  });
1152
1153  // Close menu on link click
1154  menu.querySelectorAll('a').forEach(link => {
1155    link.addEventListener('click', () => {
1156      menu.classList.remove('open');
1157      toggle.classList.remove('active');
1158    });
1159  });
1160
1161  // Scroll effect
1162  window.addEventListener('scroll', () => {
1163    navbar.classList.toggle('scrolled', window.scrollY > 50);
1164  }, { passive: true });
1165}
1166
1167// --- SMOOTH SCROLL FOR ANCHOR LINKS ---
1168function initSmoothScroll() {
1169  document.querySelectorAll('a[href^="#"]').forEach(anchor => {
1170    anchor.addEventListener('click', (e) => {
1171      const target = document.querySelector(anchor.getAttribute('href'));
1172      if (target) {
1173        e.preventDefault();
1174        const topbar = document.querySelector('#topbar');
1175        const navbar = document.querySelector('#navbar');
1176        const offset = (topbar ? topbar.offsetHeight : 0) + (navbar ? navbar.offsetHeight : 0) + 16;
1177        const targetPos = target.getBoundingClientRect().top + window.scrollY - offset;
1178        window.scrollTo({ top: targetPos, behavior: 'smooth' });
1179      }
1180    });
1181  });
1182}
1183
1184// --- INTERSECTION OBSERVER (fade-in animations) ---
1185function initAnimations() {
1186  const observer = new IntersectionObserver((entries) => {
1187    entries.forEach(entry => {
1188      if (entry.isIntersecting) {
1189        entry.target.classList.add('visible');
1190        observer.unobserve(entry.target);
1191      }
1192    });
1193  }, { threshold: 0.1 });
1194
1195  document.querySelectorAll('.service-card, .trust-card, .dest-card, .contact-card, .step-card, .faq-item').forEach(el => {
1196    el.classList.add('animate-on-scroll');
1197    observer.observe(el);
1198  });
1199}
1200
1201// --- BUSY SLOTS (driver updates this manually or via Google Calendar API later) ---
1202const BUSY_SLOTS = [
1203  // { date: '2026-02-23', hours: [8, 9, 10, 11] },
1204];
1205
1206// --- TIME SLOT PICKER (integrated into booking form) ---
1207function renderTimeSlots() {
1208  const container = document.getElementById('timeSlots');
1209  if (!container) return;
1210
1211  const dateVal = tripDate.value;
1212  if (!dateVal) { container.innerHTML = '<span style="color:var(--gray);font-size:0.82rem;">Select a date first</span>'; return; }
1213
1214  const selectedDate = new Date(dateVal + 'T00:00:00');
1215  const now = new Date();
1216
1217  container.innerHTML = '';
1218
1219  for (let hour = 5; hour < 23; hour++) {
1220    const slot = document.createElement('div');
1221    slot.className = 'time-slot';
1222    slot.textContent = `${String(hour).padStart(2, '0')}:00`;
1223
1224    const slotTime = new Date(selectedDate);
1225    slotTime.setHours(hour, 0, 0, 0);
1226
1227    if (slotTime < now) {
1228      slot.classList.add('ts-past');
1229    } else if (BUSY_SLOTS.some(s => s.date === dateVal && s.hours.includes(hour))) {
1230      slot.classList.add('ts-busy');
1231    } else {
1232      slot.classList.add('ts-available');
1233      slot.addEventListener('click', () => {
1234        tripTime.value = `${String(hour).padStart(2, '0')}:00`;
1235        renderTimeSlots();
1236        calculatePrice();
1237      });
1238    }
1239
1240    if (tripTime.value === `${String(hour).padStart(2, '0')}:00`) {
1241      slot.classList.add('ts-selected');
1242    }
1243
1244    container.appendChild(slot);
1245  }
1246}
1247
1248// --- CUSTOM LOCATION TOGGLE ---
1249function initCustomLocation() {
1250  const pickupSel = $('#pickupLocation');
1251  const dropoffSel = $('#dropoffLocation');
1252
1253  function togglePanel(selectEl, panelId, type) {
1254    selectEl.addEventListener('change', () => {
1255      const val = selectEl.value;
1256      const isCustom = val === 'custom';
1257      const hasCoords = !isCustom && val && LOCATION_COORDS[val];
1258      const showPanel = isCustom || hasCoords;
1259      const panel = document.getElementById(panelId);
1260      if (panel) {
1261        panel.style.display = showPanel ? 'block' : 'none';
1262        if (showPanel) {
1263          const mapInstance = type === 'pickup' ? pickupMapInstance : dropoffMapInstance;
1264          if (!mapInstance) initMapPicker(type);
1265          setTimeout(() => {
1266            const mi = type === 'pickup' ? pickupMapInstance : dropoffMapInstance;
1267            if (mi) mi.invalidateSize();
1268          }, 200);
1269
1270          if (hasCoords) {
1271            // Predefined location: show marker + address on map
1272            const coords = LOCATION_COORDS[val];
1273            const addr = LOCATION_ADDRESSES[val] || LOCATION_NAMES[val] || '';
1274            const input = document.getElementById(type === 'pickup' ? 'pickupCustom' : 'dropoffCustom');
1275            if (input) input.value = addr;
1276            setTimeout(() => {
1277              const mi = type === 'pickup' ? pickupMapInstance : dropoffMapInstance;
1278              if (mi && coords) {
1279                let marker = type === 'pickup' ? pickupMarker : dropoffMarker;
1280                if (marker) {
1281                  marker.setLatLng(coords);
1282                } else {
1283                  marker = L.marker(coords).addTo(mi);
1284                  if (type === 'pickup') pickupMarker = marker;
1285                  else dropoffMarker = marker;
1286                }
1287                mi.setView(coords, 14);
1288              }
1289            }, 300);
1290          } else {
1291            // Custom: focus the search input
1292            const input = document.getElementById(type === 'pickup' ? 'pickupCustom' : 'dropoffCustom');
1293            if (input) setTimeout(() => input.focus(), 250);
1294          }
1295        }
1296      }
1297      calculatePrice();
1298    });
1299  }
1300
1301  togglePanel(pickupSel, 'pickupPanel', 'pickup');
1302  togglePanel(dropoffSel, 'dropoffPanel', 'dropoff');
1303
1304  // Address search handlers
1305  initAddressSearch('pickup');
1306  initAddressSearch('dropoff');
1307}
1308
1309// --- ADDRESS SEARCH (forward geocoding) ---
1310function initAddressSearch(type) {
1311  const input = document.getElementById(type === 'pickup' ? 'pickupCustom' : 'dropoffCustom');
1312  const btn = document.getElementById(type + 'SearchBtn');
1313
1314  if (input) {
1315    input.addEventListener('keydown', (e) => {
1316      if (e.key === 'Enter') {
1317        e.preventDefault();
1318        if (input.value.trim().length >= 3) forwardGeocode(input.value, type);
1319      }
1320    });
1321  }
1322  if (btn) {
1323    btn.addEventListener('click', () => {
1324      if (input && input.value.trim().length >= 3) forwardGeocode(input.value, type);
1325    });
1326  }
1327}
1328
1329function forwardGeocode(query, type) {
1330  const input = document.getElementById(type === 'pickup' ? 'pickupCustom' : 'dropoffCustom');
1331  const btn = document.getElementById(type + 'SearchBtn');
1332
1333  if (btn) btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
1334
1335  const lang = currentLang || 'en';
1336
1337  fetch(`https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&q=${encodeURIComponent(query)}&limit=5&countrycodes=it&accept-language=${lang},it,en`)
1338    .then(r => r.json())
1339    .then(data => {
1340      if (btn) btn.innerHTML = '<i class="fas fa-search"></i>';
1341      if (data && data.length > 0) {
1342        const lat = parseFloat(data[0].lat);
1343        const lng = parseFloat(data[0].lon);
1344
1345        // Store structured address for professional labels
1346        if (data[0].address) {
1347          customLocationDetails[type] = data[0].address;
1348        }
1349        // Store Nominatim class/type for airport/port detection
1350        customLocationType[type] = {
1351          class: data[0].class || '',
1352          type: data[0].type || '',
1353          display_name: data[0].display_name || ''
1354        };
1355
1356        // Place/move marker
1357        const map = type === 'pickup' ? pickupMapInstance : dropoffMapInstance;
1358        let marker = type === 'pickup' ? pickupMarker : dropoffMarker;
1359
1360        if (map) {
1361          if (marker) {
1362            marker.setLatLng([lat, lng]);
1363          } else {
1364            marker = L.marker([lat, lng]).addTo(map);
1365            if (type === 'pickup') pickupMarker = marker;
1366            else dropoffMarker = marker;
1367          }
1368          map.setView([lat, lng], 14);
1369        }
1370
1371        if (input) input.value = data[0].display_name;
1372        updateFlightShipField();
1373        calculatePrice();
1374      } else {
1375        // No results - flash red border
1376        if (input) {
1377          input.style.borderColor = '#e74c3c';
1378          setTimeout(() => { input.style.borderColor = ''; }, 2000);
1379        }
1380      }
1381    })
1382    .catch(() => {
1383      if (btn) btn.innerHTML = '<i class="fas fa-search"></i>';
1384    });
1385}
1386
1387// --- DESTINATION PRE-FILL (with map + coords) ---
1388function initDestinationLinks() {
1389  document.querySelectorAll('.dest-book-btn').forEach(btn => {
1390    btn.addEventListener('click', (e) => {
1391      e.preventDefault();
1392      const dest = btn.dataset.dest;
1393      if (!dest || !dropoff) return;
1394
1395      // All destinations use custom location with map + predefined coords
1396      const coords = LOCATION_COORDS[dest];
1397      const address = LOCATION_ADDRESSES[dest] || LOCATION_NAMES[dest] || dest;
1398
1399      // Set dropoff to 'custom' to open map panel
1400      dropoff.value = 'custom';
1401      dropoff.dispatchEvent(new Event('change'));
1402
1403      // Wait for panel to open, then place marker
1404      setTimeout(() => {
1405        const input = $('#dropoffCustom');
1406        if (input) input.value = address;
1407
1408        // Init map if not already
1409        if (!dropoffMapInstance) initMapPicker('dropoff');
1410
1411        setTimeout(() => {
1412          if (coords && dropoffMapInstance) {
1413            // Place/move marker at predefined coords
1414            if (dropoffMarker) {
1415              dropoffMarker.setLatLng(coords);
1416            } else {
1417              dropoffMarker = L.marker(coords).addTo(dropoffMapInstance);
1418            }
1419            dropoffMapInstance.setView(coords, 13);
1420            dropoffMapInstance.invalidateSize();
1421          }
1422          calculatePrice();
1423        }, 300);
1424      }, 250);
1425
1426      // Scroll to calculator section
1427      const calcTitle = document.querySelector('.booking-form-title');
1428      if (calcTitle) {
1429        const topbar = document.querySelector('#topbar');
1430        const navbar = document.querySelector('#navbar');
1431        const offset = (topbar ? topbar.offsetHeight : 0) + (navbar ? navbar.offsetHeight : 0) + 16;
1432        const targetPos = calcTitle.getBoundingClientRect().top + window.scrollY - offset;
1433        window.scrollTo({ top: targetPos, behavior: 'smooth' });
1434      }
1435    });
1436  });
1437}
1438
1439// --- MAP PICKER (Leaflet / OpenStreetMap) ---
1440let pickupMapInstance = null, dropoffMapInstance = null;
1441let pickupMarker = null, dropoffMarker = null;
1442
1443
1444function initMapPicker(type) {
1445  const mapId = type + 'Map';
1446  const mapEl = document.getElementById(mapId);
1447  if (!mapEl || !window.L) return;
1448
1449  const map = L.map(mapId, { tap: false }).setView([41.95, 12.01], 9);
1450
1451  // Street + Satellite layers
1452  const streets = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
1453    attribution: '\u00a9 <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> \u00a9 <a href="https://carto.com/">CARTO</a>',
1454    maxZoom: 19
1455  });
1456  const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
1457    attribution: '\u00a9 Esri',
1458    maxZoom: 19
1459  });
1460  streets.addTo(map);
1461  L.control.layers({ 'Street': streets, 'Satellite': satellite }, null, { position: 'topright' }).addTo(map);
1462
1463  map.on('click', function(e) {
1464    const { lat, lng } = e.latlng;
1465    // If user clicks map while a predefined location is selected, switch to 'custom'
1466    // so calculatePrice uses marker coords instead of fixed price lookup
1467    const sel = type === 'pickup' ? pickup : dropoff;
1468    if (sel.value && sel.value !== 'custom') {
1469      sel.value = 'custom';
1470    }
1471    // Use global marker refs to avoid duplicates
1472    let marker = type === 'pickup' ? pickupMarker : dropoffMarker;
1473    if (marker) {
1474      marker.setLatLng([lat, lng]);
1475    } else {
1476      marker = L.marker([lat, lng]).addTo(map);
1477      if (type === 'pickup') pickupMarker = marker;
1478      else dropoffMarker = marker;
1479    }
1480    reverseGeocode(lat, lng, type);
1481    calculatePrice();
1482  });
1483
1484  if (type === 'pickup') pickupMapInstance = map;
1485  else dropoffMapInstance = map;
1486  setTimeout(() => map.invalidateSize(), 100);
1487}
1488
1489function reverseGeocode(lat, lng, type) {
1490  const input = document.getElementById(type === 'pickup' ? 'pickupCustom' : 'dropoffCustom');
1491  if (!input) return;
1492
1493  const lang = currentLang || 'en';
1494
1495  fetch('https://nominatim.openstreetmap.org/reverse?format=json&lat=' + lat + '&lon=' + lng + '&zoom=18&addressdetails=1', {
1496    headers: { 'Accept-Language': lang + ',it,en' }
1497  })
1498  .then(function(r) { return r.json(); })
1499  .then(function(data) {
1500    if (data.display_name) {
1501      input.value = data.display_name;
1502    }
1503    if (data.address) {
1504      customLocationDetails[type] = data.address;
1505    }
1506    customLocationType[type] = {
1507      class: data.class || '',
1508      type: data.type || '',
1509      display_name: data.display_name || ''
1510    };
1511    updateFlightShipField();
1512  })
1513  .catch(function() {
1514    input.value = lat.toFixed(5) + ', ' + lng.toFixed(5);
1515  });
1516}
1517
1518// --- CLICKABLE PRICING TABLE ROUTES ---
1519function initRouteButtons() {
1520  document.querySelectorAll('.route-row').forEach(row => {
1521    row.addEventListener('click', () => {
1522      const from = row.dataset.from;
1523      const to = row.dataset.to;
1524      if (!from || !to) return;
1525
1526      // Close any open map panels first
1527      const pickupPanel = document.getElementById('pickupPanel');
1528      const dropoffPanel = document.getElementById('dropoffPanel');
1529      if (pickupPanel) pickupPanel.style.display = 'none';
1530      if (dropoffPanel) dropoffPanel.style.display = 'none';
1531
1532      // Set dropdowns
1533      pickup.value = from;
1534      dropoff.value = to;
1535
1536      // Trigger change events for map panels + price
1537      pickup.dispatchEvent(new Event('change'));
1538      dropoff.dispatchEvent(new Event('change'));
1539
1540      // Scroll to calculator section
1541      const calcTitle = document.querySelector('.booking-form-title');
1542      if (calcTitle) {
1543        const topbar = document.querySelector('#topbar');
1544        const navbar = document.querySelector('#navbar');
1545        const offset = (topbar ? topbar.offsetHeight : 0) + (navbar ? navbar.offsetHeight : 0) + 16;
1546        const targetPos = calcTitle.getBoundingClientRect().top + window.scrollY - offset;
1547        window.scrollTo({ top: targetPos, behavior: 'smooth' });
1548      }
1549    });
1550  });
1551}
1552
1553// --- EVENT LISTENERS ---
1554function init() {
1555  initDate();
1556  initNav();
1557  initSmoothScroll();
1558  initFleetGallery();
1559  initFleetLightbox();
1560  initFleetCardThumbs();
1561  initFleetDragScroll();
1562  initFAQ();
1563  initAnimations();
1564  initLanguage();
1565  initCustomLocation();
1566  initDestinationLinks();
1567  initRouteButtons();
1568  initLuggage();
1569  renderTimeSlots();
1570
1571  // Time slots toggle (folded by default)
1572  const tsToggle = document.getElementById('timeSlotsToggle');
1573  const tsPanel = document.getElementById('timeSlotsCollapsible');
1574  if (tsToggle && tsPanel) {
1575    tsToggle.addEventListener('click', function() {
1576      const isOpen = tsPanel.style.display !== 'none';
1577      tsPanel.style.display = isOpen ? 'none' : 'block';
1578      tsToggle.classList.toggle('open', !isOpen);
1579    });
1580  }
1581
1582  // Price recalculation on any change
1583  [pickup, dropoff, passengers, tripTime].forEach(el => {
1584    el.addEventListener('change', calculatePrice);
1585  });
1586  if (childSeatCheckbox) childSeatCheckbox.addEventListener('change', calculatePrice);
1587
1588  // Flight/ship field visibility on location change
1589  pickup.addEventListener('change', updateFlightShipField);
1590  dropoff.addEventListener('change', updateFlightShipField);
1591
1592  // Payment method toggle
1593  initPaymentToggle();
1594
1595  // Re-render time slots when date changes
1596  tripDate.addEventListener('change', renderTimeSlots);
1597
1598  // Dynamic WhatsApp links — update floating/mobile bar buttons with booking data
1599  const DEFAULT_WA_URL = `https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent("Hello! I'd like to book a transfer.")}`;
1600  function updateDynamicWhatsAppLinks() {
1601    const links = document.querySelectorAll('[data-wa-dynamic]');
1602    if (!links.length) return;
1603    // Check if any meaningful booking field is filled
1604    const hasFrom = pickup.value && pickup.value !== '';
1605    const hasTo = dropoff.value && dropoff.value !== '';
1606    const hasDate = tripDate.value && tripDate.value !== '';
1607    if (hasFrom || hasTo || hasDate) {
1608      const msg = buildWhatsAppMessage();
1609      const url = `https://wa.me/${WHATSAPP_NUMBER}?text=${encodeURIComponent(msg)}`;
1610      links.forEach(function(a) { a.href = url; });
1611    } else {
1612      links.forEach(function(a) { a.href = DEFAULT_WA_URL; });
1613    }
1614  }
1615  [pickup, dropoff, passengers, tripDate, tripTime].forEach(function(el) {
1616    el.addEventListener('change', updateDynamicWhatsAppLinks);
1617  });
1618  if (childSeatCheckbox) childSeatCheckbox.addEventListener('change', updateDynamicWhatsAppLinks);
1619  // Also update on custom address input changes
1620  var pickupCustomEl = document.getElementById('pickupCustom');
1621  var dropoffCustomEl = document.getElementById('dropoffCustom');
1622  if (pickupCustomEl) pickupCustomEl.addEventListener('change', updateDynamicWhatsAppLinks);
1623  if (dropoffCustomEl) dropoffCustomEl.addEventListener('change', updateDynamicWhatsAppLinks);
1624
1625  // URL parameter pre-fill (for landing page deep links)
1626  const params = new URLSearchParams(window.location.search);
1627  const paramFrom = params.get('from');
1628  const paramTo = params.get('to');
1629  if (paramFrom || paramTo) {
1630    if (paramFrom) pickup.value = paramFrom;
1631    if (paramTo) dropoff.value = paramTo;
1632    pickup.dispatchEvent(new Event('change'));
1633    dropoff.dispatchEvent(new Event('change'));
1634    setTimeout(() => {
1635      document.getElementById('booking')?.scrollIntoView({ behavior: 'smooth' });
1636    }, 300);
1637  }
1638
1639  // Booking buttons
1640  bookingForm.addEventListener('submit', bookViaWhatsApp);
1641  $('#btnEmail').addEventListener('click', sendEmailInquiry);
1642
1643  // Preview modal close
1644  $('#previewClose').addEventListener('click', closePreview);
1645  $('#previewCancel').addEventListener('click', closePreview);
1646  $('#previewOverlay').addEventListener('click', (e) => {
1647    if (e.target === $('#previewOverlay')) closePreview();
1648  });
1649  document.addEventListener('keydown', (e) => {
1650    if (e.key === 'Escape' && $('#previewOverlay').classList.contains('active')) closePreview();
1651  });
1652}
1653
1654document.addEventListener('DOMContentLoaded', init);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.