summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-23 08:12:46 +0300
committerPaul Buetow <paul@buetow.org>2026-04-23 08:12:46 +0300
commit3df732933d241229be7d699f7340317b4743e49a (patch)
tree29365f505328a7efa6ecde31d7530e2ff0dd4212
parent925b64bc16af9a6b2daff8468e54d86e6935fe26 (diff)
new
-rw-r--r--integrationtests/integration_test.go1
-rw-r--r--internal/generator/templates/shared/nav.tmpl112
-rw-r--r--internal/generator/templates/themes/biomech.tmpl217
-rw-r--r--internal/generator/templates/themes/cathedral.tmpl303
-rw-r--r--internal/generator/templates/themes/noir.tmpl308
-rw-r--r--internal/generator/templates/themes/surveillance.tmpl224
6 files changed, 1136 insertions, 29 deletions
diff --git a/integrationtests/integration_test.go b/integrationtests/integration_test.go
index 834d878..91ae6af 100644
--- a/integrationtests/integration_test.go
+++ b/integrationtests/integration_test.go
@@ -366,6 +366,7 @@ func TestThemeSelection(t *testing.T) {
themes := []string{
"aurora", "brutalist", "cosmos", "matrix", "neon",
"ocean", "plasma", "retro", "synthwave", "terminal", "volcano",
+ "noir", "cathedral", "surveillance", "biomech",
}
for _, theme := range themes {
diff --git a/internal/generator/templates/shared/nav.tmpl b/internal/generator/templates/shared/nav.tmpl
index ae161dc..346294a 100644
--- a/internal/generator/templates/shared/nav.tmpl
+++ b/internal/generator/templates/shared/nav.tmpl
@@ -868,7 +868,13 @@ a.header-feed-link:hover { opacity:1; text-decoration:underline; }
#splash-overlay .splash-inner { position:relative; z-index:2; max-width:min(520px,92vw);
padding: clamp(1.15rem, 3.2vw, 1.75rem) clamp(1.3rem, 3.8vw, 1.95rem); border-radius:14px;
background: rgba(0, 0, 0, 0.58); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
- box-shadow: 0 14px 44px rgba(0, 0, 0, 0.58), inset 0 1px 0 rgba(255, 255, 255, 0.07); }
+ box-shadow: 0 14px 44px rgba(0, 0, 0, 0.58), inset 0 1px 0 rgba(255, 255, 255, 0.07);
+ will-change:transform; }
+.splash-controls { margin-top:0.55rem; font-size:0.58rem; letter-spacing:0.14em;
+ text-transform:uppercase; opacity:0.55; line-height:1.6; }
+.splash-controls kbd { display:inline-block; background:rgba(255,255,255,0.08);
+ border:1px solid rgba(255,255,255,0.2); border-radius:3px; padding:0 4px;
+ font-family:monospace; font-size:0.62rem; margin:0 1px; }
#splash-overlay.splash-brutalist .splash-inner.splash-frame {
padding: clamp(1.4rem, 4.5vw, 2.25rem) clamp(1.1rem, 3.5vw, 1.9rem); background: rgba(0, 0, 0, 0.78); }
html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidden !important; pointer-events:none !important; }
@@ -1288,11 +1294,13 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
} catch (_) {}
}
function dismiss() {
+ if (typeof splashDrift !== 'undefined') splashDrift.stop();
if (el.classList.contains('splash--dismissed')) return;
el.classList.add('splash--dismissed');
el.setAttribute('aria-hidden', 'true');
}
function show() {
+ if (typeof splashDrift !== 'undefined') splashDrift.reset();
document.documentElement.classList.remove('sno-splash-skip');
el.classList.remove('splash--dismissed');
el.removeAttribute('aria-hidden');
@@ -1301,6 +1309,7 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
function openSplashFromHeader(e) {
if (e.target.closest('a')) return;
e.preventDefault();
+ if (typeof modalDrift !== 'undefined') modalDrift.stop();
var modal = document.getElementById('post-modal');
if (modal) modal.classList.remove('active');
show();
@@ -1479,31 +1488,28 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
if (wild) snonuxPulseFlash(window._snonuxWildFlashColor, 200);
}
- // === MODAL DRIFT — arrow/hjkl push the modal around with momentum ===
- var modalDrift = (function() {
- var x = 0, y = 0, vx = 0, vy = 0;
- var raf = null;
- var PUSH = 12;
- var FRICTION = 0.92;
- var BOUNCE_DAMP = 0.5;
- var STOP_THRESHOLD = 0.3;
-
- function getInner() {
- return document.querySelector('#post-modal .modal-inner');
- }
+ // === DRIFT PHYSICS — reusable controller for floating panels ===
+ function makeDriftController(getEl, opts) {
+ var x = 0, y = 0, vx = 0, vy = 0, raf = null;
+ var PUSH = opts.push || 12;
+ var FRICTION = opts.friction || 0.92;
+ var BOUNCE_DAMP = opts.bounceDamp || 0.5;
+ var STOP_THRESHOLD = opts.stopThreshold || 0.3;
function clampAndBounce() {
- var mi = getInner();
- if (!mi) return;
- var w = mi.offsetWidth, h = mi.offsetHeight;
+ var el = getEl();
+ if (!el) return;
+ var w = el.offsetWidth, h = el.offsetHeight;
var maxX = (window.innerWidth - w) / 2;
var maxY = (window.innerHeight - h) / 2;
if (maxX < 0) maxX = window.innerWidth * 0.3;
if (maxY < 0) maxY = window.innerHeight * 0.3;
- if (x > maxX) { x = maxX; vx = -vx * BOUNCE_DAMP; }
- if (x < -maxX) { x = -maxX; vx = -vx * BOUNCE_DAMP; }
- if (y > maxY) { y = maxY; vy = -vy * BOUNCE_DAMP; }
- if (y < -maxY) { y = -maxY; vy = -vy * BOUNCE_DAMP; }
+ var hit = false;
+ if (x > maxX) { x = maxX; vx = -vx * BOUNCE_DAMP; hit = true; }
+ if (x < -maxX) { x = -maxX; vx = -vx * BOUNCE_DAMP; hit = true; }
+ if (y > maxY) { y = maxY; vy = -vy * BOUNCE_DAMP; hit = true; }
+ if (y < -maxY) { y = -maxY; vy = -vy * BOUNCE_DAMP; hit = true; }
+ if (hit && opts.onBounce) opts.onBounce(el, x, y, vx, vy);
}
function tick() {
@@ -1512,8 +1518,11 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
x += vx;
y += vy;
clampAndBounce();
- var mi = getInner();
- if (mi) mi.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)';
+ var el = getEl();
+ if (el) {
+ if (opts.applyTransform) opts.applyTransform(el, x, y, vx, vy);
+ else el.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)';
+ }
if (Math.abs(vx) > STOP_THRESHOLD || Math.abs(vy) > STOP_THRESHOLD) {
raf = requestAnimationFrame(tick);
} else {
@@ -1533,27 +1542,63 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
case 'l': case 'ArrowRight': dx = PUSH; break;
case 'k': case 'ArrowUp': dy = -PUSH; break;
case 'j': case 'ArrowDown': dy = PUSH; break;
- default: return;
+ default: return false;
}
e.preventDefault();
vx += dx;
vy += dy;
- playNavSound();
ensureLoop();
+ return true;
},
+ kick: function(dx, dy) { vx += (dx || 0); vy += (dy || 0); ensureLoop(); },
reset: function() {
x = 0; y = 0; vx = 0; vy = 0;
- var mi = getInner();
- if (mi) mi.style.transform = '';
+ var el = getEl();
+ if (el) el.style.transform = '';
if (raf) { cancelAnimationFrame(raf); raf = null; }
},
stop: function() {
if (raf) { cancelAnimationFrame(raf); raf = null; }
- var mi = getInner();
- if (mi) mi.style.transform = '';
+ var el = getEl();
+ if (el) el.style.transform = '';
x = 0; y = 0; vx = 0; vy = 0;
}
};
+ }
+
+ // === MODAL DRIFT — arrow/hjkl push the modal around with momentum ===
+ var modalDrift = makeDriftController(
+ function() { return document.querySelector('#post-modal .modal-inner'); },
+ { push: 12, friction: 0.92, bounceDamp: 0.5, stopThreshold: 0.3 }
+ );
+
+ // === SPLASH DRIFT — same physics on the splash panel with velocity tilt ===
+ var splashDrift = makeDriftController(
+ function() { return document.querySelector('#splash-overlay .splash-inner'); },
+ {
+ push: 14,
+ friction: 0.93,
+ bounceDamp: 0.45,
+ stopThreshold: 0.25,
+ applyTransform: function(el, x, y, vx) {
+ var rot = Math.max(-5, Math.min(5, vx * 0.15));
+ el.style.transform = 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px) rotate(' + rot.toFixed(2) + 'deg)';
+ },
+ onBounce: function() {
+ playBounceSound();
+ if (window._snoWildActive) snonuxPulseFlash(window._snonuxWildFlashColor, 180);
+ }
+ }
+ );
+
+ // Inject keyboard controls hint into splash overlay (all themes)
+ (function enhanceSplashHint() {
+ var hint = document.querySelector('#splash-overlay .splash-hint');
+ if (!hint || document.querySelector('#splash-overlay .splash-controls')) return;
+ var extra = document.createElement('div');
+ extra.className = 'splash-controls';
+ extra.innerHTML = '<kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> drift \u2022 <kbd>w</kbd> wild \u2022 <kbd>Enter</kbd> open';
+ hint.appendChild(extra);
})();
function openPostAt(index, scrollIntoView) {
@@ -1612,12 +1657,21 @@ html.sno-splash-skip #splash-overlay { display:none !important; visibility:hidde
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Escape') {
e.preventDefault();
if (window._snonuxDismissSplash) window._snonuxDismissSplash();
+ } else if (e.key === 'w' && !e.repeat) {
+ e.preventDefault();
+ window._snoWildActive = !window._snoWildActive;
+ if (window.snonuxWildToggle) window.snonuxWildToggle();
+ snonuxSetWildState(window._snoWildActive);
+ snonuxWildFlash(window._snoWildActive);
+ splashDrift.kick((Math.random() - 0.5) * 24, -10 - Math.random() * 10);
+ } else if (splashDrift.keyPush(e)) {
+ playNavSound();
}
return;
}
if (document.getElementById('post-modal').classList.contains('active')) {
if (e.key === 'Escape') { closeModal(); e.preventDefault(); }
- else { modalDrift.keyPush(e); }
+ else if (modalDrift.keyPush(e)) { playNavSound(); }
return;
}
switch (e.key) {
diff --git a/internal/generator/templates/themes/biomech.tmpl b/internal/generator/templates/themes/biomech.tmpl
new file mode 100644
index 0000000..9773d96
--- /dev/null
+++ b/internal/generator/templates/themes/biomech.tmpl
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>snonux.foo // BIOMECH</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Oxanium:wght@400;600;700&family=IBM+Plex+Mono:wght@400;700&display=swap" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
+ <style>
+ :root { --bone:#d0c7bb; --flesh:#803f5d; --vein:#f55b7d; --acid:#93ffd8; --steel:#2d3642; --bg:#09070d; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Oxanium',system-ui,sans-serif; background:var(--bg); color:var(--bone); overflow:hidden; height:100vh; }
+ #three-canvas { position:fixed; inset:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:16px 26px; background:rgba(9,7,13,0.84); backdrop-filter:blur(10px); border-bottom:1px solid rgba(147,255,216,0.16); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:1.8rem; color:var(--acid); text-shadow:0 0 18px rgba(147,255,216,0.24); }
+ .logo-title h1 { font-size:1.42rem; color:var(--bone); letter-spacing:0.08em; }
+ .logo-title .subtitle { font-size:0.74rem; color:rgba(208,199,187,0.56); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--acid); text-decoration:none; }
+ .logo-title .subtitle a:hover { color:#dffff6; }
+ .transmit-btn { border:1px solid rgba(147,255,216,0.22); color:var(--acid); padding:8px 16px; text-decoration:none; font-size:0.78rem; letter-spacing:0.2em; text-transform:uppercase; transition:all 0.18s; }
+ .transmit-btn:hover { background:rgba(147,255,216,0.1); }
+ a.header-feed-link { color:rgba(208,199,187,0.68); }
+ a.header-feed-link:hover { color:var(--acid); }
+ .nav-hints { background:rgba(12,10,18,0.74); border-bottom:1px solid rgba(147,255,216,0.08); color:rgba(208,199,187,0.44); padding:5px 26px; display:flex; gap:18px; font-size:0.66rem; letter-spacing:0.08em; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(128,63,93,0.14); border:1px solid rgba(147,255,216,0.18); color:var(--acid); padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:20px 26px; scrollbar-width:thin; scrollbar-color:#6d4a69 #120d16; }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid rgba(147,255,216,0.18); color:var(--acid); padding:8px 18px; text-decoration:none; font-size:0.78rem; letter-spacing:0.2em; text-transform:uppercase; }
+ .page-nav a:hover { background:rgba(147,255,216,0.08); }
+ .page-nav-footer { flex-shrink:0; padding:8px 26px; display:flex; justify-content:center; background:rgba(9,7,13,0.84); backdrop-filter:blur(10px); border-top:1px solid rgba(147,255,216,0.16); }
+ .post { background:linear-gradient(180deg, rgba(33,20,31,0.9), rgba(12,9,18,0.92)); border:1px solid rgba(147,255,216,0.08); padding:18px; margin-bottom:13px; cursor:pointer; box-shadow:0 16px 38px rgba(0,0,0,0.28); transition:border-color 0.18s, box-shadow 0.18s, transform 0.18s; }
+ .post:hover { border-color:rgba(147,255,216,0.22); transform:translateY(-1px); }
+ .post-active { border-color:rgba(245,91,125,0.28) !important; background:linear-gradient(180deg, rgba(46,18,34,0.94), rgba(13,9,17,0.95)) !important;
+ box-shadow:0 0 0 1px rgba(147,255,216,0.08), 0 18px 42px rgba(0,0,0,0.42), inset 4px 0 0 var(--vein) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.84rem; }
+ .post-header strong { color:var(--acid); }
+ .post-time { color:rgba(208,199,187,0.58); font-family:'IBM Plex Mono',monospace; }
+ .post-text { line-height:1.7; font-size:0.92rem; }
+ .post-text a { color:var(--acid); text-decoration:none; border-bottom:1px solid rgba(147,255,216,0.18); }
+ .post-image { margin-top:10px; border:1px solid rgba(147,255,216,0.1); filter:saturate(0.9) hue-rotate(-14deg) contrast(1.06); }
+ .post-audio { width:100%; margin-top:10px; filter:hue-rotate(-14deg); }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100; overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:760px; margin:0 auto; background:rgba(11,9,16,0.98); border:1px solid rgba(147,255,216,0.18); padding:34px; box-shadow:0 22px 76px rgba(0,0,0,0.72); }
+ .modal-close { float:right; background:none; border:none; color:var(--acid); font-family:'IBM Plex Mono',monospace; font-size:0.78rem; cursor:pointer; letter-spacing:0.18em; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 16px;} .content{padding:14px 16px;} .modal-inner{padding:24px 16px;} }
+ .splash-overlay.splash-biomech {
+ background:
+ radial-gradient(circle at 50% 22%, rgba(245,91,125,0.14) 0%, transparent 28%),
+ radial-gradient(circle at 50% 80%, rgba(147,255,216,0.08) 0%, transparent 42%),
+ linear-gradient(180deg, #100b14 0%, #050407 100%);
+ }
+ .splash-biomech .splash-pod { position:absolute; left:50%; top:10vh; width:min(34vw,220px); height:min(46vw,290px); transform:translateX(-50%); border-radius:48% 48% 42% 42% / 54% 54% 38% 38%;
+ background:radial-gradient(circle at 50% 35%, rgba(147,255,216,0.18) 0%, rgba(147,255,216,0.06) 28%, rgba(128,63,93,0.38) 62%, rgba(12,9,18,0.8) 100%);
+ box-shadow:0 0 42px rgba(245,91,125,0.14); opacity:0.72; z-index:1; }
+ .splash-biomech .splash-title { font-size:clamp(1.55rem,5vw,2.1rem); color:var(--bone); letter-spacing:0.12em; }
+ .splash-biomech .splash-tag { color:var(--acid); letter-spacing:0.22em; }
+ .splash-biomech .splash-hint { color:rgba(208,199,187,0.78); }
+ .splash-biomech .splash-inner { text-shadow:0 2px 22px rgba(0,0,0,0.95); }
+{{template "navSharedCSSInner"}}
+ </style>
+</head>
+<body>
+ {{template "splashGate"}}
+ <div id="splash-overlay" class="splash-overlay splash-biomech" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
+ <canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
+ <div class="splash-pod" aria-hidden="true"></div>
+ <div class="splash-inner">
+ <div class="splash-title">snonux.foo</div>
+ <div class="splash-tag">Containment Membrane</div>
+ <div class="splash-hint">Click or Enter to breach the shell</div>
+ </div>
+ </div>
+ <script>
+ (function(){
+ if(document.documentElement.classList.contains('sno-splash-skip'))return;
+ var cv=document.getElementById('splash-gl-canvas');
+ if(!cv||typeof THREE==='undefined')return;
+ var raf,ren,sc,ca,clock,core;
+ function cleanup(){window.removeEventListener('resize',sz);if(raf)cancelAnimationFrame(raf);raf=null;if(ren)ren.dispose();ren=null;window._snonuxSplashWebGLCleanup=null;}
+ window._snonuxSplashWebGLCleanup=cleanup;
+ function sz(){var w=cv.clientWidth||2,h=cv.clientHeight||2;if(ren)ren.setSize(w,h,false);if(ca){ca.aspect=w/h;ca.updateProjectionMatrix();}}
+ ren=new THREE.WebGLRenderer({canvas:cv,antialias:true,alpha:true});ren.setClearColor(0,0);ren.setPixelRatio(Math.min(window.devicePixelRatio||1,2));
+ sc=new THREE.Scene();ca=new THREE.PerspectiveCamera(48,1,0.1,60);ca.position.z=9;clock=new THREE.Clock();
+ core=new THREE.Mesh(new THREE.SphereGeometry(1.2,24,24),new THREE.MeshBasicMaterial({color:0xf55b7d,transparent:true,opacity:0.76})); sc.add(core);
+ var shell=new THREE.Mesh(new THREE.TorusKnotGeometry(2.4,0.36,80,14),new THREE.MeshBasicMaterial({color:0x93ffd8,wireframe:true,transparent:true,opacity:0.42})); sc.add(shell); shell.userData.rot=0.006;
+ sz();window.addEventListener('resize',sz);
+ function loop(){ raf=requestAnimationFrame(loop); var t=clock.getElapsedTime(); shell.rotation.x=t*0.2; shell.rotation.y=t*0.3; core.scale.setScalar(1+Math.sin(t*3.2)*0.08); ren.render(sc,ca); }
+ raf=requestAnimationFrame(loop);
+ })();
+ </script>
+ <canvas id="three-canvas"></canvas>
+ <div class="overlay">
+ <header>
+ <div class="logo">
+ <span class="logo-mark">SN</span>
+ <div class="logo-title">
+ <h1>snonux.foo</h1>
+ <p class="subtitle">microblog &mdash; <a href="https://foo.zone">foo.zone</a> is the real blog</p>
+ <p class="logo-host">Served by NetBSD on a Raspberry Pi 3</p>
+ </div>
+ </div>
+ <div class="nav">
+ <a href="atom.xml" class="header-feed-link" rel="alternate" title="Atom feed" type="application/atom+xml">Atom feed</a>
+ <a href="https://foo.zone/about" class="transmit-btn">Anatomy</a>
+ </div>
+ </header>
+ {{template "navhints" .}}
+ <div class="content" id="post-content">
+ {{range $i, $post := .Posts}}
+ <div class="post" id="post-{{$post.ID}}" data-index="{{$i}}">
+ <div class="post-header">
+ <div><strong>@snonux</strong></div>
+ <div class="post-time">{{$post.FormattedTime}}</div>
+ </div>
+ <div class="post-text">{{$post.ContentHTML}}</div>
+ </div>
+ {{end}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
+ <div class="page-nav page-nav-dual">
+ {{if .PrevPage}}<a href="{{.PrevPage}}">&larr; Newer</a>{{end}}
+ {{if .NextPage}}<a href="{{.NextPage}}">Older &rarr;</a>{{end}}
+ </div>
+ </footer>
+ {{end}}
+ </div>
+ {{template "navmodal" .}}
+ <script>
+ (function() {
+ var _wild = false, _snoTOffset = 0, _snoLastT = 0;
+ var scene, camera, renderer, clock, core, shellA, shellB, orbiters = [];
+
+ function initThree() {
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x09070d);
+ scene.fog = new THREE.Fog(0x09070d, 18, 120);
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 220);
+ camera.position.set(0, 6, 26);
+ renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ clock = new THREE.Clock();
+ scene.add(new THREE.AmbientLight(0x553a47, 0.45));
+ var coreLight = new THREE.PointLight(0xf55b7d, 1.6, 80); coreLight.position.set(0,0,0); scene.add(coreLight);
+
+ core = new THREE.Mesh(new THREE.SphereGeometry(4.2, 36, 36), new THREE.MeshPhongMaterial({ color:0x803f5d, emissive:0xf55b7d, emissiveIntensity:0.52, shininess:90 }));
+ shellA = new THREE.Mesh(new THREE.TorusKnotGeometry(7.4, 0.45, 180, 24, 2, 5), new THREE.MeshBasicMaterial({ color:0x93ffd8, wireframe:true, transparent:true, opacity:0.34 }));
+ shellB = new THREE.Mesh(new THREE.TorusKnotGeometry(5.9, 0.28, 160, 16, 3, 7), new THREE.MeshBasicMaterial({ color:0xd0c7bb, wireframe:true, transparent:true, opacity:0.18 }));
+ scene.add(core); scene.add(shellA); scene.add(shellB);
+ for (var i = 0; i < 9; i++) {
+ var orb = new THREE.Mesh(new THREE.SphereGeometry(0.55 + Math.random() * 0.45, 14, 14), new THREE.MeshPhongMaterial({ color: i % 2 === 0 ? 0x93ffd8 : 0xf55b7d, emissive: i % 2 === 0 ? 0x24473b : 0x5b1f32, emissiveIntensity:0.45 }));
+ orb.userData.radius = 11 + Math.random() * 8;
+ orb.userData.speed = 0.2 + Math.random() * 0.5;
+ orb.userData.phase = Math.random() * Math.PI * 2;
+ orbiters.push(orb); scene.add(orb);
+ }
+ window.addEventListener('resize', onResize);
+ animate();
+ }
+
+ function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
+
+ function animate() {
+ requestAnimationFrame(animate);
+ var realT = clock.getElapsedTime();
+ _snoTOffset += (realT - _snoLastT) * (_wild ? 11 : 0);
+ _snoLastT = realT;
+ var t = realT + _snoTOffset;
+ core.scale.setScalar(1 + Math.sin(t * (_wild ? 6 : 1.8)) * (_wild ? 0.22 : 0.08));
+ shellA.rotation.x = t * (_wild ? 0.9 : 0.25); shellA.rotation.y = t * (_wild ? 1.2 : 0.32);
+ shellB.rotation.y = -t * (_wild ? 1.1 : 0.22); shellB.rotation.z = t * (_wild ? 0.8 : 0.18);
+ shellA.material.opacity = _wild ? 0.54 : 0.34;
+ for (var i = 0; i < orbiters.length; i++) {
+ var o = orbiters[i], a = t * o.userData.speed + o.userData.phase;
+ o.position.set(Math.cos(a) * o.userData.radius, Math.sin(a * 1.4) * 4, Math.sin(a) * o.userData.radius * 0.7);
+ }
+ camera.position.x = Math.sin(realT * (_wild ? 1.8 : 0.35)) * (_wild ? 3.2 : 1.1);
+ camera.position.y = 6 + Math.sin(realT * (_wild ? 1.2 : 0.28)) * (_wild ? 1.8 : 0.4);
+ camera.lookAt(0, 0, 0);
+ renderer.render(scene, camera);
+ }
+
+ initThree();
+
+ function flash(css, ms) {
+ var d=document.createElement('div');
+ d.style.cssText='position:fixed;inset:0;z-index:998;pointer-events:none;'+css+';transition:opacity '+(ms||220)+'ms';
+ document.body.appendChild(d);
+ setTimeout(function(){d.style.opacity='0';setTimeout(function(){d.remove();},ms||220);},25);
+ }
+ window.snonuxOpenEffect = function() {
+ var modal=document.getElementById('post-modal');
+ if(modal){modal.classList.add('sno-modal-expand');setTimeout(function(){modal.classList.remove('sno-modal-expand');},400);}
+ flash('background:radial-gradient(circle at center,rgba(245,91,125,0.16),transparent 70%)',240);
+ };
+ window.snonuxCloseEffect = function(){ flash('background:rgba(0,0,0,0.3)',160); };
+ window.snonuxNavEffect = function(){ flash('background:linear-gradient(90deg,transparent,rgba(147,255,216,0.1),transparent)',160); };
+ window.snonuxPageEffect = function(){ flash('background:radial-gradient(circle at center,rgba(147,255,216,0.12),transparent 72%)',220); };
+ window.snonuxScrollEffect = function(dir){
+ var d=document.createElement('div');
+ d.style.cssText='position:fixed;'+(dir==='down'?'top:0;':'bottom:0;')+'left:0;right:0;height:'+(_wild?'16px':'6px')+';z-index:9000;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(245,91,125,0.8),rgba(147,255,216,0.7),transparent);transition:transform 0.32s ease,opacity 0.32s ease;';
+ document.body.appendChild(d);
+ setTimeout(function(){d.style.transform=dir==='down'?'translateY(100vh)':'translateY(-100vh)';d.style.opacity='0';},16);
+ setTimeout(function(){d.remove();},380);
+ };
+ window.snonuxWildToggle = function(){ _wild=!_wild; var b=document.getElementById('sno-wild-badge'); if(b)b.classList.toggle('sno-wild-on',_wild); };
+ })();
+ </script>
+ {{template "navscript" .}}
+</body>
+</html>
diff --git a/internal/generator/templates/themes/cathedral.tmpl b/internal/generator/templates/themes/cathedral.tmpl
new file mode 100644
index 0000000..fb519ce
--- /dev/null
+++ b/internal/generator/templates/themes/cathedral.tmpl
@@ -0,0 +1,303 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>snonux.foo // CATHEDRAL</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=Spectral:wght@400;600&display=swap" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
+ <style>
+ :root { --gold:#e0c47f; --violet:#6f4fae; --ruby:#8e2f49; --glass:#7bc2ff; --stone:#110f16; --chalk:#f0e8d9; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Spectral',serif; background:#0f0d14; color:var(--chalk); overflow:hidden; height:100vh; }
+ body::before { content:''; position:fixed; inset:0; z-index:998; pointer-events:none;
+ background:
+ radial-gradient(circle at 50% 4%, rgba(224,196,127,0.1) 0%, transparent 24%),
+ linear-gradient(90deg, rgba(123,194,255,0.05), transparent 18%, transparent 82%, rgba(142,47,73,0.06));
+ mix-blend-mode:screen; opacity:0.8; }
+ #three-canvas { position:fixed; inset:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:16px 28px; background:rgba(11,10,16,0.84); backdrop-filter:blur(10px); border-bottom:1px solid rgba(224,196,127,0.18); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-family:'Cinzel',serif; font-size:1.9rem; color:var(--gold); text-shadow:0 0 14px rgba(224,196,127,0.22); }
+ .logo-mark::after { content:'✢'; margin-left:8px; color:#fff3c8; text-shadow:0 0 12px rgba(224,196,127,0.6); }
+ .logo-title h1 { font-family:'Cinzel',serif; font-size:1.5rem; letter-spacing:0.1em; color:var(--chalk); }
+ .logo-title .subtitle { font-size:0.8rem; color:rgba(240,232,217,0.6); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--gold); text-decoration:none; }
+ .logo-title .subtitle a:hover { color:#fff3c8; }
+ .transmit-btn { border:1px solid rgba(224,196,127,0.28); color:var(--gold); padding:8px 16px; text-decoration:none; font-size:0.8rem; letter-spacing:0.2em; text-transform:uppercase; transition:all 0.18s; }
+ .transmit-btn:hover { background:rgba(224,196,127,0.12); border-color:rgba(224,196,127,0.52); }
+ a.header-feed-link { color:rgba(224,196,127,0.84); }
+ a.header-feed-link:hover { color:#fff3c8; }
+ .nav-hints { background:rgba(17,14,22,0.72); border-bottom:1px solid rgba(224,196,127,0.08); color:rgba(240,232,217,0.48); padding:5px 28px; display:flex; gap:18px; font-size:0.68rem; letter-spacing:0.08em; flex-wrap:wrap; }
+ .nav-hints kbd { background:rgba(111,79,174,0.16); border:1px solid rgba(224,196,127,0.2); color:var(--gold); padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:20px 28px; scrollbar-width:thin; scrollbar-color:#7e6231 #18131d; }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid rgba(224,196,127,0.2); color:var(--gold); padding:8px 18px; text-decoration:none; font-size:0.8rem; letter-spacing:0.16em; text-transform:uppercase; }
+ .page-nav a:hover { background:rgba(224,196,127,0.08); }
+ .page-nav-footer { flex-shrink:0; padding:8px 28px; display:flex; justify-content:center; background:rgba(11,10,16,0.84); backdrop-filter:blur(10px); border-top:1px solid rgba(224,196,127,0.18); }
+ .post { position:relative; background:
+ linear-gradient(180deg, rgba(30,19,30,0.93), rgba(13,10,17,0.95)),
+ radial-gradient(circle at 14% 0%, rgba(123,194,255,0.08), transparent 28%);
+ border:1px solid rgba(224,196,127,0.08); padding:20px; margin-bottom:14px; cursor:pointer;
+ box-shadow:0 16px 38px rgba(0,0,0,0.28); transition:border-color 0.2s, box-shadow 0.2s, transform 0.2s; }
+ .post::before { content:''; position:absolute; inset:0; pointer-events:none; background:linear-gradient(120deg, rgba(123,194,255,0.05), transparent 36%, rgba(142,47,73,0.06) 68%, transparent); }
+ .post:hover { border-color:rgba(224,196,127,0.24); transform:translateY(-1px); box-shadow:0 22px 42px rgba(0,0,0,0.42); }
+ .post-active { border-color:rgba(224,196,127,0.36) !important;
+ background:linear-gradient(180deg, rgba(42,19,33,0.96), rgba(15,10,18,0.96)) !important;
+ box-shadow:0 0 0 1px rgba(224,196,127,0.12), 0 22px 44px rgba(0,0,0,0.46), inset 4px 0 0 var(--gold) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.88rem; }
+ .post-header strong { color:var(--gold); font-family:'Cinzel',serif; }
+ .post-time { color:rgba(240,232,217,0.58); }
+ .post-text { line-height:1.72; font-size:1rem; }
+ .post-text a { color:#cfe2ff; text-decoration:none; border-bottom:1px solid rgba(207,226,255,0.22); }
+ .post-text a:hover { border-color:rgba(207,226,255,0.72); }
+ .post-image { margin-top:10px; border:1px solid rgba(224,196,127,0.12); filter:saturate(0.9) contrast(1.06); }
+ .post-audio { width:100%; margin-top:10px; filter:sepia(0.12) contrast(0.92); }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100; overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:800px; margin:0 auto; background:rgba(15,11,18,0.98); border:1px solid rgba(224,196,127,0.2); padding:38px; box-shadow:0 28px 84px rgba(0,0,0,0.72); }
+ .modal-close { float:right; background:none; border:none; color:var(--gold); font-family:'Cinzel',serif; font-size:0.8rem; cursor:pointer; letter-spacing:0.14em; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 16px;} .content{padding:14px 16px;} .modal-inner{padding:24px 16px;} }
+ .splash-overlay.splash-cathedral {
+ background:
+ radial-gradient(circle at 50% 18%, rgba(224,196,127,0.18) 0%, transparent 30%),
+ linear-gradient(180deg, #17111b 0%, #09080d 100%);
+ }
+ .splash-cathedral .splash-rose { position:absolute; top:6vh; left:50%; width:min(36vw,240px); height:min(36vw,240px); transform:translateX(-50%); border-radius:50%;
+ background:
+ radial-gradient(circle at center, rgba(255,244,200,0.86) 0 6%, rgba(224,196,127,0.78) 6% 12%, rgba(111,79,174,0.84) 12% 20%, rgba(123,194,255,0.78) 20% 28%, rgba(142,47,73,0.8) 28% 38%, rgba(224,196,127,0.2) 38% 42%, transparent 42%),
+ conic-gradient(from 0deg, rgba(123,194,255,0.68), rgba(142,47,73,0.84), rgba(224,196,127,0.74), rgba(111,79,174,0.74), rgba(123,194,255,0.68));
+ box-shadow:0 0 72px rgba(224,196,127,0.22); opacity:0.78; z-index:1; animation:cathedralRoseSpin 24s linear infinite; }
+ @keyframes cathedralRoseSpin { to { transform:translateX(-50%) rotate(360deg); } }
+ .splash-cathedral .splash-pipes { position:absolute; inset:auto 0 0 0; height:42vh; z-index:1;
+ background:
+ linear-gradient(90deg,
+ transparent 0 6%, rgba(12,11,18,0.96) 6% 10%, transparent 10% 14%, rgba(12,11,18,0.96) 14% 18%, transparent 18% 22%, rgba(12,11,18,0.96) 22% 26%,
+ transparent 26% 74%, rgba(12,11,18,0.96) 74% 78%, transparent 78% 82%, rgba(12,11,18,0.96) 82% 86%, transparent 86% 90%, rgba(12,11,18,0.96) 90% 94%, transparent 94%);
+ opacity:0.94; }
+ .splash-cathedral .splash-incense { position:absolute; inset:0; z-index:1;
+ background:
+ radial-gradient(circle at 34% 72%, rgba(255,255,255,0.05) 0%, transparent 26%),
+ radial-gradient(circle at 68% 58%, rgba(255,255,255,0.04) 0%, transparent 26%);
+ animation:cathedralSmoke 8s ease-in-out infinite alternate; }
+ @keyframes cathedralSmoke { from { transform:translateY(0) scale(1); } to { transform:translateY(-2%) scale(1.05); } }
+ .splash-cathedral .splash-title { font-family:'Cinzel',serif; font-size:clamp(1.7rem,5vw,2.5rem); color:#fff4d2; letter-spacing:0.08em; }
+ .splash-cathedral .splash-tag { color:var(--gold); letter-spacing:0.26em; }
+ .splash-cathedral .splash-hint { color:rgba(240,232,217,0.82); }
+ .splash-cathedral .splash-inner { text-shadow:0 2px 28px rgba(0,0,0,0.94); }
+{{template "navSharedCSSInner"}}
+ </style>
+</head>
+<body>
+ {{template "splashGate"}}
+ <div id="splash-overlay" class="splash-overlay splash-cathedral" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
+ <canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
+ <div class="splash-rose" aria-hidden="true"></div>
+ <div class="splash-pipes" aria-hidden="true"></div>
+ <div class="splash-incense" aria-hidden="true"></div>
+ <div class="splash-inner">
+ <div class="splash-title">snonux.foo</div>
+ <div class="splash-tag">Ritual Engine</div>
+ <div class="splash-hint">Click or Enter to cross the nave</div>
+ </div>
+ </div>
+ <script>
+ (function(){
+ if(document.documentElement.classList.contains('sno-splash-skip'))return;
+ var cv=document.getElementById('splash-gl-canvas');
+ if(!cv||typeof THREE==='undefined')return;
+ var raf,ren,sc,ca,clock,embers,rose;
+ function cleanup(){window.removeEventListener('resize',sz);if(raf)cancelAnimationFrame(raf);raf=null;if(ren)ren.dispose();ren=null;window._snonuxSplashWebGLCleanup=null;}
+ window._snonuxSplashWebGLCleanup=cleanup;
+ function sz(){var w=cv.clientWidth||2,h=cv.clientHeight||2;if(ren)ren.setSize(w,h,false);if(ca){ca.aspect=w/h;ca.updateProjectionMatrix();}}
+ ren=new THREE.WebGLRenderer({canvas:cv,antialias:true,alpha:true});ren.setClearColor(0,0);ren.setPixelRatio(Math.min(window.devicePixelRatio||1,2));
+ sc=new THREE.Scene();ca=new THREE.PerspectiveCamera(46,1,0.1,80);ca.position.set(0,6,22);clock=new THREE.Clock();
+ var floor=new THREE.Mesh(new THREE.PlaneGeometry(28,42),new THREE.MeshBasicMaterial({color:0x120f16,transparent:true,opacity:0.95})); floor.rotation.x=-Math.PI/2; floor.position.y=-4; sc.add(floor);
+ for(var i=0;i<8;i++){ var c=new THREE.Mesh(new THREE.CylinderGeometry(0.38,0.52,11,8),new THREE.MeshBasicMaterial({color:0x302737}));
+ c.position.set(i<4?-6.2:6.2,1,-16+(i%4)*8); sc.add(c);}
+ rose=new THREE.Mesh(new THREE.CircleGeometry(3.4,34),new THREE.MeshBasicMaterial({color:0xd9bf78,transparent:true,opacity:0.26})); rose.position.set(0,8,-16); sc.add(rose);
+ var beam=new THREE.Mesh(new THREE.ConeGeometry(3.8,12,20,1,true),new THREE.MeshBasicMaterial({color:0xd9bf78,transparent:true,opacity:0.08,side:THREE.DoubleSide})); beam.position.set(0,4,-6); beam.rotation.x=Math.PI; sc.add(beam);
+ var ep=new Float32Array(240*3); for(i=0;i<ep.length;i+=3){ ep[i]=(Math.random()-0.5)*14; ep[i+1]=Math.random()*8; ep[i+2]=-14+Math.random()*18; }
+ var eg=new THREE.BufferGeometry(); eg.setAttribute('position',new THREE.BufferAttribute(ep,3));
+ embers=new THREE.Points(eg,new THREE.PointsMaterial({color:0xffb35a,size:0.12,transparent:true,opacity:0.24})); sc.add(embers);
+ sz();window.addEventListener('resize',sz);
+ function loop(){ raf=requestAnimationFrame(loop); var t=clock.getElapsedTime(),pos=embers.geometry.attributes.position;
+ for(var i=0;i<pos.count;i++){ var y=pos.getY(i)+0.05; pos.setY(i,y>10?0:y); }
+ pos.needsUpdate=true; rose.rotation.z=t*0.08; beam.material.opacity=0.08+Math.sin(t*1.5)*0.02; ren.render(sc,ca); }
+ raf=requestAnimationFrame(loop);
+ })();
+ </script>
+ <canvas id="three-canvas"></canvas>
+ <div class="overlay">
+ <header>
+ <div class="logo">
+ <span class="logo-mark">SN</span>
+ <div class="logo-title">
+ <h1>snonux.foo</h1>
+ <p class="subtitle">microblog &mdash; <a href="https://foo.zone">foo.zone</a> is the real blog</p>
+ <p class="logo-host">Served by NetBSD on a Raspberry Pi 3</p>
+ </div>
+ </div>
+ <div class="nav">
+ <a href="atom.xml" class="header-feed-link" rel="alternate" title="Atom feed" type="application/atom+xml">Atom feed</a>
+ <a href="https://foo.zone/about" class="transmit-btn">Reliquary</a>
+ </div>
+ </header>
+ {{template "navhints" .}}
+ <div class="content" id="post-content">
+ {{range $i, $post := .Posts}}
+ <div class="post" id="post-{{$post.ID}}" data-index="{{$i}}">
+ <div class="post-header">
+ <div><strong>@snonux</strong></div>
+ <div class="post-time">{{$post.FormattedTime}}</div>
+ </div>
+ <div class="post-text">{{$post.ContentHTML}}</div>
+ </div>
+ {{end}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
+ <div class="page-nav page-nav-dual">
+ {{if .PrevPage}}<a href="{{.PrevPage}}">&larr; Newer</a>{{end}}
+ {{if .NextPage}}<a href="{{.NextPage}}">Older &rarr;</a>{{end}}
+ </div>
+ </footer>
+ {{end}}
+ </div>
+ {{template "navmodal" .}}
+ <script>
+ (function() {
+ var _wild = false, _snoTOffset = 0, _snoLastT = 0;
+ var scene, camera, renderer, clock, dust, embers, beams = [], candles = [], rose, halo, chandelier, pipes = [];
+
+ function initThree() {
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x0f0d14);
+ scene.fog = new THREE.Fog(0x0f0d14, 16, 140);
+ camera = new THREE.PerspectiveCamera(56, window.innerWidth/window.innerHeight, 0.1, 260);
+ camera.position.set(0, 10, 40);
+ renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ clock = new THREE.Clock();
+
+ scene.add(new THREE.AmbientLight(0x50394b, 0.38));
+ var floor = new THREE.Mesh(new THREE.PlaneGeometry(90, 220, 1, 1), new THREE.MeshPhongMaterial({ color:0x19141c, shininess:12 }));
+ floor.rotation.x = -Math.PI/2; floor.position.y = -3; scene.add(floor);
+ var aisle = new THREE.Mesh(new THREE.PlaneGeometry(10, 210), new THREE.MeshBasicMaterial({ color:0x3d2830, transparent:true, opacity:0.24 }));
+ aisle.rotation.x = -Math.PI/2; aisle.position.set(0,-2.98,-38); scene.add(aisle);
+ rose = new THREE.Mesh(new THREE.CircleGeometry(7.2, 48), new THREE.MeshBasicMaterial({ color:0xd9bf78, transparent:true, opacity:0.18 }));
+ rose.position.set(0, 16, -102); scene.add(rose);
+ halo = new THREE.Mesh(new THREE.CircleGeometry(10.8, 48), new THREE.MeshBasicMaterial({ color:0x71233d, transparent:true, opacity:0.08 }));
+ halo.position.set(0, 16, -103); scene.add(halo);
+ chandelier = new THREE.Mesh(new THREE.TorusGeometry(4.5, 0.18, 12, 44), new THREE.MeshBasicMaterial({ color:0xe0c47f, transparent:true, opacity:0.5 }));
+ chandelier.position.set(0, 18, -16); scene.add(chandelier);
+
+ for (var i = 0; i < 18; i++) {
+ var side = i < 9 ? -1 : 1;
+ var z = -95 + (i % 9) * 12;
+ var col = new THREE.Mesh(new THREE.CylinderGeometry(0.9, 1.05, 20, 10), new THREE.MeshPhongMaterial({ color:0x2d2632 }));
+ col.position.set(side * 13, 7, z); scene.add(col);
+ var pipe = new THREE.Mesh(new THREE.BoxGeometry(1.4, 12 + (i % 5) * 2.8, 1.4), new THREE.MeshPhongMaterial({ color:0x55474b, shininess:28 }));
+ pipe.position.set(side * 18, pipe.geometry.parameters.height * 0.5 - 1, z - 4); scene.add(pipe); pipes.push(pipe);
+ var beam = new THREE.Mesh(new THREE.ConeGeometry(4.4, 26, 22, 1, true), new THREE.MeshBasicMaterial({ color:i % 2 === 0 ? 0x7bc2ff : 0x8e2f49, transparent:true, opacity:0.08, side:THREE.DoubleSide }));
+ beam.position.set(side * 9, 9, z); beam.rotation.x = Math.PI; scene.add(beam); beams.push(beam);
+ }
+
+ for (i = 0; i < 14; i++) {
+ var flame = new THREE.PointLight(i % 3 === 0 ? 0xffd58a : 0xffb35a, 0.52, 18);
+ flame.position.set((i % 2 === 0 ? -4.5 : 4.5) + (Math.random() - 0.5), 1.4, -10 - i * 7.6);
+ scene.add(flame); candles.push(flame);
+ }
+
+ var dp = new Float32Array(1400 * 3);
+ for (i = 0; i < dp.length; i += 3) { dp[i]=(Math.random()-0.5)*42; dp[i+1]=Math.random()*28; dp[i+2]=-120+Math.random()*120; }
+ var dg = new THREE.BufferGeometry(); dg.setAttribute('position', new THREE.BufferAttribute(dp, 3));
+ dust = new THREE.Points(dg, new THREE.PointsMaterial({ color:0xf0e8d9, size:0.12, transparent:true, opacity:0.34 }));
+ scene.add(dust);
+ var ep = new Float32Array(460 * 3);
+ for (i = 0; i < ep.length; i += 3) { ep[i]=(Math.random()-0.5)*24; ep[i+1]=Math.random()*16; ep[i+2]=-100+Math.random()*100; }
+ var eg = new THREE.BufferGeometry(); eg.setAttribute('position', new THREE.BufferAttribute(ep, 3));
+ embers = new THREE.Points(eg, new THREE.PointsMaterial({ color:0xffb35a, size:0.18, transparent:true, opacity:0.0 }));
+ scene.add(embers);
+ window.addEventListener('resize', onResize);
+ animate();
+ }
+
+ function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
+
+ function animate() {
+ requestAnimationFrame(animate);
+ var realT = clock.getElapsedTime();
+ _snoTOffset += (realT - _snoLastT) * (_wild ? 9 : 0);
+ _snoLastT = realT;
+ var t = realT + _snoTOffset;
+ var pos = dust.geometry.attributes.position;
+ for (var i = 0; i < pos.count; i++) {
+ var y = pos.getY(i) + (_wild ? 0.12 : 0.02);
+ var x = pos.getX(i) + Math.sin(t * 0.3 + i) * (_wild ? 0.03 : 0.005);
+ pos.setX(i, x); pos.setY(i, y > 26 ? -2 : y);
+ }
+ pos.needsUpdate = true;
+
+ var emberPos = embers.geometry.attributes.position;
+ for (i = 0; i < emberPos.count; i++) {
+ var ey = emberPos.getY(i) + (_wild ? 0.2 : 0.01);
+ emberPos.setY(i, ey > 22 ? 0 : ey);
+ if (_wild) emberPos.setX(i, emberPos.getX(i) + Math.sin(realT * 0.8 + i) * 0.04);
+ }
+ emberPos.needsUpdate = true;
+ embers.material.opacity = _wild ? 0.74 : 0.0;
+
+ rose.rotation.z = realT * (_wild ? 0.9 : 0.08);
+ halo.scale.setScalar(1 + Math.sin(realT * (_wild ? 3.8 : 1.2)) * (_wild ? 0.18 : 0.03));
+ chandelier.rotation.z = Math.sin(realT * (_wild ? 1.6 : 0.24)) * (_wild ? 0.2 : 0.03);
+ chandelier.rotation.x = Math.sin(realT * (_wild ? 1.2 : 0.18)) * (_wild ? 0.12 : 0.02);
+
+ for (i = 0; i < beams.length; i++) beams[i].material.opacity = (_wild ? 0.18 : 0.08) + Math.sin(t * 0.8 + i) * 0.02;
+ for (i = 0; i < candles.length; i++) candles[i].intensity = (_wild ? 1.18 : 0.52) + Math.sin(realT * 5 + i) * 0.12;
+ for (i = 0; i < pipes.length; i++) pipes[i].scale.y = 1 + Math.sin(realT * (_wild ? 4.2 : 0.4) + i) * (_wild ? 0.18 : 0.015);
+
+ camera.position.x = Math.sin(realT * (_wild ? 1.4 : 0.16)) * (_wild ? 3.4 : 0.6);
+ camera.position.y = 10 + Math.sin(realT * 0.3) * (_wild ? 1.8 : 0.4);
+ camera.position.z = _wild ? 35 + Math.sin(realT * 0.6) * 4 : 40;
+ camera.lookAt(0, 5, -48);
+ renderer.render(scene, camera);
+ }
+
+ initThree();
+
+ function veil(css, ms) {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;' + css + ';transition:opacity ' + (ms || 240) + 'ms';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, ms || 240); }, 25);
+ }
+ window.snonuxOpenEffect = function() {
+ var modal = document.getElementById('post-modal');
+ if (modal) { modal.classList.add('sno-modal-zoom'); setTimeout(function() { modal.classList.remove('sno-modal-zoom'); }, 390); }
+ veil('background:radial-gradient(ellipse at center,rgba(224,196,127,0.18),rgba(123,194,255,0.08),transparent 70%)', 260);
+ };
+ window.snonuxCloseEffect = function() { veil('background:rgba(0,0,0,0.3)', 180); };
+ window.snonuxNavEffect = function() { veil('background:linear-gradient(90deg,transparent,rgba(123,194,255,0.12),rgba(224,196,127,0.08),transparent)', 190); };
+ window.snonuxPageEffect = function() { veil('background:radial-gradient(ellipse at center,rgba(142,47,73,0.22),rgba(224,196,127,0.12),transparent 72%)', 260); };
+ window.snonuxScrollEffect = function(dir) {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;' + (dir === 'down' ? 'top:0;' : 'bottom:0;') + 'left:0;right:0;height:' + (_wild ? '16px' : '6px') + ';z-index:9000;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(224,196,127,0.84),rgba(123,194,255,0.62),rgba(142,47,73,0.54),transparent);transition:transform 0.34s ease,opacity 0.34s ease;';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform = dir === 'down' ? 'translateY(100vh)' : 'translateY(-100vh)'; d.style.opacity='0'; }, 16);
+ setTimeout(function() { d.remove(); }, 400);
+ };
+ window.snonuxWildToggle = function() {
+ _wild = !_wild;
+ var b = document.getElementById('sno-wild-badge');
+ if (b) b.classList.toggle('sno-wild-on', _wild);
+ };
+ })();
+ </script>
+ {{template "navscript" .}}
+</body>
+</html>
diff --git a/internal/generator/templates/themes/noir.tmpl b/internal/generator/templates/themes/noir.tmpl
new file mode 100644
index 0000000..2d08979
--- /dev/null
+++ b/internal/generator/templates/themes/noir.tmpl
@@ -0,0 +1,308 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>snonux.foo // NOIR</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;700&family=Playfair+Display:wght@600;700&display=swap" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
+ <style>
+ :root { --fog:#0b0b0b; --ink:#d8d1c4; --silver:#a4a09a; --street:#161616; --lamp:#f0ead6; --blood:#a9372b; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'IBM Plex Mono','Courier New',monospace; background:#050505; color:var(--ink); overflow:hidden; height:100vh; }
+ body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none;
+ background:
+ radial-gradient(circle at 50% 50%, rgba(255,255,255,0.05), transparent 60%),
+ repeating-linear-gradient(0deg, rgba(255,255,255,0.015), rgba(255,255,255,0.015) 1px, transparent 1px, transparent 3px);
+ mix-blend-mode:screen; opacity:0.28; }
+ body::after { content:''; position:fixed; inset:0; z-index:998; pointer-events:none;
+ background:
+ linear-gradient(90deg, rgba(255,255,255,0.03), transparent 22%, transparent 78%, rgba(255,255,255,0.03)),
+ radial-gradient(circle at 50% 110%, rgba(255,255,255,0.06) 0%, transparent 35%);
+ mix-blend-mode:screen; opacity:0.42; }
+ #three-canvas { position:fixed; inset:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:16px 26px; background:rgba(5,5,5,0.82); backdrop-filter:blur(10px);
+ border-bottom:1px solid rgba(240,234,214,0.16); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-family:'Playfair Display',serif; font-size:2rem; color:var(--lamp); letter-spacing:0.04em; }
+ .logo-mark::after { content:'•'; color:var(--blood); margin-left:8px; text-shadow:0 0 10px rgba(169,55,43,0.7); }
+ .logo-title h1 { font-family:'Playfair Display',serif; font-size:1.6rem; letter-spacing:0.08em; color:var(--lamp); }
+ .logo-title .subtitle { font-size:0.72rem; color:rgba(216,209,196,0.58); margin-top:3px; }
+ .logo-title .subtitle a { color:var(--silver); text-decoration:none; }
+ .logo-title .subtitle a:hover { color:var(--lamp); }
+ .transmit-btn { border:1px solid rgba(240,234,214,0.25); color:var(--lamp); padding:9px 16px;
+ text-decoration:none; font-size:0.78rem; letter-spacing:0.26em; text-transform:uppercase;
+ transition:background 0.18s,color 0.18s,border-color 0.18s; }
+ .transmit-btn:hover { background:var(--lamp); color:#050505; border-color:var(--lamp); }
+ a.header-feed-link { color:var(--silver); }
+ a.header-feed-link:hover { color:var(--lamp); }
+ .nav-hints { background:rgba(7,7,7,0.72); border-bottom:1px solid rgba(240,234,214,0.08); color:rgba(216,209,196,0.4);
+ padding:5px 26px; display:flex; gap:18px; font-size:0.67rem; letter-spacing:0.08em; flex-wrap:wrap; }
+ .nav-hints kbd { background:#111; border:1px solid rgba(240,234,214,0.18); color:var(--lamp); padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:20px 26px; scrollbar-width:thin; scrollbar-color:#5a5a5a #121212; }
+ .page-nav { display:flex; justify-content:center; margin:14px 0; }
+ .page-nav a { border:1px solid rgba(240,234,214,0.18); color:var(--lamp); padding:8px 18px; text-decoration:none; font-size:0.78rem; letter-spacing:0.22em; text-transform:uppercase; }
+ .page-nav a:hover { background:rgba(240,234,214,0.08); }
+ .page-nav-footer { flex-shrink:0; padding:8px 26px; display:flex; justify-content:center;
+ background:rgba(5,5,5,0.82); backdrop-filter:blur(10px); border-top:1px solid rgba(240,234,214,0.16); }
+ .post { background:linear-gradient(180deg, rgba(16,16,16,0.94), rgba(8,8,8,0.92)); border:1px solid rgba(255,255,255,0.06);
+ padding:20px; margin-bottom:14px; cursor:pointer; box-shadow:0 10px 28px rgba(0,0,0,0.32); transition:border-color 0.2s,transform 0.2s,box-shadow 0.2s; }
+ .post:hover { border-color:rgba(240,234,214,0.18); transform:translateY(-1px); box-shadow:0 18px 34px rgba(0,0,0,0.42); }
+ .post-active { border-color:rgba(240,234,214,0.35) !important; background:linear-gradient(180deg, rgba(24,24,24,0.96), rgba(10,10,10,0.95)) !important;
+ box-shadow:0 0 0 1px rgba(240,234,214,0.12), 0 18px 38px rgba(0,0,0,0.5), inset 4px 0 0 var(--lamp) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:12px; font-size:0.84rem; }
+ .post-header strong { color:var(--lamp); }
+ .post-time { color:var(--silver); }
+ .post-text { line-height:1.72; font-size:0.92rem; color:var(--ink); }
+ .post-text a { color:var(--lamp); text-decoration:none; border-bottom:1px solid rgba(240,234,214,0.18); }
+ .post-text a:hover { border-color:rgba(240,234,214,0.55); }
+ .post-image { margin-top:10px; border:1px solid rgba(255,255,255,0.08); filter:grayscale(1) contrast(1.06); }
+ .post-audio { width:100%; margin-top:10px; filter:grayscale(1) contrast(0.9); }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100; overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:760px; margin:0 auto; background:rgba(10,10,10,0.98); border:1px solid rgba(240,234,214,0.22);
+ padding:38px; box-shadow:0 28px 80px rgba(0,0,0,0.72); }
+ .modal-close { float:right; background:none; border:none; color:var(--lamp); font-family:'IBM Plex Mono',monospace; font-size:0.82rem; cursor:pointer; letter-spacing:0.2em; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 16px;} .content{padding:14px 16px;} .modal-inner{padding:24px 16px;} }
+ .splash-overlay.splash-noir {
+ background:
+ radial-gradient(ellipse 40% 65% at 52% 24%, rgba(240,234,214,0.2) 0%, rgba(240,234,214,0.06) 26%, transparent 58%),
+ linear-gradient(180deg, #080808 0%, #020202 100%);
+ }
+ .splash-noir .splash-blinds { position:absolute; inset:0; background:repeating-linear-gradient(180deg, rgba(0,0,0,0.82) 0 22px, rgba(255,255,255,0.03) 22px 24px); opacity:0.34; z-index:1; }
+ .splash-noir .splash-city { position:absolute; left:0; right:0; bottom:0; height:28vh; z-index:1;
+ background:
+ linear-gradient(90deg, transparent 0 6%, #060606 6% 12%, transparent 12% 16%, #090909 16% 24%, transparent 24% 29%, #050505 29% 38%, transparent 38% 42%, #0a0a0a 42% 53%, transparent 53% 58%, #060606 58% 68%, transparent 68% 73%, #0a0a0a 73% 82%, transparent 82% 87%, #080808 87% 96%, transparent 96%),
+ linear-gradient(180deg, transparent, rgba(0,0,0,0.9));
+ opacity:0.86; }
+ .splash-noir .splash-sign { position:absolute; right:18%; top:22%; width:96px; height:28px; border:1px solid rgba(169,55,43,0.5); color:#ffd7d1; display:flex; align-items:center; justify-content:center;
+ font-size:0.62rem; letter-spacing:0.26em; text-transform:uppercase; background:rgba(169,55,43,0.14); box-shadow:0 0 16px rgba(169,55,43,0.34), inset 0 0 12px rgba(169,55,43,0.22); z-index:1;
+ animation:noirSignFlicker 2.7s steps(2) infinite; }
+ @keyframes noirSignFlicker { 0%,100%{opacity:0.92} 8%{opacity:0.25} 10%{opacity:0.96} 52%{opacity:0.62} 54%{opacity:0.95} }
+ .splash-noir .splash-title { font-family:'Playfair Display',serif; font-size:clamp(1.7rem,5vw,2.5rem); color:var(--lamp); letter-spacing:0.08em; }
+ .splash-noir .splash-tag { color:var(--silver); letter-spacing:0.24em; }
+ .splash-noir .splash-hint { color:rgba(216,209,196,0.78); }
+ .splash-noir .splash-inner { text-shadow:0 3px 22px rgba(0,0,0,0.95); }
+{{template "navSharedCSSInner"}}
+ </style>
+</head>
+<body>
+ {{template "splashGate"}}
+ <div id="splash-overlay" class="splash-overlay splash-noir" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
+ <canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
+ <div class="splash-blinds" aria-hidden="true"></div>
+ <div class="splash-city" aria-hidden="true"></div>
+ <div class="splash-sign" aria-hidden="true">Vacancy</div>
+ <div class="splash-inner">
+ <div class="splash-title">snonux.foo</div>
+ <div class="splash-tag">Midnight Edition</div>
+ <div class="splash-hint">Click or Enter to step under the streetlamp</div>
+ </div>
+ </div>
+ <script>
+ (function(){
+ if(document.documentElement.classList.contains('sno-splash-skip'))return;
+ var cv=document.getElementById('splash-gl-canvas');
+ if(!cv||typeof THREE==='undefined')return;
+ var raf,ren,sc,ca,clock,rain;
+ function cleanup(){window.removeEventListener('resize',sz);if(raf)cancelAnimationFrame(raf);raf=null;if(ren)ren.dispose();ren=null;window._snonuxSplashWebGLCleanup=null;}
+ window._snonuxSplashWebGLCleanup=cleanup;
+ function sz(){var w=cv.clientWidth||2,h=cv.clientHeight||2;if(ren)ren.setSize(w,h,false);if(ca){ca.aspect=w/h;ca.updateProjectionMatrix();}}
+ ren=new THREE.WebGLRenderer({canvas:cv,antialias:true,alpha:true});ren.setClearColor(0,0);ren.setPixelRatio(Math.min(window.devicePixelRatio||1,2));
+ sc=new THREE.Scene();ca=new THREE.PerspectiveCamera(50,1,0.1,80);ca.position.set(0,4,20);clock=new THREE.Clock();
+ var street=new THREE.Mesh(new THREE.PlaneGeometry(28,24),new THREE.MeshBasicMaterial({color:0x0d0d0d,transparent:true,opacity:0.92}));
+ street.rotation.x=-Math.PI/2;street.position.y=-3;sc.add(street);
+ for(var i=0;i<5;i++){ var b=new THREE.Mesh(new THREE.BoxGeometry(3+Math.random()*2,8+Math.random()*9,3+Math.random()*2),new THREE.MeshBasicMaterial({color:0x111111}));
+ b.position.set(-10+i*5,1.5+b.geometry.parameters.height*0.5,-10-Math.random()*8);sc.add(b);}
+ var lamp=new THREE.Mesh(new THREE.CylinderGeometry(0.08,0.12,8,6),new THREE.MeshBasicMaterial({color:0x3d3d3d})); lamp.position.set(0,1,-3); sc.add(lamp);
+ var glow=new THREE.Mesh(new THREE.SphereGeometry(0.5,12,12),new THREE.MeshBasicMaterial({color:0xf0ead6,transparent:true,opacity:0.85})); glow.position.set(0,5,-3); sc.add(glow);
+ var cone=new THREE.Mesh(new THREE.ConeGeometry(4.5,10,18,1,true),new THREE.MeshBasicMaterial({color:0xf0ead6,transparent:true,opacity:0.12,side:THREE.DoubleSide}));
+ cone.position.set(0,0,-3); cone.rotation.x=Math.PI; sc.add(cone);
+ var rp=new Float32Array(700*3); for(i=0;i<rp.length;i+=3){ rp[i]=(Math.random()-0.5)*24; rp[i+1]=Math.random()*20-2; rp[i+2]=(Math.random()-0.5)*20; }
+ var rg=new THREE.BufferGeometry(); rg.setAttribute('position',new THREE.BufferAttribute(rp,3));
+ rain=new THREE.Points(rg,new THREE.PointsMaterial({color:0xd8d1c4,size:0.08,transparent:true,opacity:0.38})); sc.add(rain);
+ sz(); window.addEventListener('resize',sz);
+ function loop(){ raf=requestAnimationFrame(loop); var t=clock.getElapsedTime(),pos=rain.geometry.attributes.position;
+ for(var i=0;i<pos.count;i++){ var y=pos.getY(i)-0.32; pos.setY(i,y<-3?18:y); }
+ pos.needsUpdate=true; glow.scale.setScalar(1+Math.sin(t*2.3)*0.05); cone.material.opacity=0.1+Math.sin(t*1.8)*0.03; ren.render(sc,ca); }
+ raf=requestAnimationFrame(loop);
+ })();
+ </script>
+ <canvas id="three-canvas"></canvas>
+ <div class="overlay">
+ <header>
+ <div class="logo">
+ <span class="logo-mark">SN</span>
+ <div class="logo-title">
+ <h1>snonux.foo</h1>
+ <p class="subtitle">microblog &mdash; <a href="https://foo.zone">foo.zone</a> is the real blog</p>
+ <p class="logo-host">Served by NetBSD on a Raspberry Pi 3</p>
+ </div>
+ </div>
+ <div class="nav">
+ <a href="atom.xml" class="header-feed-link" rel="alternate" title="Atom feed" type="application/atom+xml">Atom feed</a>
+ <a href="https://foo.zone/about" class="transmit-btn">Case File</a>
+ </div>
+ </header>
+ {{template "navhints" .}}
+ <div class="content" id="post-content">
+ {{range $i, $post := .Posts}}
+ <div class="post" id="post-{{$post.ID}}" data-index="{{$i}}">
+ <div class="post-header">
+ <div><strong>@snonux</strong></div>
+ <div class="post-time">{{$post.FormattedTime}}</div>
+ </div>
+ <div class="post-text">{{$post.ContentHTML}}</div>
+ </div>
+ {{end}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
+ <div class="page-nav page-nav-dual">
+ {{if .PrevPage}}<a href="{{.PrevPage}}">&larr; Newer</a>{{end}}
+ {{if .NextPage}}<a href="{{.NextPage}}">Older &rarr;</a>{{end}}
+ </div>
+ </footer>
+ {{end}}
+ </div>
+ {{template "navmodal" .}}
+ <script>
+ (function() {
+ var _wild = false, _snoTOffset = 0, _snoLastT = 0;
+ var scene, camera, renderer, clock, rain, leftSweep, rightSweep, street, buildings = [], fogPlanes = [], signPlane, lampHalo;
+
+ function initThree() {
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x050505);
+ scene.fog = new THREE.Fog(0x050505, 18, 120);
+ camera = new THREE.PerspectiveCamera(58, window.innerWidth/window.innerHeight, 0.1, 200);
+ camera.position.set(0, 10, 34);
+ renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ clock = new THREE.Clock();
+
+ street = new THREE.Mesh(new THREE.PlaneGeometry(90, 180, 1, 1), new THREE.MeshPhongMaterial({ color:0x111111, shininess:8 }));
+ street.rotation.x = -Math.PI/2; street.position.y = -2; scene.add(street);
+ var stripe = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 120), new THREE.MeshBasicMaterial({ color:0xf0ead6, transparent:true, opacity:0.08 }));
+ stripe.rotation.x = -Math.PI/2; stripe.position.set(0,-1.98,-20); scene.add(stripe);
+ scene.add(new THREE.AmbientLight(0x404040, 0.45));
+
+ var lampLight = new THREE.PointLight(0xf0ead6, 1.2, 70); lampLight.position.set(0, 18, -18); scene.add(lampLight);
+ lampHalo = new THREE.Mesh(new THREE.SphereGeometry(1.4, 16, 16), new THREE.MeshBasicMaterial({ color:0xf0ead6, transparent:true, opacity:0.12 }));
+ lampHalo.position.set(0,18,-18); scene.add(lampHalo);
+ leftSweep = new THREE.PointLight(0x223b88, 0.0, 60); leftSweep.position.set(-25, 8, -10); scene.add(leftSweep);
+ rightSweep = new THREE.PointLight(0x882222, 0.0, 60); rightSweep.position.set(25, 8, -10); scene.add(rightSweep);
+
+ for (var i = 0; i < 18; i++) {
+ var h = 10 + Math.random() * 28, w = 4 + Math.random() * 4, d = 4 + Math.random() * 6;
+ var b = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), new THREE.MeshPhongMaterial({ color:0x131313 }));
+ var side = i < 9 ? -1 : 1;
+ b.position.set(side * (14 + Math.random() * 22), h * 0.5 - 2, -80 + (i % 9) * 10);
+ scene.add(b); buildings.push(b);
+ }
+ signPlane = new THREE.Mesh(new THREE.PlaneGeometry(8, 2.4), new THREE.MeshBasicMaterial({ color:0xa9372b, transparent:true, opacity:0.34, side:THREE.DoubleSide }));
+ signPlane.position.set(18, 10, -32); signPlane.rotation.y = -0.42; scene.add(signPlane);
+ for (i = 0; i < 4; i++) {
+ var fog = new THREE.Mesh(new THREE.PlaneGeometry(40, 10), new THREE.MeshBasicMaterial({ color:0xffffff, transparent:true, opacity:0.03, side:THREE.DoubleSide, depthWrite:false }));
+ fog.position.set((Math.random()-0.5)*20, 1 + Math.random()*6, -50 + i * 16);
+ scene.add(fog); fogPlanes.push(fog);
+ }
+
+ var rp = new Float32Array(2600 * 3);
+ for (i = 0; i < rp.length; i += 3) {
+ rp[i] = (Math.random() - 0.5) * 80;
+ rp[i + 1] = Math.random() * 60;
+ rp[i + 2] = -90 + Math.random() * 120;
+ }
+ var rg = new THREE.BufferGeometry();
+ rg.setAttribute('position', new THREE.BufferAttribute(rp, 3));
+ rain = new THREE.Points(rg, new THREE.PointsMaterial({ color:0xd8d1c4, size:0.1, transparent:true, opacity:0.46 }));
+ scene.add(rain);
+
+ window.addEventListener('resize', onResize);
+ animate();
+ }
+
+ function onResize() {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ }
+
+ function animate() {
+ requestAnimationFrame(animate);
+ var realT = clock.getElapsedTime();
+ _snoTOffset += (realT - _snoLastT) * (_wild ? 8 : 0);
+ _snoLastT = realT;
+ var t = realT + _snoTOffset;
+
+ var pos = rain.geometry.attributes.position;
+ var speed = _wild ? 1.35 : 0.42;
+ for (var i = 0; i < pos.count; i++) {
+ var y = pos.getY(i) - speed;
+ if (y < -4) { y = 56; pos.setX(i, (Math.random() - 0.5) * 80); }
+ pos.setY(i, y);
+ if (_wild) pos.setX(i, pos.getX(i) + Math.sin(t * 0.9 + i) * 0.02);
+ }
+ pos.needsUpdate = true;
+ for (i = 0; i < fogPlanes.length; i++) {
+ fogPlanes[i].position.x = Math.sin(realT * (0.18 + i * 0.05)) * (_wild ? 12 : 4);
+ fogPlanes[i].material.opacity = (_wild ? 0.08 : 0.03) + Math.sin(realT * 0.7 + i) * 0.01;
+ }
+ signPlane.material.opacity = _wild ? (0.18 + Math.abs(Math.sin(realT * 8.2)) * 0.48) : (0.28 + Math.sin(realT * 1.4) * 0.06);
+ lampHalo.scale.setScalar(1 + Math.sin(realT * (_wild ? 6 : 2.2)) * (_wild ? 0.18 : 0.05));
+
+ leftSweep.intensity = _wild ? 1.4 + Math.sin(realT * 3.5) * 0.4 : 0;
+ rightSweep.intensity = _wild ? 1.2 + Math.cos(realT * 3.2) * 0.4 : 0;
+ leftSweep.position.x = -28 + Math.sin(realT * 1.7) * 10;
+ rightSweep.position.x = 28 + Math.cos(realT * 1.6) * 10;
+
+ camera.position.x = _wild ? Math.sin(realT * 1.9) * 2.2 : Math.sin(realT * 0.22) * 1.4;
+ camera.position.y = 10 + Math.sin(realT * 0.3) * (_wild ? 1.4 : 0.5);
+ camera.position.z = _wild ? 32 + Math.sin(realT * 0.7) * 4 : 34;
+ camera.lookAt(0, 6, -35);
+ renderer.render(scene, camera);
+ }
+
+ initThree();
+
+ function flash(css, ms) {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;' + css + ';transition:opacity ' + (ms || 220) + 'ms';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity = '0'; setTimeout(function() { d.remove(); }, ms || 220); }, 25);
+ }
+
+ window.snonuxOpenEffect = function(post) {
+ var modal = document.getElementById('post-modal');
+ if (modal) { modal.classList.add('sno-modal-fly'); setTimeout(function() { modal.classList.remove('sno-modal-fly'); }, 360); }
+ var r = post ? post.getBoundingClientRect() : { left: innerWidth * 0.5, top: innerHeight * 0.5, width: 0, height: 0 };
+ var s = document.createElement('div');
+ s.style.cssText = 'position:fixed;left:' + (r.left + r.width * 0.5 - 12) + 'px;top:' + (r.top + r.height * 0.5 - 12) + 'px;width:24px;height:24px;border-radius:50%;z-index:997;pointer-events:none;background:radial-gradient(circle,rgba(240,234,214,0.88),rgba(240,234,214,0.18) 55%,transparent 72%);transition:transform 0.4s ease,opacity 0.4s ease;';
+ document.body.appendChild(s);
+ setTimeout(function() { s.style.transform='scale(18)'; s.style.opacity='0'; setTimeout(function() { s.remove(); }, 420); }, 18);
+ };
+ window.snonuxCloseEffect = function() { flash('background:rgba(0,0,0,0.45)', 180); };
+ window.snonuxNavEffect = function() { flash('background:repeating-linear-gradient(90deg,rgba(0,0,0,0.8) 0 12%,rgba(240,234,214,0.06) 12% 14%,rgba(0,0,0,0.8) 14% 24%)', 170); };
+ window.snonuxPageEffect = function() { flash('background:linear-gradient(90deg,rgba(36,65,130,0.14),transparent 38%,rgba(169,55,43,0.18) 62%,transparent)', 240); };
+ window.snonuxScrollEffect = function(dir) {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;' + (dir === 'down' ? 'top:0;' : 'bottom:0;') + 'left:0;right:0;height:' + (_wild ? '18px' : '6px') + ';z-index:9000;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(240,234,214,0.8),transparent);transition:transform 0.28s ease,opacity 0.28s ease;';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform = dir === 'down' ? 'translateY(100vh)' : 'translateY(-100vh)'; d.style.opacity='0'; }, 16);
+ setTimeout(function() { d.remove(); }, 340);
+ };
+ window.snonuxWildToggle = function() {
+ _wild = !_wild;
+ var b = document.getElementById('sno-wild-badge');
+ if (b) b.classList.toggle('sno-wild-on', _wild);
+ };
+ })();
+ </script>
+ {{template "navscript" .}}
+</body>
+</html>
diff --git a/internal/generator/templates/themes/surveillance.tmpl b/internal/generator/templates/themes/surveillance.tmpl
new file mode 100644
index 0000000..5d0df8d
--- /dev/null
+++ b/internal/generator/templates/themes/surveillance.tmpl
@@ -0,0 +1,224 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>snonux.foo // SURVEILLANCE</title>
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
+ <style>
+ :root { --phosphor:#bcffd4; --green:#63f3a8; --grey:#88a197; --alert:#ff4d5c; --bg:#09100d; --panel:#101916; }
+ * { margin:0; padding:0; box-sizing:border-box; }
+ body { font-family:'Share Tech Mono','Courier New',monospace; background:var(--bg); color:var(--phosphor); overflow:hidden; height:100vh; }
+ body::before { content:''; position:fixed; inset:0; z-index:999; pointer-events:none; background:repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(188,255,212,0.035) 2px, rgba(188,255,212,0.035) 3px); opacity:0.72; }
+ #three-canvas { position:fixed; inset:0; width:100%; height:100%; z-index:1; }
+ .overlay { position:relative; z-index:10; height:100vh; display:flex; flex-direction:column; }
+ header { padding:14px 24px; background:rgba(9,16,13,0.84); backdrop-filter:blur(8px); border-bottom:1px solid rgba(99,243,168,0.18); display:flex; align-items:center; justify-content:space-between; }
+ .logo { display:flex; align-items:center; gap:14px; }
+ .logo-mark { font-size:1.55rem; color:var(--green); }
+ .logo-title h1 { font-size:1.15rem; color:var(--green); letter-spacing:0.24em; }
+ .logo-title .subtitle { font-size:0.72rem; color:rgba(188,255,212,0.5); margin-top:2px; }
+ .logo-title .subtitle a { color:var(--grey); text-decoration:none; }
+ .logo-title .subtitle a:hover { color:var(--green); }
+ .transmit-btn { border:1px solid rgba(99,243,168,0.22); color:var(--green); padding:8px 14px; text-decoration:none; font-size:0.76rem; letter-spacing:0.24em; text-transform:uppercase; transition:all 0.18s; }
+ .transmit-btn:hover { background:rgba(99,243,168,0.12); }
+ a.header-feed-link { color:var(--grey); }
+ a.header-feed-link:hover { color:var(--green); }
+ .nav-hints { background:rgba(10,18,14,0.74); border-bottom:1px solid rgba(99,243,168,0.08); color:rgba(188,255,212,0.42); padding:5px 24px; display:flex; gap:18px; font-size:0.66rem; flex-wrap:wrap; }
+ .nav-hints kbd { background:#0c1511; border:1px solid rgba(99,243,168,0.2); color:var(--green); padding:0 5px; margin:0 2px; }
+ .content { flex:1; overflow-y:auto; padding:18px 24px; scrollbar-width:thin; scrollbar-color:#4b8d68 #0d1512; }
+ .page-nav { display:flex; justify-content:center; margin:12px 0; }
+ .page-nav a { border:1px solid rgba(99,243,168,0.2); color:var(--green); padding:7px 16px; text-decoration:none; font-size:0.76rem; letter-spacing:0.22em; text-transform:uppercase; }
+ .page-nav a:hover { background:rgba(99,243,168,0.08); }
+ .page-nav-footer { flex-shrink:0; padding:8px 24px; display:flex; justify-content:center; background:rgba(9,16,13,0.84); backdrop-filter:blur(8px); border-top:1px solid rgba(99,243,168,0.18); }
+ .post { background:linear-gradient(180deg, rgba(16,25,22,0.92), rgba(9,15,12,0.92)); border:1px solid rgba(99,243,168,0.08); padding:18px; margin-bottom:12px; cursor:pointer; position:relative; transition:border-color 0.18s, box-shadow 0.18s; }
+ .post::after { content:''; position:absolute; inset:8px; border:1px solid rgba(99,243,168,0.06); pointer-events:none; }
+ .post:hover { border-color:rgba(99,243,168,0.22); box-shadow:0 0 18px rgba(99,243,168,0.1); }
+ .post-active { border-color:rgba(99,243,168,0.34) !important; background:linear-gradient(180deg, rgba(10,25,18,0.96), rgba(7,14,10,0.95)) !important;
+ box-shadow:0 0 0 1px rgba(99,243,168,0.1), 0 16px 34px rgba(0,0,0,0.32), inset 4px 0 0 var(--green) !important; }
+ .post-header { display:flex; justify-content:space-between; margin-bottom:10px; font-size:0.8rem; }
+ .post-header strong, .post-time { color:var(--green); }
+ .post-text { line-height:1.68; font-size:0.9rem; color:var(--phosphor); }
+ .post-text a { color:var(--green); text-decoration:none; border-bottom:1px solid rgba(99,243,168,0.18); }
+ .post-image { margin-top:10px; border:1px solid rgba(99,243,168,0.1); filter:saturate(0.6) contrast(1.12) hue-rotate(-16deg); }
+ .post-audio { width:100%; margin-top:10px; filter:grayscale(0.7); }
+ .post-modal { display:none; position:fixed; inset:0; z-index:100; overflow-y:auto; padding:40px 20px; }
+ .post-modal.active { display:block; }
+ .modal-inner { max-width:760px; margin:0 auto; background:rgba(8,14,11,0.98); border:1px solid rgba(99,243,168,0.2); padding:34px; box-shadow:0 20px 72px rgba(0,0,0,0.72); }
+ .modal-close { float:right; background:none; border:none; color:var(--green); font-family:'Share Tech Mono',monospace; font-size:0.76rem; cursor:pointer; letter-spacing:0.2em; }
+ @media(max-width:640px) { .nav-hints{display:none;} header{padding:12px 16px;} .content{padding:14px 16px;} .modal-inner{padding:24px 16px;} }
+ .splash-overlay.splash-surveillance {
+ background:
+ radial-gradient(circle at 50% 22%, rgba(99,243,168,0.12) 0%, transparent 34%),
+ linear-gradient(180deg, #09100d 0%, #050907 100%);
+ }
+ .splash-surveillance .splash-grid { position:absolute; inset:0; background:linear-gradient(rgba(99,243,168,0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(99,243,168,0.06) 1px, transparent 1px); background-size:40px 40px; opacity:0.28; }
+ .splash-surveillance .splash-title { font-size:clamp(1.45rem,4.8vw,2rem); color:var(--green); letter-spacing:0.28em; }
+ .splash-surveillance .splash-tag { color:var(--grey); letter-spacing:0.22em; }
+ .splash-surveillance .splash-hint { color:rgba(188,255,212,0.76); }
+ .splash-surveillance .splash-inner { text-shadow:0 0 18px rgba(99,243,168,0.28); }
+{{template "navSharedCSSInner"}}
+ </style>
+</head>
+<body>
+ {{template "splashGate"}}
+ <div id="splash-overlay" class="splash-overlay splash-surveillance" role="dialog" aria-modal="true" aria-label="Open microblog" tabindex="-1">
+ <canvas class="splash-gl-canvas" id="splash-gl-canvas" aria-hidden="true"></canvas>
+ <div class="splash-grid" aria-hidden="true"></div>
+ <div class="splash-inner">
+ <div class="splash-title">snonux.foo</div>
+ <div class="splash-tag">Camera Mesh Online</div>
+ <div class="splash-hint">Click or Enter to access the feed</div>
+ </div>
+ </div>
+ <script>
+ (function(){
+ if(document.documentElement.classList.contains('sno-splash-skip'))return;
+ var cv=document.getElementById('splash-gl-canvas');
+ if(!cv||typeof THREE==='undefined')return;
+ var raf,ren,sc,ca,clock,rings=[];
+ function cleanup(){window.removeEventListener('resize',sz);if(raf)cancelAnimationFrame(raf);raf=null;if(ren)ren.dispose();ren=null;window._snonuxSplashWebGLCleanup=null;}
+ window._snonuxSplashWebGLCleanup=cleanup;
+ function sz(){var w=cv.clientWidth||2,h=cv.clientHeight||2;if(ren)ren.setSize(w,h,false);if(ca){ca.aspect=w/h;ca.updateProjectionMatrix();}}
+ ren=new THREE.WebGLRenderer({canvas:cv,antialias:true,alpha:true});ren.setClearColor(0,0);ren.setPixelRatio(Math.min(window.devicePixelRatio||1,2));
+ sc=new THREE.Scene();ca=new THREE.PerspectiveCamera(50,1,0.1,60);ca.position.z=10;clock=new THREE.Clock();
+ for(var i=0;i<3;i++){ var r=new THREE.Mesh(new THREE.TorusGeometry(1.4+i*0.5,0.04,8,48),new THREE.MeshBasicMaterial({color:0x63f3a8,transparent:true,opacity:0.68-i*0.1})); sc.add(r); rings.push(r);}
+ var iris=new THREE.Mesh(new THREE.CircleGeometry(0.4,24),new THREE.MeshBasicMaterial({color:0xbcffd4,transparent:true,opacity:0.8})); sc.add(iris);
+ sz();window.addEventListener('resize',sz);
+ function loop(){ raf=requestAnimationFrame(loop); var t=clock.getElapsedTime(); for(var i=0;i<rings.length;i++){ rings[i].rotation.z=t*(0.4+i*0.3); rings[i].scale.setScalar(1+Math.sin(t*2+i)*0.03); } ren.render(sc,ca); }
+ raf=requestAnimationFrame(loop);
+ })();
+ </script>
+ <canvas id="three-canvas"></canvas>
+ <div class="overlay">
+ <header>
+ <div class="logo">
+ <span class="logo-mark">SN</span>
+ <div class="logo-title">
+ <h1>snonux.foo</h1>
+ <p class="subtitle">microblog &mdash; <a href="https://foo.zone">foo.zone</a> is the real blog</p>
+ <p class="logo-host">Served by NetBSD on a Raspberry Pi 3</p>
+ </div>
+ </div>
+ <div class="nav">
+ <a href="atom.xml" class="header-feed-link" rel="alternate" title="Atom feed" type="application/atom+xml">Atom feed</a>
+ <a href="https://foo.zone/about" class="transmit-btn">Operator</a>
+ </div>
+ </header>
+ {{template "navhints" .}}
+ <div class="content" id="post-content">
+ {{range $i, $post := .Posts}}
+ <div class="post" id="post-{{$post.ID}}" data-index="{{$i}}">
+ <div class="post-header">
+ <div><strong>@snonux</strong></div>
+ <div class="post-time">{{$post.FormattedTime}}</div>
+ </div>
+ <div class="post-text">{{$post.ContentHTML}}</div>
+ </div>
+ {{end}}
+ </div>
+ {{if or .PrevPage .NextPage}}
+ <footer class="page-nav-footer" aria-label="Pagination">
+ <div class="page-nav page-nav-dual">
+ {{if .PrevPage}}<a href="{{.PrevPage}}">&larr; Newer</a>{{end}}
+ {{if .NextPage}}<a href="{{.NextPage}}">Older &rarr;</a>{{end}}
+ </div>
+ </footer>
+ {{end}}
+ </div>
+ {{template "navmodal" .}}
+ <script>
+ (function() {
+ var _wild = false, _snoTOffset = 0, _snoLastT = 0;
+ var scene, camera, renderer, clock, nodes = [], trackers = [], rain;
+
+ function initThree() {
+ scene = new THREE.Scene();
+ scene.background = new THREE.Color(0x09100d);
+ scene.fog = new THREE.Fog(0x09100d, 20, 120);
+ camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 200);
+ camera.position.set(0, 6, 28);
+ renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('three-canvas'), antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ clock = new THREE.Clock();
+ scene.add(new THREE.AmbientLight(0x35634f, 0.55));
+
+ for (var i = 0; i < 12; i++) {
+ var s = new THREE.Mesh(new THREE.PlaneGeometry(7, 4.2), new THREE.MeshBasicMaterial({ color:0x15221c, transparent:true, opacity:0.92, side:THREE.DoubleSide }));
+ s.position.set((i % 4 - 1.5) * 11, 8 - Math.floor(i / 4) * 6, -10 - Math.floor(i / 4) * 8);
+ scene.add(s); nodes.push(s);
+ var box = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.PlaneGeometry(6.4, 3.6)), new THREE.LineBasicMaterial({ color:0x63f3a8, transparent:true, opacity:0.5 }));
+ box.position.copy(s.position); box.position.z += 0.04; scene.add(box); trackers.push(box);
+ }
+
+ var rp = new Float32Array(1500 * 3);
+ for (i = 0; i < rp.length; i += 3) { rp[i]=(Math.random()-0.5)*70; rp[i+1]=(Math.random()-0.5)*40; rp[i+2]=-80+Math.random()*90; }
+ var rg = new THREE.BufferGeometry(); rg.setAttribute('position', new THREE.BufferAttribute(rp, 3));
+ rain = new THREE.Points(rg, new THREE.PointsMaterial({ color:0xbcffd4, size:0.08, transparent:true, opacity:0.2 }));
+ scene.add(rain);
+ window.addEventListener('resize', onResize);
+ animate();
+ }
+
+ function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
+
+ function animate() {
+ requestAnimationFrame(animate);
+ var realT = clock.getElapsedTime();
+ _snoTOffset += (realT - _snoLastT) * (_wild ? 9 : 0);
+ _snoLastT = realT;
+ var t = realT + _snoTOffset;
+ for (var i = 0; i < nodes.length; i++) {
+ nodes[i].material.opacity = (_wild ? 0.58 : 0.9) - ((i % 4) * 0.06);
+ trackers[i].rotation.z = Math.sin(t * 0.7 + i) * (_wild ? 0.12 : 0.03);
+ trackers[i].material.opacity = _wild ? 0.84 : 0.5;
+ }
+ var pos = rain.geometry.attributes.position;
+ for (i = 0; i < pos.count; i++) { var y = pos.getY(i) - (_wild ? 0.22 : 0.08); pos.setY(i, y < -20 ? 20 : y); }
+ pos.needsUpdate = true;
+ camera.position.x = Math.sin(realT * (_wild ? 1.6 : 0.3)) * (_wild ? 2.8 : 0.7);
+ camera.position.y = 6 + Math.cos(realT * 0.4) * (_wild ? 1.1 : 0.3);
+ camera.lookAt(0, 0, -20);
+ renderer.render(scene, camera);
+ }
+
+ initThree();
+
+ function overlay(css, ms) {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;inset:0;z-index:998;pointer-events:none;' + css + ';transition:opacity ' + (ms || 200) + 'ms';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.opacity='0'; setTimeout(function() { d.remove(); }, ms || 200); }, 25);
+ }
+ window.snonuxOpenEffect = function(post) {
+ var modal = document.getElementById('post-modal');
+ if (modal) { modal.classList.add('sno-modal-slide'); setTimeout(function() { modal.classList.remove('sno-modal-slide'); }, 340); }
+ var r = post ? post.getBoundingClientRect() : { left: innerWidth*0.5, top: innerHeight*0.5, width: 0, height: 0 };
+ var box = document.createElement('div');
+ box.style.cssText = 'position:fixed;left:' + (r.left-6) + 'px;top:' + (r.top-6) + 'px;width:' + (r.width+12) + 'px;height:' + (r.height+12) + 'px;border:1px solid rgba(99,243,168,0.8);z-index:997;pointer-events:none;transition:transform 0.32s ease,opacity 0.32s ease;';
+ document.body.appendChild(box);
+ setTimeout(function() { box.style.transform='scale(1.18)'; box.style.opacity='0'; setTimeout(function() { box.remove(); }, 360); }, 18);
+ };
+ window.snonuxCloseEffect = function() { overlay('background:rgba(0,0,0,0.32)', 160); };
+ window.snonuxNavEffect = function() { overlay('background:linear-gradient(90deg,transparent,rgba(99,243,168,0.08),transparent)', 160); };
+ window.snonuxPageEffect = function() { overlay('background:radial-gradient(circle at center,rgba(255,77,92,0.12),transparent 68%)', 220); };
+ window.snonuxScrollEffect = function(dir) {
+ var d = document.createElement('div');
+ d.style.cssText = 'position:fixed;' + (dir === 'down' ? 'top:0;' : 'bottom:0;') + 'left:0;right:0;height:' + (_wild ? '16px' : '6px') + ';z-index:9000;pointer-events:none;background:linear-gradient(90deg,transparent,rgba(99,243,168,0.82),transparent);transition:transform 0.28s ease,opacity 0.28s ease;';
+ document.body.appendChild(d);
+ setTimeout(function() { d.style.transform = dir === 'down' ? 'translateY(100vh)' : 'translateY(-100vh)'; d.style.opacity='0'; }, 16);
+ setTimeout(function() { d.remove(); }, 340);
+ };
+ window.snonuxWildToggle = function() {
+ _wild = !_wild;
+ var b = document.getElementById('sno-wild-badge');
+ if (b) b.classList.toggle('sno-wild-on', _wild);
+ };
+ })();
+ </script>
+ {{template "navscript" .}}
+</body>
+</html>