feat(site): add anime.js v4 magic background, remove Three.js
Merge anime.js magic background changes to trigger deployment to GitHub Pages and hatch.surf server.
This commit is contained in:
commit
c6552da7e6
@ -1,6 +1,6 @@
|
||||
# Hatch Static Site
|
||||
|
||||
This directory contains the static site for [hatch.sh](https://hatch.sh).
|
||||
This directory contains the static site for [hatch.surf](https://hatch.surf).
|
||||
|
||||
## Structure
|
||||
|
||||
@ -29,11 +29,11 @@ The site is automatically deployed to GitHub Pages via the `deploy-site.yml` wor
|
||||
1. Go to repository Settings → Pages
|
||||
2. Source: Deploy from a branch
|
||||
3. Branch: `main`, folder: `/site`
|
||||
4. Custom domain: `hatch.sh`
|
||||
4. Custom domain: `hatch.surf`
|
||||
|
||||
### DNS Configuration
|
||||
|
||||
To point `hatch.sh` to GitHub Pages:
|
||||
To point `hatch.surf` to GitHub Pages:
|
||||
|
||||
1. Add these DNS records:
|
||||
- Type: A, Name: @, Value: 185.199.108.153
|
||||
|
||||
@ -9,21 +9,38 @@
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="Blog — Hatch">
|
||||
<meta property="og:description" content="Blog posts about Hatch, a self-hostable HTTP request inspector and mocker.">
|
||||
<meta property="og:image" content="https://hatch.sh/brand/og/default.png">
|
||||
<meta property="og:url" content="https://hatch.sh/blog/">
|
||||
<meta property="og:image" content="https://hatch.surf/brand/og/default.png">
|
||||
<meta property="og:url" content="https://hatch.surf/blog/">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Blog — Hatch">
|
||||
<meta name="twitter:description" content="Blog posts about Hatch, a self-hostable HTTP request inspector and mocker.">
|
||||
<meta name="twitter:image" content="https://hatch.sh/brand/og/default.png">
|
||||
<meta name="twitter:image" content="https://hatch.surf/brand/og/default.png">
|
||||
|
||||
<link rel="canonical" href="https://hatch.sh/blog/">
|
||||
<link rel="canonical" href="https://hatch.surf/blog/">
|
||||
<link rel="icon" type="image/png" href="/brand/favicon/favicon.png">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Anime.js magical background layers -->
|
||||
<div id="magic-bg" aria-hidden="true">
|
||||
<div class="magic-aurora"></div>
|
||||
<div class="magic-aurora magic-aurora--2"></div>
|
||||
<div class="magic-aurora magic-aurora--3"></div>
|
||||
<svg class="magic-runes" viewBox="0 0 800 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle class="rune rune-outer" cx="400" cy="400" r="350" />
|
||||
<circle class="rune rune-mid" cx="400" cy="400" r="260" />
|
||||
<circle class="rune rune-inner" cx="400" cy="400" r="170" />
|
||||
<circle class="rune rune-core" cx="400" cy="400" r="80" />
|
||||
</svg>
|
||||
<div class="magic-orbs" id="magic-orbs"></div>
|
||||
<svg class="magic-connections" id="magic-connections"></svg>
|
||||
<div class="magic-sparkles" id="magic-sparkles"></div>
|
||||
<div class="magic-cursor-glow" id="magic-cursor-glow"></div>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div class="container">
|
||||
<a href="/" class="logo">Hatch</a>
|
||||
@ -56,5 +73,9 @@
|
||||
<p>© 2026 El Foundation. <a href="https://github.com/elfoundation/hatch">Hatch</a> is released under the <a href="https://github.com/elfoundation/hatch/blob/main/LICENSE">MIT License</a>.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Anime.js v4 (UMD) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/animejs@4/dist/bundles/anime.umd.min.js"></script>
|
||||
<script src="/main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -9,8 +9,8 @@
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="Why we are building Hatch">
|
||||
<meta property="og:description" content="Hatch is a self-hostable HTTP request inspector and mocker. One Go binary, SQLite, no SaaS, no per-request fee. Open source, MIT.">
|
||||
<meta property="og:image" content="https://hatch.sh/brand/og/default.png">
|
||||
<meta property="og:url" content="https://hatch.sh/blog/why-we-are-building-hatch">
|
||||
<meta property="og:image" content="https://hatch.surf/brand/og/default.png">
|
||||
<meta property="og:url" content="https://hatch.surf/blog/why-we-are-building-hatch">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="article:published_time" content="2026-06-23">
|
||||
<meta property="article:author" content="El Foundation">
|
||||
@ -19,13 +19,30 @@
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Why we are building Hatch">
|
||||
<meta name="twitter:description" content="Hatch is a self-hostable HTTP request inspector and mocker. One Go binary, SQLite, no SaaS, no per-request fee. Open source, MIT.">
|
||||
<meta name="twitter:image" content="https://hatch.sh/brand/og/default.png">
|
||||
<meta name="twitter:image" content="https://hatch.surf/brand/og/default.png">
|
||||
|
||||
<link rel="canonical" href="https://hatch.sh/blog/why-we-are-building-hatch">
|
||||
<link rel="canonical" href="https://hatch.surf/blog/why-we-are-building-hatch">
|
||||
<link rel="icon" type="image/png" href="/brand/favicon/favicon.png">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Anime.js magical background layers -->
|
||||
<div id="magic-bg" aria-hidden="true">
|
||||
<div class="magic-aurora"></div>
|
||||
<div class="magic-aurora magic-aurora--2"></div>
|
||||
<div class="magic-aurora magic-aurora--3"></div>
|
||||
<svg class="magic-runes" viewBox="0 0 800 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle class="rune rune-outer" cx="400" cy="400" r="350" />
|
||||
<circle class="rune rune-mid" cx="400" cy="400" r="260" />
|
||||
<circle class="rune rune-inner" cx="400" cy="400" r="170" />
|
||||
<circle class="rune rune-core" cx="400" cy="400" r="80" />
|
||||
</svg>
|
||||
<div class="magic-orbs" id="magic-orbs"></div>
|
||||
<svg class="magic-connections" id="magic-connections"></svg>
|
||||
<div class="magic-sparkles" id="magic-sparkles"></div>
|
||||
<div class="magic-cursor-glow" id="magic-cursor-glow"></div>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div class="container">
|
||||
<a href="/" class="logo">Hatch</a>
|
||||
@ -116,5 +133,9 @@ docker compose up</code></pre>
|
||||
<p>© 2026 El Foundation. <a href="https://github.com/elfoundation/hatch">Hatch</a> is released under the <a href="https://github.com/elfoundation/hatch/blob/main/LICENSE">MIT License</a>.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Anime.js v4 (UMD) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/animejs@4/dist/bundles/anime.umd.min.js"></script>
|
||||
<script src="/main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -30,8 +30,22 @@
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Three.js canvas for animated particle background -->
|
||||
<canvas id="particle-canvas" aria-hidden="true"></canvas>
|
||||
<!-- Anime.js magical background layers -->
|
||||
<div id="magic-bg" aria-hidden="true">
|
||||
<div class="magic-aurora"></div>
|
||||
<div class="magic-aurora magic-aurora--2"></div>
|
||||
<div class="magic-aurora magic-aurora--3"></div>
|
||||
<svg class="magic-runes" viewBox="0 0 800 800" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle class="rune rune-outer" cx="400" cy="400" r="350" />
|
||||
<circle class="rune rune-mid" cx="400" cy="400" r="260" />
|
||||
<circle class="rune rune-inner" cx="400" cy="400" r="170" />
|
||||
<circle class="rune rune-core" cx="400" cy="400" r="80" />
|
||||
</svg>
|
||||
<div class="magic-orbs" id="magic-orbs"></div>
|
||||
<svg class="magic-connections" id="magic-connections"></svg>
|
||||
<div class="magic-sparkles" id="magic-sparkles"></div>
|
||||
<div class="magic-cursor-glow" id="magic-cursor-glow"></div>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div class="container">
|
||||
@ -162,9 +176,7 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Libraries -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.2/anime.min.js"></script>
|
||||
<script src="/main.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/animejs@4/dist/bundles/anime.umd.min.js"></script>
|
||||
<script src="/main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
566
site/main.js
566
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 += '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" stroke="rgba(59,130,246,' + opacity.toFixed(3) + ')" stroke-width="0.5" />';
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
471
site/style.css
471
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));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user