diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0f2f39d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.gitignore +README.md +*.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..571db04 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM nginx:alpine + +# Remove default config +RUN rm /etc/nginx/conf.d/default.conf + +# Copy custom nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy application files +COPY index.html /usr/share/nginx/html/ +COPY style.css /usr/share/nginx/html/ +COPY app.js /usr/share/nginx/html/ +COPY sw.js /usr/share/nginx/html/ +COPY manifest.json /usr/share/nginx/html/ +COPY icons/ /usr/share/nginx/html/icons/ + +EXPOSE 2080 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/app.js b/app.js index 8417fee..8882925 100644 --- a/app.js +++ b/app.js @@ -1,59 +1,246 @@ -// Register Service Worker for PWA -if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('./sw.js') - .then((registration) => { - console.log('SW registered: ', registration); - }) - .catch((registrationError) => { - console.log('SW registration failed: ', registrationError); - }); +/* ════════════════════════════════════════════════════ + AMUSEMENT — Application Logic + Particles, counters, cursor glow, service worker + ════════════════════════════════════════════════════ */ + +(function () { + 'use strict'; + + // ─── Cursor Glow ─── + const glow = document.getElementById('cursor-glow'); + let mouseX = -1000, mouseY = -1000; + + document.addEventListener('mousemove', (e) => { + mouseX = e.clientX; + mouseY = e.clientY; + glow.style.left = mouseX + 'px'; + glow.style.top = mouseY + 'px'; }); -} -// PWA Install Prompt Logic -let deferredPrompt; -const installBtn = document.getElementById('installBtn'); + // card glow follow + document.querySelectorAll('.feature-card').forEach((card) => { + card.addEventListener('mousemove', (e) => { + const rect = card.getBoundingClientRect(); + const x = ((e.clientX - rect.left) / rect.width) * 100; + const y = ((e.clientY - rect.top) / rect.height) * 100; + card.style.setProperty('--mouse-x', x + '%'); + card.style.setProperty('--mouse-y', y + '%'); + }); + }); -window.addEventListener('beforeinstallprompt', (e) => { - // Prevent Chrome 67 and earlier from automatically showing the prompt - e.preventDefault(); - // Stash the event so it can be triggered later. - deferredPrompt = e; - // Update UI to notify the user they can add to home screen - if(installBtn) { - installBtn.hidden = false; + // ─── Particles Canvas ─── + const canvas = document.getElementById('particles-canvas'); + const ctx = canvas.getContext('2d'); + let particles = []; + const PARTICLE_COUNT = 60; + + function resizeCanvas() { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; } -}); + resizeCanvas(); + window.addEventListener('resize', resizeCanvas); -if(installBtn) { - installBtn.addEventListener('click', async () => { - if (deferredPrompt !== null) { - // Show the install prompt - deferredPrompt.prompt(); - // Wait for the user to respond to the prompt - const { outcome } = await deferredPrompt.userChoice; - if (outcome === 'accepted') { - console.log('User accepted the install prompt'); - } else { - console.log('User dismissed the install prompt'); - } - // We've used the prompt, and can't use it again, throw it away - deferredPrompt = null; - installBtn.hidden = true; + class Particle { + constructor() { this.reset(); } + reset() { + this.x = Math.random() * canvas.width; + this.y = Math.random() * canvas.height; + this.size = Math.random() * 2 + 0.5; + this.speedX = (Math.random() - 0.5) * 0.3; + this.speedY = (Math.random() - 0.5) * 0.3; + this.opacity = Math.random() * 0.5 + 0.1; + this.hue = 260 + Math.random() * 80; // purple-pink range } - }); -} + update() { + this.x += this.speedX; + this.y += this.speedY; + if (this.x < 0 || this.x > canvas.width) this.speedX *= -1; + if (this.y < 0 || this.y > canvas.height) this.speedY *= -1; + } + draw() { + ctx.beginPath(); + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); + ctx.fillStyle = `hsla(${this.hue}, 70%, 65%, ${this.opacity})`; + ctx.fill(); + } + } -// Interactive element animation -const pulseElement = document.getElementById('pulseElement'); -if(pulseElement) { - pulseElement.addEventListener('click', () => { - pulseElement.style.transform = 'scale(0.9)'; - setTimeout(() => { - pulseElement.style.transform = 'scale(1)'; - const randomColor = Math.floor(Math.random()*16777215).toString(16); - pulseElement.style.background = `#${randomColor}`; - }, 150); + for (let i = 0; i < PARTICLE_COUNT; i++) { + particles.push(new Particle()); + } + + function drawLines() { + for (let i = 0; i < particles.length; i++) { + for (let j = i + 1; j < particles.length; j++) { + const dx = particles[i].x - particles[j].x; + const dy = particles[i].y - particles[j].y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < 150) { + ctx.beginPath(); + ctx.moveTo(particles[i].x, particles[i].y); + ctx.lineTo(particles[j].x, particles[j].y); + ctx.strokeStyle = `hsla(270, 60%, 60%, ${0.06 * (1 - dist / 150)})`; + ctx.lineWidth = 0.5; + ctx.stroke(); + } + } + } + } + + function animateParticles() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + particles.forEach((p) => { p.update(); p.draw(); }); + drawLines(); + requestAnimationFrame(animateParticles); + } + animateParticles(); + + // ─── Navigation ─── + const nav = document.getElementById('main-nav'); + const navToggle = document.getElementById('nav-toggle'); + const mobileMenu = document.getElementById('mobile-menu'); + const navLinks = document.querySelectorAll('.nav-link, .mobile-link'); + + window.addEventListener('scroll', () => { + nav.classList.toggle('scrolled', window.scrollY > 40); + updateActiveNavLink(); }); -} + + navToggle.addEventListener('click', () => { + navToggle.classList.toggle('open'); + mobileMenu.classList.toggle('show'); + }); + + navLinks.forEach((link) => { + link.addEventListener('click', () => { + navToggle.classList.remove('open'); + mobileMenu.classList.remove('show'); + }); + }); + + function updateActiveNavLink() { + const sections = document.querySelectorAll('.section'); + let current = ''; + sections.forEach((s) => { + const top = s.offsetTop - 120; + if (window.scrollY >= top) current = s.getAttribute('id'); + }); + document.querySelectorAll('.nav-link').forEach((l) => { + l.classList.toggle('active', l.dataset.section === current); + }); + } + + // ─── Scroll Reveal ─── + const revealElements = document.querySelectorAll( + '.feature-card, .stat-card, .gallery-item, .contact-container, .section-header' + ); + revealElements.forEach((el) => el.classList.add('reveal')); + + const revealObserver = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + revealObserver.unobserve(entry.target); + } + }); + }, + { threshold: 0.15, rootMargin: '0px 0px -40px 0px' } + ); + revealElements.forEach((el) => revealObserver.observe(el)); + + // ─── Stat Counters ─── + const statNumbers = document.querySelectorAll('.stat-number'); + const statFills = document.querySelectorAll('.stat-fill'); + let statAnimated = false; + + function animateCounters() { + if (statAnimated) return; + statAnimated = true; + + statNumbers.forEach((el) => { + const target = parseInt(el.dataset.target, 10); + const duration = 2000; + const start = performance.now(); + + function tick(now) { + const elapsed = now - start; + const progress = Math.min(elapsed / duration, 1); + const eased = 1 - Math.pow(1 - progress, 3); // ease-out cubic + el.textContent = Math.round(target * eased).toLocaleString('fr-FR'); + if (progress < 1) requestAnimationFrame(tick); + } + requestAnimationFrame(tick); + }); + + statFills.forEach((el) => { + const w = el.dataset.width; + setTimeout(() => { el.style.width = w + '%'; }, 200); + }); + } + + const statsSection = document.getElementById('stats'); + const statsObserver = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) { + animateCounters(); + statsObserver.unobserve(statsSection); + } + }, + { threshold: 0.3 } + ); + statsObserver.observe(statsSection); + + // ─── Contact Form ─── + const contactForm = document.getElementById('contact-form'); + contactForm.addEventListener('submit', (e) => { + e.preventDefault(); + showToast('✅ Message envoyé avec succès !'); + contactForm.reset(); + }); + + // ─── Toast ─── + const toastEl = document.getElementById('toast'); + let toastTimeout; + function showToast(msg) { + toastEl.textContent = msg; + toastEl.classList.add('show'); + clearTimeout(toastTimeout); + toastTimeout = setTimeout(() => toastEl.classList.remove('show'), 3000); + } + + // ─── PWA Install Prompt ─── + let deferredPrompt; + const installPrompt = document.getElementById('install-prompt'); + const installBtn = document.getElementById('install-btn'); + const dismissInstall = document.getElementById('dismiss-install'); + + window.addEventListener('beforeinstallprompt', (e) => { + e.preventDefault(); + deferredPrompt = e; + installPrompt.hidden = false; + }); + + installBtn.addEventListener('click', async () => { + if (!deferredPrompt) return; + deferredPrompt.prompt(); + const { outcome } = await deferredPrompt.userChoice; + if (outcome === 'accepted') showToast('📲 Application installée !'); + deferredPrompt = null; + installPrompt.hidden = true; + }); + + dismissInstall.addEventListener('click', () => { + installPrompt.hidden = true; + }); + + // ─── Service Worker Registration ─── + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('sw.js') + .then((reg) => console.log('SW registered:', reg.scope)) + .catch((err) => console.warn('SW registration failed:', err)); + }); + } +})(); diff --git a/icon.svg b/icon.svg deleted file mode 100644 index f355c7c..0000000 --- a/icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/icons/icon-192.png b/icons/icon-192.png new file mode 100644 index 0000000..6f6bc11 Binary files /dev/null and b/icons/icon-192.png differ diff --git a/icons/icon-512.png b/icons/icon-512.png new file mode 100644 index 0000000..6f6bc11 Binary files /dev/null and b/icons/icon-512.png differ diff --git a/index.html b/index.html index ae87171..58cefbd 100644 --- a/index.html +++ b/index.html @@ -1,89 +1,312 @@ - + - - - Amusement - Stylish PWA Demo - - - - - - - - - - - - - - + + + + + + + Amusement — Frontend Visual Showcase + + + + + + + + + + -
-
-
-
+ + + + + + + + + + +
+ Accueil + Cartes + Stats + Galerie + Contact
-
-
- - -
- -
+
+ +
-
Next-Gen Experience
-

