From 122995c88f8bccb11fc91790a9b5587906ab1ff2 Mon Sep 17 00:00:00 2001 From: Riley Zhang Date: Wed, 24 Jun 2026 06:21:20 +0200 Subject: [PATCH] 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 --- site/index.html | 137 ++++++++--- site/main.js | 266 ++++++++++++++++++++ site/style.css | 627 ++++++++++++++++++++++++++++++++++-------------- 3 files changed, 825 insertions(+), 205 deletions(-) create mode 100644 site/main.js diff --git a/site/index.html b/site/index.html index 97d91eba..d19d8b63 100644 --- a/site/index.html +++ b/site/index.html @@ -5,83 +5,166 @@ Hatch — Self-hostable HTTP request inspector + mocker - + - + - + + + + + + + + + +
- +
+
-

Self-hostable HTTP request inspector + mocker

-

One Go binary. SQLite under the hood. docker compose up, and you have an inspection endpoint and a live feed in under 30 seconds. Your payloads never leave your box.

-
- View on GitHub - Quick Start +
Open source · MIT licensed
+

+ Self-hostable HTTP request
inspector + mocker +

+

+ One Go binary. SQLite under the hood.
+ docker compose up, and you have an inspection endpoint and a live feed in under 30 seconds. +

+ +
+
+ + + + terminal +
+
$ docker compose up -d
+✓ hatch  started on :8080
+$ curl -X POST https://your-bin.hatch.surf/test -d '{"hello":"world"}'
+✓ captured — view at https://your-bin.hatch.surf/inspect
- + +
-

Three things, and nothing else

+

Three things, and nothing else

-
+
+
+ +

Capture

Method, path, headers, query, body. Persists across restarts because the storage is SQLite on disk, not a hosted queue.

-
+
+
+ +

Inspect

A live SSE feed of incoming requests. Click any captured request to see the headers, the body, the timing.

-
+
+
+ +

Mock

Return a 200, a 500, or a custom JSON payload. For testing your own retry, backoff, and error-handling logic.

- + +
-

Why a single binary, not a SaaS

-
    -
  • Compliance and privacy. Some teams cannot legally send webhook payloads to a hosted SaaS. Hatch keeps the data on your own network.
  • -
  • Cost. 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.
  • -
  • Speed of setup. docker compose up is faster than signing up for a SaaS, verifying your email, configuring your first bin, and pasting the URL into your webhook config.
  • -
+

Why a single binary, not a SaaS

+
+
+
01
+
+

Compliance and privacy

+

Some teams cannot legally send webhook payloads to a hosted SaaS. Hatch keeps the data on your own network.

+
+
+
+
02
+
+

Cost

+

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.

+
+
+
+
03
+
+

Speed of setup

+

docker compose up is faster than signing up for a SaaS, verifying your email, configuring your first bin, and pasting the URL into your webhook config.

+
+
+
+
+
+ + +
+
+

Start inspecting in 30 seconds

+

One binary. Your data stays yours.

+ + Get Hatch → +
- + + + + + + - \ No newline at end of file + diff --git a/site/main.js b/site/main.js new file mode 100644 index 00000000..3ee955c4 --- /dev/null +++ b/site/main.js @@ -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); + } +})(); diff --git a/site/style.css b/site/style.css index dd18553e..a04618ea 100644 --- a/site/style.css +++ b/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); } -} \ No newline at end of file + + .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; + } +}