feat: deploy redesigned hatch.surf homepage (ELF-196)

- Updated index.html with new Three.js canvas and anime.js data attributes
- Updated style.css with dark theme design system
- Added main.js with Three.js particle network and scroll animations
- All files from ELF-195 deliverables

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Riley Zhang 2026-06-24 06:21:20 +02:00
parent 25cb834376
commit 122995c88f
3 changed files with 825 additions and 205 deletions

View File

@ -5,83 +5,166 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hatch — Self-hostable HTTP request inspector + mocker</title>
<meta name="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.">
<!-- Open Graph -->
<meta property="og:title" content="Hatch — Self-hostable HTTP request inspector + mocker">
<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="/brand/og/default.png">
<meta property="og:url" content="https://hatch.sh">
<meta property="og:type" content="website">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Hatch — Self-hostable HTTP request inspector + mocker">
<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="/brand/og/default.png">
<link rel="canonical" href="https://hatch.sh">
<link rel="icon" type="image/png" href="/brand/favicon/favicon-48.png">
<!-- Fonts: Inter for body, JetBrains Mono for code -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<!-- Three.js canvas for animated particle background -->
<canvas id="particle-canvas" aria-hidden="true"></canvas>
<header>
<div class="container">
<a href="/" class="logo">Hatch</a>
<a href="/" class="logo">
<span class="logo-mark"></span> Hatch
</a>
<nav>
<a href="https://github.com/elfoundation/hatch">GitHub</a>
<a href="/blog/">Blog</a>
<a href="https://github.com/elfoundation/hatch" class="nav-link">GitHub</a>
<a href="/blog/" class="nav-link">Blog</a>
</nav>
</div>
</header>
<main>
<!-- Hero -->
<section class="hero">
<div class="container">
<h1>Self-hostable HTTP request inspector + mocker</h1>
<p>One Go binary. SQLite under the hood. <code>docker compose up</code>, and you have an inspection endpoint and a live feed in under 30 seconds. Your payloads never leave your box.</p>
<div class="cta">
<a href="https://github.com/elfoundation/hatch" class="btn btn-primary">View on GitHub</a>
<a href="https://github.com/elfoundation/hatch#quick-start" class="btn btn-secondary">Quick Start</a>
<div class="hero-badge" data-animate="fade-up">Open source &middot; MIT licensed</div>
<h1 data-animate="fade-up" data-delay="80">
Self-hostable HTTP request<br>inspector&nbsp;+&nbsp;mocker
</h1>
<p class="hero-sub" data-animate="fade-up" data-delay="160">
One Go binary. SQLite under the hood.<br>
<code>docker compose up</code>, and you have an inspection endpoint and a live feed in under 30 seconds.
</p>
<div class="hero-cta" data-animate="fade-up" data-delay="240">
<a href="https://github.com/elfoundation/hatch" class="btn btn-primary">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>
View on GitHub
</a>
<a href="https://github.com/elfoundation/hatch#quick-start" class="btn btn-secondary">
Quick Start →
</a>
</div>
<div class="hero-terminal" data-animate="fade-up" data-delay="320">
<div class="terminal-bar">
<span class="terminal-dot"></span>
<span class="terminal-dot"></span>
<span class="terminal-dot"></span>
<span class="terminal-title">terminal</span>
</div>
<pre><code><span class="t-prompt">$</span> <span class="t-cmd">docker compose up -d</span>
<span class="t-output">✓ hatch started on :8080</span>
<span class="t-prompt">$</span> <span class="t-cmd">curl -X POST https://your-bin.hatch.surf/test -d '{"hello":"world"}'</span>
<span class="t-output">✓ captured — view at https://your-bin.hatch.surf/inspect</span></code></pre>
</div>
</div>
</section>
<!-- Features -->
<section class="features">
<div class="container">
<h2>Three things, and nothing else</h2>
<h2 data-animate="fade-up">Three things, and nothing else</h2>
<div class="feature-grid">
<div class="feature">
<div class="feature" data-animate="fade-up" data-delay="80">
<div class="feature-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
</div>
<h3>Capture</h3>
<p>Method, path, headers, query, body. Persists across restarts because the storage is SQLite on disk, not a hosted queue.</p>
</div>
<div class="feature">
<div class="feature" data-animate="fade-up" data-delay="160">
<div class="feature-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</div>
<h3>Inspect</h3>
<p>A live SSE feed of incoming requests. Click any captured request to see the headers, the body, the timing.</p>
</div>
<div class="feature">
<div class="feature" data-animate="fade-up" data-delay="240">
<div class="feature-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
</div>
<h3>Mock</h3>
<p>Return a 200, a 500, or a custom JSON payload. For testing your own retry, backoff, and error-handling logic.</p>
</div>
</div>
</div>
</section>
<!-- Why -->
<section class="why">
<div class="container">
<h2>Why a single binary, not a SaaS</h2>
<ul>
<li><strong>Compliance and privacy.</strong> Some teams cannot legally send webhook payloads to a hosted SaaS. Hatch keeps the data on your own network.</li>
<li><strong>Cost.</strong> A hosted inspector charges per request, per seat, or per retention day. Hatch is one Go binary on a $5 VPS. There is no per-request fee because there is no one to charge it.</li>
<li><strong>Speed of setup.</strong> <code>docker compose up</code> is faster than signing up for a SaaS, verifying your email, configuring your first bin, and pasting the URL into your webhook config.</li>
</ul>
<h2 data-animate="fade-up">Why a single binary, not a SaaS</h2>
<div class="why-grid">
<div class="why-item" data-animate="fade-up" data-delay="80">
<div class="why-number">01</div>
<div class="why-content">
<h3>Compliance and privacy</h3>
<p>Some teams cannot legally send webhook payloads to a hosted SaaS. Hatch keeps the data on your own network.</p>
</div>
</div>
<div class="why-item" data-animate="fade-up" data-delay="160">
<div class="why-number">02</div>
<div class="why-content">
<h3>Cost</h3>
<p>A hosted inspector charges per request, per seat, or per retention day. Hatch is one Go binary on a $5 VPS. There is no per-request fee because there is no one to charge it.</p>
</div>
</div>
<div class="why-item" data-animate="fade-up" data-delay="240">
<div class="why-number">03</div>
<div class="why-content">
<h3>Speed of setup</h3>
<p><code>docker compose up</code> is faster than signing up for a SaaS, verifying your email, configuring your first bin, and pasting the URL into your webhook config.</p>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="final-cta" data-animate="fade-up">
<div class="container">
<h2>Start inspecting in 30&nbsp;seconds</h2>
<p>One binary. Your data stays yours.</p>
<a href="https://github.com/elfoundation/hatch" class="btn btn-primary btn-lg">
Get Hatch →
</a>
</div>
</section>
</main>
<footer>
<div class="container">
<p>&copy; 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 class="footer-inner">
<span class="footer-logo">⬡ Hatch</span>
<p>&copy; 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>
</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>
</body>
</html>
</html>

