// Mobile menu toggle const hamburger = document.getElementById('hamburger'); const navLinks = document.getElementById('navLinks'); hamburger.addEventListener('click', () => { navLinks.classList.toggle('active'); hamburger.classList.toggle('active'); }); // Close menu on link click navLinks.querySelectorAll('a').forEach(link => { link.addEventListener('click', () => { navLinks.classList.remove('active'); hamburger.classList.remove('active'); }); }); // Close menu when clicking outside document.addEventListener('click', (e) => { if (!navLinks.contains(e.target) && !hamburger.contains(e.target)) { navLinks.classList.remove('active'); hamburger.classList.remove('active'); } }); // Scroll animations const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } }); }, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }); document.querySelectorAll('.fade-in').forEach(el => observer.observe(el)); // Smooth scroll for nav links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // Server rack parallax background function createServerRack() { const bgContainer = document.querySelector('.server-rack-bg'); const rackContainer = document.createElement('div'); rackContainer.className = 'rack-container'; const topBar = document.createElement('div'); topBar.className = 'rack-top-bar'; rackContainer.appendChild(topBar); const unitHeight = 98; const barHeight = 72; const totalPageHeight = document.documentElement.scrollHeight; const totalUnits = Math.max(Math.ceil((totalPageHeight - 100 - barHeight) / unitHeight), 10); const brands = ['Framework']; const services = ['Borg', 'Gitea', 'Nextcloud', 'Jellyfin', 'Open WebUI', 'llama.cpp', 'Immich', 'LiteLLM', 'Caddy', 'Nginx', 'Samba', 'Slopbox']; for (let i = 0; i < totalUnits; i++) { const unit = document.createElement('div'); unit.className = 'rack-unit'; const leftRail = document.createElement('div'); leftRail.className = 'rail left'; unit.appendChild(leftRail); const rightRail = document.createElement('div'); rightRail.className = 'rail right'; unit.appendChild(rightRail); const isSwitch = i > 0 && (i % 7 === 0 || i % 11 === 0); if (isSwitch) { const switchFace = document.createElement('div'); switchFace.className = 'eth-switch-face'; const powerLed = document.createElement('div'); powerLed.className = 'eth-switch-led'; switchFace.appendChild(powerLed); const brandLabel = document.createElement('div'); brandLabel.className = 'eth-brand'; brandLabel.textContent = brands[Math.floor(Math.random() * brands.length)]; switchFace.appendChild(brandLabel); const portCount = Math.floor(Math.random() * 4) + 5; const cappedCount = Math.min(portCount, 8); const row = document.createElement('div'); row.className = 'eth-port-row'; for (let p = 0; p < cappedCount; p++) { const port = document.createElement('div'); port.className = 'eth-port'; const led = document.createElement('div'); led.className = 'eth-port-led'; if (Math.random() > 0.3) { led.classList.add('on'); if (Math.random() > 0.7) { led.classList.add('blink'); } } led.style.animationDelay = `${Math.random() * 2}s`; port.appendChild(led); row.appendChild(port); } switchFace.appendChild(row); const label = document.createElement('div'); label.className = 'eth-port-label'; label.textContent = `1-${cappedCount}`; switchFace.appendChild(label); unit.appendChild(switchFace); } else { const face = document.createElement('div'); face.className = 'server-face'; const serverType = Math.random(); if (i === 0) { createTerminal(face); } else if (serverType < 0.35) { // Vertical grill + fans const grillV = document.createElement('div'); grillV.className = 'grill-v'; for (let g = 0; g < 10; g++) { const bar = document.createElement('div'); bar.className = 'grill-v-bar'; grillV.appendChild(bar); } grillV.style.position = 'absolute'; grillV.style.left = '16px'; grillV.style.top = '50%'; grillV.style.transform = 'translateY(-50%)'; face.appendChild(grillV); const fanContainer = document.createElement('div'); fanContainer.className = 'fan-container'; fanContainer.style.position = 'absolute'; fanContainer.style.right = '16px'; fanContainer.style.top = '50%'; fanContainer.style.transform = 'translateY(-50%)'; fanContainer.style.zIndex = '1'; const fanCount = Math.floor(Math.random() * 2) + 1; const fanSpeeds = ['fan-slow', 'fan-medium', 'fan-fast']; for (let f = 0; f < fanCount; f++) { const fan = document.createElement('div'); fan.className = `fan ${fanSpeeds[Math.floor(Math.random() * fanSpeeds.length)]}`; const blade = document.createElement('div'); blade.className = 'fan-blade'; fan.appendChild(blade); const center = document.createElement('div'); center.className = 'fan-center'; fan.appendChild(center); fanContainer.appendChild(fan); } face.appendChild(fanContainer); const ledGroup = document.createElement('div'); ledGroup.className = 'led-group'; ledGroup.style.position = 'absolute'; ledGroup.style.top = '8px'; ledGroup.style.left = '50%'; ledGroup.style.transform = 'translateX(-50%)'; const ledCount = Math.floor(Math.random() * 3) + 1; const blinkClasses = ['led-blink-1', 'led-blink-2', 'led-blink-3', 'led-blink-4']; const colorClasses = ['led-green', 'led-blue', 'led-yellow', 'led-red']; for (let l = 0; l < ledCount; l++) { const led = document.createElement('div'); led.className = `led ${colorClasses[Math.floor(Math.random() * colorClasses.length)]} ${blinkClasses[Math.floor(Math.random() * blinkClasses.length)]}`; led.style.animationDelay = `${Math.random() * 3}s`; ledGroup.appendChild(led); } face.appendChild(ledGroup); } else if (serverType < 0.7) { // Horizontal grill const grillH = document.createElement('div'); grillH.className = 'grill-h'; for (let g = 0; g < 12; g++) { const bar = document.createElement('div'); bar.className = 'grill-h-bar'; grillH.appendChild(bar); } grillH.style.position = 'absolute'; grillH.style.left = '50%'; grillH.style.top = '50%'; grillH.style.transform = 'translate(-50%, -50%)'; face.appendChild(grillH); const ledGroup = document.createElement('div'); ledGroup.className = 'led-group'; ledGroup.style.position = 'absolute'; ledGroup.style.right = '16px'; ledGroup.style.top = '50%'; ledGroup.style.transform = 'translateY(-50%)'; const ledCount = Math.floor(Math.random() * 3) + 1; const blinkClasses = ['led-blink-1', 'led-blink-2', 'led-blink-3', 'led-blink-4']; const colorClasses = ['led-green', 'led-blue', 'led-yellow', 'led-red']; for (let l = 0; l < ledCount; l++) { const led = document.createElement('div'); led.className = `led ${colorClasses[Math.floor(Math.random() * colorClasses.length)]} ${blinkClasses[Math.floor(Math.random() * blinkClasses.length)]}`; led.style.animationDelay = `${Math.random() * 3}s`; ledGroup.appendChild(led); } face.appendChild(ledGroup); } else { // Drive bays const driveBayContainer = document.createElement('div'); driveBayContainer.style.display = 'flex'; driveBayContainer.style.gap = '8px'; driveBayContainer.style.alignItems = 'center'; driveBayContainer.style.position = 'absolute'; driveBayContainer.style.left = '50%'; driveBayContainer.style.top = '50%'; driveBayContainer.style.transform = 'translate(-50%, -50%)'; const driveBays = Math.floor(Math.random() * 3) + 6; for (let b = 0; b < driveBays; b++) { const bay = document.createElement('div'); bay.className = 'server-drive-bay'; driveBayContainer.appendChild(bay); } face.appendChild(driveBayContainer); const ledGroup = document.createElement('div'); ledGroup.className = 'led-group'; ledGroup.style.position = 'absolute'; ledGroup.style.right = '10px'; ledGroup.style.top = '50%'; ledGroup.style.transform = 'translateY(-50%)'; const ledCount = Math.floor(Math.random() * 3) + 1; const blinkClasses = ['led-blink-1', 'led-blink-2', 'led-blink-3', 'led-blink-4']; const colorClasses = ['led-green', 'led-blue', 'led-yellow', 'led-red']; for (let l = 0; l < ledCount; l++) { const led = document.createElement('div'); led.className = `led ${colorClasses[Math.floor(Math.random() * colorClasses.length)]} ${blinkClasses[Math.floor(Math.random() * blinkClasses.length)]}`; led.style.animationDelay = `${Math.random() * 3}s`; ledGroup.appendChild(led); } face.appendChild(ledGroup); } if (i !== 0) { const serviceLabel = document.createElement('div'); serviceLabel.className = 'service-label'; serviceLabel.textContent = services[Math.floor(Math.random() * services.length)]; face.appendChild(serviceLabel); } unit.appendChild(face); } rackContainer.appendChild(unit); } const bottomBar = document.createElement('div'); bottomBar.className = 'rack-bottom-bar'; rackContainer.appendChild(bottomBar); bgContainer.appendChild(rackContainer); let ticking = false; // Update the parallax scroll position of the rack container function updateParallax() { const scrolled = window.pageYOffset; const parallaxSpeed = 0.3; const translateY = scrolled * parallaxSpeed; rackContainer.style.transform = `translateX(-50%) translateY(${-translateY}px)`; ticking = false; } window.addEventListener('scroll', () => { if (!ticking) { requestAnimationFrame(updateParallax); ticking = true; } }); } createServerRack(); const heroEl = document.getElementById('hero'); let heroMouseDown = false; // Track mouse state to prevent state collapse during hero click heroEl.addEventListener('mousedown', () => { heroMouseDown = true; }); heroEl.addEventListener('mouseup', () => { heroMouseDown = false; }); // Toggle terminal expansion when clicking the hero section heroEl.addEventListener('click', (e) => { if (e.target.closest('.btn')) return; if (activeTerminal && activeTerminal._mobileInput) { if (activeTerminal.classList.contains('grown')) { activeTerminal.classList.remove('grown'); activeTerminal.classList.remove('opaque'); const face = activeTerminal.closest('.server-face'); const unit = face?.closest('.rack-unit'); face?.classList.remove('grown'); unit?.classList.remove('grown'); heroEl.classList.remove('shifted'); activeTerminal._mobileInput.blur(); document.querySelector('.rack-container')?.classList.remove('opaque'); } else { activeTerminal._mobileInput.focus(); activeTerminal.classList.add('grown'); activeTerminal.classList.add('opaque'); const face = activeTerminal.closest('.server-face'); const unit = face?.closest('.rack-unit'); face?.classList.add('grown'); unit?.classList.add('grown'); heroEl.classList.add('shifted'); document.querySelector('.rack-container')?.classList.add('opaque'); } } });