Stunning visuals,
Zero Compromises.

-

Experience a breathtaking Progressive Web App designed with modern aesthetics, glassmorphism, and seamless interactions.

- -
- - +
✨ Frontend Visual Showcase
+

+ Expériences + Visuelles + Modernes +

+

+ Une collection de composants UI premium avec glassmorphism, + animations fluides et design dark-mode raffiné. +

+
-
-
-
-
- -
+
+
+ Performances +
+
+
+ Analytics +
+
+
+ Engagement +
+
+
+ + +
+
+ +

Cartes Interactives

+

Survolez pour découvrir les effets de profondeur et de lumière

+
+
+
+
+
+
-
-
-
-
-
- 99% - Performance -
-
- PWA - Native Feel -
-
-
- Tap to Interact -
+

Design System

+

Tokens de design cohérents, palette de couleurs harmonieuse et typographie soignée.

+
+
24composants
+
8variantes
+
+
+
+
+
+ +
+

Micro-Animations

+

Transitions fluides et animations GPU-accélérées pour une expérience vivante.

+
+
60fps
+
<16ms
+
+
+
+
+
+ +
+

Glassmorphism

+

Effets de verre dépoli avec transparence, blur et bordures lumineuses.

+
+
CSSnatif
+
GPUaccel.
+
+
+
+
+
+ +
+

