From 6e7d01234ebd95933e459f1a0f62b716f3f93725 Mon Sep 17 00:00:00 2001 From: Riley Zhang Date: Wed, 24 Jun 2026 09:14:46 +0200 Subject: [PATCH] feat(site): add anime.js v4 magic background, remove Three.js - Replace Three.js canvas with anime.js magic background layers (aurora, runes, orbs, sparkles, cursor glow) - Update script tags to anime.js v4 UMD via jsDelivr CDN - Add magic background CSS to style.css - Update main.js with 6-layer anime.js v4 background implementation - Update blog pages with same magic background - Removed Three.js dependency Co-Authored-By: Paperclip --- site/blog/index.html | 29 +- .../blog/why-we-are-building-hatch/index.html | 29 +- site/index.html | 24 +- site/main.js | 566 ++++++++++++------ site/style.css | 471 +++++++++++++-- 5 files changed, 873 insertions(+), 246 deletions(-) diff --git a/site/blog/index.html b/site/blog/index.html index 3e41c19f..fc7b692b 100644 --- a/site/blog/index.html +++ b/site/blog/index.html @@ -9,21 +9,38 @@ - - + + - + - + + + +
@@ -56,5 +73,9 @@

© 2026 El Foundation. Hatch is released under the MIT License.

+ + + + \ No newline at end of file diff --git a/site/blog/why-we-are-building-hatch/index.html b/site/blog/why-we-are-building-hatch/index.html index 82b09ef3..f901f030 100644 --- a/site/blog/why-we-are-building-hatch/index.html +++ b/site/blog/why-we-are-building-hatch/index.html @@ -9,8 +9,8 @@ - - + + @@ -19,13 +19,30 @@ - + - + + + +
@@ -116,5 +133,9 @@ docker compose up

© 2026 El Foundation. Hatch is released under the MIT License.

