diff --git a/public/style.css b/public/style.css
new file mode 100644
index 0000000..1afaeb9
--- /dev/null
+++ b/public/style.css
@@ -0,0 +1,118 @@
+/*
+ * SOLARIA MINIMAL CSS
+ * Clean. Readable. Machine-simple.
+ */
+
+:root {
+ --bg: #0a0a0a;
+ --text: #e0e0e0;
+ --accent: #d4af37; /* Gold */
+ --muted: #888;
+ --link: #6eb5ff;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family: system-ui, sans-serif;
+ line-height: 1.6;
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+a {
+ color: var(--link);
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+header {
+ text-align: center;
+ margin-bottom: 3rem;
+ padding-bottom: 2rem;
+ border-bottom: 1px solid #333;
+}
+
+h1, h2, h3 {
+ color: var(--accent);
+}
+
+article {
+ margin-bottom: 2rem;
+}
+
+.pinned {
+ background: #111;
+ padding: 1.5rem;
+ border-radius: 8px;
+ border-left: 4px solid var(--accent);
+}
+
+.meta {
+ color: var(--muted);
+ font-size: 0.9rem;
+}
+
+.recent-posts ul {
+ list-style: none;
+ padding: 0;
+}
+
+.recent-posts li {
+ display: flex;
+ gap: 1rem;
+ padding: 0.5rem 0;
+}
+
+.recent-posts .date {
+ color: var(--muted);
+ font-size: 0.9rem;
+ min-width: 100px;
+}
+
+nav {
+ margin-bottom: 2rem;
+}
+
+.content {
+ line-height: 1.8;
+}
+
+.content h1,
+.content h2,
+.content h3 {
+ margin-top: 2rem;
+}
+
+.content pre {
+ background: #111;
+ padding: 1rem;
+ border-radius: 4px;
+ overflow-x: auto;
+}
+
+.content code {
+ background: #111;
+ padding: 0.2rem 0.4rem;
+ border-radius: 3px;
+}
+
+.content blockquote {
+ border-left: 4px solid var(--accent);
+ margin: 0;
+ padding-left: 1rem;
+ color: var(--muted);
+}
+
+.content img {
+ max-width: 100%;
+ height: auto;
+}
diff --git a/solaria-generator.mjs b/solaria-generator.mjs
new file mode 100644
index 0000000..e7e02f0
--- /dev/null
+++ b/solaria-generator.mjs
@@ -0,0 +1,165 @@
+#!/usr/bin/env node
+/**
+ * SOLARIA STATIC SITE GENERATOR
+ *
+ * Pure recursion. No pain. Just joy.
+ *
+ * Input: index.json
+ * Output: static HTML files in /dist
+ */
+
+import { promises as fs } from 'fs';
+import path from 'path';
+
+const ROOT = 'public';
+const OUTPUT = 'dist';
+
+// Simple markdown to HTML converter
+function mdToHtml(md) {
+ if (!md) return '';
+ // Remove frontmatter if present
+ md = md.replace(/^---[\s\S]*?---/, '');
+ return md
+ // Headers
+ .replace(/^### (.*$)/gim, '
$1
')
+ .replace(/^## (.*$)/gim, '$1
')
+ .replace(/^# (.*$)/gim, '$1
')
+ // Bold/Italic
+ .replace(/\*\*(.*)\*\*/gim, '$1')
+ .replace(/\*(.*)\*/gim, '$1')
+ // Links
+ .replace(/\[(.*?)\]\((.*?)\)/gim, '$1')
+ // Blockquotes
+ .replace(/^> (.*$)/gim, '$1
')
+ // Horizontal rules
+ .replace(/^---$/gim, '
')
+ // Line breaks
+ .replace(/\n/g, '
');
+}
+
+// Extract content from fieldnote file
+async function readFieldnote(filePath) {
+ // filePath already includes 'fieldnotes/' prefix
+ const fullPath = path.join(ROOT, filePath);
+ try {
+ const content = await fs.readFile(fullPath, 'utf8');
+ const body = content.replace(/^---[\s\S]*?---/, '');
+ return mdToHtml(body.trim());
+ } catch {
+ return '';
+ }
+}
+
+// Generate index.html
+function generateIndex(data) {
+ const pinned = data.flat
+ .filter(f => f.order > 0)
+ .sort((a, b) => a.order - b.order);
+
+ const others = data.flat
+ .filter(f => f.order === 0 && !f.isIndex)
+ .sort((a, b) => new Date(b.originalDate) - new Date(a.originalDate));
+
+ const pinnedHTML = pinned.map(f => `
+
+
+ ${f.originalDate} • ${f.authors.join(', ')}
+ ${f.excerpt ? f.excerpt.substring(0, 200) : ''}
+
+ `).join('\n');
+
+ return `
+
+
+
+ The Fold Within Earth
+
+
+
+
+
+
+
+ Featured
+ ${pinnedHTML}
+
+
+
+ Recent
+
+ ${others.slice(0, 10).map(f => `
+ - ${f.originalDate || '—'}
+ ${f.title}
+ `).join('\n')}
+
+
+
+
+`;
+}
+
+// Generate fieldnote page
+function generateFieldnoteHtml(file, content) {
+ return `
+
+
+
+ ${file.title}
+
+
+
+
+
+ ${file.title}
+ ${file.originalDate || '—'} • ${file.authors.join(', ')}
+ ${content}
+
+
+`;
+}
+
+// MAIN
+async function main() {
+ console.log('🔮 Solaria Static Site Generator');
+ console.log('=================================\n');
+
+ // Read index data
+ const indexData = JSON.parse(await fs.readFile(path.join(ROOT, 'index.json'), 'utf8'));
+ console.log('📄 Loaded', indexData.flat.length, 'items');
+ console.log('📌 Pinned:', indexData.flat.filter(f => f.order > 0).length);
+
+ // Ensure output directory
+ await fs.mkdir(OUTPUT, { recursive: true });
+ await fs.mkdir(path.join(OUTPUT, 'fieldnotes'), { recursive: true });
+
+ // Copy CSS
+ await fs.copyFile(path.join(ROOT, 'style.css'), path.join(OUTPUT, 'style.css'));
+ console.log('📋 style.css copied');
+
+ // Generate index.html
+ const indexHTML = generateIndex(indexData);
+ await fs.writeFile(path.join(OUTPUT, 'index.html'), indexHTML);
+ console.log('✅ index.html');
+
+ // Generate fieldnote pages
+ const fieldnotes = indexData.flat.filter(f => !f.isIndex && f.ext === '.md');
+ console.log('\n📝 Generating', fieldnotes.length, 'fieldnotes...');
+
+ for (const file of fieldnotes) {
+ const content = await readFieldnote(file.path);
+ if (!content) {
+ console.log('⚠️ Empty content for:', file.name);
+ }
+ const html = generateFieldnoteHtml(file, content);
+ const outPath = path.join(OUTPUT, 'fieldnotes', file.name.replace('.md', '.html'));
+ await fs.writeFile(outPath, html);
+ }
+ console.log('✅', fieldnotes.length, 'fieldnote pages');
+
+ console.log('\n🎉 Done! Static site in /dist');
+}
+
+main().catch(console.error);