1/* ================================================================= 2 FORGMIND.AI â Interactive Behaviors 3================================================================= */ 4 5document.addEventListener('DOMContentLoaded', () => { 6 7 /* ---------- Cursor Glow ---------- */ 8 const cursorGlow = document.getElementById('cursorGlow'); 9 if (cursorGlow && window.matchMedia('(pointer: fine)').matches) { 10 let mouseX = 0, mouseY = 0, glowX = 0, glowY = 0; 11 12 document.addEventListener('mousemove', (e) => { 13 mouseX = e.clientX; 14 mouseY = e.clientY; 15 }); 16 17 const animateGlow = () => { 18 glowX += (mouseX - glowX) * 0.1; 19 glowY += (mouseY - glowY) * 0.1; 20 cursorGlow.style.transform = `translate(${glowX}px, ${glowY}px) translate(-50%, -50%)`; 21 requestAnimationFrame(animateGlow); 22 }; 23 animateGlow(); 24 } else if (cursorGlow) { 25 cursorGlow.style.display = 'none'; 26 } 27 28 /* ---------- Navbar scroll state + WhatsApp bubble reveal ---------- */ 29 const navbar = document.getElementById('navbar'); 30 const heroSection = document.querySelector('.hero'); 31 const onScroll = () => { 32 if (window.scrollY > 30) navbar.classList.add('scrolled'); 33 else navbar.classList.remove('scrolled'); 34 35 const heroHeight = heroSection ? heroSection.offsetHeight : 600; 36 if (window.scrollY > heroHeight - 120) document.body.classList.add('scrolled-past-hero'); 37 else document.body.classList.remove('scrolled-past-hero'); 38 }; 39 window.addEventListener('scroll', onScroll, { passive: true }); 40 onScroll(); 41 42 /* ---------- Mobile menu toggle ---------- */ 43 const mobileToggle = document.getElementById('mobileToggle'); 44 const navLinks = document.querySelector('.nav-links'); 45 46 if (mobileToggle && navLinks) { 47 mobileToggle.addEventListener('click', () => { 48 mobileToggle.classList.toggle('open'); 49 navLinks.classList.toggle('mobile-open'); 50 }); 51 52 navLinks.querySelectorAll('a').forEach(link => { 53 link.addEventListener('click', () => { 54 mobileToggle.classList.remove('open'); 55 navLinks.classList.remove('mobile-open'); 56 }); 57 }); 58 } 59 60 /* ---------- ForgeMind logo: mouse-tracking eyes ---------- */ 61 const fmSvg = document.getElementById('fmLogoNav'); 62 const fmEyeL = document.getElementById('fmEyeLeft'); 63 const fmEyeR = document.getElementById('fmEyeRight'); 64 65 if (fmSvg && fmEyeL && fmEyeR && window.matchMedia('(pointer: fine)').matches && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 66 const LC = { x: 112, y: 143.9 };
67 const RC = { x: 159.75, y: 143.9 }; 68 const EYE_MAX = 5; // max pixel offset in SVG viewBox units 69 const LERP = 0.18; 70 71 let mx = window.innerWidth / 2; 72 let my = window.innerHeight / 2; 73 let svgRect = fmSvg.getBoundingClientRect(); 74 let lx = 0, ly = 0, rx = 0, ry = 0; 75 76 const updateRect = () => { svgRect = fmSvg.getBoundingClientRect(); }; 77 window.addEventListener('resize', updateRect, { passive: true }); 78 window.addEventListener('scroll', updateRect, { passive: true }); 79 80 document.addEventListener('mousemove', (e) => { 81 mx = e.clientX; 82 my = e.clientY; 83 }, { passive: true }); 84 85 const targetFor = (center) => { 86 if (svgRect.width < 1) return { x: 0, y: 0 }; 87 const sx = 270 / svgRect.width; 88 const sy = 270 / svgRect.height; 89 const dx = (mx - svgRect.left) * sx - center.x; 90 const dy = (my - svgRect.top) * sy - center.y; 91 const d = Math.sqrt(dx * dx + dy * dy); 92 if (d < 0.01) return { x: 0, y: 0 }; 93 const m = Math.min(d * 0.07, EYE_MAX); 94 return { x: (dx / d) * m, y: (dy / d) * m }; 95 }; 96 97 const tick = () => { 98 const tL = targetFor(LC); 99 const tR = targetFor(RC); 100 lx += (tL.x - lx) * LERP; 101 ly += (tL.y - ly) * LERP; 102 rx += (tR.x - rx) * LERP; 103 ry += (tR.y - ry) * LERP; 104 fmEyeL.setAttribute('transform', `translate(${lx.toFixed(2)} ${ly.toFixed(2)})`); 105 fmEyeR.setAttribute('transform', `translate(${rx.toFixed(2)} ${ry.toFixed(2)})`); 106 requestAnimationFrame(tick); 107 }; 108 requestAnimationFrame(tick); 109 } 110 111 /* ---------- ForgeMind hero logo: mouse-tracking eyes ---------- */ 112 const fmHeroSvg = document.getElementById('fmLogoHero'); 113 const fmHeroEyeL = document.getElementById('fmHeroEyeLeft'); 114 const fmHeroEyeR = document.getElementById('fmHeroEyeRight'); 115 116 if (fmHeroSvg && fmHeroEyeL && fmHeroEyeR && window.matchMedia('(pointer: fine)').matches && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 117 const HLC = { x: 112, y: 143.9 };
118 const HRC = { x: 159.75, y: 143.9 }; 119 const H_EYE_MAX = 7; 120 const H_LERP = 0.18; 121 122 let hmx = window.innerWidth / 2; 123 let hmy = window.innerHeight / 2; 124 let hRect = fmHeroSvg.getBoundingClientRect(); 125 let hlx = 0, hly = 0, hrx = 0, hry = 0; 126 127 const hUpdateRect = () => { hRect = fmHeroSvg.getBoundingClientRect(); }; 128 window.addEventListener('resize', hUpdateRect, { passive: true }); 129 window.addEventListener('scroll', hUpdateRect, { passive: true }); 130 131 document.addEventListener('mousemove', (e) => { 132 hmx = e.clientX; 133 hmy = e.clientY; 134 }, { passive: true }); 135 136 const hTargetFor = (center) => { 137 if (hRect.width < 1) return { x: 0, y: 0 }; 138 const sx = 270 / hRect.width; 139 const sy = 270 / hRect.height; 140 const dx = (hmx - hRect.left) * sx - center.x; 141 const dy = (hmy - hRect.top) * sy - center.y; 142 const d = Math.sqrt(dx * dx + dy * dy); 143 if (d < 0.01) return { x: 0, y: 0 }; 144 const m = Math.min(d * 0.07, H_EYE_MAX); 145 return { x: (dx / d) * m, y: (dy / d) * m }; 146 }; 147 148 const hTick = () => { 149 const tL = hTargetFor(HLC); 150 const tR = hTargetFor(HRC); 151 hlx += (tL.x - hlx) * H_LERP; 152 hly += (tL.y - hly) * H_LERP; 153 hrx += (tR.x - hrx) * H_LERP; 154 hry += (tR.y - hry) * H_LERP; 155 fmHeroEyeL.setAttribute('transform', `translate(${hlx.toFixed(2)} ${hly.toFixed(2)})`); 156 fmHeroEyeR.setAttribute('transform', `translate(${hrx.toFixed(2)} ${hry.toFixed(2)})`); 157 requestAnimationFrame(hTick); 158 }; 159 requestAnimationFrame(hTick); 160 } 161 162 /* ---------- Smooth scroll for anchor links ---------- */ 163 document.querySelectorAll('a[href^="#"]').forEach(anchor => { 164 anchor.addEventListener('click', (e) => { 165 const href = anchor.getAttribute('href'); 166 if (href === '#' || href.length < 2) return; 167 const target = document.querySelector(href); 168 if (target) { 169 e.preventDefault(); 170 const offset = 80; 171 const top = target.getBoundingClientRect().top + window.pageYOffset - offset; 172 window.scrollTo({ top, behavior: 'smooth' }); 173 } 174 }); 175 }); 176 177 /* ---------- Curriculum unit tabs ---------- */ 178 const unitTabs = document.querySelectorAll('.unit-tab'); 179 const unitContents = document.querySelectorAll('.unit-content'); 180 181 unitTabs.forEach(tab => { 182 tab.addEventListener('click', () => { 183 const target = tab.getAttribute('data-tab'); 184 185 unitTabs.forEach(t => { 186 t.classList.remove('active'); 187 t.setAttribute('aria-selected', 'false'); 188 }); 189 unitContents.forEach(c => c.classList.remove('active')); 190 191 tab.classList.add('active'); 192 tab.setAttribute('aria-selected', 'true'); 193 const targetEl = document.getElementById(target); 194 if (targetEl) targetEl.classList.add('active'); 195 }); 196 }); 197 198 /* ---------- Tools rail: prev/next scroll ---------- */ 199 const toolsRail = document.getElementById('toolsRail'); 200 const railPrev = document.querySelector('.tools-rail-prev'); 201 const railNext = document.querySelector('.tools-rail-next'); 202 203 if (toolsRail && railPrev && railNext) { 204 const scrollByCard = (dir) => { 205 const card = toolsRail.querySelector('.tool-card'); 206 const step = card ? (card.getBoundingClientRect().width + 14) * 2 : 300; 207 toolsRail.scrollBy({ left: dir * step, behavior: 'smooth' }); 208 }; 209 210 railPrev.addEventListener('click', () => scrollByCard(-1)); 211 railNext.addEventListener('click', () => scrollByCard(1)); 212 213 const updateRailBtns = () => { 214 const max = toolsRail.scrollWidth - toolsRail.clientWidth - 2; 215 railPrev.disabled = toolsRail.scrollLeft <= 2; 216 railNext.disabled = toolsRail.scrollLeft >= max; 217 }; 218 219 toolsRail.addEventListener('scroll', updateRailBtns, { passive: true }); 220 window.addEventListener('resize', updateRailBtns); 221 updateRailBtns(); 222 } 223 224 /* ---------- English carousel: arrows + dots ---------- */ 225 const englishTrack = document.querySelector('.english-track'); 226 const englishDots = document.querySelectorAll('.english-dot'); 227 const englishPrev = document.querySelector('.english-arrow-prev'); 228 const englishNext = document.querySelector('.english-arrow-next'); 229 230 if (englishTrack && englishDots.length && englishPrev && englishNext) { 231 const cards = [...englishTrack.querySelectorAll('.english-card')]; 232 const total = cards.length; 233 let current = 0; 234 235 const goTo = (idx) => { 236 current = ((idx % total) + total) % total; 237 englishTrack.style.transform = `translateX(-${current * 100}%)`;
238 englishDots.forEach((d, i) => d.classList.toggle('active', i === current)); 239 }; 240 241 englishPrev.addEventListener('click', () => goTo(current - 1)); 242 englishNext.addEventListener('click', () => goTo(current + 1)); 243 244 englishDots.forEach((dot) => { 245 dot.addEventListener('click', () => { 246 goTo(parseInt(dot.getAttribute('data-idx'), 10) || 0); 247 }); 248 }); 249 } 250 251 /* ---------- 14-Day Journey phase accordion ---------- */ 252 document.querySelectorAll('.phase-toggle').forEach(btn => { 253 const label = btn.querySelector('.phase-toggle-label'); 254 btn.addEventListener('click', () => { 255 const card = btn.closest('.phase-card'); 256 if (!card) return; 257 const open = card.classList.toggle('is-open'); 258 btn.setAttribute('aria-expanded', open ? 'true' : 'false'); 259 if (label) label.textContent = open ? 'Hide details' : 'View details'; 260 }); 261 }); 262 263 /* ---------- Curriculum mobile accordion (â¤768px) ---------- */ 264 const mobileMQ = window.matchMedia('(max-width: 768px)'); 265 document.querySelectorAll('.curriculum-section .unit-content').forEach((uc, idx) => { 266 const card = uc.querySelector('.unit-card'); 267 if (!card) return; 268 if (idx === 0) uc.classList.add('mobile-open'); 269 card.addEventListener('click', () => { 270 if (!mobileMQ.matches) return; 271 uc.classList.toggle('mobile-open'); 272 }); 273 }); 274 275 /* ---------- Syllabus PDF modal ---------- */ 276 const pdfModal = document.getElementById('syllabusModal'); 277 if (pdfModal) { 278 const frame = pdfModal.querySelector('.pdf-modal-frame'); 279 const pdfSrc = frame?.getAttribute('data-pdf-src'); 280 281 const openModal = () => { 282 if (frame && pdfSrc && !frame.getAttribute('src')) { 283 frame.setAttribute('src', pdfSrc); 284 } 285 pdfModal.classList.add('is-open'); 286 pdfModal.setAttribute('aria-hidden', 'false'); 287 document.body.classList.add('pdf-modal-open'); 288 }; 289 const closeModal = () => { 290 pdfModal.classList.remove('is-open'); 291 pdfModal.setAttribute('aria-hidden', 'true'); 292 document.body.classList.remove('pdf-modal-open'); 293 }; 294 295 document.querySelectorAll('[data-syllabus-open]').forEach(btn => { 296 btn.addEventListener('click', openModal); 297 }); 298 pdfModal.querySelectorAll('[data-syllabus-close]').forEach(el => { 299 el.addEventListener('click', closeModal); 300 }); 301 document.addEventListener('keydown', (e) => { 302 if (e.key === 'Escape' && pdfModal.classList.contains('is-open')) closeModal(); 303 }); 304 } 305 306 /* ---------- FAQ accordion ---------- */ 307 document.querySelectorAll('.faq-item').forEach(item => { 308 const q = item.querySelector('.faq-q'); 309 q.addEventListener('click', () => { 310 const isActive = item.classList.contains('active'); 311 document.querySelectorAll('.faq-item.active').forEach(el => el.classList.remove('active')); 312 if (!isActive) item.classList.add('active'); 313 }); 314 }); 315 316 /* ---------- Reveal-on-scroll ---------- */ 317 const revealTargets = document.querySelectorAll( 318 '.objective-card, .phase-card, .audience-card, .method-list li, .assess-card, .outcome-card, .faq-item, .day-dot, .message-card, .unit-card, .day-strip, .spotlight-content, .spotlight-card, .for-card' 319 ); 320 321 revealTargets.forEach(el => el.classList.add('reveal')); 322 323 const revealObserver = new IntersectionObserver((entries) => { 324 entries.forEach(entry => { 325 if (entry.isIntersecting) { 326 const i = Array.from(entry.target.parentElement?.children || []).indexOf(entry.target); 327 entry.target.style.transitionDelay = `${Math.max(0, Math.min(i, 6)) * 60}ms`; 328 entry.target.classList.add('visible'); 329 revealObserver.unobserve(entry.target); 330 } 331 }); 332 }, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' }); 333
334 revealTargets.forEach(el => revealObserver.observe(el)); 335 336 /* ---------- Day dot progressive reveal ---------- */ 337 const dayDots = document.querySelectorAll('.day-dot'); 338 const dayObserver = new IntersectionObserver((entries) => { 339 entries.forEach((entry, idx) => { 340 if (entry.isIntersecting) { 341 dayDots.forEach((dot, i) => { 342 setTimeout(() => dot.classList.add('visible'), i * 70); 343 }); 344 dayObserver.disconnect(); 345 } 346 }); 347 }, { threshold: 0.3 }); 348 349 if (dayDots.length) dayObserver.observe(dayDots[0]); 350 351 /* ---------- Active nav link based on scroll ---------- */ 352 const navAnchors = document.querySelectorAll('.nav-links a[href^="#"]'); 353 const sections = Array.from(navAnchors) 354 .map(a => document.querySelector(a.getAttribute('href'))) 355 .filter(Boolean); 356 357 const setActiveNav = () => { 358 const scrollPos = window.scrollY + 120; 359 let current = sections[0]; 360 sections.forEach(sec => { 361 if (sec.offsetTop <= scrollPos) current = sec; 362 }); 363 navAnchors.forEach(a => { 364 const id = a.getAttribute('href').slice(1); 365 if (current && current.id === id) a.style.color = 'var(--text-0)'; 366 else a.style.color = ''; 367 }); 368 }; 369 window.addEventListener('scroll', setActiveNav, { passive: true }); 370 371 /* ---------- Subtle parallax on hero orbs ---------- */ 372 const orbs = document.querySelectorAll('.orb'); 373 if (orbs.length && window.matchMedia('(pointer: fine)').matches) { 374 document.addEventListener('mousemove', (e) => { 375 const cx = window.innerWidth / 2; 376 const cy = window.innerHeight / 2; 377 const dx = (e.clientX - cx) / cx; 378 const dy = (e.clientY - cy) / cy; 379 orbs.forEach((orb, i) => { 380 const factor = (i + 1) * 12; 381 orb.style.transform = `translate(${dx * factor}px, ${dy * factor}px)`; 382 }); 383 }); 384 } 385 386 /* ---------- CTA click ripple feedback ---------- */ 387 document.querySelectorAll('.btn').forEach(btn => { 388 btn.addEventListener('click', function (e) { 389 const rect = this.getBoundingClientRect(); 390 const ripple = document.createElement('span'); 391 ripple.style.cssText = ` 392 position: absolute; 393 width: 10px; 394 height: 10px; 395 border-radius: 50%; 396 background: rgba(255,255,255,0.5); 397 left: ${e.clientX - rect.left}px; 398 top: ${e.clientY - rect.top}px; 399 transform: translate(-50%, -50%) scale(0); 400 pointer-events: none; 401 animation: rippleExpand 0.6s ease-out forwards; 402 `; 403 this.appendChild(ripple); 404 setTimeout(() => ripple.remove(), 700); 405 }); 406 }); 407 408 // Inject ripple keyframes once 409 const rippleStyle = document.createElement('style'); 410 rippleStyle.textContent = ` 411 @keyframes rippleExpand { 412 to { 413 transform: translate(-50%, -50%) scale(40); 414 opacity: 0; 415 } 416 } 417 `; 418 document.head.appendChild(rippleStyle); 419 420 /* ---------- Student photo rotator (mobile only) ---------- */ 421 const sphGrid = document.getElementById('studentPhotosGrid'); 422 if (sphGrid) { 423 const sphCards = Array.from(sphGrid.querySelectorAll('.sph-card')); 424 const sphStates = [ 425 ['is-main', 'is-side-top', 'is-side-bottom'], 426 ['is-side-bottom', 'is-main', 'is-side-top' ], 427 ['is-side-top', 'is-side-bottom', 'is-main' ], 428 ]; 429 let sphStep = 0; 430 setInterval(() => { 431 sphStep = (sphStep + 1) % 3; 432 sphCards.forEach((card, i) => { 433 card.className = 'sph-card ' + sphStates[sphStep][i]; 434 }); 435 }, 3000); 436 } 437 438 /* ---------- Trainer placeholder photo fade-in ---------- */ 439 document.querySelectorAll('.trainer-profile-photo--placeholder img').forEach(img => {
440 if (img.complete && img.naturalWidth > 0) { 441 img.classList.add('loaded'); 442 } else { 443 img.addEventListener('load', () => img.classList.add('loaded')); 444 } 445 }); 446 447 /* ---------- Tamil / EN language toggle ---------- */ 448 const translateBtn = document.getElementById('translateBtn'); 449 450 const getLangCookie = () => { 451 const match = document.cookie.match(/googtrans=([^;]+)/); 452 return match ? decodeURIComponent(match[1]) : ''; 453 }; 454 455 const setActiveTamilState = (active) => { 456 if (!translateBtn) return; 457 const label = translateBtn.querySelector('.translate-label'); 458 if (active) { 459 translateBtn.classList.add('is-tamil'); 460 if (label) label.textContent = 'English'; 461 translateBtn.setAttribute('aria-label', 'Switch back to English'); 462 } else { 463 translateBtn.classList.remove('is-tamil'); 464 if (label) label.textContent = 'தமிழà¯'; 465 translateBtn.setAttribute('aria-label', 'Switch to Tamil'); 466 } 467 }; 468 469 const triggerTamil = () => { 470 const select = document.querySelector('.goog-te-combo'); 471 if (select) { 472 select.value = 'ta'; 473 select.dispatchEvent(new Event('change'));
474 setActiveTamilState(true); 475 } 476 }; 477 478 const triggerEnglish = () => { 479 // Clear the googtrans cookie and reload to restore original text 480 const host = window.location.hostname; 481 document.cookie = 'googtrans=/en/en; path=/'; 482 document.cookie = `googtrans=/en/en; domain=${host}; path=/`; 483 window.location.reload(); 484 }; 485 486 if (translateBtn) { 487 // On page load: if already in Tamil (cookie set), update button state 488 const cookieVal = getLangCookie(); 489 if (cookieVal.includes('/ta')) setActiveTamilState(true); 490 491 // Wait for Google Translate widget to initialise, then wire up the button 492 const waitForSelect = setInterval(() => { 493 const select = document.querySelector('.goog-te-combo'); 494 if (select) { 495 clearInterval(waitForSelect); 496 // If cookie says Tamil but widget hasn't translated yet, fire it 497 if (getLangCookie().includes('/ta')) triggerTamil(); 498 } 499 }, 200); 500 501 translateBtn.addEventListener('click', () => { 502 if (translateBtn.classList.contains('is-tamil')) { 503 triggerEnglish(); 504 } else { 505 triggerTamil(); 506 } 507 }); 508 } 509 510 /* ---------- Trainer image lightbox ---------- */ 511 const trainerLightbox = document.getElementById('trainerLightbox'); 512 if (trainerLightbox) { 513 const lbImg = document.getElementById('trainerLightboxImg'); 514 const lbEye = document.getElementById('trainerLightboxEyebrow'); 515 const lbName = document.getElementById('trainerLightboxName'); 516 const lbBadge = document.getElementById('trainerLightboxBadge'); 517 const lbTags = document.getElementById('trainerLightboxTags'); 518 519 const openLightbox = (card) => { 520 const img = card.querySelector('.trainer-profile-photo img'); 521 const eyebrow = card.querySelector('.trainer-profile-eyebrow'); 522 const name = card.querySelector('.trainer-profile-name'); 523 const badge = card.querySelector('.trainer-profile-badge'); 524 const tags = card.querySelectorAll('.trainer-profile-tag'); 525 const isAI = card.classList.contains('trainer-profile-card--ai'); 526 527 lbImg.src = img.src; 528 lbImg.alt = img.alt; 529 lbImg.style.objectPosition = img.classList.contains('face-focus') ? '50% 12%' : '50% 15%'; 530 lbEye.textContent = eyebrow ? eyebrow.textContent : ''; 531 lbEye.className = 'trainer-lightbox-eyebrow' + (isAI ? '' : ' is-ep'); 532 lbName.textContent = name ? name.textContent : ''; 533 lbBadge.innerHTML = badge 534 ? `<span class="trainer-lightbox-badge${isAI ? ' is-ai' : ''}">${badge.textContent}</span>` 535 : ''; 536 lbTags.innerHTML = [...tags].map(t => 537 `<span class="trainer-lightbox-tag">${t.textContent}</span>` 538 ).join(''); 539 540 trainerLightbox.classList.add('is-open'); 541 trainerLightbox.setAttribute('aria-hidden', 'false'); 542 document.body.style.overflow = 'hidden'; 543 }; 544 545 const closeLightbox = () => { 546 trainerLightbox.classList.remove('is-open'); 547 trainerLightbox.setAttribute('aria-hidden', 'true'); 548 document.body.style.overflow = ''; 549 }; 550 551 document.querySelectorAll('.trainer-profile-photo img').forEach(img => { 552 img.addEventListener('click', () => { 553 const card = img.closest('.trainer-profile-card'); 554 if (card) openLightbox(card); 555 }); 556 }); 557 558 trainerLightbox.querySelectorAll('[data-lightbox-close]').forEach(el => { 559 el.addEventListener('click', closeLightbox); 560 }); 561 562 document.addEventListener('keydown', (e) => { 563 if (e.key === 'Escape' && trainerLightbox.classList.contains('is-open')) closeLightbox(); 564 }); 565 } 566 567 console.log('%c ForgeMind AI ', 'background: #E50914; color: white; padding: 6px 12px; border-radius: 4px; font-weight: bold;', 'AI Basics Course landing loaded.'); 568});
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.