feat: website

This commit is contained in:
2025-10-13 15:10:47 +02:00
commit 2f37486295
19 changed files with 2136 additions and 0 deletions

BIN
public/assets/cluster.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

BIN
public/assets/editor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
public/assets/firmware.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

BIN
public/assets/ledlab.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

BIN
public/assets/spore-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

BIN
public/assets/spore-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
public/assets/spore.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
public/assets/topology.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

358
public/index.html Normal file
View File

@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPORE - Sprocket Orchestration Engine</title>
<link rel="stylesheet" href="styles.css">
<!-- Prism.js for syntax highlighting -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
</head>
<body>
<header>
<div class="container">
<div class="header-content">
<div class="brand">
<h1>SPORE</h1>
<p class="tagline">Sprocket Orchestration Engine</p>
</div>
<button class="burger" id="menu-toggle" aria-label="Menu" aria-controls="site-nav" aria-expanded="false">
<span></span><span></span><span></span>
</button>
</div>
</div>
<nav id="site-nav" class="site-nav">
<div class="container">
<a href="#overview">Overview</a>
<a href="#spore">Core</a>
<a href="#ui">UI</a>
<a href="#apps">Apps</a>
<a href="#docs">Docs</a>
</div>
</nav>
</header>
<main>
<section id="overview" class="hero">
<div class="container">
<img src="assets/spore.png" alt="SPORE" class="hero-image">
<p>A totally based cluster engine for ESP8266 microcontrollers with automatic node discovery, health monitoring, and over-the-air updates. Built for real-time control of distributed embedded systems.</p>
</div>
<p class="subtitle">
</section>
<section id="spore" class="content-section">
<div class="container">
<h2>Core</h2>
<p class="subtitle">Cluster engine firmware for ESP8266 microcontrollers</p>
<p class="repo-link"><a href="https://git.dcentral.systems/iot/spore" target="_blank" rel="noopener">Repository: spore</a></p>
<div class="feature-grid">
<div class="feature-card">
<h3>Auto Discovery</h3>
<p>UDP-based node discovery with automatic cluster membership on port 4210</p>
</div>
<div class="feature-card">
<h3>Health Monitoring</h3>
<p>Real-time node status tracking with resource monitoring and automatic failover</p>
</div>
<div class="feature-card">
<h3>Event System</h3>
<p>Local and cluster-wide event publishing/subscription with WebSocket and UDP streaming API</p>
</div>
<div class="feature-card">
<h3>OTA Updates</h3>
<p>Seamless firmware updates across the cluster via HTTP API</p>
</div>
<div class="feature-card">
<h3>Service Registry</h3>
<p>Dynamic API endpoint discovery and registration for custom services</p>
</div>
<div class="feature-card">
<h3>Task Scheduler</h3>
<p>Cooperative multitasking system for background operations</p>
</div>
</div>
<div class="content-block">
<h3>Quick Start</h3>
<pre><code class="language-cpp">#include &lt;Arduino.h&gt;
#include "spore/Spore.h"
#include "spore/Service.h"
#include "spore/core/ApiServer.h"
#include "spore/core/TaskManager.h"
class HelloService : public Service {
public:
HelloService() {}
const char* getName() const override { return "hello"; }
void registerEndpoints(ApiServer& api) override {
api.addEndpoint("/api/hello", HTTP_GET, [](AsyncWebServerRequest* req) {
req->send(200, "application/json", "{\"message\":\"hello\"}");
});
}
void registerTasks(TaskManager& taskManager) override {
taskManager.registerTask("heartbeat", 1000, [this]() { this->heartbeat(); });
}
private:
void heartbeat() {
// e.g., blink LED, publish telemetry, etc.
}
};
Spore spore({
{"app", "my_app"},
{"role", "controller"}
});
void setup() {
spore.setup();
spore.addService(new HelloService());
spore.begin();
}
void loop() {
spore.loop();
}</code></pre>
</div>
<div class="content-block">
<h3>Technical Specifications</h3>
<ul>
<li><strong>Supported Hardware:</strong> ESP-01/ESP-01S (1MB Flash), Wemos D1 (4MB Flash)</li>
<li><strong>Discovery Protocol:</strong> UDP broadcast on port 4210</li>
<li><strong>API Interface:</strong> RESTful HTTP + WebSocket streaming</li>
<li><strong>Dependencies:</strong> ESPAsyncWebServer, ArduinoJson</li>
<li><strong>Framework:</strong> Arduino with PlatformIO build system</li>
</ul>
</div>
</div>
</section>
<section id="ui" class="content-section alt">
<div class="container">
<h2>UI</h2>
<p class="subtitle">Zero-configuration web interface for cluster monitoring and management</p>
<p class="repo-link"><a href="https://git.dcentral.systems/iot/spore-ui" target="_blank" rel="noopener">Repository: spore-ui</a></p>
<div class="screenshot-grid">
<div class="screenshot">
<img src="assets/cluster.png" alt="Cluster monitoring view">
<p>Real-time cluster member overview with auto-discovery</p>
</div>
<div class="screenshot">
<img src="assets/topology.png" alt="Network topology visualization">
<p>Network topology visualization with node relationships</p>
</div>
<div class="screenshot">
<img src="assets/monitoring.png" alt="Node monitoring dashboard">
<p>Detailed system metrics and task monitoring</p>
</div>
<div class="screenshot">
<img src="assets/firmware.png" alt="Firmware management interface">
<p>Clusterwide over-the-air firmware updates</p>
</div>
</div>
<div class="content-block">
<h3>Features</h3>
<ul>
<li>Cluster monitoring with real-time status updates</li>
<li>Node details including running tasks and available endpoints</li>
<li>Direct HTTP API access to all nodes in the cluster</li>
<li>Over-the-air firmware updates for entire cluster</li>
<li>WebSocket terminal for direct node interaction</li>
<li>UDP auto-discovery eliminates hardcoded IP addresses</li>
<li>Responsive design for all devices</li>
</ul>
</div>
<div class="content-block">
<h3>Technology Stack</h3>
<ul>
<li><strong>Backend:</strong> Express.js, Node.js</li>
<li><strong>Frontend:</strong> Vanilla JavaScript, CSS3, HTML5</li>
<li><strong>Architecture:</strong> Custom component-based framework</li>
</ul>
</div>
</div>
</section>
<section id="apps" class="content-section">
<div class="container">
<h2>Apps</h2>
<p class="subtitle">Application suite built on SPORE</p>
<div class="content-block">
<p>Explore applications that extend SPORE.</p>
<ul>
<li><a href="#ledlab">LEDLab</a> — Real-time LED matrix animation streaming and visual preset editor</li>
</ul>
</div>
</div>
</section>
<section id="ledlab" class="content-section">
<div class="container">
<h2>LEDLab</h2>
<p class="subtitle">Real-time LED matrix animation streaming and visual preset editor</p>
<p class="repo-link"><a href="https://git.dcentral.systems/iot/spore-ledlab" target="_blank" rel="noopener">Repository: spore-ledlab</a></p>
<div class="screenshot-grid">
<div class="screenshot">
<img src="assets/ledlab.png" alt="LEDLab interface">
<p>Multi-node management with live canvas preview</p>
</div>
<div class="screenshot">
<img src="assets/editor.png" alt="Preset editor">
<p>Visual preset editor with building blocks</p>
</div>
</div>
<div class="content-block">
<h3>Firmware Requirements</h3>
<p>LEDLab requires SPORE nodes running the <strong>PixelStreamController</strong> firmware, which handles UDP-based RGB data streaming to NeoPixel strips and matrices. The firmware subscribes to <code>udp/raw</code> cluster events and converts incoming hex-encoded pixel data into real-time LED animations.</p>
<p>Key firmware features include support for both strip and matrix configurations, serpentine (zig-zag) wiring patterns, and configurable pixel counts, brightness, and matrix dimensions. The controller automatically remaps pixel coordinates for proper matrix display.</p>
<p>See <a href="https://git.dcentral.systems/iot/spore/src/branch/main/examples/pixelstream/README.md" target="_blank" rel="noopener">PixelStream documentation</a> for more information.</p>
</div>
<div class="content-block">
<h3>Capabilities</h3>
<ul>
<li>Multi-node management with individual control</li>
<li>Real-time canvas preview for each node</li>
<li>10+ built-in animation presets (rainbow, lava lamp, aurora, etc.)</li>
<li>Visual preset editor with reusable building blocks</li>
<li>Live parameter control with instant feedback</li>
<li>Import/export custom presets as JSON</li>
<li>Auto-discovery of SPORE nodes on network</li>
<li>Configurable FPS (1-60) and matrix dimensions</li>
<li>Requires SPORE nodes running PixelStreamController</li>
</ul>
</div>
<div class="content-block">
<h3>Building Blocks</h3>
<div class="building-blocks">
<div>
<strong>Shapes:</strong> Circle, Rectangle, Triangle, Blob, Point, Line
</div>
<div>
<strong>Patterns:</strong> Trail, Radial, Spiral
</div>
<div>
<strong>Colors:</strong> Solid, Gradient, Palette, Rainbow
</div>
<div>
<strong>Animations:</strong> Move, Rotate, Pulse, Oscillate, Fade
</div>
</div>
</div>
</div>
</section>
<section id="docs" class="content-section">
<div class="container">
<h2>Documentation</h2>
</div>
<div class="doc-grid">
<div class="doc-card">
<h3>SPORE Core</h3>
<ul>
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/Architecture.md" target="_blank" rel="noopener">Architecture Guide</a> - System design and implementation</li>
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/API.md" target="_blank" rel="noopener">API Reference</a> - Complete REST API documentation</li>
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/Development.md" target="_blank" rel="noopener">Development Guide</a> - Build, deployment, configuration</li>
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/TaskManagement.md" target="_blank" rel="noopener">Task Management</a> - Background task system</li>
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/ClusterBroadcast.md" target="_blank" rel="noopener">Cluster Broadcast</a> - Event distribution protocol</li>
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/docs/StreamingAPI.md" target="_blank" rel="noopener">Streaming API</a> - WebSocket integration</li>
</ul>
</div>
<div class="doc-card">
<h3>SPORE UI</h3>
<ul>
<li><a href="https://git.dcentral.systems/iot/spore-ui/src/branch/main/README.md" target="_blank" rel="noopener">Getting Started</a> - Installation and setup</li>
<li><a href="https://git.dcentral.systems/iot/spore-ui/src/branch/main/docs/DISCOVERY.md" target="_blank" rel="noopener">UDP Auto Discovery</a> - Network discovery protocol</li>
<li><a href="https://git.dcentral.systems/iot/spore-ui/src/branch/main/api/openapi.yaml" target="_blank" rel="noopener">API Spec</a> - Backend API reference (OpenAPI)</li>
<li><a href="https://git.dcentral.systems/iot/spore-ui/src/branch/main/docs/FRAMEWORK_README.md" target="_blank" rel="noopener">Component Framework</a> - Custom UI architecture</li>
</ul>
</div>
<div class="doc-card">
<h3>SPORE LEDLab</h3>
<ul>
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/README.md" target="_blank" rel="noopener">Quick Start</a> - Installation and usage</li>
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/PRESET_EDITOR.md" target="_blank" rel="noopener">Preset Editor</a> - Visual animation builder</li>
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/presets/examples" target="_blank" rel="noopener">Custom Presets</a> - JSON format and examples</li>
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/presets/building-blocks.js" target="_blank" rel="noopener">Building Blocks</a> - Reusable components</li>
<li><a href="https://git.dcentral.systems/iot/spore-ledlab/src/branch/main/MULTI_NODE_UPDATE.md" target="_blank" rel="noopener">Multi-Node Management</a> - Cluster control</li>
<li><a href="https://git.dcentral.systems/iot/spore/src/branch/main/examples/pixelstream/README.md" target="_blank" rel="noopener">PixelStream Controller</a> - Required SPORE node firmware</li>
</ul>
</div>
</div>
<div class="container">
<div class="getting-started">
<h3>Getting Started</h3>
<div class="start-grid">
<div class="start-card">
<h4>SPORE Core</h4>
<pre><code># Build and flash firmware
./ctl.sh build target esp01
./ctl.sh flash target esp01</code></pre>
</div>
<div class="start-card">
<h4>SPORE UI</h4>
<pre><code># Install and start
npm install
npm start
# Open browser
http://localhost:3001</code></pre>
</div>
<div class="start-card">
<h4>SPORE LEDLab</h4>
<pre><code># Install and start
npm install
npm start
# Open browser
http://localhost:3000</code></pre>
</div>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<p>SPORE - Sprocket Orchestration Engine</p>
<p>
Source:
<a href="https://git.dcentral.systems/iot/spore" target="_blank" rel="noopener">spore</a> ·
<a href="https://git.dcentral.systems/iot/spore-ui" target="_blank" rel="noopener">spore-ui</a> ·
<a href="https://git.dcentral.systems/iot/spore-ledlab" target="_blank" rel="noopener">spore-ledlab</a>
</p>
</div>
</footer>
<!-- Lightbox Overlay for screenshots -->
<div id="lightbox" class="lightbox" aria-hidden="true" role="dialog">
<div class="lightbox-backdrop" data-close></div>
<figure class="lightbox-content">
<img id="lightbox-image" alt="Expanded screenshot" />
<figcaption id="lightbox-caption"></figcaption>
<button class="lightbox-close" type="button" title="Close" aria-label="Close" data-close>×</button>
</figure>
</div>
<script src="script.js"></script>
<!-- Prism.js for syntax highlighting -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js"></script>
</body>
</html>

