1/** 2 * clearTime Care - Modern Landing Page Scripts 3 * Handles scroll animations, tabs, accordion, and smooth interactions 4 */ 5 6(function() { 7 'use strict'; 8 9 // Wait for DOM to be ready 10 document.addEventListener('DOMContentLoaded', function() { 11 initScrollAnimations(); 12 initTabs(); 13 initAccordion(); 14 initSmoothScroll(); 15 initParallax(); 16 initMobileMenu(); 17 initHeaderScroll(); 18 initDarkModeToggle(); 19 }); 20 21 /** 22 * Dark Mode Toggle - Theme preference with localStorage persistence 23 */ 24 function initDarkModeToggle() { 25 var STORAGE_KEY = 'ct-theme-preference'; 26 var html = document.documentElement; 27 28 function getPreferredTheme() { 29 var stored = localStorage.getItem(STORAGE_KEY); 30 if (stored) return stored; 31 return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 32 } 33 34 function setTheme(theme) { 35 html.setAttribute('data-ct-theme', theme); 36 localStorage.setItem(STORAGE_KEY, theme); 37 updateToggleButtons(theme); 38 } 39 40 function updateToggleButtons(theme) { 41 document.querySelectorAll('.ct-theme-toggle__btn').forEach(function(btn) { 42 btn.classList.toggle('active', btn.dataset.theme === theme); 43 }); 44 } 45 46 // Apply theme (flash prevention script already set attribute) 47 var theme = getPreferredTheme(); 48 setTheme(theme); 49 50 // Handle toggle button clicks 51 document.addEventListener('click', function(e) { 52 var btn = e.target.closest('.ct-theme-toggle__btn'); 53 if (btn && btn.dataset.theme) { 54 setTheme(btn.dataset.theme); 55 } 56 }); 57 58 // Listen for system preference changes 59 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { 60 if (!localStorage.getItem(STORAGE_KEY)) { 61 setTheme(e.matches ? 'dark' : 'light'); 62 } 63 }); 64 } 65 66 /** 67 * Scroll Animations - Reveal elements on scroll with staggered delays 68 */ 69 function initScrollAnimations() { 70 // Select elements to animate 71 const animatedSelectors = [ 72 '.ct-section-header', 73 '.ct-card', 74 '.ct-comparison__column', 75 '.ct-pricing__card', 76 '.ct-accordion__item', 77 '.ct-blog-card', 78 '.ct-feature-row > *' 79 ]; 80 81 const animatedElements = document.querySelectorAll(animatedSelectors.join(', ')); 82 83 // Add animation classes 84 animatedElements.forEach(function(el, index) { 85 el.classList.add('ct-animate'); 86 // Create staggered delay groups 87 const delayClass = 'ct-animate-delay-' + ((index % 4) + 1); 88 el.classList.add(delayClass); 89 }); 90 91 // Intersection Observer for scroll reveal 92 const observerOptions = { 93 root: null, 94 rootMargin: '50px 0px -50px 0px', 95 threshold: 0.1 96 }; 97 98 const observer = new IntersectionObserver(function(entries) { 99 entries.forEach(function(entry) { 100 if (entry.isIntersecting) { 101 entry.target.classList.add('visible'); 102 observer.unobserve(entry.target); 103 } 104 }); 105 }, observerOptions); 106 107 animatedElements.forEach(function(el) { 108 observer.observe(el); 109 }); 110 111 // Immediately show elements already in viewport on load 112 setTimeout(function() { 113 animatedElements.forEach(function(el) { 114 const rect = el.getBoundingClientRect(); 115 if (rect.top < window.innerHeight && rect.bottom > 0) { 116 el.classList.add('visible'); 117 } 118 }); 119 }, 100); 120 } 121 122 /** 123 * Tabs Component - Modern pill-style tabs 124 */ 125 function initTabs() { 126 const tabContainers = document.querySelectorAll('.ct-tabs'); 127
128 tabContainers.forEach(function(container) { 129 const buttons = container.querySelectorAll('.ct-tabs__btn'); 130 const panels = container.querySelectorAll('.ct-tabs__panel'); 131 132 buttons.forEach(function(button) { 133 button.addEventListener('click', function() { 134 const targetId = this.getAttribute('data-tab'); 135 136 // Remove active from all 137 buttons.forEach(function(btn) { 138 btn.classList.remove('active'); 139 }); 140 panels.forEach(function(panel) { 141 panel.classList.remove('active'); 142 }); 143 144 // Activate clicked tab 145 this.classList.add('active'); 146 const targetPanel = container.querySelector('[data-tab-panel="' + targetId + '"]'); 147 if (targetPanel) { 148 targetPanel.classList.add('active'); 149 } 150 }); 151 }); 152 153 // Activate first tab by default 154 if (!container.querySelector('.ct-tabs__btn.active') && buttons.length > 0) { 155 buttons[0].click(); 156 } 157 }); 158 } 159 160 /** 161 * Accordion Component - Smooth expand/collapse 162 */ 163 function initAccordion() { 164 const accordions = document.querySelectorAll('.ct-accordion'); 165 166 accordions.forEach(function(accordion) { 167 const items = accordion.querySelectorAll('.ct-accordion__item'); 168 169 items.forEach(function(item) { 170 const header = item.querySelector('.ct-accordion__header'); 171 172 header.addEventListener('click', function() { 173 const isActive = item.classList.contains('active'); 174 175 // Close all items (single-open mode) 176 items.forEach(function(otherItem) { 177 otherItem.classList.remove('active'); 178 }); 179 180 // Toggle clicked item 181 if (!isActive) { 182 item.classList.add('active'); 183 } 184 }); 185 }); 186 }); 187 } 188 189 /** 190 * Smooth Scroll - Enhanced anchor scrolling 191 */ 192 function initSmoothScroll() { 193 document.querySelectorAll('a[href^="#"]').forEach(function(anchor) { 194 anchor.addEventListener('click', function(e) { 195 const targetId = this.getAttribute('href'); 196 197 if (targetId === '#' || targetId === '') return; 198 199 const targetElement = document.querySelector(targetId); 200 201 if (targetElement) { 202 e.preventDefault(); 203 204 const headerOffset = 100; 205 const elementPosition = targetElement.getBoundingClientRect().top; 206 const offsetPosition = elementPosition + window.pageYOffset - headerOffset; 207 208 window.scrollTo({ 209 top: offsetPosition, 210 behavior: 'smooth' 211 }); 212 } 213 }); 214 }); 215 } 216 217 /** 218 * Subtle Parallax Effect for hero blobs 219 */ 220 function initParallax() { 221 const blobs = document.querySelectorAll('.ct-hero__blob'); 222 223 if (blobs.length === 0) return; 224 225 let ticking = false; 226 227 window.addEventListener('scroll', function() { 228 if (!ticking) { 229 window.requestAnimationFrame(function() { 230 const scrolled = window.pageYOffset; 231 232 blobs.forEach(function(blob, index) { 233 const speed = 0.1 + (index * 0.05); 234 const yPos = scrolled * speed; 235 blob.style.transform = 'translateY(' + yPos + 'px)'; 236 }); 237 238 ticking = false; 239 }); 240 241 ticking = true; 242 } 243 }); 244 } 245 246 /** 247 * Mobile Menu Toggle - Modern animated menu 248 * Uses data-ct-menu-open attribute for state management (synced with CSS) 249 */ 250 function initMobileMenu() { 251 const toggle = document.querySelector('.ct-header__toggle'); 252 const mobileNav = document.querySelector('.ct-header__mobile-nav'); 253 const header = document.querySelector('.ct-header'); 254 255 if (!toggle || !mobileNav) return; 256 257 // Save scroll position to restore after closing menu 258 let savedScrollY = 0; 259 260 // Helper to check if menu is open 261 function isMenuOpen() { 262 return toggle.getAttribute('data-ct-menu-open') === 'true'; 263 } 264 265 // Toggle menu open/close 266 toggle.addEventListener('click', function(e) { 267 e.stopPropagation(); 268 const isOpen = isMenuOpen(); 269 const newState = !isOpen; 270 271 // Update state via data attribute (CSS uses this for styling) 272 this.setAttribute('aria-expanded', newState); 273 this.setAttribute('data-ct-menu-open', newState); 274 mobileNav.setAttribute('data-ct-menu-open', newState); 275 276 // Prevent body scroll when menu is open - preserve scroll position 277 if (newState) { 278 savedScrollY = window.scrollY; 279 document.body.style.overflow = 'hidden'; 280 document.body.style.position = 'fixed'; 281 document.body.style.top = '-' + savedScrollY + 'px'; 282 document.body.style.width = '100%'; 283 document.body.classList.add('ct-mobile-menu-open'); 284 } else { 285 document.body.style.overflow = ''; 286 document.body.style.position = ''; 287 document.body.style.top = ''; 288 document.body.style.width = ''; 289 document.body.classList.remove('ct-mobile-menu-open'); 290 window.scrollTo(0, savedScrollY); 291 } 292 }); 293 294 // Close menu when clicking a link 295 mobileNav.querySelectorAll('a').forEach(function(link) { 296 link.addEventListener('click', function() { 297 closeMenu(); 298 }); 299 }); 300 301 // Close menu when clicking the close button 302 var closeBtn = mobileNav.querySelector('.ct-header__mobile-close'); 303 if (closeBtn) { 304 closeBtn.addEventListener('click', function(e) { 305 e.preventDefault(); 306 e.stopPropagation(); 307 closeMenu(); 308 }); 309 } 310 311 // Close menu when clicking outside 312 document.addEventListener('click', function(e) { 313 if (isMenuOpen() && 314 !mobileNav.contains(e.target) && 315 !toggle.contains(e.target)) { 316 closeMenu(); 317 } 318 }); 319 320 // Close menu on escape key 321 document.addEventListener('keydown', function(e) { 322 if (e.key === 'Escape' && isMenuOpen()) { 323 closeMenu(); 324 } 325 }); 326 327 function closeMenu() { 328 toggle.setAttribute('aria-expanded', 'false'); 329 toggle.setAttribute('data-ct-menu-open', 'false'); 330 mobileNav.setAttribute('data-ct-menu-open', 'false'); 331 document.body.style.overflow = ''; 332 document.body.style.position = ''; 333 document.body.style.top = ''; 334 document.body.style.width = ''; 335 document.body.classList.remove('ct-mobile-menu-open'); 336 window.scrollTo(0, savedScrollY); 337 } 338 339 // Handle window resize - close menu if viewport becomes larger than mobile 340 let resizeTimer; 341 window.addEventListener('resize', function() { 342 clearTimeout(resizeTimer); 343 resizeTimer = setTimeout(function() { 344 if (window.innerWidth > 768 && isMenuOpen()) { 345 closeMenu(); 346 } 347 }, 100); 348 }); 349 } 350 351 /** 352 * Header Scroll Effect - Frosted glass background on scroll 353 */ 354 function initHeaderScroll() { 355 const header = document.querySelector('.ct-header'); 356 if (!header) return; 357 358 const scrollThreshold = 50; 359 let ticking = false;
360 361 function updateHeader() { 362 if (window.scrollY > scrollThreshold) { 363 header.classList.add('scrolled'); 364 } else { 365 header.classList.remove('scrolled'); 366 } 367 ticking = false; 368 } 369 370 window.addEventListener('scroll', function() { 371 if (!ticking) { 372 window.requestAnimationFrame(updateHeader); 373 ticking = true; 374 } 375 }); 376 377 // Check initial state 378 updateHeader(); 379 } 380 381})();
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.