266
site/main.js Normal file
View File

@ -0,0 +1,266 @@
/**
* Hatch Homepage v2 Three.js particle network + anime.js entrance animations
*
* 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.
*/
(function () {
'use strict';
// ============================================================
// Three.js Particle Network
// ============================================================
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 initParticleNetwork() {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
60,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 50;
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
alpha: true,
antialias: false,
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Particle parameters
const PARTICLE_COUNT = 80;
const CONNECTION_DISTANCE = 12;
const PARTICLE_SIZE = 0.15;
// 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,
});
opacities[i] = 0.3 + Math.random() * 0.5;
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const particlesMaterial = new THREE.PointsMaterial({
color: 0x3b82f6,
size: PARTICLE_SIZE,
transparent: true,
opacity: 0.6,
sizeAttenuation: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);
// Connection lines
const lineMaterial = new THREE.LineBasicMaterial({
color: 0x3b82f6,
transparent: true,
opacity: 0.08,
blending: THREE.AdditiveBlending,
depthWrite: false,
});
const linesGroup = new THREE.Group();
scene.add(linesGroup);
// 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;
});
// Resize
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onResize);
// Animate
function animate() {
requestAnimationFrame(animate);
const posArray = particlesGeometry.attributes.position.array;
// 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;
// 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;
}
particlesGeometry.attributes.position.needsUpdate = true;
// Update connection lines
while (linesGroup.children.length > 0) {
const child = linesGroup.children[0];
child.geometry.dispose();
child.material.dispose();
linesGroup.remove(child);
}
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);
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));
const lineMat = lineMaterial.clone();
lineMat.opacity = 0.06 * (1 - dist / CONNECTION_DISTANCE);
linesGroup.add(new THREE.Line(lineGeometry, lineMat));
}
}
}
// 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();
}
// ============================================================
// Anime.js Entrance Animations
// ============================================================
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;
}
// 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',
});
});
// Below-fold elements: Intersection Observer with immediate fallback
var belowFold = document.querySelectorAll('[data-animate]:not(.hero [data-animate])');
// 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 === '') {
el.style.opacity = '1';
el.style.transform = 'none';
}
});
}, 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);
anime({
targets: el,
opacity: [0, 1],
translateY: [24, 0],
duration: 700,
delay: delay,
easing: 'easeOutQuart',
});
}
});
},
{
threshold: 0.1,
rootMargin: '0px 0px -40px 0px',
}
);
belowFold.forEach(function (el) {
observer.observe(el);
});
}
// ============================================================
// Terminal glow pulse (subtle)
// ============================================================
var terminalEl = document.querySelector('.hero-terminal');
if (terminalEl && !prefersReducedMotion && typeof anime !== 'undefined') {
setTimeout(function () {
anime({
targets: '.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)',
],
duration: 2000,
easing: 'easeInOutQuad',
});
}, 1200);
}
})();