Responsive

+

Layouts fluides qui s'adaptent à tous les écrans, mobile-first et performants.

+
+
100score
+
PWAready
+
+
+
+
+ + +
+
+ +

Statistiques en Direct

+

Compteurs animés avec intersection observer

+
+
+
+ 0 + Composants créés +
+
+
+ 0 + Performance Score +
+
+
+ 0 + Animations fluides +
+
+
+ 0 + Jours d'uptime +
+
+
+
+ + + + + +
+
+
+ +

Restons en Contact

+

Envoyez un message pour en savoir plus sur ces composants et techniques frontend.

+
+
+ + hello@amusement.dev +
+
+ + Paris, France +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
- + +
+ +
+ + + + + + + + diff --git a/manifest.json b/manifest.json index b02da6f..d662a32 100644 --- a/manifest.json +++ b/manifest.json @@ -1,16 +1,23 @@ { - "name": "Amusement Dashboard", + "name": "Amusement — Visual Showcase", "short_name": "Amusement", - "description": "A stunning visual demo and Progressive Web App template", - "start_url": "/index.html", + "description": "Showcase de visuels frontend modernes avec glassmorphism, animations et design premium.", + "start_url": ".", "display": "standalone", - "background_color": "#0f172a", - "theme_color": "#0f172a", + "background_color": "#0a0a1a", + "theme_color": "#0a0a1a", + "orientation": "any", "icons": [ { - "src": "./icon.svg", - "sizes": "192x192 512x512", - "type": "image/svg+xml", + "src": "icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", "purpose": "any maskable" } ] diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..cea274d --- /dev/null +++ b/nginx.conf @@ -0,0 +1,33 @@ +server { + listen 2080; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/javascript application/json image/svg+xml; + gzip_min_length 256; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Cache static assets + location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + + # Service worker - no cache + location = /sw.js { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + # SPA fallback + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/package.json b/package.json deleted file mode 100644 index 87e3be2..0000000 --- a/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "amusement-pwa-template", - "version": "1.0.0", - "description": "Stylish PWA Demo", - "scripts": { - "dev": "vite", - "build": "vite build", - "serve": "vite preview" - }, - "devDependencies": { - "vite": "^5.0.0" - } -} diff --git a/style.css b/style.css index 6cf19bd..dc2bd02 100644 --- a/style.css +++ b/style.css @@ -1,352 +1,866 @@ +/* ════════════════════════════════════════════════════ + AMUSEMENT — Premium Dark Mode Design System + ════════════════════════════════════════════════════ */ + +/* ─── Design Tokens ─── */ :root { - --bg-dark: #0f172a; - --text-main: #f8fafc; - --text-muted: #94a3b8; - --primary-gradient: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%); - --glass-bg: rgba(255, 255, 255, 0.03); - --glass-border: rgba(255, 255, 255, 0.08); - --glass-highlight: rgba(255, 255, 255, 0.15); - --font-sans: 'Inter', sans-serif; - --font-display: 'Outfit', sans-serif; + /* Background & Surface */ + --bg-primary: #0a0a1a; + --bg-secondary: #0f0f2a; + --bg-surface: rgba(255, 255, 255, 0.03); + --bg-glass: rgba(255, 255, 255, 0.05); + --bg-glass-hover: rgba(255, 255, 255, 0.08); + + /* Accent gradient */ + --accent-1: #6c5ce7; + --accent-2: #a855f7; + --accent-3: #ec4899; + --accent-4: #06b6d4; + --gradient-primary: linear-gradient(135deg, var(--accent-1), var(--accent-2), var(--accent-3)); + --gradient-subtle: linear-gradient(135deg, rgba(108, 92, 231, 0.15), rgba(168, 85, 247, 0.15)); + + /* Text */ + --text-primary: #f0f0f8; + --text-secondary: rgba(240, 240, 248, 0.6); + --text-muted: rgba(240, 240, 248, 0.35); + + /* Borders */ + --border-glass: rgba(255, 255, 255, 0.08); + --border-glow: rgba(168, 85, 247, 0.3); + + /* Shadows */ + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-md: 0 8px 32px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 16px 64px rgba(0, 0, 0, 0.5); + --shadow-glow: 0 0 40px rgba(168, 85, 247, 0.15); + + /* Spacing */ + --nav-height: 72px; + --section-padding: clamp(4rem, 10vh, 8rem); + --container-max: 1200px; + + /* Typography */ + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + + /* Transitions */ + --ease-smooth: cubic-bezier(0.25, 0.46, 0.45, 0.94); + --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); + --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); + --duration-fast: 200ms; + --duration-normal: 350ms; + --duration-slow: 600ms; } -* { +/* ─── Base Reset ─── */ +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } +html { + scroll-behavior: smooth; + scroll-padding-top: var(--nav-height); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + body { - font-family: var(--font-sans); - background-color: var(--bg-dark); - color: var(--text-main); - min-height: 100vh; + font-family: var(--font-body); + background: var(--bg-primary); + color: var(--text-primary); + line-height: 1.6; overflow-x: hidden; - line-height: 1.5; + min-height: 100vh; } -/* Background Effects */ -.background-effects { +a { color: inherit; text-decoration: none; } +button { cursor: pointer; border: none; background: none; font: inherit; color: inherit; } +img { max-width: 100%; display: block; } + +::selection { + background: rgba(168, 85, 247, 0.3); + color: var(--text-primary); +} + +/* scrollbar */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: var(--bg-primary); } +::-webkit-scrollbar-thumb { background: rgba(168, 85, 247, 0.3); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: rgba(168, 85, 247, 0.5); } + +/* ─── Cursor Glow ─── */ +#cursor-glow { position: fixed; - top: 0; left: 0; right: 0; bottom: 0; - z-index: -1; - overflow: hidden; -} - -.blob { - position: absolute; - filter: blur(80px); + width: 600px; + height: 600px; border-radius: 50%; - opacity: 0.6; - animation: float 20s infinite ease-in-out; + background: radial-gradient(circle, rgba(168, 85, 247, 0.06) 0%, transparent 70%); + pointer-events: none; + z-index: 9999; + transform: translate(-50%, -50%); + transition: opacity 0.3s; + will-change: left, top; } -.blob-1 { - top: -10%; left: -10%; - width: 50vw; height: 50vw; - background: radial-gradient(circle, #6366f1, transparent 70%); - animation-delay: 0s; -} - -.blob-2 { - bottom: -20%; right: -10%; - width: 60vw; height: 60vw; - background: radial-gradient(circle, #ec4899, transparent 70%); - animation-delay: -5s; -} - -.blob-3 { - top: 30%; left: 40%; - width: 40vw; height: 40vw; - background: radial-gradient(circle, #a855f7, transparent 70%); - animation-delay: -10s; +/* ─── Particles Canvas ─── */ +#particles-canvas { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 0; opacity: 0.4; } -@keyframes float { - 0%, 100% { transform: translate(0, 0) scale(1); } - 33% { transform: translate(5%, 10%) scale(1.1); } - 66% { transform: translate(-5%, 5%) scale(0.9); } +/* ─── Glass Utilities ─── */ +.glass-card { + background: var(--bg-glass); + backdrop-filter: blur(20px) saturate(1.2); + -webkit-backdrop-filter: blur(20px) saturate(1.2); + border: 1px solid var(--border-glass); + border-radius: 16px; + transition: all var(--duration-normal) var(--ease-smooth); +} +.glass-card:hover { + background: var(--bg-glass-hover); + border-color: var(--border-glow); + box-shadow: var(--shadow-glow); } -/* Layout */ -.app-container { - max-width: 1400px; +.glass-panel { + background: rgba(10, 10, 26, 0.9); + backdrop-filter: blur(30px) saturate(1.4); + -webkit-backdrop-filter: blur(30px) saturate(1.4); + border: 1px solid var(--border-glass); +} + +.glass-nav { + background: rgba(10, 10, 26, 0.7); + backdrop-filter: blur(20px) saturate(1.2); + -webkit-backdrop-filter: blur(20px) saturate(1.2); + border-bottom: 1px solid var(--border-glass); +} + +/* ─── Gradient Text ─── */ +.gradient-text { + background: var(--gradient-primary); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* ─── Buttons ─── */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 28px; + border-radius: 12px; + font-weight: 600; + font-size: 0.95rem; + transition: all var(--duration-normal) var(--ease-smooth); + position: relative; + overflow: hidden; +} +.btn::before { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent); + opacity: 0; + transition: opacity var(--duration-fast); +} +.btn:hover::before { opacity: 1; } + +.btn-primary { + background: var(--gradient-primary); + color: white; + box-shadow: 0 4px 20px rgba(168, 85, 247, 0.3); +} +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(168, 85, 247, 0.4); +} +.btn-primary:active { transform: translateY(0); } + +.btn-ghost { + border: 1px solid var(--border-glass); + color: var(--text-secondary); +} +.btn-ghost:hover { + border-color: var(--border-glow); + color: var(--text-primary); + background: var(--bg-glass); +} + +.btn-full { width: 100%; justify-content: center; } +.btn-sm { padding: 8px 16px; font-size: 0.85rem; border-radius: 8px; } + +/* ─── Section Base ─── */ +.section { + position: relative; + z-index: 1; + padding: var(--section-padding) clamp(1.5rem, 5vw, 3rem); + max-width: var(--container-max); margin: 0 auto; - padding: 2rem; +} +.section-header { + text-align: center; + margin-bottom: 3.5rem; +} +.section-tag { + display: inline-block; + padding: 6px 16px; + border-radius: 20px; + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + background: var(--gradient-subtle); + border: 1px solid rgba(168, 85, 247, 0.2); + color: var(--accent-2); + margin-bottom: 1rem; +} +.section-title { + font-size: clamp(2rem, 5vw, 3rem); + font-weight: 800; + letter-spacing: -0.02em; + line-height: 1.2; + margin-bottom: 0.75rem; +} +.section-subtitle { + color: var(--text-secondary); + font-size: 1.05rem; + max-width: 500px; + margin: 0 auto; +} + +/* ─── Navigation ─── */ +nav.glass-nav { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + height: var(--nav-height); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 clamp(1.5rem, 5vw, 3rem); + transition: all var(--duration-normal) var(--ease-smooth); +} +nav.scrolled { + background: rgba(10, 10, 26, 0.9); + box-shadow: var(--shadow-sm); +} +.nav-brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 700; + font-size: 1.15rem; +} +.brand-icon { + font-size: 1.5rem; + filter: drop-shadow(0 0 8px rgba(168, 85, 247, 0.4)); +} +.nav-links { + display: flex; + gap: 8px; +} +.nav-link { + padding: 8px 16px; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + color: var(--text-secondary); + transition: all var(--duration-fast) var(--ease-smooth); + position: relative; +} +.nav-link:hover, .nav-link.active { + color: var(--text-primary); + background: var(--bg-glass); +} +.nav-link.active::after { + content: ''; + position: absolute; + bottom: 4px; + left: 50%; + width: 20px; + height: 2px; + background: var(--gradient-primary); + border-radius: 1px; + transform: translateX(-50%); +} +.nav-toggle { + display: none; + flex-direction: column; + gap: 5px; + padding: 8px; +} +.nav-toggle span { + width: 22px; + height: 2px; + background: var(--text-primary); + border-radius: 2px; + transition: all var(--duration-fast) var(--ease-smooth); +} +.nav-toggle.open span:nth-child(1) { transform: rotate(45deg) translate(5px, 5px); } +.nav-toggle.open span:nth-child(2) { opacity: 0; } +.nav-toggle.open span:nth-child(3) { transform: rotate(-45deg) translate(5px, -5px); } + +/* Mobile menu */ +.mobile-menu { + position: fixed; + top: var(--nav-height); + left: 0; + right: 0; + z-index: 99; + display: none; + flex-direction: column; + padding: 1rem 1.5rem 1.5rem; + border-radius: 0 0 16px 16px; + transform: translateY(-20px); + opacity: 0; + transition: all var(--duration-normal) var(--ease-smooth); +} +.mobile-menu.show { + display: flex; + transform: translateY(0); + opacity: 1; +} +.mobile-link { + padding: 12px 16px; + border-radius: 8px; + font-weight: 500; + color: var(--text-secondary); + transition: all var(--duration-fast); +} +.mobile-link:hover { + background: var(--bg-glass); + color: var(--text-primary); +} + +@media (max-width: 768px) { + .nav-links { display: none; } + .nav-toggle { display: flex; } +} + +/* ─── Hero ─── */ +.hero-section { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: space-between; + gap: 4rem; + padding-top: calc(var(--nav-height) + 2rem); +} +.hero-content { + flex: 1; + max-width: 560px; +} +.hero-badge { + display: inline-block; + padding: 8px 18px; + border-radius: 24px; + font-size: 0.85rem; + font-weight: 500; + background: var(--gradient-subtle); + border: 1px solid rgba(168, 85, 247, 0.2); + color: var(--accent-2); + margin-bottom: 1.5rem; + animation: fadeInUp 0.8s var(--ease-smooth) both; +} +.hero-title { + font-size: clamp(2.8rem, 6vw, 4.5rem); + font-weight: 900; + line-height: 1.08; + letter-spacing: -0.03em; + margin-bottom: 1.5rem; +} +.title-line { + display: block; + animation: fadeInUp 0.8s var(--ease-smooth) both; +} +.title-line:nth-child(1) { animation-delay: 0.1s; } +.title-line:nth-child(2) { animation-delay: 0.25s; } +.title-line:nth-child(3) { animation-delay: 0.4s; } + +.hero-description { + color: var(--text-secondary); + font-size: 1.1rem; + line-height: 1.7; + margin-bottom: 2rem; + max-width: 460px; + animation: fadeInUp 0.8s 0.5s var(--ease-smooth) both; +} +.hero-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + animation: fadeInUp 0.8s 0.65s var(--ease-smooth) both; +} + +/* Hero floating cards */ +.hero-visual { + flex: 1; + position: relative; + height: 420px; + max-width: 420px; +} +.floating-card { + position: absolute; + padding: 20px; + border-radius: 16px; display: flex; flex-direction: column; - min-height: 100vh; -} - -/* Header Glassmorphism */ -.glass-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 2rem; - background: var(--glass-bg); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--glass-border); - border-radius: 24px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); -} - -.logo { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.logo h1 { - font-family: var(--font-display); - font-size: 1.5rem; - font-weight: 800; - letter-spacing: -0.5px; -} - -.logo-icon { - font-size: 1.5rem; -} - -nav { - display: flex; - gap: 1rem; -} - -.nav-btn { - background: transparent; - border: none; - color: var(--text-muted); - font-family: var(--font-sans); + gap: 10px; + font-size: 0.85rem; font-weight: 500; - font-size: 1rem; - cursor: pointer; - padding: 0.5rem 1rem; - border-radius: 12px; - transition: all 0.3s ease; + color: var(--text-secondary); + animation: float 6s ease-in-out infinite; + will-change: transform; } +.card-1 { top: 10%; left: 10%; width: 180px; animation-delay: 0s; } +.card-2 { top: 35%; right: 5%; width: 190px; animation-delay: -2s; } +.card-3 { bottom: 10%; left: 20%; width: 170px; animation-delay: -4s; } -.nav-btn:hover { - color: var(--text-main); - background: var(--glass-highlight); +.mini-chart { + width: 100%; + height: 40px; + background: linear-gradient(90deg, var(--accent-1), var(--accent-2), var(--accent-3)); + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40'%3E%3Cpath d='M0 35 Q15 10 30 25 T60 15 T100 20 V40 H0Z' fill='black'/%3E%3C/svg%3E"); + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 40'%3E%3Cpath d='M0 35 Q15 10 30 25 T60 15 T100 20 V40 H0Z' fill='black'/%3E%3C/svg%3E"); + mask-size: cover; + -webkit-mask-size: cover; + border-radius: 4px; } - -.nav-btn.active { - color: var(--text-main); -} - -.nav-btn.primary { - background: var(--text-main); - color: var(--bg-dark); - font-weight: 600; -} -.nav-btn.primary:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2); -} - -/* Hero Section */ -.hero-section { - flex: 1; - display: grid; - grid-template-columns: 1fr 1fr; - align-items: center; - gap: 4rem; - margin-top: 4rem; -} - -.badge { - display: inline-block; - padding: 0.5rem 1rem; - background: rgba(99, 102, 241, 0.1); - border: 1px solid rgba(99, 102, 241, 0.3); - color: #818cf8; - border-radius: 100px; - font-size: 0.875rem; - font-weight: 600; - margin-bottom: 1.5rem; - letter-spacing: 0.5px; - text-transform: uppercase; -} - -.display-text { - font-family: var(--font-display); - font-size: 4.5rem; - line-height: 1.1; - font-weight: 800; - margin-bottom: 1.5rem; - letter-spacing: -1px; -} - -.gradient-text { - background: var(--primary-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.subtitle { - font-size: 1.25rem; - color: var(--text-muted); - max-width: 500px; - margin-bottom: 2.5rem; -} - -.action-group { +.mini-bars { display: flex; - gap: 1rem; + align-items: flex-end; + gap: 4px; + height: 40px; +} +.mini-bars::before, +.mini-bars::after { + content: ''; + flex: 1; + border-radius: 2px; + background: var(--accent-2); +} +.mini-bars::before { height: 60%; } +.mini-bars::after { height: 85%; } +.mini-ring { + width: 40px; + height: 40px; + border: 3px solid transparent; + border-top-color: var(--accent-3); + border-right-color: var(--accent-2); + border-radius: 50%; + animation: spin 3s linear infinite; } -.btn { +@media (max-width: 900px) { + .hero-section { flex-direction: column; text-align: center; } + .hero-content { max-width: 100%; } + .hero-description { margin-left: auto; margin-right: auto; } + .hero-actions { justify-content: center; } + .hero-visual { display: none; } +} + +/* ─── Feature Cards ─── */ +.cards-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1.5rem; +} +.feature-card { + padding: 2rem; + position: relative; + overflow: hidden; + cursor: default; +} +.feature-card:hover { transform: translateY(-4px); } +.card-glow { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(168,85,247,0.08), transparent 60%); + pointer-events: none; + opacity: 0; + transition: opacity var(--duration-normal); +} +.feature-card:hover .card-glow { opacity: 1; } + +.card-icon { + width: 56px; + height: 56px; display: flex; align-items: center; justify-content: center; - gap: 0.5rem; - padding: 1rem 2rem; - border-radius: 16px; - font-family: var(--font-sans); - font-weight: 600; - font-size: 1.125rem; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - border: none; + border-radius: 14px; + background: var(--gradient-subtle); + border: 1px solid rgba(168, 85, 247, 0.15); + color: var(--accent-2); + margin-bottom: 1.25rem; } - -.btn-primary { - background: var(--primary-gradient); - color: white; - box-shadow: 0 10px 30px rgba(99, 102, 241, 0.4); +.card-title { + font-size: 1.15rem; + font-weight: 700; + margin-bottom: 0.5rem; } - -.btn-primary:hover { - transform: translateY(-3px) scale(1.02); - box-shadow: 0 15px 40px rgba(99, 102, 241, 0.5); +.card-text { + color: var(--text-secondary); + font-size: 0.9rem; + line-height: 1.6; + margin-bottom: 1.25rem; } - -.btn-secondary { - background: var(--glass-bg); - border: 1px solid var(--glass-border); - color: var(--text-main); - backdrop-filter: blur(10px); -} - -.btn-secondary:hover { - background: var(--glass-highlight); - transform: translateY(-3px); -} - -/* Glass Card right side */ -.glass-card { - background: var(--glass-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--glass-border); - border-radius: 32px; - padding: 2rem; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); - position: relative; - overflow: hidden; - transform: perspective(1000px) rotateY(-5deg) rotateX(5deg); - transition: transform 0.5s ease; -} - -.glass-card:hover { - transform: perspective(1000px) rotateY(0deg) rotateX(0deg); -} - -.glass-card::before { - content: ''; - position: absolute; - top: 0; left: -100%; - width: 50%; height: 100%; - background: linear-gradient(to right, transparent, rgba(255,255,255,0.1), transparent); - transform: skewX(-20deg); - animation: shine 6s infinite; -} - -@keyframes shine { - 0% { left: -100%; } - 20% { left: 200%; } - 100% { left: 200%; } -} - -.card-header .dots { +.card-metrics { display: flex; - gap: 8px; - margin-bottom: 2rem; + gap: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--border-glass); +} +.metric { display: flex; flex-direction: column; } +.metric-value { + font-size: 1.25rem; + font-weight: 800; + font-family: var(--font-mono); + background: var(--gradient-primary); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} +.metric-label { + font-size: 0.75rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; } -.dots span { - width: 12px; height: 12px; - border-radius: 50%; -} -.dots span:nth-child(1) { background: #ef4444; } -.dots span:nth-child(2) { background: #eab308; } -.dots span:nth-child(3) { background: #22c55e; } - -.skeleton-line { - height: 24px; - background: rgba(255, 255, 255, 0.05); - border-radius: 12px; - margin-bottom: 1rem; -} -.skeleton-line.full { width: 100%; } -.skeleton-line.medium { width: 70%; margin-bottom: 2rem; } - +/* ─── Stats ─── */ .stats-grid { display: grid; - grid-template-columns: 1fr 1fr; - gap: 1rem; - margin-bottom: 2rem; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.5rem; } - -.stat-box { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 16px; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 0.5rem; +.stat-card { + padding: 2rem; + text-align: center; } - -.stat-value { - font-family: var(--font-display); - font-size: 2rem; - font-weight: 800; - color: var(--text-main); +.stat-number { + display: block; + font-size: 3rem; + font-weight: 900; + font-family: var(--font-mono); + background: var(--gradient-primary); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + line-height: 1.1; + margin-bottom: 0.5rem; } .stat-label { - font-size: 0.875rem; - color: var(--text-muted); + color: var(--text-secondary); + font-size: 0.9rem; + margin-bottom: 1rem; + display: block; +} +.stat-bar { + width: 100%; + height: 4px; + background: var(--bg-glass); + border-radius: 2px; + overflow: hidden; +} +.stat-fill { + height: 100%; + width: 0; + background: var(--gradient-primary); + border-radius: 2px; + transition: width 1.5s var(--ease-smooth); } -.interactive-element { - background: var(--primary-gradient); - padding: 1rem; - border-radius: 16px; - text-align: center; - font-weight: 600; +/* ─── Gallery ─── */ +.gallery-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.25rem; +} +.gallery-item { + position: relative; + aspect-ratio: 1; + overflow: hidden; cursor: pointer; - transition: all 0.2s; - user-select: none; } -.interactive-element:active { - transform: scale(0.95); +.gallery-wide { + grid-column: span 2; + aspect-ratio: 2 / 1; +} +.gallery-visual { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; +} +.gallery-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: flex-end; + padding: 1.25rem; + background: linear-gradient(to top, rgba(10,10,26,0.8) 0%, transparent 60%); + opacity: 0; + transition: opacity var(--duration-normal) var(--ease-smooth); +} +.gallery-item:hover .gallery-overlay { opacity: 1; } +.gallery-item:hover { transform: scale(1.02); } +.gallery-tag { + font-size: 0.85rem; + font-weight: 600; + padding: 6px 14px; + border-radius: 8px; + background: var(--bg-glass); + border: 1px solid var(--border-glass); } -/* Responsive */ -@media (max-width: 1024px) { - .hero-section { - grid-template-columns: 1fr; - text-align: center; - } - .display-text { font-size: 3.5rem; } - .subtitle { margin: 0 auto 2.5rem; } - .action-group { justify-content: center; } - .glass-card { margin-top: 2rem; transform: none; } - .glass-card:hover { transform: translateY(-5px); } - nav { display: none; } +/* Abstract shapes */ +.abstract-shape { + position: absolute; + inset: 0; + transition: transform var(--duration-slow) var(--ease-smooth); +} +.gallery-item:hover .abstract-shape { transform: scale(1.1); } + +.shape-a { + background: + radial-gradient(circle at 30% 40%, hsla(260, 80%, 60%, 0.6), transparent 50%), + radial-gradient(circle at 70% 60%, hsla(320, 80%, 55%, 0.5), transparent 45%), + radial-gradient(circle at 50% 80%, hsla(200, 80%, 50%, 0.4), transparent 40%); +} +.shape-b { + background: linear-gradient( + 45deg, + hsla(320, 80%, 50%, 0.5) 0%, + hsla(260, 70%, 60%, 0.4) 25%, + hsla(180, 60%, 50%, 0.3) 50%, + hsla(140, 70%, 45%, 0.4) 75%, + hsla(320, 80%, 50%, 0.5) 100% + ); + background-size: 300% 300%; + animation: auroraShift 8s ease-in-out infinite; +} +.shape-c { + background: radial-gradient(circle at 50% 50%, hsla(180, 80%, 55%, 0.6), transparent 55%); + filter: blur(30px); + animation: blobPulse 4s ease-in-out infinite; +} +.shape-d { + background: + linear-gradient(0deg, hsla(30, 90%, 55%, 0) 40%, hsla(30, 90%, 55%, 0.1) 42%, hsla(30, 90%, 55%, 0.6) 50%, hsla(30, 90%, 55%, 0.1) 58%, hsla(30, 90%, 55%, 0) 60%), + linear-gradient(90deg, hsla(350, 90%, 55%, 0) 40%, hsla(350, 90%, 55%, 0.1) 42%, hsla(350, 90%, 55%, 0.6) 50%, hsla(350, 90%, 55%, 0.1) 58%, hsla(350, 90%, 55%, 0) 60%); + animation: neonPulse 2s ease-in-out infinite; +} +.shape-e { + background: repeating-linear-gradient( + 90deg, + hsla(140, 70%, 50%, 0.05) 0px, + hsla(140, 70%, 50%, 0.3) 2px, + hsla(140, 70%, 50%, 0.05) 4px + ); + animation: waveScroll 3s linear infinite; +} +.shape-f { + background: + linear-gradient(135deg, hsla(210, 80%, 60%, 0.3), transparent 50%), + linear-gradient(225deg, hsla(280, 80%, 60%, 0.3), transparent 50%), + linear-gradient(315deg, hsla(340, 80%, 60%, 0.3), transparent 50%); +} + +@media (max-width: 768px) { + .gallery-grid { grid-template-columns: 1fr 1fr; } + .gallery-wide { grid-column: span 2; } +} +@media (max-width: 480px) { + .gallery-grid { grid-template-columns: 1fr; } + .gallery-wide { grid-column: span 1; aspect-ratio: 16 / 9; } +} + +/* ─── Contact ─── */ +.contact-section { padding-bottom: 6rem; } +.contact-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 3rem; + padding: 3rem; +} +.contact-info { display: flex; flex-direction: column; justify-content: center; } +.contact-text { + color: var(--text-secondary); + margin-bottom: 2rem; + line-height: 1.7; +} +.contact-details { display: flex; flex-direction: column; gap: 1rem; } +.contact-item { + display: flex; + align-items: center; + gap: 12px; + color: var(--text-secondary); + font-size: 0.95rem; +} +.contact-item svg { color: var(--accent-2); flex-shrink: 0; } + +.contact-form { display: flex; flex-direction: column; gap: 1.25rem; } +.form-group { display: flex; flex-direction: column; gap: 6px; } +.form-group label { + font-size: 0.85rem; + font-weight: 500; + color: var(--text-secondary); +} +.form-input { + padding: 12px 16px; + border-radius: 10px; + border: 1px solid var(--border-glass); + background: var(--bg-glass); + color: var(--text-primary); + font-family: var(--font-body); + font-size: 0.95rem; + transition: all var(--duration-fast) var(--ease-smooth); + outline: none; + resize: vertical; +} +.form-input::placeholder { color: var(--text-muted); } +.form-input:focus { + border-color: var(--border-glow); + box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.1); +} + +@media (max-width: 768px) { + .contact-container { grid-template-columns: 1fr; padding: 2rem; } +} + +/* ─── Footer ─── */ +.site-footer { + text-align: center; + padding: 2rem clamp(1.5rem, 5vw, 3rem); + border-top: 1px solid var(--border-glass); + border-bottom: none; +} +.footer-content { + max-width: var(--container-max); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; +} +.footer-brand { + display: flex; + align-items: center; + gap: 8px; + font-weight: 700; +} +.footer-copy { + color: var(--text-muted); + font-size: 0.85rem; +} +.footer-links { + display: flex; + gap: 1.5rem; +} +.footer-links a { + color: var(--text-secondary); + font-size: 0.85rem; + transition: color var(--duration-fast); +} +.footer-links a:hover { color: var(--text-primary); } + +/* ─── Toast ─── */ +.toast { + position: fixed; + bottom: 2rem; + left: 50%; + transform: translateX(-50%) translateY(120%); + padding: 14px 24px; + border-radius: 12px; + font-size: 0.9rem; + font-weight: 500; + background: var(--bg-glass); + backdrop-filter: blur(20px); + border: 1px solid var(--border-glass); + box-shadow: var(--shadow-md); + z-index: 200; + transition: transform var(--duration-normal) var(--ease-spring); + white-space: nowrap; +} +.toast.show { + transform: translateX(-50%) translateY(0); +} + +/* ─── Install Prompt ─── */ +.install-prompt { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + z-index: 150; + display: flex; + align-items: center; + gap: 12px; + padding: 14px 18px; + font-size: 0.9rem; + animation: fadeInUp 0.5s var(--ease-spring) both; +} +.btn-dismiss { + color: var(--text-muted); + font-size: 1.1rem; + padding: 4px; + transition: color var(--duration-fast); +} +.btn-dismiss:hover { color: var(--text-primary); } + +/* ─── Reveal Animations ─── */ +.reveal { + opacity: 0; + transform: translateY(30px); + transition: all 0.7s var(--ease-smooth); +} +.reveal.visible { + opacity: 1; + transform: translateY(0); +} + +/* ─── Keyframes ─── */ +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-15px); } +} +@keyframes spin { + to { transform: rotate(360deg); } +} +@keyframes auroraShift { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} +@keyframes blobPulse { + 0%, 100% { transform: scale(1); filter: blur(30px); } + 50% { transform: scale(1.2); filter: blur(40px); } +} +@keyframes neonPulse { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} +@keyframes waveScroll { + from { background-position-x: 0; } + to { background-position-x: 40px; } } diff --git a/sw.js b/sw.js index 93da150..46044df 100644 --- a/sw.js +++ b/sw.js @@ -1,48 +1,57 @@ -const CACHE_NAME = 'amusement-pwa-cache-v1'; -const urlsToCache = [ +/* ════════════════════════════════════════════════════ + AMUSEMENT — Service Worker + Cache-first strategy for offline PWA support + ════════════════════════════════════════════════════ */ + +const CACHE_NAME = 'amusement-v1'; +const ASSETS = [ './', './index.html', './style.css', './app.js', './manifest.json', - './icon.svg', - 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&family=Outfit:wght@400;600;800&display=swap' ]; -self.addEventListener('install', event => { +// Install: pre-cache core assets +self.addEventListener('install', (event) => { event.waitUntil( - caches.open(CACHE_NAME) - .then(cache => { - return cache.addAll(urlsToCache); - }) + caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)) ); + self.skipWaiting(); }); -self.addEventListener('fetch', event => { - event.respondWith( - caches.match(event.request) - .then(response => { - // Cache hit - return response - if (response) { - return response; - } - return fetch(event.request); - } +// Activate: clean old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all( + keys + .filter((k) => k !== CACHE_NAME) + .map((k) => caches.delete(k)) + ) ) ); + self.clients.claim(); }); -self.addEventListener('activate', event => { - const cacheWhitelist = [CACHE_NAME]; - event.waitUntil( - caches.keys().then(cacheNames => { - return Promise.all( - cacheNames.map(cacheName => { - if (cacheWhitelist.indexOf(cacheName) === -1) { - return caches.delete(cacheName); - } - }) - ); +// Fetch: cache-first, fallback to network +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request).then((cached) => { + if (cached) return cached; + return fetch(event.request).then((response) => { + if (!response || response.status !== 200 || response.type !== 'basic') { + return response; + } + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + return response; + }); + }).catch(() => { + // If both cache and network fail, return offline fallback + if (event.request.mode === 'navigate') { + return caches.match('./index.html'); + } }) ); });