169
public/script.js Normal file
View File

@@ -0,0 +1,169 @@
// Smooth scroll for navigation links
function getHeaderHeight() {
const header = document.querySelector('header');
return header ? header.offsetHeight : 80;
}
function smoothScrollToSection(hash) {
const target = document.querySelector(hash);
if (!target) return;
// Recalculate header height at the moment of scrolling
const headerHeight = getHeaderHeight();
// Use getBoundingClientRect for robust position calc across layouts
const targetTopRelative = target.getBoundingClientRect().top;
const absoluteTargetTop = window.scrollY + targetTopRelative;
// Exact offset: align section start just below the header bottom
const top = Math.max(absoluteTargetTop - headerHeight, 0);
window.scrollTo({ top, behavior: 'smooth' });
}
function updateURLHash(hash) {
// Update URL hash without triggering scroll
if (hash && hash !== '#') {
history.pushState(null, null, hash);
}
}
function scrollToTop() {
// Remove hash from URL and scroll to top
history.pushState(null, null, window.location.pathname);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
document.querySelectorAll('nav a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
// If mobile menu is open, close it first so header height is correct
const siteNavEl = document.getElementById('site-nav');
const menuBtn = document.getElementById('menu-toggle');
const wasOpen = siteNavEl && siteNavEl.classList.contains('open');
if (wasOpen) {
siteNavEl.classList.remove('open');
if (menuBtn) menuBtn.setAttribute('aria-expanded', 'false');
// Wait for next frame so layout updates and header height is correct
requestAnimationFrame(() => {
smoothScrollToSection(this.getAttribute('href'));
updateURLHash(this.getAttribute('href'));
});
} else {
smoothScrollToSection(this.getAttribute('href'));
updateURLHash(this.getAttribute('href'));
}
});
});
// Add active class to navigation based on scroll position
const sections = document.querySelectorAll('section[id]');
const navLinks = document.querySelectorAll('nav a');
function setActiveNav() {
let current = '';
const headerHeight = getHeaderHeight();
sections.forEach(section => {
const sectionTop = section.offsetTop;
const sectionHeight = section.clientHeight;
// Account for fixed header in active link detection
if (window.scrollY + headerHeight + 30 >= sectionTop && window.scrollY < sectionTop + sectionHeight) {
current = section.getAttribute('id');
}
});
navLinks.forEach(link => {
link.classList.remove('active');
if (link.getAttribute('href') === `#${current}`) {
link.classList.add('active');
}
});
}
window.addEventListener('scroll', setActiveNav);
window.addEventListener('load', setActiveNav);
// Adjust on resize (header height can change)
window.addEventListener('resize', () => {
// Re-run active link calculation to keep alignment correct
setActiveNav();
});
// Handle direct hash navigation (e.g., page load with #section)
window.addEventListener('load', () => {
if (location.hash) {
// Use the same precise scroll logic on initial load
smoothScrollToSection(location.hash);
}
});
// Lightbox for screenshots (desktop only behavior)
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-image');
const lightboxCaption = document.getElementById('lightbox-caption');
function openLightbox(src, caption) {
if (!lightbox) return;
lightboxImg.src = src;
lightboxCaption.textContent = caption || '';
lightbox.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
if (!lightbox) return;
lightbox.setAttribute('aria-hidden', 'true');
lightboxImg.src = '';
document.body.style.overflow = '';
}
// Delegate clicks on screenshots
document.addEventListener('click', (e) => {
const img = e.target.closest('.screenshot img');
if (img) {
// Only open overlay on wider screens
if (window.innerWidth >= 768) {
openLightbox(img.src, img.alt || img.nextElementSibling?.textContent || '');
e.preventDefault();
}
}
if (e.target.matches('[data-close]')) {
closeLightbox();
}
});
// Close on ESC
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeLightbox();
});
// Mobile burger menu
const menuToggle = document.getElementById('menu-toggle');
const siteNav = document.getElementById('site-nav');
if (menuToggle && siteNav) {
menuToggle.addEventListener('click', () => {
const isOpen = siteNav.classList.toggle('open');
menuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
// Close menu after navigating
siteNav.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
if (siteNav.classList.contains('open')) {
siteNav.classList.remove('open');
menuToggle.setAttribute('aria-expanded', 'false');
}
});
});
}
// Handle SPORE title click to scroll to top and remove hash
const sporeTitle = document.querySelector('.brand h1');
if (sporeTitle) {
sporeTitle.addEventListener('click', scrollToTop);
sporeTitle.style.cursor = 'pointer';
}

