diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..5a2c9cf --- /dev/null +++ b/docs/index.html @@ -0,0 +1,270 @@ + + + + + + pv - PHP dev servers, instantly + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+ + Powered by FrankenPHP +
+ + +

+ PHP dev servers,
+ instantly. +

+ + +

+ No dnsmasq. No Docker. No config files. Link a project and it's live at + https://project.test — HTTPS included. +

+ + + + + +
+
+ + + + zsh +
+
+ + + +
+
+
+
+ + +
+
+ +

+ Running PHP projects locally shouldn't require juggling dnsmasq, Docker, and Traefik. +

+

+ pv replaces all of that with a single binary. Install it, link your project directory, and it's instantly available at a .test domain with HTTPS. No containers, no proxy chains, no config files to maintain. +

+
+
+ + +
+
+ +

Everything you need. Nothing you don't.

+

A complete local PHP environment in one tool.

+
+
+ +
+ + + +

One command setup

+

+ pv install gets you FrankenPHP, PHP, Composer, and Mago — ready to serve PHP projects immediately. +

+
+ +
+ + + +

HTTPS out of the box

+

+ Every project gets an automatic .test domain with a trusted local certificate. Zero config. +

+
+ +
+ + + +

Multi-version PHP

+

+ Install PHP 8.2, 8.3, 8.4 side by side. Switch globally or per-project. No phpenv, no phpbrew. +

+
+ +
+ + + +

Auto project detection

+

+ pv link detects Laravel, Laravel Octane, and generic PHP automatically and generates the right server config. +

+
+
+
+ + +
+
+ +

Up and running in three commands.

+

From zero to a live local site in under a minute.

+
+ +
+ +
+
1
+
+

Install

+

Downloads pv and sets up FrankenPHP, PHP, Composer, and Mago.

+
+ $ curl -fsSL https://raw.githubusercontent.com/prvious/pv/main/install.sh | bash +
+
+
+ +
+
2
+
+

Link your project

+

Auto-detects your project type and sets up the server config.

+
+ $ pv link ~/code/my-laravel-app +
+
+
+ +
+
3
+
+

Start

+

Your app is now live at https://my-laravel-app.test with HTTPS.

+
+ $ pv start +
+
+
+
+
+ + +
+
+ +

Install pv

+

One command. No dependencies.

+
+ +
+
+ $ curl -fsSL https://raw.githubusercontent.com/prvious/pv/main/install.sh | bash +
+ +
+ +

+ Requires macOS. Source available on + GitHub. +

