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:
parent
25cb834376
commit
122995c88f
121
site/index.html
121
site/index.html
@ -21,44 +21,89 @@
|
||||
|
||||
<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 · MIT licensed</div>
|
||||
<h1 data-animate="fade-up" data-delay="80">
|
||||
Self-hostable HTTP request<br>inspector + 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>
|
||||
@ -66,22 +111,60 @@
|
||||
</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 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>© 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>© 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>
|
||||
266
site/main.js
Normal file
266
site/main.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
619
site/style.css
619
site/style.css
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user