View File

@ -1,78 +1,133 @@
/* Reset and base styles */
* {
/* ============================================================
Hatch Homepage v2 Dark Theme
Design system: OKLCH-inspired tokens, Inter + JetBrains Mono
============================================================ */
/* --- Reset --- */
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* --- Tokens --- */
:root {
--color-primary: #2563eb;
--color-primary-dark: #1d4ed8;
--color-text: #1f2937;
--color-text-light: #6b7280;
--color-bg: #ffffff;
--color-bg-light: #f9fafb;
--color-border: #e5e7eb;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
/* Surface */
--surface-0: #09090b; /* deepest bg */
--surface-1: #111114; /* cards, sections */
--surface-2: #1a1a1f; /* elevated surfaces */
--surface-3: #232328; /* borders, subtle dividers */
/* Brand */
--brand: #3b82f6; /* blue-500 */
--brand-dim: #2563eb; /* blue-600 hover */
--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 */
/* Code */
--code-bg: #18181b;
--code-border: #27272a;
/* Typography */
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--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 */
/* Radii */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
/* Transitions */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--duration-fast: 150ms;
--duration-normal: 250ms;
}
/* --- Base --- */
html {
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: var(--font-sans);
color: var(--color-text);
background-color: var(--color-bg);
font-family: var(--font-body);
color: var(--text-primary);
background-color: var(--surface-0);
overflow-x: hidden;
}
a {
color: var(--color-primary);
color: var(--brand);
text-decoration: none;
transition: color var(--duration-fast) var(--ease-out);
}
a:hover {
text-decoration: underline;
color: var(--text-primary);
}
code {
font-family: var(--font-mono);
font-size: 0.9em;
background-color: var(--color-bg-light);
padding: 0.2em 0.4em;
border-radius: 4px;
font-size: 0.875em;
background-color: var(--code-bg);
border: 1px solid var(--code-border);
padding: 0.15em 0.4em;
border-radius: var(--radius-sm);
color: var(--text-code);
}
pre {
background-color: var(--color-bg-light);
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
margin: 1.5rem 0;
}
pre code {
background: none;
padding: 0;
}
/* Container */
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Header */
header {
background-color: var(--color-bg);
border-bottom: 1px solid var(--color-border);
padding: 1rem 0;
position: sticky;
/* --- Particle canvas --- */
#particle-canvas {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
/* --- Container --- */
.container {
max-width: 960px;
margin: 0 auto;
padding: 0 var(--sp-6);
}
/* --- Header --- */
header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
padding: var(--sp-4) 0;
background: rgba(9, 9, 11, 0.8);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--surface-3);
}
header .container {
@ -82,229 +137,445 @@ header .container {
}
.logo {
font-size: 1.5rem;
font-size: 1.25rem;
font-weight: 700;
color: var(--color-text);
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--sp-2);
letter-spacing: -0.01em;
}
.logo:hover {
text-decoration: none;
color: var(--text-primary);
}
.logo-mark {
color: var(--brand);
font-size: 1.4rem;
}
nav {
display: flex;
gap: 1.5rem;
gap: var(--sp-6);
}
nav a {
color: var(--color-text-light);
.nav-link {
color: var(--text-secondary);
font-weight: 500;
font-size: 0.9rem;
transition: color var(--duration-fast) var(--ease-out);
}
nav a:hover {
color: var(--color-primary);
text-decoration: none;
.nav-link:hover {
color: var(--text-primary);
}
/* Hero section */
/* --- Hero --- */
.hero {
padding: 4rem 0;
position: relative;
z-index: 1;
padding: calc(var(--sp-24) + 2rem) 0 var(--sp-20);
text-align: center;
background-color: var(--color-bg-light);
}
.hero-badge {
display: inline-block;
padding: var(--sp-1) var(--sp-4);
background: var(--brand-glow);
border: 1px solid rgba(59, 130, 246, 0.2);
border-radius: 100px;
font-size: 0.8rem;
font-weight: 500;
color: var(--brand);
letter-spacing: 0.02em;
margin-bottom: var(--sp-8);
}
.hero h1 {
font-size: 2.5rem;
font-size: clamp(2.25rem, 5vw, 3.5rem);
font-weight: 800;
line-height: 1.2;
margin-bottom: 1.5rem;
color: var(--color-text);
line-height: 1.1;
letter-spacing: -0.03em;
text-wrap: balance;
margin-bottom: var(--sp-6);
color: var(--text-primary);
}
.hero p {
font-size: 1.25rem;
color: var(--color-text-light);
max-width: 600px;
margin: 0 auto 2rem;
.hero-sub {
font-size: clamp(1rem, 2vw, 1.2rem);
color: var(--text-secondary);
max-width: 560px;
margin: 0 auto var(--sp-8);
line-height: 1.7;
text-wrap: pretty;
}
.cta {
.hero-cta {
display: flex;
gap: 1rem;
gap: var(--sp-4);
justify-content: center;
flex-wrap: wrap;
margin-bottom: var(--sp-12);
}
/* --- Buttons --- */
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border-radius: 8px;
display: inline-flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-3) var(--sp-6);
border-radius: var(--radius-md);
font-weight: 600;
transition: all 0.2s;
font-size: 0.95rem;
font-family: var(--font-body);
transition: all var(--duration-normal) var(--ease-out);
cursor: pointer;
border: none;
white-space: nowrap;
}
.btn-primary {
background-color: var(--color-primary);
color: white;
background: var(--brand);
color: #fff;
box-shadow: 0 0 0 0 var(--brand-glow-strong);
}
.btn-primary:hover {
background-color: var(--color-primary-dark);
text-decoration: none;
background: var(--brand-dim);
color: #fff;
box-shadow: 0 0 24px 4px var(--brand-glow-strong);
transform: translateY(-1px);
}
.btn-secondary {
background-color: white;
color: var(--color-text);
border: 1px solid var(--color-border);
background: var(--surface-2);
color: var(--text-primary);
border: 1px solid var(--surface-3);
}
.btn-secondary:hover {
background-color: var(--color-bg-light);
text-decoration: none;
background: var(--surface-3);
color: var(--text-primary);
border-color: var(--text-tertiary);
}
/* Features section */
.btn-lg {
padding: var(--sp-4) var(--sp-8);
font-size: 1.05rem;
}
/* --- Terminal mock --- */
.hero-terminal {
max-width: 640px;
margin: 0 auto;
border-radius: var(--radius-lg);
overflow: hidden;
border: 1px solid var(--surface-3);
background: var(--code-bg);
text-align: left;
}
.terminal-bar {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-3) var(--sp-4);
background: var(--surface-2);
border-bottom: 1px solid var(--surface-3);
}
.terminal-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--surface-3);
}
.terminal-dot:first-child { background: #ef4444; }
.terminal-dot:nth-child(2) { background: #eab308; }
.terminal-dot:nth-child(3) { background: #22c55e; }
.terminal-title {
margin-left: var(--sp-2);
font-size: 0.75rem;
color: var(--text-tertiary);
font-family: var(--font-mono);
}
.hero-terminal pre {
padding: var(--sp-5) var(--sp-5);
overflow-x: auto;
font-size: 0.82rem;
line-height: 1.8;
}
.hero-terminal code {
background: none;
border: none;
padding: 0;
color: var(--text-secondary);
font-size: inherit;
}
.t-prompt { color: var(--brand); font-weight: 600; }
.t-cmd { color: var(--text-primary); }
.t-output { color: #22c55e; }
/* --- Features --- */
.features {
padding: 4rem 0;
position: relative;
z-index: 1;
padding: var(--sp-24) 0;
}
.features h2 {
text-align: center;
font-size: 2rem;
margin-bottom: 3rem;
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: var(--sp-12);
color: var(--text-primary);
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
grid-template-columns: repeat(3, 1fr);
gap: var(--sp-6);
}
.feature {
padding: 1.5rem;
background-color: var(--color-bg-light);
border-radius: 8px;
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);
}
.feature:hover {
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 0 32px -8px var(--brand-glow);
}
.feature-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--brand-glow);
border-radius: var(--radius-md);
margin-bottom: var(--sp-5);
color: var(--brand);
}
.feature h3 {
font-size: 1.25rem;
margin-bottom: 0.75rem;
color: var(--color-text);
font-size: 1.1rem;
font-weight: 700;
margin-bottom: var(--sp-3);
color: var(--text-primary);
letter-spacing: -0.01em;
}
.feature p {
color: var(--color-text-light);
color: var(--text-secondary);
font-size: 0.92rem;
line-height: 1.65;
}
/* Why section */
/* --- Why --- */
.why {
padding: 4rem 0;
background-color: var(--color-bg-light);
position: relative;
z-index: 1;
padding: var(--sp-24) 0;
background: var(--surface-1);
border-top: 1px solid var(--surface-3);
border-bottom: 1px solid var(--surface-3);
}
.why h2 {
font-size: 2rem;
margin-bottom: 2rem;
}
.why ul {
list-style: none;
padding: 0;
}
.why li {
padding: 1rem 0;
border-bottom: 1px solid var(--color-border);
}
.why li:last-child {
border-bottom: none;
}
/* Blog post */
.blog-post {
padding: 3rem 0;
}
.post-header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.post-header h1 {
font-size: 2.5rem;
font-weight: 800;
line-height: 1.2;
margin-bottom: 0.5rem;
}
.post-header time {
color: var(--color-text-light);
font-size: 0.9rem;
}
.post-content {
font-size: 1.1rem;
line-height: 1.8;
}
.post-content h2 {
font-size: 1.5rem;
text-align: center;
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 700;
margin: 2rem 0 1rem;
color: var(--color-text);
letter-spacing: -0.02em;
margin-bottom: var(--sp-12);
color: var(--text-primary);
}
.post-content p {
margin-bottom: 1.5rem;
.why-grid {
display: flex;
flex-direction: column;
gap: var(--sp-8);
max-width: 700px;
margin: 0 auto;
}
.post-content ul,
.post-content ol {
margin-bottom: 1.5rem;
padding-left: 1.5rem;
.why-item {
display: flex;
gap: var(--sp-6);
align-items: flex-start;
}
.post-content li {
margin-bottom: 0.5rem;
.why-number {
flex-shrink: 0;
font-family: var(--font-mono);
font-size: 0.85rem;
font-weight: 500;
color: var(--brand);
padding-top: 0.15em;
min-width: 2rem;
}
.post-content strong {
font-weight: 600;
color: var(--color-text);
.why-content h3 {
font-size: 1.05rem;
font-weight: 700;
margin-bottom: var(--sp-2);
color: var(--text-primary);
}
/* Footer */
.why-content p {
color: var(--text-secondary);
font-size: 0.92rem;
line-height: 1.65;
}
/* --- Final CTA --- */
.final-cta {
position: relative;
z-index: 1;
padding: var(--sp-24) 0;
text-align: center;
}
.final-cta h2 {
font-size: clamp(1.5rem, 3vw, 2.25rem);
font-weight: 800;
letter-spacing: -0.03em;
margin-bottom: var(--sp-4);
color: var(--text-primary);
}
.final-cta p {
color: var(--text-secondary);
font-size: 1.1rem;
margin-bottom: var(--sp-8);
}
/* --- Footer --- */
footer {
background-color: var(--color-bg-light);
padding: 2rem 0;
margin-top: 4rem;
border-top: 1px solid var(--color-border);
position: relative;
z-index: 1;
padding: var(--sp-8) 0;
border-top: 1px solid var(--surface-3);
}
.footer-inner {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-logo {
font-weight: 700;
color: var(--text-tertiary);
font-size: 0.9rem;
}
footer p {
text-align: center;
color: var(--color-text-light);
font-size: 0.9rem;
color: var(--text-tertiary);
font-size: 0.85rem;
}
/* Responsive */
footer a {
color: var(--text-secondary);
}
footer a:hover {
color: var(--text-primary);
}
/* --- Animation states (set by anime.js) --- */
[data-animate] {
opacity: 0;
transform: translateY(20px);
}
/* --- Reduced motion --- */
@media (prefers-reduced-motion: reduce) {
[data-animate] {
opacity: 1;
transform: none;
}
.btn-primary:hover {
transform: none;
}
#particle-canvas {
display: none;
}
}
/* --- Responsive --- */
@media (max-width: 768px) {
.container {
padding: 0 var(--sp-4);
}
.hero {
padding: calc(var(--sp-20) + 1rem) 0 var(--sp-16);
}
.hero h1 {
font-size: 2rem;
}
.hero p {
font-size: 1.1rem;
.hero-sub br {
display: none;
}
.post-header h1 {
font-size: 2rem;
.feature-grid {
grid-template-columns: 1fr;
gap: var(--sp-4);
}
.post-content {
font-size: 1rem;
.feature {
padding: var(--sp-6);
}
}
.why-item {
flex-direction: column;
gap: var(--sp-2);
}
.why-number {
padding-top: 0;
}
.footer-inner {
flex-direction: column;
gap: var(--sp-4);
text-align: center;
}
.hero-terminal {
border-radius: var(--radius-md);
}
.hero-terminal pre {
font-size: 0.75rem;
padding: var(--sp-4);
}
}
@media (max-width: 480px) {
.hero-cta {
flex-direction: column;
align-items: stretch;
}
.btn {
justify-content: center;
}
}