+
+ + + + + + + + diff --git a/docs/script.js b/docs/script.js new file mode 100644 index 0000000..f80aa38 --- /dev/null +++ b/docs/script.js @@ -0,0 +1,350 @@ +/* ========================================================================== + pv — Landing Page Scripts + Terminal typewriter animation + copy-to-clipboard + ========================================================================== */ + +(function () { + 'use strict'; + + // -------------------------------------------------------------------------- + // Terminal Typewriter Animation + // -------------------------------------------------------------------------- + + const sequences = [ + { + command: 'pv install', + output: [ + '\u2713 FrankenPHP installed', + '\u2713 PHP 8.4 ready', + '\u2713 Composer installed', + ], + pauseAfter: 1800, + }, + { + command: 'pv link ~/code/my-app', + output: [ + '\u2713 Linked my-app', + '\u2713 Starting server...', + '\u2713 Live at https://my-app.test', + ], + pauseAfter: 2200, + }, + { + command: 'pv php install 8.3', + output: [ + '\u2713 Downloading PHP 8.3...', + '\u2713 PHP 8.3 installed', + ], + pauseAfter: 1800, + }, + ]; + + const TYPING_SPEED = 45; // ms per character + const OUTPUT_LINE_DELAY = 200; // ms between output lines + const PAUSE_BEFORE_CLEAR = 600; + + function sleep(ms) { + return new Promise(function (resolve) { setTimeout(resolve, ms); }); + } + + function initTerminal() { + var body = document.getElementById('terminal-body'); + if (!body) return; + + var seqIndex = 0; + + async function runSequence() { + while (true) { + var seq = sequences[seqIndex]; + body.innerHTML = ''; + + // Create prompt line with cursor + var promptLine = document.createElement('span'); + promptLine.className = 'terminal__line terminal__line--prompt'; + body.appendChild(promptLine); + + var cursor = document.createElement('span'); + cursor.className = 'terminal__cursor'; + body.appendChild(cursor); + + // Type out the command character by character + for (var i = 0; i < seq.command.length; i++) { + promptLine.textContent += seq.command[i]; + await sleep(TYPING_SPEED); + } + + await sleep(300); + + // Remove cursor temporarily + cursor.remove(); + + // Print output lines one by one + for (var j = 0; j < seq.output.length; j++) { + var outputLine = document.createElement('span'); + outputLine.className = 'terminal__line terminal__line--output'; + outputLine.textContent = seq.output[j]; + body.appendChild(outputLine); + await sleep(OUTPUT_LINE_DELAY); + } + + // Add blinking cursor on new prompt line + var newPrompt = document.createElement('span'); + newPrompt.className = 'terminal__line terminal__line--prompt'; + body.appendChild(newPrompt); + + var newCursor = document.createElement('span'); + newCursor.className = 'terminal__cursor'; + body.appendChild(newCursor); + + // Pause to let user read + await sleep(seq.pauseAfter); + + // Move to next sequence + seqIndex = (seqIndex + 1) % sequences.length; + await sleep(PAUSE_BEFORE_CLEAR); + } + } + + runSequence(); + } + + // -------------------------------------------------------------------------- + // Copy to Clipboard + // -------------------------------------------------------------------------- + + function initCopyButton() { + var btn = document.getElementById('copy-btn'); + if (!btn) return; + + var command = 'curl -fsSL https://raw.githubusercontent.com/prvious/pv/main/install.sh | bash'; + + var copyIcon = ''; + var checkIcon = ''; + + btn.addEventListener('click', function () { + // Clipboard API requires secure context (HTTPS or localhost) + // Falls back gracefully if unavailable + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(command).then(function () { + btn.innerHTML = checkIcon; + btn.classList.add('install__copy--copied'); + setTimeout(function () { + btn.innerHTML = copyIcon; + btn.classList.remove('install__copy--copied'); + }, 2000); + }); + } else { + // Fallback: select text from a temporary textarea + var ta = document.createElement('textarea'); + ta.value = command; + ta.style.position = 'fixed'; + ta.style.left = '-9999px'; + document.body.appendChild(ta); + ta.select(); + try { + document.execCommand('copy'); + btn.innerHTML = checkIcon; + btn.classList.add('install__copy--copied'); + setTimeout(function () { + btn.innerHTML = copyIcon; + btn.classList.remove('install__copy--copied'); + }, 2000); + } catch (e) { + // silently fail + } + document.body.removeChild(ta); + } + }); + } + + // -------------------------------------------------------------------------- + // Scroll Fade-In Animation + // -------------------------------------------------------------------------- + + function initScrollAnimations() { + var elements = document.querySelectorAll('.fade-in'); + if (!elements.length) return; + + var observer = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + entry.target.classList.add('visible'); + observer.unobserve(entry.target); + } + }); + }, { + threshold: 0.1, + rootMargin: '0px 0px -40px 0px', + }); + + elements.forEach(function (el) { + observer.observe(el); + }); + } + + // -------------------------------------------------------------------------- + // Smooth Scroll for CTA + // -------------------------------------------------------------------------- + + function initSmoothScroll() { + var links = document.querySelectorAll('a[href^="#"]'); + links.forEach(function (link) { + link.addEventListener('click', function (e) { + var targetId = link.getAttribute('href'); + if (targetId === '#') return; + var target = document.querySelector(targetId); + if (target) { + e.preventDefault(); + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + }); + }); + } + + // -------------------------------------------------------------------------- + // Color Palette Switcher + // -------------------------------------------------------------------------- + + var palettes = [ + { + name: 'Electric Blue', + accent: '#155dfc', + accentHover: '#3b82f6', + swatch: '#155dfc', + }, + { + name: 'Emerald', + accent: '#10b981', + accentHover: '#34d399', + swatch: '#10b981', + }, + { + name: 'Violet', + accent: '#8b5cf6', + accentHover: '#a78bfa', + swatch: '#8b5cf6', + }, + { + name: 'Rose', + accent: '#f43f5e', + accentHover: '#fb7185', + swatch: '#f43f5e', + }, + { + name: 'Amber', + accent: '#f59e0b', + accentHover: '#fbbf24', + swatch: '#f59e0b', + }, + { + name: 'Cyan', + accent: '#06b6d4', + accentHover: '#22d3ee', + swatch: '#06b6d4', + }, + { + name: 'Orange', + accent: '#f97316', + accentHover: '#fb923c', + swatch: '#f97316', + }, + { + name: 'Pink', + accent: '#ec4899', + accentHover: '#f472b6', + swatch: '#ec4899', + }, + { + name: 'Teal', + accent: '#14b8a6', + accentHover: '#2dd4bf', + swatch: '#14b8a6', + }, + { + name: 'Indigo', + accent: '#6366f1', + accentHover: '#818cf8', + swatch: '#6366f1', + }, + ]; + + // Convert hex to RGB components + function hexToRgb(hex) { + var r = parseInt(hex.slice(1, 3), 16); + var g = parseInt(hex.slice(3, 5), 16); + var b = parseInt(hex.slice(5, 7), 16); + return { r: r, g: g, b: b }; + } + + function applyPalette(index) { + var p = palettes[index]; + var root = document.documentElement; + var rgb = hexToRgb(p.accent); + var r = rgb.r, g = rgb.g, b = rgb.b; + + root.style.setProperty('--accent', p.accent); + root.style.setProperty('--accent-hover', p.accentHover); + root.style.setProperty('--accent-glow', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.25)'); + root.style.setProperty('--accent-bg-subtle', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.1)'); + root.style.setProperty('--accent-bg-medium', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.12)'); + root.style.setProperty('--accent-border', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.25)'); + root.style.setProperty('--accent-border-strong', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.3)'); + root.style.setProperty('--accent-radial', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.18)'); + root.style.setProperty('--accent-card-glow', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.08)'); + root.style.setProperty('--accent-code-bg', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.12)'); + root.style.setProperty('--accent-code-border', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.2)'); + root.style.setProperty('--border-hover', 'rgba(' + r + ', ' + g + ', ' + b + ', 0.4)'); + } + + function initColorSwitcher() { + var currentIndex = 0; + + // Build the widget + var widget = document.createElement('div'); + widget.className = 'color-switcher'; + + // Swatch (the clickable circle) + var swatch = document.createElement('button'); + swatch.className = 'color-switcher__swatch'; + swatch.style.background = palettes[0].swatch; + swatch.setAttribute('aria-label', 'Switch color palette'); + swatch.title = palettes[0].name; + + // Label + var label = document.createElement('span'); + label.className = 'color-switcher__label'; + label.textContent = palettes[0].name; + + widget.appendChild(label); + widget.appendChild(swatch); + document.body.appendChild(widget); + + swatch.addEventListener('click', function () { + currentIndex = (currentIndex + 1) % palettes.length; + applyPalette(currentIndex); + + // Update swatch color and label + swatch.style.background = palettes[currentIndex].swatch; + swatch.title = palettes[currentIndex].name; + label.textContent = palettes[currentIndex].name; + + // Flash the label visible + label.classList.remove('color-switcher__label--visible'); + // Force reflow + void label.offsetWidth; + label.classList.add('color-switcher__label--visible'); + }); + } + + // -------------------------------------------------------------------------- + // Init + // -------------------------------------------------------------------------- + + document.addEventListener('DOMContentLoaded', function () { + initTerminal(); + initCopyButton(); + initScrollAnimations(); + initSmoothScroll(); + initColorSwitcher(); + }); +})(); diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..6c211df --- /dev/null +++ b/docs/style.css @@ -0,0 +1,954 @@ +/* ========================================================================== + pv — Landing Page Styles + Pure CSS, no frameworks, no build tools. + ========================================================================== */ + +/* -------------------------------------------------------------------------- + 1. Custom Properties + -------------------------------------------------------------------------- */ +:root { + /* Colors */ + --bg-hero: #0b0f1a; + --bg-body: #0d1117; + --bg-alt: #111827; + --bg-terminal: #161b22; + --accent: #155dfc; + --accent-hover: #3b82f6; + --accent-glow: rgba(21, 93, 252, 0.25); + --accent-bg-subtle: rgba(21, 93, 252, 0.1); + --accent-bg-medium: rgba(21, 93, 252, 0.12); + --accent-border: rgba(21, 93, 252, 0.25); + --accent-border-strong: rgba(21, 93, 252, 0.3); + --accent-radial: rgba(21, 93, 252, 0.18); + --accent-card-glow: rgba(21, 93, 252, 0.08); + --accent-code-bg: rgba(21, 93, 252, 0.12); + --accent-code-border: rgba(21, 93, 252, 0.2); + --text-primary: #f0f6fc; + --text-secondary: #8b949e; + --text-muted: #6e7681; + --border: rgba(255, 255, 255, 0.08); + --border-hover: rgba(21, 93, 252, 0.4); + + /* Terminal dots */ + --dot-red: #ff5f56; + --dot-yellow: #ffbd2e; + --dot-green: #27c93f; + + /* Typography */ + --font-body: 'Inter', system-ui, -apple-system, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace; + + /* Spacing */ + --section-padding: 96px 24px; + --section-padding-mobile: 64px 16px; + --container-max: 1080px; + --container-narrow: 720px; +} + +/* -------------------------------------------------------------------------- + 2. Reset & Base + -------------------------------------------------------------------------- */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-body); + font-size: 16px; + line-height: 1.6; + color: var(--text-primary); + background-color: var(--bg-body); +} + +a { + color: var(--accent); + text-decoration: none; + transition: color 0.2s ease; +} + +a:hover { + color: var(--accent-hover); +} + +img { + max-width: 100%; + display: block; +} + +/* -------------------------------------------------------------------------- + 3. Typography + -------------------------------------------------------------------------- */ +h1, h2, h3 { + font-weight: 700; + line-height: 1.2; + letter-spacing: -0.02em; +} + +h1 { + font-size: 3.5rem; +} + +h2 { + font-size: 2.25rem; + margin-bottom: 16px; +} + +h3 { + font-size: 1.25rem; + margin-bottom: 8px; +} + +p { + color: var(--text-secondary); +} + +code { + font-family: var(--font-mono); + font-size: 0.9em; + background: rgba(255, 255, 255, 0.06); + padding: 2px 6px; + border-radius: 4px; +} + +/* -------------------------------------------------------------------------- + 4. Layout Utilities + -------------------------------------------------------------------------- */ +.container { + max-width: var(--container-max); + margin: 0 auto; + padding: 0 24px; +} + +.container--narrow { + max-width: var(--container-narrow); +} + +.section { + padding: var(--section-padding); +} + +.section--alt { + background-color: var(--bg-alt); +} + +.text-center { + text-align: center; +} + +.section-label { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 0.8125rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--accent); + background: var(--accent-bg-subtle); + border: 1px solid var(--accent-border); + padding: 6px 14px; + border-radius: 100px; + margin-bottom: 24px; +} + +.section-heading { + font-size: 2.25rem; + margin-bottom: 16px; + color: var(--text-primary); +} + +.section-subheading { + font-size: 1.125rem; + color: var(--text-secondary); + max-width: 560px; + margin: 0 auto 48px; + line-height: 1.7; +} + +/* -------------------------------------------------------------------------- + 5. Navbar + -------------------------------------------------------------------------- */ +.navbar { + position: sticky; + top: 0; + z-index: 100; + background: rgba(11, 15, 26, 0.85); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); + padding: 0 24px; +} + +.navbar__inner { + max-width: var(--container-max); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + height: 64px; +} + +.navbar__logo { + font-family: var(--font-mono); + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary); + text-decoration: none; + display: flex; + align-items: center; + gap: 2px; +} + +.navbar__logo-accent { + color: var(--accent); +} + +.navbar__github { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--text-secondary); + font-size: 0.875rem; + font-weight: 500; + padding: 8px 16px; + border: 1px solid var(--border); + border-radius: 8px; + transition: border-color 0.2s ease, color 0.2s ease; +} + +.navbar__github:hover { + border-color: var(--border-hover); + color: var(--text-primary); +} + +.navbar__github svg { + width: 20px; + height: 20px; + fill: currentColor; +} + +/* -------------------------------------------------------------------------- + 6. Hero + -------------------------------------------------------------------------- */ +.hero { + position: relative; + background-color: var(--bg-hero); + overflow: hidden; + padding: 80px 24px 0; +} + +.hero__bg { + position: absolute; + inset: 0; + background-image: + radial-gradient(ellipse 80% 50% at 50% 0%, var(--accent-radial) 0%, transparent 70%), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Cdefs%3E%3Cpattern id='g' width='60' height='60' patternUnits='userSpaceOnUse'%3E%3Cpath d='M 60 0 L 0 0 0 60' fill='none' stroke='rgba(255,255,255,0.04)' stroke-width='0.5'/%3E%3Cline x1='-4' y1='0' x2='4' y2='0' stroke='rgba(255,255,255,0.12)' stroke-width='0.8'/%3E%3Cline x1='0' y1='-4' x2='0' y2='4' stroke='rgba(255,255,255,0.12)' stroke-width='0.8'/%3E%3Crect x='-1' y='-1' width='2' height='2' fill='rgba(255,255,255,0.08)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect width='60' height='60' fill='url(%23g)'/%3E%3C/svg%3E"); + background-size: auto, 60px 60px; + pointer-events: none; +} + +.hero__fade { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 200px; + background: linear-gradient(to bottom, transparent 0%, var(--bg-body) 100%); + pointer-events: none; +} + +.hero__content { + position: relative; + z-index: 2; + max-width: 800px; + margin: 0 auto; + text-align: center; +} + +.hero__badge { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.8125rem; + font-weight: 600; + color: var(--accent); + background: var(--accent-bg-subtle); + border: 1px solid var(--accent-border-strong); + padding: 6px 16px; + border-radius: 100px; + margin-bottom: 32px; +} + +.hero__title { + font-size: 3.5rem; + font-weight: 800; + line-height: 1.1; + letter-spacing: -0.03em; + margin-bottom: 20px; + color: var(--text-primary); +} + +.hero__title-accent { + color: var(--accent); +} + +.hero__subtitle { + font-size: 1.2rem; + color: var(--text-secondary); + line-height: 1.7; + max-width: 600px; + margin: 0 auto 36px; +} + +.hero__subtitle code { + color: var(--accent); + background: var(--accent-code-bg); + border: 1px solid var(--accent-code-border); +} + +.hero__ctas { + display: flex; + align-items: center; + justify-content: center; + gap: 16px; + margin-bottom: 56px; + flex-wrap: wrap; +} + +/* -------------------------------------------------------------------------- + 7. Buttons + -------------------------------------------------------------------------- */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: var(--font-body); + font-size: 1rem; + font-weight: 600; + padding: 12px 28px; + border-radius: 10px; + border: none; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; +} + +.btn--primary { + background: var(--accent); + color: #fff; + box-shadow: 0 0 24px var(--accent-glow); +} + +.btn--primary:hover { + background: var(--accent-hover); + color: #fff; + box-shadow: 0 0 40px var(--accent-glow); + transform: translateY(-1px); +} + +.btn--secondary { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border); +} + +.btn--secondary:hover { + border-color: var(--border-hover); + color: var(--text-primary); +} + +.btn--secondary svg { + width: 18px; + height: 18px; + fill: currentColor; +} + +/* -------------------------------------------------------------------------- + 8. Terminal Window + -------------------------------------------------------------------------- */ +.terminal { + max-width: 620px; + margin: 0 auto; + border-radius: 12px; + overflow: hidden; + background: var(--bg-terminal); + border: 1px solid var(--border); + box-shadow: + 0 4px 6px rgba(0, 0, 0, 0.3), + 0 24px 80px rgba(0, 0, 0, 0.4); + text-align: left; + position: relative; + z-index: 2; + margin-bottom: -60px; +} + +.terminal__header { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 16px; + background: rgba(255, 255, 255, 0.03); + border-bottom: 1px solid var(--border); +} + +.terminal__dot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.terminal__dot--red { background: var(--dot-red); } +.terminal__dot--yellow { background: var(--dot-yellow); } +.terminal__dot--green { background: var(--dot-green); } + +.terminal__title { + flex: 1; + text-align: center; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--text-muted); + margin-right: 44px; /* offset for dots */ +} + +.terminal__body { + padding: 20px; + min-height: 180px; + font-family: var(--font-mono); + font-size: 0.875rem; + line-height: 1.7; +} + +.terminal__line { + display: block; + white-space: pre; + color: var(--text-secondary); +} + +.terminal__line--prompt::before { + content: '$ '; + color: var(--accent); + font-weight: 700; +} + +.terminal__line--output { + color: var(--dot-green); +} + +.terminal__line--output::before { + content: ' '; +} + +.terminal__cursor { + display: inline-block; + width: 8px; + height: 18px; + background: var(--accent); + vertical-align: text-bottom; + animation: blink 1s step-end infinite; + margin-left: 1px; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +/* -------------------------------------------------------------------------- + 9. Problem Section + -------------------------------------------------------------------------- */ +.problem { + padding: var(--section-padding); + padding-top: 120px; +} + +.problem__inner { + max-width: var(--container-narrow); + margin: 0 auto; + text-align: center; +} + +.problem__quote { + font-size: 1.75rem; + font-weight: 700; + color: var(--text-primary); + line-height: 1.4; + margin-bottom: 24px; + letter-spacing: -0.01em; +} + +.problem__answer { + font-size: 1.125rem; + color: var(--text-secondary); + line-height: 1.7; +} + +.problem__answer strong { + color: var(--accent); + font-weight: 600; +} + +/* -------------------------------------------------------------------------- + 10. Features Section + -------------------------------------------------------------------------- */ +.features { + padding: var(--section-padding); + background: var(--bg-alt); +} + +.features__grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + max-width: var(--container-max); + margin: 0 auto; +} + +.feature-card { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + border-radius: 12px; + padding: 32px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.feature-card:hover { + border-color: var(--border-hover); + box-shadow: 0 0 40px var(--accent-card-glow); +} + +.feature-card__icon { + font-size: 1.75rem; + margin-bottom: 16px; + display: block; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: var(--accent-bg-subtle); + border-radius: 10px; + color: var(--accent); +} + +.feature-card__title { + font-size: 1.125rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; +} + +.feature-card__desc { + font-size: 0.9375rem; + color: var(--text-secondary); + line-height: 1.6; +} + +/* -------------------------------------------------------------------------- + 11. How It Works + -------------------------------------------------------------------------- */ +.steps { + padding: var(--section-padding); +} + +.steps__list { + max-width: 640px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 48px; +} + +.step { + display: flex; + gap: 24px; +} + +.step__number { + flex-shrink: 0; + width: 48px; + height: 48px; + background: var(--accent-bg-medium); + border: 1px solid var(--accent-border-strong); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-mono); + font-size: 1.125rem; + font-weight: 700; + color: var(--accent); +} + +.step__content { + flex: 1; + min-width: 0; +} + +.step__title { + font-size: 1.125rem; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 8px; +} + +.step__desc { + font-size: 0.9375rem; + color: var(--text-secondary); + margin-bottom: 16px; + line-height: 1.6; +} + +.step__code { + background: var(--bg-terminal); + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px 20px; + font-family: var(--font-mono); + font-size: 0.875rem; + color: var(--text-primary); + overflow-x: auto; +} + +.step__code .prompt { + color: var(--accent); + font-weight: 700; + user-select: none; +} + +/* -------------------------------------------------------------------------- + 12. Install Section + -------------------------------------------------------------------------- */ +.install { + padding: var(--section-padding); + background: var(--bg-alt); +} + +.install__box { + max-width: 640px; + margin: 0 auto; + background: var(--bg-terminal); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px 24px; + display: flex; + align-items: center; + gap: 16px; +} + +.install__command { + flex: 1; + font-family: var(--font-mono); + font-size: 0.875rem; + color: var(--text-primary); + overflow-x: auto; + white-space: nowrap; +} + +.install__command .prompt { + color: var(--accent); + font-weight: 700; + user-select: none; +} + +.install__copy { + flex-shrink: 0; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; + color: var(--text-secondary); + font-size: 1rem; + transition: all 0.2s ease; +} + +.install__copy:hover { + background: rgba(255, 255, 255, 0.1); + border-color: var(--border-hover); + color: var(--text-primary); +} + +.install__copy svg { + width: 18px; + height: 18px; + stroke: currentColor; + fill: none; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + +.install__copy--copied { + color: var(--dot-green); + border-color: var(--dot-green); +} + +.install__copy--copied svg { + stroke: var(--dot-green); +} + +.install__note { + text-align: center; + margin-top: 20px; + font-size: 0.8125rem; + color: var(--text-muted); +} + +.install__note a { + color: var(--text-secondary); + text-decoration: underline; + text-underline-offset: 2px; +} + +.install__note a:hover { + color: var(--text-primary); +} + +/* -------------------------------------------------------------------------- + 13. Footer + -------------------------------------------------------------------------- */ +.footer { + background: var(--bg-hero); + border-top: 1px solid var(--border); + padding: 40px 24px; +} + +.footer__inner { + max-width: var(--container-max); + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 16px; +} + +.footer__left { + display: flex; + align-items: center; + gap: 16px; +} + +.footer__logo { + font-family: var(--font-mono); + font-size: 1.25rem; + font-weight: 700; + color: var(--text-primary); +} + +.footer__logo-accent { + color: var(--accent); +} + +.footer__tagline { + font-size: 0.875rem; + color: var(--text-muted); +} + +.footer__right { + display: flex; + align-items: center; + gap: 24px; + font-size: 0.875rem; + color: var(--text-muted); +} + +.footer__right a { + color: var(--text-secondary); + transition: color 0.2s ease; +} + +.footer__right a:hover { + color: var(--text-primary); +} + +/* -------------------------------------------------------------------------- + 14. Responsive + -------------------------------------------------------------------------- */ +@media (max-width: 768px) { + :root { + --section-padding: 64px 16px; + } + + .hero { + padding: 48px 16px 0; + } + + .hero__title { + font-size: 2.25rem; + } + + .hero__subtitle { + font-size: 1.05rem; + } + + .hero__ctas { + flex-direction: column; + gap: 12px; + } + + .btn { + width: 100%; + justify-content: center; + } + + .terminal { + max-width: 100%; + } + + .terminal__body { + font-size: 0.8rem; + padding: 16px; + min-height: 160px; + } + + .problem__quote { + font-size: 1.375rem; + } + + .features__grid { + grid-template-columns: 1fr; + gap: 16px; + } + + .section-heading { + font-size: 1.75rem; + } + + .step { + flex-direction: column; + gap: 16px; + } + + .step__code { + font-size: 0.8rem; + } + + .install__box { + padding: 16px; + } + + .install__command { + font-size: 0.75rem; + } + + .footer__inner { + flex-direction: column; + text-align: center; + } + + .footer__left { + flex-direction: column; + gap: 8px; + } +} + +/* -------------------------------------------------------------------------- + 15. Animations + -------------------------------------------------------------------------- */ +.fade-in { + opacity: 0; + transform: translateY(20px); + transition: opacity 0.6s ease, transform 0.6s ease; +} + +.fade-in.visible { + opacity: 1; + transform: translateY(0); +} + +/* -------------------------------------------------------------------------- + 16. Color Switcher Widget + -------------------------------------------------------------------------- */ +.color-switcher { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 9999; + display: flex; + align-items: center; + gap: 12px; +} + +.color-switcher__swatch { + width: 48px; + height: 48px; + border-radius: 50%; + border: 3px solid rgba(255, 255, 255, 0.2); + cursor: pointer; + transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(0, 0, 0, 0.2); + outline: none; + -webkit-appearance: none; + appearance: none; +} + +.color-switcher__swatch:hover { + transform: scale(1.12); + border-color: rgba(255, 255, 255, 0.5); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.5), + 0 0 0 1px rgba(0, 0, 0, 0.3); +} + +.color-switcher__swatch:active { + transform: scale(0.95); +} + +.color-switcher__label { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 600; + color: var(--text-primary); + background: var(--bg-terminal); + border: 1px solid var(--border); + padding: 6px 12px; + border-radius: 8px; + white-space: nowrap; + opacity: 0; + transform: translateX(8px); + transition: opacity 0.3s ease, transform 0.3s ease; + pointer-events: none; +} + +.color-switcher__label--visible { + opacity: 1; + transform: translateX(0); + animation: label-fade 2s ease forwards; +} + +@keyframes label-fade { + 0% { opacity: 1; transform: translateX(0); } + 70% { opacity: 1; transform: translateX(0); } + 100% { opacity: 0; transform: translateX(8px); } +} + +@media (max-width: 768px) { + .color-switcher { + bottom: 16px; + right: 16px; + } + + .color-switcher__swatch { + width: 40px; + height: 40px; + } + + .color-switcher__label { + display: none; + } +}