PageSourceSearch

https://insidemiamibeach.com/miami/wp-content/themes/insidemiamibeach/js/main.js?ver=1787736469

js insidemiamibeach.com collected 2026-10-07 15:22:51 UTC 44,568 bytes, 1,121 lines download raw bytes

1/**
2 * InsideMiamiBeach 2026 - Main JS v1.5
3 * Zero jQuery.
4 */
5(function () {
6
7  'use strict';
8
9  /* ==========================================================================
10     1. MEGA MENU — stacked rows: L2 row + L3 row
11     ========================================================================== */
12  var mainNav = document.querySelector('.main-nav');
13  if (mainNav) {
14    var navList = mainNav.querySelector('.main-nav__list') || mainNav.querySelector('.menu');
15    var topParents = navList ? navList.querySelectorAll(':scope > .menu-item-has-children') : [];
16    var activeL1 = null;
17    var activeL2 = null;
18
19    function getNavCenter() {
20      var w = mainNav.querySelector('.wrapper');
21      var r = w ? w.getBoundingClientRect() : mainNav.getBoundingClientRect();
22      return r.left + r.width / 2;
23    }
24
25    function positionSub(li, sub) {
26      var liRect = li.getBoundingClientRect();
27      var navCenter = getNavCenter();
28      var subWidth = Math.min(1200, window.innerWidth);
29      sub.style.left = (navCenter - liRect.left - subWidth / 2) + 'px';
30      sub.style.width = subWidth + 'px';
31      sub.style.transform = 'none';
32    }
33
34    function closeL3() {
35      if (activeL2) {
36        activeL2.classList.remove('is-open');
37        activeL2 = null;
38      }
39      mainNav.querySelectorAll('.sub-menu > li.is-highlight').forEach(function (el) {
40        el.classList.remove('is-highlight');
41      });
42    }
43
44    function closeL2() {
45      closeL3();
46      if (activeL1) {
47        activeL1.classList.remove('is-open');
48        activeL1 = null;
49      }
50    }
51
52    topParents.forEach(function (li) {
53      var link = li.querySelector(':scope > a');
54      var sub = li.querySelector(':scope > .sub-menu');
55      if (!link || !sub) return;
56
57      link.addEventListener('click', function (e) {
58        var href = link.getAttribute('href');
59        if (!href || href === '#' || href === '#0') e.preventDefault();
60
61        if (activeL1 === li) {
62          closeL2();
63        } else {
64          closeL2();
65          li.classList.add('is-open');
66          positionSub(li, sub);
67          activeL1 = li;
68
69          var subParents = sub.querySelectorAll(':scope > li.menu-item-has-children');
70          subParents.forEach(function (subLi) {
71            var subLink = subLi.querySelector(':scope > a');
72            var subSub = subLi.querySelector(':scope > .sub-menu');
73            if (!subLink || !subSub) return;
74
75            var newLink = subLink.cloneNode(true);
76            subLink.parentNode.replaceChild(newLink, subLink);
77
78            newLink.addEventListener('click', function (ev) {
79              ev.preventDefault();
80              ev.stopPropagation();
81
82              if (activeL2 === subLi) {
83                closeL3();
84              } else {
85                closeL3();
86                subLi.classList.add('is-highlight');
87                subLi.classList.add('is-open');
88                positionSub(subLi, subSub);
89                activeL2 = subLi;
90              }
91            });
92          });
93        }
94      });
95    });
96
97    document.addEventListener('click', function (e) {
98      if (activeL1 && !mainNav.contains(e.target)) closeL2();
99    });
100  }
101
102
103  /* ==========================================================================
104     2. HERO SLIDER
105     ========================================================================== */
106  var slider = document.getElementById('hero-slider');
107  var track = document.getElementById('hero-slider-track');
108  var prevBtn = document.getElementById('slider-prev');
109  var nextBtn = document.getElementById('slider-next');
110  var dotsWrap = document.getElementById('slider-dots');
111
112  if (slider && track) {
113    var slides = track.children;
114    var total = slides.length;
115    var current = 0;
116    var autoTimer = null;
117    var isDragging = false;
118    var startX = 0;
119    var prevTranslate = 0;
120    var dragThreshold = 50;
121    var sliderWidth = 0; // cached at dragStart to avoid forced reflow in dragMove
122
123    function goTo(index, smooth) {
124      if (index < 0) index = total - 1;
125      if (index >= total) index = 0;
126      current = index;
127      var offset = -(current * (100 / total));
128      track.style.transition = smooth !== false ? 'transform .5s ease' : 'none';
129      track.style.transform = 'translateX(' + offset + '%)';
130      prevTranslate = offset;
131      updateDots();
132    }
133    function updateDots() {
134      if (!dotsWrap) return;
135      var dots = dotsWrap.children;
136      for (var i = 0; i < dots.length; i++) dots[i].classList.toggle('is-active', i === current);
137    }
138    function startAuto() { stopAuto(); autoTimer = setInterval(function () { goTo(current + 1); }, 5000); }
139    function stopAuto() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
140
141    if (prevBtn) prevBtn.addEventListener('click', function () { stopAuto(); goTo(current - 1); startAuto(); });
142    if (nextBtn) nextBtn.addEventListener('click', function () { stopAuto(); goTo(current + 1); startAuto(); });
143    if (dotsWrap) dotsWrap.addEventListener('click', function (e) {
144      var dot = e.target.closest('.hero-slider__dot');
145      if (dot) { stopAuto(); goTo(parseInt(dot.dataset.index)); startAuto(); }
146    });
147
148    function getX(e) { return e.type.indexOf('touch') >= 0 ? e.touches[0].clientX : e.clientX; }
149    function dragStart(e) { isDragging = true; startX = getX(e); sliderWidth = slider.offsetWidth; stopAuto();
149 track.style.transition = 'none'; }
150    function dragMove(e) {
151      if (!isDragging) return;
152      var pctDiff = ((getX(e) - startX) / sliderWidth) * (100 / total);
153      track.style.transform = 'translateX(' + (prevTranslate + pctDiff) + '%)';
154    }
155    function dragEnd(e) {
156      if (!isDragging) return;
157      isDragging = false;
158      var endX = e.type.indexOf('touch') >= 0 ? e.changedTouches[0].clientX : e.clientX;
159      var diff = endX - startX;
160      if (diff < -dragThreshold) goTo(current + 1);
161      else if (diff > dragThreshold) goTo(current - 1);
162      else goTo(current);
163      startAuto();
164    }
165
166    track.addEventListener('mousedown', dragStart);
167    track.addEventListener('mousemove', dragMove);
168    track.addEventListener('mouseup', dragEnd);
169    track.addEventListener('mouseleave', function () { if (isDragging) dragEnd({ clientX: startX, type: 'mouse' }); });
170    track.addEventListener('touchstart', dragStart, { passive: true });
171    track.addEventListener('touchmove', dragMove, { passive: true });
172    track.addEventListener('touchend', dragEnd);
173    track.addEventListener('dragstart', function (e) { e.preventDefault(); });
174
175    track.style.animation = 'none';
176    track.style.width = (total * 100) + '%';
177    for (var s = 0; s < total; s++) slides[s].style.width = (100 / total) + '%';
178    goTo(0, false);
179    startAuto();
180    slider.addEventListener('mouseenter', stopAuto);
181    slider.addEventListener('mouseleave', startAuto);
182  }
183
184
185  /* ==========================================================================
186     3. MOBILE MENU
187     ========================================================================== */
188  var toggle = document.querySelector('.mobile-menu-toggle');
189  var offcanvas = document.getElementById('offcanvas-menu');
190  var overlay = document.getElementById('offcanvas-overlay');
191  var closeBtn = offcanvas ? offcanvas.querySelector('.offcanvas__close') : null;
192
193  function openMenu() {
194    if (!offcanvas) return;
195    offcanvas.classList.add('is-open');
196    offcanvas.setAttribute('aria-hidden', 'false');
197    overlay.classList.add('is-open');
198    toggle.setAttribute('aria-expanded', 'true');
199    document.body.style.overflow = 'hidden';
200  }
201  function closeMenu() {
202    if (!offcanvas) return;
203    offcanvas.classList.remove('is-open');
204    offcanvas.setAttribute('aria-hidden', 'true');
205    overlay.classList.remove('is-open');
206    toggle.setAttribute('aria-expanded', 'false');
207    document.body.style.overflow = '';
208  }
209  if (toggle) toggle.addEventListener('click', function () {
210    offcanvas.classList.contains('is-open') ? closeMenu() : openMenu();
211  });
212  if (overlay) overlay.addEventListener('click', closeMenu);
213  if (closeBtn) closeBtn.addEventListener('click', closeMenu);
214
215  var mobileParents = offcanvas ? offcanvas.querySelectorAll('.menu-item-has-children > a') : [];
216  mobileParents.forEach(function (link) {
217    link.addEventListener('click', function (e) {
218      var sub = this.closest('li').querySelector('.sub-menu');
219      if (sub) {
220        e.preventDefault();
221        sub.classList.toggle('is-open');
222        this.classList.toggle('is-open');
223      }
224    });
225  });
226
227
228  /* ==========================================================================
229     3b. LANGUAGE POPUP
230     ========================================================================== */
231  var langTrigger  = document.querySelector('.lang-trigger');
232  var langPopup    = document.getElementById('lang-popup');
233  // Move popup to <body> so position:fixed works correctly regardless of parent transforms
234  if (langPopup) document.body.appendChild(langPopup);
235  var langClose    = langPopup ? langPopup.querySelector('.lang-popup__close') : null;
236  var langBackdrop = langPopup ? langPopup.querySelector('.lang-popup__backdrop') : null;
237
238  function openLang() {
239    if (!langPopup) return;
240    langPopup.classList.add('is-open');
241    langPopup.setAttribute('aria-hidden', 'false');
242    if (langTrigger) langTrigger.setAttribute('aria-expanded', 'true');
243    document.body.style.overflow = 'hidden';
244  }
245  function closeLang() {
246    if (!langPopup) return;
247    langPopup.classList.remove('is-open');
248    langPopup.setAttribute('aria-hidden', 'true');
249    if (langTrigger) langTrigger.setAttribute('aria-expanded', 'false');
250    document.body.style.overflow = '';
251  }
252  if (langTrigger) langTrigger.addEventListener('click', openLang);
253  if (langClose)   langClose.addEventListener('click', closeLang);
254  if (langBackdrop) langBackdrop.addEventListener('click', closeLang);
255
256  /* ==========================================================================
257     3c. SEARCH OVERLAY TOGGLE
258     ========================================================================== */
259  var searchToggle = document.querySelector('.search-toggle');
260  var searchOverlay = document.getElementById('search-overlay');
261  var searchClose = document.getElementById('search-overlay-close');
262  if (searchToggle && searchOverlay) {
263    searchToggle.addEventListener('click', function (e) {
264      e.preventDefault();
265      searchOverlay.classList.add('is-open');
266      document.body.style.overflow = 'hidden';
267      var input = searchOverlay.querySelector('input[type="search"]');
268      if (input) setTimeout(function () { input.focus(); }, 100);
269    });
270    if (searchClose) searchClose.addEventListener('click', function () {
271      searchOverlay.classList.remove('is-open');
272      document.body.style.overflow = '';
273    });
274    searchOverlay.addEventListener('click', function (e) {
275      if (e.target === searchOverlay) {
276        searchOverlay.classList.remove('is-open');
277        document.body.style.overflow = '';
278      }
279    });
280  }
281
282
283  /* ==========================================================================
284     3c. ACCORDION TOGGLE
285     ========================================================================== */
286  document.querySelectorAll('.su-spoiler-title').forEach(function (title) {
287    title.addEventListener('click', function () {
288      var spoiler = this.closest('.su-spoiler');
289      if (!spoiler) return;
290      if (spoiler.classList.contains('su-spoiler-open')) {
291        spoiler.classList.remove('su-spoiler-open');
292        spoiler.classList.add('su-spoiler-closed');
293      } else {
294        spoiler.classList.remove('su-spoiler-closed');
295        spoiler.classList.add('su-spoiler-open');
296      }
297    });
298  });
299
300
301  /* ==========================================================================
302     3d. GALLERY CAROUSEL — 3-col desktop / 1-col mobile, real-time swipe
303     ========================================================================== */
304
305  function makeCarousel(container, items, trackEl) {
306    var pos = 0;
307    var total = items.length;
308    var touching = false;
309    var touchStartX = 0;
310    var baseOffset = 0;
311    var cachedContainerWidth = 0; // cached at touchstart to avoid forced reflow in touchmove
312
313    function getVisible() { return window.innerWidth <= 768 ? 1 : 3; }
314
315    function clamp(p) {
316      return Math.max(0, Math.min(p, Math.max(0, total - getVisible())));
317    }
318
319    function setPos(p, animate) {
320      pos = clamp(p);
321      var vis = getVisible();
322      trackEl.style.transition = animate ? 'transform .35s cubic-bezier(.25,.46,.45,.94)' : 'none';
323      baseOffset = -(pos * (100 / vis));
324      trackEl.style.transform = 'translateX(' + baseOffset + '%)';
325    }
326
327    // Nav arrows
328    var btnPrev = document.createElement('button');
329    btnPrev.className = 'gallery-nav gallery-nav--prev';
330    btnPrev.setAttribute('aria-label', 'Previous');
331    btnPrev.innerHTML = '&#8249;';
332    var btnNext = document.createElement('button');
333    btnNext.className = 'gallery-nav gallery-nav--next';
334    btnNext.setAttribute('aria-label', 'Next');
335    btnNext.innerHTML = '&#8250;';
336    container.appendChild(btnPrev);
337    container.appendChild(btnNext);
338    btnPrev.addEventListener('click', function () { setPos(pos - 1, true); });
339    btnNext.addEventListener('click', function () { setPos(pos + 1, true); });
340
341    // Real-time touch drag
342    container.addEventListener('touchstart', function (e) {
343      touching = true;
344      touchStartX = e.touches[0].clientX;
345      cachedContainerWidth = container.offsetWidth; // read layout once, before any style changes
346      trackEl.style.transition = 'none';
347    }, { passive: true });
348
349    container.addEventListener('touchmove', function (e) {
350      if (!touching) return;
351      var diff = e.touches[0].clientX - touchStartX;
352      var vis = getVisible();
353      var dragPct = (diff / cachedContainerWidth) * 100;
354      trackEl.style.transform = 'translateX(' + (baseOffset + dragPct) + '%)';
355    }, { passive: true });
356
357    container.addEventListener('touchend', function (e) {
358      if (!touching) return;
359      touching = false;
360      var diff = e.changedTouches[0].clientX - touchStartX;
361      if (diff < -50) setPos(pos + 1, true);
362      else if (diff > 50) setPos(pos - 1, true);
363      else setPos(pos, true);
364    });
365
366    setPos(0, false);
367    window.addEventListener('resize', function () { setPos(pos, false); });
368  }
369
370  // su-image-carousel (Ultimate Shortcodes)
371  document.querySelectorAll('.su-image-carousel').forEach(function (carousel) {
372    if (carousel.flickity) { try { carousel.flickity('destroy'); } catch (e) {} }
373
374    var flickViewport = carousel.querySelector('.flickity-viewport');
375    if (flickViewport) {
376      var flickSlider = flickViewport.querySelector('.flickity-slider');
377      if (flickSlider) {
378        while (flickSlider.firstChild) carousel.insertBefore(flickSlider.firstChild, flickViewport);
379      }
380      flickViewport.remove();
381      carousel.querySelectorAll('.flickity-page-dots, .flickity-prev-next-button').forEach(function (el) { el.remove(); });
382    }
383
384    var items = Array.from(carousel.querySelectorAll('.su-image-carousel-item'));
385    if (!items.length) return;
386
387    var trackEl = document.createElement('div');
388    trackEl.className = 'su-image-carousel-inner';
389    items.forEach(function (item) { trackEl.appendChild(item); });
390    carousel.insertBefore(trackEl, carousel.firstChild);
391    carousel.classList.add('imb-carousel-ready');
392
393    makeCarousel(carousel, items, trackEl);
394  });
395
396  // imb-gallery (our own shortcode)
397  document.querySelectorAll('.imb-gallery').forEach(function (gallery) {
398    var items = Array.from(gallery.querySelectorAll('.imb-gallery__item'));
399    if (!items.length) return;
400
401    var trackEl = document.createElement('div');
402    trackEl.className = 'imb-gallery__track';
403    items.forEach(function (item) { trackEl.appendChild(item); });
404    gallery.insertBefore(trackEl, gallery.firstChild);
405
406    makeCarousel(gallery, items, trackEl);
407  });
408
409
410  /* ==========================================================================
411     3e. BOOKING BOX TOGGLE (called from meta box inline onclick)
412     ========================================================================== */
413  // Booking form — intercept submit, build Stay22 URL explicitly
414  var bkForm = document.getElementById('frm_7738');
415  if (bkForm) {
416    bkForm.addEventListener('submit', function (e) {
417      e.preventDefault();
418      var checkin  = document.getElementById('checkin_date')  ? document.getElementById('checkin_date').value  : '';
419      var checkout = document.getElementById('checkout_date') ? document.getElementById('checkout_date').value : '';
420      if (!checkin || !checkout) {
421        alert('Please select check-in and check-out dates.');
422        return;
423      }
424      if (checkin >= checkout) {
425        alert('Check-out date must be after check-in date.');
426        return;
427      }
428      var get = function (name) {
429        var el = bkForm.querySelector('input[name="' + name + '"]');
430        return el ? el.value : '';
431      };
432      var lat = get('lat');
433      var lng = get('lng');
434      var url = 'https://www.stay22.com/allez/booking'
435        + '?aid='       + encodeURIComponent(get('aid'))
436        + '&hotelname=' + encodeURIComponent(get('hotelname'))
437        + '&address='   + encodeURIComponent(get('address'));
438      if (lat && lng) {
439        url += '&lat=' + encodeURIComponent(lat) + '&lng=' + encodeURIComponent(lng);
440      }
441      url += '&checkin='  + encodeURIComponent(checkin)
442           + '&checkout=' + encodeURIComponent(checkout);
443      window.open(url, '_blank');
444    });
445  }
446
447  window.toggleBookingSection = function () {
448    if (window.innerWidth > 768) return; // desktop: always visible via CSS
449    var section = document.getElementById('booking-section');
450    var btn     = document.querySelector('.book-now-button');
451    if (!section) return;
452    var hidden = section.style.display === 'none' || section.style.display === '';
453    section.style.display = hidden ? 'block' : 'none';
454    if (btn) btn.style.display = hidden ? 'none' : 'block';
455  };
456
457  // Init booking box: mobile only needs explicit state
458  (function () {
459    var section = document.getElementById('booking-section');
460    var btn     = document.querySelector('.book-now-button');
461    if (!section) return;
462    if (window.innerWidth <= 768) {
463      section.style.display = 'none';
464      if (btn) btn.style.display = 'block';
465    }
466  })();
467
468
469  /* ==========================================================================
470     3f. IMB DATEPICKER — custom two-month calendar for booking bar
471     ========================================================================== */
472  (function () {
473    var checkinDate  = null;
474    var checkoutDate = null;
475    var activeField  = null;
476    var viewYear     = 0;
477    var viewMonth    = 0;
478    var dp           = null;
479    var slideDir     = null; // 'left' or 'right' for swipe animation
480
481    var MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
482    var DAYS   = ['M','T','W','T','F','S','S'];
483
484    function pad(n) { return n < 10 ? '0' + n : '' + n; }
485    function toISO(d) { return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); }
486    function fmtDisp(d) {
487      var days  = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
488      var months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
489      return days[d.getDay()] + ', ' + months[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear();
490    }
491    function sameDay(a, b) {
492      return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
493    }
494
495    function buildMonthCol(year, month, showPrev, showNext) {
496      var col = document.createElement('div');
497      col.className = 'imb-dp-col';
498
499      var nav = document.createElement('div');
500      nav.className = 'imb-dp-nav';
501
502      var prevBtn = document.createElement('button');
503      prevBtn.type = 'button';
504      prevBtn.className = 'imb-dp-nav-btn';
505      if (showPrev) {
506        prevBtn.innerHTML = '&#8249;';
507        prevBtn.addEventListener('click', function (e) {
508          e.stopPropagation();
509          slideDir = 'right';
510          viewMonth--;
511          if (viewMonth < 0) { viewMonth = 11; viewYear--; }
512          buildCalendar();
513        });
514      } else { prevBtn.style.visibility = 'hidden'; }
515
516      var title = document.createElement('div');
517      title.className = 'imb-dp-month-name';
518      title.textContent = MONTHS[month] + ' ' + year;
519
520      var nextBtn = document.createElement('button');
521      nextBtn.type = 'button';
522      nextBtn.className = 'imb-dp-nav-btn';
523      if (showNext) {
524        nextBtn.innerHTML = '&#8250;';
525        nextBtn.addEventListener('click', function (e) {
526          e.stopPropagation();
527          slideDir = 'left';
528          viewMonth++;
529          if (viewMonth > 11) { viewMonth = 0; viewYear++; }
530          buildCalendar();
531        });
532      } else { nextBtn.style.visibility = 'hidden'; }
533
534      nav.appendChild(prevBtn);
535      nav.appendChild(title);
536      nav.appendChild(nextBtn);
537      col.appendChild(nav);
538
539      var grid = document.createElement('div');
540      grid.className = 'imb-dp-grid';
541      DAYS.forEach(function (d) {
542        var dh = document.createElement('div');
543        dh.className = 'imb-dp-dh';
544        dh.textContent = d;
545        grid.appendChild(dh);
546      });
547
548      var today = new Date();
549      today.setHours(0, 0, 0, 0);
550      var startDow = (new Date(year, month, 1).getDay() + 6) % 7; // Mon=0
551      for (var i = 0; i < startDow; i++) {
552        var empty = document.createElement('div');
553        empty.className = 'imb-dp-cell';
554        grid.appendChild(empty);
555      }
556
557      var daysInMonth = new Date(year, month + 1, 0).getDate();
558      for (var day = 1; day <= daysInMonth; day++) {
559        var cellDate = new Date(year, month, day);
560        var iso = toISO(cellDate);
561        var cell = document.createElement('div');
562        cell.className = 'imb-dp-cell imb-dp-day';
563        cell.textContent = day;
564        cell.dataset.date = iso;
565
566        if (cellDate < today) {
567          cell.classList.add('is-past');
568        } else {
569          if (sameDay(cellDate, today))       cell.classList.add('is-today');
570          if (sameDay(cellDate, checkinDate))  cell.classList.add('is-selected', 'is-checkin', 'is-range-start');
571          if (sameDay(cellDate, checkoutDate)) cell.classList.add('is-selected', 'is-checkout', 'is-range-end');
572          if (checkinDate && checkoutDate && cellDate > checkinDate && cellDate < checkoutDate) cell.classList.add('is-range');
573          cell.addEventListener('click', handleDayClick);
574          cell.addEventListener('mouseenter', handleDayHover);
575          cell.addEventListener('mouseleave', clearHover);
576        }
577        grid.appendChild(cell);
578      }
579
580      col.appendChild(grid);
581      return col;
582    }
583
584    function buildInner() {
585      var isMobile   = window.innerWidth <= 768;
586      var monthCount = isMobile ? 1 : 2;
587      var inner = document.createElement('div');
588      inner.className = 'imb-dp-inner' + (slideDir ? ' imb-dp-slide-' + slideDir : '');
589      slideDir = null;
590      for (var m = 0; m < monthCount; m++) {
591        var abs = viewMonth + m;
592        var cy  = viewYear + Math.floor(abs / 12);
593        var cm  = ((abs % 12) + 12) % 12;
594        inner.appendChild(buildMonthCol(cy, cm, m === 0, m === monthCount - 1));
595      }
596      return inner;
597    }
598
599    function buildCalendar() {
600      var isMobile = window.innerWidth <= 768;
601
602      // If dp already exists, just swap the inner — no flash
603      if (dp) {
604        var oldInner = dp.querySelector('.imb-dp-inner');
605        if (oldInner) oldInner.remove();
606        // Update header label if on mobile
607        var lbl = dp.querySelector('.imb-dp-header-label');
608        if (lbl) lbl.textContent = activeField === 'checkout' ? 'Select check-out date' : 'Select check-in date';
609        dp.appendChild(buildInner());
610        return;
611      }
612
613      // First open — create everything
614      dp = document.createElement('div');
615      dp.className = 'imb-datepicker';
616
617      if (isMobile) {
618        var header = document.createElement('div');
619        header.className = 'imb-dp-header';
620        var label = document.createElement('span');
621        label.className = 'imb-dp-header-label';
622        label.textContent = activeField === 'checkout' ? 'Select check-out date' : 'Select check-in date';
623        var closeBtn = document.createElement('button');
624        closeBtn.type = 'button';
625        closeBtn.className = 'imb-dp-close-btn';
626        closeBtn.innerHTML = '&times;';
627        closeBtn.addEventListener('click', function(e) { e.stopPropagation(); closePicker(); });
628        header.appendChild(label);
629        header.appendChild(closeBtn);
630        dp.appendChild(header);
631
632        dp.style.top       = '0';
633        dp.style.bottom    = '0';
634        dp.style.maxHeight = 'none';
635        dp.style.overflowY = 'auto';
636
637        // Swipe left = next month, swipe right = prev month
638        var swipeStartX = 0;
639        var swipeStartY = 0;
640        dp.addEventListener('touchstart', function(e) {
641          swipeStartX = e.touches[0].clientX;
642          swipeStartY = e.touches[0].clientY;
643        }, { passive: true });
644        dp.addEventListener('touchend', function(e) {
645          var dx = e.changedTouches[0].clientX - swipeStartX;
646          var dy = e.changedTouches[0].clientY - swipeStartY;
647          if (Math.abs(dx) < 50 || Math.abs(dx) < Math.abs(dy)) return;
648          if (dx < 0) {
649            slideDir = 'left';
650            viewMonth++; if (viewMonth > 11) { viewMonth = 0; viewYear++; }
651          } else {
652            slideDir = 'right';
653            viewMonth--; if (viewMonth < 0) { viewMonth = 11; viewYear--; }
654          }
655          buildCalendar();
656        }, { passive: true });
657
658        var backdrop = document.getElementById('imb-dp-backdrop');
659        if (!backdrop) {
660          backdrop = document.createElement('div');
661          backdrop.className = 'imb-dp-backdrop';
662          backdrop.id = 'imb-dp-backdrop';
663          backdrop.addEventListener('click', closePicker);
664          document.body.appendChild(backdrop);
665        }
666        setTimeout(function () { backdrop.classList.add('is-open'); }, 10);
667      }
668
669      dp.appendChild(buildInner());
670      document.body.appendChild(dp);
671      setTimeout(function () { if (dp) dp.classList.add('is-open'); }, 10);
672    }
673
674    function clearHover() {
675      if (!dp) return;
676      dp.querySelectorAll('.is-hover-range,.is-hover-end').forEach(function (c) {
677        c.classList.remove('is-hover-range', 'is-hover-end');
678      });
679    }
680
681    function handleDayHover() {
682      if (activeField !== 'checkout' || !checkinDate || !dp) return;
683      var p   = this.dataset.date.split('-');
684      var hov = new Date(+p[0], +p[1] - 1, +p[2]);
685      clearHover();
686      if (hov <= checkinDate) return;
687      dp.querySelectorAll('.imb-dp-day:not(.is-past)').forEach(function (c) {
688        var cp = c.dataset.date.split('-');
689        var cd = new Date(+cp[0], +cp[1] - 1, +cp[2]);
690        if (cd > checkinDate && cd < hov)          c.classList.add('is-hover-range');
691        if (cd.getTime() === hov.getTime()) c.classList.add('is-hover-end');
692      });
693    }
694
695    function handleDayClick(e) {
696      e.stopPropagation();
697      var iso  = this.dataset.date;
698      var p    = iso.split('-');
699      var sel  = new Date(+p[0], +p[1] - 1, +p[2]);
700      var ciEl = document.getElementById('checkin_display');
701      var coEl = document.getElementById('checkout_display');
702      var ciIn = document.getElementById('checkin_date');
703      var coIn = document.getElementById('checkout_date');
704
705      if (activeField === 'checkin') {
706        checkinDate = sel;
707        if (checkoutDate && checkoutDate <= checkinDate) {
708          checkoutDate = null;
709          if (coEl) { coEl.textContent = 'Check-out date'; coEl.classList.add('is-placeholder'); }
710          if (coIn) coIn.value = '';
711        }
712        if (ciEl) { ciEl.textContent = fmtDisp(sel); ciEl.classList.remove('is-placeholder', 'is-active'); }
713        if (ciIn) ciIn.value = iso;
714        // Auto-switch to checkout
715        activeField = 'checkout';
716        if (coEl) coEl.classList.add('is-active');
717        buildCalendar();
718
719      } else {
720        // If user clicks same or earlier date than checkin, restart from that date as new checkin
721        if (checkinDate && sel <= checkinDate) {
722          checkinDate = sel;
723          checkoutDate = null;
724          if (ciEl) { ciEl.textContent = fmtDisp(sel); ciEl.classList.remove('is-placeholder'); }
725          if (ciIn) ciIn.value = iso;
726          if (coEl) { coEl.textContent = 'Check-out date'; coEl.classList.add('is-placeholder'); }
727          if (coIn) coIn.value = '';
728          activeField = 'checkout';
729          buildCalendar();
730          return;
731        }
732        checkoutDate = sel;
733        if (coEl) { coEl.textContent = fmtDisp(sel); coEl.classList.remove('is-placeholder', 'is-active'); }
734        if (coIn) coIn.value = iso;
735        // Show full range highlighted for a moment before closing
736        activeField = null;
737        buildCalendar();
738        setTimeout(closePicker, 600);
739      }
740    }
741
742    function closePicker() {
743      if (dp) { dp.remove(); dp = null; }
744      var backdrop = document.getElementById('imb-dp-backdrop');
745      if (backdrop) backdrop.remove();
746      var ciEl = document.getElementById('checkin_display');
747      var coEl = document.getElementById('checkout_display');
748      if (ciEl) ciEl.classList.remove('is-active');
749      if (coEl) coEl.classList.remove('is-active');
750      activeField = null;
751    }
752
753    function openPicker(field) {
754      var now = new Date();
755      activeField = field;
756      // When opening checkout, start view at checkin month so user sees context
757      var ref = (field === 'checkout' && checkinDate) ? checkinDate
758              : (checkinDate ? checkinDate : now);
759      viewYear  = ref.getFullYear();
760      viewMonth = ref.getMonth();
761      var ciEl = document.getElementById('checkin_display');
762      var coEl = document.getElementById('checkout_display');
763      if (ciEl) ciEl.classList.toggle('is-active', field === 'checkin');
764      if (coEl) coEl.classList.toggle('is-active', field === 'checkout');
765      buildCalendar();
766    }
767
768    function initPicker() {
769      var ciEl = document.getElementById('checkin_display');
770      var coEl = document.getElementById('checkout_display');
771      if (!ciEl || !coEl) return;
772
773      ciEl.addEventListener('click', function (e) {
774        e.stopPropagation();
775        if (dp && activeField === 'checkin') { closePicker(); return; }
776        openPicker('checkin');
777      });
778      coEl.addEventListener('click', function (e) {
779        e.stopPropagation();
780        if (dp && activeField === 'checkout') { closePicker(); return; }
781        openPicker('checkout');
782      });
783      [ciEl, coEl].forEach(function (el) {
784        el.addEventListener('keydown', function (e) {
785          if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); el.click(); }
786        });
787      });
788    }
789
790    // Close on outside click
791    document.addEventListener('click', function (e) {
792      if (!dp) return;
793      var ciEl = document.getElementById('checkin_display');
794      var coEl = document.getElementById('checkout_display');
795      if (dp.contains(e.target)) return;
796      if (ciEl && ciEl.contains(e.target)) return;
797      if (coEl && coEl.contains(e.target)) return;
798      closePicker();
799    });
800
801    // Close on Escape
802    document.addEventListener('keydown', function (e) {
803      if (e.key === 'Escape' && dp) closePicker();
804    });
805
806    initPicker();
807  })();
808
809
810  /* ==========================================================================
811     4. BACK TO TOP / SLIDE DOCK / BOOKING BAR
812     ========================================================================== */
813  var backTop = document.querySelector('.back-to-top');
814  if (backTop) backTop.addEventListener('click', function (e) { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }); });
815
816  var dock = document.getElementById('slide-dock');
817  var dockClose = document.getElementById('slide-dock-close');
818  var dockShown = false;
819  if (dock) {
820    // Cache scrollHeight once (reading it inside scroll handler causes forced reflow)
821    var dockScrollThreshold = Math.max(600, Math.round((document.body.scrollHeight - window.innerHeight) * 0.35));
822    window.addEventListener('scroll', function () {
823      if (dockShown) return;
824      if (window.scrollY > dockScrollThreshold) { dock.classList.add('is-visible'); dockShown = true; }
825    }, { passive: true });
826    if (dockClose) dockClose.addEventListener('click', function () { dock.classList.remove('is-visible'); });
827  }
828
829  var bookingBar = document.getElementById('booking-bar');
830  if (bookingBar) {
831    var barShown = false;
832    window.addEventListener('scroll', function () {
833      if (barShown) return;
834      if (window.scrollY > 400) { bookingBar.classList.add('is-visible'); barShown = true; }
835    }, { passive: true });
836  }
837
838  document.addEventListener('keydown', function (e) {
839    if (e.key === 'Escape') {
840      if (offcanvas && offcanvas.classList.contains('is-open')) closeMenu();
841      if (mainNav && activeL1) closeL2();
842      if (searchOverlay && searchOverlay.classList.contains('is-open')) { searchOverlay.classList.remove('is-open'); document.body.style.overflow = ''; }
843      if (langPopup && langPopup.classList.contains('is-open')) closeLang();
844      closeLightbox();
845    }
846    if (lightboxEl && lightboxEl.classList.contains('is-open')) {
847      if (e.key === 'ArrowLeft') showImage(lbIndex - 1);
848      if (e.key === 'ArrowRight') showImage(lbIndex + 1);
849    }
850  });
851
852
853  /* ==========================================================================
854     5. LIGHTBOX — single clean implementation
855     ========================================================================== */
856  var lightboxEl = null;
857  var lbImages = [];
858  var lbIndex = 0;
859
860  function createLightbox() {
861    if (lightboxEl) return;
862    lightboxEl = document.createElement('div');
863    lightboxEl.className = 'imb-lightbox';
864    lightboxEl.innerHTML =
865      '<button class="imb-lightbox__close" aria-label="Close">&times;</button>'
866      + '<button class="imb-lightbox__prev" aria-label="Previous">&#8249;</button>'
867      + '<img class="imb-lightbox__img" src="" alt="" draggable="false">'
868      + '<button class="imb-lightbox__next" aria-label="Next">&#8250;</button>'
869      + '<div class="imb-lightbox__counter"></div>';
870    document.body.appendChild(lightboxEl);
871
872    lightboxEl.querySelector('.imb-lightbox__close').addEventListener('click', closeLightbox);
873    lightboxEl.querySelector('.imb-lightbox__prev').addEventListener('click', function (e) { e.stopPropagation(); showImage(lbIndex - 1); });
874    lightboxEl.querySelector('.imb-lightbox__next').addEventListener('click', function (e) { e.stopPropagation(); showImage(lbIndex + 1); });
875    lightboxEl.addEventListener('click', function (e) { if (e.target === lightboxEl) closeLightbox(); });
876
877    // Swipe on the whole overlay (not just the image)
878    var lbTouchStart = 0;
879    lightboxEl.addEventListener('touchstart', function (e) {
880      lbTouchStart = e.touches[0].clientX;
881    }, { passive: true });
882    lightboxEl.addEventListener('touchend', function (e) {
883      var diff = e.changedTouches[0].clientX - lbTouchStart;
884      if (diff > 50) showImage(lbIndex - 1);
885      else if (diff < -50) showImage(lbIndex + 1);
886    });
887  }
888
889  function showImage(idx) {
890    if (!lightboxEl || !lbImages.length) return;
891    if (idx < 0) idx = lbImages.length - 1;
892    if (idx >= lbImages.length) idx = 0;
893    lbIndex = idx;
894    var img = lightboxEl.querySelector('.imb-lightbox__img');
895    // Fade out → swap src → fade in
896    img.style.opacity = '0';
897    var newSrc = lbImages[idx];
898    setTimeout(function () {
899      img.src = newSrc;
900      img.onload = function () { img.style.opacity = '1'; };
901      // Fallback: if cached, onload may not fire
902      setTimeout(function () { img.style.opacity = '1'; }, 60);
903    }, 100);
904    lightboxEl.querySelector('.imb-lightbox__counter').textContent = (idx + 1) + ' / ' + lbImages.length;
905  }
906
907  function openLightbox(images, startIdx) {
908    createLightbox();
909    lbImages = images;
910    // Show without fade on first open
911    lbIndex = startIdx < 0 ? 0 : startIdx >= images.length ? images.length - 1 : startIdx;
912    var img = lightboxEl.querySelector('.imb-lightbox__img');
913    img.style.opacity = '1';
914    img.src = lbImages[lbIndex];
915    lightboxEl.querySelector('.imb-lightbox__counter').textContent = (lbIndex + 1) + ' / ' + lbImages.length;
916    lightboxEl.classList.add('is-open');
917    document.body.style.overflow = 'hidden';
918  }
919
920  function closeLightbox() {
921    if (lightboxEl && lightboxEl.classList.contains('is-open')) {
922      lightboxEl.classList.remove('is-open');
923      document.body.style.overflow = '';
924    }
925  }
926
927  /* ==========================================================================
928     MOBILE SECTION NAV — sticky bottom bar with anchors to h2.title sections
929     Only on screens <= 768px. Built from h2.title elements on the page.
930     ========================================================================== */
931  (function() {
932    if (window.innerWidth > 768) return;
933
934    var headings = document.querySelectorAll('h2.title, .sub-title h2');
935    if (headings.length < 2) return;
936
937    // Inject anchor IDs onto the parent section of each heading
938    var anchors = [];
939    headings.forEach(function(h2) {
940      var text = h2.textContent.trim();
941      var id = 'section-' + text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
942      var section = h2.closest('section') || h2.parentElement;
943      section.id = id;
944      anchors.push({ id: id, label: text });
945    });
946
947    // Build the sticky bar
948    var bar = document.createElement('nav');
949    bar.className = 'mobile-section-nav';
950    bar.setAttribute('aria-label', 'Jump to section');
951
952    var inner = document.createElement('div');
953    inner.className = 'mobile-section-nav__inner';
954
955    anchors.forEach(function(a) {
956      var link = document.createElement('a');
957      link.href = '#' + a.id;
958      link.className = 'mobile-section-nav__link';
959      link.textContent = a.label;
960      link.addEventListener('click', function(e) {
961        e.preventDefault();
962        var target = document.getElementById(a.id);
963        if (!target) return;
964        var offset = target.getBoundingClientRect().top + window.pageYOffset - 60;
965        window.scrollTo({ top: offset, behavior: 'smooth' });
966      });
967      inner.appendChild(link);
968    });
969
970    bar.appendChild(inner);
971    document.body.appendChild(bar);
972
973    // Highlight active section on scroll + hide near footer
974    var footer = document.querySelector('footer') || document.querySelector('.site-footer');
975    function onScroll() {
976      // Hide near footer
977      if (footer) {
978        var fTop = footer.getBoundingClientRect().top;
979        bar.style.opacity = fTop < 120 ? '0' : '1';
980        bar.style.pointerEvents = fTop < 120 ? 'none' : '';
981      }
982      // Active link
983      var scrollY = window.pageYOffset + window.innerHeight / 2;
984      var active = null;
985      anchors.forEach(function(a) {
986        var el = document.getElementById(a.id);
987        if (el && el.offsetTop <= scrollY) active = a.id;
988      });
989      bar.querySelectorAll('.mobile-section-nav__link').forEach(function(l) {
990        l.classList.toggle('is-active', l.getAttribute('href') === '#' + active);
991      });
992    }
993    window.addEventListener('scroll', onScroll, { passive: true });
994    onScroll();
995  })();
996
997  /* ==========================================================================
998     LOOK CARD — thumbnail click swaps main image in place
999     ========================================================================== */
1000  document.addEventListener( 'click', function( e ) {
1001    var btn = e.target.closest( '.look-card__thumb' );
1002    if ( ! btn ) return;
1003    e.stopPropagation();
1004    var card = btn.closest( '.look-card' );
1005    if ( ! card ) return;
1006    var mainImg = card.querySelector( '.look-card__main-img' );
1007    if ( ! mainImg ) return;
1008
1009    mainImg.src = btn.getAttribute( 'data-src' );
1010
1011    card.querySelectorAll( '.look-card__thumb' ).forEach( function( b ) {
1012      b.classList.remove( 'is-active' );
1013    } );
1014    btn.classList.add( 'is-active' );
1015  } );
1016
1017  // Bind lightbox to gallery links — run after carousels are set up
1018  function initGalleryLightbox() {
1019
1020    // Look card thumbnails — group per card so navigation stays within the card
1021    document.querySelectorAll('.look-card').forEach(function (card) {
1022      var thumbs = Array.from(card.querySelectorAll('.look-card__thumb[href]'));
1023      if (!thumbs.length) return;
1024      var srcs = thumbs.map(function (el) { return el.getAttribute('href'); });
1025      thumbs.forEach(function (el, idx) {
1026        el.onclick = function (e) {
1027          e.preventDefault();
1028          openLightbox(srcs, idx);
1029        };
1030      });
1031    });
1032
1033    var links = Array.from(document.querySelectorAll(
1034      '.su-image-carousel-item-content a[href], .imb-gallery__item[href], .imb-gallery__item a[href]'
1035    )).filter(function (el) {
1036      var href = el.getAttribute('href');
1037      return href && /\.(jpe?g|png|gif|webp)/i.test(href);
1038    });
1039
1040    var srcs = links.map(function (el) { return el.getAttribute('href'); });
1041
1042    links.forEach(function (el, idx) {
1043      el.onclick = function (e) {
1044        e.preventDefault();
1045        e.stopImmediatePropagation();
1046        openLightbox(srcs, idx);
1047        return false;
1048      };
1049    });
1050  }
1051
1052  /* Weather widget (weatherwidget.info) rewrites HTML and can add extra links later.
1053     Force nofollow on every world-weather / weatherwidget hyperlink. */
1054  function nofollowWeatherLinks(root) {
1055    var scope = root && root.querySelectorAll ? root : document;
1056    var links = [];
1057    try {
1058      links = scope.querySelectorAll('a[href*="world-weather.info"], a[href*="weatherwidget.info"]');
1059    } catch (e) {
1060      return;
1061    }
1062    for (var i = 0; i < links.length; i++) {
1063      links[i].setAttribute('rel', 'nofollow noopener noreferrer');
1064    }
1065    if (root && root.tagName === 'A' && root.href &&
1066        (root.href.indexOf('world-weather.info') !== -1 || root.href.indexOf('weatherwidget.info') !== -1)) {
1067      root.setAttribute('rel', 'nofollow noopener noreferrer');
1068    }
1069  }
1070  nofollowWeatherLinks(document);
1071  if (window.MutationObserver && document.body) {
1072    new MutationObserver(function (mutations) {
1073      var need = false;
1074      for (var i = 0; i < mutations.length; i++) {
1075        if (mutations[i].addedNodes && mutations[i].addedNodes.length) {
1076          need = true;
1077          break;
1078        }
1079      }
1080      if (need) nofollowWeatherLinks(document);
1081    }).observe(document.body, { childList: true, subtree: true });
1082  }
1083  window.addEventListener('load', function () { nofollowWeatherLinks(document); });
1084
1085  // Small delay to let any third-party scripts finish DOM changes
1086  setTimeout(initGalleryLightbox, 300);
1087
1088  // Featherlight replacement for standalone image links
1089  document.querySelectorAll('a[data-featherlight], a[href$=".jpg"], a[href$=".jpeg"], a[href$=".png"], a[href$=".webp"]').forEach(function (link) {
1090    if (link.closest('.imb-gallery, .su-image-carousel')) return;
1091    var href = link.getAttribute('href');
1092    if (href && /\.(jpg|jpeg|png|webp|gif)/i.test(href)) {
1093      link.addEventListener('click', function (e) {
1094        e.preventDefault();
1095        openLightbox([href], 0);
1096      });
1097    }
1098  });
1099
1100})();
1101
1102/* ==========================================================================
1103   FILTER TABS — image gallery with category filter
1104   Usage: filterImages('south-beach') / filterImages('all')
1105   ========================================================================== */
1106function filterImages(category) {
1107  var wrappers = document.querySelectorAll('.image-wrapper');
1108  var tabs     = document.querySelectorAll('.filter-tabs .tab');
1109
1110  wrappers.forEach(function(el) {
1111    if (category === 'all' || el.classList.contains(category)) {
1112      el.style.display = '';
1113    } else {
1114      el.style.display = 'none';
1115    }
1116  });
1117
1118  tabs.forEach(function(btn) {
1119    btn.classList.toggle('active', btn.getAttribute('onclick') === "filterImages('" + category + "')");
1120  });
1121}

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.