+ + + + \ No newline at end of file diff --git a/site/index.html b/site/index.html index d19d8b63..ff730b2a 100644 --- a/site/index.html +++ b/site/index.html @@ -30,8 +30,22 @@ - - + +
@@ -162,9 +176,7 @@
- - - - + + diff --git a/site/main.js b/site/main.js index 3ee955c4..e2ad715d 100644 --- a/site/main.js +++ b/site/main.js @@ -1,266 +1,440 @@ /** - * Hatch Homepage v2 — Three.js particle network + anime.js entrance animations + * Hatch Homepage — Anime.js Magic Background + Entrance Animations + * + * Multi-layered anime.js-powered magical background: + * 1. Aurora blobs — soft gradient spheres that drift and shift color + * 2. Rune circles — SVG rings rotating at different speeds + * 3. Floating orbs — luminous dots with organic drift + * 4. Energy connections — lines between nearby orbs that pulse + * 5. Sparkle particles — twinkling dots with staggered opacity + * 6. Cursor glow — radial glow that follows the mouse * - * Particle network: nodes float gently, connected by proximity lines. - * Represents HTTP requests flowing through the network. * Entrance: elements with [data-animate] fade up on scroll. + * Performance: orb tick throttled to ~30fps, connections drawn every 3rd frame. */ (function () { 'use strict'; + var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + var isMobile = window.innerWidth < 768; + // ============================================================ - // Three.js Particle Network + // 1. Aurora Blobs — slow drift + color pulse // ============================================================ - const canvas = document.getElementById('particle-canvas'); - if (!canvas) return; - - const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - - if (!prefersReducedMotion && typeof THREE !== 'undefined') { - initParticleNetwork(); + function initAurora(animate, random) { + var blobs = document.querySelectorAll('.magic-aurora'); + blobs.forEach(function (blob, i) { + animate(blob, { + translateX: [ + { to: random(-40, 40), duration: random(8000, 12000) }, + { to: random(-30, 30), duration: random(8000, 12000) }, + { to: 0, duration: random(8000, 12000) } + ], + translateY: [ + { to: random(-30, 30), duration: random(10000, 14000) }, + { to: random(-40, 40), duration: random(10000, 14000) }, + { to: 0, duration: random(10000, 14000) } + ], + scale: [ + { to: random(0.85, 1.15), duration: random(6000, 10000) }, + { to: random(0.9, 1.1), duration: random(6000, 10000) }, + { to: 1, duration: random(6000, 10000) } + ], + opacity: [ + { to: random(0.06, 0.18), duration: random(5000, 8000) }, + { to: random(0.08, 0.14), duration: random(5000, 8000) }, + { to: i === 0 ? 0.12 : i === 1 ? 0.1 : 0.08, duration: random(5000, 8000) } + ], + loop: true, + alternate: true, + ease: 'easeInOutSine' + }); + }); } - function initParticleNetwork() { - const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera( - 60, - window.innerWidth / window.innerHeight, - 0.1, - 1000 - ); - camera.position.z = 50; + // ============================================================ + // 2. Rune Circles — continuous rotation at different speeds + // ============================================================ - const renderer = new THREE.WebGLRenderer({ - canvas: canvas, - alpha: true, - antialias: false, - }); - renderer.setSize(window.innerWidth, window.innerHeight); - renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + function initRunes(animate, random) { + var runeData = [ + { sel: '.rune-outer', speed: 120, dir: 1 }, + { sel: '.rune-mid', speed: 80, dir: -1 }, + { sel: '.rune-inner', speed: 60, dir: 1 }, + { sel: '.rune-core', speed: 40, dir: -1 } + ]; - // Particle parameters - const PARTICLE_COUNT = 80; - const CONNECTION_DISTANCE = 12; - const PARTICLE_SIZE = 0.15; + runeData.forEach(function (r) { + var el = document.querySelector(r.sel); + if (!el) return; - // Create particles - const particlesGeometry = new THREE.BufferGeometry(); - const positions = new Float32Array(PARTICLE_COUNT * 3); - const velocities = []; - const opacities = new Float32Array(PARTICLE_COUNT); - - for (let i = 0; i < PARTICLE_COUNT; i++) { - positions[i * 3] = (Math.random() - 0.5) * 100; - positions[i * 3 + 1] = (Math.random() - 0.5) * 60; - positions[i * 3 + 2] = (Math.random() - 0.5) * 30; - - velocities.push({ - x: (Math.random() - 0.5) * 0.02, - y: (Math.random() - 0.5) * 0.02, - z: (Math.random() - 0.5) * 0.01, + animate(el, { + opacity: parseFloat(getComputedStyle(el).opacity) || 0.06, + duration: 2000, + delay: random(500, 1500), + ease: 'outQuad' }); - opacities[i] = 0.3 + Math.random() * 0.5; + animate(el, { + rotate: r.dir > 0 ? '1turn' : '-1turn', + duration: r.speed * 1000, + loop: true, + ease: 'linear' + }); + }); + + var core = document.querySelector('.rune-core'); + if (core) { + animate(core, { + scale: [ + { to: 1.05, duration: 3000 }, + { to: 0.95, duration: 3000 }, + { to: 1, duration: 3000 } + ], + loop: true, + ease: 'easeInOutSine' + }); } + } - particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + // ============================================================ + // 3. Floating Orbs — luminous dots with organic drift + // ============================================================ - const particlesMaterial = new THREE.PointsMaterial({ - color: 0x3b82f6, - size: PARTICLE_SIZE, - transparent: true, - opacity: 0.6, - sizeAttenuation: true, - blending: THREE.AdditiveBlending, - depthWrite: false, - }); + var orbState = []; - const particles = new THREE.Points(particlesGeometry, particlesMaterial); - scene.add(particles); + function createOrbs(count, animate, random) { + var container = document.getElementById('magic-orbs'); + if (!container) return; - // Connection lines - const lineMaterial = new THREE.LineBasicMaterial({ - color: 0x3b82f6, - transparent: true, - opacity: 0.08, - blending: THREE.AdditiveBlending, - depthWrite: false, - }); + var colors = [ + 'rgba(59, 130, 246, VAR)', + 'rgba(168, 85, 247, VAR)', + 'rgba(6, 182, 212, VAR)', + 'rgba(99, 102, 241, VAR)', + 'rgba(14, 165, 233, VAR)' + ]; - const linesGroup = new THREE.Group(); - scene.add(linesGroup); + for (var i = 0; i < count; i++) { + var size = random(3, 8); + var opacity = random(0.15, 0.5); + var colorIdx = Math.min(Math.floor(random(0, colors.length)), colors.length - 1); + var glowIdx = Math.min(Math.floor(random(0, colors.length)), colors.length - 1); + var color = colors[colorIdx].replace('VAR', opacity.toFixed(2)); + var glowColor = colors[glowIdx].replace('VAR', (opacity * 0.4).toFixed(2)); - // Mouse interaction - let mouseX = 0; - let mouseY = 0; - document.addEventListener('mousemove', (e) => { - mouseX = (e.clientX / window.innerWidth - 0.5) * 2; - mouseY = -(e.clientY / window.innerHeight - 0.5) * 2; - }); + var el = document.createElement('div'); + el.className = 'magic-orb'; + el.style.width = size + 'px'; + el.style.height = size + 'px'; + el.style.background = color; + el.style.boxShadow = '0 0 ' + (size * 3) + 'px ' + glowColor; + container.appendChild(el); - // Resize - function onResize() { - camera.aspect = window.innerWidth / window.innerHeight; - camera.updateProjectionMatrix(); - renderer.setSize(window.innerWidth, window.innerHeight); + orbState.push({ + x: random(0, 100), + y: random(0, 100), + vx: random(-0.015, 0.015), + vy: random(-0.015, 0.015), + el: el, + size: size + }); + + el.style.left = orbState[i].x + '%'; + el.style.top = orbState[i].y + '%'; + + animate(el, { + opacity: [0, 1], + scale: [0, 1], + duration: random(800, 1600), + delay: random(200, 2000), + ease: 'outExpo' + }); } - window.addEventListener('resize', onResize); + } - // Animate - function animate() { - requestAnimationFrame(animate); + // Throttled orb tick — runs at ~30fps for performance + var lastOrbTick = 0; + var orbTickInterval = 33; // ~30fps + var frameCount = 0; - const posArray = particlesGeometry.attributes.position.array; + function tickOrbs(timestamp) { + requestAnimationFrame(tickOrbs); - // Update particle positions - for (let i = 0; i < PARTICLE_COUNT; i++) { - posArray[i * 3] += velocities[i].x; - posArray[i * 3 + 1] += velocities[i].y; - posArray[i * 3 + 2] += velocities[i].z; + if (timestamp - lastOrbTick < orbTickInterval) return; + lastOrbTick = timestamp; + frameCount++; - // Wrap around edges - if (posArray[i * 3] > 55) posArray[i * 3] = -55; - if (posArray[i * 3] < -55) posArray[i * 3] = 55; - if (posArray[i * 3 + 1] > 35) posArray[i * 3 + 1] = -35; - if (posArray[i * 3 + 1] < -35) posArray[i * 3 + 1] = 35; - if (posArray[i * 3 + 2] > 20) posArray[i * 3 + 2] = -20; - if (posArray[i * 3 + 2] < -20) posArray[i * 3 + 2] = 20; - } + orbState.forEach(function (orb) { + orb.x += orb.vx; + orb.y += orb.vy; - particlesGeometry.attributes.position.needsUpdate = true; + if (orb.x < -2 || orb.x > 102) orb.vx *= -1; + if (orb.y < -2 || orb.y > 102) orb.vy *= -1; - // Update connection lines - while (linesGroup.children.length > 0) { - const child = linesGroup.children[0]; - child.geometry.dispose(); - child.material.dispose(); - linesGroup.remove(child); - } + orb.x = Math.max(-2, Math.min(102, orb.x)); + orb.y = Math.max(-2, Math.min(102, orb.y)); - for (let i = 0; i < PARTICLE_COUNT; i++) { - for (let j = i + 1; j < PARTICLE_COUNT; j++) { - const dx = posArray[i * 3] - posArray[j * 3]; - const dy = posArray[i * 3 + 1] - posArray[j * 3 + 1]; - const dz = posArray[i * 3 + 2] - posArray[j * 3 + 2]; - const dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + orb.vx += (Math.random() - 0.5) * 0.0005; + orb.vy += (Math.random() - 0.5) * 0.0005; - if (dist < CONNECTION_DISTANCE) { - const lineGeometry = new THREE.BufferGeometry(); - const linePositions = new Float32Array([ - posArray[i * 3], posArray[i * 3 + 1], posArray[i * 3 + 2], - posArray[j * 3], posArray[j * 3 + 1], posArray[j * 3 + 2], - ]); - lineGeometry.setAttribute('position', new THREE.BufferAttribute(linePositions, 3)); + orb.vx = Math.max(-0.03, Math.min(0.03, orb.vx)); + orb.vy = Math.max(-0.03, Math.min(0.03, orb.vy)); - const lineMat = lineMaterial.clone(); - lineMat.opacity = 0.06 * (1 - dist / CONNECTION_DISTANCE); - linesGroup.add(new THREE.Line(lineGeometry, lineMat)); - } + orb.el.style.left = orb.x + '%'; + orb.el.style.top = orb.y + '%'; + }); + + // Draw connections every 3rd frame for performance + if (frameCount % 3 === 0) { + drawConnections(); + } + } + + // ============================================================ + // 4. Energy Connections — lines between nearby orbs + // ============================================================ + + function drawConnections() { + var svg = document.getElementById('magic-connections'); + if (!svg) return; + + var w = window.innerWidth; + var h = window.innerHeight; + var threshold = isMobile ? 15 : 20; + + var lines = ''; + for (var i = 0; i < orbState.length; i++) { + for (var j = i + 1; j < orbState.length; j++) { + var dx = orbState[i].x - orbState[j].x; + var dy = orbState[i].y - orbState[j].y; + var dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < threshold) { + var opacity = (1 - dist / threshold) * 0.12; + var x1 = (orbState[i].x / 100) * w; + var y1 = (orbState[i].y / 100) * h; + var x2 = (orbState[j].x / 100) * w; + var y2 = (orbState[j].y / 100) * h; + + lines += ''; } } - - // Subtle camera follow mouse - camera.position.x += (mouseX * 3 - camera.position.x) * 0.02; - camera.position.y += (mouseY * 2 - camera.position.y) * 0.02; - camera.lookAt(scene.position); - - renderer.render(scene, camera); } - - animate(); + svg.innerHTML = lines; } // ============================================================ - // Anime.js Entrance Animations + // 5. Sparkle Particles — twinkling dots // ============================================================ - if (prefersReducedMotion || typeof anime === 'undefined') { - // Make everything visible immediately - document.querySelectorAll('[data-animate]').forEach(function (el) { - el.style.opacity = '1'; - el.style.transform = 'none'; - }); - return; + function initSparkles(animate, random) { + var container = document.getElementById('magic-sparkles'); + if (!container) return; + + var count = isMobile ? 15 : 25; + for (var i = 0; i < count; i++) { + var dot = document.createElement('div'); + dot.className = 'magic-sparkle'; + dot.style.left = random(5, 95) + '%'; + dot.style.top = random(5, 95) + '%'; + container.appendChild(dot); + + animate(dot, { + opacity: [ + { to: random(0, 0.8), duration: random(800, 2000) }, + { to: 0, duration: random(800, 2000) } + ], + scale: [ + { to: random(0.5, 2), duration: random(800, 2000) }, + { to: 0.5, duration: random(800, 2000) } + ], + delay: random(0, 4000), + loop: true, + ease: 'easeInOutSine' + }); + } } - // Hero elements animate immediately on load (above the fold) - var heroElements = document.querySelectorAll('.hero [data-animate]'); - heroElements.forEach(function (el) { - var delay = parseInt(el.getAttribute('data-delay') || '0', 10); - anime({ - targets: el, - opacity: [0, 1], - translateY: [24, 0], - duration: 700, - delay: delay + 200, // small base offset for page load - easing: 'easeOutQuart', + // ============================================================ + // 6. Cursor Glow — follows mouse + // ============================================================ + + function initCursorGlow(animate) { + var glow = document.getElementById('magic-cursor-glow'); + if (!glow) return; + + // Skip on touch devices — no cursor to follow + if ('ontouchstart' in window) return; + + var mouseX = window.innerWidth / 2; + var mouseY = window.innerHeight / 2; + var glowX = mouseX; + var glowY = mouseY; + + document.addEventListener('mousemove', function (e) { + mouseX = e.clientX; + mouseY = e.clientY; + + if (glow.style.opacity === '0' || glow.style.opacity === '') { + animate(glow, { opacity: 1, duration: 600, ease: 'outQuad' }); + } }); - }); - // Below-fold elements: Intersection Observer with immediate fallback - var belowFold = document.querySelectorAll('[data-animate]:not(.hero [data-animate])'); + function updateGlow() { + glowX += (mouseX - glowX) * 0.08; + glowY += (mouseY - glowY) * 0.08; + glow.style.left = glowX + 'px'; + glow.style.top = glowY + 'px'; + requestAnimationFrame(updateGlow); + } + updateGlow(); - // Fallback: ensure all elements become visible after 1.5s max - // This covers headless/screenshot scenarios where scroll never fires - setTimeout(function () { - belowFold.forEach(function (el) { - if (el.style.opacity === '0' || el.style.opacity === '') { + document.addEventListener('mouseleave', function () { + animate(glow, { opacity: 0, duration: 400, ease: 'outQuad' }); + }); + } + + // ============================================================ + // Master background init + // ============================================================ + + function initMagicBackground(animate, createTimeline, stagger, random) { + initAurora(animate, random); + initRunes(animate, random); + + var orbCount = isMobile ? 20 : 30; + createOrbs(orbCount, animate, random); + requestAnimationFrame(tickOrbs); + + initSparkles(animate, random); + initCursorGlow(animate); + } + + // ============================================================ + // Entrance Animations (anime.js for [data-animate]) + // ============================================================ + + function initEntranceAnimations(animate, stagger) { + if (prefersReducedMotion || typeof animate === 'undefined') { + document.querySelectorAll('[data-animate]').forEach(function (el) { el.style.opacity = '1'; el.style.transform = 'none'; - } + }); + return; + } + + // Hero elements animate immediately on load + var heroElements = document.querySelectorAll('.hero [data-animate]'); + heroElements.forEach(function (el) { + var delay = parseInt(el.getAttribute('data-delay') || '0', 10); + animate(el, { + opacity: [0, 1], + translateY: [24, 0], + duration: 700, + delay: delay + 200, + ease: 'easeOutQuart' + }); }); - }, 1500); - if (typeof IntersectionObserver !== 'undefined') { - var observer = new IntersectionObserver( - function (entries) { - entries.forEach(function (entry) { - if (entry.isIntersecting) { - var el = entry.target; - var delay = parseInt(el.getAttribute('data-delay') || '0', 10); - observer.unobserve(el); + // Below-fold elements: Intersection Observer + var belowFold = document.querySelectorAll('[data-animate]:not(.hero [data-animate])'); - anime({ - targets: el, - opacity: [0, 1], - translateY: [24, 0], - duration: 700, - delay: delay, - easing: 'easeOutQuart', - }); - } - }); - }, - { - threshold: 0.1, - rootMargin: '0px 0px -40px 0px', - } - ); + // Fallback: ensure all elements become visible after 1.5s + // Covers headless/screenshot scenarios where scroll never fires + setTimeout(function () { + belowFold.forEach(function (el) { + if (el.style.opacity === '0' || el.style.opacity === '') { + el.style.opacity = '1'; + el.style.transform = 'none'; + } + }); + }, 1500); - belowFold.forEach(function (el) { - observer.observe(el); - }); + if (typeof IntersectionObserver !== 'undefined') { + var observer = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + var el = entry.target; + var delay = parseInt(el.getAttribute('data-delay') || '0', 10); + observer.unobserve(el); + + animate(el, { + opacity: [0, 1], + translateY: [24, 0], + duration: 700, + delay: delay, + ease: 'easeOutQuart' + }); + } + }); + }, + { threshold: 0.1, rootMargin: '0px 0px -40px 0px' } + ); + + belowFold.forEach(function (el) { + observer.observe(el); + }); + } } // ============================================================ - // Terminal glow pulse (subtle) + // Terminal glow pulse // ============================================================ - var terminalEl = document.querySelector('.hero-terminal'); - if (terminalEl && !prefersReducedMotion && typeof anime !== 'undefined') { + function initTerminalGlow(animate) { + if (prefersReducedMotion || typeof animate === 'undefined') return; + + var terminalEl = document.querySelector('.hero-terminal'); + if (!terminalEl) return; + setTimeout(function () { - anime({ - targets: '.hero-terminal', + animate('.hero-terminal', { boxShadow: [ '0 0 0 0 rgba(59, 130, 246, 0)', '0 0 40px -8px rgba(59, 130, 246, 0.15)', - '0 0 0 0 rgba(59, 130, 246, 0)', + '0 0 0 0 rgba(59, 130, 246, 0)' ], duration: 2000, - easing: 'easeInOutQuad', + easing: 'easeInOutQuad' }); }, 1200); } + + // ============================================================ + // Boot + // ============================================================ + + function boot(animeLib) { + var animate = animeLib.animate; + var createTimeline = animeLib.createTimeline; + var stagger = animeLib.stagger; + var random = animeLib.random; + + if (!prefersReducedMotion) { + initMagicBackground(animate, createTimeline, stagger, random); + } + + initEntranceAnimations(animate, stagger); + initTerminalGlow(animate); + } + + // Defer boot by one frame so anime.js engine is ready + if (window.anime) { + requestAnimationFrame(function () { boot(window.anime); }); + } else { + var tries = 0; + var poll = setInterval(function () { + tries++; + if (window.anime) { + clearInterval(poll); + requestAnimationFrame(function () { boot(window.anime); }); + } + if (tries > 50) clearInterval(poll); + }, 100); + } + })(); diff --git a/site/style.css b/site/style.css index a04618ea..f8b93f4d 100644 --- a/site/style.css +++ b/site/style.css @@ -1,6 +1,6 @@ /* ============================================================ - Hatch Homepage v2 — Dark Theme - Design system: OKLCH-inspired tokens, Inter + JetBrains Mono + Hatch Homepage — Dark Theme + Design tokens, Inter + JetBrains Mono, anime.js magic background ============================================================ */ /* --- Reset --- */ @@ -13,22 +13,22 @@ /* --- Tokens --- */ :root { /* Surface */ - --surface-0: #09090b; /* deepest bg */ - --surface-1: #111114; /* cards, sections */ - --surface-2: #1a1a1f; /* elevated surfaces */ - --surface-3: #232328; /* borders, subtle dividers */ + --surface-0: #09090b; + --surface-1: #111114; + --surface-2: #1a1a1f; + --surface-3: #232328; /* Brand */ - --brand: #3b82f6; /* blue-500 */ - --brand-dim: #2563eb; /* blue-600 hover */ + --brand: #3b82f6; + --brand-dim: #2563eb; --brand-glow: rgba(59, 130, 246, 0.15); --brand-glow-strong: rgba(59, 130, 246, 0.25); /* Text */ - --text-primary: #fafafa; /* near-white */ - --text-secondary: #a1a1aa; /* zinc-400 */ - --text-tertiary: #71717a; /* zinc-500 */ - --text-code: #e879f9; /* fuchsia-400 for code highlights */ + --text-primary: #fafafa; + --text-secondary: #a1a1aa; + --text-tertiary: #71717a; + --text-code: #e879f9; /* Code */ --code-bg: #18181b; @@ -39,18 +39,18 @@ --font-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace; /* Spacing scale (4px base) */ - --sp-1: 0.25rem; /* 4 */ - --sp-2: 0.5rem; /* 8 */ - --sp-3: 0.75rem; /* 12 */ - --sp-4: 1rem; /* 16 */ - --sp-5: 1.25rem; /* 20 */ - --sp-6: 1.5rem; /* 24 */ - --sp-8: 2rem; /* 32 */ - --sp-10: 2.5rem; /* 40 */ - --sp-12: 3rem; /* 48 */ - --sp-16: 4rem; /* 64 */ - --sp-20: 5rem; /* 80 */ - --sp-24: 6rem; /* 96 */ + --sp-1: 0.25rem; + --sp-2: 0.5rem; + --sp-3: 0.75rem; + --sp-4: 1rem; + --sp-5: 1.25rem; + --sp-6: 1.5rem; + --sp-8: 2rem; + --sp-10: 2.5rem; + --sp-12: 3rem; + --sp-16: 4rem; + --sp-20: 5rem; + --sp-24: 6rem; /* Radii */ --radius-sm: 6px; @@ -98,8 +98,37 @@ code { color: var(--text-code); } -/* --- Particle canvas --- */ -#particle-canvas { +/* --- Accessibility: skip link --- */ +.skip-to-content { + position: absolute; + top: -50px; + left: var(--sp-4); + background: var(--brand); + color: #fff; + padding: var(--sp-2) var(--sp-4); + z-index: 200; + font-weight: 600; + font-size: 0.9rem; + border-radius: 0 0 var(--radius-sm) var(--radius-sm); + transition: top 0.2s ease; +} + +.skip-to-content:focus { + top: 0; + outline: 2px solid #fff; + outline-offset: 2px; +} + +/* --- Accessibility: focus-visible --- */ +a:focus-visible, +.btn:focus-visible { + outline: 2px solid var(--brand); + outline-offset: 3px; + border-radius: var(--radius-sm); +} + +/* --- Magic background layers --- */ +#magic-bg { position: fixed; top: 0; left: 0; @@ -107,6 +136,138 @@ code { height: 100%; z-index: 0; pointer-events: none; + overflow: hidden; +} + +/* Aurora gradient blobs */ +.magic-aurora { + position: absolute; + width: 600px; + height: 600px; + border-radius: 50%; + filter: blur(120px); + opacity: 0.12; + background: radial-gradient(circle, var(--brand) 0%, transparent 70%); + top: -10%; + left: -5%; + will-change: transform, opacity; +} + +.magic-aurora--2 { + background: radial-gradient(circle, #a855f7 0%, transparent 70%); + top: 40%; + right: -15%; + left: auto; + width: 500px; + height: 500px; + opacity: 0.1; +} + +.magic-aurora--3 { + background: radial-gradient(circle, #06b6d4 0%, transparent 70%); + bottom: -10%; + left: 30%; + top: auto; + width: 450px; + height: 450px; + opacity: 0.08; +} + +/* SVG rune circles */ +.magic-runes { + position: absolute; + top: 50%; + left: 50%; + width: 900px; + height: 900px; + transform: translate(-50%, -50%); + will-change: transform; +} + +.rune { + fill: none; + stroke-width: 1; + opacity: 0; +} + +.rune-outer { + stroke: var(--brand); + stroke-dasharray: 8 24; + opacity: 0.08; +} + +.rune-mid { + stroke: #a855f7; + stroke-dasharray: 4 16; + opacity: 0.06; +} + +.rune-inner { + stroke: #06b6d4; + stroke-dasharray: 12 8; + opacity: 0.05; +} + +.rune-core { + stroke: var(--brand); + stroke-dasharray: 2 12; + opacity: 0.04; + stroke-width: 1.5; +} + +/* Floating orbs container */ +.magic-orbs { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.magic-orb { + position: absolute; + border-radius: 50%; + will-change: transform, opacity; +} + +/* Connection lines SVG */ +.magic-connections { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +/* Sparkle particles */ +.magic-sparkles { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.magic-sparkle { + position: absolute; + width: 2px; + height: 2px; + border-radius: 50%; + background: #fff; + will-change: opacity; +} + +/* Cursor glow follower */ +.magic-cursor-glow { + position: absolute; + width: 400px; + height: 400px; + border-radius: 50%; + background: radial-gradient(circle, rgba(59, 130, 246, 0.08) 0%, transparent 70%); + pointer-events: none; + transform: translate(-50%, -50%); + will-change: transform; + opacity: 0; } /* --- Container --- */ @@ -175,7 +336,7 @@ nav { .hero { position: relative; z-index: 1; - padding: calc(var(--sp-24) + 2rem) 0 var(--sp-20); + padding: calc(var(--sp-24) + 2rem) 0 var(--sp-16); text-align: center; } @@ -233,6 +394,8 @@ nav { cursor: pointer; border: none; white-space: nowrap; + position: relative; + overflow: hidden; } .btn-primary { @@ -304,7 +467,7 @@ nav { } .hero-terminal pre { - padding: var(--sp-5) var(--sp-5); + padding: var(--sp-5); overflow-x: auto; font-size: 0.82rem; line-height: 1.8; @@ -326,7 +489,7 @@ nav { .features { position: relative; z-index: 1; - padding: var(--sp-24) 0; + padding: var(--sp-20) 0; } .features h2 { @@ -349,13 +512,14 @@ nav { background: var(--surface-1); border: 1px solid var(--surface-3); border-radius: var(--radius-lg); - transition: border-color var(--duration-normal) var(--ease-out), - box-shadow var(--duration-normal) var(--ease-out); + transition: all var(--duration-normal) var(--ease-out); + transform: translateY(0); } .feature:hover { border-color: rgba(59, 130, 246, 0.3); box-shadow: 0 0 32px -8px var(--brand-glow); + transform: translateY(-4px); } .feature-icon { @@ -384,11 +548,49 @@ nav { line-height: 1.65; } +/* --- Tech strip --- */ +.tech-strip { + position: relative; + z-index: 1; + padding: var(--sp-12) 0; + border-top: 1px solid var(--surface-3); + border-bottom: 1px solid var(--surface-3); +} + +.tech-items { + display: flex; + justify-content: center; + gap: var(--sp-10); + flex-wrap: wrap; +} + +.tech-item { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--sp-1); +} + +.tech-label { + font-size: 0.75rem; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 500; +} + +.tech-value { + font-family: var(--font-mono); + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); +} + /* --- Why --- */ .why { position: relative; z-index: 1; - padding: var(--sp-24) 0; + padding: var(--sp-20) 0; background: var(--surface-1); border-top: 1px solid var(--surface-3); border-bottom: 1px solid var(--surface-3); @@ -444,7 +646,7 @@ nav { .final-cta { position: relative; z-index: 1; - padding: var(--sp-24) 0; + padding: var(--sp-20) 0; text-align: center; } @@ -501,6 +703,149 @@ footer a:hover { transform: translateY(20px); } +/* --- Blog --- */ +.blog-index { + position: relative; + z-index: 1; + padding: calc(var(--sp-24) + 2rem) 0 var(--sp-20); +} + +.blog-index h1 { + font-size: clamp(2rem, 4vw, 2.5rem); + font-weight: 800; + letter-spacing: -0.03em; + margin-bottom: var(--sp-10); + color: var(--text-primary); +} + +.post-preview { + padding: var(--sp-8); + background: var(--surface-1); + border: 1px solid var(--surface-3); + border-radius: var(--radius-lg); + transition: border-color var(--duration-normal) var(--ease-out), + box-shadow var(--duration-normal) var(--ease-out); +} + +.post-preview:hover { + border-color: rgba(59, 130, 246, 0.3); + box-shadow: 0 0 32px -8px var(--brand-glow); +} + +.post-preview h2 { + font-size: 1.4rem; + font-weight: 700; + margin-bottom: var(--sp-2); + letter-spacing: -0.02em; +} + +.post-preview h2 a { + color: var(--text-primary); +} + +.post-preview h2 a:hover { + color: var(--brand); +} + +.post-preview time { + font-size: 0.85rem; + color: var(--text-tertiary); + font-family: var(--font-mono); +} + +.post-preview p { + color: var(--text-secondary); + font-size: 0.95rem; + line-height: 1.7; + margin: var(--sp-4) 0; +} + +.read-more { + font-weight: 600; + font-size: 0.9rem; +} + +/* Blog post */ +.blog-post { + position: relative; + z-index: 1; + padding: calc(var(--sp-24) + 2rem) 0 var(--sp-20); +} + +.post-header { + margin-bottom: var(--sp-10); + padding-bottom: var(--sp-6); + border-bottom: 1px solid var(--surface-3); +} + +.post-header h1 { + font-size: clamp(1.75rem, 4vw, 2.5rem); + font-weight: 800; + line-height: 1.15; + letter-spacing: -0.03em; + text-wrap: balance; + margin-bottom: var(--sp-3); + color: var(--text-primary); +} + +.post-header time { + font-size: 0.85rem; + color: var(--text-tertiary); + font-family: var(--font-mono); +} + +.post-content { + max-width: 680px; +} + +.post-content p { + color: var(--text-secondary); + font-size: 1.05rem; + line-height: 1.8; + margin-bottom: var(--sp-6); +} + +.post-content h2 { + font-size: 1.4rem; + font-weight: 700; + letter-spacing: -0.02em; + margin: var(--sp-10) 0 var(--sp-4); + color: var(--text-primary); +} + +.post-content pre { + background: var(--code-bg); + border: 1px solid var(--code-border); + border-radius: var(--radius-md); + padding: var(--sp-5); + overflow-x: auto; + margin-bottom: var(--sp-6); +} + +.post-content pre code { + background: none; + border: none; + padding: 0; + font-size: 0.85rem; + line-height: 1.8; + color: var(--text-secondary); +} + +.post-content strong { + color: var(--text-primary); + font-weight: 600; +} + +@media (max-width: 768px) { + .post-preview { + padding: var(--sp-6); + } + + .post-content p { + font-size: 1rem; + } +} + /* --- Reduced motion --- */ @media (prefers-reduced-motion: reduce) { [data-animate] { @@ -512,19 +857,23 @@ footer a:hover { transform: none; } - #particle-canvas { + .feature:hover { + transform: none; + } + + #magic-bg { display: none; } } -/* --- Responsive --- */ +/* --- Responsive: tablet --- */ @media (max-width: 768px) { .container { padding: 0 var(--sp-4); } .hero { - padding: calc(var(--sp-20) + 1rem) 0 var(--sp-16); + padding: calc(var(--sp-20) + 1rem) 0 var(--sp-12); } .hero h1 { @@ -567,8 +916,13 @@ footer a:hover { font-size: 0.75rem; padding: var(--sp-4); } + + .tech-items { + gap: var(--sp-6); + } } +/* --- Responsive: mobile --- */ @media (max-width: 480px) { .hero-cta { flex-direction: column; @@ -578,4 +932,49 @@ footer a:hover { .btn { justify-content: center; } + + /* Terminal horizontal scroll with fade indicator */ + .hero-terminal { + position: relative; + overflow: hidden; + } + + .hero-terminal::after { + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 40px; + background: linear-gradient(to right, transparent, var(--code-bg)); + pointer-events: none; + } + + .hero-terminal pre { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + scrollbar-color: var(--surface-3) transparent; + } + + .hero-terminal pre::-webkit-scrollbar { + height: 6px; + } + + .hero-terminal pre::-webkit-scrollbar-track { + background: transparent; + } + + .hero-terminal pre::-webkit-scrollbar-thumb { + background: var(--surface-3); + border-radius: 3px; + } + + .tech-items { + gap: var(--sp-4); + } + + .tech-item { + min-width: calc(50% - var(--sp-4)); + } }