633
public/styles.css Normal file
View File

@@ -0,0 +1,633 @@
/* CSS Variables */
:root {
--color-bg: #0a0e14;
--color-surface: #1a1f28;
--color-surface-alt: #0f1318;
--color-border: #2d3541;
--color-text: #c5cdd9;
--color-text-dim: #7d8799;
--color-primary: #6db5ff;
--color-accent: #00d4aa;
--color-code-bg: #0d1117;
/* Glassmorphism */
--glass-bg: rgba(26, 31, 40, 0.55);
--glass-elevated-bg: rgba(26, 31, 40, 0.65);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-highlight: rgba(255, 255, 255, 0.18);
--glass-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
--glass-blur: 12px;
--spacing-xs: 0.5rem;
--spacing-sm: 1rem;
--spacing-md: 2rem;
--spacing-lg: 3rem;
--spacing-xl: 4rem;
--border-radius: 8px;
--max-width: 1200px;
}
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
overflow-x: hidden;
width: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
/* Subtle gradient backdrop for depth */
background:
radial-gradient(1200px 800px at 10% -10%, rgba(109,181,255,0.08), transparent 60%),
radial-gradient(1000px 700px at 110% 10%, rgba(0,212,170,0.07), transparent 55%),
linear-gradient(180deg, #0a0e14 0%, #0a0e14 100%);
color: var(--color-text);
line-height: 1.6;
overflow-x: hidden;
width: 100%;
padding-top: 90px; /* Reduced for more compact header */
}
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 var(--spacing-md);
width: 100%;
box-sizing: border-box;
}
/* Header */
header {
background: var(--glass-bg);
border-bottom: 1px solid var(--glass-border);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
width: 100%;
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
box-shadow: var(--glass-shadow);
}
.header-content {
padding: var(--spacing-xs) 0; /* More compact padding */
display: flex;
align-items: center;
justify-content: space-between;
}
/* Hide burger on desktop */
.burger {
display: none;
}
.brand h1 {
font-size: 1.5rem; /* More compact font size */
color: var(--color-primary);
margin-bottom: 0.1rem; /* Reduced margin */
}
.brand .tagline {
color: var(--color-text-dim);
font-size: 0.85rem; /* More compact tagline */
}
.site-nav {
display: flex;
gap: var(--spacing-md);
padding: 0.25rem 0; /* More compact padding */
border-top: 1px solid var(--glass-border);
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
}
.site-nav .container {
display: flex;
gap: var(--spacing-md);
}
.site-nav a {
color: var(--color-text);
text-decoration: none;
padding: 0.4rem var(--spacing-sm); /* More compact padding */
border-radius: var(--border-radius);
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
font-size: 0.9rem; /* Slightly smaller font */
border: 1px solid transparent; /* Prevent hover border pop-in */
}
.site-nav a:hover,
.site-nav a.active {
background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
border-color: var(--glass-highlight);
color: var(--color-primary);
}
/* Link styles */
a {
color: var(--color-primary);
text-decoration: none;
}
a:hover {
color: var(--color-accent);
text-decoration: underline;
}
/* Sections */
.hero {
padding: var(--spacing-xl) 0;
text-align: center;
background: linear-gradient(135deg, var(--color-surface-alt) 0%, var(--color-bg) 100%);
border-bottom: 1px solid var(--color-border);
}
.hero h2 {
font-size: 2.5rem;
color: var(--color-primary);
margin-bottom: var(--spacing-md);
}
.hero-image {
max-width: 100%;
height: auto;
width: 100%;
max-height: 60vh;
object-fit: contain;
margin-bottom: var(--spacing-lg);
display: block;
}
.hero p {
font-size: 1.2rem;
color: var(--color-text);
max-width: 800px;
margin: 0 auto;
}
.content-section {
padding: var(--spacing-xl) 0;
border-bottom: 1px solid var(--color-border);
}
.content-section.alt {
background-color: var(--color-surface-alt);
}
.content-section h2 {
font-size: 2rem;
color: var(--color-primary);
margin-bottom: var(--spacing-sm);
}
.subtitle {
color: var(--color-text-dim);
font-size: 1.1rem;
margin-bottom: var(--spacing-sm);
}
.repo-link {
margin-bottom: var(--spacing-lg);
}
.repo-link a {
color: var(--color-primary);
text-decoration: none;
font-size: 0.95rem;
font-weight: 500;
transition: color 0.2s;
}
.repo-link a:hover {
color: var(--color-accent);
text-decoration: underline;
}
/* Feature Grid */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--spacing-md);
margin: var(--spacing-lg) 0;
}
.feature-card {
background: var(--glass-elevated-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius);
padding: var(--spacing-md);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
box-shadow: var(--glass-shadow);
}
.feature-card h3 {
color: var(--color-accent);
margin-bottom: var(--spacing-sm);
font-size: 1.2rem;
}
.feature-card p {
color: var(--color-text);
font-size: 0.95rem;
}
.content-block {
margin: var(--spacing-lg) 0;
}
.content-block h3 {
color: var(--color-accent);
margin-bottom: var(--spacing-sm);
}
pre {
background-color: var(--color-code-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius);
padding: var(--spacing-md);
overflow-x: auto;
}
code {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.9rem;
color: var(--color-text);
line-height: 1.5;
}
/* Prism.js syntax highlighting overrides for dark theme */
pre[class*="language-"] {
background-color: var(--color-code-bg) !important;
border: 1px solid var(--color-border);
}
code[class*="language-"] {
color: var(--color-text);
}
/* Override Prism theme colors to match our design */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #6a9955;
}
.token.punctuation {
color: #d4d4d4;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #b5cea8;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #ce9178;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #d4d4d4;
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #569cd6;
}
.token.function,
.token.class-name {
color: #dcdcaa;
}
.token.regex,
.token.important,
.token.variable {
color: #d16969;
}
/* Tech Specs */
.content-block h3,
.architecture h3 {
color: var(--color-accent);
margin-bottom: var(--spacing-sm);
}
.content-block ul {
list-style: none;
margin-left: var(--spacing-md);
}
.content-block li {
margin: var(--spacing-xs) 0;
padding-left: var(--spacing-md);
position: relative;
}
.content-block li:before {
content: "▹";
position: absolute;
left: 0;
color: var(--color-primary);
}
/* Screenshot Grid */
.screenshot-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: var(--spacing-md);
margin: var(--spacing-lg) 0;
}
.screenshot {
background: var(--glass-elevated-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius);
overflow: hidden;
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
box-shadow: var(--glass-shadow);
}
.screenshot img {
width: 100%;
height: auto;
display: block;
border-bottom: 1px solid var(--color-border);
cursor: zoom-in;
}
.screenshot p {
padding: var(--spacing-md);
color: var(--color-text-dim);
font-size: 0.9rem;
}
/* Building Blocks */
.building-blocks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-md);
margin-top: var(--spacing-md);
}
.building-blocks > div {
background: var(--glass-elevated-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius);
padding: var(--spacing-md);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
box-shadow: var(--glass-shadow);
}
.building-blocks strong {
color: var(--color-primary);
display: block;
margin-bottom: var(--spacing-xs);
}
/* Documentation Grid */
.doc-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--spacing-md);
margin: var(--spacing-lg) 0;
max-width: var(--max-width);
margin-left: auto;
margin-right: auto;
padding: 0 var(--spacing-md);
}
.doc-card {
background: var(--glass-elevated-bg);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius);
padding: var(--spacing-md);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
box-shadow: var(--glass-shadow);
}
.doc-card h3 {
color: var(--color-primary);
margin-bottom: var(--spacing-md);
}
.doc-card ul {
list-style: none;
}
.doc-card li {
padding: var(--spacing-xs) 0;
border-bottom: 1px solid var(--color-border);
}
.doc-card li:last-child {
border-bottom: none;
}
/* Getting Started */
.getting-started {
margin: var(--spacing-lg) 0;
}
.getting-started h3 {
color: var(--color-accent);
margin-bottom: var(--spacing-md);
}
.start-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--spacing-md);
}
.start-card h4 {
color: var(--color-primary);
margin-bottom: var(--spacing-sm);
}
/* Footer */
footer {
background: var(--glass-bg);
border-top: 1px solid var(--glass-border);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
padding: var(--spacing-md) 0;
text-align: center;
color: var(--color-text-dim);
font-size: 0.9rem;
}
/* Responsive */
@media (max-width: 768px) {
.hero h2 {
font-size: 2rem;
}
.hero p {
font-size: 1rem;
}
/* Burger button */
.burger {
display: inline-flex;
flex-direction: column;
gap: 5px;
width: 44px;
height: 36px;
background: transparent;
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 10px;
cursor: pointer;
}
.burger span {
display: block;
height: 2px;
background: var(--color-text);
border-radius: 2px;
}
/* Collapsible nav */
.site-nav {
display: none;
flex-direction: column;
gap: 0;
border-top: 1px solid var(--glass-border);
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
}
.site-nav .container {
flex-direction: column;
gap: 0;
}
.site-nav.open {
display: flex;
}
.site-nav a {
padding: 12px var(--spacing-sm);
border-bottom: 1px solid var(--color-border);
}
.feature-grid,
.screenshot-grid,
.doc-grid,
.start-grid {
grid-template-columns: 1fr;
}
.screenshot-grid {
gap: var(--spacing-sm);
}
}
/* Smooth Scrolling */
html {
scroll-behavior: smooth;
}
/* Offset anchored scroll positions to account for fixed header */
section[id] {
scroll-margin-top: 58px; /* Position sections so separators align with header bottom */
}
/* Extra spacing for mobile to prevent top section cutoff */
@media (max-width: 768px) {
section[id] {
scroll-margin-top: 120px; /* Position sections so separators align with mobile header bottom */
}
}
/* Selection */
::selection {
background-color: var(--color-primary);
color: var(--color-bg);
}
/* Lightbox */
.lightbox {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.lightbox[aria-hidden="false"] {
display: flex;
}
.lightbox-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.7);
}
.lightbox-content {
position: relative;
max-width: 90vw;
max-height: 90vh;
margin: 0 var(--spacing-md);
}
.lightbox-content img {
max-width: 100%;
max-height: 85vh;
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
}
.lightbox-content figcaption {
text-align: center;
color: var(--color-text-dim);
margin-top: var(--spacing-xs);
}
.lightbox-close {
position: absolute;
top: -10px;
right: -10px;
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text);
cursor: pointer;
font-size: 20px;
}
.lightbox-close:hover {
background: var(--color-surface-alt);
color: var(--color-primary);
}